Skip to content

VSS: Value of the Stochastic Solution report (--vss, two-stage)#791

Draft
DLWoodruff wants to merge 5 commits into
Pyomo:mainfrom
DLWoodruff:vss_design
Draft

VSS: Value of the Stochastic Solution report (--vss, two-stage)#791
DLWoodruff wants to merge 5 commits into
Pyomo:mainfrom
DLWoodruff:vss_design

Conversation

@DLWoodruff

@DLWoodruff DLWoodruff commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a Value of the Stochastic Solution (VSS) report to
generic_cylinders. Passing --vss prints RP / EV / EEV / VSS at the
end of an --EF or a decomposition run. Two-stage, opt-in, report-only
(it never changes the solution). Closes the two-stage part of the
pysp_but_not_mpisppy.md §A7 gap (PySP's ef_vss.py).

Example (farmer, 3 scenarios):

================= VSS report =================
  RP  (stochastic solution, here-and-now):        -108390   [EF, exact]
  EV  (mean-value problem objective)     :        -118600
  EEV (EV first stage over scenarios)    :        -107240
  VSS = EEV - RP (sense=min)             :           1150   (1.06% of |RP|)
=============================================

How

Mirrors the do_mmw post-run report and reuses shipped machinery — no
new spoke/hub code:

  • average_scenario_creator (the Jensen's-bound contract) → the mean-value
    solve, giving EV and the first-stage x_bar;
  • Xhat_Eval.evaluate → the honest cross-scenario EEV (fix x_bar,
    solve every scenario);
  • RP is exact from ef.get_objective_value(), or the incumbent
    (BestInnerBound) with a BestOuterBound/BestInnerBound bracket from
    the wheel.

New mpisppy/generic/vss.py (vss_prep + do_vss), one config flag, two
call sites in generic_cylinders.py.

Behavior details

  • Sign-aware: VSS = EEV − RP (min) or RP − EEV (max), always
    reported ≥ 0.
  • Decomposition: conservative point value (against the incumbent) plus
    the VSS bracket implied by any unclosed optimality gap.
  • Solver options / mipgap: the EV and EEV solves reuse the run's solver
    and solver_options via solver_specificationEF_solver_options
    after --EF, else solver_options — so all three numbers are solved
    consistently and any mipgap in that string applies. Since VSS = EEV − RP
    is a difference of two optimized values, a loose gap makes a small VSS
    unreliable (documented). (Also lets --vss work on an --EF run that
    sets only --EF-solver-name.)
  • Infeasible mean-value first stage (no relatively complete recourse) ⇒
    EEV = VSS = +inf, naming the offending scenarios. MPI-safe: solves with
    need_solution=False so a clean per-scenario infeasibility does not
    deadlock the collectives.
  • Fail-fast at setup (before the solve) on multistage, proper bundles,
    ADMM, --cvar, or a missing average_scenario_creator.

Cost

EEV re-solves every scenario once with the first stage fixed. Fixing the
first stage decouples the scenarios and makes each solve easier than the
original, so the added wall-clock is model-dependent — usually a fraction
of the run (for a decomposition run, ~one iteration's worth), occasionally
significant with very many scenarios or expensive recourse. Off by default.

Tests

mpisppy/tests/test_vss.py: setup guards, sign/bracket helpers, EF
minimize and maximize end-to-end (asserting the actual RP/EV/EEV/VSS
numbers), solver-options threading, the decomposition bracket, and the
infeasible-EEV path. Wired into run_coverage.bash and a dedicated step in
the solver-equipped regression CI job so the numerical assertions run
under cplex/xpress (not just the no-solver unit job).

Drive-by: test_jensens.py had the same wiring gap (only in the
no-solver job); added a regression-job step so its solver-gated e2e cases
run in CI too.

Docs

doc/src/vss.rst (in the toctree; explains VSS, the honest cost note, the
solver-options/mipgap reuse, the EF-exact vs bracketed-RP distinction, and
infeasibility); doc/designs/vss_design.md (design);
pysp_but_not_mpisppy.md §A7 marked addressed for two-stage.

Scope / follow-ups

Two-stage only. Multistage VSS (the expected-value tree via
ef_xhat_nonants) and EVPI are noted as future work in the design doc. V1
threads the solver-options string (not the separate EF_mipgap /
*_iter*_mipgap knobs or a *_solver_options_file).

🤖 Generated with Claude Code

DLWoodruff and others added 4 commits July 9, 2026 11:15
Add doc/designs/vss_design.md: a post-run --vss report in
generic_cylinders that computes VSS = EEV - RP. Reuses shipped
machinery (average_scenario_creator for the mean-value solve;
Xhat_Eval.evaluate for the honest cross-scenario EEV; wheel/EF for RP),
mirroring the do_mmw post-run report structure. Covers the
minimization/maximization sign convention, the EF-exact vs
decomposition-bracketed RP, the infeasible-mean-value-solution case, and
a prominent warning that EEV re-solves every scenario and can roughly
double run time. Two-stage first; multistage plumbing (ef_xhat_nonants)
noted as future. Closes the pysp_but_not_mpisppy.md A7 gap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-stage)

generic_cylinders --vss prints RP / EV / EEV / VSS after an --EF or a
decomposition run. Reuses shipped machinery: average_scenario_creator for
the mean-value solve (x_bar) and Xhat_Eval.evaluate for the honest
cross-scenario EEV; RP is exact from the EF or the incumbent (with a
BestOuterBound/BestInnerBound bracket) from the wheel. Structure mirrors
do_mmw: new mpisppy/generic/vss.py (vss_prep + do_vss), one config flag,
two call sites in generic_cylinders.

- Sign-aware: VSS = EEV-RP (min) or RP-EEV (max), always reported >= 0.
- Decomposition run reports a conservative point value plus the VSS bracket
  implied by the unclosed optimality gap.
- Infeasible mean-value first stage => EEV = VSS = +inf, naming the
  offending scenarios (MPI-safe: solves with need_solution=False so a clean
  per-scenario infeasibility does not deadlock the collectives).
- Two-stage only; fails fast at setup (before the solve) on multistage,
  proper bundles, ADMM, --cvar, or a missing average_scenario_creator.

Tests: mpisppy/tests/test_vss.py (guards, sign/bracket helpers, EF min/max
e2e, decomposition bracket, infeasible EEV), wired into run_coverage.bash
and test_pr_and_main.yml. Docs: doc/src/vss.rst (+ index toctree);
pysp_but_not_mpisppy.md A7 marked addressed for two-stage. Design in
doc/designs/vss_design.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The unit-tests job installs no solver, so test_vss's solver-gated e2e
cases (the ones asserting the actual RP/EV/EEV/VSS numbers on farmer)
would skip there. Add a dedicated step in the cplex/xpress regression
job -- mirroring test_feasible_xhat -- so the numerical results are
actually checked in CI. The farmer-3 cases are LPs with a unique,
solver-independent optimum, well within community-cplex size limits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gression job

Like test_vss, test_jensens was only in the no-solver unit-tests job, so
its solver-gated end-to-end cases (farmer-3 average-scenario bound, sizes-3
integer-guard paths) skipped in CI. Add a dedicated regression-job step so
they actually run under cplex/xpress. Both models are small and within
community-cplex limits; the file is green locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.66667% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.23%. Comparing base (b2474b4) to head (95d929c).

Files with missing lines Patch % Lines
mpisppy/generic/vss.py 87.59% 17 Missing ⚠️
mpisppy/generic_cylinders.py 40.00% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #791      +/-   ##
==========================================
+ Coverage   76.16%   76.23%   +0.06%     
==========================================
  Files         169      170       +1     
  Lines       22224    22373     +149     
==========================================
+ Hits        16928    17057     +129     
- Misses       5296     5316      +20     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

… solves

The EV and EEV solves were passing no solver options, so they ran at the
solver's default mipgap -- inconsistent with the run that produced RP, and
uncontrollable. Resolve solver name + options via solver_specification
(EF_solver_options after --EF, else solver_options) and apply them to both
the mean-value solve and the Xhat_Eval EEV pass. Also lets --vss work on an
--EF run that sets only --EF-solver-name.

Because VSS = EEV - RP is a difference of two optimized values, mipgap
matters; document that the VSS solves inherit the run's solver-options
string and that a loose gap makes a small VSS unreliable.

Also reconcile the cost docs: drop the "roughly doubles run time on large/
integer models" claim (backwards -- fixing the first stage decouples the
scenarios and makes each EEV solve easier; for decomposition EEV is ~one
iteration's worth). State honestly that the added time is model-dependent
and can be significant for some problems.

Adds test_ef_solver_options_threaded; updates _compute_eev's signature and
its infeasible-path test caller.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant