solvers.py

from pylab import *
from copy import deepcopy

def euler_step(t, h, f, y):
    """Evaluates one step of the Euler method.  Takes the current value of the
    independent variable (t), the step size (h), a system of first-order
    differential equations (f), and the current values of the equations (y).
    Returns new values for t and y."""
    y = deepcopy(y) # to avoid changing the inputs
    for i in xrange(len(f)): # for each equation in the system
        # find the slope at the beginning of the interval
        k = f[i](t, y)
        # estimate the next point with the slope
        y[i] = y[i] + k * h
    return t + h, y

def RK4_step(t, h, f, y):
    """Evaluates one step of the fourth-order Runge-Kutta method.  Same parameters
    as euler_step, above."""
    y = deepcopy(y) # to avoid changing the inputs
    
    o = len(f)
    for i in xrange(o):
        k1 = []; k2 = []; k3 = []; k4 = []
    
    # find k terms
    for i in xrange(o):
        k1.append(h * f[i](t, y))
    for i in xrange(o):
        k2.append(h * f[i](t + h/2.0, y + array(k1)/2.0))
    for i in xrange(o):
        k3.append(h * f[i](t + h/2.0, y + array(k2)/2.0))
    for i in xrange(o):
        k4.append(h * f[i](t + h, y + array(k3)))

    # compute new values of y
    for i in xrange(o):
        y[i] = y[i] + (k1[i]/6.0 + k2[i]/3.0 + k3[i]/3.0 + k4[i]/6.0)

    return t+h, y
    
def solver(step_fn, f, f0, interval, h):
    """Generic numeric solver. step_fn is a function that evaluates one step of the
    method to be used, given the current value of the independent variable, the step
    size, a system of first order differential equations, and current values for
    each of the equations.  f is a system of first-order differential equations; f0
    are initial values for the functions in f, h is the step size, and interval is a
    tuple.  Returned values are t and y, where y are the values of each equation in
    the system at each step."""
    a, b = interval
    t = [a]
    y = map(lambda x: [x], f0)
    y_ = array(f0); t_ = a
    err_local = [0.0]

    for n in xrange(int((float(b) - float(a)) / h)):
        # evaluate as two steps for local error estimate
        t2, y2 = step_fn(t_, h/2.0, f, y_)
        t2, y2 = step_fn(t2, h/2.0, f, y2)
        
        # then actually take the step
        t_, y_ = step_fn(t_, h, f, y_)

        err_local.append(abs(y2[0] - y_[0]))

        for i in xrange(len(f)):
            y[i].append(y_[i])
        t.append(t_) 
    return t, y, err_local

def solver_adaptive(step_fn, f, f0, interval, err_target, err_max, h_initial, kp):
    a, b = interval
    t = [a]
    y = map(lambda x: [x], f0)
    y_ = array(f0); t_ = a
    err_local = [0.0]
    err_prev = 0.0
    err_int = 0.0
    h = h_initial
    stepsize = [h]
    steps_discarded = 0
    
    while t_ < b:
        while True:
            # evaluate as two steps for local error estimate
            t2, y2 = step_fn(t_, h/2.0, f, y_)
            t2, y2 = step_fn(t2, h/2.0, f, y2)
            
            # then actually take the step
            t1, y1 = step_fn(t_, h, f, y_)
            
            # compute the error, update the step size with a very simple control loop
            err = abs(y2[0] - y1[0])
            h += (err_target - err) * kp
            if err < err_max:
                break
            else:
                steps_discarded += 1

        t_ = t1
        y_ = y1

        stepsize.append(h)
        print "%f" % err

        err_local.append(err)

        for i in xrange(len(f)):
            y[i].append(y_[i])
        t.append(t_) 
    print "Steps discarded: %d" % steps_discarded
    return t, y, err_local, stepsize

if __name__ == "__main__":

    # Our system of differential equations
    f = (
        lambda t, y: y[1],         # y1' = y2
        lambda t, y: -1.0 * y[0]   # y2' = -y1
    )

    # Initial conditions
    f0 = (1.0, 0.0)

    # Step size
    h = 0.1

    # Interval
    interval = (0.0, 100.0 * pi)

    # Solve with Euler method and RK4:
    t, y, err_local_euler = solver(euler_step, f, f0, interval, h)
    t2, y2, err_local_rk4 = solver(RK4_step, f, f0, interval, h)
    t3, y3, err_local_adaptive, h_adaptive = solver_adaptive(RK4_step, f, f0, interval,
        0.00001, 0.00002, h, 750.0)

    #mean = lambda x: sum(x) / len(x)
    print "Average local error:"
    print " Euler: ",  mean(err_local_euler)
    print " RK4:   ", mean(err_local_rk4), max(err_local_rk4), len(err_local_rk4)
    print ( " RK4a:  ", mean(err_local_adaptive), max(err_local_adaptive),
        len(err_local_adaptive) )

    print "Average stepsize: ", mean(h_adaptive)

    # Evaluate the analytic solution for reference:
    xa = frange(interval[0], interval[1], 0.01)
    ya = cos(xa)
    
    # Plot!
    plot(xa, ya, t, y[0], t2, y2[0], t3, y3[0])
    legend(("Analytic", "Euler", "RK4", "Adaptive"))
    #plot(t3, h_adaptive)
    show()