1use std::collections::hash_map::DefaultHasher;
4use std::error::Error;
5use std::fmt::{self, LowerExp};
6use std::hash::{Hash, Hasher};
7
8use anyhow::{ensure, format_err};
9use itertools::{Itertools, MultiProduct};
10use log;
11use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis, Ix2, LinalgScalar, stack};
12use ndarray_einsum::einsum;
13use num::{Float, One};
14use num_complex::ComplexFloat;
15use num_traits::float::TotalOrder;
16
17use crate::analysis::EigenvalueComparisonMode;
18use crate::io::format::qsym2_warn;
19
20pub trait HashableFloat {
22 #[must_use]
36 fn round_factor(self, threshold: Self) -> Self;
37
38 fn integer_decode(self) -> (u64, i16, i8);
50}
51
52impl HashableFloat for f64 {
53 fn round_factor(self, factor: f64) -> Self {
54 (self / factor).round() * factor + 0.0
55 }
56
57 fn integer_decode(self) -> (u64, i16, i8) {
58 let bits: u64 = self.to_bits();
59 let sign: i8 = if bits >> 63 == 0 { 1 } else { -1 };
60 let mut exponent: i16 = ((bits >> 52) & 0x7ff) as i16;
61 let mantissa = if exponent == 0 {
62 (bits & 0x000f_ffff_ffff_ffff) << 1
63 } else {
64 (bits & 0x000f_ffff_ffff_ffff) | 0x0010_0000_0000_0000
65 };
66
67 exponent -= 1023 + 52;
68 (mantissa, exponent, sign)
69 }
70}
71
72pub fn calculate_hash<T: Hash>(t: &T) -> u64 {
82 let mut s = DefaultHasher::new();
83 t.hash(&mut s);
84 s.finish()
85}
86
87pub trait ProductRepeat: Iterator + Clone
89where
90 Self::Item: Clone,
91{
92 fn product_repeat(self, repeat: usize) -> MultiProduct<Self> {
104 std::iter::repeat_n(self, repeat).multi_cartesian_product()
105 }
106}
107
108impl<T: Iterator + Clone> ProductRepeat for T where T::Item: Clone {}
109
110#[derive(Debug, Clone)]
116pub struct GramSchmidtError<'a, T> {
117 pub mat: Option<&'a Array2<T>>,
118 pub vecs: Option<&'a [Array1<T>]>,
119}
120
121impl<'a, T: fmt::Display + fmt::Debug> fmt::Display for GramSchmidtError<'a, T> {
122 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
123 writeln!(f, "Unable to perform Gram--Schmidt orthogonalisation on:",)?;
124 if let Some(mat) = self.mat {
125 writeln!(f, "{mat}")?;
126 } else if let Some(vecs) = self.vecs {
127 for vec in vecs {
128 writeln!(f, "{vec}")?;
129 }
130 } else {
131 writeln!(f, "Unspecified basis vectors for Gram--Schmidt.")?;
132 }
133 Ok(())
134 }
135}
136
137impl<'a, T: fmt::Display + fmt::Debug> Error for GramSchmidtError<'a, T> {}
138
139pub fn complex_modified_gram_schmidt<T>(
159 vmat: &'_ Array2<T>,
160 complex_symmetric: bool,
161 thresh: T::Real,
162) -> Result<Array2<T>, GramSchmidtError<'_, T>>
163where
164 T: ComplexFloat + fmt::Display + 'static,
165{
166 let mut us: Vec<Array1<T>> = Vec::with_capacity(vmat.shape()[1]);
167 let mut us_sq_norm: Vec<T> = Vec::with_capacity(vmat.shape()[1]);
168 for (i, vi) in vmat.columns().into_iter().enumerate() {
169 us.push(vi.to_owned());
171
172 for j in 0..i {
176 let p_uj_ui = if complex_symmetric {
177 us[j].t().dot(&us[i]) / us_sq_norm[j]
178 } else {
179 us[j].t().map(|x| x.conj()).dot(&us[i]) / us_sq_norm[j]
180 };
181 us[i] = &us[i] - us[j].map(|&x| x * p_uj_ui);
182 }
183
184 let us_sq_norm_i = if complex_symmetric {
187 us[i].t().dot(&us[i])
188 } else {
189 us[i].t().map(|x| x.conj()).dot(&us[i])
190 };
191 if us_sq_norm_i.abs() < thresh {
192 log::error!("A zero-norm vector found: {}", us[i]);
193 return Err(GramSchmidtError {
194 mat: Some(vmat),
195 vecs: None,
196 });
197 }
198 us_sq_norm.push(us_sq_norm_i);
199 }
200
201 for i in 0..us.len() {
203 us[i].mapv_inplace(|x| x / us_sq_norm[i].sqrt());
204 }
205
206 let ortho_check = us.iter().enumerate().all(|(i, ui)| {
207 us.iter().enumerate().all(|(j, uj)| {
208 let ov_ij = if complex_symmetric {
209 ui.dot(uj)
210 } else {
211 ui.map(|x| x.conj()).dot(uj)
212 };
213 i == j || ov_ij.abs() < thresh
214 })
215 });
216
217 if ortho_check {
218 stack(Axis(1), &us.iter().map(|u| u.view()).collect_vec()).map_err(|err| {
219 log::error!("{}", err);
220 GramSchmidtError {
221 mat: Some(vmat),
222 vecs: None,
223 }
224 })
225 } else {
226 Err(GramSchmidtError {
227 mat: Some(vmat),
228 vecs: None,
229 })
230 }
231}
232
233pub(crate) fn sort_eigenvalues_eigenvectors<T: ComplexFloat>(
249 eigvals: &ArrayView1<T>,
250 eigvecs: &ArrayView2<T>,
251 eigenvalue_comparison_mode: &EigenvalueComparisonMode,
252) -> (Array1<T>, Array2<T>) {
253 let mut indices = (0..eigvals.len()).collect_vec();
254 match eigenvalue_comparison_mode {
255 EigenvalueComparisonMode::Modulus => {
256 indices.sort_by(|i, j| {
257 ComplexFloat::abs(eigvals[*i])
258 .partial_cmp(&ComplexFloat::abs(eigvals[*j]))
259 .unwrap()
260 });
261 }
262 EigenvalueComparisonMode::Real => {
263 indices.sort_by(|i, j| eigvals[*i].re().partial_cmp(&eigvals[*j].re()).unwrap());
264 }
265 }
266 let eigvals_sorted = eigvals.select(Axis(0), &indices);
267 let eigvecs_sorted = eigvecs.select(Axis(1), &indices);
268 (eigvals_sorted, eigvecs_sorted)
269}
270
271pub(crate) fn regularise_eigenvectors<T>(eigvecs: &ArrayView2<T>, thresh: T::Real) -> Array2<T>
283where
284 T: ComplexFloat + One,
285 T::Real: Float,
286{
287 let eigvecs_sgn = stack!(
288 Axis(0),
289 eigvecs
290 .row(0)
291 .map(|v| {
292 if Float::abs(ComplexFloat::re(*v)) > thresh {
293 T::from(v.re().signum()).expect("Unable to convert a signum to the right type.")
294 } else if Float::abs(ComplexFloat::im(*v)) > thresh {
295 T::from(v.im().signum()).expect("Unable to convert a signum to the right type.")
296 } else {
297 T::one()
298 }
299 })
300 .view()
301 );
302 eigvecs * eigvecs_sgn
303}
304
305pub(crate) fn normalise_eigenvectors_real<T>(
317 eigvecs: &ArrayView2<T>,
318 smat: &ArrayView2<T>,
319 thresh: T,
320) -> Result<Array2<T>, anyhow::Error>
321where
322 T: LinalgScalar + Float + std::fmt::LowerExp,
323{
324 let sq_norm = einsum("ji,jk,kl->il", &[eigvecs, smat, eigvecs])
325 .map_err(|err| format_err!(err))?
326 .into_dimensionality::<Ix2>()
327 .map_err(|err| format_err!(err))?;
328 let max_diff = (&sq_norm - &Array2::from_diag(&sq_norm.diag()))
329 .iter()
330 .map(|x| x.abs())
331 .max_by(|x, y| {
332 x.partial_cmp(y)
333 .expect("Unable to compare two `abs` values.")
334 })
335 .ok_or_else(|| {
336 format_err!(
337 "Unable to determine the maximum off-diagonal element of the C^T.S.C matrix."
338 )
339 })?;
340
341 ensure!(
342 max_diff <= thresh,
343 "The C^T.S.C matrix is not a diagonal matrix: the maximum absolute value of the off-diagonal elements is {max_diff:.3e} > {thresh:.3e}."
344 );
345 ensure!(
346 sq_norm.diag().iter().all(|v| *v > T::zero()),
347 "Some eigenvectors have negative squared norms and cannot be normalised over the reals."
348 );
349 let eigvecs_normalised = eigvecs / sq_norm.diag().map(|v| v.sqrt());
350 Ok(eigvecs_normalised)
351}
352
353pub(crate) fn normalise_eigenvectors_complex<T>(
367 eigvecs: &ArrayView2<T>,
368 smat: &ArrayView2<T>,
369 complex_symmetric: bool,
370 thresh: Option<T::Real>,
371) -> Result<Array2<T>, anyhow::Error>
372where
373 T: LinalgScalar + ComplexFloat + std::fmt::Display + std::fmt::LowerExp,
374 T::Real: Float + std::fmt::LowerExp,
375{
376 let sq_norm = if complex_symmetric {
377 einsum("ji,jk,kl->il", &[eigvecs, smat, eigvecs])
378 .map_err(|err| format_err!(err))?
379 .into_dimensionality::<Ix2>()
380 .map_err(|err| format_err!(err))?
381 } else {
382 einsum(
383 "ji,jk,kl->il",
384 &[&eigvecs.map(|v| v.conj()).view(), smat, eigvecs],
385 )
386 .map_err(|err| format_err!(err))?
387 .into_dimensionality::<Ix2>()
388 .map_err(|err| format_err!(err))?
389 };
390
391 if let Some(thr) = thresh {
392 let max_diff = (&sq_norm - &Array2::from_diag(&sq_norm.diag()))
393 .iter()
394 .map(|x| ComplexFloat::abs(*x))
395 .max_by(|x, y| {
396 x.partial_cmp(y)
397 .expect("Unable to compare two `abs` values.")
398 })
399 .ok_or_else(|| {
400 if complex_symmetric {
401 format_err!(
402 "Unable to determine the maximum off-diagonal element of the C^T.S.C matrix."
403 )
404 } else {
405 format_err!(
406 "Unable to determine the maximum off-diagonal element of the C^†.S.C matrix."
407 )
408 }
409 })?;
410
411 if complex_symmetric {
412 log::debug!("C^T.S.C:\n {sq_norm:+.8e}");
413 ensure!(
414 max_diff <= thr,
415 "The C^T.S.C matrix is not a diagonal matrix: the maximum absolute value of the off-diagonal elements is {max_diff:.3e} > {thr:.3e}."
416 )
417 } else {
418 log::debug!("C^†.S.C:\n {sq_norm:+.8e}");
419 ensure!(
420 max_diff <= thr,
421 "The C^†.S.C matrix is not a diagonal matrix: the maximum absolute value of the off-diagonal elements is {max_diff:.3e} > {thr:.3e}."
422 )
423 };
424 }
425 let eigvecs_normalised = eigvecs / sq_norm.diag().map(|v| v.sqrt());
426 Ok(eigvecs_normalised)
427}
428
429pub(crate) fn check_complex_matrix_symmetry<T>(
440 mat: &ArrayView2<T>,
441 complex_symmetric: bool,
442 thresh_offdiag: <T as ComplexFloat>::Real,
443 matname: &str,
444 matsymbol: &str,
445) -> Result<(), anyhow::Error>
446where
447 T: LinalgScalar + ComplexFloat + std::fmt::Display + std::fmt::LowerExp,
448 <T as ComplexFloat>::Real: Float + std::fmt::LowerExp + std::fmt::Display,
449{
450 if complex_symmetric {
451 let deviation = mat.to_owned() - mat.t();
452 let (pos, &max_offdiag) = deviation
453 .mapv(ComplexFloat::abs)
454 .iter()
455 .enumerate()
456 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or_else(|| panic!("Unable to compare {a} and {b}.")))
457 .ok_or_else(|| format_err!("Unable to find the maximum absolute value of the {matname} complex-symmetric deviation matrix."))?;
458 let (pos_i, pos_j) = (pos.div_euclid(mat.ncols()), pos.rem_euclid(mat.ncols()));
459 log::debug!("{matname} matrix:\n {mat:+.3e}");
460 log::debug!("{matname} matrix complex-symmetric deviation:\n {deviation:+.3e}",);
461 qsym2_warn!("{matname} matrix complex-symmetric deviation:\n {deviation:+.3e}",);
462 ensure!(
463 max_offdiag <= thresh_offdiag,
464 "The {matname} matrix is not complex-symmetric: ||{matsymbol} - ({matsymbol})^T||_∞ = {max_offdiag:.3e} > {thresh_offdiag:.3e} at ({pos_i}, {pos_j})."
465 );
466 } else {
467 let deviation = mat.to_owned() - mat.map(|v| v.conj()).t();
468 let (pos, &max_offdiag) = deviation
469 .mapv(ComplexFloat::abs)
470 .iter()
471 .enumerate()
472 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or_else(|| panic!("Unable to compare {a} and {b}.")))
473 .ok_or_else(|| format_err!("Unable to find the maximum absolute value of the {matname} complex-Hermitian deviation matrix."))?;
474 let (pos_i, pos_j) = (pos.div_euclid(mat.ncols()), pos.rem_euclid(mat.ncols()));
475 log::debug!("{matname} matrix:\n {mat:+.3e}");
476 log::debug!("{matname} matrix complex-Hermitian deviation:\n {deviation:+.3e}",);
477 qsym2_warn!("{matname} matrix complex-Hermitian deviation:\n {deviation:+.3e}",);
478 ensure!(
479 max_offdiag <= thresh_offdiag,
480 "The {matname} matrix is not complex-Hermitian: ||{matsymbol} - ({matsymbol})^†||_∞ = {max_offdiag:.3e} > {thresh_offdiag:.3e} at ({pos_i}, {pos_j})."
481 );
482 }
483 Ok(())
484}
485
486pub(crate) fn check_real_matrix_symmetry<T>(
495 mat: &ArrayView2<T>,
496 thresh_offdiag: T,
497 matname: &str,
498 matsymbol: &str,
499) -> Result<(), anyhow::Error>
500where
501 T: LowerExp + Clone + LinalgScalar + Float + TotalOrder,
502{
503 let deviation = mat.to_owned() - mat.t();
504 let (pos, &max_offdiag) = deviation
505 .map(|v| v.abs())
506 .iter()
507 .enumerate()
508 .max_by(|(_, a), (_, b)| a.total_cmp(b))
509 .ok_or_else(|| format_err!("Unable to find the maximum absolute value of the {matname} real-symmetric deviation matrix."))?;
510 log::debug!("{matname} matrix:\n {mat:+.3e}");
511 log::debug!("{matname} matrix real-symmetric deviation:\n {deviation:+.3e}",);
512 qsym2_warn!("{matname} matrix real-symmetric deviation:\n {deviation:+.3e}",);
513 let (pos_i, pos_j) = (pos.div_euclid(mat.ncols()), pos.rem_euclid(mat.ncols()));
514 ensure!(
515 max_offdiag <= thresh_offdiag,
516 "{matname} matrix is not real-symmetric: ||{matsymbol} - ({matsymbol})^T||_∞ = {max_offdiag:.3e} > {thresh_offdiag:.3e} at ({pos_i}, {pos_j})."
517 );
518 Ok(())
519}