Skip to main content

sci_form/scf/
gaussian_integrals.rs

1//! Primitive Gaussian integral evaluation using Obara-Saika recursion.
2//!
3//! Implements the Obara-Saika (1986) recurrence relation for computing
4//! overlap, kinetic, and nuclear attraction integrals between Cartesian
5//! Gaussian primitives:
6//!
7//!   g(α, A, l) = (x-Ax)^lx (y-Ay)^ly (z-Az)^lz exp(-α|r-A|²)
8//!
9//! # Algorithm
10//!
11//! The Gaussian product theorem states that the product of two Gaussians
12//! centered at A and B is a Gaussian centered at P:
13//!
14//!   P = (α·A + β·B) / (α + β)
15//!   μ = α·β / (α + β)
16//!
17//! The Obara-Saika recurrence builds higher angular momentum integrals
18//! from lower ones using:
19//!
20//!   S(a+1,b) = (P-A)·S(a,b) + (1/2p)·[a·S(a-1,b) + b·S(a,b-1)]
21//!
22//! where p = α + β and S(0,0) = sqrt(π/p) · exp(-μ·|AB|²)
23
24use std::f64::consts::PI;
25
26/// Gaussian product center P = (α·A + β·B) / (α + β).
27#[inline]
28pub fn gaussian_product_center(alpha: f64, a: f64, beta: f64, b: f64) -> f64 {
29    (alpha * a + beta * b) / (alpha + beta)
30}
31
32/// Distance squared between two 3D points.
33#[inline]
34pub fn distance_squared(a: &[f64; 3], b: &[f64; 3]) -> f64 {
35    let dx = a[0] - b[0];
36    let dy = a[1] - b[1];
37    let dz = a[2] - b[2];
38    dx * dx + dy * dy + dz * dz
39}
40
41/// Overlap integral between two s-type (l=0) Gaussian primitives.
42///
43/// S_00 = (π / p)^{3/2} · exp(-μ · |AB|²)
44pub fn overlap_ss(alpha: f64, center_a: &[f64; 3], beta: f64, center_b: &[f64; 3]) -> f64 {
45    let p = alpha + beta;
46    let mu = alpha * beta / p;
47    let ab2 = distance_squared(center_a, center_b);
48    (PI / p).powf(1.5) * (-mu * ab2).exp()
49}
50
51/// 1D overlap integral using Obara-Saika recursion.
52///
53/// Returns S(la, lb)_x = ∫ (x-Ax)^la (x-Bx)^lb exp(-α(x-Ax)² - β(x-Bx)²) dx
54pub fn overlap_1d(la: u32, lb: u32, pa_x: f64, pb_x: f64, p: f64, prefactor: f64) -> f64 {
55    let max_a = (la + 1) as usize;
56    let max_b = (lb + 1) as usize;
57    let mut table = vec![vec![0.0f64; max_b]; max_a];
58
59    table[0][0] = prefactor;
60
61    if max_a > 1 {
62        table[1][0] = pa_x * table[0][0];
63    }
64    for a in 2..max_a {
65        table[a][0] = pa_x * table[a - 1][0] + (a - 1) as f64 / (2.0 * p) * table[a - 2][0];
66    }
67
68    for b in 1..max_b {
69        for a in 0..max_a {
70            table[a][b] = pb_x * table[a][b - 1];
71            if b > 1 {
72                table[a][b] += (b - 1) as f64 / (2.0 * p) * table[a][b - 2];
73            }
74            if a > 0 {
75                table[a][b] += a as f64 / (2.0 * p) * table[a - 1][b - 1];
76            }
77        }
78    }
79
80    table[la as usize][lb as usize]
81}
82
83/// Full 3D overlap integral between two Cartesian Gaussian primitives.
84pub fn overlap_primitive(
85    alpha: f64,
86    center_a: &[f64; 3],
87    la: [u32; 3],
88    beta: f64,
89    center_b: &[f64; 3],
90    lb: [u32; 3],
91) -> f64 {
92    let p = alpha + beta;
93    let mu = alpha * beta / p;
94    let ab2 = distance_squared(center_a, center_b);
95
96    let prefactor_3d = (PI / p).powf(1.5) * (-mu * ab2).exp();
97
98    let px = gaussian_product_center(alpha, center_a[0], beta, center_b[0]);
99    let py = gaussian_product_center(alpha, center_a[1], beta, center_b[1]);
100    let pz = gaussian_product_center(alpha, center_a[2], beta, center_b[2]);
101
102    let pa = [px - center_a[0], py - center_a[1], pz - center_a[2]];
103    let pb = [px - center_b[0], py - center_b[1], pz - center_b[2]];
104
105    let prefactor_1d = prefactor_3d.cbrt();
106
107    let sx = overlap_1d(la[0], lb[0], pa[0], pb[0], p, prefactor_1d);
108    let sy = overlap_1d(la[1], lb[1], pa[1], pb[1], p, prefactor_1d);
109    let sz = overlap_1d(la[2], lb[2], pa[2], pb[2], p, prefactor_1d);
110
111    sx * sy * sz
112}
113
114/// Direct 1D overlap (standalone, includes all prefactors).
115pub fn overlap_1d_direct(la: u32, lb: u32, alpha: f64, ax: f64, beta: f64, bx: f64) -> f64 {
116    let p = alpha + beta;
117    let mu = alpha * beta / p;
118    let ab = ax - bx;
119    let px = gaussian_product_center(alpha, ax, beta, bx);
120    let pa = px - ax;
121    let pb = px - bx;
122
123    let prefactor = (PI / p).sqrt() * (-mu * ab * ab).exp();
124    overlap_1d(la, lb, pa, pb, p, prefactor)
125}
126
127/// Boys function F_n(x) for nuclear attraction integrals.
128///
129/// F_n(x) = ∫₀¹ t^{2n} exp(-x·t²) dt
130pub fn boys_function(n: u32, x: f64) -> f64 {
131    if x < 1.0e-10 {
132        return 1.0 / (2.0 * n as f64 + 1.0);
133    }
134
135    if x > 30.0 + n as f64 {
136        // Asymptotic: F_n(x) ≈ (2n-1)!! / 2^(n+1) · √π / x^(n+1/2)
137        let df = if n == 0 {
138            1.0
139        } else {
140            double_factorial_f64(2 * n - 1)
141        };
142        return df * PI.sqrt() / (2.0f64.powi(n as i32 + 1) * x.powi(n as i32) * x.sqrt());
143    }
144
145    let mut sum = 0.0;
146    let mut term = 1.0 / (2.0 * n as f64 + 1.0);
147    sum += term;
148
149    for k in 1..100 {
150        term *= 2.0 * x / (2.0 * (n + k) as f64 + 1.0);
151        sum += term;
152        if term.abs() < 1.0e-15 * sum.abs() {
153            break;
154        }
155    }
156
157    (-x).exp() * sum
158}
159
160/// Double factorial for f64 return: (2n-1)!! = (2n)! / (2^n · n!)
161fn double_factorial_f64(n: u32) -> f64 {
162    if n <= 1 {
163        return 1.0;
164    }
165    let mut result = 1.0;
166    let mut k = n;
167    while k > 1 {
168        result *= k as f64;
169        k -= 2;
170    }
171    result
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_overlap_ss_same_center() {
180        let s = overlap_ss(1.0, &[0.0, 0.0, 0.0], 1.0, &[0.0, 0.0, 0.0]);
181        let expected = (PI / 2.0).powf(1.5);
182        assert!((s - expected).abs() < 1e-10);
183    }
184
185    #[test]
186    fn test_overlap_ss_decays_with_distance() {
187        let a = [0.0, 0.0, 0.0];
188        let s1 = overlap_ss(1.0, &a, 1.0, &[0.0, 0.0, 0.0]);
189        let s2 = overlap_ss(1.0, &a, 1.0, &[1.0, 0.0, 0.0]);
190        let s3 = overlap_ss(1.0, &a, 1.0, &[2.0, 0.0, 0.0]);
191        assert!(s1 > s2);
192        assert!(s2 > s3);
193    }
194
195    #[test]
196    fn test_overlap_primitive_ss() {
197        let s = overlap_primitive(
198            1.0,
199            &[0.0, 0.0, 0.0],
200            [0, 0, 0],
201            1.0,
202            &[0.0, 0.0, 0.0],
203            [0, 0, 0],
204        );
205        let expected = (PI / 2.0).powf(1.5);
206        assert!((s - expected).abs() < 1e-10);
207    }
208
209    #[test]
210    fn test_boys_function_zero() {
211        let f = boys_function(0, 0.0);
212        assert!((f - 1.0).abs() < 1e-10);
213    }
214
215    #[test]
216    fn test_boys_function_large_x() {
217        let x = 50.0;
218        let f = boys_function(0, x);
219        let expected = 0.5 * (PI / x).sqrt();
220        assert!((f - expected).abs() / expected < 0.01);
221    }
222
223    #[test]
224    fn test_gaussian_product_center() {
225        let c = gaussian_product_center(1.0, 0.0, 1.0, 2.0);
226        assert!((c - 1.0).abs() < 1e-10);
227    }
228}