use omgkit_core::MolBuilder;
use crate::{
adjust_hs, assign_radicals, clean_up, cleanup_chirality, cleanup_organometallics, kekulize,
perceive_rings, set_aromaticity, set_conjugation, set_hybridization, update_property_cache,
KekulizeError, ValenceError,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SanitizeError {
Valence(ValenceError),
Kekulize(KekulizeError),
}
impl std::fmt::Display for SanitizeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Valence(e) => write!(f, "价键计算失败:{e}"),
Self::Kekulize(e) => write!(f, "kekulize 失败:{e}"),
}
}
}
impl std::error::Error for SanitizeError {}
pub fn sanitize(mol: &mut MolBuilder) -> Result<(), SanitizeError> {
clean_up(mol);
cleanup_organometallics(mol);
update_property_cache(mol).map_err(SanitizeError::Valence)?;
let _ = perceive_rings(mol);
kekulize(mol).map_err(SanitizeError::Kekulize)?;
assign_radicals(mol);
set_aromaticity(mol);
let implicit_before: Vec<u8> = mol.atoms().iter().map(|a| a.num_implicit_hs).collect();
update_property_cache(mol).map_err(SanitizeError::Valence)?;
set_conjugation(mol);
set_hybridization(mol);
cleanup_chirality(mol);
adjust_hs(mol, &implicit_before);
Ok(())
}
#[cfg(test)]
mod tests {
use omgkit_core::ChiralTag;
use omgkit_io::smiles;
use super::*;
fn total_hs(m: &MolBuilder, i: usize) -> u32 {
let a = m.atoms()[i];
u32::from(a.num_explicit_hs) + u32::from(a.num_implicit_hs)
}
#[test]
fn pyrrole_keeps_its_nh() {
let mut m = smiles::parse("c1cc[nH]c1").unwrap();
sanitize(&mut m).expect("应能净化");
assert_eq!(total_hs(&m, 3), 1, "吡咯氮上的氢");
assert_eq!(m.atoms()[3].num_explicit_hs, 1, "且记在显式那一侧");
}
#[test]
fn 净化会剔除几何上不成立的立体标记() {
let cleared = [
("[Pt@SP1](Cl)(Cl)(N)(N)Cl", 0, "平面四方标记而配位数是 5"),
("[Pt@SP1]Cl", 0, "平面四方标记而配位数是 1"),
("[Fe@TB1]Cl", 0, "三角双锥标记而配位数是 1"),
("[Fe@OH1]Cl", 0, "八面体标记而配位数是 1"),
(
"CC1[C@@-]2[C@@H](N=C[NH+]=C2O)N=C1C",
2,
"四面体标记而该原子不是 sp³",
),
];
for (smi, idx, why) in cleared {
let mut m = smiles::parse(smi).unwrap_or_else(|e| panic!("{smi}: {e}"));
assert_ne!(
m.atoms()[idx].chiral_tag,
ChiralTag::Unspecified,
"{smi}:解析出来就该带标记,否则这条用例什么也没测"
);
sanitize(&mut m).unwrap_or_else(|e| panic!("{smi}: {e}"));
assert_eq!(
m.atoms()[idx].chiral_tag,
ChiralTag::Unspecified,
"{smi} 的原子 {idx}({why})的标记该在第 11 步被清掉"
);
}
for (smi, idx) in [("C[C@H](N)O", 1), ("[C@@H](F)(Cl)Br", 0)] {
let mut m = smiles::parse(smi).unwrap_or_else(|e| panic!("{smi}: {e}"));
let want = m.atoms()[idx].chiral_tag;
assert_ne!(want, ChiralTag::Unspecified, "{smi}:用例本身要带标记");
sanitize(&mut m).unwrap_or_else(|e| panic!("{smi}: {e}"));
assert_eq!(
m.atoms()[idx].chiral_tag,
want,
"{smi} 的原子 {idx} 几何成立,标记不该被动"
);
}
}
#[test]
fn common_molecules() {
for (smi, atom, hs) in [
("CCO", 0, 3),
("CCO", 2, 1),
("c1ccccc1", 0, 1),
("c1ccc2[nH]ccc2c1", 4, 1),
("CC(=O)O", 3, 1),
] {
let mut m = smiles::parse(smi).unwrap();
sanitize(&mut m).unwrap_or_else(|e| panic!("{smi}: {e}"));
assert_eq!(total_hs(&m, atom), hs, "{smi} 的原子 {atom}");
}
}
#[test]
fn hypervalent_is_rejected() {
let mut m = smiles::parse("C(C)(C)(C)(C)C").unwrap();
assert!(matches!(sanitize(&mut m), Err(SanitizeError::Valence(_))));
}
#[test]
fn is_idempotent() {
for smi in [
"c1cc[nH]c1",
"c1ccccc1",
"CCO",
"c1ccc2[nH]ccc2c1",
"CC(=O)O",
] {
let mut m = smiles::parse(smi).unwrap();
sanitize(&mut m).unwrap();
let once: Vec<_> = m.atoms().to_vec();
let once_b: Vec<_> = m.bonds().to_vec();
sanitize(&mut m).unwrap();
assert_eq!(m.atoms(), &once[..], "{smi}:原子不幂等");
assert_eq!(m.bonds(), &once_b[..], "{smi}:键不幂等");
}
}
}