1use anyhow::{self, ensure};
4use counter::Counter;
5use indexmap::IndexMap;
6use itertools::Itertools;
7use lazy_static::lazy_static;
8use nalgebra::Vector3;
9use ordered_float::OrderedFloat;
10use regex::Regex;
11
12use crate::auxiliary::geometry::{self, PositiveHemisphere};
13use crate::chartab::chartab_group::{
14 CharacterProperties, IrcorepCharTabConstruction, IrrepCharTabConstruction,
15};
16use crate::chartab::chartab_symbols::CollectionSymbol;
17use crate::chartab::{CharacterTable, RepCharacterTable};
18use crate::group::class::ClassProperties;
19use crate::group::{GroupProperties, GroupType, MagneticRepresentedGroup, UnitaryRepresentedGroup};
20use crate::symmetry::symmetry_core::Symmetry;
21use crate::symmetry::symmetry_element::symmetry_operation::{
22 sort_operations, SpecialSymmetryTransformation,
23};
24use crate::symmetry::symmetry_element::{SymmetryOperation, SIG};
25use crate::symmetry::symmetry_symbols::{
26 deduce_mulliken_irrep_symbols, deduce_principal_classes, sort_irreps, MullikenIrcorepSymbol,
27 MullikenIrrepSymbol, SymmetryClassSymbol, FORCED_C3_PRINCIPAL_GROUPS,
28};
29
30#[cfg(test)]
31#[path = "symmetry_group_tests.rs"]
32mod symmetry_group_tests;
33
34#[cfg(test)]
35#[path = "symmetry_chartab_tests.rs"]
36mod symmetry_chartab_tests;
37
38lazy_static! {
39 static ref SQUARE_BRACKETED_GROUP_NAME_RE: Regex =
40 Regex::new(r"u\[(.+)\]").expect("Regex pattern invalid.");
41}
42
43pub type UnitaryRepresentedSymmetryGroup = UnitaryRepresentedGroup<
50 SymmetryOperation,
51 MullikenIrrepSymbol,
52 SymmetryClassSymbol<SymmetryOperation>,
53>;
54
55pub type MagneticRepresentedSymmetryGroup = MagneticRepresentedGroup<
59 SymmetryOperation,
60 UnitaryRepresentedSymmetryGroup,
61 MullikenIrcorepSymbol,
62>;
63
64pub trait SymmetryGroupProperties:
70 ClassProperties<
71 GroupElement = SymmetryOperation,
72 ClassSymbol = SymmetryClassSymbol<SymmetryOperation>,
73 > + CharacterProperties
74 + Sized
75{
76 fn from_molecular_symmetry(
93 sym: &Symmetry,
94 infinite_order_to_finite: Option<u32>,
95 ) -> Result<Self, anyhow::Error>;
96
97 fn to_double_group(&self) -> Result<Self, anyhow::Error>;
103
104 fn canonicalise_character_table(&mut self) {}
110
111 fn deduce_finite_group_name(&mut self) -> String {
118 let (full_name, full_order, grey, double) = if let Some((_, [grp_name])) =
119 SQUARE_BRACKETED_GROUP_NAME_RE
120 .captures(&self.name())
121 .map(|caps| caps.extract())
122 {
123 (
128 grp_name.to_string(),
129 self.order() * 2,
130 true,
131 grp_name.contains('*'),
132 )
133 } else {
134 (self.name(), self.order(), false, self.name().contains('*'))
135 };
136 let finite_group = if full_name.contains('∞') {
137 if full_name.as_bytes()[0] == b'D' {
139 if matches!(
140 full_name
141 .as_bytes()
142 .iter()
143 .last()
144 .expect("The last character in the group name cannot be retrieved."),
145 b'h' | b'd'
146 ) {
147 if full_name.contains('θ') {
148 assert_eq!(full_order % 8, 0);
149 full_name.replace('∞', format!("{}", full_order / 8).as_str())
150 } else {
151 assert_eq!(full_order % 4, 0);
152 full_name.replace('∞', format!("{}", full_order / 4).as_str())
153 }
154 } else if full_name.contains('θ') {
155 assert_eq!(full_order % 4, 0);
156 full_name.replace('∞', format!("{}", full_order / 4).as_str())
157 } else {
158 assert_eq!(full_order % 2, 0);
159 full_name.replace('∞', format!("{}", full_order / 2).as_str())
160 }
161 } else {
162 assert!(matches!(full_name.as_bytes()[0], b'C' | b'S'));
163 if matches!(
164 full_name
165 .as_bytes()
166 .iter()
167 .last()
168 .expect("The last character in the group name cannot be retrieved."),
169 b'h' | b'v'
170 ) {
171 if full_name.contains('θ') {
172 assert_eq!(
173 full_order % 4,
174 0,
175 "Unexpected order {} for group {full_name}.",
176 full_order
177 );
178 } else {
179 assert_eq!(
180 full_order % 2,
181 0,
182 "Unexpected order {} for group {full_name}.",
183 full_order
184 );
185 }
186 if full_order > 2 {
187 if full_name.contains('θ') {
188 full_name.replace('∞', format!("{}", full_order / 4).as_str())
189 } else {
190 full_name.replace('∞', format!("{}", full_order / 2).as_str())
191 }
192 } else {
193 assert_eq!(full_name.as_bytes()[0], b'C');
194 "Cs".to_string()
195 }
196 } else {
197 full_name.replace('∞', format!("{}", full_order).as_str())
198 }
199 }
200 } else if full_name.contains("O(3)") {
201 match (full_order, double) {
203 (8, false) => "D2h".to_string(),
204 (16, true) => "D2h*".to_string(),
205 (16, false) => "D2h + θ·D2h".to_string(),
206 (32, true) => "(D2h + θ·D2h)*".to_string(),
207 (48, false) => "Oh".to_string(),
208 (96, true) => "Oh*".to_string(),
209 (96, false) => "Oh + θ·Oh".to_string(),
210 (192, true) => "(Oh + θ·Oh)*".to_string(),
211 _ => panic!("Unsupported number of group elements ({full_order}) for a finite group of {full_name}."),
212 }
213 } else {
214 full_name
216 };
217 if grey {
218 format!("u[{finite_group}]")
219 } else {
220 finite_group
221 }
222 }
223
224 fn all_unitary(&self) -> bool {
226 self.elements()
227 .clone()
228 .into_iter()
229 .all(|op| !op.contains_time_reversal())
230 }
231
232 fn is_double_group(&self) -> bool {
239 if self.elements().clone().into_iter().all(|op| op.is_su2()) {
240 true
241 } else if self.elements().clone().into_iter().all(|op| !op.is_su2()) {
242 false
243 } else {
244 panic!("Mixed SU(2) and SO(3) proper rotations are not allowed.");
245 }
246 }
247
248 fn group_type(&self) -> GroupType {
251 let double = self.is_double_group();
252 if self.all_unitary() {
253 GroupType::Ordinary(double)
254 } else if self
255 .elements()
256 .clone()
257 .into_iter()
258 .any(|op| op.is_time_reversal())
259 {
260 GroupType::MagneticGrey(double)
261 } else {
262 GroupType::MagneticBlackWhite(double)
263 }
264 }
265
266 fn class_symbols_from_symmetry(&mut self) -> Vec<SymmetryClassSymbol<SymmetryOperation>> {
268 log::debug!("Assigning class symbols from symmetry operations...");
269 let mut undashed_class_symbols: Counter<SymmetryClassSymbol<SymmetryOperation>, usize> =
270 Counter::new();
271
272 let symmetry_class_symbols = (0..self.class_number())
273 .map(|i| {
274 let old_symbol = self.get_cc_symbol_of_index(i).unwrap_or_else(|| {
275 panic!("No symmetry symbol for class index `{i}` can be found.")
276 });
277 let rep_ele_index = *self
278 .get_cc_index(i)
279 .unwrap_or_else(|| panic!("No conjugacy class index `{i}` can be found."))
280 .iter()
281 .min_by_key(|&&j| {
282 let op = self
283 .get_index(j)
284 .unwrap_or_else(|| {
285 panic!("Element with index {j} cannot be retrieved.")
286 })
287 .to_symmetry_element();
288 (
289 op.is_su2_class_1(), !geometry::check_standard_positive_pole(
291 &op.proper_rotation_pole(),
292 op.threshold(),
293 ), op.proper_fraction()
295 .map(|frac| {
296 *frac.numer().expect(
297 "The numerator of the proper fraction cannot be retrieved.",
298 )
299 })
300 .or_else(|| op.raw_proper_power().map(|pp| pp.unsigned_abs()))
301 .expect(
302 "No angle information for the proper rotation can be found.",
303 ), )
305 })
306 .expect("Unable to obtain a representative element index.");
307 let rep_ele = self.get_index(rep_ele_index).unwrap_or_else(|| {
308 panic!("Unable to retrieve group element with index `{rep_ele_index}`.")
309 });
310
311 let su2 = if rep_ele.is_su2_class_1() {
312 "(QΣ)"
313 } else if rep_ele.is_su2() {
314 "(Σ)"
315 } else {
316 ""
317 };
318 if rep_ele.is_identity() {
319 let id_sym = SymmetryClassSymbol::new(
321 format!("1||E{su2}||").as_str(),
322 Some(vec![rep_ele]),
323 )
324 .unwrap_or_else(|_| {
325 panic!("Unable to construct a class symbol from `1||E{su2}||`.")
326 });
327 assert!(undashed_class_symbols.insert(id_sym.clone(), 1).is_none());
328 id_sym
329 } else if rep_ele.is_inversion() {
330 let inv_sym = SymmetryClassSymbol::new(
332 format!("1||i{su2}||").as_str(),
333 Some(vec![rep_ele]),
334 )
335 .unwrap_or_else(|_| {
336 panic!("Unable to construct a class symbol from `1||i{su2}||`.")
337 });
338 assert!(undashed_class_symbols.insert(inv_sym.clone(), 1).is_none());
339 inv_sym
340 } else if rep_ele.is_time_reversal() {
341 let trev_sym = SymmetryClassSymbol::new(
343 format!("1||θ{su2}||").as_str(),
344 Some(vec![rep_ele]),
345 )
346 .unwrap_or_else(|_| {
347 panic!("Unable to construct a class symbol from `1||θ{su2}||`.")
348 });
349 assert!(undashed_class_symbols.insert(trev_sym.clone(), 1).is_none());
350 trev_sym
351 } else {
352 let (mult, main_symbol, reps) = if rep_ele.is_su2() && !rep_ele.is_su2_class_1()
353 {
354 let class_1_count = self
356 .get_cc_index(i)
357 .unwrap_or_else(|| {
358 panic!("No conjugacy class index `{i}` can be found.")
359 })
360 .iter()
361 .filter(|&j| {
362 let op = self.get_index(*j).unwrap_or_else(|| {
363 panic!("Element with index {j} cannot be retrieved.")
364 });
365 op.is_su2_class_1()
366 })
367 .count();
368 if old_symbol.size().rem_euclid(2) == 0
369 && class_1_count == old_symbol.size().div_euclid(2)
370 {
371 let class_1_rep_ele = self
374 .get_cc_index(i)
375 .unwrap_or_else(|| {
376 panic!("No conjugacy class index `{i}` can be found.")
377 })
378 .iter()
379 .find_map(|&j| {
380 let op = self.get_index(j).unwrap_or_else(|| {
381 panic!("Element with index {j} cannot be retrieved.")
382 });
383 if op.is_su2_class_1() {
384 Some(op)
385 } else {
386 None
387 }
388 })
389 .expect("Unable to find a class-1 element in this class.");
390 (
391 old_symbol.size().div_euclid(2),
392 format!(
393 "{}, {}",
394 rep_ele.get_abbreviated_symbol(),
395 class_1_rep_ele.get_abbreviated_symbol()
396 ),
397 vec![rep_ele, class_1_rep_ele],
398 )
399 } else if class_1_count > 0 {
400 let ops = self
403 .get_cc_index(i)
404 .unwrap_or_else(|| {
405 panic!("No conjugacy class index `{i}` can be found.")
406 })
407 .iter()
408 .map(|&j| {
409 self.get_index(j).unwrap_or_else(|| {
410 panic!("Element with index {j} cannot be retrieved.")
411 })
412 })
413 .collect_vec();
414 (
415 1,
416 ops.iter().map(|op| op.get_abbreviated_symbol()).join(", "),
417 ops,
418 )
419 } else {
420 (
422 old_symbol.size(),
423 rep_ele.get_abbreviated_symbol(),
424 vec![rep_ele],
425 )
426 }
427 } else {
428 (
430 old_symbol.size(),
431 rep_ele.get_abbreviated_symbol(),
432 vec![rep_ele],
433 )
434 };
435 let undashed_sym =
436 SymmetryClassSymbol::new(format!("1||{main_symbol}||").as_str(), None)
437 .unwrap_or_else(|_| {
438 panic!(
439 "Unable to construct a coarse class symbol from `1||{main_symbol}||`"
440 )
441 });
442 undashed_class_symbols
443 .entry(undashed_sym.clone())
444 .and_modify(|counter| *counter += 1)
445 .or_insert(1);
446 let dash = undashed_class_symbols
447 .get(&undashed_sym)
448 .map(|&counter| "'".repeat(counter - 1))
449 .unwrap();
450
451 SymmetryClassSymbol::new(
452 format!("{mult}||{main_symbol}|^({dash})|",).as_str(),
453 Some(reps),
454 )
455 .unwrap_or_else(|_| {
456 panic!(
457 "Unable to construct a class symbol from `{mult}||{main_symbol}|^({dash})|`"
458 )
459 })
460 }
461 })
462 .collect::<Vec<_>>();
463 log::debug!("Assigning class symbols from symmetry operations... Done.");
464 symmetry_class_symbols
465 }
466}
467
468impl SymmetryGroupProperties
477 for UnitaryRepresentedGroup<
478 SymmetryOperation,
479 MullikenIrrepSymbol,
480 SymmetryClassSymbol<SymmetryOperation>,
481 >
482{
483 #[allow(clippy::too_many_lines)]
496 fn from_molecular_symmetry(
497 sym: &Symmetry,
498 infinite_order_to_finite: Option<u32>,
499 ) -> Result<Self, anyhow::Error> {
500 let group_name = sym
501 .group_name
502 .as_ref()
503 .expect("No point groups found.")
504 .clone();
505
506 let handles_infinite_group = if sym.is_infinite() {
507 assert!(
508 infinite_order_to_finite.is_some(),
509 "No finite orders specified for an infinite-order group."
510 );
511 infinite_order_to_finite
512 } else {
513 None
514 };
515
516 let sorted_operations = sym.generate_all_operations(infinite_order_to_finite);
517
518 let mut group = Self::new(group_name.as_str(), sorted_operations)?;
519 if handles_infinite_group.is_some() {
520 let finite_subgroup_name = group.deduce_finite_group_name();
521 group.set_finite_subgroup_name(Some(finite_subgroup_name));
522 }
523 let symbols = group.class_symbols_from_symmetry();
524 group.set_class_symbols(&symbols);
525 group.construct_irrep_character_table();
526 group.canonicalise_character_table();
527 Ok(group)
528 }
529
530 fn to_double_group(&self) -> Result<Self, anyhow::Error> {
536 log::debug!(
537 "Constructing the double group for unitary-represented {}...",
538 self.name()
539 );
540
541 let poshem = find_positive_hemisphere(self);
543
544 if let Some(pos_hem) = poshem.as_ref() {
545 log::debug!("New positive hemisphere:");
546 log::debug!("{pos_hem}");
547 }
548
549 let mut su2_operations = self
550 .elements()
551 .clone()
552 .into_iter()
553 .map(|op| {
554 let mut su2_op = op.to_su2_class_0();
555 su2_op.set_positive_hemisphere(poshem.as_ref());
556 su2_op
557 })
558 .collect_vec();
559 let q_identity = SymmetryOperation::from_quaternion(
560 (-1.0, -Vector3::z()),
561 true,
562 su2_operations[0].generating_element.threshold(),
563 1,
564 false,
565 true,
566 poshem,
567 );
568 let su2_1_operations = su2_operations
569 .iter()
570 .map(|op| {
571 let mut q_op = op * &q_identity;
572 if !q_op.is_proper() {
573 q_op = q_op.convert_to_improper_kind(&SIG);
574 }
575
576 q_op.generating_element.additional_subscript =
579 op.generating_element.additional_subscript.clone();
580 q_op.generating_element.additional_superscript =
581 op.generating_element.additional_superscript.clone();
582 q_op
583 })
584 .collect_vec();
585 su2_operations.extend(su2_1_operations.into_iter());
586 sort_operations(&mut su2_operations);
587
588 let group_name = if self.name().contains('+') {
589 format!("({})*", self.name())
590 } else {
591 self.name() + "*"
592 };
593 let finite_group_name = self.finite_subgroup_name().map(|name| {
594 if name.contains('+') {
595 format!("({name})*")
596 } else {
597 name.clone() + "*"
598 }
599 });
600 let mut group = Self::new(group_name.as_str(), su2_operations)?;
601 group.set_finite_subgroup_name(finite_group_name);
602 let symbols = group.class_symbols_from_symmetry();
603 group.set_class_symbols(&symbols);
604 group.construct_irrep_character_table();
605 group.canonicalise_character_table();
606 Ok(group)
607 }
608
609 fn canonicalise_character_table(&mut self) {
612 let old_chartab = self.character_table();
613 let class_symbols: IndexMap<_, _> = (0..self.class_number())
614 .map(|i| (self.get_cc_symbol_of_index(i).unwrap(), i))
615 .collect();
616
617 let su2_0 = if self.is_double_group() { "(Σ)" } else { "" };
618 let i_cc = SymmetryClassSymbol::new(format!("1||i{su2_0}||").as_str(), None)
619 .unwrap_or_else(|_| panic!("Unable to construct a class symbol from `1||i{su2_0}||`."));
620 let s_cc = SymmetryClassSymbol::new(format!("1||σh{su2_0}||").as_str(), None)
621 .unwrap_or_else(|_| {
622 panic!("Unable to construct a class symbol from `1||σh{su2_0}||`.")
623 });
624 let s2_cc = SymmetryClassSymbol::new("1||σh(Σ), σh(QΣ)||".to_string().as_str(), None)
625 .unwrap_or_else(|_| {
626 panic!("Unable to construct a class symbol from `1||σh(Σ), σh(QΣ)||`.")
627 });
628 let ts_cc = SymmetryClassSymbol::new(format!("1||θ·σh{su2_0}||").as_str(), None)
629 .unwrap_or_else(|_| panic!("Unable to construct class symbol `1||θ·σh{su2_0}||`."));
630
631 let force_principal = if FORCED_C3_PRINCIPAL_GROUPS.contains(&self.name())
632 || FORCED_C3_PRINCIPAL_GROUPS.contains(
633 self.finite_subgroup_name()
634 .unwrap_or(&String::new())
635 .as_str(),
636 ) {
637 let c3_cc: SymmetryClassSymbol<SymmetryOperation> =
638 SymmetryClassSymbol::new(format!("8||C3{su2_0}||").as_str(), None).unwrap_or_else(
639 |_| panic!("Unable to construct a class symbol from `8||C3{su2_0}||`."),
640 );
641 log::debug!(
642 "Group is {}. Principal-axis classes will be forced to be {}. This is to obtain non-standard Mulliken symbols that are in line with conventions in the literature.",
643 self.name(),
644 c3_cc
645 );
646 Some(c3_cc)
647 } else {
648 None
649 };
650
651 let principal_classes = if force_principal.is_some() {
652 deduce_principal_classes(
653 &class_symbols,
654 None::<fn(&SymmetryClassSymbol<SymmetryOperation>) -> bool>,
655 force_principal,
656 )
657 } else if class_symbols.contains_key(&i_cc) {
658 log::debug!(
659 "Inversion centre exists. Principal-axis classes will be forced to be proper."
660 );
661 deduce_principal_classes(
662 &class_symbols,
663 Some(|cc: &SymmetryClassSymbol<SymmetryOperation>| {
664 cc.is_proper() && !cc.contains_time_reversal()
665 }),
666 None,
667 )
668 } else if class_symbols.contains_key(&s_cc) || class_symbols.contains_key(&s2_cc) {
669 log::debug!(
670 "Horizontal mirror plane exists. Principal-axis classes will be forced to be proper."
671 );
672 deduce_principal_classes(
673 &class_symbols,
674 Some(|cc: &SymmetryClassSymbol<SymmetryOperation>| {
675 cc.is_proper() && !cc.contains_time_reversal()
676 }),
677 None,
678 )
679 } else if class_symbols.contains_key(&ts_cc) {
680 log::debug!(
681 "Time-reversed horizontal mirror plane exists. Principal-axis classes will be forced to be proper."
682 );
683 deduce_principal_classes(
684 &class_symbols,
685 Some(|cc: &SymmetryClassSymbol<SymmetryOperation>| {
686 cc.is_proper() && !cc.contains_time_reversal()
687 }),
688 None,
689 )
690 } else if !self
691 .elements()
692 .iter()
693 .all(|op| !op.contains_time_reversal())
694 {
695 log::debug!(
696 "Antiunitary elements exist without any inversion centres or horizonal mirror planes. Principal-axis classes will be forced to be unitary."
697 );
698 deduce_principal_classes(
699 &class_symbols,
700 Some(|cc: &SymmetryClassSymbol<SymmetryOperation>| !cc.contains_time_reversal()),
701 None,
702 )
703 } else {
704 deduce_principal_classes(
705 &class_symbols,
706 None::<fn(&SymmetryClassSymbol<SymmetryOperation>) -> bool>,
707 None,
708 )
709 };
710
711 let (char_arr, sorted_fs) = sort_irreps(
712 &old_chartab.array().view(),
713 &old_chartab.frobenius_schurs.values().copied().collect_vec(),
714 &class_symbols,
715 &principal_classes,
716 );
717
718 let ordered_irreps =
719 deduce_mulliken_irrep_symbols(&char_arr.view(), &class_symbols, &principal_classes);
720
721 self.irrep_character_table = Some(RepCharacterTable::new(
722 &old_chartab.name,
723 &ordered_irreps,
724 &class_symbols.keys().cloned().collect::<Vec<_>>(),
725 &principal_classes,
726 char_arr,
727 &sorted_fs,
728 ));
729 }
730}
731
732impl SymmetryGroupProperties
737 for MagneticRepresentedGroup<
738 SymmetryOperation,
739 UnitaryRepresentedGroup<
740 SymmetryOperation,
741 MullikenIrrepSymbol,
742 SymmetryClassSymbol<SymmetryOperation>,
743 >,
744 MullikenIrcorepSymbol,
745 >
746{
747 #[allow(clippy::too_many_lines)]
764 fn from_molecular_symmetry(
765 sym: &Symmetry,
766 infinite_order_to_finite: Option<u32>,
767 ) -> Result<Self, anyhow::Error> {
768 let group_name = sym
769 .group_name
770 .as_ref()
771 .expect("No point groups found.")
772 .clone();
773
774 let handles_infinite_group = if sym.is_infinite() {
775 assert!(
776 infinite_order_to_finite.is_some(),
777 "No finite orders specified for an infinite-order group."
778 );
779 infinite_order_to_finite
780 } else {
781 None
782 };
783
784 let sorted_operations = sym.generate_all_operations(infinite_order_to_finite);
785
786 ensure!(
787 sorted_operations
788 .iter()
789 .any(SpecialSymmetryTransformation::contains_time_reversal),
790 "A magnetic-represented group is requested, but no antiunitary operations can be found. \
791 Ensure that time reversal is considered during symmetry-group detection."
792 );
793
794 log::debug!("Constructing the unitary subgroup for the magnetic group...");
795 let unitary_operations = sorted_operations
796 .iter()
797 .filter_map(|op| {
798 if op.contains_time_reversal() {
799 None
800 } else {
801 Some(op.clone())
802 }
803 })
804 .collect::<Vec<_>>();
805 let mut unitary_subgroup = UnitaryRepresentedGroup::<
806 SymmetryOperation,
807 MullikenIrrepSymbol,
808 SymmetryClassSymbol<SymmetryOperation>,
809 >::new(&format!("u[{group_name}]"), unitary_operations)?;
810 let uni_symbols = unitary_subgroup.class_symbols_from_symmetry();
811 unitary_subgroup.set_class_symbols(&uni_symbols);
812 if handles_infinite_group.is_some() {
813 let finite_subgroup_name = unitary_subgroup.deduce_finite_group_name();
814 unitary_subgroup.set_finite_subgroup_name(Some(finite_subgroup_name));
815 }
816 unitary_subgroup.construct_irrep_character_table();
817 unitary_subgroup.canonicalise_character_table();
818 log::debug!("Constructing the unitary subgroup for the magnetic group... Done.");
819
820 log::debug!("Constructing the magnetic group...");
821 let mut group = Self::new(group_name.as_str(), sorted_operations, unitary_subgroup)?;
822 if handles_infinite_group.is_some() {
823 let finite_subgroup_name = group.deduce_finite_group_name();
824 group.set_finite_subgroup_name(Some(finite_subgroup_name));
825 }
826 let symbols = group.class_symbols_from_symmetry();
827 group.set_class_symbols(&symbols);
828 group.construct_ircorep_character_table();
829 log::debug!("Constructing the magnetic group... Done.");
830 Ok(group)
831 }
832
833 fn to_double_group(&self) -> Result<Self, anyhow::Error> {
845 log::debug!(
846 "Constructing the double group for magnetic-represented {}...",
847 self.name()
848 );
849
850 let poshem = find_positive_hemisphere(self);
852
853 if let Some(pos_hem) = poshem.as_ref() {
854 log::debug!("New positive hemisphere:");
855 log::debug!("{pos_hem}");
856 }
857
858 let mut su2_operations = self
859 .elements()
860 .clone()
861 .into_iter()
862 .map(|op| {
863 let mut su2_op = op.to_su2_class_0();
864 su2_op.set_positive_hemisphere(poshem.as_ref());
865 su2_op
866 })
867 .collect_vec();
868 let q_identity = SymmetryOperation::from_quaternion(
869 (-1.0, -Vector3::z()),
870 true,
871 su2_operations[0].generating_element.threshold(),
872 1,
873 false,
874 true,
875 poshem,
876 );
877 let su2_1_operations = su2_operations
878 .iter()
879 .map(|op| {
880 let mut q_op = op * &q_identity;
881 if !q_op.is_proper() {
882 q_op = q_op.convert_to_improper_kind(&SIG);
883 }
884
885 q_op.generating_element.additional_subscript =
888 op.generating_element.additional_subscript.clone();
889 q_op.generating_element.additional_superscript =
890 op.generating_element.additional_superscript.clone();
891 q_op
892 })
893 .collect_vec();
894 su2_operations.extend(su2_1_operations.into_iter());
895 sort_operations(&mut su2_operations);
896
897 let group_name = if self.name().contains('+') {
898 format!("({})*", self.name())
899 } else {
900 self.name() + "*"
901 };
902 let finite_group_name = self.finite_subgroup_name().map(|name| {
903 if name.contains('+') {
904 format!("({name})*")
905 } else {
906 name.clone() + "*"
907 }
908 });
909
910 log::debug!("Constructing the double unitary subgroup for the magnetic double group...");
911 let unitary_su2_operations = su2_operations
912 .iter()
913 .filter_map(|op| {
914 if op.contains_time_reversal() {
915 None
916 } else {
917 Some(op.clone())
918 }
919 })
920 .collect::<Vec<_>>();
921 let mut double_unitary_subgroup =
922 UnitaryRepresentedGroup::<
923 SymmetryOperation,
924 MullikenIrrepSymbol,
925 SymmetryClassSymbol<SymmetryOperation>,
926 >::new(&format!("u[{group_name}]"), unitary_su2_operations)?;
927 let uni_symbols = double_unitary_subgroup.class_symbols_from_symmetry();
928 double_unitary_subgroup.set_class_symbols(&uni_symbols);
929 if double_unitary_subgroup.name().contains('∞')
930 || double_unitary_subgroup.name().contains("O(3)")
931 {
932 let finite_subgroup_name = double_unitary_subgroup.deduce_finite_group_name();
933 double_unitary_subgroup.set_finite_subgroup_name(Some(finite_subgroup_name));
934 }
935 double_unitary_subgroup.construct_irrep_character_table();
936 double_unitary_subgroup.canonicalise_character_table();
937 log::debug!(
938 "Constructing the double unitary subgroup for the magnetic double group... Done."
939 );
940
941 let mut group = Self::new(group_name.as_str(), su2_operations, double_unitary_subgroup)?;
942 group.set_finite_subgroup_name(finite_group_name);
943 let symbols = group.class_symbols_from_symmetry();
944 group.set_class_symbols(&symbols);
945 group.construct_ircorep_character_table();
946 group.canonicalise_character_table();
947 Ok(group)
948 }
949}
950
951fn find_positive_hemisphere<G>(group: &G) -> Option<PositiveHemisphere>
958where
959 G: GroupProperties<GroupElement = SymmetryOperation>
960 + ClassProperties<ClassSymbol = SymmetryClassSymbol<SymmetryOperation>>,
961{
962 log::debug!("Checking for classes of odd non-coaxial binary rotations or reflections...");
963 let poshem = (0..group.class_number()).find_map(|cc_i| {
964 let cc_symbol = group
965 .get_cc_symbol_of_index(cc_i)
966 .expect("Unable to retrive a conjugacy class symbol.");
967 if cc_symbol.is_spatial_binary_rotation() || cc_symbol.is_spatial_reflection() {
968 let cc = group
969 .get_cc_index(cc_i)
970 .expect("Unable to retrieve a conjugacy class.");
971 if cc.len() > 1 && cc.len().rem_euclid(2) == 1 {
972 let mut all_c2s = cc
973 .iter()
974 .map(|&op_i| {
975 group.get_index(op_i)
976 .expect("Unable to retrieve a group operation.")
977 })
978 .collect_vec();
979 all_c2s.sort_by_key(|c2| {
980 let (axis_closeness, closest_axis) = c2.generating_element.closeness_to_cartesian_axes();
981 (OrderedFloat(axis_closeness), closest_axis)
982 });
983 let c2x = all_c2s.first().expect("Unable to retrieve the last C2 operation.");
984 let c20 = all_c2s.last().expect("Unable to retrieve the first C2 operation.");
985 let z_basis = geometry::get_standard_positive_pole(
986 &c2x
987 .generating_element
988 .raw_axis()
989 .cross(c20.generating_element.raw_axis()),
990 c2x.generating_element.threshold(),
991 );
992 let x_basis = *c2x.generating_element.raw_axis();
993 log::debug!("Found a class of odd non-coaxial binary rotations or reflections:");
994 for c2 in all_c2s.iter() {
995 log::debug!(" {c2}");
996 }
997 log::debug!("Adjusting the positive hemisphere to encompass all class-0 binary-rotation or reflection poles...");
998 Some(PositiveHemisphere::new_spherical_disjoint_equatorial_arcs(
999 z_basis,
1000 x_basis,
1001 cc.len(),
1002 ))
1003 } else {
1004 None
1005 }
1006 } else {
1007 None
1008 }
1009 });
1010 if poshem.is_none() {
1011 log::debug!("No classes of odd non-coaxial binary rotations or reflections found.");
1012 }
1013 log::debug!("Checking for classes of odd non-coaxial binary rotations or reflections... Done.");
1014 poshem
1015}