rtc-tools 2.7.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rtc_tools-2.7.3.dist-info/METADATA +53 -0
- rtc_tools-2.7.3.dist-info/RECORD +50 -0
- rtc_tools-2.7.3.dist-info/WHEEL +5 -0
- rtc_tools-2.7.3.dist-info/entry_points.txt +3 -0
- rtc_tools-2.7.3.dist-info/licenses/COPYING.LESSER +165 -0
- rtc_tools-2.7.3.dist-info/top_level.txt +1 -0
- rtctools/__init__.py +5 -0
- rtctools/_internal/__init__.py +0 -0
- rtctools/_internal/alias_tools.py +188 -0
- rtctools/_internal/caching.py +25 -0
- rtctools/_internal/casadi_helpers.py +99 -0
- rtctools/_internal/debug_check_helpers.py +41 -0
- rtctools/_version.py +21 -0
- rtctools/data/__init__.py +4 -0
- rtctools/data/csv.py +150 -0
- rtctools/data/interpolation/__init__.py +3 -0
- rtctools/data/interpolation/bspline.py +31 -0
- rtctools/data/interpolation/bspline1d.py +169 -0
- rtctools/data/interpolation/bspline2d.py +54 -0
- rtctools/data/netcdf.py +467 -0
- rtctools/data/pi.py +1236 -0
- rtctools/data/rtc.py +228 -0
- rtctools/data/storage.py +343 -0
- rtctools/optimization/__init__.py +0 -0
- rtctools/optimization/collocated_integrated_optimization_problem.py +3208 -0
- rtctools/optimization/control_tree_mixin.py +221 -0
- rtctools/optimization/csv_lookup_table_mixin.py +462 -0
- rtctools/optimization/csv_mixin.py +300 -0
- rtctools/optimization/goal_programming_mixin.py +769 -0
- rtctools/optimization/goal_programming_mixin_base.py +1094 -0
- rtctools/optimization/homotopy_mixin.py +165 -0
- rtctools/optimization/initial_state_estimation_mixin.py +89 -0
- rtctools/optimization/io_mixin.py +320 -0
- rtctools/optimization/linearization_mixin.py +33 -0
- rtctools/optimization/linearized_order_goal_programming_mixin.py +235 -0
- rtctools/optimization/min_abs_goal_programming_mixin.py +385 -0
- rtctools/optimization/modelica_mixin.py +482 -0
- rtctools/optimization/netcdf_mixin.py +177 -0
- rtctools/optimization/optimization_problem.py +1302 -0
- rtctools/optimization/pi_mixin.py +292 -0
- rtctools/optimization/planning_mixin.py +19 -0
- rtctools/optimization/single_pass_goal_programming_mixin.py +676 -0
- rtctools/optimization/timeseries.py +56 -0
- rtctools/rtctoolsapp.py +131 -0
- rtctools/simulation/__init__.py +0 -0
- rtctools/simulation/csv_mixin.py +171 -0
- rtctools/simulation/io_mixin.py +195 -0
- rtctools/simulation/pi_mixin.py +255 -0
- rtctools/simulation/simulation_problem.py +1293 -0
- rtctools/util.py +241 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
|
|
4
|
+
import rtctools.data.pi as pi
|
|
5
|
+
import rtctools.data.rtc as rtc
|
|
6
|
+
from rtctools.optimization.io_mixin import IOMixin
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("rtctools")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PIMixin(IOMixin):
|
|
12
|
+
"""
|
|
13
|
+
Adds `Delft-FEWS Published Interface
|
|
14
|
+
<https://publicwiki.deltares.nl/display/FEWSDOC/The+Delft-Fews+Published+Interface>`_
|
|
15
|
+
I/O to your optimization problem.
|
|
16
|
+
|
|
17
|
+
During preprocessing, files named ``rtcDataConfig.xml``, ``timeseries_import.xml``,
|
|
18
|
+
``rtcParameterConfig.xml``, and ``rtcParameterConfig_Numerical.xml`` are read from the
|
|
19
|
+
``input`` subfolder. ``rtcDataConfig.xml`` maps tuples of FEWS identifiers, including
|
|
20
|
+
location and parameter ID, to RTC-Tools time series identifiers.
|
|
21
|
+
|
|
22
|
+
During postprocessing, a file named ``timeseries_export.xml`` is written to the ``output``
|
|
23
|
+
subfolder.
|
|
24
|
+
|
|
25
|
+
:cvar pi_binary_timeseries:
|
|
26
|
+
Whether to use PI binary timeseries format. Default is ``False``.
|
|
27
|
+
:cvar pi_parameter_config_basenames:
|
|
28
|
+
List of parameter config file basenames to read. Default is [``rtcParameterConfig``].
|
|
29
|
+
:cvar pi_parameter_config_numerical_basename:
|
|
30
|
+
Numerical config file basename to read. Default is ``rtcParameterConfig_Numerical``.
|
|
31
|
+
:cvar pi_check_for_duplicate_parameters:
|
|
32
|
+
Check if duplicate parameters are read. Default is ``True``.
|
|
33
|
+
:cvar pi_validate_timeseries:
|
|
34
|
+
Check consistency of timeseries. Default is ``True``.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
#: Whether to use PI binary timeseries format
|
|
38
|
+
pi_binary_timeseries = False
|
|
39
|
+
|
|
40
|
+
#: Location of rtcParameterConfig files
|
|
41
|
+
pi_parameter_config_basenames = ["rtcParameterConfig"]
|
|
42
|
+
pi_parameter_config_numerical_basename = "rtcParameterConfig_Numerical"
|
|
43
|
+
|
|
44
|
+
#: Check consistency of timeseries
|
|
45
|
+
pi_validate_timeseries = True
|
|
46
|
+
|
|
47
|
+
#: Check for duplicate parameters
|
|
48
|
+
pi_check_for_duplicate_parameters = True
|
|
49
|
+
|
|
50
|
+
def __init__(self, **kwargs):
|
|
51
|
+
# Call parent class first for default behaviour.
|
|
52
|
+
super().__init__(**kwargs)
|
|
53
|
+
|
|
54
|
+
# Load rtcDataConfig.xml. We assume this file does not change over the
|
|
55
|
+
# life time of this object.
|
|
56
|
+
self.__data_config = rtc.DataConfig(self._input_folder)
|
|
57
|
+
|
|
58
|
+
def read(self):
|
|
59
|
+
# Call parent class first for default behaviour.
|
|
60
|
+
super().read()
|
|
61
|
+
|
|
62
|
+
# rtcParameterConfig
|
|
63
|
+
self.__parameter_config = []
|
|
64
|
+
try:
|
|
65
|
+
for pi_parameter_config_basename in self.pi_parameter_config_basenames:
|
|
66
|
+
self.__parameter_config.append(
|
|
67
|
+
pi.ParameterConfig(self._input_folder, pi_parameter_config_basename)
|
|
68
|
+
)
|
|
69
|
+
except IOError:
|
|
70
|
+
raise Exception(
|
|
71
|
+
"PIMixin: {}.xml not found in {}.".format(
|
|
72
|
+
pi_parameter_config_basename, self._input_folder
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
self.__parameter_config_numerical = pi.ParameterConfig(
|
|
78
|
+
self._input_folder, self.pi_parameter_config_numerical_basename
|
|
79
|
+
)
|
|
80
|
+
except IOError:
|
|
81
|
+
self.__parameter_config_numerical = None
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
self.__timeseries_import = pi.Timeseries(
|
|
85
|
+
self.__data_config,
|
|
86
|
+
self._input_folder,
|
|
87
|
+
self.timeseries_import_basename,
|
|
88
|
+
binary=self.pi_binary_timeseries,
|
|
89
|
+
pi_validate_times=self.pi_validate_timeseries,
|
|
90
|
+
)
|
|
91
|
+
except IOError:
|
|
92
|
+
raise Exception(
|
|
93
|
+
"PIMixin: {}.xml not found in {}.".format(
|
|
94
|
+
self.timeseries_import_basename, self._input_folder
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.__timeseries_export = pi.Timeseries(
|
|
99
|
+
self.__data_config,
|
|
100
|
+
self._output_folder,
|
|
101
|
+
self.timeseries_export_basename,
|
|
102
|
+
binary=self.pi_binary_timeseries,
|
|
103
|
+
pi_validate_times=False,
|
|
104
|
+
make_new_file=True,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Convert timeseries timestamps to seconds since t0 for internal use
|
|
108
|
+
timeseries_import_times = self.__timeseries_import.times
|
|
109
|
+
|
|
110
|
+
# Timestamp check
|
|
111
|
+
if self.pi_validate_timeseries:
|
|
112
|
+
for i in range(len(timeseries_import_times) - 1):
|
|
113
|
+
if timeseries_import_times[i] >= timeseries_import_times[i + 1]:
|
|
114
|
+
raise Exception("PIMixin: Time stamps must be strictly increasing.")
|
|
115
|
+
|
|
116
|
+
if self.__timeseries_import.dt:
|
|
117
|
+
# Check if the timeseries are truly equidistant
|
|
118
|
+
if self.pi_validate_timeseries:
|
|
119
|
+
dt = timeseries_import_times[1] - timeseries_import_times[0]
|
|
120
|
+
for i in range(len(timeseries_import_times) - 1):
|
|
121
|
+
if timeseries_import_times[i + 1] - timeseries_import_times[i] != dt:
|
|
122
|
+
raise Exception(
|
|
123
|
+
"PIMixin: Expecting equidistant timeseries, the time step "
|
|
124
|
+
"towards {} is not the same as the time step(s) before. Set "
|
|
125
|
+
"unit to nonequidistant if this is intended.".format(
|
|
126
|
+
timeseries_import_times[i + 1]
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Offer input timeseries to IOMixin
|
|
131
|
+
self.io.reference_datetime = self.__timeseries_import.forecast_datetime
|
|
132
|
+
|
|
133
|
+
for ensemble_member in range(self.__timeseries_import.ensemble_size):
|
|
134
|
+
for variable, values in self.__timeseries_import.items(ensemble_member):
|
|
135
|
+
self.io.set_timeseries(variable, timeseries_import_times, values, ensemble_member)
|
|
136
|
+
|
|
137
|
+
# store the parameters in the internal data store. Note that we
|
|
138
|
+
# are effectively broadcasting parameters, as ParameterConfig does
|
|
139
|
+
# not support parameters varying per ensemble member
|
|
140
|
+
for parameter_config in self.__parameter_config:
|
|
141
|
+
for location_id, model_id, parameter_id, value in parameter_config:
|
|
142
|
+
try:
|
|
143
|
+
parameter = self.__data_config.parameter(
|
|
144
|
+
parameter_id, location_id, model_id
|
|
145
|
+
)
|
|
146
|
+
except KeyError:
|
|
147
|
+
parameter = parameter_id
|
|
148
|
+
|
|
149
|
+
self.io.set_parameter(
|
|
150
|
+
parameter,
|
|
151
|
+
value,
|
|
152
|
+
ensemble_member,
|
|
153
|
+
check_duplicates=self.pi_check_for_duplicate_parameters,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def solver_options(self):
|
|
157
|
+
# Call parent
|
|
158
|
+
options = super().solver_options()
|
|
159
|
+
|
|
160
|
+
# Only do this if we have a rtcParameterConfig_Numerical
|
|
161
|
+
if self.__parameter_config_numerical is None:
|
|
162
|
+
return options
|
|
163
|
+
|
|
164
|
+
# Load solver options from parameter config
|
|
165
|
+
for _location_id, _model, option, value in self.__parameter_config_numerical:
|
|
166
|
+
options[option] = value
|
|
167
|
+
|
|
168
|
+
# Done
|
|
169
|
+
return options
|
|
170
|
+
|
|
171
|
+
def write(self):
|
|
172
|
+
# Call parent class first for default behaviour.
|
|
173
|
+
super().write()
|
|
174
|
+
|
|
175
|
+
# Get time stamps
|
|
176
|
+
times = self.times()
|
|
177
|
+
if len(set(times[1:] - times[:-1])) == 1:
|
|
178
|
+
dt = timedelta(seconds=times[1] - times[0])
|
|
179
|
+
else:
|
|
180
|
+
dt = None
|
|
181
|
+
|
|
182
|
+
# Start of write output
|
|
183
|
+
# Write the time range for the export file.
|
|
184
|
+
self.__timeseries_export.times = [
|
|
185
|
+
self.__timeseries_import.times[self.__timeseries_import.forecast_index]
|
|
186
|
+
+ timedelta(seconds=s)
|
|
187
|
+
for s in times
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
# Write other time settings
|
|
191
|
+
self.__timeseries_export.forecast_datetime = self.__timeseries_import.forecast_datetime
|
|
192
|
+
self.__timeseries_export.dt = dt
|
|
193
|
+
self.__timeseries_export.timezone = self.__timeseries_import.timezone
|
|
194
|
+
|
|
195
|
+
# Write the ensemble properties for the export file.
|
|
196
|
+
if self.ensemble_size > 1:
|
|
197
|
+
self.__timeseries_export.contains_ensemble = True
|
|
198
|
+
self.__timeseries_export.ensemble_size = self.ensemble_size
|
|
199
|
+
self.__timeseries_export.contains_ensemble = self.ensemble_size > 1
|
|
200
|
+
|
|
201
|
+
# Start looping over the ensembles for extraction of the output values.
|
|
202
|
+
for ensemble_member in range(self.ensemble_size):
|
|
203
|
+
results = self.extract_results(ensemble_member)
|
|
204
|
+
|
|
205
|
+
# For all variables that are output variables the values are
|
|
206
|
+
# extracted from the results.
|
|
207
|
+
for variable in [sym.name() for sym in self.output_variables]:
|
|
208
|
+
for alias in self.alias_relation.aliases(variable):
|
|
209
|
+
try:
|
|
210
|
+
values = results[alias]
|
|
211
|
+
if len(values) != len(times):
|
|
212
|
+
values = self.interpolate(
|
|
213
|
+
times, self.times(alias), values, self.interpolation_method(alias)
|
|
214
|
+
)
|
|
215
|
+
except KeyError:
|
|
216
|
+
try:
|
|
217
|
+
ts = self.get_timeseries(alias, ensemble_member)
|
|
218
|
+
if len(ts.times) != len(times):
|
|
219
|
+
values = self.interpolate(times, ts.times, ts.values)
|
|
220
|
+
else:
|
|
221
|
+
values = ts.values
|
|
222
|
+
except KeyError:
|
|
223
|
+
logger.error(
|
|
224
|
+
"PIMixin: Output requested for non-existent alias {}. "
|
|
225
|
+
"Will not be in output file.".format(alias)
|
|
226
|
+
)
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# Check if ID mapping is present
|
|
230
|
+
try:
|
|
231
|
+
self.__data_config.pi_variable_ids(alias)
|
|
232
|
+
except KeyError:
|
|
233
|
+
logger.debug(
|
|
234
|
+
"PIMixin: variable {} has no mapping defined in rtcDataConfig "
|
|
235
|
+
"so cannot be added to the output file.".format(alias)
|
|
236
|
+
)
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
# Add series to output file.
|
|
240
|
+
# NOTE: We use the unit of the zeroth ensemble member, as
|
|
241
|
+
# we might be outputting more ensembles than we read in.
|
|
242
|
+
self.__timeseries_export.set(
|
|
243
|
+
alias,
|
|
244
|
+
values,
|
|
245
|
+
unit=self.__timeseries_import.get_unit(alias, ensemble_member=0),
|
|
246
|
+
ensemble_member=ensemble_member,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Write output file to disk
|
|
250
|
+
self.__timeseries_export.write()
|
|
251
|
+
|
|
252
|
+
def set_timeseries(self, variable: str, *args, unit: str = None, **kwargs):
|
|
253
|
+
if unit is not None:
|
|
254
|
+
self.__timeseries_import.set_unit(variable, unit, 0)
|
|
255
|
+
|
|
256
|
+
super().set_timeseries(variable, *args, **kwargs)
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def timeseries_import(self):
|
|
260
|
+
"""
|
|
261
|
+
:class:`pi.Timeseries` object containing the input data.
|
|
262
|
+
"""
|
|
263
|
+
return self.__timeseries_import
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def timeseries_import_times(self):
|
|
267
|
+
"""
|
|
268
|
+
List of time stamps for which input data is specified.
|
|
269
|
+
|
|
270
|
+
The time stamps are in seconds since t0, and may be negative.
|
|
271
|
+
"""
|
|
272
|
+
return self.io.times_sec
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def timeseries_export(self):
|
|
276
|
+
"""
|
|
277
|
+
:class:`pi.Timeseries` object for holding the output data.
|
|
278
|
+
"""
|
|
279
|
+
return self.__timeseries_export
|
|
280
|
+
|
|
281
|
+
def set_unit(self, variable: str, unit: str):
|
|
282
|
+
"""
|
|
283
|
+
Set the unit of a time series.
|
|
284
|
+
|
|
285
|
+
:param variable: Time series ID.
|
|
286
|
+
:param unit: Unit.
|
|
287
|
+
"""
|
|
288
|
+
assert hasattr(self, "_PIMixin__timeseries_import"), (
|
|
289
|
+
"set_unit can only be called after read() in pre() has finished."
|
|
290
|
+
)
|
|
291
|
+
self.__timeseries_import.set_unit(variable, unit, 0)
|
|
292
|
+
self.__timeseries_export.set_unit(variable, unit, 0)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from rtctools.optimization.optimization_problem import OptimizationProblem
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PlanningMixin(OptimizationProblem):
|
|
5
|
+
"""
|
|
6
|
+
Uses default discretization logic for planning variables, but uses
|
|
7
|
+
dedicated per-ensemble-member decision variables for other, non-planning control
|
|
8
|
+
variables.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Planning variables
|
|
12
|
+
planning_variables = []
|
|
13
|
+
|
|
14
|
+
def discretize_control(self, variable, ensemble_member, times, offset):
|
|
15
|
+
if variable not in self.planning_variables:
|
|
16
|
+
# Non-planning variables are never shared between ensemble members
|
|
17
|
+
return slice(offset, offset + len(times))
|
|
18
|
+
else:
|
|
19
|
+
return super().discretize_control(variable, ensemble_member, times, offset)
|