I’m currently trying to solve an engineering problem where I need to solve relatively long differential equations, for which I’m using the method odeint from scipy. I changed my problem to more easy variables and equations to shorten the code below and make it clearer.
I want to add two parts of a function, here f and g, to build my final equation dydt. Both parts have the same variables y,x and other different constant arguments.
I get the error “unsupported operand type(s) for +: ‘function’ and ‘function'”.
Is there a way around this?
I assume I could try to use lambda but I’m fairly new to python, and couldn’t figure a way to make it work.
from scipy.integrate import odeint
def f(y, x, arg1, arg2):
f_result = y + x + arg1 * arg2
return f_result
def g(y, x, arg3, arg4, arg5):
g_result = y * x * (arg3 + arg4 + arg5)
return g_result
def equation(fun1, fun2):
dydt = fun1 + fun2
return dydt
y0, x_span, arg1, arg2, arg3, arg4, arg5 = 0, [x for x in range(11)] , 1, 2, 3, 4, 5
f_fun = f
g_fun = g
dydt = equation(f_fun, g_fun)
sol = odeint(dydt, y0, x_span, args=(arg1, arg2, arg3, arg4, arg5))
Advertisement
Answer
You can build that kind of stuff, but it will not be very easy to maintain. Your problem here is that you want to return a function from a function. You can do this easily in python because you can define a function inside one, and return it.
Where it gets hard is that your “inner” functions will require different arguments every time, so managing to build dynamically another function that will have the right signature is… Well, as you will see, it’s not that hard, it will just not necessarily be the easiest to maintain.
A very raw way to do what you want is this:
from scipy.integrate import odeint
def f(y, x, arg1, arg2):
f_result = y + x + arg1 * arg2
return f_result
def g(y, x, arg3, arg4, arg5):
g_result = y * x * (arg3 + arg4 + arg5)
return g_result
def equation(fun1, fun2):
def inner(*args):
y = args[0]
x = args[1]
arg1 = args[2]
arg2 = args[3]
arg3 = args[4]
arg4 = args[5]
arg5 = args[6]
dydt = fun1(x, y, arg1, arg2) + fun2(x, y, arg3, arg4, arg5)
return dydt
return inner
y0, x_span, arg1, arg2, arg3, arg4, arg5 = 0, [x for x in range(11)], 1, 2, 3, 4, 5
f_fun = f
g_fun = g
dydt = equation(f_fun, g_fun)
sol = odeint(dydt, y0, x_span, args=(arg1, arg2, arg3, arg4, arg5))
inner takes *args as arguments, which means “any number of arguments, as a list”. As you can see i precised how to extract each value from it to rebuild our function.
Here, this is really raw and straightforward. If you want to actually make it a bit more permissive, you can try to guess the number of args your functions f and g require by inspecting them, so f and g can change a bit.
warning: using some relatively advanced techniques from now on.Mostly argument unpacking
from scipy.integrate import odeint
from inspect import signature
def f(y, x, arg1, arg2):
f_result = y + x + arg1 * arg2
return f_result
def g(y, x, arg3, arg4, arg5):
g_result = y * x * (arg3 + arg4 + arg5)
return g_result
def equation(fun1, fun2):
def inner(*args):
y = args[0]
x = args[1]
fun_args = (x for x in list(args[2:]))
fun1_args = [next(fun_args) for _ in range(len(signature(fun1).parameters) - 2)] # We remove 2 because y and x are always there
fun2_args = [next(fun_args) for _ in range(len(signature(fun2).parameters) - 2)]
dydt = fun1(x, y, *fun1_args) + fun2(x, y, *fun2_args)
return dydt
return inner
y0, x_span, arg1, arg2, arg3, arg4, arg5 = 0, [x for x in range(11)], 1, 2, 3, 4, 5
f_fun = f
g_fun = g
dydt = equation(f_fun, g_fun)
sol = odeint(dydt, y0, x_span, args=(arg1, arg2, arg3, arg4, arg5))
And for good measure, you could even decide to do an arbitrary number of functions, as long as you always put the parameters in the right order!
from scipy.integrate import odeint
from inspect import signature
def f(y, x, arg1, arg2):
f_result = y + x + arg1 * arg2
return f_result
def g(y, x, arg3, arg4, arg5):
g_result = y * x * (arg3 + arg4 + arg5)
return g_result
def equation(*args):
def inner(*inner_args):
y = inner_args[0]
x = inner_args[1]
dydt = []
fun_args = (x for x in list(inner_args[2:]))
for func in args:
func_args = [next(fun_args) for _ in range(len(signature(func).parameters) - 2)] # We remove 2 because y and x are always there
dydt.extend(func(x, y, *func_args))
return sum(dydt)
return inner
y0, x_span, arg1, arg2, arg3, arg4, arg5 = 0, [x for x in range(11)], 1, 2, 3, 4, 5
f_fun = f
g_fun = g
dydt = equation(f_fun, g_fun)
sol = odeint(dydt, y0, x_span, args=(arg1, arg2, arg3, arg4, arg5))
The real question now is, is it really more maintainable that way?
Here’s how i’d do it, personnally, allowing for some quick modifications witout it being too complicated.
def equation(y, x, *fun_args):
def f():
arg1, arg2 = *fun_args[0]
f_result = y + x + arg1 * arg2
return f_result
def g():
arg3, arg4, arg5 = *fun_args[1]
g_result = y * x * (arg3 + arg4 + arg5)
return g_result
return f() + g()
y0, x_span, arg1, arg2, arg3, arg4, arg5 = 0, [x for x in range(11)], 1, 2, 3, 4, 5
sol = odeint(equation, y0, x_span, args=((arg1, arg2), (arg3, arg4, arg5)))
This allows me only to play with the inner part of the “equation” function, as long as i pass the correct args in sol = odeint(equation, y0, x_span, args=((arg1, arg2), (arg3, arg4, arg5))). I also regrouped function parameters in tuple for better visiblity.