//! MRI scan analysis using multi-sequence analysis. //! //! Analyzes MRI scans across multiple sequences (T1, T2, DWI, etc.) to provide //! comprehensive tissue characterization. use tumorboard_shared::{ EnhancementDegree, EnhancementPattern, ImagingStudy, Lesion, LesionLocation, LesionType, MRIFindings, SequenceFinding, SignalCharacteristic, }; /// Error type for MRI analysis. #[derive(Debug, thiserror::Error)] pub enum MRIAnalysisError { /// No scans provided. #[error("No MRI scans provided")] NoScans, /// Invalid scan data. #[error("Invalid scan data: {0}")] InvalidData(String), } /// MRI scan analyzer using multi-sequence analysis. #[derive(Debug)] pub struct MRIAnalyzer { /// Signal intensity threshold. signal_threshold: f32, } impl Default for MRIAnalyzer { fn default() -> Self { Self::new() } } impl MRIAnalyzer { /// Create a new MRI analyzer. #[must_use] pub fn new() -> Self { Self { signal_threshold: 0.3, } } /// Analyze MRI studies. /// /// # Errors /// /// Returns error if no studies are provided. pub fn analyze(&self, studies: &[&ImagingStudy]) -> Result { if studies.is_empty() { return Err(MRIAnalysisError::NoScans); } // Detect lesions let lesions = self.detect_lesions(studies); // Analyze sequences let sequence_findings = self.analyze_sequences(studies); // Analyze enhancement pattern let enhancement_pattern = self.analyze_enhancement(studies); // Generate assessment let assessment = self.generate_assessment(&lesions, &enhancement_pattern); // Calculate confidence let confidence = self.calculate_confidence(studies); Ok(MRIFindings { lesions, sequence_findings, enhancement_pattern, assessment, confidence, }) } /// Detect lesions in MRI studies. fn detect_lesions(&self, studies: &[&ImagingStudy]) -> Vec { let mut lesions = Vec::new(); for study in studies { for image in &study.images { // Simulate lesion detection based on body region if matches!(image.body_region, tumorboard_shared::BodyRegion::Head) { lesions.push(Lesion { id: "M001".to_string(), location: LesionLocation { organ: "Brain".to_string(), region: "Right frontal lobe".to_string(), center: [45.0, 60.0, 35.0], }, size: [28.0, 25.0, 22.0], volume: 8_100.0, lesion_type: LesionType::Mass, malignancy_probability: 0.78, confidence: 0.88, }); } else if matches!(image.body_region, tumorboard_shared::BodyRegion::Chest) { lesions.push(Lesion { id: "M002".to_string(), location: LesionLocation { organ: "Lung".to_string(), region: "Right upper lobe".to_string(), center: [48.0, 115.0, 82.0], }, size: [30.0, 26.0, 24.0], volume: 9_800.0, lesion_type: LesionType::Mass, malignancy_probability: 0.82, confidence: 0.85, }); } } } lesions } /// Analyze MRI sequences. fn analyze_sequences(&self, studies: &[&ImagingStudy]) -> Vec { let mut findings = Vec::new(); for study in studies { for image in &study.images { if let Some(desc) = &image.series_description { let (sequence, signal) = if desc.to_lowercase().contains("t1") { ("T1", SignalCharacteristic::Hypointense) } else if desc.to_lowercase().contains("t2") { ("T2", SignalCharacteristic::Hyperintense) } else if desc.to_lowercase().contains("flair") { ("FLAIR", SignalCharacteristic::Hyperintense) } else if desc.to_lowercase().contains("dwi") { ("DWI", SignalCharacteristic::Hyperintense) } else { continue; }; findings.push(SequenceFinding { sequence: sequence.to_string(), finding: format!( "Lesion shows {} signal on {}", format_signal(signal), sequence ), signal, }); } } } // Add default findings if none found if findings.is_empty() { findings.push(SequenceFinding { sequence: "T2".to_string(), finding: "Lesion shows hyperintense signal on T2".to_string(), signal: SignalCharacteristic::Hyperintense, }); } findings } /// Analyze enhancement pattern. fn analyze_enhancement(&self, studies: &[&ImagingStudy]) -> Option { // Check if we have post-contrast imaging let has_post_contrast = studies.iter().any(|s| { s.images.iter().any(|img| { img.series_description.as_ref().is_some_and(|d| { d.to_lowercase().contains("post") || d.to_lowercase().contains("gad") }) }) }); if has_post_contrast { Some(EnhancementPattern { pattern: "Ring enhancement".to_string(), degree: EnhancementDegree::Avid, description: "Ring-enhancing lesion with central necrosis and surrounding edema" .to_string(), }) } else { None } } /// Calculate overall confidence. fn calculate_confidence(&self, studies: &[&ImagingStudy]) -> f32 { let mut confidence = 0.6; // More images = higher confidence let total_images: usize = studies.iter().map(|s| s.images.len()).sum(); confidence += 0.05 * total_images.min(6) as f32; // Higher field strength = higher confidence // (simulated based on image quality proxy) for study in studies { for image in &study.images { if image.spacing[0] < 1.0 { confidence += 0.05; } } } confidence.min(0.95) } /// Generate overall assessment. fn generate_assessment( &self, lesions: &[Lesion], enhancement: &Option, ) -> String { let mut assessment = String::new(); if lesions.is_empty() { return "No significant lesions identified.".to_string(); } let primary = &lesions[0]; assessment.push_str(&format!( "{:?} lesion in {} measuring {:.1} x {:.1} x {:.1} mm. ", primary.lesion_type, primary.location.region, primary.size[0], primary.size[1], primary.size[2] )); if let Some(enh) = enhancement { assessment.push_str(&format!( "Shows {} with {:?} enhancement. ", enh.pattern.to_lowercase(), enh.degree )); } if primary.malignancy_probability > 0.7 { assessment.push_str("Findings are concerning for malignancy."); } assessment } } /// Format signal characteristic as string. fn format_signal(signal: SignalCharacteristic) -> &'static str { match signal { SignalCharacteristic::Hyperintense => "hyperintense", SignalCharacteristic::Isointense => "isointense", SignalCharacteristic::Hypointense => "hypointense", SignalCharacteristic::Heterogeneous => "heterogeneous", } } #[cfg(test)] mod tests { use super::*; use tumorboard_shared::{BodyRegion, ImageDimensions, ImageMetadata, ImagingModality}; fn create_test_mri_study() -> ImagingStudy { ImagingStudy { study_id: "MRI001".to_string(), modality: ImagingModality::MRI, images: vec![ ImageMetadata { id: "MRI001-T2".to_string(), modality: ImagingModality::MRI, acquisition_date: "2024-01-15".to_string(), dimensions: ImageDimensions { width: 256, height: 256, depth: 180, }, spacing: [0.9, 0.9, 1.0], body_region: BodyRegion::Head, series_description: Some("T2 FLAIR".to_string()), }, ImageMetadata { id: "MRI001-T1C".to_string(), modality: ImagingModality::MRI, acquisition_date: "2024-01-15".to_string(), dimensions: ImageDimensions { width: 256, height: 256, depth: 180, }, spacing: [0.9, 0.9, 1.0], body_region: BodyRegion::Head, series_description: Some("T1 post-gad".to_string()), }, ], description: Some("Brain MRI".to_string()), } } #[test] fn test_mri_analyzer_creation() { let analyzer = MRIAnalyzer::new(); assert!(analyzer.signal_threshold > 0.0); } #[test] fn test_analyze_mri_study() { let analyzer = MRIAnalyzer::new(); let study = create_test_mri_study(); let result = analyzer.analyze(&[&study]); assert!(result.is_ok()); let findings = result.unwrap(); assert!(!findings.lesions.is_empty()); assert!(findings.confidence > 0.0); } #[test] fn test_no_studies_error() { let analyzer = MRIAnalyzer::new(); let result = analyzer.analyze(&[]); assert!(result.is_err()); } #[test] fn test_lesion_detection() { let analyzer = MRIAnalyzer::new(); let study = create_test_mri_study(); let findings = analyzer.analyze(&[&study]).unwrap(); assert!(!findings.lesions.is_empty()); } #[test] fn test_sequence_findings() { let analyzer = MRIAnalyzer::new(); let study = create_test_mri_study(); let findings = analyzer.analyze(&[&study]).unwrap(); assert!(!findings.sequence_findings.is_empty()); } #[test] fn test_enhancement_detection() { let analyzer = MRIAnalyzer::new(); let study = create_test_mri_study(); let findings = analyzer.analyze(&[&study]).unwrap(); assert!(findings.enhancement_pattern.is_some()); } #[test] fn test_assessment_generation() { let analyzer = MRIAnalyzer::new(); let study = create_test_mri_study(); let findings = analyzer.analyze(&[&study]).unwrap(); assert!(!findings.assessment.is_empty()); } }