mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-09-20 18:18:31 +00:00
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:
+53
-175
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user