Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added a call to an initialize function to generic_cylinders per a sug… #498

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ccf8bba
added a call to an initialize function to generic_cylinders per a sug…
DLWoodruff Mar 25, 2025
3c00a1d
added a custom writer callout to generic cylinders
DLWoodruff Mar 26, 2025
136f4e1
switch to a function that creates the class that is used as the 'modu…
DLWoodruff Mar 27, 2025
d9d9533
Update mpisppy/generic_cylinders.py
DLWoodruff Apr 3, 2025
411f600
implement Ben and Tomas' suggestions
DLWoodruff Apr 3, 2025
1806fb3
Merge branch 'main' into initialize
DLWoodruff Apr 3, 2025
03a9a17
add a test for a class in the module
DLWoodruff Apr 3, 2025
e20ba01
Merge branch 'initialize' of https://github.com/DLWoodruff/mpi-sppy-1…
DLWoodruff Apr 3, 2025
76abab7
fix error in tree creator
DLWoodruff Apr 3, 2025
7e854e5
changed custom writers so there are now four of them; need a test
DLWoodruff Apr 3, 2025
0ab7b0e
fix error in call to getattr
DLWoodruff Apr 4, 2025
71c300c
fix error in use of getattr in generic_cylinders.py
DLWoodruff Apr 4, 2025
57c651f
fixing ef writer call in generic cylinders (working in a bad environm…
DLWoodruff Apr 4, 2025
b9efc49
name change in docs
bknueven Apr 4, 2025
e8e845b
added a smoke test for user supplied write functions for generic_cyli…
DLWoodruff Apr 4, 2025
4dfbaca
Merge branch 'initialize' of https://github.com/DLWoodruff/mpi-sppy-1…
DLWoodruff Apr 4, 2025
a894a9f
fix an error in custom writer for generic cylinders
DLWoodruff Apr 4, 2025
fec12f7
function signature for custom writer corrected
DLWoodruff Apr 4, 2025
975cc8c
needed to give more itertations to test run
DLWoodruff Apr 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions doc/src/generic_cylinders.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,41 @@ ignored.
probably needed on the command line. Consistency with the files in the
pickle directory might not be checked by the program.

A Class in the module
---------------------

If you want to have a class in the module to provide helper functions,
your module still needs to have an ``inparse_adder`` function and the module will need
to have a function called ``get_mpisppy_helper_object(cfg)`` that returns
the object. It is called by ``generic_cylinders.py`` after cfg is
populated and can be used to create a class. Note that the function
``inparser_adder`` cannot make use of the class because that function
is called before ``get_function_object``.

The class definition needs to include all helper functions other than
``inparser_adder``.


custom_writer
-------------

Advanced users might want to write their own solution output function. If the
module contains a function called ``custom_writer()``, it will be called
at the end of processing. If you are writing such a function, you should look
in ``generic_cylinders.py`` to see the two places it appears and the arguments
that are passed from each place (your function can test the type
of the first argument to see whence it was called or it can
examine the call stack for more information).


Assuming the first formal parameter in your function is assigned
to variable named wheel in the non-ef case, your function probably should
include something along the lines of this:

.. code_block:: python

if wheel.spcomm.opt.cylinder_rank == 0:

so that you avoid writing the output from every rank.
Note this verification is automatically performed by WheelSpinner if you call your custom writer functions through
``wheel.write_first_stage_solution(solution_file_name, first_stage_solution_writer=my_first_stage_writer)`` and ``wheel.write_tree_solution(solution_dir_name, scenario_tree_solution_writer=my_tree_solution_writer)``.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just be handling this for the user, e.g., pass the function they provide into write_first_stage_solution or write_tree_solution? These methods also determine which cylinder has the optimal solution to write, so it's a bit more subtle than checking the cylinder rank. Plus, if you need the tree solution, you have to write from every rank.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that is probably safer and does not really restrict what the user can do; they can just put it inside the corresponding custom writer.

8 changes: 7 additions & 1 deletion examples/generic_tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
solver_name = sys.argv[1]

# Use oversubscribe if your computer does not have enough cores.
# Don't use this unless you have to.
# Don't use oversubscribe unless you have to.
# (This may not be allowed on some versions of mpiexec)
mpiexec_arg = "" # "--oversubscribe" or "-envall"
if len(sys.argv) > 2:
Expand Down Expand Up @@ -151,6 +151,12 @@ def do_one(dirname, modname, np, argstring, xhat_baseline_dir=None, tol=1e-6):
#rebaseline_xhat("farmer", "farmer", 1, farmeref, "test_data/farmeref_baseline")
do_one("farmer", "farmer", 1, farmeref, xhat_baseline_dir = "test_data/farmeref_baseline")

# we need slammax and cross-scenario to make this work well
netdesC = (f"--max-iterations=3 --instance-name=network-10-20-L-01 --netdes-data-path ./data "
f"--solver-name={solver_name} --rel-gap=0.0 --default-rho=10000 --presolve "
f"--subgradient-hub --xhatshuffle --max-solver-threads=2")
do_one("netdes", "netdes_with_class", 2, netdesC, xhat_baseline_dir=None)
# TBD: put in a baseline

hydroef = (f"--EF --branching-factors '3 3' --EF-solver-name={solver_name}")
#rebaseline_xhat("hydro", "hydro", 1, hydroef, "test_data/hydroef_baseline")
Expand Down
173 changes: 173 additions & 0 deletions examples/netdes/netdes_with_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
###############################################################################
# mpi-sppy: MPI-based Stochastic Programming in PYthon
#
# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for
# Sustainable Energy, LLC, The Regents of the University of California, et al.
# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for
# full copyright and license information.
###############################################################################
# modified April 2025 by DLW to illustrate the use of a class for helper functions
''' Implementation of a simple network design problem. See README for more info

Created: 9 November 2019 by DTM

Scenario indices are ZERO based
'''

import os
import mpisppy.utils.sputils as sputils
import pyomo.environ as pyo
import numpy as np
from parse import parse

#=========
def inparser_adder(cfg):
# add options unique to sizes
# we don't want num_scens from the command line
cfg.mip_options()
cfg.add_to_config("instance_name",
description="netdes instance name (e.g., network-10-20-L-01)",
domain=str,
default=None)
cfg.add_to_config("netdes_data_path",
description="path to detdes data (e.g., ./data)",
domain=str,
default=None)





def get_mpisppy_helper_object(cfg):
return NetDes(cfg)

class NetDes:
def __init__(self, cfg):
self.cfg = cfg


def scenario_creator(self, scenario_name, path=None):
if path is None:
raise RuntimeError('Must provide the name of the .dat file '
'containing the instance data via the '
'path argument to scenario_creator')

scenario_ix = self._get_scenario_ix(scenario_name)
model = self.build_scenario_model(path, scenario_ix)

# now attach the one and only scenario tree node
sputils.attach_root_node(model, model.FirstStageCost, [model.x[:,:], ])

return model


def build_scenario_model(self,fname, scenario_ix):
data = parse(fname, scenario_ix=scenario_ix)
num_nodes = data['N']
adj = data['A'] # Adjacency matrix
edges = data['el'] # Edge list
c = data['c'] # First-stage cost matrix (per edge)
d = data['d'] # Second-stage cost matrix (per edge)
u = data['u'] # Capacity of each arc
b = data['b'] # Demand of each node
p = data['p'] # Probability of scenario

model = pyo.ConcreteModel()
model.x = pyo.Var(edges, domain=pyo.Binary) # First stage vars
model.y = pyo.Var(edges, domain=pyo.NonNegativeReals) # Second stage vars

model.edges = edges
model._mpisppy_probability = p

''' Objective '''
model.FirstStageCost = pyo.quicksum(c[e] * model.x[e] for e in edges)
model.SecondStageCost = pyo.quicksum(d[e] * model.y[e] for e in edges)
obj_expr = model.FirstStageCost + model.SecondStageCost
model.MinCost = pyo.Objective(expr=obj_expr, sense=pyo.minimize)

''' Variable upper bound constraints on each edge '''
model.vubs = pyo.ConstraintList()
for e in edges:
expr = model.y[e] - u[e] * model.x[e]
model.vubs.add(expr <= 0)

''' Flow balance constraints for each node '''
model.bals = pyo.ConstraintList()
for i in range(num_nodes):
in_nbs = np.where(adj[:,i] > 0)[0]
out_nbs = np.where(adj[i,:] > 0)[0]
lhs = pyo.quicksum(model.y[i,j] for j in out_nbs) - \
pyo.quicksum(model.y[j,i] for j in in_nbs)
model.bals.add(lhs == b[i])

return model




def scenario_denouement(self, rank, scenario_name, scenario):
pass

def _get_scenario_ix(self, sname):
''' Get the scenario index from the given scenario name by strpiping all
digits off of the right of the scenario name, until a non-digit is
encountered.
'''
i = len(sname) - 1
while (i > 0 and sname[i-1].isdigit()):
i -= 1
return int(sname[i:])

########## helper functions ########

#=========
def scenario_names_creator(self, num_scens,start=None):
# if start!=None, the list starts with the 'start' labeled scenario
if (start is None) :
start=0
return [f"Scenario{i}" for i in range(start,start+num_scens)]


#=========
def kw_creator(self, cfg):
# linked to the scenario_creator and inparser_adder
# side-effect is dealing with num_scens
inst = cfg.instance_name
ns = int(inst.split("-")[-3])
if hasattr(cfg, "num_scens"):
if cfg.num_scens != ns:
raise RuntimeError(f"Argument num-scens={cfg.num_scens} does not match the number "
"implied by instance name={ns} "
"\n(--num-scens is not needed for netdes)")
else:
cfg.add_and_assign("num_scens","number of scenarios", int, None, ns)
path = os.path.join(cfg.netdes_data_path, f"{inst}.dat")
kwargs = {"path": path}
return kwargs

def sample_tree_scen_creator(self, sname, stage, sample_branching_factors, seed,
given_scenario=None, **scenario_creator_kwargs):
""" Create a scenario within a sample tree. Mainly for multi-stage and simple for two-stage.
(this function supports zhat and confidence interval code)
Args:
sname (string): scenario name to be created
stage (int >=1 ): for stages > 1, fix data based on sname in earlier stages
sample_branching_factors (list of ints): branching factors for the sample tree
seed (int): To allow random sampling (for some problems, it might be scenario offset)
given_scenario (Pyomo concrete model): if not None, use this to get data for ealier stages
scenario_creator_kwargs (dict): keyword args for the standard scenario creator funcion
Returns:
scenario (Pyomo concrete model): A scenario for sname with data in stages < stage determined
by the arguments
"""
# Since this is a two-stage problem, we don't have to do much.
sca = scenario_creator_kwargs.copy()
sca["seedoffset"] = seed
sca["num_scens"] = sample_branching_factors[0] # two-stage problem
return self.scenario_creator(sname, **sca)

######## end helper functions #########


if __name__=='__main__':
print('netdes.py has no main')
14 changes: 13 additions & 1 deletion mpisppy/generic_cylinders.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,9 @@ def _do_decomp(module, cfg, scenario_creator, scenario_creator_kwargs, scenario_
first_stage_solution_writer=sputils.first_stage_nonant_npy_serializer)
wheel.write_tree_solution(f'{cfg.solution_base_name}_soldir')
global_toc("Wrote solution data.")
# callout to special solution writer if it exists
if hasattr(module, 'custom_writer'):
module.custom_writer(wheel, cfg)


#==========
Expand Down Expand Up @@ -505,6 +508,11 @@ def _do_EF(module, cfg, scenario_creator, scenario_creator_kwargs, scenario_deno
sputils.ef_ROOT_nonants_npy_serializer(ef, f'{cfg.solution_base_name}.npy')
sputils.write_ef_tree_solution(ef,f'{cfg.solution_base_name}_soldir')
global_toc("Wrote EF solution data.")
# callout to special solution writer if it exists
if hasattr(module, 'custom_writer'):
module.custom_writer(ef, cfg)



def _model_fname():
def _bad_news():
Expand Down Expand Up @@ -556,9 +564,13 @@ def _proper_bundles(cfg):
fname = os.path.basename(model_fname)
sys.path.append(dpath)
module = importlib.import_module(fname)

cfg = _parse_args(module)

# Perhaps use an object as the so-called module.
if hasattr(module, "get_mpisppy_helper_object"):
module = module.get_mpisppy_helper_object(cfg)

bundle_wrapper = None # the default
if _proper_bundles(cfg):
# TBD: remove the need for dill if you are not reading or writing
Expand Down
Loading