pybounds 0.0.1__py3-none-any.whl → 0.0.2__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/observability.py +125 -73
- pybounds/simulator.py +45 -24
- pybounds-0.0.2.dist-info/METADATA +60 -0
- pybounds-0.0.2.dist-info/RECORD +9 -0
- {pybounds-0.0.1.dist-info → pybounds-0.0.2.dist-info}/WHEEL +1 -1
- pybounds-0.0.1.dist-info/METADATA +0 -15
- pybounds-0.0.1.dist-info/RECORD +0 -9
- {pybounds-0.0.1.dist-info → pybounds-0.0.2.dist-info}/LICENSE +0 -0
- {pybounds-0.0.1.dist-info → pybounds-0.0.2.dist-info}/top_level.txt +0 -0
pybounds/observability.py
CHANGED
|
@@ -11,20 +11,20 @@ from util import LatexStates
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class EmpiricalObservabilityMatrix:
|
|
14
|
-
def __init__(self, simulator, x0,
|
|
14
|
+
def __init__(self, simulator, x0, time, u, eps=1e-5, parallel=False):
|
|
15
15
|
""" Construct an empirical observability matrix O.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
:param callable Simulator: Simulator object : y = simulator(x0, u, **kwargs)
|
|
18
|
+
y is (w x p) array. w is the number of time-steps and p is the number of measurements
|
|
19
|
+
:param dict/list/np.array x0: initial state for Simulator
|
|
20
|
+
:param dict/np.array u: inputs array
|
|
21
|
+
:param float eps: epsilon value for perturbations to construct O, should be small number
|
|
22
|
+
:param bool parallel: if True, run the perturbations in parallel
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
# Store inputs
|
|
26
26
|
self.simulator = simulator
|
|
27
|
-
self.
|
|
27
|
+
self.time = time.copy()
|
|
28
28
|
self.eps = eps
|
|
29
29
|
self.parallel = parallel
|
|
30
30
|
|
|
@@ -33,35 +33,49 @@ class EmpiricalObservabilityMatrix:
|
|
|
33
33
|
else:
|
|
34
34
|
self.x0 = np.array(x0).squeeze()
|
|
35
35
|
|
|
36
|
-
if isinstance(
|
|
37
|
-
self.
|
|
36
|
+
if isinstance(u, dict):
|
|
37
|
+
self.u = np.vstack(list(u.values())).T
|
|
38
38
|
else:
|
|
39
|
-
self.
|
|
39
|
+
self.u = np.array(u)
|
|
40
|
+
|
|
41
|
+
# Number of states
|
|
42
|
+
self.n = self.x0.shape[0]
|
|
40
43
|
|
|
41
44
|
# Simulate once for nominal trajectory
|
|
42
|
-
self.y_nominal = self.simulator.simulate(self.x0, self.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
self.y_nominal = self.simulator.simulate(self.x0, self.u)
|
|
46
|
+
|
|
47
|
+
# Number of outputs
|
|
48
|
+
self.p = self.y_nominal.shape[1]
|
|
49
|
+
|
|
50
|
+
# Check for state/measurement names
|
|
51
|
+
if hasattr(self.simulator, 'state_names'):
|
|
52
|
+
self.state_names = self.simulator.state_names
|
|
53
|
+
else:
|
|
54
|
+
self.state_names = ['x_' + str(n) for n in range(self.n)]
|
|
55
|
+
|
|
56
|
+
if hasattr(self.simulator, 'measurement_names'):
|
|
57
|
+
self.measurement_names = self.simulator.measurement_names
|
|
58
|
+
else:
|
|
59
|
+
self.measurement_names = ['y_' + str(p) for p in range(self.p)]
|
|
46
60
|
|
|
47
61
|
# Perturbation amounts
|
|
48
|
-
self.w = len(
|
|
49
|
-
self.delta_x = eps * np.eye(self.
|
|
50
|
-
self.delta_y = np.zeros((self.
|
|
51
|
-
self.y_plus = np.zeros((self.w, self.
|
|
52
|
-
self.y_minus = np.zeros((self.w, self.
|
|
62
|
+
self.w = len(self.time) # of points in time window
|
|
63
|
+
self.delta_x = eps * np.eye(self.n) # perturbation amount for each state
|
|
64
|
+
self.delta_y = np.zeros((self.p, self.n, self.w)) # preallocate delta_y
|
|
65
|
+
self.y_plus = np.zeros((self.w, self.n, self.p))
|
|
66
|
+
self.y_minus = np.zeros((self.w, self.n, self.p))
|
|
53
67
|
|
|
54
68
|
# Observability matrix
|
|
55
|
-
self.O = np.nan * np.zeros((self.
|
|
69
|
+
self.O = np.nan * np.zeros((self.p * self.w, self.n))
|
|
56
70
|
self.O_df = pd.DataFrame(self.O)
|
|
57
71
|
|
|
58
72
|
# Set measurement names
|
|
59
73
|
self.measurement_labels = []
|
|
60
74
|
self.time_labels = []
|
|
61
75
|
for w in range(self.w):
|
|
62
|
-
tl = (w * np.ones(self.
|
|
76
|
+
tl = (w * np.ones(self.p)).astype(int)
|
|
63
77
|
self.time_labels.append(tl)
|
|
64
|
-
self.measurement_labels = self.measurement_labels + list(self.
|
|
78
|
+
self.measurement_labels = self.measurement_labels + list(self.measurement_names)
|
|
65
79
|
|
|
66
80
|
self.time_labels = np.hstack(self.time_labels)
|
|
67
81
|
|
|
@@ -76,7 +90,7 @@ class EmpiricalObservabilityMatrix:
|
|
|
76
90
|
self.parallel = parallel
|
|
77
91
|
|
|
78
92
|
# Run simulations for perturbed initial conditions
|
|
79
|
-
state_index = np.arange(0, self.
|
|
93
|
+
state_index = np.arange(0, self.n).tolist()
|
|
80
94
|
if self.parallel: # multiprocessing
|
|
81
95
|
with Pool(4) as pool:
|
|
82
96
|
results = pool.map(self.simulate, state_index)
|
|
@@ -95,18 +109,18 @@ class EmpiricalObservabilityMatrix:
|
|
|
95
109
|
self.y_minus[:, n, :] = y_minus
|
|
96
110
|
|
|
97
111
|
# Construct O by stacking the 3rd dimension of delta_y along the 1st dimension, O is a (p*w x n) matrix
|
|
98
|
-
self.O = np.zeros((self.
|
|
112
|
+
self.O = np.zeros((self.p * self.w, self.n))
|
|
99
113
|
for w in range(self.w):
|
|
100
114
|
if w == 0:
|
|
101
115
|
start_index = 0
|
|
102
116
|
else:
|
|
103
|
-
start_index = int(w * self.
|
|
117
|
+
start_index = int(w * self.p)
|
|
104
118
|
|
|
105
|
-
end_index = start_index + self.
|
|
119
|
+
end_index = start_index + self.p
|
|
106
120
|
self.O[start_index:end_index] = self.delta_y[:, :, w]
|
|
107
121
|
|
|
108
122
|
# Make O into a data-frame for interpretability
|
|
109
|
-
self.O_df = pd.DataFrame(self.O, columns=self.
|
|
123
|
+
self.O_df = pd.DataFrame(self.O, columns=self.state_names, index=self.measurement_labels)
|
|
110
124
|
self.O_df['time_step'] = self.time_labels
|
|
111
125
|
self.O_df = self.O_df.set_index('time_step', append=True)
|
|
112
126
|
self.O_df.index.names = ['sensor', 'time_step']
|
|
@@ -120,8 +134,8 @@ class EmpiricalObservabilityMatrix:
|
|
|
120
134
|
x0_minus = self.x0 - self.delta_x[:, n]
|
|
121
135
|
|
|
122
136
|
# Simulate measurements from perturbed initial conditions
|
|
123
|
-
y_plus = self.simulator.simulate(x0=x0_plus, u=self.
|
|
124
|
-
y_minus = self.simulator.simulate(x0=x0_minus, u=self.
|
|
137
|
+
y_plus = self.simulator.simulate(x0=x0_plus, u=self.u)
|
|
138
|
+
y_minus = self.simulator.simulate(x0=x0_minus, u=self.u)
|
|
125
139
|
|
|
126
140
|
# Calculate the numerical Jacobian & normalize by 2x the perturbation amount
|
|
127
141
|
delta_y = np.array(y_plus - y_minus).T / (2 * self.eps)
|
|
@@ -130,23 +144,35 @@ class EmpiricalObservabilityMatrix:
|
|
|
130
144
|
|
|
131
145
|
|
|
132
146
|
class SlidingEmpiricalObservabilityMatrix:
|
|
133
|
-
def __init__(self, simulator, t_sim, x_sim, u_sim, w=None, eps=1e-5,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
def __init__(self, simulator, t_sim, x_sim, u_sim, w=None, eps=1e-5,
|
|
148
|
+
parallel_sliding=False, parallel_perturbation=False):
|
|
149
|
+
""" Construct empirical observability matrix O in sliding windows along a trajectory.
|
|
150
|
+
|
|
151
|
+
:param callable simulator: Simulator object : y = simulator(x0, u, **kwargs)
|
|
152
|
+
y is (w x p) array. w is the number of time-steps and p is the number of measurements
|
|
153
|
+
:param np.array t_sim: time vector size N
|
|
154
|
+
:param np.array x_sim: state trajectory array (N, n), can also be dict
|
|
155
|
+
:param np.array u_sim: input array (N, m), can also be dict
|
|
156
|
+
:param np.array w: window size for O calculations, will automatically set how many windows to compute
|
|
157
|
+
:params float eps: tolerance for sliding windows
|
|
158
|
+
:param dict/np.array u: inputs array
|
|
159
|
+
:param float eps: epsilon value for perturbations to construct O's, should be small number
|
|
160
|
+
:param bool parallel_sliding: if True, run the sliding windows in parallel
|
|
161
|
+
:param bool parallel_perturbation: if True, run the perturbations in parallel
|
|
143
162
|
"""
|
|
144
163
|
|
|
145
164
|
self.simulator = simulator
|
|
146
|
-
self.t_sim = t_sim.copy()
|
|
147
165
|
self.eps = eps
|
|
148
|
-
self.
|
|
166
|
+
self.parallel_sliding = parallel_sliding
|
|
167
|
+
self.parallel_perturbation = parallel_perturbation
|
|
168
|
+
|
|
169
|
+
# Set time vector
|
|
170
|
+
self.t_sim = np.array(t_sim)
|
|
149
171
|
|
|
172
|
+
# Number of points
|
|
173
|
+
self.N = self.t_sim.shape[0]
|
|
174
|
+
|
|
175
|
+
# Make x_sim & u_sim arrays
|
|
150
176
|
if isinstance(x_sim, dict):
|
|
151
177
|
self.x_sim = np.vstack((list(x_sim.values()))).T
|
|
152
178
|
else:
|
|
@@ -157,16 +183,22 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
157
183
|
else:
|
|
158
184
|
self.u_sim = np.array(u_sim).squeeze()
|
|
159
185
|
|
|
160
|
-
#
|
|
161
|
-
self.N
|
|
186
|
+
# Check sizes
|
|
187
|
+
if self.N != self.x_sim.shape[0]:
|
|
188
|
+
raise ValueError('t_sim & x_sim must have same number of rows')
|
|
189
|
+
elif self.N != self.u_sim.shape[0]:
|
|
190
|
+
raise ValueError('t_sim & u_sim must have same number of rows')
|
|
191
|
+
elif self.x_sim.shape[0] != self.u_sim.shape[0]:
|
|
192
|
+
raise ValueError('x_sim & u_sim must have same number of rows')
|
|
162
193
|
|
|
194
|
+
# Set time-window to calculate O's
|
|
163
195
|
if w is None: # set window size to full time-series size
|
|
164
196
|
self.w = self.N
|
|
165
197
|
else:
|
|
166
198
|
self.w = w
|
|
167
199
|
|
|
168
200
|
if self.w > self.N:
|
|
169
|
-
raise ValueError('
|
|
201
|
+
raise ValueError('window size must be smaller than trajectory length')
|
|
170
202
|
|
|
171
203
|
# All the indices to calculate O
|
|
172
204
|
self.O_index = np.arange(0, self.N - self.w + 1, step=1) # indices to compute O
|
|
@@ -174,7 +206,7 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
174
206
|
self.n_point = len(self.O_index) # # of times to calculate O
|
|
175
207
|
|
|
176
208
|
# Where to store sliding window trajectory data & O's
|
|
177
|
-
self.window_data = {
|
|
209
|
+
self.window_data = {}
|
|
178
210
|
self.O_sliding = []
|
|
179
211
|
self.O_df_sliding = []
|
|
180
212
|
|
|
@@ -182,21 +214,21 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
182
214
|
self.EOM = None
|
|
183
215
|
self.run()
|
|
184
216
|
|
|
185
|
-
def run(self,
|
|
217
|
+
def run(self, parallel_sliding=None):
|
|
186
218
|
""" Run.
|
|
187
219
|
"""
|
|
188
220
|
|
|
189
|
-
if
|
|
190
|
-
self.
|
|
221
|
+
if parallel_sliding is not None:
|
|
222
|
+
self.parallel_sliding = parallel_sliding
|
|
191
223
|
|
|
192
224
|
# Where to store sliding window trajectory data & O's
|
|
193
|
-
self.window_data = {'t': [], 'u': [], '
|
|
225
|
+
self.window_data = {'t': [], 'u': [], 'y': [], 'y_plus': [], 'y_minus': []}
|
|
194
226
|
self.O_sliding = []
|
|
195
227
|
self.O_df_sliding = []
|
|
196
228
|
|
|
197
229
|
# Construct O's
|
|
198
230
|
n_point_range = np.arange(0, self.n_point).astype(int)
|
|
199
|
-
if self.
|
|
231
|
+
if self.parallel_sliding: # multiprocessing
|
|
200
232
|
with Pool(4) as pool:
|
|
201
233
|
results = pool.map(self.construct, n_point_range)
|
|
202
234
|
for r in results:
|
|
@@ -230,7 +262,7 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
230
262
|
u_win = self.u_sim[win, :] # inputs in window
|
|
231
263
|
|
|
232
264
|
# Calculate O for window
|
|
233
|
-
EOM = EmpiricalObservabilityMatrix(self.simulator, x0, t_win0, u_win, eps=self.eps, parallel=
|
|
265
|
+
EOM = EmpiricalObservabilityMatrix(self.simulator, x0, t_win0, u_win, eps=self.eps, parallel=self.parallel_perturbation)
|
|
234
266
|
self.EOM = EOM
|
|
235
267
|
|
|
236
268
|
# Store data
|
|
@@ -239,23 +271,30 @@ class SlidingEmpiricalObservabilityMatrix:
|
|
|
239
271
|
|
|
240
272
|
window_data = {'t': t_win.copy(),
|
|
241
273
|
'u': u_win.copy(),
|
|
242
|
-
'x': EOM.x_nominal.copy(),
|
|
243
274
|
'y': EOM.y_nominal.copy(),
|
|
244
275
|
'y_plus': EOM.y_plus.copy(),
|
|
245
276
|
'y_minus': EOM.y_minus.copy()}
|
|
246
277
|
|
|
247
278
|
return O_sliding, O_df_sliding, window_data
|
|
248
279
|
|
|
280
|
+
def get_observability_matrix(self):
|
|
281
|
+
return self.O_df_sliding.copy()
|
|
282
|
+
|
|
249
283
|
|
|
250
284
|
class FisherObservability:
|
|
251
|
-
def __init__(self, O, R=None, sensor_noise_dict=None,
|
|
285
|
+
def __init__(self, O, R=None, sensor_noise_dict=None, lam=None):
|
|
252
286
|
""" Evaluate the observability of a state variable(s) using the Fisher Information Matrix.
|
|
253
287
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
288
|
+
:param np.array O: observability matrix (w*p, n)
|
|
289
|
+
w is the number of time-steps, p is the number of measurements, and n in the number of states
|
|
290
|
+
can also be set as pd.DataFrame where columns set the state names & a multilevel index sets the
|
|
291
|
+
measurement names: O.index names must be ('sensor', 'time_step')
|
|
292
|
+
:param np.array R: measurement noise covariance matrix (w*p x w*p)
|
|
293
|
+
can also be set as pd.DataFrame where R.index = R.columns = O.index
|
|
294
|
+
can also be a scaler where R = R * I
|
|
295
|
+
:param dict sensor_noise_dict: constructs R by setting the noise levels for each sensor across time-steps
|
|
296
|
+
keys must correspond to the 'sensor' index in O data-frame, can only be set if R is None
|
|
297
|
+
:param float lam: lamda parameter, if lam='limit' compute F^-1 symbolically, otherwise use Chernoff inverse
|
|
259
298
|
"""
|
|
260
299
|
|
|
261
300
|
# Make O a data-frame
|
|
@@ -282,21 +321,21 @@ class FisherObservability:
|
|
|
282
321
|
self.F = pd.DataFrame(self.F, index=O.columns, columns=O.columns)
|
|
283
322
|
|
|
284
323
|
# Set sigma
|
|
285
|
-
if
|
|
324
|
+
if lam is None:
|
|
286
325
|
# np.linalg.eig(self.F)
|
|
287
|
-
self.
|
|
326
|
+
self.lam = 0.0
|
|
288
327
|
else:
|
|
289
|
-
self.
|
|
328
|
+
self.lam = lam
|
|
290
329
|
|
|
291
330
|
# Invert F
|
|
292
|
-
if self.
|
|
331
|
+
if self.lam == 'limit': # calculate limit with symbolic sigma
|
|
293
332
|
sigma_sym = sp.symbols('sigma')
|
|
294
333
|
F_hat = self.F.values + sp.Matrix(sigma_sym * np.eye(self.n))
|
|
295
334
|
F_hat_inv = F_hat.inv()
|
|
296
335
|
F_hat_inv_limit = F_hat_inv.applyfunc(lambda elem: sp.limit(elem, sigma_sym, 0))
|
|
297
336
|
self.F_inv = np.array(F_hat_inv_limit, dtype=np.float64)
|
|
298
337
|
else: # numeric sigma
|
|
299
|
-
F_epsilon = self.F.values + (self.
|
|
338
|
+
F_epsilon = self.F.values + (self.lam * np.eye(self.n))
|
|
300
339
|
self.F_inv = np.linalg.inv(F_epsilon)
|
|
301
340
|
|
|
302
341
|
self.F_inv = pd.DataFrame(self.F_inv, index=O.columns, columns=O.columns)
|
|
@@ -346,17 +385,27 @@ class FisherObservability:
|
|
|
346
385
|
|
|
347
386
|
self.R_inv = pd.DataFrame(self.R_inv, index=self.R.index, columns=self.R.index)
|
|
348
387
|
|
|
388
|
+
def get_fisher_information(self):
|
|
389
|
+
return self.F.copy(), self.F_inv.copy(), self.R.copy()
|
|
390
|
+
|
|
349
391
|
|
|
350
392
|
class SlidingFisherObservability:
|
|
351
|
-
def __init__(self, O_list, time=None,
|
|
393
|
+
def __init__(self, O_list, time=None, lam=1e6, R=None, sensor_noise_dict=None,
|
|
352
394
|
states=None, sensors=None, time_steps=None, w=None):
|
|
353
395
|
|
|
354
396
|
""" Compute the Fisher information matrix & inverse in sliding windows and pull put the minimum error variance.
|
|
355
|
-
:param O_list: list of observability matrices O
|
|
356
|
-
:param time: time vector the same size as O_list
|
|
357
|
-
:param
|
|
358
|
-
:param
|
|
359
|
-
|
|
397
|
+
:param list O_list: list of observability matrices O (stored as pd.DataFrame)
|
|
398
|
+
:param np.array time: time vector the same size as O_list
|
|
399
|
+
:param np.array lam: lamda parameter, if lam='limit' compute F^-1 symbolically, otherwise use Chernoff inverse
|
|
400
|
+
:param np.array R: measurement noise covariance matrix (w*p x w*p)
|
|
401
|
+
can also be set as pd.DataFrame where R.index = R.columns = O.index
|
|
402
|
+
can also be a scaler where R = R * I
|
|
403
|
+
:param dict sensor_noise_dict: constructs R by setting the noise levels for each sensor across time-steps
|
|
404
|
+
keys must correspond to the 'sensor' index in O data-frame, can only be set if R is None
|
|
405
|
+
:param list states: list of states to use from O's. ex: ['g', 'd']
|
|
406
|
+
:param list sensors: list of sensors to use from O's, ex: ['r']
|
|
407
|
+
:param np.array time_steps: array of time steps to use from O's, ex: np.array([0, 1, 2])
|
|
408
|
+
:param np.array w: window size to use from O's, if None then just grab it from O
|
|
360
409
|
"""
|
|
361
410
|
|
|
362
411
|
self.O_list = O_list
|
|
@@ -366,7 +415,7 @@ class SlidingFisherObservability:
|
|
|
366
415
|
if time is None:
|
|
367
416
|
self.time = np.arange(0, self.n_window, step=1)
|
|
368
417
|
else:
|
|
369
|
-
self.time = time
|
|
418
|
+
self.time = np.array(time)
|
|
370
419
|
|
|
371
420
|
self.dt = np.mean(np.diff(self.time))
|
|
372
421
|
|
|
@@ -395,7 +444,7 @@ class SlidingFisherObservability:
|
|
|
395
444
|
if time_steps is None:
|
|
396
445
|
self.time_steps = O.index.get_level_values('time_step')
|
|
397
446
|
else:
|
|
398
|
-
self.time_steps = time_steps
|
|
447
|
+
self.time_steps = np.array(time_steps)
|
|
399
448
|
|
|
400
449
|
# Compute Fisher information matrix & inverse for each sliding window
|
|
401
450
|
self.EV = [] # collect error variance data for each state over time
|
|
@@ -410,7 +459,7 @@ class SlidingFisherObservability:
|
|
|
410
459
|
O_subset = O.loc[(self.sensors, self.time_steps), self.states].sort_values(['time_step', 'sensor'])
|
|
411
460
|
|
|
412
461
|
# Compute Fisher information & inverse
|
|
413
|
-
FO = FisherObservability(O_subset, sensor_noise_dict=sensor_noise_dict, R=R,
|
|
462
|
+
FO = FisherObservability(O_subset, sensor_noise_dict=sensor_noise_dict, R=R, lam=lam)
|
|
414
463
|
self.FO.append(FO)
|
|
415
464
|
|
|
416
465
|
# Collect error variance data
|
|
@@ -424,6 +473,9 @@ class SlidingFisherObservability:
|
|
|
424
473
|
time_df = pd.DataFrame(np.atleast_2d(self.time).T, columns=['time'])
|
|
425
474
|
self.EV_aligned = pd.concat((time_df, self.EV), axis=1)
|
|
426
475
|
|
|
476
|
+
def get_minimum_error_variance(self):
|
|
477
|
+
return self.EV_aligned.copy()
|
|
478
|
+
|
|
427
479
|
|
|
428
480
|
class ObservabilityMatrixImage:
|
|
429
481
|
def __init__(self, O, state_names=None, sensor_names=None, vmax_percentile=100, vmin_ratio=1.0, cmap='bwr'):
|
pybounds/simulator.py
CHANGED
|
@@ -7,42 +7,66 @@ from util import FixedKeysDict, SetDict
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class Simulator(object):
|
|
10
|
-
def __init__(self, f, h,
|
|
10
|
+
def __init__(self, f, h, dt=0.01, n=None, m=None,
|
|
11
11
|
state_names=None, input_names=None, measurement_names=None,
|
|
12
12
|
params_simulator=None):
|
|
13
|
+
|
|
13
14
|
""" Simulator.
|
|
14
|
-
|
|
15
|
-
:param
|
|
15
|
+
|
|
16
|
+
:param callable f: dynamics function f(X, U, t)
|
|
17
|
+
:param callable h: measurement function h(X, U, t)
|
|
16
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
|
|
17
25
|
"""
|
|
18
26
|
|
|
19
27
|
self.f = f
|
|
20
28
|
self.h = h
|
|
21
29
|
self.dt = dt
|
|
22
30
|
|
|
23
|
-
# Set state & input sizes
|
|
24
|
-
self.n = len(x0) # number of states
|
|
25
|
-
self.m = len(u0) # number of inputs
|
|
26
|
-
|
|
27
|
-
# Run measurement function to get measurement size
|
|
28
|
-
y = self.h(x0, u0, 0)
|
|
29
|
-
self.p = len(y) # number of measurements
|
|
30
|
-
|
|
31
31
|
# Set state names
|
|
32
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
|
+
|
|
33
38
|
self.state_names = ['x_' + str(n) for n in range(self.n)]
|
|
34
|
-
else:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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')
|
|
38
47
|
|
|
39
48
|
# Set input names
|
|
40
|
-
if input_names is None: # default
|
|
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
|
+
|
|
41
55
|
self.input_names = ['u_' + str(m) for m in range(self.m)]
|
|
42
|
-
else:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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, 0)
|
|
69
|
+
self.p = len(y) # number of measurements
|
|
46
70
|
|
|
47
71
|
# Set measurement names
|
|
48
72
|
if measurement_names is None: # default measurement names
|
|
@@ -52,8 +76,6 @@ class Simulator(object):
|
|
|
52
76
|
if len(self.measurement_names) != self.p:
|
|
53
77
|
raise ValueError('measurement_names must have length equal to y')
|
|
54
78
|
|
|
55
|
-
self.output_mode = self.measurement_names
|
|
56
|
-
|
|
57
79
|
# Initialize time vector
|
|
58
80
|
w = 10 # initialize for w time-steps, but this can change later
|
|
59
81
|
self.time = np.arange(0, w * self.dt + self.dt / 2, step=self.dt) # time vector
|
|
@@ -168,8 +190,7 @@ class Simulator(object):
|
|
|
168
190
|
|
|
169
191
|
:params x0: initial state dict or array
|
|
170
192
|
:params u: input dict or array
|
|
171
|
-
:params
|
|
172
|
-
:params run_mpc: boolean to run MPC controller
|
|
193
|
+
:params return_full_output: boolean to run (time, x, u, y) instead of y
|
|
173
194
|
"""
|
|
174
195
|
|
|
175
196
|
# Update the initial state
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pybounds
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Bounding Observability for Uncertain Nonlinear Dynamics Systems (BOUNDS)
|
|
5
|
+
Home-page: https://pypi.org/project/pybounds/
|
|
6
|
+
Author: Ben Cellini, Burak Boyacioglu, Floris van Breugel
|
|
7
|
+
Author-email: bcellini00@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
|
|
15
|
+
# pybounds
|
|
16
|
+
|
|
17
|
+
Python implementation of BOUNDS: Bounding Observability for Uncertain Nonlinear Dynamic Systems.
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="https://pynumdiff.readthedocs.io/en/master/" target="_blank" >
|
|
21
|
+
|
|
22
|
+
[//]: # ( <img alt="Python for Numerical Differentiation of noisy time series data" src="docs/source/_static/logo_PyNumDiff.png" width="300" height="200" />)
|
|
23
|
+
</a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<a href="https://pypi.org/project/pybounds/">
|
|
28
|
+
<img src="https://badge.fury.io/py/pynumdiff.svg" alt="PyPI version" height="18"></a>
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
## Introduction
|
|
32
|
+
|
|
33
|
+
This repository provides a minimal working example demonstrating how to empirically calculate the observability level of individual states for a nonlinear (partially observable) system, and accounts for sensor noise.
|
|
34
|
+
|
|
35
|
+
## Installing
|
|
36
|
+
|
|
37
|
+
The package can be installed by cloning the repo and running python setup.py install from inside the home pybounds directory.
|
|
38
|
+
|
|
39
|
+
Alternatively using pip
|
|
40
|
+
```bash
|
|
41
|
+
pip install pybounds
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Notebook examples
|
|
45
|
+
There is currently one simple example notebook. More to come.
|
|
46
|
+
* Monocular camera with optic fow measurements: [mono_camera_example.ipynb](examples%2Fmono_camera_example.ipynb)
|
|
47
|
+
|
|
48
|
+
## Citation
|
|
49
|
+
|
|
50
|
+
If you use the code or methods from this package, please cite the following paper:
|
|
51
|
+
|
|
52
|
+
Benjamin Cellini, Burak Boyacıoğlu, Stanley David Stupski, and Floris van Breugel. Discovering and exploiting active sensing motifs for estimation with empirical observability. (2024) bioRxiv.
|
|
53
|
+
|
|
54
|
+
## Related packages
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
This project utilizes the [MIT LICENSE](LICENSE.txt).
|
|
60
|
+
100% open-source, feel free to utilize the code however you like.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pybounds/__init__.py,sha256=so9LuRNw2V8MGsDs-RPstGGgJtBFp2YGolQbS0rVfhw,348
|
|
2
|
+
pybounds/observability.py,sha256=_A6HEy6wMEMhRoxXojMZYeximhz3PPR3MdLTaihJR7U,27057
|
|
3
|
+
pybounds/simulator.py,sha256=p1O17cvbr_otjGTsDTreuugeF3lEI7NpwII6uaoCkaQ,9780
|
|
4
|
+
pybounds/util.py,sha256=xxmXmpLR3yK923X6wAPKp_5w814cO3m9OqF1XIt9S8c,5818
|
|
5
|
+
pybounds-0.0.2.dist-info/LICENSE,sha256=kqeyRXtRGgBVZdXYeIX4zR9l2KZ2rqIBVEiPMTjxjcI,1093
|
|
6
|
+
pybounds-0.0.2.dist-info/METADATA,sha256=Zeh9auhV9fqq0Le2paSPD1-hrMN0AsWJ_zKZd5Symc4,2376
|
|
7
|
+
pybounds-0.0.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
8
|
+
pybounds-0.0.2.dist-info/top_level.txt,sha256=V-ofnWE3m_UkXTXJwNRD07n14m5R6sc6l4NadaCCP_A,9
|
|
9
|
+
pybounds-0.0.2.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: pybounds
|
|
3
|
-
Version: 0.0.1
|
|
4
|
-
Summary: Bounding Observability for Uncertain Nonlinear Dynamics Systems (BOUNDS)
|
|
5
|
-
Home-page: https://github.com/vanbreugel-lab/pybounds
|
|
6
|
-
Author: Ben Cellini, Burak Boyacioglu, Floris van Breugel
|
|
7
|
-
Author-email: bcellini00@gmail.com
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.9
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
License-File: LICENSE
|
|
14
|
-
|
|
15
|
-
# pybounds
|
pybounds-0.0.1.dist-info/RECORD
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
pybounds/__init__.py,sha256=so9LuRNw2V8MGsDs-RPstGGgJtBFp2YGolQbS0rVfhw,348
|
|
2
|
-
pybounds/observability.py,sha256=tiJdR5tS72SGn1df8Vnz303nDPzhOFaQWSYJoY1o8Qg,24055
|
|
3
|
-
pybounds/simulator.py,sha256=-XpPKQUcwNubw40wNfoqSfqmohbGpBjSQSNWctyfRkQ,8837
|
|
4
|
-
pybounds/util.py,sha256=xxmXmpLR3yK923X6wAPKp_5w814cO3m9OqF1XIt9S8c,5818
|
|
5
|
-
pybounds-0.0.1.dist-info/LICENSE,sha256=kqeyRXtRGgBVZdXYeIX4zR9l2KZ2rqIBVEiPMTjxjcI,1093
|
|
6
|
-
pybounds-0.0.1.dist-info/METADATA,sha256=Il4j0WbOUWga2nq69YD-fjh3OtElrZ-XA4TBSKMUUMs,539
|
|
7
|
-
pybounds-0.0.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
8
|
-
pybounds-0.0.1.dist-info/top_level.txt,sha256=V-ofnWE3m_UkXTXJwNRD07n14m5R6sc6l4NadaCCP_A,9
|
|
9
|
-
pybounds-0.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|