1use 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
55lazy_static! {
60 static ref SP_PATH_RE: Regex =
61 Regex::new(r"(.*sp)\\energy_function$").expect("Regex pattern invalid.");
62}
63
64#[derive(Clone, Builder)]
72pub struct QChemSlaterDeterminantH5Driver<'a, T>
73where
74 T: Clone,
75{
76 filename: PathBuf,
78
79 symmetry_group_detection_input: &'a SymmetryGroupDetectionInputKind,
81
82 angular_function_analysis_parameters: &'a AngularFunctionRepAnalysisParams,
84
85 rep_analysis_parameters: &'a SlaterDeterminantRepAnalysisParams<f64>,
87
88 #[builder(default = "None")]
92 result: Option<Vec<(String, String)>>,
93
94 #[builder(setter(skip), default = "PhantomData")]
96 numerical_type: PhantomData<T>,
97}
98
99impl<'a, T> QChemSlaterDeterminantH5Driver<'a, T>
104where
105 T: Clone,
106{
107 pub fn builder() -> QChemSlaterDeterminantH5DriverBuilder<'a, T> {
109 QChemSlaterDeterminantH5DriverBuilder::default()
110 }
111}
112
113impl<'a> QChemSlaterDeterminantH5Driver<'a, f64> {
120 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
315pub enum OrbitalType {
325 Canonical,
327
328 Localised,
330}
331
332#[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 sp_group: &'a hdf5::Group,
348
349 #[builder(setter(custom))]
352 energy_function_index: String,
353
354 symmetry_group_detection_input: &'a SymmetryGroupDetectionInputKind,
356
357 angular_function_analysis_parameters: &'a AngularFunctionRepAnalysisParams,
359
360 rep_analysis_parameters: &'a SlaterDeterminantRepAnalysisParams<f64>,
362
363 #[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
372impl<'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 pub fn builder() -> QChemSlaterDeterminantH5SinglePointDriverBuilder<'a, G, T> {
398 QChemSlaterDeterminantH5SinglePointDriverBuilder::default()
399 }
400
401 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 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 vec![BasisShell::new(0, ShellOrder::Cart(CartOrder::qchem(0)))]
461 } else if *shell_type == 1 {
462 vec![BasisShell::new(1, ShellOrder::Cart(CartOrder::qchem(1)))]
464 } else if *shell_type == -1 {
465 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 let l = shell_type.unsigned_abs();
473 vec![BasisShell::new(l, ShellOrder::Cart(CartOrder::qchem(l)))]
474 } else {
475 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 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 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 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 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 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 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 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 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 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 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
796impl<'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 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#[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#[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}