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    table
213        .iter()
214        .map(|&(coeff, alpha)| GaussianPrimitive {
215            coeff,
216            alpha: alpha * zeta_sq,
217        })
218        .collect()
219}
220
221/// Evaluate a normalized s-type Gaussian (2α/π)^{3/4} exp(-α r²) at distance² r2.
222pub fn gaussian_s_value(alpha: f64, r2: f64) -> f64 {
223    let norm = (2.0 * alpha / PI).powf(0.75);
224    norm * (-alpha * r2).exp()
225}
226
227/// Evaluate a normalized p-type Gaussian component: N × x × exp(-α r²).
228/// `component` is the Cartesian displacement (x, y, or z) from the center.
229pub fn gaussian_p_value(alpha: f64, r2: f64, component: f64) -> f64 {
230    let norm = (128.0 * alpha.powi(5) / (PI * PI * PI)).powf(0.25);
231    norm * component * (-alpha * r2).exp()
232}
233
234/// Build the full molecular basis set from atom positions (in Angstrom) and elements.
235/// Positions are converted internally to bohr for consistency.
236pub fn build_basis(elements: &[u8], positions: &[[f64; 3]]) -> Vec<AtomicOrbital> {
237    use super::params::get_params;
238
239    let ang_to_bohr = 1.0 / 0.529177249;
240    let mut basis = Vec::new();
241
242    for (atom_idx, (&z, pos)) in elements.iter().zip(positions.iter()).enumerate() {
243        let params = match get_params(z) {
244            Some(p) => p,
245            None => continue,
246        };
247
248        let center = [
249            pos[0] * ang_to_bohr,
250            pos[1] * ang_to_bohr,
251            pos[2] * ang_to_bohr,
252        ];
253
254        let symbol = params.symbol;
255
256        for orb_def in params.orbitals {
257            if orb_def.l == 0 {
258                // s orbital: single function
259                basis.push(AtomicOrbital {
260                    atom_index: atom_idx,
261                    center,
262                    n: orb_def.n,
263                    l: 0,
264                    m: 0,
265                    vsip: orb_def.vsip,
266                    zeta: orb_def.zeta,
267                    label: format!("{}_{}", symbol, orb_def.label),
268                    gaussians: sto3g_expansion(orb_def.n, 0, orb_def.zeta),
269                });
270            } else if orb_def.l == 1 {
271                // p orbital: px, py, pz (m = -1, 0, +1)
272                let p_labels = ["x", "y", "z"];
273                let m_values: [i8; 3] = [-1, 0, 1];
274                for (idx, &m) in m_values.iter().enumerate() {
275                    basis.push(AtomicOrbital {
276                        atom_index: atom_idx,
277                        center,
278                        n: orb_def.n,
279                        l: 1,
280                        m,
281                        vsip: orb_def.vsip,
282                        zeta: orb_def.zeta,
283                        label: format!("{}_{}{}", symbol, orb_def.label, p_labels[idx]),
284                        gaussians: sto3g_expansion(orb_def.n, 1, orb_def.zeta),
285                    });
286                }
287            } else if orb_def.l == 2 {
288                // real d orbitals: dxy, dxz, dyz, dx2-y2, dz2
289                let d_labels = ["xy", "xz", "yz", "x2-y2", "z2"];
290                let m_values: [i8; 5] = [-2, -1, 0, 1, 2];
291                for (idx, &m) in m_values.iter().enumerate() {
292                    basis.push(AtomicOrbital {
293                        atom_index: atom_idx,
294                        center,
295                        n: orb_def.n,
296                        l: 2,
297                        m,
298                        vsip: orb_def.vsip,
299                        zeta: orb_def.zeta,
300                        label: format!("{}_{}{}", symbol, orb_def.label, d_labels[idx]),
301                        gaussians: sto3g_expansion(orb_def.n, 2, orb_def.zeta),
302                    });
303                }
304            }
305        }
306    }
307
308    basis
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn test_sto3g_1s_expansion() {
317        let gs = sto3g_expansion(1, 0, 1.0);
318        assert_eq!(gs.len(), 3);
319        // Coefficients sum roughly to 1 (not exact due to normalization)
320        let sum: f64 = gs.iter().map(|g| g.coeff).sum();
321        assert!((sum - 1.134292).abs() < 0.01);
322    }
323
324    #[test]
325    fn test_sto3g_scaling() {
326        // With ζ=2.0, alphas should be 4× the ζ=1.0 values
327        let gs1 = sto3g_expansion(1, 0, 1.0);
328        let gs2 = sto3g_expansion(1, 0, 2.0);
329        for (a, b) in gs1.iter().zip(gs2.iter()) {
330            assert!((b.alpha - a.alpha * 4.0).abs() < 1e-10);
331            assert!((b.coeff - a.coeff).abs() < 1e-10);
332        }
333    }
334
335    #[test]
336    fn test_build_basis_h2() {
337        // H₂ at 0.74 Å separation along x-axis
338        let elements = [1u8, 1];
339        let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
340        let basis = build_basis(&elements, &positions);
341        // H has 1 basis function (1s) each → 2 total
342        assert_eq!(basis.len(), 2);
343        assert_eq!(basis[0].label, "H_1s");
344        assert_eq!(basis[1].label, "H_1s");
345        assert_eq!(basis[0].atom_index, 0);
346        assert_eq!(basis[1].atom_index, 1);
347    }
348
349    #[test]
350    fn test_build_basis_h2o() {
351        // H₂O: O at origin, two H's
352        let elements = [8u8, 1, 1];
353        let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
354        let basis = build_basis(&elements, &positions);
355        // O: 2s + 2px + 2py + 2pz = 4, H: 1s each = 2.  Total = 6
356        assert_eq!(basis.len(), 6);
357    }
358
359    #[test]
360    fn test_gaussian_s_normalization() {
361        // Integral of |g(r)|² over all space should be 1.0 for a normalized Gaussian
362        // For (2α/π)^{3/2} exp(-2α r²), the integral is 1.
363        // We test at the origin: value should be (2α/π)^{3/4}
364        let alpha = 1.0;
365        let val = gaussian_s_value(alpha, 0.0);
366        let expected = (2.0 * alpha / PI).powf(0.75);
367        assert!((val - expected).abs() < 1e-12);
368    }
369
370    #[test]
371    fn test_gaussian_s_decay() {
372        let alpha = 1.0;
373        let v0 = gaussian_s_value(alpha, 0.0);
374        let v1 = gaussian_s_value(alpha, 1.0);
375        let v5 = gaussian_s_value(alpha, 5.0);
376        assert!(v0 > v1);
377        assert!(v1 > v5);
378        assert!(v5 > 0.0);
379    }
380
381    #[test]
382    fn test_sto3g_d_expansion() {
383        let gs = sto3g_expansion(3, 2, 1.0);
384        assert_eq!(gs.len(), 3);
385    }
386
387    #[test]
388    fn test_fe_basis_contains_nine_functions() {
389        let elements = [26u8];
390        let positions = [[0.0, 0.0, 0.0]];
391        let basis = build_basis(&elements, &positions);
392        // Fe parameterization includes 4s (1 AO), 4p (3 AO), and 3d (5 AO).
393        assert_eq!(basis.len(), 9);
394    }
395}