qsym2/drivers/representation_analysis/
slater_determinant.rs

1//! Driver for symmetry analysis of Slater determinants.
2
3use std::collections::{HashMap, HashSet};
4use std::fmt;
5use std::ops::Mul;
6
7use anyhow::{self, bail, ensure, format_err};
8use approx::abs_diff_eq;
9use derive_builder::Builder;
10use duplicate::duplicate_item;
11use indexmap::IndexMap;
12use itertools::Itertools;
13use ndarray::{Array2, Array4, s};
14use ndarray_linalg::types::Lapack;
15use num::ToPrimitive;
16use num_complex::{Complex, ComplexFloat};
17use num_traits::Float;
18use num_traits::real::Real;
19use rayon::prelude::*;
20use serde::{Deserialize, Serialize};
21
22use crate::analysis::{
23    EigenvalueComparisonMode, Orbit, Overlap, ProjectionDecomposition, RepAnalysis,
24    log_overlap_eigenvalues,
25};
26use crate::angmom::spinor_rotation_3d::{SpinConstraint, SpinOrbitCoupled, StructureConstraint};
27use crate::chartab::chartab_group::CharacterProperties;
28use crate::chartab::chartab_symbols::ReducibleLinearSpaceSymbol;
29use crate::chartab::{CharacterTable, SubspaceDecomposable};
30use crate::drivers::QSym2Driver;
31use crate::drivers::representation_analysis::angular_function::{
32    AngularFunctionRepAnalysisParams, find_angular_function_representation,
33    find_spinor_function_representation,
34};
35use crate::drivers::representation_analysis::{
36    CharacterTableDisplay, MagneticSymmetryAnalysisKind, fn_construct_magnetic_group,
37    fn_construct_unitary_group, log_bao, log_cc_transversal,
38};
39use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
40use crate::group::{GroupProperties, MagneticRepresentedGroup, UnitaryRepresentedGroup};
41use crate::io::format::{
42    QSym2Output, log_subtitle, nice_bool, qsym2_output, write_subtitle, write_title,
43};
44use crate::symmetry::symmetry_element::symmetry_operation::{
45    SpecialSymmetryTransformation, SymmetryOperation,
46};
47use crate::symmetry::symmetry_group::{
48    MagneticRepresentedSymmetryGroup, SymmetryGroupProperties, UnitaryRepresentedSymmetryGroup,
49};
50use crate::symmetry::symmetry_symbols::{
51    MirrorParity, MullikenIrcorepSymbol, SymmetryClassSymbol, deduce_mirror_parities,
52};
53use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
54use crate::target::density::density_analysis::DensitySymmetryOrbit;
55use crate::target::determinant::SlaterDeterminant;
56use crate::target::determinant::determinant_analysis::SlaterDeterminantSymmetryOrbit;
57use crate::target::orbital::orbital_analysis::generate_det_mo_orbits;
58
59#[cfg(test)]
60#[path = "slater_determinant_tests.rs"]
61mod slater_determinant_tests;
62
63// ==================
64// Struct definitions
65// ==================
66
67// ----------
68// Parameters
69// ----------
70
71const fn default_true() -> bool {
72    true
73}
74const fn default_symbolic() -> Option<CharacterTableDisplay> {
75    Some(CharacterTableDisplay::Symbolic)
76}
77
78/// Structure containing control parameters for Slater determinant representation analysis.
79#[derive(Clone, Builder, Debug, Serialize, Deserialize)]
80pub struct SlaterDeterminantRepAnalysisParams<T: From<f64>> {
81    /// Threshold for checking if subspace multiplicities are integral.
82    pub integrality_threshold: T,
83
84    /// Threshold for determining zero eigenvalues in the orbit overlap matrix.
85    pub linear_independence_threshold: T,
86
87    /// Boolean indicating if molecular orbital symmetries are to be analysed alongside the overall
88    /// determinantal symmetry.
89    #[builder(default = "true")]
90    #[serde(default = "default_true")]
91    pub analyse_mo_symmetries: bool,
92
93    /// Boolean indicating if molecular orbital mirror parities are to be analysed alongside
94    /// molecular orbital symmetries.
95    #[builder(default = "false")]
96    #[serde(default)]
97    pub analyse_mo_mirror_parities: bool,
98
99    /// Boolean indicating if symmetry projection should be performed for molecular orbitals.
100    #[builder(default = "false")]
101    #[serde(default)]
102    pub analyse_mo_symmetry_projections: bool,
103
104    /// Boolean indicating if density symmetries are to be analysed alongside wavefunction symmetries
105    /// for this determinant.
106    #[builder(default = "false")]
107    #[serde(default)]
108    pub analyse_density_symmetries: bool,
109
110    /// Option indicating if the magnetic group is to be used for symmetry analysis, and if so,
111    /// whether unitary representations or unitary-antiunitary corepresentations should be used.
112    #[builder(default = "None")]
113    #[serde(default)]
114    pub use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
115
116    /// Boolean indicating if the double group is to be used for symmetry analysis.
117    #[builder(default = "false")]
118    #[serde(default)]
119    pub use_double_group: bool,
120
121    /// Boolean indicating if the Cayley table of the group, if available, should be used to speed
122    /// up the computation of orbit overlap matrices.
123    #[builder(default = "true")]
124    #[serde(default = "default_true")]
125    pub use_cayley_table: bool,
126
127    /// The kind of symmetry transformation to be applied on the reference determinant to generate
128    /// the orbit for symmetry analysis.
129    #[builder(default = "SymmetryTransformationKind::Spatial")]
130    #[serde(default)]
131    pub symmetry_transformation_kind: SymmetryTransformationKind,
132
133    /// Option indicating if the character table of the group used for symmetry analysis is to be
134    /// printed out.
135    #[builder(default = "Some(CharacterTableDisplay::Symbolic)")]
136    #[serde(default = "default_symbolic")]
137    pub write_character_table: Option<CharacterTableDisplay>,
138
139    /// Boolean indicating if the eigenvalues of the orbit overlap matrix are to be printed out.
140    #[builder(default = "true")]
141    #[serde(default = "default_true")]
142    pub write_overlap_eigenvalues: bool,
143
144    /// The comparison mode for filtering out orbit overlap eigenvalues.
145    #[builder(default = "EigenvalueComparisonMode::Modulus")]
146    #[serde(default)]
147    pub eigenvalue_comparison_mode: EigenvalueComparisonMode,
148
149    /// The finite order to which any infinite-order symmetry element is reduced, so that a finite
150    /// subgroup of an infinite group can be used for the symmetry analysis.
151    #[builder(default = "None")]
152    #[serde(default)]
153    pub infinite_order_to_finite: Option<u32>,
154}
155
156impl<T> SlaterDeterminantRepAnalysisParams<T>
157where
158    T: Float + From<f64>,
159{
160    /// Returns a builder to construct a [`SlaterDeterminantRepAnalysisParams`] structure.
161    pub fn builder() -> SlaterDeterminantRepAnalysisParamsBuilder<T> {
162        SlaterDeterminantRepAnalysisParamsBuilder::default()
163    }
164}
165
166impl Default for SlaterDeterminantRepAnalysisParams<f64> {
167    fn default() -> Self {
168        Self::builder()
169            .integrality_threshold(1e-7)
170            .linear_independence_threshold(1e-7)
171            .build()
172            .expect("Unable to construct a default `SlaterDeterminantRepAnalysisParams<f64>`.")
173    }
174}
175
176impl<T> fmt::Display for SlaterDeterminantRepAnalysisParams<T>
177where
178    T: From<f64> + fmt::LowerExp + fmt::Debug,
179{
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        writeln!(
182            f,
183            "Integrality threshold: {:.3e}",
184            self.integrality_threshold
185        )?;
186        writeln!(
187            f,
188            "Linear independence threshold: {:.3e}",
189            self.linear_independence_threshold
190        )?;
191        writeln!(
192            f,
193            "Orbit eigenvalue comparison mode: {}",
194            self.eigenvalue_comparison_mode
195        )?;
196        writeln!(
197            f,
198            "Write overlap eigenvalues: {}",
199            nice_bool(self.write_overlap_eigenvalues)
200        )?;
201        writeln!(f)?;
202        writeln!(
203            f,
204            "Analyse molecular orbital symmetry: {}",
205            nice_bool(self.analyse_mo_symmetries)
206        )?;
207        writeln!(
208            f,
209            "Analyse molecular orbital symmetry projection: {}",
210            nice_bool(self.analyse_mo_symmetry_projections)
211        )?;
212        writeln!(
213            f,
214            "Analyse molecular orbital mirror parity: {}",
215            nice_bool(self.analyse_mo_mirror_parities)
216        )?;
217        writeln!(
218            f,
219            "Analyse density symmetry: {}",
220            nice_bool(self.analyse_density_symmetries)
221        )?;
222        writeln!(f)?;
223        writeln!(
224            f,
225            "Use magnetic group for analysis: {}",
226            match self.use_magnetic_group {
227                None => "no",
228                Some(MagneticSymmetryAnalysisKind::Representation) =>
229                    "yes, using unitary representations",
230                Some(MagneticSymmetryAnalysisKind::Corepresentation) =>
231                    "yes, using magnetic corepresentations",
232            }
233        )?;
234        writeln!(
235            f,
236            "Use double group for analysis: {}",
237            nice_bool(self.use_double_group)
238        )?;
239        writeln!(
240            f,
241            "Use Cayley table for orbit overlap matrices: {}",
242            nice_bool(self.use_cayley_table)
243        )?;
244        if let Some(finite_order) = self.infinite_order_to_finite {
245            writeln!(f, "Infinite order to finite: {finite_order}")?;
246        }
247        writeln!(
248            f,
249            "Symmetry transformation kind: {}",
250            self.symmetry_transformation_kind
251        )?;
252        writeln!(f)?;
253        writeln!(
254            f,
255            "Write character table: {}",
256            if let Some(chartab_display) = self.write_character_table.as_ref() {
257                format!("yes, {}", chartab_display.to_string().to_lowercase())
258            } else {
259                "no".to_string()
260            }
261        )?;
262
263        Ok(())
264    }
265}
266
267// ------
268// Result
269// ------
270
271/// Structure to contain Slater determinant representation analysis results.
272#[derive(Clone, Builder)]
273pub struct SlaterDeterminantRepAnalysisResult<'a, G, T, SC>
274where
275    G: SymmetryGroupProperties + Clone,
276    G::CharTab: SubspaceDecomposable<T>,
277    T: ComplexFloat + Lapack,
278    SC: StructureConstraint + fmt::Display,
279    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
280{
281    /// The control parameters used to obtain this set of Slater determinant representation
282    /// analysis results.
283    parameters: &'a SlaterDeterminantRepAnalysisParams<<T as ComplexFloat>::Real>,
284
285    /// The Slater determinant being analysed.
286    determinant: &'a SlaterDeterminant<'a, T, SC>,
287
288    /// The group used for the representation analysis.
289    group: G,
290
291    /// The deduced overall symmetry of the determinant.
292    determinant_symmetry: Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>,
293
294    /// The deduced symmetries of the molecular orbitals constituting the determinant, if required.
295    #[allow(clippy::type_complexity)]
296    mo_symmetries: Option<Vec<Vec<Option<<G::CharTab as SubspaceDecomposable<T>>::Decomposition>>>>,
297
298    /// The deduced symmetry projections of the molecular orbitals constituting the determinant, if required.
299    #[allow(clippy::type_complexity)]
300    mo_symmetry_projections: Option<
301        Vec<
302            Vec<
303                Option<
304                    Vec<(
305                        <<G as CharacterProperties>::CharTab as CharacterTable>::RowSymbol,
306                        Complex<f64>,
307                    )>,
308                >,
309            >,
310        >,
311    >,
312
313    /// The deduced mirror parities of the molecular orbitals constituting the determinant, if required.
314    #[allow(clippy::type_complexity)]
315    mo_mirror_parities:
316        Option<Vec<Vec<Option<IndexMap<SymmetryClassSymbol<SymmetryOperation>, MirrorParity>>>>>,
317
318    /// The overlap eigenvalues above and below the linear independence threshold for each
319    /// molecular orbital symmetry deduction.
320    #[allow(clippy::type_complexity)]
321    mo_symmetries_thresholds: Option<Vec<Vec<(Option<T>, Option<T>)>>>,
322
323    /// The deduced symmetries of the various densities constructible from the determinant, if
324    /// required. In each tuple, the first element gives a description of the density corresponding
325    /// to the symmetry result.
326    #[allow(clippy::type_complexity)]
327    determinant_density_symmetries: Option<
328        Vec<(
329            String,
330            Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>,
331        )>,
332    >,
333
334    /// The deduced symmetries of the total densities of the molecular orbitals constituting the
335    /// determinant, if required.
336    #[allow(clippy::type_complexity)]
337    mo_density_symmetries:
338        Option<Vec<Vec<Option<<G::CharTab as SubspaceDecomposable<T>>::Decomposition>>>>,
339}
340
341impl<'a, G, T, SC> SlaterDeterminantRepAnalysisResult<'a, G, T, SC>
342where
343    G: SymmetryGroupProperties + Clone,
344    G::CharTab: SubspaceDecomposable<T>,
345    T: ComplexFloat + Lapack,
346    SC: StructureConstraint + Clone + fmt::Display,
347    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
348{
349    /// Returns a builder to construct a new [`SlaterDeterminantRepAnalysisResultBuilder`]
350    /// structure.
351    fn builder() -> SlaterDeterminantRepAnalysisResultBuilder<'a, G, T, SC> {
352        SlaterDeterminantRepAnalysisResultBuilder::default()
353    }
354}
355
356impl<'a, G, T, SC> SlaterDeterminantRepAnalysisResult<'a, G, T, SC>
357where
358    G: SymmetryGroupProperties + Clone,
359    G::CharTab: SubspaceDecomposable<T>,
360    T: ComplexFloat + Lapack,
361    SC: StructureConstraint + fmt::Display,
362    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
363{
364    /// Returns the group used for the representation analysis.
365    pub fn group(&self) -> &G {
366        &self.group
367    }
368
369    /// Returns the determinant symmetry obtained from the analysis result.
370    pub fn determinant_symmetry(
371        &self,
372    ) -> &Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String> {
373        &self.determinant_symmetry
374    }
375
376    /// Returns the deduced symmetries of the molecular orbitals constituting the determinant, if required.
377    #[allow(clippy::type_complexity)]
378    pub fn mo_symmetries(
379        &self,
380    ) -> &Option<Vec<Vec<Option<<G::CharTab as SubspaceDecomposable<T>>::Decomposition>>>> {
381        &self.mo_symmetries
382    }
383
384    /// Returns the deduced symmetry projections of the molecular orbitals constituting the determinant, if required.
385    #[allow(clippy::type_complexity)]
386    pub fn mo_symmetry_projections(
387        &self,
388    ) -> &Option<
389        Vec<
390            Vec<
391                Option<
392                    Vec<(
393                        <<G as CharacterProperties>::CharTab as CharacterTable>::RowSymbol,
394                        Complex<f64>,
395                    )>,
396                >,
397            >,
398        >,
399    > {
400        &self.mo_symmetry_projections
401    }
402
403    /// Returns the deduced symmetries of the various densities constructible from the determinant,
404    /// if required. In each tuple, the first element gives a description of the density corresponding
405    /// to the symmetry result.
406    #[allow(clippy::type_complexity)]
407    pub fn determinant_density_symmetries(
408        &self,
409    ) -> &Option<
410        Vec<(
411            String,
412            Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>,
413        )>,
414    > {
415        &self.determinant_density_symmetries
416    }
417
418    /// Returns the deduced symmetries of the total densities of the molecular orbitals constituting
419    /// the determinant, if required.
420    #[allow(clippy::type_complexity)]
421    pub fn mo_density_symmetries(
422        &self,
423    ) -> &Option<Vec<Vec<Option<<G::CharTab as SubspaceDecomposable<T>>::Decomposition>>>> {
424        &self.mo_density_symmetries
425    }
426}
427
428impl<'a, G, T, SC> fmt::Display for SlaterDeterminantRepAnalysisResult<'a, G, T, SC>
429where
430    G: SymmetryGroupProperties + Clone,
431    G::CharTab: SubspaceDecomposable<T>,
432    T: ComplexFloat + Lapack,
433    SC: StructureConstraint + fmt::Display,
434    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
435{
436    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437        write_subtitle(f, "Orbit-based symmetry analysis results")?;
438        writeln!(f)?;
439        writeln!(
440            f,
441            "> Group: {} ({})",
442            self.group
443                .finite_subgroup_name()
444                .map(|subgroup_name| format!("{} > {}", self.group.name(), subgroup_name))
445                .unwrap_or(self.group.name()),
446            self.group.group_type().to_string().to_lowercase()
447        )?;
448        writeln!(f)?;
449        writeln!(f, "> Overall determinantal result")?;
450        writeln!(
451            f,
452            "  Energy  : {}",
453            self.determinant
454                .energy()
455                .map(|e| e.to_string())
456                .unwrap_or_else(|err| format!("-- ({err})"))
457        )?;
458        writeln!(
459            f,
460            "  Symmetry: {}",
461            self.determinant_symmetry
462                .as_ref()
463                .map(|s| s.to_string())
464                .unwrap_or_else(|err| format!("-- ({err})"))
465        )?;
466        writeln!(f)?;
467
468        if let Some(den_syms) = self.determinant_density_symmetries.as_ref() {
469            writeln!(f, "> Overall determinantal density result")?;
470            let den_type_width = den_syms
471                .iter()
472                .map(|(den_type, _)| den_type.chars().count())
473                .max()
474                .unwrap_or(7)
475                .max(7);
476            for (den_type, den_sym_res) in den_syms.iter() {
477                writeln!(
478                    f,
479                    "  {den_type:<den_type_width$}: {}",
480                    den_sym_res
481                        .as_ref()
482                        .map(|e| e.to_string())
483                        .unwrap_or_else(|err| format!("-- ({err})"))
484                )?;
485            }
486            writeln!(f)?;
487        }
488
489        if let Some(mo_symmetries) = self.mo_symmetries.as_ref() {
490            let mo_spin_index_length = 4;
491            let mo_index_length = mo_symmetries
492                .iter()
493                .map(|spin_mo_symmetries| spin_mo_symmetries.len())
494                .max()
495                .and_then(|max_mo_length| usize::try_from(max_mo_length.ilog10() + 2).ok())
496                .unwrap_or(4);
497            let mo_occ_length = 5;
498            let mo_symmetry_length = mo_symmetries
499                .iter()
500                .flat_map(|spin_mo_symmetries| {
501                    spin_mo_symmetries.iter().map(|mo_sym| {
502                        mo_sym
503                            .as_ref()
504                            .map(|sym| sym.to_string())
505                            .unwrap_or("--".to_string())
506                            .chars()
507                            .count()
508                    })
509                })
510                .max()
511                .unwrap_or(0)
512                .max(8);
513            let mo_energies_opt = self.determinant.mo_energies();
514            let mo_energy_length = mo_energies_opt
515                .map(|mo_energies| {
516                    mo_energies
517                        .iter()
518                        .flat_map(|spin_mo_energies| {
519                            spin_mo_energies.map(|v| format!("{v:+.7}").chars().count())
520                        })
521                        .max()
522                        .unwrap_or(0)
523                })
524                .unwrap_or(0)
525                .max(6);
526
527            let mo_eig_above_length: usize = self
528                .mo_symmetries_thresholds
529                .as_ref()
530                .map(|mo_symmetries_thresholds| {
531                    mo_symmetries_thresholds
532                        .iter()
533                        .flat_map(|spin_mo_symmetries_thresholds| {
534                            spin_mo_symmetries_thresholds.iter().map(|(above, _)| {
535                                above
536                                    .as_ref()
537                                    .map(|eig| format!("{eig:+.3e}"))
538                                    .unwrap_or("--".to_string())
539                                    .chars()
540                                    .count()
541                            })
542                        })
543                        .max()
544                        .unwrap_or(10)
545                        .max(10)
546                })
547                .unwrap_or(10);
548            let mo_eig_below_length: usize = self
549                .mo_symmetries_thresholds
550                .as_ref()
551                .map(|mo_symmetries_thresholds| {
552                    mo_symmetries_thresholds
553                        .iter()
554                        .flat_map(|spin_mo_symmetries_thresholds| {
555                            spin_mo_symmetries_thresholds.iter().map(|(_, below)| {
556                                below
557                                    .as_ref()
558                                    .map(|eig| format!("{eig:+.3e}"))
559                                    .unwrap_or("--".to_string())
560                                    .chars()
561                                    .count()
562                            })
563                        })
564                        .max()
565                        .unwrap_or(10)
566                        .max(10)
567                })
568                .unwrap_or(10);
569
570            let precision = Real::ceil(ComplexFloat::abs(ComplexFloat::log10(
571                self.parameters.linear_independence_threshold,
572            )))
573            .to_usize()
574            .expect("Unable to convert the linear independence threshold exponent to `usize`.");
575            let mo_sym_projections_str_opt =
576                self.mo_symmetry_projections
577                    .as_ref()
578                    .map(|mo_sym_projectionss| {
579                        mo_sym_projectionss
580                            .iter()
581                            .enumerate()
582                            .map(|(ispin, mo_sym_projections)| {
583                                mo_sym_projections
584                                    .iter()
585                                    .enumerate()
586                                    .map(|(imo, mo_sym_projection)| {
587                                        mo_sym_projection
588                                            .as_ref()
589                                            .map(|sym_proj| {
590                                                if let Some(mo_symmetriess) = self.mo_symmetries() {
591                                                    if let Some(sym) = &mo_symmetriess[ispin][imo] {
592                                                        let sym_proj_hashmap = sym_proj
593                                                            .iter()
594                                                            .cloned()
595                                                            .collect::<HashMap<_, _>>();
596                                                        sym.subspaces()
597                                                            .iter()
598                                                            .map(|(subspace, _)| {
599                                                                format!(
600                                                                    "{subspace}: {}",
601                                                                    sym_proj_hashmap
602                                                                        .get(subspace)
603                                                                        .map(|composition| {
604                                                                            if abs_diff_eq!(
605                                                                                composition.im,
606                                                                                0.0,
607                                                                                epsilon = self.parameters.linear_independence_threshold.to_f64().expect("Unable to convert the linear independence threshold to `f64`.")
608                                                                            ) {
609                                                                                format!(
610                                                                                    "{:+.precision$}",
611                                                                                    composition.re
612                                                                                )
613                                                                            } else {
614                                                                                format!(
615                                                                            "{composition:+.precision$}")
616                                                                            }
617                                                                        })
618                                                                        .unwrap_or(
619                                                                            "--".to_string()
620                                                                        )
621                                                                )
622                                                            })
623                                                            .join(", ")
624                                                    } else {
625                                                        "--".to_string()
626                                                    }
627                                                } else {
628                                                    "--".to_string()
629                                                }
630                                            })
631                                            .unwrap_or("--".to_string())
632                                    })
633                                    .collect::<Vec<_>>()
634                            })
635                            .collect::<Vec<_>>()
636                    });
637            let mo_sym_projections_length_opt =
638                mo_sym_projections_str_opt
639                    .as_ref()
640                    .map(|mo_sym_projectionss| {
641                        mo_sym_projectionss
642                            .iter()
643                            .flat_map(|mo_sym_projections| {
644                                mo_sym_projections
645                                    .iter()
646                                    .map(|mo_sym_projection| mo_sym_projection.chars().count())
647                            })
648                            .max()
649                            .unwrap_or(18)
650                            .max(18)
651                    });
652            let mo_sym_projections_length = mo_sym_projections_length_opt.unwrap_or(0);
653            let mo_sym_projections_gap = mo_sym_projections_length_opt.map(|_| 2).unwrap_or(0);
654            let mo_sym_projections_heading = mo_sym_projections_length_opt
655                .map(|_| "MO sym. projection")
656                .unwrap_or("");
657
658            let mirrors = self
659                .group
660                .filter_cc_symbols(|cc| cc.is_spatial_reflection());
661            let mo_mirror_parities_length_opt = self.mo_mirror_parities.as_ref().map(|_| {
662                let mirror_heading = mirrors.iter().map(|sigma| format!("p[{sigma}]")).join("  ");
663                let length = mirror_heading.chars().count();
664                (mirror_heading, length)
665            });
666            let mo_mirror_parities_gap = mo_mirror_parities_length_opt
667                .as_ref()
668                .map(|_| 2)
669                .unwrap_or(0);
670            let (mo_mirror_parities_heading, mo_mirror_parities_length) =
671                mo_mirror_parities_length_opt.unwrap_or((String::new(), 0));
672
673            let mo_den_symss_str_opt = self.mo_density_symmetries.as_ref().map(|mo_den_symss| {
674                mo_den_symss
675                    .iter()
676                    .map(|mo_den_syms| {
677                        mo_den_syms
678                            .iter()
679                            .map(|mo_den_sym| {
680                                mo_den_sym
681                                    .as_ref()
682                                    .map(|sym| sym.to_string())
683                                    .unwrap_or("--".to_string())
684                            })
685                            .collect::<Vec<_>>()
686                    })
687                    .collect::<Vec<_>>()
688            });
689            let mo_density_length_opt = mo_den_symss_str_opt.as_ref().map(|mo_den_symss| {
690                mo_den_symss
691                    .iter()
692                    .flat_map(|mo_den_syms| {
693                        mo_den_syms
694                            .iter()
695                            .map(|mo_den_sym| mo_den_sym.chars().count())
696                    })
697                    .max()
698                    .unwrap_or(12)
699                    .max(12)
700            });
701            let mo_density_length = mo_density_length_opt.unwrap_or(0);
702            let mo_density_gap = mo_density_length_opt.map(|_| 2).unwrap_or(0);
703            let mo_density_heading = mo_density_length_opt.map(|_| "Density sym.").unwrap_or("");
704
705            let table_width = 14
706                + mo_spin_index_length
707                + mo_index_length
708                + mo_occ_length
709                + mo_energy_length
710                + mo_symmetry_length
711                + mo_mirror_parities_gap
712                + mo_mirror_parities_length
713                + mo_eig_above_length
714                + mo_eig_below_length
715                + mo_sym_projections_gap
716                + mo_sym_projections_length
717                + mo_density_gap
718                + mo_density_length;
719
720            writeln!(f, "> Molecular orbital results")?;
721            writeln!(
722                f,
723                "  Structure constraint: {}",
724                self.determinant
725                    .structure_constraint()
726                    .to_string()
727                    .to_lowercase()
728            )?;
729            if self.mo_mirror_parities.as_ref().is_some() {
730                writeln!(f)?;
731                writeln!(
732                    f,
733                    "Column p[σ] gives the parity under the reflection class σ: {} => even, {} => odd, {} => neither.",
734                    MirrorParity::Even,
735                    MirrorParity::Odd,
736                    MirrorParity::Neither
737                )?;
738            }
739            writeln!(f, "{}", "┈".repeat(table_width))?;
740            if mo_density_length > 0 {
741                writeln!(
742                    f,
743                    " {:>mo_spin_index_length$}  {:>mo_index_length$}  {:<mo_occ_length$}  {:<mo_energy_length$}  {:<mo_symmetry_length$}{}{:mo_mirror_parities_length$}  {:<mo_eig_above_length$}  {:<mo_eig_below_length$}{}{:mo_sym_projections_length$}{}{}",
744                    "Spin",
745                    "MO",
746                    "Occ.",
747                    "Energy",
748                    "Symmetry",
749                    " ".repeat(mo_mirror_parities_gap),
750                    mo_mirror_parities_heading,
751                    "Eig. above",
752                    "Eig. below",
753                    " ".repeat(mo_sym_projections_gap),
754                    mo_sym_projections_heading,
755                    " ".repeat(mo_density_gap),
756                    mo_density_heading
757                )?;
758            } else if mo_sym_projections_length > 0 {
759                writeln!(
760                    f,
761                    " {:>mo_spin_index_length$}  {:>mo_index_length$}  {:<mo_occ_length$}  {:<mo_energy_length$}  {:<mo_symmetry_length$}{}{:mo_mirror_parities_length$}  {:<mo_eig_above_length$}  {:<mo_eig_below_length$}{}{}",
762                    "Spin",
763                    "MO",
764                    "Occ.",
765                    "Energy",
766                    "Symmetry",
767                    " ".repeat(mo_mirror_parities_gap),
768                    mo_mirror_parities_heading,
769                    "Eig. above",
770                    "Eig. below",
771                    " ".repeat(mo_sym_projections_gap),
772                    mo_sym_projections_heading,
773                )?;
774            } else {
775                writeln!(
776                    f,
777                    " {:>mo_spin_index_length$}  {:>mo_index_length$}  {:<mo_occ_length$}  {:<mo_energy_length$}  {:<mo_symmetry_length$}{}{:mo_mirror_parities_length$}  {:<mo_eig_above_length$}  Eig. below",
778                    "Spin",
779                    "MO",
780                    "Occ.",
781                    "Energy",
782                    "Symmetry",
783                    " ".repeat(mo_mirror_parities_gap),
784                    mo_mirror_parities_heading,
785                    "Eig. above",
786                )?;
787            };
788            writeln!(f, "{}", "┈".repeat(table_width))?;
789
790            let empty_string = String::new();
791            for (spini, spin_mo_symmetries) in mo_symmetries.iter().enumerate() {
792                writeln!(f, " Spin {spini}")?;
793                for (moi, mo_sym) in spin_mo_symmetries.iter().enumerate() {
794                    let occ_str = self
795                        .determinant
796                        .occupations()
797                        .get(spini)
798                        .and_then(|spin_occs| spin_occs.get(moi))
799                        .map(|occ| format!("{occ:>.3}"))
800                        .unwrap_or("--".to_string());
801                    let mo_energy_str = mo_energies_opt
802                        .and_then(|mo_energies| mo_energies.get(spini))
803                        .and_then(|spin_mo_energies| spin_mo_energies.get(moi))
804                        .map(|mo_energy| format!("{mo_energy:>+mo_energy_length$.7}"))
805                        .unwrap_or("--".to_string());
806                    let mo_sym_str = mo_sym
807                        .as_ref()
808                        .map(|sym| sym.to_string())
809                        .unwrap_or("--".to_string());
810
811                    let mo_mirror_parities_str = self
812                        .mo_mirror_parities
813                        .as_ref()
814                        .and_then(|mo_mirror_paritiess| {
815                            mo_mirror_paritiess
816                                .get(spini)
817                                .and_then(|spin_mo_mirror_parities| {
818                                    spin_mo_mirror_parities
819                                        .get(moi)
820                                        .map(|mo_mirror_parities_opt| {
821                                            mo_mirror_parities_opt
822                                                .as_ref()
823                                                .map(|mo_mirror_parities| {
824                                                    mirrors
825                                                        .iter()
826                                                        .map(|sigma| {
827                                                            let sigma_length =
828                                                                sigma.to_string().chars().count()
829                                                                    + 3;
830                                                            mo_mirror_parities
831                                                                .get(sigma)
832                                                                .map(|parity| {
833                                                                    format!(
834                                                                        "{:^sigma_length$}",
835                                                                        parity.to_string()
836                                                                    )
837                                                                })
838                                                                .unwrap_or_else(|| {
839                                                                    format!(
840                                                                        "{:^sigma_length$}",
841                                                                        "--"
842                                                                    )
843                                                                })
844                                                        })
845                                                        .join("  ")
846                                                })
847                                                .unwrap_or(String::new())
848                                        })
849                                })
850                        })
851                        .unwrap_or_default();
852
853                    let (eig_above_str, eig_below_str) = self
854                        .mo_symmetries_thresholds
855                        .as_ref()
856                        .map(|mo_symmetries_thresholds| {
857                            mo_symmetries_thresholds
858                                .get(spini)
859                                .and_then(|spin_mo_symmetries_thresholds| {
860                                    spin_mo_symmetries_thresholds.get(moi)
861                                })
862                                .map(|(eig_above_opt, eig_below_opt)| {
863                                    (
864                                        eig_above_opt
865                                            .map(|eig_above| format!("{eig_above:>+.3e}"))
866                                            .unwrap_or("--".to_string()),
867                                        eig_below_opt
868                                            .map(|eig_below| format!("{eig_below:>+.3e}"))
869                                            .unwrap_or("--".to_string()),
870                                    )
871                                })
872                                .unwrap_or(("--".to_string(), "--".to_string()))
873                        })
874                        .unwrap_or(("--".to_string(), "--".to_string()));
875
876                    let mo_symmetry_projections_str = mo_sym_projections_str_opt
877                        .as_ref()
878                        .and_then(|mo_symmetry_projectionss| {
879                            mo_symmetry_projectionss.get(spini).and_then(
880                                |spin_mo_symmetry_projections| {
881                                    spin_mo_symmetry_projections.get(moi)
882                                },
883                            )
884                        })
885                        .unwrap_or(&empty_string);
886
887                    let mo_density_symmetries_str = mo_den_symss_str_opt
888                        .as_ref()
889                        .and_then(|mo_density_symmetriess| {
890                            mo_density_symmetriess.get(spini).and_then(
891                                |spin_mo_density_symmetries| spin_mo_density_symmetries.get(moi),
892                            )
893                        })
894                        .unwrap_or(&empty_string);
895
896                    match (mo_density_length, mo_sym_projections_length) {
897                        (0, 0) => {
898                            writeln!(
899                                f,
900                                " {spini:>mo_spin_index_length$}  \
901                                {moi:>mo_index_length$}  \
902                                {occ_str:<mo_occ_length$}  \
903                                {mo_energy_str:<mo_energy_length$}  \
904                                {mo_sym_str:<mo_symmetry_length$}\
905                                {}{:mo_mirror_parities_length$}  \
906                                {eig_above_str:<mo_eig_above_length$}  \
907                                {eig_below_str}",
908                                " ".repeat(mo_mirror_parities_gap),
909                                mo_mirror_parities_str,
910                            )?;
911                        }
912                        (_, 0) => {
913                            writeln!(
914                                f,
915                                " {spini:>mo_spin_index_length$}  \
916                                {moi:>mo_index_length$}  \
917                                {occ_str:<mo_occ_length$}  \
918                                {mo_energy_str:<mo_energy_length$}  \
919                                {mo_sym_str:<mo_symmetry_length$}\
920                                {}{:mo_mirror_parities_length$}  \
921                                {eig_above_str:<mo_eig_above_length$}  \
922                                {eig_below_str:<mo_eig_below_length$}  \
923                                {mo_density_symmetries_str}",
924                                " ".repeat(mo_mirror_parities_gap),
925                                mo_mirror_parities_str,
926                            )?;
927                        }
928                        (0, _) => {
929                            writeln!(
930                                f,
931                                " {spini:>mo_spin_index_length$}  \
932                                {moi:>mo_index_length$}  \
933                                {occ_str:<mo_occ_length$}  \
934                                {mo_energy_str:<mo_energy_length$}  \
935                                {mo_sym_str:<mo_symmetry_length$}\
936                                {}{:mo_mirror_parities_length$}  \
937                                {eig_above_str:<mo_eig_above_length$}  \
938                                {eig_below_str:<mo_eig_below_length$}  \
939                                {mo_symmetry_projections_str}",
940                                " ".repeat(mo_mirror_parities_gap),
941                                mo_mirror_parities_str,
942                            )?;
943                        }
944                        (_, _) => {
945                            writeln!(
946                                f,
947                                " {spini:>mo_spin_index_length$}  \
948                                {moi:>mo_index_length$}  \
949                                {occ_str:<mo_occ_length$}  \
950                                {mo_energy_str:<mo_energy_length$}  \
951                                {mo_sym_str:<mo_symmetry_length$}\
952                                {}{:mo_mirror_parities_length$}  \
953                                {eig_above_str:<mo_eig_above_length$}  \
954                                {eig_below_str:<mo_eig_below_length$}  \
955                                {mo_symmetry_projections_str:<mo_sym_projections_length$}  \
956                                {mo_density_symmetries_str}",
957                                " ".repeat(mo_mirror_parities_gap),
958                                mo_mirror_parities_str,
959                            )?;
960                        }
961                    }
962                }
963            }
964
965            writeln!(f, "{}", "┈".repeat(table_width))?;
966            writeln!(f)?;
967        }
968
969        Ok(())
970    }
971}
972
973impl<'a, G, T, SC> fmt::Debug for SlaterDeterminantRepAnalysisResult<'a, G, T, SC>
974where
975    G: SymmetryGroupProperties + Clone,
976    G::CharTab: SubspaceDecomposable<T>,
977    T: ComplexFloat + Lapack,
978    SC: StructureConstraint + fmt::Display,
979    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
980{
981    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
982        writeln!(f, "{self}")
983    }
984}
985
986// ------
987// Driver
988// ------
989
990// ~~~~~~~~~~~~~~~~~
991// Struct definition
992// ~~~~~~~~~~~~~~~~~
993
994/// Driver structure for performing representation analysis on Slater determinants.
995#[derive(Clone, Builder)]
996#[builder(build_fn(validate = "Self::validate"))]
997pub struct SlaterDeterminantRepAnalysisDriver<'a, G, T, SC>
998where
999    G: SymmetryGroupProperties + Clone,
1000    G::CharTab: SubspaceDecomposable<T>,
1001    T: ComplexFloat + Lapack,
1002    SC: StructureConstraint + fmt::Display,
1003    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
1004{
1005    /// The control parameters for Slater determinant representation analysis.
1006    parameters: &'a SlaterDeterminantRepAnalysisParams<<T as ComplexFloat>::Real>,
1007
1008    /// The Slater determinant to be analysed.
1009    determinant: &'a SlaterDeterminant<'a, T, SC>,
1010
1011    /// The result from symmetry-group detection on the underlying molecular structure of the
1012    /// Slater determinant.
1013    symmetry_group: &'a SymmetryGroupDetectionResult,
1014
1015    /// The atomic-orbital overlap matrix of the underlying basis set used to describe the
1016    /// determinant. This is either for a single component corresponding to the basis functions
1017    /// specified by the basis angular order structure in [`Self::determinant`], or for *all*
1018    /// explicit components specified by the coefficients in [`Self::determinant`].
1019    sao: &'a Array2<T>,
1020
1021    /// The complex-symmetric atomic-orbital overlap matrix of the underlying basis set used to
1022    /// describe the determinant. This is either for a single component corresponding to the basis
1023    /// functions specified by the basis angular order structure in [`Self::determinant`], or for
1024    /// *all* explicit components specified by the coefficients in [`Self::determinant`]. This is
1025    /// required if antiunitary symmetry operations are involved. If none is provided, this will be
1026    /// assumed to be the same as [`Self::sao`].
1027    #[builder(default = "None")]
1028    sao_h: Option<&'a Array2<T>>,
1029
1030    /// The atomic-orbital four-centre spatial overlap matrix of the underlying basis set used to
1031    /// describe the determinant. This is only required for density symmetry analysis.
1032    #[builder(default = "None")]
1033    sao_spatial_4c: Option<&'a Array4<T>>,
1034
1035    /// The complex-symmetric atomic-orbital four-centre spatial overlap matrix of the underlying
1036    /// basis set used to describe the determinant. This is only required for density symmetry
1037    /// analysis. This is required if antiunitary symmetry operations are involved. If none is
1038    /// provided, this will be assumed to be the same as [`Self::sao_spatial_4c`], if any.
1039    #[builder(default = "None")]
1040    sao_spatial_4c_h: Option<&'a Array4<T>>,
1041
1042    /// The control parameters for symmetry analysis of angular functions.
1043    angular_function_parameters: &'a AngularFunctionRepAnalysisParams,
1044
1045    /// The result of the Slater determinant representation analysis.
1046    #[builder(setter(skip), default = "None")]
1047    result: Option<SlaterDeterminantRepAnalysisResult<'a, G, T, SC>>,
1048}
1049
1050impl<'a, G, T, SC> SlaterDeterminantRepAnalysisDriverBuilder<'a, G, T, SC>
1051where
1052    G: SymmetryGroupProperties + Clone,
1053    G::CharTab: SubspaceDecomposable<T>,
1054    T: ComplexFloat + Lapack,
1055    SC: StructureConstraint + fmt::Display,
1056    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
1057{
1058    fn validate(&self) -> Result<(), String> {
1059        let params = self
1060            .parameters
1061            .ok_or("No Slater determinant representation analysis parameters found.".to_string())?;
1062
1063        let sym_res = self
1064            .symmetry_group
1065            .ok_or("No symmetry group information found.".to_string())?;
1066
1067        let sao = self.sao.ok_or("No SAO matrix found.".to_string())?;
1068
1069        if let Some(sao_h) = self.sao_h.flatten()
1070            && sao_h.shape() != sao.shape()
1071        {
1072            return Err("Mismatched shapes between `sao` and `sao_h`.".to_string());
1073        }
1074
1075        match (
1076            self.sao_spatial_4c.flatten(),
1077            self.sao_spatial_4c_h.flatten(),
1078        ) {
1079            (Some(sao_spatial_4c), Some(sao_spatial_4c_h)) => {
1080                if sao_spatial_4c_h.shape() != sao_spatial_4c.shape() {
1081                    return Err(
1082                        "Mismatched shapes between `sao_spatial_4c` and `sao_spatial_4c_h`."
1083                            .to_string(),
1084                    );
1085                }
1086            }
1087            (None, Some(_)) => {
1088                return Err("`sao_spatial_4c_h` is provided without `sao_spatial_4c`.".to_string());
1089            }
1090            _ => {}
1091        }
1092
1093        let det = self
1094            .determinant
1095            .ok_or("No Slater determinant found.".to_string())?;
1096
1097        let sym = if params.use_magnetic_group.is_some() {
1098            sym_res
1099                .magnetic_symmetry
1100                .as_ref()
1101                .ok_or("Magnetic symmetry requested for representation analysis, but no magnetic symmetry found.")?
1102        } else {
1103            &sym_res.unitary_symmetry
1104        };
1105
1106        if sym.is_infinite() && params.infinite_order_to_finite.is_none() {
1107            Err(format!(
1108                "Representation analysis cannot be performed using the entirety of the infinite group `{}`. \
1109                    Consider setting the parameter `infinite_order_to_finite` to restrict to a finite subgroup instead.",
1110                sym.group_name
1111                    .as_ref()
1112                    .expect("No symmetry group name found.")
1113            ))
1114        } else {
1115            let nfuncs_set = det
1116                .baos()
1117                .iter()
1118                .map(|bao| bao.n_funcs())
1119                .collect::<HashSet<_>>();
1120            let uniform_component_check = nfuncs_set.len() == 1
1121                && {
1122                    let nfuncs = nfuncs_set.iter().next().ok_or_else(|| "Unable to extract the uniform number of basis functions per explicit component.".to_string())?;
1123                    sao.nrows() == *nfuncs && sao.ncols() == *nfuncs
1124                };
1125            let total_component_check = {
1126                let nfuncs_tot = det.baos().iter().map(|bao| bao.n_funcs()).sum::<usize>();
1127                sao.nrows() == nfuncs_tot && sao.ncols() == nfuncs_tot
1128            };
1129            if !uniform_component_check && !total_component_check {
1130                Err("The dimensions of the SAO matrix do not match either the uniform number of AO basis functions per explicit component or the total number of AO basis functions across all explicit components.".to_string())
1131            } else {
1132                Ok(())
1133            }
1134        }
1135    }
1136}
1137
1138// ~~~~~~~~~~~~~~~~~~~~~~
1139// Struct implementations
1140// ~~~~~~~~~~~~~~~~~~~~~~
1141
1142// Generic for all symmetry groups G, determinant numeric type T, and structure constraint SC
1143// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
1144
1145impl<'a, G, T, SC> SlaterDeterminantRepAnalysisDriver<'a, G, T, SC>
1146where
1147    G: SymmetryGroupProperties + Clone,
1148    G::CharTab: SubspaceDecomposable<T>,
1149    T: ComplexFloat + Lapack,
1150    SC: StructureConstraint + Clone + fmt::Display,
1151    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
1152{
1153    /// Returns a builder to construct a [`SlaterDeterminantRepAnalysisDriver`] structure.
1154    pub fn builder() -> SlaterDeterminantRepAnalysisDriverBuilder<'a, G, T, SC> {
1155        SlaterDeterminantRepAnalysisDriverBuilder::default()
1156    }
1157
1158    /// Constructs the appropriate atomic-orbital overlap matrix based on the structure constraint
1159    /// of the determinant and the specified overlap matrix.
1160    fn construct_sao(&self) -> Result<(Array2<T>, Option<Array2<T>>), anyhow::Error> {
1161        let nbas_set = self
1162            .determinant
1163            .baos()
1164            .iter()
1165            .map(|bao| bao.n_funcs())
1166            .collect::<HashSet<_>>();
1167        let uniform_component = nbas_set.len() == 1;
1168        let ncomps = self
1169            .determinant
1170            .structure_constraint()
1171            .n_explicit_comps_per_coefficient_matrix();
1172        let provided_dim = self.sao.nrows();
1173
1174        if uniform_component {
1175            let nbas = nbas_set.iter().next().ok_or_else(|| format_err!("Unable to extract the uniform number of basis functions per explicit component."))?;
1176            if provided_dim == *nbas {
1177                let sao = {
1178                    let mut sao_mut = Array2::zeros((ncomps * nbas, ncomps * nbas));
1179                    (0..ncomps).for_each(|icomp| {
1180                        let start = icomp * nbas;
1181                        let end = (icomp + 1) * nbas;
1182                        sao_mut
1183                            .slice_mut(s![start..end, start..end])
1184                            .assign(self.sao);
1185                    });
1186                    sao_mut
1187                };
1188
1189                let sao_h = self.sao_h.map(|sao_h| {
1190                    let mut sao_h_mut = Array2::zeros((ncomps * nbas, ncomps * nbas));
1191                    (0..ncomps).for_each(|icomp| {
1192                        let start = icomp * nbas;
1193                        let end = (icomp + 1) * nbas;
1194                        sao_h_mut
1195                            .slice_mut(s![start..end, start..end])
1196                            .assign(sao_h);
1197                    });
1198                    sao_h_mut
1199                });
1200
1201                Ok((sao, sao_h))
1202            } else {
1203                ensure!(provided_dim == nbas * ncomps);
1204                Ok((self.sao.clone(), self.sao_h.cloned()))
1205            }
1206        } else {
1207            let nbas_tot = self
1208                .determinant
1209                .baos()
1210                .iter()
1211                .map(|bao| bao.n_funcs())
1212                .sum::<usize>();
1213            ensure!(provided_dim == nbas_tot);
1214            Ok((self.sao.clone(), self.sao_h.cloned()))
1215        }
1216    }
1217}
1218
1219// Specific for unitary-represented symmetry groups, but generic for determinant numeric type T and structure constraint SC
1220// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
1221
1222impl<'a, T, SC> SlaterDeterminantRepAnalysisDriver<'a, UnitaryRepresentedSymmetryGroup, T, SC>
1223where
1224    T: ComplexFloat + Lapack + Sync + Send,
1225    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + Sync + Send,
1226    SC: StructureConstraint + fmt::Display,
1227    for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
1228{
1229    fn_construct_unitary_group!(
1230        /// Constructs the unitary-represented group (which itself can be unitary or magnetic) ready
1231        /// for Slater determinant representation analysis.
1232        construct_unitary_group
1233    );
1234}
1235
1236// Specific for magnetic-represented symmetry groups, but generic for determinant numeric type T and structure constraint SC
1237// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
1238
1239impl<'a, T, SC> SlaterDeterminantRepAnalysisDriver<'a, MagneticRepresentedSymmetryGroup, T, SC>
1240where
1241    T: ComplexFloat + Lapack + Sync + Send,
1242    <T as ComplexFloat>::Real: From<f64> + Sync + Send + fmt::LowerExp + fmt::Debug,
1243    SC: StructureConstraint + fmt::Display,
1244    for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
1245{
1246    fn_construct_magnetic_group!(
1247        /// Constructs the magnetic-represented group (which itself can only be magnetic) ready for
1248        /// Slater determinant corepresentation analysis.
1249        construct_magnetic_group
1250    );
1251}
1252
1253// Specific for unitary-represented and magnetic-represented symmetry groups and determinant numeric types f64 and C128
1254// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
1255
1256#[duplicate_item(
1257    duplicate!{
1258        [
1259            dtype_nested sctype_nested;
1260            [ f64 ] [ SpinConstraint ];
1261            [ Complex<f64> ] [ SpinConstraint ];
1262            [ Complex<f64> ] [ SpinOrbitCoupled ];
1263        ]
1264        [
1265            gtype_ [ UnitaryRepresentedSymmetryGroup ]
1266            dtype_ [ dtype_nested ]
1267            sctype_ [ sctype_nested ]
1268            doc_sub_ [ "Performs representation analysis using a unitary-represented group and stores the result." ]
1269            analyse_fn_ [ analyse_representation ]
1270            construct_group_ [ self.construct_unitary_group()? ]
1271            calc_projections_ [
1272                log_subtitle("Slater determinant projection decompositions");
1273                qsym2_output!("");
1274                qsym2_output!("  Projections are defined w.r.t. the following inner product:");
1275                qsym2_output!("    {}", det_orbit.origin().overlap_definition());
1276                qsym2_output!("");
1277                det_orbit
1278                    .projections_to_string(
1279                        &det_orbit.calc_projection_compositions()?,
1280                        params.integrality_threshold,
1281                    )
1282                    .log_output_display();
1283                qsym2_output!("");
1284            ]
1285            calc_mo_projections_ [
1286                let mo_projectionss = mo_orbitss.iter().map(|mo_orbits| {
1287                    mo_orbits
1288                        .iter()
1289                        .map(|mo_orbit| mo_orbit.calc_projection_compositions().ok())
1290                        .collect::<Vec<_>>()
1291                }).collect::<Vec<_>>();
1292                Some(mo_projectionss)
1293            ]
1294        ]
1295    }
1296    duplicate!{
1297        [
1298            dtype_nested sctype_nested;
1299            [ f64 ] [ SpinConstraint ];
1300            [ Complex<f64> ] [ SpinConstraint ];
1301            [ Complex<f64> ] [ SpinOrbitCoupled ];
1302        ]
1303        [
1304            gtype_ [ MagneticRepresentedSymmetryGroup ]
1305            dtype_ [ dtype_nested ]
1306            sctype_ [ sctype_nested ]
1307            doc_sub_ [ "Performs corepresentation analysis using a magnetic-represented group and stores the result." ]
1308            analyse_fn_ [ analyse_corepresentation ]
1309            construct_group_ [ self.construct_magnetic_group()? ]
1310            calc_projections_ [ ]
1311            calc_mo_projections_ [
1312                None::<Vec<Vec<Option<Vec<(MullikenIrcorepSymbol, Complex<f64>)>>>>>
1313            ]
1314        ]
1315    }
1316)]
1317impl<'a> SlaterDeterminantRepAnalysisDriver<'a, gtype_, dtype_, sctype_> {
1318    #[doc = doc_sub_]
1319    fn analyse_fn_(&mut self) -> Result<(), anyhow::Error> {
1320        let params = self.parameters;
1321        let (sao, sao_h) = self.construct_sao()?;
1322        let group = construct_group_;
1323        log_cc_transversal(&group);
1324        let _ = find_angular_function_representation(&group, self.angular_function_parameters);
1325        if group.is_double_group() {
1326            let _ = find_spinor_function_representation(&group, self.angular_function_parameters);
1327        }
1328        for (bao_i, bao) in self.determinant.baos().iter().enumerate() {
1329            log_bao(bao, Some(bao_i));
1330        }
1331
1332        // Determinant and orbital symmetries
1333        let (
1334            det_symmetry,
1335            mo_symmetries,
1336            mo_symmetry_projections,
1337            mo_mirror_parities,
1338            mo_symmetries_thresholds,
1339        ) = if params.analyse_mo_symmetries {
1340            let mos = self.determinant.to_orbitals();
1341            let (mut det_orbit, mut mo_orbitss) = generate_det_mo_orbits(
1342                self.determinant,
1343                &mos,
1344                &group,
1345                &sao,
1346                sao_h.as_ref(),
1347                params.integrality_threshold,
1348                params.linear_independence_threshold,
1349                params.symmetry_transformation_kind.clone(),
1350                params.eigenvalue_comparison_mode.clone(),
1351                params.use_cayley_table,
1352            )?;
1353            det_orbit.calc_xmat(false)?;
1354            if params.write_overlap_eigenvalues
1355                && let Some(smat_eigvals) = det_orbit.smat_eigvals.as_ref()
1356            {
1357                log_overlap_eigenvalues(
1358                    "Determinant orbit overlap eigenvalues",
1359                    smat_eigvals,
1360                    params.linear_independence_threshold,
1361                    &params.eigenvalue_comparison_mode,
1362                );
1363                qsym2_output!("");
1364            }
1365
1366            let det_symmetry = det_orbit.analyse_rep().map_err(|err| err.to_string());
1367
1368            {
1369                calc_projections_
1370            }
1371
1372            let mo_symmetries = mo_orbitss
1373                .iter_mut()
1374                .map(|mo_orbits| {
1375                    mo_orbits
1376                        .par_iter_mut()
1377                        .map(|mo_orbit| {
1378                            mo_orbit.calc_xmat(false).ok()?;
1379                            mo_orbit.analyse_rep().ok()
1380                        })
1381                        .collect::<Vec<_>>()
1382                })
1383                .collect::<Vec<_>>();
1384
1385            let mo_symmetry_projections_opt = if params.analyse_mo_symmetry_projections {
1386                calc_mo_projections_
1387            } else {
1388                None
1389            };
1390
1391            let mo_mirror_parities_opt = if params.analyse_mo_mirror_parities {
1392                Some(
1393                    mo_symmetries
1394                        .iter()
1395                        .map(|spin_mo_symmetries| {
1396                            spin_mo_symmetries
1397                                .iter()
1398                                .map(|mo_sym_opt| {
1399                                    mo_sym_opt.as_ref().map(|mo_sym| {
1400                                        deduce_mirror_parities(det_orbit.group(), mo_sym)
1401                                    })
1402                                })
1403                                .collect::<Vec<_>>()
1404                        })
1405                        .collect::<Vec<_>>(),
1406                )
1407            } else {
1408                None
1409            };
1410
1411            let mo_symmetries_thresholds = mo_orbitss
1412                .iter_mut()
1413                .map(|mo_orbits| {
1414                    mo_orbits
1415                        .par_iter_mut()
1416                        .map(|mo_orbit| {
1417                            mo_orbit
1418                                .smat_eigvals
1419                                .as_ref()
1420                                .map(|eigvals| {
1421                                    let mut eigvals_vec = eigvals.iter().collect::<Vec<_>>();
1422                                    match mo_orbit.eigenvalue_comparison_mode {
1423                                        EigenvalueComparisonMode::Modulus => {
1424                                            eigvals_vec.sort_by(|a, b| {
1425                                                a.abs().partial_cmp(&b.abs()).expect("Unable to compare two eigenvalues based on their moduli.")
1426                                            });
1427                                        }
1428                                        EigenvalueComparisonMode::Real => {
1429                                            eigvals_vec.sort_by(|a, b| {
1430                                                a.re().partial_cmp(&b.re()).expect("Unable to compare two eigenvalues based on their real parts.")
1431                                            });
1432                                        }
1433                                    }
1434                                    let eigval_above = match mo_orbit.eigenvalue_comparison_mode {
1435                                        EigenvalueComparisonMode::Modulus => eigvals_vec
1436                                            .iter()
1437                                            .find(|val| {
1438                                                val.abs() >= mo_orbit.linear_independence_threshold
1439                                            })
1440                                            .copied()
1441                                            .copied(),
1442                                        EigenvalueComparisonMode::Real => eigvals_vec
1443                                            .iter()
1444                                            .find(|val| {
1445                                                val.re() >= mo_orbit.linear_independence_threshold
1446                                            })
1447                                            .copied()
1448                                            .copied(),
1449                                    };
1450                                    eigvals_vec.reverse();
1451                                    let eigval_below = match mo_orbit.eigenvalue_comparison_mode {
1452                                        EigenvalueComparisonMode::Modulus => eigvals_vec
1453                                            .iter()
1454                                            .find(|val| {
1455                                                val.abs() < mo_orbit.linear_independence_threshold
1456                                            })
1457                                            .copied()
1458                                            .copied(),
1459                                        EigenvalueComparisonMode::Real => eigvals_vec
1460                                            .iter()
1461                                            .find(|val| {
1462                                                val.re() < mo_orbit.linear_independence_threshold
1463                                            })
1464                                            .copied()
1465                                            .copied(),
1466                                    };
1467                                    (eigval_above, eigval_below)
1468                                })
1469                                .unwrap_or((None, None))
1470                        })
1471                        .collect::<Vec<_>>()
1472                })
1473                .collect::<Vec<_>>();
1474            (
1475                det_symmetry,
1476                Some(mo_symmetries),
1477                mo_symmetry_projections_opt,
1478                mo_mirror_parities_opt,
1479                Some(mo_symmetries_thresholds),
1480            ) // det_symmetry, mo_symmetries, mo_symmetry_projections, mo_mirror_parities, mo_symmetries_thresholds
1481        } else {
1482            let mut det_orbit = SlaterDeterminantSymmetryOrbit::builder()
1483                .group(&group)
1484                .origin(self.determinant)
1485                .integrality_threshold(params.integrality_threshold)
1486                .linear_independence_threshold(params.linear_independence_threshold)
1487                .symmetry_transformation_kind(params.symmetry_transformation_kind.clone())
1488                .eigenvalue_comparison_mode(params.eigenvalue_comparison_mode.clone())
1489                .build()?;
1490            let det_symmetry = det_orbit
1491                .calc_smat(Some(&sao), sao_h.as_ref(), params.use_cayley_table)
1492                .and_then(|det_orb| det_orb.normalise_smat())
1493                .map_err(|err| err.to_string())
1494                .and_then(|det_orb| {
1495                    det_orb.calc_xmat(false).map_err(|err| err.to_string())?;
1496                    if params.write_overlap_eigenvalues
1497                        && let Some(smat_eigvals) = det_orb.smat_eigvals.as_ref()
1498                    {
1499                        log_overlap_eigenvalues(
1500                            "Determinant orbit overlap eigenvalues",
1501                            smat_eigvals,
1502                            params.linear_independence_threshold,
1503                            &params.eigenvalue_comparison_mode,
1504                        );
1505                        qsym2_output!("");
1506                    }
1507                    det_orb.analyse_rep().map_err(|err| err.to_string())
1508                });
1509
1510            {
1511                calc_projections_
1512            }
1513
1514            (det_symmetry, None, None, None, None) // det_symmetry, mo_symmetries, mo_symmetry_projections, mo_mirror_parities, mo_symmetries_thresholds
1515        };
1516
1517        // Density and orbital density symmetries
1518        let (den_symmetries, mo_den_symmetries) = if params.analyse_density_symmetries {
1519            let den_syms = self.determinant.to_densities().map(|densities| {
1520                let mut spin_den_syms = densities
1521                    .iter()
1522                    .enumerate()
1523                    .map(|(ispin, den)| {
1524                        let den_sym_res = || {
1525                            let mut den_orbit = DensitySymmetryOrbit::builder()
1526                                .group(&group)
1527                                .origin(den)
1528                                .integrality_threshold(params.integrality_threshold)
1529                                .linear_independence_threshold(params.linear_independence_threshold)
1530                                .symmetry_transformation_kind(
1531                                    params.symmetry_transformation_kind.clone(),
1532                                )
1533                                .eigenvalue_comparison_mode(
1534                                    params.eigenvalue_comparison_mode.clone(),
1535                                )
1536                                .build()?;
1537                            den_orbit
1538                                .calc_smat(
1539                                    self.sao_spatial_4c,
1540                                    self.sao_spatial_4c_h,
1541                                    params.use_cayley_table,
1542                                )?
1543                                .normalise_smat()?
1544                                .calc_xmat(false)?;
1545                            den_orbit.analyse_rep().map_err(|err| format_err!(err))
1546                        };
1547                        (
1548                            format!("Spin-{ispin} density"),
1549                            den_sym_res().map_err(|err| err.to_string()),
1550                        )
1551                    })
1552                    .collect::<Vec<_>>();
1553                let mut extra_den_syms = densities
1554                    .calc_extra_densities()
1555                    .expect("Unable to calculate extra densities.")
1556                    .iter()
1557                    .map(|(label, den)| {
1558                        let den_sym_res = || {
1559                            let mut den_orbit = DensitySymmetryOrbit::builder()
1560                                .group(&group)
1561                                .origin(den)
1562                                .integrality_threshold(params.integrality_threshold)
1563                                .linear_independence_threshold(params.linear_independence_threshold)
1564                                .symmetry_transformation_kind(
1565                                    params.symmetry_transformation_kind.clone(),
1566                                )
1567                                .eigenvalue_comparison_mode(
1568                                    params.eigenvalue_comparison_mode.clone(),
1569                                )
1570                                .build()?;
1571                            den_orbit
1572                                .calc_smat(
1573                                    self.sao_spatial_4c,
1574                                    self.sao_spatial_4c_h,
1575                                    params.use_cayley_table,
1576                                )?
1577                                .calc_xmat(false)?;
1578                            den_orbit.analyse_rep().map_err(|err| format_err!(err))
1579                        };
1580                        (label.clone(), den_sym_res().map_err(|err| err.to_string()))
1581                    })
1582                    .collect_vec();
1583                spin_den_syms.append(&mut extra_den_syms);
1584                spin_den_syms
1585            });
1586
1587            let mo_den_syms = if params.analyse_mo_symmetries {
1588                let mo_den_symmetries = self
1589                    .determinant
1590                    .to_orbitals()
1591                    .iter()
1592                    .map(|mos| {
1593                        mos.par_iter()
1594                            .map(|mo| {
1595                                let mo_den = mo.to_total_density().ok()?;
1596                                let mut mo_den_orbit = DensitySymmetryOrbit::builder()
1597                                    .group(&group)
1598                                    .origin(&mo_den)
1599                                    .integrality_threshold(params.integrality_threshold)
1600                                    .linear_independence_threshold(
1601                                        params.linear_independence_threshold,
1602                                    )
1603                                    .symmetry_transformation_kind(
1604                                        params.symmetry_transformation_kind.clone(),
1605                                    )
1606                                    .eigenvalue_comparison_mode(
1607                                        params.eigenvalue_comparison_mode.clone(),
1608                                    )
1609                                    .build()
1610                                    .ok()?;
1611                                log::debug!("Computing overlap matrix for an MO density orbit...");
1612                                mo_den_orbit
1613                                    .calc_smat(
1614                                        self.sao_spatial_4c,
1615                                        self.sao_spatial_4c_h,
1616                                        params.use_cayley_table,
1617                                    )
1618                                    .ok()?
1619                                    .normalise_smat()
1620                                    .ok()?
1621                                    .calc_xmat(false)
1622                                    .ok()?;
1623                                log::debug!(
1624                                    "Computing overlap matrix for an MO density orbit... Done."
1625                                );
1626                                mo_den_orbit.analyse_rep().ok()
1627                            })
1628                            .collect::<Vec<_>>()
1629                    })
1630                    .collect::<Vec<_>>();
1631                Some(mo_den_symmetries)
1632            } else {
1633                None
1634            };
1635
1636            (den_syms.ok(), mo_den_syms)
1637        } else {
1638            (None, None)
1639        };
1640
1641        let result = SlaterDeterminantRepAnalysisResult::builder()
1642            .parameters(params)
1643            .determinant(self.determinant)
1644            .group(group)
1645            .determinant_symmetry(det_symmetry)
1646            .determinant_density_symmetries(den_symmetries)
1647            .mo_symmetries(mo_symmetries)
1648            .mo_symmetry_projections(mo_symmetry_projections)
1649            .mo_mirror_parities(mo_mirror_parities)
1650            .mo_symmetries_thresholds(mo_symmetries_thresholds)
1651            .mo_density_symmetries(mo_den_symmetries)
1652            .build()?;
1653        self.result = Some(result);
1654
1655        Ok(())
1656    }
1657}
1658
1659// ~~~~~~~~~~~~~~~~~~~~~
1660// Trait implementations
1661// ~~~~~~~~~~~~~~~~~~~~~
1662
1663// Generic for all symmetry groups G and determinant numeric type T
1664// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
1665
1666impl<'a, G, T, SC> fmt::Display for SlaterDeterminantRepAnalysisDriver<'a, G, T, SC>
1667where
1668    G: SymmetryGroupProperties + Clone,
1669    G::CharTab: SubspaceDecomposable<T>,
1670    T: ComplexFloat + Lapack,
1671    SC: StructureConstraint + fmt::Display,
1672    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
1673{
1674    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1675        write_title(f, "Slater Determinant Symmetry Analysis")?;
1676        writeln!(f)?;
1677        writeln!(f, "{}", self.parameters)?;
1678        Ok(())
1679    }
1680}
1681
1682impl<'a, G, T, SC> fmt::Debug for SlaterDeterminantRepAnalysisDriver<'a, G, T, SC>
1683where
1684    G: SymmetryGroupProperties + Clone,
1685    G::CharTab: SubspaceDecomposable<T>,
1686    T: ComplexFloat + Lapack,
1687    SC: StructureConstraint + fmt::Display,
1688    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
1689{
1690    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1691        writeln!(f, "{self}")
1692    }
1693}
1694
1695// Specific for unitary/magnetic-represented groups and determinant numeric type f64/Complex<f64>
1696// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
1697
1698#[duplicate_item(
1699    duplicate!{
1700        [
1701            dtype_nested sctype_nested;
1702            [ f64 ] [ SpinConstraint ];
1703            [ Complex<f64> ] [ SpinConstraint ];
1704            [ Complex<f64> ] [ SpinOrbitCoupled ];
1705        ]
1706        [
1707            gtype_ [ UnitaryRepresentedSymmetryGroup ]
1708            dtype_ [ dtype_nested ]
1709            sctype_ [ sctype_nested ]
1710            analyse_fn_ [ analyse_representation ]
1711        ]
1712    }
1713    duplicate!{
1714        [
1715            dtype_nested sctype_nested;
1716            [ f64 ] [ SpinConstraint ];
1717            [ Complex<f64> ] [ SpinConstraint ];
1718            [ Complex<f64> ] [ SpinOrbitCoupled ];
1719        ]
1720        [
1721            gtype_ [ MagneticRepresentedSymmetryGroup ]
1722            dtype_ [ dtype_nested ]
1723            sctype_ [ sctype_nested ]
1724            analyse_fn_ [ analyse_corepresentation ]
1725        ]
1726    }
1727)]
1728impl<'a> QSym2Driver for SlaterDeterminantRepAnalysisDriver<'a, gtype_, dtype_, sctype_> {
1729    type Params = SlaterDeterminantRepAnalysisParams<f64>;
1730
1731    type Outcome = SlaterDeterminantRepAnalysisResult<'a, gtype_, dtype_, sctype_>;
1732
1733    fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
1734        self.result.as_ref().ok_or_else(|| {
1735            format_err!("No Slater determinant representation analysis results found.")
1736        })
1737    }
1738
1739    fn run(&mut self) -> Result<(), anyhow::Error> {
1740        self.log_output_display();
1741        self.analyse_fn_()?;
1742        self.result()?.log_output_display();
1743        Ok(())
1744    }
1745}