rtc-tools-interface 0.8.0b4__tar.gz → 0.9.0a2__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.0a2}/PKG-INFO +1 -1
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/README.md +56 -14
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtc_tools_interface.egg-info/PKG-INFO +1 -1
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtc_tools_interface.egg-info/SOURCES.txt +3 -1
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtc_tools_interface.egg-info/requires.txt +3 -3
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/_version.py +3 -3
- rtc-tools-interface-0.9.0a2/rtctools_interface/closed_loop/config.py +73 -0
- rtc-tools-interface-0.9.0a2/rtctools_interface/closed_loop/optimization_ranges.py +131 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/closed_loop/results_construction.py +17 -4
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/closed_loop/runner.py +46 -30
- rtc-tools-interface-0.9.0a2/rtctools_interface/closed_loop/time_series_handler.py +234 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/base_goal.py +1 -1
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/setup.py +8 -1
- rtc-tools-interface-0.9.0a2/tests/closed_loop/test_optimization_ranges.py +64 -0
- rtc-tools-interface-0.9.0a2/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/rtctools_interface/closed_loop/time_series_handler.py +0 -430
- 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.0a2}/COPYING.LESSER +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtc_tools_interface.egg-info/dependency_links.txt +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtc_tools_interface.egg-info/top_level.txt +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/closed_loop/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/base_optimization_problem.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/goal_generator_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/goal_performance_metrics.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/goal_table_schema.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/helpers/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/helpers/statistics_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/plot_goals_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/plot_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/read_goals.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/plotting/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/plotting/plot_tools.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/plotting/subplot_classes.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/simulation/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/simulation/base_simulation_problem.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/simulation/plot_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/plot_table_schema.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/read_goals_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/read_plot_table.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/results_collection.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/serialization.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/type_definitions.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/setup.cfg +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/closed_loop/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/closed_loop/test_read_xml.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/optimization/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/optimization/test_base_optimization_problem.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/optimization/test_passing_goals_directly.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/optimization/test_plot_goals_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/optimization/test_read_goals.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/simulation/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/simulation/test_base_simulation_problem.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/simulation/test_plot_mixin.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/utils/__init__.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/tests/utils/get_test.py +0 -0
- {rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/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.0a2}/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-
|
|
11
|
+
"date": "2024-08-01T10:56:38+0200",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "0.
|
|
14
|
+
"full-revisionid": "ef695c81333aa6aca2b0d62cc2b86ff937294803",
|
|
15
|
+
"version": "0.9.0a2"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Module for configuring a closed-loop optimization problem."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ClosedLoopConfig():
|
|
7
|
+
"""Configuration of a closed-loop optimization problem."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
file: Path = None,
|
|
12
|
+
round_to_dates: bool = False,
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
Create a configuration for closed-loop optimization.
|
|
16
|
+
|
|
17
|
+
:param file: CSV file with two columns 'start_date' and 'end_date'.
|
|
18
|
+
Each row indicates a time range for which to optimize a given problem.
|
|
19
|
+
Note:
|
|
20
|
+
* The start time of the first time range should coincide with the
|
|
21
|
+
start time of the input timeseries.
|
|
22
|
+
* The start time of the next time range should be less or equal
|
|
23
|
+
to the end time of the current time range.
|
|
24
|
+
:param round_to_dates: If true, the start time and end time of each time range
|
|
25
|
+
is rounded to just the date.
|
|
26
|
+
In particular, the start time is rounded to the start of the day
|
|
27
|
+
and the end time is rounded to the end of the day.
|
|
28
|
+
"""
|
|
29
|
+
if file is not None:
|
|
30
|
+
file = Path(file).resolve()
|
|
31
|
+
self._file = file
|
|
32
|
+
self._forecast_timestep = None
|
|
33
|
+
self._optimization_period = None
|
|
34
|
+
self.round_to_dates = round_to_dates
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_fixed_periods(
|
|
38
|
+
cls,
|
|
39
|
+
forecast_timestep: timedelta,
|
|
40
|
+
optimization_period: timedelta,
|
|
41
|
+
round_to_dates: bool = False,
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Create a closed loop configuration based on fixed periods.
|
|
45
|
+
|
|
46
|
+
:param forecast_timestep: The time between the start of each optimization time range.
|
|
47
|
+
:param optimization_period: the duration of each optimization time range
|
|
48
|
+
"""
|
|
49
|
+
if forecast_timestep > optimization_period:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"The forecast timestep ({forecast_timestep}) should be less than or equal to"
|
|
52
|
+
f" the optimization period ({optimization_period})."
|
|
53
|
+
)
|
|
54
|
+
config = cls()
|
|
55
|
+
config._forecast_timestep = forecast_timestep
|
|
56
|
+
config._optimization_period = optimization_period
|
|
57
|
+
config.round_to_dates = round_to_dates
|
|
58
|
+
return config
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def file(self):
|
|
62
|
+
"""Get the file that defines the closed-loop periods."""
|
|
63
|
+
return self._file
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def forecast_timestep(self):
|
|
67
|
+
"""Get the forecast timestep of the closed-loop periods."""
|
|
68
|
+
return self._forecast_timestep
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def optimization_period(self):
|
|
72
|
+
"""Get the optimization period of the closed-loop periods."""
|
|
73
|
+
return self._optimization_period
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Module for calculating optimization periods."""
|
|
2
|
+
import bisect
|
|
3
|
+
import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_optimization_ranges_from_file(
|
|
10
|
+
file_path: Path, model_time_range: tuple[datetime.datetime, datetime.datetime]
|
|
11
|
+
):
|
|
12
|
+
"""Read horizon config from a csv file"""
|
|
13
|
+
if not file_path.exists():
|
|
14
|
+
raise FileNotFoundError(
|
|
15
|
+
f"The closed_loop_dates csv does not exist. Please create a horizon config file in {file_path}."
|
|
16
|
+
)
|
|
17
|
+
try:
|
|
18
|
+
closed_loop_dates = pd.read_csv(file_path)
|
|
19
|
+
except pd.errors.EmptyDataError:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
"The closed_loop_dates csv is empty. Please provide a valid file with start_date and end_date column."
|
|
22
|
+
)
|
|
23
|
+
closed_loop_dates.columns = closed_loop_dates.columns.str.replace(" ", "")
|
|
24
|
+
if not all([col in closed_loop_dates.columns for col in ["start_date", "end_date"]]):
|
|
25
|
+
raise ValueError("The closed_loop_dates csv should have both 'start_date' and 'end_date' columns.")
|
|
26
|
+
closed_loop_dates["start_date"] = pd.to_datetime(closed_loop_dates["start_date"])
|
|
27
|
+
closed_loop_dates["end_date"] = pd.to_datetime(closed_loop_dates["end_date"])
|
|
28
|
+
for i in range(1, len(closed_loop_dates)):
|
|
29
|
+
if closed_loop_dates["start_date"].iloc[i] > closed_loop_dates["end_date"].iloc[i - 1]:
|
|
30
|
+
raise ValueError(f"Closed loop date table: Start date at row {i} is later than the previous end date. ")
|
|
31
|
+
if any(closed_loop_dates["start_date"] < closed_loop_dates["start_date"].shift(1)):
|
|
32
|
+
raise ValueError("Closed loop date table: The start dates are not in ascending order.")
|
|
33
|
+
if any(closed_loop_dates["end_date"] < closed_loop_dates["end_date"].shift(1)):
|
|
34
|
+
raise ValueError("Closed loop date table: The end dates are not in ascending order.")
|
|
35
|
+
if any(closed_loop_dates["end_date"] < closed_loop_dates["start_date"]):
|
|
36
|
+
raise ValueError("Closed loop date table: For one or more rows the end date is before the start date.")
|
|
37
|
+
if any(closed_loop_dates["start_date"] > closed_loop_dates["end_date"]):
|
|
38
|
+
raise ValueError("Closed loop date table: For one or more rows the start date is after the end date.")
|
|
39
|
+
if (
|
|
40
|
+
any(closed_loop_dates["start_date"].dt.hour != 0)
|
|
41
|
+
or any(closed_loop_dates["start_date"].dt.minute != 0)
|
|
42
|
+
or any(closed_loop_dates["end_date"].dt.hour != 0)
|
|
43
|
+
or any(closed_loop_dates["end_date"].dt.minute != 0)
|
|
44
|
+
):
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"Closed loop date table: Currently, the date ranges can only be specific up to the level of days."
|
|
47
|
+
)
|
|
48
|
+
assert (
|
|
49
|
+
min(closed_loop_dates["start_date"]).date() == model_time_range[0].date()
|
|
50
|
+
), (
|
|
51
|
+
"The start day of the first optimization run is not equal"
|
|
52
|
+
" to the start day of the timeseries import."
|
|
53
|
+
)
|
|
54
|
+
assert (
|
|
55
|
+
max(closed_loop_dates["end_date"]).date() <= model_time_range[1].date()
|
|
56
|
+
), (
|
|
57
|
+
"The end date of one or more optimization runs is later"
|
|
58
|
+
" than the end date of the timeseries import."
|
|
59
|
+
)
|
|
60
|
+
closed_loop_dates = [
|
|
61
|
+
(optimization_range["start_date"], optimization_range["end_date"])
|
|
62
|
+
for _, optimization_range in closed_loop_dates.iterrows()
|
|
63
|
+
]
|
|
64
|
+
return closed_loop_dates
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_next_time_index(
|
|
68
|
+
times: list[datetime.date],
|
|
69
|
+
i_current: int,
|
|
70
|
+
timestep_size: datetime.timedelta
|
|
71
|
+
) -> int:
|
|
72
|
+
"""
|
|
73
|
+
Get the next timestep index.
|
|
74
|
+
|
|
75
|
+
The next timestep index i_next is the highest index such that:
|
|
76
|
+
* times[i_next] > times[i_current]
|
|
77
|
+
* times[i_next] <= times[i_current] + timestep_size
|
|
78
|
+
In case i_current >= i_max, i_next will be set to i_max, where i_max = len(times) - 1.
|
|
79
|
+
"""
|
|
80
|
+
i_max = len(times) - 1
|
|
81
|
+
if i_current >= i_max:
|
|
82
|
+
return i_max
|
|
83
|
+
current_time = times[i_current]
|
|
84
|
+
next_time = current_time + timestep_size
|
|
85
|
+
i_next = bisect.bisect_right(times, next_time) - 1
|
|
86
|
+
i_next = max(i_current + 1, i_next)
|
|
87
|
+
return i_next
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_optimization_ranges(
|
|
91
|
+
model_times: list[datetime.date],
|
|
92
|
+
start_time: datetime.datetime,
|
|
93
|
+
forecast_timestep: datetime.timedelta,
|
|
94
|
+
optimization_period: datetime.timedelta
|
|
95
|
+
) -> list[tuple[datetime.datetime, datetime.datetime]]:
|
|
96
|
+
"""Calculate a list of optimization periods."""
|
|
97
|
+
if forecast_timestep > optimization_period:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Forecast timestep {forecast_timestep} cannot be larger than"
|
|
100
|
+
f" the optimization period {optimization_period}."
|
|
101
|
+
)
|
|
102
|
+
if start_time not in model_times:
|
|
103
|
+
raise ValueError(f"Start time {start_time} is not in the given model times.")
|
|
104
|
+
i_max = len(model_times) - 1
|
|
105
|
+
i_start = model_times.index(start_time)
|
|
106
|
+
i_end = _get_next_time_index(model_times, i_start, optimization_period)
|
|
107
|
+
optimization_periods = []
|
|
108
|
+
optimization_periods.append((model_times[i_start], model_times[i_end]))
|
|
109
|
+
while i_end < i_max:
|
|
110
|
+
i_start = _get_next_time_index(model_times, i_start, forecast_timestep)
|
|
111
|
+
i_end = _get_next_time_index(model_times, i_start, optimization_period)
|
|
112
|
+
optimization_periods.append((model_times[i_start], model_times[i_end]))
|
|
113
|
+
return optimization_periods
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def round_datetime_ranges_to_days(
|
|
117
|
+
datetime_ranges: list[tuple[datetime.datetime, datetime.datetime]]
|
|
118
|
+
) -> list[tuple[datetime.datetime, datetime.datetime]]:
|
|
119
|
+
"""Round datetimes to dats in datetime ranges.
|
|
120
|
+
|
|
121
|
+
The start of the range is rounded to the start of the day
|
|
122
|
+
and the end of the range is rounded to the end of the day.
|
|
123
|
+
"""
|
|
124
|
+
rounded_ranges = []
|
|
125
|
+
for start, end in datetime_ranges:
|
|
126
|
+
if start.date() == end.date():
|
|
127
|
+
continue
|
|
128
|
+
start = datetime.datetime.combine(start.date(), datetime.time.min)
|
|
129
|
+
end = datetime.datetime.combine(end.date(), datetime.time.max)
|
|
130
|
+
rounded_ranges.append((start, end))
|
|
131
|
+
return rounded_ranges
|
|
@@ -10,6 +10,18 @@ from rtctools.data import pi
|
|
|
10
10
|
logger = logging.getLogger("rtctools")
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def _get_variables_from_pi(data_config: rtc.DataConfig, timeseries: pi.Timeseries):
|
|
14
|
+
"""Get all variables of a PI timeseries that are in the data configuration."""
|
|
15
|
+
variables = []
|
|
16
|
+
for var, _ in timeseries.items():
|
|
17
|
+
try:
|
|
18
|
+
data_config.pi_variable_ids(var)
|
|
19
|
+
variables.append(var)
|
|
20
|
+
except KeyError:
|
|
21
|
+
pass
|
|
22
|
+
return variables
|
|
23
|
+
|
|
24
|
+
|
|
13
25
|
def combine_xml_exports(output_base_path: Path, original_input_timeseries_path: Path, write_csv_out: bool = False):
|
|
14
26
|
"""Combine the xml exports of multiple periods into a single xml file."""
|
|
15
27
|
logger.info("Combining XML exports.")
|
|
@@ -24,9 +36,10 @@ def combine_xml_exports(output_base_path: Path, original_input_timeseries_path:
|
|
|
24
36
|
ts_export = pi.Timeseries(
|
|
25
37
|
data_config=dataconfig, folder=output_base_path / "period_0", basename="timeseries_export", binary=False
|
|
26
38
|
) # Use the first timeseries export as a starting point for the combined timeseries export.
|
|
27
|
-
ts_export._Timeseries__path_xml = os.path.join(output_base_path.parent, "timeseries_export.xml")
|
|
28
39
|
ts_export.resize(orig_start_datetime, orig_end_datetime)
|
|
29
40
|
|
|
41
|
+
variables = _get_variables_from_pi(data_config=dataconfig, timeseries=ts_export)
|
|
42
|
+
|
|
30
43
|
i = 0
|
|
31
44
|
while os.path.isfile(os.path.join(output_base_path, f"period_{i}", "timeseries_export.xml")):
|
|
32
45
|
ts_export_step = pi.Timeseries(
|
|
@@ -38,7 +51,7 @@ def combine_xml_exports(output_base_path: Path, original_input_timeseries_path:
|
|
|
38
51
|
all_times = ts_import_orig.times # Workaround to map indices to times, as ts_export does
|
|
39
52
|
# not contain all times. TODO Check whether the assumption that these times map to
|
|
40
53
|
# the correct indices for ts_export always holds.
|
|
41
|
-
for loc_par in
|
|
54
|
+
for loc_par in variables:
|
|
42
55
|
try:
|
|
43
56
|
current_values = ts_export.get(loc_par)
|
|
44
57
|
new_values = ts_export_step.get(loc_par)
|
|
@@ -60,12 +73,12 @@ def combine_xml_exports(output_base_path: Path, original_input_timeseries_path:
|
|
|
60
73
|
combined_values[start_new_data_index : start_new_data_index + len(new_values)] = new_values # noqa
|
|
61
74
|
ts_export.set(loc_par, combined_values)
|
|
62
75
|
i += 1
|
|
63
|
-
ts_export.write()
|
|
76
|
+
ts_export.write(output_folder=output_base_path.parent, output_filename="timeseries_export")
|
|
64
77
|
|
|
65
78
|
if write_csv_out:
|
|
66
79
|
data = pd.DataFrame()
|
|
67
80
|
data["date"] = all_times
|
|
68
|
-
for timeseries_id in
|
|
81
|
+
for timeseries_id in variables:
|
|
69
82
|
try:
|
|
70
83
|
values = ts_export.get(timeseries_id)
|
|
71
84
|
except KeyError:
|
{rtc-tools-interface-0.8.0b4 → rtc-tools-interface-0.9.0a2}/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):
|
|
@@ -91,10 +115,10 @@ def run_optimization_problem_closed_loop(
|
|
|
91
115
|
if not any((isinstance(h, DiagHandler) for h in logger.handlers)):
|
|
92
116
|
diag_handler = DiagHandler(original_output_folder)
|
|
93
117
|
logger.addHandler(diag_handler)
|
|
118
|
+
logger.setLevel(log_level)
|
|
94
119
|
|
|
95
120
|
if issubclass(optimization_problem_class, PIMixin):
|
|
96
121
|
original_import = XMLTimeSeriesFile(original_input_folder)
|
|
97
|
-
original_import.set_reference_data(copy.deepcopy(original_import))
|
|
98
122
|
elif issubclass(optimization_problem_class, CSVMixin):
|
|
99
123
|
original_import = CSVTimeSeriesFile(original_input_folder)
|
|
100
124
|
else:
|
|
@@ -112,24 +136,15 @@ def run_optimization_problem_closed_loop(
|
|
|
112
136
|
shutil.rmtree(modelling_periods_output_folder)
|
|
113
137
|
modelling_periods_output_folder.mkdir(exist_ok=True)
|
|
114
138
|
|
|
115
|
-
|
|
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
|
|
|
@@ -139,6 +154,7 @@ def run_optimization_problem_closed_loop(
|
|
|
139
154
|
modelling_period_input_folder_i = modelling_period_input_folder / modelling_period_name
|
|
140
155
|
write_input_folder(modelling_period_input_folder_i, original_input_folder, timeseries_import)
|
|
141
156
|
|
|
157
|
+
logger.info(f"Run optimization for period {i}: {(str(start_time), str(end_time))}.")
|
|
142
158
|
result = run_optimization_problem(
|
|
143
159
|
optimization_problem_class,
|
|
144
160
|
base_folder,
|
|
@@ -148,7 +164,7 @@ def run_optimization_problem_closed_loop(
|
|
|
148
164
|
output_folder=modelling_period_output_folder_i,
|
|
149
165
|
**kwargs,
|
|
150
166
|
)
|
|
151
|
-
logger.info(f"Finished optimization
|
|
167
|
+
logger.info(f"Finished optimization for period {i}: {(str(start_time), str(end_time))}.")
|
|
152
168
|
|
|
153
169
|
results_previous_run = {key: result.extract_results().get(key) for key in variables_in_import}
|
|
154
170
|
previous_run_datetimes = result.io.datetimes
|