567 lines
16 KiB
Rust
567 lines
16 KiB
Rust
//! 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));
|
||
}
|
||
}
|