use super::types::{NmrShieldingResult, ScfInput, ShieldingTensor};
use crate::nmr::NmrNucleus;
fn reference_shielding(nucleus: NmrNucleus) -> f64 {
nucleus.reference_shielding()
}
fn free_atom_diamagnetic(nucleus: NmrNucleus) -> f64 {
nucleus.free_atom_diamagnetic()
}
pub fn compute_nmr_shieldings(
atomic_numbers: &[u8],
positions_bohr: &[[f64; 3]],
scf: &ScfInput,
basis_to_atom: &[usize],
) -> Vec<ShieldingTensor> {
let n_atoms = atomic_numbers.len();
let compute_one = |atom_n: usize| -> ShieldingTensor {
let z_n = atomic_numbers[atom_n];
let nucleus = NmrNucleus::default_for_element(z_n)
.unwrap_or_else(|| NmrNucleus::default_for_element(1).unwrap());
let sigma_dia = compute_diamagnetic_shielding(atom_n, positions_bohr, atomic_numbers);
let sigma_para = compute_paramagnetic_shielding(atom_n, scf, basis_to_atom);
let sigma_total = sigma_dia + sigma_para;
let delta = reference_shielding(nucleus) - sigma_total;
let tensor = [
[sigma_total, 0.0, 0.0],
[0.0, sigma_total, 0.0],
[0.0, 0.0, sigma_total],
];
ShieldingTensor {
atom_index: atom_n,
element: z_n,
tensor,
isotropic: sigma_total,
anisotropy: 0.0,
chemical_shift: delta,
}
};
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
(0..n_atoms).into_par_iter().map(compute_one).collect()
}
#[cfg(not(feature = "parallel"))]
{
(0..n_atoms).map(compute_one).collect()
}
}
pub fn compute_nmr_shieldings_for_nucleus(
nucleus: NmrNucleus,
atomic_numbers: &[u8],
positions_bohr: &[[f64; 3]],
scf: &ScfInput,
basis_to_atom: &[usize],
) -> Vec<ShieldingTensor> {
let target_atomic_number = nucleus.atomic_number();
let mut shieldings = Vec::new();
for atom_n in 0..atomic_numbers.len() {
if atomic_numbers[atom_n] != target_atomic_number {
continue;
}
let sigma_dia = compute_diamagnetic_shielding(atom_n, positions_bohr, atomic_numbers);
let sigma_para = compute_paramagnetic_shielding(atom_n, scf, basis_to_atom);
let sigma_total = sigma_dia + sigma_para;
shieldings.push(ShieldingTensor {
atom_index: atom_n,
element: target_atomic_number,
tensor: [
[sigma_total, 0.0, 0.0],
[0.0, sigma_total, 0.0],
[0.0, 0.0, sigma_total],
],
isotropic: sigma_total,
anisotropy: 0.0,
chemical_shift: reference_shielding(nucleus) - sigma_total,
});
}
shieldings
}
pub fn shieldings_to_shifts(
shieldings: &[ShieldingTensor],
atomic_numbers: &[u8],
) -> NmrShieldingResult {
NmrShieldingResult {
chemical_shifts: shieldings.iter().map(|s| s.chemical_shift).collect(),
elements: atomic_numbers.to_vec(),
n_atoms: atomic_numbers.len(),
}
}
fn compute_diamagnetic_shielding(
atom_n: usize,
positions: &[[f64; 3]],
atomic_numbers: &[u8],
) -> f64 {
let z_n = atomic_numbers[atom_n];
let r_n = positions[atom_n];
let nucleus = NmrNucleus::default_for_element(z_n)
.unwrap_or_else(|| NmrNucleus::default_for_element(1).unwrap());
let sigma_atom = free_atom_diamagnetic(nucleus);
let mut neighbor_correction = 0.0;
for (a, &z_a) in atomic_numbers.iter().enumerate() {
if a == atom_n {
continue;
}
let dx = positions[a][0] - r_n[0];
let dy = positions[a][1] - r_n[1];
let dz = positions[a][2] - r_n[2];
let r = (dx * dx + dy * dy + dz * dz).sqrt();
if r > 1e-10 {
neighbor_correction += (z_a as f64) / (r * r) * 0.1;
}
}
sigma_atom - neighbor_correction
}
fn compute_paramagnetic_shielding(atom_n: usize, scf: &ScfInput, basis_to_atom: &[usize]) -> f64 {
let n_basis = scf.n_basis;
let n_occ = scf.n_electrons / 2;
let mut sigma_para = 0.0;
for i in 0..n_occ {
for a in n_occ..n_basis {
let de = scf.orbital_energies[a] - scf.orbital_energies[i];
if de.abs() < 1e-10 {
continue;
}
let mut coupling = 0.0;
for mu in 0..n_basis {
if basis_to_atom[mu] != atom_n {
continue;
}
for nu in 0..n_basis {
coupling += scf.mo_coefficients[(mu, i)]
* scf.overlap_matrix[(mu, nu)]
* scf.mo_coefficients[(nu, a)];
}
}
sigma_para -= coupling * coupling / de;
}
}
sigma_para * 100.0
}
#[cfg(test)]
mod tests {
use super::*;
use nalgebra::DMatrix;
#[test]
fn test_reference_shieldings() {
assert!(reference_shielding(NmrNucleus::H1) > 0.0);
assert!(reference_shielding(NmrNucleus::C13) > reference_shielding(NmrNucleus::H1));
}
#[test]
fn test_free_atom_diamagnetic_trend() {
let sigma_h = free_atom_diamagnetic(NmrNucleus::H1);
let sigma_c = free_atom_diamagnetic(NmrNucleus::C13);
let sigma_o = free_atom_diamagnetic(NmrNucleus::O17);
assert!(sigma_h < sigma_c);
assert!(sigma_c < sigma_o);
}
#[test]
fn test_shielding_tensor_structure() {
let st = ShieldingTensor {
atom_index: 0,
element: 1,
tensor: [[31.0, 0.0, 0.0], [0.0, 31.0, 0.0], [0.0, 0.0, 31.0]],
isotropic: 31.0,
anisotropy: 0.0,
chemical_shift: 0.7,
};
assert_eq!(st.element, 1);
assert!(st.isotropic > 0.0);
}
#[test]
fn test_compute_shieldings_h2() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [0.0, 0.0, 1.4]];
let n_basis = 2;
let scf = ScfInput {
orbital_energies: vec![-0.6, 0.7],
mo_coefficients: DMatrix::identity(n_basis, n_basis),
density_matrix: DMatrix::zeros(n_basis, n_basis),
overlap_matrix: DMatrix::identity(n_basis, n_basis),
n_basis,
n_electrons: 2,
};
let basis_to_atom = [0, 1];
let shieldings = compute_nmr_shieldings(&elements, &positions, &scf, &basis_to_atom);
assert_eq!(shieldings.len(), 2);
for s in &shieldings {
assert!(s.isotropic.is_finite());
assert!(s.chemical_shift.is_finite());
}
}
#[test]
fn test_shieldings_to_shifts() {
let shieldings = vec![
ShieldingTensor {
atom_index: 0,
element: 1,
tensor: [[17.0; 3]; 3],
isotropic: 17.0,
anisotropy: 0.0,
chemical_shift: 14.7,
},
ShieldingTensor {
atom_index: 1,
element: 6,
tensor: [[200.0; 3]; 3],
isotropic: 200.0,
anisotropy: 0.0,
chemical_shift: -12.0,
},
];
let elements = [1, 6];
let result = shieldings_to_shifts(&shieldings, &elements);
assert_eq!(result.n_atoms, 2);
assert_eq!(result.chemical_shifts.len(), 2);
}
#[test]
fn test_compute_shieldings_for_specific_nucleus() {
let elements = [17u8, 17u8];
let positions = [[0.0, 0.0, 0.0], [0.0, 0.0, 4.0]];
let n_basis = 2;
let scf = ScfInput {
orbital_energies: vec![-0.6, 0.7],
mo_coefficients: DMatrix::identity(n_basis, n_basis),
density_matrix: DMatrix::zeros(n_basis, n_basis),
overlap_matrix: DMatrix::identity(n_basis, n_basis),
n_basis,
n_electrons: 2,
};
let basis_to_atom = [0, 1];
let shieldings = compute_nmr_shieldings_for_nucleus(
NmrNucleus::Cl35,
&elements,
&positions,
&scf,
&basis_to_atom,
);
assert_eq!(shieldings.len(), 2);
assert!(shieldings
.iter()
.all(|entry| entry.chemical_shift.is_finite()));
}
}