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

567 lines
16 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.
//! Fluid properties and boundary conditions for hemodynamics simulation
//!
//! This module provides physical parameters and boundary condition types
//! for Navier-Stokes simulation of blood flow.
//!
//! # Example
//!
//! ```rust
//! use rtx_hemodynamics_shared::physics::{FluidProperties, BoundaryCondition, SimulationConfig};
//!
//! // Use blood properties
//! let blood = FluidProperties::blood();
//! println!("Blood viscosity: {} Pa.s", blood.dynamic_viscosity());
//!
//! // Set up boundary conditions
//! let inlet = BoundaryCondition::inlet_velocity(0.1).unwrap();
//! let outlet = BoundaryCondition::outlet_pressure(0.0).unwrap();
//! let wall = BoundaryCondition::no_slip_wall();
//!
//! // Configure simulation
//! let config = SimulationConfig::builder()
//! .grid_resolution(100)
//! .time_step(0.001)
//! .build()
//! .unwrap();
//! ```
use crate::error::{HemodynamicsError, Result};
use serde::{Deserialize, Serialize};
/// Fluid properties for hemodynamics simulation
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct FluidProperties {
/// Fluid density (kg/m³)
density: f64,
/// Dynamic viscosity (Pa.s)
dynamic_viscosity: f64,
}
impl FluidProperties {
/// Creates custom fluid properties
///
/// # Arguments
///
/// * `density` - Fluid density in kg/m³
/// * `dynamic_viscosity` - Dynamic viscosity in Pa.s
///
/// # Errors
///
/// Returns an error if density or viscosity is not positive.
pub fn new(density: f64, dynamic_viscosity: f64) -> Result<Self> {
if density <= 0.0 {
return Err(HemodynamicsError::invalid_physics(
"density must be positive",
));
}
if dynamic_viscosity <= 0.0 {
return Err(HemodynamicsError::invalid_physics(
"dynamic viscosity must be positive",
));
}
Ok(Self {
density,
dynamic_viscosity,
})
}
/// Returns standard blood properties
///
/// - Density: 1060 kg/m³
/// - Dynamic viscosity: 0.0035 Pa.s (at normal hematocrit)
#[must_use]
pub fn blood() -> Self {
Self {
density: 1060.0,
dynamic_viscosity: 0.0035,
}
}
/// Returns water properties at 37°C (body temperature)
///
/// - Density: 993 kg/m³
/// - Dynamic viscosity: 0.000692 Pa.s
#[must_use]
pub fn water_37c() -> Self {
Self {
density: 993.0,
dynamic_viscosity: 0.000_692,
}
}
/// Returns the fluid density
#[must_use]
pub const fn density(&self) -> f64 {
self.density
}
/// Returns the dynamic viscosity
#[must_use]
pub const fn dynamic_viscosity(&self) -> f64 {
self.dynamic_viscosity
}
/// Computes the kinematic viscosity (ν = μ/ρ)
#[must_use]
pub fn kinematic_viscosity(&self) -> f64 {
self.dynamic_viscosity / self.density
}
/// Computes the Reynolds number for given velocity and length scale
///
/// Re = ρVL/μ = VL/ν
#[must_use]
pub fn reynolds_number(&self, velocity: f64, length_scale: f64) -> f64 {
self.density * velocity * length_scale / self.dynamic_viscosity
}
/// Computes the Womersley number for pulsatile flow
///
/// α = R√(ωρ/μ) where ω = 2πf
#[must_use]
pub fn womersley_number(&self, radius: f64, frequency: f64) -> f64 {
let omega = 2.0 * std::f64::consts::PI * frequency;
radius * (omega * self.density / self.dynamic_viscosity).sqrt()
}
}
impl Default for FluidProperties {
fn default() -> Self {
Self::blood()
}
}
/// Boundary condition types
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum BoundaryType {
/// Prescribed velocity at inlet (m/s)
InletVelocity(f64),
/// Prescribed pressure at outlet (Pa)
OutletPressure(f64),
/// No-slip wall condition (velocity = 0)
NoSlipWall,
/// Slip wall (zero normal velocity, zero tangential stress)
SlipWall,
/// Periodic boundary
Periodic,
/// Pulsatile inlet with amplitude and frequency
PulsatileInlet {
/// Mean velocity (m/s)
mean_velocity: f64,
/// Amplitude of oscillation (m/s)
amplitude: f64,
/// Frequency (Hz)
frequency: f64,
},
}
/// Boundary condition with location information
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct BoundaryCondition {
/// Type of boundary condition
boundary_type: BoundaryType,
}
impl BoundaryCondition {
/// Creates a velocity inlet boundary condition
///
/// # Arguments
///
/// * `velocity` - Inlet velocity in m/s (must be non-negative)
///
/// # Errors
///
/// Returns an error if velocity is negative.
pub fn inlet_velocity(velocity: f64) -> Result<Self> {
if velocity < 0.0 {
return Err(HemodynamicsError::invalid_physics(
"inlet velocity cannot be negative",
));
}
Ok(Self {
boundary_type: BoundaryType::InletVelocity(velocity),
})
}
/// Creates a pressure outlet boundary condition
///
/// # Arguments
///
/// * `pressure` - Outlet pressure in Pa (gauge pressure)
pub fn outlet_pressure(pressure: f64) -> Result<Self> {
Ok(Self {
boundary_type: BoundaryType::OutletPressure(pressure),
})
}
/// Creates a no-slip wall boundary condition
#[must_use]
pub fn no_slip_wall() -> Self {
Self {
boundary_type: BoundaryType::NoSlipWall,
}
}
/// Creates a slip wall boundary condition
#[must_use]
pub fn slip_wall() -> Self {
Self {
boundary_type: BoundaryType::SlipWall,
}
}
/// Creates a periodic boundary condition
#[must_use]
pub fn periodic() -> Self {
Self {
boundary_type: BoundaryType::Periodic,
}
}
/// Creates a pulsatile inlet boundary condition
///
/// Velocity varies as: v(t) = mean + amplitude * sin(2πft)
///
/// # Arguments
///
/// * `mean_velocity` - Mean velocity (m/s)
/// * `amplitude` - Oscillation amplitude (m/s)
/// * `frequency` - Oscillation frequency (Hz)
///
/// # Errors
///
/// Returns an error if `mean_velocity` - amplitude < 0 (would cause backflow).
pub fn pulsatile_inlet(mean_velocity: f64, amplitude: f64, frequency: f64) -> Result<Self> {
if mean_velocity - amplitude < 0.0 {
return Err(HemodynamicsError::invalid_physics(
"pulsatile inlet would cause backflow (mean - amplitude < 0)",
));
}
if frequency <= 0.0 {
return Err(HemodynamicsError::invalid_physics(
"frequency must be positive",
));
}
Ok(Self {
boundary_type: BoundaryType::PulsatileInlet {
mean_velocity,
amplitude,
frequency,
},
})
}
/// Returns the boundary type
#[must_use]
pub const fn boundary_type(&self) -> &BoundaryType {
&self.boundary_type
}
/// Evaluates the boundary condition at a given time
#[must_use]
pub fn evaluate(&self, time: f64) -> f64 {
match self.boundary_type {
BoundaryType::InletVelocity(v) => v,
BoundaryType::OutletPressure(p) => p,
BoundaryType::NoSlipWall | BoundaryType::SlipWall => 0.0,
BoundaryType::Periodic => 0.0,
BoundaryType::PulsatileInlet {
mean_velocity,
amplitude,
frequency,
} => mean_velocity + amplitude * (2.0 * std::f64::consts::PI * frequency * time).sin(),
}
}
/// Returns true if this is a wall boundary
#[must_use]
pub const fn is_wall(&self) -> bool {
matches!(
self.boundary_type,
BoundaryType::NoSlipWall | BoundaryType::SlipWall
)
}
/// Returns true if this is an inlet boundary
#[must_use]
pub const fn is_inlet(&self) -> bool {
matches!(
self.boundary_type,
BoundaryType::InletVelocity(_) | BoundaryType::PulsatileInlet { .. }
)
}
/// Returns true if this is an outlet boundary
#[must_use]
pub const fn is_outlet(&self) -> bool {
matches!(self.boundary_type, BoundaryType::OutletPressure(_))
}
}
/// Builder for simulation configuration
#[derive(Debug, Clone)]
pub struct SimulationConfigBuilder {
grid_resolution: usize,
time_step: f64,
max_iterations: usize,
convergence_tolerance: f64,
fluid: FluidProperties,
inlet_bc: Option<BoundaryCondition>,
outlet_bc: Option<BoundaryCondition>,
}
impl Default for SimulationConfigBuilder {
fn default() -> Self {
Self {
grid_resolution: 100,
time_step: 0.001,
max_iterations: 5000,
convergence_tolerance: 1e-6,
fluid: FluidProperties::blood(),
inlet_bc: None,
outlet_bc: None,
}
}
}
impl SimulationConfigBuilder {
/// Creates a new builder with default values
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Sets the grid resolution (number of points per dimension)
#[must_use]
pub const fn grid_resolution(mut self, resolution: usize) -> Self {
self.grid_resolution = resolution;
self
}
/// Sets the time step for transient simulations
#[must_use]
pub const fn time_step(mut self, dt: f64) -> Self {
self.time_step = dt;
self
}
/// Sets the maximum number of iterations
#[must_use]
pub const fn max_iterations(mut self, max_iter: usize) -> Self {
self.max_iterations = max_iter;
self
}
/// Sets the convergence tolerance
#[must_use]
pub const fn convergence_tolerance(mut self, tol: f64) -> Self {
self.convergence_tolerance = tol;
self
}
/// Sets the fluid properties
#[must_use]
pub const fn fluid(mut self, fluid: FluidProperties) -> Self {
self.fluid = fluid;
self
}
/// Sets the inlet boundary condition
#[must_use]
pub fn inlet(mut self, bc: BoundaryCondition) -> Self {
self.inlet_bc = Some(bc);
self
}
/// Sets the outlet boundary condition
#[must_use]
pub fn outlet(mut self, bc: BoundaryCondition) -> Self {
self.outlet_bc = Some(bc);
self
}
/// Builds the simulation configuration
///
/// # Errors
///
/// Returns an error if parameters are invalid.
pub fn build(self) -> Result<SimulationConfig> {
if self.grid_resolution == 0 {
return Err(HemodynamicsError::invalid_config(
"grid resolution must be positive",
));
}
if self.time_step <= 0.0 {
return Err(HemodynamicsError::invalid_config(
"time step must be positive",
));
}
if self.max_iterations == 0 {
return Err(HemodynamicsError::invalid_config(
"max iterations must be positive",
));
}
if self.convergence_tolerance <= 0.0 {
return Err(HemodynamicsError::invalid_config(
"convergence tolerance must be positive",
));
}
Ok(SimulationConfig {
grid_resolution: self.grid_resolution,
time_step: self.time_step,
max_iterations: self.max_iterations,
convergence_tolerance: self.convergence_tolerance,
fluid: self.fluid,
inlet_bc: self
.inlet_bc
.unwrap_or_else(|| BoundaryCondition::inlet_velocity(0.1).unwrap()),
outlet_bc: self
.outlet_bc
.unwrap_or_else(|| BoundaryCondition::outlet_pressure(0.0).unwrap()),
})
}
}
/// Simulation configuration parameters
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimulationConfig {
/// Grid resolution (points per dimension)
grid_resolution: usize,
/// Time step for transient simulations (seconds)
time_step: f64,
/// Maximum number of solver iterations
max_iterations: usize,
/// Convergence tolerance for iterative solvers
convergence_tolerance: f64,
/// Fluid properties
fluid: FluidProperties,
/// Inlet boundary condition
inlet_bc: BoundaryCondition,
/// Outlet boundary condition
outlet_bc: BoundaryCondition,
}
impl SimulationConfig {
/// Creates a builder for simulation configuration
#[must_use]
pub fn builder() -> SimulationConfigBuilder {
SimulationConfigBuilder::new()
}
/// Returns the grid resolution
#[must_use]
pub const fn grid_resolution(&self) -> usize {
self.grid_resolution
}
/// Returns the time step
#[must_use]
pub const fn time_step(&self) -> f64 {
self.time_step
}
/// Returns the maximum iterations
#[must_use]
pub const fn max_iterations(&self) -> usize {
self.max_iterations
}
/// Returns the convergence tolerance
#[must_use]
pub const fn convergence_tolerance(&self) -> f64 {
self.convergence_tolerance
}
/// Returns the fluid properties
#[must_use]
pub const fn fluid(&self) -> &FluidProperties {
&self.fluid
}
/// Returns the inlet boundary condition
#[must_use]
pub const fn inlet_bc(&self) -> &BoundaryCondition {
&self.inlet_bc
}
/// Returns the outlet boundary condition
#[must_use]
pub const fn outlet_bc(&self) -> &BoundaryCondition {
&self.outlet_bc
}
/// Computes the CFL number for stability checking
///
/// CFL = u * dt / dx where dx = L / resolution
#[must_use]
pub fn cfl_number(&self, velocity: f64, length: f64) -> f64 {
let dx = length / self.grid_resolution as f64;
velocity * self.time_step / dx
}
/// Checks if the CFL condition is satisfied (CFL < 1)
#[must_use]
pub fn is_stable(&self, velocity: f64, length: f64) -> bool {
self.cfl_number(velocity, length) < 1.0
}
}
impl Default for SimulationConfig {
fn default() -> Self {
Self::builder().build().unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blood_properties() {
let blood = FluidProperties::blood();
assert!((blood.density() - 1060.0).abs() < 1.0);
assert!((blood.dynamic_viscosity() - 0.0035).abs() < 0.001);
}
#[test]
fn test_kinematic_viscosity() {
let fluid = FluidProperties::new(1000.0, 0.001).unwrap();
let nu = fluid.kinematic_viscosity();
assert!((nu - 1e-6).abs() < 1e-12);
}
#[test]
fn test_reynolds_number() {
let water = FluidProperties::new(1000.0, 0.001).unwrap();
let re = water.reynolds_number(1.0, 0.01);
// Re = 1000 * 1 * 0.01 / 0.001 = 10000
assert!((re - 10000.0).abs() < 0.1);
}
#[test]
fn test_pulsatile_inlet() {
let bc = BoundaryCondition::pulsatile_inlet(0.1, 0.05, 1.0).unwrap();
// At t=0, v = mean = 0.1
assert!((bc.evaluate(0.0) - 0.1).abs() < 1e-10);
// At t=0.25 (quarter period), v = mean + amplitude = 0.15
assert!((bc.evaluate(0.25) - 0.15).abs() < 1e-10);
}
#[test]
fn test_config_cfl() {
let config = SimulationConfig::builder()
.grid_resolution(100)
.time_step(0.0001)
.build()
.unwrap();
// CFL = 1.0 * 0.0001 / (0.1/100) = 0.1
let cfl = config.cfl_number(1.0, 0.1);
assert!((cfl - 0.1).abs() < 1e-10);
assert!(config.is_stable(1.0, 0.1));
}
}