Examples
If you installed directly from GitHub, the top
level directory examples
contains some sub-directories with examples.
If you did not get the code from GitHub (e.g., if you installed with pip), you will need to get the examples from: https://github.com/Pyomo/mpi-sppy/tree/master/examples
Tutorial: Farmer Example
In this section, we step through a simple example—the farmer example of [birge2011] (Section 1.1). This model can be expressed as the following linear program (LP):
The decision variables are as follows:
\(x_i\) = number of acres to devote to crop i (1=wheat, 2=corn, 3=sugar beets)
\(y_i\) = tons of crop i to purchase from a wholesaler (i=1,2 but not 3)
\(w_i\) = tons of crop i sold (i=1,2)
\(w_i\) = tons of beets sold at favorable (i=3) or unfavorable (i=4) price
The coefficients of the \(x_i\) variables in the second, third and fourth constraints are the number of tons per acre that each crop will yield (2.5 for wheat, 3 for corn, and 20 for sugar beets).
The following code creates an instance of the farmer’s model:
import pyomo.environ as pyo
def build_model(yields):
model = pyo.ConcreteModel()
# Variables
model.X = pyo.Var(["WHEAT", "CORN", "BEETS"], within=pyo.NonNegativeReals)
model.Y = pyo.Var(["WHEAT", "CORN"], within=pyo.NonNegativeReals)
model.W = pyo.Var(
["WHEAT", "CORN", "BEETS_FAVORABLE", "BEETS_UNFAVORABLE"],
within=pyo.NonNegativeReals,
)
# Objective function
model.PLANTING_COST = 150 * model.X["WHEAT"] + 230 * model.X["CORN"] + 260 * model.X["BEETS"]
model.PURCHASE_COST = 238 * model.Y["WHEAT"] + 210 * model.Y["CORN"]
model.SALES_REVENUE = (
170 * model.W["WHEAT"] + 150 * model.W["CORN"]
+ 36 * model.W["BEETS_FAVORABLE"] + 10 * model.W["BEETS_UNFAVORABLE"]
)
model.OBJ = pyo.Objective(
expr=model.PLANTING_COST + model.PURCHASE_COST - model.SALES_REVENUE,
sense=pyo.minimize
)
# Constraints
model.CONSTR= pyo.ConstraintList()
model.CONSTR.add(pyo.summation(model.X) <= 500)
model.CONSTR.add(
yields[0] * model.X["WHEAT"] + model.Y["WHEAT"] - model.W["WHEAT"] >= 200
)
model.CONSTR.add(
yields[1] * model.X["CORN"] + model.Y["CORN"] - model.W["CORN"] >= 240
)
model.CONSTR.add(
yields[2] * model.X["BEETS"] - model.W["BEETS_FAVORABLE"] - model.W["BEETS_UNFAVORABLE"] >= 0
)
model.W["BEETS_FAVORABLE"].setub(6000)
return model
Note that the build_model
function takes a list of values, containing the
yields for each crop. We can solve the model:
yields = [2.5, 3, 20]
model = build_model(yields)
solver = pyo.SolverFactory("cplex_direct")
solver.solve(model)
# Display the objective value to one decimal place
print(f"{pyo.value(model.OBJ):.1f}")
The optimal objective value is:
-118600.0
In practice, the farmer does not know the number of tons that each crop will yield per acre planted–the yield depends on the weather, the quality of the seeds, and other stochastic factors. Consequently, we replace the deterministic model above with the stochastic LP:
The variables \(y_i\) and \(w_i\) have been replaced with copies \(y_i^\omega\) and \(w_i^\omega\), corresponding to the values of each variable chosen under scenario \(\omega\in\Omega\), where \(\Omega\) is a finite set of scenarios. The parameter \(\xi^\omega_i\) is the number of tons of crop \(i\) yielded per acre under scenario \(\omega\).
We assume that there are three scenarios: “good”, “bad”, and “average”. We assume that each scenario is equally likely to occur. The yield values (\(\xi^\omega_i\)) are given here:
Wheat |
Corn |
Sugar Beets |
|
---|---|---|---|
Good |
3 |
3.6 |
24 |
Average |
2.5 |
3 |
20 |
Bad |
2 |
2.4 |
16 |
In order to transform the code for the deterministic model above into a
stochastic model which can be manipulated by MPI-SPPy, we need only incorporate
a few extra elements (see scenario_creator function for full details). The
scenario_creator
function is told the name of the scenario to build, and
builds a Pyomo model for that scenario appropriately:
import mpisppy.utils.sputils as sputils
def scenario_creator(scenario_name):
if scenario_name == "good":
yields = [3, 3.6, 24]
elif scenario_name == "average":
yields = [2.5, 3, 20]
elif scenario_name == "bad":
yields = [2, 2.4, 16]
else:
raise ValueError("Unrecognized scenario name")
model = build_model(yields)
sputils.attach_root_node(model, model.PLANTING_COST, [model.X])
model._mpisppy_probability = 1.0 / 3
return model
The scenario_creator
accomplishes two important tasks
It calls the
attach_root_node
function. We tell this function which part of the objective function (model.PLANTING_COST
) and which set of variables (model.X
) belong to the first stage. In this case, the problem is only two stages, so we need only specify the root node and the first-stage information–MPI-SPPy assumes the remainder of the model belongs to the second stage.It attaches an attribute called
_mpisppy_probability
to the model object. This is the probability that the specified scenario occurs. If this probability is not specified, MPI-SPPy will assume that all scenarios are equally likely.
Now that we have specified a scenario creator, we can use MPI-SPPy to solve the farmer’s stochastic program.
Solving the Extensive Form
The simplest approach is to solve the extensive form of the model directly. MPI-SPPy makes this quite simple:
from mpisppy.opt.ef import ExtensiveForm
options = {"solver": "cplex_direct"}
all_scenario_names = ["good", "average", "bad"]
ef = ExtensiveForm(options, all_scenario_names, scenario_creator)
results = ef.solve_extensive_form()
objval = ef.get_objective_value()
print(f"{objval:.1f}")
...
-108390.0
We can extract the optimal solution itself using the get_root_solution
method of the ExtensiveForm
object:
soln = ef.get_root_solution()
for (var_name, var_val) in soln.items():
print(var_name, var_val)
X[BEETS] 250.0
X[CORN] 80.0
X[WHEAT] 170.0
Solving Using Progressive Hedging (PH)
We can also solve the model using the progressive hedging (PH) algorithm. First, we must construct a PH object:
from mpisppy.opt.ph import PH
options = {
"solver_name": "cplex_persistent",
"PHIterLimit": 5,
"defaultPHrho": 10,
"convthresh": 1e-7,
"verbose": False,
"display_progress": False,
"display_timing": False,
"iter0_solver_options": dict(),
"iterk_solver_options": dict(),
}
all_scenario_names = ["good", "average", "bad"]
ph = PH(
options,
all_scenario_names,
scenario_creator,
)
Note that all of the options in the options
dict must be specified in order
to construct the PH object. Once the PH object is constructed, we can execute
the algorithm with a call to the ph_main
method:
ph.ph_main()
[ 0.00] Start SPBase.__init__
[ 0.01] Start PHBase.__init__
[ 0.01] Creating solvers
[ 0.01] Entering solve loop in PHBase.Iter0
[ 2.80] Reached user-specified limit=5 on number of PH iterations
Note that precise timing results may differ. In this toy example, we only execute 5 iterations of the algorithm. Although the algorithm does not converge completely, we can see that the first-stage variables already exhibit relatively good agreement:
variables = ph.gather_var_values_to_rank0()
for (scenario_name, variable_name) in variables:
variable_value = variables[scenario_name, variable_name]
print(scenario_name, variable_name, variable_value)
good X[BEETS] 280.6489711937925
good X[CORN] 85.26131687116064
good X[WHEAT] 134.0897119350402
average X[BEETS] 283.2796296293019
average X[CORN] 80.00000000014425
average X[WHEAT] 136.72037037055298
bad X[BEETS] 280.64897119379475
bad X[CORN] 85.26131687116226
bad X[WHEAT] 134.08971193504266
The function gather_var_values_to_rank0
can be used in parallel to collect
the values of all non-anticipative variables at the root. In this (serial)
example, it simply returns the values of the first-stage variables.
Solving Using Benders’ Decomposition
Finally, we can solve our example using Benders’ decomposition, known as the L-shaped method in stochastic programming. The setup code is similar to the previous methods:
from mpisppy.opt.lshaped import LShapedMethod
all_scenario_names = ["good", "average", "bad"]
bounds = {name: -432000 for name in all_scenario_names}
options = {
"root_solver": "cplex_persistent",
"sp_solver": "cplex_persistent",
"sp_solver_options" : {"threads" : 1},
"valid_eta_lb": bounds,
"max_iter": 10,
}
ls = LShapedMethod(options, all_scenario_names, scenario_creator)
result = ls.lshaped_algorithm()
variables = ls.gather_var_values_to_rank0()
for ((scen_name, var_name), var_value) in variables.items():
print(scen_name, var_name, var_value)
[ 0.00] Start SPBase.__init__
Current Iteration: 1 Time Elapsed: 0.00 Current Objective: -Inf
Current Iteration: 2 Time Elapsed: 0.01 Time Spent on Last Master: 0.00 Time Spent Generating Last Cut Set: 0.01 Current Objective: -1296000.00
Current Iteration: 3 Time Elapsed: 0.02 Time Spent on Last Master: 0.00 Time Spent Generating Last Cut Set: 0.01 Current Objective: -160000.00
Current Iteration: 4 Time Elapsed: 0.02 Time Spent on Last Master: 0.00 Time Spent Generating Last Cut Set: 0.00 Current Objective: -113750.00
Converged in 4 iterations.
Total Time Elapsed: 0.03 Time Spent on Last Master: 0.00 Time spent verifying second stage: 0.00 Final Objective: -108390.00
good X[BEETS] 250.0
good X[CORN] 80.0
good X[WHEAT] 170.0
average X[BEETS] 250.0
average X[CORN] 80.0
average X[WHEAT] 170.0
bad X[BEETS] 250.0
bad X[CORN] 80.0
bad X[WHEAT] 170.0
We see that, for this toy example, the L-shaped method has converged to the optimal solution within just 10 iterations.
aircond
This is fairly complicated example because it is multi-stage and the
model itself offers a lot of flexibility. The aircond example is
unusual in that the model file, aircond.py
, lives in
mpisppy.tests.examples
directory. Scripts and bash files that use
it live in examples.aircond
. A good place to start is the
aircond_cylinders.py
file that starts with some functions that
support the main program. The main program makes use of the
Config
object called cfg that creates a parser and gets arguments.
The configuration data obtained by the parser are passed directly to the vanilla hub
and spoke creator which knows how to use the arguments from a Config
object.
The arguments unique to aircond are processed by the create_kwargs
function
in the reference model file.
A simple example that uses a few of the options is shown in aircond_zhat.bash
, which
also calls the xhat4xhat
program to estimate confidence intervals for the solution
obtained.
hydro
Hydro is a three stage example that was originally coded in PySP and we make extensive use of the PySP files. Unlike farmer and aircond where the scenario data are created from distributions, for this problem the scenario data are provided in files.
Using PySPModel
In the file hydro_cylinders_pysp.py
the lines
from mpisppy.utils.pysp_model import PySPModel
...
hydro = PySPModel("./PySP/models/", "./PySP/nodedata/")
cause an object called hydro
to be created that has the methods needed by vanilla and the hub and
spoke creators as can be seen in the main
function of hydro_cylinders_pysp.py
.
Not using PySPModel
In the file hydro_cylinders.py
the file hydro.py
is imported because it provides the functions
needed by vanilla hub and spoke creators.
netdes
This is a very challenging network design problem, which has many instances each defined by a data file.
For this problem, cross scenario cuts are helpful
so the use of that spoke is illustrated in netdes_cylinders.py
.
sslp
This is a classic problem from Ntaimo and Sen with data in PySP format
so the driver code (e.g., sslp_cylinders.py
that makes use of sslp.py
) is somewhat similar to the
hydro example except sslp is simpler because it is just two stages.
UC
This example uses the egret
package for the underlying unit commitment model
and reads PySP format data using the pyomo
dataportal. Data files for a variety
of numbers of scenarios are provided.
sizes
The sizes example (Jorjani et al, IJPR, 1999) is a two-stage problem with general integers in each stage. The file
sizes_cylinders.py
is the usual cylinders driver. There are other examples in the directory, such
as sizes_demo.py
, which provides an example of serial execution (no cylinders).