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