rtc-tools-interface 0.9.0a2__tar.gz → 0.10.1__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 (58) hide show
  1. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/PKG-INFO +1 -1
  2. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/README.md +12 -0
  3. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtc_tools_interface.egg-info/PKG-INFO +1 -1
  4. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtc_tools_interface.egg-info/requires.txt +1 -0
  5. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/_version.py +3 -3
  6. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/closed_loop/config.py +4 -3
  7. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/closed_loop/optimization_ranges.py +1 -1
  8. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/closed_loop/results_construction.py +12 -4
  9. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/closed_loop/runner.py +35 -19
  10. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/closed_loop/time_series_handler.py +7 -0
  11. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/optimization/base_goal.py +1 -0
  12. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/simulation/plot_mixin.py +10 -0
  13. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/setup.py +1 -0
  14. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/closed_loop/test_run_optization_problem_closed_loop.py +81 -30
  15. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/COPYING.LESSER +0 -0
  16. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtc_tools_interface.egg-info/SOURCES.txt +0 -0
  17. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtc_tools_interface.egg-info/dependency_links.txt +0 -0
  18. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtc_tools_interface.egg-info/top_level.txt +0 -0
  19. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/__init__.py +0 -0
  20. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/closed_loop/__init__.py +0 -0
  21. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/optimization/__init__.py +0 -0
  22. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/optimization/base_optimization_problem.py +0 -0
  23. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/optimization/goal_generator_mixin.py +0 -0
  24. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/optimization/goal_performance_metrics.py +0 -0
  25. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/optimization/goal_table_schema.py +0 -0
  26. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/optimization/helpers/__init__.py +0 -0
  27. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/optimization/helpers/statistics_mixin.py +0 -0
  28. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/optimization/plot_goals_mixin.py +0 -0
  29. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/optimization/plot_mixin.py +0 -0
  30. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/optimization/read_goals.py +0 -0
  31. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/plotting/__init__.py +0 -0
  32. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/plotting/plot_tools.py +0 -0
  33. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/plotting/subplot_classes.py +0 -0
  34. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/simulation/__init__.py +0 -0
  35. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/simulation/base_simulation_problem.py +0 -0
  36. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/utils/__init__.py +0 -0
  37. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/utils/plot_table_schema.py +0 -0
  38. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/utils/read_goals_mixin.py +0 -0
  39. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/utils/read_plot_table.py +0 -0
  40. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/utils/results_collection.py +0 -0
  41. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/utils/serialization.py +0 -0
  42. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/rtctools_interface/utils/type_definitions.py +0 -0
  43. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/setup.cfg +0 -0
  44. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/__init__.py +0 -0
  45. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/closed_loop/__init__.py +0 -0
  46. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/closed_loop/test_optimization_ranges.py +0 -0
  47. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/closed_loop/test_read_xml.py +0 -0
  48. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/optimization/__init__.py +0 -0
  49. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/optimization/test_base_optimization_problem.py +0 -0
  50. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/optimization/test_passing_goals_directly.py +0 -0
  51. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/optimization/test_plot_goals_mixin.py +0 -0
  52. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/optimization/test_read_goals.py +0 -0
  53. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/simulation/__init__.py +0 -0
  54. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/simulation/test_base_simulation_problem.py +0 -0
  55. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/simulation/test_plot_mixin.py +0 -0
  56. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/utils/__init__.py +0 -0
  57. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/tests/utils/get_test.py +0 -0
  58. {rtc-tools-interface-0.9.0a2 → rtc-tools-interface-0.10.1}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rtc-tools-interface
3
- Version: 0.9.0a2
3
+ Version: 0.10.1
4
4
  Summary: Toolbox for user interfaces for RTC-Tools
5
5
  Home-page: UNKNOWN
6
6
  Author: Deltares
@@ -189,7 +189,19 @@ Import `ClosedLoopConfig` and `run_optimization_problem_closed_loop` with:
189
189
  from rtctools_interface.closed_loop.config import ClosedLoopConfig
190
190
  from rtctools_interface.closed_loop.runner import run_optimization_problem_closed_loop
191
191
  ```
192
+ #### Fixed inputs
193
+ Create the file `fixed_inputs.json`, in which you specify which variables in your
194
+ timeseries import are what we call 'fixed_inputs'. They are timeseries that the closed
195
+ loop runner should simply copy as they are, even if they contain only NaNs in a modelling
196
+ period.
192
197
 
198
+ The variables that are not mentioned in this list of fixed_inputs, and have only NaN's
199
+ in a modelling period, are considered being 'initial values'. The closed loop runner will set
200
+ the first timestep of each modelling period with the corresponding calculated value from the
201
+ previous modelling period.
202
+
203
+
204
+ #### Closed loop config
193
205
  A `ClosedLoopConfig` configuration can be created from a csv file or
194
206
  from a given forecast timestep (time between each time range)
195
207
  and optimization period (duration of each time range).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rtc-tools-interface
3
- Version: 0.9.0a2
3
+ Version: 0.10.1
4
4
  Summary: Toolbox for user interfaces for RTC-Tools
5
5
  Home-page: UNKNOWN
6
6
  Author: Deltares
@@ -3,4 +3,5 @@ numpy
3
3
  pandas
4
4
  plotly
5
5
  pydantic
6
+ casadi!=3.6.6
6
7
  rtc-tools>=2.7.0a3
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-08-01T10:56:38+0200",
11
+ "date": "2025-06-04T10:02:27+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "ef695c81333aa6aca2b0d62cc2b86ff937294803",
15
- "version": "0.9.0a2"
14
+ "full-revisionid": "33cba7abbd05364334aabef715ce319b398ff945",
15
+ "version": "0.10.1"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -1,6 +1,7 @@
1
1
  """Module for configuring a closed-loop optimization problem."""
2
- from pathlib import Path
3
2
  from datetime import timedelta
3
+ from pathlib import Path
4
+ from typing import Optional
4
5
 
5
6
 
6
7
  class ClosedLoopConfig():
@@ -29,8 +30,8 @@ class ClosedLoopConfig():
29
30
  if file is not None:
30
31
  file = Path(file).resolve()
31
32
  self._file = file
32
- self._forecast_timestep = None
33
- self._optimization_period = None
33
+ self._forecast_timestep: Optional[timedelta] = None
34
+ self._optimization_period: Optional[timedelta] = None
34
35
  self.round_to_dates = round_to_dates
35
36
 
36
37
  @classmethod
@@ -49,7 +49,7 @@ def get_optimization_ranges_from_file(
49
49
  min(closed_loop_dates["start_date"]).date() == model_time_range[0].date()
50
50
  ), (
51
51
  "The start day of the first optimization run is not equal"
52
- " to the start day of the timeseries import."
52
+ " to the start day of the forecast date (or first timestep)."
53
53
  )
54
54
  assert (
55
55
  max(closed_loop_dates["end_date"]).date() <= model_time_range[1].date()
@@ -28,8 +28,15 @@ def combine_xml_exports(output_base_path: Path, original_input_timeseries_path:
28
28
  dataconfig = rtc.DataConfig(folder=original_input_timeseries_path)
29
29
 
30
30
  ts_import_orig = pi.Timeseries(
31
- data_config=dataconfig, folder=original_input_timeseries_path, basename="timeseries_import", binary=False
31
+ data_config=dataconfig,
32
+ folder=original_input_timeseries_path,
33
+ basename="timeseries_import",
34
+ binary=False,
32
35
  )
36
+ 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
+ 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):]
33
40
  orig_start_datetime = ts_import_orig.start_datetime
34
41
  orig_end_datetime = ts_import_orig.end_datetime
35
42
 
@@ -76,15 +83,16 @@ def combine_xml_exports(output_base_path: Path, original_input_timeseries_path:
76
83
  ts_export.write(output_folder=output_base_path.parent, output_filename="timeseries_export")
77
84
 
78
85
  if write_csv_out:
79
- data = pd.DataFrame()
80
- data["date"] = all_times
86
+ data = pd.DataFrame({"date": all_times})
87
+ new_columns = []
81
88
  for timeseries_id in variables:
82
89
  try:
83
90
  values = ts_export.get(timeseries_id)
91
+ new_columns.append(pd.Series(values, name=timeseries_id))
84
92
  except KeyError:
85
93
  logger.debug("Variable {} not found in output of model horizon: {}".format(timeseries_id, i))
86
94
  continue
87
- data[timeseries_id] = values
95
+ data = pd.concat([data] + new_columns, axis=1)
88
96
  data.round(6).to_csv(output_base_path.parent / "timeseries_export.csv", index=False)
89
97
 
90
98
 
@@ -1,10 +1,13 @@
1
+ import copy
1
2
  import datetime
3
+ import json
4
+ import logging
2
5
  import os
3
- import copy
4
- from pathlib import Path
5
6
  import shutil
6
7
  import sys
8
+ from pathlib import Path
7
9
  from typing import List, Optional
10
+
8
11
  from rtctools.data.pi import DiagHandler
9
12
  from rtctools.optimization.pi_mixin import PIMixin
10
13
  from rtctools.optimization.csv_mixin import CSVMixin
@@ -13,7 +16,6 @@ from rtctools_interface.closed_loop.config import ClosedLoopConfig
13
16
  import rtctools_interface.closed_loop.optimization_ranges as opt_ranges
14
17
  from rtctools_interface.closed_loop.results_construction import combine_csv_exports, combine_xml_exports
15
18
  from rtctools_interface.closed_loop.time_series_handler import XMLTimeSeriesFile, CSVTimeSeriesFile, TimeSeriesHandler
16
- import logging
17
19
 
18
20
  logger = logging.getLogger("rtctools")
19
21
 
@@ -61,9 +63,7 @@ def _get_optimization_ranges(
61
63
  """Return a list of optimization periods."""
62
64
  if config.file is not None:
63
65
  datetime_range = input_timeseries.get_datetime_range()
64
- optimization_ranges = opt_ranges.get_optimization_ranges_from_file(
65
- config.file, datetime_range
66
- )
66
+ optimization_ranges = opt_ranges.get_optimization_ranges_from_file(config.file, datetime_range)
67
67
  elif config.optimization_period is not None:
68
68
  datetimes = input_timeseries.get_datetimes()
69
69
  optimization_ranges = opt_ranges.get_optimization_ranges(
@@ -73,9 +73,7 @@ def _get_optimization_ranges(
73
73
  optimization_period=config.optimization_period,
74
74
  )
75
75
  else:
76
- raise ValueError(
77
- "The closed-loop configuration should have either a file or optimization_period set."
78
- )
76
+ raise ValueError("The closed-loop configuration should have either a file or optimization_period set.")
79
77
  if config.round_to_dates:
80
78
  optimization_ranges = opt_ranges.round_datetime_ranges_to_days(optimization_ranges)
81
79
  return optimization_ranges
@@ -86,9 +84,10 @@ def run_optimization_problem_closed_loop(
86
84
  base_folder="..",
87
85
  log_level=logging.INFO,
88
86
  profile=False,
89
- config: ClosedLoopConfig = None,
87
+ config: Optional[ClosedLoopConfig] = None,
88
+ modelling_period_input_folder: Optional[str] = None,
90
89
  **kwargs,
91
- ):
90
+ ) -> dict:
92
91
  """
93
92
  Runs an optimization problem in closed loop mode.
94
93
 
@@ -126,7 +125,19 @@ def run_optimization_problem_closed_loop(
126
125
 
127
126
  variables_in_import = original_import.get_all_internal_ids()
128
127
 
129
- modelling_period_input_folder = base_folder / "input_modelling_periods"
128
+ fixed_input_config_file = (original_input_folder / "fixed_inputs.json").resolve()
129
+ if not fixed_input_config_file.exists():
130
+ raise FileNotFoundError(
131
+ 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)."
133
+ )
134
+ with open(fixed_input_config_file, "r") as file:
135
+ 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):
137
+ raise ValueError("Fixed input config file should be a list of strings (or an empty list).")
138
+
139
+ if modelling_period_input_folder is None:
140
+ modelling_period_input_folder = base_folder / "input_modelling_periods"
130
141
  if modelling_period_input_folder.exists():
131
142
  shutil.rmtree(modelling_period_input_folder)
132
143
  modelling_period_input_folder.mkdir(exist_ok=True)
@@ -154,7 +165,7 @@ def run_optimization_problem_closed_loop(
154
165
  modelling_period_input_folder_i = modelling_period_input_folder / modelling_period_name
155
166
  write_input_folder(modelling_period_input_folder_i, original_input_folder, timeseries_import)
156
167
 
157
- logger.info(f"Run optimization for period {i}: {(str(start_time), str(end_time))}.")
168
+ logger.info(f"Running optimization for period {i}: {(str(start_time), str(end_time))}.")
158
169
  result = run_optimization_problem(
159
170
  optimization_problem_class,
160
171
  base_folder,
@@ -164,14 +175,18 @@ def run_optimization_problem_closed_loop(
164
175
  output_folder=modelling_period_output_folder_i,
165
176
  **kwargs,
166
177
  )
167
- logger.info(f"Finished optimization for period {i}: {(str(start_time), str(end_time))}.")
178
+ period = f"period {i} {(str(start_time), str(end_time))}"
179
+ if result.solver_stats["success"]:
180
+ logger.info(f"Successful optimization for {period}.")
181
+ else:
182
+ message = f"Failed optimization for {period} with status '{result.solver_stats['return_status']}'."
183
+ logger.error(message)
184
+ raise Exception(message)
168
185
 
169
- results_previous_run = {key: result.extract_results().get(key) for key in variables_in_import}
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
+ }
170
189
  previous_run_datetimes = result.io.datetimes
171
- if len(results_previous_run) != len(variables_in_import):
172
- logger.warning("Could not find the results for all input variables.")
173
- logger.warning("Missing variables: " + str(set(variables_in_import) - set(results_previous_run.keys())))
174
- raise ValueError("Could not find the results for all input variables.")
175
190
 
176
191
  logger.info("Finished all optimization runs.")
177
192
  if issubclass(optimization_problem_class, PIMixin):
@@ -182,3 +197,4 @@ def run_optimization_problem_closed_loop(
182
197
  logger.warning(
183
198
  "Could not combine exports because the optimization problem class is not derived from PIMixin or CSVMixin."
184
199
  )
200
+ return result.solver_stats
@@ -170,6 +170,11 @@ class XMLTimeSeriesFile(TimeSeriesHandler):
170
170
  self.pi_timeseries = None
171
171
  self.read(timeseries_import_basename)
172
172
 
173
+ if self.get_datetime_range()[0] < self.forecast_date:
174
+ logger.warning("Currently, the closed loop runner does support data before the forecast date.")
175
+ logger.warning("Removing data before forecast date.")
176
+ self.select_time_range(self.forecast_date, self.pi_timeseries.times[-1])
177
+
173
178
  def read(self, file_name: str):
174
179
  """Read the timeseries data from a file."""
175
180
  timeseries_import_basename = file_name
@@ -215,6 +220,8 @@ class XMLTimeSeriesFile(TimeSeriesHandler):
215
220
  self.forecast_date = times[i_start]
216
221
 
217
222
  def write(self, file_path: Path, file_name: str = "timeseries_import"):
223
+ # By setting make_new_file headers will be recreated, neceesary for writing new forecast date
224
+ self.pi_timeseries.make_new_file = True
218
225
  self.pi_timeseries.write(output_folder=file_path, output_filename=file_name)
219
226
 
220
227
  def get_datetimes(self):
@@ -33,6 +33,7 @@ class BaseGoal(Goal):
33
33
  self,
34
34
  optimization_problem: OptimizationProblem,
35
35
  state,
36
+ *,
36
37
  goal_type="minimization_path",
37
38
  function_min=np.nan,
38
39
  function_max=np.nan,
@@ -39,6 +39,16 @@ class PlotMixin(PlottingBaseMixin):
39
39
  """Tasks after optimizing."""
40
40
  super().post()
41
41
 
42
+ # find empty arrays in self._manual_extracted_states
43
+ # for these variables try to use self.get_timeseries(variable)
44
+ for variable in self.custom_variables:
45
+ if not self._manual_extracted_states[variable] or len(self._manual_extracted_states[variable]) == 0:
46
+ logger.debug(f"Variable {variable} has empty data collected.")
47
+ try:
48
+ self._manual_extracted_states[variable] = self.get_timeseries(variable)
49
+ except KeyError:
50
+ logger.warning(f"Variable {variable} not found in output of model.")
51
+
42
52
  timeseries_data = self.collect_timeseries_data(self.custom_variables)
43
53
  self._intermediate_results.append({"timeseries_data": timeseries_data, "priority": 0})
44
54
  current_run = self.create_plot_data_and_config([])
@@ -16,6 +16,7 @@ setup(
16
16
  "pandas",
17
17
  "plotly",
18
18
  "pydantic",
19
+ "casadi != 3.6.6",
19
20
  "rtc-tools >= 2.7.0a3",
20
21
  ],
21
22
  tests_require=["pytest", "pytest-runner"],
@@ -20,7 +20,7 @@ R_TOL = 0.1
20
20
 
21
21
 
22
22
  def compare_xml_file(file_result: Path, file_ref: Path):
23
- """Compare two timeseries_export files elementwise."""
23
+ """Compare two timeseries_export files elementwise."""
24
24
  tree_result = ET.parse(file_result)
25
25
  tree_ref = ET.parse(file_ref)
26
26
  series_result = tree_result.findall("pi:series", ns)
@@ -142,39 +142,90 @@ class TestClosedLoop(TestCase):
142
142
  """
143
143
  Check if test model runs without problems and generates same results.
144
144
  """
145
+ test_cases = [
146
+ {
147
+ "description": "without forecast date",
148
+ "input_folder": "input",
149
+ },
150
+ {
151
+ "description": "with forecast date unequal to first date",
152
+ "input_folder": "input_with_forecast_date",
153
+ },
154
+ {
155
+ "description": "with forecast date equal to first date",
156
+ "input_folder": "input_with_forecast_date_equal_first_date",
157
+ }
158
+ ]
159
+
145
160
  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
- )
161
+
162
+ for case in test_cases:
163
+ with self.subTest(case["description"]):
164
+ config = ClosedLoopConfig(
165
+ file=base_folder / case["input_folder"] / "closed_loop_dates.csv",
166
+ round_to_dates=True
167
+ )
168
+ run_optimization_problem_closed_loop(
169
+ ExampleXml,
170
+ base_folder=base_folder,
171
+ config=config,
172
+ input_folder=case["input_folder"]
173
+ )
174
+
175
+ self.compare_xml_files(
176
+ output_folder=base_folder / "output" / "output_modelling_periods",
177
+ reference_folder=base_folder / "output" / "output_modelling_periods_reference",
178
+ n_periods=3
179
+ )
180
+ self.compare_xml_file(
181
+ output_file=base_folder / "output" / "timeseries_export.xml",
182
+ reference_file=base_folder / "output" / "timeseries_export_reference.xml"
183
+ )
160
184
 
161
185
  def test_running_closed_loop_xml_fixed_periods(self):
162
186
  """
163
187
  Check if test model runs for fixed optimization periods.
164
188
  """
189
+ test_cases = [
190
+ {
191
+ "description": "without forecast date",
192
+ "output_folder": "output_fixed_periods",
193
+ "input_folder": "input",
194
+ "optimization_period": timedelta(days=3),
195
+ "forecast_timestep": timedelta(days=2)
196
+ },
197
+ {
198
+ "description": "with forecast date unequal to first date",
199
+ "output_folder": "output_fixed_periods",
200
+ "input_folder": "input_with_forecast_date",
201
+ "optimization_period": timedelta(days=3),
202
+ "forecast_timestep": timedelta(days=2)
203
+ },
204
+ {
205
+ "description": "with forecast date equal to first date",
206
+ "output_folder": "output_fixed_periods",
207
+ "input_folder": "input_with_forecast_date_equal_first_date",
208
+ "optimization_period": timedelta(days=3),
209
+ "forecast_timestep": timedelta(days=2)
210
+ }
211
+ ]
212
+
165
213
  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
- )
214
+
215
+ for case in test_cases:
216
+ with self.subTest(case["description"]):
217
+ config = ClosedLoopConfig.from_fixed_periods(
218
+ optimization_period=case["optimization_period"],
219
+ forecast_timestep=case["forecast_timestep"]
220
+ )
221
+ run_optimization_problem_closed_loop(
222
+ ExampleXml,
223
+ base_folder=base_folder,
224
+ config=config,
225
+ output_folder=case["output_folder"],
226
+ input_folder=case["input_folder"]
227
+ )
228
+ self.compare_xml_file(
229
+ output_file=base_folder / case["output_folder"] / "timeseries_export.xml",
230
+ reference_file=base_folder / "output" / "timeseries_export_reference.xml"
231
+ )