Repository: scipy/scipy

Issue:
lsoda fails to detect stiff problem when called from solve_ivp
<!-- 
Thank you for taking the time to report a SciPy issue.
Please describe the issue in detail, and for bug reports
fill in the fields below. You can delete the sections that 
don't apply to your issue. You can view the final output
by clicking the preview button above.
-->

Both the `ode` and the `solve_ivp` solver interfaces wrap the venerable lsoda solver.  As far as I can see, `solve_ivp` even internally calls the `ode` wrapper.  Strangely enough, when tested on a stiff problem, lsoda when called through `ode` handles stiffness just fine.  However, when called through `solve_ivp`, it behaves as if using the non-stiff solver.  The only parameter difference I can spot is the reduced default `atol` and `rtol` parameter, however, even setting them to the `ode` defaults will not change the behavior of `solve_ivp`.  (Off-topic comment: I find the default tolerances for `solve_ivp` unnecessarily poor, especially given that any other blackbox solver I know, including the two older Scipy APIs, have stricter defaults.  I would strongly recommend using common defaults across Scipy, otherwise an already confusing situation is made more confusing and less robust.)

### Reproducing code example:
<!-- 
If you place your code between the triple backticks below, 
it will be marked as a code block automatically 
-->


```python
#!/usr/bin/python3

from pylab import *
from scipy.integrate import solve_ivp
from scipy.integrate import ode
import time
    
mu = 1000
y0 = r_[0.5, 0.5]
T = 500
tt = linspace(T/200,T,200)

def van_der_pol(t, y):
    return r_[y[1], mu*(1.0-y[0]**2)*y[1]-y[0]]

c1 = time.process_time()
sol1 = solve_ivp(van_der_pol, [0,T], y0, t_eval=tt,
                 method = 'LSODA', rtol=1e-6, atol=1e-12)

c2 = time.process_time()
r2 = ode(van_der_pol).set_integrator('lsoda')
r2.set_initial_value(y0)
sol2 = array([r2.integrate(T) for T in tt])

c3 = time.process_time() 
print ("Time for LSODA (solve_ivp): " + str(c2-c1))
print ("Time for lsoda (ode):       " + str(c3-c2))

figure()
plot(sol1.t, sol1.y[0,:], label='LSODA (solve_ivp)')
plot(tt, sol2[:,0], label='lsoda (ode)')
legend()
show()
```
This yields the following output (graphical output is qualitatively OK with both cases being very close):
<!-- If any, paste the *full* error message inside a code block
as above (starting from line Traceback)
-->

```
Time for LSODA (solve_ivp): 35.811249636000014
Time for lsoda (ode):       0.024488598000004913
```
So there is more than a factor 1000 performance difference, these numbers indicates that in the ill-performing case, `lsoda` has not switched to using a BDF method.

I tried to find any substantial differences in the code paths of the two wrappers, without success.  

### Scipy/Numpy/Python version information:
<!-- You can simply run the following and paste the result in a code block
```
import sys, scipy, numpy; print(scipy.__version__, numpy.__version__, sys.version_info)
```
-->
1.1.0 1.15.1 sys.version_info(major=3, minor=7, micro=2, releaselevel='final', serial=0)



Discussion:
Your `ode` example might have much smaller max time steps.  What happens if you use `max_step=T/200` in `solve_ivp`?  You can also check `nlu` and `njev` to assess whether it is switching to an implicit method.
I tested my suggestion and it makes no difference.  As a further wrinkle,, `odeint` performs nearly identically to `ode`.  There are **significantly more** jacobian evaluations in `solve_ivp` compared to `odeint`.  This would point me to some other factor causing this, but I'm not sure what.
 
```
from pylab import * 
...: from scipy.integrate import solve_ivp 
...: from scipy.integrate import ode, odeint 
...: import time 
...: mu = 1000 
...: y0 = r_[0.5, 0.5] 
...: T = 500 
...: tt = linspace(T/200,T,200) 
...: def van_der_pol(t, y): 
...:         return r_[y[1], mu*(1.0-y[0]**2)*y[1]-y[0]] 
...:          
...: c1 = time.process_time() 
...: sol1 = solve_ivp(van_der_pol, [0,T], y0, t_eval=tt, 
...:                  method = 'LSODA', rtol=1e-6, atol=1e-12) 
...: c2 = time.process_time() 
...: r2 = ode(van_der_pol).set_integrator('lsoda') 
...: r2.set_initial_value(y0) 
...: sol2 = array([r2.integrate(T) for T in tt]) 
...: c3 = time.process_time()  
...: sol3 = odeint(van_der_pol, y0, tt, tfirst=True, full_output=True) 
...: c4 = time.process_time() 
...: print ("Time for LSODA (solve_ivp): " + str(c2-c1)) 
...: print ("Time for lsoda (ode):       " + str(c3-c2)) 
...: print ("Time for lsoda (odeint):       " + str(c4-c3)) 
...:  
...: print ("solve_ivp jacobian evaluations: " + str(sol1.njev)) 
...: print ("odeint jacobian evaluations: " + str(sol3[1]['nje'][-1]))
```

```
Time for LSODA (solve_ivp): 101.24764199000003
Time for lsoda (ode):       0.07786113399993155
Time for lsoda (odeint):       0.08001487700005327

solve_ivp jacobian evaluations: 348567
odeint jacobian evaluations:    63
```
I noticed degraded performance on the LSODA as well. It [used to perform](https://github.com/drhagen/scipy_ode/blob/master/performance_tests/comparison.png) about as well `odeint` and `ode` with `lsoda` (because they are wrappers of the same thing). I have tracked the problem down to [this commit](https://github.com/drhagen/scipy_ode/commit/82019445feaf3ffa76cc90a478c982e322e19849). In particular, lsoda really does not [these lines ](https://github.com/scipy/scipy/blob/master/scipy/integrate/_ivp/lsoda.py#L128-L130)where the Jacobian passed in as `def jac(): None` rather than just `None`. This seems to tell it that the Jacobian cannot be computed (and therefore can only use the nonstiff solver) rather than it should compute the Jacobian itself with finite differences. @nmayorov Is it ok if these lines are reverted?
Kudos for running this down!  It works great.

Resolving patch:
diff --git a/scipy/integrate/_ivp/lsoda.py b/scipy/integrate/_ivp/lsoda.py
--- a/scipy/integrate/_ivp/lsoda.py
+++ b/scipy/integrate/_ivp/lsoda.py
@@ -125,10 +125,6 @@ def __init__(self, fun, t0, y0, t_bound, first_step=None, min_step=0.0,
 
         rtol, atol = validate_tol(rtol, atol, self.n)
 
-        if jac is None:  # No lambda as PEP8 insists.
-            def jac():
-                return None
-
         solver = ode(self.fun, jac)
         solver.set_integrator('lsoda', rtol=rtol, atol=atol, max_step=max_step,
                               min_step=min_step, first_step=first_step,
@@ -150,7 +146,7 @@ def _step_impl(self):
         itask = integrator.call_args[2]
         integrator.call_args[2] = 5
         solver._y, solver.t = integrator.run(
-            solver.f, solver.jac, solver._y, solver.t,
+            solver.f, solver.jac or (lambda: None), solver._y, solver.t,
             self.t_bound, solver.f_params, solver.jac_params)
         integrator.call_args[2] = itask
 


Test patch:
diff --git a/scipy/integrate/tests/test_ivp.py b/scipy/integrate/tests/test_ivp.py
--- a/scipy/integrate/tests/test_ivp.py
+++ b/scipy/integrate/tests/test_ivp.py
@@ -2,6 +2,7 @@
 from itertools import product
 from numpy.testing import (assert_, assert_allclose,
                            assert_equal, assert_no_warnings)
+import pytest
 from pytest import raises as assert_raises
 from scipy._lib._numpy_compat import suppress_warnings
 import numpy as np
@@ -296,6 +297,30 @@ def test_integration_const_jac():
         assert_allclose(res.sol(res.t), res.y, rtol=1e-14, atol=1e-14)
 
 
+@pytest.mark.slow
+@pytest.mark.parametrize('method', ['Radau', 'BDF', 'LSODA'])
+def test_integration_stiff(method):
+    rtol = 1e-6
+    atol = 1e-6
+    y0 = [1e4, 0, 0]
+    tspan = [0, 1e8]
+
+    def fun_robertson(t, state):
+        x, y, z = state
+        return [
+            -0.04 * x + 1e4 * y * z,
+            0.04 * x - 1e4 * y * z - 3e7 * y * y,
+            3e7 * y * y,
+        ]
+
+    res = solve_ivp(fun_robertson, tspan, y0, rtol=rtol,
+                    atol=atol, method=method)
+
+    # If the stiff mode is not activated correctly, these numbers will be much bigger
+    assert res.nfev < 5000
+    assert res.njev < 200
+
+
 def test_events():
     def event_rational_1(t, y):
         return y[0] - y[1] ** 0.7