Files
rustytorch/demos/rtx-tumorboard-demo/src/pathology_analyzer.rs
T
2026-03-04 00:08:42 +00:00

340 lines
10 KiB
Rust

//! Pathology slide analysis using deep learning.
//!
//! Analyzes whole slide images (WSI) for histological features,
//! cellular patterns, and biomarker expression.
use tumorboard_shared::{
Biomarker, BiomarkerStatus, CellClassification, ImagingStudy, PathologyFindings,
TissueArchitecture, TumorGrade,
};
/// Error type for pathology analysis.
#[derive(Debug, thiserror::Error)]
pub enum PathologyAnalysisError {
/// No slides provided.
#[error("No pathology slides provided")]
NoSlides,
/// Invalid slide data.
#[error("Invalid slide data: {0}")]
InvalidData(String),
}
/// Pathology slide analyzer using simulated deep learning.
#[derive(Debug)]
pub struct PathologyAnalyzer {
/// Cell detection threshold.
cell_detection_threshold: f32,
}
impl Default for PathologyAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl PathologyAnalyzer {
/// Create a new pathology analyzer.
#[must_use]
pub fn new() -> Self {
Self {
cell_detection_threshold: 0.5,
}
}
/// Analyze pathology studies.
///
/// # Errors
///
/// Returns error if no studies are provided.
pub fn analyze(
&self,
studies: &[&ImagingStudy],
) -> Result<PathologyFindings, PathologyAnalysisError> {
if studies.is_empty() {
return Err(PathologyAnalysisError::NoSlides);
}
// Analyze cell classifications
let cell_classifications = self.classify_cells(studies);
// Analyze tissue architecture
let tissue_architecture = self.analyze_architecture(studies);
// Analyze biomarkers
let biomarkers = self.analyze_biomarkers(studies);
// Determine grade
let grading = self.determine_grade(&cell_classifications);
// Generate assessment
let assessment = self.generate_assessment(&cell_classifications, &grading);
// Calculate confidence
let confidence = self.calculate_confidence(studies);
Ok(PathologyFindings {
cell_classifications,
tissue_architecture,
biomarkers,
grading,
assessment,
confidence,
})
}
/// Classify cells in the slides.
fn classify_cells(&self, _studies: &[&ImagingStudy]) -> Vec<CellClassification> {
vec![
CellClassification {
cell_type: "Tumor cells".to_string(),
count: 15000,
percentage: 70.0,
abnormality: 0.85,
},
CellClassification {
cell_type: "Lymphocytes".to_string(),
count: 3200,
percentage: 15.0,
abnormality: 0.1,
},
CellClassification {
cell_type: "Stromal cells".to_string(),
count: 2100,
percentage: 10.0,
abnormality: 0.05,
},
CellClassification {
cell_type: "Macrophages".to_string(),
count: 1070,
percentage: 5.0,
abnormality: 0.15,
},
]
}
/// Analyze tissue architecture.
fn analyze_architecture(&self, _studies: &[&ImagingStudy]) -> TissueArchitecture {
TissueArchitecture {
pattern: "Acinar/glandular".to_string(),
is_preserved: false,
features: vec![
"Loss of normal architecture".to_string(),
"Irregular gland formation".to_string(),
"Desmoplastic stroma".to_string(),
"Focal necrosis".to_string(),
],
}
}
/// Analyze biomarkers.
fn analyze_biomarkers(&self, _studies: &[&ImagingStudy]) -> Vec<Biomarker> {
vec![
Biomarker {
name: "PD-L1".to_string(),
status: BiomarkerStatus::Positive,
expression: Some(45.0),
},
Biomarker {
name: "TTF-1".to_string(),
status: BiomarkerStatus::Positive,
expression: Some(90.0),
},
Biomarker {
name: "CK7".to_string(),
status: BiomarkerStatus::Positive,
expression: Some(95.0),
},
Biomarker {
name: "CK20".to_string(),
status: BiomarkerStatus::Negative,
expression: Some(0.0),
},
Biomarker {
name: "Ki-67".to_string(),
status: BiomarkerStatus::Positive,
expression: Some(35.0),
},
]
}
/// Determine tumor grade.
fn determine_grade(&self, cells: &[CellClassification]) -> Option<TumorGrade> {
// Calculate grade based on cellular features
let tumor_abnormality = cells
.iter()
.find(|c| c.cell_type == "Tumor cells")
.map_or(0.5, |c| c.abnormality);
let grade = if tumor_abnormality > 0.8 {
"Grade 3 (Poorly differentiated)".to_string()
} else if tumor_abnormality > 0.5 {
"Grade 2 (Moderately differentiated)".to_string()
} else {
"Grade 1 (Well differentiated)".to_string()
};
Some(TumorGrade {
system: "Nottingham/WHO".to_string(),
grade: grade.clone(),
description: grade,
})
}
/// Calculate confidence.
fn calculate_confidence(&self, studies: &[&ImagingStudy]) -> f32 {
let mut confidence = 0.7;
// More studies = higher confidence
confidence += 0.05 * studies.len().min(4) as f32;
confidence.min(0.95)
}
/// Generate assessment.
fn generate_assessment(
&self,
cells: &[CellClassification],
grading: &Option<TumorGrade>,
) -> String {
let mut assessment = String::new();
let tumor_percentage = cells
.iter()
.find(|c| c.cell_type == "Tumor cells")
.map_or(0.0, |c| c.percentage);
assessment.push_str(&format!(
"Adenocarcinoma with {tumor_percentage:.0}% tumor cellularity. "
));
if let Some(grade) = grading {
assessment.push_str(&format!("{}. ", grade.grade));
}
let til_percentage = cells
.iter()
.find(|c| c.cell_type == "Lymphocytes")
.map_or(0.0, |c| c.percentage);
if til_percentage > 20.0 {
assessment.push_str("Brisk tumor-infiltrating lymphocytes. ");
} else if til_percentage > 10.0 {
assessment.push_str("Moderate tumor-infiltrating lymphocytes. ");
}
assessment.push_str("TTF-1 positive, consistent with lung primary.");
assessment
}
}
#[cfg(test)]
mod tests {
use super::*;
use tumorboard_shared::{BodyRegion, ImageDimensions, ImageMetadata, ImagingModality};
fn create_test_pathology_study() -> ImagingStudy {
ImagingStudy {
study_id: "PATH001".to_string(),
modality: ImagingModality::Pathology,
images: vec![ImageMetadata {
id: "PATH001-HE".to_string(),
modality: ImagingModality::Pathology,
acquisition_date: "2024-01-15".to_string(),
dimensions: ImageDimensions {
width: 80000,
height: 60000,
depth: 1,
},
spacing: [0.25, 0.25, 1.0],
body_region: BodyRegion::Chest,
series_description: Some("H&E stain".to_string()),
}],
description: Some("Lung biopsy".to_string()),
}
}
#[test]
fn test_pathology_analyzer_creation() {
let analyzer = PathologyAnalyzer::new();
assert!(analyzer.cell_detection_threshold > 0.0);
}
#[test]
fn test_analyze_pathology_study() {
let analyzer = PathologyAnalyzer::new();
let study = create_test_pathology_study();
let result = analyzer.analyze(&[&study]);
assert!(result.is_ok());
let findings = result.unwrap();
assert!(!findings.cell_classifications.is_empty());
assert!(findings.confidence > 0.0);
}
#[test]
fn test_no_studies_error() {
let analyzer = PathologyAnalyzer::new();
let result = analyzer.analyze(&[]);
assert!(result.is_err());
}
#[test]
fn test_cell_classification() {
let analyzer = PathologyAnalyzer::new();
let study = create_test_pathology_study();
let findings = analyzer.analyze(&[&study]).unwrap();
assert!(!findings.cell_classifications.is_empty());
let tumor_cells = findings
.cell_classifications
.iter()
.find(|c| c.cell_type == "Tumor cells");
assert!(tumor_cells.is_some());
}
#[test]
fn test_tissue_architecture() {
let analyzer = PathologyAnalyzer::new();
let study = create_test_pathology_study();
let findings = analyzer.analyze(&[&study]).unwrap();
assert!(!findings.tissue_architecture.pattern.is_empty());
}
#[test]
fn test_biomarkers() {
let analyzer = PathologyAnalyzer::new();
let study = create_test_pathology_study();
let findings = analyzer.analyze(&[&study]).unwrap();
assert!(!findings.biomarkers.is_empty());
let pdl1 = findings.biomarkers.iter().find(|b| b.name == "PD-L1");
assert!(pdl1.is_some());
assert_eq!(pdl1.unwrap().status, BiomarkerStatus::Positive);
}
#[test]
fn test_grading() {
let analyzer = PathologyAnalyzer::new();
let study = create_test_pathology_study();
let findings = analyzer.analyze(&[&study]).unwrap();
assert!(findings.grading.is_some());
}
#[test]
fn test_assessment() {
let analyzer = PathologyAnalyzer::new();
let study = create_test_pathology_study();
let findings = analyzer.analyze(&[&study]).unwrap();
assert!(!findings.assessment.is_empty());
assert!(findings.assessment.contains("Adenocarcinoma"));
}
}