use omgkit_core::{ChiralTag, MolBuilder};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Wedge {
#[default]
None,
Up {
narrow: u32,
},
Down {
narrow: u32,
},
}
impl Wedge {
#[must_use]
pub fn narrow(self) -> Option<u32> {
match self {
Wedge::None => None,
Wedge::Up { narrow } | Wedge::Down { narrow } => Some(narrow),
}
}
}
pub(crate) const ZERO_VOLUME_TOL: f64 = 0.1;
enum Ligand {
Atom(u32, u32),
ImplicitH,
}
pub(crate) fn total_hs(mol: &MolBuilder, atom: u32) -> u8 {
let a = mol.atoms()[atom as usize];
a.num_explicit_hs.saturating_add(a.num_implicit_hs)
}
pub(crate) fn has_lone_pair(mol: &MolBuilder, a: u32) -> bool {
let at = mol.atoms()[a as usize];
omgkit_core::element::has_stereogenic_lone_pair(at.atomic_num, at.formal_charge)
}
#[must_use]
pub fn chirality_from_wedges(
mol: &MolBuilder,
coords: &[[f64; 3]],
wedges: &[Wedge],
a: u32,
) -> Option<ChiralTag> {
let nbrs: Vec<(u32, u32)> = mol.neighbors(a).collect();
let hs = total_hs(mol, a);
let mut refs: Vec<Ligand> = Vec::with_capacity(4);
match (nbrs.len(), hs) {
(4, 0) => {
for (n, bi) in &nbrs {
refs.push(Ligand::Atom(*n, *bi));
}
}
(3, 1) | (3, 0) if hs == 1 || has_lone_pair(mol, a) => {
refs.push(Ligand::Atom(nbrs[0].0, nbrs[0].1));
refs.push(Ligand::ImplicitH);
refs.push(Ligand::Atom(nbrs[1].0, nbrs[1].1));
refs.push(Ligand::Atom(nbrs[2].0, nbrs[2].1));
}
_ => return None, }
let centre = coords[a as usize];
let mut pts: Vec<[f64; 3]> = Vec::with_capacity(4);
let mut h_slot = None;
for (k, l) in refs.iter().enumerate() {
match l {
Ligand::Atom(n, bi) => {
let p = coords[*n as usize];
let z = match wedges[*bi as usize] {
Wedge::Up { narrow } => {
if narrow == a {
1.0
} else {
-1.0
}
}
Wedge::Down { narrow } => {
if narrow == a {
-1.0
} else {
1.0
}
}
Wedge::None => 0.0,
};
pts.push([p[0] - centre[0], p[1] - centre[1], z]);
}
Ligand::ImplicitH => {
h_slot = Some(k);
pts.push([0.0, 0.0, 0.0]); }
}
}
if let Some(k) = h_slot {
let zsum: f64 = pts.iter().map(|p| p[2]).sum();
if zsum.abs() < 1e-9 {
return None;
}
let v: Vec<[f64; 3]> = pts
.iter()
.enumerate()
.filter(|(i, _)| *i != k)
.map(|(_, p)| *p)
.collect();
let cross = [
v[1][1] * v[2][2] - v[1][2] * v[2][1],
v[1][2] * v[2][0] - v[1][0] * v[2][2],
v[1][0] * v[2][1] - v[1][1] * v[2][0],
];
let vol = v[0][0] * cross[0] + v[0][1] * cross[1] + v[0][2] * cross[2];
if vol.abs() <= ZERO_VOLUME_TOL {
return None;
}
let mut sx = 0.0_f64;
let mut sy = 0.0_f64;
for p in &v {
let n = p[0].hypot(p[1]);
if n < 1e-9 {
return None; }
sx += p[0] / n;
sy += p[1] / n;
}
pts[k] = [-sx, -sy, -zsum.signum()];
}
let d = |i: usize, j: usize| pts[i][j] - pts[0][j];
let det = d(1, 0) * (d(2, 1) * d(3, 2) - d(2, 2) * d(3, 1))
- d(1, 1) * (d(2, 0) * d(3, 2) - d(2, 2) * d(3, 0))
+ d(1, 2) * (d(2, 0) * d(3, 1) - d(2, 1) * d(3, 0));
if det.abs() < 1e-12 {
None
} else if det < 0.0 {
Some(ChiralTag::Ccw)
} else {
Some(ChiralTag::Cw)
}
}
#[cfg(test)]
mod tests {
const WEDGED: &str = "\
C[C@H](N)O
RDKit 2D
4 3 0 0 0 0 0 0 0 0999 V2000
-1.2990 -0.7500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
1.2990 -0.7500 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
0.0000 1.5000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
2 1 1 1
2 3 1 0
2 4 1 0
M END
";
fn tagged(block: &str) -> usize {
let got = crate::molblock::read_v2000(block).expect("读 molblock");
let mut m = got.mol;
omgkit_chem::pipeline::sanitize(&mut m).expect("净化");
crate::stereo::assign_chirality_2d(&mut m, &got.coords, &got.wedges)
}
#[test]
fn a_wedge_on_a_flat_drawing_gives_a_centre() {
assert_eq!(tagged(WEDGED), 1);
}
#[test]
fn the_same_drawing_lifted_into_3d_gives_nothing() {
let lifted = WEDGED.replace(
" 0.0000 1.5000 0.0000 O",
" 0.0000 1.5000 0.9000 O",
);
assert_ne!(lifted, WEDGED, "原子块那一行没改到");
assert_eq!(tagged(&lifted), 0);
}
}