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
9/// A single Gaussian primitive: coefficient × exp(-alpha × r²).
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct GaussianPrimitive {
12    /// Contraction coefficient (pre-normalized).
13    pub coeff: f64,
14    /// Gaussian exponent (bohr⁻²).
15    pub alpha: f64,
16}
17
18/// A Slater-type orbital represented conceptually.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct SlaterOrbital {
21    /// Principal quantum number.
22    pub n: u8,
23    /// Angular momentum l.
24    pub l: u8,
25    /// Magnetic quantum number m.
26    pub m: i8,
27    /// Slater exponent ζ.
28    pub zeta: f64,
29}
30
31/// A single atomic orbital in the molecular basis, located on a specific atom.
32#[derive(Debug, Clone)]
33pub struct AtomicOrbital {
34    /// Index of the atom this orbital belongs to.
35    pub atom_index: usize,
36    /// Center position in 3D space (bohr).
37    pub center: [f64; 3],
38    /// Principal quantum number.
39    pub n: u8,
40    /// Angular momentum l.
41    pub l: u8,
42    /// Magnetic quantum number m.
43    pub m: i8,
44    /// VSIP value (eV) for diagonal H_ii.
45    pub vsip: f64,
46    /// Slater exponent.
47    pub zeta: f64,
48    /// Label, e.g. "C_2s", "O_2px".
49    pub label: String,
50    /// Contracted Gaussian primitives (STO-nG expansion).
51    pub gaussians: Vec<GaussianPrimitive>,
52}
53
54// ─── STO-3G contraction coefficients ─────────────────────────────────────────
55// From Hehre, Stewart, Pople, J. Chem. Phys. 51, 2657 (1969).
56// These are for the expansion of a Slater orbital with ζ=1.0.
57// When ζ ≠ 1.0, scale alpha by ζ².
58
59/// STO-3G coefficients for 1s orbital (n=1, l=0).
60static STO3G_1S: [(f64, f64); 3] = [
61    (0.154329, 2.227660),
62    (0.535328, 0.405771),
63    (0.444635, 0.109818),
64];
65
66/// STO-3G coefficients for 2s orbital (n=2, l=0).
67/// Tabulated for ζ=1.0, two-term linear combination (2s = outer - inner).
68static STO3G_2SP_INNER: [(f64, f64); 3] = [
69    (-0.099967, 2.581058),
70    (0.399513, 0.532013),
71    (0.700115, 0.168856),
72];
73
74/// STO-3G coefficients for 2p orbital (n=2, l=1).
75static STO3G_2SP_OUTER: [(f64, f64); 3] = [
76    (0.155916, 2.581058),
77    (0.607684, 0.532013),
78    (0.391957, 0.168856),
79];
80
81/// STO-3G coefficients for 3sp orbital (n=3, l=0 or 1).
82static STO3G_3SP_INNER: [(f64, f64); 3] = [
83    (-0.219620, 0.994203),
84    (0.225595, 0.231031),
85    (0.900398, 0.075142),
86];
87
88static STO3G_3SP_OUTER: [(f64, f64); 3] = [
89    (0.010588, 0.994203),
90    (0.595167, 0.231031),
91    (0.462001, 0.075142),
92];
93
94/// STO-3G for 4sp.
95static STO3G_4SP_INNER: [(f64, f64); 3] = [
96    (-0.310438, 0.494718),
97    (0.015515, 0.120193),
98    (1.023468, 0.040301),
99];
100
101static STO3G_4SP_OUTER: [(f64, f64); 3] = [
102    (-0.063311, 0.494718),
103    (0.574459, 0.120193),
104    (0.499768, 0.040301),
105];
106
107/// STO-3G for 5sp.
108static STO3G_5SP_INNER: [(f64, f64); 3] = [
109    (-0.383204, 0.289515),
110    (-0.159438, 0.073780),
111    (1.143456, 0.025335),
112];
113
114static STO3G_5SP_OUTER: [(f64, f64); 3] = [
115    (-0.094810, 0.289515),
116    (0.570520, 0.073780),
117    (0.497340, 0.025335),
118];
119
120/// Build STO-3G Gaussian primitives for an orbital with given n, l, and ζ.
121pub fn sto3g_expansion(n: u8, l: u8, zeta: f64) -> Vec<GaussianPrimitive> {
122    let zeta_sq = zeta * zeta;
123
124    let table: &[(f64, f64); 3] = match (n, l) {
125        (1, 0) => &STO3G_1S,
126        (2, 0) => &STO3G_2SP_INNER,
127        (2, 1) => &STO3G_2SP_OUTER,
128        (3, 0) => &STO3G_3SP_INNER,
129        (3, 1) => &STO3G_3SP_OUTER,
130        (4, 0) => &STO3G_4SP_INNER,
131        (4, 1) => &STO3G_4SP_OUTER,
132        (5, 0) => &STO3G_5SP_INNER,
133        (5, 1) => &STO3G_5SP_OUTER,
134        _ => return vec![],
135    };
136
137    table
138        .iter()
139        .map(|&(coeff, alpha)| GaussianPrimitive {
140            coeff,
141            alpha: alpha * zeta_sq,
142        })
143        .collect()
144}
145
146/// Evaluate a normalized s-type Gaussian (2α/π)^{3/4} exp(-α r²) at distance² r2.
147pub fn gaussian_s_value(alpha: f64, r2: f64) -> f64 {
148    let norm = (2.0 * alpha / PI).powf(0.75);
149    norm * (-alpha * r2).exp()
150}
151
152/// Evaluate a normalized p-type Gaussian component: N × x × exp(-α r²).
153/// `component` is the Cartesian displacement (x, y, or z) from the center.
154pub fn gaussian_p_value(alpha: f64, r2: f64, component: f64) -> f64 {
155    let norm = (128.0 * alpha.powi(5) / (PI * PI * PI)).powf(0.25);
156    norm * component * (-alpha * r2).exp()
157}
158
159/// Build the full molecular basis set from atom positions (in Angstrom) and elements.
160/// Positions are converted internally to bohr for consistency.
161pub fn build_basis(elements: &[u8], positions: &[[f64; 3]]) -> Vec<AtomicOrbital> {
162    use super::params::get_params;
163
164    let ang_to_bohr = 1.0 / 0.529177249;
165    let mut basis = Vec::new();
166
167    for (atom_idx, (&z, pos)) in elements.iter().zip(positions.iter()).enumerate() {
168        let params = match get_params(z) {
169            Some(p) => p,
170            None => continue,
171        };
172
173        let center = [
174            pos[0] * ang_to_bohr,
175            pos[1] * ang_to_bohr,
176            pos[2] * ang_to_bohr,
177        ];
178
179        let symbol = params.symbol;
180
181        for orb_def in params.orbitals {
182            if orb_def.l == 0 {
183                // s orbital: single function
184                basis.push(AtomicOrbital {
185                    atom_index: atom_idx,
186                    center,
187                    n: orb_def.n,
188                    l: 0,
189                    m: 0,
190                    vsip: orb_def.vsip,
191                    zeta: orb_def.zeta,
192                    label: format!("{}_{}", symbol, orb_def.label),
193                    gaussians: sto3g_expansion(orb_def.n, 0, orb_def.zeta),
194                });
195            } else if orb_def.l == 1 {
196                // p orbital: px, py, pz (m = -1, 0, +1)
197                let p_labels = ["x", "y", "z"];
198                let m_values: [i8; 3] = [-1, 0, 1];
199                for (idx, &m) in m_values.iter().enumerate() {
200                    basis.push(AtomicOrbital {
201                        atom_index: atom_idx,
202                        center,
203                        n: orb_def.n,
204                        l: 1,
205                        m,
206                        vsip: orb_def.vsip,
207                        zeta: orb_def.zeta,
208                        label: format!("{}_{}{}", symbol, orb_def.label, p_labels[idx]),
209                        gaussians: sto3g_expansion(orb_def.n, 1, orb_def.zeta),
210                    });
211                }
212            }
213        }
214    }
215
216    basis
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_sto3g_1s_expansion() {
225        let gs = sto3g_expansion(1, 0, 1.0);
226        assert_eq!(gs.len(), 3);
227        // Coefficients sum roughly to 1 (not exact due to normalization)
228        let sum: f64 = gs.iter().map(|g| g.coeff).sum();
229        assert!((sum - 1.134292).abs() < 0.01);
230    }
231
232    #[test]
233    fn test_sto3g_scaling() {
234        // With ζ=2.0, alphas should be 4× the ζ=1.0 values
235        let gs1 = sto3g_expansion(1, 0, 1.0);
236        let gs2 = sto3g_expansion(1, 0, 2.0);
237        for (a, b) in gs1.iter().zip(gs2.iter()) {
238            assert!((b.alpha - a.alpha * 4.0).abs() < 1e-10);
239            assert!((b.coeff - a.coeff).abs() < 1e-10);
240        }
241    }
242
243    #[test]
244    fn test_build_basis_h2() {
245        // H₂ at 0.74 Å separation along x-axis
246        let elements = [1u8, 1];
247        let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
248        let basis = build_basis(&elements, &positions);
249        // H has 1 basis function (1s) each → 2 total
250        assert_eq!(basis.len(), 2);
251        assert_eq!(basis[0].label, "H_1s");
252        assert_eq!(basis[1].label, "H_1s");
253        assert_eq!(basis[0].atom_index, 0);
254        assert_eq!(basis[1].atom_index, 1);
255    }
256
257    #[test]
258    fn test_build_basis_h2o() {
259        // H₂O: O at origin, two H's
260        let elements = [8u8, 1, 1];
261        let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
262        let basis = build_basis(&elements, &positions);
263        // O: 2s + 2px + 2py + 2pz = 4, H: 1s each = 2.  Total = 6
264        assert_eq!(basis.len(), 6);
265    }
266
267    #[test]
268    fn test_gaussian_s_normalization() {
269        // Integral of |g(r)|² over all space should be 1.0 for a normalized Gaussian
270        // For (2α/π)^{3/2} exp(-2α r²), the integral is 1.
271        // We test at the origin: value should be (2α/π)^{3/4}
272        let alpha = 1.0;
273        let val = gaussian_s_value(alpha, 0.0);
274        let expected = (2.0 * alpha / PI).powf(0.75);
275        assert!((val - expected).abs() < 1e-12);
276    }
277
278    #[test]
279    fn test_gaussian_s_decay() {
280        let alpha = 1.0;
281        let v0 = gaussian_s_value(alpha, 0.0);
282        let v1 = gaussian_s_value(alpha, 1.0);
283        let v5 = gaussian_s_value(alpha, 5.0);
284        assert!(v0 > v1);
285        assert!(v1 > v5);
286        assert!(v5 > 0.0);
287    }
288}