From ec600459a948b810bd436d9e6cba1e2d987dd7a8 Mon Sep 17 00:00:00 2001 From: Jukka Aho Date: Sat, 9 May 2026 16:48:26 +0300 Subject: [PATCH] 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. --- src/dofs/fields.jl | 228 +++++++++++---------------------------------- 1 file changed, 53 insertions(+), 175 deletions(-) diff --git a/src/dofs/fields.jl b/src/dofs/fields.jl index 0c00e02..e7fac50 100644 --- a/src/dofs/fields.jl +++ b/src/dofs/fields.jl @@ -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.