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         // compute mean anomaly
222         final T eSE              = position.dotProduct(velocity).divide(muA.sqrt());
223         final T eCE              = r.divide(a).negate().add(1);
224         final T E0               = FastMath.atan2(eSE, eCE);
225         final T M0               = E0.subtract(eSE);
226 
227         final T e                       = eCE.square().add(eSE.square()).sqrt();
228         final T ePlusOne = e.add(1);
229         final T oneMinusE = e.negate().add(1);
230         final T sqrt                    = ePlusOne.divide(oneMinusE).sqrt();
231 
232         // find canonical 2D frame with p pointing to perigee
233         final T v0               = sqrt.multiply((E0.divide(2)).tan()).atan().multiply(2);
234         final FieldRotation<T> rotation = new FieldRotation<>(pvM, v0, RotationConvention.FRAME_TRANSFORM);
235         final FieldVector3D<T> p = rotation.applyTo(position).normalize();
236         final FieldVector3D<T> q = pvM.crossProduct(p).normalize();
237 
238         // compute shifted eccentric anomaly
239         final T sqrtRatio = (a.reciprocal().multiply(mu)).sqrt();
240         final T meanMotion = sqrtRatio.divide(a);
241         final T M1               = M0.add(meanMotion.multiply(dt));
242         final T E1               = FieldKeplerianAnomalyUtility.ellipticMeanToEccentric(e, M1);
243 
244         // compute shifted in-plane Cartesian coordinates
245         final FieldSinCos<T> scE = FastMath.sinCos(E1);
246         final T               cE = scE.cos();
247         final T               sE = scE.sin();
248         final T            sE2m1 = oneMinusE.multiply(ePlusOne).sqrt();
249 
250         // coordinates of position and velocity in the orbital plane
251         final T x        = a.multiply(cE.subtract(e));
252         final T y        = a.multiply(sE2m1).multiply(sE);
253         final T factor   = sqrtRatio.divide(e.multiply(cE).negate().add(1));
254         final T xDot     = factor.multiply(sE).negate();
255         final T yDot     = factor.multiply(sE2m1).multiply(cE);
256 
257         final FieldVector3D<T> predictedPosition = new FieldVector3D<>(x, p, y, q);
258         final FieldVector3D<T> predictedVelocity = new FieldVector3D<>(xDot, p, yDot, q);
259         return new FieldPVCoordinates<>(predictedPosition, predictedVelocity);
260     }
261 
262     /**
263      * Method to propagate position and velocity according to Keplerian dynamics, in the case of a hyperbolic trajectory.
264      * @param <T> field type
265      * @param dt time of flight
266      * @param position initial position vector
267      * @param velocity initial velocity vector
268      * @param mu central body gravitational parameter
269      * @param a semi-major axis
270      * @param r initial radius
271      * @return predicted position-velocity
272      */
273     private static <T extends CalculusFieldElement<T>> FieldPVCoordinates<T> predictPVHyperbolic(final T dt,
274                                                                                                  final FieldVector3D<T> position,
275                                                                                                  final FieldVector3D<T> velocity,
276                                                                                                  final T mu, final T a,
277                                                                                                  final T r) {
278         // preliminary computations
279         final FieldVector3D<T> pvM   = position.crossProduct(velocity);
280         final T muA     = a.multiply(mu);
281         final T e       = a.newInstance(1.).subtract(pvM.getNormSq().divide(muA)).sqrt();
282         final T ePlusOne = e.add(1);
283         final T eMinusOne = e.subtract(1);
284         final T sqrt    = ePlusOne.divide(eMinusOne).sqrt();
285 
286         // compute mean anomaly
287         final T eSH     = position.dotProduct(velocity).divide(muA.negate().sqrt());
288         final T eCH     = r.divide(a).negate().add(1);
289         final T H0      = eCH.add(eSH).divide(eCH.subtract(eSH)).log().divide(2);
290         final T M0      = e.multiply(H0.sinh()).subtract(H0);
291 
292         // find canonical 2D frame with p pointing to perigee
293         final T v0      = sqrt.multiply(H0.divide(2).tanh()).atan().multiply(2);
294         final FieldRotation<T> rotation = new FieldRotation<>(pvM, v0, RotationConvention.FRAME_TRANSFORM);
295         final FieldVector3D<T> p     = rotation.applyTo(position).normalize();
296         final FieldVector3D<T> q     = pvM.crossProduct(p).normalize();
297 
298         // compute shifted eccentric anomaly
299         final T sqrtRatio = (a.reciprocal().negate().multiply(mu)).sqrt();
300         final T meanMotion = sqrtRatio.divide(a).negate();
301         final T M1      = M0.add(meanMotion.multiply(dt));
302         final T H1      = FieldKeplerianAnomalyUtility.hyperbolicMeanToEccentric(e, M1);
303 
304         // compute shifted in-plane Cartesian coordinates
305         final T cH     = H1.cosh();
306         final T sH     = H1.sinh();
307         final T sE2m1  = eMinusOne.multiply(ePlusOne).sqrt();
308 
309         // coordinates of position and velocity in the orbital plane
310         final T x      = a.multiply(cH.subtract(e));
311         final T y      = a.negate().multiply(sE2m1).multiply(sH);
312         final T factor = sqrtRatio.divide(e.multiply(cH).subtract(1));
313         final T xDot   = factor.negate().multiply(sH);
314         final T yDot   =  factor.multiply(sE2m1).multiply(cH);
315 
316         final FieldVector3D<T> predictedPosition = new FieldVector3D<>(x, p, y, q);
317         final FieldVector3D<T> predictedVelocity = new FieldVector3D<>(xDot, p, yDot, q);
318         return new FieldPVCoordinates<>(predictedPosition, predictedVelocity);
319     }
320 }