//! Sample data utilities for drug binding demo. //! //! Provides SMILES parsing and protein lookup functionality. use drugbinder_shared::{ Atom, BindingPocket, Bond, BondType, Coordinate3D, Element, Hybridization, MolecularProperties, Molecule, ProteinTarget, }; use crate::DrugBinderError; /// Convert SMILES string to Molecule. pub fn smiles_to_molecule(smiles: &str, name: Option) -> Result { // Parse SMILES let (atoms, bonds) = parse_smiles(smiles)?; // Calculate molecular weight let mw: f32 = atoms.iter().map(|a| element_mass(&a.element)).sum(); // Calculate properties let properties = calculate_properties(&atoms, &bonds); Ok(Molecule { id: format!("mol_{}", hash_smiles(smiles)), name, smiles: smiles.to_string(), molecular_weight: mw, num_atoms: atoms.len(), num_bonds: bonds.len(), atoms, bonds, coordinates_3d: None, properties, }) } /// Get protein by ID. pub fn get_protein_by_id(protein_id: &str) -> Result { // Look up in sample database let targets = get_protein_database(); targets .into_iter() .find(|t| t.id == protein_id) .ok_or_else(|| DrugBinderError::InvalidProtein(format!("Unknown protein ID: {protein_id}"))) } /// Convert sequence to protein. #[must_use] pub fn sequence_to_protein(sequence: &str, name: Option) -> ProteinTarget { ProteinTarget { id: format!("seq_{}", hash_sequence(sequence)), name: name.unwrap_or_else(|| "Custom protein".to_string()), organism: "Unknown".to_string(), sequence: sequence.to_string(), pdb_id: None, pockets: vec![], } } /// Parse SMILES to atoms and bonds. fn parse_smiles(smiles: &str) -> Result<(Vec, Vec), DrugBinderError> { let mut atoms = Vec::new(); let mut bonds = Vec::new(); let mut ring_atoms: std::collections::HashMap = std::collections::HashMap::new(); let mut branch_stack: Vec = Vec::new(); let mut prev_atom: Option = None; let mut pending_bond_type = BondType::Single; let mut in_aromatic = false; let chars: Vec = smiles.chars().collect(); let mut i = 0; while i < chars.len() { let c = chars[i]; match c { // Branch handling '(' => { if let Some(atom_idx) = prev_atom { branch_stack.push(atom_idx); } i += 1; continue; } ')' => { prev_atom = branch_stack.pop(); i += 1; continue; } // Bond types '-' => { pending_bond_type = BondType::Single; i += 1; continue; } '=' => { pending_bond_type = BondType::Double; i += 1; continue; } '#' => { pending_bond_type = BondType::Triple; i += 1; continue; } ':' => { pending_bond_type = BondType::Aromatic; i += 1; continue; } // Ring closures '0'..='9' => { if let Some(atom_idx) = prev_atom { if let Some(&ring_start) = ring_atoms.get(&c) { // Close ring bonds.push(Bond { atom1: ring_start, atom2: atom_idx, bond_type: if in_aromatic { BondType::Aromatic } else { BondType::Single }, is_conjugated: in_aromatic, is_in_ring: true, }); ring_atoms.remove(&c); } else { // Start ring ring_atoms.insert(c, atom_idx); } } i += 1; continue; } // Skip stereochemistry and charges '/' | '\\' | '@' | '+' | '[' | ']' => { // Skip bracket contents if c == '[' { while i < chars.len() && chars[i] != ']' { i += 1; } } i += 1; continue; } _ => {} } // Try to parse element let (element, is_aromatic_elem, chars_consumed) = parse_element(&chars, i); if let Some(elem) = element { let atom_idx = atoms.len(); // Determine aromaticity let is_aromatic_atom = is_aromatic_elem; if is_aromatic_atom { in_aromatic = true; } atoms.push(Atom { index: atom_idx, element: elem, formal_charge: 0, num_hydrogens: implicit_h_count(&elem, is_aromatic_atom), is_aromatic: is_aromatic_atom, hybridization: if is_aromatic_atom { Hybridization::Sp2 } else { Hybridization::Sp3 }, }); // Add bond to previous atom if let Some(prev_idx) = prev_atom { let bond_type = if is_aromatic_atom && atoms.get(prev_idx).is_some_and(|a| a.is_aromatic) { BondType::Aromatic } else { pending_bond_type }; bonds.push(Bond { atom1: prev_idx, atom2: atom_idx, bond_type, is_conjugated: bond_type == BondType::Aromatic || bond_type == BondType::Double, is_in_ring: false, // Updated later for ring bonds }); } prev_atom = Some(atom_idx); pending_bond_type = BondType::Single; i += chars_consumed; } else { i += 1; } } if atoms.is_empty() { return Err(DrugBinderError::InvalidMolecule(format!( "Could not parse SMILES: {smiles}" ))); } Ok((atoms, bonds)) } /// Parse element from character stream. fn parse_element(chars: &[char], start: usize) -> (Option, bool, usize) { if start >= chars.len() { return (None, false, 0); } let c = chars[start]; // Check for two-letter elements if start + 1 < chars.len() { let next = chars[start + 1]; if next.is_lowercase() { let elem_str: String = vec![c, next].into_iter().collect(); let elem = match elem_str.as_str() { "Cl" => Some(Element::Cl), "Br" => Some(Element::Br), "Na" => Some(Element::Na), "Mg" => Some(Element::Mg), "Ca" => Some(Element::Ca), "Fe" => Some(Element::Fe), "Zn" => Some(Element::Zn), "Cu" => Some(Element::Cu), _ => None, }; if elem.is_some() { return (elem, false, 2); } } } // Single letter elements match c { 'C' => (Some(Element::C), false, 1), 'N' => (Some(Element::N), false, 1), 'O' => (Some(Element::O), false, 1), 'S' => (Some(Element::S), false, 1), 'P' => (Some(Element::P), false, 1), 'F' => (Some(Element::F), false, 1), 'I' => (Some(Element::I), false, 1), 'H' => (Some(Element::H), false, 1), // Aromatic atoms (lowercase) 'c' => (Some(Element::C), true, 1), 'n' => (Some(Element::N), true, 1), 'o' => (Some(Element::O), true, 1), 's' => (Some(Element::S), true, 1), _ => (None, false, 1), } } /// Calculate implicit hydrogen count. fn implicit_h_count(element: &Element, is_aromatic: bool) -> u8 { let valence: u8 = match element { Element::C => 4, Element::N => 3, Element::O => 2, Element::S => 2, Element::P => 3, Element::F | Element::Cl | Element::Br | Element::I => 1, _ => 0, }; // Aromatic atoms typically have one less H if is_aromatic { valence.saturating_sub(1) } else { valence } } /// Calculate molecular properties. fn calculate_properties(atoms: &[Atom], bonds: &[Bond]) -> MolecularProperties { // Count HBD (NH, OH) let hbd: u8 = atoms .iter() .filter(|a| matches!(a.element, Element::N | Element::O) && a.num_hydrogens > 0) .count() as u8; // Count HBA (N, O) let hba: u8 = atoms .iter() .filter(|a| matches!(a.element, Element::N | Element::O)) .count() as u8; // Calculate LogP (Wildman-Crippen method, simplified) let mut log_p = 0.0_f32; for atom in atoms { log_p += match atom.element { Element::C => { if atom.is_aromatic { 0.29 } else { 0.50 } } Element::N => { if atom.num_hydrogens > 0 { -1.03 } else { -0.57 } } Element::O => { if atom.num_hydrogens > 0 { -0.47 } else { -0.11 } } Element::S => 0.37, Element::F => 0.37, Element::Cl => 0.71, Element::Br => 0.86, Element::I => 1.15, Element::H => 0.12, _ => 0.0, }; } // Count rotatable bonds let rotatable_bonds: u8 = bonds .iter() .filter(|b| b.bond_type == BondType::Single && !b.is_in_ring) .count() as u8; // Count rings (estimate from ring bonds) let ring_bond_count = bonds.iter().filter(|b| b.is_in_ring).count(); let num_rings = (ring_bond_count / 5).max(1) as u8; // Count aromatic rings let aromatic_atoms = atoms.iter().filter(|a| a.is_aromatic).count(); let num_aromatic_rings = (aromatic_atoms / 5) as u8; // Calculate TPSA let tpsa: f32 = atoms .iter() .map(|a| match a.element { Element::N => { if a.num_hydrogens > 0 { 26.0 } else { 12.0 } } Element::O => { if a.num_hydrogens > 0 { 20.0 } else { 9.2 } } Element::S => 25.0, _ => 0.0, }) .sum(); // Calculate MW let mw: f32 = atoms.iter().map(|a| element_mass(&a.element)).sum(); // Check Lipinski let lipinski_pass = mw <= 500.0 && log_p <= 5.0 && hbd <= 5 && hba <= 10; // Calculate QED let qed = calculate_qed(mw, log_p, hbd, hba, tpsa, rotatable_bonds); MolecularProperties { log_p, hbd, hba, tpsa, rotatable_bonds, num_rings, num_aromatic_rings, lipinski_pass, qed, } } fn calculate_qed(mw: f32, log_p: f32, hbd: u8, hba: u8, tpsa: f32, rotatable_bonds: u8) -> f32 { let gaussian = |x: f32, mean: f32, std: f32| -> f32 { (-0.5 * ((x - mean) / std).powi(2)).exp() }; let mw_score = gaussian(mw, 350.0, 100.0); let logp_score = gaussian(log_p, 2.5, 1.5); let hbd_score = gaussian(f32::from(hbd), 1.0, 2.0); let hba_score = gaussian(f32::from(hba), 4.0, 3.0); let tpsa_score = gaussian(tpsa, 70.0, 30.0); let rotb_score = gaussian(f32::from(rotatable_bonds), 3.0, 3.0); (mw_score * logp_score * hbd_score * hba_score * tpsa_score * rotb_score).powf(1.0 / 6.0) } fn element_mass(element: &Element) -> f32 { match element { Element::H => 1.008, Element::C => 12.011, Element::N => 14.007, Element::O => 15.999, Element::F => 18.998, Element::P => 30.974, Element::S => 32.065, Element::Cl => 35.453, Element::Br => 79.904, Element::I => 126.90, Element::Na => 22.990, Element::Mg => 24.305, Element::K => 39.098, Element::Ca => 40.078, Element::Fe => 55.845, Element::Zn => 65.38, Element::Cu => 63.546, Element::Other => 12.0, } } fn hash_smiles(smiles: &str) -> u32 { smiles.bytes().fold(0u32, |acc, b| { acc.wrapping_mul(31).wrapping_add(u32::from(b)) }) } fn hash_sequence(sequence: &str) -> u32 { sequence.bytes().fold(0u32, |acc, b| { acc.wrapping_mul(37).wrapping_add(u32::from(b)) }) } /// Get protein database. fn get_protein_database() -> Vec { vec![ // Carbonic anhydrase 2 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, }, ], }, // EGFR 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, }, ], }, // COX-2 ProteinTarget { id: "P35354".to_string(), name: "Prostaglandin G/H synthase 2 (COX-2)".to_string(), organism: "Homo sapiens".to_string(), sequence: "MLARALLLCAVLALSHTANPCCSHPCQNRGVCMSVGFDQYKCDCTRTGFYGENCSTPEFLTRIKLFLKPTPNTVHYILTHFKGFWNVVNNIPFLRNAIMSYVLTSRSHLIDSPPTYNADY".to_string(), pdb_id: Some("5KIR".to_string()), pockets: vec![ BindingPocket { id: 0, name: "Cyclooxygenase active site".to_string(), residues: vec![83, 89, 90, 120, 355, 523, 530], center: Coordinate3D { x: 30.0, y: 25.0, z: 35.0 }, volume: 420.0, druggability: 0.88, }, ], }, // ACE2 ProteinTarget { id: "Q9BYF1".to_string(), name: "Angiotensin-converting enzyme 2 (ACE2)".to_string(), organism: "Homo sapiens".to_string(), sequence: "MSSSSWLLLSLVAVTAAQSTIEEQAKTFLDKFNHEAEDLFYQSSLASWNYNTNITEENVQNMNNAGDKWSAFLKEQSTLAQMYPLQEIQNLTVKLQLQALQQNGSSVLSEDKSKRLNTILNTMSTIYSTGKVCNPDNPQECLLLEPGLNEIMANSLDYNERLWAWESWRSEVGKQLRPLYEEYVVLKNEMARANHYEDYGDYWRGDYEVNGVDGYDYSRGQLIEDVEHTFEEIKPLYEHLHAYVRAKLMNAYPSYISPIGCLPAHLLGDMWGRFWTNLYSLTVPFGQKPNIDVTDAMVDQAWDAQRIFKEAEKFFVSVGLPNMTQGFWENSMLTDPGNVQKAVCHPTAWDLGKGDFRILMCTKVTMDDFLTAHHEMGHIQYDMAYAAQPFLLRNGANEGFHEAVGEIMSLSAATPKHLKSIGLLSPDFQEDNETEINFLLKQALTIVGTLPFTYMLEKWRWMVFKGEIPKDQWMKKWWEMKREIVGVVEPVPHDETYCDPASLFHVSNDYSFIRYYTRTLYQFQFQEALCQAAKHEGPLHKCDISNSTEAGQKLFNMLRLGKSEPWTLALENVVGAKNMNVRPLLNYFEPLFTWLKDQNKNSFVGWSTDWSPYADQSIKVRISLKSALGDKAYEWNDNEMYLFRSSVAYAMRQYFLKVKNQMILFGEEDVRVANLKPRISFNFFVTAPKNVSDIIPRTEVEKAIRMSRSRINDAFRLNDNSLEFLGIQPTLGPPNQPPVSIWLIVFGVVMGVIVVGIVILIFTGIRDRKKKNKARSGENPYASIDISKGENNPGFQNTDDVQTSF".to_string(), pdb_id: Some("6M0J".to_string()), pockets: vec![ BindingPocket { id: 0, name: "Peptidase active site".to_string(), residues: vec![273, 345, 371, 374, 378, 384, 394, 402, 417], center: Coordinate3D { x: 40.0, y: 32.0, z: 28.0 }, volume: 520.0, druggability: 0.82, }, ], }, // HIV protease ProteinTarget { id: "P04585".to_string(), name: "HIV-1 protease".to_string(), organism: "Human immunodeficiency virus 1".to_string(), sequence: "PQITLWQRPLVTIKIGGQLKEALLDTGADDTVLEEMNLPGRWKPKMIGGIGGFIKVRQYDQILIEICGHKAIGTVLVGPTPVNIIGRNLLTQIGCTLNF".to_string(), pdb_id: Some("1HVR".to_string()), pockets: vec![ BindingPocket { id: 0, name: "Active site".to_string(), residues: vec![23, 25, 27, 28, 29, 30, 32, 47, 48, 49, 50, 76, 80, 81, 82, 84], center: Coordinate3D { x: 18.0, y: 12.0, z: 22.0 }, volume: 380.0, druggability: 0.95, }, ], }, ] } #[cfg(test)] mod tests { use super::*; #[test] fn test_smiles_to_molecule() { let mol = smiles_to_molecule("CCO", Some("Ethanol".to_string())); assert!(mol.is_ok()); let molecule = mol.unwrap(); assert_eq!(molecule.name, Some("Ethanol".to_string())); assert!(molecule.atoms.len() >= 2); // At least C and O } #[test] fn test_smiles_benzene() { let mol = smiles_to_molecule("c1ccccc1", None); assert!(mol.is_ok()); let molecule = mol.unwrap(); assert!(molecule.atoms.iter().any(|a| a.is_aromatic)); } #[test] fn test_smiles_aspirin() { let mol = smiles_to_molecule("CC(=O)OC1=CC=CC=C1C(=O)O", Some("Aspirin".to_string())); assert!(mol.is_ok()); let molecule = mol.unwrap(); assert!(molecule.molecular_weight > 150.0); assert!(molecule.molecular_weight < 200.0); } #[test] fn test_get_protein_by_id() { let result = get_protein_by_id("P00918"); assert!(result.is_ok()); let protein = result.unwrap(); assert_eq!(protein.name, "Carbonic anhydrase 2"); assert!(!protein.pockets.is_empty()); } #[test] fn test_get_unknown_protein() { let result = get_protein_by_id("UNKNOWN123"); assert!(result.is_err()); } #[test] fn test_sequence_to_protein() { let protein = sequence_to_protein("MKLAVLKLAGLLAGLLAL", Some("Test protein".to_string())); assert_eq!(protein.name, "Test protein"); assert_eq!(protein.sequence.len(), 18); } #[test] fn test_parse_smiles_with_branches() { let mol = smiles_to_molecule("CC(C)C", None); // Isobutane assert!(mol.is_ok()); let molecule = mol.unwrap(); assert!(molecule.atoms.len() >= 4); } #[test] fn test_parse_smiles_with_ring() { let mol = smiles_to_molecule("C1CCCCC1", None); // Cyclohexane assert!(mol.is_ok()); let molecule = mol.unwrap(); // Should have ring closure bond assert!(molecule.bonds.iter().any(|b| b.is_in_ring)); } #[test] fn test_molecular_properties() { let mol = smiles_to_molecule("c1ccc(O)cc1", None).unwrap(); // Phenol assert!(mol.properties.hbd >= 1); // OH is HBD assert!(mol.properties.hba >= 1); // O is HBA assert!(mol.properties.num_aromatic_rings >= 1); } #[test] fn test_lipinski_check() { // Small drug-like molecule let mol = smiles_to_molecule("CCO", None).unwrap(); assert!(mol.properties.lipinski_pass); } #[test] fn test_element_mass_lookup() { assert!((element_mass(&Element::C) - 12.011).abs() < 0.01); assert!((element_mass(&Element::O) - 15.999).abs() < 0.01); assert!((element_mass(&Element::N) - 14.007).abs() < 0.01); } #[test] fn test_protein_database() { let proteins = get_protein_database(); assert!(proteins.len() >= 5); // Check for key proteins assert!(proteins.iter().any(|p| p.id == "P00918")); // CA2 assert!(proteins.iter().any(|p| p.id == "P00533")); // EGFR assert!(proteins.iter().any(|p| p.id == "P04585")); // HIV protease } #[test] fn test_qed_calculation() { // Drug-like molecule (aspirin) let mol = smiles_to_molecule("CC(=O)OC1=CC=CC=C1C(=O)O", None).unwrap(); assert!(mol.properties.qed > 0.3); assert!(mol.properties.qed < 1.0); } #[test] fn test_halogen_parsing() { let mol = smiles_to_molecule("CCCl", None); // Chloroethane assert!(mol.is_ok()); let molecule = mol.unwrap(); assert!(molecule.atoms.iter().any(|a| a.element == Element::Cl)); } #[test] fn test_double_bond() { let mol = smiles_to_molecule("C=C", None); // Ethene assert!(mol.is_ok()); let molecule = mol.unwrap(); assert!( molecule .bonds .iter() .any(|b| b.bond_type == BondType::Double) ); } #[test] fn test_triple_bond() { let mol = smiles_to_molecule("C#N", None); // HCN assert!(mol.is_ok()); let molecule = mol.unwrap(); assert!( molecule .bonds .iter() .any(|b| b.bond_type == BondType::Triple) ); } }