Metadata-Version: 2.4
Name: fast-kalman
Version: 0.2.2
Summary: Fixed-size C++23 Kalman filters with nanobind Python bindings
Keywords: kalman,filter,tracking,estimation,nanobind,cpp
Author: Kalman
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: C++
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.9
Requires-Dist: numpy>=1.24
Provides-Extra: test
Requires-Dist: filterpy>=1.4.5; extra == "test"
Requires-Dist: numpy>=1.24; extra == "test"
Requires-Dist: pytest>=8; extra == "test"
Provides-Extra: benchmark
Requires-Dist: numpy>=1.24; extra == "benchmark"
Requires-Dist: opencv-python-headless>=4.8; extra == "benchmark"
Description-Content-Type: text/markdown

# fast-kalman

`fast-kalman` is a fixed-size C++23 Kalman filtering library with nanobind
Python bindings.

A Kalman filter estimates hidden state from noisy measurements by combining a
model prediction with observed data. It is commonly used for tracking, sensor
fusion, smoothing financial indicators, and any small state-space model where
you need a fast estimate plus uncertainty. This library implements several
Kalman filter variants for tiny dimensions, with explicit `P`, `Q`, and `R`
covariance matrices and no dependency on a matrix library.

The focus is speed for small fixed-size problems. In the included `nx=2,nz=1`
benchmark on this machine:

- the direct C++ `kalman::LinearKalmanFilter` is about **110x faster** than
  OpenCV's native `cv::KalmanFilter`
- the Python `kalman.LinearKalmanFilter` binding is about **3.3x faster** than
  OpenCV's C++-backed `cv2.KalmanFilter`
- the Python `update_many(...)` path runs linear updates at about **240 ns** per
  measurement by keeping the loop in C++

## Install

### Python

Install from PyPI:

```sh
python -m pip install fast-kalman
```

Then import the package as `kalman`.

Build and install from this source tree:

```sh
python -m pip install .
```

For development and tests:

```sh
python -m pip install -e ".[test,benchmark]"
pytest tests
```

Build a wheel without asking pip to resolve already-installed runtime
dependencies:

```sh
python -m pip wheel . --no-build-isolation --no-deps -w dist
```

The Python package is built with `scikit-build-core` and `nanobind`. Runtime
dimensions are accepted from 1 through 8 for Python constructors.

### Release Builds

Build and upload the source distribution without Docker:

```sh
rm -rf dist
python -m build --sdist
python -m twine check dist/*
python -m twine upload dist/fast_kalman-*.tar.gz
```

Build public binary wheels with `cibuildwheel`. On Linux, `cibuildwheel` runs
manylinux/musllinux builds in containers, so Docker or Podman must be installed
and running:

```sh
docker --version
python -m cibuildwheel --platform linux --output-dir wheelhouse
python -m twine check wheelhouse/*
python -m twine upload wheelhouse/*
```

With Podman:

```sh
CIBW_CONTAINER_ENGINE=podman python -m cibuildwheel --platform linux --output-dir wheelhouse
```

Only upload `wheelhouse/*` after `cibuildwheel` succeeds and the directory
contains `.whl` files. A local `linux_x86_64` wheel from `python -m build` is
useful for testing, but manylinux/musllinux wheels from `cibuildwheel` are the
right Linux binary artifacts for PyPI.

### C++

Build and install as a CMake package:

```sh
cmake -S . -B build/cpp -DCMAKE_BUILD_TYPE=Release
cmake --build build/cpp
cmake --install build/cpp --prefix /path/to/prefix
```

Consume from another CMake project:

```cmake
find_package(kalman REQUIRED)
target_link_libraries(my_target PRIVATE kalman::kalman)
```

The C++ API is template-based:

```cpp
#include <kalman/kalman.hpp>

auto kf = kalman::make_constant_velocity_1d(
    100.0, 0.0, 1.0,
    10.0, 10.0,
    1e-4, 1e-3,
    0.25);

auto stats = kf.update(kalman::Vec<1>{100.5});
```

Nonlinear C++ model functions are template callables. Pass lambdas or functor
objects; the core API does not use function pointers or `std::function` for
transition, measurement, Jacobian, innovation, or retract callbacks.

## Python Quick Start

```python
import kalman

kf = kalman.LinearKalmanFilter(2, 1)
kf.x = [100.0, 0.0]
kf.P = [10.0, 0.0, 0.0, 10.0]
kf.F = [1.0, 1.0, 0.0, 1.0]
kf.Q = [1e-4, 0.0, 0.0, 1e-3]
kf.H = [1.0, 0.0]
kf.R = [0.25]

stats = kf.update([100.5])
batch = kf.update_many([100.5, 101.0, 101.5, 102.0])
```

Compiled nonlinear model example:

```python
rb = kalman.RangeBearingEKF2D()
rb.dt = 1.0
rb.landmark_x = 0.0
rb.landmark_y = 0.0
rb.x = [1.0, 1.0, 0.1, 0.0]
rb.P = [1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0]
rb.Q = [1e-3, 0.0, 0.0, 0.0,
        0.0, 1e-3, 0.0, 0.0,
        0.0, 0.0, 1e-3, 0.0,
        0.0, 0.0, 0.0, 1e-3]
rb.R = [0.1, 0.0, 0.0, 0.1]

stats = rb.update_many([[1.5, 0.8], [1.6, 0.75]])
```

## Filter Catalog

All matrices are row-major flat arrays in Python. In C++, vectors and matrices
are `kalman::Vec<N>` and `kalman::Mat<R, C>` backed by `std::array<double, N>`.

Shared defaults:

- `kDefaultEps = 1e-15`
- `kDefaultJitter = 1e-12`
- `kDefaultVarianceFloor = 1e-15`

Update methods return `UpdateStats<NX, NZ>` in C++ and a dictionary in Python:

- `innovation`: residual vector
- `S`: innovation covariance
- `K`: Kalman gain
- `nis`: normalized innovation squared
- `ok`: whether the update succeeded numerically

### `LinearKalmanFilter<NX, NZ>`

Standard linear covariance-form Kalman filter.

State and options:

- `x`: state vector, length `NX`
- `P`: state covariance, `NX x NX`
- `F`: transition matrix, `NX x NX`
- `Q`: process noise covariance, `NX x NX`
- `H`: measurement matrix, `NZ x NX`
- `R`: measurement noise covariance, `NZ x NZ`
- `last`: last `UpdateStats`

Methods:

- `predict()`
- C++ only: `predict(B, u)` for control input
- `correct(z, eps=kDefaultEps)`
- `update(z, eps=kDefaultEps)`
- `correct_many(measurements, return_stats=False, eps=kDefaultEps)`
- `update_many(measurements, return_stats=False, eps=kDefaultEps)`
- `correct_scalar(z, h, r, eps=kDefaultEps)` when `NZ == 1`

Convenience constructors:

- `make_scalar_random_walk(initial_value, initial_variance, process_variance, measurement_variance)`
- `make_constant_velocity_1d(initial_position, initial_velocity, dt, position_variance, velocity_variance, process_position_variance, process_velocity_variance, measurement_variance)`
- `make_constant_acceleration_1d(initial_position, initial_velocity, initial_acceleration, dt, position_variance, velocity_variance, acceleration_variance, process_position_variance, process_velocity_variance, process_acceleration_variance, measurement_variance)`

Automatic tuning:

- `tools/tune_linear_kalman.py` estimates `x`, `P`, `F`, `Q`, `H`, and `R` from
  a numeric text stream.
- Python: `LinearKalmanTuner(nx, nz, dt=1.0, model="auto", min_variance=1e-12, forgetting=0.75)`
  keeps state across batches and returns a `LinearKalmanTuning` namedtuple
  ordered for `LinearKalmanFilter(*params)`.

### `ExtendedKalmanFilter<NX, NZ>`

Extended Kalman filter for nonlinear transition and measurement models with
caller-supplied Jacobians.

State and options:

- `x`, `P`, `Q`, `R`, `last`

Methods:

- `predict(transition, transition_jacobian)`
- `correct(z, measurement, measurement_jacobian, eps=kDefaultEps)`
- Python: `update_many(measurements, transition, transition_jacobian, measurement, measurement_jacobian, return_stats=False, eps=kDefaultEps)`

Python callbacks receive NumPy arrays and may return NumPy arrays or flat
sequences. C++ callables are template parameters and can be inlined.

Automatic tuning:

- Python: `ExtendedKalmanTuner(nx, nz, dt=1.0, model="auto", min_variance=1e-12, forgetting=0.75)`
  estimates initial Euclidean `x`, `P`, `Q`, and `R` and returns an
  `ExtendedKalmanTuning` namedtuple ordered for `ExtendedKalmanFilter(*params)`.
  The nonlinear transition and measurement callbacks remain caller-supplied.

### `IteratedExtendedKalmanFilter<NX, NZ>`

Iterated EKF correction for measurement models that benefit from repeated
linearization.

State and options:

- `x`, `P`, `Q`, `R`, `last`

Methods:

- `predict(transition, transition_jacobian)`
- `correct(z, measurement, measurement_jacobian, max_iterations=5, tolerance=1e-10, eps=kDefaultEps)`
- Python: `update_many(measurements, transition, transition_jacobian, measurement, measurement_jacobian, max_iterations=5, tolerance=1e-10, return_stats=False, eps=kDefaultEps)`

Automatic tuning:

- Python: `IteratedExtendedKalmanTuner(nx, nz, dt=1.0, model="auto", min_variance=1e-12, forgetting=0.75)`
  returns `IteratedExtendedKalmanTuning` ordered for
  `IteratedExtendedKalmanFilter(*params)`. Tune `max_iterations` and
  `tolerance` separately for accuracy/latency.

### `UnscentedKalmanFilter<NX, NZ>`

Unscented Kalman filter using Cholesky-generated sigma points.

State and options:

- `x`, `P`, `Q`, `R`, `last`
- `alpha`, `beta`, `kappa` through `UnscentedParameters`
- defaults: `alpha = 1e-3`, `beta = 2.0`, `kappa = 0.0`
- C++ helpers: `lambda()`, `scale()`, `wm(i)`, `wc(i)`, `sigma_points(...)`

Methods:

- `predict(transition, jitter=kDefaultJitter)`
- `correct(z, measurement, jitter=kDefaultJitter, eps=kDefaultEps)`
- Python: `update_many(measurements, transition, measurement, return_stats=False, jitter=kDefaultJitter, eps=kDefaultEps)`
- Python batch sigma-point API:
  - `predict_batch(transition_batch, jitter=kDefaultJitter)`
  - `correct_batch(z, measurement_batch, jitter=kDefaultJitter, eps=kDefaultEps)`
  - `update_many_batch(measurements, transition_batch, measurement_batch, return_stats=False, jitter=kDefaultJitter, eps=kDefaultEps)`

`transition_batch` receives a `(2 * NX + 1, NX)` NumPy array and returns the
propagated sigma points with the same shape. `measurement_batch` receives a
`(2 * NX + 1, NX)` array and returns `(2 * NX + 1, NZ)`.

Automatic tuning:

- Python: `UnscentedKalmanTuner(nx, nz, alpha=1e-3, beta=2.0, kappa=0.0, dt=1.0, model="auto", min_variance=1e-12, forgetting=0.75)`
  returns `UnscentedKalmanTuning` ordered for `UnscentedKalmanFilter(*params)`.
  Validate `alpha`, `beta`, and `kappa` against innovation behavior and
  held-out data.

### `InvariantExtendedKalmanFilter<ErrorN, NZ, StateT>`

C++ invariant-error EKF shell for non-Euclidean state spaces.

State and options:

- `state`: user-supplied state type
- `P`, `Q`, `R`, `last`

Methods:

- `predict(propagate, error_transition)`
- `correct(z, measurement, error_jacobian, innovation, retract, eps=kDefaultEps)`

This intentionally requires caller-supplied propagation, error transition,
innovation, and retract operations. There is no fake universal IEKF for all
state spaces.

Automatic tuning:

- No general automatic tuner. Tune the error-state `Q` and measurement `R` for
  the chosen manifold and sensor model.

### `EnsembleKalmanFilter<NX, NZ, EnsembleSize>`

Ensemble Kalman filter. The current Python binding exposes `EnsembleSize == 8`;
C++ supports any compile-time ensemble size of at least 2.

State and options:

- `members`: ensemble members
- `x`: ensemble mean
- `P`: ensemble covariance
- `R`: measurement noise covariance
- `last`

Methods:

- `recompute_mean_covariance()`
- `predict(transition)`
- C++: `predict(transition, process_noise)`
- C++: `correct(z, measurement, observation_noise, eps=kDefaultEps)`
- `correct_deterministic(z, measurement, eps=kDefaultEps)`
- Python: `update_many(measurements, transition, measurement, return_stats=False, eps=kDefaultEps)`
- Python batch member API:
  - `predict_batch(transition_batch)`
  - `correct_deterministic_batch(z, measurement_batch, eps=kDefaultEps)`
  - `update_many_batch(measurements, transition_batch, measurement_batch, return_stats=False, eps=kDefaultEps)`

`transition_batch` receives an `(8, NX)` NumPy array and returns `(8, NX)`.
`measurement_batch` receives `(8, NX)` and returns `(8, NZ)`.

Automatic tuning:

- Python: `EnsembleKalmanTuner(nx, nz, ensemble_size=8, dt=1.0, model="auto", min_variance=1e-12, forgetting=0.75)`
  estimates member initialization and `R`, returning `EnsembleKalmanTuning`
  ordered for `EnsembleKalmanFilter(*params)`.

### `AdaptiveKalmanFilter<NX, NZ>`

Adaptive wrapper around `LinearKalmanFilter` that can update `R` and optionally
`Q` from innovation statistics.

State and options:

- `kf`: underlying linear filter in C++
- Python exposes `x`, `P`, `F`, `Q`, `H`, `R`
- `forgetting`: exponential forgetting factor, default `0.98`
- `min_variance`: variance floor, default `1e-12`
- `adapt_R`: adapt measurement noise, default `true`
- `diagonal_R_only`: keep only diagonal `R`, default `true`
- `adapt_Q_from_state_correction`: heuristic `Q` adaptation, default `false`
- `innovation_covariance_ema`, `initialized`

Methods:

- `predict()`
- `correct(z, eps=kDefaultEps)`
- `update(z, eps=kDefaultEps)`
- `correct_many(measurements, return_stats=False, eps=kDefaultEps)`
- `update_many(measurements, return_stats=False, eps=kDefaultEps)`

Automatic tuning:

- This filter self-tunes online. Use `LinearKalmanTuner` or
  `tools/tune_linear_kalman.py` for initial `F`, `H`, `P`, `Q`, and `R`, then
  let the adaptive filter refine `R` online if the innovation statistics are
  stable.

### `RangeBearingEKF2D`

Python-facing compiled nonlinear EKF model for high-throughput range/bearing
tracking without Python model callbacks.

Model:

- state: `[x, y, vx, vy]`
- measurement: `[range, bearing]`
- constant-velocity prediction
- configurable landmark at `(landmark_x, landmark_y)`

State and options:

- `dt`
- `landmark_x`, `landmark_y`
- `x`, `P`, `Q`, `R`, `last`

Methods:

- `predict()`
- `measurement(x)`
- `measurement_jacobian(x)`
- `correct(z, eps=kDefaultEps)`
- `update(z, eps=kDefaultEps)`
- `update_many(measurements, return_stats=False, eps=kDefaultEps)`

Automatic tuning:

- Python: `RangeBearingEKF2DTuner(nx=4, nz=2, dt=1.0, landmark_x=0.0, landmark_y=0.0, min_variance=1e-12, forgetting=0.75)`
  estimates `x`, `P`, `Q`, and `R` from `[range, bearing]` batches and returns
  `RangeBearingEKF2DTuning` ordered for `RangeBearingEKF2D(*params)`.

## Tuning From Data

`tools/tune_linear_kalman.py` estimates starting linear-filter parameters from a
numeric text stream:

```sh
tools/tune_linear_kalman.py prices.txt --nx 2 --nz 1 --dt 1.0
tools/tune_linear_kalman.py samples.txt --nx 4 --nz 2 --format json
```

The script accepts commas, spaces, or newlines. Values are grouped by `--nz`.
With `--model auto`, it chooses these layouts:

- `nx == nz`: random walk
- `nx == 2 * nz`: constant velocity
- `nx == 3 * nz`: constant acceleration
- otherwise: generic identity transition

It prints row-major `F`, `H`, `P`, `Q`, and `R`, plus Python setup and C++
convenience-constructor arguments when applicable. Treat the output as initial
tuning values; validate against held-out data and inspect innovations or NIS
before shipping.

Python also exposes stateful tuning classes for library use. Each `update(...)`
call takes one independent batch of observations, such as one day's samples.
The tuner refines its internal estimate across calls, but it does not assume
that the last sample from one batch is continuous with the first sample from
the next batch.

```python
import kalman

tuner = kalman.LinearKalmanTuner(2, 1, dt=1.0)
params = tuner.update(day_1_prices)
params = tuner.update(day_2_prices)
kf = kalman.LinearKalmanFilter(*params)
```

Available tuners:

- `LinearKalmanTuner` -> `LinearKalmanTuning(nx, nz, x, P, F, Q, H, R)`
- `ExtendedKalmanTuner` -> `ExtendedKalmanTuning(nx, nz, x, P, Q, R)`
- `IteratedExtendedKalmanTuner` -> `IteratedExtendedKalmanTuning(nx, nz, x, P, Q, R)`
- `UnscentedKalmanTuner` -> `UnscentedKalmanTuning(nx, nz, x, P, Q, R, alpha, beta, kappa)`
- `EnsembleKalmanTuner` -> `EnsembleKalmanTuning(nx, nz, ensemble_size, members, R)`
- `RangeBearingEKF2DTuner` -> `RangeBearingEKF2DTuning(dt, landmark_x, landmark_y, x, P, Q, R)`

There is no `AdaptiveKalmanTuner` because `AdaptiveKalmanFilter` is the
self-tuning variant.

## Benchmarks

Run both benchmark layers:

```sh
python -m pip install -e ".[benchmark]"
benchmarks/benchmark_python_filters.py --nx 2 --nz 1
benchmarks/benchmark_cpp_core.py --nx 2 --nz 1
```

`benchmark_python_filters.py` measures the nanobind API, including Python
callback overhead, `update_many(...)`, UKF sigma-point batch callbacks, EnKF
member batch callbacks, and compiled model wrappers. When `cv2` is installed, it
compares the matching linear predict/correct workload against OpenCV's
C++-backed `cv2.KalmanFilter`.

`benchmark_cpp_core.py` generates and compiles a small C++ benchmark binary. It
measures the direct template API with inlinable lambdas/functors and covers the
C++-only invariant EKF. When OpenCV development files are available through
`pkg-config opencv4`, it also benchmarks native `cv::KalmanFilter`.

Latest local `nx=2,nz=1` run:

### Python layer

| Workload | Implementation | ns/update |
|---|---:|---:|
| linear update | `kalman.LinearKalmanFilter` | 987 |
| linear update | `cv2.KalmanFilter` | 3231 |
| linear `update_many` | `kalman.LinearKalmanFilter` | 240 |
| adaptive update | `kalman.AdaptiveKalmanFilter` | 1090 |
| adaptive `update_many` | `kalman.AdaptiveKalmanFilter` | 350 |
| EKF NumPy callbacks | `kalman.ExtendedKalmanFilter` | 7503 |
| EKF `update_many` | `kalman.ExtendedKalmanFilter` | 6056 |
| iterated EKF callbacks | `kalman.IteratedExtendedKalmanFilter` | 14550 |
| iterated EKF `update_many` | `kalman.IteratedExtendedKalmanFilter` | 13436 |
| UKF per-sigma callbacks | `kalman.UnscentedKalmanFilter` | 21861 |
| UKF batched sigma callbacks | `kalman.UnscentedKalmanFilter` | 7203 |
| UKF `update_many_batch` | `kalman.UnscentedKalmanFilter` | 5867 |
| EnKF per-member callbacks | `kalman.EnsembleKalmanFilter` | 34177 |
| EnKF batched member callbacks | `kalman.EnsembleKalmanFilter` | 7513 |
| EnKF `update_many_batch` | `kalman.EnsembleKalmanFilter` | 6719 |
| compiled range/bearing EKF `update_many` | `kalman.RangeBearingEKF2D` | 570 |

### C++ layer

| Workload | Implementation | ns/update |
|---|---:|---:|
| linear update | `kalman::LinearKalmanFilter` | 29.7 |
| linear update | `cv::KalmanFilter` | 3265 |
| scalar hot path | `kalman::LinearKalmanFilter` | 37.4 |
| EKF | `kalman::ExtendedKalmanFilter` | 44.1 |
| iterated EKF | `kalman::IteratedExtendedKalmanFilter` | 187.9 |
| UKF | `kalman::UnscentedKalmanFilter` | 296.5 |
| invariant EKF | `kalman::InvariantExtendedKalmanFilter` | 64.5 |
| EnKF, size 8 | `kalman::EnsembleKalmanFilter` | 137.9 |
| adaptive update | `kalman::AdaptiveKalmanFilter` | 119.3 |

Benchmark context matters. These numbers are for tiny fixed dimensions, release
builds, and local hardware. Run the scripts on your target machine before
making production latency claims.

## Reference Tests

Run Python reference tests against FilterPy:

```sh
python -m pip install -e ".[test]"
pytest tests
```

FilterPy is used as the comparison implementation because it is a widely used
pure-Python/NumPy Kalman filtering package with explicit matrix state, predict,
and update operations that map directly to this library's linear filter API.

## License

`fast-kalman` is licensed under the Apache License, Version 2.0. See
`LICENSE` for the full text.

## Numerical Notes

This is a covariance-form implementation using Joseph-form covariance updates.
It has hard-coded inverse paths for `1x1`, `2x2`, and `3x3`; larger dimensions
use a small fixed-size Gauss-Jordan inverse path. UKF sigma points use Cholesky
with jitter.

For poorly conditioned models or maximum numerical stability, square-root
filter variants are the likely next upgrade.
