qsym2/drivers/representation_analysis/
density.rs

1//! Driver for symmetry analysis of electron densities.
2
3use std::collections::HashSet;
4use std::fmt;
5use std::ops::Mul;
6
7use anyhow::{self, bail, format_err};
8use derive_builder::Builder;
9use duplicate::duplicate_item;
10use ndarray::Array4;
11use ndarray_linalg::types::Lapack;
12use num_complex::{Complex, ComplexFloat};
13use num_traits::Float;
14use serde::{Deserialize, Serialize};
15
16use crate::analysis::{EigenvalueComparisonMode, RepAnalysis};
17use crate::chartab::SubspaceDecomposable;
18use crate::chartab::chartab_group::CharacterProperties;
19use crate::drivers::QSym2Driver;
20use crate::drivers::representation_analysis::angular_function::{
21    AngularFunctionRepAnalysisParams, find_angular_function_representation,
22    find_spinor_function_representation,
23};
24use crate::drivers::representation_analysis::{
25    CharacterTableDisplay, MagneticSymmetryAnalysisKind, fn_construct_magnetic_group,
26    fn_construct_unitary_group, log_bao, log_cc_transversal,
27};
28use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
29use crate::group::{GroupProperties, MagneticRepresentedGroup, UnitaryRepresentedGroup};
30use crate::io::format::{
31    QSym2Output, log_subtitle, nice_bool, qsym2_output, write_subtitle, write_title,
32};
33use crate::symmetry::symmetry_group::{
34    MagneticRepresentedSymmetryGroup, SymmetryGroupProperties, UnitaryRepresentedSymmetryGroup,
35};
36use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
37use crate::target::density::Density;
38use crate::target::density::density_analysis::DensitySymmetryOrbit;
39
40#[cfg(test)]
41#[path = "density_tests.rs"]
42mod density_tests;
43
44// ==================
45// Struct definitions
46// ==================
47
48// ----------
49// Parameters
50// ----------
51
52const fn default_true() -> bool {
53    true
54}
55const fn default_symbolic() -> Option<CharacterTableDisplay> {
56    Some(CharacterTableDisplay::Symbolic)
57}
58
59/// Structure containing control parameters for electron density representation analysis.
60#[derive(Clone, Builder, Debug, Serialize, Deserialize)]
61pub struct DensityRepAnalysisParams<T: From<f64>> {
62    /// Threshold for checking if subspace multiplicities are integral.
63    pub integrality_threshold: T,
64
65    /// Threshold for determining zero eigenvalues in the orbit overlap matrix.
66    pub linear_independence_threshold: T,
67
68    /// Option indicating if the magnetic group is to be used for symmetry analysis, and if so,
69    /// whether unitary representations or unitary-antiunitary corepresentations should be used.
70    #[builder(default = "None")]
71    #[serde(default)]
72    pub use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
73
74    /// Boolean indicating if the double group is to be used for symmetry analysis.
75    #[builder(default = "false")]
76    #[serde(default)]
77    pub use_double_group: bool,
78
79    /// Boolean indicating if the Cayley table of the group, if available, should be used to speed
80    /// up the computation of orbit overlap matrices.
81    #[builder(default = "true")]
82    #[serde(default = "default_true")]
83    pub use_cayley_table: bool,
84
85    /// The kind of symmetry transformation to be applied on the reference density to generate
86    /// the orbit for symmetry analysis.
87    #[builder(default = "SymmetryTransformationKind::Spatial")]
88    #[serde(default)]
89    pub symmetry_transformation_kind: SymmetryTransformationKind,
90
91    /// Option indicating if the character table of the group used for symmetry analysis is to be
92    /// printed out.
93    #[builder(default = "Some(CharacterTableDisplay::Symbolic)")]
94    #[serde(default = "default_symbolic")]
95    pub write_character_table: Option<CharacterTableDisplay>,
96
97    /// The comparison mode for filtering out orbit overlap eigenvalues.
98    #[builder(default = "EigenvalueComparisonMode::Modulus")]
99    #[serde(default)]
100    pub eigenvalue_comparison_mode: EigenvalueComparisonMode,
101
102    /// The finite order to which any infinite-order symmetry element is reduced, so that a finite
103    /// subgroup of an infinite group can be used for the symmetry analysis.
104    #[builder(default = "None")]
105    #[serde(default)]
106    pub infinite_order_to_finite: Option<u32>,
107}
108
109impl<T> DensityRepAnalysisParams<T>
110where
111    T: Float + From<f64>,
112{
113    /// Returns a builder to construct a [`DensityRepAnalysisParams`] structure.
114    pub fn builder() -> DensityRepAnalysisParamsBuilder<T> {
115        DensityRepAnalysisParamsBuilder::default()
116    }
117}
118
119impl Default for DensityRepAnalysisParams<f64> {
120    fn default() -> Self {
121        Self::builder()
122            .integrality_threshold(1e-7)
123            .linear_independence_threshold(1e-7)
124            .build()
125            .expect("Unable to construct a default `DensityRepAnalysisParams<f64>`.")
126    }
127}
128
129impl<T> fmt::Display for DensityRepAnalysisParams<T>
130where
131    T: From<f64> + fmt::LowerExp + fmt::Debug,
132{
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        writeln!(
135            f,
136            "Integrality threshold: {:.3e}",
137            self.integrality_threshold
138        )?;
139        writeln!(
140            f,
141            "Linear independence threshold: {:.3e}",
142            self.linear_independence_threshold
143        )?;
144        writeln!(
145            f,
146            "Orbit eigenvalue comparison mode: {}",
147            self.eigenvalue_comparison_mode
148        )?;
149        writeln!(f)?;
150        writeln!(
151            f,
152            "Use magnetic group for analysis: {}",
153            match self.use_magnetic_group {
154                None => "no",
155                Some(MagneticSymmetryAnalysisKind::Representation) =>
156                    "yes, using unitary representations",
157                Some(MagneticSymmetryAnalysisKind::Corepresentation) =>
158                    "yes, using magnetic corepresentations",
159            }
160        )?;
161        writeln!(
162            f,
163            "Use double group for analysis: {}",
164            nice_bool(self.use_double_group)
165        )?;
166        writeln!(
167            f,
168            "Use Cayley table for orbit overlap matrices: {}",
169            nice_bool(self.use_cayley_table)
170        )?;
171        if let Some(finite_order) = self.infinite_order_to_finite {
172            writeln!(f, "Infinite order to finite: {finite_order}")?;
173        }
174        writeln!(
175            f,
176            "Symmetry transformation kind: {}",
177            self.symmetry_transformation_kind
178        )?;
179        writeln!(f)?;
180        writeln!(
181            f,
182            "Write character table: {}",
183            if let Some(chartab_display) = self.write_character_table.as_ref() {
184                format!("yes, {}", chartab_display.to_string().to_lowercase())
185            } else {
186                "no".to_string()
187            }
188        )?;
189
190        Ok(())
191    }
192}
193
194// ------
195// Result
196// ------
197
198/// Structure to contain electron density representation analysis results.
199#[derive(Clone, Builder)]
200pub struct DensityRepAnalysisResult<'a, G, T>
201where
202    G: SymmetryGroupProperties + Clone,
203    G::CharTab: SubspaceDecomposable<T>,
204    T: ComplexFloat + Lapack,
205    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
206{
207    /// The control parameters used to obtain this set of electron density representation analysis
208    /// results.
209    parameters: &'a DensityRepAnalysisParams<<T as ComplexFloat>::Real>,
210
211    /// The densities being analysed and their associated names or descriptions.
212    densities: Vec<(String, &'a Density<'a, T>)>,
213
214    /// The group used for the representation analysis.
215    group: G,
216
217    /// The deduced symmetries of the electron densities.
218    density_symmetries: Vec<Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>>,
219
220    /// The overlap eigenvalues above and below the linear independence threshold for each
221    /// electron density symmetry deduction.
222    density_symmetries_thresholds: Vec<(Option<T>, Option<T>)>,
223}
224
225impl<'a, G, T> DensityRepAnalysisResult<'a, G, T>
226where
227    G: SymmetryGroupProperties + Clone,
228    G::CharTab: SubspaceDecomposable<T>,
229    T: ComplexFloat + Lapack,
230    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
231{
232    /// Returns a builder to construct a new [`DensityRepAnalysisResultBuilder`]
233    /// structure.
234    pub fn builder() -> DensityRepAnalysisResultBuilder<'a, G, T> {
235        DensityRepAnalysisResultBuilder::default()
236    }
237
238    /// Returns the control parameters used to obtain this set of electron density representation
239    /// analysis results.
240    pub fn parameters(&self) -> &DensityRepAnalysisParams<<T as ComplexFloat>::Real> {
241        self.parameters
242    }
243
244    /// Returns the densities being analysed and their associated names or descriptions.
245    pub fn densities(&self) -> &Vec<(String, &'a Density<'a, T>)> {
246        &self.densities
247    }
248}
249
250impl<'a, G, T> fmt::Display for DensityRepAnalysisResult<'a, G, T>
251where
252    G: SymmetryGroupProperties + Clone,
253    G::CharTab: SubspaceDecomposable<T>,
254    T: ComplexFloat + Lapack,
255    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
256{
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        write_subtitle(f, "Orbit-based symmetry analysis results")?;
259        writeln!(f)?;
260        writeln!(
261            f,
262            "> Group: {} ({})",
263            self.group
264                .finite_subgroup_name()
265                .map(|subgroup_name| format!("{} > {}", self.group.name(), subgroup_name))
266                .unwrap_or(self.group.name()),
267            self.group.group_type().to_string().to_lowercase()
268        )?;
269        writeln!(f)?;
270
271        let density_index_length = usize::try_from(self.densities.len().ilog10() + 1)
272            .unwrap_or(1)
273            .max(1);
274        let density_text_length = self
275            .densities
276            .iter()
277            .map(|(desc, _)| desc.chars().count())
278            .max()
279            .unwrap_or(7)
280            .max(7);
281        let symmetry_length = self
282            .density_symmetries
283            .iter()
284            .map(|den_sym_res| {
285                den_sym_res
286                    .as_ref()
287                    .map(|sym| sym.to_string())
288                    .unwrap_or_else(|err| err.clone())
289                    .chars()
290                    .count()
291            })
292            .max()
293            .unwrap_or(8)
294            .max(8);
295        let eig_above_length: usize = self
296            .density_symmetries_thresholds
297            .iter()
298            .map(|(eig_above_opt, _)| {
299                eig_above_opt
300                    .as_ref()
301                    .map(|eig_above| format!("{eig_above:+.3e}").chars().count())
302                    .unwrap_or(10)
303            })
304            .max()
305            .unwrap_or(10)
306            .max(10);
307        let eig_below_length: usize = self
308            .density_symmetries_thresholds
309            .iter()
310            .map(|(_, eig_below_opt)| {
311                eig_below_opt
312                    .as_ref()
313                    .map(|eig_below| format!("{eig_below:+.3e}").chars().count())
314                    .unwrap_or(10)
315            })
316            .max()
317            .unwrap_or(10)
318            .max(10);
319
320        let table_width = 10
321            + density_index_length
322            + density_text_length
323            + symmetry_length
324            + eig_above_length
325            + eig_below_length;
326
327        writeln!(f, "> Density results")?;
328        writeln!(f, "{}", "┈".repeat(table_width))?;
329        writeln!(
330            f,
331            " {:>density_index_length$}  {:<density_text_length$}  {:<symmetry_length$}  {:<eig_above_length$}  Eig. below",
332            "#", "Density", "Symmetry", "Eig. above",
333        )?;
334        writeln!(f, "{}", "┈".repeat(table_width))?;
335        for (deni, (((den, _), den_sym), (eig_above_opt, eig_below_opt))) in self
336            .densities
337            .iter()
338            .zip(self.density_symmetries.iter())
339            .zip(self.density_symmetries_thresholds.iter())
340            .enumerate()
341        {
342            writeln!(
343                f,
344                " {:>density_index_length$}  {:<density_text_length$}  {:<symmetry_length$}  {:<eig_above_length$}  {}",
345                deni,
346                den,
347                den_sym
348                    .as_ref()
349                    .map(|sym| sym.to_string())
350                    .unwrap_or_else(|err| err.clone()),
351                eig_above_opt
352                    .map(|eig_above| format!("{eig_above:>+.3e}"))
353                    .unwrap_or("--".to_string()),
354                eig_below_opt
355                    .map(|eig_below| format!("{eig_below:>+.3e}"))
356                    .unwrap_or("--".to_string()),
357            )?;
358        }
359        writeln!(f, "{}", "┈".repeat(table_width))?;
360        writeln!(f)?;
361
362        Ok(())
363    }
364}
365
366impl<'a, G, T> fmt::Debug for DensityRepAnalysisResult<'a, G, T>
367where
368    G: SymmetryGroupProperties + Clone,
369    G::CharTab: SubspaceDecomposable<T>,
370    T: ComplexFloat + Lapack,
371    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
372{
373    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374        writeln!(f, "{self}")
375    }
376}
377
378impl<'a, G, T> DensityRepAnalysisResult<'a, G, T>
379where
380    G: SymmetryGroupProperties + Clone,
381    G::CharTab: SubspaceDecomposable<T>,
382    T: ComplexFloat + Lapack,
383    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
384{
385    /// Returns the symmetries of the specified densities obtained from the analysis result.
386    pub fn density_symmetries(
387        &self,
388    ) -> &Vec<Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>> {
389        &self.density_symmetries
390    }
391}
392
393// ------
394// Driver
395// ------
396
397// ~~~~~~~~~~~~~~~~~
398// Struct definition
399// ~~~~~~~~~~~~~~~~~
400
401/// Driver structure for performing representation analysis on electron densities.
402#[derive(Clone, Builder)]
403#[builder(build_fn(validate = "Self::validate"))]
404pub struct DensityRepAnalysisDriver<'a, G, T>
405where
406    G: SymmetryGroupProperties + Clone,
407    G::CharTab: SubspaceDecomposable<T>,
408    T: ComplexFloat + Lapack,
409    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
410{
411    /// The control parameters for electron density representation analysis.
412    parameters: &'a DensityRepAnalysisParams<<T as ComplexFloat>::Real>,
413
414    /// The densities being analysed and their associated names or descriptions.
415    densities: Vec<(String, &'a Density<'a, T>)>,
416
417    /// The result from symmetry-group detection on the underlying molecular structure of the
418    /// electron densities.
419    symmetry_group: &'a SymmetryGroupDetectionResult,
420
421    /// The atomic-orbital four-centre spatial overlap matrix of the underlying basis set used to
422    /// describe the densities.
423    sao_spatial_4c: &'a Array4<T>,
424
425    /// The complex-symmetric atomic-orbital four-centre spatial overlap matrix of the underlying
426    /// basis set used to describe the densities. This is required if antiunitary symmetry
427    /// operations are involved. If none is provided, this will be assumed to be the same as
428    /// [`Self::sao_spatial_4c`].
429    #[builder(default = "None")]
430    sao_spatial_4c_h: Option<&'a Array4<T>>,
431
432    /// The control parameters for symmetry analysis of angular functions.
433    angular_function_parameters: &'a AngularFunctionRepAnalysisParams,
434
435    /// The result of the electron density representation analysis.
436    #[builder(setter(skip), default = "None")]
437    result: Option<DensityRepAnalysisResult<'a, G, T>>,
438}
439
440impl<'a, G, T> DensityRepAnalysisDriverBuilder<'a, G, T>
441where
442    G: SymmetryGroupProperties + Clone,
443    G::CharTab: SubspaceDecomposable<T>,
444    T: ComplexFloat + Lapack,
445    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
446{
447    fn validate(&self) -> Result<(), String> {
448        let params = self
449            .parameters
450            .ok_or("No electron density representation analysis parameters found.".to_string())?;
451
452        let sym_res = self
453            .symmetry_group
454            .ok_or("No symmetry group information found.".to_string())?;
455
456        let sao_spatial_4c = self
457            .sao_spatial_4c
458            .ok_or("No four-centre spatial SAO matrix found.".to_string())?;
459
460        if let Some(sao_spatial_4c_h) = self.sao_spatial_4c_h.flatten()
461            && sao_spatial_4c_h.shape() != sao_spatial_4c.shape()
462        {
463            return Err(
464                "Mismatched shapes between `sao_spatial_4c` and `sao_spatial_4c_h`.".to_string(),
465            );
466        }
467
468        let dens = self
469            .densities
470            .as_ref()
471            .ok_or("No electron densities found.".to_string())?;
472
473        let sym = if params.use_magnetic_group.is_some() {
474            sym_res
475                .magnetic_symmetry
476                .as_ref()
477                .ok_or("Magnetic symmetry requested for representation analysis, but no magnetic symmetry found.")?
478        } else {
479            &sym_res.unitary_symmetry
480        };
481
482        if sym.is_infinite() && params.infinite_order_to_finite.is_none() {
483            Err(format!(
484                "Representation analysis cannot be performed using the entirety of the infinite group `{}`. \
485                    Consider setting the parameter `infinite_order_to_finite` to restrict to a finite subgroup instead.",
486                sym.group_name
487                    .as_ref()
488                    .expect("No symmetry group name found.")
489            ))
490        } else {
491            let baos = dens
492                .iter()
493                .map(|(_, den)| den.bao())
494                .collect::<HashSet<_>>();
495            if baos.len() != 1 {
496                Err("Inconsistent basis angular order information between densities.".to_string())
497            } else {
498                let naos = sao_spatial_4c.shape().iter().collect::<HashSet<_>>();
499                if naos.len() != 1 {
500                    Err("The shape of the four-centre spatial SAO tensor is invalid: all four dimensions must have the same length.".to_string())
501                } else {
502                    let nao = **naos.iter().next().ok_or(
503                        "Unable to extract the dimensions of the four-centre spatial SAO tensor.",
504                    )?;
505                    if !dens.iter().all(|(_, den)| den.bao().n_funcs() == nao) {
506                        Err("The dimensions of the four-centre spatial SAO tensor do not match the number of spatial AO basis functions.".to_string())
507                    } else {
508                        Ok(())
509                    }
510                }
511            }
512        }
513    }
514}
515
516// ~~~~~~~~~~~~~~~~~~~~~~
517// Struct implementations
518// ~~~~~~~~~~~~~~~~~~~~~~
519
520// Generic for all symmetry groups G and density numeric type T
521// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
522
523impl<'a, G, T> DensityRepAnalysisDriver<'a, G, T>
524where
525    G: SymmetryGroupProperties + Clone,
526    G::CharTab: SubspaceDecomposable<T>,
527    T: ComplexFloat + Lapack,
528    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
529{
530    /// Returns a builder to construct a [`DensityRepAnalysisDriver`] structure.
531    pub fn builder() -> DensityRepAnalysisDriverBuilder<'a, G, T> {
532        DensityRepAnalysisDriverBuilder::default()
533    }
534}
535
536// Specific for unitary-represented symmetry groups, but generic for density numeric type T
537// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
538
539impl<'a, T> DensityRepAnalysisDriver<'a, UnitaryRepresentedSymmetryGroup, T>
540where
541    T: ComplexFloat + Lapack + Sync + Send,
542    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + Sync + Send,
543    for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
544{
545    fn_construct_unitary_group!(
546        /// Constructs the unitary-represented group (which itself can be unitary or magnetic) ready
547        /// for electron density representation analysis.
548        construct_unitary_group
549    );
550}
551
552// Specific for magnetic-represented symmetry groups, but generic for density numeric type T
553// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
554
555impl<'a, T> DensityRepAnalysisDriver<'a, MagneticRepresentedSymmetryGroup, T>
556where
557    T: ComplexFloat + Lapack + Sync + Send,
558    <T as ComplexFloat>::Real: From<f64> + Sync + Send + fmt::LowerExp + fmt::Debug,
559    for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
560{
561    fn_construct_magnetic_group!(
562        /// Constructs the magnetic-represented group (which itself can only be magnetic) ready for
563        /// electron density corepresentation analysis.
564        construct_magnetic_group
565    );
566}
567
568// Specific for unitary-represented and magnetic-represented symmetry groups and density numeric types f64 and C128
569// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
570
571#[duplicate_item(
572    duplicate!{
573        [ dtype_nested; [f64]; [Complex<f64>] ]
574        [
575            gtype_ [ UnitaryRepresentedSymmetryGroup ]
576            dtype_ [ dtype_nested ]
577            doc_sub_ [ "Performs representation analysis using a unitary-represented group and stores the result." ]
578            analyse_fn_ [ analyse_representation ]
579            construct_group_ [ self.construct_unitary_group()? ]
580        ]
581    }
582    duplicate!{
583        [ dtype_nested; [f64]; [Complex<f64>] ]
584        [
585            gtype_ [ MagneticRepresentedSymmetryGroup ]
586            dtype_ [ dtype_nested ]
587            doc_sub_ [ "Performs corepresentation analysis using a magnetic-represented group and stores the result." ]
588            analyse_fn_ [ analyse_corepresentation ]
589            construct_group_ [ self.construct_magnetic_group()? ]
590        ]
591    }
592)]
593impl<'a> DensityRepAnalysisDriver<'a, gtype_, dtype_> {
594    #[doc = doc_sub_]
595    fn analyse_fn_(&mut self) -> Result<(), anyhow::Error> {
596        let params = self.parameters;
597        let sao_spatial_4c = self.sao_spatial_4c;
598        let sao_spatial_4c_h = self.sao_spatial_4c_h;
599        let group = construct_group_;
600        log_cc_transversal(&group);
601        let _ = find_angular_function_representation(&group, self.angular_function_parameters);
602        if group.is_double_group() {
603            let _ = find_spinor_function_representation(&group, self.angular_function_parameters);
604        }
605        let bao = self
606            .densities
607            .first()
608            .map(|(_, den)| den.bao())
609            .ok_or_else(|| {
610                format_err!("Basis angular order information could not be extracted.")
611            })?;
612        log_bao(bao, None);
613
614        let (den_symmetries, den_symmetries_thresholds): (Vec<_>, Vec<_>) =
615            self.densities.iter().map(|(_, den)| {
616                DensitySymmetryOrbit::builder()
617                    .group(&group)
618                    .origin(den)
619                    .integrality_threshold(params.integrality_threshold)
620                    .linear_independence_threshold(params.linear_independence_threshold)
621                    .symmetry_transformation_kind(params.symmetry_transformation_kind.clone())
622                    .eigenvalue_comparison_mode(params.eigenvalue_comparison_mode.clone())
623                    .build()
624                    .map_err(|err| format_err!(err))
625                    .and_then(|mut den_orbit| {
626                        den_orbit
627                            .calc_smat(Some(sao_spatial_4c), sao_spatial_4c_h, params.use_cayley_table)?
628                            .normalise_smat()?
629                            .calc_xmat(false)?;
630                        let density_symmetry_thresholds = den_orbit
631                            .smat_eigvals
632                            .as_ref()
633                            .map(|eigvals| {
634                                let mut eigvals_vec = eigvals.iter().collect::<Vec<_>>();
635                                match den_orbit.eigenvalue_comparison_mode() {
636                                    EigenvalueComparisonMode::Modulus => {
637                                        eigvals_vec.sort_by(|a, b| {
638                                            a.abs().partial_cmp(&b.abs()).expect("Unable to compare two eigenvalues based on their moduli.")
639                                        });
640                                    }
641                                    EigenvalueComparisonMode::Real => {
642                                        eigvals_vec.sort_by(|a, b| {
643                                            a.re().partial_cmp(&b.re()).expect("Unable to compare two eigenvalues based on their real parts.")
644                                        });
645                                    }
646                                }
647                                let eigval_above = match den_orbit.eigenvalue_comparison_mode() {
648                                    EigenvalueComparisonMode::Modulus => eigvals_vec
649                                        .iter()
650                                        .find(|val| {
651                                            val.abs() >= den_orbit.linear_independence_threshold
652                                        })
653                                        .copied()
654                                        .copied(),
655                                    EigenvalueComparisonMode::Real => eigvals_vec
656                                        .iter()
657                                        .find(|val| {
658                                            val.re() >= den_orbit.linear_independence_threshold
659                                        })
660                                        .copied()
661                                        .copied(),
662                                };
663                                eigvals_vec.reverse();
664                                let eigval_below = match den_orbit.eigenvalue_comparison_mode() {
665                                    EigenvalueComparisonMode::Modulus => eigvals_vec
666                                        .iter()
667                                        .find(|val| {
668                                            val.abs() < den_orbit.linear_independence_threshold
669                                        })
670                                        .copied()
671                                        .copied(),
672                                    EigenvalueComparisonMode::Real => eigvals_vec
673                                        .iter()
674                                        .find(|val| {
675                                            val.re() < den_orbit.linear_independence_threshold
676                                        })
677                                        .copied()
678                                        .copied(),
679                                };
680                                (eigval_above, eigval_below)
681                            })
682                            .unwrap_or((None, None));
683                        let den_sym = den_orbit.analyse_rep().map_err(|err| err.to_string());
684                        Ok((den_sym, density_symmetry_thresholds))
685                    })
686                    .unwrap_or_else(|err| (Err(err.to_string()), (None, None)))
687            }).unzip();
688
689        let result = DensityRepAnalysisResult::builder()
690            .parameters(params)
691            .densities(self.densities.clone())
692            .group(group)
693            .density_symmetries(den_symmetries)
694            .density_symmetries_thresholds(den_symmetries_thresholds)
695            .build()?;
696        self.result = Some(result);
697
698        Ok(())
699    }
700}
701
702// ~~~~~~~~~~~~~~~~~~~~~
703// Trait implementations
704// ~~~~~~~~~~~~~~~~~~~~~
705
706// Generic for all symmetry groups G and density numeric type T
707// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
708
709impl<'a, G, T> fmt::Display for DensityRepAnalysisDriver<'a, G, T>
710where
711    G: SymmetryGroupProperties + Clone,
712    G::CharTab: SubspaceDecomposable<T>,
713    T: ComplexFloat + Lapack,
714    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
715{
716    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
717        write_title(f, "Electron Density Symmetry Analysis")?;
718        writeln!(f)?;
719        writeln!(f, "{}", self.parameters)?;
720        Ok(())
721    }
722}
723
724impl<'a, G, T> fmt::Debug for DensityRepAnalysisDriver<'a, G, T>
725where
726    G: SymmetryGroupProperties + Clone,
727    G::CharTab: SubspaceDecomposable<T>,
728    T: ComplexFloat + Lapack,
729    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
730{
731    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732        writeln!(f, "{self}")
733    }
734}
735
736// Specific for unitary/magnetic-represented groups and density numeric type f64/Complex<f64>
737// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
738
739#[duplicate_item(
740    duplicate!{
741        [ dtype_nested; [f64]; [Complex<f64>] ]
742        [
743            gtype_ [ UnitaryRepresentedSymmetryGroup ]
744            dtype_ [ dtype_nested ]
745            analyse_fn_ [ analyse_representation ]
746        ]
747    }
748    duplicate!{
749        [ dtype_nested; [f64]; [Complex<f64>] ]
750        [
751            gtype_ [ MagneticRepresentedSymmetryGroup ]
752            dtype_ [ dtype_nested ]
753            analyse_fn_ [ analyse_corepresentation ]
754        ]
755    }
756)]
757impl<'a> QSym2Driver for DensityRepAnalysisDriver<'a, gtype_, dtype_> {
758    type Params = DensityRepAnalysisParams<f64>;
759
760    type Outcome = DensityRepAnalysisResult<'a, gtype_, dtype_>;
761
762    fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
763        self.result.as_ref().ok_or_else(|| {
764            format_err!("No electron density representation analysis results found.")
765        })
766    }
767
768    fn run(&mut self) -> Result<(), anyhow::Error> {
769        self.log_output_display();
770        self.analyse_fn_()?;
771        self.result()?.log_output_display();
772        Ok(())
773    }
774}