//! Shared IPC types for DrugBinder drug-target binding affinity demo. //! //! This crate provides data structures for communication between //! the Tauri frontend and Rust backend for drug-target binding prediction. use serde::{Deserialize, Serialize}; // ============================================================================ // Molecule Types // ============================================================================ /// A small molecule drug candidate. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Molecule { /// Unique identifier pub id: String, /// Common name pub name: Option, /// SMILES representation pub smiles: String, /// Molecular weight (g/mol) pub molecular_weight: f32, /// Number of atoms pub num_atoms: usize, /// Number of bonds pub num_bonds: usize, /// Atom information pub atoms: Vec, /// Bond information pub bonds: Vec, /// 3D coordinates (if available) pub coordinates_3d: Option>, /// Molecular properties pub properties: MolecularProperties, } /// Atom in a molecule. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Atom { /// Atom index pub index: usize, /// Element symbol pub element: Element, /// Formal charge pub formal_charge: i8, /// Number of hydrogens pub num_hydrogens: u8, /// Is aromatic pub is_aromatic: bool, /// Hybridization pub hybridization: Hybridization, } /// Chemical element. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub enum Element { H, C, N, O, F, P, S, Cl, Br, I, Na, Mg, K, Ca, Fe, Zn, Cu, Other, } impl Element { /// Get atomic number. pub fn atomic_number(&self) -> u8 { match self { Element::H => 1, Element::C => 6, Element::N => 7, Element::O => 8, Element::F => 9, Element::P => 15, Element::S => 16, Element::Cl => 17, Element::Br => 35, Element::I => 53, Element::Na => 11, Element::Mg => 12, Element::K => 19, Element::Ca => 20, Element::Fe => 26, Element::Zn => 30, Element::Cu => 29, Element::Other => 0, } } } /// Atom hybridization state. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Hybridization { Sp, Sp2, Sp3, Sp3d, Sp3d2, Other, } /// Bond in a molecule. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Bond { /// First atom index pub atom1: usize, /// Second atom index pub atom2: usize, /// Bond type pub bond_type: BondType, /// Is conjugated pub is_conjugated: bool, /// Is in ring pub is_in_ring: bool, } /// Bond type. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum BondType { Single, Double, Triple, Aromatic, Other, } /// 3D coordinate. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct Coordinate3D { pub x: f32, pub y: f32, pub z: f32, } /// Molecular properties (Lipinski's Rule of Five, etc.). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MolecularProperties { /// Octanol-water partition coefficient pub log_p: f32, /// Number of hydrogen bond donors pub hbd: u8, /// Number of hydrogen bond acceptors pub hba: u8, /// Topological polar surface area (Ų) pub tpsa: f32, /// Number of rotatable bonds pub rotatable_bonds: u8, /// Number of rings pub num_rings: u8, /// Number of aromatic rings pub num_aromatic_rings: u8, /// Passes Lipinski's Rule of Five pub lipinski_pass: bool, /// QED (Quantitative Estimate of Drug-likeness) pub qed: f32, } // ============================================================================ // Protein Types // ============================================================================ /// Protein target for binding. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProteinTarget { /// Unique identifier (e.g., UniProt ID) pub id: String, /// Protein name pub name: String, /// Organism pub organism: String, /// Sequence pub sequence: String, /// PDB ID (if structure available) pub pdb_id: Option, /// Binding pockets pub pockets: Vec, } /// Binding pocket on a protein. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BindingPocket { /// Pocket ID pub id: usize, /// Pocket name pub name: String, /// Residue indices pub residues: Vec, /// Pocket center pub center: Coordinate3D, /// Pocket volume (ų) pub volume: f32, /// Druggability score (0-1) pub druggability: f32, } // ============================================================================ // Binding Prediction Types // ============================================================================ /// Request to predict binding affinity. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BindingPredictionRequest { /// Molecule to evaluate pub molecule: MoleculeInput, /// Target protein pub target: TargetInput, /// Configuration pub config: PredictionConfig, } /// Molecule input (can be SMILES or full molecule). #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum MoleculeInput { /// SMILES string Smiles { smiles: String, name: Option, }, /// Full molecule object Full(Molecule), } /// Target input. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum TargetInput { /// Protein ID (look up from database) Id { protein_id: String }, /// Protein sequence Sequence { sequence: String, name: Option, }, /// Full protein object Full(ProteinTarget), } /// Prediction configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PredictionConfig { /// Model to use pub model: BindingModel, /// Predict binding pose pub predict_pose: bool, /// Number of poses to generate pub num_poses: usize, /// Pocket ID to dock into (if known) pub pocket_id: Option, } impl Default for PredictionConfig { fn default() -> Self { Self { model: BindingModel::GraphTransformer, predict_pose: true, num_poses: 5, pocket_id: None, } } } /// Binding affinity model. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum BindingModel { /// Graph neural network GraphNN, /// Transformer-based GraphTransformer, /// 3D CNN Cnn3D, /// Ensemble Ensemble, } /// Binding prediction result. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BindingPrediction { /// Predicted binding affinity (pKd or pIC50) pub affinity: AffinityPrediction, /// Binding poses pub poses: Vec, /// Interaction analysis pub interactions: Vec, /// Confidence score pub confidence: f32, /// Model explanation pub explanation: Option, } /// Affinity prediction with uncertainty. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AffinityPrediction { /// Predicted pKd (negative log of Kd in molar) pub pkd: f32, /// Predicted pIC50 pub pic50: f32, /// Predicted ΔG (kcal/mol) pub delta_g: f32, /// Uncertainty (standard deviation) pub uncertainty: f32, /// Affinity class pub affinity_class: AffinityClass, } /// Affinity classification. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AffinityClass { /// Very high affinity (pKd > 9) VeryHigh, /// High affinity (pKd 7-9) High, /// Medium affinity (pKd 5-7) Medium, /// Low affinity (pKd 3-5) Low, /// Very low affinity (pKd < 3) VeryLow, } impl AffinityClass { /// Get affinity class from pKd value. pub fn from_pkd(pkd: f32) -> Self { if pkd > 9.0 { AffinityClass::VeryHigh } else if pkd > 7.0 { AffinityClass::High } else if pkd > 5.0 { AffinityClass::Medium } else if pkd > 3.0 { AffinityClass::Low } else { AffinityClass::VeryLow } } } /// Predicted binding pose. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BindingPose { /// Pose ID pub id: usize, /// Ligand coordinates in binding pose pub ligand_coords: Vec, /// Docking score pub score: f32, /// RMSD from input (if applicable) pub rmsd: Option, /// Energy breakdown pub energy_terms: EnergyTerms, } /// Energy terms for binding. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EnergyTerms { /// van der Waals pub vdw: f32, /// Electrostatics pub electrostatic: f32, /// Hydrogen bonding pub hbond: f32, /// Hydrophobic pub hydrophobic: f32, /// Desolvation pub desolvation: f32, /// Entropy penalty pub entropy: f32, /// Total pub total: f32, } /// Protein-ligand interaction. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProteinLigandInteraction { /// Interaction type pub interaction_type: InteractionType, /// Protein residue pub residue: String, /// Ligand atom index pub ligand_atom: usize, /// Distance (Å) pub distance: f32, /// Strength (0-1) pub strength: f32, } /// Type of interaction. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum InteractionType { HydrogenBond, HydrophobicContact, PiStacking, PiCation, SaltBridge, HalogenBond, MetalCoordination, } /// Prediction explanation. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PredictionExplanation { /// Important atoms (by contribution) pub important_atoms: Vec, /// Important residues pub important_residues: Vec, /// Feature importance pub feature_importance: Vec, } /// Atom contribution to prediction. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AtomContribution { pub atom_index: usize, pub contribution: f32, } /// Residue contribution to prediction. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResidueContribution { pub residue: String, pub contribution: f32, } /// Feature importance. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeatureImportance { pub feature_name: String, pub importance: f32, } // ============================================================================ // Virtual Screening Types // ============================================================================ /// Virtual screening request. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScreeningRequest { /// Molecules to screen pub molecules: Vec, /// Target pub target: TargetInput, /// Configuration pub config: ScreeningConfig, } /// Screening configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScreeningConfig { /// Number of top compounds to return pub top_k: usize, /// Minimum affinity threshold (pKd) pub min_affinity: Option, /// Filter by Lipinski's rules pub lipinski_filter: bool, /// Sort by pub sort_by: ScreeningSortBy, } impl Default for ScreeningConfig { fn default() -> Self { Self { top_k: 100, min_affinity: Some(5.0), lipinski_filter: true, sort_by: ScreeningSortBy::Affinity, } } } /// Screening sort criteria. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ScreeningSortBy { Affinity, Confidence, QED, LipinskiScore, } /// Screening result. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScreeningResult { /// Screened compounds with predictions pub hits: Vec, /// Total molecules screened pub total_screened: usize, /// Molecules passing filters pub num_passed_filters: usize, /// Processing time (ms) pub processing_time_ms: u64, } /// Single screening hit. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScreeningHit { /// Rank pub rank: usize, /// Molecule pub molecule: Molecule, /// Binding prediction pub prediction: BindingPrediction, } // ============================================================================ // Molecule Generation Types // ============================================================================ /// Request to generate molecules. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GenerationRequest { /// Target protein pub target: TargetInput, /// Generation method pub method: GenerationMethod, /// Configuration pub config: GenerationConfig, } /// Generation method. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum GenerationMethod { /// Diffusion-based generation Diffusion, /// Reinforcement learning ReinforcementLearning, /// VAE-based VariationalAutoencoder, /// Fragment-based FragmentGrowing, } /// Generation configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GenerationConfig { /// Number of molecules to generate pub num_molecules: usize, /// Target affinity (pKd) pub target_affinity: Option, /// Scaffold to use pub scaffold: Option, /// Enforce Lipinski's rules pub lipinski_constraints: bool, /// Maximum molecular weight pub max_mw: f32, } impl Default for GenerationConfig { fn default() -> Self { Self { num_molecules: 100, target_affinity: Some(8.0), scaffold: None, lipinski_constraints: true, max_mw: 500.0, } } } /// Generation result. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GenerationResult { /// Generated molecules pub molecules: Vec, /// Generation statistics pub stats: GenerationStats, } /// Generated molecule with metadata. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GeneratedMolecule { /// The molecule pub molecule: Molecule, /// Predicted binding pub predicted_binding: AffinityPrediction, /// Novelty score (vs training set) pub novelty: f32, /// Synthetic accessibility score (1-10) pub sa_score: f32, } /// Generation statistics. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GenerationStats { /// Total generated pub total_generated: usize, /// Valid molecules pub valid: usize, /// Unique molecules pub unique: usize, /// Novel molecules pub novel: usize, /// High affinity (pKd > 7) pub high_affinity: usize, } // ============================================================================ // Sample Data // ============================================================================ /// Sample molecules for demo. pub fn get_sample_molecules() -> Vec { vec![ create_sample_molecule("aspirin", "Aspirin", "CC(=O)OC1=CC=CC=C1C(=O)O", 180.16), create_sample_molecule( "ibuprofen", "Ibuprofen", "CC(C)CC1=CC=C(C=C1)C(C)C(=O)O", 206.28, ), create_sample_molecule( "caffeine", "Caffeine", "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", 194.19, ), create_sample_molecule( "acetaminophen", "Acetaminophen", "CC(=O)NC1=CC=C(C=C1)O", 151.16, ), ] } fn create_sample_molecule(id: &str, name: &str, smiles: &str, mw: f32) -> Molecule { Molecule { id: id.to_string(), name: Some(name.to_string()), smiles: smiles.to_string(), molecular_weight: mw, num_atoms: count_atoms(smiles), num_bonds: count_atoms(smiles).saturating_sub(1), atoms: vec![], bonds: vec![], coordinates_3d: None, properties: MolecularProperties { log_p: 1.5, hbd: 1, hba: 3, tpsa: 60.0, rotatable_bonds: 2, num_rings: 1, num_aromatic_rings: 1, lipinski_pass: true, qed: 0.7, }, } } fn count_atoms(smiles: &str) -> usize { // Simplified atom counting smiles.chars().filter(|c| c.is_uppercase()).count() } /// Sample protein targets. pub fn get_sample_targets() -> Vec { vec![ ProteinTarget { id: "P00918".to_string(), name: "Carbonic anhydrase 2".to_string(), organism: "Homo sapiens".to_string(), sequence: "MSHHWGYGKHNGPEHWHKDFPIAKGERQSPVDIDTHTAKYDPSLKPLSVSYDQATSLRILNNGHAFNVEFDDSQDKAVLKGGPLDGTYRLIQFHFHWGSLDGQGSEHTVDKKKYAAELHLVHWNTKYGDFGKAVQQPDGLAVLGIFLKVGSAKPGLQKVVDVLDSIKTKGKSADFTNFDPRGLLPESLDYWTYPGSLTTPPLLECVTWIVLKEPISVSSEQVLKFRKLNFNGEGEPEELMVDNWRPAQPLKNRQIKASFK".to_string(), pdb_id: Some("1CA2".to_string()), pockets: vec![ BindingPocket { id: 0, name: "Active site".to_string(), residues: vec![91, 92, 94, 96, 119, 143, 198, 199, 200], center: Coordinate3D { x: 12.5, y: 8.3, z: 15.2 }, volume: 350.0, druggability: 0.85, }, ], }, ProteinTarget { id: "P00533".to_string(), name: "Epidermal growth factor receptor".to_string(), organism: "Homo sapiens".to_string(), sequence: "MRPSGTAGAALLALLAALCPASRALEEKKVCQGTSNKLTQLGTFEDHFLSLQRMFNNCEVVLGNLEITYVQRNYDLSFLKTIQEVAGYVLIALNTVERIPLENLQIIRGNMYYENSYALAVLSNYDANKTGLKELPMRNLQEILHGAVRFSNNPALCNVESIQWRDIVSSDFLSNMSMDFQNHLGSCQKCDPSCPNGSCWGAGEENCQKLTKIICAQQCSGRCRGKSPSDCCHNQCAAGCTGPRESDCLVCRKFRDEATCKDTCPPLMLYNPTTYQMDVNPEGKYSFGATCVKKCPRNYVVTDHGSCVRACGADSYEMEEDGVRKCKKCEGPCRKVCNGIGIGEFKDSLSINATNIKHFKNCTSISGDLHILPVAFRGDSFTHTPPLDPQELDILKTVKEITGFLLIQAWPENRTDLHAFENLEIIRGRTKQHGQFSLAVVSLNITSLGLRSLKEISDGDVIISGNKNLCYANTINWKKLFGTSGQKTKIISNRGENSCKATGQVCHALCSPEGCWGPEPRDCVSCRNVSRGRECVDKCNLLEGEPREFVENSECIQCHPECLPQAMNITCTGRGPDNCIQCAHYIDGPHCVKTCPAGVMGENNTLVWKYADAGHVCHLCHPNCTYGCTGPGLEGCPTNGPKIPS".to_string(), pdb_id: Some("1M17".to_string()), pockets: vec![ BindingPocket { id: 0, name: "ATP binding site".to_string(), residues: vec![718, 719, 721, 726, 745, 790, 791, 792, 793, 854, 855], center: Coordinate3D { x: 25.0, y: 18.5, z: 42.0 }, volume: 480.0, druggability: 0.92, }, ], }, ] } // ============================================================================ // Tests // ============================================================================ #[cfg(test)] mod tests { use super::*; #[test] fn test_element_atomic_number() { assert_eq!(Element::C.atomic_number(), 6); assert_eq!(Element::N.atomic_number(), 7); assert_eq!(Element::O.atomic_number(), 8); } #[test] fn test_affinity_class() { assert_eq!(AffinityClass::from_pkd(10.0), AffinityClass::VeryHigh); assert_eq!(AffinityClass::from_pkd(8.0), AffinityClass::High); assert_eq!(AffinityClass::from_pkd(6.0), AffinityClass::Medium); assert_eq!(AffinityClass::from_pkd(4.0), AffinityClass::Low); assert_eq!(AffinityClass::from_pkd(2.0), AffinityClass::VeryLow); } #[test] fn test_sample_molecules() { let molecules = get_sample_molecules(); assert_eq!(molecules.len(), 4); assert!( molecules .iter() .any(|m| m.name == Some("Aspirin".to_string())) ); } #[test] fn test_sample_targets() { let targets = get_sample_targets(); assert!(!targets.is_empty()); assert!(targets.iter().any(|t| t.name.contains("Carbonic"))); } #[test] fn test_prediction_config_default() { let config = PredictionConfig::default(); assert_eq!(config.model, BindingModel::GraphTransformer); assert!(config.predict_pose); assert_eq!(config.num_poses, 5); } #[test] fn test_screening_config_default() { let config = ScreeningConfig::default(); assert_eq!(config.top_k, 100); assert!(config.lipinski_filter); } #[test] fn test_generation_config_default() { let config = GenerationConfig::default(); assert_eq!(config.num_molecules, 100); assert!(config.lipinski_constraints); assert_eq!(config.max_mw, 500.0); } #[test] fn test_molecule_serialization() { let mol = &get_sample_molecules()[0]; let json = serde_json::to_string(mol).unwrap(); assert!(json.contains("aspirin")); } #[test] fn test_coordinate_3d() { let coord = Coordinate3D { x: 1.0, y: 2.0, z: 3.0, }; let json = serde_json::to_string(&coord).unwrap(); let parsed: Coordinate3D = serde_json::from_str(&json).unwrap(); assert!((parsed.x - 1.0).abs() < 0.001); } }