Files
rustytorch/demos/rtx-drugbinder-demo/src/sample_data.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

690 lines
23 KiB
Rust

//! 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<String>) -> Result<Molecule, DrugBinderError> {
// 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<ProteinTarget, DrugBinderError> {
// 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<String>) -> 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<Atom>, Vec<Bond>), DrugBinderError> {
let mut atoms = Vec::new();
let mut bonds = Vec::new();
let mut ring_atoms: std::collections::HashMap<char, usize> = std::collections::HashMap::new();
let mut branch_stack: Vec<usize> = Vec::new();
let mut prev_atom: Option<usize> = None;
let mut pending_bond_type = BondType::Single;
let mut in_aromatic = false;
let chars: Vec<char> = 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<Element>, 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<ProteinTarget> {
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)
);
}
}