- Reference implementation of element-by-element assembly - ElementAssemblyData and ElementContribution data structures - Sparse matrix assembly in COO then CSC format - scatter_to_global!() adds local to global system - Penalty method for Dirichlet BCs - Matrix-vector product interface for GMRES - 479 lines: Complete documentation with examples and tests
11 KiB
title, date, author, status, last_updated, tags
| title | date | author | status | last_updated | tags | ||||
|---|---|---|---|---|---|---|---|---|---|
| Traditional Element Assembly Implementation | 2025-11-11 | JuliaFEM Team | Complete | 2025-11-11 |
|
Overview
This module provides the traditional element-by-element assembly approach for finite element analysis. It serves as:
- Reference implementation for comparison with nodal assembly
- Baseline for performance benchmarking
- Production-ready assembly for CPU-based solvers
Status: Complete with comprehensive tests (47/47 passing ✓)
Key Features
✅ Standard FEM Assembly
- Element stiffness matrices → Global sparse matrix
- Element force vectors → Global force vectors
- Residual computation:
r = f_int - f_ext - Dirichlet boundary conditions (penalty method)
✅ Sparse Matrix Assembly
- COO format during assembly (triplet lists)
- Automatic CSC conversion
- Overlapping elements handled correctly (summed)
- Symmetry preserved
✅ Production Features
- Reset for iterative/incremental solvers
- Matrix-vector product interface (for GMRES)
- Boundary condition application
- Assembly statistics printing
Data Structures
ElementAssemblyData
Global assembly storage:
struct ElementAssemblyData{T}
K_global::SparseMatrixCSC{T} # Global tangent stiffness
r_global::Vector{T} # Residual: f_int - f_ext
f_int_global::Vector{T} # Internal forces
f_ext_global::Vector{T} # External forces
ndof::Int # Total DOF
end
Usage:
ndof = 3 * nnodes # 3D problem
assembly = ElementAssemblyData(ndof, Float64)
ElementContribution
Local element quantities before scattering:
struct ElementContribution{T}
element_id::Int
gdofs::Vector{Int} # Global DOF indices
K_local::Matrix{T} # Element stiffness (e.g., 12×12 for Tet4)
f_int_local::Vector{T} # Element internal forces
f_ext_local::Vector{T} # Element external forces
end
Example:
# Tet4 element connecting nodes [5, 7, 12, 15]
conn = (5, 7, 12, 15)
gdofs = get_dof_indices(conn, 3) # [13,14,15, 19,20,21, ...]
contrib = ElementContribution(elem_id, gdofs, Float64)
# Fill K_local, f_int_local, f_ext_local during element integration
# ... (loop over Gauss points)
# Scatter to global
scatter_to_global!(assembly, contrib)
Assembly Workflow
Standard FEM Assembly Loop
# 1. Initialize
ndof = 3 * nnodes
assembly = ElementAssemblyData(ndof)
# 2. Loop over elements
for (elem_id, element) in enumerate(elements)
# Get connectivity
conn = element.connectivity # e.g., (1, 2, 3, 4)
gdofs = get_dof_indices(conn, 3)
# Allocate element contribution
contrib = ElementContribution(elem_id, gdofs)
# Compute element quantities (loop over Gauss points)
for gp in gauss_points
# Compute B matrix, material tangent, stress, etc.
# Accumulate into contrib.K_local, contrib.f_int_local
end
# Scatter to global
scatter_to_global!(assembly, contrib)
end
# 3. Compute residual
compute_residual!(assembly)
# 4. Apply boundary conditions
fixed_dofs = [1, 2, 3] # Fix node 1
apply_dirichlet_bc!(assembly, fixed_dofs)
# 5. Solve (example: direct solver)
Δu = assembly.K_global \ (-assembly.r_global)
Batch Assembly (for multiple elements)
# Compute all element contributions
contributions = ElementContribution{Float64}[]
for element in elements
contrib = compute_element_contribution(element, u, time)
push!(contributions, contrib)
end
# Assemble all at once
assemble_elements!(assembly, contributions)
Key Operations
1. Scatter to Global
Adds element quantities to global system:
scatter_to_global!(assembly, contrib)
What it does:
- Adds
K_local[i,j]toK_global[gdofs[i], gdofs[j]] - Adds
f_int_local[i]tof_int_global[gdofs[i]] - Adds
f_ext_local[i]tof_ext_global[gdofs[i]]
Key property: Multiple elements can contribute to same global DOF (summed automatically)
2. Residual Computation
compute_residual!(assembly)
Computes: r = f_int - f_ext
For Newton-Raphson: solve K * Δu = -r
3. Boundary Conditions
# Fix nodes 1 and 2 to zero displacement
fixed_dofs = [1,2,3, 4,5,6]
apply_dirichlet_bc!(assembly, fixed_dofs)
# Fix with prescribed values
prescribed_values = [0.0, 0.0, 0.0, 0.1, 0.0, 0.0] # Node 2: u_x = 0.1
apply_dirichlet_bc!(assembly, fixed_dofs, prescribed_values)
Uses penalty method:
- Adds large stiffness to diagonal:
K[i,i] += penalty - Modifies RHS:
r[i] = penalty * u_prescribed
4. Matrix-Vector Product
w = matrix_vector_product(assembly, v)
For matrix-free solvers (GMRES, CG):
function matvec(v)
return matrix_vector_product(assembly, v)
end
Δu = gmres(matvec, -assembly.r_global, tol=1e-6)
Testing
Comprehensive test suite (test/test_element_assembly_structures.jl):
Test Coverage
- ✅ Data structure construction
- ✅ DOF indexing (
get_dof_indices) - ✅ Single element scatter
- ✅ Overlapping elements (accumulation)
- ✅ Residual computation
- ✅ Full assembly workflow
- ✅ Matrix-vector product
- ✅ Dirichlet BC application
- ✅ Symmetry preservation
- ✅ Reset functionality
- ✅ Statistics printing
Results: 47/47 tests passing ✓
Example Test
@testset "Overlapping Elements" begin
ndof = 15 # 5 nodes
assembly = ElementAssemblyData(ndof)
# Element 1: nodes [1,2,3,4]
# Element 2: nodes [2,3,4,5] ← shares nodes 2,3,4
# ... create contributions ...
scatter_to_global!(assembly, contrib1)
scatter_to_global!(assembly, contrib2)
# Node 2 diagonal: accumulated from both elements
@test assembly.K_global[4,4] == K1[4,4] + K2[1,1]
end
Advantages
✅ Well-Established
- Standard textbook algorithm
- Easy to understand and verify
- Decades of production use
✅ Flexible
- Works with any element type
- Handles complex meshes
- Supports all boundary conditions
✅ Sparse Matrix Support
- Leverages Julia's SparseArrays
- Efficient storage (only non-zeros)
- Fast direct solvers (UMFPACK, etc.)
Disadvantages
❌ GPU Parallelization
Problem: Multiple elements write to same global DOF
# Thread 1 (element 5):
K_global[10, 10] += K_elem5[4, 4] # Race!
# Thread 2 (element 7):
K_global[10, 10] += K_elem7[2, 2] # Race!
Solution: Requires atomic operations → slow on GPU (10-100× slower)
❌ Memory Usage
For large problems (N DOF):
- K_global: Sparse but still O(N²) worst case
- Memory: Can be 100s of MB to GBs
- Cache: Scattered access pattern
❌ Matrix Formation
For matrix-free methods:
- Still need to form K explicitly
- Cannot avoid assembly cost
- Wastes work if only need K*v
Comparison: Element vs Nodal Assembly
| Aspect | Element Assembly | Nodal Assembly |
|---|---|---|
| Outer loop | Elements | Nodes |
| Parallelization | Hard (atomics) | Easy (no conflicts) |
| Memory | Full K matrix | 3×3 blocks per thread |
| Matrix-free | Must form K | Natural K*v |
| GPU | Slow (atomics) | Fast (no atomics) |
| CPU | Fast (mature) | Comparable |
| Complexity | Simple | Moderate |
Performance Characteristics
Memory
Problem: 100K nodes (300K DOF)
Element Assembly:
- K_global: ~300K × 300K sparse
- Typical sparsity: 0.01%
- Memory: ~300K × 300K × 0.0001 × 8 bytes
= ~720 MB (just for K!)
Nodal Assembly:
- No global K
- Per-thread: ~30 nodes × 3×3 blocks
- Memory: 30 × 9 × 8 bytes = 2.16 KB per thread
Assembly Time
Typical: O(n_elem × n_gauss × ndof_elem²)
For 10K Tet4 elements:
- n_elem = 10,000
- n_gauss = 4 (typical)
- ndof_elem = 12 (4 nodes × 3 DOF)
Assembly: ~10K × 4 × 144 = 5.76M operations
CPU: ~5-50 ms (depending on material model)
Usage Examples
Example 1: Static Linear Elasticity
# Setup
nnodes = 1000
ndof = 3 * nnodes
assembly = ElementAssemblyData(ndof)
# Assemble
for element in elements
contrib = compute_linear_elastic_element(element, coordinates, material)
scatter_to_global!(assembly, contrib)
end
compute_residual!(assembly)
# Boundary conditions: fix base nodes
fixed_nodes = find_base_nodes(mesh)
fixed_dofs = vcat([3*(n-1) .+ (1:3) for n in fixed_nodes]...)
apply_dirichlet_bc!(assembly, fixed_dofs)
# Solve
u = assembly.K_global \ (-assembly.r_global)
Example 2: Newton-Raphson Nonlinear
u = zeros(ndof)
for iter in 1:max_iterations
# Assemble at current configuration
reset!(assembly)
for element in elements
contrib = compute_element(element, u, material)
scatter_to_global!(assembly, contrib)
end
compute_residual!(assembly)
apply_dirichlet_bc!(assembly, fixed_dofs)
# Check convergence
if norm(assembly.r_global) < tol
break
end
# Solve for increment
Δu = assembly.K_global \ (-assembly.r_global)
u += Δu
end
Example 3: Matrix-Free GMRES
# Assemble once
assemble_elements!(assembly, contributions)
# Define matrix-free operator
function matvec(v)
return matrix_vector_product(assembly, v)
end
# Solve with GMRES (Krylov.jl)
using Krylov
Δu, stats = gmres(matvec, -assembly.r_global,
atol=1e-8, rtol=1e-6, itmax=100)
println("GMRES converged in $(stats.niter) iterations")
Implementation Notes
Sparse Matrix Assembly
Uses triplet (COO) format during assembly:
I_rows = Int[]
J_cols = Int[]
values = Float64[]
# Accumulate triplets
for element in elements
for i in 1:ndofs_local, j in 1:ndofs_local
push!(I_rows, gdofs[i])
push!(J_cols, gdofs[j])
push!(values, K_local[i,j])
end
end
# Convert to CSC (automatic summing of duplicates)
K_global = sparse(I_rows, J_cols, values, ndof, ndof)
Why: Efficient for scattered writes, automatic duplicate handling
DOF Ordering
Standard ordering: group by node
Node 1: DOFs [1, 2, 3] (u_x, u_y, u_z)
Node 2: DOFs [4, 5, 6] (u_x, u_y, u_z)
Node 3: DOFs [7, 8, 9] (u_x, u_y, u_z)
...
Helper function:
gdofs = get_dof_indices(connectivity, dim)
Files
- Implementation:
src/element_assembly_structures.jl(356 lines) - Tests:
test/test_element_assembly_structures.jl(259 lines) - Documentation: This file
References
- Hughes: "The Finite Element Method" - Chapter 4 (Assembly)
- Zienkiewicz & Taylor: "The Finite Element Method" - Volume 1
- Bathe: "Finite Element Procedures" - Chapter 6
Next Steps
- ✅ Element assembly complete
- ✅ Nodal assembly complete
- 🔄 Performance comparison benchmarks
- 🔄 GPU implementation (nodal assembly)
- 🔄 Integration with material models
This traditional assembly serves as the baseline for validating nodal assembly correctness and measuring performance improvements!