//! Geometry types for MRE tissue domains and phantoms use serde::{Deserialize, Serialize}; /// 2D point with vector operations #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)] pub struct Point2D { pub x: f32, pub y: f32, } impl Point2D { /// Create a new point #[must_use] pub const fn new(x: f32, y: f32) -> Self { Self { x, y } } /// Distance to another point #[must_use] pub fn distance(&self, other: &Self) -> f32 { let dx = self.x - other.x; let dy = self.y - other.y; (dx * dx + dy * dy).sqrt() } /// Squared distance (faster, no sqrt) #[must_use] pub fn distance_sq(&self, other: &Self) -> f32 { let dx = self.x - other.x; let dy = self.y - other.y; dx * dx + dy * dy } } /// A circular tissue region with specific stiffness /// Used to define tumors, lesions, or other inclusions #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TissueRegion { /// Center of the circular region pub center: Point2D, /// Radius in meters pub radius: f32, /// Stiffness in kiloPascals (kPa) /// Typical values: /// - Healthy liver: 2-6 kPa /// - Fibrotic liver: 8-12 kPa /// - Tumor/HCC: 15-75 kPa pub stiffness_kpa: f32, } impl TissueRegion { /// Create a tumor-like inclusion #[must_use] pub fn tumor(center: Point2D, radius: f32) -> Self { Self { center, radius, stiffness_kpa: 15.0, // Typical tumor stiffness } } /// Create a fibrotic region #[must_use] pub fn fibrotic(center: Point2D, radius: f32) -> Self { Self { center, radius, stiffness_kpa: 10.0, } } /// Check if a point is inside this region #[must_use] pub fn contains(&self, point: &Point2D) -> bool { self.center.distance_sq(point) <= self.radius * self.radius } } /// Configuration for generating synthetic phantoms #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PhantomConfig { /// Physical domain size in meters (width, height) pub domain_size: (f32, f32), /// Background tissue stiffness in kPa (healthy tissue) pub background_stiffness: f32, /// List of tissue inclusions (tumors, lesions, etc.) pub inclusions: Vec, /// Grid resolution for stiffness map (nx, ny) pub resolution: (usize, usize), } impl Default for PhantomConfig { fn default() -> Self { Self { domain_size: (0.1, 0.1), // 10cm x 10cm background_stiffness: 3.0, // 3 kPa (healthy liver) inclusions: vec![TissueRegion::tumor(Point2D::new(0.05, 0.05), 0.015)], resolution: (64, 64), } } } impl PhantomConfig { /// Create a simple phantom with a single centered tumor #[must_use] pub fn single_tumor() -> Self { let (w, h) = (0.1, 0.1); Self { domain_size: (w, h), background_stiffness: 3.0, inclusions: vec![TissueRegion::tumor(Point2D::new(w / 2.0, h / 2.0), 0.015)], resolution: (64, 64), } } /// Create a phantom with multiple lesions #[must_use] pub fn multiple_lesions() -> Self { let (w, h) = (0.1, 0.1); Self { domain_size: (w, h), background_stiffness: 3.0, inclusions: vec![ TissueRegion::tumor(Point2D::new(0.03, 0.05), 0.01), TissueRegion::fibrotic(Point2D::new(0.07, 0.05), 0.012), TissueRegion { center: Point2D::new(0.05, 0.075), radius: 0.008, stiffness_kpa: 20.0, // Very stiff lesion }, ], resolution: (64, 64), } } /// Get stiffness at a point based on phantom configuration #[must_use] pub fn stiffness_at(&self, point: &Point2D) -> f32 { // Check each inclusion (last one wins for overlaps) for region in self.inclusions.iter().rev() { if region.contains(point) { return region.stiffness_kpa; } } self.background_stiffness } } #[cfg(test)] mod tests { use super::*; #[test] fn test_point_distance() { let p1 = Point2D::new(0.0, 0.0); let p2 = Point2D::new(3.0, 4.0); assert!((p1.distance(&p2) - 5.0).abs() < 1e-6); } #[test] fn test_region_contains() { let region = TissueRegion::tumor(Point2D::new(0.5, 0.5), 0.1); assert!(region.contains(&Point2D::new(0.5, 0.5))); assert!(region.contains(&Point2D::new(0.55, 0.55))); assert!(!region.contains(&Point2D::new(0.0, 0.0))); } #[test] fn test_phantom_stiffness() { let config = PhantomConfig::single_tumor(); // Background assert!((config.stiffness_at(&Point2D::new(0.01, 0.01)) - 3.0).abs() < 0.01); // Inside tumor assert!((config.stiffness_at(&Point2D::new(0.05, 0.05)) - 15.0).abs() < 0.01); } }