"EventLocator" Method for NDSolve

Introduction

It is often useful to be able to detect and precisely locate a change in a differential system. For example, with the detection of a singularity or state change, the appropriate action can be taken, such as restarting the integration.

An event for a differential system

is a point along the solution at which a real-valued event function is zero:

It is also possible to consider Boolean-valued event functions, in which case the event occurs when the function changes from (RefLink[True, paclet:ref/True]) to (RefLink[False, paclet:ref/False]) or vice versa.

The "EventLocator" method that is built into (RefLink[NDSolve, paclet:ref/NDSolve]) works effectively as a controller method; it handles checking for events and taking the appropriate action, but the integration of the differential system is otherwise left completely to an underlying method.

In this section, examples are given to demonstrate the basic use of the "EventLocator" method and options. Subsequent sections show more involved applications of event location, such as period detection, Poincaré sections, and discontinuity handling.

These initialization commands load some useful packages that have some differential equations to solve and define some utility functions:

Needs["DifferentialEquations`NDSolveProblems`"];
Needs["DifferentialEquations`NDSolveUtilities`"];
Needs["DifferentialEquations`InterpolatingFunctionAnatomy`"];
Needs["GUIKit`"];

A simple example is locating an event, such as when a pendulum started at a non-equilibrium position will swing through its lowest point and stopping the integration at that point.

This integrates the pendulum equation up to the first point at which the solution y[t] crosses the axis:

sol = NDSolve[{y''[t] + Sin[y[t]] == 0, y'[0] == 0, y[0] == 1},y,{t,0,10}, Method->{"EventLocator", "Event"->y[t]}]

From the solution you can see that the pendulum reaches its lowest point y(t)==0 at about t==1.675. Using the InterpolatingFunctionAnatomy package, it is possible to extract the value from the (RefLink[InterpolatingFunction, paclet:ref/InterpolatingFunction]) object.

This extracts the point at which the event occurs and makes a plot of the solution (black) and its derivative (blue) up to that point:

end = InterpolatingFunctionDomain[First[y /. sol]][[1,-1]];
Plot[Evaluate[{y[t], y'[t]} /. First[sol]], {t,0,end}, PlotStyle->{{Black},{Blue}}]

When you use the event locator method, the events to be located and the action to take upon finding an event are specified through method options of the "EventLocator" method.

The default action on detecting an event is to stop the integration as demonstrated earlier. The event action can be any expression. It is evaluated with numerical values substituted for the problem variables whenever an event is detected.

This prints the time and values each time the event y'(t)==y(t) is detected for a damped pendulum:

NDSolve[{y''[t] + .1 y'[t] + Sin[y[t]] == 0, y'[0] == 0, y[0] == 1},y,{t,0,10}, Method->{"EventLocator", "Event"->y'[t]- y[t], "EventAction":> Print["y'[", t, "] = y[", t, "] = ", y[t]]}]

Note that in the example, the "EventAction" option was given using (RefLink[RuleDelayed, paclet:ref/RuleDelayed]) (:>) to prevent it from evaluating except when the event is located.

You can see from the printed output that when the action does not stop the integration, multiple instances of an event can be detected. Events are detected when the sign of the event expression changes. You can restrict the event to be only for a sign change in a particular direction using the "Direction" option.

This collects the points at which the velocity changes from negative to positive for a damped driven pendulum. (RefLink[Reap, paclet:ref/Reap]) and (RefLink[Sow, paclet:ref/Sow]) are programming constructs that are useful for collecting data when you do not, at first, know how much data there will be. (RefLink[Reap, paclet:ref/Reap])[expr] gives the value of expr together with all expressions to which (RefLink[Sow, paclet:ref/Sow]) has been applied during its evaluation. Here (RefLink[Reap, paclet:ref/Reap]) encloses the use of (RefLink[NDSolve, paclet:ref/NDSolve]) and (RefLink[Sow, paclet:ref/Sow]) is a part of the event action, which allows you to collect data for each instance of an event:

Reap[NDSolve[{y''[t] + .1 y'[t] + Sin[y[t]] == .1 Cos[t], y'[0] == 0, y[0] == 1},y,{t,0,50}, Method->{"EventLocator", "Event"->y'[t], "Direction"->1, "EventAction":> Sow[{t, y[t], y'[t]}]}]]

You may notice from the output of the previous example that the events are detected when the derivative is only approximately zero. When the method detects the presence of an event in a step of the underlying integrator (by a sign change of the event expression), then it uses a numerical method to approximately find the position of the root. Since the location process is numerical, you should expect only approximate results. Location method options (RefLink[AccuracyGoal, paclet:ref/AccuracyGoal]), (RefLink[PrecisionGoal, paclet:ref/PrecisionGoal]), and (RefLink[MaxIterations, paclet:ref/MaxIterations]) can be given to those location methods that use (RefLink[FindRoot, paclet:ref/FindRoot]) to control tolerances for finding the root.

For Boolean valued event functions, an event occurs when the function switches from (RefLink[True, paclet:ref/True]) to (RefLink[False, paclet:ref/False]) or vice versa. The "Direction" option can be used to restrict the event only from changes from (RefLink[True, paclet:ref/True]) to (RefLink[False, paclet:ref/False]) ("Direction"->-1) or only from changes from (RefLink[False, paclet:ref/False]) to (RefLink[True, paclet:ref/True]) ("Direction"->1).

This opens up a small window with a button that, when clicked, changes the value of the variable stop to (RefLink[True, paclet:ref/True]) from its initialized value of (RefLink[False, paclet:ref/False]):

NDSolve`stop = False;
GUIRun[Widget["Panel", {Widget["Button",{
"label"->"Stop",
BindEvent["action",
Script[NDSolve`stop = True]]}]}]];

This integrates the pendulum equation up until the button is clicked (or the system runs out of memory):

NDSolve[{y''[t] + Sin[y[t]] == 0, y[0] == 1, y'[0] == 0}, y, {t, 0, ∞},Method->{"EventLocator", "Event":> NDSolve`stop}, MaxSteps->∞]

Take note that in this example, the "Event" option was specified with (RefLink[RuleDelayed, paclet:ref/RuleDelayed]) (:>) to prevent the immediate value of stop from being evaluated and set up as the function.

You can specify more than one event. If the event function evaluates numerically to a list, then each component of the list is considered to be a separate event. You can specify different actions, directions, etc. for each of these events by specifying the values of these options as lists of the appropriate length.

This integrates the pendulum equation up until the point at which the button is clicked. The number of complete swings of the pendulum is kept track of during the integration:

NDSolve`stop = False;
swings = 0;{
NDSolve[{y''[t] +Sin[y[t]] ==0, y[0] == 0, y'[0] == 1}, y, {t, 0, 1000000},Method->{"EventLocator", "Event":> {y[t],NDSolve`stop}, "EventAction":> {swings++,Throw[Null, "StopIntegration"]},"Direction"->{1,All}}, MaxSteps->Infinity],swings}

As you can see from the previous example, it is possible to mix real- and Boolean-valued event functions. The expected number of components and type of each component are based on the values at the initial condition and need to be consistent throughout the integration.

The "EventCondition" option of "EventLocator" allows you to specify additional Boolean conditions that need to be satisfied for an event to be tested. It is advantageous to use this instead of a Boolean event when possible because the root finding process can be done more efficiently.

This stops the integration of a damped pendulum at the first time that y(t)==0 once the decay has reduced the energy integral to -0.9`:

sol=NDSolve[{y''[t] + .1 y'[t] + Sin[y[t]] == 0, y'[0] == 1, y[0] == 0},y,{t,0,100}, Method->{"EventLocator", "Event"->y[t], "EventCondition"->(y'[t]^2/2 - Cos[y[t]]< -0.9), "EventAction":>Throw[end=t, "StopIntegration"]}]

This makes a plot of the solution (black), the derivative (blue), and the energy integral (green). The energy threshold is shown in red:

Plot[Evaluate[{y[t], y'[t],y'[t]^2/2 - Cos[y[t]],-.9} /. First[sol]], {t,0,end}, PlotStyle->{{Black},{Blue},{Green},{Red}}]

The (RefLink[Method, paclet:ref/Method]) option of "EventLocator" allows the specification of the numerical method to use in the integration.

Event Location Methods

The "EventLocator" method works by taking a step of the underlying method and checking to see if the sign (or parity) of any of the event functions is different at the step endpoints. Event functions are expected to be real- or Boolean-valued, so if there is a change, there must be an event in the step interval. For each event function which has an event occurrence in a step, a refinement procedure is carried out to locate the position of the event within the interval.

There are several different methods which can be used to refine the position. These include simply taking the solution at the beginning or the end of the integration interval, a linear interpolation of the event value, and using bracketed root-finding methods. The appropriate method to use depends on a trade off between execution speed and location accuracy.

If the event action is to stop the integration then the particular value at which the integration is stopped depends on the value obtained from the "EventLocationMethod" option of "EventLocator".

Location of a single event is usually fast enough so that the method used will not significantly influence the overall computation time. However, when an event is detected multiple times, the location refinement method can have a substantial effect.

"StepBegin" and "StepEnd" Methods

The crudest methods are appropriate for when the exact position of the event location does not really matter or does not reflect anything with precision in the underlying calculation. The stop button example from the previous section is such a case: time steps are computed so quickly that there is no way that you can time the click of a button to be within a particular time step, much less at a particular point within a time step. Thus, based on the inherent accuracy of the event, there is no point in refining at all. You can specify this by using the "StepBegin" or "StepEnd" location methods. In any example where the definition of the event is heuristic or somewhat imprecise, this can be an appropriate choice.

"LinearInterpolation" Method

When event results are needed for the purpose of points to plot in a graph, you only need to locate the event to the resolution of the graph. While just using the step end is usually too crude for this, a single linear interpolation based on the event function values suffices.

Denote the event function values at successive mesh points of the numerical integration:

Linear interpolation gives

A linear approximation of the event time is then:

Linear interpolation could also be used to approximate the solution at the event time. However, since derivative values (Subscript[f, n])==f((Subscript[t, n]),(Subscript[y, n]))  and (Subscript[f, n+1])==f((Subscript[t, n+1]),(Subscript[y, n+1])) are available at the mesh points, a better approximation of the solution at the event can be computed cheaply using cubic Hermite interpolation as

for suitably defined interpolation weights:

You can specify refinement based on a single linear interpolation with the setting "LinearInterpolation".

This computes the solution for a single period of the pendulum equation and plots the solution for that period:

sol = First[NDSolve[{y''[t] + Sin[y[t]] == 0, y[0] == 3, y'[0] == 0},y,{t,0,∞}, Method->{"EventLocator",
 "Event"->y'[t], 
"EventAction":> Throw[end = t, "StopIntegration"],"Direction"->-1, 
"EventLocationMethod"->"LinearInterpolation",Method->"ExplicitRungeKutta"}]];
Plot[Evaluate[{y[t], y'[t]} /. sol], {t,0,end}, PlotStyle->{{Black}, {Blue}}]

At the resolution of the plot over the entire period, you cannot see that the endpoint may not be exactly where the derivative hits the axis. However, if you zoom in enough, you can see the error.

This shows a plot just near the endpoint:

Plot[Evaluate[y'[t] /. sol], {t,end*(1 - .001),end}, PlotStyle->Blue]

The linear interpolation method is sufficient for most viewing purposes, such as the Poincaré section examples shown in the following section. Note that for Boolean-valued event functions, linear interpolation is effectively only one bisection step, so the linear interpolation method may be inadequate for graphics.

Brent's Method

The default location method is the event location method "Brent", finding the location of the event using (RefLink[FindRoot, paclet:ref/FindRoot]) with Brent's method. Brent's method starts with a bracketed root and combines steps based on interpolation and bisection, guaranteeing a convergence rate at least as good as bisection. You can control the accuracy and precision to which (RefLink[FindRoot, paclet:ref/FindRoot]) tries to get the root of the event function using method options for the "Brent" event location method. The default is to find the root to the same accuracy and precision as (RefLink[NDSolve, paclet:ref/NDSolve]) is using for local error control.

For methods that support continuous or dense output, the argument for the event function can be found quite efficiently simply by using the continuous output formula. However, for methods that do not support continuous output, the solution needs to be computed by taking a step of the underlying method, which can be relatively expensive. An alternate way of getting a solution approximation that is not accurate to the method order, but is consistent with using (RefLink[FindRoot, paclet:ref/FindRoot]) on the (RefLink[InterpolatingFunction, paclet:ref/InterpolatingFunction]) object returned from (RefLink[NDSolve, paclet:ref/NDSolve]), is to use cubic Hermite interpolation, obtaining approximate solution values in the middle of the step by interpolation based on the solution values and solution derivative values at the step ends.

Comparison

This example integrates the pendulum equation for a number of different event location methods and compares the time when the event is found.

This defines the event location methods to use:

eventmethods = {"StepBegin","StepEnd","LinearInterpolation",Automatic};

This integrates the system and prints out the method used and the value of the independent variable when the integration is terminated:

Map[
NDSolve[{y''[t] + Sin[y[t]] == 0, y[0] == 3, y'[0] == 0},y,{t,0,∞}, Method->{"EventLocator",
 "Event"->y'[t], 
"EventAction":>Throw[Print[#,": t = ",t," y'[t] = ", y'[t]], "StopIntegration"],"Direction"->-1,Method->"ExplicitRungeKutta", 
"EventLocationMethod"->#}]&,
eventmethods
];

Examples

Falling Body

This system models a body falling under the force of gravity encountering air resistance (see [M04]).

The event action stores the time when the falling body hits the ground and stops the integration:

sol=y[t]/.First[NDSolve[{y''[t]==-1+y'[t]^2,y[0]==1,y'[0]==0},y,{t,0,Infinity},Method->{"EventLocator","Event":>y[t],"EventAction":>Throw[tend=t,"StopIntegration"]}]]

This plots the solution and highlights the initial and final points (green and red) by encircling them:

plt=Plot[sol,{t,0,tend},Frame->True,Axes->False,PlotStyle->Blue,DisplayFunction->Identity];

grp=Graphics[{{Green,Circle[{0,1},0.025]},{Red,Circle[{tend,sol/.t->tend},0.025]}}];

Show[plt,grp,DisplayFunction->$DisplayFunction]

Period of the Van der Pol Oscillator

The Van der Pol oscillator is an example of an extremely stiff system of ODEs. The event locator method can call any method for actually doing the integration of the ODE system. The default method, (RefLink[Automatic, paclet:ref/Automatic]), automatically switches to a method appropriate for stiff systems when necessary, so that stiffness does not present a problem.

This integrates the Van der Pol system for a particular value of the parameter μ==1000 up to the point where the variable (Subscript[y, 2]) reaches its initial value and direction:

vsol = NDSolve[(({{(Subscript[y, 1])'[t] == (Subscript[y, 2])[t] , (Subscript[y, 1])[0]==2}, {(Subscript[y, 2])'[t]==1000 (1 - (Subscript[y, 1])[t]^2) (Subscript[y, 2])[t] - (Subscript[y, 1])[t], (Subscript[y, 2])[0]==0}})),
{(Subscript[y, 1]), (Subscript[y, 2])},{t,3000}, Method->{"EventLocator", "Event"->(Subscript[y, 2])[t], "Direction"->-1}]

Note that the event at the initial condition is not considered.

By selecting the endpoint of the (RefLink[NDSolve, paclet:ref/NDSolve]) solution, it is possible to write a function that returns the period as a function of μ.

This defines a function that returns the period as a function of μ:

vper[μ_] := Module[{vsol},
vsol = First[(Subscript[y, 2]) /. NDSolve[(({{(Subscript[y, 1])'[t] == (Subscript[y, 2])[t] , (Subscript[y, 1])[0]==2}, {(Subscript[y, 2])'[t]==μ (1 - (Subscript[y, 1])[t]^2) (Subscript[y, 2])[t] - (Subscript[y, 1])[t], (Subscript[y, 2])[0]==0}})),
{(Subscript[y, 1]), (Subscript[y, 2])},{t,Max[100, 3 μ]}, Method->{"EventLocator", "Event"->(Subscript[y, 2])[t], "Direction"->-1}]];
InterpolatingFunctionDomain[vsol][[1,-1]]];

This uses the function to compute the period at μ==1000:

vper[1000]

Of course, it is easy to generalize this method to any system with periodic solutions.

Poincaré Sections

Using Poincaré sections is a useful technique for visualizing the solutions of high-dimensional differential systems.

For an interactive graphical interface see the package (RefLinkPlain[EquationTrekker, paclet:EquationTrekker/tutorial/EquationTrekker]).

The Hénon–Heiles System

Define the Hénon–Heiles system that models stellar motion in a galaxy.

This gets the Hénon–Heiles system from the NDSolveProblems package:

system = GetNDSolveProblem["HenonHeiles"];
vars = system["DependentVariables"];eqns = {system["System"], system["InitialConditions"]}

The Poincaré section of interest in this case is the collection of points in the (Subscript[Y, 2])-(Subscript[Y, 4]) plane when the orbit passes through (Subscript[Y, 1])==0.

Since the actual result of the numerical integration is not required, it is possible to avoid storing all the data in (RefLink[InterpolatingFunction, paclet:ref/InterpolatingFunction]) by specifying the output variables list (in the second argument to (RefLink[NDSolve, paclet:ref/NDSolve])) as empty, or {}. This means that (RefLink[NDSolve, paclet:ref/NDSolve]) will produce no (RefLink[InterpolatingFunction, paclet:ref/InterpolatingFunction]) as output, avoiding storing a lot of unnecessary data. (RefLink[NDSolve, paclet:ref/NDSolve]) does give a message (RefLink[NDSolve, paclet:ref/NDSolve])::noout warning there will be no output functions, but it can safely be turned off in this case since the data of interest is collected from the event actions.

The linear interpolation event location method is used because the purpose of the computation here is to view the results in a graph with relatively low resolution. If you were doing an example where you needed to zoom in on the graph to great detail or to find a feature, such as a fixed point of the Poincaré map, it would be more appropriate to use the default location method.

This turns off the message warning about no output:

Off[NDSolve::noout];

This integrates the Hénon–Heiles system using a fourth-order explicit Runge–Kutta method with fixed step size of 0.25. The event action is to use (RefLink[Sow, paclet:ref/Sow]) on the values of (Subscript[Y, 2]) and (Subscript[Y, 4]):

data=
Reap[
NDSolve[eqns,{}, {T, 10000},
Method->{"EventLocator","Event"->(Subscript[Y, 1])[T],"EventAction":> Sow[{(Subscript[Y, 2])[T], (Subscript[Y, 4])[T]}],"EventLocationMethod"->"LinearInterpolation",
"Method"->{"FixedStep","Method"->{"ExplicitRungeKutta","DifferenceOrder"->4}}},
StartingStepSize->0.25, MaxSteps->∞];
];

This plots the Poincaré section. The collected data is found in the last part of the result of (RefLink[Reap, paclet:ref/Reap]) and the list of points is the first part of that:

psdata=data[[-1,1]];
ListPlot[psdata,Axes->False,Frame->True,AspectRatio->1]

Since the Hénon–Heiles system is Hamiltonian, a symplectic method gives much better qualitative results for this example.

This integrates the Hénon–Heiles system using a fourth-order symplectic partitioned Runge–Kutta method with fixed step size of 0.25. The event action is to use (RefLink[Sow, paclet:ref/Sow]) on the values of (Subscript[Y, 2]) and (Subscript[Y, 4]):

sdata=
Reap[
NDSolve[eqns,{}, {T, 10000},
Method->{"EventLocator","Event"->(Subscript[Y, 1])[T],"EventAction":> Sow[{(Subscript[Y, 2])[T], (Subscript[Y, 4])[T]}],"EventLocationMethod"->"LinearInterpolation",
"Method"->{"SymplecticPartitionedRungeKutta","DifferenceOrder"->4,"PositionVariables"->{(Subscript[Y, 1])[T], (Subscript[Y, 2])[T]}}},
StartingStepSize->0.25, MaxSteps->∞];
];

This plots the Poincaré section. The collected data is found in the last part of the result of (RefLink[Reap, paclet:ref/Reap]) and the list of points is the first part of that:

psdata=sdata[[-1,1]];
ListPlot[psdata,Axes->False,Frame->True,AspectRatio->1]

The ABC Flow

This loads an example problem of the Arnold–Beltrami–Childress (ABC) flow that is used to model chaos in laminar flows of the three-dimensional Euler equations:

system=GetNDSolveProblem["ArnoldBeltramiChildress"];
eqs = system["System"];
vars = system["DependentVariables"];
icvars = vars /. T->0;

This defines a splitting Y1, Y2 of the system by setting some of the right-hand side components to zero:

Y1=eqs; Y1[[2,2]] = 0;Y1

Y2=eqs;Y2[[{1,3},2]] = 0; Y2

This defines the implicit midpoint method:

ImplicitMidpoint = {"ImplicitRungeKutta", "Coefficients"->"ImplicitRungeKuttaGaussCoefficients","DifferenceOrder"->2,"ImplicitSolver"->{"FixedPoint",AccuracyGoal->10, PrecisionGoal->10,"IterationSafetyFactor"->1}};

This constructs a second-order splitting method that retains volume and reversing symmetries:

ABCSplitting = {"Splitting",
"DifferenceOrder"->2,
"Equations"->{Y2,Y1,Y2},
"Method"->{"LocallyExact",ImplicitMidpoint,"LocallyExact"}};

This defines a function that gives the Poincaré section for a particular initial condition:

psect[ics_] := 
Module[{reapdata},
reapdata = 
Reap[
NDSolve[{eqs,Thread[icvars == ics]},{},{T,1000},
 Method->{"EventLocator",
"Event"->(Subscript[Y, 2])[T], "EventAction":> Sow[{(Subscript[Y, 1])[T], (Subscript[Y, 3])[T]}], 
"EventLocationMethod"->"LinearInterpolation",Method->ABCSplitting}, 
StartingStepSize->1/4,MaxSteps->∞]
];
reapdata[[-1,1]]
];

This finds the Poincaré sections for several different initial conditions and flattens them together into a single list of points:

data = 
Mod[Map[psect, {{4.267682454609692,0,0.9952906114885919},
{1.6790790859443243,0,2.1257099470901704},
{2.9189523719753327,0,4.939152797323216},
{3.1528896559036776,0,4.926744120488727},
{0.9829282640373566,0,1.7074633238173198},
{0.4090394012299985,0,4.170087631574883},
{6.090600411133905,0,2.3736566160602277},
{6.261716134007686,0,1.4987884558838156},
{1.005126683795467,0,1.3745418575363608},
{1.5880780704325377,0,1.3039536044289253},
{3.622408133554125,0,2.289597511313432},
{0.030948690635763183,0,4.306922133429981},
{5.906038850342371,0,5.000045498132029}}],
2π];
ListPlot[data,ImageSize->Medium]

Bouncing Ball

This example is a generalization of an example in [SGT03]. It models a ball bouncing down a ramp with a given profile. The example is good for demonstrating how you can use multiple invocations of (RefLink[NDSolve, paclet:ref/NDSolve]) with event location to model some behavior.

This defines a function that computes the solution from one bounce to the next. The solution is computed until the next time the path intersects the ramp:

OneBounce[k_, ramp_][{t0_,x0_, xp0_, y0_, yp0_}] := 
Module[{sol, t1, x1, xp1, y1, yp1, gramp, gp},
sol = First[NDSolve[
{x''[t] == 0, x'[t0]== xp0, x[t0] == x0,
y''[t] == -9.8 , y'[t0] == yp0, y[t0] == y0},
{x,y},
{t,t0, ∞}, Method->{"EventLocator", "Event":> y[t] - ramp[x[t]]},MaxStepSize->0.01]];
t1 = InterpolatingFunctionDomain[x /. sol][[1,-1]];
{x1,xp1,y1, yp1} = Reflection[k, ramp][{x[t1], x'[t1], y[t1], y'[t1]} /. sol];
Sow[{x[t] /.sol,t0 ≤ t ≤ t1}, "X"];
Sow[{ y[t] /. sol,t0 ≤ t ≤ t1}, "Y"];
Sow[{x1,y1}, "Bounces"];
{t1, x1,xp1,y1, yp1}]

This defines the function for the bounce when the ball hits the ramp. The formula is based on reflection about the normal to the ramp assuming only the fraction k of energy is left after a bounce:

Reflection[k_, ramp_][{x_, xp_, y_, yp_}] := Module[{gramp, gp, xpnew, ypnew},
gramp = -ramp'[x];
If[Not[NumberQ[gramp]],
Print["Could not compute derivative "];
Throw[$Failed]];
gramp = {-ramp'[x], 1};
If[ gramp.{xp,yp} == 0,
Print["No reflection"];
Throw[$Failed]];
gp= {1,-1} Reverse[gramp];
{xpnew, ypnew} = (k/(gramp.gramp)) (gp gp.{xp,yp} - gramp gramp.{xp, yp});
{x, xpnew, y, ypnew}]

This defines the function that runs the bouncing ball simulation for a given reflection ratio, ramp, and starting position:

BouncingBall[k_, ramp_,{x0_,y0_}] := 
Module[{data, end, bounces, xmin, xmax, ymin, ymax},
If[y0 < ramp[x0],
Print["Start above the ramp"];
Return[$Failed]];
data =Reap[
Catch[Sow[{x0,y0}, "Bounces"];
NestWhile[OneBounce[k, ramp], {0,x0,0,y0,0}, Function[1 - #1[[1]]/#2[[1]] > 0.01], 2, 25]],_, Rule];
end = data[[1,1]];
data = Last[data];
bounces = ("Bounces" /. data);
xmax = Max[bounces[[All,1]]];
xmin = Min[bounces[[All,1]]];
ymax = Max[bounces[[All,2]]];
ymin = Min[bounces[[All,2]]];
Show[{Plot[ramp[x],{x,xmin, xmax},PlotRange->{{xmin, xmax},{ymin, ymax}},Epilog->{PointSize[.025],Map[Point,bounces]}, AspectRatio->(ymax - ymin)/(xmax - xmin)], ParametricPlot[Evaluate[{Piecewise["X" /. data], Piecewise["Y" /. data]}], {t,0,end}, PlotStyle->RGBColor[1,0,0]]}]]

This is the example that is done in [SGT03]:

ramp[x_] := If[x < 1,1 - x, 0];
BouncingBall[.7, ramp,{0,1.25}]

The ramp is now defined to be a quarter circle:

circle[x_] := If[x < 1, Sqrt[1 - x^2], 0];
BouncingBall[.7, circle,{.1,1.25}]

This adds a slight waviness to the ramp:

wavyramp[x_] := If[x < 1, 1 - x + .05 Cos[11 Pi x] ,0];
BouncingBall[.75, wavyramp,{0,1.25}]

Event Direction

Ordinary Differential Equation

This example illustrates the solution of the restricted three-body problem, a standard nonstiff test system of four equations. The example traces the path of a spaceship traveling around the moon and returning to the earth (see p. 246 of [SG75]). The ability to specify multiple events and the direction of the zero crossing is important.

The initial conditions have been chosen to make the orbit periodic. The value of μ corresponds to a spaceship traveling around the moon and the earth:

μ=(1/82.45);
(SuperStar[μ])=1-μ;
(Subscript[r, 1])=(Sqrt[((((Subscript[y, 1])[t]+μ))^2)+(((Subscript[y, 2])[t])^2)]);
(Subscript[r, 2])=(Sqrt[((((Subscript[y, 1])[t]-(SuperStar[μ])))^2)+(((Subscript[y, 2])[t])^2)]);
eqns={{(Subscript[y, 1])'[t]==(Subscript[y, 3])[t],(Subscript[y, 1])[0]==1.2},{(Subscript[y, 2])'[t]==(Subscript[y, 4])[t],(Subscript[y, 2])[0]==0},{(Subscript[y, 3])'[t]==2 (Subscript[y, 4])[t]+(Subscript[y, 1])[t]-(((SuperStar[μ]) ((Subscript[y, 1])[t]+μ))/(Subscript[r, 1]^3))-((μ ((Subscript[y, 1])[t]-(SuperStar[μ])))/(Subscript[r, 2]^3)),(Subscript[y, 3])[0]==0},{(Subscript[y, 4])'[t]==-2 (Subscript[y, 3])[t]+(Subscript[y, 2])[t]-(((SuperStar[μ]) (Subscript[y, 2])[t])/(Subscript[r, 1]^3))-((μ (Subscript[y, 2])[t])/(Subscript[r, 2]^3)),(Subscript[y, 4])[0]==-1.04935750983031990726`20.020923474937767}};

The event function is the derivative of the distance from the initial conditions. A local maximum or minimum occurs when the value crosses zero:

ddist = 2((Subscript[y, 3])[t]((Subscript[y, 1])[t]-1.2)+(Subscript[y, 4])[t](Subscript[y, 2])[t]);

There are two events, which for this example are the same. The first event (with (RefLink[Direction, paclet:ref/Direction]) 1) corresponds to the point where the distance from the initial point is a local minimum, so that the spaceship returns to its original position. The event action is to store the time of the event in the variable tfinal and to stop the integration. The second event corresponds to a local maximum. The event action is to store the time that the spaceship is farthest from the starting position in the variable tfar:

sol = First[NDSolve[eqns,{(Subscript[y, 1]),(Subscript[y, 2]),(Subscript[y, 3]),(Subscript[y, 4])},{t,∞},
Method->{"EventLocator",
"Event"->{ddist, ddist},
"Direction"->{1, -1},
"EventAction":>{Throw[tfinal = t,"StopIntegration"],tfar = t}}]]

The first two solution components are coordinates of the body of infinitesimal mass, so plotting one against the other gives the orbit of the body.

This displays one half-orbit when the spaceship is at the furthest point from the initial position:

ParametricPlot[{(Subscript[y, 1])[t], (Subscript[y, 2])[t]} /. sol, {t,0,tfar}]

This displays one complete orbit when the spaceship returns to the initial position:

ParametricPlot[{(Subscript[y, 1])[t], (Subscript[y, 2])[t]} /. sol, {t,0,tfinal}]

Delay Differential Equation

The following system models an infectious disease (see [HNW93], [ST00], and [ST01]):

system = {y1'[t]==-y1[t] y2[t-1]+y2[t-10],y1[t/;t≤0]==5,y2'[t]==y1[t] y2[t-1]-y2[t],y2[t/;t≤0]==1/10,y3'[t]==y2[t]-y2[t-10],y3[t/;t≤0]==1};
vars = {y1[t],y2[t],y3[t]};

Collect the data for a local maximum of each component as the integration proceeds. A separate tag for (RefLink[Sow, paclet:ref/Sow]) and (RefLink[Reap, paclet:ref/Reap]) is used to distinguish the components:

data = 
Reap[
sol = First[NDSolve[system,vars,{t,0,40},
Method->{"EventLocator",
"Event":>{y1'[t],y2'[t],y3'[t]},
"EventAction":>{Sow[{t,y1[t]},1],Sow[{t,y2[t]},2],Sow[{t,y3[t]},3]},
"Direction"->{-1,-1,-1}}]],
{1,2,3}
];

Display the local maxima together with the solution components:

colors = {{Red},{Blue},{Green}};
plots = Plot[Evaluate[vars /. sol],{t,0,40},PlotStyle->colors];
max = ListPlot[Part[data,-1,All,1],PlotStyle->colors];
Show[plots,max]

Discontinuous Equations and Switching Functions

In many applications the function in a differential system may not be analytic or continuous everywhere.

A common discontinuous problem that arises in practice involves a switching function g:

In order to illustrate the difficulty in crossing a discontinuity, consider the following example [GØ84] (see also [HNW93]):

Here is the input for the entire system. The switching function is assigned to the symbol event, and the function defining the system depends on the sign of the switching function:

t0 = 0;
ics0 = (3/10);
event = (((t+(1/20)))^2)+(((y[t]+(3/20)))^2)-1;
system = {y'[t]==If[event<=0,(t^2)+2((y[t])^2),2(t^2)+3((y[t])^2)-2], y[t0]==ics0};

The symbol odemethod is used to indicate the numerical method that should be used for the integration. For comparison, you might want to define a different method, such as "ExplicitRungeKutta", and rerun the computations in this section to see how other methods behave:

odemethod = Automatic;

This solves the system on the interval [0, 1] and collects data for the mesh points of the integration using (RefLink[Reap, paclet:ref/Reap]) and (RefLink[Sow, paclet:ref/Sow]):

data = Reap[
sol =y[t] /. First[NDSolve[system,y,{t,t0,1},Method->odemethod, MaxStepFraction->1,StepMonitor:>Sow[t]]]
][[2,1]];
sol

Here is a plot of the solution:

dirsol = Plot[sol,{t,t0,1}]

Despite the fact that a solution has been obtained, it is not clear whether it has been obtained efficiently.

The following example shows that the crossing of the discontinuity presents difficulties for the numerical solver.

This defines a function that displays the mesh points of the integration together with the number of integration steps that are taken:

StepPlot[data_,opts___?OptionQ]:=
Module[{sdata},
sdata = Transpose[{data,Range[Length[data]]}];
ListPlot[sdata,opts,Axes->False,Frame->True,PlotRange->All]
];

As the integration passes the discontinuity (near 0.6 in value), the integration method runs into difficulty, and a large number of small steps are taken—a number of rejected steps can also sometimes be observed:

StepPlot[data]

One of the most efficient methods of crossing a discontinuity is to break the integration by restarting at the point of discontinuity.

The following example shows how to use the "EventLocator" method to accomplish this.

This numerically integrates the first part of the system up to the point of discontinuity. The switching function is given as the event. The direction of the event is restricted to a change from negative to positive. When the event is found, the solution and the time of the event are stored by the event action:

system1 = {y'[t]==(t^2)+2((y[t])^2),y[t0]==ics0};

data1 = Reap[sol1 = y[t] /. First[NDSolve[system1,y,{t,t0,1},Method->{"EventLocator","Event"->event,Direction->1,EventAction:>Throw[t1 = t; ics1 = y[t]; ,"StopIntegration"],Method->odemethod},MaxStepFraction->1,StepMonitor:>Sow[t]]]
][[2,1]];
sol1

Using the discontinuity found by the "EventLocator" method as a new initial condition, the integration can now be continued.

This defines a system and initial condition, solves the system numerically, and collects the data used for the mesh points:

system2 = {y'[t]==2(t^2)+3((y[t])^2)-2,y[t1]==ics1};

data2 = Reap[
sol2 = y[t] /. First[NDSolve[system2,y,{t,t1,1},Method->odemethod,MaxStepFraction->1,StepMonitor:>Sow[t]]]
][[2,1]];
sol2

A plot of the two solutions is very similar to that obtained by solving the entire system at once:

evsol = Plot[If[t ≤ t1,sol1,sol2],{t,0,1}]

Examining the mesh points, it is clear that far fewer steps were taken by the method and that the problematic behavior encountered near the discontinuity has been eliminated:

StepPlot[Join[data1,data2]]

The value of the discontinuity is given as 0.6234 in [HNW93], which coincides with the value found by the "EventLocator" method.

In this example it is possible to analytically solve the system and use a numerical method to check the value.

The solution of the system up to the discontinuity can be represented in terms of Bessel and gamma functions:

dsol = FullSimplify[First[DSolve[system1,y[t],t]]]

Substituting in the solution into the switching function, a local minimization confirms the value of the discontinuity:

FindRoot[event /. dsol,{t,3/5}]

Avoiding Wraparound in PDEs

Many evolution equations model behavior on a spatial domain that is infinite or sufficiently large to make it impractical to discretize the entire domain without using specialized discretization methods. In practice, it is often the case that it is possible to use a smaller computational domain for as long as the solution of interest remains localized.

In situations where the boundaries of the computational domain are imposed by practical considerations rather than the actual model being studied, it is possible to pick boundary conditions appropriately. Using a pseudospectral method with periodic boundary conditions can make it possible to increase the extent of the computational domain because of the superb resolution of the periodic pseudospectral approximation. The drawback of periodic boundary conditions is that signals that propagate past the boundary persist on the other side of the domain, affecting the solution through wraparound. It is possible to use an absorbing layer near the boundary to minimize these effects, but it is not always possible to completely eliminate them.

The sine-Gordon equation turns up in differential geometry and relativistic field theory. This example integrates the equation, starting with a localized initial condition that spreads out. The periodic pseudospectral method is used for the integration. Since no absorbing layer has been instituted near the boundaries, it is most appropriate to stop the integration once wraparound becomes significant. This condition is easily detected with event location using the "EventLocator" method.

The integration is stopped when the size of the solution at the periodic wraparound point crosses a threshold of 0.01, beyond which the form of the wave would be affected by periodicity:

Timing[sgsol = First[NDSolve[{(∂_(t,t))u[t,x] ==(∂_(x,x))u[t,x] - Sin[u[t,x]],
 u[0,x] == (E^(-(((x-5))^2)))+(E^(-(((x+5))^2)/2)), Derivative[1,0][u][0,x] == 0, u[t,-50] == u[t,50]}, u, {t,0,1000}, {x,-50,50}, Method->{"MethodOfLines",
"SpatialDiscretization"->{"TensorProductGrid", "DifferenceOrder"->"Pseudospectral"},Method->{"EventLocator", "Event":> Abs[u[t,-50]] - 0.01, "EventLocationMethod"->"StepBegin"}}]]]

This extracts the ending time from the (RefLink[InterpolatingFunction, paclet:ref/InterpolatingFunction]) object and makes a plot of the computed solution. You can see that the integration has been stopped just as the first waves begin to reach the boundary:

end = InterpolatingFunctionDomain[u /. sgsol][[1,-1]];
DensityPlot[u[t,x] /. sgsol,{x,-50,50}, {t,0, end}, Mesh->False, PlotPoints->100]

The "DiscretizedMonitorVariables" option affects the way the event is interpreted for PDEs; with the setting (RefLink[True, paclet:ref/True]), u[t,x] is replaced by a vector of discretized values. This is much more efficient because it avoids explicitly constructing the (RefLink[InterpolatingFunction, paclet:ref/InterpolatingFunction]) to evaluate the event:

Timing[sgsol = First[NDSolve[{(∂_(t,t))u[t,x] == (∂_(x,x))u[t,x] - Sin[u[t,x]],
 u[0,x] == (E^(-(((x-5))^2)))+(E^(-(((x+5))^2)/2)), Derivative[1,0][u][0,x] == 0, u[t,-50] == u[t,50]}, u, {t,0,1000}, {x,-50,50}, Method->{"MethodOfLines",
"DiscretizedMonitorVariables"->True, 
"SpatialDiscretization"->{"TensorProductGrid", "DifferenceOrder"->"Pseudospectral"},Method->{"EventLocator", "Event":> Abs[First[u[t,x]]] - 0.01, "EventLocationMethod"->"StepBegin"}}]]]

Performance Comparison

The following example constructs a table making a comparison for two different integration methods.

This defines a function that returns the time it takes to compute a solution of a mildly damped pendulum equation up to the point at which the bob has momentarily been at rest 1000 times:

EventLocatorTiming[locmethod_,odemethod_] := Block[{Second = 1, y, t, p = 0},
First[Timing[NDSolve[{y''[t] + (1/1000) y'[t] + Sin[y[t]] == 0, y[0] == 3, y'[0] == 0}, y, {t,∞}, Method->{"EventLocator", "Event"->y'[t], "EventAction":> If[p++ ≥ 1000, Throw[end = t, "StopIntegration"]],"EventLocationMethod"->locmethod, "Method"->odemethod},
MaxSteps->∞]]]
];

This uses the function to make a table comparing the different location methods for two different ODE integration methods:

elmethods = {"StepBegin","StepEnd", "LinearInterpolation", {"Brent","SolutionApproximation"->"CubicHermiteInterpolation"}, Automatic};
odemethods = {Automatic, "ExplicitRungeKutta"};
TableForm[Outer[EventLocatorTiming, elmethods, odemethods,1], TableHeadings->{elmethods, odemethods},TableSpacing->{1, .25}]

While simple step begin/end and linear interpolation location are essentially the same low cost, the better location methods are more expensive. The default location method is particularly expensive for the explicit Runge–Kutta method because it does not yet support a continuous output formula—it therefore needs to repeatedly invoke the method with different step sizes during the local minimization.

It is worth noting that, often, a significant part of the extra time for computing events arises from the need to evaluate the event functions at each time step to check for the possibility of a sign change.

TableForm[
{Map[
Block[{Second=1,y,t,p=0},
First[Timing[NDSolve[{y''[t]+(1/1000) y'[t]+Sin[y[t]]==0,y[0]==3,y'[0]==0},y,{t,end},Method->#,MaxSteps->∞]]]]&,
odemethods
]},
TableHeadings->{None,odemethods}]

An optimization is performed for event functions involving only the independent variable. Such events are detected automatically at initialization time. For example, this has the advantage that interpolation of the solution of the dependent variables is not carried out at each step of the local optimization search—it is deferred until the value of the independent variable has been found.

Limitations

One limitation of the event locator method is that since the event function is only checked for sign changes over a step interval, if the event function has multiple roots in a step interval, all or some of the events may be missed. This typically only happens when the solution to the ODE varies much more slowly than the event function. When you suspect that this may have occurred, the simplest solution is to decrease the maximum step size the method can take by using the (RefLink[MaxStepSize, paclet:ref/MaxStepSize]) option to (RefLink[NDSolve, paclet:ref/NDSolve]). More sophisticated approaches can be taken, but the best approach depends on what is being computed. An example follows that demonstrates the problem and shows two approaches for fixing it.

This should compute the number of positive integers less than (e^5) (there are 148). However, most are missed because the method is taking large time steps because the solution x[t] is so simple:

Block[{n = 0},NDSolve[{y'[t] == y[t], y[-1] == (E^(-1))}, y, {t,5},Method->{"EventLocator", "Event"-> Sin[π y[t]], "EventAction":>n++}];n]

This restricts the maximum step size so that all the events are found:

Block[{n = 0},NDSolve[{y'[t] == y[t], y[-1] == (E^(-1))}, y, {t,5},Method->{"EventLocator", "Event"-> Sin[π y[t]], "EventAction":>n++}, MaxStepSize->0.001];n]

It is quite apparent from the nature of the example problem that if the endpoint is increased, it is likely that a smaller maximum step size may be required. Taking very small steps everywhere is quite inefficient. It is possible to introduce an adaptive time step restriction by setting up a variable that varies on the same time scale as the event function.

This introduces an additional function to integrate: the event function. With this modification and allowing the method to take as many steps as needed, it is possible to find the correct value up to t==10 in a reasonable amount of time:

Block[{n = 0},NDSolve[{y'[t] == y[t], y[-1] == (E^(-1)), z'[t] == D[Sin[π y[t]],t], z[-1] == Sin[π (E^(-1))]}, {y,z}, {t,10},Method->{"EventLocator", "Event"-> z[t], "EventAction":>n++}, MaxSteps->∞];n]

Option Summary

"EventLocator" Options

"EventLocator" method options.

"EventLocationMethod" Options

Settings for the "EventLocationMethod" option.

"Brent" Options

Options for event location method "Brent".