blob: 81b180737c7c4178e9eb9255c77778c297431530 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.math4.filter;
import org.apache.commons.math4.exception.DimensionMismatchException;
import org.apache.commons.math4.exception.NullArgumentException;
import org.apache.commons.math4.linear.Array2DRowRealMatrix;
import org.apache.commons.math4.linear.ArrayRealVector;
import org.apache.commons.math4.linear.CholeskyDecomposition;
import org.apache.commons.math4.linear.MatrixDimensionMismatchException;
import org.apache.commons.math4.linear.MatrixUtils;
import org.apache.commons.math4.linear.NonSquareMatrixException;
import org.apache.commons.math4.linear.RealMatrix;
import org.apache.commons.math4.linear.RealVector;
import org.apache.commons.math4.linear.SingularMatrixException;
import org.apache.commons.math4.util.MathUtils;
/**
* Implementation of a Kalman filter to estimate the state <i>x<sub>k</sub></i>
* of a discrete-time controlled process that is governed by the linear
* stochastic difference equation:
*
* <pre>
* <i>x<sub>k</sub></i> = <b>A</b><i>x<sub>k-1</sub></i> + <b>B</b><i>u<sub>k-1</sub></i> + <i>w<sub>k-1</sub></i>
* </pre>
*
* with a measurement <i>x<sub>k</sub></i> that is
*
* <pre>
* <i>z<sub>k</sub></i> = <b>H</b><i>x<sub>k</sub></i> + <i>v<sub>k</sub></i>.
* </pre>
*
* <p>
* The random variables <i>w<sub>k</sub></i> and <i>v<sub>k</sub></i> represent
* the process and measurement noise and are assumed to be independent of each
* other and distributed with normal probability (white noise).
* <p>
* The Kalman filter cycle involves the following steps:
* <ol>
* <li>predict: project the current state estimate ahead in time</li>
* <li>correct: adjust the projected estimate by an actual measurement</li>
* </ol>
* <p>
* The Kalman filter is initialized with a {@link ProcessModel} and a
* {@link MeasurementModel}, which contain the corresponding transformation and
* noise covariance matrices. The parameter names used in the respective models
* correspond to the following names commonly used in the mathematical
* literature:
* <ul>
* <li>A - state transition matrix</li>
* <li>B - control input matrix</li>
* <li>H - measurement matrix</li>
* <li>Q - process noise covariance matrix</li>
* <li>R - measurement noise covariance matrix</li>
* <li>P - error covariance matrix</li>
* </ul>
*
* @see <a href="http://www.cs.unc.edu/~welch/kalman/">Kalman filter
* resources</a>
* @see <a href="http://www.cs.unc.edu/~welch/media/pdf/kalman_intro.pdf">An
* introduction to the Kalman filter by Greg Welch and Gary Bishop</a>
* @see <a href="http://academic.csuohio.edu/simond/courses/eec644/kalman.pdf">
* Kalman filter example by Dan Simon</a>
* @see ProcessModel
* @see MeasurementModel
* @since 3.0
*/
public class KalmanFilter {
/** The process model used by this filter instance. */
private final ProcessModel processModel;
/** The measurement model used by this filter instance. */
private final MeasurementModel measurementModel;
/** The transition matrix, equivalent to A. */
private RealMatrix transitionMatrix;
/** The transposed transition matrix. */
private RealMatrix transitionMatrixT;
/** The control matrix, equivalent to B. */
private RealMatrix controlMatrix;
/** The measurement matrix, equivalent to H. */
private RealMatrix measurementMatrix;
/** The transposed measurement matrix. */
private RealMatrix measurementMatrixT;
/** The internal state estimation vector, equivalent to x hat. */
private RealVector stateEstimation;
/** The error covariance matrix, equivalent to P. */
private RealMatrix errorCovariance;
/**
* Creates a new Kalman filter with the given process and measurement models.
*
* @param process
* the model defining the underlying process dynamics
* @param measurement
* the model defining the given measurement characteristics
* @throws NullArgumentException
* if any of the given inputs is null (except for the control matrix)
* @throws NonSquareMatrixException
* if the transition matrix is non square
* @throws DimensionMismatchException
* if the column dimension of the transition matrix does not match the dimension of the
* initial state estimation vector
* @throws MatrixDimensionMismatchException
* if the matrix dimensions do not fit together
*/
public KalmanFilter(final ProcessModel process, final MeasurementModel measurement)
throws NullArgumentException, NonSquareMatrixException, DimensionMismatchException,
MatrixDimensionMismatchException {
MathUtils.checkNotNull(process);
MathUtils.checkNotNull(measurement);
this.processModel = process;
this.measurementModel = measurement;
transitionMatrix = processModel.getStateTransitionMatrix();
MathUtils.checkNotNull(transitionMatrix);
transitionMatrixT = transitionMatrix.transpose();
// create an empty matrix if no control matrix was given
if (processModel.getControlMatrix() == null) {
controlMatrix = new Array2DRowRealMatrix();
} else {
controlMatrix = processModel.getControlMatrix();
}
measurementMatrix = measurementModel.getMeasurementMatrix();
MathUtils.checkNotNull(measurementMatrix);
measurementMatrixT = measurementMatrix.transpose();
// check that the process and measurement noise matrices are not null
// they will be directly accessed from the model as they may change
// over time
RealMatrix processNoise = processModel.getProcessNoise();
MathUtils.checkNotNull(processNoise);
RealMatrix measNoise = measurementModel.getMeasurementNoise();
MathUtils.checkNotNull(measNoise);
// set the initial state estimate to a zero vector if it is not
// available from the process model
if (processModel.getInitialStateEstimate() == null) {
stateEstimation = new ArrayRealVector(transitionMatrix.getColumnDimension());
} else {
stateEstimation = processModel.getInitialStateEstimate();
}
if (transitionMatrix.getColumnDimension() != stateEstimation.getDimension()) {
throw new DimensionMismatchException(transitionMatrix.getColumnDimension(),
stateEstimation.getDimension());
}
// initialize the error covariance to the process noise if it is not
// available from the process model
if (processModel.getInitialErrorCovariance() == null) {
errorCovariance = processNoise.copy();
} else {
errorCovariance = processModel.getInitialErrorCovariance();
}
// sanity checks, the control matrix B may be null
// A must be a square matrix
if (!transitionMatrix.isSquare()) {
throw new NonSquareMatrixException(
transitionMatrix.getRowDimension(),
transitionMatrix.getColumnDimension());
}
// row dimension of B must be equal to A
// if no control matrix is available, the row and column dimension will be 0
if (controlMatrix != null &&
controlMatrix.getRowDimension() > 0 &&
controlMatrix.getColumnDimension() > 0 &&
controlMatrix.getRowDimension() != transitionMatrix.getRowDimension()) {
throw new MatrixDimensionMismatchException(controlMatrix.getRowDimension(),
controlMatrix.getColumnDimension(),
transitionMatrix.getRowDimension(),
controlMatrix.getColumnDimension());
}
// Q must be equal to A
MatrixUtils.checkAdditionCompatible(transitionMatrix, processNoise);
// column dimension of H must be equal to row dimension of A
if (measurementMatrix.getColumnDimension() != transitionMatrix.getRowDimension()) {
throw new MatrixDimensionMismatchException(measurementMatrix.getRowDimension(),
measurementMatrix.getColumnDimension(),
measurementMatrix.getRowDimension(),
transitionMatrix.getRowDimension());
}
// row dimension of R must be equal to row dimension of H
if (measNoise.getRowDimension() != measurementMatrix.getRowDimension()) {
throw new MatrixDimensionMismatchException(measNoise.getRowDimension(),
measNoise.getColumnDimension(),
measurementMatrix.getRowDimension(),
measNoise.getColumnDimension());
}
}
/**
* Returns the dimension of the state estimation vector.
*
* @return the state dimension
*/
public int getStateDimension() {
return stateEstimation.getDimension();
}
/**
* Returns the dimension of the measurement vector.
*
* @return the measurement vector dimension
*/
public int getMeasurementDimension() {
return measurementMatrix.getRowDimension();
}
/**
* Returns the current state estimation vector.
*
* @return the state estimation vector
*/
public double[] getStateEstimation() {
return stateEstimation.toArray();
}
/**
* Returns a copy of the current state estimation vector.
*
* @return the state estimation vector
*/
public RealVector getStateEstimationVector() {
return stateEstimation.copy();
}
/**
* Returns the current error covariance matrix.
*
* @return the error covariance matrix
*/
public double[][] getErrorCovariance() {
return errorCovariance.getData();
}
/**
* Returns a copy of the current error covariance matrix.
*
* @return the error covariance matrix
*/
public RealMatrix getErrorCovarianceMatrix() {
return errorCovariance.copy();
}
/**
* Predict the internal state estimation one time step ahead.
*/
public void predict() {
predict((RealVector) null);
}
/**
* Predict the internal state estimation one time step ahead.
*
* @param u
* the control vector
* @throws DimensionMismatchException
* if the dimension of the control vector does not fit
*/
public void predict(final double[] u) throws DimensionMismatchException {
predict(new ArrayRealVector(u, false));
}
/**
* Predict the internal state estimation one time step ahead.
*
* @param u
* the control vector
* @throws DimensionMismatchException
* if the dimension of the control vector does not match
*/
public void predict(final RealVector u) throws DimensionMismatchException {
// sanity checks
if (u != null &&
u.getDimension() != controlMatrix.getColumnDimension()) {
throw new DimensionMismatchException(u.getDimension(),
controlMatrix.getColumnDimension());
}
// project the state estimation ahead (a priori state)
// xHat(k)- = A * xHat(k-1) + B * u(k-1)
stateEstimation = transitionMatrix.operate(stateEstimation);
// add control input if it is available
if (u != null) {
stateEstimation = stateEstimation.add(controlMatrix.operate(u));
}
// project the error covariance ahead
// P(k)- = A * P(k-1) * A' + Q
errorCovariance = transitionMatrix.multiply(errorCovariance)
.multiply(transitionMatrixT)
.add(processModel.getProcessNoise());
}
/**
* Correct the current state estimate with an actual measurement.
*
* @param z
* the measurement vector
* @throws NullArgumentException
* if the measurement vector is {@code null}
* @throws DimensionMismatchException
* if the dimension of the measurement vector does not fit
* @throws SingularMatrixException
* if the covariance matrix could not be inverted
*/
public void correct(final double[] z)
throws NullArgumentException, DimensionMismatchException, SingularMatrixException {
correct(new ArrayRealVector(z, false));
}
/**
* Correct the current state estimate with an actual measurement.
*
* @param z
* the measurement vector
* @throws NullArgumentException
* if the measurement vector is {@code null}
* @throws DimensionMismatchException
* if the dimension of the measurement vector does not fit
* @throws SingularMatrixException
* if the covariance matrix could not be inverted
*/
public void correct(final RealVector z)
throws NullArgumentException, DimensionMismatchException, SingularMatrixException {
// sanity checks
MathUtils.checkNotNull(z);
if (z.getDimension() != measurementMatrix.getRowDimension()) {
throw new DimensionMismatchException(z.getDimension(),
measurementMatrix.getRowDimension());
}
// S = H * P(k) * H' + R
RealMatrix s = measurementMatrix.multiply(errorCovariance)
.multiply(measurementMatrixT)
.add(measurementModel.getMeasurementNoise());
// Inn = z(k) - H * xHat(k)-
RealVector innovation = z.subtract(measurementMatrix.operate(stateEstimation));
// calculate gain matrix
// K(k) = P(k)- * H' * (H * P(k)- * H' + R)^-1
// K(k) = P(k)- * H' * S^-1
// instead of calculating the inverse of S we can rearrange the formula,
// and then solve the linear equation A x X = B with A = S', X = K' and B = (H * P)'
// K(k) * S = P(k)- * H'
// S' * K(k)' = H * P(k)-'
RealMatrix kalmanGain = new CholeskyDecomposition(s).getSolver()
.solve(measurementMatrix.multiply(errorCovariance.transpose()))
.transpose();
// update estimate with measurement z(k)
// xHat(k) = xHat(k)- + K * Inn
stateEstimation = stateEstimation.add(kalmanGain.operate(innovation));
// update covariance of prediction error
// P(k) = (I - K * H) * P(k)-
RealMatrix identity = MatrixUtils.createRealIdentityMatrix(kalmanGain.getRowDimension());
errorCovariance = identity.subtract(kalmanGain.multiply(measurementMatrix)).multiply(errorCovariance);
}
}