Skip to main content

sci_form/hf/
mo_transform.rs

1//! AO → MO integral transformation for CI methods.
2//!
3//! Pre-computes the full 4-index MO integral tensor:
4//!   (pq|rs)_MO = Σ_{μνλσ} C_{μp} C_{νq} C_{λr} C_{σs} (μν|λσ)_AO
5//!
6//! Uses the half-transformation (Yoshimine) approach for O(N⁵) scaling
7//! instead of naive O(N⁸).
8
9use super::integrals::get_eri;
10use nalgebra::DMatrix;
11
12/// Pre-computed MO-basis electron repulsion integrals.
13///
14/// Stores the full (pq|rs) tensor for indices in `[0, n_mo)`.
15/// Uses chemist notation: (pq|rs) = ∫∫ φ_p(1)φ_q(1) r₁₂⁻¹ φ_r(2)φ_s(2) d1 d2
16pub struct MoIntegrals {
17    /// The MO ERI tensor stored as flat array with 4-fold symmetry.
18    data: Vec<f64>,
19    /// Number of MO indices.
20    n_mo: usize,
21}
22
23impl MoIntegrals {
24    /// Retrieve (pq|rs) in chemist notation.
25    #[inline]
26    pub fn get(&self, p: usize, q: usize, r: usize, s: usize) -> f64 {
27        // Use 4-fold symmetry: (pq|rs) = (qp|rs) = (pq|sr) = (qp|sr)
28        let pq = if p >= q {
29            p * (p + 1) / 2 + q
30        } else {
31            q * (q + 1) / 2 + p
32        };
33        let rs = if r >= s {
34            r * (r + 1) / 2 + s
35        } else {
36            s * (s + 1) / 2 + r
37        };
38        let (idx1, idx2) = if pq >= rs { (pq, rs) } else { (rs, pq) };
39        let index = idx1 * (idx1 + 1) / 2 + idx2;
40        if index < self.data.len() {
41            self.data[index]
42        } else {
43            0.0
44        }
45    }
46
47    /// Antisymmetrized MO integral: <pq||rs> = (pr|qs) - (ps|qr)
48    #[inline]
49    pub fn antisymmetrized(&self, p: usize, q: usize, r: usize, s: usize) -> f64 {
50        self.get(p, r, q, s) - self.get(p, s, q, r)
51    }
52
53    /// Number of MO indices.
54    pub fn n_mo(&self) -> usize {
55        self.n_mo
56    }
57}
58
59/// Perform the 4-index AO → MO integral transformation.
60///
61/// Uses the Yoshimine half-transformation approach:
62///   Step 1: (μν|λσ) → (pν|λσ) — first quarter transform
63///   Step 2: (pν|λσ) → (pq|λσ) — second quarter transform
64///   Step 3: (pq|λσ) → (pq|rσ) — third quarter transform  
65///   Step 4: (pq|rσ) → (pq|rs) — fourth quarter transform
66///
67/// Each step is O(N⁵), total O(N⁵). Storage for intermediates is O(N⁴).
68///
69/// `coefficients`: MO coefficient matrix (n_ao × n_mo), column p = MO p.
70/// `ao_eris`: AO-basis ERIs in flat array (from HF integrals).
71/// `n_ao`: number of AO basis functions.
72/// `n_mo`: number of MO to transform (can be < n_ao for truncated virtual space).
73pub fn ao_to_mo_transform(
74    coefficients: &DMatrix<f64>,
75    ao_eris: &[f64],
76    n_ao: usize,
77    n_mo: usize,
78) -> MoIntegrals {
79    // Total unique pairs
80    let n_pair = n_mo * (n_mo + 1) / 2;
81    let n_total = n_pair * (n_pair + 1) / 2;
82    let mut mo_eris = vec![0.0; n_total];
83
84    // Half-transformed intermediates
85    // Strategy: transform one index at a time
86    // (μν|λσ) → accumulate into (pq|rs) using 4 nested transforms
87    //
88    // For small systems (n_ao < ~50), direct transformation is fast enough.
89    // For larger systems, the Yoshimine sort approach would be preferred.
90
91    // Direct 4-quarter transformation
92    // We accumulate (pq|rs) = Σ_{μνλσ} C_{μp} C_{νq} C_{λr} C_{σs} (μν|λσ)
93    // Using half-transform approach:
94    // tmp1[p,ν,λ,σ] = Σ_μ C_{μp} (μν|λσ)           -- O(N^5) first quarter
95
96    // Allocate half-transformed tensor (p, nu, lam, sig) with p < n_mo
97    let mut half1 = vec![0.0; n_mo * n_ao * n_ao * n_ao];
98
99    // First quarter: sum over μ
100    for p in 0..n_mo {
101        for nu in 0..n_ao {
102            for lam in 0..n_ao {
103                for sig in 0..=lam {
104                    let mut val = 0.0;
105                    for mu in 0..n_ao {
106                        let c_mp = coefficients[(mu, p)];
107                        if c_mp.abs() > 1e-14 {
108                            val += c_mp * get_eri(ao_eris, mu, nu, lam, sig, n_ao);
109                        }
110                    }
111                    let idx = ((p * n_ao + nu) * n_ao + lam) * n_ao + sig;
112                    half1[idx] = val;
113                    // (μν|λσ) = (μν|σλ) symmetry
114                    let idx2 = ((p * n_ao + nu) * n_ao + sig) * n_ao + lam;
115                    half1[idx2] = val;
116                }
117            }
118        }
119    }
120
121    // Second quarter: sum over ν → (pq|λσ)
122    let mut half2 = vec![0.0; n_mo * n_mo * n_ao * n_ao];
123    for p in 0..n_mo {
124        for q in 0..=p {
125            for lam in 0..n_ao {
126                for sig in 0..n_ao {
127                    let mut val = 0.0;
128                    for nu in 0..n_ao {
129                        let c_nq = coefficients[(nu, q)];
130                        if c_nq.abs() > 1e-14 {
131                            let idx = ((p * n_ao + nu) * n_ao + lam) * n_ao + sig;
132                            val += c_nq * half1[idx];
133                        }
134                    }
135                    let idx2 = ((p * n_mo + q) * n_ao + lam) * n_ao + sig;
136                    half2[idx2] = val;
137                }
138            }
139        }
140    }
141    drop(half1);
142
143    // Third quarter: sum over λ → (pq|rσ)
144    let mut half3 = vec![0.0; n_mo * n_mo * n_mo * n_ao];
145    for p in 0..n_mo {
146        for q in 0..=p {
147            for r in 0..n_mo {
148                for sig in 0..n_ao {
149                    let mut val = 0.0;
150                    for lam in 0..n_ao {
151                        let c_lr = coefficients[(lam, r)];
152                        if c_lr.abs() > 1e-14 {
153                            let idx = ((p * n_mo + q) * n_ao + lam) * n_ao + sig;
154                            val += c_lr * half2[idx];
155                        }
156                    }
157                    let idx3 = ((p * n_mo + q) * n_mo + r) * n_ao + sig;
158                    half3[idx3] = val;
159                }
160            }
161        }
162    }
163    drop(half2);
164
165    // Fourth quarter: sum over σ → store (pq|rs)
166    for p in 0..n_mo {
167        for q in 0..=p {
168            let pq = p * (p + 1) / 2 + q;
169            for r in 0..n_mo {
170                for s in 0..=r {
171                    let rs = r * (r + 1) / 2 + s;
172                    if pq < rs {
173                        continue; // will be set by symmetry
174                    }
175                    let mut val = 0.0;
176                    for sig in 0..n_ao {
177                        let c_ss = coefficients[(sig, s)];
178                        if c_ss.abs() > 1e-14 {
179                            let idx = ((p * n_mo + q) * n_mo + r) * n_ao + sig;
180                            val += c_ss * half3[idx];
181                        }
182                    }
183                    let index = pq * (pq + 1) / 2 + rs;
184                    mo_eris[index] = val;
185                }
186            }
187        }
188    }
189
190    MoIntegrals {
191        data: mo_eris,
192        n_mo,
193    }
194}
195
196/// Perform AO → MO transform with rayon parallelism on the first quarter.
197#[cfg(feature = "parallel")]
198pub fn ao_to_mo_transform_parallel(
199    coefficients: &DMatrix<f64>,
200    ao_eris: &[f64],
201    n_ao: usize,
202    n_mo: usize,
203) -> MoIntegrals {
204    use rayon::prelude::*;
205
206    let n_pair = n_mo * (n_mo + 1) / 2;
207    let n_total = n_pair * (n_pair + 1) / 2;
208
209    // First quarter: parallelize over p
210    let half1: Vec<Vec<f64>> = (0..n_mo)
211        .into_par_iter()
212        .map(|p| {
213            let mut slice = vec![0.0; n_ao * n_ao * n_ao];
214            for nu in 0..n_ao {
215                for lam in 0..n_ao {
216                    for sig in 0..=lam {
217                        let mut val = 0.0;
218                        for mu in 0..n_ao {
219                            let c_mp = coefficients[(mu, p)];
220                            if c_mp.abs() > 1e-14 {
221                                val += c_mp * get_eri(ao_eris, mu, nu, lam, sig, n_ao);
222                            }
223                        }
224                        slice[nu * n_ao * n_ao + lam * n_ao + sig] = val;
225                        slice[nu * n_ao * n_ao + sig * n_ao + lam] = val;
226                    }
227                }
228            }
229            slice
230        })
231        .collect();
232
233    // Second quarter
234    let mut half2 = vec![0.0; n_mo * n_mo * n_ao * n_ao];
235    for p in 0..n_mo {
236        for q in 0..=p {
237            for lam in 0..n_ao {
238                for sig in 0..n_ao {
239                    let mut val = 0.0;
240                    for nu in 0..n_ao {
241                        let c_nq = coefficients[(nu, q)];
242                        if c_nq.abs() > 1e-14 {
243                            val += c_nq * half1[p][nu * n_ao * n_ao + lam * n_ao + sig];
244                        }
245                    }
246                    half2[((p * n_mo + q) * n_ao + lam) * n_ao + sig] = val;
247                }
248            }
249        }
250    }
251    drop(half1);
252
253    // Third quarter
254    let mut half3 = vec![0.0; n_mo * n_mo * n_mo * n_ao];
255    for p in 0..n_mo {
256        for q in 0..=p {
257            for r in 0..n_mo {
258                for sig in 0..n_ao {
259                    let mut val = 0.0;
260                    for lam in 0..n_ao {
261                        let c_lr = coefficients[(lam, r)];
262                        if c_lr.abs() > 1e-14 {
263                            val += c_lr * half2[((p * n_mo + q) * n_ao + lam) * n_ao + sig];
264                        }
265                    }
266                    half3[((p * n_mo + q) * n_mo + r) * n_ao + sig] = val;
267                }
268            }
269        }
270    }
271    drop(half2);
272
273    // Fourth quarter
274    let mut mo_eris = vec![0.0; n_total];
275    for p in 0..n_mo {
276        for q in 0..=p {
277            let pq = p * (p + 1) / 2 + q;
278            for r in 0..n_mo {
279                for s in 0..=r {
280                    let rs = r * (r + 1) / 2 + s;
281                    if pq < rs {
282                        continue;
283                    }
284                    let mut val = 0.0;
285                    for sig in 0..n_ao {
286                        let c_ss = coefficients[(sig, s)];
287                        if c_ss.abs() > 1e-14 {
288                            val += c_ss * half3[((p * n_mo + q) * n_mo + r) * n_ao + sig];
289                        }
290                    }
291                    mo_eris[pq * (pq + 1) / 2 + rs] = val;
292                }
293            }
294        }
295    }
296
297    MoIntegrals {
298        data: mo_eris,
299        n_mo,
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn test_mo_integrals_symmetry() {
309        // Simple 2-MO system
310        let s = std::f64::consts::FRAC_1_SQRT_2;
311        let c = DMatrix::from_row_slice(2, 2, &[s, s, s, -s]);
312        // Simple AO ERIs (all equal for testing)
313        let eris = vec![0.5; 16];
314        let mo = ao_to_mo_transform(&c, &eris, 2, 2);
315        // (pq|rs) should have 4-fold symmetry
316        assert!((mo.get(0, 0, 1, 1) - mo.get(1, 1, 0, 0)).abs() < 1e-10);
317    }
318}