Skip to content

optora.dro.phi_dro

Phi-divergence-constrained ambiguity sets (phi-DRO).

PhiAmbiguitySet

Bases: DualAmbiguitySet

Phi-divergence-constrained ambiguity set solved via its convex dual.

Bounds every candidate distribution q by \(D_\phi(q \,\|\, \mathrm{nominal}) \le \mathrm{radius}\) for a general convex generator \(\phi\) (see optora.divergences.f_divergence.PhiDivergence). The worst-case expected loss over this set admits a convex dual (Ben-Tal et al. 2013; Duchi, Glynn, and Namkoong 2021; Duchi and Namkoong 2021):

\[ \sup_{q:\, D_\phi(q \,\|\, \mathrm{nominal}) \,\le\, \mathrm{radius}} \mathbb{E}_q[\mathrm{loss}] = \inf_{\substack{\eta > 0 \\ \lambda \in \mathbb{R}}} \eta \cdot \mathrm{radius} + \lambda + \eta \, \mathbb{E}_{\mathrm{nominal}}\!\left[ \phi^*\!\left(\frac{\mathrm{loss} - \lambda}{\eta}\right) \right] \]

where \(\phi^*\) is the convex (Legendre-Fenchel) conjugate of \(\phi\) restricted to its effective domain \(t \ge 0\). This generalizes the KLAmbiguitySet dual to an arbitrary phi-divergence at the cost of a second dual variable \(\lambda\); setting \(\phi(t) = t \log t - t + 1\) (whose conjugate is \(\phi^*(s) = \exp(s) - 1\)) recovers the KL-DRO dual exactly. dual_solver minimizes this joint objective over \((\log(\eta), \lambda)\) rather than \((\eta, \lambda)\) directly, so the unconstrained GradientDescent solver keeps \(\eta\) strictly positive throughout the iteration.

This base class assumes phi_conjugate is finite everywhere on the real line (true for, for example, the chi-square generator's conjugate used by ChiSquareAmbiguitySet). Phi-divergences whose conjugate has a hard finite feasibility boundary (for example total variation, whose conjugate is +inf past a threshold) are not solved robustly by this unconstrained joint dual, since gradient descent can step past the boundary into a region of infinite objective value; TotalVariationAmbiguitySet instead computes its worst-case expectation from a dedicated closed form.

Attributes:

Name Type Description
nominal Tensor

Reference distribution the ambiguity set is centered on.

divergence

PhiDivergence instance measuring distance from nominal.

radius float | Tensor

Nonnegative bound on the phi-divergence of any distribution inside the ambiguity set from nominal, either a float or a tensor of radii evaluated as one batch.

phi_conjugate

Convex (Legendre-Fenchel) conjugate of divergence.phi, finite everywhere on the real line.

dual_solver

Solver minimizing the dual objective over (log(eta), lam).

initial_dual_point Tensor

Values of (log(eta), lam) the first dual solve starts from; later solves warm-start from the previous optimum (see optora.core.dro_base.DualAmbiguitySet).

__init__(nominal, divergence, radius, phi_conjugate, dual_solver=None, initial_log_eta=0.0, initial_lam=0.0, validate=False)

Initialize the phi-divergence ambiguity set.

Parameters:

Name Type Description Default
nominal Tensor

Reference distribution the ambiguity set is centered on, a nonnegative tensor that sums to one along its last dimension.

required
divergence PhiDivergence

PhiDivergence instance measuring distance from nominal.

required
radius float | Tensor

Nonnegative bound on the phi-divergence of any distribution inside the ambiguity set from nominal. A tensor radius is broadcast against the batch shape of worst_case_expectation's loss.

required
phi_conjugate Callable[[Tensor], Tensor]

Convex conjugate of divergence.phi, finite everywhere on the real line.

required
dual_solver Solver[MinimizationProblem, MinimizationResult] | None

Solver minimizing the dual objective over (log(eta), lam). Required when evaluating a positive-radius set.

None
initial_log_eta float

Value of log(eta) the first dual solve starts from. Later calls warm-start from the previous solve's optimum unless reset_warm_start() is called.

0.0
initial_lam float

Value of lam the first dual solve starts from, warm-started on later calls alongside initial_log_eta.

0.0
validate bool

Whether to check that nominal is nonnegative and sums to one. The check synchronizes with the device, so it is opt-in and off by default.

False

Raises:

Type Description
ValueError

If radius is a negative float, or if validate is set and nominal is not a valid probability distribution.

worst_case_expectation(loss)

Compute the worst-case expected loss over the phi-divergence ambiguity set.

Parameters:

Name Type Description Default
loss Tensor

Per-scenario loss values of shape (..., n), one trailing entry per element of nominal's support. Leading dimensions are a batch of independent loss vectors, each solved with its own dual pair (log(eta), lam) in a single joint solve.

required

Returns:

Type Description
Tensor

A tensor of shape (...) holding the worst-case expected loss:

Tensor

the exact sum(nominal * loss) when radius is zero (the

Tensor

ambiguity set then contains only nominal), otherwise the

Tensor

convex dual objective evaluated at the (log(eta), lam) found

Tensor

by dual_solver.

Raises:

Type Description
ValueError

If loss's trailing dimension does not match nominal's support size, or its batch shape does not broadcast against nominal and radius.

RuntimeError

If radius is positive and dual_solver is None.

contains(candidate)

Check whether a candidate distribution lies inside the ambiguity set.

The answer is returned as a boolean tensor on candidate's device rather than as a Python bool, so membership can be used as a mask or composed with further tensor work without forcing a device-to-host synchronization. Call bool(...) on the result only where a host-side branch is genuinely needed.

Parameters:

Name Type Description Default
candidate Tensor

Candidate distribution with the same shape as nominal.

required

Returns:

Type Description
Tensor

A boolean tensor that is True where the divergence of

Tensor

candidate from nominal does not exceed radius.

reset_warm_start()

Discard the cached dual optimum so the next solve starts cold.

After this call the next worst_case_expectation starts from initial_dual_point again, as the first one did.

ChiSquareAmbiguitySet

Bases: PhiAmbiguitySet

Chi-square-divergence-constrained ambiguity set for chi-square-DRO.

Fixes divergence to a ChiSquareDivergence and phi_conjugate to the closed-form conjugate of \(\phi(t) = (t-1)^2\), which is finite and continuously differentiable everywhere on the real line (see _chi_square_conjugate), making the joint PhiAmbiguitySet dual solve over \((\log(\eta), \lambda)\) numerically well-behaved.

In the interior regime where no candidate distribution is pushed to the boundary \(q_i = 0\), the dual optimum over \(\lambda\) reduces to \(\lambda = \mathbb{E}_{\mathrm{nominal}}[\mathrm{loss}]\), and the dual optimum over \(\eta\) reduces to \(\eta = \sqrt{\mathrm{Var}_{\mathrm{nominal}}(\mathrm{loss}) / (4\,\mathrm{radius})}\), giving the well-known closed form (Duchi and Namkoong 2021):

\[ \sup_{q:\, D_{\chi^2}(q \,\|\, \mathrm{nominal}) \,\le\, \mathrm{radius}} \mathbb{E}_q[\mathrm{loss}] = \mathbb{E}_{\mathrm{nominal}}[\mathrm{loss}] + \sqrt{\mathrm{radius} \cdot \mathrm{Var}_{\mathrm{nominal}}(\mathrm{loss})} \]

worst_case_expectation still solves the general dual rather than this closed form directly, since the closed form only holds away from the boundary regime.

__init__(nominal, radius, eps=1e-12, dual_solver=None, initial_log_eta=0.0, initial_lam=0.0, validate=False)

Initialize the chi-square ambiguity set.

Parameters:

Name Type Description Default
nominal Tensor

Reference distribution the ambiguity set is centered on, a nonnegative tensor that sums to one along its last dimension.

required
radius float | Tensor

Nonnegative bound on the chi-square divergence of any distribution inside the ambiguity set from nominal. A tensor radius is broadcast against the batch shape of worst_case_expectation's loss.

required
eps float

Small positive constant used to clamp nominal away from zero before dividing, passed through to the underlying ChiSquareDivergence.

1e-12
dual_solver Solver[MinimizationProblem, MinimizationResult] | None

Solver minimizing the dual objective over (log(eta), lam). Required when evaluating a positive-radius set.

None
initial_log_eta float

Value of log(eta) the first dual solve starts from. Later calls warm-start from the previous solve's optimum unless reset_warm_start() is called.

0.0
initial_lam float

Value of lam the first dual solve starts from, warm-started on later calls alongside initial_log_eta.

0.0
validate bool

Whether to check that nominal is nonnegative and sums to one. The check synchronizes with the device, so it is opt-in and off by default.

False

Raises:

Type Description
ValueError

If radius is a negative float, if eps is not positive, or if validate is set and nominal is not a valid probability distribution.

contains(candidate)

Check whether a candidate distribution lies inside the ambiguity set.

The answer is returned as a boolean tensor on candidate's device rather than as a Python bool, so membership can be used as a mask or composed with further tensor work without forcing a device-to-host synchronization. Call bool(...) on the result only where a host-side branch is genuinely needed.

Parameters:

Name Type Description Default
candidate Tensor

Candidate distribution with the same shape as nominal.

required

Returns:

Type Description
Tensor

A boolean tensor that is True where the divergence of

Tensor

candidate from nominal does not exceed radius.

worst_case_expectation(loss)

Compute the worst-case expected loss over the phi-divergence ambiguity set.

Parameters:

Name Type Description Default
loss Tensor

Per-scenario loss values of shape (..., n), one trailing entry per element of nominal's support. Leading dimensions are a batch of independent loss vectors, each solved with its own dual pair (log(eta), lam) in a single joint solve.

required

Returns:

Type Description
Tensor

A tensor of shape (...) holding the worst-case expected loss:

Tensor

the exact sum(nominal * loss) when radius is zero (the

Tensor

ambiguity set then contains only nominal), otherwise the

Tensor

convex dual objective evaluated at the (log(eta), lam) found

Tensor

by dual_solver.

Raises:

Type Description
ValueError

If loss's trailing dimension does not match nominal's support size, or its batch shape does not broadcast against nominal and radius.

RuntimeError

If radius is positive and dual_solver is None.

reset_warm_start()

Discard the cached dual optimum so the next solve starts cold.

After this call the next worst_case_expectation starts from initial_dual_point again, as the first one did.

TotalVariationAmbiguitySet

Bases: AmbiguitySet

Total-variation-constrained ambiguity set for total-variation-DRO.

Bounds every candidate distribution q by

\[ D_{\mathrm{TV}}(q \,\|\, \mathrm{nominal}) = \frac{1}{2} \sum_i |q_i - \mathrm{nominal}_i| \le \mathrm{radius}. \]

Unlike PhiAmbiguitySet, worst_case_expectation is computed from a direct closed form rather than the general convex dual, because total variation's conjugate has a hard finite feasibility boundary that is not well suited to unconstrained gradient-based dual optimization (see PhiAmbiguitySet).

The worst-case expectation is instead the value of a linear program over the simplex intersected with the total-variation ball, whose optimal solution has a simple combinatorial structure (Ben-Tal et al. 2013): starting from nominal, reallocate mass, in ascending order of loss, from the lowest-loss scenarios to the single highest-loss scenario, until the reallocated mass reaches radius (or every scenario but the highest-loss one has been fully drained, whichever happens first). This is implemented as a sort followed by a cumulative-sum sweep rather than an iterative solve, so it is both exact and free of solver tuning.

Attributes:

Name Type Description
nominal Tensor

Reference distribution the ambiguity set is centered on.

divergence

TotalVariationDivergence instance measuring distance from nominal.

radius float | Tensor

Nonnegative bound on the total variation distance of any distribution inside the ambiguity set from nominal, either a float or a tensor of radii evaluated as one batch.

__init__(nominal, radius, eps=1e-12, validate=False)

Initialize the total variation ambiguity set.

Parameters:

Name Type Description Default
nominal Tensor

Reference distribution the ambiguity set is centered on, a nonnegative tensor that sums to one along its last dimension.

required
radius float | Tensor

Nonnegative bound on the total variation distance of any distribution inside the ambiguity set from nominal. A tensor radius is broadcast against the batch shape of worst_case_expectation's loss, which turns a radius sweep into one vectorized evaluation of the closed form.

required
eps float

Small positive constant used to clamp the reference distribution away from zero before dividing, passed through to the underlying TotalVariationDivergence.

1e-12
validate bool

Whether to check that nominal is nonnegative and sums to one. The check synchronizes with the device, so it is opt-in and off by default.

False

Raises:

Type Description
ValueError

If radius is a negative float, if eps is not positive, or if validate is set and nominal is not a valid probability distribution.

worst_case_expectation(loss)

Compute the worst-case expected loss over the total variation ambiguity set.

Parameters:

Name Type Description Default
loss Tensor

Per-scenario loss values of shape (..., n), one trailing entry per element of nominal's support. Leading dimensions are a batch of independent loss vectors; the closed form is evaluated over the whole batch at once, with no solver and no Python-level loop over batch elements.

required

Returns:

Type Description
Tensor

A tensor of shape (...) holding the worst-case expected loss:

Tensor

the exact sum(nominal * loss) when radius is zero (the

Tensor

ambiguity set then contains only nominal), otherwise the

Tensor

closed-form value of the mass-reallocation linear program

Tensor

described in the class docstring.

Raises:

Type Description
ValueError

If loss's trailing dimension does not match nominal's support size, or its batch shape does not broadcast against nominal and radius.

contains(candidate)

Check whether a candidate distribution lies inside the ambiguity set.

The answer is returned as a boolean tensor on candidate's device rather than as a Python bool, so membership can be used as a mask or composed with further tensor work without forcing a device-to-host synchronization. Call bool(...) on the result only where a host-side branch is genuinely needed.

Parameters:

Name Type Description Default
candidate Tensor

Candidate distribution with the same shape as nominal.

required

Returns:

Type Description
Tensor

A boolean tensor that is True where the divergence of

Tensor

candidate from nominal does not exceed radius.