Files
rustytorch/demos/shared/src/geometry.rs
T
2026-03-04 00:08:42 +00:00

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);
}
}