Linear Poisson equation
In this section, we'll demonstrate how to utilize Bramble.jl
to solve a linear Poisson equation with Dirichlet boundary conditions.
Problem description
Let's consider the following Poisson equation on a $n$-dimensional square domain ($n=1,2,3$),
\[\begin{align*} - \Delta u (x) &= f(x), \, x \in \Omega \\ u(x) &= g(x), \, x \in \partial \Omega \end{align*}\]
where $\Omega = (a,b)^n$.
In order to have a working example of our Poisson problem we aim to solve, lets take $n=2$ and define
\[u(x,y) = e^{x + y}, \, (x,y) \in [0,1]^2\]
and calculate $f$ and $g$ accordingly.
Discretization
The former problem can be discretized with finite differences based on a nonuniform grid. We will now detail the mathematical tools needed to formalize our discretization method.
Let us denote by $\Lambda$ a sequence of vectors $h=(h_{x_1}, h_{x_2})$, where $h_x = (h_{x,1}, h_{x,2}, \dots, h_{x,N_x})$, $h_y = (h_{y,1}, h_{y,2}, \dots, h_{y,N_y})$ and $h_{x,j}, h_{y,j} >0$. Let us denote by $h_{max}$ the maximum over all values of $h_{x,j}$ and $h_{y,j}$.
For each $h \in \Lambda$, we define the grid space
\[\overline{\Omega}_h = \set{(x_i, y_j) \in \mathbb{R}^2: x_0 = y_0 = 0, x_i = x_{i-1} + h_{x,i}, i=1,\dots,N_x, y_j = x_{j-1} + h_{y,j}, j=1,\dots,N_y }.\]
We also denote by $\partial \overline{\Omega}_h$, the subset of grid points corresponding to the boundary $\partial \Omega$. Built upon these grids, we introduce the discrete spaces of grid functions
\[W_h(\overline{\Omega}_h) = \set{u_h: \overline{\Omega}_h: \longrightarrow \mathbb{R}}\]
as well as $W_{h,0}(\overline{\Omega}_h) \subseteq W_h(\overline{\Omega}_h)$, its subspace of function zero at the boundary.
Finally, we introduce some notations for functions $u_h$ and $v_h$ in $W_h(\overline{\Omega}_h)$
\[(u_h,v_h)_h = \sum_{i=1}^{N_x}\sum_{j=1}^{N_y} |\square_{i,j}| u_h(x_i, y_j) v_h(x_i,y_j),\]
\[(u_h,v_h)_{+x} = \sum_{i=1}^{N_x}\sum_{j=1}^{N_y-1} h_{x,i} h_{y,j+1/2} u_h(x_i, y_j) v_h(x_i,y_j)\]
\[(u_h,v_h)_{+y} = \sum_{i=1}^{N_x-1}\sum_{j=1}^{N_y} h_{x,i+1/2} h_{y,j} u_h(x_i, y_j) v_h(x_i,y_j)\]
where $\square_{i,j} = \left[x_i-\frac{h_{x,i}}{2}, x_i+\frac{h_{x,i+1}}{2} \right] \times \left[y_j-\frac{h_{y,j}}{2}, y_j+\frac{h_{y,j+1}}{2} \right]$, $|\square_{i,j}|$ is its area and $h_{\cdot,i+1/2} = \frac{h_{\cdot,i}-h_{\cdot,i+1}}{2}$.
If $D_{-x}$ and $D_{-y}$ denote the standard backward difference operators defined on the grid points of $\overline{\Omega}_h$, then, we can formulate the following discrete problem:
find $u_h \in W_h(\overline{\Omega}_h)$, with $u_h(x_i,y_j) = u(x_i,y_j)$ on $\partial \overline{\Omega}_h,$ such that
\[(D_{-x}u_h, D_{-x}v_h)_{+x} + (D_{-y}u_h, D_{-y}u_h)_{+y} = ((g)_h, v_h)_h, \, \forall v_h \in W_{h,0}(\overline{\Omega}_h).\]
Here, the $(\cdot)_h$ operator is defined as follows
\[(g)_h (x_i,y_j) = \frac{1}{|\square_{ij}|} \iint_{\square_{ij}} g(x)\, dxdy.\]
This defines a discretization that can be seen both as a finite difference and finite element method. It can be shown that under certain smoothness assumptions on $f$ and $g$, the former problem has a unique solution $u_h$ such that
\[\Vert R_h u - u_h \Vert_{1h} \leq C h_{max}^ 2,\]
where
\[\Vert v_h \Vert_{1h} = \sqrt{\Vert v_h \Vert_{h}^2 + \Vert D_{-x} v_h \Vert_{h,x}^2 + \Vert D_{y} v_h \Vert_{h,y}^2}\]
and
\[\begin{aligned} \Vert v_h \Vert_{h} &= \sqrt{(v_h,v_h)_h},\\ \Vert D_{-x} v_h \Vert_{h,x} &= \sqrt{(D_{-x} v_h, D_{-x} v_h)_{+x}},\\ \Vert D_{-y} v_h \Vert_{h,y} &= \sqrt{(D_{-y} v_h, D_{-y} v_h)_{+y}}. \end{aligned}\]
Implementation
We can now use Bramble.jl
to calculate $u_h$. As with any Julia package, we start by importing Bramble.jl
and a few other packages we will need down the line
using Bramble
using LinearSolve # solvers for linear systems
using IncompleteLU: ilu # incomplete LU for preconditioning
We start by building a grid associated with the Poisson equation's domain, as well as, the solution (to impose Dirichlet boundary conditions) and the equation's right hand side g
.
I = interval(0, 1)
Ω = domain(I × I)
Ωₕ = mesh(Ω, (10, 20), (true, false))
The first two lines create a domain object, while the third generates a mesh with 10 and 20 points along the x
and y
directions, respectively. The last input argument encodes that we want a uniform grid in the x
axis and random generated points in the y
axis.
We now move on to prepare and define functions f
and g
associated with the problem as BrambleBareFunction. This is a needed step to prepare these function to work well the the internal machinery of Bramble.jl
:
sol = @embed(Ωₕ, x -> exp(sum(x)))
rhs = @embed(Ωₕ, x -> -2 * sol(x))
The next line of code builds a marker structure with the information the equation has a Dirichlet boundary condition over the boundary of Ωₕ
(the whole boundary is the default option) defined by function sol
.
bc = dirichletbcs(sol)
We now build the gridspace and define the linear
and bilinear
forms associated with the discrete variational problem presented. We create the discrete space with
Wₕ = gridspace(Ωₕ)
and move on to define the bilinear form and assembling the associated matrix
bform = form((uₕ, vₕ) -> inner₊(∇₋ₕ(uₕ), ∇₋ₕ(vₕ)), Wₕ, Wₕ)
A = assemble(bform, bc)
In this case, we provide to the assemble
function the information on boundary conditions in order to have this information encoded in matrix A
.
Next, we create an element in the gridspace whose components are calculated with the averaging operator introduced before
uₕ = element(Wₕ)
avgₕ!(uₕ, rhs)
We now define the linear forms associated with the right hand side
lform = form(vₕ -> innerₕ(uₕ, vₕ), Wₕ)
F = assemble(lform, bc)
and assemble vector F
. The solution of the linear system Ax=F
will provide the components of vector uₕ
, solution of the discrete variational problem. To solve the system, we use GMRES (package LinearSolve.jl) preconditioned with an incomplete LU factorization as precondition (package incompleteLU.jl)
prob = LinearProblem(A, F)
solₕ = LinearSolve.solve(prob, KrylovJL_GMRES(), Pl = ilu(A, τ = 0.0001))
uₕ .= solₕ.u
and calculate the solution. If we want to calculate the error associated with this approximate solution (w.r.t norm₁ₕ), we can follow with
F .= uₕ
Rₕ!(uₕ, sol)
uₕ .-= F
norm₁ₕ(uₕ)
We can even use GLMakie to plot the solution
using GLMakie
x = points(Ωₕ(1))
y = points(Ωₕ(2))
reshaped_uₕ = reshape(uₕ, 10, 20)
fig = Figure(size = (1200, 800))
ax1 = Axis(fig[1, 1], aspect = 1)
ax2 = Axis3(fig[1, 2], aspect = (1, 1, 1),
perspectiveness = 0.5,
elevation = π / 3.5,
azimuth = 0.1π)
wireframe!(ax1, x, y, reshaped_uₕ) # mesh plot
surface!(ax2, x, y, reshaped_uₕ) # solution plot
or export it to a vtk
format to visualize, for instance, with Paraview.
export_file = "surf"
vtk_grid(export_file, x, y, z) do vtk
vtk["poisson"] = vis_uₕ
end
vtk_save(vtk)