Finite element spaces
FunctionSpace wraps
mfem.ParFiniteElementSpace together with everything it points at. That
ownership is not cosmetic: MFEM stores raw pointers, so a space built from a
temporary collection crashes the next time it is used.
Vh1 = hm.FunctionSpace.H1(mesh, order=2) # continuous Lagrange
Vl2 = hm.FunctionSpace.L2(mesh, order=1) # discontinuous Lagrange
Vvec = hm.FunctionSpace.H1(mesh, order=2, vdim=2) # vector Lagrange
Vnd = hm.FunctionSpace.ND(mesh, order=1) # Nedelec, H(curl)
Vrt = hm.FunctionSpace.RT(mesh, order=0) # Raviart-Thomas, H(div)
What each family gives the density
space |
|
derivative attribute |
|---|---|---|
H1/L2, |
scalar |
|
H1/L2, |
shape |
|
ND (H(curl)) |
shape |
|
RT (H(div)) |
shape |
|
A vector-element field has no .grad attribute, deliberately: the full gradient
is not a member of the space, so offering one would mean silently returning
something that is not the derivative of the discrete field.
An essential condition on a vector space applies to every component when its value is a
single number or None, and to one component when component= names it, which is how
a roller support (normal displacement fixed, tangential free) is written:
hm.DirichletBC(Vvec, 0.0, bdr_attributes=[4]) # clamped
hm.DirichletBC(Vvec, 0.0, bdr_attributes=[1], component=1) # roller
Observing a vector field is also a choice rather than a default:
assemblePointwiseObservation() needs
component or a components weight per target, and
assemblePointwiseLOSObservation is the line-of-sight spelling of the latter.
def curl_curl(u, m, p, x): # H(curl) state
return jnp.exp(m.val) * u.curl * p.curl + jnp.dot(u.val, p.val)
def darcy(u, m, p, x): # H(div) state
return jnp.exp(-m.val) * jnp.dot(u.val, p.val) + u.div * p.div
Why vector elements need more than a sign
Nedelec and Raviart-Thomas basis functions are Piola mapped, covariantly
(\(J^{-T} w\)) for H(curl) and contravariantly (\(J w / \det J\)) for
H(div), so unlike Lagrange shape functions they cannot be tabulated once for a
whole group of elements. MFEM’s CalcVShape applies the right map and is used
directly, so the convention is MFEM’s rather than a reimplementation.
Building those per-element tables costs a measured 4 to 8.5 microseconds per element-quadrature-point. They are built once per space and cached, so this is a setup cost and not a per-assembly one.
MFEM also encodes element orientation partly as a sign on the dof index and partly,
for H(curl) above lowest order on simplices, as a small dense
DofTransformation per element. hIPPyMFEM applies it in the three places MFEM
does and in the same order:
gather: \(u_{\mathrm{ref}} = T^{-1}(\mathrm{signs} \cdot u)\)
element matrix: \(A = T_{\mathrm{test}}^{\top} A_{\mathrm{ref}} T_{\mathrm{trial}}\), then the signs
element vector: \(b = T^{\top} b_{\mathrm{ref}}\), then the signs
Getting that order wrong does not crash; it produces a wrong matrix that is still
symmetric. The element matrices are therefore checked against
VectorFEMassIntegrator, CurlCurlIntegrator and DivDivIntegrator on
triangles, quadrilaterals, tetrahedra and hexahedra, at orders that do and do not
need a transformation, and agree to 1.4e-15.
Quadrature
The integration rule degree defaults to 2 * max(order) + 2. It is deliberately
generous: the density may be non-polynomial (exp(m), 1/m), so no exact rule
exists, and what matters for the inverse problem is not that each integral be
exact but that every derivative block use the same rule, which is automatic,
because they are all differentiated from one quadrature sum.
Pass quadrature_degree to
PDEVariationalProblem to choose
it explicitly. When comparing an assembled block against an MFEM integrator, set
the same rule on the MFEM side with integrator.SetIntRule(...), or the
comparison measures the difference between two quadrature choices rather than the
kernel.
Limitations
Variable-order spaces are rejected with a clear error; the batched element kernels assume one finite element per group.
Mixed/block spaces built from different families (Taylor-Hood, mixed Darcy) are not supported by
PDEVariationalProblem, which gives each of its three variables one space. A vector-valued space of a single family is. The kernel itself is not restricted: a density written over four slots in two families yields all four Jacobian blocks, which glue into one operator with MFEM’sHypreParMatrixFromBlocks. Tutorial 11 solves mixed Darcy that way, andtest_vectorfe.pydoes the same for poroelasticity.Non-conforming meshes work for domain integrals, whose reduction falls back to hypre’s
P^T A Pbecause the prolongation is no longer boolean (see Parallel correctness). Non-conforming boundary faces are rejected by the boundary kernels, and an interior facet density is refused on a non-conforming mesh altogether (Interior facet terms and DG).