#!/usr/bin/env python # coding: utf-8 # # # # # Validating Runge Kutta Butcher tables using Truncated Taylor Series # ## Authors: Zach Etienne & Brandon Clark # # # ## This tutorial notebook is designed to validate the Butcher tables contained within the Butcher dictionary constructed in the [RK Butcher Table Dictionary](Tutorial-RK_Butcher_Table_Dictionary.ipynb) NRPy+ module. # # ### NRPy+ Source Code for this module: # * [MoLtimestepping/RK_Butcher_Table_Validation.py](../edit/MoLtimestepping/RK_Butcher_Table_Validation.py) Stores the `Validate` function for validating convergence orders for Runge Kutta methods # * [MoLtimestepping/RK_Butcher_Table_Dictionary.py](../edit/MoLtimestepping/RK_Butcher_Table_Dictionary.py) [\[**tutorial**\]](Tutorial-RK_Butcher_Table_Dictionary.ipynb) Accesses the Butcher table dictionary `Butcher_dict` for known explicit Runge Kutta methods # # ## Introduction: # # Starting with the ODE (ordinary differential equation) initial value problem: # $$ # y'(t) = f(y,t)\ \ \ y\left(t=0\right)=y_0, # $$ # for various choices of $f(y,t)$, this module validates the Runge Kutta (RK) methods coded in [RK_Butcher_Table_Dictionary.py](../edit/MoLtimestepping/RK_Butcher_Table_Dictionary.py) [**tutorial notebook**](Tutorial-RK_Butcher_Table_Dictionary.ipynb) as follows: # # Given $y_0$, and a smooth $f(y,t)$, all explicit RK methods provide an estimate for $y_1 = y\left(\Delta t\right)$, with an error term that is proportional to $\left(\Delta t\right)^m$, where $m$ is an integer typically greater than zero. This error term corresponds to the *local* truncation error. For RK4, for example, while the *total accumulated truncation error* (i.e., the accumulated error at a fixed final time $t_f$) is proportional to $\left(\Delta t\right)^4$, the *local* truncation error (i.e., the error after one arbitrarily chosen timestep $\Delta t$) is proportional to $\left(\Delta t\right)^5$. # # If the exact solution $y(t)$ is known as a closed-form expression, then $y\left(\Delta t\right)$ can be *separately* written as a Taylor expansion about $y(t=0)$: # # $$ # y\left(\Delta t\right) = \sum_{n=0}^\infty \frac{y^{(n)}(t=0)}{n!} \left(\Delta t\right)^n, # $$ # where $y^{(n)}(t=0)$ is the $n$th derivative of $y(t)$ evaluated at $t=0$. # # The above expression will be known exactly. Further if one chooses a numerical value for $y_0$ *and leaves $\Delta t$ unspecified*, any explicit RK method will provide an estimate for $y\left(\Delta t\right)$ of the form # # $$ # y\left(\Delta t\right) = \sum_{n=0}^\infty a_n \left(\Delta t\right)^n, # $$ # where $a_n$ *must* match the Taylor expansion of the *exact* solution at least up to and including terms proportional to $\left(\Delta t\right)^m$, where $m$ is the order of the local truncation error. If this is *not* the case, then the Butcher table was almost certainly *not* typed correctly. # # Therefore, comparing the numerical result with unspecified $\Delta t$ against the exact Taylor series provides a convenient (though not perfectly robust) means to verify that the Butcher table for a given RK method was typed correctly. Multiple typos in the Butcher tables were found using this approach. # # **Example from Z. Etienne's MATH 521 (Numerical Analysis) lecture notes:** # # Consider the ODE # $$ # y' = y - 2 t e^{-2t},\quad y(0)=y(t_0)=0. # $$ # # * Solve this ODE exactly, then Taylor expand the solution about $t=0$ to # approximate the solution at $y(t=\Delta t)$ to fifth order in $\Delta # t$. # * Next solve this ODE using Heun's method (second order in total accumulated truncation error, third order in local truncation error) {\it by hand} with a step size of # $\Delta t$ to find $y(\Delta t)$. Confirm that the solution obtained # when using Heun's method has an error term that is at worst # $\mathcal{O}\left((\Delta t)^3\right)$. If the dominant error is # proportional to a higher power of $\Delta t$, explain the discrepancy. # # * Finally solve this ODE using the Ralston method {\it by hand} # with a step size of $\Delta t$ to find $y(\Delta t)$. Is the # coefficient on the dominant error term closer to the exact solution # than Heun's method? # # We can solve this equation via the method of integrating factors, # which states that ODEs of the form: # $$ # y'(t) + p(t) y(t) = g(t) # $$ # are solved via # $$ # y(t) = \frac{1}{\mu(t)} \left[ \int \mu(s) g(s) ds + c \right], # $$ # where the integrating factor $\mu(t)$ is given by # $$ # \mu(t) = \exp\left(\int p(t) dt\right) # $$ # # Here, $p(t)=-1$ and $g(t) = - 2 t e^{-2t}$. Then # # \begin{equation} # \mu(t) = \exp\left(-\int dt\right) = e^{-t+c} = k e^{-t} # \end{equation} # and # # \begin{align} # y(t) &= e^t/k \left[ \int k e^{-s} (- 2 s e^{-2s}) ds + c \right] = -2 e^t \left[ \int s e^{-3s} ds + c' \right] \\ # &= -2 e^t \left[ e^{-3 t} \left(-\frac{t}{3}-\frac{1}{9}\right) + c' \right] = -2 e^{-2t} \left(-\frac{t}{3}-\frac{1}{9}\right) -2 c' e^t \\ # &= e^{-2t} \left(2\frac{t}{3}+\frac{2}{9}\right) + c'' e^t \\ # \end{align} # # If $y(0)=0$ then we can compute the integration constant $c''$, and # $y(t)$ becomes # $$ # y(t) = \frac{2}{9} e^{-2 t} \left(3 t + 1 - e^{3 t}\right). # $$ # # The Taylor Series expansion of the exact solution about $t=0$ # evaluated at $y(\Delta t)$ yields # $$ # y(\Delta t) = -(\Delta t)^2+(\Delta t)^3-\frac{3 (\Delta t)^4}{4}+\frac{23 (\Delta # t)^5}{60}-\frac{19 (\Delta t)^6}{120}+O\left((\Delta t)^7\right). # $$ # # Next we evaluate $y(\Delta t)$ using Heun's method. We know $y(0)=y_0=0$ and # $f(y,t)=y - 2 t e^{-2t}$, so # \begin{align} # k_1 &= \Delta t f(y(0),0) \\ # &= \Delta t \times 0 \\ # &= 0 \\ # k_2 &= \Delta t f(y(0)+k_1,0+\Delta t) \\ # &= \Delta t f(y(0)+0,0+\Delta t) \\ # &= \Delta t (-2 \Delta t e^{-2\Delta t}) \\ # &= -2 (\Delta t)^2 e^{-2\Delta t} \\ # y(\Delta t) &= y_0 + \frac{1}{2} (k_1 + k_2) + \mathcal{O}\left((\Delta t)^3\right) \\ # &= 0 - (\Delta t)^2 e^{-2\Delta t} \\ # &= - (\Delta t)^2 ( 1 - 2 \Delta t + 2 (\Delta t)^2 + ...) \\ # &= - (\Delta t)^2 + 2 (\Delta t)^3 + \mathcal{O}\left((\Delta t)^4\right). # \end{align} # # Thus the coefficient on the $(\Delta t)^3$ term is wrong, but # this is completely consistent with the fact that our stepping # scheme is only third-order accurate in $\Delta t$. # # In the below approach, the RK result is subtracted from the exact Taylor series result, as a check to determine whether the RK Butcher table was coded correctly; if it was not, then the odds are good that the RK results will not match to the expected local truncation error order. Multiple $f(y,t)$ are coded below to improve the robustness of this test. # # # # Table of Contents # $$\label{toc}$$ # # This notebook is organized as follows # # 1. [Step 1](#initializenrpy): Initialize needed Python/NRPy+ modules # 1. [Step 2](#table_validate) Validate Convergence Order of Butcher Tables # 1. [Step 2.a](#rhs): Defining the right-hand side of the ODE # 1. [Step 2.b](#validfunc): Defining a Validation Function # 1. [Step 2.c](#rkvalid): Validating RK Methods against ODEs # 1. [Step 3](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file # # # # Step 1: Initialize needed Python/NRPy+ modules [Back to [top](#toc)\] # $$\label{initializenrpy}$$ # # Let's start by importing all the needed modules from Python/NRPy+: # In[1]: import sympy as sp # SymPy: The Python computer algebra package upon which NRPy+ depends import numpy as np # NumPy: A numerical methods module for Python from MoLtimestepping.RK_Butcher_Table_Dictionary import Butcher_dict # # # # Step 2: Validate Convergence Order of Butcher Tables [Back to [top](#toc)\] # $$\label{table_validate}$$ # # # Each Butcher table/Runge Kutta method is tested by solving an ODE. Comparing the Taylor series expansions of the exact solution and the numerical solution as discussed in the **Introduction** above will confirm whether the method converges to the appropriate order. # # # ## Step 2.a: Defining the right-hand side of the ODE [Back to [top](#toc)\] # $$\label{rhs}$$ # # Consider the form of ODE $y'=f(y,t)$. The following begins to construct a dictionary `rhs_dict` of right-hand side functions for us to validate explicit Runge Kutta methods. The most up-to-date catalog of functions stored in `rhs_dict` can be found in the [RK_Butcher_Table_Validation.py](../edit/MoLtimestepping/RK_Butcher_Table_Validation.py) module. # In[2]: def fypt(y,t): # Yields expected convergence order for all cases # except DP6 which converge to higher order (7, respectively) return y+t def fy(y,t): # Yields expected convergence order for all cases return y def feypt(y,t): # Yields expected convergence order for all cases return sp.exp(1.0*(y+t)) def ftpoly6(y,t): # Yields expected convergence order for all cases, L6 has 0 error return 2*t**6-389*t**5+15*t**4-22*t**3+81*t**2-t+42 rhs_dict = {'ypt':fypt, 'y':fy, 'eypt':feypt, 'tpoly6':ftpoly6} # # # ## Step 2.b: Defining a Validation Function [Back to [top](#toc)\] # $$\label{validfunc}$$ # # To validate each Butcher table we compare the exact solutions to ODEs with the numerical solutions using the Runge Kutta scheme built into each Butcher table. The following is a function that # # 1. Solves the ODE exactly, # 2. Solves the ODE numerically for a given Butcher table, and # 3. Compares the two solutions and checks for the order of convergence by returning their difference. # # The `Validate()` function inputs a specified `Butcher_key`, the starting guess solution and time `y_n`, `t_n` and the right-hand side of the ODE corresponding to a specified initial value problem, `rhs_key`. # # # # In[3]: from MoLtimestepping.RK_Butcher_Table_Dictionary import Butcher_dict def Validate(Butcher_key, yn, tn, rhs_key): # 1. First we solve the ODE exactly y = sp.Function('y') sol = sp.dsolve(sp.Eq(y(t).diff(t), rhs_dict[rhs_key](y(t), t)), y(t)).rhs constants = sp.solve([sol.subs(t,tn)-yn]) exact = sol.subs(constants) # 2. Now we solve the ODE numerically using specified Butcher table # Access the requested Butcher table Butcher = Butcher_dict[Butcher_key][0] # Determine number of predictor-corrector steps L = len(Butcher)-1 # Set a temporary array for update values k = np.zeros(L, dtype=object) # Initialize intermediate variable yhat = 0 # Initialize the updated solution ynp1 = 0 for i in range(L): #Initialize and approximate update for solution yhat = yn for j in range(i): # Update yhat for solution using a_ij Butcher table coefficients yhat += Butcher[i][j+1]*k[j] if Butcher_key == "DP8" or Butcher_key == "L6": yhat = 1.0*sp.N(yhat,20) # Otherwise the adding of fractions kills performance. # Determine the next corrector variable k_i using c_i Butcher table coefficients k[i] = dt*rhs_dict[rhs_key](yhat, tn + Butcher[i][0]*dt) # Update the solution at the next iteration ynp1 using Butcher table coefficients ynp1 += Butcher[L][i+1]*k[i] # Finish determining the solution for the next iteration ynp1 += yn # Determine the order of the RK method order = Butcher_dict[Butcher_key][1]+2 # Produces Taylor series of exact solution at t=tn about t = 0 with the specified order exact_series = sp.series(exact.subs(t, dt),dt, 0, order) num_series = sp.series(ynp1, dt, 0, order) diff = exact_series-num_series return diff # # # ## Step 2.c: Validating RK Methods against ODEs [Back to [top](#toc)\] # $$\label{rkvalid}$$ # # The following makes use of the `Validate()` function above to demonstrate that each method within the Butcher table dictionary converges to the expected order for the given right-hand side expression. # In[4]: t, dt = sp.symbols('t dt') # Set initial conditions t0 = 0 y0 = 1 # Set RHS of ODE function = 'ypt'# This can be changed, just be careful that the initial conditions are satisfied for key,value in Butcher_dict.items(): print("RK method: \""+str(key)+"\".") y = sp.Function('y') print(" When solving y'(t) = "+str(rhs_dict[function](y(t),t))+", y("+str(t0)+")="+str(y0)+",") local_truncation_order = list(value)[1]+1 print(" the first nonzero term should have local truncation error proportional to O(dt^"+str(local_truncation_order)+") or a higher power of dt.") print("Subtracting the numerical result from the exact Taylor expansion, we find a local truncation error of:") sp.pretty_print(Validate(key, y0, t0, function)) # print("\n") print(" (Coefficients of order 1e-15 or less may generally be ignored, as these are at roundoff error.)\n") # # # # Step 3: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\] # $$\label{latex_pdf_output}$$ # # The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename # [Tutorial-RK_Butcher_Table_Validation.pdf](Tutorial-RK_Butcher_Table_Validation.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.) # In[5]: import cmdline_helper as cmd # NRPy+: Multi-platform Python command-line interface cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-RK_Butcher_Table_Validation")