kalman-filter
Advanced tools
Comparing version 1.11.1 to 1.12.0
@@ -10,3 +10,3 @@ const matMul = require('../lib/linalgebra/mat-mul.js'); | ||
/** | ||
* @callback ObservationCallback | ||
* @callback PreviousCorrectedCallback | ||
* @param {Object} opts | ||
@@ -18,10 +18,3 @@ * @param {Number} opts.index | ||
/** | ||
* @typedef {Object} ObservationConfig | ||
* @property {Number} dimension | ||
* @property {Array.Array.<Number>> | ObservationCallback} stateProjection, | ||
* @property {Array.Array.<Number>> | ObservationCallback} covariance | ||
*/ | ||
/** | ||
* @callback DynamicCallback | ||
* @callback PredictedCallback | ||
* @param {Object} opts | ||
@@ -34,8 +27,23 @@ * @param {Number} opts.index | ||
/** | ||
* @typedef {Object} DynamicConfig | ||
* @typedef {Object} ObservationConfig | ||
* @property {Number} dimension | ||
* @property {Array.Array.<Number>> | DynamicCallback} transition, | ||
* @property {Array.Array.<Number>> | DynamicCallback} covariance | ||
* @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} [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 = { | ||
@@ -47,16 +55,8 @@ info: (...args) => console.log(...args), | ||
}; | ||
/** | ||
* @class | ||
* @property {DynamicConfig} dynamic the system's dynamic model | ||
* @property {ObservationConfig} observation the system's observation model | ||
*@property logger a Winston-like logger | ||
* @param {CoreConfig} options | ||
*/ | ||
class CoreKalmanFilter { | ||
/** | ||
* @param {DynamicConfig} dynamic | ||
* @param {ObservationConfig} observation the system's observation model | ||
*/ | ||
constructor({dynamic, observation, logger = defaultLogger}) { | ||
constructor(options) { | ||
const {dynamic, observation, logger = defaultLogger} = options; | ||
this.dynamic = dynamic; | ||
@@ -73,2 +73,3 @@ this.observation = observation; | ||
const {mean: meanInit, covariance: covarianceInit, index: indexInit} = this.dynamic.init; | ||
const initState = new State({ | ||
@@ -79,2 +80,3 @@ mean: meanInit, | ||
}); | ||
State.check(initState, {title: 'dynamic.init'}); | ||
return initState; | ||
@@ -94,6 +96,9 @@ } | ||
const getValueOptions = Object.assign({}, {previousCorrected, index}, options); | ||
const d = this.getValue(this.dynamic.transition, getValueOptions); | ||
const dTransposed = transpose(d); | ||
const covarianceInter = matMul(d, previousCorrected.covariance); | ||
const covariancePrevious = matMul(covarianceInter, dTransposed); | ||
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); | ||
@@ -110,2 +115,10 @@ | ||
predictMean({opts, transition}) { | ||
if (this.dynamic.fn) { | ||
return this.dynamic.fn(opts); | ||
} | ||
const {previousCorrected} = opts; | ||
return matMul(transition, previousCorrected.mean); | ||
} | ||
/** | ||
@@ -126,12 +139,10 @@ This will return the new prediction, relatively to the dynamic model chosen | ||
State.check(previousCorrected, {dimension: this.dynamic.dimension}); | ||
const getValueOptions = Object.assign({}, { | ||
const getValueOptions = Object.assign({}, options, { | ||
previousCorrected, | ||
index | ||
}, options); | ||
const d = this.getValue(this.dynamic.transition, getValueOptions); | ||
}); | ||
checkMatrix(d, [this.dynamic.dimension, this.dynamic.dimension], 'dynamic.transition'); | ||
const transition = this.getValue(this.dynamic.transition, getValueOptions); | ||
const mean = matMul(d, previousCorrected.mean); | ||
const mean = this.predictMean({transition, opts: getValueOptions}); | ||
@@ -158,4 +169,6 @@ const covariance = this.getPredictedCovariance(getValueOptions); | ||
checkMatrix(obsCovariance, [this.observation.dimension, this.observation.dimension], 'observation.covariance'); | ||
const stateProjTransposed = transpose(stateProjection); | ||
const stateProjTransposed = transpose(stateProjection); | ||
checkMatrix(stateProjection, [this.observation.dimension, this.dynamic.dimension], 'observation.stateProjection'); | ||
const noiselessInnovation = matMul( | ||
@@ -200,2 +213,11 @@ matMul(stateProjection, predicted.covariance), | ||
getPredictedObservation({opts, stateProjection}) { | ||
if (this.observation.fn) { | ||
return this.observation.fn(opts); | ||
} | ||
const {predicted} = opts; | ||
return matMul(stateProjection, predicted.mean); | ||
} | ||
/** | ||
@@ -223,4 +245,5 @@ This will return the new correction, taking into account the prediction made | ||
observation, | ||
matMul(stateProjection, predicted.mean) | ||
this.getPredictedObservation({stateProjection, opts: getValueOptions}) | ||
); | ||
const mean = add( | ||
@@ -227,0 +250,0 @@ predicted.mean, |
@@ -15,2 +15,14 @@ | ||
/** | ||
* @typedef {String} DynamicNonObjectConfig | ||
*/ | ||
/** | ||
* @typedef {DynamicConfig} DynamicObjectConfig | ||
* @property {String} name | ||
*/ | ||
/** | ||
* @param {DynamicNonObjectConfig} dynamic | ||
* @returns {DynamicObjectConfig} | ||
*/ | ||
const buildDefaultDynamic = function (dynamic) { | ||
@@ -24,2 +36,13 @@ if (typeof (dynamic) === 'string') { | ||
/** | ||
* @typedef {String | Number} ObservationNonObjectConfig | ||
*/ | ||
/** | ||
* @typedef {ObservationConfig} ObservationObjectConfig | ||
* @property {String} name | ||
*/ | ||
/** | ||
* @param {ObservationNonObjectConfig} observation | ||
* @returns {ObservationObjectConfig} | ||
*/ | ||
const buildDefaultObservation = function (observation) { | ||
@@ -40,4 +63,5 @@ if (typeof (observation) === 'number') { | ||
*is given, and initialize dynamic.init | ||
*@param {DynamicConfig} options.dynamic | ||
*@param {ObservationConfig} options.observation | ||
*@param {DynamicObjectConfig | DynamicNonObjectConfig} options.dynamic | ||
*@param {ObservationObjectConfig | ObservationNonObjectConfig} options.observation | ||
* @returns {CoreConfig} | ||
*/ | ||
@@ -69,7 +93,12 @@ | ||
/** | ||
*Returns the corresponding model without arrays as values but only functions | ||
*@param {ObservationConfig} observation | ||
*@param {DynamicConfig} dynamic | ||
*@returns {ObservationConfig, DynamicConfig} model with respect of the Core Kalman Filter properties | ||
* @typedef {Object} ModelsParameters | ||
* @property {DynamicObjectConfig} dynamic | ||
* @property {ObservationObjectConfig} observation | ||
*/ | ||
/** | ||
* Returns the corresponding model without arrays as values but only functions | ||
* @param {ModelsParameters} modelToBeChanged | ||
* @returns {CoreConfig} model with respect of the Core Kalman Filter properties | ||
*/ | ||
const modelsParametersToCoreOptions = function (modelToBeChanged) { | ||
@@ -79,8 +108,8 @@ const {observation, dynamic} = modelToBeChanged; | ||
observation: { | ||
stateProjection: toFunction(polymorphMatrix(observation.stateProjection)), | ||
covariance: toFunction(polymorphMatrix(observation.covariance, {dimension: observation.dimension})) | ||
stateProjection: toFunction(polymorphMatrix(observation.stateProjection), {label: 'observation.stateProjection'}), | ||
covariance: toFunction(polymorphMatrix(observation.covariance, {dimension: observation.dimension}), {label: 'observation.covariance'}) | ||
}, | ||
dynamic: { | ||
transition: toFunction(polymorphMatrix(dynamic.transition)), | ||
covariance: toFunction(polymorphMatrix(dynamic.covariance, {dimension: dynamic.dimension})) | ||
transition: toFunction(polymorphMatrix(dynamic.transition), {label: 'dynamic.transition'}), | ||
covariance: toFunction(polymorphMatrix(dynamic.covariance, {dimension: dynamic.dimension}), {label: 'dynamic.covariance'}) | ||
} | ||
@@ -92,5 +121,9 @@ }); | ||
/** | ||
* @param {DynamicConfig} options.dynamic | ||
* @param {ObservationConfig} options.observation the system's observation model | ||
* @typedef {Object} Config | ||
* @property {DynamicObjectConfig | DynamicNonObjectConfig} dynamic | ||
* @property {ObservationObjectConfig | ObservationNonObjectConfig} observation | ||
*/ | ||
/** | ||
* @param {Config} options | ||
*/ | ||
constructor(options = {}) { | ||
@@ -126,7 +159,3 @@ const modelsParameters = setupModelsParameters(options); | ||
filterAll(observations) { | ||
const {mean: meanInit, covariance: covarianceInit, index: indexInit} = this.dynamic.init; | ||
let previousCorrected = new State({ | ||
mean: meanInit, | ||
covariance: covarianceInit, | ||
index: indexInit}); | ||
let previousCorrected = this.getInitState(); | ||
const results = []; | ||
@@ -148,4 +177,5 @@ for (const observation of observations) { | ||
* in practice this can be used as a init.covariance value but is very costful calculation (that's why this is not made by default) | ||
* @param {Number} [limitIterations=1e2] max number of iterations | ||
* @param {Number} [tolerance=1e-6] returns when the last values differences are less than tolerance | ||
* @return {<Array.<Array.<Number>>>} covariance | ||
* @return {Array.<Array.<Number>>} covariance | ||
*/ | ||
@@ -178,3 +208,3 @@ asymptoticStateCovariance(limitIterations = 1e2, tolerance = 1e-6) { | ||
* @param {Number} [tolerance=1e-6] returns when the last values differences are less than tolerance | ||
* @return {<Array.<Array.<Number>>>} gain | ||
* @return {Array.<Array.<Number>>} gain | ||
*/ | ||
@@ -181,0 +211,0 @@ asymptoticGain(tolerance = 1e-6) { |
const elemWise = require('./elem-wise'); | ||
/** | ||
* Add matrixes together | ||
* @param {...<Array.<Array.<Number>>} args list of matrix | ||
* @param {...Array.<Array.<Number>>} args list of matrix | ||
* @returns {Array.<Array.<Number>>} sum | ||
@@ -6,0 +6,0 @@ */ |
/** | ||
* Multiply 2 matrixes together | ||
* @param {<Array.<Array.<Number>>} m1 | ||
* @param {<Array.<Array.<Number>>} m2 | ||
* @param {Array.<Array.<Number>>} m1 | ||
* @param {Array.<Array.<Number>>} m2 | ||
* @returns {Array.<Array.<Number>>} | ||
@@ -6,0 +6,0 @@ */ |
@@ -7,3 +7,3 @@ const diag = require('../linalgebra/diag.js'); | ||
*@param {DynamicConfig} dynamic | ||
*@returns {ObservationConfig, DynamicConfig} | ||
*@returns {CoreConfig} | ||
*/ | ||
@@ -30,3 +30,7 @@ | ||
if (dynamic.init && !dynamic.init.mean) { | ||
throw (new Error('dynamic.init should have a mean key')); | ||
} | ||
return {observation, dynamic}; | ||
}; |
@@ -13,3 +13,2 @@ const sub = require('./linalgebra/sub.js'); | ||
/** | ||
* @class | ||
* Class representing a multi dimensionnal gaussian, with his mean and his covariance | ||
@@ -29,2 +28,5 @@ * @property {Number} [index=0] the index of the State in the process, this is not mandatory for simple Kalman Filter, but is needed for most of the use case of extended kalman filter | ||
* Check the consistency of the State | ||
* @param {Object} options | ||
* @returns {Null} | ||
* @see check | ||
*/ | ||
@@ -37,2 +39,8 @@ check(options) { | ||
* Check the consistency of the State's attributes | ||
* @param {State} state | ||
* @param {Object} [options={}] | ||
* @param {Array} [options.dimension=null] if defined check the dimension of the state | ||
* @param {String} [options.title=null] used to log error mor explicitly | ||
* @param {Boolean} options.eigen | ||
* @returns {Null} | ||
*/ | ||
@@ -54,5 +62,5 @@ | ||
checkMatrix(mean, [meanDimension, 1], title ? title + '-mean' : 'mean'); | ||
checkMatrix(covariance, [meanDimension, meanDimension], title ? title + '-covariance' : 'covariance'); | ||
checkCovariance({covariance, eigen}, title ? title + '-covariance' : 'covariance'); | ||
checkMatrix(mean, [meanDimension, 1], title ? title + '.mean' : 'mean'); | ||
checkMatrix(covariance, [meanDimension, meanDimension], title ? title + '.covariance' : 'covariance'); | ||
checkCovariance({covariance, eigen}, title ? title + '.covariance' : 'covariance'); | ||
// If (typeof (index) !== 'number') { | ||
@@ -63,2 +71,8 @@ // throw (new TypeError('t must be a number')); | ||
/** | ||
* Multiply state with matrix | ||
* @param {State} state | ||
* @param {Array.<Array.<Number>>} matrix | ||
* @returns {State} | ||
*/ | ||
static matMul({state, matrix}) { | ||
@@ -95,4 +109,11 @@ const covariance = matMul( | ||
/** | ||
* @typedef {Object} DetailedMahalanobis | ||
* @property {Array.<[Number]>} diff | ||
* @property {Array.<Array.<Number>>} covarianceInvert | ||
* @property {Number} value | ||
*/ | ||
/** | ||
* Simple Malahanobis distance between the distribution (this) and a point | ||
* @param {Array.<[Number]>} point a Nx1 matrix representing a point | ||
* @returns {DetailedMahalanobis} | ||
*/ | ||
@@ -145,2 +166,3 @@ rawDetailedMahalanobis(point) { | ||
* @param {Array.<Number>} obsIndexes list of indexes of observation state to use for the mahalanobis distance | ||
* @returns {DetailedMahalanobis} | ||
*/ | ||
@@ -167,5 +189,3 @@ detailedMahalanobis({kf, observation, obsIndexes}) { | ||
/** | ||
* @param {KalmanFilter} kf kalman filter use to project the state in observation's space | ||
* @param {Observation} observation | ||
* @param {Array.<Number>} obsIndexes list of indexes of observation state to use for the mahalanobis distance | ||
* @param {Object} options @see detailedMahalanobis | ||
* @returns {Number} | ||
@@ -172,0 +192,0 @@ */ |
const checkShape = require('./check-shape'); | ||
module.exports = function (matrix, shape, title = 'checkMatrix') { | ||
if (!Array.isArray(matrix)) { | ||
throw (new TypeError(`[${title}] should be a 2-level array matrix and is ${matrix}`)); | ||
} | ||
matrix.forEach(row => { | ||
if (!Array.isArray(row)) { | ||
throw (new TypeError(`[${title}] 1-level array should be a matrix ${JSON.stringify(matrix)}`)); | ||
} | ||
}); | ||
if (matrix.reduce((a, b) => a.concat(b)).filter(a => Number.isNaN(a)).length > 0) { | ||
@@ -5,0 +14,0 @@ throw (new Error( |
@@ -6,5 +6,6 @@ const uniq = require('./uniq.js'); | ||
/** | ||
*Equivalent to the Object.assign methode, takes several arguments and creates a new object corresponding to the assignment of the arguments | ||
*Equivalent to the Object.assign method, takes several arguments and creates a new object corresponding to the assignment of the arguments | ||
* @param {Object} args | ||
* @param {Number} step | ||
* @returns {Object} | ||
*/ | ||
@@ -11,0 +12,0 @@ const deepAssign = function (args, step) { |
@@ -14,9 +14,9 @@ /** | ||
*/ | ||
module.exports = function (array, {dimension, title = 'polymorph'} = {}) { | ||
if (typeof (array) === 'number' || Array.isArray(array)) { | ||
if (typeof (array) === 'number' && typeof (dimension) === 'number') { | ||
return diag(new Array(dimension).fill(array)); | ||
module.exports = function (cov, {dimension, title = 'polymorph'} = {}) { | ||
if (typeof (cov) === 'number' || Array.isArray(cov)) { | ||
if (typeof (cov) === 'number' && typeof (dimension) === 'number') { | ||
return diag(new Array(dimension).fill(cov)); | ||
} | ||
if ((Array.isArray(array)) && (Array.isArray(array[0]))) { | ||
if ((Array.isArray(cov)) && (Array.isArray(cov[0]))) { | ||
let shape; | ||
@@ -27,12 +27,12 @@ if (typeof (dimension) === 'number') { | ||
checkMatrix(array, shape, title); | ||
return array; | ||
checkMatrix(cov, shape, title); | ||
return cov; | ||
} | ||
if ((Array.isArray(array)) && (typeof (array[0]) === 'number')) { | ||
return diag(array); | ||
if ((Array.isArray(cov)) && (typeof (cov[0]) === 'number')) { | ||
return diag(cov); | ||
} | ||
} | ||
return array; | ||
return cov; | ||
}; |
@@ -16,3 +16,3 @@ // Const diag = require('../linalgebra/diag.js'); | ||
module.exports = function (array) { | ||
module.exports = function (array, {label = null} = {}) { | ||
if (typeof (array) === 'function') { | ||
@@ -26,3 +26,3 @@ return array; | ||
throw (new Error('Only arrays and functions are authorized')); | ||
throw (new Error(`${label === null ? '' : label + ' : '}Only arrays and functions are authorized (got: "${array}")`)); | ||
}; |
{ | ||
"name": "kalman-filter", | ||
"version": "1.11.1", | ||
"version": "1.12.0", | ||
"description": "Kalman filter (and Extended Kalman Filter) Multi-dimensional implementation in Javascript", | ||
@@ -11,3 +11,3 @@ "main": "index.js", | ||
"serve-demo": "http-server docs/", | ||
"build": "webpack" | ||
"build": "NODE_OPTIONS=--openssl-legacy-provider webpack" | ||
}, | ||
@@ -14,0 +14,0 @@ "repository": { |
@@ -366,3 +366,3 @@ ![Kalman Filter Gif](./demo/demo.gif) | ||
### Extended Kalman Filter | ||
## Play with Kalman Filter | ||
@@ -379,3 +379,2 @@ In order to use the Kalman-Filter with a dynamic or observation model which is not strictly a [General linear model](https://en.wikipedia.org/wiki/General_linear_model), it is possible to use `function` in following parameters : | ||
```js | ||
@@ -451,3 +450,13 @@ const {KalmanFilter} = require('kalman-filter'); | ||
``` | ||
### Extended | ||
If you want to implement an [extended kalman filter](https://en.wikipedia.org/wiki/Extended_Kalman_filter) | ||
You will need to put your non-linear functions in the following parameters | ||
* `observation.fn` | ||
* `dynamic.fn` | ||
See an example in `test/issues/56.js` | ||
## Use your kalman filter | ||
@@ -463,3 +472,3 @@ | ||
``` | ||
### Online usage (run it online, forward step only) | ||
### Online filter | ||
@@ -487,3 +496,3 @@ When using online usage (only the forward step), the output of the `filter` method is an instance of the ["State"](/lib/state.js) class. | ||
observations.forEach(observation => { | ||
const predictedState = kFilter.predict({ | ||
const predicted = kFilter.predict({ | ||
previousCorrected | ||
@@ -621,3 +630,3 @@ }); | ||
observations.forEach(observation => { | ||
const predictedState = kFilter.predict({ | ||
const predicted = kFilter.predict({ | ||
previousCorrected | ||
@@ -653,3 +662,3 @@ }); | ||
observations.forEach(observation => { | ||
const predictedState = kFilter.predict({ | ||
const predicted = kFilter.predict({ | ||
previousCorrected | ||
@@ -656,0 +665,0 @@ }); |
70103
1322
681