Skip to content

Add --iter0-from-pickle; has_variable_probability unpickle-safety; ADMM pickling docs#656

Open
DLWoodruff wants to merge 34 commits intoPyomo:mainfrom
DLWoodruff:iter0_from_pickle
Open

Add --iter0-from-pickle; has_variable_probability unpickle-safety; ADMM pickling docs#656
DLWoodruff wants to merge 34 commits intoPyomo:mainfrom
DLWoodruff:iter0_from_pickle

Conversation

@DLWoodruff
Copy link
Copy Markdown
Collaborator

@DLWoodruff DLWoodruff commented Apr 17, 2026

HEY!!

Merge 653 first, even before you look at the Files changed.

Summary

  • Adds --iter0-from-pickle: PH skips its iter0 solve loop when the pickled scenario metadata carries iter0_before_pickle=True, using the pickled solution values as the iter0 warm start. Hard-fails if any local scenario is missing the metadata.
  • End-to-end tests (round-trip happy path + hard-fail path) in test_pre_pickle_pipeline.py::TestEndToEnd.
  • Defensive fix: _use_variable_probability_setter no longer clobbers a pickled has_variable_probability = True, so scenarios pickled under --admm / --stoch-admm can in principle be read back without those flags.
  • Documentation: pickling.rst gains an ADMM section; generic_admm.rst and stoch_admmWrapper.rst cross-link to it. Current status is documented explicitly — the programmatic pipeline handles ADMM (covered by test_pipeline_runs_on_admm_bundles); CLI end-to-end remains blocked at the compatibility check, with the three plumbing items needed to unblock it spelled out.

Base / stacking

This PR is stacked on #653 — the branch includes #653's commits until it merges. After #653 lands, I will rebase onto main and force-push; the diff will shrink to the 6 iter0-from-pickle commits.

Test plan

  • ruff check . clean
  • python -m unittest mpisppy.tests.test_admm_bundler — pass
  • python -m unittest mpisppy.tests.test_pre_pickle_pipeline — 11 pass (includes new round-trip + hard-fail)
  • cd doc && make html — no new warnings
  • Full CI

🤖 Generated with Claude Code

Bernard Knueven and others added 30 commits August 15, 2025 12:14
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>
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>
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>
Tier 2 of the pre-pickle preprocessing design (doc/pre_pickle_pre
processing_design.md). When the user pickles with --iter0-before-pickle
the variable values + duals + objective bound are baked into the
pickle. With --iter0-from-pickle, PHBase.Iter0 now skips its solve_loop
call and reuses those values directly: it validates that every local
scenario carries pickle_metadata['iter0_before_pickle']==True (hard
fails otherwise), then sets solution_available, outer_bound, and
inner_bound on each scenario from pyo.value(self.saved_objectives[
sname]). The rest of Iter0 -- E1 update, feasibility check, post_iter0
extension hook, Ebound, rho_setter -- runs unchanged because PH's
downstream logic only needs the variable values that the pickle
already provides.

Wires the new flag through cfg.pre_pickle_args and shared_options so
it flows from the cylinder driver to PHBase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds two end-to-end subprocess tests under
test_pre_pickle_pipeline.py::TestEndToEnd:

- test_iter0_from_pickle_skips_solve_loop pickles farmer scenarios
  with --iter0-before-pickle, then runs PH with --iter0-from-pickle
  and asserts that the run completes and the "Skipping PHBase.Iter0
  solve loop" message appears in stdout.

- test_iter0_from_pickle_without_pickled_iter0_fails pickles WITHOUT
  --iter0-before-pickle and verifies that --iter0-from-pickle hard-
  fails on the run side rather than fabricating solver state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DLWoodruff and others added 4 commits April 17, 2026 15:35
The page used to describe --iter0-from-pickle as a forthcoming flag.
Tier 2 is now implemented (commit 95838a5), so reword the relevant
subsection to describe the actual behavior, document the hard-fail
contract (every local scenario must carry the pre-pickle iter0 flag),
and add a worked pickle->run example.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The pickling_design_docs base branch added a --python-args parser shim
to test_pre_pickle_pipeline.py and threaded it into the v1 TestEndToEnd
methods so the dedicated coverage CI job can capture coverage from the
spawned subprocesses. The tier 2 test methods on this branch (added
before that change landed on the base) still hard-coded f"{python} -m
mpisppy.generic_cylinders ...", so their subprocess invocations would
not have been coverage-instrumented in the new pre-pickle-pipeline CI
job.

Mirror the v1 form so all four TestEndToEnd subprocess invocations
look the same and the new CI job picks up coverage uniformly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When unpickling ADMM scenarios without --admm/--stoch-admm,
prob_coeff/prob0_mask arrays survive because SPBase guards their
init with hasattr, but the has_variable_probability flag was being
clobbered to False. That trips gating logic in convergers and APH
despite the arrays actually being non-uniform. Only default the
flag to False if it wasn't already set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pre-pickle pipeline handles ADMM-wrapped scenarios correctly at
the programmatic level (covered by test_pipeline_runs_on_admm_bundles
in test_pre_pickle_pipeline.py). Driving pickling of
--admm / --stoch-admm through generic_cylinders is still blocked:
write_scenarios uses module.scenario_names_creator (not the ADMM
combined names), _build_pickle_pipeline_spbase does not plumb
variable_probability, and the unpickle side has no way to discover
ADMM-combined names in the pickle dir. Documented in pickling.rst
with cross-references from generic_admm.rst and stoch_admmWrapper.rst
so readers hit the status note from either side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 17, 2026

Codecov Report

❌ Patch coverage is 86.58537% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.78%. Comparing base (b726347) to head (154bfda).

Files with missing lines Patch % Lines
mpisppy/generic/admm.py 33.33% 4 Missing ⚠️
mpisppy/utils/sputils.py 25.00% 3 Missing ⚠️
mpisppy/phbase.py 91.66% 2 Missing ⚠️
mpisppy/generic_cylinders.py 0.00% 1 Missing ⚠️
mpisppy/opt/fwph.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #656      +/-   ##
==========================================
+ Coverage   70.71%   70.78%   +0.07%     
==========================================
  Files         153      153              
  Lines       19005    19061      +56     
==========================================
+ Hits        13439    13492      +53     
- Misses       5566     5569       +3     

☔ View full report in Codecov by Sentry.
📢 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.

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