use super::traits::ForceFieldContribution;
use petgraph::visit::EdgeRef;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Mmff94AtomType {
CR = 1, CSp2 = 2, CSp = 3, CO = 4, HC = 5, OR = 6, O2 = 7, NR = 8, N2 = 9, NC = 10, F = 11, Cl = 12, Br = 13, I = 14, S = 15, SdO = 16, SO = 17, SO2 = 18, SI = 19, CR4R = 20, HO = 21, CR3R = 22, HN = 23, HOCO = 24, P = 25, HaP = 26, HOS = 28, NPl3 = 30, ON2 = 31, OX = 32, OM = 33, HNR = 34, HIM = 35, CB = 37, NPOX = 38, NR2 = 39, NAm = 40, NSO = 41, Sthi = 44, NdOx = 45, NPl = 46, C5A = 63, C5B = 64, N5A = 65, N5B = 66, NAZT = 47, NSP = 48, N5M = 49, N2OX = 50, N3OX = 51, NPYD = 52, O5 = 59, Fe2 = 87, Fe3 = 88, HS = 71, HP = 72, PO = 75, Unknown = 0,
}
pub fn assign_mmff94_type_smarts(mol: &crate::graph::Molecule, atom_idx: usize) -> Mmff94AtomType {
use crate::graph::{BondOrder, Hybridization};
use petgraph::graph::NodeIndex;
let node = NodeIndex::new(atom_idx);
let atom = &mol.graph[node];
let element = atom.element;
let hyb = &atom.hybridization;
let neighbors: Vec<NodeIndex> = mol.graph.neighbors(node).collect();
let degree = neighbors.len();
let nb_elem = |ni: NodeIndex| mol.graph[ni].element;
let count_nb_elem = |z: u8| neighbors.iter().filter(|&&n| nb_elem(n) == z).count();
let count_double_bonds = || -> usize {
mol.graph
.edges(node)
.filter(|e| e.weight().order == BondOrder::Double)
.count()
};
let in_ring_of_size = |size: usize| -> bool { atom_in_ring_of_size(mol, atom_idx, size) };
let is_aromatic = is_atom_aromatic_mmff(mol, atom_idx);
let nb_has_double_bond_to = |ni: NodeIndex, target_z: u8| -> bool {
mol.graph.edges(ni).any(|e| {
e.weight().order == BondOrder::Double && {
let other = if e.source() == ni {
e.target()
} else {
e.source()
};
mol.graph[other].element == target_z
}
})
};
let adjacent_to_carbonyl = || -> bool {
neighbors
.iter()
.any(|&ni| nb_elem(ni) == 6 && nb_has_double_bond_to(ni, 8))
};
match element {
1 => {
if degree == 0 {
return Mmff94AtomType::HC;
}
let parent = neighbors[0];
let parent_z = nb_elem(parent);
match parent_z {
6 => Mmff94AtomType::HC, 7 => {
if is_atom_aromatic_mmff(mol, parent.index()) && in_ring_of_size(5) {
Mmff94AtomType::HIM } else if atom_in_any_ring(mol, parent.index()) {
Mmff94AtomType::HNR } else {
Mmff94AtomType::HN
}
}
8 => {
let parent_nbs: Vec<NodeIndex> = mol.graph.neighbors(parent).collect();
for &pnb in &parent_nbs {
if pnb.index() == atom_idx {
continue;
}
let pnb_z = nb_elem(pnb);
if pnb_z == 6 && nb_has_double_bond_to(pnb, 8) {
return Mmff94AtomType::HOCO; }
if pnb_z == 16 {
return Mmff94AtomType::HOS; }
}
Mmff94AtomType::HO
}
15 => Mmff94AtomType::HP, 16 => Mmff94AtomType::HS, _ => Mmff94AtomType::HC,
}
}
6 => {
if is_aromatic {
if in_ring_of_size(5) {
let has_hetero_nb = neighbors.iter().any(|&ni| {
let z = nb_elem(ni);
(z == 7 || z == 8 || z == 16) && is_atom_aromatic_mmff(mol, ni.index())
});
if has_hetero_nb {
Mmff94AtomType::C5A } else {
Mmff94AtomType::C5B }
} else {
Mmff94AtomType::CB
}
} else {
match hyb {
Hybridization::SP => Mmff94AtomType::CSp,
Hybridization::SP2 => {
if count_double_bonds() > 0
&& count_nb_elem(8) > 0
&& mol.graph.edges(node).any(|e| {
e.weight().order == BondOrder::Double && {
let other = if e.source() == node {
e.target()
} else {
e.source()
};
nb_elem(other) == 8
}
})
{
Mmff94AtomType::CO } else {
Mmff94AtomType::CSp2
}
}
_ => {
if in_ring_of_size(3) {
Mmff94AtomType::CR3R
} else if in_ring_of_size(4) {
Mmff94AtomType::CR4R
} else {
Mmff94AtomType::CR
}
}
}
}
}
7 => {
let n_double = count_double_bonds();
if is_aromatic {
if in_ring_of_size(5) {
if degree == 3 {
Mmff94AtomType::N5A } else {
Mmff94AtomType::N5B }
} else {
Mmff94AtomType::NR2 }
}
else if n_double >= 2 && count_nb_elem(8) >= 2 {
Mmff94AtomType::N2OX
}
else if n_double == 1 && count_nb_elem(8) >= 1 && degree >= 3 {
let has_o_double = mol.graph.edges(node).any(|e| {
e.weight().order == BondOrder::Double && {
let o = if e.source() == node {
e.target()
} else {
e.source()
};
nb_elem(o) == 8
}
});
if has_o_double && degree >= 3 {
Mmff94AtomType::NPOX
} else {
Mmff94AtomType::N2
}
}
else if adjacent_to_carbonyl()
&& matches!(hyb, Hybridization::SP2 | Hybridization::SP3)
&& degree <= 3
{
let has_so = neighbors
.iter()
.any(|&ni| nb_elem(ni) == 16 && nb_has_double_bond_to(ni, 8));
if has_so {
Mmff94AtomType::NSO
} else {
Mmff94AtomType::NAm
}
}
else if matches!(hyb, Hybridization::SP2) {
if degree == 3 && n_double == 0 {
Mmff94AtomType::NPl3
} else if n_double >= 1 {
Mmff94AtomType::N2
} else {
Mmff94AtomType::NPl
}
}
else if matches!(hyb, Hybridization::SP) {
Mmff94AtomType::NC
}
else {
Mmff94AtomType::NR
}
}
8 => {
if is_aromatic && in_ring_of_size(5) {
return Mmff94AtomType::O5; }
let n_double = count_double_bonds();
if degree == 1 {
let parent = neighbors[0];
if nb_elem(parent) == 7 {
let n_node = parent;
let n_o_count = mol
.graph
.neighbors(n_node)
.filter(|&ni| nb_elem(ni) == 8)
.count();
if n_o_count >= 2 {
return Mmff94AtomType::ON2; }
}
}
if atom.formal_charge < 0 {
return Mmff94AtomType::OM;
}
if degree == 1 && n_double == 0 {
let parent = neighbors[0];
if nb_elem(parent) == 6 && nb_has_double_bond_to(parent, 8) {
return Mmff94AtomType::OX; }
}
match hyb {
Hybridization::SP2 => Mmff94AtomType::O2, _ => Mmff94AtomType::OR, }
}
9 => Mmff94AtomType::F,
14 => Mmff94AtomType::SI,
15 => {
if degree >= 4 && count_nb_elem(8) >= 1 && nb_has_double_bond_to(node, 8) {
Mmff94AtomType::PO } else {
Mmff94AtomType::P
}
}
16 => {
if is_aromatic && in_ring_of_size(5) {
return Mmff94AtomType::Sthi; }
let n_double_o = mol
.graph
.edges(node)
.filter(|e| {
e.weight().order == BondOrder::Double && {
let other = if e.source() == node {
e.target()
} else {
e.source()
};
nb_elem(other) == 8
}
})
.count();
if n_double_o >= 2 {
Mmff94AtomType::SO2 } else if n_double_o == 1 {
Mmff94AtomType::SO } else if degree <= 2 && count_nb_elem(1) >= 1 {
Mmff94AtomType::Sthi } else {
Mmff94AtomType::S }
}
17 => Mmff94AtomType::Cl,
35 => Mmff94AtomType::Br,
26 => {
if atom.formal_charge >= 3 {
Mmff94AtomType::Fe3
} else {
Mmff94AtomType::Fe2
}
}
53 => Mmff94AtomType::I,
_ => Mmff94AtomType::Unknown,
}
}
fn atom_in_any_ring(mol: &crate::graph::Molecule, atom_idx: usize) -> bool {
atom_in_ring_of_size(mol, atom_idx, 3)
|| atom_in_ring_of_size(mol, atom_idx, 4)
|| atom_in_ring_of_size(mol, atom_idx, 5)
|| atom_in_ring_of_size(mol, atom_idx, 6)
|| atom_in_ring_of_size(mol, atom_idx, 7)
}
fn atom_in_ring_of_size(mol: &crate::graph::Molecule, atom_idx: usize, size: usize) -> bool {
use petgraph::graph::NodeIndex;
use std::collections::VecDeque;
let start = NodeIndex::new(atom_idx);
let mut queue: VecDeque<(NodeIndex, Vec<usize>)> = VecDeque::new();
for nb in mol.graph.neighbors(start) {
queue.push_back((nb, vec![atom_idx, nb.index()]));
}
while let Some((current, path)) = queue.pop_front() {
if path.len() > size + 1 {
continue;
}
if path.len() == size + 1 && current == start {
return true;
}
if path.len() > size {
continue;
}
for nb in mol.graph.neighbors(current) {
if nb == start && path.len() == size {
return true;
}
if !path.contains(&nb.index()) {
let mut new_path = path.clone();
new_path.push(nb.index());
queue.push_back((nb, new_path));
}
}
}
false
}
fn is_atom_aromatic_mmff(mol: &crate::graph::Molecule, atom_idx: usize) -> bool {
use crate::graph::BondOrder;
use petgraph::graph::NodeIndex;
let node = NodeIndex::new(atom_idx);
mol.graph
.edges(node)
.any(|e| e.weight().order == BondOrder::Aromatic)
}
pub fn assign_mmff94_type(
element: u8,
hyb: &crate::graph::Hybridization,
is_aromatic: bool,
is_amide_n: bool,
) -> Mmff94AtomType {
use crate::graph::Hybridization::*;
match element {
1 => Mmff94AtomType::HC,
5 => Mmff94AtomType::CSp2,
6 => {
if is_aromatic {
Mmff94AtomType::CB
} else {
match hyb {
SP => Mmff94AtomType::CSp,
SP2 => Mmff94AtomType::CSp2,
_ => Mmff94AtomType::CR,
}
}
}
7 => {
if is_aromatic {
Mmff94AtomType::NR2
} else if is_amide_n {
Mmff94AtomType::NAm
} else {
match hyb {
SP => Mmff94AtomType::NC,
SP2 => Mmff94AtomType::N2,
_ => Mmff94AtomType::NR,
}
}
}
8 => match hyb {
SP2 => Mmff94AtomType::O2,
_ => Mmff94AtomType::OR,
},
9 => Mmff94AtomType::F,
15 => Mmff94AtomType::P,
16 => Mmff94AtomType::S,
17 => Mmff94AtomType::Cl,
35 => Mmff94AtomType::Br,
53 => Mmff94AtomType::I,
_ => Mmff94AtomType::Unknown,
}
}
pub fn assign_all_mmff94_types(mol: &crate::graph::Molecule) -> Vec<Mmff94AtomType> {
let n = mol.graph.node_count();
(0..n).map(|i| assign_mmff94_type_smarts(mol, i)).collect()
}
pub struct Mmff94BondStretch {
pub atom_i: usize,
pub atom_j: usize,
pub k_b: f64, pub r0: f64, }
const MMFF94_CUBIC_STRETCH: f64 = -2.0;
impl ForceFieldContribution for Mmff94BondStretch {
fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
let ri = self.atom_i * 3;
let rj = self.atom_j * 3;
let dx = coords[ri] - coords[rj];
let dy = coords[ri + 1] - coords[rj + 1];
let dz = coords[ri + 2] - coords[rj + 2];
let dist = (dx * dx + dy * dy + dz * dz).sqrt().max(1e-8);
let dr = dist - self.r0;
let cs = MMFF94_CUBIC_STRETCH;
let cs2 = cs * cs;
let energy =
143.9325 * 0.5 * self.k_b * dr * dr * (1.0 + cs * dr + (7.0 / 12.0) * cs2 * dr * dr);
let de_dr = 143.9325 * self.k_b * dr * (1.0 + 1.5 * cs * dr + (7.0 / 6.0) * cs2 * dr * dr);
let scale = de_dr / dist;
grad[ri] += scale * dx;
grad[ri + 1] += scale * dy;
grad[ri + 2] += scale * dz;
grad[rj] -= scale * dx;
grad[rj + 1] -= scale * dy;
grad[rj + 2] -= scale * dz;
energy
}
}
pub struct Mmff94AngleBend {
pub atom_i: usize,
pub atom_j: usize, pub atom_k: usize,
pub k_a: f64, pub theta0: f64, }
const MMFF94_CUBIC_BEND: f64 = -0.014;
impl ForceFieldContribution for Mmff94AngleBend {
fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
let ri = self.atom_i * 3;
let rj = self.atom_j * 3;
let rk = self.atom_k * 3;
let rji = [
coords[ri] - coords[rj],
coords[ri + 1] - coords[rj + 1],
coords[ri + 2] - coords[rj + 2],
];
let rjk = [
coords[rk] - coords[rj],
coords[rk + 1] - coords[rj + 1],
coords[rk + 2] - coords[rj + 2],
];
let d_ji = (rji[0] * rji[0] + rji[1] * rji[1] + rji[2] * rji[2])
.sqrt()
.max(1e-8);
let d_jk = (rjk[0] * rjk[0] + rjk[1] * rjk[1] + rjk[2] * rjk[2])
.sqrt()
.max(1e-8);
let cos_theta = (rji[0] * rjk[0] + rji[1] * rjk[1] + rji[2] * rjk[2]) / (d_ji * d_jk);
let cos_theta_clamped = cos_theta.clamp(-1.0, 1.0);
let theta = cos_theta_clamped.acos();
let dt = (theta - self.theta0) * 180.0 / std::f64::consts::PI;
let cb = MMFF94_CUBIC_BEND;
let energy = 0.043844 * 0.5 * self.k_a * dt * dt * (1.0 + cb * dt);
let de_dtheta =
0.043844 * self.k_a * dt * (1.0 + 1.5 * cb * dt) * (180.0 / std::f64::consts::PI);
let sin_theta = (1.0 - cos_theta_clamped * cos_theta_clamped)
.sqrt()
.max(1e-8);
let dcos = -1.0 / sin_theta;
let pref = de_dtheta * dcos;
for dim in 0..3 {
let term_i = pref * (rjk[dim] / (d_ji * d_jk) - cos_theta * rji[dim] / (d_ji * d_ji))
/ d_ji
* d_ji;
let term_k = pref * (rji[dim] / (d_ji * d_jk) - cos_theta * rjk[dim] / (d_jk * d_jk))
/ d_jk
* d_jk;
grad[ri + dim] += term_i;
grad[rk + dim] += term_k;
grad[rj + dim] -= term_i + term_k;
}
energy
}
}
pub struct Mmff94Torsion {
pub atom_i: usize,
pub atom_j: usize,
pub atom_k: usize,
pub atom_l: usize,
pub v1: f64,
pub v2: f64,
pub v3: f64,
}
impl ForceFieldContribution for Mmff94Torsion {
fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
let p = |idx: usize| -> [f64; 3] {
[coords[idx * 3], coords[idx * 3 + 1], coords[idx * 3 + 2]]
};
let pi = p(self.atom_i);
let pj = p(self.atom_j);
let pk = p(self.atom_k);
let pl = p(self.atom_l);
let b1 = [pj[0] - pi[0], pj[1] - pi[1], pj[2] - pi[2]];
let b2 = [pk[0] - pj[0], pk[1] - pj[1], pk[2] - pj[2]];
let b3 = [pl[0] - pk[0], pl[1] - pk[1], pl[2] - pk[2]];
let cross = |a: [f64; 3], b: [f64; 3]| -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
};
let dot = |a: [f64; 3], b: [f64; 3]| -> f64 { a[0] * b[0] + a[1] * b[1] + a[2] * b[2] };
let n1 = cross(b1, b2);
let n2 = cross(b2, b3);
let m1 = cross(n1, b2);
let b2_len = dot(b2, b2).sqrt().max(1e-8);
let x = dot(n1, n2);
let y = dot(m1, n2) / b2_len;
let phi = (-y).atan2(x);
let energy = 0.5
* (self.v1 * (1.0 + phi.cos())
+ self.v2 * (1.0 - (2.0 * phi).cos())
+ self.v3 * (1.0 + (3.0 * phi).cos()));
let eps = 1e-5;
for atom_idx in [self.atom_i, self.atom_j, self.atom_k, self.atom_l] {
for dim in 0..3 {
let idx = atom_idx * 3 + dim;
let orig = coords[idx];
let mut c_plus = coords.to_vec();
let mut c_minus = coords.to_vec();
c_plus[idx] = orig + eps;
c_minus[idx] = orig - eps;
let phi_p = {
let pi = [
c_plus[self.atom_i * 3],
c_plus[self.atom_i * 3 + 1],
c_plus[self.atom_i * 3 + 2],
];
let pj = [
c_plus[self.atom_j * 3],
c_plus[self.atom_j * 3 + 1],
c_plus[self.atom_j * 3 + 2],
];
let pk = [
c_plus[self.atom_k * 3],
c_plus[self.atom_k * 3 + 1],
c_plus[self.atom_k * 3 + 2],
];
let pl = [
c_plus[self.atom_l * 3],
c_plus[self.atom_l * 3 + 1],
c_plus[self.atom_l * 3 + 2],
];
let b1 = [pj[0] - pi[0], pj[1] - pi[1], pj[2] - pi[2]];
let b2 = [pk[0] - pj[0], pk[1] - pj[1], pk[2] - pj[2]];
let b3 = [pl[0] - pk[0], pl[1] - pk[1], pl[2] - pk[2]];
let nn1 = cross(b1, b2);
let nn2 = cross(b2, b3);
let mm1 = cross(nn1, b2);
let b2l = dot(b2, b2).sqrt().max(1e-8);
(-dot(mm1, nn2) / b2l).atan2(dot(nn1, nn2))
};
let phi_m = {
let pi = [
c_minus[self.atom_i * 3],
c_minus[self.atom_i * 3 + 1],
c_minus[self.atom_i * 3 + 2],
];
let pj = [
c_minus[self.atom_j * 3],
c_minus[self.atom_j * 3 + 1],
c_minus[self.atom_j * 3 + 2],
];
let pk = [
c_minus[self.atom_k * 3],
c_minus[self.atom_k * 3 + 1],
c_minus[self.atom_k * 3 + 2],
];
let pl = [
c_minus[self.atom_l * 3],
c_minus[self.atom_l * 3 + 1],
c_minus[self.atom_l * 3 + 2],
];
let b1 = [pj[0] - pi[0], pj[1] - pi[1], pj[2] - pi[2]];
let b2 = [pk[0] - pj[0], pk[1] - pj[1], pk[2] - pj[2]];
let b3 = [pl[0] - pk[0], pl[1] - pk[1], pl[2] - pk[2]];
let nn1 = cross(b1, b2);
let nn2 = cross(b2, b3);
let mm1 = cross(nn1, b2);
let b2l = dot(b2, b2).sqrt().max(1e-8);
(-dot(mm1, nn2) / b2l).atan2(dot(nn1, nn2))
};
let e_p = 0.5
* (self.v1 * (1.0 + phi_p.cos())
+ self.v2 * (1.0 - (2.0 * phi_p).cos())
+ self.v3 * (1.0 + (3.0 * phi_p).cos()));
let e_m = 0.5
* (self.v1 * (1.0 + phi_m.cos())
+ self.v2 * (1.0 - (2.0 * phi_m).cos())
+ self.v3 * (1.0 + (3.0 * phi_m).cos()));
grad[idx] += (e_p - e_m) / (2.0 * eps);
}
}
energy
}
}
pub struct Mmff94BufferedVanDerWaals {
pub atom_i_idx: usize,
pub atom_j_idx: usize,
pub radius_star: f64, pub epsilon_depth: f64, }
impl ForceFieldContribution for Mmff94BufferedVanDerWaals {
fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
let root_i = self.atom_i_idx * 3;
let root_j = self.atom_j_idx * 3;
let delta_x = coords[root_i] - coords[root_j];
let delta_y = coords[root_i + 1] - coords[root_j + 1];
let delta_z = coords[root_i + 2] - coords[root_j + 2];
let dist_squared = delta_x * delta_x + delta_y * delta_y + delta_z * delta_z;
let mut dist_r = dist_squared.sqrt();
if dist_r < 1e-8 {
dist_r = 1e-8;
}
let r_star_powered_7 = self.radius_star.powi(7);
let dist_r_powered_7 = dist_r.powi(7);
let repulsive_denominator = dist_r + 0.07 * self.radius_star;
let repulsive_term = (1.07 * self.radius_star / repulsive_denominator).powi(7);
let attractive_denominator = dist_r_powered_7 + 0.12 * r_star_powered_7;
let attractive_term = (1.12 * r_star_powered_7 / attractive_denominator) - 2.0;
let vdw_total_energy = self.epsilon_depth * repulsive_term * attractive_term;
let gradient_rep_term = -7.0 * repulsive_term / repulsive_denominator;
let gradient_attr_term = -7.0 * dist_r.powi(6) * (1.12 * r_star_powered_7)
/ (attractive_denominator * attractive_denominator);
let force_scalar_magnitude = self.epsilon_depth
* (gradient_rep_term * attractive_term + repulsive_term * gradient_attr_term);
let vector_prefactor = force_scalar_magnitude / dist_r;
let grad_x = vector_prefactor * delta_x;
let grad_y = vector_prefactor * delta_y;
let grad_z = vector_prefactor * delta_z;
grad[root_i] += grad_x;
grad[root_i + 1] += grad_y;
grad[root_i + 2] += grad_z;
grad[root_j] -= grad_x;
grad[root_j + 1] -= grad_y;
grad[root_j + 2] -= grad_z;
vdw_total_energy
}
}
pub fn validate_gradients(term: &dyn ForceFieldContribution, coords: &[f64], eps: f64) -> f64 {
let n = coords.len();
let mut analytical_grad = vec![0.0; n];
term.evaluate_energy_and_inject_gradient(coords, &mut analytical_grad);
let mut max_err = 0.0f64;
for i in 0..n {
let mut c_plus = coords.to_vec();
let mut c_minus = coords.to_vec();
c_plus[i] += eps;
c_minus[i] -= eps;
let mut g_dummy = vec![0.0; n];
let e_plus = term.evaluate_energy_and_inject_gradient(&c_plus, &mut g_dummy);
g_dummy.fill(0.0);
let e_minus = term.evaluate_energy_and_inject_gradient(&c_minus, &mut g_dummy);
let numerical = (e_plus - e_minus) / (2.0 * eps);
let err = (analytical_grad[i] - numerical).abs();
max_err = max_err.max(err);
}
max_err
}
pub struct Mmff94Builder;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mmff94Variant {
Mmff94,
Mmff94s,
}
impl Mmff94Builder {
fn bond_params(elem_i: u8, elem_j: u8, _bond_order: u8) -> (f64, f64) {
let r_cov = |e: u8| -> f64 {
match e {
1 => 0.31,
6 => 0.76,
7 => 0.71,
8 => 0.66,
9 => 0.57,
15 => 1.07,
16 => 1.05,
17 => 1.02,
35 => 1.20,
53 => 1.39,
_ => 1.0,
}
};
let r0 = r_cov(elem_i) + r_cov(elem_j);
let kb = 5.0; (kb, r0)
}
pub fn build(
elements: &[u8],
bonds: &[(usize, usize, u8)],
) -> Vec<Box<dyn ForceFieldContribution>> {
Self::build_variant(elements, bonds, Mmff94Variant::Mmff94)
}
pub fn build_variant(
elements: &[u8],
bonds: &[(usize, usize, u8)],
variant: Mmff94Variant,
) -> Vec<Box<dyn ForceFieldContribution>> {
let n_atoms = elements.len();
let mut terms: Vec<Box<dyn ForceFieldContribution>> = Vec::new();
for &(i, j, order) in bonds {
let (kb, r0) = Self::bond_params(elements[i], elements[j], order);
terms.push(Box::new(Mmff94BondStretch {
atom_i: i,
atom_j: j,
k_b: kb,
r0,
}));
}
let mut neighbors: Vec<Vec<usize>> = vec![Vec::new(); n_atoms];
for &(i, j, _) in bonds {
neighbors[i].push(j);
neighbors[j].push(i);
}
for j in 0..n_atoms {
let nbrs = &neighbors[j];
for a in 0..nbrs.len() {
for b in (a + 1)..nbrs.len() {
let i = nbrs[a];
let k = nbrs[b];
terms.push(Box::new(Mmff94AngleBend {
atom_i: i,
atom_j: j,
atom_k: k,
k_a: 0.5, theta0: 109.5_f64.to_radians(), }));
}
}
}
for &(j, k, _) in bonds {
for &i in &neighbors[j] {
if i == k {
continue;
}
for &l in &neighbors[k] {
if l == j || l == i {
continue;
}
let (v1, v2, v3) = match variant {
Mmff94Variant::Mmff94s => (0.0, 1.5, 0.0),
Mmff94Variant::Mmff94 => (0.0, 1.0, 0.0),
};
terms.push(Box::new(Mmff94Torsion {
atom_i: i,
atom_j: j,
atom_k: k,
atom_l: l,
v1,
v2,
v3,
}));
}
}
}
for &(j, k, _) in bonds {
for &i in &neighbors[j] {
if i == k {
continue;
}
for &l in &neighbors[k] {
if l == j || l == i {
continue;
}
let r_star = 3.5; let eps = 0.05;
terms.push(Box::new(Mmff94BufferedVanDerWaals {
atom_i_idx: i,
atom_j_idx: l,
radius_star: r_star,
epsilon_depth: eps,
}));
}
}
}
terms
}
pub fn total_energy(
terms: &[Box<dyn ForceFieldContribution>],
coords: &[f64],
) -> (f64, Vec<f64>) {
let n = coords.len();
let mut grad = vec![0.0; n];
let mut total = 0.0;
for term in terms {
total += term.evaluate_energy_and_inject_gradient(coords, &mut grad);
}
(total, grad)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mmff94_vdw_energy() {
let term = Mmff94BufferedVanDerWaals {
atom_i_idx: 0,
atom_j_idx: 1,
radius_star: 3.6,
epsilon_depth: 0.1,
};
let coords = vec![0.0, 0.0, 0.0, 3.6, 0.0, 0.0];
let mut grad = vec![0.0; 6];
let e = term.evaluate_energy_and_inject_gradient(&coords, &mut grad);
assert!(e < 0.0 && e > -0.2, "vdW energy at R*: {e}");
}
#[test]
fn test_mmff94_vdw_gradient_validation() {
let term = Mmff94BufferedVanDerWaals {
atom_i_idx: 0,
atom_j_idx: 1,
radius_star: 3.6,
epsilon_depth: 0.1,
};
let coords = vec![0.0, 0.0, 0.0, 4.0, 0.0, 0.0];
let max_err = validate_gradients(&term, &coords, 1e-5);
assert!(max_err < 1e-4, "vdW gradient error: {max_err}");
}
#[test]
fn test_mmff94_bond_stretch() {
let term = Mmff94BondStretch {
atom_i: 0,
atom_j: 1,
k_b: 5.0,
r0: 1.5,
};
let coords_eq = vec![0.0, 0.0, 0.0, 1.5, 0.0, 0.0];
let mut grad = vec![0.0; 6];
let e_eq = term.evaluate_energy_and_inject_gradient(&coords_eq, &mut grad);
assert!(e_eq.abs() < 1e-10, "bond stretch at r0: {e_eq}");
let coords_stretch = vec![0.0, 0.0, 0.0, 2.0, 0.0, 0.0];
grad.fill(0.0);
let e_str = term.evaluate_energy_and_inject_gradient(&coords_stretch, &mut grad);
assert!(
e_str > 0.0,
"bond stretch energy should be positive: {e_str}"
);
}
#[test]
fn test_mmff94_bond_stretch_gradient_validation() {
let term = Mmff94BondStretch {
atom_i: 0,
atom_j: 1,
k_b: 5.0,
r0: 1.5,
};
let coords = vec![0.0, 0.0, 0.0, 2.0, 0.1, 0.0];
let max_err = validate_gradients(&term, &coords, 1e-5);
assert!(max_err < 1e-3, "bond stretch gradient error: {max_err}");
}
#[test]
fn test_mmff94_torsion_energy() {
let term = Mmff94Torsion {
atom_i: 0,
atom_j: 1,
atom_k: 2,
atom_l: 3,
v1: 0.0,
v2: 5.0,
v3: 0.0,
};
let coords = vec![-1.5, 1.0, 0.0, 0.0, 0.0, 0.0, 1.5, 0.0, 0.0, 3.0, 1.0, 0.0];
let mut grad = vec![0.0; 12];
let e = term.evaluate_energy_and_inject_gradient(&coords, &mut grad);
assert!(e.is_finite(), "torsion energy should be finite: {e}");
}
#[test]
fn test_mmff94_atom_typing() {
use crate::graph::Hybridization;
let t = assign_mmff94_type(6, &Hybridization::SP3, false, false);
assert_eq!(t, Mmff94AtomType::CR);
let t = assign_mmff94_type(6, &Hybridization::SP2, true, false);
assert_eq!(t, Mmff94AtomType::CB);
let t = assign_mmff94_type(7, &Hybridization::SP3, false, true);
assert_eq!(t, Mmff94AtomType::NAm);
}
#[test]
fn test_mmff94_builder_ethane() {
let elements = vec![6, 6, 1, 1, 1, 1, 1, 1]; let bonds = vec![
(0, 1, 1), (0, 2, 1),
(0, 3, 1),
(0, 4, 1), (1, 5, 1),
(1, 6, 1),
(1, 7, 1), ];
let coords = vec![
0.0, 0.0, 0.0, 1.54, 0.0, 0.0, -0.5, 0.9, 0.0, -0.5, -0.9, 0.0, -0.5, 0.0, 0.9, 2.04, 0.9, 0.0, 2.04, -0.9, 0.0, 2.04, 0.0, 0.9, ];
let terms = Mmff94Builder::build(&elements, &bonds);
assert!(!terms.is_empty(), "should produce force field terms");
let (energy, grad) = Mmff94Builder::total_energy(&terms, &coords);
assert!(
energy.is_finite(),
"total energy should be finite: {energy}"
);
assert!(
grad.iter().all(|g| g.is_finite()),
"all gradients should be finite"
);
}
#[test]
fn test_mmff94_builder_gradient_consistency() {
let elements = vec![6, 6];
let bonds = vec![(0, 1, 1)];
let coords = vec![0.0, 0.0, 0.0, 1.6, 0.1, 0.0];
let terms = Mmff94Builder::build(&elements, &bonds);
let (_, analytical_grad) = Mmff94Builder::total_energy(&terms, &coords);
let eps = 1e-5;
for i in 0..coords.len() {
let mut cp = coords.clone();
let mut cm = coords.clone();
cp[i] += eps;
cm[i] -= eps;
let (ep, _) = Mmff94Builder::total_energy(&terms, &cp);
let (em, _) = Mmff94Builder::total_energy(&terms, &cm);
let numerical = (ep - em) / (2.0 * eps);
let err = (analytical_grad[i] - numerical).abs();
assert!(
err < 0.1,
"gradient mismatch at coord {i}: anal={:.6} num={:.6} err={:.6}",
analytical_grad[i],
numerical,
err
);
}
}
}