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