Skip to content

optora.dro.wasserstein_dro

Wasserstein-distance-constrained ambiguity set (Wasserstein-DRO).

WassersteinAmbiguitySet

Bases: AmbiguitySet

Wasserstein-distance-constrained ambiguity set for Wasserstein-DRO.

Bounds every candidate distribution q, sharing nominal's finite support with pairwise ground cost cost, by the type-1 Wasserstein distance \(W_c(q, \mathrm{nominal}) \le \mathrm{radius}\), where \(W_c\) is the optimal-transport cost of moving nominal to q under cost. The worst-case expected loss over this set admits an exact strong-duality reformulation (Mohajerin Esfahani and Kuhn 2018; Blanchet and Murthy 2019; Gao and Kleywegt 2022), specialized to a finite shared support:

\[ \sup_{q:\, W_c(q, \mathrm{nominal}) \,\le\, \mathrm{radius}} \mathbb{E}_q[\mathrm{loss}] = \inf_{\gamma \ge 0} \; \gamma \cdot \mathrm{radius} + \mathbb{E}_{\mathrm{nominal}}\!\left[ \max_j \big(\mathrm{loss}_j - \gamma \cdot \mathrm{cost}(\cdot, j)\big) \right] \]

This follows from LP duality on the transportation polytope: the primal is

\[ \max_{\pi \ge 0} \sum_{ij} \pi_{ij}\, \mathrm{loss}_j \quad \text{s.t.} \quad \sum_j \pi_{ij} = \mathrm{nominal}_i, \qquad \sum_{ij} \pi_{ij}\, \mathrm{cost}_{ij} \le \mathrm{radius} \]

(the column marginal, which defines the candidate q, is left free). Dualizing the budget constraint with multiplier \(\gamma \ge 0\) and the row constraints with free multipliers, and maximizing out \(\pi\) pointwise, yields exactly the one-dimensional convex dual above.

Unlike optora.dro.KLAmbiguitySet and optora.dro.PhiAmbiguitySet, this dual is not handed to an injected first-order solver. It is a nonnegative combination of maxima of affine functions of the scalar \(\gamma\), hence convex piecewise linear: its derivative is piecewise constant and jumps across the minimizer, so a fixed-step gradient method oscillates around the kink and never satisfies a gradient-norm stopping test. What the derivative does do is increase monotonically,

\[ \frac{\partial}{\partial \gamma}\Big( \gamma \cdot \mathrm{radius} + \sum_i \mathrm{nominal}_i \max_j (\mathrm{loss}_j - \gamma\,\mathrm{cost}_{ij})\Big) = \mathrm{radius} - \sum_i \mathrm{nominal}_i\, \mathrm{cost}_{i, j^\star(i, \gamma)}, \]

where \(j^\star(i, \gamma)\) is the maximizing column of row \(i\), so the minimizer is located by bisecting that derivative's sign change. The search starts from the exact bracket \([0, \Gamma]\), where

\[ \Gamma = \max_i \; \max_{k:\, \mathrm{cost}_{ik} > m_i} \frac{\mathrm{loss}_k - \mathrm{loss}_{j_0(i)}} {\mathrm{cost}_{ik} - m_i}, \qquad m_i = \min_j \mathrm{cost}_{ij}, \]

with \(j_0(i)\) the highest-loss column attaining \(m_i\): beyond \(\Gamma\) every row is maximized at a cheapest column, the derivative is frozen at \(\mathrm{radius} - \sum_i \mathrm{nominal}_i m_i \ge 0\), and the minimizer cannot lie further right. Bisection therefore reaches the dual optimum to working precision in a fixed, dtype-determined number of steps, with no step size, iteration budget, or tolerance to tune -- the same treatment optora.dro.TotalVariationAmbiguitySet gets for its own exactly solvable inner problem. It also removes the need to reparameterize \(\gamma\): the constraint \(\gamma \ge 0\) is the left end of the bracket, and the optimum is genuinely attained at \(\gamma = 0\) once radius is large enough to move all nominal mass onto the single highest-loss support point.

The bracket assumes the ambiguity set is nonempty, that is \(\mathrm{radius} \ge \sum_i \mathrm{nominal}_i m_i\), the smallest transport cost at which any candidate distribution can be reached. That holds automatically for a ground cost with a zero diagonal, the standard choice, for which \(m_i = 0\).

Because \(\gamma^\star\) is a kink, the worst-case expectation is then read off the primal solution rather than the dual value. At a kink several columns tie for a row's inner maximum, and re-evaluating the dual objective there would differentiate through that tie with arbitrary weights, giving a transport plan that does not spend the radius exactly and therefore a wrong gradient with respect to loss. The two ends of the final bracket instead name the two extreme tie-breaks explicitly: the plan that is still worth paying for just below \(\gamma^\star\) overspends the radius, the one that takes over just above it underspends, and the unique convex combination of the two that spends exactly radius is the worst-case distribution \(q^\star\). Complementary slackness then makes \(\mathbb{E}_{q^\star}[\mathrm{loss}]\) equal to the dual minimum, and since \(q^\star\) carries no autograd graph, differentiating that expectation gives exactly the envelope-theorem gradient with respect to loss. A minimizer at \(\gamma^\star = 0\) is the degenerate case where both ends agree and \(q^\star\) simply moves all mass to the highest-loss support point.

divergence is fixed to a SinkhornDivergence over cost (the only Wasserstein-type divergence implemented in optora.divergences), so contains(...) checks the entropy-regularized Sinkhorn divergence as a fast, differentiable approximation of exact Wasserstein-ball membership. worst_case_expectation does not depend on this approximation: it solves the exact (non-regularized) dual above directly from cost, since that dual has a clean closed form and does not need entropic relaxation for tractability.

Attributes:

Name Type Description
nominal Tensor

Reference distribution the ambiguity set is centered on.

divergence

SinkhornDivergence instance approximating distance from nominal for contains(...).

radius float | Tensor

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

cost Tensor

Square, nonnegative pairwise ground cost matrix between the shared support points of nominal and any candidate distribution. Aliases divergence.cost; not a separate buffer.

cost property

Ground cost matrix, aliasing divergence.cost (no separate buffer).

__init__(nominal, cost, radius, epsilon=0.1, sinkhorn_max_iter=100, sinkhorn_tol=1e-06, eps=1e-12, validate=False)

Initialize the Wasserstein-DRO 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
cost Tensor

Square, nonnegative pairwise ground cost matrix between the shared support points of nominal and any candidate distribution, shape (n, n) where n = nominal.shape[-1].

required
radius float | Tensor

Nonnegative bound on the Wasserstein 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, evaluating a sweep of radii in one solve.

required
epsilon float

Positive entropic regularization strength for the SinkhornDivergence used by contains(...).

0.1
sinkhorn_max_iter int

Maximum number of Sinkhorn scaling iterations, passed through to SinkhornDivergence.

100
sinkhorn_tol float

Sinkhorn scaling convergence tolerance, passed through to SinkhornDivergence.

1e-06
eps float

Small positive constant used to clamp Sinkhorn scaling denominators away from zero, passed through to SinkhornDivergence.

1e-12
validate bool

Whether to check that cost is nonnegative, passed through to SinkhornDivergence, and that nominal is a valid probability distribution. The checks read reductions over cost and nominal on the host, which blocks until the device has produced them, so they are opt-in and off by default to keep construction asynchronous.

False

Raises:

Type Description
ValueError

If radius is a negative float, if cost is not a square 2D tensor, if cost's size does not match nominal's support size, or if validate is set and cost contains negative entries or nominal is not a valid probability distribution.

worst_case_expectation(loss)

Compute the worst-case expected loss over the Wasserstein 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 bisected to its own dual multiplier gamma in one vectorized search.

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

expectation of loss under the worst-case distribution

Tensor

recovered from the dual optimum.

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.