pybounds 0.0.11__py3-none-any.whl → 0.0.13__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 +1 -1
- pybounds/observability.py +95 -59
- pybounds/simulator.py +16 -11
- pybounds/util.py +79 -0
- {pybounds-0.0.11.dist-info → pybounds-0.0.13.dist-info}/METADATA +4 -1
- pybounds-0.0.13.dist-info/RECORD +10 -0
- pybounds/observability_transform.py +0 -11
- pybounds-0.0.11.dist-info/RECORD +0 -11
- {pybounds-0.0.11.dist-info → pybounds-0.0.13.dist-info}/LICENSE +0 -0
- {pybounds-0.0.11.dist-info → pybounds-0.0.13.dist-info}/WHEEL +0 -0
- {pybounds-0.0.11.dist-info → pybounds-0.0.13.dist-info}/top_level.txt +0 -0
pybounds/__init__.py
CHANGED
pybounds/observability.py
CHANGED
|
@@ -11,7 +11,7 @@ from .jacobian import SymbolicJacobian
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class EmpiricalObservabilityMatrix:
|
|
14
|
-
def __init__(self, simulator, x0, u, eps=1e-5, parallel=False,
|
|
14
|
+
def __init__(self, simulator, x0, u, aux=None, eps=1e-5, parallel=False,
|
|
15
15
|
z_function=None, z_state_names=None):
|
|
16
16
|
""" Construct an empirical observability matrix O.
|
|
17
17
|
|
|
@@ -19,18 +19,20 @@ class EmpiricalObservabilityMatrix:
|
|
|
19
19
|
y is (w x p) array. w is the number of time-steps and p is the number of measurements
|
|
20
20
|
:param dict/list/np.array x0: initial state for Simulator
|
|
21
21
|
:param dict/np.array u: inputs array
|
|
22
|
+
:param aux: auxiliary input that can be passed to Simulator class
|
|
22
23
|
:param float eps: epsilon value for perturbations to construct O, should be small number
|
|
23
24
|
:param bool parallel: if True, run the perturbations in parallel
|
|
24
25
|
:param callable z_function: function that transforms coordinates from original to new states
|
|
25
26
|
must be of the form z = z_function(x), where x & z are the same size
|
|
26
27
|
should use sympy functions wherever possible
|
|
27
28
|
leave as None to maintain original coordinates
|
|
28
|
-
:param list | tuple z_state_names: (optional) names of states in new coordinates.
|
|
29
|
+
:param list | tuple | None z_state_names: (optional) names of states in new coordinates.
|
|
29
30
|
will only have an effect if O is a data-frame & z_function is not None
|
|
30
31
|
"""
|
|
31
32
|
|
|
32
33
|
# Store inputs
|
|
33
34
|
self.simulator = simulator
|
|
35
|
+
self.aux = aux
|
|
34
36
|
self.eps = eps
|
|
35
37
|
self.parallel = parallel
|
|
36
38
|
|
|
@@ -48,7 +50,7 @@ class EmpiricalObservabilityMatrix:
|
|
|
48
50
|
self.n = self.x0.shape[0]
|
|
49
51
|
|
|
50
52
|
# Simulate once for nominal trajectory
|
|
51
|
-
self.y_nominal = self.simulator.simulate(self.x0, self.u)
|
|
53
|
+
self.y_nominal = self.simulator.simulate(x0=self.x0, u=self.u, aux=self.aux)
|
|
52
54
|
|
|
53
55
|
# Number of outputs
|
|
54
56
|
self.p = self.y_nominal.shape[1]
|
|
@@ -158,8 +160,8 @@ class EmpiricalObservabilityMatrix:
|
|
|
158
160
|
x0_minus = self.x0 - self.delta_x[:, n]
|
|
159
161
|
|
|
160
162
|
# Simulate measurements from perturbed initial conditions
|
|
161
|
-
y_plus = self.simulator.simulate(x0=x0_plus, u=self.u)
|
|
162
|
-
y_minus = self.simulator.simulate(x0=x0_minus, u=self.u)
|
|
163
|
+
y_plus = self.simulator.simulate(x0=x0_plus, u=self.u, aux=self.aux)
|
|
164
|
+
y_minus = self.simulator.simulate(x0=x0_minus, u=self.u, aux=self.aux)
|
|
163
165
|
|
|
164
166
|
# Calculate the numerical Jacobian & normalize by 2x the perturbation amount
|
|
165
167
|
delta_y = np.array(y_plus - y_minus).T / (2 * self.eps)
|
|
@@ -168,7 +170,7 @@ class EmpiricalObservabilityMatrix:
|
|
|
168
170
|
|
|
169
171
|
|
|
170
172
|
class SlidingEmpiricalObservabilityMatrix:
|
|
171
|
-
def __init__(self, simulator, t_sim, x_sim, u_sim, w=None, eps=1e-5,
|
|
173
|
+
def __init__(self, simulator, t_sim, x_sim, u_sim, aux_list=None, w=None, eps=1e-5,
|
|
172
174
|
parallel_sliding=False, parallel_perturbation=False,
|
|
173
175
|
z_function=None, z_state_names=None):
|
|
174
176
|
""" Construct empirical observability matrix O in sliding windows along a trajectory.
|
|
@@ -178,6 +180,7 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
178
180
|
:param np.array t_sim: time values along state trajectory array (N, 1)
|
|
179
181
|
:param np.array x_sim: state trajectory array (N, n), can also be dict
|
|
180
182
|
:param np.array u_sim: input array (N, m), can also be dict
|
|
183
|
+
:param aux_list: auxiliary input that can be passed to Simulator class
|
|
181
184
|
:param np.array w: window size for O calculations, will automatically set how many windows to compute
|
|
182
185
|
:params float eps: tolerance for sliding windows
|
|
183
186
|
:param float eps: epsilon value for perturbations to construct O's, should be small number
|
|
@@ -207,7 +210,7 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
207
210
|
if isinstance(u_sim, dict):
|
|
208
211
|
self.u_sim = np.vstack(list(u_sim.values())).T
|
|
209
212
|
else:
|
|
210
|
-
self.u_sim = np.array(u_sim)
|
|
213
|
+
self.u_sim = np.array(u_sim)
|
|
211
214
|
|
|
212
215
|
# Check sizes
|
|
213
216
|
if self.N != self.x_sim.shape[0]:
|
|
@@ -217,6 +220,15 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
217
220
|
elif self.x_sim.shape[0] != self.u_sim.shape[0]:
|
|
218
221
|
raise ValueError('x_sim & u_sim must have same number of rows')
|
|
219
222
|
|
|
223
|
+
# Set aux inputs
|
|
224
|
+
if aux_list is None:
|
|
225
|
+
self.aux_list = [None for k in range(self.N)]
|
|
226
|
+
else:
|
|
227
|
+
self.aux_list = aux_list
|
|
228
|
+
|
|
229
|
+
if len(self.aux_list) != self.N:
|
|
230
|
+
raise ValueError('aux_list must have same number of elements as t_sim')
|
|
231
|
+
|
|
220
232
|
# Set time-window to calculate O's
|
|
221
233
|
if w is None: # set window size to full time-series size
|
|
222
234
|
self.w = self.N
|
|
@@ -292,7 +304,9 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
292
304
|
u_win = self.u_sim[win, :] # inputs in window
|
|
293
305
|
|
|
294
306
|
# Calculate O for window
|
|
295
|
-
EOM = EmpiricalObservabilityMatrix(self.simulator, x0, u_win,
|
|
307
|
+
EOM = EmpiricalObservabilityMatrix(self.simulator, x0, u_win,
|
|
308
|
+
aux=self.aux_list[n],
|
|
309
|
+
eps=self.eps,
|
|
296
310
|
parallel=self.parallel_perturbation,
|
|
297
311
|
z_function=self.z_function,
|
|
298
312
|
z_state_names=self.z_state_names)
|
|
@@ -315,20 +329,26 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
315
329
|
|
|
316
330
|
|
|
317
331
|
class FisherObservability:
|
|
318
|
-
def __init__(self, O, R=None, lam=None
|
|
332
|
+
def __init__(self, O, R=None, lam=None, force_R_scalar=False,
|
|
333
|
+
states=None, sensors=None, time_steps=None, w=None):
|
|
319
334
|
""" Evaluate the observability of a state variable(s) using the Fisher Information Matrix.
|
|
320
335
|
|
|
321
336
|
:param np.array O: observability matrix (w*p, n)
|
|
322
337
|
w is the number of time-steps, p is the number of measurements, and n in the number of states
|
|
323
338
|
can also be set as pd.DataFrame where columns set the state names & a multilevel index sets the
|
|
324
339
|
measurement names: O.index names must be ('sensor', 'time_step')
|
|
325
|
-
:param None | np.array | float| dict R: measurement noise covariance matrix (w*p x w*p)
|
|
340
|
+
:param None | np.array | float | dict R: measurement noise covariance matrix (w*p x w*p)
|
|
326
341
|
can also be set as pd.DataFrame where R.index = R.columns = O.index
|
|
327
342
|
can also be a scaler where R = R * I_(nxn)
|
|
328
343
|
can also be dict where keys must correspond to the 'sensor' index in O data-frame
|
|
329
344
|
if None, then R = I_(nxn)
|
|
330
345
|
:param float lam: lamda parameter, if lam='limit' compute F^-1 symbolically, otherwise use Chernoff inverse
|
|
331
|
-
|
|
346
|
+
:param bool force_R_scalar: force R to be a scalar, useful when the resulting R matrix is too big to fit in memory
|
|
347
|
+
:param None | tuple | list states: list of states to use from O's. ex: ['g', 'd']
|
|
348
|
+
:param None | tuple | list sensors: list of sensors to use from O's, ex: ['r']
|
|
349
|
+
:param None | tuple | list | np.array time_steps: array of time steps to use from O's, ex: np.array([0, 1, 2])
|
|
350
|
+
:param None | tuple | list | np.array w: window size to use from O's,
|
|
351
|
+
if None then just grab it from O as the maximum window size """
|
|
332
352
|
|
|
333
353
|
# Make O a data-frame
|
|
334
354
|
self.pw = O.shape[0] # number of sensors * time-steps
|
|
@@ -344,14 +364,57 @@ class FisherObservability:
|
|
|
344
364
|
else:
|
|
345
365
|
raise TypeError('O is not a pandas data-frame or numpy array')
|
|
346
366
|
|
|
347
|
-
# Set
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
367
|
+
# Set window size
|
|
368
|
+
if w is None: # set automatically
|
|
369
|
+
self.w = np.max(np.array(self.O.index.get_level_values('time_step'))) + 1
|
|
370
|
+
else:
|
|
371
|
+
self.w = w
|
|
372
|
+
|
|
373
|
+
# Set the states to use
|
|
374
|
+
if states is None:
|
|
375
|
+
self.states = self.O.columns
|
|
376
|
+
else:
|
|
377
|
+
self.states = states
|
|
378
|
+
|
|
379
|
+
# Set the sensors to use
|
|
380
|
+
if sensors is None:
|
|
381
|
+
self.sensors = self.O.index.get_level_values('sensor')
|
|
382
|
+
else:
|
|
383
|
+
self.sensors = sensors
|
|
384
|
+
|
|
385
|
+
# Set the time-steps to use
|
|
386
|
+
if time_steps is None:
|
|
387
|
+
self.time_steps = self.O.index.get_level_values('time_step')
|
|
388
|
+
else:
|
|
389
|
+
self.time_steps = np.array(time_steps)
|
|
390
|
+
|
|
391
|
+
# Get subset of O
|
|
392
|
+
self.O = O.loc[(self.sensors, self.time_steps), self.states].sort_values(['time_step', 'sensor'])
|
|
393
|
+
|
|
394
|
+
# Reset the size of O
|
|
395
|
+
self.pw = self.O.shape[0] # number of sensors * time-steps
|
|
396
|
+
self.n = self.O.shape[1] # number of states
|
|
351
397
|
|
|
352
|
-
#
|
|
353
|
-
|
|
354
|
-
|
|
398
|
+
# Set measurement noise covariance matrix & calculate Fisher Information Matrix
|
|
399
|
+
if force_R_scalar and np.isscalar(R): # scalar R
|
|
400
|
+
self.R = pd.DataFrame({'R': {'index': float(R)}})
|
|
401
|
+
self.R_inv = pd.DataFrame({'R_inv': {'index': 1 / self.R.values.squeeze()}})
|
|
402
|
+
|
|
403
|
+
# Calculate Fisher Information Matrix for scalar R
|
|
404
|
+
self.F = self.R_inv.values.squeeze() * (self.O.values.T @ self.O.values)
|
|
405
|
+
|
|
406
|
+
elif force_R_scalar and not np.isscalar(R):
|
|
407
|
+
raise Exception('R must be a scalar')
|
|
408
|
+
|
|
409
|
+
else: # non-scalar R
|
|
410
|
+
self.R = pd.DataFrame(np.eye(self.pw), index=self.O.index, columns=self.O.index)
|
|
411
|
+
self.R_inv = pd.DataFrame(np.eye(self.pw), index=self.O.index, columns=self.O.index)
|
|
412
|
+
self.set_noise_covariance(R=R)
|
|
413
|
+
|
|
414
|
+
# Calculate Fisher Information Matrix for non-scalar R
|
|
415
|
+
self.F = self.O.values.T @ self.R_inv.values.squeeze() @ self.O.values
|
|
416
|
+
|
|
417
|
+
self.F = pd.DataFrame(self.F, index=self.O.columns, columns=self.O.columns)
|
|
355
418
|
|
|
356
419
|
# Set sigma
|
|
357
420
|
if lam is None:
|
|
@@ -371,7 +434,7 @@ class FisherObservability:
|
|
|
371
434
|
F_epsilon = self.F.values + (self.lam * np.eye(self.n))
|
|
372
435
|
self.F_inv = np.linalg.inv(F_epsilon)
|
|
373
436
|
|
|
374
|
-
self.F_inv = pd.DataFrame(self.F_inv, index=O.columns, columns=O.columns)
|
|
437
|
+
self.F_inv = pd.DataFrame(self.F_inv, index=self.O.columns, columns=self.O.columns)
|
|
375
438
|
|
|
376
439
|
# Pull out diagonal elements
|
|
377
440
|
self.error_variance = pd.DataFrame(np.diag(self.F_inv), index=self.O.columns).T
|
|
@@ -421,18 +484,19 @@ class FisherObservability:
|
|
|
421
484
|
|
|
422
485
|
|
|
423
486
|
class SlidingFisherObservability:
|
|
424
|
-
def __init__(self, O_list,
|
|
487
|
+
def __init__(self, O_list, R=None, lam=1e6, time=None,
|
|
425
488
|
states=None, sensors=None, time_steps=None, w=None):
|
|
426
489
|
|
|
427
490
|
""" Compute the Fisher information matrix & inverse in sliding windows and pull put the minimum error variance.
|
|
491
|
+
|
|
428
492
|
:param list O_list: list of observability matrices O (stored as pd.DataFrame)
|
|
429
|
-
:param None | np.array time: time vector the same size as O_list
|
|
430
|
-
:param float | np.array lam: lamda parameter, if lam='limit' compute F^-1 symbolically, otherwise use Chernoff inverse
|
|
431
493
|
:param None | np.array | float| dict R: measurement noise covariance matrix (w*p x w*p)
|
|
432
494
|
can also be set as pd.DataFrame where R.index = R.columns = O.index
|
|
433
495
|
can also be a scaler where R = R * I_(nxn)
|
|
434
496
|
can also be dict where keys must correspond to the 'sensor' index in O data-frame
|
|
435
497
|
if None, then R = I_(nxn)
|
|
498
|
+
:param float | np.array lam: lamda parameter, if lam='limit' compute F^-1 symbolically, otherwise use Chernoff inverse
|
|
499
|
+
:param None | np.array time: time vector the same size as O_list
|
|
436
500
|
:param None | tuple | list states: list of states to use from O's. ex: ['g', 'd']
|
|
437
501
|
:param None | tuple | list sensors: list of sensors to use from O's, ex: ['r']
|
|
438
502
|
:param None | tuple | list | np.array time_steps: array of time steps to use from O's, ex: np.array([0, 1, 2])
|
|
@@ -458,33 +522,6 @@ class SlidingFisherObservability:
|
|
|
458
522
|
else: # default is time-step of 1
|
|
459
523
|
self.dt = 1
|
|
460
524
|
|
|
461
|
-
# Get single O
|
|
462
|
-
O = O_list[0]
|
|
463
|
-
|
|
464
|
-
# Set window size
|
|
465
|
-
if w is None: # set automatically
|
|
466
|
-
self.w = np.max(np.array(O.index.get_level_values('time_step'))) + 1
|
|
467
|
-
else:
|
|
468
|
-
self.w = w
|
|
469
|
-
|
|
470
|
-
# Set the states to use
|
|
471
|
-
if states is None:
|
|
472
|
-
self.states = O.columns
|
|
473
|
-
else:
|
|
474
|
-
self.states = states
|
|
475
|
-
|
|
476
|
-
# Set the sensors to use
|
|
477
|
-
if sensors is None:
|
|
478
|
-
self.sensors = O.index.get_level_values('sensor')
|
|
479
|
-
else:
|
|
480
|
-
self.sensors = sensors
|
|
481
|
-
|
|
482
|
-
# Set the time-steps to use
|
|
483
|
-
if time_steps is None:
|
|
484
|
-
self.time_steps = O.index.get_level_values('time_step')
|
|
485
|
-
else:
|
|
486
|
-
self.time_steps = np.array(time_steps)
|
|
487
|
-
|
|
488
525
|
# Compute Fisher information matrix & inverse for each sliding window
|
|
489
526
|
self.EV = [] # collect error variance data for each state over windows
|
|
490
527
|
self.FO = [] # collect FisherObservability objects over windows
|
|
@@ -492,11 +529,8 @@ class SlidingFisherObservability:
|
|
|
492
529
|
# Get full O
|
|
493
530
|
O = self.O_list[k]
|
|
494
531
|
|
|
495
|
-
# Get subset of O
|
|
496
|
-
O_subset = O.loc[(self.sensors, self.time_steps), self.states].sort_values(['time_step', 'sensor'])
|
|
497
|
-
|
|
498
532
|
# Compute Fisher information & inverse
|
|
499
|
-
FO = FisherObservability(
|
|
533
|
+
FO = FisherObservability(O, R=R, lam=lam, states=states, sensors=sensors, time_steps=time_steps, w=w)
|
|
500
534
|
self.FO.append(FO)
|
|
501
535
|
|
|
502
536
|
# Collect error variance data
|
|
@@ -505,7 +539,7 @@ class SlidingFisherObservability:
|
|
|
505
539
|
self.EV.append(ev)
|
|
506
540
|
|
|
507
541
|
# Concatenate error variance & make same size as simulation data
|
|
508
|
-
self.shift_index = int(np.round((1 / 2) *
|
|
542
|
+
self.shift_index = int(np.round((1 / 2) * float(FO.w)))
|
|
509
543
|
self.shift_time = self.shift_index * self.dt # shift the time forward by half the window size
|
|
510
544
|
self.EV = pd.concat(self.EV, axis=0, ignore_index=True)
|
|
511
545
|
if self.n_window > 1: # more than 1 window
|
|
@@ -598,8 +632,10 @@ class ObservabilityMatrixImage:
|
|
|
598
632
|
|
|
599
633
|
# Default sensor names based on data-frame 'sensor' index
|
|
600
634
|
sensor_names_all = list(np.unique(O.index.get_level_values('sensor')))
|
|
601
|
-
self.
|
|
602
|
-
|
|
635
|
+
self.sensors = list(O.index.get_level_values('sensor'))
|
|
636
|
+
self.time_steps = np.array(O.index.get_level_values('time_step'))
|
|
637
|
+
self.sensor_names_default = self.sensors[0:len(sensor_names_all)]
|
|
638
|
+
self.time_steps_default = np.unique(self.time_steps)
|
|
603
639
|
else: # numpy matrix
|
|
604
640
|
raise TypeError('n-sensor must be an integer value when O is given as a numpy matrix')
|
|
605
641
|
|
|
@@ -629,7 +665,7 @@ class ObservabilityMatrixImage:
|
|
|
629
665
|
self.measurement_names = []
|
|
630
666
|
for w in range(self.n_time_step):
|
|
631
667
|
for p in range(self.n_sensor):
|
|
632
|
-
m = '$' + self.sensor_names[p] + ',_{' + 'k=' + str(w) + '}$'
|
|
668
|
+
m = '$' + self.sensor_names[p] + ',_{' + 'k=' + str(self.time_steps_default[w]) + '}$'
|
|
633
669
|
self.measurement_names.append(m)
|
|
634
670
|
|
|
635
671
|
elif len(sensor_names) == 1:
|
|
@@ -638,7 +674,7 @@ class ObservabilityMatrixImage:
|
|
|
638
674
|
self.measurement_names = []
|
|
639
675
|
for w in range(self.n_time_step):
|
|
640
676
|
for p in range(self.n_sensor):
|
|
641
|
-
m = '$' + sensor_names[0] + '_{' + str(p) + ',k=' + str(w) + '}$'
|
|
677
|
+
m = '$' + sensor_names[0] + '_{' + str(p) + ',k=' + str(self.time_steps_default[w]) + '}$'
|
|
642
678
|
self.measurement_names.append(m)
|
|
643
679
|
else:
|
|
644
680
|
raise TypeError('sensor_names must be of length p or length 1')
|
|
@@ -649,7 +685,7 @@ class ObservabilityMatrixImage:
|
|
|
649
685
|
self.measurement_names = []
|
|
650
686
|
for w in range(self.n_time_step):
|
|
651
687
|
for p in range(self.n_sensor):
|
|
652
|
-
m = '$' + self.sensor_names[p] + '_{' + ',k=' + str(w) + '}$'
|
|
688
|
+
m = '$' + self.sensor_names[p] + '_{' + ',k=' + str(self.time_steps_default[w]) + '}$'
|
|
653
689
|
self.measurement_names.append(m)
|
|
654
690
|
|
|
655
691
|
def plot(self, vmax_percentile=100, vmin_ratio=0.0, vmax_override=None, cmap='bwr', grid=True, scale=1.0, dpi=150,
|
pybounds/simulator.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import warnings
|
|
3
3
|
import numpy as np
|
|
4
4
|
import matplotlib.pyplot as plt
|
|
5
|
+
import casadi
|
|
5
6
|
import do_mpc
|
|
6
7
|
from .util import FixedKeysDict, SetDict
|
|
7
8
|
|
|
@@ -129,7 +130,7 @@ class Simulator(object):
|
|
|
129
130
|
# Define dynamics
|
|
130
131
|
Xdot = self.f(X, U)
|
|
131
132
|
for n, state_name in enumerate(self.state_names):
|
|
132
|
-
self.model.set_rhs(state_name, Xdot[n])
|
|
133
|
+
self.model.set_rhs(state_name, casadi.SX(Xdot[n]))
|
|
133
134
|
|
|
134
135
|
# Add time-varying set-point variables for later use with MPC
|
|
135
136
|
for n, state_name in enumerate(self.state_names):
|
|
@@ -271,12 +272,14 @@ class Simulator(object):
|
|
|
271
272
|
if not np.all(points_check):
|
|
272
273
|
raise Exception(name + ' not the same size')
|
|
273
274
|
|
|
274
|
-
def simulate(self, x0=None, u=None, mpc=False, return_full_output=False):
|
|
275
|
+
def simulate(self, x0=None, u=None, aux=None, mpc=False, return_full_output=False):
|
|
275
276
|
"""
|
|
276
277
|
Simulate the system.
|
|
277
278
|
|
|
278
279
|
:params x0: initial state dict or array
|
|
279
|
-
:params u: input dict or array
|
|
280
|
+
:params u: input dict or array, if True then mpc must be None
|
|
281
|
+
:params aux: auxiliary input
|
|
282
|
+
:params mpc: boolean to run MPC, if True then u must be None
|
|
280
283
|
:params return_full_output: boolean to run (time, x, u, y) instead of y
|
|
281
284
|
"""
|
|
282
285
|
|
|
@@ -420,14 +423,16 @@ class Simulator(object):
|
|
|
420
423
|
ax[n].legend(fontsize=6)
|
|
421
424
|
|
|
422
425
|
y = self.x[key]
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
426
|
+
else:
|
|
427
|
+
y = plot_dict[key]
|
|
428
|
+
|
|
429
|
+
# Set y-axis limits
|
|
430
|
+
y_min = np.min(y)
|
|
431
|
+
y_max = np.max(y)
|
|
432
|
+
delta = y_max - y_min
|
|
433
|
+
if np.abs(delta) < 0.01:
|
|
434
|
+
margin = 0.1
|
|
435
|
+
ax[n].set_ylim(y_min - margin, y_max + margin)
|
|
431
436
|
|
|
432
437
|
ax[-1].set_xlabel('time', fontsize=7)
|
|
433
438
|
ax[0].set_title(name, fontsize=8, fontweight='bold')
|
pybounds/util.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
import scipy
|
|
3
|
+
import matplotlib as mpl
|
|
2
4
|
import matplotlib.pyplot as plt
|
|
3
5
|
import matplotlib.collections as mcoll
|
|
4
6
|
import matplotlib.patheffects as path_effects
|
|
@@ -198,3 +200,80 @@ def colorline(x, y, z, ax=None, cmap=plt.get_cmap('copper'), norm=None, linewidt
|
|
|
198
200
|
ax.add_collection(lc)
|
|
199
201
|
|
|
200
202
|
return lc
|
|
203
|
+
|
|
204
|
+
def plot_heatmap_log_timeseries(data, ax=None, log_ticks=None, data_labels=None,
|
|
205
|
+
cmap='inferno_r', y_label=None,
|
|
206
|
+
aspect=0.25, interpolation=False):
|
|
207
|
+
""" Plot log-scale time-series as heatmap.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
n_label = data.shape[1]
|
|
211
|
+
|
|
212
|
+
# Set ticks
|
|
213
|
+
if log_ticks is None:
|
|
214
|
+
log_tick_low = int(np.floor(np.log10(np.min(data))))
|
|
215
|
+
log_tick_high = int(np.ceil(np.log10(np.max(data))))
|
|
216
|
+
else:
|
|
217
|
+
log_tick_low = log_ticks[0]
|
|
218
|
+
log_tick_high = log_ticks[1]
|
|
219
|
+
|
|
220
|
+
log_ticks = np.logspace(log_tick_low, log_tick_high, log_tick_high - log_tick_low + 1)
|
|
221
|
+
|
|
222
|
+
# Set color normalization
|
|
223
|
+
cnorm = mpl.colors.LogNorm(10 ** log_tick_low, 10 ** log_tick_high)
|
|
224
|
+
|
|
225
|
+
# Set labels
|
|
226
|
+
if data_labels is None:
|
|
227
|
+
data_labels = np.arange(0, n_label).tolist()
|
|
228
|
+
data_labels = [str(x) for x in data_labels]
|
|
229
|
+
|
|
230
|
+
# Make figure/axis
|
|
231
|
+
if ax is None:
|
|
232
|
+
fig, ax = plt.subplots(1, 1, figsize=(5 * 1, 4 * 1), dpi=150)
|
|
233
|
+
else:
|
|
234
|
+
# ax = plt.gca()
|
|
235
|
+
fig = plt.gcf()
|
|
236
|
+
|
|
237
|
+
# Plot heatmap
|
|
238
|
+
if interpolation:
|
|
239
|
+
data = 10**scipy.ndimage.zoom(np.log10(data), (interpolation, 1), order=1)
|
|
240
|
+
aspect = aspect / interpolation
|
|
241
|
+
|
|
242
|
+
ax.imshow(data, norm=cnorm, aspect=aspect, cmap=cmap, interpolation='none')
|
|
243
|
+
|
|
244
|
+
# Set axis properties
|
|
245
|
+
ax.grid(True, axis='x')
|
|
246
|
+
ax.tick_params(axis='both', which='both', labelsize=6, top=False, labeltop=True, bottom=False, labelbottom=False,
|
|
247
|
+
color='gray')
|
|
248
|
+
|
|
249
|
+
# Set x-ticks
|
|
250
|
+
LatexConverter = LatexStates()
|
|
251
|
+
data_labels_latex = LatexConverter.convert_to_latex(data_labels)
|
|
252
|
+
ax.set_xticks(np.arange(0, len(data_labels)) - 0.5)
|
|
253
|
+
ax.set_xticklabels(data_labels_latex)
|
|
254
|
+
|
|
255
|
+
# Set labels
|
|
256
|
+
ax.set_ylabel('time steps', fontsize=7, fontweight='bold')
|
|
257
|
+
ax.set_xlabel('states', fontsize=7, fontweight='bold')
|
|
258
|
+
ax.xaxis.set_label_position('top')
|
|
259
|
+
|
|
260
|
+
# Set x-ticks
|
|
261
|
+
xticks = ax.get_xticklabels()
|
|
262
|
+
for tick in xticks:
|
|
263
|
+
tick.set_ha('left')
|
|
264
|
+
tick.set_va('center')
|
|
265
|
+
# tick.set_rotation(0)
|
|
266
|
+
# tick.set_transform(tick.get_transform() + transforms.ScaledTranslation(6 / 72, 0, ax.figure.dpi_scale_trans))
|
|
267
|
+
|
|
268
|
+
# Colorbar
|
|
269
|
+
if y_label is None:
|
|
270
|
+
y_label = 'values'
|
|
271
|
+
|
|
272
|
+
cax = ax.inset_axes((1.03, 0.0, 0.04, 1.0))
|
|
273
|
+
cbar = fig.colorbar(mpl.cm.ScalarMappable(norm=cnorm, cmap=cmap), cax=cax, ticks=log_ticks)
|
|
274
|
+
cbar.set_label(y_label, rotation=270, fontsize=7, labelpad=8)
|
|
275
|
+
cbar.ax.tick_params(labelsize=6)
|
|
276
|
+
|
|
277
|
+
ax.spines[['bottom', 'top', 'left', 'right']].set_color('gray')
|
|
278
|
+
|
|
279
|
+
return cnorm, cmap, log_ticks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pybounds
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.13
|
|
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
|
|
@@ -41,6 +41,9 @@ For a simple system
|
|
|
41
41
|
For a more complex system
|
|
42
42
|
* Fly-wind: [fly_wind_example.ipynb](examples%2Ffly_wind_example.ipynb)
|
|
43
43
|
|
|
44
|
+
## Guides
|
|
45
|
+
* Creating a custom simulator:
|
|
46
|
+
|
|
44
47
|
## Citation
|
|
45
48
|
|
|
46
49
|
If you use the code or methods from this package, please cite the following paper:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pybounds/__init__.py,sha256=mGTxRNwJmvRhRG5M8pVX2QmPV9a6L-bP0DAAM6pAX-A,462
|
|
2
|
+
pybounds/jacobian.py,sha256=hqDOwwqZMdnlTECz0Rx6txCd4VuZ4iZHPaj62PTkKvA,2057
|
|
3
|
+
pybounds/observability.py,sha256=IIC-iaHIZSmKBnxcYKhn5Q18bBX9rwebz0F1twQHE0Y,33922
|
|
4
|
+
pybounds/simulator.py,sha256=Qx9QGFYuuFnyuq1z5dcub5T_Lz8ISWcKLLrHup9UY0A,16702
|
|
5
|
+
pybounds/util.py,sha256=_nWwDLY07Aj8UlzK-xjy-hO_8btUwqDjsiwQmNTkRug,10136
|
|
6
|
+
pybounds-0.0.13.dist-info/LICENSE,sha256=kqeyRXtRGgBVZdXYeIX4zR9l2KZ2rqIBVEiPMTjxjcI,1093
|
|
7
|
+
pybounds-0.0.13.dist-info/METADATA,sha256=fEdYfbP1PU3V2aKA-ce4nt-D5uWWGr3StZR0osVBgz4,2202
|
|
8
|
+
pybounds-0.0.13.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
9
|
+
pybounds-0.0.13.dist-info/top_level.txt,sha256=V-ofnWE3m_UkXTXJwNRD07n14m5R6sc6l4NadaCCP_A,9
|
|
10
|
+
pybounds-0.0.13.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
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-0.0.11.dist-info/RECORD
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
pybounds/__init__.py,sha256=e4SQdDBQzqr7_x5lcQsuSoWqfBg258i7CDrhhvnmCe4,433
|
|
2
|
-
pybounds/jacobian.py,sha256=hqDOwwqZMdnlTECz0Rx6txCd4VuZ4iZHPaj62PTkKvA,2057
|
|
3
|
-
pybounds/observability.py,sha256=GmWrPADr-vCbBJIv0Cjk3Gj9JJoCuOizKrF8lzX3ncs,31553
|
|
4
|
-
pybounds/observability_transform.py,sha256=YibApe7OzwrZT44BmlZwetJ1JLHOIgMkpVYggWz5Myo,585
|
|
5
|
-
pybounds/simulator.py,sha256=nKLtMQs_HgNYKIJ1YIg63lW2v-r7sT7WxLyouOV07wA,16518
|
|
6
|
-
pybounds/util.py,sha256=Gs0UgqgLXTJI9FZww90iJhqU02iJ31bXBURjGiq3YzM,7401
|
|
7
|
-
pybounds-0.0.11.dist-info/LICENSE,sha256=kqeyRXtRGgBVZdXYeIX4zR9l2KZ2rqIBVEiPMTjxjcI,1093
|
|
8
|
-
pybounds-0.0.11.dist-info/METADATA,sha256=Evpk9-4sk6w_czjxjX-CYC1Y9eFuRGuYRcYrNlMQqjg,2156
|
|
9
|
-
pybounds-0.0.11.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
10
|
-
pybounds-0.0.11.dist-info/top_level.txt,sha256=V-ofnWE3m_UkXTXJwNRD07n14m5R6sc6l4NadaCCP_A,9
|
|
11
|
-
pybounds-0.0.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|