Files
rustytorch/demos/rtx-tumorboard-demo/src/report_generator.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

553 lines
17 KiB
Rust

//! 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<CTFindings>,
mri_findings: &Option<MRIFindings>,
pathology_findings: &Option<PathologyFindings>,
fused: &FusedAnalysis,
recommendations: &[Recommendation],
) -> Result<MedicalReport, ReportError> {
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<CTFindings>,
mri: &Option<MRIFindings>,
) -> 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"));
}
}