EventState.java

  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. import java.util.function.DoubleFunction;

  19. import org.hipparchus.analysis.UnivariateFunction;
  20. import org.hipparchus.analysis.solvers.BracketedUnivariateSolver;
  21. import org.hipparchus.analysis.solvers.BracketedUnivariateSolver.Interval;
  22. import org.hipparchus.analysis.solvers.BracketingNthOrderBrentSolver;
  23. import org.hipparchus.exception.MathRuntimeException;
  24. import org.hipparchus.ode.events.Action;
  25. import org.hipparchus.util.FastMath;
  26. import org.hipparchus.util.Precision;
  27. import org.orekit.errors.OrekitException;
  28. import org.orekit.errors.OrekitInternalError;
  29. import org.orekit.errors.OrekitMessages;
  30. import org.orekit.propagation.SpacecraftState;
  31. import org.orekit.propagation.events.handlers.EventHandler;
  32. import org.orekit.propagation.sampling.OrekitStepInterpolator;
  33. import org.orekit.time.AbsoluteDate;

  34. /** This class handles the state for one {@link EventDetector
  35.  * event detector} during integration steps.
  36.  *
  37.  * <p>This class is heavily based on the class with the same name from the
  38.  * Hipparchus library. The changes performed consist in replacing
  39.  * raw types (double and double arrays) with space dynamics types
  40.  * ({@link AbsoluteDate}, {@link SpacecraftState}).</p>
  41.  * <p>Each time the propagator proposes a step, the event detector
  42.  * should be checked. This class handles the state of one detector
  43.  * during one propagation step, with references to the state at the
  44.  * end of the preceding step. This information is used to determine if
  45.  * the detector should trigger an event or not during the proposed
  46.  * step (and hence the step should be reduced to ensure the event
  47.  * occurs at a bound rather than inside the step).</p>
  48.  * @author Luc Maisonobe
  49.  * @param <T> class type for the generic version
  50.  */
  51. public class EventState<T extends EventDetector> {

  52.     /** Event detector. */
  53.     private T detector;

  54.     /** Event handler. */
  55.     private EventHandler handler;

  56.     /** Time of the previous call to g. */
  57.     private AbsoluteDate lastT;

  58.     /** Value from the previous call to g. */
  59.     private double lastG;

  60.     /** Time at the beginning of the step. */
  61.     private AbsoluteDate t0;

  62.     /** Value of the event detector at the beginning of the step. */
  63.     private double g0;

  64.     /** Simulated sign of g0 (we cheat when crossing events). */
  65.     private boolean g0Positive;

  66.     /** Indicator of event expected during the step. */
  67.     private boolean pendingEvent;

  68.     /** Occurrence time of the pending event. */
  69.     private AbsoluteDate pendingEventTime;

  70.     /**
  71.      * Time to stop propagation if the event is a stop event. Used to enable stopping at
  72.      * an event and then restarting after that event.
  73.      */
  74.     private AbsoluteDate stopTime;

  75.     /** Time after the current event. */
  76.     private AbsoluteDate afterEvent;

  77.     /** Value of the g function after the current event. */
  78.     private double afterG;

  79.     /** The earliest time considered for events. */
  80.     private AbsoluteDate earliestTimeConsidered;

  81.     /** Integration direction. */
  82.     private boolean forward;

  83.     /** Variation direction around pending event.
  84.      *  (this is considered with respect to the integration direction)
  85.      */
  86.     private boolean increasing;

  87.     /** Simple constructor.
  88.      * @param detector monitored event detector
  89.      */
  90.     public EventState(final T detector) {
  91.         this.detector     = detector;
  92.         this.handler      = detector.getHandler();

  93.         // some dummy values ...
  94.         lastT                  = AbsoluteDate.PAST_INFINITY;
  95.         lastG                  = Double.NaN;
  96.         t0                     = null;
  97.         g0                     = Double.NaN;
  98.         g0Positive             = true;
  99.         pendingEvent           = false;
  100.         pendingEventTime       = null;
  101.         stopTime               = null;
  102.         increasing             = true;
  103.         earliestTimeConsidered = null;
  104.         afterEvent             = null;
  105.         afterG                 = Double.NaN;

  106.     }

  107.     /** Get the underlying event detector.
  108.      * @return underlying event detector
  109.      */
  110.     public T getEventDetector() {
  111.         return detector;
  112.     }

  113.     /** Initialize event handler at the start of a propagation.
  114.      * <p>
  115.      * This method is called once at the start of the propagation. It
  116.      * may be used by the event handler to initialize some internal data
  117.      * if needed.
  118.      * </p>
  119.      * @param s0 initial state
  120.      * @param t target time for the integration
  121.      *
  122.      */
  123.     public void init(final SpacecraftState s0,
  124.                      final AbsoluteDate t) {
  125.         detector.init(s0, t);
  126.         lastT = AbsoluteDate.PAST_INFINITY;
  127.         lastG = Double.NaN;
  128.     }

  129.     /** Compute the value of the switching function.
  130.      * This function must be continuous (at least in its roots neighborhood),
  131.      * as the integrator will need to find its roots to locate the events.
  132.      * @param s the current state information: date, kinematics, attitude
  133.      * @return value of the switching function
  134.      */
  135.     private double g(final SpacecraftState s) {
  136.         if (!s.getDate().equals(lastT)) {
  137.             lastG = detector.g(s);
  138.             lastT = s.getDate();
  139.         }
  140.         return lastG;
  141.     }

  142.     /** Reinitialize the beginning of the step.
  143.      * @param interpolator interpolator valid for the current step
  144.      */
  145.     public void reinitializeBegin(final OrekitStepInterpolator interpolator) {
  146.         forward = interpolator.isForward();
  147.         final SpacecraftState s0 = interpolator.getPreviousState();
  148.         this.t0 = s0.getDate();
  149.         g0 = g(s0);
  150.         while (g0 == 0) {
  151.             // extremely rare case: there is a zero EXACTLY at interval start
  152.             // we will use the sign slightly after step beginning to force ignoring this zero
  153.             // try moving forward by half a convergence interval
  154.             final double dt = (forward ? 0.5 : -0.5) * detector.getThreshold();
  155.             AbsoluteDate startDate = t0.shiftedBy(dt);
  156.             // if convergence is too small move an ulp
  157.             if (t0.equals(startDate)) {
  158.                 startDate = nextAfter(startDate);
  159.             }
  160.             t0 = startDate;
  161.             g0 = g(interpolator.getInterpolatedState(t0));
  162.         }
  163.         g0Positive = g0 > 0;
  164.         // "last" event was increasing
  165.         increasing = g0Positive;
  166.     }

  167.     /** Evaluate the impact of the proposed step on the event detector.
  168.      * @param interpolator step interpolator for the proposed step
  169.      * @return true if the event detector triggers an event before
  170.      * the end of the proposed step (this implies the step should be
  171.      * rejected)
  172.      * @exception MathRuntimeException if an event cannot be located
  173.      */
  174.     public boolean evaluateStep(final OrekitStepInterpolator interpolator)
  175.         throws MathRuntimeException {

  176.         forward = interpolator.isForward();
  177.         final SpacecraftState s0 = interpolator.getPreviousState();
  178.         final SpacecraftState s1 = interpolator.getCurrentState();
  179.         final AbsoluteDate t1 = s1.getDate();
  180.         final double dt = t1.durationFrom(t0);
  181.         if (FastMath.abs(dt) < detector.getThreshold()) {
  182.             // we cannot do anything on such a small step, don't trigger any events
  183.             pendingEvent     = false;
  184.             pendingEventTime = null;
  185.             return false;
  186.         }


  187.         AbsoluteDate ta = t0;
  188.         double ga = g0;
  189.         for (SpacecraftState sb = nextCheck(s0, s1, interpolator);
  190.              sb != null;
  191.              sb = nextCheck(sb, s1, interpolator)) {

  192.             // evaluate handler value at the end of the substep
  193.             final AbsoluteDate tb = sb.getDate();
  194.             final double gb = g(sb);

  195.             // check events occurrence
  196.             if (gb == 0.0 || (g0Positive ^ gb > 0)) {
  197.                 // there is a sign change: an event is expected during this step
  198.                 if (findRoot(interpolator, ta, ga, tb, gb)) {
  199.                     return true;
  200.                 }
  201.             } else {
  202.                 // no sign change: there is no event for now
  203.                 ta = tb;
  204.                 ga = gb;
  205.             }

  206.         }

  207.         // no event during the whole step
  208.         pendingEvent     = false;
  209.         pendingEventTime = null;
  210.         return false;

  211.     }

  212.     /** Estimate next state to check.
  213.      * @param done state already checked
  214.      * @param target target state towards which we are checking
  215.      * @param interpolator step interpolator for the proposed step
  216.      * @return intermediate state to check, or exactly {@code null}
  217.      * if we already have {@code done == target}
  218.      * @since 12.0
  219.      */
  220.     private SpacecraftState nextCheck(final SpacecraftState done, final SpacecraftState target,
  221.                                       final OrekitStepInterpolator interpolator) {
  222.         if (done == target) {
  223.             // we have already reached target
  224.             return null;
  225.         } else {
  226.             // we have to select some intermediate state
  227.             // attempting to split the remaining time in an integer number of checks
  228.             final double dt       = target.getDate().durationFrom(done.getDate());
  229.             final double maxCheck = detector.getMaxCheckInterval().currentInterval(done, dt >= 0.);
  230.             final int    n        = FastMath.max(1, (int) FastMath.ceil(FastMath.abs(dt) / maxCheck));
  231.             return n == 1 ? target : interpolator.getInterpolatedState(done.getDate().shiftedBy(dt / n));
  232.         }
  233.     }

  234.     /**
  235.      * Find a root in a bracketing interval.
  236.      *
  237.      * <p> When calling this method one of the following must be true. Either ga == 0, gb
  238.      * == 0, (ga < 0  and gb > 0), or (ga > 0 and gb < 0).
  239.      *
  240.      * @param interpolator that covers the interval.
  241.      * @param ta           earliest possible time for root.
  242.      * @param ga           g(ta).
  243.      * @param tb           latest possible time for root.
  244.      * @param gb           g(tb).
  245.      * @return if a zero crossing was found.
  246.      */
  247.     private boolean findRoot(final OrekitStepInterpolator interpolator,
  248.                              final AbsoluteDate ta, final double ga,
  249.                              final AbsoluteDate tb, final double gb) {
  250.         // check there appears to be a root in [ta, tb]
  251.         check(ga == 0.0 || gb == 0.0 || ga > 0.0 && gb < 0.0 || ga < 0.0 && gb > 0.0);

  252.         final double convergence = detector.getThreshold();
  253.         final int maxIterationCount = detector.getMaxIterationCount();
  254.         final BracketedUnivariateSolver<UnivariateFunction> solver =
  255.                 new BracketingNthOrderBrentSolver(0, convergence, 0, 5);

  256.         // prepare loop below
  257.         AbsoluteDate loopT = ta;
  258.         double loopG = ga;

  259.         // event time, just at or before the actual root.
  260.         AbsoluteDate beforeRootT = null;
  261.         double beforeRootG = Double.NaN;
  262.         // time on the other side of the root.
  263.         // Initialized the the loop below executes once.
  264.         AbsoluteDate afterRootT = ta;
  265.         double afterRootG = 0.0;

  266.         // check for some conditions that the root finders don't like
  267.         // these conditions cannot not happen in the loop below
  268.         // the ga == 0.0 case is handled by the loop below
  269.         if (ta.equals(tb)) {
  270.             // both non-zero but times are the same. Probably due to reset state
  271.             beforeRootT = ta;
  272.             beforeRootG = ga;
  273.             afterRootT = shiftedBy(beforeRootT, convergence);
  274.             afterRootG = g(interpolator.getInterpolatedState(afterRootT));
  275.         } else if (ga != 0.0 && gb == 0.0) {
  276.             // hard: ga != 0.0 and gb == 0.0
  277.             // look past gb by up to convergence to find next sign
  278.             // throw an exception if g(t) = 0.0 in [tb, tb + convergence]
  279.             beforeRootT = tb;
  280.             beforeRootG = gb;
  281.             afterRootT = shiftedBy(beforeRootT, convergence);
  282.             afterRootG = g(interpolator.getInterpolatedState(afterRootT));
  283.         } else if (ga != 0.0) {
  284.             final double newGa = g(interpolator.getInterpolatedState(ta));
  285.             if (ga > 0 != newGa > 0) {
  286.                 // both non-zero, step sign change at ta, possibly due to reset state
  287.                 final AbsoluteDate nextT = minTime(shiftedBy(ta, convergence), tb);
  288.                 final double       nextG = g(interpolator.getInterpolatedState(nextT));
  289.                 if (nextG > 0.0 == g0Positive) {
  290.                     // the sign change between ga and newGa just moved the root less than one convergence
  291.                     // threshold later, we are still in a regular search for another root before tb,
  292.                     // we just need to fix the bracketing interval
  293.                     // (see issue https://github.com/Hipparchus-Math/hipparchus/issues/184)
  294.                     loopT = nextT;
  295.                     loopG = nextG;
  296.                 } else {
  297.                     beforeRootT = ta;
  298.                     beforeRootG = newGa;
  299.                     afterRootT  = nextT;
  300.                     afterRootG  = nextG;
  301.                 }
  302.             }
  303.         }

  304.         // loop to skip through "fake" roots, i.e. where g(t) = g'(t) = 0.0
  305.         // executed once if we didn't hit a special case above
  306.         while ((afterRootG == 0.0 || afterRootG > 0.0 == g0Positive) &&
  307.                 strictlyAfter(afterRootT, tb)) {
  308.             if (loopG == 0.0) {
  309.                 // ga == 0.0 and gb may or may not be 0.0
  310.                 // handle the root at ta first
  311.                 beforeRootT = loopT;
  312.                 beforeRootG = loopG;
  313.                 afterRootT = minTime(shiftedBy(beforeRootT, convergence), tb);
  314.                 afterRootG = g(interpolator.getInterpolatedState(afterRootT));
  315.             } else {
  316.                 // both non-zero, the usual case, use a root finder.
  317.                 // time zero for evaluating the function f. Needs to be final
  318.                 final AbsoluteDate fT0 = loopT;
  319.                 final double tbDouble = tb.durationFrom(fT0);
  320.                 final double middle = 0.5 * tbDouble;
  321.                 final DoubleFunction<AbsoluteDate> date = dt -> {
  322.                     // use either fT0 or tb as the base time for shifts
  323.                     // in order to ensure we reproduce exactly those times
  324.                     // using only one reference time like fT0 would imply
  325.                     // to use ft0.shiftedBy(tbDouble), which may be different
  326.                     // from tb due to numerical noise (see issue 921)
  327.                     if (forward == dt <= middle) {
  328.                         // use start of interval as reference
  329.                         return fT0.shiftedBy(dt);
  330.                     } else {
  331.                         // use end of interval as reference
  332.                         return tb.shiftedBy(dt - tbDouble);
  333.                     }
  334.                 };
  335.                 final UnivariateFunction f = dt -> g(interpolator.getInterpolatedState(date.apply(dt)));
  336.                 if (forward) {
  337.                     try {
  338.                         final Interval interval =
  339.                                 solver.solveInterval(maxIterationCount, f, 0, tbDouble);
  340.                         beforeRootT = date.apply(interval.getLeftAbscissa());
  341.                         beforeRootG = interval.getLeftValue();
  342.                         afterRootT  = date.apply(interval.getRightAbscissa());
  343.                         afterRootG  = interval.getRightValue();
  344.                         // CHECKSTYLE: stop IllegalCatch check
  345.                     } catch (RuntimeException e) {
  346.                         // CHECKSTYLE: resume IllegalCatch check
  347.                         throw new OrekitException(e, OrekitMessages.FIND_ROOT,
  348.                                 detector, loopT, loopG, tb, gb, lastT, lastG);
  349.                     }
  350.                 } else {
  351.                     try {
  352.                         final Interval interval =
  353.                                 solver.solveInterval(maxIterationCount, f, tbDouble, 0);
  354.                         beforeRootT = date.apply(interval.getRightAbscissa());
  355.                         beforeRootG = interval.getRightValue();
  356.                         afterRootT  = date.apply(interval.getLeftAbscissa());
  357.                         afterRootG  = interval.getLeftValue();
  358.                         // CHECKSTYLE: stop IllegalCatch check
  359.                     } catch (RuntimeException e) {
  360.                         // CHECKSTYLE: resume IllegalCatch check
  361.                         throw new OrekitException(e, OrekitMessages.FIND_ROOT,
  362.                                 detector, tb, gb, loopT, loopG, lastT, lastG);
  363.                     }
  364.                 }
  365.             }
  366.             // tolerance is set to less than 1 ulp
  367.             // assume tolerance is 1 ulp
  368.             if (beforeRootT.equals(afterRootT)) {
  369.                 afterRootT = nextAfter(afterRootT);
  370.                 afterRootG = g(interpolator.getInterpolatedState(afterRootT));
  371.             }
  372.             // check loop is making some progress
  373.             check(forward && afterRootT.compareTo(beforeRootT) > 0 ||
  374.                   !forward && afterRootT.compareTo(beforeRootT) < 0);
  375.             // setup next iteration
  376.             loopT = afterRootT;
  377.             loopG = afterRootG;
  378.         }

  379.         // figure out the result of root finding, and return accordingly
  380.         if (afterRootG == 0.0 || afterRootG > 0.0 == g0Positive) {
  381.             // loop gave up and didn't find any crossing within this step
  382.             return false;
  383.         } else {
  384.             // real crossing
  385.             check(beforeRootT != null && !Double.isNaN(beforeRootG));
  386.             // variation direction, with respect to the integration direction
  387.             increasing = !g0Positive;
  388.             pendingEventTime = beforeRootT;
  389.             stopTime = beforeRootG == 0.0 ? beforeRootT : afterRootT;
  390.             pendingEvent = true;
  391.             afterEvent = afterRootT;
  392.             afterG = afterRootG;

  393.             // check increasing set correctly
  394.             check(afterG > 0 == increasing);
  395.             check(increasing == gb >= ga);

  396.             return true;
  397.         }

  398.     }

  399.     /**
  400.      * Get the next number after the given number in the current propagation direction.
  401.      *
  402.      * @param t input time
  403.      * @return t +/- 1 ulp depending on the direction.
  404.      */
  405.     private AbsoluteDate nextAfter(final AbsoluteDate t) {
  406.         return t.shiftedBy(forward ? +Precision.EPSILON : -Precision.EPSILON);
  407.     }

  408.     /** Get the occurrence time of the event triggered in the current
  409.      * step.
  410.      * @return occurrence time of the event triggered in the current
  411.      * step.
  412.      */
  413.     public AbsoluteDate getEventDate() {
  414.         return pendingEventTime;
  415.     }

  416.     /**
  417.      * Try to accept the current history up to the given time.
  418.      *
  419.      * <p> It is not necessary to call this method before calling {@link
  420.      * #doEvent(SpacecraftState)} with the same state. It is necessary to call this
  421.      * method before you call {@link #doEvent(SpacecraftState)} on some other event
  422.      * detector.
  423.      *
  424.      * @param state        to try to accept.
  425.      * @param interpolator to use to find the new root, if any.
  426.      * @return if the event detector has an event it has not detected before that is on or
  427.      * before the same time as {@code state}. In other words {@code false} means continue
  428.      * on while {@code true} means stop and handle my event first.
  429.      */
  430.     public boolean tryAdvance(final SpacecraftState state,
  431.                               final OrekitStepInterpolator interpolator) {
  432.         final AbsoluteDate t = state.getDate();
  433.         // check this is only called before a pending event.
  434.         check(!pendingEvent || !strictlyAfter(pendingEventTime, t));

  435.         final boolean meFirst;

  436.         if (strictlyAfter(t, earliestTimeConsidered)) {
  437.             // just found an event and we know the next time we want to search again
  438.             meFirst = false;
  439.         } else {
  440.             // check g function to see if there is a new event
  441.             final double g = g(state);
  442.             final boolean positive = g > 0;

  443.             if (positive == g0Positive) {
  444.                 // g function has expected sign
  445.                 g0 = g; // g0Positive is the same
  446.                 meFirst = false;
  447.             } else {
  448.                 // found a root we didn't expect -> find precise location
  449.                 final AbsoluteDate oldPendingEventTime = pendingEventTime;
  450.                 final boolean foundRoot = findRoot(interpolator, t0, g0, t, g);
  451.                 // make sure the new root is not the same as the old root, if one exists
  452.                 meFirst = foundRoot && !pendingEventTime.equals(oldPendingEventTime);
  453.             }
  454.         }

  455.         if (!meFirst) {
  456.             // advance t0 to the current time so we can't find events that occur before t
  457.             t0 = t;
  458.         }

  459.         return meFirst;
  460.     }

  461.     /**
  462.      * Notify the user's listener of the event. The event occurs wholly within this method
  463.      * call including a call to {@link EventHandler#resetState(EventDetector, SpacecraftState)}
  464.      * if necessary.
  465.      *
  466.      * @param state the state at the time of the event. This must be at the same time as
  467.      *              the current value of {@link #getEventDate()}.
  468.      * @return the user's requested action and the new state if the action is {@link
  469.      * Action#RESET_STATE Action.RESET_STATE}.
  470.      * Otherwise the new state is {@code state}. The stop time indicates what time propagation
  471.      * should stop if the action is {@link Action#STOP Action.STOP}.
  472.      * This guarantees the integration will stop on or after the root, so that integration
  473.      * may be restarted safely.
  474.      */
  475.     public EventOccurrence doEvent(final SpacecraftState state) {
  476.         // check event is pending and is at the same time
  477.         check(pendingEvent);
  478.         check(state.getDate().equals(this.pendingEventTime));

  479.         final Action action = handler.eventOccurred(state, detector, increasing == forward);
  480.         final SpacecraftState newState;
  481.         if (action == Action.RESET_STATE) {
  482.             newState = handler.resetState(detector, state);
  483.         } else {
  484.             newState = state;
  485.         }
  486.         // clear pending event
  487.         pendingEvent     = false;
  488.         pendingEventTime = null;
  489.         // setup for next search
  490.         earliestTimeConsidered = afterEvent;
  491.         t0 = afterEvent;
  492.         g0 = afterG;
  493.         g0Positive = increasing;
  494.         // check g0Positive set correctly
  495.         check(g0 == 0.0 || g0Positive == g0 > 0);
  496.         return new EventOccurrence(action, newState, stopTime);
  497.     }

  498.     /**
  499.      * Shift a time value along the current integration direction: {@link #forward}.
  500.      *
  501.      * @param t     the time to shift.
  502.      * @param delta the amount to shift.
  503.      * @return t + delta if forward, else t - delta. If the result has to be rounded it
  504.      * will be rounded to be before the true value of t + delta.
  505.      */
  506.     private AbsoluteDate shiftedBy(final AbsoluteDate t, final double delta) {
  507.         if (forward) {
  508.             final AbsoluteDate ret = t.shiftedBy(delta);
  509.             if (ret.durationFrom(t) > delta) {
  510.                 return ret.shiftedBy(-Precision.EPSILON);
  511.             } else {
  512.                 return ret;
  513.             }
  514.         } else {
  515.             final AbsoluteDate ret = t.shiftedBy(-delta);
  516.             if (t.durationFrom(ret) > delta) {
  517.                 return ret.shiftedBy(+Precision.EPSILON);
  518.             } else {
  519.                 return ret;
  520.             }
  521.         }
  522.     }

  523.     /**
  524.      * Get the time that happens first along the current propagation direction: {@link
  525.      * #forward}.
  526.      *
  527.      * @param a first time
  528.      * @param b second time
  529.      * @return min(a, b) if forward, else max (a, b)
  530.      */
  531.     private AbsoluteDate minTime(final AbsoluteDate a, final AbsoluteDate b) {
  532.         return (forward ^ a.compareTo(b) > 0) ? a : b;
  533.     }

  534.     /**
  535.      * Check the ordering of two times.
  536.      *
  537.      * @param t1 the first time.
  538.      * @param t2 the second time.
  539.      * @return true if {@code t2} is strictly after {@code t1} in the propagation
  540.      * direction.
  541.      */
  542.     private boolean strictlyAfter(final AbsoluteDate t1, final AbsoluteDate t2) {
  543.         if (t1 == null || t2 == null) {
  544.             return false;
  545.         } else {
  546.             return forward ? t1.compareTo(t2) < 0 : t2.compareTo(t1) < 0;
  547.         }
  548.     }

  549.     /**
  550.      * Same as keyword assert, but throw a {@link MathRuntimeException}.
  551.      *
  552.      * @param condition to check
  553.      * @throws MathRuntimeException if {@code condition} is false.
  554.      */
  555.     private void check(final boolean condition) throws MathRuntimeException {
  556.         if (!condition) {
  557.             throw new OrekitInternalError(null);
  558.         }
  559.     }

  560.     /**
  561.      * This method finalizes the event detector's job.
  562.      * @param state state at propagation end
  563.      * @since 12.2
  564.      */
  565.     public void finish(final SpacecraftState state) {
  566.         detector.finish(state);
  567.     }

  568.     /**
  569.      * Class to hold the data related to an event occurrence that is needed to decide how
  570.      * to modify integration.
  571.      */
  572.     public static class EventOccurrence {

  573.         /** User requested action. */
  574.         private final Action action;
  575.         /** New state for a reset action. */
  576.         private final SpacecraftState newState;
  577.         /** The time to stop propagation if the action is a stop event. */
  578.         private final AbsoluteDate stopDate;

  579.         /**
  580.          * Create a new occurrence of an event.
  581.          *
  582.          * @param action   the user requested action.
  583.          * @param newState for a reset event. Should be the current state unless the
  584.          *                 action is {@link Action#RESET_STATE}.
  585.          * @param stopDate to stop propagation if the action is {@link Action#STOP}. Used
  586.          *                 to move the stop time to just after the root.
  587.          */
  588.         EventOccurrence(final Action action,
  589.                         final SpacecraftState newState,
  590.                         final AbsoluteDate stopDate) {
  591.             this.action = action;
  592.             this.newState = newState;
  593.             this.stopDate = stopDate;
  594.         }

  595.         /**
  596.          * Get the user requested action.
  597.          *
  598.          * @return the action.
  599.          */
  600.         public Action getAction() {
  601.             return action;
  602.         }

  603.         /**
  604.          * Get the new state for a reset action.
  605.          *
  606.          * @return the new state.
  607.          */
  608.         public SpacecraftState getNewState() {
  609.             return newState;
  610.         }

  611.         /**
  612.          * Get the new time for a stop action.
  613.          *
  614.          * @return when to stop propagation.
  615.          */
  616.         public AbsoluteDate getStopDate() {
  617.             return stopDate;
  618.         }

  619.     }

  620. }