qsym2/drivers/representation_analysis/
vibrational_coordinate.rs

1//! Driver for symmetry analysis of vibrational coordinates.
2
3use std::fmt;
4use std::ops::Mul;
5
6use anyhow::{self, bail, format_err};
7use derive_builder::Builder;
8use duplicate::duplicate_item;
9use ndarray_linalg::types::Lapack;
10use num_complex::{Complex, ComplexFloat};
11use num_traits::Float;
12use rayon::prelude::*;
13use serde::{Deserialize, Serialize};
14
15use crate::analysis::{EigenvalueComparisonMode, RepAnalysis};
16use crate::chartab::SubspaceDecomposable;
17use crate::chartab::chartab_group::CharacterProperties;
18use crate::drivers::QSym2Driver;
19use crate::drivers::representation_analysis::angular_function::{
20    AngularFunctionRepAnalysisParams, find_angular_function_representation,
21    find_spinor_function_representation,
22};
23use crate::drivers::representation_analysis::{
24    CharacterTableDisplay, MagneticSymmetryAnalysisKind, fn_construct_magnetic_group,
25    fn_construct_unitary_group, log_cc_transversal,
26};
27use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
28use crate::group::{GroupProperties, MagneticRepresentedGroup, UnitaryRepresentedGroup};
29use crate::io::format::{
30    QSym2Output, log_subtitle, nice_bool, qsym2_output, write_subtitle, write_title,
31};
32use crate::symmetry::symmetry_group::{
33    MagneticRepresentedSymmetryGroup, SymmetryGroupProperties, UnitaryRepresentedSymmetryGroup,
34};
35use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
36use crate::target::vibration::VibrationalCoordinateCollection;
37use crate::target::vibration::vibration_analysis::VibrationalCoordinateSymmetryOrbit;
38
39#[cfg(test)]
40#[path = "vibrational_coordinate_tests.rs"]
41mod vibrational_coordinate_tests;
42
43// ==================
44// Struct definitions
45// ==================
46
47// ----------
48// Parameters
49// ----------
50
51const fn default_true() -> bool {
52    true
53}
54const fn default_symbolic() -> Option<CharacterTableDisplay> {
55    Some(CharacterTableDisplay::Symbolic)
56}
57
58/// Structure containing control parameters for vibrational coordinate representation analysis.
59#[derive(Clone, Builder, Debug, Serialize, Deserialize)]
60pub struct VibrationalCoordinateRepAnalysisParams<T: From<f64>> {
61    /// Threshold for checking if subspace multiplicities are integral.
62    pub integrality_threshold: T,
63
64    /// Threshold for determining zero eigenvalues in the orbit overlap matrix.
65    pub linear_independence_threshold: T,
66
67    /// Option indicating if the magnetic group is to be used for symmetry analysis, and if so,
68    /// whether unitary representations or unitary-antiunitary corepresentations should be used.
69    #[builder(default = "None")]
70    #[serde(default)]
71    pub use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
72
73    /// Boolean indicating if the double group is to be used for symmetry analysis.
74    #[builder(default = "false")]
75    #[serde(default)]
76    pub use_double_group: bool,
77
78    /// Boolean indicating if the Cayley table of the group, if available, should be used to speed
79    /// up the computation of orbit overlap matrices.
80    #[builder(default = "true")]
81    #[serde(default = "default_true")]
82    pub use_cayley_table: bool,
83
84    /// The kind of symmetry transformation to be applied on the reference vibrational coordinate to
85    /// generate the orbit for symmetry analysis.
86    #[builder(default = "SymmetryTransformationKind::Spatial")]
87    #[serde(default)]
88    pub symmetry_transformation_kind: SymmetryTransformationKind,
89
90    /// Option indicating if the character table of the group used for symmetry analysis is to be
91    /// printed out.
92    #[builder(default = "Some(CharacterTableDisplay::Symbolic)")]
93    #[serde(default = "default_symbolic")]
94    pub write_character_table: Option<CharacterTableDisplay>,
95
96    /// The comparison mode for filtering out orbit overlap eigenvalues.
97    #[builder(default = "EigenvalueComparisonMode::Modulus")]
98    #[serde(default)]
99    pub eigenvalue_comparison_mode: EigenvalueComparisonMode,
100
101    /// The finite order to which any infinite-order symmetry element is reduced, so that a finite
102    /// subgroup of an infinite group can be used for the symmetry analysis.
103    #[builder(default = "None")]
104    #[serde(default)]
105    pub infinite_order_to_finite: Option<u32>,
106}
107
108impl<T> VibrationalCoordinateRepAnalysisParams<T>
109where
110    T: Float + From<f64>,
111{
112    /// Returns a builder to construct a [`VibrationalCoordinateRepAnalysisParams`] structure.
113    pub fn builder() -> VibrationalCoordinateRepAnalysisParamsBuilder<T> {
114        VibrationalCoordinateRepAnalysisParamsBuilder::default()
115    }
116}
117
118impl Default for VibrationalCoordinateRepAnalysisParams<f64> {
119    fn default() -> Self {
120        Self::builder()
121            .integrality_threshold(1e-7)
122            .linear_independence_threshold(1e-7)
123            .build()
124            .expect("Unable to construct a default `VibrationalCoordinateRepAnalysisParams<f64>`.")
125    }
126}
127
128impl<T> fmt::Display for VibrationalCoordinateRepAnalysisParams<T>
129where
130    T: From<f64> + fmt::LowerExp + fmt::Debug,
131{
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        writeln!(
134            f,
135            "Integrality threshold: {:.3e}",
136            self.integrality_threshold
137        )?;
138        writeln!(
139            f,
140            "Linear independence threshold: {:.3e}",
141            self.linear_independence_threshold
142        )?;
143        writeln!(
144            f,
145            "Orbit eigenvalue comparison mode: {}",
146            self.eigenvalue_comparison_mode
147        )?;
148        writeln!(f)?;
149        writeln!(
150            f,
151            "Use magnetic group for analysis: {}",
152            match self.use_magnetic_group {
153                None => "no",
154                Some(MagneticSymmetryAnalysisKind::Representation) =>
155                    "yes, using unitary representations",
156                Some(MagneticSymmetryAnalysisKind::Corepresentation) =>
157                    "yes, using magnetic corepresentations",
158            }
159        )?;
160        writeln!(
161            f,
162            "Use double group for analysis: {}",
163            nice_bool(self.use_double_group)
164        )?;
165        writeln!(
166            f,
167            "Use Cayley table for orbit overlap matrices: {}",
168            nice_bool(self.use_cayley_table)
169        )?;
170        if let Some(finite_order) = self.infinite_order_to_finite {
171            writeln!(f, "Infinite order to finite: {finite_order}")?;
172        }
173        writeln!(
174            f,
175            "Symmetry transformation kind: {}",
176            self.symmetry_transformation_kind
177        )?;
178        writeln!(f)?;
179        writeln!(
180            f,
181            "Write character table: {}",
182            if let Some(chartab_display) = self.write_character_table.as_ref() {
183                format!("yes, {}", chartab_display.to_string().to_lowercase())
184            } else {
185                "no".to_string()
186            }
187        )?;
188
189        Ok(())
190    }
191}
192
193// ------
194// Result
195// ------
196
197/// Structure to contain vibrational coordinate representation analysis results.
198#[derive(Clone, Builder)]
199pub struct VibrationalCoordinateRepAnalysisResult<'a, G, T>
200where
201    G: SymmetryGroupProperties + Clone,
202    G::CharTab: SubspaceDecomposable<T>,
203    T: ComplexFloat + Lapack,
204    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
205{
206    /// The control parameters used to obtain this set of vibrational coordinate representation
207    /// analysis results.
208    parameters: &'a VibrationalCoordinateRepAnalysisParams<<T as ComplexFloat>::Real>,
209
210    /// The collection of vibrational coordinates being analysed.
211    vibrational_coordinate_collection: &'a VibrationalCoordinateCollection<'a, T>,
212
213    /// The group used for the representation analysis.
214    group: G,
215
216    /// The deduced symmetries of the vibrational coordinates.
217    vib_symmetries: Vec<Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>>,
218
219    /// The overlap eigenvalues above and below the linear independence threshold for each
220    /// vibrational coordinate symmetry deduction.
221    vib_symmetries_thresholds: Option<Vec<(Option<T>, Option<T>)>>,
222}
223
224impl<'a, G, T> VibrationalCoordinateRepAnalysisResult<'a, G, T>
225where
226    G: SymmetryGroupProperties + Clone,
227    G::CharTab: SubspaceDecomposable<T>,
228    T: ComplexFloat + Lapack,
229    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
230{
231    /// Returns a builder to construct a new [`VibrationalCoordinateRepAnalysisResultBuilder`]
232    /// structure.
233    pub fn builder() -> VibrationalCoordinateRepAnalysisResultBuilder<'a, G, T> {
234        VibrationalCoordinateRepAnalysisResultBuilder::default()
235    }
236
237    /// Returns the control parameters used to obtain this set of vibrational coordinate
238    /// representation analysis results.
239    pub fn parameters(&self) -> &VibrationalCoordinateRepAnalysisParams<<T as ComplexFloat>::Real> {
240        self.parameters
241    }
242
243    /// Returns the collection of vibrational coordinates being analysed.
244    pub fn vibrational_coordinate_collection(&self) -> &VibrationalCoordinateCollection<'a, T> {
245        self.vibrational_coordinate_collection
246    }
247}
248
249impl<'a, G, T> fmt::Display for VibrationalCoordinateRepAnalysisResult<'a, G, T>
250where
251    G: SymmetryGroupProperties + Clone,
252    G::CharTab: SubspaceDecomposable<T>,
253    T: ComplexFloat + Lapack,
254    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
255{
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        write_subtitle(f, "Orbit-based symmetry analysis results")?;
258        writeln!(f)?;
259        writeln!(
260            f,
261            "> Group: {} ({})",
262            self.group
263                .finite_subgroup_name()
264                .map(|subgroup_name| format!("{} > {}", self.group.name(), subgroup_name))
265                .unwrap_or(self.group.name()),
266            self.group.group_type().to_string().to_lowercase()
267        )?;
268        writeln!(f)?;
269
270        let vib_index_length =
271            (usize::try_from(self.vibrational_coordinate_collection.n_modes().ilog10())
272                .unwrap_or(4)
273                + 2)
274            .max(4);
275        let vib_frequency_length = self
276            .vibrational_coordinate_collection
277            .frequencies()
278            .iter()
279            .map(|freq| format!("{freq:+.3}").chars().count())
280            .max()
281            .unwrap_or(9)
282            .max(9);
283        let vib_symmetry_length = self
284            .vib_symmetries
285            .iter()
286            .map(|vib_sym_res| {
287                vib_sym_res
288                    .as_ref()
289                    .map(|sym| sym.to_string())
290                    .unwrap_or("--".to_string())
291                    .chars()
292                    .count()
293            })
294            .max()
295            .unwrap_or(8)
296            .max(8);
297
298        let vib_eig_above_length: usize = self
299            .vib_symmetries_thresholds
300            .as_ref()
301            .map(|vib_symmetries_thresholds| {
302                vib_symmetries_thresholds
303                    .iter()
304                    .map(|(above, _)| {
305                        above
306                            .as_ref()
307                            .map(|eig| format!("{eig:+.3e}"))
308                            .unwrap_or("--".to_string())
309                            .chars()
310                            .count()
311                    })
312                    .max()
313                    .unwrap_or(10)
314                    .max(10)
315            })
316            .unwrap_or(10);
317        let vib_eig_below_length: usize = self
318            .vib_symmetries_thresholds
319            .as_ref()
320            .map(|vib_symmetries_thresholds| {
321                vib_symmetries_thresholds
322                    .iter()
323                    .map(|(_, below)| {
324                        below
325                            .as_ref()
326                            .map(|eig| format!("{eig:+.3e}"))
327                            .unwrap_or("--".to_string())
328                            .chars()
329                            .count()
330                    })
331                    .max()
332                    .unwrap_or(10)
333                    .max(10)
334            })
335            .unwrap_or(10);
336
337        writeln!(f, "> Vibrational coordinate results")?;
338
339        writeln!(
340            f,
341            "{}",
342            "┈".repeat(
343                10 + vib_index_length
344                    + vib_frequency_length
345                    + vib_symmetry_length
346                    + vib_eig_above_length
347                    + vib_eig_below_length
348            )
349        )?;
350        writeln!(
351            f,
352            " {:>vib_index_length$}  {:<vib_frequency_length$}  {:<vib_symmetry_length$}  {:<vib_eig_above_length$}  Eig. below",
353            "Mode", "Frequency", "Symmetry", "Eig. above"
354        )?;
355        writeln!(
356            f,
357            "{}",
358            "┈".repeat(
359                10 + vib_index_length
360                    + vib_frequency_length
361                    + vib_symmetry_length
362                    + vib_eig_above_length
363                    + vib_eig_below_length
364            )
365        )?;
366
367        for (vibi, vib_sym) in self.vib_symmetries.iter().enumerate() {
368            let vib_frequency_str = self
369                .vibrational_coordinate_collection
370                .frequencies()
371                .get(vibi)
372                .map(|freq| format!("{freq:>+vib_frequency_length$.3}"))
373                .unwrap_or("--".to_string());
374            let vib_sym_str = vib_sym
375                .as_ref()
376                .map(|sym| sym.to_string())
377                .unwrap_or("--".to_string());
378            let (eig_above_str, eig_below_str) = self
379                .vib_symmetries_thresholds
380                .as_ref()
381                .map(|vib_symmetries_thresholds| {
382                    vib_symmetries_thresholds
383                        .get(vibi)
384                        .map(|(eig_above_opt, eig_below_opt)| {
385                            (
386                                eig_above_opt
387                                    .map(|eig_above| format!("{eig_above:>+.3e}"))
388                                    .unwrap_or("--".to_string()),
389                                eig_below_opt
390                                    .map(|eig_below| format!("{eig_below:>+.3e}"))
391                                    .unwrap_or("--".to_string()),
392                            )
393                        })
394                        .unwrap_or(("--".to_string(), "--".to_string()))
395                })
396                .unwrap_or(("--".to_string(), "--".to_string()));
397            writeln!(
398                f,
399                " {vibi:>vib_index_length$}  \
400                {vib_frequency_str:<vib_frequency_length$}  \
401                {vib_sym_str:<vib_symmetry_length$}  \
402                {eig_above_str:<vib_eig_above_length$}  \
403                {eig_below_str}"
404            )?;
405        }
406        writeln!(
407            f,
408            "{}",
409            "┈".repeat(
410                10 + vib_index_length
411                    + vib_frequency_length
412                    + vib_symmetry_length
413                    + vib_eig_above_length
414                    + vib_eig_below_length
415            )
416        )?;
417        writeln!(f)?;
418
419        Ok(())
420    }
421}
422
423impl<'a, G, T> fmt::Debug for VibrationalCoordinateRepAnalysisResult<'a, G, T>
424where
425    G: SymmetryGroupProperties + Clone,
426    G::CharTab: SubspaceDecomposable<T>,
427    T: ComplexFloat + Lapack,
428    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
429{
430    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431        writeln!(f, "{self}")
432    }
433}
434
435impl<'a, G, T> VibrationalCoordinateRepAnalysisResult<'a, G, T>
436where
437    G: SymmetryGroupProperties + Clone,
438    G::CharTab: SubspaceDecomposable<T>,
439    T: ComplexFloat + Lapack,
440    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + fmt::Display,
441{
442    /// Returns the vibrational coordinate symmetries obtained from the analysis result.
443    pub fn vibrational_coordinate_symmetries(
444        &self,
445    ) -> &Vec<Result<<G::CharTab as SubspaceDecomposable<T>>::Decomposition, String>> {
446        &self.vib_symmetries
447    }
448}
449
450// ------
451// Driver
452// ------
453
454// ~~~~~~~~~~~~~~~~~
455// Struct definition
456// ~~~~~~~~~~~~~~~~~
457
458/// Driver structure for performing representation analysis on vibrational coordinates.
459#[derive(Clone, Builder)]
460#[builder(build_fn(validate = "Self::validate"))]
461pub struct VibrationalCoordinateRepAnalysisDriver<'a, G, T>
462where
463    G: SymmetryGroupProperties + Clone,
464    G::CharTab: SubspaceDecomposable<T>,
465    T: ComplexFloat + Lapack,
466    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
467{
468    /// The control parameters for vibrational coordinate representation analysis.
469    parameters: &'a VibrationalCoordinateRepAnalysisParams<<T as ComplexFloat>::Real>,
470
471    /// The collection of vibrational coordinates to be analysed.
472    vibrational_coordinate_collection: &'a VibrationalCoordinateCollection<'a, T>,
473
474    /// The result from symmetry-group detection on the underlying molecular structure of the
475    /// vibrational coordinates.
476    symmetry_group: &'a SymmetryGroupDetectionResult,
477
478    /// The control parameters for symmetry analysis of angular functions.
479    angular_function_parameters: &'a AngularFunctionRepAnalysisParams,
480
481    /// The result of the vibrational coordinate representation analysis.
482    #[builder(setter(skip), default = "None")]
483    result: Option<VibrationalCoordinateRepAnalysisResult<'a, G, T>>,
484}
485
486impl<'a, G, T> VibrationalCoordinateRepAnalysisDriverBuilder<'a, G, T>
487where
488    G: SymmetryGroupProperties + Clone,
489    G::CharTab: SubspaceDecomposable<T>,
490    T: ComplexFloat + Lapack,
491    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
492{
493    fn validate(&self) -> Result<(), String> {
494        let params = self.parameters.ok_or(
495            "No vibrational coordinate representation analysis parameters found.".to_string(),
496        )?;
497
498        let sym_res = self
499            .symmetry_group
500            .ok_or("No symmetry group information found.".to_string())?;
501
502        let sym = if params.use_magnetic_group.is_some() {
503            sym_res
504                .magnetic_symmetry
505                .as_ref()
506                .ok_or("Magnetic symmetry requested for representation analysis, but no magnetic symmetry found.")?
507        } else {
508            &sym_res.unitary_symmetry
509        };
510
511        if sym.is_infinite() && params.infinite_order_to_finite.is_none() {
512            Err(format!(
513                "Representation analysis cannot be performed using the entirety of the infinite group `{}`. \
514                    Consider setting the parameter `infinite_order_to_finite` to restrict to a finite subgroup instead.",
515                sym.group_name
516                    .as_ref()
517                    .expect("No symmetry group name found.")
518            ))
519        } else {
520            Ok(())
521        }
522    }
523}
524
525// ~~~~~~~~~~~~~~~~~~~~~~
526// Struct implementations
527// ~~~~~~~~~~~~~~~~~~~~~~
528
529// Generic for all symmetry groups G and determinant numeric type T
530// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
531
532impl<'a, G, T> VibrationalCoordinateRepAnalysisDriver<'a, G, T>
533where
534    G: SymmetryGroupProperties + Clone,
535    G::CharTab: SubspaceDecomposable<T>,
536    T: ComplexFloat + Lapack,
537    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
538{
539    /// Returns a builder to construct a [`VibrationalCoordinateRepAnalysisDriver`] structure.
540    pub fn builder() -> VibrationalCoordinateRepAnalysisDriverBuilder<'a, G, T> {
541        VibrationalCoordinateRepAnalysisDriverBuilder::default()
542    }
543}
544
545// Specific for unitary-represented symmetry groups, but generic for determinant numeric type T
546// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
547
548impl<'a, T> VibrationalCoordinateRepAnalysisDriver<'a, UnitaryRepresentedSymmetryGroup, T>
549where
550    T: ComplexFloat + Lapack + Sync + Send,
551    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug + Sync + Send,
552    for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
553{
554    fn_construct_unitary_group!(
555        /// Constructs the unitary-represented group (which itself can be unitary or magnetic) ready
556        /// for vibrational coordinate representation analysis.
557        construct_unitary_group
558    );
559}
560
561// Specific for magnetic-represented symmetry groups, but generic for determinant numeric type T
562// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
563
564impl<'a, T> VibrationalCoordinateRepAnalysisDriver<'a, MagneticRepresentedSymmetryGroup, T>
565where
566    T: ComplexFloat + Lapack + Sync + Send,
567    <T as ComplexFloat>::Real: From<f64> + Sync + Send + fmt::LowerExp + fmt::Debug,
568    for<'b> Complex<f64>: Mul<&'b T, Output = Complex<f64>>,
569{
570    fn_construct_magnetic_group!(
571        /// Constructs the magnetic-represented group (which itself can only be magnetic) ready for
572        /// vibrational coordinate corepresentation analysis.
573        construct_magnetic_group
574    );
575}
576
577// Specific for unitary-represented and magnetic-represented symmetry groups and determinant numeric types f64 and C128
578// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
579
580#[duplicate_item(
581    duplicate!{
582        [ dtype_nested; [f64]; [Complex<f64>] ]
583        [
584            gtype_ [ UnitaryRepresentedSymmetryGroup ]
585            dtype_ [ dtype_nested ]
586            doc_sub_ [ "Performs representation analysis using a unitary-represented group and stores the result." ]
587            analyse_fn_ [ analyse_representation ]
588            construct_group_ [ self.construct_unitary_group()? ]
589        ]
590    }
591    duplicate!{
592        [ dtype_nested; [f64]; [Complex<f64>] ]
593        [
594            gtype_ [ MagneticRepresentedSymmetryGroup ]
595            dtype_ [ dtype_nested ]
596            doc_sub_ [ "Performs corepresentation analysis using a magnetic-represented group and stores the result." ]
597            analyse_fn_ [ analyse_corepresentation ]
598            construct_group_ [ self.construct_magnetic_group()? ]
599        ]
600    }
601)]
602impl<'a> VibrationalCoordinateRepAnalysisDriver<'a, gtype_, dtype_> {
603    #[doc = doc_sub_]
604    fn analyse_fn_(&mut self) -> Result<(), anyhow::Error> {
605        let params = self.parameters;
606        let group = construct_group_;
607        log_cc_transversal(&group);
608        let _ = find_angular_function_representation(&group, self.angular_function_parameters);
609        if group.is_double_group() {
610            let _ = find_spinor_function_representation(&group, self.angular_function_parameters);
611        }
612
613        let (vib_symmetries, vib_symmetries_thresholds) = {
614            let vibs = self
615                .vibrational_coordinate_collection
616                .to_vibrational_coordinates();
617            let vib_orbits = vibs.par_iter().map(|vib| {
618                let mut vib_orbit = VibrationalCoordinateSymmetryOrbit::builder()
619                    .group(&group)
620                    .origin(vib)
621                    .integrality_threshold(params.integrality_threshold)
622                    .linear_independence_threshold(params.linear_independence_threshold)
623                    .symmetry_transformation_kind(params.symmetry_transformation_kind.clone())
624                    .eigenvalue_comparison_mode(params.eigenvalue_comparison_mode.clone())
625                    .build()?;
626                vib_orbit
627                    .calc_smat(None, None, params.use_cayley_table)
628                    .and_then(|orbit| orbit.calc_xmat(false))?;
629                Ok::<_, anyhow::Error>(vib_orbit)
630            });
631            let (vib_symmetries, vib_symmetries_thresholds): (Vec<_>, Vec<_>) = vib_orbits
632                .map(|vib_orbit_res| {
633                    vib_orbit_res
634                        .as_ref()
635                        .map(|vib_orbit| {
636                            let sym_res = vib_orbit.analyse_rep().map_err(|err| err.to_string());
637                            let eigs = vib_orbit
638                                .smat_eigvals
639                                .as_ref()
640                                .map(|eigvals| {
641                                    let mut eigvals_vec = eigvals.iter().collect::<Vec<_>>();
642                                    match vib_orbit.eigenvalue_comparison_mode {
643                                        EigenvalueComparisonMode::Modulus => {
644                                            eigvals_vec.sort_by(|a, b| {
645                                                a.abs().partial_cmp(&b.abs()).expect("Unable to compare two eigenvalues based on their moduli.")
646                                            });
647                                        }
648                                        EigenvalueComparisonMode::Real => {
649                                            eigvals_vec.sort_by(|a, b| {
650                                                a.re().partial_cmp(&b.re()).expect("Unable to compare two eigenvalues based on their real parts.")
651                                            });
652                                        }
653                                    }
654                                    let eigval_above = match vib_orbit.eigenvalue_comparison_mode {
655                                        EigenvalueComparisonMode::Modulus => eigvals_vec
656                                            .iter()
657                                            .find(|val| {
658                                                val.abs() >= vib_orbit.linear_independence_threshold
659                                            })
660                                            .copied()
661                                            .copied(),
662                                        EigenvalueComparisonMode::Real => eigvals_vec
663                                            .iter()
664                                            .find(|val| {
665                                                val.re() >= vib_orbit.linear_independence_threshold
666                                            })
667                                            .copied()
668                                            .copied(),
669                                    };
670                                    eigvals_vec.reverse();
671                                    let eigval_below = match vib_orbit.eigenvalue_comparison_mode {
672                                        EigenvalueComparisonMode::Modulus => eigvals_vec
673                                            .iter()
674                                            .find(|val| {
675                                                val.abs() < vib_orbit.linear_independence_threshold
676                                            })
677                                            .copied()
678                                            .copied(),
679                                        EigenvalueComparisonMode::Real => eigvals_vec
680                                            .iter()
681                                            .find(|val| {
682                                                val.re() < vib_orbit.linear_independence_threshold
683                                            })
684                                            .copied()
685                                            .copied(),
686                                    };
687                                    (eigval_above, eigval_below)
688                                })
689                                .unwrap_or_else(|| (None, None));
690                            (sym_res, eigs)
691                        })
692                        .unwrap_or((Err("--".to_string()), (None, None)))
693                })
694                .unzip();
695            (vib_symmetries, vib_symmetries_thresholds)
696        };
697
698        let result = VibrationalCoordinateRepAnalysisResult::builder()
699            .parameters(params)
700            .vibrational_coordinate_collection(self.vibrational_coordinate_collection)
701            .group(group)
702            .vib_symmetries(vib_symmetries)
703            .vib_symmetries_thresholds(Some(vib_symmetries_thresholds))
704            .build()?;
705        self.result = Some(result);
706
707        Ok(())
708    }
709}
710
711// ~~~~~~~~~~~~~~~~~~~~~
712// Trait implementations
713// ~~~~~~~~~~~~~~~~~~~~~
714
715// Generic for all symmetry groups G and determinant numeric type T
716// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
717
718impl<'a, G, T> fmt::Display for VibrationalCoordinateRepAnalysisDriver<'a, G, T>
719where
720    G: SymmetryGroupProperties + Clone,
721    G::CharTab: SubspaceDecomposable<T>,
722    T: ComplexFloat + Lapack,
723    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
724{
725    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
726        write_title(f, "Vibrational Coordinate Symmetry Analysis")?;
727        writeln!(f)?;
728        writeln!(f, "{}", self.parameters)?;
729        Ok(())
730    }
731}
732
733impl<'a, G, T> fmt::Debug for VibrationalCoordinateRepAnalysisDriver<'a, G, T>
734where
735    G: SymmetryGroupProperties + Clone,
736    G::CharTab: SubspaceDecomposable<T>,
737    T: ComplexFloat + Lapack,
738    <T as ComplexFloat>::Real: From<f64> + fmt::LowerExp + fmt::Debug,
739{
740    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
741        writeln!(f, "{self}")
742    }
743}
744
745// Specific for unitary/magnetic-represented groups and vibration numeric type f64/Complex<f64>
746// ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
747
748#[duplicate_item(
749    duplicate!{
750        [ dtype_nested; [f64]; [Complex<f64>] ]
751        [
752            gtype_ [ UnitaryRepresentedSymmetryGroup ]
753            dtype_ [ dtype_nested ]
754            analyse_fn_ [ analyse_representation ]
755        ]
756    }
757    duplicate!{
758        [ dtype_nested; [f64]; [Complex<f64>] ]
759        [
760            gtype_ [ MagneticRepresentedSymmetryGroup ]
761            dtype_ [ dtype_nested ]
762            analyse_fn_ [ analyse_corepresentation ]
763        ]
764    }
765)]
766impl<'a> QSym2Driver for VibrationalCoordinateRepAnalysisDriver<'a, gtype_, dtype_> {
767    type Params = VibrationalCoordinateRepAnalysisParams<f64>;
768
769    type Outcome = VibrationalCoordinateRepAnalysisResult<'a, gtype_, dtype_>;
770
771    fn result(&self) -> Result<&Self::Outcome, anyhow::Error> {
772        self.result
773            .as_ref()
774            .ok_or_else(|| format_err!("No vibrational representation analysis results found."))
775    }
776
777    fn run(&mut self) -> Result<(), anyhow::Error> {
778        self.log_output_display();
779        self.analyse_fn_()?;
780        self.result()?.log_output_display();
781        Ok(())
782    }
783}