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.
Files changed (50) hide show
  1. rtc_tools-2.7.3.dist-info/METADATA +53 -0
  2. rtc_tools-2.7.3.dist-info/RECORD +50 -0
  3. rtc_tools-2.7.3.dist-info/WHEEL +5 -0
  4. rtc_tools-2.7.3.dist-info/entry_points.txt +3 -0
  5. rtc_tools-2.7.3.dist-info/licenses/COPYING.LESSER +165 -0
  6. rtc_tools-2.7.3.dist-info/top_level.txt +1 -0
  7. rtctools/__init__.py +5 -0
  8. rtctools/_internal/__init__.py +0 -0
  9. rtctools/_internal/alias_tools.py +188 -0
  10. rtctools/_internal/caching.py +25 -0
  11. rtctools/_internal/casadi_helpers.py +99 -0
  12. rtctools/_internal/debug_check_helpers.py +41 -0
  13. rtctools/_version.py +21 -0
  14. rtctools/data/__init__.py +4 -0
  15. rtctools/data/csv.py +150 -0
  16. rtctools/data/interpolation/__init__.py +3 -0
  17. rtctools/data/interpolation/bspline.py +31 -0
  18. rtctools/data/interpolation/bspline1d.py +169 -0
  19. rtctools/data/interpolation/bspline2d.py +54 -0
  20. rtctools/data/netcdf.py +467 -0
  21. rtctools/data/pi.py +1236 -0
  22. rtctools/data/rtc.py +228 -0
  23. rtctools/data/storage.py +343 -0
  24. rtctools/optimization/__init__.py +0 -0
  25. rtctools/optimization/collocated_integrated_optimization_problem.py +3208 -0
  26. rtctools/optimization/control_tree_mixin.py +221 -0
  27. rtctools/optimization/csv_lookup_table_mixin.py +462 -0
  28. rtctools/optimization/csv_mixin.py +300 -0
  29. rtctools/optimization/goal_programming_mixin.py +769 -0
  30. rtctools/optimization/goal_programming_mixin_base.py +1094 -0
  31. rtctools/optimization/homotopy_mixin.py +165 -0
  32. rtctools/optimization/initial_state_estimation_mixin.py +89 -0
  33. rtctools/optimization/io_mixin.py +320 -0
  34. rtctools/optimization/linearization_mixin.py +33 -0
  35. rtctools/optimization/linearized_order_goal_programming_mixin.py +235 -0
  36. rtctools/optimization/min_abs_goal_programming_mixin.py +385 -0
  37. rtctools/optimization/modelica_mixin.py +482 -0
  38. rtctools/optimization/netcdf_mixin.py +177 -0
  39. rtctools/optimization/optimization_problem.py +1302 -0
  40. rtctools/optimization/pi_mixin.py +292 -0
  41. rtctools/optimization/planning_mixin.py +19 -0
  42. rtctools/optimization/single_pass_goal_programming_mixin.py +676 -0
  43. rtctools/optimization/timeseries.py +56 -0
  44. rtctools/rtctoolsapp.py +131 -0
  45. rtctools/simulation/__init__.py +0 -0
  46. rtctools/simulation/csv_mixin.py +171 -0
  47. rtctools/simulation/io_mixin.py +195 -0
  48. rtctools/simulation/pi_mixin.py +255 -0
  49. rtctools/simulation/simulation_problem.py +1293 -0
  50. rtctools/util.py +241 -0
@@ -0,0 +1,482 @@
1
+ import importlib.resources
2
+ import itertools
3
+ import logging
4
+ import sys
5
+ from typing import Dict, Union
6
+
7
+ # Python 3.9's importlib.metadata does not support the "group" parameter to
8
+ # entry_points yet.
9
+ if sys.version_info < (3, 10):
10
+ import importlib_metadata
11
+ else:
12
+ from importlib import metadata as importlib_metadata
13
+
14
+ import casadi as ca
15
+ import numpy as np
16
+ import pymoca
17
+ import pymoca.backends.casadi.api
18
+
19
+ from rtctools._internal.alias_tools import AliasDict
20
+ from rtctools._internal.caching import cached
21
+ from rtctools._internal.casadi_helpers import substitute_in_external
22
+
23
+ from .optimization_problem import OptimizationProblem
24
+ from .timeseries import Timeseries
25
+
26
+ logger = logging.getLogger("rtctools")
27
+
28
+
29
+ class ModelicaMixin(OptimizationProblem):
30
+ """
31
+ Adds a `Modelica <http://www.modelica.org/>`_ model to your optimization problem.
32
+
33
+ During preprocessing, the Modelica files located inside the ``model`` subfolder are loaded.
34
+
35
+ :cvar modelica_library_folders:
36
+ Folders in which any referenced Modelica libraries are to be found.
37
+ Default is an empty list.
38
+ """
39
+
40
+ # Folders in which the referenced Modelica libraries are found
41
+ modelica_library_folders = []
42
+
43
+ def __init__(self, **kwargs):
44
+ # Check arguments
45
+ assert "model_folder" in kwargs
46
+
47
+ # Log pymoca version
48
+ logger.debug("Using pymoca {}.".format(pymoca.__version__))
49
+
50
+ # Transfer model from the Modelica .mo file to CasADi using pymoca
51
+ if "model_name" in kwargs:
52
+ model_name = kwargs["model_name"]
53
+ else:
54
+ if hasattr(self, "model_name"):
55
+ model_name = self.model_name
56
+ else:
57
+ model_name = self.__class__.__name__
58
+
59
+ compiler_options = self.compiler_options()
60
+ logger.info(f"Loading/compiling model {model_name}.")
61
+ try:
62
+ self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
63
+ kwargs["model_folder"], model_name, compiler_options
64
+ )
65
+ except (RuntimeError, ModuleNotFoundError) as error:
66
+ if not compiler_options.get("cache", False):
67
+ raise error
68
+ compiler_options["cache"] = False
69
+ logger.warning(f"Loading model {model_name} using a cache file failed: {error}.")
70
+ logger.info(f"Compiling model {model_name}.")
71
+ self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
72
+ kwargs["model_folder"], model_name, compiler_options
73
+ )
74
+
75
+ # Extract the CasADi MX variables used in the model
76
+ self.__mx = {}
77
+ self.__mx["time"] = [self.__pymoca_model.time]
78
+ self.__mx["states"] = [v.symbol for v in self.__pymoca_model.states]
79
+ self.__mx["derivatives"] = [v.symbol for v in self.__pymoca_model.der_states]
80
+ self.__mx["algebraics"] = [v.symbol for v in self.__pymoca_model.alg_states]
81
+ self.__mx["parameters"] = [v.symbol for v in self.__pymoca_model.parameters]
82
+ self.__mx["string_parameters"] = [
83
+ v.name
84
+ for v in (*self.__pymoca_model.string_parameters, *self.__pymoca_model.string_constants)
85
+ ]
86
+ self.__mx["control_inputs"] = []
87
+ self.__mx["constant_inputs"] = []
88
+ self.__mx["lookup_tables"] = []
89
+
90
+ # Merge with user-specified delayed feedback
91
+ for v in self.__pymoca_model.inputs:
92
+ if v.symbol.name() in self.__pymoca_model.delay_states:
93
+ # Delayed feedback variables are local to each ensemble, and
94
+ # therefore belong to the collection of algebraic variables,
95
+ # rather than to the control inputs.
96
+ self.__mx["algebraics"].append(v.symbol)
97
+ else:
98
+ if v.symbol.name() in kwargs.get("lookup_tables", []):
99
+ self.__mx["lookup_tables"].append(v.symbol)
100
+ elif v.fixed:
101
+ self.__mx["constant_inputs"].append(v.symbol)
102
+ else:
103
+ self.__mx["control_inputs"].append(v.symbol)
104
+
105
+ # Initialize nominals and types
106
+ # These are not in @cached dictionary properties for backwards compatibility.
107
+ self.__python_types = AliasDict(self.alias_relation)
108
+ for v in itertools.chain(
109
+ self.__pymoca_model.states, self.__pymoca_model.alg_states, self.__pymoca_model.inputs
110
+ ):
111
+ self.__python_types[v.symbol.name()] = v.python_type
112
+
113
+ # Initialize dae, initial residuals, as well as delay arguments
114
+ # These are not in @cached dictionary properties so that we need to create the list
115
+ # of function arguments only once.
116
+ variable_lists = ["states", "der_states", "alg_states", "inputs", "constants", "parameters"]
117
+ function_arguments = [self.__pymoca_model.time] + [
118
+ ca.veccat(*[v.symbol for v in getattr(self.__pymoca_model, variable_list)])
119
+ for variable_list in variable_lists
120
+ ]
121
+
122
+ self.__dae_residual = self.__pymoca_model.dae_residual_function(*function_arguments)
123
+ if self.__dae_residual is None:
124
+ self.__dae_residual = ca.MX()
125
+
126
+ self.__initial_residual = self.__pymoca_model.initial_residual_function(*function_arguments)
127
+ if self.__initial_residual is None:
128
+ self.__initial_residual = ca.MX()
129
+
130
+ # Log variables in debug mode
131
+ if logger.getEffectiveLevel() == logging.DEBUG:
132
+ logger.debug(
133
+ "ModelicaMixin: Found states {}".format(
134
+ ", ".join([var.name() for var in self.__mx["states"]])
135
+ )
136
+ )
137
+ logger.debug(
138
+ "ModelicaMixin: Found derivatives {}".format(
139
+ ", ".join([var.name() for var in self.__mx["derivatives"]])
140
+ )
141
+ )
142
+ logger.debug(
143
+ "ModelicaMixin: Found algebraics {}".format(
144
+ ", ".join([var.name() for var in self.__mx["algebraics"]])
145
+ )
146
+ )
147
+ logger.debug(
148
+ "ModelicaMixin: Found control inputs {}".format(
149
+ ", ".join([var.name() for var in self.__mx["control_inputs"]])
150
+ )
151
+ )
152
+ logger.debug(
153
+ "ModelicaMixin: Found constant inputs {}".format(
154
+ ", ".join([var.name() for var in self.__mx["constant_inputs"]])
155
+ )
156
+ )
157
+ logger.debug(
158
+ "ModelicaMixin: Found parameters {}".format(
159
+ ", ".join([var.name() for var in self.__mx["parameters"]])
160
+ )
161
+ )
162
+
163
+ # Call parent class first for default behaviour.
164
+ super().__init__(**kwargs)
165
+
166
+ @cached
167
+ def compiler_options(self) -> Dict[str, Union[str, bool]]:
168
+ """
169
+ Subclasses can configure the `pymoca <http://github.com/pymoca/pymoca>`_ compiler options
170
+ here.
171
+
172
+ :returns:
173
+ A dictionary of pymoca compiler options. See the pymoca documentation for details.
174
+ """
175
+
176
+ # Default options
177
+ compiler_options = {}
178
+
179
+ # Expand vector states to multiple scalar component states.
180
+ compiler_options["expand_vectors"] = True
181
+
182
+ # Where imported model libraries are located.
183
+ library_folders = self.modelica_library_folders.copy()
184
+
185
+ for ep in importlib_metadata.entry_points(group="rtctools.libraries.modelica"):
186
+ if ep.name == "library_folder":
187
+ library_folders.append(str(importlib.resources.files(ep.module).joinpath(ep.attr)))
188
+
189
+ compiler_options["library_folders"] = library_folders
190
+
191
+ # Eliminate equations of the type 'var = const'.
192
+ compiler_options["eliminate_constant_assignments"] = True
193
+
194
+ # Eliminate constant symbols from model, replacing them with the values
195
+ # specified in the model.
196
+ compiler_options["replace_constant_values"] = True
197
+
198
+ # Replace any constant expressions into the model.
199
+ compiler_options["replace_constant_expressions"] = True
200
+
201
+ # Replace any parameter expressions into the model.
202
+ compiler_options["replace_parameter_expressions"] = True
203
+
204
+ # Eliminate variables starting with underscores.
205
+ compiler_options["eliminable_variable_expression"] = r"(.*[.]|^)_\w+(\[[\d,]+\])?\Z"
206
+
207
+ # Pymoca currently requires `expand_mx` to be set for
208
+ # `eliminable_variable_expression` to work.
209
+ compiler_options["expand_mx"] = True
210
+
211
+ # Automatically detect and eliminate alias variables.
212
+ compiler_options["detect_aliases"] = True
213
+
214
+ # Disallow aliasing to derivative states
215
+ compiler_options["allow_derivative_aliases"] = False
216
+
217
+ # Cache the model on disk
218
+ compiler_options["cache"] = True
219
+
220
+ # Done
221
+ return compiler_options
222
+
223
+ def delayed_feedback(self):
224
+ delayed_feedback = super().delayed_feedback()
225
+
226
+ # Create delayed feedback
227
+ for delay_state, delay_argument in zip(
228
+ self.__pymoca_model.delay_states, self.__pymoca_model.delay_arguments
229
+ ):
230
+ delayed_feedback.append((delay_argument.expr, delay_state, delay_argument.duration))
231
+ return delayed_feedback
232
+
233
+ @property
234
+ def dae_residual(self):
235
+ return self.__dae_residual
236
+
237
+ @property
238
+ def dae_variables(self):
239
+ return self.__mx
240
+
241
+ @property
242
+ @cached
243
+ def output_variables(self):
244
+ output_variables = [ca.MX.sym(variable) for variable in self.__pymoca_model.outputs]
245
+ output_variables.extend(self.__mx["control_inputs"])
246
+ return output_variables
247
+
248
+ @cached
249
+ def parameters(self, ensemble_member):
250
+ # Call parent class first for default values.
251
+ parameters = super().parameters(ensemble_member)
252
+
253
+ # Return parameter values from pymoca model
254
+ parameters.update({v.symbol.name(): v.value for v in self.__pymoca_model.parameters})
255
+
256
+ # Done
257
+ return parameters
258
+
259
+ @cached
260
+ def string_parameters(self, ensemble_member):
261
+ # Call parent class first for default values.
262
+ parameters = super().string_parameters(ensemble_member)
263
+
264
+ # Return parameter values from pymoca model
265
+ parameters.update({v.name: v.value for v in self.__pymoca_model.string_parameters})
266
+ parameters.update({v.name: v.value for v in self.__pymoca_model.string_constants})
267
+
268
+ # Done
269
+ return parameters
270
+
271
+ @cached
272
+ def history(self, ensemble_member):
273
+ history = super().history(ensemble_member)
274
+
275
+ initial_time = np.array([self.initial_time])
276
+
277
+ # Parameter values
278
+ parameters = self.parameters(ensemble_member)
279
+ parameter_values = [
280
+ parameters.get(param.name(), param) for param in self.__mx["parameters"]
281
+ ]
282
+
283
+ # Initial conditions obtained from start attributes.
284
+ for v in self.__pymoca_model.states:
285
+ if v.fixed:
286
+ sym_name = v.symbol.name()
287
+ start = v.start
288
+
289
+ if isinstance(start, ca.MX):
290
+ # If start contains symbolics, try substituting parameter values
291
+ if isinstance(start, ca.MX) and not start.is_constant():
292
+ [start] = substitute_in_external(
293
+ [start],
294
+ self.__mx["parameters"],
295
+ parameter_values,
296
+ resolve_numerically=True,
297
+ )
298
+ if not start.is_constant() or np.isnan(float(start)):
299
+ raise Exception(
300
+ "ModelicaMixin: Could not resolve initial value for {}".format(
301
+ sym_name
302
+ )
303
+ )
304
+
305
+ start = v.python_type(start)
306
+
307
+ history[sym_name] = Timeseries(initial_time, start)
308
+
309
+ if logger.getEffectiveLevel() == logging.DEBUG:
310
+ logger.debug(
311
+ "ModelicaMixin: Initial state variable {} = {}".format(sym_name, start)
312
+ )
313
+
314
+ return history
315
+
316
+ @property
317
+ def initial_residual(self):
318
+ return self.__initial_residual
319
+
320
+ @cached
321
+ def bounds(self):
322
+ # Call parent class first for default values.
323
+ bounds = super().bounds()
324
+
325
+ # Parameter values
326
+ parameters = self.parameters(0)
327
+ parameter_values = [
328
+ parameters.get(param.name(), param) for param in self.__mx["parameters"]
329
+ ]
330
+
331
+ # Load additional bounds from model
332
+ for v in itertools.chain(
333
+ self.__pymoca_model.states, self.__pymoca_model.alg_states, self.__pymoca_model.inputs
334
+ ):
335
+ sym_name = v.symbol.name()
336
+
337
+ try:
338
+ (m, M) = bounds[sym_name]
339
+ except KeyError:
340
+ if self.__python_types.get(sym_name, float) is bool:
341
+ (m, M) = (0, 1)
342
+ else:
343
+ (m, M) = (-np.inf, np.inf)
344
+
345
+ m_ = v.min
346
+ if isinstance(m_, ca.MX) and not m_.is_constant():
347
+ [m_] = substitute_in_external(
348
+ [m_], self.__mx["parameters"], parameter_values, resolve_numerically=True
349
+ )
350
+ if not m_.is_constant() or np.isnan(float(m_)):
351
+ raise Exception(
352
+ "Could not resolve lower bound for variable {}".format(sym_name)
353
+ )
354
+ m_ = float(m_)
355
+
356
+ M_ = v.max
357
+ if isinstance(M_, ca.MX) and not M_.is_constant():
358
+ [M_] = substitute_in_external(
359
+ [M_], self.__mx["parameters"], parameter_values, resolve_numerically=True
360
+ )
361
+ if not M_.is_constant() or np.isnan(float(M_)):
362
+ raise Exception(
363
+ "Could not resolve upper bound for variable {}".format(sym_name)
364
+ )
365
+ M_ = float(M_)
366
+
367
+ # We take the intersection of all provided bounds
368
+ m = max(m, m_)
369
+ M = min(M, M_)
370
+
371
+ bounds[sym_name] = (m, M)
372
+
373
+ return bounds
374
+
375
+ @cached
376
+ def seed(self, ensemble_member):
377
+ # Call parent class first for default values.
378
+ seed = super().seed(ensemble_member)
379
+
380
+ # Parameter values
381
+ parameters = self.parameters(ensemble_member)
382
+ parameter_values = [
383
+ parameters.get(param.name(), param) for param in self.__mx["parameters"]
384
+ ]
385
+
386
+ # Load seeds
387
+ for var in itertools.chain(self.__pymoca_model.states, self.__pymoca_model.alg_states):
388
+ if var.fixed:
389
+ # Values will be set from import timeseries
390
+ continue
391
+
392
+ start = var.start
393
+
394
+ if isinstance(start, ca.MX) or start != 0.0:
395
+ sym_name = var.symbol.name()
396
+
397
+ # If start contains symbolics, try substituting parameter values
398
+ if isinstance(start, ca.MX) and not start.is_constant():
399
+ [start] = substitute_in_external(
400
+ [start], self.__mx["parameters"], parameter_values, resolve_numerically=True
401
+ )
402
+ if not start.is_constant() or np.isnan(float(start)):
403
+ logger.error(
404
+ "ModelicaMixin: Could not resolve seed value for {}".format(sym_name)
405
+ )
406
+ continue
407
+
408
+ times = self.times(sym_name)
409
+ start = var.python_type(start)
410
+ s = Timeseries(times, np.full_like(times, start))
411
+ if logger.getEffectiveLevel() == logging.DEBUG:
412
+ logger.debug("ModelicaMixin: Seeded variable {} = {}".format(sym_name, start))
413
+ seed[sym_name] = s
414
+
415
+ return seed
416
+
417
+ def variable_is_discrete(self, variable):
418
+ return self.__python_types.get(variable, float) is not float
419
+
420
+ @property
421
+ @cached
422
+ def alias_relation(self):
423
+ return self.__pymoca_model.alias_relation
424
+
425
+ @property
426
+ @cached
427
+ def __nominals(self):
428
+ # Make the dict
429
+ nominal_dict = AliasDict(self.alias_relation)
430
+
431
+ # Grab parameters and their values
432
+ parameters = self.parameters(0)
433
+ parameter_values = [
434
+ parameters.get(param.name(), param) for param in self.__mx["parameters"]
435
+ ]
436
+
437
+ # Iterate over nominalizable states
438
+ for v in itertools.chain(
439
+ self.__pymoca_model.states, self.__pymoca_model.alg_states, self.__pymoca_model.inputs
440
+ ):
441
+ sym_name = v.symbol.name()
442
+ nominal = v.nominal
443
+
444
+ # If nominal contains parameter symbols, substitute them
445
+ if isinstance(nominal, ca.MX) and not nominal.is_constant():
446
+ [nominal] = substitute_in_external(
447
+ [nominal], self.__mx["parameters"], parameter_values, resolve_numerically=True
448
+ )
449
+ if not nominal.is_constant() or np.isnan(float(nominal)):
450
+ logger.error(
451
+ "ModelicaMixin: Could not resolve nominal value for {}".format(sym_name)
452
+ )
453
+ continue
454
+
455
+ nominal = float(nominal)
456
+
457
+ if not np.isnan(nominal):
458
+ # Take absolute value (nominal sign is meaningless- a nominal is a magnitude)
459
+ nominal = abs(nominal)
460
+
461
+ # If nominal is 0 or 1, we just use the default (1.0)
462
+ if nominal in (0.0, 1.0):
463
+ continue
464
+
465
+ nominal_dict[sym_name] = nominal
466
+
467
+ if logger.getEffectiveLevel() == logging.DEBUG:
468
+ logger.debug(
469
+ "ModelicaMixin: Set nominal value for variable {} to {}".format(
470
+ sym_name, nominal
471
+ )
472
+ )
473
+ else:
474
+ logger.warning("ModelicaMixin: Could not set nominal value for {}".format(sym_name))
475
+
476
+ return nominal_dict
477
+
478
+ def variable_nominal(self, variable):
479
+ try:
480
+ return self.__nominals[variable]
481
+ except KeyError:
482
+ return super().variable_nominal(variable)
@@ -0,0 +1,177 @@
1
+ import logging
2
+ from collections import OrderedDict
3
+ from typing import Tuple
4
+
5
+ import rtctools.data.netcdf as netcdf
6
+ from rtctools.optimization.io_mixin import IOMixin
7
+
8
+ logger = logging.getLogger("rtctools")
9
+
10
+
11
+ class NetCDFMixin(IOMixin):
12
+ """
13
+ Adds NetCDF I/O to your optimization problem.
14
+
15
+ During preprocessing, a file named timeseries_import.nc is read from the ``input`` subfolder.
16
+ During postprocessing a file named timeseries_export.nc is written to the ``output`` subfolder.
17
+
18
+ Both the input and output nc files are expected to follow the FEWS format for
19
+ scalar data in a NetCDF file, i.e.:
20
+
21
+ - They must contain a variable with the station ids (location ids) which can
22
+ be recognized by the attribute `cf_role` set to `timeseries_id`.
23
+ - They must contain a time variable with attributes `standard_name` = `time`
24
+ and `axis` = `T`
25
+
26
+ From the input file, all 2-D (or 3-D in case of ensembles) variables with dimensions equal
27
+ to the station ids and time variable (and realization) are read.
28
+
29
+ To map the NetCDF parameter identifier to and from an RTC-Tools variable name,
30
+ the overridable methods :py:meth:`netcdf_id_to_variable` and
31
+ :py:meth:`netcdf_id_from_variable` are used.
32
+
33
+ :cvar netcdf_validate_timeseries:
34
+ Check consistency of timeseries. Default is ``True``
35
+ """
36
+
37
+ #: Check consistency of timeseries.
38
+ netcdf_validate_timeseries = True
39
+
40
+ def netcdf_id_to_variable(self, station_id: str, parameter: str) -> str:
41
+ """
42
+ Maps the station_id and the parameter name to the variable name to be
43
+ used in RTC-Tools.
44
+
45
+ :return: The variable name used in RTC-Tools
46
+ """
47
+ return "{}__{}".format(station_id, parameter)
48
+
49
+ def netcdf_id_from_variable(self, variable_name: str) -> Tuple[str, str]:
50
+ """
51
+ Maps the variable name in RTC-Tools to a station_id and parameter name
52
+ for writing to a NetCDF file.
53
+
54
+ :return: A pair of station_id and parameter
55
+ """
56
+ return variable_name.split("__")
57
+
58
+ def read(self):
59
+ # Call parent class first for default behaviour
60
+ super().read()
61
+
62
+ dataset = netcdf.ImportDataset(self._input_folder, self.timeseries_import_basename)
63
+ # Although they are not used outside of this method, we add some
64
+ # variables to self for debugging purposes
65
+ self.__timeseries_import = dataset
66
+
67
+ # store the import times
68
+ times = self.__timeseries_times = dataset.read_import_times()
69
+ self.io.reference_datetime = self.__timeseries_times[0]
70
+
71
+ # Timestamp check
72
+ self.__dt = times[1] - times[0] if len(times) >= 2 else 0
73
+ for i in range(len(times) - 1):
74
+ if times[i + 1] - times[i] != self.__dt:
75
+ self.__dt = None
76
+ break
77
+
78
+ if self.netcdf_validate_timeseries:
79
+ # check if strictly increasing
80
+ for i in range(len(times) - 1):
81
+ if times[i] >= times[i + 1]:
82
+ raise Exception("NetCDFMixin: Time stamps must be strictly increasing.")
83
+
84
+ # store the station data for later use
85
+ self.__stations = dataset.read_station_data()
86
+ # read all available timeseries from the dataset
87
+ timeseries_var_keys = dataset.find_timeseries_variables()
88
+
89
+ for parameter in timeseries_var_keys:
90
+ for i, station_id in enumerate(self.__stations.station_ids):
91
+ name = self.netcdf_id_to_variable(station_id, parameter)
92
+
93
+ if dataset.ensemble_member_variable is not None:
94
+ if dataset.ensemble_member_variable.dimensions[
95
+ 0
96
+ ] in dataset.variable_dimensions(parameter):
97
+ for ensemble_member_index in range(self.__timeseries_import.ensemble_size):
98
+ values = dataset.read_timeseries_values(
99
+ i, parameter, ensemble_member_index
100
+ )
101
+ self.io.set_timeseries(
102
+ name, self.__timeseries_times, values, ensemble_member_index
103
+ )
104
+ else:
105
+ values = dataset.read_timeseries_values(i, parameter, 0)
106
+ for ensemble_member_index in range(self.__timeseries_import.ensemble_size):
107
+ self.io.set_timeseries(
108
+ name, self.__timeseries_times, values, ensemble_member_index
109
+ )
110
+ else:
111
+ values = dataset.read_timeseries_values(i, parameter, 0)
112
+ self.io.set_timeseries(name, self.__timeseries_times, values, 0)
113
+
114
+ logger.debug(
115
+ 'Read timeseries data for station id "{}" and parameter "{}", '
116
+ 'stored under variable name "{}"'.format(station_id, parameter, name)
117
+ )
118
+
119
+ logger.debug("NetCDFMixin: Read timeseries")
120
+
121
+ def write(self):
122
+ # Call parent class first for default behaviour
123
+ super().write()
124
+
125
+ dataset = netcdf.ExportDataset(self._output_folder, self.timeseries_export_basename)
126
+
127
+ times = [(dt - self.__timeseries_times[0]).seconds for dt in self.__timeseries_times]
128
+ dataset.write_times(times, self.initial_time, self.io.reference_datetime)
129
+
130
+ output_variables = [sym.name() for sym in self.output_variables]
131
+
132
+ output_station_ids, output_parameter_ids = zip(
133
+ *(self.netcdf_id_from_variable(var_name) for var_name in output_variables)
134
+ )
135
+
136
+ # Make sure that output_station_ids and output_parameter_ids are
137
+ # unique, but make sure to avoid non-deterministic ordering.
138
+ unique_station_ids = list(OrderedDict.fromkeys(output_station_ids))
139
+ unique_parameter_ids = list(OrderedDict.fromkeys(output_parameter_ids))
140
+
141
+ dataset.write_station_data(self.__stations, unique_station_ids)
142
+ dataset.write_ensemble_data(self.ensemble_size)
143
+
144
+ dataset.create_variables(unique_parameter_ids, self.ensemble_size)
145
+
146
+ for ensemble_member in range(self.ensemble_size):
147
+ results = self.extract_results(ensemble_member)
148
+
149
+ for var_name, station_id, parameter_id in zip(
150
+ output_variables, output_station_ids, output_parameter_ids
151
+ ):
152
+ # determine the output values
153
+ try:
154
+ values = results[var_name]
155
+ if len(values) != len(times):
156
+ values = self.interpolate(
157
+ times, self.times(var_name), values, self.interpolation_method(var_name)
158
+ )
159
+ except KeyError:
160
+ try:
161
+ ts = self.get_timeseries(var_name, ensemble_member)
162
+ if len(ts.times) != len(times):
163
+ values = self.interpolate(times, ts.times, ts.values)
164
+ else:
165
+ values = ts.values
166
+ except KeyError:
167
+ logger.error(
168
+ "NetCDFMixin: Output requested for non-existent variable {}. "
169
+ "Will not be in output file.".format(var_name)
170
+ )
171
+ continue
172
+
173
+ dataset.write_output_values(
174
+ station_id, parameter_id, ensemble_member, values, self.ensemble_size
175
+ )
176
+
177
+ dataset.close()