""" # 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φ = K * φ[i] λMφ = λ[i] * (M * φ[i]) @test Kφ ≈ λ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) """