340 lines
10 KiB
Rust
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"));
|
|
}
|
|
}
|