use omgkit_core::MolBuilder;
pub const CROSS_TOL: f64 = 1.1;
pub const RIGID_TOPO: u8 = 1;
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Threading {
pub crossings: usize,
pub pierces: usize,
pub min_gap: f64,
pub pairs: usize,
}
#[must_use]
pub fn segment_distance(p1: [f64; 3], q1: [f64; 3], p2: [f64; 3], q2: [f64; 3]) -> f64 {
let sub = |a: [f64; 3], b: [f64; 3]| [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
let d1 = sub(q1, p1);
let d2 = sub(q2, p2);
let r = sub(p1, p2);
let (a, e, f) = (dot(d1, d1), dot(d2, d2), dot(d2, r));
const EPS: f64 = 1e-12;
if a <= EPS && e <= EPS {
return dot(r, r).sqrt();
}
let (s, t);
if a <= EPS {
s = 0.0;
t = (f / e).clamp(0.0, 1.0);
} else {
let c = dot(d1, r);
if e <= EPS {
t = 0.0;
s = (-c / a).clamp(0.0, 1.0);
} else {
let b = dot(d1, d2);
let denom = a * e - b * b;
let s0 = if denom > EPS {
((b * f - c * e) / denom).clamp(0.0, 1.0)
} else {
0.0
};
let t0 = (b * s0 + f) / e;
if t0 < 0.0 {
t = 0.0;
s = (-c / a).clamp(0.0, 1.0);
} else if t0 > 1.0 {
t = 1.0;
s = ((b - c) / a).clamp(0.0, 1.0);
} else {
t = t0;
s = s0;
}
}
}
let c1 = [p1[0] + d1[0] * s, p1[1] + d1[1] * s, p1[2] + d1[2] * s];
let c2 = [p2[0] + d2[0] * t, p2[1] + d2[1] * t, p2[2] + d2[2] * t];
let d = sub(c1, c2);
dot(d, d).sqrt()
}
#[must_use]
pub fn segment_hits_triangle(
p: [f64; 3],
q: [f64; 3],
a: [f64; 3],
b: [f64; 3],
c: [f64; 3],
) -> bool {
let sub = |x: [f64; 3], y: [f64; 3]| [x[0] - y[0], x[1] - y[1], x[2] - y[2]];
let dot = |x: [f64; 3], y: [f64; 3]| x[0] * y[0] + x[1] * y[1] + x[2] * y[2];
let cross = |x: [f64; 3], y: [f64; 3]| {
[
x[1] * y[2] - x[2] * y[1],
x[2] * y[0] - x[0] * y[2],
x[0] * y[1] - x[1] * y[0],
]
};
let dir = sub(q, p);
let (e1, e2) = (sub(b, a), sub(c, a));
let h = cross(dir, e2);
let det = dot(e1, h);
if det.abs() < 1e-12 {
return false; }
let inv = 1.0 / det;
let s = sub(p, a);
let u = inv * dot(s, h);
if !(0.0..=1.0).contains(&u) {
return false;
}
let qv = cross(s, e1);
let v = inv * dot(dir, qv);
if v < 0.0 || u + v > 1.0 {
return false;
}
let t = inv * dot(e2, qv);
(0.0..=1.0).contains(&t)
}
fn components(mol: &MolBuilder) -> Vec<usize> {
let n = mol.num_atoms();
let mut comp = vec![usize::MAX; n];
let mut next = 0;
for start in 0..n {
if comp[start] != usize::MAX {
continue;
}
let mut q = std::collections::VecDeque::from([start]);
comp[start] = next;
while let Some(x) = q.pop_front() {
let Ok(xu) = u32::try_from(x) else { continue };
for (y, _) in mol.neighbors(xu) {
let y = y as usize;
if y < n && comp[y] == usize::MAX {
comp[y] = next;
q.push_back(y);
}
}
}
next += 1;
}
comp
}
#[must_use]
pub fn detect(mol: &MolBuilder, coords: &[[f64; 3]]) -> Threading {
assert_eq!(coords.len(), mol.num_atoms(), "坐标数与原子数对不上");
let bonds: Vec<(usize, usize)> = mol
.bonds()
.iter()
.map(|b| (b.begin as usize, b.end as usize))
.collect();
let mut t = Threading {
min_gap: f64::MAX,
..Threading::default()
};
let comp = components(mol);
let n = mol.num_atoms();
let cap = RIGID_TOPO + 1;
let mut topo = vec![cap; n * n];
for start in 0..n {
let mut d = vec![u8::MAX; n];
d[start] = 0;
let mut q = std::collections::VecDeque::from([start]);
while let Some(x) = q.pop_front() {
if d[x] >= cap {
continue;
}
let Ok(xu) = u32::try_from(x) else { continue };
for (y, _) in mol.neighbors(xu) {
let y = y as usize;
if y < n && d[y] == u8::MAX {
d[y] = d[x] + 1;
q.push_back(y);
}
}
}
for j in 0..n {
topo[start * n + j] = d[j].min(cap);
}
}
for (x, &(i, j)) in bonds.iter().enumerate() {
for &(k, l) in &bonds[(x + 1)..] {
if i == k || i == l || j == k || j == l {
continue;
}
if comp[i] != comp[k] {
continue;
}
let near = [(i, k), (i, l), (j, k), (j, l)]
.iter()
.map(|&(a, b)| topo[a * n + b])
.min()
.unwrap_or(u8::MAX);
if near <= RIGID_TOPO {
continue;
}
let d = segment_distance(coords[i], coords[j], coords[k], coords[l]);
t.pairs += 1;
t.min_gap = t.min_gap.min(d);
if d < CROSS_TOL {
t.crossings += 1;
}
}
}
for ring in omgkit_chem::sssr::ring_set(mol) {
let atoms: Vec<usize> = ring.atoms.iter().map(|a| *a as usize).collect();
if atoms.len() < 3 {
continue;
}
let n = atoms.len();
#[allow(clippy::cast_precision_loss)]
let nf = n as f64;
let mut cen = [0.0; 3];
for &a in &atoms {
for k in 0..3 {
cen[k] += coords[a][k] / nf;
}
}
for (x, &(i, j)) in bonds.iter().enumerate() {
let _ = x;
if atoms.contains(&i) || atoms.contains(&j) {
continue;
}
if comp[i] != comp[atoms[0]] {
continue;
}
let crossings = (0..n)
.filter(|&k| {
segment_hits_triangle(
coords[i],
coords[j],
cen,
coords[atoms[k]],
coords[atoms[(k + 1) % n]],
)
})
.count();
if crossings % 2 == 1 {
t.pierces += 1;
}
}
}
t
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn 线段距离的解析解() {
let d = segment_distance(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 0.0, 1.0],
[0.0, 1.0, 1.0],
);
assert!((d - 1.0).abs() < 1e-12, "{d}");
let d = segment_distance(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 2.0, 0.0],
[1.0, 2.0, 0.0],
);
assert!((d - 2.0).abs() < 1e-12, "平行线段 {d}");
let d = segment_distance(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
[4.0, 0.0, 0.0],
);
assert!((d - 2.0).abs() < 1e-12, "共线 {d}");
let d = segment_distance(
[-1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, -1.0, 0.0],
[0.0, 1.0, 0.0],
);
assert!(d < 1e-12, "相交的两条线段距离应当是 0,实得 {d}");
let d = segment_distance([0.0; 3], [0.0; 3], [3.0, 4.0, 0.0], [3.0, 4.0, 0.0]);
assert!((d - 5.0).abs() < 1e-12, "两个点 {d}");
}
fn mk(n: usize, bonds: &[(u32, u32)], xyz: &[[f64; 3]]) -> (MolBuilder, Vec<[f64; 3]>) {
let mut m = MolBuilder::new();
for _ in 0..n {
m.add_atom_data(omgkit_core::AtomData::new(6));
}
for &(i, j) in bonds {
m.add_bond(i, j, omgkit_core::BondOrder::Single).unwrap();
}
(m, xyz.to_vec())
}
#[test]
fn 被一根键连起来的两根键不算交叉() {
let (m, xyz) = mk(
4,
&[(0, 1), (1, 2), (2, 3)],
&[
[-1.0, 0.3, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.3, 0.0], ],
);
let t = detect(&m, &xyz);
let d = segment_distance(xyz[0], xyz[1], xyz[2], xyz[3]);
assert!(d < CROSS_TOL, "构型没摆够近({d}),这条测试白测");
assert_eq!(t.crossings, 0, "被一根键连起来的两根键不该记成交叉");
assert_eq!(t.pairs, 0, "这一对应当连查都不查");
}
#[test]
fn 真正够远的两根键照样查得出来() {
let (m, xyz) = mk(
6,
&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)],
&[
[-1.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[3.0, 2.0, 0.0],
[3.0, 6.0, 0.0],
[0.0, -1.0, 0.2],
[0.0, 1.0, 0.2],
],
);
let t = detect(&m, &xyz);
assert!(t.pairs > 0, "一对都没查,那个 0 只说明没在看");
assert!(
t.crossings >= 1,
"0–1 与 4–5 只差 0.2 Å 且拓扑上隔着 3 根键,必须报交叉;实得 {t:?}"
);
}
#[test]
fn 四面体烷的对棱不算交叉() {
let a = 1.508;
let s = a / 2.0_f64.sqrt() / 2.0;
let (m, xyz) = mk(
4,
&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
&[
[-a / 2.0, 0.0, -s],
[a / 2.0, 0.0, -s],
[0.0, -a / 2.0, s],
[0.0, a / 2.0, s],
],
);
let d = segment_distance(xyz[0], xyz[1], xyz[2], xyz[3]);
assert!(
(d - a / 2.0_f64.sqrt()).abs() < 1e-9,
"对棱距离该是 a/√2 = {},实得 {d}",
a / 2.0_f64.sqrt()
);
assert!(d < CROSS_TOL, "对棱距离 {d} 该低于阈值,否则这条测试白测");
assert_eq!(detect(&m, &xyz).crossings, 0, "四面体烷的对棱不该记成交叉");
}
#[test]
fn 错开的线段不能给出偏大的距离() {
let d = segment_distance(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[2.0, 0.0, 1.0],
);
assert!((d - 1.0).abs() < 1e-12, "{d}");
}
#[test]
fn 月牙环的凹口不算穿刺() {
let (m, xyz) = crescent(0.9);
let t = detect(&m, &xyz);
assert_eq!(
t.pierces, 0,
"在凹口里穿过 z 平面不是穿刺(交点 2 次,mod-2 为 0)"
);
}
#[test]
fn 月牙环的环身照样查得出来() {
let (m, xyz) = crescent(3.3);
let t = detect(&m, &xyz);
assert_eq!(t.pierces, 1, "r = 3.3 落在内外弧之间,是真穿刺");
}
fn crescent(r: f64) -> (MolBuilder, Vec<[f64; 3]>) {
const HALF: usize = 9;
let ang = |i: usize| (-150.0 + 300.0 * i as f64 / (HALF - 1) as f64).to_radians();
let mut xyz: Vec<[f64; 3]> = Vec::new();
for i in 0..HALF {
xyz.push([4.0 * ang(i).cos(), 4.0 * ang(i).sin(), 0.0]);
}
for i in (0..HALF).rev() {
xyz.push([2.6 * ang(i).cos(), 2.6 * ang(i).sin(), 0.0]);
}
let ring = 2 * HALF;
let probe = (-150.0 + 300.0 * 4.5 / (HALF - 1) as f64).to_radians();
xyz.push([xyz[0][0], xyz[0][1], 1.0]); xyz.push([r * probe.cos(), r * probe.sin(), 1.0]);
xyz.push([r * probe.cos(), r * probe.sin(), -1.0]);
let mut bonds: Vec<(u32, u32)> = (0..ring)
.map(|i| (i as u32, ((i + 1) % ring) as u32))
.collect();
bonds.push((0, ring as u32));
bonds.push((ring as u32, ring as u32 + 1));
bonds.push((ring as u32 + 1, ring as u32 + 2));
mk(ring + 3, &bonds, &xyz)
}
#[test]
fn 线段穿三角形() {
let (a, b, c) = ([0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0]);
assert!(segment_hits_triangle(
[0.5, 0.5, 1.0],
[0.5, 0.5, -1.0],
a,
b,
c
));
assert!(!segment_hits_triangle(
[5.0, 5.0, 1.0],
[5.0, 5.0, -1.0],
a,
b,
c
));
assert!(!segment_hits_triangle(
[0.5, 0.5, 1.0],
[0.5, 0.5, 0.5],
a,
b,
c
));
assert!(!segment_hits_triangle(
[0.5, 0.5, 1.0],
[1.5, 0.5, 1.0],
a,
b,
c
));
}
}