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.jacobians;
18  
19  import org.hipparchus.geometry.euclidean.threed.Vector3D;
20  import org.hipparchus.linear.MatrixUtils;
21  import org.hipparchus.linear.QRDecomposition;
22  import org.hipparchus.linear.RealMatrix;
23  import org.hipparchus.linear.RealVector;
24  import org.orekit.forces.ForceModel;
25  import org.orekit.forces.maneuvers.Maneuver;
26  import org.orekit.forces.maneuvers.trigger.ManeuverTriggersResetter;
27  import org.orekit.propagation.AdditionalDataProvider;
28  import org.orekit.propagation.SpacecraftState;
29  import org.orekit.propagation.integration.AdditionalDerivativesProvider;
30  import org.orekit.time.AbsoluteDate;
31  import org.orekit.utils.TimeSpanMap;
32  
33  /** Generator for one column of a Jacobian matrix for special case of trigger dates.
34   * <p>
35   * Typical use cases for this are estimation of maneuver start and stop date during
36   * either orbit determination or maneuver optimization.
37   * </p>
38   * <p>
39   * Let \((t_0, y_0)\) be the state at propagation start, \((t_1, y_1)\) be the state at
40   * maneuver trigger time, \((t_t, y_t)\) be the state at any arbitrary time \(t\) during
41   * propagation, and \(f_m(t, y)\) be the contribution of the maneuver to the global
42   * ODE \(\frac{dy}{dt} = f(t, y)\). We are interested in the Jacobian column
43   * \(\frac{\partial y_t}{\partial t_1}\).
44   * </p>
45   * <p>
46   * There are two parts in this Jacobian: the primary part corresponds to the full contribution
47   * of the jump in the dynamics due to the maneuver as it is delayed by a small amount \(dt_1\), whereas
48   * the secondary part corresponds to change of acceleration after maneuver start as the mass
49   * depletion is delayed and therefore the spacecraft mass is different from the mass for nominal
50   * start time.
51   * This second part is already contained in the first one when the mass is included in the transition matrix
52   * (7x7 instead of 6x6).
53   * </p>
54   * <p>
55   * The primary part is computed as follows. After trigger time \(t_1\) (according to propagation direction),
56   * \[\frac{\partial y_t}{\partial t_1} = \pm \frac{\partial y_t}{\partial y_1} f_m(t_1, y_1)\]
57   * where the sign depends on \(t_1\) being a start or stop trigger and propagation being forward
58   * or backward.
59   * </p>
60   * <p>
61   * We don't have \(\frac{\partial y_t}{\partial y_1}\) available if \(t_1 \neq t_0\), but we
62   * have \(\frac{\partial y_t}{\partial y_0}\) at any time since it can be computed by integrating
63   * variational equations for numerical propagation or by other closed form expressions for analytical
64   * propagators. We use the classical composition rule to recover the state transition matrix with
65   * respect to intermediate time \(t_1\):
66   * \[\frac{\partial y_t}{\partial y_0} = \frac{\partial y_t}{\partial y_1} \frac{\partial y_1}{\partial y_0}\]
67   * We deduce
68   * \[\frac{\partial y_t}{\partial y_1} = \frac{\partial y_t}{\partial y_0} \left(\frac{\partial y_1}{\partial y_0}\right)^{-1}\]
69   * </p>
70   * <p>
71   * The contribution of the primary part to the Jacobian column can therefore be computed using the following
72   * closed-form expression:
73   * \[\frac{\partial y_t}{\partial t_1}
74   * = \pm \frac{\partial y_t}{\partial y_0} \left(\frac{\partial y_1}{\partial y_0}\right)^{-1} f_m(t_1, y_1)
75   * = \frac{\partial y_t}{\partial y_0} c_1\]
76   * where \(c_1\) is the signed contribution of maneuver at \(t_1\) and is computed at trigger time
77   * by solving \(\frac{\partial y_1}{\partial y_0} c_1 = \pm f_m(t_1, y_1)\).
78   * </p>
79   * <p>
80   * As the primary part of the column is generated using a closed-form expression, this generator
81   * implements the {@link AdditionalDataProvider} interface and stores the column directly
82   * in the primary state during propagation.
83   * </p>
84   * <p>
85   * As the closed-form expression requires picking \(c_1\) at trigger time \(t_1\), it works only
86   * if propagation starts outside of the maneuver and passes over \(t_1\) during integration.
87   * </p>
88   * <p>
89   * The secondary part, if needed (as it is not required if the mass is already included the state transition matrix
90   * i.e. when the latter is 7x7), is computed as follows. Let m be the mass and m_s its value at switching time t_s.
91   * Let (x,y,z) be the position vector, (vx, vy, vz) the velocity
92   * and (ax, ay, az) the total acceleration, we have \(\dot \frac{\partial x} {\partial \partial m_s} = \frac{\partial vx }{\partial m_s}))
93   * and similar expressions for y and z. Furthermore, \(\dot \frac{\partial vx}{ \partial \partial m_s} = \frac{\partial ax }{\partial m}
94   * . \frac{\partial m }{\partial m_s} \), and symmetric equations for vy and vy. The fact is that \( \frac{\partial m}{ \partial m_s} = 1 \)
95   * assuming the mass rate q only depends on time. On the other hand, \( \frac{\partial m_s}{ \partial t_s }= q(t_s) \)/
96   * By the chain rule of derivation, one gets the contribution due to the mass depletion delay.
97   * </p>
98   * <p>
99   * The contribution of the secondary part to the Jacobian column can therefore be computed by integrating
100  * the partial derivative of the acceleration, to get the partial derivative of the position.
101  * </p>
102  * <p>
103  * As the secondary part of the column is generated using a differential equation, a separate
104  * underlying generator implementing the {@link AdditionalDerivativesProvider} interface is set up to
105  * perform the integration during propagation.
106  * </p>
107  * <p>
108  * This generator takes care to sum up the primary and secondary parts so the full column of the Jacobian
109  * is computed.
110  * </p>
111  * <p>
112  * The implementation takes care to <em>not</em> resetting \(c_1\) at propagation start.
113  * This allows to get proper Jacobian if we interrupt propagation in the middle of a maneuver
114  * and restart propagation where it left.
115  * </p>
116  * @author Luc Maisonobe
117  * @since 11.1
118  * @see MedianDate
119  * @see Duration
120  */
121 public class TriggerDate implements ManeuverTriggersResetter, AdditionalDataProvider<double[]> {
122 
123     /** Threshold for decomposing state transition matrix at trigger time. */
124     private static final double DECOMPOSITION_THRESHOLD = 1.0e-10;
125 
126     /** Name of the state for State Transition Matrix. */
127     private final String stmName;
128 
129     /** Name of the parameter corresponding to the column. */
130     private final String triggerName;
131 
132     /** Mass depletion effect. */
133     private final MassDepletionDelay massDepletionDelay;
134 
135     /** Start/stop management flag. */
136     private final boolean manageStart;
137 
138     /** Maneuver force model. */
139     private final Maneuver maneuver;
140 
141     /** Event detector threshold. */
142     private final double threshold;
143 
144     /** State dimension. */
145     private final int stateDimension;
146 
147     /** Signed contribution of maneuver at trigger time ±(∂y₁/∂y₀)⁻¹ fₘ(t₁, y₁). */
148     private TimeSpanMap<double[]> contribution;
149 
150     /** Trigger date. */
151     private AbsoluteDate trigger;
152 
153     /** Indicator for forward propagation. */
154     private boolean forward;
155 
156     /** Mass rate at trigger (sign depends on propagation direction). Set to zero until the maneuver has actually happened during a propagation. */
157     private double signedMassRateAtTrigger = 0.;
158 
159     /** Constructor without mass as state variable in transition matrix.
160      * @param stmName name of State Transition Matrix state
161      * @param triggerName name of the parameter corresponding to the trigger date column
162      * @param manageStart if true, we compute derivatives with respect to maneuver start
163      * @param maneuver maneuver force model
164      * @param threshold event detector threshold
165      * @param nonGravitationalForces list of non-gravitational forces, used only if mass is not in STM
166      */
167     public TriggerDate(final String stmName, final String triggerName, final boolean manageStart,
168                        final Maneuver maneuver, final double threshold, final ForceModel... nonGravitationalForces) {
169         this(stmName, triggerName, manageStart, maneuver, threshold, false, nonGravitationalForces);
170     }
171 
172     /** Constructor.
173      * @param stmName name of State Transition Matrix state
174      * @param triggerName name of the parameter corresponding to the trigger date column
175      * @param manageStart if true, we compute derivatives with respect to maneuver start
176      * @param maneuver maneuver force model
177      * @param threshold event detector threshold
178      * @param isMassInStm flag on mass inclusion as state variable in STM
179      * @param nonGravitationalForces list of non-gravitational forces, used only if mass is not in STM
180      * @since 13.1
181      */
182     public TriggerDate(final String stmName, final String triggerName, final boolean manageStart,
183                        final Maneuver maneuver, final double threshold, final boolean isMassInStm,
184                        final ForceModel... nonGravitationalForces) {
185         this.stmName            = stmName;
186         this.triggerName        = triggerName;
187         this.massDepletionDelay = isMassInStm ? null : new MassDepletionDelay(triggerName, manageStart, maneuver, nonGravitationalForces);
188         this.manageStart        = manageStart;
189         this.maneuver           = maneuver;
190         this.threshold          = threshold;
191         this.stateDimension     = isMassInStm ? 7 : 6;
192         this.contribution       = null;
193         this.trigger            = null;
194         this.forward            = true;
195     }
196 
197     /** {@inheritDoc} */
198     @Override
199     public String getName() {
200         return triggerName;
201     }
202 
203     /** {@inheritDoc}
204      * <p>
205      * The column state can be computed only if the State Transition Matrix state is available.
206      * </p>
207      */
208     @Override
209     public boolean yields(final SpacecraftState state) {
210         if (massDepletionDelay == null) {
211             return !state.hasAdditionalData(stmName);
212         } else {
213             return !(state.hasAdditionalData(stmName) && state.hasAdditionalData(massDepletionDelay.getName()));
214         }
215     }
216 
217     /** Get the mass depletion effect processor. Can be null.
218      * @return mass depletion effect processor
219      */
220     public MassDepletionDelay getMassDepletionDelay() {
221         return massDepletionDelay;
222     }
223 
224     /** {@inheritDoc} */
225     @Override
226     public void init(final SpacecraftState initialState, final AbsoluteDate target) {
227 
228         // note that we reset contribution or triggered ONLY at start or if we change
229         // propagation direction
230         // this allows to get proper Jacobian if we interrupt propagation
231         // in the middle of a maneuver and restart propagation where it left
232         final boolean newForward = target.isAfterOrEqualTo(initialState);
233         if (contribution == null || (forward ^ newForward)) {
234             contribution = new TimeSpanMap<>(null);
235             trigger      = null;
236             signedMassRateAtTrigger = 0.;
237         }
238 
239         forward = newForward;
240 
241     }
242 
243     /** {@inheritDoc} */
244     @Override
245     public double[] getAdditionalData(final SpacecraftState state) {
246         // we check contribution rather than triggered because this method
247         // is called after maneuverTriggered and before resetState,
248         // when preparing the old state to be reset
249         final double[] c = contribution == null ? null : contribution.get(state.getDate());
250         if (c == null) {
251             // no thrust, no effect
252             return new double[stateDimension];
253         } else {
254 
255             // primary effect: full maneuver contribution at (delayed) trigger date
256             final double[] effect = getStm(state).operate(c);
257 
258             if (massDepletionDelay != null) {
259                 // secondary effect: maneuver change throughout thrust as mass depletion is delayed (only needed when mass is not in the STM)
260                 final double[] secondary = state.getAdditionalState(massDepletionDelay.getName());
261 
262                 // cumulate both effects
263                 for (int i = 0; i < effect.length; ++i) {
264                     effect[i] += secondary[i] * signedMassRateAtTrigger;
265                 }
266             }
267 
268             return effect;
269 
270         }
271     }
272 
273     /** {@inheritDoc}*/
274     @Override
275     public void maneuverTriggered(final SpacecraftState state, final boolean start) {
276         trigger = (start == manageStart) ? state.getDate() : null;
277     }
278 
279     /** {@inheritDoc}*/
280     @Override
281     public SpacecraftState resetState(final SpacecraftState state) {
282 
283         if (trigger == null) {
284             // this is not the maneuver trigger we expected (start vs. stop)
285             return state;
286         }
287 
288         // get the acceleration near trigger time
289         final double[] parameters = maneuver.getParameters(state.getDate());
290         final SpacecraftState stateWhenFiring = state.shiftedBy((manageStart ? 2 : -2) * threshold);
291         final Vector3D        acceleration    = maneuver.acceleration(stateWhenFiring, parameters);
292 
293         // initialize derivatives computation
294         final double     sign = (forward == manageStart) ? -1 : +1;
295         final RealVector rhs  = MatrixUtils.createRealVector(stateDimension);
296         rhs.setEntry(3, sign * acceleration.getX());
297         rhs.setEntry(4, sign * acceleration.getY());
298         rhs.setEntry(5, sign * acceleration.getZ());
299         signedMassRateAtTrigger = sign * maneuver.getPropulsionModel().getMassDerivatives(state, parameters);
300         if (stateDimension == 7) {
301             rhs.setEntry(6, signedMassRateAtTrigger);
302         }
303 
304         // get State Transition Matrix with respect to Cartesian parameters at trigger time
305         final RealMatrix dY1dY0 = getStm(state);
306 
307         // store contribution factor for derivatives scm = ±(∂y₁/∂y₀)⁻¹ fₘ(t₁, y₁)
308         final double[] c = new QRDecomposition(dY1dY0, DECOMPOSITION_THRESHOLD).getSolver().solve(rhs).toArray();
309         if (forward) {
310             contribution.addValidAfter(c, state.getDate(), false);
311         } else {
312             contribution.addValidBefore(c, state.getDate(), false);
313         }
314 
315         // return unchanged state
316         return state;
317 
318     }
319 
320     /** Extract State Transition Matrix with respect to Cartesian parameters.
321      * @param state state containing the State Transition Matrix
322      * @return State Transition Matrix
323      */
324     private RealMatrix getStm(final SpacecraftState state) {
325         final double[] p = state.getAdditionalState(stmName);
326         final RealMatrix dYdY0 = MatrixUtils.createRealMatrix(stateDimension, stateDimension);
327         int index = 0;
328         for (int i = 0; i < stateDimension; ++i) {
329             for (int j = 0; j < stateDimension; ++j) {
330                 dYdY0.setEntry(i, j, p[index++]);
331             }
332         }
333         return dYdY0;
334     }
335 
336 }
337