use chematic_core::{AtomIdx, BondOrder, Molecule};
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UffType {
C_3, C_2, C_1, C_R,
N_3, N_2, N_1, N_R,
O_3, O_2, O_1, O_R,
S_3, S_2, S_R,
P_3, P_R,
H_,
F_, Cl, Br, I_,
Li, Na, K, Ca, Mg,
Fe, Co, Ni, Cu, Zn,
Mn, Cr, V_, Mo, W_,
Pd, Pt, Au, Ag, Hg,
Al, Si,
Unknown,
}
impl UffType {
pub fn r1(self) -> f64 {
match self {
Self::C_3 => 0.757, Self::C_2 => 0.732, Self::C_1 => 0.706, Self::C_R => 0.729,
Self::N_3 => 0.700, Self::N_2 => 0.685, Self::N_1 => 0.656, Self::N_R => 0.699,
Self::O_3 => 0.658, Self::O_2 => 0.634, Self::O_1 => 0.639, Self::O_R => 0.680,
Self::S_3 => 1.020, Self::S_2 => 0.940, Self::S_R => 1.000,
Self::P_3 => 1.101, Self::P_R => 1.060,
Self::H_ => 0.354,
Self::F_ => 0.668, Self::Cl => 1.022, Self::Br => 1.172, Self::I_ => 1.394,
Self::Li => 1.336, Self::Na => 1.539, Self::K => 1.953, Self::Ca => 1.761,
Self::Mg => 1.535, Self::Fe => 1.285, Self::Co => 1.241, Self::Ni => 1.164,
Self::Cu => 1.302, Self::Zn => 1.193, Self::Mn => 1.362, Self::Cr => 1.370,
Self::V_ => 1.359, Self::Mo => 1.458, Self::W_ => 1.526,
Self::Pd => 1.375, Self::Pt => 1.387, Self::Au => 1.340, Self::Ag => 1.420,
Self::Hg => 1.490, Self::Al => 1.244, Self::Si => 1.117,
Self::Unknown => 1.5,
}
}
pub fn theta0(self) -> f64 {
match self {
Self::C_3 => 109.47, Self::C_2 => 120.0, Self::C_1 => 180.0, Self::C_R => 120.0,
Self::N_3 => 106.70, Self::N_2 => 111.2, Self::N_1 => 180.0, Self::N_R => 120.0,
Self::O_3 => 104.51, Self::O_2 => 120.0, Self::O_1 => 180.0, Self::O_R => 110.0,
Self::S_3 => 92.10, Self::S_2 => 120.0, Self::S_R => 100.0,
Self::P_3 => 93.80, Self::P_R => 120.0,
Self::H_ => 180.0,
Self::F_ => 180.0, Self::Cl => 180.0, Self::Br => 180.0, Self::I_ => 180.0,
_ => 109.47, }
}
pub fn x1(self) -> f64 {
match self {
Self::H_ => 2.886,
Self::C_3 => 3.851, Self::C_2 => 3.851, Self::C_1 => 3.851, Self::C_R => 3.851,
Self::N_3 => 3.660, Self::N_2 => 3.660, Self::N_1 => 3.660, Self::N_R => 3.660,
Self::O_3 => 3.500, Self::O_2 => 3.500, Self::O_1 => 3.500, Self::O_R => 3.500,
Self::F_ => 3.364,
Self::Cl => 3.947,
Self::Br => 4.153,
Self::I_ => 4.590,
Self::S_3 => 4.035, Self::S_2 => 4.035, Self::S_R => 4.035,
Self::P_3 => 4.147, Self::P_R => 4.147,
Self::Si => 4.295, Self::Al => 4.499,
Self::Fe => 4.054, Self::Co => 3.898, Self::Ni => 3.782,
Self::Cu => 3.495, Self::Zn => 3.445, Self::Mg => 3.021,
Self::Ca => 3.753, Self::Mn => 4.013, Self::Cr => 3.894,
Self::V_ => 3.804, Self::Na => 3.144, Self::K => 3.812,
_ => 3.800,
}
}
pub fn d1(self) -> f64 {
match self {
Self::H_ => 0.044,
Self::C_3 => 0.105, Self::C_2 => 0.105, Self::C_1 => 0.105, Self::C_R => 0.105,
Self::N_3 => 0.069, Self::N_2 => 0.069, Self::N_1 => 0.069, Self::N_R => 0.069,
Self::O_3 => 0.060, Self::O_2 => 0.060, Self::O_1 => 0.060, Self::O_R => 0.060,
Self::F_ => 0.050,
Self::Cl => 0.227,
Self::Br => 0.251,
Self::I_ => 0.339,
Self::S_3 => 0.274, Self::S_2 => 0.274, Self::S_R => 0.274,
Self::P_3 => 0.305, Self::P_R => 0.305,
Self::Si => 0.402, Self::Al => 0.505,
Self::Fe => 0.013, Self::Co => 0.014, Self::Ni => 0.015,
Self::Cu => 0.005, Self::Zn => 0.124, Self::Mg => 0.111,
_ => 0.100,
}
}
}
pub fn assign_uff_types(mol: &Molecule) -> Vec<(AtomIdx, UffType)> {
mol.atoms()
.map(|(idx, atom)| {
let an = atom.element.atomic_number();
let degree = mol.neighbors(idx).count();
let aromatic = atom.aromatic;
let has_double = mol.neighbors(idx).any(|(_, bidx)| {
mol.bond(bidx).order == BondOrder::Double
});
let has_triple = mol.neighbors(idx).any(|(_, bidx)| {
mol.bond(bidx).order == BondOrder::Triple
});
let uff = match an {
1 => UffType::H_,
6 => {
if aromatic { UffType::C_R }
else if has_triple { UffType::C_1 }
else if has_double { UffType::C_2 }
else { UffType::C_3 }
}
7 => {
if aromatic { UffType::N_R }
else if has_triple { UffType::N_1 }
else if has_double { UffType::N_2 }
else { UffType::N_3 }
}
8 => {
if aromatic { UffType::O_R }
else if has_double { UffType::O_2 }
else if degree == 1 { UffType::O_1 }
else { UffType::O_3 }
}
9 => UffType::F_,
14 => UffType::Si,
15 => if aromatic { UffType::P_R } else { UffType::P_3 },
16 => if aromatic { UffType::S_R } else if has_double { UffType::S_2 } else { UffType::S_3 },
17 => UffType::Cl,
35 => UffType::Br,
53 => UffType::I_,
13 => UffType::Al,
3 => UffType::Li, 11 => UffType::Na, 19 => UffType::K,
20 => UffType::Ca, 12 => UffType::Mg,
26 => UffType::Fe, 27 => UffType::Co, 28 => UffType::Ni,
29 => UffType::Cu, 30 => UffType::Zn,
25 => UffType::Mn, 24 => UffType::Cr, 23 => UffType::V_,
42 => UffType::Mo, 74 => UffType::W_,
46 => UffType::Pd, 78 => UffType::Pt,
79 => UffType::Au, 47 => UffType::Ag, 80 => UffType::Hg,
_ => UffType::Unknown,
};
(idx, uff)
})
.collect()
}
fn uff_bond_length(ti: UffType, tj: UffType, bond_order: f64) -> f64 {
let rij = ti.r1() + tj.r1();
let lambda = 0.1332;
let r_bo = -lambda * rij * bond_order.ln();
rij + r_bo
}
fn bond_order_f64(bo: BondOrder) -> f64 {
match bo {
BondOrder::Single | BondOrder::Up | BondOrder::Down | BondOrder::Dative => 1.0,
BondOrder::Aromatic => 1.5,
BondOrder::Double => 2.0,
BondOrder::Triple => 3.0,
_ => 1.0,
}
}
fn dist(a: [f64; 3], b: [f64; 3]) -> f64 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
let dz = a[2] - b[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
fn cos_angle(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
let ba = [a[0]-b[0], a[1]-b[1], a[2]-b[2]];
let bc = [c[0]-b[0], c[1]-b[1], c[2]-b[2]];
let dot = ba[0]*bc[0] + ba[1]*bc[1] + ba[2]*bc[2];
let len_ba = (ba[0]*ba[0]+ba[1]*ba[1]+ba[2]*ba[2]).sqrt();
let len_bc = (bc[0]*bc[0]+bc[1]*bc[1]+bc[2]*bc[2]).sqrt();
let denom = len_ba * len_bc;
if denom < 1e-10 { return 1.0; }
(dot / denom).clamp(-1.0, 1.0)
}
pub fn uff_total_energy(
mol: &Molecule,
types: &[(AtomIdx, UffType)],
coords: &[[f64; 3]],
) -> f64 {
let type_map: std::collections::HashMap<AtomIdx, UffType> =
types.iter().map(|&(a, t)| (a, t)).collect();
let get_type = |idx: AtomIdx| type_map.get(&idx).copied().unwrap_or(UffType::Unknown);
let get_coord = |idx: AtomIdx| coords[idx.0 as usize];
let mut energy = 0.0;
for (_, bond) in mol.bonds() {
let ti = get_type(bond.atom1);
let tj = get_type(bond.atom2);
let n = bond_order_f64(bond.order);
let r0 = uff_bond_length(ti, tj, n);
let r = dist(get_coord(bond.atom1), get_coord(bond.atom2));
let k = 664.12 / (r0 * r0 * r0);
energy += 0.5 * k * (r - r0) * (r - r0);
}
for (center_idx, center_type) in types {
let theta0_deg = center_type.theta0();
let theta0 = theta0_deg.to_radians();
let cos0 = theta0.cos();
let sin0 = theta0.sin();
let neighbors: Vec<AtomIdx> = mol.neighbors(*center_idx).map(|(nb, _)| nb).collect();
for i in 0..neighbors.len() {
for j in (i+1)..neighbors.len() {
let cos_theta = cos_angle(get_coord(neighbors[i]), get_coord(*center_idx), get_coord(neighbors[j]));
let delta = cos_theta - cos0;
let k_angle = 0.5 * 332.06 / (sin0 * sin0 + 1e-10);
energy += 0.5 * k_angle * delta * delta;
}
}
}
let atom_indices: Vec<AtomIdx> = mol.atoms().map(|(idx, _)| idx).collect();
let n = atom_indices.len();
for i in 0..n {
for j in (i+2)..n {
let ai = atom_indices[i];
let aj = atom_indices[j];
if mol.bond_between(ai, aj).is_some() { continue; }
let ti = get_type(ai);
let tj = get_type(aj);
let x_ij = (ti.x1() * tj.x1()).sqrt();
let d_ij = (ti.d1() * tj.d1()).sqrt();
let r = dist(get_coord(ai), get_coord(aj)).max(0.5);
let ratio = x_ij / r;
let ratio6 = ratio.powi(6);
let ratio12 = ratio6 * ratio6;
energy += d_ij * (ratio12 - 2.0 * ratio6);
}
}
energy
}
fn uff_gradient(
mol: &Molecule,
types: &[(AtomIdx, UffType)],
coords: &[[f64; 3]],
) -> Vec<[f64; 3]> {
const DELTA: f64 = 1e-4;
let n = coords.len();
let mut grad = vec![[0.0_f64; 3]; n];
let mut perturbed = coords.to_vec();
for i in 0..n {
for k in 0..3 {
perturbed[i][k] += DELTA;
let ep = uff_total_energy(mol, types, &perturbed);
perturbed[i][k] -= 2.0 * DELTA;
let em = uff_total_energy(mol, types, &perturbed);
perturbed[i][k] += DELTA;
grad[i][k] = (ep - em) / (2.0 * DELTA);
}
}
grad
}
pub struct UffMinimizeResult {
pub coords: Vec<[f64; 3]>,
pub energy: f64,
pub iterations: usize,
pub converged: bool,
}
pub fn minimize_uff(
mol: &Molecule,
types: &[(AtomIdx, UffType)],
initial_coords: Vec<[f64; 3]>,
max_iter: usize,
) -> UffMinimizeResult {
let mut coords = initial_coords;
let mut step = 0.05_f64;
let mut prev_energy = f64::MAX;
for iter in 0..max_iter {
let energy = uff_total_energy(mol, types, &coords);
let grad = uff_gradient(mol, types, &coords);
let rms: f64 = {
let sum2: f64 = grad.iter().flat_map(|g| g.iter()).map(|v| v * v).sum();
(sum2 / (grad.len() * 3) as f64).sqrt()
};
if rms < 0.01 {
return UffMinimizeResult { coords, energy, iterations: iter, converged: true };
}
let new_coords: Vec<[f64; 3]> = coords
.iter()
.zip(&grad)
.map(|(c, g)| [c[0] - step * g[0], c[1] - step * g[1], c[2] - step * g[2]])
.collect();
let new_energy = uff_total_energy(mol, types, &new_coords);
if new_energy < energy {
coords = new_coords;
if energy - new_energy < prev_energy * 1e-7 { step *= 1.2; }
prev_energy = energy;
} else {
step *= 0.5;
if step < 1e-8 {
return UffMinimizeResult { coords, energy, iterations: iter, converged: false };
}
}
}
let energy = uff_total_energy(mol, types, &coords);
UffMinimizeResult { coords, energy, iterations: max_iter, converged: false }
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
#[test]
fn assign_types_ethanol() {
let mol = parse("CCO").unwrap();
let types = assign_uff_types(&mol);
assert_eq!(types.len(), 3);
let type_map: std::collections::HashMap<_, _> = types.into_iter().collect();
for (_, atom) in mol.atoms() {
let idx = mol.atoms().find(|(_, a)| a.element == atom.element).map(|(i, _)| i);
if atom.element.atomic_number() == 6 {
assert!(matches!(type_map[&idx.unwrap()], UffType::C_3 | UffType::C_2));
}
}
}
#[test]
fn assign_types_benzene_aromatic() {
let mol = parse("c1ccccc1").unwrap();
let types = assign_uff_types(&mol);
for (_, t) in &types {
assert_eq!(*t, UffType::C_R);
}
}
#[test]
fn energy_finite() {
let mol = parse("CCO").unwrap();
let types = assign_uff_types(&mol);
let coords: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0],
[1.54, 0.0, 0.0],
[2.5, 1.2, 0.0],
];
let e = uff_total_energy(&mol, &types, &coords);
assert!(e.is_finite(), "energy should be finite: {e}");
}
#[test]
fn minimize_reduces_energy() {
let mol = parse("CCO").unwrap();
let types = assign_uff_types(&mol);
let coords: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0],
[2.5, 0.0, 0.0], [3.5, 1.2, 0.0],
];
let e0 = uff_total_energy(&mol, &types, &coords);
let result = minimize_uff(&mol, &types, coords, 200);
assert!(result.energy < e0, "minimisation should reduce energy: {e0} → {}", result.energy);
}
#[test]
fn uff_handles_zinc_complex() {
use chematic_core::{Atom, Element, MoleculeBuilder, BondOrder};
let mut b = MoleculeBuilder::new();
let zn = b.add_atom(Atom::new(Element::ZN));
let n1 = b.add_atom(Atom::new(Element::N));
let n2 = b.add_atom(Atom::new(Element::N));
b.add_bond(zn, n1, BondOrder::Single).unwrap();
b.add_bond(zn, n2, BondOrder::Single).unwrap();
let mol = b.build();
let types = assign_uff_types(&mol);
let zn_type = types.iter().find(|(_, t)| *t == UffType::Zn);
assert!(zn_type.is_some(), "Zn should get UffType::Zn");
}
}