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