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