rtc-tools 2.5.2rc3__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.
- {rtc_tools-2.5.2rc3.dist-info → rtc_tools-2.6.0.dist-info}/METADATA +7 -7
- rtc_tools-2.6.0.dist-info/RECORD +50 -0
- {rtc_tools-2.5.2rc3.dist-info → rtc_tools-2.6.0.dist-info}/WHEEL +1 -1
- rtctools/__init__.py +2 -1
- rtctools/_internal/alias_tools.py +12 -10
- rtctools/_internal/caching.py +5 -3
- rtctools/_internal/casadi_helpers.py +11 -32
- rtctools/_internal/debug_check_helpers.py +1 -1
- rtctools/_version.py +3 -3
- rtctools/data/__init__.py +2 -2
- rtctools/data/csv.py +54 -33
- rtctools/data/interpolation/bspline.py +3 -3
- rtctools/data/interpolation/bspline1d.py +42 -29
- rtctools/data/interpolation/bspline2d.py +10 -4
- rtctools/data/netcdf.py +137 -93
- rtctools/data/pi.py +304 -210
- rtctools/data/rtc.py +64 -53
- rtctools/data/storage.py +91 -51
- rtctools/optimization/collocated_integrated_optimization_problem.py +1244 -696
- rtctools/optimization/control_tree_mixin.py +68 -66
- rtctools/optimization/csv_lookup_table_mixin.py +107 -74
- rtctools/optimization/csv_mixin.py +83 -52
- rtctools/optimization/goal_programming_mixin.py +239 -148
- rtctools/optimization/goal_programming_mixin_base.py +204 -111
- rtctools/optimization/homotopy_mixin.py +36 -27
- rtctools/optimization/initial_state_estimation_mixin.py +8 -8
- rtctools/optimization/io_mixin.py +48 -43
- rtctools/optimization/linearization_mixin.py +3 -1
- rtctools/optimization/linearized_order_goal_programming_mixin.py +57 -28
- rtctools/optimization/min_abs_goal_programming_mixin.py +72 -29
- rtctools/optimization/modelica_mixin.py +135 -81
- rtctools/optimization/netcdf_mixin.py +32 -18
- rtctools/optimization/optimization_problem.py +181 -127
- rtctools/optimization/pi_mixin.py +68 -36
- rtctools/optimization/planning_mixin.py +19 -0
- rtctools/optimization/single_pass_goal_programming_mixin.py +159 -112
- rtctools/optimization/timeseries.py +4 -6
- rtctools/rtctoolsapp.py +18 -18
- rtctools/simulation/csv_mixin.py +37 -30
- rtctools/simulation/io_mixin.py +9 -5
- rtctools/simulation/pi_mixin.py +62 -32
- rtctools/simulation/simulation_problem.py +471 -180
- rtctools/util.py +84 -56
- rtc_tools-2.5.2rc3.dist-info/RECORD +0 -49
- {rtc_tools-2.5.2rc3.dist-info → rtc_tools-2.6.0.dist-info}/COPYING.LESSER +0 -0
- {rtc_tools-2.5.2rc3.dist-info → rtc_tools-2.6.0.dist-info}/entry_points.txt +0 -0
- {rtc_tools-2.5.2rc3.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
|
|
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
|
|
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
|
|
54
|
-
model_name = kwargs[
|
|
76
|
+
if "model_name" in kwargs:
|
|
77
|
+
model_name = kwargs["model_name"]
|
|
55
78
|
else:
|
|
56
|
-
if hasattr(self,
|
|
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[
|
|
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[
|
|
68
|
-
self.__mx[
|
|
69
|
-
self.__mx[
|
|
70
|
-
self.__mx[
|
|
71
|
-
self.__mx[
|
|
72
|
-
self.__mx[
|
|
73
|
-
self.__mx[
|
|
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[
|
|
104
|
+
self.__mx["algebraics"].append(v.symbol)
|
|
81
105
|
else:
|
|
82
|
-
if v.symbol.name() in kwargs.get(
|
|
83
|
-
self.__mx[
|
|
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[
|
|
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(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 = [
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
124
|
-
|
|
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 = [
|
|
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 =
|
|
143
|
-
self.__mx[
|
|
144
|
-
|
|
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.
|
|
148
|
-
len(self.__mx[
|
|
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[
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
366
|
+
nominal_evaluator = ca.Function(
|
|
367
|
+
"nominal_evaluator", self.__mx["parameters"], [symbolic_nominals]
|
|
368
|
+
)
|
|
214
369
|
|
|
215
|
-
n_parameters = len(self.__mx[
|
|
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
|
|
244
|
-
# with names that match a symbol in the model. We only check for this
|
|
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(
|
|
253
|
-
|
|
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
|
|
259
|
-
# with names that match a symbol in the model. We only check for this
|
|
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(
|
|
268
|
-
|
|
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(
|
|
275
|
-
|
|
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[
|
|
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,
|
|
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[
|
|
299
|
-
logger.debug(
|
|
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
|
-
|
|
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(
|
|
308
|
-
|
|
309
|
-
|
|
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.
|
|
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(
|
|
337
|
-
logger.debug(
|
|
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 =
|
|
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(
|
|
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[
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
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(
|
|
380
|
-
|
|
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(
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
391
|
-
|
|
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.
|
|
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()[
|
|
403
|
-
if return_status not in {
|
|
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.
|
|
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
|
|
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[
|
|
637
|
+
parameters = ca.vertcat(*self.__mx["parameters"])
|
|
428
638
|
if n_parameters > 0:
|
|
429
|
-
constants = ca.vertcat(X_prev, *self.__sym_list[self.
|
|
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.
|
|
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[
|
|
436
|
-
derivative_approximation_residuals.append(
|
|
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
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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(
|
|
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(
|
|
692
|
+
logger.debug("SimulationProblem: DAE Residual is " + str(dae_residual))
|
|
461
693
|
|
|
462
694
|
if X.size1() != dae_residual.size1():
|
|
463
|
-
logger.error(
|
|
464
|
-
|
|
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])
|
|
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
|
|
470
|
-
|
|
471
|
-
|
|
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
|
|
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.
|
|
742
|
+
self.set_time_step(dt)
|
|
498
743
|
|
|
499
744
|
# Set time in state vector
|
|
500
|
-
self.set_var(
|
|
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
|
|
511
|
-
|
|
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(
|
|
762
|
+
self.set_var("time", self.get_current_time() + dt)
|
|
517
763
|
|
|
518
764
|
# take a step
|
|
519
|
-
guess = self.__state_vector[:self.
|
|
520
|
-
if len(self.__mx[
|
|
521
|
-
next_state = self.__do_step(
|
|
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[
|
|
775
|
+
if not rootfinder_stats["success"]:
|
|
528
776
|
message = (
|
|
529
|
-
|
|
530
|
-
|
|
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(
|
|
785
|
+
self.__res_vals(
|
|
786
|
+
next_state, self.__dt, self.__state_vector[: -len(self.__mx["parameters"])]
|
|
787
|
+
)
|
|
539
788
|
)
|
|
540
|
-
logger.debug(
|
|
789
|
+
logger.debug("Residual maximum magnitude: {:.2E}".format(float(largest_res)))
|
|
541
790
|
|
|
542
791
|
# Update state vector
|
|
543
|
-
self.__state_vector[:self.
|
|
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(
|
|
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.
|
|
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[
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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:`
|
|
1048
|
+
:returns: A dictionary of CasADi :class:`rootfinder` options.
|
|
769
1049
|
"""
|
|
770
|
-
return {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
|
1136
|
+
Subclasses can configure the `pymoca <http://github.com/pymoca/pymoca>`_ compiler options
|
|
1137
|
+
here.
|
|
847
1138
|
|
|
848
|
-
:returns:
|
|
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[
|
|
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=
|
|
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[
|
|
1156
|
+
compiler_options["library_folders"] = library_folders
|
|
866
1157
|
|
|
867
1158
|
# Eliminate equations of the type 'var = const'.
|
|
868
|
-
compiler_options[
|
|
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[
|
|
1163
|
+
compiler_options["replace_constant_values"] = True
|
|
873
1164
|
|
|
874
1165
|
# Replace any constant expressions into the model.
|
|
875
|
-
compiler_options[
|
|
1166
|
+
compiler_options["replace_constant_expressions"] = True
|
|
876
1167
|
|
|
877
1168
|
# Replace any parameter expressions into the model.
|
|
878
|
-
compiler_options[
|
|
1169
|
+
compiler_options["replace_parameter_expressions"] = True
|
|
879
1170
|
|
|
880
1171
|
# Eliminate variables starting with underscores.
|
|
881
|
-
compiler_options[
|
|
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[
|
|
1176
|
+
compiler_options["expand_mx"] = True
|
|
886
1177
|
|
|
887
1178
|
# Automatically detect and eliminate alias variables.
|
|
888
|
-
compiler_options[
|
|
1179
|
+
compiler_options["detect_aliases"] = True
|
|
889
1180
|
|
|
890
1181
|
# Cache the model on disk
|
|
891
|
-
compiler_options[
|
|
1182
|
+
compiler_options["cache"] = True
|
|
892
1183
|
|
|
893
1184
|
# Done
|
|
894
1185
|
return compiler_options
|