8

I want to check at what time the derivative of a function converges to zero. If the function starts far from equilibrium, the event is detected correctly at t=7.55822:

NDSolveValue[{x'[t] == -.9 x[t], x[0] == 1, 
   WhenEvent[Abs[x'[t]] <= 10^-3, Echo@{"FOUND at", t}; "StopIntegration"]
   }, x, {t, 0, 20}] // ListLinePlot

(* {FOUND at,7.55822} *)

enter image description here

However, I have no means to check generally within WhenEvent whether the test is initially true. Consequently, no event is triggered in the next example. Note, that I have tried to add a composite condition, dependent on time as well.

NDSolveValue[{x'[t] == 0, x[0] == 1, 
   WhenEvent[Abs[x'[t]] <= 10^-3 && t >= 0, Echo@{"FOUND at", t}; "StopIntegration"]
   }, x, {t, 0, 20}] // ListLinePlot

enter image description here

Moreover, if I change the order of conditions from Abs[x'[t]] <= 10^-3 && t >= 0 to t >= 0 && Abs[x'[t]] <= 10^-3, I get the following error messages:

NDSolveValue::nbnum1: "The function value Abs[0&]<=1/1000
is not True or False when the arguments are {0.,1.}."

How can one generally check that the condition is true initially when no crossing is detected by the integrator?

István Zachar
  • 47,032
  • 20
  • 143
  • 291
  • 2
    The design of WhenEvent[…&&…, …] is confusing. Somewhat related: https://mathematica.stackexchange.com/questions/211090/whenevent-fails-in-ndsolve/211091#comment541212_211091 I think the nbnum1 is a bug BTW. – xzczd Aug 19 '22 at 12:30
  • 2
    Related: https://mathematica.stackexchange.com/q/123550/1871 – xzczd Aug 19 '22 at 12:56
  • 1
    Can't you test that the condition is true initially "initially", that is, before you call NDSolve? – Michael E2 Aug 19 '22 at 13:37
  • @MichaelE2 I guess I can. However, I expected NDSolve to do it initially, and was surprised to see it otherwise. – István Zachar Aug 19 '22 at 13:44
  • 1
    Backing up the initial time worked in my situation (@xzczd's link), but not here. Perhaps the issue is that it's not an event, since Abs[x'[t] never changes. You might need to just test it manually before you start NDSolve. – Chris K Aug 19 '22 at 13:46
  • 1
    NDSolveValue[{x'[t] == -0.9 x[t], x[-$MachineEpsilon] == 1, WhenEvent[Abs[x'[t]] <= 10^-3, Echo@{"FOUND at", t, " x[t]=", x[t], " x'[t]=", x'[t]}; "StopIntegration"]}, x, {t, 0, 20}] reports x'[t]=-0.001, whereas NDSolveValue[{x'[t] == 0, x[-$MachineEpsilon] == 1, WhenEvent[t == 0, Echo@{"FOUND at", t, " x[t]=", x[t], " x'[t]=", x'[t]}; "StopIntegration"]}, x, {t, 0, 20}] reports x'[t]=x'[0.]. So the derivative doesn't seem to be properly set when the event is triggered by t compared to when it is triggered by x'[t]. The bug? – Chris K Aug 19 '22 at 14:02
  • @ChrisK Only state variables have their values substituted; use Method -> {"EquationSimplification" -> "Residual"} and you get the value of x'[t]. Mentioned in my answer below. – Michael E2 Aug 19 '22 at 15:40
  • @MichaelE2 I see, although x'[t] does evaluate in my first example! – Chris K Aug 19 '22 at 16:02
  • @ChrisK Oh bother. It's apparently more complicated (and hidden) than I thought. – Michael E2 Aug 19 '22 at 16:07
  • @ChrisK If I change the event Abs[x'[t]] <= 10^-3 in your first example to Abs[x[t]] <= 10^-3, then x'[t] is not evaluated. Perhaps it's whether x'[t] appears in the event (or more precisely evt in my answer) that determines whether the value of x'[t] will be substituted or not. I'll look into it later. – Michael E2 Aug 19 '22 at 16:11

2 Answers2

8

Update: Clarified when an event may depend on the highest-order derivative. Thanks to @Chris K for pointing out an exception to the original characterization.

I think the practical answer to the main question is to test whether the condition is true initially "initially", that is, before one calls NDSolve[].

However, I would like to respond to the observation in the OP of the effect of changing the order in And[] of the event. Maybe there's a better place to post this way to inspect an event as it is constructed by NDSolve[], but I didn't find one. I know the question of changing the order has come up before, though.

An event of the form evt && cond is parse in a special way. The first argument is processed as an event, that is, equations and inequalities are converted to a numeric function whose sign change in a step from t == t1 to t == t2 indicates an event has occurred. The second argument represent a boolean condition: if true at an event, then the event action is taken. It appears there is a restriction on the condition. It may depend only the highest-order derivative if either the derivative appears in evt or the option setting Method -> {"EquationSimplification" -> "Residual"} is used. (Normally in an explicit ODE $Y' = F(t, Y)$, the derivatives $Y'$ are not considered "state variables." In an implicit ODE $F(t,Y,Y')=0$, which is what "EquationSimplification" -> "Residual" enforces, the derivatives $Y'$ treated as state variables. At least that's how I keep the difference in mind, plus maybe it's more efficient to have events depend on fewer variables.)

If the form of the event is not of the form evt && cond, or if evt is not of one of the forms given in the table of example event forms in the documentation for WhenEvent[], then the whole event is treated as a boolean function and an event is indicated by its changing from False to True (only).

{state1} = NDSolve`ProcessEquations[{x'[t] == 0, x[0] == 1,
    WhenEvent[Abs[x'[t]] == 10^-3 && t >= 0 , Echo@{"FOUND at", t}; 
     "StopIntegration"]},
   x, {t, 0, 20}];

{state2} = NDSolve`ProcessEquations[{x'[t] == 0, x[0] == 1, WhenEvent[t >= 0 && Abs[x'[t]] <= 10^-3, Echo@{"FOUND at", t}; "StopIntegration"]}, x, {t, 0, 20}];

{state3} = NDSolve`ProcessEquations[{x'[t] == 0, x[0] == 1, WhenEvent[t >= 0 && Abs[x'[t]] <= 10^-3, Echo@{"FOUND at", t}; "StopIntegration"]}, x, {t, 0, 20}, Method -> {"EquationSimplification" -> "Residual"}];

The state data object has two lists of NDSolve`EventData[], one for the backward integration direction and one for the forward one. Initially they are the same, so we'll examine just one for each state.

state1["Events"]
SameQ @@ %
evt1 = state1["Events"][[2, 1]];
evt2 = state2["Events"][[2, 1]];
evt3 = state3["Events"][[2, 1]];

The following shows the structure of the NDSolve`EventData[] for each ODE, at least as much as I have been able to discover. Note the difference in the condition in the 2nd and 3rd ODEs. Depending on the method of equation simplification, in the condition, the x'[t] is replaced by a numerical function argument or left as constant parameter of the numerical function. Note also that the evt, namely, t >= 0, has be simplified to True (presumably because the interval of integration is 0 <= t <= 20}).

(* formatters *)
headers = {"{?,?,?,?,Priority,var}", "symbolic event", "evt (NF)", 
   "condition (NF)", "?", "DetectionMethod", "?", "LocationMethod", 
   "?", "?"};
gridform = 
  Grid[Transpose@{headers, #}, Alignment -> {{Center, Left}}(*,
    Dividers->All*), 
    Background -> {None, {{GrayLevel[0.95], GrayLevel[0.99]}}}] &;

List @@ evt1 // gridform

Mathematica graphics


List @@ evt2 // gridform

Mathematica graphics


List @@ evt3 // gridform

Mathematica graphics


Thanks to @xzczd for these references to related Q&A:

Michael E2
  • 235,386
  • 17
  • 334
  • 747
  • 1
    "I didn't find one. I know the question of changing the order has come up before, though." Perhaps you're looking for these :) : https://mathematica.stackexchange.com/q/39374/1871 https://mathematica.stackexchange.com/q/154539/1871 – xzczd Aug 19 '22 at 15:21
  • 1
    Always appreciate your deep dives into Mathematica internals :) – Chris K Aug 19 '22 at 16:16
  • Where did you learn about state1["Events"]? It doesn't show up in state1["Properties"]. Are any other unlisted parts available? – Chris K Aug 21 '22 at 01:27
  • 1
    @ChrisK state1@"Methods" -- for some reason, some objects in Mma have both "Properties" and "Methods" and they're not the same, although they usually overlap somewhat. – Michael E2 Aug 21 '22 at 02:22
  • This analysis is very informative, thank you @MichaelE2. However, the "testing initially" part is left for the reader, assuming that it is trivial. It is not, if one wants to do it programmatically. Please see my solution. – István Zachar Aug 29 '22 at 09:07
  • @IstvánZachar I don't quite understand. Your solution shows it can be done rather easily. I've done something similar, but with functions, since they're designed for re-use. For instance, test[xp_, yp_] := Abs[xp] + Abs[yp] <= 10^-3 and NDSolveValue[{eq, init, WhenEvent[test[x'[t], y'[t]], Echo@{"FOUND at", t}; "StopIntegration"]}, {x, y}, {t, 0, 100}]. Of course, if you have to solve for the highest-order derivative, you have to do it; but that's easy if the system can be put in the form $X' = F{t, X}$. If multiple solutions or can't be solved for $X'$, then it might be nontrivial. – Michael E2 Aug 29 '22 at 12:02
  • @IstvánZachar I guess I might add I don't see my suggestion as "assuming that it is trivial." Paraphrasing Nathan Milstein about playing the violin, either you can evaluate the test before NDSolve or you cannot. I would call the first case "feasible," not "trivial." My initial comment was to assess the feasibility of the first case, and judging by the lack of counterexamples, it seemed to pass that assessment. If the test is impossible to carry out, then that seems an insurmountable obstruction whenever the test is tried, whether before or during NDSolve. The nontrivial case seems... – Michael E2 Aug 30 '22 at 17:44
  • ...to me to be when the system has multiple solutions: Then one has to match multiple tests with the solutions, one-to-one, each test with the corresponding integration. I have an idea for doing that within NDSolve, but it will take some programming to carry out. Unfortunately, I just got Covid and what extra time I have has gone away for now. I hope to return to it. Let me know if you think it would be important to you. – Michael E2 Aug 30 '22 at 17:44
2

This post is to answer the first question, complementing Micheal E2's answer: How to test initially, outside of NDSolve, if an event condition is true?

I think my solution is not very general, and there is probably better way. Set up a 2D dynamical system with an event condition test that is defined outside of WhenEvent. It tests if the sum of derivatives is close to zero:

eq = {x'[t] == x[t] - x[t]^2, y'[t] == y[t] - x[t] y[t]};
init = {x[0] == .01, y[0] == .02};
test = Abs[x'[t]] + Abs[y'[t]] <= 10^-3;

event = WhenEvent["test", Echo@{"FOUND at", t}; "StopIntegration"] /. "test" -> test; ListLinePlot@NDSolveValue[{eq, init, event}, {x, y}, {t, 0, 100}]

(* {FOUND at,12.6008} *)

enter image description here

The event is correctly detected, but note how test had to be manually injected into WhenEvent due to its HoldAll property.

Let's start the system from equilibrium, and perform the initial testing. Note, that WhenEvent does not fire during NDSolve, but the initial test returns True, correctly capturing the equilibrium (in this case there might not be need to numerically integrate the system at all).

init = {x[0] == 1, y[0] == 2};
test /. Rule @@@ eq /. t -> 0 /. Rule @@@ init
NDSolveValue[{eq, init, event}, {x, y}, {t, 0, 100}] // ListLinePlot

(* True *)

enter image description here

My issue with my approach is that it relies on multiple factors that would be unnecessary had NDSolve done a precheck:

  1. The test that must be defined outside of WhenEvent so that it can be used both in the pre-test and by NDSolve.
  2. Dynamical equations and initial conditions must be provided separately for the replacement to work.
  3. Non-well-formed equations (e.g. where the derivative is on the right side) may not be as easily converted to rules for pre-testing.

I find this solution far from elegant, and probably prone to not work in corner cases, but is a proof of principle. Is there a better way?

István Zachar
  • 47,032
  • 20
  • 143
  • 291