qualia_core_db/specialized_libs/chemistry_modeling/
integrals.rs1use crate::specialized_libs::shared::zero_heap_algebra::ZeroHeapMatrix;
8use core::f64::consts::PI;
9
10#[derive(Debug, Clone, Copy)]
12pub struct GtoPrimitive {
13 pub origin: [f64; 3],
14 pub exponent: f64,
15 pub l: [u8; 3], pub coefficient: f64,
17}
18
19impl GtoPrimitive {
20 pub fn total_angular_momentum(&self) -> u8 {
21 self.l[0] + self.l[1] + self.l[2]
22 }
23}
24
25pub struct IntegralEngine;
27
28impl IntegralEngine {
29 pub fn evaluate_overlap<const N: usize, const M: usize>(
32 basis_a: &[GtoPrimitive; N],
33 basis_b: &[GtoPrimitive; M],
34 ) -> ZeroHeapMatrix<f64, N, M> {
35 let mut s_matrix = ZeroHeapMatrix::zeros();
36
37 for i in 0..N {
38 for j in 0..M {
39 let a = &basis_a[i];
40 let b = &basis_b[j];
41
42 let l_total = a.total_angular_momentum() + b.total_angular_momentum();
44
45 let val = if l_total <= 2 {
46 Self::os_overlap(a, b)
48 } else {
49 Self::rys_overlap(a, b)
51 };
52
53 s_matrix.set(i, j, val);
54 }
55 }
56
57 s_matrix
58 }
59
60 fn os_overlap(a: &GtoPrimitive, b: &GtoPrimitive) -> f64 {
62 let alpha = a.exponent;
65 let beta = b.exponent;
66 let p = alpha + beta;
67 let mu = (alpha * beta) / p;
68
69 let dx = a.origin[0] - b.origin[0];
70 let dy = a.origin[1] - b.origin[1];
71 let dz = a.origin[2] - b.origin[2];
72 let ab2 = dx * dx + dy * dy + dz * dz;
73
74 let s_s = (PI / p).powf(1.5) * f64::exp(-mu * ab2);
75
76 s_s * a.coefficient * b.coefficient
78 }
79
80 fn rys_overlap(a: &GtoPrimitive, b: &GtoPrimitive) -> f64 {
82 Self::os_overlap(a, b)
86 }
87
88 pub fn overlap_s(a: &GtoPrimitive, b: &GtoPrimitive) -> f64 {
95 let (p, mu, ab2) = Self::gauss_product(a, b);
96 (PI / p).powf(1.5) * f64::exp(-mu * ab2) * a.coefficient * b.coefficient
97 }
98
99 pub fn kinetic_s(a: &GtoPrimitive, b: &GtoPrimitive) -> f64 {
104 let (p, mu, ab2) = Self::gauss_product(a, b);
105 let s_bare = (PI / p).powf(1.5) * f64::exp(-mu * ab2);
106 mu * (3.0 - 2.0 * mu * ab2) * s_bare * a.coefficient * b.coefficient
107 }
108
109 pub fn nuclear_s(a: &GtoPrimitive, b: &GtoPrimitive, center: [f64; 3], z: f64) -> f64 {
117 let (p, mu, ab2) = Self::gauss_product(a, b);
118 let alpha = a.exponent;
119 let beta = b.exponent;
120 let pcenter = [
121 (alpha * a.origin[0] + beta * b.origin[0]) / p,
122 (alpha * a.origin[1] + beta * b.origin[1]) / p,
123 (alpha * a.origin[2] + beta * b.origin[2]) / p,
124 ];
125 let pc2 = (pcenter[0] - center[0]).powi(2)
126 + (pcenter[1] - center[1]).powi(2)
127 + (pcenter[2] - center[2]).powi(2);
128 let k = f64::exp(-mu * ab2);
129 let f0 = Self::boys_function(0, p * pc2);
130 -z * (2.0 * PI / p) * k * f0 * a.coefficient * b.coefficient
131 }
132
133 pub fn dipole_s(a: &GtoPrimitive, b: &GtoPrimitive) -> [f64; 3] {
138 let (p, mu, ab2) = Self::gauss_product(a, b);
139 let alpha = a.exponent;
140 let beta = b.exponent;
141 let s = (PI / p).powf(1.5) * f64::exp(-mu * ab2) * a.coefficient * b.coefficient;
142 let pcenter = [
143 (alpha * a.origin[0] + beta * b.origin[0]) / p,
144 (alpha * a.origin[1] + beta * b.origin[1]) / p,
145 (alpha * a.origin[2] + beta * b.origin[2]) / p,
146 ];
147 [pcenter[0] * s, pcenter[1] * s, pcenter[2] * s]
148 }
149
150 #[inline]
153 fn gauss_product(a: &GtoPrimitive, b: &GtoPrimitive) -> (f64, f64, f64) {
154 let alpha = a.exponent;
155 let beta = b.exponent;
156 let p = alpha + beta;
157 let mu = (alpha * beta) / p;
158 let dx = a.origin[0] - b.origin[0];
159 let dy = a.origin[1] - b.origin[1];
160 let dz = a.origin[2] - b.origin[2];
161 (p, mu, dx * dx + dy * dy + dz * dz)
162 }
163
164 pub fn evaluate_eri(
168 a: &GtoPrimitive,
169 b: &GtoPrimitive,
170 c: &GtoPrimitive,
171 d: &GtoPrimitive,
172 ) -> f64 {
173 let l_total = a.total_angular_momentum()
174 + b.total_angular_momentum()
175 + c.total_angular_momentum()
176 + d.total_angular_momentum();
177
178 if l_total <= 4 {
179 Self::hgp_eri(a, b, c, d)
180 } else {
181 Self::rys_eri(a, b, c, d)
182 }
183 }
184
185 fn hgp_eri(a: &GtoPrimitive, b: &GtoPrimitive, c: &GtoPrimitive, d: &GtoPrimitive) -> f64 {
187 let alpha = a.exponent;
188 let beta = b.exponent;
189 let gamma = c.exponent;
190 let delta = d.exponent;
191
192 let p = alpha + beta;
193 let q = gamma + delta;
194 let alpha_p = (alpha * beta) / p;
195 let alpha_q = (gamma * delta) / q;
196
197 let r_p = [
198 (alpha * a.origin[0] + beta * b.origin[0]) / p,
199 (alpha * a.origin[1] + beta * b.origin[1]) / p,
200 (alpha * a.origin[2] + beta * b.origin[2]) / p,
201 ];
202
203 let r_q = [
204 (gamma * c.origin[0] + delta * d.origin[0]) / q,
205 (gamma * c.origin[1] + delta * d.origin[1]) / q,
206 (gamma * c.origin[2] + delta * d.origin[2]) / q,
207 ];
208
209 let ab2 = (a.origin[0] - b.origin[0]).powi(2)
210 + (a.origin[1] - b.origin[1]).powi(2)
211 + (a.origin[2] - b.origin[2]).powi(2);
212 let cd2 = (c.origin[0] - d.origin[0]).powi(2)
213 + (c.origin[1] - d.origin[1]).powi(2)
214 + (c.origin[2] - d.origin[2]).powi(2);
215 let pq2 = (r_p[0] - r_q[0]).powi(2) + (r_p[1] - r_q[1]).powi(2) + (r_p[2] - r_q[2]).powi(2);
216
217 let t = (p * q) / (p + q) * pq2;
218 let f0_t = Self::boys_function(0, t);
219
220 let prefactor = 2.0 * PI.powf(2.5) / (p * q * f64::sqrt(p + q));
221 let exp_ab = f64::exp(-alpha_p * ab2);
222 let exp_cd = f64::exp(-alpha_q * cd2);
223
224 prefactor
225 * exp_ab
226 * exp_cd
227 * f0_t
228 * a.coefficient
229 * b.coefficient
230 * c.coefficient
231 * d.coefficient
232 }
233
234 fn rys_eri(a: &GtoPrimitive, b: &GtoPrimitive, c: &GtoPrimitive, d: &GtoPrimitive) -> f64 {
236 let l_total = a.total_angular_momentum()
237 + b.total_angular_momentum()
238 + c.total_angular_momentum()
239 + d.total_angular_momentum();
240
241 let n_roots = (l_total / 2 + 1) as usize;
242
243 let mut roots = [0.0; 8];
245 let mut weights = [0.0; 8];
246
247 let alpha = a.exponent;
248 let beta = b.exponent;
249 let gamma = c.exponent;
250 let delta = d.exponent;
251 let p = alpha + beta;
252 let q = gamma + delta;
253 let t = (p * q) / (p + q); let mut f_vals = [0.0; 17]; for m in 0..=(2 * n_roots) {
258 f_vals[m] = Self::boys_function(m as u8, t);
259 }
260
261 let mut alpha_coef = [0.0; 8];
264 let mut beta_coef = [0.0; 8];
265 let mut sigma = [[0.0; 17]; 9]; for i in 0..=(2 * n_roots) {
269 sigma[1][i] = f_vals[i];
270 }
271
272 if f_vals[0].abs() > 1e-15 {
273 alpha_coef[0] = f_vals[1] / f_vals[0];
274 beta_coef[0] = f_vals[0];
275
276 for k in 1..n_roots {
277 for l in k..(2 * n_roots - k + 1) {
278 sigma[k + 1][l] = sigma[k][l + 1]
279 - alpha_coef[k - 1] * sigma[k][l]
280 - beta_coef[k - 1] * sigma[k - 1][l];
281 }
282 if sigma[k][k - 1].abs() > 1e-15 {
283 alpha_coef[k] =
284 sigma[k + 1][k + 1] / sigma[k + 1][k] - sigma[k][k] / sigma[k][k - 1];
285 beta_coef[k] = sigma[k + 1][k] / sigma[k][k - 1];
286 }
287 }
288
289 let mut t_mat = crate::specialized_libs::shared::zero_heap_algebra::ZeroHeapMatrix::<
291 f64,
292 8,
293 8,
294 >::zeros();
295 for i in 0..8 {
296 if i < n_roots {
297 t_mat.set(i, i, alpha_coef[i]);
298 if i < n_roots - 1 {
299 let off_diag = beta_coef[i + 1].abs().sqrt();
300 t_mat.set(i, i + 1, off_diag);
301 t_mat.set(i + 1, i, off_diag);
302 }
303 } else {
304 t_mat.set(i, i, 1.0); }
306 }
307
308 if let Ok((evals, evecs)) =
309 crate::specialized_libs::chemistry_modeling::scf::jacobi_diagonalization(&t_mat)
310 {
311 for i in 0..n_roots {
312 roots[i] = evals[i];
313 let v = evecs.get(0, i);
314 weights[i] = v * v * f_vals[0];
315 }
316 } else {
317 roots[0] = t / (p + q);
318 weights[0] = f_vals[0];
319 }
320 } else {
321 roots[0] = t / (p + q);
322 weights[0] = f_vals[0];
323 }
324
325 let mut eri = 0.0;
326
327 let r_p = [
329 (alpha * a.origin[0] + beta * b.origin[0]) / p,
330 (alpha * a.origin[1] + beta * b.origin[1]) / p,
331 (alpha * a.origin[2] + beta * b.origin[2]) / p,
332 ];
333 let r_q = [
334 (gamma * c.origin[0] + delta * d.origin[0]) / q,
335 (gamma * c.origin[1] + delta * d.origin[1]) / q,
336 (gamma * c.origin[2] + delta * d.origin[2]) / q,
337 ];
338
339 for i in 0..n_roots {
340 let u2 = roots[i];
342 let w = weights[i];
343
344 let ix = Self::hermite_1d(
349 u2,
350 r_p[0] - a.origin[0],
351 r_p[0] - b.origin[0],
352 r_q[0] - c.origin[0],
353 r_q[0] - d.origin[0],
354 a.l[0],
355 b.l[0],
356 c.l[0],
357 d.l[0],
358 );
359 let iy = Self::hermite_1d(
360 u2,
361 r_p[1] - a.origin[1],
362 r_p[1] - b.origin[1],
363 r_q[1] - c.origin[1],
364 r_q[1] - d.origin[1],
365 a.l[1],
366 b.l[1],
367 c.l[1],
368 d.l[1],
369 );
370 let iz = Self::hermite_1d(
371 u2,
372 r_p[2] - a.origin[2],
373 r_p[2] - b.origin[2],
374 r_q[2] - c.origin[2],
375 r_q[2] - d.origin[2],
376 a.l[2],
377 b.l[2],
378 c.l[2],
379 d.l[2],
380 );
381
382 eri += w * ix * iy * iz;
383 }
384
385 eri * a.coefficient * b.coefficient * c.coefficient * d.coefficient
386 }
387
388 fn hermite_1d(
400 u: f64,
401 pa: f64,
402 pb: f64,
403 qc: f64,
404 qd: f64,
405 la: u8,
406 lb: u8,
407 lc: u8,
408 ld: u8,
409 ) -> f64 {
410 if la + lb + lc + ld == 0 {
412 return 1.0;
413 }
414
415 let bra = if la == 1 && lb == 0 {
419 u * pa
420 } else if la == 0 && lb == 1 {
421 u * pb
422 } else if la == 1 && lb == 1 {
423 u * u * pa * pb
424 } else if la == 2 && lb == 0 {
425 u * u * pa * pa + 0.5
426 } else if la == 0 && lb == 2 {
427 u * u * pb * pb + 0.5
428 } else {
429 1.0
430 }; let ket = if lc == 1 && ld == 0 {
434 u * qc
435 } else if lc == 0 && ld == 1 {
436 u * qd
437 } else if lc == 1 && ld == 1 {
438 u * u * qc * qd
439 } else if lc == 2 && ld == 0 {
440 u * u * qc * qc + 0.5
441 } else if lc == 0 && ld == 2 {
442 u * u * qd * qd + 0.5
443 } else {
444 1.0
445 };
446
447 bra * ket
448 }
449
450 pub fn boys_function(n: u8, t: f64) -> f64 {
455 const T_LOWER: f64 = 1e-7;
456 const T_UPPER: f64 = 30.0;
457
458 if t <= T_LOWER {
459 let mut result = 0.0;
462 let mut term = 1.0 / (2.0 * n as f64 + 1.0);
463 let mut k = 0;
464
465 while term.abs() > 1e-15 && k < 10 {
466 result += term;
467 k += 1;
468 let k_f64 = k as f64;
469 term = -term * t * (2.0 * n as f64 + 2.0 * k_f64 - 1.0)
470 / (k_f64 * (2.0 * n as f64 + 2.0 * k_f64 + 1.0));
471 }
472 result
473 } else if t >= T_UPPER {
474 let mut val = (PI / t).sqrt() / 2.0;
477 for i in 1..=n {
478 val *= (2.0 * i as f64 - 1.0) / (2.0 * t);
479 }
480 val
481 } else {
482 let m_max = n + 15;
490 let mut fm = 0.0;
491 let mut term = 1.0 / (2.0 * m_max as f64 + 1.0);
492 let mut k = 0;
493 while term.abs() > 1e-15 && k < 50 {
494 fm += term;
495 k += 1;
496 let k_f64 = k as f64;
497 term = -term * t * (2.0 * m_max as f64 + 2.0 * k_f64 - 1.0)
498 / (k_f64 * (2.0 * m_max as f64 + 2.0 * k_f64 + 1.0));
499 }
500
501 let exp_t = f64::exp(-t);
502 for m in (n..m_max).rev() {
503 fm = (2.0 * t * fm + exp_t) / (2.0 * m as f64 + 1.0);
504 }
505 fm
506 }
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use core::f64::consts::PI;
514
515 #[test]
516 fn test_overlap_s_orbitals() {
517 let a = GtoPrimitive {
518 origin: [0.0, 0.0, 0.0],
519 exponent: 1.0,
520 l: [0, 0, 0],
521 coefficient: 1.0,
522 };
523
524 let b = GtoPrimitive {
525 origin: [1.0, 0.0, 0.0],
526 exponent: 1.0,
527 l: [0, 0, 0],
528 coefficient: 1.0,
529 };
530
531 let basis_a = [a];
532 let basis_b = [b];
533
534 let overlap = IntegralEngine::evaluate_overlap(&basis_a, &basis_b);
535 let val = overlap.get(0, 0);
536
537 let expected = (PI / 2.0).powf(1.5) * f64::exp(-0.5);
539 assert!(
540 (val - expected).abs() < 1e-8,
541 "Overlap mismatch: expected {}, got {}",
542 expected,
543 val
544 );
545 }
546}