Skip to content

Kalman Filter Examples

example/kalman_filter/

Framework:

  • kalman.h - Type-safe Kalman filter abstractions

Examples:

These examples demonstrate implementing Kalman filtering algorithms with mp-units, showcasing how physical quantities and units integrate naturally into state estimation systems. The examples progress from simple α-β filters to full Kalman filters with process noise, covering common scenarios in aerospace, navigation, and sensor fusion applications.

The Problem: State Estimation with Units

Kalman filtering estimates the true state of a system from noisy measurements. In real-world applications:

  • System states have physical dimensions: position (length), velocity (length/time), acceleration (length/time²), temperature
  • Measurements include uncertainties expressed in physical units (meters, m/s, degrees)
  • Process noise and measurement noise have covariance matrices with appropriate units
  • Kalman gains are dimensionless quantities resulting from variance ratios
  • Different state variables have time-derivative relationships: positionvelocityacceleration

Common issues in traditional implementations:

  • Mixing incompatible physical quantities (e.g., adding position and velocity)
  • Incorrect unit conversions in measurement updates
  • Dimensionality mismatches in covariance extrapolation
  • Loss of physical meaning in intermediate calculations

Type-Safe Kalman Filter Framework

The kalman.h header provides generic, type-safe abstractions for Kalman filtering:

System State Representation

template<mp_units::QuantityPoint... QPs>
  requires(sizeof...(QPs) > 0) && (sizeof...(QPs) <= 3) &&
          detail::are_time_derivatives<QPs::dimension...>
class system_state {
  std::tuple<QPs...> variables_;
  // ...
};

The system_state class template:

  • Stores 1-3 related quantity points (absolute values on a measurement scale)
  • Enforces time-derivative relationships via concept constraints
  • Supports: position-only, position-velocity, or position-velocity-acceleration states
  • Uses quantity_point to distinguish absolute values (e.g., altitude above sea level) from relative quantities

State Estimation with Uncertainty

template<mp_units::QuantityPoint QP, mp_units::QuantityPoint... Rest>
class system_state_estimate {
  state_type state_;
  variance_type variance_;  // Type computed as pow<2>(standard_deviation_type)

  [[nodiscard]] constexpr standard_deviation_type standard_deviation() const
  { return sqrt(variance_); }
};

The system_state_estimate combines:

  • System state (position, velocity, etc.)
  • Variance with correct units (e.g., m² for position uncertainty)
  • Automatic conversion between variance and standard deviation

Core Kalman Operations

All operations preserve dimensional correctness:

template<Quantity Q1, Quantity Q2>
  requires requires { get_common_reference(Q1::reference, Q2::reference); }
[[nodiscard]] constexpr quantity<dimensionless[one]> kalman_gain(Q1 variance_in_estimate,
                                                                 Q2 variance_in_measurement)
{
  return variance_in_estimate / (variance_in_estimate + variance_in_measurement);
}

The Kalman gain is dimensionless (variance ratio), enabling:

  • Type-safe blending between prediction and measurement
  • Automatic unit cancellation in variance division
  • Range validation (always between 0 and 1)
template<typename QP, QuantityPoint QM, QuantityOf<dimensionless> K>
  requires(implicitly_convertible(QM::quantity_spec, QP::quantity_spec))
[[nodiscard]] constexpr system_state<QP> state_update(const system_state<QP>& predicted,
                                                      QM measured, K gain)
{
  return system_state<QP>{get<0>(predicted) + gain * (measured - get<0>(predicted))};
}

State update computes: state_new = state_pred + K × (measurement − state_pred)

  • Dimensionless gain K scales the innovation (measurement residual)
  • Quantity point arithmetic ensures dimensional correctness
  • Innovation (measured - predicted) is a relative quantity
template<typename QP1, typename QP2, QuantityOf<isq::time> T>
[[nodiscard]] constexpr system_state<QP1, QP2> state_extrapolation(const system_state<QP1, QP2>& estimated,
                                                                   T interval)
{
  auto to_quantity = [](const auto& qp) { return qp.quantity_ref_from(qp.point_origin); };
  const auto qp1 = fma(to_quantity(get<1>(estimated)), interval, get<0>(estimated));
  const auto qp2 = get<1>(estimated);
  return system_state<QP1, QP2>{qp1, qp2};
}

Projects state forward in time using physics:

  • position_new = position + velocity × Δt
  • Handles quantity_point to quantity conversions automatically
  • For 3-state systems, includes ½ × acceleration × Δt² term
template<Quantity Q, QuantityOf<dimensionless> K>
[[nodiscard]] constexpr Q covariance_update(Q uncertainty, K gain)
{
  return (1 * one - gain) * uncertainty;
}

Updates uncertainty after measurement: P_new = (1 − K) × P_pred

template<Quantity Q1, Quantity Q2>
  requires requires { get_common_reference(Q1::reference, Q2::reference); }
[[nodiscard]] constexpr Quantity auto covariance_extrapolation(Q1 uncertainty, Q2 process_noise_variance)
{
  return uncertainty + process_noise_variance;
}

Projects uncertainty forward: P_pred = P + Q

  • Requires compatible quantity types (same physical dimension)
  • Process noise Q accounts for model imperfections

Example Progression

The 8 examples demonstrate increasing complexity:

Examples 1-4: α-β(-γ) Filters

Simple fixed-gain filters with manually tuned gains:

Example Description State Variables Fixed Gains Measurement
1 Golden bar weight estimation mass α = 1/n (dynamic) 1D mass
2 Vehicle position tracking (constant velocity) position, velocity α = 0.2, β = 0.1 1D position
3 Vehicle tracking with acceleration position, velocity α = 0.2, β = 0.1 1D position
4 Full α-β-γ filter (constant acceleration) position, velocity, acceleration α = 0.5, β = 0.4, γ = 0.1 1D position

Key Feature: Examples 2-4 demonstrate multi-state tracking where:

  • Only position is measured (e.g., GPS coordinates)
  • Velocity and acceleration are estimated from position innovations
  • Gains distributed across state variables: α for position, β for vel_ocity, γ for acceleration
using qp = quantity_point<isq::mass[g]>;
using state = kalman::system_state<
  qp
>;

const state initial_guess{qp{1 * kg}};
state next = initial_guess;

for (int index = 1; const auto& measurement : measurements) {
  const quantity gain = 1. / index;  // Decreasing gain over time
  const state current = state_update(next, measurement, gain);
  next = current;
}

Estimates golden bar mass from 10 noisy measurements. The gain 1/n gives equal weight to all measurements, computing a running average. No state extrapolation (static system).

using qp = quantity_point<isq::displacement[m]>;
using state = kalman::system_state<
  qp,
  quantity_point<isq::velocity[m/s]>
>;

const quantity interval = isq::duration(5 * s);
const state initial{qp{30 * km}, quantity_point{40 * m/s}};
std::array gain = {0.2 * one, 0.1 * one};  // α and β

state next = state_extrapolation(initial, interval);
for (const auto& measurement : measurements) {
  const state current = state_update(next, measurement, gain, interval);
  next = state_extrapolation(current, interval);
}

Tracks vehicle with constant velocity (40 m/s). The α-β filter:

  • α = 0.2: Updates position estimate from measurement innovation
  • β = 0.1: Updates velocity estimate from same innovation
  • Measurement interval: 5 seconds between GPS readings
using qp = quantity_point<isq::displacement[m]>;
using state = kalman::system_state<
  qp,
  quantity_point<isq::velocity[m/s]>,
  quantity_point<isq::acceleration[m/s2]>
>;

const quantity interval = isq::duration(5 * s);
const state initial{qp{30 * km}, quantity_point{50 * m/s}, quantity_point{0 * m/s2}};
std::array gain = {0.5 * one, 0.4 * one, 0.1 * one};  // α, β, γ

state next = state_extrapolation(initial, interval);
for (const auto& measurement : measurements) {
  const state current = state_update(next, measurement, gain, interval);
  next = state_extrapolation(current, interval);
}

Full 3-state filter for accelerating vehicle. The γ gain estimates acceleration from the same position measurements that update position (α) and velocity (β).

Examples 5-8: Full Kalman Filters

Adaptive-gain filters that compute Kalman gain from measurement and process uncertainty:

Example Description Measurement Process Noise Initial Uncertainty
5 Building height (static) height (σ = 5 m) None 15 m
6 Liquid temperature (stable) temperature (σ = 0.1°C) 0.0001 °C² 100°C
7 Liquid temperature (changing) temperature (σ = 0.1°C) 0.0001 °C² 100°C
8 Liquid temperature (high-noise) temperature (σ = 0.1°C) 0.15 °C² 100°C

Key Feature: The Kalman gain is computed dynamically from uncertainty estimates:

\[K_k = \frac{P_{k|k-1}}{P_{k|k-1} + R}\]

Where:

  • \(P_{k|k-1}\): Predicted estimate covariance
  • \(R\): Measurement noise covariance
  • Both have units of variance (e.g., m², °C²)
  • \(K_k\): Dimensionless Kalman gain (0 to 1)
using qp = quantity_point<isq::height[m]>;
using estimate = kalman::system_state_estimate<qp>;

const estimate initial{state{qp{60. * m}}, 15. * m};              // 60m ± 15m
const quantity measurement_error = isq::height(5. * m);
const quantity measurement_variance = pow<2>(measurement_error);  // 25 m²

auto predict = [](const estimate& current) {
  return current;  // No state change (static system)
};

estimate next = predict(initial);
for (const auto& measurement : measurements) {
  const quantity gain = kalman::kalman_gain(next.variance(), measurement_variance);
  const estimate current = state_estimate_update(next, measurement, gain);
  next = predict(current);
}

Estimates building height (60 m true value) from 10 noisy measurements:

  • Initial uncertainty: 15 m standard deviation (225 m² variance)
  • Measurement noise: 5 m standard deviation (25 m² variance)
  • No process noise (building height doesn't change)
  • Uncertainty decreases with each measurement
using qp = quantity_point<isq::Celsius_temperature[deg_C]>;
using estimate = kalman::system_state_estimate<qp>;

const quantity process_noise_variance = delta<pow<2>(deg_C)>(0.15);
const estimate initial{state{qp{delta<deg_C>(10.)}}, delta<deg_C>(100.)}; // 10°C ± 100°C
const quantity measurement_error = delta<deg_C>(0.1);
const quantity measurement_variance = pow<2>(measurement_error);

auto predict = [=](const estimate& current) {
  return estimate{
    current.state(),
    covariance_extrapolation(current.variance(), process_noise_variance)
  };
};

estimate next = predict(initial);
for (const auto& measurement : measurements) {
  const quantity gain = kalman::kalman_gain(next.variance(), measurement_variance);
  const estimate current = state_estimate_update(next, measurement, gain);
  next = predict(current);
}

Tracks warming liquid temperature (true value increasing):

  • Initial uncertainty: 100°C standard deviation (very uncertain)
  • Measurement noise: 0.1°C (precise sensor)
  • Process noise: 0.15 °C² variance (temperature rising unpredictably)
  • Uncertainty converges to balance process and measurement noise

Key Features Demonstrated

1. Quantity Point Semantics

quantity_point<isq::displacement[m]> position;  // Absolute position
quantity<isq::displacement[m]> distance;        // Relative distance

auto innovation = measured_position - predicted_position;  // quantity
auto new_position = old_position + innovation;             // quantity_point

Quantity points represent absolute values (points on a scale), while quantities are relative (differences or deltas). The type system prevents mixing them incorrectly.

2. Dimensional Analysis in Filtering

// State extrapolation: position_new = position + velocity × time
auto qp1 = fma(to_quantity(velocity_qp), time_interval, position_qp);
//         ↑                 ↑             ↑              ↑
//         |                 |             |              quantity_point<m>
//         |                 quantity<m/s> quantity<s>
//         |                 └─────── quantity<m> ────────┘
//         quantity_point<m>

// Acceleration contribution: ½ × acceleration × time²
auto term = acceleration_qp * pow<2>(time_interval) / 2;
//          ↑                  ↑
//          quantity<m/s²>     quantity<s²>
//          └──────── quantity<m> ──────────┘

Every operation preserves physical dimensions, caught at compile time.

3. Variance and Standard Deviation

class system_state_estimate {
  using standard_deviation_type = QP::quantity_type;  // e.g., quantity<isq::height[m]>
  using variance_type = quantity<pow<2>(standard_deviation_type::reference), ...>;
  //                                    ↑
  //                              e.g., quantity<pow<2>(isq::height[m])> = quantity<m²>

  variance_type variance_;

  [[nodiscard]] constexpr standard_deviation_type standard_deviation() const
  { return sqrt(variance_); }  // √(m²) = m
};

The type system enforces the relationship between variance (σ²) and standard deviation (σ), preventing errors like adding standard deviations instead of variances in covariance matrices.

4. Generic State Sizes

The framework handles 1, 2, or 3 state variables:

// 1-state (position only)
system_state<quantity_point<isq::mass[g]>>

// 2-state (position + velocity)
system_state<quantity_point<isq::displacement[m]>,
             quantity_point<isq::velocity[m/s]>>

// 3-state (position + velocity + acceleration)
system_state<quantity_point<isq::displacement[m]>,
             quantity_point<isq::velocity[m/s]>,
             quantity_point<isq::acceleration[m/s2]>>

The concept constraint are_time_derivatives<QPs::dimension...> ensures state variables have the correct dimensional relationships (each is the time derivative of the previous).

5. Measurement Unit Flexibility

// Distance measurements in different units
const std::array measurements = {
  qp{30'171 * m}, qp{30'353 * m}, qp{30'756 * m}  // meters
};

// Or kilometers
const qp initial_position{30 * km};  // Automatically converts to base unit

// Temperature measurements
const std::array temps = {
  qp{delta<deg_C>(50.486)},  // Celsius
  qp{delta<deg_C>(50.963)}
};

Measurements can use any compatible unit. The framework handles conversions automatically while preserving dimensional correctness.

6. Custom Formatting for System States

std::cout << MP_UNITS_STD_FMT::format(
  "{:2} | {:8} | {:23:0[:N[.2f]]1[:N[.2f]]} | {:23:0[:N[.2f]]1[:N[.2f]]}\n",
  iteration, measured, current_state, next_state
);
// Output:
//  N | Measured |          Curr. Estimate |           Next Estimate
//  1 |  30221 m |    30244.20 m 49.42 m/s |    30491.30 m 49.42 m/s
//                ↑                          ↑
//            position, velocity (current)   position, velocity (next)

The custom formatter for system_state:

  • Formats each component with individual specifications: 0[...] for first state, 1[...] for second, etc.
  • Aligns multi-component states in tabular output
  • Preserves unit symbols in formatted output

Real-World Applications

These examples apply to:

  • Aerospace: Aircraft/spacecraft position and velocity tracking from GPS/INS
  • Robotics: Mobile robot localization and navigation with noisy sensors
  • Embedded Systems: Sensor fusion in resource-constrained environments (IMU, odometry)
  • Process Control: Temperature, pressure, flow rate estimation in industrial systems
  • Weather Forecasting: Atmospheric state estimation from satellite/ground observations

The type-safe approach prevents common bugs:

  • ❌ Mixing meters with meters/second in state updates
  • ❌ Adding variance and standard deviation incorrectly
  • ❌ Incorrect time derivative relationships (e.g., positionacceleration without velocity)
  • ❌ Dimensionally inconsistent Kalman gain computations
  • ✅ All caught at compile time with clear error messages

Learn More