femR
is an R package that solves PDEs on R relying on
the Finite Element Method (FEM). Let \(\Omega
\subset \mathbb{R}^2\) be the domain of our interest,
femR
package solves advection-reaction-diffusion problem of
the following form:
\[
\begin{cases}
-\mu \Delta u + \boldsymbol{b} \cdot \nabla u + a u= f \qquad & in
\ \Omega \\
boundary \ conditions \ (Dirichlet \ or \ Neumann) \qquad &
on \ \partial \Omega,
\end{cases}
\] where \(\mu \in \mathbb{R}\),
\(\boldsymbol{b} \in \mathbb{R}^2\),
\(a \in \mathbb{R}\) are the diffusion
coefficient, the transport coefficient and the reaction coefficient,
respectively. This vignette illustrates how to use femR
to
solve a partial differential equation. In particular, the following
section explains step by step the solution of a simple Poisson
problem.
Solving a Poisson problem
Let \(\Omega\) be the unit square \([0,1]^2\). Consider the following problem defined over \(\Omega\):
\[ \begin{cases} -\Delta u = f &in \ \Omega \\ \quad u = 0 &on \ \partial \Omega \end{cases} \]
where \(f(x,y) = 8\pi^2 \sin( 2\pi x)\sin(2\pi y)\) is the forcing term and \(\partial \Omega\) is the boundary of \(\Omega\) where we have prescribed homogeneous Dirichelet boundary condition. The exact solution of the previous problem is \(u_{ex}(x,y) = \sin(2\pi x) \sin(2 \pi y)\). The following figure shows the exact solution of the problem, on the left hand side, and the triangulation of the unit square domain, on the right hand side.
The following window wraps all the steps needed to solve the problem.
## 1. Defining domain relying on RTriangle
p <- pslg(P=rbind(c(0, 0), c(1, 0), c(1, 1), c(0, 1)),
S=rbind(c(1, 2), c(2, 3), c(3, 4), c(4,1)))
unit_square <- triangulate(p, a = 0.00125, q=30)
mesh <- Mesh(unit_square)
## 2. Defining the solution of the PDE
Vh <- FunctionSpace(mesh, fe_order=1)
u <- Function(Vh)
## 3. Defining the differential operator
Lu <- -laplace(u)
## 4. Defining the forcing term and Dirichlet boundary conditions
## as standard R functions
# forcing term
f <- function(points){
return(8.*pi^2* sin(2.*pi*points[,1])*sin(2.*pi*points[,2]))
}
# Dirichlet boundary conditions
g <- function(points){
return(matrix(0,nrow=nrow(points), ncol=1))
}
## 5. Building the PDE object
pde <- Pde(Lu, f)
# setting boundary conditions
pde$set_boundary_condition(type="dirichlet",
boundary_condition=g)
## 7. computing the discrete solution
pde$solve()
## 8. Plots
plot(u)
fig_h <- contour(u)
subplot(fig_exact %>%hide_colorbar(), fig_h,
margin = 0.05) %>%layout(annotations = list(
list(x=0.155, y=1.05, text = "exact solution", showarrow = F,
xref='paper', yref='paper'),
list(x=0.875, y=1.05, text = "discrete solution", showarrow = F,
xref='paper', yref='paper')))
The following sections explain more in detail the previous chunk of code.
Defining the domain
First, we create a proper domain object exploiting the function
Mesh
which takes a R named list as parameter. Such named
list should contain geometrical information of the domain:
nodes
, a 2-columns matrix containing the coordinates of the nodes of the mesh.elements
, a 3-columns matrix that for each row \(i\) contains the indexes of the mesh nodes that are vertices of the \(i\)-th element.boundary
, a 1-column matrix that indicates the nodes on the boundary of the domain.
femR
contains the unit_square
named list
that can be used to perform some tests.
femR
package can read also Delaunay triangulations
provided by RTriangle
package, as the following chunk
shows:
library(RTriangle)
## create a Planar Straigh Line Graph object (RTriangle)
pslg <- pslg(P=rbind(c(0, 0), c(1, 0), c(1, 1), c(0, 1)),
S=rbind(c(1, 2), c(2, 3), c(3, 4), c(4,1)))
## triangulate it using RTriangle
# a = maximum triangle area
# q = minimum triangle angle in degrees
unit_square <- triangulate(pslg, a=0.00125, q=30)
## create mesh object
mesh <- Mesh(unit_square)
plot(mesh)
Moreover, femR
package provides an utility that reads
files storing meshes provided by third-party software such as
FreeFem++
. Indeed, read_mesh(filename)
reads a
.mesh file from FreeFem++
and returns a named list that can
be passed to Mesh
function. The following chunk, that is
not executed, shows how read_mesh
works.
Defining the solution of the PDE and the differential operator
First, we initialize a FunctionSpace
object passing the
mesh that we previously build and the finite element order. Note that,
the package provide first order and second order finite elements. First
order finite element is the default parameter. Then, we define a
Function
, solution of the PDEs, belonging to the function
space Vh
.
## solution of the PDE
Vh <- FunctionSpace(mesh, fe_order=1)
u <- Function(Vh)
The following figure shows a basis function of the space
Vh
, i.e. the space of functions which are globally
continuous and are polynomials of degree 1 once restricted to each
element of the triangulation.
Then, we define the differential operator in its strong formulation.
Lu <- -laplace(u) # L <- -div(I*grad(u))
# In general:
# L <- -mu*laplace(u) + dot(b,grad(u)) + a*u
Note that, according to the well-known identity \(div(\nabla u) = \Delta u\), we could have
written L<--div(grad(u))
or
L<--div(I*grad(u))
, where I
is the
2-dimensional identity matrix. Indeed, femR
package let you
define every term of a generic second-order linear differential operator
\(Lu= -div(K\nabla u) + \mathbf{b}\cdot \nabla
u + a u\) as follows:
Diffusion:
-div(K*grad(u))
or-mu*laplacian(u)
, where eitherK
ormu
represents the diffusion matrix in an anisotropic problem or the diffusion coefficient in a isotropic problem, implements the first term of the differential operator according to the problem at hand.Advection:
dot(b,grad(u))
, whereb
is a two-dimensional vector, implements the second term of differential operator.Reaction:
a*u
, wherea
is a scalar, implements the last term of the differential operator.
Although, the interface let the user define a generic second-order
linear differential operator: -div(K*grad(u)) + ...
, at
this stage the package has been tested only in the isotropic case, i.e
K=I
, where I
is the two-dimensional identity
matrix.
Building the PDE object
We define the forcing term of the differential problem and the
Dirichlet boundary condition as simple R function
s. The
forcing term will be provided as parameter to the object which
encapsulates the concept of PDEs.
Computing the FE solution
Finally, we build a pde
object passing the differential
operator L
, as first parameter and the forcing term
f
, as second parameter:
## create pde
pde <- Pde(Lu, f)
## set Dirichlet boundary conditions
pde$set_boundary_condition(type="dirichlet",
boundary_condition=g)
We can compute the discrete solution of the Poisson problem calling
the solve
method:
## solve problem
pde$solve()
Since, we are dealing with a simple differential problem and we know
the analytic form of the exact solution, we can compute the \(L^2\) norm of the error exploiting on the
mass matrix provided by the pde
object:
Graphics
The R package plotly
is exploited to plot the computed
solution.
## plot solution
plot(u)
# contour
contour(u)
Note that, the base::plot
function and
graphics::contour
function have both been overloaded and
return a plotly
object that can be modified as one wishes.
Solving a parabolic problem
Let \(\Omega\) be the unit square \([0,1]^2\). Consider the following problem defined over \(\Omega \times [0,T]\):
\[ \begin{cases} \frac{\partial u}{\partial t}-\Delta u = f &in \ \Omega \times [0,T] \\ \quad u = u_0 &in \ \Omega, \ t=0 \\ \quad u = 0 &on \ \partial \Omega,\ t>0 \end{cases} \]
where \(u_0 = \sin( 2\pi x)\sin(2\pi y)\) is the initial condition, \(f(x,y) = 8\pi^2 \sin( 2\pi x)\sin(2\pi y) e^{-t}\) is the forcing term and \(\partial \Omega\) is the boundary of \(\Omega\) where we have prescribed homogeneous Dirichelet boundary condition for all \(t\) grater than \(0\). The exact solution of the previous problem is \(u_{ex}(x,y) = \sin(2\pi x) \sin(2 \pi y) e^{-t}\). The following window wraps all the steps needed to solve the problem.
## spatio-temporal domain
nodes <- rbind(c(0,0), c(1,0), c(1,1), c(0,1))
edges <- cbind(1:nrow(nodes), c(2:nrow(nodes),1))
domain <- Domain(list(nodes=nodes, edges=edges))
domain <- domain %X% c(0,1)
## build mesh
mesh <- build_mesh(domain, maximum_area = 0.00125, minimum_angle = 20)
# time step
dT = 1e-2
mesh$set_time_step(dT)
## create Functional Space
Vh <- FunctionSpace(mesh)
## define differential operator in its strong formulation
u <- Function(Vh)
## define differential operator in its strong formulation
Lu <- dt(u) - laplace(u)
## forcing term
f <- function(points,times){
res <- matrix(0, nrow=nrow(points), ncol=length(times))
for( t in 1:length(times)){
res[,t] = (8*pi^2 -1) * sin( 2.* pi * points[,1]) * sin(2.*pi* points[,2]) * exp(-times[t])
}
return(res)
}
## initial condition
u0 <- function(points){
return(sin( 2.* pi * points[,1]) * sin(2.*pi* points[,2]))
}
## create pde
pde <- Pde(Lu, f)
# set initial condition and dirichlet BC
pde$set_initial_condition(u0)
pde$set_boundary_condition(boundary_condition= 0.0, type="dirichlet")
## solve problem
pde$solve()
## plot solution
image(u)