qsym2/drivers/representation_analysis/
multideterminant.rs

1//! Driver for symmetry analysis of multi-determinantal wavefunctions.
2
3use std::collections::HashSet;
4use std::fmt;
5use std::hash::Hash;
6use std::ops::Mul;
7
8use anyhow::{self, bail, format_err};
9use derive_builder::Builder;
10use duplicate::duplicate_item;
11use ndarray::{s, Array2};
12use ndarray_linalg::types::Lapack;
13use num_complex::{Complex, ComplexFloat};
14use num_traits::Float;
15use serde::{Deserialize, Serialize};
16
17use crate::analysis::{
18    log_overlap_eigenvalues, EigenvalueComparisonMode, Overlap, ProjectionDecomposition,
19    RepAnalysis,
20};
21use crate::angmom::spinor_rotation_3d::{SpinConstraint, SpinOrbitCoupled, StructureConstraint};
22use crate::chartab::chartab_group::CharacterProperties;
23use crate::chartab::SubspaceDecomposable;
24use crate::drivers::representation_analysis::angular_function::{
25    find_angular_function_representation, find_spinor_function_representation,
26    AngularFunctionRepAnalysisParams,
27};
28use crate::drivers::representation_analysis::{
29    fn_construct_magnetic_group, fn_construct_unitary_group, log_bao, log_cc_transversal,
30    CharacterTableDisplay, MagneticSymmetryAnalysisKind,
31};
32use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
33use crate::drivers::QSym2Driver;
34use crate::group::{GroupProperties, MagneticRepresentedGroup, UnitaryRepresentedGroup};
35use crate::io::format::{
36    log_micsec_begin, log_micsec_end, log_subtitle, nice_bool, qsym2_output, write_subtitle,
37    write_title, QSym2Output,
38};
39use crate::symmetry::symmetry_group::{
40    MagneticRepresentedSymmetryGroup, SymmetryGroupProperties, UnitaryRepresentedSymmetryGroup,
41};
42use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
43use crate::target::determinant::SlaterDeterminant;
44use crate::target::noci::basis::{Basis, EagerBasis, OrbitBasis};
45use crate::target::noci::multideterminant::multideterminant_analysis::MultiDeterminantSymmetryOrbit;
46use crate::target::noci::multideterminant::MultiDeterminant;
47
48#[cfg(test)]
49#[path = "multideterminant_tests.rs"]
50mod multideterminant_tests;
51
52// ==================
53// Struct definitions
54// ==================
55
56// ----------
57// Parameters
58// ----------
59
60const fn default_true() -> bool {
61    true
62}
63const fn default_symbolic() -> Option<CharacterTableDisplay> {
64    Some(CharacterTableDisplay::Symbolic)
65}
66
67/// Structure containing control parameters for multi-determinantal wavefunction representation
68/// analysis.
69#[derive(Clone, Builder, Debug, Serialize, Deserialize)]
70pub struct MultiDeterminantRepAnalysisParams<T: From<f64>> {
71    /// Threshold for checking if subspace multiplicities are integral.
72    pub integrality_threshold: T,
73
74    /// Threshold for determining zero eigenvalues in the orbit overlap matrix.
75    pub linear_independence_threshold: T,
76
77    /// Option indicating if the magnetic group is to be used for symmetry analysis, and if so,
78    /// whether unitary representations or unitary-antiunitary corepresentations should be used.
79    #[builder(default = "None")]
80    #[serde(default)]
81    pub use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
82
83    /// Boolean indicating if the double group is to be used for symmetry analysis.
84    #[builder(default = "false")]
85    #[serde(default)]
86    pub use_double_group: bool,
87
88    /// Boolean indicating if the Cayley table of the group, if available, should be used to speed
89    /// up the computation of orbit overlap matrices.
90    #[builder(default = "true")]
91    #[serde(default = "default_true")]
92    pub use_cayley_table: bool,
93
94    /// The kind of symmetry transformation to be applied on the reference multi-determinantal
95    /// wavefunction to generate the orbit for symmetry analysis.
96    #[builder(default = "SymmetryTransformationKind::Spatial")]
97    #[serde(default)]
98    pub symmetry_transformation_kind: SymmetryTransformationKind,
99
100    /// Option indicating if the character table of the group used for symmetry analysis is to be
101    /// printed out.
102    #[builder(default = "Some(CharacterTableDisplay::Symbolic)")]
103    #[serde(default = "default_symbolic")]
104    pub write_character_table: Option<CharacterTableDisplay>,
105
106    /// Boolean indicating if the eigenvalues of the orbit overlap matrix are to be printed out.
107    #[builder(default = "true")]
108    #[serde(default = "default_true")]
109    pub write_overlap_eigenvalues: bool,
110
111    /// The comparison mode for filtering out orbit overlap eigenvalues.
112    #[builder(default = "EigenvalueComparisonMode::Modulus")]
113    #[serde(default)]
114    pub eigenvalue_comparison_mode: EigenvalueComparisonMode,
115
116    /// The finite order to which any infinite-order symmetry element is reduced, so that a finite
117    /// subgroup of an infinite group can be used for the symmetry analysis.
118    #[builder(default = "None")]
119    #[serde(default)]
120    pub infinite_order_to_finite: Option<u32>,
121}
122
123impl<T> MultiDeterminantRepAnalysisParams<T>
124where
125    T: Float + From<f64>,
126{
127    /// Returns a builder to construct a [`MultiDeterminantRepAnalysisParams`] structure.
128    pub fn builder() -> MultiDeterminantRepAnalysisParamsBuilder<T> {
129        MultiDeterminantRepAnalysisParamsBuilder::default()
130    }
131}
132
133impl Default for MultiDeterminantRepAnalysisParams<f64> {
134    fn default() -> Self {
135        Self::builder()
136            .integrality_threshold(1e-7)
137            .linear_independence_threshold(1e-7)
138            .build()
139            .expect("Unable to construct a default `MultiDeterminantRepAnalysisParams<f64>`.")
140    }
141}
142
143impl<T> fmt::Display for MultiDeterminantRepAnalysisParams<T>
144where
145    T: From<f64> + fmt::LowerExp + fmt::Debug,
146{
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        writeln!(
149            f,
150            "Integrality threshold: {:.3e}",
151            self.integrality_threshold
152        )?;
153        writeln!(
154            f,
155            "Linear independence threshold: {:.3e}",
156            self.linear_independence_threshold
157        )?;
158        writeln!(
159            f,
160            "Orbit eigenvalue comparison mode: {}",
161            self.eigenvalue_comparison_mode
162        )?;
163        writeln!(
164            f,
165            "Write overlap eigenvalues: {}",
166            nice_bool(self.write_overlap_eigenvalues)
167        )?;
168        writeln!(f)?;
169        writeln!(
170            f,
171            "Use magnetic group for analysis: {}",
172            match self.use_magnetic_group {
173                None => "no",
174                Some(MagneticSymmetryAnalysisKind::Representation) =>
175                    "yes, using unitary representations",
176                Some(MagneticSymmetryAnalysisKind::Corepresentation) =>
177                    "yes, using magnetic corepresentations",
178            }
179        )?;
180        writeln!(
181            f,
182            "Use double group for analysis: {}",
183            nice_bool(self.use_double_group)
184        )?;
185        writeln!(
186            f,
187            "Use Cayley table for orbit overlap matrices: {}",
188            nice_bool(self.use_cayley_table)
189        )?;
190        if let Some(finite_order) = self.infinite_order_to_finite {
191            writeln!(f, "Infinite order to finite: {finite_order}")?;
192        }
193        writeln!(
194            f,
195            "Symmetry transformation kind: {}",
196            self.symmetry_transformation_kind
197        )?;
198        writeln!(f)?;
199        writeln!(
200            f,
201            "Write character table: {}",
202            if let Some(chartab_display) = self.write_character_table.as_ref() {
203                format!("yes, {}", chartab_display.to_string().to_lowercase())
204            } else {
205                "no".to_string()
206            }
207        )?;
208
209        Ok(())
210    }
211}
212
213// ------
214// Result
215// ------
216
217/// Structure to contain multi-determinantal representation analysis results.
218#[derive(Clone, Builder)]
219pub struct MultiDeterminantRepAnalysisResult<'a, G, T, B, SC>
220where
221    G: SymmetryGroupProperties + Clone,
222    G::CharTab: SubspaceDecomposable<T>,
223    T: ComplexFloat + Lapack,
224    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
225    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
226    SC: StructureConstraint + Hash + Eq + fmt::Display,
227{
228    /// The control parameters used to obtain this set of multi-determinantal wavefunction
229    /// representation analysis results.
230    parameters: &'a MultiDeterminantRepAnalysisParams<<T as ComplexFloat>::Real>,
231
232    /// The multi-determinantal wavefunctions being analysed.
233    multidets: Vec<&'a MultiDeterminant<'a, T, B, SC>>,
234
235    /// The group used for the representation analysis.
236    group: G,
237
238    /// The deduced symmetries of the multi-determinantal wavefunctions.
239    multidet_symmetries:
240        Vec<Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>>,
241
242    /// The overlap eigenvalues above and below the linear independence threshold for each
243    /// multi-determinantal wavefunction symmetry deduction.
244    multidet_symmetries_thresholds: Vec<(Option<T>, Option<T>)>,
245}
246
247impl<'a, G, T, B, SC> MultiDeterminantRepAnalysisResult<'a, G, T, B, SC>
248where
249    G: SymmetryGroupProperties + Clone,
250    G::CharTab: SubspaceDecomposable<T>,
251    T: ComplexFloat + Lapack,
252    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
253    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
254    SC: StructureConstraint + Clone + Hash + Eq + fmt::Display,
255{
256    /// Returns a builder to construct a new [`MultiDeterminantRepAnalysisResultBuilder`]
257    /// structure.
258    fn builder() -> MultiDeterminantRepAnalysisResultBuilder<'a, G, T, B, SC> {
259        MultiDeterminantRepAnalysisResultBuilder::default()
260    }
261
262    /// Returns the multi-determinantal wavefunction symmetries obtained from the analysis result.
263    pub fn multidet_symmetries(
264        &self,
265    ) -> &Vec<Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>> {
266        &self.multidet_symmetries
267    }
268}
269
270impl<'a, G, T, B, SC> fmt::Display for MultiDeterminantRepAnalysisResult<'a, G, T, B, SC>
271where
272    G: SymmetryGroupProperties + Clone,
273    G::CharTab: SubspaceDecomposable<T>,
274    T: ComplexFloat + Lapack,
275    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
276    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
277    SC: StructureConstraint + Clone + Hash + Eq + fmt::Display,
278{
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        write_subtitle(f, "Orbit-based symmetry analysis results")?;
281        writeln!(f)?;
282        writeln!(
283            f,
284            "> Group: {} ({})",
285            self.group
286                .finite_subgroup_name()
287                .map(|subgroup_name| format!("{} > {}", self.group.name(), subgroup_name))
288                .unwrap_or(self.group.name()),
289            self.group.group_type().to_string().to_lowercase()
290        )?;
291        writeln!(f)?;
292
293        let multidet_index_length = usize::try_from(self.multidets.len().ilog10() + 1).unwrap_or(4);
294        let multidet_symmetry_length = self
295            .multidet_symmetries
296            .iter()
297            .map(|multidet_sym| {
298                multidet_sym
299                    .as_ref()
300                    .map(|sym| sym.to_string())
301                    .unwrap_or_else(|err| err.clone())
302                    .chars()
303                    .count()
304            })
305            .max()
306            .unwrap_or(0)
307            .max(8);
308        let multidet_energy_length = self
309            .multidets
310            .iter()
311            .map(|multidet| {
312                multidet
313                    .energy()
314                    .map(|v| format!("{v:+.7}").chars().count())
315                    .unwrap_or(2)
316            })
317            .max()
318            .unwrap_or(6)
319            .max(6);
320
321        let multidet_eig_above_length: usize = self
322            .multidet_symmetries_thresholds
323            .iter()
324            .map(|(above, _)| {
325                above
326                    .as_ref()
327                    .map(|eig| format!("{eig:+.3e}"))
328                    .unwrap_or("--".to_string())
329                    .chars()
330                    .count()
331            })
332            .max()
333            .unwrap_or(10)
334            .max(10);
335
336        let multidet_eig_below_length: usize = self
337            .multidet_symmetries_thresholds
338            .iter()
339            .map(|(_, below)| {
340                below
341                    .as_ref()
342                    .map(|eig| format!("{eig:+.3e}"))
343                    .unwrap_or("--".to_string())
344                    .chars()
345                    .count()
346            })
347            .max()
348            .unwrap_or(10)
349            .max(10);
350
351        let table_width = 10
352            + multidet_index_length
353            + multidet_energy_length
354            + multidet_symmetry_length
355            + multidet_eig_above_length
356            + multidet_eig_below_length;
357
358        writeln!(f, "> Multi-determinantal results")?;
359        writeln!(
360            f,
361            "  Structure constraint: {}",
362            self.multidets
363                .get(0)
364                .map(|multidet_0| multidet_0.structure_constraint().to_string().to_lowercase())
365                .unwrap_or("--".to_string())
366        )?;
367        writeln!(f, "{}", "┈".repeat(table_width))?;
368        writeln!(
369            f,
370            " {:>multidet_index_length$}  {:<multidet_energy_length$}  {:<multidet_symmetry_length$}  {:<multidet_eig_above_length$}  Eig. below",
371            "#",
372            "Energy",
373            "Symmetry",
374            "Eig. above",
375        )?;
376        writeln!(f, "{}", "┈".repeat(table_width))?;
377
378        for (multidet_i, multidet) in self.multidets.iter().enumerate() {
379            let multidet_energy_str = multidet
380                .energy()
381                .map(|multidet_energy| format!("{multidet_energy:>+multidet_energy_length$.7}"))
382                .unwrap_or("--".to_string());
383            let multidet_sym_str = self
384                .multidet_symmetries
385                .get(multidet_i)
386                .ok_or_else(|| format!("Unable to retrieve the symmetry of multideterminantal wavefunction index `{multidet_i}`."))
387                .and_then(|sym_res| sym_res.as_ref().map(|sym| sym.to_string()).map_err(|err| err.to_string()))
388                .unwrap_or_else(|err| err);
389
390            let (eig_above_str, eig_below_str) = self
391                .multidet_symmetries_thresholds
392                .get(multidet_i)
393                .map(|(eig_above_opt, eig_below_opt)| {
394                    (
395                        eig_above_opt
396                            .map(|eig_above| format!("{eig_above:>+.3e}"))
397                            .unwrap_or("--".to_string()),
398                        eig_below_opt
399                            .map(|eig_below| format!("{eig_below:>+.3e}"))
400                            .unwrap_or("--".to_string()),
401                    )
402                })
403                .unwrap_or(("--".to_string(), "--".to_string()));
404            writeln!(
405                f,
406                " {multidet_i:>multidet_index_length$}  \
407                {multidet_energy_str:<multidet_energy_length$}  \
408                {multidet_sym_str:<multidet_symmetry_length$}  \
409                {eig_above_str:<multidet_eig_above_length$}  \
410                {eig_below_str}"
411            )?;
412        }
413
414        writeln!(f, "{}", "┈".repeat(table_width))?;
415        writeln!(f)?;
416
417        Ok(())
418    }
419}
420
421impl<'a, G, T, B, SC> fmt::Debug for MultiDeterminantRepAnalysisResult<'a, G, T, B, SC>
422where
423    G: SymmetryGroupProperties + Clone,
424    G::CharTab: SubspaceDecomposable<T>,
425    T: ComplexFloat + Lapack,
426    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
427    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
428    SC: StructureConstraint + Clone + Hash + Eq + fmt::Display,
429{
430    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431        writeln!(f, "{self}")
432    }
433}
434
435// ------
436// Driver
437// ------
438
439// ~~~~~~~~~~~~~~~~~
440// Struct definition
441// ~~~~~~~~~~~~~~~~~
442
443/// Driver structure for performing representation analysis on multi-determinantal wavefunctions.
444#[derive(Clone, Builder)]
445#[builder(build_fn(validate = "Self::validate"))]
446pub struct MultiDeterminantRepAnalysisDriver<'a, G, T, B, SC>
447where
448    G: SymmetryGroupProperties + Clone,
449    G::CharTab: SubspaceDecomposable<T>,
450    T: ComplexFloat + Lapack,
451    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
452    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
453    SC: StructureConstraint + Hash + Eq + fmt::Display,
454{
455    /// The control parameters for multi-determinantal wavefunction representation analysis.
456    parameters: &'a MultiDeterminantRepAnalysisParams<<T as ComplexFloat>::Real>,
457
458    /// The multi-determinantal wavefunctions to be analysed.
459    multidets: Vec<&'a MultiDeterminant<'a, T, B, SC>>,
460
461    /// The result from symmetry-group detection on the underlying molecular structure of the
462    /// multi-determinantal wavefunctions.
463    symmetry_group: &'a SymmetryGroupDetectionResult,
464
465    /// The atomic-orbital overlap matrix of the underlying basis set used to describe the
466    /// wavefunctions. This is either for a single component corresponding to the basis functions
467    /// specified by the basis angular order structure in the determinants in [`Self::multidets`],
468    /// or for *all* explicit components specified by the coefficients in the determinants.
469    sao: &'a Array2<T>,
470
471    /// The complex-symmetric atomic-orbital overlap matrix of the underlying basis set used to
472    /// describe the wavefunctions. This is either for a single component corresponding to the basis
473    /// functions specified by the basis angular order structure in the determinants, or for *all*
474    /// explicit components specified by the coefficients in the determinants.This is required if
475    /// antiunitary symmetry operations are involved. If none is provided, this will be assumed to
476    /// be the same as [`Self::sao`].
477    #[builder(default = "None")]
478    sao_h: Option<&'a Array2<T>>,
479
480    /// The control parameters for symmetry analysis of angular functions.
481    angular_function_parameters: &'a AngularFunctionRepAnalysisParams,
482
483    /// The result of the multi-determinantal wavefunction representation analysis.
484    #[builder(setter(skip), default = "None")]
485    result: Option<MultiDeterminantRepAnalysisResult<'a, G, T, B, SC>>,
486}
487
488impl<'a, G, T, B, SC> MultiDeterminantRepAnalysisDriverBuilder<'a, G, T, B, SC>
489where
490    G: SymmetryGroupProperties + Clone,
491    G::CharTab: SubspaceDecomposable<T>,
492    T: ComplexFloat + Lapack,
493    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
494    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
495    SC: StructureConstraint + Hash + Eq + fmt::Display,
496{
497    fn validate(&self) -> Result<(), String> {
498        let params = self.parameters.ok_or(
499            "No multi-determinantal wavefunction representation analysis parameters found."
500                .to_string(),
501        )?;
502
503        let sym_res = self
504            .symmetry_group
505            .ok_or("No symmetry group information found.".to_string())?;
506
507        let sao = self.sao.ok_or("No SAO matrix found.".to_string())?;
508
509        if let Some(sao_h) = self.sao_h.flatten() {
510            if sao_h.shape() != sao.shape() {
511                return Err("Mismatched shapes between `sao` and `sao_h`.".to_string());
512            }
513        }
514
515        let multidets = self
516            .multidets
517            .as_ref()
518            .ok_or("No multi-determinantal wavefunctions found.".to_string())?;
519        let mut n_spatial_set = multidets
520            .iter()
521            .flat_map(|multidet| {
522                multidet.basis().iter().map(|det_res| {
523                    det_res.map(|det| {
524                        (
525                            det.bao().n_funcs(),
526                            det.structure_constraint()
527                                .n_explicit_comps_per_coefficient_matrix(),
528                        )
529                    })
530                })
531            })
532            .collect::<Result<HashSet<(usize, usize)>, _>>()
533            .map_err(|err| err.to_string())?;
534        let (n_spatial, n_comps) = if n_spatial_set.len() == 1 {
535            n_spatial_set
536                .drain()
537                .next()
538                .ok_or("Unable to retrieve the number of spatial AO basis functions and the number of explicit components.".to_string())
539        } else {
540            Err("Inconsistent numbers of spatial AO basis functions and/or explicit components across multi-determinantal wavefunctions.".to_string())
541        }?;
542
543        let sym = if params.use_magnetic_group.is_some() {
544            sym_res
545                .magnetic_symmetry
546                .as_ref()
547                .ok_or("Magnetic symmetry requested for representation analysis, but no magnetic symmetry found.")?
548        } else {
549            &sym_res.unitary_symmetry
550        };
551
552        if sym.is_infinite() && params.infinite_order_to_finite.is_none() {
553            Err(
554                format!(
555                    "Representation analysis cannot be performed using the entirety of the infinite group `{}`. \
556                    Consider setting the parameter `infinite_order_to_finite` to restrict to a finite subgroup instead.",
557                    sym.group_name.as_ref().expect("No symmetry group name found.")
558                )
559            )
560        } else if (n_spatial != sao.nrows() || n_spatial != sao.ncols())
561            && (n_spatial * n_comps != sao.nrows() || n_spatial * n_comps != sao.ncols())
562        {
563            Err("The dimensions of the SAO matrix do not match either the number of spatial AO basis functions or the number of spatial AO basis functions multiplied by the number of explicit components per coefficient matrix.".to_string())
564        } else {
565            Ok(())
566        }
567    }
568}
569
570// ~~~~~~~~~~~~~~~~~~~~~~
571// Struct implementations
572// ~~~~~~~~~~~~~~~~~~~~~~
573
574// Generic for all symmetry groups G and wavefunction numeric type T
575// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
576
577impl<'a, G, T, B, SC> MultiDeterminantRepAnalysisDriver<'a, G, T, B, SC>
578where
579    G: SymmetryGroupProperties + Clone,
580    G::CharTab: SubspaceDecomposable<T>,
581    T: ComplexFloat + Lapack,
582    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
583    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
584    SC: StructureConstraint + Clone + Hash + Eq + fmt::Display,
585{
586    /// Returns a builder to construct a [`MultiDeterminantRepAnalysisDriver`] structure.
587    pub fn builder() -> MultiDeterminantRepAnalysisDriverBuilder<'a, G, T, B, SC> {
588        MultiDeterminantRepAnalysisDriverBuilder::default()
589    }
590
591    /// Constructs the appropriate atomic-orbital overlap matrix based on the structure constraint of
592    /// the multi-determinantal wavefunctions and the provided overlap matrix.
593    fn construct_sao(&self) -> Result<(Array2<T>, Option<Array2<T>>), anyhow::Error> {
594        let mut structure_constraint_set = self
595            .multidets
596            .iter()
597            .map(|multidet| multidet.structure_constraint())
598            .collect::<HashSet<_>>();
599        let structure_constraint = if structure_constraint_set.len() == 1 {
600            structure_constraint_set.drain().next().ok_or(format_err!(
601                "Unable to retrieve the structure constraint of the multi-determinantal wavefunctions."
602            ))
603        } else {
604            Err(format_err!(
605                "Inconsistent structure constraints across multi-determinantal wavefunctions."
606            ))
607        }?;
608
609        let mut nbas_set = self
610            .multidets
611            .iter()
612            .map(|multidet| {
613                multidet
614                    .basis()
615                    .first()
616                    .expect("Unable to obtain the first determinant in the basis.")
617                    .bao()
618                    .n_funcs()
619            })
620            .collect::<HashSet<_>>();
621        let nbas = if nbas_set.len() == 1 {
622            nbas_set.drain().next().ok_or(format_err!(
623                "Unable to retrieve the number of basis functions describing the multi-determinantal wavefunctions."
624            ))
625        } else {
626            Err(format_err!(
627                "Inconsistent numbers of basis functions across multi-determinantal wavefunctions."
628            ))
629        }?;
630        let ncomps = structure_constraint.n_explicit_comps_per_coefficient_matrix();
631        let provided_dim = self.sao.nrows();
632
633        if provided_dim == nbas {
634            let sao = {
635                let mut sao_mut = Array2::zeros((ncomps * nbas, ncomps * nbas));
636                (0..ncomps).for_each(|icomp| {
637                    let start = icomp * nbas;
638                    let end = (icomp + 1) * nbas;
639                    sao_mut
640                        .slice_mut(s![start..end, start..end])
641                        .assign(self.sao);
642                });
643                sao_mut
644            };
645
646            let sao_h = self.sao_h.map(|sao_h| {
647                let mut sao_h_mut = Array2::zeros((ncomps * nbas, ncomps * nbas));
648                (0..ncomps).for_each(|icomp| {
649                    let start = icomp * nbas;
650                    let end = (icomp + 1) * nbas;
651                    sao_h_mut
652                        .slice_mut(s![start..end, start..end])
653                        .assign(sao_h);
654                });
655                sao_h_mut
656            });
657
658            Ok((sao, sao_h))
659        } else {
660            assert_eq!(provided_dim, nbas * ncomps);
661            Ok((self.sao.clone(), self.sao_h.cloned()))
662        }
663    }
664}
665
666// Specific for unitary-represented symmetry groups, but generic for wavefunction numeric type T
667// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
668
669impl<'a, T, B, SC> MultiDeterminantRepAnalysisDriver<'a, UnitaryRepresentedSymmetryGroup, T, B, SC>
670where
671    T: ComplexFloat + Lapack + Sync + Send,
672    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + Sync + Send,
673    for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
674    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
675    SC: StructureConstraint + Hash + Eq + fmt::Display,
676{
677    fn_construct_unitary_group!(
678        /// Constructs the unitary-represented group (which itself can be unitary or magnetic) ready
679        /// for Slater determinant representation analysis.
680        construct_unitary_group
681    );
682}
683
684// Specific for magnetic-represented symmetry groups, but generic for wavefunction numeric type T
685// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
686
687impl<'a, T, B, SC> MultiDeterminantRepAnalysisDriver<'a, MagneticRepresentedSymmetryGroup, T, B, SC>
688where
689    T: ComplexFloat + Lapack + Sync + Send,
690    <T as ComplexFloat>::Real: From<f64> + Sync + Send + fmt::LowerExp + fmt::Debug,
691    for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
692    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
693    SC: StructureConstraint + Hash + Eq + fmt::Display,
694{
695    fn_construct_magnetic_group!(
696        /// Constructs the magnetic-represented group (which itself can only be magnetic) ready for
697        /// Slater determinant corepresentation analysis.
698        construct_magnetic_group
699    );
700}
701
702// Specific for unitary-represented and magnetic-represented symmetry groups and wavefunction numeric types f64 and C128
703// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
704
705#[duplicate_item(
706    duplicate!{
707        [
708            dtype_nested sctype_nested;
709            [ f64 ] [ SpinConstraint ];
710            [ Complex<f64> ] [ SpinConstraint ];
711            [ Complex<f64> ] [ SpinOrbitCoupled ];
712        ]
713        duplicate!{
714            [
715                [
716                    btype_nested [OrbitBasis<'a, UnitaryRepresentedSymmetryGroup, SlaterDeterminant<'a, dtype_nested, sctype_nested>>]
717                    calc_smat_nested [calc_smat_optimised]
718                ]
719                [
720                    btype_nested [EagerBasis<SlaterDeterminant<'a, dtype_nested, sctype_nested>>]
721                    calc_smat_nested [calc_smat]
722                ]
723            ]
724            [
725                gtype_ [ UnitaryRepresentedSymmetryGroup ]
726                dtype_ [ dtype_nested ]
727                btype_ [ btype_nested ]
728                sctype_ [ sctype_nested ]
729                doc_sub_ [ "Performs representation analysis using a unitary-represented group and stores the result." ]
730                analyse_fn_ [ analyse_representation ]
731                construct_group_ [ self.construct_unitary_group()? ]
732                calc_smat_ [ calc_smat_nested ]
733                calc_projections_ [
734                    log_subtitle("Multi-determinantal wavefunction projection decompositions");
735                    qsym2_output!("");
736                    qsym2_output!("  Projections are defined w.r.t. the following inner product:");
737                    qsym2_output!("    {}", multidet_orbit.origin().overlap_definition());
738                    qsym2_output!("");
739                    multidet_orbit
740                        .projections_to_string(
741                            &multidet_orbit.calc_projection_compositions()?,
742                            params.integrality_threshold,
743                        )
744                        .log_output_display();
745                    qsym2_output!("");
746                ]
747            ]
748        }
749    }
750    duplicate!{
751        [
752            dtype_nested sctype_nested;
753            [ f64 ] [ SpinConstraint ];
754            [ Complex<f64> ] [ SpinConstraint ];
755            [ Complex<f64> ] [ SpinOrbitCoupled ];
756        ]
757        duplicate!{
758            [
759                [
760                    btype_nested [OrbitBasis<'a, MagneticRepresentedSymmetryGroup, SlaterDeterminant<'a, dtype_nested, sctype_nested>>]
761                    calc_smat_nested [calc_smat_optimised]
762                ]
763                [
764                    btype_nested [EagerBasis<SlaterDeterminant<'a, dtype_nested, sctype_nested>>]
765                    calc_smat_nested [calc_smat]
766                ]
767            ]
768            [
769                gtype_ [ MagneticRepresentedSymmetryGroup ]
770                dtype_ [ dtype_nested ]
771                btype_ [ btype_nested ]
772                sctype_ [ sctype_nested ]
773                doc_sub_ [ "Performs corepresentation analysis using a magnetic-represented group and stores the result." ]
774                analyse_fn_ [ analyse_corepresentation ]
775                construct_group_ [ self.construct_magnetic_group()? ]
776                calc_smat_ [ calc_smat_nested ]
777                calc_projections_ [ ]
778            ]
779        }
780    }
781)]
782impl<'a> MultiDeterminantRepAnalysisDriver<'a, gtype_, dtype_, btype_, sctype_> {
783    #[doc = doc_sub_]
784    fn analyse_fn_(&mut self) -> Result<(), anyhow::Error> {
785        let params = self.parameters;
786        let (sao, sao_h) = self.construct_sao()?;
787        let group = construct_group_;
788        log_cc_transversal(&group);
789        let _ = find_angular_function_representation(&group, self.angular_function_parameters);
790        if group.is_double_group() {
791            let _ = find_spinor_function_representation(&group, self.angular_function_parameters);
792        }
793        if let Some(det) = self
794            .multidets
795            .get(0)
796            .and_then(|multidet| multidet.basis().first())
797        {
798            log_bao(det.bao());
799        }
800
801        let (multidet_symmetries, multidet_symmetries_thresholds): (Vec<_>, Vec<_>) = self.multidets
802            .iter()
803            .enumerate()
804            .map(|(i, multidet)| {
805                log_micsec_begin(&format!("Multi-determinantal wavefunction {i}"));
806                qsym2_output!("");
807                let res = MultiDeterminantSymmetryOrbit::builder()
808                    .group(&group)
809                    .origin(multidet)
810                    .integrality_threshold(params.integrality_threshold)
811                    .linear_independence_threshold(params.linear_independence_threshold)
812                    .symmetry_transformation_kind(params.symmetry_transformation_kind.clone())
813                    .eigenvalue_comparison_mode(params.eigenvalue_comparison_mode.clone())
814                    .build()
815                    .map_err(|err| format_err!(err))
816                    .and_then(|mut multidet_orbit| {
817                        multidet_orbit
818                            .calc_smat_(Some(&sao), sao_h.as_ref(), params.use_cayley_table)?
819                            .normalise_smat()?
820                            .calc_xmat(false)?;
821                        log_overlap_eigenvalues(
822                            "Overlap eigenvalues",
823                            multidet_orbit.smat_eigvals.as_ref().ok_or(format_err!("Orbit overlap eigenvalues not found."))?,
824                            params.linear_independence_threshold,
825                            &params.eigenvalue_comparison_mode
826                        );
827                        qsym2_output!("");
828                        let multidet_symmetry_thresholds = multidet_orbit
829                            .smat_eigvals
830                            .as_ref()
831                            .map(|eigvals| {
832                                let mut eigvals_vec = eigvals.iter().collect::<Vec<_>>();
833                                match multidet_orbit.eigenvalue_comparison_mode() {
834                                    EigenvalueComparisonMode::Modulus => {
835                                        eigvals_vec.sort_by(|a, b| {
836                                            a.abs().partial_cmp(&b.abs()).expect("Unable to compare two eigenvalues based on their moduli.")
837                                        });
838                                    }
839                                    EigenvalueComparisonMode::Real => {
840                                        eigvals_vec.sort_by(|a, b| {
841                                            a.re().partial_cmp(&b.re()).expect("Unable to compare two eigenvalues based on their real parts.")
842                                        });
843                                    }
844                                }
845                                let eigval_above = match multidet_orbit.eigenvalue_comparison_mode() {
846                                    EigenvalueComparisonMode::Modulus => eigvals_vec
847                                        .iter()
848                                        .find(|val| {
849                                            val.abs() >= multidet_orbit.linear_independence_threshold
850                                        })
851                                        .copied()
852                                        .copied(),
853                                    EigenvalueComparisonMode::Real => eigvals_vec
854                                        .iter()
855                                        .find(|val| {
856                                            val.re() >= multidet_orbit.linear_independence_threshold
857                                        })
858                                        .copied()
859                                        .copied(),
860                                };
861                                eigvals_vec.reverse();
862                                let eigval_below = match multidet_orbit.eigenvalue_comparison_mode() {
863                                    EigenvalueComparisonMode::Modulus => eigvals_vec
864                                        .iter()
865                                        .find(|val| {
866                                            val.abs() < multidet_orbit.linear_independence_threshold
867                                        })
868                                        .copied()
869                                        .copied(),
870                                    EigenvalueComparisonMode::Real => eigvals_vec
871                                        .iter()
872                                        .find(|val| {
873                                            val.re() < multidet_orbit.linear_independence_threshold
874                                        })
875                                        .copied()
876                                        .copied(),
877                                };
878                                (eigval_above, eigval_below)
879                            })
880                            .unwrap_or((None, None));
881                        let multidet_sym = multidet_orbit.analyse_rep().map_err(|err| err.to_string());
882                        { calc_projections_ }
883                        Ok((multidet_sym, multidet_symmetry_thresholds))
884                    })
885                    .unwrap_or_else(|err| (Err(err.to_string()), (None, None)));
886                log_micsec_end(&format!("Multi-determinantal wavefunction {i}"));
887                qsym2_output!("");
888                res
889            }).unzip();
890
891        let result = MultiDeterminantRepAnalysisResult::builder()
892            .parameters(params)
893            .multidets(self.multidets.clone())
894            .group(group)
895            .multidet_symmetries(multidet_symmetries)
896            .multidet_symmetries_thresholds(multidet_symmetries_thresholds)
897            .build()?;
898        self.result = Some(result);
899
900        Ok(())
901    }
902}
903
904// ~~~~~~~~~~~~~~~~~~~~~
905// Trait implementations
906// ~~~~~~~~~~~~~~~~~~~~~
907
908// Generic for all symmetry groups G, basis B, and wavefunction numeric type T
909// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
910
911impl<'a, G, T, B, SC> fmt::Display for MultiDeterminantRepAnalysisDriver<'a, G, T, B, SC>
912where
913    G: SymmetryGroupProperties + Clone,
914    G::CharTab: SubspaceDecomposable<T>,
915    T: ComplexFloat + Lapack,
916    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
917    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
918    SC: StructureConstraint + Hash + Eq + fmt::Display,
919{
920    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
921        write_title(f, "Multi-determinantal Wavefunction Symmetry Analysis")?;
922        writeln!(f)?;
923        writeln!(f, "{}", self.parameters)?;
924        Ok(())
925    }
926}
927
928impl<'a, G, T, B, SC> fmt::Debug for MultiDeterminantRepAnalysisDriver<'a, G, T, B, SC>
929where
930    G: SymmetryGroupProperties + Clone,
931    G::CharTab: SubspaceDecomposable<T>,
932    T: ComplexFloat + Lapack,
933    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
934    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
935    SC: StructureConstraint + Hash + Eq + fmt::Display,
936{
937    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
938        writeln!(f, "{self}")
939    }
940}
941
942// Specific for unitary/magnetic-represented groups and determinant numeric type f64/Complex<f64>
943// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
944
945#[duplicate_item(
946    duplicate!{
947        [
948            dtype_nested sctype_nested;
949            [ f64 ] [ SpinConstraint ];
950            [ Complex<f64> ] [ SpinConstraint ];
951            [ Complex<f64> ] [ SpinOrbitCoupled ];
952        ]
953        duplicate!{
954            [
955                btype_nested;
956                [OrbitBasis<'a, UnitaryRepresentedSymmetryGroup, SlaterDeterminant<'a, dtype_nested, sctype_nested>>];
957                [EagerBasis<SlaterDeterminant<'a, dtype_nested, sctype_nested>>]
958            ]
959            [
960                gtype_ [ UnitaryRepresentedSymmetryGroup ]
961                dtype_ [ dtype_nested ]
962                btype_ [ btype_nested ]
963                sctype_ [ sctype_nested ]
964                analyse_fn_ [ analyse_representation ]
965            ]
966        }
967    }
968    duplicate!{
969        [
970            dtype_nested sctype_nested;
971            [ f64 ] [ SpinConstraint ];
972            [ Complex<f64> ] [ SpinConstraint ];
973            [ Complex<f64> ] [ SpinOrbitCoupled ];
974        ]
975        duplicate!{
976            [
977                btype_nested;
978                [OrbitBasis<'a, MagneticRepresentedSymmetryGroup, SlaterDeterminant<'a, dtype_nested, sctype_nested>>];
979                [EagerBasis<SlaterDeterminant<'a, dtype_nested, sctype_nested>>]
980            ]
981            [
982                gtype_ [ MagneticRepresentedSymmetryGroup ]
983                dtype_ [ dtype_nested ]
984                btype_ [ btype_nested ]
985                sctype_ [ sctype_nested ]
986                analyse_fn_ [ analyse_corepresentation ]
987            ]
988        }
989    }
990)]
991impl<'a> QSym2Driver for MultiDeterminantRepAnalysisDriver<'a, gtype_, dtype_, btype_, sctype_> {
992    type Params = MultiDeterminantRepAnalysisParams<f64>;
993
994    type Outcome = MultiDeterminantRepAnalysisResult<'a, gtype_, dtype_, btype_, sctype_>;
995
996    fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
997        self.result.as_ref().ok_or_else(|| {
998            format_err!(
999                "No multi-determinantal wavefunction representation analysis results found."
1000            )
1001        })
1002    }
1003
1004    fn run(&mut self) -> Result<(), anyhow::Error> {
1005        self.log_output_display();
1006        self.analyse_fn_()?;
1007        self.result()?.log_output_display();
1008        Ok(())
1009    }
1010}