use crate::sssr::RingSet;
use chematic_core::{AtomIdx, Molecule};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RingSystemKind {
Simple,
Fused,
Spiro,
Bridged,
}
#[derive(Debug, Clone)]
pub struct RingFamily {
pub ring_indices: Vec<usize>,
pub atoms: Vec<AtomIdx>,
pub kind: RingSystemKind,
}
pub fn find_ring_families(mol: &Molecule, sssr: &RingSet) -> Vec<RingFamily> {
find_ring_families_over(mol, sssr.rings())
}
pub fn find_ring_families_over(mol: &Molecule, rings: &[Vec<AtomIdx>]) -> Vec<RingFamily> {
if rings.is_empty() {
return vec![];
}
let mut parent: Vec<usize> = (0..rings.len()).collect();
fn find(parent: &mut [usize], x: usize) -> usize {
if parent[x] != x {
parent[x] = find(parent, parent[x]);
}
parent[x]
}
fn union(parent: &mut [usize], x: usize, y: usize) {
let px = find(parent, x);
let py = find(parent, y);
if px != py {
parent[px] = py;
}
}
for i in 0..rings.len() {
for j in (i + 1)..rings.len() {
if rings[i].iter().any(|a| rings[j].contains(a)) {
union(&mut parent, i, j);
}
}
}
let mut groups: std::collections::HashMap<usize, Vec<usize>> = std::collections::HashMap::new();
for i in 0..rings.len() {
let root = find(&mut parent, i);
groups.entry(root).or_default().push(i);
}
let mut families = Vec::new();
for ring_indices in groups.into_values() {
let mut atoms = std::collections::HashSet::new();
for &idx in &ring_indices {
for &atom in &rings[idx] {
atoms.insert(atom);
}
}
let mut atoms: Vec<AtomIdx> = atoms.into_iter().collect();
atoms.sort_by_key(|a| a.0);
let kind = classify_ring_system(mol, &ring_indices, rings);
families.push(RingFamily {
ring_indices,
atoms,
kind,
});
}
families.sort_by_key(|f| f.ring_indices.iter().min().copied().unwrap_or(0));
families
}
fn classify_ring_system(
mol: &Molecule,
ring_indices: &[usize],
rings: &[Vec<AtomIdx>],
) -> RingSystemKind {
if ring_indices.len() == 1 {
return RingSystemKind::Simple;
}
let mut worst_kind = RingSystemKind::Spiro;
for i in 0..ring_indices.len() {
for j in (i + 1)..ring_indices.len() {
let ring_i = &rings[ring_indices[i]];
let ring_j = &rings[ring_indices[j]];
let shared: Vec<AtomIdx> = ring_i
.iter()
.filter(|a| ring_j.contains(a))
.copied()
.collect();
if shared.is_empty() {
continue; }
let kind = if shared.len() == 1 {
RingSystemKind::Spiro
} else if is_fused_ring(mol, &shared) {
RingSystemKind::Fused
} else {
RingSystemKind::Bridged
};
worst_kind = worst_classification(worst_kind, kind);
}
}
worst_kind
}
fn is_fused_ring(mol: &Molecule, atoms: &[AtomIdx]) -> bool {
if atoms.len() <= 1 {
return true; }
if atoms.len() == 2 {
return mol.bond_between(atoms[0], atoms[1]).is_some();
}
let ordered = order_by_adjacency(mol, atoms);
let Some(path) = ordered else {
return mol.bond_between(atoms[atoms.len() - 1], atoms[0]).is_some();
};
mol.bond_between(path[path.len() - 1], path[0]).is_some()
}
fn order_by_adjacency(mol: &Molecule, atoms: &[AtomIdx]) -> Option<Vec<AtomIdx>> {
let set: std::collections::HashSet<AtomIdx> = atoms.iter().copied().collect();
let neighbors_in_set = |a: AtomIdx| -> Vec<AtomIdx> {
mol.neighbors(a)
.filter(|(nb, _)| set.contains(nb))
.map(|(nb, _)| nb)
.collect()
};
let degrees: Vec<(AtomIdx, usize)> = atoms
.iter()
.map(|&a| (a, neighbors_in_set(a).len()))
.collect();
if degrees.iter().any(|&(_, d)| d > 2) {
return None; }
let endpoints: Vec<AtomIdx> = degrees
.iter()
.filter(|&&(_, d)| d == 1)
.map(|&(a, _)| a)
.collect();
if endpoints.len() != 2 {
return None; }
let mut path = vec![endpoints[0]];
let mut prev = None;
let mut current = endpoints[0];
while path.len() < atoms.len() {
let next = neighbors_in_set(current)
.into_iter()
.find(|&nb| Some(nb) != prev)?;
path.push(next);
prev = Some(current);
current = next;
}
if path.len() == atoms.len() && *path.last().unwrap() == endpoints[1] {
Some(path)
} else {
None }
}
fn worst_classification(a: RingSystemKind, b: RingSystemKind) -> RingSystemKind {
use RingSystemKind::*;
match (a, b) {
(Bridged, _) | (_, Bridged) => Bridged,
(Fused, _) | (_, Fused) => Fused,
_ => Spiro,
}
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
#[test]
fn test_ring_families_benzene() {
let mol = parse("c1ccccc1").unwrap();
let sssr = crate::sssr::find_sssr(&mol);
let families = find_ring_families(&mol, &sssr);
assert_eq!(families.len(), 1);
assert_eq!(families[0].kind, RingSystemKind::Simple);
assert_eq!(families[0].ring_indices, vec![0]);
}
#[test]
fn test_ring_families_naphthalene() {
let mol = parse("c1ccc2ccccc2c1").unwrap(); let sssr = crate::sssr::find_sssr(&mol);
let families = find_ring_families(&mol, &sssr);
assert_eq!(families.len(), 1);
assert_eq!(families[0].kind, RingSystemKind::Fused);
assert_eq!(families[0].ring_indices.len(), 2);
}
#[test]
fn test_ring_families_biphenyl() {
let mol = parse("c1ccccc1c1ccccc1").unwrap(); let sssr = crate::sssr::find_sssr(&mol);
let families = find_ring_families(&mol, &sssr);
assert_eq!(families.len(), 2);
assert!(families.iter().all(|f| f.kind == RingSystemKind::Simple));
}
#[test]
fn test_ring_families_spiro() {
let mol = parse("C1CCC2(C1)CCC2").unwrap(); let sssr = crate::sssr::find_sssr(&mol);
let families = find_ring_families(&mol, &sssr);
assert_eq!(families.len(), 1);
assert_eq!(families[0].kind, RingSystemKind::Spiro);
}
#[test]
fn test_ring_families_norbornane() {
let mol = parse("C1CC2CCC1CC2").unwrap(); let sssr = crate::sssr::find_sssr(&mol);
let families = find_ring_families(&mol, &sssr);
assert_eq!(families.len(), 1);
assert_eq!(families[0].kind, RingSystemKind::Bridged);
assert_eq!(families[0].ring_indices.len(), 2);
}
#[test]
fn test_ring_families_adamantane() {
let mol = parse("C1C2CC3CC1CC(C2)C3").unwrap(); let sssr = crate::sssr::find_sssr(&mol);
let families = find_ring_families(&mol, &sssr);
assert_eq!(families.len(), 1);
assert_eq!(families[0].kind, RingSystemKind::Bridged);
}
#[test]
fn test_ring_families_caffeine() {
let mol = parse("CN1C=NC2=C1C(=O)N(C(=O)N2C)C").unwrap();
let sssr = crate::sssr::find_sssr(&mol);
let families = find_ring_families(&mol, &sssr);
assert_eq!(families.len(), 1);
assert_eq!(families[0].kind, RingSystemKind::Fused);
}
#[test]
fn test_ring_families_acyclic() {
let mol = parse("CCCC").unwrap();
let sssr = crate::sssr::find_sssr(&mol);
let families = find_ring_families(&mol, &sssr);
assert_eq!(families.len(), 0);
}
}