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