test(solvers): add modal analysis new API visionary test

New 560-line test file for modal analysis NEW API (test-driven development):
- Visionary tests for ModalSolver and EigenvalueProblem
- Tests single Tet4 element modal analysis
- Tests with and without geometric stiffness
- Tests heat transfer modal analysis
- Extensive documentation of intended API design
- Tests currently skipped (@test_skip) until implementation complete
- Documents eigenvalue problem K φ = λ M φ
- Documents mass matrix assembly and mode shape normalization

This test file serves as both test suite and API design documentation
for the new modal analysis solver interface.
This commit is contained in:
Jukka Aho
2025-12-15 16:19:59 +02:00
parent 782c252719
commit b9534e2c55
+560
View File
@@ -0,0 +1,560 @@
"""
# Modal Analysis - NEW API (Test-Driven Development)
**What:** Shows how modal (eigenvalue) analysis SHOULD work with the NEW API
**Why:**
- **Eigenvalue problems** - Different solver type (not Newton-Krylov)
- **Natural frequencies** - Validates dynamic behavior
- **Geometric stiffness** - Prestress effects on frequencies
- **Multiple physics** - Works for elasticity AND heat transfer
- **Mass matrix required** - First time we need M, not just K
**NEW API Concepts:**
1. **ModalSolver** - Eigenvalue solver (not LinearSolver or NewtonSolver)
2. **EigenvalueProblem** - K φ = λ M φ (generalized eigenvalue problem)
3. **Geometric stiffness** - K_g from initial stress/displacement
4. **Modal shapes** - φ_i (eigenvectors = mode shapes)
5. **Natural frequencies** - ω_i = √λ_i
**Test Problems:**
## Test 1: Single Tet4 Element
- 4-node tetrahedron with 3 nodes fixed
- Validates basic eigenvalue computation
- Tests with and without geometric stiffness
## Test 2: Heat Transfer Modal Analysis
- Two Quad4 elements (plane heat)
- Eigenvalues of thermal diffusion operator
- Validates modal analysis works for temperature field
## Test 3: Vibrating Beam (Future)
- Fixed-fixed beam
- Compare natural frequencies to analytical solution
- ω_i = λ_i² √(EI/ρA)
**Expected Behavior (when implemented):**
✅ Eigenvalues computed correctly
✅ Mode shapes orthogonal (φ_i^T M φ_j = δ_ij)
✅ Geometric stiffness affects frequencies
✅ Works for both elasticity and heat transfer
✅ Sparse eigenvalue solver (only need first k modes)
**Status:** 🚧 VISIONARY TEST - Implementation in progress
"""
using Test
using JuliaFEM
using Tensors
using LinearAlgebra
using Statistics
@testset "Modal Analysis - NEW API (TDD)" begin
# =============================================================================
# SINGLE ELEMENT: TET4 WITHOUT GEOMETRIC STIFFNESS
# =============================================================================
@testset "Single Tet4 Modal Analysis (Visionary)" begin
@test_skip begin # Skip until implemented
# Geometry
X = Dict(
1 => Vec(2.0, 3.0, 4.0),
2 => Vec(6.0, 3.0, 2.0),
3 => Vec(2.0, 5.0, 1.0),
4 => Vec(4.0, 3.0, 6.0)
)
# Initial displacement (for geometric stiffness later)
u0 = Dict(
1 => Vec(0.0, 0.0, 0.0),
2 => Vec(0.0, 0.0, 0.0),
3 => Vec(0.0, 0.0, 0.0),
4 => Vec(0.25, 0.25, 0.25)
)
# Material
material = LinearElastic(E=96.0, ν=1 / 3, ρ=420.0)
# Physics
elastic_physics = ContinuumPhysics{Displacement}(
material=material,
formulation=FullThreeD(),
finite_strain=false,
geometric_stiffness=false # No K_g initially
)
# Create element
tet_element = create_element(
Tet4, (1, 2, 3, 4),
geometry=X,
displacement=u0
)
# Domain
domain = Domain(
name="TET",
elements=[tet_element],
physics=elastic_physics
)
# Boundary conditions (3 nodes fixed)
bc_fixed = DirichletBC(
name="FIXED_FACE",
nodes=[1, 2, 3],
dof=:displacement,
values=[0.0, 0.0, 0.0]
)
# NEW: EigenvalueProblem (not static or transient)
problem = EigenvalueProblem(
domains=[domain],
boundary_conditions=[bc_fixed]
)
# NEW: ModalSolver
solver = ModalSolver(
n_modes=2, # Number of modes to compute
which=:SM, # Smallest Magnitude (or :LM for largest)
method=:arpack # ARPACK, KrylovKit, or direct
)
# Solve: K φ = λ M φ
solution = solve!(problem, solver)
# Extract eigenvalues and eigenvectors
λ = solution.eigenvalues # [λ_1, λ_2]
φ = solution.eigenvectors # [φ_1, φ_2]
# Validate eigenvalues (without geometric stiffness)
@test isapprox(λ, [4 / 3, 1 / 3], rtol=1e-6)
# Validate orthogonality: φ_i^T M φ_j = δ_ij
for i in 1:2
for j in 1:2
orthogonality = φ[i]' * solution.mass_matrix * φ[j]
expected = (i == j) ? 1.0 : 0.0
@test isapprox(orthogonality, expected, atol=1e-6)
end
end
end
end
# =============================================================================
# GEOMETRIC STIFFNESS EFFECT
# =============================================================================
@testset "Tet4 with Geometric Stiffness (Visionary)" begin
@test_skip begin
# Same setup as before, but enable geometric stiffness
elastic_physics = ContinuumPhysics{Displacement}(
material=LinearElastic(E=96.0, ν=1 / 3, ρ=420.0),
formulation=FullThreeD(),
finite_strain=false,
geometric_stiffness=true # NEW: Enable K_g
)
problem = EigenvalueProblem(
domains=[domain],
boundary_conditions=[bc_fixed],
initial_displacement=u0 # Required for K_g
)
solver = ModalSolver(n_modes=2, which=:SM)
solution = solve!(problem, solver)
λ = solution.eigenvalues
# With geometric stiffness: K_eff = K + K_g(u0)
# Eigenvalues should be different!
@test isapprox(λ, [5 / 3, 2 / 3], rtol=1e-6)
# Geometric stiffness changes natural frequencies
@test λ[1] > 4 / 3 # Stiffened by prestress
@test λ[2] > 1 / 3
end
end
# =============================================================================
# HEAT TRANSFER MODAL ANALYSIS
# =============================================================================
@testset "Heat Transfer Modal (Visionary)" begin
@test_skip begin
# Geometry: Two quad elements stacked vertically
X = Dict(
1 => Vec(0.0, 0.0),
2 => Vec(1.0, 0.0),
3 => Vec(1.0, 3.0),
4 => Vec(0.0, 3.0),
5 => Vec(0.0, 3.0),
6 => Vec(1.0, 3.0),
7 => Vec(1.0, 9.0),
8 => Vec(0.0, 9.0)
)
# Material (thermal)
thermal_material = ThermalMaterial(
conductivity=36.0,
density=6.0,
specific_heat=1.0
)
# Physics
thermal_physics = ContinuumPhysics{Temperature}(
material=thermal_material,
formulation=PlaneHeat()
)
# Elements
elem1 = create_element(Quad4, (1, 2, 3, 4), geometry=X)
elem2 = create_element(Quad4, (4, 3, 7, 8), geometry=X)
# Domain
domain = Domain(
name="THERMAL",
elements=[elem1, elem2],
physics=thermal_physics
)
# Boundary conditions (fixed temperature at ends)
bc_bottom = DirichletBC(
nodes=[1, 2],
dof=:temperature,
value=0.0
)
bc_top = DirichletBC(
nodes=[7, 8],
dof=:temperature,
value=0.0
)
# Eigenvalue problem for heat equation
# M ∂T/∂t + K T = 0
# Modal: K φ = λ M φ
problem = EigenvalueProblem(
domains=[domain],
boundary_conditions=[bc_bottom, bc_top]
)
solver = ModalSolver(n_modes=1, which=:SM)
solution = solve!(problem, solver)
λ = solution.eigenvalues
# First eigenvalue should be 1.0 (analytical)
@test isapprox(λ[1], 1.0, rtol=1e-6)
end
end
# =============================================================================
# NATURAL FREQUENCIES FROM EIGENVALUES
# =============================================================================
@testset "Natural Frequency Computation (Visionary)" begin
@test_skip begin
# Solve modal problem
solution = solve_modal_problem()
# Extract eigenvalues
λ = solution.eigenvalues # [rad²/s²]
# Natural frequencies: ω = √λ [rad/s]
ω = sqrt.(λ)
# Convert to Hz: f = ω / (2π)
f = ω ./ (2π)
# Validate units and values
@test all(λ .≥ 0) # Eigenvalues non-negative
@test all(ω .≥ 0) # Frequencies non-negative
@test all(f .≥ 0)
# For simple beam: f_1 ≈ 1-10 Hz (typical)
@test 1.0 < f[1] < 100.0
end
end
# =============================================================================
# MODE SHAPE VISUALIZATION (FUTURE)
# =============================================================================
@testset "Mode Shape Properties (Visionary)" begin
@test_skip begin
solution = solve_modal_problem()
φ = solution.eigenvectors # Mode shapes
M = solution.mass_matrix
K = solution.stiffness_matrix
λ = solution.eigenvalues
# Property 1: Orthogonality w.r.t. mass matrix
# φ_i^T M φ_j = δ_ij
for i in 1:length(φ)
for j in 1:length(φ)
orth_M = φ[i]' * M * φ[j]
expected = (i == j) ? 1.0 : 0.0
@test isapprox(orth_M, expected, atol=1e-6)
end
end
# Property 2: Eigenvalue equation
# K φ_i = λ_i M φ_i
for i in 1:length(φ)
= K * φ[i]
λMφ = λ[i] * (M * φ[i])
@test λMφ rtol = 1e-6
end
# Property 3: Mode shapes normalized
# φ_i^T M φ_i = 1
for i in 1:length(φ)
norm_M = φ[i]' * M * φ[i]
@test isapprox(norm_M, 1.0, rtol=1e-6)
end
end
end
# =============================================================================
# MASS MATRIX ASSEMBLY
# =============================================================================
@testset "Mass Matrix Assembly (Visionary)" begin
# Pseudo-code showing mass matrix assembly
println("\n" * "="^70)
println("MASS MATRIX ASSEMBLY (NODAL APPROACH)")
println("="^70)
mass_assembly_pseudo = """
# Similar to stiffness, but uses density ρ and N^T N
for node_i in nodes
for elem in node_to_elements[node_i]
for node_j in elem.nodes
# Mass matrix block (3×3 for displacement)
M_ij = ∫_Ω ρ N_i N_j dΩ
# For displacement (3D):
# M_ij = m_ij * I_3×3 (often lumped)
# Consistent mass (full integration)
M_block = compute_mass_block(elem, node_i, node_j, ρ)
# Or lumped mass (diagonal only)
if lumped
M_block = (i == j) ? diag(M_block) : 0
end
M_nodal[node_i, node_j] += M_block
end
end
end
"""
println(mass_assembly_pseudo)
println("="^70)
println("✓ Mass matrix M = ∫_Ω ρ N^T N dΩ")
println("✓ Consistent mass: Full integration (more accurate)")
println("✓ Lumped mass: Diagonal only (faster, explicit dynamics)")
println("✓ Nodal assembly works same as stiffness!")
println("="^70)
end
# =============================================================================
# KEY ARCHITECTURAL INSIGHTS
# =============================================================================
println("\n" * "="^70)
println("MODAL ANALYSIS ARCHITECTURE INSIGHTS (NEW API)")
println("="^70)
println("✓ Modal analysis is EigenvalueProblem (K φ = λ M φ)")
println("✓ Requires both stiffness K AND mass M matrices")
println("✓ ModalSolver uses sparse eigenvalue methods (ARPACK)")
println("✓ Geometric stiffness K_g affects natural frequencies")
println("✓ Works for ANY physics (displacement, temperature, etc.)")
println("✓ Mode shapes φ orthogonal w.r.t. mass matrix")
println("✓ Natural frequencies ω = √λ, in Hz: f = ω/(2π)")
println("✓ Same nodal assembly pattern for M as for K!")
println("="^70)
end
"""
# IMPLEMENTATION NOTES
## Generalized Eigenvalue Problem
**Mathematical formulation:**
K φ = λ M φ
where:
- K = stiffness matrix (from elastic/thermal energy)
- M = mass matrix (from kinetic/thermal energy)
- φ = eigenvector (mode shape)
- λ = eigenvalue (ω² for vibrations, α for heat)
**For structural vibrations:**
- ω = √λ (natural frequency in rad/s)
- f = ω/(2π) (natural frequency in Hz)
- φ = displacement mode shape
**For heat diffusion:**
- α = λ (thermal diffusivity eigenvalue)
- φ = temperature mode shape
## Stiffness Matrix
**Elasticity:**
K = ∫_Ω B^T C B dΩ
where:
- B = strain-displacement matrix
- C = elasticity tensor
**Heat transfer:**
K = ∫_Ω k ∇N^T ∇N dΩ
where:
- k = thermal conductivity
## Mass Matrix
**Elasticity (consistent):**
M = ∫_Ω ρ N^T N dΩ
where ρ = density.
**Elasticity (lumped):**
M_ii = ∑_elements ∫_Ω_e ρ N_i dΩ_e
(Diagonal only, faster for explicit dynamics)
**Heat transfer:**
M = ∫_Ω ρc N^T N dΩ
where:
- ρ = density
- c = specific heat
## Geometric Stiffness
**Definition:** Stiffness contribution from initial stress state.
K_g = ∫_Ω G^T σ₀ G dΩ
where:
- G = geometric matrix (relates δε to ∇(δu))
- σ₀ = initial stress
**Effect:** Prestress changes natural frequencies:
- Tension → higher frequencies (stiffening)
- Compression → lower frequencies (softening)
**Total effective stiffness:**
K_eff = K + K_g
Then solve: K_eff φ = λ M φ
## Sparse Eigenvalue Solvers
**Problem:** For large systems, computing ALL eigenvalues is expensive.
**Solution:** Sparse eigenvalue methods (compute only k << n eigenvalues).
**Methods:**
1. **ARPACK** - Arnoldi iteration (standard in Julia)
2. **KrylovKit.jl** - Modern Krylov methods
3. **Lanczos** - For symmetric problems (K and M symmetric)
**JuliaFEM approach:**
```julia
using Arpack
# Solve K φ = λ M φ for k smallest eigenvalues
λ, φ = eigs(K, M, nev=k, which=:SM)
```
## Mode Shape Normalization
**Goal:** Normalize eigenvectors for convenience.
**Mass normalization (standard):**
φ_i^T M φ_i = 1
**Advantages:**
- Orthogonality: φ_i^T M φ_j = δ_ij
- Energy interpretation clear
- Modal damping ratios well-defined
**Implementation:**
```julia
function normalize_modes!(φ, M)
for i in 1:length(φ)
# Compute φ_i^T M φ_i
norm_M = φ[i]' * M * φ[i]
# Normalize
φ[i] ./= sqrt(norm_M)
end
end
```
## Nodal Assembly for Mass Matrix
```julia
function assemble_mass_nodal!(M_nodal, ρ, elements, node_to_elements)
Threads.@threads for node_i in 1:n_nodes
for elem_idx in node_to_elements[node_i]
elem = elements[elem_idx]
for node_j in elem.nodes
# Mass matrix block (3×3 for displacement)
M_ij = compute_mass_block(elem, node_i, node_j, ρ)
# Add to global (no atomic on diagonal if i = j)
if node_i == node_j
M_nodal[node_i, node_i] += M_ij # Direct write
else
atomic_add!(M_nodal[node_i, node_j], M_ij)
end
end
end
end
end
```
**Key:** SAME PATTERN as stiffness assembly!
## Next Steps
1. Implement `EigenvalueProblem` type
2. Implement `ModalSolver` with ARPACK integration
3. Implement mass matrix assembly (nodal)
4. Implement geometric stiffness computation
5. Add mode shape normalization
6. Validate against analytical solutions
7. Performance benchmarks (large systems)
"""