qsym2/drivers/projection/
density.rs

1//! Driver for symmetry projection of electron densities.
2
3use std::collections::HashSet;
4use std::fmt;
5use std::str::FromStr;
6
7use anyhow::{bail, format_err};
8use derive_builder::Builder;
9use duplicate::duplicate_item;
10use indexmap::IndexMap;
11use ndarray_linalg::{Lapack, Norm};
12use num::Complex;
13use num_complex::ComplexFloat;
14use serde::{Deserialize, Serialize, de::DeserializeOwned};
15
16use crate::analysis::EigenvalueComparisonMode;
17use crate::chartab::CharacterTable;
18use crate::chartab::chartab_group::CharacterProperties;
19use crate::drivers::QSym2Driver;
20use crate::drivers::representation_analysis::{
21    CharacterTableDisplay, MagneticSymmetryAnalysisKind, fn_construct_unitary_group, log_bao,
22    log_cc_transversal,
23};
24use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
25use crate::group::{GroupProperties, UnitaryRepresentedGroup};
26use crate::io::format::{
27    QSym2Output, log_subtitle, nice_bool, qsym2_output, write_subtitle, write_title,
28};
29use crate::projection::Projectable;
30use crate::symmetry::symmetry_group::{SymmetryGroupProperties, UnitaryRepresentedSymmetryGroup};
31use crate::symmetry::symmetry_symbols::MullikenIrrepSymbol;
32use crate::symmetry::symmetry_transformation::{SymmetryTransformable, SymmetryTransformationKind};
33use crate::target::density::Density;
34use crate::target::density::density_analysis::DensitySymmetryOrbit;
35
36#[cfg(test)]
37#[path = "density_tests.rs"]
38mod density_tests;
39
40// ----------
41// Parameters
42// ----------
43
44const fn default_symbolic() -> Option<CharacterTableDisplay> {
45    Some(CharacterTableDisplay::Symbolic)
46}
47
48/// Structure containing control parameters for electron density projection.
49#[derive(Clone, Builder, Debug, Serialize, Deserialize)]
50pub struct DensityProjectionParams {
51    /// Option indicating if the magnetic group is to be used for projection, and if so,
52    /// whether unitary representations or unitary-antiunitary corepresentations should be used.
53    #[builder(default = "None")]
54    #[serde(default)]
55    pub use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
56
57    /// Boolean indicating if the double group is to be used for symmetry projection.
58    #[builder(default = "false")]
59    #[serde(default)]
60    pub use_double_group: bool,
61
62    /// Option indicating if the character table of the group used for symmetry projection is to be
63    /// printed out.
64    #[builder(default = "Some(CharacterTableDisplay::Symbolic)")]
65    #[serde(default = "default_symbolic")]
66    pub write_character_table: Option<CharacterTableDisplay>,
67
68    /// The kind of symmetry transformation to be applied on the reference density to generate
69    /// the orbit for symmetry projection.
70    #[builder(default = "SymmetryTransformationKind::Spatial")]
71    #[serde(default)]
72    pub symmetry_transformation_kind: SymmetryTransformationKind,
73
74    /// The finite order to which any infinite-order symmetry element is reduced, so that a finite
75    /// subgroup of an infinite group can be used for the symmetry projection.
76    #[builder(default = "None")]
77    #[serde(default)]
78    pub infinite_order_to_finite: Option<u32>,
79
80    /// The projection targets supplied symbolically.
81    #[builder(default = "None")]
82    #[serde(default)]
83    pub symbolic_projection_targets: Option<Vec<String>>,
84
85    /// The projection targets supplied numerically, where each value gives the index of the
86    /// projection subspace based on the group's character table.
87    #[builder(default = "None")]
88    #[serde(default)]
89    pub numeric_projection_targets: Option<Vec<usize>>,
90}
91
92impl DensityProjectionParams {
93    /// Returns a builder to construct a [`DensityProjectionParams`] structure.
94    pub fn builder() -> DensityProjectionParamsBuilder {
95        DensityProjectionParamsBuilder::default()
96    }
97}
98
99impl fmt::Display for DensityProjectionParams {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        if let Some(symbolic_projection_targets) = self.symbolic_projection_targets.as_ref() {
102            writeln!(f, "Projection subspaces (symbolic):")?;
103            for projection_target in symbolic_projection_targets.iter() {
104                writeln!(f, "  {projection_target}")?;
105            }
106            writeln!(f)?;
107        }
108        if let Some(numeric_projection_targets) = self.numeric_projection_targets.as_ref() {
109            writeln!(f, "Projection subspaces (numeric):")?;
110            for projection_target in numeric_projection_targets.iter() {
111                writeln!(f, "  {projection_target}")?;
112            }
113            writeln!(f)?;
114        }
115        writeln!(
116            f,
117            "Use magnetic group for projection: {}",
118            match self.use_magnetic_group {
119                None => "no",
120                Some(MagneticSymmetryAnalysisKind::Representation) =>
121                    "yes, using unitary representations",
122                Some(MagneticSymmetryAnalysisKind::Corepresentation) =>
123                    "yes, using magnetic corepresentations",
124            }
125        )?;
126        writeln!(
127            f,
128            "Use double group for projection: {}",
129            nice_bool(self.use_double_group)
130        )?;
131        if let Some(finite_order) = self.infinite_order_to_finite {
132            writeln!(f, "Infinite order to finite: {finite_order}")?;
133        }
134        writeln!(
135            f,
136            "Symmetry transformation kind: {}",
137            self.symmetry_transformation_kind
138        )?;
139        writeln!(f)?;
140        writeln!(
141            f,
142            "Write character table: {}",
143            if let Some(chartab_display) = self.write_character_table.as_ref() {
144                format!("yes, {}", chartab_display.to_string().to_lowercase())
145            } else {
146                "no".to_string()
147            }
148        )?;
149
150        Ok(())
151    }
152}
153
154// ------
155// Result
156// ------
157
158/// Structure to contain electron density projection results.
159#[derive(Clone, Builder)]
160pub struct DensityProjectionResult<'a, G, T>
161where
162    G: SymmetryGroupProperties + Clone + 'a,
163    T: ComplexFloat + Lapack,
164    Density<'a, T>: SymmetryTransformable,
165    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
166{
167    /// The control parameters used to obtain this set of electron density projection results.
168    parameters: &'a DensityProjectionParams,
169
170    /// The densities being projected and their associated names or descriptions.
171    densities: Vec<(String, &'a Density<'a, T>)>,
172
173    /// The group used for the projection.
174    group: G,
175
176    /// The projected densities. Each tuple in the vector contains the name or description of the
177    /// density being projected and an indexmap containing the projected densities indexed by the
178    /// requested subspace labels.
179    #[allow(clippy::type_complexity)]
180    projected_densities: Vec<(
181        String,
182        IndexMap<G::RowSymbol, Result<Density<'a, T>, String>>,
183    )>,
184}
185
186impl<'a, G, T> DensityProjectionResult<'a, G, T>
187where
188    G: SymmetryGroupProperties + Clone,
189    G::RowSymbol: Serialize + DeserializeOwned,
190    T: ComplexFloat + Lapack,
191    Density<'a, T>: SymmetryTransformable,
192    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
193{
194    pub fn builder() -> DensityProjectionResultBuilder<'a, G, T> {
195        DensityProjectionResultBuilder::default()
196    }
197
198    /// Returns the projected densities.
199    #[allow(clippy::type_complexity)]
200    pub fn projected_densities(
201        &self,
202    ) -> &Vec<(
203        String,
204        IndexMap<G::RowSymbol, Result<Density<'a, T>, String>>,
205    )> {
206        &self.projected_densities
207    }
208}
209
210impl<'a, G, T> fmt::Display for DensityProjectionResult<'a, G, T>
211where
212    G: SymmetryGroupProperties + Clone,
213    G::RowSymbol: Serialize + DeserializeOwned,
214    T: ComplexFloat + Lapack,
215    Density<'a, T>: SymmetryTransformable,
216    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
217{
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        write_subtitle(f, "Orbit-based symmetry projection summary")?;
220        writeln!(f)?;
221        writeln!(
222            f,
223            "> Group: {} ({})",
224            self.group
225                .finite_subgroup_name()
226                .map(|subgroup_name| format!("{} > {}", self.group.name(), subgroup_name))
227                .unwrap_or(self.group.name()),
228            self.group.group_type().to_string().to_lowercase()
229        )?;
230        writeln!(f)?;
231
232        for (name, projected_densities) in self.projected_densities.iter() {
233            writeln!(f, ">> Density-matrix Frobenius norm for projected {name}")?;
234
235            let (rows, norms): (Vec<String>, Vec<String>) = projected_densities
236                .iter()
237                .map(|(row, den_res)| {
238                    let norm = den_res
239                        .as_ref()
240                        .map(|den| format!("{:.3e}", den.density_matrix().norm_l2()))
241                        .unwrap_or_else(|err| err.to_string());
242                    (row.to_string(), norm)
243                })
244                .unzip();
245
246            let row_length = rows
247                .iter()
248                .map(|row| row.chars().count())
249                .max()
250                .unwrap_or(8)
251                .max(8);
252            let norm_length = norms
253                .iter()
254                .map(|norm| norm.chars().count())
255                .max()
256                .unwrap_or(14)
257                .max(14);
258            let table_width = 4 + row_length + norm_length;
259            writeln!(f, "{}", "┈".repeat(table_width))?;
260            writeln!(f, " {:<row_length$}  Frobenius norm", "Subspace",)?;
261            writeln!(f, "{}", "┈".repeat(table_width))?;
262            for (row, norm) in rows.iter().zip(norms) {
263                writeln!(f, " {:<row_length$}  {:<}", row, norm)?;
264            }
265            writeln!(f, "{}", "┈".repeat(table_width))?;
266
267            writeln!(f)?;
268        }
269        Ok(())
270    }
271}
272
273impl<'a, G, T> fmt::Debug for DensityProjectionResult<'a, G, T>
274where
275    G: SymmetryGroupProperties + Clone,
276    G::RowSymbol: Serialize + DeserializeOwned,
277    T: ComplexFloat + Lapack,
278    Density<'a, T>: SymmetryTransformable,
279    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
280{
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        writeln!(f, "{self}")
283    }
284}
285
286// ------
287// Driver
288// ------
289
290// ~~~~~~~~~~~~~~~~~
291// Struct definition
292// ~~~~~~~~~~~~~~~~~
293//
294/// Driver structure for performing projection on electron densities.
295#[derive(Clone, Builder)]
296#[builder(build_fn(validate = "Self::validate"))]
297pub struct DensityProjectionDriver<'a, G, T>
298where
299    G: SymmetryGroupProperties + Clone,
300    G::RowSymbol: Serialize + DeserializeOwned,
301    T: ComplexFloat + Lapack,
302    Density<'a, T>: SymmetryTransformable,
303    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
304{
305    /// The control parameters used to obtain this set of electron density projection results.
306    parameters: &'a DensityProjectionParams,
307
308    /// The densities being projected and their associated names or descriptions.
309    densities: Vec<(String, &'a Density<'a, T>)>,
310
311    /// The result from symmetry-group detection on the underlying molecular structure of the
312    /// electron densities. Only the unitary symmetry group will be used for projection, since
313    /// magnetic-group projection is not yet formulated.
314    symmetry_group: &'a SymmetryGroupDetectionResult,
315
316    /// The result of the electron density projection.
317    #[builder(setter(skip), default = "None")]
318    result: Option<DensityProjectionResult<'a, G, T>>,
319}
320
321impl<'a, G, T> DensityProjectionDriverBuilder<'a, G, T>
322where
323    G: SymmetryGroupProperties + Clone,
324    G::RowSymbol: Serialize + DeserializeOwned,
325    T: ComplexFloat + Lapack,
326    Density<'a, T>: SymmetryTransformable,
327    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
328{
329    fn validate(&self) -> Result<(), String> {
330        let params = self
331            .parameters
332            .ok_or("No electron density projection parameters found.".to_string())?;
333
334        let sym_res = self
335            .symmetry_group
336            .ok_or("No symmetry group information found.".to_string())?;
337
338        let dens = self
339            .densities
340            .as_ref()
341            .ok_or("No electron densities found.".to_string())?;
342
343        let sym = if params.use_magnetic_group.is_some() {
344            sym_res
345                .magnetic_symmetry
346                .as_ref()
347                .ok_or("Magnetic symmetry requested for symmetry projection, but no magnetic symmetry found.")?
348        } else {
349            &sym_res.unitary_symmetry
350        };
351
352        if sym.is_infinite() && params.infinite_order_to_finite.is_none() {
353            Err(format!(
354                "Projection cannot be performed using the entirety of the infinite group `{}`. \
355                    Consider setting the parameter `infinite_order_to_finite` to restrict to a finite subgroup instead.",
356                sym.group_name
357                    .as_ref()
358                    .expect("No symmetry group name found.")
359            ))
360        } else {
361            let baos = dens
362                .iter()
363                .map(|(_, den)| den.bao())
364                .collect::<HashSet<_>>();
365            if baos.len() != 1 {
366                Err("Inconsistent basis angular order information between densities.".to_string())
367            } else {
368                Ok(())
369            }
370        }
371    }
372}
373
374// ~~~~~~~~~~~~~~~~~~~~~~
375// Struct implementations
376// ~~~~~~~~~~~~~~~~~~~~~~
377
378// Generic for all symmetry groups G and density numeric type T
379// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
380
381impl<'a, G, T> DensityProjectionDriver<'a, G, T>
382where
383    G: SymmetryGroupProperties + Clone,
384    G::RowSymbol: Serialize + DeserializeOwned,
385    T: ComplexFloat + Lapack,
386    Density<'a, T>: SymmetryTransformable,
387    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
388{
389    /// Returns a builder to construct a [`DensityProjectionDriver`] structure.
390    pub fn builder() -> DensityProjectionDriverBuilder<'a, G, T> {
391        DensityProjectionDriverBuilder::default()
392    }
393}
394
395// Specific for unitary-represented symmetry groups, but generic for density numeric type T
396// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
397
398impl<'a, T> DensityProjectionDriver<'a, UnitaryRepresentedSymmetryGroup, T>
399where
400    T: ComplexFloat + Lapack,
401    Density<'a, T>: SymmetryTransformable,
402    DensitySymmetryOrbit<'a, UnitaryRepresentedSymmetryGroup, T>:
403        Projectable<UnitaryRepresentedSymmetryGroup, Density<'a, T>>,
404{
405    fn_construct_unitary_group!(
406        /// Constructs the unitary-represented group (which itself can be unitary or magnetic) ready
407        /// for electron density projection.
408        construct_unitary_group
409    );
410}
411
412// Specific for unitary-represented symmetry groups and density numeric types f64 and C128
413// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
414
415#[duplicate_item(
416    duplicate!{
417        [ dtype_nested; [f64]; [Complex<f64>] ]
418        [
419            gtype_ [ UnitaryRepresentedSymmetryGroup ]
420            dtype_ [ dtype_nested ]
421            doc_sub_ [ "Performs projection using a unitary-represented group and stores the result." ]
422            projection_fn_ [ projection_representation ]
423            construct_group_ [ self.construct_unitary_group()? ]
424        ]
425    }
426)]
427impl<'a> DensityProjectionDriver<'a, gtype_, dtype_> {
428    #[doc = doc_sub_]
429    fn projection_fn_(&mut self) -> Result<(), anyhow::Error> {
430        let params = self.parameters;
431        let group = construct_group_;
432        log_cc_transversal(&group);
433        let bao = self
434            .densities
435            .first()
436            .map(|(_, den)| den.bao())
437            .ok_or_else(|| {
438                format_err!("Basis angular order information could not be extracted.")
439            })?;
440        log_bao(bao, None);
441
442        let all_rows = group.character_table().get_all_rows();
443        let rows = params
444            .symbolic_projection_targets
445            .as_ref()
446            .unwrap_or(&vec![])
447            .iter()
448            .map(|row_str| MullikenIrrepSymbol::from_str(row_str).map_err(|err| format_err!(err)))
449            .chain(
450                params
451                    .numeric_projection_targets
452                    .as_ref()
453                    .unwrap_or(&vec![])
454                    .iter()
455                    .map(|row_index| {
456                        all_rows.get_index(*row_index).cloned().ok_or_else(|| {
457                            format_err!(
458                                "Unable to retrieve the subspace label with index {row_index}."
459                            )
460                        })
461                    }),
462            )
463            .collect::<Result<Vec<_>, _>>()?;
464
465        let projected_densities = self
466            .densities
467            .iter()
468            .map(|(name, den)| {
469                let projections = DensitySymmetryOrbit::builder()
470                    .group(&group)
471                    .origin(den)
472                    .symmetry_transformation_kind(params.symmetry_transformation_kind.clone())
473                    .integrality_threshold(1e-14)
474                    .linear_independence_threshold(1e-14)
475                    .eigenvalue_comparison_mode(EigenvalueComparisonMode::Modulus)
476                    .build()
477                    .map_err(|err| format_err!(err))
478                    .map(|den_orbit| {
479                        rows.iter()
480                            .map(|row| {
481                                (
482                                    row.clone(),
483                                    den_orbit
484                                        .project_onto(row)
485                                        .map_err(|err| err.to_string())
486                                        .and_then(|temp_projected_den| {
487                                            Density::builder()
488                                                .bao(bao)
489                                                .complex_symmetric(den.complex_symmetric())
490                                                .complex_conjugated(den.complex_conjugated())
491                                                .mol(den.mol())
492                                                .density_matrix(
493                                                    temp_projected_den.density_matrix().clone(),
494                                                )
495                                                .threshold(den.threshold())
496                                                .build()
497                                                .map_err(|err| err.to_string())
498                                        }),
499                                )
500                            })
501                            .collect::<IndexMap<_, _>>()
502                    })?;
503                Ok((name.clone(), projections))
504            })
505            .collect::<Result<Vec<_>, anyhow::Error>>()?;
506
507        let result = DensityProjectionResult::builder()
508            .parameters(params)
509            .densities(self.densities.clone())
510            .group(group.clone())
511            .projected_densities(projected_densities)
512            .build()?;
513        self.result = Some(result);
514
515        Ok(())
516    }
517}
518
519// ~~~~~~~~~~~~~~~~~~~~~
520// Trait implementations
521// ~~~~~~~~~~~~~~~~~~~~~
522
523// Generic for all symmetry groups G and density numeric type T
524// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
525
526impl<'a, G, T> fmt::Display for DensityProjectionDriver<'a, G, T>
527where
528    G: SymmetryGroupProperties + Clone,
529    G::RowSymbol: Serialize + DeserializeOwned,
530    T: ComplexFloat + Lapack,
531    Density<'a, T>: SymmetryTransformable,
532    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
533{
534    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
535        write_title(f, "Electron Density Symmetry Projection")?;
536        writeln!(f)?;
537        writeln!(f, "{}", self.parameters)?;
538        Ok(())
539    }
540}
541
542impl<'a, G, T> fmt::Debug for DensityProjectionDriver<'a, G, T>
543where
544    G: SymmetryGroupProperties + Clone,
545    G::RowSymbol: Serialize + DeserializeOwned,
546    T: ComplexFloat + Lapack,
547    Density<'a, T>: SymmetryTransformable,
548    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
549{
550    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
551        writeln!(f, "{self}")
552    }
553}
554
555// Specific for unitary-represented symmetry groups and density numeric types f64 and C128
556// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
557
558#[duplicate_item(
559    duplicate!{
560        [ dtype_nested; [f64]; [Complex<f64>] ]
561        [
562            gtype_ [ UnitaryRepresentedSymmetryGroup ]
563            dtype_ [ dtype_nested ]
564            doc_sub_ [ "Performs projection using a unitary-represented group and stores the result." ]
565            projection_fn_ [ projection_representation ]
566            construct_group_ [ self.construct_unitary_group()? ]
567        ]
568    }
569)]
570impl<'a> QSym2Driver for DensityProjectionDriver<'a, gtype_, dtype_> {
571    type Params = DensityProjectionParams;
572
573    type Outcome = DensityProjectionResult<'a, gtype_, dtype_>;
574
575    fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
576        self.result
577            .as_ref()
578            .ok_or_else(|| format_err!("No electron density projection results found."))
579    }
580
581    fn run(&mut self) -> Result<(), anyhow::Error> {
582        self.log_output_display();
583        self.projection_fn_()?;
584        self.result()?.log_output_display();
585        Ok(())
586    }
587}