use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RdfDescriptors {
pub radii: Vec<f64>,
pub rdf_unweighted: Vec<f64>,
pub rdf_mass: Vec<f64>,
pub rdf_electronegativity: Vec<f64>,
pub rdf_charge: Vec<f64>,
pub beta: f64,
pub n_pairs: usize,
}
pub fn compute_rdf(
elements: &[u8],
positions: &[[f64; 3]],
charges: &[f64],
beta: f64,
r_max: f64,
n_points: usize,
) -> RdfDescriptors {
let n = elements.len().min(positions.len());
let beta = if beta <= 0.0 { 100.0 } else { beta };
let r_max = if r_max <= 0.0 { 12.0 } else { r_max };
if n < 2 || n_points == 0 {
return RdfDescriptors {
radii: vec![],
rdf_unweighted: vec![],
rdf_mass: vec![],
rdf_electronegativity: vec![],
rdf_charge: vec![],
beta,
n_pairs: 0,
};
}
let dr = r_max / n_points as f64;
let radii: Vec<f64> = (0..n_points).map(|k| (k as f64 + 0.5) * dr).collect();
let mut rdf_unweighted = vec![0.0f64; n_points];
let mut rdf_mass = vec![0.0f64; n_points];
let mut rdf_en = vec![0.0f64; n_points];
let mut rdf_charge = vec![0.0f64; n_points];
let has_charges = charges.len() >= n;
let mut n_pairs = 0usize;
for i in 0..n {
let mi = atomic_mass_rdf(elements[i]);
let eni = electronegativity_rdf(elements[i]);
let qi = if has_charges { charges[i] } else { 0.0 };
for j in (i + 1)..n {
let mj = atomic_mass_rdf(elements[j]);
let enj = electronegativity_rdf(elements[j]);
let qj = if has_charges { charges[j] } else { 0.0 };
let dx = positions[i][0] - positions[j][0];
let dy = positions[i][1] - positions[j][1];
let dz = positions[i][2] - positions[j][2];
let rij = (dx * dx + dy * dy + dz * dz).sqrt();
n_pairs += 1;
for (k, &rk) in radii.iter().enumerate() {
let g = (-beta * (rk - rij) * (rk - rij)).exp();
rdf_unweighted[k] += g;
rdf_mass[k] += mi * mj * g;
rdf_en[k] += eni * enj * g;
if has_charges {
rdf_charge[k] += qi * qj * g;
}
}
}
}
for k in 0..n_points {
let r = radii[k];
let shell = 4.0 * std::f64::consts::PI * r * r;
if shell > 1e-12 {
rdf_unweighted[k] /= shell;
rdf_mass[k] /= shell;
rdf_en[k] /= shell;
rdf_charge[k] /= shell;
}
}
RdfDescriptors {
radii,
rdf_unweighted,
rdf_mass,
rdf_electronegativity: rdf_en,
rdf_charge,
beta,
n_pairs,
}
}
fn atomic_mass_rdf(z: u8) -> f64 {
match z {
1 => 1.008,
6 => 12.011,
7 => 14.007,
8 => 15.999,
9 => 18.998,
15 => 30.974,
16 => 32.065,
17 => 35.453,
35 => 79.904,
53 => 126.904,
_ => z as f64 * 1.5,
}
}
fn electronegativity_rdf(z: u8) -> f64 {
match z {
1 => 2.20,
6 => 2.55,
7 => 3.04,
8 => 3.44,
9 => 3.98,
14 => 1.90,
15 => 2.19,
16 => 2.58,
17 => 3.16,
35 => 2.96,
53 => 2.66,
_ => 2.00,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rdf_water() {
let elements = vec![8, 1, 1];
let positions = vec![
[0.0, 0.0, 0.117],
[0.0, 0.757, -0.469],
[0.0, -0.757, -0.469],
];
let rdf = compute_rdf(&elements, &positions, &[], 100.0, 12.0, 128);
assert_eq!(rdf.n_pairs, 3);
assert_eq!(rdf.radii.len(), 128);
let peak_idx = rdf
.rdf_unweighted
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(i, _)| i)
.unwrap();
assert!(rdf.radii[peak_idx] < 3.0); }
#[test]
fn test_rdf_with_charges() {
let elements = vec![8, 1, 1];
let positions = vec![
[0.0, 0.0, 0.117],
[0.0, 0.757, -0.469],
[0.0, -0.757, -0.469],
];
let charges = vec![-0.8, 0.4, 0.4];
let rdf = compute_rdf(&elements, &positions, &charges, 100.0, 12.0, 128);
let has_negative = rdf.rdf_charge.iter().any(|&v| v < 0.0);
assert!(has_negative);
}
}