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