1use omgkit_core::MolBuilder;
43
44pub const CROSS_TOL: f64 = 1.1;
74
75pub const RIGID_TOPO: u8 = 1;
85
86#[derive(Debug, Clone, Copy, Default, PartialEq)]
98pub struct Threading {
99 pub crossings: usize,
101 pub pierces: usize,
104 pub min_gap: f64,
109 pub pairs: usize,
111}
112
113#[must_use]
118pub fn segment_distance(p1: [f64; 3], q1: [f64; 3], p2: [f64; 3], q2: [f64; 3]) -> f64 {
119 let sub = |a: [f64; 3], b: [f64; 3]| [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
120 let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
121 let d1 = sub(q1, p1);
122 let d2 = sub(q2, p2);
123 let r = sub(p1, p2);
124 let (a, e, f) = (dot(d1, d1), dot(d2, d2), dot(d2, r));
125 const EPS: f64 = 1e-12;
126
127 if a <= EPS && e <= EPS {
129 return dot(r, r).sqrt();
130 }
131 let (s, t);
132 if a <= EPS {
133 s = 0.0;
135 t = (f / e).clamp(0.0, 1.0);
136 } else {
137 let c = dot(d1, r);
138 if e <= EPS {
139 t = 0.0;
141 s = (-c / a).clamp(0.0, 1.0);
142 } else {
143 let b = dot(d1, d2);
144 let denom = a * e - b * b;
145 let s0 = if denom > EPS {
147 ((b * f - c * e) / denom).clamp(0.0, 1.0)
148 } else {
149 0.0
150 };
151 let t0 = (b * s0 + f) / e;
152 if t0 < 0.0 {
155 t = 0.0;
156 s = (-c / a).clamp(0.0, 1.0);
157 } else if t0 > 1.0 {
158 t = 1.0;
159 s = ((b - c) / a).clamp(0.0, 1.0);
160 } else {
161 t = t0;
162 s = s0;
163 }
164 }
165 }
166 let c1 = [p1[0] + d1[0] * s, p1[1] + d1[1] * s, p1[2] + d1[2] * s];
167 let c2 = [p2[0] + d2[0] * t, p2[1] + d2[1] * t, p2[2] + d2[2] * t];
168 let d = sub(c1, c2);
169 dot(d, d).sqrt()
170}
171
172#[must_use]
174pub fn segment_hits_triangle(
175 p: [f64; 3],
176 q: [f64; 3],
177 a: [f64; 3],
178 b: [f64; 3],
179 c: [f64; 3],
180) -> bool {
181 let sub = |x: [f64; 3], y: [f64; 3]| [x[0] - y[0], x[1] - y[1], x[2] - y[2]];
182 let dot = |x: [f64; 3], y: [f64; 3]| x[0] * y[0] + x[1] * y[1] + x[2] * y[2];
183 let cross = |x: [f64; 3], y: [f64; 3]| {
184 [
185 x[1] * y[2] - x[2] * y[1],
186 x[2] * y[0] - x[0] * y[2],
187 x[0] * y[1] - x[1] * y[0],
188 ]
189 };
190 let dir = sub(q, p);
191 let (e1, e2) = (sub(b, a), sub(c, a));
192 let h = cross(dir, e2);
193 let det = dot(e1, h);
194 if det.abs() < 1e-12 {
195 return false; }
197 let inv = 1.0 / det;
198 let s = sub(p, a);
199 let u = inv * dot(s, h);
200 if !(0.0..=1.0).contains(&u) {
201 return false;
202 }
203 let qv = cross(s, e1);
204 let v = inv * dot(dir, qv);
205 if v < 0.0 || u + v > 1.0 {
206 return false;
207 }
208 let t = inv * dot(e2, qv);
209 (0.0..=1.0).contains(&t)
211}
212
213fn components(mol: &MolBuilder) -> Vec<usize> {
215 let n = mol.num_atoms();
216 let mut comp = vec![usize::MAX; n];
217 let mut next = 0;
218 for start in 0..n {
219 if comp[start] != usize::MAX {
220 continue;
221 }
222 let mut q = std::collections::VecDeque::from([start]);
223 comp[start] = next;
224 while let Some(x) = q.pop_front() {
225 let Ok(xu) = u32::try_from(x) else { continue };
226 for (y, _) in mol.neighbors(xu) {
227 let y = y as usize;
228 if y < n && comp[y] == usize::MAX {
229 comp[y] = next;
230 q.push_back(y);
231 }
232 }
233 }
234 next += 1;
235 }
236 comp
237}
238
239#[must_use]
245pub fn detect(mol: &MolBuilder, coords: &[[f64; 3]]) -> Threading {
246 assert_eq!(coords.len(), mol.num_atoms(), "坐标数与原子数对不上");
247 let bonds: Vec<(usize, usize)> = mol
248 .bonds()
249 .iter()
250 .map(|b| (b.begin as usize, b.end as usize))
251 .collect();
252 let mut t = Threading {
253 min_gap: f64::MAX,
254 ..Threading::default()
255 };
256 let comp = components(mol);
257 let n = mol.num_atoms();
258 let cap = RIGID_TOPO + 1;
260 let mut topo = vec![cap; n * n];
261 for start in 0..n {
262 let mut d = vec![u8::MAX; n];
263 d[start] = 0;
264 let mut q = std::collections::VecDeque::from([start]);
265 while let Some(x) = q.pop_front() {
266 if d[x] >= cap {
267 continue;
268 }
269 let Ok(xu) = u32::try_from(x) else { continue };
270 for (y, _) in mol.neighbors(xu) {
271 let y = y as usize;
272 if y < n && d[y] == u8::MAX {
273 d[y] = d[x] + 1;
274 q.push_back(y);
275 }
276 }
277 }
278 for j in 0..n {
279 topo[start * n + j] = d[j].min(cap);
280 }
281 }
282
283 for (x, &(i, j)) in bonds.iter().enumerate() {
285 for &(k, l) in &bonds[(x + 1)..] {
286 if i == k || i == l || j == k || j == l {
288 continue;
289 }
290 if comp[i] != comp[k] {
292 continue;
293 }
294 let near = [(i, k), (i, l), (j, k), (j, l)]
297 .iter()
298 .map(|&(a, b)| topo[a * n + b])
299 .min()
300 .unwrap_or(u8::MAX);
301 if near <= RIGID_TOPO {
302 continue;
303 }
304 let d = segment_distance(coords[i], coords[j], coords[k], coords[l]);
305 t.pairs += 1;
306 t.min_gap = t.min_gap.min(d);
307 if d < CROSS_TOL {
308 t.crossings += 1;
309 }
310 }
311 }
312
313 for ring in omgkit_chem::sssr::ring_set(mol) {
315 let atoms: Vec<usize> = ring.atoms.iter().map(|a| *a as usize).collect();
316 if atoms.len() < 3 {
317 continue;
318 }
319 let n = atoms.len();
322 #[allow(clippy::cast_precision_loss)]
323 let nf = n as f64;
324 let mut cen = [0.0; 3];
325 for &a in &atoms {
326 for k in 0..3 {
327 cen[k] += coords[a][k] / nf;
328 }
329 }
330 for (x, &(i, j)) in bonds.iter().enumerate() {
331 let _ = x;
332 if atoms.contains(&i) || atoms.contains(&j) {
334 continue;
335 }
336 if comp[i] != comp[atoms[0]] {
338 continue;
339 }
340 let crossings = (0..n)
351 .filter(|&k| {
352 segment_hits_triangle(
353 coords[i],
354 coords[j],
355 cen,
356 coords[atoms[k]],
357 coords[atoms[(k + 1) % n]],
358 )
359 })
360 .count();
361 if crossings % 2 == 1 {
362 t.pierces += 1;
363 }
364 }
365 }
366 t
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn 线段距离的解析解() {
375 let d = segment_distance(
378 [0.0, 0.0, 0.0],
379 [1.0, 0.0, 0.0],
380 [0.0, 0.0, 1.0],
381 [0.0, 1.0, 1.0],
382 );
383 assert!((d - 1.0).abs() < 1e-12, "{d}");
384 let d = segment_distance(
386 [0.0, 0.0, 0.0],
387 [1.0, 0.0, 0.0],
388 [0.0, 2.0, 0.0],
389 [1.0, 2.0, 0.0],
390 );
391 assert!((d - 2.0).abs() < 1e-12, "平行线段 {d}");
392 let d = segment_distance(
394 [0.0, 0.0, 0.0],
395 [1.0, 0.0, 0.0],
396 [3.0, 0.0, 0.0],
397 [4.0, 0.0, 0.0],
398 );
399 assert!((d - 2.0).abs() < 1e-12, "共线 {d}");
400 let d = segment_distance(
402 [-1.0, 0.0, 0.0],
403 [1.0, 0.0, 0.0],
404 [0.0, -1.0, 0.0],
405 [0.0, 1.0, 0.0],
406 );
407 assert!(d < 1e-12, "相交的两条线段距离应当是 0,实得 {d}");
408 let d = segment_distance([0.0; 3], [0.0; 3], [3.0, 4.0, 0.0], [3.0, 4.0, 0.0]);
410 assert!((d - 5.0).abs() < 1e-12, "两个点 {d}");
411 }
412
413 fn mk(n: usize, bonds: &[(u32, u32)], xyz: &[[f64; 3]]) -> (MolBuilder, Vec<[f64; 3]>) {
415 let mut m = MolBuilder::new();
416 for _ in 0..n {
417 m.add_atom_data(omgkit_core::AtomData::new(6));
418 }
419 for &(i, j) in bonds {
420 m.add_bond(i, j, omgkit_core::BondOrder::Single).unwrap();
421 }
422 (m, xyz.to_vec())
423 }
424
425 #[test]
426 fn 被一根键连起来的两根键不算交叉() {
427 let (m, xyz) = mk(
431 4,
432 &[(0, 1), (1, 2), (2, 3)],
433 &[
434 [-1.0, 0.3, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.3, 0.0], ],
439 );
440 let t = detect(&m, &xyz);
441 let d = segment_distance(xyz[0], xyz[1], xyz[2], xyz[3]);
443 assert!(d < CROSS_TOL, "构型没摆够近({d}),这条测试白测");
444 assert_eq!(t.crossings, 0, "被一根键连起来的两根键不该记成交叉");
445 assert_eq!(t.pairs, 0, "这一对应当连查都不查");
446 }
447
448 #[test]
449 fn 真正够远的两根键照样查得出来() {
450 let (m, xyz) = mk(
454 6,
455 &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)],
456 &[
457 [-1.0, 0.0, 0.0],
458 [1.0, 0.0, 0.0],
459 [3.0, 2.0, 0.0],
460 [3.0, 6.0, 0.0],
461 [0.0, -1.0, 0.2],
462 [0.0, 1.0, 0.2],
463 ],
464 );
465 let t = detect(&m, &xyz);
466 assert!(t.pairs > 0, "一对都没查,那个 0 只说明没在看");
467 assert!(
468 t.crossings >= 1,
469 "0–1 与 4–5 只差 0.2 Å 且拓扑上隔着 3 根键,必须报交叉;实得 {t:?}"
470 );
471 }
472
473 #[test]
474 fn 四面体烷的对棱不算交叉() {
475 let a = 1.508;
478 let s = a / 2.0_f64.sqrt() / 2.0;
479 let (m, xyz) = mk(
480 4,
481 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
482 &[
483 [-a / 2.0, 0.0, -s],
484 [a / 2.0, 0.0, -s],
485 [0.0, -a / 2.0, s],
486 [0.0, a / 2.0, s],
487 ],
488 );
489 let d = segment_distance(xyz[0], xyz[1], xyz[2], xyz[3]);
490 assert!(
491 (d - a / 2.0_f64.sqrt()).abs() < 1e-9,
492 "对棱距离该是 a/√2 = {},实得 {d}",
493 a / 2.0_f64.sqrt()
494 );
495 assert!(d < CROSS_TOL, "对棱距离 {d} 该低于阈值,否则这条测试白测");
496 assert_eq!(detect(&m, &xyz).crossings, 0, "四面体烷的对棱不该记成交叉");
497 }
498
499 #[test]
500 fn 错开的线段不能给出偏大的距离() {
501 let d = segment_distance(
505 [0.0, 0.0, 0.0],
506 [1.0, 0.0, 0.0],
507 [2.0, 0.0, 0.0],
508 [2.0, 0.0, 1.0],
509 );
510 assert!((d - 1.0).abs() < 1e-12, "{d}");
511 }
512
513 #[test]
525 fn 月牙环的凹口不算穿刺() {
526 let (m, xyz) = crescent(0.9);
527 let t = detect(&m, &xyz);
528 assert_eq!(
529 t.pierces, 0,
530 "在凹口里穿过 z 平面不是穿刺(交点 2 次,mod-2 为 0)"
531 );
532 }
533
534 #[test]
538 fn 月牙环的环身照样查得出来() {
539 let (m, xyz) = crescent(3.3);
540 let t = detect(&m, &xyz);
541 assert_eq!(t.pierces, 1, "r = 3.3 落在内外弧之间,是真穿刺");
542 }
543
544 fn crescent(r: f64) -> (MolBuilder, Vec<[f64; 3]>) {
549 const HALF: usize = 9;
550 let ang = |i: usize| (-150.0 + 300.0 * i as f64 / (HALF - 1) as f64).to_radians();
551 let mut xyz: Vec<[f64; 3]> = Vec::new();
552 for i in 0..HALF {
553 xyz.push([4.0 * ang(i).cos(), 4.0 * ang(i).sin(), 0.0]);
554 }
555 for i in (0..HALF).rev() {
556 xyz.push([2.6 * ang(i).cos(), 2.6 * ang(i).sin(), 0.0]);
557 }
558 let ring = 2 * HALF;
559 let probe = (-150.0 + 300.0 * 4.5 / (HALF - 1) as f64).to_radians();
561 xyz.push([xyz[0][0], xyz[0][1], 1.0]); xyz.push([r * probe.cos(), r * probe.sin(), 1.0]);
563 xyz.push([r * probe.cos(), r * probe.sin(), -1.0]);
564 let mut bonds: Vec<(u32, u32)> = (0..ring)
565 .map(|i| (i as u32, ((i + 1) % ring) as u32))
566 .collect();
567 bonds.push((0, ring as u32));
568 bonds.push((ring as u32, ring as u32 + 1));
569 bonds.push((ring as u32 + 1, ring as u32 + 2));
570 mk(ring + 3, &bonds, &xyz)
571 }
572
573 #[test]
574 fn 线段穿三角形() {
575 let (a, b, c) = ([0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0]);
576 assert!(segment_hits_triangle(
578 [0.5, 0.5, 1.0],
579 [0.5, 0.5, -1.0],
580 a,
581 b,
582 c
583 ));
584 assert!(!segment_hits_triangle(
586 [5.0, 5.0, 1.0],
587 [5.0, 5.0, -1.0],
588 a,
589 b,
590 c
591 ));
592 assert!(!segment_hits_triangle(
594 [0.5, 0.5, 1.0],
595 [0.5, 0.5, 0.5],
596 a,
597 b,
598 c
599 ));
600 assert!(!segment_hits_triangle(
602 [0.5, 0.5, 1.0],
603 [1.5, 0.5, 1.0],
604 a,
605 b,
606 c
607 ));
608 }
609}