#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Center {
pub atom: u32,
pub ligands: [u32; 4],
pub sign: f64,
}
impl Center {
pub const IMPLICIT: u32 = u32::MAX;
#[must_use]
pub fn is_three_coordinate(&self) -> bool {
self.ligands[3] == Self::IMPLICIT
}
#[must_use]
pub fn real_ligands(&self) -> &[u32] {
if self.is_three_coordinate() {
&self.ligands[..3]
} else {
&self.ligands
}
}
}
#[must_use]
pub fn centers(mol: &omgkit_core::MolBuilder) -> Vec<Center> {
use omgkit_core::ChiralTag;
let mut out = Vec::new();
for (idx, a) in mol.atoms().iter().enumerate() {
let sign = match a.chiral_tag {
ChiralTag::Ccw => 1.0,
ChiralTag::Cw => -1.0,
_ => continue,
};
let Ok(id) = u32::try_from(idx) else { continue };
let nb: Vec<u32> = mol.neighbors(id).map(|(y, _)| y).collect();
let ligands = match nb.len() {
4 => [nb[0], nb[1], nb[2], nb[3]],
3 if omgkit_core::element::has_stereogenic_lone_pair(a.atomic_num, a.formal_charge) => {
[nb[0], nb[1], nb[2], Center::IMPLICIT]
}
_ => continue, };
out.push(Center {
atom: id,
ligands,
sign,
});
}
out
}
#[must_use]
pub fn signed_volume(p0: [f64; 3], p1: [f64; 3], p2: [f64; 3], p3: [f64; 3]) -> f64 {
let d = |p: [f64; 3]| [p[0] - p0[0], p[1] - p0[1], p[2] - p0[2]];
let (a, b, c) = (d(p1), d(p2), d(p3));
a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0])
+ a[2] * (b[0] * c[1] - b[1] * c[0])
}
#[must_use]
pub fn center_volume(coords: &[[f64; 3]], c: &Center) -> f64 {
let o = coords[c.atom as usize];
let d = |k: usize| {
let p = coords[c.ligands[k] as usize];
[p[0] - o[0], p[1] - o[1], p[2] - o[2]]
};
let (a, b, e) = (d(0), d(1), d(2));
a[0] * (b[1] * e[2] - b[2] * e[1]) - a[1] * (b[0] * e[2] - b[2] * e[0])
+ a[2] * (b[0] * e[1] - b[1] * e[0])
}
#[must_use]
pub fn correct_count(coords: &[[f64; 3]], centers: &[Center]) -> usize {
centers
.iter()
.filter(|c| {
let v = center_volume(coords, c);
v != 0.0 && v.signum() == c.sign
})
.count()
}
#[must_use]
pub fn needs_reflection(coords: &[[f64; 3]], centers: &[Center]) -> bool {
let ok = correct_count(coords, centers);
let nonzero = centers
.iter()
.filter(|c| center_volume(coords, c) != 0.0)
.count();
nonzero - ok > ok
}
pub fn reflect(coords: &mut [[f64; 3]]) {
for p in coords.iter_mut() {
p[0] = -p[0];
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reference_tetrahedron() -> [[f64; 3]; 4] {
let r = (8.0_f64).sqrt() / 3.0;
let z = -1.0 / 3.0;
[
[0.0, 0.0, 1.0],
[r, 0.0, z],
[
r * (2.0 * std::f64::consts::PI / 3.0).cos(),
r * (2.0 * std::f64::consts::PI / 3.0).sin(),
z,
],
[
r * (4.0 * std::f64::consts::PI / 3.0).cos(),
r * (4.0 * std::f64::consts::PI / 3.0).sin(),
z,
],
]
}
#[test]
fn 参照四面体把符号钉死() {
let t = reference_tetrahedron();
let v = signed_volume(t[0], t[1], t[2], t[3]);
assert!(v < 0.0, "逆时针(@ / Ccw)的有符号体积应当是负的,实际 {v}");
let v2 = signed_volume(t[0], t[2], t[1], t[3]);
assert!(v2 > 0.0, "对换两个配体之后应当变号,实际 {v2}");
assert!((v + v2).abs() < 1e-12, "一次对换只该变号,大小不变");
let depict_pts = [
[0.0, 0.0, 1.0],
[1.0, 0.0, -0.33],
[-0.5, 0.866, -0.33],
[-0.5, -0.866, -0.33],
];
let dv = signed_volume(depict_pts[0], depict_pts[1], depict_pts[2], depict_pts[3]);
assert!(
dv < 0.0,
"与 omgkit-depict 的符号约定对不上了:同一组点它判 Ccw(det<0),这里得 {dv}"
);
}
#[test]
fn 写出去的号与读回来的标记是同一套() {
use omgkit_core::ChiralTag;
for smi in [
"C[C@H](N)O",
"C[C@@H](N)O",
"N[C@@H](C)C(=O)O",
"O[C@H]1CC[C@@H](N)CC1",
] {
let mut mol = omgkit_io::smiles::parse(smi).expect("解析");
let conf = crate::pipeline::conformer_for(&mut mol).expect("生成构型");
let want: Vec<ChiralTag> = mol.atoms().iter().map(|a| a.chiral_tag).collect();
let mut read = mol.clone();
for a in 0..u32::try_from(read.num_atoms()).expect("原子数") {
if let Some(at) = read.atom_mut(a) {
at.chiral_tag = ChiralTag::Unspecified;
}
}
let n = omgkit_io::stereo::assign_chirality_3d(&mut read, &conf.coords);
assert!(n > 0, "{smi}:一个中心都没读出来");
for (i, w) in want.iter().enumerate() {
if matches!(w, ChiralTag::Cw | ChiralTag::Ccw) {
assert_eq!(
read.atoms()[i].chiral_tag,
*w,
"{smi}:第 {i} 个原子写出去是 {w:?},读回来是 {:?} —— 两处约定对不上",
read.atoms()[i].chiral_tag
);
}
}
}
}
#[test]
fn 镜像把每个体积都变号() {
let t = reference_tetrahedron();
let mut m = t;
reflect(&mut m);
let (a, b) = (
signed_volume(t[0], t[1], t[2], t[3]),
signed_volume(m[0], m[1], m[2], m[3]),
);
assert!((a + b).abs() < 1e-12, "镜像后应当恰好变号:{a} vs {b}");
}
#[test]
fn 旋转不改变符号() {
let t = reference_tetrahedron();
let (c, s) = (0.6_f64, 0.8_f64);
let rot = |p: [f64; 3]| [c * p[0] - s * p[1], s * p[0] + c * p[1], p[2]];
let r = [rot(t[0]), rot(t[1]), rot(t[2]), rot(t[3])];
let (a, b) = (
signed_volume(t[0], t[1], t[2], t[3]),
signed_volume(r[0], r[1], r[2], r[3]),
);
assert!((a - b).abs() < 1e-12, "旋转不该改变有符号体积:{a} vs {b}");
}
fn reference_center() -> Vec<[f64; 3]> {
let mut v = vec![[0.0, 0.0, 0.0]];
v.extend_from_slice(&reference_tetrahedron());
v
}
fn ctr(sign: f64) -> Center {
Center {
atom: 0,
ligands: [1, 2, 3, 4],
sign,
}
}
#[test]
fn 参照四面体的中心基点体积号为正() {
let c = reference_center();
let vl = signed_volume(c[1], c[2], c[3], c[4]);
let vc = center_volume(&c, &ctr(1.0));
assert!(vl < 0.0, "四配体行列式该是负的,实得 {vl}");
assert!(vc > 0.0, "中心基点行列式该是正的,实得 {vc}");
assert!(
(vl / vc + 4.0).abs() < 1e-9,
"正四面体上 V_配体 该是 −4·V_中心:{vl} / {vc}"
);
}
#[test]
fn 全局定向按多数决() {
let coords = reference_center();
assert!(!needs_reflection(&coords, &[ctr(1.0)]), "号已经对了,不该翻");
assert!(needs_reflection(&coords, &[ctr(-1.0)]), "号反了,应当翻");
assert!(
!needs_reflection(&coords, &[ctr(-1.0), ctr(1.0)]),
"平局时的规则是不翻"
);
assert!(
needs_reflection(&coords, &[ctr(-1.0), ctr(-1.0), ctr(1.0)]),
"二比一应当翻"
);
}
fn flip_center(coords: &mut [[f64; 3]], lig: [usize; 3]) {
let (p, q, r) = (coords[lig[0]], coords[lig[1]], coords[lig[2]]);
let sub = |u: [f64; 3], v: [f64; 3]| [u[0] - v[0], u[1] - v[1], u[2] - v[2]];
let (a, b) = (sub(q, p), sub(r, p));
let n = [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
let nn = n[0] * n[0] + n[1] * n[1] + n[2] * n[2];
let d = sub(coords[0], p);
let t = (d[0] * n[0] + d[1] * n[1] + d[2] * n[2]) / nn;
for k in 0..3 {
coords[0][k] -= 2.0 * t * n[k];
}
}
#[test]
fn 中心原子翻到配体四面体外面时号必须跟着翻() {
let mut coords = reference_center();
let before = center_volume(&coords, &ctr(1.0));
let vl_before = signed_volume(coords[1], coords[2], coords[3], coords[4]);
flip_center(&mut coords, [1, 2, 3]);
let after = center_volume(&coords, &ctr(1.0));
let vl_after = signed_volume(coords[1], coords[2], coords[3], coords[4]);
assert!(
(vl_before - vl_after).abs() < 1e-12,
"四配体行列式**不该**变(它正是看不见翻伞的原因):{vl_before} → {vl_after}"
);
assert!(
before * after < 0.0,
"中心基点体积必须变号:{before} → {after}"
);
assert_eq!(correct_count(&coords, &[ctr(1.0)]), 0, "翻伞之后号该判错");
}
#[test]
fn 压平的中心不算数() {
let flat = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
];
assert_eq!(correct_count(&flat, &[ctr(1.0)]), 0);
assert_eq!(correct_count(&flat, &[ctr(-1.0)]), 0);
assert!(
!needs_reflection(&flat, &[ctr(1.0)]),
"全是零体积,翻了也没用"
);
}
#[test]
fn 没有手性中心时不翻() {
let coords = vec![[0.0; 3]; 4];
assert!(!needs_reflection(&coords, &[]));
}
}