// TDD: GREEN phase - Implement unstructured mesh use super::{Mesh, MeshBounds, MeshStatistics}; use crate::error::{CfdError, CfdResult}; use crate::mesh::entities::{Cell, Face, Node}; use crate::traits::MeshEntity; use indexmap::IndexMap; use nalgebra::Vector3; /// Result of cell subdivision operation struct SubdivisionResult { /// New nodes created during subdivision (position, id) nodes: Vec<(Vector3, usize)>, /// New cells created during subdivision (vertices, centroid, volume) cells: Vec<(Vec, Vector3, f64)>, } /// Unstructured (irregular) mesh for complex geometries #[derive(Debug, Clone)] pub struct UnstructuredMesh { /// Nodes storage nodes: IndexMap, /// Cells storage cells: IndexMap, /// Faces storage faces: IndexMap, /// Next available node ID next_node_id: usize, /// Next available cell ID next_cell_id: usize, /// Next available face ID next_face_id: usize, /// Mesh bounds cache bounds_cache: Option, /// Whether bounds cache is valid bounds_valid: bool, } impl UnstructuredMesh { /// Create a new empty unstructured mesh #[must_use] pub fn new() -> Self { Self { nodes: IndexMap::new(), cells: IndexMap::new(), faces: IndexMap::new(), next_node_id: 0, next_cell_id: 0, next_face_id: 0, bounds_cache: None, bounds_valid: false, } } /// Add a new node to the mesh pub fn add_node(&mut self, position: Vector3) -> CfdResult { let node_id = self.next_node_id; let node = Node::new(node_id, position); self.nodes.insert(node_id, node); self.next_node_id += 1; self.bounds_valid = false; // Invalidate bounds cache Ok(node_id) } /// Add a triangular cell pub fn add_triangle_cell(&mut self, n1: usize, n2: usize, n3: usize) -> CfdResult { // Validate that nodes exist if !self.nodes.contains_key(&n1) || !self.nodes.contains_key(&n2) || !self.nodes.contains_key(&n3) { return Err(CfdError::mesh("One or more nodes do not exist")); } let cell_id = self.next_cell_id; let vertices = vec![n1, n2, n3]; // Calculate centroid let p1 = self.nodes[&n1].position(); let p2 = self.nodes[&n2].position(); let p3 = self.nodes[&n3].position(); let centroid = (p1 + p2 + p3) / 3.0; // Calculate area using cross product let v1 = p2 - p1; let v2 = p3 - p1; let area = 0.5 * v1.cross(&v2).magnitude(); let cell = Cell::new(cell_id, vertices, centroid, area); self.cells.insert(cell_id, cell); self.next_cell_id += 1; Ok(cell_id) } /// Add a quadrilateral cell pub fn add_quadrilateral_cell( &mut self, n1: usize, n2: usize, n3: usize, n4: usize, ) -> CfdResult { // Validate that nodes exist if !self.nodes.contains_key(&n1) || !self.nodes.contains_key(&n2) || !self.nodes.contains_key(&n3) || !self.nodes.contains_key(&n4) { return Err(CfdError::mesh("One or more nodes do not exist")); } let cell_id = self.next_cell_id; let vertices = vec![n1, n2, n3, n4]; // Calculate centroid let p1 = self.nodes[&n1].position(); let p2 = self.nodes[&n2].position(); let p3 = self.nodes[&n3].position(); let p4 = self.nodes[&n4].position(); let centroid = (p1 + p2 + p3 + p4) / 4.0; // Calculate area using triangulation let v1 = p2 - p1; let v2 = p3 - p1; let v3 = p4 - p1; let area1 = 0.5 * v1.cross(&v2).magnitude(); let area2 = 0.5 * v2.cross(&v3).magnitude(); let area = area1 + area2; let cell = Cell::new(cell_id, vertices, centroid, area); self.cells.insert(cell_id, cell); self.next_cell_id += 1; Ok(cell_id) } /// Add a tetrahedral cell pub fn add_tetrahedron_cell( &mut self, n1: usize, n2: usize, n3: usize, n4: usize, ) -> CfdResult { // Validate that nodes exist if !self.nodes.contains_key(&n1) || !self.nodes.contains_key(&n2) || !self.nodes.contains_key(&n3) || !self.nodes.contains_key(&n4) { return Err(CfdError::mesh("One or more nodes do not exist")); } let cell_id = self.next_cell_id; let vertices = vec![n1, n2, n3, n4]; // Calculate centroid let p1 = self.nodes[&n1].position(); let p2 = self.nodes[&n2].position(); let p3 = self.nodes[&n3].position(); let p4 = self.nodes[&n4].position(); let centroid = (p1 + p2 + p3 + p4) / 4.0; // Calculate volume using scalar triple product let v1 = p2 - p1; let v2 = p3 - p1; let v3 = p4 - p1; let volume = (1.0 / 6.0) * v1.dot(&v2.cross(&v3)).abs(); let cell = Cell::new(cell_id, vertices, centroid, volume); self.cells.insert(cell_id, cell); self.next_cell_id += 1; Ok(cell_id) } /// Get a node by ID pub fn get_node(&self, node_id: usize) -> CfdResult<&Node> { self.nodes .get(&node_id) .ok_or_else(|| CfdError::mesh("Node not found")) } /// Get a cell by ID pub fn get_cell(&self, cell_id: usize) -> CfdResult<&Cell> { self.cells .get(&cell_id) .ok_or_else(|| CfdError::mesh("Cell not found")) } /// Get a face by ID pub fn get_face(&self, face_id: usize) -> CfdResult<&Face> { self.faces .get(&face_id) .ok_or_else(|| CfdError::mesh("Face not found")) } /// Add a face between two cells pub fn add_face( &mut self, vertices: Vec, _cell1: Option, cell2: Option, ) -> CfdResult { // Validate vertices exist for &vertex_id in &vertices { if !self.nodes.contains_key(&vertex_id) { return Err(CfdError::mesh("Face references non-existent vertex")); } } let face_id = self.next_face_id; // Calculate centroid let positions: Vec> = vertices .iter() .map(|&id| self.nodes[&id].position()) .collect(); let centroid = positions .iter() .fold(Vector3::zeros(), |acc, &pos| acc + pos) / positions.len() as f64; // Calculate area (simplified for different face types) let area = if vertices.len() == 2 { // Edge: distance between points (positions[1] - positions[0]).magnitude() } else if vertices.len() == 3 { // Triangle: cross product let v1 = positions[1] - positions[0]; let v2 = positions[2] - positions[0]; 0.5 * v1.cross(&v2).magnitude() } else if vertices.len() == 4 { // Quadrilateral: triangulation let v1 = positions[1] - positions[0]; let v2 = positions[2] - positions[0]; let v3 = positions[3] - positions[0]; let area1 = 0.5 * v1.cross(&v2).magnitude(); let area2 = 0.5 * v2.cross(&v3).magnitude(); area1 + area2 } else { return Err(CfdError::mesh("Unsupported face type")); }; // Determine if face is on boundary let is_boundary = cell2.is_none(); let face = if is_boundary { // Calculate normal for boundary face let normal = if vertices.len() >= 3 { let v1 = positions[1] - positions[0]; let v2 = positions[2] - positions[0]; v1.cross(&v2).normalize() } else { Vector3::new(0.0, 0.0, 1.0) // Default normal }; Face::new_boundary(face_id, vertices, centroid, area, normal) } else { Face::new(face_id, vertices, centroid, area) }; self.faces.insert(face_id, face); self.next_face_id += 1; Ok(face_id) } /// Generate faces automatically from cells pub fn generate_faces(&mut self) -> CfdResult<()> { // This is a simplified implementation // In practice, this would be more complex to handle shared faces properly let mut faces_to_add = Vec::new(); for (_, cell) in &self.cells { let vertices = cell.vertex_indices(); if vertices.len() == 3 { // Triangle - create 3 edges for i in 0..3 { let next_i = (i + 1) % 3; let edge_vertices = vec![vertices[i], vertices[next_i]]; faces_to_add.push((edge_vertices, Some(cell.id()), None)); } } else if vertices.len() == 4 { // Quadrilateral - create 4 edges for i in 0..4 { let next_i = (i + 1) % 4; let edge_vertices = vec![vertices[i], vertices[next_i]]; faces_to_add.push((edge_vertices, Some(cell.id()), None)); } } } // Add faces after collecting for (vertices, cell1, cell2) in faces_to_add { let _ = self.add_face(vertices, cell1, cell2); } Ok(()) } /// Calculate mesh bounds fn calculate_bounds(&self) -> MeshBounds { if self.nodes.is_empty() { return MeshBounds::new(Vector3::zeros(), Vector3::zeros()); } let mut min = Vector3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY); let mut max = Vector3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY); for (_, node) in &self.nodes { let pos = node.position(); min.x = min.x.min(pos.x); min.y = min.y.min(pos.y); min.z = min.z.min(pos.z); max.x = max.x.max(pos.x); max.y = max.y.max(pos.y); max.z = max.z.max(pos.z); } MeshBounds::new(min, max) } } impl Default for UnstructuredMesh { fn default() -> Self { Self::new() } } impl Mesh for UnstructuredMesh { fn cell_count(&self) -> usize { self.cells.len() } fn node_count(&self) -> usize { self.nodes.len() } fn bounds(&self) -> MeshBounds { if !self.bounds_valid || self.bounds_cache.is_none() { let mut mesh_mut = self.clone(); // This is not ideal, but const methods can't modify mesh_mut.bounds_cache = Some(mesh_mut.calculate_bounds()); mesh_mut.bounds_valid = true; return mesh_mut.bounds_cache.unwrap(); } self.bounds_cache.clone().unwrap() } fn validate(&self) -> CfdResult<()> { // Check that all cells have valid vertices for (_, cell) in &self.cells { for &vertex_id in cell.vertex_indices() { if !self.nodes.contains_key(&vertex_id) { return Err(CfdError::mesh("Cell references non-existent vertex")); } } // Check cell volume is positive if cell.volume() <= 0.0 { return Err(CfdError::mesh("Cell has non-positive volume")); } } // Check that all faces have valid vertices for (_, face) in &self.faces { for &vertex_id in face.vertex_indices() { if !self.nodes.contains_key(&vertex_id) { return Err(CfdError::mesh("Face references non-existent vertex")); } } } Ok(()) } fn statistics(&self) -> MeshStatistics { use crate::mesh::statistics::MeshQualityAnalyzer; let mut stats = MeshQualityAnalyzer::analyze_mesh(&self.nodes, &self.cells, &self.faces); // Calculate unstructured mesh specific metrics stats.orthogonality = MeshQualityAnalyzer::calculate_orthogonality(&self.faces); stats.non_orthogonality = MeshQualityAnalyzer::calculate_non_orthogonality(&self.faces); stats } fn is_boundary_cell(&self, cell_id: usize) -> bool { // For unstructured mesh, we need to check if any face of the cell is on boundary // This is a simplified implementation self.cells .get(&cell_id) .is_some_and(super::super::traits::MeshEntity::is_boundary) } fn get_cell_neighbors(&self, cell_id: usize) -> CfdResult> { let cell = self.get_cell(cell_id)?; let mut neighbors = Vec::new(); // For unstructured mesh, we need to find cells that share faces/edges let cell_vertices = cell.vertex_indices().to_vec(); for (other_cell_id, other_cell) in &self.cells { if *other_cell_id == cell_id { continue; } let other_vertices = other_cell.vertex_indices(); // Count shared vertices let shared_vertices: Vec<_> = cell_vertices .iter() .filter(|&&v| other_vertices.contains(&v)) .collect(); // For 2D: cells are neighbors if they share an edge (2 vertices) // For 3D: cells are neighbors if they share a face (3+ vertices) let min_shared = if cell_vertices.len() <= 4 { 2 } else { 3 }; if shared_vertices.len() >= min_shared { neighbors.push(*other_cell_id); } } Ok(neighbors) } fn refine(&mut self) -> CfdResult<()> { // Simple uniform refinement - subdivide all triangular cells let cells_to_refine: Vec<_> = self.cells.keys().copied().collect(); self.refine_cells(&cells_to_refine) } } impl UnstructuredMesh { /// Refine specific cells by subdivision pub fn refine_cells(&mut self, cell_ids: &[usize]) -> CfdResult<()> { let mut new_cells = Vec::new(); let mut new_nodes = Vec::new(); for &cell_id in cell_ids { let cell = self .cells .get(&cell_id) .ok_or_else(|| CfdError::mesh("Cell not found for refinement"))? .clone(); match cell.vertex_indices().len() { 3 => { // Subdivide triangle into 4 triangles let subdivided = self.subdivide_triangle(&cell)?; new_cells.extend(subdivided.cells); new_nodes.extend(subdivided.nodes); } 4 => { // Subdivide quadrilateral into 4 quadrilaterals let subdivided = self.subdivide_quadrilateral(&cell)?; new_cells.extend(subdivided.cells); new_nodes.extend(subdivided.nodes); } _ => { return Err(CfdError::mesh("Unsupported cell type for refinement")); } } } // Add new nodes for (position, _) in new_nodes { self.add_node(position)?; } // Remove original cells and add new ones for &cell_id in cell_ids { self.cells.remove(&cell_id); } // Add new subdivided cells for (vertices, centroid, volume) in new_cells { let cell_id = self.next_cell_id; let cell = Cell::new(cell_id, vertices, centroid, volume); self.cells.insert(cell_id, cell); self.next_cell_id += 1; } // Invalidate bounds cache self.bounds_valid = false; Ok(()) } /// Subdivide a triangle into 4 smaller triangles fn subdivide_triangle(&mut self, cell: &Cell) -> CfdResult { let vertices = cell.vertex_indices(); if vertices.len() != 3 { return Err(CfdError::mesh("Expected triangle for subdivision")); } let [v0, v1, v2] = [vertices[0], vertices[1], vertices[2]]; let p0 = self.nodes[&v0].position(); let p1 = self.nodes[&v1].position(); let p2 = self.nodes[&v2].position(); // Create midpoint nodes let mid01 = (p0 + p1) / 2.0; let mid12 = (p1 + p2) / 2.0; let mid20 = (p2 + p0) / 2.0; // Create the midpoint nodes now, so the sub-cells below reference ids // that exist. // // These ids were previously only *reserved* — computed as // `next_node_id + k` — after which `next_node_id` was advanced by 3 // and `refine_cells` called `add_node` for each midpoint, advancing it // three more times. The nodes were therefore created with ids three // higher than the ones the new cells referenced, so every refined cell // pointed at vertices that did not exist and `validate` rejected the // mesh. let mid01_id = self.add_node(mid01)?; let mid12_id = self.add_node(mid12)?; let mid20_id = self.add_node(mid20)?; // Create 4 new triangles let mut new_cells = Vec::new(); // Corner triangles let triangles = [ vec![v0, mid01_id, mid20_id], vec![v1, mid12_id, mid01_id], vec![v2, mid20_id, mid12_id], vec![mid01_id, mid12_id, mid20_id], // Center triangle ]; // Every vertex now exists in `self.nodes`, so positions are looked up // by id. // // The previous code selected a position by *slot* instead: a // not-yet-created id in the first position became `mid01`, in the // second `mid12`, in the third `mid20`, whichever midpoint the id // actually named. Three of the four sub-triangles had their areas // computed from the wrong points as a result. for triangle in triangles { let p0 = self.nodes[&triangle[0]].position(); let p1 = self.nodes[&triangle[1]].position(); let p2 = self.nodes[&triangle[2]].position(); let centroid = (p0 + p1 + p2) / 3.0; let area = 0.5 * (p1 - p0).cross(&(p2 - p0)).magnitude(); new_cells.push((triangle, centroid, area)); } Ok(SubdivisionResult { // The nodes are already in the mesh; returning them again would // have `refine_cells` add duplicates at fresh ids. nodes: Vec::new(), cells: new_cells, }) } /// Subdivide a quadrilateral into 4 smaller quadrilaterals fn subdivide_quadrilateral(&mut self, cell: &Cell) -> CfdResult { let vertices = cell.vertex_indices(); if vertices.len() != 4 { return Err(CfdError::mesh("Expected quadrilateral for subdivision")); } let [v0, v1, v2, v3] = [vertices[0], vertices[1], vertices[2], vertices[3]]; let p0 = self.nodes[&v0].position(); let p1 = self.nodes[&v1].position(); let p2 = self.nodes[&v2].position(); let p3 = self.nodes[&v3].position(); // Create edge midpoints and cell center let mid01 = (p0 + p1) / 2.0; let mid12 = (p1 + p2) / 2.0; let mid23 = (p2 + p3) / 2.0; let mid30 = (p3 + p0) / 2.0; let center = (p0 + p1 + p2 + p3) / 4.0; // Create the new nodes now, as in `subdivide_triangle` — see the note // there on why reserving ids and letting `refine_cells` add them // separately left every refined cell referencing vertices that did // not exist. let mid01_id = self.add_node(mid01)?; let mid12_id = self.add_node(mid12)?; let mid23_id = self.add_node(mid23)?; let mid30_id = self.add_node(mid30)?; let center_id = self.add_node(center)?; // Create 4 new quadrilaterals let mut new_cells = Vec::new(); let quads = [ vec![v0, mid01_id, center_id, mid30_id], vec![mid01_id, v1, mid12_id, center_id], vec![center_id, mid12_id, v2, mid23_id], vec![mid30_id, center_id, mid23_id, v3], ]; for quad in quads { // Every vertex exists now, so this is a lookup rather than the // previous fallback that mapped *any* new node id to the cell // centre — which collapsed three of each sub-quad's four corners // onto the same point. let positions: Vec<_> = quad.iter().map(|&id| self.nodes[&id].position()).collect(); let centroid = positions .iter() .fold(nalgebra::Vector3::zeros(), |acc, &pos| acc + pos) / 4.0; // Area by splitting the quadrilateral along the 0-2 diagonal. let area = 0.5 * ((positions[1] - positions[0]) .cross(&(positions[2] - positions[0])) .magnitude() + (positions[2] - positions[0]) .cross(&(positions[3] - positions[0])) .magnitude()); new_cells.push((quad, centroid, area)); } Ok(SubdivisionResult { nodes: Vec::new(), cells: new_cells, }) } /// Get cell by ID (helper method) pub fn get_cell_by_id(&self, cell_id: usize) -> CfdResult<&Cell> { self.get_cell(cell_id) } /// Get nodes for a cell (helper method) pub fn get_cell_nodes(&self, cell_id: usize) -> CfdResult> { let cell = self.get_cell(cell_id)?; let mut nodes = Vec::new(); for &vertex_id in cell.vertex_indices() { let node = self .nodes .get(&vertex_id) .ok_or_else(|| CfdError::mesh("Node not found"))?; nodes.push(node); } Ok(nodes) } /// Compute quality histogram pub fn compute_quality_histogram(&self, bins: usize) -> CfdResult> { let mut histogram = vec![0; bins]; for (_, cell) in &self.cells { let nodes = self.get_cell_nodes(cell.id())?; let aspect_ratio = cell.compute_aspect_ratio(&nodes)?; // Map aspect ratio to quality (1.0 = perfect, higher = worse) // Quality = 1.0 / aspect_ratio (clamped between 0 and 1) let quality = (1.0 / aspect_ratio).min(1.0).max(0.0); let bin_index = ((quality * bins as f64) as usize).min(bins - 1); histogram[bin_index] += 1; } Ok(histogram) } /// Get all faces #[must_use] pub fn get_faces(&self) -> Vec<&Face> { self.faces.values().collect() } /// Check if a node exists #[must_use] pub fn has_node(&self, node_id: usize) -> bool { self.nodes.contains_key(&node_id) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_unstructured_mesh_creation() { let mesh = UnstructuredMesh::new(); assert_eq!(mesh.node_count(), 0); assert_eq!(mesh.cell_count(), 0); } #[test] fn test_add_nodes() { let mut mesh = UnstructuredMesh::new(); let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap(); let n2 = mesh.add_node(Vector3::new(1.0, 0.0, 0.0)).unwrap(); let n3 = mesh.add_node(Vector3::new(0.5, 1.0, 0.0)).unwrap(); assert_eq!(n1, 0); assert_eq!(n2, 1); assert_eq!(n3, 2); assert_eq!(mesh.node_count(), 3); } #[test] fn test_triangle_cell() { let mut mesh = UnstructuredMesh::new(); let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap(); let n2 = mesh.add_node(Vector3::new(1.0, 0.0, 0.0)).unwrap(); let n3 = mesh.add_node(Vector3::new(0.5, 1.0, 0.0)).unwrap(); let cell = mesh.add_triangle_cell(n1, n2, n3).unwrap(); assert_eq!(cell, 0); assert_eq!(mesh.cell_count(), 1); let cell_obj = mesh.get_cell(cell).unwrap(); assert_eq!(cell_obj.vertex_count(), 3); assert!(cell_obj.volume() > 0.0); } #[test] fn test_quadrilateral_cell() { let mut mesh = UnstructuredMesh::new(); let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap(); let n2 = mesh.add_node(Vector3::new(1.0, 0.0, 0.0)).unwrap(); let n3 = mesh.add_node(Vector3::new(1.0, 1.0, 0.0)).unwrap(); let n4 = mesh.add_node(Vector3::new(0.0, 1.0, 0.0)).unwrap(); let cell = mesh.add_quadrilateral_cell(n1, n2, n3, n4).unwrap(); let cell_obj = mesh.get_cell(cell).unwrap(); assert_eq!(cell_obj.vertex_count(), 4); assert!((cell_obj.volume() - 1.0).abs() < 1e-10); } #[test] fn test_tetrahedron_cell() { let mut mesh = UnstructuredMesh::new(); let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap(); let n2 = mesh.add_node(Vector3::new(1.0, 0.0, 0.0)).unwrap(); let n3 = mesh.add_node(Vector3::new(0.5, 1.0, 0.0)).unwrap(); let n4 = mesh.add_node(Vector3::new(0.5, 0.5, 1.0)).unwrap(); let cell = mesh.add_tetrahedron_cell(n1, n2, n3, n4).unwrap(); let cell_obj = mesh.get_cell(cell).unwrap(); assert_eq!(cell_obj.vertex_count(), 4); assert!(cell_obj.volume() > 0.0); } #[test] fn test_invalid_cell_creation() { let mut mesh = UnstructuredMesh::new(); let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap(); // Try to create triangle with non-existent nodes assert!(mesh.add_triangle_cell(n1, 999, 1000).is_err()); } #[test] fn test_mesh_bounds() { let mut mesh = UnstructuredMesh::new(); mesh.add_node(Vector3::new(-1.0, -2.0, -3.0)).unwrap(); mesh.add_node(Vector3::new(1.0, 2.0, 3.0)).unwrap(); mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap(); let bounds = mesh.bounds(); assert_eq!(bounds.min, Vector3::new(-1.0, -2.0, -3.0)); assert_eq!(bounds.max, Vector3::new(1.0, 2.0, 3.0)); } #[test] fn test_mesh_validation() { let mut mesh = UnstructuredMesh::new(); let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap(); let n2 = mesh.add_node(Vector3::new(1.0, 0.0, 0.0)).unwrap(); let n3 = mesh.add_node(Vector3::new(0.5, 1.0, 0.0)).unwrap(); mesh.add_triangle_cell(n1, n2, n3).unwrap(); assert!(mesh.validate().is_ok()); } }