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