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:

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 x₁ x₂
        @uncertain q₁ q₂ d₁ d₂
        @variable(model, 0 <= y₁ <= d₁)
        @variable(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

@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.

Note

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.

@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 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 the Scenario type, which is based on named tuples. Consider the @uncertain annotation given above:

@uncertain q₁ q₂ d₁ d₂

This will ensure that Scenarios 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)

We can also use JuMP's container syntax:

@uncertain ξ[i in 1:5]

and then use the @container_scenario macro to generate scenarios:

ξ = @container_scenario([i in 1:5], i^2)

As shown in Stochastic data, it is also possible to introduce other scenario types, either using @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:

@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

@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

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
    @variable(model, x₁ >= 40)
    @variable(model, x₂ >= 20)
    @objective(model, Min, 100*x₁ + 150*x₂)
    @constraint(model, x₁ + x₂ <= 120)
end
@second_stage sp = begin
    @known x₁ x₂
    @uncertain q₁ q₂ d₁ d₂ from SimpleScenario
    @variable(model, 0 <= y₁ <= d₁)
    @variable(model, 0 <= y₂ <= d₂)
    @objective(model, Min, q₁*y₁ + q₂*y₂)
    @constraint(model, 6*y₁ + 10*y₂ <= 60*x₁)
    @constraint(model, 8*y₁ + 5*y₂ <= 80*x₂)
end
Deferred stochastic program

Here, @first_stage and @second_stage are just syntactic sugar for @stage 1 and @stage 2. 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. We can verify that this approach yields the same stochastic program by printing and comparing to the Quick start:

print(sp)
Deferred stochastic program

As a side note, it is possible to run stage definition macros on programs with existing models. This overwrites the previous model and reinstantiates all internal problems. For example, the following increases the lower bound on the second stage variables to 2:

@second_stage sp = begin
    @known x₁ x₂
    @uncertain q₁ q₂ d₁ d₂ from SimpleScenario
    @variable(model, 2 <= y₁ <= d₁)
    @variable(model, 2 <= y₂ <= d₂)
    @objective(model, Min, q₁*y₁ + q₂*y₂)
    @constraint(model, 6*y₁ + 10*y₂ <= 60*x₁)
    @constraint(model, 8*y₁ + 5*y₂ <= 80*x₂)
end

print(sp)
Deferred stochastic program

It is of course also possible to do this on programs instantiated from a StochasticModel.