Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,754 @@
// TDD: GREEN phase - Implement structured 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;
/// Structured (regular) mesh for rectangular domains
#[derive(Debug, Clone)]
pub struct StructuredMesh {
/// Number of nodes in x direction
nx: usize,
/// Number of nodes in y direction
ny: usize,
/// Number of nodes in z direction
nz: usize,
/// Domain width (x direction)
width: f64,
/// Domain height (y direction)
height: f64,
/// Domain depth (z direction)
depth: f64,
/// Grid spacing in x direction
dx: f64,
/// Grid spacing in y direction
dy: f64,
/// Grid spacing in z direction
dz: f64,
/// Nodes storage
nodes: IndexMap<usize, Node>,
/// Cells storage
cells: IndexMap<usize, Cell>,
/// Faces storage
faces: IndexMap<usize, Face>,
/// Whether this is a 2D mesh
is_2d: bool,
}
impl StructuredMesh {
/// Create a new 2D structured mesh
pub fn new(nx: usize, ny: usize, width: f64, height: f64) -> CfdResult<Self> {
if nx < 2 || ny < 2 {
return Err(CfdError::mesh("Mesh dimensions must be at least 2x2"));
}
if width <= 0.0 || height <= 0.0 {
return Err(CfdError::mesh("Mesh dimensions must be positive"));
}
let dx = width / (nx - 1) as f64;
let dy = height / (ny - 1) as f64;
let mut mesh = Self {
nx,
ny,
nz: 1,
width,
height,
depth: 0.0,
dx,
dy,
dz: 0.0,
nodes: IndexMap::new(),
cells: IndexMap::new(),
faces: IndexMap::new(),
is_2d: true,
};
mesh.generate_nodes()?;
mesh.generate_cells()?;
mesh.generate_faces()?;
Ok(mesh)
}
/// Create a new 3D structured mesh
pub fn new_3d(
nx: usize,
ny: usize,
nz: usize,
width: f64,
height: f64,
depth: f64,
) -> CfdResult<Self> {
if nx < 2 || ny < 2 || nz < 2 {
return Err(CfdError::mesh("Mesh dimensions must be at least 2x2x2"));
}
if width <= 0.0 || height <= 0.0 || depth <= 0.0 {
return Err(CfdError::mesh("Mesh dimensions must be positive"));
}
let dx = width / (nx - 1) as f64;
let dy = height / (ny - 1) as f64;
let dz = depth / (nz - 1) as f64;
let mut mesh = Self {
nx,
ny,
nz,
width,
height,
depth,
dx,
dy,
dz,
nodes: IndexMap::new(),
cells: IndexMap::new(),
faces: IndexMap::new(),
is_2d: false,
};
mesh.generate_nodes()?;
mesh.generate_cells()?;
mesh.generate_faces()?;
Ok(mesh)
}
/// Get number of nodes in x direction
#[must_use]
pub fn nx(&self) -> usize {
self.nx
}
/// Get number of nodes in y direction
#[must_use]
pub fn ny(&self) -> usize {
self.ny
}
/// Get number of nodes in z direction
#[must_use]
pub fn nz(&self) -> usize {
self.nz
}
/// Get domain width
#[must_use]
pub fn width(&self) -> f64 {
self.width
}
/// Get domain height
#[must_use]
pub fn height(&self) -> f64 {
self.height
}
/// Get domain depth
#[must_use]
pub fn depth(&self) -> f64 {
self.depth
}
/// Get grid spacing in x direction
#[must_use]
pub fn dx(&self) -> f64 {
self.dx
}
/// Get grid spacing in y direction
#[must_use]
pub fn dy(&self) -> f64 {
self.dy
}
/// Get grid spacing in z direction
#[must_use]
pub fn dz(&self) -> f64 {
self.dz
}
/// Convert (i,j,k) indices to linear node index
fn node_index(&self, i: usize, j: usize, k: usize) -> usize {
k * self.nx * self.ny + j * self.nx + i
}
/// Convert (i,j,k) indices to linear cell index
fn cell_index(&self, i: usize, j: usize, k: usize) -> usize {
k * (self.nx - 1) * (self.ny - 1) + j * (self.nx - 1) + i
}
/// Get node at (i,j,k) coordinates
pub fn get_node(&self, i: usize, j: usize, k: usize) -> CfdResult<&Node> {
if i >= self.nx || j >= self.ny || k >= self.nz {
return Err(CfdError::mesh("Node indices out of bounds"));
}
let index = self.node_index(i, j, k);
self.nodes
.get(&index)
.ok_or_else(|| CfdError::mesh("Node not found"))
}
/// Get cell at (i,j,k) coordinates
pub fn get_cell(&self, i: usize, j: usize, k: usize) -> CfdResult<&Cell> {
let nz_cells = if self.is_2d { 1 } else { self.nz - 1 };
if i >= self.nx - 1 || j >= self.ny - 1 || k >= nz_cells {
return Err(CfdError::mesh("Cell indices out of bounds"));
}
let index = self.cell_index(i, j, k);
self.cells
.get(&index)
.ok_or_else(|| CfdError::mesh("Cell not found"))
}
/// Generate all nodes
fn generate_nodes(&mut self) -> CfdResult<()> {
for k in 0..self.nz {
for j in 0..self.ny {
for i in 0..self.nx {
let x = i as f64 * self.dx;
let y = j as f64 * self.dy;
let z = if self.is_2d { 0.0 } else { k as f64 * self.dz };
let position = Vector3::new(x, y, z);
let index = self.node_index(i, j, k);
let node = Node::new(index, position);
self.nodes.insert(index, node);
}
}
}
Ok(())
}
/// Generate all cells
fn generate_cells(&mut self) -> CfdResult<()> {
let nz_cells = if self.is_2d { 1 } else { self.nz - 1 };
for k in 0..nz_cells {
for j in 0..self.ny - 1 {
for i in 0..self.nx - 1 {
let cell_id = self.cell_index(i, j, k);
if self.is_2d {
// 2D quadrilateral cell
let vertices = vec![
self.node_index(i, j, 0),
self.node_index(i + 1, j, 0),
self.node_index(i + 1, j + 1, 0),
self.node_index(i, j + 1, 0),
];
let centroid = Vector3::new(
(i as f64 + 0.5) * self.dx,
(j as f64 + 0.5) * self.dy,
0.0,
);
let volume = self.dx * self.dy; // Area for 2D
let mut cell = Cell::new(cell_id, vertices, centroid, volume);
// Check if cell is on boundary
if i == 0 || i == self.nx - 2 || j == 0 || j == self.ny - 2 {
cell.set_boundary(true);
}
self.cells.insert(cell_id, cell);
} else {
// 3D hexahedral cell
let vertices = vec![
self.node_index(i, j, k),
self.node_index(i + 1, j, k),
self.node_index(i + 1, j + 1, k),
self.node_index(i, j + 1, k),
self.node_index(i, j, k + 1),
self.node_index(i + 1, j, k + 1),
self.node_index(i + 1, j + 1, k + 1),
self.node_index(i, j + 1, k + 1),
];
let centroid = Vector3::new(
(i as f64 + 0.5) * self.dx,
(j as f64 + 0.5) * self.dy,
(k as f64 + 0.5) * self.dz,
);
let volume = self.dx * self.dy * self.dz;
let mut cell = Cell::new(cell_id, vertices, centroid, volume);
// Check if cell is on boundary
if i == 0
|| i == self.nx - 2
|| j == 0
|| j == self.ny - 2
|| k == 0
|| k == self.nz - 2
{
cell.set_boundary(true);
}
self.cells.insert(cell_id, cell);
}
}
}
}
Ok(())
}
/// Generate all faces
fn generate_faces(&mut self) -> CfdResult<()> {
let mut face_id = 0;
if self.is_2d {
// Generate 2D faces (edges)
// Horizontal faces
for j in 0..self.ny {
for i in 0..self.nx - 1 {
let vertices = vec![self.node_index(i, j, 0), self.node_index(i + 1, j, 0)];
let centroid =
Vector3::new((i as f64 + 0.5) * self.dx, j as f64 * self.dy, 0.0);
let area = self.dx;
let normal = Vector3::new(
0.0,
if j == 0 {
-1.0
} else if j == self.ny - 1 {
1.0
} else {
0.0
},
0.0,
);
let is_boundary = j == 0 || j == self.ny - 1;
let face = if is_boundary {
Face::new_boundary(face_id, vertices, centroid, area, normal)
} else {
Face::new(face_id, vertices, centroid, area)
};
self.faces.insert(face_id, face);
face_id += 1;
}
}
// Vertical faces
for j in 0..self.ny - 1 {
for i in 0..self.nx {
let vertices = vec![self.node_index(i, j, 0), self.node_index(i, j + 1, 0)];
let centroid =
Vector3::new(i as f64 * self.dx, (j as f64 + 0.5) * self.dy, 0.0);
let area = self.dy;
let normal = Vector3::new(
if i == 0 {
-1.0
} else if i == self.nx - 1 {
1.0
} else {
0.0
},
0.0,
0.0,
);
let is_boundary = i == 0 || i == self.nx - 1;
let face = if is_boundary {
Face::new_boundary(face_id, vertices, centroid, area, normal)
} else {
Face::new(face_id, vertices, centroid, area)
};
self.faces.insert(face_id, face);
face_id += 1;
}
}
} else {
// 3D face generation - create faces for all 6 directions
self.generate_3d_faces(&mut face_id)?;
}
Ok(())
}
/// Generate 3D faces for structured mesh
fn generate_3d_faces(&mut self, face_id: &mut usize) -> CfdResult<()> {
// X-direction faces (YZ planes)
for k in 0..self.nz - 1 {
for j in 0..self.ny - 1 {
for i in 0..self.nx {
let vertices = vec![
self.node_index(i, j, k),
self.node_index(i, j + 1, k),
self.node_index(i, j + 1, k + 1),
self.node_index(i, j, k + 1),
];
let centroid = Vector3::new(
i as f64 * self.dx,
(j as f64 + 0.5) * self.dy,
(k as f64 + 0.5) * self.dz,
);
let area = self.dy * self.dz;
let is_boundary = i == 0 || i == self.nx - 1;
let normal = Vector3::new(
if i == 0 {
-1.0
} else if i == self.nx - 1 {
1.0
} else {
0.0
},
0.0,
0.0,
);
let face = if is_boundary {
Face::new_boundary(*face_id, vertices, centroid, area, normal)
} else {
Face::new(*face_id, vertices, centroid, area)
};
self.faces.insert(*face_id, face);
*face_id += 1;
}
}
}
// Y-direction faces (XZ planes)
for k in 0..self.nz - 1 {
for j in 0..self.ny {
for i in 0..self.nx - 1 {
let vertices = vec![
self.node_index(i, j, k),
self.node_index(i, j, k + 1),
self.node_index(i + 1, j, k + 1),
self.node_index(i + 1, j, k),
];
let centroid = Vector3::new(
(i as f64 + 0.5) * self.dx,
j as f64 * self.dy,
(k as f64 + 0.5) * self.dz,
);
let area = self.dx * self.dz;
let is_boundary = j == 0 || j == self.ny - 1;
let normal = Vector3::new(
0.0,
if j == 0 {
-1.0
} else if j == self.ny - 1 {
1.0
} else {
0.0
},
0.0,
);
let face = if is_boundary {
Face::new_boundary(*face_id, vertices, centroid, area, normal)
} else {
Face::new(*face_id, vertices, centroid, area)
};
self.faces.insert(*face_id, face);
*face_id += 1;
}
}
}
// Z-direction faces (XY planes)
for k in 0..self.nz {
for j in 0..self.ny - 1 {
for i in 0..self.nx - 1 {
let vertices = vec![
self.node_index(i, j, k),
self.node_index(i + 1, j, k),
self.node_index(i + 1, j + 1, k),
self.node_index(i, j + 1, k),
];
let centroid = Vector3::new(
(i as f64 + 0.5) * self.dx,
(j as f64 + 0.5) * self.dy,
k as f64 * self.dz,
);
let area = self.dx * self.dy;
let is_boundary = k == 0 || k == self.nz - 1;
let normal = Vector3::new(
0.0,
0.0,
if k == 0 {
-1.0
} else if k == self.nz - 1 {
1.0
} else {
0.0
},
);
let face = if is_boundary {
Face::new_boundary(*face_id, vertices, centroid, area, normal)
} else {
Face::new(*face_id, vertices, centroid, area)
};
self.faces.insert(*face_id, face);
*face_id += 1;
}
}
}
Ok(())
}
/// 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)
}
}
impl Mesh for StructuredMesh {
fn cell_count(&self) -> usize {
self.cells.len()
}
fn node_count(&self) -> usize {
self.nodes.len()
}
fn bounds(&self) -> MeshBounds {
MeshBounds::new(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(self.width, self.height, self.depth),
)
}
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 mesh connectivity
if self.cells.is_empty() {
return Err(CfdError::mesh("Mesh has no cells"));
}
if self.nodes.is_empty() {
return Err(CfdError::mesh("Mesh has no nodes"));
}
Ok(())
}
fn statistics(&self) -> MeshStatistics {
let mut stats = MeshStatistics::new();
stats.total_cells = self.cells.len();
stats.total_nodes = self.nodes.len();
stats.total_faces = self.faces.len();
// Calculate volume statistics
if !self.cells.is_empty() {
let volumes: Vec<f64> = self
.cells
.values()
.map(super::super::traits::MeshEntity::volume)
.collect();
stats.min_cell_volume = volumes.iter().fold(f64::INFINITY, |a, &b| a.min(b));
stats.max_cell_volume = volumes.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
stats.average_cell_volume = volumes.iter().sum::<f64>() / volumes.len() as f64;
}
// Count boundary faces
stats.boundary_faces = self
.faces
.values()
.filter(|face| face.is_boundary())
.count();
// Calculate aspect ratio
let dimensions = self.bounds().dimensions();
let max_dim = dimensions.x.max(dimensions.y).max(dimensions.z);
let min_dim = dimensions.x.min(dimensions.y).min(dimensions.z.max(1e-10));
stats.aspect_ratio = max_dim / min_dim;
stats
}
fn is_boundary_cell(&self, cell_id: usize) -> bool {
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>> {
// For structured mesh, we can calculate neighbors directly
let mut neighbors = Vec::new();
// Find the (i,j,k) coordinates of this cell
let nz_cells = if self.is_2d { 1 } else { self.nz - 1 };
for k in 0..nz_cells {
for j in 0..self.ny - 1 {
for i in 0..self.nx - 1 {
if self.cell_index(i, j, k) == cell_id {
// Add neighboring cells
if i > 0 {
neighbors.push(self.cell_index(i - 1, j, k));
}
if i < self.nx - 2 {
neighbors.push(self.cell_index(i + 1, j, k));
}
if j > 0 {
neighbors.push(self.cell_index(i, j - 1, k));
}
if j < self.ny - 2 {
neighbors.push(self.cell_index(i, j + 1, k));
}
if !self.is_2d {
if k > 0 {
neighbors.push(self.cell_index(i, j, k - 1));
}
if k < nz_cells - 1 {
neighbors.push(self.cell_index(i, j, k + 1));
}
}
return Ok(neighbors);
}
}
}
}
Err(CfdError::mesh("Cell not found"))
}
fn refine(&mut self) -> CfdResult<()> {
// Simple uniform refinement - double the resolution
let new_nx = (self.nx - 1) * 2 + 1;
let new_ny = (self.ny - 1) * 2 + 1;
if self.is_2d {
*self = Self::new(new_nx, new_ny, self.width, self.height)?;
} else {
let new_nz = (self.nz - 1) * 2 + 1;
*self = Self::new_3d(new_nx, new_ny, new_nz, self.width, self.height, self.depth)?;
}
Ok(())
}
fn coarsen(&mut self) -> CfdResult<()> {
// Simple uniform coarsening - halve the resolution
// Ensure we have enough cells to coarsen
if self.nx < 3 || self.ny < 3 {
return Err(CfdError::mesh("Mesh too small to coarsen"));
}
let new_nx = (self.nx - 1) / 2 + 1;
let new_ny = (self.ny - 1) / 2 + 1;
if new_nx < 2 || new_ny < 2 {
return Err(CfdError::mesh("Coarsening would result in invalid mesh"));
}
if self.is_2d {
*self = Self::new(new_nx, new_ny, self.width, self.height)?;
} else {
if self.nz < 3 {
return Err(CfdError::mesh("3D mesh too small to coarsen"));
}
let new_nz = (self.nz - 1) / 2 + 1;
if new_nz < 2 {
return Err(CfdError::mesh("Coarsening would result in invalid 3D mesh"));
}
*self = Self::new_3d(new_nx, new_ny, new_nz, self.width, self.height, self.depth)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_structured_mesh_2d() {
let mesh = StructuredMesh::new(3, 3, 1.0, 1.0).unwrap();
assert_eq!(mesh.nx(), 3);
assert_eq!(mesh.ny(), 3);
assert_eq!(mesh.cell_count(), 4); // (3-1) * (3-1)
assert_eq!(mesh.node_count(), 9); // 3 * 3
}
#[test]
fn test_structured_mesh_3d() {
let mesh = StructuredMesh::new_3d(2, 2, 2, 1.0, 1.0, 1.0).unwrap();
assert_eq!(mesh.nx(), 2);
assert_eq!(mesh.ny(), 2);
assert_eq!(mesh.nz(), 2);
assert_eq!(mesh.cell_count(), 1); // (2-1) * (2-1) * (2-1)
assert_eq!(mesh.node_count(), 8); // 2 * 2 * 2
}
#[test]
fn test_invalid_dimensions() {
assert!(StructuredMesh::new(0, 5, 1.0, 1.0).is_err());
assert!(StructuredMesh::new(5, 0, 1.0, 1.0).is_err());
assert!(StructuredMesh::new(5, 5, 0.0, 1.0).is_err());
assert!(StructuredMesh::new(5, 5, 1.0, 0.0).is_err());
}
#[test]
fn test_node_access() {
let mesh = StructuredMesh::new(3, 3, 1.0, 1.0).unwrap();
let node = mesh.get_node(0, 0, 0).unwrap();
assert_eq!(node.position(), Vector3::new(0.0, 0.0, 0.0));
let node = mesh.get_node(2, 2, 0).unwrap();
assert_eq!(node.position(), Vector3::new(1.0, 1.0, 0.0));
}
#[test]
fn test_cell_access() {
let mesh = StructuredMesh::new(3, 3, 1.0, 1.0).unwrap();
let cell = mesh.get_cell(0, 0, 0).unwrap();
assert_eq!(cell.vertex_count(), 4);
assert!((cell.volume() - 0.25).abs() < 1e-10);
}
#[test]
fn test_mesh_bounds() {
let mesh = StructuredMesh::new(4, 5, 2.0, 3.0).unwrap();
let bounds = mesh.bounds();
assert_eq!(bounds.min, Vector3::new(0.0, 0.0, 0.0));
assert_eq!(bounds.max, Vector3::new(2.0, 3.0, 0.0));
}
#[test]
fn test_mesh_refinement() {
let mut mesh = StructuredMesh::new(3, 3, 1.0, 1.0).unwrap();
let original_cells = mesh.cell_count();
mesh.refine().unwrap();
assert_eq!(mesh.cell_count(), original_cells * 4);
}
}