rtc-tools-interface 0.9.0a1__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.
Files changed (59) hide show
  1. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/PKG-INFO +1 -1
  2. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtc_tools_interface.egg-info/PKG-INFO +1 -1
  3. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtc_tools_interface.egg-info/requires.txt +3 -3
  4. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/_version.py +3 -3
  5. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/closed_loop/results_construction.py +17 -4
  6. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/closed_loop/runner.py +3 -2
  7. rtc-tools-interface-0.9.0a2/rtctools_interface/closed_loop/time_series_handler.py +234 -0
  8. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/base_goal.py +1 -1
  9. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/setup.py +8 -1
  10. rtc-tools-interface-0.9.0a1/rtctools_interface/closed_loop/time_series_handler.py +0 -449
  11. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/COPYING.LESSER +0 -0
  12. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/README.md +0 -0
  13. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtc_tools_interface.egg-info/SOURCES.txt +0 -0
  14. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtc_tools_interface.egg-info/dependency_links.txt +0 -0
  15. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtc_tools_interface.egg-info/top_level.txt +0 -0
  16. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/__init__.py +0 -0
  17. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/closed_loop/__init__.py +0 -0
  18. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/closed_loop/config.py +0 -0
  19. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/closed_loop/optimization_ranges.py +0 -0
  20. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/__init__.py +0 -0
  21. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/base_optimization_problem.py +0 -0
  22. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/goal_generator_mixin.py +0 -0
  23. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/goal_performance_metrics.py +0 -0
  24. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/goal_table_schema.py +0 -0
  25. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/helpers/__init__.py +0 -0
  26. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/helpers/statistics_mixin.py +0 -0
  27. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/plot_goals_mixin.py +0 -0
  28. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/plot_mixin.py +0 -0
  29. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/optimization/read_goals.py +0 -0
  30. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/plotting/__init__.py +0 -0
  31. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/plotting/plot_tools.py +0 -0
  32. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/plotting/subplot_classes.py +0 -0
  33. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/simulation/__init__.py +0 -0
  34. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/simulation/base_simulation_problem.py +0 -0
  35. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/simulation/plot_mixin.py +0 -0
  36. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/__init__.py +0 -0
  37. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/plot_table_schema.py +0 -0
  38. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/read_goals_mixin.py +0 -0
  39. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/read_plot_table.py +0 -0
  40. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/results_collection.py +0 -0
  41. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/serialization.py +0 -0
  42. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/rtctools_interface/utils/type_definitions.py +0 -0
  43. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/setup.cfg +0 -0
  44. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/__init__.py +0 -0
  45. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/closed_loop/__init__.py +0 -0
  46. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/closed_loop/test_optimization_ranges.py +0 -0
  47. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/closed_loop/test_read_xml.py +0 -0
  48. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/closed_loop/test_run_optization_problem_closed_loop.py +0 -0
  49. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/optimization/__init__.py +0 -0
  50. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/optimization/test_base_optimization_problem.py +0 -0
  51. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/optimization/test_passing_goals_directly.py +0 -0
  52. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/optimization/test_plot_goals_mixin.py +0 -0
  53. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/optimization/test_read_goals.py +0 -0
  54. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/simulation/__init__.py +0 -0
  55. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/simulation/test_base_simulation_problem.py +0 -0
  56. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/simulation/test_plot_mixin.py +0 -0
  57. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/utils/__init__.py +0 -0
  58. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/tests/utils/get_test.py +0 -0
  59. {rtc-tools-interface-0.9.0a1 → rtc-tools-interface-0.9.0a2}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rtc-tools-interface
3
- Version: 0.9.0a1
3
+ Version: 0.9.0a2
4
4
  Summary: Toolbox for user interfaces for RTC-Tools
5
5
  Home-page: UNKNOWN
6
6
  Author: Deltares
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rtc-tools-interface
3
- Version: 0.9.0a1
3
+ Version: 0.9.0a2
4
4
  Summary: Toolbox for user interfaces for RTC-Tools
5
5
  Home-page: UNKNOWN
6
6
  Author: Deltares
@@ -1,6 +1,6 @@
1
- pandas
2
- rtc-tools>=2.5.0
3
1
  matplotlib
4
- plotly
5
2
  numpy
3
+ pandas
4
+ plotly
6
5
  pydantic
6
+ rtc-tools>=2.7.0a3
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-06-18T15:06:29+0200",
11
+ "date": "2024-08-01T10:56:38+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "478b74b0fc61a477f80531ea4e3f54f1054bc603",
15
- "version": "0.9.0a1"
14
+ "full-revisionid": "ef695c81333aa6aca2b0d62cc2b86ff937294803",
15
+ "version": "0.9.0a2"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -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 dataconfig._DataConfig__location_parameter_ids:
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 dataconfig._DataConfig__location_parameter_ids:
81
+ for timeseries_id in variables:
69
82
  try:
70
83
  values = ts_export.get(timeseries_id)
71
84
  except KeyError:
@@ -115,10 +115,10 @@ def run_optimization_problem_closed_loop(
115
115
  if not any((isinstance(h, DiagHandler) for h in logger.handlers)):
116
116
  diag_handler = DiagHandler(original_output_folder)
117
117
  logger.addHandler(diag_handler)
118
+ logger.setLevel(log_level)
118
119
 
119
120
  if issubclass(optimization_problem_class, PIMixin):
120
121
  original_import = XMLTimeSeriesFile(original_input_folder)
121
- original_import.set_reference_data(copy.deepcopy(original_import))
122
122
  elif issubclass(optimization_problem_class, CSVMixin):
123
123
  original_import = CSVTimeSeriesFile(original_input_folder)
124
124
  else:
@@ -154,6 +154,7 @@ def run_optimization_problem_closed_loop(
154
154
  modelling_period_input_folder_i = modelling_period_input_folder / modelling_period_name
155
155
  write_input_folder(modelling_period_input_folder_i, original_input_folder, timeseries_import)
156
156
 
157
+ logger.info(f"Run optimization for period {i}: {(str(start_time), str(end_time))}.")
157
158
  result = run_optimization_problem(
158
159
  optimization_problem_class,
159
160
  base_folder,
@@ -163,7 +164,7 @@ def run_optimization_problem_closed_loop(
163
164
  output_folder=modelling_period_output_folder_i,
164
165
  **kwargs,
165
166
  )
166
- logger.info(f"Finished optimization run {i} with result {result}")
167
+ logger.info(f"Finished optimization for period {i}: {(str(start_time), str(end_time))}.")
167
168
 
168
169
  results_previous_run = {key: result.extract_results().get(key) for key in variables_in_import}
169
170
  previous_run_datetimes = result.io.datetimes
@@ -0,0 +1,234 @@
1
+ import bisect
2
+ import datetime
3
+ import logging
4
+ from abc import ABC, abstractmethod
5
+ from pathlib import Path
6
+ from typing import List, Optional, Tuple
7
+
8
+ import pandas as pd
9
+ import numpy as np
10
+
11
+ from rtctools.data import pi
12
+ from rtctools.data import rtc
13
+ from rtctools.data import csv
14
+
15
+ ns = {"fews": "http://www.wldelft.nl/fews", "pi": "http://www.wldelft.nl/fews/PI"}
16
+
17
+ logger = logging.getLogger("rtctools")
18
+
19
+
20
+ class TimeSeriesHandler(ABC):
21
+ """ABC for handling timeseries data."""
22
+
23
+ # The forecast date determines at which date the optimization starts.
24
+ forecast_date: Optional[datetime.datetime] = None
25
+
26
+ @abstractmethod
27
+ def read(self, file_name: str) -> None:
28
+ """Read the timeseries."""
29
+
30
+ @abstractmethod
31
+ def select_time_range(self, start_date: datetime.datetime, end_date: datetime.datetime) -> None:
32
+ """Select a time range from the timeseries data. Removes data outside the interval.
33
+ The specified range is inclusive on both sides."""
34
+
35
+ @abstractmethod
36
+ def write(self, file_path: Path) -> None:
37
+ """Write the timeseries data to a file."""
38
+
39
+ @abstractmethod
40
+ def get_timestep(self) -> datetime.timedelta:
41
+ """Get the timestep of the timeseries data."""
42
+
43
+ @abstractmethod
44
+ def get_datetimes(self) -> List[datetime.datetime]:
45
+ """Get the dates of the timeseries."""
46
+
47
+ @abstractmethod
48
+ def get_datetime_range(self) -> Tuple[datetime.datetime, datetime.datetime]:
49
+ """Get the date range of the timeseries data (min, max)."""
50
+
51
+ @abstractmethod
52
+ def get_all_internal_ids(self) -> List[str]:
53
+ """Get all internal id's of the timeseries data."""
54
+
55
+ @abstractmethod
56
+ def set_initial_value(self, internal_id: str, value: float) -> None:
57
+ """Set the initial value of a variable in the timeseries data."""
58
+
59
+ @abstractmethod
60
+ def is_set(self, internal_id: str) -> bool:
61
+ """Check whether the variable exists in the timeseries data and whether it has a least one non-nan value"""
62
+
63
+
64
+ class CSVTimeSeriesFile(TimeSeriesHandler):
65
+ """Timeseries handler for csv files."""
66
+
67
+ def __init__(
68
+ self,
69
+ input_folder: Path,
70
+ timeseries_import_basename: str = "timeseries_import",
71
+ csv_delimiter=",",
72
+ initial_state_base_name: str = "initial_state",
73
+ ):
74
+ self.data = pd.DataFrame()
75
+ self.input_folder = input_folder
76
+ self.csv_delimiter = csv_delimiter
77
+ self.data_col = None
78
+ self.initial_state = None
79
+ self.read(timeseries_import_basename, initial_state_base_name)
80
+
81
+ def read(self, file_name: str, initial_state_base_name=None):
82
+ timeseries = csv.load(
83
+ (self.input_folder / file_name).with_suffix(".csv"),
84
+ delimiter=self.csv_delimiter,
85
+ with_time=True,
86
+ )
87
+ self.data = pd.DataFrame(timeseries)
88
+ if self.data is not None:
89
+ self.date_col = self.data.columns[0]
90
+ self.forecast_date = self.data[self.date_col].iloc[0]
91
+ else:
92
+ raise ValueError("No data to read.")
93
+ if initial_state_base_name is not None:
94
+ initial_state_file = self.input_folder / initial_state_base_name
95
+ if initial_state_file.with_suffix(".csv").exists():
96
+ initial_state = csv.load(
97
+ initial_state_file.with_suffix(".csv"),
98
+ delimiter=self.csv_delimiter,
99
+ with_time=False,
100
+ )
101
+ self.initial_state: Optional[dict] = {
102
+ field: float(initial_state[field]) for field in initial_state.dtype.names
103
+ }
104
+ else:
105
+ self.initial_state = None
106
+
107
+ def select_time_range(self, start_date: datetime.datetime, end_date: datetime.datetime):
108
+ mask = (self.data[self.date_col] >= start_date) & (self.data[self.date_col] <= end_date)
109
+ self.data = self.data.loc[mask]
110
+ self.forecast_date = start_date
111
+
112
+ def write(self, file_path: Path):
113
+ self.write_timeseries(file_path)
114
+ self.write_initial_state(file_path)
115
+
116
+ def write_timeseries(self, file_path: Path, file_name: str = "timeseries_import"):
117
+ self.data.to_csv(
118
+ (file_path / file_name).with_suffix(".csv"),
119
+ index=False,
120
+ date_format="%Y-%m-%d %H:%M:%S",
121
+ )
122
+
123
+ def write_initial_state(self, file_path: Path, file_name: str = "initial_state"):
124
+ if self.initial_state is not None:
125
+ initial_state = pd.DataFrame(self.initial_state, index=[0])
126
+ initial_state.to_csv((file_path / file_name).with_suffix(".csv"), header=True, index=False)
127
+
128
+ def get_timestep(self):
129
+ return self.data[self.date_col].diff().min()
130
+
131
+ def get_datetimes(self):
132
+ return self.data[self.date_col].to_list()
133
+
134
+ def get_datetime_range(self):
135
+ return self.data[self.date_col].min(), self.data[self.date_col].max()
136
+
137
+ def get_all_internal_ids(self):
138
+ ids = list(self.data.columns[1:])
139
+ if self.initial_state is not None:
140
+ ids.extend(list(self.initial_state.keys()))
141
+ return ids
142
+
143
+ def set_initial_value(self, internal_id, value):
144
+ if self.initial_state is None or internal_id not in self.initial_state:
145
+ self.data[internal_id].iloc[0] = value
146
+ else:
147
+ self.initial_state[internal_id] = value
148
+
149
+ def is_set(self, internal_id):
150
+ val_is_set = False
151
+ if internal_id in self.data.columns:
152
+ val_is_set = not self.data[internal_id].isna().all()
153
+ if self.initial_state is not None and internal_id in self.initial_state:
154
+ val_is_set = False
155
+ return val_is_set
156
+
157
+
158
+ class XMLTimeSeriesFile(TimeSeriesHandler):
159
+ """ "Timeseries handler for xml files"""
160
+
161
+ def __init__(
162
+ self,
163
+ input_folder: Path,
164
+ timeseries_import_basename: str = "timeseries_import",
165
+ ):
166
+ self.input_folder = input_folder
167
+ self.pi_binary_timeseries = False
168
+ self.pi_validate_timeseries = True
169
+ self.data_config = None
170
+ self.pi_timeseries = None
171
+ self.read(timeseries_import_basename)
172
+
173
+ def read(self, file_name: str):
174
+ """Read the timeseries data from a file."""
175
+ timeseries_import_basename = file_name
176
+ self.data_config = rtc.DataConfig(self.input_folder)
177
+ self.pi_timeseries = pi.Timeseries(
178
+ self.data_config,
179
+ self.input_folder,
180
+ timeseries_import_basename,
181
+ binary=self.pi_binary_timeseries,
182
+ pi_validate_times=self.pi_validate_timeseries,
183
+ )
184
+ self.forecast_date = self.pi_timeseries.forecast_datetime
185
+
186
+ def is_set(self, internal_id):
187
+ """Check whether the variable exists in the timeseries data and whether it has a value at at least
188
+ one of time steps."""
189
+ try:
190
+ var: np.ndarray = self.pi_timeseries.get(variable=internal_id)
191
+ return not np.isnan(var).all()
192
+ except KeyError:
193
+ return False
194
+
195
+ def _is_in_dataconfig(self, internal_id):
196
+ """Check if an internal id is in the data configuration."""
197
+ try:
198
+ self.data_config.pi_variable_ids(internal_id)
199
+ return True
200
+ except KeyError:
201
+ return False
202
+
203
+ def get_all_internal_ids(self):
204
+ """Get all internal id's of the timeseries data. Only returns the id's that are also in the dataconfig."""
205
+ all_ids = [var for var, _ in self.pi_timeseries.items() if self._is_in_dataconfig(var)]
206
+ return all_ids
207
+
208
+ def select_time_range(self, start_date: datetime.datetime, end_date: datetime.datetime):
209
+ times = self.pi_timeseries.times
210
+ i_start = bisect.bisect_left(times, start_date)
211
+ i_end = bisect.bisect_right(times, end_date) - 1
212
+ self.pi_timeseries.resize(start_datetime=times[i_start], end_datetime=times[i_end])
213
+ self.pi_timeseries.times = times[i_start:i_end + 1]
214
+ self.pi_timeseries.forecast_datetime = times[i_start]
215
+ self.forecast_date = times[i_start]
216
+
217
+ def write(self, file_path: Path, file_name: str = "timeseries_import"):
218
+ self.pi_timeseries.write(output_folder=file_path, output_filename=file_name)
219
+
220
+ def get_datetimes(self):
221
+ """Get the dates of all timeseries data."""
222
+ return self.pi_timeseries.times
223
+
224
+ def get_datetime_range(self):
225
+ """Get the date range of the timeseries data, minimum and maximum over all series"""
226
+ times = self.pi_timeseries.times
227
+ return times[0], times[-1]
228
+
229
+ def get_timestep(self):
230
+ """Get the timestep of the timeseries data, raise error if different stepsizes"""
231
+ return self.pi_timeseries.dt
232
+
233
+ def set_initial_value(self, internal_id: str, value: float):
234
+ self.pi_timeseries.set(internal_id, [value])
@@ -147,7 +147,7 @@ class BaseGoal(Goal):
147
147
  if not np.isfinite(self.function_nominal):
148
148
  if isinstance(self.function_range, (list, tuple)):
149
149
  if np.all(np.isfinite(self.function_range)):
150
- self.function_nominal = abs(self.function_range[0]) + abs(self.function_range[1]) / 2
150
+ self.function_nominal = (abs(self.function_range[0]) + abs(self.function_range[1])) / 2
151
151
  return
152
152
  self.function_nominal = 1.0
153
153
  logger.warning("Function nominal for goal with id '%s' not specified, nominal is set to 1.0", self.goal_id)
@@ -10,7 +10,14 @@ setup(
10
10
  packages=find_packages("."),
11
11
  author="Deltares",
12
12
  description="Toolbox for user interfaces for RTC-Tools",
13
- install_requires=["pandas", "rtc-tools >= 2.5.0", "matplotlib", "plotly", "numpy", "pydantic"],
13
+ install_requires=[
14
+ "matplotlib",
15
+ "numpy",
16
+ "pandas",
17
+ "plotly",
18
+ "pydantic",
19
+ "rtc-tools >= 2.7.0a3",
20
+ ],
14
21
  tests_require=["pytest", "pytest-runner"],
15
22
  python_requires=">=3.9",
16
23
  cmdclass=versioneer.get_cmdclass(),
@@ -1,449 +0,0 @@
1
- import copy
2
- import datetime
3
- import logging
4
- from abc import ABC, abstractmethod
5
- import xml.etree.ElementTree as ET
6
- from pathlib import Path
7
- from typing import List, Optional, Tuple
8
- import pandas as pd
9
- from rtctools.data import pi
10
- from rtctools.data import rtc
11
- from rtctools.data import csv
12
-
13
- ns = {"fews": "http://www.wldelft.nl/fews", "pi": "http://www.wldelft.nl/fews/PI"}
14
-
15
- logger = logging.getLogger("rtctools")
16
-
17
-
18
- class TimeSeriesHandler(ABC):
19
- """ABC for handling timeseries data."""
20
-
21
- # The forecast date determines at which date the optimization starts.
22
- forecast_date: Optional[datetime.datetime] = None
23
-
24
- @abstractmethod
25
- def read(self, file_name: str) -> None:
26
- """Read the timeseries."""
27
-
28
- @abstractmethod
29
- def select_time_range(self, start_date: datetime.datetime, end_date: datetime.datetime) -> None:
30
- """Select a time range from the timeseries data. Removes data outside the interval.
31
- The specified range is inclusive on both sides."""
32
-
33
- @abstractmethod
34
- def write(self, file_path: Path) -> None:
35
- """Write the timeseries data to a file."""
36
-
37
- def set_reference_data(self, reference_data: "TimeSeriesHandler"):
38
- """Set the reference TimeSeriesHandler. Only relevant for XMLTimeSeriesFile.
39
- Required when setting initial values for variables that are not in the modelling period data range."""
40
-
41
- @abstractmethod
42
- def get_timestep(self) -> datetime.timedelta:
43
- """Get the timestep of the timeseries data."""
44
-
45
- @abstractmethod
46
- def get_datetimes(self) -> List[datetime.datetime]:
47
- """Get the dates of the timeseries."""
48
-
49
- @abstractmethod
50
- def get_datetime_range(self) -> Tuple[datetime.datetime, datetime.datetime]:
51
- """Get the date range of the timeseries data (min, max)."""
52
-
53
- @abstractmethod
54
- def get_all_internal_ids(self) -> List[str]:
55
- """Get all internal id's of the timeseries data."""
56
-
57
- @abstractmethod
58
- def set_initial_value(self, internal_id: str, value: float) -> None:
59
- """Set the initial value of a variable in the timeseries data."""
60
-
61
- @abstractmethod
62
- def is_set(self, internal_id: str) -> bool:
63
- """Check whether the variable exists in the timeseries data and whether it has a least one non-nan value"""
64
-
65
-
66
- class CSVTimeSeriesFile(TimeSeriesHandler):
67
- """Timeseries handler for csv files."""
68
-
69
- def __init__(
70
- self,
71
- input_folder: Path,
72
- timeseries_import_basename: str = "timeseries_import",
73
- csv_delimiter=",",
74
- initial_state_base_name: str = "initial_state",
75
- ):
76
- self.data = pd.DataFrame()
77
- self.input_folder = input_folder
78
- self.csv_delimiter = csv_delimiter
79
- self.read(timeseries_import_basename, initial_state_base_name)
80
-
81
- def read(self, file_name: str, initial_state_base_name=None):
82
- timeseries = csv.load(
83
- (self.input_folder / file_name).with_suffix(".csv"),
84
- delimiter=self.csv_delimiter,
85
- with_time=True,
86
- )
87
- self.data = pd.DataFrame(timeseries)
88
- if self.data is not None:
89
- self.date_col = self.data.columns[0]
90
- self.forecast_date = self.data[self.date_col].iloc[0]
91
- else:
92
- raise ValueError("No data to read.")
93
- if initial_state_base_name is not None:
94
- initial_state_file = self.input_folder / initial_state_base_name
95
- if initial_state_file.with_suffix(".csv").exists():
96
- initial_state = csv.load(
97
- initial_state_file.with_suffix(".csv"),
98
- delimiter=self.csv_delimiter,
99
- with_time=False,
100
- )
101
- self.initial_state: Optional[dict] = {
102
- field: float(initial_state[field]) for field in initial_state.dtype.names
103
- }
104
- else:
105
- self.initial_state = None
106
-
107
- def select_time_range(self, start_date: datetime.datetime, end_date: datetime.datetime):
108
- mask = (self.data[self.date_col] >= start_date) & (self.data[self.date_col] <= end_date)
109
- self.data = self.data.loc[mask]
110
- self.forecast_date = start_date
111
-
112
- def write(self, file_path: Path):
113
- self.write_timeseries(file_path)
114
- self.write_initial_state(file_path)
115
-
116
- def write_timeseries(self, file_path: Path, file_name: str = "timeseries_import"):
117
- self.data.to_csv(
118
- (file_path / file_name).with_suffix(".csv"),
119
- index=False,
120
- date_format="%Y-%m-%d %H:%M:%S",
121
- )
122
-
123
- def write_initial_state(self, file_path: Path, file_name: str = "initial_state"):
124
- if self.initial_state is not None:
125
- initial_state = pd.DataFrame(self.initial_state, index=[0])
126
- initial_state.to_csv((file_path / file_name).with_suffix(".csv"), header=True, index=False)
127
-
128
- def get_timestep(self):
129
- return self.data[self.date_col].diff().min()
130
-
131
- def get_datetimes(self):
132
- return self.data[self.date_col].to_list()
133
-
134
- def get_datetime_range(self):
135
- return self.data[self.date_col].min(), self.data[self.date_col].max()
136
-
137
- def get_all_internal_ids(self):
138
- ids = list(self.data.columns[1:])
139
- if self.initial_state is not None:
140
- ids.extend(list(self.initial_state.keys()))
141
- return ids
142
-
143
- def set_initial_value(self, internal_id, value):
144
- if self.initial_state is None or internal_id not in self.initial_state:
145
- self.data[internal_id].iloc[0] = value
146
- else:
147
- self.initial_state[internal_id] = value
148
-
149
- def is_set(self, internal_id):
150
- val_is_set = False
151
- if internal_id in self.data.columns:
152
- val_is_set = not self.data[internal_id].isna().all()
153
- if self.initial_state is not None and internal_id in self.initial_state:
154
- val_is_set = False
155
- return val_is_set
156
-
157
-
158
- class XMLTimeSeriesFile(TimeSeriesHandler):
159
- """ "Timeseries handler for xml files"""
160
-
161
- # Whether the timeseries data has a forecast date in the header.
162
- forecast_date_in_header = False
163
-
164
- def __parse_date_time(self, el):
165
- return datetime.datetime.strptime(el.get("date") + " " + el.get("time"), "%Y-%m-%d %H:%M:%S")
166
-
167
- def __init__(
168
- self,
169
- input_folder: Path,
170
- timeseries_import_basename: str = "timeseries_import",
171
- ):
172
- self.input_folder = input_folder
173
- self.pi_binary_timeseries = False
174
- self.pi_validate_timeseries = True
175
- self.read(timeseries_import_basename)
176
-
177
- def read(self, file_name: str):
178
- """Read the timeseries data from a file."""
179
- timeseries_import_basename = file_name
180
- self.data_config = rtc.DataConfig(self.input_folder)
181
- self.pi_timeseries = pi.Timeseries(
182
- self.data_config,
183
- self.input_folder,
184
- timeseries_import_basename,
185
- binary=self.pi_binary_timeseries,
186
- pi_validate_times=self.pi_validate_timeseries,
187
- )
188
- tree = self.pi_timeseries._Timeseries__tree
189
- self.root = tree.getroot()
190
- if self.root is None:
191
- raise ValueError("No data to read.")
192
- self.set_forecast_date()
193
-
194
- def set_forecast_date(self):
195
- """Set the internal attribute `forecast_date` of the timeseries data.
196
-
197
- The forecast date is set to the first event date of the first series
198
- if no forecast date is present in the header."""
199
- first_series = self.root.find("pi:series", ns)
200
- first_header = first_series.find("pi:header", ns)
201
- forecast_date_element = first_header.find("pi:forecastDate", ns)
202
- if forecast_date_element is not None:
203
- self.forecast_date = self.__parse_date_time(forecast_date_element)
204
- self.forecast_date_in_header = True
205
- else:
206
- first_event = first_series.find("pi:event", ns)
207
- self.forecast_date = self.__parse_date_time(first_event)
208
- self.forecast_date_in_header = False
209
-
210
- def is_set(self, internal_id):
211
- """Check whether the variable exists in the timeseries data and whether it has a value at at least
212
- one of time steps."""
213
- location_id, parameter_id, qualifier_id = self.get_external_id_from_internal_id(internal_id)
214
- for series in self.root.findall("pi:series", ns):
215
- if (
216
- location_id,
217
- parameter_id,
218
- qualifier_id,
219
- ) == self.get_external_id_from_series(series):
220
- events = series.findall("pi:event", ns)
221
- for event in events:
222
- if event.get("value") is not None:
223
- return True
224
- return False
225
-
226
- def get_internal_id(self, series):
227
- """Get the internal id of a series element."""
228
- pi_header = series.find("pi:header", ns)
229
- return self.data_config.variable(pi_header)
230
-
231
- def get_external_id_from_internal_id(self, internal_id):
232
- """Get the external id of a series element. Returns a tuple with three elements: location, parameter
233
- and qualifier ID's."""
234
- return self.data_config.pi_variable_ids(internal_id)
235
-
236
- def get_all_internal_ids(self):
237
- """Get all internal id's of the timeseries data. Only returns the id's that are also in the dataconfig."""
238
- all_ids = [self.get_internal_id(series) for series in self.root.findall("pi:series", ns)]
239
- return [id for id in all_ids if ":" not in id] # Variables that contain ":" are not in the dataconfig.
240
-
241
- def set_new_forecast_date(self, forecast_date: datetime.datetime):
242
- for series in self.root.findall("pi:series", ns):
243
- header = series.find("pi:header", ns)
244
- start_date, end_date = self.get_single_date_range_from_series(series)
245
- if start_date <= forecast_date <= end_date:
246
- header.find("pi:forecastDate", ns).attrib = {
247
- "date": forecast_date.strftime("%Y-%m-%d"),
248
- "time": forecast_date.strftime("%H:%M:%S"),
249
- }
250
- else:
251
- raise ValueError("Forecast date is not within the date range of the timeseries data.")
252
-
253
- def select_time_range(self, start_date: datetime.datetime, end_date: datetime.datetime):
254
- assert isinstance(start_date, datetime.datetime) and isinstance(
255
- end_date, datetime.datetime
256
- ), "Dates must be datetime objects"
257
- for series in self.root.findall("pi:series", ns):
258
- new_start_date, new_end_date = datetime.datetime.max, datetime.datetime.min
259
- events = series.findall("pi:event", ns)
260
- for event in events:
261
- event_datetime = self.__parse_date_time(event)
262
- if event_datetime < start_date or event_datetime > end_date:
263
- series.remove(event)
264
- continue
265
- if event_datetime < new_start_date:
266
- new_start_date = event_datetime
267
- if event_datetime > new_end_date:
268
- new_end_date = event_datetime
269
- start_date_attrib = {
270
- "date": new_start_date.strftime("%Y-%m-%d"),
271
- "time": new_start_date.strftime("%H:%M:%S"),
272
- }
273
- end_date_attrib = {
274
- "date": new_end_date.strftime("%Y-%m-%d"),
275
- "time": new_end_date.strftime("%H:%M:%S"),
276
- }
277
- series.find("pi:header", ns).find("pi:startDate", ns).attrib = start_date_attrib
278
- series.find("pi:header", ns).find("pi:endDate", ns).attrib = end_date_attrib
279
- if self.forecast_date_in_header:
280
- series.find("pi:header", ns).find("pi:forecastDate", ns).attrib = start_date_attrib
281
- if not series.findall("pi:event", ns):
282
- self.root.remove(series)
283
- logger.warning("Removed series with no events.")
284
- self.set_forecast_date()
285
-
286
- def write(self, file_path: Path, file_name: str = "timeseries_import"):
287
- tree = ET.ElementTree(self.root)
288
- tree.write((file_path / file_name).with_suffix(".xml"))
289
-
290
- def get_external_id_from_series(self, series) -> Tuple[str, str, List[str]]:
291
- header = series.find("pi:header", ns)
292
- locationId = header.find("pi:locationId", ns).text
293
- parameterId = header.find("pi:parameterId", ns).text
294
- qualifier_ids = []
295
- qualifiers_els = header.findall("pi:qualifierId", ns)
296
- for qualifier in qualifiers_els:
297
- qualifier_ids.append(qualifier.text)
298
- return locationId, parameterId, qualifier_ids
299
-
300
- def get_single_date_range(self, locationId, paramterId, qualifier_ids):
301
- """Get the date range of the timeseries data for a single location and parameter"""
302
- min_date = datetime.datetime.max
303
- max_date = datetime.datetime.min
304
- for series in self.root.findall("pi:series", ns):
305
- if (
306
- locationId,
307
- paramterId,
308
- qualifier_ids,
309
- ) == self.get_external_id_from_series(series):
310
- events = series.findall("pi:event", ns)
311
- for event in events:
312
- event_datetime = self.__parse_date_time(event)
313
- if event_datetime < min_date:
314
- min_date = event_datetime
315
- if event_datetime > max_date:
316
- max_date = event_datetime
317
- if min_date == datetime.datetime.max:
318
- raise ValueError(
319
- "No data for locationId {}, parameterId {} and qualifierIds {}".format(
320
- locationId, paramterId, qualifier_ids
321
- )
322
- )
323
- return min_date, max_date
324
-
325
- def get_single_date_range_from_series(self, series):
326
- (
327
- location_id,
328
- parameter_id,
329
- qualifier_ids,
330
- ) = self.get_external_id_from_series(series)
331
- return self.get_single_date_range(location_id, parameter_id, qualifier_ids)
332
-
333
- def get_datetimes(self):
334
- """Get the dates of all timeseries data."""
335
- datetimes = set()
336
- for series in self.root.findall("pi:series", ns):
337
- events = series.findall("pi:event", ns)
338
- for event in events:
339
- event_datetime = self.__parse_date_time(event)
340
- datetimes.add(event_datetime)
341
- datetimes = list(datetimes)
342
- datetimes.sort()
343
- return datetimes
344
-
345
- def get_datetime_range(self):
346
- """Get the date range of the timeseries data, minimum and maximum over all series"""
347
- min_date = datetime.datetime.max
348
- max_date = datetime.datetime.min
349
- for series in self.root.findall("pi:series", ns):
350
- events = series.findall("pi:event", ns)
351
- for event in events:
352
- event_datetime = self.__parse_date_time(event)
353
- if event_datetime < min_date:
354
- min_date = event_datetime
355
- if event_datetime > max_date:
356
- max_date = event_datetime
357
- return min_date, max_date
358
-
359
- def get_timestep(self):
360
- """Get the timestep of the timeseries data, raise error if different stepsizes"""
361
- timestep = None
362
- for series in self.root.findall("pi:series", ns):
363
- events = series.findall("pi:event", ns)
364
- for i, event in enumerate(events):
365
- if i == 0:
366
- continue
367
- event_datetime = self.__parse_date_time(event)
368
- previous_event_datetime = self.__parse_date_time(events[i - 1])
369
- if timestep is None:
370
- timestep = event_datetime - previous_event_datetime
371
- elif timestep != event_datetime - previous_event_datetime:
372
- raise ValueError("Different timesteps in timeseries data.")
373
- return timestep
374
-
375
- def set_reference_data(self, reference_data):
376
- """Set the the reference XMLTimeSeriesFile object to use for creating new series."""
377
- self._reference_data = reference_data
378
-
379
- def get_series(
380
- self,
381
- location_id: str,
382
- parameter_id: str,
383
- qualifier_ids: List[str],
384
- from_reference=False,
385
- ) -> ET.Element:
386
- """Get the XML series element for a location and parameter.
387
- If from_reference is True, the series is taken from the reference data."""
388
- if from_reference:
389
- if hasattr(self, "_reference_data"):
390
- root = self._reference_data.root
391
- else:
392
- raise ValueError("No reference data available.")
393
- elif hasattr(self, "root") and self.root is not None:
394
- root = self.root
395
- else:
396
- raise ValueError("No data to select from, use read first.")
397
- for series in root.findall("pi:series", ns):
398
- if (
399
- location_id,
400
- parameter_id,
401
- qualifier_ids,
402
- ) == self.get_external_id_from_series(series):
403
- return series
404
- raise ValueError("No series found for locationId {} and parameterId {}".format(location_id, parameter_id))
405
-
406
- def set_initial_value(self, internal_id: str, value: float):
407
- location_id, parameter_id, qualifier_ids = self.get_external_id_from_internal_id(internal_id)
408
- for series in self.root.findall("pi:series", ns):
409
- if (
410
- location_id,
411
- parameter_id,
412
- qualifier_ids,
413
- ) == self.get_external_id_from_series(series):
414
- logger.info(
415
- "Overwriting initial value for locationId {} and parameterId {} and qualifier ids {}".format(
416
- location_id, parameter_id, qualifier_ids
417
- )
418
- )
419
- event = series.find("pi:event", ns)
420
- event.attrib["value"] = str(value)
421
- return
422
- else:
423
- # if no series found, create a new series with just one event (based on the reference data)
424
- reference_series = self.get_series(location_id, parameter_id, qualifier_ids, from_reference=True)
425
- new_series = copy.deepcopy(reference_series)
426
- start_date, end_date = self.get_datetime_range()
427
- first_event = new_series.find("pi:event", ns)
428
- if first_event is None:
429
- raise ValueError("No event found in reference data.")
430
- first_event.attrib["value"] = str(value)
431
- first_event.attrib["date"] = start_date.strftime("%Y-%m-%d")
432
- first_event.attrib["time"] = start_date.strftime("%H:%M:%S")
433
- header = new_series.find("pi:header", ns)
434
- if header is None:
435
- raise ValueError("No header found in reference data.")
436
- header.find("pi:startDate", ns).attrib = {
437
- "date": start_date.strftime("%Y-%m-%d"),
438
- "time": start_date.strftime("%H:%M:%S"),
439
- }
440
- header.find("pi:endDate", ns).attrib = {
441
- "date": end_date.strftime("%Y-%m-%d"),
442
- "time": end_date.strftime("%H:%M:%S"),
443
- }
444
- if self.forecast_date_in_header:
445
- header.find("pi:forecastDate", ns).attrib = {
446
- "date": start_date.strftime("%Y-%m-%d"),
447
- "time": start_date.strftime("%H:%M:%S"),
448
- }
449
- self.root.append(new_series)