qsym2/target/orbital/
mod.rs

1//! Orbitals.
2
3use std::collections::HashSet;
4use std::fmt;
5
6use anyhow::{self, ensure, format_err};
7use approx;
8use derive_builder::Builder;
9use ndarray::{Array1, Array2, Ix2, s};
10use ndarray_einsum::*;
11use ndarray_linalg::types::Lapack;
12use num_complex::{Complex, ComplexFloat};
13use num_traits::float::{Float, FloatConst};
14
15use crate::angmom::spinor_rotation_3d::{SpinConstraint, SpinOrbitCoupled, StructureConstraint};
16use crate::auxiliary::molecule::Molecule;
17use crate::basis::ao::BasisAngularOrder;
18use crate::target::density::Density;
19
20#[cfg(test)]
21mod orbital_tests;
22
23pub mod orbital_analysis;
24pub mod orbital_projection;
25mod orbital_transformation;
26
27// ==================
28// Struct definitions
29// ==================
30
31/// Structure to manage molecular orbitals. Each molecular orbital is essentially a one-electron
32/// Slater determinant.
33#[derive(Builder, Clone)]
34#[builder(build_fn(validate = "Self::validate"))]
35pub struct MolecularOrbital<'a, T, SC>
36where
37    T: ComplexFloat + Lapack,
38    SC: StructureConstraint,
39{
40    /// The structure constraint associated with the coefficients describing this molecular orbital.
41    structure_constraint: SC,
42
43    /// If the structure constraint allows for multiple components, this gives the component index of
44    /// this molecular orbital.
45    component_index: usize,
46
47    /// A boolean indicating if the orbital has been acted on by an antiunitary operation. This is
48    /// so that the correct metric can be used during overlap evaluation.
49    #[builder(default = "false")]
50    complex_conjugated: bool,
51
52    /// The angular order of the basis functions with respect to which the coefficients are
53    /// expressed. Each [`BasisAngularOrder`] corresponds to one explicit component in the
54    /// coefficient matrix (see [`StructureConstraint::n_explicit_comps_per_coefficient_matrix`]).
55    baos: Vec<&'a BasisAngularOrder<'a>>,
56
57    /// A boolean indicating if inner products involving this molecular orbital should be the
58    /// complex-symmetric bilinear form, rather than the conventional Hermitian sesquilinear form.
59    complex_symmetric: bool,
60
61    /// The associated molecule.
62    mol: &'a Molecule,
63
64    /// The coefficients describing this molecular orbital.
65    coefficients: Array1<T>,
66
67    /// The energy of this molecular orbital.
68    #[builder(default = "None")]
69    energy: Option<T>,
70
71    /// The threshold for comparing determinants.
72    threshold: <T as ComplexFloat>::Real,
73}
74
75impl<'a, T, SC> MolecularOrbitalBuilder<'a, T, SC>
76where
77    T: ComplexFloat + Lapack,
78    SC: StructureConstraint,
79{
80    fn validate(&self) -> Result<(), String> {
81        let structcons = self
82            .structure_constraint
83            .as_ref()
84            .ok_or("No structure constraints found.".to_string())?;
85        let baos = self
86            .baos
87            .as_ref()
88            .ok_or("No `BasisAngularOrder`s found.".to_string())?;
89        let baos_length_check = baos.len() == structcons.n_explicit_comps_per_coefficient_matrix();
90        if !baos_length_check {
91            log::error!(
92                "The number of `BasisAngularOrder`s provided does not match the number of explicit components per coefficient matrix."
93            );
94        }
95
96        let nbas_tot = baos.iter().map(|bao| bao.n_funcs()).sum::<usize>();
97        let coefficients = self
98            .coefficients
99            .as_ref()
100            .ok_or("No coefficients found.".to_string())?;
101
102        let coefficients_shape_check = {
103            let nrows = nbas_tot;
104            if !coefficients.shape()[0] == nrows {
105                log::error!(
106                    "Unexpected shapes of coefficient vector: {} {} expected, but {} found.",
107                    nrows,
108                    if nrows == 1 { "row" } else { "rows" },
109                    coefficients.shape()[0],
110                );
111                false
112            } else {
113                true
114            }
115        };
116
117        let mol = self.mol.ok_or("No molecule found.".to_string())?;
118        let natoms_set = baos.iter().map(|bao| bao.n_atoms()).collect::<HashSet<_>>();
119        if natoms_set.len() != 1 {
120            return Err("Inconsistent numbers of atoms between `BasisAngularOrder`s of different explicit components.".to_string());
121        };
122        let n_atoms = natoms_set.iter().next().ok_or_else(|| {
123            "Unable to retrieve the number of atoms from the `BasisAngularOrder`s.".to_string()
124        })?;
125        let natoms_check = mol.atoms.len() == *n_atoms;
126        if !natoms_check {
127            log::error!(
128                "The number of atoms in the molecule does not match the number of local sites in the basis."
129            );
130        }
131
132        if baos_length_check && coefficients_shape_check && natoms_check {
133            Ok(())
134        } else {
135            Err(format!(
136                "Molecular orbital validation failed:
137                    baos_length ({baos_length_check}),
138                    coefficients_shape ({coefficients_shape_check}),
139                    natoms ({natoms_check})."
140            ))
141        }
142    }
143}
144
145impl<'a, T, SC> MolecularOrbital<'a, T, SC>
146where
147    T: ComplexFloat + Clone + Lapack,
148    SC: StructureConstraint + Clone,
149{
150    /// Returns a builder to construct a new [`MolecularOrbital`].
151    pub fn builder() -> MolecularOrbitalBuilder<'a, T, SC> {
152        MolecularOrbitalBuilder::default()
153    }
154}
155
156impl<'a, T, SC> MolecularOrbital<'a, T, SC>
157where
158    T: ComplexFloat + Clone + Lapack,
159    SC: StructureConstraint,
160{
161    /// Returns a shared reference to the coefficient array.
162    pub fn coefficients(&self) -> &Array1<T> {
163        &self.coefficients
164    }
165
166    /// Returns a shared reference to the structure constraint.
167    pub fn structure_constraint(&self) -> &SC {
168        &self.structure_constraint
169    }
170
171    /// Returns a shared reference to the [`BasisAngularOrder`] description of the basis sets in
172    /// which the orbital coefficients are written.
173    pub fn baos(&'_ self) -> &Vec<&'_ BasisAngularOrder<'_>> {
174        &self.baos
175    }
176
177    /// Returns the molecule associated with this molecular orbital.
178    pub fn mol(&self) -> &Molecule {
179        self.mol
180    }
181
182    /// Returns the complex-symmetric flag of the molecular orbital.
183    pub fn complex_symmetric(&self) -> bool {
184        self.complex_symmetric
185    }
186
187    /// Returns the energy of this molecular orbital.
188    pub fn energy(&self) -> Option<T> {
189        self.energy
190    }
191
192    /// Returns the threshold with which molecular orbitals are compared.
193    pub fn threshold(&self) -> <T as ComplexFloat>::Real {
194        self.threshold
195    }
196}
197
198impl<'a, T> MolecularOrbital<'a, T, SpinConstraint>
199where
200    T: ComplexFloat + Clone + Lapack,
201{
202    /// Augments the encoding of coefficients in this molecular orbital to that in the
203    /// corresponding generalised spin constraint.
204    ///
205    /// # Returns
206    ///
207    /// The equivalent molecular orbital with the coefficients encoded in the generalised spin
208    /// constraint.
209    pub fn to_generalised(&self) -> Self {
210        match self.structure_constraint {
211            SpinConstraint::Restricted(n) => {
212                let bao = self.baos[0];
213                let nbas = bao.n_funcs();
214
215                let cr = &self.coefficients;
216                let mut cg = Array1::<T>::zeros(nbas * usize::from(n));
217                let start = nbas * self.component_index;
218                let end = nbas * (self.component_index + 1);
219                cg.slice_mut(s![start..end]).assign(cr);
220                Self::builder()
221                    .coefficients(cg)
222                    .baos((0..n).map(|_| bao).collect::<Vec<_>>())
223                    .mol(self.mol)
224                    .structure_constraint(SpinConstraint::Generalised(n, false))
225                    .component_index(0)
226                    .complex_symmetric(self.complex_symmetric)
227                    .threshold(self.threshold)
228                    .build()
229                    .expect("Unable to construct a generalised molecular orbital.")
230            }
231            SpinConstraint::Unrestricted(n, increasingm) => {
232                let bao = self.baos[0];
233                let nbas = bao.n_funcs();
234
235                let cr = &self.coefficients;
236                let mut cg = Array1::<T>::zeros(nbas * usize::from(n));
237                let start = nbas * self.component_index;
238                let end = nbas * (self.component_index + 1);
239                cg.slice_mut(s![start..end]).assign(cr);
240                Self::builder()
241                    .coefficients(cg)
242                    .baos((0..n).map(|_| bao).collect::<Vec<_>>())
243                    .mol(self.mol)
244                    .structure_constraint(SpinConstraint::Generalised(n, increasingm))
245                    .component_index(0)
246                    .complex_symmetric(self.complex_symmetric)
247                    .threshold(self.threshold)
248                    .build()
249                    .expect("Unable to construct a generalised molecular orbital.")
250            }
251            SpinConstraint::Generalised(_, _) => self.clone(),
252        }
253    }
254}
255
256impl<'a> MolecularOrbital<'a, f64, SpinConstraint> {
257    /// Constructs the total density of the molecular orbital.
258    pub fn to_total_density(&'a self) -> Result<Density<'a, f64>, anyhow::Error> {
259        match self.structure_constraint {
260            SpinConstraint::Restricted(nspins) => {
261                let denmat = f64::from(nspins)
262                    * einsum(
263                        "m,n->mn",
264                        &[&self.coefficients.view(), &self.coefficients.view()],
265                    )
266                    .expect("Unable to construct a density matrix from the coefficient matrix.")
267                    .into_dimensionality::<Ix2>()
268                    .expect("Unable to convert the resultant density matrix to two dimensions.");
269                Density::<f64>::builder()
270                    .density_matrix(denmat)
271                    .bao(self.baos()[0])
272                    .mol(self.mol())
273                    .complex_symmetric(self.complex_symmetric())
274                    .threshold(self.threshold())
275                    .build()
276                    .map_err(|err| format_err!(err))
277            }
278            SpinConstraint::Unrestricted(_, _) => {
279                let denmat = einsum(
280                    "m,n->mn",
281                    &[&self.coefficients.view(), &self.coefficients.view()],
282                )
283                .expect("Unable to construct a density matrix from the coefficient matrix.")
284                .into_dimensionality::<Ix2>()
285                .expect("Unable to convert the resultant density matrix to two dimensions.");
286                Density::<f64>::builder()
287                    .density_matrix(denmat)
288                    .bao(self.baos()[0])
289                    .mol(self.mol())
290                    .complex_symmetric(self.complex_symmetric())
291                    .threshold(self.threshold())
292                    .build()
293                    .map_err(|err| format_err!(err))
294            }
295            SpinConstraint::Generalised(nspins, _) => {
296                let full_denmat = einsum(
297                    "m,n->mn",
298                    &[&self.coefficients.view(), &self.coefficients.view()],
299                )
300                .expect("Unable to construct a density matrix from the coefficient matrix.")
301                .into_dimensionality::<Ix2>()
302                .expect("Unable to convert the resultant density matrix to two dimensions.");
303
304                let nspatial_set = self
305                    .baos()
306                    .iter()
307                    .map(|bao| bao.n_funcs())
308                    .collect::<HashSet<_>>();
309                ensure!(
310                    nspatial_set.len() == 1,
311                    "Mismatched numbers of basis functions between the explicit components."
312                );
313                let nspatial = *nspatial_set.iter().next().ok_or_else(|| {
314                    format_err!(
315                        "Unable to extract the number of basis functions per explicit component."
316                    )
317                })?;
318
319                let denmat = (0..usize::from(nspins)).fold(
320                    Array2::<f64>::zeros((nspatial, nspatial)),
321                    |acc, ispin| {
322                        acc + full_denmat.slice(s![
323                            ispin * nspatial..(ispin + 1) * nspatial,
324                            ispin * nspatial..(ispin + 1) * nspatial
325                        ])
326                    },
327                );
328                Density::<f64>::builder()
329                    .density_matrix(denmat)
330                    .bao(self.baos()[0])
331                    .mol(self.mol())
332                    .complex_symmetric(self.complex_symmetric())
333                    .threshold(self.threshold())
334                    .build()
335                    .map_err(|err| format_err!(err))
336            }
337        }
338    }
339}
340
341impl<'a, T> MolecularOrbital<'a, Complex<T>, SpinConstraint>
342where
343    T: Float + FloatConst + Lapack + From<u16>,
344    Complex<T>: Lapack,
345{
346    /// Constructs the total density of the molecular orbital.
347    pub fn to_total_density(&'a self) -> Result<Density<'a, Complex<T>>, anyhow::Error> {
348        match self.structure_constraint {
349            SpinConstraint::Restricted(nspins) => {
350                let nspins_t = Complex::<T>::from(<T as From<u16>>::from(nspins));
351                let denmat = einsum(
352                    "m,n->mn",
353                    &[
354                        &self.coefficients.view(),
355                        &self.coefficients.map(Complex::conj).view(),
356                    ],
357                )
358                .expect("Unable to construct a density matrix from the coefficient matrix.")
359                .into_dimensionality::<Ix2>()
360                .expect("Unable to convert the resultant density matrix to two dimensions.")
361                .map(|x| x * nspins_t);
362                Density::<Complex<T>>::builder()
363                    .density_matrix(denmat)
364                    .bao(self.baos()[0])
365                    .mol(self.mol())
366                    .complex_symmetric(self.complex_symmetric())
367                    .threshold(self.threshold())
368                    .build()
369                    .map_err(|err| format_err!(err))
370            }
371            SpinConstraint::Unrestricted(_, _) => {
372                let denmat = einsum(
373                    "m,n->mn",
374                    &[
375                        &self.coefficients.view(),
376                        &self.coefficients.map(Complex::conj).view(),
377                    ],
378                )
379                .expect("Unable to construct a density matrix from the coefficient matrix.")
380                .into_dimensionality::<Ix2>()
381                .expect("Unable to convert the resultant density matrix to two dimensions.");
382                Density::<Complex<T>>::builder()
383                    .density_matrix(denmat)
384                    .bao(self.baos()[0])
385                    .mol(self.mol())
386                    .complex_symmetric(self.complex_symmetric())
387                    .threshold(self.threshold())
388                    .build()
389                    .map_err(|err| format_err!(err))
390            }
391            SpinConstraint::Generalised(nspins, _) => {
392                let full_denmat = einsum(
393                    "m,n->mn",
394                    &[
395                        &self.coefficients.view(),
396                        &self.coefficients.map(Complex::conj).view(),
397                    ],
398                )
399                .expect("Unable to construct a density matrix from the coefficient matrix.")
400                .into_dimensionality::<Ix2>()
401                .expect("Unable to convert the resultant density matrix to two dimensions.");
402
403                let nspatial_set = self
404                    .baos()
405                    .iter()
406                    .map(|bao| bao.n_funcs())
407                    .collect::<HashSet<_>>();
408                ensure!(
409                    nspatial_set.len() == 1,
410                    "Mismatched numbers of basis functions between the explicit components in the generalised spin constraint."
411                );
412                let nspatial = *nspatial_set.iter().next().ok_or_else(|| {
413                    format_err!(
414                        "Unable to extract the number of basis functions per explicit component."
415                    )
416                })?;
417
418                let denmat = (0..usize::from(nspins)).fold(
419                    Array2::<Complex<T>>::zeros((nspatial, nspatial)),
420                    |acc, ispin| {
421                        acc + full_denmat.slice(s![
422                            ispin * nspatial..(ispin + 1) * nspatial,
423                            ispin * nspatial..(ispin + 1) * nspatial
424                        ])
425                    },
426                );
427                Density::<Complex<T>>::builder()
428                    .density_matrix(denmat)
429                    .bao(self.baos()[0])
430                    .mol(self.mol())
431                    .complex_symmetric(self.complex_symmetric())
432                    .threshold(self.threshold())
433                    .build()
434                    .map_err(|err| format_err!(err))
435            }
436        }
437    }
438}
439
440impl<'a, T> MolecularOrbital<'a, Complex<T>, SpinOrbitCoupled>
441where
442    T: Float + FloatConst + Lapack + From<u16>,
443    Complex<T>: Lapack,
444{
445    /// Constructs the total density of the molecular orbital.
446    pub fn to_total_density(&'a self) -> Result<Density<'a, Complex<T>>, anyhow::Error> {
447        Err(format_err!(
448            "The total density of a spin--orbit-coupled molecular orbital is not implemented."
449        ))
450        // match self.structure_constraint {
451        //     SpinOrbitCoupled::JAdapted(ncomps) => {
452        //         let full_denmat = einsum(
453        //             "m,n->mn",
454        //             &[
455        //                 &self.coefficients.view(),
456        //                 &self.coefficients.map(Complex::conj).view(),
457        //             ],
458        //         )
459        //         .expect("Unable to construct a density matrix from the coefficient matrix.")
460        //         .into_dimensionality::<Ix2>()
461        //         .expect("Unable to convert the resultant density matrix to two dimensions.");
462        //
463        //         let nfuncs_per_comp_set = self
464        //             .baos()
465        //             .iter()
466        //             .map(|bao| bao.n_funcs())
467        //             .collect::<HashSet<_>>();
468        //         ensure!(
469        //             nfuncs_per_comp_set.len() == 1,
470        //             "Mismatched numbers of basis functions between the explicit components."
471        //         );
472        //         let nfuncs_per_comp = *nfuncs_per_comp_set.iter().next().ok_or_else(|| {
473        //             format_err!(
474        //                 "Unable to extract the number of basis functions per explicit component."
475        //             )
476        //         })?;
477        //
478        //         let denmat = (0..usize::from(ncomps)).fold(
479        //             Array2::<Complex<T>>::zeros((nfuncs_per_comp, nfuncs_per_comp)),
480        //             |acc, icomp| {
481        //                 acc + full_denmat.slice(s![
482        //                     icomp * nfuncs_per_comp..(icomp + 1) * nfuncs_per_comp,
483        //                     icomp * nfuncs_per_comp..(icomp + 1) * nfuncs_per_comp
484        //                 ])
485        //             },
486        //         );
487        //         Density::<Complex<T>>::builder()
488        //             .density_matrix(denmat)
489        //             .bao(self.baos()[0])
490        //             .mol(self.mol())
491        //             .complex_symmetric(self.complex_symmetric())
492        //             .threshold(self.threshold())
493        //             .build()
494        //             .map_err(|err| format_err!(err))
495        //     }
496        // }
497    }
498}
499
500// =====================
501// Trait implementations
502// =====================
503
504// ----
505// From
506// ----
507impl<'a, T, SC> From<MolecularOrbital<'a, T, SC>> for MolecularOrbital<'a, Complex<T>, SC>
508where
509    T: Float + FloatConst + Lapack,
510    Complex<T>: Lapack,
511    SC: StructureConstraint + Clone,
512{
513    fn from(value: MolecularOrbital<'a, T, SC>) -> Self {
514        MolecularOrbital::<'a, Complex<T>, SC>::builder()
515            .coefficients(value.coefficients.map(Complex::from))
516            .baos(value.baos.clone())
517            .mol(value.mol)
518            .structure_constraint(value.structure_constraint)
519            .component_index(value.component_index)
520            .complex_symmetric(value.complex_symmetric)
521            .threshold(value.threshold)
522            .build()
523            .expect("Unable to construct a complex molecular orbital.")
524    }
525}
526
527// ---------
528// PartialEq
529// ---------
530impl<'a, T, SC> PartialEq for MolecularOrbital<'a, T, SC>
531where
532    T: ComplexFloat<Real = f64> + Lapack,
533    SC: StructureConstraint + PartialEq,
534{
535    fn eq(&self, other: &Self) -> bool {
536        let thresh = (self.threshold * other.threshold).sqrt();
537        let coefficients_eq = approx::relative_eq!(
538            (&self.coefficients - &other.coefficients)
539                .map(|x| ComplexFloat::abs(*x).powi(2))
540                .sum()
541                .sqrt(),
542            0.0,
543            epsilon = thresh,
544            max_relative = thresh,
545        );
546        self.structure_constraint == other.structure_constraint
547            && self.component_index == other.component_index
548            && self.baos == other.baos
549            && self.mol == other.mol
550            && coefficients_eq
551    }
552}
553
554// --
555// Eq
556// --
557impl<'a, T, SC> Eq for MolecularOrbital<'a, T, SC>
558where
559    T: ComplexFloat<Real = f64> + Lapack,
560    SC: StructureConstraint + Eq,
561{
562}
563
564// -----
565// Debug
566// -----
567impl<'a, T, SC> fmt::Debug for MolecularOrbital<'a, T, SC>
568where
569    T: fmt::Debug + ComplexFloat + Lapack,
570    SC: StructureConstraint + fmt::Debug,
571{
572    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
573        write!(
574            f,
575            "MolecularOrbital[{:?} (spin index {}): coefficient array of length {}]",
576            self.structure_constraint,
577            self.component_index,
578            self.coefficients.len()
579        )?;
580        Ok(())
581    }
582}
583
584// -------
585// Display
586// -------
587impl<'a, T, SC> fmt::Display for MolecularOrbital<'a, T, SC>
588where
589    T: fmt::Display + ComplexFloat + Lapack,
590    SC: StructureConstraint + fmt::Display,
591{
592    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593        write!(
594            f,
595            "MolecularOrbital[{} (spin index {}): coefficient array of length {}]",
596            self.structure_constraint,
597            self.component_index,
598            self.coefficients.len()
599        )?;
600        Ok(())
601    }
602}