1   /* Copyright 2020 Exotrail
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    * Exotrail 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.CalculusFieldElement;
20  import org.hipparchus.Field;
21  import org.hipparchus.geometry.euclidean.threed.Vector3D;
22  import org.hipparchus.ode.nonstiff.DormandPrince54FieldIntegrator;
23  import org.hipparchus.ode.nonstiff.DormandPrince54Integrator;
24  import org.hipparchus.util.Decimal64Field;
25  import org.hipparchus.util.FastMath;
26  import org.hipparchus.util.MathUtils;
27  import org.junit.Assert;
28  import org.junit.Before;
29  import org.junit.Test;
30  import org.orekit.Utils;
31  import org.orekit.attitudes.AttitudeProvider;
32  import org.orekit.attitudes.LofOffset;
33  import org.orekit.errors.OrekitException;
34  import org.orekit.errors.OrekitMessages;
35  import org.orekit.forces.maneuvers.propulsion.ThrustDirectionAndAttitudeProvider;
36  import org.orekit.forces.maneuvers.trigger.DateBasedManeuverTriggers;
37  import org.orekit.forces.maneuvers.trigger.EventBasedManeuverTriggers;
38  import org.orekit.frames.Frame;
39  import org.orekit.frames.FramesFactory;
40  import org.orekit.frames.LOFType;
41  import org.orekit.orbits.EquinoctialOrbit;
42  import org.orekit.orbits.FieldKeplerianOrbit;
43  import org.orekit.orbits.KeplerianOrbit;
44  import org.orekit.orbits.Orbit;
45  import org.orekit.orbits.OrbitType;
46  import org.orekit.orbits.PositionAngle;
47  import org.orekit.propagation.FieldSpacecraftState;
48  import org.orekit.propagation.SpacecraftState;
49  import org.orekit.propagation.events.AbstractDetector;
50  import org.orekit.propagation.events.BooleanDetector;
51  import org.orekit.propagation.events.EventDetector;
52  import org.orekit.propagation.events.NegateDetector;
53  import org.orekit.propagation.events.PositionAngleDetector;
54  import org.orekit.propagation.events.handlers.ContinueOnEvent;
55  import org.orekit.propagation.events.handlers.EventHandler;
56  import org.orekit.propagation.events.handlers.StopOnEvent;
57  import org.orekit.propagation.numerical.FieldNumericalPropagator;
58  import org.orekit.propagation.numerical.NumericalPropagator;
59  import org.orekit.time.AbsoluteDate;
60  import org.orekit.time.DateComponents;
61  import org.orekit.time.FieldAbsoluteDate;
62  import org.orekit.time.TimeComponents;
63  import org.orekit.time.TimeScalesFactory;
64  import org.orekit.utils.Constants;
65  import org.orekit.utils.FieldPVCoordinates;
66  import org.orekit.utils.IERSConventions;
67  
68  public class ConfigurableLowThrustManeuverTest {
69      /** */
70      private static double maxCheck = 100;
71      /** */
72      private static double maxThreshold = AbstractDetector.DEFAULT_THRESHOLD;
73  
74      /** */
75      private double thrust = 2e-3;
76      /** */
77      private double isp = 800;
78      /** */
79      private double halfThrustArc = FastMath.PI / 4;
80  
81      @Before
82      public void setUp() {
83          Utils.setDataRoot("regular-data");
84      }
85  
86      public abstract class EquinoctialLongitudeIntervalDetector<T extends EventDetector>
87              extends AbstractDetector<EquinoctialLongitudeIntervalDetector<T>> {
88  
89          /** */
90          private double halfArcLength;
91          /** */
92          private final PositionAngle type;
93  
94          /**
95           * Constructor.
96           *
97           * @param halfArcLength half length of the thrust arc. Must be in [0, pi]
98           * @param type
99           * @param handler
100          */
101         protected EquinoctialLongitudeIntervalDetector(final double halfArcLength, final PositionAngle type,
102                 final EventHandler<? super EquinoctialLongitudeIntervalDetector<T>> handler) {
103             this(halfArcLength, type, maxCheck, maxThreshold, DEFAULT_MAX_ITER, handler);
104         }
105 
106         public EquinoctialLongitudeIntervalDetector(final double halfArcLength, final PositionAngle type,
107                 final double maxCheck, final double threshold, final int maxIter,
108                 final EventHandler<? super EquinoctialLongitudeIntervalDetector<T>> handler) {
109             super(maxCheck, threshold, maxIter, handler);
110             this.halfArcLength = halfArcLength;
111             this.type = type;
112         }
113 
114         public abstract double getReferenceEquinoctialLongitude(SpacecraftState s); // node ...
115 
116         @Override
117         public double g(final SpacecraftState s) {
118             if (halfArcLength <= 0) {
119                 return -1;
120             }
121             // cast is safe because type guaranteed to match
122             final EquinoctialOrbit orbit = (EquinoctialOrbit) OrbitType.EQUINOCTIAL.convertType(s.getOrbit());
123             final double current = MathUtils.normalizeAngle(orbit.getL(type), 0);
124             final double ret;
125             final double lonStart = getReferenceEquinoctialLongitude(s) - halfArcLength;
126             final double lonEnd = getReferenceEquinoctialLongitude(s) + halfArcLength;
127 
128             final double diffStart = MathUtils.normalizeAngle(current, lonStart) - lonStart;
129             final double diffEnd = MathUtils.normalizeAngle(lonEnd, current) - current;
130 
131             final double sin1 = FastMath.sin(diffStart);
132             final double sin2 = FastMath.sin(-diffEnd);
133             if (lonEnd - lonStart < FastMath.PI) {
134                 ret = FastMath.min(sin1, -sin2);
135             } else {
136                 ret = FastMath.max(sin1, -sin2);
137             }
138             if (Double.isNaN(ret)) {
139                 if (Double.isNaN(current)) {
140                     throw new RuntimeException("Detector of type   " + this.getClass().getSimpleName() +
141                             " returned NaN because bad orbit provided");
142                 }
143                 throw new RuntimeException("Detector of type  " + this.getClass().getSimpleName() + "  returned NaN");
144             }
145             return ret;
146         }
147 
148         public double getHalfArcLength() {
149             return halfArcLength;
150         }
151 
152         public PositionAngle getType() {
153             return type;
154         }
155 
156         /**
157          * 0 to disable the detector (will return -1 for g).
158          *
159          * @param halfArcLength
160          */
161         public void setHalfArcLength(final double halfArcLength) {
162             if (Double.isNaN(halfArcLength)) {
163                 throw new RuntimeException("Trying to set an half arc with NaN value on " + getClass().getSimpleName());
164             }
165             if (halfArcLength < 0) {
166                 throw new RuntimeException("Trying to set a negative value (" + FastMath.toDegrees(halfArcLength) +
167                         "deg) on  " + getClass().getSimpleName());
168             }
169             if (halfArcLength >= FastMath.PI) {
170                 throw new RuntimeException("Trying to set an half arc higher than PI (" +
171                         FastMath.toDegrees(halfArcLength) + "deg) on " + getClass().getSimpleName());
172             }
173 
174             this.halfArcLength = halfArcLength;
175         }
176 
177     }
178 
179     public class PerigeeCenteredIntervalDetector
180             extends EquinoctialLongitudeIntervalDetector<PerigeeCenteredIntervalDetector> {
181 
182         protected PerigeeCenteredIntervalDetector(final double halfArcLength, final PositionAngle type,
183                 final double maxCheck, final double threshold, final int maxIter,
184                 final EventHandler<? super EquinoctialLongitudeIntervalDetector<PerigeeCenteredIntervalDetector>> handler) {
185 
186             super(halfArcLength, type, maxCheck, threshold, maxIter, handler);
187         }
188 
189         public PerigeeCenteredIntervalDetector(final double halfArcLength, final PositionAngle type,
190                 final EventHandler<? super EquinoctialLongitudeIntervalDetector<PerigeeCenteredIntervalDetector>> handler) {
191 
192             super(halfArcLength, type, maxCheck, maxThreshold, DEFAULT_MAX_ITER, handler);
193         }
194 
195         @Override
196         public double getReferenceEquinoctialLongitude(final SpacecraftState s) {
197 
198             final KeplerianOrbit orb = (KeplerianOrbit) OrbitType.KEPLERIAN.convertType(s.getOrbit());
199 
200             final double longitudeOfPerigee = orb.getRightAscensionOfAscendingNode() + orb.getPerigeeArgument();
201 
202             return longitudeOfPerigee;
203         }
204 
205         @Override
206         protected EquinoctialLongitudeIntervalDetector<PerigeeCenteredIntervalDetector> create(final double newMaxCheck,
207                 final double newThreshold, final int newMaxIter,
208                 final EventHandler<? super EquinoctialLongitudeIntervalDetector<PerigeeCenteredIntervalDetector>> newHandler) {
209             return new PerigeeCenteredIntervalDetector(getHalfArcLength(), getType(), newMaxCheck, newThreshold,
210                     newMaxIter, newHandler);
211         }
212     }
213 
214     public class ApogeeCenteredIntervalDetector
215             extends EquinoctialLongitudeIntervalDetector<ApogeeCenteredIntervalDetector> {
216 
217         protected ApogeeCenteredIntervalDetector(final double halfArcLength, final PositionAngle type,
218                 final double maxCheck, final double threshold, final int maxIter,
219                 final EventHandler<? super EquinoctialLongitudeIntervalDetector<ApogeeCenteredIntervalDetector>> handler) {
220 
221             super(halfArcLength, type, maxCheck, threshold, maxIter, handler);
222         }
223 
224         public ApogeeCenteredIntervalDetector(final double halfArcLength, final PositionAngle type,
225                 final EventHandler<? super EquinoctialLongitudeIntervalDetector<ApogeeCenteredIntervalDetector>> handler) {
226 
227             super(halfArcLength, type, maxCheck, maxThreshold, DEFAULT_MAX_ITER, handler);
228         }
229 
230         @Override
231         public double getReferenceEquinoctialLongitude(final SpacecraftState s) {
232 
233             final KeplerianOrbit orb = (KeplerianOrbit) OrbitType.KEPLERIAN.convertType(s.getOrbit());
234 
235             final double longitudeOfApogee = orb.getRightAscensionOfAscendingNode() + orb.getPerigeeArgument() +
236                     FastMath.PI;
237 
238             return longitudeOfApogee;
239         }
240 
241         @Override
242         protected EquinoctialLongitudeIntervalDetector<ApogeeCenteredIntervalDetector> create(final double newMaxCheck,
243                 final double newThreshold, final int newMaxIter,
244                 final EventHandler<? super EquinoctialLongitudeIntervalDetector<ApogeeCenteredIntervalDetector>> newHandler) {
245 
246             return new ApogeeCenteredIntervalDetector(getHalfArcLength(), getType(), newMaxCheck, newThreshold,
247                     newMaxIter, newHandler);
248         }
249     }
250 
251     public class DateIntervalDetector extends AbstractDetector<DateIntervalDetector> {
252 
253         /** */
254         private final AbsoluteDate startDate;
255         /** */
256         private final AbsoluteDate endDate;
257 
258         public DateIntervalDetector(final AbsoluteDate startDate, final AbsoluteDate endDate) {
259             this(startDate, endDate, 1.0e10, 1.e-9 /* values from DateDetector */, DEFAULT_MAX_ITER,
260                     new StopOnEvent<DateIntervalDetector>());
261         }
262 
263         protected DateIntervalDetector(final AbsoluteDate startDate, final AbsoluteDate endDate, final double maxCheck,
264                 final double threshold, final int maxIter, final EventHandler<? super DateIntervalDetector> handler) {
265             super(maxCheck, threshold, maxIter, handler);
266             this.startDate = startDate;
267             this.endDate = endDate;
268             if (startDate.durationFrom(endDate) >= 0) {
269                 throw new RuntimeException("StartDate(" + startDate + ") should be before EndDate(" + endDate + ")");
270             }
271         }
272 
273         @Override
274         public double g(final SpacecraftState s) {
275             final AbsoluteDate gDate = s.getDate();
276             final double durationFromStart = gDate.durationFrom(startDate);
277             if (durationFromStart < 0) {
278                 return durationFromStart; // before interval
279             }
280             final double durationBeforeEnd = endDate.durationFrom(gDate);
281             if (durationBeforeEnd < 0) {
282                 return durationBeforeEnd; // after interval
283             }
284 
285             final double ret = FastMath.min(durationFromStart, durationBeforeEnd); // take the closest date
286             if (Double.isNaN(ret)) {
287                 throw new RuntimeException("Detector of type " + this.getClass().getSimpleName() + " returned NaN");
288             }
289             return ret;
290         }
291 
292         @Override
293         protected DateIntervalDetector create(final double newMaxCheck, final double newThreshold, final int newMaxIter,
294                 final EventHandler<? super DateIntervalDetector> newHandler) {
295             return new DateIntervalDetector(startDate, endDate, newMaxCheck, newThreshold, newMaxIter, newHandler);
296         }
297 
298     }
299 
300     public static NumericalPropagator buildNumericalPropagator(final Orbit initialOrbit) {
301         final OrbitType orbitType = OrbitType.EQUINOCTIAL;
302         final double minStep = 1e-6;
303         final double maxStep = 100;
304 
305         final double[][] tol = NumericalPropagator.tolerances(1.0e-5, initialOrbit, orbitType);
306         final DormandPrince54Integrator integrator = new DormandPrince54Integrator(minStep, maxStep, tol[0], tol[1]);
307         final NumericalPropagator propagator = new NumericalPropagator(integrator);
308         propagator.setOrbitType(orbitType);
309         propagator.setAttitudeProvider(buildVelocityAttitudeProvider(initialOrbit.getFrame()));
310         return propagator;
311     }
312 
313     private static KeplerianOrbit buildInitOrbit() {
314         return buildInitOrbitWithAnomaly(FastMath.toRadians(0));
315     }
316 
317     private static KeplerianOrbit buildInitOrbitWithAnomaly(final double anomaly) {
318         final AbsoluteDate date = new AbsoluteDate(2020, 01, 01, TimeScalesFactory.getUTC());
319 
320         final double sma = Constants.EGM96_EARTH_EQUATORIAL_RADIUS + 700e3;
321         final double ecc = 0.01;
322         final double inc = FastMath.toRadians(60);
323         final double pa = FastMath.toRadians(0);
324         final double raan = 0.;
325 
326         final KeplerianOrbit kep = new KeplerianOrbit(sma, ecc, inc, pa, raan, anomaly, PositionAngle.MEAN,
327                 FramesFactory.getCIRF(IERSConventions.IERS_2010, true), date, Constants.EGM96_EARTH_MU);
328         return kep;
329     }
330 
331     private static AttitudeProvider buildVelocityAttitudeProvider(final Frame frame) {
332         return new LofOffset(frame, LOFType.TNW);
333     }
334 
335     private static ThrustDirectionAndAttitudeProvider buildVelocityThrustDirectionProvider() {
336         return ThrustDirectionAndAttitudeProvider.buildFromFixedDirectionInSatelliteFrame(Vector3D.PLUS_I);
337     }
338 
339     private ConfigurableLowThrustManeuver buildApogeeManeuver() {
340 
341         final ApogeeCenteredIntervalDetector maneuverStartDetector = new ApogeeCenteredIntervalDetector(halfThrustArc,
342                 PositionAngle.MEAN, new ContinueOnEvent<>());
343         final NegateDetector maneuverStopDetector = BooleanDetector.notCombine(maneuverStartDetector);
344 
345         // thrust in velocity direction to increase semi-major-axis
346         return new ConfigurableLowThrustManeuver(buildVelocityThrustDirectionProvider(), maneuverStartDetector,
347                 maneuverStopDetector, thrust, isp);
348     }
349 
350     private ConfigurableLowThrustManeuver buildPerigeeManeuver() {
351 
352         final PerigeeCenteredIntervalDetector maneuverStartDetector = new PerigeeCenteredIntervalDetector(halfThrustArc,
353                 PositionAngle.MEAN, new ContinueOnEvent<>());
354 
355         final NegateDetector maneuverStopDetector = BooleanDetector.notCombine(maneuverStartDetector);
356 
357         // thrust in velocity direction to increase semi-major-axis
358         return new ConfigurableLowThrustManeuver(buildVelocityThrustDirectionProvider(), maneuverStartDetector,
359                 maneuverStopDetector, thrust, isp);
360     }
361 
362     private ConfigurableLowThrustManeuver buildPsoManeuver() {
363 
364         final PositionAngleDetector maneuverStartDetector = new PositionAngleDetector(OrbitType.EQUINOCTIAL,
365                                                                                       PositionAngle.MEAN, FastMath.toRadians(0.0));
366 
367         final PositionAngleDetector maneuverStopDetector = new PositionAngleDetector(OrbitType.EQUINOCTIAL,
368                                                                                      PositionAngle.MEAN, FastMath.toRadians(90.0));
369 
370         // thrust in velocity direction to increase semi-major-axis
371         return new ConfigurableLowThrustManeuver(buildVelocityThrustDirectionProvider(), maneuverStartDetector,
372                 maneuverStopDetector, thrust, isp);
373     }
374 
375     @Test
376     public void testNominalUseCase() {
377         /////////////////// initial conditions /////////////////////////////////
378         final KeplerianOrbit initOrbit = buildInitOrbit();
379         final double initMass = 20;
380         final SpacecraftState initialState = new SpacecraftState(initOrbit, initMass);
381         final AbsoluteDate initialDate = initOrbit.getDate();
382         final double simulationDuration = 2 * 86400;
383         final AbsoluteDate finalDate = initialDate.shiftedBy(simulationDuration);
384 
385         /////////////////// propagations /////////////////////////////////
386 
387         final NumericalPropagator numericalPropagator = buildNumericalPropagator(initOrbit);
388         numericalPropagator.addForceModel(buildApogeeManeuver());
389         numericalPropagator.addForceModel(buildPerigeeManeuver());
390         numericalPropagator.setInitialState(initialState);
391         final SpacecraftState finalStateNumerical = numericalPropagator.propagate(finalDate);
392 
393         /////////////////// results check /////////////////////////////////
394         final double expectedPropellantConsumption = -0.022;
395         final double expectedDeltaSemiMajorAxisRealized = 16397;
396         Assert.assertEquals(expectedPropellantConsumption, finalStateNumerical.getMass() - initialState.getMass(),
397                 0.005);
398         Assert.assertEquals(expectedDeltaSemiMajorAxisRealized, finalStateNumerical.getA() - initialState.getA(), 100);
399     }
400 
401     @Test
402     public void testBackwardPropagationDisabled() {
403         /////////////////// initial conditions /////////////////////////////////
404         final KeplerianOrbit initOrbit = buildInitOrbit();
405         final double initMass = 20;
406         final SpacecraftState initialState = new SpacecraftState(initOrbit, initMass);
407         final AbsoluteDate initialDate = initOrbit.getDate();
408         final double simulationDuration = -2 * 86400; // backward
409         final AbsoluteDate finalDate = initialDate.shiftedBy(simulationDuration);
410 
411         /////////////////// propagation /////////////////////////////////
412         final NumericalPropagator numericalPropagatorForward = buildNumericalPropagator(initOrbit);
413         numericalPropagatorForward.addForceModel(buildApogeeManeuver());
414         numericalPropagatorForward.setInitialState(initialState);
415         try {
416             numericalPropagatorForward.propagate(finalDate);
417             Assert.fail("an exception should have been thrown");
418         } catch (OrekitException oe) {
419             Assert.assertEquals(OrekitMessages.BACKWARD_PROPAGATION_NOT_ALLOWED, oe.getSpecifier());
420         }
421 
422     }
423 
424     @Test
425     public void testFielddPropagationDisabled() {
426         doTestFielddPropagationDisabled(Decimal64Field.getInstance());
427     }
428 
429     private <T extends CalculusFieldElement<T>> void doTestFielddPropagationDisabled(Field<T> field) {
430         /////////////////// initial conditions /////////////////////////////////
431         final KeplerianOrbit o = buildInitOrbit();
432         final FieldKeplerianOrbit<T> initOrbit = new FieldKeplerianOrbit<>(new FieldPVCoordinates<>(field, o.getPVCoordinates()),
433                                                                            o.getFrame(), new FieldAbsoluteDate<>(field, o.getDate()),
434                                                                            field.getZero().newInstance(o.getMu()));
435         final double initMass = 20;
436         final FieldSpacecraftState<T> initialState = new FieldSpacecraftState<>(initOrbit, field.getZero().newInstance(initMass));
437         final FieldAbsoluteDate<T> initialDate = initOrbit.getDate();
438         final double simulationDuration = -2 * 86400; // backward
439         final FieldAbsoluteDate<T> finalDate = initialDate.shiftedBy(simulationDuration);
440 
441         /////////////////// propagation /////////////////////////////////
442         final OrbitType orbitType = OrbitType.EQUINOCTIAL;
443         final double minStep = 1e-6;
444         final double maxStep = 100;
445 
446         final double[][] tol = FieldNumericalPropagator.tolerances(field.getZero().newInstance(1.0e-5), initOrbit, orbitType);
447         final DormandPrince54FieldIntegrator<T> integrator = new DormandPrince54FieldIntegrator<>(field, minStep, maxStep, tol[0], tol[1]);
448         final FieldNumericalPropagator<T> propagator = new FieldNumericalPropagator<>(field, integrator);
449         propagator.setOrbitType(orbitType);
450         propagator.setAttitudeProvider(buildVelocityAttitudeProvider(o.getFrame()));
451         propagator.addForceModel(buildApogeeManeuver());
452         propagator.setInitialState(initialState);
453         try {
454             propagator.propagate(finalDate);
455             Assert.fail("an exception should have been thrown");
456         } catch (OrekitException oe) {
457             Assert.assertEquals(OrekitMessages.FUNCTION_NOT_IMPLEMENTED, oe.getSpecifier());
458             Assert.assertEquals("EventBasedManeuverTriggers.getFieldEventsDetectors", oe.getParts()[0]);
459         }
460 
461     }
462 
463     @Test
464     public void testDateBasedManeuverTriggers() {
465 
466         final double f = 0.1;
467         final double isp = 2000;
468         final double duration = 3000.0;
469  
470         final Orbit orbit =
471             new KeplerianOrbit(24396159, 0.72831215, FastMath.toRadians(7),
472                                FastMath.toRadians(180), FastMath.toRadians(261),
473                                FastMath.toRadians(0.0), PositionAngle.MEAN,
474                                FramesFactory.getEME2000(),
475                                new AbsoluteDate(new DateComponents(2004, 01, 01),
476                                                 new TimeComponents(23, 30, 00.000),
477                                                 TimeScalesFactory.getUTC()),
478                                Constants.EIGEN5C_EARTH_MU);
479         final AttitudeProvider law = new LofOffset(orbit.getFrame(), LOFType.LVLH_CCSDS);
480         final SpacecraftState initialState =
481             new SpacecraftState(orbit, law.getAttitude(orbit, orbit.getDate(), orbit.getFrame()), 2500.0);
482         final AbsoluteDate startDate = orbit.getDate().shiftedBy(17461.084);
483 
484         // forward propagation
485         final NumericalPropagator forwardPropagator = buildNumericalPropagator(orbit);
486         forwardPropagator.addForceModel(new ConfigurableLowThrustManeuver(buildVelocityThrustDirectionProvider(),
487                                                                           new DateBasedManeuverTriggers(startDate, duration),
488                                                                           f, isp));
489         forwardPropagator.setInitialState(initialState);
490         final SpacecraftState finalStateNumerical = forwardPropagator.propagate(startDate.shiftedBy(duration + 900.0));
491 
492         // backward propagation
493         final NumericalPropagator backwardPropagator = buildNumericalPropagator(finalStateNumerical.getOrbit());
494         backwardPropagator.addForceModel(new ConfigurableLowThrustManeuver(buildVelocityThrustDirectionProvider(),
495                                                                            new DateBasedManeuverTriggers(startDate, duration),
496                                                                            f, isp));
497         backwardPropagator.setInitialState(finalStateNumerical);
498         final SpacecraftState recoveredStateNumerical = backwardPropagator.propagate(orbit.getDate());
499 
500         /////////////////// results check /////////////////////////////////
501         Assert.assertEquals(0.0,
502                             Vector3D.distance(orbit.getPVCoordinates().getPosition(),
503                                               recoveredStateNumerical.getPVCoordinates().getPosition()),
504                             0.015);
505 
506     }
507 
508     @Test
509     public void testBackwardPropagationEnabled() {
510 
511         final double f = 0.1;
512         final double isp = 2000;
513         final double duration = 3000.0;
514  
515         final Orbit orbit =
516             new KeplerianOrbit(24396159, 0.72831215, FastMath.toRadians(7),
517                                FastMath.toRadians(180), FastMath.toRadians(261),
518                                FastMath.toRadians(0.0), PositionAngle.MEAN,
519                                FramesFactory.getEME2000(),
520                                new AbsoluteDate(new DateComponents(2004, 01, 01),
521                                                 new TimeComponents(23, 30, 00.000),
522                                                 TimeScalesFactory.getUTC()),
523                                Constants.EIGEN5C_EARTH_MU);
524         final AttitudeProvider law = new LofOffset(orbit.getFrame(), LOFType.LVLH_CCSDS);
525         final SpacecraftState initialState =
526             new SpacecraftState(orbit, law.getAttitude(orbit, orbit.getDate(), orbit.getFrame()), 2500.0);
527         final AbsoluteDate startDate = orbit.getDate().shiftedBy(17461.084);
528         final DateIntervalDetector intervalDetector = new DateIntervalDetector(startDate, startDate.shiftedBy(duration));
529 
530         // forward propagation
531         final NumericalPropagator forwardPropagator = buildNumericalPropagator(orbit);
532         final EventBasedManeuverTriggers forwardDetector = new EventBasedManeuverTriggers(intervalDetector,
533                                                                                           BooleanDetector.notCombine(intervalDetector),
534                                                                                           true);
535         forwardPropagator.addForceModel(new ConfigurableLowThrustManeuver(buildVelocityThrustDirectionProvider(),
536                                                                           forwardDetector, f, isp));
537         forwardPropagator.setInitialState(initialState);
538         final SpacecraftState finalStateNumerical = forwardPropagator.propagate(startDate.shiftedBy(duration + 900.0));
539         Assert.assertEquals(0.0, forwardDetector.getTriggeredStart().durationFrom(startDate), 1.0e-16);
540         Assert.assertEquals(duration, forwardDetector.getTriggeredEnd().durationFrom(startDate), 1.0e-16);
541 
542         // backward propagation
543         final NumericalPropagator backwardPropagator = buildNumericalPropagator(finalStateNumerical.getOrbit());
544         final EventBasedManeuverTriggers backwardDetector = new EventBasedManeuverTriggers(intervalDetector,
545                                                                                            BooleanDetector.notCombine(intervalDetector),
546                                                                                            true);
547         backwardPropagator.addForceModel(new ConfigurableLowThrustManeuver(buildVelocityThrustDirectionProvider(),
548                                                                            backwardDetector, f, isp));
549         backwardPropagator.setInitialState(finalStateNumerical);
550         final SpacecraftState recoveredStateNumerical = backwardPropagator.propagate(orbit.getDate());
551         Assert.assertEquals(0.0, backwardDetector.getTriggeredStart().durationFrom(startDate), 1.0e-16);
552         Assert.assertEquals(duration, backwardDetector.getTriggeredEnd().durationFrom(startDate), 1.0e-16);
553 
554         Assert.assertFalse(backwardDetector.isFiring(new FieldAbsoluteDate<>(Decimal64Field.getInstance(), startDate.shiftedBy(-0.001)),
555                                                      null));
556         Assert.assertTrue(backwardDetector.isFiring(new FieldAbsoluteDate<>(Decimal64Field.getInstance(), startDate.shiftedBy(+0.001)),
557                                                      null));
558         Assert.assertTrue(backwardDetector.isFiring(new FieldAbsoluteDate<>(Decimal64Field.getInstance(), startDate.shiftedBy(duration - 0.001)),
559                                                      null));
560         Assert.assertFalse(backwardDetector.isFiring(new FieldAbsoluteDate<>(Decimal64Field.getInstance(), startDate.shiftedBy(duration + 0.001)),
561                                                      null));
562         /////////////////// results check /////////////////////////////////
563         Assert.assertEquals(0.0,
564                             Vector3D.distance(orbit.getPVCoordinates().getPosition(),
565                                               recoveredStateNumerical.getPVCoordinates().getPosition()),
566                             0.015);
567 
568     }
569 
570     @Test
571     public void testInitBefore() {
572         // thrust arc is around apogee which is on anomaly PI
573         /////////////////// initial conditions /////////////////////////////////
574         final KeplerianOrbit initOrbit = buildInitOrbitWithAnomaly(0);
575         final double initMass = 20;
576         final SpacecraftState initialState = new SpacecraftState(initOrbit, initMass);
577         final AbsoluteDate initialDate = initOrbit.getDate();
578         final double simulationDuration = 10;
579         final AbsoluteDate finalDate = initialDate.shiftedBy(simulationDuration);
580 
581         /////////////////// propagation /////////////////////////////////
582         final NumericalPropagator numericalPropagatorForward = buildNumericalPropagator(initOrbit);
583         numericalPropagatorForward.addForceModel(buildApogeeManeuver());
584         numericalPropagatorForward.setInitialState(initialState);
585         final SpacecraftState finalState = numericalPropagatorForward.propagate(finalDate);
586         // check firing did not happened
587         Assert.assertTrue(finalState.getMass() == initialState.getMass());
588     }
589 
590     @Test
591     public void testInitNearStart() {
592         // thrust arc is around apogee which is on anomaly PI
593         /////////////////// initial conditions /////////////////////////////////
594         // we can not start exactly on start due to angle conversion, the g function is
595         // equal to 1e-15 so this is not exactly the test of the specific specific use
596         // case. Another test based on
597         // other detectors will do that
598         final KeplerianOrbit initOrbit = buildInitOrbitWithAnomaly(FastMath.PI - halfThrustArc);
599         final double initMass = 20;
600         final SpacecraftState initialState = new SpacecraftState(initOrbit, initMass);
601         final AbsoluteDate initialDate = initOrbit.getDate();
602         final double simulationDuration = 10;
603         final AbsoluteDate finalDate = initialDate.shiftedBy(simulationDuration);
604 
605         /////////////////// propagation /////////////////////////////////
606         final NumericalPropagator numericalPropagatorForward = buildNumericalPropagator(initOrbit);
607         final ConfigurableLowThrustManeuver maneuver = buildApogeeManeuver();
608         numericalPropagatorForward.addForceModel(maneuver);
609         numericalPropagatorForward.setInitialState(initialState);
610         final SpacecraftState finalState = numericalPropagatorForward.propagate(finalDate);
611         // check firing happened
612         Assert.assertTrue(finalState.getMass() < initialState.getMass());
613 
614         // call init again, to check nothing weir happens (and improving test coverage)
615         maneuver.init(initialState, finalDate);
616          
617     }
618 
619     @Test
620     public void testInitOnStart() {
621         /////////////////// initial conditions /////////////////////////////////
622         final KeplerianOrbit initOrbit = buildInitOrbit();
623         final double initMass = 20;
624         final SpacecraftState initialState = new SpacecraftState(initOrbit, initMass);
625         final AbsoluteDate initialDate = initOrbit.getDate();
626         final double simulationDuration = 10;
627         final AbsoluteDate finalDate = initialDate.shiftedBy(simulationDuration);
628 
629         final DateIntervalDetector maneuverStartDetector = new DateIntervalDetector(initialDate,
630                 initialDate.shiftedBy(60));
631         final NegateDetector maneuverStopDetector = BooleanDetector.notCombine(maneuverStartDetector);
632 
633         final ConfigurableLowThrustManeuver maneuver = new ConfigurableLowThrustManeuver(
634                 buildVelocityThrustDirectionProvider(), maneuverStartDetector, maneuverStopDetector, thrust, isp);
635 
636         /////////////////// propagation /////////////////////////////////
637         final NumericalPropagator numericalPropagatorForward = buildNumericalPropagator(initOrbit);
638         numericalPropagatorForward.addForceModel(maneuver);
639         numericalPropagatorForward.setInitialState(initialState);
640         final SpacecraftState finalState = numericalPropagatorForward.propagate(finalDate);
641         // check firing happened
642         Assert.assertTrue(finalState.getMass() < initialState.getMass());
643     }
644 
645     @Test
646     public void testInitFiring() {
647         // thrust arc is around apogee which is on anomaly PI
648         /////////////////// initial conditions /////////////////////////////////
649         final KeplerianOrbit initOrbit = buildInitOrbitWithAnomaly(FastMath.PI);
650         final double initMass = 20;
651         final SpacecraftState initialState = new SpacecraftState(initOrbit, initMass);
652         final AbsoluteDate initialDate = initOrbit.getDate();
653         final double simulationDuration = 10;
654         final AbsoluteDate finalDate = initialDate.shiftedBy(simulationDuration);
655 
656         /////////////////// propagation /////////////////////////////////
657         final NumericalPropagator numericalPropagatorForward = buildNumericalPropagator(initOrbit);
658         numericalPropagatorForward.addForceModel(buildApogeeManeuver());
659         numericalPropagatorForward.setInitialState(initialState);
660         final SpacecraftState finalState = numericalPropagatorForward.propagate(finalDate);
661         // check firing happened
662         Assert.assertTrue(finalState.getMass() < initialState.getMass());
663     }
664 
665     @Test
666     public void testInitNearEndOfStart() {
667         // thrust arc is around apogee which is on anomaly PI
668         /////////////////// initial conditions /////////////////////////////////
669         // we can not start exactly on end of start due to angle conversion, the g
670         // function is
671         // equal to 1e-15 so this is not exactly the test of the specific use case.
672         // Another test based on
673         // other detector will do that
674         final KeplerianOrbit initOrbit = buildInitOrbitWithAnomaly(FastMath.PI + halfThrustArc);
675         final double initMass = 20;
676         final SpacecraftState initialState = new SpacecraftState(initOrbit, initMass);
677         final AbsoluteDate initialDate = initOrbit.getDate();
678         final double simulationDuration = 10;
679         final AbsoluteDate finalDate = initialDate.shiftedBy(simulationDuration);
680 
681         /////////////////// propagation /////////////////////////////////
682         final NumericalPropagator numericalPropagatorForward = buildNumericalPropagator(initOrbit);
683         numericalPropagatorForward.addForceModel(buildApogeeManeuver());
684         numericalPropagatorForward.setInitialState(initialState);
685         final SpacecraftState finalState = numericalPropagatorForward.propagate(finalDate);
686         // check firing did not happened
687         Assert.assertTrue(finalState.getMass() == initialState.getMass());
688     }
689 
690     @Test
691     public void testInitOnStop() {
692         /////////////////// initial conditions /////////////////////////////////
693         final KeplerianOrbit initOrbit = buildInitOrbit();
694         final double initMass = 20;
695         final SpacecraftState initialState = new SpacecraftState(initOrbit, initMass);
696         final AbsoluteDate initialDate = initOrbit.getDate();
697         final double simulationDuration = 10;
698         final AbsoluteDate finalDate = initialDate.shiftedBy(simulationDuration);
699 
700         final DateIntervalDetector maneuverStartDetector = new DateIntervalDetector(initialDate.shiftedBy(-60),
701                 initialDate);
702         final NegateDetector maneuverStopDetector = BooleanDetector.notCombine(maneuverStartDetector);
703 
704         final ConfigurableLowThrustManeuver maneuver = new ConfigurableLowThrustManeuver(
705                 buildVelocityThrustDirectionProvider(), maneuverStartDetector, maneuverStopDetector, thrust, isp);
706 
707         /////////////////// propagation /////////////////////////////////
708         final NumericalPropagator numericalPropagatorForward = buildNumericalPropagator(initOrbit);
709         numericalPropagatorForward.addForceModel(maneuver);
710         numericalPropagatorForward.setInitialState(initialState);
711         final SpacecraftState finalState = numericalPropagatorForward.propagate(finalDate);
712         // check firing did not happen
713         Assert.assertTrue(finalState.getMass() == initialState.getMass());
714     }
715 
716     @Test
717     public void testInitAfter() {
718         // thrust arc is around apogee which is on anomaly PI
719         /////////////////// initial conditions /////////////////////////////////
720         final KeplerianOrbit initOrbit = buildInitOrbitWithAnomaly(FastMath.PI + 2 * halfThrustArc);
721         final double initMass = 20;
722         final SpacecraftState initialState = new SpacecraftState(initOrbit, initMass);
723         final AbsoluteDate initialDate = initOrbit.getDate();
724         final double simulationDuration = 10;
725         final AbsoluteDate finalDate = initialDate.shiftedBy(simulationDuration);
726 
727         /////////////////// propagation /////////////////////////////////
728         final NumericalPropagator numericalPropagatorForward = buildNumericalPropagator(initOrbit);
729         numericalPropagatorForward.addForceModel(buildApogeeManeuver());
730         numericalPropagatorForward.setInitialState(initialState);
731         final SpacecraftState finalState = numericalPropagatorForward.propagate(finalDate);
732         // check firing did not happened
733         Assert.assertTrue(finalState.getMass() == initialState.getMass());
734     }
735 
736     @Test
737     public void testGetters() {
738         final ApogeeCenteredIntervalDetector maneuverStartDetector = new ApogeeCenteredIntervalDetector(halfThrustArc,
739                 PositionAngle.MEAN, new ContinueOnEvent<>());
740         final NegateDetector maneuverStopDetector = BooleanDetector.notCombine(maneuverStartDetector);
741 
742         final ThrustDirectionAndAttitudeProvider attitudeProvider = buildVelocityThrustDirectionProvider();
743         final ConfigurableLowThrustManeuver maneuver = new ConfigurableLowThrustManeuver(attitudeProvider,
744                 maneuverStartDetector, maneuverStopDetector, thrust, isp);
745         Assert.assertEquals(isp, maneuver.getISP(), 1e-9);
746         Assert.assertEquals(thrust, maneuver.getThrust(), 1e-9);
747         Assert.assertEquals(attitudeProvider, maneuver.getThrustDirectionProvider());
748 
749     }
750 
751     @Test
752     public void testIssue874() {
753         /////////////////// initial conditions /////////////////////////////////
754         final KeplerianOrbit initOrbit = buildInitOrbit();
755         final double initMass = 20;
756         final SpacecraftState initialState = new SpacecraftState(initOrbit, initMass);
757         final AbsoluteDate initialDate = initOrbit.getDate();
758         final double simulationDuration = 2 * 86400;
759         final AbsoluteDate finalDate = initialDate.shiftedBy(simulationDuration);
760 
761         /////////////////// propagations /////////////////////////////////
762 
763         final NumericalPropagator numericalPropagator = buildNumericalPropagator(initOrbit);
764         numericalPropagator.addForceModel(buildPsoManeuver());
765         numericalPropagator.setInitialState(initialState);
766         final SpacecraftState finalStateNumerical = numericalPropagator.propagate(finalDate);
767 
768         /////////////////// results check /////////////////////////////////
769         final double expectedPropellantConsumption = -0.028;
770         final double expectedDeltaSemiMajorAxisRealized = 20920;
771         Assert.assertEquals(expectedPropellantConsumption, finalStateNumerical.getMass() - initialState.getMass(),
772                 0.005);
773         Assert.assertEquals(expectedDeltaSemiMajorAxisRealized, finalStateNumerical.getA() - initialState.getA(), 100);
774     }
775 
776     @Test(expected = OrekitException.class)
777     public void testStartDetectorNotSet() {
778         new ConfigurableLowThrustManeuver(buildVelocityThrustDirectionProvider(), null,
779                 new ApogeeCenteredIntervalDetector(halfThrustArc, PositionAngle.MEAN, new ContinueOnEvent<>()), thrust,
780                 isp);
781 
782     }
783 
784     @Test(expected = OrekitException.class)
785     public void testStopDetectorNotSet() {
786         new ConfigurableLowThrustManeuver(buildVelocityThrustDirectionProvider(),
787                 new ApogeeCenteredIntervalDetector(halfThrustArc, PositionAngle.MEAN, new ContinueOnEvent<>()), null,
788                 thrust, isp);
789 
790     }
791 
792 }