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 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, time, 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
- :param callable Simulator: Simulator object : y = simulator(x0, u, **kwargs)
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 vector size N
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, t_win0, u_win, eps=self.eps,
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, sensor_noise_dict=None, lam=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 * I
304
- :param dict sensor_noise_dict: constructs R by setting the noise levels for each sensor across time-steps
305
- keys must correspond to the 'sensor' index in O data-frame, can only be set if R is None
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 n in range(self.pw)])
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, sensor_noise_dict=sensor_noise_dict)
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, sensor_noise_dict=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 sensor_noise_dict is not None: # set each distinct sensor's noise level
364
- if R is not None:
365
- raise Exception('R can not be set directly if sensor_noise_dict is set')
366
- else:
367
- # for s in self.R.index.levels[0]:
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
- self.R.loc[[s], [s]] = R_sensor.values
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, sensor_noise_dict=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 * I
412
- :param dict sensor_noise_dict: constructs R by setting the noise levels for each sensor across time-steps
413
- keys must correspond to the 'sensor' index in O data-frame, can only be set if R is None
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, if None then just grab it from O
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
- self.dt = np.mean(np.diff(self.time))
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 time
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, sensor_noise_dict=sensor_noise_dict, R=R, lam=lam)
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.EV.index = np.arange(self.shift_index, self.EV.shape[0] + self.shift_index, step=1, dtype=int)
482
- time_df = pd.DataFrame(np.atleast_2d(self.time).T, columns=['time'])
483
- self.EV_aligned = pd.concat((time_df, self.EV), axis=1)
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.u.copy()
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.u.copy()
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='set-point', **plot_kwargs)
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
- 'sigma': r'$\sigma$',
100
- 'rho': r'$\rho$',
101
- 'beta': r'$\beta$',
102
- 'lambda': r'$\lambda',
103
- 'delta': r'$\delta'
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.5
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)[mono_camera_example.ipynb](examples%2Fmono_camera_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 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.
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,,
@@ -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,,