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.maneuvers.Maneuver;
25  import org.orekit.forces.maneuvers.trigger.ManeuverTriggersResetter;
26  import org.orekit.propagation.AdditionalDataProvider;
27  import org.orekit.propagation.SpacecraftState;
28  import org.orekit.propagation.integration.AdditionalDerivativesProvider;
29  import org.orekit.time.AbsoluteDate;
30  import org.orekit.utils.TimeSpanMap;
31  
32  /** Generator for one column of a Jacobian matrix for special case of trigger dates.
33   * <p>
34   * Typical use cases for this are estimation of maneuver start and stop date during
35   * either orbit determination or maneuver optimization.
36   * </p>
37   * <p>
38   * Let \((t_0, y_0)\) be the state at propagation start, \((t_1, y_1)\) be the state at
39   * maneuver trigger time, \((t_t, y_t)\) be the state at any arbitrary time \(t\) during
40   * propagation, and \(f_m(t, y)\) be the contribution of the maneuver to the global
41   * ODE \(\frac{dy}{dt} = f(t, y)\). We are interested in the Jacobian column
42   * \(\frac{\partial y_t}{\partial t_1}\).
43   * </p>
44   * <p>
45   * There are two parts in this Jacobian: the primary part corresponds to the full contribution
46   * of the acceleration due to the maneuver as it is delayed by a small amount \(dt_1\), whereas
47   * the secondary part corresponds to change of acceleration after maneuver start as the mass
48   * depletion is delayed and therefore the spacecraft mass is different from the mass for nominal
49   * start time.
50   * </p>
51   * <p>
52   * The primary part is computed as follows. After trigger time \(t_1\) (according to propagation direction),
53   * \[\frac{\partial y_t}{\partial t_1} = \pm \frac{\partial y_t}{\partial y_1} f_m(t_1, y_1)\]
54   * where the sign depends on \(t_1\) being a start or stop trigger and propagation being forward
55   * or backward.
56   * </p>
57   * <p>
58   * We don't have \(\frac{\partial y_t}{\partial y_1}\) available if \(t_1 \neq t_0\), but we
59   * have \(\frac{\partial y_t}{\partial y_0}\) at any time since it can be computed by integrating
60   * variational equations for numerical propagation or by other closed form expressions for analytical
61   * propagators. We use the classical composition rule to recover the state transition matrix with
62   * respect to intermediate time \(t_1\):
63   * \[\frac{\partial y_t}{\partial y_0} = \frac{\partial y_t}{\partial y_1} \frac{\partial y_1}{\partial y_0}\]
64   * We deduce
65   * \[\frac{\partial y_t}{\partial y_1} = \frac{\partial y_t}{\partial y_0} \left(\frac{\partial y_1}{\partial y_0}\right)^{-1}\]
66   * </p>
67   * <p>
68   * The contribution of the primary part to the Jacobian column can therefore be computed using the following
69   * closed-form expression:
70   * \[\frac{\partial y_t}{\partial t_1}
71   * = \pm \frac{\partial y_t}{\partial y_0} \left(\frac{\partial y_1}{\partial y_0}\right)^{-1} f_m(t_1, y_1)
72   * = \frac{\partial y_t}{\partial y_0} c_1\]
73   * where \(c_1\) is the signed contribution of maneuver at \(t_1\) and is computed at trigger time
74   * by solving \(\frac{\partial y_1}{\partial y_0} c_1 = \pm f_m(t_1, y_1)\).
75   * </p>
76   * <p>
77   * As the primary part of the column is generated using a closed-form expression, this generator
78   * implements the {@link AdditionalDataProvider} interface and stores the column directly
79   * in the primary state during propagation.
80   * </p>
81   * <p>
82   * As the closed-form expression requires picking \(c_1\) at trigger time \(t_1\), it works only
83   * if propagation starts outside of the maneuver and passes over \(t_1\) during integration.
84   * </p>
85   * <p>
86   * The secondary part is computed as follows. We have acceleration \(\vec{\Gamma} = \frac{\vec{F}}{m}\) and
87   * \(m = m_0 - q (t - t_s)\), where \(m\) is current mass, \(m_0\) is initial mass and \(t_s\) is
88   * maneuver trigger time. A delay \(dt_s\) on trigger time induces delaying mass depletion.
89   * We get:
90   * \[d\vec{\Gamma} = \frac{-\vec{F}}{m^2} dm = \frac{-\vec{F}}{m^2} q dt_s = -\vec{\Gamma}\frac{q}{m} dt_s\]
91   * From this total differential, we extract the partial derivative of the acceleration
92   * \[\frac{\partial\vec{\Gamma}}{\partial t_s} = -\vec{\Gamma}\frac{q}{m}\]
93   * </p>
94   * <p>
95   * The contribution of the secondary part to the Jacobian column can therefore be computed by integrating
96   * the partial derivative of the acceleration, to get the partial derivative of the position.
97   * </p>
98   * <p>
99   * As the secondary part of the column is generated using a differential equation, a separate
100  * underlying generator implementing the {@link AdditionalDerivativesProvider} interface is set up to
101  * perform the integration during propagation.
102  * </p>
103  * <p>
104  * This generator takes care to sum up the primary and secondary parts so the full column of the Jacobian
105  * is computed.
106  * </p>
107  * <p>
108  * The implementation takes care to <em>not</em> resetting \(c_1\) at propagation start.
109  * This allows to get proper Jacobian if we interrupt propagation in the middle of a maneuver
110  * and restart propagation where it left.
111  * </p>
112  * @author Luc Maisonobe
113  * @since 11.1
114  * @see MedianDate
115  * @see Duration
116  */
117 public class TriggerDate
118     implements ManeuverTriggersResetter, AdditionalDataProvider<double[]> {
119 
120     /** Dimension of the state. */
121     private static final int STATE_DIMENSION = 6;
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     /** Signed contribution of maneuver at trigger time ±(∂y₁/∂y₀)⁻¹ fₘ(t₁, y₁). */
145     private TimeSpanMap<double[]> contribution;
146 
147     /** Trigger date. */
148     private AbsoluteDate trigger;
149 
150     /** Indicator for forward propagation. */
151     private boolean forward;
152 
153     /** Simple constructor.
154      * @param stmName name of State Transition Matrix state
155      * @param triggerName name of the parameter corresponding to the trigger date column
156      * @param manageStart if true, we compute derivatives with respect to maneuver start
157      * @param maneuver maneuver force model
158      * @param threshold event detector threshold
159      */
160     public TriggerDate(final String stmName, final String triggerName, final boolean manageStart,
161                        final Maneuver maneuver, final double threshold) {
162         this.stmName            = stmName;
163         this.triggerName        = triggerName;
164         this.massDepletionDelay = new MassDepletionDelay(triggerName, manageStart, maneuver);
165         this.manageStart        = manageStart;
166         this.maneuver           = maneuver;
167         this.threshold          = threshold;
168         this.contribution       = null;
169         this.trigger            = null;
170         this.forward            = true;
171     }
172 
173     /** {@inheritDoc} */
174     @Override
175     public String getName() {
176         return triggerName;
177     }
178 
179     /** {@inheritDoc}
180      * <p>
181      * The column state can be computed only if the State Transition Matrix state is available.
182      * </p>
183      */
184     @Override
185     public boolean yields(final SpacecraftState state) {
186         return !(state.hasAdditionalData(stmName) && state.hasAdditionalData(massDepletionDelay.getName()));
187     }
188 
189     /** Get the mass depletion effect processor.
190      * @return mass depletion effect processor
191      */
192     public MassDepletionDelay getMassDepletionDelay() {
193         return massDepletionDelay;
194     }
195 
196     /** {@inheritDoc} */
197     @Override
198     public void init(final SpacecraftState initialState, final AbsoluteDate target) {
199 
200         // note that we reset contribution or triggered ONLY at start or if we change
201         // propagation direction
202         // this allows to get proper Jacobian if we interrupt propagation
203         // in the middle of a maneuver and restart propagation where it left
204         final boolean newForward = target.isAfterOrEqualTo(initialState);
205         if (contribution == null || (forward ^ newForward)) {
206             contribution = new TimeSpanMap<>(null);
207             trigger      = null;
208         }
209 
210         forward = newForward;
211 
212     }
213 
214     /** {@inheritDoc} */
215     @Override
216     public double[] getAdditionalData(final SpacecraftState state) {
217         // we check contribution rather than triggered because this method
218         // is called after maneuverTriggered and before resetState,
219         // when preparing the old state to be reset
220         final double[] c = contribution == null ? null : contribution.get(state.getDate());
221         if (c == null) {
222             // no thrust, no effect
223             return new double[STATE_DIMENSION];
224         } else {
225 
226             // primary effect: full maneuver contribution at (delayed) trigger date
227             final double[] effect = getStm(state).operate(c);
228 
229             // secondary effect: maneuver change throughout thrust as mass depletion is delayed
230             final double[] secondary = state.getAdditionalState(massDepletionDelay.getName());
231 
232             // sum up both effects
233             for (int i = 0; i < effect.length; ++i) {
234                 effect[i] += secondary[i];
235             }
236 
237             return effect;
238 
239         }
240     }
241 
242     /** {@inheritDoc}*/
243     @Override
244     public void maneuverTriggered(final SpacecraftState state, final boolean start) {
245         trigger = (start == manageStart) ? state.getDate() : null;
246     }
247 
248     /** {@inheritDoc}*/
249     @Override
250     public SpacecraftState resetState(final SpacecraftState state) {
251 
252         if (trigger == null) {
253             // this is not the maneuver trigger we expected (start vs. stop)
254             return state;
255         }
256 
257         // get the acceleration near trigger time
258         final SpacecraftState stateWhenFiring = state.shiftedBy((manageStart ? 2 : -2) * threshold);
259         final Vector3D        acceleration    = maneuver.acceleration(stateWhenFiring, maneuver.getParameters(state.getDate()));
260 
261         // initialize derivatives computation
262         final double     sign = (forward == manageStart) ? -1 : +1;
263         final RealVector rhs  = MatrixUtils.createRealVector(STATE_DIMENSION);
264         rhs.setEntry(3, sign * acceleration.getX());
265         rhs.setEntry(4, sign * acceleration.getY());
266         rhs.setEntry(5, sign * acceleration.getZ());
267 
268         // get State Transition Matrix with respect to Cartesian parameters at trigger time
269         final RealMatrix dY1dY0 = getStm(state);
270 
271         // store contribution factor for derivatives scm = ±(∂y₁/∂y₀)⁻¹ fₘ(t₁, y₁)
272         final double[] c = new QRDecomposition(dY1dY0, DECOMPOSITION_THRESHOLD).getSolver().solve(rhs).toArray();
273         if (forward) {
274             contribution.addValidAfter(c, state.getDate(), false);
275         } else {
276             contribution.addValidBefore(c, state.getDate(), false);
277         }
278 
279         // return unchanged state
280         return state;
281 
282     }
283 
284     /** Extract State Transition Matrix with respect to Cartesian parameters.
285      * @param state state containing the State Transition Matrix
286      * @return State Transition Matrix
287      */
288     private RealMatrix getStm(final SpacecraftState state) {
289         final double[] p = state.getAdditionalState(stmName);
290         final RealMatrix dYdY0 = MatrixUtils.createRealMatrix(STATE_DIMENSION, STATE_DIMENSION);
291         int index = 0;
292         for (int i = 0; i < STATE_DIMENSION; ++i) {
293             for (int j = 0; j < STATE_DIMENSION; ++j) {
294                 dYdY0.setEntry(i, j, p[index++]);
295             }
296         }
297         return dYdY0;
298     }
299 
300 }
301