Files
rustytorch/demos/rtx-hemodynamics/src/wss.rs
T
2026-03-04 00:08:42 +00:00

374 lines
11 KiB
Rust

//! Wall Shear Stress (WSS) computation for hemodynamics
//!
//! WSS is the tangential force per unit area exerted by flowing blood on
//! the vessel wall. It's a critical biomarker for:
//! - Aneurysm rupture risk (high WSS at neck)
//! - Atherosclerosis progression (low/oscillatory WSS)
//! - Endothelial cell health
use crate::vessel::VesselSdf;
use rtx_hemodynamics_shared::geometry::Point2D;
/// Wall Shear Stress computer
#[derive(Debug)]
pub struct WssComputer {
/// Dynamic viscosity (Pa.s)
mu: f64,
/// Finite difference step for gradient computation
eps: f64,
}
impl WssComputer {
/// Creates a new WSS computer
///
/// # Arguments
///
/// * `mu` - Dynamic viscosity in Pa.s
#[must_use]
pub fn new(mu: f64) -> Self {
Self { mu, eps: 1e-6 }
}
/// Creates WSS computer for blood
#[must_use]
pub fn blood() -> Self {
Self::new(0.0035)
}
/// Sets the finite difference step size
#[must_use]
pub const fn with_eps(mut self, eps: f64) -> Self {
self.eps = eps;
self
}
/// Returns the viscosity
#[must_use]
pub const fn viscosity(&self) -> f64 {
self.mu
}
/// Computes WSS at a wall point given velocity gradients
///
/// WSS = μ * |∂`u_t/∂n`| where:
/// - `u_t` is the tangential velocity component
/// - n is the wall normal direction
///
/// For a 2D case with wall normal (nx, ny):
/// - Tangent: t = (-ny, nx)
/// - ∂`u_t/∂n` = (∂u/∂n) * tx + (∂v/∂n) * ty
/// where ∂/∂n = nx * ∂/∂x + ny * ∂/∂y
#[must_use]
pub fn compute_wss(
&self,
du_dx: f64,
du_dy: f64,
dv_dx: f64,
dv_dy: f64,
normal: &Point2D,
) -> f64 {
let nx = normal.x;
let ny = normal.y;
// Tangent vector (perpendicular to normal)
let tx = -ny;
let ty = nx;
// Normal gradient of velocity components
// ∂u/∂n = nx * ∂u/∂x + ny * ∂u/∂y
let du_dn = nx * du_dx + ny * du_dy;
let dv_dn = nx * dv_dx + ny * dv_dy;
// Tangential velocity gradient in normal direction
let dut_dn = du_dn * tx + dv_dn * ty;
// WSS magnitude
self.mu * dut_dn.abs()
}
/// Computes WSS using finite differences from velocity field
///
/// # Arguments
///
/// * `wall_point` - Point on the vessel wall
/// * `vessel` - Vessel SDF for normal computation
/// * `velocity_fn` - Function that returns (u, v) at a point
#[must_use]
pub fn compute_wss_from_velocity<F>(
&self,
wall_point: &Point2D,
vessel: &VesselSdf,
velocity_fn: F,
) -> f64
where
F: Fn(&Point2D) -> (f64, f64),
{
// Get wall normal
let normal = vessel.normal(wall_point);
// Compute velocity gradients using finite differences
let (u, v) = velocity_fn(wall_point);
let (u_px, v_px) = velocity_fn(&Point2D::new(wall_point.x + self.eps, wall_point.y));
let (u_py, v_py) = velocity_fn(&Point2D::new(wall_point.x, wall_point.y + self.eps));
let du_dx = (u_px - u) / self.eps;
let du_dy = (u_py - u) / self.eps;
let dv_dx = (v_px - v) / self.eps;
let dv_dy = (v_py - v) / self.eps;
self.compute_wss(du_dx, du_dy, dv_dx, dv_dy, &normal)
}
/// Computes WSS at multiple wall points
#[must_use]
pub fn compute_wss_batch<F>(
&self,
wall_points: &[Point2D],
vessel: &VesselSdf,
velocity_fn: F,
) -> Vec<f64>
where
F: Fn(&Point2D) -> (f64, f64),
{
wall_points
.iter()
.map(|p| self.compute_wss_from_velocity(p, vessel, &velocity_fn))
.collect()
}
/// Computes WSS for Poiseuille flow (analytical validation)
///
/// `τ_w` = μ * |du/dr|_wall = 4μQ / (πR³) = 8μ * `u_mean` / R
///
/// For parabolic profile u(r) = `u_max` * (1 - r²/R²):
/// du/dr = -2 * `u_max` * r / R²
/// At wall (r = R): |du/dr| = 2 * `u_max` / R
/// `τ_w` = 2μ * `u_max` / R
#[must_use]
pub fn poiseuille_wss(&self, u_max: f64, radius: f64) -> f64 {
2.0 * self.mu * u_max / radius
}
/// Computes mean WSS from a set of values
#[must_use]
pub fn mean_wss(wss_values: &[f64]) -> f64 {
if wss_values.is_empty() {
return 0.0;
}
wss_values.iter().sum::<f64>() / wss_values.len() as f64
}
/// Computes max WSS from a set of values
#[must_use]
pub fn max_wss(wss_values: &[f64]) -> f64 {
wss_values.iter().copied().fold(0.0_f64, f64::max)
}
/// Identifies high-risk regions where WSS exceeds threshold
#[must_use]
pub fn high_risk_regions(
wall_points: &[Point2D],
wss_values: &[f64],
threshold: f64,
) -> Vec<(Point2D, f64)> {
wall_points
.iter()
.zip(wss_values.iter())
.filter(|&(_, &wss)| wss > threshold)
.map(|(p, &wss)| (*p, wss))
.collect()
}
/// Computes the Oscillatory Shear Index (OSI) for time-varying WSS
///
/// OSI = 0.5 * (1 - |∫`τ_w` dt| / ∫|`τ_w`| dt)
///
/// OSI = 0: unidirectional flow
/// OSI = 0.5: purely oscillatory flow
#[must_use]
pub fn oscillatory_shear_index(wss_time_series: &[f64]) -> f64 {
if wss_time_series.is_empty() {
return 0.0;
}
let sum_signed: f64 = wss_time_series.iter().sum();
let sum_magnitude: f64 = wss_time_series.iter().map(|w| w.abs()).sum();
if sum_magnitude.abs() < f64::EPSILON {
return 0.0;
}
0.5 * (1.0 - sum_signed.abs() / sum_magnitude)
}
/// Computes Time-Averaged WSS (TAWSS)
#[must_use]
pub fn time_averaged_wss(wss_time_series: &[f64]) -> f64 {
if wss_time_series.is_empty() {
return 0.0;
}
wss_time_series.iter().map(|w| w.abs()).sum::<f64>() / wss_time_series.len() as f64
}
}
/// WSS risk classification thresholds
#[derive(Debug, Clone, Copy)]
pub struct WssThresholds {
/// Low WSS threshold (Pa) - below this indicates atherosclerosis risk
pub low: f64,
/// Normal WSS lower bound (Pa)
pub normal_low: f64,
/// Normal WSS upper bound (Pa)
pub normal_high: f64,
/// High WSS threshold (Pa) - above this indicates rupture risk
pub high: f64,
}
impl Default for WssThresholds {
fn default() -> Self {
// Typical physiological values for arteries
Self {
low: 0.4, // < 0.4 Pa associated with atherosclerosis
normal_low: 1.0,
normal_high: 7.0,
high: 15.0, // > 15 Pa associated with aneurysm rupture risk
}
}
}
impl WssThresholds {
/// Classifies a WSS value
#[must_use]
pub fn classify(&self, wss: f64) -> WssRiskLevel {
if wss < self.low {
WssRiskLevel::Low
} else if wss < self.normal_low {
WssRiskLevel::BelowNormal
} else if wss <= self.normal_high {
WssRiskLevel::Normal
} else if wss <= self.high {
WssRiskLevel::AboveNormal
} else {
WssRiskLevel::High
}
}
}
/// WSS risk level classification
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WssRiskLevel {
/// Very low WSS - atherosclerosis risk
Low,
/// Below normal WSS
BelowNormal,
/// Normal physiological WSS
Normal,
/// Above normal WSS
AboveNormal,
/// Very high WSS - rupture risk
High,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wss_computer_creation() {
let wss = WssComputer::blood();
assert!((wss.viscosity() - 0.0035).abs() < f64::EPSILON);
}
#[test]
fn test_poiseuille_wss() {
let computer = WssComputer::blood();
// For u_max = 0.1 m/s, R = 0.005 m
// τ_w = 2 * 0.0035 * 0.1 / 0.005 = 0.14 Pa
let wss = computer.poiseuille_wss(0.1, 0.005);
assert!((wss - 0.14).abs() < 1e-10);
}
#[test]
fn test_wss_from_gradients() {
let computer = WssComputer::blood();
// Simple shear flow: u = y * γ̇, v = 0
// du/dx = 0, du/dy = γ̇, dv/dx = 0, dv/dy = 0
// At wall with normal (0, 1): WSS = μ * γ̇
let shear_rate = 100.0; // 1/s
let normal = Point2D::new(0.0, 1.0);
let wss = computer.compute_wss(0.0, shear_rate, 0.0, 0.0, &normal);
let expected = 0.0035 * shear_rate;
assert!((wss - expected).abs() < 1e-10);
}
#[test]
fn test_oscillatory_shear_index() {
// Unidirectional flow: all positive WSS
let unidirectional = vec![1.0, 1.0, 1.0, 1.0];
let osi = WssComputer::oscillatory_shear_index(&unidirectional);
assert!(osi.abs() < f64::EPSILON, "Unidirectional OSI should be 0");
// Perfectly oscillatory: equal positive and negative
let oscillatory = vec![1.0, -1.0, 1.0, -1.0];
let osi = WssComputer::oscillatory_shear_index(&oscillatory);
assert!(
(osi - 0.5).abs() < f64::EPSILON,
"Oscillatory OSI should be 0.5"
);
}
#[test]
fn test_time_averaged_wss() {
let wss_series = vec![1.0, 2.0, 3.0, 4.0];
let tawss = WssComputer::time_averaged_wss(&wss_series);
assert!((tawss - 2.5).abs() < f64::EPSILON);
// With negative values
let wss_series = vec![1.0, -2.0, 3.0, -4.0];
let tawss = WssComputer::time_averaged_wss(&wss_series);
assert!((tawss - 2.5).abs() < f64::EPSILON);
}
#[test]
fn test_wss_thresholds() {
let thresholds = WssThresholds::default();
assert_eq!(thresholds.classify(0.2), WssRiskLevel::Low);
assert_eq!(thresholds.classify(0.7), WssRiskLevel::BelowNormal);
assert_eq!(thresholds.classify(3.0), WssRiskLevel::Normal);
assert_eq!(thresholds.classify(10.0), WssRiskLevel::AboveNormal);
assert_eq!(thresholds.classify(20.0), WssRiskLevel::High);
}
#[test]
fn test_high_risk_regions() {
let points = vec![
Point2D::new(0.01, 0.005),
Point2D::new(0.02, 0.005),
Point2D::new(0.03, 0.005),
];
let wss_values = vec![1.0, 20.0, 5.0];
let high_risk = WssComputer::high_risk_regions(&points, &wss_values, 10.0);
assert_eq!(high_risk.len(), 1);
assert!((high_risk[0].0.x - 0.02).abs() < f64::EPSILON);
assert!((high_risk[0].1 - 20.0).abs() < f64::EPSILON);
}
#[test]
fn test_mean_max_wss() {
let wss_values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let mean = WssComputer::mean_wss(&wss_values);
let max = WssComputer::max_wss(&wss_values);
assert!((mean - 3.0).abs() < f64::EPSILON);
assert!((max - 5.0).abs() < f64::EPSILON);
}
}