use omgkit_core::{AtomFlags, BondFlags, BondOrder, MolBuilder};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RingPerception {
pub atom_min_ring_size: Vec<u8>,
pub atom_in_ring: Vec<bool>,
pub bond_in_ring: Vec<bool>,
}
pub const MAX_RING_SIZE: usize = 255;
const COMMON_RING_MAX: usize = 20;
#[must_use]
pub fn perceive_rings(mol: &mut MolBuilder) -> RingPerception {
let n_atoms = mol.num_atoms();
let n_bonds = mol.num_bonds();
let active: Vec<bool> = mol
.bonds()
.iter()
.map(|b| b.order != BondOrder::Dative)
.collect();
let adj = Adjacency::build(mol, &active);
let is_bridge = find_bridges(&adj);
let mut bond_in_ring = vec![false; n_bonds];
for (bi, in_ring) in bond_in_ring.iter_mut().enumerate() {
*in_ring = active[bi] && !is_bridge[bi];
}
let mut atom_in_ring = vec![false; n_atoms];
for (bi, &in_ring) in bond_in_ring.iter().enumerate() {
if in_ring {
let b = mol.bonds()[bi];
atom_in_ring[b.begin as usize] = true;
atom_in_ring[b.end as usize] = true;
}
}
let mut atom_min_ring_size = vec![0u8; n_atoms];
let mut dist = vec![u32::MAX; n_atoms];
let mut queue: Vec<u32> = Vec::new();
for a in 0..n_atoms {
if !atom_in_ring[a] {
continue;
}
let small = shortest_cycle_through(
&adj,
a as u32,
&bond_in_ring,
COMMON_RING_MAX,
&mut dist,
&mut queue,
);
atom_min_ring_size[a] = if small != 0 {
small
} else {
shortest_cycle_through(
&adj,
a as u32,
&bond_in_ring,
MAX_RING_SIZE,
&mut dist,
&mut queue,
)
};
}
debug_assert!(
dist.iter().all(|&d| d == u32::MAX),
"shortest_cycle_through 没把 dist 复位干净"
);
for (a, &in_ring) in atom_in_ring.iter().enumerate() {
if let Some(atom) = mol.atom_mut(a as u32) {
atom.flags.set(AtomFlags::IN_RING, in_ring);
}
}
for (bi, &in_ring) in bond_in_ring.iter().enumerate() {
if let Some(mut bond) = mol.bond_mut(bi as u32) {
bond.flags_mut().set(BondFlags::IN_RING, in_ring);
}
}
RingPerception {
atom_min_ring_size,
atom_in_ring,
bond_in_ring,
}
}
pub(crate) struct Adjacency {
offset: Vec<u32>,
nbr: Vec<(u32, u32)>,
n_atoms: usize,
n_bonds: usize,
}
impl Adjacency {
pub(crate) fn build(mol: &MolBuilder, active: &[bool]) -> Self {
let n_atoms = mol.num_atoms();
let mut offset = vec![0u32; n_atoms + 1];
for a in 0..n_atoms {
let deg = mol
.neighbors(a as u32)
.filter(|&(_, bi)| active[bi as usize])
.count();
offset[a + 1] = deg as u32;
}
for i in 1..=n_atoms {
offset[i] += offset[i - 1];
}
let mut nbr = Vec::with_capacity(offset[n_atoms] as usize);
for a in 0..n_atoms {
nbr.extend(
mol.neighbors(a as u32)
.filter(|&(_, bi)| active[bi as usize]),
);
}
Self {
offset,
nbr,
n_atoms,
n_bonds: mol.num_bonds(),
}
}
fn neighbors(&self, a: u32) -> &[(u32, u32)] {
let s = self.offset[a as usize] as usize;
let e = self.offset[a as usize + 1] as usize;
&self.nbr[s..e]
}
}
fn find_bridges(adj: &Adjacency) -> Vec<bool> {
let n = adj.n_atoms;
let mut is_bridge = vec![false; adj.n_bonds];
let mut disc = vec![u32::MAX; n]; let mut low = vec![u32::MAX; n];
let mut timer = 0u32;
let mut stack: Vec<(u32, u32, usize)> = Vec::new();
for root in 0..n as u32 {
if disc[root as usize] != u32::MAX {
continue;
}
disc[root as usize] = timer;
low[root as usize] = timer;
timer += 1;
stack.push((root, u32::MAX, 0));
while let Some(&mut (v, from_bond, ref mut k)) = stack.last_mut() {
let nbrs = adj.neighbors(v);
if *k < nbrs.len() {
let (u, bond) = nbrs[*k];
*k += 1;
if bond == from_bond {
continue; }
if disc[u as usize] == u32::MAX {
disc[u as usize] = timer;
low[u as usize] = timer;
timer += 1;
stack.push((u, bond, 0));
} else {
low[v as usize] = low[v as usize].min(disc[u as usize]);
}
} else {
stack.pop();
if let Some(&mut (parent, _, _)) = stack.last_mut() {
low[parent as usize] = low[parent as usize].min(low[v as usize]);
if low[v as usize] > disc[parent as usize] {
is_bridge[from_bond as usize] = true;
}
}
}
}
}
is_bridge
}
fn shortest_cycle_through(
adj: &Adjacency,
v: u32,
bond_in_ring: &[bool],
max_size: usize,
dist: &mut [u32],
queue: &mut Vec<u32>,
) -> u8 {
let nbrs: Vec<u32> = adj
.neighbors(v)
.iter()
.filter(|&&(_, bi)| bond_in_ring[bi as usize])
.map(|&(u, _)| u)
.collect();
if nbrs.len() < 2 {
return 0;
}
let max_dist = (max_size - 2) as u32;
let mut best = usize::MAX;
for (i, &start) in nbrs.iter().enumerate() {
queue.clear();
dist[start as usize] = 0;
queue.push(start);
let mut head = 0;
while head < queue.len() {
let x = queue[head];
head += 1;
let dx = dist[x as usize];
if dx >= max_dist {
continue;
}
if best != usize::MAX && dx as usize + 2 >= best {
continue;
}
for &(y, bi) in adj.neighbors(x) {
if y == v {
continue; }
if !bond_in_ring[bi as usize] {
continue; }
if dist[y as usize] == u32::MAX {
dist[y as usize] = dx + 1;
queue.push(y);
}
}
}
for &other in &nbrs[i + 1..] {
let d = dist[other as usize];
if d != u32::MAX {
best = best.min(d as usize + 2);
}
}
for &x in queue.iter() {
dist[x as usize] = u32::MAX;
}
}
if best == usize::MAX || best > max_size {
0
} else {
u8::try_from(best).unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use omgkit_io::smiles;
use super::*;
fn perceive(smi: &str) -> (MolBuilder, RingPerception) {
let mut m = smiles::parse(smi).unwrap_or_else(|e| panic!("{}", e.render()));
let r = perceive_rings(&mut m);
(m, r)
}
#[test]
fn acyclic_has_no_rings() {
let (_, r) = perceive("CCO");
assert!(r.atom_in_ring.iter().all(|&x| !x));
assert!(r.bond_in_ring.iter().all(|&x| !x));
assert_eq!(r.atom_min_ring_size, vec![0, 0, 0]);
}
#[test]
fn simple_rings() {
for (smi, size) in [
("C1CC1", 3),
("C1CCC1", 4),
("C1CCCCC1", 6),
("c1ccccc1", 6),
] {
let (_, r) = perceive(smi);
assert!(r.atom_in_ring.iter().all(|&x| x), "{smi}:全部原子应在环中");
assert!(r.bond_in_ring.iter().all(|&x| x), "{smi}:全部键应在环中");
assert!(
r.atom_min_ring_size.iter().all(|&s| s == size),
"{smi}:最小环应为 {size},实际 {:?}",
r.atom_min_ring_size
);
}
}
#[test]
fn substituent_is_not_in_ring() {
let (_, r) = perceive("Cc1ccccc1");
assert!(!r.atom_in_ring[0], "甲基碳不应在环中");
assert!(r.atom_in_ring[1..].iter().all(|&x| x));
assert!(!r.bond_in_ring[0], "甲基-环 的键是桥");
assert_eq!(r.atom_min_ring_size[0], 0);
assert!(r.atom_min_ring_size[1..].iter().all(|&s| s == 6));
}
#[test]
fn fused_rings_take_the_smallest() {
let (_, r) = perceive("c1ccc2ccccc2c1");
assert!(r.atom_min_ring_size.iter().all(|&s| s == 6));
assert!(r.atom_in_ring.iter().all(|&x| x));
}
#[test]
fn spiro_and_bridged() {
let (_, r) = perceive("C1CC12CCC2");
assert_eq!(r.atom_min_ring_size[2], 3, "螺原子应取较小的环");
let (_, r) = perceive("C1CC2CCC1CC2");
assert!(r.atom_in_ring.iter().all(|&x| x));
}
#[test]
fn disconnected_fragments() {
let (_, r) = perceive("C1CC1.CCO");
assert_eq!(&r.atom_in_ring, &[true, true, true, false, false, false]);
assert_eq!(r.atom_min_ring_size, vec![3, 3, 3, 0, 0, 0]);
}
#[test]
fn long_chain_does_not_overflow_stack() {
let smi = "C".repeat(20_000);
let mut m = smiles::parse(&smi).unwrap();
let r = perceive_rings(&mut m);
assert_eq!(m.num_atoms(), 20_000);
assert!(r.bond_in_ring.iter().all(|&x| !x));
}
#[test]
fn dative_bonds_do_not_form_rings() {
let mut m = smiles::parse("C1CC1").unwrap();
m.bond_mut(0).unwrap().set_order(BondOrder::Dative);
let r = perceive_rings(&mut m);
assert!(
r.bond_in_ring.iter().all(|&x| !x),
"配位键不参与成环,剩下的边全变成桥"
);
assert!(r.atom_in_ring.iter().all(|&x| !x));
}
#[test]
fn a_ring_reports_its_own_size_all_the_way_up_to_the_field_width() {
for n in 3..=60usize {
let smi = format!("C1{}1", "C".repeat(n - 1));
let (_, r) = perceive(&smi);
assert!(
r.atom_in_ring.iter().all(|&x| x),
"{n} 元环:有原子没被判成环原子"
);
for (i, &size) in r.atom_min_ring_size.iter().enumerate() {
assert_eq!(
usize::from(size),
n,
"{n} 元环的第 {i} 个原子报的最小环大小是 {size}"
);
}
}
}
#[test]
fn a_ring_too_big_for_the_field_still_knows_it_is_a_ring() {
let n = MAX_RING_SIZE + 40;
let smi = format!("C1{}1", "C".repeat(n - 1));
let (_, r) = perceive(&smi);
assert!(
r.atom_in_ring.iter().all(|&x| x),
"{n} 元环的原子没被判成环原子 —— 上限把两个字段一起打翻了"
);
assert!(
r.atom_min_ring_size.iter().all(|&s| s == 0),
"{n} 元环居然报出了大小,那 MAX_RING_SIZE 的注释就过期了"
);
}
#[test]
fn membership_agrees_with_min_ring_size() {
for smi in [
"CCO",
"C1CCCCC1",
"Cc1ccccc1",
"c1ccc2ccccc2c1",
"C1CC2CCC1CC2",
"C1CC12CCC2",
"CC(=O)Oc1ccccc1C(=O)O",
"CN1C=NC2=C1C(=O)N(C)C(=O)N2C",
"C1CC1.CCO",
] {
let (_, r) = perceive(smi);
for (i, (&in_ring, &size)) in r
.atom_in_ring
.iter()
.zip(r.atom_min_ring_size.iter())
.enumerate()
{
assert_eq!(
in_ring,
size > 0,
"{smi} 原子 {i}:在环中={in_ring} 但最小环={size}"
);
}
}
}
#[test]
fn flags_are_written_back() {
let (m, r) = perceive("Cc1ccccc1");
for (i, a) in m.atoms().iter().enumerate() {
assert_eq!(a.flags.contains(AtomFlags::IN_RING), r.atom_in_ring[i]);
}
for (i, b) in m.bonds().iter().enumerate() {
assert_eq!(b.flags.contains(BondFlags::IN_RING), r.bond_in_ring[i]);
}
}
}
#[cfg(test)]
mod organometallic_root_cause {
use omgkit_io::smiles;
use super::*;
const FERROCENE: &str = "CN(C)C[C-]12C3=C4C5=C1[Fe++]23456789[C-]%10C6=C7C8=C9%10";
#[test]
fn ring_result_follows_the_bond_becoming_dative() {
let mut before = smiles::parse(FERROCENE).unwrap();
crate::clean_up(&mut before);
let r_before = perceive_rings(&mut before);
assert_eq!(before.bonds()[15].order, BondOrder::Single);
assert!(r_before.bond_in_ring[15]);
assert_eq!(r_before.atom_min_ring_size[4], 3);
let mut after = smiles::parse(FERROCENE).unwrap();
crate::clean_up(&mut after);
assert_eq!(
crate::cleanup_organometallics(&mut after),
1,
"第 2 步应当恰好改动一条键"
);
assert_eq!(after.bonds()[15].order, BondOrder::Dative);
let r_after = perceive_rings(&mut after);
assert!(!r_after.bond_in_ring[15], "配位键应被排除在环外");
assert_eq!(r_after.atom_min_ring_size[4], 4, "三元环断开后应退到四元环");
}
}
#[must_use]
pub fn fused_ring_systems(mol: &MolBuilder) -> Vec<Vec<u32>> {
let active: Vec<bool> = mol
.bonds()
.iter()
.map(|b| b.order != BondOrder::Dative)
.collect();
let adj = Adjacency::build(mol, &active);
let mut systems: Vec<Vec<u32>> = biconnected_bond_components(&adj)
.into_iter()
.map(|bonds| {
let mut atoms: Vec<u32> = bonds
.iter()
.flat_map(|&bi| {
let b = mol.bonds()[bi as usize];
[b.begin, b.end]
})
.collect();
atoms.sort_unstable();
atoms.dedup();
atoms
})
.collect();
systems.sort_by_key(|s| s.first().copied().unwrap_or(u32::MAX));
systems
}
pub(crate) fn biconnected_bond_components(adj: &Adjacency) -> Vec<Vec<u32>> {
let n = adj.n_atoms;
let mut disc = vec![u32::MAX; n];
let mut low = vec![u32::MAX; n];
let mut timer = 0u32;
let mut edge_stack: Vec<(u32, u32, u32)> = Vec::new();
let mut out: Vec<Vec<u32>> = Vec::new();
let mut stack: Vec<(u32, u32, usize)> = Vec::new();
for root in 0..n as u32 {
if disc[root as usize] != u32::MAX {
continue;
}
disc[root as usize] = timer;
low[root as usize] = timer;
timer += 1;
stack.push((root, u32::MAX, 0));
while let Some(&mut (v, from_bond, ref mut k)) = stack.last_mut() {
let nbrs = adj.neighbors(v);
if *k < nbrs.len() {
let (u, bond) = nbrs[*k];
*k += 1;
if bond == from_bond {
continue;
}
if disc[u as usize] == u32::MAX {
edge_stack.push((v, u, bond));
disc[u as usize] = timer;
low[u as usize] = timer;
timer += 1;
stack.push((u, bond, 0));
} else if disc[u as usize] < disc[v as usize] {
edge_stack.push((v, u, bond));
low[v as usize] = low[v as usize].min(disc[u as usize]);
}
} else {
stack.pop();
if let Some(&mut (parent, _, _)) = stack.last_mut() {
low[parent as usize] = low[parent as usize].min(low[v as usize]);
if low[v as usize] >= disc[parent as usize] {
let mut comp: Vec<u32> = Vec::new();
while let Some(&(a, _, bi)) = edge_stack.last() {
if disc[a as usize] < disc[v as usize] {
break;
}
edge_stack.pop();
comp.push(bi);
}
if let Some(pos) = edge_stack
.iter()
.rposition(|&(a, b, _)| a == parent && b == v)
{
let (_, _, bi) = edge_stack.remove(pos);
comp.push(bi);
}
if comp.len() >= 2 {
comp.sort_unstable();
comp.dedup();
out.push(comp);
}
}
}
}
}
}
out
}
#[cfg(test)]
mod fused_system_tests {
use omgkit_io::smiles;
use super::*;
fn systems(smi: &str) -> Vec<Vec<u32>> {
let m = smiles::parse(smi).unwrap_or_else(|e| panic!("{}", e.render()));
fused_ring_systems(&m)
}
#[test]
fn acyclic_has_no_systems() {
assert!(systems("CCO").is_empty());
assert!(systems("CC(C)C(=O)O").is_empty());
}
#[test]
fn single_ring_is_one_system() {
assert_eq!(systems("c1ccccc1"), vec![vec![0, 1, 2, 3, 4, 5]]);
assert_eq!(systems("Cc1ccccc1"), vec![vec![1, 2, 3, 4, 5, 6]]);
}
#[test]
fn fused_rings_merge() {
let s = systems("c1ccc2ccccc2c1");
assert_eq!(s.len(), 1);
assert_eq!(s[0].len(), 10);
}
#[test]
fn spiro_rings_stay_separate() {
let s = systems("C1CC12CC2");
assert_eq!(s.len(), 2, "螺环应是两个独立环系,实际 {s:?}");
assert!(s.iter().all(|x| x.len() == 3));
assert!(s.iter().filter(|x| x.contains(&2)).count() == 2);
}
#[test]
fn biphenyl_rings_stay_separate() {
let s = systems("c1ccccc1-c1ccccc1");
assert_eq!(s.len(), 2, "联苯应是两个独立环系,实际 {s:?}");
assert!(s.iter().all(|x| x.len() == 6));
}
#[test]
fn bridged_bicyclic_is_one_system() {
let s = systems("C1CC2CCC1CC2");
assert_eq!(s.len(), 1);
assert_eq!(s[0].len(), 8);
}
#[test]
fn disconnected_fragments_give_separate_systems() {
let s = systems("c1ccccc1.C1CC1");
assert_eq!(s.len(), 2);
assert_eq!(s[0].len(), 6);
assert_eq!(s[1].len(), 3);
}
#[test]
fn systems_cover_exactly_the_ring_atoms() {
for smi in [
"CCO",
"c1ccccc1",
"Cc1ccccc1",
"c1ccc2ccccc2c1",
"C1CC12CC2",
"c1ccccc1-c1ccccc1",
"C1CC2CCC1CC2",
"CC(=O)Oc1ccccc1C(=O)O",
"CN1C=NC2=C1C(=O)N(C)C(=O)N2C",
"c1ccccc1.C1CC1",
] {
let mut m = smiles::parse(smi).unwrap();
let r = perceive_rings(&mut m);
let mut from_systems: Vec<u32> = fused_ring_systems(&m).concat();
from_systems.sort_unstable();
from_systems.dedup();
let from_bridges: Vec<u32> = r
.atom_in_ring
.iter()
.enumerate()
.filter(|(_, &x)| x)
.map(|(i, _)| i as u32)
.collect();
assert_eq!(from_systems, from_bridges, "{smi}: 环系与环成员判定不一致");
}
}
#[test]
fn long_chain_does_not_overflow_stack() {
let m = smiles::parse(&"C".repeat(20_000)).unwrap();
assert!(fused_ring_systems(&m).is_empty());
}
}