Search code examples
pythonscipy

Implementing a lambda function inside scipy's solve_ivp() results in too many values to unpack


from scipy.integrate import solve_ivp

def myfunc(a,b,c,d,e,f,g,h):
  t=a+2
  d=b+3
  return t,d

a=1
b=2
c=3
d=4
e=5
f=6
g=7
e=8

solve_ivp(lambda t,s:myfunc(a,b,c,d,e,f,g,h),np.linspace(0,100),3)

This is the type of problem I'm facing. Scipy's solve_ivp() solves a function with the syntax solve_ivp(function,timerange,initial guess,**kwargs).

I have a function (say myfunc()) that takes in a large amount of arguments, does some mathematical manipulation on them, and returns 2 variables. I've included a sample here. I want to make this function the reference for solve_ivp(). I've done it with lambda but I'm running into a too many values to unpack (expected 2) error.

How do I resolve this?


Solution

  • This is another case where I should have just asked for the full traceback, rather than making a quick guess as to the problem (see my comment).

    Running your code:

       ...: integrate.solve_ivp(lambda t,s:myfunc(a,b,c,d,e,f,g,h),np.linspace(0,100),3)
    ---------------------------------------------------------------------------
    ValueError                                Traceback (most recent call last)
    Cell In[9], line 15
         12 g=7
         13 e=8
    ---> 15 integrate.solve_ivp(lambda t,s:myfunc(a,b,c,d,e,f,g,h),np.linspace(0,100),3)
    
    File ~/.local/lib/python3.10/site-packages/scipy/integrate/_ivp/ivp.py:524, in solve_ivp(fun, t_span, y0, method, t_eval, dense_output, events, vectorized, args, **options)
        519 if method not in METHODS and not (
        520         inspect.isclass(method) and issubclass(method, OdeSolver)):
        521     raise ValueError("`method` must be one of {} or OdeSolver class."
        522                      .format(METHODS))
    --> 524 t0, tf = map(float, t_span)
        526 if args is not None:
        527     # Wrap the user's fun (and jac, if given) in lambdas to hide the
        528     # additional parameters.  Pass in the original fun as a keyword
        529     # argument to keep it in the scope of the lambda.
        530     try:
    
    ValueError: too many values to unpack (expected 2)
    

    This is an unpacking error, in the

    t0, tf = map(float, t_span)
    

    This trying to get 2 float values the the t_span argument, and them in 2 variables, t0,tf. Instead you supply a arange(100)!

    Running it instead with (0,100):

    In [12]: integrate.solve_ivp(lambda t,s:myfunc(a,b,c,d,e,f,g,h),(0,100),3)
    ---------------------------------------------------------------------------
    ValueError                                Traceback (most recent call last)
    Cell In[12], line 1
    ----> 1 integrate.solve_ivp(lambda t,s:myfunc(a,b,c,d,e,f,g,h),(0,100),3)
    
    File ~/.local/lib/python3.10/site-packages/scipy/integrate/_ivp/ivp.py:568, in solve_ivp(fun, t_span, y0, method, t_eval, dense_output, events, vectorized, args, **options)
        565 if method in METHODS:
        566     method = METHODS[method]
    --> 568 solver = method(fun, t0, y0, tf, vectorized=vectorized, **options)
        570 if t_eval is None:
        571     ts = [t0]
    
    File ~/.local/lib/python3.10/site-packages/scipy/integrate/_ivp/rk.py:89, in RungeKutta.__init__(self, fun, t0, y0, t_bound, max_step, rtol, atol, vectorized, first_step, **extraneous)
         85 def __init__(self, fun, t0, y0, t_bound, max_step=np.inf,
         86              rtol=1e-3, atol=1e-6, vectorized=False,
         87              first_step=None, **extraneous):
         88     warn_extraneous(extraneous)
    ---> 89     super().__init__(fun, t0, y0, t_bound, vectorized,
         90                      support_complex=True)
         91     self.y_old = None
         92     self.max_step = validate_max_step(max_step)
    
    File ~/.local/lib/python3.10/site-packages/scipy/integrate/_ivp/base.py:135, in OdeSolver.__init__(self, fun, t0, y0, t_bound, vectorized, support_complex)
        133 self.t_old = None
        134 self.t = t0
    --> 135 self._fun, self.y = check_arguments(fun, y0, support_complex)
        136 self.t_bound = t_bound
        137 self.vectorized = vectorized
    
    File ~/.local/lib/python3.10/site-packages/scipy/integrate/_ivp/base.py:17, in check_arguments(fun, y0, support_complex)
         14 y0 = y0.astype(dtype, copy=False)
         16 if y0.ndim != 1:
    ---> 17     raise ValueError("`y0` must be 1-dimensional.")
         19 if not np.isfinite(y0).all():
         20     raise ValueError("All components of the initial state `y0` must be finite.")
    
    ValueError: `y0` must be 1-dimensional.
    

    So now it doesn't like the scalar 3 for y0. Change that to a list [3] (or equivalent array):

    In [13]: integrate.solve_ivp(lambda t,s:myfunc(a,b,c,d,e,f,g,h),(0,100),[3])
    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    Cell In[13], line 1
    ----> 1 integrate.solve_ivp(lambda t,s:myfunc(a,b,c,d,e,f,g,h),(0,100),[3])
    
    File ~/.local/lib/python3.10/site-packages/scipy/integrate/_ivp/ivp.py:568, in solve_ivp(fun, t_span, y0, method, t_eval, dense_output, events, vectorized, args, **options)
        565 if method in METHODS:
        566     method = METHODS[method]
    --> 568 solver = method(fun, t0, y0, tf, vectorized=vectorized, **options)
        570 if t_eval is None:
        571     ts = [t0]
    
    File ~/.local/lib/python3.10/site-packages/scipy/integrate/_ivp/rk.py:94, in RungeKutta.__init__(self, fun, t0, y0, t_bound, max_step, rtol, atol, vectorized, first_step, **extraneous)
         92 self.max_step = validate_max_step(max_step)
         93 self.rtol, self.atol = validate_tol(rtol, atol, self.n)
    ---> 94 self.f = self.fun(self.t, self.y)
         95 if first_step is None:
         96     self.h_abs = select_initial_step(
         97         self.fun, self.t, self.y, self.f, self.direction,
         98         self.error_estimator_order, self.rtol, self.atol)
    
    File ~/.local/lib/python3.10/site-packages/scipy/integrate/_ivp/base.py:154, in OdeSolver.__init__.<locals>.fun(t, y)
        152 def fun(t, y):
        153     self.nfev += 1
    --> 154     return self.fun_single(t, y)
    
    File ~/.local/lib/python3.10/site-packages/scipy/integrate/_ivp/base.py:23, in check_arguments.<locals>.fun_wrapped(t, y)
         22 def fun_wrapped(t, y):
    ---> 23     return np.asarray(fun(t, y), dtype=dtype)
    
    Cell In[13], line 1, in <lambda>(t, s)
    ----> 1 integrate.solve_ivp(lambda t,s:myfunc(a,b,c,d,e,f,g,h),(0,100),[3])
    
    NameError: name 'h' is not defined
    

    Now we have a missing variable, h. Where's that supposed to come from?

    Correct the global variable assignments:

    In [17]: e=5; h=8
    In [18]: integrate.solve_ivp(lambda t,s:myfunc(a,b,c,d,e,f,g,h),(0,100),[3,3])
    Out[18]: 
      message: The solver successfully reached the end of the integration interval.
      success: True
       status: 0
            t: [ 0.000e+00  9.384e-02  1.032e+00  1.042e+01  1.000e+02]
            y: [[ 3.000e+00  3.282e+00  6.097e+00  3.425e+01  3.030e+02]
                [ 3.000e+00  3.469e+00  8.161e+00  5.508e+01  5.030e+02]]
          sol: None
     t_events: None
     y_events: None
         nfev: 26
         njev: 0
          nlu: 0
    

    Now it runs, though I'm not sure what good that is. The function always returns (a+2,b+3), i.e. (3,5). Which you integrate over the range (0,100), so the returned y is the initial value + 100*(3,5), ie. (303, 503):

    In [20]: 3+_18['t']*3
    Out[20]: 
    array([  3.        ,   3.28153316,   6.09686481,  34.25018124,
           303.        ])
    In [21]: 3+_18['t']*5
    Out[21]: 
    array([  3.        ,   3.46922194,   8.16144135,  55.0836354 ,
           503.        ])
    

    Your function returns 2 values, a function of globals a and b. So y0 also needs to be shape (2). solve_ivp will a scalar t, and the 2 element y value, though your lambda throws those away. I tried to use lambda t,s: my_func(t,s,a,b,...), but kept having problems with it trying to return (t,[s+(2,3)]), a 'ragged array'.

    I could use

    integrate.solve_ivp(lambda t,s:myfunc(a,b,t,s,c,d,e,f),(0,100),[0,0])

    putting t,s in slots where myfunc will ignore them.