Initial commit
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user