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
184pub 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 Positive,
212 Negative,
214 }
215
216 impl $name {
217 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 pub enum Sign, "Sign"
282 }
283
284 signed_value_type! {
285 pub enum Reflectivity, "Reflectivity"
287 }
288
289 signed_value_type! {
290 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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
348pub struct J(u32);
349
350impl J {
351 pub const fn int(value: u32) -> Self {
353 Self(2 * value)
354 }
355
356 pub const fn half(value: u32) -> Self {
358 Self(value)
359 }
360
361 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 pub const fn doubled(self) -> u32 {
369 self.0
370 }
371
372 pub const fn multiplicity(self) -> u32 {
374 self.0 + 1
375 }
376
377 pub const fn is_integer(self) -> bool {
379 self.0 & 1 == 0
380 }
381
382 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 pub(crate) const fn has_same_parity_as(self, projection: M) -> bool {
394 (self.0 & 1) as i32 == projection.doubled() & 1
395 }
396
397 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
484pub type S = J;
486
487#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
489pub struct L(u32);
490
491impl L {
492 pub const fn int(value: u32) -> Self {
494 Self(value)
495 }
496
497 pub const fn value(self) -> u32 {
499 self.0
500 }
501
502 pub const fn multiplicity(self) -> u32 {
504 2 * self.0 + 1
505 }
506
507 pub fn projections(self) -> Vec<M> {
509 J::int(self.0).projections()
510 }
511
512 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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
629pub struct M(i32);
630
631impl M {
632 pub const fn int(value: i32) -> Self {
634 Self(2 * value)
635 }
636
637 pub const fn half(value: i32) -> Self {
639 Self(value)
640 }
641
642 pub const fn doubled(self) -> i32 {
644 self.0
645 }
646
647 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#[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#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
729pub enum Statistics {
730 Boson,
732 Fermion,
734}
735
736impl Statistics {
737 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#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
766pub enum MandelstamChannel {
767 S,
769 T,
771 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}