1use integral_math::am::{cart_components, n_cart};
44use integral_math::rys::{rys_roots_weights, MAX_RYS_ROOTS};
45
46use crate::os::{Prim, Vec3, MAX_L};
47
48const L1: usize = MAX_L + 1;
50const LT1: usize = 2 * MAX_L + 1;
52
53type Axis4 = [[[[f64; L1]; L1]; L1]; L1];
55
56pub fn coulomb_into(a: Prim, b: Prim, c: Prim, d: Prim, scale: f64, out: &mut [f64]) {
66 let (la, lb, lc, ld) = (a.l, b.l, c.l, d.l);
67 debug_assert!(
68 la <= MAX_L && lb <= MAX_L && lc <= MAX_L && ld <= MAX_L,
69 "angular momentum exceeds MAX_L"
70 );
71
72 let (na, nb, nc, nd) = (n_cart(la), n_cart(lb), n_cart(lc), n_cart(ld));
73 debug_assert!(out.len() >= na * nb * nc * nd, "ERI output block too short");
74
75 let p = a.exp + b.exp;
77 let q = c.exp + d.exp;
78 let p_ctr = combine(a.exp, a.center, b.exp, b.center, p);
79 let q_ctr = combine(c.exp, c.center, d.exp, d.center, q);
80 let kab = (-(a.exp * b.exp / p) * dist2(a.center, b.center)).exp();
81 let kcd = (-(c.exp * d.exp / q) * dist2(c.center, d.center)).exp();
82
83 let pq = p + q;
84 let rho = p * q / pq;
85 let pq_vec = sub(p_ctr, q_ctr);
86 let t = rho * norm2(pq_vec);
87
88 use std::f64::consts::PI;
91 let pref = 2.0 * PI * PI * PI.sqrt() / (p * q * pq.sqrt()) * kab * kcd;
92
93 let l_total = la + lb + lc + ld;
94 let nroots = l_total / 2 + 1;
95 let mut roots = [0.0f64; MAX_RYS_ROOTS];
96 let mut wts = [0.0f64; MAX_RYS_ROOTS];
97 rys_roots_weights(nroots, t, &mut roots, &mut wts);
98
99 let pa = sub(p_ctr, a.center); let qc = sub(q_ctr, c.center); let ab = sub(a.center, b.center); let cd = sub(c.center, d.center); let comps_a = cart_components(la);
106 let comps_b = cart_components(lb);
107 let comps_c = cart_components(lc);
108 let comps_d = cart_components(ld);
109
110 let mut ix: Axis4 = zeroed();
111 let mut iy: Axis4 = zeroed();
112 let mut iz: Axis4 = zeroed();
113
114 for r in 0..nroots {
115 let x = roots[r];
116 let b00 = x / (2.0 * pq);
118 let b10 = (1.0 - x * q / pq) / (2.0 * p);
119 let b01 = (1.0 - x * p / pq) / (2.0 * q);
120 let cq = x * q / pq; let cp = x * p / pq; for (axis, out4) in [&mut ix, &mut iy, &mut iz].into_iter().enumerate() {
124 let c00 = pa[axis] - cq * pq_vec[axis];
125 let cp0 = qc[axis] + cp * pq_vec[axis];
126 build_axis(
127 la, lb, lc, ld, c00, cp0, b10, b01, b00, ab[axis], cd[axis], out4,
128 );
129 }
130
131 let pw = scale * pref * wts[r];
132 for (ia, ca) in comps_a.iter().enumerate() {
133 for (ib, cb) in comps_b.iter().enumerate() {
134 for (ic, cc) in comps_c.iter().enumerate() {
135 for (id, cd_) in comps_d.iter().enumerate() {
136 let v = ix[ca[0]][cb[0]][cc[0]][cd_[0]]
137 * iy[ca[1]][cb[1]][cc[1]][cd_[1]]
138 * iz[ca[2]][cb[2]][cc[2]][cd_[2]];
139 out[((ia * nb + ib) * nc + ic) * nd + id] += pw * v;
140 }
141 }
142 }
143 }
144 }
145}
146
147#[allow(clippy::too_many_arguments, clippy::needless_range_loop)]
153fn build_axis(
154 la: usize,
155 lb: usize,
156 lc: usize,
157 ld: usize,
158 c00: f64,
159 cp0: f64,
160 b10: f64,
161 b01: f64,
162 b00: f64,
163 ab: f64,
164 cd: f64,
165 out4: &mut Axis4,
166) {
167 let nbra = la + lb; let nket = lc + ld; let mut g = [[0.0f64; LT1]; LT1];
172 g[0][0] = 1.0;
173 if nbra >= 1 {
174 g[1][0] = c00;
175 }
176 for n in 1..nbra {
177 g[n + 1][0] = c00 * g[n][0] + (n as f64) * b10 * g[n - 1][0];
178 }
179 for m in 0..nket {
180 for n in 0..=nbra {
181 let mut term = cp0 * g[n][m];
182 if m >= 1 {
183 term += (m as f64) * b01 * g[n][m - 1];
184 }
185 if n >= 1 {
186 term += (n as f64) * b00 * g[n - 1][m];
187 }
188 g[n][m + 1] = term;
189 }
190 }
191
192 let mut h = [[[0.0f64; LT1]; L1]; LT1];
195 for i in 0..=nbra {
196 for m in 0..=nket {
197 h[i][0][m] = g[i][m];
198 }
199 }
200 for j in 1..=lb {
201 for i in 0..=(nbra - j) {
202 for m in 0..=nket {
203 h[i][j][m] = h[i + 1][j - 1][m] + ab * h[i][j - 1][m];
204 }
205 }
206 }
207
208 for i in 0..=la {
210 for j in 0..=lb {
211 let mut t = [[0.0f64; L1]; LT1];
212 for k in 0..=nket {
213 t[k][0] = h[i][j][k];
214 }
215 for l in 1..=ld {
216 for k in 0..=(nket - l) {
217 t[k][l] = t[k + 1][l - 1] + cd * t[k][l - 1];
218 }
219 }
220 for k in 0..=lc {
221 for l in 0..=ld {
222 out4[i][j][k][l] = t[k][l];
223 }
224 }
225 }
226 }
227}
228
229#[inline]
230fn zeroed() -> Axis4 {
231 [[[[0.0; L1]; L1]; L1]; L1]
232}
233
234#[inline]
235fn combine(a: f64, ca: Vec3, b: f64, cb: Vec3, p: f64) -> Vec3 {
236 [
237 (a * ca[0] + b * cb[0]) / p,
238 (a * ca[1] + b * cb[1]) / p,
239 (a * ca[2] + b * cb[2]) / p,
240 ]
241}
242
243#[inline]
244fn sub(u: Vec3, v: Vec3) -> Vec3 {
245 [u[0] - v[0], u[1] - v[1], u[2] - v[2]]
246}
247
248#[inline]
249fn dist2(u: Vec3, v: Vec3) -> f64 {
250 norm2(sub(u, v))
251}
252
253#[inline]
254fn norm2(u: Vec3) -> f64 {
255 u[0] * u[0] + u[1] * u[1] + u[2] * u[2]
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
265 fn ssss_matches_closed_form() {
266 let a = Prim::new(0.8, [0.0, 0.0, 0.0], 0);
267 let b = Prim::new(1.3, [0.0, 0.0, 0.7], 0);
268 let c = Prim::new(0.5, [0.4, 0.0, 0.0], 0);
269 let d = Prim::new(1.1, [0.0, 0.6, 0.2], 0);
270 let mut out = [0.0; 1];
271 coulomb_into(a, b, c, d, 1.0, &mut out);
272
273 let p = a.exp + b.exp;
275 let q = c.exp + d.exp;
276 let pc = combine(a.exp, a.center, b.exp, b.center, p);
277 let qc = combine(c.exp, c.center, d.exp, d.center, q);
278 let kab = (-(a.exp * b.exp / p) * dist2(a.center, b.center)).exp();
279 let kcd = (-(c.exp * d.exp / q) * dist2(c.center, d.center)).exp();
280 let rho = p * q / (p + q);
281 let t = rho * dist2(pc, qc);
282 let mut fm = [0.0; 1];
283 integral_math::boys::boys_array(0, t, &mut fm);
284 use std::f64::consts::PI;
285 let expect = 2.0 * PI * PI * PI.sqrt() / (p * q * (p + q).sqrt()) * kab * kcd * fm[0];
286 assert!(
287 (out[0] - expect).abs() < 1e-14 * expect.abs(),
288 "ssss {} vs {}",
289 out[0],
290 expect
291 );
292 }
293
294 #[test]
296 fn scale_and_accumulate() {
297 let a = Prim::new(0.8, [0.0, 0.0, 0.0], 0);
298 let b = Prim::new(1.3, [0.0, 0.0, 0.7], 0);
299 let c = Prim::new(0.5, [0.4, 0.0, 0.0], 0);
300 let d = Prim::new(1.1, [0.0, 0.6, 0.2], 0);
301 let mut one = [0.0; 1];
302 coulomb_into(a, b, c, d, 1.0, &mut one);
303 let mut acc = [0.0; 1];
304 coulomb_into(a, b, c, d, 2.0, &mut acc);
305 coulomb_into(a, b, c, d, 3.0, &mut acc);
306 assert!((acc[0] - 5.0 * one[0]).abs() < 1e-12 * one[0].abs());
307 }
308
309 #[test]
313 fn bra_ket_exchange_primitive() {
314 let a = Prim::new(0.9, [0.1, 0.0, 0.0], 1); let b = Prim::new(0.7, [0.0, 0.2, 0.0], 0); let c = Prim::new(1.2, [0.0, 0.0, 0.3], 0); let d = Prim::new(0.6, [0.2, 0.1, 0.0], 0); let mut abcd = [0.0; 3]; coulomb_into(a, b, c, d, 1.0, &mut abcd);
320 let mut cdab = [0.0; 3];
322 coulomb_into(c, d, a, b, 1.0, &mut cdab);
323 for i in 0..3 {
324 assert!(
325 (abcd[i] - cdab[i]).abs() < 1e-13 * abcd[i].abs().max(1e-300),
326 "bra-ket exchange mismatch at {i}: {} vs {}",
327 abcd[i],
328 cdab[i]
329 );
330 }
331 }
332}