Path: blob/main/latex-templates/templates/numerical-methods/ode_solver.tex
51 views
unlisted
\documentclass[a4paper, 11pt]{report}1\usepackage[utf8]{inputenc}2\usepackage[T1]{fontenc}3\usepackage{amsmath, amssymb}4\usepackage{graphicx}5\usepackage{siunitx}6\usepackage{booktabs}7\usepackage{algorithm2e}8\usepackage{xcolor}9\usepackage[makestderr]{pythontex}1011% Colors for method comparison12\definecolor{euler}{RGB}{231, 76, 60}13\definecolor{rk4}{RGB}{46, 204, 113}14\definecolor{rkf45}{RGB}{52, 152, 219}15\definecolor{exact}{RGB}{44, 62, 80}1617\title{Numerical Methods for Ordinary Differential Equations:\\18A Comparative Analysis of Integration Schemes}19\author{Computational Mathematics Division\\Technical Report CM-2024-003}20\date{\today}2122\begin{document}23\maketitle2425\begin{abstract}26This technical report presents a comprehensive comparison of numerical methods for solving ordinary differential equations. We implement and analyze Forward Euler, fourth-order Runge-Kutta (RK4), and adaptive Runge-Kutta-Fehlberg (RKF45) methods. Performance is evaluated on stiff and non-stiff test problems using accuracy, computational cost, and stability metrics. Results demonstrate the trade-offs between method complexity and solution quality, with RKF45 achieving optimal efficiency for most engineering applications.27\end{abstract}2829\tableofcontents3031\chapter{Introduction}3233Ordinary differential equations (ODEs) are fundamental to modeling physical, biological, and engineering systems. While analytical solutions exist for simple cases, most practical problems require numerical integration. This report evaluates three widely-used methods with increasing sophistication.3435\section{Problem Statement}36We consider initial value problems of the form:37\begin{equation}38\frac{dy}{dt} = f(t, y), \quad y(t_0) = y_039\end{equation}4041The challenge is to approximate $y(t)$ at discrete time points with controllable accuracy and computational efficiency.4243\section{Methods Overview}4445\subsection{Forward Euler Method}46The simplest explicit method, first-order accurate:47\begin{equation}48y_{n+1} = y_n + h f(t_n, y_n)49\end{equation}50\textbf{Stability:} Conditionally stable, $|1 + h\lambda| < 1$ for $\dot{y} = \lambda y$.5152\subsection{Classical Runge-Kutta (RK4)}53Fourth-order method using weighted average of slopes:54\begin{align}55k_1 &= f(t_n, y_n) \\56k_2 &= f(t_n + h/2, y_n + hk_1/2) \\57k_3 &= f(t_n + h/2, y_n + hk_2/2) \\58k_4 &= f(t_n + h, y_n + hk_3) \\59y_{n+1} &= y_n + \frac{h}{6}(k_1 + 2k_2 + 2k_3 + k_4)60\end{align}6162\subsection{Runge-Kutta-Fehlberg (RKF45)}63Adaptive method with embedded error estimation using 4th and 5th order formulas:64\begin{equation}65\text{Error estimate: } \epsilon = |y_{n+1}^{(5)} - y_{n+1}^{(4)}|66\end{equation}6768Step size adapts to maintain $\epsilon < \text{tolerance}$.6970\chapter{Implementation}7172\begin{pycode}73import numpy as np74import matplotlib.pyplot as plt75from time import time76plt.rc('text', usetex=True)77plt.rc('font', family='serif')7879np.random.seed(42)8081# Define numerical methods82def euler(f, y0, t_span, h):83"""Forward Euler method."""84t = np.arange(t_span[0], t_span[1] + h, h)85y = np.zeros((len(t), len(np.atleast_1d(y0))))86y[0] = np.atleast_1d(y0)8788for i in range(len(t) - 1):89y[i+1] = y[i] + h * np.atleast_1d(f(t[i], y[i]))9091return t, y, len(t) - 1 # Return function evaluations9293def rk4(f, y0, t_span, h):94"""Classical 4th-order Runge-Kutta."""95t = np.arange(t_span[0], t_span[1] + h, h)96y = np.zeros((len(t), len(np.atleast_1d(y0))))97y[0] = np.atleast_1d(y0)98n_evals = 099100for i in range(len(t) - 1):101k1 = np.atleast_1d(f(t[i], y[i]))102k2 = np.atleast_1d(f(t[i] + h/2, y[i] + h*k1/2))103k3 = np.atleast_1d(f(t[i] + h/2, y[i] + h*k2/2))104k4 = np.atleast_1d(f(t[i] + h, y[i] + h*k3))105y[i+1] = y[i] + h/6 * (k1 + 2*k2 + 2*k3 + k4)106n_evals += 4107108return t, y, n_evals109110def rkf45(f, y0, t_span, tol=1e-6, h_init=0.1):111"""Adaptive Runge-Kutta-Fehlberg 4(5) method."""112# Butcher tableau coefficients113c = np.array([0, 1/4, 3/8, 12/13, 1, 1/2])114a = np.array([115[0, 0, 0, 0, 0],116[1/4, 0, 0, 0, 0],117[3/32, 9/32, 0, 0, 0],118[1932/2197, -7200/2197, 7296/2197, 0, 0],119[439/216, -8, 3680/513, -845/4104, 0],120[-8/27, 2, -3544/2565, 1859/4104, -11/40]121])122b4 = np.array([25/216, 0, 1408/2565, 2197/4104, -1/5, 0])123b5 = np.array([16/135, 0, 6656/12825, 28561/56430, -9/50, 2/55])124125t_list = [t_span[0]]126y_list = [np.atleast_1d(y0)]127h = h_init128t = t_span[0]129y = np.atleast_1d(y0)130n_evals = 0131132while t < t_span[1]:133if t + h > t_span[1]:134h = t_span[1] - t135136# Compute stages137k = np.zeros((6, len(y)))138for i in range(6):139yi = y + h * sum(a[i, j] * k[j] for j in range(i))140k[i] = np.atleast_1d(f(t + c[i] * h, yi))141n_evals += 1142143# 4th and 5th order solutions144y4 = y + h * sum(b4[i] * k[i] for i in range(6))145y5 = y + h * sum(b5[i] * k[i] for i in range(6))146147# Error estimate148error = np.max(np.abs(y5 - y4))149150# Step size control151if error < tol or h < 1e-10:152t = t + h153y = y5154t_list.append(t)155y_list.append(y.copy())156157# Adjust step size158if error > 0:159h = 0.9 * h * (tol / error) ** 0.2160h = min(h, 0.5) # Cap step size161162return np.array(t_list), np.array(y_list), n_evals163164# Test Problem 1: Exponential decay (non-stiff)165def exp_decay(t, y):166return -2 * y167168def exp_decay_exact(t, y0=1):169return y0 * np.exp(-2 * t)170171# Test Problem 2: Van der Pol oscillator (stiff)172def vanderpol(t, y, mu=1):173y1, y2 = y174dy1 = y2175dy2 = mu * (1 - y1**2) * y2 - y1176return np.array([dy1, dy2])177178# Test Problem 3: Lorenz system (chaotic)179def lorenz(t, y, sigma=10, rho=28, beta=8/3):180x, y_l, z = y181dx = sigma * (y_l - x)182dy = x * (rho - z) - y_l183dz = x * y_l - beta * z184return np.array([dx, dy, dz])185186# Run comparison on exponential decay187t_span = (0, 5)188y0 = 1.0189step_sizes = [0.5, 0.2, 0.1, 0.05, 0.02]190191# Store results for error analysis192errors_euler = []193errors_rk4 = []194evals_euler = []195evals_rk4 = []196197for h in step_sizes:198t_e, y_e, n_e = euler(exp_decay, y0, t_span, h)199t_r, y_r, n_r = rk4(exp_decay, y0, t_span, h)200201exact_e = exp_decay_exact(t_e, y0)202exact_r = exp_decay_exact(t_r, y0)203204errors_euler.append(np.max(np.abs(y_e.flatten() - exact_e)))205errors_rk4.append(np.max(np.abs(y_r.flatten() - exact_r)))206evals_euler.append(n_e)207evals_rk4.append(n_r)208209# RKF45 with adaptive stepping210t_rkf, y_rkf, n_rkf = rkf45(exp_decay, y0, t_span, tol=1e-8)211exact_rkf = exp_decay_exact(t_rkf, y0)212error_rkf = np.max(np.abs(y_rkf.flatten() - exact_rkf))213214# Detailed comparison at h=0.1215h_test = 0.1216t_euler, y_euler, _ = euler(exp_decay, y0, t_span, h_test)217t_rk4, y_rk4, _ = rk4(exp_decay, y0, t_span, h_test)218219# Van der Pol solution220t_vdp = np.linspace(0, 20, 2000)221from scipy.integrate import solve_ivp222sol_vdp = solve_ivp(vanderpol, (0, 20), [2, 0], t_eval=t_vdp, method='RK45')223224# Create comprehensive figure225fig = plt.figure(figsize=(12, 10))226227# Plot 1: Solution comparison228ax1 = fig.add_subplot(2, 2, 1)229t_exact = np.linspace(0, 5, 200)230ax1.plot(t_exact, exp_decay_exact(t_exact), 'k-', linewidth=2, label='Exact')231ax1.plot(t_euler, y_euler, 'o-', color='#e74c3c', markersize=4, linewidth=1, label=f'Euler ($h={h_test}$)')232ax1.plot(t_rk4, y_rk4, 's-', color='#2ecc71', markersize=3, linewidth=1, label=f'RK4 ($h={h_test}$)')233ax1.plot(t_rkf, y_rkf, '^', color='#3498db', markersize=4, label=f'RKF45 ({len(t_rkf)} steps)')234ax1.set_xlabel('Time $t$')235ax1.set_ylabel('$y(t)$')236ax1.set_title('Exponential Decay: Method Comparison')237ax1.legend(fontsize=8)238ax1.grid(True, alpha=0.3)239240# Plot 2: Error convergence241ax2 = fig.add_subplot(2, 2, 2)242ax2.loglog(step_sizes, errors_euler, 'o-', color='#e74c3c', linewidth=2, label='Euler (slope$\\approx 1$)')243ax2.loglog(step_sizes, errors_rk4, 's-', color='#2ecc71', linewidth=2, label='RK4 (slope$\\approx 4$)')244ax2.axhline(error_rkf, color='#3498db', linestyle='--', label=f'RKF45 (tol=$10^{{-8}}$)')245246# Reference slopes247h_ref = np.array([0.5, 0.02])248ax2.loglog(h_ref, 0.5*h_ref, ':', color='gray', alpha=0.5, label='$O(h)$')249ax2.loglog(h_ref, 0.005*h_ref**4, ':', color='gray', alpha=0.5, label='$O(h^4)$')250251ax2.set_xlabel('Step size $h$')252ax2.set_ylabel('Maximum Error')253ax2.set_title('Error Convergence')254ax2.legend(fontsize=8)255ax2.grid(True, alpha=0.3, which='both')256257# Plot 3: Van der Pol phase portrait258ax3 = fig.add_subplot(2, 2, 3)259ax3.plot(sol_vdp.y[0], sol_vdp.y[1], color='#9b59b6', linewidth=1)260ax3.plot(sol_vdp.y[0, 0], sol_vdp.y[1, 0], 'go', markersize=8, label='Start')261ax3.set_xlabel('$y_1$')262ax3.set_ylabel('$y_2$')263ax3.set_title('Van der Pol Oscillator Phase Portrait')264ax3.legend()265ax3.grid(True, alpha=0.3)266ax3.set_aspect('equal')267268# Plot 4: Efficiency comparison269ax4 = fig.add_subplot(2, 2, 4)270ax4.loglog(evals_euler, errors_euler, 'o-', color='#e74c3c', linewidth=2, markersize=8, label='Euler')271ax4.loglog(evals_rk4, errors_rk4, 's-', color='#2ecc71', linewidth=2, markersize=8, label='RK4')272ax4.plot(n_rkf, error_rkf, '^', color='#3498db', markersize=12, label='RKF45')273274ax4.set_xlabel('Function Evaluations')275ax4.set_ylabel('Maximum Error')276ax4.set_title('Computational Efficiency')277ax4.legend(fontsize=8)278ax4.grid(True, alpha=0.3, which='both')279280plt.tight_layout()281plt.savefig('ode_solver_plot.pdf', bbox_inches='tight', dpi=150)282print(r'\begin{center}')283print(r'\includegraphics[width=\textwidth]{ode_solver_plot.pdf}')284print(r'\end{center}')285plt.close()286287# Additional stability analysis288# Create stability region plot289fig2, axes2 = plt.subplots(1, 2, figsize=(10, 4))290291# Euler stability region292theta = np.linspace(0, 2*np.pi, 200)293ax_stab1 = axes2[0]294ax_stab1.plot(np.cos(theta) - 1, np.sin(theta), 'r-', linewidth=2)295ax_stab1.fill(np.cos(theta) - 1, np.sin(theta), alpha=0.3, color='red')296ax_stab1.axhline(0, color='k', linewidth=0.5)297ax_stab1.axvline(0, color='k', linewidth=0.5)298ax_stab1.set_xlabel(r'Re($h\lambda$)')299ax_stab1.set_ylabel(r'Im($h\lambda$)')300ax_stab1.set_title('Euler Stability Region')301ax_stab1.set_xlim([-3, 1])302ax_stab1.set_ylim([-2, 2])303ax_stab1.grid(True, alpha=0.3)304ax_stab1.set_aspect('equal')305306# RK4 stability region (approximate)307x = np.linspace(-3, 1, 200)308y = np.linspace(-3, 3, 200)309X, Y = np.meshgrid(x, y)310Z = X + 1j*Y311R = 1 + Z + Z**2/2 + Z**3/6 + Z**4/24312ax_stab2 = axes2[1]313ax_stab2.contour(X, Y, np.abs(R), [1], colors='g', linewidths=2)314ax_stab2.contourf(X, Y, np.abs(R), levels=[0, 1], colors=['green'], alpha=0.3)315ax_stab2.axhline(0, color='k', linewidth=0.5)316ax_stab2.axvline(0, color='k', linewidth=0.5)317ax_stab2.set_xlabel(r'Re($h\lambda$)')318ax_stab2.set_ylabel(r'Im($h\lambda$)')319ax_stab2.set_title('RK4 Stability Region')320ax_stab2.set_xlim([-3, 1])321ax_stab2.set_ylim([-3, 3])322ax_stab2.grid(True, alpha=0.3)323ax_stab2.set_aspect('equal')324325plt.tight_layout()326plt.savefig('stability_regions.pdf', bbox_inches='tight')327print(r'\begin{center}')328print(r'\includegraphics[width=0.9\textwidth]{stability_regions.pdf}')329print(r'\end{center}')330plt.close()331332# Store key results333best_euler_error = errors_euler[-1]334best_rk4_error = errors_rk4[-1]335euler_order = np.log(errors_euler[0]/errors_euler[-1]) / np.log(step_sizes[0]/step_sizes[-1])336rk4_order = np.log(errors_rk4[0]/errors_rk4[-1]) / np.log(step_sizes[0]/step_sizes[-1])337\end{pycode}338339\chapter{Results and Analysis}340341\section{Accuracy Comparison}342343\begin{pycode}344# Generate results table345print(r'\begin{table}[h]')346print(r'\centering')347print(r'\caption{Error Analysis for Exponential Decay Problem}')348print(r'\begin{tabular}{lcccc}')349print(r'\toprule')350print(r'Step Size & Euler Error & RK4 Error & Euler Evals & RK4 Evals \\')351print(r'\midrule')352for h, e_e, e_r, n_e, n_r in zip(step_sizes, errors_euler, errors_rk4, evals_euler, evals_rk4):353print(f'{h:.2f} & {e_e:.2e} & {e_r:.2e} & {n_e} & {n_r} \\\\')354print(r'\midrule')355print(f'RKF45 (adaptive) & \\multicolumn{{2}}{{c}}{{{error_rkf:.2e}}} & \\multicolumn{{2}}{{c}}{{{n_rkf}}} \\\\')356print(r'\bottomrule')357print(r'\end{tabular}')358print(r'\end{table}')359\end{pycode}360361\subsection{Order of Convergence}362The empirical convergence rates confirm theoretical predictions:363\begin{itemize}364\item \textbf{Forward Euler}: Order $\approx$ \py{f"{euler_order:.2f}"} (theoretical: 1)365\item \textbf{RK4}: Order $\approx$ \py{f"{rk4_order:.2f}"} (theoretical: 4)366\end{itemize}367368\section{Computational Efficiency}369The efficiency plot reveals that:370\begin{enumerate}371\item RK4 requires 4x more evaluations per step than Euler372\item For the same accuracy, RK4 is significantly more efficient373\item RKF45 achieves the best accuracy-to-cost ratio through adaptive stepping374\end{enumerate}375376\section{Stability Analysis}377The stability regions show:378\begin{itemize}379\item \textbf{Euler}: Small circular region (radius 1 centered at $-1$)380\item \textbf{RK4}: Much larger region extending along the imaginary axis381\end{itemize}382383This explains why Euler requires very small step sizes for oscillatory problems, while RK4 remains stable with larger steps.384385\chapter{Method Selection Guidelines}386387\section{Recommendations by Problem Type}388389\begin{pycode}390print(r'\begin{table}[h]')391print(r'\centering')392print(r'\caption{Method Selection Guidelines}')393print(r'\begin{tabular}{lp{8cm}}')394print(r'\toprule')395print(r'Problem Type & Recommended Method \\')396print(r'\midrule')397print(r'Simple, smooth ODEs & RK4 with fixed step \\')398print(r'Problems requiring error control & RKF45 or similar adaptive method \\')399print(r'Stiff systems & Implicit methods (BDF, Radau) \\')400print(r'Long-time integration & Symplectic methods for Hamiltonian systems \\')401print(r'Real-time simulation & Euler or RK2 (when speed critical) \\')402print(r'\bottomrule')403print(r'\end{tabular}')404print(r'\end{table}')405\end{pycode}406407\section{Key Performance Metrics}408409For the test problem $y' = -2y$:410\begin{itemize}411\item Best Euler accuracy (h=0.02): \py{f"{best_euler_error:.2e}"}412\item Best RK4 accuracy (h=0.02): \py{f"{best_rk4_error:.2e}"}413\item RKF45 accuracy (tol=$10^{-8}$): \py{f"{error_rkf:.2e}"} with \py{n_rkf} evaluations414\end{itemize}415416\chapter{Conclusions}417418\section{Summary}419This report compared three numerical methods for ODE integration:420421\begin{enumerate}422\item \textbf{Forward Euler} is simple but has first-order accuracy and limited stability. Suitable only for quick estimates or when computational resources are extremely limited.423424\item \textbf{Classical RK4} provides an excellent balance of accuracy (4th order) and implementation simplicity. Recommended for most smooth, non-stiff problems with known smoothness.425426\item \textbf{RKF45} (adaptive) automatically adjusts step size to meet error tolerances. Optimal for problems where the solution behavior varies across the domain or when guaranteed accuracy is required.427\end{enumerate}428429\section{Future Work}430Extensions to consider:431\begin{itemize}432\item Implement implicit methods for stiff problems433\item Add dense output for interpolation between steps434\item Parallelize for systems of ODEs435\item Investigate symplectic integrators for Hamiltonian systems436\end{itemize}437438\appendix439\chapter{Algorithm Pseudocode}440441\begin{algorithm}[H]442\SetAlgoLined443\KwIn{$f(t,y)$, $y_0$, $[t_0, t_f]$, tolerance $\tau$}444\KwOut{Solution arrays $t[], y[]$}445$h \leftarrow h_{\text{initial}}$\;446$t \leftarrow t_0$, $y \leftarrow y_0$\;447\While{$t < t_f$}{448Compute $k_1, \ldots, k_6$ (RKF45 stages)\;449$y_4 \leftarrow y + h \sum b_i^{(4)} k_i$\;450$y_5 \leftarrow y + h \sum b_i^{(5)} k_i$\;451$\epsilon \leftarrow |y_5 - y_4|$\;452\eIf{$\epsilon < \tau$}{453Accept step: $t \leftarrow t + h$, $y \leftarrow y_5$\;454}{455Reject step\;456}457Update: $h \leftarrow 0.9 h (\tau/\epsilon)^{1/5}$\;458}459\caption{Adaptive Runge-Kutta-Fehlberg}460\end{algorithm}461462\end{document}463464465