use core::f64::consts::PI;
use std::collections::VecDeque;
use chematic_core::{AtomIdx, BondOrder, Molecule};
use chematic_perception::find_sssr;
use crate::coords::{Coords3D, Point3};
fn ideal_bond_len(mol: &Molecule, a: AtomIdx, b: AtomIdx) -> f64 {
let ea = mol.atom(a).element;
let eb = mol.atom(b).element;
let order = mol
.bond_between(a, b)
.map(|(_, bond)| bond.order)
.unwrap_or(BondOrder::Single);
let (lo, hi) = if ea.atomic_number() <= eb.atomic_number() {
(ea.atomic_number(), eb.atomic_number())
} else {
(eb.atomic_number(), ea.atomic_number())
};
match (lo, hi, order) {
(6, 6, BondOrder::Single) | (6, 6, BondOrder::Up) | (6, 6, BondOrder::Down) => 1.54,
(6, 6, BondOrder::Double) => 1.34,
(6, 6, BondOrder::Triple) => 1.20,
(6, 6, BondOrder::Aromatic) => 1.40,
(6, 7, BondOrder::Single) | (6, 7, BondOrder::Up) | (6, 7, BondOrder::Down) => 1.47,
(6, 7, BondOrder::Double) => 1.27,
(6, 7, BondOrder::Triple) => 1.16,
(6, 7, BondOrder::Aromatic) => 1.34,
(6, 8, BondOrder::Single) | (6, 8, BondOrder::Up) | (6, 8, BondOrder::Down) => 1.43,
(6, 8, BondOrder::Double) => 1.22,
(6, 8, BondOrder::Aromatic) => 1.36,
(6, 16, _) => 1.82,
(6, 9, _) => 1.35,
(6, 17, _) => 1.77,
(6, 35, _) => 1.94,
(6, 53, _) => 2.14,
(1, 6, _) => 1.09,
(1, 7, _) => 1.01,
(1, 8, _) => 0.96,
_ => 1.54,
}
}
fn ideal_angle(mol: &Molecule, center: AtomIdx) -> f64 {
let mut has_triple = false;
let mut has_double_or_arom = false;
for (_, bidx) in mol.neighbors(center) {
match mol.bond(bidx).order {
BondOrder::Triple => has_triple = true,
BondOrder::Double | BondOrder::Aromatic => has_double_or_arom = true,
_ => {}
}
}
if has_triple {
PI } else if has_double_or_arom {
PI * 2.0 / 3.0 } else {
109.5_f64.to_radians()
}
}
fn connected_components(mol: &Molecule) -> Vec<Vec<AtomIdx>> {
let n = mol.atom_count();
let mut visited = vec![false; n];
let mut components: Vec<Vec<AtomIdx>> = Vec::new();
for start in 0..n {
if visited[start] {
continue;
}
let mut component: Vec<AtomIdx> = Vec::new();
let mut queue: VecDeque<AtomIdx> = VecDeque::new();
let start_idx = AtomIdx(start as u32);
visited[start] = true;
queue.push_back(start_idx);
while let Some(current) = queue.pop_front() {
component.push(current);
for (nb, _) in mol.neighbors(current) {
if !visited[nb.0 as usize] {
visited[nb.0 as usize] = true;
queue.push_back(nb);
}
}
}
components.push(component);
}
components
}
pub fn generate_coords(mol: &Molecule) -> Coords3D {
let n = mol.atom_count();
let mut coords = Coords3D::new_zeroed(n);
if n == 0 {
return coords;
}
if n == 1 {
coords.set(AtomIdx(0), Point3::zero());
return coords;
}
let ring_set = find_sssr(mol);
let components = connected_components(mol);
let mut x_offset = 0.0_f64;
for component in &components {
place_component(mol, component, &ring_set, x_offset, &mut coords);
let max_x = component
.iter()
.map(|&idx| coords.get(idx).x)
.fold(f64::NEG_INFINITY, f64::max);
x_offset = max_x + 5.0;
}
coords
}
fn place_component(
mol: &Molecule,
component: &[AtomIdx],
ring_set: &chematic_perception::RingSet,
x_offset: f64,
coords: &mut Coords3D,
) {
if component.is_empty() {
return;
}
let mut placed = vec![false; mol.atom_count()];
place_rings(mol, component, ring_set, x_offset, coords, &mut placed);
let root = component[0];
if !placed[root.0 as usize] {
coords.set(root, Point3::new(x_offset, 0.0, 0.0));
placed[root.0 as usize] = true;
}
dfs_place(mol, root, &mut placed, coords);
}
fn place_rings(
mol: &Molecule,
component: &[AtomIdx],
ring_set: &chematic_perception::RingSet,
x_offset: f64,
coords: &mut Coords3D,
placed: &mut [bool],
) {
let component_set: std::collections::HashSet<AtomIdx> =
component.iter().copied().collect();
let mut ring_cx = x_offset;
let mut ring_cy = 0.0_f64;
let mut first_ring = true;
for ring in ring_set.rings() {
if !ring.iter().all(|a| component_set.contains(a)) {
continue;
}
let ring_size = ring.len();
if ring_size == 0 {
continue;
}
let bond_len = {
let a0 = ring[0];
let a1 = ring[1 % ring_size];
ideal_bond_len(mol, a0, a1)
};
let r = bond_len / (2.0 * (PI / ring_size as f64).sin());
if first_ring {
ring_cx = x_offset + r;
ring_cy = 0.0;
first_ring = false;
} else {
let already_placed: Vec<AtomIdx> = ring
.iter()
.copied()
.filter(|a| placed[a.0 as usize])
.collect();
if already_placed.len() >= 2 {
let p0 = coords.get(already_placed[0]);
let p1 = coords.get(already_placed[1]);
ring_cx = (p0.x + p1.x) / 2.0;
ring_cy = (p0.y + p1.y) / 2.0 + r;
} else if already_placed.len() == 1 {
let p0 = coords.get(already_placed[0]);
ring_cx = p0.x + r;
ring_cy = p0.y;
}
}
for (k, &atom_idx) in ring.iter().enumerate() {
if placed[atom_idx.0 as usize] {
continue; }
let angle = 2.0 * PI * k as f64 / ring_size as f64;
let x = ring_cx + r * angle.cos();
let y = ring_cy + r * angle.sin();
coords.set(atom_idx, Point3::new(x, y, 0.0));
placed[atom_idx.0 as usize] = true;
}
}
}
fn dfs_place(mol: &Molecule, current: AtomIdx, placed: &mut [bool], coords: &mut Coords3D) {
let pos_current = coords.get(current);
let parent = mol
.neighbors(current)
.map(|(nb, _)| nb)
.find(|nb| placed[nb.0 as usize]);
let unplaced_neighbors: Vec<AtomIdx> = mol
.neighbors(current)
.map(|(nb, _)| nb)
.filter(|nb| !placed[nb.0 as usize])
.collect();
if unplaced_neighbors.is_empty() {
return;
}
let incoming_dir: Point3 = match parent {
Some(p) => pos_current.sub(&coords.get(p)).normalize(),
None => Point3::new(1.0, 0.0, 0.0),
};
let perp = perpendicular_to(incoming_dir);
let angle = ideal_angle(mol, current);
let bend_angle = PI - angle; let dir_bent = rotate_around_axis(incoming_dir, perp, bend_angle);
for (i, &nb) in unplaced_neighbors.iter().enumerate() {
let bond_len = ideal_bond_len(mol, current, nb);
let dihedral = (i as f64) * (2.0 * PI / 3.0);
let dir_final = rotate_around_axis(dir_bent, incoming_dir, dihedral);
let new_pos = pos_current.add(&dir_final.scale(bond_len));
coords.set(nb, new_pos);
placed[nb.0 as usize] = true;
dfs_place(mol, nb, placed, coords);
}
}
fn perpendicular_to(v: Point3) -> Point3 {
let candidate = if v.x.abs() < 0.9 {
Point3::new(1.0, 0.0, 0.0)
} else {
Point3::new(0.0, 1.0, 0.0)
};
let proj = v.scale(v.dot(&candidate));
candidate.sub(&proj).normalize()
}
fn rotate_around_axis(v: Point3, axis: Point3, theta: f64) -> Point3 {
let cos_t = theta.cos();
let sin_t = theta.sin();
let dot = axis.dot(&v);
let term1 = v.scale(cos_t);
let term2 = axis.cross(&v).scale(sin_t);
let term3 = axis.scale(dot * (1.0 - cos_t));
term1.add(&term2).add(&term3)
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
#[test]
fn generate_coords_methane() {
let mol = parse("C").unwrap();
let coords = generate_coords(&mol);
assert_eq!(coords.atom_count(), 1, "methane has 1 heavy atom");
let p0 = coords.get(AtomIdx(0));
assert_eq!(p0.x, 0.0, "first atom at origin");
}
#[test]
fn generate_coords_ethane_has_reasonable_distance() {
let mol = parse("CC").unwrap();
let coords = generate_coords(&mol);
assert_eq!(coords.atom_count(), 2, "ethane has 2 carbons");
let p0 = coords.get(AtomIdx(0));
let p1 = coords.get(AtomIdx(1));
let dist = p0.distance(&p1);
assert!((dist - 1.54).abs() < 0.15, "C-C distance should be ~1.54 Å, got {}", dist);
}
#[test]
fn generate_coords_benzene_has_6_atoms() {
let mol = parse("c1ccccc1").unwrap();
let coords = generate_coords(&mol);
assert_eq!(coords.atom_count(), 6, "benzene has 6 carbons");
}
#[test]
fn generate_coords_cyclohexane_all_placed() {
let mol = parse("C1CCCCC1").unwrap();
let coords = generate_coords(&mol);
assert_eq!(coords.atom_count(), 6, "cyclohexane has 6 carbons");
}
#[test]
fn generate_coords_disconnected_molecules() {
let mol = parse("CC.CC").unwrap();
let coords = generate_coords(&mol);
assert_eq!(coords.atom_count(), 4, "two ethanes (disconnected) have 4 carbons total");
}
#[test]
fn generate_coords_propane_linear() {
let mol = parse("CCC").unwrap();
let coords = generate_coords(&mol);
assert_eq!(coords.atom_count(), 3, "propane has 3 carbons");
let mut has_nonzero = false;
for i in 0..3 {
let p = coords.get(AtomIdx(i));
if p.x != 0.0 || p.y != 0.0 || p.z != 0.0 {
has_nonzero = true;
}
}
assert!(has_nonzero, "at least some atoms should be placed away from origin");
}
#[test]
fn generate_and_minimize_dreiding_benzene() {
use crate::minimize::minimize_dreiding;
let mol = parse("c1ccccc1").unwrap();
let coords = generate_coords(&mol);
let minimized = minimize_dreiding(&mol, coords);
assert_eq!(minimized.atom_count(), 6, "benzene still has 6 carbons after minimization");
}
#[test]
fn generate_conformer_ensemble_multiple() {
use crate::conformer::ConformerEnsemble;
let mol = parse("CC").unwrap();
let ensemble = ConformerEnsemble::new(mol);
assert_eq!(ensemble.conformer_count(), 0, "fresh ensemble has 0 conformers");
}
}