1use 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
40const fn default_symbolic() -> Option<CharacterTableDisplay> {
45 Some(CharacterTableDisplay::Symbolic)
46}
47
48#[derive(Clone, Builder, Debug, Serialize, Deserialize)]
50pub struct DensityProjectionParams {
51 #[builder(default = "None")]
54 #[serde(default)]
55 pub use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
56
57 #[builder(default = "false")]
59 #[serde(default)]
60 pub use_double_group: bool,
61
62 #[builder(default = "Some(CharacterTableDisplay::Symbolic)")]
65 #[serde(default = "default_symbolic")]
66 pub write_character_table: Option<CharacterTableDisplay>,
67
68 #[builder(default = "SymmetryTransformationKind::Spatial")]
71 #[serde(default)]
72 pub symmetry_transformation_kind: SymmetryTransformationKind,
73
74 #[builder(default = "None")]
77 #[serde(default)]
78 pub infinite_order_to_finite: Option<u32>,
79
80 #[builder(default = "None")]
82 #[serde(default)]
83 pub symbolic_projection_targets: Option<Vec<String>>,
84
85 #[builder(default = "None")]
88 #[serde(default)]
89 pub numeric_projection_targets: Option<Vec<usize>>,
90}
91
92impl DensityProjectionParams {
93 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#[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 parameters: &'a DensityProjectionParams,
169
170 densities: Vec<(String, &'a Density<'a, T>)>,
172
173 group: G,
175
176 #[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 #[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#[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 parameters: &'a DensityProjectionParams,
307
308 densities: Vec<(String, &'a Density<'a, T>)>,
310
311 symmetry_group: &'a SymmetryGroupDetectionResult,
315
316 #[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
374impl<'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 pub fn builder() -> DensityProjectionDriverBuilder<'a, G, T> {
391 DensityProjectionDriverBuilder::default()
392 }
393}
394
395impl<'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 construct_unitary_group
409 );
410}
411
412#[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
519impl<'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#[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}