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