374 lines
11 KiB
Rust
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);
|
|
}
|
|
}
|