refactor(dofs): align fields.jl docs and counting with DOF{Q,E} templates

Bring module prose in line with flat `dof_indices`, `@DOFSet`, and DOFHandler-era
helpers while tightening compile-time DOF arithmetic.

- Refresh docstrings: drop markdown bold noise; document `element_dofs` /
  `field_dof_range` instead of NamedTuple-shaped `dof_indices` accessors.
- Rewrite examples to prefer `@DOFSet`; spell out NamedTuple equivalence and drop
  Tuple-based migration snippets incompatible with DOFHandler.
- Implement `field_ndofs(::Type{<:DOF}, Topo)` / `ndofs(::DOFSet, Topo)` using
  `quantity_type` / `entity_type` and generator-style sums over `fieldtypes(S)`.
- Extend `quantity_type(::Type{<:DOF})` for raw `Float64`, `Vec`, `Tensor`, and
  `SymmetricTensor` fields alongside `AbstractField` wrappers.
- Allow `field_ndofs` on raw-quantity DOFs by sizing `T` directly (pressure on
  cells/faces, etc.).
- Remove `single_field` compatibility helper and duplicate element accessors now
  canonical in `src/elements/elements.jl`.
- Point LICENSE header at LICENSE.md.
This commit is contained in:
Jukka Aho
2026-05-09 16:48:26 +03:00
parent bea37f797e
commit ec600459a9
+53 -175
View File
@@ -1,12 +1,12 @@
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
"""
Field System - Unified Multi-Field Architecture
# Core Principle: Multi-Field is Fundamental
**Single-field is just a special case of multi-field with one key!**
Single-field is just a special case of multi-field with one key!
Every element has a field specification `S` which is a `DOFSet` (currently implemented as NamedTuple):
```julia
@@ -26,24 +26,24 @@ S = @DOFSet{
}
```
**Note**: `@NamedTuple` also works (since `DOFSet = NamedTuple`), but `@DOFSet` is preferred for future compatibility.
Note: `@NamedTuple` also works (since `DOFSet = NamedTuple`), but `@DOFSet` is preferred for future compatibility.
Each field is a `DOF{FieldType, EntityType}`:
- **FieldType**: `Displacement{Dim}`, `Temperature`, `DisplacementRotation{Dim}`, etc. (from AbstractField)
- **EntityType**: `Vertex`, `Edge`, `Face`, `Cell` (topological entity)
- FieldType: `Displacement{Dim}`, `Temperature`, `DisplacementRotation{Dim}`, etc. (from AbstractField)
- EntityType: `Vertex`, `Edge`, `Face`, `Cell` (topological entity)
The field type encodes both the physical meaning and the underlying quantity type (Vec, Float64, etc.).
# Examples
**Single-field (displacement):**
Single-field (displacement):
```julia
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
Element{Tetrahedron{4}, Lagrange{1}, S}
```
**Multi-field (thermo-mechanical):**
Multi-field (thermo-mechanical):
```julia
S = @DOFSet{
T::DOF{Temperature, Vertex},
@@ -51,50 +51,42 @@ S = @DOFSet{
}
Element{Tetrahedron{4}, Lagrange{1}, S}
# DOF access: elem.dof_indices.T, elem.dof_indices.u
# DOF access (flat tuple, helper functions for per-field views):
# element_dofs(elem) # all global DOF indices, NTuple{N,UInt64}
# element_dofs(elem, :T) # global indices for field :T
# field_dof_range(typeof(elem), :u) # local index range for field :u
```
**Complex multi-physics (THM-E):**
Complex multi-physics (THM-E):
```julia
S = @NamedTuple{
S = @DOFSet{
T::DOF{Temperature, Vertex}, # Thermal
u::DOF{Displacement{3}, Vertex}, # Mechanical
p::DOF{Pressure, Cell}, # Hydraulic (discontinuous)
φ::DOF{ElectricPotential, Edge} # Electrical (Nédélec)
u::DOF{Displacement{3}, Vertex}, # Mechanical
p::DOF{Pressure, Cell}, # Hydraulic (discontinuous)
φ::DOF{ElectricPotential, Edge} # Electrical (Nédélec)
}
```
# Macro Sugar (@Fields was removed - use @DOFSet)
# Macro Sugar
```julia
@DOFSet{
T::DOF{Temperature, Vertex},
u::DOF{Displacement{3}, Vertex},
p::DOF{Pressure, Cell},
φ::DOF{ElectricPotential, Edge}
}
# Equivalent to @NamedTuple:
@NamedTuple{
T::DOF{Temperature, Vertex},
u::DOF{Displacement{3}, Vertex},
p::DOF{Pressure, Cell},
φ::DOF{ElectricPotential, Edge}
}
```
`@DOFSet{...}` is the preferred entry point. It currently expands to
`@NamedTuple{...}`, so the equivalent NamedTuple literal also works
provided every field type is a `DOF{Quantity, Entity}`. The bare
`Tuple{Quantity, Entity}` form that older drafts used is no longer
accepted by the DOFHandler.
# Compile-Time Computation
All field information is encoded in types:
```julia
# Number of DOFs for a field
@pure function field_ndofs(::Type{Tuple{T,E}}, ::Type{Topo}) where {T,E,Topo}
return dof_size(T) * nentities(Topo, E)
Base.@pure function field_ndofs(::Type{D}, ::Type{Topo}) where {D<:DOF, Topo}
return dof_size(quantity_type(D)) * nentities(Topo, entity_type(D))
end
# Total DOFs for element
@pure function ndofs(::Type{S}, ::Type{Topo}) where {S<:DOFSet, Topo}
return sum(field -> field_ndofs(field, Topo), fieldtypes(S))
Base.@pure function ndofs(::Type{S}, ::Type{Topo}) where {S<:DOFSet, Topo}
return sum(field_ndofs(F, Topo) for F in fieldtypes(S))
end
```
@@ -102,26 +94,13 @@ Everything resolves at compile time! Zero runtime overhead.
# Architecture Benefits
1. **Unified**: Single and multi-field use same code path
2. **Type-safe**: All dispatch on field specification type
3. **Named access**: `elem.dof_indices.T` self-documenting
4. **Extensible**: Add new fields without changing Element struct
5. **Efficient**: Compile-time computation, zero overhead
6. **Clean**: Multi-field is the general case, not a special case
# Migration Path
Old code with single DOF:
```julia
DOF{Vec{3}, Vertex} # Old API
```
New unified field specification:
```julia
@NamedTuple{u::Tuple{Displacement{3}, Vertex}} # New API (single field)
```
We can provide compatibility wrappers during migration.
1. Unified: Single and multi-field use same code path
2. Type-safe: All dispatch on field specification type
3. Named access: `element_dofs(elem, :T)` reads cleanly while
`dof_indices` stays a flat `NTuple` for zero-allocation assembly
4. Extensible: Add new fields without changing Element struct
5. Efficient: Compile-time computation, zero overhead
6. Clean: Multi-field is the general case, not a special case
"""
# ============================================================================
@@ -145,8 +124,9 @@ quantity_type(DOF{Temperature, Vertex}) # Float64 (via trait)
# Implementation
The function extracts the field type from the tuple, then uses the `quantity_type()` trait
to get the underlying quantity type (Vec, Float64, etc.).
The function extracts the field type `T` from `DOF{T,E}` and dispatches on
`quantity_type(::Type{T})` to get the underlying quantity type (Vec,
Float64, etc.).
# See Also
- [`quantity_type(::Type{<:AbstractField})`](@ref) - Trait for field types
@@ -157,8 +137,11 @@ to get the underlying quantity type (Vec, Float64, etc.).
T = D.parameters[1]
if T <: AbstractField
return quantity_type(T)
elseif T === Float64 || T <: Vec || T <: Tensor || T <: SymmetricTensor
# Already a raw quantity type — pass through.
return T
else
error("Expected field type (AbstractField) in DOF, got $T. Use format: DOF{FieldType, EntityType}")
error("Expected field type (AbstractField) or quantity type (Float64/Vec/Tensor) in DOF, got $T")
end
end
@@ -196,7 +179,9 @@ field_ndofs(DOF{Displacement{3}, Vertex}, Tetrahedron{4}) # 3 × 4 = 12
Q = quantity_type(T) # Get underlying quantity type (Vec, Float64, etc.)
return dof_size(Q) * nentities(Topo, E)
else
error("Expected field type (AbstractField) in DOF, got $T. Use format: DOF{FieldType, EntityType}")
# Raw quantity (`DOF{Float64, Cell}`, `DOF{Float64, Face}`, …) —
# same rule as single-field `ndofs(::DOF{T,E}, Topo)`.
return dof_size(T) * nentities(Topo, E)
end
end
@@ -236,24 +221,9 @@ ndofs(S, Tetrahedron{4}) # 4 + 12 = 16
end
# ============================================================================
# Compatibility Helpers (for migration)
# Field-Spec Accessors
# ============================================================================
"""
single_field(field_name::Symbol, quantity::Type, entity::Type)
Create a single-field specification.
# Example
```julia
S = single_field(:u, Vec{3}, Vertex)
# Equivalent to: @NamedTuple{u::Tuple{Vec{3}, Vertex}}
```
"""
function single_field(field_name::Symbol, quantity::Type, entity::Type)
return @eval @NamedTuple{$field_name::Tuple{$quantity, $entity}}
end
"""
field_names(::Type{S}) → NTuple{N,Symbol} where {S<:DOFSet}
@@ -261,7 +231,7 @@ Get field names from specification.
# Example
```julia
S = @NamedTuple{T::Tuple{Temperature,Vertex}, u::Tuple{Displacement{3},Vertex}}
S = @DOFSet{T::DOF{Temperature, Vertex}, u::DOF{Displacement{3}, Vertex}}
field_names(S) # (:T, :u)
```
"""
@@ -274,7 +244,7 @@ Number of fields in specification.
# Example
```julia
S = @NamedTuple{T::Tuple{Temperature,Vertex}, u::Tuple{Displacement{3},Vertex}}
S = @DOFSet{T::DOF{Temperature, Vertex}, u::DOF{Displacement{3}, Vertex}}
field_count(S) # 2
```
"""
@@ -287,109 +257,17 @@ Check if specification has exactly one field.
# Example
```julia
S1 = @NamedTuple{u::Tuple{Displacement{3},Vertex}}
S1 = @DOFSet{u::DOF{Displacement{3}, Vertex}}
is_single_field(S1) # true
S2 = @NamedTuple{T::Tuple{Temperature,Vertex}, u::Tuple{Displacement{3},Vertex}}
S2 = @DOFSet{T::DOF{Temperature, Vertex}, u::DOF{Displacement{3}, Vertex}}
is_single_field(S2) # false
```
"""
@inline is_single_field(::Type{S}) where {S<:DOFSet} = fieldcount(S) == 1
# ============================================================================
# Element Accessor Functions (convenience wrappers)
# ============================================================================
"""
element_id(element) → UInt
Get element ID.
# Example
```julia
elem = Element{Triangle{3}, Lagrange{1}, S}(UInt(42), dof_indices)
element_id(elem) # 42
```
"""
@inline element_id(element) = element.id
"""
n_element_dofs(element) → Int
Total number of DOFs for this element (sum over all fields).
# Example
```julia
# Single field: u ∈ Vec{3} at 4 vertices → 12 DOFs
elem = Element{Tetrahedron{4}, Lagrange{1}, S_u}(id, (u=(1:12...,),))
n_element_dofs(elem) # 12
# Multi-field: T + u → 4 + 12 = 16 DOFs
elem = Element{Tetrahedron{4}, Lagrange{1}, S_Tu}(id, (T=(1:4...,), u=(5:16...,)))
n_element_dofs(elem) # 16
```
"""
@inline function n_element_dofs(element)
total = 0
for field_indices in element.dof_indices
total += length(field_indices)
end
return total
end
"""
element_dofs(element, field_name::Symbol) → Tuple{Int,...}
Get DOF indices for specific field.
# Example
```julia
S_THM = @DOFSet{
T::Tuple{Temperature, Vertex},
p::Tuple{Pressure, Vertex},
u::Tuple{Displacement{3}, Vertex}
}
elem = Element{Tetrahedron{4}, Lagrange{1}, S_THM}(
UInt(1),
(T=(1,2,3,4), p=(5,6,7,8), u=(9,10,11,12,13,14,15,16,17,18,19,20))
)
element_dofs(elem, :T) # (1, 2, 3, 4)
element_dofs(elem, :p) # (5, 6, 7, 8)
element_dofs(elem, :u) # (9, 10, ..., 20)
```
"""
@inline element_dofs(element, field_name::Symbol) = getfield(element.dof_indices, field_name)
"""
basis_type(element) → Type{<:AbstractBasis}
Extract basis type from element type parameters.
# Example
```julia
elem = Element{Tetrahedron{4}, Lagrange{1}, S}(...)
basis_type(elem) # Lagrange{1}
```
"""
@inline function basis_type(elem)
T = typeof(elem)
return T.parameters[2] # Second type parameter is basis
end
"""
dof_type(element) → Type{<:AbstractDOF}
Extract DOF specification type from element type parameters.
# Example
```julia
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
elem = Element{Tetrahedron{4}, Lagrange{1}, S}(...)
dof_type(elem) # Returns the S type (e.g., @NamedTuple{u::Tuple{Displacement{3},Vertex}})
```
"""
@inline function dof_type(elem)
T = typeof(elem)
return T.parameters[3] # Third type parameter is DOF spec
end
# Element-level helpers (`element_id`, `element_dofs`, `n_element_dofs`,
# `basis_type`, `dof_type`) live in `src/elements/elements.jl`, where
# they are dispatched on `Element{K,P,S,N}`. Earlier generic versions
# that walked `element.dof_indices` as a NamedTuple were left over from
# a previous `Element` layout and have been removed.