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};
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
56lazy_static! {
61 static ref SP_PATH_RE: Regex =
62 Regex::new(r"(.*sp)\\energy_function$").expect("Regex pattern invalid.");
63}
64
65#[derive(Clone, Builder)]
73pub struct QChemSlaterDeterminantH5Driver<'a, T>
74where
75 T: Clone,
76{
77 filename: PathBuf,
79
80 symmetry_group_detection_input: &'a SymmetryGroupDetectionInputKind,
82
83 angular_function_analysis_parameters: &'a AngularFunctionRepAnalysisParams,
85
86 rep_analysis_parameters: &'a SlaterDeterminantRepAnalysisParams<f64>,
88
89 #[builder(default = "None")]
93 result: Option<Vec<(String, String)>>,
94
95 #[builder(setter(skip), default = "PhantomData")]
97 numerical_type: PhantomData<T>,
98}
99
100impl<'a, T> QChemSlaterDeterminantH5Driver<'a, T>
105where
106 T: Clone,
107{
108 pub fn builder() -> QChemSlaterDeterminantH5DriverBuilder<'a, T> {
110 QChemSlaterDeterminantH5DriverBuilder::default()
111 }
112}
113
114impl<'a> QChemSlaterDeterminantH5Driver<'a, f64> {
121 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
316pub enum OrbitalType {
326 Canonical,
328
329 Localised,
331}
332
333#[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 sp_group: &'a hdf5::Group,
349
350 #[builder(setter(custom))]
353 energy_function_index: String,
354
355 symmetry_group_detection_input: &'a SymmetryGroupDetectionInputKind,
357
358 angular_function_analysis_parameters: &'a AngularFunctionRepAnalysisParams,
360
361 rep_analysis_parameters: &'a SlaterDeterminantRepAnalysisParams<f64>,
363
364 #[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
373impl<'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 pub fn builder() -> QChemSlaterDeterminantH5SinglePointDriverBuilder<'a, G, T> {
399 QChemSlaterDeterminantH5SinglePointDriverBuilder::default()
400 }
401
402 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 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 vec![BasisShell::new(0, ShellOrder::Cart(CartOrder::qchem(0)))]
462 } else if *shell_type == 1 {
463 vec![BasisShell::new(1, ShellOrder::Cart(CartOrder::qchem(1)))]
465 } else if *shell_type == -1 {
466 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 let l = shell_type.unsigned_abs();
474 vec![BasisShell::new(l, ShellOrder::Cart(CartOrder::qchem(l)))]
475 } else {
476 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 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 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 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 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 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 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 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 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 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 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
797impl<'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 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#[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#[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}