integral/df.rs
1//! Density-fitting (RI) integrals: 3-center `(μν|P)` and 2-center `(P|Q)`
2//! Coulomb integrals over a main basis plus an auxiliary fitting basis.
3//!
4//! Both reduce to the existing 4-center Coulomb kernels by attaching a **unit
5//! `s` dummy** to the auxiliary index: a zero-exponent, coefficient-1 `s`
6//! primitive is the constant function `1`, so
7//!
8//! ```text
9//! (μν|P) = (μν|P·1ₛ) (P|Q) = (P·1ₛ|Q·1ₛ)
10//! ```
11//!
12//! *exactly* (no limit is taken — the Gaussian-product machinery is evaluated
13//! at `α = 0`, where every pair quantity is finite: the pair exponent is
14//! `ζ = α_P + 0 = α_P`, the prefactor `K = exp(0) = 1`, and the pair center is
15//! the aux center itself). The dummy is exempt from primitive normalization
16//! (see [`Shell::primitive_coeff`]). Both ERI engines (OS/HGP and Rys) divide
17//! only by *pair* exponent sums, never by an individual primitive exponent, so
18//! the zero-exponent index is safe in either.
19//!
20//! The public surface mirrors the 4-center family: dense + `_with(Engine)`
21//! variants, kind-aware spherical sizes, row-major blocks with the **last
22//! index fastest**, an aux-side Schwarz bound, and a parallel-ready
23//! [`Eri3cBuilder`] with the same partition/fill contract as
24//! [`crate::EriBuilder`].
25
26use crate::integrals::{
27 canonical_shell_pairs, effective_coeffs, quartet_into_scratch, quartet_into_scratch_erf,
28 Engine, EriKernel, QuartetScratch,
29};
30use crate::shell::{Basis, Shell};
31use crate::spherical::shell_transform;
32
33/// The unit `s` dummy at `center`: one zero-exponent primitive, coefficient 1,
34/// i.e. the constant function `1` (normalization-exempt; see module docs).
35/// Shared with the density-fitting gradient builders in `grad.rs`.
36pub(crate) fn unit_s(center: [f64; 3]) -> Shell {
37 Shell::new(0, center, vec![0.0], vec![1.0]).expect("unit s dummy shell is always valid")
38}
39
40impl Basis {
41 /// 2-center Coulomb metric `(P|Q) = ∫∫ φ_P(1) r₁₂⁻¹ φ_Q(2) d1 d2` over
42 /// `self` as the **auxiliary** basis.
43 ///
44 /// Row-major `[naux, naux]` with `naux = self.nao()`, kind-aware (spherical
45 /// shells contribute their `2l+1` components). The matrix is **exactly
46 /// symmetric**: each canonical shell pair is evaluated once and mirrored,
47 /// so `(P|Q)` and `(Q|P)` are the same `f64`.
48 #[must_use]
49 pub fn eri_2c(&self) -> Vec<f64> {
50 self.eri_2c_with(Engine::Auto)
51 }
52
53 /// Like [`Basis::eri_2c`] but forces a specific [`Engine`] (or
54 /// [`Engine::Auto`]). Both engines produce the same metric to tolerance.
55 #[must_use]
56 pub fn eri_2c_with(&self, engine: Engine) -> Vec<f64> {
57 let naux = self.nao();
58 let offs = self.offsets();
59 let shells = self.shells();
60 let eff: Vec<Vec<f64>> = shells.iter().map(effective_coeffs).collect();
61 let c2s: Vec<Option<Vec<f64>>> = shells.iter().map(shell_transform).collect();
62 let dummies: Vec<Shell> = shells.iter().map(|s| unit_s(s.center())).collect();
63 let unit_eff = [1.0];
64 let mut out = vec![0.0; naux * naux];
65 let mut scratch = QuartetScratch::default();
66 for (p, sp) in shells.iter().enumerate() {
67 for (q, sq) in shells.iter().enumerate().take(p + 1) {
68 let len = quartet_into_scratch(
69 &mut scratch,
70 engine,
71 [sp, &dummies[p], sq, &dummies[q]],
72 [&eff[p], &unit_eff, &eff[q], &unit_eff],
73 [c2s[p].as_deref(), None, c2s[q].as_deref(), None],
74 );
75 let (np, nq) = (sp.n_func(), sq.n_func());
76 debug_assert_eq!(len, np * nq);
77 for a in 0..np {
78 for b in 0..nq {
79 let v = scratch.block[a * nq + b];
80 out[(offs[p] + a) * naux + offs[q] + b] = v;
81 out[(offs[q] + b) * naux + offs[p] + a] = v;
82 }
83 }
84 }
85 }
86 out
87 }
88
89 /// 2-center metric over the chosen [`EriKernel`] — layout identical to
90 /// [`Basis::eri_2c`] (row-major `[naux, naux]`, exactly symmetric).
91 ///
92 /// [`EriKernel::Coulomb`] routes to [`Basis::eri_2c`] itself (bit-identical
93 /// output); [`EriKernel::Erf`] evaluates `(P| erf(ω·r₁₂)/r₁₂ |Q)` via the
94 /// same zero-exponent unit-`s` dummy construction, on the Rys engine (see
95 /// [`Basis::eri_kernel`]).
96 ///
97 /// # Panics
98 /// Panics if `k` is `Erf { omega }` with `ω ≤ 0`, NaN, or infinite.
99 #[must_use]
100 pub fn eri_2c_kernel(&self, k: EriKernel) -> Vec<f64> {
101 let omega = match k {
102 EriKernel::Coulomb => return self.eri_2c(),
103 EriKernel::Erf { omega } => {
104 crate::integrals::check_erf_omega(omega);
105 omega
106 }
107 };
108 let naux = self.nao();
109 let offs = self.offsets();
110 let shells = self.shells();
111 let eff: Vec<Vec<f64>> = shells.iter().map(effective_coeffs).collect();
112 let c2s: Vec<Option<Vec<f64>>> = shells.iter().map(shell_transform).collect();
113 let dummies: Vec<Shell> = shells.iter().map(|s| unit_s(s.center())).collect();
114 let unit_eff = [1.0];
115 let mut out = vec![0.0; naux * naux];
116 let mut scratch = QuartetScratch::default();
117 for (p, sp) in shells.iter().enumerate() {
118 for (q, sq) in shells.iter().enumerate().take(p + 1) {
119 let len = quartet_into_scratch_erf(
120 &mut scratch,
121 [sp, &dummies[p], sq, &dummies[q]],
122 [&eff[p], &unit_eff, &eff[q], &unit_eff],
123 [c2s[p].as_deref(), None, c2s[q].as_deref(), None],
124 omega,
125 );
126 let (np, nq) = (sp.n_func(), sq.n_func());
127 debug_assert_eq!(len, np * nq);
128 for a in 0..np {
129 for b in 0..nq {
130 let v = scratch.block[a * nq + b];
131 out[(offs[p] + a) * naux + offs[q] + b] = v;
132 out[(offs[q] + b) * naux + offs[p] + a] = v;
133 }
134 }
135 }
136 }
137 out
138 }
139
140 /// One 3-center Coulomb shell block `(ij|P)`: shells `ish, jsh` over `self`
141 /// (the main basis), shell `psh` over `aux` (the auxiliary basis).
142 ///
143 /// Row-major `[n_i, n_j, n_p]` with **`P` fastest-varying** (so a
144 /// per-shell-pair GEMM against the metric is contiguous), kind-aware like
145 /// [`Basis::eri_block`]: `n_x` is the shell's `n_func` (`n_cart` for
146 /// Cartesian, `2l+1` for spherical) in the usual component order.
147 #[must_use]
148 pub fn eri_3c_block(&self, aux: &Basis, ish: usize, jsh: usize, psh: usize) -> Vec<f64> {
149 self.eri_3c_block_with(Engine::Auto, aux, ish, jsh, psh)
150 }
151
152 /// Like [`Basis::eri_3c_block`] but forces a specific [`Engine`] (or
153 /// [`Engine::Auto`]). Both engines produce the same block to tolerance.
154 #[must_use]
155 pub fn eri_3c_block_with(
156 &self,
157 engine: Engine,
158 aux: &Basis,
159 ish: usize,
160 jsh: usize,
161 psh: usize,
162 ) -> Vec<f64> {
163 let s = self.shells();
164 let (si, sj) = (&s[ish], &s[jsh]);
165 let sp = &aux.shells()[psh];
166 let dummy = unit_s(sp.center());
167 let (mi, mj, mp) = (
168 shell_transform(si),
169 shell_transform(sj),
170 shell_transform(sp),
171 );
172 let mut scratch = QuartetScratch::default();
173 let len = quartet_into_scratch(
174 &mut scratch,
175 engine,
176 [si, sj, sp, &dummy],
177 [
178 &effective_coeffs(si),
179 &effective_coeffs(sj),
180 &effective_coeffs(sp),
181 &[1.0],
182 ],
183 [mi.as_deref(), mj.as_deref(), mp.as_deref(), None],
184 );
185 scratch.block[..len].to_vec()
186 }
187
188 /// One 3-center shell block over the chosen [`EriKernel`] — layout identical
189 /// to [`Basis::eri_3c_block`] (row-major `[n_i, n_j, n_p]`, `P` fastest).
190 ///
191 /// [`EriKernel::Coulomb`] routes to [`Basis::eri_3c_block`] itself
192 /// (bit-identical output); [`EriKernel::Erf`] evaluates
193 /// `(ij| erf(ω·r₁₂)/r₁₂ |P)` via the same zero-exponent unit-`s` dummy
194 /// construction, on the Rys engine (see [`Basis::eri_kernel`]). The Coulomb
195 /// Schwarz factors ([`Basis::schwarz_bounds`] / [`Basis::schwarz_aux_bounds`])
196 /// remain valid upper bounds for the attenuated blocks (`erf(ωr)/r ≤ 1/r`).
197 ///
198 /// # Panics
199 /// Panics if `k` is `Erf { omega }` with `ω ≤ 0`, NaN, or infinite.
200 #[must_use]
201 pub fn eri_3c_block_kernel(
202 &self,
203 aux: &Basis,
204 ish: usize,
205 jsh: usize,
206 psh: usize,
207 k: EriKernel,
208 ) -> Vec<f64> {
209 let omega = match k {
210 EriKernel::Coulomb => return self.eri_3c_block(aux, ish, jsh, psh),
211 EriKernel::Erf { omega } => {
212 crate::integrals::check_erf_omega(omega);
213 omega
214 }
215 };
216 let s = self.shells();
217 let (si, sj) = (&s[ish], &s[jsh]);
218 let sp = &aux.shells()[psh];
219 let dummy = unit_s(sp.center());
220 let (mi, mj, mp) = (
221 shell_transform(si),
222 shell_transform(sj),
223 shell_transform(sp),
224 );
225 let mut scratch = QuartetScratch::default();
226 let len = quartet_into_scratch_erf(
227 &mut scratch,
228 [si, sj, sp, &dummy],
229 [
230 &effective_coeffs(si),
231 &effective_coeffs(sj),
232 &effective_coeffs(sp),
233 &[1.0],
234 ],
235 [mi.as_deref(), mj.as_deref(), mp.as_deref(), None],
236 omega,
237 );
238 scratch.block[..len].to_vec()
239 }
240
241 /// Auxiliary-side Schwarz factors over `self` as the aux basis, one per
242 /// shell: `QP[p] = sqrt(max_{μ∈p} (μ|μ))` with `(μ|μ)` the diagonal of the
243 /// 2-center block `(p|p)`.
244 ///
245 /// Together with the main-basis [`Basis::schwarz_bounds`] this bounds every
246 /// 3-center integral: `|(μν|P)| ≤ Q[i,j] · QP[p]` for `μν` in shell pair
247 /// `(i, j)` and `P` in aux shell `p` (Cauchy–Schwarz in the Coulomb inner
248 /// product). Kind-aware, like the 4-center bounds.
249 #[must_use]
250 pub fn schwarz_aux_bounds(&self) -> Vec<f64> {
251 self.schwarz_aux_bounds_with(Engine::Auto)
252 }
253
254 /// Like [`Basis::schwarz_aux_bounds`] but with a forced [`Engine`]. The
255 /// bound is engine-independent to tolerance.
256 #[must_use]
257 pub fn schwarz_aux_bounds_with(&self, engine: Engine) -> Vec<f64> {
258 let shells = self.shells();
259 let eff: Vec<Vec<f64>> = shells.iter().map(effective_coeffs).collect();
260 let c2s: Vec<Option<Vec<f64>>> = shells.iter().map(shell_transform).collect();
261 let unit_eff = [1.0];
262 let mut scratch = QuartetScratch::default();
263 let mut bounds = Vec::with_capacity(shells.len());
264 for (p, sp) in shells.iter().enumerate() {
265 let dummy = unit_s(sp.center());
266 let len = quartet_into_scratch(
267 &mut scratch,
268 engine,
269 [sp, &dummy, sp, &dummy],
270 [&eff[p], &unit_eff, &eff[p], &unit_eff],
271 [c2s[p].as_deref(), None, c2s[p].as_deref(), None],
272 );
273 let np = sp.n_func();
274 debug_assert_eq!(len, np * np);
275 let mut mx = 0.0_f64;
276 for mu in 0..np {
277 mx = mx.max(scratch.block[mu * np + mu].abs());
278 }
279 bounds.push(mx.sqrt());
280 }
281 bounds
282 }
283
284 /// Create a parallel-ready [`Eri3cBuilder`] filling `(ij|P)` with `ij` over
285 /// `self` (the main basis) and `P` over `aux`, with the default
286 /// [`Engine::Auto`] dispatch. Equivalent to [`Eri3cBuilder::new`].
287 #[must_use]
288 pub fn eri_3c_builder<'a>(&'a self, aux: &'a Basis) -> Eri3cBuilder<'a> {
289 Eri3cBuilder::new(self, aux)
290 }
291}
292
293/// A reusable plan for assembling the dense 3-center tensor `(ij|P)` —
294/// row-major `[nao, nao, naux]`, `P` fastest — in parallel over canonical
295/// **bra shell-pairs** of the main basis, with no in-crate threading runtime.
296///
297/// Same contract as [`crate::EriBuilder`]: [`Eri3cBuilder::partition`] slices
298/// the caller's buffer into one `naux` row slab per `(μ, ν)` AO pair via
299/// `chunks_exact_mut` (provably disjoint `&mut` views, no `unsafe`), and hands
300/// each canonical bra-pair `(i ≥ j)` exactly the rows it owns — the `(i, j)`
301/// band and, when `i ≠ j`, the `(j, i)` band. A driver (e.g. rayon at the call
302/// site) fills the returned tasks concurrently with [`Eri3cBuilder::fill`];
303/// chemx's existing LPT dispatch over [`crate::EriBuilder`] tasks carries over
304/// unchanged.
305///
306/// Unlike the 4-center builder there is no bra↔ket exchange to trade away: the
307/// only symmetry is the bra swap `(μν|P) = (νμ|P)`, so each canonical bra-pair
308/// evaluates every aux shell once and writes both orderings.
309///
310/// # Example — serial
311/// ```
312/// use integral::{Basis, Shell};
313/// let main = Basis::new(vec![
314/// Shell::new(0, [0.0, 0.0, 0.0], vec![0.8], vec![1.0]).unwrap(),
315/// Shell::new(1, [0.1, 0.0, 0.0], vec![0.6], vec![1.0]).unwrap(),
316/// ]);
317/// let aux = Basis::new(vec![
318/// Shell::new(0, [0.0, 0.0, 0.0], vec![1.2], vec![1.0]).unwrap(),
319/// ]);
320/// let b = main.eri_3c_builder(&aux);
321/// let tensor = b.build();
322/// assert_eq!(tensor.len(), main.nao() * main.nao() * aux.nao());
323/// ```
324#[derive(Debug)]
325pub struct Eri3cBuilder<'b> {
326 main: &'b [Shell],
327 aux: &'b [Shell],
328 engine: Engine,
329 /// Output-AO offset of each main-basis shell (function-space).
330 offs: Vec<usize>,
331 /// `n_func` of each main-basis shell.
332 nfunc: Vec<usize>,
333 /// Total main-basis output AOs.
334 nao: usize,
335 /// Aux-shell AO offsets and total aux AOs.
336 aux_offs: Vec<usize>,
337 naux: usize,
338 /// Effective contraction coefficients per shell (`d_i · N(α_i, l)`).
339 eff: Vec<Vec<f64>>,
340 aux_eff: Vec<Vec<f64>>,
341 /// Cached `c2s` transform per shell (`None` = Cartesian).
342 c2s: Vec<Option<Vec<f64>>>,
343 aux_c2s: Vec<Option<Vec<f64>>>,
344 /// One unit-`s` dummy per aux shell, at that shell's center.
345 dummies: Vec<Shell>,
346 /// Canonical main-basis shell pairs `(i ≥ j)`, the parallel grain.
347 pairs: Vec<(usize, usize)>,
348}
349
350impl<'b> Eri3cBuilder<'b> {
351 /// Build a plan for `(ij|P)` over `main` × `aux` with the default
352 /// [`Engine::Auto`] dispatch.
353 #[must_use]
354 pub fn new(main: &'b Basis, aux: &'b Basis) -> Self {
355 Self::with_engine(main, aux, Engine::Auto)
356 }
357
358 /// Build a plan that forces a specific [`Engine`] (or [`Engine::Auto`]).
359 #[must_use]
360 pub fn with_engine(main: &'b Basis, aux: &'b Basis, engine: Engine) -> Self {
361 let shells = main.shells();
362 let aux_shells = aux.shells();
363 Eri3cBuilder {
364 main: shells,
365 aux: aux_shells,
366 engine,
367 offs: main.offsets(),
368 nfunc: shells.iter().map(Shell::n_func).collect(),
369 nao: main.nao(),
370 aux_offs: aux.offsets(),
371 naux: aux.nao(),
372 eff: shells.iter().map(effective_coeffs).collect(),
373 aux_eff: aux_shells.iter().map(effective_coeffs).collect(),
374 c2s: shells.iter().map(shell_transform).collect(),
375 aux_c2s: aux_shells.iter().map(shell_transform).collect(),
376 dummies: aux_shells.iter().map(|s| unit_s(s.center())).collect(),
377 pairs: canonical_shell_pairs(shells.len()),
378 }
379 }
380
381 /// The canonical bra shell-pairs `(i, j)` with `i ≥ j`, in build order —
382 /// the **external parallel grain**, aligned index-for-index with the tasks
383 /// returned by [`Eri3cBuilder::partition`].
384 #[must_use]
385 pub fn bra_pairs(&self) -> &[(usize, usize)] {
386 &self.pairs
387 }
388
389 /// Length of the dense output buffer, `nao² · naux`. Allocate
390 /// `vec![0.0; output_len()]` before [`Eri3cBuilder::partition`].
391 #[must_use]
392 pub fn output_len(&self) -> usize {
393 self.nao * self.nao * self.naux
394 }
395
396 /// Partition a freshly-zeroed `nao²·naux` output buffer into one
397 /// [`Bra3cFill`] task per canonical bra-pair (aligned with
398 /// [`Eri3cBuilder::bra_pairs`]). Each task borrows **only** the `(μ, ν)`
399 /// row slabs it owns; the borrows are mutually disjoint, so the tasks may
400 /// be filled concurrently into the same buffer.
401 ///
402 /// # Panics
403 /// If `out.len() != output_len()`.
404 #[must_use]
405 pub fn partition<'o>(&self, out: &'o mut [f64]) -> Vec<Bra3cFill<'o>> {
406 let nao = self.nao;
407 assert_eq!(
408 out.len(),
409 nao * nao * self.naux,
410 "3c output buffer must be nao²·naux = {} elements",
411 nao * nao * self.naux
412 );
413
414 // One mutable slab of naux elements per (μ, ν) AO row, row = μ·nao + ν.
415 let mut slabs: Vec<Option<&'o mut [f64]>> =
416 out.chunks_exact_mut(self.naux).map(Some).collect();
417 debug_assert_eq!(slabs.len(), nao * nao);
418
419 let mut tasks = Vec::with_capacity(self.pairs.len());
420 for &(i, j) in &self.pairs {
421 let (ni, nj) = (self.nfunc[i], self.nfunc[j]);
422 let (oi, oj) = (self.offs[i], self.offs[j]);
423
424 // (i, j) band: rows (μ∈i, ν∈j), row-major (a, b).
425 let mut ij_band = Vec::with_capacity(ni * nj);
426 for a in 0..ni {
427 for b in 0..nj {
428 ij_band.push(claim_row(&mut slabs, (oi + a) * nao + (oj + b)));
429 }
430 }
431
432 // (j, i) band: rows (μ∈j, ν∈i), row-major (b, a). Empty when i == j.
433 let mut ji_band = Vec::new();
434 if i != j {
435 ji_band.reserve(nj * ni);
436 for b in 0..nj {
437 for a in 0..ni {
438 ji_band.push(claim_row(&mut slabs, (oj + b) * nao + (oi + a)));
439 }
440 }
441 }
442
443 tasks.push(Bra3cFill {
444 bra: (i, j),
445 ij_band,
446 ji_band,
447 });
448 }
449
450 debug_assert!(
451 slabs.iter().all(Option::is_none),
452 "partition left {} output rows unclaimed",
453 slabs.iter().filter(|s| s.is_some()).count()
454 );
455
456 tasks
457 }
458
459 /// Fill one bra-pair's owned rows: evaluate `(ij|P)` once per aux shell and
460 /// write the block into the `(i, j)` band and (when `i ≠ j`) the bra-swapped
461 /// `(j, i)` band. Writes touch only `task`'s rows, so this may run
462 /// concurrently with [`Eri3cBuilder::fill`] on every *other* task.
463 pub fn fill(&self, task: &mut Bra3cFill<'_>) {
464 self.fill_filtered(task, |_| true);
465 }
466
467 /// Like [`Eri3cBuilder::fill`] but evaluates only the aux shells `p` for
468 /// which `keep(p)` is true, skipping the rest entirely — the hook for
469 /// per-aux-shell Schwarz screening (`Q[ij]·QP[p] < τ`) *inside* a bra-pair
470 /// task. Skipped shells' output slots are left untouched, i.e. zero in the
471 /// freshly-zeroed buffer [`Eri3cBuilder::partition`] requires.
472 ///
473 /// With `keep = |_| true` this is the identical code path to
474 /// [`Eri3cBuilder::fill`] (bitwise-equal output).
475 pub fn fill_filtered(&self, task: &mut Bra3cFill<'_>, keep: impl Fn(usize) -> bool) {
476 let (i, j) = task.bra;
477 let (si, sj) = (&self.main[i], &self.main[j]);
478 let (ni, nj) = (self.nfunc[i], self.nfunc[j]);
479 let mut scratch = QuartetScratch::default();
480 for (p, sp) in self.aux.iter().enumerate() {
481 if !keep(p) {
482 continue;
483 }
484 let len = quartet_into_scratch(
485 &mut scratch,
486 self.engine,
487 [si, sj, sp, &self.dummies[p]],
488 [&self.eff[i], &self.eff[j], &self.aux_eff[p], &[1.0]],
489 [
490 self.c2s[i].as_deref(),
491 self.c2s[j].as_deref(),
492 self.aux_c2s[p].as_deref(),
493 None,
494 ],
495 );
496 let np = sp.n_func();
497 debug_assert_eq!(len, ni * nj * np);
498 let op = self.aux_offs[p];
499 let block = &scratch.block[..len];
500 for a in 0..ni {
501 for b in 0..nj {
502 let row = &block[(a * nj + b) * np..(a * nj + b + 1) * np];
503 task.ij_band[a * nj + b][op..op + np].copy_from_slice(row);
504 if i != j {
505 task.ji_band[b * ni + a][op..op + np].copy_from_slice(row);
506 }
507 }
508 }
509 }
510 }
511
512 /// Assemble the whole dense `(ij|P)` tensor on the current thread by
513 /// filling every bra-pair in sequence — the identical code path a parallel
514 /// driver runs, just serially.
515 #[must_use]
516 pub fn build(&self) -> Vec<f64> {
517 let mut out = vec![0.0; self.output_len()];
518 let mut tasks = self.partition(&mut out);
519 for task in &mut tasks {
520 self.fill(task);
521 }
522 out
523 }
524}
525
526/// One unit of parallel 3-center work: the `(μ, ν)` output rows owned by a
527/// single canonical bra-pair `(i, j)`, handed out by [`Eri3cBuilder::partition`].
528/// Distinct tasks borrow disjoint regions of the same buffer, so a driver may
529/// fill them across threads with no synchronisation ([`Eri3cBuilder::fill`]).
530#[derive(Debug)]
531pub struct Bra3cFill<'o> {
532 bra: (usize, usize),
533 /// Slabs for rows `(μ∈i, ν∈j)`, row-major `(a, b)` → index `a·n_j + b`;
534 /// each slab is the `naux`-long `P` row of that `(μ, ν)`.
535 ij_band: Vec<&'o mut [f64]>,
536 /// Slabs for rows `(μ∈j, ν∈i)`, row-major `(b, a)` → index `b·n_i + a`.
537 /// Empty when `i == j`.
538 ji_band: Vec<&'o mut [f64]>,
539}
540
541impl Bra3cFill<'_> {
542 /// The canonical bra shell-pair `(i, j)` (`i ≥ j`) this task fills.
543 #[must_use]
544 pub fn bra(&self) -> (usize, usize) {
545 self.bra
546 }
547}
548
549/// Take the slab for output `row`, asserting it has not already been claimed
550/// (a double-claim would violate the disjointness contract).
551fn claim_row<'o>(slabs: &mut [Option<&'o mut [f64]>], row: usize) -> &'o mut [f64] {
552 slabs[row]
553 .take()
554 .expect("output row claimed by two bra-pairs (disjointness violated)")
555}