UNPKG

kalman-filter

Version:

Kalman filter (and Extended Kalman Filter) Multi-dimensional implementation in Javascript

267 lines (220 loc) 8.95 kB
const {matMul, transpose, add, invert, subtract: sub, identity: getIdentity} = require('simple-linalg'); const State = require('./state.js'); const checkMatrix = require('./utils/check-matrix.js'); /** * @callback PreviousCorrectedCallback * @param {Object} opts * @param {Number} opts.index * @param {Number} opts.previousCorrected * @returns {Array.Array.<Number>>} */ /** * @callback PredictedCallback * @param {Object} opts * @param {Number} opts.index * @param {State} opts.predicted * @param {Observation} opts.observation * @returns {Array.Array.<Number>>} */ /** * @typedef {Object} ObservationConfig * @property {Number} dimension * @property {PredictedCallback} [fn=null] for extended kalman filter only, the non-linear state to observation function * @property {Array.Array.<Number>> | PreviousCorrectedCallback} stateProjection the matrix to transform state to observation (for EKF, the jacobian of the fn) * @property {Array.Array.<Number>> | PreviousCorrectedCallback} covariance the covariance of the observation noise */ /** * @typedef {Object} DynamicConfig * @property {Number} dimension dimension of the state vector * @property {PreviousCorrectedCallback} [constant=null] a function that returns the control parameter B_k*u_k of the kalman filter * @property {PreviousCorrectedCallback} [fn=null] for extended kalman filter only, the non-linear state-transition model * @property {Array.Array.<Number>> | PredictedCallback} transition the state-transition model (or for EKF the jacobian of the fn) * @property {Array.Array.<Number>> | PredictedCallback} covariance the covariance of the process noise */ /** * @typedef {Object} CoreConfig * @property {DynamicConfig} dynamic the system's dynamic model * @property {ObservationConfig} observation the system's observation model * @property {Object} [logger=defaultLogger] a Winston-like logger */ const defaultLogger = { info: (...args) => console.log(...args), debug() {}, warn: (...args) => console.log(...args), error: (...args) => console.log(...args), }; /** * @param {CoreConfig} options */ class CoreKalmanFilter { constructor(options) { const {dynamic, observation, logger = defaultLogger} = options; this.dynamic = dynamic; this.observation = observation; this.logger = logger; } getValue(fn, options) { return (typeof (fn) === 'function' ? fn(options) : fn); } getInitState() { const {mean: meanInit, covariance: covarianceInit, index: indexInit} = this.dynamic.init; const initState = new State({ mean: meanInit, covariance: covarianceInit, index: indexInit, }); State.check(initState, {title: 'dynamic.init'}); return initState; } /** This will return the predicted covariance of a given previousCorrected State, this will help us to build the asymptoticState. * @param {State} previousCorrected * @returns{Array.<Array.<Number>>} */ getPredictedCovariance(options = {}) { let {previousCorrected, index} = options; previousCorrected = previousCorrected || this.getInitState(); const getValueOptions = Object.assign({}, {previousCorrected, index}, options); const transition = this.getValue(this.dynamic.transition, getValueOptions); checkMatrix(transition, [this.dynamic.dimension, this.dynamic.dimension], 'dynamic.transition'); const transitionTransposed = transpose(transition); const covarianceInter = matMul(transition, previousCorrected.covariance); const covariancePrevious = matMul(covarianceInter, transitionTransposed); const dynCov = this.getValue(this.dynamic.covariance, getValueOptions); const covariance = add( dynCov, covariancePrevious, ); checkMatrix(covariance, [this.dynamic.dimension, this.dynamic.dimension], 'predicted.covariance'); return covariance; } predictMean(o) { const mean = this.predictMeanWithoutControl(o); if (!this.dynamic.constant) { return mean; } const {opts} = o; const control = this.dynamic.constant(opts); checkMatrix(control, [this.dynamic.dimension, 1], 'dynamic.constant'); return add(mean, control); } predictMeanWithoutControl({opts, transition}) { if (this.dynamic.fn) { return this.dynamic.fn(opts); } const {previousCorrected} = opts; return matMul(transition, previousCorrected.mean); } /** This will return the new prediction, relatively to the dynamic model chosen * @param {State} previousCorrected State relative to our dynamic model * @returns{State} predicted State */ predict(options = {}) { let {previousCorrected, index} = options; previousCorrected = previousCorrected || this.getInitState(); if (typeof (index) !== 'number' && typeof (previousCorrected.index) === 'number') { index = previousCorrected.index + 1; } State.check(previousCorrected, {dimension: this.dynamic.dimension}); const getValueOptions = Object.assign({}, options, { previousCorrected, index, }); const transition = this.getValue(this.dynamic.transition, getValueOptions); const mean = this.predictMean({transition, opts: getValueOptions}); const covariance = this.getPredictedCovariance(getValueOptions); const predicted = new State({mean, covariance, index}); this.logger.debug('Prediction done', predicted); if (Number.isNaN(predicted.mean[0][0])) { throw (new TypeError('nan')); } return predicted; } /** This will return the new correction, taking into account the prediction made and the observation of the sensor * @param {State} predicted the previous State * @returns{Array<Array>} kalmanGain */ getGain(options) { let {predicted, stateProjection} = options; const getValueOptions = Object.assign({}, {index: predicted.index}, options); stateProjection = stateProjection || this.getValue(this.observation.stateProjection, getValueOptions); const obsCovariance = this.getValue(this.observation.covariance, getValueOptions); checkMatrix(obsCovariance, [this.observation.dimension, this.observation.dimension], 'observation.covariance'); const stateProjTransposed = transpose(stateProjection); checkMatrix(stateProjection, [this.observation.dimension, this.dynamic.dimension], 'observation.stateProjection'); const noiselessInnovation = matMul( matMul(stateProjection, predicted.covariance), stateProjTransposed, ); const innovationCovariance = add(noiselessInnovation, obsCovariance); const optimalKalmanGain = matMul( matMul(predicted.covariance, stateProjTransposed), invert(innovationCovariance), ); return optimalKalmanGain; } /** This will return the corrected covariance of a given predicted State, this will help us to build the asymptoticState. * @param {State} predicted the previous State * @returns{Array.<Array.<Number>>} */ getCorrectedCovariance(options) { let {predicted, optimalKalmanGain, stateProjection} = options; const identity = getIdentity(predicted.covariance.length); if (!stateProjection) { const getValueOptions = Object.assign({}, {index: predicted.index}, options); stateProjection = this.getValue(this.observation.stateProjection, getValueOptions); } if (!optimalKalmanGain) { optimalKalmanGain = this.getGain(Object.assign({stateProjection}, options)); } return matMul( sub(identity, matMul(optimalKalmanGain, stateProjection)), predicted.covariance, ); } getPredictedObservation({opts, stateProjection}) { if (this.observation.fn) { return this.observation.fn(opts); } const {predicted} = opts; return matMul(stateProjection, predicted.mean); } /** This will return the new correction, taking into account the prediction made and the observation of the sensor * @param {State} predicted the previous State * @param {Array} observation the observation of the sensor * @returns{State} corrected State of the Kalman Filter */ correct(options) { const {predicted, observation} = options; State.check(predicted, {dimension: this.dynamic.dimension}); if (!observation) { throw (new Error('no measure available')); } const getValueOptions = Object.assign({}, {observation, predicted, index: predicted.index}, options); const stateProjection = this.getValue(this.observation.stateProjection, getValueOptions); const optimalKalmanGain = this.getGain(Object.assign({predicted, stateProjection}, options)); const innovation = sub( observation, this.getPredictedObservation({stateProjection, opts: getValueOptions}), ); const mean = add( predicted.mean, matMul(optimalKalmanGain, innovation), ); if (Number.isNaN(mean[0][0])) { console.log({optimalKalmanGain, innovation, predicted}); throw (new TypeError('Mean is NaN after correction')); } const covariance = this.getCorrectedCovariance(Object.assign({predicted, optimalKalmanGain, stateProjection}, options)); const corrected = new State({mean, covariance, index: predicted.index}); this.logger.debug('Correction done', corrected); return corrected; } } module.exports = CoreKalmanFilter;