Noncollinear Perpendicular Microcrystalline

    kalman-filter

    1.10.1 • Public • Published

    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

    Install

    npm i kalman-filter

    DownloadsWeekly Downloads

    270

    Version

    1.10.1

    License

    MIT

    Unpacked Size

    37.3 MB

    Total Files

    89

    Last publish

    Collaborators

    • piercus