1use std::collections::HashMap;
28
29use integral_math::am::{cart_components, n_cart};
30use integral_math::boys::boys_array;
31
32pub const MAX_L: usize = 6;
35
36const TBL: usize = MAX_L + 3;
39
40pub type Vec3 = [f64; 3];
42
43#[derive(Debug, Clone, Copy)]
45pub struct Prim {
46 pub exp: f64,
47 pub center: Vec3,
48 pub l: usize,
49}
50
51impl Prim {
52 #[must_use]
54 pub fn new(exp: f64, center: Vec3, l: usize) -> Self {
55 Prim { exp, center, l }
56 }
57}
58
59#[derive(Debug, Clone, Copy)]
61struct Pair {
62 p: f64,
63 one_over_2p: f64,
64 p_center: Vec3,
65 mu: f64,
66 ab2: f64,
67}
68
69fn make_pair(alpha: f64, a: Vec3, beta: f64, b: Vec3) -> Pair {
70 let p = alpha + beta;
71 let p_center = [
72 (alpha * a[0] + beta * b[0]) / p,
73 (alpha * a[1] + beta * b[1]) / p,
74 (alpha * a[2] + beta * b[2]) / p,
75 ];
76 let mu = alpha * beta / p;
77 let ab2 = (a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2);
78 Pair {
79 p,
80 one_over_2p: 0.5 / p,
81 p_center,
82 mu,
83 ab2,
84 }
85}
86
87fn overlap_1d(
99 pair: &Pair,
100 pa: f64,
101 pb: f64,
102 base: f64,
103 i_max: usize,
104 j_max: usize,
105) -> [[f64; TBL]; TBL] {
106 let mut s = [[0.0f64; TBL]; TBL];
107 let h = pair.one_over_2p;
108 s[0][0] = base;
109 for i in 1..=i_max {
111 let prev2 = if i >= 2 {
112 (i - 1) as f64 * s[i - 2][0]
113 } else {
114 0.0
115 };
116 s[i][0] = pa * s[i - 1][0] + h * prev2;
117 }
118 for j in 1..=j_max {
120 for i in 0..=i_max {
121 let from_i = if i >= 1 {
122 i as f64 * s[i - 1][j - 1]
123 } else {
124 0.0
125 };
126 let from_j = if j >= 2 {
127 (j - 1) as f64 * s[i][j - 2]
128 } else {
129 0.0
130 };
131 s[i][j] = pb * s[i][j - 1] + h * (from_i + from_j);
132 }
133 }
134 s
135}
136
137fn axis_base(pair: &Pair, d: f64) -> f64 {
140 (std::f64::consts::PI / pair.p).sqrt() * (-pair.mu * d * d).exp()
141}
142
143pub fn overlap_into(a: Prim, b: Prim, scale: f64, out: &mut [f64]) {
145 let Prim {
146 exp: alpha,
147 center: a,
148 l: la,
149 } = a;
150 let Prim {
151 exp: beta,
152 center: b,
153 l: lb,
154 } = b;
155 let pair = make_pair(alpha, a, beta, b);
156 let sx = overlap_1d(
157 &pair,
158 pair.p_center[0] - a[0],
159 pair.p_center[0] - b[0],
160 axis_base(&pair, a[0] - b[0]),
161 la,
162 lb,
163 );
164 let sy = overlap_1d(
165 &pair,
166 pair.p_center[1] - a[1],
167 pair.p_center[1] - b[1],
168 axis_base(&pair, a[1] - b[1]),
169 la,
170 lb,
171 );
172 let sz = overlap_1d(
173 &pair,
174 pair.p_center[2] - a[2],
175 pair.p_center[2] - b[2],
176 axis_base(&pair, a[2] - b[2]),
177 la,
178 lb,
179 );
180
181 let comps_a = cart_components(la);
182 let comps_b = cart_components(lb);
183 let nb = n_cart(lb);
184 for (ia, ca) in comps_a.iter().enumerate() {
185 for (ib, cb) in comps_b.iter().enumerate() {
186 let v = sx[ca[0]][cb[0]] * sy[ca[1]][cb[1]] * sz[ca[2]][cb[2]];
187 out[ia * nb + ib] += scale * v;
188 }
189 }
190}
191
192pub fn kinetic_into(a: Prim, b: Prim, scale: f64, out: &mut [f64]) {
198 let Prim {
199 exp: alpha,
200 center: a,
201 l: la,
202 } = a;
203 let Prim {
204 exp: beta,
205 center: b,
206 l: lb,
207 } = b;
208 let pair = make_pair(alpha, a, beta, b);
209 let s: [[[f64; TBL]; TBL]; 3] = [
211 overlap_1d(
212 &pair,
213 pair.p_center[0] - a[0],
214 pair.p_center[0] - b[0],
215 axis_base(&pair, a[0] - b[0]),
216 la,
217 lb + 2,
218 ),
219 overlap_1d(
220 &pair,
221 pair.p_center[1] - a[1],
222 pair.p_center[1] - b[1],
223 axis_base(&pair, a[1] - b[1]),
224 la,
225 lb + 2,
226 ),
227 overlap_1d(
228 &pair,
229 pair.p_center[2] - a[2],
230 pair.p_center[2] - b[2],
231 axis_base(&pair, a[2] - b[2]),
232 la,
233 lb + 2,
234 ),
235 ];
236
237 let kin = |axis: usize, i: usize, j: usize| -> f64 {
238 let t = &s[axis];
239 let term_plus = 2.0 * beta * beta * t[i][j + 2];
240 let term_mid = beta * (2 * j + 1) as f64 * t[i][j];
241 let term_minus = if j >= 2 {
242 0.5 * (j * (j - 1)) as f64 * t[i][j - 2]
243 } else {
244 0.0
245 };
246 term_mid - term_plus - term_minus
247 };
248
249 let comps_a = cart_components(la);
250 let comps_b = cart_components(lb);
251 let nb = n_cart(lb);
252 for (ia, ca) in comps_a.iter().enumerate() {
253 for (ib, cb) in comps_b.iter().enumerate() {
254 let sx = s[0][ca[0]][cb[0]];
255 let sy = s[1][ca[1]][cb[1]];
256 let sz = s[2][ca[2]][cb[2]];
257 let tx = kin(0, ca[0], cb[0]);
258 let ty = kin(1, ca[1], cb[1]);
259 let tz = kin(2, ca[2], cb[2]);
260 let v = tx * sy * sz + sx * ty * sz + sx * sy * tz;
261 out[ia * nb + ib] += scale * v;
262 }
263 }
264}
265
266pub fn dipole_into(
269 a: Prim,
270 b: Prim,
271 o: Vec3,
272 scale: f64,
273 out_x: &mut [f64],
274 out_y: &mut [f64],
275 out_z: &mut [f64],
276) {
277 let Prim {
278 exp: alpha,
279 center: a,
280 l: la,
281 } = a;
282 let Prim {
283 exp: beta,
284 center: b,
285 l: lb,
286 } = b;
287 let pair = make_pair(alpha, a, beta, b);
288 let mut m = [[[[0.0f64; 2]; TBL]; TBL]; 3];
290 for axis in 0..3 {
291 let pa = pair.p_center[axis] - a[axis];
292 let pb = pair.p_center[axis] - b[axis];
293 let po = pair.p_center[axis] - o[axis];
294 let base = axis_base(&pair, a[axis] - b[axis]);
295 let s = overlap_1d(&pair, pa, pb, base, la, lb);
296 let h = pair.one_over_2p;
297 for i in 0..=la {
298 for j in 0..=lb {
299 m[axis][i][j][0] = s[i][j];
300 let from_i = if i >= 1 { i as f64 * s[i - 1][j] } else { 0.0 };
302 let from_j = if j >= 1 { j as f64 * s[i][j - 1] } else { 0.0 };
303 m[axis][i][j][1] = po * s[i][j] + h * (from_i + from_j);
304 }
305 }
306 }
307
308 let comps_a = cart_components(la);
309 let comps_b = cart_components(lb);
310 let nb = n_cart(lb);
311 for (ia, ca) in comps_a.iter().enumerate() {
312 for (ib, cb) in comps_b.iter().enumerate() {
313 let s0 = [
314 m[0][ca[0]][cb[0]][0],
315 m[1][ca[1]][cb[1]][0],
316 m[2][ca[2]][cb[2]][0],
317 ];
318 let m1 = [
319 m[0][ca[0]][cb[0]][1],
320 m[1][ca[1]][cb[1]][1],
321 m[2][ca[2]][cb[2]][1],
322 ];
323 let idx = ia * nb + ib;
324 out_x[idx] += scale * m1[0] * s0[1] * s0[2];
325 out_y[idx] += scale * s0[0] * m1[1] * s0[2];
326 out_z[idx] += scale * s0[0] * s0[1] * m1[2];
327 }
328 }
329}
330
331pub fn nuclear_into(a: Prim, b: Prim, charges: &[(Vec3, f64)], scale: f64, out: &mut [f64]) {
338 let Prim {
339 exp: alpha,
340 center: a,
341 l: la,
342 } = a;
343 let Prim {
344 exp: beta,
345 center: b,
346 l: lb,
347 } = b;
348 let pair = make_pair(alpha, a, beta, b);
349 let p = pair.p;
350 let k_ab = (-pair.mu * pair.ab2).exp();
351 let total_l = la + lb;
352 let ab = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
353
354 let comps_a = cart_components(la);
355 let comps_b = cart_components(lb);
356 let nb = n_cart(lb);
357
358 for &(c, z) in charges {
359 let pc = [
361 pair.p_center[0] - c[0],
362 pair.p_center[1] - c[1],
363 pair.p_center[2] - c[2],
364 ];
365 let u = p * (pc[0] * pc[0] + pc[1] * pc[1] + pc[2] * pc[2]);
366 let mut fm = vec![0.0f64; total_l + 1];
367 boys_array(total_l, u, &mut fm);
368
369 let pref = 2.0 * std::f64::consts::PI / p * k_ab;
371 let pa = [
372 pair.p_center[0] - a[0],
373 pair.p_center[1] - a[1],
374 pair.p_center[2] - a[2],
375 ];
376
377 let theta = nuclear_vertical(total_l, p, pa, pc, &fm, pref);
379
380 let mut hrr_cache: HashMap<([u8; 3], [u8; 3]), f64> = HashMap::new();
382 for (ia, ca) in comps_a.iter().enumerate() {
383 for (ib, cb) in comps_b.iter().enumerate() {
384 let g = hrr(
385 [ca[0] as u8, ca[1] as u8, ca[2] as u8],
386 [cb[0] as u8, cb[1] as u8, cb[2] as u8],
387 ab,
388 &theta,
389 &mut hrr_cache,
390 );
391 out[ia * nb + ib] += scale * (-z) * g;
392 }
393 }
394 }
395}
396
397fn nuclear_vertical(
407 total_l: usize,
408 p: f64,
409 pa: Vec3,
410 pc: Vec3,
411 fm: &[f64],
412 pref: f64,
413) -> HashMap<[u8; 3], f64> {
414 let mut ladder: HashMap<[u8; 3], Vec<f64>> = HashMap::new();
416 let one_over_2p = 0.5 / p;
417
418 let base: Vec<f64> = (0..=total_l).map(|m| pref * fm[m]).collect();
420 ladder.insert([0, 0, 0], base);
421
422 for n in 1..=total_l {
423 for t in triples_with_sum(n) {
424 let i = if t[0] > 0 {
426 0
427 } else if t[1] > 0 {
428 1
429 } else {
430 2
431 };
432 let mut a = t;
433 a[i] -= 1;
434 let a_i = a[i] as f64; let mut aa = a; let have_second = aa[i] > 0;
438 if have_second {
439 aa[i] -= 1;
440 }
441
442 let m_count = total_l - n + 1;
443 let mut vals = vec![0.0f64; m_count];
444 let src = &ladder[&a];
445 let src2 = if have_second {
446 Some(ladder[&aa].clone())
447 } else {
448 None
449 };
450 for m in 0..m_count {
451 let mut v = pa[i] * src[m] - pc[i] * src[m + 1];
452 if let Some(ref s2) = src2 {
453 v += a_i * one_over_2p * (s2[m] - s2[m + 1]);
454 }
455 vals[m] = v;
456 }
457 ladder.insert(t, vals);
458 }
459 }
460
461 ladder.into_iter().map(|(k, v)| (k, v[0])).collect()
462}
463
464fn hrr(
467 a: [u8; 3],
468 b: [u8; 3],
469 ab: Vec3,
470 theta: &HashMap<[u8; 3], f64>,
471 cache: &mut HashMap<([u8; 3], [u8; 3]), f64>,
472) -> f64 {
473 if b[0] == 0 && b[1] == 0 && b[2] == 0 {
474 return theta[&a];
475 }
476 if let Some(&v) = cache.get(&(a, b)) {
477 return v;
478 }
479 let j = if b[0] > 0 {
481 0
482 } else if b[1] > 0 {
483 1
484 } else {
485 2
486 };
487 let mut b_low = b;
488 b_low[j] -= 1;
489 let mut a_up = a;
490 a_up[j] += 1;
491
492 let v = hrr(a_up, b_low, ab, theta, cache) + ab[j] * hrr(a, b_low, ab, theta, cache);
493 cache.insert((a, b), v);
494 v
495}
496
497fn triples_with_sum(n: usize) -> Vec<[u8; 3]> {
499 let mut out = Vec::new();
500 for lx in (0..=n).rev() {
501 for ly in (0..=(n - lx)).rev() {
502 out.push([lx as u8, ly as u8, (n - lx - ly) as u8]);
503 }
504 }
505 out
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use integral_math::norm::cart_norm;
512
513 const PI: f64 = std::f64::consts::PI;
514
515 fn norm_s(alpha: f64) -> f64 {
516 cart_norm(alpha, 0, 0, 0)
517 }
518
519 #[test]
520 fn s_s_overlap_matches_analytic() {
521 let (a, b) = (1.2, 0.8);
523 let ca = [0.0, 0.0, 0.0];
524 let cb = [0.0, 0.0, 1.3];
525 let mut out = [0.0];
526 let scale = norm_s(a) * norm_s(b);
527 overlap_into(Prim::new(a, ca, 0), Prim::new(b, cb, 0), scale, &mut out);
528
529 let p = a + b;
530 let mu = a * b / p;
531 let r2 = 1.3_f64.powi(2);
532 let raw = (PI / p).powf(1.5) * (-mu * r2).exp();
533 let expected = norm_s(a) * norm_s(b) * raw;
534 assert!(
535 (out[0] - expected).abs() < 1e-13,
536 "{} vs {}",
537 out[0],
538 expected
539 );
540 }
541
542 #[test]
543 fn same_center_normalized_s_overlap_is_one() {
544 let a = 0.9;
545 let c = [0.2, -0.4, 0.5];
546 let mut out = [0.0];
547 overlap_into(
548 Prim::new(a, c, 0),
549 Prim::new(a, c, 0),
550 norm_s(a) * norm_s(a),
551 &mut out,
552 );
553 assert!((out[0] - 1.0).abs() < 1e-13, "{}", out[0]);
554 }
555
556 #[test]
557 fn s_s_kinetic_matches_analytic() {
558 let (a, b) = (1.1, 0.7);
560 let ca = [0.0, 0.0, 0.0];
561 let cb = [0.0, 0.6, 0.0];
562 let mut t = [0.0];
563 kinetic_into(Prim::new(a, ca, 0), Prim::new(b, cb, 0), 1.0, &mut t);
564
565 let p = a + b;
566 let mu = a * b / p;
567 let r2 = 0.6_f64.powi(2);
568 let s_ss = (PI / p).powf(1.5) * (-mu * r2).exp();
569 let expected = mu * (3.0 - 2.0 * mu * r2) * s_ss;
570 assert!((t[0] - expected).abs() < 1e-13, "{} vs {}", t[0], expected);
571 }
572
573 #[test]
574 fn nuclear_s_s_matches_analytic() {
575 use integral_math::boys::boys;
577 let (a, b) = (1.3, 0.9);
578 let ca = [0.0, 0.0, 0.0];
579 let cb = [0.0, 0.0, 1.0];
580 let c = [0.0, 0.0, 0.4];
581 let mut v = [0.0];
582 nuclear_into(
584 Prim::new(a, ca, 0),
585 Prim::new(b, cb, 0),
586 &[(c, 1.0)],
587 1.0,
588 &mut v,
589 );
590
591 let p = a + b;
592 let mu = a * b / p;
593 let pcenter = (a * 0.0 + b * 1.0) / p;
594 let r2 = 1.0_f64.powi(2);
595 let pc2 = (pcenter - 0.4).powi(2);
596 let expected = -(2.0 * PI / p) * (-mu * r2).exp() * boys(0, p * pc2);
597 assert!((v[0] - expected).abs() < 1e-13, "{} vs {}", v[0], expected);
598 }
599
600 #[test]
605 fn blocks_are_transpose_symmetric() {
606 let pa = Prim::new(1.4, [0.1, 0.0, -0.2], 0);
607 let pb = Prim::new(0.6, [0.0, 0.5, 0.3], 0);
608 let charges = [([0.2, -0.1, 0.4], 3.0), ([-0.3, 0.2, 0.0], 1.0)];
609
610 for (la, lb) in [(1, 0), (1, 1), (2, 1), (2, 2)] {
611 let a = Prim { l: la, ..pa };
612 let b = Prim { l: lb, ..pb };
613 let (na, nb) = (n_cart(la), n_cart(lb));
614
615 for kind in 0..3 {
616 let mut fwd = vec![0.0; na * nb];
617 let mut rev = vec![0.0; nb * na];
618 match kind {
619 0 => {
620 overlap_into(a, b, 1.0, &mut fwd);
621 overlap_into(b, a, 1.0, &mut rev);
622 }
623 1 => {
624 kinetic_into(a, b, 1.0, &mut fwd);
625 kinetic_into(b, a, 1.0, &mut rev);
626 }
627 _ => {
628 nuclear_into(a, b, &charges, 1.0, &mut fwd);
629 nuclear_into(b, a, &charges, 1.0, &mut rev);
630 }
631 }
632 for i in 0..na {
633 for j in 0..nb {
634 let f = fwd[i * nb + j];
635 let r = rev[j * na + i];
636 assert!(
637 (f - r).abs() < 1e-12 * f.abs().max(1.0),
638 "kind={kind} (la={la},lb={lb}) [{i},{j}]: {f} vs {r}"
639 );
640 }
641 }
642 }
643 }
644 }
645
646 #[test]
647 fn dipole_same_center_s_is_position_times_overlap() {
648 let a = 1.0;
650 let r = [0.3, -0.2, 0.7];
651 let o = [0.1, 0.1, 0.1];
652 let scale = norm_s(a) * norm_s(a);
653 let (mut dx, mut dy, mut dz) = ([0.0], [0.0], [0.0]);
654 dipole_into(
655 Prim::new(a, r, 0),
656 Prim::new(a, r, 0),
657 o,
658 scale,
659 &mut dx,
660 &mut dy,
661 &mut dz,
662 );
663 assert!((dx[0] - (r[0] - o[0])).abs() < 1e-13, "{}", dx[0]);
665 assert!((dy[0] - (r[1] - o[1])).abs() < 1e-13, "{}", dy[0]);
666 assert!((dz[0] - (r[2] - o[2])).abs() < 1e-13, "{}", dz[0]);
667 }
668}