Metadata-Version: 2.4
Name: turbo-ckf
Version: 0.9.0
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: License :: OSI Approved :: MIT License
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: Programming Language :: Rust
Classifier: Topic :: Scientific/Engineering
Requires-Dist: numpy>=1.22
Requires-Dist: numpy ; extra == 'dev'
Requires-Dist: filterpy ; extra == 'dev'
Requires-Dist: pytest ; extra == 'dev'
Requires-Dist: pytest-benchmark ; extra == 'dev'
Requires-Dist: pytest-cov ; extra == 'dev'
Requires-Dist: maturin ; extra == 'dev'
Provides-Extra: dev
License-File: LICENSE-MIT
License-File: LICENSE-APACHE
Summary: Turbo-CKF: High-performance Cubature Kalman Filter
Keywords: kalman-filter,cubature-kalman-filter,sensor-fusion,rust,python
Author: Mohammed Khalid
License-Expression: MIT OR Apache-2.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/mokhld/turbo-ckf
Project-URL: Issues, https://github.com/mokhld/turbo-ckf/issues
Project-URL: Repository, https://github.com/mokhld/turbo-ckf

# turbo-ckf

[![PyPI version](https://img.shields.io/pypi/v/turbo-ckf.svg)](https://pypi.org/project/turbo-ckf/)
[![Python versions](https://img.shields.io/pypi/pyversions/turbo-ckf.svg)](https://pypi.org/project/turbo-ckf/)
[![CI](https://github.com/mokhld/turbo-ckf/actions/workflows/ci.yml/badge.svg)](https://github.com/mokhld/turbo-ckf/actions/workflows/ci.yml)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)

`turbo-ckf` is a Rust-backed Cubature Kalman Filter package for high-throughput prediction/update loops. Implemented here purely as an experiment after reading the paper.

## What This Package Optimizes

- Fast prediction with built-in linear models:
  - `predict_standard_model(...)`
  - `predict_standard_model_ckf(...)`
  - `predict_linear_model(F)`
  - `predict_linear_model_ckf(F)`
- Fast AHRS update path:
  - `update_paper_ahrs(...)`

`predict(...)` and `update(...)` also run through Rust, but callback cost in Python can dominate if your models are heavy.

## Square-Root Variant

`TurboSRCKF` is a separate estimator that propagates the lower-triangular Cholesky factor of `P` directly. The filter loop never calls `stable_cholesky` on `P`, so the silent diagonal-jitter accumulation `TurboCKF` surfaces via `jitter_count` is structurally impossible in steady state. Use it when `P` becomes ill-conditioned (e.g., long predict chains with tiny `Q`/`R`, or rank-deficient seed covariances). Mirrors `predict(...)` + `update(...)`; for closed-form linear predicts or the AHRS update use `TurboCKF`.

```python
from turbo_ckf import TurboSRCKF
kf = TurboSRCKF(dim_x=4, dim_z=2, dt=0.1, hx=hx_vectorized, fx=fx_vectorized)
kf.predict()
kf.update(z)
# kf.chol_P is the lower-triangular factor; kf.P = chol_P @ chol_P.T
```

## Callback Contract

Custom `fx` and `hx` must be vectorized:

- Input shape is `(2 * dim_x, dim_x)`.
- `fx` output shape must be `(2 * dim_x, dim_x)`.
- `hx` output shape must be `(2 * dim_x, dim_z)`.

If you pass pointwise callbacks, `TurboCKF` raises immediately.

## Install

```bash
pip install turbo-ckf
```

Prebuilt wheels (one abi3 wheel per platform, CPython 3.9 or newer; CI tests
3.9 to 3.13) are published for three platforms only:

- Linux x86_64 (manylinux2014, glibc 2.17 or newer)
- macOS arm64 (Apple silicon, macOS 11 or newer)
- Windows x64

Everywhere else, including Linux aarch64 (for example Raspberry Pi), musl Linux
(Alpine) and Intel Macs, `pip` builds from the sdist. That needs a Rust
toolchain on `PATH` (install one from https://rustup.rs).

## Install from source (development)

From `turbo-ckf/`:

```bash
bash turbo_ckf/setup_env.sh
```

## Usage

```python
from turbo_ckf import TurboCKF

kf = TurboCKF(dim_x=2, dim_z=1, dt=0.1, hx=hx_vectorized, fx=fx_vectorized)
kf.predict_standard_model("constant_velocity")
kf.update(z)
```

The built-in models, `"constant_velocity"` and `"constant_acceleration"`, need
to know how the state vector is ordered. The default, `layout="blocked"`, is all
positions, then all velocities, then all accelerations: `[x, y, vx, vy]` or
`[x, y, vx, vy, ax, ay]`. FilterPy code usually orders the state per axis,
`[x, vx, y, vy]` or `[x, vx, ax, y, vy, ay]`; pass `layout="interleaved"` for
that. `predict_standard_model_ckf` takes the same argument. The layout cannot
be inferred from the state, so a mismatch gives wrong predictions without an
error. With `dim_x=2` (or 3 for constant acceleration) the two layouts are the
same.

```python
# FilterPy-style 2-D state [x, vx, y, vy]
kf = TurboCKF(dim_x=4, dim_z=2, dt=0.1, hx=hx_vectorized, fx=fx_vectorized)
kf.predict_standard_model("constant_velocity", layout="interleaved")
```

State assignment is forgiving about spelling: `x` accepts lists, integer
arrays, and column vectors; `P`, `Q`, `R` accept scalars (`kf.R = 0.25` means
`0.25 * I`), 1-D diagonals (`kf.Q = [1e-3, 1e-2]`), or full matrices. Wrong
sizes and non-finite values raise at assignment time with the attribute named
in the error. Changing `kf.dt` after construction takes effect on the next
predict, including the standard/linear-model paths.

A NaN or inf measurement raises instead of corrupting the state. For a missed
measurement pass `z=None`, which runs the predict-only step and clears the
innovation diagnostics. The same applies to `batch_filter`, which does not skip
missing measurements yet (use `run(..., nan_means_missing=True)` for sequences
with dropouts), and to callback outputs: an `fx` or `hx` that returns NaN or
inf raises a `ValueError` naming it, and the state is left unchanged. In
`batch_parallel_step`, a filter whose own `x`, `P` or `z` is non-finite gets
status 3 and is skipped while the rest of the bank updates.

### Filter state lives in Rust

The Rust backend holds `x`, `P`, `Q`, `R` and every diagnostic (`K`, `y`, `S`,
`x_prior`, `P_post`, `log_likelihood`, `nis`, the jitter counters). An
attribute is copied into NumPy only when you read it, and that copy is reused
until the next `predict`/`update`, so a loop that reads nothing pays nothing
for state sync.

In-place edits work as in FilterPy. The array returned by `kf.x`, `kf.P`,
`kf.Q` or `kf.R` is the filter's current value, and writing into it
(`kf.x[0] = 1.0`, `kf.P[2:, 2:] *= 1000`) takes effect on the next call. Once a
`predict`/`update` or an assignment replaces the value, the array you held
becomes a read-only snapshot, so a late write raises `ValueError` instead of
being dropped silently, and `history.append(kf.x)` needs no `.copy()`.
Assignment copies your array into the filter. Output arrays such as `kf.K` are
snapshots that do not feed back into the filter, and `TurboSRCKF`'s `chol_P`,
`chol_Q`, `chol_R` are read-only. Reading never changes results; in
`TurboSRCKF` it never re-factors `P` either.

`copy()`, `to_dict()`/`from_dict()` and `pickle` keep the diagnostic counters,
and `reset()` zeroes them. Pickling stores `fx` and `hx` by reference, so use
module-level functions (a lambda makes `pickle` raise). `TurboSRCKF.to_dict()`
stores `chol_P`, so restoring does not re-factor `P`.

Per step, 4-state / 2-measurement constant-velocity model with a vectorized
`hx`, Apple M4 Max, median of 7 runs:

| Loop | State copied every call | State in Rust |
| --- | --- | --- |
| `predict_standard_model` + `update` | 12.6 us | 4.1 us |
| same, reading `kf.x` and `kf.P` each step | 12.5 us | 4.7 us |
| `predict` + `update` with `fx`/`hx` callbacks | 15.7 us | 6.8 us |
| `TurboSRCKF` `predict` + `update` | 18.9 us | 6.6 us |
| `run()` over 10,000 steps (total) | 182 ms | 51 ms |

### Filtering a whole sequence

`run(...)` wraps the predict/update loop, stacks the per-step outputs, and
handles measurement dropouts:

```python
result = kf.run(zs)                # zs: (N, dim_z), or (N,) when dim_z == 1
result.xs, result.Ps               # posterior means / covariances, stacked
result.log_likelihoods, result.nis # per-step diagnostics

# Missed measurements: None entries (or all-NaN rows with
# nan_means_missing=True) skip the update for that step.
kf.run([z0, None, z2])
kf.run(zs, nan_means_missing=True)

# Per-step time steps and measurement noise:
kf.run(zs, dts=dt_array, Rs=r_stack)
```

`run` is available on both `TurboCKF` and `TurboSRCKF`. For linear models
prefer the static `TurboCKF.batch_filter`, which runs the whole loop inside
Rust in a single crossing.

AHRS path:

```python
kf.predict_linear_model(Fk)
kf.update_paper_ahrs(z6, sigma_acc2=1e-2, sigma_mag2=1e-2)
```

`z6` is `[ax, ay, az, mx, my, mz]` in any units, for example raw m/s^2 and uT.
The paper's observation model predicts unit vectors, so the update normalizes
the accelerometer and magnetometer 3-vectors to unit length before using them
(a zero or non-finite norm raises `ValueError`). `sigma_acc2` and `sigma_mag2`
are therefore variances of the unit-vector components, not of the raw readings.
The filter needs `dim_x=4` (quaternion) and `dim_z=6`, and the update overwrites
`R` with `diag(sigma_acc2, sigma_acc2, sigma_acc2, sigma_mag2, sigma_mag2, sigma_mag2)`.

## Benchmarks

### Paper-Reported Targets (Yamagishi and Jing, arXiv:2602.12283)

These are the KCKF-vs-CKF results reported in the paper:

- MacBook Pro 2021 (M1 Pro): KCKF `0.110 ms` vs CKF `0.135 ms` (`18.79%` lower time, about `1.23x` faster).
- Raspberry Pi 4 Model B: KCKF `1.28 ms` vs CKF `1.51 ms` (`15.15%` lower time, about `1.18x` faster).

### Local Results In This Repo

Measured on **February 21, 2026** on Apple M4 Max, Python 3.12.0, NumPy 2.4.2.

From:

```bash
.venv-turbo-ckf/bin/python turbo_ckf_tests/verify_before_after.py --steps 50000 --repeats 7 --warmup 1 --parity-steps 500
```

Median runtime per 50k predict+update steps:

- FilterPy (`filterpy_predict_update`): `2.354375 s` (baseline)
- TurboCKF callback path (`turbo_callback_predict_update`): `0.662003 s` (`3.56x` vs FilterPy)
- TurboCKF standard model KCKF (`turbo_standard_model_predict_update`): `0.539281 s` (`4.37x` vs FilterPy)
- TurboCKF standard model CKF (`turbo_standard_model_ckf_predict_update`): `0.549905 s` (`4.28x` vs FilterPy)
- KCKF-vs-CKF full-step speedup in this setup: `1.02x` (`0.549905 / 0.539281`)

From:

```bash
.venv-turbo-ckf/bin/python turbo_ckf_tests/benchmark_paper.py
```

- KCKF-vs-CKF (prediction only, backend): `1.67x`
- KCKF-vs-CKF (full step, backend): `1.09x`
- KCKF-vs-CKF (full step, wrapper): `1.03x`

Interpretation:

- The largest gains here are from the Rust-backed implementation versus pure-Python FilterPy loops (`~3.5x` to `~4.4x` in these runs).
- The direct KCKF-vs-CKF algorithmic gain is present but smaller on full-step end-to-end runs, and larger when isolating prediction equations.

## Research Basis

- This repo is an implementation of the KCKF AHRS equations described in:
  - Yamagishi, S. and Jing, L., "A Lightweight Cubature Kalman Filter for Attitude and Heading Reference Systems Using Simplified Prediction Equations", IEEE Access, vol. 14, pp. 73686-73697, 2026, doi:10.1109/ACCESS.2026.3686031. Preprint: arXiv:2602.12283, https://arxiv.org/abs/2602.12283.
- Credit for the method belongs to the paper authors.

## License

Dual-licensed under either:
- MIT (`LICENSE-MIT`)
- Apache-2.0 (`LICENSE-APACHE`)

