Skip to content

Commit ffc0ca9

Browse files
feat: add square root info kalman filter (#215)
* add square root info kalman filter * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * increase coverage --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 7ca1c09 commit ffc0ca9

12 files changed

Lines changed: 735 additions & 198 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Refer to the documentation to quickly integrate and utilize the library's signal
2020
| [Control Analysis](doc/control_analysis/README.md) | Frequency Response, Root Locus, Controllability/Observability Matrices & Gramians, Continuous-to-Discrete, Transfer Function ↔ State Space |
2121
| [Controllers](doc/controllers/README.md) | Bang-Bang/Hysteresis, PID, LQR, LQI (Integral/Servo State Feedback), MPC, Saturation, Rate Limiter, Slew-Limited Saturation, Feedforward/2-DOF, Gain-Scheduled Controller, Lead-Lag Compensator, Luenberger Observer |
2222
| [Estimators](doc/estimators/README.md) | Linear Regression, Polynomial Fitting, Yule-Walker (offline), Recursive Least Squares, LMS / NLMS Adaptive Filter (online), Consistency Metrics / NEES / NIS |
23-
| [Filters](doc/filters/README.md) | Kalman, Extended Kalman, Unscented Kalman, Alpha-Beta/Alpha-Beta-Gamma, FIR, IIR, Exponential Moving Average, Moving Average, Complementary, Median Filter, CIC (Cascaded Integrator-Comb), Notch/Comb Filter, Savitzky-Golay Filter, Biquad/Second-Order-Section Cascade, Madgwick/Mahony AHRS |
23+
| [Filters](doc/filters/README.md) | Kalman, Extended Kalman, Unscented Kalman, Square-Root Kalman, Alpha-Beta/Alpha-Beta-Gamma, FIR, IIR, Exponential Moving Average, Moving Average, Complementary, Median Filter, CIC (Cascaded Integrator-Comb), Notch/Comb Filter, Savitzky-Golay Filter, Biquad/Second-Order-Section Cascade, Madgwick/Mahony AHRS |
2424
| [Neural Network](doc/neural_network/README.md) | Layers, activations, losses, model |
2525
| [Optimization](doc/optimization/README.md) | Gradient Descent |
2626
| [Regularization](doc/regularization/README.md) | L1 (Lasso), L2 (Ridge) |

ROADMAP.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ Difficulty legend:
2727

2828
| # | Component | Target module | Difficulty |
2929
|----|------------------------------------------------------|---------------------------|------------|
30-
| 39 | Square-root / Information Kalman filter | `filters/active` | ★★★★☆ |
3130
| 40 | Feedback linearization | `nonlinear_control` (new) | ★★★★☆ |
3231
| 41 | Backstepping controller | `nonlinear_control` (new) | ★★★★☆ |
3332
| 42 | Symmetric eigenvalue solver (Jacobi) | `solvers` | ★★★★★ |

doc/filters/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ Filters that incorporate feedback and a dynamic internal model of the system.
1010

1111
| Algorithm | Description |
1212
|------------------------------------------------------------|--------------------------------------------------------------------------|
13-
| [Kalman Filter](active/KalmanFilter.md) | Optimal recursive state estimator for linear systems with Gaussian noise |
14-
| [Extended Kalman Filter](active/ExtendedKalmanFilter.md) | Nonlinear state estimator using first-order linearization (Jacobians) |
15-
| [Unscented Kalman Filter](active/UnscentedKalmanFilter.md) | Nonlinear state estimator using sigma points; no Jacobians required |
16-
| [Kalman Smoother](active/KalmanSmoother.md) | Offline RTS smoother providing MMSE estimates over the full observation sequence |
13+
| [Kalman Filter](active/KalmanFilter.md) | Optimal recursive state estimator for linear systems with Gaussian noise |
14+
| [Extended Kalman Filter](active/ExtendedKalmanFilter.md) | Nonlinear state estimator using first-order linearization (Jacobians) |
15+
| [Unscented Kalman Filter](active/UnscentedKalmanFilter.md) | Nonlinear state estimator using sigma points; no Jacobians required |
16+
| [Kalman Smoother](active/KalmanSmoother.md) | Offline RTS smoother providing MMSE estimates over the full observation sequence |
17+
| [Square-Root Kalman Filter](active/SquareRootKalmanFilter.md) | Numerically robust Kalman filter propagating a Cholesky factor to guarantee positive-definiteness |
1718

1819
### Passive Filters
1920

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Square-Root Kalman Filter
2+
3+
## Overview & Motivation
4+
5+
Standard Kalman filter implementations propagate the error covariance matrix $P$ directly. On
6+
resource-constrained hardware — where floating-point arithmetic is limited to 32 bits — roundoff
7+
errors accumulate with every predict-update cycle. After many iterations $P$ can lose symmetry
8+
or positive-definiteness, causing the filter to diverge catastrophically.
9+
10+
The **square-root Kalman filter** eliminates this failure mode by never storing $P$ explicitly.
11+
Instead it carries a Cholesky factor $S$ such that $P = S S^\top$, and rewrites every
12+
predict/update operation as an orthogonal triangularization. Because orthogonal transforms
13+
preserve matrix geometry exactly, the factor $S$ — and therefore the covariance $P = S S^\top$
14+
— remains symmetric positive-definite by construction even in single-precision arithmetic.
15+
16+
A further benefit is numerical conditioning: $\mathrm{cond}(S) = \sqrt{\mathrm{cond}(P)}$, so
17+
roughly twice as many significant bits survive across each filter step compared to the
18+
conventional form.
19+
20+
## Mathematical Theory
21+
22+
### State-Space Model
23+
24+
The underlying discrete-time linear system is identical to the conventional Kalman filter:
25+
26+
$$x_k = F x_{k-1} + B u_{k-1} + w_{k-1}, \quad w \sim \mathcal{N}(0,Q)$$
27+
$$z_k = H x_k + v_k, \quad v \sim \mathcal{N}(0,R)$$
28+
29+
The square-root filter parameterises uncertainty through lower-triangular factors
30+
$S$, $S_Q$, $S_R$ satisfying $P = S S^\top$, $Q = S_Q S_Q^\top$, $R = S_R S_R^\top$.
31+
32+
### Predict Step (Time Update)
33+
34+
The goal is to compute $S^-$ such that $P^- = F P F^\top + Q = S^- (S^-)^\top$ without
35+
forming $P$ or $P^-$.
36+
37+
Construct the $(2n \times n)$ pre-array:
38+
39+
$$\mathcal{A} = \begin{bmatrix} (F S)^\top \\ S_Q^\top \end{bmatrix}$$
40+
41+
Apply a QR decomposition $\mathcal{A} = Q_\perp R$ (orthogonal $Q_\perp$, upper-triangular $R$).
42+
Then $S^- = R^\top$ is the new lower-triangular factor, because:
43+
44+
$$R^\top R = \mathcal{A}^\top \mathcal{A} = S^\top F^\top F S + S_Q^\top S_Q = F P F^\top + Q = P^-$$
45+
46+
### Update Step (Measurement Update)
47+
48+
Construct the $((m+n) \times (m+n))$ pre-array:
49+
50+
$$\mathcal{B} = \begin{bmatrix} S_R & H S \\ 0 & S \end{bmatrix}$$
51+
52+
Apply QR triangularization: $\mathcal{B} = Q_\perp \begin{bmatrix} S_y & \tilde{K} \\ 0 & S^+ \end{bmatrix}$
53+
54+
The block $S_y$ (upper-left, $m \times m$) is the innovation-covariance factor satisfying
55+
$S_y S_y^\top = H P^- H^\top + R$. The block $S^+$ (lower-right, $n \times n$) is the
56+
updated covariance factor. The Kalman gain emerges from the cross block:
57+
58+
$$K = \tilde{K}^\top S_y^{-\top}$$
59+
60+
solved via a triangular back-substitution rather than an explicit matrix inverse.
61+
62+
The state update is the standard correction:
63+
64+
$$x^+ = x^- + K(z - H x^-)$$
65+
66+
### QR via Givens Rotations
67+
68+
Both pre-arrays are triangularized by sequential Givens rotations applied column by column,
69+
annihilating one sub-diagonal entry per rotation. This is cache-friendly, in-place, and
70+
requires only $O(n^2)$ temporary storage — no heap allocation.
71+
72+
## Complexity Analysis
73+
74+
| Case | Time | Space | Notes |
75+
|---------|----------------|----------|--------------------------------------|
76+
| Predict | $O(n^3)$ | $O(n^2)$ | Dominated by $F S$ multiply and QR |
77+
| Update | $O((n+m) n^2)$ | $O(n^2)$ | QR on $(m+n) \times (m+n)$ pre-array |
78+
79+
where $n =$ `StateSize`, $m =$ `MeasurementSize`. All arrays are fixed-size; no heap is used.
80+
81+
## Step-by-Step Walkthrough
82+
83+
Consider a 1-state system ($n=1$, $m=1$) with $F=1$, $H=1$, $S=0.5$, $S_Q=0.1$, $S_R=0.3$:
84+
85+
**Predict:**
86+
87+
Pre-array: $\mathcal{A} = [0.5;\; 0.1]$ (column vector, already a column).
88+
$R = \sqrt{0.5^2 + 0.1^2} = \sqrt{0.26} \approx 0.5099$.
89+
New factor $S^- = 0.5099$, i.e. $P^- \approx 0.26$.
90+
91+
**Update** (measurement $z = 1.2$, prior $x^- = 1.0$):
92+
93+
Pre-array $\mathcal{B} = \begin{bmatrix}0.3 & 0.5099 \\ 0 & 0.5099\end{bmatrix}$.
94+
One Givens rotation annihilates $\mathcal{B}_{10}=0$, yielding upper-triangular form.
95+
$S_y \approx 0.5974$, $S^+ \approx 0.424$, $K \approx 0.741$.
96+
$x^+ = 1.0 + 0.741 \times 0.2 = 1.148$.
97+
98+
## Pitfalls & Edge Cases
99+
100+
- **Factor sign convention.** The lower-triangular factor returned by Cholesky has positive
101+
diagonal; the QR step may flip signs. Diagonal entries should be taken as absolute values to
102+
maintain the canonical lower-triangular form.
103+
- **Near-zero $R$.** When measurement noise approaches zero the innovation-covariance factor
104+
$S_y$ tends to zero; the triangular solve must be guarded against near-singular diagonals.
105+
- **Near-zero $Q$.** With $S_Q = 0$ the predict step reduces the pre-array to a single block
106+
$(F S)^\top$; the QR degenerates to a transpose — no numerical issue, covariance only shrinks.
107+
- **Large initial uncertainty.** A near-singular or large $P_0$ with $\mathrm{cond}(P_0) \approx 10^8$
108+
is handled safely because $\mathrm{cond}(S_0) \approx 10^4$, which 32-bit floats can represent.
109+
110+
## Variants & Generalizations
111+
112+
- **Information filter (dual form).** Propagate a factor of $P^{-1}$ instead of $P$.
113+
Cheaper when many measurements are fused per step; trivially encodes unknown initial state
114+
as zero information.
115+
- **Square-root UKF.** Apply the same QR trick to the UKF sigma-point covariance propagation,
116+
replacing the weighted outer-product update with a rank-1 Cholesky update/downdate.
117+
- **Potter / Carlson algorithms.** Older scalar measurement variants that process one
118+
measurement component at a time; simpler but restricted to diagonal $R$.
119+
120+
## Applications
121+
122+
- **Multi-sensor fusion on microcontrollers** — the primary motivation; 32-bit floats survive
123+
hundreds of filter steps without divergence.
124+
- **INS/GNSS navigation** — long mission durations where a conventional filter would drift.
125+
- **Safety-critical estimators** — provably PSD covariance at every step simplifies validation.
126+
- **Battery state-of-charge estimation** — small $n$, constrained hardware, precision matters.
127+
128+
## Connections to Other Algorithms
129+
130+
- **Kalman Filter** — identical estimates; only the covariance bookkeeping differs. Use the
131+
conventional form when precision is not a concern and code simplicity is preferred.
132+
- **Cholesky Decomposition** — used to convert a full covariance $P_0$ into the initial factor
133+
$S_0$ before constructing this filter.
134+
- **QR Decomposition (Householder/Givens)** — the numerical engine of every predict/update step.
135+
- **Unscented Kalman Filter** — a square-root variant exists (SR-UKF) that applies the same QR
136+
trick to the sigma-point covariance propagation.
137+
138+
## References & Further Reading
139+
140+
- P. Kaminski, A. Bryson, S. Schmidt, "Discrete Square Root Filtering: A Survey of Current
141+
Techniques," *IEEE Transactions on Automatic Control*, 16(6), 1971.
142+
- R. van der Merwe and E. Wan, "The Square-Root Unscented Kalman Filter for State and
143+
Parameter-Estimation," *ICASSP*, 2001.
144+
- G. Bierman, *Factorization Methods for Discrete Sequential Estimation*, Academic Press, 1977.

numerical/filters/active/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ target_sources(numerical.filters.active PRIVATE
1919
KalmanFilter.hpp
2020
KalmanFilterBase.hpp
2121
KalmanSmoother.hpp
22+
SquareRootKalmanFilter.hpp
2223
UnscentedKalmanFilter.hpp
2324
)
2425

@@ -30,6 +31,7 @@ numerical_add_coverage_sources(numerical.filters.active
3031
KalmanFilter.cpp
3132
KalmanFilterBase.cpp
3233
KalmanSmoother.cpp
34+
SquareRootKalmanFilter.cpp
3335
UnscentedKalmanFilter.cpp
3436
)
3537

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include "numerical/filters/active/SquareRootKalmanFilter.hpp"
2+
3+
namespace filters
4+
{
5+
template class SquareRootKalmanFilter<float, 2, 1, 0>;
6+
template class SquareRootKalmanFilter<float, 2, 1, 1>;
7+
template class SquareRootKalmanFilter<float, 3, 1, 0>;
8+
}

0 commit comments

Comments
 (0)