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::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    for ga in &a.gaussians {
39        for gb in &b.gaussians {
40            let alpha = ga.alpha;
41            let beta = gb.alpha;
42            let gamma = alpha + beta;
43
44            // Gaussian product center
45            let px = (alpha * a.center[0] + beta * b.center[0]) / gamma;
46            let py = (alpha * a.center[1] + beta * b.center[1]) / gamma;
47            let pz = (alpha * a.center[2] + beta * b.center[2]) / gamma;
48
49            // Pre-exponential factor from Gaussian product theorem
50            let k_ab = (-alpha * beta / gamma * r2_ab).exp();
51
52            let contrib = match (a.l, a.m, b.l, b.m) {
53                // (s|s) overlap
54                (0, 0, 0, 0) => {
55                    let norm_a = (2.0 * alpha / PI).powf(0.75);
56                    let norm_b = (2.0 * beta / PI).powf(0.75);
57                    let s_prim = (PI / gamma).powf(1.5) * k_ab;
58                    norm_a * norm_b * s_prim
59                }
60                // (s|p) overlap
61                (0, 0, 1, m) | (1, m, 0, 0) => {
62                    let (s_orb, p_orb, s_alpha, p_alpha) = if a.l == 0 {
63                        (a, b, alpha, beta)
64                    } else {
65                        (b, a, beta, alpha)
66                    };
67                    let s_a = s_alpha;
68                    let p_a = p_alpha;
69                    let g = s_a + p_a;
70
71                    let norm_s = (2.0 * s_a / PI).powf(0.75);
72                    let norm_p = (128.0 * p_a.powi(5) / (PI * PI * PI)).powf(0.25);
73
74                    // Product center
75                    let pc = [
76                        (s_a * s_orb.center[0] + p_a * p_orb.center[0]) / g,
77                        (s_a * s_orb.center[1] + p_a * p_orb.center[1]) / g,
78                        (s_a * s_orb.center[2] + p_a * p_orb.center[2]) / g,
79                    ];
80
81                    // Component index from m: -1→x, 0→y, 1→z
82                    let comp_idx = match m {
83                        -1 => 0usize,
84                        0 => 1,
85                        1 => 2,
86                        _ => 0,
87                    };
88
89                    // (P - B_p) where B_p is the p-orbital center
90                    let pb = pc[comp_idx] - p_orb.center[comp_idx];
91
92                    let base = (PI / g).powf(1.5) * k_ab;
93                    norm_s * norm_p * pb * base
94                }
95                // (p|p) overlap
96                (1, m_a, 1, m_b) => {
97                    let norm_a = (128.0 * alpha.powi(5) / (PI * PI * PI)).powf(0.25);
98                    let norm_b = (128.0 * beta.powi(5) / (PI * PI * PI)).powf(0.25);
99
100                    let comp_a = match m_a {
101                        -1 => 0usize,
102                        0 => 1,
103                        1 => 2,
104                        _ => 0,
105                    };
106                    let comp_b = match m_b {
107                        -1 => 0usize,
108                        0 => 1,
109                        1 => 2,
110                        _ => 0,
111                    };
112
113                    // PA_i and PB_j
114                    let pa_i = px - a.center[comp_a];
115                    let pb_j;
116                    // For p-p: if same direction use shift product + 1/(2γ),
117                    // else just shift product
118                    if comp_a == comp_b {
119                        let shift_a = match comp_a {
120                            0 => px - a.center[0],
121                            1 => py - a.center[1],
122                            2 => pz - a.center[2],
123                            _ => 0.0,
124                        };
125                        let shift_b = match comp_b {
126                            0 => px - b.center[0],
127                            1 => py - b.center[1],
128                            2 => pz - b.center[2],
129                            _ => 0.0,
130                        };
131                        let base = (PI / gamma).powf(1.5) * k_ab;
132                        let val = shift_a * shift_b + 0.5 / gamma;
133                        norm_a * norm_b * val * base
134                    } else {
135                        pb_j = match comp_b {
136                            0 => px - b.center[0],
137                            1 => py - b.center[1],
138                            2 => pz - b.center[2],
139                            _ => 0.0,
140                        };
141                        let base = (PI / gamma).powf(1.5) * k_ab;
142                        norm_a * norm_b * pa_i * pb_j * base
143                    }
144                }
145                _ => 0.0,
146            };
147
148            s += ga.coeff * gb.coeff * contrib;
149        }
150    }
151
152    s
153}
154
155#[cfg(test)]
156mod tests {
157    use super::super::basis::build_basis;
158    use super::*;
159
160    #[test]
161    fn test_overlap_diagonal_is_one() {
162        let elements = [8u8, 1, 1];
163        let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
164        let basis = build_basis(&elements, &positions);
165        let s = build_overlap_matrix(&basis);
166        for i in 0..s.nrows() {
167            assert!(
168                (s[(i, i)] - 1.0).abs() < 1e-10,
169                "S[{},{}] = {}",
170                i,
171                i,
172                s[(i, i)]
173            );
174        }
175    }
176
177    #[test]
178    fn test_overlap_symmetry() {
179        let elements = [8u8, 1, 1];
180        let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
181        let basis = build_basis(&elements, &positions);
182        let s = build_overlap_matrix(&basis);
183        for i in 0..s.nrows() {
184            for j in 0..s.ncols() {
185                assert!(
186                    (s[(i, j)] - s[(j, i)]).abs() < 1e-14,
187                    "S not symmetric at ({},{})",
188                    i,
189                    j,
190                );
191            }
192        }
193    }
194
195    #[test]
196    fn test_overlap_off_diagonal_range() {
197        let elements = [8u8, 1, 1];
198        let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
199        let basis = build_basis(&elements, &positions);
200        let s = build_overlap_matrix(&basis);
201        // All off-diagonal elements should have |S_ij| < 1
202        for i in 0..s.nrows() {
203            for j in 0..s.ncols() {
204                if i != j {
205                    assert!(
206                        s[(i, j)].abs() <= 1.0 + 1e-10,
207                        "S[{},{}] = {} exceeds 1.0",
208                        i,
209                        j,
210                        s[(i, j)]
211                    );
212                }
213            }
214        }
215    }
216
217    #[test]
218    fn test_overlap_h2_ss() {
219        // Two H atoms at 0.74 Å: their 1s-1s overlap should be ~0.6-0.8
220        let elements = [1u8, 1];
221        let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
222        let basis = build_basis(&elements, &positions);
223        let s = build_overlap_matrix(&basis);
224        assert_eq!(s.nrows(), 2);
225        let s12 = s[(0, 1)];
226        assert!(s12 > 0.3 && s12 < 0.9, "S_12 for H2 = {}", s12);
227    }
228}