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