stoch-admm phase B: consensus_vars ergonomics + first-stage hook extensions#722
stoch-admm phase B: consensus_vars ergonomics + first-stage hook extensions#722DLWoodruff wants to merge 7 commits into
Conversation
Each entry of consensus_vars may now be a Pyomo Var / VarData object (or a (Var, stage) tuple for --stoch-admm) instead of a name string; mixed lists are allowed for migration. Eliminates the "y[(DC1, DC2)]" vs "y['DC1', 'DC2']" string-formatting foot-gun documented in design item B.1. The wrappers normalize via the new private helper mpisppy.utils.admmWrapper._admm_normalize_consensus_vars, called once in __init__ before _consensus_vars_number_creator runs. All subsequent code paths (find_component, dummy var attachment) see strings exactly as before. Caveat: Pyomo VarData holds its parent block via a weakref, so the .name lookup is valid only if the source scenario is still alive at wrapper-construction time. Callers that build temporary scenarios in a helper must keep references to them across the wrapper call. Documented in the helper's docstring. Tests: six unit tests of the helper directly (TestAdmmConsensusVarsNormalize), plus integration tests confirming AdmmWrapper / Stoch_AdmmWrapper produce the same varprob_dict and node lists with Var-form vs string-form consensus_vars. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cabulary
Phase B.2 (the implementation change)
-------------------------------------
When the first_stage_cost / first_stage_varlist hooks are defined,
Stoch_AdmmWrapper and AdmmBundler now auto-merge each ADMM
subproblem's first-stage Vars into its consensus_vars entry at
wrapper-construction time, so consensus_vars_creator can return
only the admm-consensus Vars. Eliminates the throwaway
before-wrap-scenario instantiation that examples/stoch_distr did
inside consensus_vars_creator just to read first-stage Vars off a
model that had not yet had its scenario tree attached.
Per-subproblem semantics: different ADMM subproblems may carry
different first-stage Vars (e.g., one region owns its factories,
another owns its own), so names are gathered per-ADMM-subproblem.
Stoch_AdmmWrapper reuses already-built before-wrap scenarios where
possible and probes a fresh before-wrap scenario only for ADMM
subproblems the local rank does not host; AdmmBundler always
probes per-ADMM-subproblem (no before-wrap scenarios are built in
its __init__).
This is setup, not wrap: the merge happens once at wrapper
construction, not per wrapped scenario.
New helper: _merge_first_stage_into_consensus_vars(consensus_vars,
first_stage_var_names_per_sub, root_stage=1) in admmWrapper.py.
ADMM vocabulary glossary
------------------------
Added a module-level glossary to mpisppy/utils/admmWrapper.py
locking in:
- before-wrap / wrapped scenario (lifecycle of the Pyomo model
the user's scenario_creator returns and the wrapper consumes)
- wrap (verb, narrow scope: ADMM-specific transformation only;
excludes sputils.attach_root_node, which is setup)
- the wrapper, stochastic scenario (paper xi), ADMM subproblem
(paper a), first-stage Vars, root-node Vars
Rewrote all B.1 + B.2 docstrings and comments across the ADMM
family, examples/stoch_distr/stoch_distr.py, and the three admm
test files to use this vocabulary consistently. Test files keep
phase-counter prefixes (B.1, B.2, ...) on their docstrings, gated
by a header comment pointing at
doc/designs/admm_user_api_automation_design.md; production code
describes rationale directly without phase tags.
Tests
-----
- TestMergeFirstStageIntoConsensusVars (admmWrapper):
basic merge, de-dup, per-sub semantics, missing-sub no-op.
- test_b2_auto_merge_first_stage_into_consensus_vars and
test_b2_per_subproblem_first_stage_vars (stoch_admmWrapper):
end-to-end auto-merge with the synthetic minimal scenarios,
including the per-subproblem case.
- TestAdmmBundlerB2AutoMerge: same for AdmmBundler with a
synthetic module.
- test_hooks_and_legacy_paths_produce_same_node_list updated to
pre-merge first-stage Vars into the legacy consensus_vars so
the hooks-vs-legacy comparison stays apples-to-apples post-B.2.
65/65 ADMM-suite tests pass (one pre-existing flaky test_values
deselected); examples/stoch_distr/stoch_admm_stage2ef.bash
converges (0.884% rel gap with xpress).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add two optional module-level hooks that forward to sputils.attach_root_node's surrogate_nonant_list and nonant_ef_suppl_list parameters respectively: - first_stage_surrogate_nonant_list(scenario) - first_stage_nonant_ef_suppl_list(scenario) Each is independent of the other (either may be defined alone), but both require the two core hooks (first_stage_cost, first_stage_varlist) to also be defined. Without B.3, users with surrogate or EF-supplemental nonants were forced back to the legacy manual-attach_root_node path; they can now stay on the recommended hook-driven API. Plumbing -------- - Stoch_AdmmWrapper.__init__: accept the two new optional kwargs; forward to attach_root_node when wrapping each before-wrap scenario. Validate advanced-without-core at construction time. - AdmmBundler.__init__ / _process_scenario: same plumbing for the bundled path. Each constituent before-wrap scenario gets the advanced lists forwarded to its attach_root_node call. - mpisppy/generic/admm.py: extracted both-or-neither / advanced- needs-core validation into _discover_first_stage_hooks, used by both setup_stoch_admm and setup_stoch_admm_with_bundles. Removed the duplicated discovery+validate blocks they each previously carried. Docs ---- - doc/src/generic_admm.rst: new "Advanced first-stage hooks (optional)" subsection documenting both new hooks and the advanced-needs-core rule. Tests ----- - test_stoch_admmWrapper.py (TestStochAdmmWrapperFirstStageHooks): test_advanced_hooks_forwarded (both hooks, end-to-end attach), test_advanced_hook_alone_ok (one of two is fine), test_advanced_hook_without_core_hooks_errors (wrapper-level), test_setup_stoch_admm_advanced_without_core_errors (module-level discovery). - test_admm_bundler.py (TestAdmmBundlerB3AdvancedHooks): spy-based forwarding test (the bundle has its own separate ROOT, so surrogates live on the per-constituent roots the EF reads from), and the bundler-level advanced-without-core error. 71/71 ADMM-suite tests pass; ruff clean; doc builds clean (the four pre-existing warnings are unrelated). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Append "Findings during implementation" subsections to each of B.1,
B.2, B.3 in the phased design doc, capturing what differed from the
original design draft now that the three commits are on this branch:
- B.1: Pyomo VarData weakref gotcha (.name needs the source
before-wrap scenario alive at wrapper-construction time);
normalization location moved from assign_variable_probs to a
single helper called in __init__.
- B.2: significant divergence from the original draft -- first-stage
Vars are per-ADMM-subproblem, not global, so the helper takes a
per-subproblem dict instead of a flat list. Documented per-rank
gathering (local before-wrap scenarios + probes for absent
subproblems) and the new vocabulary glossary established as a
fallout of writing precise B.2 prose.
- B.3: bundle ROOT does not propagate constituent surrogates -- the
advanced lists live on per-constituent before-wrap scenario roots,
not on the bundle's flattened ROOT (EF reads from constituents
when assembling). Discovery+validation extracted into a shared
_discover_first_stage_hooks helper used by both setup_stoch_admm
and setup_stoch_admm_with_bundles.
Original "Proposed" prose preserved so the design-vs-implementation
comparison stays readable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ic_admm scenario_creator.rst already documents nonant_ef_suppl_list and surrogate_nonant_list under "EF Supplement List" and "Surrogate Nonant List". Add Sphinx labels (_ef_supplement_list, _surrogate_nonant_list) and have generic_admm.rst's new "Advanced first-stage hooks (optional)" subsection cross-reference them via :ref:, so a reader meeting first_stage_surrogate_nonant_list / first_stage_nonant_ef_suppl_list for the first time can follow a link to the underlying attach_root_node parameter docs. Doc builds clean (the four pre-existing warnings are unrelated). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Advanced first-stage hooks (optional)" subsection only describes the hooks path; add a one-line clarifier pointing legacy-path users at passing surrogate_nonant_list / nonant_ef_suppl_list directly to their own sputils.attach_root_node call. Also extend the existing "First-stage attachment via manual attach_root_node (legacy)" subsection to mention the two advanced lists explicitly. Prod example examples/stoch_distr/stoch_distr.py is hooks-only; the legacy attach_root_node pattern lives in mpisppy/tests/examples/stoch_distr/stoch_distr.py for testing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #722 +/- ##
==========================================
+ Coverage 72.60% 72.71% +0.11%
==========================================
Files 162 162
Lines 20643 20720 +77
==========================================
+ Hits 14987 15066 +79
+ Misses 5656 5654 -2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
The B.2 probe-fallback path (build a fresh before-wrap scenario for each ADMM subproblem the local rank does not host) was previously exercised only by the real-MPI bash script examples/stoch_distr/stoch_admm_stage2ef.bash, which pytest's coverage tooling doesn't track because the run is launched as subprocesses. Add a unit test that uses a synthetic FakeMpicomm (Get_size=2, Get_rank=0) and admm-major scenario ordering so rank 0's slice covers only ADMM subproblem A. Verifies (1) the local slice is what we expect, (2) B's first-stage Var name appears in self.consensus_vars["B"] (so it must have come from a probe), and (3) scenario_creator is invoked 3 times -- 2 for local scenarios plus 1 for the B probe targeting the first B-flavored name in all_admm_stoch_subproblem_scenario_names. The AdmmBundler probe block was already exercised by the existing bundler tests (it always probes per-ADMM-subproblem in __init__ regardless of rank). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR implements Phase B of the stochastic-ADMM user-API automation plan by improving consensus_vars ergonomics, removing the need for throwaway scenarios to discover first-stage variables, and extending the first-stage hook mechanism to support surrogate and EF-supplemental nonants.
Changes:
- Accept Pyomo
Var/VarDataobjects (in addition to strings) inconsensus_vars, normalizing to.nameat wrapper construction time. - Auto-merge each ADMM subproblem’s first-stage variable names into its
consensus_varsentry when the Phase Afirst_stage_*hooks are used. - Add optional advanced first-stage hooks to forward
surrogate_nonant_list/nonant_ef_suppl_listintosputils.attach_root_node, with shared validation insetup_stoch_admm*.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| mpisppy/utils/stoch_admmWrapper.py | Normalizes consensus var identifiers, auto-merges first-stage vars per subproblem, and forwards advanced attach-root kwargs via new optional hooks. |
| mpisppy/utils/admmWrapper.py | Adds shared helpers for consensus-var normalization and first-stage auto-merge; applies normalization in AdmmWrapper. |
| mpisppy/utils/admm_bundler.py | Mirrors normalization/auto-merge behavior and forwards advanced attach-root kwargs for bundled stoch-ADMM. |
| mpisppy/generic/admm.py | Extracts shared hook discovery/validation into _discover_first_stage_hooks and forwards results into wrapper/bundler setup. |
| examples/stoch_distr/stoch_distr.py | Removes throwaway-scenario first-stage discovery in consensus_vars_creator, relying on wrapper auto-merge. |
| mpisppy/tests/test_stoch_admmWrapper.py | Adds/updates tests covering B.1 normalization, B.2 auto-merge (including per-subproblem + probe fallback), and B.3 advanced hook forwarding/validation. |
| mpisppy/tests/test_admmWrapper.py | Adds unit tests for normalization and first-stage merge helpers plus an end-to-end var-object consensus input test for AdmmWrapper. |
| mpisppy/tests/test_admm_bundler.py | Adds tests for bundler B.2 auto-merge semantics and B.3 advanced hook forwarding/validation. |
| doc/src/scenario_creator.rst | Adds Sphinx labels to enable cross-references for EF supplement / surrogate nonant documentation. |
| doc/src/generic_admm.rst | Documents the two new advanced first-stage hooks and clarifies legacy/manual attach behavior. |
| doc/designs/admm_user_api_automation_design.md | Records implementation findings and clarifies Phase B semantics (per-subproblem first-stage vars, probe behavior, weakref caveat). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Phase B of the stoch-admm user-API automation plan (
doc/designs/admm_user_api_automation_design.md), three closely related items that build on Phase A'sfirst_stage_cost/first_stage_varlistmodule hooks:consensus_vars_creatoraccepts PyomoVar/VarDatain addition to strings; the wrapper normalizes via.nameat construction time. Mixed lists OK for migration. Eliminates the\"flow[(DC1, DC2)]\"vs\"flow['DC1', 'DC2']\"formatting foot-gun.consensus_varsentry, soconsensus_vars_creatorcan return only the admm-consensus Vars. Eliminates the throwaway-model trickstoch_distr.py'sconsensus_vars_creatorused to walkfirst_stage_varlistfrom a freshly built before-wrap scenario.first_stage_surrogate_nonant_list/first_stage_nonant_ef_suppl_listmodule hooks that forward tosputils.attach_root_node's advanced parameters. Users of surrogates or EF-supplemental nonants are no longer forced back to the legacy manual-attach path.ADMM vocabulary glossary
Writing B.2's docstrings forced us to settle on consistent terms across the ADMM family. The full glossary now lives in the module docstring at the top of
mpisppy/utils/admmWrapper.pyand the three admm test files have header comments referencing it:scenario_creatorreturns vs. the same model after the wrapper has applied the wrap operation; what mpi-sppy iterates over.Stoch_AdmmWrapper) or flatten consensus Vars into ROOT (AdmmBundler), plus surrogate / probability bookkeeping. Does not coversputils.attach_root_node(that step is setup, not wrap).AdmmWrapper,Stoch_AdmmWrapper,AdmmBundler.admm_subproblem._mpisppy_node_list[0], the implementation-level term).All B.1 / B.2 / B.3 docstrings and comments — plus
examples/stoch_distr/stoch_distr.pyand the three admm test files — use this vocabulary consistently.Notable findings during implementation (recorded in the design doc)
VarDataweakref gotcha —.nameonly resolves to a real name if the source before-wrap scenario is alive at wrapper-construction time. Documented in the helper docstring; callers building throwaway scenarios in helpers must keep them alive across the wrapper call.sputils.attach_root_node.Files
Source
mpisppy/utils/admmWrapper.py— module docstring with vocabulary glossary; new helpers_admm_normalize_consensus_vars(consensus_vars, *, tuple_form)(B.1) and_merge_first_stage_into_consensus_vars(consensus_vars, first_stage_var_names_per_sub, root_stage=1)(B.2);AdmmWrapper.__init__calls the normalizer.mpisppy/utils/stoch_admmWrapper.py— B.1 normalizer + B.2 per-subproblem first-stage merge + B.3 advanced hooks (first_stage_surrogate_nonant_list,first_stage_nonant_ef_suppl_list) forwarded toattach_root_node.mpisppy/utils/admm_bundler.py— same B.1 / B.2 / B.3 plumbing. Builds one probe before-wrap scenario per ADMM subproblem at__init__since it does not pre-create scenarios there.mpisppy/generic/admm.py— extracted both-or-neither / advanced-needs-core validation into a shared_discover_first_stage_hooks(module)used by bothsetup_stoch_admmandsetup_stoch_admm_with_bundles.Example
examples/stoch_distr/stoch_distr.py—consensus_vars_creatorno longer instantiates a throwaway scenario; just returns the admm-consensus Vars. The wrapper merges first-stage Vars at construction time.Tests
mpisppy/tests/test_admmWrapper.py—TestAdmmConsensusVarsNormalize(6 unit tests of the B.1 normalizer),TestAdmmWrapperVarConsensusInputs(end-to-end B.1 onAdmmWrapper),TestMergeFirstStageIntoConsensusVars(4 unit tests of the B.2 merger).mpisppy/tests/test_stoch_admmWrapper.py—test_consensus_vars_accepts_var_objects(B.1),test_b2_auto_merge_first_stage_into_consensus_varsandtest_b2_per_subproblem_first_stage_vars(B.2),test_advanced_hooks_forwarded/test_advanced_hook_alone_ok/test_advanced_hook_without_core_hooks_errors/test_setup_stoch_admm_advanced_without_core_errors(B.3).test_hooks_and_legacy_paths_produce_same_node_listupdated to pre-merge first-stage Vars into the legacyconsensus_vars(apples-to-apples post-B.2).mpisppy/tests/test_admm_bundler.py—TestAdmmBundlerB2AutoMergeandTestAdmmBundlerB3AdvancedHooks.Docs
doc/designs/admm_user_api_automation_design.md— "Findings during implementation" subsections recording divergences from the original Phase B draft.doc/src/generic_admm.rst— new "Advanced first-stage hooks (optional)" subsection covering B.3, with:ref:cross-references to the existing "EF Supplement List" / "Surrogate Nonant List" sections inscenario_creator.rst(which gained Sphinx labels in this PR). The legacy-path subsection now explicitly mentions passingsurrogate_nonant_list/nonant_ef_suppl_listdirectly toattach_root_node.doc/src/scenario_creator.rst— added Sphinx labels_ef_supplement_listand_surrogate_nonant_listso the cross-references resolve.Test plan
Follow-up
Phases C, D, E in the design doc remain and can branch off main after this PR merges. None of them depend on Phase B's specific shape — they target
combining_names/split_admm_stoch_subproblem_scenario_namedefaults (C),admm_stoch_subproblem_scenario_names_creatordefaults (D), andinparser_adderboilerplate (E).🤖 Generated with Claude Code