Skip to main content

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}