turbo-ckf
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.
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). fxoutput shape must be(2 * dim_x, dim_x).hxoutput shape must be(2 * dim_x, dim_z).
If you pass pointwise callbacks, TurboCKF raises immediately.
Install
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 turbo_ckf/setup_env.sh
Usage
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.
# 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:
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:
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 msvs CKF0.135 ms(18.79%lower time, about1.23xfaster). - Raspberry Pi 4 Model B: KCKF
1.28 msvs CKF1.51 ms(15.15%lower time, about1.18xfaster).
Local Results In This Repo
Measured on February 21, 2026 on Apple M4 Max, Python 3.12.0, NumPy 2.4.2.
From:
.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.56xvs FilterPy) - TurboCKF standard model KCKF (
turbo_standard_model_predict_update):0.539281 s(4.37xvs FilterPy) - TurboCKF standard model CKF (
turbo_standard_model_ckf_predict_update):0.549905 s(4.28xvs FilterPy) - KCKF-vs-CKF full-step speedup in this setup:
1.02x(0.549905 / 0.539281)
From:
.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.5xto~4.4xin 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)
Metadata
Release files for turbo-ckf 0.9.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| turbo_ckf-0.9.0.tar.gz | 139.6 kB | Details |
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| turbo_ckf-0.9.0-cp39-abi3-win_amd64.whl | CPython 3.9 | abi3 | Windows x86-64 | Details |
| turbo_ckf-0.9.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.9 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| turbo_ckf-0.9.0-cp39-abi3-macosx_11_0_arm64.whl | CPython 3.9 | abi3 | macOS 11.0+ ARM64 | Details |
Total release size: 1.6 MB
Release files / turbo_ckf-0.9.0.tar.gz
| Download URL | turbo_ckf-0.9.0.tar.gz |
|---|---|
| Size | 139.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1259937592d72b4af9c16082b6b3b71ce80540798bd582c84cf3ee9d793b882c
|
|
BLAKE2b-256 checksum How to use checksums |
cbd93b9d620e4cc3d0d91253ff83c51521d8cbbe732a55a7e54f896c8107840e
|
| 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 Sep 24, 2026.
Transparency logRelease files / turbo_ckf-0.9.0-cp39-abi3-win_amd64.whl
| Download URL | turbo_ckf-0.9.0-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 405.0 kB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
68950e20955db730b5411d0f712b6dc750dfebf1c3bdb0c0262c2f7d5c1fa481
|
|
BLAKE2b-256 checksum How to use checksums |
58cf03fe7c0de5fab00d58ba383f91e472fbe9ef92ab05d475551129d1abd166
|
| 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 Sep 24, 2026.
Transparency logRelease files / turbo_ckf-0.9.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | turbo_ckf-0.9.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 576.2 kB |
| Tags | CPython 3.9 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
5cf9dd4347cea24e8fe006e84b974b6df123150506b69e2736774b9e279e7fca
|
|
BLAKE2b-256 checksum How to use checksums |
f734895cb7fa45bbaba111f08278acbaa79b2892db747e9b9f2c06723c641fcc
|
| 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 Sep 24, 2026.
Transparency logRelease files / turbo_ckf-0.9.0-cp39-abi3-macosx_11_0_arm64.whl
| Download URL | turbo_ckf-0.9.0-cp39-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 489.9 kB |
| Tags | CPython 3.9 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
0645ca54eff32f1fd9932a7445255ba77437da648618dfb9a707c1bb35e46b7a
|
|
BLAKE2b-256 checksum How to use checksums |
74e167c5fa20d546c49fcf2743b5257471bd53001f53fe9e35ff79626e871235
|
| 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 Sep 24, 2026.
Transparency log