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