Skip to content

Commit e697a1b

Browse files
DLWoodruffbknueven
andauthored
MPS (#497)
* created an mps readers based on pulp * created an mps_module to allow for mps based scenarios * [WIP] added to the format for json files to accompany lp and mps files * can now read mps using coin mip; here's a little demo * major corrections by dlw * mps_coin_mip.py executes, but the solutions don't match * the mps reader is ready for testing * added a test for the mps_reader * [WIP] getting start on testing for the mps module option in generic cylinders * [WIP] working on testin generic tester and fixing bugs (still need to work on cfg.num_scens * working on generic tester to get cfg.num_scens taken care of * there is now a test for mps reader and mps module in generic tester, but it needs to be easier for xpress * touch up tests * added a demonstration and some documentation. * tests are ready to run? * cleanup * trying to debug the admm_wrapper test * Update test_admmWrapper.py * make the top level of the json file schema symmetric * Update doc/src/agnostic.rst Co-authored-by: bknueven <[email protected]> * Update mpisppy/utils/mps_reader.py Co-authored-by: bknueven <[email protected]> * change the name of the mps write opion in generic_cylinders --------- Co-authored-by: bknueven <[email protected]>
1 parent 845809b commit e697a1b

14 files changed

+1377
-28
lines changed

.github/workflows/test_pr_and_main.yml

+29-1
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,34 @@ jobs:
310310
python test_pickle_bundle.py
311311
312312
313+
mps:
314+
name: MPS tests
315+
runs-on: ubuntu-latest
316+
needs: [ruff]
317+
318+
steps:
319+
- uses: actions/checkout@v3
320+
- uses: conda-incubator/setup-miniconda@v2
321+
with:
322+
activate-environment: test_env
323+
python-version: 3.11
324+
auto-activate-base: false
325+
- name: Install dependencies
326+
run: |
327+
conda install mpi4py pandas setuptools
328+
pip install pyomo xpress cplex mip
329+
330+
- name: setup the program
331+
run: |
332+
pip install -e .
333+
334+
- name: run MPS tests
335+
timeout-minutes: 2
336+
run: |
337+
cd mpisppy/tests
338+
python test_mps.py
339+
340+
313341
confidence-intervals:
314342
name: confidence intervals tests
315343
runs-on: ubuntu-latest
@@ -494,7 +522,7 @@ jobs:
494522
timeout-minutes: 10
495523
run: |
496524
cd mpisppy/tests
497-
mpiexec -np 2 python -m mpi4py test_with_cylinders.py
525+
mpiexec -np 2 python -m mpi4py test_with_cylinders.py
498526
499527
test-agnostic:
500528
name: tests on agnostic

doc/src/agnostic.rst

+28-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,34 @@ AML Agnosticism
44
The mpi-sppy package provides callouts so that algebraic modeling languages
55
(AMLs) other than Pyomo can be used. A growing number of AMLs are supported
66
as `guest` languages (we refer to mpi-sppy as the `host`). This code is
7-
in an alpha-release state; use with extreme caution.
7+
in an alpha-release state; use with extreme caution. This is referred to
8+
as `tight` integration with the guest. It is also possible to simply read
9+
scenario data from an mps file and the mps file (and the associated json
10+
nonant file) that can be created however you like.
11+
12+
Loose integration
13+
^^^^^^^^^^^^^^^^^
14+
15+
Code for creating a
16+
Pyomo model from an mps file is in ``mpisppy.utils.mps_reader.py``,
17+
but you can also just use ``generic_cylinders.py`` and give
18+
it the module ``mpisppy.utils.mps_module`` (you will need to specify
19+
that path to this module) and the ``--mps-files-directory``
20+
option. Note
21+
that at the time of this writing, the number of scenarios is obtained
22+
by counting the mps files in the directory given.
23+
24+
The file ``examples.sizes.mps_demo.bash`` has two commands. The second illustrates
25+
how to instruction ``MPI-SPPY`` to read mps/json file pairs for each scenario from a
26+
directory. The first command illustrates how to use ``MPI-SPPY`` to write
27+
them in the first place (but if ``MPI-SPPY`` can get your scenarios, there
28+
is probably no reason to write them and then read them again!). This
29+
functionality is intended to be used by users of other AMLs or other
30+
scenario-based stochastic programming applications.
31+
32+
33+
Tight integration
34+
^^^^^^^^^^^^^^^^^
835

936
From the end-user's perspective
1037
-------------------------------

doc/src/extensions.rst

+14-8
Original file line numberDiff line numberDiff line change
@@ -274,11 +274,17 @@ If some variables have zero probability in all scenarios, then you will need to
274274
all variable probabilities! So you might want to set this to False to verify that the probabilities sum to one
275275
only for the Vars you expect before setting it to True.
276276

277-
Scenario_lpwriter
278-
-----------------
279-
280-
This extension writes an lp file with the model and json file with (a) list(s) of
281-
scenario tree node names and nonanticaptive variables for each scenario before
282-
the iteration zero solve of PH or APH. Note that for two-stage problems, all
283-
json files will be the same. See ``mpisppy.generic_cylinders.py``
284-
for an example of use.
277+
Scenario_lp_mps_writer
278+
----------------------
279+
280+
This extension writes an lp file and an mps file with the model as well as a
281+
json file with (a) list(s) of scenario tree node names and
282+
nonanticaptive variables for each scenario before the iteration zero
283+
solve of PH or APH. Note that for two-stage problems, all json files
284+
will be the same. See ``mpisppy.generic_cylinders.py`` for an example
285+
of use. In that program it is activated with the
286+
``--scenario-lp-mps-writer`` option. Note that it
287+
writes the files to the current working directory and for each scenario
288+
the base name of the three files written is the scenario name.
289+
290+
Unless you know exactly why you need this, you probably don't.

examples/generic_tester.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,19 @@ def do_one(dirname, modname, np, argstring, xhat_baseline_dir=None, tol=1e-6):
177177
#rebaseline_xhat("farmer", "farmer", 3, farmer_rd, "test_data/farmer_rd_baseline")
178178
do_one("farmer", "farmer", 3, farmer_rd, xhat_baseline_dir="test_data/farmer_rd_baseline")
179179

180-
# Just a smoke test to make sure sizes_expression still exists and
181-
# that lpfiles still executes.
180+
### combined runs to test mps files ####
181+
# Make sure sizes_expression still exists and lpfiles still executes.
182182
sizese = ("--module-name sizes_expression --num-scens 3 --default-rho 1"
183183
f" --solver-name {solver_name} --max-iterations 0"
184-
" --scenario-lpfiles")
185-
do_one("sizes", "sizes", 3, sizese, xhat_baseline_dir=None)
184+
" --write-scenario-lp-mps-files")
185+
do_one("sizes", "sizes_expression", 3, sizese, xhat_baseline_dir=None)
186+
# just smoke for now
187+
sizesMPS = ("--module-name ../../mpisppy/utils/mps_module --default-rho 1"
188+
f" --solver-name {solver_name} --max-iterations 0"
189+
" --mps-files-directory=.") # we will be in the sizes dir
190+
do_one("sizes", "../../mpisppy/utils/mps_module", 1, sizesMPS, xhat_baseline_dir=None)
191+
192+
### end combined mps file runs ###
186193

187194
quit()
188195

examples/sizes/mps_demo.bash

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/bash
2+
# This is mainly to demonstrate what loose agnostic files look like and
3+
# how to use them with agnostic_cylinders.py.
4+
# To do that, we write the files based on a Pyomo model, then
5+
# read them in.
6+
# Note: if you actually have a Pyomo model, you probably don't want to do
7+
# it this way since you would have had to have written most of the
8+
# functions (e.g. scenario_creator) anyway.
9+
# If you are using some other AML, then you migth want to use the second
10+
# command line to read the files you wrote with your AML and
11+
# you can use the first command to write files as an example of the format
12+
# for the json files.
13+
14+
set -e
15+
16+
SOLVER=cplex
17+
18+
# assumes we are in the sizes directory and don't mind polluting it with 6 files
19+
python ../../mpisppy/generic_cylinders.py --module-name sizes_expression --num-scens 3 --default-rho 1 --solver-name ${SOLVER} --max-iterations 0 --scenario-lp-mps-files
20+
21+
# By specifying the module to be mps_module we will read files for the problem
22+
# from the specified mps-files-directory.
23+
mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name ../../mpisppy/utils/mps_module --xhatshuffle --lagrangian --default-rho 1 --solver-name ${SOLVER} --max-iterations 10 --mps-files-directory=.

mpisppy/extensions/scenario_lpfiles.py mpisppy/extensions/scenario_lp_mps_files.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,28 @@
1717

1818

1919
def lpize(varname):
20-
# convert varname to the string that will appear in the lp file
21-
# return varname.replace("[", "(").replace("]", ")").replace(",", "_").replace(".","_")
20+
# convert varname to the string that will appear in the lp and mps files
2221
return pyomo_label.cpxlp_label_from_name(varname)
2322

2423

25-
class Scenario_lpfiles(mpisppy.extensions.extension.Extension):
24+
class Scenario_lp_mps_files(mpisppy.extensions.extension.Extension):
2625

2726
def __init__(self, ph):
2827
self.ph = ph
2928

3029
def pre_iter0(self):
3130
for k, s in self.ph.local_subproblems.items():
3231
s.write(f"{k}.lp", io_options={'symbolic_solver_labels': True})
33-
nonants_by_node = {nd.name: [lpize(var.name) for var in nd.nonant_vardata_list] for nd in s._mpisppy_node_list}
32+
s.write(f"{k}.mps", io_options={'symbolic_solver_labels': True})
33+
scenData = {"name": s.name, "scenProb": s._mpisppy_probability}
34+
scenDict = {"scenarioData": scenData}
35+
treeData = dict()
36+
for nd in s._mpisppy_node_list:
37+
treeData[nd.name] = {"condProb": nd.cond_prob}
38+
treeData[nd.name].update({"nonAnts": [lpize(var.name) for var in nd.nonant_vardata_list]})
39+
scenDict["treeData"] = treeData
3440
with open(f"{k}_nonants.json", "w") as jfile:
35-
json.dump(nonants_by_node, jfile)
41+
json.dump(scenDict, jfile, indent=2)
3642

3743
def post_iter0(self):
3844
return

mpisppy/generic_cylinders.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from mpisppy.extensions.fixer import Fixer
3030
from mpisppy.extensions.mipgapper import Gapper
3131
from mpisppy.extensions.gradient_extension import Gradient_extension
32-
from mpisppy.extensions.scenario_lpfiles import Scenario_lpfiles
32+
from mpisppy.extensions.scenario_lp_mps_files import Scenario_lp_mps_files
3333

3434
from mpisppy.utils.wxbarwriter import WXBarWriter
3535
from mpisppy.utils.wxbarreader import WXBarReader
@@ -55,15 +55,17 @@ def _parse_args(m):
5555
description="The string used for a directory of ouput along with a csv and an npv file (default None, which means no soltion output)",
5656
domain=str,
5757
default=None)
58-
cfg.add_to_config(name="scenario_lpfiles",
59-
description="Invokes an extension that writes an model lp file and a nonants json file for each scenario before iteration 0",
58+
cfg.add_to_config(name="write_scenario_lp_mps_files",
59+
description="Invokes an extension that writes an model lp file, mps file and a nonants json file for each scenario before iteration 0",
6060
domain=bool,
6161
default=False)
6262

6363
m.inparser_adder(cfg)
6464
# many models, e.g., farmer, need num_scens_required
6565
# in which case, it should go in the inparser_adder function
6666
# cfg.num_scens_required()
67+
# On the other hand, this program really wants cfg.num_scens somehow so
68+
# maybe it should just require it.
6769

6870
cfg.EF_base() # If EF is slected, most other options will be moot
6971
# There are some arguments here that will not make sense for all models
@@ -120,7 +122,7 @@ def _name_lists(module, cfg, bundle_wrapper=None):
120122
"For now, stage2EFsolvern is required for multistage xhat"
121123
else:
122124
all_nodenames = None
123-
num_scens = cfg.num_scens
125+
num_scens = cfg.get("num_scens") # maybe None is OK
124126

125127
# proper bundles should be almost magic
126128
if cfg.unpickle_bundles_dir or cfg.scenarios_per_bundle is not None:
@@ -230,8 +232,8 @@ def _do_decomp(module, cfg, scenario_creator, scenario_creator_kwargs, scenario_
230232
ext_classes.append(Gradient_extension)
231233
hub_dict['opt_kwargs']['options']['gradient_extension_options'] = {'cfg': cfg}
232234

233-
if cfg.scenario_lpfiles:
234-
ext_classes.append(Scenario_lpfiles)
235+
if cfg.write_scenario_lp_mps_files:
236+
ext_classes.append(Scenario_lp_mps_files)
235237

236238
if cfg.W_and_xbar_reader:
237239
ext_classes.append(WXBarReader)

mpisppy/spbase.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ def __init__(
105105
global_toc("Initializing SPBase")
106106

107107
if self.n_proc > len(self.all_scenario_names):
108-
raise RuntimeError("More ranks than scenarios")
108+
raise RuntimeError(f"More ranks ({self.n_proc}) than scenarios"
109+
f" ({len(self.all_scenario_names)})")
109110

110111
self._calculate_scenario_ranks()
111112
# Put the deprecation message in the init so they should only see it once per rank

0 commit comments

Comments
 (0)