Skip to content

Commit 0477fde

Browse files
authored
Rolling horizon (#1327)
* Change rep_period profiles to handle rolling horizon Implement ProfileWithRollingHorizon and make profiles.rep_period use that instead of Vector{Float64}. Change _profile_aggregate to dispatch on Vector{Float64} and ProfileWithRollingHorizon. These should allow the rolling horizon variables to be aggregated the same way as the normal profiles. Part of #1365 * Implement high-level run_rolling_horizon function with placeholders Implement run_rolling_horizon with the main idea. Use placeholders to specify places where a more complicated function needs to be called. Update the EnergyProblem structure to hold an inner EnergyProblem for the rolling horizon model. Part of #1365 * Create rolling horizon parameters Add ParametricOptInterface. Implement adding the rolling horizon parameters in src/rolling-horizon/create.jl. Update create_model to handle the rolling horizon parameters and wrap the solver in POI.Optimizer if it's rolling horizon. Part of #1365 * Implement update of rolling horizon parameters Implement the update functions for rolling horizon and add them to the run_rolling_horizon function. Part of #1365 * Test adding rolling horizon parameters Part of #1365 * Add functions for validation and data preparation for rolling horizon Add validation of input in the rolling horizon function. Add function to prepare the input to get in the rolling horizon and to get out. Add function to save the solution in the relevant tables. Part of #1365 * Add unit tests for rolling horizon Add tests of the various parts of rolling horizon. Part of #1365 * Update the storage constraint to use the rolling initial storage level Update src/constraints/storage.jl to use the initial_storage_level stored in the rolling horizon. Add test for the rolling horizon objective values to control future changes. Part of #1365
1 parent be54bed commit 0477fde

File tree

15 files changed

+922
-24
lines changed

15 files changed

+922
-24
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
1313
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1414
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
1515
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
16+
ParametricOptInterface = "0ce4ce61-57bf-432b-a095-efac525d185e"
1617
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
1718
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1819
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
@@ -28,6 +29,7 @@ JSON = "0.21.4, 1"
2829
JuMP = "1"
2930
MathOptInterface = "1"
3031
OrderedCollections = "1"
32+
ParametricOptInterface = "0.12.1"
3133
SparseArrays = "1"
3234
Statistics = "1"
3335
TOML = "1"

src/TulipaEnergyModel.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ using TulipaIO: TulipaIO
1515
using HiGHS: HiGHS
1616
using JuMP: JuMP, @constraint, @expression, @objective, @variable
1717
using MathOptInterface: MathOptInterface
18+
using ParametricOptInterface: ParametricOptInterface as POI
1819

1920
## Others
2021
using LinearAlgebra: LinearAlgebra
@@ -25,10 +26,10 @@ using TimerOutputs: TimerOutput, @timeit
2526
const to = TimerOutput()
2627

2728
# Definitions and auxiliary files
28-
include("utils.jl")
2929
include("run-scenario.jl")
3030
include("model-parameters.jl")
3131
include("structures.jl")
32+
include("utils.jl")
3233

3334
# Data
3435
include("input-schemas.jl")
@@ -39,6 +40,9 @@ include("data-preparation.jl")
3940
# Data massage and model preparation
4041
include("model-preparation.jl")
4142

43+
# Rolling horizon
44+
include("rolling-horizon/rolling-horizon.jl")
45+
4246
# Model creation
4347
for folder_name in ["variables", "constraints", "expressions"]
4448
folder_path = joinpath(@__DIR__, folder_name)

src/constraints/storage.jl

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,27 @@ export add_storage_constraints!
55
66
Adds the storage asset constraints to the model.
77
"""
8-
function add_storage_constraints!(connection, model, variables, expressions, constraints, profiles)
8+
function add_storage_constraints!(
9+
connection,
10+
model,
11+
variables,
12+
expressions,
13+
constraints,
14+
profiles;
15+
rolling_horizon = false,
16+
)
917
var_storage_level_rep_period = variables[:storage_level_rep_period]
1018
var_storage_level_over_clustered_year = variables[:storage_level_over_clustered_year]
1119

20+
rolling_horizon_lookup = if rolling_horizon
21+
Dict{Int,Int}(
22+
row.var_storage_id::Int => row.id::Int for
23+
row in DuckDB.query(connection, "FROM param_initial_storage_level")
24+
)
25+
else
26+
Dict{Int,Int}()
27+
end
28+
1229
## REP-PERIOD CONSTRAINTS (within a representative period)
1330
# - Balance constraint (using the lowest temporal resolution)
1431
let table_name = :balance_storage_rep_period, cons = constraints[table_name]
@@ -27,7 +44,12 @@ function add_storage_constraints!(connection, model, variables, expressions, con
2744
sum,
2845
0.0,
2946
)
30-
initial_storage_level = row.initial_storage_level::Union{Float64,Missing}
47+
initial_storage_level = if rolling_horizon && row.time_block_start == 1
48+
rolling_horizon_id = rolling_horizon_lookup[row.id]
49+
variables[:param_initial_storage_level].container[rolling_horizon_id]
50+
else
51+
row.initial_storage_level::Union{Float64,Missing}
52+
end
3153
storage_charging_efficiency = row.storage_charging_efficiency::Float64
3254
storage_discharging_efficiency = row.storage_discharging_efficiency::Float64
3355

src/create-model.jl

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,19 @@ function create_model(
6969
model_file_name = "",
7070
enable_names = true,
7171
direct_model = false,
72+
rolling_horizon = false,
73+
rolling_horizon_window_length = 0,
7274
)
75+
if rolling_horizon
76+
@assert rolling_horizon_window_length > 0
77+
end
78+
7379
## Optimizer
74-
optimizer_with_attributes = JuMP.optimizer_with_attributes(optimizer, optimizer_parameters...)
80+
optimizer_with_attributes = if rolling_horizon
81+
JuMP.optimizer_with_attributes(() -> POI.Optimizer(optimizer()), optimizer_parameters...)
82+
else
83+
JuMP.optimizer_with_attributes(optimizer, optimizer_parameters...)
84+
end
7585

7686
## Model
7787
if direct_model
@@ -113,6 +123,17 @@ function create_model(
113123
constraints,
114124
)
115125

126+
## Rolling Horizon Parameters
127+
if rolling_horizon
128+
add_rolling_horizon_parameters!(
129+
connection,
130+
model,
131+
variables,
132+
profiles,
133+
rolling_horizon_window_length,
134+
)
135+
end
136+
116137
## Expressions
117138
expressions = Dict{Symbol,TulipaExpression}()
118139

@@ -165,7 +186,8 @@ function create_model(
165186
variables,
166187
expressions,
167188
constraints,
168-
profiles,
189+
profiles;
190+
rolling_horizon,
169191
)
170192

171193
@timeit to "add_hub_constraints!" add_hub_constraints!(model, constraints)

src/model-preparation.jl

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -572,19 +572,22 @@ function add_expressions_to_constraints!(connection, variables, constraints)
572572
end
573573

574574
function prepare_profiles_structure(connection)
575+
# Independent of being rolling horizon or not, these are complete
575576
rep_period = Dict(
576-
(row.profile_name, row.year, row.rep_period) => [
577-
row.value for row in DuckDB.query(
578-
connection,
579-
"SELECT profile.value
580-
FROM profiles_rep_periods AS profile
581-
WHERE
582-
profile.profile_name = '$(row.profile_name)'
583-
AND profile.year = $(row.year)
584-
AND profile.rep_period = $(row.rep_period)
585-
",
586-
)
587-
] for row in DuckDB.query(
577+
(row.profile_name, row.year, row.rep_period) => ProfileWithRollingHorizon(
578+
Float64[
579+
row.value for row in DuckDB.query(
580+
connection,
581+
"SELECT profile.value
582+
FROM profiles_rep_periods AS profile
583+
WHERE
584+
profile.profile_name = '$(row.profile_name)'
585+
AND profile.year = $(row.year)
586+
AND profile.rep_period = $(row.rep_period)
587+
",
588+
)
589+
],
590+
) for row in DuckDB.query(
588591
connection,
589592
"SELECT DISTINCT
590593
profiles.profile_name,
@@ -596,7 +599,7 @@ function prepare_profiles_structure(connection)
596599
)
597600

598601
over_clustered_year = Dict(
599-
(row.profile_name, row.year) => [
602+
(row.profile_name, row.year) => Float64[
600603
row.value for row in DuckDB.query(
601604
connection,
602605
"SELECT profile.value

src/rolling-horizon/create.jl

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""
2+
add_scalar_rolling_horizon_parameter!(model, variables, connection, param_table_name)
3+
4+
Create the rolling horizon Parameters for the table `param_table_name`.
5+
This assumes that the table named `param_table_name` has a column
6+
`original_value` and uses that as initial value for the variable.
7+
8+
The container is saved in the `model[key]` and the `TulipaVariable` for the
9+
parameter is stored in `variables[key]`, where `key = Symbol(param_table_name)`.
10+
"""
11+
function add_scalar_rolling_horizon_parameter!(model, variables, connection, param_table_name)
12+
initial_value =
13+
[row.original_value::Float64 for row in DuckDB.query(connection, "FROM $param_table_name")]
14+
num_rows = length(initial_value)
15+
param = TulipaVariable(connection, param_table_name)
16+
param.container = @variable(model, [1:num_rows] in JuMP.Parameter.(initial_value))
17+
18+
key = Symbol(param_table_name)
19+
model[key] = param.container
20+
variables[key] = param
21+
return param
22+
end
23+
24+
"""
25+
add_rolling_horizon_parameters!(connection, model, variables, profiles, window_length)
26+
27+
Create Parameters to handle rolling horizon.
28+
29+
The profile parameters are attached to `profiles.rep_period`.
30+
31+
The other parameters are the ones that have initial value (currently only initial_storage_level).
32+
These must be filtered from the corresponding indices table when time_block_start = 1.
33+
The corresponding parameters is saved in the variables and in the model.
34+
"""
35+
function add_rolling_horizon_parameters!(connection, model, variables, profiles, window_length)
36+
# Profiles
37+
for (_, profile_object) in profiles.rep_period
38+
profile_object.rolling_horizon_variables =
39+
@variable(model, [1:window_length] in JuMP.Parameter(0.0))
40+
end
41+
42+
# Scalar rolling horizon parameters
43+
# These need a table filtering where time_block_start = 1, and the current
44+
# strategy is to create a table `param_NAME` and call
45+
# add_scalar_rolling_horizon_parameter!.
46+
47+
## initial_storage_level
48+
DuckDB.query(
49+
connection,
50+
"""
51+
DROP SEQUENCE IF EXISTS id;
52+
CREATE SEQUENCE id START 1;
53+
CREATE OR REPLACE TABLE param_initial_storage_level AS
54+
SELECT
55+
nextval('id') as id,
56+
var.asset,
57+
var.year,
58+
var.rep_period,
59+
var.id as var_storage_id,
60+
asset_milestone.initial_storage_level as original_value
61+
FROM var_storage_level_rep_period as var
62+
LEFT JOIN asset_milestone
63+
ON var.asset = asset_milestone.asset
64+
AND var.year = asset_milestone.milestone_year
65+
WHERE time_block_start = 1;
66+
DROP SEQUENCE id;
67+
""",
68+
)
69+
add_scalar_rolling_horizon_parameter!(
70+
model,
71+
variables,
72+
connection,
73+
"param_initial_storage_level",
74+
)
75+
76+
return
77+
end

0 commit comments

Comments
 (0)