qsym2/basis/
ao.rs

1//! Atomic-orbital basis functions.
2
3use std::cmp::Ordering;
4use std::collections::{HashMap, HashSet};
5use std::convert::TryInto;
6use std::fmt;
7use std::hash::{Hash, Hasher};
8use std::slice::Iter;
9
10use anyhow::{self, ensure, format_err};
11use counter::Counter;
12use derive_builder::Builder;
13use itertools::Itertools;
14#[cfg(test)]
15use itertools::izip;
16use serde::{Deserialize, Serialize};
17
18use crate::angmom::ANGMOM_LABELS;
19use crate::auxiliary::atom::Atom;
20use crate::auxiliary::misc::ProductRepeat;
21use crate::permutation::{PermutableCollection, Permutation, permute_inplace};
22
23#[cfg(test)]
24#[path = "ao_tests.rs"]
25mod ao_tests;
26
27// -------------------
28// Shell order structs
29// -------------------
30
31// ~~~~~~~~~
32// PureOrder
33// ~~~~~~~~~
34
35/// Structure to contain information about the ordering of pure Gaussians of a certain rank.
36#[derive(Clone, Builder, PartialEq, Eq, Hash)]
37pub struct PureOrder {
38    /// A sequence of $`m_l`$ values giving the ordering of the pure Gaussians.
39    #[builder(setter(custom))]
40    mls: Vec<i32>,
41
42    /// The rank of the pure Gaussians.
43    pub lpure: u32,
44
45    /// The spatial parity of the pure Gaussians: `true` if even under spatial inversion and
46    /// `false` if odd. By default, this is the same as the parity of `Self::lpure`.
47    #[builder(default = "self.default_even()?")]
48    pub even: bool,
49}
50
51impl PureOrderBuilder {
52    fn mls(&mut self, mls: &[i32]) -> &mut Self {
53        let lpure = self.lpure.expect("`lpure` has not been set.");
54        assert!(
55            mls.iter()
56                .map(|m| m.unsigned_abs())
57                .max()
58                .expect("The maximum |m| value could not be determined.")
59                == lpure
60        );
61        assert_eq!(mls.len(), (2 * lpure + 1) as usize);
62        self.mls = Some(mls.to_vec());
63        self
64    }
65
66    fn default_even(&self) -> Result<bool, String> {
67        let lpure = self
68            .lpure
69            .ok_or_else(|| "`lpure` has not been set.".to_string())?;
70        Ok(lpure % 2 == 0)
71    }
72}
73
74impl PureOrder {
75    /// Returns a builder to construct a new [`PureOrder`] structure.
76    pub fn builder() -> PureOrderBuilder {
77        PureOrderBuilder::default()
78    }
79
80    /// Constructs a new [`PureOrder`] structure from its constituting $`m_l`$ values. The spatial
81    /// inversion parity is determined by the parity of the deduced $`l`$ value.
82    pub fn new(mls: &[i32]) -> Result<Self, anyhow::Error> {
83        let lpure = mls
84            .iter()
85            .map(|m| m.unsigned_abs())
86            .max()
87            .expect("The maximum |m| value could not be determined.");
88        let pure_order = PureOrder::builder()
89            .lpure(lpure)
90            .mls(mls)
91            .build()
92            .map_err(|err| format_err!(err))?;
93        ensure!(pure_order.verify(), "Invalid `PureOrder`.");
94        Ok(pure_order)
95    }
96
97    /// Constructs a new [`PureOrder`] structure for a specified rank with increasing-$`m`$ order.
98    /// The spatial inversion parity is determined by the parity of the deduced $`l`$ value.
99    ///
100    /// # Arguments
101    ///
102    /// * `lpure` - The required pure Gaussian rank.
103    ///
104    /// # Returns
105    ///
106    /// A [`PureOrder`] struct for a specified rank with increasing-$`m`$ order.
107    #[must_use]
108    pub fn increasingm(lpure: u32) -> Self {
109        let lpure_i32 = i32::try_from(lpure).expect("`lpure` cannot be converted to `i32`.");
110        let mls = (-lpure_i32..=lpure_i32).collect_vec();
111        Self::builder()
112            .lpure(lpure)
113            .mls(&mls)
114            .build()
115            .expect("Unable to construct a `PureOrder` structure with increasing-m order.")
116    }
117
118    /// Constructs a new [`PureOrder`] structure for a specified rank with decreasing-$`m`$ order.
119    /// The spatial inversion parity is determined by the parity of the deduced $`l`$ value.
120    ///
121    /// # Arguments
122    ///
123    /// * `lpure` - The required pure Gaussian rank.
124    ///
125    /// # Returns
126    ///
127    /// A [`PureOrder`] struct for a specified rank with decreasing-$`m`$ order.
128    #[must_use]
129    pub fn decreasingm(lpure: u32) -> Self {
130        let lpure_i32 = i32::try_from(lpure).expect("`lpure` cannot be converted to `i32`.");
131        let mls = (-lpure_i32..=lpure_i32).rev().collect_vec();
132        Self::builder()
133            .lpure(lpure)
134            .mls(&mls)
135            .build()
136            .expect("Unable to construct a `PureOrder` structure with decreasing-m order.")
137    }
138
139    /// Constructs a new [`PureOrder`] structure for a specified rank with Molden order. The spatial
140    /// inversion parity is determined by the parity of the deduced $`l`$ value.
141    ///
142    /// # Arguments
143    ///
144    /// * `lpure` - The required pure Gaussian rank.
145    ///
146    /// # Returns
147    ///
148    /// A [`PureOrder`] struct for a specified rank with Molden order.
149    #[must_use]
150    pub fn molden(lpure: u32) -> Self {
151        let lpure_i32 = i32::try_from(lpure).expect("`lpure` cannot be converted to `i32`.");
152        let mls = (0..=lpure_i32)
153            .flat_map(|absm| {
154                if absm == 0 {
155                    vec![0]
156                } else {
157                    vec![absm, -absm]
158                }
159            })
160            .collect_vec();
161        Self::builder()
162            .lpure(lpure)
163            .mls(&mls)
164            .build()
165            .expect("Unable to construct a `PureOrder` structure with Molden order.")
166    }
167
168    /// Verifies if this [`PureOrder`] struct is valid.
169    ///
170    /// # Returns
171    ///
172    /// A boolean indicating if this [`PureOrder`] struct is valid.
173    #[must_use]
174    pub fn verify(&self) -> bool {
175        let mls_set = self.mls.iter().collect::<HashSet<_>>();
176        let lpure = self.lpure;
177        mls_set.len() == self.ncomps() && mls_set.iter().all(|m| m.unsigned_abs() <= lpure)
178    }
179
180    /// Iterates over the constituent $`m_l`$ values.
181    pub fn iter(&'_ self) -> Iter<'_, i32> {
182        self.mls.iter()
183    }
184
185    /// Returns the number of pure components in the shell.
186    pub fn ncomps(&self) -> usize {
187        let lpure = usize::try_from(self.lpure).unwrap_or_else(|_| {
188            panic!(
189                "Unable to convert the pure degree {} to `usize`.",
190                self.lpure
191            )
192        });
193        2 * lpure + 1
194    }
195
196    /// Returns the $`m`$ value with a specified index in this shell.
197    pub fn get_m_with_index(&self, i: usize) -> Option<i32> {
198        self.mls.get(i).cloned()
199    }
200}
201
202impl fmt::Display for PureOrder {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        writeln!(f, "Pure rank: {}", self.lpure)?;
205        writeln!(f, "Order:")?;
206        for m in self.iter() {
207            writeln!(f, "  {m}")?;
208        }
209        Ok(())
210    }
211}
212
213impl fmt::Debug for PureOrder {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        writeln!(f, "Pure rank: {}", self.lpure)?;
216        writeln!(f, "Order:")?;
217        for m in self.iter() {
218            writeln!(f, "  {m:?}")?;
219        }
220        Ok(())
221    }
222}
223
224impl PermutableCollection for PureOrder {
225    type Rank = usize;
226
227    fn get_perm_of(&self, other: &Self) -> Option<Permutation<Self::Rank>> {
228        let o_mls: HashMap<&i32, usize> = other
229            .mls
230            .iter()
231            .enumerate()
232            .map(|(i, o_m)| (o_m, i))
233            .collect();
234        let image_opt: Option<Vec<Self::Rank>> =
235            self.mls.iter().map(|s_m| o_mls.get(s_m).copied()).collect();
236        image_opt.and_then(|image| Permutation::from_image(image).ok())
237    }
238
239    fn permute(&self, perm: &Permutation<Self::Rank>) -> Result<Self, anyhow::Error> {
240        let mut p_pureorder = self.clone();
241        p_pureorder.permute_mut(perm)?;
242        Ok(p_pureorder)
243    }
244
245    fn permute_mut(&mut self, perm: &Permutation<Self::Rank>) -> Result<(), anyhow::Error> {
246        permute_inplace(&mut self.mls, perm);
247        Ok(())
248    }
249}
250
251// ~~~~~~~~~
252// CartOrder
253// ~~~~~~~~~
254
255/// Structure to contain information about the ordering of Cartesian Gaussians of a certain rank.
256#[derive(Clone, Builder, PartialEq, Eq, Hash)]
257pub struct CartOrder {
258    /// A sequence of $`(l_x, l_y, l_z)`$ tuples giving the ordering of the Cartesian Gaussians.
259    #[builder(setter(custom))]
260    pub cart_tuples: Vec<(u32, u32, u32)>,
261
262    /// The rank of the Cartesian Gaussians.
263    pub lcart: u32,
264}
265
266impl CartOrderBuilder {
267    fn cart_tuples(&mut self, cart_tuples: &[(u32, u32, u32)]) -> &mut Self {
268        let lcart = self.lcart.expect("`lcart` has not been set.");
269        assert!(
270            cart_tuples.iter().all(|(lx, ly, lz)| lx + ly + lz == lcart),
271            "Inconsistent total Cartesian orders between components."
272        );
273        assert_eq!(
274            cart_tuples.len(),
275            ((lcart + 1) * (lcart + 2)).div_euclid(2) as usize,
276            "Unexpected number of components for `lcart` = {}.",
277            lcart
278        );
279        self.cart_tuples = Some(cart_tuples.to_vec());
280        self
281    }
282}
283
284impl CartOrder {
285    /// Returns a builder to construct a new [`CartOrder`] structure.
286    pub fn builder() -> CartOrderBuilder {
287        CartOrderBuilder::default()
288    }
289
290    /// Constructs a new [`CartOrder`] structure from its constituting tuples, each of which contains
291    /// the $`x`$, $`y`$, and $`z`$ exponents for one Cartesian term.
292    ///
293    /// # Errors
294    ///
295    /// Errors if the Cartesian tuples are invalid (*e.g.* missing components or containing
296    /// inconsistent components).
297    pub fn new(cart_tuples: &[(u32, u32, u32)]) -> Result<Self, anyhow::Error> {
298        let first_tuple = cart_tuples
299            .first()
300            .ok_or(format_err!("No Cartesian tuples found."))?;
301        let lcart = first_tuple.0 + first_tuple.1 + first_tuple.2;
302        let cart_order = CartOrder::builder()
303            .lcart(lcart)
304            .cart_tuples(cart_tuples)
305            .build()
306            .map_err(|err| format_err!(err))?;
307        ensure!(cart_order.verify(), "Invalid `CartOrder`.");
308        Ok(cart_order)
309    }
310
311    /// Constructs a new [`CartOrder`] structure for a specified rank with lexicographic order.
312    ///
313    /// # Arguments
314    ///
315    /// * `lcart` - The required Cartesian Gaussian rank.
316    ///
317    /// # Returns
318    ///
319    /// A [`CartOrder`] struct for a specified rank with lexicographic order.
320    #[must_use]
321    pub fn lex(lcart: u32) -> Self {
322        let mut cart_tuples =
323            Vec::with_capacity(((lcart + 1) * (lcart + 2)).div_euclid(2) as usize);
324        for lx in (0..=lcart).rev() {
325            for ly in (0..=(lcart - lx)).rev() {
326                cart_tuples.push((lx, ly, lcart - lx - ly));
327            }
328        }
329        Self::builder()
330            .lcart(lcart)
331            .cart_tuples(&cart_tuples)
332            .build()
333            .expect("Unable to construct a `CartOrder` structure with lexicographic order.")
334    }
335
336    /// Constructs a new [`CartOrder`] structure for a specified rank with Molden order.
337    ///
338    /// # Arguments
339    ///
340    /// * `lcart` - The required Cartesian Gaussian rank up to 4.
341    ///
342    /// # Returns
343    ///
344    /// A [`CartOrder`] struct for a specified rank with Q-Chem order.
345    ///
346    /// # Panics
347    ///
348    /// Panics if `lcart` is greater than 4.
349    #[must_use]
350    pub fn molden(lcart: u32) -> Self {
351        assert!(lcart <= 4, "`lcart` > 4 is not specified by Molden.");
352        let cart_tuples: Vec<(u32, u32, u32)> = match lcart {
353            0 => vec![(0, 0, 0)],
354            1 => vec![(1, 0, 0), (0, 1, 0), (0, 0, 1)],
355            2 => vec![
356                (2, 0, 0),
357                (0, 2, 0),
358                (0, 0, 2),
359                (1, 1, 0),
360                (1, 0, 1),
361                (0, 1, 1),
362            ],
363            3 => vec![
364                (3, 0, 0),
365                (0, 3, 0),
366                (0, 0, 3),
367                (1, 2, 0),
368                (2, 1, 0),
369                (2, 0, 1),
370                (1, 0, 2),
371                (0, 1, 2),
372                (0, 2, 1),
373                (1, 1, 1),
374            ],
375            4 => vec![
376                (4, 0, 0),
377                (0, 4, 0),
378                (0, 0, 4),
379                (3, 1, 0),
380                (3, 0, 1),
381                (1, 3, 0),
382                (0, 3, 1),
383                (1, 0, 3),
384                (0, 1, 3),
385                (2, 2, 0),
386                (2, 0, 2),
387                (0, 2, 2),
388                (2, 1, 1),
389                (1, 2, 1),
390                (1, 1, 2),
391            ],
392            _ => panic!("`lcart` > 4 is not specified by Molden."),
393        };
394        Self::builder()
395            .lcart(lcart)
396            .cart_tuples(&cart_tuples)
397            .build()
398            .expect("Unable to construct a `CartOrder` structure with Molden order.")
399    }
400
401    /// Constructs a new [`CartOrder`] structure for a specified rank with Q-Chem order.
402    ///
403    /// # Arguments
404    ///
405    /// * `lcart` - The required Cartesian Gaussian rank.
406    ///
407    /// # Returns
408    ///
409    /// A [`CartOrder`] struct for a specified rank with Q-Chem order.
410    #[must_use]
411    pub fn qchem(lcart: u32) -> Self {
412        let cart_tuples: Vec<(u32, u32, u32)> = if lcart > 0 {
413            (0..3)
414                .product_repeat(lcart as usize)
415                .filter_map(|tup| {
416                    let mut tup_sorted = tup.clone();
417                    tup_sorted.sort_unstable();
418                    tup_sorted.reverse();
419                    if tup == tup_sorted {
420                        let lcartqns = tup.iter().collect::<Counter<_>>();
421                        Some((
422                            <usize as TryInto<u32>>::try_into(*(lcartqns.get(&0).unwrap_or(&0)))
423                                .expect("Unable to convert Cartesian x-exponent to `u32`."),
424                            <usize as TryInto<u32>>::try_into(*(lcartqns.get(&1).unwrap_or(&0)))
425                                .expect("Unable to convert Cartesian y-exponent to `u32`."),
426                            <usize as TryInto<u32>>::try_into(*(lcartqns.get(&2).unwrap_or(&0)))
427                                .expect("Unable to convert Cartesian z-exponent to `u32`."),
428                        ))
429                    } else {
430                        None
431                    }
432                })
433                .collect()
434        } else {
435            vec![(0, 0, 0)]
436        };
437        Self::builder()
438            .lcart(lcart)
439            .cart_tuples(&cart_tuples)
440            .build()
441            .expect("Unable to construct a `CartOrder` structure with Q-Chem order.")
442    }
443
444    /// Verifies if this [`CartOrder`] struct is valid.
445    ///
446    /// # Returns
447    ///
448    /// A boolean indicating if this [`CartOrder`] struct is valid.
449    #[must_use]
450    pub fn verify(&self) -> bool {
451        let cart_tuples_set = self.cart_tuples.iter().collect::<HashSet<_>>();
452        let lcart = self.lcart;
453        cart_tuples_set.len() == self.ncomps()
454            && cart_tuples_set
455                .iter()
456                .all(|(lx, ly, lz)| lx + ly + lz == lcart)
457    }
458
459    /// Iterates over the constituent tuples.
460    pub fn iter(&'_ self) -> Iter<'_, (u32, u32, u32)> {
461        self.cart_tuples.iter()
462    }
463
464    /// Returns the number of Cartesian components in the shell.
465    pub fn ncomps(&self) -> usize {
466        let lcart = usize::try_from(self.lcart).unwrap_or_else(|_| {
467            panic!(
468                "Unable to convert the Cartesian degree {} to `usize`.",
469                self.lcart
470            )
471        });
472        ((lcart + 1) * (lcart + 2)).div_euclid(2)
473    }
474
475    /// Returns the Cartesian component with a specified index in this shell.
476    pub fn get_cart_tuple_with_index(&self, i: usize) -> Option<(u32, u32, u32)> {
477        self.cart_tuples.get(i).cloned()
478    }
479}
480
481impl fmt::Display for CartOrder {
482    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483        writeln!(f, "Cartesian rank: {}", self.lcart)?;
484        writeln!(f, "Order:")?;
485        for cart_tuple in self.iter() {
486            writeln!(f, "  {}", cart_tuple_to_str(cart_tuple, true))?;
487        }
488        Ok(())
489    }
490}
491
492impl fmt::Debug for CartOrder {
493    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494        writeln!(f, "Cartesian rank: {}", self.lcart)?;
495        writeln!(f, "Order:")?;
496        for cart_tuple in self.iter() {
497            writeln!(f, "  {cart_tuple:?}")?;
498        }
499        Ok(())
500    }
501}
502
503impl PermutableCollection for CartOrder {
504    type Rank = usize;
505
506    fn get_perm_of(&self, other: &Self) -> Option<Permutation<Self::Rank>> {
507        let o_cart_tuples: HashMap<&(u32, u32, u32), usize> = other
508            .cart_tuples
509            .iter()
510            .enumerate()
511            .map(|(i, o_cart_tuple)| (o_cart_tuple, i))
512            .collect();
513        let image_opt: Option<Vec<Self::Rank>> = self
514            .cart_tuples
515            .iter()
516            .map(|s_cart_tuple| o_cart_tuples.get(s_cart_tuple).copied())
517            .collect();
518        image_opt.and_then(|image| Permutation::from_image(image).ok())
519    }
520
521    fn permute(&self, perm: &Permutation<Self::Rank>) -> Result<Self, anyhow::Error> {
522        let mut p_cartorder = self.clone();
523        p_cartorder.permute_mut(perm)?;
524        Ok(p_cartorder)
525    }
526
527    fn permute_mut(&mut self, perm: &Permutation<Self::Rank>) -> Result<(), anyhow::Error> {
528        permute_inplace(&mut self.cart_tuples, perm);
529        Ok(())
530    }
531}
532
533/// Translates a Cartesian exponent tuple to a human-understandable string.
534///
535/// # Arguments
536///
537/// * `cart_tuple` - A tuple of $`(l_x, l_y, l_z)`$ specifying the exponents of the Cartesian
538///   components of the Cartesian Gaussian.
539/// * flat - A flag indicating if the string representation is flat (*e.g.* `xxyz`) or compact
540///   (*e.g.* `x^2yz`).
541///
542/// Returns
543///
544/// The string representation of the Cartesian exponent tuple.
545pub(crate) fn cart_tuple_to_str(cart_tuple: &(u32, u32, u32), flat: bool) -> String {
546    if cart_tuple.0 + cart_tuple.1 + cart_tuple.2 == 0u32 {
547        "1".to_string()
548    } else {
549        let cart_array = [cart_tuple.0, cart_tuple.1, cart_tuple.2];
550        let carts = ["x", "y", "z"];
551        Itertools::intersperse(
552            cart_array.iter().enumerate().map(|(i, &l)| {
553                if flat {
554                    carts[i].repeat(l as usize)
555                } else {
556                    match l.cmp(&1) {
557                        Ordering::Greater => format!("{}^{l}", carts[i]),
558                        Ordering::Equal => carts[i].to_string(),
559                        Ordering::Less => String::new(),
560                    }
561                }
562            }),
563            String::new(),
564        )
565        .collect::<String>()
566    }
567}
568
569// ~~~~~~~~~~~
570// SpinorOrder
571// ~~~~~~~~~~~
572
573// SpinorParticleType enum
574// .......................
575
576/// Enumerated type for spinor particle types.
577#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
578pub enum SpinorParticleType {
579    /// Variant for spinors describing fermionic particles. The optional associated value contains
580    /// any additional balance symmetry to be applied to the spinors.
581    Fermion(Option<SpinorBalanceSymmetry>),
582
583    /// Variant for spinors describing antifermionic particles. The optional associated value
584    /// contains any additional balance symmetry to be applied to the spinors.
585    Antifermion(Option<SpinorBalanceSymmetry>),
586}
587
588impl fmt::Display for SpinorParticleType {
589    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
590        match self {
591            SpinorParticleType::Fermion(None) => {
592                write!(f, "fermion")
593            }
594            SpinorParticleType::Fermion(Some(sbs)) => {
595                write!(f, "fermion ({sbs})")
596            }
597            SpinorParticleType::Antifermion(None) => {
598                write!(f, "antifermion")
599            }
600            SpinorParticleType::Antifermion(Some(sbs)) => {
601                write!(f, "antifermion ({sbs})")
602            }
603        }
604    }
605}
606
607// SpinorBalanceSymmetry enum
608// ..........................
609
610/// Enumerated type for spinor balance symmetry types.
611#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
612pub enum SpinorBalanceSymmetry {
613    /// Variant for kinetic balance, σ·p.
614    KineticBalance,
615}
616
617impl fmt::Display for SpinorBalanceSymmetry {
618    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
619        match self {
620            SpinorBalanceSymmetry::KineticBalance => {
621                write!(f, "σ·p")
622            }
623        }
624    }
625}
626
627/// Structure to contain information about the ordering of spinors of a certain rank.
628#[derive(Clone, Builder, PartialEq, Eq, Hash)]
629pub struct SpinorOrder {
630    /// The angular momentum (times two) of the spinor Gaussians. This must be an odd integer.
631    #[builder(setter(custom))]
632    pub two_j: u32,
633
634    /// A sequence of $`2m_j`$ values giving the ordering of the spinor Gaussians.
635    #[builder(setter(custom))]
636    two_mjs: Vec<i32>,
637
638    /// The spatial inversion parity of the spinor Gaussians: `true` if even under spatial inversion
639    /// and `false` if odd. This is intrinsic to the spinor Gaussians and independent of the
640    /// particle type and any associated balance symmetry.
641    pub even: bool,
642
643    /// The particle type described by this spinor shell.
644    pub particle_type: SpinorParticleType,
645}
646
647impl SpinorOrderBuilder {
648    fn two_j(&mut self, two_j: u32) -> &mut Self {
649        if two_j.rem_euclid(2) != 1 {
650            panic!("`two_j` must be odd.")
651        }
652        self.two_j = Some(two_j);
653        self
654    }
655
656    fn two_mjs(&mut self, two_mjs: &[i32]) -> &mut Self {
657        let two_j = self.two_j.expect("`two_j` has not been set.");
658        let max_two_m = two_mjs
659            .iter()
660            .map(|two_m| two_m.unsigned_abs())
661            .max()
662            .expect("The maximum |2m| value could not be determined.");
663        assert_eq!(
664            max_two_m, two_j,
665            "The maximum |2m| value does not equal 2j = {two_j}."
666        );
667        assert_eq!(
668            two_mjs.len(),
669            (two_j + 1) as usize,
670            "The number of 2m values specified does not equal 2j + 1 = {}",
671            two_j + 1
672        );
673        self.two_mjs = Some(two_mjs.to_vec());
674        self
675    }
676}
677
678impl SpinorOrder {
679    /// Returns a builder to construct a new [`SpinorOrder`] structure.
680    pub fn builder() -> SpinorOrderBuilder {
681        SpinorOrderBuilder::default()
682    }
683
684    /// Constructs a new [`SpinorOrder`] structure from its constituting $`2m_j`$ values and a
685    /// specified spatial parity.
686    pub fn new(
687        two_mjs: &[i32],
688        even: bool,
689        particle_type: SpinorParticleType,
690    ) -> Result<Self, anyhow::Error> {
691        let two_j = two_mjs
692            .iter()
693            .map(|two_m| two_m.unsigned_abs())
694            .max()
695            .ok_or_else(|| format_err!("The maximum |2m| value could not be determined."))?;
696        let spinor_order = SpinorOrder::builder()
697            .two_j(two_j)
698            .two_mjs(two_mjs)
699            .even(even)
700            .particle_type(particle_type)
701            .build()
702            .map_err(|err| format_err!(err))?;
703        ensure!(spinor_order.verify(), "Invalid `SpinorOrder`.");
704        Ok(spinor_order)
705    }
706
707    /// Constructs a new [`SpinorOrder`] structure for a specified angular momentum with
708    /// increasing-$`m`$ order.
709    ///
710    /// # Arguments
711    ///
712    /// * `two_j` - The required spinor angular momentum (times two).
713    /// * `even` - Boolean indicating whether the large-component spinors are even with respect to
714    ///   spatial inversion.
715    /// * `particle_type` - The associated particle type.
716    ///
717    /// # Returns
718    ///
719    /// A [`SpinorOrder`] struct for a specified angular momentum with increasing-$`m`$ order.
720    #[must_use]
721    pub fn increasingm(two_j: u32, even: bool, particle_type: SpinorParticleType) -> Self {
722        let two_j_i32 = i32::try_from(two_j).expect("`two_j` cannot be converted to `i32`.");
723        let two_mjs = (-two_j_i32..=two_j_i32).step_by(2).collect_vec();
724        Self::builder()
725            .two_j(two_j)
726            .two_mjs(&two_mjs)
727            .even(even)
728            .particle_type(particle_type)
729            .build()
730            .expect("Unable to construct a `SpinorOrder` structure with increasing-m order.")
731    }
732
733    /// Constructs a new [`SpinorOrder`] structure for a specified angular momentum with
734    /// decreasing-$`m`$ order.
735    ///
736    /// # Arguments
737    ///
738    /// * `two_j` - The required spinor angular momentum (times two).
739    /// * `even` - Boolean indicating whether the large-component spinors are even with respect to
740    ///   spatial inversion.
741    /// * `particle_type` - The associated particle type.
742    ///
743    /// # Returns
744    ///
745    /// A [`SpinorOrder`] struct for a specified angular momentum with decreasing-$`m`$ order.
746    #[must_use]
747    pub fn decreasingm(two_j: u32, even: bool, particle_type: SpinorParticleType) -> Self {
748        let two_j_i32 = i32::try_from(two_j).expect("`two_j` cannot be converted to `i32`.");
749        let two_mjs = (-two_j_i32..=two_j_i32).rev().step_by(2).collect_vec();
750        Self::builder()
751            .two_j(two_j)
752            .two_mjs(&two_mjs)
753            .even(even)
754            .particle_type(particle_type)
755            .build()
756            .expect("Unable to construct a `SpinorOrder` structure with decreasing-m order.")
757    }
758
759    /// Constructs a new [`SpinorOrder`] structure for a specified angular momentum with Molden order.
760    ///
761    /// # Arguments
762    ///
763    /// * `two_j` - The required spinor angular momentum (times two).
764    /// * `even` - Boolean indicating whether the large-component spinors are even with respect to
765    ///   spatial inversion.
766    /// * `particle_type` - The associated particle type.
767    ///
768    /// # Returns
769    ///
770    /// A [`SpinorOrder`] struct for a specified angular momentum with Molden order.
771    #[must_use]
772    pub fn molden(two_j: u32, even: bool, particle_type: SpinorParticleType) -> Self {
773        let two_j_i32 = i32::try_from(two_j).expect("`two_j` cannot be converted to `i32`.");
774        let two_mjs = (1..=two_j_i32)
775            .step_by(2)
776            .flat_map(|abs_twom| vec![abs_twom, -abs_twom])
777            .collect_vec();
778        Self::builder()
779            .two_j(two_j)
780            .two_mjs(&two_mjs)
781            .even(even)
782            .particle_type(particle_type)
783            .build()
784            .expect("Unable to construct a `SpinorOrder` structure with Molden order.")
785    }
786
787    /// Verifies if this [`SpinorOrder`] struct is valid.
788    ///
789    /// # Returns
790    ///
791    /// A boolean indicating if this [`SpinorOrder`] struct is valid.
792    #[must_use]
793    pub fn verify(&self) -> bool {
794        let two_mjs_set = self.two_mjs.iter().collect::<HashSet<_>>();
795        let two_j = self.two_j;
796        two_mjs_set.len() == self.ncomps()
797            && two_mjs_set
798                .iter()
799                .all(|two_m| two_m.unsigned_abs() <= two_j)
800    }
801
802    /// Iterates over the constituent $`2m_j`$ values.
803    pub fn iter(&'_ self) -> Iter<'_, i32> {
804        self.two_mjs.iter()
805    }
806
807    /// Returns the number of pure components in the shell.
808    pub fn ncomps(&self) -> usize {
809        let two_j = usize::try_from(self.two_j).unwrap_or_else(|_| {
810            panic!(
811                "Unable to convert the two-j value {} to `usize`.",
812                self.two_j
813            )
814        });
815        two_j + 1
816    }
817
818    /// Returns the $`2m`$ value with a specified index in this shell.
819    pub fn get_two_m_with_index(&self, i: usize) -> Option<i32> {
820        self.two_mjs.get(i).cloned()
821    }
822
823    /// Returns the angular momentum quantum number of the spatial part of the spinors in this
824    /// shell.
825    pub fn l(&self) -> u32 {
826        if self.two_j.rem_euclid(4) == 1 {
827            // even l is to the left of j, and odd l to the right
828            if self.even {
829                (self.two_j - 1).div_euclid(2)
830            } else {
831                (self.two_j + 1).div_euclid(2)
832            }
833        } else {
834            assert_eq!(self.two_j.rem_euclid(4), 3);
835            // even l is to the right of j, and odd l to the left
836            if self.even {
837                (self.two_j + 1).div_euclid(2)
838            } else {
839                (self.two_j - 1).div_euclid(2)
840            }
841        }
842    }
843
844    /// Returns the value of $`a = 2(j - l)`$.
845    pub fn a(&self) -> i8 {
846        if self.two_j.rem_euclid(4) == 1 {
847            // even l is to the left of j, and odd l to the right
848            if self.even { 1 } else { -1 }
849        } else {
850            assert_eq!(self.two_j.rem_euclid(4), 3);
851            // even l is to the right of j, and odd l to the left
852            if self.even { -1 } else { 1 }
853        }
854    }
855}
856
857impl fmt::Display for SpinorOrder {
858    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
859        writeln!(
860            f,
861            "Angular momentum: {}/2 ({}; l = {}), {}",
862            self.two_j,
863            if self.a() == 1 { "+" } else { "-" },
864            self.l(),
865            self.particle_type,
866        )?;
867        writeln!(f, "Order:")?;
868        for two_m in self.iter() {
869            writeln!(f, "  {two_m}/2")?;
870        }
871        Ok(())
872    }
873}
874
875impl fmt::Debug for SpinorOrder {
876    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
877        writeln!(
878            f,
879            "Angular momentum: {}/2 ({}; l = {}), {}",
880            self.two_j,
881            if self.a() == 1 { "+" } else { "-" },
882            self.l(),
883            self.particle_type,
884        )?;
885        writeln!(f, "Order:")?;
886        for two_m in self.iter() {
887            writeln!(f, "  {two_m:?}/2")?;
888        }
889        Ok(())
890    }
891}
892
893impl PermutableCollection for SpinorOrder {
894    type Rank = usize;
895
896    fn get_perm_of(&self, other: &Self) -> Option<Permutation<Self::Rank>> {
897        let o_two_mjs: HashMap<&i32, usize> = other
898            .two_mjs
899            .iter()
900            .enumerate()
901            .map(|(i, o_two_m)| (o_two_m, i))
902            .collect();
903        let image_opt: Option<Vec<Self::Rank>> = self
904            .two_mjs
905            .iter()
906            .map(|s_two_m| o_two_mjs.get(s_two_m).copied())
907            .collect();
908        image_opt.and_then(|image| Permutation::from_image(image).ok())
909    }
910
911    fn permute(&self, perm: &Permutation<Self::Rank>) -> Result<Self, anyhow::Error> {
912        let mut p_pureorder = self.clone();
913        p_pureorder.permute_mut(perm)?;
914        Ok(p_pureorder)
915    }
916
917    fn permute_mut(&mut self, perm: &Permutation<Self::Rank>) -> Result<(), anyhow::Error> {
918        permute_inplace(&mut self.two_mjs, perm);
919        Ok(())
920    }
921}
922
923// ----------
924// ShellOrder
925// ----------
926
927/// Enumerated type to indicate the type of the angular functions in a shell and how they are
928/// ordered.
929#[derive(Clone, PartialEq, Eq, Hash, Debug)]
930pub enum ShellOrder {
931    /// This variant indicates that the angular functions are real solid harmonics. The associated
932    /// value is a [`PureOrder`] struct containing the order of these functions.
933    Pure(PureOrder),
934
935    /// This variant indicates that the angular functions are Cartesian functions. The associated
936    /// value is a [`CartOrder`] struct containing the order of these functions.
937    Cart(CartOrder),
938
939    /// This variant indicates that the angular functions are spinors. The associated
940    /// value is a [`SpinorOrder`] struct containing the order of these functions.
941    Spinor(SpinorOrder),
942}
943
944impl fmt::Display for ShellOrder {
945    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
946        match self {
947            ShellOrder::Pure(pure_order) => write!(
948                f,
949                "Pure ({}) ({})",
950                if pure_order.even { "g" } else { "u" },
951                pure_order.iter().map(|m| m.to_string()).join(", ")
952            ),
953            ShellOrder::Cart(cart_order) => write!(
954                f,
955                "Cart ({}) ({})",
956                if cart_order.lcart % 2 == 0 { "g" } else { "u" },
957                cart_order
958                    .iter()
959                    .map(|cart_tuple| { cart_tuple_to_str(cart_tuple, true) })
960                    .join(", ")
961            ),
962            ShellOrder::Spinor(spinor_order) => write!(
963                f,
964                "Spinor ({}; l = {}), {}, ({})",
965                if spinor_order.a() == 1 { "+" } else { "-" },
966                spinor_order.l(),
967                spinor_order.particle_type,
968                spinor_order
969                    .iter()
970                    .map(|two_m| format!("{two_m}/2"))
971                    .join(", ")
972            ),
973        }
974    }
975}
976
977// ----------
978// BasisShell
979// ----------
980
981/// Structure representing a shell in an atomic-orbital basis set.
982#[derive(Clone, Builder, PartialEq, Eq, Hash, Debug)]
983pub struct BasisShell {
984    /// A non-negative integer indicating the rank of the shell. If this shell is pure, the rank
985    /// equals its angular momentum. If this shell is Cartesian, the rank is the sum of the
986    /// exponents of the Cartesian coordinates. If this shell is a spinor, the rank is twice its
987    /// angular momentum.
988    #[builder(setter(custom))]
989    pub l: u32,
990
991    /// An enum indicating the type of the angular functions in a shell and how they are ordered.
992    #[builder(setter(custom))]
993    pub shell_order: ShellOrder,
994}
995
996impl BasisShellBuilder {
997    fn l(&mut self, l: u32) -> &mut Self {
998        match self.shell_order.as_ref() {
999            Some(ShellOrder::Pure(pure_order)) => assert_eq!(pure_order.lpure, l),
1000            Some(ShellOrder::Cart(cart_order)) => assert_eq!(cart_order.lcart, l),
1001            Some(ShellOrder::Spinor(spinor_order)) => assert_eq!(spinor_order.two_j, l),
1002            None => {}
1003        }
1004        self.l = Some(l);
1005        self
1006    }
1007
1008    fn shell_order(&mut self, shl_ord: ShellOrder) -> &mut Self {
1009        match (&shl_ord, self.l) {
1010            (ShellOrder::Pure(pure_order), Some(l)) => assert_eq!(pure_order.lpure, l),
1011            (ShellOrder::Cart(cart_order), Some(l)) => assert_eq!(cart_order.lcart, l),
1012            (ShellOrder::Spinor(spinor_order), Some(l)) => assert_eq!(spinor_order.two_j, l),
1013            _ => {}
1014        }
1015        self.shell_order = Some(shl_ord);
1016        self
1017    }
1018}
1019
1020impl BasisShell {
1021    /// Returns a builder to construct a new [`BasisShell`].
1022    ///
1023    /// # Returns
1024    ///
1025    /// A builder to construct a new [`BasisShell`].
1026    pub fn builder() -> BasisShellBuilder {
1027        BasisShellBuilder::default()
1028    }
1029
1030    /// Constructs a new [`BasisShell`].
1031    ///
1032    /// # Arguments
1033    ///
1034    /// * `l` - The rank of this shell, which is equal to $`l_{\mathrm{pure}}`$ for a pure shell,
1035    ///   $`l_{\mathrm{cart}}`$ for a Cartesian shell, or $`2j`$ for a spinor shell.
1036    /// * `shl_ord` - A [`ShellOrder`] structure specifying the type and ordering of the basis
1037    ///   functions in this shell.
1038    pub fn new(l: u32, shl_ord: ShellOrder) -> Self {
1039        match &shl_ord {
1040            ShellOrder::Pure(pureorder) => assert_eq!(pureorder.lpure, l),
1041            ShellOrder::Cart(cartorder) => assert_eq!(cartorder.lcart, l),
1042            ShellOrder::Spinor(spinororder) => assert_eq!(spinororder.two_j, l),
1043        }
1044        BasisShell::builder()
1045            .l(l)
1046            .shell_order(shl_ord)
1047            .build()
1048            .expect("Unable to construct a `BasisShell`.")
1049    }
1050
1051    /// The number of basis functions in this shell.
1052    pub fn n_funcs(&self) -> usize {
1053        let lsize = self.l as usize;
1054        match self.shell_order {
1055            ShellOrder::Pure(_) => 2 * lsize + 1,
1056            ShellOrder::Cart(_) => ((lsize + 1) * (lsize + 2)).div_euclid(2),
1057            ShellOrder::Spinor(_) => lsize + 1,
1058        }
1059    }
1060}
1061
1062// ---------
1063// BasisAtom
1064// ---------
1065
1066/// Structure containing the ordered sequence of the shells for an atom.
1067#[derive(Clone, Builder, PartialEq, Eq, Hash, Debug)]
1068pub struct BasisAtom<'a> {
1069    /// An atom in the basis set.
1070    pub(crate) atom: &'a Atom,
1071
1072    /// The ordered shells associated with this atom.
1073    #[builder(setter(custom))]
1074    pub(crate) basis_shells: Vec<BasisShell>,
1075}
1076
1077impl<'a> BasisAtomBuilder<'a> {
1078    pub(crate) fn basis_shells(&mut self, bss: &[BasisShell]) -> &mut Self {
1079        self.basis_shells = Some(bss.to_vec());
1080        self
1081    }
1082}
1083
1084impl<'a> BasisAtom<'a> {
1085    /// Returns a builder to construct a new [`BasisAtom`].
1086    ///
1087    /// # Returns
1088    ///
1089    /// A builder to construct a new [`BasisAtom`].
1090    pub fn builder() -> BasisAtomBuilder<'a> {
1091        BasisAtomBuilder::default()
1092    }
1093
1094    /// Constructs a new [`BasisAtom`].
1095    ///
1096    /// # Arguments
1097    ///
1098    /// * `atm` - A reference to an atom.
1099    /// * `bss` - A sequence of [`BasisShell`]s containing the basis functions localised on this
1100    ///   atom.
1101    pub fn new(atm: &'a Atom, bss: &[BasisShell]) -> Self {
1102        BasisAtom::builder()
1103            .atom(atm)
1104            .basis_shells(bss)
1105            .build()
1106            .expect("Unable to construct a `BasisAtom`.")
1107    }
1108
1109    /// The number of basis functions localised on this atom.
1110    fn n_funcs(&self) -> usize {
1111        self.basis_shells.iter().map(BasisShell::n_funcs).sum()
1112    }
1113
1114    /// The ordered tuples of 0-based indices indicating the starting (inclusive) and ending
1115    /// (exclusive) positions of the shells on this atom.
1116    fn shell_boundary_indices(&self) -> Vec<(usize, usize)> {
1117        self.basis_shells
1118            .iter()
1119            .scan(0, |acc, basis_shell| {
1120                let start_index = *acc;
1121                *acc += basis_shell.n_funcs();
1122                Some((start_index, *acc))
1123            })
1124            .collect::<Vec<_>>()
1125    }
1126}
1127
1128// -----------------
1129// BasisAngularOrder
1130// -----------------
1131
1132/// Structure containing the angular momentum information of an atomic-orbital basis set that is
1133/// required for symmetry transformation to be performed.
1134#[derive(Clone, Builder)]
1135pub struct BasisAngularOrder<'a> {
1136    /// An ordered sequence of [`BasisAtom`] in the order the atoms are defined in the molecule.
1137    #[builder(setter(custom))]
1138    pub(crate) basis_atoms: Vec<BasisAtom<'a>>,
1139}
1140
1141impl<'a> BasisAngularOrderBuilder<'a> {
1142    pub(crate) fn basis_atoms(&mut self, batms: &[BasisAtom<'a>]) -> &mut Self {
1143        self.basis_atoms = Some(batms.to_vec());
1144        self
1145    }
1146}
1147
1148impl<'a> BasisAngularOrder<'a> {
1149    /// Returns a builder to construct a new [`BasisAngularOrder`].
1150    ///
1151    /// # Returns
1152    ///
1153    /// A builder to construct a new [`BasisAngularOrder`].
1154    #[must_use]
1155    pub fn builder() -> BasisAngularOrderBuilder<'a> {
1156        BasisAngularOrderBuilder::default()
1157    }
1158
1159    /// Constructs a new [`BasisAngularOrder`] structure from the constituting [`BasisAtom`]s
1160    /// without any additional balance symmetry auxiliary information.
1161    ///
1162    /// # Arguments
1163    ///
1164    /// * `batms` - The constituent [`BasisAtom`]s.
1165    pub fn new(batms: &[BasisAtom<'a>]) -> Self {
1166        BasisAngularOrder::builder()
1167            .basis_atoms(batms)
1168            .build()
1169            .expect("Unable to construct a `BasisAngularOrder`.")
1170    }
1171
1172    /// The number of atoms in the basis.
1173    pub fn n_atoms(&self) -> usize {
1174        self.basis_atoms.len()
1175    }
1176
1177    /// The number of basis functions in this basis.
1178    pub fn n_funcs(&self) -> usize {
1179        self.basis_atoms.iter().map(BasisAtom::n_funcs).sum()
1180    }
1181
1182    /// The ordered tuples of 0-based shell indices indicating the starting (inclusive) and ending
1183    /// (exclusive) shell positions of the atoms in this basis.
1184    pub fn atom_boundary_indices(&self) -> Vec<(usize, usize)> {
1185        self.basis_atoms
1186            .iter()
1187            .scan(0, |acc, basis_atom| {
1188                let start_index = *acc;
1189                *acc += basis_atom.n_funcs();
1190                Some((start_index, *acc))
1191            })
1192            .collect::<Vec<_>>()
1193    }
1194
1195    /// The ordered tuples of 0-based function indices indicating the starting (inclusive) and
1196    /// ending (exclusive) positions of the shells in this basis.
1197    pub fn shell_boundary_indices(&self) -> Vec<(usize, usize)> {
1198        let atom_boundary_indices = self.atom_boundary_indices();
1199        self.basis_atoms
1200            .iter()
1201            .zip(atom_boundary_indices)
1202            .flat_map(|(basis_atom, (atom_start, _))| {
1203                basis_atom
1204                    .shell_boundary_indices()
1205                    .iter()
1206                    .map(|(shell_start, shell_end)| {
1207                        (shell_start + atom_start, shell_end + atom_start)
1208                    })
1209                    .collect::<Vec<_>>()
1210            })
1211            .collect::<Vec<_>>()
1212    }
1213
1214    /// An iterator over the constituent [`BasisShell`]s in this basis.
1215    pub fn basis_shells(&self) -> impl Iterator<Item = &BasisShell> + '_ {
1216        self.basis_atoms
1217            .iter()
1218            .flat_map(|basis_atom| basis_atom.basis_shells.iter())
1219    }
1220
1221    /// Determines the permutation of the functions in this [`BasisAngularOrder`] to map `self` to
1222    /// `other`, given that the shells themselves remain unchanged while only the functions in each
1223    /// shell are permuted.
1224    ///
1225    /// For example, consider `self`:
1226    /// ```text
1227    /// S (1)
1228    /// P (x, y, z)
1229    /// D (xx, xy, xz, yy, yz, zz)
1230    /// ```
1231    ///
1232    /// and `other`:
1233    /// ```text
1234    /// S (1)
1235    /// P (y, z, x)
1236    /// D (xx, xy, yy, xz, yz, zz)
1237    /// ```
1238    ///
1239    /// the mapping permutation is given by `π(0, 3, 1, 2, 4, 5, 7, 6, 8, 9)`.
1240    ///
1241    /// # Arguments
1242    ///
1243    /// * `other` - Another [`BasisAngularOrder`] to be compared against.
1244    ///
1245    /// # Returns
1246    ///
1247    /// The mapping permutation, if any.
1248    #[cfg(test)]
1249    pub(crate) fn get_perm_of_functions_fixed_shells(
1250        &self,
1251        other: &Self,
1252    ) -> Result<Permutation<usize>, anyhow::Error> {
1253        if self.n_funcs() == other.n_funcs() && self.n_atoms() == other.n_atoms() {
1254            let s_shell_boundaries = self.shell_boundary_indices();
1255            let o_shell_boundaries = other.shell_boundary_indices();
1256            if s_shell_boundaries.len() == o_shell_boundaries.len() {
1257                izip!(
1258                    self.basis_shells(),
1259                    other.basis_shells(),
1260                    s_shell_boundaries.iter(),
1261                    o_shell_boundaries.iter()
1262                )
1263                .map(|(s_bs, o_bs, (s_start, s_end), (o_start, o_end))| {
1264                    if (s_start, s_end) == (o_start, o_end) {
1265                        let s_shl_ord = &s_bs.shell_order;
1266                        let o_shl_ord = &o_bs.shell_order;
1267                        match (s_shl_ord, o_shl_ord) {
1268                            (ShellOrder::Pure(s_po), ShellOrder::Pure(o_po)) => Ok(
1269                                s_po.get_perm_of(o_po)
1270                                    .unwrap()
1271                                    .image()
1272                                    .iter()
1273                                    .map(|x| s_start + x)
1274                                    .collect_vec(),
1275                            ),
1276                            (ShellOrder::Cart(s_co), ShellOrder::Cart(o_co)) => Ok(
1277                                s_co.get_perm_of(o_co)
1278                                    .unwrap()
1279                                    .image()
1280                                    .iter()
1281                                    .map(|x| s_start + x)
1282                                    .collect_vec(),
1283                            ),
1284                            _ => Err(format_err!("At least one pair of corresponding shells have mismatched pure/cart.")),
1285                        }
1286                    } else {
1287                        Err(format_err!("At least one pair of corresponding shells have mismatched boundary indices."))
1288                    }
1289                })
1290                .collect::<Result<Vec<_>, _>>()
1291                .and_then(|image_by_shells| {
1292                    let flattened_image = image_by_shells.into_iter().flatten().collect_vec();
1293                    Permutation::from_image(flattened_image)
1294                })
1295            } else {
1296                Err(format_err!("Mismatched numbers of shells."))
1297            }
1298        } else {
1299            Err(format_err!("Mismatched numbers of basis functions."))
1300        }
1301    }
1302}
1303
1304impl<'a> PermutableCollection for BasisAngularOrder<'a> {
1305    type Rank = usize;
1306
1307    /// Determines the permutation of [`BasisAtom`]s to map `self` to `other`.
1308    ///
1309    /// # Arguments
1310    ///
1311    /// * `other` - Another [`BasisAngularOrder`] to be compared with `self`.
1312    ///
1313    /// # Returns
1314    ///
1315    /// Returns a permutation that permutes the *ordinary* atoms of `self` to give `other`, or
1316    /// `None` if no such permutation exists.
1317    fn get_perm_of(&self, other: &Self) -> Option<Permutation<Self::Rank>> {
1318        let o_basis_atoms: HashMap<&BasisAtom, usize> = other
1319            .basis_atoms
1320            .iter()
1321            .enumerate()
1322            .map(|(i, basis_atom)| (basis_atom, i))
1323            .collect();
1324        let image_opt: Option<Vec<Self::Rank>> = self
1325            .basis_atoms
1326            .iter()
1327            .map(|s_basis_atom| o_basis_atoms.get(s_basis_atom).copied())
1328            .collect();
1329        image_opt.and_then(|image| Permutation::from_image(image).ok())
1330    }
1331
1332    fn permute(&self, perm: &Permutation<Self::Rank>) -> Result<Self, anyhow::Error> {
1333        let mut p_bao = self.clone();
1334        p_bao.permute_mut(perm)?;
1335        Ok(p_bao)
1336    }
1337
1338    fn permute_mut(&mut self, perm: &Permutation<Self::Rank>) -> Result<(), anyhow::Error> {
1339        permute_inplace(&mut self.basis_atoms, perm);
1340        Ok(())
1341    }
1342}
1343
1344impl<'a> fmt::Display for BasisAngularOrder<'a> {
1345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1346        let order_length = self
1347            .basis_shells()
1348            .map(|v| v.shell_order.to_string().chars().count())
1349            .max()
1350            .unwrap_or(20);
1351        let atom_index_length = self.n_atoms().to_string().chars().count();
1352        writeln!(f, "{}", "┈".repeat(17 + atom_index_length + order_length))?;
1353        writeln!(f, " {:>atom_index_length$}  Atom  Shell  Order", "#")?;
1354        writeln!(f, "{}", "┈".repeat(17 + atom_index_length + order_length))?;
1355        for (atm_i, batm) in self.basis_atoms.iter().enumerate() {
1356            let atm = batm.atom;
1357            for (shl_i, bshl) in batm.basis_shells.iter().enumerate() {
1358                if shl_i == 0 {
1359                    writeln!(
1360                        f,
1361                        " {:>atom_index_length$}  {:<4}  {:<5}  {:<order_length$}",
1362                        atm_i,
1363                        atm.atomic_symbol,
1364                        match &bshl.shell_order {
1365                            ShellOrder::Pure(_) | ShellOrder::Cart(_) => ANGMOM_LABELS
1366                                .get(usize::try_from(bshl.l).unwrap_or_else(|err| panic!("{err}")))
1367                                .copied()
1368                                .unwrap_or(&bshl.l.to_string())
1369                                .to_string(),
1370                            ShellOrder::Spinor(spinororder) => format!("{}/2", spinororder.two_j),
1371                        },
1372                        bshl.shell_order
1373                    )?;
1374                } else {
1375                    writeln!(
1376                        f,
1377                        " {:>atom_index_length$}  {:<4}  {:<5}  {:<order_length$}",
1378                        "",
1379                        "",
1380                        match &bshl.shell_order {
1381                            ShellOrder::Pure(_) | ShellOrder::Cart(_) => ANGMOM_LABELS
1382                                .get(usize::try_from(bshl.l).unwrap_or_else(|err| panic!("{err}")))
1383                                .copied()
1384                                .unwrap_or(&bshl.l.to_string())
1385                                .to_string(),
1386                            ShellOrder::Spinor(spinororder) => format!("{}/2", spinororder.two_j),
1387                        },
1388                        bshl.shell_order
1389                    )?;
1390                }
1391            }
1392        }
1393        writeln!(f, "{}", "┈".repeat(17 + atom_index_length + order_length))?;
1394        Ok(())
1395    }
1396}
1397
1398impl<'a> PartialEq for BasisAngularOrder<'a> {
1399    fn eq(&self, other: &Self) -> bool {
1400        self.basis_atoms == other.basis_atoms
1401    }
1402}
1403
1404impl<'a> Eq for BasisAngularOrder<'a> {}
1405
1406impl<'a> Hash for BasisAngularOrder<'a> {
1407    fn hash<H: Hasher>(&self, state: &mut H) {
1408        self.basis_atoms.hash(state);
1409    }
1410}
1411
1412impl<'a> fmt::Debug for BasisAngularOrder<'a> {
1413    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1414        writeln!(f, "{:?}", self.basis_atoms)
1415    }
1416}