feat(docs): add elements_multiphysics_teaser.md

docs/src/elements_multiphysics_teaser.md | 141 +++++++++++++++++++++++++++++++  1 file changed, 141 insertions(+)
This commit is contained in:
Jukka Aho
2026-05-09 16:30:25 +03:00
parent a03ecc09be
commit 3b761d86b7
+141
View File
@@ -0,0 +1,141 @@
# [Elements, DOF placement, materials, and multiphysics](@id elements_multiphysics_teaser)
This page is a conceptual teaser for the current (0.x) element pipeline: what the
types mean, how global numbering attaches to an element, how materials
hook in, and how a deliberately mixed DOF specification still follows one
mechanical pattern.
For API listings, see [API Reference](@ref). For maintainer detail and
invariants, see `AGENTS.md` and the `README.md` files under `src/elements/`
and `src/dofs/`.
## Element as a typed template
A finite element is Ciarlet's triple ``(K, P, \Sigma)`` carried almost
entirely in the type system:
| Role | Julia type |
|------|----------------|
| Reference domain ``K`` | Topology type, e.g. `Tetrahedron{4}`, `Hex8` |
| Local space ``P`` | Basis type, e.g. `Lagrange{1}` |
| Degrees of freedom ``\Sigma`` | `S`, a `@DOFSet{...}` (NamedTuple of `DOF{Quantity, Entity}`) |
| Total local DOF count | Type parameter `N`, inferred from `K` and `S` |
The runtime struct is minimal:
```julia
struct Element{K, P, S, N}
id::UInt
dof_indices::NTuple{N, UInt64}
end
```
`K` and `P` are not stored; they only refine the type. The only heavy
per-element data is the flat tuple `dof_indices`, one global DOF index per
local DOF, in the canonical order defined below.
## Where each DOF "lives"
You declare fields with `@DOFSet`:
```julia
S = @DOFSet{
T::DOF{Temperature, Vertex},
u::DOF{Displacement{3}, Vertex},
p::DOF{Float64, Cell}, # discontinuous scalar (e.g. pressure bubble)
}
```
Each entry is `DOF{Quantity, Entity}`:
- `Quantity` fixes the number of components per attachment point
(scalar temperature, 3-vector displacement, …) via `dof_size`.
- `Entity` selects the topological anchor: `Vertex`, `Edge`, `Face`, or
`Cell`.
The constructor for `Element{K,P,S,N}` checks that `N` equals `ndofs(K, S)`.
`create_elements!(mesh, Element{K,P,S})` builds a `DOFHandler` and fills
each element's `dof_indices` so shared mesh entities reuse the same global
indices. You do not hand-wire indices by hand in normal use.
## Compile-time layout: `local_dof_layout`
The order of entries in `dof_indices` is defined by
`local_dof_layout(::Type{Element{K,P,S,N}})`, a `@generated` function that
returns `NTuple{N, DOFLayoutEntry}`. Each entry stores
`(field_idx, entity_local, component)` for one local DOF index.
Fields are visited in NamedTuple key order. For each field, all vertex
DOFs (if `Entity === Vertex`) are enumerated in local vertex order, then
components; then the next field. A `Cell`-anchored field contributes one
logical "entity" worth of components per element (pressure bubble, …).
Assemblers and matrix-free operators index this tuple instead of doing
runtime `div`/`mod` arithmetic to decode local DOFs.
Current limitation: the generated `local_dof_layout` implements `Vertex`
and `Cell` attachments. `Edge` and `Face` are part of the long-term design
(documented in `src/dofs/fields.jl`) but are not wired through the
generator yet; adding them is a localized extension.
## Materials: traits and kernels
Materials subtype `AbstractMaterial`. Several open functions form a small
trait layer, for example:
- `material_behavior(material)` — constant tangent, strain-dependent,
stateful plasticity, …
- `supported_physics(material)` — tuple of physics markers such as
`Elasticity{3}()`, `Thermal{3}()`
- `required_state_variables(material)` — what must live in IP state
storage
Concrete materials implement these in their own files; see
`src/materials/traits.jl` and `src/materials/api.jl`.
Important separation: the element type `S` describes which global DOFs exist
and how they are ordered. A kernel decides which physics runs at a
quadrature point and which material(s) supply tangents and forces.
Example: `ContinuumKernel` pairs one elastic material with a displacement
field. `ThermoElasticKernel` (under `src/domains/thermo_elastic/`) holds
both a mechanical and a thermal material; `evaluate_entry` dispatches on
the `field_idx` pair from `DOFLayoutEntry` so `K_uu`, `K_TT`, and the
coupling blocks are assembled from one element loop. That is the intended
pattern for multiphysics: one `S`, one element storage model, kernels that
know the physics blocks.
## A deliberately mixed specification
Thermo-mechanics with temperature and displacement on vertices is the
simplest coupled story (four DOFs per node on a tetrahedron). A harsher
stress test for the DOF machinery is vertex displacement plus a
discontinuous cell pressure (Stokes-style `u``p`):
```julia
S = @DOFSet{u::DOF{Displacement{3}, Vertex},
p::DOF{Float64, Cell}}
```
The test file `test/dofs/test_multifield_dof_system.jl` walks this path
end-to-end: `ndofs` matches `local_dof_layout`, block ordering (all `u`
DOFs, then all `p` DOFs), handler connectivity, and helpers such as
`field_dof_range` / `element_dofs` aligned with the layout tuple.
Adding more named fields is mostly declaring another `DOF{…, …}` line in
`@DOFSet` and providing a kernel that implements the cross-blocks you care
about. The expensive part is physics and testing, not extending the
element struct.
## Summary
- Elements are types plus a flat global index tuple; `S` is the single
source of truth for DOF count and ordering.
- `create_elements!` maps mesh topology to those indices in a type-stable
way.
- Materials expose traits; kernels connect materials to the relevant
`field_idx` blocks via `local_dof_layout`.
- Multifield and mixed `Vertex`/`Cell` anchoring already exercise the
production DOF path; richer entity attachments follow the same layout
idea once implemented.