Skip to content

Mutable scenario probabilities (issue #797) - #799

Open
DLWoodruff wants to merge 6 commits into
Pyomo:mainfrom
DLWoodruff:design/mutable-scenario-probabilities
Open

Mutable scenario probabilities (issue #797)#799
DLWoodruff wants to merge 6 commits into
Pyomo:mainfrom
DLWoodruff:design/mutable-scenario-probabilities

Conversation

@DLWoodruff

Copy link
Copy Markdown
Collaborator

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_probability mode that
stores each probability as a mutable Pyomo Param in the objective, so the
probability 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

  • Persistence detectionsputils.has_persistent_solve_api recognizes
    the APPSI / pyomo.contrib.solver interface (e.g. appsi_highs, the
    solver in the issue) as persistent for the EF workflow, in addition to legacy
    PersistentSolver. Kept separate from is_persistent() on purpose: APPSI's
    LegacySolver lacks the update_var/add_var/add_constraint methods that
    the PH/FWPH sites call behind is_persistent, so broadening that would break
    them.
  • EF pathmutable_probability option on ExtensiveForm /
    sputils._create_EF_from_scen_dict builds the objective against a mutable
    Param; ExtensiveForm.set_scenario_probabilities(prob_map) validates and
    applies a new vector (transactionally) and re-pushes the objective;
    solve_extensive_form(reuse_instance=True) skips set_instance so the
    persistent solver keeps its loaded instance across a sweep.
  • PH pathSPBase.set_scenario_probabilities(prob_map, check_sum=True, reset_ph_duals=True) updates _mpisppy_probability, forces prob_coeff to
    be recomputed, re-applies any variable-probability overrides, and (by
    default) zeroes the PH multipliers W. Two-stage for now.
  • Docs + exampledoc/src/mutable_probability.rst and a runnable
    examples/farmer/farmer_prob_sensitivity.py that sweeps the weight on one
    scenario 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_probability for a
bundle 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_duals defaults to True)

At a converged PH solution every scenario sits at the same nonanticipative
point, so the probability-weighted xbar is independent of the weights.
Re-solving from there with new probabilities but stale W leaves xbar
unmoved and PH reports immediate (false) convergence at the old solution.
Zeroing W breaks that consensus so PH re-converges for the new probabilities.
Covered by a regression test that shows reset_ph_duals=False stays stuck.

Cross-solver behavior (verified)

  • APPSI (appsi_highs) auto-tracks the objective change on the next
    solve(); the explicit set_objective re-push is redundant but harmless.
  • Legacy persistent (gurobi_persistent) does not auto-track, so the
    re-push is required — set_scenario_probabilities does it.

Verified end-to-end on the farmer example against a rebuild oracle to machine
precision across a probability sweep, with set_instance called once, for both
appsi_highs and gurobi_persistent.

Tests

Test_mutable_probability and Test_mutable_probability_ph in
mpisppy/tests/test_ef_ph.py (EF-vs-rebuild-oracle across a sweep, single
set_instance, guards/transactionality, PH prob_coeff refresh,
fresh-PH-vs-EF-oracle, in-place reuse, and the stuck-consensus case). Full file
passes; ruff clean; docs build.

Design and follow-up

doc/designs/mutable_scenario_probabilities_design.md records the full
rationale. A follow-up (design §9 phase 3) will extend
SPBase.set_scenario_probabilities to multistage node probabilities
(ScenarioNode.cond_prob) and the variable-probability interaction; a
multistage node list currently raises NotImplementedError.

DLWoodruff and others added 6 commits July 13, 2026 13:04
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

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.21%. Comparing base (1e1b734) to head (a4a23b8).
⚠️ Report is 42 commits behind head on main.

Files with missing lines Patch % Lines
mpisppy/utils/sputils.py 82.35% 3 Missing ⚠️
mpisppy/spbase.py 92.85% 2 Missing ⚠️
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.
📢 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.

@bknueven

bknueven commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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.

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.

Mutable parameter for scenario probabilities

2 participants