qsym2/bindings/python/representation_analysis/multideterminant/
multideterminant_eager_basis.rs

1//! Python bindings for QSym² symmetry analysis of multi-determinants with eager bases.
2
3use std::collections::HashSet;
4use std::path::PathBuf;
5
6use anyhow::bail;
7use num_complex::Complex;
8use numpy::PyArrayMethods;
9use pyo3::exceptions::{PyIOError, PyRuntimeError};
10use pyo3::prelude::*;
11
12use crate::analysis::EigenvalueComparisonMode;
13use crate::angmom::spinor_rotation_3d::{SpinConstraint, SpinOrbitCoupled};
14use crate::bindings::python::integrals::{
15    PyBasisAngularOrder, PyStructureConstraint,
16};
17use crate::bindings::python::representation_analysis::slater_determinant::PySlaterDeterminant;
18use crate::bindings::python::representation_analysis::{PyArray1RC, PyArray2RC};
19use crate::drivers::QSym2Driver;
20use crate::drivers::representation_analysis::angular_function::AngularFunctionRepAnalysisParams;
21use crate::drivers::representation_analysis::multideterminant::{
22    MultiDeterminantRepAnalysisDriver, MultiDeterminantRepAnalysisParams,
23};
24use crate::drivers::representation_analysis::{
25    CharacterTableDisplay, MagneticSymmetryAnalysisKind,
26};
27use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
28use crate::io::format::qsym2_output;
29use crate::io::{QSym2FileType, read_qsym2_binary};
30use crate::symmetry::symmetry_group::{
31    MagneticRepresentedSymmetryGroup, UnitaryRepresentedSymmetryGroup,
32};
33use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
34use crate::target::determinant::SlaterDeterminant;
35use crate::target::noci::basis::EagerBasis;
36use crate::target::noci::multideterminant::MultiDeterminant;
37
38type C128 = Complex<f64>;
39
40/// Python-exposed function to perform representation symmetry analysis for real and complex
41/// multi-determinantal wavefunctions constructed from an eager basis of Slater determinants and log
42/// the result via the `qsym2-output` logger at the `INFO` level.
43///
44/// If `symmetry_transformation_kind` includes spin transformation, the provided
45/// multi-determinantal wavefunctions will be augmented to generalised spin constraint
46/// automatically.
47///
48/// # Arguments
49///
50/// * `inp_sym` - A path to the [`QSym2FileType::Sym`] file containing the symmetry-group detection
51/// result for the system. This will be used to construct abstract groups and character tables for
52/// representation analysis.
53/// * `pydets` - A list of Python-exposed Slater determinants whose coefficients are of type
54/// `float64` or `complex128`. These determinants serve as basis states for non-orthogonal
55/// configuration interaction to yield multi-determinantal wavefunctions, the symmetry of which will
56/// be analysed by this function.
57/// * `coefficients` - The coefficient matrix where each column gives the linear combination
58/// coefficients for one multi-determinantal wavefunction. The number of rows must match the number
59/// of determinants specified in `pydets`. The elements are of type `float64` or `complex128`.
60/// * `energies` - The `float64` or `complex128` energies of the multi-determinantal wavefunctions.
61/// The number of terms must match the number of columns of `coefficients`.
62/// * `pybaos` - Python-exposed structures containing basis angular order information, one for each
63/// explicit component per coefficient matrix.
64/// * `integrality_threshold` - The threshold for verifying if subspace multiplicities are
65/// integral.
66/// * `linear_independence_threshold` - The threshold for determining the linear independence
67/// subspace via the non-zero eigenvalues of the orbit overlap matrix.
68/// * `use_magnetic_group` - An option indicating if the magnetic group is to be used for symmetry
69/// analysis, and if so, whether unitary representations or unitary-antiunitary corepresentations
70/// should be used.
71/// * `use_double_group` - A boolean indicating if the double group of the prevailing symmetry
72/// group is to be used for representation analysis instead.
73/// * `use_cayley_table` - A boolean indicating if the Cayley table for the group, if available,
74/// should be used to speed up the calculation of orbit overlap matrices.
75/// * `symmetry_transformation_kind` - An enumerated type indicating the type of symmetry
76/// transformations to be performed on the origin determinant to generate the orbit. If this
77/// contains spin transformation, the multi-determinant will be augmented to generalised spin
78/// constraint automatically.
79/// * `eigenvalue_comparison_mode` - An enumerated type indicating the mode of comparison of orbit
80/// overlap eigenvalues with the specified `linear_independence_threshold`.
81/// * `sao` - The atomic-orbital overlap matrix whose elements are of type `float64` or
82/// `complex128`.
83/// * `sao_h` - The optional complex-symmetric atomic-orbital overlap matrix whose elements
84/// are of type `float64` or `complex128`. This is required if antiunitary symmetry operations are
85/// involved.
86/// * `write_overlap_eigenvalues` - A boolean indicating if the eigenvalues of the determinant
87/// orbit overlap matrix are to be written to the output.
88/// * `write_character_table` - A boolean indicating if the character table of the prevailing
89/// symmetry group is to be printed out.
90/// * `infinite_order_to_finite` - The finite order with which infinite-order generators are to be
91/// interpreted to form a finite subgroup of the prevailing infinite group. This finite subgroup
92/// will be used for symmetry analysis.
93/// * `angular_function_integrality_threshold` - The threshold for verifying if subspace
94/// multiplicities are integral for the symmetry analysis of angular functions.
95/// * `angular_function_linear_independence_threshold` - The threshold for determining the linear
96/// independence subspace via the non-zero eigenvalues of the orbit overlap matrix for the symmetry
97/// analysis of angular functions.
98/// * `angular_function_max_angular_momentum` - The maximum angular momentum order to be used in
99/// angular function symmetry analysis.
100#[pyfunction]
101#[pyo3(signature = (
102    inp_sym,
103    pydets,
104    coefficients,
105    energies,
106    pybaos,
107    integrality_threshold,
108    linear_independence_threshold,
109    use_magnetic_group,
110    use_double_group,
111    use_cayley_table,
112    symmetry_transformation_kind,
113    eigenvalue_comparison_mode,
114    sao,
115    sao_h=None,
116    write_overlap_eigenvalues=true,
117    write_character_table=true,
118    infinite_order_to_finite=None,
119    angular_function_integrality_threshold=1e-7,
120    angular_function_linear_independence_threshold=1e-7,
121    angular_function_max_angular_momentum=2
122))]
123pub fn rep_analyse_multideterminants_eager_basis(
124    py: Python<'_>,
125    inp_sym: PathBuf,
126    pydets: Vec<PySlaterDeterminant>,
127    coefficients: PyArray2RC,
128    energies: PyArray1RC,
129    pybaos: Vec<PyBasisAngularOrder>,
130    integrality_threshold: f64,
131    linear_independence_threshold: f64,
132    use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
133    use_double_group: bool,
134    use_cayley_table: bool,
135    symmetry_transformation_kind: SymmetryTransformationKind,
136    eigenvalue_comparison_mode: EigenvalueComparisonMode,
137    sao: PyArray2RC,
138    sao_h: Option<PyArray2RC>,
139    write_overlap_eigenvalues: bool,
140    write_character_table: bool,
141    infinite_order_to_finite: Option<u32>,
142    angular_function_integrality_threshold: f64,
143    angular_function_linear_independence_threshold: f64,
144    angular_function_max_angular_momentum: u32,
145) -> PyResult<()> {
146    // Read in point-group detection results
147    let pd_res: SymmetryGroupDetectionResult =
148        read_qsym2_binary(inp_sym.clone(), QSym2FileType::Sym)
149            .map_err(|err| PyIOError::new_err(err.to_string()))?;
150
151    let mut file_name = inp_sym.to_path_buf();
152    file_name.set_extension(QSym2FileType::Sym.ext());
153    qsym2_output!(
154        "Symmetry-group detection results read in from {}.",
155        file_name.display(),
156    );
157    qsym2_output!("");
158
159    // Set up basic parameters
160    let mol = &pd_res.pre_symmetry.recentred_molecule;
161
162    let baos = pybaos
163        .iter()
164        .map(|bao| {
165            bao.to_qsym2(mol)
166                .map_err(|err| PyRuntimeError::new_err(err.to_string()))
167        })
168        .collect::<Result<Vec<_>, _>>()?;
169    let baos_ref = baos.iter().collect::<Vec<_>>();
170    let augment_to_generalised = match symmetry_transformation_kind {
171        SymmetryTransformationKind::SpatialWithSpinTimeReversal
172        | SymmetryTransformationKind::Spin
173        | SymmetryTransformationKind::SpinSpatial => true,
174        SymmetryTransformationKind::Spatial => false,
175    };
176    let afa_params = AngularFunctionRepAnalysisParams::builder()
177        .integrality_threshold(angular_function_integrality_threshold)
178        .linear_independence_threshold(angular_function_linear_independence_threshold)
179        .max_angular_momentum(angular_function_max_angular_momentum)
180        .build()
181        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
182    let mda_params = MultiDeterminantRepAnalysisParams::<f64>::builder()
183        .integrality_threshold(integrality_threshold)
184        .linear_independence_threshold(linear_independence_threshold)
185        .use_magnetic_group(use_magnetic_group.clone())
186        .use_double_group(use_double_group)
187        .use_cayley_table(use_cayley_table)
188        .symmetry_transformation_kind(symmetry_transformation_kind.clone())
189        .eigenvalue_comparison_mode(eigenvalue_comparison_mode)
190        .write_overlap_eigenvalues(write_overlap_eigenvalues)
191        .write_character_table(if write_character_table {
192            Some(CharacterTableDisplay::Symbolic)
193        } else {
194            None
195        })
196        .infinite_order_to_finite(infinite_order_to_finite)
197        .build()
198        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
199
200    let all_real = pydets
201        .iter()
202        .all(|pydet| matches!(pydet, PySlaterDeterminant::Real(_)));
203
204    let structure_constraints_set = pydets
205        .iter()
206        .map(|pydet| match pydet {
207            PySlaterDeterminant::Real(pydet) => pydet.structure_constraint().clone(),
208            PySlaterDeterminant::Complex(pydet) => pydet.structure_constraint().clone(),
209        })
210        .collect::<HashSet<_>>();
211    if structure_constraints_set.len() != 1 {
212        return Err(PyRuntimeError::new_err(
213            "Inconsistent structure constraints across origin determinants.`",
214        ));
215    };
216    let structure_constraint = structure_constraints_set
217        .iter()
218        .next()
219        .ok_or_else(|| PyRuntimeError::new_err("Unable to retrieve the structure constraint."))?;
220
221    // Decision tree:
222    // - all real numerical data?
223    //   + yes:
224    //     - structure_constraint:
225    //       + SpinConstraint:
226    //         - use_magnetic_group:
227    //           + Some(Corepresentation)
228    //           + Some(Representation) | None
229    //       + SpinOrbitCoupled: not supported
230    //   - no:
231    //     - structure_constraint:
232    //       + SpinConstraint:
233    //         - use_magnetic_group:
234    //           + Some(Corepresentation)
235    //           + Some(Representation) | None
236    //       + SpinOrbitCoupled:
237    //         - use_magnetic_group:
238    //           + Some(Corepresentation)
239    //           + Some(Representation) | None
240    match (all_real, &coefficients, &energies, &sao) {
241        (
242            true,
243            PyArray2RC::Real(pycoefficients_r),
244            PyArray1RC::Real(pyenergies_r),
245            PyArray2RC::Real(pysao_r),
246        ) => {
247            // Real numeric data type
248
249            if matches!(
250                structure_constraint,
251                PyStructureConstraint::SpinOrbitCoupled(_)
252            ) {
253                return Err(PyRuntimeError::new_err(
254                    "Real determinants cannot support spin--orbit-coupled structure constraint.",
255                ));
256            }
257
258            // Preparation
259            let sao_r = pysao_r.to_owned_array();
260            let coefficients_r = pycoefficients_r.to_owned_array();
261            let energies_r = pyenergies_r.to_owned_array();
262            let dets_r = if augment_to_generalised {
263                pydets
264                    .iter()
265                    .map(|pydet| {
266                        if let PySlaterDeterminant::Real(pydet_r) = pydet {
267                            pydet_r
268                                .to_qsym2(&baos_ref, mol)
269                                .map(|det_r| det_r.to_generalised())
270                        } else {
271                            bail!("Unexpected complex type for a Slater determinant.")
272                        }
273                    })
274                    .collect::<Result<Vec<_>, _>>()
275            } else {
276                pydets
277                    .iter()
278                    .map(|pydet| {
279                        if let PySlaterDeterminant::Real(pydet_r) = pydet {
280                            pydet_r.to_qsym2(&baos_ref, mol)
281                        } else {
282                            bail!("Unexpected complex type for a Slater determinant.")
283                        }
284                    })
285                    .collect::<Result<Vec<_>, _>>()
286            }
287            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
288
289            let n_energies = energies_r.len();
290            if coefficients_r.shape()[1] != n_energies {
291                return Err(PyRuntimeError::new_err(
292                    "Mismatched number of NOCI energies and number of NOCI states.",
293                ));
294            }
295
296            // Construct the eager basis
297            let eager_basis = EagerBasis::builder()
298                .elements(dets_r)
299                .build()
300                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
301
302            // Construct the multi-determinantal wavefunctions
303            let multidets = energies_r
304                .iter()
305                .zip(coefficients_r.columns())
306                .map(|(energy, coeffs)| {
307                    MultiDeterminant::builder()
308                        .basis(eager_basis.clone())
309                        .coefficients(coeffs.to_owned())
310                        .threshold(1e-7)
311                        .energy(Ok(*energy))
312                        .build()
313                })
314                .collect::<Result<Vec<_>, _>>()
315                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
316
317            // Construct the driver
318            match &use_magnetic_group {
319                Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
320                    let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
321                        MagneticRepresentedSymmetryGroup,
322                        f64,
323                        _,
324                        SpinConstraint,
325                    >::builder()
326                    .parameters(&mda_params)
327                    .angular_function_parameters(&afa_params)
328                    .multidets(multidets.iter().collect::<Vec<_>>())
329                    .sao(&sao_r)
330                    .sao_h(None) // Real SAO.
331                    .symmetry_group(&pd_res)
332                    .build()
333                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
334
335                    // Run the driver
336                    py.detach(|| {
337                        mda_driver
338                            .run()
339                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
340                    })?
341                }
342                Some(MagneticSymmetryAnalysisKind::Representation) | None => {
343                    let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
344                        UnitaryRepresentedSymmetryGroup,
345                        f64,
346                        _,
347                        SpinConstraint,
348                    >::builder()
349                    .parameters(&mda_params)
350                    .angular_function_parameters(&afa_params)
351                    .multidets(multidets.iter().collect::<Vec<_>>())
352                    .sao(&sao_r)
353                    .sao_h(None) // Real SAO.
354                    .symmetry_group(&pd_res)
355                    .build()
356                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
357
358                    // Run the driver
359                    py.detach(|| {
360                        mda_driver
361                            .run()
362                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
363                    })?
364                }
365            }
366        }
367        (_, _, _, _) => {
368            // Complex numeric data type
369
370            // Preparation
371            let sao_c = match sao {
372                PyArray2RC::Real(pysao_r) => pysao_r.to_owned_array().mapv(Complex::from),
373                PyArray2RC::Complex(pysao_c) => pysao_c.to_owned_array(),
374            };
375            let sao_h_c = sao_h.and_then(|pysao_h| match pysao_h {
376                // sao_spatial_h must have the same reality as sao_spatial.
377                PyArray2RC::Real(pysao_h_r) => Some(pysao_h_r.to_owned_array().mapv(Complex::from)),
378                PyArray2RC::Complex(pysao_h_c) => Some(pysao_h_c.to_owned_array()),
379            });
380            let coefficients_c = match coefficients {
381                PyArray2RC::Real(pycoefficients_r) => {
382                    pycoefficients_r.to_owned_array().mapv(Complex::from)
383                }
384                PyArray2RC::Complex(pycoefficients_c) => pycoefficients_c.to_owned_array(),
385            };
386            let energies_c = match energies {
387                PyArray1RC::Real(pyenergies_r) => pyenergies_r.to_owned_array().mapv(Complex::from),
388                PyArray1RC::Complex(pyenergies_c) => pyenergies_c.to_owned_array(),
389            };
390
391            match structure_constraint {
392                PyStructureConstraint::SpinConstraint(_) => {
393                    let dets_c = if augment_to_generalised {
394                        pydets
395                            .iter()
396                            .map(|pydet| match pydet {
397                                PySlaterDeterminant::Real(pydet_r) => pydet_r
398                                    .to_qsym2::<SpinConstraint>(&baos_ref, mol)
399                                    .map(|det_r| {
400                                        SlaterDeterminant::<C128, SpinConstraint>::from(det_r)
401                                            .to_generalised()
402                                    }),
403                                PySlaterDeterminant::Complex(pydet_c) => pydet_c
404                                    .to_qsym2::<SpinConstraint>(&baos_ref, mol)
405                                    .map(|det_c| det_c.to_generalised()),
406                            })
407                            .collect::<Result<Vec<_>, _>>()
408                    } else {
409                        pydets
410                            .iter()
411                            .map(|pydet| match pydet {
412                                PySlaterDeterminant::Real(pydet_r) => pydet_r
413                                    .to_qsym2::<SpinConstraint>(&baos_ref, mol)
414                                    .map(|det_r| {
415                                        SlaterDeterminant::<C128, SpinConstraint>::from(det_r)
416                                    }),
417                                PySlaterDeterminant::Complex(pydet_c) => {
418                                    pydet_c.to_qsym2::<SpinConstraint>(&baos_ref, mol)
419                                }
420                            })
421                            .collect::<Result<Vec<_>, _>>()
422                    }
423                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
424
425                    let n_energies = energies_c.len();
426                    if coefficients_c.shape()[1] != n_energies {
427                        return Err(PyRuntimeError::new_err(
428                            "Mismatched number of NOCI energies and number of NOCI states.",
429                        ));
430                    }
431
432                    // Construct the eager basis
433                    let eager_basis = EagerBasis::builder()
434                        .elements(dets_c)
435                        .build()
436                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
437
438                    // Construct the multi-determinantal wavefunctions
439                    let multidets = energies_c
440                        .iter()
441                        .zip(coefficients_c.columns())
442                        .map(|(energy, coeffs)| {
443                            MultiDeterminant::builder()
444                                .basis(eager_basis.clone())
445                                .coefficients(coeffs.to_owned())
446                                .threshold(1e-7)
447                                .energy(Ok(*energy))
448                                .build()
449                        })
450                        .collect::<Result<Vec<_>, _>>()
451                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
452
453                    // Construct the driver
454                    match &use_magnetic_group {
455                        Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
456                            let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
457                                MagneticRepresentedSymmetryGroup,
458                                C128,
459                                _,
460                                SpinConstraint,
461                            >::builder()
462                            .parameters(&mda_params)
463                            .angular_function_parameters(&afa_params)
464                            .multidets(multidets.iter().collect::<Vec<_>>())
465                            .sao(&sao_c)
466                            .sao_h(sao_h_c.as_ref())
467                            .symmetry_group(&pd_res)
468                            .build()
469                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
470
471                            // Run the driver
472                            py.detach(|| {
473                                mda_driver
474                                    .run()
475                                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))
476                            })?
477                        }
478                        Some(MagneticSymmetryAnalysisKind::Representation) | None => {
479                            let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
480                                UnitaryRepresentedSymmetryGroup,
481                                C128,
482                                _,
483                                SpinConstraint,
484                            >::builder()
485                            .parameters(&mda_params)
486                            .angular_function_parameters(&afa_params)
487                            .multidets(multidets.iter().collect::<Vec<_>>())
488                            .sao(&sao_c)
489                            .sao_h(sao_h_c.as_ref())
490                            .symmetry_group(&pd_res)
491                            .build()
492                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
493
494                            // Run the driver
495                            py.detach(|| {
496                                mda_driver
497                                    .run()
498                                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))
499                            })?
500                        }
501                    }
502                }
503                PyStructureConstraint::SpinOrbitCoupled(_) => {
504                    let dets_c = pydets
505                        .iter()
506                        .map(|pydet| match pydet {
507                            PySlaterDeterminant::Real(pydet_r) => pydet_r
508                                .to_qsym2::<SpinOrbitCoupled>(&baos_ref, mol)
509                                .map(|det_r| {
510                                    SlaterDeterminant::<C128, SpinOrbitCoupled>::from(det_r)
511                                }),
512                            PySlaterDeterminant::Complex(pydet_c) => {
513                                pydet_c.to_qsym2::<SpinOrbitCoupled>(&baos_ref, mol)
514                            }
515                        })
516                        .collect::<Result<Vec<_>, _>>()
517                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
518
519                    let n_energies = energies_c.len();
520                    if coefficients_c.shape()[1] != n_energies {
521                        return Err(PyRuntimeError::new_err(
522                            "Mismatched number of NOCI energies and number of NOCI states.",
523                        ));
524                    }
525
526                    // Construct the eager basis
527                    let eager_basis = EagerBasis::builder()
528                        .elements(dets_c)
529                        .build()
530                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
531
532                    // Construct the multi-determinantal wavefunctions
533                    let multidets = energies_c
534                        .iter()
535                        .zip(coefficients_c.columns())
536                        .map(|(energy, coeffs)| {
537                            MultiDeterminant::builder()
538                                .basis(eager_basis.clone())
539                                .coefficients(coeffs.to_owned())
540                                .threshold(1e-7)
541                                .energy(Ok(*energy))
542                                .build()
543                        })
544                        .collect::<Result<Vec<_>, _>>()
545                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
546
547                    // Construct the driver
548                    match &use_magnetic_group {
549                        Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
550                            let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
551                                MagneticRepresentedSymmetryGroup,
552                                C128,
553                                _,
554                                SpinOrbitCoupled,
555                            >::builder()
556                            .parameters(&mda_params)
557                            .angular_function_parameters(&afa_params)
558                            .multidets(multidets.iter().collect::<Vec<_>>())
559                            .sao(&sao_c)
560                            .sao_h(sao_h_c.as_ref())
561                            .symmetry_group(&pd_res)
562                            .build()
563                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
564
565                            // Run the driver
566                            py.detach(|| {
567                                mda_driver
568                                    .run()
569                                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))
570                            })?
571                        }
572                        Some(MagneticSymmetryAnalysisKind::Representation) | None => {
573                            let mut mda_driver = MultiDeterminantRepAnalysisDriver::<
574                                UnitaryRepresentedSymmetryGroup,
575                                C128,
576                                _,
577                                SpinOrbitCoupled,
578                            >::builder()
579                            .parameters(&mda_params)
580                            .angular_function_parameters(&afa_params)
581                            .multidets(multidets.iter().collect::<Vec<_>>())
582                            .sao(&sao_c)
583                            .sao_h(sao_h_c.as_ref())
584                            .symmetry_group(&pd_res)
585                            .build()
586                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
587
588                            // Run the driver
589                            py.detach(|| {
590                                mda_driver
591                                    .run()
592                                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))
593                            })?
594                        }
595                    }
596                }
597            }
598        }
599    }
600    Ok(())
601}