question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

events in scipy.integrate.solve_ivp do not behave as expected

See original GitHub issue

In the following code, I integrate an ODE that should result in a cubic polynomial that has three real roots at x=[-6, -2, 2] on the interval (-8,4). I setup an event function to count these zeros, but it only counts the first one with the default settings.

import numpy as np
from scipy.integrate import solve_ivp

def fprime(x, y):
    return 3 * x**2 + 12 * x - 4

def event(x, y):
    return y

sol = solve_ivp(fprime, (-8, 4), np.array([-120]), events=[event])
sol

The output of this code is:

  message: 'The solver successfully reached the interval end.'
     nfev: 26
     njev: 0
      nlu: 0
      sol: None
   status: 0
  success: True
        t: array([-8.        , -7.89454203, -6.83996233,  3.70583468,  4.        ])
 t_events: [array([-6.])]
        y: array([[-120.        , -110.49687882,  -35.93785936,   94.46893375,  120.        ]])

where you can see from sol.y that there is only one sign change in the points computed by the solver. The solver, however, has skipped over the last two zeros. You can see that if you provide an array of x-values for t_eval, but that doesn’t change the list of events detected.

sol = solve_ivp(fprime, (-8, 4), np.array([-120]), events=[event], t_eval=np.linspace(-8, 4, 10))
sol.y

shows

array([[-120.        ,  -26.96296296,   16.2962963 ,   24.        ,
          10.37037037,  -10.37037037,  -24.        ,  -16.2962963 ,
          26.96296296,  120.        ]])

which has three sign changes corresponding to the three roots.

What does affect the number of events detected is if you set max_step to a small number (e.g. max_step=0.1) to limit the step size taken by the solver. Then, the events are correctly detected.

sol = solve_ivp(fprime, (-8, 4), np.array([-120]), events=[event], max_step=0.1)
sol.t_events

results in:

/Users/jkitchin/anaconda/lib/python3.6/site-packages/scipy/integrate/_ivp/rk.py:145: RuntimeWarning: divide by zero encountered in double_scalars
  max(1, SAFETY * error_norm ** (-1 / (order + 1))))

[array([-6., -2.,  2.])]

This is an unexpected behavior in my opinion, I would expect the same number of roots to occur in each case. It seems to happen because taking a solver step is independent of checking for events, so the solver is able to take a step that goes through one or more events invisibly to the event detection code.

I am not sure what the best solution is. Perhaps this behavior could be documented in the docstring at least. If it is not possible to get event detection in the solver, then maybe it should be done on the t_eval solution. That would at least be consistent with the solution you can graph.

Finally, the sol object only returns t_events. How would one get the value of the solution at these points?

Issue Analytics

  • State:open
  • Created 5 years ago
  • Comments:11 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
jkitchincommented, Sep 8, 2020

I think this is a pathological example that I think you cannot in general guarantee getting the right answer without limiting the step size. The issue is that it takes three steps which have a derivative of 0, and then a large step a region where the derivative is still zero, so it appears the solution is complete. You get the right answer with max_step because it avoids taking too large a step. You don’t know in advance what an appropriate step size here is, it depends on where the ramp starts and ends. In this specific example you need a max_step not greater than 6.3 to get the right answer, but that is specific to the x=2…7 ramp range.

The only thing you can do is verify your solution satisfies the ODE, e.g. by evaluating it on a fine grid, and using finite differences to compute the terms. That would show that this is not a solution.

1reaction
jkitchincommented, Jan 28, 2019

having dense_output have a default of True is fine, but it only makes visual detection of the problem easier. It doesn’t change the fact that you miss the roots unless you change the max_step size.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Unexpected behavior using scipy.integrate's solve_ivp
It seems that the integrator jumps over the interval where the derivative is not zero. This can happen as the internal step size...
Read more >
scipy.integrate.solve_ivp — SciPy v1.9.3 Manual
Each event occurs at the zeros of a continuous function of time and state. Each function must have the signature event(t, y) and...
Read more >
16. Numerical Methods using Python (scipy) - Hans Fangohr
The solver looks for a sign change over each step, so if multiple zero crossings occur within one step, events may be missed....
Read more >
First-order differential equations - part 2
To do this, we should compute the derivative on an array of regularly ... from scipy.integrate import solve_ivp sol = solve_ivp(yprime, (-1, ...
Read more >
scipy-ode.pdf - Zenodo
only ever use solve_ivp. solve_ivp is intended to replace odeint in. SciPy as ... the integration one step at a time if specialized...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found