FieldDetectorBasedEventState.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.  *      https://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. /*
  18.  * This is not the original file distributed by the Apache Software Foundation
  19.  * It has been modified by the Hipparchus project
  20.  */

  21. package org.hipparchus.ode.events;

  22. import org.hipparchus.CalculusFieldElement;
  23. import org.hipparchus.analysis.CalculusFieldUnivariateFunction;
  24. import org.hipparchus.analysis.solvers.BracketedRealFieldUnivariateSolver;
  25. import org.hipparchus.analysis.solvers.BracketedRealFieldUnivariateSolver.Interval;
  26. import org.hipparchus.exception.MathIllegalArgumentException;
  27. import org.hipparchus.exception.MathIllegalStateException;
  28. import org.hipparchus.exception.MathRuntimeException;
  29. import org.hipparchus.ode.FieldODEState;
  30. import org.hipparchus.ode.FieldODEStateAndDerivative;
  31. import org.hipparchus.ode.LocalizedODEFormats;
  32. import org.hipparchus.ode.sampling.FieldODEStateInterpolator;
  33. import org.hipparchus.util.FastMath;

  34. /** This class handles the state for one {@link FieldODEEventHandler
  35.  * event handler} during integration steps.
  36.  *
  37.  * <p>Each time the integrator proposes a step, the event handler
  38.  * switching function should be checked. This class handles the state
  39.  * of one handler during one integration step, with references to the
  40.  * state at the end of the preceding step. This information is used to
  41.  * decide if the handler should trigger an event or not during the
  42.  * proposed step.</p>
  43.  *
  44.  * @param <T> the type of the field elements
  45.  */
  46. public class FieldDetectorBasedEventState<T extends CalculusFieldElement<T>> implements FieldEventState<T> {

  47.     /** Event detector.
  48.      * @since 3.0
  49.      */
  50.     private final FieldODEEventDetector<T> detector;

  51.     /** Event solver.
  52.      * @since 3.0
  53.      */
  54.     private final BracketedRealFieldUnivariateSolver<T> solver;

  55.     /** Event handler. */
  56.     private final FieldODEEventHandler<T> handler;

  57.     /** Time of the previous call to g. */
  58.     private T lastT;

  59.     /** Value from the previous call to g. */
  60.     private T lastG;

  61.     /** Time at the beginning of the step. */
  62.     private T t0;

  63.     /** Value of the events handler at the beginning of the step. */
  64.     private T g0;

  65.     /** Sign of g0. */
  66.     private boolean g0Positive;

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

  69.     /** Occurrence time of the pending event. */
  70.     private T pendingEventTime;

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

  76.     /** Time after the current event. */
  77.     private T afterEvent;

  78.     /** Value of the g function after the current event. */
  79.     private T afterG;

  80.     /** The earliest time considered for events. */
  81.     private T earliestTimeConsidered;

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

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

  88.     /** Simple constructor.
  89.      * @param detector event detector
  90.      * @since 3.0
  91.      */
  92.     public FieldDetectorBasedEventState(final FieldODEEventDetector<T> detector) {

  93.         this.detector     = detector;
  94.         this.solver       = detector.getSolver();
  95.         this.handler      = detector.getHandler();

  96.         // some dummy values ...
  97.         t0                = null;
  98.         g0                = null;
  99.         g0Positive        = true;
  100.         pendingEvent      = false;
  101.         pendingEventTime  = null;
  102.         increasing        = true;
  103.         earliestTimeConsidered = null;
  104.         afterEvent = null;
  105.         afterG = null;

  106.     }

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

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

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

  143.     /** Reinitialize the beginning of the step.
  144.      * @param interpolator valid for the current step
  145.      * @exception MathIllegalStateException if the interpolator throws one because
  146.      * the number of functions evaluations is exceeded
  147.      */
  148.     public void reinitializeBegin(final FieldODEStateInterpolator<T> interpolator)
  149.         throws MathIllegalStateException {

  150.         forward = interpolator.isForward();
  151.         final FieldODEStateAndDerivative<T> s0 = interpolator.getPreviousState();
  152.         t0 = s0.getTime();
  153.         g0 = g(s0);
  154.         while (g0.isZero()) {
  155.             // excerpt from MATH-421 issue:
  156.             // If an ODE solver is setup with a FieldODEEventHandler that return STOP
  157.             // when the even is triggered, the integrator stops (which is exactly
  158.             // the expected behavior). If however the user wants to restart the
  159.             // solver from the final state reached at the event with the same
  160.             // configuration (expecting the event to be triggered again at a
  161.             // later time), then the integrator may fail to start. It can get stuck
  162.             // at the previous event. The use case for the bug MATH-421 is fairly
  163.             // general, so events occurring exactly at start in the first step should
  164.             // be ignored.

  165.             // extremely rare case: there is a zero EXACTLY at interval start
  166.             // we will use the sign slightly after step beginning to force ignoring this zero
  167.             T tStart = t0.add(solver.getAbsoluteAccuracy().multiply(forward ? 0.5 : -0.5));
  168.             if (tStart.equals(t0)) {
  169.                 tStart = nextAfter(t0);
  170.             }
  171.             t0 = tStart;
  172.             g0 = g(interpolator.getInterpolatedState(tStart));
  173.         }
  174.         g0Positive = g0.getReal() > 0;
  175.         // "last" event was increasing
  176.         increasing = g0Positive;

  177.     }

  178.     /** Evaluate the impact of the proposed step on the event handler.
  179.      * @param interpolator step interpolator for the proposed step
  180.      * @return true if the event handler triggers an event before
  181.      * the end of the proposed step
  182.      * @exception MathIllegalStateException if the interpolator throws one because
  183.      * the number of functions evaluations is exceeded
  184.      * @exception MathIllegalArgumentException if the event cannot be bracketed
  185.      */
  186.     @Override
  187.     public boolean evaluateStep(final FieldODEStateInterpolator<T> interpolator)
  188.         throws MathIllegalArgumentException, MathIllegalStateException {

  189.         forward = interpolator.isForward();
  190.         final FieldODEStateAndDerivative<T> s0 = interpolator.getPreviousState();
  191.         final FieldODEStateAndDerivative<T> s1 = interpolator.getCurrentState();
  192.         final T t1 = s1.getTime();
  193.         final T dt = t1.subtract(t0);
  194.         if (dt.abs().subtract(solver.getAbsoluteAccuracy()).getReal() < 0) {
  195.             // we cannot do anything on such a small step, don't trigger any events
  196.             pendingEvent     = false;
  197.             pendingEventTime = null;
  198.             return false;
  199.         }

  200.         T ta = t0;
  201.         T ga = g0;
  202.         for (FieldODEStateAndDerivative<T>sb = nextCheck(s0, s1, interpolator);
  203.              sb != null;
  204.              sb = nextCheck(sb, s1, interpolator)) {

  205.             // evaluate handler value at the end of the substep
  206.             final T tb = sb.getTime();
  207.             final T gb = g(sb);

  208.             // check events occurrence
  209.             if (gb.getReal() == 0.0 || (g0Positive ^ (gb.getReal() > 0))) {
  210.                 // there is a sign change: an event is expected during this step
  211.                 if (findRoot(interpolator, ta, ga, tb, gb)) {
  212.                     return true;
  213.                 }
  214.             } else {
  215.                 // no sign change: there is no event for now
  216.                 ta = tb;
  217.                 ga = gb;
  218.             }

  219.         }

  220.         // no event during the whole step
  221.         pendingEvent     = false;
  222.         pendingEventTime = null;
  223.         return false;

  224.     }

  225.     /** Estimate next state to check.
  226.      * @param done state already checked
  227.      * @param target target state towards which we are checking
  228.      * @param interpolator step interpolator for the proposed step
  229.      * @return intermediate state to check, or exactly {@code null}
  230.      * if we already have {@code done == target}
  231.      * @since 3.0
  232.      */
  233.     private FieldODEStateAndDerivative<T> nextCheck(final FieldODEStateAndDerivative<T> done, final FieldODEStateAndDerivative<T> target,
  234.                                                     final FieldODEStateInterpolator<T> interpolator) {
  235.         if (done == target) {
  236.             // we have already reached target
  237.             return null;
  238.         } else {
  239.             // we have to select some intermediate state
  240.             // attempting to split the remaining time in an integer number of checks
  241.             final T dt       = target.getTime().subtract(done.getTime());
  242.             final double maxCheck = detector.getMaxCheckInterval().currentInterval(done, dt.getReal() >= 0.);
  243.             final int    n        = FastMath.max(1, (int) FastMath.ceil(dt.abs().divide(maxCheck).getReal()));
  244.             return n == 1 ? target : interpolator.getInterpolatedState(done.getTime().add(dt.divide(n)));
  245.         }
  246.     }

  247.     /**
  248.      * Find a root in a bracketing interval.
  249.      *
  250.      * <p> When calling this method one of the following must be true. Either ga == 0, gb
  251.      * == 0, (ga < 0  and gb > 0), or (ga > 0 and gb < 0).
  252.      *
  253.      * @param interpolator that covers the interval.
  254.      * @param ta           earliest possible time for root.
  255.      * @param ga           g(ta).
  256.      * @param tb           latest possible time for root.
  257.      * @param gb           g(tb).
  258.      * @return if a zero crossing was found.
  259.      */
  260.     private boolean findRoot(final FieldODEStateInterpolator<T> interpolator,
  261.                              final T ta,
  262.                              final T ga,
  263.                              final T tb,
  264.                              final T gb) {
  265.         // check there appears to be a root in [ta, tb]
  266.         check(ga.getReal() == 0.0 || gb.getReal() == 0.0 ||
  267.                 (ga.getReal() > 0.0 && gb.getReal() < 0.0) ||
  268.                 (ga.getReal() < 0.0 && gb.getReal() > 0.0));

  269.         final int maxIterationCount = detector.getMaxIterationCount();
  270.         final CalculusFieldUnivariateFunction<T> f = t -> g(interpolator.getInterpolatedState(t));

  271.         // prepare loop below
  272.         T loopT = ta;
  273.         T loopG = ga;

  274.         // event time, just at or before the actual root.
  275.         T beforeRootT = null;
  276.         T beforeRootG = null;
  277.         // time on the other side of the root.
  278.         // Initialized the the loop below executes once.
  279.         T afterRootT = ta;
  280.         T afterRootG = ta.getField().getZero();

  281.         // check for some conditions that the root finders don't like
  282.         // these conditions cannot not happen in the loop below
  283.         // the ga == 0.0 case is handled by the loop below
  284.         if (ta.getReal() == tb.getReal()) {
  285.             // both non-zero but times are the same. Probably due to reset state
  286.             beforeRootT = ta;
  287.             beforeRootG = ga;
  288.             afterRootT = shiftedBy(beforeRootT, solver.getAbsoluteAccuracy());
  289.             afterRootG = f.value(afterRootT);
  290.         } else if (!ga.isZero() && gb.isZero()) {
  291.             // hard: ga != 0.0 and gb == 0.0
  292.             // look past gb by up to convergence to find next sign
  293.             // throw an exception if g(t) = 0.0 in [tb, tb + convergence]
  294.             beforeRootT = tb;
  295.             beforeRootG = gb;
  296.             afterRootT = shiftedBy(beforeRootT, solver.getAbsoluteAccuracy());
  297.             afterRootG = f.value(afterRootT);
  298.         } else if (!ga.isZero()) {
  299.             final T newGa = f.value(ta);
  300.             if (ga.getReal() > 0 != newGa.getReal() > 0) {
  301.                 // both non-zero, step sign change at ta, possibly due to reset state
  302.                 final T nextT = minTime(shiftedBy(ta, solver.getAbsoluteAccuracy()), tb);
  303.                 final T nextG = f.value(nextT);
  304.                 if (nextG.getReal() > 0.0 == g0Positive) {
  305.                     // the sign change between ga and new Ga just moved the root less than one convergence
  306.                     // threshold later, we are still in a regular search for another root before tb,
  307.                     // we just need to fix the bracketing interval
  308.                     // (see issue https://github.com/Hipparchus-Math/hipparchus/issues/184)
  309.                     loopT = nextT;
  310.                     loopG = nextG;
  311.                 } else {
  312.                     beforeRootT = ta;
  313.                     beforeRootG = newGa;
  314.                     afterRootT  = nextT;
  315.                     afterRootG  = nextG;
  316.                 }
  317.             }
  318.         }

  319.         // loop to skip through "fake" roots, i.e. where g(t) = g'(t) = 0.0
  320.         // executed once if we didn't hit a special case above
  321.         while ((afterRootG.isZero() || afterRootG.getReal() > 0.0 == g0Positive) &&
  322.                strictlyAfter(afterRootT, tb)) {
  323.             if (loopG.getReal() == 0.0) {
  324.                 // ga == 0.0 and gb may or may not be 0.0
  325.                 // handle the root at ta first
  326.                 beforeRootT = loopT;
  327.                 beforeRootG = loopG;
  328.                 afterRootT = minTime(shiftedBy(beforeRootT, solver.getAbsoluteAccuracy()), tb);
  329.                 afterRootG = f.value(afterRootT);
  330.             } else {
  331.                 // both non-zero, the usual case, use a root finder.
  332.                 if (forward) {
  333.                     try {
  334.                         final Interval<T> interval =
  335.                                         solver.solveInterval(maxIterationCount, f, loopT, tb);
  336.                         beforeRootT = interval.getLeftAbscissa();
  337.                         beforeRootG = interval.getLeftValue();
  338.                         afterRootT = interval.getRightAbscissa();
  339.                         afterRootG = interval.getRightValue();
  340.                         // CHECKSTYLE: stop IllegalCatch check
  341.                     } catch (RuntimeException e) { // NOPMD
  342.                         // CHECKSTYLE: resume IllegalCatch check
  343.                         throw new MathIllegalStateException(e, LocalizedODEFormats.FIND_ROOT,
  344.                                                             detector, loopT.getReal(), loopG.getReal(),
  345.                                                             tb.getReal(), gb.getReal(),
  346.                                                             lastT.getReal(), lastG.getReal());
  347.                     }
  348.                 } else {
  349.                     try {
  350.                         final Interval<T> interval =
  351.                                         solver.solveInterval(maxIterationCount, f, tb, loopT);
  352.                         beforeRootT = interval.getRightAbscissa();
  353.                         beforeRootG = interval.getRightValue();
  354.                         afterRootT = interval.getLeftAbscissa();
  355.                         afterRootG = interval.getLeftValue();
  356.                         // CHECKSTYLE: stop IllegalCatch check
  357.                     } catch (RuntimeException e) { // NOPMD
  358.                         // CHECKSTYLE: resume IllegalCatch check
  359.                         throw new MathIllegalStateException(e, LocalizedODEFormats.FIND_ROOT,
  360.                                                             detector, tb.getReal(), gb.getReal(),
  361.                                                             loopT.getReal(), loopG.getReal(),
  362.                                                             lastT.getReal(), lastG.getReal());
  363.                     }
  364.                 }
  365.             }
  366.             // tolerance is set to less than 1 ulp
  367.             // assume tolerance is 1 ulp
  368.             if (beforeRootT == afterRootT) {
  369.                 afterRootT = nextAfter(afterRootT);
  370.                 afterRootG = f.value(afterRootT);
  371.             }
  372.             // check loop is making some progress
  373.             check((forward && afterRootT.getReal() > beforeRootT.getReal()) ||
  374.                   (!forward && afterRootT.getReal() < beforeRootT.getReal()));
  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.getReal() == 0.0 || afterRootG.getReal() > 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 && beforeRootG != null);
  386.             // variation direction, with respect to the integration direction
  387.             increasing = !g0Positive;
  388.             pendingEventTime = beforeRootT;
  389.             stopTime = beforeRootG.isZero() ? beforeRootT : afterRootT;
  390.             pendingEvent = true;
  391.             afterEvent = afterRootT;
  392.             afterG = afterRootG;

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

  396.             return true;
  397.         }
  398.     }

  399.     /**
  400.      * Try to accept the current history up to the given time.
  401.      *
  402.      * <p> It is not necessary to call this method before calling {@link
  403.      * #doEvent(FieldODEStateAndDerivative)} with the same state. It is necessary to call this
  404.      * method before you call {@link #doEvent(FieldODEStateAndDerivative)} on some other event
  405.      * detector.
  406.      *
  407.      * @param state        to try to accept.
  408.      * @param interpolator to use to find the new root, if any.
  409.      * @return if the event detector has an event it has not detected before that is on or
  410.      * before the same time as {@code state}. In other words {@code false} means continue
  411.      * on while {@code true} means stop and handle my event first.
  412.      */
  413.     public boolean tryAdvance(final FieldODEStateAndDerivative<T> state,
  414.                               final FieldODEStateInterpolator<T> interpolator) {
  415.         final T t = state.getTime();
  416.         // check this is only called before a pending event.
  417.         check(!pendingEvent || !strictlyAfter(pendingEventTime, t));

  418.         final boolean meFirst;

  419.         // just found an event and we know the next time we want to search again
  420.         if (earliestTimeConsidered != null && strictlyAfter(t, earliestTimeConsidered)) {
  421.             meFirst = false;
  422.         } else {
  423.             // check g function to see if there is a new event
  424.             final T g = g(state);
  425.             final boolean positive = g.getReal() > 0;

  426.             if (positive == g0Positive) {
  427.                 // g function has expected sign
  428.                 g0 = g; // g0Positive is the same
  429.                 meFirst = false;
  430.             } else {
  431.                 // found a root we didn't expect -> find precise location
  432.                 final T oldPendingEventTime = pendingEventTime;
  433.                 final boolean foundRoot = findRoot(interpolator, t0, g0, t, g);
  434.                 // make sure the new root is not the same as the old root, if one exists
  435.                 meFirst = foundRoot && !pendingEventTime.equals(oldPendingEventTime);
  436.             }
  437.         }

  438.         if (!meFirst) {
  439.             // advance t0 to the current time so we can't find events that occur before t
  440.             t0 = t;
  441.         }

  442.         return meFirst;
  443.     }

  444.     /**
  445.      * Notify the user's listener of the event. The event occurs wholly within this method
  446.      * call including a call to {@link FieldODEEventHandler#resetState(FieldODEEventDetector,
  447.      * FieldODEStateAndDerivative)} if necessary.
  448.      *
  449.      * @param state the state at the time of the event. This must be at the same time as
  450.      *              the current value of {@link #getEventTime()}.
  451.      * @return the user's requested action and the new state if the action is {@link
  452.      * Action#RESET_STATE}. Otherwise the new state is {@code state}. The stop time
  453.      * indicates what time propagation should stop if the action is {@link Action#STOP}.
  454.      * This guarantees the integration will stop on or after the root, so that integration
  455.      * may be restarted safely.
  456.      */
  457.     @Override
  458.     public FieldEventOccurrence<T> doEvent(final FieldODEStateAndDerivative<T> state) {
  459.         // check event is pending and is at the same time
  460.         check(pendingEvent);
  461.         check(state.getTime() == this.pendingEventTime);

  462.         final Action action = handler.eventOccurred(state, detector, increasing == forward);
  463.         final FieldODEState<T> newState;
  464.         if (action == Action.RESET_STATE) {
  465.             newState = handler.resetState(detector, state);
  466.         } else {
  467.             newState = state;
  468.         }
  469.         // clear pending event
  470.         pendingEvent = false;
  471.         pendingEventTime = null;
  472.         // setup for next search
  473.         earliestTimeConsidered = afterEvent;
  474.         t0 = afterEvent;
  475.         g0 = afterG;
  476.         g0Positive = increasing;
  477.         // check g0Positive set correctly
  478.         check(g0.isZero() || g0Positive == (g0.getReal() > 0));
  479.         return new FieldEventOccurrence<>(action, newState, stopTime);
  480.     }

  481.     /**
  482.      * Check the ordering of two times.
  483.      *
  484.      * @param t1 the first time.
  485.      * @param t2 the second time.
  486.      * @return true if {@code t2} is strictly after {@code t1} in the propagation
  487.      * direction.
  488.      */
  489.     private boolean strictlyAfter(final T t1, final T t2) {
  490.         return forward ? t1.getReal() < t2.getReal() : t2.getReal() < t1.getReal();
  491.     }

  492.     /**
  493.      * Get the next number after the given number in the current propagation direction.
  494.      *
  495.      * <p> Assumes T has the same precision as a double.
  496.      *
  497.      * @param t input time
  498.      * @return t +/- 1 ulp depending on the direction.
  499.      */
  500.     private T nextAfter(final T t) {
  501.         // direction
  502.         final int sign = forward ? 1 : -1;
  503.         final double ulp = FastMath.ulp(t.getReal());
  504.         return t.add(sign * ulp);
  505.     }

  506.     /**
  507.      * Same as keyword assert, but throw a {@link MathRuntimeException}.
  508.      *
  509.      * @param condition to check
  510.      * @throws MathRuntimeException if {@code condition} is false.
  511.      */
  512.     private void check(final boolean condition) throws MathRuntimeException {
  513.         if (!condition) {
  514.             throw MathRuntimeException.createInternalError();
  515.         }
  516.     }

  517.     /**
  518.      * Get the time that happens first along the current propagation direction: {@link
  519.      * #forward}.
  520.      *
  521.      * @param a first time
  522.      * @param b second time
  523.      * @return min(a, b) if forward, else max (a, b)
  524.      */
  525.     private T minTime(final T a, final T b) {
  526.         return forward ? FastMath.min(a, b) : FastMath.max(a, b);
  527.     }

  528.     /**
  529.      * Shift a time value along the current integration direction: {@link #forward}.
  530.      *
  531.      * @param t     the time to shift.
  532.      * @param delta the amount to shift.
  533.      * @return t + delta if forward, else t - delta. If the result has to be rounded it
  534.      * will be rounded to be before the true value of t + delta.
  535.      */
  536.     private T shiftedBy(final T t, final T delta) {
  537.         if (forward) {
  538.             final T ret = t.add(delta);
  539.             if (ret.subtract(t).getReal() > delta.getReal()) {
  540.                 // nextDown(ret)
  541.                 return ret.subtract(FastMath.ulp(ret.getReal()));
  542.             } else {
  543.                 return ret;
  544.             }
  545.         } else {
  546.             final T ret = t.subtract(delta);
  547.             if (t.subtract(ret).getReal() > delta.getReal()) {
  548.                 // nextUp(ret)
  549.                 return ret.add(FastMath.ulp(ret.getReal()));
  550.             } else {
  551.                 return ret;
  552.             }
  553.         }
  554.     }

  555.     /** Get the occurrence time of the event triggered in the current step.
  556.      * @return occurrence time of the event triggered in the current
  557.      * step or infinity if no events are triggered
  558.      */
  559.     @Override
  560.     public T getEventTime() {
  561.         return pendingEvent ?
  562.                pendingEventTime :
  563.                t0.getField().getZero().add(forward ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY);
  564.     }

  565. }