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
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]>
183 lines
6.0 KiB
Rust
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));
|
|
}
|