Files
rustytorch/crates/specialized/rtx-cfd/tests/refinement_tests.rs
T
Omar SobhandClaude Opus 5 cca29aac8f
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
CI / Format Check (push) Canceled after 0s
rtx-fea: repair the eigensolver, and stop the suite lying about the rest
Lifts the 27 `#[ignore]` markers on rtx-cfd and rtx-fea. 21 of them fail;
6 were stale, marking components that have since been implemented. The
suite now reports the truth, which means it is red.

The eigensolver had three independent defects, each individually fatal.
Found by writing closed-form tests first and confirming red:

  - The generalized reduction formed M^-1 K and ran Lanczos on it.
    M^-1 K has the right eigenvalues but is not symmetric even when K
    and M both are, and Lanczos assumes symmetry -- so it returned a
    wrong answer rather than an inaccurate one. On a 2-DOF spring-mass
    chain with M = diag(2,1) it gave 1.633 against an exact root of
    1 - sqrt(2)/2 ~= 0.293. Replaced with the Cholesky reduction
    B = L^-1 (K - sigma M) L^-T.

  - Output was unsorted. nalgebra's symmetric_eigen gives no ordering
    guarantee and none was imposed; modal analysis names modes by index,
    so the ordering is part of the contract.

  - Eigenvectors could not be transformed back out of the Krylov basis.
    The Lanczos block was (n x num_iter) and the tridiagonal
    eigenvectors (min(num_iter, k) x k); whenever those differed the
    multiply panicked on a dimension mismatch -- that is, on every
    problem with more DOFs than requested modes, which is every real
    modal analysis.

Lanczos now runs shift-invert by default. Plain Lanczos converges to the
eigenvalues of largest magnitude and modal analysis wants the lowest, so
without it the solver returns the modes nobody asked for. Also switched
to full reorthogonalization, twice per step, so converged eigenvalues do
not reappear as ghosts indistinguishable from genuine repeated roots.

ModalResults computed f = sqrt(lambda / 2pi) instead of
sqrt(lambda) / 2pi. The two agree only at lambda = 2pi, so a smoke test
asserting a positive frequency would never separate them. A
`#[cfg(disabled)]` module in the same file asserted the correct formula
-- the module was disabled rather than the bug fixed. That module is
removed; tests/eigenvalue_closed_form.rs supersedes it with every
expected value derived analytically.

Corrected a fixture rather than loosening its tolerance:
implementation_tests expected the smallest eigenvalue of
tridiag(-1, 4, -1) at order 3 to be 4 - 2 sqrt(2) ~= 1.172. The
eigenvalues of tridiag(c, a, c) are a + 2c cos(k pi / (n+1)), so the
true value is 4 - sqrt(2) ~= 2.586. The test had been quarantined for
failing to match an expectation that was never right.

rtx-fsi is untouched and stays 26/26.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 07:46:01 -07:00

183 lines
6.0 KiB
Rust

// TDD: RED phase - Tests for mesh refinement algorithms
use nalgebra::Vector3;
use rtx_cfd::mesh::Mesh;
use rtx_cfd::mesh::refinement::{AdaptiveRefinement, RefinementCriteria, RefinementStrategy};
use rtx_cfd::mesh::structured::StructuredMesh;
use rtx_cfd::mesh::unstructured::UnstructuredMesh;
#[test]
fn test_adaptive_refinement_criteria_validation() {
let criteria = RefinementCriteria {
max_error: 1e-3,
min_cell_size: 1e-6,
max_cell_size: 1e6,
max_levels: 10,
};
let refinement = AdaptiveRefinement::new(criteria);
// Test refinement decision logic
assert!(refinement.needs_refinement(1e-2, 1e-3, 5)); // High error, reasonable size, low level
assert!(!refinement.needs_refinement(1e-4, 1e-3, 5)); // Low error
assert!(!refinement.needs_refinement(1e-2, 1e-7, 5)); // Too small cell
assert!(!refinement.needs_refinement(1e-2, 1e-3, 15)); // Too many levels
}
#[test]
fn test_error_based_cell_marking() {
let criteria = RefinementCriteria::default();
let refinement = AdaptiveRefinement::new(criteria);
// Test with various error distributions
let errors = vec![1e-2, 1e-4, 5e-3, 1e-5, 2e-3, 1e-6];
let cell_sizes = vec![1e-3, 1e-3, 1e-3, 1e-3, 1e-3, 1e-3];
let levels = vec![2, 2, 2, 2, 2, 2];
let marked_cells = refinement
.mark_cells_for_refinement(&errors, &cell_sizes, &levels)
.unwrap();
// Should mark cells 0, 2, 4 (indices with errors > 1e-3)
let expected_marked: Vec<usize> = vec![0, 2, 4];
assert_eq!(marked_cells, expected_marked);
}
#[test]
fn test_gradient_based_error_indicator() {
let criteria = RefinementCriteria::default();
let refinement = AdaptiveRefinement::new(criteria);
// Create a simple 2D velocity field with gradients
let velocity_field = vec![
Vector3::new(0.0, 0.0, 0.0), // Cell 0: no gradient
Vector3::new(1.0, 0.0, 0.0), // Cell 1: moderate gradient
Vector3::new(2.0, 1.0, 0.0), // Cell 2: high gradient
Vector3::new(0.1, 0.1, 0.0), // Cell 3: low gradient
];
let errors = refinement
.compute_gradient_error_indicator(&velocity_field)
.unwrap();
// Cell 2 should have highest error (highest velocity magnitude)
assert!(errors[2] > errors[1]);
assert!(errors[1] > errors[0]);
assert!(errors[1] > errors[3]);
}
#[test]
fn test_residual_based_error_indicator() {
let criteria = RefinementCriteria::default();
let refinement = AdaptiveRefinement::new(criteria);
// Mock residuals for momentum and continuity equations
let momentum_residuals = vec![1e-3, 1e-2, 5e-3, 1e-4];
let continuity_residuals = vec![1e-4, 1e-3, 2e-3, 1e-5];
let errors = refinement
.compute_residual_error_indicator(&momentum_residuals, &continuity_residuals)
.unwrap();
// Cell 1 should have highest combined residual
assert!(errors[1] > errors[2]);
assert!(errors[2] > errors[0]);
assert!(errors[0] > errors[3]);
}
#[test]
fn test_structured_mesh_refinement() {
let mut mesh = StructuredMesh::new(3, 3, 1.0, 1.0).unwrap();
let initial_cell_count = mesh.cell_count();
// Test uniform refinement
mesh.refine().unwrap();
// Should quadruple the number of cells in 2D
assert_eq!(mesh.cell_count(), initial_cell_count * 4);
// Grid spacing should be halved
assert!((mesh.dx() - 0.25).abs() < 1e-10);
assert!((mesh.dy() - 0.25).abs() < 1e-10);
}
#[test]
fn test_unstructured_mesh_cell_subdivision() {
let mut mesh = UnstructuredMesh::new();
// Create a simple triangle
let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap();
let n2 = mesh.add_node(Vector3::new(1.0, 0.0, 0.0)).unwrap();
let n3 = mesh.add_node(Vector3::new(0.5, 1.0, 0.0)).unwrap();
let cell_id = mesh.add_triangle_cell(n1, n2, n3).unwrap();
let initial_cell_count = mesh.cell_count();
// Mark this cell for refinement and subdivide
let cells_to_refine = vec![cell_id];
mesh.refine_cells(&cells_to_refine).unwrap();
// Triangle subdivision should create 4 triangles
assert_eq!(mesh.cell_count(), initial_cell_count + 3); // 1 original -> 4 total, so +3
}
#[test]
fn test_hanging_node_consistency() {
let mut mesh = UnstructuredMesh::new();
// Create two adjacent triangles
let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap();
let n2 = mesh.add_node(Vector3::new(1.0, 0.0, 0.0)).unwrap();
let n3 = mesh.add_node(Vector3::new(0.5, 1.0, 0.0)).unwrap();
let n4 = mesh.add_node(Vector3::new(1.5, 1.0, 0.0)).unwrap();
let cell1 = mesh.add_triangle_cell(n1, n2, n3).unwrap();
let cell2 = mesh.add_triangle_cell(n2, n4, n3).unwrap();
// Refine only the first cell
mesh.refine_cells(&vec![cell1]).unwrap();
// Should handle hanging nodes correctly
assert!(mesh.validate().is_ok());
}
#[test]
fn test_adaptive_refinement_quality_metrics() {
let mut mesh = StructuredMesh::new(4, 4, 2.0, 2.0).unwrap();
// Get initial quality metrics
let initial_stats = mesh.statistics();
let initial_aspect_ratio = initial_stats.aspect_ratio;
// Refine mesh
mesh.refine().unwrap();
// Quality should be maintained or improved
let refined_stats = mesh.statistics();
assert!(refined_stats.aspect_ratio <= initial_aspect_ratio * 1.1); // Allow small degradation
}
#[test]
fn test_refinement_level_tracking() {
let criteria = RefinementCriteria {
max_levels: 3,
..Default::default()
};
let refinement = AdaptiveRefinement::new(criteria);
// Test level enforcement
let errors = vec![1e-2; 10]; // All high error
let cell_sizes = vec![1e-3; 10]; // All reasonable size
let levels = vec![0, 1, 2, 3, 4, 0, 1, 2, 3, 4]; // Mixed levels
let marked_cells = refinement
.mark_cells_for_refinement(&errors, &cell_sizes, &levels)
.unwrap();
// Should not mark cells with level >= max_levels (indices 3, 4, 8, 9)
assert!(!marked_cells.contains(&3));
assert!(!marked_cells.contains(&4));
assert!(!marked_cells.contains(&8));
assert!(!marked_cells.contains(&9));
}