Mutable scenario probabilities (issue #797) - #799
Conversation
Design proposal for representing scenario probabilities as opt-in mutable Pyomo Params so EF probabilities can be updated between persistent-solver solves without rebuilding the model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…zation Mutable probability is a full-EF feature and bundles never set the flag, so requiring sum-to-1 lets the mutable path drop the objective divisor entirely. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rding - set_objective re-extracts mutable Params on appsi_highs; appsi auto-tracks param/objective changes on next solve (legacy persistent requires the push). - appsi_highs is not recognized as persistent by mpi-sppy today; detection must be extended to APPSI / pyomo.contrib.solver (phase 0 prerequisite). - reuse_instance is an explicit argument to solve_extensive_form. - Replace "spike" jargon with "experiment". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lets an ExtensiveForm's scenario probabilities be updated in place and re-solved on a persistent solver without rebuilding the model -- the rolling-horizon / probability-sensitivity use case from the issue. Phase 0 (detection): - Add sputils.has_persistent_solve_api(): recognizes both the legacy PersistentSolver and the APPSI / pyomo.contrib.solver interfaces (e.g. appsi_highs) by the set_instance/set_objective/load_vars trio. Kept separate from is_persistent() on purpose: broadening is_persistent would break PH/FWPH call sites that call update_var/add_var/add_constraint, which the APPSI legacy wrapper does not expose. - ExtensiveForm.solve_extensive_form now uses it in place of the old '"persistent" in solver_name' and is_persistent() checks, so appsi_highs finally takes the persistent path (previously it did not). Phase 1 (feature): - _create_EF_from_scen_dict(mutable_probability=False): when True, store probabilities as a mutable Param (_mpisppy_model.prob) referenced by the objective, require the probabilities to sum to 1, and drop the divisor (design option B). Rejected for bundles (sum < 1). - ExtensiveForm gains mutable_probability (kwarg or option key), set_scenario_probabilities() (validate-then-apply, transactional; re-pushes the objective to a persistent solver), and a reuse_instance argument to solve_extensive_form() to skip set_instance on re-solves. Verified end-to-end on farmer against a rebuild oracle: machine-precision agreement across a probability sweep with set_instance called once, for both appsi_highs (auto-tracks) and gurobi_persistent (needs the set_objective re-push). Tests: Test_mutable_probability in test_ef_ph.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e 2) Add SPBase.set_scenario_probabilities(prob_map, check_sum=True, reset_ph_duals=True): updates _mpisppy_probability on local scenarios and forces prob_coeff to be recomputed (new force= flag on _compute_unconditional_node_probabilities bypasses the compute-once short-circuit). Two-stage only; multistage raises NotImplementedError. reset_ph_duals (default True) zeroes the PH multipliers W: at a converged PH consensus, xbar is weight-independent, so re-solving with new probabilities but stale W falsely reports convergence at the old optimum. Zeroing W breaks the consensus so a re-solve tracks the new probabilities. Tests (Test_mutable_probability_ph in test_ef_ph.py): prob_coeff refresh, guards, fresh-PH-vs-EF-oracle match, in-place reuse match, and the reset_ph_duals=False stuck-consensus case. Full file 38 passed/1 skipped; ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e 4) Add a probability-sensitivity example and user docs for the mutable scenario probability feature. - examples/farmer/farmer_prob_sensitivity.py: builds a farmer EF once with mutable_probability=True, then sweeps the weight on one scenario, re-solving with reuse_instance=True so a persistent solver keeps its loaded instance across the sweep. Reports objective and first-stage acreage per vector. - doc/src/mutable_probability.rst: user docs for the EF and PH paths, the sum-to-1 requirement, persistent-solver reuse, and the reset_ph_duals consensus caveat; linked from index.rst after ef.rst. - design doc: mark phase 4 done; record the deliberate omission of a generic_cylinders --mutable-probability flag (no EF path to consume it; a sweep is a driver loop, so the example script is the CLI exposure). Verified: example runs on farmer with gurobi_persistent; docs build with sphinx and autodoc resolves both set_scenario_probabilities methods; ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #799 +/- ##
==========================================
+ Coverage 76.18% 76.21% +0.02%
==========================================
Files 169 169
Lines 22224 22287 +63
==========================================
+ Hits 16932 16986 +54
- Misses 5292 5301 +9 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Since you included PH -- I'm guessing just disabling prox is not a good thing, as you could still converge immediately for some problems. But could we do something less extreme than resetting W, and just do a partial reset by taking a convex combination of the converged re-normalized or re-projected W and 0? Or, we can disable prox and keep the re-normalized or re-projected W's for the first iteration. |
There was a problem hiding this comment.
Pull request overview
Adds mutable scenario probabilities for efficient EF and PH re-solves, especially with persistent solvers.
Changes:
- Adds mutable Pyomo probability parameters and persistent-instance reuse.
- Adds PH probability updates and regression tests.
- Adds documentation and a farmer sensitivity example.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
mpisppy/utils/sputils.py |
Builds mutable-probability EFs and detects persistent solver APIs. |
mpisppy/opt/ef.py |
Adds probability updates and instance reuse. |
mpisppy/spbase.py |
Refreshes PH probabilities and dual state. |
mpisppy/tests/test_ef_ph.py |
Tests EF and PH probability changes. |
examples/farmer/farmer_prob_sensitivity.py |
Demonstrates probability sweeps. |
doc/src/mutable_probability.rst |
Documents the new APIs. |
doc/src/index.rst |
Adds the documentation page. |
doc/designs/mutable_scenario_probabilities_design.md |
Records design rationale and phases. |
Suppressed comments (1)
mpisppy/spbase.py:507
- Validation happens only after probabilities,
prob_coeff, variable-probability state, andWhave been mutated. Consequently the guarded call in the new test ({"scen0": 0.9}) raises but leaves the PH object changed; a negative vector that still sums to one is accepted as well. Build a finite, nonnegative candidate vector and perform the collective sum check before applying any model-state or dual changes.
if k in prob_map:
s._mpisppy_probability = prob_map[k]
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| resulting = {sn: prob_map.get(sn, pyo.value(prob[sn])) | ||
| for sn in self.ef._ef_scenario_names} | ||
| total = sum(resulting.values()) | ||
| if abs(total - 1.0) > 1e-9: | ||
| raise ValueError( | ||
| f"scenario probabilities must sum to 1; got {total}.") | ||
| for sname, p in prob_map.items(): | ||
| prob[sname].value = p | ||
| # keep _mpisppy_probability consistent for downstream readers | ||
| getattr(self.ef, sname)._mpisppy_probability = p |
| parser.add_argument("--num-scens", type=int, default=3, | ||
| help="Number of scenarios. Default: 3.") | ||
| args = parser.parse_args() |
| if reset_ph_duals: | ||
| for s in self.local_scenarios.values(): | ||
| if hasattr(s, "_mpisppy_model") and \ | ||
| hasattr(s._mpisppy_model, "W"): | ||
| for idx in s._mpisppy_model.W: | ||
| s._mpisppy_model.W[idx]._value = 0.0 |
| Status: phases 0–1 implemented and verified (addresses issue #797); phases | ||
| 2–4 remain (§9). Covers the current behavior (§1), the use case and goals | ||
| (§2–3), the design (§4), the EF path in detail (§5), the PH/decomposition |
| NotImplementedError: | ||
| If any local scenario has more than the root and a single | ||
| leaf node (multistage is a later phase). |
Mutable scenario probabilities (issue #797)
Closes #797.
Today each scenario's probability is folded into the Extensive Form (EF)
objective as a floating-point constant when the model is built, so changing a
probability requires rebuilding the objective and, for a persistent solver,
re-loading the instance. This PR adds an opt-in
mutable_probabilitymode thatstores each probability as a mutable Pyomo
Paramin the objective, so theprobability vector can be updated in place and a persistent solver re-solved
cheaply — the motivating use case in the issue (a rolling-horizon loop that
re-weights a fixed scenario set between solves), and probability-sensitivity
studies.
What's in this PR
sputils.has_persistent_solve_apirecognizesthe APPSI /
pyomo.contrib.solverinterface (e.g.appsi_highs, thesolver in the issue) as persistent for the EF workflow, in addition to legacy
PersistentSolver. Kept separate fromis_persistent()on purpose: APPSI'sLegacySolverlacks theupdate_var/add_var/add_constraintmethods thatthe PH/FWPH sites call behind
is_persistent, so broadening that would breakthem.
mutable_probabilityoption onExtensiveForm/sputils._create_EF_from_scen_dictbuilds the objective against a mutableParam;ExtensiveForm.set_scenario_probabilities(prob_map)validates andapplies a new vector (transactionally) and re-pushes the objective;
solve_extensive_form(reuse_instance=True)skipsset_instanceso thepersistent solver keeps its loaded instance across a sweep.
SPBase.set_scenario_probabilities(prob_map, check_sum=True, reset_ph_duals=True)updates_mpisppy_probability, forcesprob_coefftobe recomputed, re-applies any variable-probability overrides, and (by
default) zeroes the PH multipliers
W. Two-stage for now.doc/src/mutable_probability.rstand a runnableexamples/farmer/farmer_prob_sensitivity.pythat sweeps the weight on onescenario and reuses the persistent instance across the sweep.
Normalization: require sum-to-1 (option B)
On the mutable path the supplied probabilities must sum to 1 (within
1e-9);there is no re-normalization, so the objective stays a plain probability-weighted
sum with no division node. The existing float path — including the normalization
divisor that scenario bundles rely on — is untouched, because the mutable
flag is opt-in and bundles never set it (requesting
mutable_probabilityfor abundle raises). Partial mappings are allowed: omitted scenarios keep their
current probability as long as the full vector still sums to 1. A rejected call
leaves the model unchanged.
PH warm-start caveat (why
reset_ph_dualsdefaults to True)At a converged PH solution every scenario sits at the same nonanticipative
point, so the probability-weighted
xbaris independent of the weights.Re-solving from there with new probabilities but stale
Wleavesxbarunmoved and PH reports immediate (false) convergence at the old solution.
Zeroing
Wbreaks that consensus so PH re-converges for the new probabilities.Covered by a regression test that shows
reset_ph_duals=Falsestays stuck.Cross-solver behavior (verified)
appsi_highs) auto-tracks the objective change on the nextsolve(); the explicitset_objectivere-push is redundant but harmless.gurobi_persistent) does not auto-track, so there-push is required —
set_scenario_probabilitiesdoes it.Verified end-to-end on the farmer example against a rebuild oracle to machine
precision across a probability sweep, with
set_instancecalled once, for bothappsi_highsandgurobi_persistent.Tests
Test_mutable_probabilityandTest_mutable_probability_phinmpisppy/tests/test_ef_ph.py(EF-vs-rebuild-oracle across a sweep, singleset_instance, guards/transactionality, PHprob_coeffrefresh,fresh-PH-vs-EF-oracle, in-place reuse, and the stuck-consensus case). Full file
passes;
ruffclean; docs build.Design and follow-up
doc/designs/mutable_scenario_probabilities_design.mdrecords the fullrationale. A follow-up (design §9 phase 3) will extend
SPBase.set_scenario_probabilitiesto multistage node probabilities(
ScenarioNode.cond_prob) and the variable-probability interaction; amultistage node list currently raises
NotImplementedError.