|
| 1 | + |
| 2 | +""" |
| 3 | + validate_rolling_horizon_input(connection, move_forward, opt_window) |
| 4 | +
|
| 5 | +Validation of the rolling horizon input: |
| 6 | +- opt_window_length ≥ move_forward |
| 7 | +- Only one representative period per year |
| 8 | +- Only 'uniform' partitions are allowed |
| 9 | +- Only partitions that exactly divide opt_window_length are allowed |
| 10 | +""" |
| 11 | +function validate_rolling_horizon_input(connection, move_forward, opt_window_length) |
| 12 | + @assert opt_window_length >= move_forward |
| 13 | + for row in DuckDB.query( |
| 14 | + connection, |
| 15 | + "SELECT year, max(rep_period) as num_rep_periods |
| 16 | + FROM rep_periods_data |
| 17 | + GROUP BY year", |
| 18 | + ) |
| 19 | + @assert row.num_rep_periods == 1 |
| 20 | + end |
| 21 | + partition_tables = [ |
| 22 | + row.table_name for row in |
| 23 | + DuckDB.query(connection, "FROM duckdb_tables() WHERE table_name LIKE '%_partitions'") |
| 24 | + ] |
| 25 | + |
| 26 | + for table_name in partition_tables |
| 27 | + for row in DuckDB.query(connection, "FROM $table_name") |
| 28 | + @assert row.specification == "uniform" "Only 'uniform' specification is accepted" |
| 29 | + partition = tryparse(Int, row.partition) |
| 30 | + @assert !isnothing(partition) "Invalid partition" |
| 31 | + @assert opt_window_length % partition == 0 |
| 32 | + end |
| 33 | + end |
| 34 | + |
| 35 | + return |
| 36 | +end |
| 37 | + |
| 38 | +""" |
| 39 | + prepare_rolling_horizon_tables!(connection, variable_tables, save_rolling_solution, opt_window_length) |
| 40 | +
|
| 41 | +Modify and create tables to prepare to start the rolling horizon execution. |
| 42 | +The changes are: |
| 43 | +
|
| 44 | +- Stored the original variable tables as `full_var_%` per variable table |
| 45 | +- Add `solution` column to each full variable table. |
| 46 | +- If `save_rolling_solution`, create the `rolling_solution_var_%` tables per variable table. |
| 47 | +- Backup `rep_periods_data` and `year_data` into `full_rep_periods_data` and `full_year_data`. |
| 48 | +- Modify `rep_periods_data` and `year_data` to use `opt_window_length` as `num_timesteps`/`length`, respectively. |
| 49 | +""" |
| 50 | +function prepare_rolling_horizon_tables!( |
| 51 | + connection, |
| 52 | + variable_tables, |
| 53 | + save_rolling_solution, |
| 54 | + opt_window_length, |
| 55 | +) |
| 56 | + # Preparing the table to save the rolling solution |
| 57 | + for table in variable_tables |
| 58 | + # Add a column solution to no-rolling variable tables |
| 59 | + DuckDB.execute(connection, "ALTER TABLE $table ADD COLUMN IF NOT EXISTS solution FLOAT8") |
| 60 | + |
| 61 | + if save_rolling_solution |
| 62 | + # Save solutions with a table linking the window to the id of the (no-rolling) variable |
| 63 | + DuckDB.execute( |
| 64 | + connection, |
| 65 | + """ |
| 66 | + CREATE OR REPLACE TABLE rolling_solution_$table ( |
| 67 | + window_id INTEGER, |
| 68 | + var_id INTEGER, |
| 69 | + solution FLOAT8, |
| 70 | + ); |
| 71 | + """, |
| 72 | + ) |
| 73 | + end |
| 74 | + end |
| 75 | + |
| 76 | + # Create backup tables for rep_periods_data, year_data, and asset_milestone |
| 77 | + # and full tables for the variables |
| 78 | + backup_tables = ["rep_periods_data", "year_data"] |
| 79 | + for table_name in [backup_tables; variable_tables] |
| 80 | + DuckDB.query( |
| 81 | + connection, |
| 82 | + "CREATE OR REPLACE TABLE full_$table_name AS |
| 83 | + SELECT *, NULL AS solution |
| 84 | + FROM $table_name", |
| 85 | + ) |
| 86 | + end |
| 87 | + |
| 88 | + # Modify tables that keep horizon information to limit the horizon to the rolling window |
| 89 | + DuckDB.query(connection, "UPDATE rep_periods_data SET num_timesteps = $opt_window_length") |
| 90 | + DuckDB.query(connection, "UPDATE year_data SET length = $opt_window_length") |
| 91 | + |
| 92 | + return |
| 93 | +end |
| 94 | + |
| 95 | +""" |
| 96 | + save_solution_into_tables!(energy_problem, variable_tables, window_id, move_forward, window_start, horizon_length, save_rolling_solution) |
| 97 | +
|
| 98 | +Save the current rolling horizon solution from the model into the connection. |
| 99 | +This involves: |
| 100 | +- Calling [`save_solution!`](@ref) to copy the internal solution from the JuMP model to the connection. |
| 101 | +- Copying the solution from the internal variables to the full variables for the `move_forward` sub-window. |
| 102 | +- If `save_rolling_solution`, save the complete rolling solution in the table `rolling_solution_var_%` per variable table. |
| 103 | +""" |
| 104 | +function save_solution_into_tables!( |
| 105 | + energy_problem, |
| 106 | + variable_tables, |
| 107 | + window_id, |
| 108 | + move_forward, |
| 109 | + window_start, |
| 110 | + horizon_length, |
| 111 | + save_rolling_solution, |
| 112 | +) |
| 113 | + @timeit to "Save internal rolling horizon solution to connection" save_solution!( |
| 114 | + energy_problem, |
| 115 | + compute_duals = false, |
| 116 | + ) |
| 117 | + # Save rolling solution of each variable |
| 118 | + for table_name in variable_tables |
| 119 | + # Guessing which columns should be used as key for matching with the larger no-rolling variables |
| 120 | + # TODO: Use the schema for this matching? |
| 121 | + key_columns = [ |
| 122 | + row.column_name for row in DuckDB.query( |
| 123 | + energy_problem.db_connection, |
| 124 | + """ |
| 125 | + FROM duckdb_columns |
| 126 | + WHERE table_name = '$table_name' |
| 127 | + AND column_name IN ('asset', 'from_asset', 'to_asset', |
| 128 | + 'milestone_year', 'commission_year', 'year', 'rep_period') |
| 129 | + """, |
| 130 | + ) |
| 131 | + ] |
| 132 | + |
| 133 | + # Construct the WHERE condition matching the keys |
| 134 | + where_condition = |
| 135 | + join(["full_$table_name.$key = $table_name.$key" for key in key_columns], " AND ") |
| 136 | + |
| 137 | + # Save solution to full table |
| 138 | + DuckDB.query( |
| 139 | + energy_problem.db_connection, |
| 140 | + """ |
| 141 | + UPDATE full_$table_name |
| 142 | + SET solution = $table_name.solution |
| 143 | + FROM $table_name WHERE $where_condition |
| 144 | + AND full_$table_name.time_block_start = ($(window_start - 1) + $table_name.time_block_start - 1) % $horizon_length + 1 |
| 145 | + AND $table_name.time_block_end <= $move_forward -- only save the move_forward window |
| 146 | + """, |
| 147 | + ) |
| 148 | + |
| 149 | + if save_rolling_solution |
| 150 | + # Store the solution in the corresponding rolling_solution_$table_name |
| 151 | + # This also uses the `where_condition`, but to join the no-rolling and rolling variable tables |
| 152 | + DuckDB.query( |
| 153 | + energy_problem.db_connection, |
| 154 | + """ |
| 155 | + WITH cte_var_solution AS ( |
| 156 | + SELECT |
| 157 | + $window_id as window_id, |
| 158 | + full_$table_name.id as var_id, -- the ids are from the main model |
| 159 | + $table_name.solution |
| 160 | + FROM $table_name |
| 161 | + LEFT JOIN full_$table_name |
| 162 | + ON $where_condition -- this condition should match |
| 163 | + AND full_$table_name.time_block_start = ($(window_start - 1) + $table_name.time_block_start - 1) % $horizon_length + 1 |
| 164 | + ) |
| 165 | + INSERT INTO rolling_solution_$table_name |
| 166 | + SELECT * |
| 167 | + FROM cte_var_solution |
| 168 | + """, |
| 169 | + ) |
| 170 | + end |
| 171 | + end |
| 172 | + |
| 173 | + return |
| 174 | +end |
| 175 | + |
| 176 | +""" |
| 177 | + prepare_tables_to_leave_rolling_horizon!(connection, variable_tables) |
| 178 | +
|
| 179 | +Undo some of the changes done by [`prepare_rolling_horizon_tables`] to go back to the original input data. |
| 180 | +This involves: |
| 181 | +- Revert `rep_periods_data` and `year_data` to their original values. |
| 182 | +- Drop the internal variable tables and replace them with the full variable tables. |
| 183 | +""" |
| 184 | +function prepare_tables_to_leave_rolling_horizon!(connection, variable_tables) |
| 185 | + DuckDB.query( |
| 186 | + connection, |
| 187 | + "UPDATE rep_periods_data |
| 188 | + SET num_timesteps = full_rep_periods_data.num_timesteps |
| 189 | + FROM full_rep_periods_data |
| 190 | + WHERE rep_periods_data.year = full_rep_periods_data.year |
| 191 | + AND rep_periods_data.rep_period = full_rep_periods_data.rep_period", |
| 192 | + ) |
| 193 | + DuckDB.query( |
| 194 | + connection, |
| 195 | + "UPDATE year_data |
| 196 | + SET length = full_year_data.length |
| 197 | + FROM full_year_data |
| 198 | + WHERE year_data.year = full_year_data.year |
| 199 | + ", |
| 200 | + ) |
| 201 | + |
| 202 | + # Drop the rolling horizon variable tables and rename the full_var_% tables |
| 203 | + for table_name in variable_tables |
| 204 | + DuckDB.query(connection, "DROP TABLE IF EXISTS $table_name") |
| 205 | + DuckDB.query(connection, "ALTER TABLE full_$table_name RENAME TO $table_name") |
| 206 | + end |
| 207 | + |
| 208 | + return |
| 209 | +end |
0 commit comments