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