View Javadoc
1   /*
2    * Licensed to the Hipparchus project under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The Hipparchus project licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.hipparchus.filtering.kalman.linear;
19  
20  import org.hipparchus.filtering.kalman.ProcessEstimate;
21  import org.hipparchus.filtering.kalman.Reference;
22  import org.hipparchus.filtering.kalman.SimpleMeasurement;
23  import org.hipparchus.linear.CholeskyDecomposer;
24  import org.hipparchus.linear.MatrixUtils;
25  import org.hipparchus.linear.RealMatrix;
26  import org.hipparchus.linear.RealVector;
27  import org.hipparchus.random.RandomGenerator;
28  import org.hipparchus.random.Well1024a;
29  import org.hipparchus.util.FastMath;
30  import org.junit.jupiter.api.Test;
31  
32  import java.util.List;
33  import java.util.stream.IntStream;
34  import java.util.stream.Stream;
35  
36  import static org.junit.jupiter.api.Assertions.assertEquals;
37  import static org.junit.jupiter.api.Assertions.assertNull;
38  import static org.junit.jupiter.api.Assertions.assertTrue;
39  
40  class LinearKalmanFilterTest {
41  
42      @Test
43      void testConstant() {
44          final RealMatrix a = MatrixUtils.createRealIdentityMatrix(1);
45          final RealMatrix b = null;
46          final RealVector u = null;
47          final RealMatrix q = MatrixUtils.createRealDiagonalMatrix(new double[] {
48                                                                        1.0e-5
49                                                                    });
50  
51          // initial estimate is perfect, and process noise is perfectly known
52          final ProcessEstimate initial = new ProcessEstimate(0,
53                                                              MatrixUtils.createRealVector(new double[] { 10.0 }),
54                                                              q);
55          assertNull(initial.getInnovationCovariance());
56  
57          // reference values from Apache Commons Math 3.6.1 unit test
58          final List<Reference> referenceData = Reference.loadReferenceData(1, 1, "constant-value.txt");
59          final Stream<SimpleMeasurement> measurements =
60                          referenceData.stream().
61                          map(r -> new SimpleMeasurement(r.getTime(),
62                                                         r.getZ(),
63                                                         MatrixUtils.createRealDiagonalMatrix(new double[] { 0.1 })));
64  
65          // set up Kalman filter
66          final LinearKalmanFilter<SimpleMeasurement> filter =
67                          new LinearKalmanFilter<>(new CholeskyDecomposer(1.0e-15, 1.0e-15),
68                                                 measurement -> new LinearEvolution(a, b, u, q,
69                                                                                    MatrixUtils.createRealMatrix(new double[][] { { 1.0 } })),
70                                                 initial);
71  
72          // sequentially process all measurements and check against the reference estimated state and covariance
73          measurements.
74          map(measurement -> filter.estimationStep(measurement)).
75          forEach(estimate -> {
76              for (Reference r : referenceData) {
77                  if (r.sameTime(estimate.getTime())) {
78                      r.checkState(estimate.getState(), 1.0e-15);
79                      r.checkCovariance(estimate.getCovariance(), 3.0e-19);
80                      return;
81                  }
82              }
83          });
84  
85      }
86  
87      @Test
88      void testConstantAcceleration() {
89          doTestConstantAcceleration("constant-acceleration.txt");
90      }
91  
92      @Test
93      void testConstantAccelerationWithIntermediateData() {
94          doTestConstantAcceleration("constant-acceleration-with-intermediate-data.txt");
95      }
96  
97      @Test
98      void testConstantAccelerationWithOutlier() {
99          doTestConstantAcceleration("constant-acceleration-with-outlier.txt");
100     }
101 
102     private void doTestConstantAcceleration(String name) {
103 
104         // state:             { position, velocity }
105         // control:           0.1 m/s² acceleration
106         // process noise:     induced by 0.2 m/s² acceleration noise
107         // measurement:       on position only
108         // measurement noise: 10 m (big!)
109 
110         final double dt      = 0.1;
111         final double dt2     = dt  * dt;
112         final double dt3     = dt2 * dt;
113         final double dt4     = dt2 * dt2;
114         final double acc     = 0.1;
115         final double aNoise  = 0.2;
116         final double aNoise2 = aNoise * aNoise;
117         final double mNoise  = 10.0;
118         final RealMatrix a = MatrixUtils.createRealMatrix(new double[][] {
119             { 1.0, dt },
120             { 0.0, 1.0 }
121         });
122         final RealMatrix b = MatrixUtils.createRealMatrix(new double[][] {
123             { 0.5 * dt2 },
124             { dt }
125         });
126         final RealVector u = MatrixUtils.createRealVector(new double[] { acc });
127         final RealMatrix q = MatrixUtils.createRealMatrix(new double[][] {
128             { 0.25 * dt4 * aNoise2, 0.5 * dt3 * aNoise2 },
129             { 0.5  * dt3 * aNoise2, dt2 * aNoise2 }
130         });
131 
132         // initial state is estimated to be at rest on origin
133         final ProcessEstimate initial = new ProcessEstimate(0,
134                                                             MatrixUtils.createRealVector(new double[] { 0.0, 0.0 }),
135                                                             MatrixUtils.createRealMatrix(new double[][] {
136                                                                 { 1.0, 1.0 },
137                                                                 { 1.0, 1.0 }
138                                                             }));
139 
140         // reference values from Apache Commons Math 3.6.1 unit test
141         // possibly with additional intermediate data
142         final List<Reference> referenceData = Reference.loadReferenceData(2, 1, name);
143         final Stream<SimpleMeasurement> measurements =
144                         referenceData.stream().
145                         map(r -> new SimpleMeasurement(r.getTime(),
146                                                        r.getZ(),
147                                                        MatrixUtils.createRealDiagonalMatrix(new double[] { mNoise * mNoise })));
148 
149         // set up Kalman filter
150         final LinearKalmanFilter<SimpleMeasurement> filter =
151         new LinearKalmanFilter<>(new CholeskyDecomposer(1.0e-15, 1.0e-15),
152                                measurement -> {
153                                    RealMatrix h = (measurement.getValue().getEntry(0) > 1.0e6) ?
154                                                   null :
155                                                   MatrixUtils.createRealMatrix(new double[][] { { 1.0, 0.0 } });
156                                    return new LinearEvolution(a, b, u, q, h);
157                                },
158                                initial);
159 
160         // sequentially process all measurements and check against the reference estimate
161         measurements.
162         map(measurement -> filter.estimationStep(measurement)).
163         forEach(estimate -> {
164             for (Reference r : referenceData) {
165                 if (r.sameTime(estimate.getTime())) {
166                     r.checkState(estimate.getState(), 4.0e-15);
167                     r.checkCovariance(estimate.getCovariance(), 4.0e-15);
168                     if (r.hasIntermediateData()) {
169                       r.checkStateTransitionMatrix(estimate.getStateTransitionMatrix(), 1.0e-14);
170                       r.checkMeasurementJacobian(estimate.getMeasurementJacobian(),     1.0e-15);
171                       r.checkInnovationCovariance(estimate.getInnovationCovariance(),   1.0e-12);
172                       r.checkKalmanGain(estimate.getKalmanGain(),                       1.0e-12);
173                       r.checkKalmanGain(estimate.getKalmanGain(),                       1.0e-15);
174                     }
175                     return;
176                 }
177             }
178         });
179 
180     }
181 
182     @Test
183     void testCannonballZeroProcessNoise() {
184         doTestCannonball(new double[][] {
185                             { 0.00, 0.00, 0.00, 0.00 },
186                             { 0.00, 0.00, 0.00, 0.00 },
187                             { 0.00, 0.00, 0.00, 0.00 },
188                             { 0.00, 0.00, 0.00, 0.00 },
189                          }, "cannonball-zero-process-noise.txt",
190                          9.0e-16, 6.0e-14);
191     }
192 
193     @Test
194     void testCannonballNonZeroProcessNoise() {
195         doTestCannonball(new double[][] {
196                             { 0.01, 0.00, 0.00, 0.00 },
197                             { 0.00, 0.10, 0.00, 0.00 },
198                             { 0.00, 0.00, 0.01, 0.00 },
199                             { 0.00, 0.00, 0.00, 0.10 },
200                          }, "cannonball-non-zero-process-noise.txt",
201                          2.0e-13, 2.0e-13);
202     }
203 
204     private void doTestCannonball(final double[][] qData, final String name,
205                                   final double tolState, final double tolCovariance) {
206 
207         final double dt       = 0.1;
208         final double g        = 9.81;
209         final double mNoise   = 30.0;
210         final double vIni     = 100.0;
211         final double alphaIni = FastMath.PI / 4;
212         final RealMatrix a = MatrixUtils.createRealMatrix(new double[][] {
213             { 1.0,  dt, 0.0, 0.0 },
214             { 0.0, 1.0, 0.0, 0.0 },
215             { 0.0, 0.0, 1.0,  dt },
216             { 0.0, 0.0, 0.0, 1.0 },
217         });
218         final RealMatrix b = MatrixUtils.createRealMatrix(new double[][] {
219             { 0.0, 0.0 },
220             { 0.0, 0.0 },
221             { 1.0, 0.0 },
222             { 0.0, 1.0 }
223         });
224         final RealVector u = MatrixUtils.createRealVector(new double[] {
225             -0.5 * g * dt * dt, -g * dt
226         });
227         final RealMatrix q = MatrixUtils.createRealMatrix(qData);
228 
229         // initial state is estimated to be a shot from origin with known angle and velocity
230         final ProcessEstimate initial = new ProcessEstimate(0,
231                                                             MatrixUtils.createRealVector(new double[] {
232                                                                  0.0, vIni * FastMath.cos(alphaIni),
233                                                                  0.0, vIni * FastMath.sin(alphaIni)
234                                                             }),
235                                                             MatrixUtils.createRealDiagonalMatrix(new double[] {
236                                                                 mNoise * mNoise, 1.0e-3, mNoise * mNoise, 1.0e-3
237                                                             }));
238 
239         // reference values from Apache Commons Math 3.6.1 unit test
240         // we have changed the test slightly, setting up a non-zero process noise
241         final List<Reference> referenceData = Reference.loadReferenceData(4, 2, name);
242         final Stream<SimpleMeasurement> measurements =
243                         referenceData.stream().
244                         map(r -> new SimpleMeasurement(r.getTime(),
245                                                        r.getZ(),
246                                                        MatrixUtils.createRealDiagonalMatrix(new double[] {
247                                                            mNoise * mNoise, mNoise * mNoise
248                                                        })));
249 
250         // set up Kalman filter
251         final LinearKalmanFilter<SimpleMeasurement> filter =
252         new LinearKalmanFilter<>(new CholeskyDecomposer(1.0e-15, 1.0e-15),
253                                   time -> new LinearEvolution(a, b, u, q,
254                                                               MatrixUtils.createRealMatrix(new double[][] {
255                                                                   { 1.0, 0.0, 0.0, 0.0 },
256                                                                   { 0.0, 0.0, 1.0, 0.0 }
257                                                               })),
258                                   initial);
259 
260         // sequentially process all measurements and check against the reference estimate
261         measurements.
262         map(measurement -> filter.estimationStep(measurement)).
263         map(estimate -> {
264             final ProcessEstimate p = filter.getPredicted();
265             final ProcessEstimate c = filter.getCorrected();
266             assertEquals(p.getTime(), c.getTime(), 1.0e-15);
267             assertTrue(p.getState().getDistance(c.getState()) > 0.005);
268             return estimate;
269         }).
270         forEach(estimate -> {
271             for (Reference r : referenceData) {
272                 if (r.sameTime(estimate.getTime())) {
273                     r.checkState(estimate.getState(), tolState);
274                     r.checkCovariance(estimate.getCovariance(), tolCovariance);
275                     return;
276                 }
277             }
278         });
279 
280     }
281 
282     @Test
283     void testWelshBishopExactR() {
284         doTestWelshBishop(0xd30a8f811e2f7c61l, -0.37727, 0.1,
285                           0.0, 1.0, 1.0e-5, 0.1 * 0.1,
286                           50, -0.389117, 1.0e-6);
287     }
288 
289     @Test
290     void testWelshBishopBigR() {
291         doTestWelshBishop(0xd30a8f811e2f7c61l, -0.37727, 0.1,
292                           0.0, 1.0, 1.0e-5, 1.0 * 1.0,
293                           50, -0.385613, 1.0e-6);
294     }
295 
296     @Test
297     void testWelshBishopSmallR() {
298         doTestWelshBishop(0xd30a8f811e2f7c61l, -0.37727, 0.1,
299                           0.0, 1.0, 1.0e-5, 0.01 * 0.01,
300                           50, -0.403015, 1.0e-6);
301     }
302 
303     private void doTestWelshBishop(final long seed,
304                                    final double trueConstant, final double trueStdv,
305                                    final double initialEstimate, final double initialCovariance,
306                                    final double qValue, final double r,
307                                    final int nbMeasurements,
308                                    final double expected, final double tolerance) {
309 
310         // this is the constant voltage example from paper
311         // An Introduction to the Kalman Filter, Greg Welch and Gary Bishop
312         // available from http://www.cs.unc.edu/~welch/media/pdf/kalman_intro.pdf
313         final RealMatrix a = MatrixUtils.createRealIdentityMatrix(1);
314         final RealMatrix b = null;
315         final RealVector u = null;
316         final RealMatrix q = MatrixUtils.createRealDiagonalMatrix(new double[] {
317             qValue
318         });
319         final ProcessEstimate initial = new ProcessEstimate(0,
320                                                       MatrixUtils.createRealVector(new double[] { initialEstimate }),
321                                                       MatrixUtils.createRealDiagonalMatrix(new double[] { initialCovariance }));
322         final RandomGenerator generator = new Well1024a(seed);
323         final Stream<SimpleMeasurement> measurements =
324                         IntStream.
325                         range(0, nbMeasurements).
326                         mapToObj(i -> new SimpleMeasurement(i,
327                                                             MatrixUtils.createRealVector(new double[] {
328                                                                 trueConstant + generator.nextGaussian() * trueStdv,
329                                                             }),
330                                                             MatrixUtils.createRealDiagonalMatrix(new double[] { r })));
331 
332         // set up Kalman filter
333         final LinearKalmanFilter<SimpleMeasurement> filter =
334                         new LinearKalmanFilter<>(new CholeskyDecomposer(1.0e-15, 1.0e-15),
335                                                measurement -> new LinearEvolution(a, b, u, q,
336                                                                                   MatrixUtils.createRealMatrix(new double[][] { { 1.0 } })),
337                                                initial);
338 
339         // sequentially process all measurements and get only the last one
340         final ProcessEstimate finalEstimate = measurements.
341                         map(measurement -> filter.estimationStep(measurement)).
342                         reduce((first, second) -> second).get();
343 
344         assertEquals(expected, finalEstimate.getState().getEntry(0), tolerance);
345 
346     }
347 
348 }