Skip to main content

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