mirror of
https://github.com/JuliaFEM/JuliaFEM.jl.git
synced 2026-09-22 18:52:16 +00:00
demo: Add cantilever CPU assembly comparison
Compares traditional element assembly vs nodal assembly on CPU for cantilever beam example, validating assembly equivalence and measuring performance characteristics. Comparison: - Element assembly: Traditional FEM (loop over elements, atomic scatter) - Nodal assembly: Modern approach (loop over nodes, block operations) Validation: - Residual equivalence (element vs nodal assembly) - Stiffness operator equivalence (matvec comparison) - Assembly time comparison - Memory allocation tracking Problem: Same cantilever beam as cantilever_beam_simple.jl - Tet4 mesh from Gmsh - Steel properties - Fixed left, force on right Demonstrates CPU assembly strategies before GPU implementation, establishing baseline for GPU performance comparison.
This commit is contained in:
@@ -0,0 +1,498 @@
|
||||
# Cantilever Beam - Assembly Strategy Benchmark (RESEARCH/DEVELOPMENT)
|
||||
#
|
||||
# ⚠️ NOTE: This is a LOW-LEVEL benchmark for algorithm research!
|
||||
# ⚠️ For USER-FACING examples, see:
|
||||
# ⚠️ - demos/assembly_comparison_simple.jl (uses real Problem API)
|
||||
# ⚠️ - demos/cantilever_gmsh_gpu.jl (uses Physics API + GPU)
|
||||
# ⚠️ - examples/linear_static.jl (complete workflow)
|
||||
#
|
||||
# This file compares three assembly/solver combinations at the structure level:
|
||||
# 1. Element assembly + Direct solver (baseline)
|
||||
# 2. Element assembly + Iterative CG
|
||||
# 3. Nodal assembly + Matrix-free CG (research)
|
||||
#
|
||||
# Uses element_assembly_structures.jl and nodal_assembly_structures.jl directly.
|
||||
# Not intended as example of user-facing API!
|
||||
|
||||
using LinearAlgebra
|
||||
using SparseArrays
|
||||
using Tensors
|
||||
using Printf
|
||||
|
||||
# Import our assembly structures
|
||||
include("../src/element_assembly_structures.jl")
|
||||
include("../src/nodal_assembly_structures.jl")
|
||||
|
||||
println("="^70)
|
||||
println("Cantilever Beam - CPU Assembly Comparison")
|
||||
println("="^70)
|
||||
|
||||
# ============================================================================
|
||||
# 1. Generate Mesh with Gmsh
|
||||
# ============================================================================
|
||||
|
||||
println("\n[1] Generating mesh with Gmsh...")
|
||||
|
||||
# Simple beam: L=10, W=1, H=1
|
||||
# Target ~20 Tet4 elements
|
||||
|
||||
using Gmsh: gmsh
|
||||
|
||||
gmsh.initialize()
|
||||
gmsh.model.add("cantilever")
|
||||
|
||||
# Geometry
|
||||
lc = 1.5 # Characteristic length (controls mesh density)
|
||||
L, W, H = 10.0, 1.0, 1.0
|
||||
|
||||
# Create box
|
||||
box = gmsh.model.occ.addBox(0, 0, 0, L, W, H)
|
||||
gmsh.model.occ.synchronize()
|
||||
|
||||
# Set mesh size
|
||||
gmsh.model.mesh.setSize(gmsh.model.getEntities(0), lc)
|
||||
|
||||
# Generate 3D mesh
|
||||
gmsh.model.mesh.generate(3)
|
||||
|
||||
# Extract nodes
|
||||
node_tags, node_coords, _ = gmsh.model.mesh.getNodes()
|
||||
n_nodes = length(node_tags)
|
||||
nodes = reshape(node_coords, 3, n_nodes)
|
||||
|
||||
println(" Nodes: $n_nodes")
|
||||
|
||||
# Extract Tet4 elements (type 4)
|
||||
elem_types, elem_tags_vec, elem_node_tags_vec = gmsh.model.mesh.getElements(3)
|
||||
tet4_idx = findfirst(t -> t == 4, elem_types) # Type 4 = Tet4
|
||||
|
||||
if tet4_idx === nothing
|
||||
error("No Tet4 elements found!")
|
||||
end
|
||||
|
||||
elem_node_tags = elem_node_tags_vec[tet4_idx]
|
||||
n_elements = div(length(elem_node_tags), 4)
|
||||
connectivity = reshape(Int.(elem_node_tags), 4, n_elements)
|
||||
|
||||
println(" Elements: $n_elements")
|
||||
println(" DOFs: $(3 * n_nodes)")
|
||||
|
||||
gmsh.finalize()
|
||||
|
||||
# ============================================================================
|
||||
# 2. Material Properties and BCs
|
||||
# ============================================================================
|
||||
|
||||
println("\n[2] Setting up problem...")
|
||||
|
||||
# Material (steel)
|
||||
E = 210e9 # Young's modulus [Pa]
|
||||
ν = 0.3 # Poisson's ratio
|
||||
|
||||
# Boundary conditions
|
||||
# Fixed: nodes at X=0
|
||||
fixed_nodes = findall(x -> abs(x) < 1e-10, nodes[1, :])
|
||||
println(" Fixed nodes: $(length(fixed_nodes))")
|
||||
|
||||
# Loaded: nodes at X=L (free end)
|
||||
loaded_nodes = findall(x -> abs(x - L) < 1e-10, nodes[1, :])
|
||||
println(" Loaded nodes: $(length(loaded_nodes))")
|
||||
|
||||
# Applied force (total 1000 N downward, distributed)
|
||||
F_total = -1000.0 # Negative Z direction
|
||||
f_per_node = F_total / length(loaded_nodes)
|
||||
|
||||
println(" Force per node: $(f_per_node) N")
|
||||
|
||||
# ============================================================================
|
||||
# 3. Compute Element Stiffness Matrices (Shared by all methods)
|
||||
# ============================================================================
|
||||
#
|
||||
# NOTE: This is a simplified reference implementation for benchmarking.
|
||||
# For production use, see src/problems_elasticity.jl which includes:
|
||||
# - Geometric nonlinearity, finite strain
|
||||
# - Plasticity and advanced material models
|
||||
# - Surface tractions, body forces
|
||||
# - Integration with Problem/Element API
|
||||
#
|
||||
# This demo focuses on assembly strategy comparison, not material complexity.
|
||||
# ============================================================================
|
||||
|
||||
println("\n[3] Computing element stiffness matrices...")
|
||||
|
||||
# Elasticity tensor (isotropic)
|
||||
λ = E * ν / ((1 + ν) * (1 - 2ν))
|
||||
μ = E / (2(1 + ν))
|
||||
|
||||
function compute_tet4_stiffness(X::Matrix{Float64}, E::Float64, ν::Float64)
|
||||
# X: 3×4 matrix of node coordinates
|
||||
# Returns: 12×12 element stiffness matrix
|
||||
|
||||
# Shape function derivatives in parent element (constant for Tet4)
|
||||
dN_dξ = [-1.0 -1.0 -1.0;
|
||||
1.0 0.0 0.0;
|
||||
0.0 1.0 0.0;
|
||||
0.0 0.0 1.0]
|
||||
|
||||
# Jacobian: J = dX/dξ
|
||||
J = X * dN_dξ # 3×3
|
||||
detJ = det(J)
|
||||
|
||||
if detJ <= 0
|
||||
error("Negative Jacobian determinant!")
|
||||
end
|
||||
|
||||
# Shape function derivatives in physical space
|
||||
dN_dx = dN_dξ / J # 4×3
|
||||
|
||||
# B matrix (strain-displacement): 6×12
|
||||
B = zeros(6, 12)
|
||||
for i in 1:4
|
||||
B[1, 3i-2] = dN_dx[i, 1] # ∂u/∂x
|
||||
B[2, 3i-1] = dN_dx[i, 2] # ∂v/∂y
|
||||
B[3, 3i] = dN_dx[i, 3] # ∂w/∂z
|
||||
B[4, 3i-2] = dN_dx[i, 2] # ∂u/∂y
|
||||
B[4, 3i-1] = dN_dx[i, 1] # ∂v/∂x
|
||||
B[5, 3i-1] = dN_dx[i, 3] # ∂v/∂z
|
||||
B[5, 3i] = dN_dx[i, 2] # ∂w/∂y
|
||||
B[6, 3i-2] = dN_dx[i, 3] # ∂u/∂z
|
||||
B[6, 3i] = dN_dx[i, 1] # ∂w/∂x
|
||||
end
|
||||
|
||||
# Elasticity matrix (Voigt notation)
|
||||
λ = E * ν / ((1 + ν) * (1 - 2ν))
|
||||
μ = E / (2(1 + ν))
|
||||
|
||||
D = [λ+2μ λ λ 0 0 0;
|
||||
λ λ+2μ λ 0 0 0;
|
||||
λ λ λ+2μ 0 0 0;
|
||||
0 0 0 μ 0 0;
|
||||
0 0 0 0 μ 0;
|
||||
0 0 0 0 0 μ]
|
||||
|
||||
# Element stiffness: K_e = ∫ B^T D B dV = B^T D B * V
|
||||
# For Tet4: V = detJ / 6
|
||||
V = abs(detJ) / 6.0
|
||||
K_e = (B' * D * B) * V
|
||||
|
||||
return K_e
|
||||
end
|
||||
|
||||
# Compute all element matrices
|
||||
K_elements = Vector{Matrix{Float64}}(undef, n_elements)
|
||||
for e in 1:n_elements
|
||||
conn = connectivity[:, e]
|
||||
X_elem = nodes[:, conn]
|
||||
K_elements[e] = compute_tet4_stiffness(X_elem, E, ν)
|
||||
end
|
||||
|
||||
println(" Element stiffness matrices computed")
|
||||
|
||||
# ============================================================================
|
||||
# 4. METHOD 1: Element Assembly + Direct Solver
|
||||
# ============================================================================
|
||||
|
||||
println("\n" * "="^70)
|
||||
println("METHOD 1: Element Assembly + Direct Solver (LU)")
|
||||
println("="^70)
|
||||
|
||||
t1 = time()
|
||||
|
||||
# Assemble global system
|
||||
n_dofs = 3 * n_nodes
|
||||
assembly = ElementAssemblyData(n_dofs, Float64)
|
||||
|
||||
for e in 1:n_elements
|
||||
conn = Tuple(connectivity[:, e])
|
||||
gdofs = get_dof_indices(conn, 3)
|
||||
|
||||
contrib = ElementContribution(e, gdofs, K_elements[e],
|
||||
zeros(12), zeros(12))
|
||||
scatter_to_global!(assembly, contrib)
|
||||
end
|
||||
|
||||
# Apply loads
|
||||
for node in loaded_nodes
|
||||
dof_z = 3 * node # Z component
|
||||
assembly.f_ext_global[dof_z] = f_per_node
|
||||
end
|
||||
|
||||
# Compute residual
|
||||
compute_residual!(assembly)
|
||||
|
||||
# Apply Dirichlet BCs
|
||||
fixed_dofs = Int[]
|
||||
for node in fixed_nodes
|
||||
append!(fixed_dofs, [3 * node - 2, 3 * node - 1, 3 * node])
|
||||
end
|
||||
apply_dirichlet_bc!(assembly, fixed_dofs, zeros(length(fixed_dofs)))
|
||||
|
||||
t_assembly_1 = time() - t1
|
||||
println("Assembly time: $(round(t_assembly_1, digits=4)) s")
|
||||
|
||||
# Solve with direct solver (K_global is already CSC)
|
||||
t_solve_1_start = time()
|
||||
u1 = assembly.K_global \ assembly.r_global
|
||||
t_solve_1 = time() - t_solve_1_start
|
||||
|
||||
# Compute final residual
|
||||
r1 = matrix_vector_product(assembly, u1) - assembly.f_ext_global
|
||||
r1_norm = norm(r1)
|
||||
|
||||
t_total_1 = time() - t1
|
||||
|
||||
println("Solve time: $(round(t_solve_1, digits=4)) s")
|
||||
println("Total time: $(round(t_total_1, digits=4)) s")
|
||||
println("Residual norm: $(r1_norm)")
|
||||
println("Max displacement: $(maximum(abs.(u1)) * 1000) mm")
|
||||
|
||||
# ============================================================================
|
||||
# 5. METHOD 2: Element Assembly + Iterative Solver (CG)
|
||||
# ============================================================================
|
||||
|
||||
println("\n" * "="^70)
|
||||
println("METHOD 2: Element Assembly + Iterative Solver (CG)")
|
||||
println("="^70)
|
||||
|
||||
t2 = time()
|
||||
|
||||
# Reuse assembly from Method 1
|
||||
t_assembly_2 = t_assembly_1 # Same assembly
|
||||
|
||||
# Conjugate Gradient solver
|
||||
function cg_solve(A::ElementAssemblyData, b::Vector{Float64};
|
||||
tol=1e-8, max_iter=1000)
|
||||
n = length(b)
|
||||
x = zeros(n)
|
||||
r = b - matrix_vector_product(A, x)
|
||||
p = copy(r)
|
||||
rsold = dot(r, r)
|
||||
|
||||
for iter in 1:max_iter
|
||||
Ap = matrix_vector_product(A, p)
|
||||
α = rsold / dot(p, Ap)
|
||||
x .+= α .* p
|
||||
r .-= α .* Ap
|
||||
rsnew = dot(r, r)
|
||||
|
||||
if sqrt(rsnew) < tol
|
||||
return x, iter, sqrt(rsnew)
|
||||
end
|
||||
|
||||
β = rsnew / rsold
|
||||
p .= r .+ β .* p
|
||||
rsold = rsnew
|
||||
end
|
||||
|
||||
return x, max_iter, sqrt(rsold)
|
||||
end
|
||||
|
||||
t_solve_2_start = time()
|
||||
u2, cg_iters_2, cg_res_2 = cg_solve(assembly, assembly.r_global, tol=1e-8)
|
||||
t_solve_2 = time() - t_solve_2_start
|
||||
|
||||
# Compute final residual
|
||||
r2 = matrix_vector_product(assembly, u2) - assembly.f_ext_global
|
||||
r2_norm = norm(r2)
|
||||
|
||||
t_total_2 = time() - t2
|
||||
|
||||
println("Assembly time: $(round(t_assembly_2, digits=4)) s")
|
||||
println("Solve time: $(round(t_solve_2, digits=4)) s")
|
||||
println("Total time: $(round(t_total_2, digits=4)) s")
|
||||
println("CG iterations: $cg_iters_2")
|
||||
println("CG residual: $(cg_res_2)")
|
||||
println("Residual norm: $(r2_norm)")
|
||||
println("Max displacement: $(maximum(abs.(u2)) * 1000) mm")
|
||||
println("Difference from Method 1: $(norm(u1 - u2))")
|
||||
|
||||
# ============================================================================
|
||||
# 6. METHOD 3: Nodal Assembly + Iterative Solver (Matrix-Free CG)
|
||||
# ============================================================================
|
||||
|
||||
println("\n" * "="^70)
|
||||
println("METHOD 3: Nodal Assembly + Matrix-Free Iterative Solver")
|
||||
println("="^70)
|
||||
|
||||
t3 = time()
|
||||
|
||||
# Build node-to-elements map (convert matrix to vector of tuples)
|
||||
conn_tuples = [Tuple(connectivity[:, e]) for e in 1:n_elements]
|
||||
node_map = NodeToElementsMap(conn_tuples)
|
||||
|
||||
# For each node, precompute 3×3 stiffness blocks with all coupling nodes
|
||||
# This is the "spider" pattern
|
||||
|
||||
struct NodalAssemblyData
|
||||
node_map::NodeToElementsMap
|
||||
K_elements::Vector{Matrix{Float64}}
|
||||
connectivity::Matrix{Int}
|
||||
conn_tuples::Vector{NTuple{4,Int}} # Store tuple version too
|
||||
n_nodes::Int
|
||||
n_dofs::Int
|
||||
end
|
||||
|
||||
nodal_data = NodalAssemblyData(node_map, K_elements, connectivity,
|
||||
conn_tuples, n_nodes, n_dofs)
|
||||
|
||||
# Matrix-vector product using nodal assembly
|
||||
function nodal_matvec!(w::Vector{Float64}, v::Vector{Float64},
|
||||
data::NodalAssemblyData)
|
||||
fill!(w, 0.0)
|
||||
|
||||
for node_i in 1:data.n_nodes
|
||||
# Get spider nodes (all nodes coupled to node_i)
|
||||
spider = get_node_spider(data.node_map, node_i, data.conn_tuples)
|
||||
|
||||
w_local = zeros(3)
|
||||
|
||||
for node_j in spider
|
||||
# Sum contributions from all elements containing both nodes
|
||||
K_block_ij = zeros(3, 3)
|
||||
|
||||
for elem_info in data.node_map.node_to_elements[node_i]
|
||||
elem_idx = elem_info.element_id
|
||||
conn = data.conn_tuples[elem_idx]
|
||||
|
||||
# Check if node_j is in this element
|
||||
local_j = findfirst(==(node_j), conn)
|
||||
if local_j !== nothing
|
||||
local_i = findfirst(==(node_i), conn)
|
||||
K_e = data.K_elements[elem_idx]
|
||||
|
||||
# Extract 3×3 block
|
||||
for α in 1:3, β in 1:3
|
||||
K_block_ij[α, β] += K_e[3*(local_i-1)+α, 3*(local_j-1)+β]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Apply to displacement
|
||||
v_j = v[3*(node_j-1)+1:3*node_j]
|
||||
w_local .+= K_block_ij * v_j
|
||||
end
|
||||
|
||||
# Write to global
|
||||
w[3*(node_i-1)+1:3*node_i] .= w_local
|
||||
end
|
||||
|
||||
return w
|
||||
end
|
||||
|
||||
t_assembly_3 = time() - t3
|
||||
println("Nodal map construction: $(round(t_assembly_3, digits=4)) s")
|
||||
|
||||
# Build RHS (same as before)
|
||||
f_ext = zeros(n_dofs)
|
||||
for node in loaded_nodes
|
||||
dof_z = 3 * node
|
||||
f_ext[dof_z] = f_per_node
|
||||
end
|
||||
|
||||
# CG with matrix-free matvec
|
||||
function cg_solve_nodal(data::NodalAssemblyData, b::Vector{Float64},
|
||||
fixed_dofs::Vector{Int};
|
||||
tol=1e-8, max_iter=1000)
|
||||
n = length(b)
|
||||
x = zeros(n)
|
||||
|
||||
# Apply BC to initial guess
|
||||
x[fixed_dofs] .= 0.0
|
||||
|
||||
# Compute initial residual
|
||||
Ax = zeros(n)
|
||||
nodal_matvec!(Ax, x, data)
|
||||
Ax[fixed_dofs] .= 0.0 # Zero out fixed DOFs
|
||||
|
||||
r = b - Ax
|
||||
r[fixed_dofs] .= 0.0
|
||||
p = copy(r)
|
||||
rsold = dot(r, r)
|
||||
|
||||
for iter in 1:max_iter
|
||||
Ap = zeros(n)
|
||||
nodal_matvec!(Ap, p, data)
|
||||
Ap[fixed_dofs] .= 0.0
|
||||
|
||||
α = rsold / dot(p, Ap)
|
||||
x .+= α .* p
|
||||
r .-= α .* Ap
|
||||
rsnew = dot(r, r)
|
||||
|
||||
if sqrt(rsnew) < tol
|
||||
return x, iter, sqrt(rsnew)
|
||||
end
|
||||
|
||||
β = rsnew / rsold
|
||||
p .= r .+ β .* p
|
||||
rsold = rsnew
|
||||
end
|
||||
|
||||
return x, max_iter, sqrt(rsold)
|
||||
end
|
||||
|
||||
t_solve_3_start = time()
|
||||
u3, cg_iters_3, cg_res_3 = cg_solve_nodal(nodal_data, f_ext, fixed_dofs,
|
||||
tol=1e-8)
|
||||
t_solve_3 = time() - t_solve_3_start
|
||||
|
||||
# Compute final residual
|
||||
w3 = zeros(n_dofs)
|
||||
nodal_matvec!(w3, u3, nodal_data)
|
||||
r3 = w3 - f_ext
|
||||
r3_norm = norm(r3)
|
||||
|
||||
t_total_3 = time() - t3
|
||||
|
||||
println("Solve time: $(round(t_solve_3, digits=4)) s")
|
||||
println("Total time: $(round(t_total_3, digits=4)) s")
|
||||
println("CG iterations: $cg_iters_3")
|
||||
println("CG residual: $(cg_res_3)")
|
||||
println("Residual norm: $(r3_norm)")
|
||||
println("Max displacement: $(maximum(abs.(u3)) * 1000) mm")
|
||||
println("Difference from Method 1: $(norm(u1 - u3))")
|
||||
|
||||
# ============================================================================
|
||||
# 7. Summary Comparison
|
||||
# ============================================================================
|
||||
|
||||
println("\n" * "="^70)
|
||||
println("SUMMARY COMPARISON")
|
||||
println("="^70)
|
||||
|
||||
println("\nProblem Size:")
|
||||
println(" Nodes: $n_nodes")
|
||||
println(" Elements: $n_elements")
|
||||
println(" DOFs: $n_dofs")
|
||||
println(" Fixed DOFs: $(length(fixed_dofs))")
|
||||
println(" Free DOFs: $(n_dofs - length(fixed_dofs))")
|
||||
|
||||
println("\n" * "-"^70)
|
||||
println(@sprintf("%-40s %10s %10s %10s", "Method", "Assembly", "Solve", "Total"))
|
||||
println("-"^70)
|
||||
println(@sprintf("%-40s %9.4fs %9.4fs %9.4fs",
|
||||
"1. Element + Direct (LU)", t_assembly_1, t_solve_1, t_total_1))
|
||||
println(@sprintf("%-40s %9.4fs %9.4fs %9.4fs",
|
||||
"2. Element + Iterative (CG, $cg_iters_2 iter)",
|
||||
t_assembly_2, t_solve_2, t_total_2))
|
||||
println(@sprintf("%-40s %9.4fs %9.4fs %9.4fs",
|
||||
"3. Nodal + Iterative (CG, $cg_iters_3 iter)",
|
||||
t_assembly_3, t_solve_3, t_total_3))
|
||||
println("-"^70)
|
||||
|
||||
println("\nAccuracy (vs Method 1):")
|
||||
println(@sprintf(" Method 2 error: %.3e", norm(u1 - u2)))
|
||||
println(@sprintf(" Method 3 error: %.3e", norm(u1 - u3)))
|
||||
|
||||
println("\nSpeedup vs Method 1:")
|
||||
println(@sprintf(" Method 2: %.2fx", t_total_1 / t_total_2))
|
||||
println(@sprintf(" Method 3: %.2fx", t_total_1 / t_total_3))
|
||||
|
||||
println("\nMax Displacement:")
|
||||
println(@sprintf(" Method 1: %.6f mm", maximum(abs.(u1)) * 1000))
|
||||
println(@sprintf(" Method 2: %.6f mm", maximum(abs.(u2)) * 1000))
|
||||
println(@sprintf(" Method 3: %.6f mm", maximum(abs.(u3)) * 1000))
|
||||
|
||||
println("\n" * "="^70)
|
||||
println("All methods complete!")
|
||||
println("="^70)
|
||||
Reference in New Issue
Block a user