//! Flow field data structures and operations //! //! This module defines the core data structures for storing and manipulating //! flow field variables (velocity, pressure) on structured grids. use crate::{CfdError, CfdResult}; use nalgebra::DMatrix; /// Flow field containing all flow variables on a structured grid #[derive(Debug, Clone)] pub struct FlowField { /// Grid dimensions pub nx: usize, pub ny: usize, /// Grid spacing pub dx: f64, pub dy: f64, /// x-component of velocity (u) - located at cell faces (i+1/2, j) pub u: DMatrix, /// y-component of velocity (v) - located at cell faces (i, j+1/2) pub v: DMatrix, /// Pressure (p) - located at cell centers (i, j) pub p: DMatrix, /// Previous time step values for time integration pub u_old: DMatrix, pub v_old: DMatrix, pub p_old: DMatrix, /// Auxiliary fields for solver algorithms pub u_star: DMatrix, // Predicted velocity (SIMPLE/PISO) pub v_star: DMatrix, // Predicted velocity (SIMPLE/PISO) pub p_prime: DMatrix, // Pressure correction (SIMPLE/PISO) /// Source terms pub su: DMatrix, // u-momentum source pub sv: DMatrix, // v-momentum source pub sp: DMatrix, // Pressure source (mass source) } impl FlowField { /// Create new flow field with given dimensions pub fn new(nx: usize, ny: usize, dx: f64, dy: f64) -> CfdResult { if nx < 3 || ny < 3 { return Err(CfdError::invalid_parameter("Grid must be at least 3x3")); } if dx <= 0.0 || dy <= 0.0 { return Err(CfdError::invalid_parameter("Grid spacing must be positive")); } // For staggered grid: // u: (nx+1, ny) - face-centered in x-direction // v: (nx, ny+1) - face-centered in y-direction // p: (nx, ny) - cell-centered let zeros_u = DMatrix::zeros(ny, nx + 1); // Note: nalgebra is (rows, cols) let zeros_v = DMatrix::zeros(ny + 1, nx); let zeros_p = DMatrix::zeros(ny, nx); Ok(Self { nx, ny, dx, dy, u: zeros_u.clone(), v: zeros_v.clone(), p: zeros_p.clone(), u_old: zeros_u.clone(), v_old: zeros_v.clone(), p_old: zeros_p.clone(), u_star: zeros_u.clone(), v_star: zeros_v.clone(), p_prime: zeros_p.clone(), su: zeros_u, sv: zeros_v, sp: zeros_p, }) } /// Set velocity at a given grid point pub fn set_velocity(&mut self, i: usize, j: usize, u_val: f64, v_val: f64) -> CfdResult<()> { if i >= self.nx || j >= self.ny { return Err(CfdError::invalid_parameter("Grid indices out of bounds")); } // For staggered grid, velocity components are at different locations // u is stored at (j, i) for face (i+1/2, j) if i < self.nx { self.u[(j, i)] = u_val; } // v is stored at (j, i) for face (i, j+1/2) if j < self.ny { self.v[(j, i)] = v_val; } Ok(()) } /// Get velocity at a given grid point (interpolated to cell center) pub fn get_velocity_at(&self, i: usize, j: usize) -> CfdResult<(f64, f64)> { if i >= self.nx || j >= self.ny { return Err(CfdError::invalid_parameter("Grid indices out of bounds")); } // Interpolate velocities to cell center let u_center = if i == 0 { self.u[(j, 0)] } else if i == self.nx - 1 { self.u[(j, self.nx - 1)] } else { 0.5 * (self.u[(j, i - 1)] + self.u[(j, i)]) }; let v_center = if j == 0 { self.v[(0, i)] } else if j == self.ny - 1 { self.v[(self.ny - 1, i)] } else { 0.5 * (self.v[(j - 1, i)] + self.v[(j, i)]) }; Ok((u_center, v_center)) } /// Set pressure at a given grid point pub fn set_pressure(&mut self, i: usize, j: usize, p_val: f64) -> CfdResult<()> { if i >= self.nx || j >= self.ny { return Err(CfdError::invalid_parameter("Grid indices out of bounds")); } self.p[(j, i)] = p_val; Ok(()) } /// Get pressure at a given grid point pub fn get_pressure_at(&self, i: usize, j: usize) -> CfdResult { if i >= self.nx || j >= self.ny { return Err(CfdError::invalid_parameter("Grid indices out of bounds")); } Ok(self.p[(j, i)]) } /// Apply boundary conditions to the flow field pub fn apply_boundary_conditions(&mut self, bcs: &super::BoundaryConditions) -> CfdResult<()> { bcs.apply_to_flow_field(self) } /// Compute divergence of velocity field (mass conservation check) pub fn compute_divergence(&self) -> CfdResult> { let mut divergence = DMatrix::zeros(self.ny, self.nx); for j in 0..self.ny { for i in 0..self.nx { // ∇·u = ∂u/∂x + ∂v/∂y let du_dx = if i == self.nx - 1 { (self.u[(j, i)] - self.u[(j, i - 1)]) / self.dx } else { (self.u[(j, i + 1)] - self.u[(j, i)]) / self.dx }; let dv_dy = if j == self.ny - 1 { (self.v[(j, i)] - self.v[(j - 1, i)]) / self.dy } else { (self.v[(j + 1, i)] - self.v[(j, i)]) / self.dy }; divergence[(j, i)] = du_dx + dv_dy; } } Ok(divergence) } /// Compute maximum divergence (for mass conservation check) pub fn compute_max_divergence(&self) -> CfdResult { let divergence = self.compute_divergence()?; Ok(divergence.iter().map(|&x| x.abs()).fold(0.0, f64::max)) } /// Find maximum u-velocity and its location pub fn find_max_u_velocity(&self) -> CfdResult<(f64, (usize, usize))> { let mut max_u = f64::NEG_INFINITY; let mut max_loc = (0, 0); for j in 0..self.ny { for i in 0..=self.nx { if self.u[(j, i)] > max_u { max_u = self.u[(j, i)]; max_loc = (i, j); } } } Ok((max_u, max_loc)) } /// Compute total kinetic energy pub fn compute_kinetic_energy(&self) -> CfdResult { let mut ke = 0.0; for j in 0..self.ny { for i in 0..self.nx { let (u_center, v_center) = self.get_velocity_at(i, j)?; ke += 0.5 * (u_center * u_center + v_center * v_center) * self.dx * self.dy; } } Ok(ke) } /// Update old values (for time stepping) pub fn update_old_values(&mut self) { self.u_old.copy_from(&self.u); self.v_old.copy_from(&self.v); self.p_old.copy_from(&self.p); } /// Copy current values to starred values (for predictor step) pub fn copy_to_starred(&mut self) { self.u_star.copy_from(&self.u); self.v_star.copy_from(&self.v); } /// Apply under-relaxation to velocity field pub fn apply_velocity_relaxation(&mut self, relaxation_factor: f64) -> CfdResult<()> { if relaxation_factor <= 0.0 || relaxation_factor > 1.0 { return Err(CfdError::invalid_parameter( "Relaxation factor must be in (0, 1]", )); } // u = α * u_new + (1 - α) * u_old for j in 0..self.ny { for i in 0..=self.nx { self.u[(j, i)] = relaxation_factor * self.u[(j, i)] + (1.0 - relaxation_factor) * self.u_old[(j, i)]; } } for j in 0..=self.ny { for i in 0..self.nx { self.v[(j, i)] = relaxation_factor * self.v[(j, i)] + (1.0 - relaxation_factor) * self.v_old[(j, i)]; } } Ok(()) } /// Apply under-relaxation to pressure field pub fn apply_pressure_relaxation(&mut self, relaxation_factor: f64) -> CfdResult<()> { if relaxation_factor <= 0.0 || relaxation_factor > 1.0 { return Err(CfdError::invalid_parameter( "Relaxation factor must be in (0, 1]", )); } for j in 0..self.ny { for i in 0..self.nx { self.p[(j, i)] = relaxation_factor * self.p[(j, i)] + (1.0 - relaxation_factor) * self.p_old[(j, i)]; } } Ok(()) } /// Compute L2 norm of residual #[must_use] pub fn compute_velocity_residual(&self) -> f64 { let mut residual = 0.0; // u-momentum residual for j in 0..self.ny { for i in 0..=self.nx { let diff = self.u[(j, i)] - self.u_old[(j, i)]; residual += diff * diff; } } // v-momentum residual for j in 0..=self.ny { for i in 0..self.nx { let diff = self.v[(j, i)] - self.v_old[(j, i)]; residual += diff * diff; } } residual.sqrt() } /// Compute pressure residual #[must_use] pub fn compute_pressure_residual(&self) -> f64 { let mut residual = 0.0; for j in 0..self.ny { for i in 0..self.nx { let diff = self.p[(j, i)] - self.p_old[(j, i)]; residual += diff * diff; } } residual.sqrt() } /// Get grid information #[must_use] pub fn grid_info(&self) -> (usize, usize, f64, f64) { (self.nx, self.ny, self.dx, self.dy) } /// Initialize with analytical solution (for testing) pub fn initialize_with_analytical( &mut self, solution_type: AnalyticalSolution, ) -> CfdResult<()> { match solution_type { AnalyticalSolution::PoiseuillePlane { u_max } => { // Plane Poiseuille flow: u(y) = u_max * 4 * y * (1-y) for j in 0..self.ny { let y = (j as f64 + 0.5) * self.dy; // Cell center y-coordinate let y_normalized = y / (self.ny as f64 * self.dy); let u_analytical = u_max * 4.0 * y_normalized * (1.0 - y_normalized); for i in 0..=self.nx { self.u[(j, i)] = u_analytical; } } // v = 0 everywhere self.v.fill(0.0); // Pressure gradient to drive the flow for j in 0..self.ny { for i in 0..self.nx { self.p[(j, i)] = -(i as f64) * self.dx; // Linear pressure drop } } } AnalyticalSolution::TaylorGreenVortex { amplitude } => { // Taylor-Green vortex: analytical solution for 2D Navier-Stokes for j in 0..self.ny { for i in 0..=self.nx { let x = i as f64 * self.dx; let y = (j as f64 + 0.5) * self.dy; self.u[(j, i)] = amplitude * (2.0 * std::f64::consts::PI * x).sin() * (2.0 * std::f64::consts::PI * y).cos(); } } for j in 0..=self.ny { for i in 0..self.nx { let x = (i as f64 + 0.5) * self.dx; let y = j as f64 * self.dy; self.v[(j, i)] = -amplitude * (2.0 * std::f64::consts::PI * x).cos() * (2.0 * std::f64::consts::PI * y).sin(); } } // Pressure field for Taylor-Green vortex for j in 0..self.ny { for i in 0..self.nx { let x = (i as f64 + 0.5) * self.dx; let y = (j as f64 + 0.5) * self.dy; self.p[(j, i)] = -0.25 * amplitude * amplitude * ((4.0 * std::f64::consts::PI * x).cos() + (4.0 * std::f64::consts::PI * y).cos()); } } } } Ok(()) } } /// Analytical solutions for testing and validation #[derive(Debug, Clone, Copy)] pub enum AnalyticalSolution { /// Plane Poiseuille flow between parallel plates PoiseuillePlane { u_max: f64 }, /// Taylor-Green vortex (decaying vortex solution) TaylorGreenVortex { amplitude: f64 }, }