2025-11-18 18:02:30 +02:00
|
|
|
|
# This file is a part of JuliaFEM.
|
|
|
|
|
|
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
COO (Coordinate format) assembly implementation.
|
|
|
|
|
|
|
|
|
|
|
|
Classical element-by-element assembly using triplet vectors (I, J, V).
|
|
|
|
|
|
Accumulates all element contributions, builds sparse matrix at end.
|
|
|
|
|
|
|
|
|
|
|
|
**Performance**: Baseline (1.0x), moderate memory usage.
|
|
|
|
|
|
**Best for**: Prototyping, debugging, simple problems.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
using SparseArrays
|
2025-12-12 23:29:02 +02:00
|
|
|
|
using ..JuliaFEM: GlobalMaterialCache, get_tangent, get_tangent_vector, extract_tangent!
|
2025-11-18 18:02:30 +02:00
|
|
|
|
|
2025-11-20 16:56:36 +02:00
|
|
|
|
# Include scatter implementations
|
|
|
|
|
|
include("scatter_to_triplets.jl")
|
|
|
|
|
|
include("scatter_blocks_to_triplets.jl")
|
|
|
|
|
|
include("scatter_blocks_to_triplets_symmetric.jl")
|
|
|
|
|
|
include("scatter_blocks_to_triplets_symmetric_manually_unrolled.jl")
|
|
|
|
|
|
include("scatter_blocks_to_triplets_symmetric_direct.jl")
|
|
|
|
|
|
include("scatter_to_force.jl")
|
|
|
|
|
|
include("scatter_blocks_to_force.jl")
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
assemble_element!(
|
|
|
|
|
|
element_cache::ElementCache,
|
|
|
|
|
|
geometry_cache::GeometryCache,
|
2025-12-12 23:29:02 +02:00
|
|
|
|
material_workspace::AssemblyMaterialWorkspace,
|
|
|
|
|
|
kernel::AbstractKernel,
|
|
|
|
|
|
elem_id::Int,
|
|
|
|
|
|
mesh::AbstractMesh,
|
|
|
|
|
|
N::Int,
|
|
|
|
|
|
u_global::Union{Nothing,Vector{Vec{3,Float64}}},
|
|
|
|
|
|
global_cache::GlobalMaterialCache,
|
|
|
|
|
|
Δt::Float64
|
|
|
|
|
|
) -> Nothing
|
|
|
|
|
|
|
|
|
|
|
|
Assemble a single element using GlobalMaterialCache (NEW API).
|
|
|
|
|
|
|
|
|
|
|
|
**New API:** Uses `GlobalMaterialCache` for persistent state storage.
|
|
|
|
|
|
State is read from and written to `global_cache` automatically.
|
|
|
|
|
|
|
|
|
|
|
|
This function encapsulates all operations performed on a single element:
|
|
|
|
|
|
1. Reset caches
|
|
|
|
|
|
2. Update element cache (extract displacements, DOF mapping)
|
|
|
|
|
|
3. Update geometry cache (extract coordinates, compute gradients, detJ*w)
|
|
|
|
|
|
4. Update material workspace (compute stress, tangent, internal state)
|
|
|
|
|
|
- Reads old state from `global_cache`
|
|
|
|
|
|
- Writes new state to `global_cache`
|
|
|
|
|
|
5. Compute element stiffness blocks
|
|
|
|
|
|
|
|
|
|
|
|
# Arguments
|
|
|
|
|
|
- `element_cache`: Element cache to update
|
|
|
|
|
|
- `geometry_cache`: Geometry cache to update
|
|
|
|
|
|
- `material_workspace`: Assembly material workspace to update (per-element temporary)
|
|
|
|
|
|
- `kernel`: Domain kernel
|
|
|
|
|
|
- `elem_id`: Current element ID
|
|
|
|
|
|
- `mesh`: Finite element mesh
|
|
|
|
|
|
- `N`: Number of nodes per element (compile-time constant)
|
|
|
|
|
|
- `u_global`: Global displacement field (nothing for linear analysis)
|
|
|
|
|
|
- `global_cache`: Global material cache (persistent state storage)
|
|
|
|
|
|
- `Δt`: Time increment
|
|
|
|
|
|
|
|
|
|
|
|
# Zero-Allocation Guarantee
|
|
|
|
|
|
This function should have ZERO allocations when called in a loop.
|
|
|
|
|
|
All operations are in-place mutations of pre-allocated caches.
|
|
|
|
|
|
"""
|
|
|
|
|
|
function assemble_element!(
|
|
|
|
|
|
element_cache::ElementCache,
|
|
|
|
|
|
geometry_cache::GeometryCache,
|
|
|
|
|
|
material_workspace::AssemblyMaterialWorkspace{FieldType, StateType},
|
|
|
|
|
|
kernel::AbstractKernel,
|
|
|
|
|
|
elem_id::Int,
|
|
|
|
|
|
mesh::AbstractMesh,
|
|
|
|
|
|
N::Int,
|
|
|
|
|
|
u_global::Union{Nothing,Vector{Vec{3,Float64}}},
|
|
|
|
|
|
global_cache::GlobalMaterialCache,
|
|
|
|
|
|
Δt::Float64,
|
|
|
|
|
|
𝔻_vec_buffer::Vector{SymmetricTensor{4,3,Float64,36}}
|
|
|
|
|
|
) where {FieldType<:NamedTuple, StateType<:NamedTuple}
|
|
|
|
|
|
# Reset caches for new element
|
|
|
|
|
|
reset!(element_cache)
|
|
|
|
|
|
reset!(geometry_cache)
|
|
|
|
|
|
reset!(material_workspace)
|
|
|
|
|
|
|
|
|
|
|
|
# PHASE 1: Update element cache (extract displacements, DOF mapping)
|
|
|
|
|
|
update_element_cache!(element_cache, kernel, elem_id, mesh, u_global)
|
|
|
|
|
|
|
|
|
|
|
|
# PHASE 2: Update geometry cache (extract coordinates, compute gradients, detJ*w)
|
|
|
|
|
|
update_geometry_cache!(geometry_cache, element_cache, kernel, elem_id, mesh)
|
|
|
|
|
|
|
|
|
|
|
|
# PHASE 3: Update material workspace (compute stress, tangent, internal state)
|
|
|
|
|
|
# Uses GlobalMaterialCache - reads old state, writes new state
|
|
|
|
|
|
update_material_cache!(material_workspace, geometry_cache, kernel.material, element_cache, global_cache, elem_id, Δt)
|
|
|
|
|
|
|
|
|
|
|
|
# PHASE 4: Compute element stiffness blocks
|
|
|
|
|
|
# Assemble only upper triangle (k ≤ l) since stiffness matrix is symmetric
|
|
|
|
|
|
# This halves computation and memory usage
|
|
|
|
|
|
# Extract tangent vector ONCE before loop for zero-allocation access
|
|
|
|
|
|
# CRITICAL FIX: Use extract_tangent! with 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
|
|
|
|
|
|
𝔻_vec = 𝔻_vec_buffer # Use buffer directly (zero allocation)
|
|
|
|
|
|
@inbounds for k in 1:N, l in k:N # Only l ≥ k (upper triangle)
|
|
|
|
|
|
compute_block!(
|
|
|
|
|
|
element_cache.K_blocks,
|
|
|
|
|
|
geometry_cache.∇N_data,
|
|
|
|
|
|
geometry_cache.detJ_w,
|
|
|
|
|
|
𝔻_vec,
|
|
|
|
|
|
k, l
|
|
|
|
|
|
)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
return nothing
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
assemble_element!(
|
|
|
|
|
|
element_cache::ElementCache,
|
|
|
|
|
|
geometry_cache::GeometryCache,
|
|
|
|
|
|
material_workspace::AssemblyMaterialWorkspace,
|
2025-11-20 16:56:36 +02:00
|
|
|
|
kernel::AbstractKernel,
|
|
|
|
|
|
elem_id::Int,
|
|
|
|
|
|
mesh::AbstractMesh,
|
|
|
|
|
|
N::Int,
|
|
|
|
|
|
u_global::Union{Nothing,Vector{Vec{3,Float64}}},
|
|
|
|
|
|
state_old::Union{Nothing,Matrix{<:AbstractMaterialState}},
|
|
|
|
|
|
Δt::Float64
|
|
|
|
|
|
) -> Nothing
|
|
|
|
|
|
|
2025-12-12 23:29:02 +02:00
|
|
|
|
Assemble a single element (LEGACY API - Matrix{<:AbstractMaterialState}).
|
|
|
|
|
|
|
|
|
|
|
|
**Legacy API:** Uses `Matrix{<:AbstractMaterialState}` for state storage.
|
|
|
|
|
|
For new code, prefer `GlobalMaterialCache` overload.
|
2025-11-20 16:56:36 +02:00
|
|
|
|
|
|
|
|
|
|
This function encapsulates all operations performed on a single element:
|
|
|
|
|
|
1. Reset caches
|
|
|
|
|
|
2. Update element cache (extract displacements, DOF mapping)
|
|
|
|
|
|
3. Update geometry cache (extract coordinates, compute gradients, detJ*w)
|
2025-12-12 23:29:02 +02:00
|
|
|
|
4. Update material workspace (compute stress, tangent, internal state)
|
2025-11-20 16:56:36 +02:00
|
|
|
|
5. Compute element stiffness blocks
|
|
|
|
|
|
|
|
|
|
|
|
# Arguments
|
|
|
|
|
|
- `element_cache`: Element cache to update
|
|
|
|
|
|
- `geometry_cache`: Geometry cache to update
|
2025-12-12 23:29:02 +02:00
|
|
|
|
- `material_workspace`: Assembly material workspace to update (per-element temporary)
|
2025-11-20 16:56:36 +02:00
|
|
|
|
- `kernel`: Domain kernel
|
|
|
|
|
|
- `elem_id`: Current element ID
|
|
|
|
|
|
- `mesh`: Finite element mesh
|
|
|
|
|
|
- `N`: Number of nodes per element (compile-time constant)
|
|
|
|
|
|
- `u_global`: Global displacement field (nothing for linear analysis)
|
2025-12-12 23:29:02 +02:00
|
|
|
|
- `state_old`: Global material state (nothing for stateless materials) - LEGACY
|
2025-11-20 16:56:36 +02:00
|
|
|
|
- `Δt`: Time increment
|
|
|
|
|
|
|
|
|
|
|
|
# Zero-Allocation Guarantee
|
|
|
|
|
|
This function should have ZERO allocations when called in a loop.
|
|
|
|
|
|
All operations are in-place mutations of pre-allocated caches.
|
|
|
|
|
|
"""
|
|
|
|
|
|
function assemble_element!(
|
|
|
|
|
|
element_cache::ElementCache,
|
|
|
|
|
|
geometry_cache::GeometryCache,
|
2025-12-12 23:29:02 +02:00
|
|
|
|
material_workspace::AssemblyMaterialWorkspace{FieldType, StateType},
|
2025-11-20 16:56:36 +02:00
|
|
|
|
kernel::AbstractKernel,
|
|
|
|
|
|
elem_id::Int,
|
|
|
|
|
|
mesh::AbstractMesh,
|
|
|
|
|
|
N::Int,
|
|
|
|
|
|
u_global::Union{Nothing,Vector{Vec{3,Float64}}},
|
|
|
|
|
|
state_old::Union{Nothing,Matrix{<:AbstractMaterialState}},
|
2025-12-12 23:29:02 +02:00
|
|
|
|
Δt::Float64,
|
|
|
|
|
|
𝔻_vec_buffer::Vector{SymmetricTensor{4,3,Float64,36}}
|
|
|
|
|
|
) where {FieldType<:NamedTuple, StateType<:NamedTuple}
|
2025-11-20 16:56:36 +02:00
|
|
|
|
# Reset caches for new element
|
|
|
|
|
|
reset!(element_cache)
|
|
|
|
|
|
reset!(geometry_cache)
|
2025-12-12 23:29:02 +02:00
|
|
|
|
reset!(material_workspace)
|
2025-11-20 16:56:36 +02:00
|
|
|
|
|
|
|
|
|
|
# PHASE 1: Update element cache (extract displacements, DOF mapping)
|
|
|
|
|
|
update_element_cache!(element_cache, kernel, elem_id, mesh, u_global)
|
|
|
|
|
|
|
|
|
|
|
|
# PHASE 2: Update geometry cache (extract coordinates, compute gradients, detJ*w)
|
|
|
|
|
|
update_geometry_cache!(geometry_cache, element_cache, kernel, elem_id, mesh)
|
|
|
|
|
|
|
2025-12-12 23:29:02 +02:00
|
|
|
|
# PHASE 3: Update material workspace (compute stress, tangent, internal state)
|
|
|
|
|
|
update_material_cache!(material_workspace, geometry_cache, kernel.material, element_cache, state_old, elem_id, Δt)
|
2025-11-20 16:56:36 +02:00
|
|
|
|
|
|
|
|
|
|
# PHASE 4: Compute element stiffness blocks
|
|
|
|
|
|
# Assemble only upper triangle (k ≤ l) since stiffness matrix is symmetric
|
|
|
|
|
|
# This halves computation and memory usage
|
2025-12-12 23:29:02 +02:00
|
|
|
|
# Extract tangent vector ONCE before loop for zero-allocation access
|
|
|
|
|
|
# CRITICAL FIX: Use extract_tangent! with 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
|
|
|
|
|
|
𝔻_vec = 𝔻_vec_buffer # Use buffer directly (zero allocation)
|
2025-11-20 16:56:36 +02:00
|
|
|
|
@inbounds for k in 1:N, l in k:N # Only l ≥ k (upper triangle)
|
2025-11-20 17:41:58 +02:00
|
|
|
|
compute_block!(
|
|
|
|
|
|
element_cache.K_blocks,
|
|
|
|
|
|
geometry_cache.∇N_data,
|
|
|
|
|
|
geometry_cache.detJ_w,
|
2025-12-12 23:29:02 +02:00
|
|
|
|
𝔻_vec,
|
2025-11-20 17:41:58 +02:00
|
|
|
|
k, l
|
|
|
|
|
|
)
|
2025-11-20 16:56:36 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
return nothing
|
|
|
|
|
|
end
|
|
|
|
|
|
|
2025-12-12 23:29:02 +02:00
|
|
|
|
"""
|
|
|
|
|
|
assemble!(
|
|
|
|
|
|
cache::COOCache,
|
|
|
|
|
|
assembler::COOAssembler,
|
|
|
|
|
|
kernel::AbstractKernel,
|
|
|
|
|
|
mesh::AbstractMesh,
|
|
|
|
|
|
u_global::Union{Nothing,Vector{Vec{3,Float64}}},
|
|
|
|
|
|
global_cache::GlobalMaterialCache,
|
|
|
|
|
|
Δt::Float64
|
|
|
|
|
|
) -> Nothing
|
|
|
|
|
|
|
|
|
|
|
|
Assemble stiffness matrix and force vector using GlobalMaterialCache (NEW API).
|
|
|
|
|
|
|
|
|
|
|
|
**New API:** Uses `GlobalMaterialCache` for persistent state storage.
|
|
|
|
|
|
State is automatically read from and written to `global_cache` during assembly.
|
|
|
|
|
|
|
|
|
|
|
|
# Arguments
|
|
|
|
|
|
- `cache`: Pre-allocated COO cache
|
|
|
|
|
|
- `assembler`: COO assembler
|
|
|
|
|
|
- `kernel`: Domain kernel (continuum, plate, beam, etc.)
|
|
|
|
|
|
- `mesh`: Finite element mesh
|
|
|
|
|
|
- `u_global`: Global displacement field [nnodes] as Vec{3} (nothing for linear analysis)
|
|
|
|
|
|
- `global_cache`: Global material cache (persistent state storage)
|
|
|
|
|
|
- `Δt`: Time increment (for rate-dependent materials)
|
|
|
|
|
|
|
|
|
|
|
|
# Side Effects
|
|
|
|
|
|
- Mutates `cache.I`, `cache.J`, `cache.V` (triplets)
|
|
|
|
|
|
- Mutates `cache.f` (force vector)
|
|
|
|
|
|
- Writes new state to `global_cache` via `set_state!()`
|
|
|
|
|
|
|
|
|
|
|
|
# Zero-Allocation Guarantee
|
|
|
|
|
|
No allocations during assembly loop. All arrays pre-allocated in cache.
|
|
|
|
|
|
Only allocation: `sparse(I, J, V)` in `extract_system(cache)` (called once).
|
|
|
|
|
|
"""
|
|
|
|
|
|
function assemble!(
|
|
|
|
|
|
cache::COOCache,
|
|
|
|
|
|
assembler::COOAssembler,
|
|
|
|
|
|
kernel::AbstractKernel,
|
|
|
|
|
|
mesh::AbstractMesh,
|
|
|
|
|
|
u_global::Union{Nothing,Vector{Vec{3,Float64}}},
|
|
|
|
|
|
global_cache::GlobalMaterialCache,
|
|
|
|
|
|
Δt::Float64
|
|
|
|
|
|
)
|
|
|
|
|
|
# Extract compile-time constants from mesh type parameters
|
|
|
|
|
|
# Mesh{N,T} where N = nodes per element, T = topology type
|
|
|
|
|
|
MeshType = typeof(mesh)
|
|
|
|
|
|
N = MeshType.parameters[1]::Int # Compile-time constant for loop unrolling
|
|
|
|
|
|
|
|
|
|
|
|
# Reset cache for new assembly
|
|
|
|
|
|
reset!(cache)
|
|
|
|
|
|
|
|
|
|
|
|
nelems = nelements(mesh)
|
|
|
|
|
|
element_cache = cache.element_cache
|
|
|
|
|
|
geometry_cache = cache.geometry_cache
|
|
|
|
|
|
material_workspace = cache.material_workspace
|
|
|
|
|
|
|
|
|
|
|
|
# Extract counter ONCE before loop to avoid Ref{Int} indirection overhead
|
|
|
|
|
|
counter = 0
|
|
|
|
|
|
|
|
|
|
|
|
# Loop over elements
|
|
|
|
|
|
𝔻_vec_buffer = cache.𝔻_vec_buffer # Cache buffer reference for zero-allocation access
|
|
|
|
|
|
for elem_id in 1:nelems
|
|
|
|
|
|
# Assemble single element (all phases) - uses GlobalMaterialCache
|
|
|
|
|
|
assemble_element!(element_cache, geometry_cache, material_workspace,
|
|
|
|
|
|
kernel, elem_id, mesh, N, u_global, global_cache, Δt, 𝔻_vec_buffer)
|
|
|
|
|
|
|
|
|
|
|
|
# Scatter blocks directly to triplets using direct version (zero dispatch!)
|
|
|
|
|
|
counter = scatter_blocks_to_triplets_symmetric_direct!(
|
|
|
|
|
|
cache.I, cache.J, cache.V, counter, cache.capacity,
|
|
|
|
|
|
element_cache.K_blocks, element_cache.dofs, N)
|
|
|
|
|
|
|
|
|
|
|
|
# Scatter blocked force to global force vector
|
|
|
|
|
|
scatter_blocks_to_force!(cache.f, element_cache.f_blocks, element_cache.dofs, N)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Write counter back ONCE after loop
|
|
|
|
|
|
cache.counter = counter # Direct assignment (Int, not Ref{Int})
|
|
|
|
|
|
|
|
|
|
|
|
return nothing
|
|
|
|
|
|
end
|
|
|
|
|
|
|
2025-11-18 18:02:30 +02:00
|
|
|
|
"""
|
|
|
|
|
|
assemble!(
|
|
|
|
|
|
cache::COOCache,
|
|
|
|
|
|
assembler::COOAssembler,
|
|
|
|
|
|
kernel::AbstractKernel,
|
2025-11-20 16:56:36 +02:00
|
|
|
|
mesh::AbstractMesh,
|
|
|
|
|
|
u_global::Union{Nothing,Vector{Vec{3,Float64}}} = nothing,
|
|
|
|
|
|
state_old::Union{Nothing,Matrix{<:AbstractMaterialState}} = nothing,
|
|
|
|
|
|
Δt::Float64 = 0.0
|
2025-11-18 18:02:30 +02:00
|
|
|
|
) -> Nothing
|
|
|
|
|
|
|
2025-12-12 23:29:02 +02:00
|
|
|
|
Assemble stiffness matrix and force vector (LEGACY API - Matrix{<:AbstractMaterialState}).
|
|
|
|
|
|
|
|
|
|
|
|
**Legacy API:** Uses `Matrix{<:AbstractMaterialState}` for state storage.
|
|
|
|
|
|
For new code, prefer `GlobalMaterialCache` overload.
|
|
|
|
|
|
|
2025-11-20 16:56:36 +02:00
|
|
|
|
Assemble global system using COO format with **three-phase approach**.
|
2025-11-18 18:02:30 +02:00
|
|
|
|
|
2025-11-20 16:56:36 +02:00
|
|
|
|
# Three-Phase Algorithm
|
2025-11-18 18:02:30 +02:00
|
|
|
|
|
|
|
|
|
|
1. Reset cache (zero arrays, reset counter)
|
|
|
|
|
|
2. Loop over elements:
|
2025-12-12 23:29:02 +02:00
|
|
|
|
a. Reset caches: `reset!(geometry_cache)`, `reset!(element_cache)`, `reset!(material_workspace)`
|
2025-11-20 16:56:36 +02:00
|
|
|
|
b. **Phase 1a (Geometry):** Extract node coordinates
|
|
|
|
|
|
- `update_geometry_cache!(geometry_cache, kernel, elem_id, mesh)`
|
|
|
|
|
|
c. **Phase 1b (Element):** Extract displacements and DOF mapping
|
|
|
|
|
|
- `update_element_cache!(element_cache, kernel, elem_id, mesh, u_global)`
|
|
|
|
|
|
d. **Phase 2 (Material):** Compute material state at all IPs
|
2025-12-12 23:29:02 +02:00
|
|
|
|
- `update_material_cache!(material_workspace, geometry_cache, material, element_cache, state_old, elem_id, Δt)`
|
2025-11-20 16:56:36 +02:00
|
|
|
|
e. **Phase 3 (Stiffness):** Compute element stiffness using precomputed state
|
2025-12-12 23:29:02 +02:00
|
|
|
|
- `compute_element_stiffness!(element_cache, geometry_cache, material_workspace, N, NIP)`
|
2025-11-20 16:56:36 +02:00
|
|
|
|
f. Scatter Ke to triplets: accumulate (i,j,value) to (I,J,V)
|
|
|
|
|
|
g. Scatter fe to global force vector: `f[dofs] += fe`
|
2025-11-18 18:02:30 +02:00
|
|
|
|
3. Use `extract_system(cache)` to build sparse matrix from triplets
|
|
|
|
|
|
|
|
|
|
|
|
# Arguments
|
|
|
|
|
|
- `cache`: Pre-allocated COO cache
|
|
|
|
|
|
- `assembler`: COO assembler
|
|
|
|
|
|
- `kernel`: Domain kernel (continuum, plate, beam, etc.)
|
|
|
|
|
|
- `mesh`: Finite element mesh
|
2025-11-20 16:56:36 +02:00
|
|
|
|
- `u_global`: Global displacement field [nnodes] as Vec{3} (nothing for linear analysis)
|
|
|
|
|
|
- `state_old`: Global material state [nips, nelems] (nothing for stateless materials)
|
|
|
|
|
|
- `Δt`: Time increment (for rate-dependent materials)
|
2025-11-18 18:02:30 +02:00
|
|
|
|
|
|
|
|
|
|
# Zero-Allocation Guarantee
|
|
|
|
|
|
|
|
|
|
|
|
No allocations during assembly loop. All arrays pre-allocated in cache.
|
|
|
|
|
|
Only allocation: `sparse(I, J, V)` in `extract_system(cache)` (called once).
|
|
|
|
|
|
|
|
|
|
|
|
# Example
|
|
|
|
|
|
|
2025-11-20 16:56:36 +02:00
|
|
|
|
# Setup (one-time)
|
|
|
|
|
|
mesh = create_cantilever_mesh(10, 2, 2)
|
|
|
|
|
|
kernel = ContinuumKernel(formulation, material, field)
|
|
|
|
|
|
assembler = COOAssembler()
|
|
|
|
|
|
cache = COOCache(mesh, kernel)
|
2025-11-18 18:02:30 +02:00
|
|
|
|
|
2025-11-20 16:56:36 +02:00
|
|
|
|
# Linear assembly (no displacement, no state)
|
|
|
|
|
|
assemble!(cache, assembler, kernel, mesh)
|
2025-11-18 18:02:30 +02:00
|
|
|
|
|
2025-11-20 16:56:36 +02:00
|
|
|
|
# Nonlinear assembly (with displacement and state)
|
|
|
|
|
|
nnodes = nnodes_total(mesh)
|
|
|
|
|
|
u_global = [zero(Vec{3,Float64}) for _ in 1:nnodes]
|
|
|
|
|
|
nips = length(cache.element_cache.ips)
|
|
|
|
|
|
nelems = nelements(mesh)
|
|
|
|
|
|
state_old = Matrix{PlasticityState}(undef, nips, nelems)
|
|
|
|
|
|
for i in 1:nips, j in 1:nelems
|
|
|
|
|
|
state_old[i,j] = PlasticityState() # Initialize with zero state
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
for iter in 1:max_iter
|
|
|
|
|
|
assemble!(cache, assembler, kernel, mesh, u_global, state_old, Δt)
|
|
|
|
|
|
K, f = extract_system(cache)
|
|
|
|
|
|
# ... solve, update u_global and state_old ...
|
|
|
|
|
|
end
|
2025-11-18 18:02:30 +02:00
|
|
|
|
|
|
|
|
|
|
# Performance
|
|
|
|
|
|
|
|
|
|
|
|
For 2500 Tet4 elements:
|
|
|
|
|
|
- Time: 9.71 ms
|
|
|
|
|
|
- Memory: 8.4 MB
|
|
|
|
|
|
- Speedup: 1.0x (baseline)
|
|
|
|
|
|
"""
|
|
|
|
|
|
function assemble!(
|
|
|
|
|
|
cache::COOCache,
|
|
|
|
|
|
assembler::COOAssembler,
|
|
|
|
|
|
kernel::AbstractKernel,
|
2025-11-20 16:56:36 +02:00
|
|
|
|
mesh::AbstractMesh,
|
|
|
|
|
|
u_global::Union{Nothing,Vector{Vec{3,Float64}}}=nothing,
|
|
|
|
|
|
state_old::Union{Nothing,Matrix{<:AbstractMaterialState}}=nothing,
|
|
|
|
|
|
Δt::Float64=0.0
|
2025-11-18 18:02:30 +02:00
|
|
|
|
)
|
2025-11-20 16:56:36 +02:00
|
|
|
|
# Extract compile-time constants from mesh type parameters
|
|
|
|
|
|
# Mesh{N,T} where N = nodes per element, T = topology type
|
|
|
|
|
|
MeshType = typeof(mesh)
|
|
|
|
|
|
N = MeshType.parameters[1]::Int # Compile-time constant for loop unrolling
|
|
|
|
|
|
|
2025-11-18 18:02:30 +02:00
|
|
|
|
# Reset cache for new assembly
|
|
|
|
|
|
reset!(cache)
|
|
|
|
|
|
|
|
|
|
|
|
nelems = nelements(mesh)
|
|
|
|
|
|
element_cache = cache.element_cache
|
2025-11-20 16:56:36 +02:00
|
|
|
|
geometry_cache = cache.geometry_cache
|
2025-12-12 23:29:02 +02:00
|
|
|
|
material_workspace = cache.material_workspace
|
2025-11-18 18:02:30 +02:00
|
|
|
|
|
2025-11-20 16:56:36 +02:00
|
|
|
|
# N (nodes per element) and NIP (integration points) are now compile-time constants
|
|
|
|
|
|
# extracted from type parameters for aggressive loop unrolling
|
2025-11-18 18:02:30 +02:00
|
|
|
|
|
2025-11-20 16:56:36 +02:00
|
|
|
|
# Extract counter ONCE before loop to avoid Ref{Int} indirection overhead
|
|
|
|
|
|
#counter = cache.counter[]
|
|
|
|
|
|
counter = 0
|
2025-11-18 18:02:30 +02:00
|
|
|
|
|
2025-11-20 16:56:36 +02:00
|
|
|
|
# Loop over elements
|
2025-12-12 23:29:02 +02:00
|
|
|
|
𝔻_vec_buffer = cache.𝔻_vec_buffer # Cache buffer reference for zero-allocation access
|
2025-11-20 16:56:36 +02:00
|
|
|
|
for elem_id in 1:nelems
|
|
|
|
|
|
# Assemble single element (all phases)
|
2025-12-12 23:29:02 +02:00
|
|
|
|
assemble_element!(element_cache, geometry_cache, material_workspace,
|
|
|
|
|
|
kernel, elem_id, mesh, N, u_global, state_old, Δt, 𝔻_vec_buffer)
|
2025-11-20 16:56:36 +02:00
|
|
|
|
|
|
|
|
|
|
# Scatter blocks directly to triplets using direct version (zero dispatch!)
|
|
|
|
|
|
# Pass counter as Int (not Ref{Int}) to eliminate indirection
|
|
|
|
|
|
counter = scatter_blocks_to_triplets_symmetric_direct!(
|
|
|
|
|
|
cache.I, cache.J, cache.V, counter, cache.capacity,
|
|
|
|
|
|
element_cache.K_blocks, element_cache.dofs, N)
|
|
|
|
|
|
|
|
|
|
|
|
# Scatter blocked force to global force vector
|
|
|
|
|
|
scatter_blocks_to_force!(cache.f, element_cache.f_blocks, element_cache.dofs, N)
|
2025-11-18 18:02:30 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
2025-11-20 16:56:36 +02:00
|
|
|
|
# Write counter back ONCE after loop
|
2025-11-20 17:41:58 +02:00
|
|
|
|
# NOTE: counter MUST be written back so extract_system() knows how many triplets to extract
|
2025-12-12 23:29:02 +02:00
|
|
|
|
cache.counter = counter # Direct assignment (Int, not Ref{Int})
|
2025-11-18 18:02:30 +02:00
|
|
|
|
|
|
|
|
|
|
return nothing
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
estimate_triplet_count(mesh::AbstractMesh, kernel::AbstractKernel) -> Int
|
|
|
|
|
|
|
|
|
|
|
|
Estimate number of triplets for COO assembly.
|
|
|
|
|
|
|
|
|
|
|
|
Used to pre-allocate triplet arrays with correct capacity.
|
|
|
|
|
|
|
|
|
|
|
|
# Formula
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
triplet_count = sum over elements of ndofs_elem^2
|
|
|
|
|
|
≈ nelems × (avg_nnodes_per_elem × ndofs_per_node)^2
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Over-allocates by 20% for irregular meshes.
|
|
|
|
|
|
|
|
|
|
|
|
# Arguments
|
|
|
|
|
|
- `mesh`: Finite element mesh
|
|
|
|
|
|
- `kernel`: Domain kernel
|
|
|
|
|
|
|
|
|
|
|
|
# Returns
|
|
|
|
|
|
- Estimated triplet count (integer)
|
|
|
|
|
|
"""
|
|
|
|
|
|
function estimate_triplet_count(mesh::AbstractMesh, kernel::AbstractKernel)
|
|
|
|
|
|
nelems = nelements(mesh)
|
|
|
|
|
|
ndofs_per_node = dofs_per_node(kernel)
|
|
|
|
|
|
|
|
|
|
|
|
# Compute average nodes per element
|
|
|
|
|
|
avg_nnodes_per_elem = sum(nnodes_per_element(mesh, i) for i in 1:nelems) / nelems
|
|
|
|
|
|
avg_ndofs_per_elem = Int(ceil(avg_nnodes_per_elem * ndofs_per_node))
|
|
|
|
|
|
|
|
|
|
|
|
# Estimate: nelems * ndofs_elem^2, with 20% safety margin
|
|
|
|
|
|
estimated = Int(ceil(1.2 * nelems * avg_ndofs_per_elem^2))
|
|
|
|
|
|
|
|
|
|
|
|
return estimated
|
|
|
|
|
|
end
|