On the previous page we saw that to solve a single ODE using an IRK method, we need to recast the stage value expressions as a linear system which is solved at each step of the method. When we have a system of multiple ODEs, this becomes more complicated as we need to do this for each equation in the system, and the stage values may form non-linear equations.
In a similar approach to solving single ODEs, we introduce an \(N \times 1\) vector \(\mathbf{Y}_i\) for the solution of the system over intermediate intervals of the step
(3.7)#\[\mathbf{Y}_i = \mathbf{y}_n + h \displaystyle\sum_{j=1}^s a_{ij} \mathbf{k}_j, \]
and the expression for the stage values \(\mathbf{k}_i\) becomes
Since \(\mathbf{Y}_i\) are \(N \times 1\) vectors then these are \(sN \times 1\) vectors. We can replace the summation with matrix multiplications using a Kronecker product.
Definition 3.4 (The Kronecker product)
The Kronecker product between an \(m \times n\) matrix \(A\) and another \(p \times q\) matix \(B\) is denoted by \(A \otimes B\) and defined by
\[\begin{split} \begin{align*}
A \otimes B =
\begin{pmatrix}
a_{11} B & \cdots & a_{1n} B \\
\vdots & \ddots & \vdots \\
a_{m1} B & \cdots & a_{mn} B
\end{pmatrix}.
\end{align*} \end{split}\]
In other words, each element of \(A\) is multiplied by the matrix \(B\).
Let’s say we have a system of \(N = 3\) ODEs. Introducing a notation where \(y_{j}\), \(Y_{i,j}\) and \(f_{i,j}\) are the \(j\)-th elements of \(\mathbf{y}_n\), \(\mathbf{Y}_i\) and \(\mathbf{f}(t_n + c_ih, \mathbf{Y}_i)\) respectively, then this can be written as
To compute the stage values \(\mathbf{Y}_1, \ldots, \mathbf{Y}_s\), we must solve the system given in equation (3.12). Unlike the examples in the previous page, this system is generally nonlinear because the function \(\mathbf{f}(t,\mathbf{y})\) may contain nonlinear terms. This means we cannot rearrange the equations into a linear system and solve them directly. To solve these equations we use Newton’s method, which is a well known numerical method to computing the solutions of \(g(x) = 0\) where \(g\) is a differentiable function.
The iterations continue until successive estimates differ by less than a specified tolerance.
3.3.1.2. Newton’s method for a system of equations#
Given a system of equations \(g(\mathbf{x}) = \mathbf{0}\) where \(g = (g_1, g_2, \ldots)\) are differentiable functions, then the matrix form of Newton’s method is
The steps used to solve an IVP using an IRK method are outlined in Algorithm 3.1.
Algorithm 3.1 (Solving an IVP using an IRK method)
Inputs A system of \(N\) first-order ODEs of the form \(\mathbf{y}' = \mathbf{f}(t, \mathbf{y})\), a domain \(t \in [t_0, t_{\max}]\), a vector of initial values \(\mathbf{y}(t_0) = y_0\) and a value of the step length \(h\)
Outputs\((t_0, t_1, \ldots)\) and \((\mathbf{y}_0, \mathbf{y}_1, \ldots)\).
\(\mathbf{y}_{n+1} \gets \mathbf{y}_n + h (\mathbf{b}^T \otimes I_N) F(\mathbf{Y})\)
\(t_{n+1} \gets t_n + h\)
Return \((t_0, t_1, \ldots)\) and \((\mathbf{y}_0, \mathbf{y}_1, \ldots)\)
The additional cost of solving an \(sN\)-dimensional nonlinear system at every step is compensated by the excellent stability properties of IRK methods, making them particularly effective for stiff systems of ODEs.
Example 3.4
The van der Pol oscillator is a well known model of an oscillating system with non-linear damping described by the second-order ODE
\[ \ddot{y} - \mu (1 - y^2) \dot{y} + y = 0, \]
where \(\mu\) is a scalar parameter that governs the strength of the damping.
Use the Radau IA method to compute the solution to the van der Pol oscillator over the first 50 seconds where \(y(0) = 2\) and \(\mu = 10\) using a step length of \(h = 0.1\) and a convergence tolerance of \(tol = 10^{-4}\) for Newton’s method.
Solution
We need to rewrite the second-order ODE as a system of first-order ODEs. Let \(y_1 = y\) and \(y_2 = \dot{y}_1\) then
Here \(\|\Delta \mathbf{Y}\| = 0.056447 > 10^{-4}\) so we need to continue iterating. Computing \(F(\mathbf{Y})\) and \(G(\mathbf{Y})\) using the new values of \(\mathbf{Y}\)
Now \(\| \Delta(\mathbf{Y}) \| = 1.1864 \times 10^{-4} > 10^{-4}\) so we need to keep iterating. One more iteration results in \(\Delta \mathbf{Y} = \mathbf{0}\) (correct to 4 significant figures) so
The code below defines the functions jac() and radauIA() which computes the Jacobian of a function \(\mathbf{f}(t, y)\) and the solution to an initial value problem using the Radau IA method respectively.
defjac(f,t,y):N=len(y)J=np.zeros((N,N))I=np.eye(N)epsilon=1e-6foriinrange(N):J[:,i]=(f(t,y+epsilon*I[:,i])-f(t,y))/epsilonreturnJdefradauIA(f,tspan,y0,h):# Determine the number of ODEs in the systemN=len(y0)# Calculate the number of steps requirednsteps=int((tspan[1]-tspan[0])/h)# Define solution arrays and assign initial valuest=np.zeros(nsteps+1)y=np.zeros((nsteps+1,N))t[0]=tspan[0]y[0,:]=y0# Define Butcher tableauA=np.array([[1/4,-1/4],[1/4,5/12]])b=np.array([1/4,3/4])c=np.array([0,2/3])s=2e=np.ones(s)# Define maximum Newton iterationsmax_iter=10# Loop through stepsforninrange(nsteps):# Use Newton's method to solve for the stage valuesY=np.kron(e,y[n,:])F=np.zeros(s*N)Jf=jac(f,t[n],y[n,:])JG=np.eye(s*N)-h*np.kron(A,Jf)forkinrange(max_iter):foriinrange(s):idx=slice(i*N,(i+1)*N)F[idx]=f(t[n]+c[i]*h,Y[idx])G=Y-np.kron(e,y[n,:])-h*np.kron(A,np.eye(N))@Fdelta_Y=np.linalg.solve(JG,-G)Y+=delta_Yifnp.linalg.norm(delta_Y)<1e-6:breaky[n+1,:]=y[n,:]+h*np.kron(b.T,np.eye(N))@Ft[n+1]=t[n]+hreturnt,y
This code uses the following NumPy functions:
np.eye(N) – returns \(I_N\)
np.kron(A,B) – returns the Kronecker product \(A \otimes B\)
np.linalg.solve(A,b) – returns the solution to the linear system \(A \mathbf{x} = \mathbf{b}\)
np.eye(n) – returns the \(n \times n\) identity matrix
np.linalg.norm(a) – returns the Euclidean norm of the vector \(\mathbf{a}\)
functionJ=jac(f, t, y)N=length(y);J=zeros(N,N);I=eye(N);epsilon=1e-6;fori=1:NJ(:,i)=(f(t,y+epsilon*I(:,i))-f(t,y))/epsilon;endendfunction[t, y] = radauIA(f, tspan, y0, h)% Determine the number of ODEs in the systemN=length(y0);% Calculate the number of steps requirednsteps=floor((tspan(2)-tspan(1))/h);% Define solution arrays and assign initial valuest=zeros(nsteps+1,1);y=zeros(nsteps+1,N);t(1)=tspan(1);y(1,:)=y0;% Define Butcher tableauA=[1/4,-1/4;1/4,5/12];b=[1/4;3/4];c=[0;2/3];s=2;e=ones(s,1);% Define maximum Newton iterationsmax_iter=10;% Loop through stepsforn=1:nsteps% Use Newton's method to solve for the stage valuesY=kron(e,y(n,:)');F=zeros(s*N,1);Jf=jac(f,t(n),y(n,:)');JG=eye(s*N)-h*kron(A,Jf);fork=1:max_iterfori=1:sidx=1+(i-1)*N:i*N;F(idx)=f(t(n)+c(i)*h,Y(idx)');endG=Y-kron(e,y(n,:)')-h*kron(A,eye(N))*F;delta_Y=JG\-G;Y=Y+delta_Y;ifnorm(delta_Y)<1e-6breakendendy(n+1,:)=y(n,:)+(h*kron(b',eye(N))*F)';t(n+1)=t(n)+h;endend
This code uses the following functions:
eye(N) – returns \(I_N\)
kron(A,B) – returns the Kronecker product \(A \otimes B\)
A\b – returns the solution to the linear system \(A \mathbf{x} = \mathbf{b}\)
eye(n) – returns the \(n \times n\) identity matrix
norm(a) – returns the Euclidean norm of the vector \(\mathbf{a}\)
Note
The solution of the linear system for Newton’s method in the code above was done using NumPy’s and MATLAB’s linear system solvers. In practice, this is done using LU decomposition which is more efficient in this case since the coefficient method is constant for all iterations in a step. I used NumPy’s and MATLAB’s solvers here since have yet to meet LU decomposition which is covered in a later chapters.