Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
+435
View File
@@ -0,0 +1,435 @@
//! `TumorBoard` AI - Agentic Multi-Modal Medical Imaging Analysis.
//!
//! This demo showcases an AI system that analyzes multiple imaging modalities
//! (CT, MRI, pathology) together to provide comprehensive diagnostic insights.
pub mod ct_analyzer;
pub mod fusion;
pub mod mri_analyzer;
pub mod pathology_analyzer;
pub mod report_generator;
pub mod sample_data;
use thiserror::Error;
use tumorboard_shared::{
AnalysisMetadata, CTFindings, CaseAnalysisRequest, CaseAnalysisResult, FusedAnalysis,
MRIFindings, PathologyFindings, Recommendation,
};
use ct_analyzer::CTAnalyzer;
use fusion::MultiModalFusion;
use mri_analyzer::MRIAnalyzer;
use pathology_analyzer::PathologyAnalyzer;
use report_generator::ReportGenerator;
/// Errors that can occur during tumor board analysis.
#[derive(Debug, Error)]
pub enum TumorBoardError {
/// No imaging data provided.
#[error("No imaging data provided for analysis")]
NoImagingData,
/// CT analysis failed.
#[error("CT analysis failed: {0}")]
CTAnalysisError(String),
/// MRI analysis failed.
#[error("MRI analysis failed: {0}")]
MRIAnalysisError(String),
/// Pathology analysis failed.
#[error("Pathology analysis failed: {0}")]
PathologyAnalysisError(String),
/// Fusion failed.
#[error("Multi-modal fusion failed: {0}")]
FusionError(String),
/// Report generation failed.
#[error("Report generation failed: {0}")]
ReportError(String),
}
/// Main tumor board AI system.
#[derive(Debug)]
pub struct TumorBoardAI {
ct_analyzer: CTAnalyzer,
mri_analyzer: MRIAnalyzer,
pathology_analyzer: PathologyAnalyzer,
fusion: MultiModalFusion,
report_generator: ReportGenerator,
}
impl Default for TumorBoardAI {
fn default() -> Self {
Self::new()
}
}
impl TumorBoardAI {
/// Create a new tumor board AI system.
#[must_use]
pub fn new() -> Self {
Self {
ct_analyzer: CTAnalyzer::new(),
mri_analyzer: MRIAnalyzer::new(),
pathology_analyzer: PathologyAnalyzer::new(),
fusion: MultiModalFusion::new(),
report_generator: ReportGenerator::new(),
}
}
/// Analyze a case.
///
/// # Errors
///
/// Returns error if analysis fails or no imaging data is provided.
pub fn analyze_case(
&self,
request: &CaseAnalysisRequest,
) -> Result<CaseAnalysisResult, TumorBoardError> {
use std::time::Instant;
let start = Instant::now();
// Check that we have at least some imaging data
if request.studies.is_empty() {
return Err(TumorBoardError::NoImagingData);
}
// Analyze each modality
let ct_findings = if request.config.analyze_ct {
self.analyze_ct(request)?
} else {
None
};
let mri_findings = if request.config.analyze_mri {
self.analyze_mri(request)?
} else {
None
};
let pathology_findings = if request.config.analyze_pathology {
self.analyze_pathology(request)?
} else {
None
};
// Fuse multi-modal findings
let fused = self
.fusion
.fuse(&ct_findings, &mri_findings, &pathology_findings)
.map_err(|e| TumorBoardError::FusionError(e.to_string()))?;
// Generate recommendations
let recommendations = self.generate_recommendations(&fused);
// Generate report if requested
let report = if request.config.generate_report {
Some(
self.report_generator
.generate(
request,
&ct_findings,
&mri_findings,
&pathology_findings,
&fused,
&recommendations,
)
.map_err(|e| TumorBoardError::ReportError(e.to_string()))?,
)
} else {
None
};
let processing_time = start.elapsed().as_secs_f32();
Ok(CaseAnalysisResult {
case_id: request.case_id.clone(),
ct_findings,
mri_findings,
pathology_findings,
fused_analysis: fused,
report,
recommendations,
metadata: AnalysisMetadata {
processing_time,
models_used: vec![
"CT-3D-CNN-v1".to_string(),
"MRI-MultiSeq-v1".to_string(),
"Pathology-WSI-v1".to_string(),
"Fusion-CrossAttention-v1".to_string(),
],
timestamp: chrono_lite_timestamp(),
},
})
}
/// Analyze CT studies.
fn analyze_ct(
&self,
request: &CaseAnalysisRequest,
) -> Result<Option<CTFindings>, TumorBoardError> {
use tumorboard_shared::ImagingModality;
let ct_studies: Vec<_> = request
.studies
.iter()
.filter(|s| s.modality == ImagingModality::CT)
.collect();
if ct_studies.is_empty() {
return Ok(None);
}
self.ct_analyzer
.analyze(&ct_studies)
.map(Some)
.map_err(|e| TumorBoardError::CTAnalysisError(e.to_string()))
}
/// Analyze MRI studies.
fn analyze_mri(
&self,
request: &CaseAnalysisRequest,
) -> Result<Option<MRIFindings>, TumorBoardError> {
use tumorboard_shared::ImagingModality;
let mri_studies: Vec<_> = request
.studies
.iter()
.filter(|s| s.modality == ImagingModality::MRI)
.collect();
if mri_studies.is_empty() {
return Ok(None);
}
self.mri_analyzer
.analyze(&mri_studies)
.map(Some)
.map_err(|e| TumorBoardError::MRIAnalysisError(e.to_string()))
}
/// Analyze pathology studies.
fn analyze_pathology(
&self,
request: &CaseAnalysisRequest,
) -> Result<Option<PathologyFindings>, TumorBoardError> {
use tumorboard_shared::ImagingModality;
let pathology_studies: Vec<_> = request
.studies
.iter()
.filter(|s| s.modality == ImagingModality::Pathology)
.collect();
if pathology_studies.is_empty() {
return Ok(None);
}
self.pathology_analyzer
.analyze(&pathology_studies)
.map(Some)
.map_err(|e| TumorBoardError::PathologyAnalysisError(e.to_string()))
}
/// Generate treatment recommendations based on fused analysis.
fn generate_recommendations(&self, fused: &FusedAnalysis) -> Vec<Recommendation> {
use tumorboard_shared::{Priority, RecommendationType};
let mut recommendations = Vec::new();
// Recommend biopsy if high malignancy probability
if fused.primary_diagnosis.probability > 0.5 {
recommendations.push(Recommendation {
recommendation_type: RecommendationType::Biopsy,
description: "Consider tissue biopsy for definitive diagnosis".to_string(),
priority: Priority::High,
rationale: format!(
"Primary diagnosis ({}) has {:.0}% probability",
fused.primary_diagnosis.name,
fused.primary_diagnosis.probability * 100.0
),
});
}
// Recommend follow-up imaging
if fused.confidence < 0.8 {
recommendations.push(Recommendation {
recommendation_type: RecommendationType::ImagingFollowUp,
description: "Consider follow-up imaging for clarification".to_string(),
priority: Priority::Moderate,
rationale: format!(
"Analysis confidence is {:.0}%, additional imaging may help",
fused.confidence * 100.0
),
});
}
// Recommend specialist referral for malignancy
if fused.primary_diagnosis.probability > 0.7 {
recommendations.push(Recommendation {
recommendation_type: RecommendationType::Referral,
description: "Refer to oncology for multidisciplinary evaluation".to_string(),
priority: Priority::High,
rationale: "High probability of malignancy requires specialist evaluation"
.to_string(),
});
}
recommendations
}
/// Get a summary of findings.
#[must_use]
pub fn get_summary(&self, result: &CaseAnalysisResult) -> String {
let mut summary = String::new();
summary.push_str(&format!("Case: {}\n", result.case_id));
summary.push('\n');
summary.push_str("PRIMARY DIAGNOSIS:\n");
summary.push_str(&format!(
" {}: {:.0}% probability\n",
result.fused_analysis.primary_diagnosis.name,
result.fused_analysis.primary_diagnosis.probability * 100.0
));
if !result.fused_analysis.differential_diagnoses.is_empty() {
summary.push_str("\nDIFFERENTIAL DIAGNOSES:\n");
for dx in &result.fused_analysis.differential_diagnoses {
summary.push_str(&format!(
" - {}: {:.0}%\n",
dx.name,
dx.probability * 100.0
));
}
}
if !result.recommendations.is_empty() {
summary.push_str("\nRECOMMENDATIONS:\n");
for rec in &result.recommendations {
summary.push_str(&format!(" [{:?}] {}\n", rec.priority, rec.description));
}
}
summary.push_str(&format!(
"\nConfidence: {:.0}%\n",
result.fused_analysis.confidence * 100.0
));
summary.push_str(&format!(
"Processing time: {:.2}s\n",
result.metadata.processing_time
));
summary
}
}
/// 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())
}
/// Run a complete tumor board session with sample data.
///
/// # Errors
///
/// Returns error if analysis fails.
pub fn run_demo() -> Result<CaseAnalysisResult, TumorBoardError> {
let request = tumorboard_shared::get_sample_case();
let ai = TumorBoardAI::new();
ai.analyze_case(&request)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tumor_board_creation() {
let ai = TumorBoardAI::new();
assert!(std::mem::size_of_val(&ai) > 0);
}
#[test]
fn test_analyze_case() {
let request = tumorboard_shared::get_sample_case();
let ai = TumorBoardAI::new();
let result = ai.analyze_case(&request);
assert!(result.is_ok());
let analysis = result.unwrap();
assert!(!analysis.case_id.is_empty());
}
#[test]
fn test_no_imaging_data_error() {
let request = CaseAnalysisRequest {
case_id: "TEST001".to_string(),
patient_info: tumorboard_shared::PatientInfo {
age: Some(60),
sex: Some(tumorboard_shared::Sex::Male),
medical_history: vec![],
},
studies: vec![],
clinical_context: tumorboard_shared::ClinicalContext {
clinical_question: "Test".to_string(),
known_diagnosis: None,
lab_values: vec![],
prior_treatments: vec![],
},
config: tumorboard_shared::AnalysisConfig::default(),
};
let ai = TumorBoardAI::new();
let result = ai.analyze_case(&request);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TumorBoardError::NoImagingData
));
}
#[test]
fn test_run_demo() {
let result = run_demo();
assert!(result.is_ok());
let analysis = result.unwrap();
assert!(!analysis.fused_analysis.primary_diagnosis.name.is_empty());
}
#[test]
fn test_get_summary() {
let request = tumorboard_shared::get_sample_case();
let ai = TumorBoardAI::new();
let result = ai.analyze_case(&request).unwrap();
let summary = ai.get_summary(&result);
assert!(summary.contains("PRIMARY DIAGNOSIS"));
assert!(summary.contains("Confidence"));
}
#[test]
fn test_recommendations_generated() {
let request = tumorboard_shared::get_sample_case();
let ai = TumorBoardAI::new();
let result = ai.analyze_case(&request).unwrap();
// Recommendations should be generated for significant findings
assert!(
!result.recommendations.is_empty()
|| result.fused_analysis.primary_diagnosis.probability < 0.5
);
}
}