505 lines
15 KiB
Rust
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);
|
|
}
|
|
}
|