estkit

Documentation

estkit is header-only: add include/ to your include path and #include the estimator you want. Everything below is on this one page.

Getting started

Build the benchmark, the tests and the examples with CMake:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j
ctest --test-dir build                       # unit tests + examples
./build/example_drone                        # EKF/UKF/PF on a drone range model
./build/example_battery combined             # SOC estimation on a hard scenario
./build/bench_cell --list                    # every cell-level estimator

Single-precision build for a microcontroller: cmake -S . -B build_f -DESTKIT_FLOAT=ON. The library also cross-compiles for a Cortex-M7 with arm-none-eabi-g++ -mcpu=cortex-m7 -mfpu=fpv5-d16 -fno-exceptions -fno-rtti — see examples/embedded_cortex_m7.cpp.

The model concept

An estimator is a template over a model: any class that provides a state transition, a measurement and the two noise covariances. Jacobians, constraints and tunable parameters are optional and supplied numerically when absent.

struct MyModel {
  static constexpr int NX = 4, NU = 1, NY = 2;
  Vec<NX> f(const Vec<NX>& x, const Vec<NU>& u) const;   // x_{k+1}
  Vec<NY> h(const Vec<NX>& x, const Vec<NU>& u) const;   // y_k
  Mat<NX,NX> Q(const Vec<NX>&, const Vec<NU>&) const;
  Mat<NY,NY> R(const Vec<NX>&, const Vec<NU>&) const;
  // optional: F(x,u), H(x,u), B(x,u), constrain(x), NP/params()/set_params()
};

Every filter and observer is a template over such a model, so the same Ekf<M>, Ukf<M> or Mhe<M,N> that estimates the state of charge of a cell estimates the position of a drone or the attitude of an airframe:

#include "estkit/filters/ukf.hpp"
using namespace estkit;

Ukf<MyModel> ukf{MyModel{}};
ukf.init(x0, P0);
for (...) {
  ukf.predict(u_prev);        // time update with the previous input
  ukf.update(y, u);           // measurement update
  auto x = ukf.x();          // state estimate; ukf.P() its covariance
}

The interface is the same four calls for every estimator in the library — from a Luenberger observer to a 500-particle filter. Offline smoothers add a run() over the whole record.

Adding an estimator

One header under the family directory, templated on the model, with the primary reference in the header comment; no heap, no exceptions, warning-free in double and float; register it in benchmarks/registry_<family>.cpp, add a unit test and a chapter. The full contract is in docs/ESTIMATOR_API.md and CONTRIBUTING.md.

Numerical & embedded rules

  • No dynamic memory, no exceptions, no recursion of unbounded depth.
  • Fixed-size linear algebra with compile-time dimensions.
  • Joseph-form covariance update; symmetrise every step.
  • Square-root and U-D forms available where the covariance dynamic range demands them.
  • Deterministic, reproducible bit-for-bit from the seed.

No estimator in the library needs double precision on this problem — the single-precision build agrees with the double build to within the seed-to-seed spread — but the factorised forms are there for larger state vectors. Below 2 µs per step the cost of an estimator is irrelevant on any flight processor.

The numerics chapter →

Validation

The Kalman-type filters are cross-checked against FilterPy, the electrochemical plant against PyBaMM, and the attitude filters against ahrs — each an independent reference implementation, run on the exported benchmark data:

1e-15
EKF vs FilterPy
max |Δ SOC|
0.6–3.2 mV
SPM vs PyBaMM
terminal voltage
0.0014°
Madgwick/Mahony vs ahrs
attitude

These libraries are used only as references and are not part of estkit; see THIRD_PARTY_NOTICES. The checks run in CI.