Files
JuliaFEM.jl/src/elements/elements.jl
T
Jukka Aho 4968acd069 refactor(elements): redesign Element type to Ciarlet's triple (K,P,Σ) with DOF-based API
Major architectural refactoring: replace old field-based element system
with modern DOF-based design following Ciarlet's finite element triple.

- Redesign Element from Element{N,NIP,F,B} to Element{K,P,S,N}
- Replace connectivity with dof_indices (flat tuple of global DOF indices)
- Remove all old field system (sfields, dfields, fields, update_field!, etc.)
- Remove old constructors and compatibility shims
- Add compile-time DOF computation via @generated ndofs(K, S)
- Add field_dof_range for compile-time local DOF range computation
- Add local_to_global_map for type-stable DOF mapping
- Add topology_type, basis_type, dof_type query functions
- Add element_id, element_dofs, n_element_dofs, nnodes accessors
- Remove 593 lines of legacy code (963 → 370 lines)
- Simplify API: elements now store DOF indices, not node connectivity
- Support multi-field elements via DOFSet specifications
- Zero-allocation design with compile-time type information
2025-12-12 23:40:15 +02:00

371 lines
12 KiB
Julia
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/FEMBase.jl/blob/master/LICENSE
# ============================================================================
# Type-Level DOF Count Computation
# ============================================================================
"""
ndofs(::Type{K}, ::Type{S}) → Int
Compute total number of DOFs for field spec S on topology K.
# Example
```julia
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
ndofs(Tetrahedron{4}, S) # → 12 (4 nodes × 3 components)
```
"""
@generated function ndofs(::Type{K}, ::Type{S}) where {K, S}
field_names = fieldnames(S)
total = 0
for fname in field_names
field_spec = fieldtype(S, fname) # Tuple{Displacement{3}, Vertex}
field_type = field_spec.parameters[1] # Displacement{3}
entity_type = field_spec.parameters[2] # Vertex
# Extract quantity type via trait
Q = quantity_type(field_spec) # Vec{3}
# Count entities
n_entities = if entity_type === Vertex
nnodes(K())
elseif entity_type === Edge
nedges(K())
elseif entity_type === Face
nfaces(K())
else
error("Unsupported entity type: $entity_type")
end
# Count components per entity
n_components = if Q === Float64
1
elseif Q isa UnionAll && Q.body <: Tensor && Q.body.parameters[1] == 1
Q.body.parameters[2]
else
error("Unsupported quantity type: $Q")
end
total += n_entities * n_components
end
return total
end
"""
AbstractElement{K, P, S, N}
Abstract supertype for finite elements following Ciarlet's triple (K, P, Σ).
# Type Parameters
- `K <: AbstractTopology`: Reference domain
- `P <: AbstractBasis`: Polynomial space
- `S`: Field specification (determines Σ functionals)
- `N::Int`: Total number of DOFs (inferred from S and K)
See `src/elements/README.md` for complete documentation.
"""
abstract type AbstractElement{K<:AbstractTopology, P<:AbstractBasis, S<:DOFSet, N} end
"""
Element{K, P, S, N}
Finite element implementing Ciarlet's triple (K, P, Σ).
# Type Parameters
- `K`: Topology (Triangle{3}, Tetrahedron{4}, ...)
- `P`: Basis (Lagrange{1}, Lagrange{2}, ...)
- `S`: Field spec with quantity types and entity locations
- `N::Int`: Total DOF count (automatically inferred from S and K)
# Fields
- `id::UInt`: Element identifier
- `dof_indices::NTuple{N,UInt64}`: Flat tuple of global DOF indices
# Examples
```julia
# Single field: 3D displacement (12 DOFs = 4 nodes × 3 components)
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
Element{Tetrahedron{4}, Lagrange{1}, S}(UInt(1), (1,2,3,4,5,6,7,8,9,10,11,12))
# Multi-field: Thermo-mechanical (16 DOFs = 4 T + 12 u)
S = @DOFSet{T::DOF{Temperature,Vertex}, u::DOF{Displacement{3},Vertex}}
Element{Tetrahedron{4}, Lagrange{1}, S}(UInt(1), (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16))
```
See `src/elements/README.md` for comprehensive documentation.
"""
struct Element{K<:AbstractTopology, P<:AbstractBasis, S<:DOFSet, N} <: AbstractElement{K,P,S,N}
id::UInt
dof_indices::NTuple{N,UInt64}
# Inner constructor validates N matches spec
function Element{K,P,S,N}(id::UInt, dof_indices::NTuple{N,UInt64}) where {K,P,S,N}
expected = ndofs(K, S)
if N != expected
error("Element{$K,$P,$S,$N}: Expected $expected DOFs (from spec), got $N")
end
return new{K,P,S,N}(id, dof_indices)
end
end
# Outer constructor infers N from tuple length
function Element{K,P,S}(id::UInt, dof_indices::NTuple{N,UInt64}) where {K,P,S,N}
return Element{K,P,S,N}(id, dof_indices)
end
# Convenience constructor from varargs or vector
function Element{K,P,S}(id::UInt, dof_indices::UInt64...) where {K,P,S}
return Element{K,P,S}(id, dof_indices)
end
function Element{K,P,S}(id::UInt, dof_indices::AbstractVector{<:Integer}) where {K,P,S}
return Element{K,P,S}(id, tuple((UInt64(i) for i in dof_indices)...))
end
# ============================================================================
# Type-Level Queries
# ============================================================================
"""
topology_type(::Element{K,P,S,N}) → Type{K}
Extract topology type K from element.
"""
topology_type(::Element{K,P,S,N}) where {K,P,S,N} = K
topology_type(::Type{Element{K,P,S,N}}) where {K,P,S,N} = K
"""
basis_type(::Element{K,P,S,N}) → Type{P}
Extract basis type P from element.
"""
basis_type(::Element{K,P,S,N}) where {K,P,S,N} = P
basis_type(::Type{Element{K,P,S,N}}) where {K,P,S,N} = P
"""
dof_type(::Element{K,P,S,N}) → Type{S}
Extract DOF specification type S from element.
"""
dof_type(::Element{K,P,S,N}) where {K,P,S,N} = S
dof_type(::Type{Element{K,P,S,N}}) where {K,P,S,N} = S
# ============================================================================
# Local-Global DOF Mapping for Coupled Assembly
# ============================================================================
"""
local_dof_count(elem::Element) → Int
Total number of local DOFs for this element (sum over all fields).
"""
@inline function local_dof_count(elem::Element{K,P,S,N}) where {K,P,S,N}
return N # Now directly available as type parameter!
end
"""
global_dof_indices(elem::Element) → Vector{UInt64}
Flattened vector of global DOF indices for this element.
See `src/elements/README.md` for assembly patterns.
"""
function global_dof_indices(elem::Element)
return collect(elem.dof_indices) # NTuple → Vector
end
"""
local_to_global_map(elem::Element) → NTuple{N,UInt64}
Mapping from local DOF index to global DOF index.
`global_dof = map[local_dof]` where `local_dof ∈ 1:N`.
Returns tuple (not Vector) for type stability and compiler optimization.
Used for coupled assembly. See `src/elements/README.md`.
"""
@inline function local_to_global_map(elem::Element{K,P,S,N}) where {K,P,S,N}
return elem.dof_indices # Already flat!
end
# ============================================================================
# Compile-Time Helper Functions for @generated field_dof_range
# ============================================================================
# Helper: Compute ndofs at compile time
function _compile_time_ndofs(@nospecialize(field_type), @nospecialize(topology_type))
# Handle DOF{FieldType, EntityType} format (new format)
if field_type isa DataType && field_type <: DOF && length(field_type.parameters) == 2
FieldType = field_type.parameters[1] # e.g., Displacement{3}
E = field_type.parameters[2] # e.g., Vertex
# Extract quantity type via trait (handles Displacement{3} → Vec{3})
Q = quantity_type(field_type)
# Number of DOFs = dof_per_entity * number_of_entities
return _dof_per_entity(Q) * _count_entities_compiletime(topology_type, E)
# Handle Tuple{FieldType, E} format (legacy format)
elseif field_type isa DataType && field_type <: Tuple && length(field_type.parameters) == 2
FieldType = field_type.parameters[1] # Could be Displacement{3} or Vec{3}
E = field_type.parameters[2]
# Extract quantity type via trait (handles both field types and quantity types)
Q = quantity_type(field_type)
# Number of DOFs = dof_per_entity * number_of_entities
return _dof_per_entity(Q) * _count_entities_compiletime(topology_type, E)
else
error("Cannot compute ndofs for field type $field_type (expected DOF{...} or Tuple{...})")
end
end
function _dof_per_entity(@nospecialize(Q))
# Use dof_size which handles all quantity types properly (Displacement{3}, Vec{3}, Float64, UnionAll, etc.)
# This is the most robust approach
try
return dof_size(Q)
catch e
# Fallback for specific cases if dof_size fails
if Q === Float64
return 1
else
error("Cannot determine dof_size for quantity type $Q: $e")
end
end
end
function _count_entities_compiletime(@nospecialize(K), @nospecialize(E))
# This must match count_entities(topology, entity_type) at runtime
# K is a TYPE (e.g., Tet4), not an instance
if E === Vertex
return nnodes(K) # nnodes accepts Type
elseif E === Edge
return nedges(K) # nedges accepts Type
elseif E === Face
return nfaces(K) # nfaces accepts Type
elseif E === Cell
return 1 # One cell per element
else
error("Unknown entity type $E")
end
end
# ============================================================================
# Local DOF Range Computation (COMPILE-TIME via @generated)
# ============================================================================
"""
field_dof_range(elem::Element, field::Symbol) → UnitRange{Int}
Local DOF range for a specific field. Computed at compile time via @generated.
See `src/elements/README.md` for usage examples.
"""
@generated function field_dof_range(::Element{K,P,S,N}, field::Symbol) where {K,P,S,N}
# This runs at COMPILE TIME!
# S is the NamedTuple type containing field specifications
if S <: NamedTuple
# Multi-field case
field_types = S.parameters[2] # Tuple of field types
field_names = fieldnames(S)
# Compute offset for each field at compile time
offset = 0
field_ranges = Expr(:block)
for (i, fname) in enumerate(field_names)
ftype = field_types.parameters[i]
n = _compile_time_ndofs(ftype, K)
range_expr = :($offset+1:$offset+$n)
# Generate: if field === :fname return range_expr end
push!(field_ranges.args, quote
if field === $(QuoteNode(fname))
return $range_expr
end
end)
offset += n
end
# Add error case
push!(field_ranges.args, :(error("Field ", field, " not found in element type $S")))
return field_ranges
else
# Single-field case (S <: AbstractDOF)
n = _compile_time_ndofs(S, K)
return :(return 1:$n)
end
end
# ============================================================================
# Local-to-Global Mapping (Type-Stable Tuple Version)
# ============================================================================
# ============================================================================
# Type Extraction (previously defined above)
# ============================================================================
# These were defined earlier but are here for reference
# topology_type, basis_type, dof_type already defined above
# ============================================================================
# Element Queries
# ============================================================================
"""
element_id(elem::Element) → UInt
Get element ID (index in mesh).
"""
element_id(elem::Element) = elem.id
"""
element_dofs(elem::Element) → NTuple{N,UInt64}
Get all global DOF indices as flat tuple.
"""
element_dofs(elem::Element) = elem.dof_indices
"""
element_dofs(elem::Element, field::Symbol) → Tuple
Get global DOF indices for specific field by extracting from flat tuple.
# Example
```julia
element_dofs(elem, :T) # Extracts T indices from flat tuple
element_dofs(elem, :u) # Extracts u indices from flat tuple
```
"""
function element_dofs(elem::Element{K,P,S,N}, field::Symbol) where {K,P,S,N}
range = field_dof_range(elem, field)
return elem.dof_indices[range]
end
"""
n_element_dofs(elem::Element) → Int
Get total number of DOFs for this element (all fields).
"""
n_element_dofs(elem::Element{K,P,S,N}) where {K,P,S,N} = N
"""
nnodes(::Element{K,P,S,N}) → Int
Get number of nodes from topology.
"""
nnodes(::Element{K,P,S,N}) where {K,P,S,N} = nnodes(K)
nnodes(::Type{Element{K,P,S,N}}) where {K,P,S,N} = nnodes(K)
# ============================================================================
# Display
# ============================================================================
function Base.show(io::IO, elem::Element{K,P,S,N}) where {K,P,S,N}
print(io, "Element{$K, $P, $S}(id=$(elem.id), ndofs=$N)")
end