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