Skip to main content

qsym2/interfaces/qchem/hdf5/
slater_determinant.rs

1//! Slater determinants from Q-Chem HDF5 archives.
2
3use std::fmt;
4use std::marker::PhantomData;
5use std::path::PathBuf;
6
7use anyhow::{self, Context, bail, format_err};
8use derive_builder::Builder;
9use duplicate::duplicate_item;
10use factorial::DoubleFactorial;
11use hdf5::{self, H5Type};
12use lazy_static::lazy_static;
13use log;
14use nalgebra::Point3;
15use ndarray::{Array1, Array2, Array4, Axis, Ix3, s};
16use ndarray_linalg::types::Lapack;
17use num_complex::ComplexFloat;
18use num_traits::{One, ToPrimitive, Zero};
19use numeric_sort;
20use periodic_table::periodic_table;
21use regex::Regex;
22
23use crate::angmom::spinor_rotation_3d::{SpinConstraint, StructureConstraint};
24use crate::auxiliary::atom::{Atom, ElementMap};
25use crate::auxiliary::molecule::Molecule;
26use crate::basis::ao::*;
27use crate::basis::ao_integrals::*;
28use crate::chartab::SubspaceDecomposable;
29use crate::chartab::chartab_group::CharacterProperties;
30use crate::drivers::QSym2Driver;
31use crate::drivers::representation_analysis::MagneticSymmetryAnalysisKind;
32use crate::drivers::representation_analysis::angular_function::AngularFunctionRepAnalysisParams;
33use crate::drivers::representation_analysis::slater_determinant::{
34    SlaterDeterminantRepAnalysisDriver, SlaterDeterminantRepAnalysisParams,
35};
36use crate::drivers::symmetry_group_detection::{
37    SymmetryGroupDetectionDriver, SymmetryGroupDetectionResult,
38};
39use crate::integrals::shell_tuple::build_shell_tuple_collection;
40use crate::interfaces::input::SymmetryGroupDetectionInputKind;
41use crate::io::format::{
42    log_macsec_begin, log_macsec_end, log_micsec_begin, log_micsec_end, qsym2_error, qsym2_output,
43};
44use crate::io::{QSym2FileType, read_qsym2_binary};
45use crate::symmetry::symmetry_core::Symmetry;
46use crate::symmetry::symmetry_group::{
47    MagneticRepresentedSymmetryGroup, SymmetryGroupProperties, UnitaryRepresentedSymmetryGroup,
48};
49use crate::target::determinant::SlaterDeterminant;
50
51#[cfg(test)]
52#[path = "slater_determinant_tests.rs"]
53mod slater_determinant_tests;
54
55// =====================
56// Full Q-Chem H5 Driver
57// =====================
58
59lazy_static! {
60    static ref SP_PATH_RE: Regex =
61        Regex::new(r"(.*sp)\\energy_function$").expect("Regex pattern invalid.");
62}
63
64// -----------------
65// Struct definition
66// -----------------
67
68/// Driver to perform symmetry-group detection and Slater determinant representation symmetry
69/// analysis for all discoverable single-point calculation data stored in a Q-Chem's `qarchive.h5`
70/// file.
71#[derive(Clone, Builder)]
72pub struct QChemSlaterDeterminantH5Driver<'a, T>
73where
74    T: Clone,
75{
76    /// The `qarchive.h5` file name.
77    filename: PathBuf,
78
79    /// The input specification controlling symmetry-group detection.
80    symmetry_group_detection_input: &'a SymmetryGroupDetectionInputKind,
81
82    /// The parameters controlling representation analysis of standard angular functions.
83    angular_function_analysis_parameters: &'a AngularFunctionRepAnalysisParams,
84
85    /// The parameters controlling representation analysis.
86    rep_analysis_parameters: &'a SlaterDeterminantRepAnalysisParams<f64>,
87
88    /// The simplified result of the analysis. Each element in the vector is a tuple containing the
89    /// group name and the representation symmetry of the Slater determinant for one single-point
90    /// calculation.
91    #[builder(default = "None")]
92    result: Option<Vec<(String, String)>>,
93
94    /// The numerical type of the Slater determinant.
95    #[builder(setter(skip), default = "PhantomData")]
96    numerical_type: PhantomData<T>,
97}
98
99// ----------------------
100// Struct implementations
101// ----------------------
102
103impl<'a, T> QChemSlaterDeterminantH5Driver<'a, T>
104where
105    T: Clone,
106{
107    /// Returns a builder to construct a [`QChemSlaterDeterminantH5Driver`].
108    pub fn builder() -> QChemSlaterDeterminantH5DriverBuilder<'a, T> {
109        QChemSlaterDeterminantH5DriverBuilder::default()
110    }
111}
112
113// ~~~~~~~~~~~~~~~~~~~
114// Slater determinants
115// ~~~~~~~~~~~~~~~~~~~
116
117// Specific for Slater determinant numeric type f64
118// ''''''''''''''''''''''''''''''''''''''''''''''''
119impl<'a> QChemSlaterDeterminantH5Driver<'a, f64> {
120    /// Performs analysis for all real-valued single-point determinants.
121    fn analyse(&mut self) -> Result<(), anyhow::Error> {
122        let f = hdf5::File::open(&self.filename)?;
123        let mut sp_paths = f
124            .group(".counters")?
125            .member_names()?
126            .iter()
127            .filter_map(|path| {
128                if SP_PATH_RE.is_match(path) {
129                    let path = path.replace("\\", "/");
130                    let mut energy_function_indices = f
131                        .group(&path)
132                        .and_then(|sp_energy_function_group| {
133                            sp_energy_function_group.member_names()
134                        })
135                        .ok()?;
136                    numeric_sort::sort(&mut energy_function_indices);
137                    Some((
138                        path.replace("/energy_function", ""),
139                        energy_function_indices,
140                    ))
141                } else {
142                    None
143                }
144            })
145            .collect::<Vec<_>>();
146        sp_paths.sort_by(|(path_a, _), (path_b, _)| numeric_sort::cmp(path_a, path_b));
147
148        let pd_input = self.symmetry_group_detection_input;
149        let afa_params = self.angular_function_analysis_parameters;
150        let sda_params = self.rep_analysis_parameters;
151        let result = sp_paths
152            .iter()
153            .flat_map(|(sp_path, energy_function_indices)| {
154                energy_function_indices.iter().map(|energy_function_index| {
155                    log_macsec_begin(&format!(
156                        "Analysis for {} (energy function {energy_function_index})",
157                        sp_path.clone()
158                    ));
159                    qsym2_output!("");
160                    let sp = f.group(sp_path)?;
161                    let sp_driver_result = match sda_params.use_magnetic_group {
162                        Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
163                            let mut sp_driver = QChemSlaterDeterminantH5SinglePointDriver::<
164                                MagneticRepresentedSymmetryGroup,
165                                f64,
166                            >::builder()
167                            .sp_group(&sp)
168                            .energy_function_index(energy_function_index)
169                            .symmetry_group_detection_input(pd_input)
170                            .angular_function_analysis_parameters(afa_params)
171                            .rep_analysis_parameters(sda_params)
172                            .build()?;
173                            let _ = sp_driver.run();
174                            sp_driver.result().map(|(sym, rep)| {
175                                (
176                                    sym.group_name
177                                        .as_ref()
178                                        .unwrap_or(&String::new())
179                                        .to_string(),
180                                    rep.as_ref()
181                                        .map(|rep| rep.to_string())
182                                        .unwrap_or_else(|err| err.to_string()),
183                                )
184                            })
185                        }
186                        Some(MagneticSymmetryAnalysisKind::Representation) | None => {
187                            let mut sp_driver = QChemSlaterDeterminantH5SinglePointDriver::<
188                                UnitaryRepresentedSymmetryGroup,
189                                f64,
190                            >::builder()
191                            .sp_group(&sp)
192                            .energy_function_index(energy_function_index)
193                            .symmetry_group_detection_input(pd_input)
194                            .angular_function_analysis_parameters(afa_params)
195                            .rep_analysis_parameters(sda_params)
196                            .build()?;
197                            let _ = sp_driver.run();
198                            sp_driver.result().map(|(sym, rep)| {
199                                (
200                                    sym.group_name
201                                        .as_ref()
202                                        .unwrap_or(&String::new())
203                                        .to_string(),
204                                    rep.as_ref()
205                                        .map(|rep| rep.to_string())
206                                        .unwrap_or_else(|err| err.to_string()),
207                                )
208                            })
209                        }
210                    };
211                    qsym2_output!("");
212                    log_macsec_end(&format!(
213                        "Analysis for {} (energy function {energy_function_index})",
214                        sp_path.clone()
215                    ));
216                    qsym2_output!("");
217                    sp_driver_result
218                })
219            })
220            .map(|res| {
221                res.unwrap_or_else(|err| {
222                    (
223                        "Unidentified symmetry group".to_string(),
224                        format!("Unidentified (co)representation: {err}"),
225                    )
226                })
227            })
228            .collect::<Vec<_>>();
229
230        log_macsec_begin("Q-Chem HDF5 Archive Summary");
231        qsym2_output!("");
232        let path_length = sp_paths
233            .iter()
234            .map(|(path, _)| path.chars().count())
235            .max()
236            .unwrap_or(18)
237            .max(18);
238        let energy_function_length = sp_paths
239            .iter()
240            .map(|(_, energy_function_indices)| {
241                energy_function_indices
242                    .iter()
243                    .map(|index| index.chars().count())
244                    .max()
245                    .unwrap_or(1)
246                    .max(1)
247            })
248            .max()
249            .unwrap_or(7)
250            .max(7);
251        let group_length = result
252            .iter()
253            .map(|(group, _)| group.chars().count())
254            .max()
255            .unwrap_or(5)
256            .max(5);
257        let sym_length = result
258            .iter()
259            .map(|(_, sym)| sym.chars().count())
260            .max()
261            .unwrap_or(13)
262            .max(13);
263        let table_width = path_length + energy_function_length + group_length + sym_length + 8;
264        qsym2_output!("{}", "┈".repeat(table_width));
265        qsym2_output!(
266            " {:<path_length$}  {:<energy_function_length$}  {:<group_length$}  {:<}",
267            "Single-point calc.",
268            "E func.",
269            "Group",
270            "Det. symmetry"
271        );
272        qsym2_output!("{}", "┈".repeat(table_width));
273        sp_paths
274            .iter()
275            .flat_map(|(sp_path, energy_function_indices)| {
276                energy_function_indices
277                    .iter()
278                    .map(|index| (sp_path.clone(), index))
279            })
280            .zip(result.iter())
281            .for_each(|((path, index), (group, sym))| {
282                qsym2_output!(
283                    " {:<path_length$}  {:<energy_function_length$}  {:<group_length$}  {:<#}",
284                    path,
285                    index,
286                    group,
287                    sym
288                );
289            });
290        qsym2_output!("{}", "┈".repeat(table_width));
291        qsym2_output!("");
292        log_macsec_end("Q-Chem HDF5 Archive Summary");
293
294        self.result = Some(result);
295        Ok(())
296    }
297}
298
299impl<'a> QSym2Driver for QChemSlaterDeterminantH5Driver<'a, f64> {
300    type Params = SlaterDeterminantRepAnalysisParams<f64>;
301
302    type Outcome = Vec<(String, String)>;
303
304    fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
305        self.result.as_ref().ok_or(format_err!(
306            "No Q-Chem HDF5 analysis results for a real Slater determinant found."
307        ))
308    }
309
310    fn run(&mut self) -> Result<(), anyhow::Error> {
311        self.analyse()
312    }
313}
314
315// ==================
316// SinglePoint Driver
317// ==================
318
319// ---------------
320// Enum definition
321// ---------------
322
323/// Enumerated type to distinguish different kinds of molecular orbitals.
324pub enum OrbitalType {
325    /// Canonical molecular orbitals as obtained by diagonalising Fock matrices.
326    Canonical,
327
328    /// Localised molecular orbitals as obtained by a localisation method.
329    Localised,
330}
331
332// -----------------
333// Struct definition
334// -----------------
335
336/// Driver to perform symmetry-group detection and representation analysis for a single-point
337/// calculation result in a Q-Chem's `qarchive.h5` file.
338#[derive(Clone, Builder)]
339pub struct QChemSlaterDeterminantH5SinglePointDriver<'a, G, T>
340where
341    G: SymmetryGroupProperties + Clone,
342    G::CharTab: SubspaceDecomposable<T>,
343    T: ComplexFloat + Lapack,
344    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
345{
346    /// A H5 group containing data from a single-point calculation.
347    sp_group: &'a hdf5::Group,
348
349    /// The index of the energy function whose results are to be considered for this single-point
350    /// symmetry analysis.
351    #[builder(setter(custom))]
352    energy_function_index: String,
353
354    /// The parameters controlling symmetry-group detection.
355    symmetry_group_detection_input: &'a SymmetryGroupDetectionInputKind,
356
357    /// The parameters controlling representation analysis of standard angular functions.
358    angular_function_analysis_parameters: &'a AngularFunctionRepAnalysisParams,
359
360    /// The parameters controlling representation analysis of Slater determinants.
361    rep_analysis_parameters: &'a SlaterDeterminantRepAnalysisParams<f64>,
362
363    /// The symmetry of the system and the representation of the Slater determinant.
364    #[allow(clippy::type_complexity)]
365    #[builder(default = "None")]
366    result: Option<(
367        Symmetry,
368        Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>,
369    )>,
370}
371
372// ----------------------
373// Struct implementations
374// ----------------------
375
376impl<'a, G, T> QChemSlaterDeterminantH5SinglePointDriverBuilder<'a, G, T>
377where
378    G: SymmetryGroupProperties + Clone,
379    G::CharTab: SubspaceDecomposable<T>,
380    T: ComplexFloat + Lapack,
381    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
382{
383    pub fn energy_function_index(&mut self, idx: &str) -> &mut Self {
384        self.energy_function_index = Some(idx.to_string());
385        self
386    }
387}
388
389impl<'a, G, T> QChemSlaterDeterminantH5SinglePointDriver<'a, G, T>
390where
391    G: SymmetryGroupProperties + Clone,
392    G::CharTab: SubspaceDecomposable<T>,
393    T: ComplexFloat + Lapack + H5Type,
394    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
395{
396    /// Returns a builder to construct a [`QChemSlaterDeterminantH5SinglePointDriver`].
397    pub fn builder() -> QChemSlaterDeterminantH5SinglePointDriverBuilder<'a, G, T> {
398        QChemSlaterDeterminantH5SinglePointDriverBuilder::default()
399    }
400
401    /// Extracts the molecular structure from the single-point H5 group.
402    pub fn extract_molecule(&self) -> Result<Molecule, anyhow::Error> {
403        let emap = ElementMap::new();
404        let coordss = self
405            .sp_group
406            .dataset("structure/coordinates")?
407            .read_2d::<f64>()?;
408        let atomic_numbers = self
409            .sp_group
410            .dataset("structure/nuclei")?
411            .read_1d::<usize>()?;
412        let atoms = coordss
413            .rows()
414            .into_iter()
415            .zip(atomic_numbers.iter())
416            .map(|(coords, atomic_number)| {
417                let element = periodic_table()
418                    .get(*atomic_number - 1)
419                    .ok_or(hdf5::Error::from(
420                        format!(
421                            "Element with atomic number {atomic_number} could not be identified."
422                        )
423                        .as_str(),
424                    ))?
425                    .symbol;
426                let coordinates = Point3::new(coords[0], coords[1], coords[2]);
427                Ok::<_, hdf5::Error>(Atom::new_ordinary(element, coordinates, &emap, 1e-8))
428            })
429            .collect::<Result<Vec<Atom>, _>>()?;
430        let mol = Molecule::from_atoms(&atoms, 1e-14);
431        Ok(mol)
432    }
433
434    /// Extracts the basis angular order information from the single-point H5 group.
435    pub fn extract_bao(&self, mol: &'a Molecule) -> Result<BasisAngularOrder<'a>, anyhow::Error> {
436        let shell_types = self
437            .sp_group
438            .dataset("aobasis/shell_types")?
439            .read_1d::<i32>()?;
440        let shell_to_atom_map = self
441            .sp_group
442            .dataset("aobasis/shell_to_atom_map")?
443            .read_1d::<usize>()?
444            .iter()
445            .zip(shell_types.iter())
446            .flat_map(|(&idx, shell_type)| {
447                if *shell_type == -1 {
448                    vec![idx, idx]
449                } else {
450                    vec![idx]
451                }
452            })
453            .collect::<Vec<_>>();
454
455        let bss: Vec<BasisShell> = shell_types
456            .iter()
457            .flat_map(|shell_type| {
458                if *shell_type == 0 {
459                    // S shell
460                    vec![BasisShell::new(0, ShellOrder::Cart(CartOrder::qchem(0)))]
461                } else if *shell_type == 1 {
462                    // P shell
463                    vec![BasisShell::new(1, ShellOrder::Cart(CartOrder::qchem(1)))]
464                } else if *shell_type == -1 {
465                    // SP shell
466                    vec![
467                        BasisShell::new(0, ShellOrder::Cart(CartOrder::qchem(0))),
468                        BasisShell::new(1, ShellOrder::Cart(CartOrder::qchem(1))),
469                    ]
470                } else if *shell_type < 0 {
471                    // Cartesian D shell or higher
472                    let l = shell_type.unsigned_abs();
473                    vec![BasisShell::new(l, ShellOrder::Cart(CartOrder::qchem(l)))]
474                } else {
475                    // Pure D shell or higher
476                    let l = shell_type.unsigned_abs();
477                    vec![BasisShell::new(
478                        l,
479                        ShellOrder::Pure(PureOrder::increasingm(l)),
480                    )]
481                }
482            })
483            .collect::<Vec<BasisShell>>();
484
485        let batms = mol
486            .atoms
487            .iter()
488            .enumerate()
489            .map(|(atom_i, atom)| {
490                let shells = bss
491                    .iter()
492                    .zip(shell_to_atom_map.iter())
493                    .filter_map(|(bs, atom_index)| {
494                        if *atom_index == atom_i {
495                            Some(bs.clone())
496                        } else {
497                            None
498                        }
499                    })
500                    .collect::<Vec<_>>();
501                BasisAtom::new(atom, &shells)
502            })
503            .collect::<Vec<BasisAtom>>();
504        Ok(BasisAngularOrder::new(&batms))
505    }
506
507    /// Extracts the spatial atomic-orbital overlap matrix from the single-point H5 group.
508    ///
509    /// Note that the overlap matrix in the HDF5 file uses lexicographic order for Cartesian
510    /// functions. This is inconsistent with the conventional Q-Chem ordering used for molecular
511    /// orbital coefficients. See [`Self::recompute_sao`] for a way to get the atomic-orbital
512    /// overlap matrix with the consistent Cartesian ordering.
513    pub fn extract_sao(&self) -> Result<Array2<T>, anyhow::Error> {
514        self.sp_group
515            .dataset("aobasis/overlap_matrix")?
516            .read_2d::<T>()
517            .map_err(|err| err.into())
518    }
519
520    /// Extracts the full basis set information from the single-point H5 group.
521    pub fn extract_basis_set(
522        &self,
523        mol: &'a Molecule,
524    ) -> Result<BasisSet<f64, f64>, anyhow::Error> {
525        let shell_types = self
526            .sp_group
527            .dataset("aobasis/shell_types")?
528            .read_1d::<i32>()?;
529        let shell_to_atom_map = self
530            .sp_group
531            .dataset("aobasis/shell_to_atom_map")?
532            .read_1d::<usize>()?
533            .iter()
534            .zip(shell_types.iter())
535            .flat_map(|(&idx, shell_type)| {
536                if *shell_type == -1 {
537                    vec![idx, idx]
538                } else {
539                    vec![idx]
540                }
541            })
542            .collect::<Vec<_>>();
543
544        let primitives_per_shell = self
545            .sp_group
546            .dataset("aobasis/primitives_per_shell")?
547            .read_1d::<usize>()?;
548        let contraction_coefficients = self
549            .sp_group
550            .dataset("aobasis/contraction_coefficients")?
551            .read_1d::<f64>()?;
552        let sp_contraction_coefficients = self
553            .sp_group
554            .dataset("aobasis/sp_contraction_coefficients")?
555            .read_1d::<f64>()?;
556        let primitive_exponents = self
557            .sp_group
558            .dataset("aobasis/primitive_exponents")?
559            .read_1d::<f64>()?;
560        // `shell_coordinates` in Bohr radius.
561        let shell_coordinates = self
562            .sp_group
563            .dataset("aobasis/shell_coordinates")?
564            .read_2d::<f64>()?;
565
566        let bscs: Vec<BasisShellContraction<f64, f64>> = primitives_per_shell
567            .iter()
568            .scan(0, |end, n_prims| {
569                let start = *end;
570                *end += n_prims;
571                Some((start, *end))
572            })
573            .zip(shell_types.iter())
574            .zip(shell_coordinates.rows())
575            .flat_map(|(((start, end), shell_type), centre)| {
576                if *shell_type == 0 {
577                    // S shell
578                    let basis_shell = BasisShell::new(0, ShellOrder::Cart(CartOrder::qchem(0)));
579                    let primitives = primitive_exponents
580                        .slice(s![start..end])
581                        .iter()
582                        .cloned()
583                        .zip(
584                            contraction_coefficients
585                                .slice(s![start..end])
586                                .iter()
587                                .cloned(),
588                        )
589                        .collect::<Vec<_>>();
590                    let contraction = GaussianContraction { primitives };
591                    let cart_origin = Point3::new(centre[0], centre[1], centre[2]);
592                    vec![BasisShellContraction {
593                        basis_shell,
594                        contraction,
595                        cart_origin,
596                        k: None,
597                    }]
598                } else if *shell_type == 1 {
599                    // P shell
600                    let basis_shell = BasisShell::new(1, ShellOrder::Cart(CartOrder::qchem(1)));
601                    let primitives = primitive_exponents
602                        .slice(s![start..end])
603                        .iter()
604                        .cloned()
605                        .zip(
606                            contraction_coefficients
607                                .slice(s![start..end])
608                                .iter()
609                                .cloned(),
610                        )
611                        .collect::<Vec<_>>();
612                    let contraction = GaussianContraction { primitives };
613                    let cart_origin = Point3::new(centre[0], centre[1], centre[2]);
614                    vec![BasisShellContraction {
615                        basis_shell,
616                        contraction,
617                        cart_origin,
618                        k: None,
619                    }]
620                } else if *shell_type == -1 {
621                    // SP shell
622                    let basis_shell_s = BasisShell::new(0, ShellOrder::Cart(CartOrder::qchem(0)));
623                    let primitives_s = primitive_exponents
624                        .slice(s![start..end])
625                        .iter()
626                        .cloned()
627                        .zip(
628                            contraction_coefficients
629                                .slice(s![start..end])
630                                .iter()
631                                .cloned(),
632                        )
633                        .collect::<Vec<_>>();
634                    let contraction_s = GaussianContraction {
635                        primitives: primitives_s,
636                    };
637
638                    let basis_shell_p = BasisShell::new(1, ShellOrder::Cart(CartOrder::qchem(1)));
639                    let primitives_p = primitive_exponents
640                        .slice(s![start..end])
641                        .iter()
642                        .cloned()
643                        .zip(
644                            sp_contraction_coefficients
645                                .slice(s![start..end])
646                                .iter()
647                                .cloned(),
648                        )
649                        .collect::<Vec<_>>();
650                    let contraction_p = GaussianContraction {
651                        primitives: primitives_p,
652                    };
653
654                    let cart_origin = Point3::new(centre[0], centre[1], centre[2]);
655                    vec![
656                        BasisShellContraction {
657                            basis_shell: basis_shell_s,
658                            contraction: contraction_s,
659                            cart_origin,
660                            k: None,
661                        },
662                        BasisShellContraction {
663                            basis_shell: basis_shell_p,
664                            contraction: contraction_p,
665                            cart_origin,
666                            k: None,
667                        },
668                    ]
669                } else if *shell_type < 0 {
670                    // Cartesian D shell or higher
671                    let l = shell_type.unsigned_abs();
672                    let basis_shell = BasisShell::new(l, ShellOrder::Cart(CartOrder::qchem(l)));
673                    let primitives = primitive_exponents
674                        .slice(s![start..end])
675                        .iter()
676                        .cloned()
677                        .zip(
678                            contraction_coefficients
679                                .slice(s![start..end])
680                                .iter()
681                                .cloned(),
682                        )
683                        .collect::<Vec<_>>();
684                    let contraction = GaussianContraction { primitives };
685                    let cart_origin = Point3::new(centre[0], centre[1], centre[2]);
686                    vec![BasisShellContraction {
687                        basis_shell,
688                        contraction,
689                        cart_origin,
690                        k: None,
691                    }]
692                } else {
693                    // Pure D shell or higher
694                    let l = shell_type.unsigned_abs();
695                    let basis_shell =
696                        BasisShell::new(l, ShellOrder::Pure(PureOrder::increasingm(l)));
697                    let primitives = primitive_exponents
698                        .slice(s![start..end])
699                        .iter()
700                        .cloned()
701                        .zip(
702                            contraction_coefficients
703                                .slice(s![start..end])
704                                .iter()
705                                .cloned(),
706                        )
707                        .collect::<Vec<_>>();
708                    let contraction = GaussianContraction { primitives };
709                    let cart_origin = Point3::new(centre[0], centre[1], centre[2]);
710                    vec![BasisShellContraction {
711                        basis_shell,
712                        contraction,
713                        cart_origin,
714                        k: None,
715                    }]
716                }
717            })
718            .collect::<Vec<BasisShellContraction<f64, f64>>>();
719
720        let basis_atoms = mol
721            .atoms
722            .iter()
723            .enumerate()
724            .map(|(atom_i, _)| {
725                bscs
726                    .iter()
727                    .zip(shell_to_atom_map.iter())
728                    .filter_map(|(bs, atom_index)| {
729                        if *atom_index == atom_i {
730                            Some(bs.clone())
731                        } else {
732                            None
733                        }
734                    })
735                    .collect::<Vec<_>>()
736            })
737            .collect::<Vec<Vec<BasisShellContraction<f64, f64>>>>();
738
739        let mut basis_set = BasisSet::<f64, f64>::new(basis_atoms);
740
741        // Q-Chem renormalises each Gaussian primitive, but this is not the convention used in
742        // QSym². We therefore un-normalise the Gaussian primitives to restore the original
743        // contraction coefficients.
744        let prefactor = (2.0 / std::f64::consts::PI).powf(0.75);
745        basis_set.all_shells_mut().for_each(|bsc| {
746            let l = bsc.basis_shell().l;
747            let l_i32 = l
748                .to_i32()
749                .unwrap_or_else(|| panic!("Unable to convert `{l}` to `i32`."));
750            let l_f64 = l
751                .to_f64()
752                .unwrap_or_else(|| panic!("Unable to convert `{l}` to `f64`."));
753            let doufac_sqrt = if l == 0 {
754                1
755            } else {
756                ((2 * l) - 1)
757                    .checked_double_factorial()
758                    .unwrap_or_else(|| panic!("Unable to obtain `{}!!`.", 2 * l - 1))
759            }
760            .to_f64()
761            .unwrap_or_else(|| panic!("Unable to convert `{}!!` to `f64`.", 2 * l - 1))
762            .sqrt();
763            bsc.contraction.primitives.iter_mut().for_each(|(a, c)| {
764                let n = prefactor * 2.0.powi(l_i32) * a.powf(l_f64 / 2.0 + 0.75) / doufac_sqrt;
765                *c /= n;
766            });
767        });
768        Ok(basis_set)
769    }
770
771    /// Recomputes the spatial atomic-orbital overlap matrix.
772    ///
773    /// The overlap matrix stored in the H5 group unfortunately uses lexicographic order for
774    /// Cartesian functions, which is inconsistent with that used in the coefficients. We thus
775    /// recompute the overlap matrix from the basis set information using the conventional Q-Chem
776    /// order for Cartesian functions.
777    pub fn recompute_sao(&self) -> Result<Array2<f64>, anyhow::Error> {
778        log::debug!("Recomputing atomic-orbital overlap matrix...");
779        let mol = self.extract_molecule()?;
780        let basis_set = self.extract_basis_set(&mol)?;
781        let stc = build_shell_tuple_collection![
782            <s1, s2>;
783            false, false;
784            &basis_set, &basis_set;
785            f64
786        ];
787        let sao_res = stc
788            .overlap([0, 0])
789            .pop()
790            .ok_or(format_err!("Unable to compute the AO overlap matrix."));
791        log::debug!("Recomputing atomic-orbital overlap matrix... Done.");
792        sao_res
793    }
794}
795
796// ~~~~~~~~~~~~~~~~~~~
797// Slater determinants
798// ~~~~~~~~~~~~~~~~~~~
799
800// Generic for all symmetry groups G and determinant numeric type T
801// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
802impl<'a, G, T> QChemSlaterDeterminantH5SinglePointDriver<'a, G, T>
803where
804    G: SymmetryGroupProperties + Clone,
805    G::CharTab: SubspaceDecomposable<T>,
806    T: ComplexFloat + Lapack + H5Type,
807    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
808{
809    /// Extracts the Slater determinant from the single-point H5 group.
810    ///
811    /// # Arguments
812    ///
813    /// * `mol` - The molecule to be associated with the extracted determinant.
814    /// * `bao` - The basis angular order information to be associated with the extracted determinant.
815    /// * `threshold` - The comparison threshold to be associated with the extracted determinant.
816    ///
817    /// # Returns
818    ///
819    /// The extracted Slater determinant.
820    pub fn extract_determinant(
821        &self,
822        mol: &'a Molecule,
823        bao: &'a BasisAngularOrder,
824        threshold: <T as ComplexFloat>::Real,
825        orbital_type: OrbitalType,
826    ) -> Result<SlaterDeterminant<'a, T, SpinConstraint>, anyhow::Error> {
827        let energy = self
828            .sp_group
829            .dataset(&format!(
830                "energy_function/{}/energy",
831                self.energy_function_index
832            ))?
833            .read_scalar::<T>()
834            .map_err(|err| err.to_string());
835        let orbital_path = match orbital_type {
836            OrbitalType::Canonical => format!(
837                "energy_function/{}/method/scf/molecular_orbitals",
838                self.energy_function_index
839            ),
840            OrbitalType::Localised => format!(
841                "energy_function/{}/analysis/localized_orbitals/{}/molecular_orbitals",
842                self.energy_function_index, self.energy_function_index
843            ),
844        };
845        let nspins = self
846            .sp_group
847            .dataset(&format!("{orbital_path}/nsets"))?
848            .read_scalar::<usize>()?;
849        let nmo = self
850            .sp_group
851            .dataset(&format!("{orbital_path}/norb",))?
852            .read_scalar::<usize>()?;
853        let (spincons, occs) = match nspins {
854            1 => {
855                log::warn!(
856                    "The number of spin spaces detected is 1. \
857                    It will be assumed that this implies an RHF calculation. \
858                    However, it must be noted that, if the calculation is GHF instead, then the \
859                    following symmetry analysis will be wrong, because Q-Chem does not archive \
860                    GHF MO coefficients correctly."
861                );
862                let nalpha = self
863                    .sp_group
864                    .dataset("structure/nalpha")?
865                    .read_scalar::<usize>()?;
866                let occ_a = Array1::from_vec(
867                    (0..nmo)
868                        .map(|i| {
869                            if i < nalpha {
870                                <T as ComplexFloat>::Real::one()
871                            } else {
872                                <T as ComplexFloat>::Real::zero()
873                            }
874                        })
875                        .collect::<Vec<_>>(),
876                );
877                (SpinConstraint::Restricted(2), vec![occ_a])
878            }
879            2 => {
880                let nalpha = self
881                    .sp_group
882                    .dataset("structure/nalpha")?
883                    .read_scalar::<usize>()?;
884                let nbeta = self
885                    .sp_group
886                    .dataset("structure/nbeta")?
887                    .read_scalar::<usize>()?;
888                let occ_a = Array1::from_vec(
889                    (0..nmo)
890                        .map(|i| {
891                            if i < nalpha {
892                                <T as ComplexFloat>::Real::one()
893                            } else {
894                                <T as ComplexFloat>::Real::zero()
895                            }
896                        })
897                        .collect::<Vec<_>>(),
898                );
899                let occ_b = Array1::from_vec(
900                    (0..nmo)
901                        .map(|i| {
902                            if i < nbeta {
903                                <T as ComplexFloat>::Real::one()
904                            } else {
905                                <T as ComplexFloat>::Real::zero()
906                            }
907                        })
908                        .collect::<Vec<_>>(),
909                );
910                (SpinConstraint::Unrestricted(2, true), vec![occ_a, occ_b])
911            }
912            _ => {
913                bail!("Unexpected number of spin spaces from Q-Chem.")
914            }
915        };
916        let cs = self
917            .sp_group
918            .dataset(&format!("{orbital_path}/mo_coefficients"))?
919            .read::<T, Ix3>()?
920            .axis_iter(Axis(0))
921            .map(|c| c.to_owned())
922            .collect::<Vec<_>>();
923        let mo_energies = self
924            .sp_group
925            .dataset(&format!("{orbital_path}/mo_energies"))
926            .and_then(|mo_energies_dataset| {
927                mo_energies_dataset.read_2d::<T>().map(|mo_energies_arr| {
928                    mo_energies_arr
929                        .columns()
930                        .into_iter()
931                        .map(|c| c.to_owned())
932                        .collect::<Vec<_>>()
933                })
934            })
935            .ok();
936
937        let ncomps = spincons.n_explicit_comps_per_coefficient_matrix();
938        SlaterDeterminant::builder()
939            .structure_constraint(spincons)
940            .baos((0..ncomps).map(|_| bao).collect::<Vec<_>>())
941            .complex_symmetric(false)
942            .mol(mol)
943            .coefficients(&cs)
944            .occupations(&occs)
945            .mo_energies(mo_energies)
946            .energy(energy)
947            .threshold(threshold)
948            .build()
949            .map_err(|err| err.into())
950    }
951}
952
953// Specific for unitary-represented and magnetic-represented symmetry groups and determinant numeric type f64
954// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
955#[duplicate_item(
956    [
957        gtype_ [ UnitaryRepresentedSymmetryGroup ]
958        doc_ [ "Performs symmetry-group detection and unitary-represented representation analysis." ]
959    ]
960    [
961        gtype_ [ MagneticRepresentedSymmetryGroup ]
962        doc_ [ "Performs symmetry-group detection and magnetic-represented corepresentation analysis." ]
963    ]
964)]
965impl<'a> QChemSlaterDeterminantH5SinglePointDriver<'a, gtype_, f64> {
966    #[doc = doc_]
967    pub fn analyse(&mut self) -> Result<(), anyhow::Error> {
968        let mol = self.extract_molecule()
969            .with_context(|| "Unable to extract the molecule from the HDF5 file while performing symmetry analysis for a single-point Q-Chem calculation")?;
970        log::debug!("Performing symmetry-group detection...");
971        let pd_res = match self.symmetry_group_detection_input {
972            SymmetryGroupDetectionInputKind::Parameters(pd_params) => {
973                let mut pd_driver = SymmetryGroupDetectionDriver::builder()
974                    .parameters(pd_params)
975                    .molecule(Some(&mol))
976                    .build()
977                    .unwrap();
978                let pd_run = pd_driver.run();
979                if let Err(err) = pd_run {
980                    qsym2_error!("Symmetry-group detection has failed with error:");
981                    qsym2_error!("  {err:#}");
982                }
983                let pd_res = pd_driver.result()?;
984                pd_res.clone()
985            }
986            SymmetryGroupDetectionInputKind::FromFile(path) => {
987                read_qsym2_binary::<SymmetryGroupDetectionResult, _>(path, QSym2FileType::Sym)
988                    .with_context(|| "Unable to read the specified .qsym2.sym file while performing symmetry analysis for a single-point Q-Chem calculation")?
989            }
990        };
991        let recentred_mol = &pd_res.pre_symmetry.recentred_molecule;
992        let sym = if self.rep_analysis_parameters.use_magnetic_group.is_some() {
993            pd_res.magnetic_symmetry.clone()
994        } else {
995            Some(pd_res.unitary_symmetry.clone())
996        }
997        .ok_or(format_err!("Symmetry not found."))?;
998        log::debug!("Performing symmetry-group detection... Done.");
999
1000        let rep = || {
1001            log::debug!("Extracting AO basis information for representation analysis...");
1002            let sao = self.recompute_sao()
1003                .with_context(|| "Unable to extract the SAO matrix from the HDF5 file while performing symmetry analysis for a single-point Q-Chem calculation")
1004                .map_err(|err| err.to_string())?;
1005            let bao = self.extract_bao(recentred_mol)
1006                .with_context(|| "Unable to extract the basis angular order information from the HDF5 file while performing symmetry analysis for a single-point Q-Chem calculation")
1007                .map_err(|err| err.to_string())?;
1008            let basis_set_opt = if self.rep_analysis_parameters.analyse_density_symmetries {
1009                self.extract_basis_set(recentred_mol).ok()
1010            } else {
1011                None
1012            };
1013            log::debug!("Extracting AO basis information for representation analysis... Done.");
1014
1015            let sao_4c: Option<Array4<f64>> = basis_set_opt.map(|basis_set| {
1016                log::debug!(
1017                    "Computing four-centre overlap integrals for density symmetry analysis..."
1018                );
1019                let stc = build_shell_tuple_collection![
1020                    <s1, s2, s3, s4>;
1021                    false, false, false, false;
1022                    &basis_set, &basis_set, &basis_set, &basis_set;
1023                    f64
1024                ];
1025                let sao_4c = stc
1026                    .overlap([0, 0, 0, 0])
1027                    .pop()
1028                    .expect("Unable to retrieve the four-centre overlap tensor.");
1029                log::debug!(
1030                    "Computing four-centre overlap integrals for density symmetry analysis... Done."
1031                );
1032                sao_4c
1033            });
1034
1035            log::debug!(
1036                "Extracting canonical determinant information for representation analysis..."
1037            );
1038            let det = self.extract_determinant(
1039                recentred_mol,
1040                &bao,
1041                self.rep_analysis_parameters
1042                    .linear_independence_threshold,
1043                OrbitalType::Canonical,
1044            )
1045            .with_context(|| "Unable to extract the determinant from the HDF5 file while performing symmetry analysis for a single-point Q-Chem calculation")
1046            .map_err(|err| err.to_string())?;
1047            log::debug!(
1048                "Extracting canonical determinant information for representation analysis... Done."
1049            );
1050
1051            log::debug!("Running representation analysis on canonical determinant...");
1052            let mut sda_driver =
1053                SlaterDeterminantRepAnalysisDriver::<gtype_, f64, SpinConstraint>::builder()
1054                    .parameters(self.rep_analysis_parameters)
1055                    .angular_function_parameters(self.angular_function_analysis_parameters)
1056                    .determinant(&det)
1057                    .sao(&sao)
1058                    .sao_spatial_4c(sao_4c.as_ref())
1059                    .symmetry_group(&pd_res)
1060                    .build()
1061                    .with_context(|| "Unable to construct a Slater determinant representation analysis driver while performing symmetry analysis for a single-point Q-Chem calculation")
1062                    .map_err(|err| err.to_string())?;
1063            log_micsec_begin("Canonical orbital representation analysis");
1064            let sda_run = sda_driver.run();
1065            log_micsec_end("Canonical orbital representation analysis");
1066            qsym2_output!("");
1067            log::debug!("Running representation analysis on canonical determinant... Done.");
1068            if let Err(err) = sda_run {
1069                qsym2_error!("Representation analysis has failed with error:");
1070                qsym2_error!("  {err:#}");
1071            }
1072
1073            let _ = self
1074                .extract_determinant(
1075                    recentred_mol,
1076                    &bao,
1077                    self.rep_analysis_parameters.linear_independence_threshold,
1078                    OrbitalType::Localised,
1079                )
1080                .and_then(|loc_det| {
1081                    log::debug!("Running representation analysis on localised determinant...");
1082                    let mut loc_sda_driver = SlaterDeterminantRepAnalysisDriver::<
1083                        UnitaryRepresentedSymmetryGroup,
1084                        f64,
1085                        SpinConstraint,
1086                    >::builder()
1087                    .parameters(self.rep_analysis_parameters)
1088                    .angular_function_parameters(self.angular_function_analysis_parameters)
1089                    .determinant(&loc_det)
1090                    .sao(&sao)
1091                    .sao_spatial_4c(sao_4c.as_ref())
1092                    .symmetry_group(&pd_res)
1093                    .build()?;
1094                    log_micsec_begin("Localised orbital representation analysis");
1095                    let res = loc_sda_driver.run();
1096                    log_micsec_end("Localised orbital representation analysis");
1097                    qsym2_output!("");
1098                    log::debug!(
1099                        "Running representation analysis on localised determinant... Done."
1100                    );
1101                    res
1102                });
1103
1104            sda_driver
1105                .result()
1106                .map_err(|err| err.to_string())
1107                .and_then(|sda_res| sda_res.determinant_symmetry().clone())
1108        };
1109        self.result = Some((sym, rep()));
1110        Ok(())
1111    }
1112}
1113
1114// ---------------------
1115// Trait implementations
1116// ---------------------
1117
1118// ~~~~~~~~~~~~~~~~~~~
1119// Slater determinants
1120// ~~~~~~~~~~~~~~~~~~~
1121
1122// Specific for unitary-represented and magnetic-represented symmetry groups and determinant numeric type f64
1123// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
1124#[duplicate_item(
1125    [
1126        gtype_ [ UnitaryRepresentedSymmetryGroup ]
1127        err_ [ "No Q-Chem single-point analysis results (unitary-represented group, real determinant) found." ]
1128    ]
1129    [
1130        gtype_ [ MagneticRepresentedSymmetryGroup ]
1131        err_ [ "No Q-Chem single-point analysis results (magnetic-represented group, real determinant) found." ]
1132    ]
1133)]
1134impl<'a> QSym2Driver for QChemSlaterDeterminantH5SinglePointDriver<'a, gtype_, f64> {
1135    type Params = SlaterDeterminantRepAnalysisParams<f64>;
1136
1137    type Outcome = (
1138        Symmetry,
1139        Result<
1140            <<gtype_ as CharacterProperties>::CharTab as SubspaceDecomposable<f64>>::Decomposition,
1141            String,
1142        >,
1143    );
1144
1145    fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
1146        self.result.as_ref().ok_or(format_err!(err_))
1147    }
1148
1149    fn run(&mut self) -> Result<(), anyhow::Error> {
1150        self.analyse()
1151    }
1152}