1use std::cmp::Ordering;
4use std::fmt;
5use std::ops::Mul;
6
7use anyhow::{self, Context, format_err};
8use duplicate::duplicate_item;
9use itertools::Itertools;
10use log;
11use ndarray::{Array, Array1, Array2, Axis, Dimension, Ix0, Ix2, s};
12use ndarray_einsum::*;
13use ndarray_linalg::{solve::Inverse, types::Lapack};
14use num_complex::{Complex, ComplexFloat};
15use num_traits::{ToPrimitive, Zero};
16#[cfg(feature = "python")]
17use pyo3::prelude::*;
18use rayon::prelude::*;
19use serde::{Deserialize, Serialize};
20
21use crate::chartab::chartab_group::CharacterProperties;
22use crate::chartab::{CharacterTable, DecompositionError, SubspaceDecomposable};
23use crate::group::{GroupProperties, class::ClassProperties};
24use crate::io::format::{log_subtitle, qsym2_output};
25use crate::symmetry::symmetry_group::UnitaryRepresentedSymmetryGroup;
26
27#[derive(Clone)]
33pub(crate) struct Metric<'a, T, D: Dimension> {
34 hermitian: &'a Array<T, D>,
39
40 complex_symmetric: Option<&'a Array<T, D>>,
46}
47
48pub trait Overlap<T, D>
61where
62 T: ComplexFloat + fmt::Debug + Lapack,
63 D: Dimension,
64{
65 fn complex_symmetric(&self) -> bool;
68
69 fn overlap(
73 &self,
74 other: &Self,
75 metric: Option<&Array<T, D>>,
76 metric_h: Option<&Array<T, D>>,
77 ) -> Result<T, anyhow::Error>;
78
79 fn overlap_definition(&self) -> String;
82}
83
84pub struct OrbitIterator<'a, G, I>
94where
95 G: GroupProperties,
96{
97 group_iter: <<G as GroupProperties>::ElementCollection as IntoIterator>::IntoIter,
100
101 origin: &'a I,
103
104 action: fn(&G::GroupElement, &I) -> Result<I, anyhow::Error>,
106}
107
108impl<'a, G, I> OrbitIterator<'a, G, I>
109where
110 G: GroupProperties,
111{
112 pub fn new(
124 group: &G,
125 origin: &'a I,
126 action: fn(&G::GroupElement, &I) -> Result<I, anyhow::Error>,
127 ) -> Self {
128 Self {
129 group_iter: group.elements().clone().into_iter(),
130 origin,
131 action,
132 }
133 }
134}
135
136impl<'a, G, I> Iterator for OrbitIterator<'a, G, I>
137where
138 G: GroupProperties,
139{
140 type Item = Result<I, anyhow::Error>;
141
142 fn next(&mut self) -> Option<Self::Item> {
143 self.group_iter
144 .next()
145 .map(|op| (self.action)(&op, self.origin))
146 }
147}
148
149pub trait Orbit<G, I>
155where
156 G: GroupProperties,
157{
158 type OrbitIter: Iterator<Item = Result<I, anyhow::Error>>;
160
161 fn group(&self) -> &G;
163
164 fn origin(&self) -> &I;
166
167 fn iter(&self) -> Self::OrbitIter;
169}
170
171#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
182#[cfg_attr(feature = "python", pyclass(eq, eq_int))]
183pub enum EigenvalueComparisonMode {
184 Real,
186
187 #[default]
189 Modulus,
190}
191
192impl fmt::Display for EigenvalueComparisonMode {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 EigenvalueComparisonMode::Real => write!(f, "Real part"),
196 EigenvalueComparisonMode::Modulus => write!(f, "Modulus"),
197 }
198 }
199}
200
201pub trait RepAnalysis<G, I, T, D>: Orbit<G, I>
212where
213 T: ComplexFloat + Lapack + fmt::Debug,
214 <T as ComplexFloat>::Real: ToPrimitive,
215 G: GroupProperties + ClassProperties + CharacterProperties,
216 G::GroupElement: fmt::Display,
217 G::CharTab: SubspaceDecomposable<T>,
218 D: Dimension,
219 I: Overlap<T, D> + Clone,
220 Self::OrbitIter: Iterator<Item = Result<I, anyhow::Error>>,
221{
222 fn set_smat(&mut self, smat: Array2<T>);
232
233 #[must_use]
235 fn smat(&self) -> Option<&Array2<T>>;
236
237 #[must_use]
256 fn xmat(&self) -> &Array2<T>;
257
258 fn norm_preserving_scalar_map(&self, i: usize) -> Result<fn(T) -> T, anyhow::Error>;
276
277 #[must_use]
280 fn integrality_threshold(&self) -> <T as ComplexFloat>::Real;
281
282 #[must_use]
285 fn eigenvalue_comparison_mode(&self) -> &EigenvalueComparisonMode;
286
287 fn calc_smat(
302 &mut self,
303 metric: Option<&Array<T, D>>,
304 metric_h: Option<&Array<T, D>>,
305 use_cayley_table: bool,
306 ) -> Result<&mut Self, anyhow::Error> {
307 let order = self.group().order();
308 let mut smat = Array2::<T>::zeros((order, order));
309 let item_0 = self.origin();
310 if let (Some(ctb), true) = (self.group().cayley_table(), use_cayley_table) {
311 log::debug!(
312 "Cayley table available. Group closure will be used to speed up overlap matrix computation."
313 );
314 let ovs = self
315 .iter()
316 .map(|item_res| {
317 let item = item_res?;
318 item.overlap(item_0, metric, metric_h)
319 })
320 .collect::<Result<Vec<_>, _>>()?;
321 for (i, j) in (0..order).cartesian_product(0..order) {
322 let jinv = ctb
323 .slice(s![.., j])
324 .iter()
325 .position(|&x| x == 0)
326 .ok_or(format_err!(
327 "Unable to find the inverse of group element `{j}`."
328 ))?;
329 let jinv_i = ctb[(jinv, i)];
330 smat[(i, j)] = self.norm_preserving_scalar_map(jinv)?(ovs[jinv_i]);
331 }
332 } else {
333 log::debug!(
334 "Cayley table not available or the use of Cayley table not requested. Overlap matrix will be constructed without group-closure speed-up."
335 );
336 for pair in self
337 .iter()
338 .map(|item_res| item_res.map_err(|err| err.to_string()))
339 .enumerate()
340 .combinations_with_replacement(2)
341 {
342 let (w, item_w_res) = &pair[0];
343 let (x, item_x_res) = &pair[1];
344 let item_w = item_w_res
345 .as_ref()
346 .map_err(|err| format_err!(err.clone()))
347 .with_context(|| "One of the items in the orbit is not available")?;
348 let item_x = item_x_res
349 .as_ref()
350 .map_err(|err| format_err!(err.clone()))
351 .with_context(|| "One of the items in the orbit is not available")?;
352 smat[(*w, *x)] = item_w.overlap(item_x, metric, metric_h).map_err(|err| {
353 log::warn!("{err}");
354 log::warn!(
355 "Unable to calculate the overlap between items `{w}` and `{x}` in the orbit."
356 );
357 err
358 })?;
359 if *w != *x {
360 smat[(*x, *w)] = item_x.overlap(item_w, metric, metric_h).map_err(|err| {
361 log::warn!("{err}");
362 log::warn!(
363 "Unable to calculate the overlap between items `{x}` and `{w}` in the orbit."
364 );
365 err
366 })?;
367 }
368 }
369 }
370 if self.origin().complex_symmetric() {
371 self.set_smat((smat.clone() + smat.t().to_owned()).mapv(|x| x / (T::one() + T::one())))
372 } else {
373 self.set_smat(
374 (smat.clone() + smat.t().to_owned().mapv(|x| x.conj()))
375 .mapv(|x| x / (T::one() + T::one())),
376 )
377 }
378 Ok(self)
379 }
380
381 fn normalise_smat(&mut self) -> Result<&mut Self, anyhow::Error> {
389 let smat = self
390 .smat()
391 .ok_or(format_err!("No orbit overlap matrix to normalise."))?;
392 let norm = smat.diag().mapv(|x| <T as ComplexFloat>::sqrt(x));
393 let nspatial = norm.len();
394 let norm_col = norm
395 .clone()
396 .into_shape_with_order([nspatial, 1])
397 .map_err(|err| format_err!(err))?;
398 let norm_row = norm
399 .into_shape_with_order([1, nspatial])
400 .map_err(|err| format_err!(err))?;
401 let norm_mat = norm_col.dot(&norm_row);
402 let normalised_smat = smat / norm_mat;
403 self.set_smat(normalised_smat);
404 Ok(self)
405 }
406
407 fn calc_tmat(&self, op: &G::GroupElement) -> Result<Array2<T>, anyhow::Error> {
429 let ctb = self
430 .group()
431 .cayley_table()
432 .expect("The Cayley table for the group cannot be found.");
433 let i = self.group().get_index_of(op).unwrap_or_else(|| {
434 panic!("Unable to retrieve the index of element `{op}` in the group.")
435 });
436 let ix = ctb.slice(s![i, ..]).iter().cloned().collect::<Vec<_>>();
437 let twx = self
438 .smat()
439 .ok_or(format_err!("No orbit overlap matrix found."))?
440 .select(Axis(1), &ix);
441 Ok(twx)
442 }
443
444 fn calc_dmat(&self, op: &G::GroupElement) -> Result<Array2<T>, anyhow::Error> {
464 let complex_symmetric = self.origin().complex_symmetric();
465 let xmath = if complex_symmetric {
466 self.xmat().t().to_owned()
467 } else {
468 self.xmat().t().mapv(|x| x.conj())
469 };
470 let smattilde = xmath
471 .dot(
472 self.smat()
473 .ok_or(format_err!("No orbit overlap matrix found."))?,
474 )
475 .dot(self.xmat());
476 let smattilde_inv = smattilde
477 .inv()
478 .expect("The inverse of S~ could not be found.");
479 einsum(
480 "ij,jk,kl,lm->im",
481 &[&smattilde_inv, &xmath, &self.calc_tmat(op)?, self.xmat()],
482 )
483 .map_err(|err| format_err!(err))
484 .with_context(|| "Unable to compute the matrix product [(S~)^(-1) X† T X].")?
485 .into_dimensionality::<Ix2>()
486 .map_err(|err| format_err!(err))
487 .with_context(
488 || "Unable to convert the matrix product [(S~)^(-1) X† T X] to two dimensions.",
489 )
490 }
491
492 fn calc_character(&self, op: &G::GroupElement) -> Result<T, anyhow::Error> {
505 let complex_symmetric = self.origin().complex_symmetric();
506 let xmath = if complex_symmetric {
507 self.xmat().t().to_owned()
508 } else {
509 self.xmat().t().mapv(|x| x.conj())
510 };
511 let smattilde = xmath
512 .dot(
513 self.smat()
514 .ok_or(format_err!("No orbit overlap matrix found."))?,
515 )
516 .dot(self.xmat());
517 let smattilde_inv = smattilde
518 .inv()
519 .expect("The inverse of S~ could not be found.");
520 let chi = einsum(
521 "ij,jk,kl,li",
522 &[&smattilde_inv, &xmath, &self.calc_tmat(op)?, self.xmat()],
523 )
524 .map_err(|err| format_err!(err))
525 .with_context(|| "Unable to compute the trace of the matrix product [(S~)^(-1) X† T X].")?
526 .into_dimensionality::<Ix0>()
527 .map_err(|err| format_err!(err))
528 .with_context(|| "Unable to convert the trace of the matrix product [(S~)^(-1) X† T X] to zero dimensions.")?;
529 chi.into_iter().next().ok_or(format_err!(
530 "Unable to extract the character from the representation matrix."
531 ))
532 }
533
534 fn calc_characters(
543 &self,
544 ) -> Result<Vec<(<G as ClassProperties>::ClassSymbol, T)>, anyhow::Error> {
545 let complex_symmetric = self.origin().complex_symmetric();
546 let xmath = if complex_symmetric {
547 self.xmat().t().to_owned()
548 } else {
549 self.xmat().t().mapv(|x| x.conj())
550 };
551 let smattilde = xmath
552 .dot(
553 self.smat()
554 .ok_or(format_err!("No orbit overlap matrix found."))?,
555 )
556 .dot(self.xmat());
557 let smattilde_inv = smattilde
558 .inv()
559 .expect("The inverse of S~ could not be found.");
560 (0..self.group().class_number()).map(|cc_i| {
561 let cc = self.group().get_cc_symbol_of_index(cc_i).unwrap();
562 let op = self.group().get_cc_transversal(cc_i).unwrap();
563 let chi = einsum(
564 "ij,jk,kl,li",
565 &[&smattilde_inv, &xmath, &self.calc_tmat(&op)?, self.xmat()],
566 )
567 .map_err(|err| format_err!(err))
568 .with_context(|| "Unable to compute the trace of the matrix product [(S~)^(-1) X† T X].")?
569 .into_dimensionality::<Ix0>()
570 .map_err(|err| format_err!(err))
571 .with_context(|| "Unable to convert the trace of the matrix product [(S~)^(-1) X† T X] to zero dimensions.")?;
572 let chi_val = chi.into_iter().next().ok_or(format_err!(
573 "Unable to extract the character from the representation matrix."
574 ))?;
575 Ok((cc, chi_val))
576 }).collect::<Result<Vec<_>, _>>()
577 }
578
579 fn analyse_rep(
591 &self,
592 ) -> Result<
593 <<G as CharacterProperties>::CharTab as SubspaceDecomposable<T>>::Decomposition,
594 DecompositionError,
595 > {
596 let chis = self
597 .calc_characters()
598 .map_err(|err| DecompositionError(err.to_string()))?;
599 self.group().character_table().reduce_characters(
600 &chis.iter().map(|(cc, chi)| (cc, *chi)).collect::<Vec<_>>(),
601 self.integrality_threshold(),
602 )
603 }
604
605 fn characters_to_string(
616 &self,
617 chis: &[(<G as ClassProperties>::ClassSymbol, T)],
618 integrality_threshold: <T as ComplexFloat>::Real,
619 ) -> String
620 where
621 T: ComplexFloat + Lapack + fmt::Debug,
622 <T as ComplexFloat>::Real: ToPrimitive,
623 {
624 let ndigits = (-integrality_threshold.log10()).to_usize().unwrap_or(6) + 1;
625 let (ccs, characters): (Vec<_>, Vec<_>) = chis
626 .iter()
627 .map(|(cc, chi)| (cc.to_string(), format!("{chi:+.ndigits$}")))
628 .unzip();
629 let cc_width = ccs
630 .iter()
631 .map(|cc| cc.chars().count())
632 .max()
633 .unwrap_or(5)
634 .max(5);
635 let characters_width = characters
636 .iter()
637 .map(|chi| chi.chars().count())
638 .max()
639 .unwrap_or(9)
640 .max(9);
641
642 let divider = "┈".repeat(cc_width + characters_width + 4);
643 let header = format!(" {:<cc_width$} {:<}", "Class", "Character");
644 let body = Itertools::intersperse(
645 chis.iter()
646 .map(|(cc, chi)| format!(" {:<cc_width$} {:<+.ndigits$}", cc.to_string(), chi)),
647 "\n".to_string(),
648 )
649 .collect::<String>();
650
651 Itertools::intersperse(
652 [divider.clone(), header, divider.clone(), body, divider].into_iter(),
653 "\n".to_string(),
654 )
655 .collect::<String>()
656 }
657}
658
659pub trait ProjectionDecomposition<G, I, T, D>: RepAnalysis<G, I, T, D>
666where
667 T: ComplexFloat + Lapack + fmt::Debug,
668 <T as ComplexFloat>::Real: ToPrimitive,
669 G: GroupProperties + ClassProperties + CharacterProperties + Sync + Send,
670 <G as CharacterProperties>::RowSymbol: Sync + Send,
671 G::GroupElement: fmt::Display,
672 G::CharTab: SubspaceDecomposable<T>,
673 D: Dimension,
674 I: Overlap<T, D> + Clone,
675 Self: Sync + Send,
676 Self::OrbitIter: Iterator<Item = Result<I, anyhow::Error>>,
677 for<'a> Complex<f64>: Mul<&'a T, Output = Complex<f64>>,
678{
679 #[allow(clippy::type_complexity)]
699 fn calc_projection_compositions(
700 &self,
701 ) -> Result<
702 Vec<(
703 <<G as CharacterProperties>::CharTab as CharacterTable>::RowSymbol,
704 Complex<f64>,
705 )>,
706 anyhow::Error,
707 > {
708 self.group()
709 .character_table()
710 .get_all_rows()
711 .iter()
712 .map(|row_label| {
713 let group = self.group();
714 let chartab = group.character_table();
715 let id_class = group
716 .get_cc_symbol_of_index(0)
717 .ok_or(format_err!("Unable to retrieve the identity class."))?;
718 let dim = chartab
719 .get_character(row_label, &id_class).complex_value();
720 let group_order = group.order().to_f64().ok_or(
721 DecompositionError("The group order cannot be converted to `f64`.".to_string())
722 )?;
723 let projection: Complex<f64> = (0..group.order())
724 .into_par_iter()
725 .try_fold(Complex::<f64>::zero, |acc, i| {
726 let cc_i = group
727 .get_cc_of_element_index(i)
728 .ok_or(format_err!("Unable to retrieve the conjugacy class index of element index {i}."))?;
729 let cc = group
730 .get_cc_symbol_of_index(cc_i)
731 .ok_or(format_err!("Unable to retrieve the conjugacy class symbol of conjugacy class index {cc_i}."))?;
732 let chi_i_star = group.character_table().get_character(row_label, &cc).complex_conjugate();
733 let s_0i = self
734 .smat()
735 .ok_or(format_err!("No orbit overlap matrix found."))?
736 .get((0, i))
737 .ok_or(format_err!("Unable to retrieve the overlap matrix element with index `(0, {i})`."))?;
738 Ok::<_, anyhow::Error>(acc + chi_i_star.complex_value() * s_0i)
739 })
740 .try_reduce(Complex::<f64>::zero, |a, s| Ok(a + s))?;
741 #[allow(clippy::op_ref)]
742 Ok::<_, anyhow::Error>((row_label.clone(), &dim * projection / group_order))
743 }).collect::<Result<Vec<_>, anyhow::Error>>()
744 }
745
746 fn projections_to_string(
758 &self,
759 projections: &[(
760 <<G as CharacterProperties>::CharTab as CharacterTable>::RowSymbol,
761 Complex<f64>,
762 )],
763 integrality_threshold: <T as ComplexFloat>::Real,
764 ) -> String {
765 let ndigits = (-integrality_threshold.log10()).to_usize().unwrap_or(6) + 1;
766 let (row_labels, projection_values): (Vec<_>, Vec<_>) = projections
767 .iter()
768 .map(|(row, proj)| (row.to_string(), format!("{proj:+.ndigits$}")))
769 .unzip();
770 let row_width = row_labels
771 .iter()
772 .map(|row| row.chars().count())
773 .max()
774 .unwrap_or(9)
775 .max(9);
776 let proj_width = projection_values
777 .iter()
778 .map(|proj| proj.chars().count())
779 .max()
780 .unwrap_or(10)
781 .max(10);
782
783 let divider = "┈".repeat(row_width + proj_width + 4);
784 let header = format!(" {:<row_width$} {:<}", "Component", "Projection");
785 let body = Itertools::intersperse(
786 projections.iter().map(|(row, proj)| {
787 format!(" {:<row_width$} {:<+.ndigits$}", row.to_string(), proj)
788 }),
789 "\n".to_string(),
790 )
791 .collect::<String>();
792
793 Itertools::intersperse(
794 [divider.clone(), header, divider.clone(), body, divider].into_iter(),
795 "\n".to_string(),
796 )
797 .collect::<String>()
798 }
799}
800
801#[duplicate_item(
804 duplicate!{
805 [ dtype_nested; [f64]; [Complex<f64>] ]
806 [
807 gtype_ [ UnitaryRepresentedSymmetryGroup ]
808 dtype_ [ dtype_nested ]
809 ]
810 }
811)]
812impl<O, I, D> ProjectionDecomposition<gtype_, I, dtype_, D> for O
813where
814 O: RepAnalysis<gtype_, I, dtype_, D>,
815 D: Dimension,
816 I: Overlap<dtype_, D> + Clone,
817 Self: Sync + Send,
818 Self::OrbitIter: Iterator<Item = Result<I, anyhow::Error>>,
819{
820}
821
822macro_rules! fn_calc_xmat_real {
827 ( $(#[$meta:meta])* $vis:vis $func:ident ) => {
828 $(#[$meta])*
829 $vis fn $func(&mut self, preserves_full_rank: bool) -> Result<&mut Self, anyhow::Error> {
830 let thresh = self.linear_independence_threshold;
832 let smat = self
833 .smat
834 .as_ref()
835 .ok_or(format_err!("No overlap matrix found for this orbit."))?;
836 use ndarray_linalg::norm::Norm;
837 if (smat.to_owned() - smat.t()).norm_l2() > thresh {
838 Err(format_err!("Overlap matrix is not symmetric."))
839 } else {
840 let (s_eig, umat) = smat.eigh(UPLO::Lower).map_err(|err| format_err!(err))?;
841 let nonzero_s_indices = match self.eigenvalue_comparison_mode {
842 EigenvalueComparisonMode::Modulus => {
843 s_eig.iter().positions(|x| x.abs() > thresh).collect_vec()
844 }
845 EigenvalueComparisonMode::Real => {
846 s_eig.iter().positions(|x| *x > thresh).collect_vec()
847 }
848 };
849 let nonzero_s_eig = s_eig.select(Axis(0), &nonzero_s_indices);
850 let nonzero_umat = umat.select(Axis(1), &nonzero_s_indices);
851 let nullity = smat.shape()[0] - nonzero_s_indices.len();
852 let xmat = if nullity == 0 && preserves_full_rank {
853 Array2::eye(smat.shape()[0])
854 } else {
855 let s_s = Array2::<f64>::from_diag(&nonzero_s_eig.mapv(|x| 1.0 / x.sqrt()));
856 nonzero_umat.dot(&s_s)
857 };
858 self.smat_eigvals = Some(s_eig);
859 self.xmat = Some(xmat);
860 Ok(self)
861 }
862 }
863 }
864}
865
866macro_rules! fn_calc_xmat_complex {
867 ( $(#[$meta:meta])* $vis:vis $func:ident ) => {
868 $(#[$meta])*
869 $vis fn $func(&mut self, preserves_full_rank: bool) -> Result<&mut Self, anyhow::Error> {
870 let thresh = self.linear_independence_threshold;
872 let smat = self
873 .smat
874 .as_ref()
875 .ok_or(format_err!("No overlap matrix found for this orbit."))?;
876 let (s_eig, umat_nonortho) = smat.eig().map_err(|err| format_err!(err))?;
877
878 let nonzero_s_indices = match self.eigenvalue_comparison_mode {
879 EigenvalueComparisonMode::Modulus => s_eig
880 .iter()
881 .positions(|x| ComplexFloat::abs(*x) > thresh)
882 .collect_vec(),
883 EigenvalueComparisonMode::Real => {
884 if s_eig
885 .iter()
886 .any(|x| Float::abs(ComplexFloat::im(*x)) > thresh)
887 {
888 log::warn!("Comparing eigenvalues using the real parts, but not all eigenvalues are real.");
889 }
890 s_eig
891 .iter()
892 .positions(|x| ComplexFloat::re(*x) > thresh)
893 .collect_vec()
894 }
895 };
896 let nonzero_s_eig = s_eig.select(Axis(0), &nonzero_s_indices);
897 let nonzero_umat_nonortho = umat_nonortho.select(Axis(1), &nonzero_s_indices);
898
899 let nonzero_umat = complex_modified_gram_schmidt(
902 &nonzero_umat_nonortho,
903 self.origin.complex_symmetric(),
904 thresh,
905 )
906 .map_err(
907 |_| format_err!("Unable to Gram--Schmidt orthonormalise the linearly-independent eigenvectors of the overlap matrix.")
908 )?;
909
910 let nullity = smat.shape()[0] - nonzero_s_indices.len();
911 let xmat = if nullity == 0 && preserves_full_rank {
912 Array2::<Complex<T>>::eye(smat.shape()[0])
913 } else {
914 let s_s = Array2::<Complex<T>>::from_diag(
915 &nonzero_s_eig.mapv(|x| Complex::<T>::from(T::one()) / x.sqrt()),
916 );
917 nonzero_umat.dot(&s_s)
918 };
919 self.smat_eigvals = Some(s_eig);
920 self.xmat = Some(xmat);
921 Ok(self)
922 }
923 }
924}
925
926pub(crate) use fn_calc_xmat_complex;
927pub(crate) use fn_calc_xmat_real;
928
929pub(crate) fn log_overlap_eigenvalues<T>(
940 title: &str,
941 eigvals: &Array1<T>,
942 thresh: <T as ComplexFloat>::Real,
943 eigenvalue_comparison_mode: &EigenvalueComparisonMode,
944) where
945 T: std::fmt::LowerExp + ComplexFloat,
946 <T as ComplexFloat>::Real: std::fmt::LowerExp,
947{
948 let mut eigvals_sorted = eigvals.iter().collect::<Vec<_>>();
949 match eigenvalue_comparison_mode {
950 EigenvalueComparisonMode::Modulus => {
951 eigvals_sorted.sort_by(|a, b| a.abs().partial_cmp(&b.abs()).unwrap());
952 }
953 EigenvalueComparisonMode::Real => {
954 eigvals_sorted.sort_by(|a, b| a.re().partial_cmp(&b.re()).unwrap());
955 }
956 }
957 eigvals_sorted.reverse();
958 let eigvals_str = eigvals_sorted
959 .iter()
960 .map(|v| format!("{v:+.3e}"))
961 .collect::<Vec<_>>();
962 log_subtitle(title);
963 qsym2_output!("");
964
965 match eigenvalue_comparison_mode {
966 EigenvalueComparisonMode::Modulus => {
967 qsym2_output!("Eigenvalues are sorted in decreasing order of their moduli.");
968 }
969 EigenvalueComparisonMode::Real => {
970 qsym2_output!("Eigenvalues are sorted in decreasing order of their real parts.");
971 }
972 }
973 let count_length = usize::try_from(eigvals.len().ilog10() + 2).unwrap_or(2);
974 let eigval_length = eigvals_str
975 .iter()
976 .map(|v| v.chars().count())
977 .max()
978 .unwrap_or(20);
979 qsym2_output!("{}", "┈".repeat(count_length + 3 + eigval_length));
980 qsym2_output!("{:>count_length$} Eigenvalue", "#");
981 qsym2_output!("{}", "┈".repeat(count_length + 3 + eigval_length));
982 let mut write_thresh = false;
983 for (i, eigval) in eigvals_str.iter().enumerate() {
984 let cmp = match eigenvalue_comparison_mode {
985 EigenvalueComparisonMode::Modulus => {
986 eigvals_sorted[i].abs().partial_cmp(&thresh).expect(
987 "Unable to compare the modulus of an eigenvalue with the specified threshold.",
988 )
989 }
990 EigenvalueComparisonMode::Real => eigvals_sorted[i].re().partial_cmp(&thresh).expect(
991 "Unable to compare the real part of an eigenvalue with the specified threshold.",
992 ),
993 };
994 if cmp == Ordering::Less && !write_thresh {
995 let comparison_mode_str = match eigenvalue_comparison_mode {
996 EigenvalueComparisonMode::Modulus => "modulus-based",
997 EigenvalueComparisonMode::Real => "real-part-based",
998 };
999 qsym2_output!(
1000 "{} <-- linear independence threshold ({comparison_mode_str}): {:+.3e}",
1001 "-".repeat(count_length + 3 + eigval_length),
1002 thresh
1003 );
1004 write_thresh = true;
1005 }
1006 qsym2_output!("{i:>count_length$} {eigval}",);
1007 }
1008 qsym2_output!("{}", "┈".repeat(count_length + 3 + eigval_length));
1009}