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/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