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.