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,1293 @@
1
+ import copy
2
+ import importlib.resources
3
+ import itertools
4
+ import logging
5
+ import math
6
+ import sys
7
+ from collections import OrderedDict
8
+ from typing import List, Union
9
+
10
+ # Python 3.9's importlib.metadata does not support the "group" parameter to
11
+ # entry_points yet.
12
+ if sys.version_info < (3, 10):
13
+ import importlib_metadata
14
+ else:
15
+ from importlib import metadata as importlib_metadata
16
+
17
+ import casadi as ca
18
+ import numpy as np
19
+ import pymoca
20
+ import pymoca.backends.casadi.api
21
+
22
+ from rtctools._internal.alias_tools import AliasDict
23
+ from rtctools._internal.caching import cached
24
+ from rtctools._internal.debug_check_helpers import DebugLevel
25
+ from rtctools.data.storage import DataStoreAccessor
26
+
27
+ logger = logging.getLogger("rtctools")
28
+
29
+
30
+ class Variable:
31
+ """
32
+ Modeled after the Variable class in pymoca.backends.casadi.model, with modifications to make it
33
+ easier for the common case in RTC-Tools to instantiate them.
34
+
35
+ That means:
36
+ - pass in name instead of ca.MX symbol
37
+ - only scalars are allowed (shape = (1, 1))
38
+ - no aliases
39
+ - no "python_type"
40
+ - able to specify nominal/min/max in constructor
41
+ """
42
+
43
+ def __init__(self, name, /, min=-np.inf, max=np.inf, nominal=1.0):
44
+ self.name = name
45
+ self.min = min
46
+ self.max = max
47
+ self.nominal = nominal
48
+ self._symbol = ca.MX.sym(name)
49
+
50
+ @property
51
+ def symbol(self):
52
+ return self._symbol
53
+
54
+
55
+ class SimulationProblem(DataStoreAccessor):
56
+ """
57
+ Implements the `BMI <http://csdms.colorado.edu/wiki/BMI_Description>`_ Interface.
58
+
59
+ Base class for all Simulation problems. Loads the Modelica Model.
60
+
61
+ :cvar modelica_library_folders: Folders containing any referenced Modelica libraries. Default
62
+ is an empty list.
63
+
64
+ """
65
+
66
+ _debug_check_level = DebugLevel.MEDIUM
67
+ _debug_check_options = {}
68
+
69
+ # Folders in which the referenced Modelica libraries are found
70
+ modelica_library_folders = []
71
+
72
+ # Force workaround for delay support by assuming zero delay. This flag
73
+ # will be removed when proper delay support is added.
74
+ _force_zero_delay = False
75
+
76
+ def __init__(self, **kwargs):
77
+ # Check arguments
78
+ assert "model_folder" in kwargs
79
+
80
+ # Log pymoca version
81
+ logger.debug("Using pymoca {}.".format(pymoca.__version__))
82
+
83
+ # Transfer model from the Modelica .mo file to CasADi using pymoca
84
+ if "model_name" in kwargs:
85
+ model_name = kwargs["model_name"]
86
+ else:
87
+ if hasattr(self, "model_name"):
88
+ model_name = self.model_name
89
+ else:
90
+ model_name = self.__class__.__name__
91
+
92
+ # Load model from pymoca backend
93
+ compiler_options = self.compiler_options()
94
+ logger.info(f"Loading/compiling model {model_name}.")
95
+ try:
96
+ self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
97
+ kwargs["model_folder"], model_name, compiler_options
98
+ )
99
+ except RuntimeError as error:
100
+ if compiler_options.get("cache", False):
101
+ raise error
102
+ compiler_options["cache"] = False
103
+ logger.warning(f"Loading model {model_name} using a cache file failed: {error}.")
104
+ logger.info(f"Compiling model {model_name}.")
105
+ self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
106
+ kwargs["model_folder"], model_name, compiler_options
107
+ )
108
+
109
+ # Extract the CasADi MX variables used in the model
110
+ self.__mx = {}
111
+ self.__mx["time"] = [self.__pymoca_model.time]
112
+ self.__mx["states"] = [v.symbol for v in self.__pymoca_model.states]
113
+ self.__mx["derivatives"] = [v.symbol for v in self.__pymoca_model.der_states]
114
+ self.__mx["algebraics"] = [v.symbol for v in self.__pymoca_model.alg_states]
115
+ self.__mx["parameters"] = [v.symbol for v in self.__pymoca_model.parameters]
116
+ self.__mx["constant_inputs"] = []
117
+ self.__mx["lookup_tables"] = []
118
+
119
+ for v in self.__pymoca_model.inputs:
120
+ if v.symbol.name() in self.__pymoca_model.delay_states:
121
+ # Delayed feedback variables are local to each ensemble, and
122
+ # therefore belong to the collection of algebraic variables,
123
+ # rather than to the control inputs.
124
+ self.__mx["algebraics"].append(v.symbol)
125
+ else:
126
+ if v.symbol.name() in kwargs.get("lookup_tables", []):
127
+ self.__mx["lookup_tables"].append(v.symbol)
128
+ else:
129
+ # All inputs are constant inputs
130
+ self.__mx["constant_inputs"].append(v.symbol)
131
+
132
+ # Set timestep size
133
+ self._dt_is_fixed = False
134
+ self.__dt = None
135
+ fixed_dt = kwargs.get("fixed_dt", None)
136
+ if fixed_dt is not None:
137
+ self._dt_is_fixed = True
138
+ self.__dt = fixed_dt
139
+
140
+ # Add auxiliary variables for keeping track of delay expressions to the algebraic states
141
+ n_delay_states = len(self.__pymoca_model.delay_states)
142
+ self.__delay_times = []
143
+ if n_delay_states > 0:
144
+ if fixed_dt is None and not self._force_zero_delay:
145
+ raise ValueError("fixed_dt should be set when using delay equations.")
146
+ self.__delay_times = self._get_delay_times()
147
+ delay_expression_states = self._create_delay_expression_states()
148
+ self.__mx["algebraics"] += delay_expression_states
149
+
150
+ # Log variables in debug mode
151
+ if logger.getEffectiveLevel() == logging.DEBUG:
152
+ logger.debug(
153
+ "SimulationProblem: Found states {}".format(
154
+ ", ".join([var.name() for var in self.__mx["states"]])
155
+ )
156
+ )
157
+ logger.debug(
158
+ "SimulationProblem: Found derivatives {}".format(
159
+ ", ".join([var.name() for var in self.__mx["derivatives"]])
160
+ )
161
+ )
162
+ logger.debug(
163
+ "SimulationProblem: Found algebraics {}".format(
164
+ ", ".join([var.name() for var in self.__mx["algebraics"]])
165
+ )
166
+ )
167
+ logger.debug(
168
+ "SimulationProblem: Found constant inputs {}".format(
169
+ ", ".join([var.name() for var in self.__mx["constant_inputs"]])
170
+ )
171
+ )
172
+ logger.debug(
173
+ "SimulationProblem: Found parameters {}".format(
174
+ ", ".join([var.name() for var in self.__mx["parameters"]])
175
+ )
176
+ )
177
+
178
+ # Get the extra variables that are user defined
179
+ self.__extra_variables = self.extra_variables()
180
+ self.__extra_variables_symbols = [v.symbol for v in self.__extra_variables]
181
+
182
+ # Store the types in an AliasDict
183
+ self.__python_types = AliasDict(self.alias_relation)
184
+ model_variable_types = [
185
+ "states",
186
+ "der_states",
187
+ "alg_states",
188
+ "inputs",
189
+ "constants",
190
+ "parameters",
191
+ ]
192
+ for t in model_variable_types:
193
+ for v in getattr(self.__pymoca_model, t):
194
+ self.__python_types[v.symbol.name()] = v.python_type
195
+
196
+ # Store the nominals in an AliasDict
197
+ self.__nominals = AliasDict(self.alias_relation)
198
+ for v in itertools.chain(self.__pymoca_model.states, self.__pymoca_model.alg_states):
199
+ sym_name = v.symbol.name()
200
+
201
+ # If the nominal is 0.0 or 1.0 or -1.0, ignore: get_variable_nominal returns a default
202
+ # of 1.0
203
+ # TODO: handle nominal vectors (update() will need to load them)
204
+ if (
205
+ ca.MX(v.nominal).is_zero()
206
+ or ca.MX(v.nominal - 1).is_zero()
207
+ or ca.MX(v.nominal + 1).is_zero()
208
+ ):
209
+ continue
210
+ else:
211
+ if ca.MX(v.nominal).size1() != 1:
212
+ logger.error("Vector Nominals not supported yet. ({})".format(sym_name))
213
+ self.__nominals[sym_name] = ca.fabs(v.nominal)
214
+ if logger.getEffectiveLevel() == logging.DEBUG:
215
+ logger.debug(
216
+ "SimulationProblem: Setting nominal value for variable {} to {}".format(
217
+ sym_name, self.__nominals[sym_name]
218
+ )
219
+ )
220
+
221
+ for v in self.__extra_variables:
222
+ self.__nominals[v.name] = v.nominal
223
+
224
+ # Initialize DAE and initial residuals
225
+ variable_lists = ["states", "der_states", "alg_states", "inputs", "constants", "parameters"]
226
+ function_arguments = [self.__pymoca_model.time] + [
227
+ ca.veccat(*[v.symbol for v in getattr(self.__pymoca_model, variable_list)])
228
+ for variable_list in variable_lists
229
+ ]
230
+
231
+ self.__dae_residual = self.__pymoca_model.dae_residual_function(*function_arguments)
232
+
233
+ if self.__dae_residual is None:
234
+ # DAE is empty, that can happen if we add the only (non-aliasing) equations
235
+ # in Python.
236
+ self.__dae_residual = ca.MX()
237
+
238
+ self.__initial_residual = self.__pymoca_model.initial_residual_function(*function_arguments)
239
+ if self.__initial_residual is None:
240
+ self.__initial_residual = ca.MX()
241
+
242
+ # Construct state vector
243
+ self.__sym_list = (
244
+ self.__mx["states"]
245
+ + self.__mx["algebraics"]
246
+ + self.__mx["derivatives"]
247
+ + self.__extra_variables_symbols
248
+ + self.__mx["time"]
249
+ + self.__mx["constant_inputs"]
250
+ + self.__mx["parameters"]
251
+ )
252
+ n_elements = np.array([var.numel() for var in self.__sym_list])
253
+ i_end = n_elements.cumsum()
254
+ i_start = np.array([0, *(i_end[:-1])])
255
+ self.__state_vector = np.full(n_elements.sum(), np.nan)
256
+
257
+ # A very handy index
258
+ self.__n_state_symbols = (
259
+ len(self.__mx["states"])
260
+ + len(self.__mx["algebraics"])
261
+ + len(self.__mx["derivatives"])
262
+ + len(self.__extra_variables)
263
+ )
264
+ self.__n_states = i_end[self.__n_state_symbols - 1]
265
+
266
+ # NOTE: Backwards compatibility allowing set_var() for parameters. These
267
+ # variables check that this is only done before calling initialize().
268
+ self.__parameters = AliasDict(self.alias_relation)
269
+ self.__parameters.update({v.name(): v for v in self.__mx["parameters"]})
270
+ self.__parameters_set_var = True
271
+
272
+ # Construct a dict to look up symbols by name (or iterate over)
273
+ self.__sym_dict = OrderedDict(((sym.name(), sym) for sym in self.__sym_list))
274
+
275
+ # Generate a dictionary that we can use to lookup the index in the state vector.
276
+ # To avoid repeated and relatively expensive `canonical_signed` calls, we
277
+ # make a dictionary for all variables and their aliases.
278
+ self.__i_start = {}
279
+ self.__i_end = {}
280
+ for i, k in enumerate(self.__sym_dict.keys()):
281
+ for alias in self.alias_relation.aliases(k):
282
+ if alias.startswith("-"):
283
+ self.__i_start[alias[1:]] = (i_start[i], -1.0)
284
+ self.__i_end[alias[1:]] = (i_end[i], -1.0)
285
+ else:
286
+ self.__i_start[alias] = (i_start[i], 1.0)
287
+ self.__i_end[alias] = (i_end[i], 1.0)
288
+ self.__indices = self.__i_start
289
+
290
+ # Call parent class for default behaviour.
291
+ super().__init__(**kwargs)
292
+
293
+ def _get_delay_times(self):
294
+ """
295
+ Get the delay times for each delay equation.
296
+ """
297
+ if self._force_zero_delay:
298
+ return [0] * len(self.__pymoca_model.delay_states)
299
+ parameter_symbols = [v.symbol for v in self.__pymoca_model.parameters]
300
+ parameter_values = [v.value for v in self.__pymoca_model.parameters]
301
+ delay_time_expressions = [
302
+ delay_arg.duration for delay_arg in self.__pymoca_model.delay_arguments
303
+ ]
304
+ delay_time_fun = ca.Function(
305
+ "delay_time_function", parameter_symbols, delay_time_expressions
306
+ )
307
+ delay_time_values = delay_time_fun(*parameter_values)
308
+ if len(delay_time_expressions) == 1:
309
+ return [delay_time_values]
310
+ return list(delay_time_values)
311
+
312
+ def _create_delay_expression_states(self):
313
+ """
314
+ Create auxiliary states for delay equations.
315
+
316
+ Create states to keep track of the history of delay expressions.
317
+ For example, if we have a delay equation of the form
318
+
319
+ .. math::
320
+ x = delay(5 * y, 2 * dt),
321
+
322
+ Then we need variables to store :math:`5 * y` at time :math`t` and time :math:`t - dt`
323
+ (For each state, we also store the previous value,
324
+ so if we have a state for :math:`5 * y` at :math:`t - dt`,
325
+ then its previous value is :math:`5 * y` at :math:`t - 2 * dt`).
326
+ """
327
+ delay_expression_states = []
328
+ for delay_state, delay_time in zip(self.__pymoca_model.delay_states, self.__delay_times):
329
+ if delay_time > 0:
330
+ n_previous_values = int(np.ceil(delay_time / self.get_time_step()))
331
+ else:
332
+ n_previous_values = 1
333
+ expression_state = delay_state + "_expr"
334
+ expression_symbol = ca.MX.sym(expression_state, n_previous_values)
335
+ delay_expression_states.append(expression_symbol)
336
+ return delay_expression_states
337
+
338
+ def initialize(self, config_file=None):
339
+ """
340
+ Initialize state vector with default values
341
+
342
+ Initial values are first read from the given Modelica files.
343
+ If an initial value equals zero or is not provided by a Modelica file,
344
+ and the variable is not marked as fixed,
345
+ then the initial value is tried to be set with the initial_state method.
346
+ When using CSVMixin, this method by default looks for initial values
347
+ in an initial_state.csv file.
348
+ Furthermore, if a variable is not marked as fixed
349
+ and no initial value is given by the initial_state method,
350
+ the initial value can be overwritten using the seed method.
351
+ When a variable is marked as fixed, the initial value is only read from the Modelica file.
352
+
353
+ :param config_file: Path to an initialization file.
354
+ """
355
+ if config_file:
356
+ # TODO read start and stop time from config_file and call:
357
+ # self.setup_experiment(start,stop)
358
+ # for now, assume that setup_experiment was called beforehand
359
+ raise NotImplementedError
360
+
361
+ # Short-hand notation for the model
362
+ model = self.__pymoca_model
363
+
364
+ # Set values of parameters defined in the model into the state vector
365
+ for var in self.__pymoca_model.parameters:
366
+ # First check to see if parameter is already set (this allows child classes to override
367
+ # model defaults)
368
+ if np.isfinite(self.get_var(var.symbol.name())):
369
+ continue
370
+
371
+ # Also test to see if the value is constant
372
+ if isinstance(var.value, ca.MX) and not var.value.is_constant():
373
+ continue
374
+
375
+ # Try to extract the value
376
+ try:
377
+ # Extract the value as a python type
378
+ val = var.python_type(var.value)
379
+ except ValueError:
380
+ # var.value is a float NaN being cast to non-float
381
+ continue
382
+ else:
383
+ # If val is finite, we set it
384
+ if np.isfinite(val):
385
+ logger.debug(
386
+ "SimulationProblem: Setting parameter {} = {}".format(
387
+ var.symbol.name(), val
388
+ )
389
+ )
390
+ self.set_var(var.symbol.name(), val)
391
+
392
+ # Nominals can be symbolic, written in terms of parameters. After all
393
+ # parameter values are known, we evaluate the numeric values of the
394
+ # nominals.
395
+ nominal_vars = list(self.__nominals.keys())
396
+ symbolic_nominals = ca.vertcat(*[self.get_variable_nominal(v) for v in nominal_vars])
397
+ nominal_evaluator = ca.Function(
398
+ "nominal_evaluator", self.__mx["parameters"], [symbolic_nominals]
399
+ )
400
+
401
+ n_parameters = len(self.__mx["parameters"])
402
+ if n_parameters > 0:
403
+ [evaluated_nominals] = nominal_evaluator.call(self.__state_vector[-n_parameters:])
404
+ else:
405
+ [evaluated_nominals] = nominal_evaluator.call([])
406
+
407
+ evaluated_nominals = np.array(evaluated_nominals).ravel()
408
+
409
+ nominal_dict = dict(zip(nominal_vars, evaluated_nominals))
410
+
411
+ self.__nominals.update(nominal_dict)
412
+
413
+ # The variables that need a mutually consistent initial condition
414
+ X = ca.vertcat(*self.__sym_list[: self.__n_state_symbols])
415
+ X_prev = ca.vertcat(
416
+ *[
417
+ ca.MX.sym(sym.name() + "_prev", sym.shape)
418
+ for sym in self.__sym_list[: self.__n_state_symbols]
419
+ ]
420
+ )
421
+
422
+ # Assemble initial residuals and set values from start attributes into the state vector
423
+ minimized_residuals = []
424
+ for var in itertools.chain(self.__pymoca_model.states, self.__pymoca_model.alg_states):
425
+ var_name = var.symbol.name()
426
+ var_nominal = self.get_variable_nominal(var_name)
427
+ start_values = {}
428
+
429
+ # Attempt to cast var.start to python type
430
+ mx_start = ca.MX(var.start)
431
+ if mx_start.is_constant():
432
+ # cast var.start to python type
433
+ start_value_pymoca = var.python_type(mx_start.to_DM())
434
+ if start_value_pymoca is not None and start_value_pymoca != 0:
435
+ start_values["modelica"] = start_value_pymoca
436
+ else:
437
+ start_values["modelica"] = mx_start
438
+
439
+ if not var.fixed:
440
+ # To make initialization easier, we allow setting initial states by providing
441
+ # timeseries with names that match a symbol in the model. We only check for this
442
+ # matching if the start and fixed attributes were left as default
443
+ try:
444
+ start_values["initial_state"] = self.initial_state()[var_name]
445
+ except KeyError:
446
+ pass
447
+ else:
448
+ # An initial state was found- add it to the constrained residuals
449
+ logger.debug(
450
+ "Initialize: Added {} = {} to initial equations "
451
+ "(found matching timeseries).".format(
452
+ var_name, start_values["initial_state"]
453
+ )
454
+ )
455
+ # Set var to be fixed
456
+ var.fixed = True
457
+
458
+ if not var.fixed:
459
+ # To make initialization easier, we allow setting initial guesses by providing
460
+ # timeseries with names that match a symbol in the model. We only check for this
461
+ # matching if the start and fixed attributes were left as default
462
+ try:
463
+ start_values["seed"] = self.seed()[var_name]
464
+ except KeyError:
465
+ pass
466
+ else:
467
+ # An initial state was found- add it to the constrained residuals
468
+ logger.debug(
469
+ "Initialize: Added {} = {} as initial guess "
470
+ "(found matching timeseries).".format(var_name, start_values["seed"])
471
+ )
472
+
473
+ # Set the start value based on the different inputs.
474
+ if "seed" in start_values:
475
+ input_source = "seed"
476
+ source_description = "seed method"
477
+ elif "modelica" in start_values:
478
+ input_source = "modelica"
479
+ source_description = "modelica file"
480
+ elif "initial_state" in start_values:
481
+ input_source = "initial_state"
482
+ source_description = "initial_state method (typically reads initial_state.csv)"
483
+ else:
484
+ start_values["modelica"] = start_value_pymoca
485
+ input_source = "modelica"
486
+ source_description = "modelica file or default value"
487
+ start_val = start_values.get(input_source, None)
488
+ start_is_numeric = start_val is not None and not isinstance(start_val, ca.MX)
489
+ numeric_start_val = start_val if start_is_numeric else 0.0
490
+ if len(start_values) > 1:
491
+ logger.warning(
492
+ "Initialize: Multiple initial values for {} are provided: {}.".format(
493
+ var_name, start_values
494
+ )
495
+ + " Value from {} will be used to continue.".format(source_description)
496
+ )
497
+
498
+ # Attempt to set start_val in the state vector. Default to zero if unknown.
499
+ try:
500
+ self.set_var(var_name, numeric_start_val)
501
+ except KeyError:
502
+ logger.warning(
503
+ "Initialize: {} not found in state vector. Initial value of {} not set.".format(
504
+ var_name, numeric_start_val
505
+ )
506
+ )
507
+
508
+ # Add a residual for the difference between the state and its starting expression
509
+ start_expr = start_val
510
+ min_is_symbolic = isinstance(var.min, ca.MX)
511
+ max_is_symbolic = isinstance(var.max, ca.MX)
512
+ if var.fixed:
513
+ # Set bounds to be equal to each other, such that IPOPT can
514
+ # turn the decision variable into a parameter.
515
+ if min_is_symbolic or max_is_symbolic or var.min != -np.inf or var.max != np.inf:
516
+ logger.info(
517
+ "Initialize: bounds of {} will be overwritten".format(var_name)
518
+ + " by the start value given by {}.".format(source_description)
519
+ )
520
+ var.min = start_expr
521
+ var.max = start_expr
522
+ else:
523
+ # minimize residual
524
+ minimized_residuals.append((var.symbol - start_expr) / var_nominal)
525
+
526
+ # Check that the start_value is in between the variable bounds.
527
+ if start_is_numeric and not min_is_symbolic and not max_is_symbolic:
528
+ if not (var.min <= start_val and start_val <= var.max):
529
+ logger.log(
530
+ (
531
+ logging.WARNING
532
+ if source_description != "modelica file or default value"
533
+ else logging.DEBUG
534
+ ),
535
+ f"Initialize: start value {var_name} = {start_val} "
536
+ f"is not in between bounds {var.min} and {var.max} and will be adjusted.",
537
+ )
538
+
539
+ # Default start var for ders is zero
540
+ for der_var in self.__mx["derivatives"]:
541
+ self.set_var(der_var.name(), 0.0)
542
+
543
+ # Residuals for initial values for the delay states / expressions.
544
+ for delay_state, delay_argument in zip(model.delay_states, model.delay_arguments):
545
+ expression_state = delay_state + "_expr"
546
+ i_delay_state, _ = self.__indices[delay_state]
547
+ i_expr_start, _ = self.__i_start[expression_state]
548
+ i_expr_end, _ = self.__i_end[expression_state]
549
+ minimized_residuals.append(X[i_expr_start:i_expr_end] - delay_argument.expr)
550
+ minimized_residuals.append(X[i_delay_state] - delay_argument.expr)
551
+
552
+ # Warn for nans in state vector (verify we didn't miss anything)
553
+ self.__warn_for_nans()
554
+
555
+ # Optionally encourage a steady-state initial condition
556
+ if getattr(self, "encourage_steady_state_initial_conditions", False):
557
+ # add penalty for der(var) != 0.0
558
+ for d in self.__mx["derivatives"]:
559
+ logger.debug("Added {} to the minimized residuals.".format(d.name()))
560
+ minimized_residuals.append(d)
561
+
562
+ # Make minimized_residuals into a single symbolic object
563
+ if minimized_residuals:
564
+ minimized_residual = ca.vertcat(*minimized_residuals)
565
+ else:
566
+ # DAE is empty
567
+ minimized_residual = ca.MX(0)
568
+
569
+ # Extra equations
570
+ extra_equations = self.extra_equations()
571
+
572
+ # Assemble symbolics needed to make a function describing the initial condition of the model
573
+ # We constrain every entry in this MX to zero
574
+ equality_constraints = ca.vertcat(
575
+ self.__dae_residual, self.__initial_residual, *extra_equations
576
+ )
577
+
578
+ # Make a list of unscaled symbols and a list of their scaled equivalent
579
+ unscaled_symbols = []
580
+ scaled_symbols = []
581
+ for sym_name, nominal in self.__nominals.items():
582
+ # Note that sym_name is always a canonical state
583
+ index, _ = self.__indices[sym_name]
584
+
585
+ # If the symbol is a state, Add the symbol to the lists
586
+ if index <= self.__n_states:
587
+ unscaled_symbols.append(X[index])
588
+ scaled_symbols.append(X[index] * nominal)
589
+
590
+ # Also scale previous states
591
+ unscaled_symbols.append(X_prev[index])
592
+ scaled_symbols.append(X_prev[index] * nominal)
593
+
594
+ unscaled_symbols = ca.vertcat(*unscaled_symbols)
595
+ scaled_symbols = ca.vertcat(*scaled_symbols)
596
+
597
+ # Substitute unscaled terms for scaled terms
598
+ equality_constraints = ca.substitute(equality_constraints, unscaled_symbols, scaled_symbols)
599
+ minimized_residual = ca.substitute(minimized_residual, unscaled_symbols, scaled_symbols)
600
+
601
+ logger.debug("SimulationProblem: Initial Equations are " + str(equality_constraints))
602
+ logger.debug("SimulationProblem: Minimized Residuals are " + str(minimized_residual))
603
+
604
+ # State bounds can be symbolic, written in terms of parameters. After all
605
+ # parameter values are known, we evaluate the numeric values of bounds.
606
+ bound_vars = (
607
+ self.__pymoca_model.states
608
+ + self.__pymoca_model.alg_states
609
+ + self.__pymoca_model.der_states
610
+ + self.__extra_variables
611
+ )
612
+
613
+ symbolic_bounds = ca.vertcat(*[ca.horzcat(v.min, v.max) for v in bound_vars])
614
+ bound_evaluator = ca.Function("bound_evaluator", self.__mx["parameters"], [symbolic_bounds])
615
+
616
+ # Evaluate bounds using values of parameters
617
+ n_parameters = len(self.__mx["parameters"])
618
+ if n_parameters > 0:
619
+ [evaluated_bounds] = bound_evaluator.call(self.__state_vector[-n_parameters:])
620
+ else:
621
+ [evaluated_bounds] = bound_evaluator.call([])
622
+
623
+ # Scale the bounds with the nominals
624
+ nominals = []
625
+ for var in bound_vars:
626
+ nominals.append(self.get_variable_nominal(var.symbol.name()))
627
+
628
+ evaluated_bounds = np.array(evaluated_bounds) / np.array(nominals)[:, None]
629
+
630
+ # Update with the bounds of delayed states / expressions
631
+ if model.delay_states:
632
+ i_start_first_delay_state, _ = self.__indices[model.delay_states[0]]
633
+ i_end_last_delay_expr, _ = self.__i_end[model.delay_states[-1] + "_expr"]
634
+ n_delay = i_end_last_delay_expr - i_start_first_delay_state
635
+ delay_bounds = np.array([-np.inf, np.inf] * n_delay).reshape((n_delay, 2))
636
+ # offset = len(self.__pymoca_model.states) + len(self.__pymoca_model.alg_states)
637
+ offset = i_start_first_delay_state
638
+ evaluated_bounds = np.vstack(
639
+ (evaluated_bounds[:offset, :], delay_bounds, evaluated_bounds[offset:, :])
640
+ )
641
+
642
+ # Construct arrays of state bounds (used in the initialize() nlp, but not in __do_step
643
+ # rootfinder)
644
+ self.__lbx = evaluated_bounds[:, 0]
645
+ self.__ubx = evaluated_bounds[:, 1]
646
+
647
+ # Constrain model equation residuals to zero
648
+ lbg = np.zeros(equality_constraints.size1())
649
+ ubg = np.zeros(equality_constraints.size1())
650
+
651
+ # Construct objective function from the input residual
652
+ objective_function = ca.dot(minimized_residual, minimized_residual)
653
+
654
+ # Substitute constants and parameters
655
+ const_and_par = ca.vertcat(
656
+ *self.__mx["time"], *self.__mx["constant_inputs"], *self.__mx["parameters"]
657
+ )
658
+ const_and_par_values = self.__state_vector[self.__n_states :]
659
+
660
+ objective_function = ca.substitute(objective_function, const_and_par, const_and_par_values)
661
+ equality_constraints = ca.substitute(
662
+ equality_constraints, const_and_par, const_and_par_values
663
+ )
664
+
665
+ # Construct nlp and solver to find initial state using ipopt
666
+ # Note that some operations cannot be expanded, e.g. ca.interpolant. So
667
+ # we _try_ to expand, but fall back on ca.MX evaluation if we cannot.
668
+ try:
669
+ expand_f_g = ca.Function("f", [X], [objective_function, equality_constraints]).expand()
670
+ except RuntimeError as e:
671
+ if "eval_sx" not in str(e):
672
+ raise
673
+ else:
674
+ logger.info(
675
+ "Cannot expand objective/constraints to SX, falling back to MX evaluation"
676
+ )
677
+ nlp = {"x": X, "f": objective_function, "g": equality_constraints}
678
+ else:
679
+ X_sx = ca.SX.sym("X", X.shape)
680
+ objective_function_sx, equality_constraints_sx = expand_f_g(X_sx)
681
+ nlp = {"x": X_sx, "f": objective_function_sx, "g": equality_constraints_sx}
682
+
683
+ solver = ca.nlpsol("solver", "ipopt", nlp, self.solver_options())
684
+
685
+ # Construct guess
686
+ guess = ca.vertcat(*np.nan_to_num(self.__state_vector[: self.__n_states]))
687
+
688
+ # Find initial state
689
+ initial_state = solver(x0=guess, lbx=self.__lbx, ubx=self.__ubx, lbg=lbg, ubg=ubg)
690
+
691
+ # If unsuccessful, stop.
692
+ return_status = solver.stats()["return_status"]
693
+ if return_status not in {"Solve_Succeeded", "Solved_To_Acceptable_Level"}:
694
+ if return_status == "Infeasible_Problem_Detected":
695
+ message = (
696
+ "Initialization Failed with return status: {}. ".format(return_status)
697
+ + "This means no initial state could be found "
698
+ + "that satisfies all equations and constraints."
699
+ )
700
+ else:
701
+ message = "Initialization Failed with return status: {}. ".format(return_status)
702
+ raise Exception(message)
703
+
704
+ # Update state vector with initial conditions
705
+ self.__state_vector[: self.__n_states] = initial_state["x"][: self.__n_states].T
706
+
707
+ # make a copy of the initialized initial state vector in case we want to run the model again
708
+ self.__initialized_state_vector = copy.deepcopy(self.__state_vector)
709
+
710
+ # Warn for nans in state vector after initialization
711
+ self.__warn_for_nans()
712
+
713
+ # No longer allow setting parameters with set_var(), as we want to be
714
+ # clear that that does not work
715
+ self.__parameters_set_var = False
716
+
717
+ self.__parameter_names_including_aliases = set()
718
+ for p in self.__parameters.keys():
719
+ self.__parameter_names_including_aliases |= self.alias_relation.aliases(p)
720
+
721
+ # Construct the rootfinder
722
+
723
+ # Assemble some symbolics, including those needed for a backwards Euler derivative
724
+ # approximation
725
+ dt = ca.MX.sym("delta_t")
726
+ parameters = ca.vertcat(*self.__mx["parameters"])
727
+ if n_parameters > 0:
728
+ constants = ca.vertcat(X_prev, *self.__sym_list[self.__n_state_symbols : -n_parameters])
729
+ else:
730
+ constants = ca.vertcat(X_prev, *self.__sym_list[self.__n_state_symbols :])
731
+
732
+ # Make a list of derivative approximations using backwards Euler formulation
733
+ derivative_approximation_residuals = []
734
+ for index, derivative_state in enumerate(self.__mx["derivatives"]):
735
+ derivative_approximation_residuals.append(
736
+ derivative_state - (X[index] - X_prev[index]) / dt
737
+ )
738
+
739
+ # Delayed feedback
740
+ delay_equations = []
741
+ for delay_state, delay_argument, delay_time in zip(
742
+ model.delay_states,
743
+ model.delay_arguments,
744
+ self.__delay_times,
745
+ ):
746
+ expression_state = delay_state + "_expr"
747
+ i_delay_state, _ = self.__indices[delay_state]
748
+ i_expr_start, _ = self.__i_start[expression_state]
749
+ i_expr_end, _ = self.__i_end[expression_state]
750
+ delay_equations.append(X[i_expr_start] - delay_argument.expr)
751
+ delay_equations.append(
752
+ X[i_expr_start + 1 : i_expr_end] - X_prev[i_expr_start : i_expr_end - 1]
753
+ )
754
+ n_previous_values = self.__sym_dict[expression_state].numel()
755
+ interpolation_weight = n_previous_values - delay_time / self.__dt
756
+ delay_equations.append(
757
+ X[i_delay_state]
758
+ - interpolation_weight * X[i_expr_end - 1]
759
+ - (1 - interpolation_weight) * X_prev[i_expr_end - 1]
760
+ )
761
+
762
+ # Append residuals for derivative approximations
763
+ dae_residual = ca.vertcat(
764
+ self.__dae_residual,
765
+ *derivative_approximation_residuals,
766
+ *delay_equations,
767
+ *extra_equations,
768
+ )
769
+
770
+ # TODO: implement lookup_tables
771
+
772
+ # Substitute unscaled terms for scaled terms
773
+ dae_residual = ca.substitute(dae_residual, unscaled_symbols, scaled_symbols)
774
+
775
+ # Substitute the parameters
776
+ if n_parameters > 0:
777
+ parameters_values = self.__state_vector[-n_parameters:]
778
+ dae_residual = ca.substitute(dae_residual, parameters, parameters_values)
779
+
780
+ if logger.getEffectiveLevel() == logging.DEBUG:
781
+ logger.debug("SimulationProblem: DAE Residual is " + str(dae_residual))
782
+
783
+ if X.size1() != dae_residual.size1():
784
+ logger.error(
785
+ "Formulation Error: Number of states ({}) "
786
+ "does not equal number of equations ({})".format(X.size1(), dae_residual.size1())
787
+ )
788
+
789
+ # Construct a function res_vals that returns the numerical residuals of a numerical state
790
+ self.__res_vals = ca.Function("res_vals", [X, dt, constants], [dae_residual])
791
+ try:
792
+ self.__res_vals = self.__res_vals.expand()
793
+ except RuntimeError as e:
794
+ if "eval_sx" not in str(e):
795
+ raise
796
+ else:
797
+ pass
798
+
799
+ # Use rootfinder() to make a function that takes a step forward in time by trying to zero
800
+ # res_vals()
801
+ options = self.rootfinder_options()
802
+ solver = options["solver"]
803
+ solver_options = options["solver_options"]
804
+ self.__do_step = ca.rootfinder("next_state", solver, self.__res_vals, solver_options)
805
+
806
+ def pre(self):
807
+ """
808
+ Any preprocessing takes place here.
809
+ """
810
+ pass
811
+
812
+ def post(self):
813
+ """
814
+ Any postprocessing takes place here.
815
+ """
816
+ pass
817
+
818
+ def setup_experiment(self, start, stop, dt):
819
+ """
820
+ Method for subclasses (PIMixin, CSVMixin, or user classes) to set timing information for a
821
+ simulation run.
822
+
823
+ :param start: Start time for the simulation.
824
+ :param stop: Final time for the simulation.
825
+ :param dt: Time step size.
826
+ """
827
+
828
+ # Set class vars with start/stop/dt values
829
+ self.__start = start
830
+ self.__stop = stop
831
+ self.set_time_step(dt)
832
+
833
+ # Set time in state vector
834
+ self.set_var("time", start)
835
+
836
+ def update(self, dt):
837
+ """
838
+ Performs one timestep.
839
+
840
+ The methods ``setup_experiment`` and ``initialize`` must have been called before.
841
+
842
+ :param dt: Time step size.
843
+ """
844
+ if dt > 0:
845
+ self.set_time_step(dt)
846
+ dt = self.get_time_step()
847
+
848
+ logger.debug("Taking a step at {} with size {}".format(self.get_current_time(), dt))
849
+
850
+ # increment time
851
+ self.set_var("time", self.get_current_time() + dt)
852
+
853
+ # take a step
854
+ if np.isnan(self.__state_vector).any():
855
+ logger.error("Found a nan in the state vector (before making the step)")
856
+ guess = self.__state_vector[: self.__n_states]
857
+ if len(self.__mx["parameters"]) > 0:
858
+ next_state = self.__do_step(
859
+ guess, dt, self.__state_vector[: -len(self.__mx["parameters"])]
860
+ )
861
+ else:
862
+ next_state = self.__do_step(guess, dt, self.__state_vector)
863
+
864
+ try:
865
+ if np.isnan(next_state).any():
866
+ index_to_name = {index[0]: name for name, index in self.__indices.items()}
867
+ named_next_state = {
868
+ index_to_name[i]: float(next_state[i]) for i in range(0, next_state.shape[0])
869
+ }
870
+ variables_with_nan = [
871
+ name for name, value in named_next_state.items() if np.isnan(value)
872
+ ]
873
+ if variables_with_nan:
874
+ logger.error(
875
+ f"Found nan(s) in the next_state vector for:\n\t {variables_with_nan}"
876
+ )
877
+ except (KeyError, IndexError, TypeError):
878
+ logger.warning("Something went wrong while checking for nans in the next_state vector")
879
+
880
+ # Check convergence of rootfinder
881
+ rootfinder_stats = self.__do_step.stats()
882
+
883
+ if not rootfinder_stats["success"]:
884
+ message = (
885
+ "Simulation has failed to converge at time {}. Solver failed with status {}"
886
+ ).format(self.get_current_time(), rootfinder_stats["nlpsol"]["return_status"])
887
+ logger.error(message)
888
+ raise Exception(message)
889
+
890
+ if logger.getEffectiveLevel() == logging.DEBUG:
891
+ # compute max residual
892
+ largest_res = ca.norm_inf(
893
+ self.__res_vals(
894
+ next_state, self.__dt, self.__state_vector[: -len(self.__mx["parameters"])]
895
+ )
896
+ )
897
+ logger.debug("Residual maximum magnitude: {:.2E}".format(float(largest_res)))
898
+
899
+ # Update state vector
900
+ self.__state_vector[: self.__n_states] = next_state.toarray().ravel()
901
+
902
+ def simulate(self):
903
+ """
904
+ Run model from start_time to end_time.
905
+ """
906
+
907
+ # Do any preprocessing, which may include changing parameter values on
908
+ # the model
909
+ logger.info("Preprocessing")
910
+ self.pre()
911
+
912
+ # Initialize model
913
+ logger.info("Initializing")
914
+ self.initialize()
915
+
916
+ # Perform all timesteps
917
+ logger.info("Running")
918
+ while self.get_current_time() < self.get_end_time():
919
+ self.update(-1)
920
+
921
+ # Do any postprocessing
922
+ logger.info("Postprocessing")
923
+ self.post()
924
+
925
+ def reset(self):
926
+ """
927
+ Reset the FMU.
928
+ """
929
+ self.__state_vector = copy.deepcopy(self.__initialized_state_vector)
930
+
931
+ def get_start_time(self):
932
+ """
933
+ Return start time of experiment.
934
+
935
+ :returns: The start time of the experiment.
936
+ """
937
+ return self.__start
938
+
939
+ def get_end_time(self):
940
+ """
941
+ Return end time of experiment.
942
+
943
+ :returns: The end time of the experiment.
944
+ """
945
+ return self.__stop
946
+
947
+ def get_current_time(self):
948
+ """
949
+ Return current time of simulation.
950
+
951
+ :returns: The current simulation time.
952
+ """
953
+ return self.get_var("time")
954
+
955
+ def get_time_step(self):
956
+ """
957
+ Return simulation timestep.
958
+
959
+ :returns: The simulation timestep.
960
+ """
961
+ return self.__dt
962
+
963
+ def get_var(self, name):
964
+ """
965
+ Return a numpy array from FMU.
966
+
967
+ :param name: Variable name.
968
+
969
+ :returns: The value of the variable.
970
+ """
971
+
972
+ # Get the index of the canonical state and sign
973
+ index, sign = self.__indices[name]
974
+ value = self.__state_vector[index]
975
+
976
+ # Adjust sign if needed
977
+ if sign < 0:
978
+ value *= sign
979
+
980
+ # Adjust for nominal value if not default
981
+ if index <= self.__n_states:
982
+ nominal = self.get_variable_nominal(name)
983
+ value *= nominal
984
+
985
+ return value
986
+
987
+ def get_var_count(self):
988
+ """
989
+ Return the number of variables in the model.
990
+
991
+ :returns: The number of variables in the model.
992
+ """
993
+ return len(self.get_variables())
994
+
995
+ def get_var_name(self, i):
996
+ """
997
+ Returns the name of a variable.
998
+
999
+ :param i: Index in ordered dictionary returned by method get_variables.
1000
+
1001
+ :returns: The name of the variable.
1002
+ """
1003
+ return list(self.get_variables())[i]
1004
+
1005
+ def get_var_type(self, name):
1006
+ """
1007
+ Return type, compatible with numpy.
1008
+
1009
+ :param name: String variable name.
1010
+
1011
+ :returns: The numpy-compatible type of the variable.
1012
+
1013
+ :raises: KeyError
1014
+ """
1015
+ return self.__python_types[name]
1016
+
1017
+ def get_var_rank(self, name):
1018
+ """
1019
+ Not implemented
1020
+ """
1021
+ raise NotImplementedError
1022
+
1023
+ def get_var_shape(self, name):
1024
+ """
1025
+ Not implemented
1026
+ """
1027
+ raise NotImplementedError
1028
+
1029
+ def get_variables(self):
1030
+ """
1031
+ Return all variables (both internal and user defined)
1032
+
1033
+ :returns: An ordered dictionary of all variables supported by the model.
1034
+ """
1035
+ return AliasDict(self.alias_relation, self.__sym_dict)
1036
+
1037
+ @cached
1038
+ def get_state_variables(self):
1039
+ return AliasDict(
1040
+ self.alias_relation,
1041
+ {sym.name(): sym for sym in (self.__mx["states"] + self.__mx["algebraics"])},
1042
+ )
1043
+
1044
+ @cached
1045
+ def get_parameter_variables(self):
1046
+ return AliasDict(self.alias_relation, {sym.name(): sym for sym in self.__mx["parameters"]})
1047
+
1048
+ @cached
1049
+ def get_input_variables(self):
1050
+ return AliasDict(
1051
+ self.alias_relation, {sym.name(): sym for sym in self.__mx["constant_inputs"]}
1052
+ )
1053
+
1054
+ @cached
1055
+ def get_output_variables(self):
1056
+ return self.__pymoca_model.outputs
1057
+
1058
+ def __warn_for_nans(self):
1059
+ """
1060
+ Test state vector for missing values and warn
1061
+ """
1062
+ value_is_nan = np.isnan(self.__state_vector)
1063
+ if any(value_is_nan):
1064
+ for sym, isnan in zip(self.__sym_list, value_is_nan):
1065
+ if isnan:
1066
+ logger.warning("Variable {} has no value.".format(sym))
1067
+
1068
+ def set_time_step(self, dt):
1069
+ """
1070
+ Set the timestep size.
1071
+
1072
+ :param dt: Timestep size of the simulation.
1073
+ """
1074
+ if self._dt_is_fixed:
1075
+ assert math.isclose(self.__dt, dt), (
1076
+ "Timestep size dt is marked as constant and cannot be changed."
1077
+ )
1078
+ else:
1079
+ self.__dt = dt
1080
+
1081
+ def set_var(self, name, value):
1082
+ """
1083
+ Set the value of the given variable.
1084
+
1085
+ :param name: Name of variable to set.
1086
+ :param value: Value(s).
1087
+ """
1088
+
1089
+ # TODO: sanitize input
1090
+
1091
+ # Check if it is a parameter, and if it is allowed to be set
1092
+ if not self.__parameters_set_var:
1093
+ if name in self.__parameter_names_including_aliases:
1094
+ raise Exception("Cannot set parameters after initialize() has been called.")
1095
+
1096
+ # Get the index of the canonical state and sign
1097
+ index, sign = self.__indices[name]
1098
+ if sign < 0:
1099
+ value *= sign
1100
+
1101
+ # Adjust for nominal value if not default
1102
+ if index <= self.__n_states:
1103
+ nominal = self.get_variable_nominal(name)
1104
+ value /= nominal
1105
+
1106
+ # Store value in state vector
1107
+ self.__state_vector[index] = value
1108
+
1109
+ def set_var_slice(self, name, start, count, var):
1110
+ """
1111
+ Not implemented.
1112
+ """
1113
+ raise NotImplementedError
1114
+
1115
+ def set_var_index(self, name, index, var):
1116
+ """
1117
+ Not implemented.
1118
+ """
1119
+ raise NotImplementedError
1120
+
1121
+ def inq_compound(self, name):
1122
+ """
1123
+ Not implemented.
1124
+ """
1125
+ raise NotImplementedError
1126
+
1127
+ def inq_compound_field(self, name, index):
1128
+ """
1129
+ Not implemented.
1130
+ """
1131
+ raise NotImplementedError
1132
+
1133
+ def solver_options(self):
1134
+ """
1135
+ Returns a dictionary of CasADi nlpsol() solver options.
1136
+
1137
+ :returns: A dictionary of CasADi :class:`nlpsol` options. See the CasADi
1138
+ documentation for details.
1139
+ """
1140
+ return {
1141
+ "ipopt.fixed_variable_treatment": "make_parameter",
1142
+ "ipopt.print_level": 0,
1143
+ "print_time": False,
1144
+ "error_on_fail": False,
1145
+ }
1146
+
1147
+ def rootfinder_options(self):
1148
+ """
1149
+ Returns a dictionary of CasADi rootfinder() options.
1150
+
1151
+ The dictionary has the following items:
1152
+ * solver: the solver used for rootfinding, e.g. nlpsol, fast_newton, etc.
1153
+ * solver_options: options for the solver
1154
+ See the CasADi documentation for details on solvers and solver options.
1155
+
1156
+ :returns: A dictionary of CasADi :class:`rootfinder` options.
1157
+ """
1158
+ return {
1159
+ "solver": "nlpsol",
1160
+ "solver_options": {
1161
+ "nlpsol": "ipopt",
1162
+ "nlpsol_options": self.solver_options(),
1163
+ "error_on_fail": False,
1164
+ },
1165
+ }
1166
+
1167
+ def get_variable_nominal(self, variable) -> Union[float, ca.MX]:
1168
+ """
1169
+ Get the value of the nominal attribute of a variable
1170
+
1171
+ NOTE: Due to backwards compatibility for allowing parameters to be set
1172
+ with set_var() instead of overriding parameters(), this method can
1173
+ return a symbolic value for nominals defined in the Modelica file. It
1174
+ can only do so until the initializion() method in this class is
1175
+ called/completed, after which it will return numeric values only.
1176
+ """
1177
+ return self.__nominals.get(variable, 1.0)
1178
+
1179
+ def timeseries_at(self, variable, t):
1180
+ """
1181
+ Get value of timeseries variable at time t: should be overridden by pi or csv mixin
1182
+ """
1183
+ raise NotImplementedError
1184
+
1185
+ @cached
1186
+ def initial_state(self) -> AliasDict[str, float]:
1187
+ """
1188
+ The initial state.
1189
+
1190
+ :returns: A dictionary of variable names and initial state (t0) values.
1191
+ """
1192
+ t0 = self.get_start_time()
1193
+ initial_state_dict = AliasDict(self.alias_relation)
1194
+
1195
+ for variable in list(self.get_state_variables()) + list(self.get_input_variables()):
1196
+ try:
1197
+ initial_state_dict[variable] = self.timeseries_at(variable, t0)
1198
+ except KeyError:
1199
+ pass
1200
+ except NotImplementedError:
1201
+ pass
1202
+ else:
1203
+ if logger.getEffectiveLevel() == logging.DEBUG:
1204
+ logger.debug("Read intial state for {}".format(variable))
1205
+
1206
+ return initial_state_dict
1207
+
1208
+ @cached
1209
+ def seed(self) -> AliasDict[str, float]:
1210
+ """
1211
+ Seed values providing an initial guess for the t0 states.
1212
+
1213
+ :returns: A dictionary of variable names and seed (t0) values.
1214
+ """
1215
+ return AliasDict(self.alias_relation)
1216
+
1217
+ @cached
1218
+ def parameters(self):
1219
+ """
1220
+ Return a dictionary of parameter values extracted from the Modelica model
1221
+ """
1222
+ # Create AliasDict
1223
+ parameters = AliasDict(self.alias_relation)
1224
+
1225
+ # Update with model parameters
1226
+ parameters.update({p.symbol.name(): p.value for p in self.__pymoca_model.parameters})
1227
+
1228
+ return parameters
1229
+
1230
+ def extra_variables(self) -> List[Variable]:
1231
+ return []
1232
+
1233
+ def extra_equations(self) -> List[ca.MX]:
1234
+ return []
1235
+
1236
+ @property
1237
+ @cached
1238
+ def alias_relation(self):
1239
+ return self.__pymoca_model.alias_relation
1240
+
1241
+ @cached
1242
+ def compiler_options(self):
1243
+ """
1244
+ Subclasses can configure the `pymoca <http://github.com/pymoca/pymoca>`_ compiler options
1245
+ here.
1246
+
1247
+ :returns:
1248
+ A dictionary of pymoca compiler options. See the pymoca documentation for details.
1249
+ """
1250
+
1251
+ # Default options
1252
+ compiler_options = {}
1253
+
1254
+ # Expand vector states to multiple scalar component states.
1255
+ compiler_options["expand_vectors"] = True
1256
+
1257
+ # Where imported model libraries are located.
1258
+ library_folders = self.modelica_library_folders.copy()
1259
+
1260
+ for ep in importlib_metadata.entry_points(group="rtctools.libraries.modelica"):
1261
+ if ep.name == "library_folder":
1262
+ library_folders.append(str(importlib.resources.files(ep.module).joinpath(ep.attr)))
1263
+
1264
+ compiler_options["library_folders"] = library_folders
1265
+
1266
+ # Eliminate equations of the type 'var = const'.
1267
+ compiler_options["eliminate_constant_assignments"] = True
1268
+
1269
+ # Eliminate constant symbols from model, replacing them with the values
1270
+ # specified in the model.
1271
+ compiler_options["replace_constant_values"] = True
1272
+
1273
+ # Replace any constant expressions into the model.
1274
+ compiler_options["replace_constant_expressions"] = True
1275
+
1276
+ # Replace any parameter expressions into the model.
1277
+ compiler_options["replace_parameter_expressions"] = True
1278
+
1279
+ # Eliminate variables starting with underscores.
1280
+ compiler_options["eliminable_variable_expression"] = r"(.*[.]|^)_\w+(\[[\d,]+\])?\Z"
1281
+
1282
+ # Pymoca currently requires `expand_mx` to be set for
1283
+ # `eliminable_variable_expression` to work.
1284
+ compiler_options["expand_mx"] = True
1285
+
1286
+ # Automatically detect and eliminate alias variables.
1287
+ compiler_options["detect_aliases"] = True
1288
+
1289
+ # Cache the model on disk
1290
+ compiler_options["cache"] = True
1291
+
1292
+ # Done
1293
+ return compiler_options