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.control.heuristics.lambert;
18  
19  import org.hipparchus.analysis.differentiation.Gradient;
20  import org.hipparchus.exception.MathIllegalStateException;
21  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
22  import org.hipparchus.geometry.euclidean.threed.Vector3D;
23  import org.hipparchus.geometry.euclidean.twod.Vector2D;
24  import org.hipparchus.linear.DecompositionSolver;
25  import org.hipparchus.linear.LUDecomposition;
26  import org.hipparchus.linear.MatrixUtils;
27  import org.hipparchus.linear.RealMatrix;
28  import org.hipparchus.util.FastMath;
29  import org.orekit.orbits.KeplerianMotionCartesianUtility;
30  import org.orekit.utils.FieldPVCoordinates;
31  import org.orekit.utils.PVCoordinates;
32  
33  /**
34   * Lambert solver, assuming Keplerian motion.
35   * <p>
36   * An orbit is determined from two position vectors.
37   * </p>
38   * <p>
39   * References:
40   *  Battin, R.H., An Introduction to the Mathematics and Methods of Astrodynamics, AIAA Education, 1999.
41   *  Lancaster, E.R. and Blanchard, R.C., A Unified Form of Lambert’s Theorem, Goddard Space Flight Center, 1968.
42   * </p>
43   * @author Joris Olympio
44   * @author Romain Serra
45   * @see LambertBoundaryConditions
46   * @see LambertBoundaryVelocities
47   * @since 13.1
48   */
49  public class LambertSolver {
50  
51      /** gravitational constant. */
52      private final double mu;
53  
54      /** Creator.
55       *
56       * @param mu gravitational constant
57       */
58      public LambertSolver(final double mu) {
59          this.mu = mu;
60      }
61  
62      /** Solve for the corresponding velocity vectors given two position vectors and a duration.
63       * <p>
64       * The logic for setting {@code posigrade} and {@code nRev} is that the
65       * sweep angle Δυ travelled by the object between {@code t1} and {@code t2} is
66       * 2π {@code nRev +1} - α if {@code posigrade} is false and 2π {@code nRev} + α
67       * if {@code posigrade} is true, where α is the separation angle between
68       * {@code p1} and {@code p2}, which is always computed between 0 and π
69       * (because in 3D without a normal reference, vector angles cannot go past π).
70       * </p>
71       * <p>
72       * This implies that {@code posigrade} should be set to true if {@code p2} is
73       * located in the half orbit starting at {@code p1} and it should be set to
74       * false if {@code p2} is located in the half orbit ending at {@code p1},
75       * regardless of the number of periods between {@code t1} and {@code t2},
76       * and {@code nRev} should be set accordingly.
77       * </p>
78       * <p>
79       * As an example, if {@code t2} is less than half a period after {@code t1},
80       * then {@code posigrade} should be {@code true} and {@code nRev} should be 0.
81       * If {@code t2} is more than half a period after {@code t1} but less than
82       * one period after {@code t1}, {@code posigrade} should be {@code false} and
83       * {@code nRev} should be 0.
84       * </p>
85       * <p>
86       * If solving fails completely, null is returned.
87       * If only the computation of terminal velocity fails, a partial pair of velocities is returned (with some NaNs).
88       * </p>
89       * @param posigrade flag indicating the direction of motion
90       * @param nRev      number of revolutions
91       * @param boundaryConditions Lambert problem boundary conditions
92       * @return boundary velocity vectors
93       */
94      public LambertBoundaryVelocities solve(final boolean posigrade, final int nRev,
95                                             final LambertBoundaryConditions boundaryConditions) {
96          final Vector3D p1 = boundaryConditions.getInitialPosition();
97          final Vector3D p2 = boundaryConditions.getTerminalPosition();
98          final double r1 = p1.getNorm();
99          final double r2 = p2.getNorm();
100         final double tau = boundaryConditions.getTerminalDate().durationFrom(boundaryConditions.getInitialDate());
101 
102         // deal with backward case recursively
103         if (tau < 0.0) {
104             final LambertBoundaryConditions backwardConditions = new LambertBoundaryConditions(boundaryConditions.getTerminalDate(),
105                     boundaryConditions.getTerminalPosition(), boundaryConditions.getInitialDate(), boundaryConditions.getInitialPosition(),
106                     boundaryConditions.getReferenceFrame());
107             final LambertBoundaryVelocities solutionForward = solve(posigrade, nRev, backwardConditions);
108             if (solutionForward != null) {
109                 return new LambertBoundaryVelocities(solutionForward.getTerminalVelocity(),
110                         solutionForward.getInitialVelocity());
111             } else {
112                 return null;
113             }
114         }
115 
116         // normalizing constants
117         final double R = FastMath.max(r1, r2); // in m
118         final double V = FastMath.sqrt(mu / R);  // in m/s
119         final double T = R / V; // in seconds
120 
121         // sweep angle
122         double dth = Vector3D.angle(p1, p2);
123         // compute the number of revolutions
124         if (!posigrade) {
125             dth = 2 * FastMath.PI - dth;
126         }
127 
128         // call Lambert's problem solver in the orbital plane, in the R-T frame
129         final Vector2D vDep = solveNormalized2D(r1 / R, r2 / R, dth, tau / T, nRev);
130 
131         if (vDep != Vector2D.NaN) {
132             final double[] Vdep = vDep.toArray();
133             // basis vectors
134             // normal to the orbital arc plane
135             final Vector3D Pn = p1.crossProduct(p2);
136             // perpendicular to the radius vector, in the orbital arc plane
137             final Vector3D Pt = Pn.crossProduct(p1);
138 
139             // tangential velocity norm
140             double RT = Pt.getNorm();
141             if (!posigrade) {
142                 RT = -RT;
143             }
144 
145             // velocity vector at P1
146             final Vector3D Vel1 = new Vector3D(V * Vdep[0] / r1, p1, V * Vdep[1] / RT, Pt);
147 
148             // propagate to get terminal velocity
149             Vector3D terminalVelocity;
150             try {
151                 final PVCoordinates pv2 = KeplerianMotionCartesianUtility.predictPositionVelocity(tau, p1, Vel1, mu);
152                 terminalVelocity = pv2.getVelocity();
153             } catch (final MathIllegalStateException exception) {  // failure can happen for hyperbolic orbits
154                 terminalVelocity = Vector3D.NaN;
155             }
156 
157             // form output
158             return new LambertBoundaryVelocities(Vel1, terminalVelocity);
159         }
160 
161         return null;
162     }
163 
164     /** Lambert's solver for the historical, planar problem.
165      * Assume mu=1.
166      *
167      * @param r1 radius 1
168      * @param r2  radius 2
169      * @param dth sweep angle
170      * @param tau time of flight
171      * @param mRev number of revs
172      * @return velocity at departure in (T, N) basis. Is Vector2D.NaN if solving fails
173      */
174     public static Vector2D solveNormalized2D(final double r1, final double r2, final double dth, final double tau,
175                                              final int mRev) {
176         // decide whether to use the left or right branch (for multi-revolution
177         // problems), and the long- or short way.
178         final boolean leftbranch = dth < FastMath.PI;
179         int longway = 1;
180         if (dth > FastMath.PI) {
181             longway = -1;
182         }
183 
184         final int m = FastMath.abs(mRev);
185         final double rtof = FastMath.abs(tau);
186 
187         // non-dimensional chord ||r2-r1||
188         final double chord = FastMath.sqrt(r1 * r1 + r2 * r2 - 2 * r1 * r2 * FastMath.cos(dth));
189 
190         // non-dimensional semi-perimeter of the triangle
191         final double speri = 0.5 * (r1 + r2 + chord);
192 
193         // minimum energy ellipse semi-major axis
194         final double minSma = speri / 2.;
195 
196         // lambda parameter (Eq 7.6)
197         final double lambda = longway * FastMath.sqrt(1 - chord / speri);
198 
199         // reference tof value for the Newton solver
200         final double logt = FastMath.log(rtof);
201 
202         // initialisation of the iterative root finding process (secant method)
203         // initial values
204         //  -1 < x < 1  =>  Elliptical orbits
205         //  x = 1           Parabolic orbit
206         //  x > 1           Hyperbolic orbits
207         final double in1;
208         final double in2;
209         double x1;
210         double x2;
211         if (m == 0) {
212             // one revolution, one solution. Define the left and right asymptotes.
213             in1 = -0.6523333;
214             in2 = 0.6523333;
215             x1   = FastMath.log(1 + in1);
216             x2   = FastMath.log(1 + in2);
217         } else {
218             // select initial values, depending on the branch
219             if (!leftbranch) {
220                 in1 = -0.523334;
221                 in2 = -0.223334;
222             } else {
223                 in1 = 0.723334;
224                 in2 = 0.523334;
225             }
226             x1 = FastMath.tan(in1 * 0.5 * FastMath.PI);
227             x2 = FastMath.tan(in2 * 0.5 * FastMath.PI);
228         }
229 
230         // initial estimates for the tof
231         final double tof1 = timeOfFlight(in1, longway, m, minSma, speri, chord);
232         final double tof2 = timeOfFlight(in2, longway, m, minSma, speri, chord);
233 
234         // initial bounds for y
235         double y1;
236         double y2;
237         if (m == 0) {
238             y1 = FastMath.log(tof1) - logt;
239             y2 = FastMath.log(tof2) - logt;
240         } else {
241             y1 = tof1 - rtof;
242             y2 = tof2 - rtof;
243         }
244 
245         // Solve for x with the secant method
246         double err = 1e20;
247         int iterations = 0;
248         final double tol = 1e-13;
249         final int maxiter = 50;
250         double xnew = 0;
251         while (err > tol && iterations < maxiter) {
252             // new x
253             xnew = (x1 * y2 - y1 * x2) / (y2 - y1);
254 
255             // evaluate new time of flight
256             final double x;
257             if (m == 0) {
258                 x = FastMath.exp(xnew) - 1;
259             } else {
260                 x = FastMath.atan(xnew) * 2 / FastMath.PI;
261             }
262 
263             final double tof = timeOfFlight(x, longway, m, minSma, speri, chord);
264 
265             // new value of y
266             final double ynew;
267             if (m == 0) {
268                 ynew = FastMath.log(tof) - logt;
269             } else {
270                 ynew = tof - rtof;
271             }
272 
273             // save previous and current values for the next iteration
274             x1 = x2;
275             x2 = xnew;
276             y1 = y2;
277             y2 = ynew;
278 
279             // update error
280             err = FastMath.abs(x1 - xnew);
281 
282             // increment number of iterations
283             ++iterations;
284         }
285 
286         // failure to converge
287         if (err > tol) {
288             return Vector2D.NaN;
289         }
290 
291         // convert converged value of x
292         final double x;
293         if (m == 0) {
294             x = FastMath.exp(xnew) - 1;
295         } else {
296             x = FastMath.atan(xnew) * 2 / FastMath.PI;
297         }
298 
299         // Solution for the semi-major axis (Eq. 7.20)
300         final double sma = minSma / (1 - x * x);
301 
302         // compute velocities
303         final double eta;
304         if (x < 1) {
305             // ellipse, Eqs. 7.7, 7.17
306             final double alfa = 2 * FastMath.acos(x);
307             final double beta = longway * 2 * FastMath.asin(FastMath.sqrt((speri - chord) / (2. * sma)));
308             final double psi  = (alfa - beta) / 2;
309             // Eq. 7.21
310             final double sinPsi = FastMath.sin(psi);
311             final double etaSq = 2 * sma * sinPsi * sinPsi / speri;
312             eta  = FastMath.sqrt(etaSq);
313         } else {
314             // hyperbola
315             final double gamma = 2 * FastMath.acosh(x);
316             final double delta = longway * 2 * FastMath.asinh(FastMath.sqrt((chord - speri) / (2 * sma)));
317             //
318             final double psi  = (gamma - delta) / 2.;
319             final double sinhPsi = FastMath.sinh(psi);
320             final double etaSq = -2 * sma * sinhPsi * sinhPsi / speri;
321             eta  = FastMath.sqrt(etaSq);
322         }
323 
324         // radial and tangential directions for departure velocity (Eq. 7.36)
325         final double VR1 = (1. / eta) * FastMath.sqrt(1. / minSma) * (2 * lambda * minSma / r1 - (lambda + x * eta));
326         final double VT1 = (1. / eta) * FastMath.sqrt(1. / minSma) * FastMath.sqrt(r2 / r1) * FastMath.sin(dth / 2);
327         return new Vector2D(VR1, VT1);
328     }
329 
330     /** Compute the time of flight of a given arc of orbit.
331      * The time of flight is evaluated via the Lagrange expression.
332      *
333      * @param x          x
334      * @param longway    solution number; the long way or the short war
335      * @param mrev       number of revolutions of the arc of orbit
336      * @param minSma     minimum possible semi-major axis
337      * @param speri      semi-parameter of the arc of orbit
338      * @param chord      chord of the arc of orbit
339      * @return the time of flight for the given arc of orbit
340      */
341     private static double timeOfFlight(final double x, final int longway, final int mrev, final double minSma,
342                                        final double speri, final double chord) {
343 
344         final double a = minSma / (1 - x * x);
345 
346         final double tof;
347         if (FastMath.abs(x) < 1) {
348             // Lagrange form of the time of flight equation Eq. (7.9)
349             // elliptical orbit (note: mu = 1)
350             final double beta = longway * 2 * FastMath.asin(FastMath.sqrt((speri - chord) / (2. * a)));
351             final double alfa = 2 * FastMath.acos(x);
352             tof = a * FastMath.sqrt(a) * ((alfa - FastMath.sin(alfa)) - (beta - FastMath.sin(beta)) + 2 * FastMath.PI * mrev);
353         } else {
354             // hyperbolic orbit
355             final double alfa = 2 * FastMath.acosh(x);
356             final double beta = longway * 2 * FastMath.asinh(FastMath.sqrt((speri - chord) / (-2. * a)));
357             tof = -a * FastMath.sqrt(-a) * ((FastMath.sinh(alfa) - alfa) - (FastMath.sinh(beta) - beta));
358         }
359 
360         return tof;
361     }
362 
363     /**
364      * Computes the Jacobian matrix of the Lambert solution.
365      * The rows represent the initial and terminal velocity vectors.
366      * The columns represent the parameters: initial time, initial position, terminal time, terminal velocity.
367      * <p>
368      * Reference:
369      * Di Lizia, P., Armellin, R., Zazzera, F. B., and Berz, M.
370      * High Order Expansion of the Solution of Two-Point Boundary Value Problems using Differential Algebra:
371      * Applications to Spacecraft Dynamics.
372      * </p>
373      * @param posigrade direction flag
374      * @param nRev number of revolutions
375      * @param boundaryConditions Lambert boundary conditions
376      * @return Jacobian matrix
377      */
378     public RealMatrix computeJacobian(final boolean posigrade, final int nRev,
379                                       final LambertBoundaryConditions boundaryConditions) {
380         final LambertBoundaryVelocities velocities = solve(posigrade, nRev, boundaryConditions);
381         if (velocities != null) {
382             return computeNonTrivialCase(boundaryConditions, velocities);
383         } else {
384             final RealMatrix nanMatrix = MatrixUtils.createRealMatrix(8, 8);
385             for (int i = 0; i < nanMatrix.getRowDimension(); i++) {
386                 for (int j = 0; j < nanMatrix.getColumnDimension(); j++) {
387                     nanMatrix.setEntry(i, j, Double.NaN);
388                 }
389             }
390             return nanMatrix;
391         }
392     }
393 
394     /**
395      * Compute Jacobian matrix assuming there is a solution.
396      * @param boundaryConditions Lambert boundary conditions
397      * @param velocities Lambert solution
398      * @return Jacobian matrix
399      */
400     private RealMatrix computeNonTrivialCase(final LambertBoundaryConditions boundaryConditions,
401                                              final LambertBoundaryVelocities velocities) {
402         // propagate with automatic differentiation, using initial position as independent variables
403         final int freeParameters = 8;
404         final Vector3D nominalInitialPosition = boundaryConditions.getInitialPosition();
405         final FieldVector3D<Gradient> initialPosition = new FieldVector3D<>(Gradient.variable(freeParameters, 5, nominalInitialPosition.getX()),
406                 Gradient.variable(freeParameters, 6, nominalInitialPosition.getY()),
407                 Gradient.variable(freeParameters, 7, nominalInitialPosition.getZ()));
408         final Vector3D nominalInitialVelocity = velocities.getInitialVelocity();
409         final FieldVector3D<Gradient> initialVelocity = new FieldVector3D<>(Gradient.variable(freeParameters, 1, nominalInitialVelocity.getX()),
410                 Gradient.variable(freeParameters, 2, nominalInitialVelocity.getY()),
411                 Gradient.variable(freeParameters, 3, nominalInitialVelocity.getZ()));
412         final double dt = boundaryConditions.getTerminalDate().durationFrom(boundaryConditions.getInitialDate());
413         final Gradient fieldDt = Gradient.variable(freeParameters, 4, dt).subtract(Gradient.variable(freeParameters, 0, 0));
414         final FieldPVCoordinates<Gradient> terminalPV = KeplerianMotionCartesianUtility.predictPositionVelocity(fieldDt,
415                 initialPosition, initialVelocity, fieldDt.newInstance(mu));
416         // fill in intermediate Jacobian matrix
417         final RealMatrix intermediate = MatrixUtils.createRealMatrix(6, freeParameters);
418         final FieldVector3D<Gradient> terminalVelocity = terminalPV.getVelocity();
419         intermediate.setRow(0, initialVelocity.getX().getGradient());
420         intermediate.setRow(1, initialVelocity.getY().getGradient());
421         intermediate.setRow(2, initialVelocity.getZ().getGradient());
422         intermediate.setRow(3, terminalVelocity.getX().getGradient());
423         intermediate.setRow(4, terminalVelocity.getY().getGradient());
424         intermediate.setRow(5, terminalVelocity.getZ().getGradient());
425         // swap variables (position becomes dependent)
426         final RealMatrix matrixToInvert = MatrixUtils.createRealIdentityMatrix(freeParameters);
427         matrixToInvert.setRow(1, initialPosition.getX().getGradient());
428         matrixToInvert.setRow(2, initialPosition.getY().getGradient());
429         matrixToInvert.setRow(3, initialPosition.getZ().getGradient());
430         final FieldVector3D<Gradient> terminalPosition = terminalPV.getPosition();
431         matrixToInvert.setRow(5, terminalPosition.getX().getGradient());
432         matrixToInvert.setRow(6, terminalPosition.getY().getGradient());
433         matrixToInvert.setRow(7, terminalPosition.getZ().getGradient());
434         final DecompositionSolver solver = new LUDecomposition(matrixToInvert).getSolver();
435         final RealMatrix inverse = solver.getInverse();
436         return intermediate.multiply(inverse);
437     }
438 }