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.forces.maneuvers;
18  
19  import org.hipparchus.geometry.euclidean.threed.Rotation;
20  import org.hipparchus.geometry.euclidean.threed.RotationConvention;
21  import org.hipparchus.geometry.euclidean.threed.RotationOrder;
22  import org.hipparchus.geometry.euclidean.threed.Vector3D;
23  import org.hipparchus.linear.RealMatrix;
24  import org.hipparchus.ode.events.Action;
25  import org.hipparchus.ode.nonstiff.DormandPrince853Integrator;
26  import org.hipparchus.util.FastMath;
27  import org.junit.jupiter.api.Assertions;
28  import org.junit.jupiter.api.BeforeEach;
29  import org.junit.jupiter.api.Test;
30  import org.junit.jupiter.params.ParameterizedTest;
31  import org.junit.jupiter.params.provider.EnumSource;
32  import org.mockito.Mockito;
33  import org.orekit.TestUtils;
34  import org.orekit.Utils;
35  import org.orekit.attitudes.Attitude;
36  import org.orekit.attitudes.AttitudeProvider;
37  import org.orekit.attitudes.FrameAlignedProvider;
38  import org.orekit.attitudes.LofOffset;
39  import org.orekit.bodies.CelestialBodyFactory;
40  import org.orekit.frames.Frame;
41  import org.orekit.frames.FramesFactory;
42  import org.orekit.frames.LOFType;
43  import org.orekit.orbits.CartesianOrbit;
44  import org.orekit.orbits.KeplerianOrbit;
45  import org.orekit.orbits.Orbit;
46  import org.orekit.orbits.PositionAngleType;
47  import org.orekit.propagation.MatricesHarvester;
48  import org.orekit.propagation.SpacecraftState;
49  import org.orekit.propagation.ToleranceProvider;
50  import org.orekit.propagation.analytical.KeplerianPropagator;
51  import org.orekit.propagation.events.*;
52  import org.orekit.propagation.events.handlers.EventHandler;
53  import org.orekit.propagation.events.handlers.StopOnIncreasing;
54  import org.orekit.propagation.numerical.NumericalPropagator;
55  import org.orekit.time.AbsoluteDate;
56  import org.orekit.time.DateComponents;
57  import org.orekit.time.TimeComponents;
58  import org.orekit.time.TimeScalesFactory;
59  import org.orekit.utils.AbsolutePVCoordinates;
60  import org.orekit.utils.Constants;
61  import org.orekit.utils.TimeStampedPVCoordinates;
62  
63  class ImpulseManeuverTest {
64  
65      @ParameterizedTest
66      @EnumSource(Action.class)
67      void testInclinationManeuver(final Action action) {
68          final Orbit initialOrbit =
69              new KeplerianOrbit(24532000.0, 0.72, 0.3, FastMath.PI, 0.4, 2.0 + 4 * FastMath.PI,
70                                 PositionAngleType.MEAN, FramesFactory.getEME2000(),
71                                 new AbsoluteDate(new DateComponents(2008, 6, 23),
72                                                  new TimeComponents(14, 18, 37),
73                                                  TimeScalesFactory.getUTC()),
74                                 3.986004415e14);
75          final double a  = initialOrbit.getA();
76          final double e  = initialOrbit.getE();
77          final double i  = initialOrbit.getI();
78          final double mu = initialOrbit.getMu();
79          final double vApo = FastMath.sqrt(mu * (1 - e) / (a * (1 + e)));
80          double dv = 0.99 * FastMath.tan(i) * vApo;
81          KeplerianPropagator propagator = new KeplerianPropagator(initialOrbit,
82                                                                   new LofOffset(initialOrbit.getFrame(), LOFType.LVLH_CCSDS));
83          final NodeDetector trigger = new NodeDetector(initialOrbit, FramesFactory.getEME2000());
84          propagator.addEventDetector(new ImpulseManeuver(trigger.withHandler((s, detector, increasing) -> action),
85                                                          new Vector3D(dv, Vector3D.PLUS_J), 400.0));
86          SpacecraftState propagated = propagator.propagate(initialOrbit.getDate().shiftedBy(8000));
87          Assertions.assertEquals(0.0028257, propagated.getOrbit().getI(), 1.0e-6);
88          Assertions.assertEquals(0.442476 + 6 * FastMath.PI, propagated.getOrbit().getLv(), 1.0e-6);
89      }
90  
91      @Test
92      void testInertialManeuver() {
93          final double mu = CelestialBodyFactory.getEarth().getGM();
94  
95          final double initialX = 7100e3;
96          final double initialY = 0.0;
97          final double initialZ = 1300e3;
98          final double initialVx = 0;
99          final double initialVy = 8000;
100         final double initialVz = 1000;
101 
102         final Vector3D position = new Vector3D(initialX, initialY, initialZ);
103         final Vector3D velocity = new Vector3D(initialVx, initialVy, initialVz);
104         final AbsoluteDate epoch = new AbsoluteDate(2010, 1, 1, 0, 0, 0, TimeScalesFactory.getUTC());
105         final TimeStampedPVCoordinates state = new TimeStampedPVCoordinates(epoch, position, velocity, Vector3D.ZERO);
106         final Orbit initialOrbit = new CartesianOrbit(state, FramesFactory.getEME2000(), mu);
107 
108         final double totalPropagationTime = 0.00001;
109         final double driftTimeInSec = totalPropagationTime / 2.0;
110         final double deltaX = 0.01;
111         final double deltaY = 0.02;
112         final double deltaZ = 0.03;
113         final double isp = 300;
114 
115         final Vector3D deltaV = new Vector3D(deltaX, deltaY, deltaZ);
116 
117         KeplerianPropagator propagator = new KeplerianPropagator(initialOrbit, new LofOffset(initialOrbit.getFrame(), LOFType.VNC));
118         DateDetector dateDetector = new DateDetector(epoch.shiftedBy(driftTimeInSec));
119         FrameAlignedProvider attitudeOverride = new FrameAlignedProvider(new Rotation(RotationOrder.XYX,
120                                                                               RotationConvention.VECTOR_OPERATOR,
121                                                                               0, 0, 0));
122         ImpulseManeuver burnAtEpoch = new ImpulseManeuver(dateDetector.withThreshold(driftTimeInSec/4), attitudeOverride, deltaV, isp);
123         Assertions.assertEquals(isp, burnAtEpoch.getIsp(), 1.0e-15);
124         Assertions.assertSame(dateDetector.getClass(), burnAtEpoch.getTrigger().getClass());
125         propagator.addEventDetector(burnAtEpoch);
126 
127         SpacecraftState finalState = propagator.propagate(epoch.shiftedBy(totalPropagationTime));
128 
129         final double finalVxExpected = initialVx + deltaX;
130         final double finalVyExpected = initialVy + deltaY;
131         final double finalVzExpected = initialVz + deltaZ;
132         final double maneuverTolerance = 1e-4;
133 
134         final Vector3D finalVelocity = finalState.getPVCoordinates().getVelocity();
135         Assertions.assertEquals(finalVxExpected, finalVelocity.getX(), maneuverTolerance);
136         Assertions.assertEquals(finalVyExpected, finalVelocity.getY(), maneuverTolerance);
137         Assertions.assertEquals(finalVzExpected, finalVelocity.getZ(), maneuverTolerance);
138 
139     }
140 
141     @Test
142     void testEventOccurredEventSlopeFilter() {
143         // GIVEN
144         final Orbit orbit = TestUtils.getDefaultOrbit(AbsoluteDate.ARBITRARY_EPOCH);
145         final ApsideDetector detector = new ApsideDetector(orbit);
146         final ImpulseManeuver maneuver = new ImpulseManeuver(new EventSlopeFilter<>(detector,
147                 FilterType.TRIGGER_ONLY_INCREASING_EVENTS), Vector3D.ZERO, Double.POSITIVE_INFINITY);
148         final KeplerianPropagator propagator = new KeplerianPropagator(orbit);
149         // WHEN & THEN
150         propagator.addEventDetector(maneuver);
151         Assertions.assertDoesNotThrow(() -> propagator.propagate(orbit.getDate().shiftedBy(1e5)));
152     }
153 
154     @Test
155     void testWithDetectionSettings() {
156         // GIVEN
157         final DateDetector dateDetector = new DateDetector();
158         final ImpulseManeuver maneuver = new ImpulseManeuver(dateDetector, Vector3D.ZERO, 1.);
159         final EventDetectionSettings expectedSettings = new EventDetectionSettings(1., 2., 3);
160         // WHEN
161         final ImpulseManeuver maneuverWithSettings = maneuver.withDetectionSettings(expectedSettings);
162         // THEN
163         Assertions.assertEquals(expectedSettings.getThreshold(), maneuverWithSettings.getThreshold());
164         Assertions.assertEquals(expectedSettings.getMaxIterationCount(), maneuverWithSettings.getMaxIterationCount());
165         Assertions.assertEquals(expectedSettings.getMaxCheckInterval(), maneuverWithSettings.getMaxCheckInterval());
166     }
167 
168     @Test
169     void testResetStateAttitudeOverride() {
170         // GIVEN
171         final AbsolutePVCoordinates pvCoordinates = new AbsolutePVCoordinates(FramesFactory.getEME2000(),
172                 AbsoluteDate.ARBITRARY_EPOCH, new Vector3D(1., 2, 3), new Vector3D(4, 5, 6));
173         final ImpulseManeuver impulseManeuver = new ImpulseManeuver(new DateDetector(),
174                 new FrameAlignedProvider(pvCoordinates.getFrame()), Vector3D.ZERO, 1.);
175         final SpacecraftState expectedSTate = new SpacecraftState(pvCoordinates);
176         // WHEN
177         final SpacecraftState actualSTate = impulseManeuver.getHandler().resetState(impulseManeuver, expectedSTate);
178         // THEN
179         compareStates(expectedSTate, actualSTate);
180     }
181 
182     @Test
183     void testResetState() {
184         // GIVEN
185         final AbsolutePVCoordinates pvCoordinates = new AbsolutePVCoordinates(FramesFactory.getEME2000(),
186                 AbsoluteDate.ARBITRARY_EPOCH, new Vector3D(1., 2, 3), new Vector3D(4, 5, 6));
187         final ImpulseManeuver impulseManeuver = new ImpulseManeuver(new DateDetector(), Vector3D.ZERO, 1.);
188         final SpacecraftState expectedSTate = new SpacecraftState(pvCoordinates);
189         // WHEN
190         final SpacecraftState actualSTate = impulseManeuver.getHandler().resetState(impulseManeuver, expectedSTate);
191         // THEN
192         compareStates(expectedSTate, actualSTate);
193     }
194 
195     private static void compareStates(final SpacecraftState expectedState, final SpacecraftState actualState) {
196         Assertions.assertEquals(expectedState.getDate(), actualState.getDate());
197         Assertions.assertEquals(expectedState.getMass(), actualState.getMass());
198         Assertions.assertEquals(expectedState.getAttitude(), actualState.getAttitude());
199         Assertions.assertEquals(expectedState.getPosition(), actualState.getPosition());
200         Assertions.assertEquals(expectedState.getPVCoordinates().getVelocity(),
201                 actualState.getPVCoordinates().getVelocity());
202     }
203 
204     @Test
205     void testBackward() {
206 
207         final AbsoluteDate iniDate = new AbsoluteDate(2003, 5, 1, 17, 30, 0.0, TimeScalesFactory.getUTC());
208         final Orbit initialOrbit = new KeplerianOrbit(7e6, 1.0e-4, FastMath.toRadians(98.5),
209                                           FastMath.toRadians(87.0), FastMath.toRadians(216.1807),
210                                           FastMath.toRadians(319.779), PositionAngleType.MEAN,
211                                           FramesFactory.getEME2000(), iniDate,
212                                           Constants.EIGEN5C_EARTH_MU);
213         KeplerianPropagator propagator = new KeplerianPropagator(initialOrbit,
214                                                                  new LofOffset(initialOrbit.getFrame(),
215                                                                                LOFType.VNC));
216         DateDetector dateDetector = new DateDetector(iniDate.shiftedBy(-300));
217         Vector3D deltaV = new Vector3D(12.0, 1.0, -4.0);
218         final double isp = 300;
219         ImpulseManeuver maneuver =
220                         new ImpulseManeuver(dateDetector.withMaxCheck(3600.0).
221                                 withThreshold(1.0e-6), deltaV, isp);
222         propagator.addEventDetector(maneuver);
223 
224         SpacecraftState finalState = propagator.propagate(initialOrbit.getDate().shiftedBy(-900));
225 
226         Assertions.assertTrue(finalState.getMass() > propagator.getInitialState().getMass());
227         Assertions.assertTrue(finalState.getDate().compareTo(propagator.getInitialState().getDate()) < 0);
228 
229     }
230 
231     @Test
232     void testBackAndForth() {
233 
234         final AttitudeProvider lof = new LofOffset(FramesFactory.getEME2000(), LOFType.VNC);
235         final double mu = Constants.EIGEN5C_EARTH_MU;
236         final AbsoluteDate iniDate = new AbsoluteDate(2003, 5, 1, 17, 30, 0.0, TimeScalesFactory.getUTC());
237         final Orbit pastOrbit = new KeplerianOrbit(7e6, 1.0e-4, FastMath.toRadians(98.5),
238                                                    FastMath.toRadians(87.0), FastMath.toRadians(216.1807),
239                                                    FastMath.toRadians(319.779), PositionAngleType.MEAN,
240                                                    FramesFactory.getEME2000(), iniDate, mu);
241         final double pastMass = 2500.0;
242         DateDetector dateDetector = new DateDetector(iniDate.shiftedBy(600));
243         Vector3D deltaV = new Vector3D(12.0, 1.0, -4.0);
244         final double isp = 300;
245         ImpulseManeuver maneuver =
246                         new ImpulseManeuver(dateDetector.withMaxCheck(3600.0).
247                                 withThreshold(1.0e-6),
248                                             new FrameAlignedProvider(Rotation.IDENTITY),
249                                             deltaV, isp);
250 
251         double span = 900.0;
252         KeplerianPropagator forwardPropagator = new KeplerianPropagator(pastOrbit, lof, mu, pastMass);
253         forwardPropagator.addEventDetector(maneuver);
254         SpacecraftState futureState = forwardPropagator.propagate(pastOrbit.getDate().shiftedBy(span));
255 
256         KeplerianPropagator backwardPropagator = new KeplerianPropagator(futureState.getOrbit(), lof,
257                                                                          mu, futureState.getMass());
258         backwardPropagator.addEventDetector(maneuver);
259         SpacecraftState rebuiltPast = backwardPropagator.propagate(pastOrbit.getDate());
260         Assertions.assertEquals(0.0,
261                             Vector3D.distance(pastOrbit.getPosition(),
262                                               rebuiltPast.getPosition()),
263                             2.0e-8);
264         Assertions.assertEquals(0.0,
265                             Vector3D.distance(pastOrbit.getPVCoordinates().getVelocity(),
266                                               rebuiltPast.getPVCoordinates().getVelocity()),
267                             2.0e-11);
268         Assertions.assertEquals(pastMass, rebuiltPast.getMass(), 5.0e-13);
269 
270     }
271 
272     @Test
273     void testAdditionalStateKeplerian() {
274         final double mu = CelestialBodyFactory.getEarth().getGM();
275 
276         final double initialX = 7100e3;
277         final double initialY = 0.0;
278         final double initialZ = 1300e3;
279         final double initialVx = 0;
280         final double initialVy = 8000;
281         final double initialVz = 1000;
282 
283         final Vector3D position = new Vector3D(initialX, initialY, initialZ);
284         final Vector3D velocity = new Vector3D(initialVx, initialVy, initialVz);
285         final AbsoluteDate epoch = new AbsoluteDate(2010, 1, 1, 0, 0, 0, TimeScalesFactory.getUTC());
286         final TimeStampedPVCoordinates pv = new TimeStampedPVCoordinates(epoch, position, velocity, Vector3D.ZERO);
287         final Orbit initialOrbit = new CartesianOrbit(pv, FramesFactory.getEME2000(), mu);
288 
289         final double totalPropagationTime = 10;
290         final double deltaX = 0.01;
291         final double deltaY = 0.02;
292         final double deltaZ = 0.03;
293         final double isp = 300;
294 
295         final Vector3D deltaV = new Vector3D(deltaX, deltaY, deltaZ);
296 
297         final AttitudeProvider attitudeProvider = new LofOffset(initialOrbit.getFrame(), LOFType.VNC);
298         final Attitude initialAttitude = attitudeProvider.getAttitude(initialOrbit, initialOrbit.getDate(), initialOrbit.getFrame());
299         final SpacecraftState initialState = new SpacecraftState(initialOrbit, initialAttitude);
300         KeplerianPropagator propagator = new KeplerianPropagator(initialOrbit);
301         propagator.resetInitialState(initialState.addAdditionalData("testOnly", -1.0));
302         DateDetector dateDetector = new DateDetector(epoch.shiftedBy(0.5 * totalPropagationTime));
303         FrameAlignedProvider attitudeOverride = new FrameAlignedProvider(new Rotation(RotationOrder.XYX,
304                                                                               RotationConvention.VECTOR_OPERATOR,
305                                                                               0, 0, 0));
306         ImpulseManeuver burnAtEpoch = new ImpulseManeuver(dateDetector.withThreshold(1.0e-3), attitudeOverride, deltaV, isp);
307         propagator.addEventDetector(burnAtEpoch);
308 
309         SpacecraftState finalState = propagator.propagate(epoch.shiftedBy(totalPropagationTime));
310         Assertions.assertEquals(1, finalState.getAdditionalDataValues().size());
311         Assertions.assertEquals(-1.0, finalState.getAdditionalState("testOnly")[0], 1.0e-15);
312 
313     }
314 
315     @Test
316     void testAdditionalStateNumerical() {
317         final double mu = CelestialBodyFactory.getEarth().getGM();
318 
319         final double initialX = 7100e3;
320         final double initialY = 0.0;
321         final double initialZ = 1300e3;
322         final double initialVx = 0;
323         final double initialVy = 8000;
324         final double initialVz = 1000;
325 
326         final Vector3D position = new Vector3D(initialX, initialY, initialZ);
327         final Vector3D velocity = new Vector3D(initialVx, initialVy, initialVz);
328         final AbsoluteDate epoch = new AbsoluteDate(2010, 1, 1, 0, 0, 0, TimeScalesFactory.getUTC());
329         final TimeStampedPVCoordinates pv = new TimeStampedPVCoordinates(epoch, position, velocity, Vector3D.ZERO);
330         final Orbit initialOrbit = new CartesianOrbit(pv, FramesFactory.getEME2000(), mu);
331 
332         final double totalPropagationTime = 10.0;
333         final double deltaX = 0.01;
334         final double deltaY = 0.02;
335         final double deltaZ = 0.03;
336         final double isp = 300;
337 
338         final Vector3D deltaV = new Vector3D(deltaX, deltaY, deltaZ);
339 
340         final AttitudeProvider attitudeProvider = new LofOffset(initialOrbit.getFrame(), LOFType.VNC);
341         final Attitude initialAttitude = attitudeProvider.getAttitude(initialOrbit, initialOrbit.getDate(), initialOrbit.getFrame());
342 
343         double[][] tolerances = ToleranceProvider.getDefaultToleranceProvider(10.).getTolerances(initialOrbit, initialOrbit.getType());
344         DormandPrince853Integrator integrator = new DormandPrince853Integrator(1.0e-3, 60, tolerances[0], tolerances[1]);
345         NumericalPropagator propagator = new NumericalPropagator(integrator);
346         propagator.setOrbitType(initialOrbit.getType());
347         MatricesHarvester harvester = propagator.setupMatricesComputation("derivatives", null, null);
348         propagator.resetInitialState(new SpacecraftState(initialOrbit, initialAttitude));
349         DateDetector dateDetector = new DateDetector(epoch.shiftedBy(0.5 * totalPropagationTime));
350         FrameAlignedProvider attitudeOverride = new FrameAlignedProvider(new Rotation(RotationOrder.XYX,
351                                                                               RotationConvention.VECTOR_OPERATOR,
352                                                                               0, 0, 0));
353         ImpulseManeuver burnAtEpoch = new ImpulseManeuver(dateDetector.withThreshold(1.0e-3), attitudeOverride, deltaV, isp);
354         propagator.addEventDetector(burnAtEpoch);
355 
356         SpacecraftState finalState = propagator.propagate(epoch.shiftedBy(totalPropagationTime));
357         Assertions.assertEquals(1, finalState.getAdditionalDataValues().size());
358         Assertions.assertEquals(36, finalState.getAdditionalState("derivatives").length);
359 
360         RealMatrix stateTransitionMatrix = harvester.getStateTransitionMatrix(finalState);
361         for (int i = 0; i < 6; ++i) {
362             for (int j = 0; j < 6; ++j) {
363                 double sIJ = stateTransitionMatrix.getEntry(i, j);
364                 if (j == i) {
365                     // dPi/dPj and dVi/dVj are roughly 1 for small propagation times
366                     Assertions.assertEquals(1.0, sIJ, 2.0e-4);
367                 } else if (j == i + 3) {
368                     // dVi/dPi is roughly the propagation time for small propagation times
369                     Assertions.assertEquals(totalPropagationTime, sIJ, 4.0e-5 * totalPropagationTime);
370                 } else {
371                     // other derivatives are almost zero for small propagation times
372                     Assertions.assertEquals(0, sIJ, 1.0e-4);
373                 }
374             }
375         }
376 
377     }
378 
379     /**
380      * The test is inspired by the example given by melvina user in the Orekit forum.
381      * https://forum.orekit.org/t/python-error-using-impulsemaneuver-with-positionangledetector/771
382      */
383     @Test
384     void testIssue663() {
385 
386         // Initial orbit
387         final Orbit initialOrbit =
388                         new KeplerianOrbit(24532000.0, 0.72, 0.3, FastMath.PI, 0.4, 2.0,
389                                            PositionAngleType.MEAN, FramesFactory.getEME2000(),
390                                            new AbsoluteDate(new DateComponents(2008, 6, 23),
391                                                             new TimeComponents(14, 18, 37),
392                                                             TimeScalesFactory.getUTC()),
393                                            3.986004415e14);
394         // create maneuver's trigger
395         final InitializationDetector trigger = new InitializationDetector();
396 
397         // create maneuver
398         final AttitudeProvider attitudeOverride = new LofOffset(FramesFactory.getEME2000(), LOFType.TNW);
399         final Vector3D         deltaVSat        = Vector3D.PLUS_I;
400         final double           isp              = 1500.0;
401         final ImpulseManeuver maneuver = new ImpulseManeuver(trigger, attitudeOverride, deltaVSat, isp);
402 
403         // add maneuver to propagator
404         KeplerianPropagator propagator = new KeplerianPropagator(initialOrbit, attitudeOverride);
405         propagator.addEventDetector(maneuver);
406 
407         // propagation
408         Assertions.assertFalse(trigger.initialized);
409         propagator.propagate(initialOrbit.getDate().shiftedBy(3600.0));
410         Assertions.assertTrue(trigger.initialized);
411 
412     }
413 
414     @Test
415     void testControl3DVectorCostType() {
416 
417         // GIVEN
418         // Initial orbit
419         final double        a             = Constants.EGM96_EARTH_EQUATORIAL_RADIUS + 2000.e3;
420         final double        e             = 1e-4;
421         final double        i             = 0.5;
422         final double        pa            = 0.;
423         final double        raan          = 6.;
424         final double        anomaly       = 1.;
425         final PositionAngleType positionAngleType = PositionAngleType.MEAN;
426         final Frame gcrf                  = FramesFactory.getGCRF();
427         final AbsoluteDate  date          = AbsoluteDate.ARBITRARY_EPOCH;
428         final double        mu            = Constants.EGM96_EARTH_MU;
429 
430         final KeplerianOrbit orbit = new KeplerianOrbit(a, e, i, pa, raan, anomaly, positionAngleType, gcrf, date, mu);
431 
432         // Thrust configuration
433         final DateDetector     dateDetector       = new DateDetector(date.shiftedBy(1000.));
434         final AbsoluteDate     endPropagationDate = dateDetector.getDate().shiftedBy(10000.);
435         final AttitudeProvider attitudeProvider = new FrameAlignedProvider(gcrf);
436         final Vector3D         deltaV             = new Vector3D(2., -1., 0.5);
437         final ImpulseProvider impulseProvider = ImpulseProvider.of(deltaV);
438         final double           initialMass        = 1000.;
439         final double           isp                = 100.;
440 
441         // Building propagators
442         final KeplerianPropagator propagatorNone    = new KeplerianPropagator(orbit, attitudeProvider, orbit.getMu(), initialMass);
443         final KeplerianPropagator propagatorNorm1   = new KeplerianPropagator(orbit, attitudeProvider, orbit.getMu(), initialMass);
444         final KeplerianPropagator propagatorNorm2   = new KeplerianPropagator(orbit, attitudeProvider, orbit.getMu(), initialMass);
445         final KeplerianPropagator propagatorNormInf = new KeplerianPropagator(orbit, attitudeProvider, orbit.getMu(), initialMass);
446 
447         // Add impulse maneuvers
448         propagatorNone.addEventDetector(new ImpulseManeuver(dateDetector, attitudeProvider, impulseProvider, isp,
449                 Control3DVectorCostType.NONE));
450         propagatorNorm1.addEventDetector(new ImpulseManeuver(dateDetector, attitudeProvider, impulseProvider, isp,
451                 Control3DVectorCostType.ONE_NORM));
452         propagatorNorm2.addEventDetector(new ImpulseManeuver(dateDetector, attitudeProvider, impulseProvider, isp,
453                 Control3DVectorCostType.TWO_NORM));
454         propagatorNormInf.addEventDetector(new ImpulseManeuver(dateDetector, attitudeProvider, impulseProvider, isp,
455                 Control3DVectorCostType.INF_NORM));
456 
457         // WHEN
458         final double finalMassWithNone    = propagatorNone.propagate(endPropagationDate).getMass();
459         final double finalMassWithNorm1   = propagatorNorm1.propagate(endPropagationDate).getMass();
460         final double finalMassWithNorm2   = propagatorNorm2.propagate(endPropagationDate).getMass();
461         final double finalMassWithNormInf = propagatorNormInf.propagate(endPropagationDate).getMass();
462 
463         // THEN
464         // Assert that we do not find the same final mass when using different control vector norm
465         Assertions.assertNotEquals(finalMassWithNorm1, finalMassWithNorm2);
466         Assertions.assertNotEquals(finalMassWithNorm1, finalMassWithNormInf);
467         Assertions.assertNotEquals(finalMassWithNormInf, finalMassWithNorm2);
468 
469         // Assert that final mass is equal to expected mass
470         Assertions.assertEquals(initialMass, finalMassWithNone);
471         final double factorExponential = -1. / (isp * Constants.G0_STANDARD_GRAVITY);
472         Assertions.assertEquals(initialMass * FastMath.exp(deltaV.getNorm1() * factorExponential), finalMassWithNorm1);
473         Assertions.assertEquals(initialMass * FastMath.exp(deltaV.getNorm() * factorExponential), finalMassWithNorm2);
474         Assertions.assertEquals(initialMass * FastMath.exp(deltaV.getNormInf() * factorExponential), finalMassWithNormInf);
475     }
476 
477     @Deprecated
478     @Test
479     void testDeprecatedConstructor() {
480         // GIVEN
481         final double expectedIsp = 10;
482         // WHEN
483         final ImpulseManeuver maneuver = new ImpulseManeuver(new DateDetector(), null, Vector3D.ZERO,
484                 expectedIsp, Control3DVectorCostType.NONE);
485         // THEN
486         Assertions.assertEquals(expectedIsp, maneuver.getIsp());
487     }
488 
489     @Test
490     void testInit() {
491         // GIVEN
492         final AbsoluteDate date = AbsoluteDate.ARBITRARY_EPOCH;
493         final SpacecraftState mockedState = Mockito.mock(SpacecraftState.class);
494         Mockito.when(mockedState.getDate()).thenReturn(date);
495         final ImpulseProvider impulseProvider = ImpulseProvider.of(Vector3D.PLUS_I);
496         final ImpulseManeuver maneuver = new ImpulseManeuver(new DateDetector(), null, impulseProvider,
497                 1, Control3DVectorCostType.NONE);
498         // WHEN
499         maneuver.init(mockedState, date);
500     }
501 
502     @Test
503     void testFinish() {
504         // GIVEN
505         final AbsoluteDate date = AbsoluteDate.ARBITRARY_EPOCH;
506         final SpacecraftState mockedState = Mockito.mock(SpacecraftState.class);
507         Mockito.when(mockedState.getDate()).thenReturn(date);
508         final ImpulseProvider impulseProvider = ImpulseProvider.of(Vector3D.PLUS_I);
509         final ImpulseManeuver maneuver = new ImpulseManeuver(new DateDetector(), null, impulseProvider,
510                 1, Control3DVectorCostType.NONE);
511         // WHEN
512         maneuver.finish(mockedState);
513     }
514 
515     @Test
516     void testMultipleDates() {
517         // GIVEN
518         final Orbit initialOrbit = getOrbit();
519         final KeplerianPropagator propagator = new KeplerianPropagator(initialOrbit);
520         final AbsoluteDate[] dates = new AbsoluteDate[10];
521         for (int i = 0; i < dates.length; i++) {
522             dates[i] = initialOrbit.getDate().shiftedBy(i * 10 + 1);
523         }
524         final double isp = Double.POSITIVE_INFINITY;
525         final ImpulseManeuver maneuver = new ImpulseManeuver(new DateDetector(dates).withMaxCheck(5), null, new GrowingImpulseProvider(), isp,
526                 Control3DVectorCostType.NONE);
527         propagator.addEventDetector(maneuver);
528         // WHEN
529         final SpacecraftState actualState = propagator.propagate(dates[dates.length - 1].shiftedBy(1));
530         // THEN
531         propagator.resetInitialState(new SpacecraftState(initialOrbit));
532         propagator.clearEventsDetectors();
533         for (int i = 0; i < dates.length; i++) {
534             propagator.addEventDetector(new ImpulseManeuver(new DateDetector(dates[i]), Vector3D.PLUS_I.scalarMultiply(i + 1), isp));
535         }
536         final SpacecraftState expectedState = propagator.propagate(actualState.getDate());
537         final Vector3D relativePosition = expectedState.getPosition().subtract(actualState.getPosition());
538         Assertions.assertEquals(0., relativePosition.getNorm(), 1e-4);
539     }
540 
541     private static class GrowingImpulseProvider implements ImpulseProvider {
542         int count = 0;
543 
544         @Override
545         public Vector3D getImpulse(SpacecraftState state, boolean isForward) {
546             count++;
547             return Vector3D.PLUS_I.scalarMultiply(count);
548         }
549     }
550 
551     private static CartesianOrbit getOrbit() {
552         final double mu = CelestialBodyFactory.getEarth().getGM();
553         final double initialX = 7100e3;
554         final double initialY = 0.0;
555         final double initialZ = 1300e3;
556         final double initialVx = 0;
557         final double initialVy = 8000;
558         final double initialVz = 1000;
559         final Vector3D position = new Vector3D(initialX, initialY, initialZ);
560         final Vector3D velocity = new Vector3D(initialVx, initialVy, initialVz);
561         final AbsoluteDate epoch = new AbsoluteDate(2010, 1, 1, 0, 0, 0, TimeScalesFactory.getUTC());
562         final TimeStampedPVCoordinates pv = new TimeStampedPVCoordinates(epoch, position, velocity, Vector3D.ZERO);
563         return new CartesianOrbit(pv, FramesFactory.getEME2000(), mu);
564     }
565 
566     @BeforeEach
567     public void setUp() {
568         Utils.setDataRoot("regular-data");
569     }
570 
571     /** Private detector used for testing issue #663. */
572     private class InitializationDetector implements EventDetector {
573 
574         /** Flag for detector initialization. */
575         private boolean initialized;
576 
577         /** {@inheritDoc} */
578         @Override
579         public void init(final SpacecraftState s0, final AbsoluteDate t) {
580             EventDetector.super.init(s0, t);
581             this.initialized = true;
582         }
583 
584         /** {@inheritDoc} */
585         @Override
586         public double g(SpacecraftState s) {
587             return 1;
588         }
589 
590         @Override
591         public EventHandler getHandler() {
592             return new StopOnIncreasing();
593         }
594 
595     }
596 
597 }