1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF 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.function.DoubleFunction;
20  
21  import org.hipparchus.CalculusFieldElement;
22  import org.hipparchus.Field;
23  import org.hipparchus.analysis.UnivariateFunction;
24  import org.hipparchus.analysis.solvers.BracketedUnivariateSolver;
25  import org.hipparchus.analysis.solvers.BracketedUnivariateSolver.Interval;
26  import org.hipparchus.analysis.solvers.BracketingNthOrderBrentSolver;
27  import org.hipparchus.exception.MathRuntimeException;
28  import org.hipparchus.ode.events.Action;
29  import org.hipparchus.util.FastMath;
30  import org.hipparchus.util.Precision;
31  import org.orekit.errors.OrekitException;
32  import org.orekit.errors.OrekitInternalError;
33  import org.orekit.errors.OrekitMessages;
34  import org.orekit.propagation.FieldSpacecraftState;
35  import org.orekit.propagation.events.handlers.FieldEventHandler;
36  import org.orekit.propagation.sampling.FieldOrekitStepInterpolator;
37  import org.orekit.time.FieldAbsoluteDate;
38  
39  /** This class handles the state for one {@link FieldEventDetector
40   * event detector} during integration steps.
41   *
42   * <p>This class is heavily based on the class with the same name from the
43   * Hipparchus library. The changes performed consist in replacing
44   * raw types (double and double arrays) with space dynamics types
45   * ({@link FieldAbsoluteDate}, {@link FieldSpacecraftState}).</p>
46   * <p>Each time the propagator proposes a step, the event detector
47   * should be checked. This class handles the state of one detector
48   * during one propagation step, with references to the state at the
49   * end of the preceding step. This information is used to determine if
50   * the detector should trigger an event or not during the proposed
51   * step (and hence the step should be reduced to ensure the event
52   * occurs at a bound rather than inside the step).</p>
53   * @author Luc Maisonobe
54   * @param <D> class type for the generic version
55   * @param <T> type of the field elements
56   */
57  public class FieldEventState<D extends FieldEventDetector<T>, T extends CalculusFieldElement<T>> {
58  
59      /** Event detector. */
60      private D detector;
61  
62      /** Event handler. */
63      private FieldEventHandler<T> handler;
64  
65      /** Time of the previous call to g. */
66      private FieldAbsoluteDate<T> lastT;
67  
68      /** Value from the previous call to g. */
69      private T lastG;
70  
71      /** Time at the beginning of the step. */
72      private FieldAbsoluteDate<T> t0;
73  
74      /** Value of the event detector at the beginning of the step. */
75      private T g0;
76  
77      /** Simulated sign of g0 (we cheat when crossing events). */
78      private boolean g0Positive;
79  
80      /** Indicator of event expected during the step. */
81      private boolean pendingEvent;
82  
83      /** Occurrence time of the pending event. */
84      private FieldAbsoluteDate<T> pendingEventTime;
85  
86      /**
87       * Time to stop propagation if the event is a stop event. Used to enable stopping at
88       * an event and then restarting after that event.
89       */
90      private FieldAbsoluteDate<T> stopTime;
91  
92      /** Time after the current event. */
93      private FieldAbsoluteDate<T> afterEvent;
94  
95      /** Value of the g function after the current event. */
96      private T afterG;
97  
98      /** The earliest time considered for events. */
99      private FieldAbsoluteDate<T> earliestTimeConsidered;
100 
101     /** Integration direction. */
102     private boolean forward;
103 
104     /** Variation direction around pending event.
105      *  (this is considered with respect to the integration direction)
106      */
107     private boolean increasing;
108 
109     /** Simple constructor.
110      * @param detector monitored event detector
111      */
112     public FieldEventState(final D detector) {
113 
114         this.detector = detector;
115         this.handler  = detector.getHandler();
116 
117         // some dummy values ...
118         final Field<T> field   = detector.getThreshold().getField();
119         final T nan            = field.getZero().add(Double.NaN);
120         lastT                  = FieldAbsoluteDate.getPastInfinity(field);
121         lastG                  = nan;
122         t0                     = null;
123         g0                     = nan;
124         g0Positive             = true;
125         pendingEvent           = false;
126         pendingEventTime       = null;
127         stopTime               = null;
128         increasing             = true;
129         earliestTimeConsidered = null;
130         afterEvent             = null;
131         afterG                 = nan;
132 
133     }
134 
135     /** Get the underlying event detector.
136      * @return underlying event detector
137      */
138     public D getEventDetector() {
139         return detector;
140     }
141 
142     /** Initialize event handler at the start of a propagation.
143      * <p>
144      * This method is called once at the start of the propagation. It
145      * may be used by the event handler to initialize some internal data
146      * if needed.
147      * </p>
148      * @param s0 initial state
149      * @param t target time for the integration
150      *
151      */
152     public void init(final FieldSpacecraftState<T> s0,
153                      final FieldAbsoluteDate<T> t) {
154         detector.init(s0, t);
155         final Field<T> field = detector.getThreshold().getField();
156         lastT = FieldAbsoluteDate.getPastInfinity(field);
157         lastG = field.getZero().add(Double.NaN);
158     }
159 
160     /** Compute the value of the switching function.
161      * This function must be continuous (at least in its roots neighborhood),
162      * as the integrator will need to find its roots to locate the events.
163      * @param s the current state information: date, kinematics, attitude
164      * @return value of the switching function
165      */
166     private T g(final FieldSpacecraftState<T> s) {
167         if (!s.getDate().equals(lastT)) {
168             lastT = s.getDate();
169             lastG = detector.g(s);
170         }
171         return lastG;
172     }
173 
174     /** Reinitialize the beginning of the step.
175      * @param interpolator interpolator valid for the current step
176      */
177     public void reinitializeBegin(final FieldOrekitStepInterpolator<T> interpolator) {
178         forward = interpolator.isForward();
179         final FieldSpacecraftState<T> s0 = interpolator.getPreviousState();
180         this.t0 = s0.getDate();
181         g0 = g(s0);
182         while (g0.getReal() == 0) {
183             // extremely rare case: there is a zero EXACTLY at interval start
184             // we will use the sign slightly after step beginning to force ignoring this zero
185             // try moving forward by half a convergence interval
186             final T dt = detector.getThreshold().multiply(forward ? 0.5 : -0.5);
187             FieldAbsoluteDate<T> startDate = t0.shiftedBy(dt);
188             // if convergence is too small move an ulp
189             if (t0.equals(startDate)) {
190                 startDate = nextAfter(startDate);
191             }
192             t0 = startDate;
193             g0 = g(interpolator.getInterpolatedState(t0));
194         }
195         g0Positive = g0.getReal() > 0;
196         // "last" event was increasing
197         increasing = g0Positive;
198     }
199 
200     /** Evaluate the impact of the proposed step on the event detector.
201      * @param interpolator step interpolator for the proposed step
202      * @return true if the event detector triggers an event before
203      * the end of the proposed step (this implies the step should be
204      * rejected)
205      * @exception MathRuntimeException if an event cannot be located
206      */
207     public boolean evaluateStep(final FieldOrekitStepInterpolator<T> interpolator)
208         throws MathRuntimeException {
209         forward = interpolator.isForward();
210         final FieldSpacecraftState<T> s0 = interpolator.getPreviousState();
211         final FieldSpacecraftState<T> s1 = interpolator.getCurrentState();
212         final FieldAbsoluteDate<T> t1 = s1.getDate();
213         final T dt = t1.durationFrom(t0);
214         if (FastMath.abs(dt.getReal()) < detector.getThreshold().getReal()) {
215             // we cannot do anything on such a small step, don't trigger any events
216             pendingEvent     = false;
217             pendingEventTime = null;
218             return false;
219         }
220 
221         FieldAbsoluteDate<T> ta = t0;
222         T ga = g0;
223         for (FieldSpacecraftState<T> sb = nextCheck(s0, s1, interpolator);
224              sb != null;
225              sb = nextCheck(sb, s1, interpolator)) {
226 
227             // evaluate handler value at the end of the substep
228             final FieldAbsoluteDate<T> tb = sb.getDate();
229             final T gb = g(sb);
230 
231             // check events occurrence
232             if (gb.getReal() == 0.0 || (g0Positive ^ gb.getReal() > 0)) {
233                 // there is a sign change: an event is expected during this step
234                 if (findRoot(interpolator, ta, ga, tb, gb)) {
235                     return true;
236                 }
237             } else {
238                 // no sign change: there is no event for now
239                 ta = tb;
240                 ga = gb;
241             }
242         }
243 
244         // no event during the whole step
245         pendingEvent     = false;
246         pendingEventTime = null;
247         return false;
248 
249     }
250 
251     /** Estimate next state to check.
252      * @param done state already checked
253      * @param target target state towards which we are checking
254      * @param interpolator step interpolator for the proposed step
255      * @return intermediate state to check, or exactly {@code null}
256      * if we already have {@code done == target}
257      * @since 12.0
258      */
259     private FieldSpacecraftState<T> nextCheck(final FieldSpacecraftState<T> done, final FieldSpacecraftState<T> target,
260                                               final FieldOrekitStepInterpolator<T> interpolator) {
261         if (done == target) {
262             // we have already reached target
263             return null;
264         } else {
265             // we have to select some intermediate state
266             // attempting to split the remaining time in an integer number of checks
267             final T dt            = target.getDate().durationFrom(done.getDate());
268             final double maxCheck = detector.getMaxCheckInterval().currentInterval(done, dt.getReal() >= 0.);
269             final int    n        = FastMath.max(1, (int) FastMath.ceil(FastMath.abs(dt).divide(maxCheck).getReal()));
270             return n == 1 ? target : interpolator.getInterpolatedState(done.getDate().shiftedBy(dt.divide(n)));
271         }
272     }
273 
274     /**
275      * Find a root in a bracketing interval.
276      *
277      * <p> When calling this method one of the following must be true. Either ga == 0, gb
278      * == 0, (ga < 0  and gb > 0), or (ga > 0 and gb < 0).
279      *
280      * @param interpolator that covers the interval.
281      * @param ta           earliest possible time for root.
282      * @param ga           g(ta).
283      * @param tb           latest possible time for root.
284      * @param gb           g(tb).
285      * @return if a zero crossing was found.
286      */
287     private boolean findRoot(final FieldOrekitStepInterpolator<T> interpolator,
288                              final FieldAbsoluteDate<T> ta, final T ga,
289                              final FieldAbsoluteDate<T> tb, final T gb) {
290 
291         final T zero = ga.getField().getZero();
292 
293         // check there appears to be a root in [ta, tb]
294         check(ga.getReal() == 0.0 || gb.getReal() == 0.0 || ga.getReal() > 0.0 && gb.getReal() < 0.0 || ga.getReal() < 0.0 && gb.getReal() > 0.0);
295         final T convergence = detector.getThreshold();
296         final int maxIterationCount = detector.getMaxIterationCount();
297         final BracketedUnivariateSolver<UnivariateFunction> solver =
298                 new BracketingNthOrderBrentSolver(0, convergence.getReal(), 0, 5);
299 
300         // prepare loop below
301         FieldAbsoluteDate<T> loopT = ta;
302         T loopG = ga;
303 
304         // event time, just at or before the actual root.
305         FieldAbsoluteDate<T> beforeRootT = null;
306         T beforeRootG = zero.add(Double.NaN);
307         // time on the other side of the root.
308         // Initialized the the loop below executes once.
309         FieldAbsoluteDate<T> afterRootT = ta;
310         T afterRootG = zero;
311 
312         // check for some conditions that the root finders don't like
313         // these conditions cannot not happen in the loop below
314         // the ga == 0.0 case is handled by the loop below
315         if (ta.equals(tb)) {
316             // both non-zero but times are the same. Probably due to reset state
317             beforeRootT = ta;
318             beforeRootG = ga;
319             afterRootT = shiftedBy(beforeRootT, convergence);
320             afterRootG = g(interpolator.getInterpolatedState(afterRootT));
321         } else if (ga.getReal() != 0.0 && gb.getReal() == 0.0) {
322             // hard: ga != 0.0 and gb == 0.0
323             // look past gb by up to convergence to find next sign
324             // throw an exception if g(t) = 0.0 in [tb, tb + convergence]
325             beforeRootT = tb;
326             beforeRootG = gb;
327             afterRootT = shiftedBy(beforeRootT, convergence);
328             afterRootG = g(interpolator.getInterpolatedState(afterRootT));
329         } else if (ga.getReal() != 0.0) {
330             final T newGa = g(interpolator.getInterpolatedState(ta));
331             if (ga.getReal() > 0 != newGa.getReal() > 0) {
332                 // both non-zero, step sign change at ta, possibly due to reset state
333                 final FieldAbsoluteDate<T> nextT = minTime(shiftedBy(ta, convergence), tb);
334                 final T                    nextG = g(interpolator.getInterpolatedState(nextT));
335                 if (nextG.getReal() > 0.0 == g0Positive) {
336                     // the sign change between ga and newGa just moved the root less than one convergence
337                     // threshold later, we are still in a regular search for another root before tb,
338                     // we just need to fix the bracketing interval
339                     // (see issue https://github.com/Hipparchus-Math/hipparchus/issues/184)
340                     loopT = nextT;
341                     loopG = nextG;
342                 } else {
343                     beforeRootT = ta;
344                     beforeRootG = newGa;
345                     afterRootT  = nextT;
346                     afterRootG  = nextG;
347                 }
348             }
349         }
350 
351         // loop to skip through "fake" roots, i.e. where g(t) = g'(t) = 0.0
352         // executed once if we didn't hit a special case above
353         while ((afterRootG.getReal() == 0.0 || afterRootG.getReal() > 0.0 == g0Positive) &&
354                 strictlyAfter(afterRootT, tb)) {
355             if (loopG.getReal() == 0.0) {
356                 // ga == 0.0 and gb may or may not be 0.0
357                 // handle the root at ta first
358                 beforeRootT = loopT;
359                 beforeRootG = loopG;
360                 afterRootT = minTime(shiftedBy(beforeRootT, convergence), tb);
361                 afterRootG = g(interpolator.getInterpolatedState(afterRootT));
362             } else {
363                 // both non-zero, the usual case, use a root finder.
364                 // time zero for evaluating the function f. Needs to be final
365                 final FieldAbsoluteDate<T> fT0 = loopT;
366                 final double tbDouble = tb.durationFrom(fT0).getReal();
367                 final double middle   = 0.5 * tbDouble;
368                 final DoubleFunction<FieldAbsoluteDate<T>> date = dt -> {
369                     // use either fT0 or tb as the base time for shifts
370                     // in order to ensure we reproduce exactly those times
371                     // using only one reference time like fT0 would imply
372                     // to use ft0.shiftedBy(tbDouble), which may be different
373                     // from tb due to numerical noise (see issue 921)
374                     if (forward == dt <= middle) {
375                         // use start of interval as reference
376                         return fT0.shiftedBy(dt);
377                     } else {
378                         // use end of interval as reference
379                         return tb.shiftedBy(dt - tbDouble);
380                     }
381                 };
382                 final UnivariateFunction f = dt -> g(interpolator.getInterpolatedState(date.apply(dt))).getReal();
383                 if (forward) {
384                     try {
385                         final Interval interval =
386                                 solver.solveInterval(maxIterationCount, f, 0, tbDouble);
387                         beforeRootT = date.apply(interval.getLeftAbscissa());
388                         beforeRootG = zero.add(interval.getLeftValue());
389                         afterRootT  = date.apply(interval.getRightAbscissa());
390                         afterRootG  = zero.add(interval.getRightValue());
391                         // CHECKSTYLE: stop IllegalCatch check
392                     } catch (RuntimeException e) {
393                         // CHECKSTYLE: resume IllegalCatch check
394                         throw new OrekitException(e, OrekitMessages.FIND_ROOT,
395                                 detector, loopT, loopG, tb, gb, lastT, lastG);
396                     }
397                 } else {
398                     try {
399                         final Interval interval =
400                                 solver.solveInterval(maxIterationCount, f, tbDouble, 0);
401                         beforeRootT = date.apply(interval.getRightAbscissa());
402                         beforeRootG = zero.newInstance(interval.getRightValue());
403                         afterRootT  = date.apply(interval.getLeftAbscissa());
404                         afterRootG  = zero.newInstance(interval.getLeftValue());
405                         // CHECKSTYLE: stop IllegalCatch check
406                     } catch (RuntimeException e) {
407                         // CHECKSTYLE: resume IllegalCatch check
408                         throw new OrekitException(e, OrekitMessages.FIND_ROOT,
409                                 detector, tb, gb, loopT, loopG, lastT, lastG);
410                     }
411                 }
412             }
413             // tolerance is set to less than 1 ulp
414             // assume tolerance is 1 ulp
415             if (beforeRootT.equals(afterRootT)) {
416                 afterRootT = nextAfter(afterRootT);
417                 afterRootG = g(interpolator.getInterpolatedState(afterRootT));
418             }
419             // check loop is making some progress
420             check(forward && afterRootT.compareTo(beforeRootT) > 0 ||
421                   !forward && afterRootT.compareTo(beforeRootT) < 0);
422             // setup next iteration
423             loopT = afterRootT;
424             loopG = afterRootG;
425         }
426 
427         // figure out the result of root finding, and return accordingly
428         if (afterRootG.getReal() == 0.0 || afterRootG.getReal() > 0.0 == g0Positive) {
429             // loop gave up and didn't find any crossing within this step
430             return false;
431         } else {
432             // real crossing
433             check(beforeRootT != null && !Double.isNaN(beforeRootG.getReal()));
434             // variation direction, with respect to the integration direction
435             increasing = !g0Positive;
436             pendingEventTime = beforeRootT;
437             stopTime = beforeRootG.getReal() == 0.0 ? beforeRootT : afterRootT;
438             pendingEvent = true;
439             afterEvent = afterRootT;
440             afterG = afterRootG;
441 
442             // check increasing set correctly
443             check(afterG.getReal() > 0 == increasing);
444             check(increasing == gb.getReal() >= ga.getReal());
445 
446             return true;
447         }
448 
449     }
450 
451     /**
452      * Get the next number after the given number in the current propagation direction.
453      *
454      * @param t input time
455      * @return t +/- 1 ulp depending on the direction.
456      */
457     private FieldAbsoluteDate<T> nextAfter(final FieldAbsoluteDate<T> t) {
458         return t.shiftedBy(forward ? +Precision.EPSILON : -Precision.EPSILON);
459     }
460 
461 
462     /** Get the occurrence time of the event triggered in the current
463      * step.
464      * @return occurrence time of the event triggered in the current
465      * step.
466      */
467     public FieldAbsoluteDate<T> getEventDate() {
468         return pendingEventTime;
469     }
470 
471     /**
472      * Try to accept the current history up to the given time.
473      *
474      * <p> It is not necessary to call this method before calling {@link
475      * #doEvent(FieldSpacecraftState)} with the same state. It is necessary to call this
476      * method before you call {@link #doEvent(FieldSpacecraftState)} on some other event
477      * detector.
478      *
479      * @param state        to try to accept.
480      * @param interpolator to use to find the new root, if any.
481      * @return if the event detector has an event it has not detected before that is on or
482      * before the same time as {@code state}. In other words {@code false} means continue
483      * on while {@code true} means stop and handle my event first.
484      */
485     public boolean tryAdvance(final FieldSpacecraftState<T> state,
486                               final FieldOrekitStepInterpolator<T> interpolator) {
487         final FieldAbsoluteDate<T> t = state.getDate();
488         // check this is only called before a pending event.
489         check(!pendingEvent || !strictlyAfter(pendingEventTime, t));
490 
491         final boolean meFirst;
492 
493         if (strictlyAfter(t, earliestTimeConsidered)) {
494             // just found an event and we know the next time we want to search again
495             meFirst = false;
496         } else {
497             // check g function to see if there is a new event
498             final T g = g(state);
499             final boolean positive = g.getReal() > 0;
500 
501             if (positive == g0Positive) {
502                 // g function has expected sign
503                 g0 = g; // g0Positive is the same
504                 meFirst = false;
505             } else {
506                 // found a root we didn't expect -> find precise location
507                 final FieldAbsoluteDate<T> oldPendingEventTime = pendingEventTime;
508                 final boolean foundRoot = findRoot(interpolator, t0, g0, t, g);
509                 // make sure the new root is not the same as the old root, if one exists
510                 meFirst = foundRoot && !pendingEventTime.equals(oldPendingEventTime);
511             }
512         }
513 
514         if (!meFirst) {
515             // advance t0 to the current time so we can't find events that occur before t
516             t0 = t;
517         }
518 
519         return meFirst;
520     }
521 
522     /**
523      * Notify the user's listener of the event. The event occurs wholly within this method
524      * call including a call to {@link FieldEventHandler#resetState(FieldEventDetector,
525      * FieldSpacecraftState)} if necessary.
526      *
527      * @param state the state at the time of the event. This must be at the same time as
528      *              the current value of {@link #getEventDate()}.
529      * @return the user's requested action and the new state if the action is {@link
530      * Action#RESET_STATE}. Otherwise
531      * the new state is {@code state}. The stop time indicates what time propagation should
532      * stop if the action is {@link Action#STOP}.
533      * This guarantees the integration will stop on or after the root, so that integration
534      * may be restarted safely.
535      */
536     public EventOccurrence<T> doEvent(final FieldSpacecraftState<T> state) {
537         // check event is pending and is at the same time
538         check(pendingEvent);
539         check(state.getDate().equals(this.pendingEventTime));
540 
541         final Action action = handler.eventOccurred(state, detector, increasing == forward);
542         final FieldSpacecraftState<T> newState;
543         if (action == Action.RESET_STATE) {
544             newState = handler.resetState(detector, state);
545         } else {
546             newState = state;
547         }
548         // clear pending event
549         pendingEvent     = false;
550         pendingEventTime = null;
551         // setup for next search
552         earliestTimeConsidered = afterEvent;
553         t0 = afterEvent;
554         g0 = afterG;
555         g0Positive = increasing;
556         // check g0Positive set correctly
557         check(g0.getReal() == 0.0 || g0Positive == g0.getReal() > 0);
558         return new EventOccurrence<>(action, newState, stopTime);
559     }
560 
561     /**
562      * Shift a time value along the current integration direction: {@link #forward}.
563      *
564      * @param t     the time to shift.
565      * @param delta the amount to shift.
566      * @return t + delta if forward, else t - delta. If the result has to be rounded it
567      * will be rounded to be before the true value of t + delta.
568      */
569     private FieldAbsoluteDate<T> shiftedBy(final FieldAbsoluteDate<T> t, final T delta) {
570         if (forward) {
571             final FieldAbsoluteDate<T> ret = t.shiftedBy(delta);
572             if (ret.durationFrom(t).getReal() > delta.getReal()) {
573                 return ret.shiftedBy(-Precision.EPSILON);
574             } else {
575                 return ret;
576             }
577         } else {
578             final FieldAbsoluteDate<T> ret = t.shiftedBy(delta.negate());
579             if (t.durationFrom(ret).getReal() > delta.getReal()) {
580                 return ret.shiftedBy(+Precision.EPSILON);
581             } else {
582                 return ret;
583             }
584         }
585     }
586 
587     /**
588      * Get the time that happens first along the current propagation direction: {@link
589      * #forward}.
590      *
591      * @param a first time
592      * @param b second time
593      * @return min(a, b) if forward, else max (a, b)
594      */
595     private FieldAbsoluteDate<T> minTime(final FieldAbsoluteDate<T> a, final FieldAbsoluteDate<T> b) {
596         return (forward ^ a.compareTo(b) > 0) ? a : b;
597     }
598 
599     /**
600      * Check the ordering of two times.
601      *
602      * @param t1 the first time.
603      * @param t2 the second time.
604      * @return true if {@code t2} is strictly after {@code t1} in the propagation
605      * direction.
606      */
607     private boolean strictlyAfter(final FieldAbsoluteDate<T> t1, final FieldAbsoluteDate<T> t2) {
608         if (t1 == null || t2 == null) {
609             return false;
610         } else {
611             return forward ? t1.compareTo(t2) < 0 : t2.compareTo(t1) < 0;
612         }
613     }
614 
615     /**
616      * Same as keyword assert, but throw a {@link MathRuntimeException}.
617      *
618      * @param condition to check
619      * @throws MathRuntimeException if {@code condition} is false.
620      */
621     private void check(final boolean condition) throws MathRuntimeException {
622         if (!condition) {
623             throw new OrekitInternalError(null);
624         }
625     }
626 
627     /**
628      * This method finalizes the event detector's job.
629      * @param state state at propagation end
630      * @since 12.2
631      */
632     public void finish(final FieldSpacecraftState<T> state) {
633         detector.finish(state);
634     }
635 
636     /**
637      * Class to hold the data related to an event occurrence that is needed to decide how
638      * to modify integration.
639      * @param <T> type of the field elements
640      */
641     public static class EventOccurrence<T extends CalculusFieldElement<T>> {
642 
643         /** User requested action. */
644         private final Action action;
645         /** New state for a reset action. */
646         private final FieldSpacecraftState<T> newState;
647         /** The time to stop propagation if the action is a stop event. */
648         private final FieldAbsoluteDate<T> stopDate;
649 
650         /**
651          * Create a new occurrence of an event.
652          *
653          * @param action   the user requested action.
654          * @param newState for a reset event. Should be the current state unless the
655          *                 action is {@link Action#RESET_STATE}.
656          * @param stopDate to stop propagation if the action is {@link Action#STOP}. Used
657          *                 to move the stop time to just after the root.
658          */
659         EventOccurrence(final Action action,
660                         final FieldSpacecraftState<T> newState,
661                         final FieldAbsoluteDate<T> stopDate) {
662             this.action = action;
663             this.newState = newState;
664             this.stopDate = stopDate;
665         }
666 
667         /**
668          * Get the user requested action.
669          *
670          * @return the action.
671          */
672         public Action getAction() {
673             return action;
674         }
675 
676         /**
677          * Get the new state for a reset action.
678          *
679          * @return the new state.
680          */
681         public FieldSpacecraftState<T> getNewState() {
682             return newState;
683         }
684 
685         /**
686          * Get the new time for a stop action.
687          *
688          * @return when to stop propagation.
689          */
690         public FieldAbsoluteDate<T> getStopDate() {
691             return stopDate;
692         }
693 
694     }
695 
696     /**Get PendingEvent.
697      * @return if there is a pending event or not
698      * */
699 
700     public boolean getPendingEvent() {
701         return pendingEvent;
702     }
703 
704 }