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