1   /* Copyright 2002-2025 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.propagation.numerical;
18  
19  import org.hipparchus.CalculusFieldElement;
20  import org.hipparchus.Field;
21  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
22  import org.hipparchus.geometry.euclidean.threed.Vector3D;
23  import org.hipparchus.linear.MatrixUtils;
24  import org.hipparchus.linear.RealMatrix;
25  import org.hipparchus.linear.RealVector;
26  import org.hipparchus.ode.ODEIntegrator;
27  import org.hipparchus.util.FastMath;
28  import org.junit.jupiter.api.Assertions;
29  import org.junit.jupiter.api.BeforeEach;
30  import org.junit.jupiter.api.Test;
31  import org.junit.jupiter.params.ParameterizedTest;
32  import org.junit.jupiter.params.provider.EnumSource;
33  import org.junit.jupiter.params.provider.ValueSource;
34  import org.orekit.Utils;
35  import org.orekit.attitudes.AttitudeProvider;
36  import org.orekit.attitudes.FrameAlignedProvider;
37  import org.orekit.attitudes.LofOffset;
38  import org.orekit.bodies.AnalyticalSolarPositionProvider;
39  import org.orekit.data.DataContext;
40  import org.orekit.forces.ForceModel;
41  import org.orekit.forces.gravity.J2OnlyPerturbation;
42  import org.orekit.forces.gravity.ThirdBodyAttraction;
43  import org.orekit.forces.gravity.potential.GravityFieldFactory;
44  import org.orekit.forces.gravity.potential.ICGEMFormatReader;
45  import org.orekit.forces.maneuvers.ConstantThrustManeuver;
46  import org.orekit.forces.maneuvers.Maneuver;
47  import org.orekit.forces.maneuvers.propulsion.BasicConstantThrustPropulsionModel;
48  import org.orekit.forces.maneuvers.propulsion.ProfileThrustPropulsionModel;
49  import org.orekit.forces.maneuvers.propulsion.PropulsionModel;
50  import org.orekit.forces.maneuvers.propulsion.SphericalConstantThrustPropulsionModel;
51  import org.orekit.forces.maneuvers.propulsion.ThrustVectorProvider;
52  import org.orekit.forces.maneuvers.trigger.DateBasedManeuverTriggers;
53  import org.orekit.forces.maneuvers.trigger.IntervalEventTrigger;
54  import org.orekit.forces.maneuvers.trigger.ManeuverTriggers;
55  import org.orekit.forces.radiation.CylindricallyShadowedLightFluxModel;
56  import org.orekit.forces.radiation.IsotropicRadiationSingleCoefficient;
57  import org.orekit.forces.radiation.LightFluxModel;
58  import org.orekit.forces.radiation.RadiationPressureModel;
59  import org.orekit.forces.radiation.RadiationSensitive;
60  import org.orekit.frames.FramesFactory;
61  import org.orekit.frames.LOFType;
62  import org.orekit.orbits.CartesianOrbit;
63  import org.orekit.orbits.EquinoctialOrbit;
64  import org.orekit.orbits.Orbit;
65  import org.orekit.orbits.OrbitType;
66  import org.orekit.orbits.PositionAngleType;
67  import org.orekit.propagation.MatricesHarvester;
68  import org.orekit.propagation.SpacecraftState;
69  import org.orekit.propagation.conversion.DormandPrince54IntegratorBuilder;
70  import org.orekit.propagation.events.ApsideDetector;
71  import org.orekit.propagation.events.EventDetectionSettings;
72  import org.orekit.propagation.events.EventDetector;
73  import org.orekit.propagation.events.FieldApsideDetector;
74  import org.orekit.propagation.events.FieldEventDetectionSettings;
75  import org.orekit.propagation.events.FieldEventDetector;
76  import org.orekit.propagation.events.ParameterDrivenDateIntervalDetector;
77  import org.orekit.propagation.events.handlers.ContinueOnEvent;
78  import org.orekit.propagation.events.handlers.FieldStopOnEvent;
79  import org.orekit.time.AbsoluteDate;
80  import org.orekit.time.FieldAbsoluteDate;
81  import org.orekit.utils.Constants;
82  import org.orekit.utils.PVCoordinates;
83  import org.orekit.utils.ParameterDriver;
84  import org.orekit.utils.TimeSpanMap;
85  
86  import java.util.Arrays;
87  import java.util.Collections;
88  import java.util.List;
89  import java.util.stream.Stream;
90  
91  import static org.junit.jupiter.api.Assertions.*;
92  
93  class ExtendedStateTransitionMatrixGeneratorTest {
94  
95      @BeforeEach
96      void setUp() {
97          Utils.setDataRoot("orbit-determination/february-2016:potential/icgem-format");
98          GravityFieldFactory.addPotentialCoefficientsReader(new ICGEMFormatReader("eigen-6s-truncated", true));
99      }
100 
101     @Test
102     void testManeuverTriggerDateParameter() {
103         // GIVEN
104         final NumericalPropagator propagator = buildPropagator(OrbitType.CARTESIAN, null);
105         final LofOffset lofOffset = new LofOffset(propagator.getFrame(), LOFType.TNW);
106         propagator.setAttitudeProvider(lofOffset);
107         final String stmName = "stm";
108         final MatricesHarvester harvester = propagator.setupMatricesComputation(stmName, MatrixUtils.createRealIdentityMatrix(7), null);
109         final double duration = 100.;
110         final double timeOfFlight = duration * 3;
111         final AbsoluteDate epoch = propagator.getInitialState().getDate();
112         final AbsoluteDate targetDate = epoch.shiftedBy(timeOfFlight);
113         final AbsoluteDate medianDate = epoch.shiftedBy(duration * 2);
114         final BasicConstantThrustPropulsionModel propulsionModel = new BasicConstantThrustPropulsionModel(0.1, 10.,
115                 Vector3D.MINUS_I, "man");
116         final AbsoluteDate startDate = medianDate.shiftedBy(-duration/2.);
117         final ConstantThrustManeuver maneuver = new ConstantThrustManeuver(startDate, duration, null, propulsionModel);
118         maneuver.getParameterDriver("man" + ParameterDrivenDateIntervalDetector.START_SUFFIX).setSelected(true);
119         propagator.addForceModel(maneuver);
120         // WHEN
121         final SpacecraftState state = propagator.propagate(targetDate);
122         // THEN
123         final double dP = 1e-1;
124         final NumericalPropagator propagatorFiniteDifference1 = setupPropagator(propagator.getOrbitType(), lofOffset,
125                 startDate, duration, propulsionModel, -dP/2.);
126         final SpacecraftState finiteDifferencesState1 = propagatorFiniteDifference1.propagate(targetDate);
127         final NumericalPropagator propagatorFiniteDifference2 = setupPropagator(propagator.getOrbitType(), lofOffset,
128                 startDate, duration, propulsionModel, dP/2.);
129         final SpacecraftState finiteDifferencesState2 = propagatorFiniteDifference2.propagate(targetDate);
130         final PVCoordinates relativePV = new PVCoordinates(finiteDifferencesState1.getPVCoordinates(),
131                 finiteDifferencesState2.getPVCoordinates());
132         final RealMatrix parameterJacobian = harvester.getParametersJacobian(state);
133         compareWithFiniteDifferences(relativePV, dP, parameterJacobian, 1e-3);
134     }
135 
136     private static NumericalPropagator setupPropagator(final OrbitType orbitType, final AttitudeProvider attitudeProvider,
137                                                          final AbsoluteDate startDate, final double duration,
138                                                          final BasicConstantThrustPropulsionModel propulsionModel,
139                                                          final double dt) {
140         final NumericalPropagator propagator = buildPropagator(orbitType, attitudeProvider);
141         final AbsoluteDate shiftedStartDate = startDate.shiftedBy(dt);
142         final ConstantThrustManeuver maneuver = new ConstantThrustManeuver(shiftedStartDate, duration - dt, null, propulsionModel);
143         propagator.addForceModel(maneuver);
144         return propagator;
145     }
146 
147     @Test
148     void testManeuverPropulsionParameter() {
149         // GIVEN
150         final NumericalPropagator propagator = buildPropagator(OrbitType.CARTESIAN);
151         final String stmName = "stm";
152         final MatricesHarvester harvester = propagator.setupMatricesComputation(stmName, MatrixUtils.createRealIdentityMatrix(7), null);
153         final double thrustMagnitude = 0.1;
154         final Maneuver maneuver = getPermanentManeuver(thrustMagnitude);
155         propagator.addForceModel(maneuver);
156         final double timeOfFlight = 1e3;
157         final AbsoluteDate epoch = propagator.getInitialState().getDate();
158         final AbsoluteDate targetDate = epoch.shiftedBy(timeOfFlight);
159         // WHEN
160         final SpacecraftState state = propagator.propagate(targetDate);
161         // THEN
162         compareMassGradientWithFiniteDifferences(propagator.getInitialState(), maneuver, targetDate,
163                 harvester.getStateTransitionMatrix(state));
164         final RealMatrix parameterJacobian = harvester.getParametersJacobian(state);
165         compareParameterJacobianWithFiniteDifferences(propagator, thrustMagnitude, targetDate, parameterJacobian);
166     }
167 
168     private static void compareParameterJacobianWithFiniteDifferences(final NumericalPropagator propagator,
169                                                                       final double thrustMagnitude,
170                                                                       final AbsoluteDate targetDate,
171                                                                       final RealMatrix parameterJacobian) {
172         final NumericalPropagator propagatorFiniteDifference1 = buildPropagator(propagator.getOrbitType(),
173                 propagator.getAttitudeProvider());
174         final double dP = 1e-5;
175         propagatorFiniteDifference1.addForceModel(getPermanentManeuver((thrustMagnitude - dP / 2.)));
176         final SpacecraftState finiteDifferencesState1 = propagatorFiniteDifference1.propagate(targetDate);
177         final NumericalPropagator propagatorFiniteDifference2 = buildPropagator(propagator.getOrbitType(),
178                 propagator.getAttitudeProvider());
179         propagatorFiniteDifference2.addForceModel(getPermanentManeuver(thrustMagnitude + dP / 2.));
180         final SpacecraftState finiteDifferencesState2 = propagatorFiniteDifference2.propagate(targetDate);
181         final PVCoordinates relativePV = new PVCoordinates(finiteDifferencesState1.getPVCoordinates(),
182                 finiteDifferencesState2.getPVCoordinates());
183         compareWithFiniteDifferences(relativePV, dP, parameterJacobian, 1e-3);
184     }
185 
186     private static void compareWithFiniteDifferences(final PVCoordinates relativePV, final double dP,
187                                                      final RealMatrix parameterJacobian, final double delta) {
188         assertEquals(relativePV.getPosition().getX() / dP, parameterJacobian.getEntry(0, 0), delta);
189         assertEquals(relativePV.getPosition().getY() / dP, parameterJacobian.getEntry(1, 0), delta);
190         assertEquals(relativePV.getPosition().getZ() / dP, parameterJacobian.getEntry(2, 0), delta);
191         assertEquals(relativePV.getVelocity().getX() / dP, parameterJacobian.getEntry(3, 0), delta * 1e-2);
192         assertEquals(relativePV.getVelocity().getY() / dP, parameterJacobian.getEntry(4, 0), delta * 1e-2);
193         assertEquals(relativePV.getVelocity().getZ() / dP, parameterJacobian.getEntry(5, 0), delta * 1e-2);
194     }
195 
196     private static Maneuver getPermanentManeuver(final double thrustMagnitude) {
197         final Vector3D thrustVector = new Vector3D(3, 2).scalarMultiply(thrustMagnitude);
198         final double isp = 100.;
199         final String maneuverName = "man";
200         final SphericalConstantThrustPropulsionModel propulsionModel = new SphericalConstantThrustPropulsionModel(isp,
201                 thrustVector, maneuverName);
202         propulsionModel.getParameterDriver(maneuverName + SphericalConstantThrustPropulsionModel.THRUST_MAGNITUDE).setSelected(true);
203         return new Maneuver(null, new TestManeuverTrigger(), propulsionModel);
204     }
205 
206     private static class TestManeuverTrigger implements ManeuverTriggers {
207 
208         @Override
209         public boolean isFiring(AbsoluteDate date, double[] parameters) {
210             return true;
211         }
212 
213         @Override
214         public <T extends CalculusFieldElement<T>> boolean isFiring(FieldAbsoluteDate<T> date, T[] parameters) {
215             return true;
216         }
217 
218         @Override
219         public Stream<EventDetector> getEventDetectors() {
220             return Stream.empty();
221         }
222 
223         @Override
224         public <T extends CalculusFieldElement<T>> Stream<FieldEventDetector<T>> getFieldEventDetectors(Field<T> field) {
225             return Stream.empty();
226         }
227 
228         @Override
229         public List<ParameterDriver> getParametersDrivers() {
230             return Collections.emptyList();
231         }
232     }
233 
234     @Test
235     void testStm7x7vs6x6Column() {
236         // GIVEN
237         final OrbitType orbitType = OrbitType.CARTESIAN;
238         final NumericalPropagator propagator = buildPropagator(orbitType);
239         final String stmName = "stm";
240         final MatricesHarvester harvester7x7 = propagator.setupMatricesComputation(stmName, MatrixUtils.createRealIdentityMatrix(7), null);
241         final double timeOfFlight = 1e5;
242         final AbsoluteDate epoch = propagator.getInitialState().getDate();
243         final AbsoluteDate targetDate = epoch.shiftedBy(timeOfFlight);
244         final ForceModel force = new ThirdBodyAttraction(new AnalyticalSolarPositionProvider(DataContext.getDefault()),
245                 "sun", Constants.JPL_SSD_SUN_GM);
246         force.getParametersDrivers().get(0).setSelected(true);
247         propagator.addForceModel(force);
248         // WHEN
249         final SpacecraftState state = propagator.propagate(targetDate);
250         final RealMatrix actualJacobian = harvester7x7.getParametersJacobian(state);
251         // THEN
252         final NumericalPropagator otherPropagator = buildPropagator(orbitType);
253         otherPropagator.addForceModel(force);
254         final MatricesHarvester harvester6x6 = otherPropagator.setupMatricesComputation(stmName, MatrixUtils.createRealIdentityMatrix(6), null);
255         final SpacecraftState otherState = otherPropagator.propagate(targetDate);
256         final RealMatrix expectedJacobian = harvester6x6.getParametersJacobian(otherState);
257         assertArrayEquals(expectedJacobian.getColumn(0), Arrays.copyOfRange(actualJacobian.getColumn(0), 0, 6));
258     }
259 
260     @ParameterizedTest
261     @EnumSource(OrbitType.class)
262     void testStm7x7vs6x6J2(final OrbitType orbitType) {
263         // GIVEN
264         final NumericalPropagator propagator = buildPropagator(orbitType);
265         final String stmName = "stm";
266         final MatricesHarvester harvester7x7 = propagator.setupMatricesComputation(stmName, MatrixUtils.createRealIdentityMatrix(7), null);
267         final double timeOfFlight = 1e5;
268         final AbsoluteDate epoch = propagator.getInitialState().getDate();
269         final AbsoluteDate targetDate = epoch.shiftedBy(timeOfFlight);
270         final ForceModel j2Perturbation = new J2OnlyPerturbation(GravityFieldFactory.getUnnormalizedProvider(2, 0),
271                 FramesFactory.getGTOD(true));
272         propagator.addForceModel(j2Perturbation);
273         // WHEN
274         final SpacecraftState state = propagator.propagate(targetDate);
275         final RealMatrix actualStm = harvester7x7.getStateTransitionMatrix(state);
276         // THEN
277         final NumericalPropagator otherPropagator = buildPropagator(orbitType);
278         otherPropagator.addForceModel(j2Perturbation);
279         final MatricesHarvester harvester6x6 = otherPropagator.setupMatricesComputation(stmName, MatrixUtils.createRealIdentityMatrix(6), null);
280         final SpacecraftState otherState = otherPropagator.propagate(targetDate);
281         final RealMatrix expectedStm = harvester6x6.getStateTransitionMatrix(otherState);
282         for (int i = 0; i < 6; i++) {
283             assertArrayEquals(expectedStm.getRow(i), Arrays.copyOfRange(actualStm.getRow(i), 0, 6));
284         }
285     }
286 
287     @Test
288     void testParameterJacobian7x7vs6x6ProfileThrust() {
289         // GIVEN
290         final NumericalPropagator propagator = buildPropagator(OrbitType.CARTESIAN);
291         final String stmName = "stm";
292         final MatricesHarvester harvester7x7 = propagator.setupMatricesComputation(stmName, MatrixUtils.createRealIdentityMatrix(7), null);
293         final double timeOfFlight = 1e3;
294         final AbsoluteDate epoch = propagator.getInitialState().getDate();
295         final AbsoluteDate targetDate = epoch.shiftedBy(timeOfFlight);
296         final double slopeThrust = 2e-2;
297         final ThrustVectorProvider thrustVectorProvider = getThrustVector(epoch, slopeThrust);
298         final ProfileThrustPropulsionModel propulsionModel = buildProfileModel(thrustVectorProvider);
299         final AbsoluteDate startDate = epoch.shiftedBy(5e2);
300         final double duration = 3e2;
301         final Maneuver maneuver = new Maneuver(null, buildDatedBasedTriggers(startDate, duration, 0.), propulsionModel);
302         final String parameterName = "triggers" + ParameterDrivenDateIntervalDetector.DURATION_SUFFIX;
303         maneuver.getParameterDriver(parameterName).setSelected(true);
304         propagator.addForceModel(maneuver);
305         // WHEN
306         final SpacecraftState state = propagator.propagate(targetDate);
307         final RealMatrix actualJacobian = harvester7x7.getParametersJacobian(state);
308         // THEN
309         final NumericalPropagator otherPropagator = buildPropagator(propagator.getOrbitType(), propagator.getAttitudeProvider());
310         otherPropagator.addForceModel(new Maneuver(null, buildDatedBasedTriggers(startDate, duration, 0.), propulsionModel));
311         otherPropagator.getAllForceModels().get(0).getParameterDriver(parameterName).setSelected(true);
312         final MatricesHarvester harvester6x6 = otherPropagator.setupMatricesComputation(stmName, MatrixUtils.createRealIdentityMatrix(6), null);
313         final SpacecraftState otherState = otherPropagator.propagate(targetDate);
314         final RealMatrix expectedJacobian = harvester6x6.getParametersJacobian(otherState);
315         assertArrayEquals(expectedJacobian.getColumn(0), Arrays.copyOfRange(actualJacobian.getColumn(0), 0, 6), 0.1);
316     }
317 
318     @Test
319     void testManeuverTriggerDateParameterWithProfileThrust() {
320         // GIVEN
321         final NumericalPropagator propagator = buildPropagator(OrbitType.CARTESIAN, null);
322         final LofOffset lofOffset = new LofOffset(propagator.getFrame(), LOFType.TNW);
323         propagator.setAttitudeProvider(lofOffset);
324         final String stmName = "stm";
325         final MatricesHarvester harvester = propagator.setupMatricesComputation(stmName, MatrixUtils.createRealIdentityMatrix(7), null);
326         final double duration = 100.;
327         final double timeOfFlight = duration * 3;
328         final AbsoluteDate epoch = propagator.getInitialState().getDate();
329         final AbsoluteDate targetDate = epoch.shiftedBy(timeOfFlight);
330         final AbsoluteDate startDate = epoch.shiftedBy(duration/2.);
331         final double slopeThrust = 1e-3;
332         final ThrustVectorProvider thrustVectorProvider = getThrustVector(epoch, slopeThrust);
333         final ProfileThrustPropulsionModel propulsionModel = buildProfileModel(thrustVectorProvider);
334         final Maneuver maneuver = new Maneuver(null, buildDatedBasedTriggers(startDate, duration, 0.), propulsionModel);
335         maneuver.getParameterDriver("triggers" + ParameterDrivenDateIntervalDetector.STOP_SUFFIX).setSelected(true);
336         propagator.addForceModel(maneuver);
337         // WHEN
338         final SpacecraftState state = propagator.propagate(targetDate);
339         // THEN
340         final double dT = 1e-1;
341         final NumericalPropagator propagatorFiniteDifference1 = buildPropagator(propagator.getOrbitType(), lofOffset);
342         final double shiftBackward = -dT /2.;
343         propagatorFiniteDifference1.addForceModel(new Maneuver(null, buildDatedBasedTriggers(startDate,
344                 duration, shiftBackward), buildProfileModel(thrustVectorProvider)));
345         final SpacecraftState finiteDifferencesState1 = propagatorFiniteDifference1.propagate(targetDate);
346         final NumericalPropagator propagatorFiniteDifference2 = buildPropagator(propagator.getOrbitType(), lofOffset);
347         final double shiftForward = dT /2.;
348         propagatorFiniteDifference2.addForceModel(new Maneuver(null, buildDatedBasedTriggers(startDate,
349                 duration, shiftForward), buildProfileModel(thrustVectorProvider)));
350         final SpacecraftState finiteDifferencesState2 = propagatorFiniteDifference2.propagate(targetDate);
351         final PVCoordinates relativePV = new PVCoordinates(finiteDifferencesState1.getPVCoordinates(),
352                 finiteDifferencesState2.getPVCoordinates());
353         final RealMatrix parameterJacobian = harvester.getParametersJacobian(state);
354         compareWithFiniteDifferences(relativePV, dT, parameterJacobian, 1e-3);
355     }
356 
357     private static DateBasedManeuverTriggers buildDatedBasedTriggers(final AbsoluteDate startDate,
358                                                                      final double duration, final double shiftEnd) {
359         return new DateBasedManeuverTriggers("triggers", startDate, duration + shiftEnd);
360     }
361 
362     private static ProfileThrustPropulsionModel buildProfileModel(final ThrustVectorProvider thrustVectorProvider) {
363         final TimeSpanMap<ThrustVectorProvider> providerTimeSpanMap = new TimeSpanMap<>(null);
364         providerTimeSpanMap.addValidBetween(thrustVectorProvider, AbsoluteDate.PAST_INFINITY, AbsoluteDate.FUTURE_INFINITY);
365         return new ProfileThrustPropulsionModel(providerTimeSpanMap, 10.,"man");
366     }
367 
368     private static ThrustVectorProvider getThrustVector(final AbsoluteDate referenceDate, final double slope) {
369         return new ThrustVectorProvider() {
370             @Override
371             public Vector3D getThrustVector(AbsoluteDate date, double mass) {
372                 final double factor = slope * FastMath.abs(date.durationFrom(referenceDate));
373                 return Vector3D.PLUS_I.scalarMultiply(factor);
374             }
375 
376             @Override
377             public <T extends CalculusFieldElement<T>> FieldVector3D<T> getThrustVector(FieldAbsoluteDate<T> date, T mass) {
378                 return new FieldVector3D<>(mass.getField(), getThrustVector(date.toAbsoluteDate(), mass.getReal()));
379             }
380         };
381     }
382 
383     private static NumericalPropagator buildPropagator(final OrbitType orbitType) {
384         final NumericalPropagator propagator = buildPropagator(orbitType, null);
385         propagator.setAttitudeProvider(new FrameAlignedProvider(propagator.getFrame()));
386         return propagator;
387     }
388 
389     private static NumericalPropagator buildPropagator(final OrbitType orbitType, final AttitudeProvider attitudeProvider) {
390         final Orbit orbit = new EquinoctialOrbit(7e6, 0.001, 0.001, 1., 2., 3., PositionAngleType.MEAN,
391                 FramesFactory.getGCRF(), AbsoluteDate.ARBITRARY_EPOCH, Constants.EGM96_EARTH_MU);
392         final ODEIntegrator integrator = new DormandPrince54IntegratorBuilder(1e-3, 50., 1e-4).buildIntegrator(orbit, orbitType);
393         final NumericalPropagator propagator = new NumericalPropagator(integrator, attitudeProvider);
394         propagator.setOrbitType(orbitType);
395         propagator.setResetAtEnd(false);
396         propagator.setInitialState(new SpacecraftState(orbit));
397         return propagator;
398     }
399 
400     private static void compareMassGradientWithFiniteDifferences(final SpacecraftState state,
401                                                                  final ForceModel forceModel,
402                                                                  final AbsoluteDate targetDate,
403                                                                  final RealMatrix stateTransitionMatrix) {
404         final double dM = 1.;
405         final NumericalPropagator propagator1 = buildPropagator(OrbitType.CARTESIAN);
406         propagator1.resetInitialState(state.withMass(state.getMass() - dM/2.));
407         propagator1.addForceModel(forceModel);
408         final SpacecraftState propagated1 = propagator1.propagate(targetDate);
409         final NumericalPropagator propagator2 = buildPropagator(OrbitType.CARTESIAN);
410         propagator2.resetInitialState(state.withMass(state.getMass() + dM/2.));
411         propagator2.addForceModel(forceModel);
412         final SpacecraftState propagated2 = propagator2.propagate(targetDate);
413         final PVCoordinates relativePV = new PVCoordinates(propagated1.getPVCoordinates(),
414                 propagated2.getPVCoordinates());
415         assertEquals(relativePV.getPosition().getX() / dM, stateTransitionMatrix.getEntry(0, 6), 1e-3);
416         assertEquals(relativePV.getPosition().getY() / dM, stateTransitionMatrix.getEntry(1, 6), 1e-3);
417         assertEquals(relativePV.getPosition().getZ() / dM, stateTransitionMatrix.getEntry(2, 6), 1e-3);
418         assertEquals(relativePV.getVelocity().getX() / dM, stateTransitionMatrix.getEntry(3, 6), 1e-5);
419         assertEquals(relativePV.getVelocity().getY() / dM, stateTransitionMatrix.getEntry(4, 6), 1e-5);
420         assertEquals(relativePV.getVelocity().getZ() / dM, stateTransitionMatrix.getEntry(5, 6), 1e-5);
421     }
422 
423     @ParameterizedTest
424     @EnumSource(value = OrbitType.class, names = {"CARTESIAN", "EQUINOCTIAL", "CIRCULAR"})
425     void testRadiationPressure(final OrbitType orbitType) {
426         // GIVEN
427         final NumericalPropagator propagator = buildPropagator(orbitType);
428         final String stmName = "stm";
429         final MatricesHarvester harvester = propagator.setupMatricesComputation(stmName, MatrixUtils.createRealIdentityMatrix(7), null);
430         final CylindricallyShadowedLightFluxModel shadowModel = new CylindricallyShadowedLightFluxModel(new AnalyticalSolarPositionProvider(),
431                 Constants.EGM96_EARTH_EQUATORIAL_RADIUS);
432         final double crossSection = 100.;
433         final double coefficientValue = 1.6;
434         final IsotropicRadiationSingleCoefficient singleCoefficient = new IsotropicRadiationSingleCoefficient(crossSection, coefficientValue);
435         singleCoefficient.getRadiationParametersDrivers()
436                 .stream().filter(driver -> driver.getName().equals(RadiationSensitive.REFLECTION_COEFFICIENT)).findFirst()
437                 .ifPresent(driver -> driver.setSelected(true));
438         final RadiationPressureModel model = new RadiationPressureModel(shadowModel, singleCoefficient);
439         propagator.addForceModel(model);
440         final double timeOfFlight = 1e4;
441         final AbsoluteDate epoch = propagator.getInitialState().getDate();
442         final AbsoluteDate targetDate = epoch.shiftedBy(timeOfFlight);
443         // WHEN
444         final SpacecraftState state = propagator.propagate(targetDate);
445         final RealMatrix jacobianMatrix = harvester.getParametersJacobian(state);
446         final double[][] conversionJacobian = new double[6][6];
447         state.getOrbit().getJacobianWrtParameters(propagator.getPositionAngleType(), conversionJacobian);
448         final RealMatrix matrix = MatrixUtils.createRealIdentityMatrix(jacobianMatrix.getRowDimension());
449         matrix.setSubMatrix(conversionJacobian, 0, 0);
450         final RealMatrix jacobianCartesian = matrix.multiply(jacobianMatrix);
451         // THEN
452         compareRadiationCoefficientGradientWithFiniteDifferences(propagator.getOrbitType(), shadowModel, crossSection,
453                 coefficientValue, targetDate, jacobianCartesian);
454         final RealMatrix stm = harvester.getStateTransitionMatrix(state);
455         for (int i = 0; i < 6; i++) {
456             Assertions.assertNotEquals(0., stm.getEntry(i, 6));
457         }
458     }
459 
460     private static void compareRadiationCoefficientGradientWithFiniteDifferences(final OrbitType orbitType,
461                                                                  final LightFluxModel fluxModel,
462                                                                  final double crossSection, final double singleCoefficient,
463                                                                  final AbsoluteDate targetDate,
464                                                                  final RealMatrix jacobianMatrix) {
465         final double dP = 0.01;
466         final NumericalPropagator propagator1 = buildPropagator(orbitType);
467         propagator1.addForceModel(new RadiationPressureModel(fluxModel, new IsotropicRadiationSingleCoefficient(crossSection,
468                 singleCoefficient - dP /2)));
469         final SpacecraftState propagated1 = propagator1.propagate(targetDate);
470         final NumericalPropagator propagator2 = buildPropagator(propagator1.getOrbitType(), propagator1.getAttitudeProvider());
471         propagator2.addForceModel(new RadiationPressureModel(fluxModel, new IsotropicRadiationSingleCoefficient(crossSection,
472                 singleCoefficient + dP /2)));
473         final SpacecraftState propagated2 = propagator2.propagate(targetDate);
474         final PVCoordinates relativePV = new PVCoordinates(propagated1.getPVCoordinates(),
475                 propagated2.getPVCoordinates());
476         final double toleranceDerivativePosition = 3e-2;
477         final double toleranceDerivativeVelocity = 5e-5;
478         assertEquals(relativePV.getPosition().getX() / dP, jacobianMatrix.getEntry(0, 0), toleranceDerivativePosition);
479         assertEquals(relativePV.getPosition().getY() / dP, jacobianMatrix.getEntry(1, 0), toleranceDerivativePosition);
480         assertEquals(relativePV.getPosition().getZ() / dP, jacobianMatrix.getEntry(2, 0), toleranceDerivativePosition);
481         assertEquals(relativePV.getVelocity().getX() / dP, jacobianMatrix.getEntry(3, 0), toleranceDerivativeVelocity);
482         assertEquals(relativePV.getVelocity().getY() / dP, jacobianMatrix.getEntry(4, 0), toleranceDerivativeVelocity);
483         assertEquals(relativePV.getVelocity().getZ() / dP, jacobianMatrix.getEntry(5, 0), toleranceDerivativeVelocity);
484     }
485 
486     @ParameterizedTest
487     @ValueSource(doubles = {-2e4, 2e4})
488     void testManeuverApside(final double timeOfFlight) {
489         // GIVEN
490         final NumericalPropagator propagator = buildPropagator(OrbitType.CARTESIAN);
491         final String stmName = "stm";
492         final MatricesHarvester harvester = propagator.setupMatricesComputation(stmName, MatrixUtils.createRealIdentityMatrix(7), null);
493         final double thrustMagnitude = 5e-3;
494         final Vector3D thrustDirection = Vector3D.PLUS_I;
495         final double isp = 10;
496         final PropulsionModel propulsionModel = new BasicConstantThrustPropulsionModel(thrustMagnitude, isp,
497                 thrustDirection, "");
498         final EventDetectionSettings detectionSettings = EventDetectionSettings.getDefaultEventDetectionSettings();
499         final ManeuverTriggers triggers = new ApsideTriggers(detectionSettings);
500         final Maneuver maneuver = new Maneuver(null, triggers, propulsionModel);
501         propagator.addForceModel(maneuver);
502         final AbsoluteDate epoch = propagator.getInitialState().getDate();
503         final AbsoluteDate targetDate = epoch.shiftedBy(timeOfFlight);
504         final SpacecraftState initialState = propagator.getInitialState();
505         // WHEN
506         final SpacecraftState state = propagator.propagate(targetDate);
507         final RealMatrix stateTransitionMatrix = harvester.getStateTransitionMatrix(state);
508         // THEN
509         final double dR = 1.;
510         final double dV = 1e-3;
511         checkStmColumnWithFiniteDifferences(0, dR, propagator, initialState, maneuver, targetDate, stateTransitionMatrix.getColumnVector(0));
512         checkStmColumnWithFiniteDifferences(1, dR, propagator, initialState, maneuver, targetDate, stateTransitionMatrix.getColumnVector(1));
513         checkStmColumnWithFiniteDifferences(2, dR, propagator, initialState, maneuver, targetDate, stateTransitionMatrix.getColumnVector(2));
514         checkStmColumnWithFiniteDifferences(3, dV, propagator, initialState, maneuver, targetDate, stateTransitionMatrix.getColumnVector(3));
515         checkStmColumnWithFiniteDifferences(4, dV, propagator, initialState, maneuver, targetDate, stateTransitionMatrix.getColumnVector(4));
516         checkStmColumnWithFiniteDifferences(5, dV, propagator, initialState, maneuver, targetDate, stateTransitionMatrix.getColumnVector(5));
517     }
518 
519     private void checkStmColumnWithFiniteDifferences(final int index, final double dX, final NumericalPropagator propagator,
520                                                      final SpacecraftState initialState, final Maneuver maneuver,
521                                                      final AbsoluteDate targetDate, final RealVector stateTransitionMatrixColumn) {
522         final double[] cartesianVector = new double[6];
523         cartesianVector[index] = -dX/2.;
524         final NumericalPropagator propagator1 = buildPropagator(propagator.getOrbitType(), propagator.getAttitudeProvider());
525         propagator1.resetInitialState(modifyState(initialState, cartesianVector));
526         propagator1.addForceModel(maneuver);
527         final SpacecraftState propagated1 = propagator1.propagate(targetDate);
528         final NumericalPropagator propagator2 = buildPropagator(propagator.getOrbitType(), propagator.getAttitudeProvider());
529         cartesianVector[index] *= -1;
530         propagator2.resetInitialState(modifyState(initialState, cartesianVector));
531         propagator2.addForceModel(maneuver);
532         final SpacecraftState propagated2 = propagator2.propagate(targetDate);
533         final PVCoordinates relativePV = new PVCoordinates(propagated1.getPVCoordinates(), propagated2.getPVCoordinates());
534         final double toleranceP = 1e0;
535         final double toleranceV = 1e-3;
536         assertEquals(relativePV.getPosition().getX() / dX, stateTransitionMatrixColumn.getEntry(0), toleranceP);
537         assertEquals(relativePV.getPosition().getY() / dX, stateTransitionMatrixColumn.getEntry(1), toleranceP);
538         assertEquals(relativePV.getPosition().getZ() / dX, stateTransitionMatrixColumn.getEntry(2), toleranceP);
539         assertEquals(relativePV.getVelocity().getX() / dX, stateTransitionMatrixColumn.getEntry(3), toleranceV);
540         assertEquals(relativePV.getVelocity().getY() / dX, stateTransitionMatrixColumn.getEntry(4), toleranceV);
541         assertEquals(relativePV.getVelocity().getZ() / dX, stateTransitionMatrixColumn.getEntry(5), toleranceV);
542     }
543 
544     private static SpacecraftState modifyState(final SpacecraftState baseState, final double[] cartesianShift) {
545         final PVCoordinates pvCoordinates = baseState.getPVCoordinates();
546         final PVCoordinates newPV = new PVCoordinates(pvCoordinates.getPosition().add(new Vector3D(Arrays.copyOfRange(cartesianShift, 0, 3))),
547                 pvCoordinates.getVelocity().add(new Vector3D(Arrays.copyOfRange(cartesianShift, 3, 6))));
548         final CartesianOrbit orbit = new CartesianOrbit(newPV, baseState.getFrame(), baseState.getDate(),
549                 baseState.getOrbit().getMu());
550         return new SpacecraftState(orbit, baseState.getAttitude()).withMass(baseState.getMass());
551     }
552 
553     private static class ApsideTriggers extends IntervalEventTrigger<ApsideDetector> {
554 
555         protected ApsideTriggers(EventDetectionSettings detectionSettings) {
556             super(new ApsideDetector(detectionSettings, new ContinueOnEvent()));
557         }
558 
559         @Override
560         public List<ParameterDriver> getParametersDrivers() {
561             return Collections.emptyList();
562         }
563 
564         private FieldApsideDetector buildFieldApsideDetector(Field field, EventDetector detector) {
565             return new FieldApsideDetector<>(new FieldEventDetectionSettings<>(field, detector.getDetectionSettings()),
566                     new FieldStopOnEvent());
567         }
568 
569         @Override
570         protected <D extends FieldEventDetector<S>, S extends CalculusFieldElement<S>> D convertIntervalDetector(Field<S> field, ApsideDetector detector) {
571             return (D) buildFieldApsideDetector(field, detector);
572         }
573     }
574 }
575