Skip to main content

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