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::{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#[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 #[builder(setter(skip), default = "self.complex_symmetric_from_basis()?")]
64 complex_symmetric: bool,
65
66 #[builder(default = "false")]
69 complex_conjugated: bool,
70
71 basis: B,
73
74 coefficients: Array1<T>,
77
78 #[builder(
80 default = "Err(\"Multi-determinantal wavefunction energy not yet set.\".to_string())"
81 )]
82 energy: Result<T, String>,
83
84 threshold: <T as ComplexFloat>::Real,
86}
87
88impl<'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 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 pub fn builder() -> MultiDeterminantBuilder<'a, T, B, SC> {
167 MultiDeterminantBuilder::default()
168 }
169
170 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 pub fn complex_conjugated(&self) -> bool {
190 self.complex_conjugated
191 }
192
193 pub fn complex_symmetric(&self) -> bool {
195 self.complex_symmetric
196 }
197
198 pub fn basis(&self) -> &B {
201 &self.basis
202 }
203
204 pub fn coefficients(&self) -> &Array1<T> {
207 &self.coefficients
208 }
209
210 pub fn energy(&self) -> Result<&T, &String> {
212 self.energy.as_ref()
213 }
214
215 pub fn threshold(&self) -> <T as ComplexFloat>::Real {
217 self.threshold
218 }
219
220 pub(crate) fn set_energy(&mut self, energy: Result<T, String>) {
222 self.energy = energy;
223 }
224}
225
226impl<'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 #[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
254impl<'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 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 #[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
507impl<'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
526impl<'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}