1   /* Copyright 2022-2025 Romain Serra
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.propagation.numerical;
18  
19  
20  import org.hipparchus.analysis.differentiation.Gradient;
21  import org.hipparchus.analysis.differentiation.GradientField;
22  import org.hipparchus.linear.MatrixUtils;
23  import org.hipparchus.linear.RealMatrix;
24  import org.hipparchus.linear.RealVector;
25  import org.hipparchus.ode.events.Action;
26  import org.hipparchus.util.FastMath;
27  import org.orekit.attitudes.Attitude;
28  import org.orekit.attitudes.AttitudeProvider;
29  import org.orekit.frames.Frame;
30  import org.orekit.orbits.CartesianOrbit;
31  import org.orekit.orbits.Orbit;
32  import org.orekit.propagation.FieldSpacecraftState;
33  import org.orekit.propagation.SpacecraftState;
34  import org.orekit.propagation.events.EventDetector;
35  import org.orekit.propagation.events.FieldEventDetector;
36  import org.orekit.propagation.events.handlers.EventHandler;
37  import org.orekit.time.AbsoluteDate;
38  import org.orekit.time.FieldAbsoluteDate;
39  import org.orekit.utils.AbsolutePVCoordinates;
40  import org.orekit.utils.DataDictionary;
41  import org.orekit.utils.DerivativeStateUtils;
42  import org.orekit.utils.PVCoordinates;
43  import org.orekit.utils.TimeStampedPVCoordinates;
44  
45  import java.util.Arrays;
46  import java.util.List;
47  
48  /**
49   * Class handling the State Transition Matrix update in the presence of dynamics discontinuities.
50   * Reference: Eq. (29-30) in Russell, R. P., “Primer Vector Theory Applied to Global Low-Thrust Trade Studies,”
51   * Journal of Guidance, Control, and Dynamics, Vol. 30, No. 2, 2007, pp. 460-472.
52   *
53   * @author Romain Serra
54   * @since 13.1
55   * @see NumericalPropagator
56   */
57  class SwitchEventHandler implements EventHandler {
58  
59      /** Original event handler to wrap. */
60      private final EventHandler wrappedHandler;
61  
62      /** Matrices harvester. */
63      private final NumericalPropagationHarvester matricesHarvester;
64  
65      /** Differential equations. */
66      private final NumericalTimeDerivativesEquations timeDerivativesEquations;
67  
68      /** Attitude provider. */
69      private final AttitudeProvider attitudeProvider;
70  
71      /** Event detectors in Taylor differential algebra. */
72      private final List<FieldEventDetector<Gradient>> fieldDetectors;
73  
74      /** Flag on propagation direction. */
75      private boolean isForward;
76  
77      /** Field version of detector defining dynamics switch. */
78      private FieldEventDetector<Gradient> switchFieldDetector;
79  
80      /**
81       * Constructor.
82       * @param wrappedHandler original event handler
83       * @param matricesHarvester matrices harvester
84       * @param timeDerivativesEquations differential equations
85       * @param attitudeProvider attitude provider
86       * @param fieldDetectors non-empty event detectors in Taylor differential algebra
87       */
88      SwitchEventHandler(final EventHandler wrappedHandler, final NumericalPropagationHarvester matricesHarvester,
89                         final NumericalTimeDerivativesEquations timeDerivativesEquations,
90                         final AttitudeProvider attitudeProvider, final List<FieldEventDetector<Gradient>> fieldDetectors) {
91          this.wrappedHandler = wrappedHandler;
92          this.matricesHarvester = matricesHarvester;
93          this.timeDerivativesEquations = timeDerivativesEquations;
94          this.attitudeProvider = attitudeProvider;
95          this.fieldDetectors = fieldDetectors;
96      }
97  
98      @Override
99      public SpacecraftState resetState(final EventDetector detector, final SpacecraftState oldState) {
100         if (switchFieldDetector == null) {
101             // failed to find corresponding Field detector
102             return wrappedHandler.resetState(detector, oldState);
103         } else {
104             final SpacecraftState newState = updateState(oldState);
105             switchFieldDetector = null;
106             return newState;
107         }
108     }
109 
110     @Override
111     public void init(final SpacecraftState initialState, final AbsoluteDate target, final EventDetector detector) {
112         isForward = initialState.getDate().isBeforeOrEqualTo(target);
113         switchFieldDetector = null;
114         final GradientField field = fieldDetectors.get(0).getThreshold().getField();
115         final FieldAbsoluteDate<Gradient> fieldTarget = new FieldAbsoluteDate<>(field, target);
116         final FieldSpacecraftState<Gradient> fieldState = new FieldSpacecraftState<>(field, initialState);
117         fieldDetectors.forEach(fieldEventDetector -> fieldEventDetector.init(fieldState, fieldTarget));
118         EventHandler.super.init(initialState, target, detector);
119     }
120 
121     @Override
122     public Action eventOccurred(final SpacecraftState s, final EventDetector detector, final boolean increasing) {
123         final Action action = wrappedHandler.eventOccurred(s, detector, increasing);
124         if (action == Action.RESET_DERIVATIVES) {
125             switchFieldDetector = findSwitchDetector(detector, s);
126             if (switchFieldDetector != null) {
127                 return Action.RESET_STATE;
128             }
129         }
130         return action;
131     }
132 
133     /**
134      * Method finding the Field detector corresponding to a non-Field, triggered one.
135      * @param detector triggered event detector
136      * @param state state
137      * @return Field detector
138      */
139     private FieldEventDetector<Gradient> findSwitchDetector(final EventDetector detector, final SpacecraftState state) {
140         final GradientField field = fieldDetectors.get(0).getThreshold().getField();
141         final FieldSpacecraftState<Gradient> fieldState = new FieldSpacecraftState<>(field, state);
142         fieldDetectors.forEach(fieldDetector -> fieldDetector.reset(fieldState, fieldState.getDate()));
143         final double g = detector.g(state);
144         return findSwitchDetector(g, fieldState);
145     }
146 
147     /**
148      * Method finding the Field detector corresponding to a non-Field, triggered one.
149      * @param g value of triggered detector
150      * @param fieldState Field state
151      * @return Field detector
152      */
153     private FieldEventDetector<Gradient> findSwitchDetector(final double g,
154                                                             final FieldSpacecraftState<Gradient> fieldState) {
155         for (final FieldEventDetector<Gradient> fieldDetector : fieldDetectors) {
156             final Gradient fieldG = fieldDetector.g(fieldState);
157             if (FastMath.abs(fieldG.getValue() - g) < 1e-11) {
158                 return fieldDetector;
159             }
160         }
161         return null;
162     }
163 
164     /**
165      * Update state, more precisely the additional data corresponding to the STM.
166      * @param stateAtSwitch original state
167      * @return updated state
168      */
169     private SpacecraftState updateState(final SpacecraftState stateAtSwitch) {
170         final RealMatrix cartesianFactorMatrix = computeUpdateMatrix(stateAtSwitch);
171         // update STM
172         final String stmName = matricesHarvester.getStmName();
173         final RealMatrix oldStm = matricesHarvester.toSquareMatrix(stateAtSwitch.getAdditionalState(stmName));
174         final RealMatrix stm = cartesianFactorMatrix.multiply(oldStm).add(oldStm);
175         final DataDictionary additionalData = new DataDictionary(stateAtSwitch.getAdditionalDataValues());
176         additionalData.put(stmName, matricesHarvester.toArray(stm.getData()));
177         // update model parameters Jacobian if present
178         if (!matricesHarvester.getJacobiansColumnsNames().isEmpty()) {
179             for (final String parameterName : matricesHarvester.getJacobiansColumnsNames()) {
180                 final RealVector oldJacobian = MatrixUtils.createRealVector(stateAtSwitch.getAdditionalState(parameterName));
181                 final RealVector jacobian = cartesianFactorMatrix.operate(oldJacobian).add(oldJacobian);
182                 additionalData.put(parameterName, jacobian.toArray());
183             }
184         }
185         return stateAtSwitch.withAdditionalData(additionalData);
186     }
187 
188     /**
189      * Compute matrix needed to update STM and the like.
190      * @param state state
191      * @return matrix
192      */
193     private RealMatrix computeUpdateMatrix(final SpacecraftState state) {
194         final double twoThreshold = switchFieldDetector.getThreshold().getValue() * 2.;
195         final double dt = isForward ? -twoThreshold : twoThreshold;
196         final SpacecraftState stateBefore = shift(state, dt);
197         final double[] derivativesBefore = timeDerivativesEquations.computeTimeDerivatives(stateBefore);
198         final SpacecraftState stateAfter = shift(state, -dt);
199         final double[] derivativesAfter = timeDerivativesEquations.computeTimeDerivatives(stateAfter);
200         final int stateDimension = matricesHarvester.getStateDimension();
201         final double[] deltaDerivatives = new double[stateDimension];
202         for (int i = 3; i < deltaDerivatives.length; i++) {
203             deltaDerivatives[i] = derivativesAfter[i] - derivativesBefore[i];
204         }
205         final Gradient g = evaluateG(buildCartesianState(state, state.getPVCoordinates()));
206         final double gLastDerivative = g.getPartialDerivative(stateDimension);
207         final double gDot = isForward ? gLastDerivative : -gLastDerivative;
208         final double[] gGradientState = Arrays.copyOfRange(g.getGradient(), 0, stateDimension);
209         final RealVector lhs = MatrixUtils.createRealVector(deltaDerivatives);
210         final RealVector rhs = MatrixUtils.createRealVector(gGradientState).mapMultiply(1. / gDot);
211         return lhs.outerProduct(rhs);
212     }
213 
214     /**
215      * Shift state using first order Taylor expansion for position.
216      * @param stateAtSwitch state
217      * @param dt time shift
218      * @return new state
219      */
220     private SpacecraftState shift(final SpacecraftState stateAtSwitch, final double dt) {
221         final PVCoordinates pvCoordinates = stateAtSwitch.getPVCoordinates();
222         final AbsoluteDate shiftedDate = stateAtSwitch.getDate().shiftedBy(dt);
223         final TimeStampedPVCoordinates shiftedPV = new TimeStampedPVCoordinates(shiftedDate,
224                 pvCoordinates.getPosition().add(pvCoordinates.getVelocity().scalarMultiply(dt)), pvCoordinates.getVelocity());
225         return buildCartesianState(stateAtSwitch, shiftedPV);
226     }
227 
228     /**
229      * Build state given template and new position-velocity vector.
230      * @param templateState template state
231      * @param pvCoordinates position-velocity vector
232      * @return new state
233      */
234     private SpacecraftState buildCartesianState(final SpacecraftState templateState,
235                                                 final TimeStampedPVCoordinates pvCoordinates) {
236         final SpacecraftState state;
237         final Frame frame = templateState.getFrame();
238         if (templateState.isOrbitDefined()) {
239             final Orbit templateOrbit = templateState.getOrbit();
240             final CartesianOrbit orbit = new CartesianOrbit(pvCoordinates, frame, templateOrbit.getMu());
241             final Attitude attitude = attitudeProvider.getAttitude(orbit, pvCoordinates.getDate(), frame);
242             state = new SpacecraftState(orbit, attitude);
243         } else {
244             final AbsolutePVCoordinates absolutePVCoordinates = new AbsolutePVCoordinates(frame, pvCoordinates);
245             final Attitude attitude = attitudeProvider.getAttitude(absolutePVCoordinates,
246                     pvCoordinates.getDate(), frame);
247             state = new SpacecraftState(absolutePVCoordinates, attitude);
248         }
249         return state.withMass(templateState.getMass())
250                 .withAdditionalData(templateState.getAdditionalDataValues())
251                 .withAdditionalStatesDerivatives(templateState.getAdditionalStatesDerivatives());
252     }
253 
254     /**
255      * Evaluate event function in Taylor algebra.
256      * @param state state
257      * @return g in Taylor algebra
258      */
259     private Gradient evaluateG(final SpacecraftState state) {
260         final int stateDimension = matricesHarvester.getStateDimension();
261         Gradient time = Gradient.variable(stateDimension + 1, stateDimension, 0);
262         if (!isForward) {
263             time = time.negate();
264         }
265         final GradientField field = time.getField();
266         final FieldSpacecraftState<Gradient> fieldState = DerivativeStateUtils.buildSpacecraftStateGradient(field,
267                 state, attitudeProvider);
268         return switchFieldDetector.g(fieldState.shiftedBy(time));
269     }
270 
271 }
272