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.forces.maneuvers;
18  
19  import java.util.Arrays;
20  
21  import org.hipparchus.geometry.euclidean.threed.Vector3D;
22  import org.hipparchus.util.FastMath;
23  import org.orekit.frames.Frame;
24  import org.orekit.orbits.Orbit;
25  import org.orekit.orbits.OrbitType;
26  import org.orekit.orbits.PositionAngleType;
27  import org.orekit.propagation.SpacecraftState;
28  import org.orekit.propagation.analytical.AdapterPropagator;
29  import org.orekit.time.AbsoluteDate;
30  import org.orekit.utils.Constants;
31  
32  /** Analytical model for small maneuvers.
33   * <p>The aim of this model is to compute quickly the effect at date t₁
34   * of a small maneuver performed at an earlier date t₀. Both the
35   * direct effect of the maneuver and the Jacobian of this effect with respect to
36   * maneuver parameters are available.
37   * </p>
38   * <p>These effect are computed analytically using two Jacobian matrices:
39   * <ol>
40   *   <li>J₀: Jacobian of Keplerian or equinoctial elements with respect
41   *   to Cartesian parameters at date t₀ allows to compute
42   *   maneuver effect as a change in orbital elements at maneuver date t₀,</li>
43   *   <li>J<sub>1/0</sub>: Jacobian of Keplerian or equinoctial elements
44   *   at date t₁ with respect to Keplerian or equinoctial elements
45   *   at date t₀ allows to propagate the change in orbital elements
46   *   to final date t₁.</li>
47   * </ol>
48   *
49   * <p>
50   * The second Jacobian, J<sub>1/0</sub>, is computed using a simple Keplerian
51   * model, i.e. it is the identity except for the mean motion row which also includes
52   * an off-diagonal element due to semi-major axis change.
53   * </p>
54   * <p>
55   * The orbital elements change at date t₁ can be added to orbital elements
56   * extracted from state, and the final elements taking account the changes are then
57   * converted back to appropriate type, which may be different from Keplerian or
58   * equinoctial elements.
59   * </p>
60   * <p>
61   * Note that this model takes <em>only</em> Keplerian effects into account. This means
62   * that using only this class to compute an inclination maneuver in Low Earth Orbit will
63   * <em>not</em> change ascending node drift rate despite inclination has changed (the
64   * same would be true for a semi-major axis change of course). In order to take this
65   * drift into account, an instance of {@link
66   * org.orekit.propagation.analytical.J2DifferentialEffect J2DifferentialEffect}
67   * must be used together with an instance of this class.
68   * </p>
69   * @author Luc Maisonobe
70   */
71  public class SmallManeuverAnalyticalModel implements AdapterPropagator.DifferentialEffect {
72  
73      /** State at maneuver date (before maneuver occurrence). */
74      private final SpacecraftState state0;
75  
76      /** Inertial velocity increment. */
77      private final Vector3D inertialDV;
78  
79      /** Mass change ratio. */
80      private final double massRatio;
81  
82      /** Type of orbit used for internal Jacobians. */
83      private final OrbitType type;
84  
85      /** Initial Keplerian (or equinoctial) Jacobian with respect to maneuver. */
86      private final double[][] j0;
87  
88      /** Time derivative of the initial Keplerian (or equinoctial) Jacobian with respect to maneuver. */
89      private double[][] j0Dot;
90  
91      /** Mean anomaly change factor. */
92      private final double ksi;
93  
94      /** Build a maneuver defined in spacecraft frame with default orbit type.
95       * @param state0 state at maneuver date, <em>before</em> the maneuver
96       * is performed
97       * @param dV velocity increment in spacecraft frame
98       * @param isp engine specific impulse (s)
99       */
100     public SmallManeuverAnalyticalModel(final SpacecraftState state0,
101                                         final Vector3D dV, final double isp) {
102         this(state0, state0.getFrame(),
103              state0.getAttitude().getRotation().applyInverseTo(dV),
104              isp);
105     }
106 
107     /** Build a maneuver defined in spacecraft frame.
108      * @param state0 state at maneuver date, <em>before</em> the maneuver
109      * is performed
110      * @param orbitType orbit type to be used later on in Jacobian conversions
111      * @param dV velocity increment in spacecraft frame
112      * @param isp engine specific impulse (s)
113      * @since 12.1 orbit type added as input
114      */
115     public SmallManeuverAnalyticalModel(final SpacecraftState state0, final OrbitType orbitType,
116                                         final Vector3D dV, final double isp) {
117         this(state0, orbitType, state0.getFrame(),
118              state0.getAttitude().getRotation().applyInverseTo(dV),
119              isp);
120     }
121 
122     /** Build a maneuver defined in user-specified frame.
123      * @param state0 state at maneuver date, <em>before</em> the maneuver
124      * is performed
125      * @param frame frame in which velocity increment is defined
126      * @param dV velocity increment in specified frame
127      * @param isp engine specific impulse (s)
128      */
129     public SmallManeuverAnalyticalModel(final SpacecraftState state0, final Frame frame,
130                                         final Vector3D dV, final double isp) {
131         // No orbit type specified, use equinoctial orbit type if possible, Keplerian if nearly hyperbolic orbits
132         this(state0, (state0.getOrbit().getE() < 0.9) ? OrbitType.EQUINOCTIAL : OrbitType.KEPLERIAN, frame, dV, isp);
133     }
134 
135     /** Build a maneuver defined in user-specified frame.
136      * @param state0 state at maneuver date, <em>before</em> the maneuver
137      * is performed
138      * @param orbitType orbit type to be used later on in Jacobian conversions
139      * @param frame frame in which velocity increment is defined
140      * @param dV velocity increment in specified frame
141      * @param isp engine specific impulse (s)
142      * @since 12.1 orbit type added as input
143      */
144     public SmallManeuverAnalyticalModel(final SpacecraftState state0, final OrbitType orbitType,
145                                         final Frame frame, final Vector3D dV, final double isp) {
146 
147         this.state0    = state0;
148         this.massRatio = FastMath.exp(-dV.getNorm() / (Constants.G0_STANDARD_GRAVITY * isp));
149         this.type = orbitType;
150 
151         // compute initial Jacobian
152         final double[][] fullJacobian = new double[6][6];
153         j0 = new double[6][3];
154         final Orbit orbit0 = orbitType.convertType(state0.getOrbit());
155         orbit0.getJacobianWrtCartesian(PositionAngleType.MEAN, fullJacobian);
156         for (int i = 0; i < j0.length; ++i) {
157             System.arraycopy(fullJacobian[i], 3, j0[i], 0, 3);
158         }
159 
160         // use lazy evaluation for j0Dot, as it is used only when Jacobians are evaluated
161         j0Dot = null;
162 
163         // compute maneuver effect on Keplerian (or equinoctial) elements
164         inertialDV = frame.getStaticTransformTo(state0.getFrame(), state0.getDate())
165                         .transformVector(dV);
166 
167         // compute mean anomaly change: dM(t1) = dM(t0) + ksi * da * (t1 - t0)
168         final Orbit orbit = state0.getOrbit();
169         final double mu = orbit.getMu();
170         final double a  = orbit.getA();
171         ksi = -1.5 * FastMath.sqrt(mu / a) / (a * a);
172 
173     }
174 
175     /** Get the date of the maneuver.
176      * @return date of the maneuver
177      */
178     public AbsoluteDate getDate() {
179         return state0.getDate();
180     }
181 
182     /** Get the inertial velocity increment of the maneuver.
183      * @return velocity increment in a state-dependent inertial frame
184      * @see #getInertialFrame()
185      */
186     public Vector3D getInertialDV() {
187         return inertialDV;
188     }
189 
190     /** Get the inertial frame in which the velocity increment is defined.
191      * @return inertial frame in which the velocity increment is defined
192      * @see #getInertialDV()
193      */
194     public Frame getInertialFrame() {
195         return state0.getFrame();
196     }
197 
198     /** Compute the effect of the maneuver on an orbit.
199      * @param orbit1 original orbit at t₁, without maneuver
200      * @return orbit at t₁, taking the maneuver
201      * into account if t₁ &gt; t₀
202      * @see #apply(SpacecraftState)
203      * @see #getJacobian(Orbit, PositionAngleType, double[][])
204      */
205     public Orbit apply(final Orbit orbit1) {
206 
207         if (orbit1.getDate().compareTo(state0.getDate()) <= 0) {
208             // the maneuver has not occurred yet, don't change anything
209             return orbit1;
210         }
211 
212         return orbit1.getType().convertType(updateOrbit(orbit1));
213 
214     }
215 
216     /** Compute the effect of the maneuver on a spacecraft state.
217      * @param state1 original spacecraft state at t₁,
218      * without maneuver
219      * @return spacecraft state at t₁, taking the maneuver
220      * into account if t₁ &gt; t₀
221      * @see #apply(Orbit)
222      * @see #getJacobian(Orbit, PositionAngleType, double[][])
223      */
224     public SpacecraftState apply(final SpacecraftState state1) {
225 
226         if (state1.getDate().compareTo(state0.getDate()) <= 0) {
227             // the maneuver has not occurred yet, don't change anything
228             return state1;
229         }
230 
231         return new SpacecraftState(state1.getOrbit().getType().convertType(updateOrbit(state1.getOrbit())),
232                                    state1.getAttitude()).withMass(updateMass(state1.getMass()));
233 
234     }
235 
236     /** Compute the effect of the maneuver on an orbit.
237      * @param orbit1 original orbit at t₁, without maneuver
238      * @return orbit at t₁, always taking the maneuver into account, always in the internal type
239      */
240     private Orbit updateOrbit(final Orbit orbit1) {
241 
242         // compute maneuver effect
243         final double dt = orbit1.getDate().durationFrom(state0.getDate());
244         final double x  = inertialDV.getX();
245         final double y  = inertialDV.getY();
246         final double z  = inertialDV.getZ();
247         final double[] delta = new double[6];
248         for (int i = 0; i < delta.length; ++i) {
249             delta[i] = j0[i][0] * x + j0[i][1] * y + j0[i][2] * z;
250         }
251         delta[5] += ksi * delta[0] * dt;
252 
253         // convert current orbital state to Keplerian or equinoctial elements
254         final double[] parameters    = new double[6];
255         type.mapOrbitToArray(type.convertType(orbit1), PositionAngleType.MEAN, parameters, null);
256         for (int i = 0; i < delta.length; ++i) {
257             parameters[i] += delta[i];
258         }
259 
260         // build updated orbit as Keplerian or equinoctial elements
261         return type.mapArrayToOrbit(parameters, null, PositionAngleType.MEAN,
262                                     orbit1.getDate(), orbit1.getMu(), orbit1.getFrame());
263 
264     }
265 
266     /** Compute the Jacobian of the orbit with respect to maneuver parameters.
267      * <p>
268      * The Jacobian matrix is a 6x4 matrix. Element jacobian[i][j] corresponds to
269      * the partial derivative of orbital parameter i with respect to maneuver
270      * parameter j. The rows order is the same order as used in {@link
271      * Orbit#getJacobianWrtCartesian(PositionAngleType, double[][]) Orbit.getJacobianWrtCartesian}
272      * method. Columns (0, 1, 2) correspond to the velocity increment coordinates
273      * (ΔV<sub>x</sub>, ΔV<sub>y</sub>, ΔV<sub>z</sub>) in the
274      * inertial frame returned by {@link #getInertialFrame()}, and column 3
275      * corresponds to the maneuver date t₀.
276      * </p>
277      * @param orbit1 original orbit at t₁, without maneuver
278      * @param positionAngleType type of the position angle to use
279      * @param jacobian placeholder 6x4 (or larger) matrix to be filled with the Jacobian, if matrix
280      * is larger than 6x4, only the 6x4 upper left corner will be modified
281      * @see #apply(Orbit)
282      */
283     public void getJacobian(final Orbit orbit1, final PositionAngleType positionAngleType,
284                             final double[][] jacobian) {
285 
286         final double dt = orbit1.getDate().durationFrom(state0.getDate());
287         if (dt < 0) {
288             // the maneuver has not occurred yet, Jacobian is null
289             for (int i = 0; i < 6; ++i) {
290                 Arrays.fill(jacobian[i], 0, 4, 0.0);
291             }
292             return;
293         }
294 
295         // derivatives of Keplerian/equinoctial elements with respect to velocity increment
296         final double x  = inertialDV.getX();
297         final double y  = inertialDV.getY();
298         final double z  = inertialDV.getZ();
299         for (int i = 0; i < 6; ++i) {
300             System.arraycopy(j0[i], 0, jacobian[i], 0, 3);
301         }
302         for (int j = 0; j < 3; ++j) {
303             jacobian[5][j] += ksi * dt * j0[0][j];
304         }
305 
306         // derivatives of Keplerian/equinoctial elements with respect to date
307         evaluateJ0Dot();
308         for (int i = 0; i < 6; ++i) {
309             jacobian[i][3] = j0Dot[i][0] * x + j0Dot[i][1] * y + j0Dot[i][2] * z;
310         }
311         final double da = j0[0][0] * x + j0[0][1] * y + j0[0][2] * z;
312         jacobian[5][3] += ksi * (jacobian[0][3] * dt - da);
313 
314         if (orbit1.getType() != type || positionAngleType != PositionAngleType.MEAN) {
315 
316             // convert to derivatives of Cartesian parameters
317             final double[][] j2         = new double[6][6];
318             final double[][] pvJacobian = new double[6][4];
319             final Orbit updated         = updateOrbit(orbit1);
320             updated.getJacobianWrtParameters(PositionAngleType.MEAN, j2);
321             for (int i = 0; i < 6; ++i) {
322                 for (int j = 0; j < 4; ++j) {
323                     pvJacobian[i][j] = j2[i][0] * jacobian[0][j] + j2[i][1] * jacobian[1][j] +
324                                     j2[i][2] * jacobian[2][j] + j2[i][3] * jacobian[3][j] +
325                                     j2[i][4] * jacobian[4][j] + j2[i][5] * jacobian[5][j];
326                 }
327             }
328 
329             // convert to derivatives of specified parameters
330             final double[][] j3 = new double[6][6];
331             orbit1.getType().convertType(updated).getJacobianWrtCartesian(positionAngleType, j3);
332             for (int j = 0; j < 4; ++j) {
333                 for (int i = 0; i < 6; ++i) {
334                     jacobian[i][j] = j3[i][0] * pvJacobian[0][j] + j3[i][1] * pvJacobian[1][j] +
335                                     j3[i][2] * pvJacobian[2][j] + j3[i][3] * pvJacobian[3][j] +
336                                     j3[i][4] * pvJacobian[4][j] + j3[i][5] * pvJacobian[5][j];
337                 }
338             }
339 
340         }
341 
342     }
343 
344     /** Lazy evaluation of the initial Jacobian time derivative.
345      */
346     private void evaluateJ0Dot() {
347 
348         if (j0Dot == null) {
349 
350             j0Dot = new double[6][3];
351             final double dt = 1.0e-5 / state0.getOrbit().getKeplerianMeanMotion();
352             final Orbit orbit = type.convertType(state0.getOrbit());
353 
354             // compute shifted Jacobians
355             final double[][] j0m1 = new double[6][6];
356             orbit.shiftedBy(-1 * dt).getJacobianWrtCartesian(PositionAngleType.MEAN, j0m1);
357             final double[][] j0p1 = new double[6][6];
358             orbit.shiftedBy(+1 * dt).getJacobianWrtCartesian(PositionAngleType.MEAN, j0p1);
359 
360             // evaluate derivative by finite differences
361             for (int i = 0; i < j0Dot.length; ++i) {
362                 final double[] m1Row    = j0m1[i];
363                 final double[] p1Row    = j0p1[i];
364                 final double[] j0DotRow = j0Dot[i];
365                 for (int j = 0; j < 3; ++j) {
366                     j0DotRow[j] = (p1Row[j + 3] - m1Row[j + 3]) / (2 * dt);
367                 }
368             }
369 
370         }
371 
372     }
373 
374     /** Update a spacecraft mass due to maneuver.
375      * @param mass masse before maneuver
376      * @return mass after maneuver
377      */
378     public double updateMass(final double mass) {
379         return massRatio * mass;
380     }
381 
382 }