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.control.indirect.shooting;
18  
19  import org.hipparchus.CalculusFieldElement;
20  import org.hipparchus.Field;
21  import org.hipparchus.analysis.differentiation.Gradient;
22  import org.hipparchus.analysis.differentiation.GradientField;
23  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
24  import org.hipparchus.linear.LUDecomposition;
25  import org.hipparchus.linear.MatrixUtils;
26  import org.hipparchus.linear.RealMatrix;
27  import org.hipparchus.ode.FieldOrdinaryDifferentialEquation;
28  import org.hipparchus.ode.nonstiff.FieldExplicitRungeKuttaIntegrator;
29  import org.hipparchus.util.FastMath;
30  import org.hipparchus.util.MathArrays;
31  import org.orekit.attitudes.FieldAttitude;
32  import org.orekit.control.indirect.adjoint.CartesianAdjointDerivativesProvider;
33  import org.orekit.control.indirect.adjoint.FieldCartesianAdjointDerivativesProvider;
34  import org.orekit.control.indirect.adjoint.cost.ControlSwitchDetector;
35  import org.orekit.control.indirect.adjoint.cost.FieldControlSwitchDetector;
36  import org.orekit.control.indirect.shooting.propagation.AdjointDynamicsProvider;
37  import org.orekit.control.indirect.shooting.propagation.ShootingPropagationSettings;
38  import org.orekit.forces.ForceModel;
39  import org.orekit.frames.Frame;
40  import org.orekit.orbits.FieldCartesianOrbit;
41  import org.orekit.propagation.FieldSpacecraftState;
42  import org.orekit.propagation.SpacecraftState;
43  import org.orekit.propagation.events.EventsLogger;
44  import org.orekit.propagation.events.FieldEventDetector;
45  import org.orekit.propagation.integration.FieldAdditionalDerivativesProvider;
46  import org.orekit.propagation.integration.FieldCombinedDerivatives;
47  import org.orekit.propagation.numerical.NumericalPropagator;
48  import org.orekit.propagation.sampling.PropagationStepRecorder;
49  import org.orekit.time.AbsoluteDate;
50  import org.orekit.time.FieldAbsoluteDate;
51  import org.orekit.utils.FieldAbsolutePVCoordinates;
52  import org.orekit.utils.FieldPVCoordinates;
53  
54  import java.util.Arrays;
55  import java.util.List;
56  import java.util.stream.Collectors;
57  
58  /**
59   * Abstract class for indirect single shooting methods with fixed initial Cartesian state.
60   * Inheritors must implement the iteration update, assuming derivatives are needed.
61   *
62   * @author Romain Serra
63   * @since 13.0
64   * @see CartesianAdjointDerivativesProvider
65   * @see org.orekit.control.indirect.adjoint.FieldCartesianAdjointDerivativesProvider
66   */
67  public abstract class AbstractFixedInitialCartesianSingleShooting extends AbstractIndirectShooting {
68  
69      /** Template for initial state. Contains the initial Cartesian coordinates. */
70      private final SpacecraftState initialSpacecraftStateTemplate;
71  
72      /**
73       * Step handler to record propagation steps.
74       */
75      private final PropagationStepRecorder propagationStepRecorder;
76  
77      /** Event logger. */
78      private final EventsLogger eventsLogger;
79  
80      /** Scales for automatic differentiation variables. */
81      private double[] scales;
82  
83      /**
84       * Constructor with boundary conditions as orbits.
85       * @param propagationSettings propagation settings
86       * @param initialSpacecraftStateTemplate template for initial propagation state
87       */
88      protected AbstractFixedInitialCartesianSingleShooting(final ShootingPropagationSettings propagationSettings,
89                                                            final SpacecraftState initialSpacecraftStateTemplate) {
90          super(propagationSettings);
91          this.initialSpacecraftStateTemplate = initialSpacecraftStateTemplate;
92          this.propagationStepRecorder = new PropagationStepRecorder();
93          this.eventsLogger = new EventsLogger();
94      }
95  
96      /**
97       * Build unit scales.
98       * @return scales
99       */
100     private double[] getUnitScales() {
101         final double[] unitScales = new double[getPropagationSettings().getAdjointDynamicsProvider().getDimension()];
102         Arrays.fill(unitScales, 1.);
103         return unitScales;
104     }
105 
106     /**
107      * Protected getter for the differentiation scales.
108      * @return scales for variable scales
109      */
110     protected double[] getScales() {
111         return scales;
112     }
113 
114     /**
115      * Returns the maximum number of iterations.
116      * @return maximum iterations
117      */
118     public abstract int getMaximumIterationCount();
119 
120     /** {@inheritDoc} */
121     @Override
122     public ShootingBoundaryOutput solve(final double initialMass, final double[] initialGuess) {
123         return solve(initialMass, initialGuess, getUnitScales());
124     }
125 
126     /**
127      * Solve for the boundary conditions, given an initial mass and an initial guess for the adjoint variables.
128      * Uses scales for automatic differentiation.
129      * @param initialMass initial mass
130      * @param initialGuess initial guess
131      * @param userScales scales
132      * @return boundary problem solution
133      */
134     public ShootingBoundaryOutput solve(final double initialMass, final double[] initialGuess,
135                                         final double[] userScales) {
136         scales = userScales.clone();
137         final SpacecraftState initialState = createStateWithMassAndAdjoint(initialMass, initialGuess);
138         final ShootingBoundaryOutput initialGuessSolution = computeCandidateSolution(initialState, 0);
139         if (initialGuessSolution.isConverged()) {
140             return initialGuessSolution;
141         } else {
142             return iterate(initialGuessSolution);
143         }
144     }
145 
146     /** {@inheritDoc} */
147     @Override
148     protected NumericalPropagator buildPropagator(final SpacecraftState initialState) {
149         final NumericalPropagator propagator = super.buildPropagator(initialState);
150         propagator.setStepHandler(propagationStepRecorder);
151         final CartesianAdjointDerivativesProvider derivativesProvider = (CartesianAdjointDerivativesProvider)
152             getPropagationSettings().getAdjointDynamicsProvider().buildAdditionalDerivativesProvider();
153         eventsLogger.clearLoggedEvents();
154         derivativesProvider.getCost().getEventDetectors()
155                 .forEach(detector -> propagator.addEventDetector(eventsLogger.monitorDetector(detector)));
156         return propagator;
157     }
158 
159     /**
160      * Form solution container with input initial state.
161      * @param initialState initial state
162      * @param iterationCount iteration count
163      * @return candidate solution
164      */
165     public abstract ShootingBoundaryOutput computeCandidateSolution(SpacecraftState initialState, int iterationCount);
166 
167     /**
168      * Iterate on initial guess to solve boundary problem.
169      * @param initialGuess initial guess
170      * @return solution (or null pointer if not converged)
171      */
172     private ShootingBoundaryOutput iterate(final ShootingBoundaryOutput initialGuess) {
173         int iterationCount = 1;
174         SpacecraftState initialState = initialGuess.getInitialState();
175         ShootingBoundaryOutput candidateSolution = initialGuess;
176         final String adjointName = getPropagationSettings().getAdjointDynamicsProvider().getAdjointName();
177         double[] initialAdjoint = initialState.getAdditionalState(adjointName).clone();
178         final double initialMass = initialState.getMass();
179         while (iterationCount < getMaximumIterationCount()) {
180             if (candidateSolution.isConverged()) {
181                 return candidateSolution;
182             }
183             final FieldSpacecraftState<Gradient> fieldInitialState = createFieldInitialStateWithMassAndAdjoint(initialMass,
184                 initialAdjoint);
185             initialState = fieldInitialState.toSpacecraftState();
186             final FieldSpacecraftState<Gradient> fieldTerminalState = propagateField(fieldInitialState);
187             initialAdjoint = updateShootingVariables(initialAdjoint, fieldTerminalState);
188             if (Double.isNaN(initialAdjoint[0])) {
189                 return candidateSolution;
190             } else {
191                 candidateSolution = computeCandidateSolution(initialState, iterationCount);
192             }
193             iterationCount++;
194         }
195         return candidateSolution;
196     }
197 
198     /**
199      * Propagate in Field. Does not use a full propagator (only the integrator, moving step by step using the history of non-Field propagation).
200      * It is faster as adaptive step and event detection are particularly slow with Field.
201      * @param fieldInitialState initial state
202      * @return terminal state
203      */
204     private FieldSpacecraftState<Gradient> propagateField(final FieldSpacecraftState<Gradient> fieldInitialState) {
205         final FieldAbsoluteDate<Gradient> initialDate = fieldInitialState.getDate();
206         final FieldExplicitRungeKuttaIntegrator<Gradient> fieldIntegrator = buildFieldIntegrator(fieldInitialState);
207         final FieldOrdinaryDifferentialEquation<Gradient> fieldODE = buildFieldODE(fieldInitialState.getDate());
208         final AdjointDynamicsProvider dynamicsProvider = getPropagationSettings().getAdjointDynamicsProvider();
209         AbsoluteDate date = initialDate.toAbsoluteDate();
210         Gradient[] integrationState = formatToArray(fieldInitialState, dynamicsProvider.getAdjointName());
211         // step-by-step integration
212         final List<EventsLogger.LoggedEvent> loggedEvents = eventsLogger.getLoggedEvents();
213         final List<AbsoluteDate> stepDates = propagationStepRecorder.copyStates().stream().map(SpacecraftState::getDate)
214                 .collect(Collectors.toList());
215         for (final AbsoluteDate stepDate: stepDates) {
216             final Gradient time = initialDate.durationFrom(date).negate();
217             final Gradient nextTime = initialDate.durationFrom(stepDate).negate();
218             integrationState = fieldIntegrator.singleStep(fieldODE, time, integrationState, nextTime);
219             // deal with switch event if any
220             if (!loggedEvents.isEmpty()) {
221                 for (final EventsLogger.LoggedEvent loggedEvent: loggedEvents) {
222                     final SpacecraftState loggedState = loggedEvent.getState();
223                     if (loggedEvent.getEventDetector() instanceof ControlSwitchDetector && loggedState.getDate().isEqualTo(stepDate)) {
224                         final ControlSwitchDetector switchDetector = (ControlSwitchDetector) loggedEvent.getEventDetector();
225                         integrationState = findSwitchEventAndUpdateState(date, integrationState,
226                                 initialDate.toAbsoluteDate(), switchDetector, dynamicsProvider);
227                     }
228                 }
229             }
230             date = new AbsoluteDate(stepDate);
231         }
232         return formatFromArray(date, integrationState);
233     }
234 
235     /**
236      * Create initial state with input mass.
237      * @param initialMass initial mass
238      * @return state
239      */
240     protected SpacecraftState createInitialStateWithMass(final double initialMass) {
241         return initialSpacecraftStateTemplate.withMass(initialMass);
242     }
243 
244     /**
245      * Create initial state with input mass and adjoint vector.
246      * @param initialMass initial mass
247      * @param initialAdjoint initial adjoint variables
248      * @return state
249      */
250     private SpacecraftState createStateWithMassAndAdjoint(final double initialMass, final double[] initialAdjoint) {
251         final String adjointName = getPropagationSettings().getAdjointDynamicsProvider().getAdjointName();
252         return createInitialStateWithMass(initialMass).addAdditionalData(adjointName, initialAdjoint);
253     }
254 
255     /**
256      * Create initial Gradient state with input mass and adjoint vector, the latter being the independent variables.
257      * @param initialMass initial mass
258      * @param initialAdjoint initial adjoint variables
259      * @return state
260      */
261     protected FieldSpacecraftState<Gradient> createFieldInitialStateWithMassAndAdjoint(final double initialMass,
262                                                                                        final double[] initialAdjoint) {
263         final int parametersNumber = initialAdjoint.length;
264         final GradientField field = GradientField.getField(parametersNumber);
265         final FieldSpacecraftState<Gradient> stateWithoutAdjoint = new FieldSpacecraftState<>(field,
266                 createInitialStateWithMass(initialMass));
267         final Gradient[] fieldInitialAdjoint = MathArrays.buildArray(field, initialAdjoint.length);
268         for (int i = 0; i < parametersNumber; i++) {
269             fieldInitialAdjoint[i] = Gradient.variable(parametersNumber, i, 0.).multiply(getScales()[i]).add(initialAdjoint[i]);
270         }
271         final String adjointName = getPropagationSettings().getAdjointDynamicsProvider().getAdjointName();
272         return stateWithoutAdjoint.addAdditionalData(adjointName, fieldInitialAdjoint);
273     }
274 
275     /**
276      * Create state.
277      * @param date epoch
278      * @param cartesian Cartesian variables
279      * @param mass mass
280      * @param adjoint adjoint variables
281      * @param <T> field type
282      * @return state
283      */
284     protected <T extends CalculusFieldElement<T>> FieldSpacecraftState<T> createFieldState(final FieldAbsoluteDate<T> date,
285                                                                                            final T[] cartesian,
286                                                                                            final T mass,
287                                                                                            final T[] adjoint) {
288         final Field<T> field = mass.getField();
289         final Frame frame = getPropagationSettings().getPropagationFrame();
290         final FieldVector3D<T> position = new FieldVector3D<>(Arrays.copyOfRange(cartesian, 0, 3));
291         final FieldVector3D<T> velocity = new FieldVector3D<>(Arrays.copyOfRange(cartesian, 3, 6));
292         final FieldPVCoordinates<T> pvCoordinates = new FieldPVCoordinates<>(position, velocity);
293         final FieldSpacecraftState<T> stateWithoutAdjoint;
294         if (initialSpacecraftStateTemplate.isOrbitDefined()) {
295             final T mu = field.getZero().newInstance(initialSpacecraftStateTemplate.getOrbit().getMu());
296             final FieldCartesianOrbit<T> orbit = new FieldCartesianOrbit<>(pvCoordinates, frame, date, mu);
297             final FieldAttitude<T> attitude = getPropagationSettings().getAttitudeProvider().getAttitude(orbit,
298                     orbit.getDate(), orbit.getFrame());
299             stateWithoutAdjoint = new FieldSpacecraftState<>(orbit, attitude).withMass(mass);
300         } else {
301             final FieldAbsolutePVCoordinates<T> absolutePVCoordinates = new FieldAbsolutePVCoordinates<>(frame, date,
302                     pvCoordinates);
303             final FieldAttitude<T> attitude = getPropagationSettings().getAttitudeProvider().getAttitude(absolutePVCoordinates,
304                     absolutePVCoordinates.getDate(), absolutePVCoordinates.getFrame());
305             stateWithoutAdjoint = new FieldSpacecraftState<>(absolutePVCoordinates, attitude).withMass(mass);
306         }
307         final String adjointName = getPropagationSettings().getAdjointDynamicsProvider().getAdjointName();
308         return stateWithoutAdjoint.addAdditionalData(adjointName, adjoint);
309     }
310 
311     /**
312      * Update shooting variables.
313      * @param originalShootingVariables original shooting variables (before update)
314      * @param fieldTerminalState final state of gradient propagation
315      * @return updated shooting variables
316      */
317     protected abstract double[] updateShootingVariables(double[] originalShootingVariables,
318                                                         FieldSpacecraftState<Gradient> fieldTerminalState);
319 
320     /**
321      * Build Ordinary Differential Equation for Field.
322      * @param referenceDate date defining the origin of times
323      * @param <T> field type
324      * @return Field ODE
325      * @since 13.0
326      */
327     protected <T extends CalculusFieldElement<T>> FieldOrdinaryDifferentialEquation<T> buildFieldODE(final FieldAbsoluteDate<T> referenceDate) {
328         final Field<T> field = referenceDate.getField();
329         final AdjointDynamicsProvider adjointDynamicsProvider = getPropagationSettings().getAdjointDynamicsProvider();
330         final FieldAdditionalDerivativesProvider<T> derivativesProvider = adjointDynamicsProvider
331                 .buildFieldAdditionalDerivativesProvider(field);
332 
333         return new FieldOrdinaryDifferentialEquation<T>() {
334             @Override
335             public int getDimension() {
336                 return 7 + adjointDynamicsProvider.getDimension();
337             }
338 
339             @Override
340             public T[] computeDerivatives(final T t, final T[] y) {
341                 // build state
342                 final T[] cartesian = Arrays.copyOfRange(y, 0, 6);
343                 final FieldAbsoluteDate<T> date = referenceDate.shiftedBy(t);
344                 final Field<T> field = date.getField();
345                 final T zero = field.getZero();
346                 final T[] adjoint = Arrays.copyOfRange(y, 7, y.length);
347                 final FieldSpacecraftState<T> state = createFieldState(date, cartesian, y[6], adjoint);
348                 // compute derivatives
349                 final T[] yDot = MathArrays.buildArray(field, getDimension());
350                 yDot[0] = zero.add(y[3]);
351                 yDot[1] = zero.add(y[4]);
352                 yDot[2] = zero.add(y[5]);
353                 FieldVector3D<T> totalAcceleration = FieldVector3D.getZero(field);
354                 for (final ForceModel forceModel: getPropagationSettings().getForceModels()) {
355                     final FieldVector3D<T> acceleration = forceModel.acceleration(state, forceModel.getParameters(field));
356                     totalAcceleration = totalAcceleration.add(acceleration);
357                 }
358                 yDot[3] = totalAcceleration.getX();
359                 yDot[4] = totalAcceleration.getY();
360                 yDot[5] = totalAcceleration.getZ();
361                 final FieldCombinedDerivatives<T> combinedDerivatives = derivativesProvider.combinedDerivatives(state);
362                 final T[] cartesianDotContribution = combinedDerivatives.getMainStateDerivativesIncrements();
363                 for (int i = 0; i < cartesianDotContribution.length; i++) {
364                     yDot[i] = yDot[i].add(cartesianDotContribution[i]);
365                 }
366                 final T[] adjointDot = combinedDerivatives.getAdditionalDerivatives();
367                 System.arraycopy(adjointDot, 0, yDot, yDot.length - adjointDot.length, adjointDot.length);
368                 return yDot;
369             }
370         };
371     }
372 
373     /**
374      * Form array from Orekit object.
375      * @param fieldState state
376      * @param adjointName adjoint name
377      * @return propagation state as array
378      */
379     private Gradient[] formatToArray(final FieldSpacecraftState<Gradient> fieldState,
380                                      final String adjointName) {
381         final Gradient[] adjoint = fieldState.getAdditionalState(adjointName);
382         final int adjointDimension = adjoint.length;
383         final Gradient[] integrationState = MathArrays.buildArray(fieldState.getMass().getField(), 7 + adjointDimension);
384         final FieldPVCoordinates<Gradient> pvCoordinates = fieldState.getPVCoordinates();
385         System.arraycopy(pvCoordinates.getPosition().toArray(), 0, integrationState, 0, 3);
386         System.arraycopy(pvCoordinates.getVelocity().toArray(), 0, integrationState, 3, 3);
387         integrationState[6] = fieldState.getMass();
388         System.arraycopy(adjoint, 0, integrationState, 7, adjointDimension);
389         return integrationState;
390     }
391 
392     /**
393      * Form Orekit object from array.
394      * @param date date
395      * @param integrationState propagation state as array
396      * @return Orekit state
397      */
398     private FieldSpacecraftState<Gradient> formatFromArray(final AbsoluteDate date, final Gradient[] integrationState) {
399         final Gradient[] terminalCartesian = Arrays.copyOfRange(integrationState, 0, 6);
400         final Gradient[] terminalAdjoint = Arrays.copyOfRange(integrationState, 7, integrationState.length);
401         final Field<Gradient> field = terminalCartesian[0].getField();
402         return createFieldState(new FieldAbsoluteDate<>(field, date), terminalCartesian, integrationState[6],
403                 terminalAdjoint);
404     }
405 
406     /**
407      * Iterate over field switch detectors and find the one that was triggered in the non-Field propagation.
408      * Then, use it to update the gradient.
409      * @param date date
410      * @param integrationState integration variables
411      * @param referenceDate date at start of propagation
412      * @param switchDetector switch detector
413      * @param dynamicsProvider adjoint dynamics provider
414      * @return update integration state
415      */
416     private Gradient[] findSwitchEventAndUpdateState(final AbsoluteDate date, final Gradient[] integrationState,
417                                                      final AbsoluteDate referenceDate,
418                                                      final ControlSwitchDetector switchDetector,
419                                                      final AdjointDynamicsProvider dynamicsProvider) {
420         final FieldSpacecraftState<Gradient> fieldState = formatFromArray(date, integrationState);
421         final double expectedG = switchDetector.g(fieldState.toSpacecraftState());
422         final int shootingVariables = dynamicsProvider.getDimension();
423         final int increasedVariables = shootingVariables + 1;
424         final GradientField increasedVariablesField = GradientField.getField(increasedVariables);
425         final FieldCartesianAdjointDerivativesProvider<Gradient> fieldDerivativesProvider =
426                 (FieldCartesianAdjointDerivativesProvider<Gradient>) dynamicsProvider.buildFieldAdditionalDerivativesProvider(increasedVariablesField);
427         final List<FieldEventDetector<Gradient>> fieldEventDetectors = fieldDerivativesProvider.getCost()
428                 .getFieldEventDetectors(increasedVariablesField).collect(Collectors.toList());
429         for (final FieldEventDetector<Gradient> fieldEventDetector : fieldEventDetectors) {
430             if (fieldEventDetector instanceof FieldControlSwitchDetector) {
431                 final double actualG = fieldEventDetector.g(fieldState).getReal();
432                 if (FastMath.abs(actualG - expectedG) < 1e-10) {
433                     return updateStateWithTaylorMapInversion(date, integrationState, referenceDate, fieldEventDetector);
434                 }
435             }
436         }
437         return integrationState;
438     }
439 
440     /**
441      * Update the differential information by taking into account that a state-dependent event was triggered.
442      * @param date date
443      * @param integrationState integration variables
444      * @param initialDate date at start of propagation
445      * @param fieldDetector switch detector
446      * @return updated integration state
447      */
448     private Gradient[] updateStateWithTaylorMapInversion(final AbsoluteDate date, final Gradient[] integrationState,
449                                                          final AbsoluteDate initialDate,
450                                                          final FieldEventDetector<Gradient> fieldDetector) {
451         // form array with increased gradient size
452         final Gradient threshold = fieldDetector.getThreshold();
453         final int increasedVariables = threshold.getFreeParameters();
454         final GradientField increasedVariablesField = GradientField.getField(increasedVariables);
455         final Gradient[] increasedVariablesArray = MathArrays.buildArray(increasedVariablesField,
456                 integrationState.length);
457         for (int i = 0; i < integrationState.length; i++) {
458             increasedVariablesArray[i] = integrationState[i].stackVariable();
459         }
460         // compute rates at switch
461         final GradientField field = increasedVariablesArray[0].getField();
462         final FieldOrdinaryDifferentialEquation<Gradient> fieldODE = buildFieldODE(new FieldAbsoluteDate<>(field, initialDate));
463         final double duration = date.durationFrom(initialDate);
464         final Gradient fieldDuration = field.getZero().newInstance(duration);
465         final Gradient[] derivatives = fieldODE.computeDerivatives(fieldDuration, increasedVariablesArray);
466         // compute differences in rates
467         final double[] deltaRates = computeDeltaRates(date, increasedVariablesArray,
468                 fieldDetector.getThreshold().getValue(), initialDate, fieldODE, derivatives);
469         // evaluate event function in Taylor algebra
470         final double[] derivativesValues = new double[derivatives.length];
471         for (int i = 0; i < derivativesValues.length; i++) {
472             derivativesValues[i] = derivatives[i].getValue();
473         }
474         final Gradient g = evaluateG(date, increasedVariablesArray, initialDate, fieldDetector, derivativesValues);
475         // perform last step involving variables swap
476         return invertTaylorMap(increasedVariablesArray, deltaRates, g);
477     }
478 
479     /**
480      * Form differences in rates before and after switch.
481      * @param date epoch
482      * @param integrationState state variables
483      * @param threshold event detection threshold
484      * @param initialDate initial date
485      * @param fieldODE ODE model
486      * @param derivatives rates at switch
487      * @return rates difference
488      */
489     private double[] computeDeltaRates(final AbsoluteDate date, final Gradient[] integrationState,
490                                        final double threshold, final AbsoluteDate initialDate,
491                                        final FieldOrdinaryDifferentialEquation<Gradient> fieldODE,
492                                        final Gradient[] derivatives) {
493         final double duration = date.durationFrom(initialDate);
494         final Gradient fieldDuration = integrationState[0].getField().getZero().newInstance(duration);
495         final double tinyStep = threshold * 2.;
496         final boolean isForward = date.isAfter(initialDate);
497         final double timeShift = isForward ? tinyStep : -tinyStep;
498         final Gradient[] derivativesBefore = shiftAndComputeDerivatives(fieldDuration, integrationState, derivatives,
499                 fieldODE, -timeShift);
500         final Gradient[] derivativesAfter = shiftAndComputeDerivatives(fieldDuration, integrationState, derivatives,
501                 fieldODE, timeShift);
502         final double[] deltaRates = new double[integrationState.length];
503         for (int i = 0; i < integrationState.length; i++) {
504             deltaRates[i] = derivativesBefore[i].getValue() - derivativesAfter[i].getValue();
505         }
506         return deltaRates;
507     }
508 
509     /**
510      * Shift state variables in time and compute rates.
511      * @param fieldDuration duration
512      * @param integrationState state variables
513      * @param derivatives rates before shift
514      * @param fieldODE ODE model
515      * @param timeShift shift
516      * @return rates
517      */
518     private Gradient[] shiftAndComputeDerivatives(final Gradient fieldDuration, final Gradient[] integrationState,
519                                                   final Gradient[] derivatives,
520                                                   final FieldOrdinaryDifferentialEquation<Gradient> fieldODE,
521                                                   final double timeShift) {
522         final Gradient[] state = integrationState.clone();
523         for (int i = 0; i < state.length; i++) {
524             state[i] = state[i].add(derivatives[i].multiply(timeShift));
525         }
526         return fieldODE.computeDerivatives(fieldDuration, state);
527     }
528 
529     /**
530      * Evaluate event function in proper Taylor algebra.
531      * @param date date
532      * @param increasedVariablesArray integration variables with increased gradient size
533      * @param initialDate date at start of propagation
534      * @param fieldDetector switch detector
535      * @param derivatives rates
536      * @return g
537      */
538     private Gradient evaluateG(final AbsoluteDate date, final Gradient[] increasedVariablesArray,
539                                final AbsoluteDate initialDate, final FieldEventDetector<Gradient> fieldDetector,
540                                final double[] derivatives) {
541         final Field<Gradient> field = increasedVariablesArray[0].getField();
542         final int increasedVariables = field.getZero().getFreeParameters();
543         final Gradient lastVariable = Gradient.variable(increasedVariables, increasedVariables - 1, 1);
544         final boolean isForward = date.isAfterOrEqualTo(initialDate);
545         final Gradient dt = isForward ? lastVariable : lastVariable.negate();
546         final Gradient[] shiftedVariables = increasedVariablesArray.clone();
547         for (int i = 0; i < shiftedVariables.length; i++) {
548             shiftedVariables[i] = shiftedVariables[i].add(dt.multiply(derivatives[i]));
549         }
550         final FieldSpacecraftState<Gradient> fieldState = formatFromArray(date, shiftedVariables);
551         return fieldDetector.g(fieldState);
552     }
553 
554     /**
555      * Invert so-called Taylor map to make the event function value an independent variable.
556      * Then nullify its variation to keep only the derivatives of interest.
557      * @param state integration variables with dt as last gradient variable
558      * @param deltaRates differences in rates from dynamics switch
559      * @param g event function evaluated with dt as last gradient variable
560      * @return update integration variables
561      */
562     private static Gradient[] invertTaylorMap(final Gradient[] state, final double[] deltaRates, final Gradient g) {
563         // swap dt and g as variables of algebra
564         final int increasedGradientDimension = g.getFreeParameters();
565         final RealMatrix rightMatrix = MatrixUtils.createRealIdentityMatrix(increasedGradientDimension);
566         rightMatrix.setRow(rightMatrix.getRowDimension() - 1, g.getGradient());
567         final LUDecomposition luDecomposition = new LUDecomposition(rightMatrix, 0.);
568         final RealMatrix inverted = luDecomposition.getSolver().getInverse();
569         final double[][] leftMatrixCoefficients = new double[state.length][];
570         for (int i = 0; i < state.length; i++) {
571             leftMatrixCoefficients[i] = state[i].getGradient();
572             leftMatrixCoefficients[i][leftMatrixCoefficients[i].length - 1] = deltaRates[i];
573         }
574         final RealMatrix product = MatrixUtils.createRealMatrix(leftMatrixCoefficients).multiply(inverted);
575         // pack into array with original gradient dimension
576         final int gradientDimension = increasedGradientDimension - 1;
577         final GradientField field = GradientField.getField(gradientDimension);
578         final Gradient[] outputState = MathArrays.buildArray(field, state.length);
579         for (int i = 0; i < outputState.length; i++) {
580             final double[] gradient = new double[gradientDimension];
581             System.arraycopy(product.getRow(i), 0, gradient, 0, gradientDimension);
582             outputState[i] = new Gradient(state[i].getValue(), gradient);
583         }
584         return outputState;
585     }
586 
587 }