1   /* Copyright 2022-2025 Thales Alenia Space
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.measurements.gnss;
18  
19  import org.hipparchus.geometry.euclidean.threed.Vector3D;
20  import org.hipparchus.stat.descriptive.moment.Mean;
21  import org.hipparchus.stat.descriptive.rank.Max;
22  import org.hipparchus.stat.descriptive.rank.Median;
23  import org.hipparchus.stat.descriptive.rank.Min;
24  import org.hipparchus.util.FastMath;
25  import org.junit.jupiter.api.Assertions;
26  import org.junit.jupiter.api.Test;
27  import org.orekit.estimation.Context;
28  import org.orekit.estimation.EstimationTestUtils;
29  import org.orekit.estimation.measurements.EstimatedMeasurementBase;
30  import org.orekit.estimation.measurements.ObservedMeasurement;
31  import org.orekit.orbits.CartesianOrbit;
32  import org.orekit.orbits.Orbit;
33  import org.orekit.orbits.OrbitType;
34  import org.orekit.orbits.PositionAngleType;
35  import org.orekit.propagation.BoundedPropagator;
36  import org.orekit.propagation.EphemerisGenerator;
37  import org.orekit.propagation.Propagator;
38  import org.orekit.propagation.SpacecraftState;
39  import org.orekit.propagation.conversion.NumericalPropagatorBuilder;
40  import org.orekit.time.AbsoluteDate;
41  import org.orekit.utils.Constants;
42  import org.orekit.utils.Differentiation;
43  import org.orekit.utils.ParameterDriver;
44  import org.orekit.utils.ParameterFunction;
45  import org.orekit.utils.TimeSpanMap.Span;
46  import org.orekit.utils.TimeStampedPVCoordinates;
47  
48  import java.util.ArrayList;
49  import java.util.Comparator;
50  import java.util.List;
51  import java.util.Locale;
52  
53  public class OneWayGNSSRangeRateTest {
54  
55      /**
56       * Test the values of the range rate comparing the observed values and the estimated values
57       * Both are calculated with a different algorithm
58       */
59      @Test
60      public void testValues() {
61          boolean printResults = false;
62          if (printResults) {
63              System.out.println("\nTest One-way GNSS range rate Values\n");
64          }
65          // Run test
66          this.genericTestValues(printResults);
67      }
68  
69      /**
70       * Test the values of the state derivatives using a numerical
71       * finite differences calculation as a reference
72       */
73      @Test
74      public void testStateDerivatives() {
75  
76          boolean printResults = false;
77          if (printResults) {
78              System.out.println("\nTest One-way GNSS range rate State Derivatives - Finite Differences Comparison\n");
79          }
80          // Run test
81          // the following relative tolerances for derivatives with respect to velocity
82          // may seem high, but they have been validated. The partial derivative of
83          // signal flight time with respect to velocity ∂τ/∂{vx, vy, vz} is about 10⁻¹³
84          // when the signal flight time τ is about 10⁻⁴, so finite differences lose
85          // about 9 significant figures, so it is expected that partial derivatives
86          // computed with finite differences will only have a few digits corrects and
87          // that there will be outliers
88          double refErrorsPMedian = 5.6e-10;
89          double refErrorsPMean   = 3.4e-09;
90          double refErrorsPMax    = 6.8e-07;
91          double refErrorsVMedian = 6.6e-11;
92          double refErrorsVMean   = 1.8e-10;
93          double refErrorsVMax    = 7.1e-09;
94          this.genericTestStateDerivatives(printResults, 0,
95                                           refErrorsPMedian, refErrorsPMean, refErrorsPMax,
96                                           refErrorsVMedian, refErrorsVMean, refErrorsVMax);
97      }
98  
99      /**
100      * Test the values of the parameters' derivatives using a numerical
101      * finite differences calculation as a reference
102      */
103     @Test
104     public void testParameterDerivatives() {
105 
106         // Print the results ?
107         boolean printResults = false;
108 
109         if (printResults) {
110             System.out.println("\nTest One-way GNSS range rate Derivatives - Finite Differences Comparison\n");
111         }
112         // Run test
113         double refErrorsMedian = 6.7e-8;
114         double refErrorsMean   = 2.1e-7;
115         double refErrorsMax    = 2.8e-6;
116         this.genericTestParameterDerivatives(printResults,
117                                              refErrorsMedian, refErrorsMean, refErrorsMax);
118 
119     }
120 
121     /**
122      * Generic test function for values of the one-way GNSS range
123      * @param printResults Print the results ?
124      */
125     void genericTestValues(final boolean printResults) {
126 
127         Context context = EstimationTestUtils.eccentricContext("regular-data:potential:tides");
128 
129         final NumericalPropagatorBuilder propagatorBuilder =
130                         context.createBuilder(OrbitType.KEPLERIAN, PositionAngleType.TRUE, true,
131                                               1.0e-6, 60.0, 0.001);
132 
133         // Create perfect inter-satellites range rate measurements
134         final TimeStampedPVCoordinates original = context.initialOrbit.getPVCoordinates();
135         final Orbit closeOrbit = new CartesianOrbit(new TimeStampedPVCoordinates(context.initialOrbit.getDate(),
136                                                                                  original.getPosition().add(new Vector3D(1000, 2000, 3000)),
137                                                                                  original.getVelocity().add(new Vector3D(-0.03, 0.01, 0.02))),
138                                                     context.initialOrbit.getFrame(),
139                                                     context.initialOrbit.getMu());
140         final Propagator closePropagator = EstimationTestUtils.createPropagator(closeOrbit,
141                                                                                 propagatorBuilder);
142         final EphemerisGenerator generator = closePropagator.getEphemerisGenerator();
143         closePropagator.propagate(context.initialOrbit.getDate().shiftedBy(3.5 * closeOrbit.getKeplerianPeriod()));
144         final BoundedPropagator ephemeris = generator.getGeneratedEphemeris();
145         final Propagator propagator = EstimationTestUtils.createPropagator(context.initialOrbit,
146                                                                            propagatorBuilder);
147 
148         final double localClockOffset        =   0.14e-06;
149         final double localClockRate          =  -0.12e-10;
150         final double localClockAcceleration  =   1.4e-13;
151         final double remoteClockOffset       = 469.0e-06;
152         final double remoteClockRate         =  33.0e-10;
153         final double remoteClockAcceleration =   0.5e-13;
154         final List<ObservedMeasurement<?>> measurements =
155                         EstimationTestUtils.createMeasurements(propagator,
156                                                                new OneWayGNSSRangeRateCreator(ephemeris,
157                                                                                               localClockOffset, localClockRate, localClockAcceleration,
158                                                                                               remoteClockOffset, remoteClockRate, remoteClockAcceleration),
159                                                                1.0, 3.0, 300.0);
160 
161         // Lists for results' storage - Used only for derivatives with respect to state
162         // "final" value to be seen by "handleStep" function of the propagator
163         final List<Double> absoluteErrors = new ArrayList<>();
164         final List<Double> relativeErrors = new ArrayList<>();
165 
166         // Use a lambda function to implement "handleStep" function
167         propagator.setStepHandler(interpolator -> {
168 
169             for (final ObservedMeasurement<?> measurement : measurements) {
170 
171                 //  Play test if the measurement date is between interpolator previous and current date
172                 if ((measurement.getDate().durationFrom(interpolator.getPreviousState().getDate()) > 0.) &&
173                     (measurement.getDate().durationFrom(interpolator.getCurrentState().getDate())  <=  0.)
174                    ) {
175                     // We intentionally propagate to a date which is close to the
176                     // real spacecraft state but is *not* the accurate date, by
177                     // compensating only part of the downlink delay. This is done
178                     // in order to validate the partial derivatives with respect
179                     // to velocity.
180                     final double          meanDelay = measurement.getObservedValue()[0] / Constants.SPEED_OF_LIGHT;
181                     final AbsoluteDate    date      = measurement.getDate().shiftedBy(-0.75 * meanDelay);
182                     final SpacecraftState state     = interpolator.getInterpolatedState(date);
183 
184                     // Values of the range rate & errors
185                     final double rangeObserved  = measurement.getObservedValue()[0];
186                     final EstimatedMeasurementBase<?> estimated = measurement.estimateWithoutDerivatives(new SpacecraftState[] {
187                                                                                                              state,
188                                                                                                              ephemeris.propagate(state.getDate())
189                                                                                                          });
190 
191                     final double rangeEstimated = estimated.getEstimatedValue()[0];
192                     final double absoluteError = rangeEstimated-rangeObserved;
193                     absoluteErrors.add(absoluteError);
194                     relativeErrors.add(FastMath.abs(absoluteError)/FastMath.abs(rangeObserved));
195 
196                     // Print results on console ?
197                     if (printResults) {
198                         final AbsoluteDate measurementDate = measurement.getDate();
199 
200                         System.out.format(Locale.US, "%-23s  %-23s  %19.6f  %19.6f  %13.6e  %13.6e%n",
201                                          measurementDate, date,
202                                          rangeObserved, rangeEstimated,
203                                          FastMath.abs(rangeEstimated-rangeObserved),
204                                          FastMath.abs((rangeEstimated-rangeObserved)/rangeObserved));
205                     }
206 
207                 } // End if measurement date between previous and current interpolator step
208             } // End for loop on the measurements
209         }); // End lambda function handlestep
210 
211         // Print results on console ? Header
212         if (printResults) {
213             System.out.format(Locale.US, "%-23s  %-23s  %19s  %19s  %19s  %19s%n",
214                               "Measurement Date", "State Date",
215                               "range rate observed [m/s]", "range rate estimated [m/s]",
216                               "Δrange rate[m/s]", "rel ΔRange rate");
217         }
218 
219         // Rewind the propagator to initial date
220         propagator.propagate(context.initialOrbit.getDate());
221 
222         // Sort measurements chronologically
223         measurements.sort(Comparator.naturalOrder());
224 
225         // Propagate to final measurement's date
226         propagator.propagate(measurements.get(measurements.size()-1).getDate());
227 
228         // Convert lists to double array
229         final double[] absErrors = absoluteErrors.stream().mapToDouble(Double::doubleValue).toArray();
230         final double[] relErrors = relativeErrors.stream().mapToDouble(Double::doubleValue).toArray();
231 
232         // Statistics' assertion
233         final double absErrorsMedian = new Median().evaluate(absErrors);
234         final double absErrorsMin    = new Min().evaluate(absErrors);
235         final double absErrorsMax    = new Max().evaluate(absErrors);
236         final double relErrorsMedian = new Median().evaluate(relErrors);
237         final double relErrorsMax    = new Max().evaluate(relErrors);
238 
239         // Print the results on console ? Final results
240         if (printResults) {
241             System.out.println();
242             System.out.println("Absolute errors median: " +  absErrorsMedian);
243             System.out.println("Absolute errors min   : " +  absErrorsMin);
244             System.out.println("Absolute errors max   : " +  absErrorsMax);
245             System.out.println("Relative errors median: " +  relErrorsMedian);
246             System.out.println("Relative errors max   : " +  relErrorsMax);
247         }
248 
249         Assertions.assertEquals(0.0, absErrorsMedian, 3.9e-9);
250         Assertions.assertEquals(0.0, absErrorsMin,    1.2e-11);
251         Assertions.assertEquals(0.0, absErrorsMax,    1.6e-8);
252         Assertions.assertEquals(0.0, relErrorsMedian, 1.1e-9);
253         Assertions.assertEquals(0.0, relErrorsMax,    1.1e-7);
254 
255         // Test measurement type
256         Assertions.assertEquals(OneWayGNSSRangeRate.MEASUREMENT_TYPE, measurements.get(0).getMeasurementType());
257     }
258 
259     void genericTestStateDerivatives(final boolean printResults, final int index,
260                                      final double refErrorsPMedian, final double refErrorsPMean, final double refErrorsPMax,
261                                      final double refErrorsVMedian, final double refErrorsVMean, final double refErrorsVMax) {
262 
263         Context context = EstimationTestUtils.eccentricContext("regular-data:potential:tides");
264 
265         final NumericalPropagatorBuilder propagatorBuilder =
266                         context.createBuilder(OrbitType.KEPLERIAN, PositionAngleType.TRUE, true,
267                                               1.0e-6, 60.0, 0.001);
268 
269         // Create perfect one-way GNSS range rate measurements
270         final TimeStampedPVCoordinates original = context.initialOrbit.getPVCoordinates();
271         final Orbit closeOrbit = new CartesianOrbit(new TimeStampedPVCoordinates(context.initialOrbit.getDate(),
272                                                                                  original.getPosition().add(new Vector3D(1000, 2000, 3000)),
273                                                                                  original.getVelocity().add(new Vector3D(-0.03, 0.01, 0.02))),
274                                                     context.initialOrbit.getFrame(),
275                                                     context.initialOrbit.getMu());
276         final Propagator closePropagator = EstimationTestUtils.createPropagator(closeOrbit,
277                                                                                 propagatorBuilder);
278         final EphemerisGenerator generator = closePropagator.getEphemerisGenerator();
279         closePropagator.propagate(context.initialOrbit.getDate().shiftedBy(3.5 * closeOrbit.getKeplerianPeriod()));
280         final BoundedPropagator ephemeris = generator.getGeneratedEphemeris();
281         final Propagator propagator = EstimationTestUtils.createPropagator(context.initialOrbit,
282                                                                            propagatorBuilder);
283 
284         final double localClockOffset        =   0.14e-06;
285         final double localClockRate          =  -0.12e-10;
286         final double localClockAcceleration  =   1.4e-13;
287         final double remoteClockOffset       = 469.0e-06;
288         final double remoteClockRate         =  33.0e-10;
289         final double remoteClockAcceleration =   0.5e-13;
290         final List<ObservedMeasurement<?>> measurements =
291                         EstimationTestUtils.createMeasurements(propagator,
292                                                                new OneWayGNSSRangeRateCreator(ephemeris,
293                                                                                               localClockOffset, localClockRate, localClockAcceleration,
294                                                                                               remoteClockOffset, remoteClockRate, remoteClockAcceleration),
295                                                                1.0, 3.0, 300.0);
296 
297         // Lists for results' storage - Used only for derivatives with respect to state
298         // "final" value to be seen by "handleStep" function of the propagator
299         final List<Double> errorsP = new ArrayList<>();
300         final List<Double> errorsV = new ArrayList<>();
301 
302         // Use a lambda function to implement "handleStep" function
303         propagator.setStepHandler(interpolator -> {
304 
305             for (final ObservedMeasurement<?> measurement : measurements) {
306 
307                 //  Play test if the measurement date is between interpolator previous and current date
308                 if ((measurement.getDate().durationFrom(interpolator.getPreviousState().getDate()) > 0.) &&
309                     (measurement.getDate().durationFrom(interpolator.getCurrentState().getDate())  <=  0.)
310                    ) {
311 
312                     // We intentionally propagate to a date which is close to the
313                     // real spacecraft state but is *not* the accurate date, by
314                     // compensating only part of the downlink delay. This is done
315                     // in order to validate the partial derivatives with respect
316                     // to velocity.
317                     final double            meanDelay = measurement.getObservedValue()[0] / Constants.SPEED_OF_LIGHT;
318                     final AbsoluteDate      date      = measurement.getDate().shiftedBy(-0.9 * meanDelay);
319                     final SpacecraftState[] states    = {
320                         interpolator.getInterpolatedState(date),
321                         ephemeris.propagate(date)
322                     };
323                     final double[][]      jacobian  = measurement.estimate(0, 0, states).getStateDerivatives(index);
324 
325                     // Jacobian reference value
326                     final double[][] jacobianRef;
327 
328                     // Compute a reference value using finite differences
329                     jacobianRef = Differentiation.differentiate(state -> {
330                         final SpacecraftState[] s = states.clone();
331                         s[index] = state;
332                         return measurement.estimateWithoutDerivatives(s).getEstimatedValue();
333                     }, measurement.getDimension(), propagator.getAttitudeProvider(),
334                                                                 OrbitType.CARTESIAN, PositionAngleType.TRUE, 8.0, 5).value(states[index]);
335 
336                     Assertions.assertEquals(jacobianRef.length, jacobian.length);
337                     Assertions.assertEquals(jacobianRef[0].length, jacobian[0].length);
338 
339                     // Errors & relative errors on the Jacobian
340                     double [][] dJacobian         = new double[jacobian.length][jacobian[0].length];
341                     double [][] dJacobianRelative = new double[jacobian.length][jacobian[0].length];
342                     for (int i = 0; i < jacobian.length; ++i) {
343                         for (int j = 0; j < jacobian[i].length; ++j) {
344                             dJacobian[i][j] = jacobian[i][j] - jacobianRef[i][j];
345                             dJacobianRelative[i][j] = FastMath.abs(dJacobian[i][j]/jacobianRef[i][j]);
346 
347                             if (j < 3) { errorsP.add(dJacobianRelative[i][j]);
348                             } else { errorsV.add(dJacobianRelative[i][j]); }
349                         }
350                     }
351                     // Print values in console ?
352                     if (printResults) {
353                         System.out.format(Locale.US, "%-23s  %-23s  " +
354                                         "%10.3e  %10.3e  %10.3e  " +
355                                         "%10.3e  %10.3e  %10.3e  " +
356                                         "%10.3e  %10.3e  %10.3e  " +
357                                         "%10.3e  %10.3e  %10.3e%n",
358                                         measurement.getDate(), date,
359                                         dJacobian[0][0], dJacobian[0][1], dJacobian[0][2],
360                                         dJacobian[0][3], dJacobian[0][4], dJacobian[0][5],
361                                         dJacobianRelative[0][0], dJacobianRelative[0][1], dJacobianRelative[0][2],
362                                         dJacobianRelative[0][3], dJacobianRelative[0][4], dJacobianRelative[0][5]);
363                     }
364                 } // End if measurement date between previous and current interpolator step
365             } // End for loop on the measurements
366         });
367 
368         // Print results on console ?
369         if (printResults) {
370             System.out.format(Locale.US, "%-23s  %-23s  " +
371                             "%10s  %10s  %10s  " +
372                             "%10s  %10s  %10s  " +
373                             "%10s  %10s  %10s  " +
374                             "%10s  %10s  %10s%n",
375                             "Measurement Date", "State Date",
376                             "ΔdPx", "ΔdPy", "ΔdPz", "ΔdVx", "ΔdVy", "ΔdVz",
377                             "rel ΔdPx", "rel ΔdPy", "rel ΔdPz",
378                             "rel ΔdVx", "rel ΔdVy", "rel ΔdVz");
379         }
380 
381         // Rewind the propagator to initial date
382         propagator.propagate(context.initialOrbit.getDate());
383 
384         // Sort measurements, primarily chronologically
385         measurements.sort(Comparator.naturalOrder());
386 
387         // Propagate to final measurement's date
388         propagator.propagate(measurements.get(measurements.size()-1).getDate());
389 
390         // Convert lists to double[] and evaluate some statistics
391         final double[] relErrorsP = errorsP.stream().mapToDouble(Double::doubleValue).toArray();
392         final double[] relErrorsV = errorsV.stream().mapToDouble(Double::doubleValue).toArray();
393 
394         final double errorsPMedian = new Median().evaluate(relErrorsP);
395         final double errorsPMean   = new Mean().evaluate(relErrorsP);
396         final double errorsPMax    = new Max().evaluate(relErrorsP);
397         final double errorsVMedian = new Median().evaluate(relErrorsV);
398         final double errorsVMean   = new Mean().evaluate(relErrorsV);
399         final double errorsVMax    = new Max().evaluate(relErrorsV);
400 
401         // Print the results on console ?
402         if (printResults) {
403             System.out.println();
404             System.out.format(Locale.US, "Relative errors dR/dP -> Median: %6.3e / Mean: %6.3e / Max: %6.3e%n",
405                               errorsPMedian, errorsPMean, errorsPMax);
406             System.out.format(Locale.US, "Relative errors dR/dV -> Median: %6.3e / Mean: %6.3e / Max: %6.3e%n",
407                               errorsVMedian, errorsVMean, errorsVMax);
408         }
409 
410         Assertions.assertEquals(0.0, errorsPMedian, refErrorsPMedian);
411         Assertions.assertEquals(0.0, errorsPMean, refErrorsPMean);
412         Assertions.assertEquals(0.0, errorsPMax, refErrorsPMax);
413         Assertions.assertEquals(0.0, errorsVMedian, refErrorsVMedian);
414         Assertions.assertEquals(0.0, errorsVMean, refErrorsVMean);
415         Assertions.assertEquals(0.0, errorsVMax, refErrorsVMax);
416     }
417 
418     void genericTestParameterDerivatives(final boolean printResults,
419                                          final double refErrorsMedian, final double refErrorsMean, final double refErrorsMax) {
420 
421         Context context = EstimationTestUtils.eccentricContext("regular-data:potential:tides");
422 
423         final NumericalPropagatorBuilder propagatorBuilder =
424                         context.createBuilder(OrbitType.KEPLERIAN, PositionAngleType.TRUE, true,
425                                               1.0e-6, 60.0, 0.001);
426 
427         // Create perfect one-way GNSS range ratemeasurements
428         final TimeStampedPVCoordinates original = context.initialOrbit.getPVCoordinates();
429         final Orbit closeOrbit = new CartesianOrbit(new TimeStampedPVCoordinates(context.initialOrbit.getDate(),
430                                                                                  original.getPosition().add(new Vector3D(1000, 2000, 3000)),
431                                                                                  original.getVelocity().add(new Vector3D(-0.03, 0.01, 0.02))),
432                                                     context.initialOrbit.getFrame(),
433                                                     context.initialOrbit.getMu());
434         final Propagator closePropagator = EstimationTestUtils.createPropagator(closeOrbit, propagatorBuilder);
435         final EphemerisGenerator generator = closePropagator.getEphemerisGenerator();
436         closePropagator.propagate(context.initialOrbit.getDate().shiftedBy(3.5 * closeOrbit.getKeplerianPeriod()));
437         final BoundedPropagator ephemeris = generator.getGeneratedEphemeris();
438 
439         // Create perfect range ratemeasurements
440         final double localClockOffset        =   0.14e-06;
441         final double localClockRate          =  -0.12e-10;
442         final double localClockAcceleration  =   1.4e-13;
443         final double remoteClockOffset       = 469.0e-06;
444         final double remoteClockRate         =  33.0e-10;
445         final double remoteClockAcceleration =   0.5e-13;
446         final OneWayGNSSRangeRateCreator creator = new OneWayGNSSRangeRateCreator(ephemeris,
447                                                                                   localClockOffset, localClockRate, localClockAcceleration,
448                                                                                   remoteClockOffset, remoteClockRate, remoteClockAcceleration);
449         creator.getLocalSatellite().getClockOffsetDriver().setSelected(true);
450 
451         final Propagator propagator = EstimationTestUtils.createPropagator(context.initialOrbit,
452                                                                            propagatorBuilder);
453         final List<ObservedMeasurement<?>> measurements =
454                         EstimationTestUtils.createMeasurements(propagator, creator, 1.0, 3.0, 300.0);
455 
456         // List to store the results
457         final List<Double> relErrorList = new ArrayList<>();
458 
459         // Use a lambda function to implement "handleStep" function
460         propagator.setStepHandler(interpolator -> {
461 
462             for (final ObservedMeasurement<?> measurement : measurements) {
463 
464                 //  Play test if the measurement date is between interpolator previous and current date
465                 if ((measurement.getDate().durationFrom(interpolator.getPreviousState().getDate()) > 0.) &&
466                     (measurement.getDate().durationFrom(interpolator.getCurrentState().getDate())  <=  0.)) {
467 
468                     // We intentionally propagate to a date which is close to the
469                     // real spacecraft state but is *not* the accurate date, by
470                     // compensating only part of the downlink delay. This is done
471                     // in order to validate the partial derivatives with respect
472                     // to velocity. If we had chosen the proper state date, the
473                     // range rate would have depended only on the current position but
474                     // not on the current velocity.
475                     final double          meanDelay = measurement.getObservedValue()[0] / Constants.SPEED_OF_LIGHT;
476                     final AbsoluteDate    date      = measurement.getDate().shiftedBy(-0.75 * meanDelay);
477                     final SpacecraftState[] states    = {
478                         interpolator.getInterpolatedState(date),
479                         ephemeris.propagate(date)
480                     };
481                     final ParameterDriver[] drivers = new ParameterDriver[] {
482                         measurement.getSatellites().get(0).getClockOffsetDriver(),
483                     };
484 
485                     for (int i = 0; i < drivers.length; ++i) {
486                         for (Span<String> span = drivers[i].getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
487 
488                             final double[] gradient  = measurement.estimate(0, 0, states).getParameterDerivatives(drivers[i], span.getStart());
489                             Assertions.assertEquals(1, measurement.getDimension());
490                             Assertions.assertEquals(1, gradient.length);
491                             
492                             // Compute a reference value using finite differences
493                             final ParameterFunction dMkdP =
494                                             Differentiation.differentiate(new ParameterFunction() {
495                                                 /** {@inheritDoc} */
496                                                 @Override
497                                                 public double value(final ParameterDriver parameterDriver, final AbsoluteDate date) {
498                                                     return measurement.
499                                                            estimateWithoutDerivatives(states).
500                                                            getEstimatedValue()[0];
501                                                 }
502                                             }, 5, 10.0 * drivers[i].getScale());
503                             final double ref = dMkdP.value(drivers[i], date);
504                             
505                             if (printResults) {
506                                 System.out.format(Locale.US, "%10.3e  %10.3e  ", gradient[0]-ref, FastMath.abs((gradient[0]-ref)/ref));
507                             }
508                             
509                             final double relError = FastMath.abs((ref-gradient[0])/ref);
510                             relErrorList.add(relError);
511                         }
512                     }
513                     if (printResults) {
514                         System.out.format(Locale.US, "%n");
515                     }
516 
517                 } // End if measurement date between previous and current interpolator step
518             } // End for loop on the measurements
519         });
520 
521         // Rewind the propagator to initial date
522         propagator.propagate(context.initialOrbit.getDate());
523 
524         // Sort measurements chronologically
525         measurements.sort(Comparator.naturalOrder());
526 
527         // Print results ? Header
528         if (printResults) {
529             System.out.format(Locale.US, "%-15s  %-23s  %-23s  " +
530                               "%10s  %10s  %10s  %10s  %10s  %10s  %10s  %10s  %10s  %10s%n",
531                               "Station", "Measurement Date", "State Date",
532                               "Δt",    "rel Δt",
533                               "ΔdQx",  "rel ΔdQx",
534                               "ΔdQy",  "rel ΔdQy",
535                               "ΔdQz",  "rel ΔdQz",
536                               "Δtsat", "rel Δtsat");
537          }
538 
539         // Propagate to final measurement's date
540         propagator.propagate(measurements.get(measurements.size()-1).getDate());
541 
542         // Convert error list to double[]
543         final double[] relErrors = relErrorList.stream().mapToDouble(Double::doubleValue).toArray();
544 
545         // Compute statistics
546         final double relErrorsMedian = new Median().evaluate(relErrors);
547         final double relErrorsMean   = new Mean().evaluate(relErrors);
548         final double relErrorsMax    = new Max().evaluate(relErrors);
549 
550         // Print the results on console ?
551         if (printResults) {
552             System.out.println();
553             System.out.format(Locale.US, "Relative errors dR/dQ -> Median: %6.3e / Mean: %6.3e / Max: %6.3e%n",
554                               relErrorsMedian, relErrorsMean, relErrorsMax);
555         }
556 
557         Assertions.assertEquals(0.0, relErrorsMedian, refErrorsMedian);
558         Assertions.assertEquals(0.0, relErrorsMean, refErrorsMean);
559         Assertions.assertEquals(0.0, relErrorsMax, refErrorsMax);
560 
561     }
562 
563 }