690 lines
21 KiB
Rust
690 lines
21 KiB
Rust
//! Vessel geometry primitives and signed distance functions
|
|
//!
|
|
//! This module provides 2D vessel geometry representations for hemodynamics
|
|
//! simulation, including straight vessels, stenoses, and aneurysms.
|
|
//!
|
|
//! # Signed Distance Functions
|
|
//!
|
|
//! All geometries implement signed distance functions (SDF) where:
|
|
//! - Negative values indicate points inside the vessel
|
|
//! - Positive values indicate points outside the vessel
|
|
//! - Zero indicates points on the vessel boundary
|
|
//!
|
|
//! # Example
|
|
//!
|
|
//! ```rust
|
|
//! use rtx_hemodynamics_shared::geometry::{VesselGeometry, Point2D, StenosisParams};
|
|
//!
|
|
//! // Create a straight vessel
|
|
//! let vessel = VesselGeometry::straight(0.1, 0.005).unwrap();
|
|
//!
|
|
//! // Check if a point is inside
|
|
//! let point = Point2D::new(0.05, 0.002);
|
|
//! let sdf = vessel.signed_distance(&point);
|
|
//! assert!(sdf < 0.0); // Inside vessel
|
|
//!
|
|
//! // Add stenosis
|
|
//! let stenosis = StenosisParams::new(0.5, 0.02, 0.005).unwrap();
|
|
//! let stenotic_vessel = vessel.with_stenosis(stenosis);
|
|
//! ```
|
|
|
|
use crate::error::{HemodynamicsError, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// 2D point representation
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub struct Point2D {
|
|
/// X coordinate (axial position along vessel)
|
|
pub x: f64,
|
|
/// Y coordinate (radial position)
|
|
pub y: f64,
|
|
}
|
|
|
|
impl Point2D {
|
|
/// Creates a new 2D point
|
|
#[must_use]
|
|
pub const fn new(x: f64, y: f64) -> Self {
|
|
Self { x, y }
|
|
}
|
|
|
|
/// Creates the origin point (0, 0)
|
|
#[must_use]
|
|
pub const fn origin() -> Self {
|
|
Self::new(0.0, 0.0)
|
|
}
|
|
|
|
/// Calculates the Euclidean distance to another point
|
|
#[must_use]
|
|
pub fn distance(&self, other: &Self) -> f64 {
|
|
let dx = self.x - other.x;
|
|
let dy = self.y - other.y;
|
|
(dx * dx + dy * dy).sqrt()
|
|
}
|
|
|
|
/// Returns the magnitude (length) of this point as a vector
|
|
#[must_use]
|
|
pub fn magnitude(&self) -> f64 {
|
|
(self.x * self.x + self.y * self.y).sqrt()
|
|
}
|
|
|
|
/// Returns a normalized (unit length) version of this point as a vector
|
|
///
|
|
/// Returns `(0, 0)` for zero-length vectors.
|
|
#[must_use]
|
|
pub fn normalize(&self) -> Self {
|
|
let mag = self.magnitude();
|
|
if mag < f64::EPSILON {
|
|
Self::origin()
|
|
} else {
|
|
Self::new(self.x / mag, self.y / mag)
|
|
}
|
|
}
|
|
|
|
/// Computes the dot product with another point/vector
|
|
#[must_use]
|
|
pub fn dot(&self, other: &Self) -> f64 {
|
|
self.x * other.x + self.y * other.y
|
|
}
|
|
|
|
/// Computes the 2D cross product (z-component of 3D cross product)
|
|
#[must_use]
|
|
pub fn cross(&self, other: &Self) -> f64 {
|
|
self.x * other.y - self.y * other.x
|
|
}
|
|
|
|
/// Adds another point/vector to this one
|
|
#[must_use]
|
|
pub fn add(&self, other: &Self) -> Self {
|
|
Self::new(self.x + other.x, self.y + other.y)
|
|
}
|
|
|
|
/// Subtracts another point/vector from this one
|
|
#[must_use]
|
|
pub fn sub(&self, other: &Self) -> Self {
|
|
Self::new(self.x - other.x, self.y - other.y)
|
|
}
|
|
|
|
/// Scales this point/vector by a scalar
|
|
#[must_use]
|
|
pub fn scale(&self, factor: f64) -> Self {
|
|
Self::new(self.x * factor, self.y * factor)
|
|
}
|
|
|
|
/// Linear interpolation between this point and another
|
|
#[must_use]
|
|
pub fn lerp(&self, other: &Self, t: f64) -> Self {
|
|
Self::new(
|
|
self.x + (other.x - self.x) * t,
|
|
self.y + (other.y - self.y) * t,
|
|
)
|
|
}
|
|
|
|
/// Returns the perpendicular vector (rotated 90 degrees counter-clockwise)
|
|
#[must_use]
|
|
pub fn perpendicular(&self) -> Self {
|
|
Self::new(-self.y, self.x)
|
|
}
|
|
}
|
|
|
|
impl Default for Point2D {
|
|
fn default() -> Self {
|
|
Self::origin()
|
|
}
|
|
}
|
|
|
|
/// Stenosis (narrowing) parameters
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub struct StenosisParams {
|
|
/// Ratio of stenotic diameter to healthy diameter (0 < ratio < 1)
|
|
diameter_ratio: f64,
|
|
/// Length of the stenotic region in meters
|
|
length: f64,
|
|
/// Center position along vessel axis (x-coordinate)
|
|
center_x: f64,
|
|
}
|
|
|
|
impl StenosisParams {
|
|
/// Creates new stenosis parameters
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `diameter_ratio` - Ratio of stenotic diameter to healthy diameter (0 < ratio < 1)
|
|
/// * `length` - Length of the stenotic region in meters
|
|
/// * `center_x` - Center position along vessel axis
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if:
|
|
/// - `diameter_ratio` is not in (0, 1)
|
|
/// - `length` is not positive
|
|
pub fn new(diameter_ratio: f64, length: f64, center_x: f64) -> Result<Self> {
|
|
if diameter_ratio <= 0.0 || diameter_ratio >= 1.0 {
|
|
return Err(HemodynamicsError::invalid_geometry(
|
|
"diameter_ratio must be between 0 and 1 (exclusive)",
|
|
));
|
|
}
|
|
if length <= 0.0 {
|
|
return Err(HemodynamicsError::invalid_geometry(
|
|
"stenosis length must be positive",
|
|
));
|
|
}
|
|
Ok(Self {
|
|
diameter_ratio,
|
|
length,
|
|
center_x,
|
|
})
|
|
}
|
|
|
|
/// Returns the diameter ratio
|
|
#[must_use]
|
|
pub const fn diameter_ratio(&self) -> f64 {
|
|
self.diameter_ratio
|
|
}
|
|
|
|
/// Returns the stenosis length
|
|
#[must_use]
|
|
pub const fn length(&self) -> f64 {
|
|
self.length
|
|
}
|
|
|
|
/// Returns the center x-position
|
|
#[must_use]
|
|
pub const fn center_x(&self) -> f64 {
|
|
self.center_x
|
|
}
|
|
|
|
/// Computes the radius modifier at a given x position
|
|
///
|
|
/// Returns 1.0 outside the stenosis, and a smooth cosine transition
|
|
/// to `diameter_ratio` at the center.
|
|
#[must_use]
|
|
pub fn radius_modifier(&self, x: f64) -> f64 {
|
|
let dx = x - self.center_x;
|
|
let half_length = self.length / 2.0;
|
|
|
|
if dx.abs() > half_length {
|
|
1.0
|
|
} else {
|
|
// Smooth cosine transition using raised cosine profile
|
|
// At center (t=0): blend_factor = 0, returns diameter_ratio
|
|
// At edges (t=±1): blend_factor = 1, returns 1.0
|
|
let t = dx / half_length; // -1 to 1
|
|
// Use (1 - cos(t*π))/2 which is 0 at t=0 and 1 at t=±1
|
|
let blend_factor = (1.0 - (t * std::f64::consts::PI).cos()) / 2.0;
|
|
self.diameter_ratio + (1.0 - self.diameter_ratio) * blend_factor
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Aneurysm (bulging) parameters
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub struct AneurysmParams {
|
|
/// Ratio of aneurysm diameter to healthy diameter (ratio > 1)
|
|
diameter_ratio: f64,
|
|
/// Radius of the aneurysm sac
|
|
sac_radius: f64,
|
|
/// Center position of the aneurysm
|
|
center: Point2D,
|
|
}
|
|
|
|
impl AneurysmParams {
|
|
/// Creates new aneurysm parameters
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `diameter_ratio` - Ratio of aneurysm diameter to healthy diameter (must be > 1)
|
|
/// * `sac_radius` - Radius of the aneurysm sac
|
|
/// * `center` - Center position of the aneurysm
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if:
|
|
/// - `diameter_ratio` is not greater than 1
|
|
/// - `sac_radius` is not positive
|
|
pub fn new(diameter_ratio: f64, sac_radius: f64, center: Point2D) -> Result<Self> {
|
|
if diameter_ratio <= 1.0 {
|
|
return Err(HemodynamicsError::invalid_geometry(
|
|
"aneurysm diameter_ratio must be greater than 1",
|
|
));
|
|
}
|
|
if sac_radius <= 0.0 {
|
|
return Err(HemodynamicsError::invalid_geometry(
|
|
"aneurysm sac_radius must be positive",
|
|
));
|
|
}
|
|
Ok(Self {
|
|
diameter_ratio,
|
|
sac_radius,
|
|
center,
|
|
})
|
|
}
|
|
|
|
/// Returns the diameter ratio
|
|
#[must_use]
|
|
pub const fn diameter_ratio(&self) -> f64 {
|
|
self.diameter_ratio
|
|
}
|
|
|
|
/// Returns the sac radius
|
|
#[must_use]
|
|
pub const fn sac_radius(&self) -> f64 {
|
|
self.sac_radius
|
|
}
|
|
|
|
/// Returns the center position
|
|
#[must_use]
|
|
pub const fn center(&self) -> &Point2D {
|
|
&self.center
|
|
}
|
|
}
|
|
|
|
/// Stent parameters for flow diversion
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct StentParams {
|
|
/// Path of the stent centerline
|
|
path: Vec<Point2D>,
|
|
/// Stent radius
|
|
radius: f64,
|
|
/// Porosity (0 to 1, where 0 is fully solid and 1 is fully open)
|
|
porosity: f64,
|
|
}
|
|
|
|
impl StentParams {
|
|
/// Creates new stent parameters
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `path` - Path of the stent centerline (at least 2 points)
|
|
/// * `radius` - Stent radius
|
|
/// * `porosity` - Porosity (0 to 1)
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if:
|
|
/// - `path` has fewer than 2 points
|
|
/// - `radius` is not positive
|
|
/// - `porosity` is not in [0, 1]
|
|
pub fn new(path: Vec<Point2D>, radius: f64, porosity: f64) -> Result<Self> {
|
|
if path.len() < 2 {
|
|
return Err(HemodynamicsError::invalid_geometry(
|
|
"stent path must have at least 2 points",
|
|
));
|
|
}
|
|
if radius <= 0.0 {
|
|
return Err(HemodynamicsError::invalid_geometry(
|
|
"stent radius must be positive",
|
|
));
|
|
}
|
|
if !(0.0..=1.0).contains(&porosity) {
|
|
return Err(HemodynamicsError::invalid_geometry(
|
|
"stent porosity must be between 0 and 1",
|
|
));
|
|
}
|
|
Ok(Self {
|
|
path,
|
|
radius,
|
|
porosity,
|
|
})
|
|
}
|
|
|
|
/// Returns the stent path
|
|
#[must_use]
|
|
pub fn path(&self) -> &[Point2D] {
|
|
&self.path
|
|
}
|
|
|
|
/// Returns the stent radius
|
|
#[must_use]
|
|
pub const fn radius(&self) -> f64 {
|
|
self.radius
|
|
}
|
|
|
|
/// Returns the stent porosity
|
|
#[must_use]
|
|
pub const fn porosity(&self) -> f64 {
|
|
self.porosity
|
|
}
|
|
}
|
|
|
|
/// Vessel type enumeration
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub enum VesselType {
|
|
/// Straight vessel with uniform radius
|
|
Straight,
|
|
/// Vessel with stenosis (narrowing)
|
|
Stenotic(StenosisParams),
|
|
/// Vessel with aneurysm (bulging)
|
|
Aneurysmal(AneurysmParams),
|
|
/// Vessel with both stenosis and aneurysm
|
|
Complex {
|
|
/// Stenosis parameters
|
|
stenosis: Option<StenosisParams>,
|
|
/// Aneurysm parameters
|
|
aneurysm: Option<AneurysmParams>,
|
|
/// Stent parameters
|
|
stent: Option<StentParams>,
|
|
},
|
|
}
|
|
|
|
/// Geometry modification commands
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub enum GeometryModification {
|
|
/// Add a stenosis to the vessel
|
|
AddStenosis(StenosisParams),
|
|
/// Add an aneurysm to the vessel
|
|
AddAneurysm(AneurysmParams),
|
|
/// Place a stent
|
|
PlaceStent(StentParams),
|
|
/// Reset to straight vessel
|
|
Reset,
|
|
/// Modify stenosis diameter ratio
|
|
ModifyStenosisSeverity(f64),
|
|
}
|
|
|
|
/// 2D vessel geometry with signed distance function
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct VesselGeometry {
|
|
/// Length of the vessel segment (meters)
|
|
length: f64,
|
|
/// Base radius of healthy vessel (meters)
|
|
base_radius: f64,
|
|
/// Type of vessel (straight, stenotic, aneurysmal, etc.)
|
|
vessel_type: VesselType,
|
|
}
|
|
|
|
impl VesselGeometry {
|
|
/// Creates a straight vessel geometry
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `length` - Length of the vessel segment in meters
|
|
/// * `radius` - Radius of the vessel in meters
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if length or radius is not positive.
|
|
pub fn straight(length: f64, radius: f64) -> Result<Self> {
|
|
if length <= 0.0 {
|
|
return Err(HemodynamicsError::invalid_geometry(
|
|
"vessel length must be positive",
|
|
));
|
|
}
|
|
if radius <= 0.0 {
|
|
return Err(HemodynamicsError::invalid_geometry(
|
|
"vessel radius must be positive",
|
|
));
|
|
}
|
|
Ok(Self {
|
|
length,
|
|
base_radius: radius,
|
|
vessel_type: VesselType::Straight,
|
|
})
|
|
}
|
|
|
|
/// Returns the vessel length
|
|
#[must_use]
|
|
pub const fn length(&self) -> f64 {
|
|
self.length
|
|
}
|
|
|
|
/// Returns the base radius
|
|
#[must_use]
|
|
pub const fn base_radius(&self) -> f64 {
|
|
self.base_radius
|
|
}
|
|
|
|
/// Returns the vessel type
|
|
#[must_use]
|
|
pub const fn vessel_type(&self) -> &VesselType {
|
|
&self.vessel_type
|
|
}
|
|
|
|
/// Creates a new vessel with added stenosis
|
|
#[must_use]
|
|
pub fn with_stenosis(mut self, stenosis: StenosisParams) -> Self {
|
|
self.vessel_type = match self.vessel_type {
|
|
VesselType::Straight => VesselType::Stenotic(stenosis),
|
|
VesselType::Aneurysmal(aneurysm) => VesselType::Complex {
|
|
stenosis: Some(stenosis),
|
|
aneurysm: Some(aneurysm),
|
|
stent: None,
|
|
},
|
|
VesselType::Stenotic(_) => VesselType::Stenotic(stenosis),
|
|
VesselType::Complex {
|
|
aneurysm, stent, ..
|
|
} => VesselType::Complex {
|
|
stenosis: Some(stenosis),
|
|
aneurysm,
|
|
stent,
|
|
},
|
|
};
|
|
self
|
|
}
|
|
|
|
/// Creates a new vessel with added aneurysm
|
|
#[must_use]
|
|
pub fn with_aneurysm(mut self, aneurysm: AneurysmParams) -> Self {
|
|
self.vessel_type = match self.vessel_type {
|
|
VesselType::Straight => VesselType::Aneurysmal(aneurysm),
|
|
VesselType::Stenotic(stenosis) => VesselType::Complex {
|
|
stenosis: Some(stenosis),
|
|
aneurysm: Some(aneurysm),
|
|
stent: None,
|
|
},
|
|
VesselType::Aneurysmal(_) => VesselType::Aneurysmal(aneurysm),
|
|
VesselType::Complex {
|
|
stenosis, stent, ..
|
|
} => VesselType::Complex {
|
|
stenosis,
|
|
aneurysm: Some(aneurysm),
|
|
stent,
|
|
},
|
|
};
|
|
self
|
|
}
|
|
|
|
/// Creates a new vessel with placed stent
|
|
#[must_use]
|
|
pub fn with_stent(mut self, stent: StentParams) -> Self {
|
|
self.vessel_type = match self.vessel_type {
|
|
VesselType::Straight => VesselType::Complex {
|
|
stenosis: None,
|
|
aneurysm: None,
|
|
stent: Some(stent),
|
|
},
|
|
VesselType::Stenotic(stenosis) => VesselType::Complex {
|
|
stenosis: Some(stenosis),
|
|
aneurysm: None,
|
|
stent: Some(stent),
|
|
},
|
|
VesselType::Aneurysmal(aneurysm) => VesselType::Complex {
|
|
stenosis: None,
|
|
aneurysm: Some(aneurysm),
|
|
stent: Some(stent),
|
|
},
|
|
VesselType::Complex {
|
|
stenosis, aneurysm, ..
|
|
} => VesselType::Complex {
|
|
stenosis,
|
|
aneurysm,
|
|
stent: Some(stent),
|
|
},
|
|
};
|
|
self
|
|
}
|
|
|
|
/// Computes the local radius at a given x position
|
|
#[must_use]
|
|
pub fn local_radius(&self, x: f64) -> f64 {
|
|
match &self.vessel_type {
|
|
VesselType::Straight => self.base_radius,
|
|
VesselType::Stenotic(stenosis) => self.base_radius * stenosis.radius_modifier(x),
|
|
VesselType::Aneurysmal(aneurysm) => {
|
|
let dx = x - aneurysm.center.x;
|
|
let influence = (-dx.powi(2) / (2.0 * aneurysm.sac_radius.powi(2))).exp();
|
|
self.base_radius * (1.0 + (aneurysm.diameter_ratio - 1.0) * influence)
|
|
}
|
|
VesselType::Complex {
|
|
stenosis, aneurysm, ..
|
|
} => {
|
|
let mut radius = self.base_radius;
|
|
|
|
if let Some(s) = stenosis {
|
|
radius *= s.radius_modifier(x);
|
|
}
|
|
|
|
if let Some(a) = aneurysm {
|
|
let dx = x - a.center.x;
|
|
let influence = (-dx.powi(2) / (2.0 * a.sac_radius.powi(2))).exp();
|
|
radius *= 1.0 + (a.diameter_ratio - 1.0) * influence;
|
|
}
|
|
|
|
radius
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Computes the signed distance from a point to the vessel boundary
|
|
///
|
|
/// - Negative values: inside vessel
|
|
/// - Positive values: outside vessel
|
|
/// - Zero: on boundary
|
|
#[must_use]
|
|
pub fn signed_distance(&self, point: &Point2D) -> f64 {
|
|
// Clamp x to vessel domain
|
|
if point.x < 0.0 || point.x > self.length {
|
|
// Outside vessel in x direction
|
|
let dx = if point.x < 0.0 {
|
|
-point.x
|
|
} else {
|
|
point.x - self.length
|
|
};
|
|
let local_r = self.local_radius(point.x.clamp(0.0, self.length));
|
|
let dy = point.y.abs() - local_r;
|
|
return (dx.powi(2) + dy.max(0.0).powi(2)).sqrt();
|
|
}
|
|
|
|
// Inside vessel x domain - compute distance to wall
|
|
let local_r = self.local_radius(point.x);
|
|
point.y.abs() - local_r
|
|
}
|
|
|
|
/// Computes the outward-pointing normal at a boundary point
|
|
#[must_use]
|
|
pub fn normal_at(&self, point: &Point2D) -> Point2D {
|
|
// Simple approximation using finite differences
|
|
let eps = 1e-6;
|
|
let sdf = self.signed_distance(point);
|
|
let sdf_dx = self.signed_distance(&Point2D::new(point.x + eps, point.y));
|
|
let sdf_dy = self.signed_distance(&Point2D::new(point.x, point.y + eps));
|
|
|
|
let grad = Point2D::new((sdf_dx - sdf) / eps, (sdf_dy - sdf) / eps);
|
|
|
|
grad.normalize()
|
|
}
|
|
|
|
/// Samples points uniformly along the vessel boundary
|
|
#[must_use]
|
|
pub fn sample_boundary(&self, num_points: usize) -> Vec<Point2D> {
|
|
let mut points = Vec::with_capacity(num_points);
|
|
let half = num_points / 2;
|
|
|
|
// Sample top boundary
|
|
for i in 0..half {
|
|
let x = self.length * (i as f64) / (half as f64 - 1.0);
|
|
let r = self.local_radius(x);
|
|
points.push(Point2D::new(x, r));
|
|
}
|
|
|
|
// Sample bottom boundary (reverse order for continuous path)
|
|
for i in 0..(num_points - half) {
|
|
let x = self.length * (1.0 - (i as f64) / ((num_points - half) as f64 - 1.0));
|
|
let r = self.local_radius(x);
|
|
points.push(Point2D::new(x, -r));
|
|
}
|
|
|
|
points
|
|
}
|
|
|
|
/// Samples points uniformly within the vessel interior
|
|
#[must_use]
|
|
pub fn sample_interior(&self, num_points: usize) -> Vec<Point2D> {
|
|
let mut points = Vec::with_capacity(num_points);
|
|
let sqrt_n = (num_points as f64).sqrt().ceil() as usize;
|
|
|
|
for i in 0..sqrt_n {
|
|
for j in 0..sqrt_n {
|
|
if points.len() >= num_points {
|
|
break;
|
|
}
|
|
|
|
let x = self.length * (i as f64 + 0.5) / (sqrt_n as f64);
|
|
let r = self.local_radius(x);
|
|
let y = r * (2.0 * (j as f64 + 0.5) / (sqrt_n as f64) - 1.0) * 0.9;
|
|
|
|
points.push(Point2D::new(x, y));
|
|
}
|
|
}
|
|
|
|
points.truncate(num_points);
|
|
points
|
|
}
|
|
|
|
/// Returns the bounding box of the vessel
|
|
#[must_use]
|
|
pub fn bounding_box(&self) -> (Point2D, Point2D) {
|
|
// Sample to find max radius
|
|
let mut max_r = self.base_radius;
|
|
for i in 0..100 {
|
|
let x = self.length * f64::from(i) / 99.0;
|
|
max_r = max_r.max(self.local_radius(x));
|
|
}
|
|
|
|
(Point2D::new(0.0, -max_r), Point2D::new(self.length, max_r))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_point2d_operations() {
|
|
let p1 = Point2D::new(1.0, 2.0);
|
|
let p2 = Point2D::new(3.0, 4.0);
|
|
|
|
assert!((p1.dot(&p2) - 11.0).abs() < f64::EPSILON);
|
|
assert!((p1.cross(&p2) - (-2.0)).abs() < f64::EPSILON);
|
|
|
|
let sum = p1.add(&p2);
|
|
assert!((sum.x - 4.0).abs() < f64::EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_stenosis_modifier() {
|
|
let stenosis = StenosisParams::new(0.5, 0.02, 0.05).unwrap();
|
|
|
|
// At center, should be at minimum (diameter_ratio)
|
|
assert!((stenosis.radius_modifier(0.05) - 0.5).abs() < 0.01);
|
|
|
|
// Far from stenosis, should be 1.0
|
|
assert!((stenosis.radius_modifier(0.0) - 1.0).abs() < f64::EPSILON);
|
|
assert!((stenosis.radius_modifier(0.1) - 1.0).abs() < f64::EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vessel_local_radius_stenotic() {
|
|
let vessel = VesselGeometry::straight(0.1, 0.005).unwrap();
|
|
let stenosis = StenosisParams::new(0.5, 0.02, 0.05).unwrap();
|
|
let stenotic = vessel.with_stenosis(stenosis);
|
|
|
|
// At stenosis center
|
|
let r_center = stenotic.local_radius(0.05);
|
|
assert!((r_center - 0.005 * 0.5).abs() < 0.0001);
|
|
|
|
// Far from stenosis
|
|
let r_inlet = stenotic.local_radius(0.0);
|
|
assert!((r_inlet - 0.005).abs() < f64::EPSILON);
|
|
}
|
|
}
|