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,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()
|