use crate::bond::{Bond, BondOrder};
use crate::descriptors::elements::covalent_radius;
use crate::error::{Result, SdfError};
use crate::molecule::Molecule;
pub const DEFAULT_TOLERANCE: f64 = 0.45;
const MIN_DISTANCE: f64 = 0.01;
#[derive(Debug, Clone)]
pub struct BondInferenceConfig {
pub tolerance: f64,
pub clear_existing_bonds: bool,
}
impl Default for BondInferenceConfig {
fn default() -> Self {
Self {
tolerance: DEFAULT_TOLERANCE,
clear_existing_bonds: true,
}
}
}
pub fn infer_bonds(mol: &mut Molecule, tolerance: Option<f64>) -> Result<()> {
let config = BondInferenceConfig {
tolerance: tolerance.unwrap_or(DEFAULT_TOLERANCE),
..Default::default()
};
infer_bonds_with_config(mol, &config)
}
pub fn infer_bonds_with_config(mol: &mut Molecule, config: &BondInferenceConfig) -> Result<()> {
let radii: Vec<f64> = mol
.atoms
.iter()
.enumerate()
.map(|(i, atom)| {
covalent_radius(&atom.element).ok_or_else(|| SdfError::BondInferenceError {
element: atom.element.clone(),
index: i,
})
})
.collect::<Result<Vec<f64>>>()?;
if config.clear_existing_bonds {
mol.bonds.clear();
}
let n = mol.atoms.len();
for i in 0..n {
for j in (i + 1)..n {
let dx = mol.atoms[i].x - mol.atoms[j].x;
let dy = mol.atoms[i].y - mol.atoms[j].y;
let dz = mol.atoms[i].z - mol.atoms[j].z;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
let max_dist = radii[i] + radii[j] + config.tolerance;
if dist > MIN_DISTANCE && dist <= max_dist {
mol.bonds.push(Bond::new(i, j, BondOrder::Single));
}
}
}
Ok(())
}
impl Molecule {
pub fn infer_bonds(&mut self, tolerance: Option<f64>) -> Result<()> {
infer_bonds(self, tolerance)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atom::Atom;
#[test]
fn test_infer_bonds_water() {
let mut mol = Molecule::new("water");
mol.atoms
.push(Atom::new(0, "O", 0.000000, 0.000000, 0.117300));
mol.atoms
.push(Atom::new(1, "H", 0.756950, 0.000000, -0.469200));
mol.atoms
.push(Atom::new(2, "H", -0.756950, 0.000000, -0.469200));
infer_bonds(&mut mol, None).unwrap();
assert_eq!(mol.bond_count(), 2);
assert!(mol.bonds.iter().all(|b| b.order == BondOrder::Single));
}
#[test]
fn test_infer_bonds_empty_molecule() {
let mut mol = Molecule::new("empty");
infer_bonds(&mut mol, None).unwrap();
assert_eq!(mol.bond_count(), 0);
}
#[test]
fn test_infer_bonds_single_atom() {
let mut mol = Molecule::new("single");
mol.atoms.push(Atom::new(0, "C", 0.0, 0.0, 0.0));
infer_bonds(&mut mol, None).unwrap();
assert_eq!(mol.bond_count(), 0);
}
#[test]
fn test_infer_bonds_unknown_element() {
let mut mol = Molecule::new("unknown");
mol.atoms.push(Atom::new(0, "Xx", 0.0, 0.0, 0.0));
mol.atoms.push(Atom::new(1, "C", 1.5, 0.0, 0.0));
let result = infer_bonds(&mut mol, None);
assert!(result.is_err());
}
#[test]
fn test_infer_bonds_distant_atoms() {
let mut mol = Molecule::new("distant");
mol.atoms.push(Atom::new(0, "C", 0.0, 0.0, 0.0));
mol.atoms.push(Atom::new(1, "C", 10.0, 0.0, 0.0));
infer_bonds(&mut mol, None).unwrap();
assert_eq!(mol.bond_count(), 0);
}
#[test]
fn test_infer_bonds_clears_existing() {
let mut mol = Molecule::new("test");
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::Double));
infer_bonds(&mut mol, None).unwrap();
assert_eq!(mol.bond_count(), 1);
assert_eq!(mol.bonds[0].order, BondOrder::Single);
}
#[test]
fn test_infer_bonds_keep_existing() {
let mut mol = Molecule::new("test");
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::Double));
let config = BondInferenceConfig {
clear_existing_bonds: false,
..Default::default()
};
infer_bonds_with_config(&mut mol, &config).unwrap();
assert_eq!(mol.bond_count(), 2);
}
#[test]
fn test_infer_bonds_tolerance_effect() {
let mut mol = Molecule::new("test");
mol.atoms.push(Atom::new(0, "C", 0.0, 0.0, 0.0));
mol.atoms.push(Atom::new(1, "C", 1.9, 0.0, 0.0));
infer_bonds(&mut mol, None).unwrap();
assert_eq!(mol.bond_count(), 1);
infer_bonds(&mut mol, Some(0.1)).unwrap();
assert_eq!(mol.bond_count(), 0);
}
}