1   /* Copyright 2002-2026 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().getFirst().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 triggerDate) {
471                 if (triggerDate.getMassDepletionDelay() != null) {
472                     removeAdditionalDerivativesProvider(triggerDate.getMassDepletionDelay().getName());
473                 }
474                 removeAdditionalDataProvider(additionalDataProvider.getName());
475             } else if (additionalDataProvider instanceof MedianDate || additionalDataProvider instanceof Duration) {
476                 removeAdditionalDataProvider(additionalDataProvider.getName());
477             }
478         }
479         super.clearMatricesComputation();
480     }
481 
482     /** {@inheritDoc} */
483     @Override
484     protected void setUpStmAndJacobianGenerators() {
485 
486         final AbstractMatricesHarvester harvester = getHarvester();
487         if (harvester != null) {
488 
489             // set up the additional equations and additional state providers
490             final AbstractStateTransitionMatrixGenerator stmGenerator = setUpStmGenerator();
491             final List<String> triggersDates = setUpTriggerDatesJacobiansColumns(stmGenerator);
492             setUpRegularParametersJacobiansColumns(stmGenerator, triggersDates);
493 
494             // as we are now starting the propagation, everything is configured
495             // we can freeze the names in the harvester
496             harvester.freezeColumnsNames();
497 
498         }
499 
500     }
501 
502     /** Set up the State Transition Matrix Generator.
503      * @return State Transition Matrix Generator
504      * @since 11.1
505      */
506     private AbstractStateTransitionMatrixGenerator setUpStmGenerator() {
507 
508         final AbstractMatricesHarvester harvester = getHarvester();
509 
510         // add the STM generator corresponding to the current settings, and setup state accordingly
511         AbstractStateTransitionMatrixGenerator stmGenerator = null;
512         for (final AdditionalDerivativesProvider equations : getAdditionalDerivativesProviders()) {
513             if (equations instanceof AbstractStateTransitionMatrixGenerator generator &&
514                 equations.getName().equals(harvester.getStmName())) {
515                 // the STM generator has already been set up in a previous propagation
516                 stmGenerator = generator;
517                 break;
518             }
519         }
520         if (stmGenerator == null) {
521             // this is the first time we need the STM generate, create it
522             if (harvester.getStateDimension() > 6) {
523                 stmGenerator = new ExtendedStateTransitionMatrixGenerator(harvester.getStmName(), getAllForceModels(),
524                         getAttitudeProvider());
525             } else {
526                 stmGenerator = new StateTransitionMatrixGenerator(harvester.getStmName(), getAllForceModels(),
527                         getAttitudeProvider());
528             }
529             addAdditionalDerivativesProvider(stmGenerator);
530         }
531 
532         if (!getInitialIntegrationState().hasAdditionalData(harvester.getStmName())) {
533             // add the initial State Transition Matrix if it is not already there
534             // (perhaps due to a previous propagation)
535             setInitialState(stmGenerator.setInitialStateTransitionMatrix(getInitialState(),
536                                                                          harvester.getInitialStateTransitionMatrix(),
537                                                                          getOrbitType(),
538                                                                          getPositionAngleType()));
539         }
540 
541         return stmGenerator;
542 
543     }
544 
545     /** Set up the Jacobians columns generator dedicated to trigger dates.
546      * @param stmGenerator State Transition Matrix generator
547      * @return names of the columns corresponding to trigger dates
548      * @since 13.1
549      */
550     private List<String> setUpTriggerDatesJacobiansColumns(final AbstractStateTransitionMatrixGenerator stmGenerator) {
551 
552         final String stmName = stmGenerator.getName();
553         final boolean isMassInStm = stmGenerator instanceof ExtendedStateTransitionMatrixGenerator;
554         final List<String> names = new ArrayList<>();
555         for (final ForceModel forceModel : getAllForceModels()) {
556             if (forceModel instanceof Maneuver maneuver && ((Maneuver) forceModel).getManeuverTriggers() instanceof ResettableManeuverTriggers) {
557                 final ResettableManeuverTriggers maneuverTriggers = (ResettableManeuverTriggers) maneuver.getManeuverTriggers();
558 
559                 final Collection<EventDetector> selectedDetectors = maneuverTriggers.getEventDetectors().
560                         filter(ManeuverTriggerDetector.class::isInstance).
561                         map(triggerDetector -> ((ManeuverTriggerDetector<?>) triggerDetector).getDetector())
562                         .collect(Collectors.toList());
563                 for (final EventDetector detector: selectedDetectors) {
564                     if (detector instanceof ParameterDrivenDateIntervalDetector d) {
565                         TriggerDate start;
566                         TriggerDate stop;
567 
568                         if (d.getStartDriver().isSelected() || d.getMedianDriver().isSelected() || d.getDurationDriver().isSelected()) {
569                             // normally datedriver should have only 1 span but just in case the user defines several span, there will
570                             // be no problem here
571                             for (Span<String> span = d.getStartDriver().getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
572                                 start = manageTriggerDate(stmName, maneuver, maneuverTriggers, span.getData(), true,
573                                         d.getThreshold(), isMassInStm);
574                                 names.add(start.getName());
575                                 start = null;
576                             }
577                         }
578                         if (d.getStopDriver().isSelected() || d.getMedianDriver().isSelected() || d.getDurationDriver().isSelected()) {
579                             // normally datedriver should have only 1 span but just in case the user defines several span, there will
580                             // be no problem here
581                             for (Span<String> span = d.getStopDriver().getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
582                                 stop = manageTriggerDate(stmName, maneuver, maneuverTriggers, span.getData(), false,
583                                         d.getThreshold(), isMassInStm);
584                                 names.add(stop.getName());
585                                 stop = null;
586                             }
587                         }
588                         if (d.getMedianDriver().isSelected()) {
589                             // for first span
590                             Span<String> currentMedianNameSpan = d.getMedianDriver().getNamesSpanMap().getFirstSpan();
591                             MedianDate median =
592                                     manageMedianDate(d.getStartDriver().getNamesSpanMap().getFirstSpan().getData(),
593                                             d.getStopDriver().getNamesSpanMap().getFirstSpan().getData(), currentMedianNameSpan.getData());
594                             names.add(median.getName());
595                             // for all span
596                             // normally datedriver should have only 1 span but just in case the user defines several span, there will
597                             // be no problem here. /!\ medianDate driver, startDate driver and stopDate driver must have same span number
598                             for (int spanNumber = 1; spanNumber < d.getMedianDriver().getNamesSpanMap().getSpansNumber(); ++spanNumber) {
599                                 currentMedianNameSpan = d.getMedianDriver().getNamesSpanMap().getSpan(currentMedianNameSpan.getEnd());
600                                 median =
601                                         manageMedianDate(d.getStartDriver().getNamesSpanMap().getSpan(currentMedianNameSpan.getStart()).getData(),
602                                                 d.getStopDriver().getNamesSpanMap().getSpan(currentMedianNameSpan.getStart()).getData(),
603                                                 currentMedianNameSpan.getData());
604                                 names.add(median.getName());
605 
606                             }
607 
608                         }
609                         if (d.getDurationDriver().isSelected()) {
610                             // for first span
611                             Span<String> currentDurationNameSpan = d.getDurationDriver().getNamesSpanMap().getFirstSpan();
612                             Duration duration =
613                                     manageManeuverDuration(d.getStartDriver().getNamesSpanMap().getFirstSpan().getData(),
614                                             d.getStopDriver().getNamesSpanMap().getFirstSpan().getData(), currentDurationNameSpan.getData());
615                             names.add(duration.getName());
616                             // for all span
617                             for (int spanNumber = 1; spanNumber < d.getDurationDriver().getNamesSpanMap().getSpansNumber(); ++spanNumber) {
618                                 currentDurationNameSpan = d.getDurationDriver().getNamesSpanMap().getSpan(currentDurationNameSpan.getEnd());
619                                 duration =
620                                         manageManeuverDuration(d.getStartDriver().getNamesSpanMap().getSpan(currentDurationNameSpan.getStart()).getData(),
621                                                 d.getStopDriver().getNamesSpanMap().getSpan(currentDurationNameSpan.getStart()).getData(),
622                                                 currentDurationNameSpan.getData());
623                                 names.add(duration.getName());
624 
625                             }
626                         }
627                     }
628                 }
629             }
630         }
631 
632         return names;
633 
634     }
635 
636     /** Manage a maneuver trigger date.
637      * @param stmName name of the State Transition Matrix state
638      * @param maneuver maneuver force model
639      * @param mt trigger to which the driver is bound
640      * @param driverName name of the date driver
641      * @param start if true, the driver is a maneuver start
642      * @param threshold event detector threshold
643      * @param isMassInStm flag on presence on mass in STM
644      * @return generator for the date driver
645      * @since 13.1
646      */
647     private TriggerDate manageTriggerDate(final String stmName,
648                                           final Maneuver maneuver,
649                                           final ResettableManeuverTriggers mt,
650                                           final String driverName,
651                                           final boolean start,
652                                           final double threshold,
653                                           final boolean isMassInStm) {
654 
655         TriggerDate triggerGenerator = null;
656 
657         // check if we already have set up the provider
658         for (final AdditionalDataProvider<?> provider : getAdditionalDataProviders()) {
659             if (provider instanceof TriggerDate date &&
660                 provider.getName().equals(driverName)) {
661                 // the Jacobian column generator has already been set up in a previous propagation
662                 triggerGenerator = date;
663                 break;
664             }
665         }
666 
667         if (triggerGenerator == null) {
668             // this is the first time we need the Jacobian column generator, create it
669             if (isMassInStm) {
670                 triggerGenerator = new TriggerDate(stmName, driverName, start, maneuver, threshold, true);
671             } else {
672                 final Optional<ForceModel> dragForce = getAllForceModels().stream().filter(AbstractDragForceModel.class::isInstance).findFirst();
673                 final Optional<ForceModel> srpForce = getAllForceModels().stream().filter(RadiationForceModel.class::isInstance).findFirst();
674                 final List<ForceModel> nonGravitationalForces = new ArrayList<>();
675                 dragForce.ifPresent(nonGravitationalForces::add);
676                 srpForce.ifPresent(nonGravitationalForces::add);
677                 triggerGenerator = new TriggerDate(stmName, driverName, start, maneuver, threshold, false,
678                         nonGravitationalForces.toArray(new ForceModel[0]));
679             }
680             mt.addResetter(triggerGenerator);
681             final MassDepletionDelay depletionDelay = triggerGenerator.getMassDepletionDelay();
682             if (depletionDelay != null) {
683                 addAdditionalDerivativesProvider(depletionDelay);
684             }
685             addAdditionalDataProvider(triggerGenerator);
686         }
687 
688         if (!getInitialIntegrationState().hasAdditionalData(driverName)) {
689             // add the initial Jacobian column if it is not already there
690             // (perhaps due to a previous propagation)
691             final MassDepletionDelay depletionDelay = triggerGenerator.getMassDepletionDelay();
692             final double[] zeroes = new double[depletionDelay == null ? 7 : 6];
693             if (depletionDelay != null) {
694                 setInitialColumn(depletionDelay.getName(), zeroes);
695             }
696             setInitialColumn(driverName, getHarvester().getInitialJacobianColumn(driverName));
697         }
698 
699         return triggerGenerator;
700 
701     }
702 
703     /** Manage a maneuver median date.
704      * @param startName name of the start driver
705      * @param stopName name of the stop driver
706      * @param medianName name of the median driver
707      * @return generator for the median driver
708      * @since 11.1
709      */
710     private MedianDate manageMedianDate(final String startName, final String stopName, final String medianName) {
711 
712         MedianDate medianGenerator = null;
713 
714         // check if we already have set up the provider
715         for (final AdditionalDataProvider<?> provider : getAdditionalDataProviders()) {
716             if (provider instanceof MedianDate date &&
717                 provider.getName().equals(medianName)) {
718                 // the Jacobian column generator has already been set up in a previous propagation
719                 medianGenerator = date;
720                 break;
721             }
722         }
723 
724         if (medianGenerator == null) {
725             // this is the first time we need the Jacobian column generator, create it
726             medianGenerator = new MedianDate(startName, stopName, medianName);
727             addAdditionalDataProvider(medianGenerator);
728         }
729 
730         if (!getInitialIntegrationState().hasAdditionalData(medianName)) {
731             // add the initial Jacobian column if it is not already there
732             // (perhaps due to a previous propagation)
733             setInitialColumn(medianName, getHarvester().getInitialJacobianColumn(medianName));
734         }
735 
736         return medianGenerator;
737 
738     }
739 
740     /** Manage a maneuver duration.
741      * @param startName name of the start driver
742      * @param stopName name of the stop driver
743      * @param durationName name of the duration driver
744      * @return generator for the median driver
745      * @since 11.1
746      */
747     private Duration manageManeuverDuration(final String startName, final String stopName, final String durationName) {
748 
749         Duration durationGenerator = null;
750 
751         // check if we already have set up the provider
752         for (final AdditionalDataProvider<?> provider : getAdditionalDataProviders()) {
753             if (provider instanceof Duration duration &&
754                 provider.getName().equals(durationName)) {
755                 // the Jacobian column generator has already been set up in a previous propagation
756                 durationGenerator = duration;
757                 break;
758             }
759         }
760 
761         if (durationGenerator == null) {
762             // this is the first time we need the Jacobian column generator, create it
763             durationGenerator = new Duration(startName, stopName, durationName);
764             addAdditionalDataProvider(durationGenerator);
765         }
766 
767         if (!getInitialIntegrationState().hasAdditionalData(durationName)) {
768             // add the initial Jacobian column if it is not already there
769             // (perhaps due to a previous propagation)
770             setInitialColumn(durationName, getHarvester().getInitialJacobianColumn(durationName));
771         }
772 
773         return durationGenerator;
774 
775     }
776 
777     /** Set up the Jacobians columns generator for regular parameters.
778      * @param stmGenerator generator for the State Transition Matrix
779      * @param triggerDates names of the columns already managed as trigger dates
780      * @since 11.1
781      */
782     private void setUpRegularParametersJacobiansColumns(final AbstractStateTransitionMatrixGenerator stmGenerator,
783                                                         final List<String> triggerDates) {
784 
785         // first pass: gather all parameters (excluding trigger dates), binding similar names together
786         final ParameterDriversList selected = new ParameterDriversList();
787         for (final ForceModel forceModel : getAllForceModels()) {
788             for (final ParameterDriver driver : forceModel.getParametersDrivers()) {
789                 if (!triggerDates.contains(driver.getNamesSpanMap().getFirstSpan().getData())) {
790                     // if the first span is not in triggerDates,
791                     // it means that the driver is not a trigger date and can be selected here
792                     selected.add(driver);
793                 }
794             }
795         }
796 
797         // second pass: now that shared parameter names are bound together,
798         // their selections status have been synchronized, we can filter them
799         selected.filter(true);
800 
801         // third pass: sort parameters lexicographically
802         selected.sort();
803 
804         // add the Jacobians column generators corresponding to parameters, and setup state accordingly
805         // a new column is needed for each value estimated so for each span of the parameterDriver
806         for (final DelegatingDriver driver : selected.getDrivers()) {
807 
808             for (Span<String> currentNameSpan = driver.getNamesSpanMap().getFirstSpan(); currentNameSpan != null; currentNameSpan = currentNameSpan.next()) {
809 
810                 IntegrableJacobianColumnGenerator generator = null;
811                 // check if we already have set up the providers
812                 for (final AdditionalDerivativesProvider provider : getAdditionalDerivativesProviders()) {
813                     if (provider instanceof IntegrableJacobianColumnGenerator columnGenerator &&
814                         provider.getName().equals(currentNameSpan.getData())) {
815                         // the Jacobian column generator has already been set up in a previous propagation
816                         generator = columnGenerator;
817                         break;
818                     }
819 
820                 }
821 
822                 if (generator == null) {
823                     // this is the first time we need the Jacobian column generator, create it
824                     final boolean isMassIncluded = stmGenerator.getStateDimension() == 7;
825                     generator = new IntegrableJacobianColumnGenerator(stmGenerator, currentNameSpan.getData(), isMassIncluded);
826                     addAdditionalDerivativesProvider(generator);
827                 }
828 
829                 if (!getInitialIntegrationState().hasAdditionalData(currentNameSpan.getData())) {
830                     // add the initial Jacobian column if it is not already there
831                     // (perhaps due to a previous propagation)
832                     setInitialColumn(currentNameSpan.getData(), getHarvester().getInitialJacobianColumn(currentNameSpan.getData()));
833                 }
834 
835             }
836 
837         }
838 
839     }
840 
841     /** Add the initial value of the column to the initial state.
842      * <p>
843      * The initial state must already contain the Cartesian State Transition Matrix.
844      * </p>
845      * @param columnName name of the column
846      * @param dYdQ column of the Jacobian ∂Y/∂qₘ with respect to propagation type,
847      * if null (which is the most frequent case), assumed to be 0
848      * @since 11.1
849      */
850     private void setInitialColumn(final String columnName, final double[] dYdQ) {
851 
852         final SpacecraftState state = getInitialState();
853 
854         final AbstractStateTransitionMatrixGenerator generator = (AbstractStateTransitionMatrixGenerator)
855                 getAdditionalDerivativesProviders().stream()
856                         .filter(AbstractStateTransitionMatrixGenerator.class::isInstance)
857                         .collect(Collectors.toList()).getFirst();
858         final int expectedSize = generator.getStateDimension();
859         if (dYdQ.length != expectedSize) {
860             throw new OrekitException(LocalizedCoreFormats.DIMENSIONS_MISMATCH, dYdQ.length, expectedSize);
861         }
862 
863         // convert to Cartesian Jacobian
864         final RealMatrix dYdC = MatrixUtils.createRealIdentityMatrix(expectedSize);
865         final double[][] jacobian = new double[6][6];
866         getOrbitType().convertType(state.getOrbit()).getJacobianWrtCartesian(getPositionAngleType(), jacobian);
867         dYdC.setSubMatrix(jacobian, 0, 0);
868         final DecompositionSolver solver = getSolver(dYdC);
869         final double[] column = solver.solve(MatrixUtils.createRealVector(dYdQ)).toArray();
870 
871         // set additional state
872         setInitialState(state.addAdditionalData(columnName, column));
873 
874     }
875 
876     /**
877      * Method to get a linear system solver.
878      * @param matrix matrix involved in linear systems
879      * @return solver
880      * @since 13.1
881      */
882     private DecompositionSolver getSolver(final RealMatrix matrix) {
883         return new QRDecomposition(matrix, THRESHOLD).getSolver();
884     }
885 
886     /** {@inheritDoc} */
887     @Override
888     protected AttitudeProvider initializeAttitudeProviderForDerivatives() {
889         return needFullAttitudeForDerivatives ? getAttitudeProvider() : getFrozenAttitudeProvider();
890     }
891 
892     /** {@inheritDoc} */
893     @Override
894     protected StateMapper createMapper(final AbsoluteDate referenceDate, final double mu,
895                                        final OrbitType orbitType, final PositionAngleType positionAngleType,
896                                        final AttitudeProvider attitudeProvider, final Frame frame) {
897         return new OsculatingMapper(referenceDate, mu, orbitType, positionAngleType, attitudeProvider, frame);
898     }
899 
900     /** Internal mapper using directly osculating parameters. */
901     private static class OsculatingMapper extends StateMapper {
902 
903         /** Simple constructor.
904          * <p>
905          * The position parameter type is meaningful only if {@link
906          * #getOrbitType() propagation orbit type}
907          * support it. As an example, it is not meaningful for propagation
908          * in {@link OrbitType#CARTESIAN Cartesian} parameters.
909          * </p>
910          * @param referenceDate reference date
911          * @param mu central attraction coefficient (m³/s²)
912          * @param orbitType orbit type to use for mapping (can be null for {@link AbsolutePVCoordinates})
913          * @param positionAngleType angle type to use for propagation
914          * @param attitudeProvider attitude provider
915          * @param frame inertial frame
916          */
917         OsculatingMapper(final AbsoluteDate referenceDate, final double mu,
918                          final OrbitType orbitType, final PositionAngleType positionAngleType,
919                          final AttitudeProvider attitudeProvider, final Frame frame) {
920             super(referenceDate, mu, orbitType, positionAngleType, attitudeProvider, frame);
921         }
922 
923         /** {@inheritDoc} */
924         @Override
925         public SpacecraftState mapArrayToState(final AbsoluteDate date, final double[] y, final double[] yDot,
926                                                final PropagationType type) {
927             // the parameter type is ignored for the Numerical Propagator
928 
929             final double mass = y[6];
930             final double massRate = yDot == null ? 0. : yDot[6];
931             if (mass <= 0.0) {
932                 throw new OrekitException(OrekitMessages.NOT_POSITIVE_SPACECRAFT_MASS, mass);
933             }
934 
935             if (super.getOrbitType() == null) {
936                 // propagation uses absolute position-velocity-acceleration
937                 final Vector3D p = new Vector3D(y[0],    y[1],    y[2]);
938                 final Vector3D v = new Vector3D(y[3],    y[4],    y[5]);
939                 final Vector3D a;
940                 final AbsolutePVCoordinates absPva;
941                 if (yDot == null) {
942                     absPva = new AbsolutePVCoordinates(getFrame(), new TimeStampedPVCoordinates(date, p, v));
943                 } else {
944                     a = new Vector3D(yDot[3], yDot[4], yDot[5]);
945                     absPva = new AbsolutePVCoordinates(getFrame(), new TimeStampedPVCoordinates(date, p, v, a));
946                 }
947 
948                 final Attitude attitude = getAttitudeProvider().getAttitude(absPva, date, getFrame());
949                 return new SpacecraftState(absPva, attitude).withMassRate(massRate).withMass(mass);
950             } else {
951                 // propagation uses regular orbits
952                 final Orbit orbit       = super.getOrbitType().mapArrayToOrbit(y, yDot, super.getPositionAngleType(), date, getMu(), getFrame());
953                 final Attitude attitude = getAttitudeProvider().getAttitude(orbit, date, getFrame());
954 
955                 return new SpacecraftState(orbit, attitude).withMassRate(massRate).withMass(mass);
956             }
957 
958         }
959 
960         /** {@inheritDoc} */
961         public void mapStateToArray(final SpacecraftState state, final double[] y, final double[] yDot) {
962             if (super.getOrbitType() == null) {
963                 // propagation uses absolute position-velocity-acceleration
964                 final Vector3D p = state.getAbsPVA().getPosition();
965                 final Vector3D v = state.getAbsPVA().getVelocity();
966                 y[0] = p.getX();
967                 y[1] = p.getY();
968                 y[2] = p.getZ();
969                 y[3] = v.getX();
970                 y[4] = v.getY();
971                 y[5] = v.getZ();
972                 y[6] = state.getMass();
973             }
974             else {
975                 super.getOrbitType().mapOrbitToArray(state.getOrbit(), super.getPositionAngleType(), y, yDot);
976                 y[6] = state.getMass();
977             }
978         }
979 
980     }
981 
982     /** {@inheritDoc} */
983     protected MainStateEquations getMainStateEquations(final ODEIntegrator integrator) {
984         return new Main(integrator, getOrbitType(), getPositionAngleType(), getAllForceModels());
985     }
986 
987     /** Internal class for osculating parameters integration. */
988     private class Main extends NumericalTimeDerivativesEquations implements MainStateEquations {
989 
990         /** Flag keeping track whether Jacobian matrix needs to be recomputed or not. */
991         private final boolean recomputingJacobian;
992 
993         /** Simple constructor.
994          * @param integrator numerical integrator to use for propagation.
995          * @param orbitType orbit type
996          * @param positionAngleType angle type
997          * @param forceModelList forces
998          */
999         Main(final ODEIntegrator integrator, final OrbitType orbitType, final PositionAngleType positionAngleType,
1000              final List<ForceModel> forceModelList) {
1001 
1002             super(orbitType, positionAngleType, forceModelList);
1003             final int numberOfForces = forceModelList.size();
1004             if (orbitType != null && orbitType != OrbitType.CARTESIAN && numberOfForces > 0) {
1005                 if (numberOfForces > 1) {
1006                     recomputingJacobian = true;
1007                 } else {
1008                     recomputingJacobian = !(forceModelList.getFirst() instanceof NewtonianAttraction);
1009                 }
1010             } else {
1011                 recomputingJacobian = false;
1012             }
1013 
1014             // feed internal event detectors
1015             setUpInternalDetectors(integrator);
1016 
1017         }
1018 
1019         /** Set up all user defined event detectors.
1020          * @param integrator numerical integrator to use for propagation.
1021          */
1022         private void setUpInternalDetectors(final ODEIntegrator integrator) {
1023             final NumericalTimeDerivativesEquations cartesianEquations = new NumericalTimeDerivativesEquations(OrbitType.CARTESIAN,
1024                     null, forceModels);
1025             final List<FieldEventDetector<Gradient>> fieldDetectors = new ArrayList<>();
1026             if (getHarvester() != null) {
1027                 final GradientField field = GradientField.getField(getHarvester().getStateDimension() + 1);
1028                 getForceModels().stream().flatMap(forceModel -> forceModel.getFieldEventDetectors(field))
1029                         .filter(fieldEventDetector -> !fieldEventDetector.getEventFunction().dependsOnTimeOnly())
1030                         .forEach(fieldDetectors::add);
1031                 getAttitudeProvider().getFieldEventDetectors(field)
1032                         .filter(fieldEventDetector -> !fieldEventDetector.getEventFunction().dependsOnTimeOnly())
1033                         .forEach(fieldDetectors::add);
1034             }
1035             for (final ForceModel forceModel : getForceModels()) {
1036                 forceModel.getEventDetectors().forEach(detector -> setUpInternalEventDetector(integrator, detector,
1037                         cartesianEquations, fieldDetectors));
1038             }
1039             getAttitudeProvider().getEventDetectors().forEach(detector -> setUpInternalEventDetector(integrator,
1040                     detector, cartesianEquations, fieldDetectors));
1041         }
1042 
1043         /** Set up one internal event detector.
1044          * @param integrator numerical integrator to use for propagation.
1045          * @param eventDetector detector
1046          * @param cartesianEquations Cartesian derivatives model
1047          * @param fieldDetectors detectors for Taylor differential algebra
1048          */
1049         private void setUpInternalEventDetector(final ODEIntegrator integrator,
1050                                                 final EventDetector eventDetector,
1051                                                 final NumericalTimeDerivativesEquations cartesianEquations,
1052                                                 final List<FieldEventDetector<Gradient>> fieldDetectors) {
1053             final EventDetector internalDetector;
1054             if (!fieldDetectors.isEmpty() && !eventDetector.getEventFunction().dependsOnTimeOnly()) {
1055                 // need to modify STM at each dynamics discontinuities
1056                 final NumericalPropagationHarvester harvester = (NumericalPropagationHarvester) getHarvester();
1057                 final SwitchEventHandler handler = new SwitchEventHandler(eventDetector.getHandler(), harvester,
1058                         cartesianEquations, getAttitudeProvider(), fieldDetectors);
1059                 internalDetector = getLocalDetector(eventDetector, handler);
1060             } else {
1061                 internalDetector = eventDetector;
1062             }
1063             setUpEventDetector(integrator, internalDetector);
1064         }
1065 
1066         /** {@inheritDoc} */
1067         @Override
1068         public void init(final SpacecraftState initialState, final AbsoluteDate target) {
1069             final List<ForceModel> forceModelList = getForceModels();
1070             needFullAttitudeForDerivatives = forceModelList.stream().anyMatch(ForceModel::dependsOnAttitudeRate);
1071 
1072             forceModelList.forEach(fm -> fm.init(initialState, target));
1073 
1074         }
1075 
1076         /** {@inheritDoc} */
1077         @Override
1078         public double[] computeDerivatives(final SpacecraftState state) {
1079             setCurrentState(state);
1080             if (recomputingJacobian) {
1081                 // propagation uses Jacobian matrix of orbital parameters w.r.t. Cartesian ones
1082                 final double[][] jacobian = new double[6][6];
1083                 state.getOrbit().getJacobianWrtCartesian(getPositionAngleType(), jacobian);
1084                 setCoordinatesJacobian(jacobian);
1085             }
1086             return computeTimeDerivatives(state);
1087 
1088         }
1089 
1090     }
1091 
1092     /** {@inheritDoc} */
1093     @Override
1094     protected void beforeIntegration(final SpacecraftState initialState, final AbsoluteDate tEnd) {
1095 
1096         if (!getFrame().isPseudoInertial()) {
1097 
1098             // inspect all force models to find InertialForces
1099             for (ForceModel force : forceModels) {
1100                 if (force instanceof InertialForces) {
1101                     return;
1102                 }
1103             }
1104 
1105             // throw exception if no inertial forces found
1106             throw new OrekitException(OrekitMessages.INERTIAL_FORCE_MODEL_MISSING, getFrame().getName());
1107 
1108         }
1109 
1110     }
1111 
1112     /**
1113      * Creates local detector wrapping input one and using specific handler for dynamics discontinuities and STM.
1114      * @param eventDetector detector
1115      * @param switchEventHandler special handler
1116      * @return wrapped detector
1117      */
1118     private static EventDetector getLocalDetector(final EventDetector eventDetector,
1119                                                   final SwitchEventHandler switchEventHandler) {
1120         return new DetectorModifier() {
1121             @Override
1122             public EventDetector getDetector() {
1123                 return eventDetector;
1124             }
1125 
1126             @Override
1127             public EventHandler getHandler() {
1128                 return switchEventHandler;
1129             }
1130         };
1131     }
1132 }