1   /* Copyright 2022-2025 Romain Serra
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.orbits;
18  
19  import org.hipparchus.CalculusFieldElement;
20  import org.hipparchus.geometry.euclidean.threed.FieldRotation;
21  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
22  import org.hipparchus.geometry.euclidean.threed.Rotation;
23  import org.hipparchus.geometry.euclidean.threed.RotationConvention;
24  import org.hipparchus.geometry.euclidean.threed.Vector3D;
25  import org.hipparchus.util.FastMath;
26  import org.hipparchus.util.FieldSinCos;
27  import org.hipparchus.util.SinCos;
28  import org.orekit.utils.FieldPVCoordinates;
29  import org.orekit.utils.PVCoordinates;
30  
31  /**
32   * Utility class to predict position and velocity under Keplerian motion, using lightweight routines based on Cartesian
33   * coordinates. Computations do not require a reference frame or an epoch.
34   *
35   * @author Andrew Goetz
36   * @author Romain Serra
37   * @see org.orekit.propagation.analytical.KeplerianPropagator
38   * @see org.orekit.propagation.analytical.FieldKeplerianPropagator
39   * @see CartesianOrbit
40   * @see FieldCartesianOrbit
41   * @since 12.1
42   */
43  public class KeplerianMotionCartesianUtility {
44  
45      private KeplerianMotionCartesianUtility() {
46          // utility class
47      }
48  
49      /**
50       * Method to propagate position and velocity according to Keplerian dynamics.
51       * For long time of flights, it is preferable to use {@link org.orekit.propagation.analytical.KeplerianPropagator}.
52       * @param dt time of flight
53       * @param position initial position vector
54       * @param velocity initial velocity vector
55       * @param mu central body gravitational parameter
56       * @return predicted position-velocity
57       */
58      public static PVCoordinates predictPositionVelocity(final double dt, final Vector3D position, final Vector3D velocity,
59                                                          final double mu) {
60          final double r = position.getNorm();
61          final double a = r / (2 - r * velocity.getNormSq() / mu);
62          if (a >= 0.) {
63              return predictPVElliptic(dt, position, velocity, mu, a, r);
64          } else {
65              return predictPVHyperbolic(dt, position, velocity, mu, a, r);
66          }
67      }
68  
69      /**
70       * Method to propagate position and velocity according to Keplerian dynamics, in the case of an elliptic trajectory.
71       * @param dt time of flight
72       * @param position initial position vector
73       * @param velocity initial velocity vector
74       * @param mu central body gravitational parameter
75       * @param a semi-major axis
76       * @param r initial radius
77       * @return predicted position-velocity
78       */
79      private static PVCoordinates predictPVElliptic(final double dt, final Vector3D position, final Vector3D velocity,
80                                                     final double mu, final double a, final double r) {
81          // preliminary computation
82          final Vector3D pvM     = position.crossProduct(velocity);
83          final double muA       = mu * a;
84  
85          // compute mean anomaly
86          final double eSE    = position.dotProduct(velocity) / FastMath.sqrt(muA);
87          final double eCE    = 1. - r / a;
88          final double E0     = FastMath.atan2(eSE, eCE);
89          final double M0     = E0 - eSE;
90  
91          final double e         = FastMath.sqrt(eCE * eCE + eSE * eSE);
92          final double sqrt      = FastMath.sqrt((1 + e) / (1 - e));
93  
94          // find canonical 2D frame with p pointing to perigee
95          final double v0     = 2 * FastMath.atan(sqrt * FastMath.tan(E0 / 2));
96          final Rotation rotation = new Rotation(pvM, v0, RotationConvention.FRAME_TRANSFORM);
97          final Vector3D p    = rotation.applyTo(position).normalize();
98          final Vector3D q    = pvM.crossProduct(p).normalize();
99  
100         // compute shifted eccentric anomaly
101         final double sqrtRatio = FastMath.sqrt(mu / a);
102         final double meanMotion = sqrtRatio / a;
103         final double M1     = M0 + meanMotion * dt;
104         final double E1     = KeplerianAnomalyUtility.ellipticMeanToEccentric(e, M1);
105 
106         // compute shifted in-plane Cartesian coordinates
107         final SinCos scE    = FastMath.sinCos(E1);
108         final double cE     = scE.cos();
109         final double sE     = scE.sin();
110         final double sE2m1  = FastMath.sqrt((1 - e) * (1 + e));
111 
112         // coordinates of position and velocity in the orbital plane
113         final double x      = a * (cE - e);
114         final double y      = a * sE2m1 * sE;
115         final double factor = sqrtRatio / (1 - e * cE);
116         final double xDot   = -factor * sE;
117         final double yDot   =  factor * sE2m1 * cE;
118 
119         final Vector3D predictedPosition = new Vector3D(x, p, y, q);
120         final Vector3D predictedVelocity = new Vector3D(xDot, p, yDot, q);
121         return new PVCoordinates(predictedPosition, predictedVelocity);
122     }
123 
124     /**
125      * Method to propagate position and velocity according to Keplerian dynamics, in the case of a hyperbolic trajectory.
126      * @param dt time of flight
127      * @param position initial position vector
128      * @param velocity initial velocity vector
129      * @param mu central body gravitational parameter
130      * @param a semi-major axis
131      * @param r initial radius
132      * @return predicted position-velocity
133      */
134     private static PVCoordinates predictPVHyperbolic(final double dt, final Vector3D position, final Vector3D velocity,
135                                                      final double mu, final double a, final double r) {
136         // preliminary computations
137         final Vector3D pvM   = position.crossProduct(velocity);
138         final double muA     = mu * a;
139         final double e       = FastMath.sqrt(1 - pvM.getNormSq() / muA);
140         final double sqrt    = FastMath.sqrt((e + 1) / (e - 1));
141 
142         // compute mean anomaly
143         final double eSH     = position.dotProduct(velocity) / FastMath.sqrt(-muA);
144         final double eCH     = 1. - r / a;
145         final double H0      = FastMath.log((eCH + eSH) / (eCH - eSH)) / 2;
146         final double M0      = e * FastMath.sinh(H0) - H0;
147 
148         // find canonical 2D frame with p pointing to perigee
149         final double v0      = 2 * FastMath.atan(sqrt * FastMath.tanh(H0 / 2));
150         final Rotation rotation = new Rotation(pvM, v0, RotationConvention.FRAME_TRANSFORM);
151         final Vector3D p     = rotation.applyTo(position).normalize();
152         final Vector3D q     = pvM.crossProduct(p).normalize();
153 
154         // compute shifted eccentric anomaly
155         final double absA = FastMath.abs(a);
156         final double sqrtRatio = FastMath.sqrt(mu / absA);
157         final double meanMotion = sqrtRatio / absA;
158         final double M1      = M0 + meanMotion * dt;
159         final double H1      = KeplerianAnomalyUtility.hyperbolicMeanToEccentric(e, M1);
160 
161         // compute shifted in-plane Cartesian coordinates
162         final double cH     = FastMath.cosh(H1);
163         final double sH     = FastMath.sinh(H1);
164         final double sE2m1  = FastMath.sqrt((e - 1) * (e + 1));
165 
166         // coordinates of position and velocity in the orbital plane
167         final double x      = a * (cH - e);
168         final double y      = -a * sE2m1 * sH;
169         final double factor = sqrtRatio / (e * cH - 1);
170         final double xDot   = -factor * sH;
171         final double yDot   =  factor * sE2m1 * cH;
172 
173         final Vector3D predictedPosition = new Vector3D(x, p, y, q);
174         final Vector3D predictedVelocity = new Vector3D(xDot, p, yDot, q);
175         return new PVCoordinates(predictedPosition, predictedVelocity);
176     }
177 
178     /**
179      * Method to propagate position and velocity according to Keplerian dynamics.
180      * For long time of flights, it is preferable to use {@link org.orekit.propagation.analytical.KeplerianPropagator}.
181      * @param <T> field type
182      * @param dt time of flight
183      * @param position initial position vector
184      * @param velocity initial velocity vector
185      * @param mu central body gravitational parameter
186      * @return predicted position-velocity
187      */
188     public static <T extends CalculusFieldElement<T>> FieldPVCoordinates<T> predictPositionVelocity(final T dt,
189                                                                                                     final FieldVector3D<T> position,
190                                                                                                     final FieldVector3D<T> velocity,
191                                                                                                     final T mu) {
192         final T r = position.getNorm();
193         final T a = r.divide(r.multiply(velocity.getNormSq()).divide(mu).negate().add(2));
194         if (a.getReal() >= 0.) {
195             return predictPVElliptic(dt, position, velocity, mu, a, r);
196         } else {
197             return predictPVHyperbolic(dt, position, velocity, mu, a, r);
198         }
199     }
200 
201     /**
202      * Method to propagate position and velocity according to Keplerian dynamics, in the case of an elliptic trajectory.
203      * @param <T> field type
204      * @param dt time of flight
205      * @param position initial position vector
206      * @param velocity initial velocity vector
207      * @param mu central body gravitational parameter
208      * @param a semi-major axis
209      * @param r initial radius
210      * @return predicted position-velocity
211      */
212     private static <T extends CalculusFieldElement<T>> FieldPVCoordinates<T> predictPVElliptic(final T dt,
213                                                                                                final FieldVector3D<T> position,
214                                                                                                final FieldVector3D<T> velocity,
215                                                                                                final T mu, final T a,
216                                                                                                final T r) {
217         // preliminary computation0
218         final FieldVector3D<T>      pvM = position.crossProduct(velocity);
219         final T muA                     = mu.multiply(a);
220 
221         // check for near-circular case as automatic differentiation with arctan2 and sqrt does not work there
222         final T eSE              = position.dotProduct(velocity).divide(muA.sqrt());
223         final T eCE              = r.divide(a).negate().add(1);
224         final T e = eCE.square().add(eSE.square()).sqrt();
225         if (e.getReal() < 1e-8) {
226             // one iteration of fixed point algorithm to solve generalized Kepler equation (see Eq. 4.43 in Battin)
227             final T meanMotion = mu.divide(a.square().multiply(a)).sqrt();
228             final T deltaM = meanMotion.multiply(dt);
229             final T sqrtA = a.sqrt();
230             final T sigma = eSE.multiply(sqrtA);
231             final FieldSinCos<T> sinCosDeltaM = FastMath.sinCos(deltaM);
232             final T deltaE = deltaM.add((r.divide(a).negate().add(1)).multiply(sinCosDeltaM.sin())).add(sigma.divide(sqrtA).multiply(sinCosDeltaM.cos().subtract(1)));
233             // F and G Lagrange functions
234             final FieldSinCos<T> sinCosDeltaE = FastMath.sinCos(deltaE);
235             final T sqrtMu = mu.sqrt();
236             final T f = (a.divide(r)).multiply(sinCosDeltaE.cos().subtract(1)).add(1);
237             final T g = a.multiply(sigma).divide(sqrtMu).multiply(sinCosDeltaE.cos().negate().add(1)).add(r.multiply(sqrtA.divide(sqrtMu)).multiply(sinCosDeltaE.sin()));
238             final T shiftedR = a.add(r.subtract(a).multiply(sinCosDeltaE.cos())).add(sigma.multiply(sqrtA).multiply(sinCosDeltaE.sin()));
239             final T fDot = sqrtMu.multiply(sqrtA).divide(r.multiply(shiftedR)).multiply(sinCosDeltaE.sin()).negate();
240             final T gDot = (a.divide(shiftedR)).multiply(sinCosDeltaE.cos().subtract(1)).add(1);
241             // Transition
242             final FieldVector3D<T> shiftedPosition = new FieldVector3D<>(f, position, g, velocity);
243             final FieldVector3D<T> shiftedVelocity = new FieldVector3D<>(fDot, position, gDot, velocity);
244             return new FieldPVCoordinates<>(shiftedPosition, shiftedVelocity);
245         }
246 
247         final T E0               = FastMath.atan2(eSE, eCE);
248         final T M0               = E0.subtract(eSE);
249         final T ePlusOne = e.add(1);
250         final T oneMinusE = e.negate().add(1);
251         final T sqrt                    = ePlusOne.divide(oneMinusE).sqrt();
252 
253         // find canonical 2D frame with p pointing to perigee
254         final T v0               = sqrt.multiply((E0.divide(2)).tan()).atan().multiply(2);
255         final FieldRotation<T> rotation = new FieldRotation<>(pvM, v0, RotationConvention.FRAME_TRANSFORM);
256         final FieldVector3D<T> p = rotation.applyTo(position).normalize();
257         final FieldVector3D<T> q = pvM.crossProduct(p).normalize();
258 
259         // compute shifted eccentric anomaly
260         final T sqrtRatio = (a.reciprocal().multiply(mu)).sqrt();
261         final T meanMotion = sqrtRatio.divide(a);
262         final T M1               = M0.add(meanMotion.multiply(dt));
263         final T E1               = FieldKeplerianAnomalyUtility.ellipticMeanToEccentric(e, M1);
264 
265         // compute shifted in-plane Cartesian coordinates
266         final FieldSinCos<T> scE = FastMath.sinCos(E1);
267         final T               cE = scE.cos();
268         final T               sE = scE.sin();
269         final T            sE2m1 = oneMinusE.multiply(ePlusOne).sqrt();
270 
271         // coordinates of position and velocity in the orbital plane
272         final T x        = a.multiply(cE.subtract(e));
273         final T y        = a.multiply(sE2m1).multiply(sE);
274         final T factor   = sqrtRatio.divide(e.multiply(cE).negate().add(1));
275         final T xDot     = factor.multiply(sE).negate();
276         final T yDot     = factor.multiply(sE2m1).multiply(cE);
277 
278         final FieldVector3D<T> predictedPosition = new FieldVector3D<>(x, p, y, q);
279         final FieldVector3D<T> predictedVelocity = new FieldVector3D<>(xDot, p, yDot, q);
280         return new FieldPVCoordinates<>(predictedPosition, predictedVelocity);
281     }
282 
283     /**
284      * Method to propagate position and velocity according to Keplerian dynamics, in the case of a hyperbolic trajectory.
285      * @param <T> field type
286      * @param dt time of flight
287      * @param position initial position vector
288      * @param velocity initial velocity vector
289      * @param mu central body gravitational parameter
290      * @param a semi-major axis
291      * @param r initial radius
292      * @return predicted position-velocity
293      */
294     private static <T extends CalculusFieldElement<T>> FieldPVCoordinates<T> predictPVHyperbolic(final T dt,
295                                                                                                  final FieldVector3D<T> position,
296                                                                                                  final FieldVector3D<T> velocity,
297                                                                                                  final T mu, final T a,
298                                                                                                  final T r) {
299         // preliminary computations
300         final FieldVector3D<T> pvM   = position.crossProduct(velocity);
301         final T muA     = a.multiply(mu);
302         final T e       = a.newInstance(1.).subtract(pvM.getNormSq().divide(muA)).sqrt();
303         final T ePlusOne = e.add(1);
304         final T eMinusOne = e.subtract(1);
305         final T sqrt    = ePlusOne.divide(eMinusOne).sqrt();
306 
307         // compute mean anomaly
308         final T eSH     = position.dotProduct(velocity).divide(muA.negate().sqrt());
309         final T eCH     = r.divide(a).negate().add(1);
310         final T H0      = eCH.add(eSH).divide(eCH.subtract(eSH)).log().divide(2);
311         final T M0      = e.multiply(H0.sinh()).subtract(H0);
312 
313         // find canonical 2D frame with p pointing to perigee
314         final T v0      = sqrt.multiply(H0.divide(2).tanh()).atan().multiply(2);
315         final FieldRotation<T> rotation = new FieldRotation<>(pvM, v0, RotationConvention.FRAME_TRANSFORM);
316         final FieldVector3D<T> p     = rotation.applyTo(position).normalize();
317         final FieldVector3D<T> q     = pvM.crossProduct(p).normalize();
318 
319         // compute shifted eccentric anomaly
320         final T sqrtRatio = (a.reciprocal().negate().multiply(mu)).sqrt();
321         final T meanMotion = sqrtRatio.divide(a).negate();
322         final T M1      = M0.add(meanMotion.multiply(dt));
323         final T H1      = FieldKeplerianAnomalyUtility.hyperbolicMeanToEccentric(e, M1);
324 
325         // compute shifted in-plane Cartesian coordinates
326         final T cH     = H1.cosh();
327         final T sH     = H1.sinh();
328         final T sE2m1  = eMinusOne.multiply(ePlusOne).sqrt();
329 
330         // coordinates of position and velocity in the orbital plane
331         final T x      = a.multiply(cH.subtract(e));
332         final T y      = a.negate().multiply(sE2m1).multiply(sH);
333         final T factor = sqrtRatio.divide(e.multiply(cH).subtract(1));
334         final T xDot   = factor.negate().multiply(sH);
335         final T yDot   =  factor.multiply(sE2m1).multiply(cH);
336 
337         final FieldVector3D<T> predictedPosition = new FieldVector3D<>(x, p, y, q);
338         final FieldVector3D<T> predictedVelocity = new FieldVector3D<>(xDot, p, yDot, q);
339         return new FieldPVCoordinates<>(predictedPosition, predictedVelocity);
340     }
341 }