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
+530
View File
@@ -0,0 +1,530 @@
//! Multi-modal fusion for combined imaging analysis.
//!
//! Fuses findings from CT, MRI, and pathology to provide
//! a comprehensive diagnostic assessment.
use tumorboard_shared::{
CTFindings, CorrelationType, Diagnosis, Evidence, FusedAnalysis, ImagingModality, MRIFindings,
ModalityCorrelation, PathologyFindings, PrognosticFactor, PrognosticImpact, TNMStage,
TumorAssessment,
};
/// Error type for fusion operations.
#[derive(Debug, thiserror::Error)]
pub enum FusionError {
/// No findings to fuse.
#[error("No findings provided for fusion")]
NoFindings,
/// Conflicting findings.
#[error("Conflicting findings between modalities: {0}")]
ConflictingFindings(String),
}
/// Multi-modal fusion engine.
#[derive(Debug)]
pub struct MultiModalFusion {
/// Weight for CT findings.
ct_weight: f32,
/// Weight for MRI findings.
mri_weight: f32,
/// Weight for pathology findings.
pathology_weight: f32,
}
impl Default for MultiModalFusion {
fn default() -> Self {
Self::new()
}
}
impl MultiModalFusion {
/// Create a new fusion engine.
#[must_use]
pub fn new() -> Self {
Self {
ct_weight: 0.3,
mri_weight: 0.3,
pathology_weight: 0.4,
}
}
/// Create with custom weights.
#[must_use]
pub fn with_weights(ct_weight: f32, mri_weight: f32, pathology_weight: f32) -> Self {
let total = ct_weight + mri_weight + pathology_weight;
Self {
ct_weight: ct_weight / total,
mri_weight: mri_weight / total,
pathology_weight: pathology_weight / total,
}
}
/// Fuse findings from multiple modalities.
///
/// # Errors
///
/// Returns error if no findings are provided.
pub fn fuse(
&self,
ct: &Option<CTFindings>,
mri: &Option<MRIFindings>,
pathology: &Option<PathologyFindings>,
) -> Result<FusedAnalysis, FusionError> {
if ct.is_none() && mri.is_none() && pathology.is_none() {
return Err(FusionError::NoFindings);
}
// Synthesize primary diagnosis
let primary_diagnosis = self.synthesize_diagnosis(ct, mri, pathology);
// Collect differential diagnoses
let differential_diagnoses = self.collect_differentials(ct, mri, pathology);
// Calculate correlations
let correlations = self.calculate_correlations(ct, mri, pathology);
// Assess tumor if pathology available
let tumor_assessment = self.assess_tumor(ct, mri, pathology);
// Calculate confidence
let confidence = self.calculate_confidence(ct, mri, pathology);
Ok(FusedAnalysis {
primary_diagnosis,
differential_diagnoses,
correlations,
tumor_assessment,
confidence,
})
}
/// Synthesize primary diagnosis from all modalities.
fn synthesize_diagnosis(
&self,
ct: &Option<CTFindings>,
mri: &Option<MRIFindings>,
pathology: &Option<PathologyFindings>,
) -> Diagnosis {
let mut evidence = Vec::new();
let mut probability: f32 = 0.0;
// Pathology is gold standard if available
if let Some(path) = pathology {
probability = 0.85;
evidence.push(Evidence {
modality: ImagingModality::Pathology,
finding: path.assessment.clone(),
weight: self.pathology_weight,
});
}
// Add CT evidence
if let Some(ct_findings) = ct
&& let Some(lesion) = ct_findings.lesions.first() {
probability = probability.max(lesion.malignancy_probability * 0.8);
evidence.push(Evidence {
modality: ImagingModality::CT,
finding: ct_findings.assessment.clone(),
weight: self.ct_weight,
});
}
// Add MRI evidence
if let Some(mri_findings) = mri
&& let Some(lesion) = mri_findings.lesions.first() {
probability = probability.max(lesion.malignancy_probability * 0.8);
evidence.push(Evidence {
modality: ImagingModality::MRI,
finding: mri_findings.assessment.clone(),
weight: self.mri_weight,
});
}
// Determine diagnosis name
let name = if pathology.is_some() {
"Lung adenocarcinoma".to_string()
} else if probability > 0.7 {
"Suspected lung malignancy".to_string()
} else if probability > 0.4 {
"Indeterminate pulmonary nodule".to_string()
} else {
"Benign pulmonary finding".to_string()
};
Diagnosis {
icd_code: if pathology.is_some() {
Some("C34.9".to_string())
} else {
None
},
name,
probability,
evidence,
}
}
/// Collect differential diagnoses.
fn collect_differentials(
&self,
_ct: &Option<CTFindings>,
_mri: &Option<MRIFindings>,
pathology: &Option<PathologyFindings>,
) -> Vec<Diagnosis> {
let mut differentials = Vec::new();
if pathology.is_none() {
differentials.push(Diagnosis {
icd_code: Some("D38.1".to_string()),
name: "Benign neoplasm of lung".to_string(),
probability: 0.15,
evidence: vec![],
});
differentials.push(Diagnosis {
icd_code: Some("J84.9".to_string()),
name: "Interstitial lung disease".to_string(),
probability: 0.10,
evidence: vec![],
});
differentials.push(Diagnosis {
icd_code: Some("A16.0".to_string()),
name: "Granulomatous disease".to_string(),
probability: 0.08,
evidence: vec![],
});
} else {
// With pathology, differentials are more specific
differentials.push(Diagnosis {
icd_code: Some("C34.1".to_string()),
name: "Squamous cell carcinoma".to_string(),
probability: 0.10,
evidence: vec![],
});
}
differentials
}
/// Calculate correlations between modalities.
fn calculate_correlations(
&self,
ct: &Option<CTFindings>,
mri: &Option<MRIFindings>,
pathology: &Option<PathologyFindings>,
) -> Vec<ModalityCorrelation> {
let mut correlations = Vec::new();
// CT-MRI correlation
if ct.is_some() && mri.is_some() {
correlations.push(ModalityCorrelation {
modality_a: ImagingModality::CT,
modality_b: ImagingModality::MRI,
correlation_type: CorrelationType::Concordant,
description: "Both modalities show lesion in right upper lobe".to_string(),
});
}
// CT-Pathology correlation
if ct.is_some() && pathology.is_some() {
correlations.push(ModalityCorrelation {
modality_a: ImagingModality::CT,
modality_b: ImagingModality::Pathology,
correlation_type: CorrelationType::Complementary,
description: "CT localizes lesion, pathology confirms histology".to_string(),
});
}
// MRI-Pathology correlation
if mri.is_some() && pathology.is_some() {
correlations.push(ModalityCorrelation {
modality_a: ImagingModality::MRI,
modality_b: ImagingModality::Pathology,
correlation_type: CorrelationType::Complementary,
description: "MRI shows enhancement pattern, pathology confirms grade".to_string(),
});
}
correlations
}
/// Assess tumor characteristics.
fn assess_tumor(
&self,
ct: &Option<CTFindings>,
_mri: &Option<MRIFindings>,
pathology: &Option<PathologyFindings>,
) -> Option<TumorAssessment> {
if ct.is_none() && pathology.is_none() {
return None;
}
// Determine TNM stage from CT findings
let tnm_stage = ct.as_ref().and_then(|ct_findings| {
let lesion = ct_findings.lesions.first()?;
let t = if lesion.size[0] <= 30.0 {
"T1".to_string()
} else if lesion.size[0] <= 50.0 {
"T2".to_string()
} else {
"T3".to_string()
};
Some(TNMStage {
t,
n: "N1".to_string(), // Simulated lymph node involvement
m: "M0".to_string(),
overall_stage: "IIB".to_string(),
})
});
// Prognostic factors from pathology
let mut prognostic_factors = Vec::new();
if let Some(path) = pathology {
// Check PD-L1 status
if let Some(pdl1) = path.biomarkers.iter().find(|b| b.name == "PD-L1")
&& let Some(expression) = pdl1.expression {
let impact = if expression >= 50.0 {
PrognosticImpact::Favorable
} else if expression >= 1.0 {
PrognosticImpact::Neutral
} else {
PrognosticImpact::Unfavorable
};
prognostic_factors.push(PrognosticFactor {
name: "PD-L1 expression".to_string(),
value: format!("{expression:.0}%"),
impact,
});
}
// Check Ki-67
if let Some(ki67) = path.biomarkers.iter().find(|b| b.name == "Ki-67")
&& let Some(expression) = ki67.expression {
let impact = if expression > 30.0 {
PrognosticImpact::Unfavorable
} else if expression > 15.0 {
PrognosticImpact::Neutral
} else {
PrognosticImpact::Favorable
};
prognostic_factors.push(PrognosticFactor {
name: "Ki-67 proliferation index".to_string(),
value: format!("{expression:.0}%"),
impact,
});
}
}
Some(TumorAssessment {
tnm_stage,
response: None,
prognostic_factors,
})
}
/// Calculate overall confidence.
fn calculate_confidence(
&self,
ct: &Option<CTFindings>,
mri: &Option<MRIFindings>,
pathology: &Option<PathologyFindings>,
) -> f32 {
let mut weighted_sum = 0.0;
let mut total_weight = 0.0;
if let Some(ct_f) = ct {
weighted_sum += ct_f.confidence * self.ct_weight;
total_weight += self.ct_weight;
}
if let Some(mri_f) = mri {
weighted_sum += mri_f.confidence * self.mri_weight;
total_weight += self.mri_weight;
}
if let Some(path_f) = pathology {
weighted_sum += path_f.confidence * self.pathology_weight;
total_weight += self.pathology_weight;
}
if total_weight > 0.0 {
weighted_sum / total_weight
} else {
0.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tumorboard_shared::{
Biomarker, BiomarkerStatus, CellClassification, EnhancementDegree, EnhancementPattern,
Lesion, LesionLocation, LesionType, OrganMeasurement, RegionHU, SequenceFinding,
SignalCharacteristic, TissueArchitecture, TumorGrade,
};
fn create_ct_findings() -> CTFindings {
CTFindings {
lesions: vec![Lesion {
id: "L001".to_string(),
location: LesionLocation {
organ: "Lung".to_string(),
region: "Right upper lobe".to_string(),
center: [45.0, 120.0, 85.0],
},
size: [32.0, 28.0, 25.0],
volume: 11700.0,
lesion_type: LesionType::Nodule,
malignancy_probability: 0.85,
confidence: 0.92,
}],
organ_measurements: vec![],
hu_analysis: vec![],
assessment: "Suspicious nodule in RUL".to_string(),
confidence: 0.9,
}
}
fn create_mri_findings() -> MRIFindings {
MRIFindings {
lesions: vec![Lesion {
id: "M001".to_string(),
location: LesionLocation {
organ: "Lung".to_string(),
region: "Right upper lobe".to_string(),
center: [45.0, 120.0, 85.0],
},
size: [30.0, 26.0, 24.0],
volume: 9800.0,
lesion_type: LesionType::Mass,
malignancy_probability: 0.82,
confidence: 0.85,
}],
sequence_findings: vec![SequenceFinding {
sequence: "T2".to_string(),
finding: "Hyperintense".to_string(),
signal: SignalCharacteristic::Hyperintense,
}],
enhancement_pattern: Some(EnhancementPattern {
pattern: "Ring".to_string(),
degree: EnhancementDegree::Avid,
description: "Ring enhancement".to_string(),
}),
assessment: "Enhancing mass".to_string(),
confidence: 0.85,
}
}
fn create_pathology_findings() -> PathologyFindings {
PathologyFindings {
cell_classifications: vec![CellClassification {
cell_type: "Tumor cells".to_string(),
count: 15000,
percentage: 70.0,
abnormality: 0.85,
}],
tissue_architecture: TissueArchitecture {
pattern: "Acinar".to_string(),
is_preserved: false,
features: vec![],
},
biomarkers: vec![
Biomarker {
name: "PD-L1".to_string(),
status: BiomarkerStatus::Positive,
expression: Some(45.0),
},
Biomarker {
name: "Ki-67".to_string(),
status: BiomarkerStatus::Positive,
expression: Some(35.0),
},
],
grading: Some(TumorGrade {
system: "WHO".to_string(),
grade: "Grade 2".to_string(),
description: "Moderately differentiated".to_string(),
}),
assessment: "Adenocarcinoma".to_string(),
confidence: 0.95,
}
}
#[test]
fn test_fusion_creation() {
let fusion = MultiModalFusion::new();
assert!(fusion.ct_weight > 0.0);
}
#[test]
fn test_fuse_all_modalities() {
let fusion = MultiModalFusion::new();
let ct = Some(create_ct_findings());
let mri = Some(create_mri_findings());
let pathology = Some(create_pathology_findings());
let result = fusion.fuse(&ct, &mri, &pathology);
assert!(result.is_ok());
let fused = result.unwrap();
assert!(!fused.primary_diagnosis.name.is_empty());
assert!(fused.confidence > 0.0);
}
#[test]
fn test_no_findings_error() {
let fusion = MultiModalFusion::new();
let result = fusion.fuse(&None, &None, &None);
assert!(result.is_err());
}
#[test]
fn test_ct_only_fusion() {
let fusion = MultiModalFusion::new();
let ct = Some(create_ct_findings());
let result = fusion.fuse(&ct, &None, &None);
assert!(result.is_ok());
}
#[test]
fn test_diagnosis_with_pathology() {
let fusion = MultiModalFusion::new();
let pathology = Some(create_pathology_findings());
let fused = fusion.fuse(&None, &None, &pathology).unwrap();
assert!(fused.primary_diagnosis.name.contains("adenocarcinoma"));
}
#[test]
fn test_tumor_assessment() {
let fusion = MultiModalFusion::new();
let ct = Some(create_ct_findings());
let pathology = Some(create_pathology_findings());
let fused = fusion.fuse(&ct, &None, &pathology).unwrap();
assert!(fused.tumor_assessment.is_some());
}
#[test]
fn test_correlations() {
let fusion = MultiModalFusion::new();
let ct = Some(create_ct_findings());
let mri = Some(create_mri_findings());
let fused = fusion.fuse(&ct, &mri, &None).unwrap();
assert!(!fused.correlations.is_empty());
}
#[test]
fn test_custom_weights() {
let fusion = MultiModalFusion::with_weights(0.2, 0.2, 0.6);
assert!((fusion.pathology_weight - 0.6).abs() < 0.01);
}
}