kalman-filter

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

Usage no npm install needed!

<script type="module">
  import kalmanFilter from 'https://cdn.skypack.dev/kalman-filter';
</script>

README

Kalman Filter Gif

kalman-filter

Kalman Filter in JavaScript (for both node.js and the browser)

This library implements following features:

  • N-dimensional Kalman Filter (for multivariate Gaussian)
  • Forward Kalman Filter (Online)
  • Forward-Backward Smoothing Kalman Filter
  • Split Prediction/Correction steps
  • Extended Kalman Filter (when using functions for dynamics and observation matrixes)
  • Correlation Matrix

Demos

Open an issue to add more examples in this section explaining how you use this library !

Installation

Npm

npm install kalman-filter
const {KalmanFilter} = require('kalman-filter');

Browser usage

Download the file kalman-filter.min.js from Releases page

Add it to your project like :

<script src="dist/kalman-filter.min.js"></script>
<script>
var {KalmanFilter} = kalmanFilter;

// ... do whatever you want with KalmanFilter

</script>

Simple Example

1D Smoothing Usage

const {KalmanFilter} = require('kalman-filter');

const observations = [0, 0.1, 0.5, 0.2, 3, 4, 2, 1, 2, 3, 5, 6];
// this is creating a smoothing
const kFilter = new KalmanFilter();
const res = kFilter.filterAll(observations)
// res is a list of list (for multidimensional filters)
// [
//   [ 0 ],
//   [ 0.06666665555510715 ],
//   [ 0.3374999890620582 ],
//   [ 0.25238094852592136 ],
//   [ 1.9509090885288296 ],
//   [ 3.2173611101031616 ],
//   [ 2.4649867370240965 ],
//   [ 1.5595744679428254 ],
//   [ 1.831772445766021 ],
//   [ 2.5537767922925685 ],
//   [ 4.065625882212133 ],
//   [ 5.26113483436549 ]
// ]

Result is :

Kalman Filter 1d example

2D Smoothing Usage

const {KalmanFilter} = require('kalman-filter');

const observations = [[0, 1], [0.1, 0.5], [0.2, 3], [4, 2], [1, 2]];
const kFilter = new KalmanFilter({observation: 2});
// equivalent to
// new KalmanFilter({
// 	observation: {
// 		name: 'sensor',
// 		sensorDimension: 2
// 	}
// });
const res = kFilter.filterAll(observations)

2D Smoothing with constant-speed model

const {KalmanFilter} = require('kalman-filter');

const observations = [[0, 1], [0.1, 0.5], [0.2, 3], [4, 2], [1, 2]];
const kFilter = new KalmanFilter({
    observation: 2,
    dynamic: 'constant-speed'
});
// equivalent to
// new KalmanFilter({
// 	observation: {
// 		name: 'sensor',
// 		sensorDimension: 2
// 	},
// 	dynamic: {
// 		name: 'constant-speed'
// 	},
// });
const res = kFilter.filterAll(observations)

How to instantiate your kalman filter

Configure the dynamic with dynamic.name

dynamic.name is a shortcut to configure commonly use models as :

  • constant-position
  • constant-speed
  • constant-acceleration

You can also register your own shortcust see Register models shortcuts

'constant-position' on 2D data

This is the default behavior

const {KalmanFilter} = require('kalman-filter');

const kFilter = new KalmanFilter({
    observation: {
        sensorDimension: 2,
        name: 'sensor'
    },
    dynamic: {
        name: 'constant-position',// observation.sensorDimension == dynamic.dimension
        covariance: [3, 4]// equivalent to diag([3, 4])
    }
});

'constant-speed' on 3D data

const {KalmanFilter} = require('kalman-filter');

const kFilter = new KalmanFilter({
    observation: {
        sensorDimension: 3,
        name: 'sensor'
    },
    dynamic: {
        name: 'constant-speed',// observation.sensorDimension * 2 == state.dimension
        timeStep: 0.1,
        covariance: [3, 3, 3, 4, 4, 4]// equivalent to diag([3, 3, 3, 4, 4, 4])
    }
});

'constant-acceleration' on 2D data

const {KalmanFilter} = require('kalman-filter');

const kFilter = new KalmanFilter({
    observation: {
        sensorDimension: 2,
        name: 'sensor'
    },
    dynamic: {
        name: 'constant-acceleration',// observation.sensorDimension * 3 == state.dimension
        timeStep: 0.1,
        covariance: [3, 3, 4, 4, 5, 5]// equivalent to diag([3, 3, 4, 4, 5, 5])
    }
});

Instanciation of a generic linear model

This is an example of how to build a constant speed model, in 3D without dynamic.name, using detailed api.

  • dynamic.dimension is the size of the state
  • dynamic.transition is the state transition model that defines the dynamic of the system
  • dynamic.covariance is the covariance matrix of the transition model
  • dynamic.init is used for initial state (we generally set a big covariance on it)
const {KalmanFilter} = require('kalman-filter');

const timeStep = 0.1;

const huge = 1e8;

const kFilter = new KalmanFilter({
    observation: {
        dimension: 3
    },
    dynamic: {
        init: {
            // We just use random-guessed values here that seems reasonable
            mean: [[500], [500], [500], [0], [0], [0]],
            // We init the dynamic model with a huge covariance cause we don't
            // have any idea where my modeled object before the first observation is located
            covariance: [
                [huge, 0, 0, 0, 0, 0],
                [0, huge, 0, 0, 0, 0],
                [0, 0, huge, 0, 0, 0],
                [0, 0, 0, huge, 0, 0],
                [0, 0, 0, 0, huge, 0],
                [0, 0, 0, 0, 0, huge],
            ],
        },
        // Corresponds to (x, y, z, vx, vy, vz)
        dimension: 6,
        // This is a constant-speed model on 3D : [ [Id , timeStep*Id], [0, Id]]
        transition: [
            [1, 0, 0, timeStep, 0, 0],
            [0, 1, 0, 0, timeStep, 0],
            [0, 0, 1, 0, 0, timeStep],
            [0, 0, 0, 1, 0, 0],
            [0, 0, 0, 0, 1, 0],
            [0, 0, 0, 0, 0, 1]
        ],
        // Diagonal covariance for independant variables
        // since timeStep = 0.1,
        // it makes sense to consider speed variance to be ~ timeStep^2 * positionVariance
        covariance: [1, 1, 1, 0.01, 0.01, 0.01]// equivalent to diag([1, 1, 1, 0.01, 0.01, 0.01])
    }
});

Configure the observation

Using sensor observation

The observation is made from 2 different sensors with identical properties (i.e. same covariances) , the input measure will be [<sensor0-dim0>, <sensor0-dim1>, <sensor1-dim0>, <sensor1-dim1>].

const {KalmanFilter} = require('kalman-filter');

const timeStep = 0.1;

const kFilter = new KalmanFilter({
    observation: {
        sensorDimension: 2,// observation.dimension == observation.sensorDimension * observation.nSensors
        nSensors: 2,
        sensorCovariance: [3, 4], // equivalent to diag([3, 4])
        name: 'sensor'
    },
    dynamic: {
        name: 'constant-speed',// observation.sensorDimension * 2 == state.dimension
        covariance: [3, 3, 4, 4]// equivalent to diag([3, 3, 4, 4])
    }
});

Custom Observation matrix

The observation is made from 2 different sensors with different properties (i.e. different covariances), the input measure will be [<sensor0-dim0>, <sensor0-dim1>, <sensor1-dim0>, <sensor1-dim1>].

This can be achived manually by using the detailed API :

  • observation.dimension is the size of the observation
  • observation.stateProjection is the matrix that transforms state into observation, also called observation model
  • observation.covariance is the covariance matrix of the observation model
const {KalmanFilter} = require('kalman-filter');

const timeStep = 0.1;

const kFilter = new KalmanFilter({
    observation: {
        dimension: 4,
        stateProjection: [
            [1, 0, 0, 0],
            [0, 1, 0, 0],
            [1, 0, 0, 0],
            [0, 1, 0, 0]
        ],
        covariance: [3, 4, 0.3, 0.4]
    },
    dynamic: {
        name: 'constant-speed',// observation.sensorDimension * 2 == state.dimension
        covariance: [3, 3, 4, 4]// equivalent to diag([3, 3, 4, 4])
    }
});

Extended Kalman Filter

In order to use the Kalman-Filter with a dynamic or observation model which is not strictly a General linear model, it is possible to use function in following parameters :

  • observation.stateProjection
  • observation.covariance
  • dynamic.transition
  • dynamic.covariance

In this situation this function will return the value of the matrix at each step of the kalman-filter.

In this example, we create a constant-speed filter with non-uniform intervals;

const {KalmanFilter} = require('kalman-filter');

const intervals = [1,1,1,1,2,1,1,1];

const kFilter = new KalmanFilter({
    observation: {
        dimension: 2,
        /**
        * @param {State} opts.predicted
        * @param {Array.<Number>} opts.observation
        * @param {Number} opts.index
        */
        stateProjection: function(opts){
            return [
                [1, 0, 0, 0],
                [0, 1, 0, 0]
            ]
        },
        /**
        * @param {State} opts.predicted
        * @param {Array.<Number>} opts.observation
        * @param {Number} opts.index
        */		
        covariance: function(opts){
            return [
                [1, 0, 0, 0],
                [0, 1, 0, 0],
                [0, 0, 1, 0],
                [0, 0, 0, 1]
            ]
        }
    },
    dynamic: {
        dimension: 4, //(x, y, vx, vy)
        /**
        * @param {State} opts.previousCorrected
        * @param {Number} opts.index
        */
        transition: function(opts){
            const dT = intervals[opts.index];
            if(typeof(dT) !== 'number' || isNaN(dT) || dT <= 0){
                throw(new Error('dT should be positive number'))
            }
            return [
                [1, 0, dT, 0],
                [0, 1, 0, dT]
                [0, 0, 1, 0]
                [0, 0, 0, 1]
            ]
        },
        /**
        * @param {State} opts.previousCorrected
        * @param {Number} opts.index
        */		
        covariance: function(opts){
            const dT = intervals[opts.index];
            if(typeof(dT) !== 'number' || isNaN(dT) || dT <= 0){
                throw(new Error('dT should be positive number'))
            }			
            return [
                [1, 0, 0, 0],
                [0, 1, 0, 0],
                [0, 0, 1*dT, 0],
                [0, 0, 0, 1*dT]
            ]
        }
    }
});

Use your kalman filter

Simple Batch usage (run it once for the whole dataset)

const observations = [[0, 2], [0.1, 4], [0.5, 9], [0.2, 12]];

// batch kalman filter
const results = kFilter.filterAll(observations);

Online usage (run it online, forward step only)

When using online usage (only the forward step), the output of the filter method is an instance of the "State" class.

// online kalman filter
let previousCorrected = null;
const results = [];
observations.forEach(observation => {
    previousCorrected = kFilter.filter({previousCorrected, observation});
    results.push(previousCorrected.mean);
});

Predict/Correct detailed usage (run it online)

If you want to use KalmanFilter in more advanced usage, you might want to dissociate the predict and the correct functions

// online kalman filter
let previousCorrected = null;
const results = [];
observations.forEach(observation => {
    const predictedState = kFilter.predict({
        previousCorrected
    });

     const correctedState = kFilter.correct({
        predicted,
        observation
    });

    results.push(correctedState.mean);

    // update the previousCorrected for next loop iteration
    previousCorrected = correctedState
});

console.log(results);

Batch Forward - Backward smoothing usage

The Forward - Backward process

// batch kalman filter
const results = kFilter.filterAll({observations, passMode: 'forward-backward'});

Register models shortcuts

To get more information on how to build a dynamic model, check in the code lib/dynamic/ (or lib/observation for observation models).

If you feel your model can be used by other, do not hesitate to create a Pull Request.

const {registerDynamic, KalmanFilter, registerObservation} = require('kalman-filter');

registerObservation('custom-sensor', function(opts1){
    // do your stuff
    return {
        dimension,
        stateProjection,
        covariance
    }
})

registerDynamic('custom-dynamic', function(opts2, observation){
    // do your stuff
    // here you can use the parameter of observation (like observation.dimension)
    // to build the parameters for dynamic
    return {
        dimension,
        transition,
        covariance
    }
})

const kFilter = new KalmanFilter({
    observation: {
        name: 'custom-sensor',
        // ... fields of opts1
    },
    dynamic: {
        name: 'custom-dynamic',
        // ... fields of opts2
    }
});

Set your model parameters from the ground truths state values

In order to find the proper values for covariance matrix, we use following approach :


const {getCovariance, KalmanFilter} = require('kalman-filter');

// Ground truth values in the dynamic model hidden state
const groundTruthStates = [ // here this is (x, vx)
    [[0, 1.1], [1.1, 1], [2.1, 0.9], [3, 1], [4, 1.2]], // example 1
    [[8, 1.1], [9.1, 1], [10.1, 0.9], [11, 1], [12, 1.2]] // example 2
]

// Observations of this values
const measures = [ // here this is x only
    [[0.1], [1.3], [2.4], [2.6], [3.8]], // example 1
    [[8.1], [9.3], [10.4], [10.6], [11.8]] // example 2
];

const kFilter = new KalmanFilter({
    observation: {
        name: 'sensor',
        sensorDimension: 1
    },
    dynamic: {
        name: 'constant-speed'
    }
})

const dynamicCovariance = getCovariance({
    measures: groundTruthStates.map(ex =>
        return ex.slice(1)
    ).reduce((a,b) => a.concat(b)),
    averages: groundTruthStates.map(ex =>
        return ex.slice(1).map((_, index) => {
            return kFilter.predict({previousCorrected: ex[index - 1]}).mean;
        })
    ).reduce((a,b) => a.concat(b))
});

const observationCovariance = getCovariance({
    measures: measures.reduce((a,b) => a.concat(b)),
    averages: groundTruthStates.map((a) => a[0]).reduce((a,b) => a.concat(b))
});

How to measure how good does a specific model fits with data

There are different ways to measure the performance of a model against some measures :

Model fits with a specific measurements

We use Mahalanobis distance

const observations = [[0, 2], [0.1, 4], [0.5, 9], [0.2, 12]];

// online kalman filter
let previousCorrected = null;
const results = [];

observations.forEach(observation => {
    const predictedState = kFilter.predict({
        previousCorrected
    });

    const dist = predicted.mahalanobis(observation)

    previousCorrected = kFilter.correct({
        predicted,
        observation
    });

    distances.push(dist);
});

const distance = distances.reduce((d1, d2) => d1 + d2, 0);

How precise is this Model

We compare the model with random generated numbers sequence.

const h = require('hasard')
const observationHasard = h.array({value: h.number({type: 'normal'}), size: 2})

const observations = observationHasard.run(200);

// online kalman filter
let previousCorrected = null;
const results = [];

observations.forEach(observation => {
    const predictedState = kFilter.predict({
        previousCorrected
    });

    const dist = predicted.mahalanobis(measure)

    previousCorrected = kFilter.correct({
        predicted,
        observation
    });

    distances.push(dist);
});

const distance = distances.reduce((d1, d2) => d1 + d2, 0);

Credits

Thanks to Adrien Pellissier for his hard work on this library.

Similar Project

For a simple 1D Kalman filter in javascript see https://github.com/wouterbulten/kalmanjs