qsym2/interfaces/qchem/hdf5/
vibrational_coordinate.rs

1//! Vibrational coordinates from Q-Chem HDF5 archives.
2
3use std::fmt;
4use std::marker::PhantomData;
5use std::path::PathBuf;
6
7use anyhow::{self, format_err, Context};
8use derive_builder::Builder;
9use duplicate::duplicate_item;
10use hdf5::{self, H5Type};
11use lazy_static::lazy_static;
12use log;
13use nalgebra::Point3;
14use ndarray::{Array2, Axis, Ix3, ShapeBuilder};
15use ndarray_linalg::types::Lapack;
16use num_complex::ComplexFloat;
17use numeric_sort;
18use periodic_table::periodic_table;
19use regex::Regex;
20
21use crate::auxiliary::atom::{Atom, ElementMap};
22use crate::auxiliary::molecule::Molecule;
23use crate::chartab::SubspaceDecomposable;
24use crate::drivers::representation_analysis::angular_function::AngularFunctionRepAnalysisParams;
25use crate::drivers::representation_analysis::vibrational_coordinate::{
26    VibrationalCoordinateRepAnalysisDriver, VibrationalCoordinateRepAnalysisParams,
27};
28use crate::drivers::representation_analysis::MagneticSymmetryAnalysisKind;
29use crate::drivers::symmetry_group_detection::{
30    SymmetryGroupDetectionDriver, SymmetryGroupDetectionResult,
31};
32use crate::drivers::QSym2Driver;
33use crate::interfaces::input::SymmetryGroupDetectionInputKind;
34use crate::io::format::{log_macsec_begin, log_macsec_end, qsym2_error, qsym2_output};
35use crate::io::{read_qsym2_binary, QSym2FileType};
36use crate::symmetry::symmetry_core::Symmetry;
37use crate::symmetry::symmetry_group::{
38    MagneticRepresentedSymmetryGroup, SymmetryGroupProperties, UnitaryRepresentedSymmetryGroup,
39};
40use crate::target::vibration::VibrationalCoordinateCollection;
41
42#[cfg(test)]
43#[path = "vibrational_coordinate_tests.rs"]
44mod vibrational_coordinate_tests;
45
46// =====================
47// Full Q-Chem H5 Driver
48// =====================
49
50lazy_static! {
51    static ref VIB_PATH_RE: Regex = Regex::new(
52        r"(?<sp_path>.*)energy_function\\(?<energy_function>.*)\\analysis\\vibrational$"
53    )
54    .expect("Regex pattern invalid.");
55}
56
57// -----------------
58// Struct definition
59// -----------------
60
61/// Driver to perform symmetry-group detection and vibration representation symmetry analysis for
62/// all discoverable single-point calculation data stored in a Q-Chem's `qarchive.h5` file.
63#[derive(Clone, Builder)]
64pub(crate) struct QChemVibrationH5Driver<'a, T>
65where
66    T: Clone,
67{
68    /// The `qarchive.h5` file name.
69    filename: PathBuf,
70
71    /// The input specification controlling symmetry-group detection.
72    symmetry_group_detection_input: &'a SymmetryGroupDetectionInputKind,
73
74    /// The parameters controlling representation analysis of standard angular functions.
75    angular_function_analysis_parameters: &'a AngularFunctionRepAnalysisParams,
76
77    /// The parameters controlling representation analysis.
78    rep_analysis_parameters: &'a VibrationalCoordinateRepAnalysisParams<f64>,
79
80    /// The simplified result of the analysis. Each element in the vector is a tuple containing the
81    /// group name and a message indicating if the analysis has been successful.
82    #[builder(default = "None")]
83    result: Option<Vec<(String, String)>>,
84
85    /// The numerical type of the vibrational coordinates.
86    #[builder(setter(skip), default = "PhantomData")]
87    numerical_type: PhantomData<T>,
88}
89
90// ----------------------
91// Struct implementations
92// ----------------------
93
94impl<'a, T> QChemVibrationH5Driver<'a, T>
95where
96    T: Clone
97{
98    /// Returns a builder to construct a [`QChemVibrationH5Driver`].
99    pub(crate) fn builder() -> QChemVibrationH5DriverBuilder<'a, T> {
100        QChemVibrationH5DriverBuilder::default()
101    }
102}
103
104// Specific for vibrational coordinates numeric type f64
105// '''''''''''''''''''''''''''''''''''''''''''''''''''''
106impl<'a> QChemVibrationH5Driver<'a, f64> {
107    /// Performs analysis for all real-valued single-point vibrational coordinates.
108    fn analyse(&mut self) -> Result<(), anyhow::Error> {
109        let f = hdf5::File::open(&self.filename)?;
110        let mut sp_paths = f
111            .group(".counters")?
112            .member_names()?
113            .iter()
114            .filter_map(|path| {
115                if let Some(caps) = VIB_PATH_RE.captures(path) {
116                    let sp_path = caps["sp_path"].replace("\\", "/");
117                    let energy_function_index = caps["energy_function"].to_string();
118                    Some((sp_path, energy_function_index))
119                } else {
120                    None
121                }
122            })
123            .collect::<Vec<_>>();
124        sp_paths.sort_by(|(path_a, _), (path_b, _)| numeric_sort::cmp(path_a, path_b));
125
126        let pd_input = self.symmetry_group_detection_input;
127        let afa_params = self.angular_function_analysis_parameters;
128        let vca_params = self.rep_analysis_parameters;
129        let result = sp_paths
130            .iter()
131            .map(|(sp_path, energy_function_index)| {
132                log_macsec_begin(&format!(
133                    "Analysis for {} (energy function {energy_function_index})",
134                    sp_path.clone()
135                ));
136                qsym2_output!("");
137                let sp = f.group(sp_path)?;
138                let sp_driver_result = match vca_params.use_magnetic_group {
139                    Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
140                        let mut sp_driver = QChemVibrationH5SinglePointDriver::<
141                            MagneticRepresentedSymmetryGroup,
142                            f64,
143                        >::builder()
144                        .sp_group(&sp)
145                        .energy_function_index(energy_function_index)
146                        .symmetry_group_detection_input(pd_input)
147                        .angular_function_analysis_parameters(afa_params)
148                        .rep_analysis_parameters(vca_params)
149                        .build()?;
150                        let _ = sp_driver.run();
151                        sp_driver.result().map(|(sym, vca_res)| {
152                            (
153                                sym.group_name
154                                    .as_ref()
155                                    .unwrap_or(&String::new())
156                                    .to_string(),
157                                vca_res
158                                    .as_ref()
159                                    .map(|_| "Ok".to_string())
160                                    .unwrap_or_else(|err| err.to_string()),
161                            )
162                        })
163                    }
164                    Some(MagneticSymmetryAnalysisKind::Representation) | None => {
165                        let mut sp_driver = QChemVibrationH5SinglePointDriver::<
166                            UnitaryRepresentedSymmetryGroup,
167                            f64,
168                        >::builder()
169                        .sp_group(&sp)
170                        .energy_function_index(energy_function_index)
171                        .symmetry_group_detection_input(pd_input)
172                        .angular_function_analysis_parameters(afa_params)
173                        .rep_analysis_parameters(vca_params)
174                        .build()?;
175                        let _ = sp_driver.run();
176                        sp_driver.result().map(|(sym, vca_res)| {
177                            (
178                                sym.group_name
179                                    .as_ref()
180                                    .unwrap_or(&String::new())
181                                    .to_string(),
182                                vca_res
183                                    .as_ref()
184                                    .map(|_| "Ok".to_string())
185                                    .unwrap_or_else(|err| err.to_string()),
186                            )
187                        })
188                    }
189                };
190                qsym2_output!("");
191                log_macsec_end(&format!(
192                    "Analysis for {} (energy function {energy_function_index})",
193                    sp_path.clone()
194                ));
195                qsym2_output!("");
196                sp_driver_result
197            })
198            .map(|res| {
199                res.unwrap_or_else(|err| {
200                    (
201                        "Unidentified symmetry group".to_string(),
202                        format!("Unidentified (co)representations: {err}"),
203                    )
204                })
205            })
206            .collect::<Vec<_>>();
207
208        log_macsec_begin("Q-Chem HDF5 Archive Summary");
209        qsym2_output!("");
210        let path_length = sp_paths
211            .iter()
212            .map(|(path, _)| path.chars().count())
213            .max()
214            .unwrap_or(18)
215            .max(18);
216        let energy_function_length = sp_paths
217            .iter()
218            .map(|(_, energy_function_index)| energy_function_index.chars().count().max(1))
219            .max()
220            .unwrap_or(7)
221            .max(7);
222        let group_length = result
223            .iter()
224            .map(|(group, _)| group.chars().count())
225            .max()
226            .unwrap_or(5)
227            .max(5);
228        let sym_length = result
229            .iter()
230            .map(|(_, sym)| sym.chars().count())
231            .max()
232            .unwrap_or(20)
233            .max(20);
234        let table_width = path_length + energy_function_length + group_length + sym_length + 8;
235        qsym2_output!("{}", "┈".repeat(table_width));
236        qsym2_output!(
237            " {:<path_length$}  {:<energy_function_length$}  {:<group_length$}  {:<}",
238            "Single-point calc.",
239            "E func.",
240            "Group",
241            "Vib. symmetry status"
242        );
243        qsym2_output!("{}", "┈".repeat(table_width));
244        sp_paths
245            .iter()
246            .map(|(sp_path, energy_function_index)| (sp_path.clone(), energy_function_index))
247            .zip(result.iter())
248            .for_each(|((path, index), (group, sym))| {
249                qsym2_output!(
250                    " {:<path_length$}  {:<energy_function_length$}  {:<group_length$}  {:<#}",
251                    path,
252                    index,
253                    group,
254                    sym
255                );
256            });
257        qsym2_output!("{}", "┈".repeat(table_width));
258        qsym2_output!("");
259        log_macsec_end("Q-Chem HDF5 Archive Summary");
260
261        self.result = Some(result);
262        Ok(())
263    }
264}
265
266impl<'a> QSym2Driver for QChemVibrationH5Driver<'a, f64> {
267    type Params = VibrationalCoordinateRepAnalysisParams<f64>;
268
269    type Outcome = Vec<(String, String)>;
270
271    fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
272        self.result.as_ref().ok_or(format_err!(
273            "No Q-Chem HDF5 analysis results for real vibrational coordinates found."
274        ))
275    }
276
277    fn run(&mut self) -> Result<(), anyhow::Error> {
278        self.analyse()
279    }
280}
281
282// ==================
283// SinglePoint Driver
284// ==================
285
286// -----------------
287// Struct definition
288// -----------------
289
290/// Driver to perform symmetry-group detection and vibration representation analysis for a
291/// single-point calculation result in a Q-Chem's `qarchive.h5` file.
292#[derive(Clone, Builder)]
293struct QChemVibrationH5SinglePointDriver<'a, G, T>
294where
295    G: SymmetryGroupProperties + Clone,
296    G::CharTab: SubspaceDecomposable<T>,
297    T: ComplexFloat + Lapack,
298    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
299{
300    /// A H5 group containing data from a single-point calculation.
301    sp_group: &'a hdf5::Group,
302
303    /// The index of the energy function whose results are to be considered for this single-point
304    /// symmetry analysis.
305    #[builder(setter(custom))]
306    energy_function_index: String,
307
308    /// The parameters controlling symmetry-group detection.
309    symmetry_group_detection_input: &'a SymmetryGroupDetectionInputKind,
310
311    /// The parameters controlling representation analysis of standard angular functions.
312    angular_function_analysis_parameters: &'a AngularFunctionRepAnalysisParams,
313
314    /// The parameters controlling the representation analysis of vibrational coordinates.
315    rep_analysis_parameters: &'a VibrationalCoordinateRepAnalysisParams<f64>,
316
317    #[builder(setter(skip), default = "PhantomData")]
318    group_type: PhantomData<G>,
319
320    #[builder(setter(skip), default = "PhantomData")]
321    numerical_type: PhantomData<T>,
322
323    /// The symmetry of the system.
324    #[builder(default = "None")]
325    result: Option<(Symmetry, Result<Vec<String>, String>)>,
326}
327
328// ----------------------
329// Struct implementations
330// ----------------------
331
332impl<'a, G, T> QChemVibrationH5SinglePointDriverBuilder<'a, G, T>
333where
334    G: SymmetryGroupProperties + Clone,
335    G::CharTab: SubspaceDecomposable<T>,
336    T: ComplexFloat + Lapack,
337    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
338{
339    fn energy_function_index(&mut self, idx: &str) -> &mut Self {
340        self.energy_function_index = Some(idx.to_string());
341        self
342    }
343}
344
345impl<'a, G, T> QChemVibrationH5SinglePointDriver<'a, G, T>
346where
347    G: SymmetryGroupProperties + Clone,
348    G::CharTab: SubspaceDecomposable<T>,
349    T: ComplexFloat + Lapack + H5Type,
350    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
351{
352    /// Returns a builder to construct a [`QChemVibrationH5SinglePointDriver`].
353    fn builder() -> QChemVibrationH5SinglePointDriverBuilder<'a, G, T> {
354        QChemVibrationH5SinglePointDriverBuilder::default()
355    }
356
357    /// Extracts the molecular structure from the single-point H5 group.
358    fn extract_molecule(&self) -> Result<Molecule, anyhow::Error> {
359        let emap = ElementMap::new();
360        let coordss = self
361            .sp_group
362            .dataset("structure/coordinates")?
363            .read_2d::<f64>()?;
364        let atomic_numbers = self
365            .sp_group
366            .dataset("structure/nuclei")?
367            .read_1d::<usize>()?;
368        let atoms = coordss
369            .rows()
370            .into_iter()
371            .zip(atomic_numbers.iter())
372            .map(|(coords, atomic_number)| {
373                let element = periodic_table()
374                    .get(*atomic_number - 1)
375                    .ok_or(hdf5::Error::from(
376                        format!(
377                            "Element with atomic number {atomic_number} could not be identified."
378                        )
379                        .as_str(),
380                    ))?
381                    .symbol;
382                let coordinates = Point3::new(coords[0], coords[1], coords[2]);
383                Ok::<_, hdf5::Error>(Atom::new_ordinary(element, coordinates, &emap, 1e-8))
384            })
385            .collect::<Result<Vec<Atom>, _>>()?;
386        let mol = Molecule::from_atoms(&atoms, 1e-14);
387        Ok(mol)
388    }
389}
390
391// Generic for all symmetry groups G and vibrational coordinates numeric type T
392// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
393impl<'a, G, T> QChemVibrationH5SinglePointDriver<'a, G, T>
394where
395    G: SymmetryGroupProperties + Clone,
396    G::CharTab: SubspaceDecomposable<T>,
397    T: ComplexFloat + Lapack + H5Type,
398    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
399{
400    /// Extracts the vibrational coordinate collection from the single-point H5 group.
401    ///
402    /// # Arguments
403    ///
404    /// * `mol` - The molecule to be associated with the extracted vibrational coordinate
405    /// collection.
406    /// * `threshold` - The comparison threshold to be associated with the extracted vibrational
407    /// coordinate collection.
408    ///
409    /// # Returns
410    ///
411    /// The extracted vibrational coordinate collection.
412    fn extract_vibrational_coordinate_collection(
413        &self,
414        mol: &'a Molecule,
415        threshold: <T as ComplexFloat>::Real,
416    ) -> Result<VibrationalCoordinateCollection<'a, T>, anyhow::Error> {
417        let frequencies = self
418            .sp_group
419            .dataset(&format!(
420                "energy_function/{}/analysis/vibrational/1/frequencies",
421                self.energy_function_index
422            ))?
423            .read_1d::<T>()
424            .map_err(|err| format_err!(err))?;
425        let natoms = self
426            .sp_group
427            .dataset(&format!(
428                "energy_function/{}/analysis/vibrational/1/natoms",
429                self.energy_function_index
430            ))?
431            .read_scalar::<usize>()
432            .map_err(|err| format_err!(err))?;
433        let nmodes = self
434            .sp_group
435            .dataset(&format!(
436                "energy_function/{}/analysis/vibrational/1/nmodes",
437                self.energy_function_index
438            ))?
439            .read_scalar::<usize>()
440            .map_err(|err| format_err!(err))?;
441        let coefficients = Array2::from_shape_vec(
442            (3 * natoms, nmodes).f(),
443            self.sp_group
444                .dataset(&format!(
445                    "energy_function/{}/analysis/vibrational/1/modes",
446                    self.energy_function_index
447                ))?
448                .read::<T, Ix3>()?
449                .axis_iter(Axis(0))
450                .flatten()
451                .cloned()
452                .collect::<Vec<_>>(),
453        )
454        .map_err(|err| format_err!(err))?;
455
456        VibrationalCoordinateCollection::builder()
457            .mol(mol)
458            .frequencies(frequencies)
459            .coefficients(coefficients)
460            .threshold(threshold)
461            .build()
462            .map_err(|err| err.into())
463    }
464}
465
466// Specific for unitary-represented and magnetic-represented symmetry groups and determinant numeric type f64
467// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
468#[duplicate_item(
469    [
470        gtype_ [ UnitaryRepresentedSymmetryGroup ]
471        doc_ [ "Performs symmetry-group detection and unitary-represented representation analysis." ]
472    ]
473    [
474        gtype_ [ MagneticRepresentedSymmetryGroup ]
475        doc_ [ "Performs symmetry-group detection and magnetic-represented corepresentation analysis." ]
476    ]
477)]
478impl<'a> QChemVibrationH5SinglePointDriver<'a, gtype_, f64> {
479    #[doc = doc_]
480    fn analyse(&mut self) -> Result<(), anyhow::Error> {
481        let mol = self.extract_molecule()
482            .with_context(|| "Unable to extract the molecule from the HDF5 file while performing symmetry analysis for a single-point Q-Chem calculation")?;
483        log::debug!("Performing symmetry-group detection...");
484        let pd_res = match self.symmetry_group_detection_input {
485            SymmetryGroupDetectionInputKind::Parameters(pd_params) => {
486                let mut pd_driver = SymmetryGroupDetectionDriver::builder()
487                    .parameters(pd_params)
488                    .molecule(Some(&mol))
489                    .build()?;
490                let pd_run = pd_driver.run();
491                if let Err(err) = pd_run {
492                    qsym2_error!("Symmetry-group detection has failed with error:");
493                    qsym2_error!("  {err:#}");
494                }
495                let pd_res = pd_driver.result()?;
496                pd_res.clone()
497            }
498            SymmetryGroupDetectionInputKind::FromFile(path) => {
499                read_qsym2_binary::<SymmetryGroupDetectionResult, _>(path, QSym2FileType::Sym)
500                    .with_context(|| "Unable to read the specified .qsym2.sym file while performing symmetry analysis for a single-point Q-Chem calculation")?
501            }
502        };
503        let recentred_mol = &pd_res.pre_symmetry.recentred_molecule;
504        let sym = if self.rep_analysis_parameters.use_magnetic_group.is_some() {
505            pd_res.magnetic_symmetry.clone()
506        } else {
507            Some(pd_res.unitary_symmetry.clone())
508        }
509        .ok_or(format_err!("Symmetry not found."))?;
510        log::debug!("Performing symmetry-group detection... Done.");
511
512        let rep = || {
513            log::debug!(
514                "Extracting vibrational coordinate information for representation analysis..."
515            );
516            let vibs = self.extract_vibrational_coordinate_collection(
517                recentred_mol,
518                self.rep_analysis_parameters
519                    .linear_independence_threshold,
520            )
521            .with_context(|| "Unable to extract the vibrational coordinate collection from the HDF5 file while performing symmetry analysis for a single-point Q-Chem calculation")
522            .map_err(|err| err.to_string())?;
523            log::debug!(
524                "Extracting vibrational coordinate information for representation analysis... Done."
525            );
526
527            log::debug!("Running representation analysis on vibrational coordinate collection...");
528            let mut vca_driver =
529                VibrationalCoordinateRepAnalysisDriver::<gtype_, f64>::builder()
530                    .parameters(self.rep_analysis_parameters)
531                    .angular_function_parameters(self.angular_function_analysis_parameters)
532                    .vibrational_coordinate_collection(&vibs)
533                    .symmetry_group(&pd_res)
534                    .build()
535                    .with_context(|| "Unable to construct a vibrational coordinate representation analysis driver while performing symmetry analysis for a single-point Q-Chem calculation")
536                    .map_err(|err| err.to_string())?;
537            let vca_run = vca_driver.run();
538            qsym2_output!("");
539            log::debug!(
540                "Running representation analysis on vibrational coordinate collection... Done."
541            );
542            if let Err(err) = vca_run {
543                qsym2_error!("Representation analysis has failed with error:");
544                qsym2_error!("  {err:#}");
545            }
546
547            vca_driver
548                .result()
549                .map(|vca_res| {
550                    vca_res
551                        .vibrational_coordinate_symmetries()
552                        .iter()
553                        .map(|vc_res| {
554                            vc_res
555                                .as_ref()
556                                .map(|vc_sym| vc_sym.to_string())
557                                .unwrap_or_else(|err| err.clone())
558                        })
559                        .collect::<Vec<_>>()
560                })
561                .map_err(|err| err.to_string())
562        };
563        self.result = Some((sym, rep()));
564        Ok(())
565    }
566}
567
568// ---------------------
569// Trait implementations
570// ---------------------
571
572// ~~~~~~~~~~~~~~~~~~~
573// Slater determinants
574// ~~~~~~~~~~~~~~~~~~~
575
576// Specific for unitary-represented and magnetic-represented symmetry groups and determinant numeric type f64
577// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
578#[duplicate_item(
579    [
580        gtype_ [ UnitaryRepresentedSymmetryGroup ]
581        err_ [ "No Q-Chem single-point analysis results (unitary-represented group, real vibrational coordinates) found." ]
582    ]
583    [
584        gtype_ [ MagneticRepresentedSymmetryGroup ]
585        err_ [ "No Q-Chem single-point analysis results (magnetic-represented group, real vibrational coordinates) found." ]
586    ]
587)]
588impl<'a> QSym2Driver for QChemVibrationH5SinglePointDriver<'a, gtype_, f64> {
589    type Params = VibrationalCoordinateRepAnalysisParams<f64>;
590
591    type Outcome = (Symmetry, Result<Vec<String>, String>);
592
593    fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
594        self.result.as_ref().ok_or(format_err!(err_))
595    }
596
597    fn run(&mut self) -> Result<(), anyhow::Error> {
598        self.analyse()
599    }
600}