|
24 | 24 | p in values(energy_problem.profiles.rep_period) |
25 | 25 | ) |
26 | 26 | 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