CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
Follows the assembly repair. Takes rtx-fea from 21 failures to 253 passing,
0 failing, 0 ignored, with every `#[ignore]` marker gone.
Shape function bugs, all found by one new test asserting two invariants
across the whole element library at once -- partition of unity, and that
the hand-written derivatives sum to zero. The second is the one that gets
skipped, and it is what caught Hexahedron20.
- Wedge15 summed to 2 at mid-height. Adding a node on a vertical edge
contributes L_i (1 - t^2) to the sum, so the two corners sharing that
edge must each give up half of it; the correction was absent. A
quadratic wedge that doubles every field interpolated through it.
- Hexahedron20 had sign errors in four hand-written corner
derivatives -- nodes 3 and 7 in dN/dr, nodes 1 and 5 in dN/ds. The
values were correct, so partition of unity passed; only the
derivative-sum invariant exposed it. The strain computed from this
element was wrong while its interpolation looked right.
- Quadrilateral9 emitted its shape functions in raw lexicographic
lattice order while Quad4 and Quad8 use the standard finite-element
order. A mesh written the usual way paired each node with the wrong
basis function, which at the element centre made the Jacobian exactly
singular.
- Pyramid13 was not a quadratic pyramid basis: it summed to 4 at the
element centre, and its `derivatives` allocated a 13x3 matrix then
wrote rows 13 through 15, having been copied from a sixteen-node
layout, so it panicked before the wrong values could be used. A
correct 13-node basis is rational, and there is no pyramid quadrature
rule to integrate it with, so implementing the basis alone would not
make the element usable. Both now report the gap explicitly rather
than panicking. Pyramid5 is unaffected and works.
Fixtures corrected rather than tolerances loosened:
- von Mises stress of an equal biaxial state expected 0, commented "no
deviatoric stress". Only a hydrostatic state has that. The correct
value is 100, and expecting 0 would mean a biaxially loaded sheet
could never yield. The unequal case expected |100-50|; the von Mises
stress is not a principal difference.
- A 3-point Gauss rule was required to integrate sin to 1e-10. No
correct implementation can. Replaced with a convergence assertion,
which a wrong rule cannot satisfy by luck.
- MathUtils::SMALL was asserted below EPSILON * 1000, which inverts the
relationship a practical zero-threshold needs.
- The Hex20 Jacobian test put all twelve mid-edge nodes at the origin,
commented "simplified for test". That is not a hexahedron, and its
mapping is genuinely singular; it only passed because of the
derivative sign errors above.
- ElementFactory was required to build every ElementType including
Point, which has no interpolation and is deliberately rejected.
MemoryInfo displayed decimal GB while its own test constructed binary
GiB, rendering an 8 GiB device as 8.59. Now GiB throughout.
test_mesh_has_real_algorithms searched the *text* of mesh/mod.rs for the
strings "add_node" and "add_element". It broke when those moved into
submodules, but the real problem is that a source-text search cannot tell
a working function from one returning zeros -- it passed throughout the
period when element matrices were a stub and quadrature returned no
points. Replaced with a test that builds a mesh and checks the result.
The crate doc example imported solvers::DirectSolver and
analysis::StaticAnalysis, neither of which has ever existed, so the
doctest never compiled. Replaced with a modal analysis that runs. Also
dropped the "Production Ready: No mocks, stubs, or TODOs - complete
implementation" line, and replaced it with what is actually validated and
what is not.
rtx-fsi unaffected at 26/26.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
350 lines
11 KiB
Rust
350 lines
11 KiB
Rust
// 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::<f64>::zeros(8, 8);
|
|
let mass = nalgebra::DMatrix::<f64>::zeros(8, 8);
|
|
let force = nalgebra::DVector::<f64>::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));
|
|
}
|