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