Files
rustytorch/crates/specialized/rtx-digital-twin/src/twin.rs
T
2026-03-04 00:08:42 +00:00

505 lines
15 KiB
Rust

//! Digital twin main module.
//!
//! This module provides the high-level `DigitalTwin` interface that
//! combines geometry, tissue properties, physics, and interventions.
use serde::{Deserialize, Serialize};
use crate::error::{DigitalTwinError, Result};
use crate::geometry::OrganGeometry;
use crate::intervention::Intervention;
use crate::physics::{BioheatModel, BioheatParams, BoundaryCondition, SimulationResult};
/// Configuration for digital twin.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TwinConfig {
/// Bioheat simulation parameters
pub bioheat_params: BioheatParams,
/// Default boundary condition
pub boundary_condition: BoundaryCondition,
/// Enable automatic damage calculation
pub compute_damage: bool,
}
impl Default for TwinConfig {
fn default() -> Self {
Self {
bioheat_params: BioheatParams::default(),
boundary_condition: BoundaryCondition::Temperature(37.0),
compute_damage: true,
}
}
}
/// Medical digital twin for patient-specific organ simulation.
///
/// The digital twin combines:
/// - Patient-specific geometry from medical imaging
/// - Tissue property database
/// - Physics simulation (bioheat equation)
/// - Intervention modeling (ablation probes, HIFU, etc.)
/// - What-if analysis for treatment planning
///
/// # Example
///
/// ```rust,ignore
/// use rtx_digital_twin::{DigitalTwin, OrganGeometry, AblationProbe};
///
/// // Create geometry from segmentation
/// let geometry = OrganGeometry::from_labels(&labels, [64, 64, 64], [1.0, 1.0, 1.0])?;
///
/// // Create digital twin
/// let mut twin = DigitalTwin::new(geometry);
///
/// // Add ablation probe
/// let probe = AblationProbe::new([32.0, 32.0, 32.0], 50.0);
///
/// // Run simulation
/// let result = twin.simulate_intervention(&probe, 60.0)?;
///
/// println!("Max temperature: {:.1}°C", result.max_temperature);
/// println!("Damaged volume: {:.1} mm³", result.damaged_volume);
/// ```
pub struct DigitalTwin {
/// Patient-specific geometry
geometry: OrganGeometry,
/// Configuration
config: TwinConfig,
/// Bioheat model
bioheat: BioheatModel,
/// Latest simulation result
last_result: Option<SimulationResult>,
}
impl DigitalTwin {
/// Create a new digital twin with the given geometry.
pub fn new(geometry: OrganGeometry) -> Self {
let config = TwinConfig::default();
let bioheat = BioheatModel::new(config.bioheat_params.clone());
Self {
geometry,
config,
bioheat,
last_result: None,
}
}
/// Create with custom configuration.
pub fn with_config(geometry: OrganGeometry, config: TwinConfig) -> Self {
let bioheat = BioheatModel::new(config.bioheat_params.clone());
Self {
geometry,
config,
bioheat,
last_result: None,
}
}
/// Get reference to geometry.
pub fn geometry(&self) -> &OrganGeometry {
&self.geometry
}
/// Get mutable reference to geometry.
pub fn geometry_mut(&mut self) -> &mut OrganGeometry {
&mut self.geometry
}
/// Get configuration.
pub fn config(&self) -> &TwinConfig {
&self.config
}
/// Update configuration.
pub fn set_config(&mut self, config: TwinConfig) {
self.bioheat = BioheatModel::new(config.bioheat_params.clone());
self.config = config;
}
/// Get the last simulation result.
pub fn last_result(&self) -> Option<&SimulationResult> {
self.last_result.as_ref()
}
/// Run steady-state bioheat simulation without intervention.
///
/// Computes the natural temperature distribution in the tissue
/// based on blood perfusion, metabolic heat, and boundary conditions.
pub fn simulate_baseline(&mut self) -> Result<&SimulationResult> {
self.bioheat.clear_heat_source();
let result = self
.bioheat
.solve_steady_state(&self.geometry, &self.config.boundary_condition)?;
self.last_result = Some(result);
Ok(self.last_result.as_ref().unwrap())
}
/// Run transient simulation with an intervention.
///
/// # Arguments
/// * `intervention` - The therapeutic intervention (probe, HIFU, etc.)
/// * `duration` - Simulation duration in seconds
///
/// # Returns
/// Simulation result with temperature and damage fields
pub fn simulate_intervention<I: Intervention>(
&mut self,
intervention: &I,
duration: f32,
) -> Result<&SimulationResult> {
// Generate heat source from intervention
let heat_source = intervention.generate_heat_source(&self.geometry)?;
self.bioheat.set_heat_source(heat_source);
// Run transient simulation
let result = self.bioheat.solve_transient(
&self.geometry,
&self.config.boundary_condition,
duration,
)?;
self.last_result = Some(result);
Ok(self.last_result.as_ref().unwrap())
}
/// Run steady-state simulation with an intervention.
///
/// Useful for quick assessment of temperature distribution.
pub fn simulate_intervention_steady<I: Intervention>(
&mut self,
intervention: &I,
) -> Result<&SimulationResult> {
let heat_source = intervention.generate_heat_source(&self.geometry)?;
self.bioheat.set_heat_source(heat_source);
let result = self
.bioheat
.solve_steady_state(&self.geometry, &self.config.boundary_condition)?;
self.last_result = Some(result);
Ok(self.last_result.as_ref().unwrap())
}
/// Perform what-if analysis for an intervention.
///
/// Runs the simulation and returns a summary of expected outcomes.
pub fn what_if<I: Intervention>(
&mut self,
intervention: &I,
duration: f32,
) -> Result<WhatIfResult> {
// Get geometry info before simulation
let voxel_volume = self.geometry.spacing().iter().product::<f32>();
let shape = self.geometry.shape();
let n = shape[0] * shape[1] * shape[2];
// Run simulation
self.simulate_intervention(intervention, duration)?;
// Clone result to avoid borrow issues
let sim_result = self.last_result.clone().ok_or_else(|| {
DigitalTwinError::SimulationError("No result after simulation".to_string())
})?;
// Analyze results
let max_temp = sim_result.max_temperature;
let damaged_volume = sim_result.damaged_volume;
// Count voxels at different damage levels
let mut severe_damage_volume = 0.0;
let mut moderate_damage_volume = 0.0;
for &damage in &sim_result.damage {
if damage > 4.6 {
// CEM43 > 240 min equivalent
severe_damage_volume += voxel_volume;
} else if damage > 1.0 {
moderate_damage_volume += voxel_volume;
}
}
// Check safety margins (temperature at boundary)
let mut max_boundary_temp = 0.0f32;
for z in [0, shape[2] - 1] {
for y in 0..shape[1] {
for x in 0..shape[0] {
let idx = z * shape[0] * shape[1] + y * shape[0] + x;
if idx < n {
max_boundary_temp = max_boundary_temp.max(sim_result.temperature[idx]);
}
}
}
}
let safety_margin_ok = max_boundary_temp < 45.0; // Below tissue damage threshold
Ok(WhatIfResult {
intervention_type: format!("{:?}", intervention.intervention_type()),
intervention_power: intervention.power(),
duration,
max_temperature: max_temp,
total_damaged_volume: damaged_volume,
severe_damage_volume,
moderate_damage_volume,
max_boundary_temperature: max_boundary_temp,
safety_margin_ok,
iterations: sim_result.iterations,
})
}
/// Update geometry with simulation results.
///
/// Copies temperature and damage from last simulation into geometry.
pub fn apply_result_to_geometry(&mut self) -> Result<()> {
if let Some(ref result) = self.last_result {
self.geometry.set_temperature_field(&result.temperature)?;
// Also update damage
for (i, voxel) in self.geometry.data_mut().iter_mut().enumerate() {
voxel.damage = result.damage[i];
}
Ok(())
} else {
Err(DigitalTwinError::SimulationError(
"No simulation result available".to_string(),
))
}
}
/// Reset temperature field to body temperature.
pub fn reset_temperature(&mut self) {
for voxel in self.geometry.data_mut() {
voxel.temperature = 37.0;
voxel.damage = 0.0;
}
self.last_result = None;
}
/// Get summary statistics of the current geometry.
pub fn geometry_summary(&self) -> GeometrySummary {
let shape = self.geometry.shape();
let spacing = self.geometry.spacing();
let dims = self.geometry.dimensions();
let histogram = self.geometry.tissue_histogram();
let total_voxels = self.geometry.num_voxels();
let tissue_voxels = total_voxels
- histogram
.get(&crate::tissue::TissueType::Air)
.copied()
.unwrap_or(0);
let voxel_volume = spacing[0] * spacing[1] * spacing[2];
let tissue_volume = tissue_voxels as f32 * voxel_volume;
GeometrySummary {
shape,
spacing,
dimensions: dims,
total_voxels,
tissue_voxels,
tissue_volume,
num_tissue_types: histogram.len(),
}
}
}
/// Result of what-if analysis.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatIfResult {
/// Type of intervention
pub intervention_type: String,
/// Power of intervention [W]
pub intervention_power: f32,
/// Simulation duration [s]
pub duration: f32,
/// Maximum temperature reached [°C]
pub max_temperature: f32,
/// Total volume with damage > 1 [mm³]
pub total_damaged_volume: f32,
/// Volume with severe damage [mm³]
pub severe_damage_volume: f32,
/// Volume with moderate damage [mm³]
pub moderate_damage_volume: f32,
/// Maximum temperature at boundary [°C]
pub max_boundary_temperature: f32,
/// Whether safety margins are satisfied
pub safety_margin_ok: bool,
/// Number of simulation iterations
pub iterations: usize,
}
impl WhatIfResult {
/// Generate a summary report.
pub fn report(&self) -> String {
format!(
r"What-If Analysis Report
======================
Intervention: {}
Power: {:.1} W
Duration: {:.1} s
Temperature Results:
- Maximum: {:.1}°C
- Boundary max: {:.1}°C
- Safety OK: {}
Damage Assessment:
- Total damaged: {:.1} mm³
- Severe damage: {:.1} mm³
- Moderate damage: {:.1} mm³
Simulation:
- Iterations: {}
",
self.intervention_type,
self.intervention_power,
self.duration,
self.max_temperature,
self.max_boundary_temperature,
if self.safety_margin_ok {
"Yes"
} else {
"NO - REVIEW REQUIRED"
},
self.total_damaged_volume,
self.severe_damage_volume,
self.moderate_damage_volume,
self.iterations,
)
}
}
/// Summary of geometry statistics.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeometrySummary {
/// Volume shape [x, y, z]
pub shape: [usize; 3],
/// Voxel spacing [mm]
pub spacing: [f32; 3],
/// Physical dimensions [mm]
pub dimensions: [f32; 3],
/// Total number of voxels
pub total_voxels: usize,
/// Number of tissue voxels (non-air)
pub tissue_voxels: usize,
/// Total tissue volume [mm³]
pub tissue_volume: f32,
/// Number of different tissue types
pub num_tissue_types: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::TissueLabel;
use crate::intervention::AblationProbe;
use crate::tissue::TissueType;
fn create_test_twin() -> DigitalTwin {
// Create a 20x20x20 geometry with liver in the center
let mut geometry = OrganGeometry::new([20, 20, 20], [1.0, 1.0, 1.0]);
// Fill interior with liver
for z in 2..18 {
for y in 2..18 {
for x in 2..18 {
geometry.set_label(x, y, z, TissueLabel::from(TissueType::Liver));
}
}
}
DigitalTwin::new(geometry)
}
#[test]
fn test_twin_creation() {
let twin = create_test_twin();
assert_eq!(twin.geometry().shape(), [20, 20, 20]);
assert!(twin.last_result().is_none());
}
#[test]
fn test_baseline_simulation() {
let mut twin = create_test_twin();
let result = twin.simulate_baseline().unwrap();
// Should converge without errors
assert!(result.iterations > 0);
// Temperature should be around body temp
assert!(result.max_temperature < 40.0);
}
#[test]
fn test_intervention_simulation() {
let mut twin = create_test_twin();
// Use higher power and larger active region for more noticeable heating
let probe = AblationProbe::new([10.0, 10.0, 10.0], 50.0)
.with_active_length(6.0)
.with_diameter(3.0);
// Run steady-state instead of transient (more stable numerically)
let result = twin.simulate_intervention_steady(&probe).unwrap();
// Should complete without error and have valid results
assert!(result.iterations > 0, "Should have run some iterations");
// Temperature should be at least body temperature
assert!(
result.max_temperature >= 37.0,
"Temperature should be at least body temp, got {}",
result.max_temperature
);
}
#[test]
fn test_what_if_analysis() {
let mut twin = create_test_twin();
let probe = AblationProbe::new([10.0, 10.0, 10.0], 30.0);
let what_if = twin.what_if(&probe, 5.0).unwrap();
// Should have results
assert!(what_if.max_temperature > 37.0);
assert!(what_if.iterations > 0);
// Report should be generated
let report = what_if.report();
assert!(report.contains("What-If Analysis"));
}
#[test]
fn test_geometry_summary() {
let twin = create_test_twin();
let summary = twin.geometry_summary();
assert_eq!(summary.shape, [20, 20, 20]);
assert_eq!(summary.total_voxels, 8000);
assert!(summary.tissue_voxels > 0);
}
#[test]
fn test_reset_temperature() {
let mut twin = create_test_twin();
// Set some non-standard temperature
twin.geometry_mut().set_temperature(10, 10, 10, 50.0);
twin.reset_temperature();
// Should be back to body temp
let voxel = twin.geometry().get(10, 10, 10).unwrap();
assert_eq!(voxel.temperature, 37.0);
assert_eq!(voxel.damage, 0.0);
}
}