use std::collections::HashMap;
use std::sync::LazyLock;
static VDW_RADII: LazyLock<HashMap<&'static str, f64>> = LazyLock::new(|| {
let mut m = HashMap::new();
m.insert("H", 1.20);
m.insert("C", 1.70);
m.insert("N", 1.55);
m.insert("O", 1.52);
m.insert("F", 1.47);
m.insert("SI", 2.10);
m.insert("P", 1.80);
m.insert("S", 1.80);
m.insert("CL", 1.75);
m.insert("AS", 1.85);
m.insert("SE", 1.90);
m.insert("BR", 1.85);
m.insert("I", 1.98);
m.insert("MG", 1.73);
m.insert("CA", 2.31);
m.insert("FE", 2.05);
m.insert("ZN", 1.39);
m.insert("CU", 1.40);
m.insert("MN", 2.05);
m.insert("CO", 2.00);
m.insert("NI", 1.63);
m.insert("NA", 2.27);
m.insert("K", 2.75);
m.insert("HE", 1.40);
m.insert("NE", 1.54);
m.insert("AR", 1.88);
m.insert("KR", 2.02);
m.insert("XE", 2.16);
m.insert("B", 1.92);
m.insert("AL", 1.84);
m.insert("GA", 1.87);
m.insert("GE", 2.11);
m.insert("SN", 2.17);
m.insert("PB", 2.02);
m.insert("BI", 2.07);
m.insert("SB", 2.06);
m.insert("TE", 2.06);
m
});
static COVALENT_RADII: LazyLock<HashMap<&'static str, f64>> = LazyLock::new(|| {
let mut m = HashMap::new();
m.insert("H", 0.31);
m.insert("C", 0.76);
m.insert("N", 0.71);
m.insert("O", 0.66);
m.insert("F", 0.57);
m.insert("SI", 1.11);
m.insert("P", 1.07);
m.insert("S", 1.05);
m.insert("CL", 1.02);
m.insert("AS", 1.19);
m.insert("SE", 1.20);
m.insert("BR", 1.20);
m.insert("I", 1.39);
m.insert("MG", 1.41);
m.insert("CA", 1.76);
m.insert("FE", 1.52);
m.insert("ZN", 1.22);
m.insert("CU", 1.32);
m.insert("MN", 1.61);
m.insert("CO", 1.50);
m.insert("NI", 1.24);
m.insert("NA", 1.66);
m.insert("K", 2.03);
m.insert("B", 0.84);
m.insert("AL", 1.21);
m
});
pub const DEFAULT_VDW_RADIUS: f64 = 1.70;
pub const DEFAULT_COVALENT_RADIUS: f64 = 1.00;
pub fn vdw_radius(element: &str) -> f64 {
let elem_upper = element.trim().to_uppercase();
*VDW_RADII
.get(elem_upper.as_str())
.unwrap_or(&DEFAULT_VDW_RADIUS)
}
pub fn covalent_radius(element: &str) -> f64 {
let elem_upper = element.trim().to_uppercase();
*COVALENT_RADII
.get(elem_upper.as_str())
.unwrap_or(&DEFAULT_COVALENT_RADIUS)
}
pub fn is_metal(element: &str) -> bool {
let elem_upper = element.trim().to_uppercase();
matches!(
elem_upper.as_str(),
"MG" | "CA"
| "FE"
| "ZN"
| "CU"
| "MN"
| "CO"
| "NI"
| "NA"
| "K"
| "AL"
| "GA"
| "SN"
| "PB"
)
}
pub fn min_contact_distance(element1: &str, element2: &str, scale: f64) -> f64 {
(vdw_radius(element1) + vdw_radius(element2)) * scale
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vdw_radius_common_elements() {
assert!((vdw_radius("H") - 1.20).abs() < 1e-10);
assert!((vdw_radius("C") - 1.70).abs() < 1e-10);
assert!((vdw_radius("N") - 1.55).abs() < 1e-10);
assert!((vdw_radius("O") - 1.52).abs() < 1e-10);
assert!((vdw_radius("S") - 1.80).abs() < 1e-10);
}
#[test]
fn test_vdw_radius_case_insensitive() {
assert_eq!(vdw_radius("C"), vdw_radius("c"));
assert_eq!(vdw_radius("CL"), vdw_radius("cl"));
assert_eq!(vdw_radius("CL"), vdw_radius("Cl"));
}
#[test]
fn test_vdw_radius_unknown_element() {
assert_eq!(vdw_radius("XX"), DEFAULT_VDW_RADIUS);
assert_eq!(vdw_radius(""), DEFAULT_VDW_RADIUS);
}
#[test]
fn test_covalent_radius_common_elements() {
assert!((covalent_radius("C") - 0.76).abs() < 1e-10);
assert!((covalent_radius("N") - 0.71).abs() < 1e-10);
assert!((covalent_radius("O") - 0.66).abs() < 1e-10);
assert!((covalent_radius("FE") - 1.52).abs() < 1e-10);
}
#[test]
fn test_covalent_radius_unknown_element() {
assert_eq!(covalent_radius("XX"), DEFAULT_COVALENT_RADIUS);
}
#[test]
fn test_is_metal() {
assert!(is_metal("FE"));
assert!(is_metal("ZN"));
assert!(is_metal("MG"));
assert!(is_metal("fe")); assert!(!is_metal("C"));
assert!(!is_metal("N"));
assert!(!is_metal("O"));
}
#[test]
fn test_min_contact_distance() {
let dist = min_contact_distance("C", "C", 0.75);
assert!((dist - 2.55).abs() < 1e-10);
let dist = min_contact_distance("C", "N", 0.75);
assert!((dist - 2.4375).abs() < 1e-10);
}
#[test]
fn test_metals_have_radii() {
let metals = ["MG", "CA", "FE", "ZN", "CU", "MN", "CO", "NI", "NA", "K"];
for metal in metals {
let r = vdw_radius(metal);
assert!(
r > 1.0 && r != DEFAULT_VDW_RADIUS,
"Metal {} should have a defined radius, got {}",
metal,
r
);
}
}
#[test]
fn test_halogens_have_radii() {
let halogens = ["F", "CL", "BR", "I"];
for hal in halogens {
let r = vdw_radius(hal);
assert!(
r > 1.0 && r != DEFAULT_VDW_RADIUS,
"Halogen {} should have a defined radius, got {}",
hal,
r
);
}
}
}