use std::collections::{BTreeMap, BTreeSet, VecDeque};
use omgkit_chem::{rings::fused_ring_systems, sssr::ring_set};
use omgkit_core::MolBuilder;
use crate::chains::place_neighbours;
use crate::geom::Point2;
use crate::rings::{self, Degradation};
use crate::style::Style;
pub(crate) struct Piece {
pub pos: BTreeMap<u32, Point2>,
pub degraded: Vec<Degradation>,
}
pub(crate) fn layout_all(
mol: &MolBuilder,
ranks: &[u32],
style: &Style,
over: crate::templates::Override<'_>,
) -> Vec<Piece> {
let comps = components(mol);
let rings_all = ring_set(mol);
let mut systems_all = rings::group(&fused_ring_systems(mol), &rings_all);
systems_all.sort_by_key(|s| {
let mut k: Vec<u32> = s.atoms.iter().map(|a| ranks[*a as usize]).collect();
k.sort_unstable();
k
});
comps
.into_iter()
.map(|atoms| layout_component(mol, &atoms, &systems_all, ranks, style, over))
.collect()
}
#[cfg(test)]
pub(crate) fn system_order(mol: &MolBuilder, ranks: &[u32]) -> Vec<Vec<u32>> {
let rings_all = ring_set(mol);
let mut systems_all = rings::group(&fused_ring_systems(mol), &rings_all);
systems_all.sort_by_key(|s| {
let mut k: Vec<u32> = s.atoms.iter().map(|a| ranks[*a as usize]).collect();
k.sort_unstable();
k
});
systems_all
.iter()
.map(|s| {
let mut k: Vec<u32> = s.atoms.iter().map(|a| ranks[*a as usize]).collect();
k.sort_unstable();
k
})
.collect()
}
fn components(mol: &MolBuilder) -> Vec<Vec<u32>> {
let n = u32::try_from(mol.num_atoms()).expect("原子数超出 u32");
let mut seen = vec![false; n as usize];
let mut out = Vec::new();
for start in 0..n {
if seen[start as usize] {
continue;
}
let mut stack: Vec<u32> = vec![start];
let mut comp: Vec<u32> = Vec::new();
seen[start as usize] = true;
while let Some(a) = stack.pop() {
comp.push(a);
for (b, _) in mol.neighbors(a) {
if !seen[b as usize] {
seen[b as usize] = true;
stack.push(b);
}
}
}
comp.sort_unstable();
out.push(comp);
}
out
}
fn note_degraded(
deg: Option<Degradation>,
atoms: impl IntoIterator<Item = u32>,
degraded: &mut Vec<Degradation>,
off_grid: &mut BTreeSet<u32>,
) {
if let Some(d) = deg {
degraded.push(d);
off_grid.extend(atoms);
}
}
fn layout_component(
mol: &MolBuilder,
atoms: &[u32],
systems: &[rings::System<'_>],
ranks: &[u32],
style: &Style,
over: crate::templates::Override<'_>,
) -> Piece {
let here: BTreeSet<u32> = atoms.iter().copied().collect();
let mine: Vec<usize> = (0..systems.len())
.filter(|i| systems[*i].atoms.first().is_some_and(|a| here.contains(a)))
.collect();
let mut of_atom: BTreeMap<u32, Vec<usize>> = BTreeMap::new();
for &i in &mine {
for &a in &systems[i].atoms {
of_atom.entry(a).or_default().push(i);
}
}
let mut pos: BTreeMap<u32, Point2> = BTreeMap::new();
let mut degraded: Vec<Degradation> = Vec::new();
let mut off_grid: BTreeSet<u32> = BTreeSet::new();
let mut done_sys: BTreeSet<usize> = BTreeSet::new();
let radii = crate::refine::radii(mol, style);
let bonded: BTreeSet<(u32, u32)> = mol
.bonds()
.iter()
.map(|b| (b.begin.min(b.end), b.begin.max(b.end)))
.collect();
let seed_atom = if let Some(&i) = mine.iter().max_by_key(|&&i| {
(
systems[i].atoms.len(),
std::cmp::Reverse(sys_key(&systems[i], ranks)),
)
}) {
let (local, deg) = rings::layout_local(mol, &systems[i], ranks, over);
note_degraded(deg, local.keys().copied(), &mut degraded, &mut off_grid);
pos.extend(local);
done_sys.insert(i);
*systems[i]
.atoms
.iter()
.min_by_key(|a| ranks[**a as usize])
.expect("环系统非空")
} else {
let a = *atoms
.iter()
.min_by_key(|a| ranks[**a as usize])
.expect("分量非空");
pos.insert(a, Point2::ORIGIN);
a
};
let mut zig: BTreeMap<u32, i8> = BTreeMap::new();
zig.insert(seed_atom, 1);
let mut queue: VecDeque<u32> = VecDeque::new();
let mut seeds: Vec<u32> = pos.keys().copied().collect();
seeds.sort_by_key(|a| (ranks[*a as usize], *a));
queue.extend(seeds);
while let Some(a) = queue.pop_front() {
let z = zig.get(&a).copied().unwrap_or(1);
let mut todo: Vec<u32> = mol
.neighbors(a)
.map(|(b, _)| b)
.filter(|b| !pos.contains_key(b))
.collect();
todo.sort_by_key(|b| (ranks[*b as usize], *b));
todo.dedup();
if todo.is_empty() {
continue;
}
for &s in of_atom.get(&a).into_iter().flatten() {
if done_sys.contains(&s) {
continue;
}
let (local, deg) = rings::layout_local(mol, &systems[s], ranks, over);
note_degraded(deg, local.keys().copied(), &mut degraded, &mut off_grid);
let mut placed: Vec<(u32, Point2)> =
pos.iter().map(|(k, v)| (ranks[*k as usize], *v)).collect();
placed.sort_unstable_by_key(|x| x.0);
let away = away_from(pos[&a], placed.iter().map(|x| x.1));
let dirs = ring_of_dirs(away);
let around = Around {
pos: &pos,
radii: &radii,
bonded: &bonded,
};
let put = place_clear(mol, &local, a, pos[&a], &dirs, &around);
for (k, p) in put {
pos.entry(k).or_insert(p);
}
done_sys.insert(s);
let mut fresh: Vec<u32> = systems[s].atoms.clone();
fresh.sort_by_key(|x| (ranks[*x as usize], *x));
for f in fresh {
zig.entry(f).or_insert(-z);
queue.push_back(f);
}
}
let mut todo: Vec<u32> = mol
.neighbors(a)
.map(|(b, _)| b)
.filter(|b| !pos.contains_key(b))
.collect();
todo.sort_by_key(|b| (ranks[*b as usize], *b));
todo.dedup();
type Plan = (usize, crate::chains::Block, Option<Degradation>);
let mut plans: BTreeMap<u32, Plan> = BTreeMap::new();
let mut planned: BTreeSet<usize> = BTreeSet::new();
for &b in &todo {
let Some(&s) = of_atom
.get(&b)
.into_iter()
.flatten()
.find(|s| !done_sys.contains(s) && !planned.contains(s))
else {
continue;
};
let (local, deg) = rings::layout_local(mol, &systems[s], ranks, over);
planned.insert(s);
plans.insert(b, (s, local, deg));
}
let blocks: BTreeMap<u32, crate::chains::Block> = plans
.iter()
.map(|(b, (_, local, _))| (*b, local.clone()))
.collect();
let env = crate::chains::Env {
mol,
ranks,
style,
radii: &radii,
bonded: &bonded,
off_grid: &off_grid,
blocks: &blocks,
};
for p in place_neighbours(&env, a, &pos, &todo, z) {
if pos.contains_key(&p.atom) {
continue;
}
pos.insert(p.atom, p.at);
zig.insert(p.atom, p.zig);
if let (Some(put), Some((s, _, deg))) = (p.block, plans.remove(&p.atom)) {
note_degraded(deg, put.keys().copied(), &mut degraded, &mut off_grid);
for (k, q) in put {
pos.entry(k).or_insert(q);
}
done_sys.insert(s);
let mut fresh: Vec<u32> = systems[s].atoms.clone();
fresh.sort_by_key(|x| (ranks[*x as usize], *x));
for f in fresh {
zig.entry(f).or_insert(p.zig);
queue.push_back(f);
}
}
let sys_here: Vec<usize> = of_atom
.get(&p.atom)
.into_iter()
.flatten()
.copied()
.filter(|s| !done_sys.contains(s))
.collect();
for s in sys_here {
let (local, deg) = rings::layout_local(mol, &systems[s], ranks, over);
note_degraded(deg, local.keys().copied(), &mut degraded, &mut off_grid);
let dir = (p.at - pos[&a]).normalized();
let around = Around {
pos: &pos,
radii: &radii,
bonded: &bonded,
};
let put = place_clear(mol, &local, p.atom, p.at, &[dir], &around);
for (k, q) in put {
pos.entry(k).or_insert(q);
}
done_sys.insert(s);
let mut fresh: Vec<u32> = systems[s].atoms.clone();
fresh.sort_by_key(|x| (ranks[*x as usize], *x));
for f in fresh {
zig.entry(f).or_insert(p.zig);
queue.push_back(f);
}
}
queue.push_back(p.atom);
}
}
debug_assert_eq!(pos.len(), atoms.len(), "有原子没被放上,BFS 漏了分支");
Piece { pos, degraded }
}
fn sys_key(s: &rings::System<'_>, ranks: &[u32]) -> Vec<u32> {
let mut k: Vec<u32> = s.atoms.iter().map(|a| ranks[*a as usize]).collect();
k.sort_unstable();
k
}
fn away_from(from: Point2, placed: impl Iterator<Item = Point2>) -> Point2 {
let mut sum = Point2::ORIGIN;
let mut n = 0.0;
for p in placed {
sum = sum + (p - from);
n += 1.0;
}
if n == 0.0 {
return Point2::new(1.0, 0.0);
}
let mean = sum * (1.0 / n);
if mean.norm() < 1e-9 {
Point2::new(1.0, 0.0)
} else {
(mean * -1.0).normalized()
}
}
fn ring_of_dirs(first: Point2) -> Vec<Point2> {
let step = std::f64::consts::FRAC_PI_6;
let mut out = vec![first];
for k in 1..=6i32 {
for sign in [1.0, -1.0] {
if k == 6 && sign < 0.0 {
continue; }
out.push(first.rotated(step * f64::from(k) * sign));
}
}
out
}
fn place_clear(
mol: &MolBuilder,
local: &BTreeMap<u32, Point2>,
anchor: u32,
at: Point2,
dirs: &[Point2],
around: &Around<'_>,
) -> BTreeMap<u32, Point2> {
const EPS: f64 = 1e-9;
let drawn: Vec<(Point2, Point2)> = mol
.bonds()
.iter()
.filter_map(|b| Some((*around.pos.get(&b.begin)?, *around.pos.get(&b.end)?)))
.collect();
let mut best: Option<((f64, usize), BTreeMap<u32, Point2>)> = None;
for d in dirs {
for cand in rings::place_candidates(mol, local, anchor, at, *d) {
let s = (
clash(&cand, around),
new_crossings(mol, &cand, around.pos, &drawn),
);
let better = match &best {
None => true,
Some((old, _)) => {
if s.0 < old.0 - EPS {
true
} else if s.0 > old.0 + EPS {
false
} else {
s.1 < old.1
}
}
};
if better {
let done = s.0 == 0.0 && s.1 == 0;
best = Some((s, cand));
if done {
return best.expect("刚放进去").1; }
}
}
}
best.expect("`dirs` 非空,候选至少有两个").1
}
struct Around<'a> {
pos: &'a BTreeMap<u32, Point2>,
radii: &'a [f64],
bonded: &'a BTreeSet<(u32, u32)>,
}
fn clash(cand: &BTreeMap<u32, Point2>, around: &Around<'_>) -> f64 {
let (pos, radii, bonded) = (around.pos, around.radii, around.bonded);
let mut parts: Vec<f64> = Vec::new();
for (i, p) in cand {
if pos.contains_key(i) {
continue;
}
for (j, q) in pos {
if bonded.contains(&((*i).min(*j), (*i).max(*j))) {
continue;
}
let want = radii[*i as usize] + radii[*j as usize];
let d = p.dist(*q);
if d < want {
parts.push((want - d).powi(2));
}
}
}
parts.sort_by(f64::total_cmp);
parts.iter().sum()
}
fn new_crossings(
mol: &MolBuilder,
cand: &BTreeMap<u32, Point2>,
pos: &BTreeMap<u32, Point2>,
drawn: &[(Point2, Point2)],
) -> usize {
mol.bonds()
.iter()
.filter(|b| !(pos.contains_key(&b.begin) && pos.contains_key(&b.end)))
.filter_map(|b| Some((*cand.get(&b.begin)?, *cand.get(&b.end)?)))
.map(|(u, v)| {
drawn
.iter()
.filter(|(x, y)| crate::geom::segments_cross(u, v, *x, *y))
.count()
})
.sum()
}
#[cfg(test)]
mod tests {
#[test]
fn three_chelate_rings_on_one_metal_do_not_land_on_top_of_each_other() {
use omgkit_chem::rings::fused_ring_systems;
let smi = "C1CN[Co]23(N1)(NCCN2)NCCN3";
let mut m = omgkit_io::smiles::parse(smi).expect("SMILES 该能解析");
omgkit_chem::pipeline::sanitize(&mut m).expect("该能 sanitize");
let systems = fused_ring_systems(&m);
let shared = (0..u32::try_from(m.num_atoms()).expect("原子数超出 u32"))
.map(|a| systems.iter().filter(|s| s.contains(&a)).count())
.max()
.unwrap_or(0);
assert!(
systems.len() >= 3 && shared >= 3,
"{smi} 只有 {} 个环系、最多共用 {shared} 个 —— 走不到「以锚点自己为锚」那一路",
systems.len()
);
for style in &crate::style::Style::ALL {
let d = crate::generate(&m, style);
for i in 0..d.coords.len() {
for j in (i + 1)..d.coords.len() {
let dist = d.coords[i].dist(d.coords[j]);
assert!(
dist >= 0.05,
"{}:原子 {i} 与 {j} 相距 {dist:.4} 个键长",
style.name
);
}
}
}
}
#[test]
fn the_ring_systems_are_consumed_in_canonical_order() {
for smi in [
"C[N+]12CCCC1C3=CC=C[N+](=C3)[Ni++]245([N+]6=CC(=CC=C6)C7CCC[N+]47C)\
[N+]8=CC(=CC=C8)C9CCC[N+]59C.SC#N",
"c1ccc2ccccc2c1.c1ccccc1", "C1CC1c1ccccc1C2CC2", ] {
let smi: String = smi.split_whitespace().collect();
let mut m = omgkit_io::smiles::parse(&smi).expect("SMILES 该能解析");
omgkit_chem::pipeline::sanitize(&mut m).expect("该能 sanitize");
let ranks = omgkit_io::canon::canonical_ranks(&m);
let base = system_order(&m, &ranks);
assert!(
base.len() >= 2,
"{smi} 只有 {} 个环系,验不出顺序",
base.len()
);
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;
}
let r2 = omgkit_io::canon::canonical_ranks(&m2);
compared += 1;
assert_eq!(
base,
system_order(&m2, &r2),
"{smi} 写成 {} 之后环系统的顺序变了",
w.smiles
);
}
assert!(compared >= 6, "{smi} 只比上了 {compared} 种写法");
}
}
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
}
use super::*;
fn prep(smi: &str) -> MolBuilder {
let mut m = omgkit_io::smiles::parse(smi).unwrap();
omgkit_chem::pipeline::sanitize(&mut m).unwrap();
m
}
fn run(smi: &str) -> (MolBuilder, Vec<Piece>) {
let m = prep(smi);
let ranks = omgkit_io::canon::canonical_ranks(&m);
let ps = layout_all(&m, &ranks, &Style::ACS_1996, None);
(m, ps)
}
#[test]
fn every_atom_gets_a_finite_coordinate() {
for smi in [
"CCO",
"c1ccccc1",
"c1ccc2ccccc2c1",
"CC(C)(C)c1ccccc1",
"C1CC2(CC1)CCCC2",
"c1ccc(-c2ccccc2)cc1",
"CC(=O)Oc1ccccc1C(=O)O",
"C1CC2CCC1CC2",
"[Na+].[Cl-]",
"O",
"C",
] {
let (m, ps) = run(smi);
let total: usize = ps.iter().map(|p| p.pos.len()).sum();
assert_eq!(total, m.num_atoms(), "{smi} 有原子没放上");
for p in &ps {
for (a, q) in &p.pos {
assert!(
q.x.is_finite() && q.y.is_finite(),
"{smi} 原子 {a} 坐标非有限"
);
}
}
}
}
#[test]
fn disconnected_pieces_come_out_separately() {
let (_, ps) = run("[Na+].[Cl-]");
assert_eq!(ps.len(), 2, "盐应当分成两个分量");
let (_, one) = run("CCO");
assert_eq!(one.len(), 1);
}
#[test]
fn a_spiro_atom_is_one_point_not_two() {
let (m, ps) = run("C1CC2(CC1)CCCC2");
assert_eq!(ps.len(), 1);
let pos = &ps[0].pos;
assert_eq!(pos.len(), m.num_atoms());
let spiro = (0..u32::try_from(m.num_atoms()).unwrap())
.find(|a| m.degree(*a) == 4)
.expect("螺[4.4]壬烷有一个四度碳");
for (b, _) in m.neighbors(spiro) {
let d = pos[&spiro].dist(pos[&b]);
assert!(
(d - 1.0).abs() < 1e-6,
"螺原子到邻居 {b} 的距离是 {d},应当是 1"
);
}
let pts: Vec<Point2> = pos.values().copied().collect();
for i in 0..pts.len() {
for j in (i + 1)..pts.len() {
assert!(pts[i].dist(pts[j]) > 0.4, "有两个原子几乎重合");
}
}
}
#[test]
fn bonded_atoms_stay_one_unit_apart() {
for smi in [
"CCO",
"c1ccccc1",
"CC(C)(C)c1ccccc1",
"CC(=O)Oc1ccccc1C(=O)O",
] {
let (m, ps) = run(smi);
let pos: BTreeMap<u32, Point2> = ps
.iter()
.flat_map(|p| p.pos.iter().map(|(k, v)| (*k, *v)))
.collect();
for b in m.bonds() {
let d = pos[&b.begin].dist(pos[&b.end]);
assert!(
(d - 1.0).abs() < 1e-6,
"{smi} 键 {}–{} 长 {d}",
b.begin,
b.end
);
}
}
}
}