Skip to main content

sci_form/eht/
basis.rs

1//! Atomic orbital basis representations: STO and contracted Gaussian (STO-nG).
2//!
3//! Provides Slater-type orbitals (STOs) and their expansion into Gaussian primitives
4//! for tractable overlap-integral evaluation.
5
6use serde::{Deserialize, Serialize};
7use std::f64::consts::PI;
8
9fn odd_double_factorial(n: i32) -> f64 {
10    if n <= 0 {
11        return 1.0;
12    }
13    let mut acc = 1.0;
14    let mut k = n;
15    while k > 0 {
16        acc *= k as f64;
17        k -= 2;
18    }
19    acc
20}
21
22/// A single Gaussian primitive: coefficient × exp(-alpha × r²).
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct GaussianPrimitive {
25    /// Contraction coefficient (pre-normalized).
26    pub coeff: f64,
27    /// Gaussian exponent (bohr⁻²).
28    pub alpha: f64,
29}
30
31/// A Slater-type orbital represented conceptually.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct SlaterOrbital {
34    /// Principal quantum number.
35    pub n: u8,
36    /// Angular momentum l.
37    pub l: u8,
38    /// Magnetic quantum number m.
39    pub m: i8,
40    /// Slater exponent ζ.
41    pub zeta: f64,
42}
43
44/// A single atomic orbital in the molecular basis, located on a specific atom.
45#[derive(Debug, Clone)]
46pub struct AtomicOrbital {
47    /// Index of the atom this orbital belongs to.
48    pub atom_index: usize,
49    /// Center position in 3D space (bohr).
50    pub center: [f64; 3],
51    /// Principal quantum number.
52    pub n: u8,
53    /// Angular momentum l.
54    pub l: u8,
55    /// Magnetic quantum number m.
56    pub m: i8,
57    /// VSIP value (eV) for diagonal H_ii.
58    pub vsip: f64,
59    /// Slater exponent.
60    pub zeta: f64,
61    /// Label, e.g. "C_2s", "O_2px".
62    pub label: String,
63    /// Contracted Gaussian primitives (STO-nG expansion).
64    pub gaussians: Vec<GaussianPrimitive>,
65}
66
67// ─── STO-3G contraction coefficients ─────────────────────────────────────────
68// From Hehre, Stewart, Pople, J. Chem. Phys. 51, 2657 (1969).
69// These are for the expansion of a Slater orbital with ζ=1.0.
70// When ζ ≠ 1.0, scale alpha by ζ².
71
72/// STO-3G coefficients for 1s orbital (n=1, l=0).
73static STO3G_1S: [(f64, f64); 3] = [
74    (0.154329, 2.227660),
75    (0.535328, 0.405771),
76    (0.444635, 0.109818),
77];
78
79/// STO-3G coefficients for 2s orbital (n=2, l=0).
80/// Tabulated for ζ=1.0, two-term linear combination (2s = outer - inner).
81static STO3G_2SP_INNER: [(f64, f64); 3] = [
82    (-0.099967, 2.581058),
83    (0.399513, 0.532013),
84    (0.700115, 0.168856),
85];
86
87/// STO-3G coefficients for 2p orbital (n=2, l=1).
88static STO3G_2SP_OUTER: [(f64, f64); 3] = [
89    (0.155916, 2.581058),
90    (0.607684, 0.532013),
91    (0.391957, 0.168856),
92];
93
94/// STO-3G coefficients for 3sp orbital (n=3, l=0 or 1).
95static STO3G_3SP_INNER: [(f64, f64); 3] = [
96    (-0.219620, 0.994203),
97    (0.225595, 0.231031),
98    (0.900398, 0.075142),
99];
100
101static STO3G_3SP_OUTER: [(f64, f64); 3] = [
102    (0.010588, 0.994203),
103    (0.595167, 0.231031),
104    (0.462001, 0.075142),
105];
106
107/// STO-3G for 4sp.
108static STO3G_4SP_INNER: [(f64, f64); 3] = [
109    (-0.310438, 0.494718),
110    (0.015515, 0.120193),
111    (1.023468, 0.040301),
112];
113
114static STO3G_4SP_OUTER: [(f64, f64); 3] = [
115    (-0.063311, 0.494718),
116    (0.574459, 0.120193),
117    (0.499768, 0.040301),
118];
119
120/// STO-3G for 5sp.
121static STO3G_5SP_INNER: [(f64, f64); 3] = [
122    (-0.383204, 0.289515),
123    (-0.159438, 0.073780),
124    (1.143456, 0.025335),
125];
126
127static STO3G_5SP_OUTER: [(f64, f64); 3] = [
128    (-0.094810, 0.289515),
129    (0.570520, 0.073780),
130    (0.497340, 0.025335),
131];
132
133/// STO-3G for nd shells (tabulated at zeta=1.0).
134///
135/// The 3d table is the canonical STO-3G contraction and we reuse it for
136/// higher nd shells as an initial EHT approximation.
137static STO3G_3D: [(f64, f64); 3] = [
138    (0.219767, 2.488296),
139    (0.655547, 0.798105),
140    (0.286573, 0.331966),
141];
142
143/// Cartesian angular terms for one AO in the real-harmonic basis.
144///
145/// Each entry is `(coefficient, [lx, ly, lz])` representing
146/// `coefficient * x^lx y^ly z^lz`.
147pub fn orbital_cartesian_terms(l: u8, m: i8) -> Vec<(f64, [u8; 3])> {
148    match (l, m) {
149        (0, _) => vec![(1.0, [0, 0, 0])],
150        (1, -1) => vec![(1.0, [1, 0, 0])], // p_x
151        (1, 0) => vec![(1.0, [0, 1, 0])],  // p_y
152        (1, 1) => vec![(1.0, [0, 0, 1])],  // p_z
153        (2, -2) => vec![(1.0, [1, 1, 0])], // d_xy
154        (2, -1) => vec![(1.0, [1, 0, 1])], // d_xz
155        (2, 0) => vec![(1.0, [0, 1, 1])],  // d_yz
156        // d_x2-y2 = (1/sqrt(2)) (d_xx - d_yy)
157        (2, 1) => vec![
158            (1.0 / 2.0f64.sqrt(), [2, 0, 0]),
159            (-1.0 / 2.0f64.sqrt(), [0, 2, 0]),
160        ],
161        // d_z2 = (1/sqrt(6)) (2 d_zz - d_xx - d_yy)
162        (2, 2) => vec![
163            (-1.0 / 6.0f64.sqrt(), [2, 0, 0]),
164            (-1.0 / 6.0f64.sqrt(), [0, 2, 0]),
165            (2.0 / 6.0f64.sqrt(), [0, 0, 2]),
166        ],
167        _ => vec![],
168    }
169}
170
171/// Normalization factor for a Cartesian Gaussian
172/// `x^lx y^ly z^lz exp(-alpha r^2)`.
173pub fn gaussian_cartesian_norm(alpha: f64, lx: u8, ly: u8, lz: u8) -> f64 {
174    let lsum = (lx + ly + lz) as i32;
175    let pref = (2.0 * alpha / PI).powf(0.75);
176    let ang = (4.0 * alpha).powf(lsum as f64 / 2.0);
177    let denom = (odd_double_factorial(2 * lx as i32 - 1)
178        * odd_double_factorial(2 * ly as i32 - 1)
179        * odd_double_factorial(2 * lz as i32 - 1))
180    .sqrt();
181    pref * ang / denom
182}
183
184/// Evaluate one normalized Cartesian Gaussian component
185/// `N x^lx y^ly z^lz exp(-alpha r^2)`.
186pub fn gaussian_cartesian_value(alpha: f64, x: f64, y: f64, z: f64, lx: u8, ly: u8, lz: u8) -> f64 {
187    let norm = gaussian_cartesian_norm(alpha, lx, ly, lz);
188    let poly = x.powi(lx as i32) * y.powi(ly as i32) * z.powi(lz as i32);
189    norm * poly * (-alpha * (x * x + y * y + z * z)).exp()
190}
191
192/// Build STO-3G Gaussian primitives for an orbital with given n, l, and ζ.
193pub fn sto3g_expansion(n: u8, l: u8, zeta: f64) -> Vec<GaussianPrimitive> {
194    let zeta_sq = zeta * zeta;
195
196    let table: &[(f64, f64); 3] = match (n, l) {
197        (1, 0) => &STO3G_1S,
198        (2, 0) => &STO3G_2SP_INNER,
199        (2, 1) => &STO3G_2SP_OUTER,
200        (3, 0) => &STO3G_3SP_INNER,
201        (3, 1) => &STO3G_3SP_OUTER,
202        (3, 2) => &STO3G_3D,
203        (4, 0) => &STO3G_4SP_INNER,
204        (4, 1) => &STO3G_4SP_OUTER,
205        (4, 2) => &STO3G_3D,
206        (5, 0) => &STO3G_5SP_INNER,
207        (5, 1) => &STO3G_5SP_OUTER,
208        (5, 2) => &STO3G_3D,
209        _ => return vec![],
210    };
211
212    // For 4d/5d orbitals, scale exponents by (3/n)² to account for the
213    // more diffuse nature of higher principal quantum number d shells.
214    let d_scale = if l == 2 && n > 3 {
215        let ratio = 3.0 / n as f64;
216        ratio * ratio
217    } else {
218        1.0
219    };
220
221    table
222        .iter()
223        .map(|&(coeff, alpha)| GaussianPrimitive {
224            coeff,
225            alpha: alpha * zeta_sq * d_scale,
226        })
227        .collect()
228}
229
230/// Evaluate a normalized s-type Gaussian (2α/π)^{3/4} exp(-α r²) at distance² r2.
231pub fn gaussian_s_value(alpha: f64, r2: f64) -> f64 {
232    let norm = (2.0 * alpha / PI).powf(0.75);
233    norm * (-alpha * r2).exp()
234}
235
236/// Evaluate a normalized p-type Gaussian component: N × x × exp(-α r²).
237/// `component` is the Cartesian displacement (x, y, or z) from the center.
238pub fn gaussian_p_value(alpha: f64, r2: f64, component: f64) -> f64 {
239    let norm = (128.0 * alpha.powi(5) / (PI * PI * PI)).powf(0.25);
240    norm * component * (-alpha * r2).exp()
241}
242
243/// Build the full molecular basis set from atom positions (in Angstrom) and elements.
244/// Positions are converted internally to bohr for consistency.
245pub fn build_basis(elements: &[u8], positions: &[[f64; 3]]) -> Vec<AtomicOrbital> {
246    use super::params::get_params;
247
248    let ang_to_bohr = 1.0 / 0.529177249;
249    let mut basis = Vec::new();
250
251    for (atom_idx, (&z, pos)) in elements.iter().zip(positions.iter()).enumerate() {
252        let params = match get_params(z) {
253            Some(p) => p,
254            None => continue,
255        };
256
257        let center = [
258            pos[0] * ang_to_bohr,
259            pos[1] * ang_to_bohr,
260            pos[2] * ang_to_bohr,
261        ];
262
263        let symbol = params.symbol;
264
265        for orb_def in params.orbitals {
266            if orb_def.l == 0 {
267                // s orbital: single function
268                basis.push(AtomicOrbital {
269                    atom_index: atom_idx,
270                    center,
271                    n: orb_def.n,
272                    l: 0,
273                    m: 0,
274                    vsip: orb_def.vsip,
275                    zeta: orb_def.zeta,
276                    label: format!("{}_{}", symbol, orb_def.label),
277                    gaussians: sto3g_expansion(orb_def.n, 0, orb_def.zeta),
278                });
279            } else if orb_def.l == 1 {
280                // p orbital: px, py, pz (m = -1, 0, +1)
281                let p_labels = ["x", "y", "z"];
282                let m_values: [i8; 3] = [-1, 0, 1];
283                for (idx, &m) in m_values.iter().enumerate() {
284                    basis.push(AtomicOrbital {
285                        atom_index: atom_idx,
286                        center,
287                        n: orb_def.n,
288                        l: 1,
289                        m,
290                        vsip: orb_def.vsip,
291                        zeta: orb_def.zeta,
292                        label: format!("{}_{}{}", symbol, orb_def.label, p_labels[idx]),
293                        gaussians: sto3g_expansion(orb_def.n, 1, orb_def.zeta),
294                    });
295                }
296            } else if orb_def.l == 2 {
297                // real d orbitals: dxy, dxz, dyz, dx2-y2, dz2
298                let d_labels = ["xy", "xz", "yz", "x2-y2", "z2"];
299                let m_values: [i8; 5] = [-2, -1, 0, 1, 2];
300                for (idx, &m) in m_values.iter().enumerate() {
301                    basis.push(AtomicOrbital {
302                        atom_index: atom_idx,
303                        center,
304                        n: orb_def.n,
305                        l: 2,
306                        m,
307                        vsip: orb_def.vsip,
308                        zeta: orb_def.zeta,
309                        label: format!("{}_{}{}", symbol, orb_def.label, d_labels[idx]),
310                        gaussians: sto3g_expansion(orb_def.n, 2, orb_def.zeta),
311                    });
312                }
313            }
314        }
315    }
316
317    basis
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn test_sto3g_1s_expansion() {
326        let gs = sto3g_expansion(1, 0, 1.0);
327        assert_eq!(gs.len(), 3);
328        // Coefficients sum roughly to 1 (not exact due to normalization)
329        let sum: f64 = gs.iter().map(|g| g.coeff).sum();
330        assert!((sum - 1.134292).abs() < 0.01);
331    }
332
333    #[test]
334    fn test_sto3g_scaling() {
335        // With ζ=2.0, alphas should be 4× the ζ=1.0 values
336        let gs1 = sto3g_expansion(1, 0, 1.0);
337        let gs2 = sto3g_expansion(1, 0, 2.0);
338        for (a, b) in gs1.iter().zip(gs2.iter()) {
339            assert!((b.alpha - a.alpha * 4.0).abs() < 1e-10);
340            assert!((b.coeff - a.coeff).abs() < 1e-10);
341        }
342    }
343
344    #[test]
345    fn test_build_basis_h2() {
346        // H₂ at 0.74 Å separation along x-axis
347        let elements = [1u8, 1];
348        let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
349        let basis = build_basis(&elements, &positions);
350        // H has 1 basis function (1s) each → 2 total
351        assert_eq!(basis.len(), 2);
352        assert_eq!(basis[0].label, "H_1s");
353        assert_eq!(basis[1].label, "H_1s");
354        assert_eq!(basis[0].atom_index, 0);
355        assert_eq!(basis[1].atom_index, 1);
356    }
357
358    #[test]
359    fn test_build_basis_h2o() {
360        // H₂O: O at origin, two H's
361        let elements = [8u8, 1, 1];
362        let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
363        let basis = build_basis(&elements, &positions);
364        // O: 2s + 2px + 2py + 2pz = 4, H: 1s each = 2.  Total = 6
365        assert_eq!(basis.len(), 6);
366    }
367
368    #[test]
369    fn test_gaussian_s_normalization() {
370        // Integral of |g(r)|² over all space should be 1.0 for a normalized Gaussian
371        // For (2α/π)^{3/2} exp(-2α r²), the integral is 1.
372        // We test at the origin: value should be (2α/π)^{3/4}
373        let alpha = 1.0;
374        let val = gaussian_s_value(alpha, 0.0);
375        let expected = (2.0 * alpha / PI).powf(0.75);
376        assert!((val - expected).abs() < 1e-12);
377    }
378
379    #[test]
380    fn test_gaussian_s_decay() {
381        let alpha = 1.0;
382        let v0 = gaussian_s_value(alpha, 0.0);
383        let v1 = gaussian_s_value(alpha, 1.0);
384        let v5 = gaussian_s_value(alpha, 5.0);
385        assert!(v0 > v1);
386        assert!(v1 > v5);
387        assert!(v5 > 0.0);
388    }
389
390    #[test]
391    fn test_sto3g_d_expansion() {
392        let gs = sto3g_expansion(3, 2, 1.0);
393        assert_eq!(gs.len(), 3);
394    }
395
396    #[test]
397    fn test_fe_basis_contains_nine_functions() {
398        let elements = [26u8];
399        let positions = [[0.0, 0.0, 0.0]];
400        let basis = build_basis(&elements, &positions);
401        // Fe parameterization includes 4s (1 AO), 4p (3 AO), and 3d (5 AO).
402        assert_eq!(basis.len(), 9);
403    }
404}