rtc-tools-interface 0.8.0b4__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/PKG-INFO +1 -1
  2. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/README.md +56 -14
  3. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtc_tools_interface.egg-info/PKG-INFO +1 -1
  4. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtc_tools_interface.egg-info/SOURCES.txt +3 -1
  5. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtc_tools_interface.egg-info/requires.txt +4 -3
  6. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/_version.py +3 -3
  7. rtc-tools-interface-0.9.0/rtctools_interface/closed_loop/config.py +73 -0
  8. rtc-tools-interface-0.9.0/rtctools_interface/closed_loop/optimization_ranges.py +131 -0
  9. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/closed_loop/results_construction.py +17 -4
  10. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/closed_loop/runner.py +52 -30
  11. rtc-tools-interface-0.9.0/rtctools_interface/closed_loop/time_series_handler.py +234 -0
  12. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/optimization/base_goal.py +1 -1
  13. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/setup.py +9 -1
  14. rtc-tools-interface-0.9.0/tests/closed_loop/test_optimization_ranges.py +64 -0
  15. rtc-tools-interface-0.9.0/tests/closed_loop/test_run_optization_problem_closed_loop.py +180 -0
  16. rtc-tools-interface-0.8.0b4/rtctools_interface/closed_loop/closed_loop_dates.py +0 -43
  17. rtc-tools-interface-0.8.0b4/rtctools_interface/closed_loop/time_series_handler.py +0 -430
  18. rtc-tools-interface-0.8.0b4/tests/closed_loop/test_run_optization_problem_closed_loop.py +0 -98
  19. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/COPYING.LESSER +0 -0
  20. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtc_tools_interface.egg-info/dependency_links.txt +0 -0
  21. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtc_tools_interface.egg-info/top_level.txt +0 -0
  22. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/__init__.py +0 -0
  23. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/closed_loop/__init__.py +0 -0
  24. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/optimization/__init__.py +0 -0
  25. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/optimization/base_optimization_problem.py +0 -0
  26. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/optimization/goal_generator_mixin.py +0 -0
  27. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/optimization/goal_performance_metrics.py +0 -0
  28. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/optimization/goal_table_schema.py +0 -0
  29. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/optimization/helpers/__init__.py +0 -0
  30. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/optimization/helpers/statistics_mixin.py +0 -0
  31. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/optimization/plot_goals_mixin.py +0 -0
  32. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/optimization/plot_mixin.py +0 -0
  33. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/optimization/read_goals.py +0 -0
  34. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/plotting/__init__.py +0 -0
  35. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/plotting/plot_tools.py +0 -0
  36. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/plotting/subplot_classes.py +0 -0
  37. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/simulation/__init__.py +0 -0
  38. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/simulation/base_simulation_problem.py +0 -0
  39. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/simulation/plot_mixin.py +0 -0
  40. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/utils/__init__.py +0 -0
  41. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/utils/plot_table_schema.py +0 -0
  42. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/utils/read_goals_mixin.py +0 -0
  43. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/utils/read_plot_table.py +0 -0
  44. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/utils/results_collection.py +0 -0
  45. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/utils/serialization.py +0 -0
  46. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/rtctools_interface/utils/type_definitions.py +0 -0
  47. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/setup.cfg +0 -0
  48. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/__init__.py +0 -0
  49. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/closed_loop/__init__.py +0 -0
  50. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/closed_loop/test_read_xml.py +0 -0
  51. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/optimization/__init__.py +0 -0
  52. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/optimization/test_base_optimization_problem.py +0 -0
  53. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/optimization/test_passing_goals_directly.py +0 -0
  54. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/optimization/test_plot_goals_mixin.py +0 -0
  55. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/optimization/test_read_goals.py +0 -0
  56. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/simulation/__init__.py +0 -0
  57. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/simulation/test_base_simulation_problem.py +0 -0
  58. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/simulation/test_plot_mixin.py +0 -0
  59. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/utils/__init__.py +0 -0
  60. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/tests/utils/get_test.py +0 -0
  61. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rtc-tools-interface
3
- Version: 0.8.0b4
3
+ Version: 0.9.0
4
4
  Summary: Toolbox for user interfaces for RTC-Tools
5
5
  Home-page: UNKNOWN
6
6
  Author: Deltares
@@ -172,30 +172,72 @@ The table could thus look like:
172
172
  After running the model, in your output folder the folder `figures` containing the figures is created.
173
173
 
174
174
  ## Closed loop runner
175
- To run a closed loop experiment one can use the `run_optimization_problem_closed_loop` function from `run_closed_loop`.
176
- This function is a drop-in replacement for the `run_optimization_problem` of rtc-tools. The user only needs to specify the `closed_loop_dates.csv` in the input folder of the optimization problem.
175
+
176
+ To run a closed loop experiment
177
+ one can use the `run_optimization_problem_closed_loop` function
178
+ from `rtctools_interface.closed_loop.runner`.
179
+ This function is a drop-in replacement for the `run_optimization_problem` of rtc-tools.
180
+ The user needs to specify a `ClosedLoopConfig` configuration
181
+ from `rtctools_interface.closed_loop.config`
182
+ to specify the time ranges for which to subsequentially solve the optimization problem.
177
183
 
178
184
  ### Setup
179
- Import `run_optimization_problem_closed_loop` with:
185
+
186
+ Import `ClosedLoopConfig` and `run_optimization_problem_closed_loop` with:
187
+
188
+ ```python
189
+ from rtctools_interface.closed_loop.config import ClosedLoopConfig
190
+ from rtctools_interface.closed_loop.runner import run_optimization_problem_closed_loop
191
+ ```
192
+
193
+ A `ClosedLoopConfig` configuration can be created from a csv file or
194
+ from a given forecast timestep (time between each time range)
195
+ and optimization period (duration of each time range).
196
+ An option `round_to_dates` can be used to round the start and end time of each range to a date,
197
+ i.e. the start time is rounded to the start of the day
198
+ and the end time is rounded to the end of a day.
199
+ Examples of creating a configuration are given below.
200
+
180
201
  ```python
181
- from rtctools_interface import run_optimization_problem_closed_loop
202
+ from datetime import timedelta
203
+
204
+ from rtctools_interface.closed_loop.config import ClosedLoopConfig
205
+
206
+ config_from_file = ClosedLoopConfig(
207
+ file="path/to/closed_loop_dates.csv",
208
+ round_to_dates=True
209
+ )
210
+
211
+ config_from_fixed_periods = ClosedLoopConfig.from_fixed_periods(
212
+ optimization_period=timedelta(days=3),
213
+ forecast_timestep=timedelta(days=2)
214
+ )
182
215
  ```
183
- Add a table named `closed_loop_dates.csv` to the input folder of your optimization problem. The table should contain two columns: `start_date` and `end_date`.
184
- Each row of the table corresponds to one modelling period.
185
216
 
186
- Example table `closed_loop_dates.csv`:
217
+ The CSV file `closed_loop_dates.csv` has two columns `start_date` and `end_date`
218
+ and looks as follows.
219
+
187
220
  ```
188
221
  start_date, end_date
189
222
  2024-05-19, 2024-05-23
190
223
  2024-05-23, 2024-05-25
191
224
  ```
192
- With this table rtc-tools will run two optimization problems (modelling periods): one with the data from 2024-05-19 upto and including 2024-05-23 and one from 2024-05-23 upto and including 2024-05-25.
193
- The `run_optimization_problem_closed_loop` will automatically set the final results from the previous as initial conditions of the next run. Note that this happens for:
194
- - All variables available at the first time step in original timeseries_import, but not available at any timestep in the modelling period.
225
+
226
+ The `run_optimization_problem_closed_loop` will solve the optimization problem
227
+ for each time range subseqentually.
228
+ It will use the final results from the previous run to set the initial values of the next run.
229
+ Note that this happens for:
230
+
231
+ - All variables available at the first time step in original timeseries_import,
232
+ but not available at any timestep in the modelling period.
195
233
  - All variables in the `initial_state.csv` (if the csv_mixin is used).
196
234
 
197
235
  ### Notes
198
- - The first start_date in your `closed_loop_dates.csv` should be equal to the start_date of your timeseries_import.
199
- - The different horizons should overlap with at least one day (to allow retrieving and setting initial values). An overlap of more days is allowed.
200
- - Currently only a single timestep is copied as an initial value.
201
- - The closed_loop runner only works in combination with the CSVMixin or the PIMixin. The CDFMixin is not supported.
236
+
237
+ - The start time of the first time range should coincide
238
+ with the start time of the input timeseries.
239
+ - The start time of the next time range should be less or equal
240
+ to the end time of the current time range.
241
+ - Currently, only the initial values of the first time step in a given time range are set.
242
+ - The closed_loop runner only works in combination with the CSVMixin or the PIMixin.
243
+ The CDFMixin is not supported.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rtc-tools-interface
3
- Version: 0.8.0b4
3
+ Version: 0.9.0
4
4
  Summary: Toolbox for user interfaces for RTC-Tools
5
5
  Home-page: UNKNOWN
6
6
  Author: Deltares
@@ -11,7 +11,8 @@ rtc_tools_interface.egg-info/top_level.txt
11
11
  rtctools_interface/__init__.py
12
12
  rtctools_interface/_version.py
13
13
  rtctools_interface/closed_loop/__init__.py
14
- rtctools_interface/closed_loop/closed_loop_dates.py
14
+ rtctools_interface/closed_loop/config.py
15
+ rtctools_interface/closed_loop/optimization_ranges.py
15
16
  rtctools_interface/closed_loop/results_construction.py
16
17
  rtctools_interface/closed_loop/runner.py
17
18
  rtctools_interface/closed_loop/time_series_handler.py
@@ -41,6 +42,7 @@ rtctools_interface/utils/serialization.py
41
42
  rtctools_interface/utils/type_definitions.py
42
43
  tests/__init__.py
43
44
  tests/closed_loop/__init__.py
45
+ tests/closed_loop/test_optimization_ranges.py
44
46
  tests/closed_loop/test_read_xml.py
45
47
  tests/closed_loop/test_run_optization_problem_closed_loop.py
46
48
  tests/optimization/__init__.py
@@ -1,6 +1,7 @@
1
- pandas
2
- rtc-tools>=2.5.0
3
1
  matplotlib
4
- plotly
5
2
  numpy
3
+ pandas
4
+ plotly
6
5
  pydantic
6
+ casadi!=3.6.6
7
+ rtc-tools>=2.7.0a3
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-06-11T11:21:50+0200",
11
+ "date": "2024-08-13T11:30:09+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "6bc6e859ff0f928c2f4c1127a2aca08ce9720051",
15
- "version": "0.8.0b4"
14
+ "full-revisionid": "a1e33a6c949d514b395d8a3a5bcd341f6c7cc7ad",
15
+ "version": "0.9.0"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -0,0 +1,73 @@
1
+ """Module for configuring a closed-loop optimization problem."""
2
+ from pathlib import Path
3
+ from datetime import timedelta
4
+
5
+
6
+ class ClosedLoopConfig():
7
+ """Configuration of a closed-loop optimization problem."""
8
+
9
+ def __init__(
10
+ self,
11
+ file: Path = None,
12
+ round_to_dates: bool = False,
13
+ ):
14
+ """
15
+ Create a configuration for closed-loop optimization.
16
+
17
+ :param file: CSV file with two columns 'start_date' and 'end_date'.
18
+ Each row indicates a time range for which to optimize a given problem.
19
+ Note:
20
+ * The start time of the first time range should coincide with the
21
+ start time of the input timeseries.
22
+ * The start time of the next time range should be less or equal
23
+ to the end time of the current time range.
24
+ :param round_to_dates: If true, the start time and end time of each time range
25
+ is rounded to just the date.
26
+ In particular, the start time is rounded to the start of the day
27
+ and the end time is rounded to the end of the day.
28
+ """
29
+ if file is not None:
30
+ file = Path(file).resolve()
31
+ self._file = file
32
+ self._forecast_timestep = None
33
+ self._optimization_period = None
34
+ self.round_to_dates = round_to_dates
35
+
36
+ @classmethod
37
+ def from_fixed_periods(
38
+ cls,
39
+ forecast_timestep: timedelta,
40
+ optimization_period: timedelta,
41
+ round_to_dates: bool = False,
42
+ ):
43
+ """
44
+ Create a closed loop configuration based on fixed periods.
45
+
46
+ :param forecast_timestep: The time between the start of each optimization time range.
47
+ :param optimization_period: the duration of each optimization time range
48
+ """
49
+ if forecast_timestep > optimization_period:
50
+ raise ValueError(
51
+ f"The forecast timestep ({forecast_timestep}) should be less than or equal to"
52
+ f" the optimization period ({optimization_period})."
53
+ )
54
+ config = cls()
55
+ config._forecast_timestep = forecast_timestep
56
+ config._optimization_period = optimization_period
57
+ config.round_to_dates = round_to_dates
58
+ return config
59
+
60
+ @property
61
+ def file(self):
62
+ """Get the file that defines the closed-loop periods."""
63
+ return self._file
64
+
65
+ @property
66
+ def forecast_timestep(self):
67
+ """Get the forecast timestep of the closed-loop periods."""
68
+ return self._forecast_timestep
69
+
70
+ @property
71
+ def optimization_period(self):
72
+ """Get the optimization period of the closed-loop periods."""
73
+ return self._optimization_period
@@ -0,0 +1,131 @@
1
+ """Module for calculating optimization periods."""
2
+ import bisect
3
+ import datetime
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+
8
+
9
+ def get_optimization_ranges_from_file(
10
+ file_path: Path, model_time_range: tuple[datetime.datetime, datetime.datetime]
11
+ ):
12
+ """Read horizon config from a csv file"""
13
+ if not file_path.exists():
14
+ raise FileNotFoundError(
15
+ f"The closed_loop_dates csv does not exist. Please create a horizon config file in {file_path}."
16
+ )
17
+ try:
18
+ closed_loop_dates = pd.read_csv(file_path)
19
+ except pd.errors.EmptyDataError:
20
+ raise ValueError(
21
+ "The closed_loop_dates csv is empty. Please provide a valid file with start_date and end_date column."
22
+ )
23
+ closed_loop_dates.columns = closed_loop_dates.columns.str.replace(" ", "")
24
+ if not all([col in closed_loop_dates.columns for col in ["start_date", "end_date"]]):
25
+ raise ValueError("The closed_loop_dates csv should have both 'start_date' and 'end_date' columns.")
26
+ closed_loop_dates["start_date"] = pd.to_datetime(closed_loop_dates["start_date"])
27
+ closed_loop_dates["end_date"] = pd.to_datetime(closed_loop_dates["end_date"])
28
+ for i in range(1, len(closed_loop_dates)):
29
+ if closed_loop_dates["start_date"].iloc[i] > closed_loop_dates["end_date"].iloc[i - 1]:
30
+ raise ValueError(f"Closed loop date table: Start date at row {i} is later than the previous end date. ")
31
+ if any(closed_loop_dates["start_date"] < closed_loop_dates["start_date"].shift(1)):
32
+ raise ValueError("Closed loop date table: The start dates are not in ascending order.")
33
+ if any(closed_loop_dates["end_date"] < closed_loop_dates["end_date"].shift(1)):
34
+ raise ValueError("Closed loop date table: The end dates are not in ascending order.")
35
+ if any(closed_loop_dates["end_date"] < closed_loop_dates["start_date"]):
36
+ raise ValueError("Closed loop date table: For one or more rows the end date is before the start date.")
37
+ if any(closed_loop_dates["start_date"] > closed_loop_dates["end_date"]):
38
+ raise ValueError("Closed loop date table: For one or more rows the start date is after the end date.")
39
+ if (
40
+ any(closed_loop_dates["start_date"].dt.hour != 0)
41
+ or any(closed_loop_dates["start_date"].dt.minute != 0)
42
+ or any(closed_loop_dates["end_date"].dt.hour != 0)
43
+ or any(closed_loop_dates["end_date"].dt.minute != 0)
44
+ ):
45
+ raise ValueError(
46
+ "Closed loop date table: Currently, the date ranges can only be specific up to the level of days."
47
+ )
48
+ assert (
49
+ min(closed_loop_dates["start_date"]).date() == model_time_range[0].date()
50
+ ), (
51
+ "The start day of the first optimization run is not equal"
52
+ " to the start day of the timeseries import."
53
+ )
54
+ assert (
55
+ max(closed_loop_dates["end_date"]).date() <= model_time_range[1].date()
56
+ ), (
57
+ "The end date of one or more optimization runs is later"
58
+ " than the end date of the timeseries import."
59
+ )
60
+ closed_loop_dates = [
61
+ (optimization_range["start_date"], optimization_range["end_date"])
62
+ for _, optimization_range in closed_loop_dates.iterrows()
63
+ ]
64
+ return closed_loop_dates
65
+
66
+
67
+ def _get_next_time_index(
68
+ times: list[datetime.date],
69
+ i_current: int,
70
+ timestep_size: datetime.timedelta
71
+ ) -> int:
72
+ """
73
+ Get the next timestep index.
74
+
75
+ The next timestep index i_next is the highest index such that:
76
+ * times[i_next] > times[i_current]
77
+ * times[i_next] <= times[i_current] + timestep_size
78
+ In case i_current >= i_max, i_next will be set to i_max, where i_max = len(times) - 1.
79
+ """
80
+ i_max = len(times) - 1
81
+ if i_current >= i_max:
82
+ return i_max
83
+ current_time = times[i_current]
84
+ next_time = current_time + timestep_size
85
+ i_next = bisect.bisect_right(times, next_time) - 1
86
+ i_next = max(i_current + 1, i_next)
87
+ return i_next
88
+
89
+
90
+ def get_optimization_ranges(
91
+ model_times: list[datetime.date],
92
+ start_time: datetime.datetime,
93
+ forecast_timestep: datetime.timedelta,
94
+ optimization_period: datetime.timedelta
95
+ ) -> list[tuple[datetime.datetime, datetime.datetime]]:
96
+ """Calculate a list of optimization periods."""
97
+ if forecast_timestep > optimization_period:
98
+ raise ValueError(
99
+ f"Forecast timestep {forecast_timestep} cannot be larger than"
100
+ f" the optimization period {optimization_period}."
101
+ )
102
+ if start_time not in model_times:
103
+ raise ValueError(f"Start time {start_time} is not in the given model times.")
104
+ i_max = len(model_times) - 1
105
+ i_start = model_times.index(start_time)
106
+ i_end = _get_next_time_index(model_times, i_start, optimization_period)
107
+ optimization_periods = []
108
+ optimization_periods.append((model_times[i_start], model_times[i_end]))
109
+ while i_end < i_max:
110
+ i_start = _get_next_time_index(model_times, i_start, forecast_timestep)
111
+ i_end = _get_next_time_index(model_times, i_start, optimization_period)
112
+ optimization_periods.append((model_times[i_start], model_times[i_end]))
113
+ return optimization_periods
114
+
115
+
116
+ def round_datetime_ranges_to_days(
117
+ datetime_ranges: list[tuple[datetime.datetime, datetime.datetime]]
118
+ ) -> list[tuple[datetime.datetime, datetime.datetime]]:
119
+ """Round datetimes to dats in datetime ranges.
120
+
121
+ The start of the range is rounded to the start of the day
122
+ and the end of the range is rounded to the end of the day.
123
+ """
124
+ rounded_ranges = []
125
+ for start, end in datetime_ranges:
126
+ if start.date() == end.date():
127
+ continue
128
+ start = datetime.datetime.combine(start.date(), datetime.time.min)
129
+ end = datetime.datetime.combine(end.date(), datetime.time.max)
130
+ rounded_ranges.append((start, end))
131
+ return rounded_ranges
@@ -10,6 +10,18 @@ from rtctools.data import pi
10
10
  logger = logging.getLogger("rtctools")
11
11
 
12
12
 
13
+ def _get_variables_from_pi(data_config: rtc.DataConfig, timeseries: pi.Timeseries):
14
+ """Get all variables of a PI timeseries that are in the data configuration."""
15
+ variables = []
16
+ for var, _ in timeseries.items():
17
+ try:
18
+ data_config.pi_variable_ids(var)
19
+ variables.append(var)
20
+ except KeyError:
21
+ pass
22
+ return variables
23
+
24
+
13
25
  def combine_xml_exports(output_base_path: Path, original_input_timeseries_path: Path, write_csv_out: bool = False):
14
26
  """Combine the xml exports of multiple periods into a single xml file."""
15
27
  logger.info("Combining XML exports.")
@@ -24,9 +36,10 @@ def combine_xml_exports(output_base_path: Path, original_input_timeseries_path:
24
36
  ts_export = pi.Timeseries(
25
37
  data_config=dataconfig, folder=output_base_path / "period_0", basename="timeseries_export", binary=False
26
38
  ) # Use the first timeseries export as a starting point for the combined timeseries export.
27
- ts_export._Timeseries__path_xml = os.path.join(output_base_path.parent, "timeseries_export.xml")
28
39
  ts_export.resize(orig_start_datetime, orig_end_datetime)
29
40
 
41
+ variables = _get_variables_from_pi(data_config=dataconfig, timeseries=ts_export)
42
+
30
43
  i = 0
31
44
  while os.path.isfile(os.path.join(output_base_path, f"period_{i}", "timeseries_export.xml")):
32
45
  ts_export_step = pi.Timeseries(
@@ -38,7 +51,7 @@ def combine_xml_exports(output_base_path: Path, original_input_timeseries_path:
38
51
  all_times = ts_import_orig.times # Workaround to map indices to times, as ts_export does
39
52
  # not contain all times. TODO Check whether the assumption that these times map to
40
53
  # the correct indices for ts_export always holds.
41
- for loc_par in dataconfig._DataConfig__location_parameter_ids:
54
+ for loc_par in variables:
42
55
  try:
43
56
  current_values = ts_export.get(loc_par)
44
57
  new_values = ts_export_step.get(loc_par)
@@ -60,12 +73,12 @@ def combine_xml_exports(output_base_path: Path, original_input_timeseries_path:
60
73
  combined_values[start_new_data_index : start_new_data_index + len(new_values)] = new_values # noqa
61
74
  ts_export.set(loc_par, combined_values)
62
75
  i += 1
63
- ts_export.write()
76
+ ts_export.write(output_folder=output_base_path.parent, output_filename="timeseries_export")
64
77
 
65
78
  if write_csv_out:
66
79
  data = pd.DataFrame()
67
80
  data["date"] = all_times
68
- for timeseries_id in dataconfig._DataConfig__location_parameter_ids:
81
+ for timeseries_id in variables:
69
82
  try:
70
83
  values = ts_export.get(timeseries_id)
71
84
  except KeyError:
@@ -9,7 +9,8 @@ from rtctools.data.pi import DiagHandler
9
9
  from rtctools.optimization.pi_mixin import PIMixin
10
10
  from rtctools.optimization.csv_mixin import CSVMixin
11
11
  from rtctools.util import run_optimization_problem, _resolve_folder
12
- from rtctools_interface.closed_loop.closed_loop_dates import read_closed_loop_dates
12
+ from rtctools_interface.closed_loop.config import ClosedLoopConfig
13
+ import rtctools_interface.closed_loop.optimization_ranges as opt_ranges
13
14
  from rtctools_interface.closed_loop.results_construction import combine_csv_exports, combine_xml_exports
14
15
  from rtctools_interface.closed_loop.time_series_handler import XMLTimeSeriesFile, CSVTimeSeriesFile, TimeSeriesHandler
15
16
  import logging
@@ -53,26 +54,49 @@ def write_input_folder(
53
54
  timeseries_import.write(modelling_period_input_folder_i)
54
55
 
55
56
 
57
+ def _get_optimization_ranges(
58
+ config: ClosedLoopConfig,
59
+ input_timeseries: TimeSeriesHandler,
60
+ ) -> list[tuple[datetime.datetime, datetime.datetime]]:
61
+ """Return a list of optimization periods."""
62
+ if config.file is not None:
63
+ datetime_range = input_timeseries.get_datetime_range()
64
+ optimization_ranges = opt_ranges.get_optimization_ranges_from_file(
65
+ config.file, datetime_range
66
+ )
67
+ elif config.optimization_period is not None:
68
+ datetimes = input_timeseries.get_datetimes()
69
+ optimization_ranges = opt_ranges.get_optimization_ranges(
70
+ model_times=datetimes,
71
+ start_time=datetimes[0],
72
+ forecast_timestep=config.forecast_timestep,
73
+ optimization_period=config.optimization_period,
74
+ )
75
+ else:
76
+ raise ValueError(
77
+ "The closed-loop configuration should have either a file or optimization_period set."
78
+ )
79
+ if config.round_to_dates:
80
+ optimization_ranges = opt_ranges.round_datetime_ranges_to_days(optimization_ranges)
81
+ return optimization_ranges
82
+
83
+
56
84
  def run_optimization_problem_closed_loop(
57
85
  optimization_problem_class,
58
86
  base_folder="..",
59
87
  log_level=logging.INFO,
60
88
  profile=False,
89
+ config: ClosedLoopConfig = None,
61
90
  **kwargs,
62
91
  ):
63
- """Runs an optimization problem in closed loop mode.
64
- This function is a drop-in replacement for the run_optimization_problem of rtc-tools. The user
65
- needs to specify a closed_loop_dates.csv in the input folder of the optimization problem. This
66
- CSV file should contain two columns: start_date and end_date, each row corresponding to one
67
- modelling period. The function will run optimization problems for each modelling period specified
68
- in the CSV file, setting the final results from the previous run as initial conditions for the next.
69
- See the readme.md for more details.
70
-
71
- Notes:
72
- - The first start_date in your closed_loop_dates.csv should be equal to the start_date of
73
- your timeseries_import.
74
- - The different horizons should overlap with at least one day to allow retrieving and setting
75
- initial values. More days of overlap are allowed.
92
+ """
93
+ Runs an optimization problem in closed loop mode.
94
+
95
+ This function is a drop-in replacement for the run_optimization_problem of rtc-tools.
96
+ The user needs to specify a closed-loop configuration that describes the time ranges
97
+ for which to subsequentially solve the optimization problem.
98
+ The results from the previous run will be used to set the initial values of the next run.
99
+ See README.md for more details.
76
100
  """
77
101
  base_folder = Path(base_folder)
78
102
  if not os.path.isabs(base_folder):
@@ -91,10 +115,10 @@ def run_optimization_problem_closed_loop(
91
115
  if not any((isinstance(h, DiagHandler) for h in logger.handlers)):
92
116
  diag_handler = DiagHandler(original_output_folder)
93
117
  logger.addHandler(diag_handler)
118
+ logger.setLevel(log_level)
94
119
 
95
120
  if issubclass(optimization_problem_class, PIMixin):
96
121
  original_import = XMLTimeSeriesFile(original_input_folder)
97
- original_import.set_reference_data(copy.deepcopy(original_import))
98
122
  elif issubclass(optimization_problem_class, CSVMixin):
99
123
  original_import = CSVTimeSeriesFile(original_input_folder)
100
124
  else:
@@ -112,24 +136,15 @@ def run_optimization_problem_closed_loop(
112
136
  shutil.rmtree(modelling_periods_output_folder)
113
137
  modelling_periods_output_folder.mkdir(exist_ok=True)
114
138
 
115
- original_date_range = original_import.get_datetime_range()
116
- closed_loop_dates = read_closed_loop_dates(original_input_folder / "closed_loop_dates.csv")
117
- assert (
118
- min(closed_loop_dates["start_date"]).date() == original_date_range[0].date()
119
- ), "The start day of the first optimization run is not equal to the start day of the timeseries import."
120
- assert (
121
- max(closed_loop_dates["end_date"]).date() <= original_date_range[1].date()
122
- ), "The end date of one or more optimization runs is later than the end date of the timeseries import."
123
-
139
+ if config is None:
140
+ config = ClosedLoopConfig(original_input_folder / "closed_loop_dates.csv")
141
+ optimization_ranges = _get_optimization_ranges(config, original_import)
124
142
  results_previous_run = None
125
143
  previous_run_datetimes = None
126
- for i, date_range in closed_loop_dates.iterrows():
144
+ for i, (start_time, end_time) in enumerate(optimization_ranges):
127
145
  timeseries_import = copy.deepcopy(original_import)
128
146
 
129
- timeseries_import.select_time_range(
130
- start_date=date_range["start_date"],
131
- end_date=date_range["end_date"] + datetime.timedelta(days=1) - datetime.timedelta(seconds=1),
132
- )
147
+ timeseries_import.select_time_range(start_date=start_time, end_date=end_time)
133
148
 
134
149
  set_initial_values_from_previous_run(results_previous_run, timeseries_import, previous_run_datetimes)
135
150
 
@@ -139,6 +154,7 @@ def run_optimization_problem_closed_loop(
139
154
  modelling_period_input_folder_i = modelling_period_input_folder / modelling_period_name
140
155
  write_input_folder(modelling_period_input_folder_i, original_input_folder, timeseries_import)
141
156
 
157
+ logger.info(f"Running optimization for period {i}: {(str(start_time), str(end_time))}.")
142
158
  result = run_optimization_problem(
143
159
  optimization_problem_class,
144
160
  base_folder,
@@ -148,7 +164,13 @@ def run_optimization_problem_closed_loop(
148
164
  output_folder=modelling_period_output_folder_i,
149
165
  **kwargs,
150
166
  )
151
- logger.info(f"Finished optimization run {i} with result {result}")
167
+ period = f"period {i} {(str(start_time), str(end_time))}"
168
+ if result.solver_stats["success"]:
169
+ logger.info(f"Successful optimization for {period}.")
170
+ else:
171
+ message = f"Failed optimization for {period} with status '{result.solver_stats['return_status']}'."
172
+ logger.error(message)
173
+ raise Exception(message)
152
174
 
153
175
  results_previous_run = {key: result.extract_results().get(key) for key in variables_in_import}
154
176
  previous_run_datetimes = result.io.datetimes