Skip to main content

qsym2/target/noci/
multideterminants.rs

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