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.
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/PKG-INFO +1 -1
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/README.md +56 -14
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtc_tools_interface.egg-info/PKG-INFO +1 -1
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtc_tools_interface.egg-info/SOURCES.txt +3 -1
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/_version.py +3 -3
- rtc-tools-interface-0.9.0a1/rtctools_interface/closed_loop/config.py +73 -0
- rtc-tools-interface-0.9.0a1/rtctools_interface/closed_loop/optimization_ranges.py +131 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/closed_loop/runner.py +43 -28
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/closed_loop/time_series_handler.py +19 -0
- rtc-tools-interface-0.9.0a1/tests/closed_loop/test_optimization_ranges.py +64 -0
- rtc-tools-interface-0.9.0a1/tests/closed_loop/test_run_optization_problem_closed_loop.py +180 -0
- rtc-tools-interface-0.8.0b4/rtctools_interface/closed_loop/closed_loop_dates.py +0 -43
- rtc-tools-interface-0.8.0b4/tests/closed_loop/test_run_optization_problem_closed_loop.py +0 -98
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/COPYING.LESSER +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtc_tools_interface.egg-info/dependency_links.txt +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtc_tools_interface.egg-info/requires.txt +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtc_tools_interface.egg-info/top_level.txt +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/closed_loop/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/closed_loop/results_construction.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/base_goal.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/base_optimization_problem.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/goal_generator_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/goal_performance_metrics.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/goal_table_schema.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/helpers/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/helpers/statistics_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/plot_goals_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/plot_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/optimization/read_goals.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/plotting/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/plotting/plot_tools.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/plotting/subplot_classes.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/simulation/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/simulation/base_simulation_problem.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/simulation/plot_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/plot_table_schema.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/read_goals_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/read_plot_table.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/results_collection.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/serialization.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/type_definitions.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/setup.cfg +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/setup.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/closed_loop/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/closed_loop/test_read_xml.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/optimization/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/optimization/test_base_optimization_problem.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/optimization/test_passing_goals_directly.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/optimization/test_plot_goals_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/optimization/test_read_goals.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/simulation/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/simulation/test_base_simulation_problem.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/simulation/test_plot_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/utils/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/utils/get_test.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/versioneer.py +0 -0
|
@@ -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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
The `run_optimization_problem_closed_loop` will
|
|
194
|
-
|
|
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
|
-
|
|
199
|
-
- The
|
|
200
|
-
|
|
201
|
-
- The
|
|
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.
|
{rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtc_tools_interface.egg-info/SOURCES.txt
RENAMED
|
@@ -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/
|
|
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-
|
|
11
|
+
"date": "2024-06-18T15:06:29+0200",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "0.
|
|
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
|
{rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/closed_loop/runner.py
RENAMED
|
@@ -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.
|
|
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
|
-
"""
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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,
|
|
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
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/plotting/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/rtctools_interface/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/closed_loop/test_read_xml.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/optimization/test_read_goals.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a1}/tests/simulation/test_plot_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|