Skip to main content

sci_form/eht/
overlap.rs

1//! Overlap matrix S construction using STO-3G Gaussian integrals.
2//!
3//! S_ij = ∫ φ_i(r) φ_j(r) dr
4//! Computed analytically using the Gaussian product theorem.
5
6use nalgebra::DMatrix;
7use std::f64::consts::PI;
8
9use super::basis::{orbital_cartesian_terms, AtomicOrbital};
10
11/// Build the symmetric overlap matrix S for the given molecular basis.
12pub fn build_overlap_matrix(basis: &[AtomicOrbital]) -> DMatrix<f64> {
13    let n = basis.len();
14    let mut s = DMatrix::zeros(n, n);
15
16    for i in 0..n {
17        s[(i, i)] = 1.0; // S_ii = 1 by definition for normalized orbitals
18        for j in (i + 1)..n {
19            let sij = overlap_integral(&basis[i], &basis[j]);
20            s[(i, j)] = sij;
21            s[(j, i)] = sij;
22        }
23    }
24
25    s
26}
27
28/// Compute the overlap integral between two atomic orbitals using
29/// their STO-3G contracted Gaussian expansions.
30fn overlap_integral(a: &AtomicOrbital, b: &AtomicOrbital) -> f64 {
31    let dx = a.center[0] - b.center[0];
32    let dy = a.center[1] - b.center[1];
33    let dz = a.center[2] - b.center[2];
34    let r2_ab = dx * dx + dy * dy + dz * dz;
35
36    let mut s = 0.0;
37
38    let terms_a = orbital_cartesian_terms(a.l, a.m);
39    let terms_b = orbital_cartesian_terms(b.l, b.m);
40
41    for ga in &a.gaussians {
42        for gb in &b.gaussians {
43            let alpha = ga.alpha;
44            let beta = gb.alpha;
45            let gamma = alpha + beta;
46
47            let px = (alpha * a.center[0] + beta * b.center[0]) / gamma;
48            let py = (alpha * a.center[1] + beta * b.center[1]) / gamma;
49            let pz = (alpha * a.center[2] + beta * b.center[2]) / gamma;
50
51            let pa = [px - a.center[0], py - a.center[1], pz - a.center[2]];
52            let pb = [px - b.center[0], py - b.center[1], pz - b.center[2]];
53            let pre = (-alpha * beta / gamma * r2_ab).exp();
54
55            let mut prim_sum = 0.0;
56            for (coef_a, [lax, lay, laz]) in &terms_a {
57                for (coef_b, [lbx, lby, lbz]) in &terms_b {
58                    let ix = overlap_1d(*lax as usize, *lbx as usize, pa[0], pb[0], gamma);
59                    let iy = overlap_1d(*lay as usize, *lby as usize, pa[1], pb[1], gamma);
60                    let iz = overlap_1d(*laz as usize, *lbz as usize, pa[2], pb[2], gamma);
61
62                    let norm_a = cart_norm(alpha, *lax as i32, *lay as i32, *laz as i32);
63                    let norm_b = cart_norm(beta, *lbx as i32, *lby as i32, *lbz as i32);
64                    prim_sum += coef_a * coef_b * norm_a * norm_b * ix * iy * iz;
65                }
66            }
67
68            s += ga.coeff * gb.coeff * pre * prim_sum;
69        }
70    }
71
72    s
73}
74
75fn odd_double_factorial(n: i32) -> f64 {
76    if n <= 0 {
77        return 1.0;
78    }
79    let mut acc = 1.0;
80    let mut k = n;
81    while k > 0 {
82        acc *= k as f64;
83        k -= 2;
84    }
85    acc
86}
87
88fn cart_norm(alpha: f64, lx: i32, ly: i32, lz: i32) -> f64 {
89    let lsum = lx + ly + lz;
90    let pref = (2.0 * alpha / PI).powf(0.75);
91    let ang = (4.0 * alpha).powf(lsum as f64 / 2.0);
92    let denom = (odd_double_factorial(2 * lx - 1)
93        * odd_double_factorial(2 * ly - 1)
94        * odd_double_factorial(2 * lz - 1))
95    .sqrt();
96    pref * ang / denom
97}
98
99fn overlap_1d(la: usize, lb: usize, pa: f64, pb: f64, gamma: f64) -> f64 {
100    let mut e = vec![vec![0.0f64; lb + 1]; la + 1];
101    e[0][0] = (PI / gamma).sqrt();
102
103    for i in 1..=la {
104        let term1 = pa * e[i - 1][0];
105        let term2 = if i > 1 {
106            (i as f64 - 1.0) * e[i - 2][0] / (2.0 * gamma)
107        } else {
108            0.0
109        };
110        e[i][0] = term1 + term2;
111    }
112
113    for j in 1..=lb {
114        let term1 = pb * e[0][j - 1];
115        let term2 = if j > 1 {
116            (j as f64 - 1.0) * e[0][j - 2] / (2.0 * gamma)
117        } else {
118            0.0
119        };
120        e[0][j] = term1 + term2;
121    }
122
123    for i in 1..=la {
124        for j in 1..=lb {
125            let t1 = pb * e[i][j - 1];
126            let t2 = if j > 1 {
127                (j as f64 - 1.0) * e[i][j - 2] / (2.0 * gamma)
128            } else {
129                0.0
130            };
131            let t3 = (i as f64) * e[i - 1][j - 1] / (2.0 * gamma);
132            e[i][j] = t1 + t2 + t3;
133        }
134    }
135
136    e[la][lb]
137}
138
139#[cfg(test)]
140mod tests {
141    use super::super::basis::build_basis;
142    use super::*;
143
144    #[test]
145    fn test_overlap_diagonal_is_one() {
146        let elements = [8u8, 1, 1];
147        let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
148        let basis = build_basis(&elements, &positions);
149        let s = build_overlap_matrix(&basis);
150        for i in 0..s.nrows() {
151            assert!(
152                (s[(i, i)] - 1.0).abs() < 1e-10,
153                "S[{},{}] = {}",
154                i,
155                i,
156                s[(i, i)]
157            );
158        }
159    }
160
161    #[test]
162    fn test_overlap_symmetry() {
163        let elements = [8u8, 1, 1];
164        let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
165        let basis = build_basis(&elements, &positions);
166        let s = build_overlap_matrix(&basis);
167        for i in 0..s.nrows() {
168            for j in 0..s.ncols() {
169                assert!(
170                    (s[(i, j)] - s[(j, i)]).abs() < 1e-14,
171                    "S not symmetric at ({},{})",
172                    i,
173                    j,
174                );
175            }
176        }
177    }
178
179    #[test]
180    fn test_overlap_off_diagonal_range() {
181        let elements = [8u8, 1, 1];
182        let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
183        let basis = build_basis(&elements, &positions);
184        let s = build_overlap_matrix(&basis);
185        // All off-diagonal elements should have |S_ij| < 1
186        for i in 0..s.nrows() {
187            for j in 0..s.ncols() {
188                if i != j {
189                    assert!(
190                        s[(i, j)].abs() <= 1.0 + 1e-10,
191                        "S[{},{}] = {} exceeds 1.0",
192                        i,
193                        j,
194                        s[(i, j)]
195                    );
196                }
197            }
198        }
199    }
200
201    #[test]
202    fn test_overlap_h2_ss() {
203        // Two H atoms at 0.74 Å: their 1s-1s overlap should be ~0.6-0.8
204        let elements = [1u8, 1];
205        let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
206        let basis = build_basis(&elements, &positions);
207        let s = build_overlap_matrix(&basis);
208        assert_eq!(s.nrows(), 2);
209        let s12 = s[(0, 1)];
210        assert!(s12 > 0.3 && s12 < 0.9, "S_12 for H2 = {}", s12);
211    }
212
213    #[test]
214    fn test_metal_overlap_matrix_is_finite() {
215        let elements = [26u8, 17, 17];
216        let positions = [[0.0, 0.0, 0.0], [2.2, 0.0, 0.0], [-2.2, 0.0, 0.0]];
217        let basis = build_basis(&elements, &positions);
218        let s = build_overlap_matrix(&basis);
219
220        for i in 0..s.nrows() {
221            for j in 0..s.ncols() {
222                assert!(s[(i, j)].is_finite(), "S[{},{}] is not finite", i, j);
223            }
224        }
225    }
226}