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