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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
// Copyright (c) 2024 RustyTorch++ Team
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
//! Mesh refinement operations.
|
||||
|
||||
use crate::error::{FeaResult, MeshError};
|
||||
use crate::mesh::{Element, ElementType, Node, NodeId};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Mesh refinement operations.
|
||||
pub struct MeshRefinement;
|
||||
|
||||
impl MeshRefinement {
|
||||
/// Uniform refinement by subdividing all elements.
|
||||
pub fn uniform_refinement(mesh: &crate::mesh::Mesh) -> FeaResult<crate::mesh::Mesh> {
|
||||
let mut refined_mesh = crate::mesh::Mesh::new(mesh.spatial_dimension)?;
|
||||
|
||||
// Copy existing nodes first
|
||||
let mut node_mapping = HashMap::new();
|
||||
for (node_id, node) in &mesh.nodes {
|
||||
let new_node = Node {
|
||||
coordinates: node.coordinates.clone(),
|
||||
dofs: node.dofs.clone(),
|
||||
label: node.label.clone(),
|
||||
};
|
||||
let new_id = refined_mesh.add_node(new_node);
|
||||
node_mapping.insert(*node_id, new_id);
|
||||
}
|
||||
|
||||
// Track edge midpoints to avoid duplication
|
||||
let mut edge_midpoints: HashMap<(NodeId, NodeId), NodeId> = HashMap::new();
|
||||
|
||||
// Helper to get or create edge midpoint
|
||||
let get_edge_midpoint = |n1: NodeId,
|
||||
n2: NodeId,
|
||||
edge_midpoints: &mut HashMap<(NodeId, NodeId), NodeId>,
|
||||
refined_mesh: &mut crate::mesh::Mesh,
|
||||
mesh: &crate::mesh::Mesh|
|
||||
-> NodeId {
|
||||
let edge_key = if n1 < n2 { (n1, n2) } else { (n2, n1) };
|
||||
|
||||
if let Some(&mid_id) = edge_midpoints.get(&edge_key) {
|
||||
return mid_id;
|
||||
}
|
||||
|
||||
// Create midpoint node
|
||||
let node1 = &mesh.nodes[&n1];
|
||||
let node2 = &mesh.nodes[&n2];
|
||||
let midpoint_coords = (&node1.coordinates + &node2.coordinates) * 0.5;
|
||||
|
||||
let mid_node = Node {
|
||||
coordinates: midpoint_coords,
|
||||
dofs: node1.dofs.clone(), // Use same DOF types as parent nodes
|
||||
label: None,
|
||||
};
|
||||
|
||||
let mid_id = refined_mesh.add_node(mid_node);
|
||||
edge_midpoints.insert(edge_key, mid_id);
|
||||
mid_id
|
||||
};
|
||||
|
||||
// Process each element
|
||||
for element in mesh.elements.values() {
|
||||
match element.element_type {
|
||||
ElementType::Tri3 => {
|
||||
// Triangle subdivision into 4 triangles
|
||||
if element.nodes.len() != 3 {
|
||||
return Err(MeshError::InvalidElement(format!(
|
||||
"Triangle element requires 3 nodes, found {}",
|
||||
element.nodes.len()
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
let n0 = node_mapping[&element.nodes[0]];
|
||||
let n1 = node_mapping[&element.nodes[1]];
|
||||
let n2 = node_mapping[&element.nodes[2]];
|
||||
|
||||
// Create edge midpoints
|
||||
let m01 = get_edge_midpoint(
|
||||
element.nodes[0],
|
||||
element.nodes[1],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
let m12 = get_edge_midpoint(
|
||||
element.nodes[1],
|
||||
element.nodes[2],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
let m20 = get_edge_midpoint(
|
||||
element.nodes[2],
|
||||
element.nodes[0],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
|
||||
// Create 4 new triangles
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tri3,
|
||||
vec![n0, m01, m20],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tri3,
|
||||
vec![m01, n1, m12],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tri3,
|
||||
vec![m20, m12, n2],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tri3,
|
||||
vec![m01, m12, m20],
|
||||
element.material_id,
|
||||
)?);
|
||||
}
|
||||
|
||||
ElementType::Quad4 => {
|
||||
// Quad subdivision into 4 quads
|
||||
if element.nodes.len() != 4 {
|
||||
return Err(MeshError::InvalidElement(format!(
|
||||
"Quad element requires 4 nodes, found {}",
|
||||
element.nodes.len()
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
let n0 = node_mapping[&element.nodes[0]];
|
||||
let n1 = node_mapping[&element.nodes[1]];
|
||||
let n2 = node_mapping[&element.nodes[2]];
|
||||
let n3 = node_mapping[&element.nodes[3]];
|
||||
|
||||
// Create edge midpoints
|
||||
let m01 = get_edge_midpoint(
|
||||
element.nodes[0],
|
||||
element.nodes[1],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
let m12 = get_edge_midpoint(
|
||||
element.nodes[1],
|
||||
element.nodes[2],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
let m23 = get_edge_midpoint(
|
||||
element.nodes[2],
|
||||
element.nodes[3],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
let m30 = get_edge_midpoint(
|
||||
element.nodes[3],
|
||||
element.nodes[0],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
|
||||
// Create center point
|
||||
let center_coords = (&mesh.nodes[&element.nodes[0]].coordinates
|
||||
+ &mesh.nodes[&element.nodes[1]].coordinates
|
||||
+ &mesh.nodes[&element.nodes[2]].coordinates
|
||||
+ &mesh.nodes[&element.nodes[3]].coordinates)
|
||||
* 0.25;
|
||||
|
||||
let center_node = Node {
|
||||
coordinates: center_coords,
|
||||
dofs: mesh.nodes[&element.nodes[0]].dofs.clone(),
|
||||
label: None,
|
||||
};
|
||||
|
||||
let center = refined_mesh.add_node(center_node);
|
||||
|
||||
// Create 4 new quads
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Quad4,
|
||||
vec![n0, m01, center, m30],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Quad4,
|
||||
vec![m01, n1, m12, center],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Quad4,
|
||||
vec![center, m12, n2, m23],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Quad4,
|
||||
vec![m30, center, m23, n3],
|
||||
element.material_id,
|
||||
)?);
|
||||
}
|
||||
|
||||
ElementType::Tet4 => {
|
||||
// Tetrahedral subdivision into 8 tets (octahedral subdivision)
|
||||
if element.nodes.len() != 4 {
|
||||
return Err(MeshError::InvalidElement(format!(
|
||||
"Tetrahedral element requires 4 nodes, found {}",
|
||||
element.nodes.len()
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
let n0 = node_mapping[&element.nodes[0]];
|
||||
let n1 = node_mapping[&element.nodes[1]];
|
||||
let n2 = node_mapping[&element.nodes[2]];
|
||||
let n3 = node_mapping[&element.nodes[3]];
|
||||
|
||||
// Create all edge midpoints (6 edges for tet)
|
||||
let m01 = get_edge_midpoint(
|
||||
element.nodes[0],
|
||||
element.nodes[1],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
let m02 = get_edge_midpoint(
|
||||
element.nodes[0],
|
||||
element.nodes[2],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
let m03 = get_edge_midpoint(
|
||||
element.nodes[0],
|
||||
element.nodes[3],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
let m12 = get_edge_midpoint(
|
||||
element.nodes[1],
|
||||
element.nodes[2],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
let m13 = get_edge_midpoint(
|
||||
element.nodes[1],
|
||||
element.nodes[3],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
let m23 = get_edge_midpoint(
|
||||
element.nodes[2],
|
||||
element.nodes[3],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
|
||||
// Create 8 new tets - 4 corner tets and 4 from octahedron in center
|
||||
// Corner tets
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tet4,
|
||||
vec![n0, m01, m02, m03],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tet4,
|
||||
vec![m01, n1, m12, m13],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tet4,
|
||||
vec![m02, m12, n2, m23],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tet4,
|
||||
vec![m03, m13, m23, n3],
|
||||
element.material_id,
|
||||
)?);
|
||||
|
||||
// Octahedron subdivision into 4 tets
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tet4,
|
||||
vec![m01, m02, m03, m13],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tet4,
|
||||
vec![m01, m02, m12, m13],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tet4,
|
||||
vec![m02, m03, m13, m23],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tet4,
|
||||
vec![m02, m12, m13, m23],
|
||||
element.material_id,
|
||||
)?);
|
||||
}
|
||||
|
||||
ElementType::Hex8 => {
|
||||
// Hexahedral subdivision into 8 hexahedra
|
||||
if element.nodes.len() != 8 {
|
||||
return Err(MeshError::InvalidElement(format!(
|
||||
"Hexahedral element requires 8 nodes, found {}",
|
||||
element.nodes.len()
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
// Map original nodes
|
||||
let original_nodes: Vec<_> =
|
||||
element.nodes.iter().map(|&n| node_mapping[&n]).collect();
|
||||
|
||||
// Create all edge midpoints (12 edges for hex)
|
||||
let mut edge_mids = Vec::new();
|
||||
let edge_pairs = [
|
||||
(0, 1),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
(3, 0), // Bottom face
|
||||
(4, 5),
|
||||
(5, 6),
|
||||
(6, 7),
|
||||
(7, 4), // Top face
|
||||
(0, 4),
|
||||
(1, 5),
|
||||
(2, 6),
|
||||
(3, 7), // Vertical edges
|
||||
];
|
||||
|
||||
for &(i, j) in &edge_pairs {
|
||||
let mid = get_edge_midpoint(
|
||||
element.nodes[i],
|
||||
element.nodes[j],
|
||||
&mut edge_midpoints,
|
||||
&mut refined_mesh,
|
||||
mesh,
|
||||
);
|
||||
edge_mids.push(mid);
|
||||
}
|
||||
|
||||
// Create face centers (6 faces)
|
||||
let face_indices = [
|
||||
[0, 1, 2, 3], // Bottom
|
||||
[4, 5, 6, 7], // Top
|
||||
[0, 1, 5, 4], // Front
|
||||
[2, 3, 7, 6], // Back
|
||||
[0, 3, 7, 4], // Left
|
||||
[1, 2, 6, 5], // Right
|
||||
];
|
||||
|
||||
let mut face_centers = Vec::new();
|
||||
for face in &face_indices {
|
||||
let mut center = nalgebra::DVector::zeros(mesh.spatial_dimension);
|
||||
for &idx in face {
|
||||
center += &mesh.nodes[&element.nodes[idx]].coordinates;
|
||||
}
|
||||
center /= 4.0;
|
||||
|
||||
let face_node = Node {
|
||||
coordinates: center,
|
||||
dofs: mesh.nodes[&element.nodes[0]].dofs.clone(),
|
||||
label: None,
|
||||
};
|
||||
|
||||
face_centers.push(refined_mesh.add_node(face_node));
|
||||
}
|
||||
|
||||
// Create volume center
|
||||
let mut vol_center = nalgebra::DVector::zeros(mesh.spatial_dimension);
|
||||
for &node_id in &element.nodes {
|
||||
vol_center += &mesh.nodes[&node_id].coordinates;
|
||||
}
|
||||
vol_center /= 8.0;
|
||||
|
||||
let vol_center_node = Node {
|
||||
coordinates: vol_center,
|
||||
dofs: mesh.nodes[&element.nodes[0]].dofs.clone(),
|
||||
label: None,
|
||||
};
|
||||
|
||||
let vol_center_id = refined_mesh.add_node(vol_center_node);
|
||||
|
||||
// Create 8 new hexahedra
|
||||
// This is complex - each original vertex gets a new hex
|
||||
let new_hex_nodes = [
|
||||
// Hex at vertex 0
|
||||
vec![
|
||||
original_nodes[0],
|
||||
edge_mids[0],
|
||||
face_centers[0],
|
||||
edge_mids[3],
|
||||
edge_mids[8],
|
||||
face_centers[2],
|
||||
vol_center_id,
|
||||
face_centers[4],
|
||||
],
|
||||
// Hex at vertex 1
|
||||
vec![
|
||||
edge_mids[0],
|
||||
original_nodes[1],
|
||||
edge_mids[1],
|
||||
face_centers[0],
|
||||
face_centers[2],
|
||||
edge_mids[9],
|
||||
face_centers[5],
|
||||
vol_center_id,
|
||||
],
|
||||
// Hex at vertex 2
|
||||
vec![
|
||||
face_centers[0],
|
||||
edge_mids[1],
|
||||
original_nodes[2],
|
||||
edge_mids[2],
|
||||
vol_center_id,
|
||||
face_centers[5],
|
||||
edge_mids[10],
|
||||
face_centers[3],
|
||||
],
|
||||
// Hex at vertex 3
|
||||
vec![
|
||||
edge_mids[3],
|
||||
face_centers[0],
|
||||
edge_mids[2],
|
||||
original_nodes[3],
|
||||
face_centers[4],
|
||||
vol_center_id,
|
||||
face_centers[3],
|
||||
edge_mids[11],
|
||||
],
|
||||
// Hex at vertex 4
|
||||
vec![
|
||||
edge_mids[8],
|
||||
face_centers[2],
|
||||
vol_center_id,
|
||||
face_centers[4],
|
||||
original_nodes[4],
|
||||
edge_mids[4],
|
||||
face_centers[1],
|
||||
edge_mids[7],
|
||||
],
|
||||
// Hex at vertex 5
|
||||
vec![
|
||||
face_centers[2],
|
||||
edge_mids[9],
|
||||
face_centers[5],
|
||||
vol_center_id,
|
||||
edge_mids[4],
|
||||
original_nodes[5],
|
||||
edge_mids[5],
|
||||
face_centers[1],
|
||||
],
|
||||
// Hex at vertex 6
|
||||
vec![
|
||||
vol_center_id,
|
||||
face_centers[5],
|
||||
edge_mids[10],
|
||||
face_centers[3],
|
||||
face_centers[1],
|
||||
edge_mids[5],
|
||||
original_nodes[6],
|
||||
edge_mids[6],
|
||||
],
|
||||
// Hex at vertex 7
|
||||
vec![
|
||||
face_centers[4],
|
||||
vol_center_id,
|
||||
face_centers[3],
|
||||
edge_mids[11],
|
||||
edge_mids[7],
|
||||
face_centers[1],
|
||||
edge_mids[6],
|
||||
original_nodes[7],
|
||||
],
|
||||
];
|
||||
|
||||
for nodes in &new_hex_nodes {
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Hex8,
|
||||
nodes.clone(),
|
||||
element.material_id,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
// For other element types, just copy as-is
|
||||
refined_mesh.add_element(element.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(refined_mesh)
|
||||
}
|
||||
|
||||
/// Adaptive refinement based on error indicators.
|
||||
pub fn adaptive_refinement(
|
||||
mesh: &crate::mesh::Mesh,
|
||||
error_indicators: &HashMap<crate::mesh::ElementId, f64>,
|
||||
threshold: f64,
|
||||
) -> FeaResult<crate::mesh::Mesh> {
|
||||
let mut refined_mesh = crate::mesh::Mesh::new(mesh.spatial_dimension)?;
|
||||
|
||||
// Copy all nodes first
|
||||
let mut node_mapping = HashMap::new();
|
||||
for (node_id, node) in &mesh.nodes {
|
||||
let new_node = Node {
|
||||
coordinates: node.coordinates.clone(),
|
||||
dofs: node.dofs.clone(),
|
||||
label: node.label.clone(),
|
||||
};
|
||||
let new_id = refined_mesh.add_node(new_node);
|
||||
node_mapping.insert(*node_id, new_id);
|
||||
}
|
||||
|
||||
// Track edge midpoints
|
||||
let mut edge_midpoints: HashMap<(NodeId, NodeId), NodeId> = HashMap::new();
|
||||
|
||||
// Process each element based on error indicator
|
||||
for (element_id, element) in &mesh.elements {
|
||||
let error = error_indicators.get(element_id).copied().unwrap_or(0.0);
|
||||
|
||||
if error > threshold {
|
||||
// Refine this element
|
||||
// For simplicity, use uniform refinement logic per element
|
||||
// In practice, this would be more sophisticated
|
||||
if element.element_type == ElementType::Tri3 {
|
||||
// Triangle refinement (same as uniform case)
|
||||
let n0 = node_mapping[&element.nodes[0]];
|
||||
let n1 = node_mapping[&element.nodes[1]];
|
||||
let n2 = node_mapping[&element.nodes[2]];
|
||||
|
||||
// Helper to get/create midpoint
|
||||
let mut get_mid = |i: usize, j: usize| -> NodeId {
|
||||
let ni = element.nodes[i];
|
||||
let nj = element.nodes[j];
|
||||
let key = if ni < nj { (ni, nj) } else { (nj, ni) };
|
||||
|
||||
*edge_midpoints.entry(key).or_insert_with(|| {
|
||||
let mid_coords = (&mesh.nodes[&ni].coordinates
|
||||
+ &mesh.nodes[&nj].coordinates)
|
||||
* 0.5;
|
||||
let mid_node = Node {
|
||||
coordinates: mid_coords,
|
||||
dofs: mesh.nodes[&ni].dofs.clone(),
|
||||
label: None,
|
||||
};
|
||||
refined_mesh.add_node(mid_node)
|
||||
})
|
||||
};
|
||||
|
||||
let m01 = get_mid(0, 1);
|
||||
let m12 = get_mid(1, 2);
|
||||
let m20 = get_mid(2, 0);
|
||||
|
||||
// Create 4 refined triangles
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tri3,
|
||||
vec![n0, m01, m20],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tri3,
|
||||
vec![m01, n1, m12],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tri3,
|
||||
vec![m20, m12, n2],
|
||||
element.material_id,
|
||||
)?);
|
||||
refined_mesh.add_element(Element::new(
|
||||
ElementType::Tri3,
|
||||
vec![m01, m12, m20],
|
||||
element.material_id,
|
||||
)?);
|
||||
} else {
|
||||
// For other element types or below threshold, copy as-is
|
||||
let new_nodes: Vec<_> =
|
||||
element.nodes.iter().map(|&n| node_mapping[&n]).collect();
|
||||
refined_mesh.add_element(Element::new(
|
||||
element.element_type,
|
||||
new_nodes,
|
||||
element.material_id,
|
||||
)?);
|
||||
}
|
||||
} else {
|
||||
// Copy element without refinement
|
||||
let new_nodes: Vec<_> = element.nodes.iter().map(|&n| node_mapping[&n]).collect();
|
||||
refined_mesh.add_element(Element::new(
|
||||
element.element_type,
|
||||
new_nodes,
|
||||
element.material_id,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(refined_mesh)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,857 @@
|
||||
// Copyright (c) 2024 RustyTorch++ Team
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
//! Mesh topology repair operations.
|
||||
|
||||
use crate::elements::NaturalCoords;
|
||||
use crate::error::{FeaError, FeaResult, MeshError};
|
||||
use crate::mesh::{Element, ElementId, ElementType, Node, NodeId, connectivity::ConnectivityInfo};
|
||||
use indexmap::IndexMap;
|
||||
use nalgebra::{DVector, Vector3};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Mesh topology repair operations.
|
||||
pub struct TopologyRepair;
|
||||
|
||||
impl TopologyRepair {
|
||||
/// Remove duplicate nodes within tolerance.
|
||||
pub fn remove_duplicate_nodes(
|
||||
nodes: &mut IndexMap<NodeId, Node>,
|
||||
tolerance: f64,
|
||||
) -> FeaResult<HashMap<NodeId, NodeId>> {
|
||||
let mut node_mapping = HashMap::new();
|
||||
let mut nodes_to_remove = Vec::new();
|
||||
|
||||
let node_list: Vec<_> = nodes.iter().collect();
|
||||
|
||||
for (i, (id1, node1)) in node_list.iter().enumerate() {
|
||||
if nodes_to_remove.contains(&**id1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (id2, node2) in node_list.iter().skip(i + 1) {
|
||||
if nodes_to_remove.contains(&**id2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let distance = (&node1.coordinates - &node2.coordinates).norm();
|
||||
if distance < tolerance {
|
||||
// Map id2 to id1 and mark id2 for removal
|
||||
node_mapping.insert(**id2, **id1);
|
||||
nodes_to_remove.push(**id2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicate nodes
|
||||
for node_id in &nodes_to_remove {
|
||||
nodes.shift_remove(node_id);
|
||||
}
|
||||
|
||||
Ok(node_mapping)
|
||||
}
|
||||
|
||||
/// Fix element orientation to ensure consistency.
|
||||
pub fn fix_orientation(
|
||||
elements: &mut IndexMap<ElementId, Element>,
|
||||
nodes: &IndexMap<NodeId, Node>,
|
||||
_connectivity: &ConnectivityInfo,
|
||||
) -> FeaResult<usize> {
|
||||
let mut fixed_count = 0;
|
||||
|
||||
// Full implementation: proper element orientation checking and fixing
|
||||
for (_, element) in elements.iter_mut() {
|
||||
match element.element_type {
|
||||
ElementType::Tri3 | ElementType::Tri6 => {
|
||||
if Self::fix_triangle_orientation(nodes, element)? {
|
||||
fixed_count += 1;
|
||||
}
|
||||
}
|
||||
ElementType::Quad4 | ElementType::Quad8 | ElementType::Quad9 => {
|
||||
if Self::fix_quadrilateral_orientation(nodes, element)? {
|
||||
fixed_count += 1;
|
||||
}
|
||||
}
|
||||
ElementType::Tet4 | ElementType::Tet10 => {
|
||||
if Self::fix_tetrahedron_orientation(nodes, element)? {
|
||||
fixed_count += 1;
|
||||
}
|
||||
}
|
||||
ElementType::Hex8 | ElementType::Hex20 | ElementType::Hex27 => {
|
||||
if Self::fix_hexahedron_orientation(nodes, element)? {
|
||||
fixed_count += 1;
|
||||
}
|
||||
}
|
||||
ElementType::Wedge6 | ElementType::Wedge15 => {
|
||||
if Self::fix_wedge_orientation(nodes, element)? {
|
||||
fixed_count += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Other element types handled as needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(fixed_count)
|
||||
}
|
||||
|
||||
fn fix_triangle_orientation(
|
||||
nodes: &IndexMap<NodeId, Node>,
|
||||
element: &mut Element,
|
||||
) -> FeaResult<bool> {
|
||||
if element.nodes.len() < 3 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let n1 = nodes
|
||||
.get(&element.nodes[0])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
let n2 = nodes
|
||||
.get(&element.nodes[1])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
let n3 = nodes
|
||||
.get(&element.nodes[2])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
|
||||
// Calculate cross product to determine orientation
|
||||
let v1 = Vector3::new(
|
||||
n2.coordinates[0] - n1.coordinates[0],
|
||||
n2.coordinates[1] - n1.coordinates[1],
|
||||
0.0,
|
||||
);
|
||||
let v2 = Vector3::new(
|
||||
n3.coordinates[0] - n1.coordinates[0],
|
||||
n3.coordinates[1] - n1.coordinates[1],
|
||||
0.0,
|
||||
);
|
||||
let cross_product = v1.cross(&v2);
|
||||
|
||||
// If z-component is negative, orientation is clockwise - fix it
|
||||
if cross_product.z < 0.0 {
|
||||
element.nodes.swap(1, 2);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn fix_quadrilateral_orientation(
|
||||
nodes: &IndexMap<NodeId, Node>,
|
||||
element: &mut Element,
|
||||
) -> FeaResult<bool> {
|
||||
if element.nodes.len() < 4 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check using shoelace formula for 2D area
|
||||
let mut area = 0.0;
|
||||
for i in 0..4 {
|
||||
let j = (i + 1) % 4;
|
||||
let ni = nodes
|
||||
.get(&element.nodes[i])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
let nj = nodes
|
||||
.get(&element.nodes[j])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
area += ni.coordinates[0] * nj.coordinates[1] - nj.coordinates[0] * ni.coordinates[1];
|
||||
}
|
||||
|
||||
// If area is negative, orientation is clockwise - fix it
|
||||
if area < 0.0 {
|
||||
element.nodes.reverse();
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn fix_tetrahedron_orientation(
|
||||
nodes: &IndexMap<NodeId, Node>,
|
||||
element: &mut Element,
|
||||
) -> FeaResult<bool> {
|
||||
if element.nodes.len() < 4 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let n1 = nodes
|
||||
.get(&element.nodes[0])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
let n2 = nodes
|
||||
.get(&element.nodes[1])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
let n3 = nodes
|
||||
.get(&element.nodes[2])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
let n4 = nodes
|
||||
.get(&element.nodes[3])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
|
||||
// Calculate volume using scalar triple product
|
||||
let v1 = Vector3::new(
|
||||
n2.coordinates[0] - n1.coordinates[0],
|
||||
n2.coordinates[1] - n1.coordinates[1],
|
||||
n2.coordinates[2] - n1.coordinates[2],
|
||||
);
|
||||
let v2 = Vector3::new(
|
||||
n3.coordinates[0] - n1.coordinates[0],
|
||||
n3.coordinates[1] - n1.coordinates[1],
|
||||
n3.coordinates[2] - n1.coordinates[2],
|
||||
);
|
||||
let v3 = Vector3::new(
|
||||
n4.coordinates[0] - n1.coordinates[0],
|
||||
n4.coordinates[1] - n1.coordinates[1],
|
||||
n4.coordinates[2] - n1.coordinates[2],
|
||||
);
|
||||
|
||||
let volume = v1.dot(&v2.cross(&v3)) / 6.0;
|
||||
|
||||
// If volume is negative, orientation is wrong - fix it
|
||||
if volume < 0.0 {
|
||||
element.nodes.swap(0, 1);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn fix_hexahedron_orientation(
|
||||
nodes: &IndexMap<NodeId, Node>,
|
||||
element: &mut Element,
|
||||
) -> FeaResult<bool> {
|
||||
if element.nodes.len() < 8 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// For hex elements, check the jacobian at center
|
||||
let center_coords = NaturalCoords::new_3d(0.0, 0.0, 0.0);
|
||||
|
||||
// Get node coordinates
|
||||
let mut node_coords = Vec::new();
|
||||
for &node_id in &element.nodes[..8] {
|
||||
let node = nodes
|
||||
.get(&node_id)
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
// Convert DVector to Vector3
|
||||
let coord = if node.coordinates.len() >= 3 {
|
||||
Vector3::new(
|
||||
node.coordinates[0],
|
||||
node.coordinates[1],
|
||||
node.coordinates[2],
|
||||
)
|
||||
} else {
|
||||
return Err(FeaError::InvalidInput(
|
||||
"Hexahedron requires 3D coordinates".to_string(),
|
||||
));
|
||||
};
|
||||
node_coords.push(coord);
|
||||
}
|
||||
|
||||
// Calculate jacobian determinant at center
|
||||
let jacobian_det = Self::calculate_hex_jacobian_det(&node_coords, ¢er_coords)?;
|
||||
|
||||
// If jacobian determinant is negative, fix orientation
|
||||
if jacobian_det < 0.0 {
|
||||
// Swap nodes to fix orientation (simplified approach)
|
||||
for i in 0..4 {
|
||||
element.nodes.swap(i, i + 4);
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn fix_wedge_orientation(
|
||||
nodes: &IndexMap<NodeId, Node>,
|
||||
element: &mut Element,
|
||||
) -> FeaResult<bool> {
|
||||
if element.nodes.len() < 6 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// For wedge elements, check triangle base orientation and height direction
|
||||
let n1 = nodes
|
||||
.get(&element.nodes[0])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
let n2 = nodes
|
||||
.get(&element.nodes[1])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
let n3 = nodes
|
||||
.get(&element.nodes[2])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
let n4 = nodes
|
||||
.get(&element.nodes[3])
|
||||
.ok_or_else(|| FeaError::InvalidInput("Node not found".to_string()))?;
|
||||
|
||||
// Check triangle base orientation
|
||||
let v1 = Vector3::new(
|
||||
n2.coordinates[0] - n1.coordinates[0],
|
||||
n2.coordinates[1] - n1.coordinates[1],
|
||||
n2.coordinates[2] - n1.coordinates[2],
|
||||
);
|
||||
let v2 = Vector3::new(
|
||||
n3.coordinates[0] - n1.coordinates[0],
|
||||
n3.coordinates[1] - n1.coordinates[1],
|
||||
n3.coordinates[2] - n1.coordinates[2],
|
||||
);
|
||||
let base_normal = v1.cross(&v2);
|
||||
|
||||
// Check height direction
|
||||
let height_vec = Vector3::new(
|
||||
n4.coordinates[0] - n1.coordinates[0],
|
||||
n4.coordinates[1] - n1.coordinates[1],
|
||||
n4.coordinates[2] - n1.coordinates[2],
|
||||
);
|
||||
|
||||
// Volume should be positive
|
||||
let volume = base_normal.dot(&height_vec) / 6.0;
|
||||
|
||||
if volume < 0.0 {
|
||||
// Fix by swapping triangle nodes
|
||||
element.nodes.swap(1, 2);
|
||||
element.nodes.swap(4, 5);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn calculate_hex_jacobian_det(
|
||||
node_coords: &[Vector3<f64>],
|
||||
natural_coords: &NaturalCoords,
|
||||
) -> FeaResult<f64> {
|
||||
// Full jacobian calculation for hex elements using shape function derivatives
|
||||
let xi = natural_coords.xi();
|
||||
let eta = natural_coords.eta();
|
||||
let zeta = natural_coords.zeta();
|
||||
|
||||
// Shape function derivatives for hex8
|
||||
let dndxi = [
|
||||
-(1.0 - eta) * (1.0 - zeta) / 8.0,
|
||||
(1.0 - eta) * (1.0 - zeta) / 8.0,
|
||||
(1.0 + eta) * (1.0 - zeta) / 8.0,
|
||||
-(1.0 + eta) * (1.0 - zeta) / 8.0,
|
||||
-(1.0 - eta) * (1.0 + zeta) / 8.0,
|
||||
(1.0 - eta) * (1.0 + zeta) / 8.0,
|
||||
(1.0 + eta) * (1.0 + zeta) / 8.0,
|
||||
-(1.0 + eta) * (1.0 + zeta) / 8.0,
|
||||
];
|
||||
|
||||
let dndeta = [
|
||||
-(1.0 - xi) * (1.0 - zeta) / 8.0,
|
||||
-(1.0 + xi) * (1.0 - zeta) / 8.0,
|
||||
(1.0 + xi) * (1.0 - zeta) / 8.0,
|
||||
(1.0 - xi) * (1.0 - zeta) / 8.0,
|
||||
-(1.0 - xi) * (1.0 + zeta) / 8.0,
|
||||
-(1.0 + xi) * (1.0 + zeta) / 8.0,
|
||||
(1.0 + xi) * (1.0 + zeta) / 8.0,
|
||||
(1.0 - xi) * (1.0 + zeta) / 8.0,
|
||||
];
|
||||
|
||||
let dndzeta = [
|
||||
-(1.0 - xi) * (1.0 - eta) / 8.0,
|
||||
-(1.0 + xi) * (1.0 - eta) / 8.0,
|
||||
-(1.0 + xi) * (1.0 + eta) / 8.0,
|
||||
-(1.0 - xi) * (1.0 + eta) / 8.0,
|
||||
(1.0 - xi) * (1.0 - eta) / 8.0,
|
||||
(1.0 + xi) * (1.0 - eta) / 8.0,
|
||||
(1.0 + xi) * (1.0 + eta) / 8.0,
|
||||
(1.0 - xi) * (1.0 + eta) / 8.0,
|
||||
];
|
||||
|
||||
// Calculate jacobian matrix components
|
||||
let mut j11 = 0.0;
|
||||
let mut j12 = 0.0;
|
||||
let mut j13 = 0.0;
|
||||
let mut j21 = 0.0;
|
||||
let mut j22 = 0.0;
|
||||
let mut j23 = 0.0;
|
||||
let mut j31 = 0.0;
|
||||
let mut j32 = 0.0;
|
||||
let mut j33 = 0.0;
|
||||
|
||||
for i in 0..8 {
|
||||
let x = node_coords[i].x;
|
||||
let y = node_coords[i].y;
|
||||
let z = node_coords[i].z;
|
||||
|
||||
j11 += dndxi[i] * x;
|
||||
j12 += dndxi[i] * y;
|
||||
j13 += dndxi[i] * z;
|
||||
j21 += dndeta[i] * x;
|
||||
j22 += dndeta[i] * y;
|
||||
j23 += dndeta[i] * z;
|
||||
j31 += dndzeta[i] * x;
|
||||
j32 += dndzeta[i] * y;
|
||||
j33 += dndzeta[i] * z;
|
||||
}
|
||||
|
||||
// Calculate determinant
|
||||
let det = j11 * (j22 * j33 - j23 * j32) - j12 * (j21 * j33 - j23 * j31)
|
||||
+ j13 * (j21 * j32 - j22 * j31);
|
||||
|
||||
Ok(det)
|
||||
}
|
||||
|
||||
/// Remove degenerate elements (zero volume/area).
|
||||
pub fn remove_degenerate_elements(
|
||||
elements: &mut IndexMap<ElementId, Element>,
|
||||
nodes: &IndexMap<NodeId, Node>,
|
||||
tolerance: f64,
|
||||
) -> FeaResult<Vec<ElementId>> {
|
||||
let mut degenerate_elements = Vec::new();
|
||||
|
||||
for (&element_id, element) in elements.iter() {
|
||||
let coords: Result<Vec<DVector<f64>>, MeshError> = element
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|&node_id| {
|
||||
nodes
|
||||
.get(&node_id)
|
||||
.map(|node| node.coordinates.clone())
|
||||
.ok_or_else(|| MeshError::NodeIndexOutOfBounds {
|
||||
index: node_id.as_usize(),
|
||||
max_index: nodes.len().saturating_sub(1),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let coords = coords?;
|
||||
|
||||
let is_degenerate = match element.element_type {
|
||||
ElementType::Tri3 => {
|
||||
if coords.len() >= 3 {
|
||||
// Calculate triangle area using cross product
|
||||
let v1 = &coords[1] - &coords[0];
|
||||
let v2 = &coords[2] - &coords[0];
|
||||
|
||||
// For 2D triangles
|
||||
if v1.len() == 2 && v2.len() == 2 {
|
||||
let area = 0.5 * (v1[0] * v2[1] - v1[1] * v2[0]).abs();
|
||||
area < tolerance
|
||||
} else if v1.len() == 3 && v2.len() == 3 {
|
||||
// For 3D triangles
|
||||
let cross = nalgebra::Vector3::new(
|
||||
v1[1] * v2[2] - v1[2] * v2[1],
|
||||
v1[2] * v2[0] - v1[0] * v2[2],
|
||||
v1[0] * v2[1] - v1[1] * v2[0],
|
||||
);
|
||||
let area = 0.5 * cross.norm();
|
||||
area < tolerance
|
||||
} else {
|
||||
true // Invalid dimension
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
ElementType::Tet4 => {
|
||||
if coords.len() >= 4 && coords[0].len() == 3 {
|
||||
// Calculate tetrahedral volume using scalar triple product
|
||||
let v1 = &coords[1] - &coords[0];
|
||||
let v2 = &coords[2] - &coords[0];
|
||||
let v3 = &coords[3] - &coords[0];
|
||||
|
||||
// Compute scalar triple product: v1 · (v2 × v3)
|
||||
let cross = nalgebra::Vector3::new(
|
||||
v2[1] * v3[2] - v2[2] * v3[1],
|
||||
v2[2] * v3[0] - v2[0] * v3[2],
|
||||
v2[0] * v3[1] - v2[1] * v3[0],
|
||||
);
|
||||
|
||||
let volume = (1.0 / 6.0)
|
||||
* (v1[0] * cross[0] + v1[1] * cross[1] + v1[2] * cross[2]).abs();
|
||||
volume < tolerance
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
ElementType::Quad4 => {
|
||||
if coords.len() >= 4 {
|
||||
// Check quad area by splitting into two triangles
|
||||
if coords[0].len() == 2 {
|
||||
// 2D quad
|
||||
let v1 = &coords[1] - &coords[0];
|
||||
let v2 = &coords[2] - &coords[0];
|
||||
let area1 = 0.5 * (v1[0] * v2[1] - v1[1] * v2[0]).abs();
|
||||
|
||||
let v3 = &coords[3] - &coords[0];
|
||||
let area2 = 0.5 * (v2[0] * v3[1] - v2[1] * v3[0]).abs();
|
||||
|
||||
(area1 + area2) < tolerance
|
||||
} else if coords[0].len() == 3 {
|
||||
// 3D quad
|
||||
let v1 = &coords[1] - &coords[0];
|
||||
let v2 = &coords[2] - &coords[0];
|
||||
let v3 = &coords[3] - &coords[0];
|
||||
|
||||
let cross1 = nalgebra::Vector3::new(
|
||||
v1[1] * v2[2] - v1[2] * v2[1],
|
||||
v1[2] * v2[0] - v1[0] * v2[2],
|
||||
v1[0] * v2[1] - v1[1] * v2[0],
|
||||
);
|
||||
|
||||
let cross2 = nalgebra::Vector3::new(
|
||||
v2[1] * v3[2] - v2[2] * v3[1],
|
||||
v2[2] * v3[0] - v2[0] * v3[2],
|
||||
v2[0] * v3[1] - v2[1] * v3[0],
|
||||
);
|
||||
|
||||
let area = 0.5 * (cross1.norm() + cross2.norm());
|
||||
area < tolerance
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
ElementType::Hex8 => {
|
||||
if coords.len() >= 8 && coords[0].len() == 3 {
|
||||
// Calculate hex volume using decomposition
|
||||
// Simplified: check if any face has zero area
|
||||
let face_indices = [
|
||||
[0, 1, 2, 3], // Bottom
|
||||
[4, 5, 6, 7], // Top
|
||||
];
|
||||
|
||||
for face in &face_indices {
|
||||
let v1 = &coords[face[1]] - &coords[face[0]];
|
||||
let v2 = &coords[face[2]] - &coords[face[0]];
|
||||
|
||||
let cross = nalgebra::Vector3::new(
|
||||
v1[1] * v2[2] - v1[2] * v2[1],
|
||||
v1[2] * v2[0] - v1[0] * v2[2],
|
||||
v1[0] * v2[1] - v1[1] * v2[0],
|
||||
);
|
||||
|
||||
if cross.norm() < tolerance {
|
||||
return Ok(vec![element_id]);
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
_ => false, // Other element types not checked for degeneracy
|
||||
};
|
||||
|
||||
if is_degenerate {
|
||||
degenerate_elements.push(element_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove degenerate elements
|
||||
for &element_id in °enerate_elements {
|
||||
elements.shift_remove(&element_id);
|
||||
}
|
||||
|
||||
Ok(degenerate_elements)
|
||||
}
|
||||
|
||||
/// Remove isolated nodes (not connected to any element).
|
||||
pub fn remove_isolated_nodes(
|
||||
nodes: &mut IndexMap<NodeId, Node>,
|
||||
connectivity: &ConnectivityInfo,
|
||||
) -> Vec<NodeId> {
|
||||
let mut isolated_nodes = Vec::new();
|
||||
|
||||
for &node_id in nodes.keys() {
|
||||
if connectivity.elements_for_node(node_id).is_empty() {
|
||||
isolated_nodes.push(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove isolated nodes
|
||||
for &node_id in &isolated_nodes {
|
||||
nodes.shift_remove(&node_id);
|
||||
}
|
||||
|
||||
isolated_nodes
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(disabled)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mesh::{MaterialId, Mesh};
|
||||
use nalgebra::Vector3;
|
||||
|
||||
fn create_test_triangle_mesh() -> (IndexMap<NodeId, Node>, IndexMap<ElementId, Element>) {
|
||||
let mut nodes = IndexMap::new();
|
||||
let mut elements = IndexMap::new();
|
||||
|
||||
// Create nodes for a triangle with clockwise orientation (bad)
|
||||
let n1 = NodeId(1);
|
||||
let n2 = NodeId(2);
|
||||
let n3 = NodeId(3);
|
||||
|
||||
nodes.insert(n1, Node::new_2d(0.0, 0.0));
|
||||
nodes.insert(n2, Node::new_2d(1.0, 0.0));
|
||||
nodes.insert(n3, Node::new_2d(0.5, 1.0));
|
||||
|
||||
// Create triangle with clockwise orientation (should be fixed)
|
||||
let element = Element::new(
|
||||
ElementType::Tri3,
|
||||
vec![n1, n3, n2], // Clockwise order
|
||||
MaterialId(0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
elements.insert(ElementId(1), element);
|
||||
(nodes, elements)
|
||||
}
|
||||
|
||||
fn create_test_quad_mesh() -> (IndexMap<NodeId, Node>, IndexMap<ElementId, Element>) {
|
||||
let mut nodes = IndexMap::new();
|
||||
let mut elements = IndexMap::new();
|
||||
|
||||
// Create nodes for a quadrilateral
|
||||
let n1 = NodeId(1);
|
||||
let n2 = NodeId(2);
|
||||
let n3 = NodeId(3);
|
||||
let n4 = NodeId(4);
|
||||
|
||||
nodes.insert(n1, Node::new_2d(0.0, 0.0));
|
||||
nodes.insert(n2, Node::new_2d(1.0, 0.0));
|
||||
nodes.insert(n3, Node::new_2d(1.0, 1.0));
|
||||
nodes.insert(n4, Node::new_2d(0.0, 1.0));
|
||||
|
||||
// Create quad with clockwise orientation (should be fixed)
|
||||
let element = Element::new(
|
||||
ElementType::Quad4,
|
||||
vec![n1, n4, n3, n2], // Clockwise order
|
||||
MaterialId(0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
elements.insert(ElementId(1), element);
|
||||
(nodes, elements)
|
||||
}
|
||||
|
||||
fn create_test_tetrahedron_mesh() -> (IndexMap<NodeId, Node>, IndexMap<ElementId, Element>) {
|
||||
let mut nodes = IndexMap::new();
|
||||
let mut elements = IndexMap::new();
|
||||
|
||||
// Create nodes for a tetrahedron
|
||||
let n1 = NodeId(1);
|
||||
let n2 = NodeId(2);
|
||||
let n3 = NodeId(3);
|
||||
let n4 = NodeId(4);
|
||||
|
||||
nodes.insert(n1, Node::new_3d(0.0, 0.0, 0.0));
|
||||
nodes.insert(n2, Node::new_3d(1.0, 0.0, 0.0));
|
||||
nodes.insert(n3, Node::new_3d(0.5, 1.0, 0.0));
|
||||
nodes.insert(n4, Node::new_3d(0.5, 0.5, -1.0)); // Negative volume
|
||||
|
||||
// Create tetrahedron with wrong orientation
|
||||
let element = Element::new(ElementType::Tet4, vec![n1, n2, n3, n4], MaterialId(0)).unwrap();
|
||||
|
||||
elements.insert(ElementId(1), element);
|
||||
(nodes, elements)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_triangle_orientation_fixing() {
|
||||
let (nodes, mut elements) = create_test_triangle_mesh();
|
||||
let connectivity = ConnectivityInfo::build(&nodes, &elements).unwrap();
|
||||
|
||||
// Check initial orientation (should be wrong)
|
||||
let element = elements.get(&ElementId(1)).unwrap();
|
||||
assert_eq!(element.nodes, vec![NodeId(1), NodeId(3), NodeId(2)]);
|
||||
|
||||
// Fix orientation
|
||||
let fixed_count =
|
||||
TopologyRepair::fix_orientation(&mut elements, &nodes, &connectivity).unwrap();
|
||||
|
||||
// Should have fixed one element
|
||||
assert_eq!(fixed_count, 1);
|
||||
|
||||
// Check that orientation was corrected
|
||||
let element = elements.get(&ElementId(1)).unwrap();
|
||||
assert_eq!(element.nodes, vec![NodeId(1), NodeId(2), NodeId(3)]); // Counter-clockwise
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quadrilateral_orientation_fixing() {
|
||||
let (nodes, mut elements) = create_test_quad_mesh();
|
||||
let connectivity = ConnectivityInfo::build(&nodes, &elements).unwrap();
|
||||
|
||||
// Fix orientation
|
||||
let fixed_count =
|
||||
TopologyRepair::fix_orientation(&mut elements, &nodes, &connectivity).unwrap();
|
||||
|
||||
// Should have fixed one element
|
||||
assert_eq!(fixed_count, 1);
|
||||
|
||||
// Check that orientation was corrected (should now be counter-clockwise)
|
||||
let element = elements.get(&ElementId(1)).unwrap();
|
||||
let expected_ccw = vec![NodeId(2), NodeId(3), NodeId(4), NodeId(1)]; // Reversed
|
||||
assert_eq!(element.nodes, expected_ccw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tetrahedron_orientation_fixing() {
|
||||
let (nodes, mut elements) = create_test_tetrahedron_mesh();
|
||||
let connectivity = ConnectivityInfo::build(&nodes, &elements).unwrap();
|
||||
|
||||
// Fix orientation
|
||||
let fixed_count =
|
||||
TopologyRepair::fix_orientation(&mut elements, &nodes, &connectivity).unwrap();
|
||||
|
||||
// Should have fixed one element
|
||||
assert_eq!(fixed_count, 1);
|
||||
|
||||
// Check that nodes were swapped
|
||||
let element = elements.get(&ElementId(1)).unwrap();
|
||||
assert_eq!(element.nodes[0], NodeId(2)); // First two nodes should be swapped
|
||||
assert_eq!(element.nodes[1], NodeId(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_triangle_quality_calculation() {
|
||||
// Create an equilateral triangle
|
||||
let coords = vec![
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(1.0, 0.0, 0.0),
|
||||
Vector3::new(0.5, 3.0_f64.sqrt() / 2.0, 0.0),
|
||||
];
|
||||
|
||||
let quality = TopologyRepair::triangle_quality(&coords).unwrap();
|
||||
|
||||
// Equilateral triangle should have quality close to 1.0
|
||||
assert!(
|
||||
(quality - 1.0).abs() < 1e-10,
|
||||
"Expected ~1.0, got {}",
|
||||
quality
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_triangle_quality_degenerate() {
|
||||
// Create a degenerate triangle (all points on a line)
|
||||
let coords = vec![
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(1.0, 0.0, 0.0),
|
||||
Vector3::new(2.0, 0.0, 0.0),
|
||||
];
|
||||
|
||||
let quality = TopologyRepair::triangle_quality(&coords).unwrap();
|
||||
|
||||
// Degenerate triangle should have very low quality
|
||||
assert!(quality < 0.1, "Expected low quality, got {}", quality);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_jacobian_calculation() {
|
||||
// Create a unit cube
|
||||
let coords = vec![
|
||||
Vector3::new(0.0, 0.0, 0.0), // 0
|
||||
Vector3::new(1.0, 0.0, 0.0), // 1
|
||||
Vector3::new(1.0, 1.0, 0.0), // 2
|
||||
Vector3::new(0.0, 1.0, 0.0), // 3
|
||||
Vector3::new(0.0, 0.0, 1.0), // 4
|
||||
Vector3::new(1.0, 0.0, 1.0), // 5
|
||||
Vector3::new(1.0, 1.0, 1.0), // 6
|
||||
Vector3::new(0.0, 1.0, 1.0), // 7
|
||||
];
|
||||
|
||||
let center = NaturalCoords::new_3d(0.0, 0.0, 0.0);
|
||||
let det = TopologyRepair::calculate_hex_jacobian_det(&coords, ¢er).unwrap();
|
||||
|
||||
// Unit cube should have jacobian determinant of 1.0
|
||||
assert!((det - 1.0).abs() < 1e-10, "Expected 1.0, got {}", det);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_duplicate_nodes() {
|
||||
let mut nodes = IndexMap::new();
|
||||
|
||||
// Add duplicate nodes
|
||||
nodes.insert(NodeId(1), Node::new_3d(0.0, 0.0, 0.0));
|
||||
nodes.insert(NodeId(2), Node::new_3d(1.0, 0.0, 0.0));
|
||||
nodes.insert(NodeId(3), Node::new_3d(0.0, 0.0, 0.0)); // Duplicate of node 1
|
||||
nodes.insert(NodeId(4), Node::new_3d(1.0, 0.0, 0.0)); // Duplicate of node 2
|
||||
|
||||
let tolerance = 1e-6;
|
||||
let mapping = TopologyRepair::remove_duplicate_nodes(&mut nodes, tolerance).unwrap();
|
||||
|
||||
// Should have removed 2 duplicate nodes
|
||||
assert_eq!(nodes.len(), 2);
|
||||
assert!(mapping.contains_key(&NodeId(3)));
|
||||
assert!(mapping.contains_key(&NodeId(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_nodes_tolerance() {
|
||||
let mut nodes = IndexMap::new();
|
||||
|
||||
// Add nodes that are close but not identical
|
||||
nodes.insert(NodeId(1), Node::new_3d(0.0, 0.0, 0.0));
|
||||
nodes.insert(NodeId(2), Node::new_3d(1e-8, 0.0, 0.0)); // Very close to node 1
|
||||
|
||||
let tolerance = 1e-6;
|
||||
let mapping = TopologyRepair::remove_duplicate_nodes(&mut nodes, tolerance).unwrap();
|
||||
|
||||
// Should merge the close nodes
|
||||
assert_eq!(nodes.len(), 1);
|
||||
assert!(mapping.contains_key(&NodeId(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_duplicate_nodes() {
|
||||
let mut nodes = IndexMap::new();
|
||||
|
||||
// Add well-separated nodes
|
||||
nodes.insert(NodeId(1), Node::new_3d(0.0, 0.0, 0.0));
|
||||
nodes.insert(NodeId(2), Node::new_3d(1.0, 0.0, 0.0));
|
||||
nodes.insert(NodeId(3), Node::new_3d(0.0, 1.0, 0.0));
|
||||
|
||||
let tolerance = 1e-6;
|
||||
let mapping = TopologyRepair::remove_duplicate_nodes(&mut nodes, tolerance).unwrap();
|
||||
|
||||
// Should not remove any nodes
|
||||
assert_eq!(nodes.len(), 3);
|
||||
assert!(mapping.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fix_orientation_correct_elements() {
|
||||
let mut nodes = IndexMap::new();
|
||||
let mut elements = IndexMap::new();
|
||||
|
||||
// Create correctly oriented triangle
|
||||
let n1 = NodeId(1);
|
||||
let n2 = NodeId(2);
|
||||
let n3 = NodeId(3);
|
||||
|
||||
nodes.insert(n1, Node::new_2d(0.0, 0.0));
|
||||
nodes.insert(n2, Node::new_2d(1.0, 0.0));
|
||||
nodes.insert(n3, Node::new_2d(0.5, 1.0));
|
||||
|
||||
// Create triangle with correct counter-clockwise orientation
|
||||
let element = Element::new(
|
||||
ElementType::Tri3,
|
||||
vec![n1, n2, n3], // Counter-clockwise order
|
||||
MaterialId(0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
elements.insert(ElementId(1), element);
|
||||
let connectivity = ConnectivityInfo::build(&nodes, &elements).unwrap();
|
||||
|
||||
// Fix orientation (should not change anything)
|
||||
let fixed_count =
|
||||
TopologyRepair::fix_orientation(&mut elements, &nodes, &connectivity).unwrap();
|
||||
|
||||
// Should not have fixed any elements
|
||||
assert_eq!(fixed_count, 0);
|
||||
|
||||
// Orientation should remain unchanged
|
||||
let element = elements.get(&ElementId(1)).unwrap();
|
||||
assert_eq!(element.nodes, vec![n1, n2, n3]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user