//! Cell type annotation algorithms. //! //! Implements marker-based annotation, transfer learning, and //! foundation model-based cell type prediction. use crate::CellAtlasError; use cellatlas_shared::{ AnalysisConfig, AnnotationMethod, Cell, CellType, CellTypeAnnotation, ClusterInfo, MarkerGeneSet, get_common_markers, }; /// Annotate cells with cell types. pub fn annotate_cells( cells: &[Cell], clusters: &[ClusterInfo], config: &AnalysisConfig, ) -> Result<(Vec, Vec), CellAtlasError> { match config.annotation.method { AnnotationMethod::MarkerBased => { let markers = config .annotation .marker_genes .clone() .unwrap_or_else(get_common_markers); marker_based_annotation(cells, clusters, &markers) } AnnotationMethod::TransferLearning => { transfer_learning_annotation(cells, clusters, &config.annotation.reference) } AnnotationMethod::FoundationModel => foundation_model_annotation(cells, clusters), } } /// Marker-based cell type annotation. /// /// Assigns cell types based on expression of known marker genes. fn marker_based_annotation( cells: &[Cell], _clusters: &[ClusterInfo], marker_sets: &[MarkerGeneSet], ) -> Result<(Vec, Vec), CellAtlasError> { let mut annotated_cells = cells.to_vec(); let mut cell_type_counts: std::collections::HashMap = std::collections::HashMap::new(); // Build gene name to index map (simulated) let gene_map = build_gene_map(); // Annotate each cell for cell in &mut annotated_cells { let best_annotation = find_best_cell_type(cell, marker_sets, &gene_map); cell.cell_type = best_annotation.clone(); if let Some(ct) = best_annotation { let stats = cell_type_counts .entry(ct.name.clone()) .or_insert_with(|| CellTypeStats { count: 0, total_confidence: 0.0, clusters: std::collections::HashSet::new(), }); stats.count += 1; stats.total_confidence += ct.confidence; if let Some(cluster_id) = cell.cluster_id { stats.clusters.insert(cluster_id); } } } // Build cell type annotations summary let total_cells = annotated_cells.len(); let cell_types: Vec = cell_type_counts .into_iter() .map(|(name, stats)| CellTypeAnnotation { name, n_cells: stats.count, percentage: (stats.count as f32 / total_cells as f32) * 100.0, avg_confidence: stats.total_confidence / stats.count as f32, clusters: stats.clusters.into_iter().collect(), }) .collect(); Ok((annotated_cells, cell_types)) } /// Transfer learning annotation using a reference dataset. fn transfer_learning_annotation( cells: &[Cell], _clusters: &[ClusterInfo], reference: &Option, ) -> Result<(Vec, Vec), CellAtlasError> { // For demo, simulate transfer learning let reference_name = reference.as_deref().unwrap_or("Human Cell Atlas"); tracing::info!("Using reference dataset: {}", reference_name); // Create reference cell type signatures (simulated) let reference_signatures = get_reference_signatures(); let mut annotated_cells = cells.to_vec(); let mut cell_type_counts: std::collections::HashMap = std::collections::HashMap::new(); for cell in &mut annotated_cells { // Match cell to reference signatures let best_match = match_to_reference(cell, &reference_signatures); cell.cell_type = Some(best_match.clone()); let stats = cell_type_counts .entry(best_match.name.clone()) .or_insert_with(|| CellTypeStats { count: 0, total_confidence: 0.0, clusters: std::collections::HashSet::new(), }); stats.count += 1; stats.total_confidence += best_match.confidence; if let Some(cluster_id) = cell.cluster_id { stats.clusters.insert(cluster_id); } } let total_cells = annotated_cells.len(); let cell_types: Vec = cell_type_counts .into_iter() .map(|(name, stats)| CellTypeAnnotation { name, n_cells: stats.count, percentage: (stats.count as f32 / total_cells as f32) * 100.0, avg_confidence: stats.total_confidence / stats.count as f32, clusters: stats.clusters.into_iter().collect(), }) .collect(); Ok((annotated_cells, cell_types)) } /// Foundation model-based annotation using `CellAtlas` transformer. fn foundation_model_annotation( cells: &[Cell], clusters: &[ClusterInfo], ) -> Result<(Vec, Vec), CellAtlasError> { use crate::cell_transformer::{CellTransformer, CellTransformerConfig}; tracing::info!("Using CellAtlas foundation model for annotation"); // Initialize transformer let config = CellTransformerConfig { num_genes: 2000, hidden_dim: 128, num_heads: 4, num_layers: 4, dropout: 0.1, max_seq_len: 512, }; let transformer = CellTransformer::new(config); // Encode cells let gene_indices: Vec> = cells .iter() .map(|c| c.expression.gene_indices.clone()) .collect(); let values: Vec> = cells.iter().map(|c| c.expression.values.clone()).collect(); let embeddings = transformer.encode(&gene_indices, &values); // Classify based on embeddings (simulated classifier) let cell_type_predictions = classify_embeddings(&embeddings, clusters); let mut annotated_cells = cells.to_vec(); let mut cell_type_counts: std::collections::HashMap = std::collections::HashMap::new(); for (i, cell) in annotated_cells.iter_mut().enumerate() { if i < cell_type_predictions.len() { let prediction = &cell_type_predictions[i]; cell.cell_type = Some(prediction.clone()); let stats = cell_type_counts .entry(prediction.name.clone()) .or_insert_with(|| CellTypeStats { count: 0, total_confidence: 0.0, clusters: std::collections::HashSet::new(), }); stats.count += 1; stats.total_confidence += prediction.confidence; if let Some(cluster_id) = cell.cluster_id { stats.clusters.insert(cluster_id); } } } let total_cells = annotated_cells.len(); let cell_types: Vec = cell_type_counts .into_iter() .map(|(name, stats)| CellTypeAnnotation { name, n_cells: stats.count, percentage: (stats.count as f32 / total_cells as f32) * 100.0, avg_confidence: stats.total_confidence / stats.count as f32, clusters: stats.clusters.into_iter().collect(), }) .collect(); Ok((annotated_cells, cell_types)) } /// Helper struct for tracking cell type statistics. #[derive(Debug)] struct CellTypeStats { count: usize, total_confidence: f32, clusters: std::collections::HashSet, } /// Build gene name to index map. fn build_gene_map() -> std::collections::HashMap { let common_genes = vec![ "CD3D", "CD3E", "CD3G", "CD4", "CD8A", "CD8B", "CD19", "MS4A1", "CD79A", "CD79B", "CD14", "LYZ", "CST3", "FCGR3A", "GNLY", "NKG7", "KLRD1", "PRF1", "FCER1A", "CD1C", "CLEC10A", "IL7R", "CCR7", "S100A4", "PPBP", "PF4", "GP9", "HBA1", "HBA2", "HBB", ]; common_genes .iter() .enumerate() .map(|(i, &gene)| (gene.to_string(), i)) .collect() } /// Find best cell type for a cell based on marker expression. fn find_best_cell_type( cell: &Cell, marker_sets: &[MarkerGeneSet], gene_map: &std::collections::HashMap, ) -> Option { use rand::SeedableRng; use rand_distr::{Distribution, Normal}; let mut rng = rand::rngs::StdRng::seed_from_u64(cell.id.len() as u64); let noise = Normal::new(0.0_f32, 0.1).unwrap(); let mut best_score = 0.0_f32; let mut best_type: Option = None; for marker_set in marker_sets { // Count positive marker expression let mut positive_count = 0; let mut positive_total = 0.0; for marker in &marker_set.positive_markers { if let Some(&gene_idx) = gene_map.get(marker) && gene_idx < cell.expression.gene_indices.len() { let expr = cell.expression.get(gene_idx); if expr > 0.0 { positive_count += 1; positive_total += expr; } } } // Check negative markers let mut negative_count = 0; for marker in &marker_set.negative_markers { if let Some(&gene_idx) = gene_map.get(marker) { let expr = cell.expression.get(gene_idx); if expr > 0.0 { negative_count += 1; } } } // Calculate score let positive_score = if marker_set.positive_markers.is_empty() { 0.5 } else { positive_count as f32 / marker_set.positive_markers.len() as f32 }; let negative_penalty = if marker_set.negative_markers.is_empty() { 0.0 } else { negative_count as f32 / marker_set.negative_markers.len() as f32 * 0.5 }; let score = (positive_score - negative_penalty + noise.sample(&mut rng)).clamp(0.0, 1.0); if score > best_score { best_score = score; best_type = Some(CellType { name: marker_set.cell_type.clone(), confidence: score, lineage: vec!["Immune".to_string(), marker_set.cell_type.clone()], markers: marker_set.positive_markers.clone(), }); } } best_type } /// Get reference cell type signatures for transfer learning. fn get_reference_signatures() -> Vec { vec![ ReferenceSignature { cell_type: "CD4+ T cell".to_string(), marker_weights: vec![ ("CD3D".to_string(), 1.0), ("CD4".to_string(), 1.0), ("IL7R".to_string(), 0.8), ], lineage: vec![ "Immune".to_string(), "T cell".to_string(), "CD4+ T cell".to_string(), ], }, ReferenceSignature { cell_type: "CD8+ T cell".to_string(), marker_weights: vec![ ("CD3D".to_string(), 1.0), ("CD8A".to_string(), 1.0), ("GZMK".to_string(), 0.7), ], lineage: vec![ "Immune".to_string(), "T cell".to_string(), "CD8+ T cell".to_string(), ], }, ReferenceSignature { cell_type: "B cell".to_string(), marker_weights: vec![ ("CD19".to_string(), 1.0), ("MS4A1".to_string(), 1.0), ("CD79A".to_string(), 0.9), ], lineage: vec!["Immune".to_string(), "B cell".to_string()], }, ReferenceSignature { cell_type: "Monocyte".to_string(), marker_weights: vec![ ("CD14".to_string(), 1.0), ("LYZ".to_string(), 0.9), ("CST3".to_string(), 0.8), ], lineage: vec![ "Immune".to_string(), "Myeloid".to_string(), "Monocyte".to_string(), ], }, ReferenceSignature { cell_type: "NK cell".to_string(), marker_weights: vec![ ("GNLY".to_string(), 1.0), ("NKG7".to_string(), 1.0), ("KLRD1".to_string(), 0.8), ], lineage: vec!["Immune".to_string(), "NK cell".to_string()], }, ReferenceSignature { cell_type: "Dendritic cell".to_string(), marker_weights: vec![("FCER1A".to_string(), 1.0), ("CD1C".to_string(), 0.9)], lineage: vec![ "Immune".to_string(), "Myeloid".to_string(), "Dendritic cell".to_string(), ], }, ] } /// Reference cell type signature. #[derive(Debug)] struct ReferenceSignature { cell_type: String, marker_weights: Vec<(String, f32)>, lineage: Vec, } /// Match a cell to reference signatures. fn match_to_reference(cell: &Cell, references: &[ReferenceSignature]) -> CellType { use rand::SeedableRng; use rand_distr::{Distribution, Normal}; let mut rng = rand::rngs::StdRng::seed_from_u64(cell.id.len() as u64 * 17); let noise = Normal::new(0.0_f32, 0.1).unwrap(); let gene_map = build_gene_map(); let mut best_score = 0.0_f32; let mut best_ref = &references[0]; for reference in references { let mut score = 0.0_f32; let mut total_weight = 0.0_f32; for (gene, weight) in &reference.marker_weights { if let Some(&gene_idx) = gene_map.get(gene) { let expr = cell.expression.get(gene_idx); if expr > 0.0 { score += weight; } total_weight += weight; } } let normalized_score = if total_weight > 0.0 { score / total_weight } else { 0.0 }; let noisy_score = (normalized_score + noise.sample(&mut rng)).clamp(0.0, 1.0); if noisy_score > best_score { best_score = noisy_score; best_ref = reference; } } CellType { name: best_ref.cell_type.clone(), confidence: best_score, lineage: best_ref.lineage.clone(), markers: best_ref .marker_weights .iter() .map(|(g, _)| g.clone()) .collect(), } } /// Classify cell embeddings using a simple classifier. fn classify_embeddings(embeddings: &[Vec], _clusters: &[ClusterInfo]) -> Vec { use rand::SeedableRng; use rand_distr::{Distribution, Normal}; let cell_types = [ ("CD4+ T cell", vec!["Immune", "T cell", "CD4+ T cell"]), ("CD8+ T cell", vec!["Immune", "T cell", "CD8+ T cell"]), ("B cell", vec!["Immune", "B cell"]), ("Monocyte", vec!["Immune", "Myeloid", "Monocyte"]), ("NK cell", vec!["Immune", "NK cell"]), ("Dendritic cell", vec!["Immune", "Myeloid", "DC"]), ]; let mut rng = rand::rngs::StdRng::seed_from_u64(42); let confidence_noise = Normal::new(0.0_f32, 0.1).unwrap(); embeddings .iter() .enumerate() .map(|(i, emb)| { // Simple classification based on embedding features let sum: f32 = emb.iter().sum(); let type_idx = (i + (sum.abs() as usize)) % cell_types.len(); let (name, lineage) = &cell_types[type_idx]; CellType { name: name.to_string(), confidence: (0.75 + confidence_noise.sample(&mut rng)).clamp(0.5, 0.99), lineage: lineage .iter() .map(std::string::ToString::to_string) .collect(), markers: vec![], } }) .collect() } #[cfg(test)] mod tests { use super::*; use cellatlas_shared::{AnnotationConfig, Embedding2D, QualityMetrics, SparseExpression}; fn create_test_cell(id: &str, gene_indices: Vec, values: Vec) -> Cell { Cell { id: id.to_string(), barcode: None, expression: SparseExpression { gene_indices, values: values.clone(), num_genes: 100, }, cell_type: None, state: None, qc_metrics: QualityMetrics { n_genes: values.len(), total_counts: values.iter().sum(), pct_mito: 3.0, pct_ribo: 10.0, doublet_score: None, }, spatial_coords: None, cluster_id: Some(0), embedding: Some(Embedding2D { x: 0.0, y: 0.0 }), } } #[test] fn test_marker_based_annotation() { let cells = vec![ create_test_cell("t_cell", vec![0, 1, 2], vec![5.0, 4.0, 3.0]), create_test_cell("b_cell", vec![6, 7, 8], vec![6.0, 5.0, 4.0]), ]; let clusters = vec![ClusterInfo { id: 0, n_cells: 2, markers: vec!["CD3D".to_string()], centroid: None, }]; let markers = get_common_markers(); let result = marker_based_annotation(&cells, &clusters, &markers); assert!(result.is_ok()); let (annotated, cell_types) = result.unwrap(); assert_eq!(annotated.len(), 2); assert!(!cell_types.is_empty()); } #[test] fn test_transfer_learning_annotation() { let cells = vec![ create_test_cell("cell_1", vec![0, 1], vec![5.0, 4.0]), create_test_cell("cell_2", vec![6, 7], vec![6.0, 5.0]), ]; let clusters = vec![ClusterInfo { id: 0, n_cells: 2, markers: vec![], centroid: None, }]; let result = transfer_learning_annotation(&cells, &clusters, &None); assert!(result.is_ok()); let (annotated, cell_types) = result.unwrap(); assert!(annotated.iter().all(|c| c.cell_type.is_some())); assert!(!cell_types.is_empty()); } #[test] fn test_foundation_model_annotation() { let cells = vec![ create_test_cell("cell_1", vec![0, 1, 2], vec![1.0, 2.0, 1.5]), create_test_cell("cell_2", vec![3, 4, 5], vec![2.0, 1.0, 0.5]), ]; let clusters = vec![ClusterInfo { id: 0, n_cells: 2, markers: vec![], centroid: None, }]; let result = foundation_model_annotation(&cells, &clusters); assert!(result.is_ok()); let (annotated, cell_types) = result.unwrap(); assert_eq!(annotated.len(), 2); } #[test] fn test_reference_signatures() { let signatures = get_reference_signatures(); assert!(!signatures.is_empty()); assert!(signatures.iter().any(|s| s.cell_type.contains("T cell"))); } #[test] fn test_build_gene_map() { let map = build_gene_map(); assert!(map.contains_key("CD3D")); assert!(map.contains_key("CD19")); assert!(map.contains_key("GNLY")); } }