use omgkit_core::{ChiralTag, Hybridization, MolBuilder};
pub fn cleanup_chirality(mol: &mut MolBuilder) -> usize {
let mut changed = 0;
for i in 0..mol.num_atoms() as u32 {
let a = mol.atoms()[i as usize];
let total_degree =
mol.degree(i) + usize::from(a.num_explicit_hs) + usize::from(a.num_implicit_hs);
let verdict = match a.chiral_tag {
ChiralTag::Unspecified => continue,
ChiralTag::Cw | ChiralTag::Ccw => {
if a.hybridization == Hybridization::Sp3 {
Verdict::Keep
} else {
Verdict::Clear
}
}
ChiralTag::SquarePlanar => geometry_verdict(total_degree, 4, a.stereo_perm, 3),
ChiralTag::TrigonalBipyramidal => geometry_verdict(total_degree, 5, a.stereo_perm, 20),
ChiralTag::Octahedral => geometry_verdict(total_degree, 6, a.stereo_perm, 30),
ChiralTag::Allene => continue,
};
match verdict {
Verdict::Keep => {}
Verdict::Clear => {
if let Some(m) = mol.atom_mut(i) {
m.chiral_tag = ChiralTag::Unspecified;
m.stereo_perm = 0;
}
changed += 1;
}
Verdict::ResetPerm => {
if let Some(m) = mol.atom_mut(i) {
m.stereo_perm = 0;
}
changed += 1;
}
}
}
changed
}
enum Verdict {
Keep,
Clear,
ResetPerm,
}
fn geometry_verdict(degree: usize, max_degree: usize, perm: u8, max_perm: u8) -> Verdict {
if !(2..=max_degree).contains(°ree) {
Verdict::Clear
} else if perm > max_perm {
Verdict::ResetPerm
} else {
Verdict::Keep
}
}
#[cfg(test)]
mod tests {
use omgkit_io::smiles;
use super::*;
use crate::{
assign_radicals, clean_up, conjugation::set_hybridization, kekulize, perceive_rings,
set_aromaticity, set_conjugation, valence::update_property_cache,
};
fn upto_step9(smi: &str) -> MolBuilder {
let mut m = smiles::parse(smi).unwrap_or_else(|e| panic!("{smi}: {}", e.render()));
clean_up(&mut m);
update_property_cache(&mut m).expect("价键");
let _ = perceive_rings(&mut m);
kekulize(&mut m).expect("kekulize");
assign_radicals(&mut m);
set_aromaticity(&mut m);
update_property_cache(&mut m).expect("收尾价键");
set_conjugation(&mut m);
set_hybridization(&mut m);
m
}
fn tag_after(smi: &str, atom: usize) -> ChiralTag {
let mut m = upto_step9(smi);
cleanup_chirality(&mut m);
m.atoms()[atom].chiral_tag
}
#[test]
fn sp3_tetrahedral_is_kept_even_when_not_a_real_stereocentre() {
for (smi, atom) in [("N[C@@H](C)C(=O)O", 1), ("[C@@H]1CCCC1", 0)] {
let before = upto_step9(smi).atoms()[atom].chiral_tag;
assert!(before.is_tetrahedral(), "{smi}:前提是它带四面体标记");
assert_eq!(
tag_after(smi, atom),
before,
"{smi}:sp³ 的四面体标记应当原样保留 —— \
即使它其实不是真手性中心,那也是 genuine_tetrahedral 该判的事"
);
}
}
#[test]
fn non_sp3_tetrahedral_is_cleared() {
let mut m = upto_step9("c1ccccc1");
if let Some(a) = m.atom_mut(0) {
a.chiral_tag = ChiralTag::Cw;
}
assert_eq!(cleanup_chirality(&mut m), 1);
assert_eq!(m.atoms()[0].chiral_tag, ChiralTag::Unspecified);
}
#[test]
fn coordination_geometry_checks_the_degree() {
assert_eq!(
tag_after("[Pt@SP1](Cl)(Cl)(N)N", 0),
ChiralTag::SquarePlanar
);
assert_eq!(
tag_after("[Pt@SP1](Cl)(Cl)(N)(N)Cl", 0),
ChiralTag::Unspecified,
"五配位的平面四方声称站不住"
);
assert_eq!(
tag_after("[Pt@SP1]Cl", 0),
ChiralTag::Unspecified,
"一配位谈不上几何"
);
assert_eq!(
tag_after("C[Co@OH25](N)(O)(S)(P)Cl", 1),
ChiralTag::Octahedral
);
assert_eq!(
tag_after("C[Co@OH25](N)(O)(S)(P)(Cl)Br", 1),
ChiralTag::Unspecified,
"七配位的八面体声称站不住"
);
}
#[test]
fn out_of_range_permutation_is_reset_but_geometry_kept() {
let mut m = upto_step9("[Pt@SP1](Cl)(Cl)(N)N");
if let Some(a) = m.atom_mut(0) {
a.stereo_perm = 99; }
assert_eq!(cleanup_chirality(&mut m), 1);
assert_eq!(m.atoms()[0].chiral_tag, ChiralTag::SquarePlanar, "类别保留");
assert_eq!(m.atoms()[0].stereo_perm, 0, "序号归零");
}
#[test]
fn is_idempotent() {
for smi in [
"N[C@@H](C)C(=O)O",
"[Pt@SP1](Cl)(Cl)(N)N",
"[Pt@SP1]Cl",
"CCO",
] {
let mut m = upto_step9(smi);
cleanup_chirality(&mut m);
let once: Vec<_> = m.atoms().to_vec();
assert_eq!(cleanup_chirality(&mut m), 0, "{smi}:第二次不该再有改动");
assert_eq!(m.atoms(), &once[..], "{smi}:不幂等");
}
}
}