1use 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
46lazy_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#[derive(Clone, Builder)]
64pub(crate) struct QChemVibrationH5Driver<'a, T>
65where
66 T: Clone,
67{
68 filename: PathBuf,
70
71 symmetry_group_detection_input: &'a SymmetryGroupDetectionInputKind,
73
74 angular_function_analysis_parameters: &'a AngularFunctionRepAnalysisParams,
76
77 rep_analysis_parameters: &'a VibrationalCoordinateRepAnalysisParams<f64>,
79
80 #[builder(default = "None")]
83 result: Option<Vec<(String, String)>>,
84
85 #[builder(setter(skip), default = "PhantomData")]
87 numerical_type: PhantomData<T>,
88}
89
90impl<'a, T> QChemVibrationH5Driver<'a, T>
95where
96 T: Clone
97{
98 pub(crate) fn builder() -> QChemVibrationH5DriverBuilder<'a, T> {
100 QChemVibrationH5DriverBuilder::default()
101 }
102}
103
104impl<'a> QChemVibrationH5Driver<'a, f64> {
107 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#[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 sp_group: &'a hdf5::Group,
302
303 #[builder(setter(custom))]
306 energy_function_index: String,
307
308 symmetry_group_detection_input: &'a SymmetryGroupDetectionInputKind,
310
311 angular_function_analysis_parameters: &'a AngularFunctionRepAnalysisParams,
313
314 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 #[builder(default = "None")]
325 result: Option<(Symmetry, Result<Vec<String>, String>)>,
326}
327
328impl<'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 fn builder() -> QChemVibrationH5SinglePointDriverBuilder<'a, G, T> {
354 QChemVibrationH5SinglePointDriverBuilder::default()
355 }
356
357 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
391impl<'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 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#[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#[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}