Skip to main content

qsym2/target/noci/
multideterminant.rs

1//! Individual multi-determinant wavefunctions for configuration interaction.
2
3use std::collections::HashSet;
4use std::fmt::{self, LowerExp};
5use std::hash::Hash;
6use std::marker::PhantomData;
7
8use anyhow::{ensure, format_err};
9use derive_builder::Builder;
10use itertools::Itertools;
11use log;
12use ndarray::{Array1, Array2, ArrayView2, ArrayView4, Axis, Ix0, Ix2, ScalarOperand};
13use ndarray_einsum::*;
14use ndarray_linalg::types::Lapack;
15use num_complex::ComplexFloat;
16use rayon::prelude::*;
17
18use crate::analysis::Overlap;
19use crate::angmom::spinor_rotation_3d::StructureConstraint;
20use crate::group::GroupProperties;
21use crate::symmetry::symmetry_group::SymmetryGroupProperties;
22use crate::symmetry::symmetry_transformation::SymmetryTransformable;
23use crate::target::determinant::SlaterDeterminant;
24use crate::target::noci::backend::matelem::OrbitMatrix;
25use crate::target::noci::backend::matelem::hamiltonian::HamiltonianAO;
26use crate::target::noci::backend::matelem::overlap::OverlapAO;
27use crate::target::noci::backend::nonortho::{calc_lowdin_pairing, calc_transition_density_matrix};
28use crate::target::noci::basis::{EagerBasis, OrbitBasis};
29
30use super::basis::Basis;
31
32#[path = "multideterminant_transformation.rs"]
33pub(crate) mod multideterminant_transformation;
34
35#[path = "multideterminant_analysis.rs"]
36pub(crate) mod multideterminant_analysis;
37
38#[cfg(test)]
39#[path = "multideterminant_tests.rs"]
40mod multideterminant_tests;
41
42// ------------------
43// Struct definitions
44// ------------------
45
46/// Structure to manage individual multi-determinantal wavefunctions.
47#[derive(Builder, Clone)]
48#[builder(build_fn(validate = "Self::validate"))]
49pub struct MultiDeterminant<'a, T, B, SC>
50where
51    T: ComplexFloat + Lapack,
52    SC: StructureConstraint + Hash + Eq + fmt::Display,
53    B: Basis<Item = SlaterDeterminant<'a, T, SC>> + Clone,
54{
55    #[builder(setter(skip), default = "PhantomData")]
56    _lifetime: PhantomData<&'a ()>,
57
58    #[builder(setter(skip), default = "PhantomData")]
59    _structure_constraint: PhantomData<SC>,
60
61    /// A boolean indicating if inner products involving this wavefunction should be the
62    /// complex-symmetric bilinear form, rather than the conventional Hermitian sesquilinear form.
63    #[builder(setter(skip), default = "self.complex_symmetric_from_basis()?")]
64    complex_symmetric: bool,
65
66    /// A boolean indicating if the wavefunction has been acted on by an antiunitary operation. This
67    /// is so that the correct metric can be used during overlap evaluation.
68    #[builder(default = "false")]
69    complex_conjugated: bool,
70
71    /// The basis of Slater determinants in which this multi-determinantal wavefunction is defined.
72    basis: B,
73
74    /// The linear combination coefficients of the elements in the multi-orbit to give this
75    /// multi-determinant wavefunction.
76    coefficients: Array1<T>,
77
78    /// The energy of this multi-determinantal wavefunction.
79    #[builder(
80        default = "Err(\"Multi-determinantal wavefunction energy not yet set.\".to_string())"
81    )]
82    energy: Result<T, String>,
83
84    /// The threshold for comparing wavefunctions.
85    threshold: <T as ComplexFloat>::Real,
86}
87
88// ----------------------
89// Struct implementations
90// ----------------------
91
92impl<'a, T, B, SC> MultiDeterminantBuilder<'a, T, B, SC>
93where
94    T: ComplexFloat + Lapack,
95    SC: StructureConstraint + Hash + Eq + Clone + fmt::Display,
96    B: Basis<Item = SlaterDeterminant<'a, T, SC>> + Clone,
97{
98    fn validate(&self) -> Result<(), String> {
99        let basis = self.basis.as_ref().ok_or("No basis found.".to_string())?;
100        let coefficients = self
101            .coefficients
102            .as_ref()
103            .ok_or("No coefficients found.".to_string())?;
104        let nbasis = basis.n_items() == coefficients.len();
105        if !nbasis {
106            log::error!(
107                "The number of coefficients does not match the number of basis determinants."
108            );
109        }
110
111        let complex_symmetric = basis
112            .iter()
113            .map(|det_res| det_res.map(|det| det.complex_symmetric()))
114            .collect::<Result<HashSet<_>, _>>()
115            .map_err(|err| err.to_string())?
116            .len()
117            == 1;
118        if !complex_symmetric {
119            log::error!("Inconsistent complex-symmetric flag across basis determinants.");
120        }
121
122        let structcons_check = basis
123            .iter()
124            .map(|det_res| det_res.map(|det| det.structure_constraint().clone()))
125            .collect::<Result<HashSet<_>, _>>()
126            .map_err(|err| err.to_string())?
127            .len()
128            == 1;
129        if !structcons_check {
130            log::error!("Inconsistent spin constraints across basis determinants.");
131        }
132
133        if nbasis && structcons_check && complex_symmetric {
134            Ok(())
135        } else {
136            Err("Multi-determinant wavefunction validation failed.".to_string())
137        }
138    }
139
140    /// Retrieves the consistent complex-symmetric flag from the basis determinants.
141    fn complex_symmetric_from_basis(&self) -> Result<bool, String> {
142        let basis = self.basis.as_ref().ok_or("No basis found.".to_string())?;
143        let complex_symmetric_set = basis
144            .iter()
145            .map(|det_res| det_res.map(|det| det.complex_symmetric()))
146            .collect::<Result<HashSet<_>, _>>()
147            .map_err(|err| err.to_string())?;
148        if complex_symmetric_set.len() == 1 {
149            complex_symmetric_set
150                .into_iter()
151                .next()
152                .ok_or("Unable to retrieve the complex-symmetric flag from the basis.".to_string())
153        } else {
154            Err("Inconsistent complex-symmetric flag across basis determinants.".to_string())
155        }
156    }
157}
158
159impl<'a, T, B, SC> MultiDeterminant<'a, T, B, SC>
160where
161    T: ComplexFloat + Lapack,
162    SC: StructureConstraint + Hash + Eq + Clone + fmt::Display,
163    B: Basis<Item = SlaterDeterminant<'a, T, SC>> + Clone,
164{
165    /// Returns a builder to construct a new [`MultiDeterminant`].
166    pub fn builder() -> MultiDeterminantBuilder<'a, T, B, SC> {
167        MultiDeterminantBuilder::default()
168    }
169
170    /// Returns the structure constraint of the multi-determinantal wavefunction.
171    pub fn structure_constraint(&self) -> SC {
172        self.basis
173            .iter()
174            .next()
175            .expect("No basis determinant found.")
176            .expect("No basis determinant found.")
177            .structure_constraint()
178            .clone()
179    }
180}
181
182impl<'a, T, B, SC> MultiDeterminant<'a, T, B, SC>
183where
184    T: ComplexFloat + Lapack,
185    SC: StructureConstraint + Hash + Eq + fmt::Display,
186    B: Basis<Item = SlaterDeterminant<'a, T, SC>> + Clone,
187{
188    /// Returns the complex-conjugated flag of the multi-determinantal wavefunction.
189    pub fn complex_conjugated(&self) -> bool {
190        self.complex_conjugated
191    }
192
193    /// Returns the complex-symmetric flag of the multi-determinantal wavefunction.
194    pub fn complex_symmetric(&self) -> bool {
195        self.complex_symmetric
196    }
197
198    /// Returns the basis of determinants in which this multi-determinantal wavefunction is
199    /// defined.
200    pub fn basis(&self) -> &B {
201        &self.basis
202    }
203
204    /// Returns the coefficients of the basis determinants constituting this multi-determinantal
205    /// wavefunction.
206    pub fn coefficients(&self) -> &Array1<T> {
207        &self.coefficients
208    }
209
210    /// Returns the energy of the multi-determinantal wavefunction.
211    pub fn energy(&self) -> Result<&T, &String> {
212        self.energy.as_ref()
213    }
214
215    /// Returns the threshold with which multi-determinantal wavefunctions are compared.
216    pub fn threshold(&self) -> <T as ComplexFloat>::Real {
217        self.threshold
218    }
219
220    /// Sets the energy of this multi-determinantal wavefunction.
221    pub(crate) fn set_energy(&mut self, energy: Result<T, String>) {
222        self.energy = energy;
223    }
224}
225
226// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
227// Specific implementations for OrbitBasis
228// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
229
230impl<'a, T, G, SC> MultiDeterminant<'a, T, OrbitBasis<G, SlaterDeterminant<'a, T, SC>>, SC>
231where
232    T: ComplexFloat + Lapack,
233    G: GroupProperties + Clone,
234    SC: StructureConstraint + Hash + Eq + fmt::Display + Clone,
235{
236    /// Converts this multi-determinant with an orbit basis into a multi-determinant with the
237    /// equivalent eager basis.
238    #[allow(clippy::type_complexity)]
239    pub fn to_eager_basis(
240        &self,
241    ) -> Result<MultiDeterminant<'a, T, EagerBasis<SlaterDeterminant<'a, T, SC>>, SC>, anyhow::Error>
242    {
243        MultiDeterminant::<T, EagerBasis<SlaterDeterminant<'a, T, SC>>, SC>::builder()
244            .complex_conjugated(self.complex_conjugated)
245            .basis(self.basis.to_eager()?)
246            .coefficients(self.coefficients().clone())
247            .energy(self.energy.clone())
248            .threshold(self.threshold)
249            .build()
250            .map_err(|err| format_err!(err))
251    }
252}
253
254// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
255// Generic implementation for all Basis
256// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
257
258impl<'a, T, B, SC> MultiDeterminant<'a, T, B, SC>
259where
260    T: ComplexFloat + Lapack + ScalarOperand + Send + Sync,
261    <T as ComplexFloat>::Real: LowerExp + fmt::Display + Sync,
262    SC: StructureConstraint + Hash + Eq + Clone + fmt::Display + Sync,
263    B: Basis<Item = SlaterDeterminant<'a, T, SC>> + Clone + Sync,
264    SlaterDeterminant<'a, T, SC>: Send + Sync,
265{
266    /// Calculates the (contravariant) density matrix $`\mathbf{P}(\hat{\iota})`$ of the
267    /// multi-determinantal wavefunction in the AO basis.
268    ///
269    /// Note that the contravariant density matrix $`\mathbf{P}(\hat{\iota})`$ needs to be converted
270    /// to the mixed form $`\tilde{\mathbf{P}}(\hat{\iota})`$ given by
271    /// ```math
272    ///     \tilde{\mathbf{P}}(\hat{\iota}) = \mathbf{P}(\hat{\iota}) \mathbf{S}_{\mathrm{AO}}
273    /// ```
274    /// before being diagonalised to obtain natural orbitals and their occupation numbers.
275    ///
276    /// # Arguments
277    ///
278    /// * `sao` - The atomic-orbital overlap matrix.
279    /// * `thresh_offdiag` - Threshold for determining non-zero off-diagonal elements in the
280    ///   orbital overlap matrix two Slater determinants during Löwdin pairing.
281    /// * `thresh_zeroov` - Threshold for identifying zero Löwdin overlaps.
282    /// * `normalised_wavefunction` - Boolean indicating whether the computed density matrix needs
283    ///   to be normalised by the squared norm of the wavefunction.
284    pub fn density_matrix(
285        &self,
286        sao: &ArrayView2<T>,
287        thresh_offdiag: <T as ComplexFloat>::Real,
288        thresh_zeroov: <T as ComplexFloat>::Real,
289        normalised_wavefunction: bool,
290    ) -> Result<Array2<T>, anyhow::Error> {
291        let nao = sao.nrows();
292        let dets = self.basis().iter().collect::<Result<Vec<_>, _>>()?;
293        let sqnorm_denmat_res = dets.iter()
294            .zip(self.coefficients().iter())
295            .cartesian_product(dets.iter().zip(self.coefficients().iter()))
296            .par_bridge()
297            .fold(
298                || Ok((T::zero(), Array2::<T>::zeros((nao, nao)))),
299                |acc_res, ((det_w, c_w), (det_x, c_x))| {
300                    ensure!(
301                        det_w.structure_constraint() == det_x.structure_constraint(),
302                        "Inconsistent spin constraints: {} != {}.",
303                        det_w.structure_constraint(),
304                        det_x.structure_constraint(),
305                    );
306
307                    if det_w.complex_symmetric() != det_x.complex_symmetric() {
308                        return Err(format_err!(
309                            "The `complex_symmetric` booleans of the specified determinants do not match: `det_w` (`{}`) != `det_x` (`{}`).",
310                            det_w.complex_symmetric(),
311                            det_x.complex_symmetric(),
312                        ));
313                    }
314                    let complex_symmetric = det_w.complex_symmetric();
315                    let lowdin_paired_coefficientss = det_w
316                        .coefficients()
317                        .iter()
318                        .zip(det_w.occupations().iter())
319                        .zip(det_x.coefficients().iter().zip(det_x.occupations().iter()))
320                        .map(|((cw, occw), (cx, occx))| {
321                            let occw_indices = occw
322                                .iter()
323                                .enumerate()
324                                .filter_map(|(i, occ_i)| {
325                                    if occ_i.abs() >= det_w.threshold() {
326                                        Some(i)
327                                    } else {
328                                        None
329                                    }
330                                })
331                                .collect::<Vec<_>>();
332                            let ne_w = occw_indices.len();
333                            let cw_occ = cw.select(Axis(1), &occw_indices);
334                            let occx_indices = occx
335                                .iter()
336                                .enumerate()
337                                .filter_map(|(i, occ_i)| {
338                                    if occ_i.abs() >= det_x.threshold() {
339                                        Some(i)
340                                    } else {
341                                        None
342                                    }
343                                })
344                                .collect::<Vec<_>>();
345                            let ne_x = occx_indices.len();
346                            ensure!(ne_w == ne_x, "Inconsistent number of electrons: {ne_w} != {ne_x}.");
347                            let cx_occ = cx.select(Axis(1), &occx_indices);
348                            calc_lowdin_pairing(
349                                &cw_occ.view(),
350                                &cx_occ.view(),
351                                sao,
352                                complex_symmetric,
353                                thresh_offdiag,
354                                thresh_zeroov,
355                            )
356                        })
357                        .collect::<Result<Vec<_>, _>>()?;
358                    let den_wx = calc_transition_density_matrix(&lowdin_paired_coefficientss, &self.structure_constraint())?;
359                    let ov_wx = lowdin_paired_coefficientss
360                        .iter()
361                        .flat_map(|lpc| lpc.lowdin_overlaps().iter())
362                        .fold(T::one(), |acc, ov| acc * *ov);
363
364                    let c_w = if self.complex_conjugated() {
365                        if complex_symmetric { c_w.conj() } else { *c_w }
366                    } else if complex_symmetric { *c_w } else { c_w.conj() };
367                    let c_x = if self.complex_conjugated() {
368                        c_x.conj()
369                    } else {
370                        *c_x
371                    };
372                    let den_wx = if self.complex_conjugated() {
373                        den_wx.mapv(|v| v.conj())
374                    } else {
375                        den_wx
376                    };
377                    acc_res.map(|(sqnorm_acc, denmat_acc)| (sqnorm_acc + ov_wx * c_w * c_x, denmat_acc + den_wx * c_w * c_x))
378                },
379            )
380            .reduce(
381                || Ok((T::zero(), Array2::<T>::zeros((nao, nao)))),
382                |sqnorm_denmat_res_a: Result<(T, Array2<T>), anyhow::Error>, sqnorm_denmat_res_b: Result<(T, Array2<T>), anyhow::Error>| {
383                    sqnorm_denmat_res_a.and_then(|(sqnorm_acc, denmat_acc)| sqnorm_denmat_res_b.map(|(sqnorm, denmat)| {
384                        (
385                            sqnorm_acc + sqnorm,
386                            denmat_acc + denmat
387                        )
388                    }))
389                }
390            );
391        sqnorm_denmat_res.map(|(sqnorm, denmat)| {
392            if normalised_wavefunction {
393                denmat / sqnorm
394            } else {
395                denmat
396            }
397        })
398    }
399}
400
401impl<'a, T, G, SC> MultiDeterminant<'a, T, OrbitBasis<G, SlaterDeterminant<'a, T, SC>>, SC>
402where
403    G: SymmetryGroupProperties + Clone,
404    T: ComplexFloat + Lapack + ScalarOperand + Send + Sync,
405    <T as ComplexFloat>::Real: LowerExp + fmt::Display + Sync,
406    SC: StructureConstraint + Hash + Eq + Clone + fmt::Display + Sync,
407    SlaterDeterminant<'a, T, SC>: SymmetryTransformable + Send + Sync,
408    for<'b> SlaterDeterminant<'b, T, SC>: Overlap<T, Ix2>,
409{
410    /// Calculates the energy of the multi-determinantal wavefunction in the orbit basis.
411    ///
412    /// # Arguments
413    ///
414    /// * `sao` - The atomic-orbital overlap matrix.
415    /// * `enuc` - The nuclear repulsion energy.
416    /// * `onee` - The one-electron integral matrix.
417    /// * `twoe` - The optional two-electron integral tensor in physicists' ordering. Either this or
418    ///   `get_jk` is required for the energy computation.
419    /// * `get_jk` - The optional function callable on a density matrix $`\mathbf{D}`$ to calculate
420    ///   the corresponding $`\mathbf{J}`$ and $`\mathbf{K}`$ matrices by contracting with
421    ///   appropriate two-electron integrals calculated on-the-fly. Either this or `twoe` is
422    ///   required for the energy computation.
423    /// * `thresh_offdiag` - Threshold for determining non-zero off-diagonal elements in the
424    ///   orbital overlap matrix two Slater determinants during Löwdin pairing.
425    /// * `thresh_zeroov` - Threshold for identifying zero Löwdin overlaps.
426    /// * `use_cayley_table` - Boolean indicating whether group closure is used in the computation
427    ///   the Hamiltonian and overlap matrices between the elements in the orbit basis.
428    ///
429    /// # Returns
430    ///
431    /// The computed energy.
432    #[allow(clippy::too_many_arguments)]
433    pub fn compute_energy<F>(
434        &self,
435        sao: ArrayView2<'a, T>,
436        enuc: T,
437        onee: ArrayView2<'a, T>,
438        twoe: Option<ArrayView4<'a, T>>,
439        get_jk: Option<F>,
440        thresh_offdiag: <T as ComplexFloat>::Real,
441        thresh_zeroov: <T as ComplexFloat>::Real,
442        use_cayley_table: bool,
443    ) -> Result<T, anyhow::Error>
444    where
445        F: Fn(&Array2<T>) -> Result<(Array2<T>, Array2<T>), anyhow::Error> + Clone + Sync,
446    {
447        let basis = self.basis();
448        let overlap_ao = OverlapAO::<T, SC>::builder()
449            .sao(sao)
450            .build()
451            .map_err(|err| format_err!(err))?;
452        let hamiltonian_ao = HamiltonianAO::<T, SC, _>::builder()
453            .enuc(enuc)
454            .onee(onee)
455            .twoe(twoe)
456            .get_jk(get_jk)
457            .build()
458            .map_err(|err| format_err!(err))?;
459        let smat = overlap_ao.calc_orbit_matrix(
460            basis,
461            use_cayley_table,
462            &sao,
463            thresh_offdiag,
464            thresh_zeroov,
465        )?;
466        let hmat = hamiltonian_ao.calc_orbit_matrix(
467            basis,
468            use_cayley_table,
469            &sao,
470            thresh_offdiag,
471            thresh_zeroov,
472        )?;
473        let sq_norm = einsum(
474            "ij,i,j->",
475            &[
476                &smat.view(),
477                &self.coefficients.mapv(ComplexFloat::conj).view(),
478                &self.coefficients.view(),
479            ],
480        )
481        .map_err(|err| format_err!(err))?
482        .into_dimensionality::<Ix0>()
483        .map_err(|err| format_err!(err))?
484        .into_scalar();
485        let psi_h_psi = einsum(
486            "ij,i,j->",
487            &[
488                &hmat.view(),
489                &self.coefficients.mapv(ComplexFloat::conj).view(),
490                &self.coefficients.view(),
491            ],
492        )
493        .map_err(|err| format_err!(err))?
494        .into_dimensionality::<Ix0>()
495        .map_err(|err| format_err!(err))?
496        .into_scalar();
497        if ComplexFloat::abs(sq_norm) >= thresh_zeroov {
498            Ok(psi_h_psi / sq_norm)
499        } else {
500            Err(format_err!(
501                "The multi-determinantal wavefunction has near-zero squared norm: |{sq_norm:+.3e}| < {thresh_zeroov:.3e}."
502            ))
503        }
504    }
505}
506
507// -----
508// Debug
509// -----
510impl<'a, T, B, SC> fmt::Debug for MultiDeterminant<'a, T, B, SC>
511where
512    T: ComplexFloat + Lapack,
513    SC: StructureConstraint + Hash + Eq + fmt::Display,
514    B: Basis<Item = SlaterDeterminant<'a, T, SC>> + Clone,
515{
516    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517        write!(
518            f,
519            "MultiDeterminant over {} basis Slater determinants",
520            self.coefficients.len(),
521        )?;
522        Ok(())
523    }
524}
525
526// -------
527// Display
528// -------
529impl<'a, T, B, SC> fmt::Display for MultiDeterminant<'a, T, B, SC>
530where
531    T: ComplexFloat + Lapack,
532    SC: StructureConstraint + Hash + Eq + fmt::Display,
533    B: Basis<Item = SlaterDeterminant<'a, T, SC>> + Clone,
534{
535    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
536        write!(
537            f,
538            "MultiDeterminant over {} basis Slater determinants",
539            self.coefficients.len(),
540        )?;
541        Ok(())
542    }
543}