Skip to main content

qsym2/drivers/representation_analysis/
angular_function.rs

1//! Driver for symmetry analysis of angular functions.
2
3use anyhow::{self, ensure, format_err};
4use derive_builder::Builder;
5use nalgebra::{Point3, Vector3};
6use ndarray::{Array1, Array2};
7use num_complex::{Complex, ComplexFloat};
8use rayon::prelude::*;
9
10use crate::analysis::{EigenvalueComparisonMode, RepAnalysis};
11use crate::angmom::sh_conversion::sh_cart2rl_mat;
12use crate::angmom::spinor_rotation_3d::{SpinConstraint, SpinOrbitCoupled};
13use crate::auxiliary::atom::{Atom, ElementMap};
14use crate::auxiliary::molecule::Molecule;
15use crate::basis::ao::{
16    BasisAngularOrder, BasisAtom, BasisShell, CartOrder, PureOrder, ShellOrder, SpinorOrder,
17    SpinorParticleType, cart_tuple_to_str,
18};
19use crate::chartab::SubspaceDecomposable;
20use crate::chartab::chartab_group::CharacterProperties;
21use crate::io::format::{log_subtitle, qsym2_output};
22use crate::symmetry::symmetry_group::SymmetryGroupProperties;
23use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
24use crate::target::determinant::SlaterDeterminant;
25use crate::target::orbital::orbital_analysis::generate_det_mo_orbits;
26use crate::target::tensor::axialvector::axialvector_analysis::AxialVector3SymmetryOrbit;
27use crate::target::tensor::axialvector::{AxialVector3, TimeParity};
28
29// ==================
30// Struct definitions
31// ==================
32
33// ----------
34// Parameters
35// ----------
36
37/// Structure containing control parameters for angular function representation analysis.
38#[derive(Clone, Builder, Debug)]
39pub struct AngularFunctionRepAnalysisParams {
40    /// Threshold for checking if subspace multiplicities are integral.
41    #[builder(default = "1e-7")]
42    pub integrality_threshold: f64,
43
44    /// Threshold for determining zero eigenvalues in the orbit overlap matrix.
45    #[builder(default = "1e-7")]
46    pub linear_independence_threshold: f64,
47
48    /// Threshold for checking symmetry of the orbit overlap matrix.
49    #[builder(default = "1e-7")]
50    pub orbit_overlap_symmetry_threshold: f64,
51
52    /// The maximum angular momentum degree to be analysed.
53    #[builder(default = "2")]
54    pub max_angular_momentum: u32,
55}
56
57impl AngularFunctionRepAnalysisParams {
58    /// Returns a builder to construct a [`AngularFunctionRepAnalysisParams`] structure.
59    pub fn builder() -> AngularFunctionRepAnalysisParamsBuilder {
60        AngularFunctionRepAnalysisParamsBuilder::default()
61    }
62}
63
64impl Default for AngularFunctionRepAnalysisParams {
65    fn default() -> Self {
66        Self::builder()
67            .build()
68            .expect("Unable to build as default `AngularFunctionRepAnalysisParams`.")
69    }
70}
71
72// =========
73// Functions
74// =========
75
76/// Determines the (co)representations of a group spanned by the angular functions (spherical/solid
77/// harmonics or Cartesian functions).
78///
79/// # Arguments
80///
81/// * `group` - A symmetry group.
82/// * `params` - A parameter structure controlling the determination of angular function
83///   symmetries.
84pub(crate) fn find_angular_function_representation<G>(
85    group: &G,
86    params: &AngularFunctionRepAnalysisParams,
87) -> Result<(), anyhow::Error>
88where
89    G: SymmetryGroupProperties + Clone + Send + Sync,
90    G::CharTab: SubspaceDecomposable<f64>,
91    <<G as CharacterProperties>::CharTab as SubspaceDecomposable<f64>>::Decomposition: Send + Sync,
92{
93    let emap = ElementMap::new();
94    let thresh = group
95        .elements()
96        .clone()
97        .into_iter()
98        .next()
99        .ok_or_else(|| format_err!("No symmetry operation found."))?
100        .generating_element
101        .threshold();
102    let mol = Molecule::from_atoms(
103        &[Atom::new_ordinary("H", Point3::origin(), &emap, thresh)],
104        thresh,
105    );
106    let lmax = params.max_angular_momentum;
107
108    let (pure_symss, cart_symss) = (0..=lmax).fold(
109        (
110            Vec::with_capacity(usize::try_from(lmax)?),
111            Vec::with_capacity(usize::try_from(lmax)?),
112        ),
113        |mut acc, l| {
114            [
115                ShellOrder::Pure(PureOrder::increasingm(l)),
116                ShellOrder::Cart(CartOrder::lex(l)),
117            ]
118            .iter()
119            .for_each(|shell_order| {
120                let bao = BasisAngularOrder::new(&[BasisAtom::new(
121                    &mol.atoms[0],
122                    &[BasisShell::new(l, shell_order.clone())],
123                )]);
124                let nbas = bao.n_funcs();
125                let cs = vec![Array2::<f64>::eye(nbas)];
126                let occs = vec![Array1::<f64>::ones(nbas)];
127                let sao = match shell_order {
128                    ShellOrder::Pure(_) | ShellOrder::Spinor(_) => {
129                        Array2::<f64>::eye(bao.n_funcs())
130                    }
131                    ShellOrder::Cart(cartorder) => {
132                        let cart2rl =
133                            sh_cart2rl_mat(l, l, cartorder, true, &PureOrder::increasingm(l));
134                        cart2rl.mapv(ComplexFloat::conj).t().dot(&cart2rl)
135                    }
136                };
137
138                let mo_symmetries = SlaterDeterminant::<f64, SpinConstraint>::builder()
139                    .structure_constraint(SpinConstraint::Restricted(1))
140                    .baos(vec![&bao])
141                    .complex_symmetric(false)
142                    .mol(&mol)
143                    .coefficients(&cs)
144                    .occupations(&occs)
145                    .threshold(params.linear_independence_threshold)
146                    .build()
147                    .map_err(|err| format_err!(err))
148                    .and_then(|det| {
149                        let mos = det.to_orbitals();
150                        generate_det_mo_orbits(
151                            &det,
152                            &mos,
153                            group,
154                            &sao,
155                            None, // Is this right for complex spherical harmonics?
156                            params.integrality_threshold,
157                            params.linear_independence_threshold,
158                            params.orbit_overlap_symmetry_threshold,
159                            SymmetryTransformationKind::Spatial,
160                            EigenvalueComparisonMode::Real,
161                            true,
162                        )
163                        .map(|(_, mut mo_orbitss)| {
164                            mo_orbitss[0]
165                                .par_iter_mut()
166                                .map(|mo_orbit| {
167                                    mo_orbit.calc_xmat(false)?;
168                                    mo_orbit.analyse_rep().map_err(|err| format_err!(err))
169                                })
170                                .collect::<Vec<_>>()
171                        })
172                    });
173                match shell_order {
174                    ShellOrder::Pure(_) => acc.0.push(mo_symmetries),
175                    ShellOrder::Cart(_) => acc.1.push(mo_symmetries),
176                    ShellOrder::Spinor(_) => {
177                        panic!("Unexpected spinor shell in `find_angular_function_representation`.")
178                    }
179                }
180            });
181            acc
182        },
183    );
184
185    let pure_sym_strss = pure_symss
186        .into_iter()
187        .map(|l_pure_syms_opt| {
188            l_pure_syms_opt
189                .map(|l_pure_syms| {
190                    l_pure_syms
191                        .into_iter()
192                        .map(|sym_opt| {
193                            sym_opt
194                                .map(|sym| sym.to_string())
195                                .unwrap_or_else(|err| err.to_string())
196                        })
197                        .collect::<Vec<_>>()
198                })
199                .unwrap_or_else(|err| vec![err.to_string()])
200        })
201        .collect::<Vec<_>>();
202    let cart_sym_strss = cart_symss
203        .into_iter()
204        .map(|l_cart_syms_opt| {
205            l_cart_syms_opt
206                .map(|l_cart_syms| {
207                    l_cart_syms
208                        .into_iter()
209                        .map(|sym_opt| {
210                            sym_opt
211                                .map(|sym| sym.to_string())
212                                .unwrap_or_else(|err| err.to_string())
213                        })
214                        .collect::<Vec<_>>()
215                })
216                .unwrap_or_else(|err| vec![err.to_string()])
217        })
218        .collect::<Vec<_>>();
219    let rot_sym_strs = [Vector3::x(), Vector3::y(), Vector3::z()]
220        .into_iter()
221        .map(|v| {
222            let rv = AxialVector3::<f64>::builder()
223                .components(v)
224                .time_parity(TimeParity::Even)
225                .threshold(params.linear_independence_threshold)
226                .build()
227                .map_err(|err| format_err!(err))?;
228            let mut orbit_rv = AxialVector3SymmetryOrbit::builder()
229                .group(group)
230                .origin(&rv)
231                .integrality_threshold(params.integrality_threshold)
232                .linear_independence_threshold(params.linear_independence_threshold)
233                .orbit_overlap_symmetry_threshold(params.orbit_overlap_symmetry_threshold)
234                .symmetry_transformation_kind(SymmetryTransformationKind::Spatial)
235                .eigenvalue_comparison_mode(EigenvalueComparisonMode::Real)
236                .build()
237                .map_err(|err| format_err!(err))?;
238            let _ = orbit_rv
239                .calc_smat(None, None, true)
240                .unwrap()
241                .calc_xmat(false);
242            orbit_rv
243                .analyse_rep()
244                .map(|sym| sym.to_string())
245                .map_err(|err| format_err!(err))
246        })
247        .map(|sym| sym.unwrap_or_else(|err| err.to_string()))
248        .collect::<Vec<_>>();
249
250    let pure_sym_width = pure_sym_strss
251        .iter()
252        .flat_map(|syms| syms.iter().map(|sym| sym.chars().count()))
253        .max()
254        .unwrap_or(9)
255        .max(9);
256    let cart_sym_width = cart_sym_strss
257        .iter()
258        .flat_map(|syms| syms.iter().map(|sym| sym.chars().count()))
259        .max()
260        .unwrap_or(9)
261        .max(9);
262    let rot_sym_width = rot_sym_strs
263        .iter()
264        .map(|sym| sym.chars().count())
265        .max()
266        .unwrap_or(8)
267        .max(8);
268
269    let l_width = lmax.to_string().chars().count();
270    let pure_width = (l_width + 1).max(4);
271    let cart_width = usize::try_from(lmax)?.max(4);
272    let rot_width = 3;
273
274    log_subtitle(&format!(
275        "Space-fixed spatial angular function symmetries in {}",
276        group.finite_subgroup_name().unwrap_or(&group.name())
277    ));
278    qsym2_output!("");
279    if lmax < 1 {
280        qsym2_output!(
281            "{}",
282            "┈".repeat(l_width + pure_width + pure_sym_width + cart_width + cart_sym_width + 11)
283        );
284        qsym2_output!(
285            " {:>l_width$}  {:>pure_width$}  {:<pure_sym_width$}   {:>cart_width$}  {:<}",
286            "l",
287            "Pure",
288            "Pure sym.",
289            "Cart",
290            "Cart sym."
291        );
292        qsym2_output!(
293            "{}",
294            "┈".repeat(l_width + pure_width + pure_sym_width + cart_width + cart_sym_width + 11)
295        );
296    } else {
297        qsym2_output!(
298            "{}",
299            "┈".repeat(
300                l_width
301                    + pure_width
302                    + pure_sym_width
303                    + cart_width
304                    + cart_sym_width
305                    + rot_width
306                    + rot_sym_width
307                    + 15
308            )
309        );
310        qsym2_output!(
311            " {:>l_width$}  {:>pure_width$}  {:<pure_sym_width$}   {:>cart_width$}  {:<cart_sym_width$}  {:>rot_width$}  {:<}",
312            "l",
313            "Pure",
314            "Pure sym.",
315            "Cart",
316            "Cart sym.",
317            "Rot",
318            "Rot sym."
319        );
320        qsym2_output!(
321            "{}",
322            "┈".repeat(
323                l_width
324                    + pure_width
325                    + pure_sym_width
326                    + cart_width
327                    + cart_sym_width
328                    + rot_width
329                    + rot_sym_width
330                    + 15
331            )
332        );
333    }
334
335    let empty_str = String::new();
336    (0..=usize::try_from(lmax)?).for_each(|l| {
337        if l > 0 {
338            qsym2_output!("");
339        }
340        let n_pure = 2 * l + 1;
341        let mut i_pure = 0;
342        let mut i_rot = 0;
343
344        let l_u32 = u32::try_from(l).unwrap_or_else(|err| panic!("{err}"));
345        let cartorder = CartOrder::lex(l_u32);
346        cartorder
347            .iter()
348            .enumerate()
349            .for_each(|(i_cart, cart_tuple)| {
350                let l_str = if i_cart == 0 {
351                    format!("{l:>l_width$}")
352                } else {
353                    " ".repeat(l_width)
354                };
355
356                let pure_str = if i_pure < n_pure {
357                    let pure_str = format!(
358                        "{:>pure_width$}  {:<pure_sym_width$}",
359                        if i_pure < l {
360                            format!("-{}", i_pure.abs_diff(l))
361                        } else {
362                            format!("+{}", i_pure.abs_diff(l))
363                        },
364                        pure_sym_strss
365                            .get(l)
366                            .and_then(|l_pure_sym_strs| l_pure_sym_strs.get(i_pure))
367                            .unwrap_or(&empty_str)
368                    );
369                    i_pure += 1;
370                    pure_str
371                } else {
372                    " ".repeat(pure_width + pure_sym_width + 2)
373                };
374
375                let cart_symbol = cart_tuple_to_str(cart_tuple, true);
376                let cart_str = if l == 1 {
377                    // Rot sym to follow.
378                    format!(
379                        "{cart_symbol:>cart_width$}  {:<cart_sym_width$}",
380                        cart_sym_strss
381                            .get(l)
382                            .and_then(|l_cart_sym_strs| l_cart_sym_strs.get(i_cart))
383                            .unwrap_or(&empty_str)
384                    )
385                } else {
386                    // No rot sym to follow.
387                    format!(
388                        "{cart_symbol:>cart_width$}  {:<}",
389                        cart_sym_strss
390                            .get(l)
391                            .and_then(|l_cart_sym_strs| l_cart_sym_strs.get(i_cart))
392                            .unwrap_or(&empty_str)
393                    )
394                };
395
396                if l == 1 && i_rot < 3 {
397                    let rot_str = format!(
398                        "{:>rot_width$}  {:<}",
399                        match i_rot {
400                            0 => "Rx",
401                            1 => "Ry",
402                            2 => "Rz",
403                            _ => "--",
404                        },
405                        rot_sym_strs.get(i_rot).unwrap_or(&"--".to_string())
406                    );
407                    i_rot += 1;
408                    qsym2_output!(" {l_str}  {pure_str}   {cart_str}  {rot_str}");
409                } else {
410                    qsym2_output!(" {l_str}  {pure_str}   {cart_str}");
411                }
412            });
413    });
414    if lmax < 1 {
415        qsym2_output!(
416            "{}",
417            "┈".repeat(l_width + pure_width + pure_sym_width + cart_width + cart_sym_width + 11)
418        );
419    } else {
420        qsym2_output!(
421            "{}",
422            "┈".repeat(
423                l_width
424                    + pure_width
425                    + pure_sym_width
426                    + cart_width
427                    + cart_sym_width
428                    + rot_width
429                    + rot_sym_width
430                    + 15
431            )
432        );
433    }
434    qsym2_output!("");
435
436    Ok(())
437}
438
439/// Determines the (co)representations of a group spanned by the spinor functions.
440///
441/// # Arguments
442///
443/// * `group` - A symmetry group.
444/// * `params` - A parameter structure controlling the determination of spinor function
445///   symmetries.
446pub(crate) fn find_spinor_function_representation<G>(
447    group: &G,
448    params: &AngularFunctionRepAnalysisParams,
449) -> Result<(), anyhow::Error>
450where
451    G: SymmetryGroupProperties + Clone + Send + Sync,
452    G::CharTab: SubspaceDecomposable<Complex<f64>>,
453    <<G as CharacterProperties>::CharTab as SubspaceDecomposable<Complex<f64>>>::Decomposition:
454        Send + Sync,
455{
456    ensure!(
457        group.is_double_group(),
458        "The specified group is not a double group."
459    );
460
461    let emap = ElementMap::new();
462    let thresh = group
463        .elements()
464        .clone()
465        .into_iter()
466        .next()
467        .ok_or_else(|| format_err!("No symmetry operation found."))?
468        .generating_element
469        .threshold();
470    let mol = Molecule::from_atoms(
471        &[Atom::new_ordinary("H", Point3::origin(), &emap, thresh)],
472        thresh,
473    );
474    let lmax = params.max_angular_momentum;
475
476    let spinor_symss = (1..2 * lmax).step_by(2).fold(
477        Vec::with_capacity(2 * usize::try_from(lmax)?),
478        |mut acc, two_j| {
479            let even_first = two_j.rem_euclid(4) == 1;
480            let shell_order_g = ShellOrder::Spinor(SpinorOrder::increasingm(
481                two_j,
482                even_first,
483                SpinorParticleType::Fermion(None),
484            ));
485            let shell_order_u = ShellOrder::Spinor(SpinorOrder::increasingm(
486                two_j,
487                !even_first,
488                SpinorParticleType::Fermion(None),
489            ));
490            let bao_g = BasisAngularOrder::new(&[BasisAtom::new(
491                &mol.atoms[0],
492                &[BasisShell::new(two_j, shell_order_g)],
493            )]);
494            let bao_u = BasisAngularOrder::new(&[BasisAtom::new(
495                &mol.atoms[0],
496                &[BasisShell::new(two_j, shell_order_u)],
497            )]);
498            let nbas = bao_g.n_funcs();
499            let cs = vec![Array2::<Complex<f64>>::eye(nbas)];
500            let occs = vec![Array1::<f64>::ones(nbas)];
501            let sao = Array2::<Complex<f64>>::eye(bao_g.n_funcs());
502
503            for bao in [bao_g, bao_u] {
504                let mo_symmetries = SlaterDeterminant::<Complex<f64>, SpinOrbitCoupled>::builder()
505                    .structure_constraint(SpinOrbitCoupled::JAdapted(1))
506                    .baos(vec![&bao])
507                    .complex_symmetric(false)
508                    .mol(&mol)
509                    .coefficients(&cs)
510                    .occupations(&occs)
511                    .threshold(params.linear_independence_threshold)
512                    .build()
513                    .map_err(|err| format_err!(err))
514                    .and_then(|det| {
515                        let mos = det.to_orbitals();
516                        generate_det_mo_orbits(
517                            &det,
518                            &mos,
519                            group,
520                            &sao,
521                            None, // Is this right for complex spherical harmonics?
522                            params.integrality_threshold,
523                            params.linear_independence_threshold,
524                            params.orbit_overlap_symmetry_threshold,
525                            SymmetryTransformationKind::SpinSpatial,
526                            EigenvalueComparisonMode::Modulus,
527                            true,
528                        )
529                        .map(|(_, mut mo_orbitss)| {
530                            mo_orbitss[0]
531                                .par_iter_mut()
532                                .map(|mo_orbit| {
533                                    mo_orbit.calc_xmat(false)?;
534                                    mo_orbit.analyse_rep().map_err(|err| format_err!(err))
535                                })
536                                .collect::<Vec<_>>()
537                        })
538                    });
539                acc.push(mo_symmetries);
540            }
541            acc
542        },
543    );
544
545    let spinor_sym_strss = spinor_symss
546        .into_iter()
547        .map(|l_spinor_syms_opt| {
548            l_spinor_syms_opt
549                .map(|l_spinor_syms| {
550                    l_spinor_syms
551                        .into_iter()
552                        .map(|sym_opt| {
553                            sym_opt
554                                .map(|sym| sym.to_string())
555                                .unwrap_or_else(|err| err.to_string())
556                        })
557                        .collect::<Vec<_>>()
558                })
559                .unwrap_or_else(|err| vec![err.to_string()])
560        })
561        .collect::<Vec<_>>();
562
563    let spinor_sym_width = spinor_sym_strss
564        .iter()
565        .flat_map(|syms| syms.iter().map(|sym| sym.chars().count()))
566        .max()
567        .unwrap_or(11)
568        .max(11);
569
570    let j_width = format!("{}/2", 2 * lmax - 1).chars().count();
571    let l_width = format!("(+; l = {lmax})").chars().count();
572    let jl_width = j_width + l_width + 1;
573    let mj_width = (j_width + 1).max(6);
574
575    log_subtitle(&format!(
576        "Space-fixed spinor function symmetries in {}",
577        group.finite_subgroup_name().unwrap_or(&group.name())
578    ));
579    qsym2_output!("");
580    qsym2_output!("{}", "┈".repeat(jl_width + mj_width + spinor_sym_width + 6));
581    qsym2_output!(
582        " {:>jl_width$}  {:>mj_width$}  {:<}",
583        "j",
584        "Spinor",
585        "Spinor sym.",
586    );
587    qsym2_output!("{}", "┈".repeat(jl_width + mj_width + spinor_sym_width + 6));
588
589    let empty_str = String::new();
590    (1..usize::try_from(2 * lmax)?)
591        .step_by(2)
592        .enumerate()
593        .for_each(|(i_two_j, two_j)| {
594            if two_j > 1 {
595                qsym2_output!("");
596            }
597            let n_spinor = two_j + 1;
598
599            let even_first = two_j.rem_euclid(4) == 1;
600            let two_j_u32 = u32::try_from(two_j).unwrap_or_else(|err| panic!("{err}"));
601            let spinororder_g =
602                SpinorOrder::increasingm(two_j_u32, even_first, SpinorParticleType::Fermion(None));
603            spinororder_g
604                .iter()
605                .enumerate()
606                .for_each(|(i_spinor, two_mj)| {
607                    let j_str = if i_spinor == 0 {
608                        let j_str_temp = format!(
609                            "{two_j}/2 ({}; l = {})",
610                            if spinororder_g.a() == 1 { "+" } else { "-" },
611                            spinororder_g.l()
612                        );
613                        format!("{j_str_temp:>jl_width$}")
614                    } else {
615                        " ".repeat(jl_width)
616                    };
617
618                    let spinor_str = if i_spinor < n_spinor {
619                        let spinor_str_temp = format!("{two_mj:+}/2");
620                        let spinor_str = format!(
621                            "{spinor_str_temp:>mj_width$}  {:<}",
622                            spinor_sym_strss
623                                .get(i_two_j * 2)
624                                .and_then(|l_spinor_sym_strs| l_spinor_sym_strs.get(i_spinor))
625                                .unwrap_or(&empty_str)
626                        );
627                        spinor_str
628                    } else {
629                        " ".repeat(mj_width + spinor_sym_width + 2)
630                    };
631
632                    qsym2_output!(" {j_str}  {spinor_str}");
633                });
634
635            qsym2_output!("");
636
637            let spinororder_u =
638                SpinorOrder::increasingm(two_j_u32, !even_first, SpinorParticleType::Fermion(None));
639            spinororder_u
640                .iter()
641                .enumerate()
642                .for_each(|(i_spinor, two_mj)| {
643                    let j_str = if i_spinor == 0 {
644                        let j_str_temp = format!(
645                            "{two_j}/2 ({}; l = {})",
646                            if spinororder_u.a() == 1 { "+" } else { "-" },
647                            spinororder_u.l()
648                        );
649                        format!("{j_str_temp:>jl_width$}")
650                    } else {
651                        " ".repeat(jl_width)
652                    };
653
654                    let spinor_str = if i_spinor < n_spinor {
655                        let spinor_str_temp = format!("{two_mj:+}/2");
656                        let spinor_str = format!(
657                            "{spinor_str_temp:>mj_width$}  {:<}",
658                            spinor_sym_strss
659                                .get(i_two_j * 2 + 1)
660                                .and_then(|l_spinor_sym_strs| l_spinor_sym_strs.get(i_spinor))
661                                .unwrap_or(&empty_str)
662                        );
663                        spinor_str
664                    } else {
665                        " ".repeat(mj_width + spinor_sym_width + 2)
666                    };
667
668                    qsym2_output!(" {j_str}  {spinor_str}");
669                });
670        });
671    qsym2_output!("{}", "┈".repeat(jl_width + mj_width + spinor_sym_width + 6));
672    qsym2_output!("");
673
674    Ok(())
675}