There is a very nice answer on how to pass extra arguments to derivative evaluator in SciPy's solve_ivp
function using a lambda
expression. This approach is official way to deal with derivatives depending on extra arguments as indicated on SciPy's github issue tracker.
This approach seems to be working really well while run inside of Python script, however once run inside of Jupyter environment's Python kernel it gets some unpredictable behavior for which I fail to identify the cause.
Following example is based on an official example.
from scipy.integrate import solve_ivp
import numpy as np
def exponential_decay(t, y, alpha, beta): return -alpha*y + beta
for alpha in np.linspace(0.5, 0.7, 5) :
beta = 1.
sol = solve_ivp(lambda t, y: exponential_decay(t, y, alpha, beta), [0, 10], [2, 4, 8])
Once it is put inside of Jupyter notebook the local variable of scope inside of the for loop, alpha
is causing a NameError
. Once run again without the loop it is working well. I do restart the kernel before each time I run all cells to make sure environment has no defined globals. This error persists with both Python 2 and Python 3 kernels.
The details can be find in this public gist I prepared. The stack trace, as requested from the comments is the following:
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<timed exec> in <module>
~/.pyenv/versions/3.6.7/envs/jupyter3/lib/python3.6/site-packages/scipy/integrate/_ivp/ivp.py in solve_ivp(fun, t_span, y0, method, t_eval, dense_output, events, vectorized, **options)
454 method = METHODS[method]
455
--> 456 solver = method(fun, t0, y0, tf, vectorized=vectorized, **options)
457
458 if t_eval is None:
~/.pyenv/versions/3.6.7/envs/jupyter3/lib/python3.6/site-packages/scipy/integrate/_ivp/rk.py in __init__(self, fun, t0, y0, t_bound, max_step, rtol, atol, vectorized, first_step, **extraneous)
98 self.max_step = validate_max_step(max_step)
99 self.rtol, self.atol = validate_tol(rtol, atol, self.n)
--> 100 self.f = self.fun(self.t, self.y)
101 if first_step is None:
102 self.h_abs = select_initial_step(
~/.pyenv/versions/3.6.7/envs/jupyter3/lib/python3.6/site-packages/scipy/integrate/_ivp/base.py in fun(t, y)
137 def fun(t, y):
138 self.nfev += 1
--> 139 return self.fun_single(t, y)
140
141 self.fun = fun
~/.pyenv/versions/3.6.7/envs/jupyter3/lib/python3.6/site-packages/scipy/integrate/_ivp/base.py in fun_wrapped(t, y)
19
20 def fun_wrapped(t, y):
---> 21 return np.asarray(fun(t, y), dtype=dtype)
22
23 return fun_wrapped, y0
<timed exec> in <lambda>(t, y)
NameError: name 'alpha' is not defined
I'm not very experienced with Jupyter and I am out of ideas, so I'd like to ask you for some advices, here are some basic questions I have
- What is a possible thing I could check further? I did test for name errors inside of the loop using
try ... catch NameError : ...
approach and everything was well defined as expected, problem occurs once inside of the lambda function. - What are best practices to work with Jupyter notebooks to avoid problems with precomputed values of variable and their scope? I like writing simple scripts as they allow me to not worry about such things, however being able to comment sections of code with LaTeX is a huge advantage Jupyter brings which I also value a lot.
- I've been trying to research this problem a bit and maybe it isn't really the scope problem but more of a pointer / reference kind of problem (based on this answer), however if that is the case, why the problem occurs only in Jupyter notebook and not in simple Python script?
Any help, advices, examples will be appreciated. Thanks!