Files
JuliaFEM.jl/docs/src/elements_multiphysics_teaser.md
T
Jukka Aho 3b761d86b7 feat(docs): add elements_multiphysics_teaser.md
docs/src/elements_multiphysics_teaser.md | 141 +++++++++++++++++++++++++++++++  1 file changed, 141 insertions(+)
2026-05-09 16:30:25 +03:00

5.5 KiB
Raw Blame History

[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. 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:

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:

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 up):

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.