use std::collections::{HashSet, VecDeque};
use crate::system::atomistic::{AtomId, Atomistic};
#[derive(Debug, Clone)]
pub struct RotatableBond {
pub j: usize,
pub k: usize,
pub downstream: Vec<usize>,
}
pub fn detect_rotatable_bonds(graph: &Atomistic) -> Vec<(AtomId, AtomId)> {
let ring_bonds = find_ring_bonds(graph);
graph
.bonds()
.filter_map(|(bid, bond)| {
let a = bond.nodes[0];
let b = bond.nodes[1];
let order = bond
.props
.get("order")
.and_then(|v| v.as_f64())
.unwrap_or(1.0);
if (order - 1.0).abs() > 0.01 {
return None;
}
if graph.neighbors(a).count() <= 1 || graph.neighbors(b).count() <= 1 {
return None;
}
if ring_bonds.contains(&bid) {
return None;
}
Some((a, b))
})
.collect()
}
pub fn downstream_atoms(
j: AtomId,
k: AtomId,
graph: &Atomistic,
id_to_idx: &std::collections::HashMap<AtomId, usize>,
) -> Vec<usize> {
let mut visited = HashSet::new();
visited.insert(j); visited.insert(k);
let mut queue = VecDeque::new();
queue.push_back(k);
let mut result = vec![id_to_idx[&k]];
while let Some(current) = queue.pop_front() {
for neighbor in graph.neighbors(current) {
if visited.insert(neighbor) {
result.push(id_to_idx[&neighbor]);
queue.push_back(neighbor);
}
}
}
result
}
pub fn atom_id_to_index(graph: &Atomistic) -> std::collections::HashMap<AtomId, usize> {
graph
.atoms()
.enumerate()
.map(|(idx, (id, _))| (id, idx))
.collect()
}
pub fn detect_rotatable_bonds_with_downstream(graph: &Atomistic) -> Vec<RotatableBond> {
let id_to_idx = atom_id_to_index(graph);
let bond_pairs = detect_rotatable_bonds(graph);
bond_pairs
.into_iter()
.map(|(j_id, k_id)| {
let downstream = downstream_atoms(j_id, k_id, graph, &id_to_idx);
RotatableBond {
j: id_to_idx[&j_id],
k: id_to_idx[&k_id],
downstream,
}
})
.collect()
}
use crate::system::atomistic::BondId;
fn find_ring_bonds(graph: &Atomistic) -> HashSet<BondId> {
let mut ring_bonds = HashSet::new();
let atom_ids: Vec<AtomId> = graph.atoms().map(|(id, _)| id).collect();
let mut parent: std::collections::HashMap<AtomId, AtomId> =
atom_ids.iter().map(|&id| (id, id)).collect();
fn find(parent: &mut std::collections::HashMap<AtomId, AtomId>, x: AtomId) -> AtomId {
let p = parent[&x];
if p == x {
return x;
}
let root = find(parent, p);
parent.insert(x, root);
root
}
fn union(parent: &mut std::collections::HashMap<AtomId, AtomId>, a: AtomId, b: AtomId) {
let ra = find(parent, a);
let rb = find(parent, b);
if ra != rb {
parent.insert(ra, rb);
}
}
let mut ring_bond_endpoints: Vec<(AtomId, AtomId)> = Vec::new();
for (bid, bond) in graph.bonds() {
let a = bond.nodes[0];
let b = bond.nodes[1];
let ra = find(&mut parent, a);
let rb = find(&mut parent, b);
if ra == rb {
ring_bonds.insert(bid);
ring_bond_endpoints.push((a, b));
} else {
union(&mut parent, a, b);
}
}
for (a, b) in ring_bond_endpoints {
mark_ring_path(graph, a, b, &mut ring_bonds);
}
ring_bonds
}
fn mark_ring_path(graph: &Atomistic, a: AtomId, b: AtomId, ring_bonds: &mut HashSet<BondId>) {
let mut visited = HashSet::new();
visited.insert(a);
let mut queue: VecDeque<(AtomId, Vec<AtomId>)> = VecDeque::new();
queue.push_back((a, vec![a]));
while let Some((current, path)) = queue.pop_front() {
for neighbor in graph.neighbors(current) {
if current == a && neighbor == b && path.len() == 1 {
continue;
}
if visited.insert(neighbor) {
let mut new_path = path.clone();
new_path.push(neighbor);
if neighbor == b {
for window in new_path.windows(2) {
let u = window[0];
let v = window[1];
for (bid, bond) in graph.bonds() {
if (bond.nodes[0] == u && bond.nodes[1] == v)
|| (bond.nodes[0] == v && bond.nodes[1] == u)
{
ring_bonds.insert(bid);
}
}
}
return;
}
queue.push_back((neighbor, new_path));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::system::molgraph::Atom;
fn chain(n: usize) -> Atomistic {
let mut g = Atomistic::new();
let mut ids = Vec::new();
for _ in 0..n {
ids.push(g.add_atom(Atom::new()));
}
for i in 0..n - 1 {
g.add_bond(ids[i], ids[i + 1]).expect("add chain bond");
}
g
}
#[test]
fn test_chain_rotatable_bonds() {
let g = chain(5);
let bonds = detect_rotatable_bonds(&g);
assert_eq!(bonds.len(), 2);
}
#[test]
fn test_ring_no_rotatable_bonds() {
let mut g = Atomistic::new();
let a = g.add_atom(Atom::new());
let b = g.add_atom(Atom::new());
let c = g.add_atom(Atom::new());
g.add_bond(a, b).expect("add bond");
g.add_bond(b, c).expect("add bond");
g.add_bond(c, a).expect("add bond");
assert_eq!(detect_rotatable_bonds(&g).len(), 0);
}
#[test]
fn test_downstream_atoms_chain() {
let g = chain(5);
let id_to_idx = atom_id_to_index(&g);
let ids: Vec<AtomId> = g.atoms().map(|(id, _)| id).collect();
let ds = downstream_atoms(ids[1], ids[2], &g, &id_to_idx);
assert_eq!(ds.len(), 3);
assert!(ds.contains(&2));
assert!(ds.contains(&3));
assert!(ds.contains(&4));
}
#[test]
fn test_detect_with_downstream() {
let g = chain(5);
let bonds = detect_rotatable_bonds_with_downstream(&g);
assert_eq!(bonds.len(), 2);
for rb in &bonds {
assert!(!rb.downstream.is_empty());
assert!(rb.downstream.contains(&rb.k));
}
}
#[test]
fn test_two_atoms_no_rotatable() {
assert_eq!(detect_rotatable_bonds(&chain(2)).len(), 0);
}
#[test]
fn test_branched_molecule() {
let mut g = Atomistic::new();
let center = g.add_atom(Atom::new());
let b0 = g.add_atom(Atom::new());
let b0p = g.add_atom(Atom::new());
let b1 = g.add_atom(Atom::new());
let b1p = g.add_atom(Atom::new());
let b2 = g.add_atom(Atom::new());
let b2p = g.add_atom(Atom::new());
g.add_bond(center, b0).expect("add bond");
g.add_bond(b0, b0p).expect("add bond");
g.add_bond(center, b1).expect("add bond");
g.add_bond(b1, b1p).expect("add bond");
g.add_bond(center, b2).expect("add bond");
g.add_bond(b2, b2p).expect("add bond");
assert_eq!(detect_rotatable_bonds(&g).len(), 3);
}
}