pybounds 0.0.14__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.
- pybounds/__init__.py +13 -0
- pybounds/jacobian.py +46 -0
- pybounds/observability.py +764 -0
- pybounds/simulator.py +477 -0
- pybounds/util.py +279 -0
- pybounds-0.0.14.dist-info/METADATA +65 -0
- pybounds-0.0.14.dist-info/RECORD +10 -0
- pybounds-0.0.14.dist-info/WHEEL +5 -0
- pybounds-0.0.14.dist-info/licenses/LICENSE +21 -0
- pybounds-0.0.14.dist-info/top_level.txt +1 -0
pybounds/simulator.py
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
|
|
2
|
+
import warnings
|
|
3
|
+
from functools import wraps
|
|
4
|
+
import numpy as np
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import casadi
|
|
7
|
+
import do_mpc
|
|
8
|
+
from .util import FixedKeysDict, SetDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Simulator(object):
|
|
12
|
+
def __init__(self, f, h, dt=0.01,
|
|
13
|
+
discrete=False,
|
|
14
|
+
n=None, m=None,
|
|
15
|
+
state_names=None, input_names=None, measurement_names=None,
|
|
16
|
+
params_simulator=None, mpc_horizon=10):
|
|
17
|
+
|
|
18
|
+
""" Simulator.
|
|
19
|
+
|
|
20
|
+
:param callable f: dynamics function f(X, U, t)
|
|
21
|
+
:param callable h: measurement function h(X, U, t)
|
|
22
|
+
:param float dt: sampling time in seconds
|
|
23
|
+
:param int n: number of states, optional but cannot be set if state_names is set
|
|
24
|
+
:param int m: number of inputs, optional but cannot be set if input_names is set
|
|
25
|
+
:param list state_names: names of states, optional but cannot be set if n is set
|
|
26
|
+
:param list input_names: names of inputs, optional but cannot be set if m is set
|
|
27
|
+
:param list measurement_names: names of measurements, optional
|
|
28
|
+
:param dict params_simulator: simulation parameters, optional
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
self.f = f
|
|
32
|
+
self.h = ensure_float_output(h)
|
|
33
|
+
self.dt = dt
|
|
34
|
+
|
|
35
|
+
# Set state names
|
|
36
|
+
if state_names is None: # default state names
|
|
37
|
+
if n is None:
|
|
38
|
+
raise ValueError('must set state_names or n')
|
|
39
|
+
else:
|
|
40
|
+
self.n = int(n)
|
|
41
|
+
|
|
42
|
+
self.state_names = ['x_' + str(n) for n in range(self.n)]
|
|
43
|
+
else: # state names given
|
|
44
|
+
if n is not None:
|
|
45
|
+
raise ValueError('cannot set state_names and n')
|
|
46
|
+
|
|
47
|
+
self.state_names = list(state_names)
|
|
48
|
+
self.n = len(self.state_names)
|
|
49
|
+
if len(self.state_names) != self.n:
|
|
50
|
+
raise ValueError('state_names must have length equal to x0')
|
|
51
|
+
|
|
52
|
+
# Set input names
|
|
53
|
+
if input_names is None: # default input names
|
|
54
|
+
if m is None:
|
|
55
|
+
raise ValueError('must set input_names or m')
|
|
56
|
+
else:
|
|
57
|
+
self.m = int(m)
|
|
58
|
+
|
|
59
|
+
self.input_names = ['u_' + str(m) for m in range(self.m)]
|
|
60
|
+
else: # input names given
|
|
61
|
+
if m is not None:
|
|
62
|
+
raise ValueError('cannot set in and n')
|
|
63
|
+
|
|
64
|
+
self.input_names = list(input_names)
|
|
65
|
+
self.m = len(self.input_names)
|
|
66
|
+
if len(self.input_names) != self.m:
|
|
67
|
+
raise ValueError('input_names must have length equal to u0')
|
|
68
|
+
|
|
69
|
+
# Run measurement function to get measurement size
|
|
70
|
+
x0 = np.ones(self.n)
|
|
71
|
+
u0 = np.ones(self.m)
|
|
72
|
+
y = self.h(np.ravel(x0), np.ravel(u0))
|
|
73
|
+
self.p = len(y) # number of measurements
|
|
74
|
+
|
|
75
|
+
# Set measurement names
|
|
76
|
+
if measurement_names is None: # default measurement names
|
|
77
|
+
self.measurement_names = ['y_' + str(p) for p in range(self.p)]
|
|
78
|
+
else:
|
|
79
|
+
self.measurement_names = measurement_names
|
|
80
|
+
if len(self.measurement_names) != self.p:
|
|
81
|
+
raise ValueError('measurement_names must have length equal to y')
|
|
82
|
+
|
|
83
|
+
# Initialize time vector
|
|
84
|
+
self.w = 11 # initialize for w time-steps, but this can change later
|
|
85
|
+
self.time = np.arange(0, self.w * self.dt, step=self.dt) # time vector
|
|
86
|
+
|
|
87
|
+
# Define initial states & initialize state time-series
|
|
88
|
+
self.x0 = {}
|
|
89
|
+
self.x = {}
|
|
90
|
+
for n, state_name in enumerate(self.state_names):
|
|
91
|
+
self.x0[state_name] = x0[n]
|
|
92
|
+
self.x[state_name] = x0[n] * np.ones(self.w)
|
|
93
|
+
|
|
94
|
+
self.x0 = FixedKeysDict(self.x0)
|
|
95
|
+
self.x = FixedKeysDict(self.x)
|
|
96
|
+
|
|
97
|
+
# Initialize input time-series
|
|
98
|
+
self.u = {}
|
|
99
|
+
for m, input_name in enumerate(self.input_names):
|
|
100
|
+
self.u[input_name] = u0[m] * np.ones(self.w)
|
|
101
|
+
|
|
102
|
+
self.u = FixedKeysDict(self.u)
|
|
103
|
+
|
|
104
|
+
# Initialize measurement time-series
|
|
105
|
+
self.y = {}
|
|
106
|
+
for p, measurement_name in enumerate(self.measurement_names):
|
|
107
|
+
self.y[measurement_name] = 0.0 * np.ones(self.w)
|
|
108
|
+
|
|
109
|
+
self.y = FixedKeysDict(self.y)
|
|
110
|
+
|
|
111
|
+
# Initialize state set-points
|
|
112
|
+
self.setpoint = {}
|
|
113
|
+
for n, state_name in enumerate(self.state_names):
|
|
114
|
+
self.setpoint[state_name] = 0.0 * np.ones(self.w)
|
|
115
|
+
|
|
116
|
+
self.setpoint = FixedKeysDict(self.setpoint)
|
|
117
|
+
|
|
118
|
+
# Define MPC model
|
|
119
|
+
if discrete:
|
|
120
|
+
model_type = 'discrete'
|
|
121
|
+
else:
|
|
122
|
+
model_type = 'continuous'
|
|
123
|
+
self.model = do_mpc.model.Model(model_type)
|
|
124
|
+
|
|
125
|
+
# Define state variables
|
|
126
|
+
X = []
|
|
127
|
+
for n, state_name in enumerate(self.state_names):
|
|
128
|
+
x = self.model.set_variable(var_type='_x', var_name=state_name, shape=(1, 1))
|
|
129
|
+
X.append(x)
|
|
130
|
+
|
|
131
|
+
# Define input variables
|
|
132
|
+
U = []
|
|
133
|
+
for m, input_name in enumerate(self.input_names):
|
|
134
|
+
u = self.model.set_variable(var_type='_u', var_name=input_name, shape=(1, 1))
|
|
135
|
+
U.append(u)
|
|
136
|
+
|
|
137
|
+
# Define dynamics
|
|
138
|
+
Xdot = self.f(X, U)
|
|
139
|
+
for n, state_name in enumerate(self.state_names):
|
|
140
|
+
self.model.set_rhs(state_name, casadi.SX(Xdot[n]))
|
|
141
|
+
|
|
142
|
+
# Add time-varying set-point variables for later use with MPC
|
|
143
|
+
for n, state_name in enumerate(self.state_names):
|
|
144
|
+
x = self.model.set_variable(var_type='_tvp', var_name=state_name + str('_set'), shape=(1, 1))
|
|
145
|
+
|
|
146
|
+
# Build model
|
|
147
|
+
self.model.setup()
|
|
148
|
+
|
|
149
|
+
# Define simulator & simulator parameters
|
|
150
|
+
self.simulator = do_mpc.simulator.Simulator(self.model)
|
|
151
|
+
|
|
152
|
+
# Set simulation parameters
|
|
153
|
+
if params_simulator is None:
|
|
154
|
+
if self.model.model_type == 'continuous':
|
|
155
|
+
self.params_simulator = {
|
|
156
|
+
'integration_tool': 'idas', # cvodes, idas
|
|
157
|
+
'abstol': 1e-8,
|
|
158
|
+
'reltol': 1e-8,
|
|
159
|
+
't_step': self.dt
|
|
160
|
+
}
|
|
161
|
+
else:
|
|
162
|
+
self.params_simulator = {
|
|
163
|
+
't_step': self.dt
|
|
164
|
+
}
|
|
165
|
+
else:
|
|
166
|
+
self.params_simulator = params_simulator
|
|
167
|
+
|
|
168
|
+
self.simulator.set_param(**self.params_simulator)
|
|
169
|
+
|
|
170
|
+
# Setup MPC
|
|
171
|
+
self.mpc = do_mpc.controller.MPC(self.model)
|
|
172
|
+
self.mpc_horizon = mpc_horizon
|
|
173
|
+
setup_mpc = {
|
|
174
|
+
'n_horizon': self.mpc_horizon,
|
|
175
|
+
'n_robust': 0,
|
|
176
|
+
'open_loop': 0,
|
|
177
|
+
't_step': self.dt,
|
|
178
|
+
'state_discretization': 'collocation',
|
|
179
|
+
'collocation_type': 'radau',
|
|
180
|
+
'collocation_deg': 2,
|
|
181
|
+
'collocation_ni': 1,
|
|
182
|
+
'store_full_solution': False,
|
|
183
|
+
|
|
184
|
+
# Use MA27 linear solver in ipopt for faster calculations:
|
|
185
|
+
'nlpsol_opts': {'ipopt.linear_solver': 'mumps', # mumps, MA27
|
|
186
|
+
'ipopt.print_level': 0,
|
|
187
|
+
'ipopt.sb': 'yes',
|
|
188
|
+
'print_time': 0,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
self.mpc.set_param(**setup_mpc)
|
|
193
|
+
|
|
194
|
+
# Get template's for MPC time-varying parameters
|
|
195
|
+
self.mpc_tvp_template = self.mpc.get_tvp_template()
|
|
196
|
+
self.simulator_tvp_template = self.simulator.get_tvp_template()
|
|
197
|
+
|
|
198
|
+
# Set time-varying set-point functions
|
|
199
|
+
self.mpc.set_tvp_fun(self.mpc_tvp_function)
|
|
200
|
+
self.simulator.set_tvp_fun(self.simulator_tvp_function)
|
|
201
|
+
|
|
202
|
+
# Setup simulator
|
|
203
|
+
self.simulator.setup()
|
|
204
|
+
|
|
205
|
+
def simulator_tvp_function(self, t):
|
|
206
|
+
""" Set the set-point function for MPC simulator.
|
|
207
|
+
:param t: current time
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
mpc_horizon = self.mpc._settings.n_horizon
|
|
211
|
+
|
|
212
|
+
# Set current step index
|
|
213
|
+
k_step = int(np.round(t / self.dt))
|
|
214
|
+
if k_step >= mpc_horizon: # point is beyond end of input data
|
|
215
|
+
k_step = mpc_horizon - 1 # set point beyond input data to last point
|
|
216
|
+
|
|
217
|
+
# Update current set-point
|
|
218
|
+
for n, state_name in enumerate(self.state_names):
|
|
219
|
+
self.simulator_tvp_template[state_name + '_set'] = self.setpoint[state_name][k_step]
|
|
220
|
+
|
|
221
|
+
return self.simulator_tvp_template
|
|
222
|
+
|
|
223
|
+
def mpc_tvp_function(self, t):
|
|
224
|
+
""" Set the set-point function for MPC optimizer.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
mpc_horizon = self.mpc._settings.n_horizon
|
|
228
|
+
|
|
229
|
+
# Set current step index
|
|
230
|
+
k_step = int(np.round(t / self.dt))
|
|
231
|
+
|
|
232
|
+
# Update set-point time horizon
|
|
233
|
+
for k in range(mpc_horizon + 1):
|
|
234
|
+
k_set = k_step + k
|
|
235
|
+
if k_set >= self.w: # horizon is beyond end of input data
|
|
236
|
+
k_set = self.w - 1 # set part of horizon beyond input data to last point
|
|
237
|
+
|
|
238
|
+
# Update each set-point over time horizon
|
|
239
|
+
for n, state_name in enumerate(self.state_names):
|
|
240
|
+
self.mpc_tvp_template['_tvp', k, state_name + '_set'] = self.setpoint[state_name][k_set]
|
|
241
|
+
|
|
242
|
+
return self.mpc_tvp_template
|
|
243
|
+
|
|
244
|
+
def set_initial_state(self, x0):
|
|
245
|
+
""" Update the initial state.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
if x0 is not None: # initial state given
|
|
249
|
+
if isinstance(x0, dict): # in dict format
|
|
250
|
+
SetDict().set_dict_with_overwrite(self.x0, x0) # update only the states in the dict given
|
|
251
|
+
elif isinstance(x0, list) or isinstance(x0, tuple) or (
|
|
252
|
+
x0, np.ndarray): # list, tuple, or numpy array format
|
|
253
|
+
x0 = np.array(x0).squeeze()
|
|
254
|
+
for n, key in enumerate(self.x0.keys()): # each state
|
|
255
|
+
self.x0[key] = x0[n]
|
|
256
|
+
else:
|
|
257
|
+
raise Exception('x0 must be either a dict, tuple, list, or numpy array')
|
|
258
|
+
|
|
259
|
+
def update_dict(self, data=None, name=None):
|
|
260
|
+
""" Update.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
update = getattr(self, name)
|
|
264
|
+
|
|
265
|
+
if data is not None: # data given
|
|
266
|
+
if isinstance(data, dict): # in dict format
|
|
267
|
+
SetDict().set_dict_with_overwrite(update, data) # update only the inputs in the dict given
|
|
268
|
+
|
|
269
|
+
# Normalize unset keys to be the length of the set keys be repeating the 1st element
|
|
270
|
+
unset_key = set(update.keys()) - set(data.keys()) # find keys that were not set
|
|
271
|
+
set_key = set(data.keys()) # find keys that were set
|
|
272
|
+
if unset_key != set_key:
|
|
273
|
+
w = data[list(set_key)[0]].squeeze().shape[0] # size of 1st set key
|
|
274
|
+
for k in unset_key: # update each unset key
|
|
275
|
+
update[k] = update[k][0] * np.ones(w)
|
|
276
|
+
|
|
277
|
+
elif isinstance(data, list) or isinstance(data, tuple): # list or tuple format, each input vector in each element
|
|
278
|
+
for n, k in enumerate(update.keys()): # each state
|
|
279
|
+
update[k] = data[n]
|
|
280
|
+
elif isinstance(data, np.ndarray): # numpy array format given as matrix where columns are the different inputs
|
|
281
|
+
if len(data.shape) <= 1: # given as 1d array, so convert to column vector
|
|
282
|
+
data = np.atleast_2d(data).T
|
|
283
|
+
|
|
284
|
+
for n, key in enumerate(update.keys()): # each input
|
|
285
|
+
update[key] = data[:, n]
|
|
286
|
+
|
|
287
|
+
else:
|
|
288
|
+
raise Exception(name + ' must be either a dict, tuple, list, or numpy array')
|
|
289
|
+
|
|
290
|
+
# Make sure inputs are the same size
|
|
291
|
+
points = np.array([update[key].shape[0] for key in update.keys()])
|
|
292
|
+
points_check = points == points[0]
|
|
293
|
+
if not np.all(points_check):
|
|
294
|
+
raise Exception(name + ' not the same size')
|
|
295
|
+
|
|
296
|
+
def simulate(self, x0=None, u=None, aux=None, mpc=False, return_full_output=False):
|
|
297
|
+
"""
|
|
298
|
+
Simulate the system.
|
|
299
|
+
|
|
300
|
+
:params x0: initial state dict or array
|
|
301
|
+
:params u: input dict or array, if True then mpc must be None
|
|
302
|
+
:params aux: auxiliary input
|
|
303
|
+
:params mpc: boolean to run MPC, if True then u must be None
|
|
304
|
+
:params return_full_output: boolean to run (time, x, u, y) instead of y
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
if (mpc is True) and (u is not None):
|
|
308
|
+
raise Exception('u must be None if running MPC')
|
|
309
|
+
|
|
310
|
+
if (mpc is False) and (u is None):
|
|
311
|
+
warnings.warn('not running MPC or setting u directly')
|
|
312
|
+
|
|
313
|
+
# Update the initial state
|
|
314
|
+
if x0 is None:
|
|
315
|
+
if mpc: # set the initial state to start at set-point if running MPC
|
|
316
|
+
x0 = {}
|
|
317
|
+
for state_name in self.state_names:
|
|
318
|
+
x0[state_name] = self.setpoint[state_name][0]
|
|
319
|
+
|
|
320
|
+
self.set_initial_state(x0=x0)
|
|
321
|
+
else:
|
|
322
|
+
self.set_initial_state(x0=x0)
|
|
323
|
+
|
|
324
|
+
# Update the inputs
|
|
325
|
+
self.update_dict(u, name='u')
|
|
326
|
+
|
|
327
|
+
# Concatenate the inputs, where rows are individual inputs and columns are time-steps
|
|
328
|
+
if mpc:
|
|
329
|
+
self.w = np.vstack(list(self.setpoint.values())).shape[1]
|
|
330
|
+
u_sim = np.zeros((self.w, self.m)) # preallocate input array
|
|
331
|
+
else:
|
|
332
|
+
self.w = np.vstack(list(self.u.values())).shape[1]
|
|
333
|
+
u_sim = np.vstack(list(self.u.values())).T
|
|
334
|
+
|
|
335
|
+
# Update time vector
|
|
336
|
+
T = (self.w - 1) * self.dt
|
|
337
|
+
self.time = np.linspace(0, T, num=self.w)
|
|
338
|
+
|
|
339
|
+
# Set array to store simulated states, where rows are individual states and columns are time-steps
|
|
340
|
+
x_step = np.array(list(self.x0.values())) # initialize state
|
|
341
|
+
x = np.nan * np.zeros((self.w, self.n))
|
|
342
|
+
x[0, :] = x_step.copy()
|
|
343
|
+
|
|
344
|
+
# Set array to store simulated measurements
|
|
345
|
+
y = np.nan * np.zeros((self.w, self.p))
|
|
346
|
+
|
|
347
|
+
# Initialize the simulator
|
|
348
|
+
# self.simulator = do_mpc.simulator.Simulator(self.model)
|
|
349
|
+
# self.simulator.set_param(**self.params_simulator)
|
|
350
|
+
# self.simulator.set_tvp_fun(self.simulator_tvp_function)
|
|
351
|
+
# self.simulator.setup()
|
|
352
|
+
self.simulator.reset_history() # reset simulator history (super important for speed)
|
|
353
|
+
self.simulator.t0 = self.time[0]
|
|
354
|
+
self.simulator.x0 = x_step.copy()
|
|
355
|
+
self.simulator.set_initial_guess()
|
|
356
|
+
|
|
357
|
+
# Initialize MPC
|
|
358
|
+
if mpc:
|
|
359
|
+
self.mpc.setup()
|
|
360
|
+
self.mpc.t0 = self.time[0]
|
|
361
|
+
self.mpc.x0 = x_step.copy()
|
|
362
|
+
self.mpc.u0 = np.zeros((self.m, 1))
|
|
363
|
+
self.mpc.set_initial_guess()
|
|
364
|
+
|
|
365
|
+
# Run simulation
|
|
366
|
+
for k in range(1, self.w):
|
|
367
|
+
# Set input
|
|
368
|
+
if mpc: # run MPC step
|
|
369
|
+
u_step = self.mpc.make_step(x_step)
|
|
370
|
+
else: # use inputs directly
|
|
371
|
+
u_step = u_sim[k - 1:k, :].T
|
|
372
|
+
|
|
373
|
+
# Calculate current measurements
|
|
374
|
+
y_step = self.h(np.ravel(x_step), np.ravel(u_step))
|
|
375
|
+
|
|
376
|
+
# Simulate one time step given current inputs
|
|
377
|
+
x_step = self.simulator.make_step(u_step)
|
|
378
|
+
|
|
379
|
+
# Store inputs
|
|
380
|
+
u_sim[k - 1, :] = u_step.squeeze()
|
|
381
|
+
|
|
382
|
+
# Store state
|
|
383
|
+
x[k, :] = x_step.squeeze()
|
|
384
|
+
|
|
385
|
+
# Store measurements
|
|
386
|
+
y[k - 1, :] = y_step.squeeze()
|
|
387
|
+
|
|
388
|
+
# Last input has no effect, so keep it the same as previous time-step
|
|
389
|
+
if mpc:
|
|
390
|
+
u_sim[-1, :] = u_sim[-2, :]
|
|
391
|
+
|
|
392
|
+
# Last measurement
|
|
393
|
+
y[-1, :] = self.h(np.ravel(x[-1, :]), np.ravel(u_sim[-1, :]))
|
|
394
|
+
|
|
395
|
+
# Update the inputs
|
|
396
|
+
self.update_dict(u_sim, name='u')
|
|
397
|
+
|
|
398
|
+
# Update state trajectory
|
|
399
|
+
self.update_dict(x, name='x')
|
|
400
|
+
|
|
401
|
+
# Update measurements
|
|
402
|
+
self.update_dict(y, name='y')
|
|
403
|
+
|
|
404
|
+
# Return the measurements in array format
|
|
405
|
+
y_array = np.vstack(list(self.y.values())).T
|
|
406
|
+
|
|
407
|
+
if return_full_output:
|
|
408
|
+
return self.time.copy(), self.x.copy(), self.u.copy(), self.y.copy()
|
|
409
|
+
else:
|
|
410
|
+
return y_array
|
|
411
|
+
|
|
412
|
+
def get_time_states_inputs_measurements(self):
|
|
413
|
+
return self.time.copy(), self.x.copy(), self.u.copy(), self.y.copy()
|
|
414
|
+
|
|
415
|
+
def plot(self, name='x', dpi=150, plot_kwargs=None):
|
|
416
|
+
""" Plot states, inputs.
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
if plot_kwargs is None:
|
|
420
|
+
plot_kwargs = {
|
|
421
|
+
'color': 'black',
|
|
422
|
+
'linewidth': 2.0,
|
|
423
|
+
'linestyle': '-',
|
|
424
|
+
'marker': '.',
|
|
425
|
+
'markersize': 0
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if name == 'x':
|
|
429
|
+
plot_kwargs['color'] = 'firebrick'
|
|
430
|
+
elif name == 'u':
|
|
431
|
+
plot_kwargs['color'] = 'royalblue'
|
|
432
|
+
elif name == 'y':
|
|
433
|
+
plot_kwargs['color'] = 'seagreen'
|
|
434
|
+
elif name == 'setpoint':
|
|
435
|
+
plot_kwargs['color'] = 'gray'
|
|
436
|
+
|
|
437
|
+
plot_dict = getattr(self, name)
|
|
438
|
+
plot_data = np.array(list(plot_dict.values()))
|
|
439
|
+
n = plot_data.shape[0]
|
|
440
|
+
|
|
441
|
+
fig, ax = plt.subplots(n, 1, figsize=(4, n * 1.5), dpi=dpi, sharex=True)
|
|
442
|
+
ax = np.atleast_1d(ax)
|
|
443
|
+
|
|
444
|
+
for n, key in enumerate(plot_dict.keys()):
|
|
445
|
+
ax[n].plot(self.time, plot_dict[key], label=name, **plot_kwargs)
|
|
446
|
+
ax[n].set_ylabel(key, fontsize=7)
|
|
447
|
+
|
|
448
|
+
# Also plot the states if plotting setpoint
|
|
449
|
+
if name == 'setpoint':
|
|
450
|
+
ax[n].plot(self.time, self.x[key], label=key, color='firebrick', linestyle='-', linewidth=0.5)
|
|
451
|
+
ax[n].legend(fontsize=6)
|
|
452
|
+
|
|
453
|
+
y = self.x[key]
|
|
454
|
+
else:
|
|
455
|
+
y = plot_dict[key]
|
|
456
|
+
|
|
457
|
+
# Set y-axis limits
|
|
458
|
+
y_min = np.min(y)
|
|
459
|
+
y_max = np.max(y)
|
|
460
|
+
delta = y_max - y_min
|
|
461
|
+
if np.abs(delta) < 0.01:
|
|
462
|
+
margin = 0.1
|
|
463
|
+
ax[n].set_ylim(y_min - margin, y_max + margin)
|
|
464
|
+
|
|
465
|
+
ax[-1].set_xlabel('time', fontsize=7)
|
|
466
|
+
ax[0].set_title(name, fontsize=8, fontweight='bold')
|
|
467
|
+
|
|
468
|
+
for a in ax.flat:
|
|
469
|
+
a.tick_params(axis='both', labelsize=6)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def ensure_float_output(func):
|
|
473
|
+
@wraps(func)
|
|
474
|
+
def wrapper(*args, **kwargs):
|
|
475
|
+
output = func(*args, **kwargs)
|
|
476
|
+
return np.array([float(e) for e in output])
|
|
477
|
+
return wrapper
|