qsym2/bindings/python/projection/
slater_determinant.rs

1//! Python bindings for QSym² symmetry projection of Slater determinants.
2
3use std::path::PathBuf;
4
5use itertools::Itertools;
6use ndarray::{Array1, Array2, ShapeBuilder};
7use num_complex::Complex;
8use numpy::{PyArrayMethods, ToPyArray};
9use pyo3::exceptions::{PyIOError, PyRuntimeError};
10use pyo3::{IntoPyObjectExt, prelude::*};
11
12use crate::angmom::spinor_rotation_3d::{SpinConstraint, SpinOrbitCoupled};
13use crate::bindings::python::integrals::{PyBasisAngularOrder, PyStructureConstraint};
14use crate::bindings::python::projection::PyProjectionTarget;
15use crate::bindings::python::representation_analysis::PyArray2RC;
16use crate::bindings::python::representation_analysis::multideterminant::{
17    PyMultiDeterminantsComplex, PyMultiDeterminantsReal,
18};
19use crate::bindings::python::representation_analysis::slater_determinant::PySlaterDeterminant;
20use crate::drivers::QSym2Driver;
21use crate::drivers::projection::slater_determinant::{
22    SlaterDeterminantProjectionDriver, SlaterDeterminantProjectionParams,
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::UnitaryRepresentedSymmetryGroup;
31use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
32use crate::target::noci::basis::Basis;
33
34type C128 = Complex<f64>;
35
36// =====================
37// Functions definitions
38// =====================
39
40/// Python-exposed function to perform symmetry projection for real and complex Slater determinants.
41///
42/// # Arguments
43///
44/// * `inp_sym` - A path to the [`QSym2FileType::Sym`] file containing the symmetry-group detection
45/// result for the system. This will be used to construct abstract groups and character tables for
46/// symmetry projection.
47/// * `pydet` - A Slater determinant whose coefficient matrices are of type `float64` or `complex128`.
48/// * `projection_targets` - A sequence of subspace labels for projection. Each label is either a
49/// symbolic string or a numerical index for the subspace in the character table of the prevailing
50/// group.
51/// * `density_matrix_calculation_thresholds` - An optional pair of thresholds for Löwdin pairing,
52/// one for checking zero off-diagonal values, one for checking zero overlaps, when computing
53/// multi-determinantal density matrices. If `None`, no density matrices will be computed.
54/// * `pybaos` - Python-exposed structures containing basis angular order information, one for each
55/// explicit component per coefficient matrix.
56/// * `use_magnetic_group` - An option indicating if the magnetic group is to be used for symmetry
57/// analysis, and if so, whether unitary representations or unitary-antiunitary corepresentations
58/// should be used.
59/// * `use_double_group` - A boolean indicating if the double group of the prevailing symmetry
60/// group is to be used for representation analysis instead.
61/// * `symmetry_transformation_kind` - An enumerated type indicating the type of symmetry
62/// transformations to be performed on the origin electron density to generate the orbit.
63/// * `write_character_table` - A boolean indicating if the character table of the prevailing
64/// symmetry group is to be printed out.
65/// * `infinite_order_to_finite` - The finite order with which infinite-order generators are to be
66/// interpreted to form a finite subgroup of the prevailing infinite group. This finite subgroup
67/// will be used for symmetry analysis.
68/// * `sao` - The optional atomic-orbital overlap matrix whose elements are of type `float64` or
69/// `complex128`. If this is not present, no squared norms of the resulting multi-determinants will
70/// be computed.
71/// * `sao_h` - The optional complex-symmetric atomic-orbital overlap matrix whose elements
72/// are of type `float64` or `complex128`. This is required if antiunitary symmetry operations are
73/// involved.
74///
75/// # Returns
76///
77/// The result will be returned as a tuple where the first item is a list of the labels of the
78/// subspaces used for projection, and the second item is an object containing the Slater
79/// determinant basis and the linear combination coefficients as a two-dimensional array with each
80/// column corresponding to one projected state.
81#[allow(clippy::too_many_arguments)]
82#[pyfunction]
83#[pyo3(signature = (
84    inp_sym,
85    pydet,
86    projection_targets,
87    density_matrix_calculation_thresholds,
88    pybaos,
89    use_magnetic_group,
90    use_double_group,
91    symmetry_transformation_kind,
92    write_character_table=true,
93    infinite_order_to_finite=None,
94    sao=None,
95    sao_h=None,
96))]
97pub fn project_slater_determinant(
98    py: Python<'_>,
99    inp_sym: PathBuf,
100    pydet: PySlaterDeterminant,
101    projection_targets: Vec<PyProjectionTarget>,
102    density_matrix_calculation_thresholds: Option<(f64, f64)>,
103    pybaos: Vec<PyBasisAngularOrder>,
104    use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
105    use_double_group: bool,
106    symmetry_transformation_kind: SymmetryTransformationKind,
107    write_character_table: bool,
108    infinite_order_to_finite: Option<u32>,
109    sao: Option<PyArray2RC>,
110    sao_h: Option<PyArray2RC>,
111) -> PyResult<(Vec<String>, Py<PyAny>)> {
112    let pd_res: SymmetryGroupDetectionResult =
113        read_qsym2_binary(inp_sym.clone(), QSym2FileType::Sym)
114            .map_err(|err| PyIOError::new_err(err.to_string()))?;
115
116    let mut file_name = inp_sym.to_path_buf();
117    file_name.set_extension(QSym2FileType::Sym.ext());
118    qsym2_output!(
119        "Symmetry-group detection results read in from {}.",
120        file_name.display(),
121    );
122    qsym2_output!("");
123
124    let mol = &pd_res.pre_symmetry.recentred_molecule;
125    let baos = pybaos
126        .iter()
127        .map(|bao| {
128            bao.to_qsym2(mol)
129                .map_err(|err| PyRuntimeError::new_err(err.to_string()))
130        })
131        .collect::<Result<Vec<_>, _>>()?;
132    let baos_ref = baos.iter().collect::<Vec<_>>();
133    let augment_to_generalised = match symmetry_transformation_kind {
134        SymmetryTransformationKind::SpatialWithSpinTimeReversal
135        | SymmetryTransformationKind::Spin
136        | SymmetryTransformationKind::SpinSpatial => true,
137        SymmetryTransformationKind::Spatial => false,
138    };
139
140    let symbolic_projection_targets = projection_targets
141        .iter()
142        .filter_map(|pt| match pt {
143            PyProjectionTarget::Symbolic(symbolic_pt) => Some(symbolic_pt.clone()),
144            PyProjectionTarget::Numeric(_) => None,
145        })
146        .collect_vec();
147    let numeric_projection_targets = projection_targets
148        .iter()
149        .filter_map(|pt| match pt {
150            PyProjectionTarget::Symbolic(_) => None,
151            PyProjectionTarget::Numeric(numeric_pt) => Some(*numeric_pt),
152        })
153        .collect_vec();
154
155    let sdp_params = SlaterDeterminantProjectionParams::builder()
156        .symbolic_projection_targets(Some(symbolic_projection_targets))
157        .numeric_projection_targets(Some(numeric_projection_targets))
158        .use_magnetic_group(use_magnetic_group.clone())
159        .use_double_group(use_double_group)
160        .symmetry_transformation_kind(symmetry_transformation_kind)
161        .write_character_table(if write_character_table {
162            Some(CharacterTableDisplay::Symbolic)
163        } else {
164            None
165        })
166        .infinite_order_to_finite(infinite_order_to_finite)
167        .build()
168        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
169
170    // Decision tree:
171    // - real determinant?
172    //   + yes:
173    //     - structure_constraint:
174    //       + SpinConstraint:
175    //         - use_magnetic_group:
176    //           + Some(Corepresentation)
177    //           + Some(Representation) | None
178    //       + SpinOrbitCoupled: not supported
179    //   - no:
180    //     - structure_constraint:
181    //       + SpinConstraint:
182    //         - use_magnetic_group:
183    //           + Some(Corepresentation)
184    //           + Some(Representation) | None
185    //       + SpinOrbitCoupled:
186    //         - use_magnetic_group:
187    //           + Some(Corepresentation)
188    //           + Some(Representation) | None
189    match pydet {
190        PySlaterDeterminant::Real(pydet_r) => {
191            if matches!(
192                pydet_r.structure_constraint,
193                PyStructureConstraint::SpinOrbitCoupled(_)
194            ) {
195                return Err(PyRuntimeError::new_err("Real determinants are not compatible with spin--orbit-coupled structure constraint.".to_string()));
196            }
197
198            let sao_opt = sao.and_then(|pysao| match pysao {
199                PyArray2RC::Real(pysao_r) => Some(pysao_r.to_owned_array()),
200                PyArray2RC::Complex(_) => None,
201            });
202            let sao_h_opt = sao_h.and_then(|pysao_h| match pysao_h {
203                PyArray2RC::Real(pysao_h_r) => Some(pysao_h_r.to_owned_array()),
204                PyArray2RC::Complex(_) => None,
205            });
206
207            let det_r = if augment_to_generalised {
208                pydet_r
209                    .to_qsym2::<SpinConstraint>(&baos_ref, mol)
210                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
211                    .to_generalised()
212            } else {
213                pydet_r
214                    .to_qsym2::<SpinConstraint>(&baos_ref, mol)
215                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
216            };
217
218            let projected_sds = match &use_magnetic_group {
219                Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
220                    return Err(PyRuntimeError::new_err(
221                        "Projection using corepresentations is not yet supported.",
222                    ));
223                }
224                Some(MagneticSymmetryAnalysisKind::Representation) | None => {
225                    let mut sdp_driver = SlaterDeterminantProjectionDriver::<
226                        UnitaryRepresentedSymmetryGroup,
227                        f64,
228                        _,
229                    >::builder()
230                    .parameters(&sdp_params)
231                    .determinant(&det_r)
232                    .symmetry_group(&pd_res)
233                    .sao(sao_opt.as_ref())
234                    .sao_h(sao_h_opt.as_ref())
235                    .build()
236                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
237                    py.detach(|| {
238                        sdp_driver
239                            .run()
240                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
241                    })?;
242                    sdp_driver
243                        .result()
244                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
245                        .projected_determinants()
246                        .clone()
247                }
248            };
249            let (rows, (coefficientss, energies)): (Vec<_>, (Vec<_>, Vec<_>)) = projected_sds
250                .iter()
251                .map(|(row, multidet_res)| {
252                    let (coefficients, energy) = multidet_res
253                        .as_ref()
254                        .map(|multidet| {
255                            let coefficients =
256                                multidet.coefficients().iter().cloned().collect_vec();
257                            let energy = *multidet.energy().unwrap_or(&f64::NAN);
258                            (coefficients, energy)
259                        })
260                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
261                    Ok::<_, PyErr>((row.to_string(), (coefficients, energy)))
262                })
263                .collect::<Result<Vec<_>, _>>()?
264                .into_iter()
265                .unzip();
266            let basis = projected_sds
267                .iter()
268                .next()
269                .and_then(|(_, multidet_res)| {
270                    multidet_res.as_ref().ok().and_then(|multidet| {
271                        multidet
272                            .basis()
273                            .iter()
274                            .map(|det_res| det_res.and_then(|det| det.to_python(py)))
275                            .collect::<Result<Vec<_>, _>>()
276                            .ok()
277                    })
278                })
279                .ok_or_else(|| {
280                    PyRuntimeError::new_err(
281                        "Unable to obtain the basis of Slater determinants.".to_string(),
282                    )
283                })?;
284            let coefficientss_arr = Array2::from_shape_vec(
285                (basis.len(), coefficientss.len()).f(),
286                coefficientss.into_iter().flatten().collect_vec(),
287            )
288            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
289            .to_pyarray(py);
290            let energies_arr = Array1::from_vec(energies).to_pyarray(py);
291            let density_matrices = match (density_matrix_calculation_thresholds, sao_opt.as_ref()) {
292                (Some((thresh_offdiag, thresh_zeroov)), Some(sao)) => Some(
293                    projected_sds
294                        .iter()
295                        .map(|(_, multidet_res)| {
296                            multidet_res
297                                .as_ref()
298                                .map_err(|err| PyRuntimeError::new_err(err.to_string()))
299                                .and_then(|multidet| {
300                                    multidet
301                                        .density_matrix(
302                                            &sao.view(),
303                                            thresh_offdiag,
304                                            thresh_zeroov,
305                                            true,
306                                        )
307                                        .map(|denmat| denmat.to_pyarray(py))
308                                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))
309                                })
310                        })
311                        .collect::<Result<Vec<_>, _>>()?,
312                ),
313                _ => None,
314            };
315            let pymultidet = PyMultiDeterminantsReal::new(
316                basis,
317                coefficientss_arr,
318                energies_arr,
319                density_matrices,
320                pydet_r.threshold,
321            )
322            .into_py_any(py)?;
323            Ok((rows, pymultidet))
324        }
325        PySlaterDeterminant::Complex(pydet_c) => {
326            let sao_opt = sao.map(|pysao| match pysao {
327                PyArray2RC::Real(pysao_r) => pysao_r.to_owned_array().mapv(Complex::from),
328                PyArray2RC::Complex(pysao_c) => pysao_c.to_owned_array(),
329            });
330            let sao_h_opt = sao_h.map(|pysao_h| match pysao_h {
331                PyArray2RC::Real(pysao_h_r) => pysao_h_r.to_owned_array().mapv(Complex::from),
332                PyArray2RC::Complex(pysao_h_c) => pysao_h_c.to_owned_array(),
333            });
334
335            match pydet_c.structure_constraint {
336                PyStructureConstraint::SpinConstraint(_) => {
337                    let det_c = if augment_to_generalised {
338                        pydet_c
339                            .to_qsym2::<SpinConstraint>(&baos_ref, mol)
340                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
341                            .to_generalised()
342                    } else {
343                        pydet_c
344                            .to_qsym2::<SpinConstraint>(&baos_ref, mol)
345                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
346                    };
347
348                    let projected_sds = match &use_magnetic_group {
349                        Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
350                            return Err(PyRuntimeError::new_err(
351                                "Projection using corepresentations is not yet supported.",
352                            ));
353                        }
354                        Some(MagneticSymmetryAnalysisKind::Representation) | None => {
355                            let mut sdp_driver = SlaterDeterminantProjectionDriver::<
356                                UnitaryRepresentedSymmetryGroup,
357                                C128,
358                                _,
359                            >::builder()
360                            .parameters(&sdp_params)
361                            .determinant(&det_c)
362                            .symmetry_group(&pd_res)
363                            .sao(sao_opt.as_ref())
364                            .sao_h(sao_h_opt.as_ref())
365                            .build()
366                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
367                            py.detach(|| {
368                                sdp_driver
369                                    .run()
370                                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))
371                            })?;
372                            sdp_driver
373                                .result()
374                                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
375                                .projected_determinants()
376                                .clone()
377                        }
378                    };
379                    let (rows, (coefficientss, energies)): (Vec<_>, (Vec<_>, Vec<_>)) =
380                        projected_sds
381                            .iter()
382                            .map(|(row, multidet_res)| {
383                                let (coefficients, energy) = multidet_res
384                                    .as_ref()
385                                    .map(|multidet| {
386                                        let coefficients =
387                                            multidet.coefficients().iter().cloned().collect_vec();
388                                        let energy =
389                                            *multidet.energy().unwrap_or(&Complex::from(f64::NAN));
390                                        (coefficients, energy)
391                                    })
392                                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
393                                Ok::<_, PyErr>((row.to_string(), (coefficients, energy)))
394                            })
395                            .collect::<Result<Vec<_>, _>>()?
396                            .into_iter()
397                            .unzip();
398                    let basis = projected_sds
399                        .iter()
400                        .next()
401                        .and_then(|(_, multidet_res)| {
402                            multidet_res.as_ref().ok().and_then(|multidet| {
403                                multidet
404                                    .basis()
405                                    .iter()
406                                    .map(|det_res| det_res.and_then(|det| det.to_python(py)))
407                                    .collect::<Result<Vec<_>, _>>()
408                                    .ok()
409                            })
410                        })
411                        .ok_or_else(|| {
412                            PyRuntimeError::new_err(
413                                "Unable to obtain the basis of Slater determinants.".to_string(),
414                            )
415                        })?;
416                    let coefficientss_arr = Array2::from_shape_vec(
417                        (basis.len(), coefficientss.len()).f(),
418                        coefficientss.into_iter().flatten().collect_vec(),
419                    )
420                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
421                    .to_pyarray(py);
422                    let energies_arr = Array1::from_vec(energies).to_pyarray(py);
423                    let density_matrices =
424                        match (density_matrix_calculation_thresholds, sao_opt.as_ref()) {
425                            (Some((thresh_offdiag, thresh_zeroov)), Some(sao)) => Some(
426                                projected_sds
427                                    .iter()
428                                    .map(|(_, multidet_res)| {
429                                        multidet_res
430                                            .as_ref()
431                                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
432                                            .and_then(|multidet| {
433                                                multidet
434                                                    .density_matrix(
435                                                        &sao.view(),
436                                                        thresh_offdiag,
437                                                        thresh_zeroov,
438                                                        true,
439                                                    )
440                                                    .map(|denmat| denmat.to_pyarray(py))
441                                                    .map_err(|err| {
442                                                        PyRuntimeError::new_err(err.to_string())
443                                                    })
444                                            })
445                                    })
446                                    .collect::<Result<Vec<_>, _>>()?,
447                            ),
448                            _ => None,
449                        };
450
451                    let pymultidet = PyMultiDeterminantsComplex::new(
452                        basis,
453                        coefficientss_arr,
454                        energies_arr,
455                        density_matrices,
456                        pydet_c.threshold,
457                    )
458                    .into_py_any(py)?;
459                    Ok((rows, pymultidet))
460                }
461                PyStructureConstraint::SpinOrbitCoupled(_) => {
462                    let det_c = pydet_c
463                        .to_qsym2::<SpinOrbitCoupled>(&baos_ref, mol)
464                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
465
466                    let projected_sds = match &use_magnetic_group {
467                        Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
468                            return Err(PyRuntimeError::new_err(
469                                "Projection using corepresentations is not yet supported.",
470                            ));
471                        }
472                        Some(MagneticSymmetryAnalysisKind::Representation) | None => {
473                            let mut sdp_driver = SlaterDeterminantProjectionDriver::<
474                                UnitaryRepresentedSymmetryGroup,
475                                C128,
476                                _,
477                            >::builder()
478                            .parameters(&sdp_params)
479                            .determinant(&det_c)
480                            .symmetry_group(&pd_res)
481                            .sao(sao_opt.as_ref())
482                            .sao_h(sao_h_opt.as_ref())
483                            .build()
484                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
485                            py.detach(|| {
486                                sdp_driver
487                                    .run()
488                                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))
489                            })?;
490                            sdp_driver
491                                .result()
492                                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
493                                .projected_determinants()
494                                .clone()
495                        }
496                    };
497                    let (rows, (coefficientss, energies)): (Vec<_>, (Vec<_>, Vec<_>)) =
498                        projected_sds
499                            .iter()
500                            .map(|(row, multidet_res)| {
501                                let (coefficients, energy) = multidet_res
502                                    .as_ref()
503                                    .map(|multidet| {
504                                        let coefficients =
505                                            multidet.coefficients().iter().cloned().collect_vec();
506                                        let energy =
507                                            *multidet.energy().unwrap_or(&Complex::from(f64::NAN));
508                                        (coefficients, energy)
509                                    })
510                                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
511                                Ok::<_, PyErr>((row.to_string(), (coefficients, energy)))
512                            })
513                            .collect::<Result<Vec<_>, _>>()?
514                            .into_iter()
515                            .unzip();
516                    let basis = projected_sds
517                        .iter()
518                        .next()
519                        .and_then(|(_, multidet_res)| {
520                            multidet_res.as_ref().ok().and_then(|multidet| {
521                                multidet
522                                    .basis()
523                                    .iter()
524                                    .map(|det_res| det_res.and_then(|det| det.to_python(py)))
525                                    .collect::<Result<Vec<_>, _>>()
526                                    .ok()
527                            })
528                        })
529                        .ok_or_else(|| {
530                            PyRuntimeError::new_err(
531                                "Unable to obtain the basis of Slater determinants.".to_string(),
532                            )
533                        })?;
534                    let coefficientss_arr = Array2::from_shape_vec(
535                        (basis.len(), coefficientss.len()).f(),
536                        coefficientss.into_iter().flatten().collect_vec(),
537                    )
538                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
539                    .to_pyarray(py);
540                    let energies_arr = Array1::from_vec(energies).to_pyarray(py);
541                    let density_matrices =
542                        match (density_matrix_calculation_thresholds, sao_opt.as_ref()) {
543                            (Some((thresh_offdiag, thresh_zeroov)), Some(sao)) => Some(
544                                projected_sds
545                                    .iter()
546                                    .map(|(_, multidet_res)| {
547                                        multidet_res
548                                            .as_ref()
549                                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
550                                            .and_then(|multidet| {
551                                                multidet
552                                                    .density_matrix(
553                                                        &sao.view(),
554                                                        thresh_offdiag,
555                                                        thresh_zeroov,
556                                                        true,
557                                                    )
558                                                    .map(|denmat| denmat.to_pyarray(py))
559                                                    .map_err(|err| {
560                                                        PyRuntimeError::new_err(err.to_string())
561                                                    })
562                                            })
563                                    })
564                                    .collect::<Result<Vec<_>, _>>()?,
565                            ),
566                            _ => None,
567                        };
568
569                    let pymultidet = PyMultiDeterminantsComplex::new(
570                        basis,
571                        coefficientss_arr,
572                        energies_arr,
573                        density_matrices,
574                        pydet_c.threshold,
575                    )
576                    .into_py_any(py)?;
577                    Ok((rows, pymultidet))
578                }
579            }
580        }
581    }
582}