Files
rustytorch/crates/specialized/rtx-cfd/src/solvers/incompressible/simple.rs
T
2026-03-04 00:08:42 +00:00

784 lines
25 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<usize>,
/// 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<KEpsilonModel>,
}
/// Workspace for linear algebra operations
struct LinearAlgebraWorkspace {
/// Matrix for pressure correction equation
pressure_matrix: Option<DMatrix<f64>>,
/// RHS vector for pressure correction
pressure_rhs: Option<DVector<f64>>,
/// Solution vector for pressure correction
pressure_solution: Option<DVector<f64>>,
/// Momentum equation coefficients
momentum_coefficients: Option<MomentumCoefficients>,
}
/// Coefficients for momentum equations discretization
#[derive(Debug, Clone)]
struct MomentumCoefficients {
/// Central coefficient (diagonal)
pub ap: DMatrix<f64>,
/// East neighbor coefficient
pub ae: DMatrix<f64>,
/// West neighbor coefficient
pub aw: DMatrix<f64>,
/// North neighbor coefficient
pub an: DMatrix<f64>,
/// South neighbor coefficient
pub as_: DMatrix<f64>,
/// Source term
pub su: DMatrix<f64>,
}
impl SimpleSolver {
/// Create new SIMPLE solver
pub fn new(config: CfdConfig, parameters: SimpleParameters) -> CfdResult<Self> {
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<f64> {
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<MomentumEquationCoeffs> {
// 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<MomentumEquationCoeffs> {
// 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<f64> {
// 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<MomentumEquationCoeffs> {
// 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<f64> {
// 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<f64> {
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> {
Self::new(config, params)
}
async fn solve_time_step(
&mut self,
flow_field: &mut FlowField,
boundary_conditions: &BoundaryConditions,
dt: f64,
) -> CfdResult<Self::Result> {
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<Self::Result> {
// 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
}
}