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

399 lines
13 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.
//! 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<f64>,
/// y-component of velocity (v) - located at cell faces (i, j+1/2)
pub v: DMatrix<f64>,
/// Pressure (p) - located at cell centers (i, j)
pub p: DMatrix<f64>,
/// Previous time step values for time integration
pub u_old: DMatrix<f64>,
pub v_old: DMatrix<f64>,
pub p_old: DMatrix<f64>,
/// Auxiliary fields for solver algorithms
pub u_star: DMatrix<f64>, // Predicted velocity (SIMPLE/PISO)
pub v_star: DMatrix<f64>, // Predicted velocity (SIMPLE/PISO)
pub p_prime: DMatrix<f64>, // Pressure correction (SIMPLE/PISO)
/// Source terms
pub su: DMatrix<f64>, // u-momentum source
pub sv: DMatrix<f64>, // v-momentum source
pub sp: DMatrix<f64>, // 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<Self> {
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<f64> {
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<DMatrix<f64>> {
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<f64> {
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<f64> {
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 },
}