#![allow(
unused_imports,
unused_variables,
dead_code,
clippy::unnecessary_cast,
clippy::needless_range_loop,
clippy::manual_repeat_n,
clippy::manual_str_repeat,
clippy::manual_is_multiple_of,
clippy::redundant_field_names,
clippy::useless_vec,
clippy::single_range_in_vec_init
)]
use nalgebra::DMatrix;
use serde::Deserialize;
use std::fs;
#[derive(Deserialize)]
struct RefAtom {
element: u8,
formal_charge: i8,
x: f64,
y: f64,
z: f64,
}
#[derive(Deserialize)]
struct RefBond {
start: usize,
end: usize,
order: String,
}
#[derive(Deserialize)]
struct RefTorsion {
atoms: Vec<usize>,
signs: Vec<i32>,
v: Vec<f64>,
}
#[derive(Deserialize)]
struct RefMolecule {
smiles: String,
atoms: Vec<RefAtom>,
bonds: Vec<RefBond>,
torsions: Vec<RefTorsion>,
}
fn build_mol_from_ref(ref_mol: &RefMolecule) -> sci_form::graph::Molecule {
let mut mol = sci_form::graph::Molecule::new(&ref_mol.smiles);
let mut node_indices = Vec::new();
for atom in &ref_mol.atoms {
let new_atom = sci_form::graph::Atom {
element: atom.element,
position: nalgebra::Vector3::new(0.0, 0.0, 0.0),
charge: 0.0,
formal_charge: atom.formal_charge,
hybridization: sci_form::graph::Hybridization::Unknown,
chiral_tag: sci_form::graph::ChiralType::Unspecified,
explicit_h: if atom.element == 1 || atom.element == 0 {
1
} else {
0
},
};
node_indices.push(mol.add_atom(new_atom));
}
for bond in &ref_mol.bonds {
let order = match bond.order.as_str() {
"DOUBLE" => sci_form::graph::BondOrder::Double,
"TRIPLE" => sci_form::graph::BondOrder::Triple,
"AROMATIC" => sci_form::graph::BondOrder::Aromatic,
_ => sci_form::graph::BondOrder::Single,
};
mol.add_bond(
node_indices[bond.start],
node_indices[bond.end],
sci_form::graph::Bond {
order,
stereo: sci_form::graph::BondStereo::None,
},
);
}
mol
}
fn build_csd_torsions(t: &[RefTorsion]) -> Vec<sci_form::forcefield::etkdg_3d::M6TorsionContrib> {
t.iter()
.filter_map(|t| {
if t.atoms.len() < 4 || t.v.len() < 6 || t.signs.len() < 6 {
return None;
}
let mut signs = [0.0f64; 6];
let mut v = [0.0f64; 6];
for k in 0..6 {
signs[k] = t.signs[k] as f64;
v[k] = t.v[k];
}
Some(sci_form::forcefield::etkdg_3d::M6TorsionContrib {
i: t.atoms[0],
j: t.atoms[1],
k: t.atoms[2],
l: t.atoms[3],
signs,
v,
})
})
.collect()
}
fn torsion_only_energy(
coords: &[f64],
n: usize,
ff: &sci_form::forcefield::etkdg_3d::Etkdg3DFF,
) -> f64 {
let c = |atom: usize, d: usize| -> f64 { coords[atom * 3 + d] };
let mut energy = 0.0f64;
for tc in &ff.torsion_contribs {
let r1 = [
c(tc.i, 0) - c(tc.j, 0),
c(tc.i, 1) - c(tc.j, 1),
c(tc.i, 2) - c(tc.j, 2),
];
let r2 = [
c(tc.k, 0) - c(tc.j, 0),
c(tc.k, 1) - c(tc.j, 1),
c(tc.k, 2) - c(tc.j, 2),
];
let r3 = [
c(tc.j, 0) - c(tc.k, 0),
c(tc.j, 1) - c(tc.k, 1),
c(tc.j, 2) - c(tc.k, 2),
];
let r4 = [
c(tc.l, 0) - c(tc.k, 0),
c(tc.l, 1) - c(tc.k, 1),
c(tc.l, 2) - c(tc.k, 2),
];
let t1 = [
r1[1] * r2[2] - r1[2] * r2[1],
r1[2] * r2[0] - r1[0] * r2[2],
r1[0] * r2[1] - r1[1] * r2[0],
];
let t2 = [
r3[1] * r4[2] - r3[2] * r4[1],
r3[2] * r4[0] - r3[0] * r4[2],
r3[0] * r4[1] - r3[1] * r4[0],
];
let d1 = (t1[0] * t1[0] + t1[1] * t1[1] + t1[2] * t1[2]).sqrt();
let d2 = (t2[0] * t2[0] + t2[1] * t2[1] + t2[2] * t2[2]).sqrt();
if d1 < 1e-10 || d2 < 1e-10 {
continue;
}
let n1 = [t1[0] / d1, t1[1] / d1, t1[2] / d1];
let n2 = [t2[0] / d2, t2[1] / d2, t2[2] / d2];
let cos_phi = (n1[0] * n2[0] + n1[1] * n2[1] + n1[2] * n2[2]).clamp(-1.0, 1.0);
let cp2 = cos_phi * cos_phi;
let cp3 = cos_phi * cp2;
let cp4 = cos_phi * cp3;
let cp5 = cos_phi * cp4;
let ss = &tc.signs;
let vv = &tc.v;
energy += vv[0] * (1.0 + ss[0] * cos_phi)
+ vv[1] * (1.0 + ss[1] * (2.0 * cp2 - 1.0))
+ vv[2] * (1.0 + ss[2] * (4.0 * cp3 - 3.0 * cos_phi))
+ vv[3] * (1.0 + ss[3] * (8.0 * cp4 - 8.0 * cp2 + 1.0))
+ vv[4] * (1.0 + ss[4] * (16.0 * cp5 - 20.0 * cp3 + 5.0 * cos_phi))
+ vv[5] * (1.0 + ss[5] * (32.0 * cp5 * cos_phi - 48.0 * cp4 + 18.0 * cp2 - 1.0));
}
energy
}
fn inversion_only_energy(
coords: &[f64],
n: usize,
ff: &sci_form::forcefield::etkdg_3d::Etkdg3DFF,
) -> f64 {
let c = |atom: usize, d: usize| -> f64 { coords[atom * 3 + d] };
let mut energy = 0.0f64;
for ic in &ff.inversion_contribs {
let rji = [
c(ic.at1, 0) - c(ic.at2, 0),
c(ic.at1, 1) - c(ic.at2, 1),
c(ic.at1, 2) - c(ic.at2, 2),
];
let rjk = [
c(ic.at3, 0) - c(ic.at2, 0),
c(ic.at3, 1) - c(ic.at2, 1),
c(ic.at3, 2) - c(ic.at2, 2),
];
let rjl = [
c(ic.at4, 0) - c(ic.at2, 0),
c(ic.at4, 1) - c(ic.at2, 1),
c(ic.at4, 2) - c(ic.at2, 2),
];
let n_vec = [
rji[1] * rjk[2] - rji[2] * rjk[1],
rji[2] * rjk[0] - rji[0] * rjk[2],
rji[0] * rjk[1] - rji[1] * rjk[0],
];
let n_len = (n_vec[0] * n_vec[0] + n_vec[1] * n_vec[1] + n_vec[2] * n_vec[2]).sqrt();
let rjl_len = (rjl[0] * rjl[0] + rjl[1] * rjl[1] + rjl[2] * rjl[2]).sqrt();
if n_len < 1e-8 || rjl_len < 1e-8 {
continue;
}
let sin_y = (n_vec[0] * rjl[0] + n_vec[1] * rjl[1] + n_vec[2] * rjl[2]) / (n_len * rjl_len);
let sin_y = sin_y.clamp(-1.0, 1.0);
let cos_y_sq = 1.0 - sin_y * sin_y;
let cos2w = 2.0 * sin_y * sin_y - 1.0; energy += ff.oop_k * ic.force_constant * (ic.c0 + ic.c1 * sin_y + ic.c2 * cos2w);
}
energy
}
fn dist_only_energy(
coords: &[f64],
n: usize,
ff: &sci_form::forcefield::etkdg_3d::Etkdg3DFF,
) -> f64 {
let c = |atom: usize, d: usize| -> f64 { coords[atom * 3 + d] };
let mut energy = 0.0f64;
for dc in ff
.dist_12
.iter()
.chain(ff.dist_13.iter())
.chain(ff.dist_long.iter())
{
let dx = c(dc.i, 0) - c(dc.j, 0);
let dy = c(dc.i, 1) - c(dc.j, 1);
let dz = c(dc.i, 2) - c(dc.j, 2);
let d = (dx * dx + dy * dy + dz * dz).sqrt();
let term = if d < dc.min_len {
d - dc.min_len
} else if d > dc.max_len {
d - dc.max_len
} else {
0.0
};
energy += dc.k * term * term;
}
energy
}
fn angle_only_energy(
coords: &[f64],
n: usize,
ff: &sci_form::forcefield::etkdg_3d::Etkdg3DFF,
) -> f64 {
let c = |atom: usize, d: usize| -> f64 { coords[atom * 3 + d] };
let mut energy = 0.0f64;
for ac in &ff.angle_constraints {
let p1 = [c(ac.i, 0), c(ac.i, 1), c(ac.i, 2)];
let p2 = [c(ac.j, 0), c(ac.j, 1), c(ac.j, 2)];
let p3 = [c(ac.k, 0), c(ac.k, 1), c(ac.k, 2)];
let r1 = [p1[0] - p2[0], p1[1] - p2[1], p1[2] - p2[2]];
let r2 = [p3[0] - p2[0], p3[1] - p2[1], p3[2] - p2[2]];
let l1 = (r1[0] * r1[0] + r1[1] * r1[1] + r1[2] * r1[2]).sqrt();
let l2 = (r2[0] * r2[0] + r2[1] * r2[1] + r2[2] * r2[2]).sqrt();
if l1 < 1e-8 || l2 < 1e-8 {
continue;
}
let cos_theta =
((r1[0] * r2[0] + r1[1] * r2[1] + r1[2] * r2[2]) / (l1 * l2)).clamp(-1.0, 1.0);
let theta_deg = cos_theta.acos() * 180.0 / std::f64::consts::PI;
let term = if theta_deg < ac.min_deg {
theta_deg - ac.min_deg
} else if theta_deg > ac.max_deg {
theta_deg - ac.max_deg
} else {
0.0
};
energy += ac.force_k * term * term;
}
energy
}
fn numerical_gradient(energy_fn: &dyn Fn(&[f64]) -> f64, coords: &[f64], eps: f64) -> Vec<f64> {
let dim = coords.len();
let mut grad = vec![0.0f64; dim];
for i in 0..dim {
let mut cp = coords.to_vec();
cp[i] = coords[i] + eps;
let ep = energy_fn(&cp);
cp[i] = coords[i] - eps;
let em = energy_fn(&cp);
grad[i] = (ep - em) / (2.0 * eps);
}
grad
}
#[test]
fn test_gradient_perterm() {
let ref_data =
sci_form::fixture_io::read_text_fixture("tests/fixtures/gdb20_reference_1k.json").unwrap();
let ref_mols: Vec<RefMolecule> = serde_json::from_str(&ref_data).unwrap();
let test_indices = [10, 13, 47, 50, 89, 93];
let eps = 1e-5;
for &mol_idx in &test_indices {
if mol_idx >= ref_mols.len() {
continue;
}
let ref_mol = &ref_mols[mol_idx];
let mol = build_mol_from_ref(ref_mol);
let csd_torsions = build_csd_torsions(&ref_mol.torsions);
let n = mol.graph.node_count();
let mut coords = vec![0.0f64; n * 3];
for (a, atom) in ref_mol.atoms.iter().enumerate() {
coords[a * 3] = atom.x;
coords[a * 3 + 1] = atom.y;
coords[a * 3 + 2] = atom.z;
}
let coords_mat = DMatrix::from_fn(n, 3, |r, c| coords[r * 3 + c]);
let bounds = {
let raw = sci_form::distgeom::calculate_bounds_matrix_opts(&mol, true);
let mut b = raw.clone();
if sci_form::distgeom::triangle_smooth_tol(&mut b, 0.0) {
b
} else {
let raw2 = sci_form::distgeom::calculate_bounds_matrix_opts(&mol, false);
let mut b2 = raw2.clone();
if sci_form::distgeom::triangle_smooth_tol(&mut b2, 0.0) {
b2
} else {
let mut b3 = raw2;
sci_form::distgeom::triangle_smooth_tol(&mut b3, 0.05);
b3
}
}
};
let ff = sci_form::forcefield::etkdg_3d::build_etkdg_3d_ff_with_torsions(
&mol,
&coords_mat,
&bounds,
&csd_torsions,
);
println!("\n=== mol[{}] {} (n={}) ===", mol_idx, ref_mol.smiles, n);
println!(
" FF: {} torsions, {} inversions, {} angles, {} dist_12, {} dist_13, {} dist_long",
ff.torsion_contribs.len(),
ff.inversion_contribs.len(),
ff.angle_constraints.len(),
ff.dist_12.len(),
ff.dist_13.len(),
ff.dist_long.len()
);
let anal_grad =
sci_form::forcefield::etkdg_3d::etkdg_3d_gradient_f64(&coords, n, &mol, &ff);
let num_grad = numerical_gradient(
&|c| sci_form::forcefield::etkdg_3d::etkdg_3d_energy_f64(c, n, &mol, &ff),
&coords,
eps,
);
let mut worst_i = 0;
let mut worst_rel = 0.0f64;
for i in 0..(n * 3) {
let denom = anal_grad[i].abs().max(num_grad[i].abs()).max(1e-8);
let rel = (anal_grad[i] - num_grad[i]).abs() / denom;
if rel > worst_rel {
worst_rel = rel;
worst_i = i;
}
}
let atom = worst_i / 3;
let dim = ["x", "y", "z"][worst_i % 3];
println!(
" TOTAL worst: component={} (atom {} {}) anal={:.8e} num={:.8e} rel_err={:.4e}",
worst_i, atom, dim, anal_grad[worst_i], num_grad[worst_i], worst_rel
);
let check_term = |name: &str, energy_fn: &dyn Fn(&[f64]) -> f64| {
let num_g = {
let mut cp = coords.clone();
cp[worst_i] = coords[worst_i] + eps;
let ep = energy_fn(&cp);
cp[worst_i] = coords[worst_i] - eps;
let em = energy_fn(&cp);
(ep - em) / (2.0 * eps)
};
println!(" {}: num_grad[{}]={:.8e}", name, worst_i, num_g);
};
let ff_ref = &ff;
check_term("Torsions", &|c| torsion_only_energy(c, n, ff_ref));
check_term("Inversions", &|c| inversion_only_energy(c, n, ff_ref));
check_term("Distances", &|c| dist_only_energy(c, n, ff_ref));
check_term("Angles", &|c| angle_only_energy(c, n, ff_ref));
let torsion_num = {
let mut cp = coords.clone();
cp[worst_i] = coords[worst_i] + eps;
let ep = torsion_only_energy(&cp, n, &ff);
cp[worst_i] = coords[worst_i] - eps;
let em = torsion_only_energy(&cp, n, &ff);
(ep - em) / (2.0 * eps)
};
let inv_num = {
let mut cp = coords.clone();
cp[worst_i] = coords[worst_i] + eps;
let ep = inversion_only_energy(&cp, n, &ff);
cp[worst_i] = coords[worst_i] - eps;
let em = inversion_only_energy(&cp, n, &ff);
(ep - em) / (2.0 * eps)
};
let dist_num = {
let mut cp = coords.clone();
cp[worst_i] = coords[worst_i] + eps;
let ep = dist_only_energy(&cp, n, &ff);
cp[worst_i] = coords[worst_i] - eps;
let em = dist_only_energy(&cp, n, &ff);
(ep - em) / (2.0 * eps)
};
let angle_num = {
let mut cp = coords.clone();
cp[worst_i] = coords[worst_i] + eps;
let ep = angle_only_energy(&cp, n, &ff);
cp[worst_i] = coords[worst_i] - eps;
let em = angle_only_energy(&cp, n, &ff);
(ep - em) / (2.0 * eps)
};
let sum_parts = torsion_num + inv_num + dist_num + angle_num;
println!(
" Sum-of-parts num: {:.8e} (torsion={:.4e} inv={:.4e} dist={:.4e} angle={:.4e})",
sum_parts, torsion_num, inv_num, dist_num, angle_num
);
println!(" Full num: {:.8e}", num_grad[worst_i]);
println!(" Analytical: {:.8e}", anal_grad[worst_i]);
}
}