use std::collections::{HashMap, HashSet, VecDeque};
use crate::system::atomistic::{AtomId, Atomistic};
use crate::system::topology::Topology;
#[derive(Debug, Clone)]
pub struct RotatableBond {
pub j: usize,
pub k: usize,
pub downstream: Vec<usize>,
}
pub fn atom_id_to_index(graph: &Atomistic) -> HashMap<AtomId, usize> {
graph
.atoms()
.enumerate()
.map(|(idx, (id, _))| (id, idx))
.collect()
}
fn build_topology(graph: &Atomistic, id_to_idx: &HashMap<AtomId, usize>) -> Topology {
let edges: Vec<[usize; 2]> = graph
.bonds()
.map(|(_, b)| [id_to_idx[&b.nodes[0]], id_to_idx[&b.nodes[1]]])
.collect();
Topology::from_edges(id_to_idx.len(), &edges)
}
fn scan_rotatable(graph: &Atomistic) -> (Vec<AtomId>, Topology, Vec<(usize, usize)>) {
let ids: Vec<AtomId> = graph.atoms().map(|(id, _)| id).collect();
let id_to_idx: HashMap<AtomId, usize> =
ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
let bonds: Vec<_> = graph.bonds().collect();
let edges: Vec<[usize; 2]> = bonds
.iter()
.map(|(_, b)| [id_to_idx[&b.nodes[0]], id_to_idx[&b.nodes[1]]])
.collect();
let topo = Topology::from_edges(ids.len(), &edges);
let ring_mask = topo.find_rings().bond_ring_mask(topo.n_bonds());
let rotatable = bonds
.iter()
.enumerate()
.filter_map(|(bi, (_, bond))| {
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;
}
let j = id_to_idx[&bond.nodes[0]];
let k = id_to_idx[&bond.nodes[1]];
if topo.degree(j) <= 1 || topo.degree(k) <= 1 {
return None;
}
if ring_mask[bi] {
return None;
}
Some((j, k))
})
.collect();
(ids, topo, rotatable)
}
fn downstream_indices(topo: &Topology, j: usize, k: 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![k];
while let Some(current) = queue.pop_front() {
for neighbor in topo.neighbors(current) {
if visited.insert(neighbor) {
result.push(neighbor);
queue.push_back(neighbor);
}
}
}
result
}
pub fn detect_rotatable_bonds(graph: &Atomistic) -> Vec<(AtomId, AtomId)> {
let (ids, _topo, rotatable) = scan_rotatable(graph);
rotatable
.into_iter()
.map(|(j, k)| (ids[j], ids[k]))
.collect()
}
pub fn downstream_atoms(
j: AtomId,
k: AtomId,
graph: &Atomistic,
id_to_idx: &HashMap<AtomId, usize>,
) -> Vec<usize> {
let topo = build_topology(graph, id_to_idx);
downstream_indices(&topo, id_to_idx[&j], id_to_idx[&k])
}
pub fn detect_rotatable_bonds_with_downstream(graph: &Atomistic) -> Vec<RotatableBond> {
let (_ids, topo, rotatable) = scan_rotatable(graph);
rotatable
.into_iter()
.map(|(j, k)| RotatableBond {
j,
k,
downstream: downstream_indices(&topo, j, k),
})
.collect()
}
#[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_fused_ring_no_rotatable_bonds() {
let mut g = Atomistic::new();
let v: Vec<_> = (0..10).map(|_| g.add_atom(Atom::new())).collect();
for (a, b) in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)] {
g.add_bond(v[a], v[b]).expect("add bond");
}
for (a, b) in [(1, 6), (6, 7), (7, 8), (8, 9), (9, 0)] {
g.add_bond(v[a], v[b]).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);
}
}