rtc-tools 2.5.2rc4__py3-none-any.whl → 2.6.0__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.

Potentially problematic release.


This version of rtc-tools might be problematic. Click here for more details.

Files changed (47) hide show
  1. {rtc_tools-2.5.2rc4.dist-info → rtc_tools-2.6.0.dist-info}/METADATA +7 -7
  2. rtc_tools-2.6.0.dist-info/RECORD +50 -0
  3. {rtc_tools-2.5.2rc4.dist-info → rtc_tools-2.6.0.dist-info}/WHEEL +1 -1
  4. rtctools/__init__.py +2 -1
  5. rtctools/_internal/alias_tools.py +12 -10
  6. rtctools/_internal/caching.py +5 -3
  7. rtctools/_internal/casadi_helpers.py +11 -32
  8. rtctools/_internal/debug_check_helpers.py +1 -1
  9. rtctools/_version.py +3 -3
  10. rtctools/data/__init__.py +2 -2
  11. rtctools/data/csv.py +54 -33
  12. rtctools/data/interpolation/bspline.py +3 -3
  13. rtctools/data/interpolation/bspline1d.py +42 -29
  14. rtctools/data/interpolation/bspline2d.py +10 -4
  15. rtctools/data/netcdf.py +137 -93
  16. rtctools/data/pi.py +304 -210
  17. rtctools/data/rtc.py +64 -53
  18. rtctools/data/storage.py +91 -51
  19. rtctools/optimization/collocated_integrated_optimization_problem.py +1244 -696
  20. rtctools/optimization/control_tree_mixin.py +68 -66
  21. rtctools/optimization/csv_lookup_table_mixin.py +107 -74
  22. rtctools/optimization/csv_mixin.py +83 -52
  23. rtctools/optimization/goal_programming_mixin.py +237 -146
  24. rtctools/optimization/goal_programming_mixin_base.py +204 -111
  25. rtctools/optimization/homotopy_mixin.py +36 -27
  26. rtctools/optimization/initial_state_estimation_mixin.py +8 -8
  27. rtctools/optimization/io_mixin.py +48 -43
  28. rtctools/optimization/linearization_mixin.py +3 -1
  29. rtctools/optimization/linearized_order_goal_programming_mixin.py +57 -28
  30. rtctools/optimization/min_abs_goal_programming_mixin.py +72 -29
  31. rtctools/optimization/modelica_mixin.py +135 -81
  32. rtctools/optimization/netcdf_mixin.py +32 -18
  33. rtctools/optimization/optimization_problem.py +181 -127
  34. rtctools/optimization/pi_mixin.py +68 -36
  35. rtctools/optimization/planning_mixin.py +19 -0
  36. rtctools/optimization/single_pass_goal_programming_mixin.py +159 -112
  37. rtctools/optimization/timeseries.py +4 -6
  38. rtctools/rtctoolsapp.py +18 -18
  39. rtctools/simulation/csv_mixin.py +37 -30
  40. rtctools/simulation/io_mixin.py +9 -5
  41. rtctools/simulation/pi_mixin.py +62 -32
  42. rtctools/simulation/simulation_problem.py +471 -180
  43. rtctools/util.py +84 -56
  44. rtc_tools-2.5.2rc4.dist-info/RECORD +0 -49
  45. {rtc_tools-2.5.2rc4.dist-info → rtc_tools-2.6.0.dist-info}/COPYING.LESSER +0 -0
  46. {rtc_tools-2.5.2rc4.dist-info → rtc_tools-2.6.0.dist-info}/entry_points.txt +0 -0
  47. {rtc_tools-2.5.2rc4.dist-info → rtc_tools-2.6.0.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,16 @@
1
1
  import copy
2
2
  import itertools
3
3
  import logging
4
+ import math
4
5
  from collections import OrderedDict
5
- from typing import Union
6
+ from typing import List, Union
6
7
 
7
8
  import casadi as ca
8
-
9
9
  import numpy as np
10
-
11
10
  import pkg_resources
12
-
13
11
  import pymoca
14
12
  import pymoca.backends.casadi.api
15
13
 
16
-
17
14
  from rtctools._internal.alias_tools import AliasDict
18
15
  from rtctools._internal.caching import cached
19
16
  from rtctools._internal.debug_check_helpers import DebugLevel
@@ -22,13 +19,39 @@ from rtctools.data.storage import DataStoreAccessor
22
19
  logger = logging.getLogger("rtctools")
23
20
 
24
21
 
22
+ class Variable:
23
+ """
24
+ Modeled after the Variable class in pymoca.backends.casadi.model, with modifications to make it
25
+ easier for the common case in RTC-Tools to instantiate them.
26
+
27
+ That means:
28
+ - pass in name instead of ca.MX symbol
29
+ - only scalars are allowed (shape = (1, 1))
30
+ - no aliases
31
+ - no "python_type"
32
+ - able to specify nominal/min/max in constructor
33
+ """
34
+
35
+ def __init__(self, name, /, min=-np.inf, max=np.inf, nominal=1.0):
36
+ self.name = name
37
+ self.min = min
38
+ self.max = max
39
+ self.nominal = nominal
40
+ self._symbol = ca.MX.sym(name)
41
+
42
+ @property
43
+ def symbol(self):
44
+ return self._symbol
45
+
46
+
25
47
  class SimulationProblem(DataStoreAccessor):
26
48
  """
27
49
  Implements the `BMI <http://csdms.colorado.edu/wiki/BMI_Description>`_ Interface.
28
50
 
29
51
  Base class for all Simulation problems. Loads the Modelica Model.
30
52
 
31
- :cvar modelica_library_folders: Folders containing any referenced Modelica libraries. Default is an empty list.
53
+ :cvar modelica_library_folders: Folders containing any referenced Modelica libraries. Default
54
+ is an empty list.
32
55
 
33
56
  """
34
57
 
@@ -44,113 +67,186 @@ class SimulationProblem(DataStoreAccessor):
44
67
 
45
68
  def __init__(self, **kwargs):
46
69
  # Check arguments
47
- assert ('model_folder' in kwargs)
70
+ assert "model_folder" in kwargs
48
71
 
49
72
  # Log pymoca version
50
73
  logger.debug("Using pymoca {}.".format(pymoca.__version__))
51
74
 
52
75
  # Transfer model from the Modelica .mo file to CasADi using pymoca
53
- if 'model_name' in kwargs:
54
- model_name = kwargs['model_name']
76
+ if "model_name" in kwargs:
77
+ model_name = kwargs["model_name"]
55
78
  else:
56
- if hasattr(self, 'model_name'):
79
+ if hasattr(self, "model_name"):
57
80
  model_name = self.model_name
58
81
  else:
59
82
  model_name = self.__class__.__name__
60
83
 
61
84
  # Load model from pymoca backend
62
85
  self.__pymoca_model = pymoca.backends.casadi.api.transfer_model(
63
- kwargs['model_folder'], model_name, self.compiler_options())
86
+ kwargs["model_folder"], model_name, self.compiler_options()
87
+ )
64
88
 
65
89
  # Extract the CasADi MX variables used in the model
66
90
  self.__mx = {}
67
- self.__mx['time'] = [self.__pymoca_model.time]
68
- self.__mx['states'] = [v.symbol for v in self.__pymoca_model.states]
69
- self.__mx['derivatives'] = [v.symbol for v in self.__pymoca_model.der_states]
70
- self.__mx['algebraics'] = [v.symbol for v in self.__pymoca_model.alg_states]
71
- self.__mx['parameters'] = [v.symbol for v in self.__pymoca_model.parameters]
72
- self.__mx['constant_inputs'] = []
73
- self.__mx['lookup_tables'] = []
91
+ self.__mx["time"] = [self.__pymoca_model.time]
92
+ self.__mx["states"] = [v.symbol for v in self.__pymoca_model.states]
93
+ self.__mx["derivatives"] = [v.symbol for v in self.__pymoca_model.der_states]
94
+ self.__mx["algebraics"] = [v.symbol for v in self.__pymoca_model.alg_states]
95
+ self.__mx["parameters"] = [v.symbol for v in self.__pymoca_model.parameters]
96
+ self.__mx["constant_inputs"] = []
97
+ self.__mx["lookup_tables"] = []
74
98
 
75
99
  for v in self.__pymoca_model.inputs:
76
100
  if v.symbol.name() in self.__pymoca_model.delay_states:
77
101
  # Delayed feedback variables are local to each ensemble, and
78
102
  # therefore belong to the collection of algebraic variables,
79
103
  # rather than to the control inputs.
80
- self.__mx['algebraics'].append(v.symbol)
104
+ self.__mx["algebraics"].append(v.symbol)
81
105
  else:
82
- if v.symbol.name() in kwargs.get('lookup_tables', []):
83
- self.__mx['lookup_tables'].append(v.symbol)
106
+ if v.symbol.name() in kwargs.get("lookup_tables", []):
107
+ self.__mx["lookup_tables"].append(v.symbol)
84
108
  else:
85
109
  # All inputs are constant inputs
86
- self.__mx['constant_inputs'].append(v.symbol)
110
+ self.__mx["constant_inputs"].append(v.symbol)
111
+
112
+ # Set timestep size
113
+ self._dt_is_fixed = False
114
+ self.__dt = None
115
+ fixed_dt = kwargs.get("fixed_dt", None)
116
+ if fixed_dt is not None:
117
+ self._dt_is_fixed = True
118
+ self.__dt = fixed_dt
119
+
120
+ # Add auxiliary variables for keeping track of delay expressions to the algebraic states
121
+ n_delay_states = len(self.__pymoca_model.delay_states)
122
+ self.__delay_times = []
123
+ if n_delay_states > 0:
124
+ if fixed_dt is None and not self._force_zero_delay:
125
+ raise ValueError("fixed_dt should be set when using delay equations.")
126
+ self.__delay_times = self._get_delay_times()
127
+ delay_expression_states = self._create_delay_expression_states()
128
+ self.__mx["algebraics"] += delay_expression_states
87
129
 
88
130
  # Log variables in debug mode
89
131
  if logger.getEffectiveLevel() == logging.DEBUG:
90
- logger.debug("SimulationProblem: Found states {}".format(
91
- ', '.join([var.name() for var in self.__mx['states']])))
92
- logger.debug("SimulationProblem: Found derivatives {}".format(
93
- ', '.join([var.name() for var in self.__mx['derivatives']])))
94
- logger.debug("SimulationProblem: Found algebraics {}".format(
95
- ', '.join([var.name() for var in self.__mx['algebraics']])))
96
- logger.debug("SimulationProblem: Found constant inputs {}".format(
97
- ', '.join([var.name() for var in self.__mx['constant_inputs']])))
98
- logger.debug("SimulationProblem: Found parameters {}".format(
99
- ', '.join([var.name() for var in self.__mx['parameters']])))
132
+ logger.debug(
133
+ "SimulationProblem: Found states {}".format(
134
+ ", ".join([var.name() for var in self.__mx["states"]])
135
+ )
136
+ )
137
+ logger.debug(
138
+ "SimulationProblem: Found derivatives {}".format(
139
+ ", ".join([var.name() for var in self.__mx["derivatives"]])
140
+ )
141
+ )
142
+ logger.debug(
143
+ "SimulationProblem: Found algebraics {}".format(
144
+ ", ".join([var.name() for var in self.__mx["algebraics"]])
145
+ )
146
+ )
147
+ logger.debug(
148
+ "SimulationProblem: Found constant inputs {}".format(
149
+ ", ".join([var.name() for var in self.__mx["constant_inputs"]])
150
+ )
151
+ )
152
+ logger.debug(
153
+ "SimulationProblem: Found parameters {}".format(
154
+ ", ".join([var.name() for var in self.__mx["parameters"]])
155
+ )
156
+ )
157
+
158
+ # Get the extra variables that are user defined
159
+ self.__extra_variables = self.extra_variables()
160
+ self.__extra_variables_symbols = [v.symbol for v in self.__extra_variables]
100
161
 
101
162
  # Store the types in an AliasDict
102
163
  self.__python_types = AliasDict(self.alias_relation)
103
- model_variable_types = ["states", "der_states", "alg_states", "inputs", "constants", "parameters"]
164
+ model_variable_types = [
165
+ "states",
166
+ "der_states",
167
+ "alg_states",
168
+ "inputs",
169
+ "constants",
170
+ "parameters",
171
+ ]
104
172
  for t in model_variable_types:
105
173
  for v in getattr(self.__pymoca_model, t):
106
174
  self.__python_types[v.symbol.name()] = v.python_type
107
175
 
108
176
  # Store the nominals in an AliasDict
109
177
  self.__nominals = AliasDict(self.alias_relation)
110
- for v in itertools.chain(
111
- self.__pymoca_model.states, self.__pymoca_model.alg_states):
178
+ for v in itertools.chain(self.__pymoca_model.states, self.__pymoca_model.alg_states):
112
179
  sym_name = v.symbol.name()
113
180
 
114
- # If the nominal is 0.0 or 1.0 or -1.0, ignore: get_variable_nominal returns a default of 1.0
181
+ # If the nominal is 0.0 or 1.0 or -1.0, ignore: get_variable_nominal returns a default
182
+ # of 1.0
115
183
  # TODO: handle nominal vectors (update() will need to load them)
116
- if ca.MX(v.nominal).is_zero() or ca.MX(v.nominal - 1).is_zero() or ca.MX(v.nominal + 1).is_zero():
184
+ if (
185
+ ca.MX(v.nominal).is_zero()
186
+ or ca.MX(v.nominal - 1).is_zero()
187
+ or ca.MX(v.nominal + 1).is_zero()
188
+ ):
117
189
  continue
118
190
  else:
119
191
  if ca.MX(v.nominal).size1() != 1:
120
- logger.error('Vector Nominals not supported yet. ({})'.format(sym_name))
192
+ logger.error("Vector Nominals not supported yet. ({})".format(sym_name))
121
193
  self.__nominals[sym_name] = ca.fabs(v.nominal)
122
194
  if logger.getEffectiveLevel() == logging.DEBUG:
123
- logger.debug("SimulationProblem: Setting nominal value for variable {} to {}".format(
124
- sym_name, self.__nominals[sym_name]))
195
+ logger.debug(
196
+ "SimulationProblem: Setting nominal value for variable {} to {}".format(
197
+ sym_name, self.__nominals[sym_name]
198
+ )
199
+ )
200
+
201
+ for v in self.__extra_variables:
202
+ self.__nominals[v.name] = v.nominal
125
203
 
126
204
  # Initialize DAE and initial residuals
127
- variable_lists = ['states', 'der_states', 'alg_states', 'inputs', 'constants', 'parameters']
205
+ variable_lists = ["states", "der_states", "alg_states", "inputs", "constants", "parameters"]
128
206
  function_arguments = [self.__pymoca_model.time] + [
129
207
  ca.veccat(*[v.symbol for v in getattr(self.__pymoca_model, variable_list)])
130
- for variable_list in variable_lists]
131
-
132
- if self.__pymoca_model.delay_states and not self._force_zero_delay:
133
- raise NotImplementedError("Delayed states are not supported")
208
+ for variable_list in variable_lists
209
+ ]
134
210
 
135
211
  self.__dae_residual = self.__pymoca_model.dae_residual_function(*function_arguments)
136
212
 
213
+ if self.__dae_residual is None:
214
+ # DAE is empty, that can happen if we add the only (non-aliasing) equations
215
+ # in Python.
216
+ self.__dae_residual = ca.MX()
217
+
137
218
  self.__initial_residual = self.__pymoca_model.initial_residual_function(*function_arguments)
138
219
  if self.__initial_residual is None:
139
220
  self.__initial_residual = ca.MX()
140
221
 
141
222
  # Construct state vector
142
- self.__sym_list = self.__mx['states'] + self.__mx['algebraics'] + self.__mx['derivatives'] + \
143
- self.__mx['time'] + self.__mx['constant_inputs'] + self.__mx['parameters']
144
- self.__state_vector = np.full(len(self.__sym_list), np.nan)
223
+ self.__sym_list = (
224
+ self.__mx["states"]
225
+ + self.__mx["algebraics"]
226
+ + self.__mx["derivatives"]
227
+ + self.__extra_variables_symbols
228
+ + self.__mx["time"]
229
+ + self.__mx["constant_inputs"]
230
+ + self.__mx["parameters"]
231
+ )
232
+ n_elements = np.array([var.numel() for var in self.__sym_list])
233
+ i_end = n_elements.cumsum()
234
+ i_start = np.array([0, *(i_end[:-1])])
235
+ self.__state_vector = np.full(n_elements.sum(), np.nan)
145
236
 
146
237
  # A very handy index
147
- self.__states_end_index = len(self.__mx['states']) + \
148
- len(self.__mx['algebraics']) + len(self.__mx['derivatives'])
238
+ self.__n_state_symbols = (
239
+ len(self.__mx["states"])
240
+ + len(self.__mx["algebraics"])
241
+ + len(self.__mx["derivatives"])
242
+ + len(self.__extra_variables)
243
+ )
244
+ self.__n_states = i_end[self.__n_state_symbols - 1]
149
245
 
150
246
  # NOTE: Backwards compatibility allowing set_var() for parameters. These
151
247
  # variables check that this is only done before calling initialize().
152
248
  self.__parameters = AliasDict(self.alias_relation)
153
- self.__parameters.update({v.name(): v for v in self.__mx['parameters']})
249
+ self.__parameters.update({v.name(): v for v in self.__mx["parameters"]})
154
250
  self.__parameters_set_var = True
155
251
 
156
252
  # Construct a dict to look up symbols by name (or iterate over)
@@ -159,17 +255,66 @@ class SimulationProblem(DataStoreAccessor):
159
255
  # Generate a dictionary that we can use to lookup the index in the state vector.
160
256
  # To avoid repeated and relatively expensive `canonical_signed` calls, we
161
257
  # make a dictionary for all variables and their aliases.
162
- self.__indices = {}
258
+ self.__i_start = {}
259
+ self.__i_end = {}
163
260
  for i, k in enumerate(self.__sym_dict.keys()):
164
261
  for alias in self.alias_relation.aliases(k):
165
- if alias.startswith('-'):
166
- self.__indices[alias[1:]] = (i, -1.0)
262
+ if alias.startswith("-"):
263
+ self.__i_start[alias[1:]] = (i_start[i], -1.0)
264
+ self.__i_end[alias[1:]] = (i_end[i], -1.0)
167
265
  else:
168
- self.__indices[alias] = (i, 1.0)
266
+ self.__i_start[alias] = (i_start[i], 1.0)
267
+ self.__i_end[alias] = (i_end[i], 1.0)
268
+ self.__indices = self.__i_start
169
269
 
170
270
  # Call parent class for default behaviour.
171
271
  super().__init__(**kwargs)
172
272
 
273
+ def _get_delay_times(self):
274
+ """
275
+ Get the delay times for each delay equation.
276
+ """
277
+ if self._force_zero_delay:
278
+ return [0] * len(self.__pymoca_model.delay_states)
279
+ parameter_symbols = [v.symbol for v in self.__pymoca_model.parameters]
280
+ parameter_values = [v.value for v in self.__pymoca_model.parameters]
281
+ delay_time_expressions = [
282
+ delay_arg.duration for delay_arg in self.__pymoca_model.delay_arguments
283
+ ]
284
+ delay_time_fun = ca.Function(
285
+ "delay_time_function", parameter_symbols, delay_time_expressions
286
+ )
287
+ delay_time_values = delay_time_fun(*parameter_values)
288
+ if len(delay_time_expressions) == 1:
289
+ return [delay_time_values]
290
+ return list(delay_time_values)
291
+
292
+ def _create_delay_expression_states(self):
293
+ """
294
+ Create auxiliary states for delay equations.
295
+
296
+ Create states to keep track of the history of delay expressions.
297
+ For example, if we have a delay equation of the form
298
+
299
+ .. math::
300
+ x = delay(5 * y, 2 * dt),
301
+
302
+ Then we need variables to store :math:`5 * y` at time :math`t` and time :math:`t - dt`
303
+ (For each state, we also store the previous value,
304
+ so if we have a state for :math:`5 * y` at :math:`t - dt`,
305
+ then its previous value is :math:`5 * y` at :math:`t - 2 * dt`).
306
+ """
307
+ delay_expression_states = []
308
+ for delay_state, delay_time in zip(self.__pymoca_model.delay_states, self.__delay_times):
309
+ if delay_time > 0:
310
+ n_previous_values = int(np.ceil(delay_time / self.get_time_step()))
311
+ else:
312
+ n_previous_values = 1
313
+ expression_state = delay_state + "_expr"
314
+ expression_symbol = ca.MX.sym(expression_state, n_previous_values)
315
+ delay_expression_states.append(expression_symbol)
316
+ return delay_expression_states
317
+
173
318
  def initialize(self, config_file=None):
174
319
  """
175
320
  Initialize state vector with default values
@@ -182,9 +327,13 @@ class SimulationProblem(DataStoreAccessor):
182
327
  # for now, assume that setup_experiment was called beforehand
183
328
  raise NotImplementedError
184
329
 
330
+ # Short-hand notation for the model
331
+ model = self.__pymoca_model
332
+
185
333
  # Set values of parameters defined in the model into the state vector
186
334
  for var in self.__pymoca_model.parameters:
187
- # First check to see if parameter is already set (this allows child classes to override model defaults)
335
+ # First check to see if parameter is already set (this allows child classes to override
336
+ # model defaults)
188
337
  if np.isfinite(self.get_var(var.symbol.name())):
189
338
  continue
190
339
 
@@ -202,7 +351,11 @@ class SimulationProblem(DataStoreAccessor):
202
351
  else:
203
352
  # If val is finite, we set it
204
353
  if np.isfinite(val):
205
- logger.debug('SimulationProblem: Setting parameter {} = {}'.format(var.symbol.name(), val))
354
+ logger.debug(
355
+ "SimulationProblem: Setting parameter {} = {}".format(
356
+ var.symbol.name(), val
357
+ )
358
+ )
206
359
  self.set_var(var.symbol.name(), val)
207
360
 
208
361
  # Nominals can be symbolic, written in terms of parameters. After all
@@ -210,9 +363,11 @@ class SimulationProblem(DataStoreAccessor):
210
363
  # nominals.
211
364
  nominal_vars = list(self.__nominals.keys())
212
365
  symbolic_nominals = ca.vertcat(*[self.get_variable_nominal(v) for v in nominal_vars])
213
- nominal_evaluator = ca.Function('nominal_evaluator', self.__mx['parameters'], [symbolic_nominals])
366
+ nominal_evaluator = ca.Function(
367
+ "nominal_evaluator", self.__mx["parameters"], [symbolic_nominals]
368
+ )
214
369
 
215
- n_parameters = len(self.__mx['parameters'])
370
+ n_parameters = len(self.__mx["parameters"])
216
371
  if n_parameters > 0:
217
372
  [evaluated_nominals] = nominal_evaluator.call(self.__state_vector[-n_parameters:])
218
373
  else:
@@ -224,6 +379,15 @@ class SimulationProblem(DataStoreAccessor):
224
379
 
225
380
  self.__nominals.update(nominal_dict)
226
381
 
382
+ # The variables that need a mutually consistent initial condition
383
+ X = ca.vertcat(*self.__sym_list[: self.__n_state_symbols])
384
+ X_prev = ca.vertcat(
385
+ *[
386
+ ca.MX.sym(sym.name() + "_prev", sym.shape)
387
+ for sym in self.__sym_list[: self.__n_state_symbols]
388
+ ]
389
+ )
390
+
227
391
  # Assemble initial residuals and set values from start attributes into the state vector
228
392
  minimized_residuals = []
229
393
  for var in itertools.chain(self.__pymoca_model.states, self.__pymoca_model.alg_states):
@@ -240,39 +404,45 @@ class SimulationProblem(DataStoreAccessor):
240
404
  start_val = None
241
405
 
242
406
  if start_val == 0.0 and not var.fixed:
243
- # To make initialization easier, we allow setting initial states by providing timeseries
244
- # with names that match a symbol in the model. We only check for this matching if the start
245
- # and fixed attributes were left as default
407
+ # To make initialization easier, we allow setting initial states by providing
408
+ # timeseries with names that match a symbol in the model. We only check for this
409
+ # matching if the start and fixed attributes were left as default
246
410
  try:
247
411
  start_val = self.initial_state()[var_name]
248
412
  except KeyError:
249
413
  pass
250
414
  else:
251
415
  # An initial state was found- add it to the constrained residuals
252
- logger.debug('Initialize: Added {} = {} to initial equations (found matching timeseries).'.format(
253
- var_name, start_val))
416
+ logger.debug(
417
+ "Initialize: Added {} = {} to initial equations "
418
+ "(found matching timeseries).".format(var_name, start_val)
419
+ )
254
420
  # Set var to be fixed
255
421
  var.fixed = True
256
422
 
257
423
  if not var.fixed:
258
- # To make initialization easier, we allow setting initial guesses by providing timeseries
259
- # with names that match a symbol in the model. We only check for this matching if the start
260
- # and fixed attributes were left as default
424
+ # To make initialization easier, we allow setting initial guesses by providing
425
+ # timeseries with names that match a symbol in the model. We only check for this
426
+ # matching if the start and fixed attributes were left as default
261
427
  try:
262
428
  start_val = self.seed()[var_name]
263
429
  except KeyError:
264
430
  pass
265
431
  else:
266
432
  # An initial state was found- add it to the constrained residuals
267
- logger.debug('Initialize: Added {} = {} as initial guess (found matching timeseries).'.format(
268
- var_name, start_val))
433
+ logger.debug(
434
+ "Initialize: Added {} = {} as initial guess "
435
+ "(found matching timeseries).".format(var_name, start_val)
436
+ )
269
437
 
270
438
  # Attempt to set start_val in the state vector. Default to zero if unknown.
271
439
  try:
272
440
  self.set_var(var_name, start_val if start_val is not None else 0.0)
273
441
  except KeyError:
274
- logger.warning('Initialize: {} not found in state vector. Initial value of {} not set.'.format(
275
- var_name, start_val))
442
+ logger.warning(
443
+ "Initialize: {} not found in state vector. "
444
+ "Initial value of {} not set.".format(var_name, start_val)
445
+ )
276
446
 
277
447
  # Add a residual for the difference between the state and its starting expression
278
448
  start_expr = start_val if start_val is not None else var.start
@@ -286,29 +456,43 @@ class SimulationProblem(DataStoreAccessor):
286
456
  minimized_residuals.append((var.symbol - start_expr) / var_nominal)
287
457
 
288
458
  # Default start var for ders is zero
289
- for der_var in self.__mx['derivatives']:
459
+ for der_var in self.__mx["derivatives"]:
290
460
  self.set_var(der_var.name(), 0.0)
291
461
 
462
+ # Residuals for initial values for the delay states / expressions.
463
+ for delay_state, delay_argument in zip(model.delay_states, model.delay_arguments):
464
+ expression_state = delay_state + "_expr"
465
+ i_delay_state, _ = self.__indices[delay_state]
466
+ i_expr_start, _ = self.__i_start[expression_state]
467
+ i_expr_end, _ = self.__i_end[expression_state]
468
+ minimized_residuals.append(X[i_expr_start:i_expr_end] - delay_argument.expr)
469
+ minimized_residuals.append(X[i_delay_state] - delay_argument.expr)
470
+
292
471
  # Warn for nans in state vector (verify we didn't miss anything)
293
472
  self.__warn_for_nans()
294
473
 
295
474
  # Optionally encourage a steady-state initial condition
296
- if getattr(self, 'encourage_steady_state_initial_conditions', False):
475
+ if getattr(self, "encourage_steady_state_initial_conditions", False):
297
476
  # add penalty for der(var) != 0.0
298
- for d in self.__mx['derivatives']:
299
- logger.debug('Added {} to the minimized residuals.'.format(d.name()))
477
+ for d in self.__mx["derivatives"]:
478
+ logger.debug("Added {} to the minimized residuals.".format(d.name()))
300
479
  minimized_residuals.append(d)
301
480
 
302
481
  # Make minimized_residuals into a single symbolic object
303
- minimized_residual = ca.vertcat(*minimized_residuals)
482
+ if minimized_residuals:
483
+ minimized_residual = ca.vertcat(*minimized_residuals)
484
+ else:
485
+ # DAE is empty
486
+ minimized_residual = ca.MX(0)
487
+
488
+ # Extra equations
489
+ extra_equations = self.extra_equations()
304
490
 
305
491
  # Assemble symbolics needed to make a function describing the initial condition of the model
306
492
  # We constrain every entry in this MX to zero
307
- equality_constraints = ca.vertcat(self.__dae_residual, self.__initial_residual)
308
-
309
- # The variables that need a mutually consistent initial condition
310
- X = ca.vertcat(*self.__sym_list[:self.__states_end_index])
311
- X_prev = ca.vertcat(*[ca.MX.sym(sym.name() + '_prev') for sym in self.__sym_list[:self.__states_end_index]])
493
+ equality_constraints = ca.vertcat(
494
+ self.__dae_residual, self.__initial_residual, *extra_equations
495
+ )
312
496
 
313
497
  # Make a list of unscaled symbols and a list of their scaled equivalent
314
498
  unscaled_symbols = []
@@ -318,7 +502,7 @@ class SimulationProblem(DataStoreAccessor):
318
502
  index, _ = self.__indices[sym_name]
319
503
 
320
504
  # If the symbol is a state, Add the symbol to the lists
321
- if index <= self.__states_end_index:
505
+ if index <= self.__n_states:
322
506
  unscaled_symbols.append(X[index])
323
507
  scaled_symbols.append(X[index] * nominal)
324
508
 
@@ -333,17 +517,23 @@ class SimulationProblem(DataStoreAccessor):
333
517
  equality_constraints = ca.substitute(equality_constraints, unscaled_symbols, scaled_symbols)
334
518
  minimized_residual = ca.substitute(minimized_residual, unscaled_symbols, scaled_symbols)
335
519
 
336
- logger.debug('SimulationProblem: Initial Equations are ' + str(equality_constraints))
337
- logger.debug('SimulationProblem: Minimized Residuals are ' + str(minimized_residual))
520
+ logger.debug("SimulationProblem: Initial Equations are " + str(equality_constraints))
521
+ logger.debug("SimulationProblem: Minimized Residuals are " + str(minimized_residual))
338
522
 
339
523
  # State bounds can be symbolic, written in terms of parameters. After all
340
524
  # parameter values are known, we evaluate the numeric values of bounds.
341
- bound_vars = self.__pymoca_model.states + self.__pymoca_model.alg_states + self.__pymoca_model.der_states
525
+ bound_vars = (
526
+ self.__pymoca_model.states
527
+ + self.__pymoca_model.alg_states
528
+ + self.__pymoca_model.der_states
529
+ + self.__extra_variables
530
+ )
531
+
342
532
  symbolic_bounds = ca.vertcat(*[ca.horzcat(v.min, v.max) for v in bound_vars])
343
- bound_evaluator = ca.Function('bound_evaluator', self.__mx['parameters'], [symbolic_bounds])
533
+ bound_evaluator = ca.Function("bound_evaluator", self.__mx["parameters"], [symbolic_bounds])
344
534
 
345
535
  # Evaluate bounds using values of parameters
346
- n_parameters = len(self.__mx['parameters'])
536
+ n_parameters = len(self.__mx["parameters"])
347
537
  if n_parameters > 0:
348
538
  [evaluated_bounds] = bound_evaluator.call(self.__state_vector[-n_parameters:])
349
539
  else:
@@ -356,15 +546,20 @@ class SimulationProblem(DataStoreAccessor):
356
546
 
357
547
  evaluated_bounds = np.array(evaluated_bounds) / np.array(nominals)[:, None]
358
548
 
359
- # Update with the bounds of delayed states
360
- n_delay = len(self.__pymoca_model.delay_states)
361
- delay_bounds = np.array([-np.inf, np.inf] * n_delay).reshape((n_delay, 2))
362
- offset = len(self.__pymoca_model.states) + len(self.__pymoca_model.alg_states)
363
- evaluated_bounds = np.vstack((evaluated_bounds[:offset, :],
364
- delay_bounds,
365
- evaluated_bounds[offset:, :]))
549
+ # Update with the bounds of delayed states / expressions
550
+ if model.delay_states:
551
+ i_start_first_delay_state, _ = self.__indices[model.delay_states[0]]
552
+ i_end_last_delay_expr, _ = self.__i_end[model.delay_states[-1] + "_expr"]
553
+ n_delay = i_end_last_delay_expr - i_start_first_delay_state
554
+ delay_bounds = np.array([-np.inf, np.inf] * n_delay).reshape((n_delay, 2))
555
+ # offset = len(self.__pymoca_model.states) + len(self.__pymoca_model.alg_states)
556
+ offset = i_start_first_delay_state
557
+ evaluated_bounds = np.vstack(
558
+ (evaluated_bounds[:offset, :], delay_bounds, evaluated_bounds[offset:, :])
559
+ )
366
560
 
367
- # Construct arrays of state bounds (used in the initialize() nlp, but not in __do_step rootfinder)
561
+ # Construct arrays of state bounds (used in the initialize() nlp, but not in __do_step
562
+ # rootfinder)
368
563
  self.__lbx = evaluated_bounds[:, 0]
369
564
  self.__ubx = evaluated_bounds[:, 1]
370
565
 
@@ -376,35 +571,49 @@ class SimulationProblem(DataStoreAccessor):
376
571
  objective_function = ca.dot(minimized_residual, minimized_residual)
377
572
 
378
573
  # Substitute constants and parameters
379
- const_and_par = ca.vertcat(*self.__mx['time'], *self.__mx['constant_inputs'], *self.__mx['parameters'])
380
- const_and_par_values = self.__state_vector[self.__states_end_index:]
574
+ const_and_par = ca.vertcat(
575
+ *self.__mx["time"], *self.__mx["constant_inputs"], *self.__mx["parameters"]
576
+ )
577
+ const_and_par_values = self.__state_vector[self.__n_states :]
381
578
 
382
579
  objective_function = ca.substitute(objective_function, const_and_par, const_and_par_values)
383
- equality_constraints = ca.substitute(equality_constraints, const_and_par, const_and_par_values)
384
-
385
- expand_f_g = ca.Function('f', [X], [objective_function, equality_constraints]).expand()
386
- X_sx = ca.SX.sym('X', X.shape)
387
- objective_function_sx, equality_constraints_sx = expand_f_g(X_sx)
580
+ equality_constraints = ca.substitute(
581
+ equality_constraints, const_and_par, const_and_par_values
582
+ )
388
583
 
389
584
  # Construct nlp and solver to find initial state using ipopt
390
- nlp = {'x': X_sx, 'f': objective_function_sx, 'g': equality_constraints_sx}
391
- solver = ca.nlpsol('solver', 'ipopt', nlp, self.solver_options())
585
+ # Note that some operations cannot be expanded, e.g. ca.interpolant. So
586
+ # we _try_ to expand, but fall back on ca.MX evaluation if we cannot.
587
+ try:
588
+ expand_f_g = ca.Function("f", [X], [objective_function, equality_constraints]).expand()
589
+ except RuntimeError as e:
590
+ if "eval_sx" not in str(e):
591
+ raise
592
+ else:
593
+ logger.info(
594
+ "Cannot expand objective/constraints to SX, falling back to MX evaluation"
595
+ )
596
+ nlp = {"x": X, "f": objective_function, "g": equality_constraints}
597
+ else:
598
+ X_sx = ca.SX.sym("X", X.shape)
599
+ objective_function_sx, equality_constraints_sx = expand_f_g(X_sx)
600
+ nlp = {"x": X_sx, "f": objective_function_sx, "g": equality_constraints_sx}
601
+
602
+ solver = ca.nlpsol("solver", "ipopt", nlp, self.solver_options())
392
603
 
393
604
  # Construct guess
394
- guess = ca.vertcat(*np.nan_to_num(self.__state_vector[:self.__states_end_index]))
605
+ guess = ca.vertcat(*np.nan_to_num(self.__state_vector[: self.__n_states]))
395
606
 
396
607
  # Find initial state
397
- initial_state = solver(x0=guess,
398
- lbx=self.__lbx, ubx=self.__ubx,
399
- lbg=lbg, ubg=ubg)
608
+ initial_state = solver(x0=guess, lbx=self.__lbx, ubx=self.__ubx, lbg=lbg, ubg=ubg)
400
609
 
401
610
  # If unsuccessful, stop.
402
- return_status = solver.stats()['return_status']
403
- if return_status not in {'Solve_Succeeded', 'Solved_To_Acceptable_Level'}:
611
+ return_status = solver.stats()["return_status"]
612
+ if return_status not in {"Solve_Succeeded", "Solved_To_Acceptable_Level"}:
404
613
  raise Exception('Initialization Failed with return status "{}"'.format(return_status))
405
614
 
406
615
  # Update state vector with initial conditions
407
- self.__state_vector[:self.__states_end_index] = initial_state['x'][:self.__states_end_index].T
616
+ self.__state_vector[: self.__n_states] = initial_state["x"][: self.__n_states].T
408
617
 
409
618
  # make a copy of the initialized initial state vector in case we want to run the model again
410
619
  self.__initialized_state_vector = copy.deepcopy(self.__state_vector)
@@ -422,29 +631,52 @@ class SimulationProblem(DataStoreAccessor):
422
631
 
423
632
  # Construct the rootfinder
424
633
 
425
- # Assemble some symbolics, including those needed for a backwards Euler derivative approximation
634
+ # Assemble some symbolics, including those needed for a backwards Euler derivative
635
+ # approximation
426
636
  dt = ca.MX.sym("delta_t")
427
- parameters = ca.vertcat(*self.__mx['parameters'])
637
+ parameters = ca.vertcat(*self.__mx["parameters"])
428
638
  if n_parameters > 0:
429
- constants = ca.vertcat(X_prev, *self.__sym_list[self.__states_end_index:-n_parameters])
639
+ constants = ca.vertcat(X_prev, *self.__sym_list[self.__n_state_symbols : -n_parameters])
430
640
  else:
431
- constants = ca.vertcat(X_prev, *self.__sym_list[self.__states_end_index:])
641
+ constants = ca.vertcat(X_prev, *self.__sym_list[self.__n_state_symbols :])
432
642
 
433
643
  # Make a list of derivative approximations using backwards Euler formulation
434
644
  derivative_approximation_residuals = []
435
- for index, derivative_state in enumerate(self.__mx['derivatives']):
436
- derivative_approximation_residuals.append(derivative_state - (X[index] - X_prev[index]) / dt)
645
+ for index, derivative_state in enumerate(self.__mx["derivatives"]):
646
+ derivative_approximation_residuals.append(
647
+ derivative_state - (X[index] - X_prev[index]) / dt
648
+ )
437
649
 
438
- # Delayed feedback (assuming zero delay)
439
- # TODO: implement delayed feedback support for delay != 0
440
- delayed_feedback_equations = []
441
- for delay_state, delay_argument in zip(self.__pymoca_model.delay_states,
442
- self.__pymoca_model.delay_arguments):
443
- logger.warning("Assuming zero delay for delay state '{}'".format(delay_state))
444
- delayed_feedback_equations.append(delay_argument.expr - self.__sym_dict[delay_state])
650
+ # Delayed feedback
651
+ delay_equations = []
652
+ for delay_state, delay_argument, delay_time in zip(
653
+ model.delay_states,
654
+ model.delay_arguments,
655
+ self.__delay_times,
656
+ ):
657
+ expression_state = delay_state + "_expr"
658
+ i_delay_state, _ = self.__indices[delay_state]
659
+ i_expr_start, _ = self.__i_start[expression_state]
660
+ i_expr_end, _ = self.__i_end[expression_state]
661
+ delay_equations.append(X[i_expr_start] - delay_argument.expr)
662
+ delay_equations.append(
663
+ X[i_expr_start + 1 : i_expr_end] - X_prev[i_expr_start : i_expr_end - 1]
664
+ )
665
+ n_previous_values = self.__sym_dict[expression_state].numel()
666
+ interpolation_weight = n_previous_values - delay_time / self.__dt
667
+ delay_equations.append(
668
+ X[i_delay_state]
669
+ - interpolation_weight * X[i_expr_end - 1]
670
+ - (1 - interpolation_weight) * X_prev[i_expr_end - 1]
671
+ )
445
672
 
446
673
  # Append residuals for derivative approximations
447
- dae_residual = ca.vertcat(self.__dae_residual, *derivative_approximation_residuals, *delayed_feedback_equations)
674
+ dae_residual = ca.vertcat(
675
+ self.__dae_residual,
676
+ *derivative_approximation_residuals,
677
+ *delay_equations,
678
+ *extra_equations,
679
+ )
448
680
 
449
681
  # TODO: implement lookup_tables
450
682
 
@@ -457,18 +689,30 @@ class SimulationProblem(DataStoreAccessor):
457
689
  dae_residual = ca.substitute(dae_residual, parameters, parameters_values)
458
690
 
459
691
  if logger.getEffectiveLevel() == logging.DEBUG:
460
- logger.debug('SimulationProblem: DAE Residual is ' + str(dae_residual))
692
+ logger.debug("SimulationProblem: DAE Residual is " + str(dae_residual))
461
693
 
462
694
  if X.size1() != dae_residual.size1():
463
- logger.error('Formulation Error: Number of states ({}) does not equal number of equations ({})'.format(
464
- X.size1(), dae_residual.size1()))
695
+ logger.error(
696
+ "Formulation Error: Number of states ({}) "
697
+ "does not equal number of equations ({})".format(X.size1(), dae_residual.size1())
698
+ )
465
699
 
466
700
  # Construct a function res_vals that returns the numerical residuals of a numerical state
467
- self.__res_vals = ca.Function("res_vals", [X, dt, constants], [dae_residual]).expand()
701
+ self.__res_vals = ca.Function("res_vals", [X, dt, constants], [dae_residual])
702
+ try:
703
+ self.__res_vals = self.__res_vals.expand()
704
+ except RuntimeError as e:
705
+ if "eval_sx" not in str(e):
706
+ raise
707
+ else:
708
+ pass
468
709
 
469
- # Use rootfinder() to make a function that takes a step forward in time by trying to zero res_vals()
470
- options = {'nlpsol': 'ipopt', 'nlpsol_options': self.solver_options(), 'error_on_fail': False}
471
- self.__do_step = ca.rootfinder("next_state", "nlpsol", self.__res_vals, options)
710
+ # Use rootfinder() to make a function that takes a step forward in time by trying to zero
711
+ # res_vals()
712
+ options = self.rootfinder_options()
713
+ solver = options["solver"]
714
+ solver_options = options["solver_options"]
715
+ self.__do_step = ca.rootfinder("next_state", solver, self.__res_vals, solver_options)
472
716
 
473
717
  def pre(self):
474
718
  """
@@ -484,7 +728,8 @@ class SimulationProblem(DataStoreAccessor):
484
728
 
485
729
  def setup_experiment(self, start, stop, dt):
486
730
  """
487
- Method for subclasses (PIMixin, CSVMixin, or user classes) to set timing information for a simulation run.
731
+ Method for subclasses (PIMixin, CSVMixin, or user classes) to set timing information for a
732
+ simulation run.
488
733
 
489
734
  :param start: Start time for the simulation.
490
735
  :param stop: Final time for the simulation.
@@ -494,10 +739,10 @@ class SimulationProblem(DataStoreAccessor):
494
739
  # Set class vars with start/stop/dt values
495
740
  self.__start = start
496
741
  self.__stop = stop
497
- self.__dt = dt
742
+ self.set_time_step(dt)
498
743
 
499
744
  # Set time in state vector
500
- self.set_var('time', start)
745
+ self.set_var("time", start)
501
746
 
502
747
  def update(self, dt):
503
748
  """
@@ -507,40 +752,44 @@ class SimulationProblem(DataStoreAccessor):
507
752
 
508
753
  :param dt: Time step size.
509
754
  """
510
- if dt < 0:
511
- dt = self.__dt
755
+ if dt > 0:
756
+ self.set_time_step(dt)
757
+ dt = self.get_time_step()
512
758
 
513
759
  logger.debug("Taking a step at {} with size {}".format(self.get_current_time(), dt))
514
760
 
515
761
  # increment time
516
- self.set_var('time', self.get_current_time() + dt)
762
+ self.set_var("time", self.get_current_time() + dt)
517
763
 
518
764
  # take a step
519
- guess = self.__state_vector[:self.__states_end_index]
520
- if len(self.__mx['parameters']) > 0:
521
- next_state = self.__do_step(guess, dt, self.__state_vector[:-len(self.__mx['parameters'])])
765
+ guess = self.__state_vector[: self.__n_states]
766
+ if len(self.__mx["parameters"]) > 0:
767
+ next_state = self.__do_step(
768
+ guess, dt, self.__state_vector[: -len(self.__mx["parameters"])]
769
+ )
522
770
  else:
523
771
  next_state = self.__do_step(guess, dt, self.__state_vector)
524
772
  # Check convergence of rootfinder
525
773
  rootfinder_stats = self.__do_step.stats()
526
774
 
527
- if not rootfinder_stats['success']:
775
+ if not rootfinder_stats["success"]:
528
776
  message = (
529
- 'Simulation has failed to converge at time {}. '
530
- 'Solver failed with status {}'
531
- ).format(self.get_current_time(), rootfinder_stats['nlpsol']['return_status'])
777
+ "Simulation has failed to converge at time {}. Solver failed with status {}"
778
+ ).format(self.get_current_time(), rootfinder_stats["nlpsol"]["return_status"])
532
779
  logger.error(message)
533
780
  raise Exception(message)
534
781
 
535
782
  if logger.getEffectiveLevel() == logging.DEBUG:
536
783
  # compute max residual
537
784
  largest_res = ca.norm_inf(
538
- self.__res_vals(next_state, self.__dt, self.__state_vector[:-len(self.__mx['parameters'])])
785
+ self.__res_vals(
786
+ next_state, self.__dt, self.__state_vector[: -len(self.__mx["parameters"])]
787
+ )
539
788
  )
540
- logger.debug('Residual maximum magnitude: {:.2E}'.format(float(largest_res)))
789
+ logger.debug("Residual maximum magnitude: {:.2E}".format(float(largest_res)))
541
790
 
542
791
  # Update state vector
543
- self.__state_vector[:self.__states_end_index] = next_state.toarray().ravel()
792
+ self.__state_vector[: self.__n_states] = next_state.toarray().ravel()
544
793
 
545
794
  def simulate(self):
546
795
  """
@@ -593,7 +842,7 @@ class SimulationProblem(DataStoreAccessor):
593
842
 
594
843
  :returns: The current simulation time.
595
844
  """
596
- return self.get_var('time')
845
+ return self.get_var("time")
597
846
 
598
847
  def get_time_step(self):
599
848
  """
@@ -621,7 +870,7 @@ class SimulationProblem(DataStoreAccessor):
621
870
  value *= sign
622
871
 
623
872
  # Adjust for nominal value if not default
624
- if index <= self.__states_end_index:
873
+ if index <= self.__n_states:
625
874
  nominal = self.get_variable_nominal(name)
626
875
  value *= nominal
627
876
 
@@ -675,25 +924,24 @@ class SimulationProblem(DataStoreAccessor):
675
924
 
676
925
  :returns: An ordered dictionary of all variables supported by the model.
677
926
  """
678
- return self.__sym_dict
927
+ return AliasDict(self.alias_relation, self.__sym_dict)
679
928
 
680
929
  @cached
681
930
  def get_state_variables(self):
682
931
  return AliasDict(
683
932
  self.alias_relation,
684
- {sym.name(): sym for sym in (self.__mx['states'] + self.__mx['algebraics'])})
933
+ {sym.name(): sym for sym in (self.__mx["states"] + self.__mx["algebraics"])},
934
+ )
685
935
 
686
936
  @cached
687
937
  def get_parameter_variables(self):
688
- return AliasDict(
689
- self.alias_relation,
690
- {sym.name(): sym for sym in self.__mx['parameters']})
938
+ return AliasDict(self.alias_relation, {sym.name(): sym for sym in self.__mx["parameters"]})
691
939
 
692
940
  @cached
693
941
  def get_input_variables(self):
694
942
  return AliasDict(
695
- self.alias_relation,
696
- {sym.name(): sym for sym in self.__mx['constant_inputs']})
943
+ self.alias_relation, {sym.name(): sym for sym in self.__mx["constant_inputs"]}
944
+ )
697
945
 
698
946
  @cached
699
947
  def get_output_variables(self):
@@ -707,7 +955,20 @@ class SimulationProblem(DataStoreAccessor):
707
955
  if any(value_is_nan):
708
956
  for sym, isnan in zip(self.__sym_list, value_is_nan):
709
957
  if isnan:
710
- logger.warning('Variable {} has no value.'.format(sym))
958
+ logger.warning("Variable {} has no value.".format(sym))
959
+
960
+ def set_time_step(self, dt):
961
+ """
962
+ Set the timestep size.
963
+
964
+ :param dt: Timestep size of the simulation.
965
+ """
966
+ if self._dt_is_fixed:
967
+ assert math.isclose(
968
+ self.__dt, dt
969
+ ), "Timestep size dt is marked as constant and cannot be changed."
970
+ else:
971
+ self.__dt = dt
711
972
 
712
973
  def set_var(self, name, value):
713
974
  """
@@ -730,7 +991,7 @@ class SimulationProblem(DataStoreAccessor):
730
991
  value *= sign
731
992
 
732
993
  # Adjust for nominal value if not default
733
- if index <= self.__states_end_index:
994
+ if index <= self.__n_states:
734
995
  nominal = self.get_variable_nominal(name)
735
996
  value /= nominal
736
997
 
@@ -763,14 +1024,37 @@ class SimulationProblem(DataStoreAccessor):
763
1024
 
764
1025
  def solver_options(self):
765
1026
  """
766
- Returns a dictionary of CasADi root_finder() solver options.
1027
+ Returns a dictionary of CasADi nlpsol() solver options.
1028
+
1029
+ :returns: A dictionary of CasADi :class:`nlpsol` options. See the CasADi
1030
+ documentation for details.
1031
+ """
1032
+ return {
1033
+ "ipopt.fixed_variable_treatment": "make_parameter",
1034
+ "ipopt.print_level": 0,
1035
+ "print_time": False,
1036
+ "error_on_fail": False,
1037
+ }
1038
+
1039
+ def rootfinder_options(self):
1040
+ """
1041
+ Returns a dictionary of CasADi rootfinder() options.
1042
+
1043
+ The dictionary has the following items:
1044
+ * solver: the solver used for rootfinding, e.g. nlpsol, fast_newton, etc.
1045
+ * solver_options: options for the solver
1046
+ See the CasADi documentation for details on solvers and solver options.
767
1047
 
768
- :returns: A dictionary of CasADi :class:`root_finder` options. See the CasADi documentation for details.
1048
+ :returns: A dictionary of CasADi :class:`rootfinder` options.
769
1049
  """
770
- return {'ipopt.fixed_variable_treatment': 'make_parameter',
771
- 'ipopt.print_level': 0,
772
- 'print_time': False,
773
- 'error_on_fail': False}
1050
+ return {
1051
+ "solver": "nlpsol",
1052
+ "solver_options": {
1053
+ "nlpsol": "ipopt",
1054
+ "nlpsol_options": self.solver_options(),
1055
+ "error_on_fail": False,
1056
+ },
1057
+ }
774
1058
 
775
1059
  def get_variable_nominal(self, variable) -> Union[float, ca.MX]:
776
1060
  """
@@ -835,6 +1119,12 @@ class SimulationProblem(DataStoreAccessor):
835
1119
 
836
1120
  return parameters
837
1121
 
1122
+ def extra_variables(self) -> List[Variable]:
1123
+ return []
1124
+
1125
+ def extra_equations(self) -> List[ca.MX]:
1126
+ return []
1127
+
838
1128
  @property
839
1129
  @cached
840
1130
  def alias_relation(self):
@@ -843,52 +1133,53 @@ class SimulationProblem(DataStoreAccessor):
843
1133
  @cached
844
1134
  def compiler_options(self):
845
1135
  """
846
- Subclasses can configure the `pymoca <http://github.com/pymoca/pymoca>`_ compiler options here.
1136
+ Subclasses can configure the `pymoca <http://github.com/pymoca/pymoca>`_ compiler options
1137
+ here.
847
1138
 
848
- :returns: A dictionary of pymoca compiler options. See the pymoca documentation for details.
1139
+ :returns:
1140
+ A dictionary of pymoca compiler options. See the pymoca documentation for details.
849
1141
  """
850
1142
 
851
1143
  # Default options
852
1144
  compiler_options = {}
853
1145
 
854
1146
  # Expand vector states to multiple scalar component states.
855
- compiler_options['expand_vectors'] = True
1147
+ compiler_options["expand_vectors"] = True
856
1148
 
857
1149
  # Where imported model libraries are located.
858
1150
  library_folders = self.modelica_library_folders.copy()
859
1151
 
860
- for ep in pkg_resources.iter_entry_points(group='rtctools.libraries.modelica'):
1152
+ for ep in pkg_resources.iter_entry_points(group="rtctools.libraries.modelica"):
861
1153
  if ep.name == "library_folder":
862
- library_folders.append(
863
- pkg_resources.resource_filename(ep.module_name, ep.attrs[0]))
1154
+ library_folders.append(pkg_resources.resource_filename(ep.module_name, ep.attrs[0]))
864
1155
 
865
- compiler_options['library_folders'] = library_folders
1156
+ compiler_options["library_folders"] = library_folders
866
1157
 
867
1158
  # Eliminate equations of the type 'var = const'.
868
- compiler_options['eliminate_constant_assignments'] = True
1159
+ compiler_options["eliminate_constant_assignments"] = True
869
1160
 
870
1161
  # Eliminate constant symbols from model, replacing them with the values
871
1162
  # specified in the model.
872
- compiler_options['replace_constant_values'] = True
1163
+ compiler_options["replace_constant_values"] = True
873
1164
 
874
1165
  # Replace any constant expressions into the model.
875
- compiler_options['replace_constant_expressions'] = True
1166
+ compiler_options["replace_constant_expressions"] = True
876
1167
 
877
1168
  # Replace any parameter expressions into the model.
878
- compiler_options['replace_parameter_expressions'] = True
1169
+ compiler_options["replace_parameter_expressions"] = True
879
1170
 
880
1171
  # Eliminate variables starting with underscores.
881
- compiler_options['eliminable_variable_expression'] = r'(.*[.]|^)_\w+(\[[\d,]+\])?\Z'
1172
+ compiler_options["eliminable_variable_expression"] = r"(.*[.]|^)_\w+(\[[\d,]+\])?\Z"
882
1173
 
883
1174
  # Pymoca currently requires `expand_mx` to be set for
884
1175
  # `eliminable_variable_expression` to work.
885
- compiler_options['expand_mx'] = True
1176
+ compiler_options["expand_mx"] = True
886
1177
 
887
1178
  # Automatically detect and eliminate alias variables.
888
- compiler_options['detect_aliases'] = True
1179
+ compiler_options["detect_aliases"] = True
889
1180
 
890
1181
  # Cache the model on disk
891
- compiler_options['cache'] = True
1182
+ compiler_options["cache"] = True
892
1183
 
893
1184
  # Done
894
1185
  return compiler_options