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