use nalgebra::{DMatrix, DVector};
#[derive(Debug, Clone)]
pub struct EemAtomParams {
pub chi: f64,
pub eta: f64,
pub gamma: f64,
}
pub fn solve_eem(
positions_flat: &[f64],
atom_params: &[EemAtomParams],
total_charge: f64,
) -> Result<Vec<f64>, String> {
let n = atom_params.len();
if n == 0 {
return Ok(vec![]);
}
let dim = n + 1;
let mut a = DMatrix::zeros(dim, dim);
let mut b = DVector::zeros(dim);
for i in 0..n {
a[(i, i)] = atom_params[i].eta;
for j in (i + 1)..n {
let dx = positions_flat[3 * i] - positions_flat[3 * j];
let dy = positions_flat[3 * i + 1] - positions_flat[3 * j + 1];
let dz = positions_flat[3 * i + 2] - positions_flat[3 * j + 2];
let r2 = dx * dx + dy * dy + dz * dz;
let gamma_ij = (atom_params[i].gamma + atom_params[j].gamma) / 2.0;
let shielded = 1.0 / (r2.powf(1.5) + gamma_ij.powi(3)).powf(1.0 / 3.0);
let coulomb = 14.4 * shielded;
a[(i, j)] = coulomb;
a[(j, i)] = coulomb;
}
a[(i, n)] = 1.0;
a[(n, i)] = 1.0;
b[i] = -atom_params[i].chi;
}
b[n] = total_charge;
let solution = a.lu().solve(&b).ok_or("EEM linear system is singular")?;
let charges: Vec<f64> = solution.rows(0, n).iter().copied().collect();
Ok(charges)
}
pub fn default_eem_params(element: u8) -> EemAtomParams {
match element {
1 => EemAtomParams {
chi: 3.7248,
eta: 9.6093,
gamma: 0.8203,
}, 6 => EemAtomParams {
chi: 5.9666,
eta: 7.0000,
gamma: 0.9000,
}, 7 => EemAtomParams {
chi: 6.8418,
eta: 8.0555,
gamma: 0.7820,
}, 8 => EemAtomParams {
chi: 8.5000,
eta: 8.3122,
gamma: 1.0898,
}, _ => EemAtomParams {
chi: 5.0,
eta: 7.0,
gamma: 0.9,
}, }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn eem_h2_charges_sum_to_zero() {
let positions = [0.0, 0.0, 0.0, 0.74, 0.0, 0.0];
let params = vec![default_eem_params(1), default_eem_params(1)];
let charges = solve_eem(&positions, ¶ms, 0.0).unwrap();
let sum: f64 = charges.iter().sum();
assert!(
sum.abs() < 1e-10,
"charges should sum to total charge (0), got {sum}"
);
}
#[test]
fn eem_symmetric_molecule_equal_charges() {
let positions = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
let params = vec![default_eem_params(1), default_eem_params(1)];
let charges = solve_eem(&positions, ¶ms, 0.0).unwrap();
assert!(
(charges[0] - charges[1]).abs() < 1e-10,
"symmetric molecule should have equal charges"
);
}
#[test]
fn eem_nonzero_total_charge() {
let positions = [0.0, 0.0, 0.0, 1.5, 0.0, 0.0];
let params = vec![default_eem_params(6), default_eem_params(8)];
let charges = solve_eem(&positions, ¶ms, 1.0).unwrap();
let sum: f64 = charges.iter().sum();
assert!(
(sum - 1.0).abs() < 1e-8,
"charges should sum to total charge (+1), got {sum}"
);
}
#[test]
fn default_eem_params_known_elements() {
let h = default_eem_params(1);
let c = default_eem_params(6);
assert!(h.eta > 0.0);
assert!(
c.chi != h.chi,
"C and H should have different electronegativities"
);
}
}