Alternative forward models
This tutorial was generated using Literate.jl. Download the source as a .jl
file. Download the source as a .ipynb
file.
This example demonstrates how to train convex and non-convex models.
This example uses the following packages:
using SDDP
import Ipopt
import PowerModels
import Test
Formulation
For our model, we build a simple optimal power flow model with a single hydro-electric generator.
The formulation of our optimal power flow problem depends on model_type
, which must be one of the PowerModels
formulations.
(To run locally, download pglib_opf_case5_pjm.m
and update filename
appropriately.)
function build_model(model_type)
filename = joinpath(@__DIR__, "pglib_opf_case5_pjm.m")
data = PowerModels.parse_file(filename)
return SDDP.PolicyGraph(
SDDP.UnicyclicGraph(0.95);
sense = :Min,
lower_bound = 0.0,
optimizer = Ipopt.Optimizer,
) do sp, t
power_model = PowerModels.instantiate_model(
data,
model_type,
PowerModels.build_opf;
jump_model = sp,
)
# Now add hydro power models. Assume that generator 5 is hydro, and the
# rest are thermal.
pg = power_model.var[:it][:pm][:nw][0][:pg][5]
sp[:pg] = pg
@variable(sp, x >= 0, SDDP.State, initial_value = 10.0)
@variable(sp, deficit >= 0)
@constraint(sp, balance, x.out == x.in - pg + deficit)
@stageobjective(sp, objective_function(sp) + 1e6 * deficit)
SDDP.parameterize(sp, [0, 2, 5]) do ω
return SDDP.set_normalized_rhs(balance, ω)
end
return
end
end
build_model (generic function with 1 method)
Training a convex model
We can build and train a convex approximation of the optimal power flow problem.
The problem with the convex model is that it does not accurately simulate the true dynamics of the problem. Therefore, it under-estimates the true cost of operation.
convex = build_model(PowerModels.DCPPowerModel)
SDDP.train(convex; iteration_limit = 10)
-------------------------------------------------------------------
SDDP.jl (c) Oscar Dowson and contributors, 2017-24
-------------------------------------------------------------------
problem
nodes : 1
state variables : 1
scenarios : Inf
existing cuts : false
options
solver : serial mode
risk measure : SDDP.Expectation()
sampling scheme : SDDP.InSampleMonteCarlo
subproblem structure
VariableRef : [20, 20]
AffExpr in MOI.EqualTo{Float64} : [13, 13]
AffExpr in MOI.Interval{Float64} : [6, 6]
VariableRef in MOI.GreaterThan{Float64} : [14, 14]
VariableRef in MOI.LessThan{Float64} : [11, 11]
numerical stability report
matrix range [1e+00, 2e+02]
objective range [1e+00, 1e+06]
bounds range [4e-01, 6e+00]
rhs range [5e-01, 5e+00]
-------------------------------------------------------------------
iteration simulation bound time (s) solves pid
-------------------------------------------------------------------
1 3.495977e+04 4.986138e+04 4.571486e-02 11 1
4 7.206185e+06 1.965958e+05 2.877257e+00 520 1
6 6.304298e+05 3.272599e+05 4.193045e+00 714 1
8 1.157579e+06 4.135730e+05 5.637067e+00 940 1
9 1.063171e+06 4.167801e+05 7.289496e+00 1131 1
10 6.422997e+04 4.198681e+05 7.394857e+00 1146 1
-------------------------------------------------------------------
status : iteration_limit
total time (s) : 7.394857e+00
total solves : 1146
best bound : 4.198681e+05
simulation ci : 1.496667e+06 ± 1.443494e+06
numeric issues : 0
-------------------------------------------------------------------
To more accurately simulate the dynamics of the problem, a common approach is to write the cuts representing the policy to a file, and then read them into a non-convex model:
SDDP.write_cuts_to_file(convex, "convex.cuts.json")
non_convex = build_model(PowerModels.ACPPowerModel)
SDDP.read_cuts_from_file(non_convex, "convex.cuts.json")
Now we can simulate non_convex
to evaluate the policy.
result = SDDP.simulate(non_convex, 1)
1-element Vector{Vector{Dict{Symbol, Any}}}:
[Dict(:bellman_term => 397475.5640335222, :noise_term => 5, :node_index => 1, :stage_objective => 17608.09729050211, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 396855.7460908836, :noise_term => 5, :node_index => 1, :stage_objective => 17608.097290113546, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 401759.11365072837, :noise_term => 2, :node_index => 1, :stage_objective => 17608.097294491152, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 401139.2957044874, :noise_term => 5, :node_index => 1, :stage_objective => 17608.097293715917, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 400519.47775893623, :noise_term => 5, :node_index => 1, :stage_objective => 17608.097293025996, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 399899.65981400345, :noise_term => 5, :node_index => 1, :stage_objective => 17608.097292407772, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 399279.8418696282, :noise_term => 5, :node_index => 1, :stage_objective => 17608.09729185012, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 398660.0239257576, :noise_term => 5, :node_index => 1, :stage_objective => 17608.097291345523, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 403563.39148267615, :noise_term => 2, :node_index => 1, :stage_objective => 17608.097297417346, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 408466.75901395525, :noise_term => 2, :node_index => 1, :stage_objective => 17608.09732305676, :objective_state => nothing, :belief => Dict(1 => 1.0)) … Dict(:bellman_term => 410492.31883703655, :noise_term => 2, :node_index => 1, :stage_objective => 23580.697455192985, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 416480.1383869184, :noise_term => 0, :node_index => 1, :stage_objective => 27420.55349566041, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 410492.31883734843, :noise_term => 2, :node_index => 1, :stage_objective => 25694.765180759525, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 409872.50080969825, :noise_term => 5, :node_index => 1, :stage_objective => 17608.097368131526, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 410492.318836888, :noise_term => 2, :node_index => 1, :stage_objective => 22580.720348532643, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 409872.5008093154, :noise_term => 5, :node_index => 1, :stage_objective => 17608.097368131526, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 409252.68281774677, :noise_term => 5, :node_index => 1, :stage_objective => 17608.097339043215, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 408632.8648396366, :noise_term => 5, :node_index => 1, :stage_objective => 17608.097325584822, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 410492.31883747043, :noise_term => 0, :node_index => 1, :stage_objective => 26527.481284410424, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 410492.3188370366, :noise_term => 2, :node_index => 1, :stage_objective => 23580.697455442853, :objective_state => nothing, :belief => Dict(1 => 1.0))]
A problem with reading and writing the cuts to file is that the cuts have been generated from trial points of the convex model. Therefore, the policy may be arbitrarily bad at points visited by the non-convex model.
Training a non-convex model
We can also build and train a non-convex formulation of the optimal power flow problem.
The problem with the non-convex model is that because it is non-convex, SDDP.jl may find a sub-optimal policy. Therefore, it may over-estimate the true cost of operation.
non_convex = build_model(PowerModels.ACPPowerModel)
SDDP.train(non_convex; iteration_limit = 10)
result = SDDP.simulate(non_convex, 1)
1-element Vector{Vector{Dict{Symbol, Any}}}:
[Dict(:bellman_term => 382348.0354804149, :noise_term => 0, :node_index => 1, :stage_objective => 21433.375510483747, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 388846.90392441297, :noise_term => 0, :node_index => 1, :stage_objective => 21433.375510483755, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 390573.1327968642, :noise_term => 2, :node_index => 1, :stage_objective => 21433.37551048375, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 392299.36166931526, :noise_term => 2, :node_index => 1, :stage_objective => 21433.375510483776, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 395499.1128002878, :noise_term => 0, :node_index => 1, :stage_objective => 25541.572900364, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 390066.382314971, :noise_term => 5, :node_index => 1, :stage_objective => 21433.375510483766, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 391792.6111874222, :noise_term => 2, :node_index => 1, :stage_objective => 21433.375510483776, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 393518.8400598733, :noise_term => 2, :node_index => 1, :stage_objective => 21433.37551048379, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 395499.1128011092, :noise_term => 0, :node_index => 1, :stage_objective => 27064.684951379422, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 390066.38231578877, :noise_term => 5, :node_index => 1, :stage_objective => 21433.37551048377, :objective_state => nothing, :belief => Dict(1 => 1.0)) … Dict(:bellman_term => 395499.1127992089, :noise_term => 2, :node_index => 1, :stage_objective => 23580.69745577078, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 395499.1127992089, :noise_term => 2, :node_index => 1, :stage_objective => 23580.69745486107, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 395499.1127992089, :noise_term => 2, :node_index => 1, :stage_objective => 23580.697454861074, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 390066.38231389684, :noise_term => 5, :node_index => 1, :stage_objective => 21433.37551048377, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 384633.65182902757, :noise_term => 5, :node_index => 1, :stage_objective => 21433.375510483755, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 379200.92134415807, :noise_term => 5, :node_index => 1, :stage_objective => 21433.375510483755, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 373768.19085928873, :noise_term => 5, :node_index => 1, :stage_objective => 21433.37551048376, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 380267.0593032868, :noise_term => 0, :node_index => 1, :stage_objective => 21433.375510483747, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 381993.28817573807, :noise_term => 2, :node_index => 1, :stage_objective => 21433.375510483755, :objective_state => nothing, :belief => Dict(1 => 1.0)), Dict(:bellman_term => 388492.15661973617, :noise_term => 0, :node_index => 1, :stage_objective => 21433.375510483762, :objective_state => nothing, :belief => Dict(1 => 1.0))]
Combining convex and non-convex models
To summarize, training with the convex model constructs cuts at points that may never be visited by the non-convex model, and training with the non-convex model may construct arbitrarily poor cuts because a key assumption of SDDP is convexity.
As a compromise, we can train a policy using a combination of the convex and non-convex models; we'll use the non-convex model to generate trial points on the forward pass, and we'll use the convex model to build cuts on the backward pass.
convex = build_model(PowerModels.DCPPowerModel)
A policy graph with 1 nodes.
Node indices: 1
non_convex = build_model(PowerModels.ACPPowerModel)
A policy graph with 1 nodes.
Node indices: 1
To do so, we train convex
using the SDDP.AlternativeForwardPass
forward pass, which simulates the model using non_convex
, and we use SDDP.AlternativePostIterationCallback
as a post-iteration callback, which copies cuts from the convex
model back into the non_convex
model.
SDDP.train(
convex;
forward_pass = SDDP.AlternativeForwardPass(non_convex),
post_iteration_callback = SDDP.AlternativePostIterationCallback(non_convex),
iteration_limit = 10,
)
-------------------------------------------------------------------
SDDP.jl (c) Oscar Dowson and contributors, 2017-24
-------------------------------------------------------------------
problem
nodes : 1
state variables : 1
scenarios : Inf
existing cuts : false
options
solver : serial mode
risk measure : SDDP.Expectation()
sampling scheme : SDDP.InSampleMonteCarlo
subproblem structure
VariableRef : [20, 20]
AffExpr in MOI.EqualTo{Float64} : [13, 13]
AffExpr in MOI.Interval{Float64} : [6, 6]
VariableRef in MOI.GreaterThan{Float64} : [14, 14]
VariableRef in MOI.LessThan{Float64} : [11, 11]
numerical stability report
matrix range [1e+00, 2e+02]
objective range [1e+00, 1e+06]
bounds range [4e-01, 6e+00]
rhs range [5e-01, 5e+00]
-------------------------------------------------------------------
iteration simulation bound time (s) solves pid
-------------------------------------------------------------------
1 3.753992e+06 4.849381e+04 4.474690e-01 69 1
3 1.158351e+06 3.757529e+05 2.494482e+00 258 1
4 3.920319e+06 3.770033e+05 3.641930e+00 348 1
6 4.057757e+05 3.896854e+05 4.899188e+00 468 1
9 3.225505e+05 4.067978e+05 6.341502e+00 600 1
10 9.203335e+05 4.280274e+05 7.618310e+00 720 1
-------------------------------------------------------------------
status : iteration_limit
total time (s) : 7.618310e+00
total solves : 720
best bound : 4.280274e+05
simulation ci : 1.211539e+06 ± 8.794011e+05
numeric issues : 0
-------------------------------------------------------------------
In practice, if we were to simulate non_convex
now, we should obtain a better policy than either of the two previous approaches.