pybounds 0.0.14__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.
- pybounds/__init__.py +13 -0
- pybounds/jacobian.py +46 -0
- pybounds/observability.py +764 -0
- pybounds/simulator.py +477 -0
- pybounds/util.py +279 -0
- pybounds-0.0.14.dist-info/METADATA +65 -0
- pybounds-0.0.14.dist-info/RECORD +10 -0
- pybounds-0.0.14.dist-info/WHEEL +5 -0
- pybounds-0.0.14.dist-info/licenses/LICENSE +21 -0
- pybounds-0.0.14.dist-info/top_level.txt +1 -0
pybounds/util.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy
|
|
3
|
+
import matplotlib as mpl
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import matplotlib.collections as mcoll
|
|
6
|
+
import matplotlib.patheffects as path_effects
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FixedKeysDict(dict):
|
|
10
|
+
def __init__(self, *args, **kwargs):
|
|
11
|
+
super(FixedKeysDict, self).__init__(*args, **kwargs)
|
|
12
|
+
self._frozen_keys = set(self.keys()) # Capture initial keys
|
|
13
|
+
|
|
14
|
+
def __setitem__(self, key, value):
|
|
15
|
+
if key not in self._frozen_keys:
|
|
16
|
+
raise KeyError(f"Key '{key}' cannot be added.")
|
|
17
|
+
super(FixedKeysDict, self).__setitem__(key, value)
|
|
18
|
+
|
|
19
|
+
def __delitem__(self, key):
|
|
20
|
+
raise KeyError(f"Key '{key}' cannot be deleted.")
|
|
21
|
+
|
|
22
|
+
def pop(self, key, default=None):
|
|
23
|
+
raise KeyError(f"Key '{key}' cannot be popped.")
|
|
24
|
+
|
|
25
|
+
def popitem(self):
|
|
26
|
+
raise KeyError("Cannot pop item from FixedKeysDict.")
|
|
27
|
+
|
|
28
|
+
def clear(self):
|
|
29
|
+
raise KeyError("Cannot clear FixedKeysDict.")
|
|
30
|
+
|
|
31
|
+
def update(self, *args, **kwargs):
|
|
32
|
+
for key in dict(*args, **kwargs):
|
|
33
|
+
if key not in self._frozen_keys:
|
|
34
|
+
raise KeyError(f"Key '{key}' cannot be added.")
|
|
35
|
+
super(FixedKeysDict, self).update(*args, **kwargs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SetDict(object):
|
|
39
|
+
# set_dict(self, dTarget, dSource, bPreserve)
|
|
40
|
+
# Takes a target dictionary, and enters values from the source dictionary, overwriting or not, as asked.
|
|
41
|
+
# For example,
|
|
42
|
+
# dT={'a':1, 'b':2}
|
|
43
|
+
# dS={'a':0, 'c':0}
|
|
44
|
+
# Set(dT, dS, True)
|
|
45
|
+
# dT is {'a':1, 'b':2, 'c':0}
|
|
46
|
+
#
|
|
47
|
+
# dT={'a':1, 'b':2}
|
|
48
|
+
# dS={'a':0, 'c':0}
|
|
49
|
+
# Set(dT, dS, False)
|
|
50
|
+
# dT is {'a':0, 'b':2, 'c':0}
|
|
51
|
+
#
|
|
52
|
+
def set_dict(self, dTarget, dSource, bPreserve):
|
|
53
|
+
for k, v in dSource.items():
|
|
54
|
+
bKeyExists = (k in dTarget)
|
|
55
|
+
if (not bKeyExists) and type(v) == type({}):
|
|
56
|
+
dTarget[k] = {}
|
|
57
|
+
if ((not bKeyExists) or not bPreserve) and (type(v) != type({})):
|
|
58
|
+
dTarget[k] = v
|
|
59
|
+
|
|
60
|
+
if type(v) == type({}):
|
|
61
|
+
self.set_dict(dTarget[k], v, bPreserve)
|
|
62
|
+
|
|
63
|
+
def set_dict_with_preserve(self, dTarget, dSource):
|
|
64
|
+
self.set_dict(dTarget, dSource, True)
|
|
65
|
+
|
|
66
|
+
def set_dict_with_overwrite(self, dTarget, dSource):
|
|
67
|
+
self.set_dict(dTarget, dSource, False)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LatexStates:
|
|
71
|
+
""" Holds LaTex format corresponding to set symbolic variables.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, dict=None):
|
|
75
|
+
self.dict = {'v_para': r'$v_{\parallel}$',
|
|
76
|
+
'v_perp': r'$v_{\perp}$',
|
|
77
|
+
'phi': r'$\phi$',
|
|
78
|
+
'phidot': r'$\dot{\phi}$',
|
|
79
|
+
'phi_dot': r'$\dot{\phi}$',
|
|
80
|
+
'phiddot': r'$\ddot{\phi}$',
|
|
81
|
+
'w': r'$w$',
|
|
82
|
+
'zeta': r'$\zeta$',
|
|
83
|
+
'I': r'$I$',
|
|
84
|
+
'm': r'$m$',
|
|
85
|
+
'C_para': r'$C_{\parallel}$',
|
|
86
|
+
'C_perp': r'$C_{\perp}$',
|
|
87
|
+
'C_phi': r'$C_{\phi}$',
|
|
88
|
+
'km1': r'$k_{m_1}$',
|
|
89
|
+
'km2': r'$k_{m_2}$',
|
|
90
|
+
'km3': r'$k_{m_3}$',
|
|
91
|
+
'km4': r'$k_{m_4}$',
|
|
92
|
+
'd': r'$d$',
|
|
93
|
+
'psi': r'$\psi$',
|
|
94
|
+
'gamma': r'$\gamma$',
|
|
95
|
+
'alpha': r'$\alpha$',
|
|
96
|
+
'of': r'$\frac{g}{d}$',
|
|
97
|
+
'gdot': r'$\dot{g}$',
|
|
98
|
+
'v_para_dot': r'$\dot{v_{\parallel}}$',
|
|
99
|
+
'v_perp_dot': r'$\dot{v_{\perp}}$',
|
|
100
|
+
'v_para_dot_ratio': r'$\frac{\Delta v_{\parallel}}{v_{\parallel}}$',
|
|
101
|
+
'x': r'$x$',
|
|
102
|
+
'y': r'$y$',
|
|
103
|
+
'v_x': r'$v_{x}$',
|
|
104
|
+
'v_y': r'$v_{y}$',
|
|
105
|
+
'v_z': r'$v_{z}$',
|
|
106
|
+
'w_x': r'$w_{x}$',
|
|
107
|
+
'w_y': r'$w_{y}$',
|
|
108
|
+
'w_z': r'$w_{z}$',
|
|
109
|
+
'a_x': r'$a_{x}$',
|
|
110
|
+
'a_y': r'$a_{y}$',
|
|
111
|
+
'vx': r'$v_x$',
|
|
112
|
+
'vy': r'$v_y$',
|
|
113
|
+
'vz': r'$v_z$',
|
|
114
|
+
'wx': r'$w_x$',
|
|
115
|
+
'wy': r'$w_y$',
|
|
116
|
+
'wz': r'$w_z$',
|
|
117
|
+
'ax': r'$ax$',
|
|
118
|
+
'ay': r'$ay$',
|
|
119
|
+
'beta': r'$\beta',
|
|
120
|
+
'thetadot': r'$\dot{\theta}$',
|
|
121
|
+
'theta_dot': r'$\dot{\theta}$',
|
|
122
|
+
'psidot': r'$\dot{\psi}$',
|
|
123
|
+
'psi_dot': r'$\dot{\psi}$',
|
|
124
|
+
'theta': r'$\theta$',
|
|
125
|
+
'Yaw': r'$\psi$',
|
|
126
|
+
'R': r'$\phi$',
|
|
127
|
+
'P': r'$\theta$',
|
|
128
|
+
'dYaw': r'$\dot{\psi}$',
|
|
129
|
+
'dP': r'$\dot{\theta}$',
|
|
130
|
+
'dR': r'$\dot{\phi}$',
|
|
131
|
+
'acc_x': r'$\dot{v}x$',
|
|
132
|
+
'acc_y': r'$\dot{v}y$',
|
|
133
|
+
'acc_z': r'$\dot{v}z$',
|
|
134
|
+
'Psi': r'$\Psi$',
|
|
135
|
+
'Ix': r'$I_x$',
|
|
136
|
+
'Iy': r'$I_y$',
|
|
137
|
+
'Iz': r'$I_z$',
|
|
138
|
+
'Jr': r'$J_r$',
|
|
139
|
+
'Dl': r'$D_l$',
|
|
140
|
+
'Dr': r'$D_r$',
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if dict is not None:
|
|
144
|
+
SetDict().set_dict_with_overwrite(self.dict, dict)
|
|
145
|
+
|
|
146
|
+
def convert_to_latex(self, list_of_strings, remove_dollar_signs=False):
|
|
147
|
+
""" Loop through list of strings and if any match the dict, then swap in LaTex symbol.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
if isinstance(list_of_strings, str): # if single string is given instead of list
|
|
151
|
+
list_of_strings = [list_of_strings]
|
|
152
|
+
string_flag = True
|
|
153
|
+
else:
|
|
154
|
+
string_flag = False
|
|
155
|
+
|
|
156
|
+
list_of_strings = list_of_strings.copy()
|
|
157
|
+
for n, s in enumerate(list_of_strings): # each string in list
|
|
158
|
+
for k in self.dict.keys(): # check each key in Latex dict
|
|
159
|
+
if s == k: # string contains key
|
|
160
|
+
# print(s, ',', self.dict[k])
|
|
161
|
+
list_of_strings[n] = self.dict[k] # replace string with LaTex
|
|
162
|
+
if remove_dollar_signs:
|
|
163
|
+
list_of_strings[n] = list_of_strings[n].replace('$', '')
|
|
164
|
+
|
|
165
|
+
if string_flag:
|
|
166
|
+
list_of_strings = list_of_strings[0]
|
|
167
|
+
|
|
168
|
+
return list_of_strings
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def make_segments(x, y):
|
|
172
|
+
points = np.array([x, y]).T.reshape(-1, 1, 2)
|
|
173
|
+
segments = np.concatenate([points[:-1], points[1:]], axis=1)
|
|
174
|
+
return segments
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def colorline(x, y, z, ax=None, cmap=plt.get_cmap('copper'), norm=None, linewidth=1.5, alpha=1.0):
|
|
178
|
+
# Special case if a single number:
|
|
179
|
+
if not hasattr(z, "__iter__"): # to check for numerical input -- this is a hack
|
|
180
|
+
z = np.array([z])
|
|
181
|
+
|
|
182
|
+
z = np.asarray(z)
|
|
183
|
+
|
|
184
|
+
# Set normalization
|
|
185
|
+
if norm is None:
|
|
186
|
+
norm = plt.Normalize(np.min(z), np.max(z))
|
|
187
|
+
|
|
188
|
+
print(norm)
|
|
189
|
+
|
|
190
|
+
# Make segments
|
|
191
|
+
segments = make_segments(x, y)
|
|
192
|
+
lc = mcoll.LineCollection(segments, array=z, cmap=cmap, norm=norm,
|
|
193
|
+
linewidth=linewidth, alpha=alpha,
|
|
194
|
+
path_effects=[path_effects.Stroke(capstyle="round")])
|
|
195
|
+
|
|
196
|
+
# Plot
|
|
197
|
+
if ax is None:
|
|
198
|
+
ax = plt.gca()
|
|
199
|
+
|
|
200
|
+
ax.add_collection(lc)
|
|
201
|
+
|
|
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
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pybounds
|
|
3
|
+
Version: 0.0.14
|
|
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.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: author-email
|
|
16
|
+
Dynamic: classifier
|
|
17
|
+
Dynamic: description
|
|
18
|
+
Dynamic: description-content-type
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
Dynamic: summary
|
|
23
|
+
|
|
24
|
+
# pybounds
|
|
25
|
+
|
|
26
|
+
Python implementation of BOUNDS: Bounding Observability for Uncertain Nonlinear Dynamic Systems.
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<a href="https://pypi.org/project/pybounds/">
|
|
30
|
+
<img src="https://badge.fury.io/py/pybounds.svg" alt="PyPI version" height="18"></a>
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
## Introduction
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
## Installing
|
|
38
|
+
|
|
39
|
+
The package can be installed by cloning the repo and running python setup.py install from inside the home pybounds directory.
|
|
40
|
+
|
|
41
|
+
Alternatively using pip
|
|
42
|
+
```bash
|
|
43
|
+
pip install pybounds
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Notebook examples
|
|
47
|
+
For a simple system
|
|
48
|
+
* Monocular camera with optic fow measurements: [mono_camera_example.ipynb](examples%2Fmono_camera_example.ipynb)
|
|
49
|
+
|
|
50
|
+
For a more complex system
|
|
51
|
+
* Fly-wind: [fly_wind_example.ipynb](examples%2Ffly_wind_example.ipynb)
|
|
52
|
+
|
|
53
|
+
## Citation
|
|
54
|
+
|
|
55
|
+
If you use the code or methods from this package, please cite the following paper:
|
|
56
|
+
|
|
57
|
+
Cellini, B., Boyacioglu, B., Lopez, A., & van Breugel, F. (2025). Discovering and exploiting active sensing motifs for estimation (arXiv:2511.08766). arXiv. https://arxiv.org/abs/2511.08766
|
|
58
|
+
|
|
59
|
+
## Related packages
|
|
60
|
+
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.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
This project utilizes the [MIT LICENSE](LICENSE.txt).
|
|
65
|
+
100% open-source, feel free to utilize the code however you like.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pybounds/__init__.py,sha256=ehF8bSQK7jeH-Tg6DY2CR6DzxGtaoRGpCY0DW-V3JfY,449
|
|
2
|
+
pybounds/jacobian.py,sha256=HfQwwYv4lEYldWM97dfp9Q4y56ALcmOugN4mtjM-IjQ,2011
|
|
3
|
+
pybounds/observability.py,sha256=GUNBWdNft5vamschXPZNzp-Hoh6Yz-Y0Wxsl7Wm_RSE,33158
|
|
4
|
+
pybounds/simulator.py,sha256=NFztAB3IbrB4UCDEmcxUxJRBgl3q-Q8geqd7jymD9dI,17615
|
|
5
|
+
pybounds/util.py,sha256=lBeqU21qo-EngTyMZ4K_P9q_OD_PpWKVVVsFNu1Fvec,9858
|
|
6
|
+
pybounds-0.0.14.dist-info/licenses/LICENSE,sha256=AadU7OxPR9xChQ2FQ4aYpLKCe_7xh7Ox0L5LTGOtaC8,1072
|
|
7
|
+
pybounds-0.0.14.dist-info/METADATA,sha256=-3m34Wu5nYo1dyhQR7oIBbjtP20-cr76htVgpRNSNCU,2293
|
|
8
|
+
pybounds-0.0.14.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
+
pybounds-0.0.14.dist-info/top_level.txt,sha256=V-ofnWE3m_UkXTXJwNRD07n14m5R6sc6l4NadaCCP_A,9
|
|
10
|
+
pybounds-0.0.14.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 van Breugel lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pybounds
|