//! SIMPLE (Semi-Implicit Method for Pressure Linked Equations) algorithm //! //! The SIMPLE algorithm is a widely-used iterative solution method for //! the incompressible Navier-Stokes equations. It uses a pressure-velocity //! coupling approach to handle the incompressibility constraint. //! //! Algorithm steps: //! 1. Solve momentum equations with guessed pressure field → u*, v* //! 2. Solve pressure correction equation → p' //! 3. Correct velocities and pressure //! 4. Check convergence and iterate use super::{BoundaryConditions, FlowField, IncompressibleSolver, SolverResult}; use crate::turbulence::{KEpsilonModel, KEpsilonVariant, TurbulenceModel, TurbulenceState}; use crate::{CfdConfig, CfdError, CfdResult}; use async_trait::async_trait; use nalgebra::{DMatrix, DVector, Vector3}; use std::time::Instant; /// Parameters for SIMPLE algorithm #[derive(Debug, Clone)] pub struct SimpleParameters { /// Under-relaxation factor for pressure (typically 0.2-0.8) pub pressure_relaxation: f64, /// Under-relaxation factor for velocity (typically 0.5-0.8) pub velocity_relaxation: f64, /// Maximum number of iterations pub max_iterations: usize, /// Convergence tolerance for residuals pub tolerance: f64, /// Time step for transient problems pub time_step: f64, /// Maximum Courant number for stability pub max_courant: f64, /// Enable turbulence modeling pub use_turbulence: bool, } impl SimpleParameters { /// Create new SIMPLE parameters with default values #[must_use] pub fn new() -> Self { Self::default() } /// Set pressure under-relaxation factor #[must_use] pub fn with_pressure_relaxation(mut self, factor: f64) -> Self { self.pressure_relaxation = factor.max(0.0); // Ensure non-negative self } /// Set velocity under-relaxation factor #[must_use] pub fn with_velocity_relaxation(mut self, factor: f64) -> Self { self.velocity_relaxation = factor.max(0.0); self } /// Set maximum iterations #[must_use] pub fn with_max_iterations(mut self, max_iter: usize) -> Self { self.max_iterations = max_iter; self } /// Set convergence tolerance #[must_use] pub fn with_tolerance(mut self, tol: f64) -> Self { self.tolerance = tol.abs(); self } /// Set time step #[must_use] pub fn with_time_step(mut self, dt: f64) -> Self { self.time_step = dt.abs(); self } /// Validate parameters pub fn validate(&self) -> CfdResult<()> { if self.pressure_relaxation <= 0.0 || self.pressure_relaxation > 1.0 { return Err(CfdError::invalid_parameter( "Pressure relaxation factor must be in (0, 1]", )); } if self.velocity_relaxation <= 0.0 || self.velocity_relaxation > 1.0 { return Err(CfdError::invalid_parameter( "Velocity relaxation factor must be in (0, 1]", )); } if self.tolerance <= 0.0 { return Err(CfdError::invalid_parameter("Tolerance must be positive")); } if self.time_step <= 0.0 { return Err(CfdError::invalid_parameter("Time step must be positive")); } Ok(()) } } impl Default for SimpleParameters { fn default() -> Self { Self { pressure_relaxation: 0.3, velocity_relaxation: 0.7, max_iterations: 1000, tolerance: 1e-6, time_step: 0.001, max_courant: 1.0, use_turbulence: false, } } } /// Result of SIMPLE algorithm execution #[derive(Debug, Clone)] pub struct SimpleResult { /// Base solver result information pub solver_result: SolverResult, /// Pressure correction iterations per SIMPLE iteration pub pressure_iterations: Vec, /// Final mass residual pub mass_residual: f64, /// Final momentum residual pub momentum_residual: f64, } /// SIMPLE algorithm implementation pub struct SimpleSolver { /// CFD configuration config: CfdConfig, /// SIMPLE parameters parameters: SimpleParameters, /// Linear algebra workspace workspace: LinearAlgebraWorkspace, /// Turbulence model (optional) turbulence_model: Option, } /// Workspace for linear algebra operations struct LinearAlgebraWorkspace { /// Matrix for pressure correction equation pressure_matrix: Option>, /// RHS vector for pressure correction pressure_rhs: Option>, /// Solution vector for pressure correction pressure_solution: Option>, /// Momentum equation coefficients momentum_coefficients: Option, } /// Coefficients for momentum equations discretization #[derive(Debug, Clone)] struct MomentumCoefficients { /// Central coefficient (diagonal) pub ap: DMatrix, /// East neighbor coefficient pub ae: DMatrix, /// West neighbor coefficient pub aw: DMatrix, /// North neighbor coefficient pub an: DMatrix, /// South neighbor coefficient pub as_: DMatrix, /// Source term pub su: DMatrix, } impl SimpleSolver { /// Create new SIMPLE solver pub fn new(config: CfdConfig, parameters: SimpleParameters) -> CfdResult { config.validate()?; parameters.validate()?; // Initialize turbulence model if enabled (will be properly sized when flow field is available) let turbulence_model = if parameters.use_turbulence { None // Will be initialized when flow field dimensions are known } else { None }; Ok(Self { config, parameters, workspace: LinearAlgebraWorkspace { pressure_matrix: None, pressure_rhs: None, pressure_solution: None, momentum_coefficients: None, }, turbulence_model, }) } /// Solve one SIMPLE iteration pub async fn solve_simple_iteration( &mut self, flow_field: &mut FlowField, boundary_conditions: &BoundaryConditions, dt: f64, ) -> CfdResult<(f64, f64)> { // Step 1: Solve momentum equations with current pressure field self.momentum_prediction_step(flow_field, dt).await?; // Step 2: Solve pressure correction equation let mass_residual = self.pressure_correction_step(flow_field).await?; // Step 3: Correct velocities self.velocity_correction_step(flow_field).await?; // Step 4: Update pressure field self.pressure_update_step(flow_field).await?; // Step 5: Solve turbulence equations if enabled if self.parameters.use_turbulence { self.solve_turbulence(flow_field, dt).await?; } // Step 6: Apply boundary conditions flow_field.apply_boundary_conditions(boundary_conditions)?; // Step 7: Apply under-relaxation flow_field.apply_velocity_relaxation(self.parameters.velocity_relaxation)?; flow_field.apply_pressure_relaxation(self.parameters.pressure_relaxation)?; // Compute momentum residual let momentum_residual = flow_field.compute_velocity_residual(); Ok((mass_residual, momentum_residual)) } /// Momentum prediction step: solve momentum equations with current pressure pub async fn momentum_prediction_step( &self, flow_field: &mut FlowField, dt: f64, ) -> CfdResult<()> { let (_nx, _ny, dx, dy) = flow_field.grid_info(); let rho = self.config.density; let mu = self.config.viscosity; // Copy current velocities to old values for time derivatives flow_field.update_old_values(); // Solve u-momentum equation self.solve_u_momentum(flow_field, dt, rho, mu, dx, dy) .await?; // Solve v-momentum equation self.solve_v_momentum(flow_field, dt, rho, mu, dx, dy) .await?; // Store predicted velocities flow_field.copy_to_starred(); Ok(()) } /// Solve u-momentum equation async fn solve_u_momentum( &self, flow_field: &mut FlowField, dt: f64, rho: f64, mu: f64, dx: f64, dy: f64, ) -> CfdResult<()> { let (nx, ny, _, _) = flow_field.grid_info(); // For each u-velocity point (face-centered) for j in 1..ny - 1 { for i in 1..nx { // u goes from 1 to nx-1 for interior // Discretize u-momentum equation at (i, j) let coeffs = self.compute_u_momentum_coefficients(flow_field, i, j, dt, rho, mu, dx, dy)?; // Solve for new u velocity using Gauss-Seidel let u_new = (coeffs.source + coeffs.east * flow_field.u[(j, i + 1)] + coeffs.west * flow_field.u[(j, i - 1)] + coeffs.north * flow_field.u[(j + 1, i)] + coeffs.south * flow_field.u[(j - 1, i)]) / coeffs.center; flow_field.u[(j, i)] = u_new; } } Ok(()) } /// Solve v-momentum equation async fn solve_v_momentum( &self, flow_field: &mut FlowField, dt: f64, rho: f64, mu: f64, dx: f64, dy: f64, ) -> CfdResult<()> { let (nx, ny, _, _) = flow_field.grid_info(); // For each v-velocity point (face-centered) for j in 1..ny { // v goes from 1 to ny-1 for interior for i in 1..nx - 1 { // Discretize v-momentum equation at (i, j) let coeffs = self.compute_v_momentum_coefficients(flow_field, i, j, dt, rho, mu, dx, dy)?; // Solve for new v velocity using Gauss-Seidel let v_new = (coeffs.source + coeffs.east * flow_field.v[(j, i + 1)] + coeffs.west * flow_field.v[(j, i - 1)] + coeffs.north * flow_field.v[(j + 1, i)] + coeffs.south * flow_field.v[(j - 1, i)]) / coeffs.center; flow_field.v[(j, i)] = v_new; } } Ok(()) } /// Pressure correction step: solve pressure Poisson equation pub async fn pressure_correction_step(&self, flow_field: &mut FlowField) -> CfdResult { let (nx, ny, dx, dy) = flow_field.grid_info(); let rho = self.config.density; // Setup pressure correction equation: ∇²p' = ρ/Δt * ∇·u* for j in 1..ny - 1 { for i in 1..nx - 1 { // Compute mass source (continuity equation residual) let mass_source = self.compute_mass_source(flow_field, i, j, dx, dy, rho)?; flow_field.sp[(j, i)] = mass_source; } } // Solve pressure correction equation using Gauss-Seidel let mut max_residual = 0.0; for _iteration in 0..100 { // Inner pressure correction iterations let mut residual = 0.0; for j in 1..ny - 1 { for i in 1..nx - 1 { let coeffs = self.compute_pressure_coefficients(dx, dy)?; let p_new = (flow_field.sp[(j, i)] + coeffs.east * flow_field.p_prime[(j, i + 1)] + coeffs.west * flow_field.p_prime[(j, i - 1)] + coeffs.north * flow_field.p_prime[(j + 1, i)] + coeffs.south * flow_field.p_prime[(j - 1, i)]) / coeffs.center; let correction = p_new - flow_field.p_prime[(j, i)]; residual += correction * correction; flow_field.p_prime[(j, i)] = p_new; } } residual = residual.sqrt(); max_residual = residual; if residual < 1e-8 { break; } } Ok(max_residual) } /// Velocity correction step: correct velocities with pressure correction pub async fn velocity_correction_step(&self, flow_field: &mut FlowField) -> CfdResult<()> { let (nx, ny, dx, dy) = flow_field.grid_info(); let rho = self.config.density; // Correct u-velocities for j in 1..ny - 1 { for i in 1..nx { if i > 0 && i < nx { let dp_dx = (flow_field.p_prime[(j, i)] - flow_field.p_prime[(j, i - 1)]) / dx; let ap_u = self.compute_u_momentum_center_coefficient(flow_field, i, j, dx, dy, rho)?; flow_field.u[(j, i)] = flow_field.u_star[(j, i)] - (dx * dy / ap_u) * dp_dx; } } } // Correct v-velocities for j in 1..ny { for i in 1..nx - 1 { if j > 0 && j < ny { let dp_dy = (flow_field.p_prime[(j, i)] - flow_field.p_prime[(j - 1, i)]) / dy; let ap_v = self.compute_v_momentum_center_coefficient(flow_field, i, j, dx, dy, rho)?; flow_field.v[(j, i)] = flow_field.v_star[(j, i)] - (dx * dy / ap_v) * dp_dy; } } } Ok(()) } /// Pressure update step: add pressure correction to pressure pub async fn pressure_update_step(&self, flow_field: &mut FlowField) -> CfdResult<()> { let (nx, ny, _, _) = flow_field.grid_info(); for j in 0..ny { for i in 0..nx { flow_field.p[(j, i)] += self.parameters.pressure_relaxation * flow_field.p_prime[(j, i)]; flow_field.p_prime[(j, i)] = 0.0; // Reset pressure correction } } Ok(()) } /// Solve turbulence model equations async fn solve_turbulence(&mut self, flow_field: &FlowField, dt: f64) -> CfdResult<()> { if self.parameters.use_turbulence { // Initialize turbulence model if not already done let (nx, ny, dx, dy) = flow_field.grid_info(); let n_cells = nx * ny; if self.turbulence_model.is_none() { self.turbulence_model = Some(KEpsilonModel::new(KEpsilonVariant::Standard, n_cells)); } let turbulence_model = self.turbulence_model.as_mut().unwrap(); // Convert velocity field to Vector3 format let mut velocity_vec = Vec::with_capacity(n_cells); for j in 0..ny { for i in 0..nx { let u = if i < nx && j < ny { flow_field.u[(j, i)] } else { 0.0 }; let v = if i < nx && j < ny { flow_field.v[(j, i)] } else { 0.0 }; velocity_vec.push(Vector3::new(u, v, 0.0)); // 2D case, w=0 } } // Simplified velocity gradients (zero for now) let velocity_gradients = vec![[[0.0; 3]; 3]; n_cells]; // Create pressure vector let mut pressure = DVector::zeros(n_cells); for j in 0..ny { for i in 0..nx { if i < nx && j < ny { pressure[j * nx + i] = flow_field.p[(j, i)]; } } } let turbulence_state = TurbulenceState { velocity: velocity_vec, velocity_gradients, pressure, turbulent_ke: None, // Will be initialized by model epsilon: None, // Will be initialized by model omega: None, wall_distance: DVector::from_element(n_cells, 1.0), // Simplified cell_volumes: DVector::from_element(n_cells, dx * dy), // 2D cell volume molecular_viscosity: self.config.viscosity / self.config.density, // kinematic viscosity density: self.config.density, }; // Update turbulence model with new state turbulence_model.update(&turbulence_state, dt)?; } Ok(()) } /// Compute effective viscosity (molecular + turbulent) fn compute_effective_viscosity( &self, flow_field: &FlowField, i: usize, j: usize, mu: f64, ) -> f64 { if let Some(ref turbulence_model) = self.turbulence_model { // Get turbulent viscosity from the model let (nx, _ny, _, _) = flow_field.grid_info(); let cell_idx = j * nx + i; if cell_idx < turbulence_model.nu_t.len() { let nu_t = turbulence_model.nu_t[cell_idx]; // kinematic turbulent viscosity let mu_t = nu_t * self.config.density; // convert to dynamic viscosity mu + mu_t } else { mu } } else { mu } } /// Compute coefficients for u-momentum equation fn compute_u_momentum_coefficients( &self, flow_field: &FlowField, i: usize, j: usize, dt: f64, rho: f64, mu: f64, dx: f64, dy: f64, ) -> CfdResult { // Compute effective viscosity (molecular + turbulent) let mu_eff = self.compute_effective_viscosity(flow_field, i, j, mu); // Diffusion coefficients using effective viscosity let gamma_e = mu_eff / dx; let gamma_w = mu_eff / dx; let gamma_n = mu_eff / dy; let gamma_s = mu_eff / dy; // Convection coefficients (using upwind) let (u_center, v_center) = flow_field.get_velocity_at(i, j)?; let fe = rho * u_center * dy; // East face mass flux let fw = rho * u_center * dy; // West face mass flux let fn_ = rho * v_center * dx; // North face mass flux let fs = rho * v_center * dx; // South face mass flux // Compute coefficients with upwind scheme let ae = gamma_e + f64::max(-fe, 0.0); let aw = gamma_w + f64::max(fw, 0.0); let an = gamma_n + f64::max(-fn_, 0.0); let as_ = gamma_s + f64::max(fs, 0.0); // Time derivative coefficient let ap0 = rho * dx * dy / dt; // Central coefficient let ap = ae + aw + an + as_ + ap0; // Source term (pressure gradient + old time step) let pressure_gradient = -(flow_field.p[(j, i)] - flow_field.p[(j, i - 1)]) * dy; let time_term = ap0 * flow_field.u_old[(j, i)]; let source = pressure_gradient + time_term; Ok(MomentumEquationCoeffs { center: ap, east: ae, west: aw, north: an, south: as_, source, }) } /// Compute coefficients for v-momentum equation fn compute_v_momentum_coefficients( &self, flow_field: &FlowField, i: usize, j: usize, dt: f64, rho: f64, mu: f64, dx: f64, dy: f64, ) -> CfdResult { // Compute effective viscosity (molecular + turbulent) let mu_eff = self.compute_effective_viscosity(flow_field, i, j, mu); // Similar to u-momentum but for v-component let gamma_e = mu_eff / dx; let gamma_w = mu_eff / dx; let gamma_n = mu_eff / dy; let gamma_s = mu_eff / dy; let (u_center, v_center) = flow_field.get_velocity_at(i, j)?; let fe = rho * u_center * dy; let fw = rho * u_center * dy; let fn_ = rho * v_center * dx; let fs = rho * v_center * dx; let ae = gamma_e + f64::max(-fe, 0.0); let aw = gamma_w + f64::max(fw, 0.0); let an = gamma_n + f64::max(-fn_, 0.0); let as_ = gamma_s + f64::max(fs, 0.0); let ap0 = rho * dx * dy / dt; let ap = ae + aw + an + as_ + ap0; // Pressure gradient in y-direction let pressure_gradient = -(flow_field.p[(j, i)] - flow_field.p[(j - 1, i)]) * dx; let time_term = ap0 * flow_field.v_old[(j, i)]; let source = pressure_gradient + time_term; Ok(MomentumEquationCoeffs { center: ap, east: ae, west: aw, north: an, south: as_, source, }) } /// Compute mass source term for pressure correction equation fn compute_mass_source( &self, flow_field: &FlowField, i: usize, j: usize, dx: f64, dy: f64, rho: f64, ) -> CfdResult { // Mass source = ρ * ∇·u* let u_e = flow_field.u_star[(j, i + 1)]; let u_w = flow_field.u_star[(j, i)]; let v_n = flow_field.v_star[(j + 1, i)]; let v_s = flow_field.v_star[(j, i)]; let mass_flux_imbalance = rho * ((u_e - u_w) * dy + (v_n - v_s) * dx); Ok(-mass_flux_imbalance) // Negative because we want ∇²p' = -∇·u* } /// Compute coefficients for pressure correction equation fn compute_pressure_coefficients(&self, dx: f64, dy: f64) -> CfdResult { // Pressure correction equation: ∇²p' = S // Standard 5-point stencil with unit coefficients let ae = 1.0 / (dx * dx); let aw = 1.0 / (dx * dx); let an = 1.0 / (dy * dy); let as_ = 1.0 / (dy * dy); let ap = ae + aw + an + as_; Ok(MomentumEquationCoeffs { center: ap, east: ae, west: aw, north: an, south: as_, source: 0.0, // Source is set separately }) } /// Compute center coefficient for u-momentum equation fn compute_u_momentum_center_coefficient( &self, _flow_field: &FlowField, _i: usize, _j: usize, dx: f64, dy: f64, rho: f64, ) -> CfdResult { // Simplified calculation for velocity correction // This would be the diagonal coefficient from momentum discretization Ok(rho * dx * dy / self.parameters.time_step) } /// Compute center coefficient for v-momentum equation fn compute_v_momentum_center_coefficient( &self, _flow_field: &FlowField, _i: usize, _j: usize, dx: f64, dy: f64, rho: f64, ) -> CfdResult { Ok(rho * dx * dy / self.parameters.time_step) } } /// Coefficients for momentum equation discretization #[derive(Debug, Clone)] struct MomentumEquationCoeffs { pub center: f64, pub east: f64, pub west: f64, pub north: f64, pub south: f64, pub source: f64, } #[async_trait] impl IncompressibleSolver for SimpleSolver { type Parameters = SimpleParameters; type Result = SimpleResult; fn new(config: CfdConfig, params: Self::Parameters) -> CfdResult { Self::new(config, params) } async fn solve_time_step( &mut self, flow_field: &mut FlowField, boundary_conditions: &BoundaryConditions, dt: f64, ) -> CfdResult { let start_time = Instant::now(); let mut residual_history = Vec::new(); let pressure_iterations = Vec::new(); for iteration in 0..self.parameters.max_iterations { let (mass_residual, momentum_residual) = self .solve_simple_iteration(flow_field, boundary_conditions, dt) .await?; let total_residual = (mass_residual * mass_residual + momentum_residual * momentum_residual).sqrt(); residual_history.push(total_residual); if total_residual < self.parameters.tolerance { let solve_time = start_time.elapsed(); return Ok(SimpleResult { solver_result: SolverResult { converged: true, iterations: iteration + 1, final_residual: total_residual, residual_history, solve_time, }, pressure_iterations, mass_residual, momentum_residual, }); } } // Did not converge let solve_time = start_time.elapsed(); Ok(SimpleResult { solver_result: SolverResult { converged: false, iterations: self.parameters.max_iterations, final_residual: residual_history.last().copied().unwrap_or(f64::INFINITY), residual_history, solve_time, }, pressure_iterations, mass_residual: f64::INFINITY, momentum_residual: f64::INFINITY, }) } async fn solve( &mut self, flow_field: &mut FlowField, boundary_conditions: &BoundaryConditions, ) -> CfdResult { // For steady-state solve, use default time step self.solve_time_step(flow_field, boundary_conditions, self.parameters.time_step) .await } fn config(&self) -> &CfdConfig { &self.config } fn parameters(&self) -> &Self::Parameters { &self.parameters } }