1use std::collections::HashSet;
4use std::fmt;
5use std::ops::Mul;
6
7use anyhow::{self, bail, format_err};
8use derive_builder::Builder;
9use duplicate::duplicate_item;
10use ndarray::Array4;
11use ndarray_linalg::types::Lapack;
12use num_complex::{Complex, ComplexFloat};
13use num_traits::Float;
14use serde::{Deserialize, Serialize};
15
16use crate::analysis::{EigenvalueComparisonMode, RepAnalysis};
17use crate::chartab::chartab_group::CharacterProperties;
18use crate::chartab::SubspaceDecomposable;
19use crate::drivers::representation_analysis::angular_function::{
20 find_angular_function_representation, find_spinor_function_representation,
21 AngularFunctionRepAnalysisParams,
22};
23use crate::drivers::representation_analysis::{
24 fn_construct_magnetic_group, fn_construct_unitary_group, log_bao, log_cc_transversal,
25 CharacterTableDisplay, MagneticSymmetryAnalysisKind,
26};
27use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
28use crate::drivers::QSym2Driver;
29use crate::group::{GroupProperties, MagneticRepresentedGroup, UnitaryRepresentedGroup};
30use crate::io::format::{
31 log_subtitle, nice_bool, qsym2_output, write_subtitle, write_title, QSym2Output,
32};
33use crate::symmetry::symmetry_group::{
34 MagneticRepresentedSymmetryGroup, SymmetryGroupProperties, UnitaryRepresentedSymmetryGroup,
35};
36use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
37use crate::target::density::density_analysis::DensitySymmetryOrbit;
38use crate::target::density::Density;
39
40#[cfg(test)]
41#[path = "density_tests.rs"]
42mod density_tests;
43
44const fn default_true() -> bool {
53 true
54}
55const fn default_symbolic() -> Option<CharacterTableDisplay> {
56 Some(CharacterTableDisplay::Symbolic)
57}
58
59#[derive(Clone, Builder, Debug, Serialize, Deserialize)]
61pub struct DensityRepAnalysisParams<T: From<f64>> {
62 pub integrality_threshold: T,
64
65 pub linear_independence_threshold: T,
67
68 #[builder(default = "None")]
71 #[serde(default)]
72 pub use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
73
74 #[builder(default = "false")]
76 #[serde(default)]
77 pub use_double_group: bool,
78
79 #[builder(default = "true")]
82 #[serde(default = "default_true")]
83 pub use_cayley_table: bool,
84
85 #[builder(default = "SymmetryTransformationKind::Spatial")]
88 #[serde(default)]
89 pub symmetry_transformation_kind: SymmetryTransformationKind,
90
91 #[builder(default = "Some(CharacterTableDisplay::Symbolic)")]
94 #[serde(default = "default_symbolic")]
95 pub write_character_table: Option<CharacterTableDisplay>,
96
97 #[builder(default = "EigenvalueComparisonMode::Modulus")]
99 #[serde(default)]
100 pub eigenvalue_comparison_mode: EigenvalueComparisonMode,
101
102 #[builder(default = "None")]
105 #[serde(default)]
106 pub infinite_order_to_finite: Option<u32>,
107}
108
109impl<T> DensityRepAnalysisParams<T>
110where
111 T: Float + From<f64>,
112{
113 pub fn builder() -> DensityRepAnalysisParamsBuilder<T> {
115 DensityRepAnalysisParamsBuilder::default()
116 }
117}
118
119impl Default for DensityRepAnalysisParams<f64> {
120 fn default() -> Self {
121 Self::builder()
122 .integrality_threshold(1e-7)
123 .linear_independence_threshold(1e-7)
124 .build()
125 .expect("Unable to construct a default `DensityRepAnalysisParams<f64>`.")
126 }
127}
128
129impl<T> fmt::Display for DensityRepAnalysisParams<T>
130where
131 T: From<f64> + fmt::LowerExp + fmt::Debug,
132{
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 writeln!(
135 f,
136 "Integrality threshold: {:.3e}",
137 self.integrality_threshold
138 )?;
139 writeln!(
140 f,
141 "Linear independence threshold: {:.3e}",
142 self.linear_independence_threshold
143 )?;
144 writeln!(
145 f,
146 "Orbit eigenvalue comparison mode: {}",
147 self.eigenvalue_comparison_mode
148 )?;
149 writeln!(f)?;
150 writeln!(
151 f,
152 "Use magnetic group for analysis: {}",
153 match self.use_magnetic_group {
154 None => "no",
155 Some(MagneticSymmetryAnalysisKind::Representation) =>
156 "yes, using unitary representations",
157 Some(MagneticSymmetryAnalysisKind::Corepresentation) =>
158 "yes, using magnetic corepresentations",
159 }
160 )?;
161 writeln!(
162 f,
163 "Use double group for analysis: {}",
164 nice_bool(self.use_double_group)
165 )?;
166 writeln!(
167 f,
168 "Use Cayley table for orbit overlap matrices: {}",
169 nice_bool(self.use_cayley_table)
170 )?;
171 if let Some(finite_order) = self.infinite_order_to_finite {
172 writeln!(f, "Infinite order to finite: {finite_order}")?;
173 }
174 writeln!(
175 f,
176 "Symmetry transformation kind: {}",
177 self.symmetry_transformation_kind
178 )?;
179 writeln!(f)?;
180 writeln!(
181 f,
182 "Write character table: {}",
183 if let Some(chartab_display) = self.write_character_table.as_ref() {
184 format!("yes, {}", chartab_display.to_string().to_lowercase())
185 } else {
186 "no".to_string()
187 }
188 )?;
189
190 Ok(())
191 }
192}
193
194#[derive(Clone, Builder)]
200pub struct DensityRepAnalysisResult<'a, G, T>
201where
202 G: SymmetryGroupProperties + Clone,
203 G::CharTab: SubspaceDecomposable<T>,
204 T: ComplexFloat + Lapack,
205 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
206{
207 parameters: &'a DensityRepAnalysisParams<<T as ComplexFloat>::Real>,
210
211 densities: Vec<(String, &'a Density<'a, T>)>,
213
214 group: G,
216
217 density_symmetries: Vec<Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>>,
219
220 density_symmetries_thresholds: Vec<(Option<T>, Option<T>)>,
223}
224
225impl<'a, G, T> DensityRepAnalysisResult<'a, G, T>
226where
227 G: SymmetryGroupProperties + Clone,
228 G::CharTab: SubspaceDecomposable<T>,
229 T: ComplexFloat + Lapack,
230 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
231{
232 fn builder() -> DensityRepAnalysisResultBuilder<'a, G, T> {
235 DensityRepAnalysisResultBuilder::default()
236 }
237}
238
239impl<'a, G, T> fmt::Display for DensityRepAnalysisResult<'a, G, T>
240where
241 G: SymmetryGroupProperties + Clone,
242 G::CharTab: SubspaceDecomposable<T>,
243 T: ComplexFloat + Lapack,
244 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
245{
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 write_subtitle(f, "Orbit-based symmetry analysis results")?;
248 writeln!(f)?;
249 writeln!(
250 f,
251 "> Group: {} ({})",
252 self.group
253 .finite_subgroup_name()
254 .map(|subgroup_name| format!("{} > {}", self.group.name(), subgroup_name))
255 .unwrap_or(self.group.name()),
256 self.group.group_type().to_string().to_lowercase()
257 )?;
258 writeln!(f)?;
259
260 let density_index_length = usize::try_from(self.densities.len().ilog10() + 1)
261 .unwrap_or(1)
262 .max(1);
263 let density_text_length = self
264 .densities
265 .iter()
266 .map(|(desc, _)| desc.chars().count())
267 .max()
268 .unwrap_or(7)
269 .max(7);
270 let symmetry_length = self
271 .density_symmetries
272 .iter()
273 .map(|den_sym_res| {
274 den_sym_res
275 .as_ref()
276 .map(|sym| sym.to_string())
277 .unwrap_or_else(|err| err.clone())
278 .chars()
279 .count()
280 })
281 .max()
282 .unwrap_or(8)
283 .max(8);
284 let eig_above_length: usize = self
285 .density_symmetries_thresholds
286 .iter()
287 .map(|(eig_above_opt, _)| {
288 eig_above_opt
289 .as_ref()
290 .map(|eig_above| format!("{eig_above:+.3e}").chars().count())
291 .unwrap_or(10)
292 })
293 .max()
294 .unwrap_or(10)
295 .max(10);
296 let eig_below_length: usize = self
297 .density_symmetries_thresholds
298 .iter()
299 .map(|(_, eig_below_opt)| {
300 eig_below_opt
301 .as_ref()
302 .map(|eig_below| format!("{eig_below:+.3e}").chars().count())
303 .unwrap_or(10)
304 })
305 .max()
306 .unwrap_or(10)
307 .max(10);
308
309 let table_width = 10
310 + density_index_length
311 + density_text_length
312 + symmetry_length
313 + eig_above_length
314 + eig_below_length;
315
316 writeln!(f, "> Density results")?;
317 writeln!(f, "{}", "┈".repeat(table_width))?;
318 writeln!(
319 f,
320 " {:>density_index_length$} {:<density_text_length$} {:<symmetry_length$} {:<eig_above_length$} Eig. below",
321 "#",
322 "Density",
323 "Symmetry",
324 "Eig. above",
325 )?;
326 writeln!(f, "{}", "┈".repeat(table_width))?;
327 for (deni, (((den, _), den_sym), (eig_above_opt, eig_below_opt))) in self
328 .densities
329 .iter()
330 .zip(self.density_symmetries.iter())
331 .zip(self.density_symmetries_thresholds.iter())
332 .enumerate()
333 {
334 writeln!(
335 f,
336 " {:>density_index_length$} {:<density_text_length$} {:<symmetry_length$} {:<eig_above_length$} {}",
337 deni,
338 den,
339 den_sym.as_ref().map(|sym| sym.to_string()).unwrap_or_else(|err| err.clone()),
340 eig_above_opt
341 .map(|eig_above| format!("{eig_above:>+.3e}"))
342 .unwrap_or("--".to_string()),
343 eig_below_opt
344 .map(|eig_below| format!("{eig_below:>+.3e}"))
345 .unwrap_or("--".to_string()),
346 )?;
347 }
348 writeln!(f, "{}", "┈".repeat(table_width))?;
349 writeln!(f)?;
350
351 Ok(())
352 }
353}
354
355impl<'a, G, T> fmt::Debug for DensityRepAnalysisResult<'a, G, T>
356where
357 G: SymmetryGroupProperties + Clone,
358 G::CharTab: SubspaceDecomposable<T>,
359 T: ComplexFloat + Lapack,
360 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
361{
362 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363 writeln!(f, "{self}")
364 }
365}
366
367impl<'a, G, T> DensityRepAnalysisResult<'a, G, T>
368where
369 G: SymmetryGroupProperties + Clone,
370 G::CharTab: SubspaceDecomposable<T>,
371 T: ComplexFloat + Lapack,
372 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
373{
374 pub fn density_symmetries(
376 &self,
377 ) -> &Vec<Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>> {
378 &self.density_symmetries
379 }
380}
381
382#[derive(Clone, Builder)]
392#[builder(build_fn(validate = "Self::validate"))]
393pub struct DensityRepAnalysisDriver<'a, G, T>
394where
395 G: SymmetryGroupProperties + Clone,
396 G::CharTab: SubspaceDecomposable<T>,
397 T: ComplexFloat + Lapack,
398 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
399{
400 parameters: &'a DensityRepAnalysisParams<<T as ComplexFloat>::Real>,
402
403 densities: Vec<(String, &'a Density<'a, T>)>,
405
406 symmetry_group: &'a SymmetryGroupDetectionResult,
409
410 sao_spatial_4c: &'a Array4<T>,
413
414 #[builder(default = "None")]
419 sao_spatial_4c_h: Option<&'a Array4<T>>,
420
421 angular_function_parameters: &'a AngularFunctionRepAnalysisParams,
423
424 #[builder(setter(skip), default = "None")]
426 result: Option<DensityRepAnalysisResult<'a, G, T>>,
427}
428
429impl<'a, G, T> DensityRepAnalysisDriverBuilder<'a, G, T>
430where
431 G: SymmetryGroupProperties + Clone,
432 G::CharTab: SubspaceDecomposable<T>,
433 T: ComplexFloat + Lapack,
434 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
435{
436 fn validate(&self) -> Result<(), String> {
437 let params = self
438 .parameters
439 .ok_or("No electron density representation analysis parameters found.".to_string())?;
440
441 let sym_res = self
442 .symmetry_group
443 .ok_or("No symmetry group information found.".to_string())?;
444
445 let sao_spatial_4c = self
446 .sao_spatial_4c
447 .ok_or("No four-centre spatial SAO matrix found.".to_string())?;
448
449 if let Some(sao_spatial_4c_h) = self.sao_spatial_4c_h.flatten() {
450 if sao_spatial_4c_h.shape() != sao_spatial_4c.shape() {
451 return Err(
452 "Mismatched shapes between `sao_spatial_4c` and `sao_spatial_4c_h`."
453 .to_string(),
454 );
455 }
456 }
457
458 let dens = self
459 .densities
460 .as_ref()
461 .ok_or("No electron densities found.".to_string())?;
462
463 let sym = if params.use_magnetic_group.is_some() {
464 sym_res
465 .magnetic_symmetry
466 .as_ref()
467 .ok_or("Magnetic symmetry requested for representation analysis, but no magnetic symmetry found.")?
468 } else {
469 &sym_res.unitary_symmetry
470 };
471
472 if sym.is_infinite() && params.infinite_order_to_finite.is_none() {
473 Err(
474 format!(
475 "Representation analysis cannot be performed using the entirety of the infinite group `{}`. \
476 Consider setting the parameter `infinite_order_to_finite` to restrict to a finite subgroup instead.",
477 sym.group_name.as_ref().expect("No symmetry group name found.")
478 )
479 )
480 } else {
481 let baos = dens
482 .iter()
483 .map(|(_, den)| den.bao())
484 .collect::<HashSet<_>>();
485 if baos.len() != 1 {
486 Err("Inconsistent basis angular order information between densities.".to_string())
487 } else {
488 let naos = sao_spatial_4c.shape().iter().collect::<HashSet<_>>();
489 if naos.len() != 1 {
490 Err("The shape of the four-centre spatial SAO tensor is invalid: all four dimensions must have the same length.".to_string())
491 } else {
492 let nao = **naos.iter().next().ok_or(
493 "Unable to extract the dimensions of the four-centre spatial SAO tensor.",
494 )?;
495 if !dens.iter().all(|(_, den)| den.bao().n_funcs() == nao) {
496 Err("The dimensions of the four-centre spatial SAO tensor do not match the number of spatial AO basis functions.".to_string())
497 } else {
498 Ok(())
499 }
500 }
501 }
502 }
503 }
504}
505
506impl<'a, G, T> DensityRepAnalysisDriver<'a, G, T>
514where
515 G: SymmetryGroupProperties + Clone,
516 G::CharTab: SubspaceDecomposable<T>,
517 T: ComplexFloat + Lapack,
518 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
519{
520 pub fn builder() -> DensityRepAnalysisDriverBuilder<'a, G, T> {
522 DensityRepAnalysisDriverBuilder::default()
523 }
524}
525
526impl<'a, T> DensityRepAnalysisDriver<'a, UnitaryRepresentedSymmetryGroup, T>
530where
531 T: ComplexFloat + Lapack + Sync + Send,
532 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + Sync + Send,
533 for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
534{
535 fn_construct_unitary_group!(
536 construct_unitary_group
539 );
540}
541
542impl<'a, T> DensityRepAnalysisDriver<'a, MagneticRepresentedSymmetryGroup, T>
546where
547 T: ComplexFloat + Lapack + Sync + Send,
548 <T as ComplexFloat>::Real: From<f64> + Sync + Send + fmt::LowerExp + fmt::Debug,
549 for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
550{
551 fn_construct_magnetic_group!(
552 construct_magnetic_group
555 );
556}
557
558#[duplicate_item(
562 duplicate!{
563 [ dtype_nested; [f64]; [Complex<f64>] ]
564 [
565 gtype_ [ UnitaryRepresentedSymmetryGroup ]
566 dtype_ [ dtype_nested ]
567 doc_sub_ [ "Performs representation analysis using a unitary-represented group and stores the result." ]
568 analyse_fn_ [ analyse_representation ]
569 construct_group_ [ self.construct_unitary_group()? ]
570 ]
571 }
572 duplicate!{
573 [ dtype_nested; [f64]; [Complex<f64>] ]
574 [
575 gtype_ [ MagneticRepresentedSymmetryGroup ]
576 dtype_ [ dtype_nested ]
577 doc_sub_ [ "Performs corepresentation analysis using a magnetic-represented group and stores the result." ]
578 analyse_fn_ [ analyse_corepresentation ]
579 construct_group_ [ self.construct_magnetic_group()? ]
580 ]
581 }
582)]
583impl<'a> DensityRepAnalysisDriver<'a, gtype_, dtype_> {
584 #[doc = doc_sub_]
585 fn analyse_fn_(&mut self) -> Result<(), anyhow::Error> {
586 let params = self.parameters;
587 let sao_spatial_4c = self.sao_spatial_4c;
588 let sao_spatial_4c_h = self.sao_spatial_4c_h;
589 let group = construct_group_;
590 log_cc_transversal(&group);
591 let _ = find_angular_function_representation(&group, self.angular_function_parameters);
592 if group.is_double_group() {
593 let _ = find_spinor_function_representation(&group, self.angular_function_parameters);
594 }
595 let bao = self
596 .densities
597 .iter()
598 .next()
599 .map(|(_, den)| den.bao())
600 .ok_or_else(|| {
601 format_err!("Basis angular order information could not be extracted.")
602 })?;
603 log_bao(bao);
604
605 let (den_symmetries, den_symmetries_thresholds): (Vec<_>, Vec<_>) =
606 self.densities.iter().map(|(_, den)| {
607 DensitySymmetryOrbit::builder()
608 .group(&group)
609 .origin(den)
610 .integrality_threshold(params.integrality_threshold)
611 .linear_independence_threshold(params.linear_independence_threshold)
612 .symmetry_transformation_kind(params.symmetry_transformation_kind.clone())
613 .eigenvalue_comparison_mode(params.eigenvalue_comparison_mode.clone())
614 .build()
615 .map_err(|err| format_err!(err))
616 .and_then(|mut den_orbit| {
617 den_orbit
618 .calc_smat(Some(sao_spatial_4c), sao_spatial_4c_h, params.use_cayley_table)?
619 .normalise_smat()?
620 .calc_xmat(false)?;
621 let density_symmetry_thresholds = den_orbit
622 .smat_eigvals
623 .as_ref()
624 .map(|eigvals| {
625 let mut eigvals_vec = eigvals.iter().collect::<Vec<_>>();
626 match den_orbit.eigenvalue_comparison_mode() {
627 EigenvalueComparisonMode::Modulus => {
628 eigvals_vec.sort_by(|a, b| {
629 a.abs().partial_cmp(&b.abs()).expect("Unable to compare two eigenvalues based on their moduli.")
630 });
631 }
632 EigenvalueComparisonMode::Real => {
633 eigvals_vec.sort_by(|a, b| {
634 a.re().partial_cmp(&b.re()).expect("Unable to compare two eigenvalues based on their real parts.")
635 });
636 }
637 }
638 let eigval_above = match den_orbit.eigenvalue_comparison_mode() {
639 EigenvalueComparisonMode::Modulus => eigvals_vec
640 .iter()
641 .find(|val| {
642 val.abs() >= den_orbit.linear_independence_threshold
643 })
644 .copied()
645 .copied(),
646 EigenvalueComparisonMode::Real => eigvals_vec
647 .iter()
648 .find(|val| {
649 val.re() >= den_orbit.linear_independence_threshold
650 })
651 .copied()
652 .copied(),
653 };
654 eigvals_vec.reverse();
655 let eigval_below = match den_orbit.eigenvalue_comparison_mode() {
656 EigenvalueComparisonMode::Modulus => eigvals_vec
657 .iter()
658 .find(|val| {
659 val.abs() < den_orbit.linear_independence_threshold
660 })
661 .copied()
662 .copied(),
663 EigenvalueComparisonMode::Real => eigvals_vec
664 .iter()
665 .find(|val| {
666 val.re() < den_orbit.linear_independence_threshold
667 })
668 .copied()
669 .copied(),
670 };
671 (eigval_above, eigval_below)
672 })
673 .unwrap_or((None, None));
674 let den_sym = den_orbit.analyse_rep().map_err(|err| err.to_string());
675 Ok((den_sym, density_symmetry_thresholds))
676 })
677 .unwrap_or_else(|err| (Err(err.to_string()), (None, None)))
678 }).unzip();
679
680 let result = DensityRepAnalysisResult::builder()
681 .parameters(params)
682 .densities(self.densities.clone())
683 .group(group)
684 .density_symmetries(den_symmetries)
685 .density_symmetries_thresholds(den_symmetries_thresholds)
686 .build()?;
687 self.result = Some(result);
688
689 Ok(())
690 }
691}
692
693impl<'a, G, T> fmt::Display for DensityRepAnalysisDriver<'a, G, T>
701where
702 G: SymmetryGroupProperties + Clone,
703 G::CharTab: SubspaceDecomposable<T>,
704 T: ComplexFloat + Lapack,
705 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
706{
707 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
708 write_title(f, "Electron Density Symmetry Analysis")?;
709 writeln!(f)?;
710 writeln!(f, "{}", self.parameters)?;
711 Ok(())
712 }
713}
714
715impl<'a, G, T> fmt::Debug for DensityRepAnalysisDriver<'a, G, T>
716where
717 G: SymmetryGroupProperties + Clone,
718 G::CharTab: SubspaceDecomposable<T>,
719 T: ComplexFloat + Lapack,
720 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
721{
722 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
723 writeln!(f, "{self}")
724 }
725}
726
727#[duplicate_item(
731 duplicate!{
732 [ dtype_nested; [f64]; [Complex<f64>] ]
733 [
734 gtype_ [ UnitaryRepresentedSymmetryGroup ]
735 dtype_ [ dtype_nested ]
736 analyse_fn_ [ analyse_representation ]
737 ]
738 }
739 duplicate!{
740 [ dtype_nested; [f64]; [Complex<f64>] ]
741 [
742 gtype_ [ MagneticRepresentedSymmetryGroup ]
743 dtype_ [ dtype_nested ]
744 analyse_fn_ [ analyse_corepresentation ]
745 ]
746 }
747)]
748impl<'a> QSym2Driver for DensityRepAnalysisDriver<'a, gtype_, dtype_> {
749 type Params = DensityRepAnalysisParams<f64>;
750
751 type Outcome = DensityRepAnalysisResult<'a, gtype_, dtype_>;
752
753 fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
754 self.result.as_ref().ok_or_else(|| {
755 format_err!("No electron density representation analysis results found.")
756 })
757 }
758
759 fn run(&mut self) -> Result<(), anyhow::Error> {
760 self.log_output_display();
761 self.analyse_fn_()?;
762 self.result()?.log_output_display();
763 Ok(())
764 }
765}