1use 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 = "1e-7")]
50 pub orbit_overlap_symmetry_threshold: f64,
51
52 #[builder(default = "2")]
54 pub max_angular_momentum: u32,
55}
56
57impl AngularFunctionRepAnalysisParams {
58 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
72pub(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, 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 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 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
439pub(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, 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}