Skip to content

Commit 0ee290b

Browse files
committed
Add unit tests for rolling horizon
Add tests of the various parts of rolling horizon. Part of #1365
1 parent aef2825 commit 0ee290b

File tree

1 file changed

+190
-0
lines changed

1 file changed

+190
-0
lines changed

test/test-rolling-horizon.jl

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,193 @@
2424
p in values(energy_problem.profiles.rep_period)
2525
)
2626
end
27+
28+
@testitem "Verify tables created by rolling horizon" setup = [CommonSetup] tags =
29+
[:rolling_horizon, :unit] begin
30+
connection = DBInterface.connect(DuckDB.DB)
31+
_read_csv_folder(connection, joinpath(INPUT_FOLDER, "Rolling Horizon"))
32+
33+
# Not hardcoding these as they might change when the input changes
34+
move_forward = 24
35+
opt_window_length = move_forward * 2
36+
horizon_length = TEM.get_single_element_from_query_and_ensure_its_only_one(
37+
DuckDB.query(connection, "SELECT max(timestep) FROM profiles_rep_periods"),
38+
)
39+
energy_problem = run_rolling_horizon(connection, move_forward, opt_window_length)
40+
41+
# Table rolling_horizon_window
42+
@test "rolling_horizon_window" in
43+
[row.table_name for row in DuckDB.query(connection, "FROM duckdb_tables()")]
44+
45+
number_of_windows = ceil(Int, horizon_length / move_forward)
46+
df_rolling_horizon_window = DataFrame(DuckDB.query(connection, "FROM rolling_horizon_window"))
47+
@test maximum(df_rolling_horizon_window.id) == number_of_windows
48+
@test sum(df_rolling_horizon_window.move_forward) == horizon_length
49+
end
50+
51+
@testitem "If the optimisation window is very large, the first rolling solution is the same as no-horizon" setup =
52+
[CommonSetup] tags = [:rolling_horizon, :unit] begin
53+
connection = DBInterface.connect(DuckDB.DB)
54+
_read_csv_folder(connection, joinpath(INPUT_FOLDER, "Rolling Horizon"))
55+
56+
horizon_length = TEM.get_single_element_from_query_and_ensure_its_only_one(
57+
DuckDB.query(connection, "SELECT max(timestep) FROM profiles_rep_periods"),
58+
)
59+
opt_window_length = horizon_length
60+
energy_problem = TulipaEnergyModel.run_scenario(connection; show_log = false)
61+
expected_objective = energy_problem.objective_value
62+
63+
for move_forward in [div(horizon_length, k) for k in (2, 3, 4, 6, 12, 24)]
64+
energy_problem =
65+
run_rolling_horizon(connection, move_forward, opt_window_length; show_log = false)
66+
df_rolling_horizon_window =
67+
DataFrame(DuckDB.query(connection, "FROM rolling_horizon_window"))
68+
@test df_rolling_horizon_window.objective_value[1] == expected_objective # The first solution should be the full problem
69+
end
70+
end
71+
72+
@testitem "Correctness of rolling_solution_var_flow" setup = [CommonSetup] tags =
73+
[:rolling_horizon, :unit] begin
74+
connection = DBInterface.connect(DuckDB.DB)
75+
_read_csv_folder(connection, joinpath(INPUT_FOLDER, "Rolling Horizon"))
76+
77+
horizon_length = TEM.get_single_element_from_query_and_ensure_its_only_one(
78+
DuckDB.query(connection, "SELECT max(timestep) FROM profiles_rep_periods"),
79+
)
80+
move_forward = 24
81+
opt_window_length = horizon_length
82+
energy_problem = run_rolling_horizon(
83+
connection,
84+
move_forward,
85+
opt_window_length;
86+
save_rolling_solution = true,
87+
)
88+
number_of_windows = ceil(Int, horizon_length / move_forward)
89+
90+
@test "rolling_solution_var_flow" in
91+
[row.table_name for row in DuckDB.query(connection, "FROM duckdb_tables()")]
92+
93+
df_rolsol_var_flow = DataFrame(DuckDB.query(connection, "FROM rolling_solution_var_flow"))
94+
# All window_ids are there
95+
@test sort(unique(df_rolsol_var_flow.window_id)) == 1:number_of_windows
96+
# All variable ids are there
97+
number_of_flows = TEM.get_num_rows(connection, "flow")
98+
expected_number_of_var_flow = move_forward * number_of_flows * number_of_windows # 1 year, 1 rp
99+
@test sort(unique(df_rolsol_var_flow.var_id)) == 1:expected_number_of_var_flow
100+
end
101+
102+
@testitem "Test infeasible rolling horizon nice end" setup = [CommonSetup] tags =
103+
[:rolling_horizon, :unit] begin
104+
connection = DBInterface.connect(DuckDB.DB)
105+
_read_csv_folder(connection, joinpath(INPUT_FOLDER, "Rolling Horizon"))
106+
107+
DuckDB.execute( # Make it infeasible
108+
connection,
109+
"UPDATE asset_milestone SET peak_demand = 1500",
110+
)
111+
@test_logs (:warn, "Model status different from optimal") match_mode = :any TEM.run_rolling_horizon(
112+
connection,
113+
24,
114+
48,
115+
show_log = false,
116+
)
117+
energy_problem = TEM.run_rolling_horizon(connection, 24, 48; show_log = false)
118+
@test energy_problem.termination_status == JuMP.INFEASIBLE
119+
end
120+
121+
# Test validation of time resolution (uniform and resolutions are divisors of move_forward)
122+
@testitem "Test that opt_window_length must be divisible by all time resolutions and that they are uniform" setup =
123+
[CommonSetup] tags = [:rolling_horizon, :unit] begin
124+
connection = DBInterface.connect(DuckDB.DB)
125+
_read_csv_folder(connection, joinpath(INPUT_FOLDER, "Rolling Horizon"))
126+
127+
# First, it should allow more complex resolutions
128+
DuckDB.query(
129+
connection,
130+
"CREATE OR REPLACE TABLE assets_rep_periods_partitions (
131+
asset TEXT,
132+
specification TEXT,
133+
partition TEXT,
134+
year INT,
135+
rep_period INT
136+
);
137+
INSERT INTO assets_rep_periods_partitions (asset, specification, partition, year, rep_period)
138+
VALUES
139+
('solar', 'uniform', '2', 2030, 1),
140+
('thermal', 'uniform', '3', 2030, 1),
141+
('battery', 'uniform', '4', 2030, 1),
142+
('demand', 'uniform', '6', 2030, 1),
143+
",
144+
)
145+
energy_problem = TEM.run_rolling_horizon(connection, 24, 48; show_log = false)
146+
147+
# It should fail when opt_window_length is not a multiple of any of these
148+
DuckDB.query(
149+
connection,
150+
"UPDATE assets_rep_periods_partitions SET partition='5' WHERE asset='battery'
151+
",
152+
)
153+
@test_throws AssertionError TEM.run_rolling_horizon(connection, 24, 48; show_log = false)
154+
155+
# Working again with different move_forward and opt_window_length that are divisible by 5
156+
TEM.run_rolling_horizon(connection, 12 * 5, 24 * 5; show_log = false)
157+
158+
# It should fail when partition is not uniform
159+
DuckDB.query(
160+
connection,
161+
"UPDATE assets_rep_periods_partitions SET partition='4', specification='math' WHERE asset='battery'
162+
",
163+
)
164+
@test_throws AssertionError TEM.run_rolling_horizon(connection, 24, 48; show_log = false)
165+
end
166+
167+
# Test optionality of the full rolling_solution_var_* tables
168+
@testitem "Test option save_rolling_solution" setup = [CommonSetup] tags = [:rolling_horizon, :unit] begin
169+
connection = DBInterface.connect(DuckDB.DB)
170+
_read_csv_folder(connection, joinpath(INPUT_FOLDER, "Rolling Horizon"))
171+
172+
TEM.run_rolling_horizon(connection, 24, 48; show_log = false, save_rolling_solution = false)
173+
tables = [row.table_name for row in DuckDB.query(connection, "FROM duckdb_tables")]
174+
@test !("rolling_solution_var_flow" in tables)
175+
TEM.run_rolling_horizon(connection, 24, 48; show_log = false, save_rolling_solution = true)
176+
tables = [row.table_name for row in DuckDB.query(connection, "FROM duckdb_tables")]
177+
@test "rolling_solution_var_flow" in tables
178+
end
179+
180+
@testitem "Test internal rolling_horizon_energy_problem" setup = [CommonSetup] tags =
181+
[:rolling_horizon, :unit] begin
182+
connection = DBInterface.connect(DuckDB.DB)
183+
_read_csv_folder(connection, joinpath(INPUT_FOLDER, "Rolling Horizon"))
184+
185+
# There is no internal rolling_horizon_energy_problem when we're not running rolling_horizon
186+
energy_problem = TEM.run_scenario(connection; show_log = false)
187+
@test isnothing(energy_problem.rolling_horizon_energy_problem)
188+
@test !isnan(energy_problem.objective_value)
189+
190+
# Now there will be one
191+
energy_problem = TEM.run_rolling_horizon(connection, 24, 48; show_log = false)
192+
@test energy_problem.rolling_horizon_energy_problem isa EnergyProblem
193+
@test isnan(energy_problem.objective_value)
194+
@test !isnan(energy_problem.rolling_horizon_energy_problem.objective_value)
195+
end
196+
197+
@testitem "Test exporting output of rolling horizon to CSV works" setup = [CommonSetup] tags =
198+
[:integration, :io, :fast] begin
199+
connection = DBInterface.connect(DuckDB.DB)
200+
_read_csv_folder(connection, joinpath(INPUT_FOLDER, "Rolling Horizon"))
201+
TulipaEnergyModel.run_rolling_horizon(
202+
connection,
203+
24,
204+
48;
205+
output_folder = joinpath(OUTPUT_FOLDER),
206+
show_log = false,
207+
)
208+
for filename in (
209+
"var_flow.csv",
210+
"var_flows_investment.csv",
211+
"cons_balance_consumer.csv",
212+
"cons_capacity_incoming_simple_method.csv",
213+
)
214+
@test isfile(joinpath(OUTPUT_FOLDER, filename))
215+
end
216+
end

0 commit comments

Comments
 (0)