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/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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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