FieldKeplerianAnomalyUtility.java

  1. /* Copyright 2002-2025 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.orbits;

  18. import org.hipparchus.CalculusFieldElement;
  19. import org.hipparchus.Field;
  20. import org.hipparchus.exception.MathIllegalStateException;
  21. import org.hipparchus.util.FastMath;
  22. import org.hipparchus.util.FieldSinCos;
  23. import org.hipparchus.util.MathUtils;
  24. import org.hipparchus.util.Precision;
  25. import org.orekit.errors.OrekitInternalError;
  26. import org.orekit.errors.OrekitMessages;

  27. /**
  28.  * Utility methods for converting between different Keplerian anomalies.
  29.  * @author Luc Maisonobe
  30.  * @author Andrea Antolino
  31.  * @author Andrew Goetz
  32.  */
  33. public class FieldKeplerianAnomalyUtility {

  34.     /** First coefficient to compute elliptic Kepler equation solver starter. */
  35.     private static final double A;

  36.     /** Second coefficient to compute elliptic Kepler equation solver starter. */
  37.     private static final double B;

  38.     static {
  39.         final double k1 = 3 * FastMath.PI + 2;
  40.         final double k2 = FastMath.PI - 1;
  41.         final double k3 = 6 * FastMath.PI - 1;
  42.         A = 3 * k2 * k2 / k1;
  43.         B = k3 * k3 / (6 * k1);
  44.     }

  45.     /** Private constructor for utility class. */
  46.     private FieldKeplerianAnomalyUtility() {
  47.         // Nothing to do
  48.     }

  49.     /**
  50.      * Computes the elliptic true anomaly from the elliptic mean anomaly.
  51.      * @param <T> field type
  52.      * @param e eccentricity such that 0 &le; e &lt; 1
  53.      * @param M elliptic mean anomaly (rad)
  54.      * @return elliptic true anomaly (rad)
  55.      */
  56.     public static <T extends CalculusFieldElement<T>> T ellipticMeanToTrue(final T e, final T M) {
  57.         final T E = ellipticMeanToEccentric(e, M);
  58.         final T v = ellipticEccentricToTrue(e, E);
  59.         return v;
  60.     }

  61.     /**
  62.      * Computes the elliptic mean anomaly from the elliptic true anomaly.
  63.      * @param <T> field type
  64.      * @param e eccentricity such that 0 &le; e &lt; 1
  65.      * @param v elliptic true anomaly (rad)
  66.      * @return elliptic mean anomaly (rad)
  67.      */
  68.     public static <T extends CalculusFieldElement<T>> T ellipticTrueToMean(final T e, final T v) {
  69.         final T E = ellipticTrueToEccentric(e, v);
  70.         return ellipticEccentricToMean(e, E);
  71.     }

  72.     /**
  73.      * Computes the elliptic true anomaly from the elliptic eccentric anomaly.
  74.      * @param <T> field type
  75.      * @param e eccentricity such that 0 &le; e &lt; 1
  76.      * @param E elliptic eccentric anomaly (rad)
  77.      * @return elliptic true anomaly (rad)
  78.      */
  79.     public static <T extends CalculusFieldElement<T>> T ellipticEccentricToTrue(final T e, final T E) {
  80.         final T beta = e.divide(e.square().negate().add(1).sqrt().add(1));
  81.         final FieldSinCos<T> scE = FastMath.sinCos(E);
  82.         return E.add(beta.multiply(scE.sin()).divide(beta.multiply(scE.cos()).subtract(1).negate()).atan().multiply(2));
  83.     }

  84.     /**
  85.      * Computes the elliptic eccentric anomaly from the elliptic true anomaly.
  86.      * @param <T> field type
  87.      * @param e eccentricity such that 0 &le; e &lt; 1
  88.      * @param v elliptic true anomaly (rad)
  89.      * @return elliptic eccentric anomaly (rad)
  90.      */
  91.     public static <T extends CalculusFieldElement<T>> T ellipticTrueToEccentric(final T e, final T v) {
  92.         final T beta = e.divide(e.square().negate().add(1).sqrt().add(1));
  93.         final FieldSinCos<T> scv = FastMath.sinCos(v);
  94.         return v.subtract((beta.multiply(scv.sin()).divide(beta.multiply(scv.cos()).add(1))).atan().multiply(2));
  95.     }

  96.     /**
  97.      * Computes the elliptic eccentric anomaly from the elliptic mean anomaly.
  98.      * <p>
  99.      * The algorithm used here for solving hyperbolic Kepler equation is from Odell,
  100.      * A.W., Gooding, R.H. "Procedures for solving Kepler's equation." Celestial
  101.      * Mechanics 38, 307–334 (1986). https://doi.org/10.1007/BF01238923
  102.      * </p>
  103.      * @param <T> field type
  104.      * @param e eccentricity such that 0 &le; e &lt; 1
  105.      * @param M elliptic mean anomaly (rad)
  106.      * @return elliptic eccentric anomaly (rad)
  107.      */
  108.     public static <T extends CalculusFieldElement<T>> T ellipticMeanToEccentric(final T e, final T M) {
  109.         // reduce M to [-PI PI) interval
  110.         final T reducedM = MathUtils.normalizeAngle(M, M.getField().getZero());

  111.         // compute start value according to A. W. Odell and R. H. Gooding S12 starter
  112.         T E;
  113.         if (reducedM.abs().getReal() < 1.0 / 6.0) {
  114.             if (FastMath.abs(reducedM.getReal()) < Precision.SAFE_MIN) {
  115.                 // this is an Orekit change to the S12 starter, mainly used when T is some kind
  116.                 // of derivative structure. If reducedM is 0.0, the derivative of cbrt is
  117.                 // infinite which induces NaN appearing later in the computation. As in this
  118.                 // case E and M are almost equal, we initialize E with reducedM
  119.                 E = reducedM;
  120.             } else {
  121.                 // this is the standard S12 starter
  122.                 E = reducedM.add(e.multiply((reducedM.multiply(6).cbrt()).subtract(reducedM)));
  123.             }
  124.         } else {
  125.             final T pi = e.getPi();
  126.             if (reducedM.getReal() < 0) {
  127.                 final T w = reducedM.add(pi);
  128.                 E = reducedM.add(e.multiply(w.multiply(A).divide(w.negate().add(B)).subtract(pi).subtract(reducedM)));
  129.             } else {
  130.                 final T w = reducedM.negate().add(pi);
  131.                 E = reducedM
  132.                         .add(e.multiply(w.multiply(A).divide(w.negate().add(B)).negate().subtract(reducedM).add(pi)));
  133.             }
  134.         }

  135.         final T e1 = e.negate().add(1);
  136.         final boolean noCancellationRisk = (e1.getReal() + E.getReal() * E.getReal() / 6) >= 0.1;

  137.         // perform two iterations, each consisting of one Halley step and one
  138.         // Newton-Raphson step
  139.         for (int j = 0; j < 2; ++j) {

  140.             final T f;
  141.             T fd;
  142.             final FieldSinCos<T> scE = FastMath.sinCos(E);
  143.             final T fdd = e.multiply(scE.sin());
  144.             final T fddd = e.multiply(scE.cos());

  145.             if (noCancellationRisk) {

  146.                 f = (E.subtract(fdd)).subtract(reducedM);
  147.                 fd = fddd.negate().add(1);
  148.             } else {

  149.                 f = eMeSinE(e, E).subtract(reducedM);
  150.                 final T s = E.multiply(0.5).sin();
  151.                 fd = e1.add(e.multiply(s.square()).multiply(2));
  152.             }
  153.             final T dee = f.multiply(fd).divide(f.multiply(fdd).multiply(0.5).subtract(fd.square()));

  154.             // update eccentric anomaly, using expressions that limit underflow problems
  155.             final T w = fd.add(dee.multiply(fdd.add(dee.multiply(fddd.divide(3)))).multiply(0.5));
  156.             fd = fd.add(dee.multiply(fdd.add(dee.multiply(fddd).multiply(0.5))));
  157.             E = E.subtract(f.subtract(dee.multiply(fd.subtract(w))).divide(fd));

  158.         }

  159.         // expand the result back to original range
  160.         E = E.add(M).subtract(reducedM);
  161.         return E;
  162.     }

  163.     /**
  164.      * Accurate computation of E - e sin(E).
  165.      * <p>
  166.      * This method is used when E is close to 0 and e close to 1, i.e. near the
  167.      * perigee of almost parabolic orbits
  168.      * </p>
  169.      * @param <T> field type
  170.      * @param e eccentricity
  171.      * @param E eccentric anomaly (rad)
  172.      * @return E - e sin(E)
  173.      */
  174.     private static <T extends CalculusFieldElement<T>> T eMeSinE(final T e, final T E) {
  175.         T x = (e.negate().add(1)).multiply(E.sin());
  176.         final T mE2 = E.square().negate();
  177.         T term = E;
  178.         double d = 0;
  179.         // the inequality test below IS intentional and should NOT be replaced by a
  180.         // check with a small tolerance
  181.         for (T x0 = E.getField().getZero().add(Double.NaN); !Double.valueOf(x.getReal())
  182.                 .equals(x0.getReal());) {
  183.             d += 2;
  184.             term = term.multiply(mE2.divide(d * (d + 1)));
  185.             x0 = x;
  186.             x = x.subtract(term);
  187.         }
  188.         return x;
  189.     }

  190.     /**
  191.      * Computes the elliptic mean anomaly from the elliptic eccentric anomaly.
  192.      * @param <T> field type
  193.      * @param e eccentricity such that 0 &le; e &lt; 1
  194.      * @param E elliptic eccentric anomaly (rad)
  195.      * @return elliptic mean anomaly (rad)
  196.      */
  197.     public static <T extends CalculusFieldElement<T>> T ellipticEccentricToMean(final T e, final T E) {
  198.         return E.subtract(e.multiply(E.sin()));
  199.     }

  200.     /**
  201.      * Computes the hyperbolic true anomaly from the hyperbolic mean anomaly.
  202.      * @param <T> field type
  203.      * @param e eccentricity &gt; 1
  204.      * @param M hyperbolic mean anomaly
  205.      * @return hyperbolic true anomaly (rad)
  206.      */
  207.     public static <T extends CalculusFieldElement<T>> T hyperbolicMeanToTrue(final T e, final T M) {
  208.         final T H = hyperbolicMeanToEccentric(e, M);
  209.         final T v = hyperbolicEccentricToTrue(e, H);
  210.         return v;
  211.     }

  212.     /**
  213.      * Computes the hyperbolic mean anomaly from the hyperbolic true anomaly.
  214.      * @param <T> field type
  215.      * @param e eccentricity &gt; 1
  216.      * @param v hyperbolic true anomaly (rad)
  217.      * @return hyperbolic mean anomaly
  218.      */
  219.     public static <T extends CalculusFieldElement<T>> T hyperbolicTrueToMean(final T e, final T v) {
  220.         final T H = hyperbolicTrueToEccentric(e, v);
  221.         return hyperbolicEccentricToMean(e, H);
  222.     }

  223.     /**
  224.      * Computes the hyperbolic true anomaly from the hyperbolic eccentric anomaly.
  225.      * @param <T> field type
  226.      * @param e eccentricity &gt; 1
  227.      * @param H hyperbolic eccentric anomaly
  228.      * @return hyperbolic true anomaly (rad)
  229.      */
  230.     public static <T extends CalculusFieldElement<T>> T hyperbolicEccentricToTrue(final T e, final T H) {
  231.         final T s = e.add(1).divide(e.subtract(1)).sqrt();
  232.         final T tanH = H.multiply(0.5).tanh();
  233.         return s.multiply(tanH).atan().multiply(2);
  234.     }

  235.     /**
  236.      * Computes the hyperbolic eccentric anomaly from the hyperbolic true anomaly.
  237.      * @param <T> field type
  238.      * @param e eccentricity &gt; 1
  239.      * @param v hyperbolic true anomaly (rad)
  240.      * @return hyperbolic eccentric anomaly
  241.      */
  242.     public static <T extends CalculusFieldElement<T>> T hyperbolicTrueToEccentric(final T e, final T v) {
  243.         final FieldSinCos<T> scv = FastMath.sinCos(v);
  244.         final T sinhH = e.square().subtract(1).sqrt().multiply(scv.sin()).divide(e.multiply(scv.cos()).add(1));
  245.         return sinhH.asinh();
  246.     }

  247.     /**
  248.      * Computes the hyperbolic eccentric anomaly from the hyperbolic mean anomaly.
  249.      * <p>
  250.      * The algorithm used here for solving hyperbolic Kepler equation is from
  251.      * Gooding, R.H., Odell, A.W. "The hyperbolic Kepler equation (and the elliptic
  252.      * equation revisited)." Celestial Mechanics 44, 267–282 (1988).
  253.      * https://doi.org/10.1007/BF01235540
  254.      * </p>
  255.      * @param <T> field type
  256.      * @param e eccentricity &gt; 1
  257.      * @param M hyperbolic mean anomaly
  258.      * @return hyperbolic eccentric anomaly
  259.      */
  260.     public static <T extends CalculusFieldElement<T>> T hyperbolicMeanToEccentric(final T e, final T M) {
  261.         final Field<T> field = e.getField();
  262.         final T zero = field.getZero();
  263.         final T one = field.getOne();
  264.         final T two = zero.newInstance(2.0);
  265.         final T three = zero.newInstance(3.0);
  266.         final T half = zero.newInstance(0.5);
  267.         final T onePointFive = zero.newInstance(1.5);
  268.         final T fourThirds = zero.newInstance(4.0).divide(3.0);

  269.         // Solve L = S - g * asinh(S) for S = sinh(H).
  270.         final T L = M.divide(e);
  271.         final T g = e.reciprocal();
  272.         final T g1 = one.subtract(g);

  273.         // Starter based on Lagrange's theorem.
  274.         T S = L;
  275.         if (L.isZero()) {
  276.             return M.getField().getZero();
  277.         }
  278.         final T cl = L.square().add(one).sqrt();
  279.         final T al = L.asinh();
  280.         final T w = g.square().multiply(al).divide(cl.multiply(cl).multiply(cl));
  281.         S = one.subtract(g.divide(cl));
  282.         S = L.add(g.multiply(al).divide(S.square().multiply(S)
  283.                 .add(w.multiply(L).multiply(onePointFive.subtract(fourThirds.multiply(g)))).cbrt()));

  284.         // Two iterations (at most) of Halley-then-Newton process.
  285.         for (int i = 0; i < 2; ++i) {
  286.             final T s0 = S.square();
  287.             final T s1 = s0.add(one);
  288.             final T s2 = s1.sqrt();
  289.             final T s3 = s1.multiply(s2);
  290.             final T fdd = g.multiply(S).divide(s3);
  291.             final T fddd = g.multiply(one.subtract(two.multiply(s0))).divide(s1.multiply(s3));

  292.             T f;
  293.             T fd;
  294.             if (s0.divide(6.0).add(g1).getReal() >= 0.5) {
  295.                 f = S.subtract(g.multiply(S.asinh())).subtract(L);
  296.                 fd = one.subtract(g.divide(s2));
  297.             } else {
  298.                 // Accurate computation of S - (1 - g1) * asinh(S)
  299.                 // when (g1, S) is close to (0, 0).
  300.                 final T t = S.divide(one.add(one.add(S.multiply(S)).sqrt()));
  301.                 final T tsq = t.square();
  302.                 T x = S.multiply(g1.add(g.multiply(tsq)));
  303.                 T term = two.multiply(g).multiply(t);
  304.                 T twoI1 = one;
  305.                 T x0;
  306.                 int j = 0;
  307.                 do {
  308.                     if (++j == 1000000) {
  309.                         // This isn't expected to happen, but it protects against an infinite loop.
  310.                         throw new MathIllegalStateException(
  311.                                 OrekitMessages.UNABLE_TO_COMPUTE_HYPERBOLIC_ECCENTRIC_ANOMALY, j);
  312.                     }
  313.                     twoI1 = twoI1.add(2.0);
  314.                     term = term.multiply(tsq);
  315.                     x0 = x;
  316.                     x = x.subtract(term.divide(twoI1));
  317.                 } while (x.getReal() != x0.getReal());
  318.                 f = x.subtract(L);
  319.                 fd = s0.divide(s2.add(one)).add(g1).divide(s2);
  320.             }
  321.             final T ds = f.multiply(fd).divide(half.multiply(f).multiply(fdd).subtract(fd.multiply(fd)));
  322.             final T stemp = S.add(ds);
  323.             if (S.getReal() == stemp.getReal()) {
  324.                 break;
  325.             }
  326.             f = f.add(ds.multiply(fd.add(half.multiply(ds.multiply(fdd.add(ds.divide(three).multiply(fddd)))))));
  327.             fd = fd.add(ds.multiply(fdd.add(half.multiply(ds).multiply(fddd))));
  328.             S = stemp.subtract(f.divide(fd));
  329.         }

  330.         final T H = S.asinh();
  331.         return H;
  332.     }

  333.     /**
  334.      * Computes the hyperbolic mean anomaly from the hyperbolic eccentric anomaly.
  335.      * @param <T> field type
  336.      * @param e eccentricity &gt; 1
  337.      * @param H hyperbolic eccentric anomaly
  338.      * @return hyperbolic mean anomaly
  339.      */
  340.     public static <T extends CalculusFieldElement<T>> T hyperbolicEccentricToMean(final T e, final T H) {
  341.         return e.multiply(H.sinh()).subtract(H);
  342.     }

  343.     /**
  344.      * Convert anomaly.
  345.      * @param oldType old position angle type
  346.      * @param anomaly old value for anomaly
  347.      * @param e eccentricity
  348.      * @param newType new position angle type
  349.      * @param <T> field type
  350.      * @return converted anomaly
  351.      * @since 12.2
  352.      */
  353.     public static <T extends CalculusFieldElement<T>> T convertAnomaly(final PositionAngleType oldType, final T anomaly,
  354.                                                                        final T e, final PositionAngleType newType) {
  355.         if (oldType == newType) {
  356.             return anomaly;

  357.         } else {
  358.             if (e.getReal() > 1) {
  359.                 switch (newType) {
  360.                     case MEAN:
  361.                         if (oldType == PositionAngleType.ECCENTRIC) {
  362.                             return FieldKeplerianAnomalyUtility.hyperbolicEccentricToMean(e, anomaly);
  363.                         } else {
  364.                             return FieldKeplerianAnomalyUtility.hyperbolicTrueToMean(e, anomaly);
  365.                         }

  366.                     case ECCENTRIC:
  367.                         if (oldType == PositionAngleType.MEAN) {
  368.                             return FieldKeplerianAnomalyUtility.hyperbolicMeanToEccentric(e, anomaly);
  369.                         } else {
  370.                             return FieldKeplerianAnomalyUtility.hyperbolicTrueToEccentric(e, anomaly);
  371.                         }

  372.                     case TRUE:
  373.                         if (oldType == PositionAngleType.ECCENTRIC) {
  374.                             return FieldKeplerianAnomalyUtility.hyperbolicEccentricToTrue(e, anomaly);
  375.                         } else {
  376.                             return FieldKeplerianAnomalyUtility.hyperbolicMeanToTrue(e, anomaly);
  377.                         }

  378.                     default:
  379.                         break;
  380.                 }

  381.             } else {
  382.                 switch (newType) {
  383.                     case MEAN:
  384.                         if (oldType == PositionAngleType.ECCENTRIC) {
  385.                             return FieldKeplerianAnomalyUtility.ellipticEccentricToMean(e, anomaly);
  386.                         } else {
  387.                             return FieldKeplerianAnomalyUtility.ellipticTrueToMean(e, anomaly);
  388.                         }

  389.                     case ECCENTRIC:
  390.                         if (oldType == PositionAngleType.MEAN) {
  391.                             return FieldKeplerianAnomalyUtility.ellipticMeanToEccentric(e, anomaly);
  392.                         } else {
  393.                             return FieldKeplerianAnomalyUtility.ellipticTrueToEccentric(e, anomaly);
  394.                         }

  395.                     case TRUE:
  396.                         if (oldType == PositionAngleType.ECCENTRIC) {
  397.                             return FieldKeplerianAnomalyUtility.ellipticEccentricToTrue(e, anomaly);
  398.                         } else {
  399.                             return FieldKeplerianAnomalyUtility.ellipticMeanToTrue(e, anomaly);
  400.                         }

  401.                     default:
  402.                         break;
  403.                 }
  404.             }
  405.             throw new OrekitInternalError(null);
  406.         }
  407.     }
  408. }