Skip to content

Commit e97b3dd

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix_xhatshuffle
2 parents a38ba49 + 85d1bff commit e97b3dd

15 files changed

+550
-168
lines changed

.ruff.toml

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ extend-exclude = [
44
"./mpisppy/utils/callbacks/termination/tests/markshare2.py",
55
"./mpisppy/tests/examples/hydro/hydro.py",
66
"./examples/hydro/hydro.py",
7+
"./examples/sizes/models/ExpressionModel.py",
78
]
+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
###############################################################################
2+
# mpi-sppy: MPI-based Stochastic Programming in PYthon
3+
#
4+
# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for
5+
# Sustainable Energy, LLC, The Regents of the University of California, et al.
6+
# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for
7+
# full copyright and license information.
8+
###############################################################################
9+
# ___________________________________________________________________________
10+
#
11+
# Pyomo: Python Optimization Modeling Objects
12+
# Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
13+
# Under the terms of Contract DE-NA0003525 with National Technology and
14+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
15+
# rights in this software.
16+
# This software is distributed under the 3-clause BSD License.
17+
# ___________________________________________________________________________
18+
#
19+
# This is the two-period version of the SIZES optimization model
20+
# derived from the three stage model in:
21+
#A. L{\o}kketangen and D. L. Woodruff,
22+
#"Progressive Hedging and Tabu Search Applied to Mixed Integer (0,1) Multistage Stochastic Programming",
23+
#Journal of Heuristics, 1996, Vol 2, Pages 111-128.
24+
# This version uses expressions for stage costs. January 2025.
25+
26+
from pyomo.core import *
27+
28+
#
29+
# Model
30+
#
31+
32+
model = AbstractModel()
33+
34+
#
35+
# Parameters
36+
#
37+
38+
# the number of product sizes.
39+
model.NumSizes = Param(within=NonNegativeIntegers)
40+
41+
# the set of sizes, labeled 1 through NumSizes.
42+
def product_sizes_rule(model):
43+
return list(range(1, model.NumSizes()+1))
44+
model.ProductSizes = Set(initialize=product_sizes_rule)
45+
46+
# the deterministic demands for product at each size.
47+
model.DemandsFirstStage = Param(model.ProductSizes, within=NonNegativeIntegers)
48+
model.DemandsSecondStage = Param(model.ProductSizes, within=NonNegativeIntegers)
49+
50+
# the unit production cost at each size.
51+
model.UnitProductionCosts = Param(model.ProductSizes, within=NonNegativeReals)
52+
53+
# the setup cost for producing any units of size i.
54+
model.SetupCosts = Param(model.ProductSizes, within=NonNegativeReals)
55+
56+
# the cost to reduce a unit i to a lower unit j.
57+
model.UnitReductionCost = Param(within=NonNegativeReals)
58+
59+
# a cap on the overall production within any time stage.
60+
model.Capacity = Param(within=PositiveReals)
61+
62+
# a derived set to constrain the NumUnitsCut variable domain.
63+
# TBD: the (i,j) with i >= j set should be a generic utility.
64+
def num_units_cut_domain_rule(model):
65+
ans = list()
66+
for i in range(1,model.NumSizes()+1):
67+
for j in range(1, i+1):
68+
ans.append((i,j))
69+
return ans
70+
71+
model.NumUnitsCutDomain = Set(initialize=num_units_cut_domain_rule, dimen=2)
72+
73+
#
74+
# Variables
75+
#
76+
77+
# are any products at size i produced?
78+
model.ProduceSizeFirstStage = Var(model.ProductSizes, domain=Boolean)
79+
model.ProduceSizeSecondStage = Var(model.ProductSizes, domain=Boolean)
80+
81+
# NOTE: The following (num-produced and num-cut) variables are implicitly integer
82+
# under the normal cost objective, but with the PH cost objective, this isn't
83+
# the case.
84+
85+
# the number of units at each size produced.
86+
model.NumProducedFirstStage = Var(model.ProductSizes, domain=NonNegativeIntegers, bounds=(0.0, model.Capacity))
87+
model.NumProducedSecondStage = Var(model.ProductSizes, domain=NonNegativeIntegers, bounds=(0.0, model.Capacity))
88+
89+
# the number of units of size i cut (down) to meet demand for units of size j.
90+
model.NumUnitsCutFirstStage = Var(model.NumUnitsCutDomain, domain=NonNegativeIntegers, bounds=(0.0, model.Capacity))
91+
model.NumUnitsCutSecondStage = Var(model.NumUnitsCutDomain, domain=NonNegativeIntegers, bounds=(0.0, model.Capacity))
92+
93+
# stage-specific cost variables for use in the pysp scenario tree / analysis.
94+
# changed to Expressions January 2025
95+
#model.FirstStageCost = Var(domain=NonNegativeReals)
96+
#model.SecondStageCost = Var(domain=NonNegativeReals)
97+
98+
#
99+
# Constraints
100+
#
101+
102+
# ensure that demand is satisfied in each time stage, accounting for cut-downs.
103+
def demand_satisfied_first_stage_rule(model, i):
104+
return (0.0, sum([model.NumUnitsCutFirstStage[j,i] for j in model.ProductSizes if j >= i]) - model.DemandsFirstStage[i], None)
105+
106+
def demand_satisfied_second_stage_rule(model, i):
107+
return (0.0, sum([model.NumUnitsCutSecondStage[j,i] for j in model.ProductSizes if j >= i]) - model.DemandsSecondStage[i], None)
108+
109+
model.DemandSatisfiedFirstStage = Constraint(model.ProductSizes, rule=demand_satisfied_first_stage_rule)
110+
model.DemandSatisfiedSecondStage = Constraint(model.ProductSizes, rule=demand_satisfied_second_stage_rule)
111+
112+
# ensure that you don't produce any units if the decision has been made to disable producion.
113+
def enforce_production_first_stage_rule(model, i):
114+
# The production capacity per time stage serves as a simple upper bound for "M".
115+
return (None, model.NumProducedFirstStage[i] - model.Capacity * model.ProduceSizeFirstStage[i], 0.0)
116+
117+
def enforce_production_second_stage_rule(model, i):
118+
# The production capacity per time stage serves as a simple upper bound for "M".
119+
return (None, model.NumProducedSecondStage[i] - model.Capacity * model.ProduceSizeSecondStage[i], 0.0)
120+
121+
model.EnforceProductionBinaryFirstStage = Constraint(model.ProductSizes, rule=enforce_production_first_stage_rule)
122+
model.EnforceProductionBinarySecondStage = Constraint(model.ProductSizes, rule=enforce_production_second_stage_rule)
123+
124+
# ensure that the production capacity is not exceeded for each time stage.
125+
def enforce_capacity_first_stage_rule(model):
126+
return (None, sum([model.NumProducedFirstStage[i] for i in model.ProductSizes]) - model.Capacity, 0.0)
127+
128+
def enforce_capacity_second_stage_rule(model):
129+
return (None, sum([model.NumProducedSecondStage[i] for i in model.ProductSizes]) - model.Capacity, 0.0)
130+
131+
model.EnforceCapacityLimitFirstStage = Constraint(rule=enforce_capacity_first_stage_rule)
132+
model.EnforceCapacityLimitSecondStage = Constraint(rule=enforce_capacity_second_stage_rule)
133+
134+
# ensure that you can't generate inventory out of thin air.
135+
def enforce_inventory_first_stage_rule(model, i):
136+
return (None, \
137+
sum([model.NumUnitsCutFirstStage[i,j] for j in model.ProductSizes if j <= i]) - \
138+
model.NumProducedFirstStage[i], \
139+
0.0)
140+
141+
def enforce_inventory_second_stage_rule(model, i):
142+
return (None, \
143+
sum([model.NumUnitsCutFirstStage[i,j] for j in model.ProductSizes if j <= i]) + \
144+
sum([model.NumUnitsCutSecondStage[i,j] for j in model.ProductSizes if j <= i]) \
145+
- model.NumProducedFirstStage[i] - model.NumProducedSecondStage[i], \
146+
0.0)
147+
148+
model.EnforceInventoryFirstStage = Constraint(model.ProductSizes, rule=enforce_inventory_first_stage_rule)
149+
model.EnforceInventorySecondStage = Constraint(model.ProductSizes, rule=enforce_inventory_second_stage_rule)
150+
151+
# stage-specific cost computations.
152+
def first_stage_cost_rule(model):
153+
production_costs = sum([model.SetupCosts[i] * model.ProduceSizeFirstStage[i] + \
154+
model.UnitProductionCosts[i] * model.NumProducedFirstStage[i] \
155+
for i in model.ProductSizes])
156+
cut_costs = sum([model.UnitReductionCost * model.NumUnitsCutFirstStage[i,j] \
157+
for (i,j) in model.NumUnitsCutDomain if i != j])
158+
return (production_costs - cut_costs)
159+
160+
model.FirstStageCost = Expression(rule=first_stage_cost_rule)
161+
162+
def second_stage_cost_rule(model):
163+
production_costs = sum([model.SetupCosts[i] * model.ProduceSizeSecondStage[i] + \
164+
model.UnitProductionCosts[i] * model.NumProducedSecondStage[i] \
165+
for i in model.ProductSizes])
166+
cut_costs = sum([model.UnitReductionCost * model.NumUnitsCutSecondStage[i,j] \
167+
for (i,j) in model.NumUnitsCutDomain if i != j])
168+
return (production_costs - cut_costs)
169+
170+
model.SecondStageCost = Expression(rule=second_stage_cost_rule)
171+
172+
#
173+
# PySP Auto-generated Objective
174+
#
175+
# minimize: sum of StageCosts
176+
#
177+
# A active scenario objective equivalent to that generated by PySP is
178+
# included here for informational purposes.
179+
def total_cost_rule(model):
180+
return model.FirstStageCost + model.SecondStageCost
181+
model.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize)
182+

examples/sizes/sizes_expression.py

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
###############################################################################
2+
# mpi-sppy: MPI-based Stochastic Programming in PYthon
3+
#
4+
# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for
5+
# Sustainable Energy, LLC, The Regents of the University of California, et al.
6+
# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for
7+
# full copyright and license information.
8+
###############################################################################
9+
import os
10+
import models.ExpressionModel as ref # the version with stage cost expressions
11+
import mpisppy.utils.sputils as sputils
12+
13+
def scenario_creator(scenario_name, scenario_count=None):
14+
if scenario_count not in (3, 10):
15+
raise RuntimeError(
16+
"scenario_count passed to scenario counter must equal either 3 or 10"
17+
)
18+
19+
sizes_dir = os.path.dirname(__file__)
20+
datadir = os.sep.join((sizes_dir, f"SIZES{scenario_count}"))
21+
try:
22+
fname = datadir + os.sep + scenario_name + ".dat"
23+
except Exception:
24+
print("FAIL: datadir=", datadir, " scenario_name=", scenario_name)
25+
26+
model = ref.model.create_instance(fname)
27+
28+
# now attach the one and only tree node
29+
varlist = [model.NumProducedFirstStage, model.NumUnitsCutFirstStage]
30+
sputils.attach_root_node(model, model.FirstStageCost, varlist)
31+
32+
return model
33+
34+
35+
def scenario_denouement(rank, scenario_name, scenario):
36+
pass
37+
38+
########## helper functions ########
39+
40+
#=========
41+
def scenario_names_creator(num_scens,start=None):
42+
# if start!=None, the list starts with the 'start' labeled scenario
43+
# note that the scenarios for the sizes problem are one-based
44+
if (start is None) :
45+
start=1
46+
return [f"Scenario{i}" for i in range(start, start+num_scens)]
47+
48+
49+
#=========
50+
def inparser_adder(cfg):
51+
# add options unique to sizes
52+
cfg.num_scens_required()
53+
cfg.mip_options()
54+
55+
56+
#=========
57+
def kw_creator(cfg):
58+
# (for Amalgamator): linked to the scenario_creator and inparser_adder
59+
if cfg.num_scens not in (3, 10):
60+
raise RuntimeError(f"num_scen must the 3 or 10; was {cfg.num_scen}")
61+
kwargs = {"scenario_count": cfg.num_scens}
62+
return kwargs
63+
64+
def sample_tree_scen_creator(sname, stage, sample_branching_factors, seed,
65+
given_scenario=None, **scenario_creator_kwargs):
66+
""" Create a scenario within a sample tree. Mainly for multi-stage and simple for two-stage.
67+
(this function supports zhat and confidence interval code)
68+
Args:
69+
sname (string): scenario name to be created
70+
stage (int >=1 ): for stages > 1, fix data based on sname in earlier stages
71+
sample_branching_factors (list of ints): branching factors for the sample tree
72+
seed (int): To allow random sampling (for some problems, it might be scenario offset)
73+
given_scenario (Pyomo concrete model): if not None, use this to get data for ealier stages
74+
scenario_creator_kwargs (dict): keyword args for the standard scenario creator funcion
75+
Returns:
76+
scenario (Pyomo concrete model): A scenario for sname with data in stages < stage determined
77+
by the arguments
78+
"""
79+
# Since this is a two-stage problem, we don't have to do much.
80+
sca = scenario_creator_kwargs.copy()
81+
sca["seedoffset"] = seed
82+
sca["num_scens"] = sample_branching_factors[0] # two-stage problem
83+
return scenario_creator(sname, **sca)
84+
85+
######## end helper functions #########
86+
87+
########## a customized rho setter #############
88+
# If you are using sizes.py as a starting point for your model,
89+
# you should be aware that you don't need a _rho_setter function.
90+
# This demonstrates how to use a customized rho setter; consider instead
91+
# a gradient based rho setter.
92+
# note that _rho_setter is a reserved name....
93+
94+
def _rho_setter(scen, **kwargs):
95+
""" rho values for the scenario.
96+
Args:
97+
scen (pyo.ConcreteModel): the scenario
98+
Returns:
99+
a list of (id(vardata), rho)
100+
Note:
101+
This rho_setter will not work with proper bundles.
102+
"""
103+
retlist = []
104+
if not hasattr(scen, "UnitReductionCost"):
105+
print("WARNING: _rho_setter not used (probably because of proper bundles)")
106+
return retlist
107+
RF = 0.001 # a factor for rho, if you like
108+
109+
if "RF" in kwargs and isinstance(kwargs["RF"], float):
110+
RF = kwargs["RF"]
111+
112+
cutrho = scen.UnitReductionCost * RF
113+
114+
for i in scen.ProductSizes:
115+
idv = id(scen.NumProducedFirstStage[i])
116+
rho = scen.UnitProductionCosts[i] * RF
117+
retlist.append((idv, rho))
118+
119+
for j in scen.ProductSizes:
120+
if j <= i:
121+
idv = id(scen.NumUnitsCutFirstStage[i, j])
122+
retlist.append((idv, cutrho))
123+
124+
return retlist
125+
126+
127+
def id_fix_list_fct(s):
128+
""" specify tuples used by the fixer.
129+
130+
Args:
131+
s (ConcreteModel): the sizes instance.
132+
Returns:
133+
i0, ik (tuples): one for iter 0 and other for general iterations.
134+
Var id, threshold, nb, lb, ub
135+
The threshold is on the square root of the xbar squared differnce
136+
nb, lb an bu an "no bound", "upper" and "lower" and give the numver
137+
of iterations or None for ik and for i0 anything other than None
138+
or None. In both cases, None indicates don't fix.
139+
"""
140+
import mpisppy.extensions.fixer as fixer
141+
142+
iter0tuples = []
143+
iterktuples = []
144+
for i in s.ProductSizes:
145+
iter0tuples.append(
146+
fixer.Fixer_tuple(s.NumProducedFirstStage[i], th=0.01, nb=None, lb=0, ub=0)
147+
)
148+
iterktuples.append(
149+
fixer.Fixer_tuple(s.NumProducedFirstStage[i], th=0.2, nb=3, lb=1, ub=2)
150+
)
151+
for j in s.ProductSizes:
152+
if j <= i:
153+
iter0tuples.append(
154+
fixer.Fixer_tuple(
155+
s.NumUnitsCutFirstStage[i, j], th=0.5, nb=None, lb=0, ub=0
156+
)
157+
)
158+
iterktuples.append(
159+
fixer.Fixer_tuple(
160+
s.NumUnitsCutFirstStage[i, j], th=0.2, nb=3, lb=1, ub=2
161+
)
162+
)
163+
164+
return iter0tuples, iterktuples

mpisppy/cylinders/spoke.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,8 @@ def __init__(self, spbase_object, fullcomm, strata_comm, cylinder_comm, options=
327327
self.best_inner_bound = math.inf if self.is_minimizing else -math.inf
328328
self.solver_options = None # can be overwritten by derived classes
329329

330-
def update_if_improving(self, candidate_inner_bound, update_cache=True):
331-
if update_cache:
330+
def update_if_improving(self, candidate_inner_bound, update_best_solution_cache=True):
331+
if update_best_solution_cache:
332332
update = self.opt.update_best_solution_if_improving(candidate_inner_bound)
333333
else:
334334
update = ( (candidate_inner_bound < self.best_inner_bound)

0 commit comments

Comments
 (0)