1   /* Copyright 2002-2022 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS 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    *   http://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  package org.orekit.estimation.sequential;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  
23  import org.hipparchus.geometry.euclidean.threed.Vector3D;
24  import org.hipparchus.linear.LUDecomposition;
25  import org.hipparchus.linear.MatrixUtils;
26  import org.hipparchus.linear.RealMatrix;
27  import org.hipparchus.linear.RealVector;
28  import org.junit.Assert;
29  import org.junit.Before;
30  import org.junit.Test;
31  import org.orekit.estimation.Context;
32  import org.orekit.estimation.EstimationTestUtils;
33  import org.orekit.estimation.Force;
34  import org.orekit.estimation.measurements.EstimatedMeasurement;
35  import org.orekit.estimation.measurements.GroundStation;
36  import org.orekit.estimation.measurements.ObservableSatellite;
37  import org.orekit.estimation.measurements.ObservedMeasurement;
38  import org.orekit.estimation.measurements.PV;
39  import org.orekit.estimation.measurements.Range;
40  import org.orekit.estimation.measurements.modifiers.Bias;
41  import org.orekit.forces.radiation.RadiationSensitive;
42  import org.orekit.orbits.Orbit;
43  import org.orekit.orbits.OrbitType;
44  import org.orekit.orbits.PositionAngle;
45  import org.orekit.propagation.MatricesHarvester;
46  import org.orekit.propagation.SpacecraftState;
47  import org.orekit.propagation.conversion.NumericalPropagatorBuilder;
48  import org.orekit.propagation.numerical.NumericalPropagator;
49  import org.orekit.time.AbsoluteDate;
50  import org.orekit.utils.PVCoordinates;
51  import org.orekit.utils.ParameterDriver;
52  import org.orekit.utils.ParameterDriversList;
53  
54  /** Test class for Kalman model.
55   * This class is deeply entangled with KalmanEstimator class. Thus it is difficult to test as a stand-alone.
56   * Here we simply test the functions from KalmanEstimation interface that return the "physical" values of the
57   * different state vectors and matrices used in a Kalman filter:
58   * state transition, measurement, kalman gain matrices etc.
59   * @author Maxime Journot
60   */
61  public class KalmanModelTest {
62  
63      /** Orbit type for propagation. */
64      private final OrbitType orbitType = OrbitType.CARTESIAN;
65      
66      /** Position angle for propagation. */
67      private final PositionAngle positionAngle = PositionAngle.TRUE;
68      
69      /** Initial orbit. */
70      private Orbit orbit0;
71      
72      /** Propagator builder. */
73      private NumericalPropagatorBuilder propagatorBuilder;
74      
75      /** Covariance matrix provider. */
76      private CovarianceMatrixProvider covMatrixProvider;
77      
78      /** Estimated measurement parameters list. */
79      private ParameterDriversList estimatedMeasurementsParameters;
80      
81      /** Kalman extended estimator containing models. */
82      private KalmanEstimator kalman;
83      
84      /** Kalman observer. */
85      private ModelLogger modelLogger;
86      
87      /** State size. */
88      private int M;
89      
90      /** PV at t0. */
91      private PV pv;
92      
93      /** Range after t0. */
94      private Range range;
95      
96      /** Driver for satellite range bias. */
97      private ParameterDriver satRangeBiasDriver;
98      
99      /** Driver for SRP coefficient. */
100     private ParameterDriver srpCoefDriver;
101     
102     /** Tolerance for the test. */
103     private final double tol = 1e-16;
104     
105     /** Setup an eccentric orbit with Keplerian gravity force and isotropic solar radiation pressure model.
106      * Add a satellite range bias to the range measurements and select its estimation.
107      * Select estimation of radiation pressure reflection coefficient.
108      * Select estimation of all orbital parameters.
109      * Create a Kalman filter from this.
110      * 
111      * Create one perfect PV measurement at t0
112      * Create one range measurement at t0 + 10s, modified by the satellite range bias mentionned above.
113      */
114     @Before
115     public void setup() {
116         // Create context
117         final Context context = EstimationTestUtils.eccentricContext("regular-data:potential:tides");
118         
119         // Initial orbit and date
120         this.orbit0 = context.initialOrbit;
121         ObservableSatellite sat = new ObservableSatellite(0);
122         
123         // Create propagator builder
124         this.propagatorBuilder = context.createBuilder(orbitType, positionAngle, true,
125                                                        1.0e-6, 60.0, 10., Force.SOLAR_RADIATION_PRESSURE);
126 
127         // Create PV at t0
128         final AbsoluteDate date0 = context.initialOrbit.getDate();
129         this.pv = new PV(date0,
130                              context.initialOrbit.getPVCoordinates().getPosition(),
131                              context.initialOrbit.getPVCoordinates().getVelocity(),
132                              new double[] {1., 2., 3., 1e-3, 2e-3, 3e-3}, 1.,
133                              sat);
134         
135         // Create one 0m range measurement at t0 + 10s
136         final AbsoluteDate date  = date0.shiftedBy(10.);
137         final GroundStation station = context.stations.get(0);
138         this.range = new Range(station, true, date, 18616150., 10., 1., sat);
139         // Exact range value is 1.8616150246470984E7 m
140 
141         // Add sat range bias to PV and select it
142         final Bias<Range> satRangeBias = new Bias<Range>(new String[] {"sat range bias"},
143                                                          new double[] {100.},
144                                                          new double[] {10.},
145                                                          new double[] {0.},
146                                                          new double[] {100.});
147         this.satRangeBiasDriver = satRangeBias.getParametersDrivers().get(0);
148         satRangeBiasDriver.setSelected(true);
149         satRangeBiasDriver.setReferenceDate(date);
150         range.addModifier(satRangeBias);
151         for (ParameterDriver driver : range.getParametersDrivers()) {
152             driver.setReferenceDate(date);
153         }
154 
155         // Gather list of meas parameters (only sat range bias here)
156         this.estimatedMeasurementsParameters = new ParameterDriversList();
157         for (final ParameterDriver driver : range.getParametersDrivers()) {
158             if (driver.isSelected()) {
159                 estimatedMeasurementsParameters.add(driver);
160             }
161         }
162         // Select SRP coefficient
163         this.srpCoefDriver = propagatorBuilder.getPropagationParametersDrivers().
164                         findByName(RadiationSensitive.REFLECTION_COEFFICIENT);
165         srpCoefDriver.setReferenceDate(date);
166         srpCoefDriver.setSelected(true);
167         
168         // Create a covariance matrix using the scales of the estimated parameters
169         final double[] scales = getParametersScale(propagatorBuilder, estimatedMeasurementsParameters);
170         this.M = scales.length;
171         this.covMatrixProvider = setInitialCovarianceMatrix(scales);
172 
173         // Initialize Kalman
174         final KalmanEstimatorBuilder kalmanBuilder = new KalmanEstimatorBuilder();
175         kalmanBuilder.addPropagationConfiguration(propagatorBuilder, covMatrixProvider);
176         kalmanBuilder.estimatedMeasurementsParameters(estimatedMeasurementsParameters, null);
177         this.kalman = kalmanBuilder.build();
178         this.modelLogger = new ModelLogger();
179         kalman.setObserver(modelLogger);
180     }
181     
182     /** Test of the physical matrices and vectors returned by the methods from KalmanEstimation interface.
183      *  First, we perform a check before any measurement is added. Most of the matrices should be null.
184      *  Then we process a perfect PV at t0 in the Kalman and check the matrices.
185      *  Finally we process a range measurement after t0 in the Kalman and check the matrices.
186      */
187     @Test
188     public void ModelPhysicalOutputsTest() {
189         
190         // Check model at t0 before any measurement is added
191         // -------------------------------------------------
192         checkModelAtT0();
193         
194         
195         // Check model after PV measurement at t0 is added
196         // -----------------------------------------------
197         
198         // Constant process noise covariance matrix Q
199         final RealMatrix Q = covMatrixProvider.getProcessNoiseMatrix(new SpacecraftState(orbit0),
200                                                                      new SpacecraftState(orbit0));
201         
202         // Initial covariance matrix
203         final RealMatrix P0 = covMatrixProvider.getInitialCovarianceMatrix(new SpacecraftState(orbit0));
204         
205         // Physical predicted covariance matrix at t0
206         // State transition matrix is the identity matrix at t0
207         RealMatrix Ppred = P0.add(Q);
208         
209         // Predicted orbit is equal to initial orbit at t0
210         Orbit orbitPred = orbit0;
211         
212         // Expected measurement matrix for a PV measurement is the 6-sized identity matrix for cartesian orbital parameters
213         // + zeros for other estimated parameters
214         RealMatrix expH = MatrixUtils.createRealMatrix(6, M);
215         for (int i = 0; i < 6; i++) {
216             expH.setEntry(i, i, 1.);
217         }
218         
219         // Expected state transition matrix
220         // State transition matrix is the identity matrix at t0
221         RealMatrix expPhi = MatrixUtils.createRealIdentityMatrix(M);
222         // Add PV measurement and check model afterwards
223         checkModelAfterMeasurementAdded(1, pv, Ppred, orbitPred, expPhi, expH);
224 
225 
226         // Check model after range measurement after t0 is added
227         // -----------------------------------------------------
228         
229         // Get the estimated propagator from Kalman filter and propagate it to
230         // range measurement date
231         NumericalPropagator propagator =
232                         propagatorBuilder.buildPropagator(propagatorBuilder.getSelectedNormalizedParameters());
233         
234         // Set derivatives computation for the propagator
235         final String equationName = KalmanEstimator.class.getName() + "-derivatives-";
236         final MatricesHarvester harvester = propagator.setupMatricesComputation(equationName, null, null);
237         
238         // Propagate to range date and get predicted orbit
239         final SpacecraftState scPred = propagator.propagate(range.getDate());
240         orbitPred = scPred.getOrbit();
241         
242         // Expected state transition matrix
243         expPhi = MatrixUtils.createRealIdentityMatrix(M);
244         
245         // Derivatives of the state vector with respect to initial state vector
246         final double[][] dYdY0 =  harvester.getStateTransitionMatrix(scPred).getData();
247         expPhi.setSubMatrix(dYdY0, 0, 0);
248 
249         // Derivatives of SRP coef with respect to state
250         final double[][] dYdPp  = harvester.getParametersJacobian(scPred).getData();
251         expPhi.setSubMatrix(dYdPp, 0, 6);
252         
253         // Estimated cov matrix from last measurement
254         RealMatrix Pest = kalman.getPhysicalEstimatedCovarianceMatrix();
255         
256         // Predicted covariance matrix for this measurement
257         Ppred = expPhi.multiply(Pest.multiplyTransposed(expPhi)).add(Q);
258         
259         // Expected measurement matrix
260         expH = MatrixUtils.createRealMatrix(1, M);
261         // State part
262         EstimatedMeasurement<Range> rangeEstimated = range.estimate(0, 0, new SpacecraftState[] {scPred});
263         final RealMatrix dMdY = MatrixUtils.createRealMatrix(rangeEstimated.getStateDerivatives(0));
264         expH.setSubMatrix(dMdY.getData(), 0, 0);
265         // SRP part
266         final SpacecraftState scTransition = scPred.shiftedBy(-rangeEstimated.getTimeOffset());
267         final double[][] dYdPpTransition = harvester.getParametersJacobian(scTransition).getData();
268         final RealMatrix dMdCr = dMdY.multiply(MatrixUtils.createRealMatrix(dYdPpTransition));
269         expH.setEntry(0, 6, dMdCr.getEntry(0, 0));
270         // Sat range bias part
271         expH.setEntry(0, 7, rangeEstimated.getParameterDerivatives(satRangeBiasDriver)[0]);
272         
273         // Add range measurement and check model afterwards
274         checkModelAfterMeasurementAdded(2, range, Ppred, orbitPred, expPhi, expH);
275     }
276     
277     /** Check the model physical outputs at t0 before any measurement is added. */
278     private void checkModelAtT0() {
279 
280         // Instantiate a Model from attributes
281         final KalmanModel model = new KalmanModel(Collections.singletonList(propagatorBuilder),
282                                                   Collections.singletonList(covMatrixProvider),
283                                                   estimatedMeasurementsParameters,
284                                                   null);
285 
286         // Evaluate at t0
287         // --------------
288         
289         // Time
290         Assert.assertEquals(0., model.getEstimate().getTime(), 0.);
291         Assert.assertEquals(0., model.getCurrentDate().durationFrom(orbit0.getDate()), 0.);
292         
293         // Measurement number
294         Assert.assertEquals(0, model.getCurrentMeasurementNumber());
295         
296         // Normalized state - is zeros
297         final RealVector stateN = model.getEstimate().getState();
298         Assert.assertArrayEquals(new double[M], stateN.toArray(), tol);
299         
300         // Physical state - = initialized
301         final RealVector x = model.getPhysicalEstimatedState();
302         final RealVector expX = MatrixUtils.createRealVector(M);
303         final double[] orbitState0 = new double[6];
304         orbitType.mapOrbitToArray(orbit0, positionAngle, orbitState0, null);
305         expX.setSubVector(0, MatrixUtils.createRealVector(orbitState0));
306         expX.setEntry(6, srpCoefDriver.getReferenceValue());
307         expX.setEntry(7, satRangeBiasDriver.getReferenceValue());
308         final double[] dX = x.subtract(expX).toArray();
309         Assert.assertArrayEquals(new double[M], dX, tol);
310         
311         // Normalized covariance - filled with 1
312         final double[][] Pn = model.getEstimate().getCovariance().getData();
313         final double[][] expPn = new double[M][M];
314         for (int i = 0; i < M; i++) {
315             for (int j = 0; j < M; j++) {
316                 expPn[i][j] = 1.;
317             }
318             Assert.assertArrayEquals("Failed on line " + i, expPn[i], Pn[i], tol);
319         }
320         
321         // Physical covariance = initialized
322         final RealMatrix P   = model.getPhysicalEstimatedCovarianceMatrix();
323         final RealMatrix expP = covMatrixProvider.getInitialCovarianceMatrix(new SpacecraftState(orbit0));
324         final double[][] dP = P.subtract(expP).getData();
325         for (int i = 0; i < M; i++) {
326             Assert.assertArrayEquals("Failed on line " + i, new double[M], dP[i], tol);
327         }
328         
329         // Check that other "physical" matrices are null
330         Assert.assertNull(model.getEstimate().getInnovationCovariance());
331         Assert.assertNull(model.getPhysicalInnovationCovarianceMatrix());
332         Assert.assertNull(model.getEstimate().getKalmanGain());
333         Assert.assertNull(model.getPhysicalKalmanGain());
334         Assert.assertNull(model.getEstimate().getMeasurementJacobian());
335         Assert.assertNull(model.getPhysicalMeasurementJacobian());
336         Assert.assertNull(model.getEstimate().getStateTransitionMatrix());
337         Assert.assertNull(model.getPhysicalStateTransitionMatrix());
338     }
339     
340     /** Add a measurement to the Kalman filter.
341      * Check model physical outputs afterwards.
342      */
343     private void checkModelAfterMeasurementAdded(final int expMeasurementNumber,
344                                                 final ObservedMeasurement<?> meas,
345                                                 final RealMatrix expPpred,
346                                                 final Orbit expOrbitPred,
347                                                 final RealMatrix expPhi,
348                                                 final RealMatrix expH) {
349         
350         // Predicted value of SRP coef and sat range bias
351         // (= value before adding measurement to the filter)
352         final double srpCoefPred = srpCoefDriver.getValue();
353         final double satRangeBiasPred = satRangeBiasDriver.getValue();
354         
355         // Expected predicted measurement
356         final double[] expMeasPred = 
357                         meas.estimate(0, 0,
358                                       new SpacecraftState[] {new SpacecraftState(expOrbitPred)}).getEstimatedValue();
359 
360         // Process PV measurement in Kalman and get model
361         kalman.processMeasurements(Collections.singletonList(meas));
362         KalmanEstimation model = modelLogger.estimation;
363         
364         // Measurement size
365         final int N = meas.getDimension();
366         
367         // Time
368         Assert.assertEquals(0., model.getCurrentDate().durationFrom(expOrbitPred.getDate()), 0.);
369         
370         // Measurement number
371         Assert.assertEquals(expMeasurementNumber, model.getCurrentMeasurementNumber());
372         
373         // State transition matrix
374         final RealMatrix phi    = model.getPhysicalStateTransitionMatrix();
375         final double[][] dPhi   = phi.subtract(expPhi).getData();
376         for (int i = 0; i < M; i++) {
377             Assert.assertArrayEquals("Failed on line " + i, new double[M], dPhi[i], tol*100);
378         }
379         
380         // Measurement matrix
381         final RealMatrix H = model.getPhysicalMeasurementJacobian();
382         final double[][] dH = H.subtract(expH).getData();
383         for (int i = 0; i < N; i++) {
384             Assert.assertArrayEquals("Failed on line " + i, new double[M], dH[i], tol);
385         }
386         
387         // Measurement covariance matrix 
388         final double[] measSigmas = meas.getTheoreticalStandardDeviation();
389         final RealMatrix R = MatrixUtils.createRealMatrix(N, N);
390         for (int i = 0; i < N; i++) {
391             R.setEntry(i, i, measSigmas[i] * measSigmas[i]);
392         }
393         
394         // Innovation matrix
395         final RealMatrix expS = expH.multiply(expPpred.multiplyTransposed(expH)).add(R);
396         final RealMatrix S = model.getPhysicalInnovationCovarianceMatrix();
397         final double[][] dS = S.subtract(expS).getData();
398         for (int i = 0; i < N; i++) {
399             Assert.assertArrayEquals("Failed on line " + i, new double[N], dS[i], tol*1e4);
400         }
401         
402         // Kalman gain
403         final RealMatrix expK = expPpred.multiplyTransposed(expH).multiply(new LUDecomposition(expS).getSolver().getInverse());
404         final RealMatrix K = model.getPhysicalKalmanGain();
405         final double[][] dK = K.subtract(expK).getData();
406         for (int i = 0; i < M; i++) {
407             Assert.assertArrayEquals("Failed on line " + i, new double[N], dK[i], tol*1e5);
408         }
409         
410         // Predicted orbit
411         final Orbit orbitPred = model.getPredictedSpacecraftStates()[0].getOrbit();
412         final PVCoordinates pvOrbitPred = orbitPred.getPVCoordinates();
413         final PVCoordinates expPVOrbitPred = expOrbitPred.getPVCoordinates();
414         final double dpOrbitPred = Vector3D.distance(expPVOrbitPred.getPosition(), pvOrbitPred.getPosition());
415         final double dvOrbitPred = Vector3D.distance(expPVOrbitPred.getVelocity(), pvOrbitPred.getVelocity());
416         Assert.assertEquals(0., dpOrbitPred, tol);
417         Assert.assertEquals(0., dvOrbitPred, tol);
418         
419         // Predicted measurement
420         final double[] measPred = model.getPredictedMeasurement().getEstimatedValue();
421         Assert.assertArrayEquals(expMeasPred, measPred, tol);
422         
423         // Predicted state
424         final double[] orbitPredState = new double[6];
425         orbitPred.getType().mapOrbitToArray(orbitPred, PositionAngle.TRUE, orbitPredState, null);
426         final RealVector expXpred = MatrixUtils.createRealVector(M);
427         for (int i = 0; i < 6; i++) {
428             expXpred.setEntry(i, orbitPredState[i]);
429         }
430         expXpred.setEntry(6, srpCoefPred);
431         expXpred.setEntry(7, satRangeBiasPred);
432         
433         // Innovation vector
434         final RealVector observedMeas  = MatrixUtils.createRealVector(model.getPredictedMeasurement().getObservedValue());
435         final RealVector predictedMeas = MatrixUtils.createRealVector(model.getPredictedMeasurement().getEstimatedValue());
436         final RealVector innovation = observedMeas.subtract(predictedMeas);
437         
438         // Corrected state
439         final RealVector expectedXcor = expXpred.add(expK.operate(innovation));
440         final RealVector Xcor = model.getPhysicalEstimatedState();
441         final double[] dXcor = Xcor.subtract(expectedXcor).toArray();
442         Assert.assertArrayEquals(new double[M], dXcor, tol);
443         
444         // Corrected covariance
445         final RealMatrix expectedPcor = 
446                         (MatrixUtils.createRealIdentityMatrix(M).
447                         subtract(expK.multiply(expH))).multiply(expPpred);
448         final RealMatrix Pcor = model.getPhysicalEstimatedCovarianceMatrix();
449         final double[][] dPcor = Pcor.subtract(expectedPcor).getData();
450         for (int i = 0; i < M; i++) {
451             Assert.assertArrayEquals("Failed on line " + i, new double[M], dPcor[i], tol*1e5);
452         }
453     }
454     
455     /** Return expected Kalman current state using propagator builder and measurement driver list.
456      * @param stateSize the size of the state vector
457      * @param builder propagator builder
458      * @param estimatedMeasurementsParameters estimated measurements parameters
459      * @return expected Kalman state
460      */
461     /*private RealVector getExpectedKalmanState(final int stateSize,
462                                              final NumericalPropagatorBuilder builder,
463                                              ParameterDriversList estimatedMeasurementsParameters) {
464 
465         final double[] kalmanState = new double[stateSize];
466         
467         int i = 0;
468         // Orbital parameters
469         for (ParameterDriver driver : builder.getOrbitalParametersDrivers().getDrivers()) {
470             if (driver.isSelected()) {
471                 kalmanState[i++] = driver.getValue();
472             }
473         }
474         
475         // Propagation parameters
476         for (ParameterDriver driver : builder.getPropagationParametersDrivers().getDrivers()) {
477             if (driver.isSelected()) {
478                 kalmanState[i++] = driver.getValue();
479             }
480         }
481         
482         // Measurement parameters
483         for (ParameterDriver driver : estimatedMeasurementsParameters.getDrivers()) {
484             if (driver.isSelected()) {
485                 kalmanState[i++] = driver.getValue();
486             }
487         }
488         
489         return MatrixUtils.createRealVector(kalmanState);
490     }*/
491     
492     /** Get an array of the scales of the estimated parameters.
493      * @param builder propagator builder
494      * @param estimatedMeasurementsParameters estimated measurements parameters
495      * @return array containing the scales of the estimated parameter
496      */
497     private double[] getParametersScale(final NumericalPropagatorBuilder builder,
498                                        ParameterDriversList estimatedMeasurementsParameters) {
499         final List<Double> scaleList = new ArrayList<>();
500         
501         // Orbital parameters
502         for (ParameterDriver driver : builder.getOrbitalParametersDrivers().getDrivers()) {
503             if (driver.isSelected()) {
504                 scaleList.add(driver.getScale());
505             }
506         }
507         
508         // Propagation parameters
509         for (ParameterDriver driver : builder.getPropagationParametersDrivers().getDrivers()) {
510             if (driver.isSelected()) {
511                 scaleList.add(driver.getScale());
512             }
513         }
514         
515         // Measurement parameters
516         for (ParameterDriver driver : estimatedMeasurementsParameters.getDrivers()) {
517             if (driver.isSelected()) {
518                 scaleList.add(driver.getScale());
519             }
520         }
521         
522         final double[] scales = new double[scaleList.size()];
523         for (int i = 0; i < scaleList.size(); i++) {
524             scales[i] = scaleList.get(i);
525         }
526         return scales;
527     }
528     
529     /** Create a covariance matrix provider with initial and process noise matrix constant and identical.
530      * Each element Pij of the initial covariance matrix equals:
531      * Pij = scales[i]*scales[j]
532      * @param scales scales of the estimated parameters
533      * @return covariance matrix provider
534      */
535     private CovarianceMatrixProvider setInitialCovarianceMatrix(final double[] scales) {
536 
537         final int n = scales.length;
538         final RealMatrix cov = MatrixUtils.createRealMatrix(n, n);
539         for (int i = 0; i < n; i++) {
540             for (int j = 0; j < n; j++) {
541                 cov.setEntry(i, j, scales[i] * scales[j]);
542             }
543         }
544         return new ConstantProcessNoise(cov); 
545     }
546     
547     
548     /** Observer allowing to get Kalman model after a measurement was processed in the Kalman filter. */
549     public class ModelLogger implements KalmanObserver {
550         KalmanEstimation estimation;
551 
552         @Override
553         public void evaluationPerformed(KalmanEstimation estimation) {
554             this.estimation = estimation;
555         }
556     }
557 }