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, 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#[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 #[builder(setter(skip), default = "self.complex_symmetric_from_basis()?")]
63 complex_symmetric: bool,
64
65 #[builder(default = "false")]
69 complex_conjugated: bool,
70
71 basis: B,
74
75 coefficients: Array2<T>,
79
80 #[builder(
82 default = "Err(\"Multi-determinantal wavefunction energies not yet set.\".to_string())"
83 )]
84 energies: Result<Array1<T>, String>,
85
86 threshold: <T as ComplexFloat>::Real,
88}
89
90impl<'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 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 pub fn builder() -> MultiDeterminantsBuilder<'a, T, B, SC> {
169 MultiDeterminantsBuilder::default()
170 }
171
172 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 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 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 pub fn complex_conjugated(&self) -> bool {
282 self.complex_conjugated
283 }
284
285 pub fn complex_symmetric(&self) -> bool {
288 self.complex_symmetric
289 }
290
291 pub fn basis(&self) -> &B {
294 &self.basis
295 }
296
297 pub fn coefficients(&self) -> &Array2<T> {
300 &self.coefficients
301 }
302
303 pub fn energies(&self) -> Result<&Array1<T>, &String> {
305 self.energies.as_ref()
306 }
307
308 pub fn threshold(&self) -> <T as ComplexFloat>::Real {
310 self.threshold
311 }
312
313 pub fn n_states(&self) -> usize {
315 self.coefficients().ncols()
316 }
317}
318
319impl<'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 #[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
347impl<'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 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
511impl<'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
530impl<'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}