Skip to content
Advertisement

Adding functions y(x) together in python – impossible?

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.

8 People found this is helpful
Advertisement