use crate::constants::codata2018::BOHR_RADIUS_ANGSTROMS as A0_BOHR;
use crate::constants::standard_atomic_mass;
use crate::integrals::multipoles::DerivedMultipoleParams;
use crate::parameters::ParameterModel;
use crate::types::{AlignedMatrix, MolecularBatch};
pub const E_ANGSTROM_TO_DEBYE: f64 = 4.80320425;
#[derive(Debug, Clone, PartialEq)]
pub struct DipoleResult {
pub point_charge: [f64; 4],
pub hybridization: [f64; 4],
pub total: [f64; 4],
pub net_charge: f64,
pub center_of_mass: [f64; 3],
pub atomic_charges: Vec<f64>,
}
#[allow(clippy::needless_range_loop)]
pub fn compute_dipole_moment<M: ?Sized + ParameterModel>(
batch: &MolecularBatch,
model: &M,
density: &AlignedMatrix<f64>,
) -> DipoleResult {
let natoms = batch.natoms;
let mut charges = Vec::with_capacity(natoms);
let mut net_charge = 0.0;
for a in 0..natoms {
let z = batch.atomic_numbers[a];
let core_charge = model.get_element(z).map(|p| p.core_charge).unwrap_or(0.0);
let orb_start = batch.orbital_offsets[a];
let norbs = batch.basis_types[a].num_orbitals();
let mut pop = 0.0;
for o in 0..norbs {
pop += density.get(orb_start + o, orb_start + o);
}
let q = core_charge - pop;
charges.push(q);
net_charge += q;
}
let is_charged = net_charge.abs() > 0.5;
let mut com = [0.0; 3];
let mut total_mass = 0.0;
if is_charged {
for a in 0..natoms {
let m = standard_atomic_mass(batch.atomic_numbers[a]);
total_mass += m;
com[0] += m * batch.x[a];
com[1] += m * batch.y[a];
com[2] += m * batch.z[a];
}
if total_mass > 0.0 {
com[0] /= total_mass;
com[1] /= total_mass;
com[2] /= total_mass;
}
}
let mut pt_dip = [0.0; 4];
for a in 0..natoms {
let q = charges[a];
let rx = batch.x[a] - com[0];
let ry = batch.y[a] - com[1];
let rz = batch.z[a] - com[2];
pt_dip[0] += q * rx * E_ANGSTROM_TO_DEBYE;
pt_dip[1] += q * ry * E_ANGSTROM_TO_DEBYE;
pt_dip[2] += q * rz * E_ANGSTROM_TO_DEBYE;
}
pt_dip[3] = (pt_dip[0] * pt_dip[0] + pt_dip[1] * pt_dip[1] + pt_dip[2] * pt_dip[2]).sqrt();
let mut hyb_dip = [0.0; 4];
for a in 0..natoms {
let z = batch.atomic_numbers[a];
if z == 1 {
continue;
}
let p = model
.get_element(z)
.unwrap_or_else(|| panic!("Parameters missing for element Z={}", z));
let mp = DerivedMultipoleParams::from_element(&p);
let orb_start = batch.orbital_offsets[a];
let norbs = batch.basis_types[a].num_orbitals();
if norbs >= 4 {
let hyfsp = 2.0 * mp.dd * A0_BOHR * E_ANGSTROM_TO_DEBYE;
let s_idx = orb_start;
let px_idx = orb_start + 1;
let py_idx = orb_start + 2;
let pz_idx = orb_start + 3;
let p_spx = density.get(s_idx, px_idx);
let p_spy = density.get(s_idx, py_idx);
let p_spz = density.get(s_idx, pz_idx);
hyb_dip[0] -= hyfsp * p_spx;
hyb_dip[1] -= hyfsp * p_spy;
hyb_dip[2] -= hyfsp * p_spz;
}
}
hyb_dip[3] =
(hyb_dip[0] * hyb_dip[0] + hyb_dip[1] * hyb_dip[1] + hyb_dip[2] * hyb_dip[2]).sqrt();
let mut tot_dip = [0.0; 4];
tot_dip[0] = pt_dip[0] + hyb_dip[0];
tot_dip[1] = pt_dip[1] + hyb_dip[1];
tot_dip[2] = pt_dip[2] + hyb_dip[2];
tot_dip[3] =
(tot_dip[0] * tot_dip[0] + tot_dip[1] * tot_dip[1] + tot_dip[2] * tot_dip[2]).sqrt();
DipoleResult {
point_charge: pt_dip,
hybridization: hyb_dip,
total: tot_dip,
net_charge,
center_of_mass: com,
atomic_charges: charges,
}
}