#![cfg(feature = "alpha-reaxff")]
use sci_form::forcefield::reaxff::bond_order::compute_bond_orders;
use sci_form::forcefield::reaxff::eem::{default_eem_params, solve_eem};
use sci_form::forcefield::reaxff::energy::compute_bonded_energy;
use sci_form::forcefield::reaxff::gradients::compute_reaxff_gradient;
use sci_form::forcefield::reaxff::nonbonded::compute_nonbonded_energy;
use sci_form::forcefield::reaxff::params::ReaxffParams;
#[test]
fn h2_bond_order_near_one_at_equilibrium() {
let params = ReaxffParams::default_chon();
let h_idx = params.element_index(1).unwrap();
let atom_params = vec![
params.atom_params[h_idx].clone(),
params.atom_params[h_idx].clone(),
];
let positions = [0.0, 0.0, 0.0, 0.74, 0.0, 0.0];
let bo = compute_bond_orders(&positions, &atom_params, params.cutoff);
let total = bo[0][1].total;
assert!(
total > 0.3,
"H₂ bond order at 0.74 Å should be > 0.3, got {:.4}",
total
);
}
#[test]
fn bond_order_decreases_with_distance() {
let params = ReaxffParams::default_chon();
let h_idx = params.element_index(1).unwrap();
let atom_params = vec![
params.atom_params[h_idx].clone(),
params.atom_params[h_idx].clone(),
];
let mut last_bo = f64::MAX;
for &r in &[0.7, 1.0, 1.5, 2.0, 3.0] {
let positions = [0.0, 0.0, 0.0, r, 0.0, 0.0];
let bo = compute_bond_orders(&positions, &atom_params, params.cutoff);
let total = bo[0][1].total;
assert!(
total <= last_bo + 1e-10,
"BO at {:.1} Å ({:.4}) should be ≤ BO at shorter distance ({:.4})",
r,
total,
last_bo
);
last_bo = total;
}
}
#[test]
fn bond_order_zero_beyond_cutoff() {
let params = ReaxffParams::default_chon();
let h_idx = params.element_index(1).unwrap();
let atom_params = vec![
params.atom_params[h_idx].clone(),
params.atom_params[h_idx].clone(),
];
let r = params.cutoff + 1.0;
let positions = [0.0, 0.0, 0.0, r, 0.0, 0.0];
let bo = compute_bond_orders(&positions, &atom_params, params.cutoff);
assert!(
bo[0][1].total < 1e-10,
"BO beyond cutoff should be ~0, got {:.6}",
bo[0][1].total
);
}
#[test]
fn eem_h2_charge_neutrality() {
let positions = [0.0, 0.0, 0.0, 0.74, 0.0, 0.0];
let eem_params = vec![default_eem_params(1), default_eem_params(1)];
let charges = solve_eem(&positions, &eem_params, 0.0).unwrap();
let sum: f64 = charges.iter().sum();
assert!(
sum.abs() < 1e-10,
"EEM charges must sum to 0 for neutral H₂, got {:.12}",
sum
);
}
#[test]
fn eem_h2_charges_symmetric() {
let positions = [0.0, 0.0, 0.0, 0.74, 0.0, 0.0];
let eem_params = vec![default_eem_params(1), default_eem_params(1)];
let charges = solve_eem(&positions, &eem_params, 0.0).unwrap();
assert!(
(charges[0] - charges[1]).abs() < 1e-10,
"Symmetric H₂ should have equal charges: {:.6} vs {:.6}",
charges[0],
charges[1]
);
}
#[test]
fn eem_co_polarized_charges() {
let positions = [0.0, 0.0, 0.0, 1.128, 0.0, 0.0]; let eem_params = vec![default_eem_params(6), default_eem_params(8)];
let charges = solve_eem(&positions, &eem_params, 0.0).unwrap();
let sum: f64 = charges.iter().sum();
assert!(sum.abs() < 1e-10, "Total charge must be neutral");
assert!(
(charges[0] + charges[1]).abs() < 1e-10,
"Charges should be equal and opposite"
);
}
#[test]
fn nonbonded_repulsive_at_short_range() {
let positions = [0.0, 0.0, 0.0, 0.3, 0.0, 0.0]; let charges = [0.0, 0.0];
let elements = [1u8, 1];
let (e_vdw, _e_coul) = compute_nonbonded_energy(&positions, &charges, &elements, 10.0);
assert!(
e_vdw > 0.0,
"vdW energy at 0.3 Å should be repulsive, got {:.4}",
e_vdw
);
}
#[test]
fn nonbonded_zero_coulomb_for_neutral_atoms() {
let positions = [0.0, 0.0, 0.0, 2.0, 0.0, 0.0];
let charges = [0.0, 0.0];
let elements = [1u8, 1];
let (_e_vdw, e_coul) = compute_nonbonded_energy(&positions, &charges, &elements, 10.0);
assert!(
e_coul.abs() < 1e-10,
"Coulomb energy for neutral atoms should be 0, got {:.6}",
e_coul
);
}
#[test]
fn reaxff_gradient_h2_finite() {
let positions = [0.0, 0.0, 0.0, 0.74, 0.0, 0.0];
let elements = [1u8, 1];
let params = ReaxffParams::default_chon();
let (energy, grad) = compute_reaxff_gradient(&positions, &elements, ¶ms).unwrap();
assert!(energy.is_finite(), "energy must be finite");
assert_eq!(grad.len(), 6);
for (i, g) in grad.iter().enumerate() {
assert!(g.is_finite(), "gradient[{}] must be finite, got {}", i, g);
}
}
#[test]
fn reaxff_gradient_newtons_third_law() {
let positions = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
let elements = [1u8, 1];
let params = ReaxffParams::default_chon();
let (_, grad) = compute_reaxff_gradient(&positions, &elements, ¶ms).unwrap();
for d in 0..3 {
assert!(
(grad[d] + grad[3 + d]).abs() < 0.1,
"Forces should be opposite in dim {}: {:.6} vs {:.6}",
d,
grad[d],
grad[3 + d]
);
}
}
#[test]
fn reaxff_h2_energy_minimum_near_equilibrium() {
let elements = [1u8, 1];
let params = ReaxffParams::default_chon();
let e_equil = compute_reaxff_gradient(&[0.0, 0.0, 0.0, 0.74, 0.0, 0.0], &elements, ¶ms)
.unwrap()
.0;
let e_compressed =
compute_reaxff_gradient(&[0.0, 0.0, 0.0, 0.50, 0.0, 0.0], &elements, ¶ms)
.unwrap()
.0;
let e_stretched = compute_reaxff_gradient(&[0.0, 0.0, 0.0, 2.00, 0.0, 0.0], &elements, ¶ms)
.unwrap()
.0;
assert!(
e_equil.is_finite() && e_compressed.is_finite() && e_stretched.is_finite(),
"All energies must be finite"
);
}
#[test]
fn bonded_energy_decomposition_consistent() {
let params = ReaxffParams::default_chon();
let h_idx = params.element_index(1).unwrap();
let atom_params_2h = vec![
params.atom_params[h_idx].clone(),
params.atom_params[h_idx].clone(),
];
let local_params = ReaxffParams {
atom_params: atom_params_2h.clone(),
bond_de: vec![vec![100.0; 2]; 2],
..params.clone()
};
let positions = [0.0, 0.0, 0.0, 0.74, 0.0, 0.0];
let bo = compute_bond_orders(&positions, &atom_params_2h, local_params.cutoff);
let e = compute_bonded_energy(&positions, &bo, &local_params);
let sum = e.bond_energy
+ e.angle_energy
+ e.torsion_energy
+ e.over_coord_energy
+ e.under_coord_energy;
assert!(
(sum - e.total).abs() < 1e-10,
"Components ({:.6}) should equal total ({:.6})",
sum,
e.total
);
}