qsym2/bindings/python/representation_analysis/multideterminant/multideterminant_orbit_basis_external_solver.rs
1//! Python bindings for QSym² symmetry analysis of multi-determinants with orbit bases using
2//! external NOCI solvers.
3
4use std::collections::HashSet;
5use std::path::PathBuf;
6
7use anyhow::{Context, bail, format_err};
8use itertools::Itertools;
9use ndarray::{Array1, Array2, Axis, ShapeBuilder};
10use num_complex::Complex;
11use numpy::{PyArray1, PyArray2, PyArrayMethods, ToPyArray};
12use pyo3::exceptions::{PyIOError, PyRuntimeError};
13use pyo3::types::PyFunction;
14use pyo3::{IntoPyObjectExt, prelude::*};
15
16use crate::analysis::EigenvalueComparisonMode;
17use crate::angmom::spinor_rotation_3d::{SpinConstraint, SpinOrbitCoupled};
18use crate::bindings::python::integrals::{PyBasisAngularOrder, PyStructureConstraint};
19use crate::bindings::python::representation_analysis::PyArray2RC;
20use crate::bindings::python::representation_analysis::multideterminant::{
21 PyMultiDeterminantsComplex, PyMultiDeterminantsReal,
22};
23use crate::bindings::python::representation_analysis::slater_determinant::{
24 PySlaterDeterminant, PySlaterDeterminantComplex, PySlaterDeterminantReal,
25};
26use crate::drivers::QSym2Driver;
27use crate::drivers::representation_analysis::angular_function::AngularFunctionRepAnalysisParams;
28use crate::drivers::representation_analysis::multideterminant::{
29 MultiDeterminantRepAnalysisDriver, MultiDeterminantRepAnalysisParams,
30};
31use crate::drivers::representation_analysis::{
32 CharacterTableDisplay, MagneticSymmetryAnalysisKind,
33};
34use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
35use crate::io::format::qsym2_output;
36use crate::io::{QSym2FileType, read_qsym2_binary};
37use crate::symmetry::symmetry_group::{
38 MagneticRepresentedSymmetryGroup, SymmetryGroupProperties, UnitaryRepresentedSymmetryGroup,
39};
40use crate::symmetry::symmetry_transformation::{SymmetryTransformable, SymmetryTransformationKind};
41use crate::target::determinant::SlaterDeterminant;
42use crate::target::noci::basis::{Basis, OrbitBasis};
43use crate::target::noci::multideterminant::MultiDeterminant;
44use crate::target::noci::multideterminants::MultiDeterminants;
45
46type C128 = Complex<f64>;
47
48// ~~~~~~~~~~~~~
49// Macro helpers
50// ~~~~~~~~~~~~~
51macro_rules! generate_noci_solver {
52 ($noci_solver_name:ident, $py_solver_func:ident, $pysd:ty, $t:ty, $sc:ty) => {
53 let $noci_solver_name = |multidets: &Vec<SlaterDeterminant<$t, $sc>>| {
54 Python::attach(|py_inner| {
55 let pymultidets = multidets
56 .iter()
57 .map(|det| {
58 let pysc = det.structure_constraint().clone().try_into()?;
59 Ok(<$pysd>::new(
60 pysc,
61 det.complex_symmetric(),
62 det.coefficients()
63 .iter()
64 .map(|arr| PyArray2::from_array(py_inner, arr))
65 .collect::<Vec<_>>(),
66 det.occupations()
67 .iter()
68 .map(|arr| PyArray1::from_array(py_inner, arr))
69 .collect::<Vec<_>>(),
70 det.threshold(),
71 det.mo_energies().map(|mo_energies| {
72 mo_energies
73 .iter()
74 .map(|arr| PyArray1::from_array(py_inner, arr))
75 .collect::<Vec<_>>()
76 }),
77 det.energy().ok().cloned(),
78 ))
79 })
80 .collect::<Result<Vec<_>, anyhow::Error>>()
81 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
82 $py_solver_func
83 .call1(py_inner, (pymultidets,))
84 .and_then(|res| res.extract::<(Vec<$t>, Vec<Vec<$t>>)>(py_inner))
85 })
86 };
87 };
88}
89
90// ~~~~~~~~~
91// Functions
92// ~~~~~~~~~
93
94/// Python-exposed function to perform representation symmetry analysis for real and complex
95/// multi-determinantal wavefunctions constructed from group-generated orbits and log the result via
96/// the `qsym2-output` logger at the `INFO` level.
97///
98/// If `symmetry_transformation_kind` includes spin transformation, the provided
99/// multi-determinantal wavefunctions will be augmented to generalised spin constraint
100/// automatically.
101///
102/// # Arguments
103///
104/// * `inp_sym` - A path to the [`QSym2FileType::Sym`] file containing the symmetry-group detection
105/// result for the system. This will be used to construct abstract groups and character tables for
106/// representation analysis.
107/// * `pyorigins` - A list of Python-exposed Slater determinants whose coefficients are of type
108/// `float64` or `complex128`. These determinants serve as origins for group-generated orbits which
109/// serve as basis states for non-orthogonal configuration interaction to yield multi-determinantal
110/// wavefunctions, the symmetry of which will be analysed by this function.
111/// * `py_noci_solver` - A Python function callable on a sequence of Slater determinants to perform
112/// non-orthogonal configuration interaction (NOCI) and return a list of NOCI energies and a
113/// corresponding list of lists of linear combination coefficients, where each inner list is for one
114/// multi-determinantal wavefunction resulting from the NOCI calculation.
115/// Python type: `Callable[[list[PySlaterDeterminantReal | PySlaterDeterminantComplex]], tuple[list[float], list[list[float]]] | tuple[list[complex], list[list[complex]]]]`.
116/// * `pybaos` - Python-exposed structures containing basis angular order information, one for each
117/// explicit component per coefficient matrix.
118/// * `density_matrix_calculation_thresholds` - An optional pair of thresholds for Löwdin pairing,
119/// one for checking zero off-diagonal values, one for checking zero overlaps, when computing
120/// multi-determinantal density matrices. If `None`, no density matrices for the resulting
121/// multi-determinants will be computed.
122/// * `integrality_threshold` - The threshold for verifying if subspace multiplicities are integral.
123/// * `linear_independence_threshold` - The threshold for determining the linear independence
124/// subspace via the non-zero eigenvalues of the orbit overlap matrix.
125/// * `use_magnetic_group` - An option indicating if the magnetic group is to be used for symmetry
126/// analysis, and if so, whether unitary representations or unitary-antiunitary corepresentations
127/// should be used.
128/// * `use_double_group` - A boolean indicating if the double group of the prevailing symmetry
129/// group is to be used for representation analysis instead.
130/// * `use_cayley_table` - A boolean indicating if the Cayley table for the group, if available,
131/// should be used to speed up the calculation of orbit overlap matrices.
132/// * `symmetry_transformation_kind` - An enumerated type indicating the type of symmetry
133/// transformations to be performed on the origin determinant to generate the orbit. If this
134/// contains spin transformation, the multi-determinant will be augmented to generalised spin
135/// constraint automatically.
136/// * `eigenvalue_comparison_mode` - An enumerated type indicating the mode of comparison of orbit
137/// overlap eigenvalues with the specified `linear_independence_threshold`.
138/// * `sao` - The atomic-orbital overlap matrix whose elements are of type `float64` or
139/// `complex128`.
140/// * `sao_h` - The optional complex-symmetric atomic-orbital overlap matrix whose elements
141/// are of type `float64` or `complex128`. This is required if antiunitary symmetry operations are
142/// involved.
143/// * `write_overlap_eigenvalues` - A boolean indicating if the eigenvalues of the determinant
144/// orbit overlap matrix are to be written to the output.
145/// * `write_character_table` - A boolean indicating if the character table of the prevailing
146/// symmetry group is to be printed out.
147/// * `infinite_order_to_finite` - The finite order with which infinite-order generators are to be
148/// interpreted to form a finite subgroup of the prevailing infinite group. This finite subgroup
149/// will be used for symmetry analysis.
150/// * `angular_function_integrality_threshold` - The threshold for verifying if subspace
151/// multiplicities are integral for the symmetry analysis of angular functions.
152/// * `angular_function_linear_independence_threshold` - The threshold for determining the linear
153/// independence subspace via the non-zero eigenvalues of the orbit overlap matrix for the symmetry
154/// analysis of angular functions.
155/// * `angular_function_max_angular_momentum` - The maximum angular momentum order to be used in
156/// angular function symmetry analysis.
157#[allow(clippy::too_many_arguments)]
158#[pyfunction]
159#[pyo3(signature = (
160 inp_sym,
161 pyorigins,
162 py_noci_solver,
163 pybaos,
164 density_matrix_calculation_thresholds,
165 integrality_threshold,
166 linear_independence_threshold,
167 use_magnetic_group,
168 use_double_group,
169 use_cayley_table,
170 symmetry_transformation_kind,
171 eigenvalue_comparison_mode,
172 sao,
173 sao_h=None,
174 write_overlap_eigenvalues=true,
175 write_character_table=true,
176 infinite_order_to_finite=None,
177 angular_function_integrality_threshold=1e-7,
178 angular_function_linear_independence_threshold=1e-7,
179 angular_function_max_angular_momentum=2
180))]
181pub fn rep_analyse_multideterminants_orbit_basis_external_solver(
182 py: Python<'_>,
183 inp_sym: PathBuf,
184 pyorigins: Vec<PySlaterDeterminant>,
185 py_noci_solver: Py<PyFunction>,
186 pybaos: Vec<PyBasisAngularOrder>,
187 density_matrix_calculation_thresholds: Option<(f64, f64)>,
188 integrality_threshold: f64,
189 linear_independence_threshold: f64,
190 use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
191 use_double_group: bool,
192 use_cayley_table: bool,
193 symmetry_transformation_kind: SymmetryTransformationKind,
194 eigenvalue_comparison_mode: EigenvalueComparisonMode,
195 sao: PyArray2RC,
196 sao_h: Option<PyArray2RC>,
197 write_overlap_eigenvalues: bool,
198 write_character_table: bool,
199 infinite_order_to_finite: Option<u32>,
200 angular_function_integrality_threshold: f64,
201 angular_function_linear_independence_threshold: f64,
202 angular_function_max_angular_momentum: u32,
203) -> PyResult<Py<PyAny>> {
204 // Read in point-group detection results
205 let pd_res: SymmetryGroupDetectionResult =
206 read_qsym2_binary(inp_sym.clone(), QSym2FileType::Sym)
207 .map_err(|err| PyIOError::new_err(err.to_string()))?;
208
209 let mut file_name = inp_sym.to_path_buf();
210 file_name.set_extension(QSym2FileType::Sym.ext());
211 qsym2_output!(
212 "Symmetry-group detection results read in from {}.",
213 file_name.display(),
214 );
215 qsym2_output!("");
216
217 // Set up basic parameters
218 let mol = &pd_res.pre_symmetry.recentred_molecule;
219
220 let baos = pybaos
221 .iter()
222 .map(|bao| {
223 bao.to_qsym2(mol)
224 .map_err(|err| PyRuntimeError::new_err(err.to_string()))
225 })
226 .collect::<Result<Vec<_>, _>>()?;
227 let baos_ref = baos.iter().collect::<Vec<_>>();
228 let augment_to_generalised = match symmetry_transformation_kind {
229 SymmetryTransformationKind::SpatialWithSpinTimeReversal
230 | SymmetryTransformationKind::Spin
231 | SymmetryTransformationKind::SpinSpatial => true,
232 SymmetryTransformationKind::Spatial => false,
233 };
234 let afa_params = AngularFunctionRepAnalysisParams::builder()
235 .integrality_threshold(angular_function_integrality_threshold)
236 .linear_independence_threshold(angular_function_linear_independence_threshold)
237 .max_angular_momentum(angular_function_max_angular_momentum)
238 .build()
239 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
240 let mda_params = MultiDeterminantRepAnalysisParams::<f64>::builder()
241 .integrality_threshold(integrality_threshold)
242 .linear_independence_threshold(linear_independence_threshold)
243 .use_magnetic_group(use_magnetic_group.clone())
244 .use_double_group(use_double_group)
245 .use_cayley_table(use_cayley_table)
246 .symmetry_transformation_kind(symmetry_transformation_kind.clone())
247 .eigenvalue_comparison_mode(eigenvalue_comparison_mode)
248 .write_overlap_eigenvalues(write_overlap_eigenvalues)
249 .write_character_table(if write_character_table {
250 Some(CharacterTableDisplay::Symbolic)
251 } else {
252 None
253 })
254 .infinite_order_to_finite(infinite_order_to_finite)
255 .build()
256 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
257
258 // Set up NOCI function
259 generate_noci_solver!(
260 noci_solver_r,
261 py_noci_solver,
262 PySlaterDeterminantReal,
263 f64,
264 SpinConstraint
265 );
266 generate_noci_solver!(
267 noci_solver_c_sc,
268 py_noci_solver,
269 PySlaterDeterminantComplex,
270 C128,
271 SpinConstraint
272 );
273 generate_noci_solver!(
274 noci_solver_c_soc,
275 py_noci_solver,
276 PySlaterDeterminantComplex,
277 C128,
278 SpinOrbitCoupled
279 );
280
281 let all_real = pyorigins
282 .iter()
283 .all(|pyorigin| matches!(pyorigin, PySlaterDeterminant::Real(_)));
284
285 let structure_constraints_set = pyorigins
286 .iter()
287 .map(|pyorigin| match pyorigin {
288 PySlaterDeterminant::Real(pydet) => pydet.structure_constraint().clone(),
289 PySlaterDeterminant::Complex(pydet) => pydet.structure_constraint().clone(),
290 })
291 .collect::<HashSet<_>>();
292 if structure_constraints_set.len() != 1 {
293 return Err(PyRuntimeError::new_err(
294 "Inconsistent structure constraints across origin determinants.`",
295 ));
296 };
297 let structure_constraint = structure_constraints_set
298 .iter()
299 .next()
300 .ok_or_else(|| PyRuntimeError::new_err("Unable to retrieve the structure constraint."))?;
301
302 // Decision tree:
303 // - all_real and real SAO?
304 // + yes:
305 // - structure_constraint:
306 // + SpinConstraint:
307 // - use_magnetic_group:
308 // + Some(Corepresentation)
309 // + Some(Representation) | None
310 // + SpinOrbitCoupled: not supported
311 // - no:
312 // - structure_constraint:
313 // + SpinConstraint:
314 // - use_magnetic_group:
315 // + Some(Corepresentation)
316 // + Some(Representation) | None
317 // + SpinOrbitCoupled:
318 // - use_magnetic_group:
319 // + Some(Corepresentation)
320 // + Some(Representation) | None
321 match (all_real, &sao) {
322 (true, PyArray2RC::Real(pysao_r)) => {
323 // Real numeric data type
324
325 if matches!(
326 structure_constraint,
327 PyStructureConstraint::SpinOrbitCoupled(_)
328 ) {
329 return Err(PyRuntimeError::new_err(
330 "Real determinants cannot support spin--orbit-coupled structure constraint.",
331 ));
332 }
333
334 // Preparation
335 let sao_r = pysao_r.to_owned_array();
336 let origins_r = if augment_to_generalised {
337 pyorigins
338 .iter()
339 .map(|pydet| {
340 if let PySlaterDeterminant::Real(pydet_r) = pydet {
341 pydet_r
342 .to_qsym2(&baos_ref, mol)
343 .map(|det_r| det_r.to_generalised())
344 } else {
345 bail!("Unexpected complex type for an origin Slater determinant.")
346 }
347 })
348 .collect::<Result<Vec<_>, _>>()
349 } else {
350 pyorigins
351 .iter()
352 .map(|pydet| {
353 if let PySlaterDeterminant::Real(pydet_r) = pydet {
354 pydet_r.to_qsym2(&baos_ref, mol)
355 } else {
356 bail!("Unexpected complex type for an origin Slater determinant.")
357 }
358 })
359 .collect::<Result<Vec<_>, _>>()
360 }
361 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
362
363 match &use_magnetic_group {
364 Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
365 // Magnetic groups with corepresentations
366 let group = py
367 .detach(|| {
368 let magsym = pd_res
369 .magnetic_symmetry
370 .as_ref()
371 .ok_or(format_err!("Magnetic group required for orbit construction, but no magnetic symmetry found."))?;
372 if use_double_group {
373 MagneticRepresentedSymmetryGroup::from_molecular_symmetry(
374 magsym,
375 infinite_order_to_finite,
376 )
377 .and_then(|grp| grp.to_double_group())
378 } else {
379 MagneticRepresentedSymmetryGroup::from_molecular_symmetry(
380 magsym,
381 infinite_order_to_finite,
382 )
383 }
384 })
385 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
386
387 // Construct the orbit basis
388 let orbit_basis = match symmetry_transformation_kind {
389 SymmetryTransformationKind::Spatial => {
390 OrbitBasis::builder()
391 .group(&group)
392 .origins(origins_r)
393 .action(|op, det| {
394 det.sym_transform_spatial(op).with_context(|| {
395 format!("Unable to apply `{op}` spatially on the origin multi-determinantal wavefunction")
396 })
397 })
398 .build()
399 }
400 SymmetryTransformationKind::SpatialWithSpinTimeReversal => {
401 OrbitBasis::builder()
402 .group(&group)
403 .origins(origins_r)
404 .action(|op, det| {
405 det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
406 format!("Unable to apply `{op}` spatially (with spin-including time reversal) on the origin multi-determinantal wavefunction")
407 })
408 })
409 .build()
410 }
411 SymmetryTransformationKind::Spin => {
412 OrbitBasis::builder()
413 .group(&group)
414 .origins(origins_r)
415 .action(|op, det| {
416 det.sym_transform_spin(op).with_context(|| {
417 format!("Unable to apply `{op}` spin-wise on the origin multi-determinantal wavefunction")
418 })
419 })
420 .build()
421 }
422 SymmetryTransformationKind::SpinSpatial => {
423 OrbitBasis::builder()
424 .group(&group)
425 .origins(origins_r)
426 .action(|op, det| {
427 det.sym_transform_spin_spatial(op).with_context(|| {
428 format!("Unable to apply `{op}` spin-spatially on the origin multi-determinantal wavefunction")
429 })
430 })
431 .build()
432 }
433 }.map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
434
435 // Run NOCI using the real-valued external solver
436 let dets = orbit_basis
437 .iter()
438 .collect::<Result<Vec<_>, _>>()
439 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
440
441 let (noci_energies_vec, noci_coeffs_vec) = noci_solver_r(&dets)
442 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
443 if noci_energies_vec.len() != noci_coeffs_vec.len()
444 || noci_coeffs_vec
445 .iter()
446 .map(|coeffs| coeffs.len())
447 .collect::<HashSet<_>>()
448 .len()
449 != 1
450 {
451 return Err(PyRuntimeError::new_err(
452 "Inconsistent dimensions encountered in NOCI results.",
453 ));
454 }
455 let multidets = noci_energies_vec
456 .into_iter()
457 .zip(noci_coeffs_vec)
458 .map(|(energy, coeffs)| {
459 MultiDeterminant::builder()
460 .basis(orbit_basis.clone())
461 .coefficients(Array1::from_vec(coeffs))
462 .threshold(1e-7)
463 .energy(Ok(energy))
464 .build()
465 })
466 .collect::<Result<Vec<_>, _>>()
467 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
468
469 let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
470 MagneticRepresentedSymmetryGroup,
471 f64,
472 _,
473 SpinConstraint,
474 >::builder()
475 .parameters(&mda_params)
476 .angular_function_parameters(&afa_params)
477 .multidets(multidets.iter().collect::<Vec<_>>())
478 .sao(&sao_r)
479 .sao_h(None) // Real SAO.
480 .symmetry_group(&pd_res)
481 .build()
482 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
483 py.detach(|| {
484 mda_driver
485 .run()
486 .map_err(|err| PyRuntimeError::new_err(err.to_string()))
487 })?;
488
489 // Collect real multi-determinantal wavefunctions for returning
490 let basis = multidets
491 .first()
492 .and_then(|multidet| {
493 multidet
494 .basis()
495 .iter()
496 .map(|det_res| det_res.and_then(|det| det.to_python(py)))
497 .collect::<Result<Vec<_>, _>>()
498 .ok()
499 })
500 .ok_or_else(|| {
501 PyRuntimeError::new_err(
502 "Unable to obtain the basis of Slater determinants.".to_string(),
503 )
504 })?;
505 let (coefficientss, energies): (Vec<_>, Vec<_>) = multidets
506 .iter()
507 .map(|multidet| {
508 let coefficients =
509 multidet.coefficients().iter().cloned().collect_vec();
510 let energy = *multidet.energy().unwrap_or(&f64::NAN);
511 Ok::<_, PyErr>((coefficients, energy))
512 })
513 .collect::<Result<Vec<_>, _>>()?
514 .into_iter()
515 .unzip();
516 let coefficientss_arr = Array2::from_shape_vec(
517 (basis.len(), coefficientss.len()).f(),
518 coefficientss.into_iter().flatten().collect_vec(),
519 )
520 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
521 .to_pyarray(py);
522 let energies_arr = Array1::from_vec(energies).to_pyarray(py);
523 let density_matrices = density_matrix_calculation_thresholds.and_then(
524 |(thresh_offdiag, thresh_zeroov)| {
525 log::debug!("Calculating density matrices...");
526 let multidets_collection =
527 MultiDeterminants::from_multideterminant_vec(
528 &multidets.iter().collect_vec(),
529 )
530 .ok()?;
531 let denmats_opt = multidets_collection
532 .density_matrices(
533 &sao_r.view(),
534 thresh_offdiag,
535 thresh_zeroov,
536 true,
537 )
538 .map(|denmats| {
539 denmats
540 .axis_iter(Axis(0))
541 .map(|denmat| denmat.to_pyarray(py))
542 .collect_vec()
543 })
544 .ok();
545 log::debug!("Calculating density matrices... Done.");
546 denmats_opt
547 },
548 );
549 let pymultidet = PyMultiDeterminantsReal::new(
550 basis,
551 coefficientss_arr,
552 energies_arr,
553 density_matrices,
554 multidets[0].threshold(),
555 )
556 .into_py_any(py)?;
557 Ok(pymultidet)
558 }
559 Some(MagneticSymmetryAnalysisKind::Representation) | None => {
560 // Unitary groups or magnetic groups with representations
561 let group = py
562 .detach(|| {
563 let sym = if use_magnetic_group.is_some() {
564 pd_res
565 .magnetic_symmetry
566 .as_ref()
567 .ok_or(format_err!("Magnetic group required for orbit construction, but no magnetic symmetry found."))?
568 } else {
569 &pd_res.unitary_symmetry
570 };
571 if use_double_group {
572 UnitaryRepresentedSymmetryGroup::from_molecular_symmetry(
573 sym,
574 infinite_order_to_finite,
575 )
576 .and_then(|grp| grp.to_double_group())
577 } else {
578 UnitaryRepresentedSymmetryGroup::from_molecular_symmetry(
579 sym,
580 infinite_order_to_finite,
581 )
582 }
583 })
584 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
585
586 // Construct the orbit basis
587 let orbit_basis = match symmetry_transformation_kind {
588 SymmetryTransformationKind::Spatial => {
589 OrbitBasis::builder()
590 .group(&group)
591 .origins(origins_r)
592 .action(|op, det| {
593 det.sym_transform_spatial(op).with_context(|| {
594 format!("Unable to apply `{op}` spatially on the origin multi-determinantal wavefunction")
595 })
596 })
597 .build()
598 }
599 SymmetryTransformationKind::SpatialWithSpinTimeReversal => {
600 OrbitBasis::builder()
601 .group(&group)
602 .origins(origins_r)
603 .action(|op, det| {
604 det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
605 format!("Unable to apply `{op}` spatially (with spin-including time reversal) on the origin multi-determinantal wavefunction")
606 })
607 })
608 .build()
609 }
610 SymmetryTransformationKind::Spin => {
611 OrbitBasis::builder()
612 .group(&group)
613 .origins(origins_r)
614 .action(|op, det| {
615 det.sym_transform_spin(op).with_context(|| {
616 format!("Unable to apply `{op}` spin-wise on the origin multi-determinantal wavefunction")
617 })
618 })
619 .build()
620 }
621 SymmetryTransformationKind::SpinSpatial => {
622 OrbitBasis::builder()
623 .group(&group)
624 .origins(origins_r)
625 .action(|op, det| {
626 det.sym_transform_spin_spatial(op).with_context(|| {
627 format!("Unable to apply `{op}` spin-spatially on the origin multi-determinantal wavefunction")
628 })
629 })
630 .build()
631 }
632 }.map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
633
634 // Run NOCI using the real-valued external solver
635 let dets = orbit_basis
636 .iter()
637 .collect::<Result<Vec<_>, _>>()
638 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
639
640 let (noci_energies_vec, noci_coeffs_vec) = noci_solver_r(&dets)
641 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
642 if noci_energies_vec.len() != noci_coeffs_vec.len()
643 || noci_coeffs_vec
644 .iter()
645 .map(|coeffs| coeffs.len())
646 .collect::<HashSet<_>>()
647 .len()
648 != 1
649 {
650 return Err(PyRuntimeError::new_err(
651 "Inconsistent dimensions encountered in NOCI results.",
652 ));
653 }
654 let multidets = noci_energies_vec
655 .into_iter()
656 .zip(noci_coeffs_vec)
657 .map(|(energy, coeffs)| {
658 MultiDeterminant::builder()
659 .basis(orbit_basis.clone())
660 .coefficients(Array1::from_vec(coeffs))
661 .threshold(1e-7)
662 .energy(Ok(energy))
663 .build()
664 })
665 .collect::<Result<Vec<_>, _>>()
666 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
667
668 let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
669 UnitaryRepresentedSymmetryGroup,
670 f64,
671 _,
672 SpinConstraint,
673 >::builder()
674 .parameters(&mda_params)
675 .angular_function_parameters(&afa_params)
676 .multidets(multidets.iter().collect::<Vec<_>>())
677 .sao(&sao_r)
678 .sao_h(None) // Real SAO.
679 .symmetry_group(&pd_res)
680 .build()
681 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
682 py.detach(|| {
683 mda_driver
684 .run()
685 .map_err(|err| PyRuntimeError::new_err(err.to_string()))
686 })?;
687
688 // Collect real multi-determinantal wavefunctions for returning
689 let basis = multidets
690 .first()
691 .and_then(|multidet| {
692 multidet
693 .basis()
694 .iter()
695 .map(|det_res| det_res.and_then(|det| det.to_python(py)))
696 .collect::<Result<Vec<_>, _>>()
697 .ok()
698 })
699 .ok_or_else(|| {
700 PyRuntimeError::new_err(
701 "Unable to obtain the basis of Slater determinants.".to_string(),
702 )
703 })?;
704 let (coefficientss, energies): (Vec<_>, Vec<_>) = multidets
705 .iter()
706 .map(|multidet| {
707 let coefficients =
708 multidet.coefficients().iter().cloned().collect_vec();
709 let energy = *multidet.energy().unwrap_or(&f64::NAN);
710 Ok::<_, PyErr>((coefficients, energy))
711 })
712 .collect::<Result<Vec<_>, _>>()?
713 .into_iter()
714 .unzip();
715 let coefficientss_arr = Array2::from_shape_vec(
716 (basis.len(), coefficientss.len()).f(),
717 coefficientss.into_iter().flatten().collect_vec(),
718 )
719 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
720 .to_pyarray(py);
721 let energies_arr = Array1::from_vec(energies).to_pyarray(py);
722 let density_matrices = density_matrix_calculation_thresholds.and_then(
723 |(thresh_offdiag, thresh_zeroov)| {
724 log::debug!("Calculating density matrices...");
725 let multidets_collection =
726 MultiDeterminants::from_multideterminant_vec(
727 &multidets.iter().collect_vec(),
728 )
729 .ok()?;
730 let denmats_opt = multidets_collection
731 .density_matrices(
732 &sao_r.view(),
733 thresh_offdiag,
734 thresh_zeroov,
735 true,
736 )
737 .map(|denmats| {
738 denmats
739 .axis_iter(Axis(0))
740 .map(|denmat| denmat.to_pyarray(py))
741 .collect_vec()
742 })
743 .ok();
744 log::debug!("Calculating density matrices... Done.");
745 denmats_opt
746 },
747 );
748 let pymultidet = PyMultiDeterminantsReal::new(
749 basis,
750 coefficientss_arr,
751 energies_arr,
752 density_matrices,
753 multidets[0].threshold(),
754 )
755 .into_py_any(py)?;
756 Ok(pymultidet)
757 }
758 }
759 }
760 (_, _) => {
761 // Complex numeric data type
762
763 // Preparation
764 let sao_c = match sao {
765 PyArray2RC::Real(pysao_r) => pysao_r.to_owned_array().mapv(Complex::from),
766 PyArray2RC::Complex(pysao_c) => pysao_c.to_owned_array(),
767 };
768 let sao_h_c = sao_h.map(|pysao_h| match pysao_h {
769 // sao_h must have the same reality as sao.
770 PyArray2RC::Real(pysao_h_r) => pysao_h_r.to_owned_array().mapv(Complex::from),
771 PyArray2RC::Complex(pysao_h_c) => pysao_h_c.to_owned_array(),
772 });
773
774 match structure_constraint {
775 PyStructureConstraint::SpinConstraint(_) => {
776 let origins_c = if augment_to_generalised {
777 pyorigins
778 .iter()
779 .map(|pydet| match pydet {
780 PySlaterDeterminant::Real(pydet_r) => pydet_r
781 .to_qsym2::<SpinConstraint>(&baos_ref, mol)
782 .map(|det_r| {
783 SlaterDeterminant::<C128, SpinConstraint>::from(det_r)
784 .to_generalised()
785 }),
786 PySlaterDeterminant::Complex(pydet_c) => pydet_c
787 .to_qsym2::<SpinConstraint>(&baos_ref, mol)
788 .map(|det_c| det_c.to_generalised()),
789 })
790 .collect::<Result<Vec<_>, _>>()
791 } else {
792 pyorigins
793 .iter()
794 .map(|pydet| match pydet {
795 PySlaterDeterminant::Real(pydet_r) => pydet_r
796 .to_qsym2::<SpinConstraint>(&baos_ref, mol)
797 .map(|det_r| {
798 SlaterDeterminant::<C128, SpinConstraint>::from(det_r)
799 }),
800 PySlaterDeterminant::Complex(pydet_c) => {
801 pydet_c.to_qsym2::<SpinConstraint>(&baos_ref, mol)
802 }
803 })
804 .collect::<Result<Vec<_>, _>>()
805 }
806 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
807
808 match &use_magnetic_group {
809 Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
810 // Magnetic groups with corepresentations
811 let group = py
812 .detach(|| {
813 let magsym = pd_res
814 .magnetic_symmetry
815 .as_ref()
816 .ok_or(format_err!("Magnetic group required for orbit construction, but no magnetic symmetry found."))?;
817 if use_double_group {
818 MagneticRepresentedSymmetryGroup::from_molecular_symmetry(
819 magsym,
820 infinite_order_to_finite,
821 )
822 .and_then(|grp| grp.to_double_group())
823 } else {
824 MagneticRepresentedSymmetryGroup::from_molecular_symmetry(
825 magsym,
826 infinite_order_to_finite,
827 )
828 }
829 })
830 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
831
832 // Construct the orbit basis
833 let orbit_basis = match symmetry_transformation_kind {
834 SymmetryTransformationKind::Spatial => {
835 OrbitBasis::builder()
836 .group(&group)
837 .origins(origins_c)
838 .action(|op, det| {
839 det.sym_transform_spatial(op).with_context(|| {
840 format!("Unable to apply `{op}` spatially on the origin multi-determinantal wavefunction")
841 })
842 })
843 .build()
844 }
845 SymmetryTransformationKind::SpatialWithSpinTimeReversal => {
846 OrbitBasis::builder()
847 .group(&group)
848 .origins(origins_c)
849 .action(|op, det| {
850 det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
851 format!("Unable to apply `{op}` spatially (with spin-including time reversal) on the origin multi-determinantal wavefunction")
852 })
853 })
854 .build()
855 }
856 SymmetryTransformationKind::Spin => {
857 OrbitBasis::builder()
858 .group(&group)
859 .origins(origins_c)
860 .action(|op, det| {
861 det.sym_transform_spin(op).with_context(|| {
862 format!("Unable to apply `{op}` spin-wise on the origin multi-determinantal wavefunction")
863 })
864 })
865 .build()
866 }
867 SymmetryTransformationKind::SpinSpatial => {
868 OrbitBasis::builder()
869 .group(&group)
870 .origins(origins_c)
871 .action(|op, det| {
872 det.sym_transform_spin_spatial(op).with_context(|| {
873 format!("Unable to apply `{op}` spin-spatially on the origin multi-determinantal wavefunction")
874 })
875 })
876 .build()
877 }
878 }.map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
879
880 // Run non-orthogonal configuration interaction
881 let dets = orbit_basis
882 .iter()
883 .collect::<Result<Vec<_>, _>>()
884 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
885
886 let (noci_energies_vec, noci_coeffs_vec) = noci_solver_c_sc(&dets)
887 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
888 if noci_energies_vec.len() != noci_coeffs_vec.len()
889 || noci_coeffs_vec
890 .iter()
891 .map(|coeffs| coeffs.len())
892 .collect::<HashSet<_>>()
893 .len()
894 != 1
895 {
896 return Err(PyRuntimeError::new_err(
897 "Inconsistent dimensions encountered in NOCI results.",
898 ));
899 }
900 let multidets = noci_energies_vec
901 .into_iter()
902 .zip(noci_coeffs_vec)
903 .map(|(energy, coeffs)| {
904 MultiDeterminant::builder()
905 .basis(orbit_basis.clone())
906 .coefficients(Array1::from_vec(coeffs))
907 .threshold(1e-7)
908 .energy(Ok(energy))
909 .build()
910 })
911 .collect::<Result<Vec<_>, _>>()
912 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
913
914 let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
915 MagneticRepresentedSymmetryGroup,
916 C128,
917 _,
918 SpinConstraint,
919 >::builder()
920 .parameters(&mda_params)
921 .angular_function_parameters(&afa_params)
922 .multidets(multidets.iter().collect::<Vec<_>>())
923 .sao(&sao_c)
924 .sao_h(sao_h_c.as_ref())
925 .symmetry_group(&pd_res)
926 .build()
927 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
928 py.detach(|| {
929 mda_driver
930 .run()
931 .map_err(|err| PyRuntimeError::new_err(err.to_string()))
932 })?;
933
934 // Collect complex multi-determinantal wavefunctions for returning
935 let basis = multidets
936 .first()
937 .and_then(|multidet| {
938 multidet
939 .basis()
940 .iter()
941 .map(|det_res| det_res.and_then(|det| det.to_python(py)))
942 .collect::<Result<Vec<_>, _>>()
943 .ok()
944 })
945 .ok_or_else(|| {
946 PyRuntimeError::new_err(
947 "Unable to obtain the basis of Slater determinants."
948 .to_string(),
949 )
950 })?;
951 let (coefficientss, energies): (Vec<_>, Vec<_>) = multidets
952 .iter()
953 .map(|multidet| {
954 let coefficients =
955 multidet.coefficients().iter().cloned().collect_vec();
956 let energy =
957 *multidet.energy().unwrap_or(&Complex::from(f64::NAN));
958 Ok::<_, PyErr>((coefficients, energy))
959 })
960 .collect::<Result<Vec<_>, _>>()?
961 .into_iter()
962 .unzip();
963 let coefficientss_arr = Array2::from_shape_vec(
964 (basis.len(), coefficientss.len()).f(),
965 coefficientss.into_iter().flatten().collect_vec(),
966 )
967 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
968 .to_pyarray(py);
969 let energies_arr = Array1::from_vec(energies).to_pyarray(py);
970 let density_matrices = density_matrix_calculation_thresholds.and_then(
971 |(thresh_offdiag, thresh_zeroov)| {
972 log::debug!("Calculating density matrices...");
973 let multidets_collection =
974 MultiDeterminants::from_multideterminant_vec(
975 &multidets.iter().collect_vec(),
976 )
977 .ok()?;
978 let denmats_opt = multidets_collection
979 .density_matrices(
980 &sao_c.view(),
981 thresh_offdiag,
982 thresh_zeroov,
983 true,
984 )
985 .map(|denmats| {
986 denmats
987 .axis_iter(Axis(0))
988 .map(|denmat| denmat.to_pyarray(py))
989 .collect_vec()
990 })
991 .ok();
992 log::debug!("Calculating density matrices... Done.");
993 denmats_opt
994 },
995 );
996 let pymultidet = PyMultiDeterminantsComplex::new(
997 basis,
998 coefficientss_arr,
999 energies_arr,
1000 density_matrices,
1001 multidets[0].threshold(),
1002 )
1003 .into_py_any(py)?;
1004 Ok(pymultidet)
1005 }
1006 Some(MagneticSymmetryAnalysisKind::Representation) | None => {
1007 // Unitary groups or magnetic groups with representations
1008 let group = py
1009 .detach(|| {
1010 let sym = if use_magnetic_group.is_some() {
1011 pd_res
1012 .magnetic_symmetry
1013 .as_ref()
1014 .ok_or(format_err!("Magnetic group required for orbit construction, but no magnetic symmetry found."))?
1015 } else {
1016 &pd_res.unitary_symmetry
1017 };
1018 if use_double_group {
1019 UnitaryRepresentedSymmetryGroup::from_molecular_symmetry(
1020 sym,
1021 infinite_order_to_finite,
1022 )
1023 .and_then(|grp| grp.to_double_group())
1024 } else {
1025 UnitaryRepresentedSymmetryGroup::from_molecular_symmetry(
1026 sym,
1027 infinite_order_to_finite,
1028 )
1029 }
1030 })
1031 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1032
1033 // Construct the orbit basis
1034 let orbit_basis = match symmetry_transformation_kind {
1035 SymmetryTransformationKind::Spatial => {
1036 OrbitBasis::builder()
1037 .group(&group)
1038 .origins(origins_c)
1039 .action(|op, det| {
1040 det.sym_transform_spatial(op).with_context(|| {
1041 format!("Unable to apply `{op}` spatially on the origin multi-determinantal wavefunction")
1042 })
1043 })
1044 .build()
1045 }
1046 SymmetryTransformationKind::SpatialWithSpinTimeReversal => {
1047 OrbitBasis::builder()
1048 .group(&group)
1049 .origins(origins_c)
1050 .action(|op, det| {
1051 det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
1052 format!("Unable to apply `{op}` spatially (with spin-including time reversal) on the origin multi-determinantal wavefunction")
1053 })
1054 })
1055 .build()
1056 }
1057 SymmetryTransformationKind::Spin => {
1058 OrbitBasis::builder()
1059 .group(&group)
1060 .origins(origins_c)
1061 .action(|op, det| {
1062 det.sym_transform_spin(op).with_context(|| {
1063 format!("Unable to apply `{op}` spin-wise on the origin multi-determinantal wavefunction")
1064 })
1065 })
1066 .build()
1067 }
1068 SymmetryTransformationKind::SpinSpatial => {
1069 OrbitBasis::builder()
1070 .group(&group)
1071 .origins(origins_c)
1072 .action(|op, det| {
1073 det.sym_transform_spin_spatial(op).with_context(|| {
1074 format!("Unable to apply `{op}` spin-spatially on the origin multi-determinantal wavefunction")
1075 })
1076 })
1077 .build()
1078 }
1079 }.map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1080
1081 // Run non-orthogonal configuration interaction
1082 let dets = orbit_basis
1083 .iter()
1084 .collect::<Result<Vec<_>, _>>()
1085 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1086
1087 let (noci_energies_vec, noci_coeffs_vec) = noci_solver_c_sc(&dets)
1088 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1089 if noci_energies_vec.len() != noci_coeffs_vec.len()
1090 || noci_coeffs_vec
1091 .iter()
1092 .map(|coeffs| coeffs.len())
1093 .collect::<HashSet<_>>()
1094 .len()
1095 != 1
1096 {
1097 return Err(PyRuntimeError::new_err(
1098 "Inconsistent dimensions encountered in NOCI results.",
1099 ));
1100 }
1101 let multidets = noci_energies_vec
1102 .into_iter()
1103 .zip(noci_coeffs_vec)
1104 .map(|(energy, coeffs)| {
1105 MultiDeterminant::builder()
1106 .basis(orbit_basis.clone())
1107 .coefficients(Array1::from_vec(coeffs))
1108 .threshold(1e-7)
1109 .energy(Ok(energy))
1110 .build()
1111 })
1112 .collect::<Result<Vec<_>, _>>()
1113 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1114
1115 let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
1116 UnitaryRepresentedSymmetryGroup,
1117 C128,
1118 _,
1119 SpinConstraint,
1120 >::builder()
1121 .parameters(&mda_params)
1122 .angular_function_parameters(&afa_params)
1123 .multidets(multidets.iter().collect::<Vec<_>>())
1124 .sao(&sao_c)
1125 .sao_h(sao_h_c.as_ref())
1126 .symmetry_group(&pd_res)
1127 .build()
1128 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1129 py.detach(|| {
1130 mda_driver
1131 .run()
1132 .map_err(|err| PyRuntimeError::new_err(err.to_string()))
1133 })?;
1134
1135 // Collect complex multi-determinantal wavefunctions for returning
1136 let basis = multidets
1137 .first()
1138 .and_then(|multidet| {
1139 multidet
1140 .basis()
1141 .iter()
1142 .map(|det_res| det_res.and_then(|det| det.to_python(py)))
1143 .collect::<Result<Vec<_>, _>>()
1144 .ok()
1145 })
1146 .ok_or_else(|| {
1147 PyRuntimeError::new_err(
1148 "Unable to obtain the basis of Slater determinants."
1149 .to_string(),
1150 )
1151 })?;
1152 let (coefficientss, energies): (Vec<_>, Vec<_>) = multidets
1153 .iter()
1154 .map(|multidet| {
1155 let coefficients =
1156 multidet.coefficients().iter().cloned().collect_vec();
1157 let energy =
1158 *multidet.energy().unwrap_or(&Complex::from(f64::NAN));
1159 Ok::<_, PyErr>((coefficients, energy))
1160 })
1161 .collect::<Result<Vec<_>, _>>()?
1162 .into_iter()
1163 .unzip();
1164 let coefficientss_arr = Array2::from_shape_vec(
1165 (basis.len(), coefficientss.len()).f(),
1166 coefficientss.into_iter().flatten().collect_vec(),
1167 )
1168 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
1169 .to_pyarray(py);
1170 let energies_arr = Array1::from_vec(energies).to_pyarray(py);
1171 let density_matrices = density_matrix_calculation_thresholds.and_then(
1172 |(thresh_offdiag, thresh_zeroov)| {
1173 log::debug!("Calculating density matrices...");
1174 let multidets_collection =
1175 MultiDeterminants::from_multideterminant_vec(
1176 &multidets.iter().collect_vec(),
1177 )
1178 .ok()?;
1179 let denmats_opt = multidets_collection
1180 .density_matrices(
1181 &sao_c.view(),
1182 thresh_offdiag,
1183 thresh_zeroov,
1184 true,
1185 )
1186 .map(|denmats| {
1187 denmats
1188 .axis_iter(Axis(0))
1189 .map(|denmat| denmat.to_pyarray(py))
1190 .collect_vec()
1191 })
1192 .ok();
1193 log::debug!("Calculating density matrices... Done.");
1194 denmats_opt
1195 },
1196 );
1197 let pymultidet = PyMultiDeterminantsComplex::new(
1198 basis,
1199 coefficientss_arr,
1200 energies_arr,
1201 density_matrices,
1202 multidets[0].threshold(),
1203 )
1204 .into_py_any(py)?;
1205 Ok(pymultidet)
1206 }
1207 }
1208 }
1209 PyStructureConstraint::SpinOrbitCoupled(_) => {
1210 let origins_c = pyorigins
1211 .iter()
1212 .map(|pydet| match pydet {
1213 PySlaterDeterminant::Real(pydet_r) => pydet_r
1214 .to_qsym2::<SpinOrbitCoupled>(&baos_ref, mol)
1215 .map(|det_r| {
1216 SlaterDeterminant::<C128, SpinOrbitCoupled>::from(det_r)
1217 }),
1218 PySlaterDeterminant::Complex(pydet_c) => {
1219 pydet_c.to_qsym2::<SpinOrbitCoupled>(&baos_ref, mol)
1220 }
1221 })
1222 .collect::<Result<Vec<_>, _>>()
1223 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1224
1225 match &use_magnetic_group {
1226 Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
1227 // Magnetic groups with corepresentations
1228 let group = py
1229 .detach(|| {
1230 let magsym = pd_res
1231 .magnetic_symmetry
1232 .as_ref()
1233 .ok_or(format_err!("Magnetic group required for orbit construction, but no magnetic symmetry found."))?;
1234 if use_double_group {
1235 MagneticRepresentedSymmetryGroup::from_molecular_symmetry(
1236 magsym,
1237 infinite_order_to_finite,
1238 )
1239 .and_then(|grp| grp.to_double_group())
1240 } else {
1241 MagneticRepresentedSymmetryGroup::from_molecular_symmetry(
1242 magsym,
1243 infinite_order_to_finite,
1244 )
1245 }
1246 })
1247 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1248
1249 // Construct the orbit basis
1250 let orbit_basis = match symmetry_transformation_kind {
1251 SymmetryTransformationKind::Spatial => {
1252 OrbitBasis::builder()
1253 .group(&group)
1254 .origins(origins_c)
1255 .action(|op, det| {
1256 det.sym_transform_spatial(op).with_context(|| {
1257 format!("Unable to apply `{op}` spatially on the origin multi-determinantal wavefunction")
1258 })
1259 })
1260 .build()
1261 }
1262 SymmetryTransformationKind::SpatialWithSpinTimeReversal => {
1263 OrbitBasis::builder()
1264 .group(&group)
1265 .origins(origins_c)
1266 .action(|op, det| {
1267 det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
1268 format!("Unable to apply `{op}` spatially (with spin-including time reversal) on the origin multi-determinantal wavefunction")
1269 })
1270 })
1271 .build()
1272 }
1273 SymmetryTransformationKind::Spin => {
1274 OrbitBasis::builder()
1275 .group(&group)
1276 .origins(origins_c)
1277 .action(|op, det| {
1278 det.sym_transform_spin(op).with_context(|| {
1279 format!("Unable to apply `{op}` spin-wise on the origin multi-determinantal wavefunction")
1280 })
1281 })
1282 .build()
1283 }
1284 SymmetryTransformationKind::SpinSpatial => {
1285 OrbitBasis::builder()
1286 .group(&group)
1287 .origins(origins_c)
1288 .action(|op, det| {
1289 det.sym_transform_spin_spatial(op).with_context(|| {
1290 format!("Unable to apply `{op}` spin-spatially on the origin multi-determinantal wavefunction")
1291 })
1292 })
1293 .build()
1294 }
1295 }.map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1296
1297 // Run non-orthogonal configuration interaction
1298 let dets = orbit_basis
1299 .iter()
1300 .collect::<Result<Vec<_>, _>>()
1301 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1302
1303 let (noci_energies_vec, noci_coeffs_vec) = noci_solver_c_soc(&dets)
1304 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1305 if noci_energies_vec.len() != noci_coeffs_vec.len()
1306 || noci_coeffs_vec
1307 .iter()
1308 .map(|coeffs| coeffs.len())
1309 .collect::<HashSet<_>>()
1310 .len()
1311 != 1
1312 {
1313 return Err(PyRuntimeError::new_err(
1314 "Inconsistent dimensions encountered in NOCI results.",
1315 ));
1316 }
1317 let multidets = noci_energies_vec
1318 .into_iter()
1319 .zip(noci_coeffs_vec)
1320 .map(|(energy, coeffs)| {
1321 MultiDeterminant::builder()
1322 .basis(orbit_basis.clone())
1323 .coefficients(Array1::from_vec(coeffs))
1324 .threshold(1e-7)
1325 .energy(Ok(energy))
1326 .build()
1327 })
1328 .collect::<Result<Vec<_>, _>>()
1329 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1330
1331 let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
1332 MagneticRepresentedSymmetryGroup,
1333 C128,
1334 _,
1335 SpinOrbitCoupled,
1336 >::builder()
1337 .parameters(&mda_params)
1338 .angular_function_parameters(&afa_params)
1339 .multidets(multidets.iter().collect::<Vec<_>>())
1340 .sao(&sao_c)
1341 .sao_h(sao_h_c.as_ref())
1342 .symmetry_group(&pd_res)
1343 .build()
1344 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1345 py.detach(|| {
1346 mda_driver
1347 .run()
1348 .map_err(|err| PyRuntimeError::new_err(err.to_string()))
1349 })?;
1350
1351 // Collect complex multi-determinantal wavefunctions for returning
1352 let basis = multidets
1353 .first()
1354 .and_then(|multidet| {
1355 multidet
1356 .basis()
1357 .iter()
1358 .map(|det_res| det_res.and_then(|det| det.to_python(py)))
1359 .collect::<Result<Vec<_>, _>>()
1360 .ok()
1361 })
1362 .ok_or_else(|| {
1363 PyRuntimeError::new_err(
1364 "Unable to obtain the basis of Slater determinants."
1365 .to_string(),
1366 )
1367 })?;
1368 let (coefficientss, energies): (Vec<_>, Vec<_>) = multidets
1369 .iter()
1370 .map(|multidet| {
1371 let coefficients =
1372 multidet.coefficients().iter().cloned().collect_vec();
1373 let energy =
1374 *multidet.energy().unwrap_or(&Complex::from(f64::NAN));
1375 Ok::<_, PyErr>((coefficients, energy))
1376 })
1377 .collect::<Result<Vec<_>, _>>()?
1378 .into_iter()
1379 .unzip();
1380 let coefficientss_arr = Array2::from_shape_vec(
1381 (basis.len(), coefficientss.len()).f(),
1382 coefficientss.into_iter().flatten().collect_vec(),
1383 )
1384 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
1385 .to_pyarray(py);
1386 let energies_arr = Array1::from_vec(energies).to_pyarray(py);
1387 let density_matrices = density_matrix_calculation_thresholds.and_then(
1388 |(thresh_offdiag, thresh_zeroov)| {
1389 log::debug!("Calculating density matrices...");
1390 let multidets_collection =
1391 MultiDeterminants::from_multideterminant_vec(
1392 &multidets.iter().collect_vec(),
1393 )
1394 .ok()?;
1395 let denmats_opt = multidets_collection
1396 .density_matrices(
1397 &sao_c.view(),
1398 thresh_offdiag,
1399 thresh_zeroov,
1400 true,
1401 )
1402 .map(|denmats| {
1403 denmats
1404 .axis_iter(Axis(0))
1405 .map(|denmat| denmat.to_pyarray(py))
1406 .collect_vec()
1407 })
1408 .ok();
1409 log::debug!("Calculating density matrices... Done.");
1410 denmats_opt
1411 },
1412 );
1413 let pymultidet = PyMultiDeterminantsComplex::new(
1414 basis,
1415 coefficientss_arr,
1416 energies_arr,
1417 density_matrices,
1418 multidets[0].threshold(),
1419 )
1420 .into_py_any(py)?;
1421 Ok(pymultidet)
1422 }
1423 Some(MagneticSymmetryAnalysisKind::Representation) | None => {
1424 // Unitary groups or magnetic groups with representations
1425 let group = py
1426 .detach(|| {
1427 let sym = if use_magnetic_group.is_some() {
1428 pd_res
1429 .magnetic_symmetry
1430 .as_ref()
1431 .ok_or(format_err!("Magnetic group required for orbit construction, but no magnetic symmetry found."))?
1432 } else {
1433 &pd_res.unitary_symmetry
1434 };
1435 if use_double_group {
1436 UnitaryRepresentedSymmetryGroup::from_molecular_symmetry(
1437 sym,
1438 infinite_order_to_finite,
1439 )
1440 .and_then(|grp| grp.to_double_group())
1441 } else {
1442 UnitaryRepresentedSymmetryGroup::from_molecular_symmetry(
1443 sym,
1444 infinite_order_to_finite,
1445 )
1446 }
1447 })
1448 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1449
1450 // Construct the orbit basis
1451 let orbit_basis = match symmetry_transformation_kind {
1452 SymmetryTransformationKind::Spatial => {
1453 OrbitBasis::builder()
1454 .group(&group)
1455 .origins(origins_c)
1456 .action(|op, det| {
1457 det.sym_transform_spatial(op).with_context(|| {
1458 format!("Unable to apply `{op}` spatially on the origin multi-determinantal wavefunction")
1459 })
1460 })
1461 .build()
1462 }
1463 SymmetryTransformationKind::SpatialWithSpinTimeReversal => {
1464 OrbitBasis::builder()
1465 .group(&group)
1466 .origins(origins_c)
1467 .action(|op, det| {
1468 det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
1469 format!("Unable to apply `{op}` spatially (with spin-including time reversal) on the origin multi-determinantal wavefunction")
1470 })
1471 })
1472 .build()
1473 }
1474 SymmetryTransformationKind::Spin => {
1475 OrbitBasis::builder()
1476 .group(&group)
1477 .origins(origins_c)
1478 .action(|op, det| {
1479 det.sym_transform_spin(op).with_context(|| {
1480 format!("Unable to apply `{op}` spin-wise on the origin multi-determinantal wavefunction")
1481 })
1482 })
1483 .build()
1484 }
1485 SymmetryTransformationKind::SpinSpatial => {
1486 OrbitBasis::builder()
1487 .group(&group)
1488 .origins(origins_c)
1489 .action(|op, det| {
1490 det.sym_transform_spin_spatial(op).with_context(|| {
1491 format!("Unable to apply `{op}` spin-spatially on the origin multi-determinantal wavefunction")
1492 })
1493 })
1494 .build()
1495 }
1496 }.map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1497
1498 // Run non-orthogonal configuration interaction
1499 let dets = orbit_basis
1500 .iter()
1501 .collect::<Result<Vec<_>, _>>()
1502 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1503
1504 let (noci_energies_vec, noci_coeffs_vec) = noci_solver_c_soc(&dets)
1505 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1506 if noci_energies_vec.len() != noci_coeffs_vec.len()
1507 || noci_coeffs_vec
1508 .iter()
1509 .map(|coeffs| coeffs.len())
1510 .collect::<HashSet<_>>()
1511 .len()
1512 != 1
1513 {
1514 return Err(PyRuntimeError::new_err(
1515 "Inconsistent dimensions encountered in NOCI results.",
1516 ));
1517 }
1518 let multidets = noci_energies_vec
1519 .into_iter()
1520 .zip(noci_coeffs_vec)
1521 .map(|(energy, coeffs)| {
1522 MultiDeterminant::builder()
1523 .basis(orbit_basis.clone())
1524 .coefficients(Array1::from_vec(coeffs))
1525 .threshold(1e-7)
1526 .energy(Ok(energy))
1527 .build()
1528 })
1529 .collect::<Result<Vec<_>, _>>()
1530 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1531
1532 let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
1533 UnitaryRepresentedSymmetryGroup,
1534 C128,
1535 _,
1536 SpinOrbitCoupled,
1537 >::builder()
1538 .parameters(&mda_params)
1539 .angular_function_parameters(&afa_params)
1540 .multidets(multidets.iter().collect::<Vec<_>>())
1541 .sao(&sao_c)
1542 .sao_h(sao_h_c.as_ref())
1543 .symmetry_group(&pd_res)
1544 .build()
1545 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1546 py.detach(|| {
1547 mda_driver
1548 .run()
1549 .map_err(|err| PyRuntimeError::new_err(err.to_string()))
1550 })?;
1551
1552 // Collect complex multi-determinantal wavefunctions for returning
1553 let basis = multidets
1554 .first()
1555 .and_then(|multidet| {
1556 multidet
1557 .basis()
1558 .iter()
1559 .map(|det_res| det_res.and_then(|det| det.to_python(py)))
1560 .collect::<Result<Vec<_>, _>>()
1561 .ok()
1562 })
1563 .ok_or_else(|| {
1564 PyRuntimeError::new_err(
1565 "Unable to obtain the basis of Slater determinants."
1566 .to_string(),
1567 )
1568 })?;
1569 let (coefficientss, energies): (Vec<_>, Vec<_>) = multidets
1570 .iter()
1571 .map(|multidet| {
1572 let coefficients =
1573 multidet.coefficients().iter().cloned().collect_vec();
1574 let energy =
1575 *multidet.energy().unwrap_or(&Complex::from(f64::NAN));
1576 Ok::<_, PyErr>((coefficients, energy))
1577 })
1578 .collect::<Result<Vec<_>, _>>()?
1579 .into_iter()
1580 .unzip();
1581 let coefficientss_arr = Array2::from_shape_vec(
1582 (basis.len(), coefficientss.len()).f(),
1583 coefficientss.into_iter().flatten().collect_vec(),
1584 )
1585 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
1586 .to_pyarray(py);
1587 let energies_arr = Array1::from_vec(energies).to_pyarray(py);
1588 let density_matrices = density_matrix_calculation_thresholds.and_then(
1589 |(thresh_offdiag, thresh_zeroov)| {
1590 log::debug!("Calculating density matrices...");
1591 let multidets_collection =
1592 MultiDeterminants::from_multideterminant_vec(
1593 &multidets.iter().collect_vec(),
1594 )
1595 .ok()?;
1596 let denmats_opt = multidets_collection
1597 .density_matrices(
1598 &sao_c.view(),
1599 thresh_offdiag,
1600 thresh_zeroov,
1601 true,
1602 )
1603 .map(|denmats| {
1604 denmats
1605 .axis_iter(Axis(0))
1606 .map(|denmat| denmat.to_pyarray(py))
1607 .collect_vec()
1608 })
1609 .ok();
1610 log::debug!("Calculating density matrices... Done.");
1611 denmats_opt
1612 },
1613 );
1614 let pymultidet = PyMultiDeterminantsComplex::new(
1615 basis,
1616 coefficientss_arr,
1617 energies_arr,
1618 density_matrices,
1619 multidets[0].threshold(),
1620 )
1621 .into_py_any(py)?;
1622 Ok(pymultidet)
1623 }
1624 }
1625 }
1626 }
1627 }
1628 }
1629}