Skip to main content

pybounds

Python implementation of BOUNDS: Bounding Observability for Uncertain Nonlinear Dynamic Systems.

PyPI version Tests Coverage Docs

Introduction

This repository provides python code to empirically calculate the observability level of individual states for a nonlinear (partially observable) system, and accounts for sensor noise. Below is a graphical example of how pybounds can discover active sensing motifs. Minimal working examples are described below.

Installing

The package can be installed from PyPi:

pip install pybounds

or from source, for development, after cloning the repo:

pip install -e .

Quick Start

To demonstrate pybounds with a simple example we use a downward-pointing camera moving horizontally with acceleration that is controlled directly with control inputs (u). The two states are ground speed g and (constant) altitude d, and the only measurement is the ventral optic flow ratio r = g/d. We use pybounds to understand when g and d are observable.

See notebooks in next section for more detailed usage examples.

import numpy as np
import matplotlib.pyplot as plt
import pybounds

# 1. Define continuous time system dynamics f(X, U) and measurement h(X, U)
def f(X, U):         # states: gap g, distance d — input u drives g
    return [U[0], 0] # returns: d/dt(g), d/dt(d) 

def h(X, U):        # monocular camera measures the g/d ratio
    return [X[0] / X[1]]

# 2. Simulate a trajectory
sim = pybounds.Simulator(f, h, dt=0.01,
                         state_names=['g', 'd'], input_names=['u'],
                         measurement_names=['r'])
t, x, u, _ = sim.simulate(x0={'g': 2.0, 'd': 3.0},
                           u={'u': 0.1 * np.ones(500)},
                           return_full_output=True)

# 3. Set up the observability analysis (nothing is computed yet), then run it
oa = pybounds.ObservabilityAnalysis(sim, t, x, u, w=6, R={'r': 0.1}, lam=1e-8)
oa.run()

# 4. Plot minimum error variance over time for each state
ev = oa.min_error_variance()
ev.set_index('time')[['g', 'd']].plot(logy=True, ylabel='Min. error variance')
plt.show()
  • Window: w is the sliding-window length in time-steps. Without it, the whole trajectory is analyzed as one window.
  • Noise: R is the measurement noise variance, per sensor.
  • Regularization lam (λ): the Fisher information matrix F is inverted as (F + λI)⁻¹. 1e-8 is also the default. 1/λ is the ceiling on the minimum error variance: a state whose error variance sits near 1/λ (1e8 by default) is unobservable, not merely poorly estimated. λ is an absolute value, so it should be small compared to the eigenvalues of F, which depend on the sensor noise R and on the units of each state.
  • One-call shortcut: pybounds.compute_observability(sim, t, x, u, R={'r': 0.1}, w=6, lam=1e-8) returns the same result as steps 3 and 4 in a single call, without keeping the analysis.

Selecting states, and saving settings and results

oa keeps the observability matrices from run(), so you can ask about different selections without recomputing them:

# Drop a state: treat d as known and ask how well g alone can be estimated
ev_g = oa.min_error_variance(states=['g'])

# Other selections and parameters work the same way
ev_short = oa.min_error_variance(time_steps=[0, 1, 2])   # only the first 3 steps of each window
ev_noisy = oa.min_error_variance(R={'r': 1.0})           # a different noise level

# Save every setting to YAML, and load it into another analysis later
oa.save_settings('observability_settings.yaml')
oa2 = pybounds.ObservabilityAnalysis(sim, t, x, u).load_settings('observability_settings.yaml')

# Save results for a selection into a directory: min_error_variance.csv, a YAML sidecar
# (selection, full state/sensor lists, settings) and, optionally, all observability matrices (.npz)
oa.save_results('results_g', states=['g'], include_observability_matrices=True)
  • Dropping a state is conditional: the states you leave out are treated as known, so the remaining ones usually look more observable than when every state is estimated together.
  • Changing settings: update_settings(...) changes settings before or after run(). Changing anything that affects the observability matrices (e.g. w, eps, z_function) discards the results until you call run() again; changing R or lam does not.
  • Backends: method picks how the observability matrices are built: 'empirical' (finite differences) or 'jax' (autodiff). It is chosen automatically from the simulator type.
  • Memory: run() keeps every window's observability matrix (8·n_windows·w·p·n bytes). For long windows, storage='fisher_per_sensor' keeps each sensor's Fisher information instead, which is smaller when w > (n+1)/2 and still supports selecting states and sensors with a scalar or per-sensor R. With method='jax', batch_size=... computes windows in chunks to cap JAX's memory. See the storage design note.

Notebook examples

Basic Examples

These notebooks provide a more detailed example of pybounds functionality including:

  • How to use model predictive control to drive systems along specified trajectories
  • Demonstration of what happens inside the pybounds.compute_observability wrapper function, allowing for detailed investigations of the observability calculations

Examples using pybounds with continuous time dynamics, see these notebook examples:

JAX Accelerated Examples

pybounds includes a JAX backend (JaxSimulator, JaxSlidingEmpiricalObservabilityMatrix) that replaces the numerical finite-difference Jacobian with exact autodiff via jax.vmap + jax.jacfwd. The simulation and all downstream analysis (Fisher information, plotting) are unchanged.

When JAX helps most: the speedup scales with the number of sliding windows. Short trajectories with few windows see modest gains; long trajectories benefit dramatically.

System States Windows Legacy JAX (hot) Speedup
Mono-camera 2 895 ~21 s ~1.1 s ~19×
Fly-wind 18 37 ~6 s ~2.6 s ~2.4×

To use the JAX backend, install JAX and rewrite your dynamics f and measurement h using jax.numpy instead of numpy. See the notebooks below for worked examples.

Using a Custom Simulator

This has received the least development, however, a working tutorial can be found here.

Citation

If you use the code or methods from this package, please cite the following paper:

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

Additional resources

To learn more about nonlinear observability, its relation to Fisher information, see Boyacioglu and van Breugel

To start with the basics, check out these open source course materials: Nonlinear and Data Driven Estimation.

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.

License

This project utilizes the MIT LICENSE. 100% open-source, feel free to utilize the code however you like.

Metadata

Release files for pybounds 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pybounds 0.2.0
File Size Uploaded
pybounds-0.2.0.tar.gz 95.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pybounds 0.2.0
File Interpreter ABI Platform
pybounds-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 148.1 kB

Release files / pybounds-0.2.0.tar.gz

Download URL pybounds-0.2.0.tar.gz
Size 95.3 kB
Tags Source
SHA-256 checksum
How to use checksums
924b41ae24ef9e5f70efac0e9a56b80dee5a73ae6f6d3c2ce94e6ed5a6a6c561
BLAKE2b-256 checksum
How to use checksums
5b18f689114d3a2fb1de74c608ea88fa655b9b59ca963e6f8dc415f1c1f3ae4d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / pybounds-0.2.0-py3-none-any.whl

Download URL pybounds-0.2.0-py3-none-any.whl
Size 52.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cf8752fedc00e10f00cad151e8dd96d2aa21913c2bad6c3c8041986b6f12b59e
BLAKE2b-256 checksum
How to use checksums
1f3ed2ac7c3b7b359fc7c44fa4034263bd9d5a834f62e70b95f16817581f331a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

0.3.1

2 release files

0.3.0

2 release files

This release

0.2.0 This release

2 release files

0.1.0

2 release files

0.0.14

2 release files

0.0.13

2 release files

0.0.12

2 release files

0.0.11

2 release files

0.0.10

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page