Initial commit
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
//! 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user