use omgkit_core::{BondOrder, MolBuilder};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ring {
pub atoms: Vec<u32>,
pub bonds: Vec<u32>,
}
impl Ring {
#[must_use]
pub fn len(&self) -> usize {
self.atoms.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.atoms.is_empty()
}
}
#[must_use]
pub fn ring_set(mol: &MolBuilder) -> Vec<Ring> {
ring_set_counted(mol).0
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct SearchStats {
pub bfs_visits: u64,
pub edge_tests: u64,
pub path_steps: u64,
}
#[must_use]
pub fn ring_set_counted(mol: &MolBuilder) -> (Vec<Ring>, SearchStats) {
let active: Vec<bool> = mol
.bonds()
.iter()
.map(|b| b.order != BondOrder::Dative)
.collect();
let adj = crate::rings::Adjacency::build(mol, &active);
let mut stats = SearchStats::default();
let mut out: Vec<Ring> = Vec::new();
for comp_bonds in crate::rings::biconnected_bond_components(&adj) {
out.extend(component_ring_set(mol, &comp_bonds, &mut stats));
}
out.sort_by_key(|r| {
(
r.atoms.len(),
r.atoms.iter().copied().min().unwrap_or(u32::MAX),
r.atoms.iter().copied().max().unwrap_or(u32::MAX),
)
});
(out, stats)
}
struct Component {
atoms: Vec<u32>,
bonds: Vec<u32>,
offset: Vec<u32>,
nbr: Vec<(u32, u32)>,
edge_ends: Vec<(u32, u32)>,
}
impl Component {
fn build(mol: &MolBuilder, comp_bonds: &[u32]) -> Self {
let mut atoms: Vec<u32> = comp_bonds
.iter()
.flat_map(|&bi| {
let b = mol.bonds()[bi as usize];
[b.begin, b.end]
})
.collect();
atoms.sort_unstable();
atoms.dedup();
let local_of = |g: u32| atoms.binary_search(&g).expect("端点必在分量内") as u32;
let n = atoms.len();
let mut degree = vec![0u32; n];
for &bi in comp_bonds {
let b = mol.bonds()[bi as usize];
degree[local_of(b.begin) as usize] += 1;
degree[local_of(b.end) as usize] += 1;
}
let mut offset = vec![0u32; n + 1];
for i in 0..n {
offset[i + 1] = offset[i] + degree[i];
}
let mut cursor = offset[..n].to_vec();
let mut nbr = vec![(0u32, 0u32); offset[n] as usize];
let mut edge_ends = Vec::with_capacity(comp_bonds.len());
for (lb, &bi) in comp_bonds.iter().enumerate() {
let b = mol.bonds()[bi as usize];
let (u, v) = (local_of(b.begin), local_of(b.end));
edge_ends.push((u, v));
let lb = lb as u32;
nbr[cursor[u as usize] as usize] = (v, lb);
cursor[u as usize] += 1;
nbr[cursor[v as usize] as usize] = (u, lb);
cursor[v as usize] += 1;
}
Self {
atoms,
bonds: comp_bonds.to_vec(),
offset,
nbr,
edge_ends,
}
}
fn n_atoms(&self) -> usize {
self.atoms.len()
}
fn n_bonds(&self) -> usize {
self.bonds.len()
}
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]
}
}
#[derive(Clone, PartialEq, Eq)]
struct BitSet(Vec<u64>);
impl BitSet {
fn new(n_bits: usize) -> Self {
Self(vec![0; n_bits.div_ceil(64)])
}
fn set(&mut self, i: usize) {
self.0[i / 64] |= 1u64 << (i % 64);
}
fn xor_with(&mut self, other: &Self) {
for (a, b) in self.0.iter_mut().zip(&other.0) {
*a ^= b;
}
}
fn is_zero(&self) -> bool {
self.0.iter().all(|&w| w == 0)
}
fn leading(&self) -> Option<usize> {
self.0
.iter()
.enumerate()
.rev()
.find(|(_, &w)| w != 0)
.map(|(i, &w)| i * 64 + (63 - w.leading_zeros() as usize))
}
}
struct Gf2Basis {
rows: Vec<Option<BitSet>>,
}
impl Gf2Basis {
fn new(n_bits: usize) -> Self {
Self {
rows: vec![None; n_bits],
}
}
fn reduce(&self, v: &BitSet) -> BitSet {
let mut cur = v.clone();
while let Some(lead) = cur.leading() {
match &self.rows[lead] {
Some(row) => cur.xor_with(row),
None => break,
}
}
cur
}
fn insert(&mut self, v: &BitSet) -> bool {
let reduced = self.reduce(v);
match reduced.leading() {
Some(lead) => {
self.rows[lead] = Some(reduced);
true
}
None => false,
}
}
}
struct Candidate {
atoms: Vec<u32>,
bonds: Vec<u32>,
bits: BitSet,
key: Vec<u32>,
}
fn component_ring_set(mol: &MolBuilder, comp_bonds: &[u32], stats: &mut SearchStats) -> Vec<Ring> {
let c = Component::build(mol, comp_bonds);
if let Some(ring) = single_cycle_component(&c) {
return vec![ring];
}
let rank_target = c.n_bonds() + 1 - c.n_atoms();
let mut limit = INITIAL_LENGTH_LIMIT;
loop {
let capped = limit >= c.n_atoms();
let (rings, rank) = cycles_up_to(&c, limit.min(c.n_atoms()), stats);
if rank >= rank_target || capped {
debug_assert!(
rank >= rank_target,
"搜遍全部长度仍未张满圈空间:秩 {rank} < 圈秩 {rank_target}"
);
return rings;
}
limit *= 2;
}
}
const INITIAL_LENGTH_LIMIT: usize = 8;
fn single_cycle_component(c: &Component) -> Option<Ring> {
let n = c.n_atoms();
if n < 3 || c.n_bonds() != n {
return None;
}
if (0..n as u32).any(|a| c.neighbors(a).len() != 2) {
return None;
}
let mut atoms = Vec::with_capacity(n);
let mut bonds = Vec::with_capacity(n);
let (mut prev, mut cur) = (u32::MAX, 0u32);
for _ in 0..n {
atoms.push(c.atoms[cur as usize]);
let (next, lb) = *c
.neighbors(cur)
.iter()
.find(|&&(y, _)| y != prev)
.expect("度数为 2 的顶点必有一个非来路的邻居");
bonds.push(c.bonds[lb as usize]);
prev = cur;
cur = next;
}
if cur != 0 {
return None;
}
Some(Ring { atoms, bonds })
}
fn cycles_up_to(c: &Component, limit: usize, stats: &mut SearchStats) -> (Vec<Ring>, usize) {
let mut cands = horton_candidates(c, limit, stats);
cands.sort_by(|a, b| a.atoms.len().cmp(&b.atoms.len()).then(a.key.cmp(&b.key)));
let mut basis = Gf2Basis::new(c.n_bonds());
let mut rank = 0usize;
let mut out: Vec<Ring> = Vec::new();
let mut i = 0usize;
while i < cands.len() {
let len = cands[i].atoms.len();
let mut j = i;
while j < cands.len() && cands[j].atoms.len() == len {
j += 1;
}
for cand in &cands[i..j] {
if !basis.reduce(&cand.bits).is_zero() {
out.push(Ring {
atoms: cand.atoms.iter().map(|&a| c.atoms[a as usize]).collect(),
bonds: cand.bonds.iter().map(|&b| c.bonds[b as usize]).collect(),
});
}
}
for cand in &cands[i..j] {
if basis.insert(&cand.bits) {
rank += 1;
}
}
i = j;
}
(out, rank)
}
fn horton_candidates(c: &Component, limit: usize, stats: &mut SearchStats) -> Vec<Candidate> {
let n = c.n_atoms();
let m = c.n_bonds();
let depth_limit = limit.div_ceil(2) as u32;
let mut seen: std::collections::BTreeSet<Vec<u32>> = std::collections::BTreeSet::new();
let mut out: Vec<Candidate> = Vec::new();
let mut dist = vec![u32::MAX; n];
let mut parent = vec![u32::MAX; n];
let mut parent_bond = vec![u32::MAX; n];
let mut branch = vec![u32::MAX; n];
let mut queue: Vec<u32> = Vec::with_capacity(n);
let mut on_path = vec![false; n];
let mut edge_done = vec![false; m];
for v in 0..n as u32 {
for &x in &queue {
dist[x as usize] = u32::MAX;
}
queue.clear();
dist[v as usize] = 0;
parent[v as usize] = u32::MAX;
branch[v as usize] = u32::MAX;
queue.push(v);
let mut head = 0;
while head < queue.len() {
let x = queue[head];
head += 1;
stats.bfs_visits += 1;
if dist[x as usize] >= depth_limit {
continue; }
for &(y, bi) in c.neighbors(x) {
if dist[y as usize] == u32::MAX {
dist[y as usize] = dist[x as usize] + 1;
parent[y as usize] = x;
parent_bond[y as usize] = bi;
branch[y as usize] = if x == v { y } else { branch[x as usize] };
queue.push(y);
}
}
}
for &x in queue.iter() {
for &(_, lb) in c.neighbors(x) {
if edge_done[lb as usize] {
continue;
}
edge_done[lb as usize] = true;
stats.edge_tests += 1;
let (a, b) = c.edge_ends[lb as usize];
if dist[a as usize] == u32::MAX || dist[b as usize] == u32::MAX {
continue;
}
let (da, db) = (dist[a as usize], dist[b as usize]);
let len = (da + db + 1) as usize;
if len < 3 || len > limit {
continue;
}
debug_assert!(
da.abs_diff(db) <= 1,
"无权 BFS 里一条边两端的距离差应当 ≤ 1,实得 {da} 与 {db}"
);
if da.abs_diff(db) > 1 {
continue;
}
if branch[a as usize] == branch[b as usize] {
continue;
}
let px = trace(&parent, &parent_bond, v, a);
let py = trace(&parent, &parent_bond, v, b);
stats.path_steps += (px.len() + py.len()) as u64;
for &(t, _) in &px {
on_path[t as usize] = true;
}
let disjoint = py[..py.len() - 1]
.iter()
.all(|&(t, _)| !on_path[t as usize]);
for &(t, _) in &px {
on_path[t as usize] = false;
}
if !disjoint {
continue;
}
let mut atoms: Vec<u32> = px.iter().map(|&(t, _)| t).collect();
atoms.extend(py[..py.len() - 1].iter().rev().map(|&(t, _)| t));
let bonds = ring_bonds(&px, &py, lb);
debug_assert_eq!(atoms.len(), len);
debug_assert_eq!(bonds.len(), len);
let mut key = atoms.clone();
key.sort_unstable();
if !seen.insert(key.clone()) {
continue;
}
let mut bits = BitSet::new(m);
for &bd in &bonds {
bits.set(bd as usize);
}
out.push(Candidate {
atoms,
bonds,
bits,
key,
});
}
}
for &x in queue.iter() {
for &(_, lb) in c.neighbors(x) {
edge_done[lb as usize] = false;
}
}
}
out
}
fn trace(parent: &[u32], parent_bond: &[u32], v: u32, t: u32) -> Vec<(u32, u32)> {
let mut out = Vec::new();
let mut cur = t;
while cur != v {
out.push((cur, parent_bond[cur as usize]));
cur = parent[cur as usize];
}
out.push((v, u32::MAX));
out
}
fn ring_bonds(px: &[(u32, u32)], py: &[(u32, u32)], closing: u32) -> Vec<u32> {
let mut bonds: Vec<u32> = px[..px.len() - 1].iter().map(|&(_, b)| b).collect();
for &(_, b) in py[..py.len() - 1].iter().rev() {
bonds.push(b);
}
bonds.push(closing);
bonds
}
#[cfg(test)]
mod tests {
use omgkit_io::smiles;
use std::collections::BTreeSet;
use super::*;
fn ring_atom_sets(smi: &str) -> BTreeSet<BTreeSet<u32>> {
let m = smiles::parse(smi).unwrap_or_else(|e| panic!("{}", e.render()));
ring_set(&m)
.into_iter()
.map(|r| r.atoms.into_iter().collect())
.collect()
}
fn ring_sizes(smi: &str) -> Vec<usize> {
let mut v: Vec<usize> = ring_atom_sets(smi).iter().map(BTreeSet::len).collect();
v.sort_unstable();
v
}
#[test]
fn acyclic_has_no_rings() {
assert!(ring_sizes("CCO").is_empty());
assert!(ring_sizes("CC(C)(C)CC").is_empty());
}
#[test]
fn simple_rings() {
assert_eq!(ring_sizes("C1CC1"), vec![3]);
assert_eq!(ring_sizes("c1ccccc1"), vec![6]);
assert_eq!(ring_sizes("C1CCCCCCC1"), vec![8]);
}
#[test]
fn fused_aromatics() {
assert_eq!(ring_sizes("c1ccc2ccccc2c1"), vec![6, 6]);
assert_eq!(ring_sizes("c1ccc2c(c1)ccc1ccccc12"), vec![6, 6, 6]);
assert_eq!(ring_sizes("c1cc2ccc3cccc4ccc(c1)c2c34"), vec![6, 6, 6, 6]);
}
#[test]
fn cages_have_more_rings_than_the_cyclomatic_number() {
for (smi, name, expect) in [
("C12C3C4C1C5C4C3C25", "立方烷", vec![4; 6]),
("C12C3C1C1C2C31", "棱晶烷", vec![3, 3, 4, 4, 4]),
("C1CC2CCC1CC2", "双环[2.2.2]辛烷", vec![6, 6, 6]),
("C1C2CC3CC1CC(C2)C3", "金刚烷", vec![6, 6, 6, 6]),
("C1CC2CCC1C2", "降冰片烷", vec![5, 5]),
] {
assert_eq!(ring_sizes(smi), expect, "{name}");
}
}
#[test]
fn ring_set_is_independent_of_atom_numbering() {
fn permute(m: &omgkit_core::MolBuilder, perm: &[u32]) -> omgkit_core::MolBuilder {
let mut inv = vec![0u32; m.num_atoms()];
for (new, &old) in perm.iter().enumerate() {
inv[old as usize] = new as u32;
}
let mut out = omgkit_core::MolBuilder::with_capacity(m.num_atoms(), m.num_bonds());
for &old in perm {
out.add_atom_data(m.atoms()[old as usize]);
}
for b in m.bonds() {
let mut nb = *b;
nb.begin = inv[b.begin as usize];
nb.end = inv[b.end as usize];
out.add_bond_data(nb).expect("重排后端点仍合法");
}
out
}
for smi in [
"c1ccccc1",
"c1ccc2ccccc2c1",
"C12C3C4C1C5C4C3C25", "C12C3C1C1C2C31", "C1C2CC3CC1CC(C2)C3", "C1CC2CCC1CC2", "c1cc2ccc3cccc4ccc(c1)c2c34", "C1CCC2(CC1)CCCCC2", "c1ccccc1Cc1ccccc1", ] {
let m = smiles::parse(smi).unwrap_or_else(|e| panic!("{}", e.render()));
let n = m.num_atoms() as u32;
let base: BTreeSet<BTreeSet<u32>> = ring_set(&m)
.into_iter()
.map(|r| r.atoms.into_iter().collect())
.collect();
let perms: Vec<Vec<u32>> = vec![
(0..n).rev().collect(),
(0..n).map(|i| (i + n / 2) % n).collect(),
(0..n).step_by(2).chain((1..n).step_by(2)).collect(),
];
for perm in perms {
let pm = permute(&m, &perm);
let got: BTreeSet<BTreeSet<u32>> = ring_set(&pm)
.into_iter()
.map(|r| r.atoms.into_iter().map(|a| perm[a as usize]).collect())
.collect();
assert_eq!(got, base, "{smi}:重排 {perm:?} 后环集改变");
}
}
}
#[test]
fn ring_atoms_and_bonds_are_in_cycle_order() {
for smi in [
"c1ccccc1",
"c1ccc2ccccc2c1",
"C1C2CC3CC1CC(C2)C3",
"C12C3C4C1C5C4C3C25",
"C1CCC2(CC1)CCCCC2",
] {
let m = smiles::parse(smi).unwrap();
for r in ring_set(&m) {
assert_eq!(r.atoms.len(), r.bonds.len(), "{smi}: 原子数应等于键数");
for i in 0..r.len() {
let a = r.atoms[i];
let b = r.atoms[(i + 1) % r.len()];
let bond = m.bonds()[r.bonds[i] as usize];
assert!(
(bond.begin == a && bond.end == b) || (bond.begin == b && bond.end == a),
"{smi}: 键 {} 未连接 {a} 与 {b}",
r.bonds[i]
);
}
let uniq: BTreeSet<u32> = r.atoms.iter().copied().collect();
assert_eq!(uniq.len(), r.atoms.len(), "{smi}: 环上原子重复");
}
}
}
#[test]
fn dative_bonds_do_not_close_rings() {
let mut m = smiles::parse("C1CCCCC1").unwrap();
assert_eq!(ring_set(&m).len(), 1);
m.bond_mut(0).unwrap().set_order(BondOrder::Dative);
assert!(
ring_set(&m).is_empty(),
"把一条环键改成配位键后,该环不应再成立"
);
}
#[test]
fn separate_ring_systems_are_all_found() {
assert_eq!(ring_sizes("c1ccccc1.c1ccccc1"), vec![6, 6]);
assert_eq!(ring_sizes("c1ccccc1Cc1ccccc1"), vec![6, 6]);
assert_eq!(ring_sizes("C1CC1CCC1CCC1"), vec![3, 4]);
}
}