Skip to main content

qsym2/bindings/python/projection/
slater_determinant.rs

1//! Python bindings for QSym² symmetry projection of Slater determinants.
2
3use std::collections::HashSet;
4use std::path::PathBuf;
5
6use anyhow::format_err;
7use itertools::Itertools;
8use ndarray::{Array2, Axis, ShapeBuilder};
9use num_complex::Complex;
10use numpy::{PyArrayMethods, ToPyArray};
11use pyo3::exceptions::{PyIOError, PyRuntimeError};
12use pyo3::types::PyFunction;
13use pyo3::{IntoPyObjectExt, prelude::*};
14
15use crate::angmom::spinor_rotation_3d::{SpinConstraint, SpinOrbitCoupled};
16use crate::bindings::python::integrals::{PyBasisAngularOrder, PyStructureConstraint};
17use crate::bindings::python::projection::PyProjectionTarget;
18use crate::bindings::python::representation_analysis::multideterminant::{
19    PyMultiDeterminantsComplex, PyMultiDeterminantsReal,
20};
21use crate::bindings::python::representation_analysis::slater_determinant::PySlaterDeterminant;
22use crate::bindings::python::representation_analysis::{PyArray2RC, PyArray4RC, PyScalarRC};
23use crate::drivers::QSym2Driver;
24use crate::drivers::projection::slater_determinant::{
25    SlaterDeterminantProjectionDriver, SlaterDeterminantProjectionParams,
26};
27use crate::drivers::representation_analysis::{
28    CharacterTableDisplay, MagneticSymmetryAnalysisKind,
29};
30use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
31use crate::io::format::qsym2_output;
32use crate::io::{QSym2FileType, read_qsym2_binary};
33use crate::symmetry::symmetry_group::UnitaryRepresentedSymmetryGroup;
34use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
35use crate::target::noci::basis::Basis;
36use crate::target::noci::multideterminants::MultiDeterminants;
37
38type C128 = Complex<f64>;
39
40// =====================
41// Functions definitions
42// =====================
43
44/// Python-exposed function to perform symmetry projection for real and complex Slater determinants.
45///
46/// # Arguments
47///
48/// * `inp_sym` - A path to the [`QSym2FileType::Sym`] file containing the symmetry-group detection
49/// result for the system. This will be used to construct abstract groups and character tables for
50/// symmetry projection.
51/// * `pydet` - A Slater determinant whose coefficient matrices are of type `float64` or `complex128`.
52/// * `projection_targets` - A sequence of subspace labels for projection. Each label is either a
53/// symbolic string or a numerical index for the subspace in the character table of the prevailing
54/// group.
55/// * `calculate_energies` - A boolean indicating if the energies of the projected wavefunctions
56/// (which are multi-determinantal wavefunctions specified in an orbit basis) are to be calculated.
57/// If `true`, then `thresh_offdiag`, `thresh_zeroov`, `enuc`, `onee`, and either `twoe` or
58/// `py_get_jk` must also be specified for Löwdin pairing and Hamiltonian matrix element computation.
59/// * `calculate_density_matrices` - A boolean indicating if the density matrices for the projected
60/// wavefunctions (which are multi-determinantal wavefunctions) are to be calculated. If `true`,
61/// then `thresh_offdiag` and `thresh_zeroov` must also be specified for Löwdin pairing.
62/// * `pybaos` - Python-exposed structures containing basis angular order information, one for each
63/// explicit component per coefficient matrix.
64/// * `use_magnetic_group` - An option indicating if the magnetic group is to be used for symmetry
65/// analysis, and if so, whether unitary representations or unitary-antiunitary corepresentations
66/// should be used.
67/// * `use_double_group` - A boolean indicating if the double group of the prevailing symmetry
68/// group is to be used for representation analysis instead.
69/// * `symmetry_transformation_kind` - An enumerated type indicating the type of symmetry
70/// transformations to be performed on the origin Slater determinant to generate the orbit.
71/// * `write_character_table` - A boolean indicating if the character table of the prevailing
72/// symmetry group is to be printed out.
73/// * `infinite_order_to_finite` - The finite order with which infinite-order generators are to be
74/// interpreted to form a finite subgroup of the prevailing infinite group. This finite subgroup
75/// will be used for symmetry analysis.
76/// * `sao` - The optional atomic-orbital overlap matrix whose elements are of type `float64` or
77/// `complex128`. If this is not present, no squared norms, energies, or density matrices of the
78/// resulting multi-determinants will be computed.
79/// * `sao_h` - The optional complex-symmetric atomic-orbital overlap matrix whose elements
80/// are of type `float64` or `complex128`. This is required if antiunitary symmetry operations are
81/// involved.
82/// * `enuc` - The optional nuclear repulsion energy, which is only required for `calculate_energies`.
83/// * `onee` - The optional one-electron integral matrix whose elements are of type `float64` or
84/// `complex128`, which is only required for `calculate_energies`.
85/// * `twoe` - The optional two-electron integral tensor in physicists' ordering whose elements are
86/// of type `float64` or `complex128`, which is only required for `calculate_energies`. Either this
87/// or `py_get_jk` must be specified, but not both.
88/// * `py_get_jk` - An optional Python function callable on a density matrix $`\mathbf{D}`$ to
89/// calculate the corresponding $`\mathbf{J}`$ and $`\mathbf{K}`$ matrices by contracting with
90/// appropriate two-electron integrals calculated on-the-fly, which is only required for
91/// `calculate_energies`. Either this or `twoe` must be specified, but not both.
92/// Python type: `Callable[[numpy.2darray[float] | numpy.2darray[complex]], tuple[numpy.2darray[float], numpy.2darray[float]] | tuple[numpy.2darray[complex], numpy.2darray[complex]]] | None`.
93/// * `thresh_offdiag` - The optional threshold for identifying non-zero off-diagonal elements in
94/// Löwdin pairing, which is required for `calculate_energies` and `calculate_density_matrices`.
95/// * `thresh_zeroov` - The optional threshold for identifying non-zero overlaps in Löwdin pairing,
96/// which is required for `calculate_energies` and `calculate_density_matrices`.
97///
98/// # Returns
99///
100/// The result will be returned as a tuple where the first item is a list of the labels of the
101/// subspaces used for projection, and the second item is an object containing
102///   * the Slater determinant basis,
103///   * the linear combination coefficients as a two-dimensional array with each column corresponding
104///   to one projected state,
105///   * the energies of the projected states if requested, or `NaN` if not, and
106///   * the density matrices of the projected states if requested, or `None` if not.
107#[allow(clippy::too_many_arguments)]
108#[pyfunction]
109#[pyo3(signature = (
110    inp_sym,
111    pydet,
112    projection_targets,
113    calculate_energies,
114    calculate_density_matrices,
115    pybaos,
116    use_magnetic_group,
117    use_double_group,
118    symmetry_transformation_kind,
119    write_character_table=true,
120    infinite_order_to_finite=None,
121    sao=None,
122    sao_h=None,
123    enuc=None,
124    onee=None,
125    twoe=None,
126    py_get_jk=None,
127    thresh_offdiag=None,
128    thresh_zeroov=None,
129))]
130pub fn project_slater_determinant(
131    py: Python<'_>,
132    inp_sym: PathBuf,
133    pydet: PySlaterDeterminant,
134    projection_targets: Vec<PyProjectionTarget>,
135    calculate_energies: bool,
136    calculate_density_matrices: bool,
137    pybaos: Vec<PyBasisAngularOrder>,
138    use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
139    use_double_group: bool,
140    symmetry_transformation_kind: SymmetryTransformationKind,
141    write_character_table: bool,
142    infinite_order_to_finite: Option<u32>,
143    sao: Option<PyArray2RC>,
144    sao_h: Option<PyArray2RC>,
145    enuc: Option<PyScalarRC>,
146    onee: Option<PyArray2RC>,
147    twoe: Option<PyArray4RC>,
148    py_get_jk: Option<Py<PyFunction>>,
149    thresh_offdiag: Option<f64>,
150    thresh_zeroov: Option<f64>,
151) -> PyResult<(Vec<String>, Py<PyAny>)> {
152    let pd_res: SymmetryGroupDetectionResult =
153        read_qsym2_binary(inp_sym.clone(), QSym2FileType::Sym)
154            .map_err(|err| PyIOError::new_err(err.to_string()))?;
155
156    let mut file_name = inp_sym.to_path_buf();
157    file_name.set_extension(QSym2FileType::Sym.ext());
158    qsym2_output!(
159        "Symmetry-group detection results read in from {}.",
160        file_name.display(),
161    );
162    qsym2_output!("");
163
164    let mol = &pd_res.pre_symmetry.recentred_molecule;
165    let baos = pybaos
166        .iter()
167        .map(|bao| {
168            bao.to_qsym2(mol)
169                .map_err(|err| PyRuntimeError::new_err(err.to_string()))
170        })
171        .collect::<Result<Vec<_>, _>>()?;
172    let baos_ref = baos.iter().collect::<Vec<_>>();
173    let augment_to_generalised = match symmetry_transformation_kind {
174        SymmetryTransformationKind::SpatialWithSpinTimeReversal
175        | SymmetryTransformationKind::Spin
176        | SymmetryTransformationKind::SpinSpatial => true,
177        SymmetryTransformationKind::Spatial => false,
178    };
179
180    let symbolic_projection_targets = projection_targets
181        .iter()
182        .filter_map(|pt| match pt {
183            PyProjectionTarget::Symbolic(symbolic_pt) => Some(symbolic_pt.clone()),
184            PyProjectionTarget::Numeric(_) => None,
185        })
186        .collect_vec();
187    let numeric_projection_targets = projection_targets
188        .iter()
189        .filter_map(|pt| match pt {
190            PyProjectionTarget::Symbolic(_) => None,
191            PyProjectionTarget::Numeric(numeric_pt) => Some(*numeric_pt),
192        })
193        .collect_vec();
194
195    let sdp_params = SlaterDeterminantProjectionParams::builder()
196        .symbolic_projection_targets(Some(symbolic_projection_targets))
197        .numeric_projection_targets(Some(numeric_projection_targets))
198        .use_magnetic_group(use_magnetic_group.clone())
199        .use_double_group(use_double_group)
200        .symmetry_transformation_kind(symmetry_transformation_kind)
201        .write_character_table(if write_character_table {
202            Some(CharacterTableDisplay::Symbolic)
203        } else {
204            None
205        })
206        .infinite_order_to_finite(infinite_order_to_finite)
207        .build()
208        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
209
210    // Decision tree:
211    // - real determinant?
212    //   + yes:
213    //     - structure_constraint:
214    //       + SpinConstraint:
215    //         - use_magnetic_group:
216    //           + Some(Corepresentation)
217    //           + Some(Representation) | None
218    //       + SpinOrbitCoupled: not supported
219    //   - no:
220    //     - structure_constraint:
221    //       + SpinConstraint:
222    //         - use_magnetic_group:
223    //           + Some(Corepresentation)
224    //           + Some(Representation) | None
225    //       + SpinOrbitCoupled:
226    //         - use_magnetic_group:
227    //           + Some(Corepresentation)
228    //           + Some(Representation) | None
229    match pydet {
230        PySlaterDeterminant::Real(pydet_r) => {
231            if matches!(
232                pydet_r.structure_constraint,
233                PyStructureConstraint::SpinOrbitCoupled(_)
234            ) {
235                return Err(PyRuntimeError::new_err("Real determinants are not compatible with spin--orbit-coupled structure constraint.".to_string()));
236            }
237
238            let sao_opt = sao.and_then(|pysao| match pysao {
239                PyArray2RC::Real(pysao_r) => Some(pysao_r.to_owned_array()),
240                PyArray2RC::Complex(_) => None,
241            });
242            let sao_h_opt = sao_h.and_then(|pysao_h| match pysao_h {
243                PyArray2RC::Real(pysao_h_r) => Some(pysao_h_r.to_owned_array()),
244                PyArray2RC::Complex(_) => None,
245            });
246            let enuc_opt = enuc.and_then(|pyenuc| match pyenuc {
247                PyScalarRC::Real(pyenuc_r) => Some(pyenuc_r),
248                PyScalarRC::Complex(_) => None,
249            });
250            let onee_opt = onee.and_then(|pyonee| match pyonee {
251                PyArray2RC::Real(pyonee_r) => Some(pyonee_r.to_owned_array()),
252                PyArray2RC::Complex(_) => None,
253            });
254            let twoe_opt = twoe.and_then(|pytwoe| match pytwoe {
255                PyArray4RC::Real(pytwoe_r) => Some(pytwoe_r.to_owned_array()),
256                PyArray4RC::Complex(_) => None,
257            });
258            let get_jk_opt = py_get_jk.map(|py_get_jk_inner| {
259                crate::generate_get_jk!(get_jk_r, py_get_jk_inner, f64);
260                get_jk_r
261            });
262
263            let det_r = if augment_to_generalised {
264                pydet_r
265                    .to_qsym2::<SpinConstraint>(&baos_ref, mol)
266                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
267                    .to_generalised()
268            } else {
269                pydet_r
270                    .to_qsym2::<SpinConstraint>(&baos_ref, mol)
271                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
272            };
273
274            let projected_sds = match &use_magnetic_group {
275                Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
276                    return Err(PyRuntimeError::new_err(
277                        "Projection using corepresentations is not yet supported.",
278                    ));
279                }
280                Some(MagneticSymmetryAnalysisKind::Representation) | None => {
281                    let mut sdp_driver = if calculate_energies {
282                        SlaterDeterminantProjectionDriver::<
283                            UnitaryRepresentedSymmetryGroup,
284                            f64,
285                            SpinConstraint,
286                            _,
287                        >::builder()
288                        .parameters(&sdp_params)
289                        .determinant(&det_r)
290                        .symmetry_group(&pd_res)
291                        .sao(sao_opt.as_ref())
292                        .sao_h(sao_h_opt.as_ref())
293                        .enuc(enuc_opt)
294                        .onee(onee_opt.as_ref())
295                        .twoe(twoe_opt.as_ref())
296                        .get_jk(get_jk_opt)
297                        .thresh_offdiag(thresh_offdiag)
298                        .thresh_zeroov(thresh_zeroov)
299                        .build()
300                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
301                    } else {
302                        SlaterDeterminantProjectionDriver::<
303                            UnitaryRepresentedSymmetryGroup,
304                            f64,
305                            SpinConstraint,
306                            _,
307                        >::builder()
308                        .parameters(&sdp_params)
309                        .determinant(&det_r)
310                        .symmetry_group(&pd_res)
311                        .sao(sao_opt.as_ref())
312                        .sao_h(sao_h_opt.as_ref())
313                        .build()
314                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
315                    };
316                    py.detach(|| {
317                        sdp_driver
318                            .run()
319                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
320                    })?;
321                    sdp_driver
322                        .result()
323                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
324                        .projected_determinants()
325                        .clone()
326                }
327            };
328            let multidets_vec = projected_sds
329                .values()
330                .map(|v| v.as_ref())
331                .collect::<Result<Vec<_>, _>>()
332                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
333            let multidets = MultiDeterminants::from_multideterminant_vec(
334                &multidets_vec.into_iter().collect::<Vec<_>>(),
335            )
336            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
337
338            let energies = multidets.energies();
339
340            let (rows, coefficientss): (Vec<_>, Vec<_>) = projected_sds
341                .iter()
342                .map(|(row, multidet_res)| {
343                    let coefficients = multidet_res
344                        .as_ref()
345                        .map(|multidet| multidet.coefficients().iter().cloned().collect_vec())
346                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
347                    Ok::<_, PyErr>((row.to_string(), coefficients))
348                })
349                .collect::<Result<Vec<_>, _>>()?
350                .into_iter()
351                .unzip();
352            let basis = projected_sds
353                .iter()
354                .next()
355                .and_then(|(_, multidet_res)| {
356                    multidet_res.as_ref().ok().and_then(|multidet| {
357                        multidet
358                            .basis()
359                            .iter()
360                            .map(|det_res| det_res.and_then(|det| det.to_python(py)))
361                            .collect::<Result<Vec<_>, _>>()
362                            .ok()
363                    })
364                })
365                .ok_or_else(|| {
366                    PyRuntimeError::new_err(
367                        "Unable to obtain the basis of Slater determinants.".to_string(),
368                    )
369                })?;
370            let coefficientss_arr = Array2::from_shape_vec(
371                (basis.len(), coefficientss.len()).f(),
372                coefficientss.into_iter().flatten().collect_vec(),
373            )
374            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
375            .to_pyarray(py);
376            let energies_arr = energies.mapv(|v| v.unwrap_or(f64::NAN)).to_pyarray(py);
377
378            let density_matrices = match (
379                calculate_density_matrices,
380                thresh_offdiag,
381                thresh_zeroov,
382                sao_opt.as_ref(),
383            ) {
384                (true, Some(thresh_offdiag), Some(thresh_zeroov), Some(sao)) => {
385                    log::debug!("Calculating density matrices...");
386                    let denmats_opt = multidets
387                        .density_matrices(&sao.view(), thresh_offdiag, thresh_zeroov, true)
388                        .map(|denmats| {
389                            denmats
390                                .axis_iter(Axis(0))
391                                .map(|denmat| denmat.to_pyarray(py))
392                                .collect_vec()
393                        })
394                        .ok();
395                    log::debug!("Calculating density matrices... Done.");
396                    denmats_opt
397                }
398                _ => None,
399            };
400
401            let pymultidet = PyMultiDeterminantsReal::new(
402                basis,
403                coefficientss_arr,
404                energies_arr,
405                density_matrices,
406                pydet_r.threshold,
407            )
408            .into_py_any(py)?;
409            Ok((rows, pymultidet))
410        }
411        PySlaterDeterminant::Complex(pydet_c) => {
412            let sao_opt = sao.map(|pysao| match pysao {
413                PyArray2RC::Real(pysao_r) => pysao_r.to_owned_array().mapv(Complex::from),
414                PyArray2RC::Complex(pysao_c) => pysao_c.to_owned_array(),
415            });
416            let sao_h_opt = sao_h.map(|pysao_h| match pysao_h {
417                PyArray2RC::Real(pysao_h_r) => pysao_h_r.to_owned_array().mapv(Complex::from),
418                PyArray2RC::Complex(pysao_h_c) => pysao_h_c.to_owned_array(),
419            });
420            let enuc_opt = enuc.map(|pyenuc| match pyenuc {
421                PyScalarRC::Real(pyenuc_r) => Complex::from(pyenuc_r),
422                PyScalarRC::Complex(pyenuc_c) => pyenuc_c,
423            });
424            let onee_opt = onee.map(|pyonee| match pyonee {
425                PyArray2RC::Real(pyonee_r) => pyonee_r.to_owned_array().mapv(Complex::from),
426                PyArray2RC::Complex(pyonee_c) => pyonee_c.to_owned_array(),
427            });
428            let twoe_opt = twoe.map(|pytwoe| match pytwoe {
429                PyArray4RC::Real(pytwoe_r) => pytwoe_r.to_owned_array().mapv(Complex::from),
430                PyArray4RC::Complex(pytwoe_c) => pytwoe_c.to_owned_array(),
431            });
432            let get_jk_opt = py_get_jk.map(|py_get_jk_inner| {
433                crate::generate_get_jk!(get_jk_c, py_get_jk_inner, Complex<f64>);
434                get_jk_c
435            });
436
437            match pydet_c.structure_constraint {
438                PyStructureConstraint::SpinConstraint(_) => {
439                    let det_c = if augment_to_generalised {
440                        pydet_c
441                            .to_qsym2::<SpinConstraint>(&baos_ref, mol)
442                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
443                            .to_generalised()
444                    } else {
445                        pydet_c
446                            .to_qsym2::<SpinConstraint>(&baos_ref, mol)
447                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
448                    };
449
450                    let projected_sds = match &use_magnetic_group {
451                        Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
452                            return Err(PyRuntimeError::new_err(
453                                "Projection using corepresentations is not yet supported.",
454                            ));
455                        }
456                        Some(MagneticSymmetryAnalysisKind::Representation) | None => {
457                            let mut sdp_driver = if calculate_energies {
458                                SlaterDeterminantProjectionDriver::<
459                                    UnitaryRepresentedSymmetryGroup,
460                                    C128,
461                                    SpinConstraint,
462                                    _,
463                                >::builder()
464                                .parameters(&sdp_params)
465                                .determinant(&det_c)
466                                .symmetry_group(&pd_res)
467                                .sao(sao_opt.as_ref())
468                                .sao_h(sao_h_opt.as_ref())
469                                .enuc(enuc_opt)
470                                .onee(onee_opt.as_ref())
471                                .twoe(twoe_opt.as_ref())
472                                .get_jk(get_jk_opt)
473                                .thresh_offdiag(thresh_offdiag)
474                                .thresh_zeroov(thresh_zeroov)
475                                .build()
476                                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
477                            } else {
478                                SlaterDeterminantProjectionDriver::<
479                                    UnitaryRepresentedSymmetryGroup,
480                                    C128,
481                                    SpinConstraint,
482                                    _,
483                                >::builder()
484                                .parameters(&sdp_params)
485                                .determinant(&det_c)
486                                .symmetry_group(&pd_res)
487                                .sao(sao_opt.as_ref())
488                                .sao_h(sao_h_opt.as_ref())
489                                .build()
490                                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
491                            };
492                            py.detach(|| {
493                                sdp_driver
494                                    .run()
495                                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))
496                            })?;
497                            sdp_driver
498                                .result()
499                                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
500                                .projected_determinants()
501                                .clone()
502                        }
503                    };
504                    let multidets_vec = projected_sds
505                        .values()
506                        .map(|v| v.as_ref())
507                        .collect::<Result<Vec<_>, _>>()
508                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
509                    let multidets = MultiDeterminants::from_multideterminant_vec(
510                        &multidets_vec.into_iter().collect::<Vec<_>>(),
511                    )
512                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
513
514                    let energies = multidets.energies();
515
516                    let (rows, coefficientss): (Vec<_>, Vec<_>) = projected_sds
517                        .iter()
518                        .map(|(row, multidet_res)| {
519                            let coefficients = multidet_res
520                                .as_ref()
521                                .map(|multidet| {
522                                    multidet.coefficients().iter().cloned().collect_vec()
523                                })
524                                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
525                            Ok::<_, PyErr>((row.to_string(), coefficients))
526                        })
527                        .collect::<Result<Vec<_>, _>>()?
528                        .into_iter()
529                        .unzip();
530                    let basis = projected_sds
531                        .iter()
532                        .next()
533                        .and_then(|(_, multidet_res)| {
534                            multidet_res.as_ref().ok().and_then(|multidet| {
535                                multidet
536                                    .basis()
537                                    .iter()
538                                    .map(|det_res| det_res.and_then(|det| det.to_python(py)))
539                                    .collect::<Result<Vec<_>, _>>()
540                                    .ok()
541                            })
542                        })
543                        .ok_or_else(|| {
544                            PyRuntimeError::new_err(
545                                "Unable to obtain the basis of Slater determinants.".to_string(),
546                            )
547                        })?;
548                    let coefficientss_arr = Array2::from_shape_vec(
549                        (basis.len(), coefficientss.len()).f(),
550                        coefficientss.into_iter().flatten().collect_vec(),
551                    )
552                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
553                    .to_pyarray(py);
554                    let energies_arr = energies
555                        .mapv(|v| v.unwrap_or(Complex::from(f64::NAN)))
556                        .to_pyarray(py);
557
558                    let density_matrices = match (
559                        calculate_density_matrices,
560                        thresh_offdiag,
561                        thresh_zeroov,
562                        sao_opt.as_ref(),
563                    ) {
564                        (true, Some(thresh_offdiag), Some(thresh_zeroov), Some(sao)) => {
565                            log::debug!("Calculating density matrices...");
566                            let denmats_opt = multidets
567                                .density_matrices(&sao.view(), thresh_offdiag, thresh_zeroov, true)
568                                .map(|denmats| {
569                                    denmats
570                                        .axis_iter(Axis(0))
571                                        .map(|denmat| denmat.to_pyarray(py))
572                                        .collect_vec()
573                                })
574                                .ok();
575                            log::debug!("Calculating density matrices... Done.");
576                            denmats_opt
577                        }
578                        _ => None,
579                    };
580
581                    let pymultidet = PyMultiDeterminantsComplex::new(
582                        basis,
583                        coefficientss_arr,
584                        energies_arr,
585                        density_matrices,
586                        pydet_c.threshold,
587                    )
588                    .into_py_any(py)?;
589                    Ok((rows, pymultidet))
590                }
591                PyStructureConstraint::SpinOrbitCoupled(_) => {
592                    let det_c = pydet_c
593                        .to_qsym2::<SpinOrbitCoupled>(&baos_ref, mol)
594                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
595
596                    let projected_sds = match &use_magnetic_group {
597                        Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
598                            return Err(PyRuntimeError::new_err(
599                                "Projection using corepresentations is not yet supported.",
600                            ));
601                        }
602                        Some(MagneticSymmetryAnalysisKind::Representation) | None => {
603                            let mut sdp_driver = if calculate_energies {
604                                SlaterDeterminantProjectionDriver::<
605                                    UnitaryRepresentedSymmetryGroup,
606                                    C128,
607                                    SpinOrbitCoupled,
608                                    _,
609                                >::builder()
610                                .parameters(&sdp_params)
611                                .determinant(&det_c)
612                                .symmetry_group(&pd_res)
613                                .sao(sao_opt.as_ref())
614                                .sao_h(sao_h_opt.as_ref())
615                                .enuc(enuc_opt)
616                                .onee(onee_opt.as_ref())
617                                .twoe(twoe_opt.as_ref())
618                                .get_jk(get_jk_opt)
619                                .thresh_offdiag(thresh_offdiag)
620                                .thresh_zeroov(thresh_zeroov)
621                                .build()
622                                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
623                            } else {
624                                SlaterDeterminantProjectionDriver::<
625                                    UnitaryRepresentedSymmetryGroup,
626                                    C128,
627                                    SpinOrbitCoupled,
628                                    _,
629                                >::builder()
630                                .parameters(&sdp_params)
631                                .determinant(&det_c)
632                                .symmetry_group(&pd_res)
633                                .sao(sao_opt.as_ref())
634                                .sao_h(sao_h_opt.as_ref())
635                                .build()
636                                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
637                            };
638                            py.detach(|| {
639                                sdp_driver
640                                    .run()
641                                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))
642                            })?;
643                            sdp_driver
644                                .result()
645                                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
646                                .projected_determinants()
647                                .clone()
648                        }
649                    };
650                    let multidets_vec = projected_sds
651                        .values()
652                        .map(|v| v.as_ref())
653                        .collect::<Result<Vec<_>, _>>()
654                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
655                    let multidets = MultiDeterminants::from_multideterminant_vec(
656                        &multidets_vec.into_iter().collect::<Vec<_>>(),
657                    )
658                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
659
660                    let energies = multidets.energies();
661
662                    let (rows, coefficientss): (Vec<_>, Vec<_>) = projected_sds
663                        .iter()
664                        .map(|(row, multidet_res)| {
665                            let coefficients = multidet_res
666                                .as_ref()
667                                .map(|multidet| {
668                                    multidet.coefficients().iter().cloned().collect_vec()
669                                })
670                                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
671                            Ok::<_, PyErr>((row.to_string(), coefficients))
672                        })
673                        .collect::<Result<Vec<_>, _>>()?
674                        .into_iter()
675                        .unzip();
676                    let basis = projected_sds
677                        .iter()
678                        .next()
679                        .and_then(|(_, multidet_res)| {
680                            multidet_res.as_ref().ok().and_then(|multidet| {
681                                multidet
682                                    .basis()
683                                    .iter()
684                                    .map(|det_res| det_res.and_then(|det| det.to_python(py)))
685                                    .collect::<Result<Vec<_>, _>>()
686                                    .ok()
687                            })
688                        })
689                        .ok_or_else(|| {
690                            PyRuntimeError::new_err(
691                                "Unable to obtain the basis of Slater determinants.".to_string(),
692                            )
693                        })?;
694                    let coefficientss_arr = Array2::from_shape_vec(
695                        (basis.len(), coefficientss.len()).f(),
696                        coefficientss.into_iter().flatten().collect_vec(),
697                    )
698                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
699                    .to_pyarray(py);
700                    let energies_arr = energies
701                        .mapv(|v| v.unwrap_or(Complex::from(f64::NAN)))
702                        .to_pyarray(py);
703
704                    let density_matrices = match (
705                        calculate_density_matrices,
706                        thresh_offdiag,
707                        thresh_zeroov,
708                        sao_opt.as_ref(),
709                    ) {
710                        (true, Some(thresh_offdiag), Some(thresh_zeroov), Some(sao)) => {
711                            log::debug!("Calculating density matrices...");
712                            let denmats_opt = multidets
713                                .density_matrices(&sao.view(), thresh_offdiag, thresh_zeroov, true)
714                                .map(|denmats| {
715                                    denmats
716                                        .axis_iter(Axis(0))
717                                        .map(|denmat| denmat.to_pyarray(py))
718                                        .collect_vec()
719                                })
720                                .ok();
721                            log::debug!("Calculating density matrices... Done.");
722                            denmats_opt
723                        }
724                        _ => None,
725                    };
726
727                    let pymultidet = PyMultiDeterminantsComplex::new(
728                        basis,
729                        coefficientss_arr,
730                        energies_arr,
731                        density_matrices,
732                        pydet_c.threshold,
733                    )
734                    .into_py_any(py)?;
735                    Ok((rows, pymultidet))
736                }
737            }
738        }
739    }
740}