1use 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#[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 #[builder(setter(skip), default = "self.complex_symmetric_from_basis()?")]
72 complex_symmetric: bool,
73
74 #[builder(default = "false")]
78 complex_conjugated: bool,
79
80 basis: B,
83
84 coefficients: Array2<T>,
88
89 #[builder(default = "self.default_energies()?")]
91 energies: Array1<Result<T, String>>,
92
93 threshold: <T as ComplexFloat>::Real,
95}
96
97impl<'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 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 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 pub fn builder() -> MultiDeterminantsBuilder<'a, T, B, SC> {
189 MultiDeterminantsBuilder::default()
190 }
191
192 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 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 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 pub fn complex_conjugated(&self) -> bool {
294 self.complex_conjugated
295 }
296
297 pub fn complex_symmetric(&self) -> bool {
300 self.complex_symmetric
301 }
302
303 pub fn basis(&self) -> &B {
306 &self.basis
307 }
308
309 pub fn coefficients(&self) -> &Array2<T> {
312 &self.coefficients
313 }
314
315 pub fn energies(&self) -> &Array1<Result<T, String>> {
317 &self.energies
318 }
319
320 pub fn threshold(&self) -> <T as ComplexFloat>::Real {
322 self.threshold
323 }
324
325 pub fn n_states(&self) -> usize {
327 self.coefficients().ncols()
328 }
329}
330
331impl<'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 #[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
359impl<'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 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 #[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
632impl<'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
651impl<'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}