use std::collections::BTreeMap;
use omgkit_core::{BondOrder, MolBuilder};
use crate::geom::Point2;
pub(crate) fn skeleton_of(mol: &MolBuilder, atoms: &[u32], ranks: &[u32]) -> Option<String> {
let mut order: Vec<u32> = atoms.to_vec();
order.sort_by_key(|a| (ranks[*a as usize], *a));
let mut b = MolBuilder::new();
let mut map: BTreeMap<u32, u32> = BTreeMap::new();
for a in &order {
map.insert(*a, b.add_atom(6));
}
for bd in mol.bonds() {
if let (Some(x), Some(y)) = (map.get(&bd.begin), map.get(&bd.end)) {
b.add_bond(*x, *y, BondOrder::Single).ok()?;
}
}
omgkit_chem::pipeline::sanitize(&mut b).ok()?;
Some(omgkit_io::canon::canonical_smiles(&b).smiles)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
Hit,
NotInTable,
NoFingerprint,
}
pub(crate) type Override<'a> = Option<(&'a str, &'a [(f64, f64)])>;
pub(crate) fn lookup_with(
mol: &MolBuilder,
atoms: &[u32],
ranks: &[u32],
over: Override<'_>,
) -> (Option<BTreeMap<u32, Point2>>, Status) {
let Some(skel) = skeleton_of(mol, atoms, ranks) else {
return (None, Status::NoFingerprint);
};
let coords = match over {
Some((k, v)) if k == skel => v,
Some(_) => return (None, Status::NotInTable),
None => match TABLE.iter().find(|(k, _)| *k == skel).map(|(_, v)| *v) {
Some(v) => v,
None => return (None, Status::NotInTable),
},
};
let mut order: Vec<u32> = atoms.to_vec();
order.sort_by_key(|a| (ranks[*a as usize], *a));
let mut b = MolBuilder::new();
let mut back: Vec<u32> = Vec::with_capacity(order.len());
for a in &order {
b.add_atom(6);
back.push(*a);
}
let idx: BTreeMap<u32, u32> = order
.iter()
.enumerate()
.map(|(i, a)| (*a, u32::try_from(i).unwrap_or(0)))
.collect();
for bd in mol.bonds() {
if let (Some(x), Some(y)) = (idx.get(&bd.begin), idx.get(&bd.end)) {
if b.add_bond(*x, *y, BondOrder::Single).is_err() {
return (None, Status::NotInTable);
}
}
}
if omgkit_chem::pipeline::sanitize(&mut b).is_err() {
return (None, Status::NotInTable);
}
let skel_ranks = omgkit_io::canon::canonical_ranks(&b);
if skel_ranks.len() != coords.len() {
return (None, Status::NotInTable); }
let mut out = BTreeMap::new();
for (i, parent) in back.iter().enumerate() {
let Some(r) = skel_ranks.get(i) else {
return (None, Status::NotInTable);
};
let Some((x, y)) = coords.get(*r as usize) else {
return (None, Status::NotInTable);
};
out.insert(*parent, Point2::new(*x, *y));
}
(Some(out), Status::Hit)
}
pub fn lookup(
mol: &MolBuilder,
atoms: &[u32],
ranks: &[u32],
) -> (Option<BTreeMap<u32, Point2>>, Status) {
lookup_with(mol, atoms, ranks, None)
}
include!("templates_data.rs");
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Style;
fn prep(smi: &str) -> MolBuilder {
let mut m = omgkit_io::smiles::parse(smi).unwrap();
omgkit_chem::pipeline::sanitize(&mut m).unwrap();
m
}
const HIT: [&str; 4] = [
"C1CC2CCC1CC2", "C1C2CC3CC1CC(C2)C3", "CC1(C)[C@@H]2CC[C@@]1(C)C(=O)C2", "CN1[C@H]2CC[C@@H]1C[C@@H](C2)OC(=O)C(CO)c1ccccc1", ];
#[test]
fn without_an_override_the_table_is_what_comes_back() {
let mut checked = 0usize;
for (skel, coords) in TABLE {
let mut m = omgkit_io::smiles::parse(skel).expect("表里的骨架该能解析");
omgkit_chem::pipeline::sanitize(&mut m).expect("该能 sanitize");
let ranks = omgkit_io::canon::canonical_ranks(&m);
let atoms: Vec<u32> = (0..u32::try_from(m.num_atoms()).unwrap()).collect();
let (got, st) = lookup_with(&m, &atoms, &ranks, None);
let Some(got) = got else { continue };
assert_eq!(st, Status::Hit);
for (a, p) in &got {
let (x, y) = coords[ranks[*a as usize] as usize];
assert!(
p.x.to_bits() == x.to_bits() && p.y.to_bits() == y.to_bits(),
"{skel} 的原子 {a}:取回 {p:?},表里是 ({x}, {y})"
);
}
checked += 1;
}
assert!(checked >= 40, "只验到 {checked} 条,判据太弱");
}
#[test]
fn an_override_really_replaces_that_one_row() {
let skel = "C1C2CCC1CC2";
let mut m = omgkit_io::smiles::parse(skel).expect("该能解析");
omgkit_chem::pipeline::sanitize(&mut m).expect("该能 sanitize");
let ranks = omgkit_io::canon::canonical_ranks(&m);
let atoms: Vec<u32> = (0..u32::try_from(m.num_atoms()).unwrap()).collect();
let n = m.num_atoms();
#[allow(clippy::cast_precision_loss)]
let fake: Vec<(f64, f64)> = (0..n).map(|i| (i as f64, -(i as f64))).collect();
let (got, st) = lookup_with(&m, &atoms, &ranks, Some((skel, &fake)));
assert_eq!(st, Status::Hit);
let got = got.expect("装了覆盖就该拿得到");
for (a, p) in &got {
let (x, y) = fake[ranks[*a as usize] as usize];
assert!(
(p.x - x).abs() < 1e-12 && (p.y - y).abs() < 1e-12,
"{skel} 的原子 {a}:拿回 {p:?},覆盖里是 ({x}, {y})"
);
}
let (other, st2) = lookup_with(&m, &atoms, &ranks, Some(("C1CC2CCC1CC2", &fake)));
assert!(other.is_none(), "覆盖了别的骨架,这一条不该还能读到表");
assert_eq!(st2, Status::NotInTable);
let (plain, st3) = lookup_with(&m, &atoms, &ranks, None);
assert!(
plain.is_some() && st3 == Status::Hit,
"不给覆盖时该读得到表"
);
}
#[test]
fn the_stored_coordinates_do_not_cross_more_than_they_used_to() {
let mut total = 0usize;
let mut worst: Vec<(usize, &str)> = Vec::new();
for (skel, coords) in TABLE {
let mut m = omgkit_io::smiles::parse(skel).expect("表里的骨架该能解析");
omgkit_chem::pipeline::sanitize(&mut m).expect("表里的骨架该能 sanitize");
let ranks = omgkit_io::canon::canonical_ranks(&m);
assert_eq!(
ranks.len(),
coords.len(),
"{skel}:表里 {} 组坐标,骨架 {} 个原子",
coords.len(),
ranks.len()
);
let pos: Vec<Point2> = (0..m.num_atoms())
.map(|i| {
let (x, y) = coords[ranks[i] as usize];
Point2::new(x, y)
})
.collect();
let segs: Vec<(Point2, Point2)> = m
.bonds()
.iter()
.map(|b| (pos[b.begin as usize], pos[b.end as usize]))
.collect();
let mut cross = 0usize;
for (k, (u1, v1)) in segs.iter().enumerate() {
for (u2, v2) in &segs[k + 1..] {
if crate::geom::segments_cross(*u1, *v1, *u2, *v2) {
cross += 1;
}
}
}
if cross > 0 {
worst.push((cross, skel));
}
total += cross;
}
worst.sort_unstable();
assert!(
total <= 9,
"表里的自交总数涨到了 {total},还剩 {} 条自交:{worst:?}",
worst.len()
);
}
#[test]
fn the_table_is_actually_used() {
let mut hits = 0usize;
for smi in HIT {
let m = prep(smi);
let ranks = omgkit_io::canon::canonical_ranks(&m);
let rs = omgkit_chem::sssr::ring_set(&m);
for sys in crate::rings::group(&omgkit_chem::rings::fused_ring_systems(&m), &rs) {
if lookup(&m, &sys.atoms, &ranks).0.is_some() {
hits += 1;
}
}
}
assert!(hits > 0, "这批桥环分子一个都没命中模板 —— 表白建了");
}
#[test]
fn a_template_hit_does_not_depend_on_how_the_molecule_was_written() {
let mut checked = 0usize;
for smi in HIT {
for style in &Style::ALL {
let m = prep(smi);
let n = m.num_atoms();
let want = crate::generate(&m, style);
let priority: Vec<u32> = (0..n)
.map(|i| u32::try_from(n - 1 - i).expect("原子数超出 u32"))
.collect();
let w = omgkit_io::smiles::write_with_priority(&m, &priority);
let Some(m2) = omgkit_io::smiles::parse(&w.smiles)
.ok()
.and_then(|mut x| omgkit_chem::pipeline::sanitize(&mut x).ok().map(|()| x))
else {
continue;
};
if omgkit_io::canon::canonical_smiles(&m).smiles
!= omgkit_io::canon::canonical_smiles(&m2).smiles
{
continue;
}
checked += 1;
let got = crate::generate(&m2, style);
let q = |c: &[Point2]| {
let mut v: Vec<(i64, i64)> = c
.iter()
.map(|p| ((p.x * 1e4).round() as i64, (p.y * 1e4).round() as i64))
.collect();
v.sort_unstable();
v
};
assert_eq!(
q(&want.coords),
q(&got.coords),
"[{}] {smi}:换成 {} 之后画出来不一样了",
style.name,
w.smiles
);
}
}
assert!(checked > 0, "一次都没比成,判据空过了");
}
#[test]
fn a_templated_system_still_reports_itself_degraded() {
for smi in HIT {
for style in &Style::ALL {
let m = prep(smi);
let d = crate::generate(&m, style);
assert!(
!d.degraded.is_empty(),
"[{}] {smi}:桥环命中模板之后就不报退化了",
style.name
);
}
}
}
}