// Copyright (c) 2024 RustyTorch++ Team // Licensed under the Apache License, Version 2.0 //! Integration tests for RTX-FEA crate. use rtx_fea::prelude::*; use rtx_fea::{ analysis::{AnalysisConfig, StaticLinearAnalysis}, assembly::{AdvancedDofNumbering, DofComponent, DofMappingStrategy, GlobalAssembler}, boundary::{BoundaryConditionSet, DirichletBC, NeumannBC}, elements::{ElementFactory, FiniteElement}, materials::{LinearElastic, MaterialDatabase}, mesh::{ElementType, MaterialId}, solvers::{CholeskyDirect, LuDirect}, }; #[test] fn test_complete_fea_workflow() { // Test a complete FEA workflow from mesh to results let mesh = Mesh::generate_rectangle(1.0, 1.0, 3, 3).unwrap(); let mut materials = MaterialDatabase::new(); let steel = LinearElastic::new(200e9, 0.3); materials.add_material(MaterialId(0), steel, Some("Steel".to_string())); let boundary_conditions = BoundaryConditionSet::new(); let config = AnalysisConfig::default(); let mut analysis = StaticLinearAnalysis::new(mesh, materials, boundary_conditions, config); // Analysis should be created successfully assert_eq!(analysis.analysis_type(), "Static Linear"); assert!(!analysis.is_complete()); assert_eq!(analysis.progress(), 0.0); } #[test] fn test_mesh_generation() { // Test various mesh generation capabilities let quad_mesh = Mesh::generate_rectangle(2.0, 1.0, 5, 3).unwrap(); assert_eq!(quad_mesh.num_nodes(), 15); // 5x3 grid assert_eq!(quad_mesh.num_elements(), 8); // 4x2 quads let tri_mesh = Mesh::generate_rectangle(2.0, 1.0, 5, 3).unwrap(); assert!(tri_mesh.num_elements() > 0); // Note: validate() method not yet implemented on Mesh // assert!(tri_mesh.validate().is_ok()); } #[test] fn test_material_database() { let mut materials = MaterialDatabase::new(); let steel = LinearElastic::new(200e9, 0.3); let aluminum = LinearElastic::new(70e9, 0.33); let steel_id = MaterialId(0); let aluminum_id = MaterialId(1); materials.add_material(steel_id, steel, Some("Steel".to_string())); materials.add_material(aluminum_id, aluminum, Some("Aluminum".to_string())); assert_ne!(steel_id, aluminum_id); assert!(materials.get_material(steel_id).is_some()); assert!(materials.get_material(aluminum_id).is_some()); assert_eq!(materials.get_name(steel_id), Some("Steel")); assert_eq!(materials.get_name(aluminum_id), Some("Aluminum")); } #[test] fn test_element_factory() { // `Point`, `Line2` and `Line3` have no interpolation over an area or // volume, so the factory deliberately rejects them. This previously // required every variant of `ElementType::all()` to construct, which // could only pass if the factory stopped making that distinction. let unsupported = [ElementType::Point, ElementType::Line2, ElementType::Line3]; for element_type in ElementType::all() { let element = ElementFactory::create(element_type); if unsupported.contains(&element_type) { assert!( element.is_err(), "{element_type:?} has no area or volume interpolation and should be rejected" ); continue; } let elem = element.unwrap_or_else(|e| panic!("failed to create {element_type:?}: {e}")); assert_eq!( elem.num_nodes(), element_type.node_count(), "{element_type:?} reported the wrong node count" ); assert!(elem.spatial_dimension() >= 1 && elem.spatial_dimension() <= 3); } } #[test] fn test_dof_numbering() { let mesh = Mesh::generate_rectangle(1.0, 1.0, 3, 3).unwrap(); let dof_numbering = AdvancedDofNumbering::displacement_only(&mesh, DofMappingStrategy::Sequential).unwrap(); // 9 nodes x 2 displacement components: `generate_rectangle` builds a // planar mesh, which has no out-of-plane displacement to number. // // This previously expected 27, from numbering all three components on a // 2-D mesh. That gave every node a degree of freedom the element // matrices never supply, so assembly rejected every element with a // dimension mismatch — and `comprehensive_tdd_tests::test_dof_numbering` // asserts `num_nodes * 2` for the same situation, so the two tests // contradicted each other. assert_eq!(dof_numbering.total_dofs, 18); assert_eq!(dof_numbering.num_free_dofs(), 18); assert_eq!(dof_numbering.num_constrained_dofs(), 0); // Test bandwidth optimized numbering. Reordering DOFs must not change how // many there are. let optimized_numbering = AdvancedDofNumbering::displacement_only(&mesh, DofMappingStrategy::BandwidthOptimized) .unwrap(); assert_eq!(optimized_numbering.total_dofs, 18); } #[test] fn test_boundary_conditions() { let mesh = Mesh::generate_rectangle(1.0, 1.0, 2, 2).unwrap(); let mut boundary_conditions = BoundaryConditionSet::new(); // Get some nodes let node_ids: Vec<_> = mesh.nodes.keys().take(2).copied().collect(); let dirichlet_bc = DirichletBC::fixed_support(vec![node_ids[0]]); let neumann_bc = NeumannBC::fixed_force(vec![node_ids[1]], vec![DofComponent::DisplacementX], 100.0); boundary_conditions.add_condition(rtx_fea::boundary::BoundaryCondition::Dirichlet( dirichlet_bc, )); boundary_conditions.add_condition(rtx_fea::boundary::BoundaryCondition::Neumann(neumann_bc)); let stats = boundary_conditions.statistics(); assert_eq!(stats.total_conditions, 2); assert_eq!(stats.dirichlet_count, 1); assert_eq!(stats.neumann_count, 1); let validation = boundary_conditions.validate(&mesh).unwrap(); assert!(validation.is_valid); } #[test] fn test_solvers() { use nalgebra::DVector; use rtx_fea::assembly::SparseMatrix; // Create a simple 3x3 SPD matrix let mut matrix = SparseMatrix::new(3, 3); matrix.add_entry(0, 0, 4.0).unwrap(); matrix.add_entry(0, 1, 2.0).unwrap(); matrix.add_entry(1, 0, 2.0).unwrap(); matrix.add_entry(1, 1, 5.0).unwrap(); matrix.add_entry(1, 2, 1.0).unwrap(); matrix.add_entry(2, 1, 1.0).unwrap(); matrix.add_entry(2, 2, 3.0).unwrap(); matrix.finalize().unwrap(); let rhs = DVector::from_vec(vec![8.0, 13.0, 7.0]); let options = rtx_fea::solvers::SolverOptions::default(); // Test Cholesky solver let mut cholesky_solver = CholeskyDirect::new(); let result = cholesky_solver.solve(&matrix, &rhs, &options); assert!(result.is_ok()); let (solution, info) = result.unwrap(); assert_eq!(solution.len(), 3); assert!(info.converged); // Test LU solver let mut lu_solver = LuDirect::new(); let result = lu_solver.solve(&matrix, &rhs, &options); assert!(result.is_ok()); } #[test] fn test_sparse_matrix_operations() { use nalgebra::DVector; use rtx_fea::assembly::SparseMatrix; let mut matrix = SparseMatrix::new(4, 4); // Add some entries matrix.add_entry(0, 0, 1.0).unwrap(); matrix.add_entry(1, 1, 2.0).unwrap(); matrix.add_entry(2, 2, 3.0).unwrap(); matrix.add_entry(3, 3, 4.0).unwrap(); matrix.add_entry(0, 1, 0.5).unwrap(); matrix.add_entry(1, 0, 0.5).unwrap(); matrix.finalize().unwrap(); assert_eq!(matrix.nnz(), 6); // Note: is_finalized() method not exposed on SparseMatrix // assert!(matrix.is_finalized()); // Test matrix-vector multiplication let x = DVector::from_vec(vec![1.0, 1.0, 1.0, 1.0]); let result = matrix.multiply_vector(&x); assert!(result.is_ok()); let y = result.unwrap(); assert_eq!(y.len(), 4); assert_eq!(y[0], 1.5); // 1.0*1.0 + 0.5*1.0 assert_eq!(y[1], 2.5); // 0.5*1.0 + 2.0*1.0 assert_eq!(y[2], 3.0); // 3.0*1.0 assert_eq!(y[3], 4.0); // 4.0*1.0 } #[test] fn test_mesh_validation() { let mesh = Mesh::generate_rectangle(1.0, 1.0, 3, 3).unwrap(); // Note: validate() and statistics() methods not yet on Mesh, but on MeshStatistics // let validation_result = mesh.validate(); // assert!(validation_result.is_ok()); // Basic checks instead assert_eq!(mesh.num_nodes(), 9); assert_eq!(mesh.num_elements(), 4); assert_eq!(mesh.spatial_dimension, 2); } #[test] fn test_gpu_context() { use rtx_fea::kernels::{GpuContext, GpuUtils}; // Test GPU availability check let has_gpu = GpuUtils::is_cuda_available(); let device_count = GpuUtils::device_count(); // These should not panic assert!(device_count <= 16); // Reasonable upper bound if has_gpu { let context_result = GpuContext::new(); // If CUDA is available, context creation should succeed if context_result.is_ok() { let context = context_result.unwrap(); assert!(context.properties.max_threads_per_block > 0); } } } #[test] fn test_coordinate_transforms() { use nalgebra::Vector3; use rtx_fea::utils::CoordinateTransforms; let cartesian = Vector3::new(1.0, 1.0, 1.0); let cylindrical = CoordinateTransforms::cartesian_to_cylindrical(&cartesian); let back_to_cartesian = CoordinateTransforms::cylindrical_to_cartesian(&cylindrical); let error = (cartesian - back_to_cartesian).norm(); assert!( error < 1e-10, "Coordinate transformation round-trip error too large" ); } #[test] fn test_memory_utils() { use rtx_fea::utils::MemoryUtils; let formatted_1mb = MemoryUtils::format_bytes(1024 * 1024); assert_eq!(formatted_1mb, "1.00 MB"); let formatted_1gb = MemoryUtils::format_bytes(1024 * 1024 * 1024); assert_eq!(formatted_1gb, "1.00 GB"); let sparse_memory = MemoryUtils::estimate_sparse_matrix_memory(1000, 4, 8); assert!(sparse_memory > 0); let dense_memory = MemoryUtils::estimate_dense_matrix_memory(100, 100, 8); assert_eq!(dense_memory, 80000); // 100*100*8 bytes } #[test] fn test_analysis_config() { let config = AnalysisConfig::default(); assert_eq!(config.name, "FEA Analysis"); assert!(config.performance_options.use_gpu); assert!(config.output_options.write_displacements); } #[test] fn test_element_matrices() { // Note: ElementMatrices is not publicly exported yet // This test is disabled until the type is made public // Test that we can create basic matrices for elements let stiffness = nalgebra::DMatrix::::zeros(8, 8); let mass = nalgebra::DMatrix::::zeros(8, 8); let force = nalgebra::DVector::::zeros(8); assert_eq!(stiffness.nrows(), 8); assert_eq!(stiffness.ncols(), 8); assert_eq!(mass.nrows(), 8); assert_eq!(force.len(), 8); } // Performance benchmark test #[test] fn test_performance_benchmark() { use rtx_fea::utils::BenchmarkUtils; use std::time::Duration; // Benchmark a simple operation let stats = BenchmarkUtils::benchmark_function( || { // Simple computation let _result: f64 = (0..1000).map(|i| (i as f64).sin()).sum(); }, 5, ); assert_eq!(stats.iterations, 5); assert!(stats.mean > Duration::new(0, 0)); assert!(stats.min <= stats.mean); assert!(stats.max >= stats.mean); } #[test] fn test_library_info() { use rtx_fea::info; assert!(!info::version().is_empty()); assert_eq!(info::name(), "rtx-fea"); let build_info = info::build_info(); assert!(!build_info.version.is_empty()); assert!(!build_info.element_types.is_empty()); let supported_elements = info::supported_elements(); assert!(supported_elements.contains(&ElementType::Tri3)); assert!(supported_elements.contains(&ElementType::Quad4)); }