events in scipy.integrate.solve_ivp do not behave as expected
See original GitHub issueIn 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:
- Created 5 years ago
- Comments:11 (4 by maintainers)
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.
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.