rtc-tools-interface 0.8.0b4__tar.gz → 0.9.0a1__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 (60) hide show
  1. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/PKG-INFO +1 -1
  2. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/README.md +56 -14
  3. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtc_tools_interface.egg-info/PKG-INFO +1 -1
  4. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtc_tools_interface.egg-info/SOURCES.txt +3 -1
  5. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/_version.py +3 -3
  6. rtc-tools-interface-0.9.0a1/rtctools_interface/closed_loop/config.py +73 -0
  7. rtc-tools-interface-0.9.0a1/rtctools_interface/closed_loop/optimization_ranges.py +131 -0
  8. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/closed_loop/runner.py +43 -28
  9. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/closed_loop/time_series_handler.py +19 -0
  10. rtc-tools-interface-0.9.0a1/tests/closed_loop/test_optimization_ranges.py +64 -0
  11. rtc-tools-interface-0.9.0a1/tests/closed_loop/test_run_optization_problem_closed_loop.py +180 -0
  12. rtc-tools-interface-0.8.0b4/rtctools_interface/closed_loop/closed_loop_dates.py +0 -43
  13. rtc-tools-interface-0.8.0b4/tests/closed_loop/test_run_optization_problem_closed_loop.py +0 -98
  14. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/COPYING.LESSER +0 -0
  15. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtc_tools_interface.egg-info/dependency_links.txt +0 -0
  16. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtc_tools_interface.egg-info/requires.txt +0 -0
  17. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtc_tools_interface.egg-info/top_level.txt +0 -0
  18. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/__init__.py +0 -0
  19. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/closed_loop/__init__.py +0 -0
  20. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/closed_loop/results_construction.py +0 -0
  21. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/__init__.py +0 -0
  22. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/base_goal.py +0 -0
  23. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/base_optimization_problem.py +0 -0
  24. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/goal_generator_mixin.py +0 -0
  25. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/goal_performance_metrics.py +0 -0
  26. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/goal_table_schema.py +0 -0
  27. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/helpers/__init__.py +0 -0
  28. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/helpers/statistics_mixin.py +0 -0
  29. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/plot_goals_mixin.py +0 -0
  30. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/plot_mixin.py +0 -0
  31. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/read_goals.py +0 -0
  32. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/plotting/__init__.py +0 -0
  33. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/plotting/plot_tools.py +0 -0
  34. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/plotting/subplot_classes.py +0 -0
  35. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/simulation/__init__.py +0 -0
  36. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/simulation/base_simulation_problem.py +0 -0
  37. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/simulation/plot_mixin.py +0 -0
  38. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/__init__.py +0 -0
  39. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/plot_table_schema.py +0 -0
  40. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/read_goals_mixin.py +0 -0
  41. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/read_plot_table.py +0 -0
  42. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/results_collection.py +0 -0
  43. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/serialization.py +0 -0
  44. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/type_definitions.py +0 -0
  45. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/setup.cfg +0 -0
  46. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/setup.py +0 -0
  47. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/__init__.py +0 -0
  48. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/closed_loop/__init__.py +0 -0
  49. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/closed_loop/test_read_xml.py +0 -0
  50. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/optimization/__init__.py +0 -0
  51. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/optimization/test_base_optimization_problem.py +0 -0
  52. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/optimization/test_passing_goals_directly.py +0 -0
  53. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/optimization/test_plot_goals_mixin.py +0 -0
  54. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/optimization/test_read_goals.py +0 -0
  55. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/simulation/__init__.py +0 -0
  56. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/simulation/test_base_simulation_problem.py +0 -0
  57. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/simulation/test_plot_mixin.py +0 -0
  58. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/utils/__init__.py +0 -0
  59. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/utils/get_test.py +0 -0
  60. {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/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.0a1
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.0a1
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
@@ -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-06-18T15:06:29+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "6bc6e859ff0f928c2f4c1127a2aca08ce9720051",
15
- "version": "0.8.0b4"
14
+ "full-revisionid": "478b74b0fc61a477f80531ea4e3f54f1054bc603",
15
+ "version": "0.9.0a1"
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
@@ -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):
@@ -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
 
@@ -42,6 +42,10 @@ class TimeSeriesHandler(ABC):
42
42
  def get_timestep(self) -> datetime.timedelta:
43
43
  """Get the timestep of the timeseries data."""
44
44
 
45
+ @abstractmethod
46
+ def get_datetimes(self) -> List[datetime.datetime]:
47
+ """Get the dates of the timeseries."""
48
+
45
49
  @abstractmethod
46
50
  def get_datetime_range(self) -> Tuple[datetime.datetime, datetime.datetime]:
47
51
  """Get the date range of the timeseries data (min, max)."""
@@ -124,6 +128,9 @@ class CSVTimeSeriesFile(TimeSeriesHandler):
124
128
  def get_timestep(self):
125
129
  return self.data[self.date_col].diff().min()
126
130
 
131
+ def get_datetimes(self):
132
+ return self.data[self.date_col].to_list()
133
+
127
134
  def get_datetime_range(self):
128
135
  return self.data[self.date_col].min(), self.data[self.date_col].max()
129
136
 
@@ -323,6 +330,18 @@ class XMLTimeSeriesFile(TimeSeriesHandler):
323
330
  ) = self.get_external_id_from_series(series)
324
331
  return self.get_single_date_range(location_id, parameter_id, qualifier_ids)
325
332
 
333
+ def get_datetimes(self):
334
+ """Get the dates of all timeseries data."""
335
+ datetimes = set()
336
+ for series in self.root.findall("pi:series", ns):
337
+ events = series.findall("pi:event", ns)
338
+ for event in events:
339
+ event_datetime = self.__parse_date_time(event)
340
+ datetimes.add(event_datetime)
341
+ datetimes = list(datetimes)
342
+ datetimes.sort()
343
+ return datetimes
344
+
326
345
  def get_datetime_range(self):
327
346
  """Get the date range of the timeseries data, minimum and maximum over all series"""
328
347
  min_date = datetime.datetime.max
@@ -0,0 +1,64 @@
1
+ """Tests for getting optimization periods."""
2
+ import unittest
3
+ from datetime import datetime, timedelta
4
+
5
+ import rtctools_interface.closed_loop.optimization_ranges as opt_ranges
6
+
7
+
8
+ class TestGetOptimizationPeriods(unittest.TestCase):
9
+ """Test calculating optimization periods."""
10
+ def check_ranges(
11
+ self,
12
+ ranges: list[tuple[datetime, datetime]],
13
+ expected_ranges: list[tuple[datetime, datetime]]
14
+ ):
15
+ """Check if two lists of ranges are the same."""
16
+ n_exp_ranges = len(expected_ranges)
17
+ n_ranges = len(ranges)
18
+ self.assertEqual(n_ranges, n_exp_ranges)
19
+ for i_period in range(n_ranges):
20
+ exp_start, exp_end = expected_ranges[i_period]
21
+ start, end = expected_ranges[i_period]
22
+ self.assertEqual(start, exp_start)
23
+ self.assertEqual(end, exp_end)
24
+
25
+ def test_get_optimization_ranges(self):
26
+ """Test get_optimization_ranges."""
27
+ model_times = [
28
+ datetime(2024,1,1),
29
+ datetime(2024,1,2),
30
+ datetime(2024,1,4),
31
+ datetime(2024,1,5),
32
+ datetime(2024,1,7),
33
+ datetime(2024,1,8),
34
+ datetime(2024,1,10),
35
+ ]
36
+ start_time = datetime(2024,1,2)
37
+ forecast_timestep = timedelta(days=2)
38
+ optimization_period = timedelta(days=5)
39
+ ranges = opt_ranges.get_optimization_ranges(
40
+ model_times=model_times,
41
+ start_time=start_time,
42
+ forecast_timestep=forecast_timestep,
43
+ optimization_period=optimization_period
44
+ )
45
+ expected_ranges = [
46
+ (datetime(2024,1,2), datetime(2024,1,7)),
47
+ (datetime(2024,1,4), datetime(2024,1,8)),
48
+ (datetime(2024,1,5), datetime(2024,1,10)),
49
+ ]
50
+ self.check_ranges(ranges, expected_ranges)
51
+
52
+ def test_round_datetime_ranges_to_days(self):
53
+ """Test round_datetime_ranges_to_days."""
54
+ ranges = [
55
+ (datetime(2020,1,1), datetime(2020,1,2)),
56
+ (datetime(2020,1,2), datetime(2020,1,2)),
57
+ (datetime(2020,1,2,12), datetime(2020,1,5,23)),
58
+ ]
59
+ rounded_ranges = opt_ranges.round_datetime_ranges_to_days(ranges)
60
+ expected_ranges = [
61
+ (datetime(2020,1,1), datetime(2020,1,2,23,59,59,999999)),
62
+ (datetime(2020,1,2,12), datetime(2020,1,5,23,59,59,999999)),
63
+ ]
64
+ self.check_ranges(rounded_ranges, expected_ranges)
@@ -0,0 +1,180 @@
1
+ """Test the closed loop runner"""
2
+ import math
3
+ import xml.etree.ElementTree as ET
4
+ from datetime import timedelta
5
+ from pathlib import Path
6
+ from unittest import TestCase
7
+
8
+ import pandas as pd
9
+
10
+ from rtctools_interface.closed_loop.config import ClosedLoopConfig
11
+ from rtctools_interface.closed_loop.runner import run_optimization_problem_closed_loop
12
+ from .test_models.goal_programming_xml.src.example import Example as ExampleXml
13
+ from .test_models.goal_programming_csv.src.example import Example as ExampleCsv
14
+
15
+ ns = {"fews": "http://www.wldelft.nl/fews", "pi": "http://www.wldelft.nl/fews/PI"}
16
+
17
+ # Elementwise comparisons are practially disabled.
18
+ A_TOL = 0.1
19
+ R_TOL = 0.1
20
+
21
+
22
+ def compare_xml_file(file_result: Path, file_ref: Path):
23
+ """Compare two timeseries_export files elementwise."""
24
+ tree_result = ET.parse(file_result)
25
+ tree_ref = ET.parse(file_ref)
26
+ series_result = tree_result.findall("pi:series", ns)
27
+ series_ref = tree_ref.findall("pi:series", ns)
28
+ assert len(series_result) == len(series_ref), "Different number of series found in exports."
29
+ for serie_result, serie_ref in zip(series_result, series_ref):
30
+ for event_result, event_ref in zip(serie_result.findall("pi:event", ns), serie_ref.findall("pi:event", ns)):
31
+ value_result = float(event_result.attrib["value"])
32
+ value_ref = float(event_ref.attrib["value"])
33
+ assert math.isclose(
34
+ value_result, value_ref, rel_tol=R_TOL, abs_tol=A_TOL
35
+ ), f"Difference found in event: {value_result} != {value_ref}"
36
+
37
+
38
+ def compare_xml_files(output_modelling_period_folder: Path, reference_folder: Path):
39
+ """Compare the timeseries_export.xml files in the output and reference folders."""
40
+ for folder in output_modelling_period_folder.iterdir():
41
+ if not folder.is_dir():
42
+ continue
43
+ file_name = "timeseries_export.xml"
44
+ file_result = folder / file_name
45
+ file_ref = reference_folder / folder.name / file_name
46
+ compare_xml_file(file_result, file_ref)
47
+
48
+
49
+ class TestClosedLoop(TestCase):
50
+ """
51
+ Class for testing closed loop runner.
52
+ """
53
+
54
+ def compare_csv_files(self, output_folder: Path, reference_folder: Path, n_periods: int):
55
+ """Compare the csv files in output and reference subfolders."""
56
+ self.assertTrue(
57
+ output_folder.exists(),
58
+ "Output modelling period folder should be created."
59
+ )
60
+ self.assertEqual(
61
+ len(list(output_folder.iterdir())),
62
+ n_periods,
63
+ f"Error: {n_periods} modelling periods should be created."
64
+ )
65
+ for folder in output_folder.iterdir():
66
+ self.assertTrue((folder / "timeseries_export.csv").exists())
67
+ for folder in output_folder.iterdir():
68
+ reference_folder_i = reference_folder / folder.name
69
+ for file in folder.iterdir():
70
+ df_result = pd.read_csv(file)
71
+ df_ref = pd.read_csv(reference_folder_i / file.name)
72
+ pd.testing.assert_frame_equal(df_result, df_ref, atol=A_TOL, rtol=R_TOL)
73
+
74
+ def compare_csv_file(self, output_file: Path, reference_file: Path):
75
+ """Compare the main csv output file in output and reference folder."""
76
+ df_result = pd.read_csv(output_file)
77
+ df_ref = pd.read_csv(reference_file)
78
+ pd.testing.assert_frame_equal(df_result, df_ref, atol=A_TOL, rtol=R_TOL)
79
+
80
+ def compare_xml_files(self, output_folder: Path, reference_folder: Path, n_periods: int):
81
+ """Compare the xml files in output and reference subfolders."""
82
+ self.assertTrue(
83
+ output_folder.exists(),
84
+ "Output modelling period folder should be created."
85
+ )
86
+ self.assertEqual(
87
+ len([f for f in output_folder.iterdir() if f.is_dir()]),
88
+ n_periods,
89
+ f"Error: {n_periods} modelling periods should be created.",
90
+ )
91
+ for folder in output_folder.iterdir():
92
+ if folder.is_dir():
93
+ self.assertTrue((folder / "timeseries_export.xml").exists())
94
+ compare_xml_files(output_folder, reference_folder)
95
+
96
+ def compare_xml_file(self, output_file: Path, reference_file: Path):
97
+ """Compare the main xml output file in output and reference folder."""
98
+ compare_xml_file(output_file, reference_file)
99
+
100
+ def test_running_closed_loop_csv(self):
101
+ """
102
+ Check if test model runs without problems and generates same results.
103
+ """
104
+ base_folder = Path(__file__).parent / "test_models" / "goal_programming_csv"
105
+ config = ClosedLoopConfig(
106
+ file=base_folder / "input" / "closed_loop_dates.csv",
107
+ round_to_dates=True
108
+ )
109
+ run_optimization_problem_closed_loop(ExampleCsv, base_folder=base_folder, config=config)
110
+ self.compare_csv_files(
111
+ output_folder=base_folder / "output" / "output_modelling_periods",
112
+ reference_folder=base_folder / "output" / "output_modelling_periods_reference",
113
+ n_periods=3
114
+ )
115
+ self.compare_csv_file(
116
+ output_file=base_folder / "output" / "timeseries_export.csv",
117
+ reference_file=base_folder / "output" / "timeseries_export_reference.csv"
118
+ )
119
+
120
+ def test_running_closed_loop_csv_fixed_periods(self):
121
+ """
122
+ Check if test model runs for fixed optimization periods.
123
+ """
124
+ base_folder = Path(__file__).parent / "test_models" / "goal_programming_csv"
125
+ output_folder = "output_fixed_periods"
126
+ config = ClosedLoopConfig.from_fixed_periods(
127
+ optimization_period=timedelta(days=3),
128
+ forecast_timestep=timedelta(days=2)
129
+ )
130
+ run_optimization_problem_closed_loop(
131
+ ExampleCsv,
132
+ base_folder=base_folder,
133
+ config=config,
134
+ output_folder=output_folder
135
+ )
136
+ self.compare_csv_file(
137
+ output_file=base_folder / output_folder / "timeseries_export.csv",
138
+ reference_file=base_folder / "output" / "timeseries_export_reference.csv"
139
+ )
140
+
141
+ def test_running_closed_loop_xml(self):
142
+ """
143
+ Check if test model runs without problems and generates same results.
144
+ """
145
+ base_folder = Path(__file__).parent / "test_models" / "goal_programming_xml"
146
+ config = ClosedLoopConfig(
147
+ file=base_folder / "input" / "closed_loop_dates.csv",
148
+ round_to_dates=True
149
+ )
150
+ run_optimization_problem_closed_loop(ExampleXml, base_folder=base_folder, config=config)
151
+ self.compare_xml_files(
152
+ output_folder=base_folder / "output" / "output_modelling_periods",
153
+ reference_folder=base_folder / "output" / "output_modelling_periods_reference",
154
+ n_periods=3
155
+ )
156
+ self.compare_xml_file(
157
+ output_file=base_folder / "output" / "timeseries_export.xml",
158
+ reference_file=base_folder / "output" / "timeseries_export_reference.xml"
159
+ )
160
+
161
+ def test_running_closed_loop_xml_fixed_periods(self):
162
+ """
163
+ Check if test model runs for fixed optimization periods.
164
+ """
165
+ base_folder = Path(__file__).parent / "test_models" / "goal_programming_xml"
166
+ output_folder = "output_fixed_periods"
167
+ config = ClosedLoopConfig.from_fixed_periods(
168
+ optimization_period=timedelta(days=3),
169
+ forecast_timestep=timedelta(days=2)
170
+ )
171
+ run_optimization_problem_closed_loop(
172
+ ExampleXml,
173
+ base_folder=base_folder,
174
+ config=config,
175
+ output_folder=output_folder
176
+ )
177
+ self.compare_xml_file(
178
+ output_file=base_folder / output_folder / "timeseries_export.xml",
179
+ reference_file=base_folder / "output" / "timeseries_export_reference.xml"
180
+ )
@@ -1,43 +0,0 @@
1
- from pathlib import Path
2
-
3
- import pandas as pd
4
-
5
-
6
- def read_closed_loop_dates(file_path: Path):
7
- """Read horizon config from a csv file"""
8
- if not file_path.exists():
9
- raise FileNotFoundError(
10
- f"The closed_loop_dates csv does not exist. Please create a horizon config file in {file_path}."
11
- )
12
- try:
13
- closed_loop_dates = pd.read_csv(file_path)
14
- except pd.errors.EmptyDataError:
15
- raise ValueError(
16
- "The closed_loop_dates csv is empty. Please provide a valid file with start_date and end_date column."
17
- )
18
- closed_loop_dates.columns = closed_loop_dates.columns.str.replace(" ", "")
19
- if not all([col in closed_loop_dates.columns for col in ["start_date", "end_date"]]):
20
- raise ValueError("The closed_loop_dates csv should have both 'start_date' and 'end_date' columns.")
21
- closed_loop_dates["start_date"] = pd.to_datetime(closed_loop_dates["start_date"])
22
- closed_loop_dates["end_date"] = pd.to_datetime(closed_loop_dates["end_date"])
23
- for i in range(1, len(closed_loop_dates)):
24
- if closed_loop_dates["start_date"].iloc[i] > closed_loop_dates["end_date"].iloc[i - 1]:
25
- raise ValueError(f"Closed loop date table: Start date at row {i} is later than the previous end date. ")
26
- if any(closed_loop_dates["start_date"] < closed_loop_dates["start_date"].shift(1)):
27
- raise ValueError("Closed loop date table: The start dates are not in ascending order.")
28
- if any(closed_loop_dates["end_date"] < closed_loop_dates["end_date"].shift(1)):
29
- raise ValueError("Closed loop date table: The end dates are not in ascending order.")
30
- if any(closed_loop_dates["end_date"] < closed_loop_dates["start_date"]):
31
- raise ValueError("Closed loop date table: For one or more rows the end date is before the start date.")
32
- if any(closed_loop_dates["start_date"] > closed_loop_dates["end_date"]):
33
- raise ValueError("Closed loop date table: For one or more rows the start date is after the end date.")
34
- if (
35
- any(closed_loop_dates["start_date"].dt.hour != 0)
36
- or any(closed_loop_dates["start_date"].dt.minute != 0)
37
- or any(closed_loop_dates["end_date"].dt.hour != 0)
38
- or any(closed_loop_dates["end_date"].dt.minute != 0)
39
- ):
40
- raise ValueError(
41
- "Closed loop date table: Currently, the date ranges can only be specific up to the level of days."
42
- )
43
- return closed_loop_dates
@@ -1,98 +0,0 @@
1
- """Test the closed loop runner"""
2
- import math
3
- import xml.etree.ElementTree as ET
4
- from unittest import TestCase
5
- from pathlib import Path
6
- import pandas as pd
7
- from rtctools_interface.closed_loop.runner import run_optimization_problem_closed_loop
8
- from .test_models.goal_programming_xml.src.example import Example as ExampleXml
9
- from .test_models.goal_programming_csv.src.example import Example as ExampleCsv
10
-
11
- ns = {"fews": "http://www.wldelft.nl/fews", "pi": "http://www.wldelft.nl/fews/PI"}
12
-
13
- # Elementwise comparisons are practially disabled.
14
- A_TOL = 0.1
15
- R_TOL = 0.1
16
-
17
-
18
- def compare_xml_file(file_result: Path, file_ref: Path):
19
- """Compare two timeseries_export files elementwise."""
20
- tree_result = ET.parse(file_result)
21
- tree_ref = ET.parse(file_ref)
22
- series_result = tree_result.findall("pi:series", ns)
23
- series_ref = tree_ref.findall("pi:series", ns)
24
- assert len(series_result) == len(series_ref), "Different number of series found in exports."
25
- for serie_result, serie_ref in zip(series_result, series_ref):
26
- for event_result, event_ref in zip(serie_result.findall("pi:event", ns), serie_ref.findall("pi:event", ns)):
27
- value_result = float(event_result.attrib["value"])
28
- value_ref = float(event_ref.attrib["value"])
29
- assert math.isclose(
30
- value_result, value_ref, rel_tol=R_TOL, abs_tol=A_TOL
31
- ), f"Difference found in event: {value_result} != {value_ref}"
32
-
33
-
34
- def compare_xml_files(output_modelling_period_folder: Path, reference_folder: Path):
35
- """Compare the timeseries_export.xml files in the output and reference folders."""
36
- for folder in output_modelling_period_folder.iterdir():
37
- if not folder.is_dir():
38
- continue
39
- file_name = "timeseries_export.xml"
40
- file_result = folder / file_name
41
- file_ref = reference_folder / folder.name / file_name
42
- compare_xml_file(file_result, file_ref)
43
-
44
-
45
- class TestClosedLoop(TestCase):
46
- """
47
- Class for testing closed loop runner.
48
- """
49
-
50
- def test_running_closed_loop_csv(self):
51
- """
52
- Check if test model runs without problems and generates same results.
53
- """
54
- base_folder = Path(__file__).parent / "test_models" / "goal_programming_csv"
55
- run_optimization_problem_closed_loop(ExampleCsv, base_folder=base_folder)
56
-
57
- output_modelling_period_folder = base_folder / "output" / "output_modelling_periods"
58
- self.assertTrue(output_modelling_period_folder.exists(), "Output modelling period folder should be created.")
59
- self.assertEqual(
60
- len(list(output_modelling_period_folder.iterdir())), 3, "Three modelling periods should be created."
61
- )
62
- for folder in output_modelling_period_folder.iterdir():
63
- self.assertTrue((folder / "timeseries_export.csv").exists())
64
- reference_folder = base_folder / "output" / "output_modelling_periods_reference"
65
- for folder in output_modelling_period_folder.iterdir():
66
- reference_folder_i = reference_folder / folder.name
67
- for file in folder.iterdir():
68
- df_result = pd.read_csv(file)
69
- df_ref = pd.read_csv(reference_folder_i / file.name)
70
- pd.testing.assert_frame_equal(df_result, df_ref, atol=A_TOL, rtol=R_TOL)
71
- # Also compare the combined timeseries_export.csv
72
- df_result = pd.read_csv(base_folder / "output" / "timeseries_export.csv")
73
- df_ref = pd.read_csv(base_folder / "output" / "timeseries_export_reference.csv")
74
- pd.testing.assert_frame_equal(df_result, df_ref, atol=A_TOL, rtol=R_TOL)
75
-
76
- def test_running_closed_loop_xml(self):
77
- """
78
- Check if test model runs without problems and generates same results.
79
- """
80
- base_folder = Path(__file__).parent / "test_models" / "goal_programming_xml"
81
- run_optimization_problem_closed_loop(ExampleXml, base_folder=base_folder)
82
-
83
- output_modelling_period_folder = base_folder / "output" / "output_modelling_periods"
84
- self.assertTrue(output_modelling_period_folder.exists(), "Output modelling period folder should be created.")
85
- self.assertEqual(
86
- len([f for f in output_modelling_period_folder.iterdir() if f.is_dir()]),
87
- 3,
88
- "Three modelling periods should be created.",
89
- )
90
- for folder in output_modelling_period_folder.iterdir():
91
- if folder.is_dir():
92
- self.assertTrue((folder / "timeseries_export.xml").exists())
93
- reference_folder = base_folder / "output" / "output_modelling_periods_reference"
94
- compare_xml_files(output_modelling_period_folder, reference_folder)
95
- # Also compare the combined timeseries_export
96
- compare_xml_file(
97
- base_folder / "output" / "timeseries_export.xml", base_folder / "output" / "timeseries_export_reference.xml"
98
- )