Files
rustytorch/demos/rtx-aeroflow-demo/src/physics.rs
T
2026-03-04 00:08:42 +00:00

431 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.
//! Physics-informed losses for CFD neural operators.
use aeroflow_shared::{FlowConditions, FlowField2D};
/// Navier-Stokes residual calculator for physics-informed losses.
#[derive(Debug)]
pub struct NavierStokesLoss {
/// Kinematic viscosity (m²/s).
nu: f32,
/// Density (kg/m³).
rho: f32,
}
impl Default for NavierStokesLoss {
fn default() -> Self {
Self::new()
}
}
impl NavierStokesLoss {
/// Create a new Navier-Stokes loss calculator.
pub fn new() -> Self {
Self {
nu: 1.5e-5, // Air at sea level
rho: 1.225,
}
}
/// Create with specific fluid properties.
pub fn with_properties(nu: f32, rho: f32) -> Self {
Self { nu, rho }
}
/// Evaluate physics residuals on the flow field.
pub fn evaluate(&self, flow_field: &FlowField2D, conditions: &FlowConditions) -> f32 {
let continuity = self.continuity_residual(flow_field);
let momentum_x = self.momentum_x_residual(flow_field, conditions);
let momentum_y = self.momentum_y_residual(flow_field, conditions);
(continuity.powi(2) + momentum_x.powi(2) + momentum_y.powi(2)).sqrt()
}
/// Compute all residual components.
pub fn compute_residuals(
&self,
flow_field: &FlowField2D,
conditions: &FlowConditions,
) -> PhysicsResiduals {
PhysicsResiduals {
continuity: self.continuity_residual(flow_field),
momentum_x: self.momentum_x_residual(flow_field, conditions),
momentum_y: self.momentum_y_residual(flow_field, conditions),
boundary: self.boundary_residual(flow_field, conditions),
}
}
/// Continuity equation residual: ∂u/∂x + ∂v/∂y = 0
fn continuity_residual(&self, flow_field: &FlowField2D) -> f32 {
let nx = flow_field.velocity_x.len();
let ny = flow_field.velocity_x[0].len();
if nx < 3 || ny < 3 {
return 0.0;
}
let dx = (flow_field.bounds.x_max - flow_field.bounds.x_min) / (nx - 1) as f32;
let dy = (flow_field.bounds.y_max - flow_field.bounds.y_min) / (ny - 1) as f32;
let mut total_residual = 0.0;
let mut count = 0;
for i in 1..nx - 1 {
for j in 1..ny - 1 {
let du_dx = (flow_field.velocity_x[i + 1][j] - flow_field.velocity_x[i - 1][j])
/ (2.0 * dx);
let dv_dy = (flow_field.velocity_y[i][j + 1] - flow_field.velocity_y[i][j - 1])
/ (2.0 * dy);
total_residual += (du_dx + dv_dy).abs();
count += 1;
}
}
if count > 0 {
total_residual / count as f32
} else {
0.0
}
}
/// X-momentum equation residual.
/// u·∂u/∂x + v·∂u/∂y = -1/ρ·∂p/∂x + ν·∇²u
fn momentum_x_residual(&self, flow_field: &FlowField2D, _conditions: &FlowConditions) -> f32 {
let nx = flow_field.velocity_x.len();
let ny = flow_field.velocity_x[0].len();
if nx < 3 || ny < 3 {
return 0.0;
}
let dx = (flow_field.bounds.x_max - flow_field.bounds.x_min) / (nx - 1) as f32;
let dy = (flow_field.bounds.y_max - flow_field.bounds.y_min) / (ny - 1) as f32;
let mut total_residual = 0.0;
let mut count = 0;
for i in 2..nx - 2 {
for j in 2..ny - 2 {
let u = flow_field.velocity_x[i][j];
let v = flow_field.velocity_y[i][j];
// First derivatives
let du_dx = (flow_field.velocity_x[i + 1][j] - flow_field.velocity_x[i - 1][j])
/ (2.0 * dx);
let du_dy = (flow_field.velocity_x[i][j + 1] - flow_field.velocity_x[i][j - 1])
/ (2.0 * dy);
// Second derivatives (Laplacian)
let d2u_dx2 = (flow_field.velocity_x[i + 1][j] - 2.0 * flow_field.velocity_x[i][j]
+ flow_field.velocity_x[i - 1][j])
/ (dx * dx);
let d2u_dy2 = (flow_field.velocity_x[i][j + 1] - 2.0 * flow_field.velocity_x[i][j]
+ flow_field.velocity_x[i][j - 1])
/ (dy * dy);
// Pressure gradient
let dp_dx =
(flow_field.pressure[i + 1][j] - flow_field.pressure[i - 1][j]) / (2.0 * dx);
// Momentum residual
let convection = u * du_dx + v * du_dy;
let pressure_term = dp_dx / self.rho;
let diffusion = self.nu * (d2u_dx2 + d2u_dy2);
let residual = convection + pressure_term - diffusion;
total_residual += residual.abs();
count += 1;
}
}
if count > 0 {
total_residual / count as f32
} else {
0.0
}
}
/// Y-momentum equation residual.
fn momentum_y_residual(&self, flow_field: &FlowField2D, _conditions: &FlowConditions) -> f32 {
let nx = flow_field.velocity_y.len();
let ny = flow_field.velocity_y[0].len();
if nx < 3 || ny < 3 {
return 0.0;
}
let dx = (flow_field.bounds.x_max - flow_field.bounds.x_min) / (nx - 1) as f32;
let dy = (flow_field.bounds.y_max - flow_field.bounds.y_min) / (ny - 1) as f32;
let mut total_residual = 0.0;
let mut count = 0;
for i in 2..nx - 2 {
for j in 2..ny - 2 {
let u = flow_field.velocity_x[i][j];
let v = flow_field.velocity_y[i][j];
// First derivatives
let dv_dx = (flow_field.velocity_y[i + 1][j] - flow_field.velocity_y[i - 1][j])
/ (2.0 * dx);
let dv_dy = (flow_field.velocity_y[i][j + 1] - flow_field.velocity_y[i][j - 1])
/ (2.0 * dy);
// Second derivatives
let d2v_dx2 = (flow_field.velocity_y[i + 1][j] - 2.0 * flow_field.velocity_y[i][j]
+ flow_field.velocity_y[i - 1][j])
/ (dx * dx);
let d2v_dy2 = (flow_field.velocity_y[i][j + 1] - 2.0 * flow_field.velocity_y[i][j]
+ flow_field.velocity_y[i][j - 1])
/ (dy * dy);
// Pressure gradient
let dp_dy =
(flow_field.pressure[i][j + 1] - flow_field.pressure[i][j - 1]) / (2.0 * dy);
// Momentum residual
let convection = u * dv_dx + v * dv_dy;
let pressure_term = dp_dy / self.rho;
let diffusion = self.nu * (d2v_dx2 + d2v_dy2);
let residual = convection + pressure_term - diffusion;
total_residual += residual.abs();
count += 1;
}
}
if count > 0 {
total_residual / count as f32
} else {
0.0
}
}
/// Boundary condition residual.
fn boundary_residual(&self, flow_field: &FlowField2D, conditions: &FlowConditions) -> f32 {
let nx = flow_field.velocity_x.len();
let ny = flow_field.velocity_x[0].len();
let mut residual = 0.0;
let mut count = 0;
// Inlet boundary (left edge)
for j in 0..ny {
let u_inlet = conditions.velocity * conditions.angle_of_attack.to_radians().cos();
let v_inlet = conditions.velocity * conditions.angle_of_attack.to_radians().sin();
residual += (flow_field.velocity_x[0][j] - u_inlet).abs();
residual += (flow_field.velocity_y[0][j] - v_inlet).abs();
count += 2;
}
// Farfield boundaries (top and bottom)
for i in 0..nx {
// Top
residual += (flow_field.velocity_x[i][ny - 1] - conditions.velocity).abs();
// Bottom
residual += (flow_field.velocity_x[i][0] - conditions.velocity).abs();
count += 2;
}
if count > 0 {
residual / count as f32
} else {
0.0
}
}
}
/// Physics residuals for monitoring.
#[derive(Debug, Clone, Copy, Default)]
pub struct PhysicsResiduals {
/// Continuity equation residual.
pub continuity: f32,
/// X-momentum residual.
pub momentum_x: f32,
/// Y-momentum residual.
pub momentum_y: f32,
/// Boundary condition residual.
pub boundary: f32,
}
impl PhysicsResiduals {
/// Total L2 norm of residuals.
pub fn total(&self) -> f32 {
(self.continuity.powi(2)
+ self.momentum_x.powi(2)
+ self.momentum_y.powi(2)
+ self.boundary.powi(2))
.sqrt()
}
}
/// Euler equations residual (inviscid).
#[derive(Debug)]
pub struct EulerLoss {
/// Ratio of specific heats.
#[allow(dead_code)]
gamma: f32,
}
impl Default for EulerLoss {
fn default() -> Self {
Self::new()
}
}
impl EulerLoss {
/// Create a new Euler loss calculator.
pub fn new() -> Self {
Self { gamma: 1.4 }
}
/// Evaluate Euler equation residuals.
pub fn evaluate(&self, _flow_field: &FlowField2D, conditions: &FlowConditions) -> f32 {
// For compressible flow, would compute:
// ∂ρ/∂t + ∇·(ρu) = 0 (mass)
// ∂(ρu)/∂t + ∇·(ρuu + pI) = 0 (momentum)
// ∂E/∂t + ∇·((E + p)u) = 0 (energy)
// Simplified: just check for low Mach incompressible behavior
if conditions.mach < 0.3 {
0.0
} else {
0.1 * (conditions.mach - 0.3)
}
}
}
/// Wall function for near-wall treatment.
#[derive(Debug)]
pub struct WallFunction {
/// Von Karman constant.
kappa: f32,
/// Integration constant.
b: f32,
}
impl Default for WallFunction {
fn default() -> Self {
Self::new()
}
}
impl WallFunction {
/// Create a new wall function.
pub fn new() -> Self {
Self {
kappa: 0.41,
b: 5.0,
}
}
/// Law of the wall: u+ = (1/κ)ln(y+) + B
pub fn u_plus(&self, y_plus: f32) -> f32 {
if y_plus < 11.6 {
// Viscous sublayer
y_plus
} else {
// Log layer
(1.0 / self.kappa) * y_plus.ln() + self.b
}
}
/// Compute wall shear stress.
pub fn wall_shear(&self, u_tau: f32, rho: f32) -> f32 {
rho * u_tau.powi(2)
}
}
#[cfg(test)]
mod tests {
use super::*;
use aeroflow_shared::DomainBounds;
fn create_test_flow_field() -> FlowField2D {
let nx = 10;
let ny = 8;
FlowField2D {
pressure: vec![vec![101325.0; ny]; nx],
velocity_x: vec![vec![100.0; ny]; nx],
velocity_y: vec![vec![0.0; ny]; nx],
velocity_magnitude: vec![vec![100.0; ny]; nx],
cp: vec![vec![0.0; ny]; nx],
vorticity: vec![vec![0.0; ny]; nx],
tke: None,
grid_x: vec![vec![0.0; ny]; nx],
grid_y: vec![vec![0.0; ny]; nx],
bounds: DomainBounds::default(),
}
}
#[test]
fn test_ns_loss_creation() {
let loss = NavierStokesLoss::new();
assert!((loss.nu - 1.5e-5).abs() < 1e-7);
}
#[test]
fn test_continuity_residual() {
let loss = NavierStokesLoss::new();
let flow_field = create_test_flow_field();
let residual = loss.continuity_residual(&flow_field);
// Uniform flow should have zero continuity residual
assert!(residual.abs() < 1e-3);
}
#[test]
fn test_evaluate() {
let loss = NavierStokesLoss::new();
let flow_field = create_test_flow_field();
let conditions = FlowConditions::default();
let total = loss.evaluate(&flow_field, &conditions);
assert!(total.is_finite());
}
#[test]
fn test_physics_residuals() {
let loss = NavierStokesLoss::new();
let flow_field = create_test_flow_field();
let conditions = FlowConditions::default();
let residuals = loss.compute_residuals(&flow_field, &conditions);
assert!(residuals.continuity.is_finite());
assert!(residuals.momentum_x.is_finite());
assert!(residuals.momentum_y.is_finite());
assert!(residuals.total().is_finite());
}
#[test]
fn test_euler_loss() {
let loss = EulerLoss::new();
let flow_field = create_test_flow_field();
let low_mach = FlowConditions {
mach: 0.2,
..Default::default()
};
assert_eq!(loss.evaluate(&flow_field, &low_mach), 0.0);
let high_mach = FlowConditions {
mach: 0.5,
..Default::default()
};
assert!(loss.evaluate(&flow_field, &high_mach) > 0.0);
}
#[test]
fn test_wall_function() {
let wf = WallFunction::new();
// Viscous sublayer: u+ = y+
assert!((wf.u_plus(5.0) - 5.0).abs() < 0.01);
// Log layer: u+ = (1/κ)ln(y+) + B
let y_plus: f32 = 100.0;
let expected = (1.0 / 0.41) * y_plus.ln() + 5.0;
assert!((wf.u_plus(y_plus) - expected).abs() < 0.01);
}
}