Skip to main content

laddu_physics/quantum/
types.rs

1use std::{fmt::Display, str::FromStr};
2
3use auto_ops::impl_op_ex;
4use num::rational::Ratio;
5use serde::{Deserialize, Serialize};
6
7use crate::LadduPhysicsError;
8
9const QUANTUM_NUMBER_FLOAT_TOLERANCE: f64 = 1.0e-12;
10
11macro_rules! impl_try_from_str {
12    ($ty:ty) => {
13        impl ::std::convert::TryFrom<&str> for $ty {
14            type Error = <$ty as ::std::str::FromStr>::Err;
15
16            fn try_from(value: &str) -> Result<Self, Self::Error> {
17                <Self as ::std::str::FromStr>::from_str(value)
18            }
19        }
20
21        impl ::std::convert::TryFrom<String> for $ty {
22            type Error = <$ty as ::std::str::FromStr>::Err;
23
24            fn try_from(value: String) -> Result<Self, Self::Error> {
25                <Self as ::std::str::FromStr>::from_str(&value)
26            }
27        }
28    };
29}
30
31macro_rules! impl_try_from_signed_ints {
32    ($ty:ty; $($int:ty),+ $(,)?) => {
33        $(
34            impl TryFrom<$int> for $ty {
35                type Error = LadduPhysicsError;
36
37                fn try_from(value: $int) -> Result<Self, Self::Error> {
38                    <$ty>::try_from_i128(value as i128)
39                }
40            }
41        )+
42    };
43}
44
45macro_rules! impl_try_from_unsigned_ints {
46    ($ty:ty; $($int:ty),+ $(,)?) => {
47        $(
48            impl TryFrom<$int> for $ty {
49                type Error = LadduPhysicsError;
50
51                fn try_from(value: $int) -> Result<Self, Self::Error> {
52                    <$ty>::try_from_u128(value as u128)
53                }
54            }
55        )+
56    };
57}
58
59macro_rules! impl_try_from_signed_ratios {
60    ($ty:ty; $($int:ty),+ $(,)?) => {
61        $(
62            impl TryFrom<Ratio<$int>> for $ty {
63                type Error = LadduPhysicsError;
64
65                fn try_from(value: Ratio<$int>) -> Result<Self, Self::Error> {
66                    <$ty>::try_from_ratio_i128(
67                        *value.numer() as i128,
68                        *value.denom() as i128,
69                    )
70                }
71            }
72        )+
73    };
74}
75
76macro_rules! impl_try_from_unsigned_ratios {
77    ($ty:ty; $($int:ty),+ $(,)?) => {
78        $(
79            impl TryFrom<Ratio<$int>> for $ty {
80                type Error = LadduPhysicsError;
81
82                fn try_from(value: Ratio<$int>) -> Result<Self, Self::Error> {
83                    let numer = i128::try_from(*value.numer()).map_err(|_| {
84                        LadduPhysicsError::invalid_value(
85        "ratio numerator",
86        "representable as i128",
87        *value.numer(),
88    )
89                    })?;
90                    let denom = i128::try_from(*value.denom()).map_err(|_| {
91                        LadduPhysicsError::invalid_value(
92        "ratio denominator",
93        "representable as i128",
94        *value.denom(),
95    )
96                    })?;
97                    <$ty>::try_from_ratio_i128(numer, denom)
98                }
99            }
100        )+
101    };
102}
103
104macro_rules! impl_try_from_floats {
105    ($ty:ty; $($float:ty),+ $(,)?) => {
106        $(
107            impl TryFrom<$float> for $ty {
108                type Error = LadduPhysicsError;
109
110                fn try_from(value: $float) -> Result<Self, Self::Error> {
111                    <$ty>::try_from_f64(value as f64)
112                }
113            }
114        )+
115    };
116}
117
118macro_rules! impl_from_quantum_number_for_floats {
119    ($ty:ty, $getter:ident, $scale:expr) => {
120        impl From<$ty> for f32 {
121            fn from(value: $ty) -> Self {
122                value.$getter() as Self / $scale as Self
123            }
124        }
125
126        impl From<$ty> for f64 {
127            fn from(value: $ty) -> Self {
128                value.$getter() as Self / $scale as Self
129            }
130        }
131    };
132}
133
134macro_rules! impl_half_integer_display {
135    ($ty:ty, $getter:ident) => {
136        impl Display for $ty {
137            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138                let value = self.$getter();
139                if value % 2 == 0 {
140                    write!(f, "{}", value / 2)
141                } else {
142                    write!(f, "{value}/2")
143                }
144            }
145        }
146    };
147}
148
149macro_rules! impl_j_conversions {
150    () => {
151        impl_try_from_signed_ints!(J; i8, i16, i32, i64, i128, isize);
152        impl_try_from_unsigned_ints!(J; u8, u16, u32, u64, u128, usize);
153        impl_try_from_signed_ratios!(J; i8, i16, i32, i64, i128, isize);
154        impl_try_from_unsigned_ratios!(J; u8, u16, u32, u64, u128, usize);
155        impl_try_from_floats!(J; f32, f64);
156        impl_from_quantum_number_for_floats!(J, doubled, 2);
157        impl_half_integer_display!(J, doubled);
158    };
159}
160
161macro_rules! impl_l_conversions {
162    () => {
163        impl_try_from_signed_ints!(L; i8, i16, i32, i64, i128, isize);
164        impl_try_from_unsigned_ints!(L; u8, u16, u32, u64, u128, usize);
165        impl_try_from_signed_ratios!(L; i8, i16, i32, i64, i128, isize);
166        impl_try_from_unsigned_ratios!(L; u8, u16, u32, u64, u128, usize);
167        impl_try_from_floats!(L; f32, f64);
168        impl_from_quantum_number_for_floats!(L, value, 1);
169    };
170}
171
172macro_rules! impl_m_conversions {
173    () => {
174        impl_try_from_signed_ints!(M; i8, i16, i32, i64, i128, isize);
175        impl_try_from_unsigned_ints!(M; u8, u16, u32, u64, u128, usize);
176        impl_try_from_signed_ratios!(M; i8, i16, i32, i64, i128, isize);
177        impl_try_from_unsigned_ratios!(M; u8, u16, u32, u64, u128, usize);
178        impl_try_from_floats!(M; f32, f64);
179        impl_from_quantum_number_for_floats!(M, doubled, 2);
180        impl_half_integer_display!(M, doubled);
181    };
182}
183
184/// Signed discrete quantum numbers such as parity and reflectivity.
185pub mod signed {
186    use super::*;
187
188    macro_rules! signed_value_type {
189        (
190            $(#[$meta:meta])*
191            $vis:vis enum $name:ident, $object:literal
192        ) => {
193            signed_value_type! {
194                @impl
195                $(#[$meta])*
196                $vis enum $name, $object;
197                i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64
198            }
199        };
200
201        (
202            @impl
203            $(#[$meta:meta])*
204            $vis:vis enum $name:ident, $object:literal;
205            $($number:ty)*
206        ) => {
207            $(#[$meta])*
208            #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
209            $vis enum $name {
210                /// A positive value.
211                Positive,
212                /// A negative value.
213                Negative,
214            }
215
216            impl $name {
217                /// Return `+1` or `-1` for this signed quantum number.
218                pub const fn value(self) -> i32 {
219                    match self {
220                        Self::Positive => 1,
221                        Self::Negative => -1,
222                    }
223                }
224            }
225
226            impl ::std::fmt::Display for $name {
227                fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
228                    match self {
229                        Self::Positive => write!(f, "+"),
230                        Self::Negative => write!(f, "-"),
231                    }
232                }
233            }
234
235            impl ::std::str::FromStr for $name {
236                type Err = LadduPhysicsError;
237
238                fn from_str(s: &str) -> Result<Self, Self::Err> {
239                    match parse_sign_value(s, $object)? {
240                        Sign::Positive => Ok(Self::Positive),
241                        Sign::Negative => Ok(Self::Negative),
242                    }
243                }
244            }
245
246            impl_try_from_str!($name);
247
248            $(
249                impl From<$name> for $number {
250                    fn from(value: $name) -> Self {
251                        value.value() as $number
252                    }
253                }
254            )*
255
256            impl_op_ex!(* |p1: &$name, p2: &$name| -> $name {
257                match (p1, p2) {
258                    ($name::Positive, $name::Positive)
259                    | ($name::Negative, $name::Negative) => $name::Positive,
260
261                    ($name::Positive, $name::Negative)
262                    | ($name::Negative, $name::Positive) => $name::Negative,
263                }
264            });
265
266            impl_op_ex!(*= |p1: &mut $name, p2: &$name| {
267                *p1 = *p1 * p2
268            });
269
270            impl_op_ex!(- |p: &$name| -> $name {
271                match p {
272                    $name::Positive => $name::Negative,
273                    $name::Negative => $name::Positive,
274                }
275            });
276        };
277    }
278
279    signed_value_type! {
280        /// A helper enum denoting the sign of a state.
281        pub enum Sign, "Sign"
282    }
283
284    signed_value_type! {
285        /// An enum describing the reflectivity of a state.
286        pub enum Reflectivity, "Reflectivity"
287    }
288
289    signed_value_type! {
290        /// An enum describing the parity (or G/C-parity) of a state.
291        pub enum Parity, "Parity"
292    }
293
294    fn parse_sign_value(s: &str, object: &str) -> Result<Sign, LadduPhysicsError> {
295        match s.to_lowercase().as_ref() {
296            "+" | "plus" | "pos" | "positive" => Ok(Sign::Positive),
297            "-" | "minus" | "neg" | "negative" => Ok(Sign::Negative),
298            _ => Err(LadduPhysicsError::ParseError {
299                name: s.to_string(),
300                object: object.to_string(),
301            }),
302        }
303    }
304
305    impl From<Reflectivity> for Sign {
306        fn from(value: Reflectivity) -> Self {
307            match value {
308                Reflectivity::Positive => Self::Positive,
309                Reflectivity::Negative => Self::Negative,
310            }
311        }
312    }
313
314    impl From<Sign> for Reflectivity {
315        fn from(value: Sign) -> Self {
316            match value {
317                Sign::Positive => Self::Positive,
318                Sign::Negative => Self::Negative,
319            }
320        }
321    }
322
323    impl From<Parity> for Sign {
324        fn from(value: Parity) -> Self {
325            match value {
326                Parity::Positive => Self::Positive,
327                Parity::Negative => Self::Negative,
328            }
329        }
330    }
331
332    impl From<Sign> for Parity {
333        fn from(value: Sign) -> Self {
334            match value {
335                Sign::Positive => Self::Positive,
336                Sign::Negative => Self::Negative,
337            }
338        }
339    }
340}
341pub use signed::*;
342
343/// A non-negative total angular momentum stored as twice its physical value.
344///
345/// This representation keeps integer and half-integer quantum numbers exact. For example,
346/// `J::half(1)` represents `$1/2$`, and `J::int(1)` represents `$1$`.
347#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
348pub struct J(u32);
349
350impl J {
351    /// Construct a non-negative integer angular momentum.
352    pub const fn int(value: u32) -> Self {
353        Self(2 * value)
354    }
355
356    /// Construct a non-negative angular momentum from the given numerator over two.
357    pub const fn half(value: u32) -> Self {
358        Self(value)
359    }
360
361    /// Enumerate the valid signed projections for this angular momentum.
362    pub fn projections(self) -> Vec<M> {
363        let twice = self.0 as i32;
364        (-twice..=twice).step_by(2).map(M::half).collect()
365    }
366
367    /// Return the doubled integer value.
368    pub const fn doubled(self) -> u32 {
369        self.0
370    }
371
372    /// Return the number of projections, $2J + 1$.
373    pub const fn multiplicity(self) -> u32 {
374        self.0 + 1
375    }
376
377    /// Return whether this quantum number represents an integer value.
378    pub const fn is_integer(self) -> bool {
379        self.0 & 1 == 0
380    }
381
382    /// Enumerate the total angular momenta obtainable by coupling this value to `other`.
383    ///
384    /// Results run from $\lvert J_1 - J_2\rvert$ through $J_1 + J_2$ in unit steps.
385    pub fn coupled_with(self, other: Self) -> Vec<Self> {
386        let min = self.doubled().abs_diff(other.doubled());
387        let max = self.doubled() + other.doubled();
388        (min..=max).step_by(2).map(Self::half).collect()
389    }
390
391    /// Return whether this angular momentum has the same integer/half-integer parity as
392    /// `projection`.
393    pub(crate) const fn has_same_parity_as(self, projection: M) -> bool {
394        (self.0 & 1) as i32 == projection.doubled() & 1
395    }
396
397    /// Return whether `j1` and `j2` can couple to produce this angular momentum.
398    pub fn can_couple_to(self, j1: Self, j2: Self) -> bool {
399        let min = j1.doubled().abs_diff(j2.doubled());
400        let max = j1.doubled() + j2.doubled();
401        self.doubled() >= min && self.doubled() <= max && (self.doubled() - min).is_multiple_of(2)
402    }
403
404    fn try_from_i128(value: i128) -> Result<Self, LadduPhysicsError> {
405        if value < 0 {
406            return Err(LadduPhysicsError::invalid_value(
407                "angular momentum",
408                "nonnegative",
409                value,
410            ));
411        }
412        Self::try_from_scaled_i128(value.checked_mul(2).ok_or_else(|| {
413            LadduPhysicsError::numeric_overflow(format!("2 * angular momentum for value {value}"))
414        })?)
415    }
416
417    fn try_from_u128(value: u128) -> Result<Self, LadduPhysicsError> {
418        let value = i128::try_from(value).map_err(|_| {
419            LadduPhysicsError::invalid_value("angular momentum", "representable as i128", value)
420        })?;
421        Self::try_from_i128(value)
422    }
423
424    fn try_from_ratio_i128(numer: i128, denom: i128) -> Result<Self, LadduPhysicsError> {
425        let scaled = numer.checked_mul(2).ok_or_else(|| {
426            LadduPhysicsError::numeric_overflow(format!("2 * angular momentum numerator {numer}"))
427        })?;
428        if scaled % denom != 0 {
429            return Err(LadduPhysicsError::invalid_value(
430                "angular momentum",
431                "integer or half-integer",
432                format!("{numer}/{denom}"),
433            ));
434        }
435        Self::try_from_scaled_i128(scaled / denom)
436    }
437
438    fn try_from_f64(value: f64) -> Result<Self, LadduPhysicsError> {
439        if !value.is_finite() {
440            return Err(LadduPhysicsError::invalid_value(
441                "angular momentum",
442                "finite",
443                value,
444            ));
445        }
446        let scaled = 2.0 * value;
447        let rounded = scaled.round();
448        if (scaled - rounded).abs() > QUANTUM_NUMBER_FLOAT_TOLERANCE {
449            return Err(LadduPhysicsError::invalid_value(
450                "angular momentum",
451                "integer or half-integer",
452                value,
453            ));
454        }
455        if rounded < i128::MIN as f64 || rounded > i128::MAX as f64 {
456            return Err(LadduPhysicsError::invalid_value(
457                "angular momentum",
458                "representable as i128",
459                rounded,
460            ));
461        }
462        Self::try_from_scaled_i128(rounded as i128)
463    }
464
465    fn try_from_scaled_i128(value: i128) -> Result<Self, LadduPhysicsError> {
466        Ok(Self(u32::try_from(value).map_err(|_| {
467            LadduPhysicsError::invalid_value(
468                "angular momentum",
469                "nonnegative and representable as doubled u32",
470                value,
471            )
472        })?))
473    }
474}
475
476impl From<L> for J {
477    fn from(value: L) -> Self {
478        Self::int(value.0)
479    }
480}
481
482impl_j_conversions!();
483
484/// Coupled intrinsic spin, represented by the same quantum-number type as total angular momentum.
485pub type S = J;
486
487/// A non-negative integer orbital angular momentum.
488#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
489pub struct L(u32);
490
491impl L {
492    /// Construct an orbital angular momentum.
493    pub const fn int(value: u32) -> Self {
494        Self(value)
495    }
496
497    /// Return the integer orbital angular momentum.
498    pub const fn value(self) -> u32 {
499        self.0
500    }
501
502    /// Return the number of projections, $2L + 1$.
503    pub const fn multiplicity(self) -> u32 {
504        2 * self.0 + 1
505    }
506
507    /// Enumerate the valid signed projections for this orbital angular momentum.
508    pub fn projections(self) -> Vec<M> {
509        J::int(self.0).projections()
510    }
511
512    /// Returns the parity derived from the orbital angular momentum, $`(-1)^{\ell}`$
513    pub const fn orbital_parity(self) -> Parity {
514        if self.value().is_multiple_of(2) {
515            Parity::Positive
516        } else {
517            Parity::Negative
518        }
519    }
520
521    fn try_from_i128(value: i128) -> Result<Self, LadduPhysicsError> {
522        if value < 0 {
523            return Err(LadduPhysicsError::invalid_value(
524                "orbital angular momentum",
525                "nonnegative",
526                value,
527            ));
528        }
529        Self::try_from_scaled_i128(value)
530    }
531
532    fn try_from_u128(value: u128) -> Result<Self, LadduPhysicsError> {
533        let value = i128::try_from(value).map_err(|_| {
534            LadduPhysicsError::invalid_value(
535                "orbital angular momentum",
536                "representable as i128",
537                value,
538            )
539        })?;
540        Self::try_from_i128(value)
541    }
542
543    fn try_from_ratio_i128(numer: i128, denom: i128) -> Result<Self, LadduPhysicsError> {
544        if numer % denom != 0 {
545            return Err(LadduPhysicsError::invalid_value(
546                "orbital angular momentum",
547                "integer",
548                format!("{numer}/{denom}"),
549            ));
550        }
551        Self::try_from_scaled_i128(numer / denom)
552    }
553
554    fn try_from_f64(value: f64) -> Result<Self, LadduPhysicsError> {
555        if !value.is_finite() {
556            return Err(LadduPhysicsError::invalid_value(
557                "orbital angular momentum",
558                "finite",
559                value,
560            ));
561        }
562        let rounded = value.round();
563        if (value - rounded).abs() > QUANTUM_NUMBER_FLOAT_TOLERANCE {
564            return Err(LadduPhysicsError::invalid_value(
565                "orbital angular momentum",
566                "integer",
567                value,
568            ));
569        }
570        if rounded < i128::MIN as f64 || rounded > i128::MAX as f64 {
571            return Err(LadduPhysicsError::invalid_value(
572                "orbital angular momentum",
573                "representable as i128",
574                rounded,
575            ));
576        }
577        Self::try_from_scaled_i128(rounded as i128)
578    }
579
580    fn try_from_scaled_i128(value: i128) -> Result<Self, LadduPhysicsError> {
581        Ok(Self(u32::try_from(value).map_err(|_| {
582            LadduPhysicsError::invalid_value(
583                "orbital angular momentum",
584                "nonnegative and representable as u32",
585                value,
586            )
587        })?))
588    }
589}
590
591impl TryFrom<J> for L {
592    type Error = LadduPhysicsError;
593
594    fn try_from(value: J) -> Result<Self, Self::Error> {
595        if !value.is_integer() {
596            return Err(LadduPhysicsError::invalid_value(
597                "orbital angular momentum",
598                "integer",
599                value,
600            ));
601        }
602        Ok(Self::int(value.doubled() / 2))
603    }
604}
605
606impl_l_conversions!();
607
608impl Display for L {
609    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
610        write!(
611            f,
612            "{}",
613            match self.value() {
614                0 => "S".to_string(),
615                1 => "P".to_string(),
616                2 => "D".to_string(),
617                3 => "F".to_string(),
618                4 => "G".to_string(),
619                5 => "H".to_string(),
620                6 => "I".to_string(),
621                n => format!("L{n}"),
622            }
623        )
624    }
625}
626
627/// A signed integer or half-integer projection stored as twice its physical value.
628#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
629pub struct M(i32);
630
631impl M {
632    /// Construct a signed integer projection.
633    pub const fn int(value: i32) -> Self {
634        Self(2 * value)
635    }
636
637    /// Construct a signed projection from the given numerator over two.
638    pub const fn half(value: i32) -> Self {
639        Self(value)
640    }
641
642    /// Return the doubled integer value.
643    pub const fn doubled(self) -> i32 {
644        self.0
645    }
646
647    /// Return whether this projection represents an integer value.
648    pub const fn is_integer(self) -> bool {
649        self.0 & 1 == 0
650    }
651
652    fn try_from_i128(value: i128) -> Result<Self, LadduPhysicsError> {
653        Self::try_from_scaled_i128(value.checked_mul(2).ok_or_else(|| {
654            LadduPhysicsError::numeric_overflow(format!("2 * projection for value {value}"))
655        })?)
656    }
657
658    fn try_from_u128(value: u128) -> Result<Self, LadduPhysicsError> {
659        let value = i128::try_from(value).map_err(|_| {
660            LadduPhysicsError::invalid_value("projection", "representable as i128", value)
661        })?;
662        Self::try_from_i128(value)
663    }
664
665    fn try_from_ratio_i128(numer: i128, denom: i128) -> Result<Self, LadduPhysicsError> {
666        let scaled = numer.checked_mul(2).ok_or_else(|| {
667            LadduPhysicsError::numeric_overflow(format!("2 * projection numerator {numer}"))
668        })?;
669        if scaled % denom != 0 {
670            return Err(LadduPhysicsError::invalid_value(
671                "projection",
672                "integer or half-integer",
673                format!("{numer}/{denom}"),
674            ));
675        }
676        Self::try_from_scaled_i128(scaled / denom)
677    }
678
679    fn try_from_f64(value: f64) -> Result<Self, LadduPhysicsError> {
680        if !value.is_finite() {
681            return Err(LadduPhysicsError::Custom(
682                "projection must be finite".to_string(),
683            ));
684        }
685        let scaled = 2.0 * value;
686        let rounded = scaled.round();
687        if (scaled - rounded).abs() > QUANTUM_NUMBER_FLOAT_TOLERANCE {
688            return Err(LadduPhysicsError::invalid_value(
689                "projection",
690                "integer or half-integer",
691                value,
692            ));
693        }
694        if rounded < i128::MIN as f64 || rounded > i128::MAX as f64 {
695            return Err(LadduPhysicsError::invalid_value(
696                "projection",
697                "representable as i128",
698                rounded,
699            ));
700        }
701        Self::try_from_scaled_i128(rounded as i128)
702    }
703
704    fn try_from_scaled_i128(value: i128) -> Result<Self, LadduPhysicsError> {
705        Ok(Self(i32::try_from(value).map_err(|_| {
706            LadduPhysicsError::invalid_value("projection", "representable as i32", value)
707        })?))
708    }
709}
710
711impl_m_conversions!();
712
713// Operations
714#[rustfmt::skip]
715impl_op_ex!(+ |j1: &J, j2: &J| -> J { J::half(j1.doubled() + j2.doubled()) });
716#[rustfmt::skip]
717impl_op_ex!(+ |m1: &M, m2: &M| -> M { M::half(m1.doubled() + m2.doubled()) });
718#[rustfmt::skip]
719impl_op_ex!(- |m1: &M, m2: &M| -> M { M::half(m1.doubled() - m2.doubled()) });
720#[rustfmt::skip]
721impl_op_ex!(- |m: &M| -> M { M::half(-m.doubled()) });
722#[rustfmt::skip]
723impl_op_ex!(+= |m1: &mut M, m2: &M| { *m1 = *m1 + m2 });
724#[rustfmt::skip]
725impl_op_ex!(-= |m1: &mut M, m2: &M| { *m1 = *m1 - m2 });
726
727/// An enum representing the statistics of a particle.
728#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
729pub enum Statistics {
730    /// Variant for bosonic statistics
731    Boson,
732    /// Variant for fermionic statistics
733    Fermion,
734}
735
736impl Statistics {
737    /// Construct quantum statistics from a spin value.
738    pub fn from_spin(spin: J) -> Self {
739        if spin.is_integer() {
740            Self::Boson
741        } else {
742            Self::Fermion
743        }
744    }
745}
746
747impl FromStr for Statistics {
748    type Err = LadduPhysicsError;
749
750    fn from_str(s: &str) -> Result<Self, Self::Err> {
751        match s.to_lowercase().as_ref() {
752            "fermion" => Ok(Self::Fermion),
753            "boson" => Ok(Self::Boson),
754            _ => Err(LadduPhysicsError::ParseError {
755                name: s.to_string(),
756                object: "Statistics".to_string(),
757            }),
758        }
759    }
760}
761
762impl_try_from_str!(Statistics);
763
764/// An enum for Mandelstam variables.
765#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
766pub enum MandelstamChannel {
767    /// s-channel
768    S,
769    /// t-channel
770    T,
771    /// u-channel
772    U,
773}
774
775impl Display for MandelstamChannel {
776    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
777        match self {
778            MandelstamChannel::S => write!(f, "s"),
779            MandelstamChannel::T => write!(f, "t"),
780            MandelstamChannel::U => write!(f, "u"),
781        }
782    }
783}
784
785impl FromStr for MandelstamChannel {
786    type Err = LadduPhysicsError;
787
788    fn from_str(s: &str) -> Result<Self, Self::Err> {
789        match s.to_lowercase().as_ref() {
790            "s" => Ok(Self::S),
791            "t" => Ok(Self::T),
792            "u" => Ok(Self::U),
793            _ => Err(LadduPhysicsError::ParseError {
794                name: s.to_string(),
795                object: "MandelstamChannel".to_string(),
796            }),
797        }
798    }
799}
800
801impl_try_from_str!(MandelstamChannel);
802
803#[cfg(test)]
804mod tests {
805    use super::*;
806
807    #[test]
808    fn orbital_angular_momentum_rejects_half_integer_values() {
809        assert_eq!(L::try_from(Ratio::new(2, 1)).unwrap().value(), 2);
810        assert!(L::try_from(Ratio::new(3, 2)).is_err());
811    }
812
813    #[test]
814    fn angular_momentum_accepts_ratio_and_float_physical_values() {
815        assert_eq!(J::try_from(Ratio::new(3, 2)).unwrap().doubled(), 3);
816        assert_eq!(J::try_from(1.5).unwrap().doubled(), 3);
817        assert_eq!(M::try_from(Ratio::new(-1, 2)).unwrap().doubled(), -1);
818        assert_eq!(M::try_from(-0.5).unwrap().doubled(), -1);
819        assert!(J::try_from(Ratio::new(1, 3)).is_err());
820        assert!(M::try_from(0.25).is_err());
821    }
822
823    #[test]
824    fn orbital_angular_momentum_accepts_integer_ratio_and_float_values() {
825        assert_eq!(L::try_from(Ratio::new(2, 1)).unwrap().value(), 2);
826        assert_eq!(L::try_from(2.0).unwrap().value(), 2);
827        assert!(L::try_from(Ratio::new(3, 2)).is_err());
828        assert!(L::try_from(1.5).is_err());
829    }
830
831    #[test]
832    fn parity_returns_signed_value() {
833        assert_eq!(Parity::Positive.value(), 1);
834        assert_eq!(Parity::Negative.value(), -1);
835    }
836
837    #[test]
838    fn quantum_numbers_convert_to_floats() {
839        assert_eq!(f64::from(J::half(3)), 1.5);
840        assert_eq!(f32::from(L::int(2)), 2.0);
841        assert_eq!(f64::from(M::half(-1)), -0.5);
842    }
843
844    #[test]
845    fn angular_momenta_add_and_report_multiplicities() {
846        assert_eq!(J::int(1) + J::half(1), J::half(3));
847        assert_eq!(J::half(3).multiplicity(), 4);
848        assert_eq!(L::int(2).multiplicity(), 5);
849    }
850
851    #[test]
852    fn angular_momenta_enumerate_and_validate_couplings() {
853        assert_eq!(
854            J::half(1).coupled_with(J::int(1)),
855            vec![J::half(1), J::half(3)]
856        );
857        assert!(J::half(3).can_couple_to(J::half(1), J::int(1)));
858        assert!(!J::int(0).can_couple_to(J::half(1), J::int(1)));
859    }
860
861    #[test]
862    fn quantum_numbers_accept_more_numeric_inputs() {
863        assert_eq!(J::try_from(2_u8).unwrap().doubled(), 4);
864        assert_eq!(L::try_from(2_u16).unwrap().value(), 2);
865        assert_eq!(M::try_from(-2_i8).unwrap().doubled(), -4);
866        assert!(J::try_from(-1_i8).is_err());
867        assert!(L::try_from(-1_i8).is_err());
868    }
869}