diff --git a/benchmarks/README_GPU_BENCHMARKS.md b/benchmarks/README_GPU_BENCHMARKS.md
deleted file mode 100644
index bc9accc..0000000
--- a/benchmarks/README_GPU_BENCHMARKS.md
+++ /dev/null
@@ -1,244 +0,0 @@
-# GPU Benchmark Suite
-
-Comprehensive benchmarks demonstrating GPU-friendly FEM architecture strategies.
-
-## Prerequisites
-
-```bash
-# Add required packages
-julia --project=. -e 'using Pkg; Pkg.add(["CUDA", "Tensors", "BenchmarkTools", "IterativeSolvers"])'
-```
-
-## Benchmarks
-
-### 1. State Management Strategy Comparison
-
-**File:** `gpu_state_management_benchmark.jl`
-
-**What it tests:**
-
-- **Strategy 1:** Immutable elements (Array of Structs - AoS)
-- **Strategy 2:** Separate mutable state (Structure of Arrays - SoA)
-
-**Metrics:**
-
-- Memory bandwidth (GB/s)
-- Execution time
-- Allocation counts
-
-**Run:**
-
-```bash
-julia --project=. benchmarks/gpu_state_management_benchmark.jl
-```
-
-**Expected Results:**
-
-- CPU: Strategy 2 is 3-5× faster (better cache utilization)
-- GPU: Strategy 2 is 5-10× faster (coalesced memory access)
-- Strategy 2 bandwidth: 500-900 GB/s (depending on GPU)
-- Strategy 1 bandwidth: 50-150 GB/s (non-coalesced)
-
-### 2. Matrix-Free Newton-Krylov
-
-**File:** `matrix_free_gpu_benchmark.jl`
-
-**What it tests:**
-
-- **Traditional Newton:** Full Jacobian assembly + direct solve
-- **Matrix-Free NK:** Jacobian-free with GMRES
-- **Anderson-Accelerated:** Matrix-Free + Anderson acceleration
-
-**Metrics:**
-
-- Total time
-- Iterations to convergence
-- Time per iteration
-
-**Run:**
-
-```bash
-julia --project=. benchmarks/matrix_free_gpu_benchmark.jl
-```
-
-**Expected Results:**
-
-- Matrix-Free: 2-4× faster than traditional (no assembly)
-- Anderson: 2-3× fewer iterations (superlinear convergence)
-- GPU: Additional 3-10× speedup for large problems (>10K DOFs)
-- **Total:** 5-10× speedup with all optimizations
-
-## Understanding the Results
-
-### State Management Benchmark Output
-
-```text
-GPU Benchmark: 1000000 elements
-====================================================================
-
-📊 Strategy 1 (Immutable Elements - AoS on GPU):
- Time: 12.456 ms
- Bandwidth: 87.3 GB/s
-
-📊 Strategy 2 (Separate State - SoA on GPU):
- Time: 1.234 ms
- Bandwidth: 743.2 GB/s
-
-✅ GPU Speedup (Strategy 2 / Strategy 1): 10.09×
-✅ Bandwidth Improvement: 8.51×
- Strategy 1: 87.3 GB/s (non-coalesced)
- Strategy 2: 743.2 GB/s (coalesced)
-```
-
-**Interpretation:**
-
-- Strategy 2 achieves 8-10× higher bandwidth
-- Approaching GPU memory bandwidth limit (~1000 GB/s for high-end GPUs)
-- Coalesced memory access is critical for GPU performance
-
-### Matrix-Free Benchmark Output
-
-```text
-CPU Benchmark: 10000 DOFs
-====================================================================
-
-📊 Traditional Newton (Full Jacobian):
- Time: 456.78 ms
- Iterations: 8
- Time/iter: 57.10 ms
-
-📊 Matrix-Free Newton-Krylov:
- Time: 123.45 ms
- Iterations: 10
- Time/iter: 12.35 ms
-
-📊 Anderson-Accelerated Matrix-Free:
- Time: 67.89 ms
- Iterations: 5
- Time/iter: 13.58 ms
-
-✅ CPU Speedups:
- Matrix-Free vs Traditional: 3.70×
- Anderson vs Traditional: 6.73×
- Anderson vs Matrix-Free: 1.82×
-```
-
-**Interpretation:**
-
-- Matrix-Free eliminates expensive Jacobian assembly
-- Anderson reduces Newton iterations (superlinear convergence)
-- Combined: 6-10× speedup on CPU, more on GPU
-
-## Hardware Requirements
-
-### Minimum
-
-- CPU: x86_64 with AVX2
-- RAM: 8 GB
-- Julia: 1.9+
-
-### Recommended for GPU Benchmarks
-
-- GPU: NVIDIA with CUDA Compute Capability 7.0+ (RTX 20XX or newer)
-- VRAM: 4 GB+
-- CUDA: 11.0+
-
-### Tested On
-
-- NVIDIA RTX 4090 (24 GB VRAM)
-- NVIDIA RTX 3090 (24 GB VRAM)
-- NVIDIA RTX 3080 (10 GB VRAM)
-
-## Troubleshooting
-
-### "CUDA not available"
-
-If you see this warning, the benchmark will run CPU-only comparison:
-
-```julia
-⚠️ CUDA not available! Running CPU-only comparison.
-```
-
-**Solution:**
-
-1. Check GPU is recognized: `nvidia-smi`
-2. Verify CUDA installation: `julia -e 'using CUDA; CUDA.versioninfo()'`
-3. Rebuild CUDA.jl: `julia --project=. -e 'using Pkg; Pkg.build("CUDA")'`
-
-### Out of Memory Errors
-
-If benchmarks crash with OOM:
-
-```julia
-ERROR: Out of memory
-```
-
-**Solution:**
-
-- Reduce problem sizes in benchmark scripts
-- Edit `sizes = [10_000, 100_000, 1_000_000]` → smaller values
-- Close other GPU applications
-
-### Slow CPU Benchmarks
-
-Matrix-Free benchmark may be slow on CPU for large problems (>100K DOFs).
-
-**Solution:**
-
-- Use smaller test sizes for CPU
-- Focus on GPU results for large problems
-- Enable BLAS threading: `export JULIA_NUM_THREADS=8`
-
-## Performance Expectations
-
-### State Management (1M elements)
-
-| Strategy | CPU Time | GPU Time | GPU Bandwidth |
-|----------|----------|----------|---------------|
-| Strategy 1 (AoS) | 18 ms | 12 ms | 80-150 GB/s |
-| Strategy 2 (SoA) | 5 ms | 1.2 ms | 500-900 GB/s |
-| **Speedup** | **3.6×** | **10×** | **6-10×** |
-
-### Matrix-Free Newton-Krylov (100K DOFs)
-
-| Method | CPU Time | GPU Time | Iterations |
-|--------|----------|----------|------------|
-| Traditional Newton | 8200 ms | N/A | 20 |
-| Matrix-Free NK | 2100 ms | 450 ms | 20 |
-| MF-NK + Anderson | 1050 ms | 180 ms | 8 |
-| **Speedup** | **7.8×** | **45×** | **2.5× fewer** |
-
-## Validation
-
-Both benchmarks validate correctness:
-
-1. **State Management:**
- - Verifies final state matches between strategies
- - Checks zero allocations for Strategy 2
-
-2. **Matrix-Free:**
- - Compares solution accuracy (||u_traditional - u_matrixfree|| < 1e-6)
- - Validates convergence to same residual norm
-
-## Citation
-
-If you use these benchmarks in research, please cite:
-
-```bibtex
-@software{juliafem2025,
- title = {JuliaFEM: GPU-Accelerated Finite Element Method},
- author = {Aho, Jukka},
- year = {2025},
- url = {https://github.com/JuliaFEM/JuliaFEM.jl}
-}
-```
-
-## References
-
-1. **Knoll & Keyes (2004):** "Jacobian-free Newton–Krylov methods"
-2. **Walker & Ni (2011):** "Anderson acceleration for fixed-point iterations"
-3. **CUDA Programming Guide:**
-
-## Contact
-
-Questions or issues? Open an issue at:
diff --git a/benchmarks/VALIDATION_RESULTS.md b/benchmarks/VALIDATION_RESULTS.md
deleted file mode 100644
index 146dc73..0000000
--- a/benchmarks/VALIDATION_RESULTS.md
+++ /dev/null
@@ -1,85 +0,0 @@
-# Benchmark Validation Results
-
-**Date:** November 9, 2025
-**Platform:** Julia 1.12.1
-**Document:** `docs/book/zero_allocation_fields.md`
-**Benchmark:** `benchmarks/field_storage_comparison.jl`
-
-## Summary
-
-✅ **All performance claims validated**
-
-The zero-allocation field storage design achieves **9-92× speedup** over `Dict{String,Any}` with **zero allocations in hot paths**.
-
-## Measured Results
-
-| Test | OLD (Dict) | NEW (Typed) | Speedup |
-|------|------------|-------------|---------|
-| Constant field access | 19.2ns, 0 allocs | 2.1ns, 0 allocs | **9×** |
-| Nodal field access | 262ns, 3 allocs | 6.5ns, 0 allocs | **40×** |
-| Interpolation (uncached) | 2.6μs, 50 allocs | 44ns, 2 allocs | **59×** |
-| Interpolation (cached) | 2.6μs, 50 allocs | 53ns, **0 allocs** ✅ | **49×** |
-| Assembly (1000 elem) | 109μs, 4000 allocs | 1.2μs, **0 allocs** ✅ | **92×** |
-
-## Key Achievements
-
-1. ✅ **Zero allocations** in cached interpolation (53ns)
-2. ✅ **Zero allocations** in assembly loop (1.2μs vs 109μs)
-3. ✅ **Type stability** eliminates runtime dispatch
-4. ✅ **9-92× speedup** across all operations
-5. ✅ **Simple implementation** (~200 LOC for field types)
-
-## Design Validated
-
-The `NamedTuple` + typed field structs approach is proven effective:
-
-```julia
-# Simple field types
-struct ConstantField{T}
- value::T
-end
-
-struct NodalField{T}
- values::Matrix{T}
-end
-
-# Type-stable container
-fields = (
- youngs_modulus = ConstantField(210e3),
- displacement = NodalField(zeros(3, 1000)),
-)
-
-# Fast access (zero allocations)
-E = fields.youngs_modulus.value # 2.1ns, 0 allocs
-u = @view fields.displacement.values[:, nodes] # 6.5ns, 0 allocs
-```
-
-## Claims Verification
-
-| Claim | Measured | Status |
-|-------|----------|--------|
-| 50× faster | 9-92× across operations | ✅ VALIDATED |
-| 0 allocations | 0 allocs in hot paths | ✅ VALIDATED |
-| Type stability | No runtime dispatch | ✅ VALIDATED |
-| Simple implementation | ~200 LOC field types | ✅ VALIDATED |
-
-## Reproduction
-
-```bash
-cd /home/juajukka/dev/JuliaFEM.jl
-julia --project=. benchmarks/field_storage_comparison.jl
-```
-
-## Next Steps
-
-1. ✅ Document written and validated
-2. ⏭️ Implement field types in `src/fields/types.jl`
-3. ⏭️ Update `Element` struct for `ElementSet` pattern
-4. ⏭️ Add CI benchmarks to prevent regression
-5. ⏭️ Migrate examples to new field system
-
-## Conclusion
-
-The zero-allocation field storage design is **ready for v1.0 implementation**. Measured performance exceeds targets with 9-92× speedup and zero allocations in hot paths.
-
-**Design Decision:** Use `NamedTuple` of typed field structs for JuliaFEM v1.0
diff --git a/benchmarks/basis_function_access_patterns.jl b/benchmarks/basis_function_access_patterns.jl
deleted file mode 100644
index b72a413..0000000
--- a/benchmarks/basis_function_access_patterns.jl
+++ /dev/null
@@ -1,412 +0,0 @@
-# Benchmark: Basis Function Access Patterns for Tet10 (Realistic 3D Case)
-#
-# Focus: 10-node quadratic tetrahedron (Tet10) - the workhorse for 3D simulations
-#
-# Goal: Find the fastest way to access basis functions and their DERIVATIVES with:
-# 1. Return all 10 basis functions as tuple (zero allocation)
-# 2. Return single basis function by index (must be inlineable)
-# 3. Return all 10 derivatives as tuple of Vec{3} (zero allocation)
-# 4. Return single derivative by index (must be inlineable)
-# 5. Pass topology separately (separation of concerns)
-# 6. Must be type-stable and superfast
-#
-# Use Case:
-# - Stiffness matrix assembly: Need derivatives (B matrix construction)
-# - Mass matrix assembly: Need basis functions (M matrix construction)
-# - Nodal assembly: Need single basis function/derivative at a time
-#
-# Usage: julia --project=. benchmarks/basis_function_access_patterns.jl
-
-using BenchmarkTools
-using Tensors
-
-# ============================================================================
-# Define minimal types for testing
-# ============================================================================
-
-abstract type AbstractTopology end
-struct Tetrahedron <: AbstractTopology end
-
-abstract type AbstractBasis end
-struct Lagrange{P} <: AbstractBasis end
-
-# Vec type comes from Tensors.jl (already available in JuliaFEM)
-# Vec{3,Float64} for 3D gradients
-
-# ============================================================================
-# Strategy 1: Return tuple, index with getindex
-# ============================================================================
-# Advantages: Natural Julia syntax, type-stable
-# Disadvantages: Might not inline getindex?
-
-"""
-Get all basis functions for Triangle, P1 Lagrange (3 functions).
-Returns tuple of 3 Float64 values.
-"""
-@inline function get_basis_functions_v1(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
- u, v = xi
- # P1 triangle: N1 = 1-u-v, N2 = u, N3 = v
- return (1 - u - v, u, v)
-end
-
-"""
-Get single basis function by index (1-based).
-"""
-@inline function get_basis_function_v1(topology::Triangle, basis::Lagrange{1}, xi::Vec{2,T}, i::Int) where T
- N_all = get_basis_functions_v1(topology, basis, xi)
- return N_all[i] # Tuple indexing
-end
-
-# ============================================================================
-# Strategy 2: Generated function for single access
-# ============================================================================
-# Advantages: Compiler can specialize for each index
-# Disadvantages: More complex code
-
-@inline function get_basis_functions_v2(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
- u, v = xi
- return (1 - u - v, u, v)
-end
-
-"""
-Use @generated to create specialized code for each index at compile time.
-"""
-@generated function get_basis_function_v2(::Triangle, ::Lagrange{1}, xi::Vec{2,T}, ::Val{I}) where {T,I}
- if I == 1
- return :(1 - xi[1] - xi[2])
- elseif I == 2
- return :(xi[1])
- elseif I == 3
- return :(xi[2])
- else
- return :(error("Invalid basis function index: $I"))
- end
-end
-
-# ============================================================================
-# Strategy 3: Manual dispatch on Val (type-stable index)
-# ============================================================================
-# Advantages: Explicit, clear what's happening
-# Disadvantages: Verbose, need to write each case
-
-@inline function get_basis_functions_v3(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
- u, v = xi
- return (1 - u - v, u, v)
-end
-
-@inline get_basis_function_v3(t::Triangle, b::Lagrange{1}, xi::Vec{2,T}, ::Val{1}) where T = 1 - xi[1] - xi[2]
-@inline get_basis_function_v3(t::Triangle, b::Lagrange{1}, xi::Vec{2,T}, ::Val{2}) where T = xi[1]
-@inline get_basis_function_v3(t::Triangle, b::Lagrange{1}, xi::Vec{2,T}, ::Val{3}) where T = xi[2]
-
-# ============================================================================
-# Strategy 4: Struct with getindex (most Julian)
-# ============================================================================
-# Advantages: Can use N[i] syntax naturally
-# Disadvantages: Extra struct allocation?
-
-struct BasisFunctions{N,T}
- data::NTuple{N,T}
-end
-
-Base.@propagate_inbounds Base.getindex(bf::BasisFunctions, i::Int) = bf.data[i]
-Base.length(::BasisFunctions{N}) where N = N
-
-@inline function get_basis_functions_v4(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
- u, v = xi
- return BasisFunctions((1 - u - v, u, v))
-end
-
-# Can use natural indexing
-@inline function get_basis_function_v4(topology::Triangle, basis::Lagrange{1}, xi::Vec{2,T}, i::Int) where T
- N = get_basis_functions_v4(topology, basis, xi)
- return N[i]
-end
-
-# ============================================================================
-# Strategy 5: Separate implementation per basis function (extreme specialization)
-# ============================================================================
-# Advantages: Maximum performance, no tuple allocation at all
-# Disadvantages: Lots of code duplication
-
-@inline function get_basis_function_1_v5(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
- return 1 - xi[1] - xi[2]
-end
-
-@inline function get_basis_function_2_v5(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
- return xi[1]
-end
-
-@inline function get_basis_function_3_v5(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
- return xi[2]
-end
-
-@inline function get_basis_functions_v5(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
- u, v = xi
- return (1 - u - v, u, v)
-end
-
-# ============================================================================
-# Benchmark: Access all basis functions (typical in assembly loop)
-# ============================================================================
-
-function benchmark_all_access()
- println("\n" * "="^80)
- println("BENCHMARK: Access ALL basis functions")
- println("="^80)
-
- topology = Triangle()
- basis = Lagrange{1}()
- xi = Vec(0.25, 0.25)
-
- println("\nStrategy 1: Tuple return + getindex")
- @btime get_basis_functions_v1($topology, $basis, $xi)
-
- println("\nStrategy 2: Generated function")
- @btime get_basis_functions_v2($topology, $basis, $xi)
-
- println("\nStrategy 3: Val dispatch")
- @btime get_basis_functions_v3($topology, $basis, $xi)
-
- println("\nStrategy 4: BasisFunctions struct")
- @btime get_basis_functions_v4($topology, $basis, $xi)
-
- println("\nStrategy 5: Separate functions")
- @btime get_basis_functions_v5($topology, $basis, $xi)
-
- # Verify all return same values
- r1 = get_basis_functions_v1(topology, basis, xi)
- r2 = get_basis_functions_v2(topology, basis, xi)
- r3 = get_basis_functions_v3(topology, basis, xi)
- r4 = get_basis_functions_v4(topology, basis, xi).data
- r5 = get_basis_functions_v5(topology, basis, xi)
-
- @assert r1 == r2 == r3 == r4 == r5 "Results don't match!"
- println("\n✓ All strategies return identical values: $r1")
-end
-
-# ============================================================================
-# Benchmark: Access SINGLE basis function (for nodal assembly)
-# ============================================================================
-
-function benchmark_single_access()
- println("\n" * "="^80)
- println("BENCHMARK: Access SINGLE basis function (nodal assembly)")
- println("="^80)
-
- topology = Triangle()
- basis = Lagrange{1}()
- xi = Vec(0.25, 0.25)
-
- println("\nStrategy 1: Tuple + runtime index")
- @btime get_basis_function_v1($topology, $basis, $xi, 2)
-
- println("\nStrategy 2: Generated function with Val{2}")
- @btime get_basis_function_v2($topology, $basis, $xi, Val(2))
-
- println("\nStrategy 3: Val dispatch")
- @btime get_basis_function_v3($topology, $basis, $xi, Val(2))
-
- println("\nStrategy 4: BasisFunctions struct + index")
- @btime get_basis_function_v4($topology, $basis, $xi, 2)
-
- println("\nStrategy 5: Direct function call")
- @btime get_basis_function_2_v5($topology, $basis, $xi)
-
- # Verify all return same value
- r1 = get_basis_function_v1(topology, basis, xi, 2)
- r2 = get_basis_function_v2(topology, basis, xi, Val(2))
- r3 = get_basis_function_v3(topology, basis, xi, Val(2))
- r4 = get_basis_function_v4(topology, basis, xi, 2)
- r5 = get_basis_function_2_v5(topology, basis, xi)
-
- @assert r1 == r2 == r3 == r4 == r5 "Results don't match!"
- println("\n✓ All strategies return identical value: $r1")
-end
-
-# ============================================================================
-# Benchmark: Typical assembly loop pattern
-# ============================================================================
-
-function benchmark_assembly_loop()
- println("\n" * "="^80)
- println("BENCHMARK: Typical assembly loop (iterate over all basis functions)")
- println("="^80)
-
- topology = Triangle()
- basis = Lagrange{1}()
- xi = Vec(0.25, 0.25)
-
- # Pattern 1: Get all, iterate over tuple
- println("\nPattern 1: Get all as tuple, iterate")
- function assemble_v1()
- N_all = get_basis_functions_v1(topology, basis, xi)
- s = 0.0
- for N_i in N_all
- s += N_i * N_i # Dummy computation
- end
- return s
- end
- @btime assemble_v1()
-
- # Pattern 2: Get all, index in loop
- println("\nPattern 2: Get all, index with i")
- function assemble_v2()
- N_all = get_basis_functions_v1(topology, basis, xi)
- s = 0.0
- for i in 1:3
- s += N_all[i] * N_all[i]
- end
- return s
- end
- @btime assemble_v2()
-
- # Pattern 3: Get one at a time (nodal assembly style)
- println("\nPattern 3: Get one at a time with Val")
- function assemble_v3()
- s = 0.0
- # Unrolled loop (what compiler would do with Val)
- N1 = get_basis_function_v3(topology, basis, xi, Val(1))
- s += N1 * N1
- N2 = get_basis_function_v3(topology, basis, xi, Val(2))
- s += N2 * N2
- N3 = get_basis_function_v3(topology, basis, xi, Val(3))
- s += N3 * N3
- return s
- end
- @btime assemble_v3()
-
- # Pattern 4: Direct function calls (strategy 5)
- println("\nPattern 4: Direct function calls (extreme specialization)")
- function assemble_v4()
- s = 0.0
- N1 = get_basis_function_1_v5(topology, basis, xi)
- s += N1 * N1
- N2 = get_basis_function_2_v5(topology, basis, xi)
- s += N2 * N2
- N3 = get_basis_function_3_v5(topology, basis, xi)
- s += N3 * N3
- return s
- end
- @btime assemble_v4()
-
- # Verify all compute same result
- r1 = assemble_v1()
- r2 = assemble_v2()
- r3 = assemble_v3()
- r4 = assemble_v4()
- @assert r1 == r2 == r3 == r4 "Assembly results don't match!"
- println("\n✓ All patterns compute same result: $r1")
-end
-
-# ============================================================================
-# Benchmark: Basis derivatives (return Vec)
-# ============================================================================
-
-function benchmark_derivatives()
- println("\n" * "="^80)
- println("BENCHMARK: Basis function DERIVATIVES (return Vec)")
- println("="^80)
-
- topology = Triangle()
- basis = Lagrange{1}()
- xi = Vec(0.25, 0.25)
-
- # Triangle P1 derivatives (constant):
- # dN1/d(u,v) = (-1, -1)
- # dN2/d(u,v) = (1, 0)
- # dN3/d(u,v) = (0, 1)
-
- println("\nStrategy 1: Return tuple of Vecs")
- @inline function get_basis_derivatives_v1(::Triangle, ::Lagrange{1}, xi::Vec{2,T}) where T
- return (Vec(-1.0, -1.0), Vec(1.0, 0.0), Vec(0.0, 1.0))
- end
- @btime get_basis_derivatives_v1($topology, $basis, $xi)
-
- println("\nStrategy 2: Return single Vec with Val indexing")
- @inline get_basis_derivative_v2(::Triangle, ::Lagrange{1}, xi::Vec{2,T}, ::Val{1}) where T = Vec(-1.0, -1.0)
- @inline get_basis_derivative_v2(::Triangle, ::Lagrange{1}, xi::Vec{2,T}, ::Val{2}) where T = Vec(1.0, 0.0)
- @inline get_basis_derivative_v2(::Triangle, ::Lagrange{1}, xi::Vec{2,T}, ::Val{3}) where T = Vec(0.0, 1.0)
- @btime get_basis_derivative_v2($topology, $basis, $xi, Val(2))
-
- # Verify
- all_derivs = get_basis_derivatives_v1(topology, basis, xi)
- single_deriv = get_basis_derivative_v2(topology, basis, xi, Val(2))
- @assert all_derivs[2] == single_deriv
- println("\n✓ Derivatives match: $single_deriv")
-end
-
-# ============================================================================
-# Main execution
-# ============================================================================
-
-function main()
- println("\n")
- println("╔" * "="^78 * "╗")
- println("║" * " "^78 * "║")
- println("║" * " "^20 * "BASIS FUNCTION ACCESS PATTERNS BENCHMARK" * " "^18 * "║")
- println("║" * " "^78 * "║")
- println("╚" * "="^78 * "╝")
-
- println("\nGoal: Find fastest way to access basis functions for nodal assembly")
- println("Requirements:")
- println(" - Zero allocation")
- println(" - Type stable")
- println(" - Inlineable")
- println(" - Support both 'all at once' and 'one at a time' access")
-
- benchmark_all_access()
- benchmark_single_access()
- benchmark_assembly_loop()
- benchmark_derivatives()
-
- println("\n" * "="^80)
- println("SUMMARY & RECOMMENDATIONS")
- println("="^80)
- println("""
-
- For TRADITIONAL ASSEMBLY (get all basis functions at integration point):
- → Use Strategy 1 or 4: Simple tuple return
- → Should be 0-5 ns, zero allocation
-
- For NODAL ASSEMBLY (get single basis function):
- → Use Strategy 3: Val dispatch for compile-time index
- → Should be 0-2 ns, zero allocation, fully inlined
- → Usage: get_basis_function(Triangle(), Lagrange{1}(), xi, Val(i))
-
- For DERIVATIVES:
- → Return tuple of Vec for all derivatives
- → Use Val indexing for single derivative
- → Same performance as basis functions
-
- RECOMMENDED API:
- ```julia
- # Get all basis functions (returns tuple)
- N_all = get_basis_functions(Triangle(), Lagrange{1}(), xi)
-
- # Get single basis function (Val for compile-time specialization)
- N_i = get_basis_function(Triangle(), Lagrange{1}(), xi, Val(i))
-
- # Get all derivatives (returns tuple of Vec)
- dN_all = get_basis_derivatives(Triangle(), Lagrange{1}(), xi)
-
- # Get single derivative (returns Vec)
- dN_i = get_basis_derivative(Triangle(), Lagrange{1}(), xi, Val(i))
- ```
-
- WHY Val?
- - Compiler knows index at compile time
- - Can generate optimal code for each basis function
- - Zero runtime overhead
- - Type stable
-
- NOTE: For runtime indexing (i not known at compile time), tuple indexing
- is still very fast (typically 1-2 ns overhead).
- """)
-
- println("\n" * "="^80)
-end
-
-# Run benchmarks
-if abspath(PROGRAM_FILE) == @__FILE__
- main()
-end
diff --git a/benchmarks/basis_function_access_tet10.jl b/benchmarks/basis_function_access_tet10.jl
deleted file mode 100644
index 0dd6f12..0000000
--- a/benchmarks/basis_function_access_tet10.jl
+++ /dev/null
@@ -1,504 +0,0 @@
-# Benchmark: Basis Function Access Patterns for Tet10 (Realistic 3D Case)
-#
-# Focus: 10-node quadratic tetrahedron (Tet10) - the workhorse for 3D simulations
-#
-# Goal: Find the fastest way to access basis functions and their DERIVATIVES with:
-# 1. Return all 10 basis functions as tuple (zero allocation)
-# 2. Return single basis function by index (must be inlineable)
-# 3. Return all 10 derivatives as tuple of Vec{3} (zero allocation)
-# 4. Return single derivative by index (must be inlineable)
-# 5. Pass topology separately (separation of concerns: Element(Tetrahedron, Lagrange{2}, ...))
-# 6. Must be type-stable and superfast
-#
-# Use Case:
-# - Stiffness matrix assembly: Need derivatives (B matrix construction)
-# - Mass matrix assembly: Need basis functions (M matrix construction)
-# - Nodal assembly: Need single basis function/derivative at a time
-#
-# Usage: julia --project=. benchmarks/basis_function_access_tet10.jl
-
-using BenchmarkTools
-using Tensors
-
-# ============================================================================
-# Define minimal types for testing
-# ============================================================================
-
-abstract type AbstractTopology end
-struct Tetrahedron <: AbstractTopology end
-
-abstract type AbstractBasis end
-struct Lagrange{P} <: AbstractBasis end
-
-# ============================================================================
-# Tet10 Basis Functions (Quadratic Tetrahedron, 10 nodes)
-# ============================================================================
-# Node numbering:
-# 1-4: vertices
-# 5-10: edge midpoints (5: 1-2, 6: 2-3, 7: 3-1, 8: 1-4, 9: 2-4, 10: 3-4)
-#
-# Parametric coordinates: (u, v, w) where u+v+w ≤ 1
-# Reference element: vertices at (0,0,0), (1,0,0), (0,1,0), (0,0,1)
-
-"""
-Get all 10 basis functions for Tet10 at parametric point (u,v,w).
-Returns NTuple{10, Float64}.
-
-The basis functions are:
-- N1 = (1-u-v-w)(1-2u-2v-2w) [vertex 1]
-- N2 = u(2u-1) [vertex 2]
-- N3 = v(2v-1) [vertex 3]
-- N4 = w(2w-1) [vertex 4]
-- N5 = 4u(1-u-v-w) [edge 1-2]
-- N6 = 4uv [edge 2-3]
-- N7 = 4v(1-u-v-w) [edge 3-1]
-- N8 = 4w(1-u-v-w) [edge 1-4]
-- N9 = 4uw [edge 2-4]
-- N10= 4vw [edge 3-4]
-"""
-@inline function get_basis_functions(::Tetrahedron, ::Lagrange{2}, xi::Vec{3,T}) where T
- u, v, w = xi
- λ = 1 - u - v - w # barycentric coordinate for vertex 1
-
- # Vertex nodes (1-4)
- N1 = λ * (2λ - 1)
- N2 = u * (2u - 1)
- N3 = v * (2v - 1)
- N4 = w * (2w - 1)
-
- # Edge midpoint nodes (5-10)
- N5 = 4 * u * λ
- N6 = 4 * u * v
- N7 = 4 * v * λ
- N8 = 4 * w * λ
- N9 = 4 * u * w
- N10 = 4 * v * w
-
- return (N1, N2, N3, N4, N5, N6, N7, N8, N9, N10)
-end
-
-"""
-Get all 10 basis function derivatives for Tet10.
-Returns NTuple{10, Vec{3,Float64}}.
-
-Each derivative is ∇N_i = (∂N_i/∂u, ∂N_i/∂v, ∂N_i/∂w)
-"""
-@inline function get_basis_derivatives(::Tetrahedron, ::Lagrange{2}, xi::Vec{3,T}) where T
- u, v, w = xi
- λ = 1 - u - v - w
-
- # Derivatives of vertex nodes
- dN1 = Vec(-3 + 4u + 4v + 4w, -3 + 4u + 4v + 4w, -3 + 4u + 4v + 4w)
- dN2 = Vec(4u - 1, 0.0, 0.0)
- dN3 = Vec(0.0, 4v - 1, 0.0)
- dN4 = Vec(0.0, 0.0, 4w - 1)
-
- # Derivatives of edge midpoint nodes
- dN5 = Vec(4λ - 4u, -4u, -4u)
- dN6 = Vec(4v, 4u, 0.0)
- dN7 = Vec(-4v, 4λ - 4v, -4v)
- dN8 = Vec(-4w, -4w, 4λ - 4w)
- dN9 = Vec(4w, 0.0, 4u)
- dN10 = Vec(0.0, 4w, 4v)
-
- return (dN1, dN2, dN3, dN4, dN5, dN6, dN7, dN8, dN9, dN10)
-end
-
-# ============================================================================
-# Strategy 1: Tuple indexing (runtime index)
-# ============================================================================
-
-@inline function get_basis_function_v1(topology::Tetrahedron, basis::Lagrange{2},
- xi::Vec{3,T}, i::Int) where T
- N_all = get_basis_functions(topology, basis, xi)
- return N_all[i]
-end
-
-@inline function get_basis_derivative_v1(topology::Tetrahedron, basis::Lagrange{2},
- xi::Vec{3,T}, i::Int) where T
- dN_all = get_basis_derivatives(topology, basis, xi)
- return dN_all[i]
-end
-
-# ============================================================================
-# Strategy 2: Val dispatch (compile-time index)
-# ============================================================================
-
-@inline function get_basis_function_v2(t::Tetrahedron, b::Lagrange{2},
- xi::Vec{3,T}, ::Val{I}) where {T,I}
- N_all = get_basis_functions(t, b, xi)
- return N_all[I]
-end
-
-@inline function get_basis_derivative_v2(t::Tetrahedron, b::Lagrange{2},
- xi::Vec{3,T}, ::Val{I}) where {T,I}
- dN_all = get_basis_derivatives(t, b, xi)
- return dN_all[I]
-end
-
-# ============================================================================
-# Strategy 3: Generated function (compute only requested basis function)
-# ============================================================================
-
-@generated function get_basis_function_v3(::Tetrahedron, ::Lagrange{2},
- xi::Vec{3,T}, ::Val{I}) where {T,I}
- # Generate specialized code for each index
- if I == 1
- return quote
- u, v, w = xi
- λ = 1 - u - v - w
- return λ * (2λ - 1)
- end
- elseif I == 2
- return quote
- u = xi[1]
- return u * (2u - 1)
- end
- elseif I == 3
- return quote
- v = xi[2]
- return v * (2v - 1)
- end
- elseif I == 4
- return quote
- w = xi[3]
- return w * (2w - 1)
- end
- elseif I == 5
- return quote
- u, v, w = xi
- λ = 1 - u - v - w
- return 4 * u * λ
- end
- elseif I == 6
- return quote
- u, v = xi[1], xi[2]
- return 4 * u * v
- end
- elseif I == 7
- return quote
- u, v, w = xi
- λ = 1 - u - v - w
- return 4 * v * λ
- end
- elseif I == 8
- return quote
- u, v, w = xi
- λ = 1 - u - v - w
- return 4 * w * λ
- end
- elseif I == 9
- return quote
- u, w = xi[1], xi[3]
- return 4 * u * w
- end
- elseif I == 10
- return quote
- v, w = xi[2], xi[3]
- return 4 * v * w
- end
- else
- return :(error("Invalid basis function index: $I for Tet10"))
- end
-end
-
-@generated function get_basis_derivative_v3(::Tetrahedron, ::Lagrange{2},
- xi::Vec{3,T}, ::Val{I}) where {T,I}
- if I == 1
- return quote
- u, v, w = xi
- return Vec(-3 + 4u + 4v + 4w, -3 + 4u + 4v + 4w, -3 + 4u + 4v + 4w)
- end
- elseif I == 2
- return quote
- u = xi[1]
- return Vec(4u - 1, 0.0, 0.0)
- end
- elseif I == 3
- return quote
- v = xi[2]
- return Vec(0.0, 4v - 1, 0.0)
- end
- elseif I == 4
- return quote
- w = xi[3]
- return Vec(0.0, 0.0, 4w - 1)
- end
- elseif I == 5
- return quote
- u, v, w = xi
- λ = 1 - u - v - w
- return Vec(4λ - 4u, -4u, -4u)
- end
- elseif I == 6
- return quote
- u, v = xi[1], xi[2]
- return Vec(4v, 4u, 0.0)
- end
- elseif I == 7
- return quote
- u, v, w = xi
- λ = 1 - u - v - w
- return Vec(-4v, 4λ - 4v, -4v)
- end
- elseif I == 8
- return quote
- u, v, w = xi
- λ = 1 - u - v - w
- return Vec(-4w, -4w, 4λ - 4w)
- end
- elseif I == 9
- return quote
- u, w = xi[1], xi[3]
- return Vec(4w, 0.0, 4u)
- end
- elseif I == 10
- return quote
- v, w = xi[2], xi[3]
- return Vec(0.0, 4w, 4v)
- end
- else
- return :(error("Invalid basis function index: $I for Tet10"))
- end
-end
-
-# ============================================================================
-# Benchmark Functions
-# ============================================================================
-
-function benchmark_all_basis_functions()
- println("\n" * "="^80)
- println("BENCHMARK 1: Get ALL 10 basis functions")
- println("="^80)
- println("Use case: Mass matrix assembly, need all N_i at integration point")
-
- topology = Tetrahedron()
- basis = Lagrange{2}()
- xi = Vec(0.25, 0.25, 0.2) # Typical integration point
-
- println("\nAccess all 10 basis functions:")
- @btime get_basis_functions($topology, $basis, $xi)
-
- result = get_basis_functions(topology, basis, xi)
- println("\n✓ Result (10 values): ", result)
- println("✓ Sum of basis functions (partition of unity): ", sum(result))
- @assert abs(sum(result) - 1.0) < 1e-10 "Partition of unity violated!"
-end
-
-function benchmark_single_basis_function()
- println("\n" * "="^80)
- println("BENCHMARK 2: Get SINGLE basis function (nodal assembly)")
- println("="^80)
- println("Use case: Nodal assembly, need N_i for specific node")
-
- topology = Tetrahedron()
- basis = Lagrange{2}()
- xi = Vec(0.25, 0.25, 0.2)
- node_idx = 5 # Edge midpoint node
-
- println("\nStrategy 1: Tuple + runtime index")
- @btime get_basis_function_v1($topology, $basis, $xi, $node_idx)
-
- println("\nStrategy 2: Val dispatch (compile-time index)")
- @btime get_basis_function_v2($topology, $basis, $xi, Val($node_idx))
-
- println("\nStrategy 3: @generated function (minimal computation)")
- @btime get_basis_function_v3($topology, $basis, $xi, Val($node_idx))
-
- # Verify all return same value
- r1 = get_basis_function_v1(topology, basis, xi, node_idx)
- r2 = get_basis_function_v2(topology, basis, xi, Val(node_idx))
- r3 = get_basis_function_v3(topology, basis, xi, Val(node_idx))
- @assert r1 ≈ r2 ≈ r3 "Strategies return different values!"
- println("\n✓ All strategies return: N_$node_idx = $r1")
-end
-
-function benchmark_all_derivatives()
- println("\n" * "="^80)
- println("BENCHMARK 3: Get ALL 10 basis function derivatives")
- println("="^80)
- println("Use case: Stiffness matrix assembly (B matrix construction)")
- println("Most important benchmark for 3D simulations!")
-
- topology = Tetrahedron()
- basis = Lagrange{2}()
- xi = Vec(0.25, 0.25, 0.2)
-
- println("\nAccess all 10 derivatives (each is Vec{3}):")
- @btime get_basis_derivatives($topology, $basis, $xi)
-
- result = get_basis_derivatives(topology, basis, xi)
- println("\n✓ Result (10 Vec{3} gradients):")
- for (i, dN) in enumerate(result)
- println(" ∇N_$i = $dN")
- end
-end
-
-function benchmark_single_derivative()
- println("\n" * "="^80)
- println("BENCHMARK 4: Get SINGLE basis function derivative")
- println("="^80)
- println("Use case: Nodal assembly for stiffness matrix")
-
- topology = Tetrahedron()
- basis = Lagrange{2}()
- xi = Vec(0.25, 0.25, 0.2)
- node_idx = 5
-
- println("\nStrategy 1: Tuple + runtime index")
- @btime get_basis_derivative_v1($topology, $basis, $xi, $node_idx)
-
- println("\nStrategy 2: Val dispatch")
- @btime get_basis_derivative_v2($topology, $basis, $xi, Val($node_idx))
-
- println("\nStrategy 3: @generated function")
- @btime get_basis_derivative_v3($topology, $basis, $xi, Val($node_idx))
-
- # Verify
- r1 = get_basis_derivative_v1(topology, basis, xi, node_idx)
- r2 = get_basis_derivative_v2(topology, basis, xi, Val(node_idx))
- r3 = get_basis_derivative_v3(topology, basis, xi, Val(node_idx))
- @assert r1 ≈ r2 ≈ r3 "Strategies return different values!"
- println("\n✓ All strategies return: ∇N_$node_idx = $r1")
-end
-
-function benchmark_stiffness_assembly_pattern()
- println("\n" * "="^80)
- println("BENCHMARK 5: Realistic stiffness matrix assembly loop")
- println("="^80)
- println("Use case: Compute element stiffness K_e (typical FEM inner loop)")
- println("Pattern: B^T D B where B = strain-displacement matrix")
-
- topology = Tetrahedron()
- basis = Lagrange{2}()
- xi = Vec(0.25, 0.25, 0.2) # Integration point
-
- # Typical pattern: Get all derivatives, compute B matrix terms
- println("\nPattern 1: Get all derivatives at once (traditional)")
- stiffness_loop_v1 = let topology = topology, basis = basis, xi = xi
- () -> begin
- dN_all = get_basis_derivatives(topology, basis, xi)
- s = 0.0
- # Simplified: compute trace of K (just for benchmarking)
- for i in 1:10
- for j in 1:10
- dNi = dN_all[i]
- dNj = dN_all[j]
- s += dot(dNi, dNj) # Simplified K_ij computation
- end
- end
- return s
- end
- end
- @btime $stiffness_loop_v1()
-
- println("\nPattern 2: Get derivatives with @generated (manual unroll)")
- stiffness_loop_v3 = let topology = topology, basis = basis, xi = xi
- () -> begin
- s = 0.0
- # Unrolled loop (compiler would do this with Val)
- dN1 = get_basis_derivative_v3(topology, basis, xi, Val(1))
- dN2 = get_basis_derivative_v3(topology, basis, xi, Val(2))
- dN3 = get_basis_derivative_v3(topology, basis, xi, Val(3))
- dN4 = get_basis_derivative_v3(topology, basis, xi, Val(4))
- dN5 = get_basis_derivative_v3(topology, basis, xi, Val(5))
- dN6 = get_basis_derivative_v3(topology, basis, xi, Val(6))
- dN7 = get_basis_derivative_v3(topology, basis, xi, Val(7))
- dN8 = get_basis_derivative_v3(topology, basis, xi, Val(8))
- dN9 = get_basis_derivative_v3(topology, basis, xi, Val(9))
- dN10 = get_basis_derivative_v3(topology, basis, xi, Val(10))
-
- # Compute all pairs (100 dot products)
- for dNi in (dN1, dN2, dN3, dN4, dN5, dN6, dN7, dN8, dN9, dN10)
- for dNj in (dN1, dN2, dN3, dN4, dN5, dN6, dN7, dN8, dN9, dN10)
- s += dot(dNi, dNj)
- end
- end
- return s
- end
- end
- @btime $stiffness_loop_v3()
-
- # Verify all compute same result
- r1 = stiffness_loop_v1()
- r3 = stiffness_loop_v3()
- @assert abs(r1 - r3) < 1e-10 "Assembly patterns give different results!"
- println("\n✓ Assembly result: $r1")
-end
-
-# ============================================================================
-# Main execution
-# ============================================================================
-
-function main()
- println("\n")
- println("╔" * "="^78 * "╗")
- println("║" * " "^78 * "║")
- println("║" * " "^15 * "TET10 BASIS FUNCTION ACCESS BENCHMARK" * " "^25 * "║")
- println("║" * " "^20 * "(10-node Quadratic Tetrahedron)" * " "^26 * "║")
- println("║" * " "^78 * "║")
- println("╚" * "="^78 * "╝")
-
- println("\nElement: Tet10 (10-node quadratic tetrahedron)")
- println("Nodes: 4 vertices + 6 edge midpoints")
- println("Polynomial degree: P2 (quadratic)")
- println("Dimension: 3D")
- println("\nSeparation of concerns API:")
- println(" Element(Tetrahedron, Lagrange{2}, connectivity)")
- println(" get_basis_functions(Tetrahedron(), Lagrange{2}(), xi)")
- println(" get_basis_derivatives(Tetrahedron(), Lagrange{2}(), xi)")
-
- benchmark_all_basis_functions()
- benchmark_single_basis_function()
- benchmark_all_derivatives()
- benchmark_single_derivative()
- benchmark_stiffness_assembly_pattern()
-
- println("\n" * "="^80)
- println("SUMMARY & RECOMMENDATIONS FOR 3D SIMULATIONS")
- println("="^80)
- println("""
-
- FOR STIFFNESS MATRIX ASSEMBLY (derivatives):
- → Use get_basis_derivatives() - returns all 10 gradients as tuple
- → Expected: ~10-30 ns, zero allocation
- → This is the HOT PATH for 3D FEM!
-
- FOR MASS MATRIX ASSEMBLY (basis functions):
- → Use get_basis_functions() - returns all 10 values as tuple
- → Expected: ~5-15 ns, zero allocation
-
- FOR NODAL ASSEMBLY (single node operations):
- → Use Val dispatch: get_basis_derivative(t, b, xi, Val(i))
- → @generated gives minimal computation (only compute requested function)
- → Expected: ~5-10 ns per node
-
- API DESIGN DECISION:
- ```julia
- # Separation of concerns (RECOMMENDED):
- Element(Tetrahedron, Lagrange{2}, connectivity)
-
- # Functions take topology explicitly:
- dN_all = get_basis_derivatives(Tetrahedron(), Lagrange{2}(), xi)
- dN_i = get_basis_derivative(Tetrahedron(), Lagrange{2}(), xi, Val(i))
- ```
-
- WHY THIS API?
- - Clear separation: topology is geometry, basis is interpolation
- - Topology passed to basis evaluation (no redundancy in type parameters)
- - Type-stable, zero-allocation, fully inlined
- - Works with any basis type (Lagrange, Hierarchical, Nedelec, etc.)
-
- PERFORMANCE TARGET:
- - 10-node Tet10 derivatives: < 30 ns (achieved!)
- - 100× faster than old Dict-based approach
- - Ready for million-element meshes
- """)
-
- println("\n" * "="^80)
-end
-
-# Run benchmarks
-if abspath(PROGRAM_FILE) == @__FILE__
- main()
-end
diff --git a/benchmarks/deformation_gradient_analysis.jl b/benchmarks/deformation_gradient_analysis.jl
deleted file mode 100644
index 057bc0a..0000000
--- a/benchmarks/deformation_gradient_analysis.jl
+++ /dev/null
@@ -1,240 +0,0 @@
-# Performance Analysis for Deformation Gradient Implementation
-# This script analyzes the machine code generated and validates zero-allocation claims
-
-using JuliaFEM
-using Tensors
-using BenchmarkTools
-using InteractiveUtils
-
-# Load the deformation gradient code
-include("../src/physics/deformation_gradient.jl")
-
-println("="^80)
-println("DEFORMATION GRADIENT PERFORMANCE ANALYSIS")
-println("="^80)
-println()
-
-# Setup test data
-X_nodes = (
- Vec(0.0, 0.0, 0.0),
- Vec(1.0, 0.0, 0.0),
- Vec(1.0, 1.0, 0.0),
- Vec(0.0, 1.0, 0.0),
- Vec(0.0, 0.0, 1.0),
- Vec(1.0, 0.0, 1.0),
- Vec(1.0, 1.0, 1.0),
- Vec(0.0, 1.0, 1.0)
-)
-
-u_nodes = (
- Vec(0.0, 0.0, 0.0),
- Vec(0.1, 0.0, 0.0),
- Vec(0.1, 0.0, 0.0),
- Vec(0.0, 0.0, 0.0),
- Vec(0.0, 0.0, 0.0),
- Vec(0.1, 0.0, 0.0),
- Vec(0.1, 0.0, 0.0),
- Vec(0.0, 0.0, 0.0)
-)
-
-ξ = Vec(0.0, 0.0, 0.0)
-dN_dξ = get_basis_derivatives(Hexahedron(), Lagrange{Hexahedron,1}(), ξ)
-
-# Compute Jacobian
-function compute_jacobian(X_nodes, dN_dξ)
- J = zero(Tensor{2,3,Float64,9})
- for i in 1:8
- J += X_nodes[i] ⊗ dN_dξ[i]
- end
- return J
-end
-
-J = compute_jacobian(X_nodes, dN_dξ)
-
-# ============================================================================
-# 1. ALLOCATION ANALYSIS
-# ============================================================================
-println("1. ALLOCATION ANALYSIS")
-println("-"^80)
-
-# Warm up (compile)
-F = compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain())
-
-# Measure allocations
-allocs = @allocated compute_deformation_gradient(X_nodes, u_nodes, dN_dξ, J, FiniteStrain())
-println("Allocations: $allocs bytes")
-
-if allocs == 0
- println("✅ ZERO ALLOCATIONS CONFIRMED!")
-else
- println("❌ WARNING: Found $allocs bytes allocated!")
-end
-println()
-
-# ============================================================================
-# 2. PERFORMANCE BENCHMARKING
-# ============================================================================
-println("2. PERFORMANCE BENCHMARKING")
-println("-"^80)
-
-println("Benchmarking compute_deformation_gradient...")
-result = @benchmark compute_deformation_gradient($X_nodes, $u_nodes, $dN_dξ, $J, FiniteStrain())
-println(result)
-println()
-
-median_time_ns = median(result.times)
-println("Median time: $(median_time_ns) ns = $(median_time_ns/1000) μs")
-println()
-
-# ============================================================================
-# 3. LLVM IR ANALYSIS
-# ============================================================================
-println("3. LLVM IR ANALYSIS")
-println("-"^80)
-println("Examining LLVM IR for signs of optimization...")
-println()
-
-io_llvm = IOBuffer()
-code_llvm(io_llvm, compute_deformation_gradient,
- typeof((X_nodes, u_nodes, dN_dξ, J, FiniteStrain())))
-llvm_code = String(take!(io_llvm))
-
-# Count key indicators
-n_allocations = count(r"@julia.gc_alloc_obj", llvm_code)
-n_stores = count(r"store", llvm_code)
-n_loads = count(r"load", llvm_code)
-n_vector_ops = count(r"<\d+ x ", llvm_code) # SIMD vector operations
-
-println("LLVM IR Statistics:")
-println(" - GC allocations: $n_allocations")
-println(" - Store operations: $n_stores")
-println(" - Load operations: $n_loads")
-println(" - Vector operations (SIMD): $n_vector_ops")
-println()
-
-if n_allocations == 0
- println("✅ No GC allocations in LLVM IR!")
-else
- println("❌ WARNING: Found $n_allocations GC allocation calls!")
-end
-
-if n_vector_ops > 0
- println("✅ SIMD vectorization detected!")
-end
-println()
-
-# Print full LLVM (first 100 lines)
-println("Full LLVM IR (first 100 lines):")
-println("-"^80)
-llvm_lines = split(llvm_code, '\n')
-for (i, line) in enumerate(llvm_lines[1:min(100, length(llvm_lines))])
- println(line)
-end
-println()
-
-# ============================================================================
-# 4. NATIVE ASSEMBLY ANALYSIS
-# ============================================================================
-println("4. NATIVE ASSEMBLY ANALYSIS")
-println("-"^80)
-println("Examining native assembly...")
-println()
-
-io_native = IOBuffer()
-code_native(io_native, compute_deformation_gradient,
- typeof((X_nodes, u_nodes, dN_dξ, J, FiniteStrain())))
-native_code = String(take!(io_native))
-
-# Count key assembly features
-n_movsd = count(r"movsd", native_code) # Scalar moves
-n_movapd = count(r"movapd", native_code) # Aligned packed moves
-n_movupd = count(r"movupd", native_code) # Unaligned packed moves
-n_mulpd = count(r"mulpd", native_code) # Packed multiply
-n_addpd = count(r"addpd", native_code) # Packed add
-n_call = count(r"call", native_code) # Function calls
-
-println("Native Assembly Statistics:")
-println(" - Scalar moves (movsd): $n_movsd")
-println(" - Aligned packed moves (movapd): $n_movapd")
-println(" - Unaligned packed moves (movupd): $n_movupd")
-println(" - Packed multiplies (mulpd): $n_mulpd")
-println(" - Packed adds (addpd): $n_addpd")
-println(" - Function calls: $n_call")
-println()
-
-if n_mulpd > 0 || n_addpd > 0
- println("✅ SSE/AVX SIMD instructions detected!")
-end
-
-if n_call == 0
- println("✅ Fully inlined - no function calls!")
-else
- println("⚠️ Note: $n_call function calls detected (may include math library)")
-end
-println()
-
-# Print full assembly (first 100 lines)
-println("Full Native Assembly (first 100 lines):")
-println("-"^80)
-native_lines = split(native_code, '\n')
-for (i, line) in enumerate(native_lines[1:min(100, length(native_lines))])
- println(line)
-end
-println()
-
-# ============================================================================
-# 5. TYPE STABILITY ANALYSIS
-# ============================================================================
-println("5. TYPE STABILITY ANALYSIS")
-println("-"^80)
-println("Checking type stability with @code_warntype...")
-println()
-
-io_warntype = IOBuffer()
-code_warntype(io_warntype, compute_deformation_gradient,
- typeof((X_nodes, u_nodes, dN_dξ, J, FiniteStrain())))
-warntype_output = String(take!(io_native))
-
-# Check for type instabilities
-has_any = contains(warntype_output, "Any")
-has_union = contains(warntype_output, "Union{")
-
-if has_any
- println("⚠️ WARNING: 'Any' types detected (type instability)")
-else
- println("✅ No 'Any' types detected!")
-end
-
-if has_union
- println("⚠️ Note: Union types detected (may be intentional)")
-else
- println("✅ No Union types detected!")
-end
-println()
-
-# Print warntype output (first 50 lines)
-println("@code_warntype output (first 50 lines):")
-println("-"^80)
-warntype_lines = split(warntype_output, '\n')
-for (i, line) in enumerate(warntype_lines[1:min(50, length(warntype_lines))])
- println(line)
-end
-println()
-
-# ============================================================================
-# 6. SUMMARY
-# ============================================================================
-println("="^80)
-println("PERFORMANCE SUMMARY")
-println("="^80)
-println()
-println("✅ Implementation validated as:")
-println(" - Zero allocation (confirmed)")
-println(" - Type stable")
-println(" - SIMD optimized ($(n_vector_ops) vector ops in LLVM)")
-println(" - Median execution time: $(round(median_time_ns, digits=2)) ns")
-println()
-println("This implementation achieves the best possible performance for")
-println("deformation gradient computation in Julia.")
-println()
-println("="^80)
diff --git a/benchmarks/element_immutability_benchmark.jl b/benchmarks/element_immutability_benchmark.jl
deleted file mode 100644
index f86f3df..0000000
--- a/benchmarks/element_immutability_benchmark.jl
+++ /dev/null
@@ -1,396 +0,0 @@
-# ==============================================================================
-# ELEMENT IMMUTABILITY BENCHMARK
-# ==============================================================================
-#
-# Purpose: Demonstrate why immutable elements with type-stable fields are faster
-# than mutable elements with Dict-based fields, despite seeming
-# counterintuitive.
-#
-# Hypothesis: Immutable + type-stable >> Mutable + Dict
-#
-# What we measure:
-# 1. Field access time (reading)
-# 2. Field update time (writing)
-# 3. Memory allocations
-# 4. Assembly loop performance (realistic FEM workload)
-#
-# Expected results:
-# - Dict lookup: O(1) amortized, but ~100ns overhead per access
-# - Type-stable access: O(1), but ~1ns (inlined, no overhead)
-# - Immutable update: Allocates new struct, but compiler optimizes away
-# - Dict update: Mutates in-place, but loses type stability
-#
-# Conclusion: For FEM assembly (tight loops, millions of field accesses),
-# type stability dominates. Immutability enables GPU/HPC.
-#
-# ==============================================================================
-
-using BenchmarkTools
-using Statistics
-
-println("="^80)
-println("ELEMENT IMMUTABILITY BENCHMARK")
-println("="^80)
-println()
-println("Comparing two implementations of P2 Lagrange Tetrahedron (Tet10):")
-println(" 1. Mutable element with Dict-based fields (OLD API)")
-println(" 2. Immutable element with NamedTuple fields (NEW API)")
-println()
-println("Measuring: field access, field update, assembly loop")
-println("="^80)
-println()
-
-# ==============================================================================
-# IMPLEMENTATION 1: Mutable Element with Dict-based Fields (OLD)
-# ==============================================================================
-
-"""
-Mutable element: fields stored in Dict{Symbol,Any}
-- Pro: Can add/remove fields dynamically
-- Con: Type-unstable, Dict lookup overhead, no GPU support
-"""
-mutable struct MutableElement
- id::UInt
- connectivity::Vector{UInt}
- fields::Dict{Symbol,Any} # Type-unstable!
-end
-
-function MutableElement(connectivity::Vector{UInt})
- return MutableElement(UInt(0), connectivity, Dict{Symbol,Any}())
-end
-
-# Old-style update: mutate in-place
-function update_field!(elem::MutableElement, field_name::Symbol, value)
- elem.fields[field_name] = value
- return nothing
-end
-
-# Old-style access: Dict lookup
-function get_field(elem::MutableElement, field_name::Symbol)
- return elem.fields[field_name]
-end
-
-# ==============================================================================
-# IMPLEMENTATION 2: Immutable Element with NamedTuple Fields (NEW)
-# ==============================================================================
-
-"""
-Immutable element: fields stored in NamedTuple
-- Pro: Type-stable, zero overhead access, GPU-compatible
-- Con: Cannot mutate, must create new element (but compiler optimizes!)
-"""
-struct ImmutableElement{F}
- id::UInt
- connectivity::NTuple{10,UInt} # Fixed size, stack-allocated
- fields::F # Type-stable! (NamedTuple)
-end
-
-function ImmutableElement(connectivity::NTuple{10,UInt}, fields::NamedTuple)
- return ImmutableElement{typeof(fields)}(UInt(0), connectivity, fields)
-end
-
-# New-style update: return new element (immutable)
-function update_field(elem::ImmutableElement, updates::NamedTuple)
- new_fields = merge(elem.fields, updates)
- return ImmutableElement(elem.connectivity, new_fields)
-end
-
-# New-style access: direct field access (inlined!)
-function get_field(elem::ImmutableElement, field_name::Symbol)
- return getfield(elem.fields, field_name)
-end
-
-# ==============================================================================
-# BENCHMARK 1: Field Access (Read Performance)
-# ==============================================================================
-
-println("BENCHMARK 1: Field Access (Reading E, ν, ρ in tight loop)")
-println("-"^80)
-
-# Setup test elements
-connectivity_vec = UInt.(1:10)
-connectivity_tuple = ntuple(i -> UInt(i), 10)
-
-mutable_elem = MutableElement(connectivity_vec)
-update_field!(mutable_elem, :E, 210e9)
-update_field!(mutable_elem, :ν, 0.3)
-update_field!(mutable_elem, :ρ, 7850.0)
-
-immutable_elem = ImmutableElement(connectivity_tuple, (E=210e9, ν=0.3, ρ=7850.0))
-
-# Benchmark: Read fields 1000 times (simulating assembly loop)
-function read_fields_mutable(elem, n)
- sum_val = 0.0
- for _ in 1:n
- E = get_field(elem, :E)
- ν = get_field(elem, :ν)
- ρ = get_field(elem, :ρ)
- sum_val += E + ν + ρ
- end
- return sum_val
-end
-
-function read_fields_immutable(elem, n)
- sum_val = 0.0
- for _ in 1:n
- E = get_field(elem, :E)
- ν = get_field(elem, :ν)
- ρ = get_field(elem, :ρ)
- sum_val += E + ν + ρ
- end
- return sum_val
-end
-
-n_reads = 1000
-
-println("Reading fields $n_reads times:")
-println()
-
-t_mutable = @benchmark read_fields_mutable($mutable_elem, $n_reads)
-println("Mutable (Dict): ", minimum(t_mutable.times) / n_reads, " ns/read")
-println(" Median: ", median(t_mutable.times) / n_reads, " ns/read")
-println(" Allocs: ", t_mutable.allocs)
-
-t_immutable = @benchmark read_fields_immutable($immutable_elem, $n_reads)
-println("Immutable (Tuple): ", minimum(t_immutable.times) / n_reads, " ns/read")
-println(" Median: ", median(t_immutable.times) / n_reads, " ns/read")
-println(" Allocs: ", t_immutable.allocs)
-
-speedup_read = minimum(t_mutable.times) / minimum(t_immutable.times)
-println()
-println("Speedup: ", round(speedup_read, digits=1), "x faster")
-println()
-
-# ==============================================================================
-# BENCHMARK 2: Field Update (Write Performance)
-# ==============================================================================
-
-println("BENCHMARK 2: Field Update (Updating temperature field)")
-println("-"^80)
-
-# Benchmark: Update temperature field 100 times
-function update_temperature_mutable(elem, n)
- for i in 1:n
- update_field!(elem, :temperature, Float64(i) * 293.15)
- end
- return get_field(elem, :temperature)
-end
-
-function update_temperature_immutable(elem, n)
- current = elem
- for i in 1:n
- current = update_field(current, (temperature=Float64(i) * 293.15,))
- end
- return get_field(current, :temperature)
-end
-
-n_updates = 100
-
-println("Updating temperature field $n_updates times:")
-println()
-
-# Reset elements
-mutable_elem2 = MutableElement(connectivity_vec)
-update_field!(mutable_elem2, :E, 210e9)
-update_field!(mutable_elem2, :ν, 0.3)
-
-immutable_elem2 = ImmutableElement(connectivity_tuple, (E=210e9, ν=0.3))
-
-t_mutable_update = @benchmark update_temperature_mutable($mutable_elem2, $n_updates)
-println("Mutable (mutate): ", minimum(t_mutable_update.times) / n_updates, " ns/update")
-println(" Median: ", median(t_mutable_update.times) / n_updates, " ns/update")
-println(" Allocs: ", t_mutable_update.allocs)
-println(" Memory: ", t_mutable_update.memory, " bytes")
-
-t_immutable_update = @benchmark update_temperature_immutable($immutable_elem2, $n_updates)
-println("Immutable (copy): ", minimum(t_immutable_update.times) / n_updates, " ns/update")
-println(" Median: ", median(t_immutable_update.times) / n_updates, " ns/update")
-println(" Allocs: ", t_immutable_update.allocs)
-println(" Memory: ", t_immutable_update.memory, " bytes")
-
-println()
-println("Note: Immutable creates new structs, but compiler optimizes stack allocation")
-println()
-
-# ==============================================================================
-# BENCHMARK 3: Realistic Assembly Loop (FEM Workload)
-# ==============================================================================
-
-println("BENCHMARK 3: Realistic FEM Assembly Loop")
-println("-"^80)
-println("Simulating element stiffness matrix assembly:")
-println(" - Read E, ν from element fields")
-println(" - Compute 10 Gauss integration points")
-println(" - Each point: read fields, compute B matrix, add to K")
-println()
-
-# Simplified assembly kernel
-function assemble_stiffness_mutable(elem)
- E = get_field(elem, :E)
- ν = get_field(elem, :ν)
-
- # Compute material matrix (simplified)
- λ = E * ν / ((1 + ν) * (1 - 2ν))
- μ = E / (2 * (1 + ν))
-
- K = 0.0
- # Simulate 10 integration points
- for ip in 1:10
- # Simulate field reads at integration point
- E_ip = get_field(elem, :E)
- ν_ip = get_field(elem, :ν)
-
- # Simplified stiffness contribution
- detJ = 1.0 + 0.1 * ip # Fake Jacobian
- weight = 0.1
- K += (λ + 2μ) * detJ * weight
- end
-
- return K
-end
-
-function assemble_stiffness_immutable(elem)
- E = get_field(elem, :E)
- ν = get_field(elem, :ν)
-
- # Compute material matrix (simplified)
- λ = E * ν / ((1 + ν) * (1 - 2ν))
- μ = E / (2 * (1 + ν))
-
- K = 0.0
- # Simulate 10 integration points
- for ip in 1:10
- # Simulate field reads at integration point
- E_ip = get_field(elem, :E)
- ν_ip = get_field(elem, :ν)
-
- # Simplified stiffness contribution
- detJ = 1.0 + 0.1 * ip # Fake Jacobian
- weight = 0.1
- K += (λ + 2μ) * detJ * weight
- end
-
- return K
-end
-
-t_assembly_mutable = @benchmark assemble_stiffness_mutable($mutable_elem)
-t_assembly_immutable = @benchmark assemble_stiffness_immutable($immutable_elem)
-
-println("Assembly time per element:")
-println()
-println("Mutable (Dict): ", minimum(t_assembly_mutable.times), " ns")
-println(" Median: ", median(t_assembly_mutable.times), " ns")
-println(" Allocs: ", t_assembly_mutable.allocs)
-
-println("Immutable (Tuple): ", minimum(t_assembly_immutable.times), " ns")
-println(" Median: ", median(t_assembly_immutable.times), " ns")
-println(" Allocs: ", t_assembly_immutable.allocs)
-
-speedup_assembly = minimum(t_assembly_mutable.times) / minimum(t_assembly_immutable.times)
-println()
-println("Speedup: ", round(speedup_assembly, digits=1), "x faster")
-println()
-
-# ==============================================================================
-# BENCHMARK 4: Large-Scale Mesh (1000 elements)
-# ==============================================================================
-
-println("BENCHMARK 4: Large-Scale Assembly (1000 elements)")
-println("-"^80)
-
-n_elements = 1000
-
-# Create mesh
-mutable_mesh = [
- begin
- elem = MutableElement(UInt.(1:10) .+ UInt(i * 10))
- update_field!(elem, :E, 210e9)
- update_field!(elem, :ν, 0.3)
- elem
- end for i in 1:n_elements
-]
-
-immutable_mesh = [
- begin
- conn = ntuple(j -> UInt(j + i * 10), 10)
- ImmutableElement(conn, (E=210e9, ν=0.3))
- end for i in 1:n_elements
-]
-
-function assemble_mesh_mutable(mesh)
- K_total = 0.0
- for elem in mesh
- K_total += assemble_stiffness_mutable(elem)
- end
- return K_total
-end
-
-function assemble_mesh_immutable(mesh)
- K_total = 0.0
- for elem in mesh
- K_total += assemble_stiffness_immutable(elem)
- end
- return K_total
-end
-
-println("Assembling $n_elements elements:")
-println()
-
-t_mesh_mutable = @benchmark assemble_mesh_mutable($mutable_mesh)
-println("Mutable (Dict): ", minimum(t_mesh_mutable.times) / 1e6, " ms")
-println(" Median: ", median(t_mesh_mutable.times) / 1e6, " ms")
-println(" Allocs: ", t_mesh_mutable.allocs)
-println(" Memory: ", t_mesh_mutable.memory / 1024, " KB")
-
-t_mesh_immutable = @benchmark assemble_mesh_immutable($immutable_mesh)
-println("Immutable (Tuple): ", minimum(t_mesh_immutable.times) / 1e6, " ms")
-println(" Median: ", median(t_mesh_immutable.times) / 1e6, " ms")
-println(" Allocs: ", t_mesh_immutable.allocs)
-println(" Memory: ", t_mesh_immutable.memory / 1024, " KB")
-
-speedup_mesh = minimum(t_mesh_mutable.times) / minimum(t_mesh_immutable.times)
-println()
-println("Speedup: ", round(speedup_mesh, digits=1), "x faster")
-println()
-
-# ==============================================================================
-# SUMMARY
-# ==============================================================================
-
-println("="^80)
-println("SUMMARY")
-println("="^80)
-println()
-println("Key findings:")
-println()
-println("1. Field Access:")
-println(" - Type-stable (immutable) is ", round(speedup_read, digits=1), "x faster")
-println(" - Dict lookup: ~100-200ns overhead per access")
-println(" - NamedTuple: ~1ns (inlined, zero overhead)")
-println()
-println("2. Assembly Performance:")
-println(" - Single element: ", round(speedup_assembly, digits=1), "x faster")
-println(" - Large mesh: ", round(speedup_mesh, digits=1), "x faster")
-println()
-println("3. Memory:")
-println(" - Immutable elements: no allocations in hot path")
-println(" - Mutable elements: Dict overhead + dynamic dispatch")
-println()
-println("4. GPU/HPC Compatibility:")
-println(" - Immutable: ✓ All bits types, can transfer to GPU")
-println(" - Mutable: ✗ Pointers, heap allocations, no GPU support")
-println()
-println("CONCLUSION:")
-println("-"^80)
-println("Despite appearing counterintuitive, IMMUTABLE elements with type-stable")
-println("fields are SIGNIFICANTLY FASTER for FEM assembly. The key insight:")
-println()
-println(" • Dict lookup cost dominates in tight loops (millions of accesses)")
-println(" • Type stability enables compiler optimizations (inlining, SIMD)")
-println(" • Immutability enables GPU/HPC parallelization (no race conditions)")
-println(" • Modern compilers optimize away struct copies on stack")
-println()
-println("For FEM with millions of field accesses per assembly, type stability")
-println("is the critical factor. Immutability is a small price for 10-100x speedup.")
-println()
-println("="^80)
diff --git a/benchmarks/field_storage_comparison.jl b/benchmarks/field_storage_comparison.jl
deleted file mode 100755
index c7df4c1..0000000
--- a/benchmarks/field_storage_comparison.jl
+++ /dev/null
@@ -1,334 +0,0 @@
-#!/usr/bin/env julia
-#
-# Benchmark: Dict{String,Any} vs Type-Stable Field Storage
-#
-# This benchmark validates the performance claims in:
-# docs/book/zero_allocation_fields.md
-#
-# Expected results:
-# - Constant field access: 50× faster, 0 allocations
-# - Nodal field access: 50× faster, 0 allocations
-# - Interpolation: 16× faster with zero allocations (cached)
-#
-
-using BenchmarkTools
-using LinearAlgebra
-
-# ============================================================================
-# Field Type Definitions (from proposal)
-# ============================================================================
-
-abstract type AbstractField{T} end
-
-struct ConstantField{T} <: AbstractField{T}
- value::T
-end
-
-struct NodalField{T} <: AbstractField{T}
- values::Matrix{T} # N_components × N_nodes
-end
-
-# Accessors
-@inline value(f::ConstantField) = f.value
-@inline function value(f::NodalField, node_ids::AbstractVector{Int})
- return @view f.values[:, node_ids]
-end
-
-# ============================================================================
-# Mock Element (simplified for benchmarking)
-# ============================================================================
-
-struct MockElement
- connectivity::Vector{Int}
-end
-
-function mock_eval_basis(x::Vector{Float64})
- # Mock basis function values for 8-node element
- return [0.1, 0.15, 0.05, 0.1, 0.2, 0.15, 0.15, 0.1]
-end
-
-# ============================================================================
-# Setup: OLD (Dict-based) vs NEW (Typed)
-# ============================================================================
-
-const OLD_FIELDS = Dict{String,Any}(
- "youngs_modulus" => 210e3,
- "poissons_ratio" => 0.3,
- "displacement" => zeros(3, 8),
-)
-
-const NEW_FIELDS = (
- youngs_modulus=ConstantField(210e3),
- poissons_ratio=ConstantField(0.3),
- displacement=NodalField(zeros(3, 8)),
-)
-
-# ============================================================================
-# Benchmark 1: Constant Field Access
-# ============================================================================
-
-println("="^70)
-println("Benchmark 1: Constant Field Access")
-println("="^70)
-
-println("\nOLD (Dict{String,Any}):")
-old_constant = @benchmark $OLD_FIELDS["youngs_modulus"]
-display(old_constant)
-
-println("\nNEW (ConstantField):")
-new_constant = @benchmark value($NEW_FIELDS.youngs_modulus)
-display(new_constant)
-
-old_time_1 = median(old_constant).time
-new_time_1 = median(new_constant).time
-speedup_1 = old_time_1 / new_time_1
-allocs_old_1 = median(old_constant).allocs
-allocs_new_1 = median(new_constant).allocs
-
-println("\n📊 Results:")
-println(" OLD: $(round(old_time_1, digits=1)) ns, $(allocs_old_1) allocations")
-println(" NEW: $(round(new_time_1, digits=1)) ns, $(allocs_new_1) allocations")
-println(" Speedup: $(round(speedup_1, digits=1))×")
-println(" Allocation reduction: $(allocs_old_1 - allocs_new_1)")
-
-# ============================================================================
-# Benchmark 2: Nodal Field Access
-# ============================================================================
-
-println("\n" * "="^70)
-println("Benchmark 2: Nodal Field Access (4 nodes)")
-println("="^70)
-
-node_ids = [1, 2, 3, 4]
-
-println("\nOLD (Dict with Array slicing):")
-old_nodal = @benchmark $OLD_FIELDS["displacement"][:, $node_ids]
-display(old_nodal)
-
-println("\nNEW (NodalField with @view):")
-new_nodal = @benchmark value($NEW_FIELDS.displacement, $node_ids)
-display(new_nodal)
-
-old_time_2 = median(old_nodal).time
-new_time_2 = median(new_nodal).time
-speedup_2 = old_time_2 / new_time_2
-allocs_old_2 = median(old_nodal).allocs
-allocs_new_2 = median(new_nodal).allocs
-
-println("\n📊 Results:")
-println(" OLD: $(round(old_time_2, digits=1)) ns, $(allocs_old_2) allocations")
-println(" NEW: $(round(new_time_2, digits=1)) ns, $(allocs_new_2) allocations")
-println(" Speedup: $(round(speedup_2, digits=1))×")
-println(" Allocation reduction: $(allocs_old_2 - allocs_new_2)")
-
-# ============================================================================
-# Benchmark 3: Interpolation (Without Cache)
-# ============================================================================
-
-println("\n" * "="^70)
-println("Benchmark 3: Spatial Interpolation (No Cache)")
-println("="^70)
-
-element = MockElement([1, 2, 3, 4, 5, 6, 7, 8])
-x = [0.1, 0.2, 0.3]
-N = mock_eval_basis(x)
-
-function interpolate_old(element, N, fields_dict)
- u = fields_dict["displacement"] # Type: Any
- result = zeros(3)
- for i in 1:length(N)
- result .+= N[i] .* u[:, element.connectivity[i]]
- end
- return result
-end
-
-function interpolate_new(element, N, fields)
- u_nodal = value(fields.displacement, element.connectivity)
- result = zeros(3)
- for i in 1:length(N)
- result .+= N[i] .* @view u_nodal[:, i]
- end
- return result
-end
-
-println("\nOLD (Dict-based):")
-old_interp = @benchmark interpolate_old($element, $N, $OLD_FIELDS)
-display(old_interp)
-
-println("\nNEW (Typed fields):")
-new_interp = @benchmark interpolate_new($element, $N, $NEW_FIELDS)
-display(new_interp)
-
-old_time_3 = median(old_interp).time
-new_time_3 = median(new_interp).time
-speedup_3 = old_time_3 / new_time_3
-allocs_old_3 = median(old_interp).allocs
-allocs_new_3 = median(new_interp).allocs
-
-println("\n📊 Results:")
-println(" OLD: $(round(old_time_3/1000, digits=1)) μs, $(allocs_old_3) allocations")
-println(" NEW: $(round(new_time_3, digits=1)) ns, $(allocs_new_3) allocations")
-println(" Speedup: $(round(speedup_3, digits=1))×")
-println(" Allocation reduction: $(allocs_old_3 - allocs_new_3)")
-
-# ============================================================================
-# Benchmark 4: Interpolation (With Cache - Zero Allocation Target)
-# ============================================================================
-
-println("\n" * "="^70)
-println("Benchmark 4: Spatial Interpolation (WITH Cache)")
-println("="^70)
-
-struct InterpolationCache
- result::Vector{Float64}
-end
-
-function interpolate_cached!(cache, element, N, fields)
- u_nodal = value(fields.displacement, element.connectivity)
- fill!(cache.result, 0.0)
- for i in eachindex(N)
- cache.result .+= N[i] .* @view u_nodal[:, i]
- end
- return cache.result
-end
-
-cache = InterpolationCache(zeros(3))
-
-println("\nNEW (Cached - Zero Allocation Target):")
-cached_interp = @benchmark interpolate_cached!($cache, $element, $N, $NEW_FIELDS)
-display(cached_interp)
-
-cached_time = median(cached_interp).time
-cached_allocs = median(cached_interp).allocs
-speedup_4 = old_time_3 / cached_time
-
-println("\n📊 Results:")
-println(" Cached: $(round(cached_time, digits=1)) ns, $(cached_allocs) allocations")
-println(" Speedup vs OLD: $(round(speedup_4, digits=1))×")
-println(" Zero allocation target: $(cached_allocs == 0 ? "✅ MET" : "❌ FAILED")")
-
-# ============================================================================
-# Benchmark 5: Assembly Loop (1000 elements)
-# ============================================================================
-
-println("\n" * "="^70)
-println("Benchmark 5: Assembly Loop (1000 elements)")
-println("="^70)
-
-n_elements = 1000
-elements = [MockElement(collect(1:8)) for _ in 1:n_elements]
-
-function assemble_old_style(elements, fields_dict)
- total = 0.0
- for element in elements
- E = fields_dict["youngs_modulus"] # Type-unstable access
- ν = fields_dict["poissons_ratio"]
-
- # Mock stiffness computation
- K_local = E * (1 - ν^2)
- total += K_local
- end
- return total
-end
-
-function assemble_new_style(elements, fields)
- E = value(fields.youngs_modulus) # Type-stable access (once)
- ν = value(fields.poissons_ratio)
-
- total = 0.0
- for element in elements
- # Mock stiffness computation
- K_local = E * (1 - ν^2)
- total += K_local
- end
- return total
-end
-
-println("\nOLD (Dict access in loop):")
-old_assembly = @benchmark assemble_old_style($elements, $OLD_FIELDS)
-display(old_assembly)
-
-println("\nNEW (Typed fields, hoisted access):")
-new_assembly = @benchmark assemble_new_style($elements, $NEW_FIELDS)
-display(new_assembly)
-
-old_time_5 = median(old_assembly).time
-new_time_5 = median(new_assembly).time
-speedup_5 = old_time_5 / new_time_5
-allocs_old_5 = median(old_assembly).allocs
-allocs_new_5 = median(new_assembly).allocs
-
-println("\n📊 Results:")
-println(" OLD: $(round(old_time_5/1000, digits=1)) μs, $(allocs_old_5) allocations")
-println(" NEW: $(round(new_time_5/1000, digits=1)) μs, $(allocs_new_5) allocations")
-println(" Speedup: $(round(speedup_5, digits=1))×")
-println(" Allocation reduction: $(allocs_old_5 - allocs_new_5)")
-
-# ============================================================================
-# Summary and Validation
-# ============================================================================
-
-println("\n" * "="^70)
-println("SUMMARY - Validation Against Claims")
-println("="^70)
-
-validation_passed = true
-
-# Claim 1: Constant field access should be ~50× faster, 0 allocations
-println("\n1. Constant Field Access:")
-println(" Claimed: ~50× faster, 0 allocations")
-println(" Actual: $(round(speedup_1, digits=1))× faster, $(allocs_new_1) allocations")
-if speedup_1 >= 10 && allocs_new_1 == 0
- println(" Status: ✅ VALIDATED ($(round(speedup_1, digits=1))× > 10× threshold)")
-else
- println(" Status: ⚠️ PARTIAL (speedup or allocation target not met)")
- validation_passed = false
-end
-
-# Claim 2: Nodal field access should be ~50× faster, 0 allocations
-println("\n2. Nodal Field Access:")
-println(" Claimed: ~50× faster, 0 allocations")
-println(" Actual: $(round(speedup_2, digits=1))× faster, $(allocs_new_2) allocations")
-if speedup_2 >= 10 && allocs_new_2 == 0
- println(" Status: ✅ VALIDATED ($(round(speedup_2, digits=1))× > 10× threshold)")
-else
- println(" Status: ⚠️ PARTIAL (speedup or allocation target not met)")
- validation_passed = false
-end
-
-# Claim 3: Cached interpolation should be ~16× faster, 0 allocations
-println("\n3. Interpolation (Cached):")
-println(" Claimed: ~16× faster, 0 allocations")
-println(" Actual: $(round(speedup_4, digits=1))× faster, $(cached_allocs) allocations")
-if speedup_4 >= 10 && cached_allocs == 0
- println(" Status: ✅ VALIDATED ($(round(speedup_4, digits=1))× > 10× threshold)")
-else
- println(" Status: ⚠️ PARTIAL (speedup or allocation target not met)")
- validation_passed = false
-end
-
-# Claim 4: Assembly should be 10-100× faster
-println("\n4. Assembly Loop:")
-println(" Claimed: 10-100× faster")
-println(" Actual: $(round(speedup_5, digits=1))× faster")
-if speedup_5 >= 10
- println(" Status: ✅ VALIDATED ($(round(speedup_5, digits=1))× > 10× threshold)")
-else
- println(" Status: ⚠️ PARTIAL (speedup target not met)")
- validation_passed = false
-end
-
-println("\n" * "="^70)
-if validation_passed
- println("✅ ALL PERFORMANCE CLAIMS VALIDATED")
-else
- println("⚠️ SOME CLAIMS NOT FULLY VALIDATED (but likely still significant improvement)")
-end
-println("="^70)
-
-println("\nKey Insights:")
-println(" • Type stability (NamedTuple) eliminates runtime dispatch")
-println(" • Zero allocations achieved with @view and pre-allocated caches")
-println(" • Hoisting invariant access out of loops provides massive speedup")
-println(" • The combination gives 10-100× speedup in realistic scenarios")
-println("\n✅ This validates the NamedTuple + typed fields design for v1.0")
diff --git a/benchmarks/gpu_state_management_benchmark.jl b/benchmarks/gpu_state_management_benchmark.jl
deleted file mode 100644
index 4eac1a6..0000000
--- a/benchmarks/gpu_state_management_benchmark.jl
+++ /dev/null
@@ -1,468 +0,0 @@
-"""
-GPU State Management Benchmark
-
-Demonstrates Strategy 1 (immutable elements) vs Strategy 2 (separate mutable state)
-and validates memory coalescing patterns on actual GPU hardware.
-
-Run with:
- julia --project=. benchmarks/gpu_state_management_benchmark.jl
-"""
-
-using CUDA
-using Tensors
-using BenchmarkTools
-using Printf
-
-# Check GPU availability
-if !CUDA.functional()
- error("CUDA not available! This benchmark requires a CUDA-capable GPU.")
-end
-
-println("GPU Device: $(CUDA.device())")
-println("GPU Memory: $(CUDA.name(CUDA.device())) - $(round(CUDA.total_memory()/1e9, digits=1)) GB")
-println()
-
-# ============================================================================
-# Strategy 1: Immutable Elements (Array of Structs - AoS)
-# ============================================================================
-
-"""
-Strategy 1: Element contains its own state (immutable).
-Update creates new element (allocation + copy).
-"""
-struct Element_Strategy1{T}
- connectivity::NTuple{8,Int32}
- material_id::Int32
- # State (plastic strain, hardening)
- ε_p::SymmetricTensor{2,3,T,6}
- α::T
-end
-
-"""
-Update state for Strategy 1 (returns new element - allocation!).
-"""
-function update_element_strategy1(elem::Element_Strategy1{T}, Δε_p, Δα) where T
- return Element_Strategy1(
- elem.connectivity,
- elem.material_id,
- elem.ε_p + Δε_p,
- elem.α + Δα
- )
-end
-
-"""
-CPU kernel: Update all elements (Strategy 1).
-"""
-function update_elements_strategy1_cpu!(
- elements::Vector{Element_Strategy1{T}},
- strain_increments::Vector{SymmetricTensor{2,3,T,6}},
- hardening_increments::Vector{T}
-) where T
- n = length(elements)
- for i in 1:n
- elements[i] = update_element_strategy1(
- elements[i],
- strain_increments[i],
- hardening_increments[i]
- )
- end
-end
-
-"""
-GPU kernel: Update all elements (Strategy 1).
-
-Problem: Each thread accesses scattered memory (pointer chasing).
-"""
-function update_elements_strategy1_kernel!(
- elements::CuDeviceVector{Element_Strategy1{T}},
- strain_increments::CuDeviceVector{SymmetricTensor{2,3,T,6}},
- hardening_increments::CuDeviceVector{T}
-) where T
- i = (blockIdx().x - 1) * blockDim().x + threadIdx().x
-
- if i <= length(elements)
- elem = elements[i] # Non-coalesced read!
- Δε_p = strain_increments[i]
- Δα = hardening_increments[i]
-
- # Update (creates new element - allocation on GPU!)
- new_elem = Element_Strategy1(
- elem.connectivity,
- elem.material_id,
- elem.ε_p + Δε_p,
- elem.α + Δα
- )
-
- elements[i] = new_elem # Non-coalesced write!
- end
-
- return nothing
-end
-
-function update_elements_strategy1_gpu!(
- elements::CuVector{Element_Strategy1{T}},
- strain_increments::CuVector{SymmetricTensor{2,3,T,6}},
- hardening_increments::CuVector{T}
-) where T
- n = length(elements)
- threads = 256
- blocks = cld(n, threads)
-
- @cuda threads = threads blocks = blocks update_elements_strategy1_kernel!(
- elements, strain_increments, hardening_increments
- )
- CUDA.synchronize()
-end
-
-# ============================================================================
-# Strategy 2: Separate Mutable State (Structure of Arrays - SoA)
-# ============================================================================
-
-"""
-Strategy 2: Geometry is immutable, state is separate and mutable.
-"""
-struct ElementGeometry
- connectivity::NTuple{8,Int32}
- material_id::Int32
-end
-
-"""
-Mutable state storage (flat arrays for GPU coalescing).
-"""
-mutable struct AssemblyState{T,VecT}
- # Plastic strain (Voigt notation: 6 components per state)
- ε_p_flat::VecT # [N_states × 6]
-
- # Hardening variable (1 component per state)
- α_flat::VecT # [N_states]
-
- n_states::Int
-end
-
-function AssemblyState{T}(n_states::Int) where T
- return AssemblyState{T,Vector{T}}(
- zeros(T, n_states * 6),
- zeros(T, n_states),
- n_states
- )
-end
-
-"""
-CPU kernel: Update state (Strategy 2 - in-place!).
-"""
-function update_state_strategy2_cpu!(
- state::AssemblyState{T,Vector{T}},
- strain_increments::Vector{SymmetricTensor{2,3,T,6}},
- hardening_increments::Vector{T}
-) where T
- n = state.n_states
-
- for i in 1:n
- # Flat indexing (cache-friendly!)
- offset = (i - 1) * 6
-
- Δε_p = strain_increments[i]
-
- # Update in-place (no allocation!)
- state.ε_p_flat[offset+1] += Δε_p[1, 1]
- state.ε_p_flat[offset+2] += Δε_p[2, 2]
- state.ε_p_flat[offset+3] += Δε_p[3, 3]
- state.ε_p_flat[offset+4] += Δε_p[1, 2]
- state.ε_p_flat[offset+5] += Δε_p[1, 3]
- state.ε_p_flat[offset+6] += Δε_p[2, 3]
-
- state.α_flat[i] += hardening_increments[i]
- end
-end
-
-"""
-GPU kernel: Update state (Strategy 2).
-
-Advantage: Coalesced memory access!
-- Thread 0 accesses state.ε_p_flat[0:5]
-- Thread 1 accesses state.ε_p_flat[6:11]
-- Thread 2 accesses state.ε_p_flat[12:17]
-All consecutive in memory!
-"""
-function update_state_strategy2_kernel!(
- ε_p_flat::CuDeviceVector{T},
- α_flat::CuDeviceVector{T},
- strain_increments_flat::CuDeviceVector{T},
- hardening_increments::CuDeviceVector{T},
- n_states::Int
-) where T
- i = (blockIdx().x - 1) * blockDim().x + threadIdx().x
-
- if i <= n_states
- # Flat indexing (coalesced access!)
- offset = (i - 1) * 6
- strain_offset = (i - 1) * 6
-
- # Update plastic strain (6 consecutive reads/writes)
- ε_p_flat[offset+1] += strain_increments_flat[strain_offset+1]
- ε_p_flat[offset+2] += strain_increments_flat[strain_offset+2]
- ε_p_flat[offset+3] += strain_increments_flat[strain_offset+3]
- ε_p_flat[offset+4] += strain_increments_flat[strain_offset+4]
- ε_p_flat[offset+5] += strain_increments_flat[strain_offset+5]
- ε_p_flat[offset+6] += strain_increments_flat[strain_offset+6]
-
- # Update hardening (1 read/write)
- α_flat[i] += hardening_increments[i]
- end
-
- return nothing
-end
-
-function update_state_strategy2_gpu!(
- state_gpu::AssemblyState{T,<:CuVector{T}},
- strain_increments_flat::CuVector{T},
- hardening_increments::CuVector{T}
-) where T
- n = state_gpu.n_states
- threads = 256
- blocks = cld(n, threads)
-
- @cuda threads = threads blocks = blocks update_state_strategy2_kernel!(
- state_gpu.ε_p_flat,
- state_gpu.α_flat,
- strain_increments_flat,
- hardening_increments,
- n
- )
- CUDA.synchronize()
-end
-
-# ============================================================================
-# Benchmark Setup
-# ============================================================================
-
-function setup_benchmark(n_elements::Int)
- T = Float64
-
- # Create random strain increments
- Δε_p_tensors = [SymmetricTensor{2,3}((
- rand(T) * 1e-5,
- rand(T) * 1e-5,
- rand(T) * 1e-5,
- rand(T) * 1e-6,
- rand(T) * 1e-6,
- rand(T) * 1e-6
- )) for _ in 1:n_elements]
-
- Δα = rand(T, n_elements) .* 1e-5
-
- # Strategy 1: Array of immutable elements
- elements_s1 = [Element_Strategy1(
- ntuple(j -> Int32(j), 8),
- Int32(1),
- zero(SymmetricTensor{2,3,T}),
- zero(T)
- ) for _ in 1:n_elements]
-
- # Strategy 2: Separate geometry and state
- geometry_s2 = [ElementGeometry(
- ntuple(j -> Int32(j), 8),
- Int32(1)
- ) for _ in 1:n_elements]
-
- state_s2 = AssemblyState{T}(n_elements)
-
- return Δε_p_tensors, Δα, elements_s1, geometry_s2, state_s2
-end
-
-# ============================================================================
-# CPU Benchmarks
-# ============================================================================
-
-function benchmark_cpu(n_elements::Int)
- println("="^70)
- println("CPU Benchmark: $n_elements elements")
- println("="^70)
-
- Δε_p, Δα, elements_s1, geometry_s2, state_s2 = setup_benchmark(n_elements)
-
- # Strategy 1: Update immutable elements
- println("\n📊 Strategy 1 (Immutable Elements - AoS):")
- elements_s1_copy = copy(elements_s1)
- t1 = @belapsed update_elements_strategy1_cpu!(
- $elements_s1_copy, $Δε_p, $Δα
- ) samples = 10
-
- println(" Time: $(round(t1 * 1000, digits=3)) ms")
- println(" Bandwidth: N/A (CPU cache)")
-
- # Check allocations
- allocs = @allocated update_elements_strategy1_cpu!(elements_s1_copy, Δε_p, Δα)
- println(" Allocations: $(allocs) bytes ($(allocs ÷ n_elements) bytes/element)")
-
- # Strategy 2: Update mutable state
- println("\n📊 Strategy 2 (Separate State - SoA):")
- state_s2_copy = deepcopy(state_s2)
- t2 = @belapsed update_state_strategy2_cpu!(
- $state_s2_copy, $Δε_p, $Δα
- ) samples = 10
-
- println(" Time: $(round(t2 * 1000, digits=3)) ms")
- println(" Bandwidth: N/A (CPU cache)")
-
- # Check allocations
- allocs2 = @allocated update_state_strategy2_cpu!(state_s2_copy, Δε_p, Δα)
- println(" Allocations: $(allocs2) bytes")
-
- # Speedup
- speedup = t1 / t2
- println("\n✅ CPU Speedup (Strategy 2 / Strategy 1): $(round(speedup, digits=2))×")
-
- println()
-end
-
-# ============================================================================
-# GPU Benchmarks
-# ============================================================================
-
-function benchmark_gpu(n_elements::Int)
- println("="^70)
- println("GPU Benchmark: $n_elements elements")
- println("="^70)
-
- T = Float64
- Δε_p, Δα, elements_s1, geometry_s2, state_s2 = setup_benchmark(n_elements)
-
- # ========================================================================
- # Strategy 1: GPU
- # ========================================================================
- println("\n📊 Strategy 1 (Immutable Elements - AoS on GPU):")
-
- # Transfer to GPU
- elements_s1_gpu = CuArray(elements_s1)
- Δε_p_gpu = CuArray(Δε_p)
- Δα_gpu = CuArray(Δα)
-
- # Warmup
- update_elements_strategy1_gpu!(elements_s1_gpu, Δε_p_gpu, Δα_gpu)
-
- # Benchmark
- t1_gpu = CUDA.@elapsed begin
- update_elements_strategy1_gpu!(elements_s1_gpu, Δε_p_gpu, Δα_gpu)
- end
-
- println(" Time: $(round(t1_gpu * 1000, digits=3)) ms")
-
- # Estimate bandwidth (reading + writing entire element)
- bytes_per_elem = sizeof(Element_Strategy1{T})
- total_bytes = bytes_per_elem * n_elements * 2 # Read + write
- bandwidth_s1 = total_bytes / t1_gpu / 1e9
- println(" Bandwidth: $(round(bandwidth_s1, digits=1)) GB/s")
-
- # ========================================================================
- # Strategy 2: GPU
- # ========================================================================
- println("\n📊 Strategy 2 (Separate State - SoA on GPU):")
-
- # Transfer to GPU (flat arrays!)
- state_s2_gpu = AssemblyState{T,CuVector{T}}(
- CuArray(state_s2.ε_p_flat),
- CuArray(state_s2.α_flat),
- state_s2.n_states
- )
-
- # Flatten strain increments for GPU
- Δε_p_flat = zeros(T, n_elements * 6)
- for i in 1:n_elements
- offset = (i - 1) * 6
- ε = Δε_p[i]
- Δε_p_flat[offset+1] = ε[1, 1]
- Δε_p_flat[offset+2] = ε[2, 2]
- Δε_p_flat[offset+3] = ε[3, 3]
- Δε_p_flat[offset+4] = ε[1, 2]
- Δε_p_flat[offset+5] = ε[1, 3]
- Δε_p_flat[offset+6] = ε[2, 3]
- end
-
- Δε_p_flat_gpu = CuArray(Δε_p_flat)
- Δα_flat_gpu = CuArray(Δα)
-
- # Warmup
- update_state_strategy2_gpu!(state_s2_gpu, Δε_p_flat_gpu, Δα_flat_gpu)
-
- # Benchmark
- t2_gpu = CUDA.@elapsed begin
- update_state_strategy2_gpu!(state_s2_gpu, Δε_p_flat_gpu, Δα_flat_gpu)
- end
-
- println(" Time: $(round(t2_gpu * 1000, digits=3)) ms")
-
- # Estimate bandwidth (only state data, not geometry!)
- bytes_per_state = 6 * sizeof(T) + sizeof(T) # 6 strain + 1 hardening
- total_bytes_s2 = bytes_per_state * n_elements * 2 # Read + write
- bandwidth_s2 = total_bytes_s2 / t2_gpu / 1e9
- println(" Bandwidth: $(round(bandwidth_s2, digits=1)) GB/s")
-
- # ========================================================================
- # Comparison
- # ========================================================================
- speedup = t1_gpu / t2_gpu
- bandwidth_ratio = bandwidth_s2 / bandwidth_s1
-
- println("\n✅ GPU Speedup (Strategy 2 / Strategy 1): $(round(speedup, digits=2))×")
- println("✅ Bandwidth Improvement: $(round(bandwidth_ratio, digits=2))×")
- println(" Strategy 1: $(round(bandwidth_s1, digits=1)) GB/s (non-coalesced)")
- println(" Strategy 2: $(round(bandwidth_s2, digits=1)) GB/s (coalesced)")
-
- # Theoretical peak (example: RTX 4090 = ~1000 GB/s)
- gpu_name = CUDA.name(CUDA.device())
- println("\n💡 GPU Memory Bandwidth:")
- println(" Achieved: $(round(bandwidth_s2, digits=1)) GB/s")
- println(" Device: $gpu_name")
-
- println()
-
- # Cleanup
- CUDA.unsafe_free!(elements_s1_gpu)
- CUDA.unsafe_free!(Δε_p_gpu)
- CUDA.unsafe_free!(Δα_gpu)
- CUDA.unsafe_free!(state_s2_gpu.ε_p_flat)
- CUDA.unsafe_free!(state_s2_gpu.α_flat)
- CUDA.unsafe_free!(Δε_p_flat_gpu)
- CUDA.unsafe_free!(Δα_flat_gpu)
-end
-
-# ============================================================================
-# Main Benchmark
-# ============================================================================
-
-function main()
- println("\n" * "=" * 70)
- println("GPU State Management Strategy Benchmark")
- println("=" * 70)
- println()
-
- # Test sizes
- sizes = [10_000, 100_000, 1_000_000]
-
- for n in sizes
- # CPU benchmark
- benchmark_cpu(n)
-
- # GPU benchmark
- benchmark_gpu(n)
-
- println()
- end
-
- println("="^70)
- println("Benchmark Complete!")
- println("="^70)
- println()
- println("Key Findings:")
- println(" - Strategy 1 (AoS): Non-coalesced memory access on GPU")
- println(" - Strategy 2 (SoA): Coalesced memory access on GPU")
- println(" - Strategy 2 achieves 5-10× higher memory bandwidth")
- println(" - Strategy 2 has zero allocations (in-place update)")
- println()
-end
-
-# Run benchmark
-if abspath(PROGRAM_FILE) == @__FILE__
- main()
-end
diff --git a/benchmarks/integration_points_benchmark.jl b/benchmarks/integration_points_benchmark.jl
deleted file mode 100644
index 3898e51..0000000
--- a/benchmarks/integration_points_benchmark.jl
+++ /dev/null
@@ -1,301 +0,0 @@
-# Benchmark: Integration Point Access Patterns
-# ============================================
-#
-# This benchmark compares different approaches to storing and accessing
-# integration points in finite element assembly loops.
-#
-# Key Question: What's the fastest way to get integration points?
-#
-# Approaches tested:
-# 1. OLD: Runtime dispatch + mutable struct with Dict (type-unstable)
-# 2. NEW Option A: Compile-time function (like eval_basis!)
-# 3. NEW Option B: Store in element as NTuple
-# 4. NEW Option C: Hybrid (compile-time generation + caching)
-
-using BenchmarkTools
-using Tensors
-using StaticArrays
-
-# ============================================================================
-# OLD APPROACH: Runtime dispatch with mutable struct
-# ============================================================================
-
-struct OldIP
- id::UInt
- weight::Float64
- coords::Tuple{Vararg{Float64}}
- fields::Dict{String,Any} # Type-unstable!
-end
-
-function get_integration_points_old(::Type{Val{:Tri3}})
- # Simulate runtime dispatch to get IPs
- return [
- OldIP(UInt(1), 0.5, (1 / 3, 1 / 3), Dict{String,Any}()),
- ]
-end
-
-function assembly_loop_old()
- sum_val = 0.0
- for _ in 1:1000 # Simulate 1000 elements
- ips = get_integration_points_old(Val{:Tri3})
- for ip in ips
- w = ip.weight
- xi, eta = ip.coords
- # Simulate some computation
- sum_val += w * (xi + eta)
- end
- end
- return sum_val
-end
-
-# ============================================================================
-# NEW OPTION A: Compile-time function (zero allocation)
-# ============================================================================
-
-"""
-Return integration points as tuple at compile time.
-Similar to eval_basis! - zero allocation, fully inlined.
-"""
-@inline function get_integration_points!(::Type{Val{:Tri3_Gauss1}})
- # Return as tuple of (weight, coords) pairs
- return (
- (0.5, (1 / 3, 1 / 3)),
- )
-end
-
-@inline function get_integration_points!(::Type{Val{:Tet4_Gauss1}})
- return (
- (1 / 24, (0.25, 0.25, 0.25)),
- )
-end
-
-function assembly_loop_option_a()
- sum_val = 0.0
- for _ in 1:1000
- ips = get_integration_points!(Val{:Tri3_Gauss1})
- for (w, (xi, eta)) in ips
- sum_val += w * (xi + eta)
- end
- end
- return sum_val
-end
-
-# ============================================================================
-# NEW OPTION B: Store in element as NTuple (what we have now)
-# ============================================================================
-
-struct IntegrationPoint{D}
- ξ::NTuple{D,Float64}
- weight::Float64
-end
-
-struct MockElement{NIP}
- ips::NTuple{NIP,IntegrationPoint{2}}
-end
-
-function create_element_b()
- ips = (
- IntegrationPoint((1 / 3, 1 / 3), 0.5),
- )
- return MockElement(ips)
-end
-
-function assembly_loop_option_b()
- elements = [create_element_b() for _ in 1:1000]
- sum_val = 0.0
- for element in elements
- for ip in element.ips
- w = ip.weight
- xi, eta = ip.ξ
- sum_val += w * (xi + eta)
- end
- end
- return sum_val
-end
-
-# ============================================================================
-# NEW OPTION C: Compile-time with Tensors.jl Vec (recommended for FEM)
-# ============================================================================
-
-"""
-Return integration points with Vec{D} coordinates (Tensors.jl).
-This matches the golden standard from nodal assembly demos.
-"""
-@inline function get_integration_points_vec!(::Type{Val{:Tri3_Gauss1}})
- return (
- (0.5, Vec{2}((1 / 3, 1 / 3))),
- )
-end
-
-@inline function get_integration_points_vec!(::Type{Val{:Tet4_Gauss1}})
- return (
- (1 / 24, Vec{3}((0.25, 0.25, 0.25))),
- )
-end
-
-function assembly_loop_option_c()
- sum_val = 0.0
- for _ in 1:1000
- ips = get_integration_points_vec!(Val{:Tri3_Gauss1})
- for (w, xi) in ips
- # Vec arithmetic is optimized by Tensors.jl
- sum_val += w * sum(xi)
- end
- end
- return sum_val
-end
-
-# ============================================================================
-# NEW OPTION D: Pre-generated global constants (ultimate zero-cost)
-# ============================================================================
-
-const TRI3_GAUSS1_IPS = (
- (0.5, Vec{2}((1 / 3, 1 / 3))),
-)
-
-const TET4_GAUSS1_IPS = (
- (1 / 24, Vec{3}((0.25, 0.25, 0.25))),
-)
-
-function assembly_loop_option_d()
- sum_val = 0.0
- for _ in 1:1000
- for (w, xi) in TRI3_GAUSS1_IPS
- sum_val += w * sum(xi)
- end
- end
- return sum_val
-end
-
-# ============================================================================
-# OPTION E: Hybrid - Function returns pre-computed constant
-# ============================================================================
-
-@inline get_ips_tri3_gauss1() = TRI3_GAUSS1_IPS
-@inline get_ips_tet4_gauss1() = TET4_GAUSS1_IPS
-
-function assembly_loop_option_e()
- sum_val = 0.0
- for _ in 1:1000
- for (w, xi) in get_ips_tri3_gauss1()
- sum_val += w * sum(xi)
- end
- end
- return sum_val
-end
-
-# ============================================================================
-# Run Benchmarks
-# ============================================================================
-
-println("="^80)
-println("Integration Point Access Pattern Benchmark")
-println("="^80)
-println()
-
-println("OLD APPROACH: Runtime dispatch + mutable struct with Dict")
-println("-"^80)
-@btime assembly_loop_old()
-println()
-
-println("OPTION A: Compile-time function returning tuples")
-println("-"^80)
-@btime assembly_loop_option_a()
-println()
-
-println("OPTION B: Store in element as NTuple (current approach)")
-println("-"^80)
-@btime assembly_loop_option_b()
-println()
-
-println("OPTION C: Compile-time function with Vec{D} (Tensors.jl)")
-println("-"^80)
-@btime assembly_loop_option_c()
-println()
-
-println("OPTION D: Pre-generated global constants")
-println("-"^80)
-@btime assembly_loop_option_d()
-println()
-
-println("OPTION E: Function returning pre-computed constant")
-println("-"^80)
-@btime assembly_loop_option_e()
-println()
-
-# ============================================================================
-# Realistic FEM Assembly Benchmark
-# ============================================================================
-
-println()
-println("="^80)
-println("REALISTIC FEM ASSEMBLY COMPARISON")
-println("="^80)
-println()
-
-# Simulate realistic element stiffness computation
-function compute_element_stiffness_old(element_type::Type{Val{:Tri3}})
- K_elem = zeros(6, 6)
- ips = get_integration_points_old(element_type)
- for ip in ips
- w = ip.weight
- xi, eta = ip.coords
- # Simulate shape function evaluation and stiffness computation
- N1 = 1 - xi - eta
- N2 = xi
- N3 = eta
- # Accumulate (simplified)
- K_elem[1, 1] += w * (N1^2)
- end
- return K_elem[1, 1]
-end
-
-function compute_element_stiffness_new()
- K_elem = 0.0
- for (w, xi) in get_integration_points_vec!(Val{:Tri3_Gauss1})
- # Vec arithmetic
- xi_val = xi[1]
- eta_val = xi[2]
- N1 = 1 - xi_val - eta_val
- K_elem += w * (N1^2)
- end
- return K_elem
-end
-
-println("OLD: Realistic element stiffness assembly")
-@btime begin
- sum_val = 0.0
- for _ in 1:1000
- sum_val += compute_element_stiffness_old(Val{:Tri3})
- end
- sum_val
-end
-
-println()
-println("NEW: Realistic element stiffness assembly")
-@btime begin
- sum_val = 0.0
- for _ in 1:1000
- sum_val += compute_element_stiffness_new()
- end
- sum_val
-end
-
-println()
-println("="^80)
-println("SUMMARY")
-println("="^80)
-println()
-println("Expected ranking (fastest to slowest):")
-println("1. Option D/E: Pre-computed constants (ultimate zero-cost)")
-println("2. Option C: Compile-time with Vec{D} (recommended for FEM)")
-println("3. Option A: Compile-time with plain tuples")
-println("4. Option B: Stored in element NTuple (slight overhead)")
-println("5. OLD: Runtime dispatch + Dict (type-unstable)")
-println()
-println("RECOMMENDATION:")
-println(" Use Option C or D/E for integration points:")
-println(" - Compile-time generation like eval_basis!")
-println(" - Return as Tuple of (weight, Vec{D}) pairs")
-println(" - Zero allocation, fully inlined")
-println(" - Matches golden standard architecture")
diff --git a/benchmarks/linear_elastic_analysis.jl b/benchmarks/linear_elastic_analysis.jl
deleted file mode 100644
index 29ed274..0000000
--- a/benchmarks/linear_elastic_analysis.jl
+++ /dev/null
@@ -1,272 +0,0 @@
-"""
-Performance analysis for LinearElastic material model.
-
-Analyzes:
-1. Execution time (@btime)
-2. Memory allocations (@allocated)
-3. Type stability (@code_warntype)
-4. LLVM IR optimization (code_llvm)
-5. Native assembly (code_native)
-"""
-
-using BenchmarkTools
-using Tensors
-using InteractiveUtils
-
-# Load implementation
-include("../src/materials/linear_elastic.jl")
-
-println("="^80)
-println("LINEAR ELASTIC MATERIAL - PERFORMANCE ANALYSIS")
-println("="^80)
-println()
-
-# Test material (steel)
-steel = LinearElastic(E=200e9, ν=0.3)
-
-# Test strain (uniaxial extension)
-ε = SymmetricTensor{2,3}((0.001, 0.0, 0.0, 0.0, 0.0, 0.0))
-
-println("Material: Steel (E = 200 GPa, ν = 0.3)")
-println("Strain: Uniaxial extension (ε₁₁ = 0.001)")
-println()
-
-# ============================================================================
-# BENCHMARK 1: Execution Time
-# ============================================================================
-println("BENCHMARK 1: Execution Time")
-println("-"^80)
-
-# Warmup
-compute_stress(steel, ε, nothing, 0.0)
-
-# Benchmark
-println("Running @btime compute_stress(steel, ε, nothing, 0.0)...")
-t = @benchmark compute_stress($steel, $ε, nothing, 0.0)
-println()
-display(t)
-println()
-println()
-
-# ============================================================================
-# BENCHMARK 2: Memory Allocations
-# ============================================================================
-println("BENCHMARK 2: Memory Allocations")
-println("-"^80)
-
-# First call to compile
-compute_stress(steel, ε, nothing, 0.0)
-
-# Check allocations
-allocs = @allocated compute_stress(steel, ε, nothing, 0.0)
-println("Allocations: $allocs bytes")
-
-if allocs == 0
- println("✅ ZERO ALLOCATIONS (stack-only computation)")
-else
- println("⚠️ WARNING: Non-zero allocations detected!")
-end
-println()
-println()
-
-# ============================================================================
-# BENCHMARK 3: Type Stability
-# ============================================================================
-println("BENCHMARK 3: Type Stability")
-println("-"^80)
-
-println("Running @code_warntype compute_stress(steel, ε, nothing, 0.0)...")
-println()
-@code_warntype compute_stress(steel, ε, nothing, 0.0)
-println()
-println()
-
-# ============================================================================
-# BENCHMARK 4: LLVM IR Analysis
-# ============================================================================
-println("BENCHMARK 4: LLVM IR Analysis")
-println("-"^80)
-
-println("Running @code_llvm compute_stress(steel, ε, nothing, 0.0)...")
-println()
-@code_llvm compute_stress(steel, ε, nothing, 0.0)
-println()
-println()
-
-# ============================================================================
-# BENCHMARK 5: Native Assembly
-# ============================================================================
-println("BENCHMARK 5: Native Assembly")
-println("-"^80)
-
-println("Running @code_native compute_stress(steel, ε, nothing, 0.0)...")
-println()
-@code_native compute_stress(steel, ε, nothing, 0.0)
-println()
-println()
-
-# ============================================================================
-# LLVM IR INSPECTION (Detailed Analysis)
-# ============================================================================
-println("LLVM IR INSPECTION")
-println("-"^80)
-
-# Get LLVM IR as string
-llvm_ir = sprint(io -> code_llvm(io, compute_stress, typeof.((steel, ε, nothing, 0.0))))
-
-# Count key operations
-n_fadd = count(r"fadd", llvm_ir)
-n_fmul = count(r"fmul", llvm_ir)
-n_load = count(r"load", llvm_ir)
-n_store = count(r"store", llvm_ir)
-n_call = count(r"call", llvm_ir)
-n_alloca = count(r"alloca", llvm_ir)
-
-# Count vector operations (SIMD)
-n_vector_ops = count(r"<\d+ x ", llvm_ir)
-n_shufflevector = count(r"shufflevector", llvm_ir)
-n_insertelement = count(r"insertelement", llvm_ir)
-n_extractelement = count(r"extractelement", llvm_ir)
-
-println("LLVM Operations Count:")
-println(" Floating-point additions: $n_fadd")
-println(" Floating-point multiplications: $n_fmul")
-println(" Memory loads: $n_load")
-println(" Memory stores: $n_store")
-println(" Function calls: $n_call")
-println(" Stack allocations (alloca): $n_alloca")
-println()
-println("SIMD Vectorization:")
-println(" Vector operations: $n_vector_ops")
-println(" Shuffle operations: $n_shufflevector")
-println(" Insert element operations: $n_insertelement")
-println(" Extract element operations: $n_extractelement")
-println()
-
-if n_call == 0
- println("✅ No function calls (fully inlined)")
-else
- println("⚠️ Contains $n_call function calls (may not be fully inlined)")
-end
-
-if n_alloca == 0
- println("✅ No stack allocations (register-only computation)")
-else
- println("ℹ️ Contains $n_alloca stack allocations")
-end
-println()
-println()
-
-# ============================================================================
-# NATIVE ASSEMBLY INSPECTION
-# ============================================================================
-println("NATIVE ASSEMBLY INSPECTION")
-println("-"^80)
-
-# Get native assembly as string
-native_asm = sprint(io -> code_native(io, compute_stress, typeof.((steel, ε, nothing, 0.0))))
-
-# Count SIMD instructions (AVX/SSE)
-n_vmul = count(r"vmul", native_asm)
-n_vadd = count(r"vadd", native_asm)
-n_vsub = count(r"vsub", native_asm)
-n_vfma = count(r"vfma", native_asm)
-n_vmov = count(r"vmov", native_asm)
-n_vbroadcast = count(r"vbroadcast", native_asm)
-
-# Count total vector instructions
-n_total_simd = n_vmul + n_vadd + n_vsub + n_vfma + n_vmov + n_vbroadcast
-
-println("x86-64 Assembly SIMD Instructions:")
-println(" vmulpd/vmulsd: $n_vmul")
-println(" vaddpd/vaddsd: $n_vadd")
-println(" vsubpd/vsubsd: $n_vsub")
-println(" vfmadd/vfmsub: $n_vfma (fused multiply-add)")
-println(" vmovapd/vmovsd: $n_vmov")
-println(" vbroadcast: $n_vbroadcast")
-println(" Total SIMD ops: $n_total_simd")
-println()
-
-if n_vfma > 0
- println("✅ FMA (Fused Multiply-Add) instructions detected (optimal)")
-end
-
-if n_total_simd > 0
- println("✅ SIMD vectorization active (AVX/AVX2)")
-else
- println("⚠️ No SIMD instructions detected")
-end
-println()
-println()
-
-# ============================================================================
-# PERFORMANCE SUMMARY
-# ============================================================================
-println("="^80)
-println("PERFORMANCE SUMMARY")
-println("="^80)
-println()
-
-# Extract median time from benchmark
-median_time = median(t.times)
-median_ns = median_time # Already in nanoseconds
-
-println("Execution Time:")
-println(" Median: $(round(median_ns, digits=2)) ns")
-println(" Mean: $(round(mean(t.times), digits=2)) ns")
-println(" Minimum: $(round(minimum(t.times), digits=2)) ns")
-println()
-
-println("Memory:")
-println(" Allocations: $allocs bytes")
-if allocs == 0
- println(" ✅ Zero allocation (confirmed)")
-end
-println()
-
-println("Code Quality:")
-if n_call == 0
- println(" ✅ Fully inlined (no function calls)")
-end
-if n_alloca == 0
- println(" ✅ Register-only computation (no stack usage)")
-end
-if n_total_simd > 0
- println(" ✅ SIMD optimized ($n_total_simd vector instructions)")
-end
-if n_vfma > 0
- println(" ✅ FMA instructions ($n_vfma fused multiply-adds)")
-end
-println()
-
-println("Expected Operations:")
-println(" Hooke's law: σ = λ·tr(ε)·I + 2μ·ε")
-println(" - 1 trace computation: 3 additions")
-println(" - 1 scalar multiplication: 1 multiply")
-println(" - 6 scalar multiplications for diagonal")
-println(" - 6 additions for final stress")
-println(" Tangent: 𝔻 = λ·I⊗I + 2μ·𝕀ˢʸᵐ")
-println(" - Constant tensor construction (may be compile-time)")
-println()
-
-# Theoretical lower bound
-theoretical_flops = 3 + 1 + 6 + 6 # From expected operations
-println("Theoretical minimum FLOPs: ~$theoretical_flops")
-println("LLVM FLOPs: $(n_fadd + n_fmul)")
-println()
-
-# Throughput calculation
-elements_per_second = 1e9 / median_ns
-println("Throughput:")
-println(" ~$(round(elements_per_second / 1e6, digits=1)) million stress evaluations/second/core")
-println()
-
-println("✅ Implementation validated as:")
-println(" - Zero allocation (confirmed)")
-println(" - Type stable")
-if n_total_simd > 0
- println(" - SIMD optimized ($n_total_simd vector ops)")
-end
-println(" - Median execution time: $(round(median_ns, digits=2)) ns")
-println()
-println("="^80)
diff --git a/benchmarks/material_models_benchmark.jl b/benchmarks/material_models_benchmark.jl
deleted file mode 100644
index d8e4b14..0000000
--- a/benchmarks/material_models_benchmark.jl
+++ /dev/null
@@ -1,865 +0,0 @@
-"""
-Material Models Performance Benchmark (Extended Version)
-
-Validates performance claims from docs/book/material_modeling.md:
-- Zero allocation claims
-- 5-50× speedup over Voigt/Dict approach
-- Type stability analysis (especially 'nothing' return for stateless materials)
-- Manual vs automatic differentiation for Neo-Hookean
-- Material state handling for Newton iterations
-
-Compares:
-1. New approach: Tensors.jl with SymmetricTensor
-2. Old approach: Voigt notation with arrays/Dict
-3. Neo-Hookean: Manual derivatives vs automatic differentiation
-
-Materials tested:
-- Linear Elastic (Hookean) - Stateless
-- Neo-Hookean Hyperelasticity - Stateless (AD and manual versions)
-- Perfect Plasticity (von Mises) - Stateful
-
-Type hierarchy:
-- AbstractMaterial - Base type for all materials
-- AbstractMaterialState - Base type for material internal state
- - NoState - For stateless materials
- - PlasticityState - For plasticity with history
-"""
-
-using Tensors
-using BenchmarkTools
-using LinearAlgebra
-using InteractiveUtils # For @code_warntype
-
-println("="^80)
-println("Material Models Performance Benchmark (Extended)")
-println("="^80)
-println()
-
-#=============================================================================
-TYPE HIERARCHY
-=============================================================================#
-
-"""
-Abstract base type for all materials.
-
-All concrete materials must implement:
-- `compute_stress(material, ε, state_old, Δt) -> (σ, 𝔻, state_new)`
-- `initial_state(material) -> AbstractMaterialState`
-"""
-abstract type AbstractMaterial end
-
-"""
-Abstract base type for material internal state.
-
-Used to track history-dependent variables during Newton iterations:
-- Old state (beginning of time step)
-- Trial state (current Newton iteration)
-- New state (converged solution)
-"""
-abstract type AbstractMaterialState end
-
-"""
-State for stateless materials (no history dependence).
-
-Using singleton type instead of `nothing` for type hierarchy consistency.
-Performance identical to `nothing` (zero-sized type).
-"""
-struct NoState <: AbstractMaterialState end
-
-"""
-Initial state for stateless materials.
-"""
-initial_state(::AbstractMaterial) = NoState()
-
-#=============================================================================
-NEW APPROACH: Tensors.jl Implementation
-=============================================================================#
-
-# ---------------------------------------------------------------------------
-# 1. Linear Elastic (Hookean)
-# ---------------------------------------------------------------------------
-
-"""Linear elastic material with Tensors.jl"""
-struct LinearElastic <: AbstractMaterial
- E::Float64 # Young's modulus [Pa]
- ν::Float64 # Poisson's ratio [-]
-end
-
-LinearElastic(; E, ν) = LinearElastic(E, ν)
-
-λ(mat::LinearElastic) = mat.E * mat.ν / ((1 + mat.ν) * (1 - 2mat.ν))
-μ(mat::LinearElastic) = mat.E / (2(1 + mat.ν))
-
-"""Compute stress for linear elastic material."""
-function compute_stress(
- material::LinearElastic,
- ε::SymmetricTensor{2,3,T},
- state_old::NoState,
- Δt::Float64
-) where T
-
- # Lamé parameters
- λ_val = λ(material)
- μ_val = μ(material)
-
- # Identity tensor
- I = one(ε)
-
- # Hooke's law: σ = λ·tr(ε)·I + 2μ·ε
- σ = λ_val * tr(ε) * I + 2μ_val * ε
-
- # Tangent modulus: 𝔻 = λ I⊗I + 2μ 𝕀ˢʸᵐ
- 𝕀ˢʸᵐ = one(SymmetricTensor{4,3,T}) # Symmetric 4th order identity
- 𝔻 = λ_val * I ⊗ I + 2μ_val * 𝕀ˢʸᵐ
-
- return σ, 𝔻, NoState() # No state change (stateless)
-end# ---------------------------------------------------------------------------
-# 2. Neo-Hookean Hyperelasticity (Automatic Differentiation)
-# ---------------------------------------------------------------------------
-
-"""Neo-Hookean hyperelastic material (using automatic differentiation)."""
-struct NeoHookeanAD <: AbstractMaterial
- μ::Float64 # Shear modulus [Pa]
- λ::Float64 # Lamé parameter [Pa]
-end
-
-function NeoHookeanAD(; E, ν)
- μ = E / (2(1 + ν))
- λ = E * ν / ((1 + ν) * (1 - 2ν))
- return NeoHookeanAD(μ, λ)
-end
-
-"""Strain energy density for Neo-Hookean model."""
-function strain_energy(material::NeoHookeanAD, C::SymmetricTensor{2,3})
- μ, λ = material.μ, material.λ
-
- # Invariants
- I₁ = tr(C)
- J = √(det(C))
-
- # Strain energy: ψ = μ/2(I₁ - 3) - μln(J) + λ/2·ln²(J)
- ψ = μ / 2 * (I₁ - 3) - μ * log(J) + λ / 2 * log(J)^2
-
- return ψ
-end
-
-"""Compute stress for Neo-Hookean material using automatic differentiation."""
-function compute_stress(
- material::NeoHookeanAD,
- E::SymmetricTensor{2,3,T}, # Green-Lagrange strain
- state_old::NoState,
- Δt::Float64
-) where T
-
- # Right Cauchy-Green tensor: C = 2E + I
- I = one(E)
- C = 2E + I
-
- # Strain energy function (closure capturing material)
- ψ(C_) = strain_energy(material, C_)
-
- # Automatic differentiation!
- 𝔻, S = hessian(ψ, C, :all) # Returns both hessian and gradient!
-
- # Note: We want S = 2·∂ψ/∂C, 𝔻 = 4·∂²ψ/∂C²
- S = 2 * S
- 𝔻 = 4 * 𝔻
-
- return S, 𝔻, NoState() # No state change (stateless)
-end
-
-# ---------------------------------------------------------------------------
-# 3. Neo-Hookean Hyperelasticity (Manual Derivatives)
-# ---------------------------------------------------------------------------
-
-"""
-Neo-Hookean hyperelastic material (hand-coded derivatives).
-
-Strain energy: ψ(C) = μ/2(I₁ - 3) - μln(J) + λ/2·ln²(J)
-
-Where:
-- I₁ = tr(C) - First invariant
-- J = √det(C) - Jacobian determinant
-
-Derivatives (computed by hand):
-- S = 2∂ψ/∂C = μ(I - C⁻¹) + λln(J)C⁻¹
-- 𝔻 = 4∂²ψ/∂C² = λ(C⁻¹⊗C⁻¹) + 2(μ - λln(J))∂C⁻¹/∂C
-
-The second derivative uses the identity:
-∂C⁻¹/∂C : X = -C⁻¹:(X:C⁻¹) for any symmetric X
-"""
-struct NeoHookeanManual <: AbstractMaterial
- μ::Float64 # Shear modulus [Pa]
- λ::Float64 # Lamé parameter [Pa]
-end
-
-function NeoHookeanManual(; E, ν)
- μ = E / (2(1 + ν))
- λ = E * ν / ((1 + ν) * (1 - 2ν))
- return NeoHookeanManual(μ, λ)
-end
-
-"""Compute stress for Neo-Hookean material with manual derivatives."""
-function compute_stress(
- material::NeoHookeanManual,
- E::SymmetricTensor{2,3,T}, # Green-Lagrange strain
- state_old::NoState,
- Δt::Float64
-) where T
- μ, λ = material.μ, material.λ
-
- # Right Cauchy-Green tensor: C = 2E + I
- I = one(E)
- C = 2E + I
-
- # Invariants
- J = √(det(C))
- C_inv = inv(C)
-
- # Second Piola-Kirchhoff stress: S = μ(I - C⁻¹) + λln(J)C⁻¹
- S = μ * (I - C_inv) + λ * log(J) * C_inv
-
- # Material tangent: 𝔻 = 4∂²ψ/∂C²
- # Term 1: λ(C⁻¹⊗C⁻¹)
- 𝔻₁ = λ * (C_inv ⊗ C_inv)
-
- # Term 2: 2(μ - λln(J))∂C⁻¹/∂C
- # The derivative ∂C⁻¹/∂C can be computed as:
- # (∂C⁻¹/∂C)ᵢⱼₖₗ = -1/2(C⁻¹ᵢₖC⁻¹ⱼₗ + C⁻¹ᵢₗC⁻¹ⱼₖ)
- #
- # For SymmetricTensor, we build this fourth-order tensor
- # by exploiting the symmetry structure
-
- # Build the symmetric fourth-order tensor manually
- # This is the most expensive part of the computation
- 𝕀ˢʸᵐ = one(SymmetricTensor{4,3,T})
-
- # For compressible Neo-Hookean, the full tangent is:
- # 𝔻 = λ(C⁻¹⊗C⁻¹) - 2(μ - λln(J))(C⁻¹⊙C⁻¹)
- # where ⊙ is the symmetric dyadic product for fourth-order tensors
-
- # Construct C⁻¹⊗C⁻¹ part (already have 𝔻₁)
- # Construct symmetric part: use Tensors.jl identity operations
- # The fourth-order identity for symmetric tensors handles this
-
- coeff = 2(μ - λ * log(J))
-
- # For the symmetric outer product of C⁻¹ with itself,
- # we can use the following approach:
- # Build component-wise using Voigt ordering
-
- # Simplified: Use the property that for small strains,
- # this reduces to a simpler form. For full nonlinear case:
- 𝔻₂ = -coeff * inv_symmetric_outer(C_inv)
-
- 𝔻 = 𝔻₁ + 𝔻₂
-
- return S, 𝔻, NoState()
-end
-
-"""
-Compute symmetric fourth-order tensor from inverse: ∂C⁻¹/∂C
-
-For symmetric second-order tensor C⁻¹, compute the fourth-order tensor:
-(∂C⁻¹/∂C)ᵢⱼₖₗ = -1/2(C⁻¹ᵢₖC⁻¹ⱼₗ + C⁻¹ᵢₗC⁻¹ⱼₖ)
-
-This appears in the material tangent of hyperelastic materials.
-"""
-function inv_symmetric_outer(C_inv::SymmetricTensor{2,3,T}) where T
- # Extract components (Voigt notation: 11, 22, 33, 12, 23, 13)
- c = [C_inv[1, 1], C_inv[2, 2], C_inv[3, 3],
- C_inv[1, 2], C_inv[2, 3], C_inv[1, 3]]
-
- # Build fourth-order tensor in Voigt notation (6x6 matrix representation)
- # Then convert to SymmetricTensor{4,3}
- #
- # This is the -1/2(CᵢₖCⱼₗ + CᵢₗCⱼₖ) tensor
-
- # For now, use a simpler approximation that works for Neo-Hookean
- # Full implementation would build all 36 components
-
- # Use outer product and symmetrize
- result = C_inv ⊗ C_inv
-
- # Add symmetric component
- # (This is a simplified version - full implementation needs more care)
- return result
-end
-
-# ---------------------------------------------------------------------------
-# 4. Perfect Plasticity (von Mises)
-# ---------------------------------------------------------------------------
-
-"""Perfect plasticity with von Mises yield criterion."""
-struct PerfectPlasticity <: AbstractMaterial
- E::Float64 # Young's modulus [Pa]
- ν::Float64 # Poisson's ratio [-]
- σ_y::Float64 # Yield stress [Pa]
-end
-
-PerfectPlasticity(; E, ν, σ_y) = PerfectPlasticity(E, ν, σ_y)
-
-λ(mat::PerfectPlasticity) = mat.E * mat.ν / ((1 + mat.ν) * (1 - 2mat.ν))
-μ(mat::PerfectPlasticity) = mat.E / (2(1 + mat.ν))
-
-"""
-Internal state for plasticity (history-dependent variables).
-
-This struct is passed through Newton iterations:
-- state_old: State at beginning of time step (t_n)
-- state_trial: Trial state during iteration (may not converge)
-- state_new: Updated state for next iteration (t_n+1)
-"""
-struct PlasticityState{T} <: AbstractMaterialState
- ε_p::SymmetricTensor{2,3,T} # Plastic strain
- α::T # Equivalent plastic strain
-end
-
-"""Initial state for plasticity (zero plastic strain)."""
-initial_state(::PerfectPlasticity) = PlasticityState(zero(SymmetricTensor{2,3}), 0.0)
-
-"""Von Mises equivalent stress."""
-function von_mises_stress(σ::SymmetricTensor{2,3})
- s = dev(σ) # Deviatoric stress
- return √(3 / 2 * s ⊡ s)
-end
-
-"""Compute stress for perfectly plastic material with radial return."""
-function compute_stress(
- material::PerfectPlasticity,
- ε::SymmetricTensor{2,3,T},
- state_old::PlasticityState{T},
- Δt::Float64
-) where T
-
- # Material parameters
- λ_val = λ(material)
- μ_val = μ(material)
- σ_y = material.σ_y
-
- # Elastic constitutive tensor
- I = one(ε)
- 𝕀ˢʸᵐ = one(SymmetricTensor{4,3,T})
- 𝔻ᵉ = λ_val * I ⊗ I + 2μ_val * 𝕀ˢʸᵐ
-
- # Elastic predictor
- ε_e = ε - state_old.ε_p
- σ_trial = λ_val * tr(ε_e) * I + 2μ_val * ε_e
- σ_eq_trial = von_mises_stress(σ_trial)
-
- # Yield function
- f = σ_eq_trial - σ_y
-
- if f ≤ 0.0
- # Elastic step
- σ = σ_trial
- 𝔻 = 𝔻ᵉ
- state_new = state_old
- else
- # Plastic step: Radial return
- s_trial = dev(σ_trial)
- p = tr(σ_trial) / 3
-
- # Return to yield surface
- σ = p * I + (σ_y / σ_eq_trial) * s_trial
-
- # Plastic multiplier
- Δγ = f / (3μ_val)
-
- # Flow direction
- n = √(3 / 2) * s_trial / σ_eq_trial
-
- # Update plastic strain
- ε_p_new = state_old.ε_p + Δγ * n
- α_new = state_old.α + Δγ
-
- state_new = PlasticityState(ε_p_new, α_new)
-
- # Algorithmic tangent (simplified)
- θ = 1 - σ_y / σ_eq_trial
- β = 6μ_val^2 / (3μ_val + θ * 3μ_val)
-
- 𝔻 = 𝔻ᵉ - β * (n ⊗ n)
- end
-
- return σ, 𝔻, state_new
-end
-
-#=============================================================================
-OLD APPROACH: Voigt Notation + Array Implementation
-=============================================================================#
-
-"""Old-style linear elastic with Voigt notation."""
-struct LinearElasticOld
- E::Float64
- ν::Float64
-end
-
-"""Compute 6×6 constitutive matrix (Voigt notation)."""
-function constitutive_matrix(mat::LinearElasticOld)
- E, ν = mat.E, mat.ν
- λ = E * ν / ((1 + ν) * (1 - 2ν))
- μ = E / (2(1 + ν))
-
- D = zeros(6, 6)
- D[1:3, 1:3] .= λ
- D[1, 1] = D[2, 2] = D[3, 3] = λ + 2μ
- D[4, 4] = D[5, 5] = D[6, 6] = μ
-
- return D
-end
-
-"""Compute stress (old approach with arrays)."""
-function compute_stress_old(
- material::LinearElasticOld,
- ε_vec::Vector{Float64}, # [ε11, ε22, ε33, 2ε12, 2ε23, 2ε13]
- state_old::Dict{String,Any},
- Δt::Float64
-)
- D = constitutive_matrix(material)
- σ_vec = D * ε_vec
- return σ_vec, D, state_old
-end
-
-"""Old-style Neo-Hookean (manual derivatives)."""
-struct NeoHookeanOld
- μ::Float64
- λ::Float64
-end
-
-"""Compute stress manually (simplified, no actual derivatives for brevity)."""
-function compute_stress_old(
- material::NeoHookeanOld,
- E_vec::Vector{Float64},
- state_old::Dict{String,Any},
- Δt::Float64
-)
- # This would normally have 50+ lines of manual derivative calculations
- # For benchmark purposes, just do some array operations
- D = zeros(6, 6)
- for i in 1:6
- D[i, i] = material.μ + material.λ / 3
- end
- σ_vec = D * E_vec
- return σ_vec, D, state_old
-end
-
-"""Old-style plasticity with Dict storage."""
-struct PerfectPlasticityOld
- E::Float64
- ν::Float64
- σ_y::Float64
-end
-
-"""Compute stress with Dict field storage."""
-function compute_stress_old(
- material::PerfectPlasticityOld,
- ε_vec::Vector{Float64},
- state_old::Dict{String,Any},
- Δt::Float64
-)
- # Get plastic strain from Dict (type instability!)
- if haskey(state_old, "epsilon_plastic")
- ε_p_vec = state_old["epsilon_plastic"]
- else
- ε_p_vec = zeros(6)
- end
-
- # Elastic trial
- D = constitutive_matrix(LinearElasticOld(material.E, material.ν))
- ε_e_vec = ε_vec - ε_p_vec
- σ_trial_vec = D * ε_e_vec
-
- # Von Mises check (manual calculation with arrays)
- s11, s22, s33 = σ_trial_vec[1:3]
- s12, s23, s13 = σ_trial_vec[4:6]
- p = (s11 + s22 + s33) / 3
- dev_vec = [s11 - p, s22 - p, s33 - p, s12, s23, s13]
- σ_eq = √(3 / 2 * (dev_vec[1]^2 + dev_vec[2]^2 + dev_vec[3]^2 +
- 2 * (dev_vec[4]^2 + dev_vec[5]^2 + dev_vec[6]^2)))
-
- f = σ_eq - material.σ_y
-
- state_new = copy(state_old)
-
- if f > 0.0
- # Plastic correction
- factor = material.σ_y / σ_eq
- σ_vec = [p, p, p, 0.0, 0.0, 0.0] + factor * dev_vec
-
- # Update state in Dict
- Δγ = f / (3 * material.E / (2(1 + material.ν)))
- n_vec = √(3 / 2) * dev_vec / σ_eq
- state_new["epsilon_plastic"] = ε_p_vec + Δγ * n_vec
- else
- σ_vec = σ_trial_vec
- end
-
- return σ_vec, D, state_new
-end
-
-#=============================================================================
-MATERIAL STATE HANDLING FOR NEWTON ITERATIONS
-=============================================================================#
-
-"""
-Example: How to handle material state during Newton-Raphson iterations.
-
-In FEM nonlinear analysis, each time step requires iterative solution:
-
-1. **Beginning of time step (t_n):**
- - state_old = converged state from previous time step
-
-2. **During Newton iterations (t_n → t_n+1):**
- - For each iteration k = 1, 2, ...
- - Compute: σ, 𝔻, state_trial = compute_stress(material, ε_k, state_old, Δt)
- - state_trial is NOT committed yet (iteration may not converge)
-
-3. **After convergence:**
- - state_new = state_trial from final iteration
- - Commit: state_old ← state_new for next time step
-
-This ensures:
-- Failed iterations don't corrupt material history
-- Material state is consistent with converged solution
-- Internal variables (plastic strain, damage, etc.) evolve correctly
-"""
-
-"""
-Simulate Newton-Raphson iteration with material state handling.
-
-Returns:
-- converged: Whether iterations converged
-- n_iter: Number of iterations
-- state_converged: Final material state (only valid if converged)
-"""
-function newton_with_material_state(
- material::AbstractMaterial,
- ε_target::SymmetricTensor{2,3},
- state_old::AbstractMaterialState,
- Δt::Float64;
- max_iter=10,
- tol=1e-8
-)
- println(" Newton iteration with material state tracking:")
- println(" " * "="^60)
-
- # Initial guess
- ε_k = zero(ε_target)
-
- for k in 1:max_iter
- # Compute stress and tangent (state_trial is NOT committed yet!)
- σ_k, 𝔻_k, state_trial = compute_stress(material, ε_k, state_old, Δt)
-
- println(" Iteration $k:")
- println(" strain: $(norm(ε_k))")
- println(" stress: $(norm(σ_k))")
- println(" state: $(state_trial)")
-
- # Residual (simplified: just strain error)
- r = norm(ε_k - ε_target)
-
- if r < tol
- println(" → Converged!")
- println(" Final state committed: $(state_trial)")
- return true, k, state_trial
- end
-
- # Newton update (simplified)
- ε_k = ε_k + 0.5 * (ε_target - ε_k)
- end
-
- println(" → Failed to converge!")
- println(" State NOT committed (keeping state_old)")
- return false, max_iter, state_old # Keep old state on failure!
-end
-
-println()
-println("="^80)
-println("NEWTON ITERATION STATE HANDLING EXAMPLE")
-println("="^80)
-println()
-
-# Example 1: Stateless material (LinearElastic)
-println("Example 1: Stateless Material (LinearElastic)")
-println("-"^80)
-steel_example = LinearElastic(E=200e9, ν=0.3)
-state_stateless = initial_state(steel_example)
-ε_test = SymmetricTensor{2,3}((0.001, 0.0, 0.0, 0.0, 0.0, 0.0))
-
-converged, n_iter, state_final = newton_with_material_state(
- steel_example, ε_test, state_stateless, 1.0, max_iter=3
-)
-println("Result: state_final = $state_final (NoState, always)")
-println()
-
-# Example 2: Stateful material (PerfectPlasticity)
-println("Example 2: Stateful Material (PerfectPlasticity)")
-println("-"^80)
-plastic_example = PerfectPlasticity(E=200e9, ν=0.3, σ_y=250e6)
-state_stateful = initial_state(plastic_example)
-ε_test_plastic = SymmetricTensor{2,3}((0.002, 0.0, 0.0, 0.0, 0.0, 0.0)) # Large strain → plastic
-
-converged, n_iter, state_final = newton_with_material_state(
- plastic_example, ε_test_plastic, state_stateful, 1.0, max_iter=3
-)
-println("Result: state_final = $state_final (plastic strain accumulated)")
-println()
-
-println("Key insight: State handling is IDENTICAL for all materials due to")
-println("AbstractMaterialState type hierarchy. Assembly code doesn't need")
-println("to know whether material is stateless or stateful!")
-println()
-
-#=============================================================================
-BENCHMARK SETUP
-=============================================================================#
-
-println("Setting up materials and test cases...")
-println()
-
-# Materials (realistic steel properties)
-steel_new = LinearElastic(E=200e9, ν=0.3)
-steel_old = LinearElasticOld(200e9, 0.3)
-
-rubber_ad = NeoHookeanAD(E=10e6, ν=0.45)
-rubber_manual = NeoHookeanManual(E=10e6, ν=0.45)
-rubber_old = NeoHookeanOld(10e6 / (2 * 1.45), 10e6 * 0.45 / (1.45 * 0.1))
-
-plastic_new = PerfectPlasticity(E=200e9, ν=0.3, σ_y=250e6)
-plastic_old = PerfectPlasticityOld(200e9, 0.3, 250e6)
-
-# Test strain (small elastic deformation)
-ε11, ε22, ε33 = 0.001, -0.0003, -0.0003 # Uniaxial tension with Poisson effect
-ε12, ε23, ε13 = 0.0, 0.0, 0.0
-
-# New approach: SymmetricTensor
-ε_tensor = SymmetricTensor{2,3}((ε11, ε12, ε13, ε22, ε23, ε33))
-E_tensor = ε_tensor # For Neo-Hookean (Green-Lagrange ≈ small strain here)
-
-# Old approach: Voigt vector (note factor of 2 for shear!)
-ε_voigt = [ε11, ε22, ε33, 2 * ε12, 2 * ε23, 2 * ε13]
-
-# States (using proper type hierarchy)
-state_nostate = NoState()
-state_dict_empty = Dict{String,Any}()
-state_plastic_new = initial_state(plastic_new)
-state_plastic_old = Dict{String,Any}("epsilon_plastic" => zeros(6))
-
-println("Materials configured:")
-println(" - Linear Elastic: E = 200 GPa, ν = 0.3")
-println(" - Neo-Hookean (AD): μ ≈ 3.4 MPa, λ ≈ 45 MPa (automatic differentiation)")
-println(" - Neo-Hookean (Manual): μ ≈ 3.4 MPa, λ ≈ 45 MPa (hand-coded derivatives)")
-println(" - Perfect Plasticity: E = 200 GPa, σ_y = 250 MPa")
-println()
-println("Test strain: ε11 = 0.001 (uniaxial tension)")
-println()
-
-#=============================================================================
-TYPE STABILITY CHECK
-=============================================================================#
-
-println("="^80)
-println("TYPE STABILITY ANALYSIS")
-println("="^80)
-println()
-
-println("Checking for type instabilities...")
-println()
-
-# Check LinearElastic
-println("1. Linear Elastic (Tensors.jl):")
-@code_warntype compute_stress(steel_new, ε_tensor, state_nostate, 0.0)
-println()
-
-println("2. Linear Elastic (Old Voigt/Dict):")
-@code_warntype compute_stress_old(steel_old, ε_voigt, state_dict_empty, 0.0)
-println()
-
-println("3. Neo-Hookean AD (Tensors.jl with automatic differentiation):")
-@code_warntype compute_stress(rubber_ad, E_tensor, state_nostate, 0.0)
-println()
-
-println("4. Neo-Hookean Manual (Tensors.jl with hand-coded derivatives):")
-@code_warntype compute_stress(rubber_manual, E_tensor, state_nostate, 0.0)
-println()
-
-println("5. Perfect Plasticity (Tensors.jl):")
-@code_warntype compute_stress(plastic_new, ε_tensor, state_plastic_new, 0.0)
-println()
-
-println("6. Perfect Plasticity (Old Dict):")
-@code_warntype compute_stress_old(plastic_old, ε_voigt, state_plastic_old, 0.0)
-println()
-
-#=============================================================================
-ALLOCATION TESTS
-=============================================================================#
-
-println("="^80)
-println("ALLOCATION TESTS")
-println("="^80)
-println()
-
-println("Testing for allocations (should be 0 for new approach)...")
-println()
-
-# Linear Elastic
-println("1. Linear Elastic")
-println(" NEW (Tensors.jl):")
-allocs_le_new = @allocated compute_stress(steel_new, ε_tensor, state_nostate, 0.0)
-println(" Allocations: $allocs_le_new bytes")
-
-println(" OLD (Voigt/Dict):")
-allocs_le_old = @allocated compute_stress_old(steel_old, ε_voigt, state_dict_empty, 0.0)
-println(" Allocations: $allocs_le_old bytes")
-println()
-
-# Neo-Hookean
-println("2. Neo-Hookean")
-println(" NEW (Tensors.jl + AD):")
-allocs_nh_ad = @allocated compute_stress(rubber_ad, E_tensor, state_nostate, 0.0)
-println(" Allocations: $allocs_nh_ad bytes")
-
-println(" NEW (Tensors.jl + Manual):")
-allocs_nh_manual = @allocated compute_stress(rubber_manual, E_tensor, state_nostate, 0.0)
-println(" Allocations: $allocs_nh_manual bytes")
-
-println(" OLD (Array):")
-allocs_nh_old = @allocated compute_stress_old(rubber_old, ε_voigt, state_dict_empty, 0.0)
-println(" Allocations: $allocs_nh_old bytes")
-println()
-
-# Perfect Plasticity
-println("3. Perfect Plasticity (elastic branch)")
-println(" NEW (Tensors.jl):")
-allocs_pp_new = @allocated compute_stress(plastic_new, ε_tensor, state_plastic_new, 0.0)
-println(" Allocations: $allocs_pp_new bytes")
-
-println(" OLD (Dict):")
-allocs_pp_old = @allocated compute_stress_old(plastic_old, ε_voigt, state_plastic_old, 0.0)
-println(" Allocations: $allocs_pp_old bytes")
-println()
-
-#=============================================================================
-PERFORMANCE BENCHMARKS
-=============================================================================#
-
-println("="^80)
-println("PERFORMANCE BENCHMARKS")
-println("="^80)
-println()
-
-println("Running detailed benchmarks (this may take a minute)...")
-println()
-
-# Linear Elastic
-println("1. LINEAR ELASTIC")
-println("-"^40)
-println("NEW (Tensors.jl):")
-bench_le_new = @benchmark compute_stress($steel_new, $ε_tensor, $state_nostate, 0.0)
-display(bench_le_new)
-println()
-
-println("OLD (Voigt/Dict):")
-bench_le_old = @benchmark compute_stress_old($steel_old, $ε_voigt, $state_dict_empty, 0.0)
-display(bench_le_old)
-println()
-
-speedup_le = median(bench_le_old.times) / median(bench_le_new.times)
-println("SPEEDUP: $(round(speedup_le, digits=1))×")
-println()
-
-# Neo-Hookean
-println("2. NEO-HOOKEAN")
-println("-"^40)
-println("NEW (Tensors.jl + Automatic Differentiation):")
-bench_nh_ad = @benchmark compute_stress($rubber_ad, $E_tensor, $state_nostate, 0.0)
-display(bench_nh_ad)
-println()
-
-println("NEW (Tensors.jl + Manual Derivatives):")
-bench_nh_manual = @benchmark compute_stress($rubber_manual, $E_tensor, $state_nostate, 0.0)
-display(bench_nh_manual)
-println()
-
-println("OLD (Array):")
-bench_nh_old = @benchmark compute_stress_old($rubber_old, $ε_voigt, $state_dict_empty, 0.0)
-display(bench_nh_old)
-println()
-
-speedup_nh_ad = median(bench_nh_old.times) / median(bench_nh_ad.times)
-speedup_nh_manual = median(bench_nh_old.times) / median(bench_nh_manual.times)
-ad_overhead = median(bench_nh_ad.times) / median(bench_nh_manual.times)
-println("SPEEDUP (AD): $(round(speedup_nh_ad, digits=1))×")
-println("SPEEDUP (Manual): $(round(speedup_nh_manual, digits=1))×")
-println("AD OVERHEAD: $(round(ad_overhead, digits=1))× (AD / Manual)")
-println()
-
-# Perfect Plasticity
-println("3. PERFECT PLASTICITY (elastic branch)")
-println("-"^40)
-println("NEW (Tensors.jl):")
-bench_pp_new = @benchmark compute_stress($plastic_new, $ε_tensor, $state_plastic_new, 0.0)
-display(bench_pp_new)
-println()
-
-println("OLD (Dict):")
-bench_pp_old = @benchmark compute_stress_old($plastic_old, $ε_voigt, $state_plastic_old, 0.0)
-display(bench_pp_old)
-println()
-
-speedup_pp = median(bench_pp_old.times) / median(bench_pp_new.times)
-println("SPEEDUP: $(round(speedup_pp, digits=1))×")
-println()
-
-#=============================================================================
-SUMMARY
-=============================================================================#
-
-println("="^80)
-println("SUMMARY")
-println("="^80)
-println()
-
-println("ALLOCATIONS:")
-println(" LinearElastic: NEW = $allocs_le_new bytes, OLD = $allocs_le_old bytes")
-println(" NeoHookean (AD): NEW = $allocs_nh_ad bytes, OLD = $allocs_nh_old bytes")
-println(" NeoHookean (Manual): NEW = $allocs_nh_manual bytes")
-println(" PerfectPlasticity: NEW = $allocs_pp_new bytes, OLD = $allocs_pp_old bytes")
-println()
-
-println("MEDIAN TIMING:")
-println(" LinearElastic: NEW = $(median(bench_le_new.times)) ns, OLD = $(median(bench_le_old.times)) ns")
-println(" NeoHookean (AD): NEW = $(median(bench_nh_ad.times)) ns, OLD = $(median(bench_nh_old.times)) ns")
-println(" NeoHookean (Manual): NEW = $(median(bench_nh_manual.times)) ns")
-println(" PerfectPlasticity: NEW = $(median(bench_pp_new.times)) ns, OLD = $(median(bench_pp_old.times)) ns")
-println()
-
-println("SPEEDUP (OLD / NEW):")
-println(" LinearElastic: $(round(speedup_le, digits=1))×")
-println(" NeoHookean (AD): $(round(speedup_nh_ad, digits=1))×")
-println(" NeoHookean (Manual): $(round(speedup_nh_manual, digits=1))×")
-println(" PerfectPlasticity: $(round(speedup_pp, digits=1))×")
-println()
-
-println("AD OVERHEAD:")
-println(" NeoHookean: AD is $(round(ad_overhead, digits=1))× slower than manual derivatives")
-println()
-
-avg_speedup = (speedup_le + speedup_nh_manual + speedup_pp) / 3
-println("AVERAGE SPEEDUP: $(round(avg_speedup, digits=1))× (using manual Neo-Hookean)")
-println()
-
-# Validate claims
-println("VALIDATION OF CLAIMS:")
-println(" - Zero allocations for new approach: ",
- allocs_le_new == 0 && allocs_nh_ad == 0 && allocs_nh_manual == 0 && allocs_pp_new == 0 ? "✓ PASS" : "✗ FAIL")
-println(" - Manual derivatives outperform AD: ",
- median(bench_nh_manual.times) < median(bench_nh_ad.times) ? "✓ PASS" : "✗ FAIL")
-println(" - Type stability with NoState return: Check @code_warntype output above")
-println()
-
-println("="^80)
-println("Benchmark complete! Results saved to: material_models_benchmark_results.txt")
-println("="^80)
diff --git a/benchmarks/material_models_benchmark_results.txt b/benchmarks/material_models_benchmark_results.txt
deleted file mode 100644
index c9a2b26..0000000
--- a/benchmarks/material_models_benchmark_results.txt
+++ /dev/null
@@ -1,922 +0,0 @@
-================================================================================
-Material Models Performance Benchmark (Extended)
-================================================================================
-
-
-================================================================================
-NEWTON ITERATION STATE HANDLING EXAMPLE
-================================================================================
-
-Example 1: Stateless Material (LinearElastic)
---------------------------------------------------------------------------------
- Newton iteration with material state tracking:
- ============================================================
- Iteration 1:
- strain: 0.0
- stress: 0.0
- state: NoState()
- Iteration 2:
- strain: 0.0005
- stress: 1.5741063022831637e8
- state: NoState()
- Iteration 3:
- strain: 0.00075
- stress: 2.3611594534247452e8
- state: NoState()
- → Failed to converge!
- State NOT committed (keeping state_old)
-Result: state_final = NoState() (NoState, always)
-
-Example 2: Stateful Material (PerfectPlasticity)
---------------------------------------------------------------------------------
- Newton iteration with material state tracking:
- ============================================================
- Iteration 1:
- strain: 0.0
- stress: 0.0
- state: PlasticityState{Float64}([0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0], 0.0)
- Iteration 2:
- strain: 0.001
- stress: 3.1482126045663273e8
- state: PlasticityState{Float64}([0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0], 0.0)
- Iteration 3:
- strain: 0.0015
- stress: 4.7223189068494904e8
- state: PlasticityState{Float64}([0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0], 0.0)
- → Failed to converge!
- State NOT committed (keeping state_old)
-Result: state_final = PlasticityState{Float64}([0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0], 0.0) (plastic strain accumulated)
-
-Key insight: State handling is IDENTICAL for all materials due to
-AbstractMaterialState type hierarchy. Assembly code doesn't need
-to know whether material is stateless or stateful!
-
-Setting up materials and test cases...
-
-Materials configured:
- - Linear Elastic: E = 200 GPa, ν = 0.3
- - Neo-Hookean (AD): μ ≈ 3.4 MPa, λ ≈ 45 MPa (automatic differentiation)
- - Neo-Hookean (Manual): μ ≈ 3.4 MPa, λ ≈ 45 MPa (hand-coded derivatives)
- - Perfect Plasticity: E = 200 GPa, σ_y = 250 MPa
-
-Test strain: ε11 = 0.001 (uniaxial tension)
-
-================================================================================
-TYPE STABILITY ANALYSIS
-================================================================================
-
-Checking for type instabilities...
-
-1. Linear Elastic (Tensors.jl):
-MethodInstance for compute_stress(::LinearElastic, ::SymmetricTensor{2, 3, Float64, 6}, ::NoState, ::Float64)
- from compute_stress(material::LinearElastic, ε::SymmetricTensor{2, 3, T}, state_old::NoState, Δt::Float64) where T @ Main ~/dev/JuliaFEM.jl/benchmarks/material_models_benchmark.jl:94
-Static Parameters
- T = Float64
-Arguments
- #self#::Core.Const(Main.compute_stress)
- material::LinearElastic
- ε::SymmetricTensor{2, 3, Float64, 6}
- state_old::Core.Const(NoState())
- Δt::Float64
-Locals
- 𝔻::SymmetricTensor{4, 3, Float64, 36}
- 𝕀ˢʸᵐ::SymmetricTensor{4, 3, Float64, 36}
- σ::SymmetricTensor{2, 3, Float64, 6}
- I::SymmetricTensor{2, 3, Float64, 6}
- μ_val::Float64
- λ_val::Float64
-Body::Tuple{SymmetricTensor{2, 3, Float64, 6}, SymmetricTensor{4, 3, Float64, 36}, NoState}
-1 ─ %1 = Main.λ::Core.Const(Main.λ)
-│ (λ_val = (%1)(material))
-│ %3 = Main.μ::Core.Const(Main.μ)
-│ (μ_val = (%3)(material))
-│ %5 = Main.one::Core.Const(one)
-│ (I = (%5)(ε))
-│ %7 = Main.:+::Core.Const(+)
-│ %8 = Main.:*::Core.Const(*)
-│ %9 = λ_val::Float64
-│ %10 = Main.tr::Core.Const(LinearAlgebra.tr)
-│ %11 = (%10)(ε)::Float64
-│ %12 = I::Core.Const([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])
-│ %13 = (%8)(%9, %11, %12)::SymmetricTensor{2, 3, Float64, 6}
-│ %14 = Main.:*::Core.Const(*)
-│ %15 = Main.:*::Core.Const(*)
-│ %16 = μ_val::Float64
-│ %17 = (%15)(2, %16)::Float64
-│ %18 = (%14)(%17, ε)::SymmetricTensor{2, 3, Float64, 6}
-│ (σ = (%7)(%13, %18))
-│ %20 = Main.one::Core.Const(one)
-│ %21 = Main.SymmetricTensor::Core.Const(SymmetricTensor)
-│ %22 = $(Expr(:static_parameter, 1))::Core.Const(Float64)
-│ %23 = Core.apply_type(%21, 4, 3, %22)::Core.Const(SymmetricTensor{4, 3, Float64})
-│ (𝕀ˢʸᵐ = (%20)(%23))
-│ %25 = Main.:+::Core.Const(+)
-│ %26 = Main.:⊗::Core.Const(Tensors.otimes)
-│ %27 = Main.:*::Core.Const(*)
-│ %28 = λ_val::Float64
-│ %29 = I::Core.Const([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])
-│ %30 = (%27)(%28, %29)::SymmetricTensor{2, 3, Float64, 6}
-│ %31 = I::Core.Const([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])
-│ %32 = (%26)(%30, %31)::SymmetricTensor{4, 3, Float64, 36}
-│ %33 = Main.:*::Core.Const(*)
-│ %34 = Main.:*::Core.Const(*)
-│ %35 = μ_val::Float64
-│ %36 = (%34)(2, %35)::Float64
-│ %37 = 𝕀ˢʸᵐ::Core.Const([1.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0;;; 0.0 0.5 0.0; 0.5 0.0 0.0; 0.0 0.0 0.0;;; 0.0 0.0 0.5; 0.0 0.0 0.0; 0.5 0.0 0.0;;;; 0.0 0.5 0.0; 0.5 0.0 0.0; 0.0 0.0 0.0;;; 0.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 0.0;;; 0.0 0.0 0.0; 0.0 0.0 0.5; 0.0 0.5 0.0;;;; 0.0 0.0 0.5; 0.0 0.0 0.0; 0.5 0.0 0.0;;; 0.0 0.0 0.0; 0.0 0.0 0.5; 0.0 0.5 0.0;;; 0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 1.0])
-│ %38 = (%33)(%36, %37)::SymmetricTensor{4, 3, Float64, 36}
-│ (𝔻 = (%25)(%32, %38))
-│ %40 = σ::SymmetricTensor{2, 3, Float64, 6}
-│ %41 = 𝔻::SymmetricTensor{4, 3, Float64, 36}
-│ %42 = Main.NoState::Core.Const(NoState)
-│ %43 = (%42)()::Core.Const(NoState())
-│ %44 = Core.tuple(%40, %41, %43)::Tuple{SymmetricTensor{2, 3, Float64, 6}, SymmetricTensor{4, 3, Float64, 36}, NoState}
-└── return %44
-
-
-2. Linear Elastic (Old Voigt/Dict):
-MethodInstance for compute_stress_old(::LinearElasticOld, ::Vector{Float64}, ::Dict{String, Any}, ::Float64)
- from compute_stress_old(material::LinearElasticOld, ε_vec::Vector{Float64}, state_old::Dict{String, Any}, Δt::Float64) @ Main ~/dev/JuliaFEM.jl/benchmarks/material_models_benchmark.jl:413
-Arguments
- #self#::Core.Const(Main.compute_stress_old)
- material::LinearElasticOld
- ε_vec::Vector{Float64}
- state_old::Dict{String, Any}
- Δt::Float64
-Locals
- σ_vec::Vector{Float64}
- D::Matrix{Float64}
-Body::Tuple{Vector{Float64}, Matrix{Float64}, Dict{String, Any}}
-1 ─ %1 = Main.constitutive_matrix::Core.Const(Main.constitutive_matrix)
-│ (D = (%1)(material))
-│ %3 = Main.:*::Core.Const(*)
-│ %4 = D::Matrix{Float64}
-│ (σ_vec = (%3)(%4, ε_vec))
-│ %6 = σ_vec::Vector{Float64}
-│ %7 = D::Matrix{Float64}
-│ %8 = Core.tuple(%6, %7, state_old)::Tuple{Vector{Float64}, Matrix{Float64}, Dict{String, Any}}
-└── return %8
-
-
-3. Neo-Hookean AD (Tensors.jl with automatic differentiation):
-MethodInstance for compute_stress(::NeoHookeanAD, ::SymmetricTensor{2, 3, Float64, 6}, ::NoState, ::Float64)
- from compute_stress(material::NeoHookeanAD, E::SymmetricTensor{2, 3, T}, state_old::NoState, Δt::Float64) where T @ Main ~/dev/JuliaFEM.jl/benchmarks/material_models_benchmark.jl:147
-Static Parameters
- T = Float64
-Arguments
- #self#::Core.Const(Main.compute_stress)
- material::NeoHookeanAD
- E::SymmetricTensor{2, 3, Float64, 6}
- state_old::Core.Const(NoState())
- Δt::Float64
-Locals
- @_6::Int64
- S::SymmetricTensor{2, 3, Float64, 6}
- 𝔻::SymmetricTensor{4, 3, Float64, 36}
- ψ::var"#ψ#compute_stress##0"{NeoHookeanAD}
- C::SymmetricTensor{2, 3, Float64, 6}
- I::SymmetricTensor{2, 3, Float64, 6}
-Body::Tuple{SymmetricTensor{2, 3, Float64, 6}, SymmetricTensor{4, 3, Float64, 36}, NoState}
-1 ─ %1 = Main.one::Core.Const(one)
-│ (I = (%1)(E))
-│ %3 = Main.:+::Core.Const(+)
-│ %4 = Main.:*::Core.Const(*)
-│ %5 = (%4)(2, E)::SymmetricTensor{2, 3, Float64, 6}
-│ %6 = I::Core.Const([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])
-│ (C = (%3)(%5, %6))
-│ %8 = Main.:(var"#ψ#compute_stress##0")::Core.Const(var"#ψ#compute_stress##0")
-│ %9 = Core._typeof_captured_variable(material)::Core.Const(NeoHookeanAD)
-│ %10 = Core.apply_type(%8, %9)::Core.Const(var"#ψ#compute_stress##0"{NeoHookeanAD})
-│ (ψ = %new(%10, material))
-│ %12 = Main.hessian::Core.Const(Tensors.hessian)
-│ %13 = ψ::var"#ψ#compute_stress##0"{NeoHookeanAD}
-│ %14 = C::SymmetricTensor{2, 3, Float64, 6}
-│ %15 = (%12)(%13, %14, :all)::Tuple{SymmetricTensor{4, 3, Float64, 36}, SymmetricTensor{2, 3, Float64, 6}, Float64}
-│ %16 = Base.indexed_iterate(%15, 1)::Core.PartialStruct(Tuple{SymmetricTensor{4, 3, Float64, 36}, Int64}, Any[SymmetricTensor{4, 3, Float64, 36}, Core.Const(2)])
-│ (𝔻 = Core.getfield(%16, 1))
-│ (@_6 = Core.getfield(%16, 2))
-│ %19 = @_6::Core.Const(2)
-│ %20 = Base.indexed_iterate(%15, 2, %19)::Core.PartialStruct(Tuple{SymmetricTensor{2, 3, Float64, 6}, Int64}, Any[SymmetricTensor{2, 3, Float64, 6}, Core.Const(3)])
-│ (S = Core.getfield(%20, 1))
-│ %22 = Main.:*::Core.Const(*)
-│ %23 = S::SymmetricTensor{2, 3, Float64, 6}
-│ (S = (%22)(2, %23))
-│ %25 = Main.:*::Core.Const(*)
-│ %26 = 𝔻::SymmetricTensor{4, 3, Float64, 36}
-│ (𝔻 = (%25)(4, %26))
-│ %28 = S::SymmetricTensor{2, 3, Float64, 6}
-│ %29 = 𝔻::SymmetricTensor{4, 3, Float64, 36}
-│ %30 = Main.NoState::Core.Const(NoState)
-│ %31 = (%30)()::Core.Const(NoState())
-│ %32 = Core.tuple(%28, %29, %31)::Tuple{SymmetricTensor{2, 3, Float64, 6}, SymmetricTensor{4, 3, Float64, 36}, NoState}
-└── return %32
-
-
-4. Neo-Hookean Manual (Tensors.jl with hand-coded derivatives):
-MethodInstance for compute_stress(::NeoHookeanManual, ::SymmetricTensor{2, 3, Float64, 6}, ::NoState, ::Float64)
- from compute_stress(material::NeoHookeanManual, E::SymmetricTensor{2, 3, T}, state_old::NoState, Δt::Float64) where T @ Main ~/dev/JuliaFEM.jl/benchmarks/material_models_benchmark.jl:203
-Static Parameters
- T = Float64
-Arguments
- #self#::Core.Const(Main.compute_stress)
- material::NeoHookeanManual
- E::SymmetricTensor{2, 3, Float64, 6}
- state_old::Core.Const(NoState())
- Δt::Float64
-Locals
- 𝔻::SymmetricTensor{4, 3, Float64, 36}
- 𝔻₂::SymmetricTensor{4, 3, Float64, 36}
- coeff::Float64
- 𝕀ˢʸᵐ::SymmetricTensor{4, 3, Float64, 36}
- 𝔻₁::SymmetricTensor{4, 3, Float64, 36}
- S::SymmetricTensor{2, 3, Float64, 6}
- C_inv::SymmetricTensor{2, 3, Float64, 6}
- J::Float64
- C::SymmetricTensor{2, 3, Float64, 6}
- I::SymmetricTensor{2, 3, Float64, 6}
- λ::Float64
- μ::Float64
-Body::Tuple{SymmetricTensor{2, 3, Float64, 6}, SymmetricTensor{4, 3, Float64, 36}, NoState}
-1 ─ %1 = Base.getproperty(material, :μ)::Float64
-│ %2 = Base.getproperty(material, :λ)::Float64
-│ (μ = %1)
-│ (λ = %2)
-│ %5 = Main.one::Core.Const(one)
-│ (I = (%5)(E))
-│ %7 = Main.:+::Core.Const(+)
-│ %8 = Main.:*::Core.Const(*)
-│ %9 = (%8)(2, E)::SymmetricTensor{2, 3, Float64, 6}
-│ %10 = I::Core.Const([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])
-│ (C = (%7)(%9, %10))
-│ %12 = Main.:√::Core.Const(sqrt)
-│ %13 = Main.det::Core.Const(LinearAlgebra.det)
-│ %14 = C::SymmetricTensor{2, 3, Float64, 6}
-│ %15 = (%13)(%14)::Float64
-│ (J = (%12)(%15))
-│ %17 = Main.inv::Core.Const(inv)
-│ %18 = C::SymmetricTensor{2, 3, Float64, 6}
-│ (C_inv = (%17)(%18))
-│ %20 = Main.:+::Core.Const(+)
-│ %21 = Main.:*::Core.Const(*)
-│ %22 = μ::Float64
-│ %23 = Main.:-::Core.Const(-)
-│ %24 = I::Core.Const([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])
-│ %25 = C_inv::SymmetricTensor{2, 3, Float64, 6}
-│ %26 = (%23)(%24, %25)::SymmetricTensor{2, 3, Float64, 6}
-│ %27 = (%21)(%22, %26)::SymmetricTensor{2, 3, Float64, 6}
-│ %28 = Main.:*::Core.Const(*)
-│ %29 = λ::Float64
-│ %30 = Main.log::Core.Const(log)
-│ %31 = J::Float64
-│ %32 = (%30)(%31)::Float64
-│ %33 = C_inv::SymmetricTensor{2, 3, Float64, 6}
-│ %34 = (%28)(%29, %32, %33)::SymmetricTensor{2, 3, Float64, 6}
-│ (S = (%20)(%27, %34))
-│ %36 = Main.:*::Core.Const(*)
-│ %37 = λ::Float64
-│ %38 = Main.:⊗::Core.Const(Tensors.otimes)
-│ %39 = C_inv::SymmetricTensor{2, 3, Float64, 6}
-│ %40 = C_inv::SymmetricTensor{2, 3, Float64, 6}
-│ %41 = (%38)(%39, %40)::SymmetricTensor{4, 3, Float64, 36}
-│ (𝔻₁ = (%36)(%37, %41))
-│ %43 = Main.one::Core.Const(one)
-│ %44 = Main.SymmetricTensor::Core.Const(SymmetricTensor)
-│ %45 = $(Expr(:static_parameter, 1))::Core.Const(Float64)
-│ %46 = Core.apply_type(%44, 4, 3, %45)::Core.Const(SymmetricTensor{4, 3, Float64})
-│ (𝕀ˢʸᵐ = (%43)(%46))
-│ %48 = Main.:*::Core.Const(*)
-│ %49 = Main.:-::Core.Const(-)
-│ %50 = μ::Float64
-│ %51 = Main.:*::Core.Const(*)
-│ %52 = λ::Float64
-│ %53 = Main.log::Core.Const(log)
-│ %54 = J::Float64
-│ %55 = (%53)(%54)::Float64
-│ %56 = (%51)(%52, %55)::Float64
-│ %57 = (%49)(%50, %56)::Float64
-│ (coeff = (%48)(2, %57))
-│ %59 = Main.:*::Core.Const(*)
-│ %60 = Main.:-::Core.Const(-)
-│ %61 = coeff::Float64
-│ %62 = (%60)(%61)::Float64
-│ %63 = Main.inv_symmetric_outer::Core.Const(Main.inv_symmetric_outer)
-│ %64 = C_inv::SymmetricTensor{2, 3, Float64, 6}
-│ %65 = (%63)(%64)::SymmetricTensor{4, 3, Float64, 36}
-│ (𝔻₂ = (%59)(%62, %65))
-│ %67 = Main.:+::Core.Const(+)
-│ %68 = 𝔻₁::SymmetricTensor{4, 3, Float64, 36}
-│ %69 = 𝔻₂::SymmetricTensor{4, 3, Float64, 36}
-│ (𝔻 = (%67)(%68, %69))
-│ %71 = S::SymmetricTensor{2, 3, Float64, 6}
-│ %72 = 𝔻::SymmetricTensor{4, 3, Float64, 36}
-│ %73 = Main.NoState::Core.Const(NoState)
-│ %74 = (%73)()::Core.Const(NoState())
-│ %75 = Core.tuple(%71, %72, %74)::Tuple{SymmetricTensor{2, 3, Float64, 6}, SymmetricTensor{4, 3, Float64, 36}, NoState}
-└── return %75
-
-
-5. Perfect Plasticity (Tensors.jl):
-MethodInstance for compute_stress(::PerfectPlasticity, ::SymmetricTensor{2, 3, Float64, 6}, ::PlasticityState{Float64}, ::Float64)
- from compute_stress(material::PerfectPlasticity, ε::SymmetricTensor{2, 3, T}, state_old::PlasticityState{T}, Δt::Float64) where T @ Main ~/dev/JuliaFEM.jl/benchmarks/material_models_benchmark.jl:328
-Static Parameters
- T = Float64
-Arguments
- #self#::Core.Const(Main.compute_stress)
- material::PerfectPlasticity
- ε::SymmetricTensor{2, 3, Float64, 6}
- state_old::PlasticityState{Float64}
- Δt::Float64
-Locals
- 𝔻::SymmetricTensor{4, 3, Float64, 36}
- β::Float64
- θ::Float64
- state_new::PlasticityState{Float64}
- α_new::Float64
- ε_p_new::SymmetricTensor{2, 3, Float64, 6}
- n::SymmetricTensor{2, 3, Float64, 6}
- Δγ::Float64
- σ::SymmetricTensor{2, 3, Float64, 6}
- p::Float64
- s_trial::SymmetricTensor{2, 3, Float64, 6}
- f::Float64
- σ_eq_trial::Float64
- σ_trial::SymmetricTensor{2, 3, Float64, 6}
- ε_e::SymmetricTensor{2, 3, Float64, 6}
- 𝔻ᵉ::SymmetricTensor{4, 3, Float64, 36}
- 𝕀ˢʸᵐ::SymmetricTensor{4, 3, Float64, 36}
- I::SymmetricTensor{2, 3, Float64, 6}
- σ_y::Float64
- μ_val::Float64
- λ_val::Float64
-Body::Tuple{SymmetricTensor{2, 3, Float64, 6}, SymmetricTensor{4, 3, Float64, 36}, PlasticityState{Float64}}
-1 ─ Core.NewvarNode(:(𝔻))
-│ Core.NewvarNode(:(β))
-│ Core.NewvarNode(:(θ))
-│ Core.NewvarNode(:(state_new))
-│ Core.NewvarNode(:(α_new))
-│ Core.NewvarNode(:(ε_p_new))
-│ Core.NewvarNode(:(n))
-│ Core.NewvarNode(:(Δγ))
-│ Core.NewvarNode(:(σ))
-│ Core.NewvarNode(:(p))
-│ Core.NewvarNode(:(s_trial))
-│ %12 = Main.λ::Core.Const(Main.λ)
-│ (λ_val = (%12)(material))
-│ %14 = Main.μ::Core.Const(Main.μ)
-│ (μ_val = (%14)(material))
-│ (σ_y = Base.getproperty(material, :σ_y))
-│ %17 = Main.one::Core.Const(one)
-│ (I = (%17)(ε))
-│ %19 = Main.one::Core.Const(one)
-│ %20 = Main.SymmetricTensor::Core.Const(SymmetricTensor)
-│ %21 = $(Expr(:static_parameter, 1))::Core.Const(Float64)
-│ %22 = Core.apply_type(%20, 4, 3, %21)::Core.Const(SymmetricTensor{4, 3, Float64})
-│ (𝕀ˢʸᵐ = (%19)(%22))
-│ %24 = Main.:+::Core.Const(+)
-│ %25 = Main.:⊗::Core.Const(Tensors.otimes)
-│ %26 = Main.:*::Core.Const(*)
-│ %27 = λ_val::Float64
-│ %28 = I::Core.Const([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])
-│ %29 = (%26)(%27, %28)::SymmetricTensor{2, 3, Float64, 6}
-│ %30 = I::Core.Const([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])
-│ %31 = (%25)(%29, %30)::SymmetricTensor{4, 3, Float64, 36}
-│ %32 = Main.:*::Core.Const(*)
-│ %33 = Main.:*::Core.Const(*)
-│ %34 = μ_val::Float64
-│ %35 = (%33)(2, %34)::Float64
-│ %36 = 𝕀ˢʸᵐ::Core.Const([1.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0;;; 0.0 0.5 0.0; 0.5 0.0 0.0; 0.0 0.0 0.0;;; 0.0 0.0 0.5; 0.0 0.0 0.0; 0.5 0.0 0.0;;;; 0.0 0.5 0.0; 0.5 0.0 0.0; 0.0 0.0 0.0;;; 0.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 0.0;;; 0.0 0.0 0.0; 0.0 0.0 0.5; 0.0 0.5 0.0;;;; 0.0 0.0 0.5; 0.0 0.0 0.0; 0.5 0.0 0.0;;; 0.0 0.0 0.0; 0.0 0.0 0.5; 0.0 0.5 0.0;;; 0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 1.0])
-│ %37 = (%32)(%35, %36)::SymmetricTensor{4, 3, Float64, 36}
-│ (𝔻ᵉ = (%24)(%31, %37))
-│ %39 = Main.:-::Core.Const(-)
-│ %40 = Base.getproperty(state_old, :ε_p)::SYMMETRICTENSOR{2, 3, FLOAT64}
-│ (ε_e = (%39)(ε, %40))
-│ %42 = Main.:+::Core.Const(+)
-│ %43 = Main.:*::Core.Const(*)
-│ %44 = λ_val::Float64
-│ %45 = Main.tr::Core.Const(LinearAlgebra.tr)
-│ %46 = ε_e::SymmetricTensor{2, 3, Float64, 6}
-│ %47 = (%45)(%46)::Float64
-│ %48 = I::Core.Const([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])
-│ %49 = (%43)(%44, %47, %48)::SymmetricTensor{2, 3, Float64, 6}
-│ %50 = Main.:*::Core.Const(*)
-│ %51 = Main.:*::Core.Const(*)
-│ %52 = μ_val::Float64
-│ %53 = (%51)(2, %52)::Float64
-│ %54 = ε_e::SymmetricTensor{2, 3, Float64, 6}
-│ %55 = (%50)(%53, %54)::SymmetricTensor{2, 3, Float64, 6}
-│ (σ_trial = (%42)(%49, %55))
-│ %57 = Main.von_mises_stress::Core.Const(Main.von_mises_stress)
-│ %58 = σ_trial::SymmetricTensor{2, 3, Float64, 6}
-│ (σ_eq_trial = (%57)(%58))
-│ %60 = Main.:-::Core.Const(-)
-│ %61 = σ_eq_trial::Float64
-│ %62 = σ_y::Float64
-│ (f = (%60)(%61, %62))
-│ %64 = Main.:≤::Core.Const(<=)
-│ %65 = f::Float64
-│ %66 = (%64)(%65, 0.0)::Bool
-└── goto #3 if not %66
-2 ─ %68 = σ_trial::SymmetricTensor{2, 3, Float64, 6}
-│ (σ = %68)
-│ %70 = 𝔻ᵉ::SymmetricTensor{4, 3, Float64, 36}
-│ (𝔻 = %70)
-│ %72 = state_old::PlasticityState{Float64}
-│ (state_new = %72)
-└── goto #4
-3 ─ %75 = Main.dev::Core.Const(Tensors.dev)
-│ %76 = σ_trial::SymmetricTensor{2, 3, Float64, 6}
-│ (s_trial = (%75)(%76))
-│ %78 = Main.:/::Core.Const(/)
-│ %79 = Main.tr::Core.Const(LinearAlgebra.tr)
-│ %80 = σ_trial::SymmetricTensor{2, 3, Float64, 6}
-│ %81 = (%79)(%80)::Float64
-│ (p = (%78)(%81, 3))
-│ %83 = Main.:+::Core.Const(+)
-│ %84 = Main.:*::Core.Const(*)
-│ %85 = p::Float64
-│ %86 = I::Core.Const([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0])
-│ %87 = (%84)(%85, %86)::SymmetricTensor{2, 3, Float64, 6}
-│ %88 = Main.:*::Core.Const(*)
-│ %89 = Main.:/::Core.Const(/)
-│ %90 = σ_y::Float64
-│ %91 = σ_eq_trial::Float64
-│ %92 = (%89)(%90, %91)::Float64
-│ %93 = s_trial::SymmetricTensor{2, 3, Float64, 6}
-│ %94 = (%88)(%92, %93)::SymmetricTensor{2, 3, Float64, 6}
-│ (σ = (%83)(%87, %94))
-│ %96 = Main.:/::Core.Const(/)
-│ %97 = f::Float64
-│ %98 = Main.:*::Core.Const(*)
-│ %99 = μ_val::Float64
-│ %100 = (%98)(3, %99)::Float64
-│ (Δγ = (%96)(%97, %100))
-│ %102 = Main.:/::Core.Const(/)
-│ %103 = Main.:*::Core.Const(*)
-│ %104 = Main.:√::Core.Const(sqrt)
-│ %105 = Main.:/::Core.Const(/)
-│ %106 = (%105)(3, 2)::Core.Const(1.5)
-│ %107 = (%104)(%106)::Core.Const(1.224744871391589)
-│ %108 = s_trial::SymmetricTensor{2, 3, Float64, 6}
-│ %109 = (%103)(%107, %108)::SymmetricTensor{2, 3, Float64, 6}
-│ %110 = σ_eq_trial::Float64
-│ (n = (%102)(%109, %110))
-│ %112 = Main.:+::Core.Const(+)
-│ %113 = Base.getproperty(state_old, :ε_p)::SYMMETRICTENSOR{2, 3, FLOAT64}
-│ %114 = Main.:*::Core.Const(*)
-│ %115 = Δγ::Float64
-│ %116 = n::SymmetricTensor{2, 3, Float64, 6}
-│ %117 = (%114)(%115, %116)::SymmetricTensor{2, 3, Float64, 6}
-│ (ε_p_new = (%112)(%113, %117))
-│ %119 = Main.:+::Core.Const(+)
-│ %120 = Base.getproperty(state_old, :α)::Float64
-│ %121 = Δγ::Float64
-│ (α_new = (%119)(%120, %121))
-│ %123 = Main.PlasticityState::Core.Const(PlasticityState)
-│ %124 = ε_p_new::SymmetricTensor{2, 3, Float64, 6}
-│ %125 = α_new::Float64
-│ (state_new = (%123)(%124, %125))
-│ %127 = Main.:-::Core.Const(-)
-│ %128 = Main.:/::Core.Const(/)
-│ %129 = σ_y::Float64
-│ %130 = σ_eq_trial::Float64
-│ %131 = (%128)(%129, %130)::Float64
-│ (θ = (%127)(1, %131))
-│ %133 = Main.:/::Core.Const(/)
-│ %134 = Main.:*::Core.Const(*)
-│ %135 = Main.:^::Core.Const(^)
-│ %136 = μ_val::Float64
-│ %137 = Core.apply_type(Base.Val, 2)::Core.Const(Val{2})
-│ %138 = (%137)()::Core.Const(Val{2}())
-│ %139 = Base.literal_pow(%135, %136, %138)::Float64
-│ %140 = (%134)(6, %139)::Float64
-│ %141 = Main.:+::Core.Const(+)
-│ %142 = Main.:*::Core.Const(*)
-│ %143 = μ_val::Float64
-│ %144 = (%142)(3, %143)::Float64
-│ %145 = Main.:*::Core.Const(*)
-│ %146 = θ::Float64
-│ %147 = Main.:*::Core.Const(*)
-│ %148 = μ_val::Float64
-│ %149 = (%147)(3, %148)::Float64
-│ %150 = (%145)(%146, %149)::Float64
-│ %151 = (%141)(%144, %150)::Float64
-│ (β = (%133)(%140, %151))
-│ %153 = Main.:-::Core.Const(-)
-│ %154 = 𝔻ᵉ::SymmetricTensor{4, 3, Float64, 36}
-│ %155 = Main.:*::Core.Const(*)
-│ %156 = β::Float64
-│ %157 = Main.:⊗::Core.Const(Tensors.otimes)
-│ %158 = n::SymmetricTensor{2, 3, Float64, 6}
-│ %159 = n::SymmetricTensor{2, 3, Float64, 6}
-│ %160 = (%157)(%158, %159)::SymmetricTensor{4, 3, Float64, 36}
-│ %161 = (%155)(%156, %160)::SymmetricTensor{4, 3, Float64, 36}
-└── (𝔻 = (%153)(%154, %161))
-4 ┄ %163 = σ::SymmetricTensor{2, 3, Float64, 6}
-│ %164 = 𝔻::SymmetricTensor{4, 3, Float64, 36}
-│ %165 = state_new::PlasticityState{Float64}
-│ %166 = Core.tuple(%163, %164, %165)::Tuple{SymmetricTensor{2, 3, Float64, 6}, SymmetricTensor{4, 3, Float64, 36}, PlasticityState{Float64}}
-└── return %166
-
-
-6. Perfect Plasticity (Old Dict):
-MethodInstance for compute_stress_old(::PerfectPlasticityOld, ::Vector{Float64}, ::Dict{String, Any}, ::Float64)
- from compute_stress_old(material::PerfectPlasticityOld, ε_vec::Vector{Float64}, state_old::Dict{String, Any}, Δt::Float64) @ Main ~/dev/JuliaFEM.jl/benchmarks/material_models_benchmark.jl:455
-Arguments
- #self#::Core.Const(Main.compute_stress_old)
- material::PerfectPlasticityOld
- ε_vec::Vector{Float64}
- state_old::Dict{String, Any}
- Δt::Float64
-Locals
- @_6::ANY
- @_7::ANY
- σ_vec::ANY
- n_vec::ANY
- Δγ::ANY
- factor::ANY
- state_new::Dict{String, Any}
- f::ANY
- σ_eq::ANY
- dev_vec::ANY
- p::ANY
- s13::ANY
- s23::ANY
- s12::ANY
- s33::ANY
- s22::ANY
- s11::ANY
- σ_trial_vec::ANY
- ε_e_vec::ANY
- D::Matrix{Float64}
- ε_p_vec::ANY
-Body::TUPLE{ANY, MATRIX{FLOAT64}, DICT{STRING, ANY}}
-1 ─ Core.NewvarNode(:(@_6))
-│ Core.NewvarNode(:(@_7))
-│ Core.NewvarNode(:(σ_vec))
-│ Core.NewvarNode(:(n_vec))
-│ Core.NewvarNode(:(Δγ))
-│ Core.NewvarNode(:(factor))
-│ Core.NewvarNode(:(state_new))
-│ Core.NewvarNode(:(f))
-│ Core.NewvarNode(:(σ_eq))
-│ Core.NewvarNode(:(dev_vec))
-│ Core.NewvarNode(:(p))
-│ Core.NewvarNode(:(s13))
-│ Core.NewvarNode(:(s23))
-│ Core.NewvarNode(:(s12))
-│ Core.NewvarNode(:(s33))
-│ Core.NewvarNode(:(s22))
-│ Core.NewvarNode(:(s11))
-│ Core.NewvarNode(:(σ_trial_vec))
-│ Core.NewvarNode(:(ε_e_vec))
-│ Core.NewvarNode(:(D))
-│ Core.NewvarNode(:(ε_p_vec))
-│ %22 = Main.haskey::Core.Const(haskey)
-│ %23 = (%22)(state_old, "epsilon_plastic")::Bool
-└── goto #3 if not %23
-2 ─ (ε_p_vec = Base.getindex(state_old, "epsilon_plastic"))
-└── goto #4
-3 ─ %27 = Main.zeros::Core.Const(zeros)
-└── (ε_p_vec = (%27)(6))
-4 ┄ %29 = Main.constitutive_matrix::Core.Const(Main.constitutive_matrix)
-│ %30 = Main.LinearElasticOld::Core.Const(LinearElasticOld)
-│ %31 = Base.getproperty(material, :E)::Float64
-│ %32 = Base.getproperty(material, :ν)::Float64
-│ %33 = (%30)(%31, %32)::LinearElasticOld
-│ (D = (%29)(%33))
-│ %35 = Main.:-::Core.Const(-)
-│ %36 = ε_p_vec::ANY
-│ (ε_e_vec = (%35)(ε_vec, %36))
-│ %38 = Main.:*::Core.Const(*)
-│ %39 = D::Matrix{Float64}
-│ %40 = ε_e_vec::ANY
-│ (σ_trial_vec = (%38)(%39, %40))
-│ %42 = σ_trial_vec::ANY
-│ %43 = Main.:(:)::Core.Const(Colon())
-│ %44 = (%43)(1, 3)::Core.Const(1:3)
-│ %45 = Base.getindex(%42, %44)::ANY
-│ %46 = Base.indexed_iterate(%45, 1)::ANY
-│ (s11 = Core.getfield(%46, 1))
-│ (@_7 = Core.getfield(%46, 2))
-│ %49 = @_7::ANY
-│ %50 = Base.indexed_iterate(%45, 2, %49)::ANY
-│ (s22 = Core.getfield(%50, 1))
-│ (@_7 = Core.getfield(%50, 2))
-│ %53 = @_7::ANY
-│ %54 = Base.indexed_iterate(%45, 3, %53)::ANY
-│ (s33 = Core.getfield(%54, 1))
-│ %56 = σ_trial_vec::ANY
-│ %57 = Main.:(:)::Core.Const(Colon())
-│ %58 = (%57)(4, 6)::Core.Const(4:6)
-│ %59 = Base.getindex(%56, %58)::ANY
-│ %60 = Base.indexed_iterate(%59, 1)::ANY
-│ (s12 = Core.getfield(%60, 1))
-│ (@_6 = Core.getfield(%60, 2))
-│ %63 = @_6::ANY
-│ %64 = Base.indexed_iterate(%59, 2, %63)::ANY
-│ (s23 = Core.getfield(%64, 1))
-│ (@_6 = Core.getfield(%64, 2))
-│ %67 = @_6::ANY
-│ %68 = Base.indexed_iterate(%59, 3, %67)::ANY
-│ (s13 = Core.getfield(%68, 1))
-│ %70 = Main.:/::Core.Const(/)
-│ %71 = Main.:+::Core.Const(+)
-│ %72 = s11::ANY
-│ %73 = s22::ANY
-│ %74 = s33::ANY
-│ %75 = (%71)(%72, %73, %74)::ANY
-│ (p = (%70)(%75, 3))
-│ %77 = Main.:-::Core.Const(-)
-│ %78 = s11::ANY
-│ %79 = p::ANY
-│ %80 = (%77)(%78, %79)::ANY
-│ %81 = Main.:-::Core.Const(-)
-│ %82 = s22::ANY
-│ %83 = p::ANY
-│ %84 = (%81)(%82, %83)::ANY
-│ %85 = Main.:-::Core.Const(-)
-│ %86 = s33::ANY
-│ %87 = p::ANY
-│ %88 = (%85)(%86, %87)::ANY
-│ %89 = s12::ANY
-│ %90 = s23::ANY
-│ %91 = s13::ANY
-│ (dev_vec = Base.vect(%80, %84, %88, %89, %90, %91))
-│ %93 = Main.:√::Core.Const(sqrt)
-│ %94 = Main.:*::Core.Const(*)
-│ %95 = Main.:/::Core.Const(/)
-│ %96 = (%95)(3, 2)::Core.Const(1.5)
-│ %97 = Main.:+::Core.Const(+)
-│ %98 = Main.:^::Core.Const(^)
-│ %99 = dev_vec::ANY
-│ %100 = Base.getindex(%99, 1)::ANY
-│ %101 = Core.apply_type(Base.Val, 2)::Core.Const(Val{2})
-│ %102 = (%101)()::Core.Const(Val{2}())
-│ %103 = Base.literal_pow(%98, %100, %102)::ANY
-│ %104 = Main.:^::Core.Const(^)
-│ %105 = dev_vec::ANY
-│ %106 = Base.getindex(%105, 2)::ANY
-│ %107 = Core.apply_type(Base.Val, 2)::Core.Const(Val{2})
-│ %108 = (%107)()::Core.Const(Val{2}())
-│ %109 = Base.literal_pow(%104, %106, %108)::ANY
-│ %110 = Main.:^::Core.Const(^)
-│ %111 = dev_vec::ANY
-│ %112 = Base.getindex(%111, 3)::ANY
-│ %113 = Core.apply_type(Base.Val, 2)::Core.Const(Val{2})
-│ %114 = (%113)()::Core.Const(Val{2}())
-│ %115 = Base.literal_pow(%110, %112, %114)::ANY
-│ %116 = Main.:*::Core.Const(*)
-│ %117 = Main.:+::Core.Const(+)
-│ %118 = Main.:^::Core.Const(^)
-│ %119 = dev_vec::ANY
-│ %120 = Base.getindex(%119, 4)::ANY
-│ %121 = Core.apply_type(Base.Val, 2)::Core.Const(Val{2})
-│ %122 = (%121)()::Core.Const(Val{2}())
-│ %123 = Base.literal_pow(%118, %120, %122)::ANY
-│ %124 = Main.:^::Core.Const(^)
-│ %125 = dev_vec::ANY
-│ %126 = Base.getindex(%125, 5)::ANY
-│ %127 = Core.apply_type(Base.Val, 2)::Core.Const(Val{2})
-│ %128 = (%127)()::Core.Const(Val{2}())
-│ %129 = Base.literal_pow(%124, %126, %128)::ANY
-│ %130 = Main.:^::Core.Const(^)
-│ %131 = dev_vec::ANY
-│ %132 = Base.getindex(%131, 6)::ANY
-│ %133 = Core.apply_type(Base.Val, 2)::Core.Const(Val{2})
-│ %134 = (%133)()::Core.Const(Val{2}())
-│ %135 = Base.literal_pow(%130, %132, %134)::ANY
-│ %136 = (%117)(%123, %129, %135)::ANY
-│ %137 = (%116)(2, %136)::ANY
-│ %138 = (%97)(%103, %109, %115, %137)::ANY
-│ %139 = (%94)(%96, %138)::ANY
-│ (σ_eq = (%93)(%139))
-│ %141 = Main.:-::Core.Const(-)
-│ %142 = σ_eq::ANY
-│ %143 = Base.getproperty(material, :σ_y)::Float64
-│ (f = (%141)(%142, %143))
-│ %145 = Main.copy::Core.Const(copy)
-│ (state_new = (%145)(state_old))
-│ %147 = Main.:>::Core.Const(>)
-│ %148 = f::ANY
-│ %149 = (%147)(%148, 0.0)::ANY
-└── goto #6 if not %149
-5 ─ %151 = Main.:/::Core.Const(/)
-│ %152 = Base.getproperty(material, :σ_y)::Float64
-│ %153 = σ_eq::ANY
-│ (factor = (%151)(%152, %153))
-│ %155 = Main.:+::Core.Const(+)
-│ %156 = p::ANY
-│ %157 = p::ANY
-│ %158 = p::ANY
-│ %159 = Base.vect(%156, %157, %158, 0.0, 0.0, 0.0)::ANY
-│ %160 = Main.:*::Core.Const(*)
-│ %161 = factor::ANY
-│ %162 = dev_vec::ANY
-│ %163 = (%160)(%161, %162)::ANY
-│ (σ_vec = (%155)(%159, %163))
-│ %165 = Main.:/::Core.Const(/)
-│ %166 = f::ANY
-│ %167 = Main.:/::Core.Const(/)
-│ %168 = Main.:*::Core.Const(*)
-│ %169 = Base.getproperty(material, :E)::Float64
-│ %170 = (%168)(3, %169)::Float64
-│ %171 = Main.:*::Core.Const(*)
-│ %172 = Main.:+::Core.Const(+)
-│ %173 = Base.getproperty(material, :ν)::Float64
-│ %174 = (%172)(1, %173)::Float64
-│ %175 = (%171)(2, %174)::Float64
-│ %176 = (%167)(%170, %175)::Float64
-│ (Δγ = (%165)(%166, %176))
-│ %178 = Main.:/::Core.Const(/)
-│ %179 = Main.:*::Core.Const(*)
-│ %180 = Main.:√::Core.Const(sqrt)
-│ %181 = Main.:/::Core.Const(/)
-│ %182 = (%181)(3, 2)::Core.Const(1.5)
-│ %183 = (%180)(%182)::Core.Const(1.224744871391589)
-│ %184 = dev_vec::ANY
-│ %185 = (%179)(%183, %184)::ANY
-│ %186 = σ_eq::ANY
-│ (n_vec = (%178)(%185, %186))
-│ %188 = Main.:+::Core.Const(+)
-│ %189 = ε_p_vec::ANY
-│ %190 = Main.:*::Core.Const(*)
-│ %191 = Δγ::ANY
-│ %192 = n_vec::ANY
-│ %193 = (%190)(%191, %192)::ANY
-│ %194 = (%188)(%189, %193)::ANY
-│ %195 = state_new::Dict{String, Any}
-│ Base.setindex!(%195, %194, "epsilon_plastic")
-└── goto #7
-6 ─ %198 = σ_trial_vec::ANY
-└── (σ_vec = %198)
-7 ┄ %200 = σ_vec::ANY
-│ %201 = D::Matrix{Float64}
-│ %202 = state_new::Dict{String, Any}
-│ %203 = Core.tuple(%200, %201, %202)::TUPLE{ANY, MATRIX{FLOAT64}, DICT{STRING, ANY}}
-└── return %203
-
-
-================================================================================
-ALLOCATION TESTS
-================================================================================
-
-Testing for allocations (should be 0 for new approach)...
-
-1. Linear Elastic
- NEW (Tensors.jl):
- Allocations: 0 bytes
- OLD (Voigt/Dict):
- Allocations: 496 bytes
-
-2. Neo-Hookean
- NEW (Tensors.jl + AD):
- Allocations: 0 bytes
- NEW (Tensors.jl + Manual):
- Allocations: 0 bytes
- OLD (Array):
- Allocations: 496 bytes
-
-3. Perfect Plasticity (elastic branch)
- NEW (Tensors.jl):
- Allocations: 0 bytes
- OLD (Dict):
- Allocations: 8828848 bytes
-
-================================================================================
-PERFORMANCE BENCHMARKS
-================================================================================
-
-Running detailed benchmarks (this may take a minute)...
-
-1. LINEAR ELASTIC
-----------------------------------------
-NEW (Tensors.jl):
-BenchmarkTools.Trial: 10000 samples with 997 evaluations per sample.
- Range (min … max): 19.464 ns … 45.831 ns ┊ GC (min … max): 0.00% … 0.00%
- Time (median): 19.577 ns ┊ GC (median): 0.00%
- Time (mean ± σ): 19.670 ns ± 0.675 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
-
- ▁█▄
- ███▇▄▃▂▂▂▂▂▁▁▂▂▂▂▂▂▂▂▂▂▂▂▁▁▁▂▁▁▁▂▁▁▁▂▂▁▁▁▁▂▁▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂ ▂
- 19.5 ns Histogram: frequency by time 22.8 ns <
-
- Memory estimate: 0 bytes, allocs estimate: 0.
-
-OLD (Voigt/Dict):
-BenchmarkTools.Trial: 10000 samples with 950 evaluations per sample.
- Range (min … max): 93.356 ns … 8.107 μs ┊ GC (min … max): 0.00% … 97.34%
- Time (median): 100.107 ns ┊ GC (median): 0.00%
- Time (mean ± σ): 139.733 ns ± 249.840 ns ┊ GC (mean ± σ): 25.28% ± 13.73%
-
- █▂ ▁ ▁
- ██▄▄██▁▁▁▁▁▁▁▁▁▁▁▁▁▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▃▆▇▇▄▃▄▅▄▄▅▅▅▅▅▃▆ █
- 93.4 ns Histogram: log(frequency) by time 1.74 μs <
-
- Memory estimate: 496 bytes, allocs estimate: 4.
-
-SPEEDUP: 5.1×
-
-2. NEO-HOOKEAN
-----------------------------------------
-NEW (Tensors.jl + Automatic Differentiation):
-BenchmarkTools.Trial: 10000 samples with 23 evaluations per sample.
- Range (min … max): 1.050 μs … 2.802 μs ┊ GC (min … max): 0.00% … 0.00%
- Time (median): 1.051 μs ┊ GC (median): 0.00%
- Time (mean ± σ): 1.055 μs ± 31.780 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
-
- █
- █▄▂▂▁▂▁▂▂▁▁▁▁▁▁▁▁▁▂▁▁▂▁▁▁▁▁▁▁▁▁▁▁▂▁▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂ ▂
- 1.05 μs Histogram: frequency by time 1.19 μs <
-
- Memory estimate: 0 bytes, allocs estimate: 0.
-
-NEW (Tensors.jl + Manual Derivatives):
-BenchmarkTools.Trial: 10000 samples with 987 evaluations per sample.
- Range (min … max): 49.806 ns … 1.787 μs ┊ GC (min … max): 0.00% … 0.00%
- Time (median): 49.922 ns ┊ GC (median): 0.00%
- Time (mean ± σ): 50.262 ns ± 17.399 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
-
- ▅██▄▁ ▂
- █████▆▄▁▄▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▃▁▁▃▄▄▅▅▆▆▆▆▆▇▇█▇▇▇▇▇██▇▇▆▇▇ █
- 49.8 ns Histogram: log(frequency) by time 53.3 ns <
-
- Memory estimate: 0 bytes, allocs estimate: 0.
-
-OLD (Array):
-BenchmarkTools.Trial: 10000 samples with 955 evaluations per sample.
- Range (min … max): 91.182 ns … 9.723 μs ┊ GC (min … max): 0.00% … 97.56%
- Time (median): 99.922 ns ┊ GC (median): 0.00%
- Time (mean ± σ): 142.795 ns ± 307.090 ns ┊ GC (mean ± σ): 22.77% ± 11.92%
-
- █▃ ▄▁ ▁
- ██▆▄██▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄▇█ █
- 91.2 ns Histogram: log(frequency) by time 1.77 μs <
-
- Memory estimate: 496 bytes, allocs estimate: 4.
-
-SPEEDUP (AD): 0.1×
-SPEEDUP (Manual): 2.0×
-AD OVERHEAD: 21.1× (AD / Manual)
-
-3. PERFECT PLASTICITY (elastic branch)
-----------------------------------------
-NEW (Tensors.jl):
-BenchmarkTools.Trial: 10000 samples with 976 evaluations per sample.
- Range (min … max): 69.677 ns … 151.814 ns ┊ GC (min … max): 0.00% … 0.00%
- Time (median): 70.389 ns ┊ GC (median): 0.00%
- Time (mean ± σ): 70.566 ns ± 1.326 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
-
- ▂▆▇▆▇▆▇▇▇█▇▆▇▆▅▃ ▁▁▁▁▁▁▂▁▁▁▁▁ ▃
- ▆███████████████████▇▇▇▆▇▆▆▄▃▁▁▁▁▁▆▅▅▆▇▆▇▇██████████████████ █
- 69.7 ns Histogram: log(frequency) by time 73.9 ns <
-
- Memory estimate: 0 bytes, allocs estimate: 0.
-
-OLD (Dict):
-BenchmarkTools.Trial: 10000 samples with 10 evaluations per sample.
- Range (min … max): 1.371 μs … 998.345 μs ┊ GC (min … max): 0.00% … 99.45%
- Time (median): 1.480 μs ┊ GC (median): 0.00%
- Time (mean ± σ): 1.701 μs ± 9.970 μs ┊ GC (mean ± σ): 5.84% ± 0.99%
-
- ▅█▆▂
- ▁▂▃▆████▆▅▄▃▃▃▃▂▂▂▂▂▁▁▂▁▁▁▁▁▂▂▂▂▂▂▂▂▂▂▃▃▃▃▂▂▂▂▂▂▂▂▂▂▁▁▁▁▁▁▁ ▂
- 1.37 μs Histogram: frequency by time 2.16 μs <
-
- Memory estimate: 1.98 KiB, allocs estimate: 53.
-
-SPEEDUP: 21.0×
-
-================================================================================
-SUMMARY
-================================================================================
-
-ALLOCATIONS:
- LinearElastic: NEW = 0 bytes, OLD = 496 bytes
- NeoHookean (AD): NEW = 0 bytes, OLD = 496 bytes
- NeoHookean (Manual): NEW = 0 bytes
- PerfectPlasticity: NEW = 0 bytes, OLD = 8828848 bytes
-
-MEDIAN TIMING:
- LinearElastic: NEW = 19.576730190571716 ns, OLD = 100.10684210526315 ns
- NeoHookean (AD): NEW = 1051.304347826087 ns, OLD = 99.92198952879582 ns
- NeoHookean (Manual): NEW = 49.92198581560284 ns
- PerfectPlasticity: NEW = 70.38934426229508 ns, OLD = 1479.55 ns
-
-SPEEDUP (OLD / NEW):
- LinearElastic: 5.1×
- NeoHookean (AD): 0.1×
- NeoHookean (Manual): 2.0×
- PerfectPlasticity: 21.0×
-
-AD OVERHEAD:
- NeoHookean: AD is 21.1× slower than manual derivatives
-
-AVERAGE SPEEDUP: 9.4× (using manual Neo-Hookean)
-
-VALIDATION OF CLAIMS:
- - Zero allocations for new approach: ✓ PASS
- - Manual derivatives outperform AD: ✓ PASS
- - Type stability with NoState return: Check @code_warntype output above
-
-================================================================================
-Benchmark complete! Results saved to: material_models_benchmark_results.txt
-================================================================================
diff --git a/benchmarks/matrix_free_gpu_benchmark.jl b/benchmarks/matrix_free_gpu_benchmark.jl
deleted file mode 100644
index 82cc9b9..0000000
--- a/benchmarks/matrix_free_gpu_benchmark.jl
+++ /dev/null
@@ -1,835 +0,0 @@
-"""
-Matrix-Free Newton-Krylov GPU Benchmark
-
-Demonstrates traditional Newton vs Matrix-Free Newton-Krylov with Anderson
-acceleration, running on GPU.
-
-Run with:
- julia --project=. benchmarks/matrix_free_gpu_benchmark.jl
-"""
-
-using CUDA
-using LinearAlgebra
-using IterativeSolvers
-using Printf
-
-# Check GPU availability
-if !CUDA.functional()
- @warn "CUDA not available! Running CPU-only comparison."
- USE_GPU = false
-else
- println("GPU Device: $(CUDA.name(CUDA.device()))")
- println("GPU Memory: $(CUDA.total_memory() / 1e9) GB")
- println()
- USE_GPU = true
-end
-
-# ============================================================================
-# Problem Setup: 3D Nonlinear Elasticity
-# ============================================================================
-
-"""
-Residual for 3D nonlinear elasticity with cubic nonlinearity.
-
- r(u) = K·u + β·(K·u).^3 - f
-
-where K is stiffness matrix, β is nonlinearity parameter.
-"""
-struct NonlinearProblem{T,MatT,VecT}
- K::MatT # Stiffness matrix (sparse or LinearMap)
- f::VecT # Force vector
- β::T # Nonlinearity parameter
- n::Int # DOF count
-end
-
-"""
-Compute residual: r(u) = K·u + β·(K·u).^3 - f
-"""
-function compute_residual!(r::AbstractVector, prob::NonlinearProblem, u::AbstractVector)
- # Linear part
- mul!(r, prob.K, u) # r = K·u
-
- # Nonlinear part: r += β·(K·u).^3
- if prob.β != 0
- # Reuse r (which contains K·u)
- @. r = r + prob.β * r^3
- end
-
- # Apply forcing
- @. r = r - prob.f
-
- return r
-end
-
-"""
-Jacobian-vector product: J·v ≈ [r(u+ε·v) - r(u)] / ε (finite difference)
-"""
-function jacobian_vector_product!(
- Jv::AbstractVector,
- prob::NonlinearProblem,
- u::AbstractVector,
- v::AbstractVector,
- r_u::AbstractVector, # Pre-computed r(u)
- temp::AbstractVector # Workspace
-)
- ε = 1e-7
-
- # temp = u + ε·v
- @. temp = u + ε * v
-
- # Jv = r(u + ε·v)
- compute_residual!(Jv, prob, temp)
-
- # Jv = [r(u + ε·v) - r(u)] / ε
- @. Jv = (Jv - r_u) / ε
-
- return Jv
-end
-
-# ============================================================================
-# Traditional Newton Solver
-# ============================================================================
-
-"""
-Traditional Newton with full Jacobian assembly.
-
- u_{k+1} = u_k - J(u_k)^{-1} · r(u_k)
-
-Expensive: Assembles full Jacobian matrix at each iteration.
-"""
-function newton_traditional!(
- u::AbstractVector{T},
- prob::NonlinearProblem{T},
- r::AbstractVector{T},
- du::AbstractVector{T};
- tol=1e-8,
- max_iter=20,
- verbose=true
-) where T
-
- n = length(u)
-
- # Build Jacobian matrix (expensive!)
- # J ≈ K + 3β·diag((K·u).^2)·K
- Ku = prob.K * u
- J = copy(prob.K)
-
- for iter in 1:max_iter
- # Compute residual
- compute_residual!(r, prob, u)
-
- norm_r = norm(r)
-
- if verbose
- @printf(" Iter %2d: ||r|| = %.6e\n", iter, norm_r)
- end
-
- if norm_r < tol
- if verbose
- println(" ✅ Converged!")
- end
- return iter
- end
-
- # Update Jacobian (expensive!)
- Ku .= prob.K * u
- for i in 1:n
- J[i, i] = prob.K[i, i] + 3 * prob.β * Ku[i]^2 * prob.K[i, i]
- end
-
- # Solve linear system (expensive!)
- du .= -(J \ r)
-
- # Update
- u .+= du
- end
-
- if verbose
- println(" ⚠️ Did not converge in $max_iter iterations")
- end
-
- return max_iter
-end
-
-# ============================================================================
-# Helper: Matrix-free operator wrapper for GMRES
-# ============================================================================
-
-"""
-Wrapper to make a function look like a matrix for GMRES.
-"""
-struct MatrixFreeOperator{F}
- matvec!::F
- n::Int
-end
-
-Base.size(A::MatrixFreeOperator) = (A.n, A.n)
-Base.size(A::MatrixFreeOperator, d::Int) = d <= 2 ? A.n : 1
-Base.eltype(::MatrixFreeOperator{F}) where F = Float64
-
-function LinearAlgebra.mul!(y, A::MatrixFreeOperator, x)
- A.matvec!(y, x)
- return y
-end
-
-# ============================================================================
-# Matrix-Free Newton-Krylov
-# ============================================================================
-
-"""
-Matrix-Free Newton-Krylov with GMRES.
-
- J·v ≈ [r(u+ε·v) - r(u)] / ε (no matrix!)
- du = gmres(Jv_op, -r)
- u_{k+1} = u_k + du
-
-Cheap: Only residual evaluations, no Jacobian assembly.
-"""
-function newton_matrix_free!(
- u::AbstractVector{T},
- prob::NonlinearProblem{T},
- r::AbstractVector{T},
- du::AbstractVector{T},
- temp::AbstractVector{T},
- Jv::AbstractVector{T};
- tol=1e-8,
- max_iter=20,
- gmres_tol=1e-6,
- verbose=true
-) where T
-
- for iter in 1:max_iter
- # Compute residual
- compute_residual!(r, prob, u)
-
- norm_r = norm(r)
-
- if verbose
- @printf(" Iter %2d: ||r|| = %.6e", iter, norm_r)
- end
-
- if norm_r < tol
- if verbose
- println(" ✅ Converged!")
- end
- return iter
- end
-
- # Matrix-free operator: J·v
- function Jv_matvec!(out, v)
- jacobian_vector_product!(out, prob, u, v, r, temp)
- return out
- end
- Jv_op = MatrixFreeOperator(Jv_matvec!, length(u))
-
- # Solve J·du = -r using GMRES (matrix-free!)
- du .= 0
- gmres!(du, Jv_op, -r;
- abstol=gmres_tol, reltol=0, maxiter=50, verbose=false)
-
- gmres_iters = 50 # Would need to extract from gmres! return
-
- if verbose
- @printf(" [GMRES: ~%d iters]\n", gmres_iters)
- end
-
- # Update
- u .+= du
- end
-
- if verbose
- println(" ⚠️ Did not converge in $max_iter iterations")
- end
-
- return max_iter
-end
-
-# ============================================================================
-# Anderson-Accelerated Newton-Krylov
-# ============================================================================
-
-"""
-Anderson acceleration for Newton-Krylov.
-
-Combines m previous iterates via least-squares:
- u_new = ∑ αᵢ·uᵢ where argmin ||∑ αᵢ·rᵢ||² s.t. ∑ αᵢ = 1
-
-Transforms linear convergence → superlinear convergence.
-"""
-function anderson_newton_matrix_free!(
- u::AbstractVector{T},
- prob::NonlinearProblem{T},
- r::AbstractVector{T},
- du::AbstractVector{T},
- temp::AbstractVector{T},
- Jv::AbstractVector{T};
- m=5, # Anderson history
- tol=1e-8,
- max_iter=20,
- gmres_tol=1e-6,
- verbose=true
-) where T
-
- n = length(u)
-
- # Anderson history
- U_history = [zeros(T, n) for _ in 1:m]
- R_history = [zeros(T, n) for _ in 1:m]
- history_count = 0
-
- for iter in 1:max_iter
- # Compute residual
- compute_residual!(r, prob, u)
-
- norm_r = norm(r)
-
- if verbose
- @printf(" Iter %2d: ||r|| = %.6e", iter, norm_r)
- end
-
- if norm_r < tol
- if verbose
- println(" ✅ Converged!")
- end
- return iter
- end
-
- # Matrix-free operator
- function Jv_matvec!(out, v)
- jacobian_vector_product!(out, prob, u, v, r, temp)
- return out
- end
- Jv_op = MatrixFreeOperator(Jv_matvec!, n)
-
- # Solve J·du = -r using GMRES
- du .= 0
- gmres!(du, Jv_op, -r;
- abstol=gmres_tol, reltol=0, maxiter=50, verbose=false)
-
- # Store in history (circular buffer)
- idx = mod1(history_count + 1, m)
- U_history[idx] .= u
- R_history[idx] .= r
- history_count = min(history_count + 1, m)
-
- if verbose
- @printf(" [GMRES: ~50 iters, history: %d]", history_count)
- end
-
- # Anderson acceleration (if enough history)
- if history_count >= 2
- # Build residual difference matrix
- k = history_count
- R_diff = zeros(T, n, k)
- for i in 1:k
- R_diff[:, i] .= R_history[i] .- r
- end
-
- # Check condition number before QR
- # If matrix is ill-conditioned, skip Anderson this iteration
- R_norm = norm(R_diff)
- if R_norm < 1e-10
- # Matrix too small, use standard update
- u .+= du
- if verbose
- println(" [Anderson: skipped (residuals too small)]")
- end
- else
- # Least-squares: min ||R_diff·α||² s.t. sum(α) = 1
- # Use QR factorization with regularization
- try
- Q, Rt = qr(R_diff)
-
- # Add small regularization to diagonal if needed
- Rt_diag = diag(Rt)
- if any(abs.(Rt_diag) .< 1e-12)
- # Add Tikhonov regularization
- λ = 1e-8
- Rt_reg = Rt + λ * I
- α = Rt_reg \ (Q' * r)
- else
- α = Rt \ (Q' * r)
- end
-
- α ./= sum(α) # Normalize
-
- # Combine previous iterates
- u_combined = zeros(T, n)
- for i in 1:k
- u_combined .+= α[i] .* U_history[i]
- end
-
- # Update with combination
- u .= u_combined .+ du
-
- if verbose
- println(" [Anderson: α=$(round.(α, digits=3))]")
- end
- catch e
- # If QR fails, fall back to standard Newton
- u .+= du
- if verbose
- println(" [Anderson: failed ($e), using standard update]")
- end
- end
- end
- else
- # Standard Newton update
- u .+= du
-
- if verbose
- println()
- end
- end
- end
-
- if verbose
- println(" ⚠️ Did not converge in $max_iter iterations")
- end
-
- return max_iter
-end
-
-# ============================================================================
-# GPU Implementations
-# ============================================================================
-
-"""
-GPU version of residual computation.
-"""
-function compute_residual_gpu!(
- r::CuVector{T},
- K::CuMatrix{T},
- u::CuVector{T},
- f::CuVector{T},
- β::T
-) where T
- # r = K·u
- mul!(r, K, u)
-
- # r = r + β·r³ - f
- if β != 0
- r .= r .+ β .* r .^ 3 .- f
- else
- r .= r .- f
- end
-
- return r
-end
-
-"""
-GPU version of Matrix-Free Newton-Krylov.
-"""
-function newton_matrix_free_gpu!(
- u::CuVector{T},
- K::CuMatrix{T},
- f::CuVector{T},
- β::T;
- tol=1e-8,
- max_iter=20,
- gmres_tol=1e-6,
- verbose=true
-) where T
-
- n = length(u)
- r = CUDA.zeros(T, n)
- du = CUDA.zeros(T, n)
- temp = CUDA.zeros(T, n)
- Jv = CUDA.zeros(T, n)
-
- for iter in 1:max_iter
- # Compute residual on GPU
- compute_residual_gpu!(r, K, u, f, β)
-
- norm_r = norm(Array(r)) # Transfer to CPU for norm
-
- if verbose
- @printf(" Iter %2d: ||r|| = %.6e\n", iter, norm_r)
- end
-
- if norm_r < tol
- if verbose
- println(" ✅ Converged!")
- end
- return iter
- end
-
- # Jacobian-vector product (on GPU)
- ε = T(1e-7)
- function Jv_matvec_gpu!(out_cpu, v_cpu)
- v = CuArray(v_cpu)
- # temp = u + ε·v
- temp .= u .+ ε .* v
- # Jv = r(u + ε·v)
- compute_residual_gpu!(Jv, K, temp, f, β)
- # Jv = [r(u + ε·v) - r(u)] / ε
- Jv .= (Jv .- r) ./ ε
- out_cpu .= Array(Jv)
- return out_cpu
- end
- Jv_op_gpu = MatrixFreeOperator(Jv_matvec_gpu!, n)
-
- # Solve on CPU (GMRES doesn't have GPU version in IterativeSolvers.jl)
- r_cpu = Array(r)
- du_cpu = zeros(T, n)
- gmres!(du_cpu, Jv_op_gpu, -r_cpu;
- abstol=gmres_tol, reltol=0, maxiter=50, verbose=false)
-
- # Update on GPU
- du .= CuArray(du_cpu)
- u .+= du
- end
-
- if verbose
- println(" ⚠️ Did not converge in $max_iter iterations")
- end
-
- return max_iter
-end
-
-# GPU Anderson-Accelerated Newton-Krylov
-function anderson_newton_matrix_free_gpu!(
- u::CuVector{T},
- K::CuMatrix{T},
- f::CuVector{T},
- β::T;
- tol=1e-8,
- max_iter=20,
- gmres_tol=1e-6,
- history_size=5,
- verbose=true
-) where T
-
- n = length(u)
- r = CUDA.zeros(T, n)
- du = CUDA.zeros(T, n)
- temp = CUDA.zeros(T, n)
- Jv = CUDA.zeros(T, n)
-
- # Anderson acceleration storage (CPU)
- R_history = Vector{Vector{T}}()
- U_history = Vector{Vector{T}}()
- history_count = 0
-
- for iter in 1:max_iter
- # Compute residual on GPU
- compute_residual_gpu!(r, K, u, f, β)
-
- norm_r = norm(Array(r)) # Transfer to CPU for norm
-
- if verbose
- @printf(" Iter %2d: ||r|| = %.6e", iter, norm_r)
- end
-
- if norm_r < tol
- if verbose
- println("\n ✅ Converged!")
- end
- return iter
- end
-
- # Jacobian-vector product (on GPU)
- ε = T(1e-7)
- function Jv_matvec_gpu!(out_cpu, v_cpu)
- v = CuArray(v_cpu)
- # temp = u + ε·v
- temp .= u .+ ε .* v
- # Jv = r(u + ε·v)
- compute_residual_gpu!(Jv, K, temp, f, β)
- # Jv = [r(u + ε·v) - r(u)] / ε
- Jv .= (Jv .- r) ./ ε
- out_cpu .= Array(Jv)
- return out_cpu
- end
- Jv_op_gpu = MatrixFreeOperator(Jv_matvec_gpu!, n)
-
- # Solve on CPU (GMRES doesn't have GPU version)
- r_cpu = Array(r)
- u_cpu = Array(u)
- du_cpu = zeros(T, n)
- gmres!(du_cpu, Jv_op_gpu, -r_cpu;
- abstol=gmres_tol, reltol=0, maxiter=50, verbose=false)
-
- # Anderson acceleration (on CPU)
- if history_count >= 2
- # Build residual difference matrix
- k = history_count
- R_diff = zeros(T, n, k)
- for i in 1:k
- R_diff[:, i] .= R_history[i] .- r_cpu
- end
-
- # Check condition number before QR
- R_norm = norm(R_diff)
- if R_norm < 1e-10
- # Matrix too small, use standard update
- u .+= CuArray(du_cpu)
- if verbose
- println(" [Anderson: skipped (residuals too small)]")
- end
- else
- # Least-squares with regularization
- try
- Q, Rt = qr(R_diff)
-
- # Add small regularization to diagonal if needed
- Rt_diag = diag(Rt)
- if any(abs.(Rt_diag) .< 1e-12)
- # Add Tikhonov regularization
- λ = 1e-8
- Rt_reg = Rt + λ * I
- α = Rt_reg \ (Q' * r_cpu)
- else
- α = Rt \ (Q' * r_cpu)
- end
-
- α ./= sum(α) # Normalize
-
- # Combine previous iterates
- u_combined = zeros(T, n)
- for i in 1:k
- u_combined .+= α[i] .* U_history[i]
- end
-
- # Update with combination (transfer to GPU)
- u .= CuArray(u_combined .+ du_cpu)
-
- if verbose
- println(" [Anderson: α=$(round.(α, digits=3))]")
- end
- catch e
- # If QR fails, fall back to standard Newton
- u .+= CuArray(du_cpu)
- if verbose
- println(" [Anderson: failed ($e), using standard update]")
- end
- end
- end
- else
- # Standard Newton update (transfer du to GPU)
- u .+= CuArray(du_cpu)
- if verbose
- println()
- end
- end
-
- # Store history (on CPU to avoid GPU memory overhead)
- push!(R_history, copy(r_cpu))
- push!(U_history, copy(u_cpu))
- history_count += 1
-
- # Maintain history size
- if history_count > history_size
- popfirst!(R_history)
- popfirst!(U_history)
- history_count = history_size
- end
- end
-
- if verbose
- println(" ⚠️ Did not converge in $max_iter iterations")
- end
-
- return max_iter
-end
-
-# ============================================================================
-# Benchmark Runners
-# ============================================================================
-
-function benchmark_cpu(n::Int)
- println("="^70)
- println("CPU Benchmark: $n DOFs")
- println("="^70)
-
- # Setup problem
- T = Float64
- K = Matrix(Tridiagonal(
- -ones(T, n - 1),
- 2ones(T, n),
- -ones(T, n - 1)
- ))
- f = ones(T, n) * 0.1
- β = T(1e-3) # Nonlinearity
-
- prob = NonlinearProblem(K, f, β, n)
-
- # Initial guess
- u0 = zeros(T, n)
-
- # Allocate workspace
- r = zeros(T, n)
- du = zeros(T, n)
- temp = zeros(T, n)
- Jv = zeros(T, n)
-
- # Benchmark Traditional Newton
- println("\n📊 Traditional Newton (Full Jacobian):")
- u_trad = copy(u0)
- t_trad = @elapsed iters_trad = newton_traditional!(u_trad, prob, r, du; verbose=false)
- println(" Time: $(round(t_trad * 1000, digits=2)) ms")
- println(" Iterations: $iters_trad")
- println(" Time/iter: $(round(t_trad / iters_trad * 1000, digits=2)) ms")
-
- # Benchmark Matrix-Free
- println("\n📊 Matrix-Free Newton-Krylov:")
- u_mf = copy(u0)
- t_mf = @elapsed iters_mf = newton_matrix_free!(
- u_mf, prob, r, du, temp, Jv; verbose=false
- )
- println(" Time: $(round(t_mf * 1000, digits=2)) ms")
- println(" Iterations: $iters_mf")
- println(" Time/iter: $(round(t_mf / iters_mf * 1000, digits=2)) ms")
-
- # Benchmark Anderson-Accelerated
- println("\n📊 Anderson-Accelerated Matrix-Free:")
- u_anderson = copy(u0)
- t_anderson = @elapsed iters_anderson = anderson_newton_matrix_free!(
- u_anderson, prob, r, du, temp, Jv; m=5, verbose=false
- )
- println(" Time: $(round(t_anderson * 1000, digits=2)) ms")
- println(" Iterations: $iters_anderson")
- println(" Time/iter: $(round(t_anderson / iters_anderson * 1000, digits=2)) ms")
-
- # Speedups
- println("\n✅ CPU Speedups:")
- println(" Matrix-Free vs Traditional: $(round(t_trad / t_mf, digits=2))×")
- println(" Anderson vs Traditional: $(round(t_trad / t_anderson, digits=2))×")
- println(" Anderson vs Matrix-Free: $(round(t_mf / t_anderson, digits=2))×")
-
- println()
-end
-
-function benchmark_gpu(n::Int)
- if !USE_GPU
- println("⚠️ GPU not available, skipping GPU benchmark\n")
- return
- end
-
- println("="^70)
- println("GPU Benchmark: $n DOFs")
- println("="^70)
-
- # Setup problem
- T = Float64
- K_cpu = Matrix(Tridiagonal(
- -ones(T, n - 1),
- 2ones(T, n),
- -ones(T, n - 1)
- ))
- f_cpu = ones(T, n) * 0.1
- β = T(1e-3)
-
- # Transfer to GPU
- K_gpu = CuArray(K_cpu)
- f_gpu = CuArray(f_cpu)
- u0_gpu = CUDA.zeros(T, n)
-
- # Benchmark Matrix-Free on GPU
- println("\n📊 Matrix-Free Newton-Krylov (GPU):")
- u_gpu = copy(u0_gpu)
-
- # Warmup
- newton_matrix_free_gpu!(u_gpu, K_gpu, f_gpu, β; max_iter=2, verbose=false)
-
- # Benchmark
- CUDA.synchronize()
- t_gpu = CUDA.@elapsed begin
- iters_gpu = newton_matrix_free_gpu!(u_gpu, K_gpu, f_gpu, β; verbose=false)
- CUDA.synchronize()
- end
-
- println(" Time: $(round(t_gpu * 1000, digits=2)) ms")
- println(" Iterations: $iters_gpu")
- println(" Time/iter: $(round(t_gpu / iters_gpu * 1000, digits=2)) ms")
-
- # Compare with CPU
- prob_cpu = NonlinearProblem(K_cpu, f_cpu, β, n)
- u_cpu = zeros(T, n)
- r = zeros(T, n)
- du = zeros(T, n)
- temp = zeros(T, n)
- Jv = zeros(T, n)
-
- t_cpu = @elapsed iters_cpu = newton_matrix_free!(
- u_cpu, prob_cpu, r, du, temp, Jv; verbose=false
- )
-
- println("\n✅ GPU vs CPU Speedup: $(round(t_cpu / t_gpu, digits=2))×")
- println(" CPU: $(round(t_cpu * 1000, digits=2)) ms")
- println(" GPU: $(round(t_gpu * 1000, digits=2)) ms")
-
- # Benchmark Anderson-Accelerated on GPU
- println("\n📊 Anderson-Accelerated Newton-Krylov (GPU):")
- u_gpu_anderson = copy(u0_gpu)
-
- # Warmup
- anderson_newton_matrix_free_gpu!(u_gpu_anderson, K_gpu, f_gpu, β; max_iter=2, verbose=false)
-
- # Benchmark
- CUDA.synchronize()
- t_gpu_anderson = CUDA.@elapsed begin
- iters_gpu_anderson = anderson_newton_matrix_free_gpu!(u_gpu_anderson, K_gpu, f_gpu, β; verbose=false)
- CUDA.synchronize()
- end
-
- println(" Time: $(round(t_gpu_anderson * 1000, digits=2)) ms")
- println(" Iterations: $iters_gpu_anderson")
- println(" Time/iter: $(round(t_gpu_anderson / iters_gpu_anderson * 1000, digits=2)) ms")
-
- # Compare with CPU Anderson
- u_cpu_anderson = zeros(T, n)
- t_cpu_anderson = @elapsed iters_cpu_anderson = anderson_newton_matrix_free!(
- u_cpu_anderson, prob_cpu, r, du, temp, Jv; verbose=false
- )
-
- println("\n✅ GPU vs CPU Speedup (Anderson): $(round(t_cpu_anderson / t_gpu_anderson, digits=2))×")
- println(" CPU: $(round(t_cpu_anderson * 1000, digits=2)) ms")
- println(" GPU: $(round(t_gpu_anderson * 1000, digits=2)) ms")
-
- # Overall comparison
- println("\n📊 Summary:")
- println(" Matrix-Free GPU speedup: $(round(t_cpu / t_gpu, digits=2))×")
- println(" Anderson GPU speedup: $(round(t_cpu_anderson / t_gpu_anderson, digits=2))×")
-
- println()
-end
-
-# ============================================================================
-# Main
-# ============================================================================
-
-function main()
- println("\n" * "="^70)
- println("Matrix-Free Newton-Krylov GPU Benchmark")
- println("="^70)
- println()
-
- # Test sizes (reasonable for demonstration)
- sizes = [1000, 5_000, 10_000]
-
- for n in sizes
- # CPU comparison
- benchmark_cpu(n)
-
- # GPU benchmark
- if USE_GPU
- benchmark_gpu(n)
- end
- end
-
- println("="^70)
- println("Benchmark Complete!")
- println("="^70)
- println()
- println("Key Findings:")
- println(" - Matrix-Free eliminates Jacobian assembly cost")
- println(" - Anderson acceleration reduces Newton iterations")
- println(" - GPU provides additional speedup for large problems")
- println(" - Total speedup: 5-10× depending on problem size")
- println()
-end
-
-if abspath(PROGRAM_FILE) == @__FILE__
- main()
-end
diff --git a/benchmarks/multigpu_mpi_benchmark.jl b/benchmarks/multigpu_mpi_benchmark.jl
deleted file mode 100755
index 99b31b5..0000000
--- a/benchmarks/multigpu_mpi_benchmark.jl
+++ /dev/null
@@ -1,571 +0,0 @@
-#!/usr/bin/env julia
-#
-# Multi-GPU Nodal Assembly Benchmark with MPI + CUDA
-#
-# Usage:
-# mpirun -np 2 julia --project=. benchmarks/multigpu_mpi_benchmark.jl
-# mpirun -np 4 julia --project=. benchmarks/multigpu_mpi_benchmark.jl
-#
-# Each MPI rank gets one GPU
-
-using MPI
-using CUDA
-using LinearAlgebra
-using Printf
-
-MPI.Init()
-
-const comm = MPI.COMM_WORLD
-const rank = MPI.Comm_rank(comm)
-const nranks = MPI.Comm_size(comm)
-
-# Set GPU device based on rank
-if CUDA.functional()
- CUDA.device!(rank % CUDA.ndevices())
- if rank == 0
- println("="^70)
- println("Multi-GPU Nodal Assembly Benchmark (MPI + CUDA)")
- println("="^70)
- println("MPI ranks: $nranks")
- println("CUDA devices: $(CUDA.ndevices())")
- println("CUDA functional: $(CUDA.functional())")
- println("="^70)
- println()
- end
-else
- if rank == 0
- println("ERROR: CUDA not functional!")
- println("Install CUDA.jl: using Pkg; Pkg.add(\"CUDA\")")
- end
- MPI.Finalize()
- exit(1)
-end
-
-# ============================================================================
-# Data Structures
-# ============================================================================
-
-struct Node
- id::Int32
- x::Float32
- y::Float32
- z::Float32
-end
-
-struct Element
- id::Int32
- connectivity::NTuple{8,Int32} # Hex8
-end
-
-struct Partition
- rank::Int
- owned_nodes::UnitRange{Int}
- ghost_nodes::Vector{Int}
- local_elements::Vector{Int}
- node_to_elements::Vector{Vector{Int}}
- interface_neighbors::Vector{Int} # Neighbor ranks
- interface_send::Dict{Int,Vector{Int}} # rank → local DOF indices to send
- interface_recv::Dict{Int,Vector{Int}} # rank → local DOF indices to receive
-end
-
-# ============================================================================
-# Mesh Generation
-# ============================================================================
-
-function create_hex_mesh(nx, ny, nz)
- """Create structured hexahedral mesh"""
- n_nodes = nx * ny * nz
- n_elements = (nx - 1) * (ny - 1) * (nz - 1)
-
- nodes = Node[]
- for k in 1:nz, j in 1:ny, i in 1:nx
- node_id = Int32((k - 1) * nx * ny + (j - 1) * nx + i)
- push!(nodes, Node(node_id, Float32(i), Float32(j), Float32(k)))
- end
-
- elements = Element[]
- for k in 1:(nz-1), j in 1:(ny-1), i in 1:(nx-1)
- n1 = Int32((k - 1) * nx * ny + (j - 1) * nx + i)
- n2 = n1 + 1
- n3 = n2 + nx
- n4 = n1 + nx
- n5 = n1 + nx * ny
- n6 = n2 + nx * ny
- n7 = n3 + nx * ny
- n8 = n4 + nx * ny
-
- elem_id = Int32(length(elements) + 1)
- push!(elements, Element(elem_id, (n1, n2, n3, n4, n5, n6, n7, n8)))
- end
-
- return nodes, elements
-end
-
-function build_node_to_elements(nodes, elements)
- node_to_elems = [Int[] for _ in 1:length(nodes)]
-
- for (elem_id, element) in enumerate(elements)
- for node_id in element.connectivity
- push!(node_to_elems[node_id], elem_id)
- end
- end
-
- return node_to_elems
-end
-
-# ============================================================================
-# Partitioning
-# ============================================================================
-
-function partition_mesh_for_rank(nodes, elements, my_rank, n_ranks)
- """Create partition for this MPI rank"""
- n_nodes = length(nodes)
- nodes_per_rank = ceil(Int, n_nodes / n_ranks)
-
- # Owned nodes
- start_node = my_rank * nodes_per_rank + 1
- end_node = min((my_rank + 1) * nodes_per_rank, n_nodes)
- owned_nodes = start_node:end_node
-
- node_to_elems = build_node_to_elements(nodes, elements)
-
- # Find local elements (touching owned nodes)
- local_elements = Int[]
- ghost_nodes = Set{Int}()
-
- for (elem_id, element) in enumerate(elements)
- if any(Int(nid) in owned_nodes for nid in element.connectivity)
- push!(local_elements, elem_id)
-
- for nid in element.connectivity
- if !(Int(nid) in owned_nodes)
- push!(ghost_nodes, Int(nid))
- end
- end
- end
- end
-
- # Build local node_to_elements
- local_node_to_elems = [
- filter(eid -> eid in local_elements, node_to_elems[nid])
- for nid in owned_nodes
- ]
-
- # Find interface nodes with each neighbor
- interface_send = Dict{Int,Vector{Int}}()
- interface_recv = Dict{Int,Vector{Int}}()
-
- for neighbor_rank in 0:(n_ranks-1)
- if neighbor_rank == my_rank
- continue
- end
-
- neighbor_start = neighbor_rank * nodes_per_rank + 1
- neighbor_end = min((neighbor_rank + 1) * nodes_per_rank, n_nodes)
- neighbor_owned = neighbor_start:neighbor_end
-
- # Nodes I own that neighbor needs (I send)
- send_nodes = Int[]
- for elem_id in local_elements
- element = elements[elem_id]
- has_neighbor = any(Int(nid) in neighbor_owned for nid in element.connectivity)
- if has_neighbor
- for nid in element.connectivity
- if Int(nid) in owned_nodes && !(Int(nid) in send_nodes)
- push!(send_nodes, Int(nid))
- end
- end
- end
- end
-
- # Nodes neighbor owns that I need (I receive)
- recv_nodes = Int[]
- for nid in ghost_nodes
- if Int(nid) in neighbor_owned
- push!(recv_nodes, Int(nid))
- end
- end
-
- if !isempty(send_nodes) || !isempty(recv_nodes)
- # Convert to local DOF indices
- send_dofs = Int[]
- for nid in send_nodes
- local_idx = nid - start_node + 1
- for d in 0:2
- push!(send_dofs, (local_idx - 1) * 3 + d + 1)
- end
- end
-
- recv_dofs = Int[]
- for nid in recv_nodes
- ghost_idx = findfirst(==(nid), sort(collect(ghost_nodes)))
- for d in 0:2
- # Ghost DOFs come after owned DOFs
- push!(recv_dofs, length(owned_nodes) * 3 + (ghost_idx - 1) * 3 + d + 1)
- end
- end
-
- if !isempty(send_dofs)
- interface_send[neighbor_rank] = send_dofs
- end
- if !isempty(recv_dofs)
- interface_recv[neighbor_rank] = recv_dofs
- end
- end
- end
-
- interface_neighbors = sort(collect(keys(interface_send) ∪ keys(interface_recv)))
-
- return Partition(
- my_rank,
- owned_nodes,
- sort(collect(ghost_nodes)),
- local_elements,
- local_node_to_elems,
- interface_neighbors,
- interface_send,
- interface_recv
- )
-end
-
-# ============================================================================
-# GPU Kernel: Nodal Assembly
-# ============================================================================
-
-function gpu_matvec_kernel!(
- y::CuDeviceArray{Float32,1},
- x::CuDeviceArray{Float32,1},
- nodes::CuDeviceArray{Node,1},
- elements::CuDeviceArray{Element,1},
- node_to_elems_offsets::CuDeviceArray{Int32,1},
- node_to_elems_data::CuDeviceArray{Int32,1},
- n_owned_nodes::Int32,
-)
- idx = (blockIdx().x - 1) * blockDim().x + threadIdx().x
-
- if idx > n_owned_nodes
- return
- end
-
- # This thread processes owned node idx
- node = nodes[idx]
-
- dof_start = (idx - 1) * 3 + 1
-
- # Initialize nodal contribution
- y1 = Float32(0.0)
- y2 = Float32(0.0)
- y3 = Float32(0.0)
-
- # Get connected elements using CSR-like format (1-based indexing)
- if idx + Int32(1) > length(node_to_elems_offsets)
- return
- end
-
- elem_start = node_to_elems_offsets[idx] + Int32(1)
- elem_end = node_to_elems_offsets[idx+Int32(1)]
-
- for i in elem_start:elem_end
- if i > length(node_to_elems_data)
- return
- end
- elem_id = node_to_elems_data[i]
- if elem_id > length(elements)
- return
- end
- element = elements[elem_id]
-
- # Add contribution from all nodes in this element
- for j in 1:8
- nid = element.connectivity[j]
- x_dof_start = (nid - 1) * 3 + 1
-
- # Mock stiffness contribution
- y1 += Float32(0.1) * x[x_dof_start]
- y2 += Float32(0.1) * x[x_dof_start+1]
- y3 += Float32(0.1) * x[x_dof_start+2]
- end
- end
-
- # Write to output
- y[dof_start] = y1
- y[dof_start+1] = y2
- y[dof_start+2] = y3
-
- return nothing
-end
-
-# ============================================================================
-# Multi-GPU Communication
-# ============================================================================
-
-function exchange_ghost_values!(
- x_local::CuArray{Float32,1},
- partition::Partition,
- comm::MPI.Comm
-)
- """Exchange interface DOF values between MPI ranks"""
-
- # Prepare send/recv buffers on CPU
- send_bufs = Dict{Int,Vector{Float32}}()
- recv_bufs = Dict{Int,Vector{Float32}}()
-
- # Copy data from GPU to CPU for sending
- x_cpu = Array(x_local)
-
- for neighbor in partition.interface_neighbors
- if haskey(partition.interface_send, neighbor)
- send_dofs = partition.interface_send[neighbor]
- send_bufs[neighbor] = x_cpu[send_dofs]
- end
-
- if haskey(partition.interface_recv, neighbor)
- recv_dofs = partition.interface_recv[neighbor]
- recv_bufs[neighbor] = zeros(Float32, length(recv_dofs))
- end
- end
-
- # MPI communication
- requests = MPI.Request[]
-
- # Post receives
- for neighbor in partition.interface_neighbors
- if haskey(recv_bufs, neighbor)
- req = MPI.Irecv!(recv_bufs[neighbor], comm; source=neighbor, tag=neighbor)
- push!(requests, req)
- end
- end
-
- # Post sends
- for neighbor in partition.interface_neighbors
- if haskey(send_bufs, neighbor)
- req = MPI.Isend(send_bufs[neighbor], comm; dest=neighbor, tag=partition.rank)
- push!(requests, req)
- end
- end
-
- # Wait for all communications
- MPI.Waitall(requests)
-
- # Copy received data back to GPU
- for neighbor in partition.interface_neighbors
- if haskey(partition.interface_recv, neighbor)
- recv_dofs = partition.interface_recv[neighbor]
- x_cpu[recv_dofs] .= recv_bufs[neighbor]
- end
- end
-
- # Update GPU array
- copyto!(x_local, x_cpu)
-end
-
-# ============================================================================
-# Benchmark
-# ============================================================================
-
-function run_multigpu_benchmark(nx, ny, nz, n_warmup=5, n_runs=10)
- if rank == 0
- println("\n" * "="^70)
- println("Multi-GPU Benchmark: $nx × $ny × $nz mesh")
- println("="^70)
- end
-
- # Create full mesh on all ranks
- nodes, elements = create_hex_mesh(nx, ny, nz)
-
- if rank == 0
- println(" Total nodes: ", length(nodes))
- println(" Total elements: ", length(elements))
- println(" Total DOFs: ", 3 * length(nodes))
- end
-
- # Partition for this rank
- partition = partition_mesh_for_rank(nodes, elements, rank, nranks)
-
- n_owned = length(partition.owned_nodes)
- n_ghost = length(partition.ghost_nodes)
- n_local_dofs = 3 * (n_owned + n_ghost)
-
- println("Rank $rank: $n_owned owned nodes, $n_ghost ghost nodes, " *
- "$(length(partition.local_elements)) elements")
-
- # Prepare GPU data
- local_nodes = [nodes[i] for i in vcat(collect(partition.owned_nodes), partition.ghost_nodes)]
-
- # Create mapping from global node ID to local index
- global_to_local_node = Dict{Int,Int32}()
- for (local_idx, global_nid) in enumerate(vcat(collect(partition.owned_nodes), partition.ghost_nodes))
- global_to_local_node[global_nid] = Int32(local_idx)
- end
-
- # Remap element connectivity to local node indices
- local_elements = Element[]
- for global_eid in partition.local_elements
- element = elements[global_eid]
- # Convert global node IDs to local indices
- local_conn = ntuple(8) do i
- global_nid = Int(element.connectivity[i])
- global_to_local_node[global_nid]
- end
- push!(local_elements, Element(element.id, local_conn))
- end
-
- # Create mapping from global element ID to local index (for CSR data)
- global_to_local_elem = Dict{Int,Int}()
- for (local_idx, global_id) in enumerate(partition.local_elements)
- global_to_local_elem[global_id] = local_idx
- end
-
- # Convert node_to_elements to GPU-friendly flat format
- # Format: offsets array + flat data array (CSR-like)
- # IMPORTANT: Convert global element IDs to local indices
- node_to_elems_offsets = Int32[0]
- node_to_elems_data = Int32[]
- for arr in partition.node_to_elements
- # Map global element IDs to local indices
- local_indices = [global_to_local_elem[global_id] for global_id in arr]
- append!(node_to_elems_data, Int32.(local_indices))
- push!(node_to_elems_offsets, length(node_to_elems_data))
- end
-
- # Debug: check element ID range
- if rank == 0 && length(node_to_elems_data) > 0
- min_elem_id = minimum(node_to_elems_data)
- max_elem_id = maximum(node_to_elems_data)
- println("\nCSR data element ID range: $min_elem_id to $max_elem_id")
- println("Local elements array size: $(length(local_elements))")
- if max_elem_id > length(local_elements)
- println("❌ WARNING: Element ID $max_elem_id > array size $(length(local_elements))")
- end
- end
-
- # Transfer to GPU
- nodes_gpu = CuArray(local_nodes)
- elements_gpu = CuArray(local_elements)
- node_to_elems_offsets_gpu = CuArray(node_to_elems_offsets)
- node_to_elems_data_gpu = CuArray(node_to_elems_data)
-
- # Debug: print array sizes
- if rank == 0
- println("\nArray sizes on GPU:")
- println(" nodes: $(length(nodes_gpu))")
- println(" elements: $(length(elements_gpu))")
- println(" node_to_elems_offsets: $(length(node_to_elems_offsets_gpu))")
- println(" node_to_elems_data: $(length(node_to_elems_data_gpu))")
- println(" Expected offsets length: $(n_owned + 1)")
- end
-
- # Test vectors
- x_local = CUDA.rand(Float32, n_local_dofs)
- y_local = CUDA.zeros(Float32, n_local_dofs)
-
- # Kernel launch parameters
- threads_per_block = 256
- n_blocks = cld(n_owned, threads_per_block)
-
- if rank == 0
- println("\nGPU configuration:")
- println(" Threads per block: $threads_per_block")
- println(" Blocks per rank: $n_blocks")
- end
-
- # Warmup
- for _ in 1:n_warmup
- exchange_ghost_values!(x_local, partition, comm)
- CUDA.@sync @cuda threads = threads_per_block blocks = n_blocks gpu_matvec_kernel!(
- y_local, x_local, nodes_gpu, elements_gpu,
- node_to_elems_offsets_gpu, node_to_elems_data_gpu, Int32(n_owned)
- )
- end
-
- MPI.Barrier(comm)
-
- # Benchmark
- times = Float64[]
- comm_times = Float64[]
- compute_times = Float64[]
-
- for _ in 1:n_runs
- t_start = time_ns()
-
- # Communication
- t_comm_start = time_ns()
- exchange_ghost_values!(x_local, partition, comm)
- MPI.Barrier(comm)
- t_comm_end = time_ns()
-
- # Computation
- t_compute_start = time_ns()
- CUDA.@sync @cuda threads = threads_per_block blocks = n_blocks gpu_matvec_kernel!(
- y_local, x_local, nodes_gpu, elements_gpu,
- node_to_elems_offsets_gpu, node_to_elems_data_gpu, Int32(n_owned)
- )
- MPI.Barrier(comm)
- t_compute_end = time_ns()
-
- t_end = time_ns()
-
- push!(times, (t_end - t_start) / 1e9)
- push!(comm_times, (t_comm_end - t_comm_start) / 1e9)
- push!(compute_times, (t_compute_end - t_compute_start) / 1e9)
- end
-
- # Gather results
- local_time = minimum(times)
- local_comm = minimum(comm_times)
- local_compute = minimum(compute_times)
-
- all_times = MPI.Gather(local_time, 0, comm)
- all_comm = MPI.Gather(local_comm, 0, comm)
- all_compute = MPI.Gather(local_compute, 0, comm)
-
- if rank == 0
- println("\nResults:")
- println(" Rank | Owned Nodes | Total Time | Comm Time | Compute Time | Comm %")
- println(" " * "-"^70)
- for r in 0:(nranks-1)
- nodes_str = lpad(string(length(partition.owned_nodes)), 11)
- total_str = @sprintf("%.3f ms", all_times[r+1] * 1000)
- comm_str = @sprintf("%.3f ms", all_comm[r+1] * 1000)
- compute_str = @sprintf("%.3f ms", all_compute[r+1] * 1000)
- comm_pct = @sprintf("%.1f%%", all_comm[r+1] / all_times[r+1] * 100)
-
- println(" $r | $nodes_str | $(lpad(total_str, 10)) | " *
- "$(lpad(comm_str, 9)) | $(lpad(compute_str, 12)) | $(lpad(comm_pct, 6))")
- end
-
- max_time = maximum(all_times)
- avg_compute = sum(all_compute) / length(all_compute)
- avg_comm = sum(all_comm) / length(all_comm)
-
- println("\n Maximum time: ", @sprintf("%.3f ms", max_time * 1000))
- println(" Average compute: ", @sprintf("%.3f ms", avg_compute * 1000))
- println(" Average communication: ", @sprintf("%.3f ms", avg_comm * 1000))
- println(" Communication overhead: ", @sprintf("%.1f%%", avg_comm / max_time * 100))
-
- throughput = length(nodes) / max_time / 1e6
- println(" Throughput: ", @sprintf("%.2f Mnodes/s", throughput))
- end
-end
-
-# ============================================================================
-# Main
-# ============================================================================
-
-if rank == 0
- println("Starting benchmarks...")
- println()
-end
-
-# Run benchmarks with increasing mesh sizes
-run_multigpu_benchmark(30, 30, 30)
-run_multigpu_benchmark(50, 50, 50)
-run_multigpu_benchmark(70, 70, 70)
-
-if rank == 0
- println("\n" * "="^70)
- println("✓ Multi-GPU Benchmark Complete")
- println("="^70)
-end
-
-MPI.Finalize()
diff --git a/benchmarks/multigpu_results_2025-11-09.md b/benchmarks/multigpu_results_2025-11-09.md
deleted file mode 100644
index 928bf54..0000000
--- a/benchmarks/multigpu_results_2025-11-09.md
+++ /dev/null
@@ -1,329 +0,0 @@
-# Multi-GPU Nodal Assembly Results
-
-**Date:** November 9, 2025
-**Hardware:** 2× MPI ranks, 1× NVIDIA RTX A2000 12GB (shared between ranks)
-**Software:** Julia 1.12.1, CUDA.jl, MPI.jl
-
----
-
-## Executive Summary
-
-**✅ Multi-GPU implementation WORKS!** Successfully ran multi-GPU nodal assembly with MPI + CUDA.
-
-**Key Achievement:** Implemented nodal assembly on GPU with:
-- CSR-format node-to-elements connectivity (zero allocation)
-- Proper global-to-local index remapping for elements and nodes
-- MPI ghost value exchange for domain interfaces
-- Validated correctness (all ranks complete successfully)
-
-**Performance:**
-- **Throughput:** 115-302 Mnodes/s (scales with mesh size)
-- **Communication overhead:** 29-61% (MPI transfers dominate for small/medium meshes)
-- **Compute performance:** GPU kernel is fast (0.16-0.56 ms), communication is bottleneck
-
----
-
-## Detailed Results
-
-### Benchmark Configuration
-- **MPI ranks:** 2
-- **GPU per rank:** 1 (shared device for both ranks in this test)
-- **Partitioning:** Slab decomposition (nodes split evenly)
-- **Element type:** Hex8 (8-node hexahedron)
-- **Kernel:** Mock stiffness (simplified matvec for validation)
-- **Warmup:** 10 iterations
-- **Measurement:** 100 iterations (timed)
-
-### Performance Table
-
-| Mesh Size | Total Nodes | Total DOFs | Owned/Rank | Ghost/Rank | Throughput | Comm % |
-|-----------|-------------|------------|------------|------------|------------|--------|
-| 30³ | 27,000 | 81,000 | 13,500 | 900 | 114.84 Mnodes/s | 29.1% |
-| 50³ | 125,000 | 375,000 | 62,500 | 2,500 | 130.64 Mnodes/s | 60.7% |
-| 70³ | 343,000 | 1,029,000 | 171,500 | 4,900 | 301.83 Mnodes/s | 50.9% |
-
-### Detailed Timing Breakdown
-
-**30×30×30 mesh:**
-```
-Rank 0: Total 0.235 ms = Compute 0.159 ms + Comm 0.069 ms (29.4%)
-Rank 1: Total 0.227 ms = Compute 0.159 ms + Comm 0.068 ms (29.9%)
-Throughput: 114.84 Mnodes/s
-```
-
-**50×50×50 mesh:**
-```
-Rank 0: Total 0.957 ms = Compute 0.373 ms + Comm 0.581 ms (60.7%)
-Rank 1: Total 0.957 ms = Compute 0.375 ms + Comm 0.581 ms (60.7%)
-Throughput: 130.64 Mnodes/s
-```
-
-**70×70×70 mesh:**
-```
-Rank 0: Total 1.136 ms = Compute 0.557 ms + Comm 0.579 ms (50.9%)
-Rank 1: Total 1.136 ms = Compute 0.557 ms + Comm 0.579 ms (50.9%)
-Throughput: 301.83 Mnodes/s
-```
-
----
-
-## Comparison with CPU Baseline
-
-**From `nodal_assembly_scalability.jl` (validated Nov 9, 2025):**
-
-### CPU Multi-Threading (8 threads, single node)
-
-| Mesh Size | Nodes | Single-Thread | 8 Threads | Speedup | Efficiency |
-|-----------|---------|---------------|-----------|---------|------------|
-| 20³ | 8,000 | 3.4 Mnodes/s | 49.6 Mnodes/s | 14.6× | 182% |
-| 40³ | 64,000 | 3.6 Mnodes/s | 54.8 Mnodes/s | 15.2× | 189% |
-| 60³ | 216,000 | 3.6 Mnodes/s | 47.9 Mnodes/s | 13.3× | 166% |
-
-### CPU Partitioned (4 partitions, sequential)
-
-| Mesh Size | Nodes | Throughput | Speedup vs Single-Thread | Interface Overhead |
-|-----------|---------|------------|--------------------------|-------------------|
-| 20³ | 8,000 | 29.7 Mnodes/s | 8.5× | 40.1% |
-| 40³ | 64,000 | 28.8 Mnodes/s | 8.0× | 22.4% |
-| 60³ | 216,000 | 27.3 Mnodes/s | 7.6× | 10.3% |
-
-### GPU vs CPU Comparison
-
-**Throughput Comparison (approximate mesh sizes):**
-
-| Mesh | CPU Single-Thread | CPU 8-Thread | CPU 4-Partition | GPU 2-Rank (MPI) | GPU Speedup vs 8-Thread |
-|------|-------------------|--------------|-----------------|------------------|-------------------------|
-| ~30³ | 3.5 Mnodes/s | ~50 Mnodes/s | ~29 Mnodes/s | 114.84 Mnodes/s | **2.3×** |
-| ~60³ | 3.6 Mnodes/s | 47.9 Mnodes/s | 27.3 Mnodes/s | ~200 Mnodes/s (interpolated) | **4.2×** |
-| 70³ | 3.6 Mnodes/s | ~48 Mnodes/s (est) | ~28 Mnodes/s (est) | 301.83 Mnodes/s | **6.3×** |
-
-**Key Observations:**
-1. ✅ GPU is **2-6× faster** than CPU 8-thread for same mesh size
-2. ✅ GPU throughput scales better with mesh size (114 → 302 Mnodes/s)
-3. ⚠️ GPU communication overhead (29-61%) higher than CPU partitioned (10-40%)
-4. 🎯 GPU shines on larger meshes (70³: 6.3× faster than CPU)
-
----
-
-## Analysis & Insights
-
-### What Worked Well ✅
-
-1. **Nodal assembly pattern on GPU:**
- - Each thread processes one node (no atomics!)
- - Gathers contributions from connected elements
- - Direct write to owned DOFs (no race conditions)
-
-2. **CSR-format node-to-elements:**
- - `offsets[node_id]` → start of element list
- - `data[offsets[i]:offsets[i+1]]` → element IDs
- - Zero allocation, type-stable, GPU-friendly
-
-3. **Global-to-local index remapping:**
- - Element IDs: Global mesh → Local partition indices
- - Node IDs in connectivity: Global mesh → Local partition indices
- - Critical for correctness with sliced arrays
-
-4. **MPI ghost exchange:**
- - Interface nodes identified correctly
- - Ghost values exchanged between ranks
- - Enables domain decomposition
-
-### Performance Bottlenecks ⚠️
-
-1. **Communication overhead dominates small/medium meshes:**
- - 30³ mesh: 29% communication
- - 50³ mesh: **61% communication** (worst case!)
- - 70³ mesh: 51% communication
- - **Root cause:** MPI transfers CPU ↔ GPU for every iteration
-
-2. **Single GPU shared between 2 MPI ranks:**
- - Both ranks compete for same GPU
- - No true parallelism in this test configuration
- - Need multiple GPUs for real multi-GPU scaling
-
-3. **Mock kernel (simplified stiffness):**
- - Real FEM kernel would be more compute-intensive
- - Would reduce communication percentage
- - Current kernel is memory-bound
-
-### Opportunities for Improvement 🎯
-
-1. **CUDA-aware MPI:**
- - Direct GPU-to-GPU transfers (no CPU staging)
- - Can reduce communication time by 50-80%
- - Requires recompilation of MPI with CUDA support
-
-2. **Multiple physical GPUs:**
- - Current test uses 1 GPU for 2 ranks (shared)
- - True multi-GPU: Each rank gets own GPU
- - Would enable concurrent execution
-
-3. **Larger elements (higher-order):**
- - Tet10, Hex20, Hex27 have more work per element
- - More compute per node → reduces communication %
- - Better compute/communication ratio
-
-4. **Full element stiffness:**
- - Real FEM: Integration loops, material models, plasticity
- - 10-100× more work per element
- - Communication becomes negligible (<5%)
-
-5. **Batched assembly:**
- - Assemble multiple timesteps before MPI sync
- - Amortize communication cost
- - Useful for explicit dynamics
-
----
-
-## Technical Details
-
-### Data Structures
-
-**Node (immutable, 32 bytes):**
-```julia
-struct Node
- id::Int32
- x::Float32
- y::Float32
- z::Float32
-end
-```
-
-**Element (immutable, 36 bytes):**
-```julia
-struct Element
- id::Int32
- connectivity::NTuple{8, Int32} # Hex8
-end
-```
-
-**Partition:**
-- `owned_nodes`: Nodes owned by this rank
-- `ghost_nodes`: Nodes owned by neighbors (interface)
-- `local_elements`: Elements touching owned nodes
-- `node_to_elements`: Inverse connectivity (node → elements)
-- `interface_send/recv`: MPI communication patterns
-
-### GPU Kernel (Simplified)
-
-```julia
-function gpu_matvec_kernel!(y, x, nodes, elements, offsets, data, n_owned)
- idx = (blockIdx().x - 1) * blockDim().x + threadIdx().x
-
- if idx > n_owned
- return # Thread beyond owned nodes
- end
-
- # Initialize accumulator
- fx = fy = fz = 0.0f0
-
- # Loop over connected elements (CSR access)
- elem_start = offsets[idx] + 1
- elem_end = offsets[idx + 1]
- for i in elem_start:elem_end
- elem_id = data[i]
- element = elements[elem_id]
-
- # Gather from element nodes
- for j in 1:8
- nid = element.connectivity[j]
- dof_base = (nid - 1) * 3 + 1
- fx += 0.1f0 * x[dof_base]
- fy += 0.1f0 * x[dof_base + 1]
- fz += 0.1f0 * x[dof_base + 2]
- end
- end
-
- # Write result (owned DOFs only)
- dof_base = (idx - 1) * 3 + 1
- y[dof_base] = fx
- y[dof_base + 1] = fy
- y[dof_base + 2] = fz
-end
-```
-
-### Key Implementation Challenges & Solutions
-
-**Challenge 1:** CuArray{CuArray} not supported
-**Solution:** Flatten to CSR format (offsets + data arrays)
-
-**Challenge 2:** Global element IDs in CSR data, but local array
-**Solution:** Create `global_to_local_elem` mapping, remap before GPU transfer
-
-**Challenge 3:** Global node IDs in element connectivity
-**Solution:** Create `global_to_local_node` mapping, rebuild elements with local indices
-
-**Challenge 4:** BoundsError during kernel execution
-**Solution:** All three index spaces must be consistent (nodes, elements, DOFs)
-
----
-
-## Conclusions
-
-### Claims We Can Now Make ✅
-
-1. ✅ **Nodal assembly works on GPU** - Validated with working implementation
-2. ✅ **2-6× faster than CPU multi-threading** - Real measurements on same mesh
-3. ✅ **Scales to 343K nodes / 1M DOFs** - Successfully ran 70³ mesh
-4. ✅ **Communication overhead acceptable** - 29-61% (will improve with CUDA-aware MPI)
-5. ✅ **CSR format enables zero-allocation** - No dynamic memory in kernel
-
-### Claims We CANNOT Yet Make ⚠️
-
-1. ⚠️ **Multi-GPU strong scaling** - Only tested 1 GPU with 2 ranks (not true multi-GPU)
-2. ⚠️ **Production-ready performance** - Mock kernel, needs real FEM stiffness
-3. ⚠️ **Weak scaling to N GPUs** - Need cluster with multiple GPUs
-4. ⚠️ **Better than Gridap/Ferrite** - Haven't compared with other libraries
-5. ⚠️ **Contact mechanics on GPU** - Not yet implemented
-
-### Next Steps 🎯
-
-**Immediate (validate architecture):**
-1. Test with multiple physical GPUs (2-4 GPUs on cluster)
-2. Implement real element stiffness (not mock)
-3. Measure CUDA-aware MPI improvement
-4. Add higher-order elements (Tet10, Hex20)
-
-**Short-term (production features):**
-1. Material state updates on GPU (plasticity, damage)
-2. Contact detection and assembly on GPU
-3. Preconditioned GMRES on GPU (full solver)
-4. Integration with JuliaFEM element library
-
-**Long-term (scale-up):**
-1. Weak scaling study (1-64 GPUs)
-2. Strong scaling study (fixed problem, varying GPUs)
-3. Comparison with Gridap.jl + PETSc
-4. Real-world contact mechanics problem (1M+ DOFs)
-
----
-
-## Files & Artifacts
-
-**Benchmark code:**
-- `benchmarks/multigpu_mpi_benchmark.jl` (555 lines, working)
-
-**CPU baseline (validated):**
-- `benchmarks/nodal_assembly_scalability.jl` (500 lines)
-
-**Documentation:**
-- `docs/book/multigpu_nodal_assembly.md` (design, needs update with real data)
-- `docs/book/nodal_assembly_gpu_pattern.md` (architecture)
-- `demos/gpu_nodal_assembly_demo.jl` (educational demo)
-
-**This report:**
-- `benchmarks/multigpu_results_2025-11-09.md`
-
----
-
-## Acknowledgments
-
-**User (Jukka):** Demanded real measurements, not designs. Caught AI making unvalidated claims. Insisted on "Just run" - forcing validation before documentation.
-
-**Key Insight:** "Did you actually run that code?" - Best engineering feedback possible. No more design documents without validation!
-
----
-
-**Status:** ✅ Multi-GPU architecture VALIDATED
-**Verdict:** Nodal assembly on GPU is **feasible and fast**. Communication overhead acceptable. Ready for production implementation.
diff --git a/benchmarks/neo_hookean_analysis.jl b/benchmarks/neo_hookean_analysis.jl
deleted file mode 100644
index 3eae963..0000000
--- a/benchmarks/neo_hookean_analysis.jl
+++ /dev/null
@@ -1,260 +0,0 @@
-"""
-Performance Analysis: NeoHookean Hyperelastic Material
-
-Benchmarks automatic differentiation overhead in hyperelastic stress computation.
-
-Key Questions:
-1. What is the cost of AD compared to LinearElastic?
-2. Is the implementation allocation-free?
-3. How does performance scale with problem size?
-4. What is the breakdown of strain energy vs stress vs tangent?
-
-Results inform whether AD is suitable for production FEM assembly loops.
-"""
-
-using BenchmarkTools
-using Tensors
-using LinearAlgebra
-using Printf
-
-# Load implementations
-include("../src/materials/abstract_material.jl")
-include("../src/materials/linear_elastic.jl")
-include("../src/materials/neo_hookean.jl")
-
-println("="^80)
-println("NeoHookean Hyperelastic Material - Performance Analysis")
-println("="^80)
-println()
-
-# =============================================================================
-# Test 1: Single Stress Evaluation (typical FEM use case)
-# =============================================================================
-println("Test 1: Single Stress Evaluation")
-println("-"^80)
-
-# Create materials
-neo = NeoHookean(E_mod=200e9, nu=0.3) # Steel-like properties
-linear = LinearElastic(E=200e9, ν=0.3)
-
-# Test strain (moderate deformation)
-E_strain = SymmetricTensor{2,3}((0.01, 0.005, 0.003, -0.002, 0.004, 0.006))
-
-# Compile first
-compute_stress(neo, E_strain, nothing, 0.0)
-compute_stress(linear, E_strain, nothing, 0.0)
-
-# Benchmark
-println("\nLinearElastic (manual derivatives):")
-t_linear = @benchmark compute_stress($linear, $E_strain, nothing, 0.0)
-display(t_linear)
-println()
-
-println("\nNeoHookean (automatic differentiation):")
-t_neo = @benchmark compute_stress($neo, $E_strain, nothing, 0.0)
-display(t_neo)
-println()
-
-# Compute overhead
-overhead = median(t_neo).time / median(t_linear).time
-println("\nAD Overhead: $(round(overhead, digits=1))x")
-println("Absolute difference: $(round((median(t_neo).time - median(t_linear).time)/1e3, digits=1)) μs")
-
-# =============================================================================
-# Test 2: Allocation Check
-# =============================================================================
-println("\n" * "="^80)
-println("Test 2: Allocation Analysis")
-println("-"^80)
-
-allocs_neo = @allocated compute_stress(neo, E_strain, nothing, 0.0)
-allocs_linear = @allocated compute_stress(linear, E_strain, nothing, 0.0)
-
-println("LinearElastic allocations: $allocs_linear bytes")
-println("NeoHookean allocations: $allocs_neo bytes")
-
-if allocs_neo == 0
- println("✅ Zero-allocation achieved!")
-else
- println("⚠️ Allocations detected - investigate")
-end
-
-# =============================================================================
-# Test 3: Component Breakdown (where does time go?)
-# =============================================================================
-println("\n" * "="^80)
-println("Test 3: Component Breakdown")
-println("-"^80)
-
-# Test strain energy only
-C = 2E_strain + one(E_strain)
-println("\nStrain energy computation:")
-t_energy = @benchmark strain_energy($neo, $C)
-display(t_energy)
-println()
-
-# Stress only (includes strain energy + gradient)
-println("\nComplete stress computation (energy + gradient + hessian):")
-display(t_neo)
-println()
-
-energy_fraction = median(t_energy).time / median(t_neo).time
-println("Strain energy is $(round(energy_fraction*100, digits=1))% of total time")
-println("AD overhead (gradient + hessian) is $(round((1-energy_fraction)*100, digits=1))% of total time")
-
-# =============================================================================
-# Test 4: Deformation Magnitude Sensitivity
-# =============================================================================
-println("\n" * "="^80)
-println("Test 4: Performance vs Deformation Magnitude")
-println("-"^80)
-
-strain_levels = [1e-6, 1e-4, 1e-2, 0.1, 0.5]
-times = Float64[]
-
-for ε_mag in strain_levels
- E_test = SymmetricTensor{2,3}((ε_mag, 0.0, 0.0, 0.0, 0.0, 0.0))
- compute_stress(neo, E_test, nothing, 0.0) # Compile
- t = @benchmark compute_stress($neo, $E_test, nothing, 0.0) samples = 1000
- push!(times, median(t).time)
- @printf("Strain magnitude: %.1e → Time: %.1f ns\n", ε_mag, median(t).time)
-end
-
-time_variation = (maximum(times) - minimum(times)) / minimum(times) * 100
-println("\nTime variation across strain levels: $(round(time_variation, digits=1))%")
-if time_variation < 10
- println("✅ Performance is strain-independent (good for Newton solvers)")
-else
- println("⚠️ Performance varies with strain (may impact Newton convergence)")
-end
-
-# =============================================================================
-# Test 5: Tangent Accuracy vs Finite Difference
-# =============================================================================
-println("\n" * "="^80)
-println("Test 5: Tangent Accuracy (AD vs Finite Difference)")
-println("-"^80)
-
-E_base = SymmetricTensor{2,3}((0.01, 0.005, 0.003, -0.002, 0.004, 0.006))
-S_ad, 𝔻_ad, _ = compute_stress(neo, E_base, nothing, 0.0)
-
-# Finite difference tangent
-ε_fd = 1e-8
-errors = Float64[]
-for i in 1:6
- E_pert_data = collect(E_base.data)
- E_pert_data[i] += ε_fd
- E_pert = SymmetricTensor{2,3}(tuple(E_pert_data...))
-
- S_pert, _, _ = compute_stress(neo, E_pert, nothing, 0.0)
-
- ∂S∂E_fd = (S_pert - S_ad) / ε_fd
- error = norm(∂S∂E_fd) # Simplified error metric
- push!(errors, error)
-end
-
-println("Tangent norm comparison:")
-println(" AD tangent: $(round(norm(𝔻_ad), sigdigits=6))")
-println(" FD derivative: $(round(mean(errors), sigdigits=6))")
-println(" Relative diff: $(round((norm(𝔻_ad) - mean(errors))/norm(𝔻_ad)*100, digits=2))%")
-println("\n✅ AD provides exact derivatives (limited only by machine precision)")
-
-# =============================================================================
-# Test 6: Memory Footprint
-# =============================================================================
-println("\n" * "="^80)
-println("Test 6: Memory Footprint")
-println("-"^80)
-
-println("Struct sizes:")
-println(" NeoHookean: $(sizeof(neo)) bytes")
-println(" LinearElastic: $(sizeof(linear)) bytes")
-println("\nReturn value sizes:")
-println(" SymmetricTensor{2,3}: $(sizeof(S_ad)) bytes")
-println(" SymmetricTensor{4,3}: $(sizeof(𝔻_ad)) bytes")
-println(" Total per evaluation: $(sizeof(S_ad) + sizeof(𝔻_ad)) bytes")
-
-# =============================================================================
-# Test 7: Scaling with Multiple Evaluations
-# =============================================================================
-println("\n" * "="^80)
-println("Test 7: Assembly Loop Simulation (1000 evaluations)")
-println("-"^80)
-
-n_evals = 1000
-strain_samples = [SymmetricTensor{2,3}((rand(), rand(), rand(), rand(), rand(), rand())) * 0.01
- for _ in 1:n_evals]
-
-# Compile
-for E in strain_samples[1:10]
- compute_stress(neo, E, nothing, 0.0)
- compute_stress(linear, E, nothing, 0.0)
-end
-
-println("\nLinearElastic ($n_evals evaluations):")
-t_linear_loop = @benchmark begin
- for E in $strain_samples
- compute_stress($linear, E, nothing, 0.0)
- end
-end
-display(t_linear_loop)
-println()
-
-println("\nNeoHookean ($n_evals evaluations):")
-t_neo_loop = @benchmark begin
- for E in $strain_samples
- compute_stress($neo, E, nothing, 0.0)
- end
-end
-display(t_neo_loop)
-println()
-
-overhead_loop = median(t_neo_loop).time / median(t_linear_loop).time
-per_eval_neo = median(t_neo_loop).time / n_evals
-per_eval_linear = median(t_linear_loop).time / n_evals
-
-println("Per-evaluation time:")
-println(" LinearElastic: $(round(per_eval_linear, digits=1)) ns")
-println(" NeoHookean: $(round(per_eval_neo, digits=1)) ns")
-println(" Overhead: $(round(overhead_loop, digits=1))x")
-
-# =============================================================================
-# Summary and Recommendations
-# =============================================================================
-println("\n" * "="^80)
-println("SUMMARY AND RECOMMENDATIONS")
-println("="^80)
-
-total_overhead = median(t_neo).time / median(t_linear).time
-
-println("\n📊 Performance Metrics:")
-println(" Single evaluation: $(round(median(t_neo).time, digits=1)) ns")
-println(" AD overhead: $(round(total_overhead, digits=1))x")
-println(" Allocations: $(allocs_neo) bytes")
-println(" Strain-independent: $(time_variation < 10 ? "Yes ✅" : "No ⚠️")")
-
-println("\n🎯 Recommendations:")
-
-if total_overhead < 5
- println(" ✅ EXCELLENT: AD overhead < 5x, suitable for production FEM")
-elseif total_overhead < 10
- println(" ✅ GOOD: AD overhead < 10x, acceptable for most applications")
-elseif total_overhead < 20
- println(" ⚠️ MODERATE: AD overhead < 20x, consider for prototyping only")
-else
- println(" ❌ HIGH: AD overhead > 20x, manual derivatives recommended for production")
-end
-
-if allocs_neo == 0
- println(" ✅ Zero allocations achieved - suitable for tight loops")
-else
- println(" ⚠️ Allocations detected - profile and optimize")
-end
-
-println("\n💡 Use Cases:")
-println(" • Research code: Strongly recommended (correctness > speed)")
-println(" • Prototyping: Excellent (rapid implementation)")
-println(" • Production: $(total_overhead < 10 ? "Acceptable" : "Profile first") ($(round(total_overhead, digits=1))x overhead)")
-println(" • Contact: Excellent (unsymmetric tangent, complex derivatives)")
-
-println("\n" * "="^80)
diff --git a/benchmarks/nodal_assembly_scalability.jl b/benchmarks/nodal_assembly_scalability.jl
deleted file mode 100755
index 6d073c2..0000000
--- a/benchmarks/nodal_assembly_scalability.jl
+++ /dev/null
@@ -1,438 +0,0 @@
-#!/usr/bin/env julia
-#
-# Nodal Assembly Scalability Benchmark
-#
-# Tests:
-# 1. Single-threaded baseline
-# 2. Multi-threaded scaling (2, 4, 8 threads)
-# 3. Actual speedup measurements
-# 4. Interface communication overhead
-#
-# Run with: julia --project=. -t 8 benchmarks/nodal_assembly_scalability.jl
-
-using LinearAlgebra
-using Printf
-using Base.Threads
-
-println("="^70)
-println("Nodal Assembly Scalability Benchmark")
-println("="^70)
-println()
-println("Julia threads available: ", nthreads())
-println()
-
-# ============================================================================
-# Data Structures
-# ============================================================================
-
-struct Node
- id::Int
- x::Float64
- y::Float64
- z::Float64
-end
-
-struct Element
- id::Int
- connectivity::NTuple{8,Int} # Hex8
-end
-
-struct Partition
- rank::Int
- owned_nodes::UnitRange{Int}
- ghost_nodes::Vector{Int}
- local_elements::Vector{Int}
- node_to_elements::Vector{Vector{Int}}
- interface_nodes::Dict{Int,Vector{Int}}
-end
-
-# ============================================================================
-# Mesh Generation
-# ============================================================================
-
-function create_hex_mesh(nx, ny, nz)
- """Create structured hexahedral mesh"""
- n_nodes = nx * ny * nz
- n_elements = (nx - 1) * (ny - 1) * (nz - 1)
-
- # Create nodes
- nodes = Node[]
- for k in 1:nz, j in 1:ny, i in 1:nx
- node_id = (k - 1) * nx * ny + (j - 1) * nx + i
- push!(nodes, Node(node_id, Float64(i), Float64(j), Float64(k)))
- end
-
- # Create elements (Hex8)
- elements = Element[]
- for k in 1:(nz-1), j in 1:(ny-1), i in 1:(nx-1)
- n1 = (k - 1) * nx * ny + (j - 1) * nx + i
- n2 = n1 + 1
- n3 = n2 + nx
- n4 = n1 + nx
- n5 = n1 + nx * ny
- n6 = n2 + nx * ny
- n7 = n3 + nx * ny
- n8 = n4 + nx * ny
-
- elem_id = length(elements) + 1
- push!(elements, Element(elem_id, (n1, n2, n3, n4, n5, n6, n7, n8)))
- end
-
- return nodes, elements
-end
-
-function build_node_to_elements(nodes, elements)
- """Build inverse connectivity"""
- node_to_elems = [Int[] for _ in 1:length(nodes)]
-
- for (elem_id, element) in enumerate(elements)
- for node_id in element.connectivity
- push!(node_to_elems[node_id], elem_id)
- end
- end
-
- return node_to_elems
-end
-
-# ============================================================================
-# Partitioning
-# ============================================================================
-
-function partition_mesh(nodes, elements, n_partitions)
- """Partition mesh by nodes"""
- n_nodes = length(nodes)
- nodes_per_partition = ceil(Int, n_nodes / n_partitions)
-
- node_to_elems = build_node_to_elements(nodes, elements)
-
- partitions = Partition[]
-
- for rank in 0:(n_partitions-1)
- # Owned nodes
- start_node = rank * nodes_per_partition + 1
- end_node = min((rank + 1) * nodes_per_partition, n_nodes)
- owned_nodes = start_node:end_node
-
- # Find elements touching owned nodes
- local_elements = Int[]
- ghost_nodes = Set{Int}()
-
- for (elem_id, element) in enumerate(elements)
- if any(nid in owned_nodes for nid in element.connectivity)
- push!(local_elements, elem_id)
-
- # Mark ghost nodes
- for nid in element.connectivity
- if !(nid in owned_nodes)
- push!(ghost_nodes, nid)
- end
- end
- end
- end
-
- # Build local node_to_elements (owned nodes only)
- local_node_to_elems = [node_to_elems[nid] for nid in owned_nodes]
-
- # Find interface nodes (owned nodes that couple to other partitions)
- interface = Dict{Int,Vector{Int}}()
- for neighbor_rank in 0:(n_partitions-1)
- if neighbor_rank == rank
- continue
- end
-
- neighbor_start = neighbor_rank * nodes_per_partition + 1
- neighbor_end = min((neighbor_rank + 1) * nodes_per_partition, n_nodes)
- neighbor_owned = neighbor_start:neighbor_end
-
- interface_with_neighbor = Int[]
- for elem_id in local_elements
- element = elements[elem_id]
- has_owned = any(nid in owned_nodes for nid in element.connectivity)
- has_neighbor = any(nid in neighbor_owned for nid in element.connectivity)
-
- if has_owned && has_neighbor
- for nid in element.connectivity
- if nid in owned_nodes && !(nid in interface_with_neighbor)
- push!(interface_with_neighbor, nid)
- end
- end
- end
- end
-
- if !isempty(interface_with_neighbor)
- interface[neighbor_rank] = interface_with_neighbor
- end
- end
-
- partition = Partition(
- rank,
- owned_nodes,
- collect(ghost_nodes),
- local_elements,
- local_node_to_elems,
- interface
- )
-
- push!(partitions, partition)
- end
-
- return partitions
-end
-
-# ============================================================================
-# Nodal Assembly (Matrix-Vector Product)
-# ============================================================================
-
-function matvec_single_threaded!(y, x, nodes, elements, node_to_elements)
- """Single-threaded nodal assembly"""
- fill!(y, 0.0)
-
- for node_id in 1:length(nodes)
- node = nodes[node_id]
-
- # Get node DOFs
- dof_start = (node_id - 1) * 3 + 1
- y_nodal = zeros(3)
-
- # Gather from connected elements
- for elem_id in node_to_elements[node_id]
- element = elements[elem_id]
-
- # Mock element contribution (just for timing)
- for nid in element.connectivity
- x_dof_start = (nid - 1) * 3 + 1
- for d in 1:3
- y_nodal[d] += 0.1 * x[x_dof_start+d-1]
- end
- end
- end
-
- # Write to global
- for d in 1:3
- y[dof_start+d-1] = y_nodal[d]
- end
- end
-end
-
-function matvec_multi_threaded!(y, x, nodes, elements, node_to_elements)
- """Multi-threaded nodal assembly (direct, no partitioning)"""
- fill!(y, 0.0)
-
- @threads for node_id in 1:length(nodes)
- node = nodes[node_id]
-
- dof_start = (node_id - 1) * 3 + 1
- y_nodal = zeros(3)
-
- for elem_id in node_to_elements[node_id]
- element = elements[elem_id]
-
- for nid in element.connectivity
- x_dof_start = (nid - 1) * 3 + 1
- for d in 1:3
- y_nodal[d] += 0.1 * x[x_dof_start+d-1]
- end
- end
- end
-
- for d in 1:3
- y[dof_start+d-1] = y_nodal[d]
- end
- end
-end
-
-function matvec_partitioned!(y, x, nodes, elements, partitions)
- """Multi-threaded with explicit partitioning (simulates multi-GPU)"""
- fill!(y, 0.0)
-
- # Each partition processed by one thread
- @threads for partition in partitions
- # Process owned nodes
- for (local_idx, node_id) in enumerate(partition.owned_nodes)
- node = nodes[node_id]
-
- dof_start = (node_id - 1) * 3 + 1
- y_nodal = zeros(3)
-
- for elem_id in partition.node_to_elements[local_idx]
- element = elements[elem_id]
-
- for nid in element.connectivity
- x_dof_start = (nid - 1) * 3 + 1
- for d in 1:3
- y_nodal[d] += 0.1 * x[x_dof_start+d-1]
- end
- end
- end
-
- for d in 1:3
- y[dof_start+d-1] = y_nodal[d]
- end
- end
- end
-end
-
-# ============================================================================
-# Benchmarks
-# ============================================================================
-
-function run_benchmark(name, nx, ny, nz, n_warmup=2, n_runs=10)
- println("\n" * "="^70)
- println("Benchmark: $name")
- println(" Mesh: $nx × $ny × $nz = $(nx*ny*nz) nodes, $((nx-1)*(ny-1)*(nz-1)) elements")
- println("="^70)
-
- # Create mesh
- print("Creating mesh... ")
- nodes, elements = create_hex_mesh(nx, ny, nz)
- node_to_elements = build_node_to_elements(nodes, elements)
- n_dofs = 3 * length(nodes)
- println("✓")
-
- println(" Nodes: ", length(nodes))
- println(" Elements: ", length(elements))
- println(" DOFs: ", n_dofs)
- println(" Avg elements/node: ", sum(length.(node_to_elements)) / length(nodes))
-
- # Test vectors
- x = randn(n_dofs)
- y_ref = zeros(n_dofs)
- y_test = zeros(n_dofs)
-
- # ========================================================================
- # 1. Single-threaded baseline
- # ========================================================================
- println("\n1. Single-threaded baseline:")
-
- # Warmup
- for _ in 1:n_warmup
- matvec_single_threaded!(y_ref, x, nodes, elements, node_to_elements)
- end
-
- # Benchmark
- times = Float64[]
- for _ in 1:n_runs
- t_start = time_ns()
- matvec_single_threaded!(y_ref, x, nodes, elements, node_to_elements)
- t_end = time_ns()
- push!(times, (t_end - t_start) / 1e9)
- end
-
- t_single = minimum(times)
- println(" Time: ", @sprintf("%.3f ms", t_single * 1000))
- println(" Throughput: ", @sprintf("%.2f Mnodes/s", length(nodes) / t_single / 1e6))
-
- # ========================================================================
- # 2. Multi-threaded (all available threads)
- # ========================================================================
- if nthreads() > 1
- println("\n2. Multi-threaded ($(nthreads()) threads):")
-
- # Warmup
- for _ in 1:n_warmup
- matvec_multi_threaded!(y_test, x, nodes, elements, node_to_elements)
- end
-
- # Verify correctness
- error = norm(y_test - y_ref) / norm(y_ref)
- println(" Verification: ", error < 1e-10 ? "✓ PASS" : "✗ FAIL (error=$error)")
-
- # Benchmark
- times = Float64[]
- for _ in 1:n_runs
- t_start = time_ns()
- matvec_multi_threaded!(y_test, x, nodes, elements, node_to_elements)
- t_end = time_ns()
- push!(times, (t_end - t_start) / 1e9)
- end
-
- t_multi = minimum(times)
- speedup = t_single / t_multi
- efficiency = speedup / nthreads() * 100
-
- println(" Time: ", @sprintf("%.3f ms", t_multi * 1000))
- println(" Speedup: ", @sprintf("%.2fx", speedup))
- println(" Efficiency: ", @sprintf("%.1f%%", efficiency))
- println(" Throughput: ", @sprintf("%.2f Mnodes/s", length(nodes) / t_multi / 1e6))
- end
-
- # ========================================================================
- # 3. Partitioned (simulates multi-GPU)
- # ========================================================================
- if nthreads() >= 4
- n_partitions = 4
- println("\n3. Partitioned ($n_partitions partitions, 4 threads):")
-
- # Create partitions
- print(" Creating partitions... ")
- partitions = partition_mesh(nodes, elements, n_partitions)
- println("✓")
-
- # Print partition info
- for partition in partitions
- n_owned = length(partition.owned_nodes)
- n_ghost = length(partition.ghost_nodes)
- n_interface = sum(length.(values(partition.interface_nodes)))
- interface_pct = n_interface / n_owned * 100
-
- println(" Partition $(partition.rank): $n_owned owned, $n_ghost ghost, " *
- "$n_interface interface ($(round(interface_pct, digits=1))%)")
- end
-
- # Warmup
- for _ in 1:n_warmup
- matvec_partitioned!(y_test, x, nodes, elements, partitions)
- end
-
- # Verify correctness
- error = norm(y_test - y_ref) / norm(y_ref)
- println(" Verification: ", error < 1e-10 ? "✓ PASS" : "✗ FAIL (error=$error)")
-
- # Benchmark
- times = Float64[]
- for _ in 1:n_runs
- t_start = time_ns()
- matvec_partitioned!(y_test, x, nodes, elements, partitions)
- t_end = time_ns()
- push!(times, (t_end - t_start) / 1e9)
- end
-
- t_partitioned = minimum(times)
- speedup = t_single / t_partitioned
- efficiency = speedup / n_partitions * 100
-
- println(" Time: ", @sprintf("%.3f ms", t_partitioned * 1000))
- println(" Speedup: ", @sprintf("%.2fx", speedup))
- println(" Efficiency: ", @sprintf("%.1f%%", efficiency))
- println(" Throughput: ", @sprintf("%.2f Mnodes/s", length(nodes) / t_partitioned / 1e6))
- end
-
- println()
-end
-
-# ============================================================================
-# Run Benchmarks
-# ============================================================================
-
-# Small mesh
-run_benchmark("Small Mesh", 20, 20, 20)
-
-# Medium mesh
-run_benchmark("Medium Mesh", 40, 40, 40)
-
-# Large mesh (if enough threads)
-if nthreads() >= 4
- run_benchmark("Large Mesh", 60, 60, 60)
-end
-
-println("="^70)
-println("✓ Benchmark Complete")
-println("="^70)
-println()
-println("Notes:")
-println(" - Single-threaded: Baseline performance")
-println(" - Multi-threaded: All available threads, direct parallelization")
-println(" - Partitioned: Simulates multi-GPU with explicit partitions")
-println(" - Efficiency = Speedup / N_threads × 100%")
-println(" - Near 100% efficiency = perfect scaling")
-println()
diff --git a/benchmarks/perfect_plasticity_analysis.jl b/benchmarks/perfect_plasticity_analysis.jl
deleted file mode 100644
index e06ea4d..0000000
--- a/benchmarks/perfect_plasticity_analysis.jl
+++ /dev/null
@@ -1,320 +0,0 @@
-"""
-Performance Analysis: PerfectPlasticity Material Model
-
-Comprehensive benchmarking of J2 plasticity implementation with radial return mapping.
-
-Tests:
-1. Single evaluation performance (elastic vs plastic)
-2. Zero-allocation verification
-3. Type stability verification
-4. State overhead measurement
-5. Hardening parameter sensitivity
-6. Assembly loop simulation
-7. Comparison to LinearElastic and NeoHookean
-8. Strain level scalability
-
-Run with:
- julia --project=. benchmarks/perfect_plasticity_analysis.jl
-"""
-
-using BenchmarkTools
-using Tensors
-using Statistics
-using Printf
-using Dates
-
-# Load implementations
-include("../src/materials/abstract_material.jl")
-include("../src/materials/linear_elastic.jl")
-include("../src/materials/neo_hookean.jl")
-include("../src/materials/perfect_plasticity.jl")
-
-println("="^80)
-println("PERFECT PLASTICITY MATERIAL - PERFORMANCE ANALYSIS")
-println("="^80)
-println()
-
-# ==============================================================================
-# TEST 1: Single Evaluation - Elastic Path
-# ==============================================================================
-println("TEST 1: Single Evaluation - Elastic Path")
-println("-"^80)
-
-steel = PerfectPlasticity(E=200e9, ν=0.3, σ_y=250e6, H=1e9)
-ε_elastic = SymmetricTensor{2,3}((1e-5, 0.0, 0.0, 0.0, 0.0, 0.0)) # Below yield
-state = PlasticityState()
-
-# Benchmark elastic path
-bench_elastic = @benchmark compute_stress($steel, $ε_elastic, $state, 0.0)
-t_elastic = median(bench_elastic.times)
-allocs_elastic = bench_elastic.allocs
-
-println("Elastic path (no yielding):")
-println(" Time: ", @sprintf("%.2f ns", t_elastic))
-println(" Allocations: ", allocs_elastic)
-println(" Memory: ", bench_elastic.memory, " bytes")
-println()
-
-# ==============================================================================
-# TEST 2: Single Evaluation - Plastic Path
-# ==============================================================================
-println("TEST 2: Single Evaluation - Plastic Path")
-println("-"^80)
-
-ε_plastic = SymmetricTensor{2,3}((0.003, 0.0, 0.0, 0.0, 0.0, 0.0)) # Beyond yield
-
-# Benchmark plastic path
-bench_plastic = @benchmark compute_stress($steel, $ε_plastic, $state, 0.0)
-t_plastic = median(bench_plastic.times)
-allocs_plastic = bench_plastic.allocs
-
-println("Plastic path (radial return):")
-println(" Time: ", @sprintf("%.2f ns", t_plastic))
-println(" Allocations: ", allocs_plastic)
-println(" Memory: ", bench_plastic.memory, " bytes")
-println()
-
-println("Plastic overhead:")
-println(" Ratio: ", @sprintf("%.2fx", t_plastic / t_elastic))
-println()
-
-# ==============================================================================
-# TEST 3: State Management Overhead
-# ==============================================================================
-println("TEST 3: State Management Overhead")
-println("-"^80)
-
-# Compare with and without state history
-σ1, 𝔻1, state1 = compute_stress(steel, ε_plastic, nothing, 0.0) # Fresh state
-σ2, 𝔻2, state2 = compute_stress(steel, ε_plastic, state1, 0.0) # With history
-
-bench_fresh = @benchmark compute_stress($steel, $ε_plastic, nothing, 0.0)
-bench_history = @benchmark compute_stress($steel, $ε_plastic, $state1, 0.0)
-
-println("Fresh state (ε_p = 0, α = 0):")
-println(" Time: ", @sprintf("%.2f ns", median(bench_fresh.times)))
-println()
-println("With history (ε_p ≠ 0, α ≠ 0):")
-println(" Time: ", @sprintf("%.2f ns", median(bench_history.times)))
-println()
-println("State overhead: ", @sprintf("%.1f%%",
- (median(bench_history.times) - median(bench_fresh.times)) / median(bench_fresh.times) * 100))
-println()
-
-# ==============================================================================
-# TEST 4: Hardening Parameter Sensitivity
-# ==============================================================================
-println("TEST 4: Hardening Parameter Sensitivity")
-println("-"^80)
-
-hardening_values = [0.0, 1e8, 1e9, 10e9, 100e9] # Perfect to strong hardening
-times_H = Float64[]
-
-for H in hardening_values
- mat = PerfectPlasticity(E=200e9, ν=0.3, σ_y=250e6, H=H)
- bench = @benchmark compute_stress($mat, $ε_plastic, $state, 0.0)
- push!(times_H, median(bench.times))
-end
-
-println("H (Pa) Time (ns) Overhead")
-println(repeat("-", 45))
-for (H, t) in zip(hardening_values, times_H)
- overhead = (t - times_H[1]) / times_H[1] * 100
- println(@sprintf("%-15.1e %8.2f %+6.1f%%", H, t, overhead))
-end
-println()
-
-# ==============================================================================
-# TEST 5: Comparison to Other Materials
-# ==============================================================================
-println("TEST 5: Comparison to Other Materials")
-println("-"^80)
-
-# LinearElastic
-linear = LinearElastic(E=200e9, ν=0.3)
-bench_linear = @benchmark compute_stress($linear, $ε_plastic)
-t_linear = median(bench_linear.times)
-
-# NeoHookean (uses Green-Lagrange strain for small deformation)
-μ_neo = 200e9 / (2 * (1 + 0.3))
-λ_neo = 200e9 * 0.3 / ((1 + 0.3) * (1 - 2 * 0.3))
-neo = NeoHookean(μ=μ_neo, λ=λ_neo)
-E_gl = ε_plastic # For small strains, E_GL ≈ ε
-bench_neo = @benchmark compute_stress($neo, $E_gl)
-t_neo = median(bench_neo.times)
-
-println("Material Time (ns) Ratio vs Linear")
-println(repeat("-", 50))
-println(@sprintf("LinearElastic %8.2f 1.00x (baseline)", t_linear))
-println(@sprintf("PerfectPlasticity %8.2f %.2fx", t_plastic, t_plastic / t_linear))
-println(@sprintf("NeoHookean %8.2f %.2fx", t_neo, t_neo / t_linear))
-println()
-
-println("Performance ranking:")
-println(" 1. LinearElastic (fastest, no state, manual derivatives)")
-println(" 2. PerfectPlasticity (", @sprintf("%.1fx", t_plastic / t_linear),
- " - state management + radial return)")
-println(" 3. NeoHookean (", @sprintf("%.1fx", t_neo / t_linear),
- " - automatic differentiation overhead)")
-println()
-
-# ==============================================================================
-# TEST 6: Assembly Loop Simulation
-# ==============================================================================
-println("TEST 6: Assembly Loop Simulation (1000 Gauss points)")
-println("-"^80)
-
-n_gauss = 1000
-strains = [SymmetricTensor{2,3}((0.001 + 0.002 * rand(), 0.0, 0.0, 0.0, 0.0, 0.0))
- for _ in 1:n_gauss]
-
-# Elastic assembly
-function assembly_elastic(material, strains)
- total = zero(SymmetricTensor{2,3})
- for ε in strains
- σ, _, _ = compute_stress(material, ε)
- total += σ
- end
- return total
-end
-
-# Plastic assembly (stateful)
-function assembly_plastic(material, strains, state)
- total = zero(SymmetricTensor{2,3})
- for ε in strains
- σ, _, state = compute_stress(material, ε, state, 0.0)
- total += σ
- end
- return total, state
-end
-
-bench_asm_linear = @benchmark assembly_elastic($linear, $strains)
-bench_asm_plastic = @benchmark assembly_plastic($steel, $strains, $state)
-
-t_asm_linear = median(bench_asm_linear.times) / 1e6 # Convert to ms
-t_asm_plastic = median(bench_asm_plastic.times) / 1e6
-
-println("LinearElastic assembly: ", @sprintf("%.3f ms", t_asm_linear))
-println("PerfectPlasticity assembly: ", @sprintf("%.3f ms", t_asm_plastic))
-println("Overhead: ", @sprintf("%.2fx", t_asm_plastic / t_asm_linear))
-println()
-
-# ==============================================================================
-# TEST 7: Strain Level Scalability
-# ==============================================================================
-println("TEST 7: Strain Level Scalability")
-println("-"^80)
-
-strain_magnitudes = [0.0005, 0.001, 0.002, 0.005, 0.01, 0.02]
-times_strain = Float64[]
-yields = Bool[]
-
-for ε_mag in strain_magnitudes
- ε_test = SymmetricTensor{2,3}((ε_mag, 0.0, 0.0, 0.0, 0.0, 0.0))
- σ_test, _, state_test = compute_stress(steel, ε_test, nothing, 0.0)
-
- bench = @benchmark compute_stress($steel, $ε_test, nothing, 0.0)
- push!(times_strain, median(bench.times))
- push!(yields, state_test.κ > 0.0)
-end
-
-println("ε_magnitude Time (ns) Yielded?")
-println(repeat("-", 40))
-for (ε_mag, t, y) in zip(strain_magnitudes, times_strain, yields)
- status = y ? "YES" : "no"
- println(@sprintf("%.4f %8.2f %s", ε_mag, t, status))
-end
-println()
-
-# ==============================================================================
-# TEST 8: Cyclic Loading Performance
-# ==============================================================================
-println("TEST 8: Cyclic Loading (Bauschinger Effect)")
-println("-"^80)
-
-# Simulate cyclic loading path
-ε_cycle = [
- SymmetricTensor{2,3}((0.003, 0.0, 0.0, 0.0, 0.0, 0.0)), # Tension
- SymmetricTensor{2,3}((0.0, 0.0, 0.0, 0.0, 0.0, 0.0)), # Unload
- SymmetricTensor{2,3}((-0.002, 0.0, 0.0, 0.0, 0.0, 0.0)), # Compression
- SymmetricTensor{2,3}((0.0, 0.0, 0.0, 0.0, 0.0, 0.0)), # Unload
-]
-
-function cyclic_loading(material, strains, state)
- for ε in strains
- σ, 𝔻, state = compute_stress(material, ε, state, 0.0)
- end
- return state
-end
-
-bench_cyclic = @benchmark cyclic_loading($steel, $ε_cycle, $state)
-t_cyclic = median(bench_cyclic.times)
-
-println("Cyclic loading (4 load steps):")
-println(" Total time: ", @sprintf("%.2f ns", t_cyclic))
-println(" Per load step: ", @sprintf("%.2f ns", t_cyclic / 4))
-println()
-
-# ==============================================================================
-# TEST 9: Type Stability Verification
-# ==============================================================================
-println("TEST 9: Type Stability")
-println("-"^80)
-
-using InteractiveUtils
-
-println("Return type inference:")
-result_type = @code_typed compute_stress(steel, ε_plastic, state, 0.0)
-println(" ✓ Type stable: ", result_type[2])
-println()
-
-# ==============================================================================
-# SUMMARY
-# ==============================================================================
-println("="^80)
-println("SUMMARY")
-println("="^80)
-println()
-
-println("Performance Characteristics:")
-println(" • Elastic path: ", @sprintf("%.0f ns", t_elastic), " (no allocations)")
-println(" • Plastic path: ", @sprintf("%.0f ns", t_plastic), " (~128 bytes for state)")
-println(" • Plastic overhead:", @sprintf("%.2fx", t_plastic / t_elastic))
-println()
-
-println("Comparison to other materials:")
-println(" • ", @sprintf("%.2fx", t_plastic / t_linear), " slower than LinearElastic (baseline)")
-println(" • ", @sprintf("%.2fx", t_neo / t_plastic), " faster than NeoHookean (AD)")
-println()
-
-println("Key findings:")
-println(" ✓ Zero allocations on elastic path")
-println(" ✓ Minimal allocations on plastic path (state struct only)")
-println(" ✓ Type stable")
-println(" ✓ Hardening parameter has negligible performance impact")
-println(" ✓ Performance independent of strain level")
-println(" ✓ Suitable for production FEM with ~",
- @sprintf("%.0f", 1e9 / t_plastic), " evaluations/second")
-println()
-
-println("Recommendations:")
-if t_plastic < 500
- println(" ✓ Excellent performance - suitable for all applications")
-elseif t_plastic < 1000
- println(" ✓ Good performance - suitable for most applications")
- println(" • Consider caching for problems with >10M DOF")
-else
- println(" ⚠ Acceptable performance - profile before using with >1M DOF")
- println(" • Consider precomputation for repeated analyses")
-end
-println()
-
-println("Expected performance in FEM assembly:")
-println(" • Small problems (<10K DOF): Negligible overhead")
-println(" • Medium problems (10K-1M DOF): ", @sprintf("<%.1f seconds", 1e6 * t_plastic / 1e9))
-println(" • Large problems (>1M DOF): ", @sprintf("<%.1f seconds", 1e7 * t_plastic / 1e9))
-println()
-
-println("="^80)
-println("Analysis complete: ", now())
-println("="^80)
diff --git a/benchmarks/results/benchmark_output.txt b/benchmarks/results/benchmark_output.txt
deleted file mode 100644
index b7ba028..0000000
--- a/benchmarks/results/benchmark_output.txt
+++ /dev/null
@@ -1,348 +0,0 @@
-Precompiling packages...
- 669.3 ms ✓ EpollShim_jll
- 725.5 ms ✓ libfdk_aac_jll
- 760.9 ms ✓ Graphite2_jll
- 752.3 ms ✓ fzf_jll
- 756.4 ms ✓ LERC_jll
- 822.1 ms ✓ Xorg_libICE_jll
- 815.2 ms ✓ LAME_jll
- 797.9 ms ✓ Ogg_jll
- 788.9 ms ✓ x265_jll
- 829.1 ms ✓ libaom_jll
- 849.2 ms ✓ mtdev_jll
- 843.9 ms ✓ MbedTLS_jll
- 840.0 ms ✓ x264_jll
- 860.8 ms ✓ XZ_jll
- 674.8 ms ✓ libevdev_jll
- 769.9 ms ✓ Opus_jll
- 743.6 ms ✓ eudev_jll
- 738.4 ms ✓ FriBidi_jll
- 707.7 ms ✓ Dbus_jll
- 730.8 ms ✓ Xorg_libxkbfile_jll
- 779.8 ms ✓ Xorg_xcb_util_jll
- 770.5 ms ✓ Xorg_libXi_jll
- 772.1 ms ✓ Xorg_libXrandr_jll
- 793.7 ms ✓ Xorg_libXcursor_jll
- 892.3 ms ✓ Wayland_jll
- 2016.1 ms ✓ ColorVectorSpace
- 1008.1 ms ✓ HarfBuzz_jll
- 711.6 ms ✓ JLFzf
- 1304.3 ms ✓ Ghostscript_jll
- 737.4 ms ✓ Xorg_libSM_jll
- 1455.9 ms ✓ RecipesBase
- 803.4 ms ✓ libvorbis_jll
- 731.4 ms ✓ libinput_jll
- 785.6 ms ✓ Libtiff_jll
- 742.5 ms ✓ Xorg_xkbcomp_jll
- 731.2 ms ✓ Xorg_xcb_util_image_jll
- 733.0 ms ✓ Xorg_xcb_util_keysyms_jll
- 741.3 ms ✓ Xorg_xcb_util_renderutil_jll
- 2530.6 ms ✓ StatsBase
- 764.4 ms ✓ Xorg_xcb_util_wm_jll
- 1120.5 ms ✓ MbedTLS
- 879.4 ms ✓ libass_jll
- 653.5 ms ✓ Xorg_xkeyboard_config_jll
- 962.5 ms ✓ Pango_jll
- 711.2 ms ✓ Xorg_xcb_util_cursor_jll
- 758.4 ms ✓ xkbcommon_jll
- 1332.1 ms ✓ FFMPEG_jll
- 999.6 ms ✓ Vulkan_Loader_jll
- 1149.2 ms ✓ libdecor_jll
- 839.5 ms ✓ FFMPEG
- 2960.5 ms ✓ Latexify
- 974.9 ms ✓ GLFW_jll
- 4035.5 ms ✓ ColorSchemes
- 852.9 ms ✓ Latexify → SparseArraysExt
- 1490.1 ms ✓ Qt6Base_jll
- 918.9 ms ✓ Qt6ShaderTools_jll
- 1397.3 ms ✓ GR_jll
- 2814.7 ms ✓ Qt6Declarative_jll
- 1317.4 ms ✓ Qt6Wayland_jll
- 7309.2 ms ✓ PlotUtils
- 12361.6 ms ✓ HTTP
- 2980.3 ms ✓ PlotThemes
- 3588.3 ms ✓ RecipesPipeline
- 5614.7 ms ✓ GR
- 64840.3 ms ✓ Plots
- 65 dependencies successfully precompiled in 87 seconds. 112 already precompiled.
-================================================================================
-SYSTEM INFORMATION
-================================================================================
-
-CPU Model: Intel(R) Xeon(R) Gold 6326 CPU @ 2.90GHz
-CPU Cores: 32 threads (32 physical cores)
-CPU Speed: 3300 MHz
-
-Julia Version: 1.12.1
-OS: Linux x86_64-linux-gnu
-Word Size: 64 bits
-
-Approximate CPU Cache Sizes:
- L1 Cache: ~32-64 KB per core (typical)
- L2 Cache: ~256-512 KB per core (typical)
- L3 Cache: ~8-32 MB shared (typical)
-
-Note: Testing up to 8KB structs to exceed L1 cache
-
-================================================================================
-SYSTEM INFORMATION
-================================================================================
-Julia Version: 1.12.1
-CPU Model: Intel(R) Xeon(R) Gold 6326 CPU @ 2.90GHz
-CPU Cores: 32
-Total Memory: 503.35 GB
-L1 Cache: 48K
-L2 Cache: 1280K
-L3 Cache: 24576K
-
-================================================================================
-STRUCT SIZE SCALING BENCHMARK
-================================================================================
-
-Testing hypothesis: Immutable slows down with struct size, mutable stays constant
-
-Testing struct with 1 Float64 fields (8 bytes)...
- Access: Mut=4.62ns Imm=2.02ns Speedup=2.3x
- Update: Mut=7.2ns Imm=2.32ns Speedup=3.1x
- Iterate: Mut=11.77ns Imm=2.02ns Speedup=5.8x
- Copy: 2.02ns
-
-Testing struct with 2 Float64 fields (16 bytes)...
- Access: Mut=4.9ns Imm=2.02ns Speedup=2.4x
- Update: Mut=7.21ns Imm=2.31ns Speedup=3.1x
- Iterate: Mut=14.49ns Imm=2.02ns Speedup=7.2x
- Copy: 2.03ns
-
-Testing struct with 5 Float64 fields (40 bytes)...
- Access: Mut=4.9ns Imm=2.37ns Speedup=2.1x
- Update: Mut=7.2ns Imm=2.6ns Speedup=2.8x
- Iterate: Mut=16.71ns Imm=2.31ns Speedup=7.2x
- Copy: 2.38ns
-
-Testing struct with 10 Float64 fields (80 bytes)...
- Access: Mut=4.62ns Imm=2.03ns Speedup=2.3x
- Update: Mut=7.2ns Imm=2.6ns Speedup=2.8x
- Iterate: Mut=23.13ns Imm=2.6ns Speedup=8.9x
- Copy: 3.31ns
-
-Testing struct with 20 Float64 fields (160 bytes)...
- Access: Mut=4.62ns Imm=2.02ns Speedup=2.3x
- Update: Mut=7.2ns Imm=3.16ns Speedup=2.3x
- Iterate: Mut=63.13ns Imm=5.52ns Speedup=11.4x
- Copy: 3.18ns
-
-Testing struct with 50 Float64 fields (400 bytes)...
- Access: Mut=4.62ns Imm=2.03ns Speedup=2.3x
- Update: Mut=7.2ns Imm=6.02ns Speedup=1.2x
- Iterate: Mut=196.85ns Imm=24.78ns Speedup=7.9x
- Copy: 6.9ns
-
-Testing struct with 100 Float64 fields (800 bytes)...
- Access: Mut=4.62ns Imm=2.02ns Speedup=2.3x
- Update: Mut=7.2ns Imm=11.75ns Speedup=0.6x
- Iterate: Mut=260.56ns Imm=68.5ns Speedup=3.8x
- Copy: 9.48ns
-
-Testing struct with 200 Float64 fields (1600 bytes)...
- Access: Mut=4.62ns Imm=2.37ns Speedup=1.9x
- Update: Mut=7.41ns Imm=24.63ns Speedup=0.3x
- Iterate: Mut=777.91ns Imm=155.16ns Speedup=5.0x
- Copy: 20.95ns
-
-Testing struct with 500 Float64 fields (4000 bytes)...
- Access: Mut=4.62ns Imm=2.03ns Speedup=2.3x
- Update: Mut=7.2ns Imm=80.84ns Speedup=0.1x
- Iterate: Mut=1156.3ns Imm=499.36ns Speedup=2.3x
- Copy: 58.68ns
-
-Testing struct with 1000 Float64 fields (8000 bytes)...
- Access: Mut=4.67ns Imm=2.08ns Speedup=2.2x
- Update: Mut=7.2ns Imm=193.26ns Speedup=0.0x
- Iterate: Mut=3465.75ns Imm=1100.2ns Speedup=3.2x
- Copy: 45.7ns
-
-Testing struct with 2000 Float64 fields (16000 bytes)...
- Access: Mut=4.62ns Imm=2.02ns Speedup=2.3x
- Update: Mut=7.2ns Imm=410.85ns Speedup=0.0x
- Iterate: Mut=4811.71ns Imm=2226.22ns Speedup=2.2x
- Copy: 82.95ns
-
-Testing struct with 5000 Float64 fields (40000 bytes)...
- Access: Mut=4.62ns Imm=2.08ns Speedup=2.2x
- Update: Mut=7.2ns Imm=1936.1ns Speedup=0.0x
- Iterate: Mut=33969.0ns Imm=5667.17ns Speedup=6.0x
- Copy: 867.04ns
-
-================================================================================
-RESULTS SUMMARY
-================================================================================
-
-Field Access Performance:
-Size (fields) | Bytes | Mutable (ns) | Immutable (ns) | Speedup
-----------------------------------------------------------------------
- 1 | 8 | 4.62 | 2.02 | 2.3x
- 2 | 16 | 4.90 | 2.02 | 2.4x
- 5 | 40 | 4.90 | 2.37 | 2.1x
- 10 | 80 | 4.62 | 2.03 | 2.3x
- 20 | 160 | 4.62 | 2.02 | 2.3x
- 50 | 400 | 4.62 | 2.03 | 2.3x
- 100 | 800 | 4.62 | 2.02 | 2.3x
- 200 | 1600 | 4.62 | 2.37 | 1.9x
- 500 | 4000 | 4.62 | 2.03 | 2.3x
- 1000 | 8000 | 4.67 | 2.08 | 2.2x
- 2000 | 16000 | 4.62 | 2.02 | 2.3x
- 5000 | 40000 | 4.62 | 2.08 | 2.2x
-
-Field Update Performance:
-Size (fields) | Bytes | Mutable (ns) | Immutable (ns) | Speedup
-----------------------------------------------------------------------
- 1 | 8 | 7.20 | 2.31 | 3.1x
- 2 | 16 | 7.21 | 2.31 | 3.1x
- 5 | 40 | 7.20 | 2.60 | 2.8x
- 10 | 80 | 7.20 | 2.60 | 2.8x
- 20 | 160 | 7.20 | 3.16 | 2.3x
- 50 | 400 | 7.20 | 6.02 | 1.2x
- 100 | 800 | 7.20 | 11.75 | 0.6x
- 200 | 1600 | 7.41 | 24.63 | 0.3x
- 500 | 4000 | 7.20 | 80.84 | 0.1x
- 1000 | 8000 | 7.20 | 193.26 | 0.0x
- 2000 | 16000 | 7.20 | 410.85 | 0.0x
- 5000 | 40000 | 7.20 | 1936.10 | 0.0x
-
-Iteration Performance:
-Size (fields) | Bytes | Mutable (ns) | Immutable (ns) | Speedup
-----------------------------------------------------------------------
- 1 | 8 | 11.77 | 2.02 | 5.8x
- 2 | 16 | 14.49 | 2.02 | 7.2x
- 5 | 40 | 16.71 | 2.31 | 7.2x
- 10 | 80 | 23.13 | 2.60 | 8.9x
- 20 | 160 | 63.13 | 5.52 | 11.4x
- 50 | 400 | 196.85 | 24.78 | 7.9x
- 100 | 800 | 260.56 | 68.50 | 3.8x
- 200 | 1600 | 777.91 | 155.16 | 5.0x
- 500 | 4000 | 1156.30 | 499.36 | 2.3x
- 1000 | 8000 | 3465.75 | 1100.20 | 3.2x
- 2000 | 16000 | 4811.71 | 2226.22 | 2.2x
- 5000 | 40000 | 33969.00 | 5667.17 | 6.0x
-
-Immutable Copy Cost (ns):
-Size (fields) | Bytes | Copy Time (ns)
-----------------------------------------
- 1 | 8 | 2.02
- 2 | 16 | 2.03
- 5 | 40 | 2.38
- 10 | 80 | 3.31
- 20 | 160 | 3.18
- 50 | 400 | 6.90
- 100 | 800 | 9.48
- 200 | 1600 | 20.95
- 500 | 4000 | 58.68
- 1000 | 8000 | 45.70
- 2000 | 16000 | 82.95
- 5000 | 40000 | 867.04
-
-================================================================================
-ANALYSIS
-================================================================================
-
-✓ Immutable ALWAYS faster for field access (even at 1000 fields = 8KB)
- Minimum speedup: 1.9x at 5000 fields
-
-⚠ Mutable wins for field update at 100 fields
-
-✓ Immutable ALWAYS faster for iteration (even at 1000 fields = 8KB)
- Minimum speedup: 2.2x at 5000 fields
-
-Scaling Analysis:
-
-Copy time scaling:
- Linear fit: time(ns) = -25.47 + 0.1587 * nfields
- Per-field cost: 0.1587 ns/field
- Base overhead: -25.47 ns
-
-Is copy time linear? (checking R²)
- R² = 0.9006
- ⚠ Copy time not perfectly linear (compiler optimizations?)
-
-KEY INSIGHT:
---------------------------------------------------------------------------------
-
-Even at 1000 fields (8KB struct), immutable is STILL faster because:
- 1. Dict lookup cost (~40-50ns) >> copy cost per field (~0.1587ns)
- 2. Type stability enables compiler optimizations (inlining, SIMD)
- 3. Stack allocation has better cache locality than heap pointers
-
-Theoretical crossover point (if it exists):
- Would occur at ~413 fields (3KB)
-
-================================================================================
-CONCLUSION
-================================================================================
-
-Your intuition about O(n) scaling is CORRECT, BUT:
-
- • Dict lookup base cost is SO high (~40-50ns)
- • Copy cost per field is SO low (~0.1587ns)
- • Compiler optimizations are SO good (inlining, SIMD, escape analysis)
-
-That immutable wins even for unrealistically large structs (8KB+)!
-
-For typical FEM elements:
- • Material properties: 3-10 fields (24-80 bytes)
- • State variables: 10-50 fields (80-400 bytes)
- • Even with 100 fields (800 bytes), immutable is >10x faster
-
-Type stability > Everything else.
-
-================================================================================
-SAVING DATA
-================================================================================
-
-✓ Data saved to: /home/juajukka/dev/JuliaFEM.jl/benchmarks/results/struct_size_scaling.json
-✓ CSV saved to: /home/juajukka/dev/JuliaFEM.jl/benchmarks/results/struct_size_scaling.csv
-
-================================================================================
-GENERATING PLOTS
-================================================================================
-
-]1337;ReportCellSizeP+q544e\GKS: cannot open display - headless operation mode active
-✓ Plot saved: /home/juajukka/dev/JuliaFEM.jl/benchmarks/results/field_access_scaling.png
-✓ Plot saved: /home/juajukka/dev/JuliaFEM.jl/benchmarks/results/field_update_scaling.png
-✓ Plot saved: /home/juajukka/dev/JuliaFEM.jl/benchmarks/results/iteration_scaling.png
-✓ Plot saved: /home/juajukka/dev/JuliaFEM.jl/benchmarks/results/copy_cost_linear.png
-✓ Plot saved: /home/juajukka/dev/JuliaFEM.jl/benchmarks/results/speedup_ratios.png
-
-All plots saved to: /home/juajukka/dev/JuliaFEM.jl/benchmarks/results
-
-================================================================================
-SAVING DATA
-================================================================================
-
-✓ Data saved to: benchmarks/results/struct_scaling_20251109_201256.json
-
-✓ CSV saved to: benchmarks/results/struct_scaling_20251109_201256.csv
-
-================================================================================
-GENERATING PLOTS
-================================================================================
-
-┌ Warning: Assignment to `p1` in soft scope is ambiguous because a global variable by the same name exists: `p1` will be treated as a new local. Disambiguate by using `local p1` to suppress this warning or `global p1` to assign to the existing global variable.
-└ @ ~/dev/JuliaFEM.jl/benchmarks/struct_size_scaling.jl:633
-┌ Warning: Assignment to `p2` in soft scope is ambiguous because a global variable by the same name exists: `p2` will be treated as a new local. Disambiguate by using `local p2` to suppress this warning or `global p2` to assign to the existing global variable.
-└ @ ~/dev/JuliaFEM.jl/benchmarks/struct_size_scaling.jl:649
-┌ Warning: Assignment to `p3` in soft scope is ambiguous because a global variable by the same name exists: `p3` will be treated as a new local. Disambiguate by using `local p3` to suppress this warning or `global p3` to assign to the existing global variable.
-└ @ ~/dev/JuliaFEM.jl/benchmarks/struct_size_scaling.jl:664
-┌ Warning: Assignment to `p4` in soft scope is ambiguous because a global variable by the same name exists: `p4` will be treated as a new local. Disambiguate by using `local p4` to suppress this warning or `global p4` to assign to the existing global variable.
-└ @ ~/dev/JuliaFEM.jl/benchmarks/struct_size_scaling.jl:679
-┌ Warning: Assignment to `p5` in soft scope is ambiguous because a global variable by the same name exists: `p5` will be treated as a new local. Disambiguate by using `local p5` to suppress this warning or `global p5` to assign to the existing global variable.
-└ @ ~/dev/JuliaFEM.jl/benchmarks/struct_size_scaling.jl:697
-✓ Saved: field_access_20251109_201256.png
-✓ Saved: field_update_20251109_201256.png
-✓ Saved: iteration_20251109_201256.png
-✓ Saved: speedup_factors_20251109_201256.png
-✓ Saved: copy_cost_20251109_201256.png
-✓ Saved: combined_20251109_201256.png
-
-All plots saved successfully!
-
-================================================================================
diff --git a/benchmarks/results/combined_20251109_201256.png b/benchmarks/results/combined_20251109_201256.png
deleted file mode 100644
index ca0f87a..0000000
Binary files a/benchmarks/results/combined_20251109_201256.png and /dev/null differ
diff --git a/benchmarks/results/copy_cost_20251109_201256.png b/benchmarks/results/copy_cost_20251109_201256.png
deleted file mode 100644
index 004a6c9..0000000
Binary files a/benchmarks/results/copy_cost_20251109_201256.png and /dev/null differ
diff --git a/benchmarks/results/copy_cost_linear.png b/benchmarks/results/copy_cost_linear.png
deleted file mode 100644
index d736f6a..0000000
Binary files a/benchmarks/results/copy_cost_linear.png and /dev/null differ
diff --git a/benchmarks/results/field_access_20251109_201256.png b/benchmarks/results/field_access_20251109_201256.png
deleted file mode 100644
index 5bfd2fe..0000000
Binary files a/benchmarks/results/field_access_20251109_201256.png and /dev/null differ
diff --git a/benchmarks/results/field_access_scaling.png b/benchmarks/results/field_access_scaling.png
deleted file mode 100644
index 5c053a5..0000000
Binary files a/benchmarks/results/field_access_scaling.png and /dev/null differ
diff --git a/benchmarks/results/field_update_20251109_201256.png b/benchmarks/results/field_update_20251109_201256.png
deleted file mode 100644
index b5ba29d..0000000
Binary files a/benchmarks/results/field_update_20251109_201256.png and /dev/null differ
diff --git a/benchmarks/results/field_update_scaling.png b/benchmarks/results/field_update_scaling.png
deleted file mode 100644
index 677e9c4..0000000
Binary files a/benchmarks/results/field_update_scaling.png and /dev/null differ
diff --git a/benchmarks/results/iteration_20251109_201256.png b/benchmarks/results/iteration_20251109_201256.png
deleted file mode 100644
index 3809daa..0000000
Binary files a/benchmarks/results/iteration_20251109_201256.png and /dev/null differ
diff --git a/benchmarks/results/iteration_scaling.png b/benchmarks/results/iteration_scaling.png
deleted file mode 100644
index e0997e9..0000000
Binary files a/benchmarks/results/iteration_scaling.png and /dev/null differ
diff --git a/benchmarks/results/speedup_factors_20251109_201256.png b/benchmarks/results/speedup_factors_20251109_201256.png
deleted file mode 100644
index 1a943c1..0000000
Binary files a/benchmarks/results/speedup_factors_20251109_201256.png and /dev/null differ
diff --git a/benchmarks/results/speedup_ratios.png b/benchmarks/results/speedup_ratios.png
deleted file mode 100644
index abe88a7..0000000
Binary files a/benchmarks/results/speedup_ratios.png and /dev/null differ
diff --git a/benchmarks/results/struct_scaling_20251109_201256.csv b/benchmarks/results/struct_scaling_20251109_201256.csv
deleted file mode 100644
index 8c5fdfd..0000000
--- a/benchmarks/results/struct_scaling_20251109_201256.csv
+++ /dev/null
@@ -1,13 +0,0 @@
-nfields,bytes,mut_access_ns,imm_access_ns,speedup_access,mut_update_ns,imm_update_ns,speedup_update,mut_iter_ns,imm_iter_ns,speedup_iter,imm_copy_ns
-1,8,4.615,2.023,2.2812654473554126,7.1991991991991995,2.315,3.1098052696324836,11.773773773773774,2.024,5.817081904038426,2.023
-2,16,4.904,2.023,2.4241225902125554,7.207207207207207,2.314,3.11460985618289,14.48997995991984,2.025,7.1555456592196744,2.029
-5,40,4.897,2.373,2.063632532659081,7.201201201201201,2.604,2.765438249309217,16.70941883767535,2.312,7.227257282731553,2.383
-10,80,4.621,2.029,2.2774765894529327,7.1991991991991995,2.602,2.7667944654877785,23.13152610441767,2.602,8.889902422912249,3.311
-20,160,4.618,2.023,2.282748393475037,7.201201201201201,3.1633266533066133,2.2764646179280326,63.131632653061224,5.523,11.43067764857165,3.177
-50,400,4.617,2.029,2.275505174963036,7.197197197197197,6.022,1.1951506471599465,196.84902597402598,24.783132530120483,7.942862982909166,6.895895895895896
-100,800,4.618,2.025,2.280493827160494,7.198198198198198,11.745745745745745,0.612834498039884,260.55786350148367,68.50307377049181,3.803593753682347,9.476476476476476
-200,1600,4.621,2.373,1.9473240623683101,7.41041041041041,24.633534136546185,0.30082611651798524,777.9142857142857,155.1606475716065,5.013605562294905,20.948897795591183
-500,4000,4.624,2.027,2.281203749383325,7.198198198198198,80.83854166666667,0.08904413723690831,1156.3,499.35567010309273,2.3155840000000003,58.68463886063072
-1000,8000,4.671,2.084,2.241362763915547,7.2002002002002,193.26257861635222,0.03725604952469045,3465.75,1100.2,3.1501090710779853,45.70171890798787
-2000,16000,4.625,2.024,2.2850790513833994,7.197197197197197,410.8542713567839,0.0175176399491468,4811.714285714285,2226.222222222222,2.1613809428742545,82.94813278008299
-5000,40000,4.623,2.083,2.2193951032165145,7.2042042042042045,1936.1,0.0037209876577677828,33969.0,5667.166666666667,5.994000529365056,867.0408163265306
diff --git a/benchmarks/results/struct_scaling_20251109_201256.json b/benchmarks/results/struct_scaling_20251109_201256.json
deleted file mode 100644
index 3cdf65f..0000000
--- a/benchmarks/results/struct_scaling_20251109_201256.json
+++ /dev/null
@@ -1,187 +0,0 @@
-{
- "julia_version": "1.12.1",
- "analysis": {
- "copy_intercept_ns": -25.467962218157183,
- "copy_slope_ns_per_field": 0.1586673181436861,
- "r_squared": 0.9005527188987935
- },
- "results": [
- {
- "mutable_update_ns": 7.1991991991991995,
- "immutable_copy_ns": 2.023,
- "mutable_access_ns": 4.615,
- "bytes": 8,
- "speedup_update": 3.1098052696324836,
- "speedup_iter": 5.817081904038426,
- "immutable_access_ns": 2.023,
- "speedup_access": 2.2812654473554126,
- "nfields": 1,
- "immutable_update_ns": 2.315,
- "mutable_iter_ns": 11.773773773773774,
- "immutable_iter_ns": 2.024
- },
- {
- "mutable_update_ns": 7.207207207207207,
- "immutable_copy_ns": 2.029,
- "mutable_access_ns": 4.904,
- "bytes": 16,
- "speedup_update": 3.11460985618289,
- "speedup_iter": 7.1555456592196744,
- "immutable_access_ns": 2.023,
- "speedup_access": 2.4241225902125554,
- "nfields": 2,
- "immutable_update_ns": 2.314,
- "mutable_iter_ns": 14.48997995991984,
- "immutable_iter_ns": 2.025
- },
- {
- "mutable_update_ns": 7.201201201201201,
- "immutable_copy_ns": 2.383,
- "mutable_access_ns": 4.897,
- "bytes": 40,
- "speedup_update": 2.765438249309217,
- "speedup_iter": 7.227257282731553,
- "immutable_access_ns": 2.373,
- "speedup_access": 2.063632532659081,
- "nfields": 5,
- "immutable_update_ns": 2.604,
- "mutable_iter_ns": 16.70941883767535,
- "immutable_iter_ns": 2.312
- },
- {
- "mutable_update_ns": 7.1991991991991995,
- "immutable_copy_ns": 3.311,
- "mutable_access_ns": 4.621,
- "bytes": 80,
- "speedup_update": 2.7667944654877785,
- "speedup_iter": 8.889902422912249,
- "immutable_access_ns": 2.029,
- "speedup_access": 2.2774765894529327,
- "nfields": 10,
- "immutable_update_ns": 2.602,
- "mutable_iter_ns": 23.13152610441767,
- "immutable_iter_ns": 2.602
- },
- {
- "mutable_update_ns": 7.201201201201201,
- "immutable_copy_ns": 3.177,
- "mutable_access_ns": 4.618,
- "bytes": 160,
- "speedup_update": 2.2764646179280326,
- "speedup_iter": 11.43067764857165,
- "immutable_access_ns": 2.023,
- "speedup_access": 2.282748393475037,
- "nfields": 20,
- "immutable_update_ns": 3.1633266533066133,
- "mutable_iter_ns": 63.131632653061224,
- "immutable_iter_ns": 5.523
- },
- {
- "mutable_update_ns": 7.197197197197197,
- "immutable_copy_ns": 6.895895895895896,
- "mutable_access_ns": 4.617,
- "bytes": 400,
- "speedup_update": 1.1951506471599465,
- "speedup_iter": 7.942862982909166,
- "immutable_access_ns": 2.029,
- "speedup_access": 2.275505174963036,
- "nfields": 50,
- "immutable_update_ns": 6.022,
- "mutable_iter_ns": 196.84902597402598,
- "immutable_iter_ns": 24.783132530120483
- },
- {
- "mutable_update_ns": 7.198198198198198,
- "immutable_copy_ns": 9.476476476476476,
- "mutable_access_ns": 4.618,
- "bytes": 800,
- "speedup_update": 0.612834498039884,
- "speedup_iter": 3.803593753682347,
- "immutable_access_ns": 2.025,
- "speedup_access": 2.280493827160494,
- "nfields": 100,
- "immutable_update_ns": 11.745745745745745,
- "mutable_iter_ns": 260.55786350148367,
- "immutable_iter_ns": 68.50307377049181
- },
- {
- "mutable_update_ns": 7.41041041041041,
- "immutable_copy_ns": 20.948897795591183,
- "mutable_access_ns": 4.621,
- "bytes": 1600,
- "speedup_update": 0.30082611651798524,
- "speedup_iter": 5.013605562294905,
- "immutable_access_ns": 2.373,
- "speedup_access": 1.9473240623683101,
- "nfields": 200,
- "immutable_update_ns": 24.633534136546185,
- "mutable_iter_ns": 777.9142857142857,
- "immutable_iter_ns": 155.1606475716065
- },
- {
- "mutable_update_ns": 7.198198198198198,
- "immutable_copy_ns": 58.68463886063072,
- "mutable_access_ns": 4.624,
- "bytes": 4000,
- "speedup_update": 0.08904413723690831,
- "speedup_iter": 2.3155840000000003,
- "immutable_access_ns": 2.027,
- "speedup_access": 2.281203749383325,
- "nfields": 500,
- "immutable_update_ns": 80.83854166666667,
- "mutable_iter_ns": 1156.3,
- "immutable_iter_ns": 499.35567010309273
- },
- {
- "mutable_update_ns": 7.2002002002002,
- "immutable_copy_ns": 45.70171890798787,
- "mutable_access_ns": 4.671,
- "bytes": 8000,
- "speedup_update": 0.03725604952469045,
- "speedup_iter": 3.1501090710779853,
- "immutable_access_ns": 2.084,
- "speedup_access": 2.241362763915547,
- "nfields": 1000,
- "immutable_update_ns": 193.26257861635222,
- "mutable_iter_ns": 3465.75,
- "immutable_iter_ns": 1100.2
- },
- {
- "mutable_update_ns": 7.197197197197197,
- "immutable_copy_ns": 82.94813278008299,
- "mutable_access_ns": 4.625,
- "bytes": 16000,
- "speedup_update": 0.0175176399491468,
- "speedup_iter": 2.1613809428742545,
- "immutable_access_ns": 2.024,
- "speedup_access": 2.2850790513833994,
- "nfields": 2000,
- "immutable_update_ns": 410.8542713567839,
- "mutable_iter_ns": 4811.714285714285,
- "immutable_iter_ns": 2226.222222222222
- },
- {
- "mutable_update_ns": 7.2042042042042045,
- "immutable_copy_ns": 867.0408163265306,
- "mutable_access_ns": 4.623,
- "bytes": 40000,
- "speedup_update": 0.0037209876577677828,
- "speedup_iter": 5.994000529365056,
- "immutable_access_ns": 2.083,
- "speedup_access": 2.2193951032165145,
- "nfields": 5000,
- "immutable_update_ns": 1936.1,
- "mutable_iter_ns": 33969.0,
- "immutable_iter_ns": 5667.166666666667
- }
- ],
- "timestamp": "20251109_201256",
- "system": {
- "cpu_model": "Intel(R) Xeon(R) Gold 6326 CPU @ 2.90GHz",
- "cpu_speed_mhz": 3300,
- "machine": "x86_64-linux-gnu",
- "word_size": 64,
- "cpu_cores": 32,
- "os": "Linux"
- }
-}
\ No newline at end of file
diff --git a/benchmarks/results/struct_size_scaling.csv b/benchmarks/results/struct_size_scaling.csv
deleted file mode 100644
index fc8e7c4..0000000
--- a/benchmarks/results/struct_size_scaling.csv
+++ /dev/null
@@ -1,13 +0,0 @@
-nfields,bytes,mut_access_ns,imm_access_ns,speedup_access,mut_update_ns,imm_update_ns,speedup_update,imm_copy_ns,mut_iter_ns,imm_iter_ns,speedup_iter
-1,8,4.615,2.023,2.2812654473554126,7.1991991991991995,2.315,3.1098052696324836,2.023,11.773773773773774,2.024,5.817081904038426
-2,16,4.904,2.023,2.4241225902125554,7.207207207207207,2.314,3.11460985618289,2.029,14.48997995991984,2.025,7.1555456592196744
-5,40,4.897,2.373,2.063632532659081,7.201201201201201,2.604,2.765438249309217,2.383,16.70941883767535,2.312,7.227257282731553
-10,80,4.621,2.029,2.2774765894529327,7.1991991991991995,2.602,2.7667944654877785,3.311,23.13152610441767,2.602,8.889902422912249
-20,160,4.618,2.023,2.282748393475037,7.201201201201201,3.1633266533066133,2.2764646179280326,3.177,63.131632653061224,5.523,11.43067764857165
-50,400,4.617,2.029,2.275505174963036,7.197197197197197,6.022,1.1951506471599465,6.895895895895896,196.84902597402598,24.783132530120483,7.942862982909166
-100,800,4.618,2.025,2.280493827160494,7.198198198198198,11.745745745745745,0.612834498039884,9.476476476476476,260.55786350148367,68.50307377049181,3.803593753682347
-200,1600,4.621,2.373,1.9473240623683101,7.41041041041041,24.633534136546185,0.30082611651798524,20.948897795591183,777.9142857142857,155.1606475716065,5.013605562294905
-500,4000,4.624,2.027,2.281203749383325,7.198198198198198,80.83854166666667,0.08904413723690831,58.68463886063072,1156.3,499.35567010309273,2.3155840000000003
-1000,8000,4.671,2.084,2.241362763915547,7.2002002002002,193.26257861635222,0.03725604952469045,45.70171890798787,3465.75,1100.2,3.1501090710779853
-2000,16000,4.625,2.024,2.2850790513833994,7.197197197197197,410.8542713567839,0.0175176399491468,82.94813278008299,4811.714285714285,2226.222222222222,2.1613809428742545
-5000,40000,4.623,2.083,2.2193951032165145,7.2042042042042045,1936.1,0.0037209876577677828,867.0408163265306,33969.0,5667.166666666667,5.994000529365056
diff --git a/benchmarks/results/struct_size_scaling.json b/benchmarks/results/struct_size_scaling.json
deleted file mode 100644
index a391a4b..0000000
--- a/benchmarks/results/struct_size_scaling.json
+++ /dev/null
@@ -1,196 +0,0 @@
-{
- "struct_sizes": [
- 1,
- 2,
- 5,
- 10,
- 20,
- 50,
- 100,
- 200,
- 500,
- 1000,
- 2000,
- 5000
- ],
- "system_info": {
- "cpu_model": "Intel(R) Xeon(R) Gold 6326 CPU @ 2.90GHz",
- "julia_version": "1.12.1",
- "total_memory_gb": 503.35,
- "l1_cache": "48K",
- "l2_cache": "1280K",
- "l3_cache": "24576K",
- "cpu_cores": 32
- },
- "results": [
- {
- "mutable_update_ns": 7.1991991991991995,
- "immutable_copy_ns": 2.023,
- "mutable_access_ns": 4.615,
- "bytes": 8,
- "speedup_update": 3.1098052696324836,
- "speedup_iter": 5.817081904038426,
- "immutable_access_ns": 2.023,
- "speedup_access": 2.2812654473554126,
- "nfields": 1,
- "immutable_update_ns": 2.315,
- "mutable_iter_ns": 11.773773773773774,
- "immutable_iter_ns": 2.024
- },
- {
- "mutable_update_ns": 7.207207207207207,
- "immutable_copy_ns": 2.029,
- "mutable_access_ns": 4.904,
- "bytes": 16,
- "speedup_update": 3.11460985618289,
- "speedup_iter": 7.1555456592196744,
- "immutable_access_ns": 2.023,
- "speedup_access": 2.4241225902125554,
- "nfields": 2,
- "immutable_update_ns": 2.314,
- "mutable_iter_ns": 14.48997995991984,
- "immutable_iter_ns": 2.025
- },
- {
- "mutable_update_ns": 7.201201201201201,
- "immutable_copy_ns": 2.383,
- "mutable_access_ns": 4.897,
- "bytes": 40,
- "speedup_update": 2.765438249309217,
- "speedup_iter": 7.227257282731553,
- "immutable_access_ns": 2.373,
- "speedup_access": 2.063632532659081,
- "nfields": 5,
- "immutable_update_ns": 2.604,
- "mutable_iter_ns": 16.70941883767535,
- "immutable_iter_ns": 2.312
- },
- {
- "mutable_update_ns": 7.1991991991991995,
- "immutable_copy_ns": 3.311,
- "mutable_access_ns": 4.621,
- "bytes": 80,
- "speedup_update": 2.7667944654877785,
- "speedup_iter": 8.889902422912249,
- "immutable_access_ns": 2.029,
- "speedup_access": 2.2774765894529327,
- "nfields": 10,
- "immutable_update_ns": 2.602,
- "mutable_iter_ns": 23.13152610441767,
- "immutable_iter_ns": 2.602
- },
- {
- "mutable_update_ns": 7.201201201201201,
- "immutable_copy_ns": 3.177,
- "mutable_access_ns": 4.618,
- "bytes": 160,
- "speedup_update": 2.2764646179280326,
- "speedup_iter": 11.43067764857165,
- "immutable_access_ns": 2.023,
- "speedup_access": 2.282748393475037,
- "nfields": 20,
- "immutable_update_ns": 3.1633266533066133,
- "mutable_iter_ns": 63.131632653061224,
- "immutable_iter_ns": 5.523
- },
- {
- "mutable_update_ns": 7.197197197197197,
- "immutable_copy_ns": 6.895895895895896,
- "mutable_access_ns": 4.617,
- "bytes": 400,
- "speedup_update": 1.1951506471599465,
- "speedup_iter": 7.942862982909166,
- "immutable_access_ns": 2.029,
- "speedup_access": 2.275505174963036,
- "nfields": 50,
- "immutable_update_ns": 6.022,
- "mutable_iter_ns": 196.84902597402598,
- "immutable_iter_ns": 24.783132530120483
- },
- {
- "mutable_update_ns": 7.198198198198198,
- "immutable_copy_ns": 9.476476476476476,
- "mutable_access_ns": 4.618,
- "bytes": 800,
- "speedup_update": 0.612834498039884,
- "speedup_iter": 3.803593753682347,
- "immutable_access_ns": 2.025,
- "speedup_access": 2.280493827160494,
- "nfields": 100,
- "immutable_update_ns": 11.745745745745745,
- "mutable_iter_ns": 260.55786350148367,
- "immutable_iter_ns": 68.50307377049181
- },
- {
- "mutable_update_ns": 7.41041041041041,
- "immutable_copy_ns": 20.948897795591183,
- "mutable_access_ns": 4.621,
- "bytes": 1600,
- "speedup_update": 0.30082611651798524,
- "speedup_iter": 5.013605562294905,
- "immutable_access_ns": 2.373,
- "speedup_access": 1.9473240623683101,
- "nfields": 200,
- "immutable_update_ns": 24.633534136546185,
- "mutable_iter_ns": 777.9142857142857,
- "immutable_iter_ns": 155.1606475716065
- },
- {
- "mutable_update_ns": 7.198198198198198,
- "immutable_copy_ns": 58.68463886063072,
- "mutable_access_ns": 4.624,
- "bytes": 4000,
- "speedup_update": 0.08904413723690831,
- "speedup_iter": 2.3155840000000003,
- "immutable_access_ns": 2.027,
- "speedup_access": 2.281203749383325,
- "nfields": 500,
- "immutable_update_ns": 80.83854166666667,
- "mutable_iter_ns": 1156.3,
- "immutable_iter_ns": 499.35567010309273
- },
- {
- "mutable_update_ns": 7.2002002002002,
- "immutable_copy_ns": 45.70171890798787,
- "mutable_access_ns": 4.671,
- "bytes": 8000,
- "speedup_update": 0.03725604952469045,
- "speedup_iter": 3.1501090710779853,
- "immutable_access_ns": 2.084,
- "speedup_access": 2.241362763915547,
- "nfields": 1000,
- "immutable_update_ns": 193.26257861635222,
- "mutable_iter_ns": 3465.75,
- "immutable_iter_ns": 1100.2
- },
- {
- "mutable_update_ns": 7.197197197197197,
- "immutable_copy_ns": 82.94813278008299,
- "mutable_access_ns": 4.625,
- "bytes": 16000,
- "speedup_update": 0.0175176399491468,
- "speedup_iter": 2.1613809428742545,
- "immutable_access_ns": 2.024,
- "speedup_access": 2.2850790513833994,
- "nfields": 2000,
- "immutable_update_ns": 410.8542713567839,
- "mutable_iter_ns": 4811.714285714285,
- "immutable_iter_ns": 2226.222222222222
- },
- {
- "mutable_update_ns": 7.2042042042042045,
- "immutable_copy_ns": 867.0408163265306,
- "mutable_access_ns": 4.623,
- "bytes": 40000,
- "speedup_update": 0.0037209876577677828,
- "speedup_iter": 5.994000529365056,
- "immutable_access_ns": 2.083,
- "speedup_access": 2.2193951032165145,
- "nfields": 5000,
- "immutable_update_ns": 1936.1,
- "mutable_iter_ns": 33969.0,
- "immutable_iter_ns": 5667.166666666667
- }
- ],
- "timestamp": "2025-11-09T20:12:52.503"
-}
\ No newline at end of file
diff --git a/benchmarks/struct_size_scaling.jl b/benchmarks/struct_size_scaling.jl
deleted file mode 100644
index a84412d..0000000
--- a/benchmarks/struct_size_scaling.jl
+++ /dev/null
@@ -1,731 +0,0 @@
-# Benchmark: Does immutable performance degrade with struct size?
-# Theory: Stack copying is O(n), heap pointers are O(1)
-# Question: At what size does mutable win?
-
-# NOTE: Using packages from global environment (not project)
-using BenchmarkTools
-using Printf
-using JSON
-using Plots
-using JSON
-using Dates
-
-# Get system information
-println("="^80)
-println("SYSTEM INFORMATION")
-println("="^80)
-println()
-
-# CPU info
-cpu_info = Sys.cpu_info()
-println("CPU Model: ", cpu_info[1].model)
-println("CPU Cores: ", Sys.CPU_THREADS, " threads (", length(cpu_info), " physical cores)")
-println("CPU Speed: ", cpu_info[1].speed, " MHz")
-println()
-
-# Julia and system info
-println("Julia Version: ", VERSION)
-println("OS: ", Sys.KERNEL, " ", Sys.MACHINE)
-println("Word Size: ", Sys.WORD_SIZE, " bits")
-println()
-
-# Memory and cache info (approximate)
-println("Approximate CPU Cache Sizes:")
-println(" L1 Cache: ~32-64 KB per core (typical)")
-println(" L2 Cache: ~256-512 KB per core (typical)")
-println(" L3 Cache: ~8-32 MB shared (typical)")
-println()
-println("Note: Testing up to 8KB structs to exceed L1 cache")
-println()
-
-using BenchmarkTools
-using Printf
-using JSON
-using Plots
-
-# Collect system information
-function get_system_info()
- info = Dict{String,Any}()
- info["julia_version"] = string(VERSION)
- info["cpu_model"] = Sys.cpu_info()[1].model
- info["cpu_cores"] = Sys.CPU_THREADS
- info["total_memory_gb"] = round(Sys.total_memory() / 1024^3, digits=2)
-
- # Try to get CPU cache info (Linux)
- try
- if Sys.islinux()
- l1_cache = read("/sys/devices/system/cpu/cpu0/cache/index0/size", String) |> strip
- l2_cache = read("/sys/devices/system/cpu/cpu0/cache/index2/size", String) |> strip
- l3_cache = read("/sys/devices/system/cpu/cpu0/cache/index3/size", String) |> strip
- info["l1_cache"] = l1_cache
- info["l2_cache"] = l2_cache
- info["l3_cache"] = l3_cache
- end
- catch
- info["cache_info"] = "Not available"
- end
-
- return info
-end
-
-system_info = get_system_info()
-
-println("="^80)
-println("SYSTEM INFORMATION")
-println("="^80)
-println("Julia Version: $(system_info["julia_version"])")
-println("CPU Model: $(system_info["cpu_model"])")
-println("CPU Cores: $(system_info["cpu_cores"])")
-println("Total Memory: $(system_info["total_memory_gb"]) GB")
-if haskey(system_info, "l1_cache")
- println("L1 Cache: $(system_info["l1_cache"])")
- println("L2 Cache: $(system_info["l2_cache"])")
- println("L3 Cache: $(system_info["l3_cache"])")
-end
-println()
-
-println("="^80)
-println("STRUCT SIZE SCALING BENCHMARK")
-println("="^80)
-println()
-println("Testing hypothesis: Immutable slows down with struct size, mutable stays constant")
-println()
-
-# Test different struct sizes (number of Float64 fields)
-# Extended range to go well beyond register file and L1 cache
-STRUCT_SIZES = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000]
-
-results = []
-
-for nfields in STRUCT_SIZES
- println("Testing struct with $nfields Float64 fields ($(nfields * 8) bytes)...")
-
- # Generate mutable version
- field_names_mut = [Symbol("field$i") for i in 1:nfields]
-
- # Mutable: Dict-based
- mutable_data = Dict{Symbol,Float64}()
- for fname in field_names_mut
- mutable_data[fname] = rand()
- end
-
- # Immutable: NamedTuple-based
- immutable_data = NamedTuple{Tuple(field_names_mut)}(Tuple(rand() for _ in 1:nfields))
-
- # Benchmark 1: Field Access (read first field)
- first_field = field_names_mut[1]
-
- time_mut_access = @belapsed $mutable_data[$first_field]
- time_imm_access = @belapsed $immutable_data.$first_field
-
- # Benchmark 2: Field Update (change first field)
- time_mut_update = @belapsed begin
- $mutable_data[$first_field] = 42.0
- end
-
- time_imm_update = @belapsed begin
- $immutable_data = (; $immutable_data..., $first_field=42.0)
- end
-
- # Benchmark 3: Struct Copy (merge with empty to force copy)
- time_imm_copy = @belapsed merge($immutable_data, NamedTuple())
-
- # Benchmark 4: Iteration over all fields
- time_mut_iter = @belapsed begin
- sum = 0.0
- for (k, v) in $mutable_data
- sum += v
- end
- sum
- end
-
- time_imm_iter = @belapsed begin
- sum = 0.0
- for v in $immutable_data
- sum += v
- end
- sum
- end
-
- speedup_access = time_mut_access / time_imm_access
- speedup_update = time_mut_update / time_imm_update
- speedup_iter = time_mut_iter / time_imm_iter
-
- push!(results, (
- nfields=nfields,
- bytes=nfields * 8,
- # Access times
- mut_access=time_mut_access,
- imm_access=time_imm_access,
- speedup_access=speedup_access,
- # Update times
- mut_update=time_mut_update,
- imm_update=time_imm_update,
- speedup_update=speedup_update,
- # Copy time
- imm_copy=time_imm_copy,
- # Iteration times
- mut_iter=time_mut_iter,
- imm_iter=time_imm_iter,
- speedup_iter=speedup_iter
- ))
-
- println(" Access: Mut=$(round(time_mut_access*1e9, digits=2))ns Imm=$(round(time_imm_access*1e9, digits=2))ns Speedup=$(round(speedup_access, digits=1))x")
- println(" Update: Mut=$(round(time_mut_update*1e9, digits=2))ns Imm=$(round(time_imm_update*1e9, digits=2))ns Speedup=$(round(speedup_update, digits=1))x")
- println(" Iterate: Mut=$(round(time_mut_iter*1e9, digits=2))ns Imm=$(round(time_imm_iter*1e9, digits=2))ns Speedup=$(round(speedup_iter, digits=1))x")
- println(" Copy: $(round(time_imm_copy*1e9, digits=2))ns")
- println()
-end
-
-println("="^80)
-println("RESULTS SUMMARY")
-println("="^80)
-println()
-
-println("Field Access Performance:")
-println("Size (fields) | Bytes | Mutable (ns) | Immutable (ns) | Speedup")
-println("-"^70)
-for r in results
- @printf("%13d | %5d | %12.2f | %14.2f | %6.1fx\n",
- r.nfields, r.bytes, r.mut_access * 1e9, r.imm_access * 1e9, r.speedup_access)
-end
-println()
-
-println("Field Update Performance:")
-println("Size (fields) | Bytes | Mutable (ns) | Immutable (ns) | Speedup")
-println("-"^70)
-for r in results
- @printf("%13d | %5d | %12.2f | %14.2f | %6.1fx\n",
- r.nfields, r.bytes, r.mut_update * 1e9, r.imm_update * 1e9, r.speedup_update)
-end
-println()
-
-println("Iteration Performance:")
-println("Size (fields) | Bytes | Mutable (ns) | Immutable (ns) | Speedup")
-println("-"^70)
-for r in results
- @printf("%13d | %5d | %12.2f | %14.2f | %6.1fx\n",
- r.nfields, r.bytes, r.mut_iter * 1e9, r.imm_iter * 1e9, r.speedup_iter)
-end
-println()
-
-println("Immutable Copy Cost (ns):")
-println("Size (fields) | Bytes | Copy Time (ns)")
-println("-"^40)
-for r in results
- @printf("%13d | %5d | %13.2f\n", r.nfields, r.bytes, r.imm_copy * 1e9)
-end
-println()
-
-# Analysis
-println("="^80)
-println("ANALYSIS")
-println("="^80)
-println()
-
-# Check if mutable ever wins
-access_wins = [r for r in results if r.speedup_access < 1.0]
-update_wins = [r for r in results if r.speedup_update < 1.0]
-iter_wins = [r for r in results if r.speedup_iter < 1.0]
-
-if isempty(access_wins)
- println("✓ Immutable ALWAYS faster for field access (even at 1000 fields = 8KB)")
- min_speedup = minimum(r.speedup_access for r in results)
- println(" Minimum speedup: $(round(min_speedup, digits=1))x at $(results[end].nfields) fields")
-else
- println("⚠ Mutable wins for field access at $(access_wins[1].nfields) fields")
-end
-println()
-
-if isempty(update_wins)
- println("✓ Immutable ALWAYS faster for field update (even at 1000 fields = 8KB)")
- min_speedup = minimum(r.speedup_update for r in results)
- println(" Minimum speedup: $(round(min_speedup, digits=1))x at $(results[end].nfields) fields")
-else
- println("⚠ Mutable wins for field update at $(update_wins[1].nfields) fields")
-end
-println()
-
-if isempty(iter_wins)
- println("✓ Immutable ALWAYS faster for iteration (even at 1000 fields = 8KB)")
- min_speedup = minimum(r.speedup_iter for r in results)
- println(" Minimum speedup: $(round(min_speedup, digits=1))x at $(results[end].nfields) fields")
-else
- println("⚠ Mutable wins for iteration at $(iter_wins[1].nfields) fields")
-end
-println()
-
-# Check scaling behavior
-println("Scaling Analysis:")
-println()
-
-# Linear regression on copy time vs size
-sizes = [r.nfields for r in results]
-copy_times = [r.imm_copy * 1e9 for r in results] # Convert to ns
-
-# Simple linear fit: time = a + b*size
-n = length(sizes)
-mean_size = sum(sizes) / n
-mean_time = sum(copy_times) / n
-cov = sum((sizes[i] - mean_size) * (copy_times[i] - mean_time) for i in 1:n) / n
-var_size = sum((s - mean_size)^2 for s in sizes) / n
-slope = cov / var_size
-intercept = mean_time - slope * mean_size
-
-println("Copy time scaling:")
-println(" Linear fit: time(ns) = $(round(intercept, digits=2)) + $(round(slope, digits=4)) * nfields")
-println(" Per-field cost: $(round(slope, digits=4)) ns/field")
-println(" Base overhead: $(round(intercept, digits=2)) ns")
-println()
-
-# Check if copy time grows linearly
-println("Is copy time linear? (checking R²)")
-ss_tot = sum((t - mean_time)^2 for t in copy_times)
-ss_res = sum((copy_times[i] - (intercept + slope * sizes[i]))^2 for i in 1:n)
-r_squared = 1 - ss_res / ss_tot
-println(" R² = $(round(r_squared, digits=4))")
-if r_squared > 0.95
- println(" ✓ Copy time is linear in struct size (as expected)")
-else
- println(" ⚠ Copy time not perfectly linear (compiler optimizations?)")
-end
-println()
-
-# Key insight
-println("KEY INSIGHT:")
-println("-"^80)
-println()
-println("Even at 1000 fields (8KB struct), immutable is STILL faster because:")
-println(" 1. Dict lookup cost (~40-50ns) >> copy cost per field (~$(round(slope, digits=4))ns)")
-println(" 2. Type stability enables compiler optimizations (inlining, SIMD)")
-println(" 3. Stack allocation has better cache locality than heap pointers")
-println()
-println("Theoretical crossover point (if it exists):")
-crossover_fields = (40.0 - intercept) / slope # When copy cost = Dict lookup
-println(" Would occur at ~$(round(Int, crossover_fields)) fields ($(round(Int, crossover_fields*8/1024))KB)")
-if crossover_fields > 1000
- println(" But this is beyond any realistic FEM element!")
-end
-println()
-
-println("="^80)
-println("CONCLUSION")
-println("="^80)
-println()
-println("Your intuition about O(n) scaling is CORRECT, BUT:")
-println()
-println(" • Dict lookup base cost is SO high (~40-50ns)")
-println(" • Copy cost per field is SO low (~$(round(slope, digits=4))ns)")
-println(" • Compiler optimizations are SO good (inlining, SIMD, escape analysis)")
-println()
-println("That immutable wins even for unrealistically large structs (8KB+)!")
-println()
-println("For typical FEM elements:")
-println(" • Material properties: 3-10 fields (24-80 bytes)")
-println(" • State variables: 10-50 fields (80-400 bytes)")
-println(" • Even with 100 fields (800 bytes), immutable is >10x faster")
-println()
-println("Type stability > Everything else.")
-println()
-
-# ============================================================================
-# SAVE DATA TO DISK
-# ============================================================================
-
-println("="^80)
-println("SAVING DATA")
-println("="^80)
-println()
-
-# Create results directory
-results_dir = joinpath(@__DIR__, "results")
-mkpath(results_dir)
-
-# Prepare data for JSON
-data_to_save = Dict(
- "system_info" => system_info,
- "timestamp" => string(now()),
- "struct_sizes" => STRUCT_SIZES,
- "results" => [
- Dict(
- "nfields" => r.nfields,
- "bytes" => r.bytes,
- "mutable_access_ns" => r.mut_access * 1e9,
- "immutable_access_ns" => r.imm_access * 1e9,
- "speedup_access" => r.speedup_access,
- "mutable_update_ns" => r.mut_update * 1e9,
- "immutable_update_ns" => r.imm_update * 1e9,
- "speedup_update" => r.speedup_update,
- "immutable_copy_ns" => r.imm_copy * 1e9,
- "mutable_iter_ns" => r.mut_iter * 1e9,
- "immutable_iter_ns" => r.imm_iter * 1e9,
- "speedup_iter" => r.speedup_iter
- )
- for r in results
- ]
-)
-
-# Save as JSON
-json_file = joinpath(results_dir, "struct_size_scaling.json")
-open(json_file, "w") do f
- JSON.print(f, data_to_save, 2)
-end
-println("✓ Data saved to: $json_file")
-
-# Save as CSV for easy plotting in other tools
-csv_file = joinpath(results_dir, "struct_size_scaling.csv")
-open(csv_file, "w") do f
- println(f, "nfields,bytes,mut_access_ns,imm_access_ns,speedup_access,mut_update_ns,imm_update_ns,speedup_update,imm_copy_ns,mut_iter_ns,imm_iter_ns,speedup_iter")
- for r in results
- println(f, "$(r.nfields),$(r.bytes),$(r.mut_access*1e9),$(r.imm_access*1e9),$(r.speedup_access),$(r.mut_update*1e9),$(r.imm_update*1e9),$(r.speedup_update),$(r.imm_copy*1e9),$(r.mut_iter*1e9),$(r.imm_iter*1e9),$(r.speedup_iter)")
- end
-end
-println("✓ CSV saved to: $csv_file")
-println()
-
-# ============================================================================
-# GENERATE PLOTS
-# ============================================================================
-
-println("="^80)
-println("GENERATING PLOTS")
-println("="^80)
-println()
-
-# Extract data for plotting
-bytes_vals = [r.bytes for r in results]
-mut_access = [r.mut_access * 1e9 for r in results]
-imm_access = [r.imm_access * 1e9 for r in results]
-mut_update = [r.mut_update * 1e9 for r in results]
-imm_update = [r.imm_update * 1e9 for r in results]
-mut_iter = [r.mut_iter * 1e9 for r in results]
-imm_iter = [r.imm_iter * 1e9 for r in results]
-imm_copy = [r.imm_copy * 1e9 for r in results]
-
-# Typical FEM element sizes
-fem_small = 40 # 5 fields (E, ν, ρ, etc.)
-fem_medium = 160 # 20 fields (material + state)
-fem_large = 400 # 50 fields (complex plasticity)
-
-# Plot 1: Field Access Performance
-p1 = plot(bytes_vals, mut_access,
- label="Mutable (Dict)",
- xlabel="Struct Size (bytes)",
- ylabel="Time (nanoseconds)",
- title="Field Access Performance vs Struct Size",
- linewidth=2,
- marker=:circle,
- legend=:topleft,
- size=(800, 600))
-plot!(p1, bytes_vals, imm_access,
- label="Immutable (NamedTuple)",
- linewidth=2,
- marker=:square)
-vline!(p1, [fem_small, fem_medium, fem_large],
- label="Typical FEM sizes",
- linestyle=:dash,
- linecolor=:gray,
- linewidth=1)
-annotate!(p1, fem_small, maximum(mut_access) * 0.9, text("Small\n(5 fields)", 8, :left))
-annotate!(p1, fem_medium, maximum(mut_access) * 0.8, text("Medium\n(20 fields)", 8, :left))
-annotate!(p1, fem_large, maximum(mut_access) * 0.7, text("Large\n(50 fields)", 8, :left))
-
-plot_file1 = joinpath(results_dir, "field_access_scaling.png")
-savefig(p1, plot_file1)
-println("✓ Plot saved: $plot_file1")
-
-# Plot 2: Field Update Performance (showing crossover)
-p2 = plot(bytes_vals, mut_update,
- label="Mutable (Dict)",
- xlabel="Struct Size (bytes)",
- ylabel="Time (nanoseconds)",
- title="Field Update Performance vs Struct Size (Crossover at ~800 bytes)",
- linewidth=2,
- marker=:circle,
- legend=:topleft,
- size=(800, 600))
-plot!(p2, bytes_vals, imm_update,
- label="Immutable (NamedTuple)",
- linewidth=2,
- marker=:square)
-vline!(p2, [fem_small, fem_medium, fem_large, 800],
- label=["", "", "", "Crossover (~100 fields)"],
- linestyle=[:dash, :dash, :dash, :dot],
- linecolor=[:gray, :gray, :gray, :red],
- linewidth=[1, 1, 1, 2])
-annotate!(p2, fem_small, maximum(imm_update) * 0.2, text("Small", 8, :left))
-annotate!(p2, fem_medium, maximum(imm_update) * 0.3, text("Medium", 8, :left))
-annotate!(p2, fem_large, maximum(imm_update) * 0.4, text("Large", 8, :left))
-
-plot_file2 = joinpath(results_dir, "field_update_scaling.png")
-savefig(p2, plot_file2)
-println("✓ Plot saved: $plot_file2")
-
-# Plot 3: Iteration Performance
-p3 = plot(bytes_vals, mut_iter,
- label="Mutable (Dict)",
- xlabel="Struct Size (bytes)",
- ylabel="Time (nanoseconds)",
- title="Iteration Performance vs Struct Size",
- linewidth=2,
- marker=:circle,
- legend=:topleft,
- size=(800, 600),
- yscale=:log10)
-plot!(p3, bytes_vals, imm_iter,
- label="Immutable (NamedTuple)",
- linewidth=2,
- marker=:square)
-vline!(p3, [fem_small, fem_medium, fem_large],
- label="Typical FEM sizes",
- linestyle=:dash,
- linecolor=:gray,
- linewidth=1)
-
-plot_file3 = joinpath(results_dir, "iteration_scaling.png")
-savefig(p3, plot_file3)
-println("✓ Plot saved: $plot_file3")
-
-# Plot 4: Copy Cost (linear scaling)
-p4 = plot(bytes_vals, imm_copy,
- label="Measured",
- xlabel="Struct Size (bytes)",
- ylabel="Copy Time (nanoseconds)",
- title="Immutable Struct Copy Cost (Linear Scaling)",
- linewidth=2,
- marker=:circle,
- legend=:topright,
- size=(800, 600))
-# Add linear fit line
-plot!(p4, bytes_vals, [intercept + slope * (b / 8) for b in bytes_vals],
- label="Linear fit: $(round(intercept, digits=1)) + $(round(slope, digits=3)) × nfields",
- linestyle=:dash,
- linewidth=2)
-vline!(p4, [fem_small, fem_medium, fem_large],
- label="Typical FEM sizes",
- linestyle=:dash,
- linecolor=:gray,
- linewidth=1)
-
-plot_file4 = joinpath(results_dir, "copy_cost_linear.png")
-savefig(p4, plot_file4)
-println("✓ Plot saved: $plot_file4")
-
-# Plot 5: Speedup ratios (showing where immutable wins)
-p5 = plot(bytes_vals, [r.speedup_access for r in results],
- label="Field Access",
- xlabel="Struct Size (bytes)",
- ylabel="Speedup (Immutable / Mutable)",
- title="Performance Speedup: Immutable vs Mutable",
- linewidth=2,
- marker=:circle,
- legend=:right,
- size=(800, 600))
-plot!(p5, bytes_vals, [r.speedup_update for r in results],
- label="Field Update",
- linewidth=2,
- marker=:square)
-plot!(p5, bytes_vals, [r.speedup_iter for r in results],
- label="Iteration",
- linewidth=2,
- marker=:diamond)
-hline!(p5, [1.0],
- label="Break-even",
- linestyle=:dot,
- linecolor=:black,
- linewidth=2)
-vline!(p5, [fem_small, fem_medium, fem_large],
- label="",
- linestyle=:dash,
- linecolor=:gray,
- linewidth=1)
-annotate!(p5, fem_large, 0.5, text("Typical FEM range →", 8, :left))
-
-plot_file5 = joinpath(results_dir, "speedup_ratios.png")
-savefig(p5, plot_file5)
-println("✓ Plot saved: $plot_file5")
-
-println()
-println("All plots saved to: $results_dir")
-println()
-
-# Save results to JSON
-println("="^80)
-println("SAVING DATA")
-println("="^80)
-println()
-
-timestamp = Dates.format(now(), "yyyymmdd_HHMMSS")
-output_dir = "benchmarks/results"
-mkpath(output_dir)
-
-# Prepare data for saving
-benchmark_data = Dict(
- "timestamp" => timestamp,
- "julia_version" => string(VERSION),
- "system" => Dict(
- "cpu_model" => cpu_info[1].model,
- "cpu_cores" => Sys.CPU_THREADS,
- "cpu_speed_mhz" => cpu_info[1].speed,
- "os" => string(Sys.KERNEL),
- "machine" => string(Sys.MACHINE),
- "word_size" => Sys.WORD_SIZE
- ),
- "results" => [
- Dict(
- "nfields" => r.nfields,
- "bytes" => r.bytes,
- "mutable_access_ns" => r.mut_access * 1e9,
- "immutable_access_ns" => r.imm_access * 1e9,
- "speedup_access" => r.speedup_access,
- "mutable_update_ns" => r.mut_update * 1e9,
- "immutable_update_ns" => r.imm_update * 1e9,
- "speedup_update" => r.speedup_update,
- "mutable_iter_ns" => r.mut_iter * 1e9,
- "immutable_iter_ns" => r.imm_iter * 1e9,
- "speedup_iter" => r.speedup_iter,
- "immutable_copy_ns" => r.imm_copy * 1e9
- ) for r in results
- ],
- "analysis" => Dict(
- "copy_slope_ns_per_field" => slope,
- "copy_intercept_ns" => intercept,
- "r_squared" => r_squared
- )
-)
-
-json_file = joinpath(output_dir, "struct_scaling_$(timestamp).json")
-open(json_file, "w") do f
- JSON.print(f, benchmark_data, 2)
-end
-println("✓ Data saved to: $json_file")
-println()
-
-# Also save as CSV for easy plotting
-csv_file = joinpath(output_dir, "struct_scaling_$(timestamp).csv")
-open(csv_file, "w") do f
- println(f, "nfields,bytes,mut_access_ns,imm_access_ns,speedup_access,mut_update_ns,imm_update_ns,speedup_update,mut_iter_ns,imm_iter_ns,speedup_iter,imm_copy_ns")
- for r in results
- println(f, "$(r.nfields),$(r.bytes),$(r.mut_access*1e9),$(r.imm_access*1e9),$(r.speedup_access),$(r.mut_update*1e9),$(r.imm_update*1e9),$(r.speedup_update),$(r.mut_iter*1e9),$(r.imm_iter*1e9),$(r.speedup_iter),$(r.imm_copy*1e9)")
- end
-end
-println("✓ CSV saved to: $csv_file")
-println()
-
-println("="^80)
-println("GENERATING PLOTS")
-println("="^80)
-println()
-
-# Note: Using Plots from global environment
-try
- # Import from global environment
- pushfirst!(LOAD_PATH, "@stdlib")
- import Plots
-
- # Set backend
- Plots.gr()
-
- # Extract data for plotting
- bytes_sizes = [r.bytes for r in results]
-
- # Plot 1: Field Access Performance
- p1 = Plots.plot(bytes_sizes, [r.mut_access * 1e9 for r in results],
- label="Mutable (Dict)", linewidth=2, marker=:circle,
- xlabel="Struct Size (bytes)", ylabel="Time (nanoseconds)",
- title="Field Access Performance",
- legend=:topleft, xscale=:log10, grid=true)
- Plots.plot!(p1, bytes_sizes, [r.imm_access * 1e9 for r in results],
- label="Immutable (NamedTuple)", linewidth=2, marker=:square)
-
- # Add typical FEM element size markers
- Plots.vline!(p1, [40, 400], label="Typical FEM (5-50 fields)",
- linestyle=:dash, linewidth=1, color=:gray)
-
- Plots.savefig(p1, joinpath(output_dir, "field_access_$(timestamp).png"))
- println("✓ Saved: field_access_$(timestamp).png")
-
- # Plot 2: Field Update Performance
- p2 = Plots.plot(bytes_sizes, [r.mut_update * 1e9 for r in results],
- label="Mutable (Dict)", linewidth=2, marker=:circle,
- xlabel="Struct Size (bytes)", ylabel="Time (nanoseconds)",
- title="Field Update Performance",
- legend=:topleft, xscale=:log10, grid=true)
- Plots.plot!(p2, bytes_sizes, [r.imm_update * 1e9 for r in results],
- label="Immutable (NamedTuple)", linewidth=2, marker=:square)
-
- Plots.vline!(p2, [40, 400], label="Typical FEM (5-50 fields)",
- linestyle=:dash, linewidth=1, color=:gray)
-
- Plots.savefig(p2, joinpath(output_dir, "field_update_$(timestamp).png"))
- println("✓ Saved: field_update_$(timestamp).png")
-
- # Plot 3: Iteration Performance
- p3 = Plots.plot(bytes_sizes, [r.mut_iter * 1e9 for r in results],
- label="Mutable (Dict)", linewidth=2, marker=:circle,
- xlabel="Struct Size (bytes)", ylabel="Time (nanoseconds)",
- title="Field Iteration Performance",
- legend=:topleft, xscale=:log10, yscale=:log10, grid=true)
- Plots.plot!(p3, bytes_sizes, [r.imm_iter * 1e9 for r in results],
- label="Immutable (NamedTuple)", linewidth=2, marker=:square)
-
- Plots.vline!(p3, [40, 400], label="Typical FEM (5-50 fields)",
- linestyle=:dash, linewidth=1, color=:gray)
-
- Plots.savefig(p3, joinpath(output_dir, "iteration_$(timestamp).png"))
- println("✓ Saved: iteration_$(timestamp).png")
-
- # Plot 4: Speedup Factors
- p4 = Plots.plot(bytes_sizes, [r.speedup_access for r in results],
- label="Access Speedup", linewidth=2, marker=:circle,
- xlabel="Struct Size (bytes)", ylabel="Speedup Factor (Immutable/Mutable)",
- title="Performance Advantage of Immutable Elements",
- legend=:right, xscale=:log10, grid=true)
- Plots.plot!(p4, bytes_sizes, [r.speedup_update for r in results],
- label="Update Speedup", linewidth=2, marker=:square)
- Plots.plot!(p4, bytes_sizes, [r.speedup_iter for r in results],
- label="Iteration Speedup", linewidth=2, marker=:diamond)
-
- Plots.hline!(p4, [1.0], label="Break-even", linestyle=:dash, color=:black, linewidth=1)
- Plots.vline!(p4, [40, 400], label="Typical FEM",
- linestyle=:dash, linewidth=1, color=:gray)
-
- Plots.savefig(p4, joinpath(output_dir, "speedup_factors_$(timestamp).png"))
- println("✓ Saved: speedup_factors_$(timestamp).png")
-
- # Plot 5: Copy Cost Scaling
- p5 = Plots.plot(bytes_sizes, [r.imm_copy * 1e9 for r in results],
- label="Measured", linewidth=2, marker=:circle,
- xlabel="Struct Size (bytes)", ylabel="Copy Time (nanoseconds)",
- title="Immutable Struct Copy Cost",
- legend=:topleft, xscale=:log10, grid=true)
-
- # Add linear fit
- fitted = [intercept + slope * r.nfields for r in results]
- Plots.plot!(p5, bytes_sizes, fitted,
- label="Linear Fit ($(round(slope, digits=4)) ns/field)",
- linewidth=2, linestyle=:dash)
-
- Plots.vline!(p5, [40, 400], label="Typical FEM",
- linestyle=:dash, linewidth=1, color=:gray)
-
- Plots.savefig(p5, joinpath(output_dir, "copy_cost_$(timestamp).png"))
- println("✓ Saved: copy_cost_$(timestamp).png")
-
- # Combined plot
- layout = Plots.@layout [a b; c d]
- p_combined = Plots.plot(p1, p2, p3, p4, layout=layout, size=(1200, 900))
- Plots.savefig(p_combined, joinpath(output_dir, "combined_$(timestamp).png"))
- println("✓ Saved: combined_$(timestamp).png")
-
- println()
- println("All plots saved successfully!")
-
-catch e
- println("⚠ Could not generate plots (Plots.jl not available in global environment)")
- println(" Error: $e")
- println(" Install with: julia -e 'using Pkg; Pkg.add(\"Plots\")'")
-end
-
-println()
-println("="^80)
diff --git a/benchmarks/test_gpu_benchmarks.sh b/benchmarks/test_gpu_benchmarks.sh
deleted file mode 100755
index ffe9d84..0000000
--- a/benchmarks/test_gpu_benchmarks.sh
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/bin/bash
-# Quick test script for GPU benchmarks
-
-echo "======================================================================"
-echo "GPU Benchmark Quick Test"
-echo "======================================================================"
-echo ""
-
-# Check Julia
-echo "Checking Julia installation..."
-if ! command -v julia &> /dev/null; then
- echo "❌ Julia not found! Please install Julia 1.9+"
- exit 1
-fi
-
-julia_version=$(julia --version)
-echo "✅ Found: $julia_version"
-echo ""
-
-# Check GPU
-echo "Checking GPU availability..."
-if command -v nvidia-smi &> /dev/null; then
- echo "✅ NVIDIA GPU detected:"
- nvidia-smi --query-gpu=name,memory.total --format=csv,noheader
- echo ""
-else
- echo "⚠️ No NVIDIA GPU detected. Benchmarks will run CPU-only."
- echo ""
-fi
-
-# Check CUDA.jl
-echo "Checking CUDA.jl..."
-julia --project=. -e '
-using Pkg
-try
- using CUDA
- if CUDA.functional()
- println("✅ CUDA.jl functional: ", CUDA.name(CUDA.device()))
- else
- println("⚠️ CUDA.jl installed but GPU not functional")
- end
-catch
- println("⚠️ CUDA.jl not installed. Run: Pkg.add(\"CUDA\")")
-end
-' 2>/dev/null
-echo ""
-
-# Run quick state management test (small size)
-echo "======================================================================"
-echo "Test 1: State Management (10K elements)"
-echo "======================================================================"
-julia --project=. -e '
-n = 10_000
-println("Running state management benchmark with $n elements...")
-include("benchmarks/gpu_state_management_benchmark.jl")
-
-# Override main() to run smaller test
-Δε_p, Δα, elements_s1, geometry_s2, state_s2 = setup_benchmark(n)
-
-# CPU test
-println("\n📊 CPU Test:")
-state_s2_copy = deepcopy(state_s2)
-t = @elapsed update_state_strategy2_cpu!(state_s2_copy, Δε_p, Δα)
-println("Time: $(round(t * 1000, digits=2)) ms")
-println("✅ CPU benchmark works!")
-
-# GPU test (if available)
-if USE_GPU
- println("\n📊 GPU Test:")
- try
- T = Float64
- state_gpu = AssemblyState{T}(
- CUDA.zeros(T, n * 6),
- CUDA.zeros(T, n),
- n
- )
- Δε_p_flat = zeros(T, n * 6)
- Δα_gpu = CuArray(Δα)
- for i in 1:n
- offset = (i - 1) * 6
- ε = Δε_p[i]
- Δε_p_flat[offset + 1] = ε[1, 1]
- Δε_p_flat[offset + 2] = ε[2, 2]
- Δε_p_flat[offset + 3] = ε[3, 3]
- Δε_p_flat[offset + 4] = ε[1, 2]
- Δε_p_flat[offset + 5] = ε[1, 3]
- Δε_p_flat[offset + 6] = ε[2, 3]
- end
- Δε_p_flat_gpu = CuArray(Δε_p_flat)
-
- update_state_strategy2_gpu!(state_gpu, Δε_p_flat_gpu, Δα_gpu)
- println("✅ GPU benchmark works!")
- catch e
- println("⚠️ GPU test failed: $e")
- end
-end
-'
-echo ""
-
-# Run quick matrix-free test (small size)
-echo "======================================================================"
-echo "Test 2: Matrix-Free Newton-Krylov (1K DOFs)"
-echo "======================================================================"
-julia --project=. -e '
-n = 1000
-println("Running matrix-free benchmark with $n DOFs...")
-include("benchmarks/matrix_free_gpu_benchmark.jl")
-
-# Override to run small test
-T = Float64
-K = Matrix(Tridiagonal(-ones(T, n-1), 2ones(T, n), -ones(T, n-1)))
-f = ones(T, n) * 0.1
-β = T(1e-3)
-prob = NonlinearProblem(K, f, β, n)
-
-println("\n📊 CPU Test:")
-u = zeros(T, n)
-r = zeros(T, n)
-du = zeros(T, n)
-temp = zeros(T, n)
-Jv = zeros(T, n)
-
-t = @elapsed iters = newton_matrix_free!(u, prob, r, du, temp, Jv;
- max_iter=10, verbose=false)
-println("Time: $(round(t * 1000, digits=2)) ms")
-println("Iterations: $iters")
-println("✅ CPU benchmark works!")
-
-if USE_GPU
- println("\n📊 GPU Test:")
- try
- K_gpu = CuArray(K)
- f_gpu = CuArray(f)
- u_gpu = CUDA.zeros(T, n)
-
- t_gpu = CUDA.@elapsed begin
- iters_gpu = newton_matrix_free_gpu!(u_gpu, K_gpu, f_gpu, β;
- max_iter=10, verbose=false)
- CUDA.synchronize()
- end
-
- println("Time: $(round(t_gpu * 1000, digits=2)) ms")
- println("Iterations: $iters_gpu")
- println("✅ GPU benchmark works!")
- catch e
- println("⚠️ GPU test failed: $e")
- end
-end
-'
-echo ""
-
-echo "======================================================================"
-echo "Quick Test Complete!"
-echo "======================================================================"
-echo ""
-echo "To run full benchmarks:"
-echo " julia --project=. benchmarks/gpu_state_management_benchmark.jl"
-echo " julia --project=. benchmarks/matrix_free_gpu_benchmark.jl"
-echo ""
diff --git a/benchmarks/tet10_derivatives_benchmark.jl b/benchmarks/tet10_derivatives_benchmark.jl
deleted file mode 100644
index 80d7207..0000000
--- a/benchmarks/tet10_derivatives_benchmark.jl
+++ /dev/null
@@ -1,284 +0,0 @@
-# Benchmark: Tet10 Shape Function Derivatives - Manual vs AD
-#
-# Compares two approaches:
-# 1. Manual: Hand-calculated derivatives (traditional FEM)
-# 2. AD: Automatic differentiation using Tensors.jl gradient()
-#
-# Run with: julia --project=. benchmarks/tet10_derivatives_benchmark.jl
-
-using BenchmarkTools
-using Tensors
-using Printf
-
-println("="^70)
-println("Tet10 Shape Function Derivatives: Manual vs AD Benchmark")
-println("="^70)
-println()
-
-# ============================================================================
-# METHOD 1: MANUAL (Hand-Calculated Derivatives)
-# ============================================================================
-
-"""
-Tet10 shape functions (manual implementation).
-Reference element: ξ ∈ [0,1], η ∈ [0,1], ζ ∈ [0,1], ξ+η+ζ ≤ 1
-"""
-module ManualTet10
-using Tensors
-
-# Shape functions
-@inline N1(ξ, η, ζ) = (1 - ξ - η - ζ) * (2 * (1 - ξ - η - ζ) - 1)
-@inline N2(ξ, η, ζ) = ξ * (2 * ξ - 1)
-@inline N3(ξ, η, ζ) = η * (2 * η - 1)
-@inline N4(ξ, η, ζ) = ζ * (2 * ζ - 1)
-@inline N5(ξ, η, ζ) = 4 * ξ * (1 - ξ - η - ζ)
-@inline N6(ξ, η, ζ) = 4 * ξ * η
-@inline N7(ξ, η, ζ) = 4 * η * (1 - ξ - η - ζ)
-@inline N8(ξ, η, ζ) = 4 * ζ * (1 - ξ - η - ζ)
-@inline N9(ξ, η, ζ) = 4 * ξ * ζ
-@inline N10(ξ, η, ζ) = 4 * η * ζ
-
-# Derivatives (calculated by hand - error-prone!)
-@inline dN1_dξ(ξ, η, ζ) = 4 * ξ + 4 * η + 4 * ζ - 3
-@inline dN1_dη(ξ, η, ζ) = 4 * ξ + 4 * η + 4 * ζ - 3
-@inline dN1_dζ(ξ, η, ζ) = 4 * ξ + 4 * η + 4 * ζ - 3
-
-@inline dN2_dξ(ξ, η, ζ) = 4 * ξ - 1
-@inline dN2_dη(ξ, η, ζ) = 0.0
-@inline dN2_dζ(ξ, η, ζ) = 0.0
-
-@inline dN3_dξ(ξ, η, ζ) = 0.0
-@inline dN3_dη(ξ, η, ζ) = 4 * η - 1
-@inline dN3_dζ(ξ, η, ζ) = 0.0
-
-@inline dN4_dξ(ξ, η, ζ) = 0.0
-@inline dN4_dη(ξ, η, ζ) = 0.0
-@inline dN4_dζ(ξ, η, ζ) = 4 * ζ - 1
-
-@inline dN5_dξ(ξ, η, ζ) = 4 * (1 - 2 * ξ - η - ζ)
-@inline dN5_dη(ξ, η, ζ) = -4 * ξ
-@inline dN5_dζ(ξ, η, ζ) = -4 * ξ
-
-@inline dN6_dξ(ξ, η, ζ) = 4 * η
-@inline dN6_dη(ξ, η, ζ) = 4 * ξ
-@inline dN6_dζ(ξ, η, ζ) = 0.0
-
-@inline dN7_dξ(ξ, η, ζ) = -4 * η
-@inline dN7_dη(ξ, η, ζ) = 4 * (1 - ξ - 2 * η - ζ)
-@inline dN7_dζ(ξ, η, ζ) = -4 * η
-
-@inline dN8_dξ(ξ, η, ζ) = -4 * ζ
-@inline dN8_dη(ξ, η, ζ) = -4 * ζ
-@inline dN8_dζ(ξ, η, ζ) = 4 * (1 - ξ - η - 2 * ζ)
-
-@inline dN9_dξ(ξ, η, ζ) = 4 * ζ
-@inline dN9_dη(ξ, η, ζ) = 0.0
-@inline dN9_dζ(ξ, η, ζ) = 4 * ξ
-
-@inline dN10_dξ(ξ, η, ζ) = 0.0
-@inline dN10_dη(ξ, η, ζ) = 4 * ζ
-@inline dN10_dζ(ξ, η, ζ) = 4 * η
-
-# Evaluation function (returns tuple - zero allocation)
-@inline function eval_basis_and_grad(xi::Vec{3})
- ξ, η, ζ = xi[1], xi[2], xi[3]
-
- N = (N1(ξ, η, ζ), N2(ξ, η, ζ), N3(ξ, η, ζ), N4(ξ, η, ζ), N5(ξ, η, ζ),
- N6(ξ, η, ζ), N7(ξ, η, ζ), N8(ξ, η, ζ), N9(ξ, η, ζ), N10(ξ, η, ζ))
-
- dN = (Vec(dN1_dξ(ξ, η, ζ), dN1_dη(ξ, η, ζ), dN1_dζ(ξ, η, ζ)),
- Vec(dN2_dξ(ξ, η, ζ), dN2_dη(ξ, η, ζ), dN2_dζ(ξ, η, ζ)),
- Vec(dN3_dξ(ξ, η, ζ), dN3_dη(ξ, η, ζ), dN3_dζ(ξ, η, ζ)),
- Vec(dN4_dξ(ξ, η, ζ), dN4_dη(ξ, η, ζ), dN4_dζ(ξ, η, ζ)),
- Vec(dN5_dξ(ξ, η, ζ), dN5_dη(ξ, η, ζ), dN5_dζ(ξ, η, ζ)),
- Vec(dN6_dξ(ξ, η, ζ), dN6_dη(ξ, η, ζ), dN6_dζ(ξ, η, ζ)),
- Vec(dN7_dξ(ξ, η, ζ), dN7_dη(ξ, η, ζ), dN7_dζ(ξ, η, ζ)),
- Vec(dN8_dξ(ξ, η, ζ), dN8_dη(ξ, η, ζ), dN8_dζ(ξ, η, ζ)),
- Vec(dN9_dξ(ξ, η, ζ), dN9_dη(ξ, η, ζ), dN9_dζ(ξ, η, ζ)),
- Vec(dN10_dξ(ξ, η, ζ), dN10_dη(ξ, η, ζ), dN10_dζ(ξ, η, ζ)))
-
- return N, dN
-end
-end
-
-# ============================================================================
-# ============================================================================
-# METHOD 2: AD (Tensors.jl gradient)
-# ============================================================================
-
-module ADTet10
-using Tensors
-
-# Just shape functions (no manual derivatives!)
-@inline N1(xi) = (1 - xi[1] - xi[2] - xi[3]) * (2 * (1 - xi[1] - xi[2] - xi[3]) - 1)
-@inline N2(xi) = xi[1] * (2 * xi[1] - 1)
-@inline N3(xi) = xi[2] * (2 * xi[2] - 1)
-@inline N4(xi) = xi[3] * (2 * xi[3] - 1)
-@inline N5(xi) = 4 * xi[1] * (1 - xi[1] - xi[2] - xi[3])
-@inline N6(xi) = 4 * xi[1] * xi[2]
-@inline N7(xi) = 4 * xi[2] * (1 - xi[1] - xi[2] - xi[3])
-@inline N8(xi) = 4 * xi[3] * (1 - xi[1] - xi[2] - xi[3])
-@inline N9(xi) = 4 * xi[1] * xi[3]
-@inline N10(xi) = 4 * xi[2] * xi[3]
-
-const shape_fns = (N1, N2, N3, N4, N5, N6, N7, N8, N9, N10)
-
-@inline function eval_basis_and_grad(xi::Vec{3})
- # Evaluate basis functions
- N = ntuple(i -> shape_fns[i](xi), 10)
-
- # Compute gradients with Tensors.jl gradient()
- dN = ntuple(i -> gradient(shape_fns[i], xi), 10)
-
- return N, dN
-end
-end
-
-# ============================================================================
-# BENCHMARKING
-# ============================================================================
-
-println("Setting up benchmark...")
-println()
-
-# Test point (typical integration point)
-const ξ_test = Vec(0.25, 0.25, 0.25)
-
-# Verification: Both methods should give same results
-println("Verifying correctness...")
-N_manual, dN_manual = ManualTet10.eval_basis_and_grad(ξ_test)
-N_ad, dN_ad = ADTet10.eval_basis_and_grad(ξ_test)
-
-println(" Manual basis: ", N_manual)
-println(" AD basis: ", N_ad)
-println()
-
-# Check agreement
-rtol = 1e-10
-if !all(isapprox.(N_manual, N_ad, rtol=rtol))
- @warn "Manual and AD basis functions disagree!"
-end
-
-# Check derivatives
-for i in 1:10
- if !isapprox(dN_manual[i], dN_ad[i], rtol=rtol)
- @warn "Manual and AD derivative $i disagree!" dN_manual[i] dN_ad[i]
- end
-end
-
-println("✓ Both methods agree (within tolerance)")
-println()
-
-# ============================================================================
-# RUN BENCHMARKS
-# ============================================================================
-
-println("Running benchmarks (this may take a minute)...")
-println()
-
-# Warm-up
-for _ in 1:1000
- ManualTet10.eval_basis_and_grad(ξ_test)
- ADTet10.eval_basis_and_grad(ξ_test)
-end
-
-# Benchmark each method
-b_manual = @benchmark ManualTet10.eval_basis_and_grad($ξ_test)
-b_ad = @benchmark ADTet10.eval_basis_and_grad($ξ_test)
-
-# ============================================================================
-# RESULTS
-# ============================================================================
-
-# ============================================================================
-# RESULTS
-# ============================================================================
-
-println("="^70)
-println("RESULTS")
-println("="^70)
-println()
-
-# Extract median times
-t_manual = median(b_manual.times)
-t_ad = median(b_ad.times)
-
-# Extract allocations
-alloc_manual = b_manual.allocs
-alloc_ad = b_ad.allocs
-
-# Calculate relative speed
-rel_ad = t_ad / t_manual
-
-println("Method | Time (ns) | Allocations | Relative Speed")
-println("----------------|-----------|-------------|----------------")
-@printf "Manual | %9.1f | %11d | %.2f× (baseline)\n" t_manual alloc_manual 1.0
-@printf "AD (Tensors.jl) | %9.1f | %11d | %.2f×\n" t_ad alloc_ad rel_ad
-println()
-
-# Detailed stats
-println("Detailed Statistics:")
-println()
-println("Manual (Hand-Calculated):")
-display(b_manual)
-println()
-println()
-println("AD (Tensors.jl gradient):")
-display(b_ad)
-println()
-println()
-
-# ============================================================================
-# ANALYSIS
-# ============================================================================
-
-println("="^70)
-println("ANALYSIS")
-println("="^70)
-println()
-
-if rel_ad < 2.0
- println("🎉 RECOMMENDATION: Use AD everywhere!")
- println()
- println("Tensors.jl AD is within 2× of manual, providing:")
- println(" ✓ Zero maintenance burden")
- println(" ✓ No manual derivative errors")
- println(" ✓ Easy to add new elements")
- println(" ✓ Supports any basis type")
- println()
- println("Small performance cost is acceptable for these benefits.")
-elseif rel_ad < 5.0
- println("⚠️ RECOMMENDATION: Hybrid approach")
- println()
- println("AD is 2-5× slower than manual. Consider:")
- println(" • Common elements (Tet10, Hex8, Quad4): Manual")
- println(" • Rare elements: AD-generated")
- println(" • Research/prototype elements: Always AD")
- println()
- println("This balances performance and maintainability.")
-else
- println("❌ RECOMMENDATION: Manual derivatives (with symbolic generation)")
- println()
- println("AD is >5× slower than manual. For performance-critical code:")
- println(" • Generate derivatives with SymPy/Symbolics.jl")
- println(" • Unit test against AD to verify correctness")
- println(" • Accept the maintenance burden")
- println()
- println("Consider AD only for prototyping.")
-end
-
-println()
-println("Memory analysis:")
-if alloc_manual == 0 && alloc_ad == 0
- println(" ✓ Both methods achieve zero allocations (excellent!)")
-elseif alloc_manual == 0 && alloc_ad > 0
- println(" ⚠️ AD allocates (", alloc_ad, " allocs)")
- println(" This will hurt performance in tight loops.")
-else
- println(" ⚠️ Unexpected allocation pattern - investigate!")
-end
-
-println()
-println("="^70)
-println("Benchmark complete! Results saved to console.")
-println("="^70)