1use std::fmt::LowerExp;
2use std::iter::Product;
3
4use anyhow::{self, ensure, format_err};
5use derive_builder::Builder;
6use duplicate::duplicate_item;
7use indexmap::IndexSet;
8use itertools::Itertools;
9use ndarray::{
10 Array1, Array2, ArrayView1, ArrayView2, ArrayView4, Axis, Ix0, Ix2, ScalarOperand, stack,
11};
12use ndarray_einsum::einsum;
13use ndarray_linalg::types::Lapack;
14use ndarray_linalg::{Determinant, Eig, Eigh, SVD, Scalar, UPLO};
15use num::{Complex, Float};
16use num_complex::ComplexFloat;
17
18use crate::analysis::EigenvalueComparisonMode;
19use crate::angmom::spinor_rotation_3d::StructureConstraint;
20use crate::auxiliary::misc::{check_complex_matrix_symmetry, check_real_matrix_symmetry};
21
22use super::denmat::{calc_unweighted_codensity_matrix, calc_weighted_codensity_matrix};
23
24#[cfg(test)]
25#[path = "nonortho_tests.rs"]
26mod nonortho_tests;
27
28#[derive(Builder)]
40#[builder(build_fn(validate = "Self::validate"))]
41pub struct LowdinPairedCoefficients<T: ComplexFloat> {
42 paired_cw: Array2<T>,
44
45 paired_cx: Array2<T>,
47
48 lowdin_overlaps: Vec<T>,
50
51 #[builder(default = "self.default_zero_indices()?")]
55 zero_indices: IndexSet<usize>,
56
57 thresh_zeroov: T::Real,
59
60 complex_symmetric: bool,
63}
64
65impl<T: ComplexFloat> LowdinPairedCoefficientsBuilder<T> {
66 fn validate(&self) -> Result<(), String> {
67 let paired_cw = self
68 .paired_cw
69 .as_ref()
70 .ok_or("Löwdin-paired coefficients `paired_cw` not set.".to_string())?;
71 let paired_cx = self
72 .paired_cx
73 .as_ref()
74 .ok_or("Löwdin-paired coefficients `paired_cx` not set.".to_string())?;
75 let lowdin_overlaps = self
76 .lowdin_overlaps
77 .as_ref()
78 .ok_or("Löwdin overlaps not set.".to_string())?;
79 let zero_indices = self
80 .zero_indices
81 .as_ref()
82 .ok_or("Indices of zero Löwdin overlaps not set.".to_string())?;
83
84 if paired_cw.shape() == paired_cx.shape() {
85 let lowdin_dim = paired_cw.shape()[1];
86 if lowdin_dim == lowdin_overlaps.len() {
87 if zero_indices.iter().all(|i| *i < lowdin_dim) {
88 Ok(())
89 } else {
90 Err("Some indices of zero Löwdin overlaps are out-of-bound!".to_string())
91 }
92 } else {
93 Err(
94 "Inconsistent number of Löwdin-paired orbitals and Löwdin overlaps."
95 .to_string(),
96 )
97 }
98 } else {
99 Err(format!(
100 "Inconsistent shapes between `paired_cw` ({:?}) and `paired_cx` ({:?}).",
101 paired_cw.shape(),
102 paired_cx.shape()
103 ))
104 }
105 }
106
107 fn default_zero_indices(&self) -> Result<IndexSet<usize>, String> {
108 let lowdin_overlaps = self
109 .lowdin_overlaps
110 .as_ref()
111 .ok_or("Löwdin overlaps not set.".to_string())?;
112 let thresh_zeroov = self
113 .thresh_zeroov
114 .as_ref()
115 .ok_or("threshold for zero Löwdin overlaps not set.".to_string())?;
116 let zero_indices = lowdin_overlaps
117 .iter()
118 .enumerate()
119 .filter(|(_, ov)| ComplexFloat::abs(**ov) < *thresh_zeroov)
120 .map(|(i, _)| i)
121 .collect::<IndexSet<_>>();
122 Ok(zero_indices)
123 }
124}
125
126impl<T: ComplexFloat> LowdinPairedCoefficients<T> {
127 pub fn builder() -> LowdinPairedCoefficientsBuilder<T> {
128 LowdinPairedCoefficientsBuilder::<T>::default()
129 }
130
131 pub fn paired_coefficients(&self) -> (&Array2<T>, &Array2<T>) {
133 (&self.paired_cw, &self.paired_cx)
134 }
135
136 pub fn nbasis(&self) -> usize {
139 self.paired_cw.nrows()
140 }
141
142 pub fn lowdin_dim(&self) -> usize {
144 self.lowdin_overlaps.len()
145 }
146
147 pub fn n_lowdin_zeros(&self) -> usize {
149 self.zero_indices.len()
150 }
151
152 pub fn lowdin_overlaps(&self) -> &Vec<T> {
154 &self.lowdin_overlaps
155 }
156
157 pub fn zero_indices(&self) -> &IndexSet<usize> {
159 &self.zero_indices
160 }
161
162 pub fn thresh_zeroov(&self) -> T::Real {
164 self.thresh_zeroov
165 }
166
167 pub fn nonzero_indices(&self) -> IndexSet<usize> {
169 (0..self.lowdin_dim())
170 .filter(|i| !self.zero_indices.contains(i))
171 .collect::<IndexSet<_>>()
172 }
173
174 pub fn complex_symmetric(&self) -> bool {
177 self.complex_symmetric
178 }
179}
180
181impl<T: ComplexFloat + Product> LowdinPairedCoefficients<T> {
182 pub fn reduced_overlap(&self) -> T {
184 self.nonzero_indices()
185 .iter()
186 .map(|i| self.lowdin_overlaps[*i])
187 .product()
188 }
189}
190
191pub fn calc_lowdin_pairing<T>(
242 cw: &ArrayView2<T>,
243 cx: &ArrayView2<T>,
244 sao: &ArrayView2<T>,
245 complex_symmetric: bool,
246 thresh_offdiag: <T as ComplexFloat>::Real,
247 thresh_zeroov: <T as ComplexFloat>::Real,
248) -> Result<LowdinPairedCoefficients<T>, anyhow::Error>
249where
250 T: ComplexFloat + Lapack,
251 <T as ComplexFloat>::Real: PartialOrd + LowerExp,
252{
253 if cw.shape() != cx.shape() {
254 Err(format_err!(
255 "Coefficient dimensions mismatched: cw ({:?}) !~ cx ({:?}).",
256 cw.shape(),
257 cx.shape()
258 ))
259 } else {
260 let init_orb_ovmat = if complex_symmetric {
261 einsum("ji,jk,kl->il", &[cw, sao, cx])
262 } else {
263 einsum("ji,jk,kl->il", &[&cw.map(|x| x.conj()), sao, cx])
264 }
265 .map_err(|err| format_err!(err))?
266 .into_dimensionality::<Ix2>()?;
267
268 let max_offdiag = (&init_orb_ovmat - &Array2::from_diag(&init_orb_ovmat.diag().to_owned()))
269 .iter()
270 .map(|x| ComplexFloat::abs(*x))
271 .max_by(|x, y| {
272 x.partial_cmp(y)
273 .expect("Unable to compare two `abs` values.")
274 })
275 .ok_or_else(|| {
276 format_err!(
277 "Unable to determine the maximum off-diagonal element for\n{}.",
278 init_orb_ovmat
279 )
280 })?;
281
282 if max_offdiag <= thresh_offdiag {
283 let lowdin_overlaps = init_orb_ovmat.into_diag().to_vec();
284 let zero_indices = lowdin_overlaps
285 .iter()
286 .enumerate()
287 .filter(|(_, ov)| ComplexFloat::abs(**ov) < thresh_zeroov)
288 .map(|(i, _)| i)
289 .collect::<IndexSet<_>>();
290 LowdinPairedCoefficients::builder()
291 .paired_cw(cw.to_owned())
292 .paired_cx(cx.to_owned())
293 .lowdin_overlaps(lowdin_overlaps)
294 .zero_indices(zero_indices)
295 .thresh_zeroov(thresh_zeroov)
296 .complex_symmetric(complex_symmetric)
297 .build()
298 .map_err(|err| format_err!(err))
299 } else {
300 let (u_opt, _, vh_opt) = init_orb_ovmat.svd(true, true)?;
301 let u = u_opt.ok_or_else(|| format_err!("Unable to compute the U matrix from SVD."))?;
302 let vh =
303 vh_opt.ok_or_else(|| format_err!("Unable to compute the V matrix from SVD."))?;
304 let v = vh.t().map(|x| x.conj());
305 let det_v_c = v.det()?.conj();
306
307 let paired_cw = if complex_symmetric {
308 let uc = u.map(|x| x.conj());
309 let mut cwt = cw.dot(&uc);
310 let det_uc_c = uc.det()?.conj();
311 cwt.column_mut(0).iter_mut().for_each(|x| *x *= det_uc_c);
312 cwt
313 } else {
314 let mut cwt = cw.dot(&u);
315 let det_u_c = u.det()?.conj();
316 cwt.column_mut(0).iter_mut().for_each(|x| *x *= det_u_c);
317 cwt
318 };
319
320 let paired_cx = {
321 let mut cxt = cx.dot(&v);
322 cxt.column_mut(0).iter_mut().for_each(|x| *x *= det_v_c);
323 cxt
324 };
325
326 let lowdin_orb_ovmat = if complex_symmetric {
327 einsum("ji,jk,kl->il", &[&paired_cw, sao, &paired_cx])
328 } else {
329 einsum(
330 "ji,jk,kl->il",
331 &[&paired_cw.map(|x| x.conj()), sao, &paired_cx],
332 )
333 }
334 .map_err(|err| format_err!(err))?
335 .into_dimensionality::<Ix2>()?;
336
337 let max_offdiag_lowdin = (&lowdin_orb_ovmat - &Array2::from_diag(&lowdin_orb_ovmat.diag().to_owned()))
338 .iter()
339 .map(|x| ComplexFloat::abs(*x))
340 .max_by(|x, y| {
341 x.partial_cmp(y)
342 .expect("Unable to compare two `abs` values.")
343 })
344 .ok_or_else(|| format_err!("Unable to determine the maximum off-diagonal element of the Lowdin-paired overlap matrix."))?;
345 if max_offdiag_lowdin <= thresh_offdiag {
346 let lowdin_overlaps = lowdin_orb_ovmat.into_diag().to_vec();
347 let zero_indices = lowdin_overlaps
348 .iter()
349 .enumerate()
350 .filter(|(_, ov)| ComplexFloat::abs(**ov) < thresh_zeroov)
351 .map(|(i, _)| i)
352 .collect::<IndexSet<_>>();
353 LowdinPairedCoefficients::builder()
354 .paired_cw(paired_cw.clone())
355 .paired_cx(paired_cx.clone())
356 .lowdin_overlaps(lowdin_overlaps)
357 .zero_indices(zero_indices)
358 .thresh_zeroov(thresh_zeroov)
359 .complex_symmetric(complex_symmetric)
360 .build()
361 .map_err(|err| format_err!(err))
362 } else {
363 Err(format_err!(
364 "Löwdin overlap matrix deviates from diagonality. Maximum off-diagonal overlap has magnitude {max_offdiag_lowdin:.3e} > threshold of {thresh_offdiag:.3e}. Löwdin pairing has failed."
365 ))
366 }
367 }
368 }
369}
370
371pub fn calc_o0_matrix_element<T, SC>(
385 lowdin_paired_coefficientss: &[LowdinPairedCoefficients<T>],
386 o0: T,
387 structure_constraint: &SC,
388) -> Result<T, anyhow::Error>
389where
390 T: ComplexFloat + ScalarOperand + Product,
391 SC: StructureConstraint,
392{
393 let nzeros_explicit: usize = lowdin_paired_coefficientss
394 .iter()
395 .map(|lpc| lpc.n_lowdin_zeros())
396 .sum();
397 let nzeros = nzeros_explicit * structure_constraint.implicit_factor()?;
398 if nzeros > 0 {
399 Ok(T::zero())
400 } else {
401 let reduced_ov_explicit: T = lowdin_paired_coefficientss
402 .iter()
403 .map(|lpc| lpc.reduced_overlap())
404 .product();
405 let reduced_ov = (0..structure_constraint.implicit_factor()?)
406 .fold(T::one(), |acc, _| acc * reduced_ov_explicit);
407 Ok(reduced_ov * o0)
408 }
409}
410
411pub fn calc_o1_matrix_element<T, SC>(
425 lowdin_paired_coefficientss: &[LowdinPairedCoefficients<T>],
426 o1: &ArrayView2<T>,
427 structure_constraint: &SC,
428) -> Result<T, anyhow::Error>
429where
430 T: ComplexFloat + ScalarOperand + Product,
431 SC: StructureConstraint,
432{
433 let nzeros_explicit: usize = lowdin_paired_coefficientss
434 .iter()
435 .map(|lpc| lpc.n_lowdin_zeros())
436 .sum();
437 let nzeros = nzeros_explicit * structure_constraint.implicit_factor()?;
438 if nzeros > 1 {
439 Ok(T::zero())
440 } else {
441 let reduced_ov_explicit: T = lowdin_paired_coefficientss
442 .iter()
443 .map(|lpc| lpc.reduced_overlap())
444 .product();
445 let reduced_ov = (0..structure_constraint.implicit_factor()?)
446 .fold(T::one(), |acc, _| acc * reduced_ov_explicit);
447
448 if nzeros == 0 {
449 let nbasis = lowdin_paired_coefficientss[0].nbasis();
450 let w = (0..structure_constraint.implicit_factor()?)
451 .cartesian_product(lowdin_paired_coefficientss.iter())
452 .try_fold(Array2::<T>::zeros((nbasis, nbasis)), |acc, (_, lpc)| {
453 calc_weighted_codensity_matrix(lpc).map(|w| acc + w)
454 })?;
455 einsum("ij,ji->", &[o1, &w.view()])
457 .map_err(|err| format_err!(err))?
458 .into_dimensionality::<Ix0>()?
459 .into_iter()
460 .next()
461 .ok_or_else(|| {
462 format_err!("Unable to extract the result of the einsum contraction.")
463 })
464 .map(|v| v * reduced_ov)
465 } else {
466 ensure!(
467 nzeros == 1,
468 "Unexpected number of zero Löwdin overlaps: {nzeros} != 1."
469 );
470 let ps = (0..structure_constraint.implicit_factor()?)
471 .flat_map(|_| {
472 lowdin_paired_coefficientss.iter().flat_map(|lpc| {
473 lpc.zero_indices()
474 .iter()
475 .map(|mbar| calc_unweighted_codensity_matrix(lpc, *mbar))
476 })
477 })
478 .collect::<Result<Vec<_>, _>>()?;
479 ensure!(
480 ps.len() == 1,
481 "Unexpected number of unweighted codensity matrices ({}) for one zero overlap.",
482 ps.len()
483 );
484 let p_mbar = ps.first().ok_or_else(|| {
485 format_err!("Unable to retrieve the computed unweighted codensity matrix.")
486 })?;
487
488 einsum("ij,ji->", &[o1, &p_mbar.view()])
490 .map_err(|err| format_err!(err))?
491 .into_dimensionality::<Ix0>()?
492 .into_iter()
493 .next()
494 .ok_or_else(|| {
495 format_err!("Unable to extract the result of the einsum contraction.")
496 })
497 .map(|v| v * reduced_ov)
498 }
499 }
500}
501
502pub fn calc_o2_matrix_element<'a, 'b, T, SC, F>(
520 lowdin_paired_coefficientss: &[LowdinPairedCoefficients<T>],
521 o2_opt: Option<&'b ArrayView4<'a, T>>,
522 get_jk_opt: Option<&F>,
523 structure_constraint: &SC,
524) -> Result<T, anyhow::Error>
525where
526 'a: 'b,
527 T: ComplexFloat + ScalarOperand + Product + std::fmt::Display,
528 SC: StructureConstraint,
529 F: Fn(&Array2<T>) -> Result<(Array2<T>, Array2<T>), anyhow::Error>,
530{
531 let nzeros_explicit: usize = lowdin_paired_coefficientss
532 .iter()
533 .map(|lpc| lpc.n_lowdin_zeros())
534 .sum();
535 let nzeros = nzeros_explicit * structure_constraint.implicit_factor()?;
536 if nzeros > 2 {
537 Ok(T::zero())
538 } else {
539 let reduced_ov_explicit: T = lowdin_paired_coefficientss
540 .iter()
541 .map(|lpc| lpc.reduced_overlap())
542 .product();
543 let reduced_ov = (0..structure_constraint.implicit_factor()?)
544 .fold(T::one(), |acc, _| acc * reduced_ov_explicit);
545
546 if nzeros == 0 {
547 let nbasis = lowdin_paired_coefficientss[0].nbasis();
548 let w_sigmas = (0..structure_constraint.implicit_factor()?)
549 .cartesian_product(lowdin_paired_coefficientss.iter())
550 .map(|(_, lpc)| calc_weighted_codensity_matrix(lpc))
551 .collect::<Result<Vec<_>, _>>()?;
552 let w = w_sigmas
553 .iter()
554 .fold(Array2::<T>::zeros((nbasis, nbasis)), |acc, w_sigma| {
555 acc + w_sigma
556 });
557
558 match (o2_opt, get_jk_opt) {
559 (Some(o2), None) => {
560 let j_term = einsum("ikjl,ji,lk->", &[o2, &w.view(), &w.view()])
562 .map_err(|err| format_err!(err))?
563 .into_dimensionality::<Ix0>()?
564 .into_iter()
565 .next()
566 .ok_or_else(|| {
567 format_err!("Unable to extract the result of the einsum contraction.")
568 })
569 .map(|v| v * reduced_ov / (T::one() + T::one()))?;
570 let k_term = w_sigmas
571 .iter()
572 .try_fold(T::zero(), |acc, w_sigma| {
573 einsum("ikjl,li,jk->", &[o2, &w_sigma.view(), &w_sigma.view()])
574 .map_err(|err| format_err!(err))?
575 .into_dimensionality::<Ix0>()?
576 .into_iter()
577 .next()
578 .ok_or_else(|| {
579 format_err!(
580 "Unable to extract the result of the einsum contraction."
581 )
582 })
583 .map(|v| acc + v)
584 })
585 .map(|v| v * reduced_ov / (T::one() + T::one()))?;
586 Ok(j_term - k_term)
587 }
588 (None, Some(get_jk)) => {
589 let (j_w, _) = get_jk(&w)?;
591 let j_term = einsum("ij,ji->", &[&j_w.view(), &w.view()])
592 .map_err(|err| format_err!(err))?
593 .into_dimensionality::<Ix0>()?
594 .into_iter()
595 .next()
596 .ok_or_else(|| {
597 format_err!("Unable to extract the result of the einsum contraction.")
598 })
599 .map(|v| v * reduced_ov / (T::one() + T::one()))?;
600 let k_term = w_sigmas
601 .iter()
602 .try_fold(T::zero(), |acc, w_sigma| {
603 let (_, k_w_sigma) = get_jk(w_sigma)?;
604 einsum("il,li->", &[&k_w_sigma.view(), &w_sigma.view()])
605 .map_err(|err| format_err!(err))?
606 .into_dimensionality::<Ix0>()?
607 .into_iter()
608 .next()
609 .ok_or_else(|| {
610 format_err!(
611 "Unable to extract the result of the einsum contraction."
612 )
613 })
614 .map(|v| acc + v)
615 })
616 .map(|v| v * reduced_ov / (T::one() + T::one()))?;
617 Ok(j_term - k_term)
618 }
619 _ => Err(format_err!(
620 "One and only one of `o2_opt` or `get_jk_opt` should be provided."
621 )),
622 }
623 } else if nzeros == 1 {
624 ensure!(
625 nzeros_explicit == 1,
626 "Unexpected number of explicit zero Löwdin overlaps: {nzeros_explicit} != 1."
627 );
628
629 let nbasis = lowdin_paired_coefficientss[0].nbasis();
630 let w = (0..structure_constraint.implicit_factor()?)
631 .cartesian_product(lowdin_paired_coefficientss.iter())
632 .try_fold(Array2::<T>::zeros((nbasis, nbasis)), |acc, (_, lpc)| {
633 calc_weighted_codensity_matrix(lpc).map(|w_sigma| acc + w_sigma)
634 })?;
635
636 lowdin_paired_coefficientss
637 .iter()
638 .filter_map(|lpc| {
639 if lpc.n_lowdin_zeros() == 1 {
640 let w_sigma_res = calc_weighted_codensity_matrix(lpc);
641 let mbar = lpc.zero_indices()[0];
642 let p_mbar_sigma_res = calc_unweighted_codensity_matrix(lpc, mbar);
643 Some((w_sigma_res, p_mbar_sigma_res))
644 } else {
645 None
646 }
647 })
648 .try_fold(T::zero(), |acc, (w_sigma_res, p_mbar_sigma_res)| {
649 w_sigma_res.and_then(|w_sigma| {
650 p_mbar_sigma_res.and_then(|p_mbar_sigma| {
651 match (o2_opt, get_jk_opt) {
652 (Some(o2), None) => {
653 let j_term_1 = einsum(
655 "ikjl,ji,lk->",
656 &[o2, &w.view(), &p_mbar_sigma.view()],
657 )
658 .map_err(|err| format_err!(err))?
659 .into_dimensionality::<Ix0>()?
660 .into_iter()
661 .next()
662 .ok_or_else(|| {
663 format_err!(
664 "Unable to extract the result of the einsum contraction."
665 )
666 })?;
667 let j_term_2 = einsum(
668 "ikjl,ji,lk->",
669 &[o2, &p_mbar_sigma.view(), &w.view()],
670 )
671 .map_err(|err| format_err!(err))?
672 .into_dimensionality::<Ix0>()?
673 .into_iter()
674 .next()
675 .ok_or_else(|| {
676 format_err!(
677 "Unable to extract the result of the einsum contraction."
678 )
679 })?;
680 let k_term_1 = einsum(
681 "ikjl,li,jk->",
682 &[o2, &w_sigma.view(), &p_mbar_sigma.view()],
683 )
684 .map_err(|err| format_err!(err))?
685 .into_dimensionality::<Ix0>()?
686 .into_iter()
687 .next()
688 .ok_or_else(|| {
689 format_err!(
690 "Unable to extract the result of the einsum contraction."
691 )
692 })?;
693 let k_term_2 = einsum(
694 "ikjl,li,jk->",
695 &[o2, &p_mbar_sigma.view(), &w_sigma.view()],
696 )
697 .map_err(|err| format_err!(err))?
698 .into_dimensionality::<Ix0>()?
699 .into_iter()
700 .next()
701 .ok_or_else(|| {
702 format_err!(
703 "Unable to extract the result of the einsum contraction."
704 )
705 })?;
706 Ok(acc + j_term_1 + j_term_2 - k_term_1 - k_term_2)
707 },
708 (None, Some(get_jk)) => {
709 let (j_p_mbar_sigma, k_p_mbar_sigma) = get_jk(&p_mbar_sigma)?;
711 let j_term_1 = einsum(
712 "ij,ji->",
713 &[&j_p_mbar_sigma.view(), &w.view()],
714 )
715 .map_err(|err| format_err!(err))?
716 .into_dimensionality::<Ix0>()?
717 .into_iter()
718 .next()
719 .ok_or_else(|| {
720 format_err!(
721 "Unable to extract the result of the einsum contraction."
722 )
723 })?;
724 let (j_w, _) = get_jk(&w)?;
725 let j_term_2 = einsum(
726 "ij,ji->",
727 &[&j_w.view(), &p_mbar_sigma.view()],
728 )
729 .map_err(|err| format_err!(err))?
730 .into_dimensionality::<Ix0>()?
731 .into_iter()
732 .next()
733 .ok_or_else(|| {
734 format_err!(
735 "Unable to extract the result of the einsum contraction."
736 )
737 })?;
738 let k_term_1 = einsum(
739 "il,li->",
740 &[&k_p_mbar_sigma.view(), &w_sigma.view()],
741 )
742 .map_err(|err| format_err!(err))?
743 .into_dimensionality::<Ix0>()?
744 .into_iter()
745 .next()
746 .ok_or_else(|| {
747 format_err!(
748 "Unable to extract the result of the einsum contraction."
749 )
750 })?;
751 let (_, k_w_sigma) = get_jk(&w_sigma)?;
752 let k_term_2 = einsum(
753 "il,li->",
754 &[&k_w_sigma.view(), &p_mbar_sigma.view()],
755 )
756 .map_err(|err| format_err!(err))?
757 .into_dimensionality::<Ix0>()?
758 .into_iter()
759 .next()
760 .ok_or_else(|| {
761 format_err!(
762 "Unable to extract the result of the einsum contraction."
763 )
764 })?;
765 Ok(acc + j_term_1 + j_term_2 - k_term_1 - k_term_2)
766 }
767 _ => Err(format_err!(
768 "One and only one of `o2` or `get_jk` should be provided."
769 )),
770 }
771 })
772 })
773 })
774 .map(|v| v * reduced_ov / (T::one() + T::one()))
775 } else {
776 ensure!(
777 nzeros == 2,
778 "Unexpected number of zero Löwdin overlaps: {nzeros} != 2."
779 );
780
781 let ps = (0..structure_constraint.implicit_factor()?)
782 .flat_map(|_| {
783 lowdin_paired_coefficientss.iter().flat_map(|lpc| {
784 lpc.zero_indices()
785 .iter()
786 .map(|mbar| calc_unweighted_codensity_matrix(lpc, *mbar))
787 })
788 })
789 .collect::<Result<Vec<_>, _>>()?;
790 ensure!(
791 ps.len() == 2,
792 "Unexpected number of unweighted codensity matrices ({}) for two zero overlaps.",
793 ps.len()
794 );
795 let p_mbar = ps.first().ok_or_else(|| {
796 format_err!("Unable to retrieve the first computed unweighted codensity matrix.")
797 })?;
798 let p_nbar = ps.last().ok_or_else(|| {
799 format_err!("Unable to retrieve the second computed unweighted codensity matrix.")
800 })?;
801
802 match (o2_opt, get_jk_opt) {
803 (Some(o2), None) => {
804 let j_term_1 = einsum("ikjl,ji,lk->", &[o2, &p_mbar.view(), &p_nbar.view()])
806 .map_err(|err| format_err!(err))?
807 .into_dimensionality::<Ix0>()?
808 .into_iter()
809 .next()
810 .ok_or_else(|| {
811 format_err!("Unable to extract the result of the einsum contraction.")
812 })?;
813 let j_term_2 = einsum("ikjl,ji,lk->", &[o2, &p_nbar.view(), &p_mbar.view()])
814 .map_err(|err| format_err!(err))?
815 .into_dimensionality::<Ix0>()?
816 .into_iter()
817 .next()
818 .ok_or_else(|| {
819 format_err!("Unable to extract the result of the einsum contraction.")
820 })?;
821
822 let (k_term_1, k_term_2) = if lowdin_paired_coefficientss
823 .iter()
824 .any(|lpc| lpc.n_lowdin_zeros() == 2)
825 {
826 let k_term_1 =
827 einsum("ikjl,li,jk->", &[o2, &p_mbar.view(), &p_nbar.view()])
828 .map_err(|err| format_err!(err))?
829 .into_dimensionality::<Ix0>()?
830 .into_iter()
831 .next()
832 .ok_or_else(|| {
833 format_err!(
834 "Unable to extract the result of the einsum contraction."
835 )
836 })?;
837 let k_term_2 =
838 einsum("ikjl,li,jk->", &[o2, &p_nbar.view(), &p_mbar.view()])
839 .map_err(|err| format_err!(err))?
840 .into_dimensionality::<Ix0>()?
841 .into_iter()
842 .next()
843 .ok_or_else(|| {
844 format_err!(
845 "Unable to extract the result of the einsum contraction."
846 )
847 })?;
848 (k_term_1, k_term_2)
849 } else {
850 (T::zero(), T::zero())
851 };
852 Ok(reduced_ov * (j_term_1 - k_term_1 + j_term_2 - k_term_2)
853 / (T::one() + T::one()))
854 }
855 (None, Some(get_jk)) => {
856 let (j_p_nbar, k_p_nbar) = get_jk(p_nbar)?;
858 let j_term_1 = einsum("ij,ji->", &[&j_p_nbar.view(), &p_mbar.view()])
859 .map_err(|err| format_err!(err))?
860 .into_dimensionality::<Ix0>()?
861 .into_iter()
862 .next()
863 .ok_or_else(|| {
864 format_err!("Unable to extract the result of the einsum contraction.")
865 })?;
866 let (j_p_mbar, k_p_mbar) = get_jk(p_mbar)?;
867 let j_term_2 = einsum("ij,ji->", &[&j_p_mbar.view(), &p_nbar.view()])
868 .map_err(|err| format_err!(err))?
869 .into_dimensionality::<Ix0>()?
870 .into_iter()
871 .next()
872 .ok_or_else(|| {
873 format_err!("Unable to extract the result of the einsum contraction.")
874 })?;
875
876 let (k_term_1, k_term_2) = if lowdin_paired_coefficientss
877 .iter()
878 .any(|lpc| lpc.n_lowdin_zeros() == 2)
879 {
880 let k_term_1 = einsum("il,li->", &[&k_p_nbar.view(), &p_mbar.view()])
881 .map_err(|err| format_err!(err))?
882 .into_dimensionality::<Ix0>()?
883 .into_iter()
884 .next()
885 .ok_or_else(|| {
886 format_err!(
887 "Unable to extract the result of the einsum contraction."
888 )
889 })?;
890 let k_term_2 = einsum("il,li->", &[&k_p_mbar.view(), &p_nbar.view()])
891 .map_err(|err| format_err!(err))?
892 .into_dimensionality::<Ix0>()?
893 .into_iter()
894 .next()
895 .ok_or_else(|| {
896 format_err!(
897 "Unable to extract the result of the einsum contraction."
898 )
899 })?;
900 (k_term_1, k_term_2)
901 } else {
902 (T::zero(), T::zero())
903 };
904 Ok(reduced_ov * (j_term_1 - k_term_1 + j_term_2 - k_term_2)
905 / (T::one() + T::one()))
906 }
907 _ => Err(format_err!(
908 "One and only one of `o2_opt` or `get_jk_opt` should be provided."
909 )),
910 }
911 }
912 }
913}
914
915pub fn calc_transition_density_matrix<T, SC>(
927 lowdin_paired_coefficientss: &[LowdinPairedCoefficients<T>],
928 structure_constraint: &SC,
929) -> Result<Array2<T>, anyhow::Error>
930where
931 T: ComplexFloat + ScalarOperand + Product,
932 SC: StructureConstraint,
933{
934 let nzeros_explicit: usize = lowdin_paired_coefficientss
935 .iter()
936 .map(|lpc| lpc.n_lowdin_zeros())
937 .sum();
938 let nzeros = nzeros_explicit * structure_constraint.implicit_factor()?;
939 let nbasis = lowdin_paired_coefficientss[0].nbasis();
940 if nzeros > 1 {
941 Ok(Array2::<T>::zeros((nbasis, nbasis)))
942 } else {
943 let reduced_ov_explicit: T = lowdin_paired_coefficientss
944 .iter()
945 .map(|lpc| lpc.reduced_overlap())
946 .product();
947 let reduced_ov = (0..structure_constraint.implicit_factor()?)
948 .fold(T::one(), |acc, _| acc * reduced_ov_explicit);
949
950 if nzeros == 0 {
951 let nbasis = lowdin_paired_coefficientss[0].nbasis();
952 let w = (0..structure_constraint.implicit_factor()?)
953 .cartesian_product(lowdin_paired_coefficientss.iter())
954 .try_fold(Array2::<T>::zeros((nbasis, nbasis)), |acc, (_, lpc)| {
955 calc_weighted_codensity_matrix(lpc).map(|w| acc + w)
956 })?;
957 Ok(w.mapv(|v| v * reduced_ov))
958 } else {
959 ensure!(
960 nzeros == 1,
961 "Unexpected number of zero Löwdin overlaps: {nzeros} != 1."
962 );
963 let ps = (0..structure_constraint.implicit_factor()?)
964 .flat_map(|_| {
965 lowdin_paired_coefficientss.iter().flat_map(|lpc| {
966 lpc.zero_indices()
967 .iter()
968 .map(|mbar| calc_unweighted_codensity_matrix(lpc, *mbar))
969 })
970 })
971 .collect::<Result<Vec<_>, _>>()?;
972 ensure!(
973 ps.len() == 1,
974 "Unexpected number of unweighted codensity matrices ({}) for one zero overlap.",
975 ps.len()
976 );
977 let p_mbar = ps.first().ok_or_else(|| {
978 format_err!("Unable to retrieve the computed unweighted codensity matrix.")
979 })?;
980 Ok(p_mbar.mapv(|v| v * reduced_ov))
981 }
982 }
983}
984
985pub fn complex_modified_gram_schmidt<T>(
1005 vmat: &ArrayView2<T>,
1006 complex_symmetric: bool,
1007 thresh: <T as ComplexFloat>::Real,
1008) -> Result<Array2<T>, anyhow::Error>
1009where
1010 T: ComplexFloat + std::fmt::Display + 'static,
1011{
1012 let mut us: Vec<Array1<T>> = Vec::with_capacity(vmat.shape()[1]);
1013 let mut us_sq_norm: Vec<T> = Vec::with_capacity(vmat.shape()[1]);
1014 for (i, vi) in vmat.columns().into_iter().enumerate() {
1015 us.push(vi.to_owned());
1017
1018 for j in 0..i {
1022 let p_uj_ui = if complex_symmetric {
1023 us[j].t().dot(&us[i]) / us_sq_norm[j]
1024 } else {
1025 us[j].t().map(|x| x.conj()).dot(&us[i]) / us_sq_norm[j]
1026 };
1027 us[i] = &us[i] - us[j].map(|&x| x * p_uj_ui);
1028 }
1029
1030 let us_sq_norm_i = if complex_symmetric {
1033 us[i].t().dot(&us[i])
1034 } else {
1035 us[i].t().map(|x| x.conj()).dot(&us[i])
1036 };
1037 if us_sq_norm_i.abs() < thresh {
1038 return Err(format_err!("A zero-norm vector found: {}", us[i]));
1039 }
1040 us_sq_norm.push(us_sq_norm_i);
1041 }
1042
1043 for i in 0..us.len() {
1045 us[i].mapv_inplace(|x| x / us_sq_norm[i].sqrt());
1046 }
1047
1048 let ortho_check = us.iter().enumerate().all(|(i, ui)| {
1049 us.iter().enumerate().all(|(j, uj)| {
1050 let ov_ij = if complex_symmetric {
1051 ui.dot(uj)
1052 } else {
1053 ui.map(|x| x.conj()).dot(uj)
1054 };
1055 i == j || ov_ij.abs() < thresh
1056 })
1057 });
1058
1059 if ortho_check {
1060 stack(Axis(1), &us.iter().map(|u| u.view()).collect_vec()).map_err(|err| format_err!(err))
1061 } else {
1062 Err(format_err!(
1063 "Post-Gram--Schmidt orthogonality check failed."
1064 ))
1065 }
1066}
1067
1068pub trait CanonicalOrthogonalisable {
1070 type NumType;
1072
1073 type RealType;
1075
1076 fn calc_canonical_orthogonal_matrix(
1095 &self,
1096 complex_symmetric: bool,
1097 preserves_full_rank: bool,
1098 thresh_offdiag: Self::RealType,
1099 thresh_zeroov: Self::RealType,
1100 eigenvalue_comparion_mode: &EigenvalueComparisonMode,
1101 ) -> Result<CanonicalOrthogonalisationResult<Self::NumType>, anyhow::Error>;
1102}
1103
1104pub struct CanonicalOrthogonalisationResult<T> {
1106 eigenvalues: Array1<T>,
1108
1109 xmat: Array2<T>,
1111
1112 xmat_d: Array2<T>,
1116}
1117
1118impl<T> CanonicalOrthogonalisationResult<T> {
1119 pub fn eigenvalues(&'_ self) -> ArrayView1<'_, T> {
1121 self.eigenvalues.view()
1122 }
1123
1124 pub fn xmat(&'_ self) -> ArrayView2<'_, T> {
1126 self.xmat.view()
1127 }
1128
1129 pub fn xmat_d(&'_ self) -> ArrayView2<'_, T> {
1133 self.xmat_d.view()
1134 }
1135}
1136
1137#[duplicate_item(
1138 [
1139 dtype_ [ f64 ]
1140 ]
1141 [
1142 dtype_ [ f32 ]
1143 ]
1144)]
1145impl CanonicalOrthogonalisable for ArrayView2<'_, dtype_> {
1146 type NumType = dtype_;
1147
1148 type RealType = dtype_;
1149
1150 fn calc_canonical_orthogonal_matrix(
1151 &self,
1152 _: bool,
1153 preserves_full_rank: bool,
1154 thresh_offdiag: dtype_,
1155 thresh_zeroov: dtype_,
1156 eigenvalue_comparison_mode: &EigenvalueComparisonMode,
1157 ) -> Result<CanonicalOrthogonalisationResult<Self::NumType>, anyhow::Error> {
1158 let smat = self;
1159
1160 check_real_matrix_symmetry(
1169 &smat.view(),
1170 thresh_offdiag,
1171 "Real-valued overlap matrix",
1172 "S(ℝ)",
1173 )?;
1174
1175 let (s_eig, umat) = smat.eigh(UPLO::Lower).map_err(|err| format_err!(err))?;
1177 let mut sorted_s_eig = s_eig.iter().collect_vec();
1178 sorted_s_eig.sort_by(|a, b| match eigenvalue_comparison_mode {
1179 EigenvalueComparisonMode::Modulus => a.abs().total_cmp(&b.abs()),
1180 EigenvalueComparisonMode::Real => a.total_cmp(b),
1181 });
1182 sorted_s_eig.reverse();
1183 log::debug!("Overlap eigenvalues for canonical orthogonalisation (real-symmetric matrix):");
1184 for (i, eig) in s_eig.iter().enumerate() {
1185 log::debug!(" {i}: {eig:+.8e}");
1186 }
1187 log::debug!("");
1188
1189 let nonzero_s_indices = s_eig
1192 .iter()
1193 .positions(|x| match eigenvalue_comparison_mode {
1194 EigenvalueComparisonMode::Modulus => x.abs() > thresh_zeroov,
1195 EigenvalueComparisonMode::Real => *x > thresh_zeroov,
1196 })
1197 .collect_vec();
1198 log::debug!(
1199 "Non-zero overlap indices w.r.t. threshold {thresh_zeroov:.8e} ({eigenvalue_comparison_mode}):"
1200 );
1201 log::debug!(
1202 " {}",
1203 nonzero_s_indices.iter().map(|i| i.to_string()).join(", ")
1204 );
1205
1206 let nonzero_s_eig = s_eig.select(Axis(0), &nonzero_s_indices);
1207 if nonzero_s_eig.iter().any(|v| *v < 0.0) {
1208 return Err(format_err!(
1209 "The matrix has negative eigenvalues and therefore cannot be orthogonalised over the reals."
1210 ));
1211 }
1212 let nonzero_umat = umat.select(Axis(1), &nonzero_s_indices);
1213 let nullity = smat.shape()[0] - nonzero_s_indices.len();
1214 let (xmat, xmat_d) = if nullity == 0 && preserves_full_rank {
1215 (Array2::eye(smat.shape()[0]), Array2::eye(smat.shape()[0]))
1216 } else {
1217 let s_s = Array2::<dtype_>::from_diag(&nonzero_s_eig.mapv(|x| 1.0 / x.sqrt()));
1218 (nonzero_umat.dot(&s_s), s_s.dot(&nonzero_umat.t()))
1219 };
1220 let res = CanonicalOrthogonalisationResult {
1221 eigenvalues: s_eig,
1222 xmat,
1223 xmat_d,
1224 };
1225 Ok(res)
1226 }
1227}
1228
1229impl<T> CanonicalOrthogonalisable for ArrayView2<'_, Complex<T>>
1230where
1231 T: Float + Scalar<Complex = Complex<T>>,
1232 Complex<T>: ComplexFloat<Real = T> + Scalar<Real = T, Complex = Complex<T>> + Lapack,
1233{
1234 type NumType = Complex<T>;
1235
1236 type RealType = T;
1237
1238 fn calc_canonical_orthogonal_matrix(
1239 &self,
1240 complex_symmetric: bool,
1241 preserves_full_rank: bool,
1242 thresh_offdiag: T,
1243 thresh_zeroov: T,
1244 eigenvalue_comparison_mode: &EigenvalueComparisonMode,
1245 ) -> Result<CanonicalOrthogonalisationResult<Self::NumType>, anyhow::Error> {
1246 let smat = self;
1247
1248 check_complex_matrix_symmetry(
1249 &smat.view(),
1250 complex_symmetric,
1251 thresh_offdiag,
1252 "Complex-valued overlap matrix",
1253 "S(ℂ)",
1254 )?;
1255
1256 let (s_eig, umat_nonortho) = smat.eig().map_err(|err| format_err!(err))?;
1257 let mut sorted_s_eig = s_eig.iter().collect_vec();
1258 sorted_s_eig.sort_by(|a, b| match eigenvalue_comparison_mode {
1259 EigenvalueComparisonMode::Modulus => ComplexFloat::abs(**a)
1260 .partial_cmp(&ComplexFloat::abs(**b))
1261 .expect("Unable to compare the moduli of two complex numbers."),
1262 EigenvalueComparisonMode::Real => ComplexFloat::re(**a)
1263 .partial_cmp(&ComplexFloat::re(**b))
1264 .expect("Unable to compare the real parts of two complex numbers."),
1265 });
1266 sorted_s_eig.reverse();
1267 log::debug!(
1268 "Overlap eigenvalues for canonical orthogonalisation ({} matrix):",
1269 if complex_symmetric {
1270 "complex-symmetric"
1271 } else {
1272 "complex-Hermitian"
1273 }
1274 );
1275 for (i, eig) in sorted_s_eig.iter().enumerate() {
1276 log::debug!(" {i}: {eig:+.8e}");
1277 }
1278 log::debug!("");
1279
1280 let nonzero_s_indices = s_eig
1281 .iter()
1282 .positions(|x| match eigenvalue_comparison_mode {
1283 EigenvalueComparisonMode::Modulus => ComplexFloat::abs(*x) > thresh_zeroov,
1284 EigenvalueComparisonMode::Real => ComplexFloat::re(*x) > thresh_zeroov,
1285 })
1286 .collect_vec();
1287 log::debug!(
1288 "Non-zero overlap indices w.r.t. threshold {thresh_zeroov:.8e} ({eigenvalue_comparison_mode}):"
1289 );
1290 log::debug!(
1291 " {}",
1292 nonzero_s_indices.iter().map(|i| i.to_string()).join(", ")
1293 );
1294 log::debug!("");
1295
1296 let nonzero_s_eig = s_eig.select(Axis(0), &nonzero_s_indices);
1297 let nonzero_umat_nonortho = umat_nonortho.select(Axis(1), &nonzero_s_indices);
1298
1299 let nonzero_umat = complex_modified_gram_schmidt(
1302 &nonzero_umat_nonortho.view(),
1303 complex_symmetric,
1304 thresh_zeroov,
1305 )
1306 .map_err(
1307 |_| format_err!("Unable to orthonormalise the linearly-independent eigenvectors of the overlap matrix.")
1308 )?;
1309
1310 let nonzero_s_eig_from_u = if complex_symmetric {
1311 nonzero_umat.t().dot(smat).dot(&nonzero_umat)
1312 } else {
1313 nonzero_umat
1314 .map(|v| v.conj())
1315 .t()
1316 .dot(smat)
1317 .dot(&nonzero_umat)
1318 };
1319
1320 let max_offdiag_s = *(nonzero_s_eig_from_u - Array2::from_diag(&nonzero_s_eig)).mapv(ComplexFloat::abs).iter()
1321 .max_by(|a, b| a.partial_cmp(b).unwrap_or_else(|| panic!("Unable to compare {a} and {b}.")))
1322 .ok_or_else(|| format_err!("Unable to find the maximum absolute value of the overlap symmetric deviation matrix."))?;
1323 ensure!(
1324 max_offdiag_s <= thresh_offdiag,
1325 if complex_symmetric {
1326 "Canonical orthogonalisation has failed: ||U^T.S.U - s||_∞ = {max_offdiag_s:.3e} > {thresh_offdiag:.3e}."
1327 } else {
1328 "Canonical orthogonalisation has failed: ||U^†.S.U - s||_∞ = {max_offdiag_s:.3e} > {thresh_offdiag:.3e}."
1329 }
1330 );
1331
1332 let nullity = smat.shape()[0] - nonzero_s_indices.len();
1333 let (xmat, xmat_d) = if nullity == 0 && preserves_full_rank {
1334 (
1335 Array2::<Complex<T>>::eye(smat.shape()[0]),
1336 Array2::<Complex<T>>::eye(smat.shape()[0]),
1337 )
1338 } else {
1339 let s_s = Array2::<Complex<T>>::from_diag(
1340 &nonzero_s_eig.mapv(|x| Complex::<T>::from(T::one()) / x.sqrt()),
1341 );
1342 let xmat = nonzero_umat.dot(&s_s);
1343 let xmat_d = if complex_symmetric {
1344 xmat.t().to_owned()
1346 } else {
1347 xmat.map(|v| v.conj()).t().to_owned()
1348 };
1349 (xmat, xmat_d)
1350 };
1351 let res = CanonicalOrthogonalisationResult {
1352 eigenvalues: s_eig,
1353 xmat,
1354 xmat_d,
1355 };
1356 Ok(res)
1357 }
1358}