Skip to content

Commit 521fc62

Browse files
authored
Merge pull request #60 from piercus/extended
feat: extended kalman filter with fn parameters
2 parents 64898c2 + 768d061 commit 521fc62

File tree

15 files changed

+258
-86
lines changed

15 files changed

+258
-86
lines changed

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ jobs:
1111
steps:
1212
- name: Checkout
1313
uses: actions/checkout@v3
14-
- name: Use Node.js 12
14+
- name: Use Node.js 18
1515
uses: actions/setup-node@v3
1616
with:
17-
node-version: 12
17+
node-version: 18
1818
- run: npm ci
1919
- run: npm run build
2020
- name: Semantic Release

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ jobs:
99

1010
strategy:
1111
matrix:
12-
node-version: [10.x, 12.x, 14.x, 15.x]
12+
node-version: [18.x]
1313

1414
steps:
1515
- uses: actions/checkout@v3
1616
- name: Use Node.js ${{ matrix.node-version }}
1717
uses: actions/setup-node@v3
1818
with:
19-
node-version: ${{ matrix.node-version }}
19+
node-version: ${{ matrix.node-version }}
2020
- run: npm ci
2121
- run: npm run build --if-present
2222
- run: npm test

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ const kFilter = new KalmanFilter({
363363
});
364364
```
365365

366-
### Extended Kalman Filter
366+
## Play with Kalman Filter
367367

368368
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 :
369369
* `observation.stateProjection`
@@ -375,7 +375,6 @@ In this situation this `function` will return the value of the matrix at each st
375375

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

378-
379378
```js
380379
const {KalmanFilter} = require('kalman-filter');
381380

@@ -446,6 +445,16 @@ const kFilter = new KalmanFilter({
446445
}
447446
});
448447
```
448+
### Extended
449+
450+
If you want to implement an [extended kalman filter](https://en.wikipedia.org/wiki/Extended_Kalman_filter)
451+
452+
You will need to put your non-linear functions in the following parameters
453+
454+
* `observation.fn`
455+
* `dynamic.fn`
456+
457+
See an example in `test/issues/56.js`
449458

450459
## Use your kalman filter
451460

@@ -457,7 +466,7 @@ const observations = [[0, 2], [0.1, 4], [0.5, 9], [0.2, 12]];
457466
// batch kalman filter
458467
const results = kFilter.filterAll(observations);
459468
```
460-
### Online usage (run it online, forward step only)
469+
### Online filter
461470

462471
When using online usage (only the forward step), the output of the `filter` method is an instance of the ["State"](/lib/state.js) class.
463472

lib/core-kalman-filter.js

Lines changed: 58 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,54 +7,54 @@ const getIdentity = require('../lib/linalgebra/identity.js');
77
const State = require('./state.js');
88
const checkMatrix = require('./utils/check-matrix.js');
99
/**
10-
* @callback ObservationCallback
10+
* @callback PreviousCorrectedCallback
1111
* @param {Object} opts
1212
* @param {Number} opts.index
1313
* @param {Number} opts.previousCorrected
1414
*/
1515

1616
/**
17-
* @typedef {Object} ObservationConfig
18-
* @property {Number} dimension
19-
* @property {Array.Array.<Number>> | ObservationCallback} stateProjection,
20-
* @property {Array.Array.<Number>> | ObservationCallback} covariance
21-
*/
22-
23-
/**
24-
* @callback DynamicCallback
17+
* @callback PredictedCallback
2518
* @param {Object} opts
2619
* @param {Number} opts.index
2720
* @param {State} opts.predicted
2821
* @param {Observation} opts.observation
2922
*/
3023

3124
/**
32-
* @typedef {Object} DynamicConfig
25+
* @typedef {Object} ObservationConfig
3326
* @property {Number} dimension
34-
* @property {Array.Array.<Number>> | DynamicCallback} transition,
35-
* @property {Array.Array.<Number>> | DynamicCallback} covariance
27+
* @property {PredictedCallback} [fn=null] for extended kalman filter only, the non-linear state to observation function
28+
* @property {Array.Array.<Number>> | PreviousCorrectedCallback} stateProjection the matrix to transform state to observation (for EKF, the jacobian of the fn)
29+
* @property {Array.Array.<Number>> | PreviousCorrectedCallback} covariance the covariance of the observation noise
3630
*/
3731

32+
/**
33+
* @typedef {Object} DynamicConfig
34+
* @property {Number} dimension dimension of the state vector
35+
* @property {PreviousCorrectedCallback} [fn=null] for extended kalman filter only, the non-linear state-transition model
36+
* @property {Array.Array.<Number>> | PredictedCallback} transition the state-transition model (or for EKF the jacobian of the fn)
37+
* @property {Array.Array.<Number>> | PredictedCallback} covariance the covariance of the process noise
38+
*/
39+
40+
/**
41+
* @typedef {Object} CoreConfig
42+
* @property {DynamicConfig} dynamic the system's dynamic model
43+
* @property {ObservationConfig} observation the system's observation model
44+
* @property {Object} [logger=defaultLogger] a Winston-like logger
45+
*/
3846
const defaultLogger = {
3947
info: (...args) => console.log(...args),
4048
debug: () => {},
4149
warn: (...args) => console.log(...args),
4250
error: (...args) => console.log(...args)
4351
};
44-
4552
/**
46-
* @class
47-
* @property {DynamicConfig} dynamic the system's dynamic model
48-
* @property {ObservationConfig} observation the system's observation model
49-
*@property logger a Winston-like logger
53+
* @param {CoreConfig} options
5054
*/
5155
class CoreKalmanFilter {
52-
/**
53-
* @param {DynamicConfig} dynamic
54-
* @param {ObservationConfig} observation the system's observation model
55-
*/
56-
57-
constructor({dynamic, observation, logger = defaultLogger}) {
56+
constructor(options) {
57+
const {dynamic, observation, logger = defaultLogger} = options;
5858
this.dynamic = dynamic;
5959
this.observation = observation;
6060
this.logger = logger;
@@ -66,11 +66,13 @@ class CoreKalmanFilter {
6666

6767
getInitState() {
6868
const {mean: meanInit, covariance: covarianceInit, index: indexInit} = this.dynamic.init;
69+
6970
const initState = new State({
7071
mean: meanInit,
7172
covariance: covarianceInit,
7273
index: indexInit
7374
});
75+
State.check(initState, {title: 'dynamic.init'});
7476
return initState;
7577
}
7678

@@ -85,10 +87,13 @@ class CoreKalmanFilter {
8587
previousCorrected = previousCorrected || this.getInitState();
8688

8789
const getValueOptions = Object.assign({}, {previousCorrected, index}, options);
88-
const d = this.getValue(this.dynamic.transition, getValueOptions);
89-
const dTransposed = transpose(d);
90-
const covarianceInter = matMul(d, previousCorrected.covariance);
91-
const covariancePrevious = matMul(covarianceInter, dTransposed);
90+
const transition = this.getValue(this.dynamic.transition, getValueOptions);
91+
92+
checkMatrix(transition, [this.dynamic.dimension, this.dynamic.dimension], 'dynamic.transition');
93+
94+
const transitionTransposed = transpose(transition);
95+
const covarianceInter = matMul(transition, previousCorrected.covariance);
96+
const covariancePrevious = matMul(covarianceInter, transitionTransposed);
9297
const dynCov = this.getValue(this.dynamic.covariance, getValueOptions);
9398

9499
const covariance = add(
@@ -100,6 +105,14 @@ class CoreKalmanFilter {
100105
return covariance;
101106
}
102107

108+
predictMean({opts, transition}) {
109+
if (this.dynamic.fn) {
110+
return this.dynamic.fn(opts);
111+
}
112+
113+
const {previousCorrected} = opts;
114+
return matMul(transition, previousCorrected.mean);
115+
}
103116
/**
104117
This will return the new prediction, relatively to the dynamic model chosen
105118
* @param {State} previousCorrected State relative to our dynamic model
@@ -115,16 +128,14 @@ class CoreKalmanFilter {
115128
}
116129

117130
State.check(previousCorrected, {dimension: this.dynamic.dimension});
118-
119-
const getValueOptions = Object.assign({}, {
131+
const getValueOptions = Object.assign({}, options, {
120132
previousCorrected,
121133
index
122-
}, options);
123-
const d = this.getValue(this.dynamic.transition, getValueOptions);
134+
});
124135

125-
checkMatrix(d, [this.dynamic.dimension, this.dynamic.dimension], 'dynamic.transition');
136+
const transition = this.getValue(this.dynamic.transition, getValueOptions);
126137

127-
const mean = matMul(d, previousCorrected.mean);
138+
const mean = this.predictMean({transition, opts: getValueOptions});
128139

129140
const covariance = this.getPredictedCovariance(getValueOptions);
130141

@@ -146,8 +157,10 @@ class CoreKalmanFilter {
146157
stateProjection = stateProjection || this.getValue(this.observation.stateProjection, getValueOptions);
147158
const obsCovariance = this.getValue(this.observation.covariance, getValueOptions);
148159
checkMatrix(obsCovariance, [this.observation.dimension, this.observation.dimension], 'observation.covariance');
149-
150160
const stateProjTransposed = transpose(stateProjection);
161+
162+
checkMatrix(stateProjection, [this.observation.dimension, this.dynamic.dimension], 'observation.stateProjection');
163+
151164
const noiselessInnovation = matMul(
152165
matMul(stateProjection, predicted.covariance),
153166
stateProjTransposed
@@ -187,6 +200,15 @@ class CoreKalmanFilter {
187200
);
188201
}
189202

203+
getPredictedObservation({opts, stateProjection}) {
204+
if (this.observation.fn) {
205+
return this.observation.fn(opts);
206+
}
207+
208+
const {predicted} = opts;
209+
return matMul(stateProjection, predicted.mean);
210+
}
211+
190212
/**
191213
This will return the new correction, taking into account the prediction made
192214
and the observation of the sensor
@@ -209,8 +231,9 @@ class CoreKalmanFilter {
209231

210232
const innovation = sub(
211233
observation,
212-
matMul(stateProjection, predicted.mean)
234+
this.getPredictedObservation({stateProjection, opts: getValueOptions})
213235
);
236+
214237
const mean = add(
215238
predicted.mean,
216239
matMul(optimalKalmanGain, innovation)

lib/kalman-filter.js

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ const State = require('./state.js');
1212
const modelCollection = require('./model-collection.js');
1313
const CoreKalmanFilter = require('./core-kalman-filter.js');
1414

15+
/**
16+
* @typedef {String} DynamicNonObjectConfig
17+
*/
18+
/**
19+
* @typedef {DynamicConfig} DynamicObjectConfig
20+
* @property {String} name
21+
*/
22+
/**
23+
* @param {DynamicNonObjectConfig} dynamic
24+
* @returns {DynamicObjectConfig}
25+
*/
26+
1527
const buildDefaultDynamic = function (dynamic) {
1628
if (typeof (dynamic) === 'string') {
1729
return {name: dynamic};
@@ -20,6 +32,17 @@ const buildDefaultDynamic = function (dynamic) {
2032
return {name: 'constant-position'};
2133
};
2234

35+
/**
36+
* @typedef {String | Number} ObservationNonObjectConfig
37+
*/
38+
/**
39+
* @typedef {ObservationConfig} ObservationObjectConfig
40+
* @property {String} name
41+
*/
42+
/**
43+
* @param {ObservationNonObjectConfig} observation
44+
* @returns {ObservationObjectConfig}
45+
*/
2346
const buildDefaultObservation = function (observation) {
2447
if (typeof (observation) === 'number') {
2548
return {name: 'sensor', sensorDimension: observation};
@@ -35,8 +58,9 @@ const buildDefaultObservation = function (observation) {
3558
*This function fills the given options by successively checking if it uses a registered model,
3659
* it builds and checks the dynamic and observation dimensions, build the stateProjection if only observedProjection
3760
*is given, and initialize dynamic.init
38-
*@param {DynamicConfig} options.dynamic
39-
*@param {ObservationConfig} options.observation
61+
*@param {DynamicObjectConfig | DynamicNonObjectConfig} options.dynamic
62+
*@param {ObservationObjectConfig | ObservationNonObjectConfig} options.observation
63+
* @returns {CoreConfig}
4064
*/
4165

4266
const setupModelsParameters = function ({observation, dynamic}) {
@@ -63,29 +87,38 @@ const setupModelsParameters = function ({observation, dynamic}) {
6387
};
6488

6589
/**
66-
*Returns the corresponding model without arrays as values but only functions
67-
*@param {ObservationConfig} observation
68-
*@param {DynamicConfig} dynamic
69-
*@returns {ObservationConfig, DynamicConfig} model with respect of the Core Kalman Filter properties
90+
* @typedef {Object} ModelsParameters
91+
* @property {DynamicObjectConfig} dynamic
92+
* @property {ObservationObjectConfig} observation
93+
*/
94+
95+
/**
96+
* Returns the corresponding model without arrays as values but only functions
97+
* @param {ModelsParameters} modelToBeChanged
98+
* @returns {CoreConfig} model with respect of the Core Kalman Filter properties
7099
*/
71100
const modelsParametersToCoreOptions = function (modelToBeChanged) {
72101
const {observation, dynamic} = modelToBeChanged;
73102
return deepAssign(modelToBeChanged, {
74103
observation: {
75-
stateProjection: toFunction(polymorphMatrix(observation.stateProjection)),
76-
covariance: toFunction(polymorphMatrix(observation.covariance, {dimension: observation.dimension}))
104+
stateProjection: toFunction(polymorphMatrix(observation.stateProjection), {label: 'observation.stateProjection'}),
105+
covariance: toFunction(polymorphMatrix(observation.covariance, {dimension: observation.dimension}), {label: 'observation.covariance'})
77106
},
78107
dynamic: {
79-
transition: toFunction(polymorphMatrix(dynamic.transition)),
80-
covariance: toFunction(polymorphMatrix(dynamic.covariance, {dimension: dynamic.dimension}))
108+
transition: toFunction(polymorphMatrix(dynamic.transition), {label: 'dynamic.transition'}),
109+
covariance: toFunction(polymorphMatrix(dynamic.covariance, {dimension: dynamic.dimension}), {label: 'dynamic.covariance'})
81110
}
82111
});
83112
};
84113

85114
class KalmanFilter extends CoreKalmanFilter {
86115
/**
87-
* @param {DynamicConfig} options.dynamic
88-
* @param {ObservationConfig} options.observation the system's observation model
116+
* @typedef {Object} Config
117+
* @property {DynamicObjectConfig | DynamicNonObjectConfig} dynamic
118+
* @property {ObservationObjectConfig | ObservationNonObjectConfig} observation
119+
*/
120+
/**
121+
* @param {Config} options
89122
*/
90123
constructor(options = {}) {
91124
const modelsParameters = setupModelsParameters(options);
@@ -117,11 +150,7 @@ class KalmanFilter extends CoreKalmanFilter {
117150
*@returns {Array.<Array.<Number>>} the mean of the corrections
118151
*/
119152
filterAll(observations) {
120-
const {mean: meanInit, covariance: covarianceInit, index: indexInit} = this.dynamic.init;
121-
let previousCorrected = new State({
122-
mean: meanInit,
123-
covariance: covarianceInit,
124-
index: indexInit});
153+
let previousCorrected = this.getInitState();
125154
const results = [];
126155
for (const observation of observations) {
127156
const predicted = this.predict({previousCorrected});
@@ -138,8 +167,9 @@ class KalmanFilter extends CoreKalmanFilter {
138167
/**
139168
* Returns an estimation of the asymptotic state covariance as explained in https://en.wikipedia.org/wiki/Kalman_filter#Asymptotic_form
140169
* 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)
170+
* @param {Number} [limitIterations=1e2] max number of iterations
141171
* @param {Number} [tolerance=1e-6] returns when the last values differences are less than tolerance
142-
* @return {<Array.<Array.<Number>>>} covariance
172+
* @return {Array.<Array.<Number>>} covariance
143173
*/
144174
asymptoticStateCovariance(limitIterations = 1e2, tolerance = 1e-6) {
145175
let previousCorrected = super.getInitState();
@@ -167,7 +197,7 @@ class KalmanFilter extends CoreKalmanFilter {
167197
/**
168198
* Returns an estimation of the asymptotic gain, as explained in https://en.wikipedia.org/wiki/Kalman_filter#Asymptotic_form
169199
* @param {Number} [tolerance=1e-6] returns when the last values differences are less than tolerance
170-
* @return {<Array.<Array.<Number>>>} gain
200+
* @return {Array.<Array.<Number>>} gain
171201
*/
172202
asymptoticGain(tolerance = 1e-6) {
173203
const covariance = this.asymptoticStateCovariance(tolerance);

lib/linalgebra/add.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const elemWise = require('./elem-wise');
22
/**
33
* Add matrixes together
4-
* @param {...<Array.<Array.<Number>>} args list of matrix
4+
* @param {...Array.<Array.<Number>>} args list of matrix
55
* @returns {Array.<Array.<Number>>} sum
66
*/
77
module.exports = function (...args) {

0 commit comments

Comments
 (0)