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]>
690 lines
23 KiB
Rust
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)
|
|
);
|
|
}
|
|
}
|