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