// 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 { connectivity.boundary_nodes() } /// Find connected components in the mesh. pub fn find_connected_components(connectivity: &ConnectivityInfo) -> Vec> { 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(¤t_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>> { 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, elements: &indexmap::IndexMap, ) -> FeaResult> { 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, start: NodeId, end: NodeId, ) -> FeaResult>> { 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 { 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(¤t) { 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, } /// 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); } }