\section{Numerical solution of the diffusion equation in a finite domain}
We are given the 1D unsteady diffusion equation
\begin{equation}\label{eq:ex1}
\frac{\partial C}{\partial t} - D \frac{\partial^2 C}{\partial x^2} = 0
\end{equation}
with a scalar diffusion coefficient $D =10^{-6}$. The domain size is $h$ which is discretized with $N_x$ points to give the grid space $\Delta x$ with a time step $\Delta t$. The initial condition is $C =0$ inside the domain.
Furthermore, denote with $N_t\in\mathbb{N}$ are the number of time steps of the simulation where $t_0=0$ is the initial time. All $N_t$ discretized time points $t_n = n \Delta t$ leading to the final time point of the simulation as $t_{N_t}= N_t \Delta t$. The discretized $x$ points of the domain are $x_i =(i -1)\Delta x =(i -1)h /(N_x -1)$ for $i =1, ..., N_x$. This gives exactly $N_x$ grid points with equal distance between adjacent points on the domain $[0, h]$ with the first point $x_1=0$ and the last point $x_{N_x}= h$.
for $k\in\mathbb{N}$ as well as $i$ and $n$ are the space and time discretization indices, respectively. To distringuish between the exact solution and the approximation we add a ``hat'' $\widehat{\phantom{C}}$ to the approximation. For example $C_i^n$ is the true value and $\widehat{C}_i^n$ is the discretized solution with discretization errors at the evaluation point $(x_i, t_n)$.
Given the Dirichlet boundary conditions $C(0, t)=0$ and the Neumann boundary conditions $\partial_x C(h, t)=0$. First we derive a $2^{nd}$ order \emph{explicit} finite difference approach with a $1^{st}$ order discretization in time (see Figure~\ref{fig:ex1}).
\caption{\label{fig:ex1}Dependency relation for the \emph{explicit} (left) and \emph{implicit} (right) finite difference approximation at a grid point indexed $(i, n)$.}
\end{figure}
To derive a second order scheme (assuming $C$ is three time continuously differentiable as a function from $[0, h]\times\mathbb{R}^+\to\mathbb{R}$) in space we first considure the Taylor expantion of $C$ with respect to $x$ given by
which is an approximation of the second derivative with error proportional to $\Delta x^2$. For the first order approximation in time the same trick can be used except that only the first two terms of the Taylor expantion need to be considured.
This holds at $x_i$ where $i =2, ..., N_x -1$, or in other words, everywhere inside the space domain except the boundary. The boundary needs to be handled seperately. The left boundary condition is a Dirichlet constraint which is simply a constant giving $C_1^n =1$ for all time while the Neumann condition on the right requires an additional discretization step for computing the next value. To achieve a $2^{nd}$ order discretization scheme it is usualy required to take the sum/difference of an Taylor expantion to the left and right direction at a given point.However, on the boundary there is no point at the right, which can be simply solved by adding a ghost point $x_{N_x+1}$ to the system (which will not show up in the final scheme). This leads to the two $3^{rd}$ order expantions evaluated at the right boundary as
\caption{\label{fig:task01_stable}Comparison for stable (left, $d =0.48\leq0.5$) vs. unstable (right, $d =0.55 > 0.5$). At the top the simulation snapshot after 120 time steps and at the bottom after 1000 timesteps.}
In this task two Dirichlet boundary conditions are given. The new boundary condition is a Dirichlet BC with $C_{N_x}^n =0$ which alters the equation system from Section~\ref{sec:task01_1} in the last two equations to
See Figure~\ref{fig:task01_inf} for $\lim_{t\to\infty}C(x, t)$ simulation (many timesteps) for subtask 1 and 2. In comparison both lead to a straight line solution (last energy) such that the Dirichlet or Neumann BC are fullfilled.
\caption{\label{fig:task01_inf}Solution from Section~\ref{sec:task01_1} (left) and Section~\ref{sec:task01_2} (right) for $t\to\infty$ simulated by setting number of time steps to $N_t =20000$.}
The following computations are similar in nature to the explicit scheme. The implicit discretization for the derivatives are (see: Figure~\ref{fig:ex1})
\caption{\label{fig:task01_all}Comparison of all four subtasks. As expected 1.1, 1.3 and 1.4 solve the same probelm and as such have (basically) identical solution while the subtask 1.2 solves for different BC (Dirichlet) which is fullfilled.}
To derive the second order accurate scheme in time one uses the Crank-Nicolson approach to derive the descritization at the time midpoints $t_{n+1/2}=(n +1/2)\Delta t$ which are indexed with $n+1/2$.
Next one need to ensure the evaluation to be done at the known grid points instead of the unknown time midpoints. Therefore, replacing the evaluation at the time midpoints by the time mean of the two direct neighbouring grid points
For comparison against the other methods see Figure~\ref{fig:task01_all}.
\subsection{Notes on Implementation}
All four tasks follow the same basic sructure which can be written as a generalized linear system of the form
\begin{displaymath}
L C^{n+1} = R C^n
\end{displaymath}
for updating the solution by one time step. The two matrices are dependent on the used scheme and the boundary conditions, see Table~\ref{tab:ex1}.
\begin{table}[h!]
\centering
\begin{tabular}{l | c c}
Task &$L$&$R$\\\hline
1.1 &$I$&$A_1$\\
1.2 &$I$&$A_2$\\
1.3 &$A_3$&$I$\\
1.4 &$I + A_4$&$I - A_4$\\
\end{tabular}
\caption{\label{tab:ex1}Left and right hand side matrices of the general update rule $L C^{n+1}= R C^n$ with $A_i$ depending on the subtask as defined in Section~\ref{sec:task01_1} till \ref{sec:task01_4} and $I$ the identity matrix.}
\end{table}
As a boild down solver the following is sufficient to compute the solution given an initial solution $C_0$ (argument \texttt{C}) with the appropriately defined matrices $L, R$ (arguments \texttt{L, R}) which incorporate the scheme, space and time step size $\Delta x, \Delta t$ as well as the dispersion parameter $D$ and the boundary conditions) at time $T = N_t \Delta$ (implicitly defined through the parameter \texttt{nx}).
\begin{python}
def solve(C, L, R, nt):
for t in range(nt):
rhs = C if R is None else R @ C
C = rhs if L is None else solve_banded((1, 1), L.data, rhs)
return C
\end{python}
This code assumes \texttt{C} to be a 1D \texttt{numpy} array as well as $L, R$ to by \texttt{scipy.sparse.spmatrix} sparse tridiagonal matrices or \texttt{None} which is equivalent to the identity matrix.
The additional code in the script \texttt{task01.py} contains setting up the needed matrices and plotting.
\newpage
\section{Numerical solution of the linear advection equation in a periodic domain}
First a few words about the upwind scheme. With a positive $U$ the upwind means that it is discretized by ``going left'', meaning into the opposite direction of the current position. In a similar faschion as above this leads to the discretization
with the current $C_0=\frac{U \Delta x}{\Delta t}$. The boundary condition enforces periodizity which means $C_1^n = C_{N_x}^n$ and therefore the update for the left most point is
\caption{\label{fig:task02_all}For initial Gauss wavelet (top) and Square wavelet (bottom) after 150 iterations (one leap around the boundary) with different current $C_0$. Left: $C_0=0.7$, Center: $C_0=1$ (perfect) and Right: $C_0=1.1$ (unstable).}
\end{figure}
For implementation one gets a very simple solver which computes the wave form at time $T = N_t \Delta t$ with $\Delta t = U \Delta x / C_0$ as the following;
\begin{python}
def solve(C, C0, nt):
nx = C.shape[0]
im1 = np.array([nx - 1] + list(range(nx - 1)))
for t in range(nt):
C = (1 - C0) * C + C0 * C[im1]
return C
\end{python}
where \texttt{C} is the initial wave form, the constant \texttt{C0} is the current $C_0$ and \texttt{nt} is the number of time steps to be performed. Again, the attached script \texttt{task02.py} implements this with additional code for setting up the initial conditions and plotting.