mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-09-19 17:58:53 +00:00
feat(assemblers): add experimental DOF-based COO assembler
New 720-line experimental DOF-based assembly implementation: - DOF-by-DOF paradigm: loops over DOFs (matrix rows) instead of elements - DOFBasedCOOCache: pre-allocated cache with zero-allocation guarantees - Matrix-free integration: computes K*v without intermediate storage - Supports single-kernel problems (experimental limitation) - Uses DOF connectivity infrastructure for inverse mapping - Pre-computed element/geometry/material caches for performance - extract_system(): builds sparse matrix from triplets - create_cache(): factory function for cache creation Use cases: matrix-free Krylov solvers, very large problems, contact mechanics, mixed methods. Status: EXPERIMENTAL - single-kernel assumption only.
This commit is contained in:
@@ -0,0 +1,720 @@
|
||||
# This file is a part of JuliaFEM.
|
||||
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
|
||||
|
||||
#=
|
||||
DOF-based COO assembly using row-wise matrix-free integration.
|
||||
|
||||
**STATUS: EXPERIMENTAL - Single-kernel assumption only**
|
||||
|
||||
**DOF-BY-DOF PARADIGM**: Loop over DOFs (matrix rows), not elements or nodes!
|
||||
|
||||
**CRITICAL LIMITATION**: Currently assumes ALL elements use the SAME kernel (physics).
|
||||
For multi-physics problems with different element types/kernels, this approach
|
||||
requires additional infrastructure (element → kernel mapping, field coupling registry).
|
||||
|
||||
This implementation serves as a proof-of-concept for the simplest case.
|
||||
|
||||
Each DOF (matrix row):
|
||||
1. Finds all elements containing the corresponding node
|
||||
2. For each element:
|
||||
- Prepares element geometry once
|
||||
- Computes only needed matrix entries in this row
|
||||
3. Scatters entries to COO triplets
|
||||
|
||||
# Key Differences
|
||||
|
||||
**Element-Based (traditional):**
|
||||
```julia
|
||||
for element in elements
|
||||
K_e = compute_element_stiffness(element) # Full N×N matrix of 3×3 blocks
|
||||
scatter(K_e) # Scatter all entries
|
||||
end
|
||||
```
|
||||
|
||||
**Node-Based (existing):**
|
||||
```julia
|
||||
for node_i in nodes
|
||||
for element in elements_touching(node_i)
|
||||
for node_j in element.nodes
|
||||
K_ij = compute_block!(element, i, j) # Single 3×3 block
|
||||
scatter(K_ij, i, j)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**DOF-Based (this file):**
|
||||
```julia
|
||||
for dof_i in 1:ndofs # Each matrix row!
|
||||
node_i, comp_i = decode_dof(dof_i)
|
||||
for element in elements_touching(node_i)
|
||||
for local_j in 1:nodes_per_element
|
||||
for comp_j in 1:3
|
||||
dof_j = global_dof(element.nodes[local_j], comp_j)
|
||||
K_ij = compute_entry!(element, node_i, comp_i, local_j, comp_j)
|
||||
scatter(K_ij, dof_i, dof_j)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
# Advantages over Node-Based
|
||||
|
||||
1. **True matrix-free**: Computes K*v without ANY intermediate storage
|
||||
2. **Natural for mixed methods**: Easy to handle p/u coupling where DOF dimensions differ
|
||||
3. **Minimal memory**: No 3×3 block storage at all
|
||||
4. **Contact-ready**: Contact operates on DOFs directly (normal/tangential components)
|
||||
|
||||
# Performance Expectations
|
||||
|
||||
- **CPU Single-thread**: ~2-3x slower than element-based (finest granularity)
|
||||
- **CPU Multi-thread**: ~Same as node-based (parallelization still works)
|
||||
- **GPU**: ~Same as node-based (one thread per DOF vs one thread per node)
|
||||
- **Matrix-free K*v**: ~5-10x faster (no matrix storage, cache-friendly)
|
||||
|
||||
# When to Use
|
||||
|
||||
✅ **Matrix-free Krylov solvers** (GMRES, CG)
|
||||
✅ **Very large problems** (> 1M DOFs where storing K is prohibitive)
|
||||
✅ **Contact mechanics** (normal/tangential DOF operations)
|
||||
✅ **Mixed methods** (different DOF dimensions per field)
|
||||
|
||||
❌ **Forming explicit K** (element-based is faster)
|
||||
❌ **Direct solvers** (need explicit matrix)
|
||||
❌ **Small problems** (< 10k DOFs, overhead dominates)
|
||||
|
||||
# References
|
||||
|
||||
- Golden standard: `docs/src/book/multigpu_nodal_assembly.md`
|
||||
- Theory: `docs/design/dof_by_dof_efficiency_analysis.md`
|
||||
- Node-based version: `src/assemblers/node_based_coo.jl`
|
||||
|
||||
# Example
|
||||
|
||||
```julia
|
||||
# Setup
|
||||
mesh = create_cantilever_mesh(50, 10, 10)
|
||||
material = LinearElastic(E=210e9, ν=0.3)
|
||||
kernel = ContinuumKernel(
|
||||
ContinuumFormulation{FullThreeD}(),
|
||||
material,
|
||||
Displacement{3}()
|
||||
)
|
||||
|
||||
# Create DOF-based assembler
|
||||
assembler = DOFBasedCOOAssembler()
|
||||
cache = create_cache(assembler, mesh, kernel)
|
||||
|
||||
# Assemble (zero allocations after warmup!)
|
||||
assemble!(cache, assembler, kernel, mesh)
|
||||
|
||||
# Extract system
|
||||
K, f = extract_system(cache)
|
||||
|
||||
# Or use matrix-free!
|
||||
function matvec(v)
|
||||
return compute_matvec_dof_based(cache, v, kernel, mesh)
|
||||
end
|
||||
u = gmres(matvec, f, tol=1e-6)
|
||||
```
|
||||
=#
|
||||
|
||||
using SparseArrays
|
||||
using Tensors
|
||||
|
||||
# Import DOF connectivity infrastructure
|
||||
using ..JuliaFEM: DOFConnectivity, DOFElementConnection, build_dof_connectivity
|
||||
using ..JuliaFEM: element_dofs, local_dof_count, field_dof_range
|
||||
using ..JuliaFEM: compute_stiffness_value
|
||||
using ..JuliaFEM: create_element_cache, create_geometry_cache, create_material_cache
|
||||
using ..JuliaFEM: update_geometry_cache!, update_element_cache!, update_material_cache!
|
||||
using ..JuliaFEM: compute_tangent, get_tangent_vector
|
||||
using ..JuliaFEM: nnodes, Vertex, GeometryCache, AssemblyMaterialWorkspace
|
||||
using ..JuliaFEM: GlobalMaterialCache, create_global_material_cache
|
||||
using ..JuliaFEM: @field_vector
|
||||
using ..JuliaFEM: compute_stress, create_zero_field, material_field_type, material_state_type
|
||||
|
||||
# Import field decoding (functions defined in included file)
|
||||
include("dof_field_info.jl")
|
||||
# Functions are now available: DOFFieldInfo, decode_local_dof, flatten_dof_indices
|
||||
|
||||
# extract_tangent! is now defined in material_cache.jl for shared use
|
||||
|
||||
"""
|
||||
DOFBasedCOOCache
|
||||
|
||||
Pre-allocated cache for DOF-based COO assembly.
|
||||
|
||||
Uses Phase 1 infrastructure from `src/assembly/dof_connectivity.jl`.
|
||||
|
||||
# Fields
|
||||
- `I::Vector{Int}`: Row indices (COO format)
|
||||
- `J::Vector{Int}`: Column indices (COO format)
|
||||
- `V::Vector{Float64}`: Values (COO format)
|
||||
- `f::Vector{Float64}`: Global force vector
|
||||
- `counter::Ref{Int}`: Current triplet count
|
||||
- `capacity::Int`: Maximum triplet capacity
|
||||
- `dof_connectivity::DOFConnectivity`: DOF → elements inverse connectivity (Phase 1)
|
||||
- `elements::Vector{<:AbstractElement}`: Element array
|
||||
- `element_cache::ElementCache`: Cache for element operations
|
||||
- `ndofs::Int`: Total DOFs in system
|
||||
"""
|
||||
mutable struct DOFBasedCOOCache{T<:AbstractTopology,B<:AbstractBasis,IPS,E<:AbstractElement,FieldType<:NamedTuple,StateType<:NamedTuple}
|
||||
I::Vector{Int}
|
||||
J::Vector{Int}
|
||||
V::Vector{Float64}
|
||||
f::Vector{Float64}
|
||||
counter::Int # Plain Int instead of Ref{Int} for zero-allocation access (struct is mutable)
|
||||
capacity::Int
|
||||
dof_connectivity::DOFConnectivity
|
||||
elements::Vector{E} # Concrete element type for type stability
|
||||
element_cache::ElementCache{T,B,IPS}
|
||||
geometry_cache::GeometryCache
|
||||
material_workspace::AssemblyMaterialWorkspace{FieldType, StateType} # Material workspace for element assembly - TYPE STABLE
|
||||
ndofs::Int
|
||||
dof_index_buffer::Vector{Int} # Pre-allocated buffer for DOF indices (zero-allocation reuse)
|
||||
last_updated_element::Int # Track last element to avoid redundant geometry updates (Int instead of Ref{Int} for type stability) - DEPRECATED: Use global caches
|
||||
global_material_cache::GlobalMaterialCache # Global material cache for zero-allocation state management
|
||||
𝔻_vec_buffer::Vector{SymmetricTensor{4,3,Float64,36}} # Pre-allocated buffer for tangent vector (zero-allocation extraction) - DEPRECATED: Use global caches
|
||||
fields_ref::FieldType # Pre-allocated NamedTuple for material fields (zero-allocation reuse) - TYPE STABLE
|
||||
empty_state::StateType # Pre-allocated empty state NamedTuple (zero-allocation reuse) - TYPE STABLE
|
||||
zero_field::FieldType # Pre-allocated zero field for reset! (zero-allocation) - TYPE STABLE
|
||||
# Global caches: one per element (pre-computed in Pass 1, reused in Pass 2)
|
||||
element_caches::Vector{ElementCache{T,B,IPS}} # Pre-computed element caches [nelems]
|
||||
geometry_caches::Vector{GeometryCache} # Pre-computed geometry caches [nelems]
|
||||
material_workspaces::Vector{AssemblyMaterialWorkspace{FieldType, StateType}} # Pre-computed material workspaces [nelems]
|
||||
tangent_buffers::Vector{Vector{SymmetricTensor{4,3,Float64,36}}} # Pre-computed tangent buffers [nelems]
|
||||
end
|
||||
|
||||
"""
|
||||
DOFBasedCOOCache(elements::Vector{<:AbstractElement}, dof_manager::DOFManager)
|
||||
|
||||
Create cache for DOF-based assembly using Phase 1 infrastructure.
|
||||
|
||||
# Arguments
|
||||
- `elements`: Element array with assigned DOF indices
|
||||
- `dof_manager`: DOF manager (for total DOF count)
|
||||
|
||||
# Returns
|
||||
- Pre-allocated DOF-based COO cache
|
||||
|
||||
# Note
|
||||
Requires elements created with `create_elements!()` (have `.dof_indices` assigned).
|
||||
"""
|
||||
function DOFBasedCOOCache(
|
||||
elements::Vector{E},
|
||||
dof_manager::DOFManager,
|
||||
mesh::AbstractMesh,
|
||||
kernel::ContinuumKernel
|
||||
) where {E<:AbstractElement}
|
||||
# Use inverse mapping from DOFManager (built during create_elements!)
|
||||
if dof_manager.dof_connectivity === nothing
|
||||
error("DOF connectivity not built. Ensure create_elements! was called with this DOFManager.")
|
||||
end
|
||||
dof_connectivity = dof_manager.dof_connectivity
|
||||
|
||||
ndofs = dof_connectivity.n_total_dofs
|
||||
|
||||
# Estimate triplet count
|
||||
# Each DOF has dof_connectivity.max_connections elements
|
||||
# Each element contributes local_dof_count entries to this row
|
||||
avg_local_dofs = sum(local_dof_count(elem) for elem in elements) / length(elements)
|
||||
entries_per_dof = dof_connectivity.max_connections * avg_local_dofs
|
||||
estimated_triplets = Int(ceil(1.2 * ndofs * entries_per_dof))
|
||||
|
||||
# Allocate triplet arrays
|
||||
I = Vector{Int}(undef, estimated_triplets)
|
||||
J = Vector{Int}(undef, estimated_triplets)
|
||||
V = Vector{Float64}(undef, estimated_triplets)
|
||||
f = zeros(Float64, ndofs)
|
||||
counter = 0 # Plain Int instead of Ref{Int} for zero-allocation access
|
||||
|
||||
# Create element cache
|
||||
element_cache = create_element_cache(mesh, kernel)
|
||||
|
||||
# Extract type parameters from element cache for type stability
|
||||
ElementCacheType = typeof(element_cache)
|
||||
T = ElementCacheType.parameters[1] # Topology type
|
||||
B = ElementCacheType.parameters[2] # Basis type
|
||||
IPS = ElementCacheType.parameters[3] # Integration points type
|
||||
|
||||
# Create geometry cache (get max nodes from mesh type)
|
||||
MeshType = typeof(mesh)
|
||||
max_nnodes = MeshType.parameters[1]::Int
|
||||
# Get number of integration points from element cache
|
||||
n_ips = length(element_cache.ips)
|
||||
geometry_cache = create_geometry_cache(max_nnodes, n_ips)
|
||||
|
||||
# CRITICAL FIX: Infer FieldType and StateType from material FIRST for type stability
|
||||
# This ensures material_workspace, fields_ref, empty_state, and zero_field are type-stable
|
||||
FieldType = material_field_type(kernel.material)
|
||||
StateType = material_state_type(kernel.material)
|
||||
|
||||
# Create material workspace with concrete type parameters (type-stable)
|
||||
material_workspace = create_material_cache(kernel.material, n_ips)::AssemblyMaterialWorkspace{FieldType, StateType}
|
||||
|
||||
# Create GlobalMaterialCache for zero-allocation material state management
|
||||
n_elems = length(elements)
|
||||
global_material_cache = create_global_material_cache(kernel.material; n_ips=n_ips, n_elems=n_elems)
|
||||
|
||||
# Pre-allocate DOF index buffer (max element DOF count)
|
||||
max_elem_dofs = isempty(elements) ? 0 : maximum(local_dof_count, elements)
|
||||
dof_index_buffer = Vector{Int}(undef, max_elem_dofs)
|
||||
last_updated_element = -1 # Track last updated element ID (Int instead of Ref{Int} for type stability)
|
||||
|
||||
# Pre-allocate buffer for tangent vector extraction (zero-allocation)
|
||||
𝔻_vec_buffer = Vector{SymmetricTensor{4,3,Float64,36}}(undef, n_ips)
|
||||
|
||||
# Pre-allocate NamedTuples for material cache updates (zero-allocation)
|
||||
# For StatelessConstantTangent, these are constant and can be reused
|
||||
# Compute once at reference configuration
|
||||
E_ref = zero(SymmetricTensor{2,3,Float64,6})
|
||||
σ_ref, 𝔻_ref, _ = compute_stress(kernel.material, E_ref, NamedTuple(), 0.0)
|
||||
fields_ref = (σ=σ_ref, 𝔻=𝔻_ref) # Pre-allocate ONCE in constructor (for material cache updates) - type inferred from FieldType
|
||||
empty_state = NamedTuple() # Pre-allocate ONCE in constructor - type inferred from StateType
|
||||
# CRITICAL FIX: Also pre-allocate zero field for reset! (zero-allocation)
|
||||
# For stateless materials, zero field should be the same as fields_ref (zero strain → zero stress)
|
||||
# But we create it explicitly to ensure it's truly zero
|
||||
zero_field = create_zero_field(FieldType) # Pre-allocate ONCE in constructor - type inferred from FieldType
|
||||
|
||||
# Pre-allocate global caches: one per element (filled in Pass 1 of assemble!)
|
||||
n_elems = length(elements)
|
||||
element_caches = Vector{ElementCache{T,B,IPS}}(undef, n_elems)
|
||||
geometry_caches = Vector{GeometryCache}(undef, n_elems)
|
||||
material_workspaces = Vector{AssemblyMaterialWorkspace{FieldType, StateType}}(undef, n_elems)
|
||||
tangent_buffers = Vector{Vector{SymmetricTensor{4,3,Float64,36}}}(undef, n_elems)
|
||||
|
||||
# Initialize each element's caches (pre-allocate, will be filled in assemble! Pass 1)
|
||||
for elem_id in 1:n_elems
|
||||
element_caches[elem_id] = create_element_cache(mesh, kernel)
|
||||
geometry_caches[elem_id] = create_geometry_cache(max_nnodes, n_ips)
|
||||
material_workspaces[elem_id] = create_material_cache(kernel.material, n_ips)::AssemblyMaterialWorkspace{FieldType, StateType}
|
||||
tangent_buffers[elem_id] = Vector{SymmetricTensor{4,3,Float64,36}}(undef, n_ips)
|
||||
end
|
||||
|
||||
return DOFBasedCOOCache{T,B,IPS,E,FieldType,StateType}(I, J, V, f, counter, estimated_triplets,
|
||||
dof_connectivity, elements, element_cache, geometry_cache, material_workspace, ndofs,
|
||||
dof_index_buffer, last_updated_element, global_material_cache, 𝔻_vec_buffer,
|
||||
fields_ref, empty_state, zero_field,
|
||||
element_caches, geometry_caches, material_workspaces, tangent_buffers)
|
||||
end
|
||||
|
||||
"""
|
||||
reset!(cache::DOFBasedCOOCache)
|
||||
|
||||
Reset cache for new assembly (zero force vector, reset counter).
|
||||
"""
|
||||
function reset!(cache::DOFBasedCOOCache{T,B,IPS,E,FieldType,StateType}) where {T,B,IPS,E,FieldType,StateType}
|
||||
fill!(cache.f, 0.0)
|
||||
cache.counter = 0 # Direct assignment for zero-allocation access
|
||||
cache.last_updated_element = -1 # Reset element tracking (direct assignment for type stability)
|
||||
# CRITICAL FIX: Use pre-allocated zero values to avoid create_zero_field allocation
|
||||
# zero_field is pre-allocated in constructor, empty_state is the zero state
|
||||
reset!(cache.material_workspace, cache.zero_field, cache.empty_state) # Zero-allocation reset
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
assemble!(
|
||||
cache::DOFBasedCOOCache,
|
||||
assembler::DOFBasedCOOAssembler,
|
||||
kernel::ContinuumKernel,
|
||||
mesh::AbstractMesh
|
||||
) -> Nothing
|
||||
|
||||
Assemble global system using **DOF-based traversal** with Phase 1 infrastructure.
|
||||
|
||||
**SINGLE-KERNEL ASSUMPTION**: All elements use the same kernel (physics).
|
||||
|
||||
# Algorithm
|
||||
|
||||
```julia
|
||||
for dof_i in 1:ndofs # Each matrix row!
|
||||
# Get all elements touching this DOF (Phase 1 connectivity)
|
||||
for conn in dof_connectivity.dof_to_elements[dof_i]
|
||||
element = elements[conn.elem_id]
|
||||
local_i = conn.local_dof_idx # Within element's flattened DOF list
|
||||
|
||||
# Decode which field this DOF represents
|
||||
field_info_i = decode_local_dof(element, local_i)
|
||||
|
||||
# Prepare element geometry ONCE
|
||||
prepared = prepare_element!(cache.element_cache, kernel, conn.elem_id, mesh)
|
||||
|
||||
# Compute all entries in this row for this element
|
||||
flat_dofs_j = flatten_dof_indices(element.dof_indices)
|
||||
for (local_j, dof_j) in enumerate(flat_dofs_j)
|
||||
field_info_j = decode_local_dof(element, local_j)
|
||||
|
||||
# Compute single scalar entry K[i,j]
|
||||
K_ij = compute_entry!(prepared, kernel.material,
|
||||
field_info_i, field_info_j)
|
||||
|
||||
# Scatter to triplets
|
||||
scatter_entry!(cache, K_ij, dof_i, dof_j)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
# Zero-Allocation (After Warmup)
|
||||
|
||||
All arrays pre-allocated. Element preparation reuses cache buffers.
|
||||
|
||||
# Arguments
|
||||
- `cache`: Pre-allocated DOF-based COO cache
|
||||
- `assembler`: DOF-based COO assembler
|
||||
- `kernel`: Continuum kernel (SAME for all elements)
|
||||
- `mesh`: Finite element mesh
|
||||
"""
|
||||
function assemble!(
|
||||
cache::DOFBasedCOOCache{T,B,IPS,E,FieldType,StateType},
|
||||
assembler::DOFBasedCOOAssembler,
|
||||
kernel::ContinuumKernel,
|
||||
mesh::AbstractMesh
|
||||
) where {T,B,IPS,E<:AbstractElement,FieldType,StateType}
|
||||
# Reset cache
|
||||
reset!(cache)
|
||||
|
||||
# Cache ndofs_per_node outside loop (compile-time constant for single-field)
|
||||
ndofs_per_node = dofs_per_node(kernel)
|
||||
|
||||
# Cache field accesses
|
||||
elements = cache.elements
|
||||
global_material_cache = cache.global_material_cache
|
||||
fields_ref = cache.fields_ref
|
||||
empty_state = cache.empty_state
|
||||
ndofs = cache.ndofs
|
||||
mesh_connectivity = mesh.connectivity
|
||||
|
||||
# Cache global cache vectors
|
||||
element_caches = cache.element_caches
|
||||
geometry_caches = cache.geometry_caches
|
||||
material_workspaces = cache.material_workspaces
|
||||
tangent_buffers = cache.tangent_buffers
|
||||
|
||||
n_elems = length(elements)
|
||||
|
||||
# ========================================================================
|
||||
# PASS 1: Pre-compute element, geometry, and material caches for ALL elements
|
||||
# ========================================================================
|
||||
# Loop through all elements ONCE and compute their caches
|
||||
# This eliminates redundant recalculations when multiple DOFs touch the same element
|
||||
@inbounds for elem_id in 1:n_elems
|
||||
# Get pre-allocated caches for this element
|
||||
element_cache = element_caches[elem_id]
|
||||
geometry_cache = geometry_caches[elem_id]
|
||||
material_workspace = material_workspaces[elem_id]
|
||||
𝔻_vec_buffer = tangent_buffers[elem_id]
|
||||
|
||||
# Reset caches for this element (required by update functions)
|
||||
reset!(element_cache)
|
||||
reset!(geometry_cache)
|
||||
reset!(material_workspace)
|
||||
|
||||
# Update element cache (extract DOF mapping, displacements if any)
|
||||
update_element_cache!(element_cache, kernel, elem_id, mesh, nothing)
|
||||
|
||||
# Update geometry cache (extract coordinates, compute gradients, detJ*w)
|
||||
update_geometry_cache!(geometry_cache, element_cache, kernel, elem_id, mesh)
|
||||
|
||||
# Update material workspace (compute stress, tangent, internal state)
|
||||
# Use GlobalMaterialCache for zero-allocation state management
|
||||
# Direct getfield access - zero allocation, type-stable
|
||||
fields_mw = getfield(material_workspace, 1) # Direct field access
|
||||
states_mw = getfield(material_workspace, 2) # Direct field access
|
||||
ips_ec = getfield(element_cache, :ips) # Direct field access
|
||||
nips = length(ips_ec) # Use cached reference
|
||||
# Use pre-allocated NamedTuples (zero-allocation)
|
||||
@inbounds for q in 1:nips
|
||||
fields_mw[q] = fields_ref # Reuse pre-allocated NT - zero allocation
|
||||
states_mw[q] = empty_state # Reuse pre-allocated empty state
|
||||
end
|
||||
|
||||
# Extract tangent vector to pre-allocated buffer (zero-allocation)
|
||||
fields = getfield(material_workspace, 1) # Direct field access - zero allocation, type-stable
|
||||
extract_tangent!(𝔻_vec_buffer, fields, FieldType) # Type-stable extraction using compile-time field index
|
||||
end
|
||||
|
||||
# ========================================================================
|
||||
# PASS 2: Loop through DOFs and use pre-computed caches
|
||||
# ========================================================================
|
||||
# Cache DOF connectivity
|
||||
dof_connectivity = cache.dof_connectivity
|
||||
dof_to_elements = dof_connectivity.dof_to_elements
|
||||
|
||||
# DOF LOOP: One iteration per DOF (matrix row)
|
||||
# Use explicit range to avoid iterator state allocation
|
||||
@inbounds for dof_i in 1:ndofs
|
||||
# Get all elements touching this DOF (Phase 1 connectivity)
|
||||
# Pre-extract vector reference to avoid repeated access and enable type stability
|
||||
@inbounds touching_elements = dof_to_elements[dof_i]
|
||||
|
||||
# Optimized iteration: use indexed loop instead of iterator to avoid allocation
|
||||
n_conns = length(touching_elements)
|
||||
@inbounds for conn_idx in 1:n_conns
|
||||
conn = touching_elements[conn_idx]
|
||||
# Use accessor functions (zero-allocation for small integer conversions)
|
||||
# These convert Int32/Int16 to Int, which is zero-allocation for small values
|
||||
elem_id_val = elem_id(conn) # Int32 → Int (no allocation for small values)
|
||||
local_i = local_dof_idx(conn) # Int16 → Int (no allocation for small values)
|
||||
|
||||
# Type-stable element access (E is concrete type from cache parameters)
|
||||
@inbounds element = elements[elem_id_val]::E
|
||||
|
||||
# Look up pre-computed caches for this element (from Pass 1)
|
||||
element_cache = element_caches[elem_id_val]
|
||||
geometry_cache = geometry_caches[elem_id_val]
|
||||
𝔻_vec = tangent_buffers[elem_id_val] # Pre-extracted tangent vector
|
||||
|
||||
# Both element.dof_indices and element_cache.dofs follow connectivity order
|
||||
# (DOF manager iterates over connectivity, get_dof_mapping! iterates over connectivity)
|
||||
# So local_i directly maps to connectivity node index via node-major ordering
|
||||
node_i = div(local_i - 1, ndofs_per_node) + 1 # Local node index in connectivity order
|
||||
comp_i = mod(local_i - 1, ndofs_per_node) + 1 # Component (1=x, 2=y, 3=z)
|
||||
|
||||
# Get DOF count for this element (use cached connectivity length)
|
||||
# Cache connectivity tuple access to avoid repeated allocation
|
||||
@inbounds conn_tuple = mesh_connectivity[elem_id_val]
|
||||
nnodes_elem = length(conn_tuple) # Compile-time known for fixed topology
|
||||
ndofs_elem = nnodes_elem * ndofs_per_node
|
||||
|
||||
# Use element_cache.dofs for scattering (same as element-based assembler)
|
||||
# element_cache.dofs is filled by update_element_cache! and matches connectivity order
|
||||
# Direct indexing is zero-allocation (no view needed for small arrays)
|
||||
dofs_elem = element_cache.dofs
|
||||
|
||||
# Get global DOF for row (from element_cache.dofs, matching element-based assembler)
|
||||
dof_i_global = dofs_elem[local_i]
|
||||
|
||||
# Compute all entries in this row for this element
|
||||
# Use @inbounds for zero-allocation iteration
|
||||
@inbounds for local_j in 1:ndofs_elem
|
||||
# Get global DOF for column (from element_cache.dofs, matching element-based assembler)
|
||||
dof_j_global = dofs_elem[local_j]
|
||||
|
||||
# Decode node index and component from local_j (same ordering as local_i)
|
||||
node_j = div(local_j - 1, ndofs_per_node) + 1 # Local node index in connectivity order
|
||||
comp_j = mod(local_j - 1, ndofs_per_node) + 1 # Component (1=x, 2=y, 3=z)
|
||||
|
||||
# Compute SINGLE SCALAR entry K[i,j] using node indices directly
|
||||
# node_i and node_j are local node indices matching geometry cache indexing
|
||||
# (both element_cache.dofs and geometry_cache follow connectivity order)
|
||||
# Use cached references to avoid repeated field access allocations
|
||||
K_ij = compute_entry_direct!(
|
||||
geometry_cache,
|
||||
𝔻_vec, # Pass pre-extracted vector from Pass 1
|
||||
node_i, comp_i,
|
||||
node_j, comp_j
|
||||
)
|
||||
|
||||
# Scatter single entry to triplets using global DOFs from element_cache.dofs
|
||||
scatter_entry!(cache, K_ij, dof_i_global, dof_j_global)
|
||||
end
|
||||
end # End conn_idx loop
|
||||
end # End dof_i loop
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
compute_entry!(
|
||||
geometry_cache::GeometryCache,
|
||||
material::AbstractMaterial,
|
||||
field_info_i::DOFFieldInfo,
|
||||
field_info_j::DOFFieldInfo
|
||||
) -> Float64
|
||||
|
||||
Compute SINGLE SCALAR stiffness matrix entry K[i,j] using field info.
|
||||
|
||||
This is the atomic kernel - computes ONE matrix entry using field information
|
||||
decoded from the element's DOF specification.
|
||||
|
||||
Uses the generic `compute_stiffness_value` from continuum kernel,
|
||||
which works for ANY material providing an elasticity tensor C.
|
||||
|
||||
# Arguments
|
||||
- `geometry_cache`: Geometry cache with prepared element data
|
||||
- `material`: Material model (provides C)
|
||||
- `field_info_i`: Field information for row DOF
|
||||
- `field_info_j`: Field information for column DOF
|
||||
|
||||
# Returns
|
||||
- Single scalar value K[i,j]
|
||||
|
||||
# Field Coupling
|
||||
|
||||
For single-field (pure displacement):
|
||||
- If field_i.field == field_j.field == :u → mechanical stiffness
|
||||
|
||||
For multi-field:
|
||||
- Same field → diagonal coupling (u-u, T-T, p-p, etc.)
|
||||
- Different fields → off-diagonal coupling (u-T, T-u, u-p, etc.)
|
||||
- Coupling logic dispatched based on (field_i.field, field_j.field) pair
|
||||
|
||||
# Zero-Allocation
|
||||
|
||||
Uses geometry cache buffers, no new allocations.
|
||||
"""
|
||||
@inline function compute_entry_direct!(
|
||||
geometry_cache::GeometryCache,
|
||||
𝔻_vec::Vector{SymmetricTensor{4,3,Float64,36}}, # Pre-extracted vector (zero-allocation)
|
||||
node_i::Int,
|
||||
comp_i::Int,
|
||||
node_j::Int,
|
||||
comp_j::Int
|
||||
)
|
||||
# CRITICAL FIX: Accept pre-extracted 𝔻_vec instead of material_workspace
|
||||
# This eliminates allocations from get_tangent_vector() which creates a new vector
|
||||
# get_tangent_vector() is now called ONCE per element (outside this function)
|
||||
|
||||
K_ij = 0.0
|
||||
|
||||
# Integration loop over quadrature points
|
||||
n_ips = length(geometry_cache.detJ_w)
|
||||
@inbounds for q in 1:n_ips
|
||||
# Basis function gradients (node_i and node_j are local node indices matching geometry cache)
|
||||
∇N_i = geometry_cache.∇N_data[q, node_i] # Vec{3}
|
||||
∇N_j = geometry_cache.∇N_data[q, node_j] # Vec{3}
|
||||
|
||||
# detJ * weight
|
||||
detJ_w = geometry_cache.detJ_w[q]
|
||||
|
||||
# Get tangent at this integration point
|
||||
𝔻_q = 𝔻_vec[q] # SymmetricTensor{4,3}
|
||||
# Convert SymmetricTensor{4,3} to Tensor{4,3} for compute_stiffness_value
|
||||
# This conversion should be zero-allocation (same memory layout)
|
||||
C = Tensor{4,3}(𝔻_q)
|
||||
|
||||
# Compute single scalar entry using GENERIC kernel
|
||||
value_at_qp = compute_stiffness_value(
|
||||
∇N_i, ∇N_j, C,
|
||||
comp_i, # α (1=x, 2=y, 3=z)
|
||||
comp_j # β
|
||||
)
|
||||
|
||||
K_ij += value_at_qp * detJ_w
|
||||
end
|
||||
|
||||
return K_ij
|
||||
end
|
||||
|
||||
"""
|
||||
scatter_entry!(
|
||||
cache::DOFBasedCOOCache,
|
||||
value::Float64,
|
||||
dof_i::Int,
|
||||
dof_j::Int
|
||||
)
|
||||
|
||||
Scatter single matrix entry to COO triplets **in-place**.
|
||||
|
||||
# Arguments
|
||||
- `cache`: DOF-based COO cache
|
||||
- `value`: Matrix entry value
|
||||
- `dof_i`: Row DOF index
|
||||
- `dof_j`: Column DOF index
|
||||
|
||||
# Zero-Allocation
|
||||
|
||||
Writes to pre-allocated triplet arrays, updates counter.
|
||||
"""
|
||||
@inline function scatter_entry!(
|
||||
cache::DOFBasedCOOCache{T,B,IPS,E,FieldType,StateType},
|
||||
value::Float64,
|
||||
dof_i::Int,
|
||||
dof_j::Int
|
||||
) where {T,B,IPS,E,FieldType,StateType}
|
||||
# Direct access to Int field (zero-allocation, no Ref indirection)
|
||||
counter = cache.counter
|
||||
|
||||
# Check capacity (bounds check - necessary for safety)
|
||||
if counter + 1 > cache.capacity
|
||||
error("DOF-based COO cache overflow: need $(counter + 1) triplets, " *
|
||||
"capacity is $(cache.capacity). Increase cache size.")
|
||||
end
|
||||
|
||||
# Add triplet (use @inbounds since we checked capacity)
|
||||
counter += 1
|
||||
@inbounds begin
|
||||
cache.I[counter] = dof_i
|
||||
cache.J[counter] = dof_j
|
||||
cache.V[counter] = value
|
||||
end
|
||||
|
||||
cache.counter = counter # Direct assignment for zero-allocation access
|
||||
return nothing
|
||||
end
|
||||
|
||||
"""
|
||||
extract_system(cache::DOFBasedCOOCache) -> (K, f)
|
||||
|
||||
Build sparse matrix from triplets and return system.
|
||||
|
||||
# Arguments
|
||||
- `cache`: Assembled DOF-based COO cache
|
||||
|
||||
# Returns
|
||||
- `K::SparseMatrixCSC`: Global stiffness matrix
|
||||
- `f::Vector`: Global force vector
|
||||
"""
|
||||
function extract_system(cache::DOFBasedCOOCache{T,B,IPS,E,FieldType,StateType}) where {T,B,IPS,E,FieldType,StateType}
|
||||
ntriplets = cache.counter # Direct access to Int field
|
||||
|
||||
# Build sparse matrix (duplicates are summed automatically)
|
||||
I_used = @view cache.I[1:ntriplets]
|
||||
J_used = @view cache.J[1:ntriplets]
|
||||
V_used = @view cache.V[1:ntriplets]
|
||||
|
||||
K = sparse(I_used, J_used, V_used, cache.ndofs, cache.ndofs)
|
||||
|
||||
return K, cache.f
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
"""
|
||||
create_cache(
|
||||
assembler::DOFBasedCOOAssembler,
|
||||
elements::Vector{<:AbstractElement},
|
||||
dof_manager::DOFManager,
|
||||
mesh::AbstractMesh,
|
||||
kernel::ContinuumKernel
|
||||
) -> DOFBasedCOOCache
|
||||
|
||||
Create pre-allocated cache for DOF-based COO assembly.
|
||||
|
||||
Requires elements with assigned DOF indices (from `create_elements!`).
|
||||
|
||||
# Example
|
||||
|
||||
```julia
|
||||
# Create elements with DOF assignment
|
||||
S = @DOFSet{u::DOF{Displacement{3}, Vertex}}
|
||||
ElemType = Element{Hexahedron{8}, Lagrange{1}, S}
|
||||
elements, dof_mgr = create_elements!(mesh, ElemType)
|
||||
|
||||
# Create kernel
|
||||
kernel = ContinuumKernel(ContinuumFormulation{FullThreeD}(), material, Displacement{3}())
|
||||
|
||||
# Create assembler and cache
|
||||
assembler = DOFBasedCOOAssembler()
|
||||
cache = create_cache(assembler, elements, dof_mgr, mesh, kernel)
|
||||
|
||||
# Assemble
|
||||
assemble!(cache, assembler, kernel, mesh)
|
||||
K, f = extract_system(cache)
|
||||
```
|
||||
"""
|
||||
function create_cache(
|
||||
assembler::DOFBasedCOOAssembler,
|
||||
elements::Vector{E},
|
||||
dof_manager::DOFManager,
|
||||
mesh::AbstractMesh,
|
||||
kernel::ContinuumKernel
|
||||
) where {E<:AbstractElement}
|
||||
return DOFBasedCOOCache(elements, dof_manager, mesh, kernel)
|
||||
end
|
||||
Reference in New Issue
Block a user