Initial commit
This commit is contained in:
@@ -0,0 +1,805 @@
|
||||
// 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<f64>, usize)>,
|
||||
/// New cells created during subdivision (vertices, centroid, volume)
|
||||
cells: Vec<(Vec<usize>, Vector3<f64>, f64)>,
|
||||
}
|
||||
|
||||
/// Unstructured (irregular) mesh for complex geometries
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UnstructuredMesh {
|
||||
/// Nodes storage
|
||||
nodes: IndexMap<usize, Node>,
|
||||
/// Cells storage
|
||||
cells: IndexMap<usize, Cell>,
|
||||
/// Faces storage
|
||||
faces: IndexMap<usize, Face>,
|
||||
/// 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<MeshBounds>,
|
||||
/// 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<f64>) -> CfdResult<usize> {
|
||||
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<usize> {
|
||||
// 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<usize> {
|
||||
// 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<usize> {
|
||||
// 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<usize>,
|
||||
_cell1: Option<usize>,
|
||||
cell2: Option<usize>,
|
||||
) -> CfdResult<usize> {
|
||||
// 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<Vector3<f64>> = 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<Vec<usize>> {
|
||||
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<SubdivisionResult> {
|
||||
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;
|
||||
|
||||
let mid01_id = self.next_node_id;
|
||||
let mid12_id = self.next_node_id + 1;
|
||||
let mid20_id = self.next_node_id + 2;
|
||||
|
||||
let mut new_nodes = Vec::new();
|
||||
new_nodes.push((mid01, mid01_id));
|
||||
new_nodes.push((mid12, mid12_id));
|
||||
new_nodes.push((mid20, mid20_id));
|
||||
|
||||
// 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
|
||||
];
|
||||
|
||||
for triangle in triangles {
|
||||
let p0 = if triangle[0] < self.next_node_id {
|
||||
self.nodes[&triangle[0]].position()
|
||||
} else {
|
||||
mid01 // This is simplified, should look up correct midpoint
|
||||
};
|
||||
let p1 = if triangle[1] < self.next_node_id {
|
||||
self.nodes[&triangle[1]].position()
|
||||
} else {
|
||||
mid12
|
||||
};
|
||||
let p2 = if triangle[2] < self.next_node_id {
|
||||
self.nodes[&triangle[2]].position()
|
||||
} else {
|
||||
mid20
|
||||
};
|
||||
|
||||
let centroid = (p0 + p1 + p2) / 3.0;
|
||||
let v1 = p1 - p0;
|
||||
let v2 = p2 - p0;
|
||||
let area = 0.5 * v1.cross(&v2).magnitude();
|
||||
|
||||
new_cells.push((triangle, centroid, area));
|
||||
}
|
||||
|
||||
self.next_node_id += 3;
|
||||
|
||||
Ok(SubdivisionResult {
|
||||
nodes: new_nodes,
|
||||
cells: new_cells,
|
||||
})
|
||||
}
|
||||
|
||||
/// Subdivide a quadrilateral into 4 smaller quadrilaterals
|
||||
fn subdivide_quadrilateral(&mut self, cell: &Cell) -> CfdResult<SubdivisionResult> {
|
||||
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;
|
||||
|
||||
let mid01_id = self.next_node_id;
|
||||
let mid12_id = self.next_node_id + 1;
|
||||
let mid23_id = self.next_node_id + 2;
|
||||
let mid30_id = self.next_node_id + 3;
|
||||
let center_id = self.next_node_id + 4;
|
||||
|
||||
let mut new_nodes = Vec::new();
|
||||
new_nodes.push((mid01, mid01_id));
|
||||
new_nodes.push((mid12, mid12_id));
|
||||
new_nodes.push((mid23, mid23_id));
|
||||
new_nodes.push((mid30, mid30_id));
|
||||
new_nodes.push((center, center_id));
|
||||
|
||||
// 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 {
|
||||
let positions: Vec<_> = quad
|
||||
.iter()
|
||||
.map(|&id| {
|
||||
if id < self.next_node_id {
|
||||
self.nodes[&id].position()
|
||||
} else {
|
||||
// This is simplified - should properly map new node IDs to positions
|
||||
center
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let centroid = positions
|
||||
.iter()
|
||||
.fold(nalgebra::Vector3::zeros(), |acc, &pos| acc + pos)
|
||||
/ 4.0;
|
||||
|
||||
// Calculate area using 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();
|
||||
let area = area1 + area2;
|
||||
|
||||
new_cells.push((quad, centroid, area));
|
||||
}
|
||||
|
||||
self.next_node_id += 5;
|
||||
|
||||
Ok(SubdivisionResult {
|
||||
nodes: new_nodes,
|
||||
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<Vec<&Node>> {
|
||||
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<Vec<usize>> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user