//! Medical report generation. //! //! Generates structured medical reports from multi-modal analysis. use tumorboard_shared::{ CTFindings, CaseAnalysisRequest, FusedAnalysis, MRIFindings, MedicalReport, PathologyFindings, Recommendation, ReportMetadata, ReportSection, }; /// Error type for report generation. #[derive(Debug, thiserror::Error)] pub enum ReportError { /// Missing required data. #[error("Missing required data: {0}")] MissingData(String), /// Template error. #[error("Report template error: {0}")] TemplateError(String), } /// Report generator. #[derive(Debug)] pub struct ReportGenerator { /// Include detailed methodology. include_methodology: bool, /// Model version. model_version: String, } impl Default for ReportGenerator { fn default() -> Self { Self::new() } } impl ReportGenerator { /// Create a new report generator. #[must_use] pub fn new() -> Self { Self { include_methodology: true, model_version: "TumorBoard-AI-v1.0".to_string(), } } /// Generate a medical report. /// /// # Errors /// /// Returns error if required data is missing. pub fn generate( &self, request: &CaseAnalysisRequest, ct_findings: &Option, mri_findings: &Option, pathology_findings: &Option, fused: &FusedAnalysis, recommendations: &[Recommendation], ) -> Result { let mut sections = Vec::new(); // Clinical history section sections.push(self.build_clinical_section(request)); // Imaging findings section if ct_findings.is_some() || mri_findings.is_some() { sections.push(self.build_imaging_section(ct_findings, mri_findings)); } // Pathology section if let Some(path) = pathology_findings { sections.push(self.build_pathology_section(path)); } // Integrated findings section sections.push(self.build_integrated_section(fused)); // Recommendations section if !recommendations.is_empty() { sections.push(self.build_recommendations_section(recommendations)); } // Methodology section if self.include_methodology { sections.push(self.build_methodology_section()); } // Generate summary let summary = self.build_summary(fused); // Generate impression let impression = self.build_impression(fused); Ok(MedicalReport { sections, summary, impression, metadata: ReportMetadata { generated_at: chrono_lite_timestamp(), model_version: self.model_version.clone(), disclaimer: self.generate_disclaimer(), }, }) } /// Build clinical history section. fn build_clinical_section(&self, request: &CaseAnalysisRequest) -> ReportSection { let mut content = String::new(); content.push_str("CLINICAL HISTORY:\n\n"); if let Some(age) = request.patient_info.age { content.push_str(&format!("Age: {age} years\n")); } if let Some(sex) = &request.patient_info.sex { content.push_str(&format!("Sex: {sex:?}\n")); } if !request.patient_info.medical_history.is_empty() { content.push_str("\nRelevant History:\n"); for item in &request.patient_info.medical_history { content.push_str(&format!(" - {item}\n")); } } content.push_str(&format!( "\nClinical Question: {}\n", request.clinical_context.clinical_question )); if !request.clinical_context.lab_values.is_empty() { content.push_str("\nRelevant Lab Values:\n"); for lab in &request.clinical_context.lab_values { content.push_str(&format!(" - {}: {} {}\n", lab.name, lab.value, lab.unit)); } } ReportSection { title: "Clinical History".to_string(), content, image_references: vec![], } } /// Build imaging findings section. fn build_imaging_section( &self, ct: &Option, mri: &Option, ) -> ReportSection { let mut content = String::new(); content.push_str("IMAGING FINDINGS:\n\n"); if let Some(ct_findings) = ct { content.push_str("CT Findings:\n"); content.push_str(&format!(" {}\n", ct_findings.assessment)); if !ct_findings.lesions.is_empty() { content.push_str("\n Lesions:\n"); for lesion in &ct_findings.lesions { content.push_str(&format!( " - {}: {:.1} x {:.1} x {:.1} mm, {:?}\n", lesion.location.region, lesion.size[0], lesion.size[1], lesion.size[2], lesion.lesion_type )); } } content.push_str(&format!( "\n Confidence: {:.0}%\n", ct_findings.confidence * 100.0 )); } if let Some(mri_findings) = mri { if ct.is_some() { content.push('\n'); } content.push_str("MRI Findings:\n"); content.push_str(&format!(" {}\n", mri_findings.assessment)); if !mri_findings.sequence_findings.is_empty() { content.push_str("\n Sequence Findings:\n"); for seq in &mri_findings.sequence_findings { content.push_str(&format!(" - {}: {}\n", seq.sequence, seq.finding)); } } if let Some(enh) = &mri_findings.enhancement_pattern { content.push_str(&format!( "\n Enhancement: {} ({:?})\n", enh.pattern, enh.degree )); } content.push_str(&format!( "\n Confidence: {:.0}%\n", mri_findings.confidence * 100.0 )); } ReportSection { title: "Imaging Findings".to_string(), content, image_references: vec![], } } /// Build pathology section. fn build_pathology_section(&self, path: &PathologyFindings) -> ReportSection { let mut content = String::new(); content.push_str("PATHOLOGY FINDINGS:\n\n"); content.push_str(&format!("Diagnosis: {}\n", path.assessment)); if let Some(grade) = &path.grading { content.push_str(&format!("\nGrade: {} ({})\n", grade.grade, grade.system)); } if !path.biomarkers.is_empty() { content.push_str("\nBiomarkers:\n"); for marker in &path.biomarkers { let expr = marker .expression .map(|e| format!(" ({e:.0}%)")) .unwrap_or_default(); content.push_str(&format!( " - {}: {:?}{}\n", marker.name, marker.status, expr )); } } content.push_str(&format!("\nConfidence: {:.0}%\n", path.confidence * 100.0)); ReportSection { title: "Pathology Findings".to_string(), content, image_references: vec![], } } /// Build integrated findings section. fn build_integrated_section(&self, fused: &FusedAnalysis) -> ReportSection { let mut content = String::new(); content.push_str("INTEGRATED ANALYSIS:\n\n"); content.push_str(&format!( "Primary Diagnosis: {} (probability: {:.0}%)\n", fused.primary_diagnosis.name, fused.primary_diagnosis.probability * 100.0 )); if let Some(icd) = &fused.primary_diagnosis.icd_code { content.push_str(&format!("ICD-10: {icd}\n")); } if !fused.differential_diagnoses.is_empty() { content.push_str("\nDifferential Diagnoses:\n"); for dx in &fused.differential_diagnoses { content.push_str(&format!( " - {} ({:.0}%)\n", dx.name, dx.probability * 100.0 )); } } if let Some(assessment) = &fused.tumor_assessment { if let Some(tnm) = &assessment.tnm_stage { content.push_str(&format!( "\nTNM Staging: T{} N{} M{} (Stage {})\n", tnm.t, tnm.n, tnm.m, tnm.overall_stage )); } if !assessment.prognostic_factors.is_empty() { content.push_str("\nPrognostic Factors:\n"); for factor in &assessment.prognostic_factors { content.push_str(&format!( " - {}: {} ({:?})\n", factor.name, factor.value, factor.impact )); } } } if !fused.correlations.is_empty() { content.push_str("\nCross-Modal Correlations:\n"); for corr in &fused.correlations { content.push_str(&format!( " - {:?} ↔ {:?}: {} ({:?})\n", corr.modality_a, corr.modality_b, corr.description, corr.correlation_type )); } } content.push_str(&format!( "\nOverall Confidence: {:.0}%\n", fused.confidence * 100.0 )); ReportSection { title: "Integrated Analysis".to_string(), content, image_references: vec![], } } /// Build recommendations section. fn build_recommendations_section(&self, recommendations: &[Recommendation]) -> ReportSection { let mut content = String::new(); content.push_str("RECOMMENDATIONS:\n\n"); for (i, rec) in recommendations.iter().enumerate() { content.push_str(&format!( "{}. [{:?}] {:?}: {}\n", i + 1, rec.priority, rec.recommendation_type, rec.description )); content.push_str(&format!(" Rationale: {}\n\n", rec.rationale)); } ReportSection { title: "Recommendations".to_string(), content, image_references: vec![], } } /// Build methodology section. fn build_methodology_section(&self) -> ReportSection { let content = r"METHODOLOGY: This analysis was performed using the TumorBoard AI system, which employs deep learning models for multi-modal medical image analysis. CT Analysis: - 3D convolutional neural network for lesion detection - Volumetric measurement using automated segmentation - Malignancy probability estimation based on imaging features MRI Analysis: - Multi-sequence feature extraction - Enhancement pattern characterization - Diffusion analysis when available Pathology Analysis: - Whole slide image analysis - Cell detection and classification - Biomarker quantification using validated algorithms Multi-Modal Fusion: - Cross-attention mechanism for modality integration - Concordance assessment between modalities - Integrated staging using AJCC guidelines " .to_string(); ReportSection { title: "Methodology".to_string(), content, image_references: vec![], } } /// Build summary. fn build_summary(&self, fused: &FusedAnalysis) -> String { format!( "{} with {:.0}% probability. {}", fused.primary_diagnosis.name, fused.primary_diagnosis.probability * 100.0, fused .tumor_assessment .as_ref() .and_then(|a| a.tnm_stage.as_ref()) .map(|s| format!("Stage {}.", s.overall_stage)) .unwrap_or_default() ) } /// Build impression. fn build_impression(&self, fused: &FusedAnalysis) -> String { let mut impression = String::new(); impression.push_str(&format!( "1. {}: {:.0}% probability\n", fused.primary_diagnosis.name, fused.primary_diagnosis.probability * 100.0 )); if let Some(assessment) = &fused.tumor_assessment && let Some(tnm) = &assessment.tnm_stage { impression.push_str(&format!( "2. Clinical stage: {} (T{}N{}M{})\n", tnm.overall_stage, tnm.t, tnm.n, tnm.m )); } impression.push_str(&format!( "3. Overall analysis confidence: {:.0}%\n", fused.confidence * 100.0 )); impression } /// Generate disclaimer. fn generate_disclaimer(&self) -> String { "DISCLAIMER: This AI-generated report is intended to assist healthcare providers \ and should not replace clinical judgment. All findings should be correlated with \ clinical history and verified by qualified medical professionals before treatment \ decisions are made. This system is not FDA-approved for clinical use." .to_string() } } /// Generate a simple timestamp. fn chrono_lite_timestamp() -> String { use std::time::{SystemTime, UNIX_EPOCH}; let duration = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default(); format!("{}000", duration.as_secs()) } #[cfg(test)] mod tests { use super::*; use tumorboard_shared::{ AnalysisConfig, ClinicalContext, Diagnosis, ImagingModality, LabValue, PatientInfo, Sex, }; fn create_test_request() -> CaseAnalysisRequest { CaseAnalysisRequest { case_id: "TEST001".to_string(), patient_info: PatientInfo { age: Some(65), sex: Some(Sex::Male), medical_history: vec!["Former smoker".to_string()], }, studies: vec![], clinical_context: ClinicalContext { clinical_question: "Evaluate lung nodule".to_string(), known_diagnosis: None, lab_values: vec![LabValue { name: "CEA".to_string(), value: 8.5, unit: "ng/mL".to_string(), reference_range: Some((0.0, 3.0)), }], prior_treatments: vec![], }, config: AnalysisConfig::default(), } } fn create_fused_analysis() -> FusedAnalysis { FusedAnalysis { primary_diagnosis: Diagnosis { icd_code: Some("C34.9".to_string()), name: "Lung adenocarcinoma".to_string(), probability: 0.85, evidence: vec![], }, differential_diagnoses: vec![], correlations: vec![], tumor_assessment: None, confidence: 0.88, } } #[test] fn test_report_generator_creation() { let generator = ReportGenerator::new(); assert!(generator.include_methodology); } #[test] fn test_generate_report() { let generator = ReportGenerator::new(); let request = create_test_request(); let fused = create_fused_analysis(); let result = generator.generate(&request, &None, &None, &None, &fused, &[]); assert!(result.is_ok()); let report = result.unwrap(); assert!(!report.sections.is_empty()); assert!(!report.summary.is_empty()); } #[test] fn test_clinical_section() { let generator = ReportGenerator::new(); let request = create_test_request(); let section = generator.build_clinical_section(&request); assert!(section.content.contains("Age: 65")); assert!(section.content.contains("Former smoker")); } #[test] fn test_summary_generation() { let generator = ReportGenerator::new(); let fused = create_fused_analysis(); let summary = generator.build_summary(&fused); assert!(summary.contains("Lung adenocarcinoma")); assert!(summary.contains("85%")); } #[test] fn test_impression_generation() { let generator = ReportGenerator::new(); let fused = create_fused_analysis(); let impression = generator.build_impression(&fused); assert!(impression.contains("Lung adenocarcinoma")); } #[test] fn test_disclaimer() { let generator = ReportGenerator::new(); let disclaimer = generator.generate_disclaimer(); assert!(disclaimer.contains("DISCLAIMER")); assert!(disclaimer.contains("clinical judgment")); } #[test] fn test_methodology_section() { let generator = ReportGenerator::new(); let section = generator.build_methodology_section(); assert!(section.content.contains("METHODOLOGY")); assert!(section.content.contains("deep learning")); } }