Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3bbbb32

Browse files
authoredJan 31, 2025··
Merge branch 'main' into fix_xhatshuffle
2 parents 169fe3e + 4f0d94f commit 3bbbb32

File tree

8 files changed

+98
-37
lines changed

8 files changed

+98
-37
lines changed
 

‎doc/src/properbundles.rst

+11-5
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,22 @@ there are two modules that have most of the support for proper bundles:
6868
- ``mpisppy.utils.proper_bundler.py`` has wrappers for cylinder programs
6969

7070

71-
Multistage and notes
72-
--------------------
71+
Multistage
72+
----------
7373

74-
At the time of this writing, multi-stage proper, pickled bundles is a
75-
little bit beyond the bleeding edge. The idea is that bundles are
76-
formed and then saved as dill pickle files for rapid retrieval. The
74+
The most flexible way to create proper bundles is to write
75+
your own problem-specific code to do it. The
7776
file ``aircond_cylinders.py`` in the aircond example directory
7877
provides an example. The latter part of the ``allways.bash`` script
7978
demonstrates how to run it.
8079

80+
There is support for multi-stage bundles in mpi-sppy, but the scenario
81+
probabilities must be uniform and the bundles must span the same number
82+
of entire second stage nodes.
83+
84+
Notes
85+
-----
86+
8187
Pickled bundles are clearly useful for algorithm tuning and algorithm
8288
experimentation. In some, but not all, settings they can also improve
8389
wall-clock performance for a single optimization run. The pickler

‎examples/generic_cylinders.bash

+15-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@
44
SOLVER="cplex"
55
SPB=1
66

7+
echo "^^^ Multi-stage AirCond ^^^"
8+
mpiexec -np 3 python -m mpi4py ../mpisppy/generic_cylinders.py --module-name mpisppy.tests.examples.aircond --branching-factors "3 3 3" --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatxbar --rel-gap 0.01 --solution-base-name aircond_nonants
9+
# --xhatshuffle --stag2EFsolvern
10+
11+
echo "^^^ Multi-stage AirCond, pickle the scenarios ^^^"
12+
mpiexec -np 3 python -m mpi4py ../mpisppy/generic_cylinders.py --module-name mpisppy.tests.examples.aircond --branching-factors "3 3 3" --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatxbar --rel-gap 0.01 --solution-base-name aircond_nonants --pickle-scenarios-dir aircond/pickles
13+
14+
echo "^^^ Multi-stage AirCond, bundle the scenarios ^^^"
15+
mpiexec -np 3 python -m mpi4py ../mpisppy/generic_cylinders.py --module-name mpisppy.tests.examples.aircond --branching-factors "3 3 3" --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatxbar --rel-gap 0.01 --solution-base-name aircond_nonants --scenarios-per-bundle 9
16+
17+
echo "^^^ Multi-stage AirCond, bundle the scenarios and write ^^^"
18+
mpiexec -np 3 python -m mpi4py ../mpisppy/generic_cylinders.py --module-name mpisppy.tests.examples.aircond --branching-factors "3 3 3" --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatxbar --rel-gap 0.01 --solution-base-name aircond_nonants --pickle-scenarios-dir aircond/pickles --scenarios-per-bundle 9
19+
20+
#### HEY! check on error messages for bad bundle sizes
21+
722
echo "^^^ write scenario lp and nonant json files ^^^"
823
cd sizes
924
python ../../mpisppy/generic_cylinders.py --module-name sizes --num-scens 3 --default-rho 1 --solver-name ${SOLVER} --max-iterations 0 --scenario-lpfiles
@@ -20,8 +35,6 @@ echo "^^^ unpickle the sizes bundles and write the lp and nonant files ^^^"
2035
cd sizes
2136
python ../../mpisppy/generic_cylinders.py --module-name sizes --num-scens 10 --default-rho 1 --solver-name ${SOLVER} --max-iterations 0 --scenario-lpfiles --unpickle-bundles-dir sizes_pickles --scenarios-per-bundle 5
2237
cd ..
23-
echo "xxxx Early exit. xxxx"
24-
exit
2538

2639
echo "^^^ pickle the scenarios ^^^"
2740
cd farmer

‎examples/generic_tester.py

+6
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ def do_one(dirname, modname, np, argstring, xhat_baseline_dir=None, tol=1e-6):
163163
#rebaseline_xhat("hydro", "hydro", 3, hydroa, "test_data/hydroa_baseline")
164164
do_one("hydro", "hydro", 3, hydroa, xhat_baseline_dir="test_data/hydroa_baseline")
165165

166+
# write hydro bundles for at least some testing of multi-stage proper bundles
167+
# (just looking for smoke)
168+
hydro_wr = ("--pickle-bundles-dir hydro_pickles --scenarios-per-bundle 3"
169+
"--branching-factors '3 3' ")
170+
do_one("hydro", "hydro", 3, hydro_wr, xhat_baseline_dir=None)
171+
166172
# write, then read, pickled scenarios
167173
print("starting write/read pickled scenarios")
168174
farmer_wr = "--pickle-scenarios-dir farmer_pickles --crops-mult 2 --num-scens 10"

‎mpisppy/generic_cylinders.py

-2
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,6 @@ def _name_lists(module, cfg, bundle_wrapper=None):
9696
num_scens = np.prod(cfg.branching_factors)
9797
assert not cfg.xhatshuffle or cfg.get("stage2EFsolvern") is not None,\
9898
"For now, stage2EFsolvern is required for multistage xhat"
99-
assert cfg.scenarios_per_bundle is None, "proper bundles in generic_cylinders does not yet support multistage"
100-
10199
else:
102100
all_nodenames = None
103101
num_scens = cfg.num_scens

‎mpisppy/spopt.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,9 @@ def _vb(msg):
189189
if Ag is not None:
190190
assert not disable_pyomo_signal_handling, "Not thinking about agnostic APH yet"
191191
kws = {"s": s, "solve_keyword_args": solve_keyword_args, "gripe": gripe, "tee": tee, "need_solution": need_solution}
192-
Ag.callout_agnostic(kws)
192+
Ag.callout_agnostic(kws) # not going to use the return values
193193
else:
194+
# didcallout = False (returned true by the callout, but not used)
194195
try:
195196
results = s._solver_plugin.solve(s,
196197
**solve_keyword_args,

‎mpisppy/tests/examples/aircond.py

+24-14
Original file line numberDiff line numberDiff line change
@@ -319,17 +319,8 @@ def scenario_creator(sname, **kwargs):
319319

320320
#Constructing the nodes used by the scenario
321321
model._mpisppy_node_list = MakeNodesforScen(model, nodenames, branching_factors)
322-
model._mpisppy_probability = 1 / np.prod(branching_factors)
323-
"""
324-
from mpisppy import MPI
325-
326-
comm = MPI.COMM_WORLD
327-
rank = comm.Get_rank()
328-
if rank == 0:
329-
with open("efmodel.txt", "w") as fileh:
330-
model.pprint(fileh)
331-
quit()
332-
"""
322+
#model._mpisppy_probability = 1 / np.prod(branching_factors)
323+
model._mpisppy_probability = "uniform"
333324
return(model)
334325

335326

@@ -427,7 +418,12 @@ def kw_creator(cfg, optionsin=None):
427418
create the key word arguments for the creator fucntion(s)
428419
args:
429420
cfg (Config object): probably has parsed values
430-
optionsin (dict): programmatic options"""
421+
optionsin (dict): programmatic options
422+
returns:
423+
kwargs (dict): the keyword args
424+
side-effect:
425+
checks and/or adds cfg.num_scens
426+
"""
431427
# use an empty dict instead of None
432428
options = optionsin if optionsin is not None else dict()
433429
if "kwargs" in options:
@@ -443,7 +439,7 @@ def _kwarg(option_name, default = None, arg_name=None):
443439
return
444440
# if not in the options, see if it is availalbe globally
445441
aname = option_name if arg_name is None else arg_name
446-
retval = getattr(cfg, aname) if hasattr(cfg, aname) else None
442+
retval = cfg.get(aname)
447443
retval = default if retval is None else retval
448444
kwargs[option_name] = retval
449445

@@ -457,7 +453,21 @@ def _kwarg(option_name, default = None, arg_name=None):
457453
raise ValueError(f"kw_creator called, but no value given for start_ups, options={options}")
458454
if kwargs["start_seed"] is None:
459455
raise ValueError(f"kw_creator called, but no value given for start_seed, options={options}")
460-
456+
if kwargs["branching_factors"] is not None:
457+
BFs = kwargs["branching_factors"]
458+
ns = cfg.get("num_scens")
459+
if BFs is not None:
460+
if ns is None:
461+
cfg.add_and_assign("num_scens",
462+
description="Number of scenarios",
463+
domain=int,
464+
value=np.prod(BFs),
465+
default=None
466+
)
467+
else:
468+
pass # we cannot do the assert because of tree sampling
469+
#assert ns == np.prod(BFs),\
470+
#f"num_scens != prod(BFs); {ns=}, {BFs=}"
461471
return kwargs
462472

463473

‎mpisppy/tests/test_pickle_bundle.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for
77
# full copyright and license information.
88
###############################################################################
9-
# Provide some test for pickled bundles
9+
# Provide a test for special-purpose (aircond only)multi-stage pickled bundles
1010
"""
1111
1212
"""

‎mpisppy/utils/proper_bundler.py

+39-12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# TBD: we should consider restructuring this and moving the capability to spbase
1111

1212
import os
13+
import numpy as np
1314
import mpisppy.utils.sputils as sputils
1415
import mpisppy.utils.pickle_bundle as pickle_bundle
1516

@@ -19,8 +20,12 @@
1920
# - read a bundle
2021
# - make a bundle
2122
# - make a bundle and write it
22-
# Multi-stage (as of Sept 2024) not supported in class or generic_cylinders
23-
# You need to do something clever like in aircondB
23+
# Multi-stage
24+
# Is not very special because all we do is make sure that
25+
# bundles cover entire second-stage nodes so the new bundled
26+
# problem is a two-stage problem no matter how many stages
27+
# were in the original problem. As of Dec 2024, support
28+
# is provided only when there are branching factors.
2429
# NOTE:: the caller needs to make sure it is two stage
2530
# the caller needs to worry about what is in what rank
2631
# (local_scenarios might have bundle names, e.g.)
@@ -50,14 +55,28 @@ def scenario_names_creator(self, num_scens, start=None, cfg=None):
5055
return cfg.model.scenario_names_creator(num_scens, start=start)
5156

5257
def bundle_names_creator(self, num_buns, start=None, cfg=None):
58+
59+
def _multistage_check(bunsize):
60+
# returns bunBFs as a side-effect
61+
BFs = cfg.branching_factors
62+
beyond2size = np.prod(BFs[1:])
63+
if bunsize % beyond2size!= 0:
64+
raise RuntimeError(f"Bundles must consume the same number of entire second stage nodes: {beyond2size=} {bunsize=}")
65+
# we need bunBFs for EF formulation
66+
self.bunBFs = [bunsize // beyond2size] + BFs[1:]
67+
5368
# start refers to the bundle number; bundles are always zero-based
5469
if start is None:
5570
start = 0
5671
assert cfg is not None, "ProperBundler needs cfg for bundle names"
5772
assert cfg.get("num_scens") is not None
5873
assert cfg.get("scenarios_per_bundle") is not None
59-
assert cfg.num_scens % cfg.scenarios_per_bundle == 0
74+
assert cfg.num_scens % cfg.scenarios_per_bundle == 0, "Bundles must consume the same number of entire second stage nodes: {cfg.num_scens=} {bunsize=}"
6075
bsize = cfg.scenarios_per_bundle # typing aid
76+
if cfg.get("branching_factors") is not None:
77+
_multistage_check(bsize)
78+
else:
79+
self.bunBFs = None
6180
# We need to know if scenarios (not bundles) are one-based.
6281
inum = sputils.extract_num(self.module.scenario_names_creator(1)[0])
6382
names = [f"Bundle_{bn*bsize+inum}_{(bn+1)*bsize-1+inum}" for bn in range(start+num_buns)]
@@ -80,42 +99,50 @@ def scenario_creator(self, sname, **kwargs):
8099
cfg = kwargs["cfg"]
81100
if "scen" in sname or "Scen" in sname:
82101
# In case the user passes in kwargs from scenario_creator_kwargs.
83-
return self.module.scenario_creator(sname, {**self.original_kwargs, **kwargs})
102+
return self.module.scenario_creator(sname, **{**self.original_kwargs, **kwargs})
84103

85104
elif "Bundle" in sname and cfg.get("unpickle_bundles_dir") is not None:
86105
fname = os.path.join(cfg.unpickle_bundles_dir, sname+".pkl")
87106
bundle = pickle_bundle.dill_unpickle(fname)
88107
return bundle
89108
elif "Bundle" in sname and cfg.get("unpickle_bundles_dir") is None:
90-
# this is also the branch for proper_no_files
91109
# If we are still here, we have to create the bundle.
92110
firstnum = int(sname.split("_")[1]) # sname is a bundle name
93111
lastnum = int(sname.split("_")[2])
94112
# snames are scenario names
95113
snames = self.module.scenario_names_creator(lastnum-firstnum+1,
96114
firstnum)
115+
kws = self.original_kwargs
116+
if self.bunBFs is not None:
117+
# The original scenario creator needs to handle these
118+
kws["branching_factors"] = self.bunBFs
119+
97120
# We are assuming seeds are managed by the *scenario* creator.
98121
bundle = sputils.create_EF(snames, self.module.scenario_creator,
99-
scenario_creator_kwargs=self.original_kwargs,
122+
scenario_creator_kwargs=kws,
100123
EF_name=sname,
101124
suppress_warnings=True,
102125
nonant_for_fixed_vars = False)
103126

104127
nonantlist = [v for idx, v in bundle.ref_vars.items() if idx[0] =="ROOT"]
105128
# if the original scenarios were uniform, this needs to be also
106129
# (EF formation will recompute for the bundle if uniform)
130+
# Get an arbitrary scenario.
107131
scen = self.module.scenario_creator(snames[0], **self.original_kwargs)
108132
if scen._mpisppy_probability == "uniform":
109133
bprob = "uniform"
110134
else:
111-
bprob = bundle._mpisppy_probability
135+
raise RuntimeError("Proper bundles created by proper_bundle.py require uniform probability (consider creating problem-specific bundles)")
136+
bprob = bundle._mpisppy_probability
112137
sputils.attach_root_node(bundle, 0, nonantlist)
113138
bundle._mpisppy_probability = bprob
139+
140+
if len(scen._mpisppy_node_list) > 1 and self.bunBFs is None:
141+
raise RuntimeError("You are creating proper bundles for a\n"
142+
"multi-stage problem, but without cfg.branching_factors.\n"
143+
"We need branching factors and all bundles must cover\n"
144+
"the same number of entire second stage nodes.\n"
145+
)
114146
return bundle
115147
else:
116148
raise RuntimeError (f"Scenario name does not have scen or Bundle: {sname}")
117-
118-
119-
120-
121-

0 commit comments

Comments
 (0)
Please sign in to comment.