Merge PRs #651 + #652: stage2_ef_solver_name and stoch_admm changes#653
Merge PRs #651 + #652: stage2_ef_solver_name and stoch_admm changes#653DLWoodruff wants to merge 29 commits intoPyomo:mainfrom
Conversation
Register the stage2 EF solver option in xhatshuffle_args() so it is available as --stage2-ef-solver-name from the generic_cylinders CLI. Rename the option from stage2EFsolvern to stage2_ef_solver_name to follow project naming conventions. Relax the hard assertion requiring this option for multistage xhatshuffle to a warning, since the regular multi-stage xhat path also works. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #653 +/- ##
==========================================
+ Coverage 70.71% 70.73% +0.02%
==========================================
Files 153 153
Lines 19005 19041 +36
==========================================
+ Hits 13439 13469 +30
- Misses 5566 5572 +6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
PR Pyomo#651 relaxed a hard assert into a warnings.warn call when multistage xhatshuffle is used without --stage2-ef-solver-name. Without a test, silent drift (warning removed, branch not taken) would go unnoticed. Covers both cases: warning fires when missing, does not fire when set. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stoch_AdmmWrapper.assign_variable_probs synthesizes dummy nonants for subproblems that don't own a given consensus variable, renaming them (e.g. y[F1_1] -> y__F1_1__) to distinguish the fixed-at-zero dummies from real consensus vars. Across ADMM subproblem scenarios this causes the post-Pyomo#634 nonant name validator to trip. Same auto-disable pattern used for proper_bundles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ef_options was hard-coded to emit options={"solver": ...} only, so the
auto-disable we set on cfg for --admm/--stoch-admm runs never reached
SPBase._verify_nonant_names and the EF path still tripped on the dummy
nonant name mismatch. Mirror cfg_vanilla.shared_options, honoring both
cfg.turn_off_names_check and proper-bundles. Regression test added.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The merge of main into merge-651-652 kept both the pre-split unindented test list and the indented if-block version from Pyomo#654, causing every test to run twice on each runall CI job. Drop the unindented duplicate block and port the --stage2EFsolvern -> --stage2-ef-solver-name rename (from commit e053324) onto the indented hydro line in run_second_part. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stoch_AdmmWrapper injects fixed-to-0 dummy Vars for consensus variables that a given ADMM subproblem does not own, so every scenario's nonant_vardata_list has the same length and positional correspondence at each shared tree node. PH handles these correctly via probability 0 in var_prob_list, but EF with nonant_for_fixed_vars=True (the default via ExtensiveForm) was creating hard equality constraints tying the real owners to the zero-fixed dummies, forcing them to zero and producing noticeably worse objectives than PH for num_admm_subproblems > 2. Reported by a user running stoch_distr with 4 admm x 3 stoch. Tag the dummies post-hoc on each ScenarioNode's surrogate_vardatas so EF skips their nonant constraints and fixers ignore them, without disturbing positional alignment across scenarios (the normal surrogate_nonant_list API appends at the end, which would misalign). Also handle the iteration-order-dependent case in _create_EF_from_scen_dict where a surrogate was installed as ref before a real var arrived: upgrade the ref to the real var and drop the surrogate tag so subsequent real vars tie to it. While here, preserve user-supplied surrogate_vardatas and nonant_ef_suppl_vardata_list across Stoch_AdmmWrapper's stage-node rewrite, which had been silently dropping them. Tests: test_ef_partial_consensus drives generic_cylinders --EF on the user's 3x4 reproducer and asserts EF sits within the PH outer/inner envelope; test_preserves_user_surrogates_and_ef_suppl constructs a minimal partial-consensus scenario with user surrogates and EF-suppl vars and asserts both survive the rewrite. Both verified to fail without the corresponding change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR merges prior work to standardize the stage2_ef_solver_name option across the codebase/CLI/docs and to improve stochastic-ADMM interoperability with EF/PH (including partial-consensus behavior and config-driven scenario/subproblem naming).
Changes:
- Renames and wires up
stage2_ef_solver_nameend-to-end (Config registration, generic CLI/spokes, examples, and docs) and relaxes multistage xhatshuffle’s hard requirement to a warning. - Updates stochastic-ADMM plumbing: creators now accept
cfg,Stoch_AdmmWrapperbetter handles partial consensus with EF (surrogate-tagged dummies + EF ref handling), and exposesget_scenario_unscaled. - Adds/updates tests and documentation to cover the new option wiring and ADMM/EF behaviors.
Reviewed changes
Copilot reviewed 29 out of 30 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| mpisppy/utils/stoch_admmWrapper.py | Tags dummy consensus vars as surrogates and preserves user nonant metadata during node rewrites; adds component-name fallback and get_scenario_unscaled. |
| mpisppy/utils/sputils.py | Adjusts EF creation to “upgrade” surrogate refs to real vars for partial-consensus ADMM cases. |
| mpisppy/utils/config.py | Adds guarded registration helpers for branching_factors and stage2_ef_solver_name; registers stage2 solver name from multistage()/xhatshuffle_args(). |
| mpisppy/utils/cfg_vanilla.py | Forwards turn_off_names_check into EF options dict (and auto-disables when bundled). |
| mpisppy/tests/test_with_cylinders.py | Adds hydro multistage test for xhatshuffle with stage2_ef_solver_name. |
| mpisppy/tests/test_stoch_admmWrapper.py | Adds regression tests for user surrogate/EF-supplemental preservation and EF partial-consensus correctness. |
| mpisppy/tests/test_admm_bundler.py | Updates creator signatures to take cfg; adds tests for multistage warning and EF option forwarding. |
| mpisppy/tests/examples/stoch_distr/stoch_distr_admm_cylinders.py | Updates stoch_distr creator calls to pass cfg. |
| mpisppy/tests/examples/stoch_distr/stoch_distr.py | Updates scenario/subproblem name creators to accept cfg. |
| mpisppy/spopt.py | Fixes indentation for extension hook calls. |
| mpisppy/opt/fwph.py | Updates option name usage to stage2_ef_solver_name. |
| mpisppy/generic_cylinders.py | Auto-disables nonant name checking for ADMM wrappers (dummy/renamed components). |
| mpisppy/generic/spokes.py | Passes stage2_ef_solver_name + branching_factors into xhatshuffle spoke options for multistage. |
| mpisppy/generic/parsing.py | Replaces multistage xhatshuffle assert with a warning when stage2_ef_solver_name is missing. |
| mpisppy/generic/admm.py | Uses cfg-driven name creators; registers branching factors + stage2 EF solver arg for ADMM CLI path. |
| mpisppy/extensions/xhatbase.py | Renames stage2 EF solver kwarg and updates usage in xhat evaluation path. |
| mpisppy/cylinders/xhatshufflelooper_bounder.py | Renames option key plumbing for stage2 EF solve path. |
| examples/stoch_distr/stoch_distr_admm_cylinders.py | Updates creator calls to pass cfg. |
| examples/stoch_distr/stoch_distr.py | Updates scenario/subproblem name creators to accept cfg. |
| examples/run_more.py | Updates comment text to new option name. |
| examples/run_all.py | Updates CLI flag to --stage2-ef-solver-name. |
| examples/hydro/hydro_cylinders.py | Updates config key/usage to stage2_ef_solver_name. |
| examples/hydro/hydro.py | Updates config key registration to stage2_ef_solver_name. |
| examples/hydro/demo.bash | Updates CLI flag to --stage2-ef-solver-name. |
| examples/hydro/cfg.bash | Updates CLI flag to --stage2-ef-solver-name. |
| examples/generic_tester.py | Updates CLI flag to --stage2-ef-solver-name. |
| examples/generic_cylinders.bash | Updates CLI flag to --stage2-ef-solver-name. |
| doc/src/spokes.rst | Documents the new flag and option key name for stage2 EF behavior. |
| doc/src/generic_cylinders.rst | Adds multistage documentation for --stage2-ef-solver-name. |
| doc/src/generic_admm.rst | Updates docs to reflect cfg-driven creator signatures and related usage notes. |
Comments suppressed due to low confidence (1)
mpisppy/utils/stoch_admmWrapper.py:227
varlist[stage-1].append(v)runs even whenvisNone(e.g., when a consensus var is listed but not found on the scenario model). This can trigger an AttributeError insputils.build_vardatalist()during ScenarioNode construction before the latererror_list1/error_list2RuntimeError is raised. Consider raising immediately whenv is None, or skip appending/creating nodes until after validation succeeds.
dummy_varlist[stage-1].append(v)
else:
error_list2.append((sname,vstr))
varlist[stage-1].append(v)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| user_surrogates = list(old_node.surrogate_vardatas) or None | ||
| user_ef_suppl = old_node.nonant_ef_suppl_vardata_list or None | ||
| new_node = scenario_tree.ScenarioNode( | ||
| old_node.name, | ||
| old_node.cond_prob, | ||
| old_node.stage, | ||
| old_node.cost_expression * self.number_admm_subproblems, | ||
| varlist[stage-1], | ||
| s, | ||
| nonant_ef_suppl_list=user_ef_suppl, | ||
| surrogate_nonant_list=user_surrogates) | ||
| _tag_dummies_as_surrogate(new_node, dummy_varlist[stage-1]) |
There was a problem hiding this comment.
When rebuilding each existing ScenarioNode, user_surrogates = list(old_node.surrogate_vardatas) converts a set-like ComponentSet to a list. This can produce non-deterministic ordering, which can change the positional layout of surrogate nonants across scenarios. Prefer preserving the original user-provided ordering by passing old_node.surrogate_nonant_list (and similarly old_node.nonant_ef_suppl_list) into the new ScenarioNode instead of reconstructing from the vardata sets/lists.
There was a problem hiding this comment.
Good catch to flag this, but node.surrogate_vardatas is a Pyomo ComponentSet, which is backed by a dict keyed by an id-based hasher. Iteration yields self._data.values(), so order is deterministic (Python dict insertion order) — list(old_node.surrogate_vardatas) would have preserved the order it was built in.
That said, the edit is still worth taking: passing the raw user surrogate_nonant_list / nonant_ef_suppl_list avoids the vardata round-trip and matches what the user originally supplied, so the change landed as suggested.
| # try quote wrap around variable name | ||
| vvstr = vstr[:vstr.find('[')] | ||
| tvstr = vstr[vstr.find('[')+1:vstr.find(']')] | ||
| v = s.find_component(vvstr+'["'+tvstr+'"]') |
There was a problem hiding this comment.
The quote-wrapping fallback assumes vstr contains [...] and uses find('[') / find(']') unconditionally. If vstr has no brackets, find returns -1 and this will generate a truncated/invalid component name. Guard this logic with an explicit bracket check (or use parsing that can handle missing/complex indices) before attempting the wrapped lookup.
| # try quote wrap around variable name | |
| vvstr = vstr[:vstr.find('[')] | |
| tvstr = vstr[vstr.find('[')+1:vstr.find(']')] | |
| v = s.find_component(vvstr+'["'+tvstr+'"]') | |
| # try quote wrap around variable name when vstr has a | |
| # simple bracketed index (e.g., name[index]) | |
| lbracket = vstr.find('[') | |
| rbracket = vstr.find(']') | |
| if lbracket != -1 and rbracket != -1 and lbracket < rbracket: | |
| vvstr = vstr[:lbracket] | |
| tvstr = vstr[lbracket+1:rbracket] | |
| v = s.find_component(vvstr+'["'+tvstr+'"]') |
| "For now, stage2EFsolvern is required for multistage xhat" | ||
| if cfg.xhatshuffle and cfg.get("stage2_ef_solver_name") is None: | ||
| import warnings | ||
| warnings.warn("stage2_ef_solver_name is recommended for multistage xhatshuffle") |
There was a problem hiding this comment.
The new warnings.warn(...) will point at parsing.py as the source. Setting stacklevel=2 (and optionally a warning category) will make the warning more actionable by pointing users to their call site / CLI invocation that triggered multistage xhatshuffle without stage2_ef_solver_name.
| warnings.warn("stage2_ef_solver_name is recommended for multistage xhatshuffle") | |
| warnings.warn( | |
| "stage2_ef_solver_name is recommended for multistage xhatshuffle", | |
| UserWarning, | |
| stacklevel=2, | |
| ) |
| cfg.add_to_config(name ="stage2_ef_solver_name", | ||
| description="Solver to use for xhatlooper stage2ef option (default None)", | ||
| domain = str, | ||
| default=None) |
There was a problem hiding this comment.
cfg.multistage() now registers stage2_ef_solver_name (via Config.add_stage2_ef_solver_name_arg()), so this manual add_to_config(name="stage2_ef_solver_name", ...) is redundant and will be silently ignored as a duplicate. Consider removing it (or switching to the shared helper) to avoid confusion about which option definition/description is actually active.
| cfg.add_to_config(name ="stage2_ef_solver_name", | |
| description="Solver to use for xhatlooper stage2ef option (default None)", | |
| domain = str, | |
| default=None) |
| cfg.add_to_config(name ="stage2_ef_solver_name", | ||
| description="Solver to use for xhatlooper stage2ef option (default None)", | ||
| domain = str, | ||
| default=None) |
There was a problem hiding this comment.
This file calls cfg.multistage() (which now registers stage2_ef_solver_name) and then manually calls add_to_config(name="stage2_ef_solver_name", ...) again. Since Config.add_to_config ignores duplicates, this second declaration is a no-op and can be misleading—consider removing it or using the shared helper consistently.
Co-authored-by: bknueven <30801372+bknueven@users.noreply.github.com>
The Pyomo#652 changes made admm_subproblem_names_creator and stoch_scenario_names_creator take cfg. Update the pre-pickle pipeline test (brought in from upstream/main via this merge) to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- stoch_admmWrapper: pass user's raw surrogate_nonant_list / nonant_ef_suppl_list into rebuilt ScenarioNodes (not the rebuilt ComponentSet/vardata lists) so ordering stays deterministic. - stoch_admmWrapper: guard the bracket-wrap fallback in assign_variable_probs so malformed vstr without '[' / ']' doesn't produce a truncated component name. - generic/parsing: give the multistage xhatshuffle stage2 warning a UserWarning category and stacklevel=2 so it points at the caller. - examples/hydro: drop the redundant stage2_ef_solver_name add_to_config calls; cfg.multistage() / xhatshuffle_args() already register it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
It collides visually with the hub-and-spoke terminology used throughout the codebase. Use "custom" instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Combined merge of #651 and #652 with #651 taking precedence on naming.
From #651 (unchanged)
stage2EFsolvern→stage2_ef_solver_namethroughout.Config.xhatshuffle_args()so--stage2-ef-solver-nameis available fromgeneric_cylinders.py.mpisppy/generic/parsing.py.test_xhatshuffle_stage2efMPI test.From #652 (most of it, renamed to match #651)
admm_subproblem_names_creatorandstoch_scenario_names_creatornow take thecfgobject instead of specific scalar args (enables use cases like CEP that split by date range).add_stage2_ef_solver_name_arg()helper registered from bothmultistage()andadmm_args(), with dedup guards so calling from multiple CLIs doesn't error.add_branching_factors()also guarded against double-registration.find_componentfallback inStoch_AdmmWrapper.assign_variable_probsto quote-wrap bracketed index strings.get_scenario_unscaled()helper onStoch_AdmmWrapper(covered by new test).mpisppy/spopt.py.Dropped from #652
Stoch_AdmmWrapper(would have made every rank hold all scenarios — suspected regression).xhatxbar_bounder.py.Conflict resolution
add_stage2EF()helper renamed toadd_stage2_ef_solver_name_arg()and key changed tostage2_ef_solver_nameto match 651's convention.multistage(),xhatshuffle_args(), andadmm_args()all go through the guarded helper; no duplicate-key errors.Test plan
ruff check .cleanpython -m unittest mpisppy.tests.test_admm_bundler— 18/18 passpython -m unittest mpisppy.tests.test_config— passpython -m unittest mpisppy.tests.test_stoch_admmWrapper.TestStochAdmmWrapper.test_get_scenario_unscaled— passmpiexec -np 2 python -m mpi4py -m unittest mpisppy.tests.test_with_cylinders.Test_hydro_with_cylinders.test_xhatshuffle_stage2ef— passNote: a few pre-existing failures on
upstream/main(test_scenario_lpwriter_extension,test_admmWrapper.test_values,test_with_cylinders.test_xhatshuffle_coverage) are unaffected by this merge.Addendum
stoch-admm EF partial-consensus fix + user-surrogate preservation