//! 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 { 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 { 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 { 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 { 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, outlet_bc: Option, } 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 { 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)); } }