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::SubspaceDecomposable;
18use crate::chartab::chartab_group::CharacterProperties;
19use crate::drivers::QSym2Driver;
20use crate::drivers::representation_analysis::angular_function::{
21 AngularFunctionRepAnalysisParams, find_angular_function_representation,
22 find_spinor_function_representation,
23};
24use crate::drivers::representation_analysis::{
25 CharacterTableDisplay, MagneticSymmetryAnalysisKind, fn_construct_magnetic_group,
26 fn_construct_unitary_group, log_bao, log_cc_transversal,
27};
28use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
29use crate::group::{GroupProperties, MagneticRepresentedGroup, UnitaryRepresentedGroup};
30use crate::io::format::{
31 QSym2Output, log_subtitle, nice_bool, qsym2_output, write_subtitle, write_title,
32};
33use crate::symmetry::symmetry_group::{
34 MagneticRepresentedSymmetryGroup, SymmetryGroupProperties, UnitaryRepresentedSymmetryGroup,
35};
36use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
37use crate::target::density::Density;
38use crate::target::density::density_analysis::DensitySymmetryOrbit;
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 "#", "Density", "Symmetry", "Eig. above",
322 )?;
323 writeln!(f, "{}", "┈".repeat(table_width))?;
324 for (deni, (((den, _), den_sym), (eig_above_opt, eig_below_opt))) in self
325 .densities
326 .iter()
327 .zip(self.density_symmetries.iter())
328 .zip(self.density_symmetries_thresholds.iter())
329 .enumerate()
330 {
331 writeln!(
332 f,
333 " {:>density_index_length$} {:<density_text_length$} {:<symmetry_length$} {:<eig_above_length$} {}",
334 deni,
335 den,
336 den_sym
337 .as_ref()
338 .map(|sym| sym.to_string())
339 .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 && sao_spatial_4c_h.shape() != sao_spatial_4c.shape()
451 {
452 return Err(
453 "Mismatched shapes between `sao_spatial_4c` and `sao_spatial_4c_h`.".to_string(),
454 );
455 }
456
457 let dens = self
458 .densities
459 .as_ref()
460 .ok_or("No electron densities found.".to_string())?;
461
462 let sym = if params.use_magnetic_group.is_some() {
463 sym_res
464 .magnetic_symmetry
465 .as_ref()
466 .ok_or("Magnetic symmetry requested for representation analysis, but no magnetic symmetry found.")?
467 } else {
468 &sym_res.unitary_symmetry
469 };
470
471 if sym.is_infinite() && params.infinite_order_to_finite.is_none() {
472 Err(format!(
473 "Representation analysis cannot be performed using the entirety of the infinite group `{}`. \
474 Consider setting the parameter `infinite_order_to_finite` to restrict to a finite subgroup instead.",
475 sym.group_name
476 .as_ref()
477 .expect("No symmetry group name found.")
478 ))
479 } else {
480 let baos = dens
481 .iter()
482 .map(|(_, den)| den.bao())
483 .collect::<HashSet<_>>();
484 if baos.len() != 1 {
485 Err("Inconsistent basis angular order information between densities.".to_string())
486 } else {
487 let naos = sao_spatial_4c.shape().iter().collect::<HashSet<_>>();
488 if naos.len() != 1 {
489 Err("The shape of the four-centre spatial SAO tensor is invalid: all four dimensions must have the same length.".to_string())
490 } else {
491 let nao = **naos.iter().next().ok_or(
492 "Unable to extract the dimensions of the four-centre spatial SAO tensor.",
493 )?;
494 if !dens.iter().all(|(_, den)| den.bao().n_funcs() == nao) {
495 Err("The dimensions of the four-centre spatial SAO tensor do not match the number of spatial AO basis functions.".to_string())
496 } else {
497 Ok(())
498 }
499 }
500 }
501 }
502 }
503}
504
505impl<'a, G, T> DensityRepAnalysisDriver<'a, G, T>
513where
514 G: SymmetryGroupProperties + Clone,
515 G::CharTab: SubspaceDecomposable<T>,
516 T: ComplexFloat + Lapack,
517 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
518{
519 pub fn builder() -> DensityRepAnalysisDriverBuilder<'a, G, T> {
521 DensityRepAnalysisDriverBuilder::default()
522 }
523}
524
525impl<'a, T> DensityRepAnalysisDriver<'a, UnitaryRepresentedSymmetryGroup, T>
529where
530 T: ComplexFloat + Lapack + Sync + Send,
531 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + Sync + Send,
532 for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
533{
534 fn_construct_unitary_group!(
535 construct_unitary_group
538 );
539}
540
541impl<'a, T> DensityRepAnalysisDriver<'a, MagneticRepresentedSymmetryGroup, T>
545where
546 T: ComplexFloat + Lapack + Sync + Send,
547 <T as ComplexFloat>::Real: From<f64> + Sync + Send + fmt::LowerExp + fmt::Debug,
548 for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
549{
550 fn_construct_magnetic_group!(
551 construct_magnetic_group
554 );
555}
556
557#[duplicate_item(
561 duplicate!{
562 [ dtype_nested; [f64]; [Complex<f64>] ]
563 [
564 gtype_ [ UnitaryRepresentedSymmetryGroup ]
565 dtype_ [ dtype_nested ]
566 doc_sub_ [ "Performs representation analysis using a unitary-represented group and stores the result." ]
567 analyse_fn_ [ analyse_representation ]
568 construct_group_ [ self.construct_unitary_group()? ]
569 ]
570 }
571 duplicate!{
572 [ dtype_nested; [f64]; [Complex<f64>] ]
573 [
574 gtype_ [ MagneticRepresentedSymmetryGroup ]
575 dtype_ [ dtype_nested ]
576 doc_sub_ [ "Performs corepresentation analysis using a magnetic-represented group and stores the result." ]
577 analyse_fn_ [ analyse_corepresentation ]
578 construct_group_ [ self.construct_magnetic_group()? ]
579 ]
580 }
581)]
582impl<'a> DensityRepAnalysisDriver<'a, gtype_, dtype_> {
583 #[doc = doc_sub_]
584 fn analyse_fn_(&mut self) -> Result<(), anyhow::Error> {
585 let params = self.parameters;
586 let sao_spatial_4c = self.sao_spatial_4c;
587 let sao_spatial_4c_h = self.sao_spatial_4c_h;
588 let group = construct_group_;
589 log_cc_transversal(&group);
590 let _ = find_angular_function_representation(&group, self.angular_function_parameters);
591 if group.is_double_group() {
592 let _ = find_spinor_function_representation(&group, self.angular_function_parameters);
593 }
594 let bao = self
595 .densities
596 .first()
597 .map(|(_, den)| den.bao())
598 .ok_or_else(|| {
599 format_err!("Basis angular order information could not be extracted.")
600 })?;
601 log_bao(bao, None);
602
603 let (den_symmetries, den_symmetries_thresholds): (Vec<_>, Vec<_>) =
604 self.densities.iter().map(|(_, den)| {
605 DensitySymmetryOrbit::builder()
606 .group(&group)
607 .origin(den)
608 .integrality_threshold(params.integrality_threshold)
609 .linear_independence_threshold(params.linear_independence_threshold)
610 .symmetry_transformation_kind(params.symmetry_transformation_kind.clone())
611 .eigenvalue_comparison_mode(params.eigenvalue_comparison_mode.clone())
612 .build()
613 .map_err(|err| format_err!(err))
614 .and_then(|mut den_orbit| {
615 den_orbit
616 .calc_smat(Some(sao_spatial_4c), sao_spatial_4c_h, params.use_cayley_table)?
617 .normalise_smat()?
618 .calc_xmat(false)?;
619 let density_symmetry_thresholds = den_orbit
620 .smat_eigvals
621 .as_ref()
622 .map(|eigvals| {
623 let mut eigvals_vec = eigvals.iter().collect::<Vec<_>>();
624 match den_orbit.eigenvalue_comparison_mode() {
625 EigenvalueComparisonMode::Modulus => {
626 eigvals_vec.sort_by(|a, b| {
627 a.abs().partial_cmp(&b.abs()).expect("Unable to compare two eigenvalues based on their moduli.")
628 });
629 }
630 EigenvalueComparisonMode::Real => {
631 eigvals_vec.sort_by(|a, b| {
632 a.re().partial_cmp(&b.re()).expect("Unable to compare two eigenvalues based on their real parts.")
633 });
634 }
635 }
636 let eigval_above = match den_orbit.eigenvalue_comparison_mode() {
637 EigenvalueComparisonMode::Modulus => eigvals_vec
638 .iter()
639 .find(|val| {
640 val.abs() >= den_orbit.linear_independence_threshold
641 })
642 .copied()
643 .copied(),
644 EigenvalueComparisonMode::Real => eigvals_vec
645 .iter()
646 .find(|val| {
647 val.re() >= den_orbit.linear_independence_threshold
648 })
649 .copied()
650 .copied(),
651 };
652 eigvals_vec.reverse();
653 let eigval_below = match den_orbit.eigenvalue_comparison_mode() {
654 EigenvalueComparisonMode::Modulus => eigvals_vec
655 .iter()
656 .find(|val| {
657 val.abs() < den_orbit.linear_independence_threshold
658 })
659 .copied()
660 .copied(),
661 EigenvalueComparisonMode::Real => eigvals_vec
662 .iter()
663 .find(|val| {
664 val.re() < den_orbit.linear_independence_threshold
665 })
666 .copied()
667 .copied(),
668 };
669 (eigval_above, eigval_below)
670 })
671 .unwrap_or((None, None));
672 let den_sym = den_orbit.analyse_rep().map_err(|err| err.to_string());
673 Ok((den_sym, density_symmetry_thresholds))
674 })
675 .unwrap_or_else(|err| (Err(err.to_string()), (None, None)))
676 }).unzip();
677
678 let result = DensityRepAnalysisResult::builder()
679 .parameters(params)
680 .densities(self.densities.clone())
681 .group(group)
682 .density_symmetries(den_symmetries)
683 .density_symmetries_thresholds(den_symmetries_thresholds)
684 .build()?;
685 self.result = Some(result);
686
687 Ok(())
688 }
689}
690
691impl<'a, G, T> fmt::Display for DensityRepAnalysisDriver<'a, G, T>
699where
700 G: SymmetryGroupProperties + Clone,
701 G::CharTab: SubspaceDecomposable<T>,
702 T: ComplexFloat + Lapack,
703 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
704{
705 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
706 write_title(f, "Electron Density Symmetry Analysis")?;
707 writeln!(f)?;
708 writeln!(f, "{}", self.parameters)?;
709 Ok(())
710 }
711}
712
713impl<'a, G, T> fmt::Debug for DensityRepAnalysisDriver<'a, G, T>
714where
715 G: SymmetryGroupProperties + Clone,
716 G::CharTab: SubspaceDecomposable<T>,
717 T: ComplexFloat + Lapack,
718 <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
719{
720 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
721 writeln!(f, "{self}")
722 }
723}
724
725#[duplicate_item(
729 duplicate!{
730 [ dtype_nested; [f64]; [Complex<f64>] ]
731 [
732 gtype_ [ UnitaryRepresentedSymmetryGroup ]
733 dtype_ [ dtype_nested ]
734 analyse_fn_ [ analyse_representation ]
735 ]
736 }
737 duplicate!{
738 [ dtype_nested; [f64]; [Complex<f64>] ]
739 [
740 gtype_ [ MagneticRepresentedSymmetryGroup ]
741 dtype_ [ dtype_nested ]
742 analyse_fn_ [ analyse_corepresentation ]
743 ]
744 }
745)]
746impl<'a> QSym2Driver for DensityRepAnalysisDriver<'a, gtype_, dtype_> {
747 type Params = DensityRepAnalysisParams<f64>;
748
749 type Outcome = DensityRepAnalysisResult<'a, gtype_, dtype_>;
750
751 fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
752 self.result.as_ref().ok_or_else(|| {
753 format_err!("No electron density representation analysis results found.")
754 })
755 }
756
757 fn run(&mut self) -> Result<(), anyhow::Error> {
758 self.log_output_display();
759 self.analyse_fn_()?;
760 self.result()?.log_output_display();
761 Ok(())
762 }
763}