//! 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 { 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 { 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, /// 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, radius: f64, porosity: f64) -> Result { 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, /// Aneurysm parameters aneurysm: Option, /// Stent parameters stent: Option, }, } /// 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 { 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 { 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 { 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); } }