feat(basis): Add nbasis() function for compile-time basis function count

Add zero-cost nbasis(topology, basis) -> Int function that returns the number
of basis functions at compile time. This is essential for validating Ciarlet
triplet (K, P, Σ) consistency and enables type-stable pre-allocations.

Key features:
- Zero-cost: compiles to single constant return (ret i64 N)
- Instance-based dispatch: nbasis(Triangle{3}(), Lagrange{1}())
- Comprehensive documentation with compile-time verification examples
- Replaces removed ndofs() function with clearer semantics

The function is generated automatically by basis_generator.jl alongside
basis function implementations, ensuring consistency.
This commit is contained in:
Jukka Aho
2025-11-22 12:06:20 +02:00
parent 6ad8c43e7e
commit df449f23df
+74 -19
View File
@@ -120,6 +120,79 @@ Equivalent to `get_basis_derivatives(topology, basis, xi)[i]`.
return get_basis_derivatives(topology, basis, xi)[i]
end
"""
nbasis(topology::AbstractTopology, basis::AbstractBasis) -> Int
Return the number of basis functions for the given basis on the given topology.
**Zero-cost:** This function compiles to a constant integer for concrete types.
The return value is compile-time known, enabling type-stable allocations and
constant propagation throughout assembly code.
# Arguments
- `topology::AbstractTopology`: Element topology instance (e.g., `Triangle{3}()`, `Tetrahedron{10}()`)
- `basis::AbstractBasis`: Basis instance (e.g., `Lagrange{1}()`, `Lagrange{2}()`)
# Returns
- `Int`: Number of basis functions (compile-time constant for generated bases)
# Examples
```julia
# Standard nodal bases
nbasis(Triangle{3}(), Lagrange{1}()) # 3 (linear triangle)
nbasis(Triangle{6}(), Lagrange{2}()) # 6 (quadratic triangle)
nbasis(Tetrahedron{10}(), Lagrange{2}()) # 10 (quadratic tetrahedron)
# Exotic elements (nodes ≠ basis functions)
nbasis(Triangle{3}(), DKT()) # 9 (3 nodes but 9 DOFs!)
# Serendipity families
nbasis(Quadrilateral{8}(), Serendipity{2}()) # 8 (reduced quadratic quad)
```
# Implementation Notes
For generated bases (Lagrange, Serendipity), `nbasis` functions are automatically
generated by `basis_generator.jl` alongside the basis function implementations.
This ensures consistency: the generator knows exactly how many basis functions
it produced, so `nbasis` always returns the correct value.
For custom/special bases (DKT, hierarchical, etc.), implement manually:
```julia
@inline nbasis(::Triangle{3}, ::DKT) = 9
```
For truly dynamic bases (adaptive refinement), use instance-based dispatch:
```julia
@inline nbasis(::K, basis::HierarchicalBasis) where {K<:AbstractTopology} = basis.active_modes
```
# Compile-Time Verification
You can verify zero-cost compilation:
```julia
@code_llvm nbasis(Triangle{3}(), Lagrange{1}())
# Should show: ret i64 3 (direct constant return)
@code_native nbasis(Triangle{3}(), Lagrange{1}())
# Should show: mov rax, 3; ret (single instruction)
```
# Usage in Validation
The primary use case is validating the Ciarlet triplet (K, P, Σ):
```julia
function validate_dof_consistency(dof, K, P)
n_dofs = ndofs(dof, K)
n_basis = nbasis(K(), P()) # ← Zero-cost!
@assert n_dofs == n_basis "Inconsistent element: \$n_basis basis ≠ \$n_dofs DOFs"
end
```
See also: [`get_basis_functions`](@ref), [`validate_dof_consistency`](@ref)
"""
function nbasis end
# ---------------------------------------------------------------------------
# Deprecated bridge (old API names)
# ---------------------------------------------------------------------------
@@ -140,25 +213,7 @@ Provided temporarily for migration.
"""
function eval_dbasis! end
# ---------------------------------------------------------------------------
# Degrees of freedom utility
# ---------------------------------------------------------------------------
"""
ndofs(basis::AbstractBasis)
ndofs(::Type{<:AbstractBasis})
Total degrees of freedom for this basis. For standard nodal bases, this is
usually equal to the number of basis functions; specialized bases (e.g., plates)
can override to return multiple DOFs per node.
"""
function ndofs end
# Default: basis implementations should override; no assumption about nnodes here.
ndofs(::AbstractBasis) = error("ndofs not implemented for this basis")
ndofs(::Type{<:AbstractBasis}) = error("ndofs not implemented for this basis type")
export AbstractBasis, Lagrange, Serendipity
export ndofs
export get_basis_functions, get_basis_derivatives, get_basis_function, get_basis_derivative
export nbasis
export eval_basis!, eval_dbasis!