1   /* Copyright 2022-2025 Romain Serra
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.control.indirect.shooting;
18  
19  import org.hipparchus.CalculusFieldElement;
20  import org.hipparchus.Field;
21  import org.hipparchus.analysis.differentiation.Gradient;
22  import org.hipparchus.analysis.differentiation.GradientField;
23  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
24  import org.hipparchus.geometry.euclidean.threed.Vector3D;
25  import org.hipparchus.ode.ODEIntegrator;
26  import org.junit.jupiter.api.Assertions;
27  import org.junit.jupiter.api.BeforeEach;
28  import org.junit.jupiter.api.Test;
29  import org.junit.jupiter.params.ParameterizedTest;
30  import org.junit.jupiter.params.provider.ValueSource;
31  import org.mockito.Mockito;
32  import org.orekit.Utils;
33  import org.orekit.control.indirect.adjoint.CartesianAdjointDerivativesProvider;
34  import org.orekit.control.indirect.adjoint.CartesianAdjointEquationTerm;
35  import org.orekit.control.indirect.adjoint.CartesianAdjointJ2Term;
36  import org.orekit.control.indirect.adjoint.CartesianAdjointKeplerianTerm;
37  import org.orekit.control.indirect.adjoint.FieldCartesianAdjointDerivativesProvider;
38  import org.orekit.control.indirect.adjoint.cost.CartesianCost;
39  import org.orekit.control.indirect.adjoint.cost.FieldCartesianCost;
40  import org.orekit.control.indirect.adjoint.cost.FieldUnboundedCartesianEnergy;
41  import org.orekit.control.indirect.adjoint.cost.FieldUnboundedCartesianEnergyNeglectingMass;
42  import org.orekit.control.indirect.adjoint.cost.UnboundedCartesianEnergy;
43  import org.orekit.control.indirect.adjoint.cost.UnboundedCartesianEnergyNeglectingMass;
44  import org.orekit.control.indirect.shooting.boundary.CartesianBoundaryConditionChecker;
45  import org.orekit.control.indirect.shooting.boundary.FixedTimeBoundaryOrbits;
46  import org.orekit.control.indirect.shooting.boundary.FixedTimeCartesianBoundaryStates;
47  import org.orekit.control.indirect.shooting.boundary.NormBasedCartesianConditionChecker;
48  import org.orekit.control.indirect.shooting.propagation.*;
49  import org.orekit.forces.ForceModel;
50  import org.orekit.forces.gravity.J2OnlyPerturbation;
51  import org.orekit.forces.gravity.NewtonianAttraction;
52  import org.orekit.frames.Frame;
53  import org.orekit.frames.FramesFactory;
54  import org.orekit.orbits.*;
55  import org.orekit.propagation.CartesianToleranceProvider;
56  import org.orekit.propagation.FieldSpacecraftState;
57  import org.orekit.propagation.SpacecraftState;
58  import org.orekit.propagation.ToleranceProvider;
59  import org.orekit.propagation.analytical.KeplerianPropagator;
60  import org.orekit.propagation.events.EventDetectionSettings;
61  import org.orekit.propagation.events.FieldEventDetectionSettings;
62  import org.orekit.propagation.numerical.NumericalPropagator;
63  import org.orekit.time.AbsoluteDate;
64  import org.orekit.time.DateTimeComponents;
65  import org.orekit.time.FieldAbsoluteDate;
66  import org.orekit.time.TimeScalesFactory;
67  import org.orekit.utils.*;
68  
69  import java.util.ArrayList;
70  import java.util.Arrays;
71  import java.util.List;
72  
73  class NewtonFixedBoundaryCartesianSingleShootingTest {
74  
75      private static final double THRESHOLD_LU_DECOMPOSITION = 1e-11;
76      private static final String ADJOINT_NAME = "adjoint";
77  
78      @BeforeEach
79      public void setUp() {
80          Utils.setDataRoot("regular-data");
81      }
82  
83      @Test
84      void testUpdateShootingVariablesZeroDefects() {
85          // GIVEN
86          final double[] originalAdjoint = new double[] { 1, 2, 3, 4, 5, 6 };
87          final GradientField field = GradientField.getField(6);
88          final TimeStampedPVCoordinates targetPV = new TimeStampedPVCoordinates(AbsoluteDate.ARBITRARY_EPOCH,
89                  new PVCoordinates(new Vector3D(10., 1., 0.1), new Vector3D(0.001, 0.1, -0.0001)));
90          final TimeStampedFieldPVCoordinates<Gradient> fieldPVCoordinates = new TimeStampedFieldPVCoordinates<>(field,
91                  targetPV);
92          final FieldCartesianOrbit<Gradient> fieldOrbit = new FieldCartesianOrbit<>(
93                  new FieldPVCoordinates<>(fieldPVCoordinates.getPosition().add(getFieldVector3D(field, 0, 1, 2)),
94                          fieldPVCoordinates.getVelocity().add(getFieldVector3D(field, 3, 4, 5))),
95                  FramesFactory.getGCRF(), new FieldAbsoluteDate<>(field, targetPV.getDate()), field.getOne());
96          final FieldSpacecraftState<Gradient> fieldState = new FieldSpacecraftState<>(fieldOrbit);
97          final NewtonFixedBoundaryCartesianSingleShooting shooting = Mockito.mock(NewtonFixedBoundaryCartesianSingleShooting.class);
98          final double one = 1;
99          Mockito.when(shooting.getScalePositionDefects()).thenReturn(one);
100         Mockito.when(shooting.getScaleVelocityDefects()).thenReturn(one);
101         Mockito.when(shooting.updateShootingVariables(originalAdjoint, fieldState)).thenCallRealMethod();
102         Mockito.when(shooting.getTerminalCartesianState()).thenReturn(targetPV);
103         Mockito.when(shooting.getScales()).thenReturn(new double[] {1, 1, 1, 1, 1, 1});
104         // WHEN
105         final double[] adjoint = shooting.updateShootingVariables(originalAdjoint, fieldState);
106         // THEN
107         Assertions.assertArrayEquals(originalAdjoint, adjoint);
108     }
109 
110     private static FieldVector3D<Gradient> getFieldVector3D(final GradientField field, final int i1,
111                                                             final int i2, final int i3) {
112         final int parametersNumber = field.getOne().getFreeParameters();
113         return new FieldVector3D<>(Gradient.variable(parametersNumber, i1, 0),
114                 Gradient.variable(parametersNumber, i2, 0), Gradient.variable(parametersNumber, i3, 0));
115     }
116 
117     @ParameterizedTest
118     @ValueSource(doubles = {5e5, 8e5, 1.5e6})
119     void testSolveOrbitVersusAbsolutePV(final double approximateAltitude) {
120         // GIVEN
121         final double tolerancePosition = 1e0;
122         final double toleranceVelocity = 1e-4;
123         final CartesianBoundaryConditionChecker conditionChecker = new NormBasedCartesianConditionChecker(10,
124                 tolerancePosition, toleranceVelocity);
125         final Orbit initialOrbit = createSomeInitialOrbit(approximateAltitude);
126         final double timeOfFlight = 1e4;
127         final Orbit terminalOrbit = createTerminalBoundary(initialOrbit, timeOfFlight);
128         final FixedTimeBoundaryOrbits boundaryOrbits = new FixedTimeBoundaryOrbits(initialOrbit, terminalOrbit);
129         final ShootingPropagationSettings propagationSettings = createShootingSettings(initialOrbit, 0., Double.POSITIVE_INFINITY,
130                 ShootingIntegrationSettingsFactory.getClassicalRungeKuttaIntegratorSettings(60.));
131         final NewtonFixedBoundaryCartesianSingleShooting shooting = new NewtonFixedBoundaryCartesianSingleShooting(propagationSettings,
132                 boundaryOrbits, conditionChecker);
133         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
134         shooting.setStepFactor(1.);
135         shooting.setScalePositionDefects(1.);
136         shooting.setScaleVelocityDefects(1.);
137         final double mass = 1e3;
138         final double[] guess = new double[6];
139         // WHEN
140         final ShootingBoundaryOutput output = shooting.solve(mass, guess);
141         // THEN
142         Assertions.assertTrue(output.isConverged());
143         Assertions.assertNotEquals(0, output.getIterationCount());
144         final PVCoordinates expectedPV = terminalOrbit.getPVCoordinates();
145         final PVCoordinates actualPV = output.getTerminalState().getPVCoordinates();
146         Assertions.assertEquals(0., expectedPV.getPosition().subtract(actualPV.getPosition()).getNorm(),
147                 tolerancePosition);
148         Assertions.assertEquals(0., expectedPV.getVelocity().subtract(actualPV.getVelocity()).getNorm(),
149                 toleranceVelocity);
150         compareToAbsolutePV(mass, guess, propagationSettings, boundaryOrbits, conditionChecker, 0.,
151                 output);
152     }
153 
154     private static ShootingPropagationSettings createShootingSettings(final Orbit initialOrbit, final double massFlowRate,
155                                                                       final double maximumThrustMagnitude,
156                                                                       final ShootingIntegrationSettings integrationSettings) {
157         final NewtonianAttraction newtonianAttraction = new NewtonianAttraction(initialOrbit.getMu());
158         final Frame j2Frame = initialOrbit.getFrame(); // approximation for speed
159         final J2OnlyPerturbation j2OnlyPerturbation = new J2OnlyPerturbation(initialOrbit.getMu(),
160                 Constants.EGM96_EARTH_EQUATORIAL_RADIUS, -Constants.EGM96_EARTH_C20, j2Frame);
161         final List<ForceModel> forceModelList = new ArrayList<>();
162         forceModelList.add(newtonianAttraction);
163         forceModelList.add(j2OnlyPerturbation);
164         final CartesianAdjointKeplerianTerm keplerianTerm = new CartesianAdjointKeplerianTerm(initialOrbit.getMu());
165         final CartesianAdjointJ2Term j2Term = new CartesianAdjointJ2Term(j2OnlyPerturbation.getMu(), j2OnlyPerturbation.getrEq(),
166                 j2OnlyPerturbation.getJ2(initialOrbit.getDate()), j2OnlyPerturbation.getFrame());
167         return new ShootingPropagationSettings(forceModelList, getAdjointDynamicsProvider(massFlowRate,
168                 maximumThrustMagnitude, keplerianTerm, j2Term), integrationSettings);
169     }
170 
171     private static Orbit createSomeInitialOrbit(final double approximateAltitude) {
172         return new KeplerianOrbit(Constants.EGM96_EARTH_EQUATORIAL_RADIUS + approximateAltitude, 1e-4, 1., 2., 3., 4., PositionAngleType.ECCENTRIC,
173                 FramesFactory.getGCRF(), AbsoluteDate.ARBITRARY_EPOCH, Constants.EGM96_EARTH_MU);
174     }
175 
176     private static Orbit createTerminalBoundary(final Orbit initialOrbit, final double timeOfFlight) {
177         final KeplerianPropagator propagator = new KeplerianPropagator(initialOrbit);
178         final AbsoluteDate epoch = initialOrbit.getDate();
179         return propagator.propagate(epoch.shiftedBy(timeOfFlight)).getOrbit();
180     }
181 
182     private void compareToAbsolutePV(final double mass, final double[] guess,
183                                      final ShootingPropagationSettings propagationSettings,
184                                      final FixedTimeBoundaryOrbits boundaryOrbits,
185                                      final CartesianBoundaryConditionChecker conditionChecker,
186                                      final double toleranceMassAdjoint,
187                                      final ShootingBoundaryOutput output) {
188         final Orbit initialOrbit = boundaryOrbits.getInitialOrbit();
189         final Orbit terminalOrbit = boundaryOrbits.getTerminalOrbit();
190         final FixedTimeCartesianBoundaryStates boundaryStates = new FixedTimeCartesianBoundaryStates(convertToAbsolutePVCoordinates(initialOrbit),
191                 convertToAbsolutePVCoordinates(terminalOrbit));
192         final NewtonFixedBoundaryCartesianSingleShooting shooting = new NewtonFixedBoundaryCartesianSingleShooting(propagationSettings,
193                 boundaryStates, conditionChecker);
194         shooting.setStepFactor(1.);
195         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
196         shooting.setToleranceMassAdjoint(toleranceMassAdjoint);
197         final ShootingBoundaryOutput otherOutput = shooting.solve(mass, guess);
198         Assertions.assertEquals(otherOutput.getIterationCount(), output.getIterationCount());
199         Assertions.assertEquals(otherOutput.getTerminalState().getPosition(), otherOutput.getTerminalState().getPosition());
200         final String adjointName = propagationSettings.getAdjointDynamicsProvider().getAdjointName();
201         Assertions.assertArrayEquals(output.getInitialState().getAdditionalState(adjointName),
202                 otherOutput.getInitialState().getAdditionalState(adjointName));
203     }
204 
205     private static AbsolutePVCoordinates convertToAbsolutePVCoordinates(final Orbit orbit) {
206         return new AbsolutePVCoordinates(orbit.getFrame(), orbit.getDate(), orbit.getPVCoordinates());
207     }
208 
209     @Test
210     void testSolveSequential() {
211         // GIVEN
212         final double tolerancePosition = 1e-0;
213         final double toleranceVelocity = 1e-4;
214         final CartesianBoundaryConditionChecker conditionChecker = new NormBasedCartesianConditionChecker(10,
215                 tolerancePosition, toleranceVelocity);
216         final Orbit initialOrbit = createSomeInitialOrbit(1e6);
217         final double timeOfFlight = 1e4;
218         final Orbit terminalOrbit = createTerminalBoundary(initialOrbit, timeOfFlight);
219         final FixedTimeBoundaryOrbits boundaryOrbits = new FixedTimeBoundaryOrbits(initialOrbit, terminalOrbit);
220         final double flowRateFactor = 1e-3;
221         final ShootingPropagationSettings propagationSettings = createShootingSettings(initialOrbit, flowRateFactor, Double.POSITIVE_INFINITY,
222                 ShootingIntegrationSettingsFactory.getDormandPrince54IntegratorSettings(1e-1, 1e2,
223                         ToleranceProvider.of(CartesianToleranceProvider.of(1e-3, 1e-6, CartesianToleranceProvider.DEFAULT_ABSOLUTE_MASS_TOLERANCE))));
224         final NewtonFixedBoundaryCartesianSingleShooting shooting = new NewtonFixedBoundaryCartesianSingleShooting(propagationSettings,
225                 boundaryOrbits, conditionChecker);
226         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
227         shooting.setStepFactor(1.);
228         final double toleranceMassAdjoint = 1e-10;
229         shooting.setToleranceMassAdjoint(toleranceMassAdjoint);
230         final double mass = 1e3;
231         final double[] guess = new double[]{-2.305656141544546E-6, -6.050107349447073E-6, -4.484389270662034E-6,
232                 -9.635757291472267E-4, -0.0026076008704216066, 8.621848929368622E-5, 0.};
233         // WHEN
234         final ShootingBoundaryOutput output = shooting.solve(mass, guess);
235         // THEN
236         final double thrustBound = 1e5;
237         final ShootingPropagationSettings propagationSettingsBoundedEnergy = createShootingSettings(initialOrbit,
238                 flowRateFactor, thrustBound, propagationSettings.getIntegrationSettings());
239         final NewtonFixedBoundaryCartesianSingleShooting shootingBoundedEnergy = new NewtonFixedBoundaryCartesianSingleShooting(propagationSettingsBoundedEnergy,
240                 boundaryOrbits, conditionChecker);
241         shootingBoundedEnergy.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
242         final double[] unboundedEnergyAdjoint = output.getInitialState().getAdditionalState(ADJOINT_NAME);
243         double[] guessBoundedEnergy = unboundedEnergyAdjoint.clone();
244         final ShootingBoundaryOutput outputBoundedEnergy = shootingBoundedEnergy.solve(mass, guessBoundedEnergy);
245         Assertions.assertTrue(outputBoundedEnergy.isConverged());
246         Assertions.assertEquals(0, outputBoundedEnergy.getIterationCount());
247     }
248 
249     @Test
250     void testSolveRegression() {
251         // GIVEN
252         final double massFlowRateFactor = 2e-6;
253         final Orbit initialOrbit = createSomeInitialOrbit(1e6);
254         final double timeOfFlight = initialOrbit.getKeplerianPeriod() * 5;
255         final Orbit terminalOrbit = createTerminalBoundary(initialOrbit, timeOfFlight);
256         final NewtonFixedBoundaryCartesianSingleShooting shooting = getShootingMethod(massFlowRateFactor,
257                 new FixedTimeBoundaryOrbits(initialOrbit, terminalOrbit),
258                 ShootingIntegrationSettingsFactory.getClassicalRungeKuttaIntegratorSettings(100.));
259         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
260         shooting.setStepFactor(1.);
261         final double toleranceMassAdjoint = 1e-10;
262         shooting.setToleranceMassAdjoint(toleranceMassAdjoint);
263         final double mass = 1.;
264         final double[] guess = {-1.3440754763650783E-6, -6.346307866897998E-6, -4.25736594074492E-6,
265                 -4.54324936872417E-4, -0.0020329894350755227, -8.358161689612435E-4, 0.};
266         // WHEN
267         final ShootingBoundaryOutput output = shooting.solve(mass, guess);
268         // THEN
269         Assertions.assertTrue(output.isConverged());
270         final double[] initialAdjoint = output.getInitialState().getAdditionalState(shooting.getPropagationSettings()
271                 .getAdjointDynamicsProvider().getAdjointName());
272         final double[] expectedAdjoint = new double[] {-1.3432883741256684E-6, -6.343244627959342E-6, -4.2552646864846415E-6,
273                 -4.540374638007354E-4, -0.002031906384904598, -8.355018662664441E-4, -1.0320210230861449};
274         final double tolerance = 1e-8;
275         for (int i = 0; i < expectedAdjoint.length; i++) {
276             Assertions.assertEquals(expectedAdjoint[i], initialAdjoint[i], tolerance);
277         }
278         Assertions.assertNotEquals(1., output.getTerminalState().getMass());
279     }
280 
281     private static NewtonFixedBoundaryCartesianSingleShooting getShootingMethod(final double massFlowRateFactor,
282                                                                                 final FixedTimeBoundaryOrbits fixedTimeBoundaryOrbits,
283                                                                                 final ShootingIntegrationSettings integrationSettings) {
284         final double tolerancePosition = 1e-0;
285         final double toleranceVelocity = 1e-4;
286         final CartesianBoundaryConditionChecker conditionChecker = new NormBasedCartesianConditionChecker(10,
287                 tolerancePosition, toleranceVelocity);
288         final FixedTimeBoundaryOrbits boundaryOrbits = new FixedTimeBoundaryOrbits(fixedTimeBoundaryOrbits.getInitialOrbit(),
289                 fixedTimeBoundaryOrbits.getTerminalOrbit());
290         final ShootingPropagationSettings propagationSettings = createShootingSettings(fixedTimeBoundaryOrbits.getInitialOrbit(),
291                 massFlowRateFactor, Double.POSITIVE_INFINITY, integrationSettings);
292         final NewtonFixedBoundaryCartesianSingleShooting shooting = new NewtonFixedBoundaryCartesianSingleShooting(propagationSettings,
293                 boundaryOrbits, conditionChecker);
294         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
295         shooting.setStepFactor(1.);
296         shooting.setScalePositionDefects(1e3);
297         shooting.setScaleVelocityDefects(1.);
298         return shooting;
299     }
300 
301     @Test
302     void testSolveForwardBackward() {
303         // GIVEN
304         final Orbit initialOrbit = createGeoInitialOrbit();
305         final double timeOfFlight = initialOrbit.getKeplerianPeriod() * 3;
306         final Orbit terminalOrbit = createTerminalBoundary(initialOrbit, timeOfFlight);
307         final ShootingIntegrationSettings integrationSettings = ShootingIntegrationSettingsFactory.getClassicalRungeKuttaIntegratorSettings(100.);
308         final NewtonFixedBoundaryCartesianSingleShooting shooting = getShootingMethod(0.,
309                 new FixedTimeBoundaryOrbits(initialOrbit, terminalOrbit), integrationSettings);
310         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
311         shooting.setStepFactor(1.);
312         final double toleranceMassAdjoint = 1e-10;
313         final double initialMass = 3e3;
314         shooting.setToleranceMassAdjoint(toleranceMassAdjoint);
315         final double[] guess = new double[] {-1.429146468892837E-10, 4.5022870335769276E-11, -1.318194179536703E-12,
316                 -1.098381235039422E-6, -1.6798876678906052E-6, -3.207856651454041E-9};
317         // WHEN
318         final ShootingBoundaryOutput forwardOutput = shooting.solve(initialMass, guess);
319         // THEN
320         final SpacecraftState terminalState = forwardOutput.getTerminalState();
321         final String adjointName = ADJOINT_NAME;
322         final double[] terminalAdjointForward = terminalState.getAdditionalState(adjointName);
323         final NewtonFixedBoundaryCartesianSingleShooting backwardShooting = getShootingMethod(0.,
324                 new FixedTimeBoundaryOrbits(terminalOrbit, initialOrbit), integrationSettings);
325         backwardShooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
326         final ShootingBoundaryOutput backwardOutput = backwardShooting.solve(terminalState.getMass(), terminalAdjointForward);
327         Assertions.assertTrue(backwardOutput.isConverged());
328         Assertions.assertEquals(0, backwardOutput.getIterationCount());
329         final double[] initialAdjointForward = forwardOutput.getInitialState().getAdditionalState(adjointName);
330         final double[] terminalAdjointBackward = backwardOutput.getTerminalState().getAdditionalState(adjointName);
331         for (int i = 0; i < initialAdjointForward.length; i++) {
332             Assertions.assertEquals(initialAdjointForward[i], terminalAdjointBackward[i], 1e-13);
333         }
334     }
335 
336     private static Orbit createGeoInitialOrbit() {
337         return new KeplerianOrbit(Constants.EGM96_EARTH_EQUATORIAL_RADIUS + 35000e3, 1e-5, 0.001, 2., 3., 4., PositionAngleType.ECCENTRIC,
338                 FramesFactory.getGCRF(), AbsoluteDate.ARBITRARY_EPOCH, Constants.EGM96_EARTH_MU);
339     }
340 
341     @ParameterizedTest
342     @ValueSource(doubles = {1, 10, 100, 1000})
343     void testSolveScales(final double scale) {
344         // GIVEN
345         final double tolerancePosition = 1e1;
346         final double toleranceVelocity = 1e-3;
347         final CartesianBoundaryConditionChecker conditionChecker = new NormBasedCartesianConditionChecker(10,
348                 tolerancePosition, toleranceVelocity);
349         final FixedTimeBoundaryOrbits boundaryOrbits = createBoundaryForKeplerianSettings();
350         final ShootingIntegrationSettings integrationSettings = ShootingIntegrationSettingsFactory.getDormandPrince54IntegratorSettings(1e-2, 2e2,
351                 ToleranceProvider.of(CartesianToleranceProvider.of(1e-3, 1e-6, CartesianToleranceProvider.DEFAULT_ABSOLUTE_MASS_TOLERANCE)));
352         final ShootingPropagationSettings propagationSettings = createKeplerianShootingSettings(boundaryOrbits.getInitialOrbit(),
353                 0, integrationSettings);
354         // WHEN
355         final ShootingBoundaryOutput output = getShootingBoundaryOutput(propagationSettings, boundaryOrbits, conditionChecker, scale);
356         // THEN
357         final ShootingBoundaryOutput expectedOutput = getShootingBoundaryOutput(propagationSettings, boundaryOrbits, conditionChecker, 1);
358         Assertions.assertEquals(expectedOutput.getIterationCount(), output.getIterationCount());
359         Assertions.assertArrayEquals(expectedOutput.getInitialState().getAdditionalState(ADJOINT_NAME),
360                 output.getInitialState().getAdditionalState(ADJOINT_NAME), 1e-20);
361     }
362 
363     private static ShootingBoundaryOutput getShootingBoundaryOutput(final ShootingPropagationSettings propagationSettings,
364                                                                     final FixedTimeBoundaryOrbits boundaryOrbits,
365                                                                     final CartesianBoundaryConditionChecker conditionChecker,
366                                                                     final double scale) {
367         final NewtonFixedBoundaryCartesianSingleShooting shooting = new NewtonFixedBoundaryCartesianSingleShooting(propagationSettings,
368                 boundaryOrbits, conditionChecker);
369         shooting.setStepFactor(1.);
370         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
371         shooting.setStepFactor(1.);
372         final double toleranceMassAdjoint = 1e-8;
373         shooting.setToleranceMassAdjoint(toleranceMassAdjoint);
374         final double mass = 1e3;
375         final double[] guess = new double[6];
376         final double[] scales = guess.clone();
377         Arrays.fill(scales, scale);
378         return shooting.solve(mass, guess, scales);
379     }
380 
381     @ParameterizedTest
382     @ValueSource(doubles = {1e-4, 1e-3, 1e-2})
383     void testSolveUnboundedCartesianEnergy(final double flowRateFactor) {
384         // GIVEN
385         final double tolerancePosition = 1e1;
386         final double toleranceVelocity = 1e-3;
387         final CartesianBoundaryConditionChecker conditionChecker = new NormBasedCartesianConditionChecker(10,
388                 tolerancePosition, toleranceVelocity);
389         final FixedTimeBoundaryOrbits boundaryOrbits = createBoundaryForKeplerianSettings();
390         final ShootingIntegrationSettings integrationSettings = ShootingIntegrationSettingsFactory.getDormandPrince54IntegratorSettings(1e-2, 2e2,
391                 ToleranceProvider.of(CartesianToleranceProvider.of(1e-3, 1e-6, CartesianToleranceProvider.DEFAULT_ABSOLUTE_MASS_TOLERANCE)));
392         final ShootingPropagationSettings propagationSettings = createKeplerianShootingSettings(boundaryOrbits.getInitialOrbit(),
393                 flowRateFactor, integrationSettings);
394         final NewtonFixedBoundaryCartesianSingleShooting shooting = new NewtonFixedBoundaryCartesianSingleShooting(propagationSettings,
395                 boundaryOrbits, conditionChecker);
396         shooting.setStepFactor(1);
397         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
398         final double toleranceMassAdjoint = 1e-8;
399         shooting.setToleranceMassAdjoint(toleranceMassAdjoint);
400         final double mass = 1e3;
401         final double[] guess = guessWithoutMass(propagationSettings.getAdjointDynamicsProvider().getAdjointName(), mass, integrationSettings, boundaryOrbits,
402                 conditionChecker);
403         // WHEN
404         final ShootingBoundaryOutput output = shooting.solve(mass, guess);
405         // THEN
406         Assertions.assertTrue(output.isConverged());
407     }
408 
409     private static FixedTimeBoundaryOrbits createBoundaryForKeplerianSettings() {
410         final Orbit initialOrbit = createSomeInitialOrbit(2e6);
411         final PVCoordinates templatePV = initialOrbit.getPVCoordinates();
412         final Orbit modifiedOrbit = new CartesianOrbit(new PVCoordinates(templatePV.getPosition().add(new Vector3D(1e3, 2e3, 3e3)), templatePV.getVelocity()),
413                 initialOrbit.getFrame(), initialOrbit.getDate(), initialOrbit.getMu());
414         final double timeOfFlight = 1e5;
415         final Orbit terminalOrbit = createTerminalBoundary(modifiedOrbit, timeOfFlight);
416         return new FixedTimeBoundaryOrbits(modifiedOrbit, terminalOrbit);
417     }
418 
419     private static ShootingPropagationSettings createKeplerianShootingSettings(final Orbit initialOrbit,
420                                                                                final double massFlowRateFactor,
421                                                                                final ShootingIntegrationSettings integrationSettings) {
422         final NewtonianAttraction newtonianAttraction = new NewtonianAttraction(initialOrbit.getMu());
423         final List<ForceModel> forceModelList = new ArrayList<>();
424         forceModelList.add(newtonianAttraction);
425         final CartesianAdjointKeplerianTerm keplerianTerm = new CartesianAdjointKeplerianTerm(initialOrbit.getMu());
426         final AdjointDynamicsProvider adjointDynamicsProvider = getAdjointDynamicsProvider(massFlowRateFactor, Double.POSITIVE_INFINITY,
427                 keplerianTerm);
428         return new ShootingPropagationSettings(forceModelList, adjointDynamicsProvider, integrationSettings);
429     }
430 
431     private static double[] guessWithoutMass(final String adjointName, final double mass,
432                                              final ShootingIntegrationSettings integrationSettings,
433                                              final FixedTimeBoundaryOrbits boundaryOrbits,
434                                              final CartesianBoundaryConditionChecker conditionChecker) {
435         final ShootingPropagationSettings propagationSettings = createKeplerianShootingSettings(boundaryOrbits.getInitialOrbit(),
436                 0., integrationSettings);
437         final NewtonFixedBoundaryCartesianSingleShooting shooting = new NewtonFixedBoundaryCartesianSingleShooting(propagationSettings,
438                 boundaryOrbits, conditionChecker);
439         shooting.setStepFactor(1.);
440         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
441         final ShootingBoundaryOutput output = shooting.solve(mass, new double[6]);
442         final double squaredMass = mass * mass;
443         final double[] adjoint = output.getInitialState().getAdditionalState(adjointName);
444         final double[] adjointWithMass = new double[7];
445         for (int i = 0; i < adjoint.length; i++) {
446             adjointWithMass[i] = adjoint[i] * squaredMass;
447         }
448         adjointWithMass[adjointWithMass.length - 1] = -1.;
449         return adjointWithMass;
450     }
451 
452     private static AdjointDynamicsProvider getAdjointDynamicsProvider(final double massFlowRateFactor,
453                                                                       final double maximumThrustMagnitude,
454                                                                       final CartesianAdjointEquationTerm... terms) {
455         final String adjointName = ADJOINT_NAME;
456         if (maximumThrustMagnitude == Double.POSITIVE_INFINITY) {
457             if (massFlowRateFactor == 0) {
458                 return CartesianAdjointDynamicsProviderFactory.buildUnboundedEnergyProviderNeglectingMass(adjointName,
459                         terms);
460             }
461             return CartesianAdjointDynamicsProviderFactory.buildUnboundedEnergyProvider(adjointName, massFlowRateFactor,
462                     EventDetectionSettings.getDefaultEventDetectionSettings(), terms);
463         } else {
464             return CartesianAdjointDynamicsProviderFactory.buildBoundedEnergyProvider(adjointName, massFlowRateFactor,
465                     maximumThrustMagnitude, EventDetectionSettings.getDefaultEventDetectionSettings(), terms);
466         }
467     }
468 
469     private NewtonFixedBoundaryCartesianSingleShooting getHeliocentricShootingMethod(final double massFlowRateFactor,
470                                                                                      final FixedTimeBoundaryOrbits boundaryOrbits) {
471         // GIVEN
472         final double tolerancePosition = 1e5;
473         final double toleranceVelocity = 1e0;
474         final CartesianBoundaryConditionChecker conditionChecker = new NormBasedCartesianConditionChecker(10,
475                 tolerancePosition, toleranceVelocity);
476         final ShootingIntegrationSettings integrationSettings = ShootingIntegrationSettingsFactory.getDormandPrince54IntegratorSettings(2e2, 1e5,
477                 ToleranceProvider.of(CartesianToleranceProvider.of(1e5, 1e-1, CartesianToleranceProvider.DEFAULT_ABSOLUTE_MASS_TOLERANCE)));
478         final EventDetectionSettings detectionSettings = new EventDetectionSettings(1e5, 1e3, EventDetectionSettings.DEFAULT_MAX_ITER);
479         final ShootingPropagationSettings propagationSettings = createHeliocentricShootingSettings(boundaryOrbits.getInitialOrbit(),
480                 massFlowRateFactor, detectionSettings, integrationSettings);
481         final NewtonFixedBoundaryCartesianSingleShooting shooting = new NewtonFixedBoundaryCartesianSingleShooting(propagationSettings,
482                 boundaryOrbits, conditionChecker);
483         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
484         final double toleranceMassAdjoint = 1e-7;
485         shooting.setToleranceMassAdjoint(toleranceMassAdjoint);
486         return shooting;
487     }
488 
489     @Test
490     void testGetSingularityThreshold() {
491         // GIVEN
492         final FixedTimeBoundaryOrbits boundaryOrbits = getHeliocentricBoundary();
493         final NewtonFixedBoundaryCartesianSingleShooting shooting = getHeliocentricShootingMethod(1. / (4000. * Constants.G0_STANDARD_GRAVITY),
494                 boundaryOrbits);
495         final double expectedThreshold = 1.;
496         shooting.setSingularityThreshold(expectedThreshold);
497         // WHEN
498         final double actualThreshold = shooting.getSingularityThreshold();
499         // THEN
500         Assertions.assertEquals(expectedThreshold, actualThreshold);
501     }
502 
503     @Test
504     void testSolveHeliocentric() {
505         // GIVEN
506         final FixedTimeBoundaryOrbits boundaryOrbits = getHeliocentricBoundary();
507         final NewtonFixedBoundaryCartesianSingleShooting shooting = getHeliocentricShootingMethod(1. / (4000. * Constants.G0_STANDARD_GRAVITY),
508                 boundaryOrbits);
509         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
510         final double mass = 2e3;
511         final double[] guess = guessWithoutMass(ADJOINT_NAME, mass, shooting.getPropagationSettings().getIntegrationSettings(),
512                 boundaryOrbits, shooting.getConditionChecker());
513         // WHEN
514         final ShootingBoundaryOutput output = shooting.solve(mass, guess);
515         // THEN
516         Assertions.assertTrue(output.isConverged());
517     }
518 
519     private static FixedTimeBoundaryOrbits getHeliocentricBoundary() {
520         final double mu = Constants.JPL_SSD_SUN_GM;
521         final Frame frame = FramesFactory.getGCRF();
522         final AbsoluteDate date = new AbsoluteDate(new DateTimeComponents(2035, 1, 1, 0, 0, 0),
523                 TimeScalesFactory.getUTC());
524         final Vector3D position = new Vector3D(269630575634.1845, -317928797663.87445, -117503661424.1842);
525         final Vector3D velocity = new Vector3D(12803.992418160833, 12346.009014593829, 2789.3378661767967);
526         final Orbit initialOrbit = new CartesianOrbit(new TimeStampedPVCoordinates(date, position, velocity), frame, mu);
527         final AbsoluteDate terminalDate = new AbsoluteDate(new DateTimeComponents(2038, 4, 20, 7, 48, 0),
528                 TimeScalesFactory.getUTC());
529         final Vector3D terminalPosition = new Vector3D(-254040098474.26975, 292309940514.6629, 61765199864.609174);
530         final Vector3D terminalVelocity = new Vector3D(-15342.352873059252, -10427.635262141607, -7365.033285214819);
531         final Orbit terminalOrbit = new CartesianOrbit(new TimeStampedPVCoordinates(terminalDate, terminalPosition, terminalVelocity), frame, mu);
532         return new FixedTimeBoundaryOrbits(initialOrbit, terminalOrbit);
533     }
534 
535     private static ShootingPropagationSettings createHeliocentricShootingSettings(final Orbit initialOrbit,
536                                                                                final double massFlowRateFactor,
537                                                                                final EventDetectionSettings eventDetectionSettings,
538                                                                                final ShootingIntegrationSettings integrationSettings) {
539         final NewtonianAttraction newtonianAttraction = new NewtonianAttraction(initialOrbit.getMu());
540         final List<ForceModel> forceModelList = new ArrayList<>();
541         forceModelList.add(newtonianAttraction);
542         final CartesianAdjointKeplerianTerm keplerianTerm = new CartesianAdjointKeplerianTerm(initialOrbit.getMu());
543         final int dimension = (massFlowRateFactor == 0) ? 6 : 7;
544         final CartesianAdjointDynamicsProvider adjointDynamicsProvider = new CartesianAdjointDynamicsProvider(ADJOINT_NAME, dimension) {
545 
546             @Override
547             public CartesianAdjointDerivativesProvider buildAdditionalDerivativesProvider() {
548                 final CartesianCost cost;
549                 if (massFlowRateFactor == 0) {
550                     cost = new UnboundedCartesianEnergyNeglectingMass(getAdjointName());
551                 } else {
552                     cost = new UnboundedCartesianEnergy(getAdjointName(), massFlowRateFactor, eventDetectionSettings);
553                 }
554                 return new CartesianAdjointDerivativesProvider(cost, keplerianTerm);
555             }
556 
557             @Override
558             public <T extends CalculusFieldElement<T>> FieldCartesianAdjointDerivativesProvider<T> buildFieldAdditionalDerivativesProvider(Field<T> field) {
559                 final FieldCartesianCost<T> cost;
560                 if (massFlowRateFactor == 0) {
561                     cost = new FieldUnboundedCartesianEnergyNeglectingMass<>(getAdjointName(), field);
562                 } else {
563                     cost = new FieldUnboundedCartesianEnergy<>(getAdjointName(), field.getZero().newInstance(massFlowRateFactor),
564                             new FieldEventDetectionSettings<>(field, eventDetectionSettings));
565                 }
566                 return new FieldCartesianAdjointDerivativesProvider<>(cost, keplerianTerm);
567             }
568         };
569         return new ShootingPropagationSettings(forceModelList, adjointDynamicsProvider, integrationSettings);
570     }
571 
572     @Test
573     void testSolveHeliocentricWithoutMass() {
574         // GIVEN
575         final FixedTimeBoundaryOrbits boundaryOrbits = getHeliocentricBoundary();
576         final NewtonFixedBoundaryCartesianSingleShooting shooting = getHeliocentricShootingMethod(0.,
577                 boundaryOrbits);
578         shooting.setStepFactor(1.);
579         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
580         final double mass = 3e3;
581         // WHEN
582         final ShootingBoundaryOutput output = shooting.solve(mass, new double[6]);
583         // THEN
584         Assertions.assertTrue(output.isConverged());
585         final SpacecraftState repropagatedState = repropagate(shooting.getPropagationSettings(), output.getInitialState(),
586                 boundaryOrbits.getTerminalOrbit().getDate());
587         final Vector3D relativePosition = repropagatedState.getPosition().subtract(boundaryOrbits.getTerminalOrbit().getPosition());
588         Assertions.assertEquals(0, relativePosition.getNorm(), 1e2);
589     }
590 
591     private SpacecraftState repropagate(final ShootingPropagationSettings propagationSettings,
592                                         final SpacecraftState initialState, final AbsoluteDate terminalDate) {
593         final OrbitType orbitType = OrbitType.CARTESIAN;
594         final ODEIntegrator integrator = propagationSettings.getIntegrationSettings().getIntegratorBuilder()
595                 .buildIntegrator(initialState.getOrbit(), orbitType);
596         final NumericalPropagator propagator = new NumericalPropagator(integrator);
597         propagator.setInitialState(initialState);
598         propagator.setOrbitType(orbitType);
599         propagator.addAdditionalDerivativesProvider(propagationSettings.getAdjointDynamicsProvider().buildAdditionalDerivativesProvider());
600         return propagator.propagate(terminalDate);
601     }
602 
603     @Test
604     void testQuadraticContinuation() {
605         // GIVEN
606         final double tolerancePosition = 1e-0;
607         final double toleranceVelocity = 1e-4;
608         final CartesianBoundaryConditionChecker conditionChecker = new NormBasedCartesianConditionChecker(10,
609                 tolerancePosition, toleranceVelocity);
610         final Orbit initialOrbit = createSomeInitialOrbit(2e6);
611         final double timeOfFlight = 1e4;
612         final Orbit terminalOrbit = createTerminalBoundary(initialOrbit, timeOfFlight);
613         final FixedTimeBoundaryOrbits boundaryOrbits = new FixedTimeBoundaryOrbits(initialOrbit, terminalOrbit);
614         final double flowRateFactor = 1e-2;
615         final ShootingIntegrationSettings integrationSettings = ShootingIntegrationSettingsFactory
616                 .getDormandPrince54IntegratorSettings(1e-1, 1e2,  ToleranceProvider.of(CartesianToleranceProvider.of(1e-3, 1e-6, CartesianToleranceProvider.DEFAULT_ABSOLUTE_MASS_TOLERANCE)));
617         final double maximumThrust = 1e1;
618         final ShootingPropagationSettings propagationSettings = createShootingSettingsForQuadraticPenalty(initialOrbit, flowRateFactor,
619                 maximumThrust, 1., integrationSettings);
620         final NewtonFixedBoundaryCartesianSingleShooting shooting = new NewtonFixedBoundaryCartesianSingleShooting(propagationSettings,
621                 boundaryOrbits, conditionChecker);
622         shooting.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
623         final double toleranceMassAdjoint = 1e-10;
624         shooting.setToleranceMassAdjoint(toleranceMassAdjoint);
625         final double mass = 2e3;
626         final double[] guess = new double[]{-1.3144902474363005, -7.770298698677809, -5.328110916176676,
627                 -275.6031033370172, -3049.432734131893, -1560.1101732794737, -172.14906242868724};
628         ShootingBoundaryOutput output = shooting.solve(mass, guess);
629         // WHEN & THEN
630         for (double epsilon = 0.9; epsilon > 0.7; epsilon -= 0.05) {
631             double[] previousAdjoint = output.getInitialState().getAdditionalState(ADJOINT_NAME);
632             final ShootingPropagationSettings propagationSettingsWithNewEpsilon = createShootingSettingsForQuadraticPenalty(initialOrbit,
633                     flowRateFactor, maximumThrust, epsilon, propagationSettings.getIntegrationSettings());
634             final NewtonFixedBoundaryCartesianSingleShooting shootingWithNewEpsilon = new NewtonFixedBoundaryCartesianSingleShooting(propagationSettingsWithNewEpsilon,
635                     boundaryOrbits, conditionChecker);
636             shootingWithNewEpsilon.setSingularityThreshold(THRESHOLD_LU_DECOMPOSITION);
637             output = shootingWithNewEpsilon.solve(mass, previousAdjoint);
638             Assertions.assertTrue(output.isConverged());
639         }
640     }
641 
642     private static ShootingPropagationSettings createShootingSettingsForQuadraticPenalty(final Orbit initialOrbit,
643                                                                                          final double massFlowRate,
644                                                                                          final double maximumThrustMagnitude,
645                                                                                          final double epsilon,
646                                                                                          final ShootingIntegrationSettings integrationSettings) {
647         final NewtonianAttraction newtonianAttraction = new NewtonianAttraction(initialOrbit.getMu());
648         final Frame j2Frame = initialOrbit.getFrame(); // approximation for speed
649         final J2OnlyPerturbation j2OnlyPerturbation = new J2OnlyPerturbation(initialOrbit.getMu(),
650                 Constants.EGM96_EARTH_EQUATORIAL_RADIUS, -Constants.EGM96_EARTH_C20, j2Frame);
651         final List<ForceModel> forceModelList = new ArrayList<>();
652         forceModelList.add(newtonianAttraction);
653         forceModelList.add(j2OnlyPerturbation);
654         final CartesianAdjointKeplerianTerm keplerianTerm = new CartesianAdjointKeplerianTerm(initialOrbit.getMu());
655         final CartesianAdjointJ2Term j2Term = new CartesianAdjointJ2Term(j2OnlyPerturbation.getMu(), j2OnlyPerturbation.getrEq(),
656                 j2OnlyPerturbation.getJ2(initialOrbit.getDate()), j2OnlyPerturbation.getFrame());
657         return new ShootingPropagationSettings(forceModelList,
658                 CartesianAdjointDynamicsProviderFactory.buildQuadraticPenaltyFuelCostProvider(ADJOINT_NAME,
659                         massFlowRate, maximumThrustMagnitude, epsilon, EventDetectionSettings.getDefaultEventDetectionSettings(),
660                         keplerianTerm, j2Term), integrationSettings);
661     }
662 }