pybounds 0.0.5__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 +142 -52
- pybounds/observability_transform.py +11 -0
- pybounds/simulator.py +13 -4
- pybounds/util.py +42 -7
- {pybounds-0.0.5.dist-info → pybounds-0.0.7.dist-info}/METADATA +3 -3
- pybounds-0.0.7.dist-info/RECORD +11 -0
- pybounds-0.0.5.dist-info/RECORD +0 -9
- {pybounds-0.0.5.dist-info → pybounds-0.0.7.dist-info}/LICENSE +0 -0
- {pybounds-0.0.5.dist-info → pybounds-0.0.7.dist-info}/WHEEL +0 -0
- {pybounds-0.0.5.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,23 +9,30 @@ 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,
|
|
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
|
-
:param callable
|
|
20
|
+
:param callable simulator: simulator object that has a method y = simulator.simulate(x0, u, **kwargs)
|
|
19
21
|
y is (w x p) array. w is the number of time-steps and p is the number of measurements
|
|
20
22
|
:param dict/list/np.array x0: initial state for Simulator
|
|
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
|
|
27
35
|
self.simulator = simulator
|
|
28
|
-
self.time = time.copy()
|
|
29
36
|
self.eps = eps
|
|
30
37
|
self.parallel = parallel
|
|
31
38
|
|
|
@@ -48,6 +55,9 @@ class EmpiricalObservabilityMatrix:
|
|
|
48
55
|
# Number of outputs
|
|
49
56
|
self.p = self.y_nominal.shape[1]
|
|
50
57
|
|
|
58
|
+
# Number of time-steps
|
|
59
|
+
self.w = self.y_nominal.shape[0] # of points in time window
|
|
60
|
+
|
|
51
61
|
# Check for state/measurement names
|
|
52
62
|
if hasattr(self.simulator, 'state_names'):
|
|
53
63
|
self.state_names = self.simulator.state_names
|
|
@@ -60,7 +70,6 @@ class EmpiricalObservabilityMatrix:
|
|
|
60
70
|
self.measurement_names = ['y_' + str(p) for p in range(self.p)]
|
|
61
71
|
|
|
62
72
|
# Perturbation amounts
|
|
63
|
-
self.w = len(self.time) # of points in time window
|
|
64
73
|
self.delta_x = eps * np.eye(self.n) # perturbation amount for each state
|
|
65
74
|
self.delta_y = np.zeros((self.p, self.n, self.w)) # preallocate delta_y
|
|
66
75
|
self.y_plus = np.zeros((self.w, self.n, self.p))
|
|
@@ -80,9 +89,22 @@ class EmpiricalObservabilityMatrix:
|
|
|
80
89
|
|
|
81
90
|
self.time_labels = np.hstack(self.time_labels)
|
|
82
91
|
|
|
83
|
-
# Run
|
|
92
|
+
# Run simulations to construct O
|
|
84
93
|
self.run()
|
|
85
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
|
+
|
|
86
108
|
def run(self, parallel=None):
|
|
87
109
|
""" Construct empirical observability matrix.
|
|
88
110
|
"""
|
|
@@ -149,17 +171,17 @@ class EmpiricalObservabilityMatrix:
|
|
|
149
171
|
|
|
150
172
|
class SlidingEmpiricalObservabilityMatrix:
|
|
151
173
|
def __init__(self, simulator, t_sim, x_sim, u_sim, w=None, eps=1e-5,
|
|
152
|
-
parallel_sliding=False, parallel_perturbation=False
|
|
174
|
+
parallel_sliding=False, parallel_perturbation=False,
|
|
175
|
+
z_function=None, z_state_names=None):
|
|
153
176
|
""" Construct empirical observability matrix O in sliding windows along a trajectory.
|
|
154
177
|
|
|
155
178
|
:param callable simulator: Simulator object : y = simulator(x0, u, **kwargs)
|
|
156
179
|
y is (w x p) array. w is the number of time-steps and p is the number of measurements
|
|
157
|
-
:param np.array t_sim: time
|
|
180
|
+
:param np.array t_sim: time values along state trajectory array (N, 1)
|
|
158
181
|
:param np.array x_sim: state trajectory array (N, n), can also be dict
|
|
159
182
|
:param np.array u_sim: input array (N, m), can also be dict
|
|
160
183
|
:param np.array w: window size for O calculations, will automatically set how many windows to compute
|
|
161
184
|
:params float eps: tolerance for sliding windows
|
|
162
|
-
:param dict/np.array u: inputs array
|
|
163
185
|
:param float eps: epsilon value for perturbations to construct O's, should be small number
|
|
164
186
|
:param bool parallel_sliding: if True, run the sliding windows in parallel
|
|
165
187
|
:param bool parallel_perturbation: if True, run the perturbations in parallel
|
|
@@ -169,6 +191,8 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
169
191
|
self.eps = eps
|
|
170
192
|
self.parallel_sliding = parallel_sliding
|
|
171
193
|
self.parallel_perturbation = parallel_perturbation
|
|
194
|
+
self.z_function = z_function
|
|
195
|
+
self.z_state_names = z_state_names
|
|
172
196
|
|
|
173
197
|
# Set time vector
|
|
174
198
|
self.t_sim = np.array(t_sim)
|
|
@@ -266,12 +290,14 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
266
290
|
|
|
267
291
|
# Pull out time & control inputs in window
|
|
268
292
|
t_win = self.t_sim[win] # time in window
|
|
269
|
-
t_win0 = t_win - t_win[0] # start at 0
|
|
293
|
+
# t_win0 = t_win - t_win[0] # start at 0
|
|
270
294
|
u_win = self.u_sim[win, :] # inputs in window
|
|
271
295
|
|
|
272
296
|
# Calculate O for window
|
|
273
|
-
EOM = EmpiricalObservabilityMatrix(self.simulator, x0,
|
|
274
|
-
parallel=self.parallel_perturbation
|
|
297
|
+
EOM = EmpiricalObservabilityMatrix(self.simulator, x0, u_win, eps=self.eps,
|
|
298
|
+
parallel=self.parallel_perturbation,
|
|
299
|
+
z_function=self.z_function,
|
|
300
|
+
z_state_names=self.z_state_names)
|
|
275
301
|
self.EOM = EOM
|
|
276
302
|
|
|
277
303
|
# Store data
|
|
@@ -291,18 +317,18 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
291
317
|
|
|
292
318
|
|
|
293
319
|
class FisherObservability:
|
|
294
|
-
def __init__(self, O, R=None,
|
|
320
|
+
def __init__(self, O, R=None, lam=None):
|
|
295
321
|
""" Evaluate the observability of a state variable(s) using the Fisher Information Matrix.
|
|
296
322
|
|
|
297
323
|
:param np.array O: observability matrix (w*p, n)
|
|
298
324
|
w is the number of time-steps, p is the number of measurements, and n in the number of states
|
|
299
325
|
can also be set as pd.DataFrame where columns set the state names & a multilevel index sets the
|
|
300
326
|
measurement names: O.index names must be ('sensor', 'time_step')
|
|
301
|
-
: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)
|
|
302
328
|
can also be set as pd.DataFrame where R.index = R.columns = O.index
|
|
303
|
-
can also be a scaler where R = R *
|
|
304
|
-
|
|
305
|
-
|
|
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)
|
|
306
332
|
:param float lam: lamda parameter, if lam='limit' compute F^-1 symbolically, otherwise use Chernoff inverse
|
|
307
333
|
"""
|
|
308
334
|
|
|
@@ -314,7 +340,7 @@ class FisherObservability:
|
|
|
314
340
|
self.sensor_names = tuple(O.index.get_level_values('sensor'))
|
|
315
341
|
self.state_names = tuple(O.columns)
|
|
316
342
|
elif isinstance(O, np.ndarray): # array given
|
|
317
|
-
self.sensor_names = tuple(['y' for
|
|
343
|
+
self.sensor_names = tuple(['y' for _ in range(self.pw)])
|
|
318
344
|
self.state_names = tuple(['x_' + str(n) for n in range(self.n)])
|
|
319
345
|
self.O = pd.DataFrame(O, index=self.sensor_names, columns=self.state_names)
|
|
320
346
|
else:
|
|
@@ -323,7 +349,7 @@ class FisherObservability:
|
|
|
323
349
|
# Set measurement noise covariance matrix
|
|
324
350
|
self.R = pd.DataFrame(np.eye(self.pw), index=self.O.index, columns=self.O.index)
|
|
325
351
|
self.R_inv = pd.DataFrame(np.eye(self.pw), index=self.O.index, columns=self.O.index)
|
|
326
|
-
self.set_noise_covariance(R=R
|
|
352
|
+
self.set_noise_covariance(R=R)
|
|
327
353
|
|
|
328
354
|
# Calculate Fisher Information Matrix
|
|
329
355
|
self.F = self.O.values.T @ self.R_inv.values @ self.O.values
|
|
@@ -352,7 +378,7 @@ class FisherObservability:
|
|
|
352
378
|
# Pull out diagonal elements
|
|
353
379
|
self.error_variance = pd.DataFrame(np.diag(self.F_inv), index=self.O.columns).T
|
|
354
380
|
|
|
355
|
-
def set_noise_covariance(self, R=None
|
|
381
|
+
def set_noise_covariance(self, R=None):
|
|
356
382
|
""" Set the measurement noise covariance matrix.
|
|
357
383
|
"""
|
|
358
384
|
|
|
@@ -360,19 +386,15 @@ class FisherObservability:
|
|
|
360
386
|
self.R = pd.DataFrame(np.eye(self.pw), index=self.O.index, columns=self.O.index)
|
|
361
387
|
|
|
362
388
|
# Set R based on values in dict
|
|
363
|
-
if
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
for s in pd.unique(self.R.index.get_level_values('sensor')):
|
|
369
|
-
R_sensor = self.R.loc[[s], [s]]
|
|
370
|
-
for r in range(R_sensor.shape[0]):
|
|
371
|
-
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]
|
|
372
394
|
|
|
373
|
-
|
|
395
|
+
self.R.loc[[s], [s]] = R_sensor.values
|
|
374
396
|
else:
|
|
375
|
-
if R is None:
|
|
397
|
+
if R is None: # set R as identity matrix
|
|
376
398
|
warnings.warn('R not set, defaulting to identity matrix')
|
|
377
399
|
else: # set R directly
|
|
378
400
|
if np.atleast_1d(R).shape[0] == 1: # given scalar
|
|
@@ -381,8 +403,10 @@ class FisherObservability:
|
|
|
381
403
|
self.R = R.copy()
|
|
382
404
|
elif isinstance(R, np.ndarray): # matrix in array
|
|
383
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
|
|
384
408
|
else:
|
|
385
|
-
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')
|
|
386
410
|
|
|
387
411
|
# Inverse of R
|
|
388
412
|
R_diagonal = np.diag(self.R.values)
|
|
@@ -399,22 +423,23 @@ class FisherObservability:
|
|
|
399
423
|
|
|
400
424
|
|
|
401
425
|
class SlidingFisherObservability:
|
|
402
|
-
def __init__(self, O_list, time=None, lam=1e6, R=None,
|
|
426
|
+
def __init__(self, O_list, time=None, lam=1e6, R=None,
|
|
403
427
|
states=None, sensors=None, time_steps=None, w=None):
|
|
404
428
|
|
|
405
429
|
""" Compute the Fisher information matrix & inverse in sliding windows and pull put the minimum error variance.
|
|
406
430
|
:param list O_list: list of observability matrices O (stored as pd.DataFrame)
|
|
407
|
-
:param np.array time: time vector the same size as O_list
|
|
408
|
-
:param np.array lam: lamda parameter, if lam='limit' compute F^-1 symbolically, otherwise use Chernoff inverse
|
|
409
|
-
: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)
|
|
410
434
|
can also be set as pd.DataFrame where R.index = R.columns = O.index
|
|
411
|
-
can also be a scaler where R = R *
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
:param list states: list of states to use from O's. ex: ['g', 'd']
|
|
415
|
-
:param list sensors: list of sensors to use from O's, ex: ['r']
|
|
416
|
-
:param np.array time_steps: array of time steps to use from O's, ex: np.array([0, 1, 2])
|
|
417
|
-
: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
|
|
418
443
|
"""
|
|
419
444
|
|
|
420
445
|
self.O_list = O_list
|
|
@@ -426,7 +451,14 @@ class SlidingFisherObservability:
|
|
|
426
451
|
else:
|
|
427
452
|
self.time = np.array(time)
|
|
428
453
|
|
|
429
|
-
|
|
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
|
|
430
462
|
|
|
431
463
|
# Get single O
|
|
432
464
|
O = O_list[0]
|
|
@@ -456,10 +488,8 @@ class SlidingFisherObservability:
|
|
|
456
488
|
self.time_steps = np.array(time_steps)
|
|
457
489
|
|
|
458
490
|
# Compute Fisher information matrix & inverse for each sliding window
|
|
459
|
-
self.EV = [] # collect error variance data for each state over
|
|
460
|
-
self.FO = []
|
|
461
|
-
self.shift_index = int(np.round((1 / 2) * self.w))
|
|
462
|
-
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
|
|
463
493
|
for k in range(self.n_window): # each window
|
|
464
494
|
# Get full O
|
|
465
495
|
O = self.O_list[k]
|
|
@@ -468,7 +498,7 @@ class SlidingFisherObservability:
|
|
|
468
498
|
O_subset = O.loc[(self.sensors, self.time_steps), self.states].sort_values(['time_step', 'sensor'])
|
|
469
499
|
|
|
470
500
|
# Compute Fisher information & inverse
|
|
471
|
-
FO = FisherObservability(O_subset,
|
|
501
|
+
FO = FisherObservability(O_subset, R=R, lam=lam)
|
|
472
502
|
self.FO.append(FO)
|
|
473
503
|
|
|
474
504
|
# Collect error variance data
|
|
@@ -477,15 +507,75 @@ class SlidingFisherObservability:
|
|
|
477
507
|
self.EV.append(ev)
|
|
478
508
|
|
|
479
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
|
|
480
512
|
self.EV = pd.concat(self.EV, axis=0, ignore_index=True)
|
|
481
|
-
self.
|
|
482
|
-
|
|
483
|
-
|
|
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()
|
|
484
519
|
|
|
485
520
|
def get_minimum_error_variance(self):
|
|
486
521
|
return self.EV_aligned.copy()
|
|
487
522
|
|
|
488
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
|
+
|
|
489
579
|
class ObservabilityMatrixImage:
|
|
490
580
|
def __init__(self, O, state_names=None, sensor_names=None, vmax_percentile=100, vmin_ratio=1.0, cmap='bwr'):
|
|
491
581
|
""" Display an image of an observability matrix.
|
|
@@ -637,4 +727,4 @@ class ObservabilityMatrixImage:
|
|
|
637
727
|
# Store figure & axis
|
|
638
728
|
self.fig = fig
|
|
639
729
|
self.ax = ax
|
|
640
|
-
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
|
@@ -369,12 +369,12 @@ class Simulator(object):
|
|
|
369
369
|
y_array = np.vstack(list(self.y.values())).T
|
|
370
370
|
|
|
371
371
|
if return_full_output:
|
|
372
|
-
return self.time.copy(), self.x.copy(), self.u.copy(), self.
|
|
372
|
+
return self.time.copy(), self.x.copy(), self.u.copy(), self.y.copy()
|
|
373
373
|
else:
|
|
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.
|
|
@@ -406,7 +406,7 @@ class Simulator(object):
|
|
|
406
406
|
ax = np.atleast_1d(ax)
|
|
407
407
|
|
|
408
408
|
for n, key in enumerate(plot_dict.keys()):
|
|
409
|
-
ax[n].plot(self.time, plot_dict[key], label=
|
|
409
|
+
ax[n].plot(self.time, plot_dict[key], label=name, **plot_kwargs)
|
|
410
410
|
ax[n].set_ylabel(key, fontsize=7)
|
|
411
411
|
|
|
412
412
|
# Also plot the states if plotting setpoint
|
|
@@ -414,9 +414,18 @@ class Simulator(object):
|
|
|
414
414
|
ax[n].plot(self.time, self.x[key], label=key, color='firebrick', linestyle='-', linewidth=0.5)
|
|
415
415
|
ax[n].legend(fontsize=6)
|
|
416
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
|
+
|
|
417
427
|
ax[-1].set_xlabel('time', fontsize=7)
|
|
418
428
|
ax[0].set_title(name, fontsize=8, fontweight='bold')
|
|
419
429
|
|
|
420
|
-
|
|
421
430
|
for a in ax.flat:
|
|
422
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)
|
|
@@ -96,11 +96,46 @@ class LatexStates:
|
|
|
96
96
|
'v_para_dot': r'$\dot{v_{\parallel}}$',
|
|
97
97
|
'v_perp_dot': r'$\dot{v_{\perp}}$',
|
|
98
98
|
'v_para_dot_ratio': r'$\frac{\Delta v_{\parallel}}{v_{\parallel}}$',
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
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$',
|
|
104
139
|
}
|
|
105
140
|
|
|
106
141
|
if dict is not None:
|
|
@@ -162,4 +197,4 @@ def colorline(x, y, z, ax=None, cmap=plt.get_cmap('copper'), norm=None, linewidt
|
|
|
162
197
|
|
|
163
198
|
ax.add_collection(lc)
|
|
164
199
|
|
|
165
|
-
return lc
|
|
200
|
+
return lc
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pybounds
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
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
|
|
@@ -39,13 +39,13 @@ For a simple system
|
|
|
39
39
|
* Monocular camera with optic fow measurements: [mono_camera_example.ipynb](examples%2Fmono_camera_example.ipynb)
|
|
40
40
|
|
|
41
41
|
For a more complex system
|
|
42
|
-
* Fly-wind: [fly_wind_example.ipynb](examples%2Ffly_wind_example.ipynb)
|
|
42
|
+
* Fly-wind: [fly_wind_example.ipynb](examples%2Ffly_wind_example.ipynb)
|
|
43
43
|
|
|
44
44
|
## Citation
|
|
45
45
|
|
|
46
46
|
If you use the code or methods from this package, please cite the following paper:
|
|
47
47
|
|
|
48
|
-
Benjamin Cellini, Burak
|
|
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.
|
|
49
49
|
|
|
50
50
|
## Related packages
|
|
51
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,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-0.0.5.dist-info/RECORD
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
pybounds/__init__.py,sha256=so9LuRNw2V8MGsDs-RPstGGgJtBFp2YGolQbS0rVfhw,348
|
|
2
|
-
pybounds/observability.py,sha256=MUdwufFf200SYseP42JCP978O4F7GbeBVtm0yGjGzOE,27490
|
|
3
|
-
pybounds/simulator.py,sha256=-CrQVpNfiBDFECd6H7FU5has4sYGW1gyS2RhOgXUqZg,15858
|
|
4
|
-
pybounds/util.py,sha256=l6S9G88S-OZO9mi7F_58bVAPSr3PGQKcbHUghgni4JY,5956
|
|
5
|
-
pybounds-0.0.5.dist-info/LICENSE,sha256=kqeyRXtRGgBVZdXYeIX4zR9l2KZ2rqIBVEiPMTjxjcI,1093
|
|
6
|
-
pybounds-0.0.5.dist-info/METADATA,sha256=sQnyiQHrQHFzXHh_mr4ymujDAh7MZved-QnSKC08JTM,2226
|
|
7
|
-
pybounds-0.0.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
8
|
-
pybounds-0.0.5.dist-info/top_level.txt,sha256=V-ofnWE3m_UkXTXJwNRD07n14m5R6sc6l4NadaCCP_A,9
|
|
9
|
-
pybounds-0.0.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|