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

610 lines
18 KiB
Rust

//! Integration tests for Medical Digital Twin Platform
//!
//! These tests demonstrate the complete workflow from segmentation to
//! intervention planning and what-if analysis.
use rtx_digital_twin::{
AblationProbe, BioheatParams, BoundaryCondition, DigitalTwin, HifuTransducer, Intervention,
InterventionType, OrganGeometry, TissueDatabase, TissueLabel, TissueType, TwinConfig,
};
/// Test complete workflow: Create geometry from voxel data
#[test]
fn test_organ_geometry_from_voxels() {
// Create a 3D volume with liver and tumor
let shape = [30, 30, 30];
let n = shape[0] * shape[1] * shape[2];
let mut labels = vec![0u8; n]; // Start with air
// Fill center region with liver tissue
for z in 5..25 {
for y in 5..25 {
for x in 5..25 {
let idx = z * shape[0] * shape[1] + y * shape[0] + x;
labels[idx] = TissueType::Liver.label();
}
}
}
// Add a spherical tumor at center
let center = [15, 15, 15];
let tumor_radius = 3;
for z in 0..shape[2] {
for y in 0..shape[1] {
for x in 0..shape[0] {
let dx = x as i32 - center[0] as i32;
let dy = y as i32 - center[1] as i32;
let dz = z as i32 - center[2] as i32;
let dist_sq = dx * dx + dy * dy + dz * dz;
if dist_sq <= tumor_radius * tumor_radius {
let idx = z * shape[0] * shape[1] + y * shape[0] + x;
labels[idx] = TissueType::Tumor.label();
}
}
}
}
// Create geometry
let geometry = OrganGeometry::from_labels(&labels, shape, [1.0, 1.0, 1.0])
.expect("Failed to create geometry");
// Verify shape
assert_eq!(geometry.shape(), shape);
assert_eq!(geometry.num_voxels(), n);
// Verify tissue distribution
let histogram = geometry.tissue_histogram();
assert!(histogram.contains_key(&TissueType::Air), "Should have air");
assert!(
histogram.contains_key(&TissueType::Liver),
"Should have liver"
);
assert!(
histogram.contains_key(&TissueType::Tumor),
"Should have tumor"
);
// Verify center is tumor
let center_voxel = geometry.get(center[0], center[1], center[2]).unwrap();
assert_eq!(center_voxel.label.tissue_type(), TissueType::Tumor);
}
/// Test tissue database with different tissue types
#[test]
fn test_tissue_map_properties() {
let db = TissueDatabase::standard();
// Verify key tissues exist
let liver = db.get(TissueType::Liver).expect("Liver should exist");
assert!(liver.thermal_conductivity > 0.5);
assert!(liver.perfusion_rate > 0.01);
assert!(liver.metabolic_heat > 1000.0);
let tumor = db.get(TissueType::Tumor).expect("Tumor should exist");
assert!(
tumor.perfusion_rate < liver.perfusion_rate,
"Tumor has less perfusion"
);
// Verify thermal diffusivity calculation
let alpha = liver.thermal_diffusivity();
assert!(
alpha > 1e-8 && alpha < 1e-6,
"Reasonable thermal diffusivity"
);
}
/// Test boundary condition application
#[test]
fn test_boundary_conditions() {
let mut geometry = OrganGeometry::new([10, 10, 10], [1.0, 1.0, 1.0]);
// Fill with liver
for z in 0..10 {
for y in 0..10 {
for x in 0..10 {
geometry.set_label(x, y, z, TissueLabel::from(TissueType::Liver));
}
}
}
let config = TwinConfig {
bioheat_params: BioheatParams {
max_iterations: 100,
tolerance: 0.1,
..Default::default()
},
boundary_condition: BoundaryCondition::Temperature(37.0),
compute_damage: true,
};
let mut twin = DigitalTwin::with_config(geometry, config);
// Run baseline simulation
let result = twin.simulate_baseline().expect("Baseline should succeed");
assert!(result.iterations > 0, "Should run some iterations");
assert!(result.iterations <= 100, "Should not exceed max iterations");
// Temperature should be reasonable
for &temp in &result.temperature {
assert!(temp >= 36.0 && temp <= 38.0, "Body temperature range");
}
}
/// Test digital twin simulation with ablation probe
#[test]
fn test_digital_twin_simulation_with_ablation() {
// Create liver with tumor - larger geometry for better numerical stability
let mut geometry = OrganGeometry::new([30, 30, 30], [1.0, 1.0, 1.0]);
// Fill interior with liver
for z in 3..27 {
for y in 3..27 {
for x in 3..27 {
geometry.set_label(x, y, z, TissueLabel::from(TissueType::Liver));
}
}
}
// Add tumor sphere
geometry.create_sphere(
[15.0, 15.0, 15.0],
4.0,
TissueLabel::from(TissueType::Tumor),
);
let config = TwinConfig {
bioheat_params: BioheatParams {
max_iterations: 500,
tolerance: 0.01,
dt: 0.01,
..Default::default()
},
..Default::default()
};
let mut twin = DigitalTwin::with_config(geometry, config);
// Create ablation probe with wider heating zone for stability
// Use larger active length and diameter to distribute heat over more voxels
let probe = AblationProbe::new([15.0, 15.0, 15.0], 30.0)
.with_type(InterventionType::RadiofrequencyAblation)
.with_active_length(10.0)
.with_diameter(4.0);
// Run steady-state simulation
let result = twin
.simulate_intervention_steady(&probe)
.expect("Simulation should succeed");
// Verify results structure
assert!(result.iterations > 0, "Should run iterations");
assert_eq!(result.temperature.len(), 30 * 30 * 30);
assert_eq!(result.damage.len(), 30 * 30 * 30);
// Temperature should be in physical range
assert!(
result.max_temperature >= 37.0,
"Should be at least body temperature"
);
// With heat source, temperature distribution should vary across volume
let temp_variance: f32 = result
.temperature
.iter()
.map(|&t| (t - 37.0).powi(2))
.sum::<f32>()
/ result.temperature.len() as f32;
assert!(
temp_variance > 0.0,
"Temperature should vary with heat source"
);
// Verify simulation ran and produced results
assert!(result.iterations <= 500, "Should not exceed max iterations");
}
/// Test what-if analysis for intervention planning
#[test]
fn test_what_if_analysis_workflow() {
// Create realistic liver geometry
let mut geometry = OrganGeometry::new([40, 40, 40], [1.0, 1.0, 1.0]);
// Fill with liver
for z in 5..35 {
for y in 5..35 {
for x in 5..35 {
geometry.set_label(x, y, z, TissueLabel::from(TissueType::Liver));
}
}
}
// Add tumor
geometry.create_sphere(
[20.0, 20.0, 20.0],
5.0,
TissueLabel::from(TissueType::Tumor),
);
let mut twin = DigitalTwin::new(geometry);
// Test RFA ablation
let rfa_probe = AblationProbe::new([20.0, 20.0, 20.0], 50.0)
.with_type(InterventionType::RadiofrequencyAblation)
.with_active_length(10.0);
let rfa_result = twin
.what_if(&rfa_probe, 10.0)
.expect("RFA what-if should succeed");
// Verify what-if result structure
assert_eq!(rfa_result.intervention_type, "RadiofrequencyAblation");
assert_eq!(rfa_result.intervention_power, 50.0);
assert_eq!(rfa_result.duration, 10.0);
assert!(
rfa_result.max_temperature >= 37.0,
"Temperature should be at least body temp"
);
assert!(rfa_result.iterations > 0);
assert!(rfa_result.total_damaged_volume >= 0.0);
// Generate report
let report = rfa_result.report();
assert!(report.contains("What-If Analysis Report"));
assert!(report.contains("Radiofrequency"));
// Test HIFU
twin.reset_temperature();
let hifu = HifuTransducer::new([20.0, 20.0, 20.0], 100.0);
let hifu_result = twin
.what_if(&hifu, 5.0)
.expect("HIFU what-if should succeed");
assert_eq!(hifu_result.intervention_type, "HIFU");
assert_eq!(hifu_result.intervention_power, 100.0);
assert!(hifu_result.iterations > 0);
}
/// Test multiple interventions and scenario comparison
#[test]
fn test_multiple_intervention_scenarios() {
let mut geometry = OrganGeometry::new([30, 30, 30], [1.0, 1.0, 1.0]);
// Fill with liver
for z in 3..27 {
for y in 3..27 {
for x in 3..27 {
geometry.set_label(x, y, z, TissueLabel::from(TissueType::Liver));
}
}
}
// Add tumor
geometry.create_sphere(
[15.0, 15.0, 15.0],
3.0,
TissueLabel::from(TissueType::Tumor),
);
let mut twin = DigitalTwin::new(geometry);
// Scenario 1: Low power, longer duration
let probe1 = AblationProbe::new([15.0, 15.0, 15.0], 30.0);
let result1 = twin.what_if(&probe1, 15.0).expect("Scenario 1 failed");
// Scenario 2: High power, shorter duration
twin.reset_temperature();
let probe2 = AblationProbe::new([15.0, 15.0, 15.0], 60.0);
let result2 = twin.what_if(&probe2, 7.0).expect("Scenario 2 failed");
// Both scenarios should complete successfully
assert!(result1.iterations > 0, "Scenario 1 should run");
assert!(result2.iterations > 0, "Scenario 2 should run");
assert!(
result1.max_temperature >= 37.0,
"Scenario 1 temperature valid"
);
assert!(
result2.max_temperature >= 37.0,
"Scenario 2 temperature valid"
);
// Both should have valid report structures
let report1 = result1.report();
let report2 = result2.report();
assert!(report1.contains("What-If Analysis"));
assert!(report2.contains("What-If Analysis"));
}
/// Test geometry summary statistics
#[test]
fn test_geometry_summary_statistics() {
let mut geometry = OrganGeometry::new([20, 20, 20], [2.0, 2.0, 2.0]);
// Fill center with liver
for z in 5..15 {
for y in 5..15 {
for x in 5..15 {
geometry.set_label(x, y, z, TissueLabel::from(TissueType::Liver));
}
}
}
let twin = DigitalTwin::new(geometry);
let summary = twin.geometry_summary();
// Verify summary
assert_eq!(summary.shape, [20, 20, 20]);
assert_eq!(summary.spacing, [2.0, 2.0, 2.0]);
assert_eq!(summary.dimensions, [40.0, 40.0, 40.0]);
assert_eq!(summary.total_voxels, 8000);
assert!(summary.tissue_voxels > 0);
assert!(summary.tissue_volume > 0.0);
assert!(summary.num_tissue_types >= 2); // Air + Liver
}
/// Test temperature field updates
#[test]
fn test_temperature_field_operations() {
let mut geometry = OrganGeometry::new([10, 10, 10], [1.0, 1.0, 1.0]);
// Set all to liver
for z in 0..10 {
for y in 0..10 {
for x in 0..10 {
geometry.set_label(x, y, z, TissueLabel::from(TissueType::Liver));
}
}
}
// Set temperature field
let mut temp_field = vec![37.0f32; 1000];
temp_field[500] = 45.0; // Hot spot
geometry
.set_temperature_field(&temp_field)
.expect("Should set temperature");
// Verify
let retrieved = geometry.temperature_field();
assert_eq!(retrieved.len(), 1000);
assert_eq!(retrieved[500], 45.0);
}
/// Test transient simulation
#[test]
fn test_transient_simulation() {
let mut geometry = OrganGeometry::new([15, 15, 15], [1.0, 1.0, 1.0]);
// Fill with liver
for z in 1..14 {
for y in 1..14 {
for x in 1..14 {
geometry.set_label(x, y, z, TissueLabel::from(TissueType::Liver));
}
}
}
let config = TwinConfig {
bioheat_params: BioheatParams {
dt: 0.01,
max_iterations: 1000,
tolerance: 0.001,
..Default::default()
},
..Default::default()
};
let mut twin = DigitalTwin::with_config(geometry, config);
// Small probe for fast simulation
let probe = AblationProbe::new([7.5, 7.5, 7.5], 20.0)
.with_active_length(3.0)
.with_diameter(1.5);
// Run transient simulation for 1 second
let result = twin
.simulate_intervention(&probe, 1.0)
.expect("Transient simulation failed");
// Verify time progression
assert!(
result.time > 0.9 && result.time <= 1.1,
"Should simulate ~1 second"
);
assert!(result.iterations > 0);
// Damage should accumulate over time
let max_damage = result.damage.iter().cloned().fold(0.0f32, f32::max);
assert!(max_damage >= 0.0, "Damage should be non-negative");
}
/// Test thermal property field extraction
#[test]
fn test_property_field_extraction() {
let mut geometry = OrganGeometry::new([8, 8, 8], [1.0, 1.0, 1.0]);
// Create heterogeneous tissue distribution
for z in 0..8 {
for y in 0..8 {
for x in 0..8 {
let tissue = if x < 4 {
TissueType::Liver
} else {
TissueType::Muscle
};
geometry.set_label(x, y, z, TissueLabel::from(tissue));
}
}
}
// Extract property fields
let k_field = geometry.thermal_conductivity_field();
let rho_field = geometry.density_field();
let c_field = geometry.specific_heat_field();
let omega_field = geometry.perfusion_field();
assert_eq!(k_field.len(), 512);
assert_eq!(rho_field.len(), 512);
assert_eq!(c_field.len(), 512);
assert_eq!(omega_field.len(), 512);
// Liver and muscle should have different properties
let liver_props = geometry.tissue_db().get(TissueType::Liver).unwrap();
let muscle_props = geometry.tissue_db().get(TissueType::Muscle).unwrap();
assert!(
(liver_props.thermal_conductivity - muscle_props.thermal_conductivity).abs() > 0.01,
"Different tissues should have different conductivity"
);
}
/// Test intervention heat source generation
#[test]
fn test_intervention_heat_source_generation() {
let geometry = OrganGeometry::new([20, 20, 20], [1.0, 1.0, 1.0]);
// Test ablation probe heat source
let probe = AblationProbe::new([10.0, 10.0, 10.0], 50.0)
.with_active_length(6.0)
.with_diameter(2.0);
let heat_source = probe
.generate_heat_source(&geometry)
.expect("Should generate heat source");
assert_eq!(heat_source.len(), 8000);
// Center should have heat
let center_idx = 10 * 20 * 20 + 10 * 20 + 10;
assert!(heat_source[center_idx] > 0.0, "Center should be heated");
// Corners should be zero
assert_eq!(heat_source[0], 0.0, "Corner should be unheated");
// Test HIFU heat source
let hifu = HifuTransducer::new([10.0, 10.0, 10.0], 100.0);
let hifu_source = hifu
.generate_heat_source(&geometry)
.expect("Should generate HIFU source");
assert_eq!(hifu_source.len(), 8000);
assert!(hifu_source[center_idx] > 0.0, "Focus should be heated");
// HIFU should have Gaussian falloff
let edge_idx = 5 * 20 * 20 + 10 * 20 + 10;
assert!(
hifu_source[center_idx] > hifu_source[edge_idx],
"Focus should be hotter than periphery"
);
}
/// Test damage calculation
#[test]
fn test_damage_calculation() {
let mut geometry = OrganGeometry::new([12, 12, 12], [1.0, 1.0, 1.0]);
// Fill with liver
for z in 1..11 {
for y in 1..11 {
for x in 1..11 {
geometry.set_label(x, y, z, TissueLabel::from(TissueType::Liver));
}
}
}
let mut twin = DigitalTwin::new(geometry);
// High-power ablation
let probe = AblationProbe::new([6.0, 6.0, 6.0], 60.0)
.with_active_length(4.0)
.with_diameter(2.0);
// Run for longer time to accumulate damage
let result = twin
.simulate_intervention(&probe, 5.0)
.expect("Should simulate");
// Some voxels should have damage > 0
let damaged_count = result.damage.iter().filter(|&&d| d > 0.0).count();
assert!(damaged_count > 0, "Should cause some damage");
// Damaged volume should be reported
assert!(
result.damaged_volume >= 0.0,
"Damaged volume should be non-negative"
);
}
/// Test complete clinical workflow
#[test]
fn test_complete_clinical_workflow() {
// Step 1: Create patient-specific geometry from segmentation
let shape = [50, 50, 50];
let n = shape[0] * shape[1] * shape[2];
let mut labels = vec![0u8; n];
// Liver organ
for z in 10..40 {
for y in 10..40 {
for x in 10..40 {
let idx = z * shape[0] * shape[1] + y * shape[0] + x;
labels[idx] = TissueType::Liver.label();
}
}
}
// Tumor lesion
for z in 22..28 {
for y in 22..28 {
for x in 22..28 {
let idx = z * shape[0] * shape[1] + y * shape[0] + x;
labels[idx] = TissueType::Tumor.label();
}
}
}
let geometry = OrganGeometry::from_labels(&labels, shape, [1.0, 1.0, 1.0])
.expect("Should create geometry");
// Step 2: Create digital twin
let mut twin = DigitalTwin::new(geometry);
// Step 3: Run baseline (no intervention)
let baseline = twin.simulate_baseline().expect("Baseline should work");
assert!(
baseline.max_temperature >= 36.0 && baseline.max_temperature < 39.0,
"Baseline temperature in normal range"
);
assert!(baseline.iterations > 0);
// Step 4: Plan intervention targeting tumor
twin.reset_temperature();
let probe = AblationProbe::new([25.0, 25.0, 25.0], 50.0)
.with_type(InterventionType::RadiofrequencyAblation)
.with_active_length(8.0);
// Step 5: What-if analysis
let what_if = twin.what_if(&probe, 10.0).expect("What-if analysis failed");
// Step 6: Evaluate outcome
assert!(
what_if.max_temperature >= 37.0,
"Intervention should produce valid temperature"
);
assert!(
what_if.total_damaged_volume >= 0.0,
"Damaged volume should be non-negative"
);
assert!(what_if.iterations > 0, "Should run simulation iterations");
// Generate clinical report
let report = what_if.report();
assert!(report.contains("Temperature Results"));
assert!(report.contains("Damage Assessment"));
}