Skip to content

Commit 670caec

Browse files
committed
feat: add KalmanFilter, a scalar Kalman filter with sensor fusion
Co-authored-by: Oleksandr Klymenko <19151554+alxkm@users.noreply.github.com> Signed-off-by: alxkm <19151554+alxkm@users.noreply.github.com>
1 parent 14c8db8 commit 670caec

2 files changed

Lines changed: 516 additions & 0 deletions

File tree

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
package com.thealgorithms.streaming;
2+
3+
/**
4+
* A scalar (one dimensional) <b>Kalman filter</b>: the optimal way to track a quantity that drifts
5+
* slowly while every measurement of it is noisy.
6+
*
7+
* <p>The filter carries two numbers: the current estimate {@code x} and how much it distrusts that
8+
* estimate, the error variance {@code p}. Each step has two halves.
9+
*
10+
* <pre>
11+
* predict: x &lt;- x + u p &lt;- p + q
12+
* update: k &lt;- p / (p + r) x &lt;- x + k * (z - x) p &lt;- (1 - k) * p
13+
* </pre>
14+
*
15+
* <p>where {@code q} is the process noise (how much the tracked quantity is expected to wander
16+
* between two steps), {@code r} the measurement noise, {@code z} the measurement and {@code k} the
17+
* Kalman gain. The gain is the whole story: it is the share of the measurement that gets believed.
18+
* When the filter is unsure ({@code p} large) or the sensor is good ({@code r} small), {@code k}
19+
* approaches 1 and the filter follows the sensor; in the opposite case it clings to its own
20+
* prediction. Nothing tunes this by hand, the variances do it.
21+
*
22+
* <h2>Sensor fusion</h2>
23+
*
24+
* <p>Fusing several sensors is not a separate algorithm: it is what happens when the same estimate
25+
* is corrected once per sensor, each with its own noise level. A cheap sensor with a large {@code r}
26+
* nudges the estimate a little, a precise one pulls it a lot, and the result is exactly the
27+
* inverse-variance weighted combination that {@link #fuse(double[], double[])} computes in closed
28+
* form:
29+
*
30+
* <pre>{@code
31+
* KalmanFilter filter = new KalmanFilter(startingHeight, 1.0, 0.01, 1.0);
32+
* for (int t = 0; t < steps; t++) {
33+
* filter.predict();
34+
* filter.update(barometer[t], barometerVariance);
35+
* filter.update(gps[t], gpsVariance); // second sensor, same estimate
36+
* double height = filter.estimate();
37+
* }
38+
* }</pre>
39+
*
40+
* <p>Both steps run in O(1) time and memory. This class is not thread-safe.
41+
*
42+
* @see <a href="https://en.wikipedia.org/wiki/Kalman_filter">Kalman filter</a>
43+
*/
44+
public final class KalmanFilter {
45+
46+
private final double processNoise;
47+
private final double measurementNoise;
48+
49+
private double estimate;
50+
private double errorCovariance;
51+
private double lastGain;
52+
53+
private final double initialEstimate;
54+
private final double initialErrorCovariance;
55+
56+
/**
57+
* Creates a filter.
58+
*
59+
* @param initialEstimate the starting guess for the tracked quantity
60+
* @param initialErrorCovariance how uncertain that guess is; a large value makes the filter trust
61+
* the first measurements almost completely
62+
* @param processNoise variance added on every {@link #predict()}, i.e. how fast the quantity is
63+
* expected to change on its own
64+
* @param measurementNoise default variance of a measurement, used by {@link #update(double)}
65+
* @throws IllegalArgumentException if any argument is not finite, or if a variance is negative,
66+
* or if {@code measurementNoise} is zero
67+
*/
68+
public KalmanFilter(double initialEstimate, double initialErrorCovariance, double processNoise, double measurementNoise) {
69+
requireFinite(initialEstimate, "initialEstimate");
70+
requireNonNegativeVariance(initialErrorCovariance, "initialErrorCovariance");
71+
requireNonNegativeVariance(processNoise, "processNoise");
72+
requirePositiveVariance(measurementNoise, "measurementNoise");
73+
74+
this.initialEstimate = initialEstimate;
75+
this.initialErrorCovariance = initialErrorCovariance;
76+
this.processNoise = processNoise;
77+
this.measurementNoise = measurementNoise;
78+
reset();
79+
}
80+
81+
/**
82+
* Advances the model by one step without any control input, growing the uncertainty by the
83+
* process noise.
84+
*
85+
* @return the predicted estimate, unchanged in value for this constant model
86+
*/
87+
public double predict() {
88+
return predict(0.0);
89+
}
90+
91+
/**
92+
* Advances the model by one step, shifting the estimate by a known control input.
93+
*
94+
* @param controlInput the change the estimate is expected to undergo, e.g. velocity times the
95+
* time step when tracking a position
96+
* @return the predicted estimate
97+
* @throws IllegalArgumentException if {@code controlInput} is not finite
98+
*/
99+
public double predict(double controlInput) {
100+
requireFinite(controlInput, "controlInput");
101+
estimate += controlInput;
102+
errorCovariance += processNoise;
103+
return estimate;
104+
}
105+
106+
/**
107+
* Corrects the estimate with a measurement taken by the default sensor.
108+
*
109+
* @param measurement the observed value
110+
* @return the corrected estimate
111+
* @throws IllegalArgumentException if {@code measurement} is not finite
112+
*/
113+
public double update(double measurement) {
114+
return update(measurement, measurementNoise);
115+
}
116+
117+
/**
118+
* Corrects the estimate with a measurement whose noise differs from the default one. Calling this
119+
* several times per step, once per sensor, is the whole of sensor fusion.
120+
*
121+
* @param measurement the observed value
122+
* @param noise variance of this particular measurement, strictly positive
123+
* @return the corrected estimate
124+
* @throws IllegalArgumentException if {@code measurement} is not finite or {@code noise} is not strictly positive
125+
*/
126+
public double update(double measurement, double noise) {
127+
requireFinite(measurement, "measurement");
128+
requirePositiveVariance(noise, "noise");
129+
130+
double innovationVariance = errorCovariance + noise;
131+
lastGain = errorCovariance / innovationVariance;
132+
estimate += lastGain * (measurement - estimate);
133+
// Algebraically this is (1 - gain) * p, but computing 1 - gain cancels away most of the
134+
// significant digits whenever the gain is close to one, as it is on the first measurements.
135+
errorCovariance = errorCovariance * noise / innovationVariance;
136+
return estimate;
137+
}
138+
139+
/**
140+
* Runs one full cycle: predict, then correct with the given measurement.
141+
*
142+
* @param measurement the observed value
143+
* @return the filtered estimate
144+
* @throws IllegalArgumentException if {@code measurement} is not finite
145+
*/
146+
public double filter(double measurement) {
147+
predict();
148+
return update(measurement);
149+
}
150+
151+
/**
152+
* Filters a whole signal offline, one cycle per sample.
153+
*
154+
* @param measurements the noisy signal
155+
* @return a new array holding the filtered signal, of the same length
156+
* @throws IllegalArgumentException if any measurement is not finite
157+
* @throws NullPointerException if {@code measurements} is {@code null}
158+
*/
159+
public double[] filter(double[] measurements) {
160+
double[] filtered = new double[measurements.length];
161+
for (int i = 0; i < measurements.length; i++) {
162+
filtered[i] = filter(measurements[i]);
163+
}
164+
return filtered;
165+
}
166+
167+
/**
168+
* Combines simultaneous readings of the same quantity taken by independent sensors, weighting
169+
* each by the inverse of its variance. This is the closed form of what repeated
170+
* {@link #update(double, double)} calls achieve within one step.
171+
*
172+
* @param measurements one reading per sensor
173+
* @param variances the noise variance of each sensor, strictly positive, same length as {@code measurements}
174+
* @return the fused reading together with its variance, which is never larger than the variance of
175+
* the best single sensor
176+
* @throws IllegalArgumentException if the arrays are empty, differ in length, hold a non-finite
177+
* measurement or a non-positive variance
178+
* @throws NullPointerException if either array is {@code null}
179+
*/
180+
public static Estimate fuse(double[] measurements, double[] variances) {
181+
if (measurements.length != variances.length) {
182+
throw new IllegalArgumentException("There must be exactly one variance per measurement, but got " + measurements.length + " and " + variances.length);
183+
}
184+
if (measurements.length == 0) {
185+
throw new IllegalArgumentException("At least one measurement is required");
186+
}
187+
188+
double weightSum = 0.0;
189+
double weightedSum = 0.0;
190+
for (int i = 0; i < measurements.length; i++) {
191+
requireFinite(measurements[i], "measurement");
192+
requirePositiveVariance(variances[i], "variance");
193+
double weight = 1.0 / variances[i];
194+
weightSum += weight;
195+
weightedSum += weight * measurements[i];
196+
}
197+
return new Estimate(weightedSum / weightSum, 1.0 / weightSum);
198+
}
199+
200+
/**
201+
* Returns the current estimate of the tracked quantity.
202+
*
203+
* @return the state estimate
204+
*/
205+
public double estimate() {
206+
return estimate;
207+
}
208+
209+
/**
210+
* Returns the variance of the current estimate; it shrinks with every update and grows with every
211+
* prediction.
212+
*
213+
* @return the error covariance
214+
*/
215+
public double errorCovariance() {
216+
return errorCovariance;
217+
}
218+
219+
/**
220+
* Returns the Kalman gain used by the most recent update, a number in {@code [0, 1)} telling how
221+
* much of that measurement was believed.
222+
*
223+
* @return the last gain, {@code 0} if no update has happened yet
224+
*/
225+
public double lastGain() {
226+
return lastGain;
227+
}
228+
229+
/**
230+
* Returns the process noise variance.
231+
*
232+
* @return the value given at construction time
233+
*/
234+
public double processNoise() {
235+
return processNoise;
236+
}
237+
238+
/**
239+
* Returns the default measurement noise variance.
240+
*
241+
* @return the value given at construction time
242+
*/
243+
public double measurementNoise() {
244+
return measurementNoise;
245+
}
246+
247+
/**
248+
* Restores the state the filter had right after construction.
249+
*/
250+
public void reset() {
251+
estimate = initialEstimate;
252+
errorCovariance = initialErrorCovariance;
253+
lastGain = 0.0;
254+
}
255+
256+
@Override
257+
public String toString() {
258+
return "KalmanFilter{estimate=" + estimate + ", errorCovariance=" + errorCovariance + ", lastGain=" + lastGain + '}';
259+
}
260+
261+
private static void requireFinite(double value, String name) {
262+
if (!Double.isFinite(value)) {
263+
throw new IllegalArgumentException("The " + name + " must be finite, but was " + value);
264+
}
265+
}
266+
267+
private static void requireNonNegativeVariance(double value, String name) {
268+
if (!(value >= 0.0) || !Double.isFinite(value)) {
269+
throw new IllegalArgumentException("The " + name + " must be finite and non-negative, but was " + value);
270+
}
271+
}
272+
273+
private static void requirePositiveVariance(double value, String name) {
274+
if (!(value > 0.0) || !Double.isFinite(value)) {
275+
throw new IllegalArgumentException("The " + name + " must be finite and strictly positive, but was " + value);
276+
}
277+
}
278+
279+
/**
280+
* A value paired with the variance that describes how much it can be trusted.
281+
*
282+
* @param value the estimated quantity
283+
* @param variance the variance of that estimate
284+
*/
285+
public record Estimate(double value, double variance) {
286+
}
287+
}

0 commit comments

Comments
 (0)