Skip to content

Commit 6bd675b

Browse files
committed
Add unit tests for rolling horizon
Add tests of the various parts of rolling horizon. Part of #1365
1 parent 1773088 commit 6bd675b

File tree

1 file changed

+191
-0
lines changed

1 file changed

+191
-0
lines changed

test/test-rolling-horizon.jl

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

0 commit comments

Comments
 (0)