//! 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, } 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( &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( &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( &mut self, intervention: &I, duration: f32, ) -> Result { // Get geometry info before simulation let voxel_volume = self.geometry.spacing().iter().product::(); 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); } }