rtx-fea: build the missing mesh-generation APIs and re-enable 8 test modules
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

The largest cluster of the 128 compile errors behind the disabled test
modules was one missing API family. Now built, each with invariant tests
a plausible-wrong mesh fails:

- Rectangle::generate_quad_mesh / generate_tri_mesh — structured grids,
  CCW elements, exact area sums asserted
- Circle::generate_tri_mesh — centre fan plus ring bands; tiles the
  inscribed polygon exactly
- Box3D::generate_hex_mesh / generate_tet_mesh — the tet split is the
  Kuhn/Freudenthal 6-tet subdivision, conforming across cells, positive
  volumes summing exactly to the box
- Sphere::generate_tet_mesh — concentric UV shells, centre fan, prisms
  split by the Dompierre smallest-index diagonal rule so neighbouring
  prisms agree; conformity and closed-boundary asserted via face counting
- Mesh::validate — empty/inconsistent/orphan checks plus signed-area
  orientation for planar Tri3/Quad4, which is what an inverted
  connectivity fails
- Mesh::find_boundary_edges / find_boundary_faces / calculate_edge_normal,
  Node::distance_to / with_label

Re-enabling the tests found a real defect: geometry::Face derived
order-sensitive PartialEq/Hash, so the same face listed by two adjacent
elements (different start node, opposite winding) never compared equal.
A 2x2x2 hex mesh reported 32 boundary faces instead of 24 — and
find_boundary_nodes in 3-D and the 3-D surface-area statistic sit on the
same counting. Face identity is now canonical (sorted ids; quads keep
their diagonal pairing).

Partitioning: the fixtures targeted an instance API that never existed —
MeshPartitioner::partition is an associated function. Two real gaps fixed:
interface_elements was never populated, and requesting more partitions
than elements produced useless empty partitions (now clamps).

Fixtures corrected rather than the code where they encoded abandoned
designs: global DOF numbers on nodes (DofMap's job), element
thickness/property bags nothing reads, a 0-to-1 quality score that never
existed, and a clockwise sliver that validate now rightly rejects. The
GPU data conversion test stays disabled with the GPU solver tranche.

Lib tests 72 -> 117, stable across 5 runs, all integration suites green.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-19 18:29:55 -07:00
co-authored by Claude Fable 5
parent 1a740e0b2c
commit 8495a690d9
9 changed files with 1325 additions and 243 deletions
@@ -9,7 +9,14 @@ pub mod partitioning_types;
pub use partitioning_algorithms::*;
pub use partitioning_types::*;
#[cfg(disabled)]
// Fixture corrections on re-enable (2026-08-19): the original tests were
// written against an instance API (`MeshPartitioner::new()`, `set_direction`)
// that never existed — `MeshPartitioner::partition` is an associated function
// returning a `PartitioningResult`, and the direction is a parameter of
// `coordinate_partitioning`. Re-enabling also surfaced two real gaps, both
// fixed in the algorithms: `interface_elements` was never populated, and
// requesting more partitions than elements produced empty partitions.
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::{MaterialId, geometry::Rectangle};
@@ -19,14 +26,17 @@ mod tests {
let rect = Rectangle::new(1.0, 1.0);
let mesh = rect.generate_quad_mesh(2, 2, MaterialId(0)).unwrap();
let partitioner = MeshPartitioner::new();
let partitions = partitioner
.partition(&mesh, 1, PartitioningStrategy::Coordinate)
.unwrap();
let result =
MeshPartitioner::partition(&mesh, 1, PartitioningStrategy::Coordinate).unwrap();
let partitions = &result.partitions;
assert_eq!(partitions.len(), 1);
assert_eq!(partitions[0].element_count(), 4);
assert_eq!(partitions[0].node_count(), 9);
// A single partition shares nothing.
assert_eq!(partitions[0].internal_node_count(), 9);
assert_eq!(partitions[0].boundary_node_count(), 0);
assert!(partitions[0].neighbors.is_empty());
}
#[test]
@@ -34,15 +44,23 @@ mod tests {
let rect = Rectangle::new(2.0, 1.0);
let mesh = rect.generate_quad_mesh(4, 2, MaterialId(0)).unwrap();
let partitioner = MeshPartitioner::new();
let partitions = partitioner
.partition(&mesh, 2, PartitioningStrategy::Coordinate)
.unwrap();
let result =
MeshPartitioner::partition(&mesh, 2, PartitioningStrategy::Coordinate).unwrap();
let partitions = &result.partitions;
// 8 elements split by centroid coordinate: exactly 4 and 4, and every
// element lands in exactly one partition.
assert_eq!(partitions.len(), 2);
// Each partition should have roughly half the elements
assert!(partitions[0].element_count() > 0);
assert!(partitions[1].element_count() > 0);
assert_eq!(partitions[0].element_count(), 4);
assert_eq!(partitions[1].element_count(), 4);
assert_eq!(result.stats.total_elements, 8);
for element_id in mesh.elements.keys() {
let owners = partitions
.iter()
.filter(|p| p.contains_element(element_id))
.count();
assert_eq!(owners, 1);
}
}
#[test]
@@ -50,14 +68,15 @@ mod tests {
let rect = Rectangle::new(3.0, 3.0);
let mesh = rect.generate_quad_mesh(6, 6, MaterialId(0)).unwrap();
let partitioner = MeshPartitioner::new();
let partitions = partitioner
.partition(&mesh, 4, PartitioningStrategy::LoadBalanced)
.unwrap();
let result =
MeshPartitioner::partition(&mesh, 4, PartitioningStrategy::LoadBalanced).unwrap();
let stats = PartitioningStats::from_partitions(&partitions);
assert!(stats.is_well_balanced());
assert!(stats.load_imbalance < 0.2);
// 36 identical elements over 4 partitions must balance exactly.
assert!(result.stats.is_well_balanced());
assert!(result.stats.load_imbalance < 1e-12);
for partition in &result.partitions {
assert_eq!(partition.element_count(), 9);
}
}
#[test]
@@ -65,15 +84,19 @@ mod tests {
let rect = Rectangle::new(1.0, 1.0);
let mesh = rect.generate_quad_mesh(2, 2, MaterialId(0)).unwrap();
let partitioner = MeshPartitioner::new();
let partitions = partitioner
.partition(&mesh, 2, PartitioningStrategy::Coordinate)
.unwrap();
let result =
MeshPartitioner::partition(&mesh, 2, PartitioningStrategy::Coordinate).unwrap();
// Check that boundary nodes are correctly identified
for partition in &partitions {
assert!(!partition.boundary_nodes.is_empty());
assert!(!partition.internal_nodes.is_empty());
// Two halves of a 2×2 grid share exactly the 3 nodes of the cut line,
// and a node is internal exactly when it is not shared.
for partition in &result.partitions {
assert_eq!(partition.boundary_node_count(), 3);
assert_eq!(partition.internal_node_count(), 3);
for &node_id in &partition.nodes {
assert!(
partition.is_boundary_node(&node_id) != partition.is_internal_node(&node_id)
);
}
}
}
@@ -82,14 +105,12 @@ mod tests {
let rect = Rectangle::new(2.0, 2.0);
let mesh = rect.generate_quad_mesh(4, 4, MaterialId(0)).unwrap();
let partitioner = MeshPartitioner::new();
let partitions = partitioner
.partition(&mesh, 4, PartitioningStrategy::Graph)
.unwrap();
let result = MeshPartitioner::partition(&mesh, 4, PartitioningStrategy::Graph).unwrap();
let stats = &result.stats;
let stats = PartitioningStats::from_partitions(&partitions);
assert_eq!(stats.num_partitions, 4);
assert_eq!(stats.total_elements, 16);
assert_eq!(stats.total_nodes, 25);
assert!(stats.efficiency() > 0.5);
}
@@ -98,38 +119,36 @@ mod tests {
let rect = Rectangle::new(0.5, 0.5);
let mesh = rect.generate_quad_mesh(2, 2, MaterialId(0)).unwrap();
let partitioner = MeshPartitioner::new();
// Request more partitions than elements
let partitions = partitioner
.partition(&mesh, 8, PartitioningStrategy::LoadBalanced)
.unwrap();
// Request more partitions than elements: an empty partition can do no
// work, so the count clamps to the number of elements.
let result =
MeshPartitioner::partition(&mesh, 8, PartitioningStrategy::LoadBalanced).unwrap();
// Should create only as many partitions as there are elements
assert_eq!(partitions.len(), 4);
for partition in &partitions {
assert_eq!(result.partitions.len(), 4);
for partition in &result.partitions {
assert_eq!(partition.element_count(), 1);
}
}
#[test]
fn test_partition_direction_auto() {
// Create a mesh that's wider than tall
// A mesh wider than tall: Auto must cut across X, splitting the 8
// element columns into two equal halves.
let rect = Rectangle::new(4.0, 1.0);
let mesh = rect.generate_quad_mesh(8, 2, MaterialId(0)).unwrap();
let partitioner = MeshPartitioner::new();
partitioner.set_direction(PartitionDirection::Auto);
let result =
MeshPartitioner::coordinate_partitioning(&mesh, 2, PartitionDirection::Auto).unwrap();
let partitions = &result.partitions;
let partitions = partitioner
.partition(&mesh, 2, PartitioningStrategy::Coordinate)
.unwrap();
// Auto should choose X direction for this wide mesh
assert_eq!(partitions.len(), 2);
// Elements should be divided roughly equally
let diff =
(partitions[0].element_count() as i32 - partitions[1].element_count() as i32).abs();
assert!(diff <= 2);
assert_eq!(partitions[0].element_count(), 8);
assert_eq!(partitions[1].element_count(), 8);
// An X cut of the 9×3 node grid shares one 3-node column; a Y cut
// would share a 9-node row, so the boundary size pins the direction.
assert_eq!(partitions[0].boundary_node_count(), 3);
assert_eq!(partitions[1].boundary_node_count(), 3);
}
#[test]
@@ -137,14 +156,23 @@ mod tests {
let rect = Rectangle::new(1.0, 1.0);
let mesh = rect.generate_quad_mesh(3, 3, MaterialId(0)).unwrap();
let partitioner = MeshPartitioner::new();
let partitions = partitioner
.partition(&mesh, 2, PartitioningStrategy::Graph)
.unwrap();
let result = MeshPartitioner::partition(&mesh, 2, PartitioningStrategy::Graph).unwrap();
// Check that interface elements are correctly identified
let total_interface: usize = partitions.iter().map(|p| p.interface_elements.len()).sum();
assert!(total_interface > 0);
// Interface elements touch a shared node; on a mesh this small every
// partition must have some, and each interface element must actually
// contain a boundary node of its partition.
for partition in &result.partitions {
assert!(!partition.interface_elements.is_empty());
for element_id in &partition.interface_elements {
let element = mesh.get_element(*element_id).unwrap();
assert!(
element
.nodes
.iter()
.any(|node_id| partition.is_boundary_node(node_id))
);
}
}
}
#[test]
@@ -152,16 +180,26 @@ mod tests {
let rect = Rectangle::new(2.0, 2.0);
let mesh = rect.generate_quad_mesh(4, 4, MaterialId(0)).unwrap();
let partitioner = MeshPartitioner::new();
let partitions = partitioner
.partition(&mesh, 4, PartitioningStrategy::Coordinate)
.unwrap();
let result =
MeshPartitioner::partition(&mesh, 4, PartitioningStrategy::Coordinate).unwrap();
// Each partition should have neighbors
for partition in &partitions {
if partitions.len() > 1 {
assert!(!partition.neighbors.is_empty());
// Neighbouring is symmetric and every partition of a connected mesh
// has at least one neighbour.
for partition in &result.partitions {
assert!(!partition.neighbors.is_empty());
for &other in &partition.neighbors {
assert!(result.partitions[other].neighbors.contains(&partition.id));
}
}
}
#[test]
fn test_degenerate_partition_requests() {
let rect = Rectangle::new(1.0, 1.0);
let mesh = rect.generate_quad_mesh(2, 2, MaterialId(0)).unwrap();
assert!(MeshPartitioner::partition(&mesh, 0, PartitioningStrategy::Coordinate).is_err());
let empty = crate::mesh::Mesh::new(2).unwrap();
assert!(MeshPartitioner::partition(&empty, 2, PartitioningStrategy::Coordinate).is_err());
}
}
@@ -36,6 +36,14 @@ impl MeshPartitioner {
.into());
}
if mesh.num_elements() == 0 {
return Err(MeshError::EmptyMesh.into());
}
// An empty partition cannot do any work, so never create more
// partitions than there are elements.
let num_partitions = num_partitions.min(mesh.num_elements());
if num_partitions == 1 {
return Self::create_single_partition(mesh);
}
@@ -478,6 +486,25 @@ impl MeshPartitioner {
}
}
// Interface elements: elements touching a node shared with another
// partition. These are the ones whose contributions must be exchanged.
for partition in partitions.iter_mut() {
let interface: Vec<ElementId> = partition
.elements
.iter()
.copied()
.filter(|&element_id| {
mesh.get_element(element_id).is_some_and(|element| {
element
.nodes
.iter()
.any(|node_id| partition.boundary_nodes.contains(node_id))
})
})
.collect();
partition.interface_elements.extend(interface);
}
Ok(())
}