414 lines
13 KiB
Rust
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(¤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<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(¤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<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);
|
|
}
|
|
}
|