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