|
| 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 <- x + u p <- p + q |
| 12 | + * update: k <- p / (p + r) x <- x + k * (z - x) p <- (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