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.propagation.numerical;
18  
19  import org.hipparchus.analysis.differentiation.Gradient;
20  import org.hipparchus.analysis.differentiation.GradientField;
21  import org.hipparchus.exception.LocalizedCoreFormats;
22  import org.hipparchus.geometry.euclidean.threed.Vector3D;
23  import org.hipparchus.linear.DecompositionSolver;
24  import org.hipparchus.linear.MatrixUtils;
25  import org.hipparchus.linear.QRDecomposition;
26  import org.hipparchus.linear.RealMatrix;
27  import org.hipparchus.ode.ODEIntegrator;
28  import org.hipparchus.util.Precision;
29  import org.orekit.annotation.DefaultDataContext;
30  import org.orekit.attitudes.Attitude;
31  import org.orekit.attitudes.AttitudeProvider;
32  import org.orekit.data.DataContext;
33  import org.orekit.errors.OrekitException;
34  import org.orekit.errors.OrekitInternalError;
35  import org.orekit.errors.OrekitMessages;
36  import org.orekit.forces.ForceModel;
37  import org.orekit.forces.drag.AbstractDragForceModel;
38  import org.orekit.forces.gravity.NewtonianAttraction;
39  import org.orekit.forces.inertia.InertialForces;
40  import org.orekit.forces.maneuvers.Maneuver;
41  import org.orekit.forces.maneuvers.jacobians.Duration;
42  import org.orekit.forces.maneuvers.jacobians.MassDepletionDelay;
43  import org.orekit.forces.maneuvers.jacobians.MedianDate;
44  import org.orekit.forces.maneuvers.jacobians.TriggerDate;
45  import org.orekit.forces.maneuvers.trigger.ManeuverTriggerDetector;
46  import org.orekit.forces.maneuvers.trigger.ResettableManeuverTriggers;
47  import org.orekit.forces.radiation.RadiationForceModel;
48  import org.orekit.frames.Frame;
49  import org.orekit.orbits.Orbit;
50  import org.orekit.orbits.OrbitType;
51  import org.orekit.orbits.PositionAngleType;
52  import org.orekit.propagation.AbstractMatricesHarvester;
53  import org.orekit.propagation.AdditionalDataProvider;
54  import org.orekit.propagation.MatricesHarvester;
55  import org.orekit.propagation.PropagationType;
56  import org.orekit.propagation.Propagator;
57  import org.orekit.propagation.SpacecraftState;
58  import org.orekit.propagation.events.DetectorModifier;
59  import org.orekit.propagation.events.EventDetector;
60  import org.orekit.propagation.events.FieldEventDetector;
61  import org.orekit.propagation.events.ParameterDrivenDateIntervalDetector;
62  import org.orekit.propagation.events.handlers.EventHandler;
63  import org.orekit.propagation.integration.AbstractIntegratedPropagator;
64  import org.orekit.propagation.integration.AdditionalDerivativesProvider;
65  import org.orekit.propagation.integration.StateMapper;
66  import org.orekit.time.AbsoluteDate;
67  import org.orekit.utils.AbsolutePVCoordinates;
68  import org.orekit.utils.DoubleArrayDictionary;
69  import org.orekit.utils.ParameterDriver;
70  import org.orekit.utils.ParameterDriversList;
71  import org.orekit.utils.ParameterDriversList.DelegatingDriver;
72  import org.orekit.utils.ParameterObserver;
73  import org.orekit.utils.TimeSpanMap;
74  import org.orekit.utils.TimeSpanMap.Span;
75  import org.orekit.utils.TimeStampedPVCoordinates;
76  
77  import java.util.ArrayList;
78  import java.util.Collection;
79  import java.util.Collections;
80  import java.util.List;
81  import java.util.Optional;
82  import java.util.stream.Collectors;
83  
84  /** This class propagates {@link org.orekit.orbits.Orbit orbits} using
85   * numerical integration.
86   * <p>Numerical propagation is much more accurate than analytical propagation
87   * like for example {@link org.orekit.propagation.analytical.KeplerianPropagator
88   * Keplerian} or {@link org.orekit.propagation.analytical.EcksteinHechlerPropagator
89   * Eckstein-Hechler}, but requires a few more steps to set up to be used properly.
90   * Whereas analytical propagators are configured only thanks to their various
91   * constructors and can be used immediately after construction, numerical propagators
92   * configuration involve setting several parameters between construction time
93   * and propagation time.</p>
94   * <p>The configuration parameters that can be set are:</p>
95   * <ul>
96   *   <li>the initial spacecraft state ({@link #setInitialState(SpacecraftState)})</li>
97   *   <li>the central attraction coefficient ({@link #setMu(double)})</li>
98   *   <li>the various force models ({@link #addForceModel(ForceModel)},
99   *   {@link #removeForceModels()})</li>
100  *   <li>the {@link OrbitType type} of orbital parameters to be used for propagation
101  *   ({@link #setOrbitType(OrbitType)}),</li>
102  *   <li>the {@link PositionAngleType type} of position angle to be used in orbital parameters
103  *   to be used for propagation where it is relevant ({@link
104  *   #setPositionAngleType(PositionAngleType)}),</li>
105  *   <li>whether {@link MatricesHarvester state transition matrices and Jacobians matrices}
106  *   (with the option to include mass if a 7x7 initial matrix is passed) should be propagated along with orbital state
107  *   ({@link #setupMatricesComputation(String, RealMatrix, DoubleArrayDictionary)}),</li>
108  *   <li>whether {@link org.orekit.propagation.integration.AdditionalDerivativesProvider additional derivatives}
109  *   should be propagated along with orbital state ({@link
110  *   #addAdditionalDerivativesProvider(AdditionalDerivativesProvider)}),</li>
111  *   <li>the discrete events that should be triggered during propagation
112  *   ({@link #addEventDetector(EventDetector)},
113  *   {@link #clearEventsDetectors()})</li>
114  *   <li>the binding logic with the rest of the application ({@link #getMultiplexer()})</li>
115  * </ul>
116  * <p>From these configuration parameters, only the initial state is mandatory. The default
117  * propagation settings are in {@link OrbitType#EQUINOCTIAL equinoctial} parameters with
118  * {@link PositionAngleType#ECCENTRIC} longitude argument. If the central attraction coefficient
119  * is not explicitly specified, the one used to define the initial orbit will be used.
120  * However, specifying only the initial state and perhaps the central attraction coefficient
121  * would mean the propagator would use only Keplerian forces. In this case, the simpler {@link
122  * org.orekit.propagation.analytical.KeplerianPropagator KeplerianPropagator} class would
123  * perhaps be more effective.</p>
124  * <p>The underlying numerical integrator set up in the constructor may also have its own
125  * configuration parameters. Typical configuration parameters for adaptive stepsize integrators
126  * are the min, max and perhaps start step size as well as the absolute and/or relative errors
127  * thresholds.</p>
128  * <p>The state that is seen by the integrator is a simple seven elements double array.
129  * The six first elements are either:
130  * <ul>
131  *   <li>the {@link org.orekit.orbits.EquinoctialOrbit equinoctial orbit parameters} (a, e<sub>x</sub>,
132  *   e<sub>y</sub>, h<sub>x</sub>, h<sub>y</sub>, λ<sub>M</sub> or λ<sub>E</sub>
133  *   or λ<sub>v</sub>) in meters and radians,</li>
134  *   <li>the {@link org.orekit.orbits.KeplerianOrbit Keplerian orbit parameters} (a, e, i, ω, Ω,
135  *   M or E or v) in meters and radians,</li>
136  *   <li>the {@link org.orekit.orbits.CircularOrbit circular orbit parameters} (a, e<sub>x</sub>, e<sub>y</sub>, i,
137  *   Ω, α<sub>M</sub> or α<sub>E</sub> or α<sub>v</sub>) in meters
138  *   and radians,</li>
139  *   <li>the {@link org.orekit.orbits.CartesianOrbit Cartesian orbit parameters} (x, y, z, v<sub>x</sub>,
140  *   v<sub>y</sub>, v<sub>z</sub>) in meters and meters per seconds.
141  * </ul>
142  * <p> The last element is the mass in kilograms and changes only during thrusters firings
143  *
144  * <p>The following code snippet shows a typical setting for Low Earth Orbit propagation in
145  * equinoctial parameters and true longitude argument:</p>
146  * <pre>
147  * final double dP       = 0.001;
148  * final double minStep  = 0.001;
149  * final double maxStep  = 500;
150  * final double initStep = 60;
151  * final double[][] tolerance = ToleranceProvider.getDefaultToleranceProvider(dP).getTolerances(orbit, OrbitType.EQUINOCTIAL);
152  * AdaptiveStepsizeIntegrator integrator = new DormandPrince853Integrator(minStep, maxStep, tolerance[0], tolerance[1]);
153  * integrator.setInitialStepSize(initStep);
154  * propagator = new NumericalPropagator(integrator);
155  * </pre>
156  * <p>By default, at the end of the propagation, the propagator resets the initial state to the final state,
157  * thus allowing a new propagation to be started from there without recomputing the part already performed.
158  * This behaviour can be changed by calling {@link #setResetAtEnd(boolean)}.
159  * </p>
160  * <p>Beware the same instance cannot be used simultaneously by different threads, the class is <em>not</em>
161  * thread-safe.</p>
162  *
163  * @see SpacecraftState
164  * @see ForceModel
165  * @see org.orekit.propagation.sampling.OrekitStepHandler
166  * @see org.orekit.propagation.sampling.OrekitFixedStepHandler
167  * @see org.orekit.propagation.integration.IntegratedEphemeris
168  * @see TimeDerivativesEquations
169  *
170  * @author Mathieu Rom&eacute;ro
171  * @author Luc Maisonobe
172  * @author Guylaine Prat
173  * @author Fabien Maussion
174  * @author V&eacute;ronique Pommier-Maurussane
175  */
176 public class NumericalPropagator extends AbstractIntegratedPropagator {
177 
178     /** Default orbit type. */
179     public static final OrbitType DEFAULT_ORBIT_TYPE = OrbitType.EQUINOCTIAL;
180 
181     /** Default position angle type. */
182     public static final PositionAngleType DEFAULT_POSITION_ANGLE_TYPE = PositionAngleType.ECCENTRIC;
183 
184     /** Threshold for matrix solving. */
185     private static final double THRESHOLD = Precision.SAFE_MIN;
186 
187     /** Force models used during the extrapolation of the orbit. */
188     private final List<ForceModel> forceModels;
189 
190     /** boolean to ignore or not the creation of a NewtonianAttraction. */
191     private boolean ignoreCentralAttraction;
192 
193     /**
194      * boolean to know if a full attitude (with rates) is needed when computing derivatives for the ODE.
195      * since 12.1
196      */
197     private boolean needFullAttitudeForDerivatives = true;
198 
199     /** Create a new instance of NumericalPropagator, based on orbit definition mu.
200      * After creation, the instance is empty, i.e. the attitude provider is set to an
201      * unspecified default law and there are no perturbing forces at all.
202      * This means that if {@link #addForceModel addForceModel} is not
203      * called after creation, the integrated orbit will follow a Keplerian
204      * evolution only. The defaults are {@link OrbitType#EQUINOCTIAL}
205      * for {@link #setOrbitType(OrbitType) propagation
206      * orbit type} and {@link PositionAngleType#ECCENTRIC} for {@link
207      * #setPositionAngleType(PositionAngleType) position angle type}.
208      *
209      * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
210      *
211      * @param integrator numerical integrator to use for propagation.
212      * @see #NumericalPropagator(ODEIntegrator, AttitudeProvider)
213      */
214     @DefaultDataContext
215     public NumericalPropagator(final ODEIntegrator integrator) {
216         this(integrator,
217                 Propagator.getDefaultLaw(DataContext.getDefault().getFrames()));
218     }
219 
220     /** Create a new instance of NumericalPropagator, based on orbit definition mu.
221      * After creation, the instance is empty, i.e. the attitude provider is set to an
222      * unspecified default law and there are no perturbing forces at all.
223      * This means that if {@link #addForceModel addForceModel} is not
224      * called after creation, the integrated orbit will follow a Keplerian
225      * evolution only. The defaults are {@link OrbitType#EQUINOCTIAL}
226      * for {@link #setOrbitType(OrbitType) propagation
227      * orbit type} and {@link PositionAngleType#ECCENTRIC} for {@link
228      * #setPositionAngleType(PositionAngleType) position angle type}.
229      * @param integrator numerical integrator to use for propagation.
230      * @param attitudeProvider the attitude law.
231      * @since 10.1
232      */
233     public NumericalPropagator(final ODEIntegrator integrator,
234                                final AttitudeProvider attitudeProvider) {
235         super(integrator, PropagationType.OSCULATING);
236         forceModels             = new ArrayList<>();
237         ignoreCentralAttraction = false;
238         initMapper();
239         setAttitudeProvider(attitudeProvider);
240         clearStepHandlers();
241         setOrbitType(DEFAULT_ORBIT_TYPE);
242         setPositionAngleType(DEFAULT_POSITION_ANGLE_TYPE);
243     }
244 
245     /** Set the flag to ignore or not the creation of a {@link NewtonianAttraction}.
246      * @param ignoreCentralAttraction if true, {@link NewtonianAttraction} is <em>not</em>
247      * added automatically if missing
248      */
249     public void setIgnoreCentralAttraction(final boolean ignoreCentralAttraction) {
250         this.ignoreCentralAttraction = ignoreCentralAttraction;
251     }
252 
253      /** Set the central attraction coefficient μ.
254       * <p>
255       * Setting the central attraction coefficient is
256       * equivalent to {@link #addForceModel(ForceModel) add}
257       * a {@link NewtonianAttraction} force model.
258       * * </p>
259       * @param mu central attraction coefficient (m³/s²)
260       * @see #addForceModel(ForceModel)
261       * @see #getAllForceModels()
262       */
263     @Override
264     public void setMu(final double mu) {
265         if (ignoreCentralAttraction) {
266             superSetMu(mu);
267         } else {
268             addForceModel(new NewtonianAttraction(mu));
269             superSetMu(mu);
270         }
271     }
272 
273     /** Set the central attraction coefficient μ only in upper class.
274      * @param mu central attraction coefficient (m³/s²)
275      */
276     private void superSetMu(final double mu) {
277         super.setMu(mu);
278     }
279 
280     /** Check if Newtonian attraction force model is available.
281      * <p>
282      * Newtonian attraction is always the last force model in the list.
283      * </p>
284      * @return true if Newtonian attraction force model is available
285      */
286     private boolean hasNewtonianAttraction() {
287         final int last = forceModels.size() - 1;
288         return last >= 0 && forceModels.get(last) instanceof NewtonianAttraction;
289     }
290 
291     /** Add a force model.
292      * <p>If this method is not called at all, the integrated orbit will follow
293      * a Keplerian evolution only.</p>
294      * @param model {@link ForceModel} to add (it can be either a perturbing force
295      * model or an instance of {@link NewtonianAttraction})
296      * @see #removeForceModels()
297      * @see #setMu(double)
298      */
299     public void addForceModel(final ForceModel model) {
300 
301         if (model instanceof NewtonianAttraction) {
302             // we want to add the central attraction force model
303 
304             try {
305                 // ensure we are notified of any mu change
306                 model.getParametersDrivers().get(0).addObserver(new ParameterObserver() {
307                     /** {@inheritDoc} */
308                     @Override
309                     public void valueSpanMapChanged(final TimeSpanMap<Double> previousValue, final ParameterDriver driver) {
310                         superSetMu(driver.getValue());
311                     }
312                     /** {@inheritDoc} */
313                     @Override
314                     public void valueChanged(final double previousValue, final ParameterDriver driver, final AbsoluteDate date) {
315                         superSetMu(driver.getValue());
316                     }
317                 });
318             } catch (OrekitException oe) {
319                 // this should never happen
320                 throw new OrekitInternalError(oe);
321             }
322 
323             if (hasNewtonianAttraction()) {
324                 // there is already a central attraction model, replace it
325                 forceModels.set(forceModels.size() - 1, model);
326             } else {
327                 // there are no central attraction model yet, add it at the end of the list
328                 forceModels.add(model);
329             }
330         } else {
331             // we want to add a perturbing force model
332             if (hasNewtonianAttraction()) {
333                 // insert the new force model before Newtonian attraction,
334                 // which should always be the last one in the list
335                 forceModels.add(forceModels.size() - 1, model);
336             } else {
337                 // we only have perturbing force models up to now, just append at the end of the list
338                 forceModels.add(model);
339             }
340         }
341 
342     }
343 
344     /** Remove all force models (except central attraction).
345      * <p>Once all perturbing forces have been removed (and as long as no new force
346      * model is added), the integrated orbit will follow a Keplerian evolution
347      * only.</p>
348      * @see #addForceModel(ForceModel)
349      */
350     public void removeForceModels() {
351         final int last = forceModels.size() - 1;
352         if (hasNewtonianAttraction()) {
353             // preserve the Newtonian attraction model at the end
354             final ForceModel newton = forceModels.get(last);
355             forceModels.clear();
356             forceModels.add(newton);
357         } else {
358             forceModels.clear();
359         }
360     }
361 
362     /** Get all the force models, perturbing forces and Newtonian attraction included.
363      * @return list of perturbing force models, with Newtonian attraction being the
364      * last one
365      * @see #addForceModel(ForceModel)
366      * @see #setMu(double)
367      */
368     public List<ForceModel> getAllForceModels() {
369         return Collections.unmodifiableList(forceModels);
370     }
371 
372     /** Set propagation orbit type.
373      * @param orbitType orbit type to use for propagation, null for
374      * propagating using {@link org.orekit.utils.AbsolutePVCoordinates} rather than {@link Orbit}
375      */
376     @Override
377     public void setOrbitType(final OrbitType orbitType) {
378         super.setOrbitType(orbitType);
379     }
380 
381     /** Get propagation parameter type.
382      * @return orbit type used for propagation, null for
383      * propagating using {@link org.orekit.utils.AbsolutePVCoordinates} rather than {@link Orbit}
384      */
385     @Override
386     public OrbitType getOrbitType() {
387         return super.getOrbitType();
388     }
389 
390     /** Set position angle type.
391      * <p>
392      * The position parameter type is meaningful only if {@link
393      * #getOrbitType() propagation orbit type}
394      * support it. As an example, it is not meaningful for propagation
395      * in {@link OrbitType#CARTESIAN Cartesian} parameters.
396      * </p>
397      * @param positionAngleType angle type to use for propagation
398      */
399     @Override
400     public void setPositionAngleType(final PositionAngleType positionAngleType) {
401         super.setPositionAngleType(positionAngleType);
402     }
403 
404     /** Get propagation parameter type.
405      * @return angle type to use for propagation
406      */
407     @Override
408     public PositionAngleType getPositionAngleType() {
409         return super.getPositionAngleType();
410     }
411 
412     /** Set the initial state.
413      * @param initialState initial state
414      */
415     public void setInitialState(final SpacecraftState initialState) {
416         resetInitialState(initialState);
417     }
418 
419     /** {@inheritDoc} */
420     @Override
421     public void resetInitialState(final SpacecraftState state) {
422         super.resetInitialState(state);
423         if (!hasNewtonianAttraction()) {
424             // use the state to define central attraction
425             setMu(state.isOrbitDefined() ? state.getOrbit().getMu() : Double.NaN);
426         }
427         setStartDate(state.getDate());
428     }
429 
430     /** Get the names of the parameters in the matrix returned by {@link MatricesHarvester#getParametersJacobian}.
431      * @return names of the parameters (i.e. columns) of the Jacobian matrix
432      */
433     List<String> getJacobiansColumnsNames() {
434         final List<String> columnsNames = new ArrayList<>();
435         for (final ForceModel forceModel : getAllForceModels()) {
436             for (final ParameterDriver driver : forceModel.getParametersDrivers()) {
437                 if (driver.isSelected() && !columnsNames.contains(driver.getNamesSpanMap().getFirstSpan().getData())) {
438                     // As driver with same name should have same NamesSpanMap we only check if the first span is present,
439                     // if not we add all span names to columnsNames
440                     for (Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
441                         columnsNames.add(span.getData());
442                     }
443                 }
444             }
445         }
446         Collections.sort(columnsNames);
447         return columnsNames;
448     }
449 
450     /** {@inheritDoc}
451      * <p>
452      * Unlike other propagators, the numerical one can consider the mass as a state variable in the transition matrix.
453      * To do so, a 7x7 initial matrix is to be passed instead of 6x6.
454      * </p>
455      * */
456     @Override
457     protected AbstractMatricesHarvester createHarvester(final String stmName, final RealMatrix initialStm,
458                                                         final DoubleArrayDictionary initialJacobianColumns) {
459         return new NumericalPropagationHarvester(this, stmName, initialStm, initialJacobianColumns);
460     }
461 
462     /** {@inheritDoc} */
463     @Override
464     public void clearMatricesComputation() {
465         final List<AdditionalDerivativesProvider> copiedDerivativesProviders = new ArrayList<>(getAdditionalDerivativesProviders());
466         copiedDerivativesProviders.stream().filter(AbstractStateTransitionMatrixGenerator.class::isInstance)
467                 .forEach(provider -> removeAdditionalDerivativesProvider(provider.getName()));
468         final List<AdditionalDataProvider<?>> copiedDataProviders = new ArrayList<>(getAdditionalDataProviders());
469         for (final AdditionalDataProvider<?> additionalDataProvider: copiedDataProviders) {
470             if (additionalDataProvider instanceof TriggerDate) {
471                 final TriggerDate triggerDate = (TriggerDate) additionalDataProvider;
472                 if (triggerDate.getMassDepletionDelay() != null) {
473                     removeAdditionalDerivativesProvider(triggerDate.getMassDepletionDelay().getName());
474                 }
475                 removeAdditionalDataProvider(additionalDataProvider.getName());
476             } else if (additionalDataProvider instanceof MedianDate || additionalDataProvider instanceof Duration) {
477                 removeAdditionalDataProvider(additionalDataProvider.getName());
478             }
479         }
480         super.clearMatricesComputation();
481     }
482 
483     /** {@inheritDoc} */
484     @Override
485     protected void setUpStmAndJacobianGenerators() {
486 
487         final AbstractMatricesHarvester harvester = getHarvester();
488         if (harvester != null) {
489 
490             // set up the additional equations and additional state providers
491             final AbstractStateTransitionMatrixGenerator stmGenerator = setUpStmGenerator();
492             final List<String> triggersDates = setUpTriggerDatesJacobiansColumns(stmGenerator);
493             setUpRegularParametersJacobiansColumns(stmGenerator, triggersDates);
494 
495             // as we are now starting the propagation, everything is configured
496             // we can freeze the names in the harvester
497             harvester.freezeColumnsNames();
498 
499         }
500 
501     }
502 
503     /** Set up the State Transition Matrix Generator.
504      * @return State Transition Matrix Generator
505      * @since 11.1
506      */
507     private AbstractStateTransitionMatrixGenerator setUpStmGenerator() {
508 
509         final AbstractMatricesHarvester harvester = getHarvester();
510 
511         // add the STM generator corresponding to the current settings, and setup state accordingly
512         AbstractStateTransitionMatrixGenerator stmGenerator = null;
513         for (final AdditionalDerivativesProvider equations : getAdditionalDerivativesProviders()) {
514             if (equations instanceof AbstractStateTransitionMatrixGenerator &&
515                 equations.getName().equals(harvester.getStmName())) {
516                 // the STM generator has already been set up in a previous propagation
517                 stmGenerator = (AbstractStateTransitionMatrixGenerator) equations;
518                 break;
519             }
520         }
521         if (stmGenerator == null) {
522             // this is the first time we need the STM generate, create it
523             if (harvester.getStateDimension() > 6) {
524                 stmGenerator = new ExtendedStateTransitionMatrixGenerator(harvester.getStmName(), getAllForceModels(),
525                         getAttitudeProvider());
526             } else {
527                 stmGenerator = new StateTransitionMatrixGenerator(harvester.getStmName(), getAllForceModels(),
528                         getAttitudeProvider());
529             }
530             addAdditionalDerivativesProvider(stmGenerator);
531         }
532 
533         if (!getInitialIntegrationState().hasAdditionalData(harvester.getStmName())) {
534             // add the initial State Transition Matrix if it is not already there
535             // (perhaps due to a previous propagation)
536             setInitialState(stmGenerator.setInitialStateTransitionMatrix(getInitialState(),
537                                                                          harvester.getInitialStateTransitionMatrix(),
538                                                                          getOrbitType(),
539                                                                          getPositionAngleType()));
540         }
541 
542         return stmGenerator;
543 
544     }
545 
546     /** Set up the Jacobians columns generator dedicated to trigger dates.
547      * @param stmGenerator State Transition Matrix generator
548      * @return names of the columns corresponding to trigger dates
549      * @since 13.1
550      */
551     private List<String> setUpTriggerDatesJacobiansColumns(final AbstractStateTransitionMatrixGenerator stmGenerator) {
552 
553         final String stmName = stmGenerator.getName();
554         final boolean isMassInStm = stmGenerator instanceof ExtendedStateTransitionMatrixGenerator;
555         final List<String> names = new ArrayList<>();
556         for (final ForceModel forceModel : getAllForceModels()) {
557             if (forceModel instanceof Maneuver && ((Maneuver) forceModel).getManeuverTriggers() instanceof ResettableManeuverTriggers) {
558                 final Maneuver maneuver = (Maneuver) forceModel;
559                 final ResettableManeuverTriggers maneuverTriggers = (ResettableManeuverTriggers) maneuver.getManeuverTriggers();
560 
561                 final Collection<EventDetector> selectedDetectors = maneuverTriggers.getEventDetectors().
562                         filter(ManeuverTriggerDetector.class::isInstance).
563                         map(triggerDetector -> ((ManeuverTriggerDetector<?>) triggerDetector).getDetector())
564                         .collect(Collectors.toList());
565                 for (final EventDetector detector: selectedDetectors) {
566                     if (detector instanceof ParameterDrivenDateIntervalDetector) {
567                         final ParameterDrivenDateIntervalDetector d = (ParameterDrivenDateIntervalDetector) detector;
568                         TriggerDate start;
569                         TriggerDate stop;
570 
571                         if (d.getStartDriver().isSelected() || d.getMedianDriver().isSelected() || d.getDurationDriver().isSelected()) {
572                             // normally datedriver should have only 1 span but just in case the user defines several span, there will
573                             // be no problem here
574                             for (Span<String> span = d.getStartDriver().getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
575                                 start = manageTriggerDate(stmName, maneuver, maneuverTriggers, span.getData(), true,
576                                         d.getThreshold(), isMassInStm);
577                                 names.add(start.getName());
578                                 start = null;
579                             }
580                         }
581                         if (d.getStopDriver().isSelected() || d.getMedianDriver().isSelected() || d.getDurationDriver().isSelected()) {
582                             // normally datedriver should have only 1 span but just in case the user defines several span, there will
583                             // be no problem here
584                             for (Span<String> span = d.getStopDriver().getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
585                                 stop = manageTriggerDate(stmName, maneuver, maneuverTriggers, span.getData(), false,
586                                         d.getThreshold(), isMassInStm);
587                                 names.add(stop.getName());
588                                 stop = null;
589                             }
590                         }
591                         if (d.getMedianDriver().isSelected()) {
592                             // for first span
593                             Span<String> currentMedianNameSpan = d.getMedianDriver().getNamesSpanMap().getFirstSpan();
594                             MedianDate median =
595                                     manageMedianDate(d.getStartDriver().getNamesSpanMap().getFirstSpan().getData(),
596                                             d.getStopDriver().getNamesSpanMap().getFirstSpan().getData(), currentMedianNameSpan.getData());
597                             names.add(median.getName());
598                             // for all span
599                             // normally datedriver should have only 1 span but just in case the user defines several span, there will
600                             // be no problem here. /!\ medianDate driver, startDate driver and stopDate driver must have same span number
601                             for (int spanNumber = 1; spanNumber < d.getMedianDriver().getNamesSpanMap().getSpansNumber(); ++spanNumber) {
602                                 currentMedianNameSpan = d.getMedianDriver().getNamesSpanMap().getSpan(currentMedianNameSpan.getEnd());
603                                 median =
604                                         manageMedianDate(d.getStartDriver().getNamesSpanMap().getSpan(currentMedianNameSpan.getStart()).getData(),
605                                                 d.getStopDriver().getNamesSpanMap().getSpan(currentMedianNameSpan.getStart()).getData(),
606                                                 currentMedianNameSpan.getData());
607                                 names.add(median.getName());
608 
609                             }
610 
611                         }
612                         if (d.getDurationDriver().isSelected()) {
613                             // for first span
614                             Span<String> currentDurationNameSpan = d.getDurationDriver().getNamesSpanMap().getFirstSpan();
615                             Duration duration =
616                                     manageManeuverDuration(d.getStartDriver().getNamesSpanMap().getFirstSpan().getData(),
617                                             d.getStopDriver().getNamesSpanMap().getFirstSpan().getData(), currentDurationNameSpan.getData());
618                             names.add(duration.getName());
619                             // for all span
620                             for (int spanNumber = 1; spanNumber < d.getDurationDriver().getNamesSpanMap().getSpansNumber(); ++spanNumber) {
621                                 currentDurationNameSpan = d.getDurationDriver().getNamesSpanMap().getSpan(currentDurationNameSpan.getEnd());
622                                 duration =
623                                         manageManeuverDuration(d.getStartDriver().getNamesSpanMap().getSpan(currentDurationNameSpan.getStart()).getData(),
624                                                 d.getStopDriver().getNamesSpanMap().getSpan(currentDurationNameSpan.getStart()).getData(),
625                                                 currentDurationNameSpan.getData());
626                                 names.add(duration.getName());
627 
628                             }
629                         }
630                     }
631                 }
632             }
633         }
634 
635         return names;
636 
637     }
638 
639     /** Manage a maneuver trigger date.
640      * @param stmName name of the State Transition Matrix state
641      * @param maneuver maneuver force model
642      * @param mt trigger to which the driver is bound
643      * @param driverName name of the date driver
644      * @param start if true, the driver is a maneuver start
645      * @param threshold event detector threshold
646      * @param isMassInStm flag on presence on mass in STM
647      * @return generator for the date driver
648      * @since 13.1
649      */
650     private TriggerDate manageTriggerDate(final String stmName,
651                                           final Maneuver maneuver,
652                                           final ResettableManeuverTriggers mt,
653                                           final String driverName,
654                                           final boolean start,
655                                           final double threshold,
656                                           final boolean isMassInStm) {
657 
658         TriggerDate triggerGenerator = null;
659 
660         // check if we already have set up the provider
661         for (final AdditionalDataProvider<?> provider : getAdditionalDataProviders()) {
662             if (provider instanceof TriggerDate &&
663                 provider.getName().equals(driverName)) {
664                 // the Jacobian column generator has already been set up in a previous propagation
665                 triggerGenerator = (TriggerDate) provider;
666                 break;
667             }
668         }
669 
670         if (triggerGenerator == null) {
671             // this is the first time we need the Jacobian column generator, create it
672             if (isMassInStm) {
673                 triggerGenerator = new TriggerDate(stmName, driverName, start, maneuver, threshold, true);
674             } else {
675                 final Optional<ForceModel> dragForce = getAllForceModels().stream().filter(AbstractDragForceModel.class::isInstance).findFirst();
676                 final Optional<ForceModel> srpForce = getAllForceModels().stream().filter(RadiationForceModel.class::isInstance).findFirst();
677                 final List<ForceModel> nonGravitationalForces = new ArrayList<>();
678                 dragForce.ifPresent(nonGravitationalForces::add);
679                 srpForce.ifPresent(nonGravitationalForces::add);
680                 triggerGenerator = new TriggerDate(stmName, driverName, start, maneuver, threshold, false,
681                         nonGravitationalForces.toArray(new ForceModel[0]));
682             }
683             mt.addResetter(triggerGenerator);
684             final MassDepletionDelay depletionDelay = triggerGenerator.getMassDepletionDelay();
685             if (depletionDelay != null) {
686                 addAdditionalDerivativesProvider(depletionDelay);
687             }
688             addAdditionalDataProvider(triggerGenerator);
689         }
690 
691         if (!getInitialIntegrationState().hasAdditionalData(driverName)) {
692             // add the initial Jacobian column if it is not already there
693             // (perhaps due to a previous propagation)
694             final MassDepletionDelay depletionDelay = triggerGenerator.getMassDepletionDelay();
695             final double[] zeroes = new double[depletionDelay == null ? 7 : 6];
696             if (depletionDelay != null) {
697                 setInitialColumn(depletionDelay.getName(), zeroes);
698             }
699             setInitialColumn(driverName, getHarvester().getInitialJacobianColumn(driverName));
700         }
701 
702         return triggerGenerator;
703 
704     }
705 
706     /** Manage a maneuver median date.
707      * @param startName name of the start driver
708      * @param stopName name of the stop driver
709      * @param medianName name of the median driver
710      * @return generator for the median driver
711      * @since 11.1
712      */
713     private MedianDate manageMedianDate(final String startName, final String stopName, final String medianName) {
714 
715         MedianDate medianGenerator = null;
716 
717         // check if we already have set up the provider
718         for (final AdditionalDataProvider<?> provider : getAdditionalDataProviders()) {
719             if (provider instanceof MedianDate &&
720                 provider.getName().equals(medianName)) {
721                 // the Jacobian column generator has already been set up in a previous propagation
722                 medianGenerator = (MedianDate) provider;
723                 break;
724             }
725         }
726 
727         if (medianGenerator == null) {
728             // this is the first time we need the Jacobian column generator, create it
729             medianGenerator = new MedianDate(startName, stopName, medianName);
730             addAdditionalDataProvider(medianGenerator);
731         }
732 
733         if (!getInitialIntegrationState().hasAdditionalData(medianName)) {
734             // add the initial Jacobian column if it is not already there
735             // (perhaps due to a previous propagation)
736             setInitialColumn(medianName, getHarvester().getInitialJacobianColumn(medianName));
737         }
738 
739         return medianGenerator;
740 
741     }
742 
743     /** Manage a maneuver duration.
744      * @param startName name of the start driver
745      * @param stopName name of the stop driver
746      * @param durationName name of the duration driver
747      * @return generator for the median driver
748      * @since 11.1
749      */
750     private Duration manageManeuverDuration(final String startName, final String stopName, final String durationName) {
751 
752         Duration durationGenerator = null;
753 
754         // check if we already have set up the provider
755         for (final AdditionalDataProvider<?> provider : getAdditionalDataProviders()) {
756             if (provider instanceof Duration &&
757                 provider.getName().equals(durationName)) {
758                 // the Jacobian column generator has already been set up in a previous propagation
759                 durationGenerator = (Duration) provider;
760                 break;
761             }
762         }
763 
764         if (durationGenerator == null) {
765             // this is the first time we need the Jacobian column generator, create it
766             durationGenerator = new Duration(startName, stopName, durationName);
767             addAdditionalDataProvider(durationGenerator);
768         }
769 
770         if (!getInitialIntegrationState().hasAdditionalData(durationName)) {
771             // add the initial Jacobian column if it is not already there
772             // (perhaps due to a previous propagation)
773             setInitialColumn(durationName, getHarvester().getInitialJacobianColumn(durationName));
774         }
775 
776         return durationGenerator;
777 
778     }
779 
780     /** Set up the Jacobians columns generator for regular parameters.
781      * @param stmGenerator generator for the State Transition Matrix
782      * @param triggerDates names of the columns already managed as trigger dates
783      * @since 11.1
784      */
785     private void setUpRegularParametersJacobiansColumns(final AbstractStateTransitionMatrixGenerator stmGenerator,
786                                                         final List<String> triggerDates) {
787 
788         // first pass: gather all parameters (excluding trigger dates), binding similar names together
789         final ParameterDriversList selected = new ParameterDriversList();
790         for (final ForceModel forceModel : getAllForceModels()) {
791             for (final ParameterDriver driver : forceModel.getParametersDrivers()) {
792                 if (!triggerDates.contains(driver.getNamesSpanMap().getFirstSpan().getData())) {
793                     // if the first span is not in triggerDates,
794                     // it means that the driver is not a trigger date and can be selected here
795                     selected.add(driver);
796                 }
797             }
798         }
799 
800         // second pass: now that shared parameter names are bound together,
801         // their selections status have been synchronized, we can filter them
802         selected.filter(true);
803 
804         // third pass: sort parameters lexicographically
805         selected.sort();
806 
807         // add the Jacobians column generators corresponding to parameters, and setup state accordingly
808         // a new column is needed for each value estimated so for each span of the parameterDriver
809         for (final DelegatingDriver driver : selected.getDrivers()) {
810 
811             for (Span<String> currentNameSpan = driver.getNamesSpanMap().getFirstSpan(); currentNameSpan != null; currentNameSpan = currentNameSpan.next()) {
812 
813                 IntegrableJacobianColumnGenerator generator = null;
814                 // check if we already have set up the providers
815                 for (final AdditionalDerivativesProvider provider : getAdditionalDerivativesProviders()) {
816                     if (provider instanceof IntegrableJacobianColumnGenerator &&
817                         provider.getName().equals(currentNameSpan.getData())) {
818                         // the Jacobian column generator has already been set up in a previous propagation
819                         generator = (IntegrableJacobianColumnGenerator) provider;
820                         break;
821                     }
822 
823                 }
824 
825                 if (generator == null) {
826                     // this is the first time we need the Jacobian column generator, create it
827                     final boolean isMassIncluded = stmGenerator.getStateDimension() == 7;
828                     generator = new IntegrableJacobianColumnGenerator(stmGenerator, currentNameSpan.getData(), isMassIncluded);
829                     addAdditionalDerivativesProvider(generator);
830                 }
831 
832                 if (!getInitialIntegrationState().hasAdditionalData(currentNameSpan.getData())) {
833                     // add the initial Jacobian column if it is not already there
834                     // (perhaps due to a previous propagation)
835                     setInitialColumn(currentNameSpan.getData(), getHarvester().getInitialJacobianColumn(currentNameSpan.getData()));
836                 }
837 
838             }
839 
840         }
841 
842     }
843 
844     /** Add the initial value of the column to the initial state.
845      * <p>
846      * The initial state must already contain the Cartesian State Transition Matrix.
847      * </p>
848      * @param columnName name of the column
849      * @param dYdQ column of the Jacobian ∂Y/∂qₘ with respect to propagation type,
850      * if null (which is the most frequent case), assumed to be 0
851      * @since 11.1
852      */
853     private void setInitialColumn(final String columnName, final double[] dYdQ) {
854 
855         final SpacecraftState state = getInitialState();
856 
857         final AbstractStateTransitionMatrixGenerator generator = (AbstractStateTransitionMatrixGenerator)
858                 getAdditionalDerivativesProviders().stream()
859                         .filter(AbstractStateTransitionMatrixGenerator.class::isInstance)
860                         .collect(Collectors.toList()).get(0);
861         final int expectedSize = generator.getStateDimension();
862         if (dYdQ.length != expectedSize) {
863             throw new OrekitException(LocalizedCoreFormats.DIMENSIONS_MISMATCH, dYdQ.length, expectedSize);
864         }
865 
866         // convert to Cartesian Jacobian
867         final RealMatrix dYdC = MatrixUtils.createRealIdentityMatrix(expectedSize);
868         final double[][] jacobian = new double[6][6];
869         getOrbitType().convertType(state.getOrbit()).getJacobianWrtCartesian(getPositionAngleType(), jacobian);
870         dYdC.setSubMatrix(jacobian, 0, 0);
871         final DecompositionSolver solver = getSolver(dYdC);
872         final double[] column = solver.solve(MatrixUtils.createRealVector(dYdQ)).toArray();
873 
874         // set additional state
875         setInitialState(state.addAdditionalData(columnName, column));
876 
877     }
878 
879     /**
880      * Method to get a linear system solver.
881      * @param matrix matrix involved in linear systems
882      * @return solver
883      * @since 13.1
884      */
885     private DecompositionSolver getSolver(final RealMatrix matrix) {
886         return new QRDecomposition(matrix, THRESHOLD).getSolver();
887     }
888 
889     /** {@inheritDoc} */
890     @Override
891     protected AttitudeProvider initializeAttitudeProviderForDerivatives() {
892         return needFullAttitudeForDerivatives ? getAttitudeProvider() : getFrozenAttitudeProvider();
893     }
894 
895     /** {@inheritDoc} */
896     @Override
897     protected StateMapper createMapper(final AbsoluteDate referenceDate, final double mu,
898                                        final OrbitType orbitType, final PositionAngleType positionAngleType,
899                                        final AttitudeProvider attitudeProvider, final Frame frame) {
900         return new OsculatingMapper(referenceDate, mu, orbitType, positionAngleType, attitudeProvider, frame);
901     }
902 
903     /** Internal mapper using directly osculating parameters. */
904     private static class OsculatingMapper extends StateMapper {
905 
906         /** Simple constructor.
907          * <p>
908          * The position parameter type is meaningful only if {@link
909          * #getOrbitType() propagation orbit type}
910          * support it. As an example, it is not meaningful for propagation
911          * in {@link OrbitType#CARTESIAN Cartesian} parameters.
912          * </p>
913          * @param referenceDate reference date
914          * @param mu central attraction coefficient (m³/s²)
915          * @param orbitType orbit type to use for mapping (can be null for {@link AbsolutePVCoordinates})
916          * @param positionAngleType angle type to use for propagation
917          * @param attitudeProvider attitude provider
918          * @param frame inertial frame
919          */
920         OsculatingMapper(final AbsoluteDate referenceDate, final double mu,
921                          final OrbitType orbitType, final PositionAngleType positionAngleType,
922                          final AttitudeProvider attitudeProvider, final Frame frame) {
923             super(referenceDate, mu, orbitType, positionAngleType, attitudeProvider, frame);
924         }
925 
926         /** {@inheritDoc} */
927         @Override
928         public SpacecraftState mapArrayToState(final AbsoluteDate date, final double[] y, final double[] yDot,
929                                                final PropagationType type) {
930             // the parameter type is ignored for the Numerical Propagator
931 
932             final double mass = y[6];
933             final double massRate = yDot == null ? 0. : yDot[6];
934             if (mass <= 0.0) {
935                 throw new OrekitException(OrekitMessages.NOT_POSITIVE_SPACECRAFT_MASS, mass);
936             }
937 
938             if (super.getOrbitType() == null) {
939                 // propagation uses absolute position-velocity-acceleration
940                 final Vector3D p = new Vector3D(y[0],    y[1],    y[2]);
941                 final Vector3D v = new Vector3D(y[3],    y[4],    y[5]);
942                 final Vector3D a;
943                 final AbsolutePVCoordinates absPva;
944                 if (yDot == null) {
945                     absPva = new AbsolutePVCoordinates(getFrame(), new TimeStampedPVCoordinates(date, p, v));
946                 } else {
947                     a = new Vector3D(yDot[3], yDot[4], yDot[5]);
948                     absPva = new AbsolutePVCoordinates(getFrame(), new TimeStampedPVCoordinates(date, p, v, a));
949                 }
950 
951                 final Attitude attitude = getAttitudeProvider().getAttitude(absPva, date, getFrame());
952                 return new SpacecraftState(absPva, attitude).withMassRate(massRate).withMass(mass);
953             } else {
954                 // propagation uses regular orbits
955                 final Orbit orbit       = super.getOrbitType().mapArrayToOrbit(y, yDot, super.getPositionAngleType(), date, getMu(), getFrame());
956                 final Attitude attitude = getAttitudeProvider().getAttitude(orbit, date, getFrame());
957 
958                 return new SpacecraftState(orbit, attitude).withMassRate(massRate).withMass(mass);
959             }
960 
961         }
962 
963         /** {@inheritDoc} */
964         public void mapStateToArray(final SpacecraftState state, final double[] y, final double[] yDot) {
965             if (super.getOrbitType() == null) {
966                 // propagation uses absolute position-velocity-acceleration
967                 final Vector3D p = state.getAbsPVA().getPosition();
968                 final Vector3D v = state.getAbsPVA().getVelocity();
969                 y[0] = p.getX();
970                 y[1] = p.getY();
971                 y[2] = p.getZ();
972                 y[3] = v.getX();
973                 y[4] = v.getY();
974                 y[5] = v.getZ();
975                 y[6] = state.getMass();
976             }
977             else {
978                 super.getOrbitType().mapOrbitToArray(state.getOrbit(), super.getPositionAngleType(), y, yDot);
979                 y[6] = state.getMass();
980             }
981         }
982 
983     }
984 
985     /** {@inheritDoc} */
986     protected MainStateEquations getMainStateEquations(final ODEIntegrator integrator) {
987         return new Main(integrator, getOrbitType(), getPositionAngleType(), getAllForceModels());
988     }
989 
990     /** Internal class for osculating parameters integration. */
991     private class Main extends NumericalTimeDerivativesEquations implements MainStateEquations {
992 
993         /** Flag keeping track whether Jacobian matrix needs to be recomputed or not. */
994         private final boolean recomputingJacobian;
995 
996         /** Simple constructor.
997          * @param integrator numerical integrator to use for propagation.
998          * @param orbitType orbit type
999          * @param positionAngleType angle type
1000          * @param forceModelList forces
1001          */
1002         Main(final ODEIntegrator integrator, final OrbitType orbitType, final PositionAngleType positionAngleType,
1003              final List<ForceModel> forceModelList) {
1004 
1005             super(orbitType, positionAngleType, forceModelList);
1006             final int numberOfForces = forceModelList.size();
1007             if (orbitType != null && orbitType != OrbitType.CARTESIAN && numberOfForces > 0) {
1008                 if (numberOfForces > 1) {
1009                     recomputingJacobian = true;
1010                 } else {
1011                     recomputingJacobian = !(forceModelList.get(0) instanceof NewtonianAttraction);
1012                 }
1013             } else {
1014                 recomputingJacobian = false;
1015             }
1016 
1017             // feed internal event detectors
1018             setUpInternalDetectors(integrator);
1019 
1020         }
1021 
1022         /** Set up all user defined event detectors.
1023          * @param integrator numerical integrator to use for propagation.
1024          */
1025         private void setUpInternalDetectors(final ODEIntegrator integrator) {
1026             final NumericalTimeDerivativesEquations cartesianEquations = new NumericalTimeDerivativesEquations(OrbitType.CARTESIAN,
1027                     null, forceModels);
1028             final List<FieldEventDetector<Gradient>> fieldDetectors = new ArrayList<>();
1029             if (getHarvester() != null) {
1030                 final GradientField field = GradientField.getField(getHarvester().getStateDimension() + 1);
1031                 getForceModels().stream().flatMap(forceModel -> forceModel.getFieldEventDetectors(field))
1032                         .filter(fieldEventDetector -> !fieldEventDetector.getEventFunction().dependsOnTimeOnly())
1033                         .forEach(fieldDetectors::add);
1034                 getAttitudeProvider().getFieldEventDetectors(field)
1035                         .filter(fieldEventDetector -> !fieldEventDetector.getEventFunction().dependsOnTimeOnly())
1036                         .forEach(fieldDetectors::add);
1037             }
1038             for (final ForceModel forceModel : getForceModels()) {
1039                 forceModel.getEventDetectors().forEach(detector -> setUpInternalEventDetector(integrator, detector,
1040                         cartesianEquations, fieldDetectors));
1041             }
1042             getAttitudeProvider().getEventDetectors().forEach(detector -> setUpInternalEventDetector(integrator,
1043                     detector, cartesianEquations, fieldDetectors));
1044         }
1045 
1046         /** Set up one internal event detector.
1047          * @param integrator numerical integrator to use for propagation.
1048          * @param eventDetector detector
1049          * @param cartesianEquations Cartesian derivatives model
1050          * @param fieldDetectors detectors for Taylor differential algebra
1051          */
1052         private void setUpInternalEventDetector(final ODEIntegrator integrator,
1053                                                 final EventDetector eventDetector,
1054                                                 final NumericalTimeDerivativesEquations cartesianEquations,
1055                                                 final List<FieldEventDetector<Gradient>> fieldDetectors) {
1056             final EventDetector internalDetector;
1057             if (!fieldDetectors.isEmpty() && !eventDetector.getEventFunction().dependsOnTimeOnly()) {
1058                 // need to modify STM at each dynamics discontinuities
1059                 final NumericalPropagationHarvester harvester = (NumericalPropagationHarvester) getHarvester();
1060                 final SwitchEventHandler handler = new SwitchEventHandler(eventDetector.getHandler(), harvester,
1061                         cartesianEquations, getAttitudeProvider(), fieldDetectors);
1062                 internalDetector = getLocalDetector(eventDetector, handler);
1063             } else {
1064                 internalDetector = eventDetector;
1065             }
1066             setUpEventDetector(integrator, internalDetector);
1067         }
1068 
1069         /** {@inheritDoc} */
1070         @Override
1071         public void init(final SpacecraftState initialState, final AbsoluteDate target) {
1072             final List<ForceModel> forceModelList = getForceModels();
1073             needFullAttitudeForDerivatives = forceModelList.stream().anyMatch(ForceModel::dependsOnAttitudeRate);
1074 
1075             forceModelList.forEach(fm -> fm.init(initialState, target));
1076 
1077         }
1078 
1079         /** {@inheritDoc} */
1080         @Override
1081         public double[] computeDerivatives(final SpacecraftState state) {
1082             setCurrentState(state);
1083             if (recomputingJacobian) {
1084                 // propagation uses Jacobian matrix of orbital parameters w.r.t. Cartesian ones
1085                 final double[][] jacobian = new double[6][6];
1086                 state.getOrbit().getJacobianWrtCartesian(getPositionAngleType(), jacobian);
1087                 setCoordinatesJacobian(jacobian);
1088             }
1089             return computeTimeDerivatives(state);
1090 
1091         }
1092 
1093     }
1094 
1095     /** {@inheritDoc} */
1096     @Override
1097     protected void beforeIntegration(final SpacecraftState initialState, final AbsoluteDate tEnd) {
1098 
1099         if (!getFrame().isPseudoInertial()) {
1100 
1101             // inspect all force models to find InertialForces
1102             for (ForceModel force : forceModels) {
1103                 if (force instanceof InertialForces) {
1104                     return;
1105                 }
1106             }
1107 
1108             // throw exception if no inertial forces found
1109             throw new OrekitException(OrekitMessages.INERTIAL_FORCE_MODEL_MISSING, getFrame().getName());
1110 
1111         }
1112 
1113     }
1114 
1115     /**
1116      * Creates local detector wrapping input one and using specific handler for dynamics discontinuities and STM.
1117      * @param eventDetector detector
1118      * @param switchEventHandler special handler
1119      * @return wrapped detector
1120      */
1121     private static EventDetector getLocalDetector(final EventDetector eventDetector,
1122                                                   final SwitchEventHandler switchEventHandler) {
1123         return new DetectorModifier() {
1124             @Override
1125             public EventDetector getDetector() {
1126                 return eventDetector;
1127             }
1128 
1129             @Override
1130             public EventHandler getHandler() {
1131                 return switchEventHandler;
1132             }
1133         };
1134     }
1135 }