|
| 1 | +# In this tutorial, we will learn |
| 2 | +# |
| 3 | +# - How to define a `ReferenceFEName` type to tag a new element family |
| 4 | +# - How to construct a polynomial prebasis using `MonomialBasis` |
| 5 | +# - How to encode moment-based DOFs as weighted integrals over element faces |
| 6 | +# - How to implement a moment integrand σ(φ,μ,ds) using Gridap's `Field` algebra |
| 7 | +# - How to assemble a complete `ReferenceFE` via `MomentBasedReferenceFE` |
| 8 | +# - How to plug the element into the standard `ReferenceFE` dispatch system |
| 9 | +# - How to verify the implementation against the built-in CR element and via convergence |
| 10 | + |
| 11 | +using Gridap |
| 12 | +using Gridap.ReferenceFEs |
| 13 | +using Gridap.Polynomials |
| 14 | +using Gridap.Fields |
| 15 | + |
| 16 | +# |
| 17 | +# ## What constitutes a ReferenceFE |
| 18 | +# |
| 19 | +# A `ReferenceFE{D}` in Gridap encodes everything the assembly loop needs to |
| 20 | +# know about a single element type on a reference polytope: |
| 21 | +# |
| 22 | +# | Field | Role | |
| 23 | +# |-------|------| |
| 24 | +# | `prebasis` | polynomial space $\mathcal{P}$, $\dim\mathcal{P} = n$ | |
| 25 | +# | `dofs` | $n$ linear functionals $\sigma_1,\ldots,\sigma_n$ on $\mathcal{P}$ | |
| 26 | +# | `shapefuns` | dual basis $\varphi_j$: $\sigma_i(\varphi_j)=\delta_{ij}$ | |
| 27 | +# | `face_own_dofs` | which DOF indices belong to each face of the polytope | |
| 28 | +# | `conformity` | how adjacent cells share DOFs across facets | |
| 29 | +# |
| 30 | +# The shape functions are the columns of $\mathbf{M}^{-1}$ where |
| 31 | +# $M_{ij}=\sigma_i(p_j)$ is the Vandermonde matrix of the prebasis $\{p_j\}$. |
| 32 | +# Gridap's `MomentBasedReferenceFE` constructor handles the inversion and |
| 33 | +# assembles the full struct, provided you supply the prebasis and a description |
| 34 | +# of the DOFs as moment integrals over faces. |
| 35 | + |
| 36 | +# |
| 37 | +# ## The Crouzeix–Raviart element |
| 38 | +# |
| 39 | +# The Crouzeix–Raviart (CR) element [1] is the canonical lowest-order |
| 40 | +# nonconforming scalar element on simplices. On a $D$-simplex $K$ it pairs |
| 41 | +# $\mathcal{P}_1(K)$ (dimension $D+1$) with one DOF per facet $F_i$: |
| 42 | +# |
| 43 | +# ```math |
| 44 | +# \sigma_{F_i}(\varphi) = \frac{1}{|F_i|}\int_{F_i}\varphi\;dF, |
| 45 | +# \qquad i=1,\ldots,D+1. |
| 46 | +# ``` |
| 47 | +# |
| 48 | +# A simplex has exactly $D+1$ facets, so the set is unisolvent. |
| 49 | +# Global CR functions are only weakly continuous across inter-element boundaries |
| 50 | +# — adjacent cells agree on facet averages, not on pointwise values. |
| 51 | +# This places global functions in the broken space $H^1_h(\mathcal{T}_h)$ |
| 52 | +# rather than $H^1(\Omega)$. |
| 53 | + |
| 54 | +# |
| 55 | +# ## Step 1 — The ReferenceFEName type |
| 56 | +# |
| 57 | +# Every element family is identified by a singleton subtype of `ReferenceFEName`. |
| 58 | +# The type carries no data; it is a pure dispatch tag. |
| 59 | +# The constants you already know from high-level drivers — `lagrangian`, |
| 60 | +# `raviart_thomas`, `nedelec` — are exactly this: singleton instances of their |
| 61 | +# respective `ReferenceFEName` subtypes, used as the second argument to |
| 62 | +# `ReferenceFE(polytope, name, T, order)`. |
| 63 | + |
| 64 | +struct MyCrouzeixRaviart <: ReferenceFEName end |
| 65 | +const my_crouzeix_raviart = MyCrouzeixRaviart() |
| 66 | + |
| 67 | +# |
| 68 | +# ## Step 2 — The polynomial prebasis |
| 69 | +# |
| 70 | +# `MonomialBasis(Val(D), T, order, filter)` builds the basis of scalar monomials |
| 71 | +# in `D` variables of type `T` up to degree `order`, keeping only monomials |
| 72 | +# $x^\alpha$ for which `filter(α,order)` is true. The standard |
| 73 | +# complete-polynomial filter `Polynomials._p_filter` accepts $|\alpha|\le\text{order}$, |
| 74 | +# spanning $\mathcal{P}_\text{order}$. Two other built-in filters are |
| 75 | +# `Polynomials._q_filter` (tensor-product $\mathcal{Q}_\text{order}$: each |
| 76 | +# component $\alpha_i\le\text{order}$, used for Lagrangian elements on quads/hexes) |
| 77 | +# and `Polynomials._s_filter` (serendipity space: drops high-degree corner monomials |
| 78 | +# to reduce the DOF count on quads/hexes while retaining full approximation order). |
| 79 | + |
| 80 | +D = 2 |
| 81 | +T = Float64 |
| 82 | +p = TRI |
| 83 | + |
| 84 | +prebasis = MonomialBasis(Val(D), T, 1, Polynomials._p_filter) |
| 85 | + |
| 86 | +# On a triangle $\mathcal{P}_1$ has dimension $D+1=3$, spanned by |
| 87 | +# $\{1,\,x_1,\,x_2\}$: |
| 88 | + |
| 89 | +length(prebasis) |
| 90 | + |
| 91 | +# Evaluating at the centroid $(1/3,1/3)$ of the reference triangle confirms |
| 92 | +# the three monomials: |
| 93 | + |
| 94 | +evaluate(prebasis, [Point(1/3, 1/3)]) |
| 95 | + |
| 96 | +# |
| 97 | +# ## Step 3 — The moment test basis on facets |
| 98 | +# |
| 99 | +# Each DOF integral $\int_F \varphi\cdot\mu\,dF$ involves a test polynomial |
| 100 | +# $\mu$ defined on the facet $F$. For CR, $\mu=1$ ($\mathcal{P}_0$), so we |
| 101 | +# need one constant basis function per facet. Since a facet is a |
| 102 | +# $(D-1)$-simplex, we build a `MonomialBasis` in $D-1$ variables of degree 0: |
| 103 | + |
| 104 | +fb = MonomialBasis(Val(D-1), T, 0, Polynomials._p_filter) |
| 105 | +length(fb) |
| 106 | + |
| 107 | +# There is exactly one moment per facet, so the total DOF count is 3 — matching |
| 108 | +# the three edges of `TRI` and the dimension of $\mathcal{P}_1$. |
| 109 | + |
| 110 | +# |
| 111 | +# ## Step 4 — The moment integrand |
| 112 | +# |
| 113 | +# A moment descriptor is a triplet `(face_range, σ, μ)` where |
| 114 | +# - `face_range` selects which faces of the polytope carry the moments |
| 115 | +# (an index range into the global face list of `p`), |
| 116 | +# - `σ(φ,μ,ds)` is the integrand (linear in both `φ` and `μ`), |
| 117 | +# - `μ` is the test basis on the face. |
| 118 | +# |
| 119 | +# When `MomentBasedReferenceFE` builds the DOF basis it calls `σ` with: |
| 120 | +# - `φ`: the prebasis restricted and pulled back to the current face, |
| 121 | +# - `μ`: the test basis in the face reference domain, |
| 122 | +# - `ds`: a `FaceMeasure` carrying `cpoly` (cell polytope), `face` (local face |
| 123 | +# index), and the quadrature and face-to-cell coordinate map. |
| 124 | +# |
| 125 | +# `σ` must return a `Field` (or array of `Field`s) representing the integrand. |
| 126 | +# No numerical values are computed here; the framework evaluates the result at |
| 127 | +# quadrature points internally. |
| 128 | +# |
| 129 | +# The CR integrand $\sigma_F(\varphi) = |F|^{-1}\int_F\varphi\,dF$ is: |
| 130 | + |
| 131 | +function fmom(φ, μ, ds) |
| 132 | + D = num_dims(ds.cpoly) |
| 133 | + facet_meas = ReferenceFEs._get_dfaces_measure(ds.cpoly, D-1) # vector of facet measures |F| |
| 134 | + inv_meas = Fields.ConstantField(1 / facet_meas[ds.face]) |
| 135 | + φμ = Broadcasting(Operation(⋅))(φ, μ) # pointwise product of Field arrays |
| 136 | + Broadcasting(Operation(*))(φμ, inv_meas) # scale by 1/|F| |
| 137 | +end |
| 138 | + |
| 139 | +# `_get_dfaces_measure` is an internal Gridap helper that returns the |
| 140 | +# $d$-volume of each $d$-face of a polytope (edge lengths, face areas, etc.). |
| 141 | +# |
| 142 | +# `Broadcasting(Operation(f))` lifts a binary scalar operation `f` to act |
| 143 | +# element-wise on arrays of `Field`s, returning a new array of `Field`s. |
| 144 | +# The computation is deferred: the resulting expression tree is only evaluated |
| 145 | +# at quadrature points when `MomentBasedDofBasis` assembles the Vandermonde matrix. |
| 146 | + |
| 147 | +# |
| 148 | +# ## Step 5 — Assembling the ReferenceFE |
| 149 | +# |
| 150 | +# `get_dimrange(p, d)` returns the range of face indices for all $d$-faces of |
| 151 | +# `p`; passing `D-1` selects all edges (facets of a triangle). |
| 152 | + |
| 153 | +moments = Tuple[ |
| 154 | + (get_dimrange(p, D-1), fmom, fb), |
| 155 | +] |
| 156 | + |
| 157 | +my_cr = ReferenceFEs.MomentBasedReferenceFE(my_crouzeix_raviart, p, prebasis, moments, L2Conformity()) |
| 158 | + |
| 159 | +# Inspecting the face-DOF ownership confirms one DOF per edge (faces 4–6 in |
| 160 | +# the TRI face numbering, after 3 vertices and the cell interior) and none on |
| 161 | +# vertices or interior: |
| 162 | + |
| 163 | +get_face_own_dofs(my_cr) |
| 164 | + |
| 165 | +# The CR shape functions are the unique degree-1 polynomials whose average |
| 166 | +# over each edge is a Kronecker delta: $\sigma_i(\varphi_j)=\delta_{ij}$. |
| 167 | +# We verify this by evaluating the DOF functionals against the shape functions |
| 168 | +# — the result should be the $3\times 3$ identity: |
| 169 | + |
| 170 | +dof_basis = get_dof_basis(my_cr) |
| 171 | +shp = get_shapefuns(my_cr) |
| 172 | +evaluate(dof_basis, shp) |
| 173 | + |
| 174 | +# It can also be instructive to compare the shape functions against the prebasis. |
| 175 | +# The Vandermonde matrix $M_{ij} = \sigma_i(p_j)$ (DOFs applied to monomials) |
| 176 | +# encodes how the shape functions are formed as linear combinations of monomials: |
| 177 | + |
| 178 | +evaluate(dof_basis, prebasis) |
| 179 | + |
| 180 | +# |
| 181 | +# ## Comparing with the built-in implementation |
| 182 | +# |
| 183 | +# Gridap ships its own `CrouzeixRaviartRefFE`. Both DOF bases must produce the |
| 184 | +# same Vandermonde matrix when applied to the common prebasis: |
| 185 | + |
| 186 | +cr_builtin = CrouzeixRaviartRefFE(Float64, TRI, 1) |
| 187 | +dofs_builtin = get_dof_basis(cr_builtin) |
| 188 | + |
| 189 | +M_mine = evaluate(dof_basis, prebasis) |
| 190 | +M_ref = evaluate(dofs_builtin, prebasis) |
| 191 | +@assert M_mine ≈ M_ref |
| 192 | + |
| 193 | +# |
| 194 | +# ## Step 6 — Registering the dispatch hook |
| 195 | +# |
| 196 | +# For the element to be usable through the standard `ReferenceFE(polytope, name, T, order)` |
| 197 | +# API (and hence in `FESpace`, `TestFESpace`, etc.) we add one method. |
| 198 | + |
| 199 | +function ReferenceFE(p::Polytope, ::MyCrouzeixRaviart, ::Type{T}, order) where T |
| 200 | + @assert is_simplex(p) && order == 1 "MyCrouzeixRaviart is only defined for simplices at order 1" |
| 201 | + D = num_dims(p) |
| 202 | + pre = MonomialBasis(Val(D), T, order, Polynomials._p_filter) |
| 203 | + fb = MonomialBasis(Val(D-1), T, 0, Polynomials._p_filter) |
| 204 | + function σ(φ, μ, ds) |
| 205 | + d = num_dims(ds.cpoly) |
| 206 | + fm = ReferenceFEs._get_dfaces_measure(ds.cpoly, d-1) |
| 207 | + scale = Fields.ConstantField(1 / fm[ds.face]) |
| 208 | + Broadcasting(Operation(*))(Broadcasting(Operation(⋅))(φ, μ), scale) |
| 209 | + end |
| 210 | + moms = Tuple[(get_dimrange(p, D-1), σ, fb)] |
| 211 | + ReferenceFEs.MomentBasedReferenceFE(my_crouzeix_raviart, p, pre, moms, L2Conformity()) |
| 212 | +end |
| 213 | + |
| 214 | +# We also need a `get_face_own_dofs` override for `L2Conformity`. Without it, |
| 215 | +# the default `L2Conformity` dispatch places all DOFs on the cell interior, |
| 216 | +# which would make adjacent cells independent and break the nonconforming |
| 217 | +# inter-element coupling that CR relies on. |
| 218 | + |
| 219 | +function ReferenceFEs.get_face_own_dofs(reffe::GenericRefFE{MyCrouzeixRaviart}, ::L2Conformity) |
| 220 | + get_face_own_dofs(reffe) |
| 221 | +end |
| 222 | + |
| 223 | +# This one-liner redirects to the stored `face_own_dofs` (the moment-derived |
| 224 | +# facet ownership), so that when the `FESpace` machinery builds the global DOF |
| 225 | +# numbering, it recognises that the edge DOF is shared between the two adjacent |
| 226 | +# cells — exactly the nonconforming H1 interpretation. |
| 227 | + |
| 228 | +# |
| 229 | +# ## Verification: Poisson convergence |
| 230 | +# |
| 231 | +# We solve |
| 232 | +# ```math |
| 233 | +# -\Delta u = f \quad\text{in } \Omega=[0,1]^2, \qquad u=0 \quad\text{on }\partial\Omega, |
| 234 | +# ``` |
| 235 | +# with manufactured solution $u(x)=\sin(\pi x_1)\sin(\pi x_2)$ and |
| 236 | +# $f=2\pi^2 u$. For the nonconforming CR discretisation (broken $H^1$ bilinear |
| 237 | +# form, no penalty), the expected convergence rates are $O(h^2)$ in $L^2$ and |
| 238 | +# $O(h)$ in the broken $H^1$ seminorm [1]. |
| 239 | + |
| 240 | +u_ex(x) = sin(π*x[1]) * sin(π*x[2]) |
| 241 | +f_ex(x) = 2π^2 * sin(π*x[1]) * sin(π*x[2]) |
| 242 | + |
| 243 | +function solve_cr_poisson(n) |
| 244 | + model = simplexify(CartesianDiscreteModel((0,1,0,1),(n,n))) |
| 245 | + reffe = ReferenceFE(TRI, my_crouzeix_raviart, Float64, 1) |
| 246 | + V = TestFESpace(model, reffe; conformity=:L2, dirichlet_tags="boundary") |
| 247 | + U = TrialFESpace(V, u_ex) |
| 248 | + |
| 249 | + Ω = Triangulation(model) |
| 250 | + dΩ = Measure(Ω, 4) |
| 251 | + |
| 252 | + a(u,v) = ∫( ∇(u)⊙∇(v) )dΩ |
| 253 | + l(v) = ∫( f_ex * v )dΩ |
| 254 | + |
| 255 | + op = AffineFEOperator(a, l, U, V) |
| 256 | + uh = solve(op) |
| 257 | + |
| 258 | + e = u_ex - uh |
| 259 | + el2 = sqrt(sum( ∫(e*e)dΩ )) |
| 260 | + eh1 = sqrt(sum( ∫(∇(e)⊙∇(e))dΩ )) |
| 261 | + return el2, eh1 |
| 262 | +end |
| 263 | + |
| 264 | +# Run on a sequence of successively refined meshes and compute convergence rates: |
| 265 | + |
| 266 | +ns = [4, 8, 16] |
| 267 | +errs = [solve_cr_poisson(n) for n in ns] |
| 268 | + |
| 269 | +for i in 2:length(ns) |
| 270 | + r_l2 = log(errs[i-1][1]/errs[i][1]) / log(2) |
| 271 | + r_h1 = log(errs[i-1][2]/errs[i][2]) / log(2) |
| 272 | + println("n=$(ns[i]): L² rate ≈ $(round(r_l2,digits=2)), broken H¹ rate ≈ $(round(r_h1,digits=2))") |
| 273 | +end |
| 274 | + |
| 275 | +# |
| 276 | +# ## References |
| 277 | +# |
| 278 | +# [1] M. Crouzeix, P.-A. Raviart. *Conforming and nonconforming finite element |
| 279 | +# methods for solving the stationary Stokes equations I.* |
| 280 | +# RAIRO Anal. Numér., 7(R-3):33–75, 1973. |
| 281 | +# doi:[10.1051/m2an/197307R300331](http://dx.doi.org/10.1051/m2an/197307R300331) |
| 282 | + |
| 283 | +# |
| 284 | +# ## Further reading |
| 285 | +# |
| 286 | +# The following source files in `Gridap.jl` are the most relevant to browse |
| 287 | +# alongside this tutorial: |
| 288 | +# |
| 289 | +# - `src/ReferenceFEs/MomentBasedReferenceFEs.jl` — `MomentBasedDofBasis`, |
| 290 | +# `FaceMeasure`, and `MomentBasedReferenceFE`; the core machinery used here. |
| 291 | +# - `src/ReferenceFEs/CrouzeixRaviartRefFEs.jl` — the built-in CR element |
| 292 | +# whose logic this tutorial re-derives step by step. |
| 293 | +# - `src/ReferenceFEs/RaviartThomasRefFEs.jl` — a more involved moment-based |
| 294 | +# element: vector-valued prebasis, $H(\text{div})$ conformity, and normal-trace |
| 295 | +# moments instead of plain averages. |
| 296 | +# - `src/ReferenceFEs/NedelecRefFEs.jl` — $H(\text{curl})$ analogue of RT; |
| 297 | +# tangential-trace moments, useful as a contrast to the normal-trace case. |
| 298 | +# - `src/ReferenceFEs/LagrangianRefFEs.jl` — point-evaluation DOFs (the |
| 299 | +# `LagrangianDofBasis` counterpart to `MomentBasedDofBasis`). |
| 300 | +# - `src/ReferenceFEs/ReferenceFEInterfaces.jl` — abstract interface: |
| 301 | +# `ReferenceFE`, `ReferenceFEName`, `Conformity`, and `GenericRefFE`. |
| 302 | +# - `src/Polynomials/MonomialBases.jl` — `MonomialBasis` and the three |
| 303 | +# polynomial filters (`_p_filter`, `_q_filter`, `_s_filter`). |
0 commit comments