Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A package for computing unassembled matrices appearing in $`C^0`$ continuous Galerkin
finite element models.

# Methods
# Matrices in one coordinate (1D)

This package computes matrices of the following form.
```math
Expand Down Expand Up @@ -45,6 +45,10 @@ N_{ijk} = \int^{x_u}_{x_l} P_i(x) Q_j(x) R_k(x) \rho(x) d x = \int^{1}_{-1} S_i(
where $`R_i(x)`$ may be either of $`\Phi(x)`$ or $`d\Phi/dx`$ and
$`U_i(z)`$ may be either of $`l_i(z)`$ or $`(1/s)d l_i(z)/d z`$.

# p-adaptive Gaussian quadrature

In order to guarantee that the matrices $`A_{ij}`$ with non-polynomial kernels $`\rho(x)`$ are calculated with sufficient accuracy, we use p-adaptive quadrature with Gauss-Legendre quadrature rules from `FastGaussQuadrature` at each iteration. We iteratively calculate the matrices with an increasingly large number of quadrature points and iterate until the result is constant up to absolute and relative tolerances. The inputs used to control this process are described in the section on usage below. For polynomial kernels we can deduce the number of quadrature points for exact results, avoiding the need for the iterative calculation in the pure polynomial case.

# Usage

To create these matrices, the user must supply the key grid
Expand Down Expand Up @@ -75,6 +79,16 @@ using FiniteElementMatrices
coordinate = ElementCoordinates(x,scale,shift)
M = finite_element_matrix(lagrange_x,lagrange_x,0,coordinate)
```
Note that the order in which the `@enum` Lagrange polynomials are given corresponds to the indices of `M`. E.g., for a matrix describing a first derivative
```
l_i = lagrange_x
l_j = d_lagrange_dx
P = finite_element_matrix(l_i,l_j,0,coordinate)
```
the number `P[i,j]` corresponds to the matrix element
```math
P_{ij} = \int^{x_u}_{x_l} \Phi_i(x) \frac{d \Phi_j}{d x} d x.
```
For a stiffness matrix describing a second derivative, we could
use the following.
```
Expand Down Expand Up @@ -108,13 +122,36 @@ To specify an arbitrary kernel function $`\rho(x)`$, the following function call
```
coordinate = ElementCoordinates(x,scale,shift)
M = finite_element_matrix(lagrange_x,lagrange_x,coordinate,
kernel_function=(v -> rho(v)),additional_quadrature_points=n)
kernel_function=(v -> rho(v)),additional_quadrature_points=n,
quadrature_increment=m,
max_iterations=q, atol=atol, rtol=rtol)
```
where we specify some function `rho(v)` to be the kernel, and we specify that we `n` additional quadrature
points should be used in addition to the number assumed from the size of the reference grid `x`. If `q` is greater than zero, for each iteration we proceed to calculate the matrix again with a further additional `m` quadrature points, until the matrix is calculated to the specified absolute tolerance `atol` and relative tolerance `rtol`, or we call `error()` with an appropriate message for the user. Note that `rho(v)` should be specified in terms of the physical coordinate including the scale and shift factors `v = scale * x + shift`, not in terms of the reference coordinate `x`.

# Matrices in two coordinates (2D)

It is possible to write down weak forms where the multi-dimensional integrals do not separate into a product of one-dimensional integrals, even for a tensor-product basis. This occurs where the kernel is not a separable function, e.g., for the matrix
```math
K_{ikjn} = \int^{x_{2u}}_{x_{2l}}\int^{x_{1u}}_{x_{1l}} P_i(x_1) R_k(x_2) Q_j(x_1) S_n(x_2)\rho(x_1,x_2) d x_1 d x_2,
```
where we specify some function `rho(v)` to be the kernel, and we specify than `n` additional quadrature
points should be used in addition to the number assumed from the size of the reference grid `x`.
Note that `rho(v)` should be specified in terms of the physical coordinate including the scale and shift
factors `v = scale * x + shift`, not in terms of the reference coordinate `x`.
where $`P_i(x_1)`$, $`R_i(x_2)`$, $`Q_i(x_1)`$, $`S_i(x_2)`$ are from the set of Lagrange polynomials basis
functions (and their first derivatives) in $`x_1`$ and $`x_2`$, respectively.

The syntax that we use to create matrices in two coordinates is as follows
```
l_i = lagrange_x
l_j = lagrange_x
l_k = d_lagrange_dx
l_n = d_lagrange_dx
K_2D = finite_element_matrix(l_i,l_j,coordinate_x1,
l_k,l_n,coordinate_x2;
kernel_function=((x1,x2)->rho(x1,x2)),
max_iterations=10, quadrature_increment=10,
atol=1.0e-13, rtol=1.0e-13)
```
where we have two coordinates, `x1` (with polynomials `l_i` and `l_j`) and `x2` (with polynomials `l_k` and `l_n`). Note that the `@enum` variables used to select the Lagrange polynomials are the same between the two coordinates.
The matrix created above is indexed in the order `K_2D[i,k,j,n]`, indexing over the first polynomial `l_i` of `x1`, then the first polynomial `l_k` of `x2`, then the second polynomial `l_j` of `x1` with the final index representing the polynomial `l_n` of `x2`. The same adaptive quadrature keyword arguments are used as in the 1D case.
# Examples

There are several examples of taking first and second derivatives
Expand Down
221 changes: 184 additions & 37 deletions src/FiniteElementMatrices.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ module FiniteElementMatrices

using LagrangePolynomials: lagrange_poly,
lagrange_poly_derivative,
LagrangePolyData
LagrangePolyData,
LagrangePolyDataBase
using FastGaussQuadrature: gausslegendre

export lagrange_x,
Expand Down Expand Up @@ -49,44 +50,89 @@ struct ElementCoordinates
end
end

function select_lagrange_function(fn_type::LagrangeFunctionType)
function select_lagrange_function(fn_type::LagrangeFunctionType,scale::Float64)
# including the normalisation factors due to v = scale * x + shift
if fn_type == lagrange_x
func = lagrange_poly
func = ((lpoly_data::LagrangePolyDataBase,z::Float64) -> lagrange_poly(lpoly_data,z))
elseif fn_type == d_lagrange_dx
func = lagrange_poly_derivative
# 1/scale due to scale in x in d l(z(x)) /d x
func = ((lpoly_data::LagrangePolyDataBase,z::Float64) -> (1.0/scale)*lagrange_poly_derivative(lpoly_data,z))
end
return func
end

function select_lagrange_prefactor(fn_type::LagrangeFunctionType,
scale::Float64)
if fn_type == lagrange_x
prefactor = 1.0
elseif fn_type == d_lagrange_dx
prefactor = 1.0/scale
function check_power(power::Int64, varname::String)
if power < 0
error("Require power > 0, whereas $varname=$power was passed")
end
return prefactor
return nothing
end
# Method for p-adaptive quadrature in 1D and 2D.
# Note that the the rate at which the number of quadrature
# points is increased is the same for each coordinate.
function finite_element_matrix(args...;
atol=1.0e-12::Float64, rtol=1.0e-12::Float64, max_iterations=1::Int64,
quadrature_increment=5::Int64, verbose=false::Bool, kwargs...)
matrix = _finite_element_matrix(args...;
adaptive_quadrature_points=0,
kwargs...)
iteration = 0
maxvalue = maximum(abs.(matrix))
maxerr = maxvalue
tolerance_exceeded = max_iterations > 0
while iteration < max_iterations && tolerance_exceeded
# increment the iteration
iteration += 1
matrixnew = _finite_element_matrix(args...;
adaptive_quadrature_points=quadrature_increment*iteration,
kwargs...)
maxvalue = maximum(abs.(matrixnew))
maxerr = maximum(abs.(matrixnew .- matrix))
tolerance_exceeded = maxerr > atol + rtol*maxvalue
# update the matrix
matrix .= matrixnew
end
if verbose && !(tolerance_exceeded)
if iteration > 0
println("finite_element_matrix() converged after $iteration iterations \n with maxerr = $maxerr, adaptive_quadrature_points=$(quadrature_increment*iteration) \n")
else
println("finite_element_matrix() returned a matrix without p-adaptive quadrature refinement.")
end
end
if tolerance_exceeded
error("Unable to achieve specified error bounds: \n
maxerr < atol + rtol*maxvalue \n
after max_iterations = $max_iterations with atol=$atol and rtol=$rtol we obtain \n
maxerr = $maxerr > $atol + $rtol*$maxvalue")
end
return matrix
end

# Method for computing polynomial integrals exactly
# for matrices with two Lagrange polymomials in 1D
function finite_element_matrix(
fn1_type::LagrangeFunctionType,
fn2_type::LagrangeFunctionType,
power::Int64,
coordinate::ElementCoordinates
)
return finite_element_matrix(fn1_type, fn2_type, coordinate;
check_power(power, "power")
return _finite_element_matrix(fn1_type, fn2_type, coordinate;
kernel_function=((v -> v^power)),
additional_quadrature_points=power)
end

function finite_element_matrix(
# The basic method for matrices with two Lagrange polymomials in 1D
function _finite_element_matrix(
fn1_type::LagrangeFunctionType,
fn2_type::LagrangeFunctionType,
coordinate::ElementCoordinates;
# function of the "physical" coord v = s z + c
# rather than of the reference coordinate z on [-1,1]
kernel_function=((v -> 1.0))::Function,
additional_quadrature_points=0::Int64,
# argument to permit single implementation of
adaptive_quadrature_points=0::Int64
)
lpoly_data = coordinate.lpoly_data
ngrid = length(coordinate.lpoly_data.x_nodes)
Expand All @@ -95,13 +141,11 @@ function finite_element_matrix(
# the finite element array to be returned
matrix = zeros(Float64,ngrid,ngrid)
# the function objects for the required polynomials
lagrange1 = select_lagrange_function(fn1_type)
lagrange2 = select_lagrange_function(fn2_type)
# the normalisation factors due to v = scale * x + shift
prefactor1 = select_lagrange_prefactor(fn1_type,scale)
prefactor2 = select_lagrange_prefactor(fn2_type,scale)
# nquad chosen for exact results for all power, ngrid
nquad = ngrid + additional_quadrature_points
lagrange1 = select_lagrange_function(fn1_type,scale)
lagrange2 = select_lagrange_function(fn2_type,scale)
# nquad chosen for exact results for default inputs
# with kernel = 1.0 and zero additional quadrature points
nquad = ngrid + additional_quadrature_points + adaptive_quadrature_points
zz, wz = gausslegendre(nquad)
# compute integral
# int P_i(z) Q_j(z) poly(z) s d z
Expand All @@ -116,26 +160,31 @@ function finite_element_matrix(
zzl = zz[l]
funcz = kernel_function(scale*zzl+shift)::Float64
matrix[i,j] += (scale*wz[l]*funcz*
prefactor1*lagrange1(ith_lpoly_data,zzl)*
prefactor2*lagrange2(jth_lpoly_data,zzl))
lagrange1(ith_lpoly_data,zzl)*
lagrange2(jth_lpoly_data,zzl))
end
end
end
return matrix
end

# Method for computing polynomial integrals exactly
# for matrices with three Lagrange polymomials in 1D
function finite_element_matrix(
fn1_type::LagrangeFunctionType,
fn2_type::LagrangeFunctionType,
fn3_type::LagrangeFunctionType,
power::Int64,
coordinate::ElementCoordinates
)
return finite_element_matrix(fn1_type, fn2_type, fn3_type,
check_power(power, "power")
return _finite_element_matrix(fn1_type, fn2_type, fn3_type,
coordinate; kernel_function=((v -> v^power)),
additional_quadrature_points=power)
end
function finite_element_matrix(

# The basic method for matrices with three Lagrange polymomials in 2D
function _finite_element_matrix(
fn1_type::LagrangeFunctionType,
fn2_type::LagrangeFunctionType,
fn3_type::LagrangeFunctionType,
Expand All @@ -144,6 +193,7 @@ function finite_element_matrix(
# rather than of the reference coordinate z on [-1,1]
kernel_function=((v -> 1.0))::Function,
additional_quadrature_points=0::Int64,
adaptive_quadrature_points=0::Int64
)
lpoly_data = coordinate.lpoly_data
ngrid = length(coordinate.lpoly_data.x_nodes)
Expand All @@ -152,15 +202,12 @@ function finite_element_matrix(
# the finite element array to be returned
matrix = zeros(Float64,ngrid,ngrid,ngrid)
# the function objects for the required polynomials
lagrange1 = select_lagrange_function(fn1_type)
lagrange2 = select_lagrange_function(fn2_type)
lagrange3 = select_lagrange_function(fn3_type)
# the normalisation factors due to v = scale * x + shift
prefactor1 = select_lagrange_prefactor(fn1_type,scale)
prefactor2 = select_lagrange_prefactor(fn2_type,scale)
prefactor3 = select_lagrange_prefactor(fn3_type,scale)
# nquad chosen for exact results for all power, ngrid
nquad = 2*ngrid + additional_quadrature_points
lagrange1 = select_lagrange_function(fn1_type,scale)
lagrange2 = select_lagrange_function(fn2_type,scale)
lagrange3 = select_lagrange_function(fn3_type,scale)
# nquad chosen for exact results for default inputs
# with kernel = 1.0 and zero additional quadrature points
nquad = 2*ngrid + additional_quadrature_points + adaptive_quadrature_points
zz, wz = gausslegendre(nquad)
# compute integral
# int P_i(z) Q_j(z) S_k(z) poly(z) d z
Expand All @@ -175,16 +222,116 @@ function finite_element_matrix(
ith_lpoly_data = lpoly_data.lpoly_data[i]
for l in 1:nquad
zzl = zz[l]
funcz = kernel_function(scale*zzl+shift)
funcz = kernel_function(scale*zzl+shift)::Float64
matrix[i,j,k] += (scale*wz[l]*funcz*
prefactor1*lagrange1(ith_lpoly_data,zzl)*
prefactor2*lagrange2(jth_lpoly_data,zzl)*
prefactor3*lagrange3(kth_lpoly_data,zzl))
lagrange1(ith_lpoly_data,zzl)*
lagrange2(jth_lpoly_data,zzl)*
lagrange3(kth_lpoly_data,zzl))
end
end
end
end
return matrix
end

# Method for computing polynomial integrals exactly
# for matrices with two 2D basis functions
# (two Lagrange polymomials per coordinate) in 2D
function finite_element_matrix(
fn1_x1_type::LagrangeFunctionType,
fn2_x1_type::LagrangeFunctionType,
power_x1::Int64,
coordinate_x1::ElementCoordinates,
fn1_x2_type::LagrangeFunctionType,
fn2_x2_type::LagrangeFunctionType,
power_x2::Int64,
coordinate_x2::ElementCoordinates
)
check_power(power_x1, "power_x1")
check_power(power_x2, "power_x2")
return _finite_element_matrix(fn1_x1_type, fn2_x1_type, coordinate_x1,
fn1_x2_type, fn2_x2_type, coordinate_x2;
kernel_function=((v1,v2) -> (v1^power_x1)*(v2^power_x2)),
additional_quadrature_points_x1=power_x1,
additional_quadrature_points_x2=power_x2)
end

# Basic method for computing matrices with two 2D basis functions
# (two Lagrange polymomials per coordinate) in 2D
function _finite_element_matrix(
fn1_x1_type::LagrangeFunctionType,
fn2_x1_type::LagrangeFunctionType,
coordinate_x1::ElementCoordinates,
fn1_x2_type::LagrangeFunctionType,
fn2_x2_type::LagrangeFunctionType,
coordinate_x2::ElementCoordinates;
# function of the "physical" coord v = s z + c
# rather than of the reference coordinate z on [-1,1]
kernel_function=((v1,v2) -> 1.0)::Function,
additional_quadrature_points_x1=0::Int64,
additional_quadrature_points_x2=0::Int64,
adaptive_quadrature_points=0::Int64
)
# coordinate x1 data
lpoly_data_x1 = coordinate_x1.lpoly_data
ngrid_x1 = length(coordinate_x1.lpoly_data.x_nodes)
scale_x1 = coordinate_x1.scale
shift_x1 = coordinate_x1.shift
# coordinate x2 data
lpoly_data_x2 = coordinate_x2.lpoly_data
ngrid_x2 = length(coordinate_x2.lpoly_data.x_nodes)
scale_x2 = coordinate_x2.scale
shift_x2 = coordinate_x2.shift
# the finite element array to be returned
matrix = zeros(Float64,ngrid_x1,ngrid_x2,ngrid_x1,ngrid_x2)
# the function objects for the required polynomials
lagrange11 = select_lagrange_function(fn1_x1_type,scale_x1)
lagrange12 = select_lagrange_function(fn1_x2_type,scale_x2)
lagrange21 = select_lagrange_function(fn2_x1_type,scale_x1)
lagrange22 = select_lagrange_function(fn2_x2_type,scale_x2)
# the normalisation factors due to v = scale * x + shift
# nquad chosen for exact results for default inputs
# with kernel = 1.0 and zero additional quadrature points
nquad_x1 = ngrid_x1 + additional_quadrature_points_x1 + adaptive_quadrature_points
zz_x1, wz_x1 = gausslegendre(nquad_x1)
nquad_x2 = ngrid_x2 + additional_quadrature_points_x2 + adaptive_quadrature_points
zz_x2, wz_x2 = gausslegendre(nquad_x2)
# compute integral
# \int \int \left(P1_i(z_1) Q1_j(z_1) P2_i(z_1) Q2_j(z_2)
# kernel(s_1 z_1 + c_1, s_2 z_2 + c_2) \right) s_1 s_2 d z_1 d z_2
# s = scale, c = shift and
# P_i(z), Q_i(z) in [l_i(z), (1/s) d l_i(z) / d z]
for jx2 in 1:ngrid_x2
jx2_lpoly_data = lpoly_data_x2.lpoly_data[jx2]
for jx1 in 1:ngrid_x1
jx1_lpoly_data = lpoly_data_x1.lpoly_data[jx1]
for ix2 in 1:ngrid_x2
ix2_lpoly_data = lpoly_data_x2.lpoly_data[ix2]
for ix1 in 1:ngrid_x1
ix1_lpoly_data = lpoly_data_x1.lpoly_data[ix1]
for lx2 in 1:nquad_x2
zzl2 = zz_x2[lx2]
wgt2 = scale_x2*wz_x2[lx2]
lagrange_factor2 = (lagrange12(ix2_lpoly_data,zzl2)*
lagrange22(jx2_lpoly_data,zzl2))
for lx1 in 1:nquad_x1
zzl1 = zz_x1[lx1]
wgt1 = scale_x1*wz_x1[lx1]
lagrange_factor1 = (lagrange11(ix1_lpoly_data,zzl1)*
lagrange21(jx1_lpoly_data,zzl1))
kernel_x1_x2 = kernel_function(scale_x1*zzl1+shift_x1,
scale_x2*zzl2+shift_x2)::Float64
matrix[ix1,ix2,jx1,jx2] += (wgt1*wgt2*kernel_x1_x2*
lagrange_factor1*
lagrange_factor2)
end
end
end
end
end
end
return matrix
end


end
Loading