Skip to content

Commit 0a2d4d4

Browse files
jeanpaulwatsonJean-Paul WatsonDLWoodruff
authored
Xbar-W reader writer options processing re-work (#488)
* Initial refactor of xbar-w reader and writer options processing * Improving name of xbar-w reader/writer test * Fixing typo * Fixing import name issue * Deprecation not necessary * Bug fix * add w writer and reader to generic_cylinders.bash * Adding user-defined extension processing to generic cylinders * Adding experts-only option to mess with hubdict at the last minute, prior to wheel spinning * Adding spoke dicts to callback * Bug fix * Band-aid for old farmer_clyinder driver - should be culled soon * Fix to test_gradient_rho to comply with latest option processing for xhat-w reader/writer. * Bug fix * put cfg in options for test_gradient * improve wxbar* handling of the activating cfg option * trying to handle cfg options correcitly in wxbar* * update tests for new cfg * not_active --> active --------- Co-authored-by: Jean-Paul Watson <[email protected]> Co-authored-by: David L. Woodruff <[email protected]> Co-authored-by: Dave Woodruff <[email protected]> Co-authored-by: David L Woodruff <[email protected]>
1 parent e487023 commit 0a2d4d4

11 files changed

+156
-99
lines changed

Diff for: .github/workflows/test_pr_and_main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ jobs:
407407
run: |
408408
cd mpisppy/tests
409409
python test_gradient_rho.py
410-
python test_w_writer.py
410+
python test_xbar_w_reader_writer.py
411411
412412
test-headers:
413413
name: header test

Diff for: examples/farmer/farmer_cylinders.py

+2
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ def main():
112112
ph_converger=ph_converger,
113113
rho_setter = rho_setter)
114114

115+
hub_dict['opt_kwargs']['options']['cfg'] = cfg
116+
115117
if cfg.primal_dual_converger:
116118
hub_dict['opt_kwargs']['options']\
117119
['primal_dual_converger_options'] = {

Diff for: examples/generic_cylinders.bash

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

7+
echo "^^^ hub only with w-writer (smoke) ^^^"
8+
python -m mpi4py ../mpisppy/generic_cylinders.py --module-name farmer/farmer --num-scens 3 --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --W-writer --W-fname w_values.csv
9+
10+
echo "^^^ hub only with w-reader (smoke) ^^^"
11+
python -m mpi4py ../mpisppy/generic_cylinders.py --module-name farmer/farmer --num-scens 3 --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --W-reader --init-W-fname w_values.csv
12+
713
echo "^^^ Multi-stage AirCond ^^^"
814
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
915
# --xhatshuffle --stag2EFsolvern

Diff for: mpisppy/generic_cylinders.py

+50-3
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,31 @@
1414
import shutil
1515
import numpy as np
1616
import pyomo.environ as pyo
17+
import pyomo.common.config as pyofig
18+
1719
from mpisppy.spin_the_wheel import WheelSpinner
20+
1821
import mpisppy.utils.cfg_vanilla as vanilla
1922
import mpisppy.utils.config as config
2023
import mpisppy.utils.sputils as sputils
24+
2125
from mpisppy.convergers.norm_rho_converger import NormRhoConverger
2226
from mpisppy.convergers.primal_dual_converger import PrimalDualConverger
27+
2328
from mpisppy.extensions.extension import MultiExtension
2429
from mpisppy.extensions.fixer import Fixer
2530
from mpisppy.extensions.mipgapper import Gapper
2631
from mpisppy.extensions.gradient_extension import Gradient_extension
2732
from mpisppy.extensions.scenario_lpfiles import Scenario_lpfiles
33+
34+
from mpisppy.utils.wxbarwriter import WXBarWriter
35+
from mpisppy.utils.wxbarreader import WXBarReader
36+
2837
import mpisppy.utils.solver_spec as solver_spec
38+
2939
from mpisppy import global_toc
3040
from mpisppy import MPI
3141

32-
3342
def _parse_args(m):
3443
# m is the model file module
3544
cfg = config.Config()
@@ -82,6 +91,18 @@ def _parse_args(m):
8291
cfg.coeff_rho_args()
8392
cfg.sensi_rho_args()
8493
cfg.reduced_costs_rho_args()
94+
95+
cfg.add_to_config("user_defined_extensions",
96+
description="Space-delimited module names for user extensions",
97+
domain=pyofig.ListOf(str),
98+
default=None)
99+
# TBD - think about adding directory for json options files
100+
101+
cfg.add_to_config("hub_and_spoke_dict_callback",
102+
description="[FOR EXPERTS ONLY] Module that contains the function hub_and_spoke_dict_callback that will be passed the hubdict and list of spokedicts prior to spin-the-wheel (last chance for intervention)",
103+
domain=str,
104+
default=None)
105+
85106
cfg.parse_command_line(f"mpi-sppy for {cfg.module_name}")
86107

87108
cfg.checker() # looks for inconsistencies
@@ -163,7 +184,11 @@ def _do_decomp(module, cfg, scenario_creator, scenario_creator_kwargs, scenario_
163184
rho_setter = rho_setter,
164185
all_nodenames = all_nodenames,
165186
)
166-
187+
188+
# the intent of the following is to transition to strictly
189+
# cfg-based option passing, as opposed to dictionary-based processing.
190+
hub_dict['opt_kwargs']['options']['cfg'] = cfg
191+
167192
# Extend and/or correct the vanilla dictionary
168193
ext_classes = list()
169194
# TBD: add cross_scenario_cuts, which also needs a cylinder
@@ -198,6 +223,24 @@ def _do_decomp(module, cfg, scenario_creator, scenario_creator_kwargs, scenario_
198223
if cfg.scenario_lpfiles:
199224
ext_classes.append(Scenario_lpfiles)
200225

226+
if cfg.W_and_xbar_reader:
227+
ext_classes.append(WXBarReader)
228+
229+
if cfg.W_and_xbar_writer:
230+
ext_classes.append(WXBarWriter)
231+
232+
if cfg.user_defined_extensions is not None:
233+
for ext_name in cfg.user_defined_extensions:
234+
module = sputils.module_name_to_module(ext_name)
235+
vanilla.extension_adder(module)
236+
# grab JSON for this module's option dictionary
237+
json_filename = ext_name+".json"
238+
if os.path.exists(json_filename):
239+
ext_options= json.load(json_filename)
240+
hub_dict['opt_kwargs']['options'][ext_name] = ext_options
241+
else:
242+
raise RuntimeError(f"JSON options file {json_filename} for user defined extension not found")
243+
201244
if cfg.sep_rho:
202245
vanilla.add_sep_rho(hub_dict, cfg)
203246

@@ -322,7 +365,11 @@ def _do_decomp(module, cfg, scenario_creator, scenario_creator_kwargs, scenario_
322365
list_of_spoke_dict.append(xhatxbar_spoke)
323366
if cfg.reduced_costs:
324367
list_of_spoke_dict.append(reduced_costs_spoke)
325-
368+
369+
# if the user dares, let them mess with the hubdict prior to solve
370+
if cfg.hub_and_spoke_dict_callback is not None:
371+
module = sputils.module_name_to_module(cfg.hub_and_spoke_dict_callback)
372+
module.hub_and_spoke_dict_callback(hub_dict, list_of_spoke_dict)
326373

327374
wheel = WheelSpinner(hub_dict, list_of_spoke_dict)
328375
wheel.spin()

Diff for: mpisppy/phbase.py

-2
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,6 @@ class PHBase(mpisppy.spopt.SPOpt):
239239
Function to set rho values throughout the PH algorithm.
240240
variable_probability (callable, optional):
241241
Function to set variable specific probabilities.
242-
cfg (config object, optional?) controls (mainly from user)
243-
(Maybe this should move up to spbase)
244242
245243
"""
246244
def __init__(

Diff for: mpisppy/tests/test_gradient_rho.py

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def _create_ph_farmer(self):
5757
scenario_creator_kwargs = farmer.kw_creator(self.cfg)
5858
beans = (self.cfg, scenario_creator, scenario_denouement, all_scenario_names)
5959
hub_dict = vanilla.ph_hub(*beans, scenario_creator_kwargs=scenario_creator_kwargs)
60+
hub_dict['opt_kwargs']['options']['cfg'] = self.cfg
6061
list_of_spoke_dict = list()
6162
wheel = WheelSpinner(hub_dict, list_of_spoke_dict)
6263
wheel.spin()

Diff for: mpisppy/tests/test_w_writer.py renamed to mpisppy/tests/test_xbar_w_reader_writer.py

+19-17
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@
2222
import mpisppy.tests.examples.farmer as farmer
2323
from mpisppy.spin_the_wheel import WheelSpinner
2424
from mpisppy.tests.utils import get_solver
25-
from mpisppy.utils.wxbarwriter import WXBarWriter
26-
from mpisppy.utils.wxbarreader import WXBarReader
25+
import mpisppy.utils.wxbarreader as wxbarreader
26+
import mpisppy.utils.wxbarwriter as wxbarwriter
2727

28-
29-
__version__ = 0.1
28+
__version__ = 0.2
3029

3130
solver_available,solver_name, persistent_available, persistent_solver_name= get_solver()
3231

3332
def _create_cfg():
3433
cfg = config.Config()
34+
wxbarreader.add_options_to_config(cfg)
35+
wxbarwriter.add_options_to_config(cfg)
3536
cfg.add_branching_factors()
3637
cfg.num_scens_required()
3738
cfg.popular_args()
@@ -43,7 +44,7 @@ def _create_cfg():
4344

4445
#*****************************************************************************
4546

46-
class Test_w_writer_farmer(unittest.TestCase):
47+
class Test_xbar_w_reader_writer_farmer(unittest.TestCase):
4748
""" Test the gradient code using farmer."""
4849

4950
def _create_ph_farmer(self, ph_extensions=None, max_iter=100):
@@ -59,14 +60,15 @@ def _create_ph_farmer(self, ph_extensions=None, max_iter=100):
5960
self.cfg.max_iterations = max_iter
6061
beans = (self.cfg, scenario_creator, scenario_denouement, all_scenario_names)
6162
hub_dict = vanilla.ph_hub(*beans, scenario_creator_kwargs=scenario_creator_kwargs, ph_extensions=ph_extensions)
62-
if ph_extensions==WXBarWriter: #tbd
63-
hub_dict['opt_kwargs']['options']["W_and_xbar_writer"] = {"Wcsvdir": "Wdir"}
64-
hub_dict['opt_kwargs']['options']['W_fname'] = self.temp_w_file_name
65-
hub_dict['opt_kwargs']['options']['Xbar_fname'] = self.temp_xbar_file_name
66-
if ph_extensions==WXBarReader:
67-
hub_dict['opt_kwargs']['options']["W_and_xbar_reader"] = {"Wcsvdir": "Wdir"}
68-
hub_dict['opt_kwargs']['options']['init_W_fname'] = self.w_file_name
69-
hub_dict['opt_kwargs']['options']['init_Xbar_fname'] = self.xbar_file_name
63+
hub_dict['opt_kwargs']['options']['cfg'] = self.cfg
64+
if ph_extensions==wxbarwriter.WXBarWriter:
65+
self.cfg.W_and_xbar_writer = True
66+
self.cfg.W_fname = self.temp_w_file_name
67+
self.cfg.Xbar_fname = self.temp_xbar_file_name
68+
if ph_extensions==wxbarreader.WXBarReader:
69+
self.cfg.W_and_xbar_reader = True
70+
self.cfg.init_W_fname = self.w_file_name
71+
self.cfg.init_Xbar_fname = self.xbar_file_name
7072
list_of_spoke_dict = list()
7173
wheel = WheelSpinner(hub_dict, list_of_spoke_dict)
7274
wheel.spin()
@@ -79,7 +81,7 @@ def setUp(self):
7981
self.ph_object = None
8082

8183
def test_wwriter(self):
82-
self.ph_object = self._create_ph_farmer(ph_extensions=WXBarWriter, max_iter=5)
84+
self.ph_object = self._create_ph_farmer(ph_extensions=wxbarwriter.WXBarWriter, max_iter=5)
8385
with open(self.temp_w_file_name, 'r') as f:
8486
read = csv.reader(f)
8587
rows = list(read)
@@ -88,7 +90,7 @@ def test_wwriter(self):
8890
os.remove(self.temp_w_file_name)
8991

9092
def test_xbarwriter(self):
91-
self.ph_object = self._create_ph_farmer(ph_extensions=WXBarWriter, max_iter=5)
93+
self.ph_object = self._create_ph_farmer(ph_extensions=wxbarwriter.WXBarWriter, max_iter=5)
9294
with open(self.temp_xbar_file_name, 'r') as f:
9395
read = csv.reader(f)
9496
rows = list(read)
@@ -97,15 +99,15 @@ def test_xbarwriter(self):
9799
os.remove(self.temp_xbar_file_name)
98100

99101
def test_wreader(self):
100-
self.ph_object = self._create_ph_farmer(ph_extensions=WXBarReader, max_iter=1)
102+
self.ph_object = self._create_ph_farmer(ph_extensions=wxbarreader.WXBarReader, max_iter=1)
101103
for sname, scenario in self.ph_object.local_scenarios.items():
102104
if sname == 'scen0':
103105
self.assertAlmostEqual(scenario._mpisppy_model.W[("ROOT", 1)]._value, 70.84705093609978)
104106
if sname == 'scen1':
105107
self.assertAlmostEqual(scenario._mpisppy_model.W[("ROOT", 0)]._value, -41.104251445950844)
106108

107109
def test_xbarreader(self):
108-
self.ph_object = self._create_ph_farmer(ph_extensions=WXBarReader, max_iter=1)
110+
self.ph_object = self._create_ph_farmer(ph_extensions=wxbarreader.WXBarReader, max_iter=1)
109111
for sname, scenario in self.ph_object.local_scenarios.items():
110112
if sname == 'scen0':
111113
self.assertAlmostEqual(scenario._mpisppy_model.xbars[("ROOT", 1)]._value, 274.2239371483933)

Diff for: mpisppy/utils/config.py

+5-25
Original file line numberDiff line numberDiff line change
@@ -964,32 +964,12 @@ def tracking_args(self):
964964
domain=int,
965965
default=0)
966966

967-
968967
def wxbar_read_write_args(self):
969-
self.add_to_config("init_W_fname",
970-
description="Path of initial W file (default None)",
971-
domain=str,
972-
default=None)
973-
self.add_to_config("init_Xbar_fname",
974-
description="Path of initial Xbar file (default None)",
975-
domain=str,
976-
default=None)
977-
self.add_to_config("init_separate_W_files",
978-
description="If True, W is read from separate files (default False)",
979-
domain=bool,
980-
default=False)
981-
self.add_to_config("W_fname",
982-
description="Path of final W file (default None)",
983-
domain=str,
984-
default=None)
985-
self.add_to_config("Xbar_fname",
986-
description="Path of final Xbar file (default None)",
987-
domain=str,
988-
default=None)
989-
self.add_to_config("separate_W_files",
990-
description="If True, writes W to separate files (default False)",
991-
domain=bool,
992-
default=False)
968+
import mpisppy.utils.wxbarreader as wxbarreader
969+
wxbarreader.add_options_to_config(self)
970+
971+
import mpisppy.utils.wxbarwriter as wxbarwriter
972+
wxbarwriter.add_options_to_config(self)
993973

994974
def proper_bundle_config(self):
995975
self.add_to_config('pickle_bundles_dir',

Diff for: mpisppy/utils/gradient.py

+2-24
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import csv
1616
import copy
1717

18-
from mpisppy.utils import config
1918
import mpisppy.utils.cfg_vanilla as vanilla
2019
from mpisppy.utils.wxbarwriter import WXBarWriter
2120
from mpisppy.spin_the_wheel import WheelSpinner
@@ -190,29 +189,6 @@ def write_grad_rho(self):
190189
###################################################################################
191190

192191

193-
def _parser_setup():
194-
""" Set up config object and return it, but don't parse
195-
196-
Returns:
197-
cfg (Config): config object
198-
199-
Notes:
200-
parsers for the non-model-specific arguments; but the model_module_name will be pulled off first
201-
202-
"""
203-
204-
cfg = config.Config()
205-
cfg.add_branching_factors()
206-
cfg.num_scens_required()
207-
cfg.popular_args()
208-
cfg.two_sided_args()
209-
cfg.ph_args()
210-
211-
cfg.gradient_args()
212-
213-
return cfg
214-
215-
216192
def grad_cost_and_rho(mname, original_cfg):
217193
""" Creates a ph object from cfg and using the module 'mname' functions. Then computes the corresponding grad cost and rho.
218194
@@ -244,10 +220,12 @@ def grad_cost_and_rho(mname, original_cfg):
244220
if hasattr(model_module, '_variable_probability'):
245221
variable_probability = model_module._variable_probability
246222
beans = (cfg, scenario_creator, scenario_denouement, all_scenario_names)
223+
# Note: the callers need to set w_writer cfg options
247224
hub_dict = vanilla.ph_hub(*beans,
248225
scenario_creator_kwargs=scenario_creator_kwargs,
249226
ph_extensions=WXBarWriter,
250227
variable_probability=variable_probability)
228+
hub_dict['opt_kwargs']['options']['cfg'] = cfg
251229
list_of_spoke_dict = list()
252230
wheel = WheelSpinner(hub_dict, list_of_spoke_dict)
253231
wheel.spin() #TODO: steal only what's needed in WheelSpinner

Diff for: mpisppy/utils/wxbarreader.py

+32-7
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,42 @@
3939
n_proc = MPI.COMM_WORLD.Get_size()
4040
rank = MPI.COMM_WORLD.Get_rank()
4141

42+
def add_options_to_config(cfg):
43+
44+
cfg.add_to_config("W_and_xbar_reader",
45+
description="Enables the w and xbar reader (default False)",
46+
domain=bool,
47+
default=False)
48+
cfg.add_to_config("init_W_fname",
49+
description="Path of initial W file (default None)",
50+
domain=str,
51+
default=None)
52+
cfg.add_to_config("init_Xbar_fname",
53+
description="Path of initial Xbar file (default None)",
54+
domain=str,
55+
default=None)
56+
cfg.add_to_config("init_separate_W_files",
57+
description="If True, W is read from separate files (default False)",
58+
domain=bool,
59+
default=False)
60+
4261
class WXBarReader(mpisppy.extensions.extension.Extension):
4362
""" Extension class for reading W values
4463
"""
4564
def __init__(self, ph):
4665

66+
assert 'cfg' in ph.options
67+
self.cfg = ph.options['cfg']
68+
if self.cfg.get("W_and_xbar_reader") is None or not self.cfg.W_and_xbar_reader:
69+
self.not_active = True
70+
return # nothing to do here
71+
else:
72+
self.not_active = False
73+
4774
''' Do a bunch of checking if files exist '''
48-
w_fname, x_fname, sep_files = None, None, False
49-
if ('init_separate_W_files' in ph.options):
50-
sep_files = ph.options['init_separate_W_files']
75+
w_fname, x_fname, sep_files = self.cfg.init_W_fname, self.cfg.init_Xbar_fname, self.cfg.init_separate_W_files
5176

52-
if ('init_W_fname' in ph.options):
53-
w_fname = ph.options['init_W_fname']
77+
if w_fname is not None:
5478
if (not os.path.exists(w_fname)):
5579
if (rank == 0):
5680
if (sep_files):
@@ -59,8 +83,7 @@ def __init__(self, ph):
5983
print('Cannot find file', w_fname)
6084
quit()
6185

62-
if ('init_Xbar_fname' in ph.options):
63-
x_fname = ph.options['init_Xbar_fname']
86+
if x_fname is not None:
6487
if (not os.path.exists(x_fname)):
6588
if (rank == 0):
6689
print('Cannot find file', x_fname)
@@ -85,6 +108,8 @@ def post_iter0(self):
85108

86109
def miditer(self):
87110
''' Called before the solveloop is called '''
111+
if self.not_active:
112+
return # nothing to do.
88113
if self.PHB._PHIter == 1:
89114
if self.w_fname:
90115
mpisppy.utils.wxbarutils.set_W_from_file(

0 commit comments

Comments
 (0)