1   /* Copyright 2002-2025 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.propagation.events;
18  
19  import java.lang.reflect.Array;
20  import java.util.Locale;
21  import java.util.function.Function;
22  
23  import org.hamcrest.CoreMatchers;
24  import org.hamcrest.MatcherAssert;
25  import org.hipparchus.CalculusFieldElement;
26  import org.hipparchus.Field;
27  import org.hipparchus.exception.LocalizedCoreFormats;
28  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
29  import org.hipparchus.ode.FieldODEIntegrator;
30  import org.hipparchus.ode.events.Action;
31  import org.hipparchus.ode.nonstiff.ClassicalRungeKuttaFieldIntegrator;
32  import org.hipparchus.ode.nonstiff.DormandPrince853FieldIntegrator;
33  import org.hipparchus.util.Binary64;
34  import org.hipparchus.util.Binary64Field;
35  import org.hipparchus.util.FastMath;
36  import org.junit.jupiter.api.Assertions;
37  import org.junit.jupiter.api.BeforeEach;
38  import org.junit.jupiter.api.Test;
39  import org.mockito.Mockito;
40  import org.orekit.Utils;
41  import org.orekit.errors.OrekitException;
42  import org.orekit.frames.Frame;
43  import org.orekit.frames.FramesFactory;
44  import org.orekit.orbits.FieldCircularOrbit;
45  import org.orekit.orbits.FieldEquinoctialOrbit;
46  import org.orekit.orbits.FieldKeplerianOrbit;
47  import org.orekit.orbits.FieldOrbit;
48  import org.orekit.orbits.OrbitType;
49  import org.orekit.orbits.PositionAngleType;
50  import org.orekit.propagation.FieldPropagator;
51  import org.orekit.propagation.FieldSpacecraftState;
52  import org.orekit.propagation.ToleranceProvider;
53  import org.orekit.propagation.analytical.FieldKeplerianPropagator;
54  import org.orekit.propagation.events.handlers.FieldContinueOnEvent;
55  import org.orekit.propagation.events.handlers.FieldEventHandler;
56  import org.orekit.propagation.events.handlers.FieldStopOnEvent;
57  import org.orekit.propagation.events.intervals.FieldAdaptableInterval;
58  import org.orekit.propagation.numerical.FieldNumericalPropagator;
59  import org.orekit.propagation.sampling.FieldOrekitFixedStepHandler;
60  import org.orekit.time.AbsoluteDate;
61  import org.orekit.time.FieldAbsoluteDate;
62  import org.orekit.time.TimeScale;
63  import org.orekit.time.TimeScalesFactory;
64  import org.orekit.utils.Constants;
65  import org.orekit.utils.FieldPVCoordinates;
66  import org.orekit.utils.FieldPVCoordinatesProvider;
67  
68  class FieldEventDetectorTest {
69  
70      private double mu;
71  
72      @Test
73      @SuppressWarnings("unchecked")
74      void testFinish() {
75          // GIVEN
76          final FinishingHandler handler = new FinishingHandler();
77          final FieldEventDetector<?> detector = new DummyDetector(new FieldEventDetectionSettings<>(1.0, Binary64.ONE, 100), handler);
78          // WHEN
79          detector.finish(Mockito.mock(FieldSpacecraftState.class));
80          // THEN
81          Assertions.assertTrue(handler.isFinished);
82      }
83  
84      private static class FinishingHandler extends FieldContinueOnEvent<Binary64> {
85          boolean isFinished = false;
86  
87          @Override
88          public void finish(FieldSpacecraftState<Binary64> finalState, FieldEventDetector<Binary64> detector) {
89              isFinished = true;
90          }
91      }
92  
93      private static class DummyDetector implements FieldEventDetector<Binary64> {
94  
95          private final FieldEventDetectionSettings<Binary64> detectionSettings;
96          private final FieldEventHandler<Binary64> handler;
97  
98          public DummyDetector(final FieldEventDetectionSettings<Binary64> detectionSettings,
99                               final FieldEventHandler<Binary64> handler) {
100             this.detectionSettings = detectionSettings;
101             this.handler = handler;
102         }
103 
104         public Binary64 g(final FieldSpacecraftState<Binary64> s) {
105             return s.getDate().getField().getZero();
106         }
107 
108         @Override
109         public FieldEventHandler<Binary64> getHandler() {
110             return handler;
111         }
112 
113         @Override
114         public FieldEventDetectionSettings<Binary64> getDetectionSettings() {
115             return detectionSettings;
116         }
117 
118     }
119 
120     @Test
121     @SuppressWarnings("unchecked")
122     void testGetDetectionSettings() {
123         // GIVEN
124         final FieldAdaptableInterval<Binary64> mockedInterval = Mockito.mock(FieldAdaptableInterval.class);
125         final FieldEventDetectionSettings<Binary64> settings = new FieldEventDetectionSettings<>(mockedInterval, Binary64.ONE, 10);
126         final FieldEventDetector<Binary64> detector = new DummyDetector(settings, null);
127         // WHEN
128         final FieldEventDetectionSettings<Binary64> actualSettings = detector.getDetectionSettings();
129         // THEN
130         Assertions.assertEquals(mockedInterval, actualSettings.getMaxCheckInterval());
131         Assertions.assertEquals(settings.getMaxIterationCount(), actualSettings.getMaxIterationCount());
132         Assertions.assertEquals(settings.getThreshold(), actualSettings.getThreshold());
133     }
134 
135     @Test
136     void testEventHandlerInit() {
137         doTestEventHandlerInit(Binary64Field.getInstance());
138     }
139 
140     private <T extends CalculusFieldElement<T>> void doTestEventHandlerInit(Field<T> field) {
141 
142         final T zero = field.getZero();
143         final TimeScale utc = TimeScalesFactory.getUTC();
144         final FieldVector3D<T> position = new FieldVector3D<>(zero.add(-6142438.668),
145                                                               zero.add(3492467.56),
146                                                               zero.add(-25767.257));
147         final FieldVector3D<T> velocity = new FieldVector3D<>(zero.add(505.848),
148                                                               zero.add(942.781),
149                                                               zero.add(7435.922));
150         final FieldAbsoluteDate<T> date = new FieldAbsoluteDate<>(field, 2003, 9, 16, utc);
151         final FieldOrbit<T> orbit = new FieldCircularOrbit<>(new FieldPVCoordinates<>(position,  velocity),
152                                                              FramesFactory.getEME2000(), date, zero.add(mu));
153         // mutable boolean
154         final boolean[] eventOccurred = new boolean[1];
155         FieldEventHandler<T> handler = new FieldEventHandler<T>() {
156             private boolean initCalled;
157             @Override
158             public Action eventOccurred(FieldSpacecraftState<T> s,
159                                         FieldEventDetector<T> detector,
160                                         boolean increasing) {
161                 if (!initCalled) {
162                     throw new RuntimeException("init() not called before eventOccurred()");
163                 }
164                 eventOccurred[0] = true;
165                 return Action.STOP;
166             }
167 
168             @Override
169             public void init(final FieldSpacecraftState<T> initialState,
170                              final FieldAbsoluteDate<T> target,
171                              final FieldEventDetector<T> detector) {
172                 initCalled = true;
173             }
174         };
175 
176         FieldPropagator<T> propagator = new FieldKeplerianPropagator<>(orbit);
177         T stepSize = zero.add(60.0);
178         final FieldDateDetector<T> detector = new FieldDateDetector<>(field, date.shiftedBy(stepSize.multiply(5.25))).withHandler(handler);
179         propagator.addEventDetector(detector);
180         propagator.propagate(date.shiftedBy(stepSize.multiply(10)));
181         Assertions.assertTrue(eventOccurred[0]);
182 
183     }
184 
185     @Test
186     void testBasicScheduling() {
187         doTestBasicScheduling(Binary64Field.getInstance());
188     }
189 
190     private <T extends CalculusFieldElement<T>> void doTestBasicScheduling(Field<T> field) {
191 
192         final T zero = field.getZero();
193         final TimeScale utc = TimeScalesFactory.getUTC();
194         final FieldVector3D<T> position = new FieldVector3D<>(zero.add(-6142438.668),
195                                                               zero.add(3492467.56),
196                                                               zero.add(-25767.257));
197         final FieldVector3D<T> velocity = new FieldVector3D<>(zero.add(505.848),
198                                                               zero.add(942.781),
199                                                               zero.add(7435.922));
200         final FieldAbsoluteDate<T> date = new FieldAbsoluteDate<>(field, 2003, 9, 16, utc);
201         final FieldOrbit<T> orbit = new FieldCircularOrbit<>(new FieldPVCoordinates<>(position,  velocity),
202                                                              FramesFactory.getEME2000(), date, zero.add(mu));
203 
204         FieldPropagator<T> propagator = new FieldKeplerianPropagator<>(orbit);
205         T stepSize = zero.add(60.0);
206         OutOfOrderChecker<T> checker = new OutOfOrderChecker<>(stepSize);
207         FieldDateDetector<T> detector = new FieldDateDetector<>(field, date.shiftedBy(stepSize.multiply(5.25))).withHandler(checker);
208         propagator.addEventDetector(detector);
209         propagator.setStepHandler(stepSize, checker);
210         propagator.propagate(date.shiftedBy(stepSize.multiply(10)));
211         Assertions.assertTrue(checker.outOfOrderCallDetected());
212 
213     }
214 
215     private static class OutOfOrderChecker<T extends CalculusFieldElement<T>>
216         implements FieldEventHandler<T>, FieldOrekitFixedStepHandler<T> {
217 
218         private FieldAbsoluteDate<T> triggerDate;
219         private boolean outOfOrderCallDetected;
220         private final T stepSize;
221 
222         public OutOfOrderChecker(final T stepSize) {
223             triggerDate = null;
224             outOfOrderCallDetected = false;
225             this.stepSize = stepSize;
226         }
227 
228         public Action eventOccurred(FieldSpacecraftState<T> s, FieldEventDetector<T> detector, boolean increasing) {
229             triggerDate = s.getDate();
230             return Action.CONTINUE;
231         }
232 
233         public void handleStep(FieldSpacecraftState<T> currentState) {
234             // step handling and event occurrences may be out of order up to one step
235             // with variable steps, and two steps with fixed steps (due to the delay
236             // induced by StepNormalizer)
237             if (triggerDate != null) {
238                 double dt = currentState.getDate().durationFrom(triggerDate).getReal();
239                 if (dt < 0) {
240                     outOfOrderCallDetected = true;
241                     Assertions.assertTrue(FastMath.abs(dt) < (2 * stepSize.getReal()));
242                 }
243             }
244         }
245 
246         public boolean outOfOrderCallDetected() {
247             return outOfOrderCallDetected;
248         }
249 
250     }
251 
252     @Test
253     void testIssue108Numerical() {
254         doTestIssue108Numerical(Binary64Field.getInstance());
255     }
256 
257     private <T extends CalculusFieldElement<T>> void doTestIssue108Numerical(Field<T> field) {
258         final T zero = field.getZero();
259         final TimeScale utc = TimeScalesFactory.getUTC();
260         final FieldVector3D<T> position = new FieldVector3D<>(zero.add(-6142438.668),
261                                                               zero.add(3492467.56),
262                                                               zero.add(-25767.257));
263         final FieldVector3D<T> velocity = new FieldVector3D<>(zero.add(505.848),
264                                                               zero.add(942.781),
265                                                               zero.add(7435.922));
266         final FieldAbsoluteDate<T> date = new FieldAbsoluteDate<>(field, 2003, 9, 16, utc);
267         final FieldOrbit<T> orbit = new FieldCircularOrbit<>(new FieldPVCoordinates<>(position,  velocity),
268                                                              FramesFactory.getEME2000(), date, zero.add(mu));
269         final T step = zero.add(60.0);
270         final int    n    = 100;
271         FieldNumericalPropagator<T> propagator = new FieldNumericalPropagator<>(field, new ClassicalRungeKuttaFieldIntegrator<>(field, step));
272         propagator.setOrbitType(OrbitType.EQUINOCTIAL);
273         propagator.resetInitialState(new FieldSpacecraftState<>(orbit));
274         GCallsCounter<T> counter = new GCallsCounter<>(FieldAdaptableInterval.of(100000.0), zero.add(1.0e-6), 20,
275                                                        new FieldStopOnEvent<>());
276         propagator.addEventDetector(counter);
277         propagator.propagate(date.shiftedBy(step.multiply(n)));
278         Assertions.assertEquals(n + 1, counter.getCount());
279     }
280 
281     @Test
282     void testIssue108Analytical() {
283         doTestIssue108Analytical(Binary64Field.getInstance());
284     }
285 
286     private <T extends CalculusFieldElement<T>> void doTestIssue108Analytical(Field<T> field) {
287         final T zero = field.getZero();
288         final TimeScale utc = TimeScalesFactory.getUTC();
289         final FieldVector3D<T> position = new FieldVector3D<>(zero.add(-6142438.668),
290                                                               zero.add(3492467.56),
291                                                               zero.add(-25767.257));
292         final FieldVector3D<T> velocity = new FieldVector3D<>(zero.add(505.848),
293                                                               zero.add(942.781),
294                                                               zero.add(7435.922));
295         final FieldAbsoluteDate<T> date = new FieldAbsoluteDate<>(field, 2003, 9, 16, utc);
296         final FieldOrbit<T> orbit = new FieldCircularOrbit<>(new FieldPVCoordinates<>(position,  velocity),
297                         FramesFactory.getEME2000(), date, zero.add(mu));
298         final T step = zero.add(60.0);
299         final int    n    = 100;
300         FieldKeplerianPropagator<T> propagator = new FieldKeplerianPropagator<>(orbit);
301         GCallsCounter<T> counter = new GCallsCounter<>(FieldAdaptableInterval.of(100000.0), zero.add(1.0e-6), 20,
302                                                        new FieldStopOnEvent<>());
303         propagator.addEventDetector(counter);
304         propagator.setStepHandler(step, currentState -> {});
305         propagator.propagate(date.shiftedBy(step.multiply(n)));
306         // analytical propagator can take one big step, further reducing calls to g()
307         Assertions.assertEquals(2, counter.getCount());
308     }
309 
310     private static class GCallsCounter<T extends CalculusFieldElement<T>> extends FieldAbstractDetector<GCallsCounter<T>, T> {
311 
312         private int count;
313 
314         public GCallsCounter(final FieldAdaptableInterval<T> maxCheck, final T threshold,
315                              final int maxIter, final FieldEventHandler<T> handler) {
316             super(new FieldEventDetectionSettings<>(maxCheck, threshold, maxIter), handler);
317             count = 0;
318         }
319 
320         protected GCallsCounter<T> create(final FieldEventDetectionSettings<T> detectionSettings,
321                                           final FieldEventHandler<T> newHandler) {
322             return new GCallsCounter<>(detectionSettings.getMaxCheckInterval(), detectionSettings.getThreshold(),
323                     detectionSettings.getMaxIterationCount(), newHandler);
324         }
325 
326         public int getCount() {
327             return count;
328         }
329 
330         public T g(FieldSpacecraftState<T> s) {
331             count++;
332             return s.getMass().getField().getZero().add(1.0);
333         }
334 
335     }
336 
337     @Test
338     void testNoisyGFunction() {
339         doTestNoisyGFunction(Binary64Field.getInstance());
340     }
341 
342     private <T extends CalculusFieldElement<T>> void doTestNoisyGFunction(Field<T> field) {
343 
344         final T zero = field.getZero();
345 
346         // initial conditions
347         Frame eme2000 = FramesFactory.getEME2000();
348         TimeScale utc = TimeScalesFactory.getUTC();
349         FieldAbsoluteDate<T> initialDate   = new FieldAbsoluteDate<>(field, 2011, 5, 11, utc);
350         FieldAbsoluteDate<T> startDate     = new FieldAbsoluteDate<>(field, 2032, 10, 17, utc);
351         @SuppressWarnings("unchecked")
352         FieldAbsoluteDate<T>[] interruptDates =
353                         ( FieldAbsoluteDate<T>[]) Array.newInstance(FieldAbsoluteDate.class, 1);
354         interruptDates[0] = new FieldAbsoluteDate<>(field, 2032, 10, 18, utc);
355         FieldAbsoluteDate<T> targetDate    = new FieldAbsoluteDate<>(field, 2211, 5, 11, utc);
356         FieldKeplerianPropagator<T> k1 =
357                 new FieldKeplerianPropagator<>(new FieldEquinoctialOrbit<>(new FieldPVCoordinates<>(new FieldVector3D<>(zero.add(4008462.4706055815),
358                                                                                                                         zero.add(-3155502.5373837613),
359                                                                                                                         zero.add(-5044275.9880020910)),
360                                                                                                     new FieldVector3D<>(zero.add(-5012.9298276860990),
361                                                                                                                         zero.add(1920.3567095973078),
362                                                                                                                         zero.add(-5172.7403501801580))),
363                                                                            eme2000, initialDate, zero.add(Constants.WGS84_EARTH_MU)));
364         FieldKeplerianPropagator<T> k2 =
365                 new FieldKeplerianPropagator<>(new FieldEquinoctialOrbit<>(new FieldPVCoordinates<>(new FieldVector3D<>(zero.add(4008912.4039522274),
366                                                                                                                         zero.add(-3155453.3125615157),
367                                                                                                                         zero.add(-5044297.6484738905)),
368                                                                                                     new FieldVector3D<>(zero.add(-5012.5883854112530),
369                                                                                                                         zero.add(1920.6332221785074),
370                                                                                                                         zero.add(-5172.2177085540500))),
371                                                              eme2000, initialDate, zero.add(Constants.WGS84_EARTH_MU)));
372         k2.addEventDetector(new FieldCloseApproachDetector<>(FieldAdaptableInterval.of(2015.243454166727), zero.add(0.0001), 100,
373                                                              new FieldContinueOnEvent<>(),
374                                                              k1));
375         k2.addEventDetector(new FieldDateDetector<>(field, interruptDates).
376                             withMaxCheck(FieldAdaptableInterval.of(Constants.JULIAN_DAY)).
377                             withThreshold(field.getZero().newInstance(1.0e-6)));
378         FieldSpacecraftState<T> s = k2.propagate(startDate, targetDate);
379         Assertions.assertEquals(0.0, interruptDates[0].durationFrom(s.getDate()).getReal(), 1.1e-6);
380     }
381 
382     private static class FieldCloseApproachDetector<T extends CalculusFieldElement<T>>
383         extends FieldAbstractDetector<FieldCloseApproachDetector<T>, T> {
384 
385         private final FieldPVCoordinatesProvider<T> provider;
386 
387         public FieldCloseApproachDetector(FieldAdaptableInterval<T> maxCheck, T threshold,
388                                           final int maxIter, final FieldEventHandler<T> handler,
389                                           FieldPVCoordinatesProvider<T> provider) {
390             super(new FieldEventDetectionSettings<>(maxCheck, threshold, maxIter), handler);
391             this.provider = provider;
392         }
393 
394         public T g(final FieldSpacecraftState<T> s) {
395             FieldPVCoordinates<T> pv1     = provider.getPVCoordinates(s.getDate(), s.getFrame());
396             FieldPVCoordinates<T> pv2     = s.getPVCoordinates();
397             FieldVector3D<T> deltaP       = pv1.getPosition().subtract(pv2.getPosition());
398             FieldVector3D<T> deltaV       = pv1.getVelocity().subtract(pv2.getVelocity());
399             return FieldVector3D.dotProduct(deltaP.normalize(), deltaV);
400         }
401 
402         protected FieldCloseApproachDetector<T> create(final FieldEventDetectionSettings<T> detectionSettings,
403                                                        final FieldEventHandler<T> newHandler) {
404             return new FieldCloseApproachDetector<>(detectionSettings.getMaxCheckInterval(), detectionSettings.getThreshold(),
405                     detectionSettings.getMaxIterationCount(), newHandler, provider);
406         }
407 
408     }
409 
410     @Test
411     void testWrappedException() {
412         doTestWrappedException(Binary64Field.getInstance());
413     }
414 
415     private <T extends CalculusFieldElement<T>> void doTestWrappedException(Field<T> field) {
416         final T zero = field.getZero();
417         final Throwable dummyCause = new RuntimeException();
418         try {
419             // initial conditions
420             Frame eme2000 = FramesFactory.getEME2000();
421             TimeScale utc = TimeScalesFactory.getUTC();
422             final FieldAbsoluteDate<T> initialDate   = new FieldAbsoluteDate<>(field, 2011, 5, 11, utc);
423             final FieldAbsoluteDate<T> exceptionDate = initialDate.shiftedBy(3600.0);
424             FieldKeplerianPropagator<T> k =
425                             new FieldKeplerianPropagator<>(new FieldEquinoctialOrbit<>(new FieldPVCoordinates<>(new FieldVector3D<>(zero.add(4008462.4706055815),
426                                                                                                                                     zero.add(-3155502.5373837613),
427                                                                                                                                     zero.add(-5044275.9880020910)),
428                                                                                                                 new FieldVector3D<>(zero.add(-5012.9298276860990),
429                                                                                                                                     zero.add(1920.3567095973078),
430                                                                                                                                     zero.add(-5172.7403501801580))),
431                                             eme2000, initialDate, zero.add(Constants.WGS84_EARTH_MU)));
432             k.addEventDetector(new FieldDateDetector<T>(field, initialDate.shiftedBy(Constants.JULIAN_DAY)) {
433                 @Override
434                 public T g(final FieldSpacecraftState<T> s) {
435                     final T dt = s.getDate().durationFrom(exceptionDate);
436                     if (dt.abs().getReal() < 1.0) {
437                         throw new OrekitException(dummyCause, LocalizedCoreFormats.SIMPLE_MESSAGE, "dummy");
438                     }
439                     return dt;
440                 }
441             });
442             k.propagate(initialDate.shiftedBy(Constants.JULIAN_YEAR));
443             Assertions.fail("an exception should have been thrown");
444         } catch (OrekitException oe) {
445             Assertions.assertSame(OrekitException.class, oe.getClass());
446             Assertions.assertSame(dummyCause, oe.getCause().getCause());
447             String expected = "failed to find root between 2011-05-11T00:00:00.000Z " +
448                     "(g=-3.6E3) and 2012-05-10T06:00:00.000Z (g=3.1554E7)\n" +
449                     "Last iteration at 2011-05-11T01:00:00.000Z (g=-3.6E3)";
450             MatcherAssert.assertThat(oe.getMessage(Locale.US),
451                     CoreMatchers.containsString(expected));
452         }
453     }
454 
455     @Test
456     void testDefaultMethods() {
457         doTestDefaultMethods(Binary64Field.getInstance());
458     }
459 
460     private <T extends CalculusFieldElement<T>> void doTestDefaultMethods(final Field<T> field) {
461         FieldEventDetector<T> dummyDetector = new FieldEventDetector<T>() {
462 
463             @Override
464             public FieldEventDetectionSettings<T> getDetectionSettings() {
465                 return new FieldEventDetectionSettings<>(FieldAdaptableInterval.of(60), field.getZero().newInstance(1e-10), 100);
466             }
467 
468             @Override
469             public T g(FieldSpacecraftState<T> s) {
470                 return s.getDate().durationFrom(AbsoluteDate.J2000_EPOCH);
471             }
472 
473             @Override
474             public FieldEventHandler<T> getHandler() {
475                 return (state, detector, increasing) ->  Action.RESET_STATE;
476             }
477        };
478 
479        // by default, this method does nothing, so this should pass without exception
480        dummyDetector.init(null, null);
481 
482        // by default, this method returns its argument
483        FieldSpacecraftState<T> s = new FieldSpacecraftState<>(new FieldKeplerianOrbit<>(field.getZero().add(7e6),
484                                                                                         field.getZero().add(0.01),
485                                                                                         field.getZero().add(0.3),
486                                                                                         field.getZero().add(0),
487                                                                                         field.getZero().add(0),
488                                                                                         field.getZero().add(0), PositionAngleType.TRUE,
489                                                                                         FramesFactory.getEME2000(),
490                                                                                         FieldAbsoluteDate.getJ2000Epoch(field),
491                                                                                         field.getZero().add(Constants.EIGEN5C_EARTH_MU)));
492        Assertions.assertSame(s, dummyDetector.getHandler().resetState(dummyDetector, s));
493 
494     }
495 
496     @Test
497     void testForwardAnalytical() {
498         doTestScheduling(Binary64Field.getInstance(), 0.0, 1.0, 21, this::buildAnalytical);
499     }
500 
501     @Test
502     void testBackwardAnalytical() {
503         doTestScheduling(Binary64Field.getInstance(), 1.0, 0.0, 21, this::buildAnalytical);
504     }
505 
506     @Test
507     void testForwardNumerical() {
508         doTestScheduling(Binary64Field.getInstance(), 0.0, 1.0, 23, this::buildNumerical);
509     }
510 
511     @Test
512     void testBackwardNumerical() {
513         doTestScheduling(Binary64Field.getInstance(), 1.0, 0.0, 23, this::buildNumerical);
514     }
515 
516     private <T extends CalculusFieldElement<T>> FieldPropagator<T> buildAnalytical(final FieldOrbit<T> orbit) {
517         return  new FieldKeplerianPropagator<>(orbit);
518     }
519 
520     private <T extends CalculusFieldElement<T>> FieldPropagator<T> buildNumerical(final FieldOrbit<T> orbit) {
521         Field<T>            field      = orbit.getDate().getField();
522         OrbitType           type       = OrbitType.CARTESIAN;
523         double[][]          tol        = ToleranceProvider.getDefaultToleranceProvider(0.0001).getTolerances(orbit, type);
524         FieldODEIntegrator<T> integrator = new DormandPrince853FieldIntegrator<>(field, 0.0001, 10.0, tol[0], tol[1]);
525         FieldNumericalPropagator<T> propagator = new FieldNumericalPropagator<>(field, integrator);
526         propagator.setOrbitType(type);
527         propagator.setInitialState(new FieldSpacecraftState<>(orbit));
528         return propagator;
529     }
530 
531     private <T extends CalculusFieldElement<T>> void doTestScheduling(final Field<T> field,
532                                                                       final double start, final double stop, final int expectedCalls,
533                                                                       final Function<FieldOrbit<T>, FieldPropagator<T>> propagatorBuilder) {
534 
535         // initial conditions
536         Frame eme2000 = FramesFactory.getEME2000();
537         TimeScale utc = TimeScalesFactory.getUTC();
538         final FieldAbsoluteDate<T> initialDate   = new FieldAbsoluteDate<>(field, 2011, 5, 11, utc);
539         final FieldOrbit<T> orbit = new FieldEquinoctialOrbit<>(new FieldPVCoordinates<>(new FieldVector3D<>(field.getZero().newInstance(4008462.4706055815),
540                                                                                                              field.getZero().newInstance(-3155502.5373837613),
541                                                                                                              field.getZero().newInstance(-5044275.9880020910)),
542                                                                                          new FieldVector3D<>(field.getZero().newInstance(-5012.9298276860990),
543                                                                                                              field.getZero().newInstance(1920.3567095973078),
544                                                                                                              field.getZero().newInstance(-5172.7403501801580))),
545                                                  eme2000, initialDate, field.getZero().newInstance(Constants.WGS84_EARTH_MU));
546         FieldPropagator<T> propagator = propagatorBuilder.apply(orbit.shiftedBy(start));
547 
548         // checker that will be used in both step handler and events handlers
549         // to check they are called in consistent order
550         final ScheduleChecker<T> checker = new ScheduleChecker<>(initialDate.shiftedBy(start),
551                                                                  initialDate.shiftedBy(stop));
552         propagator.setStepHandler(interpolator -> checker.callDate(interpolator.getCurrentState().getDate()));
553 
554         for (int i = 0; i < 10; ++i) {
555             FieldDateDetector<T> detector = new FieldDateDetector<>(field, initialDate.shiftedBy(0.0625 * (i + 1))).
556                                             withHandler((state, d, increasing) -> {
557                                                 checker.callDate(state.getDate());
558                                                 return Action.CONTINUE;
559                                             });
560             propagator.addEventDetector(detector);
561         }
562 
563         propagator.propagate(initialDate.shiftedBy(start), initialDate.shiftedBy(stop));
564 
565         Assertions.assertEquals(expectedCalls, checker.calls);
566 
567     }
568 
569     /** Checker for method calls scheduling. */
570     private static class ScheduleChecker<T extends CalculusFieldElement<T>> {
571 
572         private final FieldAbsoluteDate<T> start;
573         private final FieldAbsoluteDate<T> stop;
574         private FieldAbsoluteDate<T>       last;
575         private int                        calls;
576 
577         ScheduleChecker(final FieldAbsoluteDate<T> start, final FieldAbsoluteDate<T> stop) {
578             this.start = start;
579             this.stop  = stop;
580             this.last  = null;
581             this.calls = 0;
582         }
583 
584         void callDate(final FieldAbsoluteDate<T> date) {
585             if (last != null) {
586                 // check scheduling is always consistent with integration direction
587                 if (start.isBefore(stop)) {
588                     // forward direction
589                     Assertions.assertTrue(date.isAfterOrEqualTo(start));
590                     Assertions.assertTrue(date.isBeforeOrEqualTo(stop));
591                     Assertions.assertTrue(date.isAfterOrEqualTo(last));
592                } else {
593                     // backward direction
594                    Assertions.assertTrue(date.isBeforeOrEqualTo(start));
595                    Assertions.assertTrue(date.isAfterOrEqualTo(stop));
596                    Assertions.assertTrue(date.isBeforeOrEqualTo(last));
597                 }
598             }
599             last = date;
600             ++calls;
601         }
602 
603     }
604 
605     @BeforeEach
606     public void setUp() {
607         Utils.setDataRoot("regular-data");
608         mu = Constants.EIGEN5C_EARTH_MU;
609     }
610 
611 }
612