use std::{
collections::{HashSet, VecDeque},
f64::consts::PI,
};
use bio_files::BondType;
use dynamics::find_tetra_posits;
use lin_alg::f64::Vec3;
use na_seq::Element;
use crate::molecules::{
Atom, Bond,
common::{BondGeom, MoleculeCommon, find_appended_posit},
};
impl MoleculeCommon {
pub fn assign_posits(&mut self) {
let n = self.atoms.len();
if n == 0 {
return;
}
let mut positioned = vec![false; n];
let mut component_x = 0.0_f64;
loop {
let start = match (0..n).find(|&i| !positioned[i]) {
None => break,
Some(s) => s,
};
let offset = Vec3::new(component_x, 0., 0.);
let component = find_component(&self.adjacency_list, start, n);
let rings = find_rings_sssr(&component, &self.adjacency_list);
if rings.is_empty() {
self.atoms[start].posit = offset;
positioned[start] = true;
} else {
place_ring_systems(
&rings,
&mut self.atoms,
&mut positioned,
&self.bonds,
&self.adjacency_list,
offset,
);
}
let mut queue: VecDeque<usize> = component
.iter()
.copied()
.filter(|&a| positioned[a])
.collect();
bfs_place_substituents(
&mut queue,
&mut self.atoms,
&mut positioned,
&self.bonds,
&self.adjacency_list,
);
let max_x = component
.iter()
.map(|&i| self.atoms[i].posit.x)
.fold(component_x, f64::max);
component_x = max_x + 5.;
}
self.reset_posits();
}
pub fn cleanup_geometry(&mut self) {
let n = self.atoms.len();
if n == 0 {
return;
}
const LO: f64 = 0.6; const HI: f64 = 1.5;
let anchored: Vec<bool> = (0..n)
.map(|i| {
let mut ok = 0usize;
let mut total = 0usize;
for b in &self.bonds {
let j = if b.atom_0 == i {
b.atom_1
} else if b.atom_1 == i {
b.atom_0
} else {
continue;
};
total += 1;
let expected = estimate_bond_length(
self.atoms[i].element,
self.atoms[j].element,
b.bond_type,
);
let actual = (self.atoms[i].posit - self.atoms[j].posit).magnitude();
if actual >= expected * LO && actual <= expected * HI {
ok += 1;
}
}
total == 0 || ok * 2 >= total
})
.collect();
let any_anchored = anchored.iter().any(|&a| a);
if !any_anchored {
self.assign_posits();
return;
}
let mut positioned = anchored.clone();
let mut queue: VecDeque<usize> = (0..n).filter(|&i| anchored[i]).collect();
bfs_place_substituents(
&mut queue,
&mut self.atoms,
&mut positioned,
&self.bonds,
&self.adjacency_list,
);
self.reset_posits();
}
}
fn find_component(adj: &[Vec<usize>], start: usize, n: usize) -> Vec<usize> {
let mut visited = vec![false; n];
let mut out = Vec::new();
let mut q = VecDeque::new();
visited[start] = true;
q.push_back(start);
while let Some(u) = q.pop_front() {
out.push(u);
for &v in &adj[u] {
if !visited[v] {
visited[v] = true;
q.push_back(v);
}
}
}
out
}
fn find_rings_sssr(component: &[usize], adj: &[Vec<usize>]) -> Vec<Vec<usize>> {
let n_total = adj.len();
let mut visited = vec![false; n_total];
let mut in_stack = vec![false; n_total];
let mut back_edges: Vec<(usize, usize)> = Vec::new();
for &start in component {
if !visited[start] {
dfs_back_edges(
start,
usize::MAX,
adj,
&mut visited,
&mut in_stack,
&mut back_edges,
);
}
}
let mut rings: Vec<Vec<usize>> = Vec::new();
let mut seen: HashSet<Vec<usize>> = HashSet::new();
for (u, v) in back_edges {
let skip = (u.min(v), u.max(v));
if let Some(path) = bfs_shortest_path(v, u, skip, adj) {
let mut key = path.clone();
key.sort_unstable();
if seen.insert(key) {
rings.push(path);
}
}
}
rings
}
fn dfs_back_edges(
u: usize,
parent: usize,
adj: &[Vec<usize>],
visited: &mut Vec<bool>,
in_stack: &mut Vec<bool>,
back_edges: &mut Vec<(usize, usize)>,
) {
visited[u] = true;
in_stack[u] = true;
for &v in &adj[u] {
if v == parent {
continue;
}
if in_stack[v] {
back_edges.push((u, v));
} else if !visited[v] {
dfs_back_edges(v, u, adj, visited, in_stack, back_edges);
}
}
in_stack[u] = false;
}
fn bfs_shortest_path(
start: usize,
end: usize,
skip_edge: (usize, usize),
adj: &[Vec<usize>],
) -> Option<Vec<usize>> {
let n = adj.len();
let mut prev = vec![usize::MAX; n];
let mut visited = vec![false; n];
let mut q = VecDeque::new();
visited[start] = true;
q.push_back(start);
while let Some(u) = q.pop_front() {
if u == end {
let mut path = Vec::new();
let mut cur = end;
loop {
path.push(cur);
let p = prev[cur];
if p == usize::MAX {
break;
}
cur = p;
}
path.reverse();
return Some(path);
}
for &v in &adj[u] {
let e = (u.min(v), u.max(v));
if e == skip_edge || visited[v] {
continue;
}
visited[v] = true;
prev[v] = u;
q.push_back(v);
}
}
None
}
fn place_ring_systems(
rings: &[Vec<usize>],
atoms: &mut [Atom],
positioned: &mut Vec<bool>,
bonds: &[Bond],
adj: &[Vec<usize>],
offset: Vec3,
) {
let shared_counts: Vec<usize> = (0..rings.len())
.map(|ri| {
let set: HashSet<usize> = rings[ri].iter().copied().collect();
rings
.iter()
.enumerate()
.filter(|(rj, _)| *rj != ri)
.flat_map(|(_, other)| other.iter())
.filter(|&&a| set.contains(&a))
.count()
})
.collect();
let mut order: Vec<usize> = (0..rings.len()).collect();
order.sort_by(|&a, &b| {
shared_counts[b]
.cmp(&shared_counts[a])
.then(rings[a].len().cmp(&rings[b].len()))
});
let mut done = vec![false; rings.len()];
let mut progress = true;
while progress {
progress = false;
for &ri in &order {
if done[ri] {
continue;
}
let ring = &rings[ri];
let placed_count = ring.iter().filter(|&&a| positioned[a]).count();
if placed_count == ring.len() {
done[ri] = true;
progress = true;
continue;
}
if placed_count == 0 {
let has_external = ring
.iter()
.any(|&a| adj[a].iter().any(|&nb| positioned[nb]));
if has_external {
if place_ring_attached(ring, atoms, positioned, bonds, adj) {
done[ri] = true;
progress = true;
}
} else {
place_ring_regular(ring, atoms, positioned, bonds, offset);
done[ri] = true;
progress = true;
}
} else if place_ring_fused(ring, atoms, positioned) {
done[ri] = true;
progress = true;
} else if placed_count == 1 {
if place_ring_spiro(ring, atoms, positioned, bonds, adj) {
place_ring_fused(ring, atoms, positioned);
done[ri] = true;
progress = true;
}
}
}
}
}
fn place_ring_regular(
ring: &[usize],
atoms: &mut [Atom],
positioned: &mut Vec<bool>,
bonds: &[Bond],
center: Vec3,
) {
let n = ring.len();
if n == 0 {
return;
}
let bond_len = avg_ring_bond_len(ring, atoms, bonds);
let radius = ring_circumradius(bond_len, n);
for (i, &ai) in ring.iter().enumerate() {
let angle = PI / 2.0 - 2.0 * PI * i as f64 / n as f64;
atoms[ai].posit = center + Vec3::new(radius * angle.cos(), radius * angle.sin(), 0.);
positioned[ai] = true;
}
}
fn place_ring_fused(ring: &[usize], atoms: &mut [Atom], positioned: &mut [bool]) -> bool {
let n = ring.len();
let anchor = (0..n).find_map(|i| {
let j = (i + 1) % n;
let a = ring[i];
let b = ring[j];
if positioned[a] && positioned[b] {
Some((i, a, b))
} else {
None
}
});
let (i0, a0, a1) = match anchor {
Some(x) => x,
None => return false,
};
let pos0 = atoms[a0].posit;
let pos1 = atoms[a1].posit;
let edge_len = (pos1 - pos0).magnitude();
if edge_len < 1e-6 {
return false;
}
let radius = ring_circumradius(edge_len, n);
let edge_mid = (pos0 + pos1) * 0.5;
let edge_dir = (pos1 - pos0) * (1.0 / edge_len);
let perp = Vec3::new(-edge_dir.y, edge_dir.x, 0.0);
let half_edge = edge_len * 0.5;
let center_dist = (radius * radius - half_edge * half_edge).max(0.0).sqrt();
let (com_sum, nplaced) = ring
.iter()
.filter(|&&a| positioned[a])
.fold((Vec3::new(0., 0., 0.), 0usize), |(acc, cnt), &a| {
(acc + atoms[a].posit, cnt + 1)
});
let existing_com = if nplaced > 0 {
com_sum * (1.0 / nplaced as f64)
} else {
edge_mid
};
let new_center = if (existing_com - edge_mid).dot(perp) > 0.0 {
edge_mid - perp * center_dist
} else {
edge_mid + perp * center_dist
};
let theta0 = {
let d = pos0 - new_center;
d.y.atan2(d.x)
};
let theta1_actual = {
let d = pos1 - new_center;
d.y.atan2(d.x)
};
let step = 2.0 * PI / n as f64;
let step_sign: f64 =
if angle_diff(theta1_actual, theta0 + step) <= angle_diff(theta1_actual, theta0 - step) {
1.0
} else {
-1.0
};
for i in 0..n {
let ai = ring[i];
if positioned[ai] {
continue;
}
let steps = ((i as isize - i0 as isize).rem_euclid(n as isize)) as f64;
let angle = theta0 + steps * step * step_sign;
atoms[ai].posit = new_center + Vec3::new(radius * angle.cos(), radius * angle.sin(), 0.);
positioned[ai] = true;
}
true
}
fn place_ring_attached(
ring: &[usize],
atoms: &mut [Atom],
positioned: &mut [bool],
bonds: &[Bond],
adj: &[Vec<usize>],
) -> bool {
let n = ring.len();
let (ra_idx, ra, ext_nb) = match ring.iter().enumerate().find_map(|(idx, &a)| {
adj[a]
.iter()
.find(|&&nb| positioned[nb])
.map(|&nb| (idx, a, nb))
}) {
Some(x) => x,
None => return false,
};
let ext_nb_pos = atoms[ext_nb].posit;
let ext_adj_placed: Vec<usize> = adj[ext_nb]
.iter()
.copied()
.filter(|&nb| positioned[nb])
.collect();
let ext_geom = geom_for_atom(ext_nb, bonds);
let bt = bond_type_between(ra, ext_nb, bonds);
let bond_len = estimate_bond_length(atoms[ra].element, atoms[ext_nb].element, bt);
let ra_element = atoms[ra].element;
let ra_pos = find_appended_posit(
ext_nb_pos,
atoms,
&ext_adj_placed,
Some(bond_len),
ra_element,
ext_geom,
)
.unwrap_or_else(|| ext_nb_pos + Vec3::new(bond_len, 0., 0.));
let bond_dir = {
let d = ra_pos - ext_nb_pos;
if d.magnitude_squared() < 1e-12 {
Vec3::new(1., 0., 0.)
} else {
d.to_normalized()
}
};
let avg_bl = avg_ring_bond_len(ring, atoms, bonds);
let radius = ring_circumradius(avg_bl, n);
let ring_center = ra_pos + bond_dir * radius;
atoms[ra].posit = ra_pos;
positioned[ra] = true;
let ra_angle = {
let d = ra_pos - ring_center;
d.y.atan2(d.x)
};
let step = 2.0 * PI / n as f64;
for k in 1..n {
let ai = ring[(ra_idx + k) % n];
if positioned[ai] {
continue;
}
let angle = ra_angle + k as f64 * step;
atoms[ai].posit = ring_center + Vec3::new(radius * angle.cos(), radius * angle.sin(), 0.);
positioned[ai] = true;
}
true
}
fn place_ring_spiro(
ring: &[usize],
atoms: &mut [Atom],
positioned: &mut [bool],
bonds: &[Bond],
adj: &[Vec<usize>],
) -> bool {
let n = ring.len();
let (sc_idx, sc) = match ring.iter().enumerate().find(|&(_, &a)| positioned[a]) {
Some((i, &a)) => (i, a),
None => return false,
};
let placed_nbrs: Vec<usize> = adj[sc]
.iter()
.copied()
.filter(|&nb| positioned[nb])
.collect();
if placed_nbrs.len() < 2 {
return false; }
let sc_pos = atoms[sc].posit;
let pn0 = atoms[placed_nbrs[0]].posit;
let pn1 = atoms[placed_nbrs[1]].posit;
let (tp0, tp1) = find_tetra_posits(sc_pos, pn0, pn1);
let prev_ai = ring[(sc_idx + n - 1) % n];
let next_ai = ring[(sc_idx + 1) % n];
let len_prev = estimate_bond_length(
atoms[sc].element,
atoms[prev_ai].element,
bond_type_between(sc, prev_ai, bonds),
);
let len_next = estimate_bond_length(
atoms[sc].element,
atoms[next_ai].element,
bond_type_between(sc, next_ai, bonds),
);
atoms[prev_ai].posit = sc_pos + (tp0 - sc_pos).to_normalized() * len_prev;
positioned[prev_ai] = true;
atoms[next_ai].posit = sc_pos + (tp1 - sc_pos).to_normalized() * len_next;
positioned[next_ai] = true;
true
}
fn bfs_place_substituents(
queue: &mut VecDeque<usize>,
atoms: &mut Vec<Atom>,
positioned: &mut [bool],
bonds: &[Bond],
adj: &[Vec<usize>],
) {
while let Some(u) = queue.pop_front() {
let geom = geom_for_atom(u, bonds);
let posit_u = atoms[u].posit;
let neighbours: Vec<usize> = adj[u].to_vec();
for v in neighbours {
if positioned[v] {
continue;
}
let adj_placed: Vec<usize> = adj[u]
.iter()
.copied()
.filter(|&w| w != v && positioned[w])
.collect();
let bt = bond_type_between(u, v, bonds);
let bond_len = estimate_bond_length(atoms[u].element, atoms[v].element, bt);
let v_element = atoms[v].element;
let n_placed_before_v = adj_placed.len();
let p =
find_appended_posit(posit_u, atoms, &adj_placed, Some(bond_len), v_element, geom)
.unwrap_or_else(|| {
no_context_position(posit_u, bond_len, geom, n_placed_before_v)
});
atoms[v].posit = p;
positioned[v] = true;
queue.push_back(v);
}
}
}
fn no_context_position(parent: Vec3, bond_len: f64, geom: BondGeom, n_placed: usize) -> Vec3 {
let (n_spokes, base_angle): (usize, f64) = match geom {
BondGeom::Linear => (2, 0.0),
BondGeom::Planar => (3, 0.0),
BondGeom::Tetrahedral => (4, PI / 6.0),
};
let angle = base_angle + 2.0 * PI * n_placed as f64 / n_spokes as f64;
parent + Vec3::new(bond_len * angle.cos(), bond_len * angle.sin(), 0.)
}
fn geom_for_atom(i: usize, bonds: &[Bond]) -> BondGeom {
let atom_bonds: Vec<&Bond> = bonds
.iter()
.filter(|b| b.atom_0 == i || b.atom_1 == i)
.collect();
if atom_bonds.iter().any(|b| b.bond_type == BondType::Triple) {
return BondGeom::Linear;
}
let double_count = atom_bonds
.iter()
.filter(|b| b.bond_type == BondType::Double)
.count();
if double_count >= 2 {
return BondGeom::Linear;
}
if atom_bonds
.iter()
.any(|b| matches!(b.bond_type, BondType::Double | BondType::Aromatic))
{
BondGeom::Planar
} else {
BondGeom::Tetrahedral
}
}
fn bond_type_between(a: usize, b: usize, bonds: &[Bond]) -> BondType {
bonds
.iter()
.find(|bond| {
(bond.atom_0 == a && bond.atom_1 == b) || (bond.atom_0 == b && bond.atom_1 == a)
})
.map(|bond| bond.bond_type)
.unwrap_or(BondType::Single)
}
fn estimate_bond_length(el0: Element, el1: Element, bt: BondType) -> f64 {
let r0 = el0.covalent_radius().max(0.5);
let r1 = el1.covalent_radius().max(0.5);
let base = r0 + r1;
match bt {
BondType::Double => base * 0.87,
BondType::Triple => base * 0.78,
BondType::Aromatic => base * 0.91,
_ => base,
}
}
fn avg_ring_bond_len(ring: &[usize], atoms: &[Atom], bonds: &[Bond]) -> f64 {
let n = ring.len();
if n < 2 {
return 1.5;
}
let total: f64 = (0..n)
.map(|i| {
let a = ring[i];
let b = ring[(i + 1) % n];
let bt = bond_type_between(a, b, bonds);
estimate_bond_length(atoms[a].element, atoms[b].element, bt)
})
.sum();
total / n as f64
}
fn ring_circumradius(bond_len: f64, n: usize) -> f64 {
if n < 2 {
return bond_len;
}
bond_len / (2.0 * (PI / n as f64).sin())
}
fn angle_diff(a: f64, b: f64) -> f64 {
let d = ((a - b) % (2.0 * PI)).abs();
if d > PI { 2.0 * PI - d } else { d }
}