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
27pub trait Overlap<T, D>
40where
41 T: ComplexFloat + fmt::Debug + Lapack,
42 D: Dimension,
43{
44 fn complex_symmetric(&self) -> bool;
47
48 fn overlap(
52 &self,
53 other: &Self,
54 metric: Option<&Array<T, D>>,
55 metric_h: Option<&Array<T, D>>,
56 ) -> Result<T, anyhow::Error>;
57
58 fn overlap_definition(&self) -> String;
61}
62
63pub struct OrbitIterator<'a, G, I>
73where
74 G: GroupProperties,
75{
76 group_iter: <<G as GroupProperties>::ElementCollection as IntoIterator>::IntoIter,
79
80 origin: &'a I,
82
83 action: fn(&G::GroupElement, &I) -> Result<I, anyhow::Error>,
85}
86
87impl<'a, G, I> OrbitIterator<'a, G, I>
88where
89 G: GroupProperties,
90{
91 pub fn new(
103 group: &G,
104 origin: &'a I,
105 action: fn(&G::GroupElement, &I) -> Result<I, anyhow::Error>,
106 ) -> Self {
107 Self {
108 group_iter: group.elements().clone().into_iter(),
109 origin,
110 action,
111 }
112 }
113}
114
115impl<'a, G, I> Iterator for OrbitIterator<'a, G, I>
116where
117 G: GroupProperties,
118{
119 type Item = Result<I, anyhow::Error>;
120
121 fn next(&mut self) -> Option<Self::Item> {
122 self.group_iter
123 .next()
124 .map(|op| (self.action)(&op, self.origin))
125 }
126}
127
128pub trait Orbit<G, I>
134where
135 G: GroupProperties,
136{
137 type OrbitIter: Iterator<Item = Result<I, anyhow::Error>>;
139
140 fn group(&self) -> &G;
142
143 fn origin(&self) -> &I;
145
146 fn iter(&self) -> Self::OrbitIter;
148}
149
150#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
161#[cfg_attr(feature = "python", pyclass(eq, eq_int))]
162pub enum EigenvalueComparisonMode {
163 Real,
165
166 #[default]
168 Modulus,
169}
170
171impl fmt::Display for EigenvalueComparisonMode {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 match self {
174 EigenvalueComparisonMode::Real => write!(f, "Real part"),
175 EigenvalueComparisonMode::Modulus => write!(f, "Modulus"),
176 }
177 }
178}
179
180pub trait RepAnalysis<G, I, T, D>: Orbit<G, I>
191where
192 T: ComplexFloat + Lapack + fmt::Debug,
193 <T as ComplexFloat>::Real: ToPrimitive,
194 G: GroupProperties + ClassProperties + CharacterProperties,
195 G::GroupElement: fmt::Display,
196 G::CharTab: SubspaceDecomposable<T>,
197 D: Dimension,
198 I: Overlap<T, D> + Clone,
199 Self::OrbitIter: Iterator<Item = Result<I, anyhow::Error>>,
200{
201 fn set_smat(&mut self, smat: Array2<T>);
211
212 #[must_use]
214 fn smat(&self) -> Option<&Array2<T>>;
215
216 #[must_use]
235 fn xmat(&self) -> &Array2<T>;
236
237 fn norm_preserving_scalar_map(&self, i: usize) -> Result<fn(T) -> T, anyhow::Error>;
255
256 #[must_use]
259 fn integrality_threshold(&self) -> <T as ComplexFloat>::Real;
260
261 #[must_use]
264 fn eigenvalue_comparison_mode(&self) -> &EigenvalueComparisonMode;
265
266 fn calc_smat(
281 &mut self,
282 metric: Option<&Array<T, D>>,
283 metric_h: Option<&Array<T, D>>,
284 use_cayley_table: bool,
285 ) -> Result<&mut Self, anyhow::Error> {
286 let order = self.group().order();
287 let mut smat = Array2::<T>::zeros((order, order));
288 let item_0 = self.origin();
289 if let (Some(ctb), true) = (self.group().cayley_table(), use_cayley_table) {
290 log::debug!(
291 "Cayley table available. Group closure will be used to speed up overlap matrix computation."
292 );
293 let ovs = self
294 .iter()
295 .map(|item_res| {
296 let item = item_res?;
297 item.overlap(item_0, metric, metric_h)
298 })
299 .collect::<Result<Vec<_>, _>>()?;
300 for (i, j) in (0..order).cartesian_product(0..order) {
301 let jinv = ctb
302 .slice(s![.., j])
303 .iter()
304 .position(|&x| x == 0)
305 .ok_or(format_err!(
306 "Unable to find the inverse of group element `{j}`."
307 ))?;
308 let jinv_i = ctb[(jinv, i)];
309 smat[(i, j)] = self.norm_preserving_scalar_map(jinv)?(ovs[jinv_i]);
310 }
311 } else {
312 log::debug!(
313 "Cayley table not available or the use of Cayley table not requested. Overlap matrix will be constructed without group-closure speed-up."
314 );
315 for pair in self
316 .iter()
317 .map(|item_res| item_res.map_err(|err| err.to_string()))
318 .enumerate()
319 .combinations_with_replacement(2)
320 {
321 let (w, item_w_res) = &pair[0];
322 let (x, item_x_res) = &pair[1];
323 let item_w = item_w_res
324 .as_ref()
325 .map_err(|err| format_err!(err.clone()))
326 .with_context(|| "One of the items in the orbit is not available")?;
327 let item_x = item_x_res
328 .as_ref()
329 .map_err(|err| format_err!(err.clone()))
330 .with_context(|| "One of the items in the orbit is not available")?;
331 smat[(*w, *x)] = item_w.overlap(item_x, metric, metric_h).map_err(|err| {
332 log::warn!("{err}");
333 log::warn!(
334 "Unable to calculate the overlap between items `{w}` and `{x}` in the orbit."
335 );
336 err
337 })?;
338 if *w != *x {
339 smat[(*x, *w)] = item_x.overlap(item_w, metric, metric_h).map_err(|err| {
340 log::warn!("{err}");
341 log::warn!(
342 "Unable to calculate the overlap between items `{x}` and `{w}` in the orbit."
343 );
344 err
345 })?;
346 }
347 }
348 }
349 if self.origin().complex_symmetric() {
350 self.set_smat((smat.clone() + smat.t().to_owned()).mapv(|x| x / (T::one() + T::one())))
351 } else {
352 self.set_smat(
353 (smat.clone() + smat.t().to_owned().mapv(|x| x.conj()))
354 .mapv(|x| x / (T::one() + T::one())),
355 )
356 }
357 Ok(self)
358 }
359
360 fn normalise_smat(&mut self) -> Result<&mut Self, anyhow::Error> {
368 let smat = self
369 .smat()
370 .ok_or(format_err!("No orbit overlap matrix to normalise."))?;
371 let norm = smat.diag().mapv(|x| <T as ComplexFloat>::sqrt(x));
372 let nspatial = norm.len();
373 let norm_col = norm
374 .clone()
375 .into_shape_with_order([nspatial, 1])
376 .map_err(|err| format_err!(err))?;
377 let norm_row = norm
378 .into_shape_with_order([1, nspatial])
379 .map_err(|err| format_err!(err))?;
380 let norm_mat = norm_col.dot(&norm_row);
381 let normalised_smat = smat / norm_mat;
382 self.set_smat(normalised_smat);
383 Ok(self)
384 }
385
386 fn calc_tmat(&self, op: &G::GroupElement) -> Result<Array2<T>, anyhow::Error> {
408 let ctb = self
409 .group()
410 .cayley_table()
411 .expect("The Cayley table for the group cannot be found.");
412 let i = self.group().get_index_of(op).unwrap_or_else(|| {
413 panic!("Unable to retrieve the index of element `{op}` in the group.")
414 });
415 let ix = ctb.slice(s![i, ..]).iter().cloned().collect::<Vec<_>>();
416 let twx = self
417 .smat()
418 .ok_or(format_err!("No orbit overlap matrix found."))?
419 .select(Axis(1), &ix);
420 Ok(twx)
421 }
422
423 fn calc_dmat(&self, op: &G::GroupElement) -> Result<Array2<T>, anyhow::Error> {
443 let complex_symmetric = self.origin().complex_symmetric();
444 let xmath = if complex_symmetric {
445 self.xmat().t().to_owned()
446 } else {
447 self.xmat().t().mapv(|x| x.conj())
448 };
449 let smattilde = xmath
450 .dot(
451 self.smat()
452 .ok_or(format_err!("No orbit overlap matrix found."))?,
453 )
454 .dot(self.xmat());
455 let smattilde_inv = smattilde
456 .inv()
457 .expect("The inverse of S~ could not be found.");
458 einsum(
459 "ij,jk,kl,lm->im",
460 &[&smattilde_inv, &xmath, &self.calc_tmat(op)?, self.xmat()],
461 )
462 .map_err(|err| format_err!(err))
463 .with_context(|| "Unable to compute the matrix product [(S~)^(-1) X† T X].")?
464 .into_dimensionality::<Ix2>()
465 .map_err(|err| format_err!(err))
466 .with_context(
467 || "Unable to convert the matrix product [(S~)^(-1) X† T X] to two dimensions.",
468 )
469 }
470
471 fn calc_character(&self, op: &G::GroupElement) -> Result<T, anyhow::Error> {
484 let complex_symmetric = self.origin().complex_symmetric();
485 let xmath = if complex_symmetric {
486 self.xmat().t().to_owned()
487 } else {
488 self.xmat().t().mapv(|x| x.conj())
489 };
490 let smattilde = xmath
491 .dot(
492 self.smat()
493 .ok_or(format_err!("No orbit overlap matrix found."))?,
494 )
495 .dot(self.xmat());
496 let smattilde_inv = smattilde
497 .inv()
498 .expect("The inverse of S~ could not be found.");
499 let chi = einsum(
500 "ij,jk,kl,li",
501 &[&smattilde_inv, &xmath, &self.calc_tmat(op)?, self.xmat()],
502 )
503 .map_err(|err| format_err!(err))
504 .with_context(|| "Unable to compute the trace of the matrix product [(S~)^(-1) X† T X].")?
505 .into_dimensionality::<Ix0>()
506 .map_err(|err| format_err!(err))
507 .with_context(|| "Unable to convert the trace of the matrix product [(S~)^(-1) X† T X] to zero dimensions.")?;
508 chi.into_iter().next().ok_or(format_err!(
509 "Unable to extract the character from the representation matrix."
510 ))
511 }
512
513 fn calc_characters(
522 &self,
523 ) -> Result<Vec<(<G as ClassProperties>::ClassSymbol, T)>, anyhow::Error> {
524 let complex_symmetric = self.origin().complex_symmetric();
525 let xmath = if complex_symmetric {
526 self.xmat().t().to_owned()
527 } else {
528 self.xmat().t().mapv(|x| x.conj())
529 };
530 let smattilde = xmath
531 .dot(
532 self.smat()
533 .ok_or(format_err!("No orbit overlap matrix found."))?,
534 )
535 .dot(self.xmat());
536 let smattilde_inv = smattilde
537 .inv()
538 .expect("The inverse of S~ could not be found.");
539 (0..self.group().class_number()).map(|cc_i| {
540 let cc = self.group().get_cc_symbol_of_index(cc_i).unwrap();
541 let op = self.group().get_cc_transversal(cc_i).unwrap();
542 let chi = einsum(
543 "ij,jk,kl,li",
544 &[&smattilde_inv, &xmath, &self.calc_tmat(&op)?, self.xmat()],
545 )
546 .map_err(|err| format_err!(err))
547 .with_context(|| "Unable to compute the trace of the matrix product [(S~)^(-1) X† T X].")?
548 .into_dimensionality::<Ix0>()
549 .map_err(|err| format_err!(err))
550 .with_context(|| "Unable to convert the trace of the matrix product [(S~)^(-1) X† T X] to zero dimensions.")?;
551 let chi_val = chi.into_iter().next().ok_or(format_err!(
552 "Unable to extract the character from the representation matrix."
553 ))?;
554 Ok((cc, chi_val))
555 }).collect::<Result<Vec<_>, _>>()
556 }
557
558 fn analyse_rep(
570 &self,
571 ) -> Result<
572 <<G as CharacterProperties>::CharTab as SubspaceDecomposable<T>>::Decomposition,
573 DecompositionError,
574 > {
575 let chis = self
576 .calc_characters()
577 .map_err(|err| DecompositionError(err.to_string()))?;
578 self.group().character_table().reduce_characters(
579 &chis.iter().map(|(cc, chi)| (cc, *chi)).collect::<Vec<_>>(),
580 self.integrality_threshold(),
581 )
582 }
583
584 fn characters_to_string(
595 &self,
596 chis: &[(<G as ClassProperties>::ClassSymbol, T)],
597 integrality_threshold: <T as ComplexFloat>::Real,
598 ) -> String
599 where
600 T: ComplexFloat + Lapack + fmt::Debug,
601 <T as ComplexFloat>::Real: ToPrimitive,
602 {
603 let ndigits = (-integrality_threshold.log10()).to_usize().unwrap_or(6) + 1;
604 let (ccs, characters): (Vec<_>, Vec<_>) = chis
605 .iter()
606 .map(|(cc, chi)| (cc.to_string(), format!("{chi:+.ndigits$}")))
607 .unzip();
608 let cc_width = ccs
609 .iter()
610 .map(|cc| cc.chars().count())
611 .max()
612 .unwrap_or(5)
613 .max(5);
614 let characters_width = characters
615 .iter()
616 .map(|chi| chi.chars().count())
617 .max()
618 .unwrap_or(9)
619 .max(9);
620
621 let divider = "┈".repeat(cc_width + characters_width + 4);
622 let header = format!(" {:<cc_width$} {:<}", "Class", "Character");
623 let body = Itertools::intersperse(
624 chis.iter()
625 .map(|(cc, chi)| format!(" {:<cc_width$} {:<+.ndigits$}", cc.to_string(), chi)),
626 "\n".to_string(),
627 )
628 .collect::<String>();
629
630 Itertools::intersperse(
631 [divider.clone(), header, divider.clone(), body, divider].into_iter(),
632 "\n".to_string(),
633 )
634 .collect::<String>()
635 }
636}
637
638pub trait ProjectionDecomposition<G, I, T, D>: RepAnalysis<G, I, T, D>
645where
646 T: ComplexFloat + Lapack + fmt::Debug,
647 <T as ComplexFloat>::Real: ToPrimitive,
648 G: GroupProperties + ClassProperties + CharacterProperties + Sync + Send,
649 <G as CharacterProperties>::RowSymbol: Sync + Send,
650 G::GroupElement: fmt::Display,
651 G::CharTab: SubspaceDecomposable<T>,
652 D: Dimension,
653 I: Overlap<T, D> + Clone,
654 Self: Sync + Send,
655 Self::OrbitIter: Iterator<Item = Result<I, anyhow::Error>>,
656 for<'a> Complex<f64>: Mul<&'a T, Output = Complex<f64>>,
657{
658 #[allow(clippy::type_complexity)]
678 fn calc_projection_compositions(
679 &self,
680 ) -> Result<
681 Vec<(
682 <<G as CharacterProperties>::CharTab as CharacterTable>::RowSymbol,
683 Complex<f64>,
684 )>,
685 anyhow::Error,
686 > {
687 self.group()
688 .character_table()
689 .get_all_rows()
690 .iter()
691 .map(|row_label| {
692 let group = self.group();
693 let chartab = group.character_table();
694 let id_class = group
695 .get_cc_symbol_of_index(0)
696 .ok_or(format_err!("Unable to retrieve the identity class."))?;
697 let dim = chartab
698 .get_character(row_label, &id_class).complex_value();
699 let group_order = group.order().to_f64().ok_or(
700 DecompositionError("The group order cannot be converted to `f64`.".to_string())
701 )?;
702 let projection: Complex<f64> = (0..group.order())
703 .into_par_iter()
704 .try_fold(Complex::<f64>::zero, |acc, i| {
705 let cc_i = group
706 .get_cc_of_element_index(i)
707 .ok_or(format_err!("Unable to retrieve the conjugacy class index of element index {i}."))?;
708 let cc = group
709 .get_cc_symbol_of_index(cc_i)
710 .ok_or(format_err!("Unable to retrieve the conjugacy class symbol of conjugacy class index {cc_i}."))?;
711 let chi_i_star = group.character_table().get_character(row_label, &cc).complex_conjugate();
712 let s_0i = self
713 .smat()
714 .ok_or(format_err!("No orbit overlap matrix found."))?
715 .get((0, i))
716 .ok_or(format_err!("Unable to retrieve the overlap matrix element with index `(0, {i})`."))?;
717 Ok::<_, anyhow::Error>(acc + chi_i_star.complex_value() * s_0i)
718 })
719 .try_reduce(Complex::<f64>::zero, |a, s| Ok(a + s))?;
720 #[allow(clippy::op_ref)]
721 Ok::<_, anyhow::Error>((row_label.clone(), &dim * projection / group_order))
722 }).collect::<Result<Vec<_>, anyhow::Error>>()
723 }
724
725 fn projections_to_string(
737 &self,
738 projections: &[(
739 <<G as CharacterProperties>::CharTab as CharacterTable>::RowSymbol,
740 Complex<f64>,
741 )],
742 integrality_threshold: <T as ComplexFloat>::Real,
743 ) -> String {
744 let ndigits = (-integrality_threshold.log10()).to_usize().unwrap_or(6) + 1;
745 let (row_labels, projection_values): (Vec<_>, Vec<_>) = projections
746 .iter()
747 .map(|(row, proj)| (row.to_string(), format!("{proj:+.ndigits$}")))
748 .unzip();
749 let row_width = row_labels
750 .iter()
751 .map(|row| row.chars().count())
752 .max()
753 .unwrap_or(9)
754 .max(9);
755 let proj_width = projection_values
756 .iter()
757 .map(|proj| proj.chars().count())
758 .max()
759 .unwrap_or(10)
760 .max(10);
761
762 let divider = "┈".repeat(row_width + proj_width + 4);
763 let header = format!(" {:<row_width$} {:<}", "Component", "Projection");
764 let body = Itertools::intersperse(
765 projections.iter().map(|(row, proj)| {
766 format!(" {:<row_width$} {:<+.ndigits$}", row.to_string(), proj)
767 }),
768 "\n".to_string(),
769 )
770 .collect::<String>();
771
772 Itertools::intersperse(
773 [divider.clone(), header, divider.clone(), body, divider].into_iter(),
774 "\n".to_string(),
775 )
776 .collect::<String>()
777 }
778}
779
780#[duplicate_item(
783 duplicate!{
784 [ dtype_nested; [f64]; [Complex<f64>] ]
785 [
786 gtype_ [ UnitaryRepresentedSymmetryGroup ]
787 dtype_ [ dtype_nested ]
788 ]
789 }
790)]
791impl<O, I, D> ProjectionDecomposition<gtype_, I, dtype_, D> for O
792where
793 O: RepAnalysis<gtype_, I, dtype_, D>,
794 D: Dimension,
795 I: Overlap<dtype_, D> + Clone,
796 Self: Sync + Send,
797 Self::OrbitIter: Iterator<Item = Result<I, anyhow::Error>>,
798{
799}
800
801macro_rules! fn_calc_xmat_real {
806 ( $(#[$meta:meta])* $vis:vis $func:ident ) => {
807 $(#[$meta])*
808 $vis fn $func(&mut self, preserves_full_rank: bool) -> Result<&mut Self, anyhow::Error> {
809 let thresh = self.linear_independence_threshold;
811 let smat = self
812 .smat
813 .as_ref()
814 .ok_or(format_err!("No overlap matrix found for this orbit."))?;
815 use ndarray_linalg::norm::Norm;
816 if (smat.to_owned() - smat.t()).norm_l2() > thresh {
817 Err(format_err!("Overlap matrix is not symmetric."))
818 } else {
819 let (s_eig, umat) = smat.eigh(UPLO::Lower).map_err(|err| format_err!(err))?;
820 let nonzero_s_indices = match self.eigenvalue_comparison_mode {
821 EigenvalueComparisonMode::Modulus => {
822 s_eig.iter().positions(|x| x.abs() > thresh).collect_vec()
823 }
824 EigenvalueComparisonMode::Real => {
825 s_eig.iter().positions(|x| *x > thresh).collect_vec()
826 }
827 };
828 let nonzero_s_eig = s_eig.select(Axis(0), &nonzero_s_indices);
829 let nonzero_umat = umat.select(Axis(1), &nonzero_s_indices);
830 let nullity = smat.shape()[0] - nonzero_s_indices.len();
831 let xmat = if nullity == 0 && preserves_full_rank {
832 Array2::eye(smat.shape()[0])
833 } else {
834 let s_s = Array2::<f64>::from_diag(&nonzero_s_eig.mapv(|x| 1.0 / x.sqrt()));
835 nonzero_umat.dot(&s_s)
836 };
837 self.smat_eigvals = Some(s_eig);
838 self.xmat = Some(xmat);
839 Ok(self)
840 }
841 }
842 }
843}
844
845macro_rules! fn_calc_xmat_complex {
846 ( $(#[$meta:meta])* $vis:vis $func:ident ) => {
847 $(#[$meta])*
848 $vis fn $func(&mut self, preserves_full_rank: bool) -> Result<&mut Self, anyhow::Error> {
849 let thresh = self.linear_independence_threshold;
851 let smat = self
852 .smat
853 .as_ref()
854 .ok_or(format_err!("No overlap matrix found for this orbit."))?;
855 let (s_eig, umat_nonortho) = smat.eig().map_err(|err| format_err!(err))?;
856
857 let nonzero_s_indices = match self.eigenvalue_comparison_mode {
858 EigenvalueComparisonMode::Modulus => s_eig
859 .iter()
860 .positions(|x| ComplexFloat::abs(*x) > thresh)
861 .collect_vec(),
862 EigenvalueComparisonMode::Real => {
863 if s_eig
864 .iter()
865 .any(|x| Float::abs(ComplexFloat::im(*x)) > thresh)
866 {
867 log::warn!("Comparing eigenvalues using the real parts, but not all eigenvalues are real.");
868 }
869 s_eig
870 .iter()
871 .positions(|x| ComplexFloat::re(*x) > thresh)
872 .collect_vec()
873 }
874 };
875 let nonzero_s_eig = s_eig.select(Axis(0), &nonzero_s_indices);
876 let nonzero_umat_nonortho = umat_nonortho.select(Axis(1), &nonzero_s_indices);
877
878 let nonzero_umat = complex_modified_gram_schmidt(
881 &nonzero_umat_nonortho,
882 self.origin.complex_symmetric(),
883 thresh,
884 )
885 .map_err(
886 |_| format_err!("Unable to Gram--Schmidt orthonormalise the linearly-independent eigenvectors of the overlap matrix.")
887 )?;
888
889 let nullity = smat.shape()[0] - nonzero_s_indices.len();
890 let xmat = if nullity == 0 && preserves_full_rank {
891 Array2::<Complex<T>>::eye(smat.shape()[0])
892 } else {
893 let s_s = Array2::<Complex<T>>::from_diag(
894 &nonzero_s_eig.mapv(|x| Complex::<T>::from(T::one()) / x.sqrt()),
895 );
896 nonzero_umat.dot(&s_s)
897 };
898 self.smat_eigvals = Some(s_eig);
899 self.xmat = Some(xmat);
900 Ok(self)
901 }
902 }
903}
904
905pub(crate) use fn_calc_xmat_complex;
906pub(crate) use fn_calc_xmat_real;
907
908pub(crate) fn log_overlap_eigenvalues<T>(
919 title: &str,
920 eigvals: &Array1<T>,
921 thresh: <T as ComplexFloat>::Real,
922 eigenvalue_comparison_mode: &EigenvalueComparisonMode,
923) where
924 T: std::fmt::LowerExp + ComplexFloat,
925 <T as ComplexFloat>::Real: std::fmt::LowerExp,
926{
927 let mut eigvals_sorted = eigvals.iter().collect::<Vec<_>>();
928 match eigenvalue_comparison_mode {
929 EigenvalueComparisonMode::Modulus => {
930 eigvals_sorted.sort_by(|a, b| a.abs().partial_cmp(&b.abs()).unwrap());
931 }
932 EigenvalueComparisonMode::Real => {
933 eigvals_sorted.sort_by(|a, b| a.re().partial_cmp(&b.re()).unwrap());
934 }
935 }
936 eigvals_sorted.reverse();
937 let eigvals_str = eigvals_sorted
938 .iter()
939 .map(|v| format!("{v:+.3e}"))
940 .collect::<Vec<_>>();
941 log_subtitle(title);
942 qsym2_output!("");
943
944 match eigenvalue_comparison_mode {
945 EigenvalueComparisonMode::Modulus => {
946 qsym2_output!("Eigenvalues are sorted in decreasing order of their moduli.");
947 }
948 EigenvalueComparisonMode::Real => {
949 qsym2_output!("Eigenvalues are sorted in decreasing order of their real parts.");
950 }
951 }
952 let count_length = usize::try_from(eigvals.len().ilog10() + 2).unwrap_or(2);
953 let eigval_length = eigvals_str
954 .iter()
955 .map(|v| v.chars().count())
956 .max()
957 .unwrap_or(20);
958 qsym2_output!("{}", "┈".repeat(count_length + 3 + eigval_length));
959 qsym2_output!("{:>count_length$} Eigenvalue", "#");
960 qsym2_output!("{}", "┈".repeat(count_length + 3 + eigval_length));
961 let mut write_thresh = false;
962 for (i, eigval) in eigvals_str.iter().enumerate() {
963 let cmp = match eigenvalue_comparison_mode {
964 EigenvalueComparisonMode::Modulus => {
965 eigvals_sorted[i].abs().partial_cmp(&thresh).expect(
966 "Unable to compare the modulus of an eigenvalue with the specified threshold.",
967 )
968 }
969 EigenvalueComparisonMode::Real => eigvals_sorted[i].re().partial_cmp(&thresh).expect(
970 "Unable to compare the real part of an eigenvalue with the specified threshold.",
971 ),
972 };
973 if cmp == Ordering::Less && !write_thresh {
974 let comparison_mode_str = match eigenvalue_comparison_mode {
975 EigenvalueComparisonMode::Modulus => "modulus-based",
976 EigenvalueComparisonMode::Real => "real-part-based",
977 };
978 qsym2_output!(
979 "{} <-- linear independence threshold ({comparison_mode_str}): {:+.3e}",
980 "-".repeat(count_length + 3 + eigval_length),
981 thresh
982 );
983 write_thresh = true;
984 }
985 qsym2_output!("{i:>count_length$} {eigval}",);
986 }
987 qsym2_output!("{}", "┈".repeat(count_length + 3 + eigval_length));
988}