TriggerDate.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.forces.maneuvers.jacobians;

  18. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  19. import org.hipparchus.linear.MatrixUtils;
  20. import org.hipparchus.linear.QRDecomposition;
  21. import org.hipparchus.linear.RealMatrix;
  22. import org.hipparchus.linear.RealVector;
  23. import org.orekit.forces.ForceModel;
  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. /** Generator for one column of a Jacobian matrix for special case of trigger dates.
  32.  * <p>
  33.  * Typical use cases for this are estimation of maneuver start and stop date during
  34.  * either orbit determination or maneuver optimization.
  35.  * </p>
  36.  * <p>
  37.  * Let \((t_0, y_0)\) be the state at propagation start, \((t_1, y_1)\) be the state at
  38.  * maneuver trigger time, \((t_t, y_t)\) be the state at any arbitrary time \(t\) during
  39.  * propagation, and \(f_m(t, y)\) be the contribution of the maneuver to the global
  40.  * ODE \(\frac{dy}{dt} = f(t, y)\). We are interested in the Jacobian column
  41.  * \(\frac{\partial y_t}{\partial t_1}\).
  42.  * </p>
  43.  * <p>
  44.  * There are two parts in this Jacobian: the primary part corresponds to the full contribution
  45.  * of the jump in the dynamics due to the maneuver as it is delayed by a small amount \(dt_1\), whereas
  46.  * the secondary part corresponds to change of acceleration after maneuver start as the mass
  47.  * depletion is delayed and therefore the spacecraft mass is different from the mass for nominal
  48.  * start time.
  49.  * This second part is already contained in the first one when the mass is included in the transition matrix
  50.  * (7x7 instead of 6x6).
  51.  * </p>
  52.  * <p>
  53.  * The primary part is computed as follows. After trigger time \(t_1\) (according to propagation direction),
  54.  * \[\frac{\partial y_t}{\partial t_1} = \pm \frac{\partial y_t}{\partial y_1} f_m(t_1, y_1)\]
  55.  * where the sign depends on \(t_1\) being a start or stop trigger and propagation being forward
  56.  * or backward.
  57.  * </p>
  58.  * <p>
  59.  * We don't have \(\frac{\partial y_t}{\partial y_1}\) available if \(t_1 \neq t_0\), but we
  60.  * have \(\frac{\partial y_t}{\partial y_0}\) at any time since it can be computed by integrating
  61.  * variational equations for numerical propagation or by other closed form expressions for analytical
  62.  * propagators. We use the classical composition rule to recover the state transition matrix with
  63.  * respect to intermediate time \(t_1\):
  64.  * \[\frac{\partial y_t}{\partial y_0} = \frac{\partial y_t}{\partial y_1} \frac{\partial y_1}{\partial y_0}\]
  65.  * We deduce
  66.  * \[\frac{\partial y_t}{\partial y_1} = \frac{\partial y_t}{\partial y_0} \left(\frac{\partial y_1}{\partial y_0}\right)^{-1}\]
  67.  * </p>
  68.  * <p>
  69.  * The contribution of the primary part to the Jacobian column can therefore be computed using the following
  70.  * closed-form expression:
  71.  * \[\frac{\partial y_t}{\partial t_1}
  72.  * = \pm \frac{\partial y_t}{\partial y_0} \left(\frac{\partial y_1}{\partial y_0}\right)^{-1} f_m(t_1, y_1)
  73.  * = \frac{\partial y_t}{\partial y_0} c_1\]
  74.  * where \(c_1\) is the signed contribution of maneuver at \(t_1\) and is computed at trigger time
  75.  * by solving \(\frac{\partial y_1}{\partial y_0} c_1 = \pm f_m(t_1, y_1)\).
  76.  * </p>
  77.  * <p>
  78.  * As the primary part of the column is generated using a closed-form expression, this generator
  79.  * implements the {@link AdditionalDataProvider} interface and stores the column directly
  80.  * in the primary state during propagation.
  81.  * </p>
  82.  * <p>
  83.  * As the closed-form expression requires picking \(c_1\) at trigger time \(t_1\), it works only
  84.  * if propagation starts outside of the maneuver and passes over \(t_1\) during integration.
  85.  * </p>
  86.  * <p>
  87.  * The secondary part, if needed (as it is not required if the mass is already included the state transition matrix
  88.  * 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.
  89.  * Let (x,y,z) be the position vector, (vx, vy, vz) the velocity
  90.  * and (ax, ay, az) the total acceleration, we have \(\dot \frac{\partial x} {\partial \partial m_s} = \frac{\partial vx }{\partial m_s}))
  91.  * and similar expressions for y and z. Furthermore, \(\dot \frac{\partial vx}{ \partial \partial m_s} = \frac{\partial ax }{\partial m}
  92.  * . \frac{\partial m }{\partial m_s} \), and symmetric equations for vy and vy. The fact is that \( \frac{\partial m}{ \partial m_s} = 1 \)
  93.  * assuming the mass rate q only depends on time. On the other hand, \( \frac{\partial m_s}{ \partial t_s }= q(t_s) \)/
  94.  * By the chain rule of derivation, one gets the contribution due to the mass depletion delay.
  95.  * </p>
  96.  * <p>
  97.  * The contribution of the secondary part to the Jacobian column can therefore be computed by integrating
  98.  * the partial derivative of the acceleration, to get the partial derivative of the position.
  99.  * </p>
  100.  * <p>
  101.  * As the secondary part of the column is generated using a differential equation, a separate
  102.  * underlying generator implementing the {@link AdditionalDerivativesProvider} interface is set up to
  103.  * perform the integration during propagation.
  104.  * </p>
  105.  * <p>
  106.  * This generator takes care to sum up the primary and secondary parts so the full column of the Jacobian
  107.  * is computed.
  108.  * </p>
  109.  * <p>
  110.  * The implementation takes care to <em>not</em> resetting \(c_1\) at propagation start.
  111.  * This allows to get proper Jacobian if we interrupt propagation in the middle of a maneuver
  112.  * and restart propagation where it left.
  113.  * </p>
  114.  * @author Luc Maisonobe
  115.  * @since 11.1
  116.  * @see MedianDate
  117.  * @see Duration
  118.  */
  119. public class TriggerDate implements ManeuverTriggersResetter, AdditionalDataProvider<double[]> {

  120.     /** Threshold for decomposing state transition matrix at trigger time. */
  121.     private static final double DECOMPOSITION_THRESHOLD = 1.0e-10;

  122.     /** Name of the state for State Transition Matrix. */
  123.     private final String stmName;

  124.     /** Name of the parameter corresponding to the column. */
  125.     private final String triggerName;

  126.     /** Mass depletion effect. */
  127.     private final MassDepletionDelay massDepletionDelay;

  128.     /** Start/stop management flag. */
  129.     private final boolean manageStart;

  130.     /** Maneuver force model. */
  131.     private final Maneuver maneuver;

  132.     /** Event detector threshold. */
  133.     private final double threshold;

  134.     /** State dimension. */
  135.     private final int stateDimension;

  136.     /** Signed contribution of maneuver at trigger time ±(∂y₁/∂y₀)⁻¹ fₘ(t₁, y₁). */
  137.     private TimeSpanMap<double[]> contribution;

  138.     /** Trigger date. */
  139.     private AbsoluteDate trigger;

  140.     /** Indicator for forward propagation. */
  141.     private boolean forward;

  142.     /** Mass rate at trigger (sign depends on propagation direction). Set to zero until the maneuver has actually happened during a propagation. */
  143.     private double signedMassRateAtTrigger = 0.;

  144.     /** Constructor without mass as state variable in transition matrix.
  145.      * @param stmName name of State Transition Matrix state
  146.      * @param triggerName name of the parameter corresponding to the trigger date column
  147.      * @param manageStart if true, we compute derivatives with respect to maneuver start
  148.      * @param maneuver maneuver force model
  149.      * @param threshold event detector threshold
  150.      * @param nonGravitationalForces list of non-gravitational forces, used only if mass is not in STM
  151.      */
  152.     public TriggerDate(final String stmName, final String triggerName, final boolean manageStart,
  153.                        final Maneuver maneuver, final double threshold, final ForceModel... nonGravitationalForces) {
  154.         this(stmName, triggerName, manageStart, maneuver, threshold, false, nonGravitationalForces);
  155.     }

  156.     /** Constructor.
  157.      * @param stmName name of State Transition Matrix state
  158.      * @param triggerName name of the parameter corresponding to the trigger date column
  159.      * @param manageStart if true, we compute derivatives with respect to maneuver start
  160.      * @param maneuver maneuver force model
  161.      * @param threshold event detector threshold
  162.      * @param isMassInStm flag on mass inclusion as state variable in STM
  163.      * @param nonGravitationalForces list of non-gravitational forces, used only if mass is not in STM
  164.      * @since 13.1
  165.      */
  166.     public TriggerDate(final String stmName, final String triggerName, final boolean manageStart,
  167.                        final Maneuver maneuver, final double threshold, final boolean isMassInStm,
  168.                        final ForceModel... nonGravitationalForces) {
  169.         this.stmName            = stmName;
  170.         this.triggerName        = triggerName;
  171.         this.massDepletionDelay = isMassInStm ? null : new MassDepletionDelay(triggerName, manageStart, maneuver, nonGravitationalForces);
  172.         this.manageStart        = manageStart;
  173.         this.maneuver           = maneuver;
  174.         this.threshold          = threshold;
  175.         this.stateDimension     = isMassInStm ? 7 : 6;
  176.         this.contribution       = null;
  177.         this.trigger            = null;
  178.         this.forward            = true;
  179.     }

  180.     /** {@inheritDoc} */
  181.     @Override
  182.     public String getName() {
  183.         return triggerName;
  184.     }

  185.     /** {@inheritDoc}
  186.      * <p>
  187.      * The column state can be computed only if the State Transition Matrix state is available.
  188.      * </p>
  189.      */
  190.     @Override
  191.     public boolean yields(final SpacecraftState state) {
  192.         if (massDepletionDelay == null) {
  193.             return !state.hasAdditionalData(stmName);
  194.         } else {
  195.             return !(state.hasAdditionalData(stmName) && state.hasAdditionalData(massDepletionDelay.getName()));
  196.         }
  197.     }

  198.     /** Get the mass depletion effect processor. Can be null.
  199.      * @return mass depletion effect processor
  200.      */
  201.     public MassDepletionDelay getMassDepletionDelay() {
  202.         return massDepletionDelay;
  203.     }

  204.     /** {@inheritDoc} */
  205.     @Override
  206.     public void init(final SpacecraftState initialState, final AbsoluteDate target) {

  207.         // note that we reset contribution or triggered ONLY at start or if we change
  208.         // propagation direction
  209.         // this allows to get proper Jacobian if we interrupt propagation
  210.         // in the middle of a maneuver and restart propagation where it left
  211.         final boolean newForward = target.isAfterOrEqualTo(initialState);
  212.         if (contribution == null || (forward ^ newForward)) {
  213.             contribution = new TimeSpanMap<>(null);
  214.             trigger      = null;
  215.             signedMassRateAtTrigger = 0.;
  216.         }

  217.         forward = newForward;

  218.     }

  219.     /** {@inheritDoc} */
  220.     @Override
  221.     public double[] getAdditionalData(final SpacecraftState state) {
  222.         // we check contribution rather than triggered because this method
  223.         // is called after maneuverTriggered and before resetState,
  224.         // when preparing the old state to be reset
  225.         final double[] c = contribution == null ? null : contribution.get(state.getDate());
  226.         if (c == null) {
  227.             // no thrust, no effect
  228.             return new double[stateDimension];
  229.         } else {

  230.             // primary effect: full maneuver contribution at (delayed) trigger date
  231.             final double[] effect = getStm(state).operate(c);

  232.             if (massDepletionDelay != null) {
  233.                 // secondary effect: maneuver change throughout thrust as mass depletion is delayed (only needed when mass is not in the STM)
  234.                 final double[] secondary = state.getAdditionalState(massDepletionDelay.getName());

  235.                 // cumulate both effects
  236.                 for (int i = 0; i < effect.length; ++i) {
  237.                     effect[i] += secondary[i] * signedMassRateAtTrigger;
  238.                 }
  239.             }

  240.             return effect;

  241.         }
  242.     }

  243.     /** {@inheritDoc}*/
  244.     @Override
  245.     public void maneuverTriggered(final SpacecraftState state, final boolean start) {
  246.         trigger = (start == manageStart) ? state.getDate() : null;
  247.     }

  248.     /** {@inheritDoc}*/
  249.     @Override
  250.     public SpacecraftState resetState(final SpacecraftState state) {

  251.         if (trigger == null) {
  252.             // this is not the maneuver trigger we expected (start vs. stop)
  253.             return state;
  254.         }

  255.         // get the acceleration near trigger time
  256.         final double[] parameters = maneuver.getParameters(state.getDate());
  257.         final SpacecraftState stateWhenFiring = state.shiftedBy((manageStart ? 2 : -2) * threshold);
  258.         final Vector3D        acceleration    = maneuver.acceleration(stateWhenFiring, parameters);

  259.         // initialize derivatives computation
  260.         final double     sign = (forward == manageStart) ? -1 : +1;
  261.         final RealVector rhs  = MatrixUtils.createRealVector(stateDimension);
  262.         rhs.setEntry(3, sign * acceleration.getX());
  263.         rhs.setEntry(4, sign * acceleration.getY());
  264.         rhs.setEntry(5, sign * acceleration.getZ());
  265.         signedMassRateAtTrigger = sign * maneuver.getPropulsionModel().getMassDerivatives(state, parameters);
  266.         if (stateDimension == 7) {
  267.             rhs.setEntry(6, signedMassRateAtTrigger);
  268.         }

  269.         // get State Transition Matrix with respect to Cartesian parameters at trigger time
  270.         final RealMatrix dY1dY0 = getStm(state);

  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.         // return unchanged state
  279.         return state;

  280.     }

  281.     /** Extract State Transition Matrix with respect to Cartesian parameters.
  282.      * @param state state containing the State Transition Matrix
  283.      * @return State Transition Matrix
  284.      */
  285.     private RealMatrix getStm(final SpacecraftState state) {
  286.         final double[] p = state.getAdditionalState(stmName);
  287.         final RealMatrix dYdY0 = MatrixUtils.createRealMatrix(stateDimension, stateDimension);
  288.         int index = 0;
  289.         for (int i = 0; i < stateDimension; ++i) {
  290.             for (int j = 0; j < stateDimension; ++j) {
  291.                 dYdY0.setEntry(i, j, p[index++]);
  292.             }
  293.         }
  294.         return dYdY0;
  295.     }

  296. }