pybounds 0.0.6__py3-none-any.whl → 0.0.7__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.
- pybounds/__init__.py +2 -0
- pybounds/jacobian.py +46 -0
- pybounds/observability.py +136 -47
- pybounds/observability_transform.py +11 -0
- pybounds/simulator.py +1 -1
- {pybounds-0.0.6.dist-info → pybounds-0.0.7.dist-info}/METADATA +1 -1
- pybounds-0.0.7.dist-info/RECORD +11 -0
- pybounds/drone_simulator.py +0 -422
- pybounds-0.0.6.dist-info/RECORD +0 -10
- {pybounds-0.0.6.dist-info → pybounds-0.0.7.dist-info}/LICENSE +0 -0
- {pybounds-0.0.6.dist-info → pybounds-0.0.7.dist-info}/WHEEL +0 -0
- {pybounds-0.0.6.dist-info → pybounds-0.0.7.dist-info}/top_level.txt +0 -0
pybounds/__init__.py
CHANGED
|
@@ -6,6 +6,8 @@ from .observability import SlidingEmpiricalObservabilityMatrix
|
|
|
6
6
|
from .observability import FisherObservability
|
|
7
7
|
from .observability import SlidingFisherObservability
|
|
8
8
|
from .observability import ObservabilityMatrixImage
|
|
9
|
+
from .observability import transform_states
|
|
10
|
+
from .observability import SymbolicJacobian
|
|
9
11
|
|
|
10
12
|
from .util import colorline
|
|
11
13
|
|
pybounds/jacobian.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import sympy as sp
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SymbolicJacobian:
|
|
6
|
+
def __init__(self, func, state_vars):
|
|
7
|
+
"""
|
|
8
|
+
Initialize the Jacobian calculator with a function or symbolic expression for f(x).
|
|
9
|
+
|
|
10
|
+
:param func: A Python function or sympy Matrix representing f(x).
|
|
11
|
+
If func is a Python function, it should take in x and u (e.g., f(x))
|
|
12
|
+
and return a sympy Matrix for xdot.
|
|
13
|
+
If func is a sympy Matrix, it directly represents xdot.
|
|
14
|
+
:param state_vars: List of sympy symbols for variables (e.g., [x1, x2]).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
self.state_vars = sp.Matrix(state_vars) # state variables as sympy Matrix
|
|
18
|
+
|
|
19
|
+
# Detect if func is symbolic (sympy.Matrix) or a Python function
|
|
20
|
+
if isinstance(func, sp.Matrix):
|
|
21
|
+
# If func is a symbolic Matrix, use it directly as xdot
|
|
22
|
+
self.z_sym = func
|
|
23
|
+
else:
|
|
24
|
+
# If func is a Python function, call it with symbolic state and control variables
|
|
25
|
+
x_sym = self.state_vars
|
|
26
|
+
self.z_sym = func(x_sym) # get the symbolic expression from the function
|
|
27
|
+
|
|
28
|
+
# Calculate the Jacobian of xdot_sym with respect to the state variables
|
|
29
|
+
self.jacobian_symbolic = self.z_sym.jacobian(self.state_vars)
|
|
30
|
+
self.jacobian_symbolic = sp.simplify(self.jacobian_symbolic)
|
|
31
|
+
|
|
32
|
+
def get_jacobian_function(self):
|
|
33
|
+
"""
|
|
34
|
+
Returns a numerical function to evaluate the Jacobian at specific values of x and u.
|
|
35
|
+
|
|
36
|
+
:return: A function that calculates the Jacobian matrix given values of x and u.
|
|
37
|
+
The function takes numpy arrays x and u as inputs.
|
|
38
|
+
"""
|
|
39
|
+
# Convert the symbolic Jacobian to a numerical function using lambdify
|
|
40
|
+
jacobian_func = sp.lambdify(self.state_vars, self.jacobian_symbolic, 'numpy')
|
|
41
|
+
|
|
42
|
+
# Return a wrapper function that accepts x as numpy array
|
|
43
|
+
def jacobian_numerical(x):
|
|
44
|
+
return jacobian_func(*np.array(x))
|
|
45
|
+
|
|
46
|
+
return jacobian_numerical
|
pybounds/observability.py
CHANGED
|
@@ -9,10 +9,12 @@ import matplotlib.pyplot as plt
|
|
|
9
9
|
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
|
|
10
10
|
import sympy as sp
|
|
11
11
|
from .util import LatexStates
|
|
12
|
+
from jacobian import SymbolicJacobian
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class EmpiricalObservabilityMatrix:
|
|
15
|
-
def __init__(self, simulator, x0, u, eps=1e-5, parallel=False
|
|
16
|
+
def __init__(self, simulator, x0, u, eps=1e-5, parallel=False,
|
|
17
|
+
z_function=None, z_state_names=None):
|
|
16
18
|
""" Construct an empirical observability matrix O.
|
|
17
19
|
|
|
18
20
|
:param callable simulator: simulator object that has a method y = simulator.simulate(x0, u, **kwargs)
|
|
@@ -21,6 +23,12 @@ class EmpiricalObservabilityMatrix:
|
|
|
21
23
|
:param dict/np.array u: inputs array
|
|
22
24
|
:param float eps: epsilon value for perturbations to construct O, should be small number
|
|
23
25
|
:param bool parallel: if True, run the perturbations in parallel
|
|
26
|
+
:param callable z_function: function that transforms coordinates from original to new states
|
|
27
|
+
must be of the form z = z_function(x), where x & z are the same size
|
|
28
|
+
should use sympy functions wherever possible
|
|
29
|
+
leave as None to maintain original coordinates
|
|
30
|
+
:param list | tuple z_state_names: (optional) names of states in new coordinates.
|
|
31
|
+
will only have an effect if O is a data-frame & z_function is not None
|
|
24
32
|
"""
|
|
25
33
|
|
|
26
34
|
# Store inputs
|
|
@@ -81,9 +89,22 @@ class EmpiricalObservabilityMatrix:
|
|
|
81
89
|
|
|
82
90
|
self.time_labels = np.hstack(self.time_labels)
|
|
83
91
|
|
|
84
|
-
# Run
|
|
92
|
+
# Run simulations to construct O
|
|
85
93
|
self.run()
|
|
86
94
|
|
|
95
|
+
# Perform coordinate transformation on O, if specified
|
|
96
|
+
if z_function is not None:
|
|
97
|
+
self.O_df, self.dzdx, self.dxdz_sym = transform_states(O=self.O_df,
|
|
98
|
+
square_flag=False,
|
|
99
|
+
z_function=z_function,
|
|
100
|
+
x0=self.x0,
|
|
101
|
+
z_state_names=z_state_names)
|
|
102
|
+
self.state_names = tuple(self.O_df.columns)
|
|
103
|
+
self.O = self.O_df.values
|
|
104
|
+
else:
|
|
105
|
+
self.dzdx = None
|
|
106
|
+
self.dxdz_sym = None
|
|
107
|
+
|
|
87
108
|
def run(self, parallel=None):
|
|
88
109
|
""" Construct empirical observability matrix.
|
|
89
110
|
"""
|
|
@@ -150,7 +171,8 @@ class EmpiricalObservabilityMatrix:
|
|
|
150
171
|
|
|
151
172
|
class SlidingEmpiricalObservabilityMatrix:
|
|
152
173
|
def __init__(self, simulator, t_sim, x_sim, u_sim, w=None, eps=1e-5,
|
|
153
|
-
parallel_sliding=False, parallel_perturbation=False
|
|
174
|
+
parallel_sliding=False, parallel_perturbation=False,
|
|
175
|
+
z_function=None, z_state_names=None):
|
|
154
176
|
""" Construct empirical observability matrix O in sliding windows along a trajectory.
|
|
155
177
|
|
|
156
178
|
:param callable simulator: Simulator object : y = simulator(x0, u, **kwargs)
|
|
@@ -160,7 +182,6 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
160
182
|
:param np.array u_sim: input array (N, m), can also be dict
|
|
161
183
|
:param np.array w: window size for O calculations, will automatically set how many windows to compute
|
|
162
184
|
:params float eps: tolerance for sliding windows
|
|
163
|
-
:param dict/np.array u: inputs array
|
|
164
185
|
:param float eps: epsilon value for perturbations to construct O's, should be small number
|
|
165
186
|
:param bool parallel_sliding: if True, run the sliding windows in parallel
|
|
166
187
|
:param bool parallel_perturbation: if True, run the perturbations in parallel
|
|
@@ -170,6 +191,8 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
170
191
|
self.eps = eps
|
|
171
192
|
self.parallel_sliding = parallel_sliding
|
|
172
193
|
self.parallel_perturbation = parallel_perturbation
|
|
194
|
+
self.z_function = z_function
|
|
195
|
+
self.z_state_names = z_state_names
|
|
173
196
|
|
|
174
197
|
# Set time vector
|
|
175
198
|
self.t_sim = np.array(t_sim)
|
|
@@ -267,12 +290,14 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
267
290
|
|
|
268
291
|
# Pull out time & control inputs in window
|
|
269
292
|
t_win = self.t_sim[win] # time in window
|
|
270
|
-
t_win0 = t_win - t_win[0] # start at 0
|
|
293
|
+
# t_win0 = t_win - t_win[0] # start at 0
|
|
271
294
|
u_win = self.u_sim[win, :] # inputs in window
|
|
272
295
|
|
|
273
296
|
# Calculate O for window
|
|
274
297
|
EOM = EmpiricalObservabilityMatrix(self.simulator, x0, u_win, eps=self.eps,
|
|
275
|
-
parallel=self.parallel_perturbation
|
|
298
|
+
parallel=self.parallel_perturbation,
|
|
299
|
+
z_function=self.z_function,
|
|
300
|
+
z_state_names=self.z_state_names)
|
|
276
301
|
self.EOM = EOM
|
|
277
302
|
|
|
278
303
|
# Store data
|
|
@@ -292,18 +317,18 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
292
317
|
|
|
293
318
|
|
|
294
319
|
class FisherObservability:
|
|
295
|
-
def __init__(self, O, R=None,
|
|
320
|
+
def __init__(self, O, R=None, lam=None):
|
|
296
321
|
""" Evaluate the observability of a state variable(s) using the Fisher Information Matrix.
|
|
297
322
|
|
|
298
323
|
:param np.array O: observability matrix (w*p, n)
|
|
299
324
|
w is the number of time-steps, p is the number of measurements, and n in the number of states
|
|
300
325
|
can also be set as pd.DataFrame where columns set the state names & a multilevel index sets the
|
|
301
326
|
measurement names: O.index names must be ('sensor', 'time_step')
|
|
302
|
-
:param np.array R: measurement noise covariance matrix (w*p x w*p)
|
|
327
|
+
:param None | np.array | float| dict R: measurement noise covariance matrix (w*p x w*p)
|
|
303
328
|
can also be set as pd.DataFrame where R.index = R.columns = O.index
|
|
304
|
-
can also be a scaler where R = R *
|
|
305
|
-
|
|
306
|
-
|
|
329
|
+
can also be a scaler where R = R * I_(nxn)
|
|
330
|
+
can also be dict where keys must correspond to the 'sensor' index in O data-frame
|
|
331
|
+
if None, then R = I_(nxn)
|
|
307
332
|
:param float lam: lamda parameter, if lam='limit' compute F^-1 symbolically, otherwise use Chernoff inverse
|
|
308
333
|
"""
|
|
309
334
|
|
|
@@ -315,7 +340,7 @@ class FisherObservability:
|
|
|
315
340
|
self.sensor_names = tuple(O.index.get_level_values('sensor'))
|
|
316
341
|
self.state_names = tuple(O.columns)
|
|
317
342
|
elif isinstance(O, np.ndarray): # array given
|
|
318
|
-
self.sensor_names = tuple(['y' for
|
|
343
|
+
self.sensor_names = tuple(['y' for _ in range(self.pw)])
|
|
319
344
|
self.state_names = tuple(['x_' + str(n) for n in range(self.n)])
|
|
320
345
|
self.O = pd.DataFrame(O, index=self.sensor_names, columns=self.state_names)
|
|
321
346
|
else:
|
|
@@ -324,7 +349,7 @@ class FisherObservability:
|
|
|
324
349
|
# Set measurement noise covariance matrix
|
|
325
350
|
self.R = pd.DataFrame(np.eye(self.pw), index=self.O.index, columns=self.O.index)
|
|
326
351
|
self.R_inv = pd.DataFrame(np.eye(self.pw), index=self.O.index, columns=self.O.index)
|
|
327
|
-
self.set_noise_covariance(R=R
|
|
352
|
+
self.set_noise_covariance(R=R)
|
|
328
353
|
|
|
329
354
|
# Calculate Fisher Information Matrix
|
|
330
355
|
self.F = self.O.values.T @ self.R_inv.values @ self.O.values
|
|
@@ -353,7 +378,7 @@ class FisherObservability:
|
|
|
353
378
|
# Pull out diagonal elements
|
|
354
379
|
self.error_variance = pd.DataFrame(np.diag(self.F_inv), index=self.O.columns).T
|
|
355
380
|
|
|
356
|
-
def set_noise_covariance(self, R=None
|
|
381
|
+
def set_noise_covariance(self, R=None):
|
|
357
382
|
""" Set the measurement noise covariance matrix.
|
|
358
383
|
"""
|
|
359
384
|
|
|
@@ -361,19 +386,15 @@ class FisherObservability:
|
|
|
361
386
|
self.R = pd.DataFrame(np.eye(self.pw), index=self.O.index, columns=self.O.index)
|
|
362
387
|
|
|
363
388
|
# Set R based on values in dict
|
|
364
|
-
if
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
for s in pd.unique(self.R.index.get_level_values('sensor')):
|
|
370
|
-
R_sensor = self.R.loc[[s], [s]]
|
|
371
|
-
for r in range(R_sensor.shape[0]):
|
|
372
|
-
R_sensor.iloc[r, r] = sensor_noise_dict[s]
|
|
389
|
+
if isinstance(R, dict): # set each distinct sensor's noise level
|
|
390
|
+
for s in pd.unique(self.R.index.get_level_values('sensor')):
|
|
391
|
+
R_sensor = self.R.loc[[s], [s]]
|
|
392
|
+
for r in range(R_sensor.shape[0]):
|
|
393
|
+
R_sensor.iloc[r, r] = R[s]
|
|
373
394
|
|
|
374
|
-
|
|
395
|
+
self.R.loc[[s], [s]] = R_sensor.values
|
|
375
396
|
else:
|
|
376
|
-
if R is None:
|
|
397
|
+
if R is None: # set R as identity matrix
|
|
377
398
|
warnings.warn('R not set, defaulting to identity matrix')
|
|
378
399
|
else: # set R directly
|
|
379
400
|
if np.atleast_1d(R).shape[0] == 1: # given scalar
|
|
@@ -382,8 +403,10 @@ class FisherObservability:
|
|
|
382
403
|
self.R = R.copy()
|
|
383
404
|
elif isinstance(R, np.ndarray): # matrix in array
|
|
384
405
|
self.R = pd.DataFrame(R, index=self.R.index, columns=self.R.columns)
|
|
406
|
+
elif isinstance(R, float) or isinstance(R, int): # set as scalar multiplied by identity matrix
|
|
407
|
+
self.R = R * self.R
|
|
385
408
|
else:
|
|
386
|
-
raise Exception('R must be a numpy array, pandas data-frame, or scalar value')
|
|
409
|
+
raise Exception('R must be a dict, numpy array, pandas data-frame, or scalar value')
|
|
387
410
|
|
|
388
411
|
# Inverse of R
|
|
389
412
|
R_diagonal = np.diag(self.R.values)
|
|
@@ -400,22 +423,23 @@ class FisherObservability:
|
|
|
400
423
|
|
|
401
424
|
|
|
402
425
|
class SlidingFisherObservability:
|
|
403
|
-
def __init__(self, O_list, time=None, lam=1e6, R=None,
|
|
426
|
+
def __init__(self, O_list, time=None, lam=1e6, R=None,
|
|
404
427
|
states=None, sensors=None, time_steps=None, w=None):
|
|
405
428
|
|
|
406
429
|
""" Compute the Fisher information matrix & inverse in sliding windows and pull put the minimum error variance.
|
|
407
430
|
:param list O_list: list of observability matrices O (stored as pd.DataFrame)
|
|
408
|
-
:param np.array time: time vector the same size as O_list
|
|
409
|
-
:param np.array lam: lamda parameter, if lam='limit' compute F^-1 symbolically, otherwise use Chernoff inverse
|
|
410
|
-
:param np.array R: measurement noise covariance matrix (w*p x w*p)
|
|
431
|
+
:param None | np.array time: time vector the same size as O_list
|
|
432
|
+
:param float | np.array lam: lamda parameter, if lam='limit' compute F^-1 symbolically, otherwise use Chernoff inverse
|
|
433
|
+
:param None | np.array | float| dict R: measurement noise covariance matrix (w*p x w*p)
|
|
411
434
|
can also be set as pd.DataFrame where R.index = R.columns = O.index
|
|
412
|
-
can also be a scaler where R = R *
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
:param list states: list of states to use from O's. ex: ['g', 'd']
|
|
416
|
-
:param list sensors: list of sensors to use from O's, ex: ['r']
|
|
417
|
-
:param np.array time_steps: array of time steps to use from O's, ex: np.array([0, 1, 2])
|
|
418
|
-
:param np.array w: window size to use from O's,
|
|
435
|
+
can also be a scaler where R = R * I_(nxn)
|
|
436
|
+
can also be dict where keys must correspond to the 'sensor' index in O data-frame
|
|
437
|
+
if None, then R = I_(nxn)
|
|
438
|
+
:param None | tuple | list states: list of states to use from O's. ex: ['g', 'd']
|
|
439
|
+
:param None | tuple | list sensors: list of sensors to use from O's, ex: ['r']
|
|
440
|
+
:param None | tuple | list | np.array time_steps: array of time steps to use from O's, ex: np.array([0, 1, 2])
|
|
441
|
+
:param None | tuple | list | np.array w: window size to use from O's,
|
|
442
|
+
if None then just grab it from O as the maximum window size
|
|
419
443
|
"""
|
|
420
444
|
|
|
421
445
|
self.O_list = O_list
|
|
@@ -427,7 +451,14 @@ class SlidingFisherObservability:
|
|
|
427
451
|
else:
|
|
428
452
|
self.time = np.array(time)
|
|
429
453
|
|
|
430
|
-
|
|
454
|
+
# Set time-step
|
|
455
|
+
if time is not None:
|
|
456
|
+
if self.n_window > 1: # compute time-step from vector
|
|
457
|
+
self.dt = np.mean(np.diff(self.time))
|
|
458
|
+
else:
|
|
459
|
+
self.dt = 0.0
|
|
460
|
+
else: # default is time-step of 1
|
|
461
|
+
self.dt = 1
|
|
431
462
|
|
|
432
463
|
# Get single O
|
|
433
464
|
O = O_list[0]
|
|
@@ -457,10 +488,8 @@ class SlidingFisherObservability:
|
|
|
457
488
|
self.time_steps = np.array(time_steps)
|
|
458
489
|
|
|
459
490
|
# Compute Fisher information matrix & inverse for each sliding window
|
|
460
|
-
self.EV = [] # collect error variance data for each state over
|
|
461
|
-
self.FO = []
|
|
462
|
-
self.shift_index = int(np.round((1 / 2) * self.w))
|
|
463
|
-
self.shift_time = self.shift_index * self.dt # shift the time forward by half the window size
|
|
491
|
+
self.EV = [] # collect error variance data for each state over windows
|
|
492
|
+
self.FO = [] # collect FisherObservability objects over windows
|
|
464
493
|
for k in range(self.n_window): # each window
|
|
465
494
|
# Get full O
|
|
466
495
|
O = self.O_list[k]
|
|
@@ -469,7 +498,7 @@ class SlidingFisherObservability:
|
|
|
469
498
|
O_subset = O.loc[(self.sensors, self.time_steps), self.states].sort_values(['time_step', 'sensor'])
|
|
470
499
|
|
|
471
500
|
# Compute Fisher information & inverse
|
|
472
|
-
FO = FisherObservability(O_subset,
|
|
501
|
+
FO = FisherObservability(O_subset, R=R, lam=lam)
|
|
473
502
|
self.FO.append(FO)
|
|
474
503
|
|
|
475
504
|
# Collect error variance data
|
|
@@ -478,15 +507,75 @@ class SlidingFisherObservability:
|
|
|
478
507
|
self.EV.append(ev)
|
|
479
508
|
|
|
480
509
|
# Concatenate error variance & make same size as simulation data
|
|
510
|
+
self.shift_index = int(np.round((1 / 2) * self.w))
|
|
511
|
+
self.shift_time = self.shift_index * self.dt # shift the time forward by half the window size
|
|
481
512
|
self.EV = pd.concat(self.EV, axis=0, ignore_index=True)
|
|
482
|
-
self.
|
|
483
|
-
|
|
484
|
-
|
|
513
|
+
if self.n_window > 1: # more than 1 window
|
|
514
|
+
self.EV.index = np.arange(self.shift_index, self.EV.shape[0] + self.shift_index, step=1, dtype=int)
|
|
515
|
+
time_df = pd.DataFrame(np.atleast_2d(self.time).T, columns=['time'])
|
|
516
|
+
self.EV_aligned = pd.concat((time_df, self.EV), axis=1)
|
|
517
|
+
else:
|
|
518
|
+
self.EV_aligned = self.EV.copy()
|
|
485
519
|
|
|
486
520
|
def get_minimum_error_variance(self):
|
|
487
521
|
return self.EV_aligned.copy()
|
|
488
522
|
|
|
489
523
|
|
|
524
|
+
def transform_states(O=None, square_flag=False, z_function=None, x0=None, z_state_names=None):
|
|
525
|
+
""" Transform the coordinates of an observability matrix (O) or Fisher information matrix (F)
|
|
526
|
+
from the original coordinates (x) to new user defined coordinates (z).
|
|
527
|
+
|
|
528
|
+
:param O: observability matrix or Fisher information matrix
|
|
529
|
+
:param boolean square_flag: whether to square the transform Jacobian or not
|
|
530
|
+
should be set to False if passing squared an observability matrix as O
|
|
531
|
+
should be set to True if passing squared a Fisher information matrix as O
|
|
532
|
+
:param callable z_function: function that transforms coordinates from original to new states
|
|
533
|
+
must be of the form z = z_function(x), where x & z are the same size
|
|
534
|
+
should use sympy functions wherever possible
|
|
535
|
+
:param np.array x0: initial state in original coordinates
|
|
536
|
+
:param list | tuple z_state_names: (optional) names of states in new coordinates.
|
|
537
|
+
will only have an effect if O or F is a data-frame
|
|
538
|
+
|
|
539
|
+
:return:
|
|
540
|
+
Z: observability matrix or Fisher information matrix in transformed coordinates
|
|
541
|
+
dzdx: numerical Jacobian dz/dx (inverse of dx/dz) evaluated at x0
|
|
542
|
+
dxdz_sym: symbolic Jacobian dx/dz
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
# Symbolic vector of original states
|
|
546
|
+
x_sym = sp.symbols('x_0:%d' % O.shape[1])
|
|
547
|
+
|
|
548
|
+
# Initialize the Jacobian calculator with a Python function
|
|
549
|
+
jacobian_calculator_func = SymbolicJacobian(func=z_function, state_vars=x_sym)
|
|
550
|
+
|
|
551
|
+
# Get the symbolic Jacobian dx/dz
|
|
552
|
+
dxdz_sym = jacobian_calculator_func.jacobian_symbolic
|
|
553
|
+
|
|
554
|
+
# Get the Jacobian calculator function
|
|
555
|
+
dxdz_function = jacobian_calculator_func.get_jacobian_function()
|
|
556
|
+
|
|
557
|
+
# Evaluate the Jacobian at x0
|
|
558
|
+
dxdz = dxdz_function(np.array(x0))
|
|
559
|
+
|
|
560
|
+
# Take the inverse
|
|
561
|
+
dzdx = np.linalg.inv(dxdz)
|
|
562
|
+
|
|
563
|
+
# Compute the new O or F
|
|
564
|
+
if square_flag: # F
|
|
565
|
+
O_z = dzdx.T @ O @ dzdx
|
|
566
|
+
else: # O
|
|
567
|
+
O_z = O @ dzdx
|
|
568
|
+
|
|
569
|
+
# Set column/index names if data-frame was passed
|
|
570
|
+
if isinstance(O_z, pd.DataFrame):
|
|
571
|
+
if z_state_names is not None:
|
|
572
|
+
O_z.columns = z_state_names
|
|
573
|
+
if square_flag:
|
|
574
|
+
O_z.index = z_state_names
|
|
575
|
+
|
|
576
|
+
return O_z, dzdx, dxdz_sym
|
|
577
|
+
|
|
578
|
+
|
|
490
579
|
class ObservabilityMatrixImage:
|
|
491
580
|
def __init__(self, O, state_names=None, sensor_names=None, vmax_percentile=100, vmin_ratio=1.0, cmap='bwr'):
|
|
492
581
|
""" Display an image of an observability matrix.
|
|
@@ -638,4 +727,4 @@ class ObservabilityMatrixImage:
|
|
|
638
727
|
# Store figure & axis
|
|
639
728
|
self.fig = fig
|
|
640
729
|
self.ax = ax
|
|
641
|
-
self.cbar = cbar
|
|
730
|
+
self.cbar = cbar
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
# from .observability import EmpiricalObservabilityMatrix
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
# class DroneSimulator(EmpiricalObservabilityMatrix):
|
|
6
|
+
# def __init__(self, dt=0.1, mpc_horizon=10, r_u=1e-2, input_mode='direct', control_mode='velocity_body_level'):
|
|
7
|
+
# self.dynamics = DroneModel()
|
|
8
|
+
# super().__init__(self.dynamics.f, self.dynamics.h, dt=dt, mpc_horizon=mpc_horizon,
|
|
9
|
+
# state_names=self.dynamics.state_names,
|
|
10
|
+
# input_names=self.dynamics.input_names,
|
|
11
|
+
# measurement_names=self.dynamics.measurement_names)
|
pybounds/simulator.py
CHANGED
|
@@ -374,7 +374,7 @@ class Simulator(object):
|
|
|
374
374
|
return y_array
|
|
375
375
|
|
|
376
376
|
def get_time_states_inputs_measurements(self):
|
|
377
|
-
return self.time.copy(), self.x.copy(), self.u.copy(), self.
|
|
377
|
+
return self.time.copy(), self.x.copy(), self.u.copy(), self.y.copy()
|
|
378
378
|
|
|
379
379
|
def plot(self, name='x', dpi=150, plot_kwargs=None):
|
|
380
380
|
""" Plot states, inputs.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pybounds/__init__.py,sha256=0MZnwxcaT4ERtEUgcKpkwZVwQ8vVkSIQbYEqSnEQw6Y,438
|
|
2
|
+
pybounds/jacobian.py,sha256=hqDOwwqZMdnlTECz0Rx6txCd4VuZ4iZHPaj62PTkKvA,2057
|
|
3
|
+
pybounds/observability.py,sha256=F3tKpOgf-Ltx_jmNsrdSw5YL99OOkjZCwMZdnj7n9II,31675
|
|
4
|
+
pybounds/observability_transform.py,sha256=YibApe7OzwrZT44BmlZwetJ1JLHOIgMkpVYggWz5Myo,585
|
|
5
|
+
pybounds/simulator.py,sha256=MhK7NjYHdRMToGzwVEuwEnu5Aa6MguF1KE7fAbWCiUA,16194
|
|
6
|
+
pybounds/util.py,sha256=Gs0UgqgLXTJI9FZww90iJhqU02iJ31bXBURjGiq3YzM,7401
|
|
7
|
+
pybounds-0.0.7.dist-info/LICENSE,sha256=kqeyRXtRGgBVZdXYeIX4zR9l2KZ2rqIBVEiPMTjxjcI,1093
|
|
8
|
+
pybounds-0.0.7.dist-info/METADATA,sha256=zi7w4FqcUvDxvYqcVrcdWK3Yxsozc2F6pNibEBDcz4Q,2155
|
|
9
|
+
pybounds-0.0.7.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
10
|
+
pybounds-0.0.7.dist-info/top_level.txt,sha256=V-ofnWE3m_UkXTXJwNRD07n14m5R6sc6l4NadaCCP_A,9
|
|
11
|
+
pybounds-0.0.7.dist-info/RECORD,,
|
pybounds/drone_simulator.py
DELETED
|
@@ -1,422 +0,0 @@
|
|
|
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-0.0.6.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|