CVaR: bound the Value-at-Risk variable eta#795
Open
DLWoodruff wants to merge 3 commits into
Open
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #795 +/- ##
==========================================
+ Coverage 76.16% 76.19% +0.02%
==========================================
Files 169 169
Lines 22224 22379 +155
==========================================
+ Hits 16928 17052 +124
- Misses 5296 5327 +31 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
8d3f81b to
af0f8b3
Compare
The CVaR VaR variable eta was completely free. eta lives on the objective
scale, so a free eta hurts some solvers, and it makes the Lagrangian /
subgradient outer-bound relaxations (which dualize eta's non-anticipativity)
unbounded, so those spoke solves fail. eta's optimum is VaR_alpha(Cost),
which is provably within the range of Cost over all scenarios, so bounding eta
by the global cost range is valid (non-cutting).
Two automatic methods, chosen by --cvar-eta-bound-method (default fbbt):
- fbbt: structural cost range per scenario via FBBT (no solves).
- solve: per scenario, find the cost range by solving, treating the two ends
differently. The "how cheap/good" (slack) side only needs a valid enclosing
value, so a relaxation (LP) is enough. The worst-case ("how bad") side cannot
come from a relaxation -- to bound how bad the true integer problem gets you
solve the MIP -- but only a valid bound is needed, so the MIP runs with a
loose gap (--cvar-eta-mipgap) and we read its dual bound (valid at any gap;
a loose gap only makes it looser, never invalid). The worst-case side is the
max cost for a minimize, the min reward for a maximize.
Mechanism:
- add_cvar: compute this scenario's FBBT cost range and stash it; apply any
explicit eta_lb/eta_ub override to eta immediately.
- set_cvar_eta_bounds: reduce the stashed per-scenario ranges to the global
[lb, ub] over the ROOT comm and bound eta. The bound MUST be global, not
per-scenario: eta is tied by non-anticipativity, so a per-scenario box could
intersect away the true VaR.
- SPBase.__init__ calls it once per opt object (EF and every cylinder); no-op
for non-CVaR runs.
- compute_cvar_eta_bounds_by_solve: the 'solve' method; generic_cylinders runs
it once up front and injects the result as eta bounds.
- Flags: --cvar-eta-lb/--cvar-eta-ub override a side; --cvar-eta-bound-method
none turns the automatic bound off. When a side is structurally unbounded
(e.g. farmer's purchases) no method can bound it; supply it by hand.
Tests extend test_cvar.py; Risk Management docs get a "Bounding eta" section
with the farmer worked example (solve for the profit side, hand-derive the
unbounded worst-case side).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
af0f8b3 to
fdda57b
Compare
…ution, gated The 'solve' method's worst-case side maximized cost over the feasible region, which is unbounded whenever the model can act arbitrarily wastefully (any big-M formulation, e.g. aircond, or farmer's unlimited purchases), so it returned an unbounded (useless) tail. Instead evaluate the cost at a single feasible point, the risk-neutral solution x^RN (which minimizes E[Cost]), and take max_s Cost_s(x^RN) (min for a maximize). This is finite and never cuts the optimum: eta* = VaR(x*) <= CVaR(x*) <= CVaR(x^RN) <= max_s Cost_s(x^RN), the middle step using E(x^RN) <= E(x*). Finding x^RN is a coupled solve, so this builds and solves the risk-neutral extensive form -- only tractable when the EF is -- so it is gated by the new --cvar-eta-solve-max-scenarios (default 1000) and left free above the gate; large models should use fbbt or an explicit --cvar-eta-lb/ub. The easy side is unchanged (distributed LP relaxation). --cvar-eta-mipgap is now the EF-solve gap (tight default 1e-4). Docstrings, risk_management.rst, and tests updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rio-count gate Scenario count is a crude proxy for EF solve cost (it ignores per-scenario size and integrality), so replace --cvar-eta-solve-max-scenarios with --cvar-eta-solve-time-limit (seconds, default 60): the worst-case-side EF solve is time-boxed, and if it does not reach optimality in time the tail is left free. A timed-out incumbent is discarded, never used, so the bound can never be invalid. time_limit <= 0 skips the EF solve entirely (a deterministic off-switch, also the test hook). Adds a portable 'time_limit' key to translate_solver_options (gurobi TimeLimit / cplex timelimit / xpress maxtime; HiGHS passes through). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Gives the CVaR Value-at-Risk variable
etaa valid automatic bound. Until nowetawas a completely free variable living on the objective scale.eta's optimum isVaR_alpha(Cost), which is provably within the range of thecost over all scenarios, so bounding
etaby the global[min cost, max cost]is valid (it can never cut off the optimum). Besides helping solvers and keeping
the PH subproblems well posed, this can be necessary for the outer-bound
spokes: a Lagrangian/subgradient relaxation dualizes
eta's non-anticipativity,and with
etafree that relaxed subproblem is easily unbounded and its solvesfail.
What's added
Two automatic methods, selected by
--cvar-eta-bound-method(defaultfbbt):fbbt— structural per-scenario cost range via feasibility-based boundstightening (no solves).
solve— find the cost range by solving, treating the two endsdifferently:
maximize — only needs a valid enclosing value, so a relaxation (LP) over
the feasible region is enough (that side sits far from the VaR). It is finite
for any real model and is solved per scenario, distributed across the ranks.
maximize — cannot come from maximizing cost over the feasible region:
that is unbounded whenever the model can act arbitrarily wastefully (any
big-M formulation, or farmer's unlimited purchases). Instead we evaluate the
cost at a single feasible point, the risk-neutral solution
x^RN(whichminimizes
E[Cost]), and takemax_s Cost_s(x^RN)(min for a maximize).This is finite and never cuts the optimum, because
eta* = VaR(x*) <= CVaR(x*) <= CVaR(x^RN) <= max_s Cost_s(x^RN), the middlestep using
E(x^RN) <= E(x*).Finding
x^RNis a coupled solve — one common first-stage decision — sosolvebuilds and solves the risk-neutral extensive form. That is onlytractable when the EF is, which is exactly when you would not need decomposition,
so
solveis a convenience for small/medium models wherefbbtleaves theworst-case side unbounded but one EF solve is affordable. The solve is
time-boxed by
--cvar-eta-solve-time-limit(default60s); if it does notreach optimality in time the worst-case side is left free (the timed-out solution
is discarded, never used, so the bound can never be invalid). For a genuinely
large model, use
fbbtor an explicit--cvar-eta-ub/--cvar-eta-lbinstead.New flags:
--cvar-eta-lb/--cvar-eta-ub— explicit overrides (each wins on its side).--cvar-eta-bound-method—fbbt(default) /solve/none.--cvar-eta-mipgap— the gap for thesolvesolves; for the worst-case sideit is the risk-neutral EF solve gap, so keep it tight (default
1e-4).--cvar-eta-solve-time-limit— seconds for the coupled EF solve (default60); if it does not finish in time the worst-case side is left free (<= 0skips it).
Implementation
add_cvarcomputes/stashes the FBBT cost range and applies any explicitoverride to
etaimmediately.set_cvar_eta_boundsreduces the stashed ranges to the global[lb, ub]overthe ROOT communicator and bounds
eta(only user-unset, finite sides).SPBase.__init__calls it once per opt object — EF and every cylinder — and isa no-op for non-CVaR runs.
compute_cvar_eta_bounds_by_solveimplements thesolvemethod: the easy sideis a distributed LP relaxation; the worst-case side is the cost range at the
risk-neutral solution from a time-boxed, rank-0 EF solve (broadcast to all
ranks).
generic_cylindersruns it once up front and injects the result. Aportable
time_limitkey was added totranslate_solver_options(gurobiTimeLimit/ cplextimelimit/ xpressmaxtime; HiGHS passes through).Scope / limitation
solve's worst-case side needs an EF-tractable risk-neutral problem, so it is asmall/medium convenience and is time-boxed. For a genuinely large model use
fbbtor an explicit--cvar-eta-ub/--cvar-eta-lb. Whenfbbtcannot bounda side and the EF is too big to solve in time, supply that side by hand; the docs
work the farmer both ways (small:
solvebounds both sides automatically; large:the worst-case side hand-derived).
Testing
mpisppy/tests/test_cvar.py(29 tests): the override / auto-off /unbounded-side / global-reduction paths, an EF end-to-end bound check, and for
the
solvemethod — the tiny cost range, the maximize mirror, therisk-neutral-point tail being finite where max-over-region was unbounded,
a
time_limit <= 0leaving the tail free, and both sides of the farmerbeing bounded (worst-case side matching an independent risk-neutral EF solve).
skip under
mpiexec -np 2(ranks agree), and end-to-end on a big-MaircondEF that the worst-case side is now finite (was
unbounded) and the EF stillsolves.
Docs
"Bounding the Value-at-Risk variable eta" section in
doc/src/risk_management.rstrewritten for the risk-neutral-point method and thegate, with the farmer worked both automatically (small) and by hand (large).
🤖 Generated with Claude Code