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