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