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
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:
co-authored by
Claude Fable 5
parent
1a740e0b2c
commit
8495a690d9
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user