rtc-tools-interface 0.10.2a2__tar.gz → 0.10.2a4__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 (67) hide show
  1. rtc_tools_interface-0.10.2a4/PKG-INFO +21 -0
  2. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/README.md +34 -7
  3. rtc_tools_interface-0.10.2a4/rtc_tools_interface.egg-info/PKG-INFO +21 -0
  4. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtc_tools_interface.egg-info/SOURCES.txt +6 -0
  5. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/_version.py +3 -3
  6. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/closed_loop/config.py +4 -4
  7. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/closed_loop/optimization_ranges.py +25 -19
  8. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/closed_loop/results_construction.py +42 -25
  9. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/closed_loop/runner.py +86 -35
  10. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/closed_loop/time_series_handler.py +24 -19
  11. rtc_tools_interface-0.10.2a4/rtctools_interface/optimization/active_constraint_helpers.py +102 -0
  12. rtc_tools_interface-0.10.2a4/rtctools_interface/optimization/active_constraint_mixin.py +302 -0
  13. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/optimization/base_goal.py +30 -12
  14. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/optimization/base_optimization_problem.py +1 -0
  15. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/optimization/goal_generator_mixin.py +18 -7
  16. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/optimization/goal_performance_metrics.py +33 -14
  17. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/optimization/goal_table_schema.py +16 -9
  18. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/optimization/helpers/statistics_mixin.py +10 -10
  19. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/optimization/plot_goals_mixin.py +4 -1
  20. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/optimization/plot_mixin.py +14 -5
  21. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/optimization/read_goals.py +31 -13
  22. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/plotting/plot_tools.py +60 -25
  23. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/plotting/subplot_classes.py +65 -31
  24. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/simulation/base_simulation_problem.py +1 -0
  25. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/simulation/plot_mixin.py +14 -7
  26. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/utils/plot_table_schema.py +8 -5
  27. rtc_tools_interface-0.10.2a4/rtctools_interface/utils/read_goals_mixin.py +55 -0
  28. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/utils/read_plot_table.py +27 -9
  29. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/utils/results_collection.py +39 -20
  30. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/utils/serialization.py +7 -2
  31. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/utils/type_definitions.py +34 -13
  32. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/setup.py +0 -1
  33. rtc_tools_interface-0.10.2a4/tests/closed_loop/test_combine_xml_exports.py +106 -0
  34. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/closed_loop/test_optimization_ranges.py +20 -18
  35. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/closed_loop/test_read_xml.py +1 -0
  36. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/closed_loop/test_run_optization_problem_closed_loop.py +31 -38
  37. rtc_tools_interface-0.10.2a4/tests/closed_loop/test_set_initial_values_from_previous_run.py +61 -0
  38. rtc_tools_interface-0.10.2a4/tests/optimization/test_active_constraint_helpers.py +87 -0
  39. rtc_tools_interface-0.10.2a4/tests/optimization/test_active_constraint_mixin.py +89 -0
  40. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/optimization/test_base_optimization_problem.py +1 -0
  41. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/optimization/test_plot_goals_mixin.py +19 -1
  42. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/optimization/test_read_goals.py +7 -1
  43. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/simulation/test_plot_mixin.py +10 -5
  44. rtc_tools_interface-0.10.2a2/PKG-INFO +0 -14
  45. rtc_tools_interface-0.10.2a2/rtc_tools_interface.egg-info/PKG-INFO +0 -14
  46. rtc_tools_interface-0.10.2a2/rtctools_interface/utils/read_goals_mixin.py +0 -31
  47. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/COPYING.LESSER +0 -0
  48. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtc_tools_interface.egg-info/dependency_links.txt +0 -0
  49. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtc_tools_interface.egg-info/requires.txt +0 -0
  50. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtc_tools_interface.egg-info/top_level.txt +0 -0
  51. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/__init__.py +0 -0
  52. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/closed_loop/__init__.py +0 -0
  53. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/optimization/__init__.py +0 -0
  54. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/optimization/helpers/__init__.py +0 -0
  55. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/plotting/__init__.py +0 -0
  56. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/simulation/__init__.py +0 -0
  57. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/rtctools_interface/utils/__init__.py +0 -0
  58. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/setup.cfg +0 -0
  59. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/__init__.py +0 -0
  60. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/closed_loop/__init__.py +0 -0
  61. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/optimization/__init__.py +0 -0
  62. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/optimization/test_passing_goals_directly.py +1 -1
  63. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/simulation/__init__.py +0 -0
  64. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/simulation/test_base_simulation_problem.py +1 -1
  65. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/utils/__init__.py +0 -0
  66. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/tests/utils/get_test.py +1 -1
  67. {rtc_tools_interface-0.10.2a2 → rtc_tools_interface-0.10.2a4}/versioneer.py +0 -0
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: rtc_tools_interface
3
+ Version: 0.10.2a4
4
+ Summary: Toolbox for user interfaces for RTC-Tools
5
+ Author: Deltares
6
+ Maintainer: Deltares
7
+ Requires-Python: >=3.9
8
+ License-File: COPYING.LESSER
9
+ Requires-Dist: matplotlib
10
+ Requires-Dist: numpy
11
+ Requires-Dist: pandas
12
+ Requires-Dist: plotly
13
+ Requires-Dist: pydantic
14
+ Requires-Dist: casadi!=3.6.6
15
+ Requires-Dist: rtc-tools>=2.7.0a3
16
+ Dynamic: author
17
+ Dynamic: license-file
18
+ Dynamic: maintainer
19
+ Dynamic: requires-dist
20
+ Dynamic: requires-python
21
+ Dynamic: summary
@@ -11,8 +11,9 @@ pip install rtc-tools-interface
11
11
  ## Table of Contents
12
12
  1. [Goal generator](#goal-generator)
13
13
  2. [Goal performance metrics](#goal-performance-metrics)
14
- 3. [Automatic plotting of results](#automatic-plotting-of-results)
15
- 4. [Closed loop runner](#closed-loop-runner)
14
+ 3. [Active constraint diagnostics](#active-constraint-diagnostics)
15
+ 4. [Automatic plotting of results](#automatic-plotting-of-results)
16
+ 5. [Closed loop runner](#closed-loop-runner)
16
17
 
17
18
  ## Goal generator
18
19
  The `goal generator` can be used to automatically add goals based on a csv file. Currently, the following goal types are supported:
@@ -91,18 +92,19 @@ by minimizing, if any, the sum of exceedances for the timesteps. For more detail
91
92
 
92
93
 
93
94
  #### range_rate_of_change
94
- The range_rate_of_change goal can be used to set a target range on ramp rate. Like the range goal, one needs to set the `target_min` and `target_max` for that. **Importantly** for the range_rate_of_change goal, the supplied values are relative to the nominal of the function. So supplying a `target_max` of `10` corresponds to the aim of having a maximum increase per timestep of 10% * `nominal`, where the nominal automatically set to `maximum rate of change`/2 or specified manually. To formulate the target of having a maximum increase and decrease by of 10% per timestep, one would set the `target_min` to `-10` and the `target_max` to `10`.
95
+ The range_rate_of_change goal can be used to set a target range on ramp rate for a model state. Like the range goal, one needs to set the `target_min` and `target_max` for that. **Importantly** for the range_rate_of_change goal, the supplied values are relative to the nominal of the function. So supplying a `target_max` of `10` corresponds to the aim of having a maximum increase per timestep of 10% * `nominal`, where the nominal automatically set to `maximum rate of change`/2 or specified manually. To formulate the target of having a maximum increase and decrease by of 10% per timestep, one would set the `target_min` to `-10` and the `target_max` to `10`.
95
96
 
96
- The equations for the range_rate_of_change goal are almost the same as for the range goal, which can be found above. The only difference is that $x_t$ is replaced by the derivative of $x$ with respect to $t$, calculated as $\frac{x_t - x_{t-1}}{\Delta t}$.
97
+ The equations for the range_rate_of_change goal are almost the same as for the range goal, which can be found above. The only difference is that $x_t$ is replaced by the derivative of $x$ with respect to $t$, calculated as $\frac{x_t - x_{t-1}}{\Delta t}$. RTC-Tools computes the derivative (the ramp rate) internally for the state variable specified in the goal table.
97
98
 
98
99
  ### Example goal table
99
100
  See the table below for an example content of the `goal_table.csv`.
100
101
 
101
102
  | id | state | active | goal_type | function_min | function_max | function_nominal | target_data_type | target_min | target_max | priority | weight | order |
102
103
  |--------|-------|--------|--------------|--------------|--------------|------------------|------------------|------------|------------|----------|--------|-------|
103
- | goal_1 | reservoir_1_waterlevel | 1 | range | 0 | 15 | 10 | value | 5.0 | 10.0 | 5 | | |
104
- | goal_2 | reservoir_2_waterlevel | 1 | range | 0 | 15 | 10 | timeseries | "target_series" | "target_series" | 10 | | |
105
- | goal_3 | electricity_cost | 1 | minimization_path | | | | | | | 20 | | |
104
+ | goal_1 | reservoir_1_waterlevel | 1 | range | 0 | 15 | 10 | value | 5.0 | 10.0 | 5 | 1 | 1 |
105
+ | goal_2 | reservoir_2_waterlevel | 1 | range | 0 | 15 | 10 | timeseries | "target_series" | "target_series" | 10 | 1 | 1 |
106
+ | goal_3 | electricity_cost | 1 | minimization_path | | | | | | | 20 | 1 | 1 |
107
+ | goal_4 | reservoir_2_volume | 1 | range_rate_of_change | -100 | 100 | 10 | value | -10 | 10 | 30 | 1 | 1 |
106
108
 
107
109
  ## Goal performance metrics
108
110
  For all goals defined with the goal generator this rtc-tools-interface module will also calculate performance metrics. By default, these performance metrics are saved to a .csv in the folder `output/perfomance_metrics`, with one csv file per goal. With the class variable `calculate_performance_metrics` this functionality can be disabled (by default it is enabled).
@@ -116,6 +118,23 @@ The calculated metrics are:
116
118
  - `mean_absolute_percentual_difference`: The mean of the absolute percentual difference per timestep over all timesteps (only for range goals).
117
119
  - `mean_absolute_difference`: The mean absolute difference per timestep of the state variable over all timesteps (only for range goals).
118
120
 
121
+ ## Active constraint diagnostics
122
+ The `ActiveConstraintMixin` writes diagnostics about active constraints after each RTC-Tools goal-programming priority. A constraint is considered active when its evaluated value hits either its lower or upper bound within `active_constraint_tolerance` (default `1e-6`).
123
+
124
+ To enable the diagnostics, import the mixin and add it before `BaseOptimizationProblem` or before RTC-Tools' `GoalProgrammingMixin` in the class inheritance order:
125
+
126
+ ```python
127
+ from rtctools_interface.optimization.active_constraint_mixin import ActiveConstraintMixin
128
+ from rtctools_interface.optimization.base_optimization_problem import BaseOptimizationProblem
129
+
130
+
131
+ class MyOptimizationProblem(ActiveConstraintMixin, BaseOptimizationProblem):
132
+ pass
133
+ ```
134
+
135
+ After optimization, the mixin creates `output/active_constraints/active_constraints_of_previous_goals.csv`.
136
+ This file contains detailed rows for active constraints that RTC-Tools created from goals optimized in previous priorities. For each priority with active previous-goal constraints, it reports the total number of previous-goal constraints, how many are active, which bound (`lower`, `upper`, or `both`) was hit, and the corresponding bound value. Path-goal constraints are reported in one row per active path goal; the `active_times` column lists the timesteps at which that path-goal constraint is active.
137
+
119
138
 
120
139
  ## Automatic plotting of results
121
140
  With the `PlotMixin` one can easily make plots of the results of rtc-tools. This functionality can be used both for optimization and simulation problems. For optimization problems, use:
@@ -255,3 +274,11 @@ Note that this happens for:
255
274
  - Currently, only the initial values of the first time step in a given time range are set.
256
275
  - The closed_loop runner only works in combination with the CSVMixin or the PIMixin.
257
276
  The CDFMixin is not supported.
277
+
278
+ ### Matplotlib in Headless Environments
279
+
280
+ For CI or servers without a display, set `MPLBACKEND=Agg` to ensure Matplotlib uses a non-interactive backend:
281
+
282
+ ```bash
283
+ export MPLBACKEND=Agg
284
+ ```
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: rtc_tools_interface
3
+ Version: 0.10.2a4
4
+ Summary: Toolbox for user interfaces for RTC-Tools
5
+ Author: Deltares
6
+ Maintainer: Deltares
7
+ Requires-Python: >=3.9
8
+ License-File: COPYING.LESSER
9
+ Requires-Dist: matplotlib
10
+ Requires-Dist: numpy
11
+ Requires-Dist: pandas
12
+ Requires-Dist: plotly
13
+ Requires-Dist: pydantic
14
+ Requires-Dist: casadi!=3.6.6
15
+ Requires-Dist: rtc-tools>=2.7.0a3
16
+ Dynamic: author
17
+ Dynamic: license-file
18
+ Dynamic: maintainer
19
+ Dynamic: requires-dist
20
+ Dynamic: requires-python
21
+ Dynamic: summary
@@ -17,6 +17,8 @@ rtctools_interface/closed_loop/results_construction.py
17
17
  rtctools_interface/closed_loop/runner.py
18
18
  rtctools_interface/closed_loop/time_series_handler.py
19
19
  rtctools_interface/optimization/__init__.py
20
+ rtctools_interface/optimization/active_constraint_helpers.py
21
+ rtctools_interface/optimization/active_constraint_mixin.py
20
22
  rtctools_interface/optimization/base_goal.py
21
23
  rtctools_interface/optimization/base_optimization_problem.py
22
24
  rtctools_interface/optimization/goal_generator_mixin.py
@@ -42,10 +44,14 @@ rtctools_interface/utils/serialization.py
42
44
  rtctools_interface/utils/type_definitions.py
43
45
  tests/__init__.py
44
46
  tests/closed_loop/__init__.py
47
+ tests/closed_loop/test_combine_xml_exports.py
45
48
  tests/closed_loop/test_optimization_ranges.py
46
49
  tests/closed_loop/test_read_xml.py
47
50
  tests/closed_loop/test_run_optization_problem_closed_loop.py
51
+ tests/closed_loop/test_set_initial_values_from_previous_run.py
48
52
  tests/optimization/__init__.py
53
+ tests/optimization/test_active_constraint_helpers.py
54
+ tests/optimization/test_active_constraint_mixin.py
49
55
  tests/optimization/test_base_optimization_problem.py
50
56
  tests/optimization/test_passing_goals_directly.py
51
57
  tests/optimization/test_plot_goals_mixin.py
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-10-30T09:45:54+0100",
11
+ "date": "2026-09-17T07:52:27+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "dcc43a518f9d2672d43280e14083500fb85225cb",
15
- "version": "0.10.2a2"
14
+ "full-revisionid": "8d59f3b94a76dba915ac4488e060b927d4820acc",
15
+ "version": "0.10.2a4"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -1,10 +1,10 @@
1
1
  """Module for configuring a closed-loop optimization problem."""
2
+
2
3
  from datetime import timedelta
3
4
  from pathlib import Path
4
- from typing import Optional
5
5
 
6
6
 
7
- class ClosedLoopConfig():
7
+ class ClosedLoopConfig:
8
8
  """Configuration of a closed-loop optimization problem."""
9
9
 
10
10
  def __init__(
@@ -30,8 +30,8 @@ class ClosedLoopConfig():
30
30
  if file is not None:
31
31
  file = Path(file).resolve()
32
32
  self._file = file
33
- self._forecast_timestep: Optional[timedelta] = None
34
- self._optimization_period: Optional[timedelta] = None
33
+ self._forecast_timestep: timedelta | None = None
34
+ self._optimization_period: timedelta | None = None
35
35
  self.round_to_dates = round_to_dates
36
36
 
37
37
  @classmethod
@@ -1,4 +1,5 @@
1
1
  """Module for calculating optimization periods."""
2
+
2
3
  import bisect
3
4
  import datetime
4
5
  from pathlib import Path
@@ -12,30 +13,40 @@ def get_optimization_ranges_from_file(
12
13
  """Read horizon config from a csv file"""
13
14
  if not file_path.exists():
14
15
  raise FileNotFoundError(
15
- f"The closed_loop_dates csv does not exist. Please create a horizon config file in {file_path}."
16
+ "The closed_loop_dates csv does not exist. Please create a horizon "
17
+ f"config file in {file_path}."
16
18
  )
17
19
  try:
18
20
  closed_loop_dates = pd.read_csv(file_path)
19
21
  except pd.errors.EmptyDataError:
20
22
  raise ValueError(
21
- "The closed_loop_dates csv is empty. Please provide a valid file with start_date and end_date column."
23
+ "The closed_loop_dates csv is empty. Please provide a valid file with "
24
+ "start_date and end_date column."
22
25
  )
23
26
  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.")
27
+ if not all(col in closed_loop_dates.columns for col in ["start_date", "end_date"]):
28
+ raise ValueError(
29
+ "The closed_loop_dates csv should have both 'start_date' and 'end_date' columns."
30
+ )
26
31
  closed_loop_dates["start_date"] = pd.to_datetime(closed_loop_dates["start_date"])
27
32
  closed_loop_dates["end_date"] = pd.to_datetime(closed_loop_dates["end_date"])
28
33
  for i in range(1, len(closed_loop_dates)):
29
34
  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. ")
35
+ raise ValueError(
36
+ f"Closed loop date table: Start date at row {i} is later than previous end date."
37
+ )
31
38
  if any(closed_loop_dates["start_date"] < closed_loop_dates["start_date"].shift(1)):
32
39
  raise ValueError("Closed loop date table: The start dates are not in ascending order.")
33
40
  if any(closed_loop_dates["end_date"] < closed_loop_dates["end_date"].shift(1)):
34
41
  raise ValueError("Closed loop date table: The end dates are not in ascending order.")
35
42
  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.")
43
+ raise ValueError(
44
+ "Closed loop date table: For one or more rows the end date is before the start date."
45
+ )
37
46
  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.")
47
+ raise ValueError(
48
+ "Closed loop date table: For one or more rows the start date is after the end date."
49
+ )
39
50
  if (
40
51
  any(closed_loop_dates["start_date"].dt.hour != 0)
41
52
  or any(closed_loop_dates["start_date"].dt.minute != 0)
@@ -43,17 +54,14 @@ def get_optimization_ranges_from_file(
43
54
  or any(closed_loop_dates["end_date"].dt.minute != 0)
44
55
  ):
45
56
  raise ValueError(
46
- "Closed loop date table: Currently, the date ranges can only be specific up to the level of days."
57
+ "Closed loop date table: Currently, the date ranges can only be specific "
58
+ "up to the level of days."
47
59
  )
48
- assert (
49
- min(closed_loop_dates["start_date"]).date() == model_time_range[0].date()
50
- ), (
60
+ assert min(closed_loop_dates["start_date"]).date() == model_time_range[0].date(), (
51
61
  "The start day of the first optimization run is not equal"
52
62
  " to the start day of the forecast date (or first timestep)."
53
63
  )
54
- assert (
55
- max(closed_loop_dates["end_date"]).date() <= model_time_range[1].date()
56
- ), (
64
+ assert max(closed_loop_dates["end_date"]).date() <= model_time_range[1].date(), (
57
65
  "The end date of one or more optimization runs is later"
58
66
  " than the end date of the timeseries import."
59
67
  )
@@ -65,9 +73,7 @@ def get_optimization_ranges_from_file(
65
73
 
66
74
 
67
75
  def _get_next_time_index(
68
- times: list[datetime.date],
69
- i_current: int,
70
- timestep_size: datetime.timedelta
76
+ times: list[datetime.date], i_current: int, timestep_size: datetime.timedelta
71
77
  ) -> int:
72
78
  """
73
79
  Get the next timestep index.
@@ -91,7 +97,7 @@ def get_optimization_ranges(
91
97
  model_times: list[datetime.date],
92
98
  start_time: datetime.datetime,
93
99
  forecast_timestep: datetime.timedelta,
94
- optimization_period: datetime.timedelta
100
+ optimization_period: datetime.timedelta,
95
101
  ) -> list[tuple[datetime.datetime, datetime.datetime]]:
96
102
  """Calculate a list of optimization periods."""
97
103
  if forecast_timestep > optimization_period:
@@ -114,7 +120,7 @@ def get_optimization_ranges(
114
120
 
115
121
 
116
122
  def round_datetime_ranges_to_days(
117
- datetime_ranges: list[tuple[datetime.datetime, datetime.datetime]]
123
+ datetime_ranges: list[tuple[datetime.datetime, datetime.datetime]],
118
124
  ) -> list[tuple[datetime.datetime, datetime.datetime]]:
119
125
  """Round datetimes to dats in datetime ranges.
120
126
 
@@ -2,10 +2,9 @@ import copy
2
2
  import logging
3
3
  import os
4
4
  from pathlib import Path
5
- from typing import List
5
+
6
6
  import pandas as pd
7
- from rtctools.data import rtc
8
- from rtctools.data import pi
7
+ from rtctools.data import pi, rtc
9
8
 
10
9
  logger = logging.getLogger("rtctools")
11
10
 
@@ -22,7 +21,9 @@ def _get_variables_from_pi(data_config: rtc.DataConfig, timeseries: pi.Timeserie
22
21
  return variables
23
22
 
24
23
 
25
- def combine_xml_exports(output_base_path: Path, original_input_timeseries_path: Path, write_csv_out: bool = False):
24
+ def combine_xml_exports(
25
+ output_base_path: Path, original_input_timeseries_path: Path, write_csv_out: bool = False
26
+ ):
26
27
  """Combine the xml exports of multiple periods into a single xml file."""
27
28
  logger.info("Combining XML exports.")
28
29
  dataconfig = rtc.DataConfig(folder=original_input_timeseries_path)
@@ -34,14 +35,22 @@ def combine_xml_exports(output_base_path: Path, original_input_timeseries_path:
34
35
  binary=False,
35
36
  )
36
37
  if ts_import_orig.forecast_datetime > ts_import_orig.start_datetime:
37
- logger.info("Timeseries export will start at original forecast date, disregarding data before forecast date.")
38
+ logger.info(
39
+ "Timeseries export will start at original forecast date, disregarding data before"
40
+ " forecast date."
41
+ )
38
42
  ts_import_orig.resize(ts_import_orig.forecast_datetime, ts_import_orig.end_datetime)
39
- ts_import_orig.times = ts_import_orig.times[ts_import_orig.times.index(ts_import_orig.forecast_datetime):]
43
+ ts_import_orig.times = ts_import_orig.times[
44
+ ts_import_orig.times.index(ts_import_orig.forecast_datetime) :
45
+ ]
40
46
  orig_start_datetime = ts_import_orig.start_datetime
41
47
  orig_end_datetime = ts_import_orig.end_datetime
42
48
 
43
49
  ts_export = pi.Timeseries(
44
- data_config=dataconfig, folder=output_base_path / "period_0", basename="timeseries_export", binary=False
50
+ data_config=dataconfig,
51
+ folder=output_base_path / "period_0",
52
+ basename="timeseries_export",
53
+ binary=False,
45
54
  ) # Use the first timeseries export as a starting point for the combined timeseries export.
46
55
  ts_export.resize(orig_start_datetime, orig_end_datetime)
47
56
 
@@ -58,26 +67,32 @@ def combine_xml_exports(output_base_path: Path, original_input_timeseries_path:
58
67
  all_times = ts_import_orig.times # Workaround to map indices to times, as ts_export does
59
68
  # not contain all times. TODO Check whether the assumption that these times map to
60
69
  # the correct indices for ts_export always holds.
70
+ new_times = ts_export_step.times
71
+ if len(new_times) <= 1:
72
+ logger.debug(f"Skipping model horizon {i}: only a single timestep was exported.")
73
+ i += 1
74
+ continue
75
+ try:
76
+ start_new_data_index = all_times.index(new_times[1])
77
+ except ValueError:
78
+ if all_times[-1] + ts_export.dt == new_times[0]:
79
+ start_new_data_index = len(all_times)
80
+ else:
81
+ raise ValueError(
82
+ "Could not match the start data of the timeseries export file "
83
+ + "with the end of the previous."
84
+ )
61
85
  for loc_par in variables:
62
86
  try:
63
87
  current_values = ts_export.get(loc_par)
64
88
  new_values = ts_export_step.get(loc_par)
65
89
  except KeyError:
66
- logger.debug("Variable {} not found in output of model horizon: {}".format(loc_par, i))
90
+ logger.debug(f"Variable {loc_par} not found in output of model horizon: {i}")
67
91
  continue
68
- new_times = ts_export_step.times
69
- try:
70
- start_new_data_index = all_times.index(new_times[0])
71
- except ValueError:
72
- if all_times[-1] + ts_export.dt == new_times[0]:
73
- start_new_data_index = len(all_times)
74
- else:
75
- raise ValueError(
76
- "Could not match the start data of the timeseries export file "
77
- + "with the end of the previous."
78
- )
79
92
  combined_values = copy.deepcopy(current_values)
80
- combined_values[start_new_data_index : start_new_data_index + len(new_values)] = new_values # noqa
93
+ combined_values[start_new_data_index : start_new_data_index + len(new_values[1:])] = (
94
+ new_values[1:]
95
+ ) # noqa
81
96
  ts_export.set(loc_par, combined_values)
82
97
  i += 1
83
98
  ts_export.write(output_folder=output_base_path.parent, output_filename="timeseries_export")
@@ -90,16 +105,16 @@ def combine_xml_exports(output_base_path: Path, original_input_timeseries_path:
90
105
  values = ts_export.get(timeseries_id)
91
106
  new_columns.append(pd.Series(values, name=timeseries_id))
92
107
  except KeyError:
93
- logger.debug("Variable {} not found in output of model horizon: {}".format(timeseries_id, i))
108
+ logger.debug(f"Variable {timeseries_id} not found in output of model horizon: {i}")
94
109
  continue
95
110
  data = pd.concat([data] + new_columns, axis=1)
96
111
  data.round(6).to_csv(output_base_path.parent / "timeseries_export.csv", index=False)
97
112
 
98
113
 
99
- def combine_dataframes(dfs: List[pd.DataFrame], index_col: str = "time"):
114
+ def combine_dataframes(dfs: list[pd.DataFrame], index_col: str = "time"):
100
115
  """Combine multiple dataframes with the same index column.
101
- The dataframes are combined in the order they are passed, with the last dataframe taking precedence
102
- in case of overlapping indices."""
116
+ The dataframes are combined in the order they are passed, with the last dataframe taking
117
+ precedence in case of overlapping indices."""
103
118
  combined_df = pd.DataFrame()
104
119
  for df in dfs:
105
120
  df.set_index(index_col, inplace=True)
@@ -125,5 +140,7 @@ if __name__ == "__main__":
125
140
  output_base_path = closed_loop_test_folder / Path(
126
141
  r"test_models\goal_programming_xml\output\output_modelling_periods_reference"
127
142
  )
128
- original_input_timeseries_path = closed_loop_test_folder / Path(r"test_models\goal_programming_xml\input")
143
+ original_input_timeseries_path = closed_loop_test_folder / Path(
144
+ r"test_models\goal_programming_xml\input"
145
+ )
129
146
  combine_xml_exports(output_base_path, original_input_timeseries_path, True)
@@ -6,28 +6,38 @@ import os
6
6
  import shutil
7
7
  import sys
8
8
  from pathlib import Path
9
- from typing import List, Optional
10
9
 
11
10
  from rtctools.data.pi import DiagHandler
12
- from rtctools.optimization.pi_mixin import PIMixin
13
11
  from rtctools.optimization.csv_mixin import CSVMixin
14
- from rtctools.util import run_optimization_problem, _resolve_folder
15
- from rtctools_interface.closed_loop.config import ClosedLoopConfig
12
+ from rtctools.optimization.pi_mixin import PIMixin
13
+ from rtctools.util import _resolve_folder, run_optimization_problem
14
+
16
15
  import rtctools_interface.closed_loop.optimization_ranges as opt_ranges
17
- from rtctools_interface.closed_loop.results_construction import combine_csv_exports, combine_xml_exports
18
- from rtctools_interface.closed_loop.time_series_handler import XMLTimeSeriesFile, CSVTimeSeriesFile, TimeSeriesHandler
16
+ from rtctools_interface.closed_loop.config import ClosedLoopConfig
17
+ from rtctools_interface.closed_loop.results_construction import (
18
+ combine_csv_exports,
19
+ combine_xml_exports,
20
+ )
21
+ from rtctools_interface.closed_loop.time_series_handler import (
22
+ CSVTimeSeriesFile,
23
+ TimeSeriesHandler,
24
+ XMLTimeSeriesFile,
25
+ )
19
26
 
20
27
  logger = logging.getLogger("rtctools")
21
28
 
22
29
 
23
30
  def set_initial_values_from_previous_run(
24
- results_previous_run: Optional[dict],
31
+ results_previous_run: dict | None,
25
32
  timeseries: TimeSeriesHandler,
26
- previous_run_datetimes: List[datetime.datetime],
33
+ previous_run_datetimes: list[datetime.datetime],
27
34
  ) -> None:
28
- """Modifies the initial values of `timeseries` based on the results of the previous run (if any)"""
35
+ """Modifies the initial values of `timeseries` based on the results of the previous run
36
+ (if any)"""
29
37
  if results_previous_run is not None:
30
- variables_to_set = {key: value for key, value in results_previous_run.items() if not timeseries.is_set(key)}
38
+ variables_to_set = {
39
+ key: value for key, value in results_previous_run.items() if not timeseries.is_set(key)
40
+ }
31
41
  if timeseries.forecast_date:
32
42
  index_of_initial_value = previous_run_datetimes.index(timeseries.forecast_date)
33
43
  else:
@@ -63,7 +73,9 @@ def _get_optimization_ranges(
63
73
  """Return a list of optimization periods."""
64
74
  if config.file is not None:
65
75
  datetime_range = input_timeseries.get_datetime_range()
66
- optimization_ranges = opt_ranges.get_optimization_ranges_from_file(config.file, datetime_range)
76
+ optimization_ranges = opt_ranges.get_optimization_ranges_from_file(
77
+ config.file, datetime_range
78
+ )
67
79
  elif config.optimization_period is not None:
68
80
  datetimes = input_timeseries.get_datetimes()
69
81
  optimization_ranges = opt_ranges.get_optimization_ranges(
@@ -73,7 +85,9 @@ def _get_optimization_ranges(
73
85
  optimization_period=config.optimization_period,
74
86
  )
75
87
  else:
76
- raise ValueError("The closed-loop configuration should have either a file or optimization_period set.")
88
+ raise ValueError(
89
+ "The closed-loop configuration should have either a file or optimization_period set."
90
+ )
77
91
  if config.round_to_dates:
78
92
  optimization_ranges = opt_ranges.round_datetime_ranges_to_days(optimization_ranges)
79
93
  return optimization_ranges
@@ -84,8 +98,8 @@ def run_optimization_problem_closed_loop(
84
98
  base_folder="..",
85
99
  log_level=logging.INFO,
86
100
  profile=False,
87
- config: Optional[ClosedLoopConfig] = None,
88
- modelling_period_input_folder: Optional[str] = None,
101
+ config: ClosedLoopConfig | None = None,
102
+ modelling_period_input_folder: str | None = None,
89
103
  **kwargs,
90
104
  ) -> dict:
91
105
  """
@@ -111,7 +125,7 @@ def run_optimization_problem_closed_loop(
111
125
  handler.setFormatter(formatter)
112
126
  logger.addHandler(handler)
113
127
  if issubclass(optimization_problem_class, PIMixin):
114
- if not any((isinstance(h, DiagHandler) for h in logger.handlers)):
128
+ if not any(isinstance(h, DiagHandler) for h in logger.handlers):
115
129
  diag_handler = DiagHandler(original_output_folder)
116
130
  logger.addHandler(diag_handler)
117
131
  logger.setLevel(log_level)
@@ -129,11 +143,14 @@ def run_optimization_problem_closed_loop(
129
143
  if not fixed_input_config_file.exists():
130
144
  raise FileNotFoundError(
131
145
  f"Could not find fixed inputs configuration file: {fixed_input_config_file}"
132
- "Create a file with a list of strings that represent the fixed inputs (can be an empty list)."
146
+ "Create a file with a list of strings that represent the fixed inputs"
147
+ " (can be an empty list)."
133
148
  )
134
- with open(fixed_input_config_file, "r") as file:
149
+ with open(fixed_input_config_file) as file:
135
150
  fixed_input_series = json.load(file)
136
- if not isinstance(fixed_input_series, list) and all(isinstance(item, str) for item in fixed_input_series):
151
+ if not isinstance(fixed_input_series, list) and all(
152
+ isinstance(item, str) for item in fixed_input_series
153
+ ):
137
154
  raise ValueError("Fixed input config file should be a list of strings (or an empty list).")
138
155
 
139
156
  if modelling_period_input_folder is None:
@@ -157,44 +174,78 @@ def run_optimization_problem_closed_loop(
157
174
 
158
175
  timeseries_import.select_time_range(start_date=start_time, end_date=end_time)
159
176
 
160
- set_initial_values_from_previous_run(results_previous_run, timeseries_import, previous_run_datetimes)
177
+ set_initial_values_from_previous_run(
178
+ results_previous_run, timeseries_import, previous_run_datetimes
179
+ )
161
180
 
162
181
  modelling_period_name = f"period_{i}"
163
182
  modelling_period_output_folder_i = modelling_periods_output_folder / modelling_period_name
164
183
  modelling_period_output_folder_i.mkdir(exist_ok=True)
165
184
  modelling_period_input_folder_i = modelling_period_input_folder / modelling_period_name
166
- write_input_folder(modelling_period_input_folder_i, original_input_folder, timeseries_import)
185
+ write_input_folder(
186
+ modelling_period_input_folder_i, original_input_folder, timeseries_import
187
+ )
167
188
 
168
189
  logger.info(f"Running optimization for period {i}: {(str(start_time), str(end_time))}.")
169
- result = run_optimization_problem(
170
- optimization_problem_class,
171
- base_folder,
172
- log_level,
173
- profile,
174
- input_folder=modelling_period_input_folder_i,
175
- output_folder=modelling_period_output_folder_i,
176
- **kwargs,
177
- )
190
+ run_number_in_fallback_list = 1
191
+ # Fallback mechanism: allows optimization class to request a retry
192
+ # run_number_in_fallback_list: 1=first attempt, 2=retry with fallback, 3=done
193
+ for _k in range(2):
194
+ if run_number_in_fallback_list < 3:
195
+ result = run_optimization_problem(
196
+ optimization_problem_class,
197
+ base_folder,
198
+ log_level,
199
+ profile,
200
+ input_folder=modelling_period_input_folder_i,
201
+ output_folder=modelling_period_output_folder_i,
202
+ run_number_in_fallback_list=run_number_in_fallback_list,
203
+ **kwargs,
204
+ )
205
+ if hasattr(result, "run_number_in_fallback_list"):
206
+ run_number_in_fallback_list = result.run_number_in_fallback_list
207
+ else:
208
+ run_number_in_fallback_list = 3
209
+ if run_number_in_fallback_list == 2:
210
+ logger.info("The model failed, it will run it again with a fallback option")
178
211
  period = f"period {i} {(str(start_time), str(end_time))}"
179
212
  if result.solver_stats["success"]:
180
213
  logger.info(f"Successful optimization for {period}.")
181
214
  else:
182
- message = f"Failed optimization for {period} with status '{result.solver_stats['return_status']}'."
215
+ message = (
216
+ f"Failed optimization for {period} with status"
217
+ + f"{result.solver_stats['return_status']}'."
218
+ )
183
219
  logger.error(message)
184
220
  raise Exception(message)
185
221
 
186
- results_previous_run = {
187
- key: result.extract_results().get(key) for key in variables_in_import if key not in fixed_input_series
188
- }
222
+ extracted_results = result.extract_results() or {}
223
+ results_previous_run = {}
224
+ for key in variables_in_import:
225
+ if key not in fixed_input_series:
226
+ extracted_value = extracted_results.get(key)
227
+ if extracted_value is not None:
228
+ results_previous_run[key] = extracted_value
229
+ else:
230
+ try:
231
+ fallback_value = result.get_timeseries(key)
232
+ except KeyError:
233
+ fallback_value = None
234
+ results_previous_run[key] = (
235
+ fallback_value.values if fallback_value is not None else None
236
+ )
189
237
  previous_run_datetimes = result.io.datetimes
190
238
 
191
239
  logger.info("Finished all optimization runs.")
192
240
  if issubclass(optimization_problem_class, PIMixin):
193
- combine_xml_exports(modelling_periods_output_folder, original_input_folder, write_csv_out=True)
241
+ combine_xml_exports(
242
+ modelling_periods_output_folder, original_input_folder, write_csv_out=True
243
+ )
194
244
  elif issubclass(optimization_problem_class, CSVMixin):
195
245
  combine_csv_exports(modelling_periods_output_folder)
196
246
  else:
197
247
  logger.warning(
198
- "Could not combine exports because the optimization problem class is not derived from PIMixin or CSVMixin."
248
+ "Could not combine exports because the optimization problem class is not "
249
+ "derived from PIMixin or CSVMixin."
199
250
  )
200
251
  return result.solver_stats