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::chartab_group::CharacterProperties;
18use crate::chartab::SubspaceDecomposable;
19use crate::drivers::representation_analysis::angular_function::{
20    find_angular_function_representation, find_spinor_function_representation,
21    AngularFunctionRepAnalysisParams,
22};
23use crate::drivers::representation_analysis::{
24    fn_construct_magnetic_group, fn_construct_unitary_group, log_bao, log_cc_transversal,
25    CharacterTableDisplay, MagneticSymmetryAnalysisKind,
26};
27use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
28use crate::drivers::QSym2Driver;
29use crate::group::{GroupProperties, MagneticRepresentedGroup, UnitaryRepresentedGroup};
30use crate::io::format::{
31    log_subtitle, nice_bool, qsym2_output, write_subtitle, write_title, QSym2Output,
32};
33use crate::symmetry::symmetry_group::{
34    MagneticRepresentedSymmetryGroup, SymmetryGroupProperties, UnitaryRepresentedSymmetryGroup,
35};
36use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
37use crate::target::density::density_analysis::DensitySymmetryOrbit;
38use crate::target::density::Density;
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    fn builder() -> DensityRepAnalysisResultBuilder<'a, G, T> {
235        DensityRepAnalysisResultBuilder::default()
236    }
237}
238
239impl<'a, G, T> fmt::Display for DensityRepAnalysisResult<'a, G, T>
240where
241    G: SymmetryGroupProperties + Clone,
242    G::CharTab: SubspaceDecomposable<T>,
243    T: ComplexFloat + Lapack,
244    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
245{
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        write_subtitle(f, "Orbit-based symmetry analysis results")?;
248        writeln!(f)?;
249        writeln!(
250            f,
251            "> Group: {} ({})",
252            self.group
253                .finite_subgroup_name()
254                .map(|subgroup_name| format!("{} > {}", self.group.name(), subgroup_name))
255                .unwrap_or(self.group.name()),
256            self.group.group_type().to_string().to_lowercase()
257        )?;
258        writeln!(f)?;
259
260        let density_index_length = usize::try_from(self.densities.len().ilog10() + 1)
261            .unwrap_or(1)
262            .max(1);
263        let density_text_length = self
264            .densities
265            .iter()
266            .map(|(desc, _)| desc.chars().count())
267            .max()
268            .unwrap_or(7)
269            .max(7);
270        let symmetry_length = self
271            .density_symmetries
272            .iter()
273            .map(|den_sym_res| {
274                den_sym_res
275                    .as_ref()
276                    .map(|sym| sym.to_string())
277                    .unwrap_or_else(|err| err.clone())
278                    .chars()
279                    .count()
280            })
281            .max()
282            .unwrap_or(8)
283            .max(8);
284        let eig_above_length: usize = self
285            .density_symmetries_thresholds
286            .iter()
287            .map(|(eig_above_opt, _)| {
288                eig_above_opt
289                    .as_ref()
290                    .map(|eig_above| format!("{eig_above:+.3e}").chars().count())
291                    .unwrap_or(10)
292            })
293            .max()
294            .unwrap_or(10)
295            .max(10);
296        let eig_below_length: usize = self
297            .density_symmetries_thresholds
298            .iter()
299            .map(|(_, eig_below_opt)| {
300                eig_below_opt
301                    .as_ref()
302                    .map(|eig_below| format!("{eig_below:+.3e}").chars().count())
303                    .unwrap_or(10)
304            })
305            .max()
306            .unwrap_or(10)
307            .max(10);
308
309        let table_width = 10
310            + density_index_length
311            + density_text_length
312            + symmetry_length
313            + eig_above_length
314            + eig_below_length;
315
316        writeln!(f, "> Density results")?;
317        writeln!(f, "{}", "┈".repeat(table_width))?;
318        writeln!(
319            f,
320            " {:>density_index_length$}  {:<density_text_length$}  {:<symmetry_length$}  {:<eig_above_length$}  Eig. below",
321            "#",
322            "Density",
323            "Symmetry",
324            "Eig. above",
325        )?;
326        writeln!(f, "{}", "┈".repeat(table_width))?;
327        for (deni, (((den, _), den_sym), (eig_above_opt, eig_below_opt))) in self
328            .densities
329            .iter()
330            .zip(self.density_symmetries.iter())
331            .zip(self.density_symmetries_thresholds.iter())
332            .enumerate()
333        {
334            writeln!(
335                f,
336                " {:>density_index_length$}  {:<density_text_length$}  {:<symmetry_length$}  {:<eig_above_length$}  {}",
337                deni,
338                den,
339                den_sym.as_ref().map(|sym| sym.to_string()).unwrap_or_else(|err| err.clone()),
340                eig_above_opt
341                    .map(|eig_above| format!("{eig_above:>+.3e}"))
342                    .unwrap_or("--".to_string()),
343                eig_below_opt
344                    .map(|eig_below| format!("{eig_below:>+.3e}"))
345                    .unwrap_or("--".to_string()),
346            )?;
347        }
348        writeln!(f, "{}", "┈".repeat(table_width))?;
349        writeln!(f)?;
350
351        Ok(())
352    }
353}
354
355impl<'a, G, T> fmt::Debug for DensityRepAnalysisResult<'a, G, T>
356where
357    G: SymmetryGroupProperties + Clone,
358    G::CharTab: SubspaceDecomposable<T>,
359    T: ComplexFloat + Lapack,
360    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
361{
362    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363        writeln!(f, "{self}")
364    }
365}
366
367impl<'a, G, T> DensityRepAnalysisResult<'a, G, T>
368where
369    G: SymmetryGroupProperties + Clone,
370    G::CharTab: SubspaceDecomposable<T>,
371    T: ComplexFloat + Lapack,
372    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
373{
374    /// Returns the symmetries of the specified densities obtained from the analysis result.
375    pub fn density_symmetries(
376        &self,
377    ) -> &Vec<Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>> {
378        &self.density_symmetries
379    }
380}
381
382// ------
383// Driver
384// ------
385
386// ~~~~~~~~~~~~~~~~~
387// Struct definition
388// ~~~~~~~~~~~~~~~~~
389
390/// Driver structure for performing representation analysis on electron densities.
391#[derive(Clone, Builder)]
392#[builder(build_fn(validate = "Self::validate"))]
393pub struct DensityRepAnalysisDriver<'a, G, T>
394where
395    G: SymmetryGroupProperties + Clone,
396    G::CharTab: SubspaceDecomposable<T>,
397    T: ComplexFloat + Lapack,
398    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
399{
400    /// The control parameters for electron density representation analysis.
401    parameters: &'a DensityRepAnalysisParams<<T as ComplexFloat>::Real>,
402
403    /// The densities being analysed and their associated names or descriptions.
404    densities: Vec<(String, &'a Density<'a, T>)>,
405
406    /// The result from symmetry-group detection on the underlying molecular structure of the
407    /// electron densities.
408    symmetry_group: &'a SymmetryGroupDetectionResult,
409
410    /// The atomic-orbital four-centre spatial overlap matrix of the underlying basis set used to
411    /// describe the densities.
412    sao_spatial_4c: &'a Array4<T>,
413
414    /// The complex-symmetric atomic-orbital four-centre spatial overlap matrix of the underlying
415    /// basis set used to describe the densities. This is required if antiunitary symmetry
416    /// operations are involved. If none is provided, this will be assumed to be the same as
417    /// [`Self::sao_spatial_4c`].
418    #[builder(default = "None")]
419    sao_spatial_4c_h: Option<&'a Array4<T>>,
420
421    /// The control parameters for symmetry analysis of angular functions.
422    angular_function_parameters: &'a AngularFunctionRepAnalysisParams,
423
424    /// The result of the electron density representation analysis.
425    #[builder(setter(skip), default = "None")]
426    result: Option<DensityRepAnalysisResult<'a, G, T>>,
427}
428
429impl<'a, G, T> DensityRepAnalysisDriverBuilder<'a, G, T>
430where
431    G: SymmetryGroupProperties + Clone,
432    G::CharTab: SubspaceDecomposable<T>,
433    T: ComplexFloat + Lapack,
434    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
435{
436    fn validate(&self) -> Result<(), String> {
437        let params = self
438            .parameters
439            .ok_or("No electron density representation analysis parameters found.".to_string())?;
440
441        let sym_res = self
442            .symmetry_group
443            .ok_or("No symmetry group information found.".to_string())?;
444
445        let sao_spatial_4c = self
446            .sao_spatial_4c
447            .ok_or("No four-centre spatial SAO matrix found.".to_string())?;
448
449        if let Some(sao_spatial_4c_h) = self.sao_spatial_4c_h.flatten() {
450            if sao_spatial_4c_h.shape() != sao_spatial_4c.shape() {
451                return Err(
452                    "Mismatched shapes between `sao_spatial_4c` and `sao_spatial_4c_h`."
453                        .to_string(),
454                );
455            }
456        }
457
458        let dens = self
459            .densities
460            .as_ref()
461            .ok_or("No electron densities found.".to_string())?;
462
463        let sym = if params.use_magnetic_group.is_some() {
464            sym_res
465                .magnetic_symmetry
466                .as_ref()
467                .ok_or("Magnetic symmetry requested for representation analysis, but no magnetic symmetry found.")?
468        } else {
469            &sym_res.unitary_symmetry
470        };
471
472        if sym.is_infinite() && params.infinite_order_to_finite.is_none() {
473            Err(
474                format!(
475                    "Representation analysis cannot be performed using the entirety of the infinite group `{}`. \
476                    Consider setting the parameter `infinite_order_to_finite` to restrict to a finite subgroup instead.",
477                    sym.group_name.as_ref().expect("No symmetry group name found.")
478                )
479            )
480        } else {
481            let baos = dens
482                .iter()
483                .map(|(_, den)| den.bao())
484                .collect::<HashSet<_>>();
485            if baos.len() != 1 {
486                Err("Inconsistent basis angular order information between densities.".to_string())
487            } else {
488                let naos = sao_spatial_4c.shape().iter().collect::<HashSet<_>>();
489                if naos.len() != 1 {
490                    Err("The shape of the four-centre spatial SAO tensor is invalid: all four dimensions must have the same length.".to_string())
491                } else {
492                    let nao = **naos.iter().next().ok_or(
493                        "Unable to extract the dimensions of the four-centre spatial SAO tensor.",
494                    )?;
495                    if !dens.iter().all(|(_, den)| den.bao().n_funcs() == nao) {
496                        Err("The dimensions of the four-centre spatial SAO tensor do not match the number of spatial AO basis functions.".to_string())
497                    } else {
498                        Ok(())
499                    }
500                }
501            }
502        }
503    }
504}
505
506// ~~~~~~~~~~~~~~~~~~~~~~
507// Struct implementations
508// ~~~~~~~~~~~~~~~~~~~~~~
509
510// Generic for all symmetry groups G and density numeric type T
511// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
512
513impl<'a, G, T> DensityRepAnalysisDriver<'a, G, T>
514where
515    G: SymmetryGroupProperties + Clone,
516    G::CharTab: SubspaceDecomposable<T>,
517    T: ComplexFloat + Lapack,
518    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
519{
520    /// Returns a builder to construct a [`DensityRepAnalysisDriver`] structure.
521    pub fn builder() -> DensityRepAnalysisDriverBuilder<'a, G, T> {
522        DensityRepAnalysisDriverBuilder::default()
523    }
524}
525
526// Specific for unitary-represented symmetry groups, but generic for density numeric type T
527// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
528
529impl<'a, T> DensityRepAnalysisDriver<'a, UnitaryRepresentedSymmetryGroup, T>
530where
531    T: ComplexFloat + Lapack + Sync + Send,
532    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + Sync + Send,
533    for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
534{
535    fn_construct_unitary_group!(
536        /// Constructs the unitary-represented group (which itself can be unitary or magnetic) ready
537        /// for electron density representation analysis.
538        construct_unitary_group
539    );
540}
541
542// Specific for magnetic-represented symmetry groups, but generic for density numeric type T
543// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
544
545impl<'a, T> DensityRepAnalysisDriver<'a, MagneticRepresentedSymmetryGroup, T>
546where
547    T: ComplexFloat + Lapack + Sync + Send,
548    <T as ComplexFloat>::Real: From<f64> + Sync + Send + fmt::LowerExp + fmt::Debug,
549    for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
550{
551    fn_construct_magnetic_group!(
552        /// Constructs the magnetic-represented group (which itself can only be magnetic) ready for
553        /// electron density corepresentation analysis.
554        construct_magnetic_group
555    );
556}
557
558// Specific for unitary-represented and magnetic-represented symmetry groups and density numeric types f64 and C128
559// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
560
561#[duplicate_item(
562    duplicate!{
563        [ dtype_nested; [f64]; [Complex<f64>] ]
564        [
565            gtype_ [ UnitaryRepresentedSymmetryGroup ]
566            dtype_ [ dtype_nested ]
567            doc_sub_ [ "Performs representation analysis using a unitary-represented group and stores the result." ]
568            analyse_fn_ [ analyse_representation ]
569            construct_group_ [ self.construct_unitary_group()? ]
570        ]
571    }
572    duplicate!{
573        [ dtype_nested; [f64]; [Complex<f64>] ]
574        [
575            gtype_ [ MagneticRepresentedSymmetryGroup ]
576            dtype_ [ dtype_nested ]
577            doc_sub_ [ "Performs corepresentation analysis using a magnetic-represented group and stores the result." ]
578            analyse_fn_ [ analyse_corepresentation ]
579            construct_group_ [ self.construct_magnetic_group()? ]
580        ]
581    }
582)]
583impl<'a> DensityRepAnalysisDriver<'a, gtype_, dtype_> {
584    #[doc = doc_sub_]
585    fn analyse_fn_(&mut self) -> Result<(), anyhow::Error> {
586        let params = self.parameters;
587        let sao_spatial_4c = self.sao_spatial_4c;
588        let sao_spatial_4c_h = self.sao_spatial_4c_h;
589        let group = construct_group_;
590        log_cc_transversal(&group);
591        let _ = find_angular_function_representation(&group, self.angular_function_parameters);
592        if group.is_double_group() {
593            let _ = find_spinor_function_representation(&group, self.angular_function_parameters);
594        }
595        let bao = self
596            .densities
597            .iter()
598            .next()
599            .map(|(_, den)| den.bao())
600            .ok_or_else(|| {
601                format_err!("Basis angular order information could not be extracted.")
602            })?;
603        log_bao(bao);
604
605        let (den_symmetries, den_symmetries_thresholds): (Vec<_>, Vec<_>) =
606            self.densities.iter().map(|(_, den)| {
607                DensitySymmetryOrbit::builder()
608                    .group(&group)
609                    .origin(den)
610                    .integrality_threshold(params.integrality_threshold)
611                    .linear_independence_threshold(params.linear_independence_threshold)
612                    .symmetry_transformation_kind(params.symmetry_transformation_kind.clone())
613                    .eigenvalue_comparison_mode(params.eigenvalue_comparison_mode.clone())
614                    .build()
615                    .map_err(|err| format_err!(err))
616                    .and_then(|mut den_orbit| {
617                        den_orbit
618                            .calc_smat(Some(sao_spatial_4c), sao_spatial_4c_h, params.use_cayley_table)?
619                            .normalise_smat()?
620                            .calc_xmat(false)?;
621                        let density_symmetry_thresholds = den_orbit
622                            .smat_eigvals
623                            .as_ref()
624                            .map(|eigvals| {
625                                let mut eigvals_vec = eigvals.iter().collect::<Vec<_>>();
626                                match den_orbit.eigenvalue_comparison_mode() {
627                                    EigenvalueComparisonMode::Modulus => {
628                                        eigvals_vec.sort_by(|a, b| {
629                                            a.abs().partial_cmp(&b.abs()).expect("Unable to compare two eigenvalues based on their moduli.")
630                                        });
631                                    }
632                                    EigenvalueComparisonMode::Real => {
633                                        eigvals_vec.sort_by(|a, b| {
634                                            a.re().partial_cmp(&b.re()).expect("Unable to compare two eigenvalues based on their real parts.")
635                                        });
636                                    }
637                                }
638                                let eigval_above = match den_orbit.eigenvalue_comparison_mode() {
639                                    EigenvalueComparisonMode::Modulus => eigvals_vec
640                                        .iter()
641                                        .find(|val| {
642                                            val.abs() >= den_orbit.linear_independence_threshold
643                                        })
644                                        .copied()
645                                        .copied(),
646                                    EigenvalueComparisonMode::Real => eigvals_vec
647                                        .iter()
648                                        .find(|val| {
649                                            val.re() >= den_orbit.linear_independence_threshold
650                                        })
651                                        .copied()
652                                        .copied(),
653                                };
654                                eigvals_vec.reverse();
655                                let eigval_below = match den_orbit.eigenvalue_comparison_mode() {
656                                    EigenvalueComparisonMode::Modulus => eigvals_vec
657                                        .iter()
658                                        .find(|val| {
659                                            val.abs() < den_orbit.linear_independence_threshold
660                                        })
661                                        .copied()
662                                        .copied(),
663                                    EigenvalueComparisonMode::Real => eigvals_vec
664                                        .iter()
665                                        .find(|val| {
666                                            val.re() < den_orbit.linear_independence_threshold
667                                        })
668                                        .copied()
669                                        .copied(),
670                                };
671                                (eigval_above, eigval_below)
672                            })
673                            .unwrap_or((None, None));
674                        let den_sym = den_orbit.analyse_rep().map_err(|err| err.to_string());
675                        Ok((den_sym, density_symmetry_thresholds))
676                    })
677                    .unwrap_or_else(|err| (Err(err.to_string()), (None, None)))
678            }).unzip();
679
680        let result = DensityRepAnalysisResult::builder()
681            .parameters(params)
682            .densities(self.densities.clone())
683            .group(group)
684            .density_symmetries(den_symmetries)
685            .density_symmetries_thresholds(den_symmetries_thresholds)
686            .build()?;
687        self.result = Some(result);
688
689        Ok(())
690    }
691}
692
693// ~~~~~~~~~~~~~~~~~~~~~
694// Trait implementations
695// ~~~~~~~~~~~~~~~~~~~~~
696
697// Generic for all symmetry groups G and density numeric type T
698// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
699
700impl<'a, G, T> fmt::Display for DensityRepAnalysisDriver<'a, G, T>
701where
702    G: SymmetryGroupProperties + Clone,
703    G::CharTab: SubspaceDecomposable<T>,
704    T: ComplexFloat + Lapack,
705    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
706{
707    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
708        write_title(f, "Electron Density Symmetry Analysis")?;
709        writeln!(f)?;
710        writeln!(f, "{}", self.parameters)?;
711        Ok(())
712    }
713}
714
715impl<'a, G, T> fmt::Debug for DensityRepAnalysisDriver<'a, G, T>
716where
717    G: SymmetryGroupProperties + Clone,
718    G::CharTab: SubspaceDecomposable<T>,
719    T: ComplexFloat + Lapack,
720    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
721{
722    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
723        writeln!(f, "{self}")
724    }
725}
726
727// Specific for unitary/magnetic-represented groups and density numeric type f64/Complex<f64>
728// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
729
730#[duplicate_item(
731    duplicate!{
732        [ dtype_nested; [f64]; [Complex<f64>] ]
733        [
734            gtype_ [ UnitaryRepresentedSymmetryGroup ]
735            dtype_ [ dtype_nested ]
736            analyse_fn_ [ analyse_representation ]
737        ]
738    }
739    duplicate!{
740        [ dtype_nested; [f64]; [Complex<f64>] ]
741        [
742            gtype_ [ MagneticRepresentedSymmetryGroup ]
743            dtype_ [ dtype_nested ]
744            analyse_fn_ [ analyse_corepresentation ]
745        ]
746    }
747)]
748impl<'a> QSym2Driver for DensityRepAnalysisDriver<'a, gtype_, dtype_> {
749    type Params = DensityRepAnalysisParams<f64>;
750
751    type Outcome = DensityRepAnalysisResult<'a, gtype_, dtype_>;
752
753    fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
754        self.result.as_ref().ok_or_else(|| {
755            format_err!("No electron density representation analysis results found.")
756        })
757    }
758
759    fn run(&mut self) -> Result<(), anyhow::Error> {
760        self.log_output_display();
761        self.analyse_fn_()?;
762        self.result()?.log_output_display();
763        Ok(())
764    }
765}