use crate::bond::BondOrder;
use crate::molecule::Molecule;
pub fn ring_count(mol: &Molecule) -> usize {
if mol.atoms.is_empty() {
return 0;
}
let num_atoms = mol.atoms.len();
let num_bonds = mol.bonds.len();
let num_components = connected_component_count(mol);
if num_bonds + num_components > num_atoms {
num_bonds - num_atoms + num_components
} else {
0
}
}
pub fn ring_atoms(mol: &Molecule) -> Vec<bool> {
let (atom_in_ring, _) = compute_ring_membership(mol);
atom_in_ring
}
pub fn ring_bonds(mol: &Molecule) -> Vec<bool> {
let (_, bond_in_ring) = compute_ring_membership(mol);
bond_in_ring
}
pub fn rotatable_bond_count(mol: &Molecule) -> usize {
if mol.bonds.is_empty() {
return 0;
}
let (_, bond_in_ring) = compute_ring_membership(mol);
let heavy_degree = compute_heavy_atom_degrees(mol);
mol.bonds
.iter()
.enumerate()
.filter(|(i, bond)| {
if bond.order != BondOrder::Single {
return false;
}
if bond_in_ring[*i] {
return false;
}
let atom1_elem = mol.atoms.get(bond.atom1).map(|a| a.element.as_str());
let atom2_elem = mol.atoms.get(bond.atom2).map(|a| a.element.as_str());
if is_hydrogen(atom1_elem) || is_hydrogen(atom2_elem) {
return false;
}
let hdeg1 = heavy_degree.get(bond.atom1).copied().unwrap_or(0);
let hdeg2 = heavy_degree.get(bond.atom2).copied().unwrap_or(0);
if hdeg1 <= 1 || hdeg2 <= 1 {
return false;
}
true
})
.count()
}
fn connected_component_count(mol: &Molecule) -> usize {
if mol.atoms.is_empty() {
return 0;
}
let n = mol.atoms.len();
let mut parent: Vec<usize> = (0..n).collect();
let mut rank = vec![0usize; n];
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], rank: &mut [usize], x: usize, y: usize) {
let px = find(parent, x);
let py = find(parent, y);
if px != py {
if rank[px] < rank[py] {
parent[px] = py;
} else if rank[px] > rank[py] {
parent[py] = px;
} else {
parent[py] = px;
rank[px] += 1;
}
}
}
for bond in &mol.bonds {
if bond.atom1 < n && bond.atom2 < n {
union(&mut parent, &mut rank, bond.atom1, bond.atom2);
}
}
let mut roots = std::collections::HashSet::new();
for i in 0..n {
roots.insert(find(&mut parent, i));
}
roots.len()
}
fn compute_ring_membership(mol: &Molecule) -> (Vec<bool>, Vec<bool>) {
let n = mol.atoms.len();
let m = mol.bonds.len();
if n == 0 {
return (vec![], vec![]);
}
let mut atom_in_ring = vec![false; n];
let mut bond_in_ring = vec![false; m];
let mut adj: Vec<Vec<(usize, usize)>> = vec![vec![]; n]; for (bond_idx, bond) in mol.bonds.iter().enumerate() {
if bond.atom1 < n && bond.atom2 < n {
adj[bond.atom1].push((bond.atom2, bond_idx));
adj[bond.atom2].push((bond.atom1, bond_idx));
}
}
let mut visited = vec![false; n];
let mut in_stack = vec![false; n];
let mut parent = vec![usize::MAX; n];
let mut parent_bond = vec![usize::MAX; n];
for start in 0..n {
if visited[start] {
continue;
}
let mut stack: Vec<(usize, usize, bool)> = vec![(start, 0, true)];
while let Some((node, next_neighbor_idx, entering)) = stack.pop() {
if entering {
if visited[node] {
continue;
}
visited[node] = true;
in_stack[node] = true;
stack.push((node, 0, false));
} else {
if next_neighbor_idx < adj[node].len() {
let (neighbor, bond_idx) = adj[node][next_neighbor_idx];
stack.push((node, next_neighbor_idx + 1, false));
if parent_bond[node] == bond_idx {
continue;
}
if !visited[neighbor] {
parent[neighbor] = node;
parent_bond[neighbor] = bond_idx;
stack.push((neighbor, 0, true));
} else if in_stack[neighbor] {
mark_cycle_path(
node,
neighbor,
bond_idx,
&parent,
&parent_bond,
&mut atom_in_ring,
&mut bond_in_ring,
);
}
} else {
in_stack[node] = false;
}
}
}
}
(atom_in_ring, bond_in_ring)
}
fn mark_cycle_path(
u: usize,
v: usize,
back_edge_bond: usize,
parent: &[usize],
parent_bond: &[usize],
atom_in_ring: &mut [bool],
bond_in_ring: &mut [bool],
) {
bond_in_ring[back_edge_bond] = true;
let mut current = u;
while current != v && current != usize::MAX {
atom_in_ring[current] = true;
if parent_bond[current] != usize::MAX {
bond_in_ring[parent_bond[current]] = true;
}
current = parent[current];
}
if current == v {
atom_in_ring[v] = true;
}
}
#[allow(dead_code)]
fn compute_degrees(mol: &Molecule) -> Vec<usize> {
let n = mol.atoms.len();
let mut degrees = vec![0usize; n];
for bond in &mol.bonds {
if bond.atom1 < n {
degrees[bond.atom1] += 1;
}
if bond.atom2 < n {
degrees[bond.atom2] += 1;
}
}
degrees
}
fn compute_heavy_atom_degrees(mol: &Molecule) -> Vec<usize> {
let n = mol.atoms.len();
let mut degrees = vec![0usize; n];
for bond in &mol.bonds {
if bond.atom1 < n && bond.atom2 < n {
let elem1 = mol.atoms.get(bond.atom1).map(|a| a.element.as_str());
let elem2 = mol.atoms.get(bond.atom2).map(|a| a.element.as_str());
if !is_hydrogen(elem2) {
degrees[bond.atom1] += 1;
}
if !is_hydrogen(elem1) {
degrees[bond.atom2] += 1;
}
}
}
degrees
}
fn is_hydrogen(element: Option<&str>) -> bool {
match element {
Some(e) => {
let e = e.trim().to_uppercase();
e == "H" || e == "D" || e == "T"
}
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atom::Atom;
use crate::bond::Bond;
fn make_benzene() -> Molecule {
let mut mol = Molecule::new("benzene");
for i in 0..6 {
let angle = std::f64::consts::PI * 2.0 * i as f64 / 6.0;
mol.atoms
.push(Atom::new(i, "C", angle.cos(), angle.sin(), 0.0));
}
for i in 0..6 {
mol.bonds
.push(Bond::new(i, (i + 1) % 6, BondOrder::Aromatic));
}
mol
}
fn make_propane() -> Molecule {
let mut mol = Molecule::new("propane");
mol.atoms.push(Atom::new(0, "C", 0.0, 0.0, 0.0));
mol.atoms.push(Atom::new(1, "C", 1.5, 0.0, 0.0));
mol.atoms.push(Atom::new(2, "C", 3.0, 0.0, 0.0));
for i in 3..11 {
mol.atoms.push(Atom::new(i, "H", 0.0, 0.0, 0.0));
}
mol.bonds.push(Bond::new(0, 1, BondOrder::Single));
mol.bonds.push(Bond::new(1, 2, BondOrder::Single));
mol.bonds.push(Bond::new(0, 3, BondOrder::Single));
mol.bonds.push(Bond::new(0, 4, BondOrder::Single));
mol.bonds.push(Bond::new(0, 5, BondOrder::Single));
mol.bonds.push(Bond::new(1, 6, BondOrder::Single));
mol.bonds.push(Bond::new(1, 7, BondOrder::Single));
mol.bonds.push(Bond::new(2, 8, BondOrder::Single));
mol.bonds.push(Bond::new(2, 9, BondOrder::Single));
mol.bonds.push(Bond::new(2, 10, BondOrder::Single));
mol
}
fn make_naphthalene() -> Molecule {
let mut mol = Molecule::new("naphthalene");
for i in 0..10 {
mol.atoms.push(Atom::new(i, "C", 0.0, 0.0, 0.0));
}
mol.bonds.push(Bond::new(0, 1, BondOrder::Aromatic));
mol.bonds.push(Bond::new(1, 2, BondOrder::Aromatic));
mol.bonds.push(Bond::new(2, 3, BondOrder::Aromatic));
mol.bonds.push(Bond::new(3, 4, BondOrder::Aromatic));
mol.bonds.push(Bond::new(4, 5, BondOrder::Aromatic));
mol.bonds.push(Bond::new(5, 0, BondOrder::Aromatic));
mol.bonds.push(Bond::new(4, 6, BondOrder::Aromatic));
mol.bonds.push(Bond::new(6, 7, BondOrder::Aromatic));
mol.bonds.push(Bond::new(7, 8, BondOrder::Aromatic));
mol.bonds.push(Bond::new(8, 9, BondOrder::Aromatic));
mol.bonds.push(Bond::new(9, 3, BondOrder::Aromatic));
mol
}
#[test]
fn test_ring_count_benzene() {
let mol = make_benzene();
assert_eq!(ring_count(&mol), 1);
}
#[test]
fn test_ring_count_propane() {
let mol = make_propane();
assert_eq!(ring_count(&mol), 0);
}
#[test]
fn test_ring_count_naphthalene() {
let mol = make_naphthalene();
assert_eq!(ring_count(&mol), 2);
}
#[test]
fn test_ring_count_empty() {
let mol = Molecule::new("empty");
assert_eq!(ring_count(&mol), 0);
}
#[test]
fn test_ring_count_single_atom() {
let mut mol = Molecule::new("single");
mol.atoms.push(Atom::new(0, "C", 0.0, 0.0, 0.0));
assert_eq!(ring_count(&mol), 0);
}
#[test]
fn test_ring_atoms_benzene() {
let mol = make_benzene();
let in_ring = ring_atoms(&mol);
assert_eq!(in_ring.len(), 6);
assert!(
in_ring.iter().all(|&r| r),
"All atoms in benzene should be in ring"
);
}
#[test]
fn test_ring_atoms_propane() {
let mol = make_propane();
let in_ring = ring_atoms(&mol);
assert!(
in_ring.iter().all(|&r| !r),
"No atoms in propane should be in ring"
);
}
#[test]
fn test_ring_bonds_benzene() {
let mol = make_benzene();
let in_ring = ring_bonds(&mol);
assert_eq!(in_ring.len(), 6);
assert!(
in_ring.iter().all(|&r| r),
"All bonds in benzene should be in ring"
);
}
#[test]
fn test_ring_bonds_propane() {
let mol = make_propane();
let in_ring = ring_bonds(&mol);
assert!(
in_ring.iter().all(|&r| !r),
"No bonds in propane should be in ring"
);
}
#[test]
fn test_rotatable_bond_count_benzene() {
let mol = make_benzene();
assert_eq!(rotatable_bond_count(&mol), 0);
}
#[test]
fn test_rotatable_bond_count_propane() {
let mol = make_propane();
assert_eq!(rotatable_bond_count(&mol), 0);
}
#[test]
fn test_rotatable_bond_count_empty() {
let mol = Molecule::new("empty");
assert_eq!(rotatable_bond_count(&mol), 0);
}
#[test]
fn test_rotatable_bond_count_ethane_skeleton() {
let mut mol = Molecule::new("ethane_skeleton");
mol.atoms.push(Atom::new(0, "C", 0.0, 0.0, 0.0));
mol.atoms.push(Atom::new(1, "C", 1.5, 0.0, 0.0));
mol.bonds.push(Bond::new(0, 1, BondOrder::Single));
assert_eq!(rotatable_bond_count(&mol), 0);
}
#[test]
fn test_connected_components_single() {
let mol = make_benzene();
assert_eq!(connected_component_count(&mol), 1);
}
#[test]
fn test_connected_components_multiple() {
let mut mol = Molecule::new("two_fragments");
mol.atoms.push(Atom::new(0, "C", 0.0, 0.0, 0.0));
mol.atoms.push(Atom::new(1, "C", 1.5, 0.0, 0.0));
mol.bonds.push(Bond::new(0, 1, BondOrder::Single));
mol.atoms.push(Atom::new(2, "O", 5.0, 0.0, 0.0));
assert_eq!(connected_component_count(&mol), 2);
}
#[test]
fn test_connected_components_empty() {
let mol = Molecule::new("empty");
assert_eq!(connected_component_count(&mol), 0);
}
}