package com.thealgorithms.streaming;
/**
* A scalar (one dimensional) <b>Kalman filter</b>: the optimal way to track a quantity that drifts
* slowly while every measurement of it is noisy.
*
* <p>The filter carries two numbers: the current estimate {@code x} and how much it distrusts that
* estimate, the error variance {@code p}. Each step has two halves.
*
* <pre>
* predict: x <- x + u p <- p + q
* update: k <- p / (p + r) x <- x + k * (z - x) p <- (1 - k) * p
* </pre>
*
* <p>where {@code q} is the process noise (how much the tracked quantity is expected to wander
* between two steps), {@code r} the measurement noise, {@code z} the measurement and {@code k} the
* Kalman gain. The gain is the whole story: it is the share of the measurement that gets believed.
* When the filter is unsure ({@code p} large) or the sensor is good ({@code r} small), {@code k}
* approaches 1 and the filter follows the sensor; in the opposite case it clings to its own
* prediction. Nothing tunes this by hand, the variances do it.
*
* <h2>Sensor fusion</h2>
*
* <p>Fusing several sensors is not a separate algorithm: it is what happens when the same estimate
* is corrected once per sensor, each with its own noise level. A cheap sensor with a large {@code r}
* nudges the estimate a little, a precise one pulls it a lot, and the result is exactly the
* inverse-variance weighted combination that {@link #fuse(double[], double[])} computes in closed
* form:
*
* <pre>{@code
* KalmanFilter filter = new KalmanFilter(startingHeight, 1.0, 0.01, 1.0);
* for (int t = 0; t < steps; t++) {
* filter.predict();
* filter.update(barometer[t], barometerVariance);
* filter.update(gps[t], gpsVariance); // second sensor, same estimate
* double height = filter.estimate();
* }
* }</pre>
*
* <p>Both steps run in O(1) time and memory. This class is not thread-safe.
*
* @see <a href="https://en.wikipedia.org/wiki/Kalman_filter">Kalman filter</a>
*/
public final class KalmanFilter {
private final double processNoise;
private final double measurementNoise;
private double estimate;
private double errorCovariance;
private double lastGain;
private final double initialEstimate;
private final double initialErrorCovariance;
/**
* Creates a filter.
*
* @param initialEstimate the starting guess for the tracked quantity
* @param initialErrorCovariance how uncertain that guess is; a large value makes the filter trust
* the first measurements almost completely
* @param processNoise variance added on every {@link #predict()}, i.e. how fast the quantity is
* expected to change on its own
* @param measurementNoise default variance of a measurement, used by {@link #update(double)}
* @throws IllegalArgumentException if any argument is not finite, or if a variance is negative,
* or if {@code measurementNoise} is zero
*/
public KalmanFilter(double initialEstimate, double initialErrorCovariance, double processNoise, double measurementNoise) {
requireFinite(initialEstimate, "initialEstimate");
requireNonNegativeVariance(initialErrorCovariance, "initialErrorCovariance");
requireNonNegativeVariance(processNoise, "processNoise");
requirePositiveVariance(measurementNoise, "measurementNoise");
this.initialEstimate = initialEstimate;
this.initialErrorCovariance = initialErrorCovariance;
this.processNoise = processNoise;
this.measurementNoise = measurementNoise;
reset();
}
/**
* Advances the model by one step without any control input, growing the uncertainty by the
* process noise.
*
* @return the predicted estimate, unchanged in value for this constant model
*/
public double predict() {
return predict(0.0);
}
/**
* Advances the model by one step, shifting the estimate by a known control input.
*
* @param controlInput the change the estimate is expected to undergo, e.g. velocity times the
* time step when tracking a position
* @return the predicted estimate
* @throws IllegalArgumentException if {@code controlInput} is not finite
*/
public double predict(double controlInput) {
requireFinite(controlInput, "controlInput");
estimate += controlInput;
errorCovariance += processNoise;
return estimate;
}
/**
* Corrects the estimate with a measurement taken by the default sensor.
*
* @param measurement the observed value
* @return the corrected estimate
* @throws IllegalArgumentException if {@code measurement} is not finite
*/
public double update(double measurement) {
return update(measurement, measurementNoise);
}
/**
* Corrects the estimate with a measurement whose noise differs from the default one. Calling this
* several times per step, once per sensor, is the whole of sensor fusion.
*
* @param measurement the observed value
* @param noise variance of this particular measurement, strictly positive
* @return the corrected estimate
* @throws IllegalArgumentException if {@code measurement} is not finite or {@code noise} is not strictly positive
*/
public double update(double measurement, double noise) {
requireFinite(measurement, "measurement");
requirePositiveVariance(noise, "noise");
double innovationVariance = errorCovariance + noise;
lastGain = errorCovariance / innovationVariance;
estimate += lastGain * (measurement - estimate);
// Algebraically this is (1 - gain) * p, but computing 1 - gain cancels away most of the
// significant digits whenever the gain is close to one, as it is on the first measurements.
errorCovariance = errorCovariance * noise / innovationVariance;
return estimate;
}
/**
* Runs one full cycle: predict, then correct with the given measurement.
*
* @param measurement the observed value
* @return the filtered estimate
* @throws IllegalArgumentException if {@code measurement} is not finite
*/
public double filter(double measurement) {
predict();
return update(measurement);
}
/**
* Filters a whole signal offline, one cycle per sample.
*
* @param measurements the noisy signal
* @return a new array holding the filtered signal, of the same length
* @throws IllegalArgumentException if any measurement is not finite
* @throws NullPointerException if {@code measurements} is {@code null}
*/
public double[] filter(double[] measurements) {
double[] filtered = new double[measurements.length];
for (int i = 0; i < measurements.length; i++) {
filtered[i] = filter(measurements[i]);
}
return filtered;
}
/**
* Combines simultaneous readings of the same quantity taken by independent sensors, weighting
* each by the inverse of its variance. This is the closed form of what repeated
* {@link #update(double, double)} calls achieve within one step.
*
* @param measurements one reading per sensor
* @param variances the noise variance of each sensor, strictly positive, same length as {@code measurements}
* @return the fused reading together with its variance, which is never larger than the variance of
* the best single sensor
* @throws IllegalArgumentException if the arrays are empty, differ in length, hold a non-finite
* measurement or a non-positive variance
* @throws NullPointerException if either array is {@code null}
*/
public static Estimate fuse(double[] measurements, double[] variances) {
if (measurements.length != variances.length) {
throw new IllegalArgumentException("There must be exactly one variance per measurement, but got " + measurements.length + " and " + variances.length);
}
if (measurements.length == 0) {
throw new IllegalArgumentException("At least one measurement is required");
}
double weightSum = 0.0;
double weightedSum = 0.0;
for (int i = 0; i < measurements.length; i++) {
requireFinite(measurements[i], "measurement");
requirePositiveVariance(variances[i], "variance");
double weight = 1.0 / variances[i];
weightSum += weight;
weightedSum += weight * measurements[i];
}
return new Estimate(weightedSum / weightSum, 1.0 / weightSum);
}
/**
* Returns the current estimate of the tracked quantity.
*
* @return the state estimate
*/
public double estimate() {
return estimate;
}
/**
* Returns the variance of the current estimate; it shrinks with every update and grows with every
* prediction.
*
* @return the error covariance
*/
public double errorCovariance() {
return errorCovariance;
}
/**
* Returns the Kalman gain used by the most recent update, a number in {@code [0, 1)} telling how
* much of that measurement was believed.
*
* @return the last gain, {@code 0} if no update has happened yet
*/
public double lastGain() {
return lastGain;
}
/**
* Returns the process noise variance.
*
* @return the value given at construction time
*/
public double processNoise() {
return processNoise;
}
/**
* Returns the default measurement noise variance.
*
* @return the value given at construction time
*/
public double measurementNoise() {
return measurementNoise;
}
/**
* Restores the state the filter had right after construction.
*/
public void reset() {
estimate = initialEstimate;
errorCovariance = initialErrorCovariance;
lastGain = 0.0;
}
@Override
public String toString() {
return "KalmanFilter{estimate=" + estimate + ", errorCovariance=" + errorCovariance + ", lastGain=" + lastGain + '}';
}
private static void requireFinite(double value, String name) {
if (!Double.isFinite(value)) {
throw new IllegalArgumentException("The " + name + " must be finite, but was " + value);
}
}
private static void requireNonNegativeVariance(double value, String name) {
if (!(value >= 0.0) || !Double.isFinite(value)) {
throw new IllegalArgumentException("The " + name + " must be finite and non-negative, but was " + value);
}
}
private static void requirePositiveVariance(double value, String name) {
if (!(value > 0.0) || !Double.isFinite(value)) {
throw new IllegalArgumentException("The " + name + " must be finite and strictly positive, but was " + value);
}
}
/**
* A value paired with the variance that describes how much it can be trusted.
*
* @param value the estimated quantity
* @param variance the variance of that estimate
*/
public record Estimate(double value, double variance) {
}
}