Basic Example
Problem
A basic example using ProgressiveHedging.jl to solve a stochastic program. This example is also available as the script basic_example.jl in the example directory.
Here we will use ProgressiveHedging.jl to solve the simple problem
$\min_x x + \sum_{s=0}^1 p_s c_s y_s$
subject to
$x \ge 0$
$y_s \ge 0, s \in {0,1}$
$x + y = b_s$
where
$b_s = \begin{cases} 11, & s=0 \\ 4, & s=1 \end{cases},$
$c_s = \begin{cases} 0.5, & s=0 \\ 10, & s=1 \end{cases}$
and for now we will take equally probable scenarios, that is, $p_0 = p_1 = 0.5$.
Setup
First we need to bring in the needed packages
using ProgressiveHedging
import JuMP
import IpoptWe will need JuMP to build the model for each subproblem and we will use Ipopt to solve each subproblem.
There are some functions that both JuMP and ProgressiveHedging export. To avoid confusion (and warnings) it is best to import one or both of these packages.
Subproblem Creation Function
Next, we write a function that will generate the subproblems for each scenario. The following creates the subproblem for a simple two stage stochastic program.
function two_stage_model(scenario_id::ScenarioID)
model = JuMP.Model(()->Ipopt.Optimizer())
JuMP.set_optimizer_attribute(model, "print_level", 0)
JuMP.set_optimizer_attribute(model, "tol", 1e-12)
JuMP.set_optimizer_attribute(model, "acceptable_tol", 1e-12)
scen = value(scenario_id)
ref = JuMP.@variable(model, x >= 0.0)
stage1 = [ref]
ref = JuMP.@variable(model, y >= 0.0)
stage2 = [ref]
b_s = scen == 0 ? 11.0 : 4.0
c_s = scen == 0 ? 0.5 : 10.0
JuMP.@constraint(model, x + y == b_s)
JuMP.@objective(model, Min, 1.0*x + c_s*y)
return JuMPSubproblem(model,
scenario_id,
Dict(stid(1) => stage1,
stid(2) => stage2)
)
endThere are a few things to note here:
- This function must take as an argument a
ScenarioID. - The mathematical model is built using JuMP.
- The function returns a
JuMPSubproblem. This is an implementation of theAbstractSubprobleminterface which uses JuMP as the algebraic modeling language. - In addition to the model, the subproblem also requires the
ScenarioIDand a dictionary identifying which variables belong in which stage.
Scenario Tree Construction
Second we need to construct a ScenarioTree that captures the structure of our stochastic program. We can do this using the ScenarioTree constructor and the function add_leaf:
scen_tree = ScenarioTree()
scenario_id_0 = add_leaf(scen_tree, root(scen_tree), 0.5)
scenario_id_1 = add_leaf(scen_tree, root(scen_tree), 0.5)Here we created a scenario tree and added two leaf nodes each representing the two scenarios in our problem. We specified both occur with probability 0.5. The other thing to note is that add_leaf returns a ScenarioID. This is a type-safe integer that PH uses to uniquely indentify the each scenario and subproblem. Note that the numbering starts at 0.
@show scenario_id_0
@show scenario_id_1scenario_id_0 = ScenarioID(0) scenario_id_1 = ScenarioID(1)
The subproblem created by the subproblem creation function should correspond to the path in the scenario tree that leads to this leaf. In our case, we have a two-stage tree and it does not matter which scenario is identified as scenario 0 and scenario 1.
Since two-stage stochastic programs are extremely common, ProgressiveHedging.jl provides a convenience function to generate two-stage trees with arbitrarily many scenarios: two_stage_tree. So we could have used
scen_tree = two_stage_tree(2)Solution
We are now ready to solve the problem. To do so we just use the solve function and provide it with our scenario tree, the subproblem creation function and a penalty parameter.
(niter, abs_res, rel_res, obj, soln_df, phd) = solve(scen_tree,
two_stage_model,
ScalarPenaltyParameter(1.0)
)
@show niter
@show abs_res
@show rel_res
@show obj
@show soln_dfniter = 39
abs_res = 3.3717478904804885e-6
rel_res = 8.429376739147918e-7
obj = 5.749998288558977
soln_df = 3×4 DataFrame
Row │ variable value stage scenarios
│ String Float64 Int64 String
─────┼─────────────────────────────────────
1 │ x 4.0 1 0,1
2 │ y 7.00001 2 0
3 │ y 0.0 2 1The solve function returns the number of iteration, the absolute and relative residual, the objective value, a DataFrame containing the solution and, lastly, a PHData instance.
The PHData contains additional information like the dual (Lagrange multiplier) values for the nonanticipativity constraints
dual_df = retrieve_w(phd)
@show dual_dfdual_df = 2×5 DataFrame
Row │ variable value stage scenario index
│ String Float64 Int64 Int64 Int64
─────┼───────────────────────────────────────────
1 │ W_x -0.5 1 0 1
2 │ W_x 0.5 1 1 1as well as the raw values of the consensus contributing variables
raw_values_df = retrieve_no_hats(phd)
@show raw_values_dfraw_values_df = 4×5 DataFrame
Row │ variable value stage scenario index
│ String Float64 Int64 Int64 Int64
─────┼───────────────────────────────────────────
1 │ x 3.99999 1 0 1
2 │ x 4.0 1 1 1
3 │ y 7.00001 2 0 1
4 │ y 0.0 2 1 1Determining the Consensus Variables
You may notice that we never specifically indicated which variables needed nonanticipativity constraints. ProgressiveHedging determines this automatically for us using the scenario tree we constructed, the stage the variable is in and the name the variable is given. When using the JuMPSubproblem, the name of the variable is determined by calling the function JuMP.name on each JuMP.VariableRef object. The stage is given by the dictionary given to the JuMPSubproblem constructor as seen in the subproblem creation function above. This is possible because the combination of a ScenarioID and a StageID uniquely determine a node on the scenario tree. Any variables with the same names that belong to this node are assumed to require nonanticipativity constraints and so are identified as consensus variables.
Extensive Solve
For smaller problems, it is also possible to solve the extensive form of the problem directly. ProgressiveHedging.jl is capable of building the extensive form from the previously defined function and scenario tree.
ef_model = solve_extensive(scen_tree,
two_stage_model,
()->Ipopt.Optimizer(),
opt_args=(print_level=0, tol=1e-12, acceptable_tol=1e-12)
)This function builds, optimizes and returns a JuMP model of the extensive form. As such, information is obtained from it as from any other JuMP model.
@show JuMP.termination_status(ef_model)
@show JuMP.objective_value(ef_model)
for v in JuMP.all_variables(ef_model)
println("$v = $(JuMP.value(v))")
endJuMP.termination_status(ef_model) = MathOptInterface.LOCALLY_SOLVED
JuMP.objective_value(ef_model) = 5.749999957500125
y_{0} = 6.99999999000003
x_{0,1} = 4.00000000999997
y_{1} = 0.0The subscripts in the variable names are the scenarios to which the variable belongs.