Stochastic models
The @stochastic_model
command is now introduced in more detail. The discussion will as before revolve around the simple example introduced in the Quick start:
@stochastic_model simple_model begin
@stage 1 begin
@decision(simple_model, x₁ >= 40)
@decision(simple_model, x₂ >= 20)
@objective(simple_model, Min, 100*x₁ + 150*x₂)
@constraint(simple_model, x₁ + x₂ <= 120)
end
@stage 2 begin
@known(simple_model, x₁, x₂)
@uncertain q₁ q₂ d₁ d₂
@recourse(simple_model, 0 <= y₁ <= d₁)
@recourse(simple_model, 0 <= y₂ <= d₂)
@objective(simple_model, Max, q₁*y₁ + q₂*y₂)
@constraint(simple_model, 6*y₁ + 10*y₂ <= 60*x₁)
@constraint(simple_model, 8*y₁ + 5*y₂ <= 80*x₂)
end
end
Note, that the resulting model object is stored in simple_model
, and that the same name is used to reference the stochastic program in the @stage
blocks. The following anonymous syntax is also supported:
simple_model = @stochastic_model begin
@stage 1 begin
@decision(model, x₁ >= 40)
@decision(model, x₂ >= 20)
@objective(model, Min, 100*x₁ + 150*x₂)
@constraint(model, x₁ + x₂ <= 120)
end
@stage 2 begin
@known(model, x₁, x₂)
@uncertain q₁ q₂ d₁ d₂
@recourse(model, 0 <= y₁ <= d₁)
@recourse(model, 0 <= y₂ <= d₂)
@objective(model, Max, q₁*y₁ + q₂*y₂)
@constraint(model, 6*y₁ + 10*y₂ <= 60*x₁)
@constraint(model, 8*y₁ + 5*y₂ <= 80*x₂)
end
end
where the reserved keyword model
is used in the @stage
blocks.
@stage
blocks
The body of a @stochastic_model
definition consists of a number of @stage
blocks, following the syntax:
@stage N begin
...
end
Here, N
is the stage number and the body is made up of JuMP syntax as well as @parameters
, @decision
, and @uncertain
blocks. At least two stages must be defined and the stages must be defined in consecutive order starting with the first stage. The number of stage blocks included in the @stochastic_model
definition determines the number of stages that a stochastic program instantiated from the resulting stochastic model will have.
It is possible to define and instantiate stochastic models with more than two stages. However, most internal tools and solvers only support two-stage models at this point.
@parameters
blocks
The @parameters
blocks are used to introduce deterministic parameters to a @stage
block. See for example Stage data. The following:
@parameters a b
makes the constants a
and b
available as model parameters. This incurs a promise that those parameters will be injected when instantiating the model, and if no default values are available they must be supplied by the user. In other words, if sm
is a stochastic model that includes the above @parameters
annotation in one of its @stage
blocks, then those parameters must be supplied as keyword arguments when instantiating stochastic programs using this model:
instantiate(sm, scenarios, a = 1, b = 2)
Alternatively, default values can be specified directly in the @parameters
block:
@parameters begin
a = 1
b = 2
end
Values supplied to instantiate
are always used, and otherwise the default values are used. The responsibility is on the user to ensure that the supplied parameters support the operations used in the @stage
blocks. Parameters can be reused in multiple blocks, but each occurance must be annotated by @parameters
in each of the stages.
@decision
blocks
The @decision
blocks are used to annotate linking variables between stages. Their usage is identical syntax-wise to JuMP's @variable
macros. Internally, they create specialized JuMP variables with context-dependent behaviour.
@recourse
blocks
The @recourse
macro functions like the @decision
macro, but is only used to annotate recourse decisions in the final stage.
@known
blocks
A @known
annotation is used in subsequent stages to bring a decision defined in a previous stage into scope. Any decision defined by @decision
inside a @stochastic_model
automatically annotates subsequent stages with appropriate @known
lines.
The @known
block in the simple example above is given by
@known(simple_moel, x₁, x₂)
This states that the second stage of the stochastic model depends on the decisions x₁
and x₂
taken in the previous stage. Note again that this lines is implicitly added by @stochastic_model
and is not required.
@uncertain
blocks
The @uncertain
blocks are used to annotate stochastic data in the stochastic model. For flexibility, there are several different ways of doing this. However, an @uncertain
annotation is always connected to some AbstractScenario
type, as introduced in Scenario data. Note, that a @stage
block can only include one @uncertain
block. All stochastic information in a given stage must therefore be captured in the @uncertain
block of that stage.
The most simple approach is to use leverage the Scenario
type. Consider the @uncertain
annotation given above:
@uncertain q₁ q₂ d₁ d₂
This will ensure that Scenario
s that are expected to have the fields q₁
, q₂
, d₁
and d₂
are injected when constructing second-stage models. Each such scenario must be supplied or sampled using a supplied sampler object. It is the responsibility of the user to ensure that each supplied or sampled Scenario
has the correct fields. For example, the following yields a Scenario
compatible with the above @uncertain
line:
Scenario(q₁ = 24.0,
q₂ = 28.0,
d₁ = 500.0,
d₂ = 100.0,
probability = 0.4)
Alternatively, the same scenario is conveniently created using the @scenario
macro, matching the syntax of the @uncertain
declaration:
@scenario q₁ = 24.0 q₂ = 28.0 d₁ = 500.0 d₂ = 100.0 probability = 0.4
We can also use JuMP's container syntax:
@uncertain ξ[1:5]
@uncertain ξ[i in 1:5]
@uncertain ξ[i in 1:5, i != 3]
@uncertain ξ[i in 1:5, j in 1:5]
@uncertain ξ[i in 1:5, k in [:a,:b,:c]]
and then use the a corresponding formulation in the @scenario
macro to generate scenarios:
ξ = @scenario ξ[1:5] = rand(5) probability = rand()
ξ = @scenario ξ[i in 1:5] i * rand() probability = rand()
ξ = @scenario ξ[i in 1:5, i != 3] i * rand() probability = rand()
ξ = @scenario ξ[i in 1:5, j in 1:5] = rand(5,5) probability = rand()
ξ = @scenario ξ[i in 1:5, k in [:a,:b,:c]] = rand(5,5) probability = rand()
Note, that we above sometimes assign the full random vector directly, and sometimes provide an indexed based formula.
As shown in Stochastic data, it is also possible to introduce other scenario types, either using @define_scenario
or manally as explained in Custom scenarios and demonstrated in the Continuous scenario distribution example. If we instead define the necessary scenario structure as follows:
@define_scenario SimpleScenario = begin
q₁::Float64
q₂::Float64
d₁::Float64
d₂::Float64
end
One can then use:
@uncertain ξ::SimpleScenario
and extract the required fields from ξ
which will be of type SimpleScenario
after data injection. Again, it is the responsibility of the user to supply scenarios of this type when instantiating the model. For example, the following constructs a SimpleScenario
compatible with the above @uncertain
line:
SimpleScenario(-24.0, -28.0, 500.0, 100.0, probability = 0.4)
It is also possible to directly unpack the necessary fields using the following syntactic sugar:
@uncertain q₁ q₂ d₁ d₂ from SimpleScenario
The actual scenario instance can still be annotated and used if necessary:
@uncertain q₁ q₂ d₁ d₂ from ξ::SimpleScenario
Finally, if the @uncertain
block is used within a @stochastic_model
environment, it is possible to simultaneosly define the underlying scenario type. In other words,
@uncertain ξ::SimpleScenario = begin
q₁::Float64
q₂::Float64
d₁::Float64
d₂::Float64
end
@uncertain q₁ q₂ d₁ d₂ from SimpleScenario = begin
q₁::Float64
q₂::Float64
d₁::Float64
d₂::Float64
end
and
@uncertain q₁ q₂ d₁ d₂ from ξ::SimpleScenario = begin
q₁::Float64
q₂::Float64
d₁::Float64
d₂::Float64
end
are all possible methods of defining and using the SimpleScenario
type in a @stage
block.
Model instantiation
A model object sm
defined using @stochastic_model
can be used to instantiate stochastic programs over both finite/infinite sample spaces and discrete/continuous random variables.
If the scenarios are associated with a discrete random variable over a finite sample space, then the corresponding stochastic program is finite and can be instantiated by providing the full list of scenarios:
sp = instantiate(sm, scenarios)
Here, scenarios
is a vector of scenarios consistent with the @uncertain
annotation used in the second stage of sm
. It is the responsibility of the user to ensure that the individual probabilities of the scenarios
sum up to one, so that the model is consistent.
If the scenarios are instead associated with a continuous random variable, with finite second moments, over an infinite sample space, then the corresponding stochastic program is not finite and must be approximated. The only supported way of doing so in StochasticPrograms is by using sampled average approximations. A finite stochastic program that approximates the stochastic model is obtained through
sp = instantiate(sm, sampler, n)
where sampler
is an AbstractSampler
, as outlined in Sampling, and n
is the number of samples to include.
Instant models
It is possible to create one-off stochastic programs without needing to first define a model object. To do so, any required scenario data structure must be defined first. Consider:
using StochasticPrograms
@define_scenario SimpleScenario = begin
q₁::Float64
q₂::Float64
d₁::Float64
d₂::Float64
end
ξ₁ = SimpleScenario(-24.0, -28.0, 500.0, 100.0, probability = 0.4)
ξ₂ = SimpleScenario(-28.0, -32.0, 300.0, 300.0, probability = 0.6)
SimpleScenario with probability 0.6
q₁: -28.0
q₂: -32.0
d₁: 300.0
d₂: 300.0
Next, an unmodeled stochastic program can be instantiated using the two created scenarios:
sp = StochasticProgram([ξ₁, ξ₂], Deterministic())
Deferred stochastic program with 2 scenarios
Note that we must provide the instantiation type explicitly as well. A slightly diferrent modeling syntax is now used to define the stage models of sp
:
@first_stage sp = begin
@decision(sp, x₁ >= 40)
@decision(sp, x₂ >= 20)
@objective(sp, Min, 100*x₁ + 150*x₂)
@constraint(sp, x₁ + x₂ <= 120)
end
@second_stage sp = begin
@known(sp, x₁, x₂)
@uncertain q₁ q₂ d₁ d₂ from SimpleScenario
@recourse(sp, 0 <= y₁ <= d₁)
@recourse(sp, 0 <= y₂ <= d₂)
@objective(sp, Min, q₁*y₁ + q₂*y₂)
@constraint(sp, 6*y₁ + 10*y₂ <= 60*x₁)
@constraint(sp, 8*y₁ + 5*y₂ <= 80*x₂)
end
Here, @first_stage
and @second_stage
are just syntactic sugar for @stage 1
and @stage 2
. Note, that the model name sp
must be used internally in the @stage
blocks when referencing the model. This is is the definition syntax used internally by StochasticModel
objects when instantiating stochastic programs. Note, that we must explicitly add the @known
annotations to the second stage with this approach, while models created using @stochastic_model
does this automatically. We can verify that this approach yields the same stochastic program by printing and comparing to the Quick start:
print(sp)
Deterministic equivalent problem
Min 100 x₁ + 150 x₂ - 9.600000000000001 y₁₁ - 11.200000000000001 y₂₁ - 16.8 y₁₂ - 19.2 y₂₂
Subject to
x₁ ∈ Decisions
x₂ ∈ Decisions
y₁₁ ∈ RecourseDecisions
y₂₁ ∈ RecourseDecisions
y₁₂ ∈ RecourseDecisions
y₂₂ ∈ RecourseDecisions
x₁ ≥ 40.0
x₂ ≥ 20.0
y₁₁ ≥ 0.0
y₂₁ ≥ 0.0
y₁₂ ≥ 0.0
y₂₂ ≥ 0.0
x₁ + x₂ ≤ 120.0
-60 x₁ + 6 y₁₁ + 10 y₂₁ ≤ 0.0
-80 x₂ + 8 y₁₁ + 5 y₂₁ ≤ 0.0
-60 x₁ + 6 y₁₂ + 10 y₂₂ ≤ 0.0
-80 x₂ + 8 y₁₂ + 5 y₂₂ ≤ 0.0
y₁₁ ≤ 500.0
y₂₁ ≤ 100.0
y₁₂ ≤ 300.0
y₂₂ ≤ 300.0
Solver name: No optimizer attached.
As a side note, it is possible to run stage definition macros on programs with existing models. This overwrites the previous model and upon regeneration all internal problems. For example, the following increases the lower bound on the second stage variables to 2:
@stage 2 sp = begin
@known(sp, x₁, x₂)
@uncertain q₁ q₂ d₁ d₂ from SimpleScenario
@recourse(sp, 2 <= y₁ <= d₁)
@recourse(sp, 2 <= y₂ <= d₂)
@objective(sp, Min, q₁*y₁ + q₂*y₂)
@constraint(sp, 6*y₁ + 10*y₂ <= 60*x₁)
@constraint(sp, 8*y₁ + 5*y₂ <= 80*x₂)
end
generate!(sp)
print(sp)
Deterministic equivalent problem
Min 100 x₁ + 150 x₂ - 9.600000000000001 y₁₁ - 11.200000000000001 y₂₁ - 16.8 y₁₂ - 19.2 y₂₂
Subject to
x₁ ∈ Decisions
x₂ ∈ Decisions
y₁₁ ∈ RecourseDecisions
y₂₁ ∈ RecourseDecisions
y₁₂ ∈ RecourseDecisions
y₂₂ ∈ RecourseDecisions
x₁ ≥ 40.0
x₂ ≥ 20.0
y₁₁ ≥ 2.0
y₂₁ ≥ 2.0
y₁₂ ≥ 2.0
y₂₂ ≥ 2.0
x₁ + x₂ ≤ 120.0
-60 x₁ + 6 y₁₁ + 10 y₂₁ ≤ 0.0
-80 x₂ + 8 y₁₁ + 5 y₂₁ ≤ 0.0
-60 x₁ + 6 y₁₂ + 10 y₂₂ ≤ 0.0
-80 x₂ + 8 y₁₂ + 5 y₂₂ ≤ 0.0
y₁₁ ≤ 500.0
y₂₁ ≤ 100.0
y₁₂ ≤ 300.0
y₂₂ ≤ 300.0
Solver name: No optimizer attached.
It is of course also possible to do this on programs instantiated from a StochasticModel
.
SMPS
StochasticPrograms also support reading models specified in the SMPS format. Specifically, SMPS definitions with uncertain data of types INDEP or BLOCKS are supported. We show how the simple example can be specified in SMPS. An SMPS definition consist of the following files:
- problem.smps
- problem.tim
- problem.cor
- problem.sto
Here, problem.smps
is just an empty file that shares the name with the others to simplify IO commands. The problem.tim
file specifies the stage structure of the stochastic program. An example for the simple problem is given below.
TIME SIMPLE
PERIODS
X1 BOUND STAGE1
Y1 LINK1 STAGE2
ENDATA
Row and column delimeters are given for each stage. The problem.cor
file specifies the optimization structure of the problem in MPS format. An example for the simple problem as follows.
NAME SIMPLE
ROWS
N OBJ
L BOUND
L LINK1
L LINK2
L Y1UP
L Y2UP
COLUMNS
X1 OBJ 100.0 BOUND 1.0
X2 OBJ 150.0 BOUND 1.0
X1 LINK1 -60.0
X2 LINK2 -80.0
Y1 OBJ 26.0 Y1UP 1.0
Y2 OBJ 30.0 Y2UP 1.0
Y1 LINK1 6.0 LINK2 8.0
Y2 LINK1 10.0 LINK2 5.0
RHS
RHS BOUND 120.0
RHS LINK1 0.0
RHS LINK2 0.0
RHS Y1UP 400.0
RHS Y2UP 200.0
BOUNDS
LO X1LIM X1 40.0
LO X2LIM X2 20.0
LO Y1LIM Y1 0.0
LO Y2LIM Y2 0.0
ENDATA
Finally, the problem.sto
file specifies the uncertain data. We use the BLOCKS format to specify the simple scenarios.
STOCH SIMPLE
BLOCKS DISCRETE
BL BLOCK1 STAGE2 0.4
Y1 OBJ -24.0
Y2 OBJ -28.0
RHS Y1UP 500.0
RHS Y2UP 100.0
BL BLOCK1 STAGE2 0.6
Y1 OBJ -28.0
Y2 OBJ -32.0
RHS Y1UP 300.0
RHS Y2UP 300.0
ENDATA
We specify the two scenarios, giving the value in each corresponding coordinate in the problem.cor
file. Now, we can read this model into Julia in several ways, assuming all files are in the same folder. First, consider
model = read("problem.smps", StochasticModel)
Two-Stage Stochastic Model
minimize f₀(x) + 𝔼[f(x,ξ)]
x∈𝒳
where
f(x,ξ) = min f(y; x, ξ)
y ∈ 𝒴 (x, ξ)
This returns a StochasticModel
object that can be used as usual, assuming it is instantiated with the scenarios of the special SMPSScenario
type. To that end, we can read a specialized sampler object for the specified SMPS model:
sampler = read("problem.smps", SMPSSampler)
sampler()
SMPSScenario with probability 1.0 and underlying data:
Δq = [0.0, 0.0, -54.0, -62.0]
ΔT = 0×2 SparseArrays.SparseMatrixCSC{Float64,Int64} with 0 stored entries
ΔW = 0×2 SparseArrays.SparseMatrixCSC{Float64,Int64} with 0 stored entries
Δh = Float64[]
ΔC = 4×4 SparseArrays.SparseMatrixCSC{Float64,Int64} with 0 stored entries
Δd₁ = [0.0, 0.0, 0.0, 0.0]
Δd₂ = [0.0, 0.0, -100.0, 100.0]
We can now instantiate a specific instance of the read model:
sp = instantiate(model, sampler, 2)
Stochastic program with:
* 2 decision variables
* 2 recourse variables
* 2 scenarios of type SMPSScenario
Structure: Deterministic equivalent
Solver name: No optimizer attached.
The same result, up to sampling, can be achieved directly through
sp = read("problem.smps", StochasticProgram, num_scenarios = 2)
Stochastic program with:
* 2 decision variables
* 2 recourse variables
* 2 scenarios of type SMPSScenario
Structure: Deterministic equivalent
Solver name: No optimizer attached.
This read
variant takes the same keyword arguments as instantiate
. Because the specified scenario structure in BLOCKS or INDEP format has finite support, it is possible to read the stochastic program corresponding to the full support by not specifying num_scenarios
:
sp = read("problem.smps", StochasticProgram)
Stochastic program with:
* 2 decision variables
* 2 recourse variables
* 2 scenarios of type SMPSScenario
Structure: Deterministic equivalent
Solver name: No optimizer attached.
In this case, the full support correspond exactly to the simple model we have considered before:
print(sp)
Deterministic equivalent problem
Min 100 x[1] + 150 x[2] - 16.8 y₂[1] - 19.2 y₂[2] - 9.600000000000001 y₁[1] - 11.200000000000001 y₁[2]
Subject to
y₁[1] ≥ 0.0
y₁[2] ≥ 0.0
y₂[1] ≥ 0.0
y₂[2] ≥ 0.0
[x[1], x[2]] ∈ Decisions
x[1] ≥ 40.0
x[2] ≥ 20.0
[x[1] + x[2] - 120] ∈ MathOptInterface.Nonpositives(1)
[-60 x[1] + 6 y₁[1] + 10 y₁[2], -80 x[2] + 8 y₁[1] + 5 y₁[2], y₁[1] - 500, y₁[2] - 100] ∈ MathOptInterface.Nonpositives(4)
[-60 x[1] + 6 y₂[1] + 10 y₂[2], -80 x[2] + 8 y₂[1] + 5 y₂[2], y₂[1] - 300, y₂[2] - 300] ∈ MathOptInterface.Nonpositives(4)
Solver name: No optimizer attached.
A warning is issued if the full support contains more than 1e5
scenarios.