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