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}