qsym2/integrals/
shell_tuple.rs

1//! Management of shell tuples for generic $`n`$-centre integrals.
2
3use std::marker::PhantomData;
4
5use derive_builder::Builder;
6use indexmap::{IndexMap, IndexSet};
7use itertools::{Itertools, izip};
8use nalgebra::{Point3, Vector3};
9use ndarray::{Array, Array1, Array2, Dim};
10use rayon::prelude::*;
11
12use crate::basis::ao_integrals::{BasisSet, BasisShellContraction};
13
14/// Structure to handle pre-computed properties of a tuple of shells consisting of
15/// non-integration primitives.
16#[derive(Builder)]
17pub struct ShellTuple<'a, const RANK: usize, T: Clone> {
18    /// The data type of the overlap values from this shell tuple.
19    typ: PhantomData<T>,
20
21    /// The non-integration shells in this shell tuple. Each shell has an associated
22    /// boolean indicating if it is to be complex-conjugated in the integral
23    /// evalulation.
24    shells: [(&'a BasisShellContraction<f64, f64>, bool); RANK],
25
26    /// A fixed-size array indicating the angular-shape of this shell tuple.
27    ///
28    /// Each element in the array gives the number of angular functions of the corresponding shell.
29    angular_shell_shape: [usize; RANK],
30
31    /// A fixed-size array indicating the primitive-shape of this shell tuple.
32    ///
33    /// Each element in the array gives the number of Gaussian primitives of the
34    /// corresponding shell.
35    primitive_shell_shape: [usize; RANK],
36
37    // -----------------------------------------------
38    // Quantities common to all primitive combinations
39    // -----------------------------------------------
40    /// A fixed-size array containing the $`\mathbf{k}`$ vectors of the shells in this
41    /// shell tuple. Each $`\mathbf{k}`$ vector is appropriately signed to take into
42    /// account the complex conjugation pattern of the shell tuple.
43    ks: [Option<Vector3<f64>>; RANK],
44
45    /// The sum of all signed $`\mathbf{k}`$ vectors across all shells.
46    k: Vector3<f64>,
47
48    /// A fixed-size array containing the Cartesian origins of the shells in this shell
49    /// tuple.
50    rs: [&'a Point3<f64>; RANK],
51
52    /// A fixed-size array containing the angular momentum orders of the shells in this
53    /// shell tuple.
54    ns: [usize; RANK],
55
56    /// A fixed-size array containing conversion matrices to convert overlap values involving
57    /// shells that have specified pure orders from internally computed Cartesian orders to the
58    /// specified pure ones.
59    rl2carts: [Option<Array2<f64>>; RANK],
60
61    // -----------------------------------------------
62    // Quantities unique to each primitive combination
63    // -----------------------------------------------
64    /// A fixed-size array of arrays of non-integration primitive exponents.
65    ///
66    /// This quantity is $`\zeta_g^{(k)}`$ appearing in Equations 81 and 83 of Honda, M.,
67    /// Sato, K. & Obara, S. Formulation of molecular integrals over Gaussian functions
68    /// treatable by both the Laplace and Fourier transforms of spatial operators by
69    /// using derivative of Fourier-kernel multiplied Gaussians. *The Journal of
70    /// Chemical Physics* **94**, 3790–3804 (1991),
71    /// [DOI](https://doi.org/10.1063/1.459751).
72    ///
73    /// The i-th array in the vector is for the i-th shell. The j-th element in that
74    /// array then gives the exponent of the j-th primitive exponent of that shell.
75    zs: [Array1<&'a f64>; RANK],
76
77    /// An array containing the sums of all possible combinations of non-integration
78    /// primitive exponents across all shells.
79    ///
80    /// This quantity is $`\zeta_G^{(k)}`$ defined in Equation 81 of Honda, M.,
81    /// Sato, K. & Obara, S. Formulation of molecular integrals over Gaussian functions
82    /// treatable by both the Laplace and Fourier transforms of spatial operators by
83    /// using derivative of Fourier-kernel multiplied Gaussians. *The Journal of
84    /// Chemical Physics* **94**, 3790–3804 (1991),
85    /// [DOI](https://doi.org/10.1063/1.459751).
86    ///
87    /// This is a `RANK`-dimensional array. The element `zg[i, j, k, ...]`
88    /// gives the sum of the i-th primitive exponent on the first shell, the j-th
89    /// primitive exponent on the second shell, the k-th primitive exponent on the
90    /// third shell, and so on.
91    zg: Array<f64, Dim<[usize; RANK]>>,
92
93    /// An array containing the products of all possible combinations of
94    /// non-integration primitive exponents across all shells.
95    ///
96    /// This is a `RANK`-dimensional array. The element `sd[i, j, k, ...]`
97    /// gives the product of the i-th primitive exponent on the first shell, the j-th
98    /// primitive exponent on the second shell, the k-th primitive exponent on the
99    /// third shell, and so on.
100    zd: Array<f64, Dim<[usize; RANK]>>,
101
102    /// A fixed-size array of arrays of contraction coefficients of non-integration
103    /// primitives.
104    ///
105    /// The i-th array in the vector is for the i-th shell. The j-th element in that
106    /// array then gives the contraction coefficient of the j-th primitive exponent of
107    /// that shell.
108    ds: [Array1<&'a f64>; RANK],
109
110    /// An array containing the product of all possible combinations of non-integration
111    /// primitive coefficients across all shells.
112    ///
113    /// This is a `RANK`-dimensional array. The element `dd[i, j, k, ...]` gives
114    /// the product of the i-th primitive's coefficient on the first shell, the j-th
115    /// primitive's coefficient on the second shell, the k-th primitive's coefficient
116    /// on the third shell, and so on.
117    dd: Array<f64, Dim<[usize; RANK]>>,
118
119    /// An array containing the exponent-weighted Cartesian centres of all possible
120    /// combinations of primitives across all shells.
121    ///
122    /// This is a `RANK`-dimensional array. The element `rg[i, j, k, ...]` gives
123    /// the exponent-weighted Cartesian centre of the i-th primitive on the first shell,
124    /// the j-th primitive on the second shell, the k-th primitive on the third shell,
125    /// and so on.
126    rg: Array<Point3<f64>, Dim<[usize; RANK]>>,
127
128    /// A fixed-size array of arrays giving the optional quantity $`\mathbf{Q}_j`$ for
129    /// the j-th shell.
130    ///
131    /// This quantity is defined in Equation 122 of Honda, M., Sato, K. & Obara, S.
132    /// Formulation of molecular integrals over Gaussian functions treatable by both
133    /// the Laplace and Fourier transforms of spatial operators by using derivative of
134    /// Fourier-kernel multiplied Gaussians. *The Journal of Chemical Physics* **94**,
135    /// 3790–3804 (1991), [DOI](https://doi.org/10.1063/1.459751). Since there are no
136    /// integration exponents in the current implementation of [`ShellTuple`], the
137    /// summation over $`v`$ in Equation 122 is not included. See also Equation 171 in
138    /// the reference.
139    ///
140    /// The j-th array is for the j-th shell. Each array is a `RANK`-dimensional
141    /// array. The element `qs[j][i, m, k, ...]` gives the $`\mathbf{Q}_j`$ vector
142    /// defined using the i-th primitive exponent on the first shell, the m-th
143    /// primitive exponent on the second shell, the k-th primitive exponent on the
144    /// third shell, and so on. The exponent-combination dependence comes from the
145    /// $`\mathbf{R}_G`$ term.
146    ///
147    /// If the j-th shell does not have a $`\mathbf{k}_j`$ plane-wave vector, then the
148    /// corresponding $`\mathbf{Q}_j`$ is set to `None`.
149    #[allow(clippy::type_complexity)]
150    qs: [Option<Array<Vector3<f64>, Dim<[usize; RANK]>>>; RANK],
151}
152
153impl<'a, const RANK: usize, T: Clone> ShellTuple<'a, RANK, T> {
154    pub fn builder() -> ShellTupleBuilder<'a, RANK, T> {
155        ShellTupleBuilder::<RANK, T>::default()
156    }
157
158    /// The number of shells in this tuple.
159    pub fn rank(&self) -> usize {
160        RANK
161    }
162
163    /// A fixed-size array indicating the angular-shape of this shell tuple.
164    ///
165    /// Each element in the array gives the number of angular functions of the corresponding shell.
166    pub fn angular_shell_shape(&self) -> &[usize; RANK] {
167        &self.angular_shell_shape
168    }
169
170    /// A fixed-size array of arrays of contraction coefficients of non-integration
171    /// primitives.
172    ///
173    /// The i-th array in the vector is for the i-th shell. The j-th element in that
174    /// array then gives the contraction coefficient of the j-th primitive exponent of
175    /// that shell.
176    pub fn ds(&self) -> &[Array1<&'a f64>; RANK] {
177        &self.ds
178    }
179
180    /// The maximum angular momentum across all shells.
181    pub fn lmax(&self) -> u32 {
182        self.shells
183            .iter()
184            .map(|(bsc, _)| bsc.basis_shell().l)
185            .max()
186            .expect("The maximum angular momentum across all shells cannot be found.")
187    }
188}
189
190/// Structure to handle all possible shell tuples for a particular type of integral.
191#[derive(Builder)]
192pub struct ShellTupleCollection<'a, const RANK: usize, T: Clone> {
193    /// The data type of the overlap values from shell tuples in this collection.
194    typ: PhantomData<T>,
195
196    /// A fixed-size array containing basis sets, where each basis set at a certain shell position
197    /// contains all shells to be considered for that position.
198    basis_sets: [&'a BasisSet<f64, f64>; RANK],
199
200    /// The maximum angular momentum across all shells in this collection.
201    lmax: u32,
202
203    /// The complex-conjugation pattern across all shell positions in this collection.
204    ccs: [bool; RANK],
205
206    /// The numbers of shells across all shell positions in this collection.
207    n_shells: [usize; RANK],
208
209    /// The total numbers of angular functions across all shell positions in this collection. Each
210    /// value in the fixed-size array gives the total number of angular functions from all shells
211    /// at that position. In other words, this is the number of basis functions in the basis set at
212    /// that position.
213    angular_all_shell_shape: [usize; RANK],
214}
215
216impl<'a, const RANK: usize, T: Clone> ShellTupleCollection<'a, RANK, T> {
217    pub fn builder() -> ShellTupleCollectionBuilder<'a, RANK, T> {
218        ShellTupleCollectionBuilder::<RANK, T>::default()
219    }
220
221    /// The maximum angular momentum across all shell tuples.
222    pub fn lmax(&self) -> u32 {
223        self.lmax
224    }
225
226    /// The number of shells in each tuple in the collection.
227    pub fn rank(&self) -> usize {
228        RANK
229    }
230
231    /// Returns an iterator of the shell tuples unique with respect to permutations of the
232    /// constituent shells, taking into account complex conjugation, symmetry
233    /// transformation, and derivative patterns.
234    ///
235    /// # Arguments
236    ///
237    /// * `ls` - The derivative pattern.
238    fn unique_shell_tuples_iter<'it>(
239        &'it self,
240        ls: [usize; RANK],
241    ) -> UniqueShellTupleIterator<'it, 'a, RANK, T>
242    where
243        'a: 'it,
244    {
245        // Example:
246        //     ccs = [true, true, false, true, false]
247        //     ls  = [   1,    1,     0,    2,     0]
248        //     sym = [   1,    0,     0,    0,     0] -- not considered for now
249        //     nsh = [   2,    1,     2,    2,     2]
250        //           i.e. Each shell position has three possible shells.
251
252        // Each shell type is a tuple of its complex-conjugationness, its derivative
253        // order, and its length. The vector `shell_types` collects these tuples together.
254        // Example:
255        // shell_types = [
256        //     (true, 1, 3), (true, 1, 4), (false, 0, 3), (true, 2, 3), (false, 0, 3)
257        // ].
258        // We see that there are four types here, and only shell positions that
259        // have the same type have permutation equivalence, i.e. [0], [1], [2, 4], [3].
260        let shell_types: Vec<(bool, usize, usize)> =
261            izip!(self.ccs, ls, self.n_shells).collect::<Vec<_>>();
262
263        // The map `shell_types_classified` keeps track of the unique shell types in this
264        // shell tuple and the associated shell positions as tuples.
265        // Example:
266        // shell_types_classified = {
267        //     (true , 1, 2): {0},
268        //     (true , 1, 1): {1},
269        //     (false, 0, 2): {2, 4},
270        //     (true , 2, 2): {3}
271        // }.
272        let mut shell_types_classified: IndexMap<(bool, usize, usize), IndexSet<usize>> =
273            IndexMap::new();
274        shell_types
275            .into_iter()
276            .enumerate()
277            .for_each(|(shell_index, shell_type)| {
278                shell_types_classified
279                    .entry(shell_type)
280                    .or_default()
281                    .insert(shell_index);
282            });
283
284        // The map `shell_indices_unique_combinations` maps, for each shell type, the
285        // corresponding indices of shells of that type to the unique index combinations.
286        // Example:
287        // shell_type_unique_combinations = {
288        //     [0]   : [[0], [1]],
289        //     [1]   : [[0]],
290        //     [2, 4]: [[0, 0], [0, 1], [1, 1]],
291        //     [3]   : [[0], [1]],
292        // }
293        let shell_indices_unique_combinations = shell_types_classified
294            .iter()
295            .map(|(shell_type, shell_indices)| {
296                (
297                    shell_indices.iter().collect::<Vec<_>>(),
298                    (0..shell_type.2)
299                        .combinations_with_replacement(shell_indices.len())
300                        .collect::<Vec<_>>(),
301                )
302            })
303            .collect::<IndexMap<_, _>>();
304
305        // Example:
306        // sis = [0, 1, 2, 4, 3]
307        // gg = [
308        //     [[0], [1]],
309        //     [[0]],
310        //     [[0, 0], [0, 1], [1, 1]],
311        //     [[0], [1]]
312        // ]
313        // `order` gives the indices to sort `sis`.
314        // order = [0, 1, 2, 4, 3]
315        let sis = shell_indices_unique_combinations
316            .keys()
317            .flatten()
318            .collect::<Vec<_>>();
319        let mut order = (0..sis.len()).collect::<Vec<_>>();
320        order.sort_by_key(|&i| &sis[i]);
321        let gg = shell_indices_unique_combinations
322            .into_values()
323            .collect::<Vec<_>>();
324
325        // `unordered_recombined_shell_indices` forms all possible combinations of
326        // shell indices across all different shell types.
327        // Example:
328        // unordered_recombined_shell_indices = [
329        //     [[0], [0], [0, 0], [0]],
330        //     [[0], [0], [0, 0], [1]],
331        //     [[0], [0], [0, 1], [0]],
332        //     [[0], [0], [0, 1], [1]],
333        //     [[0], [0], [1, 1], [0]],
334        //     [[0], [0], [1, 1], [1]],
335        //     ...
336        // ] (12 = 2 * 1 * 3 * 2 terms in total)
337        let unordered_recombined_shell_indices =
338            gg.into_iter().multi_cartesian_product().collect::<Vec<_>>();
339
340        log::debug!("Rank-{RANK} shell tuple collection information:");
341        log::debug!(
342            "  Total number of unique tuples: {}",
343            unordered_recombined_shell_indices.len()
344        );
345
346        UniqueShellTupleIterator::<'it, 'a, RANK, T> {
347            index: 0,
348            shell_order: order,
349            unordered_recombined_shell_indices,
350            stc: self,
351        }
352    }
353}
354
355/// Iterator over unique shell tuples in a collection.
356struct UniqueShellTupleIterator<'it, 'a: 'it, const RANK: usize, T: Clone> {
357    /// The current index of iteration.
358    index: usize,
359
360    /// Indices to reorder shell positions in each flattened `Vec<Vec<usize>>` of
361    /// [`Self::unordered_recombined_shell_indices`] to put them in the right place.
362    shell_order: Vec<usize>,
363
364    /// All possible combinations of shell indices across all different shell types. Each
365    /// `Vec<Vec<usize>>` gives one unique shell tuple combination. Each `Vec<usize>` gives the
366    /// indices of shells within a shell type.
367    unordered_recombined_shell_indices: Vec<Vec<Vec<usize>>>,
368
369    /// The shell tuple collection with respect to which this iterator is defined.
370    stc: &'it ShellTupleCollection<'a, RANK, T>,
371}
372
373/// Implements methods for shell tuples of a specified pattern.
374macro_rules! impl_shell_tuple {
375    ( $RANK:ident, <$($shell_name:ident),+> ) => {
376        const $RANK: usize = count_exprs!($($shell_name),+);
377
378        impl<'it, 'a: 'it, T: Clone> Iterator for UniqueShellTupleIterator<'it, 'a, $RANK, T> {
379            type Item = (ShellTuple<'a, $RANK, T>, [usize; $RANK], Vec<[usize; $RANK]>);
380
381            fn next(&mut self) -> Option<Self::Item> {
382                let unordered_shell_index = self.unordered_recombined_shell_indices.get(self.index)?;
383
384                // Now, for each term in `unordered_recombined_shell_indices`, we need to
385                // flatten and then reorder to put the shell indices at the correct positions. This
386                // gives `ordered_shell_index` which gives a unique shell tuple permutation.
387                let flattened_unordered_shell_index = unordered_shell_index
388                    .clone()
389                    .into_iter()
390                    .flatten()
391                    .collect::<Vec<_>>();
392                let mut ordered_shell_index_iter = self.shell_order.iter().map(|i| {
393                    flattened_unordered_shell_index[*i]
394                });
395                $(
396                    let $shell_name = ordered_shell_index_iter
397                        .next()
398                        .expect("Shell index out of range.");
399                )+
400                let ordered_shell_index = [$($shell_name),+];
401
402                let mut ordered_shell_index_iter = ordered_shell_index.iter().enumerate();
403                $(
404                    let $shell_name = ordered_shell_index_iter
405                        .next()
406                        .map(|(shell_index, &i)| {
407                            (&self.stc.basis_sets[shell_index][i], self.stc.ccs[shell_index])
408                        })
409                        .expect("Shell index out of range.");
410                )+
411                let shell_tuple = build_shell_tuple!($($shell_name),+; T);
412
413                // For each term in `unordered_recombined_shell_indices`, all unique
414                // permutations of each sub-vector gives an equivalent permutation.
415                // Example: consider [[0], [0], [0, 1], [1]]. This gives the following
416                // equivalent permutations:
417                //   [[0], [0], [0, 1], [1]]
418                //   [[0], [0], [1, 0], [1]]
419                // There are two of them (1 * 1 * 2 * 1).
420                // Each equivalent permutation undergoes the same 'flattening' and
421                // 'reordering' process as for the unique term.
422                let equiv_perms = unordered_shell_index
423                    .iter()
424                    .map(|y| y.into_iter().permutations(y.len()).into_iter().unique())
425                    .multi_cartesian_product()
426                    .into_iter()
427                    .map(|x| {
428                        let mut equiv_perm_iter = x.into_iter().flatten().cloned();
429                        $(
430                            let $shell_name = equiv_perm_iter
431                                .next()
432                                .expect("Shell index out of range.");
433                        )+
434                        [$($shell_name),+]
435                    })
436                    .collect::<Vec<_>>();
437
438                log::debug!("Generated unique permutation: {ordered_shell_index:?}");
439                self.index += 1;
440                Some((shell_tuple, ordered_shell_index, equiv_perms))
441            }
442        }
443
444        crate::integrals::overlap::impl_shell_tuple_overlap!($RANK, <$($shell_name),+>);
445    }
446}
447
448/// Constructs a shell tuple given a pattern and a matching sequence of shells.
449///
450/// # Patterns
451///
452/// * `$shell` - A tuple `(shell, cc)` where `shell` is a [`BasisShellContraction`] and `cc` a
453///   boolean indicating if the shell is complex-conjugated.
454/// * `$ty` - The data type for the overlap values from this shell tuple.
455macro_rules! build_shell_tuple {
456    ( $($shell:expr),+; $ty:ty ) => {
457        {
458            use std::marker::PhantomData;
459
460            use itertools::Itertools;
461            use ndarray::{Array, Array1, Dim};
462            use nalgebra::{Point3, Vector3};
463
464            use crate::angmom::sh_conversion::sh_rl2cart_mat;
465            use crate::basis::ao::CartOrder;
466            use crate::integrals::shell_tuple::ShellTuple;
467
468            const RANK: usize = count_exprs!($($shell),+);
469
470            let zg = {
471                let arr_vec = [$(
472                    $shell.0.contraction.primitives.iter().map(|(e, _)| e).collect::<Vec<_>>()
473                ),+].into_iter()
474                    .multi_cartesian_product()
475                    .map(|s| s.into_iter().sum()).collect::<Vec<_>>();
476                let arr = Array::<f64, Dim<[usize; RANK]>>::from_shape_vec(
477                    ($($shell.0.contraction.primitives.len()),+), arr_vec
478                ).unwrap_or_else(|err| {
479                    log::error!("{err}");
480                    panic!("Unable to construct the {RANK}-dimensional array of exponent sums.")
481                });
482                arr
483            };
484
485            let rg = {
486                let arr_vec = [$(
487                    $shell
488                        .0
489                        .contraction.primitives
490                        .iter()
491                        .map(|(e, _)| *e * $shell.0.cart_origin).collect::<Vec<_>>()
492                ),+].into_iter()
493                    .multi_cartesian_product()
494                    .map(|s| {
495                        s.into_iter()
496                            .fold(Point3::origin(), |acc, r| acc + r.coords)
497                    })
498                    .collect::<Vec<_>>();
499                let arr = Array::<Point3<f64>, Dim<[usize; RANK]>>::from_shape_vec(
500                    ($($shell.0.contraction.primitives.len()),+), arr_vec
501                ).unwrap_or_else(|err| {
502                    log::error!("{err}");
503                    panic!("Unable to construct the {RANK}-dimensional array of exponent-weighted centres.")
504                }) / &zg;
505                arr
506            };
507
508            let qs = [$(
509                $shell.0.k().map(|_| rg.map(|r| (r - $shell.0.cart_origin().coords).coords))
510            ),+];
511
512            ShellTuple::<RANK, $ty>::builder()
513                .typ(PhantomData)
514                .shells([$($shell),+])
515                .angular_shell_shape([$($shell.0.basis_shell().n_funcs()),+])
516                .primitive_shell_shape([$($shell.0.contraction_length()),+])
517                .rs([$($shell.0.cart_origin()),+])
518                .ks([$(
519                    if $shell.1 {
520                        // true, hence -(-) = +
521                        $shell.0.k().copied()
522                    } else {
523                        // false, hence -
524                        $shell.0.k().copied().map(|k| -k)
525                    }
526                ),+])
527                .k([$(
528                    if $shell.1 {
529                        // true, hence -(-) = +
530                        $shell.0.k().copied()
531                    } else {
532                        // false, hence -
533                        $shell.0.k().copied().map(|k| -k)
534                    }
535                ),+]
536                    .into_iter()
537                    .filter_map(|k| k)
538                    .fold(Vector3::zeros(), |acc, k| acc + k))
539                .ns([$(
540                    usize::try_from($shell.0.basis_shell().l)
541                        .expect("Unable to convert an angular momentum `l` value to `usize`.")
542                ),+])
543                .rl2carts([$(
544                    match &$shell.0.basis_shell().shell_order {
545                        ShellOrder::Cart(_) | ShellOrder::Spinor(_) => None,
546                        ShellOrder::Pure(po) => Some(sh_rl2cart_mat(
547                            po.lpure,
548                            po.lpure,
549                            &CartOrder::lex(po.lpure),
550                            true,
551                            &po
552                        )),
553                    }
554                ),+])
555                .zs([$(
556                    Array1::from_iter($shell.0.contraction.primitives.iter().map(|(e, _)| e))
557                ),+])
558                .zg(zg)
559                .zd({
560                    let arr_vec = [$(
561                        $shell.0.contraction.primitives.iter().map(|(e, _)| e).collect::<Vec<_>>()
562                    ),+].into_iter()
563                        .multi_cartesian_product()
564                        .map(|s| s.into_iter().fold(1.0, |acc, e| acc * e)).collect::<Vec<_>>();
565                    let arr = Array::<f64, Dim<[usize; RANK]>>::from_shape_vec(
566                        ($($shell.0.contraction.primitives.len()),+), arr_vec
567                    ).unwrap_or_else(|err| {
568                        log::error!("{err}");
569                        panic!("Unable to construct the {RANK}-dimensional array of exponent products.")
570                    });
571                    arr
572                })
573                .ds([$(
574                    Array1::from_iter($shell.0.contraction.primitives.iter().map(|(_, c)| c))
575                ),+])
576                .dd({
577                    let arr_vec = [$(
578                        $shell.0.contraction.primitives.iter().map(|(_, c)| c).collect::<Vec<_>>()
579                    ),+].into_iter()
580                        .multi_cartesian_product()
581                        .map(|s| s.into_iter().fold(1.0, |acc, c| acc * c)).collect::<Vec<_>>();
582                    let arr = Array::<f64, Dim<[usize; RANK]>>::from_shape_vec(
583                        ($($shell.0.contraction.primitives.len()),+), arr_vec
584                    ).unwrap_or_else(|err| {
585                        log::error!("{err}");
586                        panic!("Unable to construct the {RANK}-dimensional array of coefficient products.")
587                    });
588                    arr
589                })
590                .rg(rg)
591                .qs(qs)
592                .build()
593                .unwrap_or_else(|_| panic!("Unable to construct a shell tuple of rank {RANK}."))
594        }
595    }
596}
597
598/// Constructs a shell tuple collection given a pattern and a matching sequence of basis sets.
599///
600/// # Patterns
601///
602/// * `$shell_name` - An identifier for a shell position.
603/// * `$shell_cc` - A boolean indicating if the shell position is complex-conjugated.
604/// * `$basisset` - A [`BasisSet`] giving all shells at the corresponding shell position.
605/// * `$ty` - The data type for the overlap values from this shell tuple collection.
606macro_rules! build_shell_tuple_collection {
607    ( <$($shell_name:ident),+>; $($shell_cc:expr),+; $($basisset:expr),+; $ty:ty ) => {
608        {
609            use std::marker::PhantomData;
610
611            use crate::integrals::shell_tuple::ShellTupleCollection;
612
613            const RANK: usize = count_exprs!($($shell_name),+);
614
615            let lmax: u32 = *[$(
616                $basisset.all_shells().map(|shell| shell.basis_shell.l).collect::<Vec<_>>()
617            ),+].iter()
618                .flatten()
619                .max()
620                .expect("Unable to determine the maximum angular momentum across all shells.");
621
622            let n_shells = [$($basisset.n_shells()),+];
623            log::debug!("Rank-{RANK} shell tuple collection construction:");
624            log::debug!(
625                "  Total number of tuples: {}",
626                n_shells.iter().product::<usize>()
627            );
628            ShellTupleCollection::<RANK, $ty>::builder()
629                .typ(PhantomData)
630                .basis_sets([$($basisset),+])
631                .lmax(lmax)
632                .ccs([$($shell_cc),+])
633                .n_shells(n_shells)
634                .angular_all_shell_shape([$(
635                    $basisset
636                        .all_shells()
637                        .map(|shell| shell.basis_shell().n_funcs())
638                        .sum::<usize>()
639                ),+])
640                .build()
641                .unwrap_or_else(|_| panic!("Unable to construct a shell tuple collection of rank {RANK}."))
642        }
643    }
644}
645
646use duplicate::duplicate_item;
647use factorial::DoubleFactorial;
648use log;
649use ndarray::{Zip, s};
650use ndarray_einsum::*;
651use num_complex::Complex;
652use num_traits::ToPrimitive;
653
654use crate::basis::ao::{CartOrder, ShellOrder};
655
656type C128 = Complex<f64>;
657
658impl_shell_tuple![RANK_2, <s1, s2>];
659impl_shell_tuple![RANK_3, <s1, s2, s3>];
660impl_shell_tuple![RANK_4, <s1, s2, s3, s4>];
661impl_shell_tuple![RANK_5, <s1, s2, s3, s4, s5>];
662
663pub(crate) use {build_shell_tuple, build_shell_tuple_collection};
664
665#[cfg(test)]
666#[path = "shell_tuple_tests.rs"]
667mod shell_tuple_tests;
668
669#[cfg(test)]
670#[path = "shell_tuple_collection_tests.rs"]
671mod shell_tuple_collection_tests;