Skip to main content

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