1.. _tut_events:
2
3Event detection
4===============
5
6.. versionadded:: 0.6.0
7
8When integrating systems of ODEs, the need often arises to detect the occurrence of specific
9conditions (or *events*) in the state of the system. Many real systems, for instance, are described by equations
10that change discontinuously in response to particular conditions
11(e.g., a spacecraft entering the cone of shadow of a planet,
12or a thermostat switching on once the temperature reaches a certain level). In other situations,
13detection of specific system states may suffice (e.g., in the computation of
14`Poincaré sections <https://en.wikipedia.org/wiki/Poincar%C3%A9_map>`__).
15
16An event in a system of ODEs can be defined by an *event equation* of the form
17
18.. math::
19
20    g\left( t, \boldsymbol{x} \left( t \right) \right) = 0,
21
22where, as usual, :math:`t` is the independent variable (time) and :math:`\boldsymbol{x} \left( t \right)` the state vector of the system.
23As a concrete example, the collision between two spheres of radius 1 moving in a three-dimensional space can be described
24by the event equation
25
26.. math::
27
28    \left( x_1 - x_0 \right)^2 + \left( y_1 - y_0 \right)^2 + \left( z_1 - z_0 \right)^2 - 4 = 0,
29
30where :math:`\left( x_0, y_0, z_0 \right)` and :math:`\left( x_1, y_1, z_1 \right)` are the Cartesian coordinates
31of the spheres' centres.
32
33heyoka features a flexible and accurate event detection framework in which the :ref:`expression system <tut_expression_system>`
34can be used to formulate arbitrary event equations. The event equations are then added to the ODE system and
35integrated together with the other equations, so that, at every timestep, a Taylor series expansion of the event equations
36in powers of time is available. Polynomial root finding techniques :cite:`collins1976polynomial` are then employed
37on the Taylor series of the event equations to accurately locate the time of occurrence of an event within the timestep.
38
39Like many other ODE integration libraries, heyoka makes a fundamental distinction between two types of events, *terminal* and *non-terminal*.
40We will begin with non-terminal events, as they are conceptually simpler.
41
42Non-terminal events
43-------------------
44
45Non-terminal events are events that do not modify the state of an ODE system. That is, the occurrence of a non-terminal event does not
46change the system's dynamics and it does not alter the state vector of the system. A typical use of non-terminal events is to detect and log
47when the system reaches a particular state of interest (e.g., flagging close encounters between celestial bodies, detecting when
48a velocity or coordinate is zero, etc.).
49
50As an initial example, we will turn to our good ole friend, the simple pendulum:
51
52.. math::
53
54    \begin{cases}
55    x^\prime = v \\
56    v^\prime = -9.8 \sin x
57    \end{cases}.
58
59Our goal will be to detect when the bob reaches the point of maximum amplitude, which corresponds to the angular velocity
60:math:`v` going to zero. In other words, out (very simple) event equation is
61
62.. math::
63
64    v = 0.
65
66We begin, as usual, with the definition of the symbolic variables:
67
68.. literalinclude:: ../tutorial/event_basic.cpp
69    :language: c++
70    :lines: 17-18
71
72Next, we create a vector into which we will log the times at which :math:`v = 0`:
73
74.. literalinclude:: ../tutorial/event_basic.cpp
75    :language: c++
76    :lines: 20-22
77
78We can now proceed to create a non-terminal event:
79
80.. literalinclude:: ../tutorial/event_basic.cpp
81    :language: c++
82    :lines: 24-38
83
84Non-terminal events are represented in heyoka by the ``nt_event`` class. Like the :ref:`adaptive integrator <tut_adaptive>`
85class, ``nt_event`` is parametrised over the floating-point type we want to use for  event detection
86(in this case, ``double``). The first mandatory argument for the construction of a non-terminal event is the left-hand
87side of the event equation, which in this case is simply :math:`v`.
88
89The second mandatory construction argument is a callback function that will be invoked when the event is detected.
90The callback function can be a lambda, a regular function, or a function object - the only requirement is that the
91callback is a callable object with the following signature:
92
93.. code-block:: c++
94
95   void (taylor_adaptive<double> &, double, int);
96
97The first argument is a mutable reference to the integrator object, the second argument is the absolute time
98at which the event was detected (i.e., the trigger time), and the last argument is the sign of the derivative
99of the event equation at the trigger time (-1 for negative derivative, 1 for positive derivative and 0 for
100zero derivative).
101
102Because non-terminal event detection is performed at the end of an integration step,
103when the callback is invoked the state and time of the integrator object are those *at the end* of the integration
104step in which the event was detected. Note that when integrating an ODE system with events, the ``taylor_adaptive``
105class ensures that the Taylor coefficients are always kept up to date (as explained in the tutorial about
106:ref:`dense output <tut_d_output>`), and thus in the callback function it is always possible to use the ``update_d_output()``
107function to compute the dense output at any time within the last timestep that was taken.
108
109.. warning::
110
111    The ``taylor_adaptive`` object is passed as a non-const reference only so that it is possible to call
112    non-const functions on it (such as ``update_d_output()``). Do not try to assign a new integrator object
113    from within the callback, as that will result in undefined behaviour.
114
115In this example, we perform two actions in the callback:
116
117- first, we compute the dense output at the event trigger time and print
118  the value of the ``x`` coordinate,
119- second, we append to ``zero_vel_times`` the trigger time.
120
121We are now ready to create our first event-detecting integrator:
122
123.. literalinclude:: ../tutorial/event_basic.cpp
124    :language: c++
125    :lines: 40-50
126
127The list of non-terminal events is passed to the constructor of the
128integrator via the ``kw::nt_events`` keyword argument. Note how we
129set up the initial conditions so that the bob is at rest at an
130angle of amplitude :math:`0.05`.
131
132Let us now integrate for a few time units and observe the screen output:
133
134.. literalinclude:: ../tutorial/event_basic.cpp
135    :language: c++
136    :lines: 55-56
137
138.. code-block:: console
139
140   Value of x when v is zero: -0.05
141   Value of x when v is zero: 0.05
142   Value of x when v is zero: -0.05
143   Value of x when v is zero: 0.05
144   Value of x when v is zero: -0.05
145
146As expected, when the velocity of the bob goes to zero
147the absolute value the :math:`x` angle corresponds to the
148initial amplitude of :math:`0.05`.
149
150Let us now print the event times:
151
152.. literalinclude:: ../tutorial/event_basic.cpp
153    :language: c++
154    :lines: 58-61
155
156.. code-block:: console
157
158   Event detection time: 0
159   Event detection time: 1.003701787940065
160   Event detection time: 2.00740357588013
161   Event detection time: 3.011105363820196
162   Event detection time: 4.014807151760261
163
164We can see how the the initial condition :math:`v_0 = 0` immediately
165and correctly triggers an event at :math:`t = 0`. Physically, we know that the time
166interval between the events must be half the period :math:`T` of the pendulum,
167which can be computed exactly via elliptic functions. With the specific
168initial conditions of this example, :math:`T = 2.0074035758801299\ldots`, and
169we can see from the event times printed to screen
170how the event detection system was accurate to machine precision.
171
172Event direction
173^^^^^^^^^^^^^^^
174
175By default, heyoka will detect all zeroes of the event equations regardless
176of the *direction* of the zero crossing (i.e., the value of the time derivative
177of the event equation at the zero). However, it is sometimes useful to trigger the detection
178of an event only if its direction is positive or negative. Event direction is represented
179in heyoka by the ``event_direction`` enum, whose values can be
180
181- ``event_direction::any`` (the default),
182- ``event_direction::positive`` (derivative > 0),
183- ``event_direction::negative`` (derivative < 0).
184
185Event direction can be specified upon construction via the ``kw::direction`` keyword:
186
187.. literalinclude:: ../tutorial/event_basic.cpp
188    :language: c++
189    :lines: 63-72
190
191In this specific case, constraining the event direction to be positive is equivalent
192to detecting :math:`v = 0` only when the pendulum reaches the maximum amplitude on the left.
193Let us take a look at the event times:
194
195.. literalinclude:: ../tutorial/event_basic.cpp
196    :language: c++
197    :lines: 74-80
198
199.. code-block:: console
200
201   Event detection time: 0
202   Event detection time: 2.00740357588013
203   Event detection time: 4.014807151760261
204
205Indeed, the event now triggers only 3 times (instead of 5), and the times confirm
206that the event is detected only when :math:`v` switches from negative to positive, i.e.,
207at :math:`t=0`, :math:`t=T` and :math:`t=2T`.
208
209Multiple events
210^^^^^^^^^^^^^^^
211
212When multiple events trigger within the same timestep (or if the same event triggers
213multiple times), heyoka will process the events in chronological order
214(or reverse chronological order when integrating backwards in time).
215
216Let us demonstrate this with another example with the simple pendulum.
217We will now aim to detect two events defined by the equations:
218
219.. math::
220
221    \begin{cases}
222    v = 0 \\
223    v^2 - 10^{-12} = 0
224    \end{cases}.
225
226In other words, we are looking to determine the time of maximum amplitude (:math:`v = 0`) and
227the time at which the absolute value of the angular velocity is small but not zero. Because
228of the closeness of these events, we can expect both events to be detected during the same timestep,
229with the second event triggering twice.
230
231Let's begin by defining the two events:
232
233.. literalinclude:: ../tutorial/event_basic.cpp
234    :language: c++
235    :lines: 82-88
236
237This time the events' callbacks just print the event time to screen, without
238modifying the ``zero_vel_times`` list.
239
240We can then reset the integrator, propagate for a few time units and check the screen output:
241
242.. literalinclude:: ../tutorial/event_basic.cpp
243    :language: c++
244    :lines: 90-94
245
246.. code-block:: console
247
248    Event 0 triggering at t=0
249    Event 1 triggering at t=2.041666914753826e-06
250    Event 1 triggering at t=1.003699746272244
251    Event 0 triggering at t=1.003701787940065
252    Event 1 triggering at t=1.003703829606799
253    Event 1 triggering at t=2.007401534213656
254    Event 0 triggering at t=2.00740357588013
255    Event 1 triggering at t=2.00740561754654
256    Event 1 triggering at t=3.011103322152711
257    Event 0 triggering at t=3.011105363820196
258    Event 1 triggering at t=3.011107405487484
259    Event 1 triggering at t=4.014805110093445
260    Event 0 triggering at t=4.014807151760261
261    Event 1 triggering at t=4.014809193427102
262
263Note how the events are indeed processed in chronological order, and how the event detection system is able to
264successfully recognize the second event triggering twice in close succession.
265
266Terminal events
267---------------
268
269The fundamental characteristic of terminal events is that, in contrast to non-terminal events,
270they alter the dynamics and/or the state of the system. A typical example of a terminal event is the
271`elastic collision <https://en.wikipedia.org/wiki/Elastic_collision>`__ of
272two rigid bodies, which instantaneously and discontinuously changes the bodies' velocity vectors.
273Another example is the switching on of a spacecraft engine, which alters the differential
274equations governing the dynamics of the spacecraft.
275
276Terminal events are represented in heyoka by the ``t_event`` class. Similarly to
277the ``nt_event`` class, the construction of a ``t_event`` requires
278at the very least the expression corresponding to the left-hand side of the event equation.
279A number of additional optional keyword arguments can be passed to customise the behaviour
280of a terminal event:
281
282- ``kw::callback``: a callback function that will be called when the event triggers. Note that,
283  for terminal events, the presence of a callback is optional (whereas it is mandatory for
284  non-terminal events);
285- ``kw::cooldown``: a floating-point value representing the cooldown time for the terminal event
286  (see :ref:`below <tut_t_event_cooldown>` for an explanation);
287- ``kw::direction``: a value of the ``event_direction`` enum which, like for non-terminal
288  events, can be used to specify that the event should be detected only for a specific direction
289  of the zero crossing.
290
291It is important to understand how heyoka reacts to terminal events. At every integration timestep, heyoka
292performs event detection for both terminal and non-terminal events. If one or more terminal events
293are detected, heyoka will sort the detected terminal events by time and will select the first
294terminal event triggering in chronological order (or reverse chronological order when integrating
295backwards in time). All the other terminal events and all the non-terminal events triggering *after*
296the first terminal event are discarded. heyoka then propagates the state of the system up to the
297trigger time of the first terminal event, executes the callbacks of the surviving non-terminal events
298in chronological order and finally executes the callback of the first terminal event (if provided).
299
300In order to illustrate the use of terminal events, we will consider a damped pendulum with a small twist:
301the friction coefficient :math:`\alpha` switches discontinuously between 1 and 0 every time the angular
302velocity :math:`v` is zero. The ODE system reads:
303
304.. math::
305
306    \begin{cases}
307    x^\prime = v \\
308    v^\prime = - 9.8\sin x - \alpha v
309    \end{cases},
310
311and the terminal event equation is, again, simply :math:`v = 0`.
312
313Let us begin with the definition of the terminal event:
314
315.. literalinclude:: ../tutorial/event_basic.cpp
316    :language: c++
317    :lines: 96-114
318
319Like in the case of non-terminal events, we specified as first construction argument
320the event equation. As second argument we passed a callback function that will be invoked
321when the event triggers.
322
323As you can see from the code snippet, the callback signature for terminal events
324differs from the signature non-terminal callbacks. Specifically:
325
326- the event trigger time is not passed to the callback. This is not necessary
327  because, when a terminal event triggers, the state of the integrator is propagated
328  up to the event, and thus the trigger time is the current integrator time
329  (which can be fetched via ``ta.get_time()``);
330- there is an additional boolean function argument, here called ``mr``. We will be ignoring
331  this extra argument for the moment, its meaning will be clarified in the
332  :ref:`cooldown section <tut_t_event_cooldown>`;
333- whereas non-terminal event callbacks do not return anything, terminal event callbacks
334  are required to return ``true`` or ``false``. If the callback returns ``false`` the integration
335  will always be stopped after the execution of the callback. Otherwise, when using the
336  ``propagate_*()`` family of functions, the integration will resume after the execution
337  of the callback.
338
339Note that, for the purpose of stopping the integration, an event *without* a callback is considered
340equivalent to an event whose callback returns ``false``.
341We thus refer to terminal events without a callback or whose callback returns ``false``
342as *stopping* terminal events, because their occurrence will prevent the integrator from continuing
343without user intervention.
344
345Like for non-terminal events, the last callback argument is the sign of the time derivative
346of the event equation at the event trigger time.
347
348In this example, within the callback code we alter the value of the drag coefficient :math:`\alpha`
349(which is stored within the :ref:`runtime parameters <tut_param>` of the integrator): if :math:`\alpha`
350is currently 0, we set it to 1, otherwise we set it to 0.
351
352Let us proceed to the construction of the integrator:
353
354.. literalinclude:: ../tutorial/event_basic.cpp
355    :language: c++
356    :lines: 116-124
357
358Similarly to the non-terminal events case, the list of terminal events
359is specified when constructing an integrator via the ``kw::t_events`` keyword argument.
360
361If a terminal event triggers within the single-step functions (``step()`` and ``step_backward()``),
362the outcome of the integration will contain the index of the event that triggered. Let us see a simple example:
363
364.. literalinclude:: ../tutorial/event_basic.cpp
365    :language: c++
366    :lines: 126-134
367
368.. code-block:: console
369
370   Integration outcome: taylor_outcome::terminal_event_0 (continuing)
371   Event index        : 0
372
373The screen output confirms that the first (and only) event triggered. For stopping terminal events,
374the numerical value of the outcome is the opposite of the event index minus one.
375
376Because here we used the single step
377function, even if the event's callback returned ``true`` the integration was stopped in correspondence of the
378event. Let us now use the ``propagate_grid()`` function instead, so that the integration resumes after the
379execution of the callback:
380
381.. literalinclude:: ../tutorial/event_basic.cpp
382    :language: c++
383    :lines: 136-145
384
385.. code-block:: console
386
387   [-0.02976504606251412, -0.02063006479837935]
388   [0.02970666582653454, 0.02099345736431702]
389   [-0.01761378049610636, -0.01622382722426959]
390   [0.01757771112979705, 0.01613903817360225]
391   [-0.01037481471383597, -0.01205316233867281]
392   [0.01035648925410416, 0.01177669636844242]
393   [-0.006080605964468329, -0.008627473720971276]
394   [0.006074559637531474, 0.008299135527482404]
395   [-0.003544733998720797, -0.006013682818278612]
396   [0.003546198899884463, 0.005703010459398463]
397
398   Final time: 10
399
400The screen output confirms that indeed the integration continued up to the final time :math:`t = 10`. The values
401of the angle and angular velocity throughout the integration show how the drag coefficient (which was on roughly
402for half of the total integration time) progressively slowed the bob down.
403
404.. _tut_t_event_cooldown:
405
406Cooldown
407^^^^^^^^
408
409One notable complication when restarting an integration that was stopped in correspondence of a terminal event
410is the risk of immediately re-triggering the same event, which would lead to an endless loop without any progress
411being made in the integration. This phenomenon is sometimes called *discontinuity sticking* in the literature.
412
413In order to avoid this issue, whenever a terminal event occurs the event enters
414a *cooldown* period. Within the cooldown period, occurrences of the same event are ignored by the event detection
415system.
416
417The length of the cooldown period is, by default, automatically deduced by heyoka, following a heuristic
418that takes into account:
419
420- the error tolerance of the integrator,
421- the derivative of the event equation at the trigger time.
422
423The heuristic works best under the assumption that the event equation does not change (much) after the
424execution of the event's callback. If, for any reason, the automatic deduction heuristic is
425to be avoided, it is possible to set a custom value for the cooldown.
426A custom cooldown period can be selected when constructing
427a terminal event via the ``kw::cooldown`` keyword argument.
428
429When a terminal event triggers and enters the cooldown period, the event detection system will also try to detect
430the occurrence of multiple roots of the event equation within the cooldown period. If such multiple roots are detected,
431then the ``mr`` boolean parameter in the terminal event callback will be set to ``true``, so that the user
432has the possibility to handle such occurrence. Note that an ``mr`` value of ``false`` in the callback does not imply
433that multiple roots do not exist, just that they were not detected.
434
435Note that manually modifying the integrator's time or state does **not** automatically reset the cooldown values
436for terminal events. This could in principle lead to missing terminal events when the integration restarts.
437For this reason, a member function called ``reset_cooldowns()`` is available to clear the cooldown timers of
438all terminal events.
439
440Limitations and caveats
441-----------------------
442
443Badly-conditioned event equations
444^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
445
446Because heyoka's event detection system is based on polynomial root finding techniques, it will experience
447issues when the Taylor series of the event equations have roots of multiplicity greater than 1. This is usually
448not a problem in practice, unless the event equations are written in such a way to always generate polynomials
449with multiple roots.
450
451For instance, an event equation such as
452
453.. math::
454
455    \left[ g\left( t, \boldsymbol{x} \left( t \right) \right) \right]^2 = 0
456
457will be troublesome, because both the event equation *and* its time derivative will be zero
458when the event triggers. This will translate to a Taylor series with a double root in correspondence
459of the event trigger time, which will lead to a breakdown of the root finding algorithm.
460This, at best, will result in reduced performance and, at worst, in missing events altogether.
461Additionally, in case of terminal events the automatically-deduced cooldown value in correspondence of
462a double root will tend to infinity.
463
464As a general rule, users should then avoid defining event equations in which the event trigger times
465are stationary points.
466
467Note that missed events due to badly-conditioned polynomials will likely be flagged by heyoka's logging system.
468
469Event equations and timestepping
470^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
471
472As explained earlier, the differential equations of the events are added to the ODE system and
473integrated together with the original equations. Because of this, event equations influence the
474selection of the adaptive timestep, even if no event is ever detected throughout the integration.
475
476For instance, the absolute value of the event equation at the beginning of the timestep is taken
477into account for the determination of the timestep size in relative error control mode. Thus, if
478the typical magnitude of the event equation throughout the integration is much larger than the typical
479magnitude of the state variables, the integration error for the state variables will increase with respect
480to an integration without event detection.
481
482As another example, an event equation which requires small timesteps for accurate numerical propagation
483will inevitably slow down also the propagation of the ODEs.
484
485
486Full code listing
487-----------------
488
489.. literalinclude:: ../tutorial/event_basic.cpp
490   :language: c++
491   :lines: 9-
492