1   /* Copyright 2002-2025 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.io.File;
20  import java.io.FileInputStream;
21  import java.io.IOException;
22  import java.net.URISyntaxException;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.List;
26  import java.util.Locale;
27  
28  import org.hipparchus.geometry.euclidean.threed.Vector3D;
29  import org.hipparchus.linear.MatrixUtils;
30  import org.hipparchus.linear.RealMatrix;
31  import org.hipparchus.stat.descriptive.StreamingStatistics;
32  import org.hipparchus.util.FastMath;
33  import org.hipparchus.util.MerweUnscentedTransform;
34  import org.junit.jupiter.api.Assertions;
35  import org.junit.jupiter.api.Test;
36  import org.orekit.Utils;
37  import org.orekit.attitudes.FrameAlignedProvider;
38  import org.orekit.bodies.CelestialBody;
39  import org.orekit.bodies.CelestialBodyFactory;
40  import org.orekit.bodies.OneAxisEllipsoid;
41  import org.orekit.data.DataFilter;
42  import org.orekit.data.DataSource;
43  import org.orekit.data.GzipFilter;
44  import org.orekit.data.UnixCompressFilter;
45  import org.orekit.estimation.measurements.EstimatedMeasurement;
46  import org.orekit.estimation.measurements.ObservableSatellite;
47  import org.orekit.estimation.measurements.ObservedMeasurement;
48  import org.orekit.estimation.measurements.Position;
49  import org.orekit.files.ilrs.CPF;
50  import org.orekit.files.ilrs.CPF.CPFCoordinate;
51  import org.orekit.files.ilrs.CPF.CPFEphemeris;
52  import org.orekit.files.ilrs.CPFParser;
53  import org.orekit.files.rinex.HatanakaCompressFilter;
54  import org.orekit.forces.drag.DragForce;
55  import org.orekit.forces.drag.DragSensitive;
56  import org.orekit.forces.drag.IsotropicDrag;
57  import org.orekit.forces.gravity.potential.GravityFieldFactory;
58  import org.orekit.forces.gravity.potential.ICGEMFormatReader;
59  import org.orekit.forces.gravity.potential.SphericalHarmonicsProvider;
60  import org.orekit.forces.gravity.potential.UnnormalizedSphericalHarmonicsProvider;
61  import org.orekit.forces.radiation.IsotropicRadiationSingleCoefficient;
62  import org.orekit.forces.radiation.RadiationSensitive;
63  import org.orekit.frames.Frame;
64  import org.orekit.frames.FramesFactory;
65  import org.orekit.models.earth.atmosphere.Atmosphere;
66  import org.orekit.models.earth.atmosphere.NRLMSISE00;
67  import org.orekit.models.earth.atmosphere.data.MarshallSolarActivityFutureEstimation;
68  import org.orekit.orbits.CartesianOrbit;
69  import org.orekit.orbits.EquinoctialOrbit;
70  import org.orekit.orbits.Orbit;
71  import org.orekit.orbits.OrbitType;
72  import org.orekit.propagation.BoundedPropagator;
73  import org.orekit.propagation.PropagationType;
74  import org.orekit.propagation.conversion.ClassicalRungeKuttaIntegratorBuilder;
75  import org.orekit.propagation.conversion.DSSTPropagatorBuilder;
76  import org.orekit.propagation.conversion.ODEIntegratorBuilder;
77  import org.orekit.propagation.semianalytical.dsst.forces.DSSTAtmosphericDrag;
78  import org.orekit.propagation.semianalytical.dsst.forces.DSSTForceModel;
79  import org.orekit.propagation.semianalytical.dsst.forces.DSSTNewtonianAttraction;
80  import org.orekit.propagation.semianalytical.dsst.forces.DSSTSolarRadiationPressure;
81  import org.orekit.propagation.semianalytical.dsst.forces.DSSTTesseral;
82  import org.orekit.propagation.semianalytical.dsst.forces.DSSTThirdBody;
83  import org.orekit.propagation.semianalytical.dsst.forces.DSSTZonal;
84  import org.orekit.time.AbsoluteDate;
85  import org.orekit.utils.Constants;
86  import org.orekit.utils.IERSConventions;
87  import org.orekit.utils.ParameterDriver;
88  import org.orekit.utils.TimeStampedPVCoordinates;
89  
90  public class UnscentedSemiAnalyticalKalmanOrbitDeterminationTest {
91  
92      /** Header. */
93      private static final String HEADER = "%-25s\t%16s\t%16s\t%16s\t%16s\t%16s\t%16s\t%16s\t%16s\t%16s\t%16s\t%16s\t%16s\t%16s\t%16s\t%16s\t%16s\t%16s";
94  
95      /** Data line. */
96      private static final String DATA_LINE = "%-25s\t%-16.6f\t%-16.6f\t%16.9f\t%-16.6f\t%-16.6f\t%16.9f\t%-16.6f\t%-16.6f\t%16.9f\t%16.9f\t%16.9f\t%16.9f\t%16.9f\t%16.9f\t%16.9f\t%16.9f\t%16.9f";
97  
98      /** Print. */
99      private static boolean print;
100 
101     /**
102      * Test the Lageos 2 orbit determination based on an unscented Kalman filter.
103      * <p>
104      * Lageos 2 positions from a CPF file are used as observations during the
105      * estimation process.
106      * </p>
107      */
108     @Test
109     public void testLageos() throws URISyntaxException, IOException {
110 
111         // Print
112         print = false;
113 
114         // Configure Orekit data access
115         Utils.setDataRoot("orbit-determination/february-2016:potential/icgem-format");
116         GravityFieldFactory.addPotentialCoefficientsReader(new ICGEMFormatReader("eigen-6s-truncated", true));
117 
118         // Observations
119         final CPFEphemeris observations = initializeObservations("orbit-determination/Lageos2/lageos2_cpf_160213_5441.sgf");
120 
121         // Central body
122         final IERSConventions convention = IERSConventions.IERS_2010;
123         final boolean         simpleEop  = true;
124         final OneAxisEllipsoid centralBody = initializeBody(convention, simpleEop);
125 
126         // Gravity field
127         final int degree = 16;
128         final int order  = 16;
129         final SphericalHarmonicsProvider gravityField = initializeGravityField(degree, order);
130 
131         // Initial orbit
132         final Orbit initialOrbit = initializeOrbit(observations, gravityField);
133 
134         // Initialize propagator
135         final double  step     = 43200.0;
136         final double  surface  = 0.2831331;
137         final double  mass     = 405.38;
138         final boolean useDrag  = false;
139         final boolean useSrp   = true;
140         final boolean useMoon  = true;
141         final boolean useSun   = true;
142         final DSSTPropagatorBuilder propagator = initializePropagator(initialOrbit, centralBody, gravityField,
143                                                                       convention, simpleEop, step,
144                                                                       mass, surface, useDrag, useSrp,
145                                                                       useSun, useMoon);
146 
147         // Measurements
148         final double sigma = 1.0;
149         final List<ObservedMeasurement<?>> measurements = initializeMeasurements(observations, initialOrbit, sigma);
150 
151         // Covariance
152         final RealMatrix orbitalP = MatrixUtils.createRealDiagonalMatrix(new double[] {
153             100.0, 100.0, 100.0, 1.0e-3, 1.0e-3, 1.0e-3
154         });
155 
156         // Cartesian initial covariance matrix
157         final RealMatrix orbitalQ = MatrixUtils.createRealDiagonalMatrix(new double[] {1.0e-9, 1.0e-9, 1.0e-9, 1.0e-12, 1.0e-12, 1.0e-12});
158         final CovarianceMatrixProvider provider = buildCovarianceProvider(orbitalP, orbitalQ, propagator, initialOrbit);
159 
160         // Create estimator and run it
161         final Observer observer = initializeEstimator(propagator, measurements, provider);
162 
163         // Verify
164         final KalmanEstimation    estimation = observer.getEstimation();
165         final StreamingStatistics statX      = observer.getXStatistics();
166         final StreamingStatistics statY      = observer.getYStatistics();
167         final StreamingStatistics statZ      = observer.getZStatistics();
168         Assertions.assertEquals(0.0, statX.getMean(), 1.365e-4);
169         Assertions.assertEquals(0.0, statY.getMean(), 4.931e-4);
170         Assertions.assertEquals(0.0, statZ.getMean(), 3.80e-4);
171         Assertions.assertEquals(0.0, statX.getMin(),  0.027); // Value is negative
172         Assertions.assertEquals(0.0, statY.getMin(),  0.028); // Value is negative
173         Assertions.assertEquals(0.0, statZ.getMin(),  0.026); // Value is negative
174         Assertions.assertEquals(0.0, statX.getMax(),  0.029);
175         Assertions.assertEquals(0.0, statY.getMax(),  0.027);
176         Assertions.assertEquals(0.0, statZ.getMax(),  0.026);
177 
178         // Check that "physical" matrices are not null
179         Assertions.assertNotNull(estimation.getPhysicalInnovationCovarianceMatrix());
180         Assertions.assertNotNull(estimation.getPhysicalKalmanGain());
181 
182         // Verify that station transition and measurement matrices are null
183         Assertions.assertNull(estimation.getPhysicalMeasurementJacobian());
184         Assertions.assertNull(estimation.getPhysicalStateTransitionMatrix());
185 
186     }
187 
188     /**
189      * Initialize the Position/Velocity observations.
190      * @param fileName measurement file name
191      * @return the ephemeris contained in the input file
192      * @throws IOException if observations file cannot be read properly
193      * @throws URISyntaxException if URI syntax is wrong
194      */
195     private CPFEphemeris initializeObservations(final String fileName) throws URISyntaxException, IOException {
196 
197         // Input in tutorial resources directory
198         final String inputPath = UnscentedSemiAnalyticalKalmanOrbitDeterminationTest.class.getClassLoader().
199                                  getResource(fileName).
200                                  toURI().
201                                  getPath();
202         final File file = new File(inputPath);
203 
204         // Set up filtering for measurements files
205         DataSource source = new DataSource(file.getName(), () -> new FileInputStream(new File(file.getParentFile(), file.getName())));
206         for (final DataFilter filter : Arrays.asList(new GzipFilter(),
207                                                      new UnixCompressFilter(),
208                                                      new HatanakaCompressFilter())) {
209             source = filter.filter(source);
210         }
211 
212         // Return the CPF ephemeris for the wanted satellite
213         final CPF cpfFile = new CPFParser().parse(source);
214         return cpfFile.getSatellites().get(cpfFile.getHeader().getIlrsSatelliteId());
215 
216     }
217 
218     /**
219      * Initialize the central body (i.e. the Earth).
220      * @param convention IERS convention
221      * @param simpleEop if true, tidal effects are ignored when interpolating EOP
222      * @return a configured central body
223      */
224     private static OneAxisEllipsoid initializeBody(final IERSConventions convention, final boolean simpleEop) {
225         // Return the configured body
226         return new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
227                                     Constants.WGS84_EARTH_FLATTENING,
228                                     FramesFactory.getITRF(convention, simpleEop));
229     }
230 
231     /**
232      * Initialize the spherical harmonics provider.
233      * @param degree degree
234      * @param order order
235      * @return a configured spherical harmonics provider
236      */
237     private static SphericalHarmonicsProvider initializeGravityField(final int degree, final int order) {
238         return GravityFieldFactory.getUnnormalizedProvider(degree, order);
239     }
240 
241     /**
242      * Initialize initial guess.
243      * <p>
244      * Initial guess corresponds to the first orbit in the CPF file.
245      * It is converted in EME2000 frame.
246      * </p>
247      * @param ephemeris CPF ephemeris
248      * @param gravityField gravity field (used for the central attraction coefficient)
249      * @return the configured orbit
250      */
251     private static Orbit initializeOrbit(final CPFEphemeris ephemeris, final SphericalHarmonicsProvider gravityField) {
252 
253         // Frame
254         final Frame orbitFrame = FramesFactory.getEME2000();
255 
256         // Bounded propagator from the CPF file
257         final BoundedPropagator bounded = ephemeris.getPropagator(new FrameAlignedProvider(orbitFrame));
258 
259         // Initial date
260         final AbsoluteDate initialDate = bounded.getMinDate();
261 
262         // Initial orbit
263         final TimeStampedPVCoordinates pvInitITRF = bounded.getPVCoordinates(initialDate, ephemeris.getFrame());
264         final TimeStampedPVCoordinates pvInitInertial = ephemeris.getFrame().getTransformTo(orbitFrame, initialDate).
265                                                                              transformPVCoordinates(pvInitITRF);
266 
267         // Return orbit (in J2000 frame)
268         return new CartesianOrbit(new TimeStampedPVCoordinates(pvInitInertial.getDate(),
269                                                                new Vector3D(pvInitInertial.getPosition().getX(),
270                                                                             pvInitInertial.getPosition().getY(),
271                                                                             pvInitInertial.getPosition().getZ()),
272                                                                new Vector3D(pvInitInertial.getVelocity().getX(),
273                                                                             pvInitInertial.getVelocity().getY(),
274                                                                             pvInitInertial.getVelocity().getZ())),
275                                   orbitFrame, gravityField.getMu());
276 
277     }
278 
279     /**
280      * Initialize the propagator builder.
281      * @param orbit initial guess
282      * @param centralBody central body
283      * @param gravityField gravity field
284      * @param convention IERS convention
285      * @param simpleEop if true, tidal effects are ignored when interpolating EOP
286      * @param mass spacecraft mass (kg)
287      * @param surface surface (m²)
288      * @param useDrag true if drag acceleration must be added
289      * @param useSrp true if acceleration due to the solar radiation pressure must be added
290      * @param useSun true if gravitational acceleration due to the Sun attraction must be added
291      * @param useMoon true if gravitational acceleration due to the Moon attraction must be added
292      * @return a configured propagator builder
293      */
294     private static DSSTPropagatorBuilder initializePropagator(final Orbit orbit,
295                                                               final OneAxisEllipsoid centralBody,
296                                                               final SphericalHarmonicsProvider gravityField,
297                                                               final IERSConventions convention, final boolean simpleEop,
298                                                               final double step, final double mass, final double surface,
299                                                               final boolean useDrag, final boolean useSrp,
300                                                               final boolean useSun, final boolean useMoon) {
301 
302         // Initialize numerical integrator
303         final ODEIntegratorBuilder integrator = new ClassicalRungeKuttaIntegratorBuilder(step);
304 
305         // Convert initial orbit in equinoctial elements
306         final EquinoctialOrbit equinoctial = (EquinoctialOrbit) OrbitType.EQUINOCTIAL.convertType(orbit);
307 
308         // Initialize the numerical builder
309         final DSSTPropagatorBuilder propagator = new DSSTPropagatorBuilder(equinoctial, integrator, 1.0, PropagationType.MEAN, PropagationType.OSCULATING);
310 
311         // Add force models to the numerical propagator
312         addDSSTForceModels(propagator, orbit, centralBody, gravityField, convention, simpleEop, surface, useDrag, useSrp, useSun, useMoon);
313 
314         // Mass
315         propagator.setMass(mass);
316 
317         // Reset the orbit
318         propagator.resetOrbit(orbit);
319 
320         // Return the fully configured propagator builder
321         return propagator;
322 
323     }
324 
325     /**
326      * Add the force models to the DSST propagator.
327      * @param propagator propagator
328      * @param initialOrbit initial orbit
329      * @param centralBody central body
330      * @param gravityField gravity field
331      * @param convention IERS convention
332      * @param simpleEop if true, tidal effects are ignored when interpolating EOP
333      * @param surface surface of the satellite (m²)
334      * @param useDrag true if drag acceleration must be added
335      * @param useSrp true if acceleration due to the solar radiation pressure must be added
336      * @param useSun true if gravitational acceleration due to the Sun attraction must be added
337      * @param useMoon true if gravitational acceleration due to the Moon attraction must be added
338      */
339     private static void addDSSTForceModels(final DSSTPropagatorBuilder propagator,
340                                            final Orbit initialOrbit,
341                                            final OneAxisEllipsoid centralBody,
342                                            final SphericalHarmonicsProvider gravityField,
343                                            final IERSConventions convention, final boolean simpleEop,
344                                            final double surface,
345                                            final boolean useDrag, final boolean useSrp,
346                                            final boolean useSun, final boolean useMoon) {
347 
348         // List of celestial bodies used for solid tides
349         final List<CelestialBody> solidTidesBodies = new ArrayList<>();
350 
351         // Drag
352         if (useDrag) {
353 
354             // Atmosphere model
355             final MarshallSolarActivityFutureEstimation msafe =
356                             new MarshallSolarActivityFutureEstimation(MarshallSolarActivityFutureEstimation.DEFAULT_SUPPORTED_NAMES,
357                                                                       MarshallSolarActivityFutureEstimation.StrengthLevel.AVERAGE);
358             final Atmosphere atmosphere = new NRLMSISE00(msafe, CelestialBodyFactory.getSun(), centralBody);
359 
360             // Drag force
361             // Assuming spherical satellite
362             final DSSTForceModel drag = new DSSTAtmosphericDrag(new DragForce(atmosphere, new IsotropicDrag(surface, 1.0)), gravityField.getMu());
363             for (final ParameterDriver driver : drag.getParametersDrivers()) {
364                 if (driver.getName().equals(DragSensitive.DRAG_COEFFICIENT)) {
365                     driver.setSelected(true);
366                 }
367             }
368 
369             // Add the force model
370             propagator.addForceModel(drag);
371 
372         }
373 
374         // Solar radiation pressure
375         if (useSrp) {
376 
377             // Satellite model (spherical)
378             final RadiationSensitive spacecraft = new IsotropicRadiationSingleCoefficient(surface, 1.13);
379 
380             // Solar radiation pressure
381             final DSSTForceModel srp = new DSSTSolarRadiationPressure(CelestialBodyFactory.getSun(), centralBody, spacecraft, gravityField.getMu());
382             for (final ParameterDriver driver : srp.getParametersDrivers()) {
383                 if (driver.getName().equals(RadiationSensitive.REFLECTION_COEFFICIENT)) {
384                     //driver.setSelected(true);
385                 }
386             }
387 
388             // Add the force model
389             propagator.addForceModel(srp);
390 
391         }
392 
393         // Sun
394         if (useSun) {
395             solidTidesBodies.add(CelestialBodyFactory.getSun());
396             propagator.addForceModel(new DSSTThirdBody(CelestialBodyFactory.getSun(), gravityField.getMu()));
397         }
398 
399         // Moon
400         if (useMoon) {
401             solidTidesBodies.add(CelestialBodyFactory.getMoon());
402             propagator.addForceModel(new DSSTThirdBody(CelestialBodyFactory.getMoon(), gravityField.getMu()));
403         }
404 
405         // Potential
406         propagator.addForceModel(new DSSTTesseral(centralBody.getBodyFrame(), Constants.WGS84_EARTH_ANGULAR_VELOCITY, (UnnormalizedSphericalHarmonicsProvider) gravityField));
407         propagator.addForceModel(new DSSTZonal((UnnormalizedSphericalHarmonicsProvider) gravityField));
408 
409         // Newton
410         propagator.addForceModel(new DSSTNewtonianAttraction(gravityField.getMu()));
411 
412     }
413 
414     /**
415      * Initialize the list of measurements.
416      * @param ephemeris CPF ephemeris
417      * @param orbit initial guess (used for orbit determination epoch)
418      * @param sigma standard deviation for position measurement
419      * @return the list of measurements
420      */
421     private static List<ObservedMeasurement<?>> initializeMeasurements(final CPFEphemeris ephemeris,
422                                                                        final Orbit orbit,
423                                                                        final double sigma) {
424 
425         // Satellite
426         final ObservableSatellite satellite = new ObservableSatellite(0);
427 
428         // Initialize an empty list of measurements
429         final List<ObservedMeasurement<?>> measurements = new ArrayList<>();
430 
431         // Loop on measurements
432         for (final CPFCoordinate coordinate : ephemeris.getCoordinates()) {
433 
434             // Position in inertial frames
435             final Vector3D posInertial = ephemeris.getFrame().getStaticTransformTo(orbit.getFrame(), coordinate.getDate()).
436                                                               transformPosition(coordinate.getPosition());
437 
438             // Initialize measurement
439             final Position measurement = new Position(coordinate.getDate(), posInertial, sigma, 1.0, satellite);
440 
441             // Add the measurement to the list
442             measurements.add(measurement);
443 
444         }
445 
446         // Return the filled list
447         return measurements;
448 
449     }
450 
451     /**
452      * Initialize the estimator used for the orbit determination and run the estimation.
453      * @param propagator orbit propagator
454      * @param measurements list of measurements
455      * @param provider covariance matrix provider
456      */
457     private static Observer initializeEstimator(final DSSTPropagatorBuilder propagator,
458                                                 final List<ObservedMeasurement<?>> measurements,
459                                                 final CovarianceMatrixProvider provider) {
460 
461         // Initialize builder
462         final SemiAnalyticalUnscentedKalmanEstimatorBuilder builder = new SemiAnalyticalUnscentedKalmanEstimatorBuilder();
463 
464         // Add the propagation configuration
465         builder.addPropagationConfiguration(propagator, provider);
466 
467         // Unscented transform provider
468         builder.unscentedTransformProvider(new MerweUnscentedTransform(propagator.getSelectedNormalizedParameters().length));
469 
470         // Build filter
471         final SemiAnalyticalUnscentedKalmanEstimator estimator = builder.build();
472 
473         final Observer observer = new Observer();
474         estimator.setObserver(observer);
475 
476         // Estimation
477         estimator.processMeasurements(measurements);
478 
479         // Return the observer
480         return observer;
481 
482     }
483 
484     /**
485      * Build the covariance matrix provider.
486      * @param initialNoiseMatrix initial process noise
487      * @param processNoiseMatrix constant process noise
488      * @param propagatorBuilder propagator builder
489      * @param orbit initial orbit
490      * @return the covariance matrix provider
491      */
492     private static CovarianceMatrixProvider buildCovarianceProvider(final RealMatrix initialNoiseMatrix, final RealMatrix processNoiseMatrix,
493                                                                     final DSSTPropagatorBuilder propagatorBuilder, final Orbit orbit)  {
494         // Jacobian of the orbital parameters w/r to Cartesian
495         final Orbit initialOrbit = OrbitType.EQUINOCTIAL.convertType(orbit);
496         final double[][] dYdC = new double[6][6];
497         initialOrbit.getJacobianWrtCartesian(propagatorBuilder.getPositionAngleType(), dYdC);
498         final RealMatrix Jac = MatrixUtils.createRealMatrix(dYdC);
499 
500         // Keplerian initial covariance matrix
501         final RealMatrix orbitalP = Jac.multiply(initialNoiseMatrix.multiply(Jac.transpose()));
502 
503         // Return
504         return new ConstantProcessNoise(orbitalP, processNoiseMatrix);
505     }
506 
507     /** Observer for Kalman estimation. */
508     public static class Observer implements KalmanObserver {
509 
510         /** Statistics on X position residuals. */
511         private StreamingStatistics statX;
512 
513         /** Statistics on Y position residuals. */
514         private StreamingStatistics statY;
515 
516         /** Statistics on Z position residuals. */
517         private StreamingStatistics statZ;
518 
519         /** Kalman estimation. */
520         private KalmanEstimation estimation;
521 
522         /** Constructor. */
523         public Observer() {
524             statX = new StreamingStatistics();
525             statY = new StreamingStatistics();
526             statZ = new StreamingStatistics();
527             if (print) {
528                 final String header = String.format(Locale.US, HEADER, "Epoch",
529                                                     "X Observed (m)", "X Estimated (m)", "X residual (m)",
530                                                     "Y Observed (m)", "Y Estimated (m)", "Y residual (m)",
531                                                     "Z Observed (m)", "Z Estimated (m)", "Z residual (m)",
532                                                     "Cov(0;0)", "Cov(1;1)", "Cov(2;2)", "Cov(3;3)", "Cov(4;4)", "Cov(5;5)",
533                                                     "3D Pos Cov (m)", "3D Vel Cov (m)");
534                 System.out.println(header);
535             }
536 
537         }
538 
539         /** {@inheritDoc} */
540         @Override
541         public void evaluationPerformed(final KalmanEstimation estimation) {
542 
543             // Estimated and observed measurements
544             final EstimatedMeasurement<?> estimatedMeasurement = estimation.getCorrectedMeasurement();
545 
546             // Check
547             if (estimatedMeasurement.getObservedMeasurement() instanceof Position) {
548 
549                 if (estimatedMeasurement.getStatus() == EstimatedMeasurement.Status.REJECTED) {
550                     if (print) {
551                         System.out.println("REJECTED");
552                     }
553                 } else {
554                     final double[] estimated = estimatedMeasurement.getEstimatedValue();
555                     final double[] observed  = estimatedMeasurement.getObservedValue();
556 
557                     // Observed
558                     final double observedX = observed[0];
559                     final double observedY = observed[1];
560                     final double observedZ = observed[2];
561 
562                     // Estimated
563                     final double estimatedX = estimated[0];
564                     final double estimatedY = estimated[1];
565                     final double estimatedZ = estimated[2];
566 
567                     // Calculate residuals
568                     final double resX  = estimatedX - observedX;
569                     final double resY  = estimatedY - observedY;
570                     final double resZ  = estimatedZ - observedZ;
571                     statX.addValue(resX);
572                     statY.addValue(resY);
573                     statZ.addValue(resZ);
574 
575                     if (print) {
576 
577                         // Covariance (diagonal elements)
578                         final RealMatrix covariance = estimation.getPhysicalEstimatedCovarianceMatrix();
579                         final double cov11 = covariance.getEntry(0, 0);
580                         final double cov22 = covariance.getEntry(1, 1);
581                         final double cov33 = covariance.getEntry(2, 2);
582                         final double cov44 = covariance.getEntry(3, 3);
583                         final double cov55 = covariance.getEntry(4, 4);
584                         final double cov66 = covariance.getEntry(5, 5);
585                         final double cPos  = FastMath.sqrt(cov11 + cov22 + cov33);
586                         final double cVel  = FastMath.sqrt(cov44 + cov55 + cov66);
587 
588                         // Add measurement line
589                         final String line = String.format(Locale.US, DATA_LINE, estimatedMeasurement.getDate(),
590                                                           observedX, estimatedX, resX,
591                                                           observedY, estimatedY, resY,
592                                                           observedZ, estimatedZ, resZ,
593                                                           cov11, cov22, cov33, cov44, cov55, cov66,
594                                                           cPos, cVel);
595                         System.out.println(line);
596                     }
597 
598                 }
599 
600             }
601 
602             this.estimation = estimation;
603 
604         }
605 
606         /**
607          * Get the statistics on the X coordinate residuals.
608          * @return the statistics on the X coordinate residuals
609          */
610         public StreamingStatistics getXStatistics() {
611             if (print) {
612                 System.out.println("Min X res (m): " + statX.getMin() + " Max X res (m): " + statX.getMax() + " Mean X res (m): " + statX.getMean() + " STD: " + statX.getStandardDeviation());
613             }
614             return statX;
615         }
616 
617         /**
618          * Get the statistics on the Y coordinate residuals.
619          * @return the statistics on the Y coordinate residuals
620          */
621         public StreamingStatistics getYStatistics() {
622             if (print) {
623                 System.out.println("Min Y res (m): " + statY.getMin() + " Max Y res (m): " + statY.getMax() + " Mean Y res (m): " + statY.getMean() + " STD: " + statY.getStandardDeviation());
624             }
625             return statY;
626         }
627 
628         /**
629          * Get the statistics on the Z coordinate residuals.
630          * @return the statistics on the Z coordinate residuals
631          */
632         public StreamingStatistics getZStatistics() {
633             if (print) {
634                 System.out.println("Min Z res (m): " + statZ.getMin() + " Max Z res (m): " + statZ.getMax() + " Mean Z res (m): " + statZ.getMean() + " STD: " + statZ.getStandardDeviation());
635             }
636             return statZ;
637         }
638 
639         /**
640          * Get the Kalman estimation.
641          * @return the Kalman estimation
642          */
643         public KalmanEstimation getEstimation() {
644             return estimation;
645         }
646 
647     }
648 
649 }