Search code examples
pythonpython-3.xlambdaodenested-function

How does the inner lambda use the outer lambda fuction in this RK4 ODE-solver? Python


I found the following code in my search to improve my codes for numerical analysis:

from math import sqrt

def RK4(f):
    return lambda t, y, dt: (
        lambda dy1: (
            lambda dy2: (
                lambda dy3: (
                    lambda dy4: (dy1 + 2 * dy2 + 2 * dy3 + dy4) / 6
                )(dt * f(t + dt, y + dy3))
            )(dt * f(t + dt / 2, y + dy2 / 2))
        )(dt * f(t + dt / 2, y + dy1 / 2))
    )(dt * f(t, y))

def theory(t): 
    return (t**2 + 4)**2 / 16

dy = RK4(lambda t, y: t * sqrt(y))

t, y, dt = 0., 1., .1
while t <= 10:
    if abs(round(t) - t) < 1e-5:
        print("y(%2.1f)\t= %4.6f \t error: %4.6g" % (t, y, abs(y - theory(t))))
    t, y = t + dt, y + dy(t, y, dt)

Now, the program works fine and can see the overall algorithm of the program. But i can't understand how does the lambda nesting works. If i understand correctly all the dyN in RK4 are functions that are defined inside as they are needed for this ODE-solving method. But i don't know how the inner lambdas use the outer lambdas in their definition. Perhaps I'm not understanding the lambda syntax correctly.


Solution

  • What happens here is that lambda functions are defined and immediately called. First, realise that the following is just a complicated way of calculating the square of y:

    ( lambda x:
        x**2
    )( y )
    

    The nesting in turn passes wraps another lambda around such a construction to modify its argument. Again, the following would be a complicated way to calculate the square of the sine of w, i.e., (math.sin(w))**2

    ( lambda z: (
            lambda x: x**2
        )(math.sin(z))
    ) (w)
    

    So, the way to read this is to apply the steps from bottom to top to each other.

    Writing the lambda-construction from your example with normal functions and without nested calls, it becomes:

    def step_5(f,dy1,dy2,dy3,dy4):
        return (dy1 + 2 * dy2 + 2 * dy3 + dy4) / 6
    
    def step_4(f,dy1,dy2,dy3):
        return step_5( dy1,dy2,dy3, dt * f(t + dt, y + dy3) )
    
    def step_3(f,dy1,dy2):
        return step_4( dy1,dy2, dt * f(t + dt / 2, y + dy2 / 2) )
    
    def step_2(f,dy1):
        return step_3( dy1, dt * f(t + dt / 2, y + dy1 / 2))
    
    def RK4(f):
        return step_2( dt * f(t, y) )
    

    From there, it is only a short step to a readably written Runge–Kutta method.