Quick start
Installation
StochasticPrograms is installed as follows:
pkg> add StochasticPrograms
Afterwards, the functionality can be made available in a module or REPL through:
using StochasticPrograms
Stochastic programs
Consider some probability space $(\Omega,\mathcal{F},\pi)$ where $\Omega$ is a sample space, $\mathcal{F}$ is a $\sigma$-algebra over $\Omega$ and $\pi: \mathcal{F} \to [0,1]$ is a probability measure. Let $\xi(\omega): \Omega \to \mathbb{R}^{N}$ be some random variable on $\Omega$ with finite second moments. A two-stage linear stochastic program has the following mathematical representation:
\[\begin{aligned} \operatorname*{minimize}_{x \in \mathbb{R}^n} & \quad c^T x + \operatorname{\mathbb{E}}_{\omega} \left[Q(x,\xi(\omega))\right] \\ \text{s.t.} & \quad Ax = b \\ & \quad x \geq 0 \end{aligned}\]
where
\[\begin{aligned} Q(x,\xi(\omega)) = \min_{y \in \mathbb{R}^m} & \quad q_{\omega}^T y \\ \text{s.t.} & \quad T_{\omega}x + Wy = h_{\omega} \\ & \quad y \geq 0 \end{aligned}\]
If the sample space $\Omega$ is finite, stochastic program has a closed form that can be represented on a computer. Such functionality is provided by StochasticPrograms. If the sample space $\Omega$ is infinite, sampling techniques can be used to represent the stochastic program using finite instances generated using sample
.
A simple stochastic program
To showcase the use of StochasticPrograms we will walk through a simple example. Consider the following stochastic program: (taken from Introduction to Stochastic Programming).
\[\begin{aligned} \operatorname*{minimize}_{x_1, x_2 \in \mathbb{R}} & \quad 100x_1 + 150x_2 + \operatorname{\mathbb{E}}_{\omega} \left[Q(x_1,x_2,\xi(\omega))\right] \\ \text{s.t.} & \quad x_1+x_2 \leq 120 \\ & \quad x_1 \geq 40 \\ & \quad x_2 \geq 20 \end{aligned}\]
where
\[\begin{aligned} Q(x_1,x_2,\xi(\omega)) = \min_{y_1,y_2 \in \mathbb{R}} & \quad q_1(\omega)y_1 + q_2(\omega)y_2 \\ \text{s.t.} & \quad 6y_1+10y_2 \leq 60x_1 \\ & \quad 8y_1 + 5y_2 \leq 80x_2 \\ & \quad 0 \leq y_1 \leq d_1(\omega) \\ & \quad 0 \leq y_2 \leq d_2(\omega) \end{aligned}\]
and the stochastic variable
\[ \xi(\omega) = \begin{pmatrix} q_1(\omega) & q_2(\omega) & d_1(\omega) & d_2(\omega) \end{pmatrix}^T\]
parameterizes the second-stage model. In the following, we consider how to model, analyze, and solve this stochastic program using StochasticPrograms. In many examples, a MathOptInterface
solver is required. Hence, we load the GLPK solver:
using GLPK
We also load Ipopt to solve quadratic problems:
using Ipopt
Stochastic model definition
First, we define a stochastic model that describes the introduced stochastic program above.
@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
@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
Two-Stage Stochastic Model
minimize f₀(x) + 𝔼[f(x,ξ)]
x∈𝒳
where
f(x,ξ) = min f(y; x, ξ)
y ∈ 𝒴 (x, ξ)
The optimization models in the first and second stage are defined using JuMP syntax inside @stage
blocks. Every first-stage variable is annotated with @decision
. This allows us to use the variable in the second stage. The @uncertain
annotation specifies that the variables q₁
, q₂
, d₁
and d₂
are uncertain. Instances of the uncertain variables will later be injected to create instances of the second stage model. We will consider two stochastic models of the uncertainty and showcase the main functionality of the framework for each.
Finite sample space
First, let $\xi$ be a discrete distribution, taking on the value
\[ \xi_1 = \begin{pmatrix} 24 & 28 & 500 & 100 \end{pmatrix}^T\]
with probability $0.4$ and
\[ \xi_1 = \begin{pmatrix} 28 & 32 & 300 & 300 \end{pmatrix}^T\]
with probability $0.6$.
Instantiation
First, we create the two instances $\xi_1$ and $\xi_2$ of the random variable. For simple models this is conveniently achieved through the Scenario
type. $\xi_1$ and $\xi_2$ can be created as follows:
ξ₁ = @scenario q₁ = 24.0 q₂ = 28.0 d₁ = 500.0 d₂ = 100.0 probability = 0.4
Scenario with probability 0.4
q₁: 24.0
q₂: 28.0
d₁: 500.0
d₂: 100.0
and
ξ₂ = @scenario q₁ = 28.0 q₂ = 32.0 d₁ = 300.0 d₂ = 300.0 probability = 0.6
Scenario with probability 0.6
q₁: 28.0
q₂: 32.0
d₁: 300.0
d₂: 300.0
where the variable names should match those given in the @uncertain
annotation. We are now ready to instantiate the stochastic program introduced above.
sp = instantiate(simple_model, [ξ₁, ξ₂], optimizer = GLPK.Optimizer)
Stochastic program with:
* 2 decision variables
* 2 recourse variables
* 2 scenarios of type Scenario
Structure: Deterministic equivalent
Solver name: GLPK
By default, the stochastic program is instantiated with a deterministic equivalent structure. It is straightforward to work out the extended form because the example problem is small:
\[\begin{aligned} \operatorname*{minimize}_{x_1, x_2, y_{11}, y_{21}, y_{12}, y_{22} \in \mathbb{R}} & \quad 100x_1 + 150x_2 - 9.6y_{11} - 11.2y_{21} - 16.8y_{12} - 19.2y_{22} \\ \text{s.t.} & \quad x_1 + x_2 \leq 120 \\ & \quad 6 y_{11} + 10 y_{21} \leq 60 x_1 \\ & \quad 8 y_{11} + 5 y_{21} \leq 80 x_2 \\ & \quad 6 y_{12} + 10 y_{22} \leq 60 x_1 \\ & \quad 8 y_{12} + 5 y_{22} \leq 80 x_2 \\ & \quad x_1 \geq 40 \\ & \quad x_2 \geq 20 \\ & \quad 0 \leq y_{11} \leq 500 \\ & \quad 0 \leq y_{21} \leq 100 \\ & \quad 0 \leq y_{12} \leq 300 \\ & \quad 0 \leq y_{22} \leq 300 \end{aligned}\]
We can print the stochastic program and confirm that it indeed models the example recourse problem given above:
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: GLPK
Optimization
The most common operation is to solve the instantiated stochastic program for an optimal first-stage decision. We instantiated the problem with the GLPK
optimizer, so we can solve the problem directly:
optimize!(sp)
We can then query the resulting optimal value:
objective_value(sp)
-855.8333333333321
and the optimal first-stage decision:
optimal_decision(sp)
2-element Vector{Float64}:
46.66666666666667
36.25
Alternatively, we can solve the problem with a structure-exploiting solver. The framework provides both LShaped
and ProgressiveHedging
solvers. We first re-instantiate the problem using an L-shaped optimizer:
sp_lshaped = instantiate(simple_model, [ξ₁, ξ₂], optimizer = LShaped.Optimizer)
Stochastic program with:
* 2 decision variables
* 2 recourse variables
* 2 scenarios of type Scenario
Structure: Stage-decomposition
Solver name: L-shaped with disaggregate cuts
It should be noted that the memory representation of the stochastic program is now different. Because we instantiated the model with an L-shaped optimizer it generated the program according to a stage-decomposition structure:
print(sp_lshaped)
First-stage
==============
Min 100 x₁ + 150 x₂
Subject to
x₁ ∈ Decisions
x₂ ∈ Decisions
x₁ ≥ 40.0
x₂ ≥ 20.0
x₁ + x₂ ≤ 120.0
Second-stage
==============
Subproblem 1 (p = 0.40):
Max 24 y₁ + 28 y₂
Subject to
x₁ ∈ Known(value = 40.0)
x₂ ∈ Known(value = 20.0)
y₁ ∈ RecourseDecisions
y₂ ∈ RecourseDecisions
y₁ ≥ 0.0
y₂ ≥ 0.0
y₁ ≤ 500.0
y₂ ≤ 100.0
-60 x₁ + 6 y₁ + 10 y₂ ≤ 0.0
-80 x₂ + 8 y₁ + 5 y₂ ≤ 0.0
Subproblem 2 (p = 0.60):
Max 28 y₁ + 32 y₂
Subject to
x₁ ∈ Known(value = 40.0)
x₂ ∈ Known(value = 20.0)
y₁ ∈ RecourseDecisions
y₂ ∈ RecourseDecisions
y₁ ≥ 0.0
y₂ ≥ 0.0
y₁ ≤ 300.0
y₂ ≤ 300.0
-60 x₁ + 6 y₁ + 10 y₂ ≤ 0.0
-80 x₂ + 8 y₁ + 5 y₂ ≤ 0.0
Solver name: L-shaped with disaggregate cuts
To solve the problem with L-shaped, we must first specify internal optimizers that can solve emerging subproblems:
set_optimizer_attribute(sp_lshaped, MasterOptimizer(), GLPK.Optimizer)
set_optimizer_attribute(sp_lshaped, SubProblemOptimizer(), GLPK.Optimizer)
We can now run the optimization procedure:
optimize!(sp_lshaped)
L-Shaped Gap Time: 0:00:01 (6 iterations)
Objective: -855.8333333333339
Gap: 0.0
Number of cuts: 7
Iterations: 6
and verify that we get the same results:
objective_value(sp_lshaped)
-855.8333333333339
and
optimal_decision(sp_lshaped)
2-element Array{Float64,1}:
46.66666666666673
36.25000000000003
Likewise, we can solve the problem with progressive-hedging. Consider:
sp_progressivehedging = instantiate(simple_model, [ξ₁, ξ₂], optimizer = ProgressiveHedging.Optimizer)
Stochastic program with:
* 2 decision variables
* 2 recourse variables
* 2 scenarios of type Scenario
Structure: Scenario-decomposition
Solver name: Progressive-hedging with fixed penalty
Now, the induced structure is the scenario-decomposition that decomposes the stochastic program completely into subproblems over the scenarios. Consider the printout:
print(sp_progressivehedging)
Scenario problems
==============
Subproblem 1 (p = 0.40):
Min 100 x₁ + 150 x₂ - 24 y₁ - 28 y₂
Subject to
y₁ ≥ 0.0
y₂ ≥ 0.0
y₁ ≤ 500.0
y₂ ≤ 100.0
x₁ ∈ Decisions
x₂ ∈ Decisions
x₁ ≥ 40.0
x₂ ≥ 20.0
x₁ + x₂ ≤ 120.0
-60 x₁ + 6 y₁ + 10 y₂ ≤ 0.0
-80 x₂ + 8 y₁ + 5 y₂ ≤ 0.0
Subproblem 2 (p = 0.60):
Min 100 x₁ + 150 x₂ - 28 y₁ - 32 y₂
Subject to
y₁ ≥ 0.0
y₂ ≥ 0.0
y₁ ≤ 300.0
y₂ ≤ 300.0
x₁ ∈ Decisions
x₂ ∈ Decisions
x₁ ≥ 40.0
x₂ ≥ 20.0
x₁ + x₂ ≤ 120.0
-60 x₁ + 6 y₁ + 10 y₂ ≤ 0.0
-80 x₂ + 8 y₁ + 5 y₂ ≤ 0.0
Solver name: Progressive-hedging with fixed penalty
To solve the problem with progressive-hedging, we must also specify an internal optimizers that can solve the subproblems:
set_optimizer_attribute(sp_progressivehedging, SubProblemOptimizer(), Ipopt.Optimizer)
set_suboptimizer_attribute(sp_progressivehedging, MOI.RawParameter("print_level"), 0) # Silence Ipopt
We can now run the optimization procedure:
optimize!(sp_progressivehedging)
Progressive Hedging Time: 0:00:07 (303 iterations)
Objective: -855.5842547490254
Primal gap: 7.2622997706326046e-6
Dual gap: 8.749063651111478e-6
Iterations: 302
and verify that we get the same results:
objective_value(sp_progressivehedging)
-855.5842547490254
and
optimal_decision(sp_progressivehedging)
2-element Array{Float64,1}:
46.65459574079722
36.24298005619633
Decision evaluation
Decision evaluation is an important concept in stochastic programming. The expected result of taking a given first-stage decision $x$ is given by
\[V(x) = c^T x + \operatorname{\mathbb{E}}_{\omega} \left[Q(x,\xi(\omega))\right]\]
If the sample space is finite, the above expressions has a closed form that is readily calculated. Consider the following first-stage decision:
x = [40., 20.]
2-element Vector{Float64}:
40.0
20.0
The expected result of taking this decision in the simple finite model can be determined through:
evaluate_decision(sp, x)
-470.39999999999964
Internally, this fixes all occurances of the first-stage variables in the deterministic equivalent and solves the resulting problem. An equivalent approach is to fix the decisions manually:
another_sp = instantiate(simple_model, [ξ₁, ξ₂], optimizer = GLPK.Optimizer)
fix.(all_decision_variables(another_sp, 1), x)
optimize!(another_sp)
objective_value(another_sp)
-470.39999999999964
Decision evaluation is supported by the other storage structures as well:
evaluate_decision(sp_lshaped, x)
-470.39999999999964
and
evaluate_decision(sp_progressivehedging, x)
-470.40000522896185
In a stage-decomposition structure, the occurances of first-stage decisions in the second-stage subproblems are treated as known decisions with parameter values that can be set. We can explicitly create such a subproblem to clearly see this in action:
print(outcome_model(sp, x, ξ₁))
Min -24 y₁ - 28 y₂ + 100 x₁ + 150 x₂
Subject to
x₁ ∈ Known(value = 40.0)
x₂ ∈ Known(value = 20.0)
y₁ ∈ RecourseDecisions
y₂ ∈ RecourseDecisions
y₁ ≥ 0.0
y₂ ≥ 0.0
y₁ ≤ 500.0
y₂ ≤ 100.0
-60 x₁ + 6 y₁ + 10 y₂ ≤ 0.0
-80 x₂ + 8 y₁ + 5 y₂ ≤ 0.0
Moreover, we can evaluate the result of the decision in a given scenario, i.e. solving a single outcome model, through:
evaluate_decision(sp, x, ξ₁)
900.0
Solution caching
StochasticPrograms minimizes memory usage by reusing the same underlying structure when optimizing the model as when evaluating a decision. As a consequence, calls to for example evaluate_decision
replaces any previosly found optimal solution. Hence,
objective_value(sp)
-470.39999999999964
and
optimal_decision(sp)
2-element Vector{Float64}:
40.0
20.0
returns values consistent with above call to evaluate_decision
. Optionally, the solution obtained from calling optimize!
can be cached by setting a cache
flag during the call:
optimize!(sp; cache = true)
or by directly calling cache_solution!
:
cache_solution!(sp)
after optimizing. Now, we again obtain
objective_value(sp)
-855.8333333333321
and
optimal_decision(sp)
2-element Vector{Float64}:
46.66666666666667
36.25
However, if we now also re-run decision evaluation:
evaluate_decision(sp, x)
-470.39999999999964
the solution stays consistent with the optimization call:
objective_value(sp)
-855.8333333333321
and
optimal_decision(sp)
2-element Vector{Float64}:
46.66666666666667
36.25
The caching procedure attempts to save as many MathOptInterface attributes as possible, so for example dual values of the constraints should stay consistent with the optimized model as well. For larger models, the caching procedure can be time consuming and is therefore not enabled by default. For the same reason, the subproblem solutions are only cached for models with fewer than 100 scenarios. The first-stage solution is always cached if caching is enabled.
Stochastic performance
Apart from solving the stochastic program, we can compute two classical measures of stochastic performance. The first measures the value of knowing the random outcome before making the decision. This is achieved by taking the expectation in the original model outside the minimization, to obtain the wait-and-see problem:
\[\mathrm{EWS} = \operatorname{\mathbb{E}}_{\omega}\left[ \begin{aligned} \min_{x \in \mathbb{R}^n} & \quad c^T x + Q(x,\xi(\omega)) \\ \text{s.t.} & \quad Ax = b \\ & \quad x \geq 0. \end{aligned}\right]\]
Now, the first- and second-stage decisions are taken with knowledge about the uncertainty. If we assume that we know what the actual outcome will be, we would be interested in the optimal course of action in that scenario. This is the concept of wait-and-see models. For example if $ξ₁$ is believed to be the actual outcome, we can define a wait-and-see model as follows:
ws = WS(sp, ξ₁)
print(ws)
Min 100 x₁ + 150 x₂ - 24 y₁ - 28 y₂
Subject to
x₁ ∈ Decisions
x₂ ∈ Decisions
y₁ ∈ RecourseDecisions
y₂ ∈ RecourseDecisions
x₁ ≥ 40.0
x₂ ≥ 20.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
y₁ ≤ 500.0
y₂ ≤ 100.0
The optimal first-stage decision in this scenario can be determined through:
x₁ = wait_and_see_decision(sp, ξ₁)
2-element Vector{Float64}:
40.0
29.583333333333336
We can evaluate this decision:
evaluate_decision(sp, x₁)
-762.5000000000014
The outcome is of course worse than taking the optimal decision. However, it would perform better if $ξ₁$ is the actual outcome:
evaluate_decision(sp, x₁, ξ₁)
37.49999999999909
as compared to:
evaluate_decision(sp, optimal_decision(sp), ξ₁)
104.16666666666788
The difference between the expected wait-and-see value and the value of the recourse problem is known as the expected value of perfect information:
\[\mathrm{EVPI} = \mathrm{EWS} - \mathrm{VRP}.\]
The EVPI measures the expected loss of not knowing the exact outcome beforehand. It quantifies the value of having access to an accurate forecast. We calculate it in the framework through:
EVPI(sp)
662.9166666666679
EVPI is supported in the other structures as well:
EVPI(sp_lshaped)
662.9166666666661
and
EVPI(sp_progressivehedging)
663.165763660815
We can also compute EWS directly using EWS
. Note, that the scenario-decomposition structure is ideal for solving wait-and-see type problems.
If the expectation in the original model is instead taken inside the second-stage objective function $Q$, we obtain the expected-value-problem:
\[\begin{aligned} \operatorname*{minimize}_{x \in \mathbb{R}^n} & \quad c^T x + Q(x,\operatorname{\mathbb{E}}_{\omega}[\xi(\omega)]) \\ \text{s.t.} & \quad Ax = b \\ & \quad x \geq 0. \end{aligned}\]
The solution to the expected-value-problem is known as the expected value decision, and is denoted by $\bar{x}$. We can compute it through
x̄ = expected_value_decision(sp)
2-element Vector{Float64}:
71.45833333333334
48.54166666666667
The expected result of taking the expected value decision is known as the expected result of the expected value decision:
\[\mathrm{EEV} = c^T \bar{x} + \operatorname{\mathbb{E}}_{\xi}{Q(\bar{x},\xi(\omega))}.\]
The difference between the value of the recourse problem and the expected result of the expected value decision is known as the value of the stochastic solution:
\[\mathrm{VSS} = \mathrm{EEV} - \mathrm{VRP}.\]
The VSS measures the expected loss of ignoring the uncertainty in the problem. A large VSS indicates that the second stage is sensitive to the stochastic data. We calculate it using
VSS(sp)
286.91666666666515
VSS is supported in the other structures as well:
VSS(sp_lshaped)
286.91666666666606
and
VSS(sp_progressivehedging)
286.6675823650668
We can also compute EEV directly using EEV
. Note, that the stage-decomposition structure is ideal for solving VSS type problems.
Infinite sample space
In the above, the probability space consists of only two scenarios and the stochastic program can hence be represented in a closed form. If it instead holds that $\xi$ follows say a normal distribution, then it is no longer possible to represent the full stochastic program since this would require infinite scenarios. We then revert to sampling-based techniques. For example, let $\xi \sim \mathcal{N}(\mu, \Sigma)$ with
\[\mu = \begin{pmatrix} 24 \\ 32 \\ 400 \\ 200 \end{pmatrix}, \quad \Sigma = \begin{pmatrix} 2 & 0.5 & 0 & 0 \\ 0.5 & 1 & 0 & 0 \\ 0 & 0 & 50 & 20 \\ 0 & 0 & 20 & 30 \end{pmatrix}\]
Instantiation
To approximate the resulting stochastic program in StochasticPrograms, we first create a sampler object capable of generating scenarios from this distribution. This is most conveniently achieved using the @sampler
macro:
using Distributions
@sampler SimpleSampler = begin
N::MvNormal
SimpleSampler(μ, Σ) = new(MvNormal(μ, Σ))
@sample Scenario begin
x = rand(sampler.N)
return Scenario(q₁ = x[1], q₂ = x[2], d₁ = x[3], d₂ = x[4])
end
end
μ = [24, 32, 400, 200]
Σ = [2 0.5 0 0
0.5 1 0 0
0 0 50 20
0 0 20 30]
sampler = SimpleSampler(μ, Σ)
Scenario sampler
Now, we can use the same stochastic model created before and the created sampler object to generate a sampled approximation of the stochastic program. For now, we create a small sampled model of just 5 scenarios:
sampled_sp = instantiate(simple_model, sampler, 5, optimizer = GLPK.Optimizer)
Stochastic program with:
* 2 decision variables
* 2 recourse variables
* 5 scenarios of type Scenario
Structure: Deterministic equivalent
Solver name: GLPK
An optimal solution to this sampled model approximates the optimal solution to the infinite model in the sense that the empirical average second-stage cost converges pointwise with probability one to the true optimal value as the number of sampled scenarios goes to infinity. Moreoever, we can apply a central limit theorem to calculate confidence intervals around the objective value, as well as around the EVPI and VSS. This is the basis for the technique known as sample average approximation. In the following, we show how we can achieve approximations of the finite sample space functionality. Note that most operations are now performed directly on the simple_model
object together with a supplied sampler object.
Optimization
To approximately solve the stochastic program over normally distributed scenarios, we must first set a sample-based solver. The framework provides the SAA
solver:
set_optimizer(simple_model, SAA.Optimizer)
We must first set an instance optimizer that can solve emerging sampled instances:
set_optimizer_attribute(simple_model, InstanceOptimizer(), GLPK.Optimizer)
Note, that we can use a structure-exploiting solver for the instance optimizer. We now set a desired confidence level and the number of samples:
set_optimizer_attribute(simple_model, Confidence(), 0.9)
set_optimizer_attribute(simple_model, NumSamples(), 100)
set_optimizer_attribute(simple_model, NumEvalSamples(), 300)
We can now calculate a confidence interval around the optimal value through:
confidence_interval(simple_model, sampler)
Confidence interval (p = 90%): [-1088.41 − -1065.72]
The optimization procedure provided by SAA
iteratively calculates confidence intervals for growing sample sizes until a desired relative tolerance is reached:
set_optimizer_attribute(simple_model, RelativeTolerance(), 5e-2)
We can now optimize the model:
optimize!(simple_model, sampler)
SAA gap Time: 0:00:03 (4 iterations)
Confidence interval: Confidence interval (p = 95%): [-1095.65 − -1072.36]
Relative error: 0.021487453807842415
Sample size: 64
and query the result:
objective_value(simple_model);objective_value(simple_model);
objective_value(simple_model) = Confidence interval (p = 95%): [-1095.65 − -1072.36]
Note, that we can just replace the sampler object to use another model of the uncertainty.
Decision evaluation
If the sample space is infinite, or if the underlying random variable $\xi$ is continuous, a first-stage decision also can only be evaluated in a stochastic sense. For example, note the result of evaluating the decision on the sampled model created above:
evaluate_decision(sampled_sp, x)
-1045.6472509382693
and compare it to the result of evaluating it on another sampled model of similar size:
another_sp = instantiate(simple_model, sampler, 5, optimizer = GLPK.Optimizer)
evaluate_decision(another_sp, x)
-1008.0143554432134
which, if any, of these values should be a candidate for the true value of $V(x)$? A more precise result is obtained by evaluating the decision using a sampled-based approach:
evaluate_decision(simple_model, x, sampler)
Confidence interval (p = 90%): [-1101.83 − -1063.44]
Stochastic performance
Using the same techniques as above, we can calculate confidence intervals around the EVPI and VSS:
EVPI(simple_model, sampler)
Confidence interval (p = 99%): [32.96 − 144.51]
and
VSS(simple_model, sampler)
Warning: VSS is not statistically significant to the chosen confidence level and tolerance
Confidence interval (p = 95%): [-0.05 − 0.05]
Note, that the VSS is not statistically significant. This is not surprising for a normally distributed uncertainty model. The expected value decision is expected to perform well.