qsym2/bindings/python/
integrals.rs

1//! Python bindings for QSym² atomic-orbital integral evaluations.
2
3use anyhow::{self, bail, ensure, format_err};
4use lazy_static::lazy_static;
5#[cfg(feature = "integrals")]
6use nalgebra::{Point3, Vector3};
7#[cfg(feature = "integrals")]
8use num_complex::Complex;
9#[cfg(feature = "integrals")]
10use numpy::{IntoPyArray, PyArray2, PyArray4};
11use periodic_table;
12#[cfg(feature = "integrals")]
13use pyo3::exceptions::PyValueError;
14use pyo3::prelude::*;
15use pyo3::types::PyType;
16#[cfg(feature = "qchem")]
17use regex::Regex;
18use serde::{Deserialize, Serialize};
19
20use crate::angmom::spinor_rotation_3d::{SpinConstraint, SpinOrbitCoupled};
21use crate::auxiliary::molecule::Molecule;
22use crate::basis::ao::{
23    BasisAngularOrder, BasisAtom, BasisShell, CartOrder, PureOrder, ShellOrder,
24    SpinorBalanceSymmetry, SpinorOrder, SpinorParticleType,
25};
26#[cfg(feature = "integrals")]
27use crate::basis::ao_integrals::{BasisSet, BasisShellContraction, GaussianContraction};
28#[cfg(feature = "integrals")]
29use crate::integrals::shell_tuple::build_shell_tuple_collection;
30#[cfg(feature = "qchem")]
31use crate::io::format::{QSym2Output, log_title, qsym2_output};
32
33#[cfg(feature = "qchem")]
34lazy_static! {
35    static ref SP_PATH_RE: Regex =
36        Regex::new(r"(.*sp)\\energy_function$").expect("Regex pattern invalid.");
37}
38
39/// Python-exposed enumerated type to handle the union type `(bool, bool) | (list[int], bool)` in
40/// Python for specifying pure-spherical-harmonic order or spinor order.
41#[derive(Clone, FromPyObject)]
42pub enum PyPureSpinorOrder {
43    /// Variant for standard pure or spinor shell order. The first associated boolean indicates if
44    /// the functions are arranged in increasing-$`m`$ order, and the second associated boolean
45    /// indicates if the shell is even with respect to spatial inversion.
46    Standard((bool, bool)),
47
48    /// Variant for custom pure or spinor shell order. The associated vector contains a sequence of
49    /// integers specifying the order of $`m`$ values for pure or $`2m`$ values for spinor in the
50    /// shell, and the associated boolean indicates if the shell is even with respect to spatial
51    /// inversion.
52    Custom((Vec<i32>, bool)),
53}
54
55/// Python-exposed enumerated type to handle the `ShellOrder` union type `PyPureSpinorOrder |
56/// Optional[list[tuple[int, int, int]]]` in Python.
57#[derive(Clone, FromPyObject)]
58pub enum PyShellOrder {
59    /// Variant for pure or spinor shell order. The associated value is a tuple of two values: the
60    /// first is either a boolean indicating if the functions are arranged in increasing-$`m`$
61    /// order or a sequence of integers specifying a custom $`m`$-order for pure or $`2m`$-order
62    /// for spinor, and the second is a boolean indicating whether the spatial parts of the
63    /// functions in the shell are even with respect to spatial inversion.
64    PureSpinorOrder(PyPureSpinorOrder),
65
66    /// Variant for Cartesian shell order. If the associated `Option` is `None`, the order will be
67    /// taken to be lexicographic. Otherwise, the order will be as specified by the $`(x, y, z)`$
68    /// exponent tuples.
69    CartOrder(Option<Vec<(u32, u32, u32)>>),
70}
71
72/// Python-exposed enumerated type indicating the shell type.
73#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
74#[pyclass(eq, eq_int)]
75pub enum ShellType {
76    /// Variant for a pure shell.
77    Pure,
78
79    /// Variant for a Cartesian shell.
80    Cartesian,
81
82    /// Variant for a spinor shell corresponding to a fermion without any additional balance
83    /// symmetries.
84    SpinorFermion,
85
86    /// Variant for a spinor shell corresponding to a fermion with the kinetic balance symmetry due
87    /// to $`\mathbf{\sigma} \cdot \hat{\mathbf{p}}`$.
88    SpinorFermionKineticBalance,
89
90    /// Variant for a spinor shell corresponding to an antifermion without any additional balance
91    /// symmetries.
92    SpinorAntifermion,
93
94    /// Variant for a spinor shell describing an antifermion with the kinetic balance symmetry due
95    /// to $`\mathbf{\sigma} \cdot \hat{\mathbf{p}}`$.
96    SpinorAntifermionKineticBalance,
97}
98
99// ===================
100// PyBasisAngularOrder
101// ===================
102
103/// Python-exposed structure to marshall basis angular order information between Python and Rust.
104#[pyclass]
105#[derive(Clone)]
106pub struct PyBasisAngularOrder {
107    /// A vector of tuples, each of which provides information for one basis atom in the form
108    /// `tuple[element, basis_shells]`. Here:
109    ///   * `element` is a string giving the element symbol of the atom, and
110    ///   * `basis_shells` is a vector of tuples, each of which provides information for one basis
111    ///   shell on the atom in the form `(angmom, shelltype, order)`. Here:
112    ///     * `angmom` is an integer specifying the angular momentum of the shell, the meaning of
113    ///     which depends on the shell type,
114    ///     * `shelltype` is an enumerated type indicating the type of the shell, and
115    ///     * `order` specifies how the functions in the shell are ordered:
116    ///       * if `shelltype` is `ShellType.Cartesian`, `order` can be `None` for lexicographic
117    ///       order, or a list of tuples `(lx, ly, lz)` specifying a custom order for the Cartesian
118    ///       functions where `lx`, `ly`, and `lz` are the $`x`$-, $`y`$-, and $`z`$-exponents,
119    ///       respectively;
120    ///       * if `shelltype` is `ShellType.Pure`, `ShellType.SpinorFermion`,
121    ///       `SpinorFermionKineticBalance`, `ShellType.SpinorAntifermion`, or
122    ///       `ShellType.SpinorAntifermionKineticBalance`, `order` is a tuple of two values: the
123    ///       first can be `true` for increasing-$`m`$ order, `false` for decreasing-$`m`$ order, or
124    ///       a list of $`m`$ values for custom order, and the second is a boolean indicating whether
125    ///       the spatial parts of the functions in the shell are even with respect to spatial inversion.
126    basis_atoms: Vec<(String, Vec<(u32, ShellType, PyShellOrder)>)>,
127}
128
129#[pymethods]
130impl PyBasisAngularOrder {
131    /// Constructs a new `PyBasisAngularOrder` structure.
132    ///
133    /// # Arguments
134    ///
135    /// * `basis_atoms` - A vector of tuples, each of which provides information for one basis
136    /// atom in the form `tuple[element, basis_shells]`. Here:
137    ///   * `element` is a string giving the element symbol of the atom, and
138    ///   * `basis_shells` is a vector of tuples, each of which provides information for one basis
139    ///   shell on the atom in the form `(angmom, shelltype, order)`. Here:
140    ///     * `angmom` is an integer specifying the angular momentum of the shell, the meaning of
141    ///     which depends on the shell type,
142    ///     * `shelltype` is an enumerated type with possible variants `ShellType.Pure`,
143    ///     `ShellType.Cartesian`, `ShellType.Spinor`, and `ShellType.SpinorKineticBalance`
144    ///     indicating the type of the shell, and
145    ///     * `order` specifies how the functions in the shell are ordered:
146    ///       * if `shelltype` is `Cartesian`, `order` can be `None` for lexicographic order, or a
147    ///       list of tuples `(lx, ly, lz)` specifying a custom order for the Cartesian functions
148    ///       where `lx`, `ly`, and `lz` are the $`x`$-, $`y`$-, and $`z`$-exponents, respectively;
149    ///       * if `shelltype` is `Pure`, `Spinor`, or `SpinorKineticBalance`, `order` is a tuple of
150    ///       two values: the first can be `true` for increasing-$`m`$ order, `false` for
151    ///       decreasing-$`m`$ order, or a list of $`m`$ values for custom order, and the second is
152    ///       a boolean indicating whether the spatial parts of the functions in the shell are even
153    ///       with respect to spatial inversion.
154    #[new]
155    fn new(basis_atoms: Vec<(String, Vec<(u32, ShellType, PyShellOrder)>)>) -> Self {
156        Self { basis_atoms }
157    }
158
159    /// Extracts basis angular order information from a Q-Chem HDF5 archive file.
160    ///
161    /// # Arguments
162    ///
163    /// * `filename` - A path to a Q-Chem HDF5 archive file.
164    ///
165    /// # Returns
166    ///
167    /// A sequence of `PyBasisAngularOrder` objects, one for each Q-Chem calculation found in the
168    /// HDF5 archive file.
169    ///
170    /// A summary showing how the `PyBasisAngularOrder` objects map onto the Q-Chem calculations in
171    /// the HDF5 archive file is also logged at the `INFO` level.
172    #[cfg(feature = "qchem")]
173    #[classmethod]
174    fn from_qchem_archive(_cls: &Bound<'_, PyType>, filename: &str) -> PyResult<Vec<Self>> {
175        use hdf5;
176        use indexmap::IndexMap;
177        use num::ToPrimitive;
178
179        let f = hdf5::File::open(filename).map_err(|err| PyValueError::new_err(err.to_string()))?;
180        let mut sp_paths = f
181            .group(".counters")
182            .map_err(|err| PyValueError::new_err(err.to_string()))?
183            .member_names()
184            .map_err(|err| PyValueError::new_err(err.to_string()))?
185            .iter()
186            .filter_map(|path| {
187                if SP_PATH_RE.is_match(path) {
188                    let path = path.replace("\\", "/");
189                    Some(path.replace("/energy_function", ""))
190                } else {
191                    None
192                }
193            })
194            .collect::<Vec<_>>();
195        sp_paths.sort_by(|path_a, path_b| numeric_sort::cmp(path_a, path_b));
196
197        let elements = periodic_table::periodic_table();
198
199        log_title(&format!(
200            "Basis angular order extraction from Q-Chem HDF5 archive files",
201        ));
202        let pybaos = sp_paths
203            .iter()
204            .map(|sp_path| {
205                let sp_group = f
206                    .group(sp_path)
207                    .map_err(|err| PyValueError::new_err(err.to_string()))?;
208                let shell_types = sp_group
209                    .dataset("aobasis/shell_types")
210                    .map_err(|err| PyValueError::new_err(err.to_string()))?
211                    .read_1d::<i32>()
212                    .map_err(|err| PyValueError::new_err(err.to_string()))?;
213                let shell_to_atom_map = sp_group
214                    .dataset("aobasis/shell_to_atom_map")
215                    .map_err(|err| PyValueError::new_err(err.to_string()))?
216                    .read_1d::<usize>()
217                    .map_err(|err| PyValueError::new_err(err.to_string()))?
218                    .iter()
219                    .zip(shell_types.iter())
220                    .flat_map(|(&idx, shell_type)| {
221                        if *shell_type == -1 {
222                            vec![idx, idx]
223                        } else {
224                            vec![idx]
225                        }
226                    })
227                    .collect::<Vec<_>>();
228                let nuclei = sp_group
229                    .dataset("structure/nuclei")
230                    .map_err(|err| PyValueError::new_err(err.to_string()))?
231                    .read_1d::<usize>()
232                    .map_err(|err| PyValueError::new_err(err.to_string()))?;
233
234                let mut basis_atoms_map: IndexMap<usize, Vec<(u32, ShellType, PyShellOrder)>> =
235                    IndexMap::new();
236                shell_types.iter().zip(shell_to_atom_map.iter()).for_each(
237                    |(shell_type, atom_idx)| {
238                        if *shell_type == 0 {
239                            // S shell
240                            basis_atoms_map.entry(*atom_idx).or_insert(vec![]).push((
241                                0,
242                                ShellType::Cartesian,
243                                PyShellOrder::CartOrder(Some(CartOrder::qchem(0).cart_tuples)),
244                            ));
245                        } else if *shell_type == 1 {
246                            // P shell
247                            basis_atoms_map.entry(*atom_idx).or_insert(vec![]).push((
248                                1,
249                                ShellType::Cartesian,
250                                PyShellOrder::CartOrder(Some(CartOrder::qchem(1).cart_tuples)),
251                            ));
252                        } else if *shell_type == -1 {
253                            // SP shell
254                            basis_atoms_map
255                                .entry(*atom_idx)
256                                .or_insert(vec![])
257                                .extend_from_slice(&[
258                                    (
259                                        0,
260                                        ShellType::Cartesian,
261                                        PyShellOrder::CartOrder(Some(
262                                            CartOrder::qchem(0).cart_tuples,
263                                        )),
264                                    ),
265                                    (
266                                        1,
267                                        ShellType::Cartesian,
268                                        PyShellOrder::CartOrder(Some(
269                                            CartOrder::qchem(1).cart_tuples,
270                                        )),
271                                    ),
272                                ]);
273                        } else if *shell_type < 0 {
274                            // Cartesian D shell or higher
275                            let l = shell_type.unsigned_abs();
276                            // let l_usize = l
277                            //     .to_usize()
278                            //     .unwrap_or_else(|| panic!("Unable to convert the angular momentum value `|{shell_type}|` to `usize`."));
279                            basis_atoms_map.entry(*atom_idx).or_insert(vec![]).push((
280                                l,
281                                ShellType::Cartesian,
282                                PyShellOrder::CartOrder(Some(CartOrder::qchem(l).cart_tuples)),
283                            ));
284                        } else {
285                            // Pure D shell or higher
286                            let l = shell_type.unsigned_abs();
287                            // let l_usize = l
288                            //     .to_usize()
289                            //     .unwrap_or_else(|| panic!("Unable to convert the angular momentum value `|{shell_type}|` to `usize`."));
290                            basis_atoms_map.entry(*atom_idx).or_insert(vec![]).push((
291                                l,
292                                ShellType::Pure,
293                                PyShellOrder::PureSpinorOrder(PyPureSpinorOrder::Standard((
294                                    true,
295                                    l % 2 == 0,
296                                ))),
297                            ));
298                        }
299                    },
300                );
301                let pybao = basis_atoms_map
302                    .into_iter()
303                    .map(|(atom_idx, v)| {
304                        let element = elements
305                            .get(nuclei[atom_idx])
306                            .map(|el| el.symbol.to_string())
307                            .ok_or_else(|| {
308                                PyValueError::new_err(format!(
309                                    "Unable to identify an element for atom index `{atom_idx}`."
310                                ))
311                            })?;
312                        Ok((element, v))
313                    })
314                    .collect::<Result<Vec<_>, _>>()
315                    .map(|basis_atoms| Self::new(basis_atoms));
316                pybao
317            })
318            .collect::<Result<Vec<_>, _>>();
319
320        let idx_width = sp_paths.len().ilog10().to_usize().unwrap_or(4).max(4) + 1;
321        let sp_path_width = sp_paths
322            .iter()
323            .map(|sp_path| sp_path.chars().count())
324            .max()
325            .unwrap_or(10)
326            .max(10);
327        let table_width = idx_width + sp_path_width + 4;
328        qsym2_output!("");
329        "Each single-point calculation has associated with it a `PyBasisAngularOrder` object.\n\
330        The table below shows the `PyBasisAngularOrder` index in the generated list and the\n\
331        corresponding single-point calculation."
332            .log_output_display();
333        qsym2_output!("{}", "┈".repeat(table_width));
334        qsym2_output!(" {:<idx_width$}  {:<}", "Index", "Q-Chem job");
335        qsym2_output!("{}", "┈".repeat(table_width));
336        sp_paths.iter().enumerate().for_each(|(i, sp_path)| {
337            qsym2_output!(" {:<idx_width$}  {:<}", i, sp_path);
338        });
339        qsym2_output!("{}", "┈".repeat(table_width));
340        qsym2_output!("");
341
342        pybaos
343    }
344}
345
346impl PyBasisAngularOrder {
347    /// Extracts the information in the [`PyBasisAngularOrder`] structure into `QSym2`'s native
348    /// [`BasisAngularOrder`] structure.
349    ///
350    /// # Arguments
351    ///
352    /// * `mol` - The molecule with which the basis set information is associated.
353    ///
354    /// # Returns
355    ///
356    /// The [`BasisAngularOrder`] structure with the same information.
357    ///
358    /// # Errors
359    ///
360    /// Errors if the number of atoms or the atom elements in `mol` do not match the number of
361    /// atoms and atom elements in `self`, or if incorrect shell order types are specified.
362    pub fn to_qsym2<'b, 'a: 'b>(
363        &'b self,
364        mol: &'a Molecule,
365    ) -> Result<BasisAngularOrder<'b>, anyhow::Error> {
366        ensure!(
367            self.basis_atoms.len() == mol.atoms.len(),
368            "The number of basis atoms does not match the number of ordinary atoms."
369        );
370        let basis_atoms = self
371            .basis_atoms
372            .iter()
373            .zip(mol.atoms.iter())
374            .flat_map(|((element, basis_shells), atom)| {
375                ensure!(
376                    *element == atom.atomic_symbol,
377                    "Expected element `{element}`, but found atom `{}`.",
378                    atom.atomic_symbol
379                );
380                let bss = basis_shells
381                    .iter()
382                    .flat_map(|(angmom, cart, shell_order)| {
383                        create_basis_shell(*angmom, cart, shell_order)
384                    })
385                    .collect::<Vec<_>>();
386                Ok(BasisAtom::new(atom, &bss))
387            })
388            .collect::<Vec<_>>();
389        Ok(BasisAngularOrder::new(&basis_atoms))
390    }
391}
392
393// ===========================
394// StructureConstraint structs
395// ===========================
396
397/// Python-exposed enumerated type to marshall basis spin constraint information between Rust and
398/// Python.
399#[pyclass(eq, eq_int)]
400#[derive(Clone, PartialEq, Eq, Hash)]
401pub enum PySpinConstraint {
402    /// Variant for restricted spin constraint. Only two spin spaces are exposed.
403    Restricted,
404
405    /// Variant for unrestricted spin constraint. Only two spin spaces arranged in decreasing-$`m`$
406    /// order (*i.e.* $`(\alpha, \beta)`$) are exposed.
407    Unrestricted,
408
409    /// Variant for generalised spin constraint. Only two spin spaces arranged in decreasing-$`m`$
410    /// order (*i.e.* $`(\alpha, \beta)`$) are exposed.
411    Generalised,
412}
413
414impl From<PySpinConstraint> for SpinConstraint {
415    fn from(pysc: PySpinConstraint) -> Self {
416        match pysc {
417            PySpinConstraint::Restricted => SpinConstraint::Restricted(2),
418            PySpinConstraint::Unrestricted => SpinConstraint::Unrestricted(2, false),
419            PySpinConstraint::Generalised => SpinConstraint::Generalised(2, false),
420        }
421    }
422}
423
424impl TryFrom<SpinConstraint> for PySpinConstraint {
425    type Error = anyhow::Error;
426
427    fn try_from(sc: SpinConstraint) -> Result<Self, Self::Error> {
428        match sc {
429            SpinConstraint::Restricted(2) => Ok(PySpinConstraint::Restricted),
430            SpinConstraint::Unrestricted(2, false) => Ok(PySpinConstraint::Unrestricted),
431            SpinConstraint::Generalised(2, false) => Ok(PySpinConstraint::Generalised),
432            _ => Err(format_err!(
433                "`PySpinConstraint` can only support two spin spaces."
434            )),
435        }
436    }
437}
438
439/// Python-exposed enumerated type to marshall basis spin--orbit-coupled layout in the coupled
440/// treatment of spin and spatial degrees of freedom between Rust and Python.
441#[pyclass(eq, eq_int)]
442#[derive(Clone, PartialEq, Eq, Hash)]
443pub enum PySpinOrbitCoupled {
444    /// Variant for two-component $`j`$-adapted basis functions.
445    JAdapted2C,
446
447    /// Variant for four-component $`j`$-adapted basis functions.
448    JAdapted4C,
449}
450
451impl From<PySpinOrbitCoupled> for SpinOrbitCoupled {
452    fn from(pysoc: PySpinOrbitCoupled) -> Self {
453        match pysoc {
454            PySpinOrbitCoupled::JAdapted2C => SpinOrbitCoupled::JAdapted(1),
455            PySpinOrbitCoupled::JAdapted4C => SpinOrbitCoupled::JAdapted(2),
456        }
457    }
458}
459
460impl TryFrom<SpinOrbitCoupled> for PySpinOrbitCoupled {
461    type Error = anyhow::Error;
462
463    fn try_from(soc: SpinOrbitCoupled) -> Result<Self, Self::Error> {
464        match soc {
465            SpinOrbitCoupled::JAdapted(1) => Ok(PySpinOrbitCoupled::JAdapted2C),
466            SpinOrbitCoupled::JAdapted(2) => Ok(PySpinOrbitCoupled::JAdapted4C),
467            _ => Err(format_err!(
468                "`PySpinOrbitCoupled` can only support two-component or four-component basis functions (i.e. one or two spinor blocks)."
469            )),
470        }
471    }
472}
473
474/// Python-exposed enumerated type to handle the union type `PySpinConstraint | PySpinOrbitCoupled`
475/// in Python.
476#[derive(FromPyObject, Clone, PartialEq, Eq, Hash)]
477pub enum PyStructureConstraint {
478    /// Variant for Python-exposed spin constraint layout.
479    SpinConstraint(PySpinConstraint),
480
481    /// Variant for Python-exposed spin--orbit-coupled layout.
482    SpinOrbitCoupled(PySpinOrbitCoupled),
483}
484
485impl TryFrom<SpinConstraint> for PyStructureConstraint {
486    type Error = anyhow::Error;
487
488    fn try_from(sc: SpinConstraint) -> Result<Self, Self::Error> {
489        match sc {
490            SpinConstraint::Restricted(2) => Ok(PyStructureConstraint::SpinConstraint(
491                PySpinConstraint::Restricted,
492            )),
493            SpinConstraint::Unrestricted(2, false) => Ok(PyStructureConstraint::SpinConstraint(
494                PySpinConstraint::Unrestricted,
495            )),
496            SpinConstraint::Generalised(2, false) => Ok(PyStructureConstraint::SpinConstraint(
497                PySpinConstraint::Generalised,
498            )),
499            _ => Err(format_err!(
500                "`PySpinConstraint` can only support two spin spaces."
501            )),
502        }
503    }
504}
505
506impl TryFrom<PyStructureConstraint> for SpinConstraint {
507    type Error = anyhow::Error;
508
509    fn try_from(py_sc: PyStructureConstraint) -> Result<Self, Self::Error> {
510        match py_sc {
511            PyStructureConstraint::SpinConstraint(py_sc) => Ok(py_sc.into()),
512            PyStructureConstraint::SpinOrbitCoupled(_) => Err(format_err!(
513                "`SpinConstraint` cannot be created from `PySpinOrbitCoupled`."
514            )),
515        }
516    }
517}
518
519impl TryFrom<SpinOrbitCoupled> for PyStructureConstraint {
520    type Error = anyhow::Error;
521
522    fn try_from(soc: SpinOrbitCoupled) -> Result<Self, Self::Error> {
523        match soc {
524            SpinOrbitCoupled::JAdapted(1) => Ok(PyStructureConstraint::SpinOrbitCoupled(
525                PySpinOrbitCoupled::JAdapted2C,
526            )),
527            SpinOrbitCoupled::JAdapted(2) => Ok(PyStructureConstraint::SpinOrbitCoupled(
528                PySpinOrbitCoupled::JAdapted4C,
529            )),
530            _ => Err(format_err!(
531                "`PySpinOrbitCoupled` can only support two-component or four-component basis functions (i.e. one or two spinor blocks)."
532            )),
533        }
534    }
535}
536
537impl TryFrom<PyStructureConstraint> for SpinOrbitCoupled {
538    type Error = anyhow::Error;
539
540    fn try_from(py_sc: PyStructureConstraint) -> Result<Self, Self::Error> {
541        match py_sc {
542            PyStructureConstraint::SpinOrbitCoupled(py_soc) => Ok(py_soc.into()),
543            PyStructureConstraint::SpinConstraint(_) => Err(format_err!(
544                "`SpinOrbitCoupled` cannot be created from `PySpinConstraint`."
545            )),
546        }
547    }
548}
549
550#[cfg(feature = "integrals")]
551#[pyclass]
552#[derive(Clone)]
553/// Python-exposed structure to marshall basis shell contraction information between Rust and
554/// Python.
555pub struct PyBasisShellContraction {
556    /// A triplet of the form `(angmom, cart, order)` where:
557    ///     * `angmom` is an integer specifying the angular momentum of the shell, the meaning of
558    ///     which depends on the shell type,
559    ///     * `shelltype` is an enumerated type indicating the type of the shell, and
560    ///     * `order` specifies how the functions in the shell are ordered:
561    ///       * if `shelltype` is `ShellType.Cartesian`, `order` can be `None` for lexicographic
562    ///       order, or a list of tuples `(lx, ly, lz)` specifying a custom order for the Cartesian
563    ///       functions where `lx`, `ly`, and `lz` are the $`x`$-, $`y`$-, and $`z`$-exponents,
564    ///       respectively;
565    ///       * if `shelltype` is `ShellType.Pure`, `ShellType.SpinorFermion`,
566    ///       `SpinorFermionKineticBalance`, `ShellType.SpinorAntifermion`, or
567    ///       `ShellType.SpinorAntifermionKineticBalance`, `order` is a tuple of two values: the
568    ///       first can be `true` for increasing-$`m`$ order, `false` for decreasing-$`m`$ order, or
569    ///       a list of $`m`$ values for custom order, and the second is a boolean indicating whether
570    ///       the spatial parts of the functions in the shell are even with respect to spatial inversion.
571    pub basis_shell: (u32, ShellType, PyShellOrder),
572
573    /// A list of tuples, each of which contains the exponent and the contraction coefficient of a
574    /// Gaussian primitive in this shell.
575    pub primitives: Vec<(f64, f64)>,
576
577    /// A fixed-size list of length 3 containing the Cartesian coordinates of the origin
578    /// $`\mathbf{R}`$ of this shell in Bohr radii.
579    pub cart_origin: [f64; 3],
580
581    /// An optional fixed-size list of length 3 containing the Cartesian components of the
582    /// $`\mathbf{k}`$ vector of this shell that appears in the complex phase factor
583    /// $`\exp[i\mathbf{k} \cdot (\mathbf{r} - \mathbf{R})]`$.
584    pub k: Option<[f64; 3]>,
585}
586
587#[cfg(feature = "integrals")]
588#[pymethods]
589impl PyBasisShellContraction {
590    /// Creates a new `PyBasisShellContraction` structure.
591    ///
592    /// # Arguments
593    ///
594    /// * `basis_shell` - A triplet of the form `(angmom, cart, order)` where:
595    ///     * `angmom` is an integer specifying the angular momentum of the shell, the meaning of
596    ///     which depends on the shell type,
597    ///     * `shelltype` is an enumerated type indicating the type of the shell, and
598    ///     * `order` specifies how the functions in the shell are ordered:
599    ///       * if `shelltype` is `ShellType.Cartesian`, `order` can be `None` for lexicographic
600    ///       order, or a list of tuples `(lx, ly, lz)` specifying a custom order for the Cartesian
601    ///       functions where `lx`, `ly`, and `lz` are the $`x`$-, $`y`$-, and $`z`$-exponents,
602    ///       respectively;
603    ///       * if `shelltype` is `ShellType.Pure`, `ShellType.SpinorFermion`,
604    ///       `SpinorFermionKineticBalance`, `ShellType.SpinorAntifermion`, or
605    ///       `ShellType.SpinorAntifermionKineticBalance`, `order` is a tuple of two values: the
606    ///       first can be `true` for increasing-$`m`$ order, `false` for decreasing-$`m`$ order, or
607    ///       a list of $`m`$ values for custom order, and the second is a boolean indicating whether
608    ///       the spatial parts of the functions in the shell are even with respect to spatial inversion.
609    /// * `primitives` - A list of tuples, each of which contains the exponent and the contraction
610    /// coefficient of a Gaussian primitive in this shell.
611    /// * `cart_origin` - A fixed-size list of length 3 containing the Cartesian coordinates of the
612    /// origin of this shell.
613    /// * `k` - An optional fixed-size list of length 3 containing the Cartesian components of the
614    /// $`\mathbf{k}`$ vector of this shell.
615    #[new]
616    #[pyo3(signature = (basis_shell, primitives, cart_origin, k=None))]
617    pub fn new(
618        basis_shell: (u32, ShellType, PyShellOrder),
619        primitives: Vec<(f64, f64)>,
620        cart_origin: [f64; 3],
621        k: Option<[f64; 3]>,
622    ) -> Self {
623        Self {
624            basis_shell,
625            primitives,
626            cart_origin,
627            k,
628        }
629    }
630}
631
632#[cfg(feature = "integrals")]
633impl TryFrom<PyBasisShellContraction> for BasisShellContraction<f64, f64> {
634    type Error = anyhow::Error;
635
636    fn try_from(pybsc: PyBasisShellContraction) -> Result<Self, Self::Error> {
637        let (order, cart, shell_order) = pybsc.basis_shell;
638        let basis_shell = create_basis_shell(order, &cart, &shell_order)?;
639        let contraction = GaussianContraction::<f64, f64> {
640            primitives: pybsc.primitives,
641        };
642        let cart_origin = Point3::from_slice(&pybsc.cart_origin);
643        let k = pybsc.k.map(|k| Vector3::from_row_slice(&k));
644        Ok(Self {
645            basis_shell,
646            contraction,
647            cart_origin,
648            k,
649        })
650    }
651}
652
653// ================
654// Helper functions
655// ================
656
657/// Creates a [`BasisShell`] structure from the `(angmom, cart, shell_order)` triplet.
658///
659/// # Arguments
660/// * `order` is an integer indicating the order of the shell,
661/// * `cart` is a boolean indicating if the functions in the shell are Cartesian (`true`)
662/// or pure / solid harmonics (`false`), and
663/// * `shell_order` specifies how the functions in the shell are ordered:
664///   * if `cart` is `true`, `order` can be `None` for lexicographic order, or a list of
665///   tuples `(lx, ly, lz)` specifying a custom order for the Cartesian functions where
666///   `lx`, `ly`, and `lz` are the $`x`$-, $`y`$-, and $`z`$-exponents;
667///   * if `cart` is `false`, `order` can be `true` for increasing-$`m`$ or `false` for
668///   decreasing-$`m`$ order.
669///
670/// # Returns
671///
672/// A [`BasisShell`] structure.
673///
674/// # Errors
675///
676/// Errors if `angmom` is not a valid angular momentum, or if there is a mismatch between `cart`
677/// and `shell_order`.
678fn create_basis_shell(
679    order: u32,
680    shell_type: &ShellType,
681    shell_order: &PyShellOrder,
682) -> Result<BasisShell, anyhow::Error> {
683    let shl_ord = match shell_type {
684        ShellType::Cartesian => {
685            let cart_order = match shell_order {
686                PyShellOrder::CartOrder(cart_tuples_opt) => {
687                    if let Some(cart_tuples) = cart_tuples_opt {
688                        CartOrder::new(cart_tuples)?
689                    } else {
690                        CartOrder::lex(order)
691                    }
692                }
693                PyShellOrder::PureSpinorOrder(_) => {
694                    log::error!(
695                        "Cartesian shell order expected, but specification for pure/spinor shell order found."
696                    );
697                    bail!(
698                        "Cartesian shell order expected, but specification for pure/spinor shell order found."
699                    )
700                }
701            };
702            ShellOrder::Cart(cart_order)
703        }
704        ShellType::Pure => match shell_order {
705            PyShellOrder::PureSpinorOrder(pypureorder) => match pypureorder {
706                PyPureSpinorOrder::Standard((increasingm, _even)) => {
707                    if *increasingm {
708                        ShellOrder::Pure(PureOrder::increasingm(order))
709                    } else {
710                        ShellOrder::Pure(PureOrder::decreasingm(order))
711                    }
712                }
713                PyPureSpinorOrder::Custom((mls, _even)) => ShellOrder::Pure(PureOrder::new(mls)?),
714            },
715            PyShellOrder::CartOrder(_) => {
716                log::error!(
717                    "Pure shell order expected, but specification for Cartesian shell order found."
718                );
719                bail!(
720                    "Pure shell order expected, but specification for Cartesian shell order found."
721                )
722            }
723        },
724        ShellType::SpinorFermion => {
725            match shell_order {
726                PyShellOrder::PureSpinorOrder(pyspinororder) => match pyspinororder {
727                    PyPureSpinorOrder::Standard((increasingm, even)) => {
728                        if *increasingm {
729                            ShellOrder::Spinor(SpinorOrder::increasingm(
730                                order,
731                                *even,
732                                SpinorParticleType::Fermion(None),
733                            ))
734                        } else {
735                            ShellOrder::Spinor(SpinorOrder::decreasingm(
736                                order,
737                                *even,
738                                SpinorParticleType::Fermion(None),
739                            ))
740                        }
741                    }
742                    PyPureSpinorOrder::Custom((two_mjs, even)) => ShellOrder::Spinor(
743                        SpinorOrder::new(two_mjs, *even, SpinorParticleType::Fermion(None))?,
744                    ),
745                },
746                PyShellOrder::CartOrder(_) => {
747                    log::error!(
748                        "Spinor shell order expected, but specification for Cartesian shell order found."
749                    );
750                    bail!(
751                        "Spinor shell order expected, but specification for Cartesian shell order found."
752                    )
753                }
754            }
755        }
756        ShellType::SpinorFermionKineticBalance => match shell_order {
757            PyShellOrder::PureSpinorOrder(pyspinororder) => match pyspinororder {
758                PyPureSpinorOrder::Standard((increasingm, even)) => {
759                    if *increasingm {
760                        ShellOrder::Spinor(SpinorOrder::increasingm(
761                            order,
762                            *even,
763                            SpinorParticleType::Fermion(Some(
764                                SpinorBalanceSymmetry::KineticBalance,
765                            )),
766                        ))
767                    } else {
768                        ShellOrder::Spinor(SpinorOrder::decreasingm(
769                            order,
770                            *even,
771                            SpinorParticleType::Fermion(Some(
772                                SpinorBalanceSymmetry::KineticBalance,
773                            )),
774                        ))
775                    }
776                }
777                PyPureSpinorOrder::Custom((two_mjs, even)) => ShellOrder::Spinor(SpinorOrder::new(
778                    two_mjs,
779                    *even,
780                    SpinorParticleType::Fermion(Some(SpinorBalanceSymmetry::KineticBalance)),
781                )?),
782            },
783            PyShellOrder::CartOrder(_) => {
784                log::error!(
785                    "Spinor shell order expected, but specification for Cartesian shell order found."
786                );
787                bail!(
788                    "Spinor shell order expected, but specification for Cartesian shell order found."
789                )
790            }
791        },
792        ShellType::SpinorAntifermion => {
793            match shell_order {
794                PyShellOrder::PureSpinorOrder(pyspinororder) => match pyspinororder {
795                    PyPureSpinorOrder::Standard((increasingm, even)) => {
796                        if *increasingm {
797                            ShellOrder::Spinor(SpinorOrder::increasingm(
798                                order,
799                                *even,
800                                SpinorParticleType::Antifermion(None),
801                            ))
802                        } else {
803                            ShellOrder::Spinor(SpinorOrder::decreasingm(
804                                order,
805                                *even,
806                                SpinorParticleType::Antifermion(None),
807                            ))
808                        }
809                    }
810                    PyPureSpinorOrder::Custom((two_mjs, even)) => ShellOrder::Spinor(
811                        SpinorOrder::new(two_mjs, *even, SpinorParticleType::Antifermion(None))?,
812                    ),
813                },
814                PyShellOrder::CartOrder(_) => {
815                    log::error!(
816                        "Spinor shell order expected, but specification for Cartesian shell order found."
817                    );
818                    bail!(
819                        "Spinor shell order expected, but specification for Cartesian shell order found."
820                    )
821                }
822            }
823        }
824        ShellType::SpinorAntifermionKineticBalance => match shell_order {
825            PyShellOrder::PureSpinorOrder(pyspinororder) => match pyspinororder {
826                PyPureSpinorOrder::Standard((increasingm, even)) => {
827                    if *increasingm {
828                        ShellOrder::Spinor(SpinorOrder::increasingm(
829                            order,
830                            *even,
831                            SpinorParticleType::Antifermion(Some(
832                                SpinorBalanceSymmetry::KineticBalance,
833                            )),
834                        ))
835                    } else {
836                        ShellOrder::Spinor(SpinorOrder::decreasingm(
837                            order,
838                            *even,
839                            SpinorParticleType::Antifermion(Some(
840                                SpinorBalanceSymmetry::KineticBalance,
841                            )),
842                        ))
843                    }
844                }
845                PyPureSpinorOrder::Custom((two_mjs, even)) => ShellOrder::Spinor(SpinorOrder::new(
846                    two_mjs,
847                    *even,
848                    SpinorParticleType::Antifermion(Some(SpinorBalanceSymmetry::KineticBalance)),
849                )?),
850            },
851            PyShellOrder::CartOrder(_) => {
852                log::error!(
853                    "Spinor shell order expected, but specification for Cartesian shell order found."
854                );
855                bail!(
856                    "Spinor shell order expected, but specification for Cartesian shell order found."
857                )
858            }
859        },
860    };
861    Ok::<_, anyhow::Error>(BasisShell::new(order, shl_ord))
862}
863
864// =================
865// Exposed functions
866// =================
867
868#[cfg(feature = "integrals")]
869#[pyfunction]
870/// Calculates the real-valued two-centre overlap matrix for a basis set.
871///
872/// # Arguments
873///
874/// * `basis_set` - A list of lists of [`PyBasisShellContraction`]. Each inner list contains shells
875/// on one atom.
876///
877/// # Returns
878///
879/// A two-dimensional array containing the real two-centre overlap values.
880///
881/// # Panics
882///
883/// Panics if any shell contains a finite $`\mathbf{k}`$ vector.
884pub fn calc_overlap_2c_real<'py>(
885    py: Python<'py>,
886    basis_set: Vec<Vec<PyBasisShellContraction>>,
887) -> PyResult<Bound<'py, PyArray2<f64>>> {
888    let bscs = BasisSet::new(
889        basis_set
890            .into_iter()
891            .map(|basis_atom| {
892                basis_atom
893                    .into_iter()
894                    .map(|pybsc| BasisShellContraction::<f64, f64>::try_from(pybsc))
895                    .collect::<Result<Vec<_>, _>>()
896            })
897            .collect::<Result<Vec<_>, _>>()
898            .map_err(|err| PyValueError::new_err(err.to_string()))?,
899    );
900    let sao_2c = py.detach(|| {
901        let stc = build_shell_tuple_collection![
902            <s1, s2>;
903            false, false;
904            &bscs, &bscs;
905            f64
906        ];
907        stc.overlap([0, 0])
908            .pop()
909            .expect("Unable to retrieve the two-centre overlap matrix.")
910    });
911    let pysao_2c = sao_2c.into_pyarray(py);
912    Ok(pysao_2c)
913}
914
915#[cfg(feature = "integrals")]
916#[pyfunction]
917/// Calculates the complex-valued two-centre overlap matrix for a basis set.
918///
919/// # Arguments
920///
921/// * `basis_set` - A list of lists of [`PyBasisShellContraction`]. Each inner list contains shells
922/// on one atom.
923/// * `complex_symmetric` - A boolean indicating if the complex-symmetric overlap is to be
924/// calculated.
925///
926/// # Returns
927///
928/// A two-dimensional array containing the complex two-centre overlap values.
929pub fn calc_overlap_2c_complex<'py>(
930    py: Python<'py>,
931    basis_set: Vec<Vec<PyBasisShellContraction>>,
932    complex_symmetric: bool,
933) -> PyResult<Bound<'py, PyArray2<Complex<f64>>>> {
934    let bscs = BasisSet::new(
935        basis_set
936            .into_iter()
937            .map(|basis_atom| {
938                basis_atom
939                    .into_iter()
940                    .map(|pybsc| BasisShellContraction::<f64, f64>::try_from(pybsc))
941                    .collect::<Result<Vec<_>, _>>()
942            })
943            .collect::<Result<Vec<_>, _>>()
944            .map_err(|err| PyValueError::new_err(err.to_string()))?,
945    );
946    let sao_2c = py.detach(|| {
947        let stc = build_shell_tuple_collection![
948            <s1, s2>;
949            !complex_symmetric, false;
950            &bscs, &bscs;
951            Complex<f64>
952        ];
953        stc.overlap([0, 0])
954            .pop()
955            .expect("Unable to retrieve the two-centre overlap matrix.")
956    });
957    let pysao_2c = sao_2c.into_pyarray(py);
958    Ok(pysao_2c)
959}
960
961#[cfg(feature = "integrals")]
962#[pyfunction]
963/// Calculates the real-valued four-centre overlap tensor for a basis set.
964///
965/// # Arguments
966///
967/// * `basis_set` - A list of lists of [`PyBasisShellContraction`]. Each inner list contains shells
968/// on one atom.
969///
970/// # Returns
971///
972/// A four-dimensional array containing the real four-centre overlap values.
973///
974/// # Panics
975///
976/// Panics if any shell contains a finite $`\mathbf{k}`$ vector.
977pub fn calc_overlap_4c_real<'py>(
978    py: Python<'py>,
979    basis_set: Vec<Vec<PyBasisShellContraction>>,
980) -> PyResult<Bound<'py, PyArray4<f64>>> {
981    let bscs = BasisSet::new(
982        basis_set
983            .into_iter()
984            .map(|basis_atom| {
985                basis_atom
986                    .into_iter()
987                    .map(|pybsc| BasisShellContraction::<f64, f64>::try_from(pybsc))
988                    .collect::<Result<Vec<_>, _>>()
989            })
990            .collect::<Result<Vec<_>, _>>()
991            .map_err(|err| PyValueError::new_err(err.to_string()))?,
992    );
993    let sao_4c = py.detach(|| {
994        let stc = build_shell_tuple_collection![
995            <s1, s2, s3, s4>;
996            false, false, false, false;
997            &bscs, &bscs, &bscs, &bscs;
998            f64
999        ];
1000        stc.overlap([0, 0, 0, 0])
1001            .pop()
1002            .expect("Unable to retrieve the four-centre overlap tensor.")
1003    });
1004    let pysao_4c = sao_4c.into_pyarray(py);
1005    Ok(pysao_4c)
1006}
1007
1008#[cfg(feature = "integrals")]
1009#[pyfunction]
1010/// Calculates the complex-valued four-centre overlap tensor for a basis set.
1011///
1012/// # Arguments
1013///
1014/// * `basis_set` - A list of lists of [`PyBasisShellContraction`]. Each inner list contains shells
1015/// on one atom.
1016/// * `complex_symmetric` - A boolean indicating if the complex-symmetric overlap tensor is to be
1017/// calculated.
1018///
1019/// # Returns
1020///
1021/// A four-dimensional array containing the complex four-centre overlap values.
1022pub fn calc_overlap_4c_complex<'py>(
1023    py: Python<'py>,
1024    basis_set: Vec<Vec<PyBasisShellContraction>>,
1025    complex_symmetric: bool,
1026) -> PyResult<Bound<'py, PyArray4<Complex<f64>>>> {
1027    let bscs = BasisSet::new(
1028        basis_set
1029            .into_iter()
1030            .map(|basis_atom| {
1031                basis_atom
1032                    .into_iter()
1033                    .map(|pybsc| BasisShellContraction::<f64, f64>::try_from(pybsc))
1034                    .collect::<Result<Vec<_>, _>>()
1035            })
1036            .collect::<Result<Vec<_>, _>>()
1037            .map_err(|err| PyValueError::new_err(err.to_string()))?,
1038    );
1039    let sao_4c = py.detach(|| {
1040        let stc = build_shell_tuple_collection![
1041            <s1, s2, s3, s4>;
1042            !complex_symmetric, !complex_symmetric, false, false;
1043            &bscs, &bscs, &bscs, &bscs;
1044            Complex<f64>
1045        ];
1046        stc.overlap([0, 0, 0, 0])
1047            .pop()
1048            .expect("Unable to retrieve the four-centre overlap tensor.")
1049    });
1050    let pysao_4c = sao_4c.into_pyarray(py);
1051    Ok(pysao_4c)
1052}