use std::collections::{HashMap, VecDeque};
use crate::system::atomistic::{AtomId, Atomistic, BondId};
#[derive(Debug, Clone)]
pub struct RingInfo {
rings: Vec<Vec<AtomId>>,
atom_rings: HashMap<AtomId, Vec<usize>>,
bond_rings: HashMap<BondId, Vec<usize>>,
}
impl RingInfo {
fn empty() -> Self {
Self {
rings: Vec::new(),
atom_rings: HashMap::new(),
bond_rings: HashMap::new(),
}
}
pub fn is_atom_in_ring(&self, id: AtomId) -> bool {
self.atom_rings.get(&id).is_some_and(|v| !v.is_empty())
}
pub fn is_bond_in_ring(&self, id: BondId) -> bool {
self.bond_rings.get(&id).is_some_and(|v| !v.is_empty())
}
pub fn num_atom_rings(&self, id: AtomId) -> usize {
self.atom_rings.get(&id).map_or(0, Vec::len)
}
pub fn num_bond_rings(&self, id: BondId) -> usize {
self.bond_rings.get(&id).map_or(0, Vec::len)
}
pub fn ring_sizes(&self) -> Vec<usize> {
self.rings.iter().map(Vec::len).collect()
}
pub fn rings_of_size(&self, n: usize) -> Vec<&Vec<AtomId>> {
self.rings.iter().filter(|r| r.len() == n).collect()
}
pub fn smallest_ring_containing_atom(&self, id: AtomId) -> Option<usize> {
self.atom_rings
.get(&id)?
.iter()
.map(|&ri| self.rings[ri].len())
.min()
}
pub fn num_rings(&self) -> usize {
self.rings.len()
}
pub fn rings(&self) -> &[Vec<AtomId>] {
&self.rings
}
}
pub fn find_rings(mol: &Atomistic) -> RingInfo {
if mol.n_atoms() == 0 {
return RingInfo::empty();
}
let atom_vec: Vec<AtomId> = mol.atoms().map(|(id, _)| id).collect();
let atom_to_idx: HashMap<AtomId, usize> = atom_vec
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
let bond_vec: Vec<BondId> = mol.bonds().map(|(id, _)| id).collect();
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); atom_vec.len()];
let mut edges: Vec<[usize; 2]> = Vec::with_capacity(bond_vec.len());
for &bid in &bond_vec {
let (n0, n1) = mol.bond_endpoints(bid).expect("bond must exist");
let u = atom_to_idx[&n0];
let v = atom_to_idx[&n1];
edges.push([u, v]);
adj[u].push(v);
adj[v].push(u);
}
let n_edges = edges.len();
let n_components = count_components(&adj);
let cycle_rank = n_edges as isize - atom_vec.len() as isize + n_components as isize;
if cycle_rank <= 0 {
return RingInfo::empty();
}
let cycle_rank = cycle_rank as usize;
let mut candidates: Vec<Vec<usize>> = Vec::new();
for edge in &edges {
let (u, v) = (edge[0], edge[1]);
let skip = if u < v { (u, v) } else { (v, u) };
if let Some(path) = bfs_shortest_path(&adj, u, v, skip) {
candidates.push(path);
}
}
candidates.sort_by_key(|c| c.len());
let mut edge_lookup: HashMap<(usize, usize), usize> = HashMap::new();
for (ei, edge) in edges.iter().enumerate() {
let (a, b) = (edge[0], edge[1]);
let key = if a < b { (a, b) } else { (b, a) };
edge_lookup.insert(key, ei);
}
let mut basis_vectors: Vec<Vec<u64>> = Vec::new();
let words = n_edges.div_ceil(64);
let mut selected_cycles: Vec<Vec<usize>> = Vec::new();
for cycle in &candidates {
if selected_cycles.len() >= cycle_rank {
break;
}
let mut bitvec = vec![0u64; words];
let n = cycle.len();
for i in 0..n {
let a = cycle[i];
let b = cycle[(i + 1) % n];
let key = if a < b { (a, b) } else { (b, a) };
if let Some(&ei) = edge_lookup.get(&key) {
bitvec[ei / 64] |= 1u64 << (ei % 64);
}
}
if is_linearly_independent(&mut basis_vectors, bitvec, words) {
selected_cycles.push(cycle.clone());
}
}
let mut raw_rings: Vec<Vec<AtomId>> = selected_cycles
.into_iter()
.map(|cycle| cycle.iter().map(|&ni| atom_vec[ni]).collect())
.collect();
raw_rings.sort_by_key(Vec::len);
let mut bond_map: HashMap<(AtomId, AtomId), BondId> = HashMap::new();
for &bid in &bond_vec {
let (a, bb) = mol.bond_endpoints(bid).expect("bond must exist");
bond_map.insert((a, bb), bid);
bond_map.insert((bb, a), bid);
}
let mut atom_rings: HashMap<AtomId, Vec<usize>> = HashMap::new();
let mut bond_rings: HashMap<BondId, Vec<usize>> = HashMap::new();
for (ri, ring) in raw_rings.iter().enumerate() {
let n = ring.len();
for i in 0..n {
let a = ring[i];
let b = ring[(i + 1) % n];
atom_rings.entry(a).or_default().push(ri);
if let Some(&bid) = bond_map.get(&(a, b)) {
bond_rings.entry(bid).or_default().push(ri);
}
}
}
RingInfo {
rings: raw_rings,
atom_rings,
bond_rings,
}
}
fn count_components(adj: &[Vec<usize>]) -> usize {
let n = adj.len();
let mut visited = vec![false; n];
let mut components = 0usize;
for start in 0..n {
if visited[start] {
continue;
}
components += 1;
let mut queue = VecDeque::new();
visited[start] = true;
queue.push_back(start);
while let Some(current) = queue.pop_front() {
for &neighbor in &adj[current] {
if !visited[neighbor] {
visited[neighbor] = true;
queue.push_back(neighbor);
}
}
}
}
components
}
fn bfs_shortest_path(
adj: &[Vec<usize>],
start: usize,
goal: usize,
skip: (usize, usize),
) -> Option<Vec<usize>> {
let n = adj.len();
let mut visited = vec![false; n];
let mut parent: Vec<i64> = vec![-1; n];
let mut queue = VecDeque::new();
visited[start] = true;
queue.push_back(start);
while let Some(current) = queue.pop_front() {
if current == goal {
let mut path = vec![goal];
let mut node = goal;
while node != start {
node = parent[node] as usize;
path.push(node);
}
path.reverse();
return Some(path);
}
for &neighbor in &adj[current] {
let key = if current < neighbor {
(current, neighbor)
} else {
(neighbor, current)
};
if key == skip {
continue;
}
if !visited[neighbor] {
visited[neighbor] = true;
parent[neighbor] = current as i64;
queue.push_back(neighbor);
}
}
}
None
}
fn is_linearly_independent(basis: &mut Vec<Vec<u64>>, mut vec: Vec<u64>, words: usize) -> bool {
for basis_vec in basis.iter() {
let lead = leading_bit(basis_vec, words);
if let Some(lead) = lead
&& (vec[lead / 64] >> (lead % 64)) & 1 == 1
{
for w in 0..words {
vec[w] ^= basis_vec[w];
}
}
}
let is_nonzero = vec.iter().any(|&w| w != 0);
if is_nonzero {
basis.push(vec);
}
is_nonzero
}
fn leading_bit(vec: &[u64], words: usize) -> Option<usize> {
for w in (0..words).rev() {
if vec[w] != 0 {
return Some(w * 64 + (63 - vec[w].leading_zeros() as usize));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::system::molgraph::Atom;
fn cycle(n: usize) -> Atomistic {
let mut g = Atomistic::new();
let ids: Vec<AtomId> = (0..n).map(|_| g.add_atom(Atom::new())).collect();
for i in 0..n {
g.add_bond(ids[i], ids[(i + 1) % n])
.expect("add cycle bond");
}
g
}
#[test]
fn test_single_6ring() {
let g = cycle(6);
let ri = find_rings(&g);
assert_eq!(ri.num_rings(), 1);
assert_eq!(ri.ring_sizes(), vec![6]);
}
#[test]
fn test_linear_no_rings() {
let mut g = Atomistic::new();
let ids: Vec<AtomId> = (0..6).map(|_| g.add_atom(Atom::new())).collect();
for i in 0..5 {
g.add_bond(ids[i], ids[i + 1]).expect("add chain bond");
}
assert_eq!(find_rings(&g).num_rings(), 0);
}
#[test]
fn test_all_atoms_in_6ring() {
let g = cycle(6);
let ri = find_rings(&g);
for (id, _) in g.atoms() {
assert!(ri.is_atom_in_ring(id));
}
}
#[test]
fn test_all_bonds_in_6ring() {
let g = cycle(6);
let ri = find_rings(&g);
for (bid, _) in g.bonds() {
assert!(ri.is_bond_in_ring(bid));
}
}
#[test]
fn test_empty_mol() {
assert_eq!(find_rings(&Atomistic::new()).num_rings(), 0);
}
#[test]
fn test_naphthalene() {
let mut g = Atomistic::new();
let ids: Vec<AtomId> = (0..10).map(|_| g.add_atom(Atom::new())).collect();
for i in 0..5 {
g.add_bond(ids[i], ids[i + 1]).expect("bond");
}
g.add_bond(ids[5], ids[0]).expect("bond");
g.add_bond(ids[3], ids[6]).expect("bond");
g.add_bond(ids[6], ids[7]).expect("bond");
g.add_bond(ids[7], ids[8]).expect("bond");
g.add_bond(ids[8], ids[9]).expect("bond");
g.add_bond(ids[9], ids[2]).expect("bond");
let ri = find_rings(&g);
assert_eq!(ri.num_rings(), 2);
let mut sizes = ri.ring_sizes();
sizes.sort_unstable();
assert_eq!(sizes, vec![6, 6]);
}
}