Skip to main content

sci_form/hf/
nuclear.rs

1//! Nuclear attraction integral evaluator.
2//!
3//! Computes the matrix elements $V_{\mu\nu} = -\sum_A Z_A (\mu|\frac{1}{r_A}|\nu)$
4//! where the sum runs over all nuclei A with charge $Z_A$.
5//! Uses the Boys function for the incomplete gamma integral.
6
7use super::basis::{BasisSet, ShellType};
8use nalgebra::DMatrix;
9
10/// Compute the nuclear attraction matrix V.
11pub fn compute_nuclear_matrix(
12    basis: &BasisSet,
13    elements: &[u8],
14    positions_bohr: &[[f64; 3]],
15) -> DMatrix<f64> {
16    let n = basis.n_basis();
17    let mut v_mat = DMatrix::zeros(n, n);
18    let mut mu = 0;
19
20    for shell_i in &basis.shells {
21        let ni = shell_i.n_functions();
22        let mut nu = 0;
23
24        for shell_j in &basis.shells {
25            let nj = shell_j.n_functions();
26            let block = nuclear_shell_pair(shell_i, shell_j, elements, positions_bohr);
27
28            for i in 0..ni {
29                for j in 0..nj {
30                    v_mat[(mu + i, nu + j)] = block[i * nj + j];
31                }
32            }
33            nu += nj;
34        }
35        mu += ni;
36    }
37    v_mat
38}
39
40fn nuclear_shell_pair(
41    a: &super::basis::Shell,
42    b: &super::basis::Shell,
43    elements: &[u8],
44    atoms_bohr: &[[f64; 3]],
45) -> Vec<f64> {
46    let na = a.n_functions();
47    let nb = b.n_functions();
48    let mut result = vec![0.0; na * nb];
49    let ab2 = dist_sq(&a.center, &b.center);
50
51    for (&ea, &ca) in a.exponents.iter().zip(&a.coefficients) {
52        for (&eb, &cb) in b.exponents.iter().zip(&b.coefficients) {
53            let gamma = ea + eb;
54            let p = [
55                (ea * a.center[0] + eb * b.center[0]) / gamma,
56                (ea * a.center[1] + eb * b.center[1]) / gamma,
57                (ea * a.center[2] + eb * b.center[2]) / gamma,
58            ];
59
60            let exp_ab = (-ea * eb / gamma * ab2).exp();
61
62            for (atom_idx, &z) in elements.iter().enumerate() {
63                let c = &atoms_bohr[atom_idx];
64                let pc2 = dist_sq(&p, c);
65                let t = gamma * pc2;
66                let f0 = boys_function(0, t);
67
68                let prefactor = -2.0 * std::f64::consts::PI / gamma * exp_ab * z as f64 * ca * cb;
69
70                match (a.shell_type, b.shell_type) {
71                    (ShellType::S, ShellType::S) => {
72                        result[0] += prefactor * f0;
73                    }
74                    (ShellType::S, ShellType::P) => {
75                        let f1 = boys_function(1, t);
76                        for d in 0..3 {
77                            let pb = p[d] - b.center[d];
78                            let pc = p[d] - c[d];
79                            result[d] += prefactor * (pb * f0 - pc * f1);
80                        }
81                    }
82                    (ShellType::P, ShellType::S) => {
83                        let f1 = boys_function(1, t);
84                        for d in 0..3 {
85                            let pa = p[d] - a.center[d];
86                            let pc = p[d] - c[d];
87                            result[d * nb] += prefactor * (pa * f0 - pc * f1);
88                        }
89                    }
90                    (ShellType::P, ShellType::P) => {
91                        let f1 = boys_function(1, t);
92                        let f2 = boys_function(2, t);
93                        for i in 0..3 {
94                            for j in 0..3 {
95                                let pa = p[i] - a.center[i];
96                                let pb = p[j] - b.center[j];
97                                let pc_i = p[i] - c[i];
98                                let pc_j = p[j] - c[j];
99                                let mut val = pa * pb * f0 - pa * pc_j * f1 - pb * pc_i * f1
100                                    + pc_i * pc_j * f2;
101                                if i == j {
102                                    val += (f0 - f1) / (2.0 * gamma);
103                                }
104                                result[i * nb + j] += prefactor * val;
105                            }
106                        }
107                    }
108                }
109            }
110        }
111    }
112    result
113}
114
115/// Boys function $F_n(t) = \int_0^1 u^{2n} e^{-tu^2} du$.
116///
117/// Uses series expansion for small t and asymptotic form for large t.
118pub fn boys_function(n: usize, t: f64) -> f64 {
119    if t < 1e-15 {
120        return 1.0 / (2.0 * n as f64 + 1.0);
121    }
122    if t > 30.0 {
123        // Asymptotic: F_n(t) ≈ (2n-1)!! / 2^(n+1) * sqrt(π/t^(2n+1))
124        let _nf = n as f64;
125        return double_factorial(2 * n) as f64 / 2.0f64.powi(n as i32 + 1)
126            * (std::f64::consts::PI / t.powi(2 * n as i32 + 1)).sqrt();
127    }
128
129    // Series expansion: F_n(t) = e^{-t} Σ_{k=0}^∞ t^k / (2n+2k+1)!!
130    let mut sum = 0.0;
131    let mut term = 1.0 / (2 * n + 1) as f64;
132    sum += term;
133    for k in 1..100 {
134        term *= t / (n as f64 + k as f64 + 0.5);
135        sum += term;
136        if term.abs() < 1e-15 * sum.abs() {
137            break;
138        }
139    }
140    sum * (-t).exp()
141}
142
143fn double_factorial(n: usize) -> u64 {
144    if n <= 1 {
145        return 1;
146    }
147    let mut result = 1u64;
148    let mut k = n;
149    while k > 1 {
150        result *= k as u64;
151        k -= 2;
152    }
153    result
154}
155
156#[inline]
157fn dist_sq(a: &[f64; 3], b: &[f64; 3]) -> f64 {
158    let dx = a[0] - b[0];
159    let dy = a[1] - b[1];
160    let dz = a[2] - b[2];
161    dx * dx + dy * dy + dz * dz
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_boys_f0_zero() {
170        let f = boys_function(0, 0.0);
171        assert!((f - 1.0).abs() < 1e-10, "F_0(0) should be 1.0, got {f}");
172    }
173
174    #[test]
175    fn test_boys_f0_large() {
176        let f = boys_function(0, 50.0);
177        let expected = (std::f64::consts::PI / 50.0).sqrt() * 0.5;
178        assert!(
179            (f - expected).abs() < 0.01 * expected,
180            "F_0(50) = {f}, expected ≈ {expected}"
181        );
182    }
183}