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_spanning_forest(mol);
69 let r = (e as isize) - (v as isize) + (components as isize);
70
71 if r <= 0 {
72 return RingSet(Vec::new());
73 }
74 let r = r as usize;
75
76 let mut candidate_cycles: Vec<(Vec<BondIdx>, Vec<AtomIdx>)> = Vec::new();
78
79 for (bidx, bond) in mol.bonds() {
80 let u = bond.atom1;
81 let v_atom = bond.atom2;
82
83 let u_parent = parent[u.0 as usize];
86 let v_parent = parent[v_atom.0 as usize];
87
88 let is_tree_edge = (u_parent == Some(v_atom)) || (v_parent == Some(u));
89
90 if !is_tree_edge {
91 if let Some((bond_set, atom_seq)) = fundamental_cycle(mol, u, v_atom, bidx, &parent) {
93 candidate_cycles.push((bond_set, atom_seq));
94 }
95 }
96 }
97
98 candidate_cycles.sort_by_key(|(bonds, _)| bonds.len());
100
101 let mut basis: HashMap<BondIdx, Vec<BondIdx>> = HashMap::new();
104 let mut selected_atoms: Vec<Vec<AtomIdx>> = Vec::new();
105
106 for (bond_set, atom_seq) in candidate_cycles {
107 let reduced = gf2_reduce(&bond_set, &basis);
109
110 if !reduced.is_empty() {
111 let pivot = *reduced.iter().min().unwrap();
113 basis.insert(pivot, reduced);
114 selected_atoms.push(atom_seq);
115
116 if selected_atoms.len() == r {
117 break;
118 }
119 }
120 }
121
122 selected_atoms.sort_by_key(|ring| ring.len());
124 RingSet(selected_atoms)
125}
126
127fn bfs_spanning_forest(mol: &Molecule) -> (usize, Vec<Option<AtomIdx>>) {
138 let n = mol.atom_count();
139 let mut visited = vec![false; n];
140 let mut parent: Vec<Option<AtomIdx>> = vec![None; n];
141 let mut components = 0;
142 let mut queue: VecDeque<AtomIdx> = VecDeque::new();
143
144 for start in 0..n {
145 if visited[start] {
146 continue;
147 }
148 components += 1;
149 let start_idx = AtomIdx(start as u32);
150 visited[start] = true;
151 queue.push_back(start_idx);
152
153 while let Some(current) = queue.pop_front() {
154 for (neighbor, _bidx) in mol.neighbors(current) {
155 let ni = neighbor.0 as usize;
156 if !visited[ni] {
157 visited[ni] = true;
158 parent[ni] = Some(current);
159 queue.push_back(neighbor);
160 }
161 }
162 }
163 }
164
165 (components, parent)
166}
167
168fn fundamental_cycle(
178 mol: &Molecule,
179 u: AtomIdx,
180 v: AtomIdx,
181 bidx: BondIdx,
182 parent: &[Option<AtomIdx>],
183) -> Option<(Vec<BondIdx>, Vec<AtomIdx>)> {
184 let (path_u, path_v) = paths_to_lca(u, v, parent);
188
189 if path_u.is_empty() || path_v.is_empty() {
190 return None;
191 }
192
193 let mut ring_atoms: Vec<AtomIdx> = path_u.clone();
196 for &a in path_v.iter().rev().skip(1) {
198 ring_atoms.push(a);
199 }
200
201 let mut bond_set: Vec<BondIdx> = Vec::new();
203
204 for i in 0..path_u.len().saturating_sub(1) {
206 if let Some((b, _)) = mol.bond_between(path_u[i], path_u[i + 1]) {
207 bond_set.push(b);
208 } else {
209 return None;
210 }
211 }
212 for i in 0..path_v.len().saturating_sub(1) {
214 if let Some((b, _)) = mol.bond_between(path_v[i], path_v[i + 1]) {
215 bond_set.push(b);
216 } else {
217 return None;
218 }
219 }
220 bond_set.push(bidx);
222
223 bond_set.sort();
226 bond_set.dedup();
227
228 Some((bond_set, ring_atoms))
229}
230
231fn paths_to_lca(
236 u: AtomIdx,
237 v: AtomIdx,
238 parent: &[Option<AtomIdx>],
239) -> (Vec<AtomIdx>, Vec<AtomIdx>) {
240 let ancestors_u = ancestors(u, parent);
242 let ancestors_v = ancestors(v, parent);
243
244 let set_u: HashMap<AtomIdx, usize> = ancestors_u
245 .iter()
246 .enumerate()
247 .map(|(i, &a)| (a, i))
248 .collect();
249
250 let Some((idx_in_v, lca)) = ancestors_v
252 .iter()
253 .enumerate()
254 .find_map(|(i, a)| set_u.contains_key(a).then_some((i, *a)))
255 else {
256 return (Vec::new(), Vec::new());
259 };
260
261 let idx_in_u = set_u[&lca];
262 let path_u = ancestors_u[..=idx_in_u].to_vec();
263 let path_v = ancestors_v[..=idx_in_v].to_vec();
264 (path_u, path_v)
265}
266
267fn ancestors(start: AtomIdx, parent: &[Option<AtomIdx>]) -> Vec<AtomIdx> {
270 let mut chain = Vec::new();
271 let mut current = start;
272 loop {
273 chain.push(current);
274 match parent[current.0 as usize] {
275 Some(p) => current = p,
276 None => break,
277 }
278 }
279 chain
280}
281
282fn gf2_reduce(cycle: &[BondIdx], basis: &HashMap<BondIdx, Vec<BondIdx>>) -> Vec<BondIdx> {
293 let mut current: Vec<BondIdx> = cycle.to_vec();
294 while let Some(&pivot) = current.iter().min() {
295 match basis.get(&pivot) {
296 None => return current, Some(basis_row) => current = sym_diff(¤t, basis_row),
299 }
300 }
301 current
302}
303
304fn sym_diff(a: &[BondIdx], b: &[BondIdx]) -> Vec<BondIdx> {
306 let mut result = Vec::new();
307 let mut i = 0;
308 let mut j = 0;
309 while i < a.len() && j < b.len() {
310 match a[i].cmp(&b[j]) {
311 std::cmp::Ordering::Less => {
312 result.push(a[i]);
313 i += 1;
314 }
315 std::cmp::Ordering::Greater => {
316 result.push(b[j]);
317 j += 1;
318 }
319 std::cmp::Ordering::Equal => {
320 i += 1;
322 j += 1;
323 }
324 }
325 }
326 result.extend_from_slice(&a[i..]);
327 result.extend_from_slice(&b[j..]);
328 result
329}
330
331#[cfg(test)]
336mod tests {
337 use super::*;
338 use chematic_core::{Atom, BondOrder, Element, MoleculeBuilder};
339
340 fn cyclohexane() -> chematic_core::Molecule {
342 let mut b = MoleculeBuilder::new();
343 let atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::new(Element::C))).collect();
344 for i in 0..6 {
345 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Single)
346 .unwrap();
347 }
348 b.build()
349 }
350
351 fn benzene() -> chematic_core::Molecule {
353 let mut b = MoleculeBuilder::new();
354 let atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::new(Element::C))).collect();
355 for i in 0..6 {
356 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Single)
357 .unwrap();
358 }
359 b.build()
360 }
361
362 fn naphthalene() -> chematic_core::Molecule {
367 let mut b = MoleculeBuilder::new();
368 let atoms: Vec<_> = (0..10).map(|_| b.add_atom(Atom::new(Element::C))).collect();
369 let ring1 = [0usize, 1, 2, 3, 4, 9];
371 for i in 0..6 {
372 b.add_bond(
373 atoms[ring1[i]],
374 atoms[ring1[(i + 1) % 6]],
375 BondOrder::Single,
376 )
377 .unwrap();
378 }
379 b.add_bond(atoms[4], atoms[5], BondOrder::Single).unwrap();
382 b.add_bond(atoms[5], atoms[6], BondOrder::Single).unwrap();
383 b.add_bond(atoms[6], atoms[7], BondOrder::Single).unwrap();
384 b.add_bond(atoms[7], atoms[8], BondOrder::Single).unwrap();
385 b.add_bond(atoms[8], atoms[9], BondOrder::Single).unwrap();
386 b.build()
387 }
388
389 fn norbornane() -> chematic_core::Molecule {
396 let mut b = MoleculeBuilder::new();
397 let atoms: Vec<_> = (0..7).map(|_| b.add_atom(Atom::new(Element::C))).collect();
398 b.add_bond(atoms[0], atoms[1], BondOrder::Single).unwrap();
400 b.add_bond(atoms[1], atoms[2], BondOrder::Single).unwrap();
401 b.add_bond(atoms[2], atoms[3], BondOrder::Single).unwrap();
402 b.add_bond(atoms[0], atoms[4], BondOrder::Single).unwrap();
404 b.add_bond(atoms[4], atoms[5], BondOrder::Single).unwrap();
405 b.add_bond(atoms[5], atoms[3], BondOrder::Single).unwrap();
406 b.add_bond(atoms[0], atoms[6], BondOrder::Single).unwrap();
408 b.add_bond(atoms[6], atoms[3], BondOrder::Single).unwrap();
409 b.build()
410 }
411
412 #[test]
413 fn test_cyclohexane_sssr() {
414 let mol = cyclohexane();
415 let rings = find_sssr(&mol);
416 assert_eq!(rings.ring_count(), 1, "cyclohexane has exactly 1 ring");
417 assert_eq!(rings.rings()[0].len(), 6, "cyclohexane ring has 6 atoms");
418 }
419
420 #[test]
421 fn test_benzene_sssr() {
422 let mol = benzene();
423 let rings = find_sssr(&mol);
424 assert_eq!(rings.ring_count(), 1, "benzene has exactly 1 ring");
425 assert_eq!(rings.rings()[0].len(), 6, "benzene ring has 6 atoms");
426 }
427
428 #[test]
429 fn test_naphthalene_sssr() {
430 let mol = naphthalene();
431 let rings = find_sssr(&mol);
432 assert_eq!(rings.ring_count(), 2, "naphthalene SSSR has 2 rings");
435 for ring in rings.rings() {
436 assert_eq!(ring.len(), 6, "each naphthalene SSSR ring has 6 atoms");
437 }
438 }
439
440 #[test]
441 fn test_norbornane_sssr() {
442 let mol = norbornane();
443 let rings = find_sssr(&mol);
444 assert_eq!(rings.ring_count(), 2, "norbornane SSSR has 2 rings");
446 for ring in rings.rings() {
448 assert_eq!(ring.len(), 5, "each norbornane SSSR ring has 5 atoms");
449 }
450 }
451
452 #[test]
453 fn test_acyclic_molecule() {
454 let mut b = MoleculeBuilder::new();
456 let c1 = b.add_atom(Atom::new(Element::C));
457 let c2 = b.add_atom(Atom::new(Element::C));
458 b.add_bond(c1, c2, BondOrder::Single).unwrap();
459 let mol = b.build();
460 let rings = find_sssr(&mol);
461 assert_eq!(rings.ring_count(), 0);
462 }
463
464 #[test]
465 fn test_contains_atom() {
466 let mol = cyclohexane();
467 let rings = find_sssr(&mol);
468 for i in 0..6u32 {
469 assert!(
470 rings.contains_atom(AtomIdx(i)),
471 "atom {} should be in a ring",
472 i
473 );
474 }
475 }
476
477 #[test]
478 fn test_atoms_in_ring_count_benzene() {
479 let mol = benzene();
480 let rings = find_sssr(&mol);
481 for i in 0..6u32 {
482 assert_eq!(
483 rings.atoms_in_ring_count(AtomIdx(i)),
484 1,
485 "each benzene atom is in exactly 1 ring"
486 );
487 }
488 }
489
490 fn anthracene() -> chematic_core::Molecule {
493 let mut b = MoleculeBuilder::new();
494 let atoms: Vec<_> = (0..14).map(|_| b.add_atom(Atom::new(Element::C))).collect();
495 b.add_bond(atoms[0], atoms[1], BondOrder::Single).unwrap();
497 b.add_bond(atoms[1], atoms[2], BondOrder::Single).unwrap();
498 b.add_bond(atoms[2], atoms[3], BondOrder::Single).unwrap();
499 b.add_bond(atoms[3], atoms[8], BondOrder::Single).unwrap();
500 b.add_bond(atoms[8], atoms[9], BondOrder::Single).unwrap();
501 b.add_bond(atoms[9], atoms[0], BondOrder::Single).unwrap();
502 b.add_bond(atoms[3], atoms[4], BondOrder::Single).unwrap();
504 b.add_bond(atoms[4], atoms[5], BondOrder::Single).unwrap();
505 b.add_bond(atoms[5], atoms[6], BondOrder::Single).unwrap();
506 b.add_bond(atoms[6], atoms[7], BondOrder::Single).unwrap();
507 b.add_bond(atoms[7], atoms[8], BondOrder::Single).unwrap();
508 b.add_bond(atoms[7], atoms[10], BondOrder::Single).unwrap();
510 b.add_bond(atoms[10], atoms[11], BondOrder::Single).unwrap();
511 b.add_bond(atoms[11], atoms[12], BondOrder::Single).unwrap();
512 b.add_bond(atoms[12], atoms[13], BondOrder::Single).unwrap();
513 b.add_bond(atoms[13], atoms[6], BondOrder::Single).unwrap();
514 b.build()
515 }
516
517 fn spiro_nonane() -> chematic_core::Molecule {
520 let mut b = MoleculeBuilder::new();
521 let atoms: Vec<_> = (0..9).map(|_| b.add_atom(Atom::new(Element::C))).collect();
522 b.add_bond(atoms[0], atoms[1], BondOrder::Single).unwrap();
525 b.add_bond(atoms[1], atoms[2], BondOrder::Single).unwrap();
526 b.add_bond(atoms[2], atoms[3], BondOrder::Single).unwrap();
527 b.add_bond(atoms[3], atoms[4], BondOrder::Single).unwrap();
528 b.add_bond(atoms[4], atoms[0], BondOrder::Single).unwrap();
529 b.add_bond(atoms[0], atoms[5], BondOrder::Single).unwrap();
531 b.add_bond(atoms[5], atoms[6], BondOrder::Single).unwrap();
532 b.add_bond(atoms[6], atoms[7], BondOrder::Single).unwrap();
533 b.add_bond(atoms[7], atoms[8], BondOrder::Single).unwrap();
534 b.add_bond(atoms[8], atoms[0], BondOrder::Single).unwrap();
535 b.build()
536 }
537
538 fn dodecane_ring() -> chematic_core::Molecule {
540 let mut b = MoleculeBuilder::new();
541 let atoms: Vec<_> = (0..12).map(|_| b.add_atom(Atom::new(Element::C))).collect();
542 for i in 0..12 {
543 b.add_bond(atoms[i], atoms[(i + 1) % 12], BondOrder::Single)
544 .unwrap();
545 }
546 b.build()
547 }
548
549 fn disconnected_rings() -> chematic_core::Molecule {
551 let mut b = MoleculeBuilder::new();
552 let benzene_atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::new(Element::C))).collect();
554 for i in 0..6 {
555 b.add_bond(
556 benzene_atoms[i],
557 benzene_atoms[(i + 1) % 6],
558 BondOrder::Single,
559 )
560 .unwrap();
561 }
562 let hexane_atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::new(Element::C))).collect();
564 for i in 0..6 {
565 b.add_bond(
566 hexane_atoms[i],
567 hexane_atoms[(i + 1) % 6],
568 BondOrder::Single,
569 )
570 .unwrap();
571 }
572 b.build()
573 }
574
575 fn adamantane() -> chematic_core::Molecule {
578 let mut b = MoleculeBuilder::new();
579 let atoms: Vec<_> = (0..10).map(|_| b.add_atom(Atom::new(Element::C))).collect();
580 b.add_bond(atoms[0], atoms[1], BondOrder::Single).unwrap();
583 b.add_bond(atoms[1], atoms[2], BondOrder::Single).unwrap();
584 b.add_bond(atoms[2], atoms[5], BondOrder::Single).unwrap();
585 b.add_bond(atoms[0], atoms[3], BondOrder::Single).unwrap();
587 b.add_bond(atoms[3], atoms[4], BondOrder::Single).unwrap();
588 b.add_bond(atoms[4], atoms[5], BondOrder::Single).unwrap();
589 b.add_bond(atoms[0], atoms[6], BondOrder::Single).unwrap();
591 b.add_bond(atoms[6], atoms[7], BondOrder::Single).unwrap();
592 b.add_bond(atoms[7], atoms[5], BondOrder::Single).unwrap();
593 b.add_bond(atoms[1], atoms[3], BondOrder::Single).unwrap();
596 b.add_bond(atoms[2], atoms[4], BondOrder::Single).unwrap();
597 b.build()
598 }
599
600 #[test]
601 fn test_anthracene_sssr() {
602 let mol = anthracene();
603 let rings = find_sssr(&mol);
604 assert_eq!(rings.ring_count(), 3, "anthracene SSSR has 3 rings");
606 let all_ring_atoms: std::collections::HashSet<_> = rings
609 .rings()
610 .iter()
611 .flat_map(|r| r.iter().copied())
612 .collect();
613 assert!(
614 all_ring_atoms.len() >= 10,
615 "anthracene SSSR atoms cover most of the structure"
616 );
617 }
618
619 #[test]
620 fn test_spiro_nonane_sssr() {
621 let mol = spiro_nonane();
622 let rings = find_sssr(&mol);
623 assert_eq!(rings.ring_count(), 2, "spiro[4.4]nonane SSSR has 2 rings");
627 for ring in rings.rings() {
628 assert_eq!(ring.len(), 5, "each spiro nonane SSSR ring is 5-membered");
629 }
630 }
631
632 #[test]
633 fn test_dodecane_ring_sssr() {
634 let mol = dodecane_ring();
635 let rings = find_sssr(&mol);
636 assert_eq!(rings.ring_count(), 1, "12-membered ring has 1 SSSR entry");
637 assert_eq!(
638 rings.rings()[0].len(),
639 12,
640 "12-membered ring SSSR has 12 atoms"
641 );
642 }
643
644 #[test]
645 fn test_disconnected_rings_sssr() {
646 let mol = disconnected_rings();
647 let rings = find_sssr(&mol);
648 assert_eq!(
650 rings.ring_count(),
651 2,
652 "two disconnected rings yield 2 SSSR entries"
653 );
654 let sizes: Vec<_> = rings.rings().iter().map(|r| r.len()).collect();
655 assert!(sizes.contains(&6), "one ring should be 6-membered");
656 }
657
658 #[test]
659 fn test_adamantane_sssr() {
660 let mol = adamantane();
661 let rings = find_sssr(&mol);
662 assert!(
666 rings.ring_count() >= 3,
667 "adamantane SSSR has at least 3 rings"
668 );
669 for ring in rings.rings() {
671 assert!(!ring.is_empty(), "each ring should have atoms");
672 assert!(ring.len() <= 10, "ring should not exceed molecule size");
673 }
674 }
675
676 #[test]
677 fn test_macrocycle_atom_in_ring_count() {
678 let mol = dodecane_ring();
679 let rings = find_sssr(&mol);
680 for i in 0..12u32 {
681 assert_eq!(
682 rings.atoms_in_ring_count(AtomIdx(i)),
683 1,
684 "each dodecane atom is in exactly 1 ring"
685 );
686 }
687 }
688}