1   /* Copyright 2002-2026 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.propagation.analytical.gnss;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  
23  import org.hipparchus.analysis.differentiation.Gradient;
24  import org.hipparchus.analysis.differentiation.GradientField;
25  import org.hipparchus.analysis.differentiation.UnivariateDerivative2;
26  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
27  import org.hipparchus.geometry.euclidean.threed.Vector3D;
28  import org.hipparchus.linear.MatrixUtils;
29  import org.hipparchus.linear.QRDecomposition;
30  import org.hipparchus.linear.RealMatrix;
31  import org.hipparchus.linear.RealVector;
32  import org.hipparchus.util.FastMath;
33  import org.hipparchus.util.FieldSinCos;
34  import org.hipparchus.util.SinCos;
35  import org.orekit.attitudes.Attitude;
36  import org.orekit.attitudes.AttitudeProvider;
37  import org.orekit.attitudes.FrameAlignedProvider;
38  import org.orekit.frames.Frame;
39  import org.orekit.orbits.FieldKeplerianAnomalyUtility;
40  import org.orekit.orbits.FieldKeplerianOrbit;
41  import org.orekit.orbits.FieldKeplerianParameters;
42  import org.orekit.orbits.KeplerianAnomalyUtility;
43  import org.orekit.orbits.KeplerianOrbit;
44  import org.orekit.orbits.Orbit;
45  import org.orekit.orbits.PositionAngleType;
46  import org.orekit.propagation.AbstractMatricesHarvester;
47  import org.orekit.propagation.Propagator;
48  import org.orekit.propagation.SpacecraftState;
49  import org.orekit.propagation.analytical.AbstractAnalyticalPropagator;
50  import org.orekit.propagation.analytical.gnss.data.FieldGnssOrbitalElements;
51  import org.orekit.propagation.analytical.gnss.data.GNSSOrbitalElements;
52  import org.orekit.propagation.analytical.gnss.data.GNSSOrbitalElementsFactory;
53  import org.orekit.propagation.analytical.gnss.data.NonKeplerianDriversFactory;
54  import org.orekit.time.AbsoluteDate;
55  import org.orekit.time.FieldAbsoluteDate;
56  import org.orekit.time.GNSSDate;
57  import org.orekit.utils.DoubleArrayDictionary;
58  import org.orekit.utils.FieldPVCoordinates;
59  import org.orekit.utils.PVCoordinates;
60  import org.orekit.utils.ParameterDriver;
61  import org.orekit.utils.ParameterDriversProvider;
62  import org.orekit.utils.TimeSpanMap.Span;
63  
64  /** Common handling of {@link AbstractAnalyticalPropagator} methods for GNSS propagators.
65   * <p>
66   * This class allows to provide easily a subset of {@link AbstractAnalyticalPropagator} methods
67   * for specific GNSS propagators.
68   * </p>
69   * @param <O> type of the orbital elements
70   * @author Pascal Parraud
71   */
72  public class GNSSPropagator<O extends GNSSOrbitalElements<O>>
73      extends AbstractAnalyticalPropagator implements ParameterDriversProvider {
74  
75      /** Maximum number of iterations for internal loops.
76       * @since 13.0
77       */
78      private static final int MAX_ITER = 100;
79  
80      /** Tolerance on position for rebuilding orbital elements from initial state.
81       * @since 13.0
82       */
83      private static final double TOL_P = 1.0e-6;
84  
85      /** Tolerance on velocity for rebuilding orbital elements from initial state.
86       * @since 13.0
87       */
88      private static final double TOL_V = 1.0e-9;
89  
90      /** Number of free parameters for orbital elements.
91       * @since 13.0
92       */
93      private static final int FREE_PARAMETERS = 6;
94  
95      /** Convergence parameter.
96       * @since 13.0
97       */
98      private static final double EPS = 1.0e-12;
99  
100     /** The GNSS propagation model used. */
101     private O orbitalElements;
102 
103     /** Factory for non-Keplerian elements drivers.
104      * @since 14.0
105      */
106     private final NonKeplerianDriversFactory driversFactory;
107 
108     /** The ECI frame used for GNSS propagation. */
109     private final Frame eci;
110 
111     /** The ECEF frame used for GNSS propagation. */
112     private final Frame ecef;
113 
114     /** Build a new instance.
115      * <p>
116      * Beware that since GNSS orbital elements refer to an Earth frame frozen at a specific date
117      * to become an inertial frame, the factory date <em>must</em> have been initialized properly
118      * (by calling {@link GNSSOrbitalElementsFactory#setTimeOfEphemeris(GNSSDate)}). Setting the date
119      * allows the frozen frame to be set properly, otherwise a null pointer exception will be thrown
120      * when the propagator builder attempts to use the frame.
121      * </p>
122      * <p>
123      * The attitude provider is set by default to be aligned with the provided inertial frame.
124      * This can be changed (typically to {@link org.orekit.gnss.attitude.GenericGNSS}) after
125      * construction by calling {@link #setAttitudeProvider(org.orekit.attitudes.AttitudeProvider)
126      * setAttitudeProvider}
127      * </p>
128      * <p>
129      * The mass is set to the {@link org.orekit.propagation.Propagator#DEFAULT_MASS DEFAULT_MASS}.
130      * </p>
131      * @param factory factory for the elements and frames
132      * @since 14.0
133      */
134 
135     public GNSSPropagator(final GNSSOrbitalElementsFactory<O> factory) {
136         this(factory.createFromDrivers(), factory.getInertial(), factory.getBodyFixed(),
137              FrameAlignedProvider.of(factory.getInertial()),
138              Propagator.DEFAULT_MASS);
139     }
140 
141     /** Build a new instance.
142      * @param orbitalElements GNSS orbital elements
143      * @param eci Earth Centered Inertial frame
144      * @param ecef Earth Centered Earth Fixed frame
145      * @param provider attitude provider
146      * @param mass satellite mass (kg)
147      */
148     public GNSSPropagator(final O orbitalElements, final Frame eci, final Frame ecef,
149                           final AttitudeProvider provider, final double mass) {
150         super(provider);
151         // Stores the GNSS orbital elements
152         this.orbitalElements = orbitalElements;
153         this.driversFactory  = new NonKeplerianDriversFactory();
154         driversFactory.reset(orbitalElements);
155         // Sets the Earth Centered Inertial frame
156         this.eci  = eci;
157         // Sets the Earth Centered Earth Fixed frame
158         this.ecef = ecef;
159 
160         // Sets initial state
161         final Orbit orbit = propagateOrbit(orbitalElements.getDate());
162         final Attitude attitude = provider.getAttitude(orbit, orbit.getDate(), orbit.getFrame());
163 
164         // calling the method from constructor because the one overridden below recomputes the orbital elements
165         super.resetInitialState(new SpacecraftState(orbit, attitude).withMass(mass));
166 
167     }
168 
169     /**
170      * Build a new instance from an initial state.
171      * <p>
172      * The Keplerian elements already present in the {@code nonKeplerianElements} argument
173      * will be ignored as it is the {@code initialState} argument that will be used to
174      * build the complete orbital elements of the propagator
175      * </p>
176      * @param initialState         initial state
177      * @param nonKeplerianElements non-Keplerian orbital elements (the Keplerian orbital elements will be ignored)
178      * @param ecef                 Earth Centered Earth Fixed frame
179      * @param provider             attitude provider
180      * @param mass                 spacecraft mass
181      * @since 13.0
182      */
183     public GNSSPropagator(final SpacecraftState initialState, final O nonKeplerianElements,
184                           final Frame ecef, final AttitudeProvider provider, final double mass) {
185         this(buildOrbitalElements(initialState, nonKeplerianElements, new NonKeplerianDriversFactory(),
186                                   ecef, provider, mass),
187              initialState.getFrame(), ecef, provider, initialState.getMass());
188     }
189 
190     /** {@inheritDoc} */
191     @Override
192     public List<ParameterDriver> getParametersDrivers() {
193         return driversFactory.getParametersDrivers();
194     }
195 
196     /** Get the factory for non-Keplerian elements drivers.
197      * @return factory for non-Keplerian elements drivers
198      */
199     public NonKeplerianDriversFactory getDriversFactory() {
200         return driversFactory;
201     }
202 
203     /**
204      * Gets the Earth Centered Inertial frame used to propagate the orbit.
205      *
206      * @return the ECI frame
207      */
208     public Frame getECI() {
209         return eci;
210     }
211 
212     /**
213      * Gets the Earth Centered Earth Fixed frame used to propagate GNSS orbits according to the
214      * Interface Control Document.
215      *
216      * @return the ECEF frame
217      */
218     public Frame getECEF() {
219         return ecef;
220     }
221 
222     /**
223      * Gets the Earth gravity coefficient used for GNSS propagation.
224      *
225      * @return the Earth gravity coefficient.
226      */
227     public double getMU() {
228         return orbitalElements.getOrbit().getMu();
229     }
230 
231     /** Get the underlying GNSS propagation orbital elements.
232      * @return the underlying GNSS orbital elements
233      * @since 13.0
234      */
235     public O getOrbitalElements() {
236         return orbitalElements;
237     }
238 
239     /** {@inheritDoc}
240      * @since 13.0
241      */
242     @Override
243     protected AbstractMatricesHarvester createHarvester(final String stmName, final RealMatrix initialStm,
244                                                         final DoubleArrayDictionary initialJacobianColumns) {
245 
246         // Create the harvester
247         final GnssHarvester<O> harvester = new GnssHarvester<>(this, stmName, initialStm, initialJacobianColumns);
248 
249         // Update the list of additional state provider
250         addAdditionalDataProvider(harvester);
251 
252         // Return the configured harvester
253         return harvester;
254 
255     }
256 
257     /** {@inheritDoc}
258      * @since 13.0
259      */
260     @Override
261     protected List<String> getJacobiansColumnsNames() {
262         final List<String> columnsNames = new ArrayList<>();
263         for (final ParameterDriver driver : getParametersDrivers()) {
264             if (driver.isSelected() && !columnsNames.contains(driver.getNamesSpanMap().getFirstSpan().getData())) {
265                 // As driver with same name should have same NamesSpanMap we only check if the first span is present,
266                 // if not we add all span names to columnsNames
267                 for (Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
268                     columnsNames.add(span.getData());
269                 }
270             }
271         }
272         Collections.sort(columnsNames);
273         return columnsNames;
274     }
275 
276     /** {@inheritDoc} */
277     @Override
278     public Orbit propagateOrbit(final AbsoluteDate date) {
279         // Get the PVCoordinates in ECEF frame
280         final PVCoordinates pvaInECEF = propagateInEcef(date);
281         // Transform the PVCoordinates to ECI frame
282         final PVCoordinates pvaInECI = ecef.getTransformTo(eci, date).transformPVCoordinates(pvaInECEF);
283         // Return the Keplerian orbit
284         return new KeplerianOrbit(pvaInECI, eci, date, getMU());
285     }
286 
287     /**
288      * Gets the PVCoordinates of the GNSS SV in {@link #getECEF() ECEF frame}.
289      *
290      * <p>The algorithm uses automatic differentiation to compute velocity and
291      * acceleration.</p>
292      *
293      * @param date the computation date
294      * @return the GNSS SV PVCoordinates in {@link #getECEF() ECEF frame}
295      */
296     public PVCoordinates propagateInEcef(final AbsoluteDate date) {
297 
298         final KeplerianOrbit orbit = orbitalElements.getOrbit();
299 
300         // Duration from GNSS ephemeris Reference date
301         final UnivariateDerivative2 tk = new UnivariateDerivative2(getTk(date), 1.0, 0.0);
302         // Semi-major axis
303         final UnivariateDerivative2 ak = tk.multiply(orbitalElements.getADot()).add(orbit.getA());
304         // Mean motion
305         final UnivariateDerivative2 nA = tk.multiply(orbitalElements.getDeltaN0Dot() * 0.5).
306                                          add(orbitalElements.getDeltaN0()).
307                                          add(orbit.getKeplerianMeanMotion());
308         // Mean anomaly
309         final UnivariateDerivative2 mk = tk.multiply(nA).add(orbit.getMeanAnomaly());
310         // Eccentric Anomaly
311         final UnivariateDerivative2 e  = tk.newInstance(orbit.getE());
312         final UnivariateDerivative2 ek = FieldKeplerianAnomalyUtility.ellipticMeanToEccentric(e, mk);
313         // True Anomaly
314         final UnivariateDerivative2 vk =  FieldKeplerianAnomalyUtility.ellipticEccentricToTrue(e, ek);
315         // Argument of Latitude
316         final UnivariateDerivative2 phik    = vk.add(orbit.getPeriapsisArgument());
317         final FieldSinCos<UnivariateDerivative2> cs2phi = FastMath.sinCos(phik.multiply(2));
318         // Argument of Latitude Correction
319         final UnivariateDerivative2 dphik = cs2phi.cos().multiply(orbitalElements.getCuc()).add(cs2phi.sin().multiply(orbitalElements.getCus()));
320         // Radius Correction
321         final UnivariateDerivative2 drk = cs2phi.cos().multiply(orbitalElements.getCrc()).add(cs2phi.sin().multiply(orbitalElements.getCrs()));
322         // Inclination Correction
323         final UnivariateDerivative2 dik = cs2phi.cos().multiply(orbitalElements.getCic()).add(cs2phi.sin().multiply(orbitalElements.getCis()));
324         // Corrected Argument of Latitude
325         final FieldSinCos<UnivariateDerivative2> csuk = FastMath.sinCos(phik.add(dphik));
326         // Corrected Radius
327         final UnivariateDerivative2 rk = ek.cos().multiply(e.negate()).add(1).multiply(ak).add(drk);
328         // Corrected Inclination
329         final UnivariateDerivative2 ik  = tk.multiply(orbitalElements.getIDot()).add(orbit.getI()).add(dik);
330         final FieldSinCos<UnivariateDerivative2> csik = FastMath.sinCos(ik);
331         // Positions in orbital plane
332         final UnivariateDerivative2 xk = csuk.cos().multiply(rk);
333         final UnivariateDerivative2 yk = csuk.sin().multiply(rk);
334         // Corrected longitude of ascending node
335         final double thetaDot = orbitalElements.getAngularVelocity();
336         final double toe = orbitalElements.getTimeOfEphemeris().getSecondsInWeek();
337         final FieldSinCos<UnivariateDerivative2> csomk =
338             FastMath.sinCos(tk.multiply(orbitalElements.getOmegaDot() - thetaDot).
339                             add(orbit.getRightAscensionOfAscendingNode() - thetaDot * toe));
340         // returns the Earth-fixed coordinates
341         final FieldVector3D<UnivariateDerivative2> positionWithDerivatives =
342                         new FieldVector3D<>(xk.multiply(csomk.cos()).subtract(yk.multiply(csomk.sin()).multiply(csik.cos())),
343                                             xk.multiply(csomk.sin()).add(yk.multiply(csomk.cos()).multiply(csik.cos())),
344                                             yk.multiply(csik.sin()));
345         return new PVCoordinates(positionWithDerivatives);
346     }
347 
348     /**
349      * Gets the duration from GNSS Reference epoch.
350      * <p>This takes the GNSS week roll-over into account.</p>
351      * @param date the considered date
352      * @return the duration from GNSS orbit Reference epoch (s)
353      */
354     private double getTk(final AbsoluteDate date) {
355         final double cycleDuration = orbitalElements.getCycleDuration();
356         // Time from ephemeris reference epoch
357         double tk = date.durationFrom(orbitalElements.getDate());
358         // Adjusts the time to take roll over week into account
359         while (tk > 0.5 * cycleDuration) {
360             tk -= cycleDuration;
361         }
362         while (tk < -0.5 * cycleDuration) {
363             tk += cycleDuration;
364         }
365         // Returns the time from ephemeris reference epoch
366         return tk;
367     }
368 
369     /** {@inheritDoc} */
370     @Override
371     public Frame getFrame() {
372         return eci;
373     }
374 
375     /** {@inheritDoc} */
376     @Override
377     protected double getMass(final AbsoluteDate date) {
378         return getInitialState().getMass();
379     }
380 
381     /** {@inheritDoc} */
382     @Override
383     public void resetInitialState(final SpacecraftState state) {
384         orbitalElements = buildOrbitalElements(state, orbitalElements, driversFactory,
385                                                ecef, getAttitudeProvider(), state.getMass());
386         final Orbit orbit = propagateOrbit(orbitalElements.getDate());
387         final Attitude attitude = getAttitudeProvider().getAttitude(orbit, orbit.getDate(), orbit.getFrame());
388         super.resetInitialState(new SpacecraftState(orbit, attitude).withMass(state.getMass()));
389     }
390 
391     /** {@inheritDoc} */
392     @Override
393     protected void resetIntermediateState(final SpacecraftState state, final boolean forward) {
394         resetInitialState(state);
395     }
396 
397     /**
398      * Build orbital elements from initial state.
399      * <p>
400      * This method is roughly the inverse of {@link #propagateInEcef(AbsoluteDate)}, except it starts from a state in
401      * inertial frame
402      * </p>
403      *
404      * @param <O> type of the orbital elements (non-field version)
405      * @param initialState         initial state
406      * @param nonKeplerianElements non-Keplerian orbital elements (the Keplerian orbital elements will be overridden)
407      * @param driversFactory       factory for non-Keplerian drivers
408      * @param ecef                 Earth Centered Earth Fixed frame
409      * @param provider             attitude provider
410      * @param mass                 satellite mass (kg)
411      * @return orbital elements that generate the {@code initialState} when used with a propagator
412      * @since 13.0
413      */
414     public static <O extends GNSSOrbitalElements<O>>
415         O buildOrbitalElements(final SpacecraftState initialState,
416                                final O nonKeplerianElements,
417                                final NonKeplerianDriversFactory driversFactory,
418                                final Frame ecef, final AttitudeProvider provider,
419                                final double mass) {
420 
421         // get approximate initial orbit
422         final Frame frozenEcef = ecef.getFrozenFrame(initialState.getFrame(), initialState.getDate(),
423                                                      GNSSOrbitalElementsFactory.FROZEN + ecef.getName());
424         final KeplerianOrbit orbit = approximateInitialOrbit(initialState, nonKeplerianElements, frozenEcef);
425         driversFactory.reset(nonKeplerianElements);
426 
427         // refine orbit using simple differential correction to reach target PV
428         final PVCoordinates targetPV = initialState.getPVCoordinates(frozenEcef);
429         FieldGnssOrbitalElements<Gradient, O> gElements = toGradient(nonKeplerianElements, orbit, driversFactory);
430         for (int i = 0; i < MAX_ITER; ++i) {
431 
432             // get position-velocity derivatives with respect to initial orbit
433             final FieldGnssPropagator<Gradient, O> gPropagator =
434                 new FieldGnssPropagator<>(gElements, frozenEcef, ecef, provider,
435                                           gElements.getTgd().newInstance(mass));
436             final FieldPVCoordinates<Gradient> gPV = gPropagator.getInitialState().getPVCoordinates();
437 
438             // compute Jacobian matrix
439             final RealMatrix jacobian = MatrixUtils.createRealMatrix(FREE_PARAMETERS, FREE_PARAMETERS);
440             jacobian.setRow(0, gPV.getPosition().getX().getGradient());
441             jacobian.setRow(1, gPV.getPosition().getY().getGradient());
442             jacobian.setRow(2, gPV.getPosition().getZ().getGradient());
443             jacobian.setRow(3, gPV.getVelocity().getX().getGradient());
444             jacobian.setRow(4, gPV.getVelocity().getY().getGradient());
445             jacobian.setRow(5, gPV.getVelocity().getZ().getGradient());
446 
447             // compute residuals
448             final RealVector residuals = MatrixUtils.createRealVector(FREE_PARAMETERS);
449             residuals.setEntry(0, targetPV.getPosition().getX() - gPV.getPosition().getX().getValue());
450             residuals.setEntry(1, targetPV.getPosition().getY() - gPV.getPosition().getY().getValue());
451             residuals.setEntry(2, targetPV.getPosition().getZ() - gPV.getPosition().getZ().getValue());
452             residuals.setEntry(3, targetPV.getVelocity().getX() - gPV.getVelocity().getX().getValue());
453             residuals.setEntry(4, targetPV.getVelocity().getY() - gPV.getVelocity().getY().getValue());
454             residuals.setEntry(5, targetPV.getVelocity().getZ() - gPV.getVelocity().getZ().getValue());
455 
456             // check convergence
457             final double deltaP = FastMath.sqrt(residuals.getEntry(0) * residuals.getEntry(0) +
458                                                 residuals.getEntry(1) * residuals.getEntry(1) +
459                                                 residuals.getEntry(2) * residuals.getEntry(2));
460             final double deltaV = FastMath.sqrt(residuals.getEntry(3) * residuals.getEntry(3) +
461                                                 residuals.getEntry(4) * residuals.getEntry(4) +
462                                                 residuals.getEntry(5) * residuals.getEntry(5));
463             if (deltaP < TOL_P && deltaV < TOL_V) {
464                 break;
465             }
466 
467             // linear correction to get closer to target PV
468             final RealVector correction = new QRDecomposition(jacobian, EPS).getSolver().solve(residuals);
469 
470             // prevent correction to produce invalid values
471             final FieldKeplerianOrbit<Gradient> previous = gElements.getOrbit();
472             Gradient updatedA;
473             Gradient updatedE;
474             double factor = 2;
475             do {
476                 // loop until eccentricity is valid
477                 factor *= 0.5;
478                 updatedA = previous.getA().add(correction.getEntry(0) * factor);
479                 updatedE = previous.getE().add(correction.getEntry(1) * factor);
480             } while (updatedA.getValue() < 0 || updatedE.getValue() < 0 || updatedE.getValue() >= 1);
481 
482             // update initial orbit
483             final FieldKeplerianOrbit<Gradient> updated =
484                 new FieldKeplerianOrbit<>(new FieldKeplerianParameters<>(updatedA,
485                                                                          updatedE,
486                                                                          previous.getI().add(correction.getEntry(2) * factor),
487                                                                          previous.getPeriapsisArgument().add(correction.getEntry(3) * factor),
488                                                                          previous.getRightAscensionOfAscendingNode().add(correction.getEntry(4) * factor),
489                                                                          previous.getMeanAnomaly().add(correction.getEntry(5) * factor),
490                                                                          PositionAngleType.MEAN),
491                                           previous.getFrame(), previous.getDate(), previous.getMu());
492             gElements = toGradient(nonKeplerianElements, updated.toOrbit(), driversFactory);
493 
494         }
495 
496         return gElements.toNonField();
497 
498     }
499 
500     /** Compute approximate initial orbit.
501      * @param initialState         initial state
502      * @param nonKeplerianElements non-Keplerian orbital elements (the Keplerian orbital elements will be ignored)
503      * @param frozenEcef           inertial frame aligned with Earth Centered Earth Fixed frame at orbit date
504      * @return approximate initial orbit that generate a state close to {@code initialState}
505      * @since 13.0
506      */
507     private static KeplerianOrbit approximateInitialOrbit(final SpacecraftState initialState,
508                                                           final GNSSOrbitalElements<?> nonKeplerianElements,
509                                                           final Frame frozenEcef) {
510 
511         // rotate the state to a frame that is inertial but aligned with Earth frame,
512         // as analytical model is expressed in Earth frame
513         final PVCoordinates pv = initialState.getPVCoordinates(frozenEcef);
514         final Vector3D      p  = pv.getPosition();
515         final Vector3D      v  = pv.getVelocity();
516 
517         // compute Keplerian orbital parameters
518         final double   rk  = p.getNorm();
519 
520         // compute orbital plane orientation
521         final Vector3D normal = pv.getMomentum().normalize();
522         final double   cosIk  = normal.getZ();
523         final double   ik     = Vector3D.angle(normal, Vector3D.PLUS_K);
524 
525         // compute position in orbital plane
526         final double   q   = FastMath.hypot(normal.getX(), normal.getY());
527         final double   cos = -normal.getY() / q;
528         final double   sin =  normal.getX() / q;
529         final double   xk  =  p.getX() * cos + p.getY() * sin;
530         final double   yk  = (p.getY() * cos - p.getX() * sin) / cosIk;
531 
532         // corrected latitude argument
533         final double   uk  = FastMath.atan2(yk, xk);
534 
535         // recover latitude argument before correction, using a fixed-point method
536         double phi = uk;
537         for (int i = 0; i < MAX_ITER; ++i) {
538             final double previous = phi;
539             final SinCos cs2Phi = FastMath.sinCos(2 * phi);
540             phi = uk - (cs2Phi.cos() * nonKeplerianElements.getCuc() + cs2Phi.sin() * nonKeplerianElements.getCus());
541             if (FastMath.abs(phi - previous) <= EPS) {
542                 break;
543             }
544         }
545         final SinCos cs2phi = FastMath.sinCos(2 * phi);
546 
547         // recover plane orientation before correction
548         // here, we know that tk = 0 since our orbital elements will be at initial state date
549         final double i0  = ik - (cs2phi.cos() * nonKeplerianElements.getCic() + cs2phi.sin() * nonKeplerianElements.getCis());
550         final double om0 = FastMath.atan2(sin, cos) +
551                            nonKeplerianElements.getAngularVelocity() *
552                            nonKeplerianElements.getTimeOfEphemeris().getSecondsInWeek();
553 
554         // recover eccentricity and anomaly
555         final double mu = initialState.getOrbit().getMu();
556         final double rV2OMu           = rk * v.getNorm2Sq() / mu;
557         final double sma              = rk / (2 - rV2OMu);
558         final double eCosE            = rV2OMu - 1;
559         final double eSinE            = Vector3D.dotProduct(p, v) / FastMath.sqrt(mu * sma);
560         final double e                = FastMath.hypot(eCosE, eSinE);
561         final double eccentricAnomaly = FastMath.atan2(eSinE, eCosE);
562         final double aop              = phi - eccentricAnomaly;
563         final double meanAnomaly      = KeplerianAnomalyUtility.ellipticEccentricToMean(e, eccentricAnomaly);
564 
565         return new KeplerianOrbit(sma, e, i0, aop, om0, meanAnomaly, PositionAngleType.MEAN, frozenEcef,
566                                   initialState.getDate(), mu);
567 
568     }
569 
570     /** Convert orbital elements to gradient.
571      * @param <O> type of the orbital elements (non-field version)
572      * @param elements       primitive double elements
573      * @param orbit          Keplerian orbit
574      * @param driversFactory factory for non-Kepleria drivers
575      * @return converted elements, set up as gradient relative to Keplerian orbit
576      * @since 14.0
577      */
578     private static <O extends GNSSOrbitalElements<O>>
579         FieldGnssOrbitalElements<Gradient, O> toGradient(final O elements,
580                                                          final KeplerianOrbit orbit,
581                                                          final NonKeplerianDriversFactory driversFactory) {
582 
583         // build orbit with gradient
584         final Gradient aG    = Gradient.variable(FREE_PARAMETERS, 0, orbit.getA());
585         final Gradient eG    = Gradient.variable(FREE_PARAMETERS, 1, orbit.getE());
586         final Gradient iG    = Gradient.variable(FREE_PARAMETERS, 2, orbit.getI());
587         final Gradient paG   = Gradient.variable(FREE_PARAMETERS, 3, orbit.getPeriapsisArgument());
588         final Gradient raanG = Gradient.variable(FREE_PARAMETERS, 4, orbit.getRightAscensionOfAscendingNode());
589         final Gradient mG    = Gradient.variable(FREE_PARAMETERS, 5, orbit.getMeanAnomaly());
590         final FieldKeplerianOrbit<Gradient> orbitG =
591             new FieldKeplerianOrbit<>(new FieldKeplerianParameters<>(aG, eG, iG, paG, raanG, mG,
592                                                                      PositionAngleType.MEAN),
593                                       orbit.getFrame(),
594                                       new FieldAbsoluteDate<>(GradientField.getField(FREE_PARAMETERS),
595                                                               orbit.getDate()),
596                                       Gradient.constant(FREE_PARAMETERS, orbit.getMu()));
597 
598         // convert to GNSS orbital elements
599         return elements.toField(orbitG,
600                                 driversFactory.toGradients(FREE_PARAMETERS),
601                                 d -> Gradient.constant(FREE_PARAMETERS, d));
602 
603     }
604 
605 }