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 analysis: {}",
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    projected_densities: Vec<(
180        String,
181        IndexMap<G::RowSymbol, Result<Density<'a, T>, String>>,
182    )>,
183}
184
185impl<'a, G, T> DensityProjectionResult<'a, G, T>
186where
187    G: SymmetryGroupProperties + Clone,
188    G::RowSymbol: Serialize + DeserializeOwned,
189    T: ComplexFloat + Lapack,
190    Density<'a, T>: SymmetryTransformable,
191    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
192{
193    pub fn builder() -> DensityProjectionResultBuilder<'a, G, T> {
194        DensityProjectionResultBuilder::default()
195    }
196
197    /// Returns the projected densities.
198    pub fn projected_densities(
199        &self,
200    ) -> &Vec<(
201        String,
202        IndexMap<G::RowSymbol, Result<Density<'a, T>, String>>,
203    )> {
204        &self.projected_densities
205    }
206}
207
208impl<'a, G, T> fmt::Display for DensityProjectionResult<'a, G, T>
209where
210    G: SymmetryGroupProperties + Clone,
211    G::RowSymbol: Serialize + DeserializeOwned,
212    T: ComplexFloat + Lapack,
213    Density<'a, T>: SymmetryTransformable,
214    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
215{
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        write_subtitle(f, "Orbit-based symmetry projection summary")?;
218        writeln!(f)?;
219        writeln!(
220            f,
221            "> Group: {} ({})",
222            self.group
223                .finite_subgroup_name()
224                .map(|subgroup_name| format!("{} > {}", self.group.name(), subgroup_name))
225                .unwrap_or(self.group.name()),
226            self.group.group_type().to_string().to_lowercase()
227        )?;
228        writeln!(f)?;
229
230        for (name, projected_densities) in self.projected_densities.iter() {
231            writeln!(f, ">> Density-matrix Frobenius norm for projected {name}")?;
232
233            let (rows, norms): (Vec<String>, Vec<String>) = projected_densities
234                .iter()
235                .map(|(row, den_res)| {
236                    let norm = den_res
237                        .as_ref()
238                        .map(|den| format!("{:.3e}", den.density_matrix().norm_l2()))
239                        .unwrap_or_else(|err| err.to_string());
240                    (row.to_string(), norm)
241                })
242                .unzip();
243
244            let row_length = rows
245                .iter()
246                .map(|row| row.chars().count())
247                .max()
248                .unwrap_or(8)
249                .max(8);
250            let norm_length = norms
251                .iter()
252                .map(|norm| norm.chars().count())
253                .max()
254                .unwrap_or(14)
255                .max(14);
256            let table_width = 4 + row_length + norm_length;
257            writeln!(f, "{}", "┈".repeat(table_width))?;
258            writeln!(f, " {:<row_length$}  Frobenius norm", "Subspace",)?;
259            writeln!(f, "{}", "┈".repeat(table_width))?;
260            for (row, norm) in rows.iter().zip(norms) {
261                writeln!(f, " {:<row_length$}  {:<}", row, norm)?;
262            }
263            writeln!(f, "{}", "┈".repeat(table_width))?;
264
265            writeln!(f)?;
266        }
267        Ok(())
268    }
269}
270
271impl<'a, G, T> fmt::Debug for DensityProjectionResult<'a, G, T>
272where
273    G: SymmetryGroupProperties + Clone,
274    G::RowSymbol: Serialize + DeserializeOwned,
275    T: ComplexFloat + Lapack,
276    Density<'a, T>: SymmetryTransformable,
277    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
278{
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        writeln!(f, "{self}")
281    }
282}
283
284// ------
285// Driver
286// ------
287
288// ~~~~~~~~~~~~~~~~~
289// Struct definition
290// ~~~~~~~~~~~~~~~~~
291//
292/// Driver structure for performing projection on electron densities.
293#[derive(Clone, Builder)]
294#[builder(build_fn(validate = "Self::validate"))]
295pub struct DensityProjectionDriver<'a, G, T>
296where
297    G: SymmetryGroupProperties + Clone,
298    G::RowSymbol: Serialize + DeserializeOwned,
299    T: ComplexFloat + Lapack,
300    Density<'a, T>: SymmetryTransformable,
301    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
302{
303    /// The control parameters used to obtain this set of electron density projection results.
304    parameters: &'a DensityProjectionParams,
305
306    /// The densities being projected and their associated names or descriptions.
307    densities: Vec<(String, &'a Density<'a, T>)>,
308
309    /// The result from symmetry-group detection on the underlying molecular structure of the
310    /// electron densities. Only the unitary symmetry group will be used for projection, since
311    /// magnetic-group projection is not yet formulated.
312    symmetry_group: &'a SymmetryGroupDetectionResult,
313
314    /// The result of the electron density projection.
315    #[builder(setter(skip), default = "None")]
316    result: Option<DensityProjectionResult<'a, G, T>>,
317}
318
319impl<'a, G, T> DensityProjectionDriverBuilder<'a, G, T>
320where
321    G: SymmetryGroupProperties + Clone,
322    G::RowSymbol: Serialize + DeserializeOwned,
323    T: ComplexFloat + Lapack,
324    Density<'a, T>: SymmetryTransformable,
325    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
326{
327    fn validate(&self) -> Result<(), String> {
328        let params = self
329            .parameters
330            .ok_or("No electron density projection parameters found.".to_string())?;
331
332        let sym_res = self
333            .symmetry_group
334            .ok_or("No symmetry group information found.".to_string())?;
335
336        let dens = self
337            .densities
338            .as_ref()
339            .ok_or("No electron densities found.".to_string())?;
340
341        let sym = if params.use_magnetic_group.is_some() {
342            sym_res
343                .magnetic_symmetry
344                .as_ref()
345                .ok_or("Magnetic symmetry requested for representation analysis, but no magnetic symmetry found.")?
346        } else {
347            &sym_res.unitary_symmetry
348        };
349
350        if sym.is_infinite() && params.infinite_order_to_finite.is_none() {
351            Err(format!(
352                "Projection cannot be performed using the entirety of the infinite group `{}`. \
353                    Consider setting the parameter `infinite_order_to_finite` to restrict to a finite subgroup instead.",
354                sym.group_name
355                    .as_ref()
356                    .expect("No symmetry group name found.")
357            ))
358        } else {
359            let baos = dens
360                .iter()
361                .map(|(_, den)| den.bao())
362                .collect::<HashSet<_>>();
363            if baos.len() != 1 {
364                Err("Inconsistent basis angular order information between densities.".to_string())
365            } else {
366                Ok(())
367            }
368        }
369    }
370}
371
372// ~~~~~~~~~~~~~~~~~~~~~~
373// Struct implementations
374// ~~~~~~~~~~~~~~~~~~~~~~
375
376// Generic for all symmetry groups G and density numeric type T
377// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
378
379impl<'a, G, T> DensityProjectionDriver<'a, G, T>
380where
381    G: SymmetryGroupProperties + Clone,
382    G::RowSymbol: Serialize + DeserializeOwned,
383    T: ComplexFloat + Lapack,
384    Density<'a, T>: SymmetryTransformable,
385    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
386{
387    /// Returns a builder to construct a [`DensityProjectionDriver`] structure.
388    pub fn builder() -> DensityProjectionDriverBuilder<'a, G, T> {
389        DensityProjectionDriverBuilder::default()
390    }
391}
392
393// Specific for unitary-represented symmetry groups, but generic for density numeric type T
394// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
395
396impl<'a, T> DensityProjectionDriver<'a, UnitaryRepresentedSymmetryGroup, T>
397where
398    T: ComplexFloat + Lapack,
399    Density<'a, T>: SymmetryTransformable,
400    DensitySymmetryOrbit<'a, UnitaryRepresentedSymmetryGroup, T>:
401        Projectable<UnitaryRepresentedSymmetryGroup, Density<'a, T>>,
402{
403    fn_construct_unitary_group!(
404        /// Constructs the unitary-represented group (which itself can be unitary or magnetic) ready
405        /// for electron density projection.
406        construct_unitary_group
407    );
408}
409
410// Specific for unitary-represented symmetry groups and density numeric types f64 and C128
411// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
412
413#[duplicate_item(
414    duplicate!{
415        [ dtype_nested; [f64]; [Complex<f64>] ]
416        [
417            gtype_ [ UnitaryRepresentedSymmetryGroup ]
418            dtype_ [ dtype_nested ]
419            doc_sub_ [ "Performs projection using a unitary-represented group and stores the result." ]
420            projection_fn_ [ projection_representation ]
421            construct_group_ [ self.construct_unitary_group()? ]
422        ]
423    }
424)]
425impl<'a> DensityProjectionDriver<'a, gtype_, dtype_> {
426    #[doc = doc_sub_]
427    fn projection_fn_(&mut self) -> Result<(), anyhow::Error> {
428        let params = self.parameters;
429        let group = construct_group_;
430        log_cc_transversal(&group);
431        let bao = self
432            .densities
433            .iter()
434            .next()
435            .map(|(_, den)| den.bao())
436            .ok_or_else(|| {
437                format_err!("Basis angular order information could not be extracted.")
438            })?;
439        log_bao(bao, None);
440
441        let all_rows = group.character_table().get_all_rows();
442        let rows = params
443            .symbolic_projection_targets
444            .as_ref()
445            .unwrap_or(&vec![])
446            .iter()
447            .map(|row_str| MullikenIrrepSymbol::from_str(row_str).map_err(|err| format_err!(err)))
448            .chain(
449                params
450                    .numeric_projection_targets
451                    .as_ref()
452                    .unwrap_or(&vec![])
453                    .iter()
454                    .map(|row_index| {
455                        all_rows.get_index(*row_index).cloned().ok_or_else(|| {
456                            format_err!(
457                                "Unable to retrieve the subspace label with index {row_index}."
458                            )
459                        })
460                    }),
461            )
462            .collect::<Result<Vec<_>, _>>()?;
463
464        let projected_densities = self
465            .densities
466            .iter()
467            .map(|(name, den)| {
468                let projections = DensitySymmetryOrbit::builder()
469                    .group(&group)
470                    .origin(den)
471                    .symmetry_transformation_kind(params.symmetry_transformation_kind.clone())
472                    .integrality_threshold(1e-14)
473                    .linear_independence_threshold(1e-14)
474                    .eigenvalue_comparison_mode(EigenvalueComparisonMode::Modulus)
475                    .build()
476                    .map_err(|err| format_err!(err))
477                    .map(|den_orbit| {
478                        rows.iter()
479                            .map(|row| {
480                                (
481                                    row.clone(),
482                                    den_orbit
483                                        .project_onto(row)
484                                        .map_err(|err| err.to_string())
485                                        .and_then(|temp_projected_den| {
486                                            Density::builder()
487                                                .bao(bao)
488                                                .complex_symmetric(den.complex_symmetric())
489                                                .complex_conjugated(den.complex_conjugated())
490                                                .mol(den.mol())
491                                                .density_matrix(
492                                                    temp_projected_den.density_matrix().clone(),
493                                                )
494                                                .threshold(den.threshold())
495                                                .build()
496                                                .map_err(|err| err.to_string())
497                                        }),
498                                )
499                            })
500                            .collect::<IndexMap<_, _>>()
501                    })?;
502                Ok((name.clone(), projections))
503            })
504            .collect::<Result<Vec<_>, anyhow::Error>>()?;
505
506        let result = DensityProjectionResult::builder()
507            .parameters(params)
508            .densities(self.densities.clone())
509            .group(group.clone())
510            .projected_densities(projected_densities)
511            .build()?;
512        self.result = Some(result);
513
514        Ok(())
515    }
516}
517
518// ~~~~~~~~~~~~~~~~~~~~~
519// Trait implementations
520// ~~~~~~~~~~~~~~~~~~~~~
521
522// Generic for all symmetry groups G and density numeric type T
523// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
524
525impl<'a, G, T> fmt::Display for DensityProjectionDriver<'a, G, T>
526where
527    G: SymmetryGroupProperties + Clone,
528    G::RowSymbol: Serialize + DeserializeOwned,
529    T: ComplexFloat + Lapack,
530    Density<'a, T>: SymmetryTransformable,
531    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
532{
533    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
534        write_title(f, "Electron Density Symmetry Projection")?;
535        writeln!(f)?;
536        writeln!(f, "{}", self.parameters)?;
537        Ok(())
538    }
539}
540
541impl<'a, G, T> fmt::Debug for DensityProjectionDriver<'a, G, T>
542where
543    G: SymmetryGroupProperties + Clone,
544    G::RowSymbol: Serialize + DeserializeOwned,
545    T: ComplexFloat + Lapack,
546    Density<'a, T>: SymmetryTransformable,
547    DensitySymmetryOrbit<'a, G, T>: Projectable<G, Density<'a, T>>,
548{
549    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
550        writeln!(f, "{self}")
551    }
552}
553
554// Specific for unitary-represented symmetry groups and density numeric types f64 and C128
555// '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
556
557#[duplicate_item(
558    duplicate!{
559        [ dtype_nested; [f64]; [Complex<f64>] ]
560        [
561            gtype_ [ UnitaryRepresentedSymmetryGroup ]
562            dtype_ [ dtype_nested ]
563            doc_sub_ [ "Performs projection using a unitary-represented group and stores the result." ]
564            projection_fn_ [ projection_representation ]
565            construct_group_ [ self.construct_unitary_group()? ]
566        ]
567    }
568)]
569impl<'a> QSym2Driver for DensityProjectionDriver<'a, gtype_, dtype_> {
570    type Params = DensityProjectionParams;
571
572    type Outcome = DensityProjectionResult<'a, gtype_, dtype_>;
573
574    fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
575        self.result
576            .as_ref()
577            .ok_or_else(|| format_err!("No electron density projection results found."))
578    }
579
580    fn run(&mut self) -> Result<(), anyhow::Error> {
581        self.log_output_display();
582        self.projection_fn_()?;
583        self.result()?.log_output_display();
584        Ok(())
585    }
586}