Skip to main content

kalman-py logo

kalman-py

Fast, exact and numerically robust Kalman filters for Python

CI Documentation Python 3.10+ MIT License Typed, mypy strict Linted with ruff NumPy and JAX backends Pre-release

Up to 93x faster than pykalman in batch mode Matches FilterPy and pykalman Automatic EKF Jacobians Square-root forms

Quick Start · Highlights · Benchmarks · Architecture · Roadmap · Development · Documentation


kalman-py is a typed Kalman filtering library with linear, extended and unscented filters, RTS smoothers, maximum-likelihood noise learning and consistency diagnostics. It runs on NumPy out of the box, and on an optional JAX backend that compiles the whole time loop with jax.lax.scan. On linear-Gaussian problems it produces the same estimates as FilterPy and pykalman (within 1e-11), so it can replace them without changing your results.

Time per filter step: kalman-py's JAX backend takes 0.41, 0.58 and 5.5 microseconds on scenarios S1, S2 and S5, against 38, 39 and 52 for pykalman; in per-step mode kalman-py takes 13.3, 13.4 and 22.6 microseconds against FilterPy's 10.3, 10.6 and 18.9.

📌 Project status

done Linear KF, EKF and UKF with RTS smoothers, each on the NumPy and JAX backends, step-by-step or batch. Joseph-form and square-root covariance updates.
done Q/R learning by EM or gradient-based maximum likelihood, and NIS/NEES consistency checks with plotting helpers.
done Reproducible comparison with FilterPy and pykalman on five frozen scenarios (S1–S5), shared with the C, C++ and Rust implementations.
done GitHub Actions on every push and pull request: Python 3.10–3.14 on Linux, macOS and Windows, with and without JAX, the minimum supported dependency versions, the docs, and a comparison report against FilterPy and pykalman.
done Documentation site (MkDocs Material) with guides, an API reference and a migration guide from FilterPy, plus three tutorial notebooks: target tracking, multi-rate sensor fusion and learning the noise, published at joslo2345.github.io/kalman-py.
next Release automation is ready: a GitHub release builds, checks and tests the package, then publishes it to PyPI with trusted publishing. The first release (0.1.0) is pending the PyPI-side setup. See the changelog.

✨ Highlights

⚡ Compiled JAX backend

The time loop runs in jax.lax.scan, compiled once and reused across filters. Small matrices use fused kernels instead of BLAS calls: 0.4 µs per step on a constant-velocity model.

Benchmarks →
🎯 Exact by construction

Same estimates as FilterPy (within 1e-14) and pykalman (within 1e-11) on linear problems; the batch steady-state shortcut is bitwise identical to the full computation.

Equivalence tests →
🧮 Automatic Jacobians

Write f and h once with jax.numpy: the EKF derives its Jacobians with jax.jacfwd on either backend, and the UKF needs none.

EKF / UKF example →
🛡️ Numerical robustness

Joseph-form updates with exact symmetry by default; square_root=True keeps every covariance positive-definite by construction, even in float32.

Stability results →
📈 Noise learning

fit_noise estimates Q and R by EM or by BFGS on the exact likelihood (autodiff through the filter), reaching a higher likelihood than pykalman's EM in less time.

Learning example →
🩺 Diagnostics built in

NIS/NEES consistency checks against χ² bounds (no SciPy needed) over Monte Carlo runs, and plots of estimates with uncertainty bands.

Diagnostics example →

🚀 Quick Start

kalman-py isn't on PyPI yet. Install it from a clone, with the optional extras you need:

pip install -e ".[jax,plot]"   # or just -e . for NumPy only

Linear filter and smoother

import numpy as np
from kalman_py import KalmanFilter

dt = 0.1
F = np.array([[1.0, dt], [0.0, 1.0]])  # constant velocity: state (position, velocity)
H = np.array([[1.0, 0.0]])  # measure position
Q = 0.5 * np.array([[dt**3 / 3, dt**2 / 2], [dt**2 / 2, dt]])
R = np.array([[0.25]])
kf = KalmanFilter(F, H, Q, R, x0=[0.0, 1.0], P0=np.eye(2))

rng = np.random.default_rng(0)
zs = (np.arange(1, 201) * dt + 0.5 * rng.standard_normal(200))[:, None]

# Real-time: one measurement at a time
kf.predict()
kf.update(zs[0])

# Batch: the whole sequence at once (backend="jax" compiles it), then smooth
result = kf.filter(zs)
smoothed = kf.smooth(result)
print(result.means[-1], float(result.log_likelihood))

(x0, P0) is the prior before the first prediction, as in FilterPy. Pass square_root=True for the square-root form.

Extended and unscented filters

import jax
import jax.numpy as jnp
from kalman_py import ExtendedKalmanFilter, UnscentedKalmanFilter

jax.config.update("jax_enable_x64", True)


def f(x, dt):  # 2-D constant velocity: state (px, py, vx, vy)
    return jnp.array([x[0] + dt * x[2], x[1] + dt * x[3], x[2], x[3]])


def h(x):  # range and bearing from a sensor at the origin
    return jnp.array([jnp.hypot(x[0], x[1]), jnp.arctan2(x[1], x[0])])


def wrap_bearing(z, z_pred):  # bearing residuals in [-pi, pi)
    d = jnp.asarray(z) - jnp.asarray(z_pred)
    return d.at[1].set((d[1] + jnp.pi) % (2 * jnp.pi) - jnp.pi)


truth = np.column_stack([np.linspace(-200, -150, 100), np.linspace(60, -20, 100)])
truth = np.hstack([truth, np.tile([0.5, -0.8], (100, 1))])
radar = np.asarray(jax.vmap(h)(truth)) + rng.normal(0, [0.5, 0.01], (100, 2))

model = dict(Q=0.01 * np.eye(4), R=np.diag([0.25, 1e-4]), x0=truth[0], P0=np.eye(4))
ekf = ExtendedKalmanFilter(f, h, **model, residual_z=wrap_bearing)  # Jacobians by autodiff
ukf = UnscentedKalmanFilter(f, h, **model, residual_z=wrap_bearing, square_root=True)

ekf_result = ekf.filter(radar, dt=1.0, backend="jax")
ukf_result = ukf.filter(radar, dt=1.0, backend="jax")
ukf_smoothed = ukf.smooth(ukf_result)

The target crosses the negative x-axis, where bearings wrap from +π to −π; residual_z is all either filter needs to handle it.

Learn Q and R from data

from kalman_py.learning import fit_noise

fit = fit_noise(F, H, zs, x0=[0.0, 1.0], P0=np.eye(2))  # gradient-based when JAX is installed
print(fit.R, fit.log_likelihood, fit.converged)

Check consistency and plot

from kalman_py.diagnostics import consistency_check
from kalman_py.plotting import plot_estimates

check = consistency_check(result.nis, dof=1)
print(f"{check.fraction_inside:.0%} of steps inside the 95% NIS bounds")

axes = plot_estimates(smoothed, labels=["position", "velocity"])

📊 Benchmarks

Measured against FilterPy (per-step mode) and pykalman (batch mode) on the shared scenarios in tests/vectors/, with the same inputs, seeds and precision for every library. Estimates match in every linear case, so the comparison is about speed, stability and features.

Scenario kalman-py Best other Result
S1 · 1-D constant velocity, batch 0.41 µs/step (JAX) 38.0 µs (pykalman) 93× faster
S2 · 2-D constant velocity, batch 0.58 µs/step (JAX) 39.0 µs (pykalman) 67× faster
S5 · 15-state INS, batch 5.5 µs/step (JAX) 52.3 µs (pykalman) 9.5× faster
S1/S2/S5 · per-step loop 13.3 / 13.4 / 22.6 µs 10.3 / 10.6 / 18.9 µs (FilterPy) 0.78–0.84× (slower)
S3 · range-bearing, EKF & UKF RMSE 0.890, NEES 4.00 RMSE 0.890 (FilterPy) same accuracy
S4 · float32 stability square-root & JAX: never fail textbook filter: 67,589 steps see below

In per-step mode both libraries make the same number of small NumPy calls; kalman-py's extra cost is the symmetrization that keeps every covariance exactly symmetric.

Float32 stability (S4)

Steps before the float32 covariance stops being usable over 1,000,000 steps: square-root form and JAX backend never fail, the textbook filter fails at 67,589, and kalman-py's NumPy default (Joseph form) at 2,753.

Full results table (generated by scripts/make_table.py)

Measured on Apple M3 Pro (arm64), macOS 27.0.1, python-3.12.13, commit 9a2edc7, 2026-10-05.

Scenario Filter Precision Metric kalman-py filterpy kalman-py-jax kalman-py-numpy kalman-py-sqrt naive pykalman Ours vs best other
S1 KF batch float64 max_abs_diff (state) n/a n/a n/a n/a n/a n/a 5.99e-12 n/a
S1 KF batch float64 peak_memory (bytes) n/a n/a n/a 1,524,384 n/a n/a 1,136,154 n/a
S1 KF batch float64 rmse (state) 0.4233 n/a n/a 0.4233 n/a n/a 0.4233 1.00x
S1 KF batch float64 time_per_step (ns) 406.8 n/a n/a 2,606 n/a n/a 38,047 93.53x
S1 KF per-step float64 max_abs_diff (state) n/a 1.78e-15 n/a n/a n/a n/a n/a n/a
S1 KF per-step float64 peak_memory (bytes) 164,260 163,912 n/a n/a n/a n/a n/a 1.00x
S1 KF per-step float64 rmse (state) 0.4233 0.4233 n/a n/a n/a n/a n/a 1.00x
S1 KF per-step float64 time_per_step (ns) 13,252 10,320 n/a n/a n/a n/a n/a 0.78x
S2 KF batch float64 max_abs_diff (state) n/a n/a n/a n/a n/a n/a 3.32e-12 n/a
S2 KF batch float64 peak_memory (bytes) n/a n/a n/a 4,165,368 n/a n/a 3,866,229 n/a
S2 KF batch float64 rmse (state) 0.5073 n/a n/a 0.5073 n/a n/a 0.5073 1.00x
S2 KF batch float64 time_per_step (ns) 577.5 n/a n/a 2,786 n/a n/a 38,978 67.49x
S2 KF per-step float64 max_abs_diff (state) n/a 3.55e-15 n/a n/a n/a n/a n/a n/a
S2 KF per-step float64 peak_memory (bytes) 325,324 324,632 n/a n/a n/a n/a n/a 1.00x
S2 KF per-step float64 rmse (state) 0.5073 0.5073 n/a n/a n/a n/a n/a 1.00x
S2 KF per-step float64 time_per_step (ns) 13,442 10,556 n/a n/a n/a n/a n/a 0.79x
S3 EKF float64 nees (-) 4.002 4.002 n/a n/a n/a n/a n/a –
S3 EKF float64 rmse (state) 0.8896 0.8896 n/a n/a n/a n/a n/a 1.00x
S3 UKF float64 nees (-) 4.001 3.979 n/a n/a n/a n/a n/a –
S3 UKF float64 rmse (state) 0.8896 0.8896 n/a n/a n/a n/a n/a 1.00x
S4 KF float32 steps_to_failure (steps) 2,753 n/a 1,000,000 n/a 1,000,000 67,589 n/a 0.04x
S4 KF float32 steps_to_indefinite (steps) 2,753 n/a 1,000,000 n/a 1,000,000 1,000,000 n/a 0.0028x
S5 KF batch float64 peak_memory (bytes) n/a n/a n/a 42,821,472 n/a n/a 45,672,193 n/a
S5 KF batch float64 rmse (state) 0.1865 n/a n/a 0.1865 n/a n/a 0.1865 1.00x
S5 KF batch float64 time_per_step (ns) 5,535 n/a n/a 23,730 n/a n/a 52,290 9.45x
S5 KF per-step float64 peak_memory (bytes) 1,222,492 1,220,296 n/a n/a n/a n/a n/a 1.00x
S5 KF per-step float64 rmse (state) 0.1865 0.1865 n/a n/a n/a n/a n/a 1.00x
S5 KF per-step float64 time_per_step (ns) 22,639 18,923 n/a n/a n/a n/a n/a 0.84x
  • KF per-step rows: kalman-py is the step-by-step API (NumPy). KF batch rows: kalman-py is the JAX backend on CPU and kalman-py-numpy the NumPy backend; JAX has no peak_memory because tracemalloc can't see its buffers. max_abs_diff is the largest difference between kalman-py and that library's estimates.
  • S3 UKF: kalman-py redraws sigma points from the predicted distribution before each update, while FilterPy reuses the propagated ones, so their estimates differ slightly; the EKFs agree to ~1e-13. A consistent filter has average NEES = 4.
  • S4 (float32): FilterPy is n/a because it converts float32 inputs to float64 internally (its identity matrix is float64), so it can't run S4 as specified.
  • S4: kalman-py is the default configuration (NumPy backend, Joseph form); kalman-py-sqrt uses square_root=True; kalman-py-jax is the JAX backend. 1,000,000 means the filter never failed.

Reproduce everything:

ENV="$(git rev-parse --short HEAD),<cpu>,<os>,python-$(python -c 'import platform; print(platform.python_version())'),$(date +%F)"
uv run python benchmarks/run.py "$ENV"          # S1, S2, S5: speed, accuracy, memory
uv run python benchmarks/accuracy.py "$ENV"     # S3 accuracy, S4 stability
uv run python scripts/make_table.py results/results.csv kalman-py --readme README.md
uv run python scripts/plot_benchmarks.py        # the charts above

🏗️ Architecture

flowchart LR
    subgraph API["Filter classes"]
        KF["KalmanFilter"]
        EKF["ExtendedKalmanFilter"]
        UKF["UnscentedKalmanFilter"]
    end
    subgraph Backends
        NP["numpy_backend<br/>step + batch loops"]
        JX["jax_backend<br/>jit + lax.scan"]
    end
    KF & EKF & UKF -- "backend='numpy'" --> NP
    KF & EKF & UKF -- "backend='jax'" --> JX
    NP & JX --> RES["FilterResult<br/>means · covs · priors · NIS · log-likelihood"]
    RES --> SM["RTS smoother"]
    RES --> DIAG["diagnostics<br/>NIS / NEES / χ²"]
    RES --> PLOT["plotting"]
    JX --> LEARN["learning.fit_noise<br/>EM · BFGS"]
    NP --> LEARN
Component Responsibility
KalmanFilter, ExtendedKalmanFilter, UnscentedKalmanFilter Validate inputs, keep the step-by-step state (x, P), and dispatch filter/smooth to a backend.
backends.numpy_backend Pure-function filters and smoothers; Joseph and square-root forms; exact steady-state shortcut in batch mode. Needs only NumPy.
backends.jax_backend The same algorithms compiled with jax.lax.scan, with fused kernels for small matrices and vmap over sigma points. Optional.
result FilterResult / SmootherResult, generic over NumPy or JAX arrays and registered as JAX pytrees.
learning fit_noise: maximum-likelihood Q and R by EM (NumPy) or BFGS through the JAX filter.
diagnostics, plotting NEES/NIS consistency checks with in-house χ² quantiles; matplotlib helpers (optional extra).

🗺️ Roadmap

Step Description
First PyPI release 0.1.0 through the release workflow (trusted publishing), then a conda-forge recipe once the API is stable
Missing features A control input (B u), angle-valued UKF states (x_mean_fn/residual_x), missing measurements in batch mode
Published benchmarks Numbers from a dedicated, frequency-pinned machine, alongside the C, C++ and Rust implementations

🛠️ Development

uv sync --all-extras          # add --group docs for the documentation tools
uv run pytest                 # fast tests
uv run pytest -m ""           # everything, including slow comparison tests
uv run ruff check && uv run ruff format --check && uv run mypy src tests

uv sync --all-extras --group docs
uv run --no-sync mkdocs serve     # documentation at http://127.0.0.1:8000

CI (.github/workflows/ci.yml) runs lint and types, the tests on Python 3.10–3.14 × Linux/macOS/Windows (with JAX, and without it on 3.10 and 3.14), and a comparison job. That job runs every pass condition including the slow tests, benchmarks against FilterPy and pykalman, and compares speed with the latest main run. Timing regressions over 10% are reported as warnings, because shared runners vary by 10–20%; set REGRESSION_MODE to --fail to turn them into errors.

  • 🐞 Found a bug? Open an issue with a minimal reproduction.
  • 🧪 Changing numerics? The equivalence tests against FilterPy and pykalman and the stability property tests must keep passing; benchmark scenarios in tests/vectors/ are frozen.
  • 📐 Conventions: filters predict before the first update; covariances stay exactly symmetric; float32 inputs stay float32 end to end.
  • 📚 Docs and tutorials: code in the README and in docs/ is executed by the tests, and the notebooks in examples/ are re-run in CI, so keep them runnable.

🙏 Acknowledgements

kalman-py is measured against, and owes a great deal to, FilterPy and pykalman, and is built on NumPy and JAX.

📄 License

Released under the MIT License.

Metadata

Release files for kalman-py 0.1.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 kalman-py 0.1.0
File Size Uploaded
kalman_py-0.1.0.tar.gz 90.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for kalman-py 0.1.0
File Interpreter ABI Platform
kalman_py-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 132.1 kB

Release files / kalman_py-0.1.0.tar.gz

Download URL kalman_py-0.1.0.tar.gz
Size 90.3 kB
Tags Source
SHA-256 checksum
How to use checksums
b1d3696dda25ebfef800535dc0f5888de0a65f27021a816780eb4b5703dd9b70
BLAKE2b-256 checksum
How to use checksums
25d408ea76ebd031ca072a6d1cf8511428f824e924e6eb42e1000982210f74c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / kalman_py-0.1.0-py3-none-any.whl

Download URL kalman_py-0.1.0-py3-none-any.whl
Size 41.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3f06663a5dcc5c2feb31fcc96ba46e43e22f658f66e414219f58ea8481b80461
BLAKE2b-256 checksum
How to use checksums
aa56fbc3835ec2a40a475b847383524e3567be669059f99b62a0d4cad0645172
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.1

2 release files

This release

0.1.0 This release

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