qsym2/bindings/python/representation_analysis/
multideterminant.rs

1//! Python bindings for QSym² symmetry analysis of Slater determinants.
2
3use std::collections::HashSet;
4use std::path::PathBuf;
5
6use anyhow::{bail, format_err, Context};
7use ndarray::Array1;
8use num_complex::Complex;
9use numpy::{PyArray1, PyArray2, PyArrayMethods};
10use pyo3::exceptions::{PyIOError, PyRuntimeError};
11use pyo3::prelude::*;
12use pyo3::types::PyFunction;
13
14use crate::analysis::EigenvalueComparisonMode;
15use crate::angmom::spinor_rotation_3d::SpinConstraint;
16use crate::bindings::python::integrals::PyBasisAngularOrder;
17use crate::bindings::python::representation_analysis::slater_determinant::{
18    PySlaterDeterminant, PySlaterDeterminantComplex, PySlaterDeterminantReal,
19};
20use crate::bindings::python::representation_analysis::{PyArray1RC, PyArray2RC};
21use crate::drivers::representation_analysis::angular_function::AngularFunctionRepAnalysisParams;
22use crate::drivers::representation_analysis::multideterminant::{
23    MultiDeterminantRepAnalysisDriver, MultiDeterminantRepAnalysisParams,
24};
25use crate::drivers::representation_analysis::{
26    CharacterTableDisplay, MagneticSymmetryAnalysisKind,
27};
28use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
29use crate::drivers::QSym2Driver;
30use crate::io::format::qsym2_output;
31use crate::io::{read_qsym2_binary, QSym2FileType};
32use crate::symmetry::symmetry_group::{
33    MagneticRepresentedSymmetryGroup, SymmetryGroupProperties, UnitaryRepresentedSymmetryGroup,
34};
35use crate::symmetry::symmetry_transformation::{SymmetryTransformable, SymmetryTransformationKind};
36use crate::target::determinant::SlaterDeterminant;
37use crate::target::noci::basis::{Basis, EagerBasis, OrbitBasis};
38use crate::target::noci::multideterminant::MultiDeterminant;
39
40type C128 = Complex<f64>;
41
42// =====================
43// Functions definitions
44// =====================
45
46/// Python-exposed function to perform representation symmetry analysis for real and complex
47/// multi-determinantal wavefunctions constructed from group-generated orbits and log the result via
48/// the `qsym2-output` logger at the `INFO` level.
49///
50/// If `symmetry_transformation_kind` includes spin transformation, the provided
51/// multi-determinantal wavefunctions will be augmented to generalised spin constraint
52/// automatically.
53///
54/// # Arguments
55///
56/// * `inp_sym` - A path to the [`QSym2FileType::Sym`] file containing the symmetry-group detection
57/// result for the system. This will be used to construct abstract groups and character tables for
58/// representation analysis. Python type: `str`.
59/// * `pyorigins` - A list of Python-exposed Slater determinants whose coefficients are of type
60/// `float64` or `complex128`. These determinants serve as origins for group-generated orbits which
61/// serve as basis states for non-orthogonal configuration interaction to yield multi-determinantal
62/// wavefunctions, the symmetry of which will be analysed by this function.
63/// Python type: `list[PySlaterDeterminantReal | PySlaterDeterminantComplex]`.
64/// * `py_noci_solver` - A Python function callable on a sequence of Slater determinants to perform
65/// non-orthogonal configuration interaction (NOCI) and return a list of NOCI energies and a
66/// corresponding list of lists of linear combination coefficients, where each inner list is for one
67/// multi-determinantal wavefunction resulting from the NOCI calculation.
68/// Python type: `Callable[[list[PySlaterDeterminantReal | PySlaterDeterminantComplex]], tuple[list[float], list[list[float]]] | tuple[list[complex], list[list[complex]]]]`.
69/// * `pybao` - A Python-exposed Python-exposed structure containing basis angular order information.
70/// Python type: `PyBasisAngularOrder`.
71/// * `integrality_threshold` - The threshold for verifying if subspace multiplicities are
72/// integral. Python type: `float`.
73/// * `linear_independence_threshold` - The threshold for determining the linear independence
74/// subspace via the non-zero eigenvalues of the orbit overlap matrix. Python type: `float`.
75/// * `use_magnetic_group` - An option indicating if the magnetic group is to be used for symmetry
76/// analysis, and if so, whether unitary representations or unitary-antiunitary corepresentations
77/// should be used. Python type: `None | MagneticSymmetryAnalysisKind`.
78/// * `use_double_group` - A boolean indicating if the double group of the prevailing symmetry
79/// group is to be used for representation analysis instead. Python type: `bool`.
80/// * `use_cayley_table` - A boolean indicating if the Cayley table for the group, if available,
81/// should be used to speed up the calculation of orbit overlap matrices. Python type: `bool`.
82/// * `symmetry_transformation_kind` - An enumerated type indicating the type of symmetry
83/// transformations to be performed on the origin determinant to generate the orbit. If this
84/// contains spin transformation, the determinant will be augmented to generalised spin constraint
85/// automatically. Python type: `SymmetryTransformationKind`.
86/// * `eigenvalue_comparison_mode` - An enumerated type indicating the mode of comparison of orbit
87/// overlap eigenvalues with the specified `linear_independence_threshold`.
88/// Python type: `EigenvalueComparisonMode`.
89/// * `sao` - The atomic-orbital overlap matrix whose elements are of type `float64` or
90/// `complex128`. Python type: `numpy.2darray[float] | numpy.2darray[complex]`.
91/// * `sao_h` - The optional complex-symmetric atomic-orbital overlap matrix whose elements
92/// are of type `float64` or `complex128`. This is required if antiunitary symmetry operations are
93/// involved. Python type: `None | numpy.2darray[float] | numpy.2darray[complex]`.
94/// * `write_overlap_eigenvalues` - A boolean indicating if the eigenvalues of the determinant
95/// orbit overlap matrix are to be written to the output. Python type: `bool`.
96/// * `write_character_table` - A boolean indicating if the character table of the prevailing
97/// symmetry group is to be printed out. Python type: `bool`.
98/// * `infinite_order_to_finite` - The finite order with which infinite-order generators are to be
99/// interpreted to form a finite subgroup of the prevailing infinite group. This finite subgroup
100/// will be used for symmetry analysis. Python type: `Optional[int]`.
101/// * `angular_function_integrality_threshold` - The threshold for verifying if subspace
102/// multiplicities are integral for the symmetry analysis of angular functions. Python type:
103/// `float`.
104/// * `angular_function_linear_independence_threshold` - The threshold for determining the linear
105/// independence subspace via the non-zero eigenvalues of the orbit overlap matrix for the symmetry
106/// analysis of angular functions. Python type: `float`.
107/// * `angular_function_max_angular_momentum` - The maximum angular momentum order to be used in
108/// angular function symmetry analysis. Python type: `int`.
109#[pyfunction]
110#[pyo3(signature = (
111    inp_sym,
112    pyorigins,
113    py_noci_solver,
114    pybao,
115    integrality_threshold,
116    linear_independence_threshold,
117    use_magnetic_group,
118    use_double_group,
119    use_cayley_table,
120    symmetry_transformation_kind,
121    eigenvalue_comparison_mode,
122    sao,
123    sao_h=None,
124    write_overlap_eigenvalues=true,
125    write_character_table=true,
126    infinite_order_to_finite=None,
127    angular_function_integrality_threshold=1e-7,
128    angular_function_linear_independence_threshold=1e-7,
129    angular_function_max_angular_momentum=2
130))]
131pub fn rep_analyse_multideterminants_orbit_basis(
132    py: Python<'_>,
133    inp_sym: PathBuf,
134    pyorigins: Vec<PySlaterDeterminant>,
135    py_noci_solver: Py<PyFunction>,
136    pybao: &PyBasisAngularOrder,
137    integrality_threshold: f64,
138    linear_independence_threshold: f64,
139    use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
140    use_double_group: bool,
141    use_cayley_table: bool,
142    symmetry_transformation_kind: SymmetryTransformationKind,
143    eigenvalue_comparison_mode: EigenvalueComparisonMode,
144    sao: PyArray2RC,
145    sao_h: Option<PyArray2RC>,
146    write_overlap_eigenvalues: bool,
147    write_character_table: bool,
148    infinite_order_to_finite: Option<u32>,
149    angular_function_integrality_threshold: f64,
150    angular_function_linear_independence_threshold: f64,
151    angular_function_max_angular_momentum: u32,
152) -> PyResult<()> {
153    // Read in point-group detection results
154    let pd_res: SymmetryGroupDetectionResult =
155        read_qsym2_binary(inp_sym.clone(), QSym2FileType::Sym)
156            .map_err(|err| PyIOError::new_err(err.to_string()))?;
157
158    let mut file_name = inp_sym.to_path_buf();
159    file_name.set_extension(QSym2FileType::Sym.ext());
160    qsym2_output!(
161        "Symmetry-group detection results read in from {}.",
162        file_name.display(),
163    );
164    qsym2_output!("");
165
166    // Set up basic parameters
167    let mol = &pd_res.pre_symmetry.recentred_molecule;
168    let bao = pybao
169        .to_qsym2(mol)
170        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
171    let augment_to_generalised = match symmetry_transformation_kind {
172        SymmetryTransformationKind::SpatialWithSpinTimeReversal
173        | SymmetryTransformationKind::Spin
174        | SymmetryTransformationKind::SpinSpatial => true,
175        SymmetryTransformationKind::Spatial => false,
176    };
177    let afa_params = AngularFunctionRepAnalysisParams::builder()
178        .integrality_threshold(angular_function_integrality_threshold)
179        .linear_independence_threshold(angular_function_linear_independence_threshold)
180        .max_angular_momentum(angular_function_max_angular_momentum)
181        .build()
182        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
183    let mda_params = MultiDeterminantRepAnalysisParams::<f64>::builder()
184        .integrality_threshold(integrality_threshold)
185        .linear_independence_threshold(linear_independence_threshold)
186        .use_magnetic_group(use_magnetic_group.clone())
187        .use_double_group(use_double_group)
188        .use_cayley_table(use_cayley_table)
189        .symmetry_transformation_kind(symmetry_transformation_kind.clone())
190        .eigenvalue_comparison_mode(eigenvalue_comparison_mode)
191        .write_overlap_eigenvalues(write_overlap_eigenvalues)
192        .write_character_table(if write_character_table {
193            Some(CharacterTableDisplay::Symbolic)
194        } else {
195            None
196        })
197        .infinite_order_to_finite(infinite_order_to_finite)
198        .build()
199        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
200
201    // Set up NOCI function
202    let noci_solver_r = |multidets: &Vec<SlaterDeterminant<f64, SpinConstraint>>| {
203        Python::with_gil(|py_inner| {
204            let pymultidets = multidets
205                .iter()
206                .map(|det| {
207                    let pysc = det.structure_constraint().clone().try_into()?;
208                    Ok(PySlaterDeterminantReal::new(
209                        pysc,
210                        det.complex_symmetric(),
211                        det.coefficients()
212                            .iter()
213                            .map(|arr| PyArray2::from_array(py_inner, arr))
214                            .collect::<Vec<_>>(),
215                        det.occupations()
216                            .iter()
217                            .map(|arr| PyArray1::from_array(py_inner, arr))
218                            .collect::<Vec<_>>(),
219                        det.threshold(),
220                        det.mo_energies().map(|mo_energies| {
221                            mo_energies
222                                .iter()
223                                .map(|arr| PyArray1::from_array(py_inner, arr))
224                                .collect::<Vec<_>>()
225                        }),
226                        det.energy().ok().cloned(),
227                    ))
228                })
229                .collect::<Result<Vec<_>, anyhow::Error>>()
230                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
231            py_noci_solver
232                .call1(py_inner, (pymultidets,))
233                .and_then(|res| res.extract::<(Vec<f64>, Vec<Vec<f64>>)>(py_inner))
234        })
235    };
236    let noci_solver_c = |multidets: &Vec<SlaterDeterminant<C128, SpinConstraint>>| {
237        Python::with_gil(|py_inner| {
238            let pymultidets = multidets
239                .iter()
240                .map(|det| {
241                    let pysc = det.structure_constraint().clone().try_into()?;
242                    Ok(PySlaterDeterminantComplex::new(
243                        pysc,
244                        det.complex_symmetric(),
245                        det.coefficients()
246                            .iter()
247                            .map(|arr| PyArray2::from_array(py_inner, arr))
248                            .collect::<Vec<_>>(),
249                        det.occupations()
250                            .iter()
251                            .map(|arr| PyArray1::from_array(py_inner, arr))
252                            .collect::<Vec<_>>(),
253                        det.threshold(),
254                        det.mo_energies().map(|mo_energies| {
255                            mo_energies
256                                .iter()
257                                .map(|arr| PyArray1::from_array(py_inner, arr))
258                                .collect::<Vec<_>>()
259                        }),
260                        det.energy().ok().cloned(),
261                    ))
262                })
263                .collect::<Result<Vec<_>, anyhow::Error>>()
264                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
265            py_noci_solver
266                .call1(py_inner, (pymultidets,))
267                .and_then(|res| res.extract::<(Vec<C128>, Vec<Vec<C128>>)>(py_inner))
268        })
269    };
270
271    let all_real = pyorigins
272        .iter()
273        .all(|pyorigin| matches!(pyorigin, PySlaterDeterminant::Real(_)));
274
275    match (all_real, &sao) {
276        (true, PyArray2RC::Real(pysao_r)) => {
277            // Real numeric data type
278
279            // Preparation
280            let sao = pysao_r.to_owned_array();
281            let origins_r = if augment_to_generalised {
282                pyorigins
283                    .iter()
284                    .map(|pydet| {
285                        if let PySlaterDeterminant::Real(pydet_r) = pydet {
286                            pydet_r
287                                .to_qsym2(&bao, mol)
288                                .map(|det_r| det_r.to_generalised())
289                        } else {
290                            bail!("Unexpected complex type for an origin Slater determinant.")
291                        }
292                    })
293                    .collect::<Result<Vec<_>, _>>()
294            } else {
295                pyorigins
296                    .iter()
297                    .map(|pydet| {
298                        if let PySlaterDeterminant::Real(pydet_r) = pydet {
299                            pydet_r.to_qsym2(&bao, mol)
300                        } else {
301                            bail!("Unexpected complex type for an origin Slater determinant.")
302                        }
303                    })
304                    .collect::<Result<Vec<_>, _>>()
305            }
306            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
307
308            match &use_magnetic_group {
309                Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
310                    // Magnetic groups with corepresentations
311                    let group = py
312                        .allow_threads(|| {
313                            let magsym = pd_res
314                                .magnetic_symmetry
315                                .as_ref()
316                                .ok_or(format_err!("Magnetic group required for orbit construction, but no magnetic symmetry found."))?;
317                            if use_double_group {
318                                MagneticRepresentedSymmetryGroup::from_molecular_symmetry(
319                                    magsym,
320                                    infinite_order_to_finite,
321                                )
322                                .and_then(|grp| grp.to_double_group())
323                            } else {
324                                MagneticRepresentedSymmetryGroup::from_molecular_symmetry(
325                                    magsym,
326                                    infinite_order_to_finite,
327                                )
328                            }
329                        })
330                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
331
332                    // Construct the orbit basis
333                    let orbit_basis = match symmetry_transformation_kind {
334                        SymmetryTransformationKind::Spatial => {
335                            OrbitBasis::builder()
336                            .group(&group)
337                            .origins(origins_r)
338                            .action(|op, det| {
339                                det.sym_transform_spatial(op).with_context(|| {
340                                    format!("Unable to apply `{op}` spatially on the origin multi-determinantal wavefunction")
341                                })
342                            })
343                            .build()
344                        }
345                        SymmetryTransformationKind::SpatialWithSpinTimeReversal => {
346                            OrbitBasis::builder()
347                            .group(&group)
348                            .origins(origins_r)
349                            .action(|op, det| {
350                                 det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
351                                    format!("Unable to apply `{op}` spatially (with spin-including time reversal) on the origin multi-determinantal wavefunction")
352                                })
353                            })
354                            .build()
355                        }
356                        SymmetryTransformationKind::Spin => {
357                            OrbitBasis::builder()
358                            .group(&group)
359                            .origins(origins_r)
360                            .action(|op, det| {
361                                 det.sym_transform_spin(op).with_context(|| {
362                                    format!("Unable to apply `{op}` spin-wise on the origin multi-determinantal wavefunction")
363                                })
364                            })
365                            .build()
366                        }
367                        SymmetryTransformationKind::SpinSpatial => {
368                            OrbitBasis::builder()
369                            .group(&group)
370                            .origins(origins_r)
371                            .action(|op, det| {
372                                 det.sym_transform_spin_spatial(op).with_context(|| {
373                                    format!("Unable to apply `{op}` spin-spatially on the origin multi-determinantal wavefunction")
374                                })
375                            })
376                            .build()
377                        }
378                    }.map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
379
380                    // Run non-orthogonal configuration interaction
381                    let dets = orbit_basis
382                        .iter()
383                        .collect::<Result<Vec<_>, _>>()
384                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
385
386                    let (noci_energies_vec, noci_coeffs_vec) = noci_solver_r(&dets)
387                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
388                    if noci_energies_vec.len() != noci_coeffs_vec.len()
389                        || noci_coeffs_vec
390                            .iter()
391                            .map(|coeffs| coeffs.len())
392                            .collect::<HashSet<_>>()
393                            .len()
394                            != 1
395                    {
396                        return Err(PyRuntimeError::new_err(
397                            "Inconsistent dimensions encountered in NOCI results.",
398                        ));
399                    }
400                    let multidets = noci_energies_vec
401                        .into_iter()
402                        .zip(noci_coeffs_vec.into_iter())
403                        .map(|(energy, coeffs)| {
404                            MultiDeterminant::builder()
405                                .basis(orbit_basis.clone())
406                                .coefficients(Array1::from_vec(coeffs))
407                                .threshold(1e-7)
408                                .energy(Ok(energy))
409                                .build()
410                        })
411                        .collect::<Result<Vec<_>, _>>()
412                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
413
414                    let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
415                        MagneticRepresentedSymmetryGroup,
416                        f64,
417                        _,
418                        SpinConstraint,
419                    >::builder()
420                    .parameters(&mda_params)
421                    .angular_function_parameters(&afa_params)
422                    .multidets(multidets.iter().collect::<Vec<_>>())
423                    .sao(&sao)
424                    .sao_h(None) // Real SAO.
425                    .symmetry_group(&pd_res)
426                    .build()
427                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
428                    py.allow_threads(|| {
429                        mda_driver
430                            .run()
431                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
432                    })?
433                }
434                Some(MagneticSymmetryAnalysisKind::Representation) | None => {
435                    // Unitary groups or magnetic groups with representations
436                    let group = py
437                        .allow_threads(|| {
438                            let sym = if use_magnetic_group.is_some() {
439                                pd_res
440                                    .magnetic_symmetry
441                                    .as_ref()
442                                    .ok_or(format_err!("Magnetic group required for orbit construction, but no magnetic symmetry found."))?
443                            } else {
444                                &pd_res.unitary_symmetry
445                            };
446                            if use_double_group {
447                                UnitaryRepresentedSymmetryGroup::from_molecular_symmetry(
448                                    sym,
449                                    infinite_order_to_finite,
450                                )
451                                .and_then(|grp| grp.to_double_group())
452                            } else {
453                                UnitaryRepresentedSymmetryGroup::from_molecular_symmetry(
454                                    sym,
455                                    infinite_order_to_finite,
456                                )
457                            }
458                        })
459                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
460
461                    // Construct the orbit basis
462                    let orbit_basis = match symmetry_transformation_kind {
463                        SymmetryTransformationKind::Spatial => {
464                            OrbitBasis::builder()
465                            .group(&group)
466                            .origins(origins_r)
467                            .action(|op, det| {
468                                det.sym_transform_spatial(op).with_context(|| {
469                                    format!("Unable to apply `{op}` spatially on the origin multi-determinantal wavefunction")
470                                })
471                            })
472                            .build()
473                        }
474                        SymmetryTransformationKind::SpatialWithSpinTimeReversal => {
475                            OrbitBasis::builder()
476                            .group(&group)
477                            .origins(origins_r)
478                            .action(|op, det| {
479                                 det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
480                                    format!("Unable to apply `{op}` spatially (with spin-including time reversal) on the origin multi-determinantal wavefunction")
481                                })
482                            })
483                            .build()
484                        }
485                        SymmetryTransformationKind::Spin => {
486                            OrbitBasis::builder()
487                            .group(&group)
488                            .origins(origins_r)
489                            .action(|op, det| {
490                                 det.sym_transform_spin(op).with_context(|| {
491                                    format!("Unable to apply `{op}` spin-wise on the origin multi-determinantal wavefunction")
492                                })
493                            })
494                            .build()
495                        }
496                        SymmetryTransformationKind::SpinSpatial => {
497                            OrbitBasis::builder()
498                            .group(&group)
499                            .origins(origins_r)
500                            .action(|op, det| {
501                                 det.sym_transform_spin_spatial(op).with_context(|| {
502                                    format!("Unable to apply `{op}` spin-spatially on the origin multi-determinantal wavefunction")
503                                })
504                            })
505                            .build()
506                        }
507                    }.map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
508
509                    // Run non-orthogonal configuration interaction
510                    let dets = orbit_basis
511                        .iter()
512                        .collect::<Result<Vec<_>, _>>()
513                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
514
515                    let (noci_energies_vec, noci_coeffs_vec) = noci_solver_r(&dets)
516                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
517                    if noci_energies_vec.len() != noci_coeffs_vec.len()
518                        || noci_coeffs_vec
519                            .iter()
520                            .map(|coeffs| coeffs.len())
521                            .collect::<HashSet<_>>()
522                            .len()
523                            != 1
524                    {
525                        return Err(PyRuntimeError::new_err(
526                            "Inconsistent dimensions encountered in NOCI results.",
527                        ));
528                    }
529                    let multidets = noci_energies_vec
530                        .into_iter()
531                        .zip(noci_coeffs_vec.into_iter())
532                        .map(|(energy, coeffs)| {
533                            MultiDeterminant::builder()
534                                .basis(orbit_basis.clone())
535                                .coefficients(Array1::from_vec(coeffs))
536                                .threshold(1e-7)
537                                .energy(Ok(energy))
538                                .build()
539                        })
540                        .collect::<Result<Vec<_>, _>>()
541                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
542
543                    let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
544                        UnitaryRepresentedSymmetryGroup,
545                        f64,
546                        _,
547                        SpinConstraint,
548                    >::builder()
549                    .parameters(&mda_params)
550                    .angular_function_parameters(&afa_params)
551                    .multidets(multidets.iter().collect::<Vec<_>>())
552                    .sao(&sao)
553                    .sao_h(None) // Real SAO.
554                    .symmetry_group(&pd_res)
555                    .build()
556                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
557                    py.allow_threads(|| {
558                        mda_driver
559                            .run()
560                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
561                    })?
562                }
563            };
564        }
565        (_, _) => {
566            // Complex numeric data type
567
568            // Preparation
569            let sao_c = match sao {
570                PyArray2RC::Real(pysao_r) => pysao_r.to_owned_array().mapv(Complex::from),
571                PyArray2RC::Complex(pysao_c) => pysao_c.to_owned_array(),
572            };
573            let sao_h_c = sao_h.and_then(|pysao_h| match pysao_h {
574                // sao_h must have the same reality as sao.
575                PyArray2RC::Real(pysao_h_r) => Some(pysao_h_r.to_owned_array().mapv(Complex::from)),
576                PyArray2RC::Complex(pysao_h_c) => Some(pysao_h_c.to_owned_array()),
577            });
578            let origins_c = if augment_to_generalised {
579                pyorigins
580                    .iter()
581                    .map(|pydet| match pydet {
582                        PySlaterDeterminant::Real(pydet_r) => {
583                            pydet_r.to_qsym2::<SpinConstraint>(&bao, mol).map(|det_r| {
584                                SlaterDeterminant::<C128, SpinConstraint>::from(det_r)
585                                    .to_generalised()
586                            })
587                        }
588                        PySlaterDeterminant::Complex(pydet_c) => pydet_c
589                            .to_qsym2::<SpinConstraint>(&bao, mol)
590                            .map(|det_c| det_c.to_generalised()),
591                    })
592                    .collect::<Result<Vec<_>, _>>()
593            } else {
594                pyorigins
595                    .iter()
596                    .map(|pydet| match pydet {
597                        PySlaterDeterminant::Real(pydet_r) => pydet_r
598                            .to_qsym2::<SpinConstraint>(&bao, mol)
599                            .map(|det_r| SlaterDeterminant::<C128, SpinConstraint>::from(det_r)),
600                        PySlaterDeterminant::Complex(pydet_c) => {
601                            pydet_c.to_qsym2::<SpinConstraint>(&bao, mol)
602                        }
603                    })
604                    .collect::<Result<Vec<_>, _>>()
605            }
606            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
607
608            match &use_magnetic_group {
609                Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
610                    // Magnetic groups with corepresentations
611                    let group = py
612                        .allow_threads(|| {
613                            let magsym = pd_res
614                                .magnetic_symmetry
615                                .as_ref()
616                                .ok_or(format_err!("Magnetic group required for orbit construction, but no magnetic symmetry found."))?;
617                            if use_double_group {
618                                MagneticRepresentedSymmetryGroup::from_molecular_symmetry(
619                                    magsym,
620                                    infinite_order_to_finite,
621                                )
622                                .and_then(|grp| grp.to_double_group())
623                            } else {
624                                MagneticRepresentedSymmetryGroup::from_molecular_symmetry(
625                                    magsym,
626                                    infinite_order_to_finite,
627                                )
628                            }
629                        })
630                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
631
632                    // Construct the orbit basis
633                    let orbit_basis = match symmetry_transformation_kind {
634                        SymmetryTransformationKind::Spatial => {
635                            OrbitBasis::builder()
636                            .group(&group)
637                            .origins(origins_c)
638                            .action(|op, det| {
639                                det.sym_transform_spatial(op).with_context(|| {
640                                    format!("Unable to apply `{op}` spatially on the origin multi-determinantal wavefunction")
641                                })
642                            })
643                            .build()
644                        }
645                        SymmetryTransformationKind::SpatialWithSpinTimeReversal => {
646                            OrbitBasis::builder()
647                            .group(&group)
648                            .origins(origins_c)
649                            .action(|op, det| {
650                                 det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
651                                    format!("Unable to apply `{op}` spatially (with spin-including time reversal) on the origin multi-determinantal wavefunction")
652                                })
653                            })
654                            .build()
655                        }
656                        SymmetryTransformationKind::Spin => {
657                            OrbitBasis::builder()
658                            .group(&group)
659                            .origins(origins_c)
660                            .action(|op, det| {
661                                 det.sym_transform_spin(op).with_context(|| {
662                                    format!("Unable to apply `{op}` spin-wise on the origin multi-determinantal wavefunction")
663                                })
664                            })
665                            .build()
666                        }
667                        SymmetryTransformationKind::SpinSpatial => {
668                            OrbitBasis::builder()
669                            .group(&group)
670                            .origins(origins_c)
671                            .action(|op, det| {
672                                 det.sym_transform_spin_spatial(op).with_context(|| {
673                                    format!("Unable to apply `{op}` spin-spatially on the origin multi-determinantal wavefunction")
674                                })
675                            })
676                            .build()
677                        }
678                    }.map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
679
680                    // Run non-orthogonal configuration interaction
681                    let dets = orbit_basis
682                        .iter()
683                        .collect::<Result<Vec<_>, _>>()
684                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
685
686                    let (noci_energies_vec, noci_coeffs_vec) = noci_solver_c(&dets)
687                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
688                    if noci_energies_vec.len() != noci_coeffs_vec.len()
689                        || noci_coeffs_vec
690                            .iter()
691                            .map(|coeffs| coeffs.len())
692                            .collect::<HashSet<_>>()
693                            .len()
694                            != 1
695                    {
696                        return Err(PyRuntimeError::new_err(
697                            "Inconsistent dimensions encountered in NOCI results.",
698                        ));
699                    }
700                    let multidets = noci_energies_vec
701                        .into_iter()
702                        .zip(noci_coeffs_vec.into_iter())
703                        .map(|(energy, coeffs)| {
704                            MultiDeterminant::builder()
705                                .basis(orbit_basis.clone())
706                                .coefficients(Array1::from_vec(coeffs))
707                                .threshold(1e-7)
708                                .energy(Ok(energy))
709                                .build()
710                        })
711                        .collect::<Result<Vec<_>, _>>()
712                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
713
714                    let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
715                        MagneticRepresentedSymmetryGroup,
716                        C128,
717                        _,
718                        SpinConstraint,
719                    >::builder()
720                    .parameters(&mda_params)
721                    .angular_function_parameters(&afa_params)
722                    .multidets(multidets.iter().collect::<Vec<_>>())
723                    .sao(&sao_c)
724                    .sao_h(sao_h_c.as_ref())
725                    .symmetry_group(&pd_res)
726                    .build()
727                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
728                    py.allow_threads(|| {
729                        mda_driver
730                            .run()
731                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
732                    })?
733                }
734                Some(MagneticSymmetryAnalysisKind::Representation) | None => {
735                    // Unitary groups or magnetic groups with representations
736                    let group = py
737                        .allow_threads(|| {
738                            let sym = if use_magnetic_group.is_some() {
739                                pd_res
740                                    .magnetic_symmetry
741                                    .as_ref()
742                                    .ok_or(format_err!("Magnetic group required for orbit construction, but no magnetic symmetry found."))?
743                            } else {
744                                &pd_res.unitary_symmetry
745                            };
746                            if use_double_group {
747                                UnitaryRepresentedSymmetryGroup::from_molecular_symmetry(
748                                    sym,
749                                    infinite_order_to_finite,
750                                )
751                                .and_then(|grp| grp.to_double_group())
752                            } else {
753                                UnitaryRepresentedSymmetryGroup::from_molecular_symmetry(
754                                    sym,
755                                    infinite_order_to_finite,
756                                )
757                            }
758                        })
759                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
760
761                    // Construct the orbit basis
762                    let orbit_basis = match symmetry_transformation_kind {
763                        SymmetryTransformationKind::Spatial => {
764                            OrbitBasis::builder()
765                            .group(&group)
766                            .origins(origins_c)
767                            .action(|op, det| {
768                                det.sym_transform_spatial(op).with_context(|| {
769                                    format!("Unable to apply `{op}` spatially on the origin multi-determinantal wavefunction")
770                                })
771                            })
772                            .build()
773                        }
774                        SymmetryTransformationKind::SpatialWithSpinTimeReversal => {
775                            OrbitBasis::builder()
776                            .group(&group)
777                            .origins(origins_c)
778                            .action(|op, det| {
779                                 det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
780                                    format!("Unable to apply `{op}` spatially (with spin-including time reversal) on the origin multi-determinantal wavefunction")
781                                })
782                            })
783                            .build()
784                        }
785                        SymmetryTransformationKind::Spin => {
786                            OrbitBasis::builder()
787                            .group(&group)
788                            .origins(origins_c)
789                            .action(|op, det| {
790                                 det.sym_transform_spin(op).with_context(|| {
791                                    format!("Unable to apply `{op}` spin-wise on the origin multi-determinantal wavefunction")
792                                })
793                            })
794                            .build()
795                        }
796                        SymmetryTransformationKind::SpinSpatial => {
797                            OrbitBasis::builder()
798                            .group(&group)
799                            .origins(origins_c)
800                            .action(|op, det| {
801                                 det.sym_transform_spin_spatial(op).with_context(|| {
802                                    format!("Unable to apply `{op}` spin-spatially on the origin multi-determinantal wavefunction")
803                                })
804                            })
805                            .build()
806                        }
807                    }.map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
808
809                    // Run non-orthogonal configuration interaction
810                    let dets = orbit_basis
811                        .iter()
812                        .collect::<Result<Vec<_>, _>>()
813                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
814
815                    let (noci_energies_vec, noci_coeffs_vec) = noci_solver_c(&dets)
816                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
817                    if noci_energies_vec.len() != noci_coeffs_vec.len()
818                        || noci_coeffs_vec
819                            .iter()
820                            .map(|coeffs| coeffs.len())
821                            .collect::<HashSet<_>>()
822                            .len()
823                            != 1
824                    {
825                        return Err(PyRuntimeError::new_err(
826                            "Inconsistent dimensions encountered in NOCI results.",
827                        ));
828                    }
829                    let multidets = noci_energies_vec
830                        .into_iter()
831                        .zip(noci_coeffs_vec.into_iter())
832                        .map(|(energy, coeffs)| {
833                            MultiDeterminant::builder()
834                                .basis(orbit_basis.clone())
835                                .coefficients(Array1::from_vec(coeffs))
836                                .threshold(1e-7)
837                                .energy(Ok(energy))
838                                .build()
839                        })
840                        .collect::<Result<Vec<_>, _>>()
841                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
842
843                    let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
844                        UnitaryRepresentedSymmetryGroup,
845                        C128,
846                        _,
847                        SpinConstraint,
848                    >::builder()
849                    .parameters(&mda_params)
850                    .angular_function_parameters(&afa_params)
851                    .multidets(multidets.iter().collect::<Vec<_>>())
852                    .sao(&sao_c)
853                    .sao_h(sao_h_c.as_ref())
854                    .symmetry_group(&pd_res)
855                    .build()
856                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
857                    py.allow_threads(|| {
858                        mda_driver
859                            .run()
860                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
861                    })?
862                }
863            };
864        }
865    }
866    Ok(())
867}
868
869/// Python-exposed function to perform representation symmetry analysis for real and complex
870/// multi-determinantal wavefunctions constructed from an eager basis of Slater determinants and log
871/// the result via the `qsym2-output` logger at the `INFO` level.
872///
873/// If `symmetry_transformation_kind` includes spin transformation, the provided
874/// multi-determinantal wavefunctions will be augmented to generalised spin constraint
875/// automatically.
876///
877/// # Arguments
878///
879/// * `inp_sym` - A path to the [`QSym2FileType::Sym`] file containing the symmetry-group detection
880/// result for the system. This will be used to construct abstract groups and character tables for
881/// representation analysis. Python type: `str`.
882/// * `pydets` - A list of Python-exposed Slater determinants whose coefficients are of type
883/// `float64` or `complex128`. These determinants serve as basis states for non-orthogonal
884/// configuration interaction to yield multi-determinantal wavefunctions, the symmetry of which will
885/// be analysed by this function.
886/// Python type: `list[PySlaterDeterminantReal | PySlaterDeterminantComplex]`.
887/// * `coefficients` - The coefficient matrix where each column gives the linear combination
888/// coefficients for one multi-determinantal wavefunction. The number of rows must match the number
889/// of determinants specified in `pydets`. The elements are of type `float64` or `complex128`.
890/// Python type: `numpy.2darray[float] | numpy.2darray[complex]`.
891/// * `energies` - The `float64` or `complex128` energies of the multi-determinantal wavefunctions.
892/// The number of terms must match the number of columns of `coefficients`.
893/// Python type: `numpy.1darray[float] | numpy.1darray[complex]`.
894/// * `pybao` - A Python-exposed Python-exposed structure containing basis angular order information.
895/// Python type: `PyBasisAngularOrder`.
896/// * `integrality_threshold` - The threshold for verifying if subspace multiplicities are
897/// integral. Python type: `float`.
898/// * `linear_independence_threshold` - The threshold for determining the linear independence
899/// subspace via the non-zero eigenvalues of the orbit overlap matrix. Python type: `float`.
900/// * `use_magnetic_group` - An option indicating if the magnetic group is to be used for symmetry
901/// analysis, and if so, whether unitary representations or unitary-antiunitary corepresentations
902/// should be used. Python type: `None | MagneticSymmetryAnalysisKind`.
903/// * `use_double_group` - A boolean indicating if the double group of the prevailing symmetry
904/// group is to be used for representation analysis instead. Python type: `bool`.
905/// * `use_cayley_table` - A boolean indicating if the Cayley table for the group, if available,
906/// should be used to speed up the calculation of orbit overlap matrices. Python type: `bool`.
907/// * `symmetry_transformation_kind` - An enumerated type indicating the type of symmetry
908/// transformations to be performed on the origin determinant to generate the orbit. If this
909/// contains spin transformation, the determinant will be augmented to generalised spin constraint
910/// automatically. Python type: `SymmetryTransformationKind`.
911/// * `eigenvalue_comparison_mode` - An enumerated type indicating the mode of comparison of orbit
912/// overlap eigenvalues with the specified `linear_independence_threshold`.
913/// Python type: `EigenvalueComparisonMode`.
914/// * `sao` - The atomic-orbital overlap matrix whose elements are of type `float64` or
915/// `complex128`. Python type: `numpy.2darray[float] | numpy.2darray[complex]`.
916/// * `sao_h` - The optional complex-symmetric atomic-orbital overlap matrix whose elements
917/// are of type `float64` or `complex128`. This is required if antiunitary symmetry operations are
918/// involved. Python type: `None | numpy.2darray[float] | numpy.2darray[complex]`.
919/// * `write_overlap_eigenvalues` - A boolean indicating if the eigenvalues of the determinant
920/// orbit overlap matrix are to be written to the output. Python type: `bool`.
921/// * `write_character_table` - A boolean indicating if the character table of the prevailing
922/// symmetry group is to be printed out. Python type: `bool`.
923/// * `infinite_order_to_finite` - The finite order with which infinite-order generators are to be
924/// interpreted to form a finite subgroup of the prevailing infinite group. This finite subgroup
925/// will be used for symmetry analysis. Python type: `Optional[int]`.
926/// * `angular_function_integrality_threshold` - The threshold for verifying if subspace
927/// multiplicities are integral for the symmetry analysis of angular functions. Python type:
928/// `float`.
929/// * `angular_function_linear_independence_threshold` - The threshold for determining the linear
930/// independence subspace via the non-zero eigenvalues of the orbit overlap matrix for the symmetry
931/// analysis of angular functions. Python type: `float`.
932/// * `angular_function_max_angular_momentum` - The maximum angular momentum order to be used in
933/// angular function symmetry analysis. Python type: `int`.
934#[pyfunction]
935#[pyo3(signature = (
936    inp_sym,
937    pydets,
938    coefficients,
939    energies,
940    pybao,
941    integrality_threshold,
942    linear_independence_threshold,
943    use_magnetic_group,
944    use_double_group,
945    use_cayley_table,
946    symmetry_transformation_kind,
947    eigenvalue_comparison_mode,
948    sao,
949    sao_h=None,
950    write_overlap_eigenvalues=true,
951    write_character_table=true,
952    infinite_order_to_finite=None,
953    angular_function_integrality_threshold=1e-7,
954    angular_function_linear_independence_threshold=1e-7,
955    angular_function_max_angular_momentum=2
956))]
957pub fn rep_analyse_multideterminants_eager_basis(
958    py: Python<'_>,
959    inp_sym: PathBuf,
960    pydets: Vec<PySlaterDeterminant>,
961    coefficients: PyArray2RC,
962    energies: PyArray1RC,
963    pybao: &PyBasisAngularOrder,
964    integrality_threshold: f64,
965    linear_independence_threshold: f64,
966    use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
967    use_double_group: bool,
968    use_cayley_table: bool,
969    symmetry_transformation_kind: SymmetryTransformationKind,
970    eigenvalue_comparison_mode: EigenvalueComparisonMode,
971    sao: PyArray2RC,
972    sao_h: Option<PyArray2RC>,
973    write_overlap_eigenvalues: bool,
974    write_character_table: bool,
975    infinite_order_to_finite: Option<u32>,
976    angular_function_integrality_threshold: f64,
977    angular_function_linear_independence_threshold: f64,
978    angular_function_max_angular_momentum: u32,
979) -> PyResult<()> {
980    // Read in point-group detection results
981    let pd_res: SymmetryGroupDetectionResult =
982        read_qsym2_binary(inp_sym.clone(), QSym2FileType::Sym)
983            .map_err(|err| PyIOError::new_err(err.to_string()))?;
984
985    let mut file_name = inp_sym.to_path_buf();
986    file_name.set_extension(QSym2FileType::Sym.ext());
987    qsym2_output!(
988        "Symmetry-group detection results read in from {}.",
989        file_name.display(),
990    );
991    qsym2_output!("");
992
993    // Set up basic parameters
994    let mol = &pd_res.pre_symmetry.recentred_molecule;
995    let bao = pybao
996        .to_qsym2(mol)
997        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
998    let augment_to_generalised = match symmetry_transformation_kind {
999        SymmetryTransformationKind::SpatialWithSpinTimeReversal
1000        | SymmetryTransformationKind::Spin
1001        | SymmetryTransformationKind::SpinSpatial => true,
1002        SymmetryTransformationKind::Spatial => false,
1003    };
1004    let afa_params = AngularFunctionRepAnalysisParams::builder()
1005        .integrality_threshold(angular_function_integrality_threshold)
1006        .linear_independence_threshold(angular_function_linear_independence_threshold)
1007        .max_angular_momentum(angular_function_max_angular_momentum)
1008        .build()
1009        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1010    let mda_params = MultiDeterminantRepAnalysisParams::<f64>::builder()
1011        .integrality_threshold(integrality_threshold)
1012        .linear_independence_threshold(linear_independence_threshold)
1013        .use_magnetic_group(use_magnetic_group.clone())
1014        .use_double_group(use_double_group)
1015        .use_cayley_table(use_cayley_table)
1016        .symmetry_transformation_kind(symmetry_transformation_kind.clone())
1017        .eigenvalue_comparison_mode(eigenvalue_comparison_mode)
1018        .write_overlap_eigenvalues(write_overlap_eigenvalues)
1019        .write_character_table(if write_character_table {
1020            Some(CharacterTableDisplay::Symbolic)
1021        } else {
1022            None
1023        })
1024        .infinite_order_to_finite(infinite_order_to_finite)
1025        .build()
1026        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1027
1028    let all_real = pydets
1029        .iter()
1030        .all(|pydet| matches!(pydet, PySlaterDeterminant::Real(_)));
1031
1032    match (all_real, &coefficients, &energies, &sao) {
1033        (
1034            true,
1035            PyArray2RC::Real(pycoefficients_r),
1036            PyArray1RC::Real(pyenergies_r),
1037            PyArray2RC::Real(pysao_r),
1038        ) => {
1039            // Real numeric data type
1040
1041            // Preparation
1042            let sao = pysao_r.to_owned_array();
1043            let coefficients_r = pycoefficients_r.to_owned_array();
1044            let energies_r = pyenergies_r.to_owned_array();
1045            let dets_r = if augment_to_generalised {
1046                pydets
1047                    .iter()
1048                    .map(|pydet| {
1049                        if let PySlaterDeterminant::Real(pydet_r) = pydet {
1050                            pydet_r
1051                                .to_qsym2(&bao, mol)
1052                                .map(|det_r| det_r.to_generalised())
1053                        } else {
1054                            bail!("Unexpected complex type for a Slater determinant.")
1055                        }
1056                    })
1057                    .collect::<Result<Vec<_>, _>>()
1058            } else {
1059                pydets
1060                    .iter()
1061                    .map(|pydet| {
1062                        if let PySlaterDeterminant::Real(pydet_r) = pydet {
1063                            pydet_r.to_qsym2(&bao, mol)
1064                        } else {
1065                            bail!("Unexpected complex type for a Slater determinant.")
1066                        }
1067                    })
1068                    .collect::<Result<Vec<_>, _>>()
1069            }
1070            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1071
1072            let n_energies = energies_r.len();
1073            if coefficients_r.shape()[1] != n_energies {
1074                return Err(PyRuntimeError::new_err(
1075                    "Mismatched number of NOCI energies and number of NOCI states.",
1076                ));
1077            }
1078
1079            // Construct the eager basis
1080            let eager_basis = EagerBasis::builder()
1081                .elements(dets_r)
1082                .build()
1083                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1084
1085            // Construct the multi-determinantal wavefunctions
1086            let multidets = energies_r
1087                .iter()
1088                .zip(coefficients_r.columns())
1089                .map(|(energy, coeffs)| {
1090                    MultiDeterminant::builder()
1091                        .basis(eager_basis.clone())
1092                        .coefficients(coeffs.to_owned())
1093                        .threshold(1e-7)
1094                        .energy(Ok(*energy))
1095                        .build()
1096                })
1097                .collect::<Result<Vec<_>, _>>()
1098                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1099
1100            // Construct the driver
1101            match &use_magnetic_group {
1102                Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
1103                    let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
1104                        MagneticRepresentedSymmetryGroup,
1105                        f64,
1106                        _,
1107                        SpinConstraint,
1108                    >::builder()
1109                    .parameters(&mda_params)
1110                    .angular_function_parameters(&afa_params)
1111                    .multidets(multidets.iter().collect::<Vec<_>>())
1112                    .sao(&sao)
1113                    .sao_h(None) // Real SAO.
1114                    .symmetry_group(&pd_res)
1115                    .build()
1116                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1117
1118                    // Run the driver
1119                    py.allow_threads(|| {
1120                        mda_driver
1121                            .run()
1122                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
1123                    })?
1124                }
1125                Some(MagneticSymmetryAnalysisKind::Representation) | None => {
1126                    let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
1127                        UnitaryRepresentedSymmetryGroup,
1128                        f64,
1129                        _,
1130                        SpinConstraint,
1131                    >::builder()
1132                    .parameters(&mda_params)
1133                    .angular_function_parameters(&afa_params)
1134                    .multidets(multidets.iter().collect::<Vec<_>>())
1135                    .sao(&sao)
1136                    .sao_h(None) // Real SAO.
1137                    .symmetry_group(&pd_res)
1138                    .build()
1139                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1140
1141                    // Run the driver
1142                    py.allow_threads(|| {
1143                        mda_driver
1144                            .run()
1145                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
1146                    })?
1147                }
1148            }
1149        }
1150        (_, _, _, _) => {
1151            // Complex numeric data type
1152
1153            // Preparation
1154            let sao_c = match sao {
1155                PyArray2RC::Real(pysao_r) => pysao_r.to_owned_array().mapv(Complex::from),
1156                PyArray2RC::Complex(pysao_c) => pysao_c.to_owned_array(),
1157            };
1158            let sao_h_c = sao_h.and_then(|pysao_h| match pysao_h {
1159                // sao_spatial_h must have the same reality as sao_spatial.
1160                PyArray2RC::Real(pysao_h_r) => Some(pysao_h_r.to_owned_array().mapv(Complex::from)),
1161                PyArray2RC::Complex(pysao_h_c) => Some(pysao_h_c.to_owned_array()),
1162            });
1163            let coefficients_c = match coefficients {
1164                PyArray2RC::Real(pycoefficients_r) => {
1165                    pycoefficients_r.to_owned_array().mapv(Complex::from)
1166                }
1167                PyArray2RC::Complex(pycoefficients_c) => pycoefficients_c.to_owned_array(),
1168            };
1169            let energies_c = match energies {
1170                PyArray1RC::Real(pyenergies_r) => pyenergies_r.to_owned_array().mapv(Complex::from),
1171                PyArray1RC::Complex(pyenergies_c) => pyenergies_c.to_owned_array(),
1172            };
1173            let dets_c = if augment_to_generalised {
1174                pydets
1175                    .iter()
1176                    .map(|pydet| match pydet {
1177                        PySlaterDeterminant::Real(pydet_r) => {
1178                            pydet_r.to_qsym2::<SpinConstraint>(&bao, mol).map(|det_r| {
1179                                SlaterDeterminant::<C128, SpinConstraint>::from(det_r)
1180                                    .to_generalised()
1181                            })
1182                        }
1183                        PySlaterDeterminant::Complex(pydet_c) => pydet_c
1184                            .to_qsym2::<SpinConstraint>(&bao, mol)
1185                            .map(|det_c| det_c.to_generalised()),
1186                    })
1187                    .collect::<Result<Vec<_>, _>>()
1188            } else {
1189                pydets
1190                    .iter()
1191                    .map(|pydet| match pydet {
1192                        PySlaterDeterminant::Real(pydet_r) => pydet_r
1193                            .to_qsym2::<SpinConstraint>(&bao, mol)
1194                            .map(|det_r| SlaterDeterminant::<C128, SpinConstraint>::from(det_r)),
1195                        PySlaterDeterminant::Complex(pydet_c) => {
1196                            pydet_c.to_qsym2::<SpinConstraint>(&bao, mol)
1197                        }
1198                    })
1199                    .collect::<Result<Vec<_>, _>>()
1200            }
1201            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1202
1203            let n_energies = energies_c.len();
1204            if coefficients_c.shape()[1] != n_energies {
1205                return Err(PyRuntimeError::new_err(
1206                    "Mismatched number of NOCI energies and number of NOCI states.",
1207                ));
1208            }
1209
1210            // Construct the eager basis
1211            let eager_basis = EagerBasis::builder()
1212                .elements(dets_c)
1213                .build()
1214                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1215
1216            // Construct the multi-determinantal wavefunctions
1217            let multidets = energies_c
1218                .iter()
1219                .zip(coefficients_c.columns())
1220                .map(|(energy, coeffs)| {
1221                    MultiDeterminant::builder()
1222                        .basis(eager_basis.clone())
1223                        .coefficients(coeffs.to_owned())
1224                        .threshold(1e-7)
1225                        .energy(Ok(*energy))
1226                        .build()
1227                })
1228                .collect::<Result<Vec<_>, _>>()
1229                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1230
1231            // Construct the driver
1232            match &use_magnetic_group {
1233                Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
1234                    let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
1235                        MagneticRepresentedSymmetryGroup,
1236                        C128,
1237                        _,
1238                        SpinConstraint,
1239                    >::builder()
1240                    .parameters(&mda_params)
1241                    .angular_function_parameters(&afa_params)
1242                    .multidets(multidets.iter().collect::<Vec<_>>())
1243                    .sao(&sao_c)
1244                    .sao_h(sao_h_c.as_ref())
1245                    .symmetry_group(&pd_res)
1246                    .build()
1247                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1248
1249                    // Run the driver
1250                    py.allow_threads(|| {
1251                        mda_driver
1252                            .run()
1253                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
1254                    })?
1255                }
1256                Some(MagneticSymmetryAnalysisKind::Representation) | None => {
1257                    let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
1258                        UnitaryRepresentedSymmetryGroup,
1259                        C128,
1260                        _,
1261                        SpinConstraint,
1262                    >::builder()
1263                    .parameters(&mda_params)
1264                    .angular_function_parameters(&afa_params)
1265                    .multidets(multidets.iter().collect::<Vec<_>>())
1266                    .sao(&sao_c)
1267                    .sao_h(sao_h_c.as_ref())
1268                    .symmetry_group(&pd_res)
1269                    .build()
1270                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
1271
1272                    // Run the driver
1273                    py.allow_threads(|| {
1274                        mda_driver
1275                            .run()
1276                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
1277                    })?
1278                }
1279            }
1280        }
1281    }
1282    Ok(())
1283}