Lecture 10: Linear Multistep Methods (LMMs) 2nd-order Adams-Bashforth Method The approximation for the 2nd-order Adams-Bashforth method is given by equation (10.10) in the lecture note for week 10, as follows y i+1 y i + 3 2 f(x i, y i )h 1 2 f(x i 1, y i 1 )h. (1) Since the computation of y i+1 can be performed directly using known quantities of previous points, i.e., y i and y i 1, this method is referred to as being explicit. Because explicit methods are known for being conditionally stable, the above formulation shall be used as a predictor in a predictor-corrector mechanism for the 2nd-order Adams-Bashforth method. We rewrite equation (1), and use it in a corrector approximation, P i+1 y i + 3 2 f(x i, y i )h 1 2 f(x i 1, y i 1 )h, (2) y i+1 y i + 1 2 h (f(x i+1, P i+1 + f(x i, y i )). (3) To implement in Python, we first calculate the predictor in equation (2). At initial point of calculation, this formula requires initial values at two different points. Because the initial calculation must start at i = 1, to calculate P 2 we need initial values y 1 and y 0. To get these initial values, we must invoke another solver that we may choose from the solvers we have done previously. Let us use the 4th-order Runge-Kutta solver, def RungeKutta4thOrder ( func, yinit, xspan, h): m = len ( yinit ) n = int (( xspan [-1] - xspan [0]) / h) x = xspan [0] y = yinit 1
xsol = np. empty ((0)) ysol = np. empty ((0)) ysol = np. append (ysol, y) for i in range (n): k1 = feval (func, x, y) k2 = feval (func, x+h/2, y + k1*(h/2)) k3 = feval (func, x+h/2, y + k2*(h/2)) k4 = feval (func, x+h, y + k3*h) for j in range (m): y[j] = y[j] + (h/6)*(k1[j] + 2*k2[j] + 2*k3[j] + k4[j]) x = x + h for r in range ( len (y)): ysol = np. append (ysol, y[r]) return [ xsol, ysol ] Since only two quantities of y to be calculated (y 0 and y 1 ), the xspan of the Runge-Kutta function also takes only two values of x, that is x 0 and x 1. Outside of the Runge-Kutta function, we create another function for the Adams-Bashforth method. This function takes in the same input arguments as the Runge-Kutta function, such as the function func, initial condition yinit, x interval x range, and step size h, def ABM2ndOrder ( func, yinit, x_range, h): After dividing the interval x range into smaller subintervals, dx = int (( x_range [-1] - x_range [0]) / h) xrk = [ x_range [0] + k*h for k in range (dx + 1)] the first two subintervals are passed as input for the x interval of the Runge-Kutta function, t = ( xrk [0], xrk [1]) [xx, yy] = RungeKutta4thOrder (func, yinit, t, h) The two outputs of the Runge-Kutta function, xx and yy, are Numpy arrays with dimension (2,) each. It means, each array xx and yy contains 2 elements (x 0 and x 1 and y 0 and y 1, 2
respectively) which will be used in the Adams-Bashforth function. Then we initialize variables x and y along with containers for storing the results after each iteration. The elements of xx are passed as initial values for the variable x and yy for y. We also initialize variables xsol and ysol that will store results of the calculation after each iteration, x = xx xsol = np. empty (0) y = yy ysol = np. empty (0) ysol = np. append (ysol, y) We also need to initialize another variable, which we call it yn, yn = np. array ([yy[0]]) Then we start the iteration over all points of the subintervals. Within the for loop, from equation (2), we define the two points x i and x i 1, x00 = x[i] x11 = x[i - 1] also for y i and y i 1, y00 = np. array ([y[i]]) y11 = np. array ([y[i - 1]]) and evaluate the derivative(s) based on x s and y s, y0prime = feval ( func, x00, y00 ) y1prime = feval ( func, x11, y11 ) which are used to calculate P i+1, or the predictor, ypredictor = y00 + (3/2)*h* y0prime - (1/2)*h* y1prime The predictor equation is then used to evaluate the derivative(s), based on the equation (3), evaluated at point x i+1, which we also need to initialize its variable, xpp = x[i] + h and use it for ypp = feval ( func, xpp, ypredictor ) 3
We finally arrive at the equation (3) of the 2nd-order Adams-Bashforth method, for j in range (m): yn[j] = y00 [j] + (h/2)* ypp [j] + (h/2)* y0prime [j] The full implementation of the 2nd-order Adams-Bashforth method can be done as follows, def ABM2ndOrder (func, yinit, xspan, h): m = len ( yinit ) dx = int (( xspan [-1] - xspan [0]) / h) xrk = [ xspan [0] + k*h for k in range (dx + 1)] t = ( xrk [0], xrk [1]) [xx, yy] = RungeKutta4thOrder (func, yinit, t, h) x = xx xsol = np. empty (0) y = yy yn = np. array ([yy[0]]) ysol = np. empty (0) ysol = np. append (ysol, y) for i in range (1, dx): x00 = x[i] x11 = x[i - 1] xpp = x[i] + h y00 = np. array ([y[i]]) y11 = np. array ([y[i - 1]]) y0prime = feval ( func, x00, y00 ) y1prime = feval ( func, x11, y11 ) ypredictor = y00 + (3/2)*h* y0prime - (1/2)*h* y1prime ypp = feval ( func, xpp, ypredictor ) for j in range (m): yn[j] = y00 [j] + (h/2)* ypp [j] + (h/2)* y0prime [j] xs = x[i] + h xsol = np. append (xsol, xs) x = xsol for r in range ( len (yn)): ysol = np. append (ysol, yn) 4
y = ysol return [ xsol, ysol ] 4th-order Adams-Bashforth-Moulton Method The approximation for the 4th-order Adams-Bashforth-Moulton method is given by equation (10.21) in the lecture note for week 10, as follows where y i+1 y i + h 24 (9f(x i+1, P i+1 ) + 19f(x i, y i ) 5f(x i 1, y i 1 ) + f(x i 2, y i 2 )), (4) P i+1 y i + h 24 (55f(x i, y i ) 59f(x i 1, y i 1 ) + 37f(x i 2, y i 2 ) 9f(x i 3, y i 3 )), (5) is the prediction equation. The algorithm to implement this equation in Python is similar to the 2nd-order Adams- Bashforth method above, where it invokes another solver to get initial conditions, except that the 4th-order method requires 4 initial conditions evaluated at 4 previous points. Hence, for the input arguments of the Runge-Kutta method, we modify the initial subintervals to t = ( xrk [0], x[3]) [xx, yy] = RungeKutta4thOrder (func, yinit, t, h) In order to be able to solve multiple ODEs or a system of ODEs, the variable yn needs to be initialized more general, hence yn = yy[0:m] The other thing we need to modify is the following variables in the for loop, y00 = np. array ([y[i]]) y11 = np. array ([y[i-1]]) y22 = np. array ([y[i-2]]) y33 = np. array ([y[i-3]]) or, to be general for multiple ODEs, y00 = y[m*i:] y11 = y[m*(i-1):m*i] y22 = y[m*(i-2):m*(i-1)] y33 = y[m*(i-3):m*(i-2)] 5
And finally, for solving multiple ODEs, saving the result of yn in the container ysol does not require the for loop, hence, ysol = np. append (ysol, yn) 6