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, ensure, format_err};
9use derive_builder::Builder;
10use duplicate::duplicate_item;
11use ndarray::{Array2, s};
12use ndarray_linalg::types::Lapack;
13use num_complex::{Complex, ComplexFloat};
14use num_traits::Float;
15use serde::{Deserialize, Serialize};
16
17use crate::analysis::{
18    EigenvalueComparisonMode, Overlap, ProjectionDecomposition, RepAnalysis,
19    log_overlap_eigenvalues,
20};
21use crate::angmom::spinor_rotation_3d::{SpinConstraint, SpinOrbitCoupled, StructureConstraint};
22use crate::chartab::SubspaceDecomposable;
23use crate::chartab::chartab_group::CharacterProperties;
24use crate::drivers::QSym2Driver;
25use crate::drivers::representation_analysis::angular_function::{
26    AngularFunctionRepAnalysisParams, find_angular_function_representation,
27    find_spinor_function_representation,
28};
29use crate::drivers::representation_analysis::{
30    CharacterTableDisplay, MagneticSymmetryAnalysisKind, fn_construct_magnetic_group,
31    fn_construct_unitary_group, log_bao, log_cc_transversal,
32};
33use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
34use crate::group::{GroupProperties, MagneticRepresentedGroup, UnitaryRepresentedGroup};
35use crate::io::format::{
36    QSym2Output, log_micsec_begin, log_micsec_end, log_subtitle, nice_bool, qsym2_output,
37    write_subtitle, write_title,
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;
46use crate::target::noci::multideterminant::multideterminant_analysis::MultiDeterminantSymmetryOrbit;
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            "#", "Energy", "Symmetry", "Eig. above",
372        )?;
373        writeln!(f, "{}", "┈".repeat(table_width))?;
374
375        for (multidet_i, multidet) in self.multidets.iter().enumerate() {
376            let multidet_energy_str = multidet
377                .energy()
378                .map(|multidet_energy| format!("{multidet_energy:>+multidet_energy_length$.7}"))
379                .unwrap_or("--".to_string());
380            let multidet_sym_str = self
381                .multidet_symmetries
382                .get(multidet_i)
383                .ok_or_else(|| format!("Unable to retrieve the symmetry of multideterminantal wavefunction index `{multidet_i}`."))
384                .and_then(|sym_res| sym_res.as_ref().map(|sym| sym.to_string()).map_err(|err| err.to_string()))
385                .unwrap_or_else(|err| err);
386
387            let (eig_above_str, eig_below_str) = self
388                .multidet_symmetries_thresholds
389                .get(multidet_i)
390                .map(|(eig_above_opt, eig_below_opt)| {
391                    (
392                        eig_above_opt
393                            .map(|eig_above| format!("{eig_above:>+.3e}"))
394                            .unwrap_or("--".to_string()),
395                        eig_below_opt
396                            .map(|eig_below| format!("{eig_below:>+.3e}"))
397                            .unwrap_or("--".to_string()),
398                    )
399                })
400                .unwrap_or(("--".to_string(), "--".to_string()));
401            writeln!(
402                f,
403                " {multidet_i:>multidet_index_length$}  \
404                {multidet_energy_str:<multidet_energy_length$}  \
405                {multidet_sym_str:<multidet_symmetry_length$}  \
406                {eig_above_str:<multidet_eig_above_length$}  \
407                {eig_below_str}"
408            )?;
409        }
410
411        writeln!(f, "{}", "┈".repeat(table_width))?;
412        writeln!(f)?;
413
414        Ok(())
415    }
416}
417
418impl<'a, G, T, B, SC> fmt::Debug for MultiDeterminantRepAnalysisResult<'a, G, T, B, SC>
419where
420    G: SymmetryGroupProperties + Clone,
421    G::CharTab: SubspaceDecomposable<T>,
422    T: ComplexFloat + Lapack,
423    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
424    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
425    SC: StructureConstraint + Clone + Hash + Eq + fmt::Display,
426{
427    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428        writeln!(f, "{self}")
429    }
430}
431
432// ------
433// Driver
434// ------
435
436// ~~~~~~~~~~~~~~~~~
437// Struct definition
438// ~~~~~~~~~~~~~~~~~
439
440/// Driver structure for performing representation analysis on multi-determinantal wavefunctions.
441#[derive(Clone, Builder)]
442#[builder(build_fn(validate = "Self::validate"))]
443pub struct MultiDeterminantRepAnalysisDriver<'a, G, T, B, SC>
444where
445    G: SymmetryGroupProperties + Clone,
446    G::CharTab: SubspaceDecomposable<T>,
447    T: ComplexFloat + Lapack,
448    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
449    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
450    SC: StructureConstraint + Hash + Eq + fmt::Display,
451{
452    /// The control parameters for multi-determinantal wavefunction representation analysis.
453    parameters: &'a MultiDeterminantRepAnalysisParams<<T as ComplexFloat>::Real>,
454
455    /// The multi-determinantal wavefunctions to be analysed.
456    multidets: Vec<&'a MultiDeterminant<'a, T, B, SC>>,
457
458    /// The result from symmetry-group detection on the underlying molecular structure of the
459    /// multi-determinantal wavefunctions.
460    symmetry_group: &'a SymmetryGroupDetectionResult,
461
462    /// The atomic-orbital overlap matrix of the underlying basis set used to describe the
463    /// wavefunctions. This is either for a single component corresponding to the basis functions
464    /// specified by the basis angular order structure in the determinants in [`Self::multidets`],
465    /// or for *all* explicit components specified by the coefficients in the determinants.
466    sao: &'a Array2<T>,
467
468    /// The complex-symmetric atomic-orbital overlap matrix of the underlying basis set used to
469    /// describe the wavefunctions. This is either for a single component corresponding to the basis
470    /// functions specified by the basis angular order structure in the determinants, or for *all*
471    /// explicit components specified by the coefficients in the determinants. This is required if
472    /// antiunitary symmetry operations are involved. If none is provided, this will be assumed to
473    /// be the same as [`Self::sao`].
474    #[builder(default = "None")]
475    sao_h: Option<&'a Array2<T>>,
476
477    /// The control parameters for symmetry analysis of angular functions.
478    angular_function_parameters: &'a AngularFunctionRepAnalysisParams,
479
480    /// The result of the multi-determinantal wavefunction representation analysis.
481    #[builder(setter(skip), default = "None")]
482    result: Option<MultiDeterminantRepAnalysisResult<'a, G, T, B, SC>>,
483}
484
485impl<'a, G, T, B, SC> MultiDeterminantRepAnalysisDriverBuilder<'a, G, T, B, SC>
486where
487    G: SymmetryGroupProperties + Clone,
488    G::CharTab: SubspaceDecomposable<T>,
489    T: ComplexFloat + Lapack,
490    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
491    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
492    SC: StructureConstraint + Hash + Eq + fmt::Display,
493{
494    fn validate(&self) -> Result<(), String> {
495        let params = self.parameters.ok_or(
496            "No multi-determinantal wavefunction representation analysis parameters found."
497                .to_string(),
498        )?;
499
500        let sym_res = self
501            .symmetry_group
502            .ok_or("No symmetry group information found.".to_string())?;
503
504        let sao = self.sao.ok_or("No SAO matrix found.".to_string())?;
505
506        if let Some(sao_h) = self.sao_h.flatten() {
507            if sao_h.shape() != sao.shape() {
508                return Err("Mismatched shapes between `sao` and `sao_h`.".to_string());
509            }
510        }
511
512        let multidets = self
513            .multidets
514            .as_ref()
515            .ok_or("No multi-determinantal wavefunctions found.".to_string())?;
516        let mut nfuncs_ncomps_set = multidets
517            .iter()
518            .flat_map(|multidet| {
519                multidet.basis().iter().map(|det_res| {
520                    det_res.map(|det| {
521                        (
522                            det.baos()
523                                .iter()
524                                .map(|bao| bao.n_funcs())
525                                .collect::<Vec<_>>(),
526                            det.structure_constraint()
527                                .n_explicit_comps_per_coefficient_matrix(),
528                        )
529                    })
530                })
531            })
532            .collect::<Result<HashSet<(Vec<usize>, usize)>, _>>()
533            .map_err(|err| err.to_string())?;
534        let (nfuncs, _) = if nfuncs_ncomps_set.len() == 1 {
535            nfuncs_ncomps_set
536                .drain()
537                .next()
538                .ok_or("Unable to retrieve the number of AO basis functions and the number of explicit components.".to_string())
539        } else {
540            Err("Inconsistent numbers of 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(format!(
554                "Representation analysis cannot be performed using the entirety of the infinite group `{}`. \
555                    Consider setting the parameter `infinite_order_to_finite` to restrict to a finite subgroup instead.",
556                sym.group_name
557                    .as_ref()
558                    .expect("No symmetry group name found.")
559            ))
560        } else {
561            let total_component_check = {
562                let nfuncs_tot = nfuncs.iter().sum::<usize>();
563                sao.nrows() == nfuncs_tot && sao.ncols() == nfuncs_tot
564            };
565            let nfuncs_set = nfuncs.into_iter().collect::<HashSet<_>>();
566            let uniform_component_check = nfuncs_set.len() == 1
567                && {
568                    let nfuncs = nfuncs_set.iter().next().ok_or_else(|| "Unable to extract the uniform number of basis functions per explicit component.".to_string())?;
569                    sao.nrows() == *nfuncs && sao.ncols() == *nfuncs
570                };
571            if !uniform_component_check && !total_component_check {
572                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 of the basis determinants.".to_string())
573            } else {
574                Ok(())
575            }
576        }
577    }
578}
579
580// ~~~~~~~~~~~~~~~~~~~~~~
581// Struct implementations
582// ~~~~~~~~~~~~~~~~~~~~~~
583
584// Generic for all symmetry groups G and wavefunction numeric type T
585// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
586
587impl<'a, G, T, B, SC> MultiDeterminantRepAnalysisDriver<'a, G, T, B, SC>
588where
589    G: SymmetryGroupProperties + Clone,
590    G::CharTab: SubspaceDecomposable<T>,
591    T: ComplexFloat + Lapack,
592    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
593    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
594    SC: StructureConstraint + Clone + Hash + Eq + fmt::Display,
595{
596    /// Returns a builder to construct a [`MultiDeterminantRepAnalysisDriver`] structure.
597    pub fn builder() -> MultiDeterminantRepAnalysisDriverBuilder<'a, G, T, B, SC> {
598        MultiDeterminantRepAnalysisDriverBuilder::default()
599    }
600
601    /// Constructs the appropriate atomic-orbital overlap matrix based on the structure constraint of
602    /// the multi-determinantal wavefunctions and the provided overlap matrix.
603    fn construct_sao(&self) -> Result<(Array2<T>, Option<Array2<T>>), anyhow::Error> {
604        let mut structure_constraint_set = self
605            .multidets
606            .iter()
607            .map(|multidet| multidet.structure_constraint())
608            .collect::<HashSet<_>>();
609        let structure_constraint = if structure_constraint_set.len() == 1 {
610            structure_constraint_set.drain().next().ok_or(format_err!(
611                "Unable to retrieve the structure constraint of the multi-determinantal wavefunctions."
612            ))
613        } else {
614            Err(format_err!(
615                "Inconsistent structure constraints across multi-determinantal wavefunctions."
616            ))
617        }?;
618
619        let mut nfuncss_set = self
620            .multidets
621            .iter()
622            .map(|multidet| {
623                multidet
624                    .basis()
625                    .first()
626                    .expect("Unable to obtain the first determinant in the basis.")
627                    .baos()
628                    .iter()
629                    .map(|bao| bao.n_funcs())
630                    .collect::<Vec<_>>()
631            })
632            .collect::<HashSet<Vec<usize>>>();
633        let nfuncs_vec = if nfuncss_set.len() == 1 {
634            nfuncss_set.drain().next().ok_or(format_err!(
635                "Unable to retrieve the number of basis functions describing the multi-determinantal wavefunctions."
636            ))
637        } else {
638            Err(format_err!(
639                "Inconsistent numbers of basis functions across multi-determinantal wavefunctions."
640            ))
641        }?;
642        // let nbas_set = self
643        //     .multidets
644        //     .iter()
645        //     .next()
646        //     .ok_or_else(|| format_err!("Unable to retrieve "))
647        //     .baos()
648        //     .iter()
649        //     .map(|bao| bao.n_funcs())
650        //     .collect::<HashSet<_>>();
651        let nfuncs_set = nfuncs_vec.iter().cloned().collect::<HashSet<_>>();
652        let uniform_component = nfuncs_set.len() == 1;
653        let ncomps = structure_constraint.n_explicit_comps_per_coefficient_matrix();
654        let provided_dim = self.sao.nrows();
655
656        if uniform_component {
657            let nfuncs = *nfuncs_set.iter().next().ok_or_else(|| format_err!("Unable to extract the uniform number of basis functions per explicit component."))?;
658            if provided_dim == nfuncs {
659                let sao = {
660                    let mut sao_mut = Array2::zeros((ncomps * nfuncs, ncomps * nfuncs));
661                    (0..ncomps).for_each(|icomp| {
662                        let start = icomp * nfuncs;
663                        let end = (icomp + 1) * nfuncs;
664                        sao_mut
665                            .slice_mut(s![start..end, start..end])
666                            .assign(self.sao);
667                    });
668                    sao_mut
669                };
670
671                let sao_h = self.sao_h.map(|sao_h| {
672                    let mut sao_h_mut = Array2::zeros((ncomps * nfuncs, ncomps * nfuncs));
673                    (0..ncomps).for_each(|icomp| {
674                        let start = icomp * nfuncs;
675                        let end = (icomp + 1) * nfuncs;
676                        sao_h_mut
677                            .slice_mut(s![start..end, start..end])
678                            .assign(sao_h);
679                    });
680                    sao_h_mut
681                });
682
683                Ok((sao, sao_h))
684            } else {
685                ensure!(provided_dim == nfuncs * ncomps);
686                Ok((self.sao.clone(), self.sao_h.cloned()))
687            }
688        } else {
689            let nfuncs_tot = nfuncs_vec.iter().sum::<usize>();
690            ensure!(provided_dim == nfuncs_tot);
691            Ok((self.sao.clone(), self.sao_h.cloned()))
692        }
693    }
694}
695
696// Specific for unitary-represented symmetry groups, but generic for wavefunction numeric type T
697// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
698
699impl<'a, T, B, SC> MultiDeterminantRepAnalysisDriver<'a, UnitaryRepresentedSymmetryGroup, T, B, SC>
700where
701    T: ComplexFloat + Lapack + Sync + Send,
702    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + Sync + Send,
703    for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
704    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
705    SC: StructureConstraint + Hash + Eq + fmt::Display,
706{
707    fn_construct_unitary_group!(
708        /// Constructs the unitary-represented group (which itself can be unitary or magnetic) ready
709        /// for Slater determinant representation analysis.
710        construct_unitary_group
711    );
712}
713
714// Specific for magnetic-represented symmetry groups, but generic for wavefunction numeric type T
715// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
716
717impl<'a, T, B, SC> MultiDeterminantRepAnalysisDriver<'a, MagneticRepresentedSymmetryGroup, T, B, SC>
718where
719    T: ComplexFloat + Lapack + Sync + Send,
720    <T as ComplexFloat>::Real: From<f64> + Sync + Send + fmt::LowerExp + fmt::Debug,
721    for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
722    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
723    SC: StructureConstraint + Hash + Eq + fmt::Display,
724{
725    fn_construct_magnetic_group!(
726        /// Constructs the magnetic-represented group (which itself can only be magnetic) ready for
727        /// Slater determinant corepresentation analysis.
728        construct_magnetic_group
729    );
730}
731
732// Specific for unitary-represented and magnetic-represented symmetry groups and wavefunction numeric types f64 and C128
733// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
734
735#[duplicate_item(
736    duplicate!{
737        [
738            dtype_nested sctype_nested;
739            [ f64 ] [ SpinConstraint ];
740            [ Complex<f64> ] [ SpinConstraint ];
741            [ Complex<f64> ] [ SpinOrbitCoupled ];
742        ]
743        duplicate!{
744            [
745                [
746                    btype_nested [OrbitBasis<'a, UnitaryRepresentedSymmetryGroup, SlaterDeterminant<'a, dtype_nested, sctype_nested>>]
747                    calc_smat_nested [calc_smat_optimised]
748                ]
749                [
750                    btype_nested [EagerBasis<SlaterDeterminant<'a, dtype_nested, sctype_nested>>]
751                    calc_smat_nested [calc_smat]
752                ]
753            ]
754            [
755                gtype_ [ UnitaryRepresentedSymmetryGroup ]
756                dtype_ [ dtype_nested ]
757                btype_ [ btype_nested ]
758                sctype_ [ sctype_nested ]
759                doc_sub_ [ "Performs representation analysis using a unitary-represented group and stores the result." ]
760                analyse_fn_ [ analyse_representation ]
761                construct_group_ [ self.construct_unitary_group()? ]
762                calc_smat_ [ calc_smat_nested ]
763                calc_projections_ [
764                    log_subtitle("Multi-determinantal wavefunction projection decompositions");
765                    qsym2_output!("");
766                    qsym2_output!("  Projections are defined w.r.t. the following inner product:");
767                    qsym2_output!("    {}", multidet_orbit.origin().overlap_definition());
768                    qsym2_output!("");
769                    multidet_orbit
770                        .projections_to_string(
771                            &multidet_orbit.calc_projection_compositions()?,
772                            params.integrality_threshold,
773                        )
774                        .log_output_display();
775                    qsym2_output!("");
776                ]
777            ]
778        }
779    }
780    duplicate!{
781        [
782            dtype_nested sctype_nested;
783            [ f64 ] [ SpinConstraint ];
784            [ Complex<f64> ] [ SpinConstraint ];
785            [ Complex<f64> ] [ SpinOrbitCoupled ];
786        ]
787        duplicate!{
788            [
789                [
790                    btype_nested [OrbitBasis<'a, MagneticRepresentedSymmetryGroup, SlaterDeterminant<'a, dtype_nested, sctype_nested>>]
791                    calc_smat_nested [calc_smat_optimised]
792                ]
793                [
794                    btype_nested [EagerBasis<SlaterDeterminant<'a, dtype_nested, sctype_nested>>]
795                    calc_smat_nested [calc_smat]
796                ]
797            ]
798            [
799                gtype_ [ MagneticRepresentedSymmetryGroup ]
800                dtype_ [ dtype_nested ]
801                btype_ [ btype_nested ]
802                sctype_ [ sctype_nested ]
803                doc_sub_ [ "Performs corepresentation analysis using a magnetic-represented group and stores the result." ]
804                analyse_fn_ [ analyse_corepresentation ]
805                construct_group_ [ self.construct_magnetic_group()? ]
806                calc_smat_ [ calc_smat_nested ]
807                calc_projections_ [ ]
808            ]
809        }
810    }
811)]
812impl<'a> MultiDeterminantRepAnalysisDriver<'a, gtype_, dtype_, btype_, sctype_> {
813    #[doc = doc_sub_]
814    fn analyse_fn_(&mut self) -> Result<(), anyhow::Error> {
815        let params = self.parameters;
816        let (sao, sao_h) = self.construct_sao()?;
817        let group = construct_group_;
818        log_cc_transversal(&group);
819        let _ = find_angular_function_representation(&group, self.angular_function_parameters);
820        if group.is_double_group() {
821            let _ = find_spinor_function_representation(&group, self.angular_function_parameters);
822        }
823        if let Some(det) = self
824            .multidets
825            .get(0)
826            .and_then(|multidet| multidet.basis().first())
827        {
828            for (bao_i, bao) in det.baos().iter().enumerate() {
829                log_bao(bao, Some(bao_i));
830            }
831        }
832
833        let (multidet_symmetries, multidet_symmetries_thresholds): (Vec<_>, Vec<_>) = self.multidets
834            .iter()
835            .enumerate()
836            .map(|(i, multidet)| {
837                log_micsec_begin(&format!("Multi-determinantal wavefunction {i}"));
838                qsym2_output!("");
839                let res = MultiDeterminantSymmetryOrbit::builder()
840                    .group(&group)
841                    .origin(multidet)
842                    .integrality_threshold(params.integrality_threshold)
843                    .linear_independence_threshold(params.linear_independence_threshold)
844                    .symmetry_transformation_kind(params.symmetry_transformation_kind.clone())
845                    .eigenvalue_comparison_mode(params.eigenvalue_comparison_mode.clone())
846                    .build()
847                    .map_err(|err| format_err!(err))
848                    .and_then(|mut multidet_orbit| {
849                        multidet_orbit
850                            .calc_smat_(Some(&sao), sao_h.as_ref(), params.use_cayley_table)?
851                            .normalise_smat()?
852                            .calc_xmat(false)?;
853                        log_overlap_eigenvalues(
854                            "Overlap eigenvalues",
855                            multidet_orbit.smat_eigvals.as_ref().ok_or(format_err!("Orbit overlap eigenvalues not found."))?,
856                            params.linear_independence_threshold,
857                            &params.eigenvalue_comparison_mode
858                        );
859                        qsym2_output!("");
860                        let multidet_symmetry_thresholds = multidet_orbit
861                            .smat_eigvals
862                            .as_ref()
863                            .map(|eigvals| {
864                                let mut eigvals_vec = eigvals.iter().collect::<Vec<_>>();
865                                match multidet_orbit.eigenvalue_comparison_mode() {
866                                    EigenvalueComparisonMode::Modulus => {
867                                        eigvals_vec.sort_by(|a, b| {
868                                            a.abs().partial_cmp(&b.abs()).expect("Unable to compare two eigenvalues based on their moduli.")
869                                        });
870                                    }
871                                    EigenvalueComparisonMode::Real => {
872                                        eigvals_vec.sort_by(|a, b| {
873                                            a.re().partial_cmp(&b.re()).expect("Unable to compare two eigenvalues based on their real parts.")
874                                        });
875                                    }
876                                }
877                                let eigval_above = match multidet_orbit.eigenvalue_comparison_mode() {
878                                    EigenvalueComparisonMode::Modulus => eigvals_vec
879                                        .iter()
880                                        .find(|val| {
881                                            val.abs() >= multidet_orbit.linear_independence_threshold
882                                        })
883                                        .copied()
884                                        .copied(),
885                                    EigenvalueComparisonMode::Real => eigvals_vec
886                                        .iter()
887                                        .find(|val| {
888                                            val.re() >= multidet_orbit.linear_independence_threshold
889                                        })
890                                        .copied()
891                                        .copied(),
892                                };
893                                eigvals_vec.reverse();
894                                let eigval_below = match multidet_orbit.eigenvalue_comparison_mode() {
895                                    EigenvalueComparisonMode::Modulus => eigvals_vec
896                                        .iter()
897                                        .find(|val| {
898                                            val.abs() < multidet_orbit.linear_independence_threshold
899                                        })
900                                        .copied()
901                                        .copied(),
902                                    EigenvalueComparisonMode::Real => eigvals_vec
903                                        .iter()
904                                        .find(|val| {
905                                            val.re() < multidet_orbit.linear_independence_threshold
906                                        })
907                                        .copied()
908                                        .copied(),
909                                };
910                                (eigval_above, eigval_below)
911                            })
912                            .unwrap_or((None, None));
913                        let multidet_sym = multidet_orbit.analyse_rep().map_err(|err| err.to_string());
914                        { calc_projections_ }
915                        Ok((multidet_sym, multidet_symmetry_thresholds))
916                    })
917                    .unwrap_or_else(|err| (Err(err.to_string()), (None, None)));
918                log_micsec_end(&format!("Multi-determinantal wavefunction {i}"));
919                qsym2_output!("");
920                res
921            }).unzip();
922
923        let result = MultiDeterminantRepAnalysisResult::builder()
924            .parameters(params)
925            .multidets(self.multidets.clone())
926            .group(group)
927            .multidet_symmetries(multidet_symmetries)
928            .multidet_symmetries_thresholds(multidet_symmetries_thresholds)
929            .build()?;
930        self.result = Some(result);
931
932        Ok(())
933    }
934}
935
936// ~~~~~~~~~~~~~~~~~~~~~
937// Trait implementations
938// ~~~~~~~~~~~~~~~~~~~~~
939
940// Generic for all symmetry groups G, basis B, and wavefunction numeric type T
941// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
942
943impl<'a, G, T, B, SC> fmt::Display for MultiDeterminantRepAnalysisDriver<'a, G, T, B, SC>
944where
945    G: SymmetryGroupProperties + Clone,
946    G::CharTab: SubspaceDecomposable<T>,
947    T: ComplexFloat + Lapack,
948    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
949    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
950    SC: StructureConstraint + Hash + Eq + fmt::Display,
951{
952    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
953        write_title(f, "Multi-determinantal Wavefunction Symmetry Analysis")?;
954        writeln!(f)?;
955        writeln!(f, "{}", self.parameters)?;
956        Ok(())
957    }
958}
959
960impl<'a, G, T, B, SC> fmt::Debug for MultiDeterminantRepAnalysisDriver<'a, G, T, B, SC>
961where
962    G: SymmetryGroupProperties + Clone,
963    G::CharTab: SubspaceDecomposable<T>,
964    T: ComplexFloat + Lapack,
965    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
966    B: Basis<SlaterDeterminant<'a, T, SC>> + Clone,
967    SC: StructureConstraint + Hash + Eq + fmt::Display,
968{
969    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
970        writeln!(f, "{self}")
971    }
972}
973
974// Specific for unitary/magnetic-represented groups and determinant numeric type f64/Complex<f64>
975// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
976
977#[duplicate_item(
978    duplicate!{
979        [
980            dtype_nested sctype_nested;
981            [ f64 ] [ SpinConstraint ];
982            [ Complex<f64> ] [ SpinConstraint ];
983            [ Complex<f64> ] [ SpinOrbitCoupled ];
984        ]
985        duplicate!{
986            [
987                btype_nested;
988                [OrbitBasis<'a, UnitaryRepresentedSymmetryGroup, SlaterDeterminant<'a, dtype_nested, sctype_nested>>];
989                [EagerBasis<SlaterDeterminant<'a, dtype_nested, sctype_nested>>]
990            ]
991            [
992                gtype_ [ UnitaryRepresentedSymmetryGroup ]
993                dtype_ [ dtype_nested ]
994                btype_ [ btype_nested ]
995                sctype_ [ sctype_nested ]
996                analyse_fn_ [ analyse_representation ]
997            ]
998        }
999    }
1000    duplicate!{
1001        [
1002            dtype_nested sctype_nested;
1003            [ f64 ] [ SpinConstraint ];
1004            [ Complex<f64> ] [ SpinConstraint ];
1005            [ Complex<f64> ] [ SpinOrbitCoupled ];
1006        ]
1007        duplicate!{
1008            [
1009                btype_nested;
1010                [OrbitBasis<'a, MagneticRepresentedSymmetryGroup, SlaterDeterminant<'a, dtype_nested, sctype_nested>>];
1011                [EagerBasis<SlaterDeterminant<'a, dtype_nested, sctype_nested>>]
1012            ]
1013            [
1014                gtype_ [ MagneticRepresentedSymmetryGroup ]
1015                dtype_ [ dtype_nested ]
1016                btype_ [ btype_nested ]
1017                sctype_ [ sctype_nested ]
1018                analyse_fn_ [ analyse_corepresentation ]
1019            ]
1020        }
1021    }
1022)]
1023impl<'a> QSym2Driver for MultiDeterminantRepAnalysisDriver<'a, gtype_, dtype_, btype_, sctype_> {
1024    type Params = MultiDeterminantRepAnalysisParams<f64>;
1025
1026    type Outcome = MultiDeterminantRepAnalysisResult<'a, gtype_, dtype_, btype_, sctype_>;
1027
1028    fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
1029        self.result.as_ref().ok_or_else(|| {
1030            format_err!(
1031                "No multi-determinantal wavefunction representation analysis results found."
1032            )
1033        })
1034    }
1035
1036    fn run(&mut self) -> Result<(), anyhow::Error> {
1037        self.log_output_display();
1038        self.analyse_fn_()?;
1039        self.result()?.log_output_display();
1040        Ok(())
1041    }
1042}