Skip to content

Commit 072715a

Browse files
committed
Added new CR low-level tutorial
1 parent b2cf203 commit 072715a

2 files changed

Lines changed: 304 additions & 0 deletions

File tree

deps/build.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ files = [
3232
"Lagrange multipliers" => "lagrange_multipliers.jl",
3333
"Low-level API - Poisson equation"=>"poisson_dev_fe.jl",
3434
"Low-level API - Geometry" => "geometry_dev.jl",
35+
"Low-level API - Custom ReferenceFEs" => "crouzeix_raviart_reffe.jl",
3536
]
3637

3738
Sys.rm(notebooks_dir;recursive=true,force=true)

src/crouzeix_raviart_reffe.jl

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
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+
= 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

Comments
 (0)