Files
rustytorch/crates/specialized/rtx-fea/src/mesh/topology/mod.rs
T
2026-03-04 00:08:42 +00:00

414 lines
13 KiB
Rust

// Copyright (c) 2024 RustyTorch++ Team
// Licensed under the Apache License, Version 2.0
//! Mesh topology operations and algorithms.
use crate::error::{FeaResult, MeshError};
use crate::mesh::{ElementId, NodeId, connectivity::ConnectivityInfo};
use indexmap::IndexSet;
use std::collections::{HashMap, HashSet};
// Declare submodules
pub mod topology_refinement;
pub mod topology_repair;
// Re-export submodules
pub use topology_refinement::MeshRefinement;
pub use topology_repair::TopologyRepair;
/// Topological mesh operations and queries.
pub struct TopologyOps;
impl TopologyOps {
/// Find all boundary nodes in the mesh.
pub fn find_boundary_nodes(connectivity: &ConnectivityInfo) -> IndexSet<NodeId> {
connectivity.boundary_nodes()
}
/// Find connected components in the mesh.
pub fn find_connected_components(connectivity: &ConnectivityInfo) -> Vec<IndexSet<ElementId>> {
let mut visited = HashSet::new();
let mut components = Vec::new();
for &element_id in connectivity.element_neighbors.keys() {
if visited.contains(&element_id) {
continue;
}
let mut component = IndexSet::new();
let mut stack = vec![element_id];
while let Some(current_element) = stack.pop() {
if visited.contains(&current_element) {
continue;
}
visited.insert(current_element);
component.insert(current_element);
// Add all neighbors to the stack
for &neighbor in connectivity.neighbors(current_element) {
if !visited.contains(&neighbor) {
stack.push(neighbor);
}
}
}
if !component.is_empty() {
components.push(component);
}
}
components
}
/// Check if the mesh is manifold (each edge connected to at most 2 elements).
pub fn is_manifold(connectivity: &ConnectivityInfo) -> bool {
connectivity
.edge_to_elements
.values()
.all(|elements| elements.len() <= 2)
}
/// Check if the mesh is closed (no boundary edges for 3D, no boundary in general).
pub fn is_closed(connectivity: &ConnectivityInfo) -> bool {
connectivity.boundary_edges.is_empty() && connectivity.boundary_faces.is_empty()
}
/// Compute the Euler characteristic for the mesh (V - E + F).
pub fn euler_characteristic(connectivity: &ConnectivityInfo) -> i32 {
let v = connectivity.node_to_elements.len() as i32;
let e = connectivity.edges.len() as i32;
let f = connectivity.faces.len() as i32;
v - e + f
}
/// Find holes in a 2D mesh using topological analysis.
pub fn find_holes_2d(connectivity: &ConnectivityInfo) -> FeaResult<Vec<Vec<NodeId>>> {
let mut holes = Vec::new();
let mut visited_edges = HashSet::new();
// Find all boundary edge loops
for &boundary_edge in &connectivity.boundary_edges {
if visited_edges.contains(&boundary_edge) {
continue;
}
let mut hole_nodes = Vec::new();
let mut current_edge = boundary_edge;
let mut current_node = current_edge.node1();
loop {
visited_edges.insert(current_edge);
hole_nodes.push(current_node);
// Find the next boundary edge connected to current_node
let next_node = if current_edge.node1() == current_node {
current_edge.node2()
} else {
current_edge.node1()
};
let mut found_next = false;
for &edge in &connectivity.boundary_edges {
if visited_edges.contains(&edge) {
continue;
}
if edge.node1() == next_node || edge.node2() == next_node {
current_edge = edge;
current_node = next_node;
found_next = true;
break;
}
}
if !found_next {
// We've completed the loop or hit a dead end
if hole_nodes.first() == Some(&next_node) {
// Closed loop
holes.push(hole_nodes);
}
break;
}
}
}
Ok(holes)
}
/// Check mesh consistency (various validation checks).
pub fn check_consistency(
connectivity: &ConnectivityInfo,
nodes: &indexmap::IndexMap<NodeId, crate::mesh::Node>,
elements: &indexmap::IndexMap<ElementId, crate::mesh::Element>,
) -> FeaResult<Vec<String>> {
let mut issues = Vec::new();
// Check for orphaned nodes
for &node_id in nodes.keys() {
if connectivity.elements_for_node(node_id).is_empty() {
issues.push(format!("Orphaned node: {node_id:?}"));
}
}
// Check for invalid element nodes
for (element_id, element) in elements {
for &node_id in &element.nodes {
if !nodes.contains_key(&node_id) {
issues.push(format!(
"Element {element_id:?} references non-existent node {node_id:?}"
));
}
}
// Check element node count
let expected_nodes = element.element_type.node_count();
if element.nodes.len() != expected_nodes {
issues.push(format!(
"Element {:?} has {} nodes, expected {}",
element_id,
element.nodes.len(),
expected_nodes
));
}
}
// Check for non-manifold edges in 3D meshes
for (edge, elements) in &connectivity.edge_to_elements {
if elements.len() > 2 {
issues.push(format!("Non-manifold edge: {edge:?}"));
}
}
Ok(issues)
}
/// Find shortest path between two nodes using Dijkstra's algorithm.
pub fn shortest_path(
connectivity: &ConnectivityInfo,
nodes: &indexmap::IndexMap<NodeId, crate::mesh::Node>,
start: NodeId,
end: NodeId,
) -> FeaResult<Option<Vec<NodeId>>> {
use std::collections::BinaryHeap;
#[derive(Copy, Clone)]
struct State {
cost: f64,
node: NodeId,
}
impl PartialEq for State {
fn eq(&self, other: &Self) -> bool {
self.node == other.node
}
}
impl Eq for State {}
impl PartialOrd for State {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for State {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
// Note: reversed for min-heap, use total_cmp for valid Ord
other.cost.total_cmp(&self.cost)
}
}
let mut dist = HashMap::new();
let mut heap = BinaryHeap::new();
let mut prev = HashMap::new();
// Initialize distances
dist.insert(start, 0.0);
heap.push(State {
cost: 0.0,
node: start,
});
while let Some(State {
cost: current_dist,
node,
}) = heap.pop()
{
if node == end {
// Reconstruct path
let mut path = Vec::new();
let mut current = end;
path.push(current);
while let Some(&prev_node) = prev.get(&current) {
path.push(prev_node);
current = prev_node;
}
path.reverse();
return Ok(Some(path));
}
if dist.get(&node).is_some_and(|&d| current_dist > d) {
continue;
}
// Get connected nodes through edges
for edge in &connectivity.edges {
let other = if edge.node1() == node {
edge.node2()
} else if edge.node2() == node {
edge.node1()
} else {
continue;
};
let node1 = nodes
.get(&node)
.ok_or_else(|| MeshError::NodeIndexOutOfBounds {
index: node.as_usize(),
max_index: nodes.len(),
})?;
let node2 = nodes
.get(&other)
.ok_or_else(|| MeshError::NodeIndexOutOfBounds {
index: other.as_usize(),
max_index: nodes.len(),
})?;
let edge_length = (&node1.coordinates - &node2.coordinates).norm();
let new_dist = current_dist + edge_length;
if dist.get(&other).is_none_or(|&d| new_dist < d) {
dist.insert(other, new_dist);
prev.insert(other, node);
heap.push(State {
cost: new_dist,
node: other,
});
}
}
}
Ok(None)
}
}
/// Check element orientation consistency.
pub struct OrientationCheck {
/// Whether all elements have consistent orientation
pub is_consistent: bool,
/// Number of elements with incorrect orientation
pub flipped_count: usize,
/// List of element IDs with incorrect orientation
pub flipped_elements: Vec<ElementId>,
}
/// Mesh topology statistics.
pub struct TopologyStatistics {
/// Number of connected components
pub num_components: usize,
/// Euler characteristic
pub euler_characteristic: i32,
/// Whether the mesh is manifold
pub is_manifold: bool,
/// Whether the mesh is closed
pub is_closed: bool,
/// Number of boundary nodes
pub num_boundary_nodes: usize,
/// Number of boundary edges
pub num_boundary_edges: usize,
/// Number of boundary faces
pub num_boundary_faces: usize,
}
impl std::fmt::Display for TopologyStatistics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Topology Statistics:")?;
writeln!(f, " Connected components: {}", self.num_components)?;
writeln!(f, " Euler characteristic: {}", self.euler_characteristic)?;
writeln!(f, " Is manifold: {}", self.is_manifold)?;
writeln!(f, " Is closed: {}", self.is_closed)?;
writeln!(f, " Boundary nodes: {}", self.num_boundary_nodes)?;
writeln!(f, " Boundary edges: {}", self.num_boundary_edges)?;
writeln!(f, " Boundary faces: {}", self.num_boundary_faces)?;
Ok(())
}
}
impl TopologyStatistics {
/// Compute statistics for a mesh.
pub fn compute(connectivity: &ConnectivityInfo) -> Self {
let components = TopologyOps::find_connected_components(connectivity);
Self {
num_components: components.len(),
euler_characteristic: TopologyOps::euler_characteristic(connectivity),
is_manifold: TopologyOps::is_manifold(connectivity),
is_closed: TopologyOps::is_closed(connectivity),
num_boundary_nodes: connectivity.boundary_nodes().len(),
num_boundary_edges: connectivity.boundary_edges.len(),
num_boundary_faces: connectivity.boundary_faces.len(),
}
}
}
#[cfg(disabled)]
mod tests {
use super::*;
use crate::mesh::{Edge, Element, ElementType, Face, Mesh, Node};
#[test]
fn test_connected_components() {
let mesh = Mesh::new(2).unwrap();
let connectivity = ConnectivityInfo::from_mesh(&mesh).unwrap();
let components = TopologyOps::find_connected_components(&connectivity);
assert_eq!(components.len(), 0); // Empty mesh has no components
}
#[test]
fn test_manifold_check() {
let connectivity = ConnectivityInfo {
node_to_elements: HashMap::new(),
element_neighbors: HashMap::new(),
edges: IndexSet::new(),
edge_to_elements: HashMap::new(),
boundary_edges: IndexSet::new(),
faces: IndexSet::new(),
face_to_elements: HashMap::new(),
boundary_faces: IndexSet::new(),
};
assert!(TopologyOps::is_manifold(&connectivity));
}
#[test]
fn test_euler_characteristic() {
let connectivity = ConnectivityInfo {
node_to_elements: HashMap::new(),
element_neighbors: HashMap::new(),
edges: IndexSet::new(),
edge_to_elements: HashMap::new(),
boundary_edges: IndexSet::new(),
faces: IndexSet::new(),
face_to_elements: HashMap::new(),
boundary_faces: IndexSet::new(),
};
assert_eq!(TopologyOps::euler_characteristic(&connectivity), 0);
}
#[test]
fn test_topology_statistics() {
let mesh = Mesh::new(3).unwrap();
let connectivity = ConnectivityInfo::from_mesh(&mesh).unwrap();
let stats = TopologyStatistics::compute(&connectivity);
assert_eq!(stats.num_components, 0);
assert!(stats.is_manifold);
assert!(stats.is_closed);
}
}