1use std::collections::{HashMap, VecDeque};
15
16use chematic_core::{AtomIdx, BondIdx, Molecule};
17
18#[derive(Debug, Clone)]
27pub struct RingSet(Vec<Vec<AtomIdx>>);
28
29impl RingSet {
30 pub fn rings(&self) -> &[Vec<AtomIdx>] {
32 &self.0
33 }
34
35 pub fn ring_count(&self) -> usize {
37 self.0.len()
38 }
39
40 pub fn contains_atom(&self, atom: AtomIdx) -> bool {
42 self.0.iter().any(|ring| ring.contains(&atom))
43 }
44
45 pub fn atoms_in_ring_count(&self, atom: AtomIdx) -> usize {
47 self.0.iter().filter(|ring| ring.contains(&atom)).count()
48 }
49}
50
51pub fn find_sssr(mol: &Molecule) -> RingSet {
60 let v = mol.atom_count();
61 let e = mol.bond_count();
62
63 if v == 0 || e == 0 {
64 return RingSet(Vec::new());
65 }
66
67 let (components, parent, bfs_depth) = bfs_spanning_forest(mol);
69 let c = components;
70 let r = (e as isize) - (v as isize) + (c as isize);
71
72 if r <= 0 {
73 return RingSet(Vec::new());
74 }
75 let r = r as usize;
76
77 let mut candidate_cycles: Vec<(Vec<BondIdx>, Vec<AtomIdx>)> = Vec::new();
79
80 for (bidx, bond) in mol.bonds() {
81 let u = bond.atom1;
82 let v_atom = bond.atom2;
83
84 let u_parent = parent[u.0 as usize];
87 let v_parent = parent[v_atom.0 as usize];
88
89 let is_tree_edge = (u_parent == Some(v_atom)) || (v_parent == Some(u));
90
91 if !is_tree_edge {
92 if let Some((bond_set, atom_seq)) = fundamental_cycle(mol, u, v_atom, bidx, &parent, &bfs_depth) {
94 candidate_cycles.push((bond_set, atom_seq));
95 }
96 }
97 }
98
99 candidate_cycles.sort_by_key(|(bonds, _)| bonds.len());
101
102 let mut basis: HashMap<BondIdx, Vec<BondIdx>> = HashMap::new();
105 let mut selected_atoms: Vec<Vec<AtomIdx>> = Vec::new();
106
107 for (bond_set, atom_seq) in candidate_cycles {
108 let reduced = gf2_reduce(&bond_set, &basis);
110
111 if !reduced.is_empty() {
112 let pivot = *reduced.iter().min().unwrap();
114 basis.insert(pivot, reduced);
115 selected_atoms.push(atom_seq);
116
117 if selected_atoms.len() == r {
118 break;
119 }
120 }
121 }
122
123 selected_atoms.sort_by_key(|ring| ring.len());
125 RingSet(selected_atoms)
126}
127
128fn bfs_spanning_forest(mol: &Molecule) -> (usize, Vec<Option<AtomIdx>>, Vec<u32>) {
140 let n = mol.atom_count();
141 let mut visited = vec![false; n];
142 let mut parent: Vec<Option<AtomIdx>> = vec![None; n];
143 let mut depth = vec![0u32; n];
144 let mut components = 0;
145 let mut queue: VecDeque<AtomIdx> = VecDeque::new();
146
147 for start in 0..n {
148 if visited[start] {
149 continue;
150 }
151 components += 1;
152 let start_idx = AtomIdx(start as u32);
153 visited[start] = true;
154 queue.push_back(start_idx);
155
156 while let Some(current) = queue.pop_front() {
157 for (neighbor, _bidx) in mol.neighbors(current) {
158 let ni = neighbor.0 as usize;
159 if !visited[ni] {
160 visited[ni] = true;
161 parent[ni] = Some(current);
162 depth[ni] = depth[current.0 as usize] + 1;
163 queue.push_back(neighbor);
164 }
165 }
166 }
167 }
168
169 (components, parent, depth)
170}
171
172fn fundamental_cycle(
182 mol: &Molecule,
183 u: AtomIdx,
184 v: AtomIdx,
185 bidx: BondIdx,
186 parent: &[Option<AtomIdx>],
187 depth: &[u32],
188) -> Option<(Vec<BondIdx>, Vec<AtomIdx>)> {
189 let (path_u, path_v) = paths_to_lca(u, v, parent, depth);
193
194 if path_u.is_empty() || path_v.is_empty() {
195 return None;
196 }
197
198 let mut ring_atoms: Vec<AtomIdx> = path_u.clone();
201 for &a in path_v.iter().rev().skip(1) {
203 ring_atoms.push(a);
204 }
205
206 let mut bond_set: Vec<BondIdx> = Vec::new();
208
209 for i in 0..path_u.len().saturating_sub(1) {
211 if let Some((b, _)) = mol.bond_between(path_u[i], path_u[i + 1]) {
212 bond_set.push(b);
213 } else {
214 return None;
215 }
216 }
217 for i in 0..path_v.len().saturating_sub(1) {
219 if let Some((b, _)) = mol.bond_between(path_v[i], path_v[i + 1]) {
220 bond_set.push(b);
221 } else {
222 return None;
223 }
224 }
225 bond_set.push(bidx);
227
228 bond_set.sort();
231 bond_set.dedup();
232
233 Some((bond_set, ring_atoms))
234}
235
236fn paths_to_lca(
241 u: AtomIdx,
242 v: AtomIdx,
243 parent: &[Option<AtomIdx>],
244 depth: &[u32],
245) -> (Vec<AtomIdx>, Vec<AtomIdx>) {
246 let ancestors_u = ancestors(u, parent);
248 let ancestors_v = ancestors(v, parent);
249
250 let set_u: HashMap<AtomIdx, usize> = ancestors_u
251 .iter()
252 .enumerate()
253 .map(|(i, &a)| (a, i))
254 .collect();
255
256 let mut lca = None;
258 let mut idx_in_v = 0;
259 for (i, &a) in ancestors_v.iter().enumerate() {
260 if set_u.contains_key(&a) {
261 lca = Some(a);
262 idx_in_v = i;
263 break;
264 }
265 }
266
267 let lca = match lca {
268 Some(a) => a,
269 None => {
270 return (Vec::new(), Vec::new());
273 }
274 };
275
276 let idx_in_u = *set_u.get(&lca).unwrap();
277
278 let path_u = ancestors_u[..=idx_in_u].to_vec();
279 let path_v = ancestors_v[..=idx_in_v].to_vec();
280
281 let _ = depth;
283
284 (path_u, path_v)
285}
286
287fn ancestors(start: AtomIdx, parent: &[Option<AtomIdx>]) -> Vec<AtomIdx> {
290 let mut chain = Vec::new();
291 let mut current = start;
292 loop {
293 chain.push(current);
294 match parent[current.0 as usize] {
295 Some(p) => current = p,
296 None => break,
297 }
298 }
299 chain
300}
301
302fn gf2_reduce(cycle: &[BondIdx], basis: &HashMap<BondIdx, Vec<BondIdx>>) -> Vec<BondIdx> {
313 let mut current: Vec<BondIdx> = cycle.to_vec();
314
315 loop {
316 if current.is_empty() {
317 return current;
318 }
319 let pivot = *current.iter().min().unwrap();
320 match basis.get(&pivot) {
321 None => return current, Some(basis_row) => {
323 current = sym_diff(¤t, basis_row);
325 }
326 }
327 }
328}
329
330fn sym_diff(a: &[BondIdx], b: &[BondIdx]) -> Vec<BondIdx> {
332 let mut result = Vec::new();
333 let mut i = 0;
334 let mut j = 0;
335 while i < a.len() && j < b.len() {
336 match a[i].cmp(&b[j]) {
337 std::cmp::Ordering::Less => {
338 result.push(a[i]);
339 i += 1;
340 }
341 std::cmp::Ordering::Greater => {
342 result.push(b[j]);
343 j += 1;
344 }
345 std::cmp::Ordering::Equal => {
346 i += 1;
348 j += 1;
349 }
350 }
351 }
352 result.extend_from_slice(&a[i..]);
353 result.extend_from_slice(&b[j..]);
354 result
355}
356
357#[cfg(test)]
362mod tests {
363 use super::*;
364 use chematic_core::{Atom, BondOrder, Element, MoleculeBuilder};
365
366 fn cyclohexane() -> chematic_core::Molecule {
368 let mut b = MoleculeBuilder::new();
369 let atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::new(Element::C))).collect();
370 for i in 0..6 {
371 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Single).unwrap();
372 }
373 b.build()
374 }
375
376 fn benzene() -> chematic_core::Molecule {
378 let mut b = MoleculeBuilder::new();
379 let atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::new(Element::C))).collect();
380 for i in 0..6 {
381 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Single).unwrap();
382 }
383 b.build()
384 }
385
386 fn naphthalene() -> chematic_core::Molecule {
391 let mut b = MoleculeBuilder::new();
392 let atoms: Vec<_> = (0..10).map(|_| b.add_atom(Atom::new(Element::C))).collect();
393 let ring1 = [0usize, 1, 2, 3, 4, 9];
395 for i in 0..6 {
396 b.add_bond(atoms[ring1[i]], atoms[ring1[(i + 1) % 6]], BondOrder::Single).unwrap();
397 }
398 b.add_bond(atoms[4], atoms[5], BondOrder::Single).unwrap();
401 b.add_bond(atoms[5], atoms[6], BondOrder::Single).unwrap();
402 b.add_bond(atoms[6], atoms[7], BondOrder::Single).unwrap();
403 b.add_bond(atoms[7], atoms[8], BondOrder::Single).unwrap();
404 b.add_bond(atoms[8], atoms[9], BondOrder::Single).unwrap();
405 b.build()
406 }
407
408 fn norbornane() -> chematic_core::Molecule {
415 let mut b = MoleculeBuilder::new();
416 let atoms: Vec<_> = (0..7).map(|_| b.add_atom(Atom::new(Element::C))).collect();
417 b.add_bond(atoms[0], atoms[1], BondOrder::Single).unwrap();
419 b.add_bond(atoms[1], atoms[2], BondOrder::Single).unwrap();
420 b.add_bond(atoms[2], atoms[3], BondOrder::Single).unwrap();
421 b.add_bond(atoms[0], atoms[4], BondOrder::Single).unwrap();
423 b.add_bond(atoms[4], atoms[5], BondOrder::Single).unwrap();
424 b.add_bond(atoms[5], atoms[3], BondOrder::Single).unwrap();
425 b.add_bond(atoms[0], atoms[6], BondOrder::Single).unwrap();
427 b.add_bond(atoms[6], atoms[3], BondOrder::Single).unwrap();
428 b.build()
429 }
430
431 #[test]
432 fn test_cyclohexane_sssr() {
433 let mol = cyclohexane();
434 let rings = find_sssr(&mol);
435 assert_eq!(rings.ring_count(), 1, "cyclohexane has exactly 1 ring");
436 assert_eq!(rings.rings()[0].len(), 6, "cyclohexane ring has 6 atoms");
437 }
438
439 #[test]
440 fn test_benzene_sssr() {
441 let mol = benzene();
442 let rings = find_sssr(&mol);
443 assert_eq!(rings.ring_count(), 1, "benzene has exactly 1 ring");
444 assert_eq!(rings.rings()[0].len(), 6, "benzene ring has 6 atoms");
445 }
446
447 #[test]
448 fn test_naphthalene_sssr() {
449 let mol = naphthalene();
450 let rings = find_sssr(&mol);
451 assert_eq!(rings.ring_count(), 2, "naphthalene SSSR has 2 rings");
454 for ring in rings.rings() {
455 assert_eq!(ring.len(), 6, "each naphthalene SSSR ring has 6 atoms");
456 }
457 }
458
459 #[test]
460 fn test_norbornane_sssr() {
461 let mol = norbornane();
462 let rings = find_sssr(&mol);
463 assert_eq!(rings.ring_count(), 2, "norbornane SSSR has 2 rings");
465 for ring in rings.rings() {
467 assert_eq!(ring.len(), 5, "each norbornane SSSR ring has 5 atoms");
468 }
469 }
470
471 #[test]
472 fn test_acyclic_molecule() {
473 let mut b = MoleculeBuilder::new();
475 let c1 = b.add_atom(Atom::new(Element::C));
476 let c2 = b.add_atom(Atom::new(Element::C));
477 b.add_bond(c1, c2, BondOrder::Single).unwrap();
478 let mol = b.build();
479 let rings = find_sssr(&mol);
480 assert_eq!(rings.ring_count(), 0);
481 }
482
483 #[test]
484 fn test_contains_atom() {
485 let mol = cyclohexane();
486 let rings = find_sssr(&mol);
487 for i in 0..6u32 {
488 assert!(rings.contains_atom(AtomIdx(i)), "atom {} should be in a ring", i);
489 }
490 }
491
492 #[test]
493 fn test_atoms_in_ring_count_benzene() {
494 let mol = benzene();
495 let rings = find_sssr(&mol);
496 for i in 0..6u32 {
497 assert_eq!(
498 rings.atoms_in_ring_count(AtomIdx(i)),
499 1,
500 "each benzene atom is in exactly 1 ring"
501 );
502 }
503 }
504}