pybounds 0.0.4__py3-none-any.whl → 0.0.6__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 pybounds might be problematic. Click here for more details.

@@ -0,0 +1,422 @@
1
+
2
+ import warnings
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import do_mpc
6
+ from .util import FixedKeysDict, SetDict
7
+
8
+
9
+ class Simulator(object):
10
+ def __init__(self, f, h, dt=0.01, n=None, m=None,
11
+ state_names=None, input_names=None, measurement_names=None,
12
+ params_simulator=None, mpc_horizon=10):
13
+
14
+ """ Simulator.
15
+
16
+ :param callable f: dynamics function f(X, U, t)
17
+ :param callable h: measurement function h(X, U, t)
18
+ :param float dt: sampling time in seconds
19
+ :param int n: number of states, optional but cannot be set if state_names is set
20
+ :param int m: number of inputs, optional but cannot be set if input_names is set
21
+ :param list state_names: names of states, optional but cannot be set if n is set
22
+ :param list input_names: names of inputs, optional but cannot be set if m is set
23
+ :param list measurement_names: names of measurements, optional
24
+ :param dict params_simulator: simulation parameters, optional
25
+ """
26
+
27
+ self.f = f
28
+ self.h = h
29
+ self.dt = dt
30
+
31
+ # Set state names
32
+ if state_names is None: # default state names
33
+ if n is None:
34
+ raise ValueError('must set state_names or n')
35
+ else:
36
+ self.n = int(n)
37
+
38
+ self.state_names = ['x_' + str(n) for n in range(self.n)]
39
+ else: # state names given
40
+ if n is not None:
41
+ raise ValueError('cannot set state_names and n')
42
+
43
+ self.state_names = list(state_names)
44
+ self.n = len(self.state_names)
45
+ # if len(self.state_names) != self.n:
46
+ # raise ValueError('state_names must have length equal to x0')
47
+
48
+ # Set input names
49
+ if input_names is None: # default input names
50
+ if m is None:
51
+ raise ValueError('must set input_names or m')
52
+ else:
53
+ self.m = int(m)
54
+
55
+ self.input_names = ['u_' + str(m) for m in range(self.m)]
56
+ else: # input names given
57
+ if m is not None:
58
+ raise ValueError('cannot set in and n')
59
+
60
+ self.input_names = list(input_names)
61
+ self.m = len(self.input_names)
62
+ # if len(self.input_names) != self.m:
63
+ # raise ValueError('input_names must have length equal to u0')
64
+
65
+ # Run measurement function to get measurement size
66
+ x0 = np.ones(self.n)
67
+ u0 = np.ones(self.m)
68
+ y = self.h(x0, u0)
69
+ self.p = len(y) # number of measurements
70
+
71
+ # Set measurement names
72
+ if measurement_names is None: # default measurement names
73
+ self.measurement_names = ['y_' + str(p) for p in range(self.p)]
74
+ else:
75
+ self.measurement_names = measurement_names
76
+ if len(self.measurement_names) != self.p:
77
+ raise ValueError('measurement_names must have length equal to y')
78
+
79
+ # Initialize time vector
80
+ self.w = 11 # initialize for w time-steps, but this can change later
81
+ self.time = np.arange(0, self.w * self.dt, step=self.dt) # time vector
82
+
83
+ # Define initial states & initialize state time-series
84
+ self.x0 = {}
85
+ self.x = {}
86
+ for n, state_name in enumerate(self.state_names):
87
+ self.x0[state_name] = x0[n]
88
+ self.x[state_name] = x0[n] * np.ones(self.w)
89
+
90
+ self.x0 = FixedKeysDict(self.x0)
91
+ self.x = FixedKeysDict(self.x)
92
+
93
+ # Initialize input time-series
94
+ self.u = {}
95
+ for m, input_name in enumerate(self.input_names):
96
+ self.u[input_name] = u0[m] * np.ones(self.w)
97
+
98
+ self.u = FixedKeysDict(self.u)
99
+
100
+ # Initialize measurement time-series
101
+ self.y = {}
102
+ for p, measurement_name in enumerate(self.measurement_names):
103
+ self.y[measurement_name] = 0.0 * np.ones(self.w)
104
+
105
+ self.y = FixedKeysDict(self.y)
106
+
107
+ # Initialize state set-points
108
+ self.setpoint = {}
109
+ for n, state_name in enumerate(self.state_names):
110
+ self.setpoint[state_name] = 0.0 * np.ones(self.w)
111
+
112
+ self.setpoint = FixedKeysDict(self.setpoint)
113
+
114
+ # Define continuous-time MPC model
115
+ self.model = do_mpc.model.Model('continuous')
116
+
117
+ # Define state variables
118
+ X = []
119
+ for n, state_name in enumerate(self.state_names):
120
+ x = self.model.set_variable(var_type='_x', var_name=state_name, shape=(1, 1))
121
+ X.append(x)
122
+
123
+ # Define input variables
124
+ U = []
125
+ for m, input_name in enumerate(self.input_names):
126
+ u = self.model.set_variable(var_type='_u', var_name=input_name, shape=(1, 1))
127
+ U.append(u)
128
+
129
+ # Define dynamics
130
+ Xdot = self.f(X, U)
131
+ for n, state_name in enumerate(self.state_names):
132
+ self.model.set_rhs(state_name, Xdot[n])
133
+
134
+ # Add time-varying set-point variables for later use with MPC
135
+ for n, state_name in enumerate(self.state_names):
136
+ x = self.model.set_variable(var_type='_tvp', var_name=state_name + str('_set'), shape=(1, 1))
137
+
138
+ # Build model
139
+ self.model.setup()
140
+
141
+ # Define simulator & simulator parameters
142
+ self.simulator = do_mpc.simulator.Simulator(self.model)
143
+
144
+ # Set simulation parameters
145
+ if params_simulator is None:
146
+ self.params_simulator = {
147
+ 'integration_tool': 'idas', # cvodes, idas
148
+ 'abstol': 1e-8,
149
+ 'reltol': 1e-8,
150
+ 't_step': self.dt
151
+ }
152
+ else:
153
+ self.params_simulator = params_simulator
154
+
155
+ self.simulator.set_param(**self.params_simulator)
156
+
157
+ # Setup MPC
158
+ self.mpc = do_mpc.controller.MPC(self.model)
159
+ self.mpc_horizon = mpc_horizon
160
+ setup_mpc = {
161
+ 'n_horizon': self.mpc_horizon,
162
+ 'n_robust': 0,
163
+ 'open_loop': 0,
164
+ 't_step': self.dt,
165
+ 'state_discretization': 'collocation',
166
+ 'collocation_type': 'radau',
167
+ 'collocation_deg': 2,
168
+ 'collocation_ni': 1,
169
+ 'store_full_solution': False,
170
+
171
+ # Use MA27 linear solver in ipopt for faster calculations:
172
+ 'nlpsol_opts': {'ipopt.linear_solver': 'mumps', # mumps, MA27
173
+ 'ipopt.print_level': 0,
174
+ 'ipopt.sb': 'yes',
175
+ 'print_time': 0,
176
+ }
177
+ }
178
+
179
+ self.mpc.set_param(**setup_mpc)
180
+
181
+ # Get template's for MPC time-varying parameters
182
+ self.mpc_tvp_template = self.mpc.get_tvp_template()
183
+ self.simulator_tvp_template = self.simulator.get_tvp_template()
184
+
185
+ # Set time-varying set-point functions
186
+ self.mpc.set_tvp_fun(self.mpc_tvp_function)
187
+ self.simulator.set_tvp_fun(self.simulator_tvp_function)
188
+
189
+ # Setup simulator
190
+ self.simulator.setup()
191
+
192
+ def simulator_tvp_function(self, t):
193
+ """ Set the set-point function for MPC simulator.
194
+ :param t: current time
195
+ """
196
+
197
+ mpc_horizon = self.mpc._settings.n_horizon
198
+
199
+ # Set current step index
200
+ k_step = int(np.round(t / self.dt))
201
+ if k_step >= mpc_horizon: # point is beyond end of input data
202
+ k_step = mpc_horizon - 1 # set point beyond input data to last point
203
+
204
+ # Update current set-point
205
+ for n, state_name in enumerate(self.state_names):
206
+ self.simulator_tvp_template[state_name + '_set'] = self.setpoint[state_name][k_step]
207
+
208
+ return self.simulator_tvp_template
209
+
210
+ def mpc_tvp_function(self, t):
211
+ """ Set the set-point function for MPC optimizer.
212
+ """
213
+
214
+ mpc_horizon = self.mpc._settings.n_horizon
215
+
216
+ # Set current step index
217
+ k_step = int(np.round(t / self.dt))
218
+
219
+ # Update set-point time horizon
220
+ for k in range(mpc_horizon + 1):
221
+ k_set = k_step + k
222
+ if k_set >= self.w: # horizon is beyond end of input data
223
+ k_set = self.w - 1 # set part of horizon beyond input data to last point
224
+
225
+ # Update each set-point over time horizon
226
+ for n, state_name in enumerate(self.state_names):
227
+ self.mpc_tvp_template['_tvp', k, state_name + '_set'] = self.setpoint[state_name][k_set]
228
+
229
+ return self.mpc_tvp_template
230
+
231
+ def set_initial_state(self, x0):
232
+ """ Update the initial state.
233
+ """
234
+
235
+ if x0 is not None: # initial state given
236
+ if isinstance(x0, dict): # in dict format
237
+ SetDict().set_dict_with_overwrite(self.x0, x0) # update only the states in the dict given
238
+ elif isinstance(x0, list) or isinstance(x0, tuple) or (
239
+ x0, np.ndarray): # list, tuple, or numpy array format
240
+ x0 = np.array(x0).squeeze()
241
+ for n, key in enumerate(self.x0.keys()): # each state
242
+ self.x0[key] = x0[n]
243
+ else:
244
+ raise Exception('x0 must be either a dict, tuple, list, or numpy array')
245
+
246
+ def update_dict(self, data=None, name=None):
247
+ """ Update.
248
+ """
249
+
250
+ update = getattr(self, name)
251
+
252
+ if data is not None: # data given
253
+ if isinstance(data, dict): # in dict format
254
+ SetDict().set_dict_with_overwrite(update, data) # update only the inputs in the dict given
255
+ elif isinstance(data, list) or isinstance(data, tuple): # list or tuple format, each input vector in each element
256
+ for n, k in enumerate(update.keys()): # each state
257
+ update[k] = data[n]
258
+ elif isinstance(data, np.ndarray): # numpy array format given as matrix where columns are the different inputs
259
+ if len(data.shape) <= 1: # given as 1d array, so convert to column vector
260
+ data = np.atleast_2d(data).T
261
+
262
+ for n, key in enumerate(update.keys()): # each input
263
+ update[key] = data[:, n]
264
+
265
+ else:
266
+ raise Exception(name + ' must be either a dict, tuple, list, or numpy array')
267
+
268
+ # Make sure inputs are the same size
269
+ points = np.array([update[key].shape[0] for key in update.keys()])
270
+ points_check = points == points[0]
271
+ if not np.all(points_check):
272
+ raise Exception(name + ' not the same size')
273
+
274
+ def simulate(self, x0=None, u=None, mpc=False, return_full_output=False):
275
+ """
276
+ Simulate the system.
277
+
278
+ :params x0: initial state dict or array
279
+ :params u: input dict or array
280
+ :params return_full_output: boolean to run (time, x, u, y) instead of y
281
+ """
282
+
283
+ if (mpc is True) and (u is not None):
284
+ raise Exception('u must be None if running MPC')
285
+
286
+ if (mpc is False) and (u is None):
287
+ warnings.warn('not running MPC or setting u directly')
288
+
289
+ # Update the initial state
290
+ if x0 is None:
291
+ if mpc: # set the initial state to start at set-point if running MPC
292
+ x0 = {}
293
+ for state_name in self.state_names:
294
+ x0[state_name] = self.setpoint[state_name][0]
295
+
296
+ self.set_initial_state(x0=x0)
297
+ else:
298
+ self.set_initial_state(x0=x0)
299
+
300
+ # Update the inputs
301
+ self.update_dict(u, name='u')
302
+
303
+ # Concatenate the inputs, where rows are individual inputs and columns are time-steps
304
+ if mpc:
305
+ self.w = np.vstack(list(self.setpoint.values())).shape[1]
306
+ u_sim = np.zeros((self.w, self.m)) # preallocate input array
307
+ else:
308
+ self.w = np.vstack(list(self.u.values())).shape[1]
309
+ u_sim = np.vstack(list(self.u.values())).T
310
+
311
+ # Update time vector
312
+ T = (self.w - 1) * self.dt
313
+ self.time = np.linspace(0, T, num=self.w)
314
+
315
+ # Set array to store simulated states, where rows are individual states and columns are time-steps
316
+ x_step = np.array(list(self.x0.values())) # initialize state
317
+ x = np.nan * np.zeros((self.w, self.n))
318
+ x[0, :] = x_step.copy()
319
+
320
+ # Initialize the simulator
321
+ self.simulator.t0 = self.time[0]
322
+ self.simulator.x0 = x_step.copy()
323
+ self.simulator.set_initial_guess()
324
+
325
+ # Initialize MPC
326
+ if mpc:
327
+ self.mpc.setup()
328
+ self.mpc.t0 = self.time[0]
329
+ self.mpc.x0 = x_step.copy()
330
+ self.mpc.u0 = np.zeros((self.m, 1))
331
+ self.mpc.set_initial_guess()
332
+
333
+ # Run simulation
334
+ for k in range(1, self.w):
335
+ # Set input
336
+ if mpc: # run MPC step
337
+ u_step = self.mpc.make_step(x_step)
338
+ else: # use inputs directly
339
+ u_step = u_sim[k - 1:k, :].T
340
+
341
+ # Store inputs
342
+ u_sim[k - 1, :] = u_step.squeeze()
343
+
344
+ # Simulate one time step given current inputs
345
+ x_step = self.simulator.make_step(u_step)
346
+
347
+ # Store new states
348
+ x[k, :] = x_step.squeeze()
349
+
350
+ # Last input has no effect, so keep it the same as previous time-step
351
+ if mpc:
352
+ u_sim[-1, :] = u_sim[-2, :]
353
+
354
+ # Update the inputs
355
+ self.update_dict(u_sim, name='u')
356
+
357
+ # Update state trajectory
358
+ self.update_dict(x, name='x')
359
+
360
+ # Calculate measurements
361
+ x_list = list(self.x.values())
362
+ u_list = list(self.u.values())
363
+ y = self.h(x_list, u_list)
364
+
365
+ # Set measurements
366
+ self.update_dict(y, name='y')
367
+
368
+ # Return the measurements in array format
369
+ y_array = np.vstack(list(self.y.values())).T
370
+
371
+ if return_full_output:
372
+ return self.time.copy(), self.x.copy(), self.u.copy(), self.u.copy()
373
+ else:
374
+ return y_array
375
+
376
+ def get_time_states_inputs_measurements(self):
377
+ return self.time.copy(), self.x.copy(), self.u.copy(), self.u.copy()
378
+
379
+ def plot(self, name='x', dpi=150, plot_kwargs=None):
380
+ """ Plot states, inputs.
381
+ """
382
+
383
+ if plot_kwargs is None:
384
+ plot_kwargs = {
385
+ 'color': 'black',
386
+ 'linewidth': 2.0,
387
+ 'linestyle': '-',
388
+ 'marker': '.',
389
+ 'markersize': 0
390
+ }
391
+
392
+ if name == 'x':
393
+ plot_kwargs['color'] = 'firebrick'
394
+ elif name == 'u':
395
+ plot_kwargs['color'] = 'royalblue'
396
+ elif name == 'y':
397
+ plot_kwargs['color'] = 'seagreen'
398
+ elif name == 'setpoint':
399
+ plot_kwargs['color'] = 'gray'
400
+
401
+ plot_dict = getattr(self, name)
402
+ plot_data = np.array(list(plot_dict.values()))
403
+ n = plot_data.shape[0]
404
+
405
+ fig, ax = plt.subplots(n, 1, figsize=(4, n * 1.5), dpi=dpi, sharex=True)
406
+ ax = np.atleast_1d(ax)
407
+
408
+ for n, key in enumerate(plot_dict.keys()):
409
+ ax[n].plot(self.time, plot_dict[key], label='set-point', **plot_kwargs)
410
+ ax[n].set_ylabel(key, fontsize=7)
411
+
412
+ # Also plot the states if plotting setpoint
413
+ if name == 'setpoint':
414
+ ax[n].plot(self.time, self.x[key], label=key, color='firebrick', linestyle='-', linewidth=0.5)
415
+ ax[n].legend(fontsize=6)
416
+
417
+ ax[-1].set_xlabel('time', fontsize=7)
418
+ ax[0].set_title(name, fontsize=8, fontweight='bold')
419
+
420
+
421
+ for a in ax.flat:
422
+ a.tick_params(axis='both', labelsize=6)
pybounds/observability.py CHANGED
@@ -1,9 +1,10 @@
1
-
2
1
  import numpy as np
3
2
  import pandas as pd
4
- from multiprocessing import Pool
3
+ # from multiprocessing import Pool
4
+ # from pathos.multiprocessing import ProcessingPool as Pool
5
+ from concurrent.futures import ThreadPoolExecutor
5
6
  import warnings
6
- import matplotlib as mpl
7
+ # import matplotlib as mpl
7
8
  import matplotlib.pyplot as plt
8
9
  from mpl_toolkits.axes_grid1.inset_locator import inset_axes
9
10
  import sympy as sp
@@ -11,10 +12,10 @@ from .util import LatexStates
11
12
 
12
13
 
13
14
  class EmpiricalObservabilityMatrix:
14
- def __init__(self, simulator, x0, time, u, eps=1e-5, parallel=False):
15
+ def __init__(self, simulator, x0, u, eps=1e-5, parallel=False):
15
16
  """ Construct an empirical observability matrix O.
16
17
 
17
- :param callable Simulator: Simulator object : y = simulator(x0, u, **kwargs)
18
+ :param callable simulator: simulator object that has a method y = simulator.simulate(x0, u, **kwargs)
18
19
  y is (w x p) array. w is the number of time-steps and p is the number of measurements
19
20
  :param dict/list/np.array x0: initial state for Simulator
20
21
  :param dict/np.array u: inputs array
@@ -24,7 +25,6 @@ class EmpiricalObservabilityMatrix:
24
25
 
25
26
  # Store inputs
26
27
  self.simulator = simulator
27
- self.time = time.copy()
28
28
  self.eps = eps
29
29
  self.parallel = parallel
30
30
 
@@ -47,6 +47,9 @@ class EmpiricalObservabilityMatrix:
47
47
  # Number of outputs
48
48
  self.p = self.y_nominal.shape[1]
49
49
 
50
+ # Number of time-steps
51
+ self.w = self.y_nominal.shape[0] # of points in time window
52
+
50
53
  # Check for state/measurement names
51
54
  if hasattr(self.simulator, 'state_names'):
52
55
  self.state_names = self.simulator.state_names
@@ -59,7 +62,6 @@ class EmpiricalObservabilityMatrix:
59
62
  self.measurement_names = ['y_' + str(p) for p in range(self.p)]
60
63
 
61
64
  # Perturbation amounts
62
- self.w = len(self.time) # of points in time window
63
65
  self.delta_x = eps * np.eye(self.n) # perturbation amount for each state
64
66
  self.delta_y = np.zeros((self.p, self.n, self.w)) # preallocate delta_y
65
67
  self.y_plus = np.zeros((self.w, self.n, self.p))
@@ -92,8 +94,11 @@ class EmpiricalObservabilityMatrix:
92
94
  # Run simulations for perturbed initial conditions
93
95
  state_index = np.arange(0, self.n).tolist()
94
96
  if self.parallel: # multiprocessing
95
- with Pool(4) as pool:
96
- results = pool.map(self.simulate, state_index)
97
+ # with Pool(4) as pool:
98
+ # results = pool.map(self.simulate, state_index)
99
+
100
+ with ThreadPoolExecutor() as executor:
101
+ results = list(executor.map(self.simulate, state_index))
97
102
 
98
103
  for n, r in enumerate(results):
99
104
  delta_y, y_plus, y_minus = r
@@ -147,10 +152,10 @@ class SlidingEmpiricalObservabilityMatrix:
147
152
  def __init__(self, simulator, t_sim, x_sim, u_sim, w=None, eps=1e-5,
148
153
  parallel_sliding=False, parallel_perturbation=False):
149
154
  """ Construct empirical observability matrix O in sliding windows along a trajectory.
150
-
155
+
151
156
  :param callable simulator: Simulator object : y = simulator(x0, u, **kwargs)
152
157
  y is (w x p) array. w is the number of time-steps and p is the number of measurements
153
- :param np.array t_sim: time vector size N
158
+ :param np.array t_sim: time values along state trajectory array (N, 1)
154
159
  :param np.array x_sim: state trajectory array (N, n), can also be dict
155
160
  :param np.array u_sim: input array (N, m), can also be dict
156
161
  :param np.array w: window size for O calculations, will automatically set how many windows to compute
@@ -201,7 +206,7 @@ class SlidingEmpiricalObservabilityMatrix:
201
206
  raise ValueError('window size must be smaller than trajectory length')
202
207
 
203
208
  # All the indices to calculate O
204
- self.O_index = np.arange(0, self.N - self.w + 1, step=1) # indices to compute O
209
+ self.O_index = np.arange(0, self.N - self.w + 1, step=1) # indices to compute O
205
210
  self.O_time = self.t_sim[self.O_index] # times to compute O
206
211
  self.n_point = len(self.O_index) # # of times to calculate O
207
212
 
@@ -229,8 +234,12 @@ class SlidingEmpiricalObservabilityMatrix:
229
234
  # Construct O's
230
235
  n_point_range = np.arange(0, self.n_point).astype(int)
231
236
  if self.parallel_sliding: # multiprocessing
232
- with Pool(4) as pool:
233
- results = pool.map(self.construct, n_point_range)
237
+ # with Pool(4) as pool:
238
+ # results = pool.map(self.construct, n_point_range)
239
+
240
+ with ThreadPoolExecutor(max_workers=12) as executor:
241
+ results = list(executor.map(self.construct, n_point_range))
242
+
234
243
  for r in results:
235
244
  self.O_sliding.append(r[0])
236
245
  self.O_df_sliding.append(r[1])
@@ -262,7 +271,8 @@ class SlidingEmpiricalObservabilityMatrix:
262
271
  u_win = self.u_sim[win, :] # inputs in window
263
272
 
264
273
  # Calculate O for window
265
- EOM = EmpiricalObservabilityMatrix(self.simulator, x0, t_win0, u_win, eps=self.eps, parallel=self.parallel_perturbation)
274
+ EOM = EmpiricalObservabilityMatrix(self.simulator, x0, u_win, eps=self.eps,
275
+ parallel=self.parallel_perturbation)
266
276
  self.EOM = EOM
267
277
 
268
278
  # Store data
pybounds/simulator.py CHANGED
@@ -1,5 +1,7 @@
1
1
 
2
+ import warnings
2
3
  import numpy as np
4
+ import matplotlib.pyplot as plt
3
5
  import do_mpc
4
6
  from .util import FixedKeysDict, SetDict
5
7
 
@@ -7,7 +9,7 @@ from .util import FixedKeysDict, SetDict
7
9
  class Simulator(object):
8
10
  def __init__(self, f, h, dt=0.01, n=None, m=None,
9
11
  state_names=None, input_names=None, measurement_names=None,
10
- params_simulator=None):
12
+ params_simulator=None, mpc_horizon=10):
11
13
 
12
14
  """ Simulator.
13
15
 
@@ -63,7 +65,7 @@ class Simulator(object):
63
65
  # Run measurement function to get measurement size
64
66
  x0 = np.ones(self.n)
65
67
  u0 = np.ones(self.m)
66
- y = self.h(x0, u0, 0)
68
+ y = self.h(x0, u0)
67
69
  self.p = len(y) # number of measurements
68
70
 
69
71
  # Set measurement names
@@ -75,32 +77,40 @@ class Simulator(object):
75
77
  raise ValueError('measurement_names must have length equal to y')
76
78
 
77
79
  # Initialize time vector
78
- w = 10 # initialize for w time-steps, but this can change later
79
- self.time = np.arange(0, w * self.dt + self.dt / 2, step=self.dt) # time vector
80
+ self.w = 11 # initialize for w time-steps, but this can change later
81
+ self.time = np.arange(0, self.w * self.dt, step=self.dt) # time vector
80
82
 
81
83
  # Define initial states & initialize state time-series
82
84
  self.x0 = {}
83
85
  self.x = {}
84
86
  for n, state_name in enumerate(self.state_names):
85
87
  self.x0[state_name] = x0[n]
86
- self.x[state_name] = x0[n] * np.ones(w)
88
+ self.x[state_name] = x0[n] * np.ones(self.w)
87
89
 
88
90
  self.x0 = FixedKeysDict(self.x0)
91
+ self.x = FixedKeysDict(self.x)
89
92
 
90
93
  # Initialize input time-series
91
94
  self.u = {}
92
95
  for m, input_name in enumerate(self.input_names):
93
- self.u[input_name] = u0[m] * np.ones(w)
96
+ self.u[input_name] = u0[m] * np.ones(self.w)
94
97
 
95
98
  self.u = FixedKeysDict(self.u)
96
99
 
97
100
  # Initialize measurement time-series
98
101
  self.y = {}
99
102
  for p, measurement_name in enumerate(self.measurement_names):
100
- self.y[measurement_name] = 0.0 * np.ones(w)
103
+ self.y[measurement_name] = 0.0 * np.ones(self.w)
101
104
 
102
105
  self.y = FixedKeysDict(self.y)
103
106
 
107
+ # Initialize state set-points
108
+ self.setpoint = {}
109
+ for n, state_name in enumerate(self.state_names):
110
+ self.setpoint[state_name] = 0.0 * np.ones(self.w)
111
+
112
+ self.setpoint = FixedKeysDict(self.setpoint)
113
+
104
114
  # Define continuous-time MPC model
105
115
  self.model = do_mpc.model.Model('continuous')
106
116
 
@@ -117,10 +127,14 @@ class Simulator(object):
117
127
  U.append(u)
118
128
 
119
129
  # Define dynamics
120
- Xdot = self.f(X, U, 0)
130
+ Xdot = self.f(X, U)
121
131
  for n, state_name in enumerate(self.state_names):
122
132
  self.model.set_rhs(state_name, Xdot[n])
123
133
 
134
+ # Add time-varying set-point variables for later use with MPC
135
+ for n, state_name in enumerate(self.state_names):
136
+ x = self.model.set_variable(var_type='_tvp', var_name=state_name + str('_set'), shape=(1, 1))
137
+
124
138
  # Build model
125
139
  self.model.setup()
126
140
 
@@ -139,8 +153,81 @@ class Simulator(object):
139
153
  self.params_simulator = params_simulator
140
154
 
141
155
  self.simulator.set_param(**self.params_simulator)
156
+
157
+ # Setup MPC
158
+ self.mpc = do_mpc.controller.MPC(self.model)
159
+ self.mpc_horizon = mpc_horizon
160
+ setup_mpc = {
161
+ 'n_horizon': self.mpc_horizon,
162
+ 'n_robust': 0,
163
+ 'open_loop': 0,
164
+ 't_step': self.dt,
165
+ 'state_discretization': 'collocation',
166
+ 'collocation_type': 'radau',
167
+ 'collocation_deg': 2,
168
+ 'collocation_ni': 1,
169
+ 'store_full_solution': False,
170
+
171
+ # Use MA27 linear solver in ipopt for faster calculations:
172
+ 'nlpsol_opts': {'ipopt.linear_solver': 'mumps', # mumps, MA27
173
+ 'ipopt.print_level': 0,
174
+ 'ipopt.sb': 'yes',
175
+ 'print_time': 0,
176
+ }
177
+ }
178
+
179
+ self.mpc.set_param(**setup_mpc)
180
+
181
+ # Get template's for MPC time-varying parameters
182
+ self.mpc_tvp_template = self.mpc.get_tvp_template()
183
+ self.simulator_tvp_template = self.simulator.get_tvp_template()
184
+
185
+ # Set time-varying set-point functions
186
+ self.mpc.set_tvp_fun(self.mpc_tvp_function)
187
+ self.simulator.set_tvp_fun(self.simulator_tvp_function)
188
+
189
+ # Setup simulator
142
190
  self.simulator.setup()
143
191
 
192
+ def simulator_tvp_function(self, t):
193
+ """ Set the set-point function for MPC simulator.
194
+ :param t: current time
195
+ """
196
+
197
+ mpc_horizon = self.mpc._settings.n_horizon
198
+
199
+ # Set current step index
200
+ k_step = int(np.round(t / self.dt))
201
+ if k_step >= mpc_horizon: # point is beyond end of input data
202
+ k_step = mpc_horizon - 1 # set point beyond input data to last point
203
+
204
+ # Update current set-point
205
+ for n, state_name in enumerate(self.state_names):
206
+ self.simulator_tvp_template[state_name + '_set'] = self.setpoint[state_name][k_step]
207
+
208
+ return self.simulator_tvp_template
209
+
210
+ def mpc_tvp_function(self, t):
211
+ """ Set the set-point function for MPC optimizer.
212
+ """
213
+
214
+ mpc_horizon = self.mpc._settings.n_horizon
215
+
216
+ # Set current step index
217
+ k_step = int(np.round(t / self.dt))
218
+
219
+ # Update set-point time horizon
220
+ for k in range(mpc_horizon + 1):
221
+ k_set = k_step + k
222
+ if k_set >= self.w: # horizon is beyond end of input data
223
+ k_set = self.w - 1 # set part of horizon beyond input data to last point
224
+
225
+ # Update each set-point over time horizon
226
+ for n, state_name in enumerate(self.state_names):
227
+ self.mpc_tvp_template['_tvp', k, state_name + '_set'] = self.setpoint[state_name][k_set]
228
+
229
+ return self.mpc_tvp_template
230
+
144
231
  def set_initial_state(self, x0):
145
232
  """ Update the initial state.
146
233
  """
@@ -156,33 +243,35 @@ class Simulator(object):
156
243
  else:
157
244
  raise Exception('x0 must be either a dict, tuple, list, or numpy array')
158
245
 
159
- def set_inputs(self, u):
160
- """ Update the inputs.
246
+ def update_dict(self, data=None, name=None):
247
+ """ Update.
161
248
  """
162
249
 
163
- if u is not None: # inputs given
164
- if isinstance(u, dict): # in dict format
165
- SetDict().set_dict_with_overwrite(self.u, u) # update only the inputs in the dict given
166
- elif isinstance(u, list) or isinstance(u, tuple): # list or tuple format, each input vector in each element
167
- for n, k in enumerate(self.u.keys()): # each input
168
- self.u[k] = u[n]
169
- elif isinstance(u, np.ndarray): # numpy array format given as matrix where columns are the different inputs
170
- if len(u.shape) <= 1: # given as 1d array, so convert to column vector
171
- u = np.atleast_2d(u).T
250
+ update = getattr(self, name)
251
+
252
+ if data is not None: # data given
253
+ if isinstance(data, dict): # in dict format
254
+ SetDict().set_dict_with_overwrite(update, data) # update only the inputs in the dict given
255
+ elif isinstance(data, list) or isinstance(data, tuple): # list or tuple format, each input vector in each element
256
+ for n, k in enumerate(update.keys()): # each state
257
+ update[k] = data[n]
258
+ elif isinstance(data, np.ndarray): # numpy array format given as matrix where columns are the different inputs
259
+ if len(data.shape) <= 1: # given as 1d array, so convert to column vector
260
+ data = np.atleast_2d(data).T
172
261
 
173
- for m, key in enumerate(self.u.keys()): # each input
174
- self.u[key] = u[:, m]
262
+ for n, key in enumerate(update.keys()): # each input
263
+ update[key] = data[:, n]
175
264
 
176
265
  else:
177
- raise Exception('u must be either a dict, tuple, list, or numpy array')
266
+ raise Exception(name + ' must be either a dict, tuple, list, or numpy array')
178
267
 
179
268
  # Make sure inputs are the same size
180
- points = np.array([self.u[key].shape[0] for key in self.u.keys()])
269
+ points = np.array([update[key].shape[0] for key in update.keys()])
181
270
  points_check = points == points[0]
182
271
  if not np.all(points_check):
183
- raise Exception('inputs are not the same size')
272
+ raise Exception(name + ' not the same size')
184
273
 
185
- def simulate(self, x0=None, u=None, return_full_output=False):
274
+ def simulate(self, x0=None, u=None, mpc=False, return_full_output=False):
186
275
  """
187
276
  Simulate the system.
188
277
 
@@ -191,34 +280,63 @@ class Simulator(object):
191
280
  :params return_full_output: boolean to run (time, x, u, y) instead of y
192
281
  """
193
282
 
283
+ if (mpc is True) and (u is not None):
284
+ raise Exception('u must be None if running MPC')
285
+
286
+ if (mpc is False) and (u is None):
287
+ warnings.warn('not running MPC or setting u directly')
288
+
194
289
  # Update the initial state
195
- self.set_initial_state(x0=x0.copy())
290
+ if x0 is None:
291
+ if mpc: # set the initial state to start at set-point if running MPC
292
+ x0 = {}
293
+ for state_name in self.state_names:
294
+ x0[state_name] = self.setpoint[state_name][0]
295
+
296
+ self.set_initial_state(x0=x0)
297
+ else:
298
+ self.set_initial_state(x0=x0)
196
299
 
197
300
  # Update the inputs
198
- self.set_inputs(u=u)
301
+ self.update_dict(u, name='u')
199
302
 
200
303
  # Concatenate the inputs, where rows are individual inputs and columns are time-steps
201
- u_sim = np.vstack(list(self.u.values())).T
202
- n_point = u_sim.shape[0]
304
+ if mpc:
305
+ self.w = np.vstack(list(self.setpoint.values())).shape[1]
306
+ u_sim = np.zeros((self.w, self.m)) # preallocate input array
307
+ else:
308
+ self.w = np.vstack(list(self.u.values())).shape[1]
309
+ u_sim = np.vstack(list(self.u.values())).T
203
310
 
204
311
  # Update time vector
205
- T = (n_point - 1) * self.dt
206
- self.time = np.linspace(0, T, num=n_point)
312
+ T = (self.w - 1) * self.dt
313
+ self.time = np.linspace(0, T, num=self.w)
207
314
 
208
315
  # Set array to store simulated states, where rows are individual states and columns are time-steps
209
316
  x_step = np.array(list(self.x0.values())) # initialize state
210
- x_sim = np.nan * np.zeros((n_point, self.n))
211
- x_sim[0, :] = x_step.copy()
317
+ x = np.nan * np.zeros((self.w, self.n))
318
+ x[0, :] = x_step.copy()
212
319
 
213
320
  # Initialize the simulator
214
321
  self.simulator.t0 = self.time[0]
215
322
  self.simulator.x0 = x_step.copy()
216
323
  self.simulator.set_initial_guess()
217
324
 
325
+ # Initialize MPC
326
+ if mpc:
327
+ self.mpc.setup()
328
+ self.mpc.t0 = self.time[0]
329
+ self.mpc.x0 = x_step.copy()
330
+ self.mpc.u0 = np.zeros((self.m, 1))
331
+ self.mpc.set_initial_guess()
332
+
218
333
  # Run simulation
219
- for k in range(1, n_point):
334
+ for k in range(1, self.w):
220
335
  # Set input
221
- u_step = u_sim[k - 1:k, :].T
336
+ if mpc: # run MPC step
337
+ u_step = self.mpc.make_step(x_step)
338
+ else: # use inputs directly
339
+ u_step = u_sim[k - 1:k, :].T
222
340
 
223
341
  # Store inputs
224
342
  u_sim[k - 1, :] = u_step.squeeze()
@@ -227,34 +345,87 @@ class Simulator(object):
227
345
  x_step = self.simulator.make_step(u_step)
228
346
 
229
347
  # Store new states
230
- x_sim[k, :] = x_step.squeeze()
348
+ x[k, :] = x_step.squeeze()
349
+
350
+ # Last input has no effect, so keep it the same as previous time-step
351
+ if mpc:
352
+ u_sim[-1, :] = u_sim[-2, :]
231
353
 
232
354
  # Update the inputs
233
- self.set_inputs(u=u_sim)
355
+ self.update_dict(u_sim, name='u')
234
356
 
235
357
  # Update state trajectory
236
- for n, key in enumerate(self.x.keys()):
237
- self.x[key] = x_sim[:, n]
358
+ self.update_dict(x, name='x')
238
359
 
239
360
  # Calculate measurements
240
361
  x_list = list(self.x.values())
241
362
  u_list = list(self.u.values())
242
- y = self.h(x_list, u_list, 0)
363
+ y = self.h(x_list, u_list)
243
364
 
244
- # Set outputs
245
- self.y = {}
246
- for p, measurement_name in enumerate(self.measurement_names):
247
- self.y[measurement_name] = y[p]
365
+ # Set measurements
366
+ self.update_dict(y, name='y')
248
367
 
249
- # Return the outputs in array format
368
+ # Return the measurements in array format
250
369
  y_array = np.vstack(list(self.y.values())).T
251
370
 
252
371
  if return_full_output:
253
- return self.time.copy(), self.x.copy(), self.u.copy(), self.u.copy()
372
+ return self.time.copy(), self.x.copy(), self.u.copy(), self.y.copy()
254
373
  else:
255
374
  return y_array
256
375
 
257
- def get_time_states_input_measurements(self):
376
+ def get_time_states_inputs_measurements(self):
258
377
  return self.time.copy(), self.x.copy(), self.u.copy(), self.u.copy()
259
378
 
379
+ def plot(self, name='x', dpi=150, plot_kwargs=None):
380
+ """ Plot states, inputs.
381
+ """
382
+
383
+ if plot_kwargs is None:
384
+ plot_kwargs = {
385
+ 'color': 'black',
386
+ 'linewidth': 2.0,
387
+ 'linestyle': '-',
388
+ 'marker': '.',
389
+ 'markersize': 0
390
+ }
260
391
 
392
+ if name == 'x':
393
+ plot_kwargs['color'] = 'firebrick'
394
+ elif name == 'u':
395
+ plot_kwargs['color'] = 'royalblue'
396
+ elif name == 'y':
397
+ plot_kwargs['color'] = 'seagreen'
398
+ elif name == 'setpoint':
399
+ plot_kwargs['color'] = 'gray'
400
+
401
+ plot_dict = getattr(self, name)
402
+ plot_data = np.array(list(plot_dict.values()))
403
+ n = plot_data.shape[0]
404
+
405
+ fig, ax = plt.subplots(n, 1, figsize=(4, n * 1.5), dpi=dpi, sharex=True)
406
+ ax = np.atleast_1d(ax)
407
+
408
+ for n, key in enumerate(plot_dict.keys()):
409
+ ax[n].plot(self.time, plot_dict[key], label=name, **plot_kwargs)
410
+ ax[n].set_ylabel(key, fontsize=7)
411
+
412
+ # Also plot the states if plotting setpoint
413
+ if name == 'setpoint':
414
+ ax[n].plot(self.time, self.x[key], label=key, color='firebrick', linestyle='-', linewidth=0.5)
415
+ ax[n].legend(fontsize=6)
416
+
417
+ y = self.x[key]
418
+ y_min = np.min(y)
419
+ y_max = np.max(y)
420
+ delta = y_max - y_min
421
+ if np.abs(delta) < 0.01:
422
+ margin = 0.1
423
+ ax[n].set_ylim(y_min - margin, y_max + margin)
424
+ else:
425
+ margin = 0.0
426
+
427
+ ax[-1].set_xlabel('time', fontsize=7)
428
+ ax[0].set_title(name, fontsize=8, fontweight='bold')
429
+
430
+ for a in ax.flat:
431
+ a.tick_params(axis='both', labelsize=6)
pybounds/util.py CHANGED
@@ -1,9 +1,9 @@
1
-
2
1
  import numpy as np
3
2
  import matplotlib.pyplot as plt
4
3
  import matplotlib.collections as mcoll
5
4
  import matplotlib.patheffects as path_effects
6
5
 
6
+
7
7
  class FixedKeysDict(dict):
8
8
  def __init__(self, *args, **kwargs):
9
9
  super(FixedKeysDict, self).__init__(*args, **kwargs)
@@ -74,6 +74,7 @@ class LatexStates:
74
74
  'v_perp': r'$v_{\perp}$',
75
75
  'phi': r'$\phi$',
76
76
  'phidot': r'$\dot{\phi}$',
77
+ 'phi_dot': r'$\dot{\phi}$',
77
78
  'phiddot': r'$\ddot{\phi}$',
78
79
  'w': r'$w$',
79
80
  'zeta': r'$\zeta$',
@@ -95,9 +96,46 @@ class LatexStates:
95
96
  'v_para_dot': r'$\dot{v_{\parallel}}$',
96
97
  'v_perp_dot': r'$\dot{v_{\perp}}$',
97
98
  'v_para_dot_ratio': r'$\frac{\Delta v_{\parallel}}{v_{\parallel}}$',
98
- 'sigma': r'$\sigma$',
99
- 'rho': r'$\rho$',
100
- 'beta': r'$\beta$'
99
+ 'x': r'$x$',
100
+ 'y': r'$y$',
101
+ 'v_x': r'$v_{x}$',
102
+ 'v_y': r'$v_{y}$',
103
+ 'v_z': r'$v_{z}$',
104
+ 'w_x': r'$w_{x}$',
105
+ 'w_y': r'$w_{y}$',
106
+ 'w_z': r'$w_{z}$',
107
+ 'a_x': r'$a_{x}$',
108
+ 'a_y': r'$a_{y}$',
109
+ 'vx': r'$v_x$',
110
+ 'vy': r'$v_y$',
111
+ 'vz': r'$v_z$',
112
+ 'wx': r'$w_x$',
113
+ 'wy': r'$w_y$',
114
+ 'wz': r'$w_z$',
115
+ 'ax': r'$ax$',
116
+ 'ay': r'$ay$',
117
+ 'beta': r'$\beta',
118
+ 'thetadot': r'$\dot{\theta}$',
119
+ 'theta_dot': r'$\dot{\theta}$',
120
+ 'psidot': r'$\dot{\psi}$',
121
+ 'psi_dot': r'$\dot{\psi}$',
122
+ 'theta': r'$\theta$',
123
+ 'Yaw': r'$\psi$',
124
+ 'R': r'$\phi$',
125
+ 'P': r'$\theta$',
126
+ 'dYaw': r'$\dot{\psi}$',
127
+ 'dP': r'$\dot{\theta}$',
128
+ 'dR': r'$\dot{\phi}$',
129
+ 'acc_x': r'$\dot{v}x$',
130
+ 'acc_y': r'$\dot{v}y$',
131
+ 'acc_z': r'$\dot{v}z$',
132
+ 'Psi': r'$\Psi$',
133
+ 'Ix': r'$I_x$',
134
+ 'Iy': r'$I_y$',
135
+ 'Iz': r'$I_z$',
136
+ 'Jr': r'$J_r$',
137
+ 'Dl': r'$D_l$',
138
+ 'Dr': r'$D_r$',
101
139
  }
102
140
 
103
141
  if dict is not None:
@@ -159,4 +197,4 @@ def colorline(x, y, z, ax=None, cmap=plt.get_cmap('copper'), norm=None, linewidt
159
197
 
160
198
  ax.add_collection(lc)
161
199
 
162
- return lc
200
+ return lc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pybounds
3
- Version: 0.0.4
3
+ Version: 0.0.6
4
4
  Summary: Bounding Observability for Uncertain Nonlinear Dynamics Systems (BOUNDS)
5
5
  Home-page: https://pypi.org/project/pybounds/
6
6
  Author: Ben Cellini, Burak Boyacioglu, Floris van Breugel
@@ -16,16 +16,9 @@ License-File: LICENSE
16
16
 
17
17
  Python implementation of BOUNDS: Bounding Observability for Uncertain Nonlinear Dynamic Systems.
18
18
 
19
- <p align="center">
20
- <a href="https://pynumdiff.readthedocs.io/en/master/" target="_blank" >
21
-
22
- [//]: # ( <img alt="Python for Numerical Differentiation of noisy time series data" src="docs/source/_static/logo_PyNumDiff.png" width="300" height="200" />)
23
- </a>
24
- </p>
25
-
26
19
  <p align="center">
27
20
  <a href="https://pypi.org/project/pybounds/">
28
- <img src="https://badge.fury.io/py/pynumdiff.svg" alt="PyPI version" height="18"></a>
21
+ <img src="https://badge.fury.io/py/pybounds.svg" alt="PyPI version" height="18"></a>
29
22
  </p>
30
23
 
31
24
  ## Introduction
@@ -42,14 +35,17 @@ pip install pybounds
42
35
  ```
43
36
 
44
37
  ## Notebook examples
45
- There is currently one simple example notebook. More to come.
38
+ For a simple system
46
39
  * Monocular camera with optic fow measurements: [mono_camera_example.ipynb](examples%2Fmono_camera_example.ipynb)
47
40
 
41
+ For a more complex system
42
+ * Fly-wind: [fly_wind_example.ipynb](examples%2Ffly_wind_example.ipynb)
43
+
48
44
  ## Citation
49
45
 
50
46
  If you use the code or methods from this package, please cite the following paper:
51
47
 
52
- Benjamin Cellini, Burak Boyacıoğlu, Stanley David Stupski, and Floris van Breugel. Discovering and exploiting active sensing motifs for estimation with empirical observability. (2024) bioRxiv.
48
+ Benjamin Cellini, Burak Boyacioglu, Stanley David Stupski, and Floris van Breugel. Discovering and exploiting active sensing motifs for estimation with empirical observability. (2024) bioRxiv.
53
49
 
54
50
  ## Related packages
55
51
  This repository is the evolution of the EISO repo (https://github.com/BenCellini/EISO), and is intended as a companion to the repository directly associated with the paper above.
@@ -0,0 +1,10 @@
1
+ pybounds/__init__.py,sha256=so9LuRNw2V8MGsDs-RPstGGgJtBFp2YGolQbS0rVfhw,348
2
+ pybounds/drone_simulator.py,sha256=-CrQVpNfiBDFECd6H7FU5has4sYGW1gyS2RhOgXUqZg,15858
3
+ pybounds/observability.py,sha256=4tdK6AK678zoorbkQ2psvzMRLY32CIj2QwVb-0w-GXk,27541
4
+ pybounds/simulator.py,sha256=ReaCRHA-DjiE1EbmCStw2Top9EyJeg41S_lO-iqnjv4,16194
5
+ pybounds/util.py,sha256=Gs0UgqgLXTJI9FZww90iJhqU02iJ31bXBURjGiq3YzM,7401
6
+ pybounds-0.0.6.dist-info/LICENSE,sha256=kqeyRXtRGgBVZdXYeIX4zR9l2KZ2rqIBVEiPMTjxjcI,1093
7
+ pybounds-0.0.6.dist-info/METADATA,sha256=MAp33xdjWLOfB17N32E-GdKRZnaCHtIwBc65XmFQXb4,2155
8
+ pybounds-0.0.6.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
9
+ pybounds-0.0.6.dist-info/top_level.txt,sha256=V-ofnWE3m_UkXTXJwNRD07n14m5R6sc6l4NadaCCP_A,9
10
+ pybounds-0.0.6.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- pybounds/__init__.py,sha256=so9LuRNw2V8MGsDs-RPstGGgJtBFp2YGolQbS0rVfhw,348
2
- pybounds/observability.py,sha256=OqJ7fZmflvgZ9dQprIkZ3aahmvwLKVa-5RRHGpnv_gI,27058
3
- pybounds/simulator.py,sha256=GiQHDGkRtiUsMiiNB8tLBXbG2d5UQJwa7WvXXalFtXg,9748
4
- pybounds/util.py,sha256=xxmXmpLR3yK923X6wAPKp_5w814cO3m9OqF1XIt9S8c,5818
5
- pybounds-0.0.4.dist-info/LICENSE,sha256=kqeyRXtRGgBVZdXYeIX4zR9l2KZ2rqIBVEiPMTjxjcI,1093
6
- pybounds-0.0.4.dist-info/METADATA,sha256=u32cRGxW05dWQFSAAKk_oQ-WYpXki6CBMSBvU-j3jT0,2376
7
- pybounds-0.0.4.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
8
- pybounds-0.0.4.dist-info/top_level.txt,sha256=V-ofnWE3m_UkXTXJwNRD07n14m5R6sc6l4NadaCCP_A,9
9
- pybounds-0.0.4.dist-info/RECORD,,