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

385 lines
11 KiB
Rust

//! 3D Geometry types for thermal ablation simulation
use serde::{Deserialize, Serialize};
/// 3D point with vector operations
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
pub struct Point3D {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Point3D {
/// Create a new 3D point
#[must_use]
pub const fn new(x: f32, y: f32, z: f32) -> Self {
Self { x, y, z }
}
/// Create a point at the origin
#[must_use]
pub const fn origin() -> Self {
Self::new(0.0, 0.0, 0.0)
}
/// Distance to another point
#[must_use]
pub fn distance(&self, other: &Self) -> f32 {
self.distance_sq(other).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;
let dz = self.z - other.z;
dx * dx + dy * dy + dz * dz
}
/// Magnitude (length) of the vector from origin
#[must_use]
pub fn magnitude(&self) -> f32 {
(self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
}
/// Normalize to unit vector
#[must_use]
pub fn normalize(&self) -> Self {
let mag = self.magnitude();
if mag > 1e-10 {
Self::new(self.x / mag, self.y / mag, self.z / mag)
} else {
Self::new(0.0, 0.0, 1.0) // Default direction
}
}
/// Dot product with another vector
#[must_use]
pub fn dot(&self, other: &Self) -> f32 {
self.x * other.x + self.y * other.y + self.z * other.z
}
/// Cross product with another vector
#[must_use]
pub fn cross(&self, other: &Self) -> Self {
Self::new(
self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x,
)
}
/// Add two points/vectors
#[must_use]
pub fn add(&self, other: &Self) -> Self {
Self::new(self.x + other.x, self.y + other.y, self.z + other.z)
}
/// Subtract another point/vector
#[must_use]
pub fn sub(&self, other: &Self) -> Self {
Self::new(self.x - other.x, self.y - other.y, self.z - other.z)
}
/// Scale by a scalar value
#[must_use]
pub fn scale(&self, s: f32) -> Self {
Self::new(self.x * s, self.y * s, self.z * s)
}
/// Convert to array
#[must_use]
pub fn to_array(&self) -> [f32; 3] {
[self.x, self.y, self.z]
}
/// Create from array
#[must_use]
pub fn from_array(arr: [f32; 3]) -> Self {
Self::new(arr[0], arr[1], arr[2])
}
}
/// 3D axis-aligned bounding box
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct BoundingBox3D {
/// Minimum corner (x_min, y_min, z_min)
pub min: Point3D,
/// Maximum corner (x_max, y_max, z_max)
pub max: Point3D,
}
impl BoundingBox3D {
/// Create a new bounding box
#[must_use]
pub fn new(min: Point3D, max: Point3D) -> Self {
Self { min, max }
}
/// Create a centered cube with given half-size
#[must_use]
pub fn centered_cube(center: Point3D, half_size: f32) -> Self {
Self {
min: Point3D::new(
center.x - half_size,
center.y - half_size,
center.z - half_size,
),
max: Point3D::new(
center.x + half_size,
center.y + half_size,
center.z + half_size,
),
}
}
/// Create a box from dimensions centered at origin
#[must_use]
pub fn from_dimensions(width: f32, height: f32, depth: f32) -> Self {
Self {
min: Point3D::new(-width / 2.0, -height / 2.0, -depth / 2.0),
max: Point3D::new(width / 2.0, height / 2.0, depth / 2.0),
}
}
/// Get the size of the box in each dimension
#[must_use]
pub fn size(&self) -> Point3D {
self.max.sub(&self.min)
}
/// Get the center of the box
#[must_use]
pub fn center(&self) -> Point3D {
Point3D::new(
f32::midpoint(self.min.x, self.max.x),
f32::midpoint(self.min.y, self.max.y),
f32::midpoint(self.min.z, self.max.z),
)
}
/// Check if a point is inside the box
#[must_use]
pub fn contains(&self, point: &Point3D) -> bool {
point.x >= self.min.x
&& point.x <= self.max.x
&& point.y >= self.min.y
&& point.y <= self.max.y
&& point.z >= self.min.z
&& point.z <= self.max.z
}
/// Get volume of the box
#[must_use]
pub fn volume(&self) -> f32 {
let s = self.size();
s.x * s.y * s.z
}
}
impl Default for BoundingBox3D {
fn default() -> Self {
// Default 10cm x 10cm x 10cm box (typical liver section)
Self::from_dimensions(0.1, 0.1, 0.1)
}
}
/// Geometry of the ablation probe (e.g., RF needle, laser fiber)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProbeGeometry {
/// Position of the probe tip in meters
pub position: Point3D,
/// Direction vector of the probe axis (normalized)
pub direction: [f32; 3],
/// Active heating length in meters (the part that generates heat)
pub active_length: f32,
/// Probe radius in meters
pub radius: f32,
}
impl ProbeGeometry {
/// Create a new probe geometry
#[must_use]
pub fn new(position: Point3D, direction: [f32; 3], active_length: f32, radius: f32) -> Self {
// Normalize direction
let mag = (direction[0] * direction[0]
+ direction[1] * direction[1]
+ direction[2] * direction[2])
.sqrt();
let normalized = if mag > 1e-10 {
[direction[0] / mag, direction[1] / mag, direction[2] / mag]
} else {
[0.0, 0.0, 1.0] // Default to Z-axis
};
Self {
position,
direction: normalized,
active_length,
radius,
}
}
/// Create a typical RF ablation needle
#[must_use]
pub fn rf_needle(position: Point3D) -> Self {
Self::new(
position,
[0.0, 0.0, 1.0], // Aligned with Z-axis
0.03, // 3cm active length
0.0009, // ~17 gauge needle (0.9mm radius)
)
}
/// Create a typical microwave ablation antenna
#[must_use]
pub fn microwave_antenna(position: Point3D) -> Self {
Self::new(
position,
[0.0, 0.0, 1.0],
0.04, // 4cm active length
0.0012, // ~14 gauge (1.2mm radius)
)
}
/// Create a laser fiber probe
#[must_use]
pub fn laser_fiber(position: Point3D) -> Self {
Self::new(
position,
[0.0, 0.0, 1.0],
0.01, // 1cm diffusing tip
0.0003, // 600μm fiber (0.3mm radius)
)
}
/// Get the direction as a Point3D vector
#[must_use]
pub fn direction_vec(&self) -> Point3D {
Point3D::from_array(self.direction)
}
/// Get the end point of the active region
#[must_use]
pub fn active_end(&self) -> Point3D {
let dir = self.direction_vec();
self.position.add(&dir.scale(self.active_length))
}
/// Calculate distance from a point to the probe axis (cylindrical distance)
/// Returns (axial_distance, radial_distance) where:
/// - axial_distance is distance along the probe axis from tip (negative if before tip)
/// - radial_distance is perpendicular distance from axis
#[must_use]
pub fn distance_to_axis(&self, point: &Point3D) -> (f32, f32) {
let dir = self.direction_vec();
let to_point = point.sub(&self.position);
// Project onto axis
let axial = to_point.dot(&dir);
// Perpendicular component
let axial_component = dir.scale(axial);
let perpendicular = to_point.sub(&axial_component);
let radial = perpendicular.magnitude();
(axial, radial)
}
/// Check if a point is within the active heating region
#[must_use]
pub fn is_in_active_region(&self, point: &Point3D) -> bool {
let (axial, radial) = self.distance_to_axis(point);
axial >= 0.0 && axial <= self.active_length && radial <= self.radius
}
}
impl Default for ProbeGeometry {
fn default() -> Self {
Self::rf_needle(Point3D::origin())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_point3d_distance() {
let p1 = Point3D::origin();
let p2 = Point3D::new(3.0, 4.0, 0.0);
assert!((p1.distance(&p2) - 5.0).abs() < 1e-6);
}
#[test]
fn test_point3d_normalize() {
let p = Point3D::new(3.0, 4.0, 0.0);
let n = p.normalize();
assert!((n.magnitude() - 1.0).abs() < 1e-6);
assert!((n.x - 0.6).abs() < 1e-6);
assert!((n.y - 0.8).abs() < 1e-6);
}
#[test]
fn test_point3d_cross() {
let x = Point3D::new(1.0, 0.0, 0.0);
let y = Point3D::new(0.0, 1.0, 0.0);
let z = x.cross(&y);
assert!((z.x).abs() < 1e-6);
assert!((z.y).abs() < 1e-6);
assert!((z.z - 1.0).abs() < 1e-6);
}
#[test]
fn test_bounding_box_contains() {
let bbox = BoundingBox3D::centered_cube(Point3D::origin(), 1.0);
assert!(bbox.contains(&Point3D::origin()));
assert!(bbox.contains(&Point3D::new(0.5, 0.5, 0.5)));
assert!(!bbox.contains(&Point3D::new(2.0, 0.0, 0.0)));
}
#[test]
fn test_probe_distance_to_axis() {
let probe = ProbeGeometry::rf_needle(Point3D::origin());
// Point on the axis
let (axial, radial) = probe.distance_to_axis(&Point3D::new(0.0, 0.0, 0.01));
assert!((axial - 0.01).abs() < 1e-6);
assert!(radial.abs() < 1e-6);
// Point off the axis
let (axial, radial) = probe.distance_to_axis(&Point3D::new(0.01, 0.0, 0.01));
assert!((axial - 0.01).abs() < 1e-6);
assert!((radial - 0.01).abs() < 1e-6);
}
#[test]
fn test_probe_active_region() {
let probe = ProbeGeometry::new(
Point3D::origin(),
[0.0, 0.0, 1.0],
0.03, // 3cm active length
0.001, // 1mm radius
);
// Inside active region
assert!(probe.is_in_active_region(&Point3D::new(0.0, 0.0, 0.015)));
// Outside (beyond active length)
assert!(!probe.is_in_active_region(&Point3D::new(0.0, 0.0, 0.04)));
// Outside (too far radially)
assert!(!probe.is_in_active_region(&Point3D::new(0.01, 0.0, 0.015)));
}
#[test]
fn test_serialize_probe() {
let probe = ProbeGeometry::rf_needle(Point3D::new(0.05, 0.05, 0.0));
let json = serde_json::to_string(&probe).unwrap();
let deserialized: ProbeGeometry = serde_json::from_str(&json).unwrap();
assert_eq!(probe.position, deserialized.position);
}
}