use std::collections::{BTreeMap, BTreeSet};
use omgkit_chem::sssr::Ring;
use crate::geom::Point2;
const CLASH: f64 = 0.5;
pub(crate) fn place(rings: &[&Ring], ranks: &[u32]) -> Option<BTreeMap<u32, Point2>> {
if rings.is_empty() {
return None;
}
let mut pos: BTreeMap<u32, Point2> = BTreeMap::new();
let mut order: Vec<&Ring> = rings.to_vec();
order.sort_by_key(|r| (std::cmp::Reverse(r.atoms.len()), ring_key(r, ranks)));
let cyc = canonical_cycle(&order[0].atoms, ranks);
let n = cyc.len();
let rad = 1.0 / (2.0 * (std::f64::consts::PI / n as f64).sin());
for (i, a) in cyc.iter().enumerate() {
let t = std::f64::consts::TAU * i as f64 / n as f64;
pos.insert(*a, Point2::new(rad * t.cos(), rad * t.sin()));
}
let mut done: BTreeSet<usize> = BTreeSet::from([0]);
while done.len() < order.len() {
let (i, shared) = (0..order.len())
.filter(|i| !done.contains(i))
.map(|i| {
let sh = order[i]
.atoms
.iter()
.filter(|a| pos.contains_key(a))
.count();
(i, sh)
})
.max_by_key(|(i, sh)| {
(
*sh,
order[*i].atoms.len(),
std::cmp::Reverse(ring_key(order[*i], ranks)),
)
})?;
if shared == 0 {
return None; }
done.insert(i);
place_ring(order[i], ranks, &mut pos)?;
}
if rings
.iter()
.any(|r| r.atoms.iter().any(|a| !pos.contains_key(a)))
{
return None;
}
Some(pos)
}
fn place_ring(ring: &Ring, ranks: &[u32], pos: &mut BTreeMap<u32, Point2>) -> Option<()> {
let seq = canonical_cycle(&ring.atoms, ranks);
let n = seq.len();
if seq.iter().all(|a| pos.contains_key(a)) {
return Some(()); }
let mut j = 0usize;
while j < n {
if pos.contains_key(&seq[j]) {
j += 1;
continue;
}
let mut s = j;
while !pos.contains_key(&seq[(s + n - 1) % n]) {
s = (s + n - 1) % n;
if s == j {
break;
}
}
let mut e = s;
let mut run = vec![seq[s]];
while !pos.contains_key(&seq[(e + 1) % n]) {
e = (e + 1) % n;
run.push(seq[e]);
if e == s {
break;
}
}
let (a, b) = (seq[(s + n - 1) % n], seq[(e + 1) % n]);
let (pa, pb) = (*pos.get(&a)?, *pos.get(&b)?);
let k = run.len();
let d = pa.dist(pb);
let theta = if d < 1e-9 {
std::f64::consts::TAU / (k + 1) as f64
} else {
solve_theta(k, d)?
};
let cands = arc_points(pa, pb, k, theta);
let chosen = pick(&cands, pos, a, b)?;
for (at, p) in run.iter().zip(chosen.iter()) {
pos.insert(*at, *p);
}
j = if e >= s { e + 1 } else { n };
}
Some(())
}
fn pick(
cands: &[Vec<Point2>; 2],
pos: &BTreeMap<u32, Point2>,
a: u32,
b: u32,
) -> Option<Vec<Point2>> {
let nearest = |v: &Vec<Point2>| -> f64 {
let mut mn = f64::INFINITY;
for p in v {
for (at, q) in pos {
if *at == a || *at == b {
continue;
}
mn = mn.min(p.dist(*q));
}
for q in v {
if !std::ptr::eq(p, q) {
mn = mn.min(p.dist(*q));
}
}
}
if mn.is_finite() {
mn
} else {
f64::MAX
}
};
#[allow(clippy::cast_possible_truncation)]
let key = |v: &Vec<Point2>| -> (i64, Vec<(i64, i64)>) {
(
-((nearest(v) * 1e6).round() as i64), v.iter()
.map(|p| ((p.x * 1e6).round() as i64, (p.y * 1e6).round() as i64))
.collect(),
)
};
let (k0, k1) = (key(&cands[0]), key(&cands[1]));
let best = if k0 <= k1 { &cands[0] } else { &cands[1] };
if nearest(best) < CLASH {
return None;
}
Some(best.clone())
}
fn solve_theta(k: usize, d: f64) -> Option<f64> {
let m = (k + 1) as f64;
if d >= m - 1e-12 {
return None;
}
let f = |t: f64| (m * t / 2.0).sin() / (t / 2.0).sin() - d;
let (mut lo, mut hi) = (1e-12, std::f64::consts::TAU / m - 1e-12);
if f(lo) < 0.0 {
return None;
}
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if f(mid) > 0.0 {
lo = mid;
} else {
hi = mid;
}
}
Some(0.5 * (lo + hi))
}
fn arc_points(a: Point2, b: Point2, k: usize, theta: f64) -> [Vec<Point2>; 2] {
let m = (k + 1) as f64;
let r = 1.0 / (2.0 * (theta / 2.0).sin());
let d = a.dist(b);
let along = if d > 1e-12 {
Point2::new((b.x - a.x) / d, (b.y - a.y) / d)
} else {
Point2::new(1.0, 0.0)
};
let normal = Point2::new(-along.y, along.x);
let h = r * (m * theta / 2.0).cos(); let mid = Point2::new(0.5 * (a.x + b.x), 0.5 * (a.y + b.y));
let rot = |v: Point2, ang: f64| {
Point2::new(
v.x * ang.cos() - v.y * ang.sin(),
v.x * ang.sin() + v.y * ang.cos(),
)
};
let mut out = [Vec::new(), Vec::new()];
for (s, side) in [1.0_f64, -1.0].iter().enumerate() {
let c = Point2::new(mid.x + normal.x * h * side, mid.y + normal.y * h * side);
let va = Point2::new(a.x - c.x, a.y - c.y);
let sign = {
let p = rot(va, m * theta);
if Point2::new(c.x + p.x, c.y + p.y).dist(b) < 1e-6 {
1.0
} else {
-1.0
}
};
out[s] = (1..=k)
.map(|j| {
let p = rot(va, sign * theta * j as f64);
Point2::new(c.x + p.x, c.y + p.y)
})
.collect();
}
out
}
fn ring_key(r: &Ring, ranks: &[u32]) -> Vec<u32> {
let mut k: Vec<u32> = r.atoms.iter().map(|a| ranks[*a as usize]).collect();
k.sort_unstable();
k
}
fn canonical_cycle(atoms: &[u32], ranks: &[u32]) -> Vec<u32> {
let n = atoms.len();
let Some(start) = (0..n).min_by_key(|i| (ranks[atoms[*i] as usize], atoms[*i])) else {
return Vec::new();
};
let fwd: Vec<u32> = (0..n).map(|k| atoms[(start + k) % n]).collect();
let bwd: Vec<u32> = (0..n).map(|k| atoms[(start + n - k) % n]).collect();
let key = |v: &[u32]| -> Vec<u32> { v.iter().map(|a| ranks[*a as usize]).collect() };
if key(&bwd) < key(&fwd) {
bwd
} else {
fwd
}
}
#[cfg(test)]
mod tests {
use super::*;
use omgkit_core::MolBuilder;
#[test]
#[ignore]
fn arc_coverage() {
use std::collections::BTreeMap as Map;
let text = std::fs::read_to_string("../../harness/corpus/large.smi").expect("读语料");
let (mut sys_n, mut ok, mut span, mut clash, mut noanchor) = (0, 0, 0, 0, 0);
let (mut in_table, mut miss, mut miss_but_arc, mut miss_and_no_arc) = (0, 0, 0, 0);
let mut miss_arc_skel: BTreeSet<String> = BTreeSet::new();
let mut miss_noarc_skel: BTreeSet<String> = BTreeSet::new();
let (mut arc_x, mut rlx_x, mut arc_win, mut rlx_win, mut tie) = (0, 0, 0, 0, 0);
let (mut arc_dev, mut rlx_dev) = (0.0f64, 0.0f64);
let (mut self_x, mut worst_dev) = (0usize, 0.0f64);
let mut by_skel: Map<String, (usize, usize)> = Map::new();
for line in text.lines() {
let smi = line.split_whitespace().next().unwrap_or("");
if smi.is_empty() || smi.starts_with('#') {
continue;
}
let Ok(mut m) = omgkit_io::smiles::parse(smi) else {
continue;
};
if omgkit_chem::pipeline::sanitize(&mut m).is_err() {
continue;
}
let ranks = crate::ranks_of(&m);
let rings_all = omgkit_chem::sssr::ring_set(&m);
for sys in crate::rings::group(&omgkit_chem::rings::fused_ring_systems(&m), &rings_all)
{
if !is_bridged(&m, &sys, &ranks) {
continue;
}
sys_n += 1;
let skel = crate::templates::skeleton_of(&m, &sys.atoms, &ranks)
.unwrap_or_else(|| "?".into());
let e = by_skel.entry(skel.clone()).or_default();
e.0 += 1;
let hit = matches!(
crate::templates::lookup_with(&m, &sys.atoms, &ranks, None).1,
crate::templates::Status::Hit
);
if hit {
in_table += 1;
} else {
miss += 1;
if place(&sys.rings, &ranks).is_some() {
miss_but_arc += 1;
miss_arc_skel.insert(skel.clone());
} else {
miss_and_no_arc += 1;
miss_noarc_skel.insert(skel.clone());
}
}
match place(&sys.rings, &ranks) {
Some(pos) => {
ok += 1;
e.1 += 1;
let mut dev = 0.0f64;
for bd in m.bonds() {
if let (Some(p), Some(q)) = (pos.get(&bd.begin), pos.get(&bd.end)) {
dev = dev.max((p.dist(*q) - 1.0).abs());
}
}
if dev > worst_dev {
worst_dev = dev;
}
let inside: Vec<(u32, u32)> = m
.bonds()
.iter()
.filter(|bd| pos.contains_key(&bd.begin) && pos.contains_key(&bd.end))
.map(|bd| (bd.begin, bd.end))
.collect();
let mut x = 0usize;
for (i, (a1, b1)) in inside.iter().enumerate() {
for (a2, b2) in &inside[i + 1..] {
if a1 == a2 || a1 == b2 || b1 == a2 || b1 == b2 {
continue;
}
if crate::geom::segments_cross(pos[a1], pos[b1], pos[a2], pos[b2]) {
x += 1;
}
}
}
if x > 0 {
self_x += 1;
arc_x += 1;
}
arc_dev = arc_dev.max(dev);
let (rp, _) = crate::rings::relax(
&m,
&sys.atoms,
&ranks,
&sys.rings,
Some(("", &[])),
);
let mut rdev = 0.0f64;
for bd in m.bonds() {
if let (Some(p), Some(q)) = (rp.get(&bd.begin), rp.get(&bd.end)) {
rdev = rdev.max((p.dist(*q) - 1.0).abs());
}
}
rlx_dev = rlx_dev.max(rdev);
let rin: Vec<(u32, u32)> = m
.bonds()
.iter()
.filter(|bd| rp.contains_key(&bd.begin) && rp.contains_key(&bd.end))
.map(|bd| (bd.begin, bd.end))
.collect();
let mut rx = 0usize;
for (i, (a1, b1)) in rin.iter().enumerate() {
for (a2, b2) in &rin[i + 1..] {
if a1 == a2 || a1 == b2 || b1 == a2 || b1 == b2 {
continue;
}
if crate::geom::segments_cross(rp[a1], rp[b1], rp[a2], rp[b2]) {
rx += 1;
}
}
}
if rx > 0 {
rlx_x += 1;
}
match x.cmp(&rx) {
std::cmp::Ordering::Less => arc_win += 1,
std::cmp::Ordering::Greater => rlx_win += 1,
std::cmp::Ordering::Equal => tie += 1,
}
}
None => {
match why(&sys.rings, &ranks) {
1 => span += 1,
2 => clash += 1,
_ => noanchor += 1,
}
}
}
}
}
println!(
"桥环体系 {sys_n} 个;弧法摆得出来 {ok}({:.1}%)",
100.0 * ok as f64 / sys_n as f64
);
println!(" 摆不出来的分因:弦跨不过去 {span} 原子叠一起(鸽笼){clash} 没锚点 {noanchor}");
println!("成功的那些:内部有自交的 {self_x} 个;最大键长偏差 {worst_dev:.2e}");
let mut v: Vec<_> = by_skel.iter().collect();
v.sort_by_key(|(_, (n, _))| std::cmp::Reverse(*n));
println!("按骨架(出现次数最多的 15 条):");
for (skel, (n, k)) in v.iter().take(15) {
println!(" {n:4} 次 摆出 {k:4} {skel}");
}
let total_skel = by_skel.len();
let full = by_skel.values().filter(|(n, k)| n == k).count();
let none = by_skel.values().filter(|(_, k)| *k == 0).count();
println!("骨架 {total_skel} 条:全摆得出 {full} 条,一条都摆不出 {none} 条");
println!();
println!("=== 与现状比:接进运行时能买到什么 ===");
println!(" 查表命中(现状已经是最优候选,弧法买不到东西) {in_table}");
println!(" 没命中 {miss}");
println!(
" 其中弧法能摆的 {miss_but_arc} 骨架 {:?}",
miss_arc_skel
);
println!(
" 其中弧法也摆不了 {miss_and_no_arc} 骨架 {:?}",
miss_noarc_skel
);
println!();
println!("=== 语料外的新骨架会怎样:把表遮住,弧法 vs relax ===");
println!(" (只看弧法摆得出来的那 {ok} 个体系)");
println!(" 弧法:自交 {arc_x} 个体系,最大键长偏差 {arc_dev:.4}");
println!(" relax:自交 {rlx_x} 个体系,最大键长偏差 {rlx_dev:.4}");
println!(" 逐体系比:弧法交叉更少 {arc_win} 个,relax 更少 {rlx_win} 个,打平 {tie} 个");
}
fn why(rings: &[&Ring], ranks: &[u32]) -> u8 {
let mut pos: BTreeMap<u32, Point2> = BTreeMap::new();
let mut order: Vec<&Ring> = rings.to_vec();
order.sort_by_key(|r| (std::cmp::Reverse(r.atoms.len()), ring_key(r, ranks)));
let cyc = canonical_cycle(&order[0].atoms, ranks);
let n = cyc.len();
let rad = 1.0 / (2.0 * (std::f64::consts::PI / n as f64).sin());
for (i, a) in cyc.iter().enumerate() {
let t = std::f64::consts::TAU * i as f64 / n as f64;
pos.insert(*a, Point2::new(rad * t.cos(), rad * t.sin()));
}
let mut done: BTreeSet<usize> = BTreeSet::from([0]);
while done.len() < order.len() {
let Some((i, shared)) = (0..order.len())
.filter(|i| !done.contains(i))
.map(|i| {
let sh = order[i]
.atoms
.iter()
.filter(|a| pos.contains_key(a))
.count();
(i, sh)
})
.max_by_key(|(i, sh)| {
(
*sh,
order[*i].atoms.len(),
std::cmp::Reverse(ring_key(order[*i], ranks)),
)
})
else {
return 3;
};
if shared == 0 {
return 3;
}
done.insert(i);
let seq = canonical_cycle(&order[i].atoms, ranks);
let m = seq.len();
let mut j = 0usize;
while j < m {
if pos.contains_key(&seq[j]) {
j += 1;
continue;
}
let mut s = j;
while !pos.contains_key(&seq[(s + m - 1) % m]) {
s = (s + m - 1) % m;
if s == j {
break;
}
}
let mut e = s;
let mut run = vec![seq[s]];
while !pos.contains_key(&seq[(e + 1) % m]) {
e = (e + 1) % m;
run.push(seq[e]);
if e == s {
break;
}
}
let (a, b) = (seq[(s + m - 1) % m], seq[(e + 1) % m]);
let (Some(pa), Some(pb)) = (pos.get(&a).copied(), pos.get(&b).copied()) else {
return 3;
};
let k = run.len();
let d = pa.dist(pb);
let theta = if d < 1e-9 {
std::f64::consts::TAU / (k + 1) as f64
} else {
match solve_theta(k, d) {
Some(t) => t,
None => return 1,
}
};
let cands = arc_points(pa, pb, k, theta);
match pick(&cands, &pos, a, b) {
Some(chosen) => {
for (at, p) in run.iter().zip(chosen.iter()) {
pos.insert(*at, *p);
}
}
None => return 2,
}
j = if e >= s { e + 1 } else { m };
}
}
0
}
fn is_bridged(mol: &MolBuilder, sys: &crate::rings::System<'_>, ranks: &[u32]) -> bool {
crate::rings::layout_local(mol, sys, ranks, None)
.1
.is_some()
}
fn prep(smi: &str) -> MolBuilder {
let mut m = omgkit_io::smiles::parse(smi).expect("SMILES 该能解析");
omgkit_chem::pipeline::sanitize(&mut m).expect("该能 sanitize");
m
}
fn lay(smi: &str) -> (MolBuilder, Option<BTreeMap<u32, Point2>>) {
let m = prep(smi);
let ranks = omgkit_io::canon::canonical_ranks(&m);
let rs = omgkit_chem::sssr::ring_set(&m);
let syss = crate::rings::group(&omgkit_chem::rings::fused_ring_systems(&m), &rs);
let Some(sys) = syss.iter().max_by_key(|s| s.atoms.len()) else {
return (m, None);
};
let out = place(&sys.rings, &ranks);
(m, out)
}
#[test]
fn every_bond_it_places_is_exactly_one_bond_long() {
for smi in [
"C1C2CCC1CC2", "C1C2CCC1CCC2",
"C1C2CC1CC2",
"c1ccc2ccccc2c1", "C1C2CCCC1CCC2",
] {
let (m, pos) = lay(smi);
let pos = pos.unwrap_or_else(|| panic!("{smi} 该摆得出来"));
let mut closing = 0usize;
let mut measured = 0usize;
let mut worst = 0.0_f64;
for b in m.bonds() {
let (Some(u), Some(v)) = (pos.get(&b.begin), pos.get(&b.end)) else {
continue;
};
measured += 1;
let d = (u.dist(*v) - 1.0).abs();
if d > 1e-9 {
closing += 1;
worst = worst.max(d);
}
}
assert_eq!(
measured,
m.bonds().len(),
"{smi}:有键的两端没坐标,判据空过了"
);
assert!(
closing * 3 <= m.bonds().len(),
"{smi}:{closing} 根键长度不是 1(共 {} 根),最差差 {worst:.4}",
m.bonds().len()
);
}
}
#[test]
fn it_refuses_instead_of_stacking_atoms_on_top_of_each_other() {
for smi in [
"C1CC2CCC1CC2", "C1C2CC3CC1CC(C2)C3", "C1C2CCCC34C5C(CC4CCCC23)CCCC15", ] {
assert!(lay(smi).1.is_none(), "{smi} 该被拒 —— 这套摆法解不了它");
}
for smi in ["C1C2CCC1CC2", "C1C2CCC1CCC2", "c1ccc2ccccc2c1"] {
let (_, pos) = lay(smi);
let pos = pos.unwrap_or_else(|| panic!("{smi} 该摆得出来"));
let pts: Vec<Point2> = pos.values().copied().collect();
for (i, p) in pts.iter().enumerate() {
for q in &pts[i + 1..] {
assert!(
p.dist(*q) >= CLASH,
"{smi}:两个原子只隔 {:.4},摆法该报 None 而不是发出来",
p.dist(*q)
);
}
}
}
}
#[test]
fn how_it_was_written_does_not_change_where_the_atoms_go() {
for smi in [
"C1CC2CCC1CC2",
"C1CC2CCC1C2",
"C1C2CC3CC1CC(C2)C3",
"CN1CC[C@]23c4c5ccc(O)c4O[C@H]2[C@@H](O)C=C[C@H]3[C@H]1C5",
] {
let m = prep(smi);
let base = fingerprint(&m);
let mut compared = 0usize;
for seed in 0..12u64 {
let w = omgkit_io::smiles::write_with_priority(&m, &shuffled(m.num_atoms(), seed));
let Ok(mut m2) = omgkit_io::smiles::parse(&w.smiles) else {
continue;
};
if omgkit_chem::pipeline::sanitize(&mut m2).is_err() {
continue;
}
if omgkit_io::canon::canonical_smiles(&m2).smiles
!= omgkit_io::canon::canonical_smiles(&m).smiles
{
continue;
}
compared += 1;
assert_eq!(
base,
fingerprint(&m2),
"{smi} 写成 {} 之后摆位变了",
w.smiles
);
}
assert_eq!(compared, 12, "{smi} 只比上了 {compared} 种写法");
}
}
#[test]
fn the_runtime_reaches_for_the_arc_before_it_falls_back_to_relaxing() {
use crate::rings::{group, layout_local};
let mask: crate::templates::Override<'_> = Some(("表外的骨架", &[]));
for smi in [
"C1C2CCC1CC2", "C1C2CCC1CCC2", "C1C2CC1CCC2", ] {
let m = prep(smi);
let ranks = crate::ranks_of(&m);
let rings_all = omgkit_chem::sssr::ring_set(&m);
let systems = group(&omgkit_chem::rings::fused_ring_systems(&m), &rings_all);
let sys = systems
.iter()
.max_by_key(|s| s.rings.len())
.expect("该有一个环系统");
let st = crate::templates::lookup_with(&m, &sys.atoms, &ranks, mask).1;
assert!(
!matches!(st, crate::templates::Status::Hit),
"{smi} 遮了表还命中,遮法失效了"
);
assert!(
matches!(
crate::templates::lookup_with(&m, &sys.atoms, &ranks, None).1,
crate::templates::Status::Hit
),
"{smi} 本来就不在表里,那这条判据里的遮表是空动作"
);
let arc = place(&sys.rings, &ranks).expect("弧法该摆得出这个骨架");
let (got, deg) = layout_local(&m, sys, &ranks, mask);
assert!(deg.is_some(), "{smi} 是桥环,该如实报退化");
for (a, p) in &arc {
let q = got.get(a).expect("原子都该有坐标");
assert!(
(p.x - q.x).abs() < 1e-9 && (p.y - q.y).abs() < 1e-9,
"{smi}:运行时没走弧法 —— 原子 {a} 弧法给 ({:.4},{:.4}),\
实得 ({:.4},{:.4})",
p.x,
p.y,
q.x,
q.y
);
}
for bd in m.bonds() {
if let (Some(p), Some(q)) = (got.get(&bd.begin), got.get(&bd.end)) {
assert!(
(p.dist(*q) - 1.0).abs() < 1e-9,
"{smi}:桥环系统里的键长该精确是 1,实得 {:.6}",
p.dist(*q)
);
}
}
}
}
#[test]
fn the_table_wins_over_the_arc_when_it_has_an_answer() {
use crate::rings::{group, layout_local};
let mut checked = 0usize;
for smi in [
"C1C2CCC1CC2", "C1C2CCC1CCC2", ] {
let m = prep(smi);
let ranks = crate::ranks_of(&m);
let rings_all = omgkit_chem::sssr::ring_set(&m);
let systems = group(&omgkit_chem::rings::fused_ring_systems(&m), &rings_all);
let sys = systems
.iter()
.max_by_key(|s| s.rings.len())
.expect("该有一个环系统");
assert!(
matches!(
crate::templates::lookup_with(&m, &sys.atoms, &ranks, None).1,
crate::templates::Status::Hit
),
"{smi} 不在表里,这条判据说明不了次序"
);
let arc = place(&sys.rings, &ranks).expect("弧法该摆得出这个骨架");
let (got, _) = layout_local(&m, sys, &ranks, None);
let (tbl, _) = crate::templates::lookup_with(&m, &sys.atoms, &ranks, None);
let tbl = tbl.expect("命中就该有坐标");
for (a, p) in &tbl {
let q = got.get(a).expect("原子都该有坐标");
assert!(
(p.x - q.x).abs() < 1e-9 && (p.y - q.y).abs() < 1e-9,
"{smi}:运行时没走查表 —— 原子 {a} 表里是 ({:.4},{:.4}),\
实得 ({:.4},{:.4})",
p.x,
p.y,
q.x,
q.y
);
}
if tbl.iter().any(|(a, p)| {
arc.get(a)
.is_some_and(|q| (p.x - q.x).abs() > 1e-6 || (p.y - q.y).abs() > 1e-6)
}) {
checked += 1;
}
}
assert!(
checked > 0,
"没有一个骨架能分出「表」与「弧法」,这条判据是空过的"
);
}
#[test]
fn when_the_arc_cannot_do_it_the_runtime_falls_back_instead_of_giving_up() {
use crate::rings::{group, layout_local};
let mask: crate::templates::Override<'_> = Some(("表外的骨架", &[]));
let mut checked = 0usize;
for smi in [
"C1C2CC3CC1CC(C2)C3", "C1CC2CCC1CC2", ] {
let Ok(mut m) = omgkit_io::smiles::parse(smi) else {
continue;
};
if omgkit_chem::pipeline::sanitize(&mut m).is_err() {
continue;
}
let ranks = crate::ranks_of(&m);
let rings_all = omgkit_chem::sssr::ring_set(&m);
let systems = group(&omgkit_chem::rings::fused_ring_systems(&m), &rings_all);
let Some(sys) = systems.iter().max_by_key(|s| s.rings.len()) else {
continue;
};
assert!(
!matches!(
crate::templates::lookup_with(&m, &sys.atoms, &ranks, mask).1,
crate::templates::Status::Hit
),
"{smi} 遮了表还命中,遮法失效了"
);
assert!(
place(&sys.rings, &ranks).is_none(),
"{smi} 弧法摆得出来,这条判据验不了退回那一支"
);
checked += 1;
let (got, deg) = layout_local(&m, sys, &ranks, mask);
assert!(deg.is_some(), "{smi} 是桥环,该如实报退化");
for a in &sys.atoms {
assert!(
got.contains_key(a),
"{smi}:弧法摆不了就该退回松弛,不能让原子 {a} 没坐标"
);
}
}
assert!(checked >= 2, "只验到 {checked} 个鸽笼骨架,这条判据太弱");
}
fn fingerprint(m: &MolBuilder) -> Option<Vec<(i64, i64)>> {
let ranks = omgkit_io::canon::canonical_ranks(m);
let rs = omgkit_chem::sssr::ring_set(m);
let syss = crate::rings::group(&omgkit_chem::rings::fused_ring_systems(m), &rs);
let sys = syss.iter().max_by_key(|s| s.atoms.len())?;
let pos = place(&sys.rings, &ranks)?;
let mut v: Vec<(u32, Point2)> = pos.iter().map(|(a, p)| (ranks[*a as usize], *p)).collect();
v.sort_by_key(|x| x.0);
#[allow(clippy::cast_possible_truncation)]
Some(
v.iter()
.map(|(_, p)| ((p.x * 1e6).round() as i64, (p.y * 1e6).round() as i64))
.collect(),
)
}
fn shuffled(n: usize, seed: u64) -> Vec<u32> {
let mut s = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut next = || {
s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
};
let mut v: Vec<u32> = (0..u32::try_from(n).unwrap()).collect();
for i in (1..n).rev() {
let j = usize::try_from(next() % (i as u64 + 1)).unwrap();
v.swap(i, j);
}
v
}
}