Skip to main content

laddu_core/quantum/
state.rs

1use std::fmt::Display;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    quantum::types::Statistics, AngularMomentum, Charge, LadduError, LadduResult,
7    OrbitalAngularMomentum, Parity, Projection,
8};
9
10/// A validated spin state with spin and projection stored as doubled quantum numbers.
11#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
12pub struct SpinState {
13    spin: AngularMomentum,
14    projection: Projection,
15}
16
17impl SpinState {
18    /// Construct a spin state after validating projection bounds and parity.
19    pub fn new(spin: AngularMomentum, projection: Projection) -> LadduResult<Self> {
20        validate_projection(spin, projection)?;
21        Ok(Self { spin, projection })
22    }
23
24    /// Return the spin quantum number.
25    pub const fn spin(self) -> AngularMomentum {
26        self.spin
27    }
28
29    /// Return the spin projection quantum number.
30    pub const fn projection(self) -> Projection {
31        self.projection
32    }
33
34    /// Enumerate all allowed projections for `spin`.
35    pub fn allowed_projections(spin: AngularMomentum) -> Vec<Self> {
36        let spin_value = spin.value() as i32;
37        (-spin_value..=spin_value)
38            .step_by(2)
39            .map(|projection| Self {
40                spin,
41                projection: Projection::half_integer(projection),
42            })
43            .collect()
44    }
45}
46
47/// An isospin state with optional projection.
48#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
49pub struct Isospin {
50    /// The total isospin of the state.
51    pub isospin: AngularMomentum,
52    /// The isospin projection of the state.
53    pub projection: Option<Projection>,
54}
55
56impl Isospin {
57    /// Construct a new isospin state from the given total isospin and optional projection.
58    pub fn new(isospin: AngularMomentum, projection: Option<Projection>) -> LadduResult<Self> {
59        if let Some(projection) = projection {
60            validate_projection(isospin, projection)?;
61        }
62        Ok(Self {
63            isospin,
64            projection,
65        })
66    }
67    /// The total isospin of the state.
68    pub fn isospin(self) -> AngularMomentum {
69        self.isospin
70    }
71    /// The isospin projection of the state.
72    ///
73    /// Returns an error if this property is not known.
74    pub fn projection(self) -> LadduResult<Projection> {
75        self.projection
76            .ok_or_else(|| LadduError::MissingParticleProperty {
77                property: "isospin.projection",
78            })
79    }
80}
81
82/// The set of properties which define the quantum state of a particle.
83#[derive(Clone, Debug, Default, Serialize, Deserialize)]
84pub struct ParticleProperties {
85    /// The name of the particle, if known.
86    pub name: Option<String>,
87    /// The species of the particle, if known (used to compare to [`ParticleProperties::antiparticle_species`]).
88    pub species: Option<String>,
89    /// The species of the particle's antiparticle, if known (used to compare to [`ParticleProperties::species`]).
90    pub antiparticle_species: Option<String>,
91    /// Whether the particle is its own antiparticle.
92    pub self_conjugate: Option<bool>,
93    /// The spin of the particle, if known.
94    pub spin: Option<AngularMomentum>,
95    /// The intrinsic parity of the particle, if known.
96    pub parity: Option<Parity>,
97    /// The intrinsic C-parity of the particle, if known or applicable.
98    pub c_parity: Option<Parity>,
99    /// The intrinsic G-parity of the particle, if known or applicable.
100    pub g_parity: Option<Parity>,
101    /// The electric charge of the particle, if known.
102    pub charge: Option<Charge>,
103    /// The isospin of the particle, if known.
104    pub isospin: Option<Isospin>,
105    /// The total strangeness of the particle, if known.
106    pub strangeness: Option<i32>,
107    /// The total charm of the particle, if known.
108    pub charm: Option<i32>,
109    /// The total bottomness of the particle, if known.
110    pub bottomness: Option<i32>,
111    /// The total topness of the particle, if known.
112    pub topness: Option<i32>,
113    /// The total baryon number of the particle, if known.
114    pub baryon_number: Option<i32>,
115    /// The electron lepton number of the particle, if known.
116    pub electron_lepton_number: Option<i32>,
117    /// The muon lepton number of the particle, if known.
118    pub muon_lepton_number: Option<i32>,
119    /// The tau lepton number of the particle, if known.
120    pub tau_lepton_number: Option<i32>,
121    /// The particle's statistical nature, if known.
122    pub statistics: Option<Statistics>,
123}
124
125impl ParticleProperties {
126    /// Get the particle's name
127    ///
128    /// Returns an error if this property is not known.
129    pub fn name(&self) -> LadduResult<String> {
130        self.name
131            .clone()
132            .ok_or_else(|| LadduError::MissingParticleProperty { property: "name" })
133            .clone()
134    }
135    /// Get the particle's species
136    ///
137    /// Returns an error if this property is not known.
138    pub fn species(&self) -> LadduResult<String> {
139        self.species
140            .clone()
141            .ok_or_else(|| LadduError::MissingParticleProperty {
142                property: "species",
143            })
144            .clone()
145    }
146    /// Get the particle's antiparticle species
147    ///
148    /// Returns an error if this property is not known.
149    pub fn antiparticle_species(&self) -> LadduResult<String> {
150        self.antiparticle_species
151            .clone()
152            .ok_or_else(|| LadduError::MissingParticleProperty {
153                property: "antiparticle_species",
154            })
155            .clone()
156    }
157    /// Get the particle's self-conjugate status
158    ///
159    /// Returns an error if this property is not known.
160    pub fn self_conjugate(&self) -> LadduResult<bool> {
161        self.self_conjugate
162            .ok_or_else(|| LadduError::MissingParticleProperty {
163                property: "self_conjugate",
164            })
165            .clone()
166    }
167    /// Get the particle's spin
168    ///
169    /// Returns an error if this property is not known.
170    pub fn spin(&self) -> LadduResult<AngularMomentum> {
171        self.spin
172            .ok_or_else(|| LadduError::MissingParticleProperty { property: "spin" })
173            .clone()
174    }
175    /// Get the particle's intrinsic parity
176    ///
177    /// Returns an error if this property is not known.
178    pub fn parity(&self) -> LadduResult<Parity> {
179        self.parity
180            .ok_or_else(|| LadduError::MissingParticleProperty { property: "parity" })
181            .clone()
182    }
183    /// Get the particle's intrinsic C-parity
184    ///
185    /// Returns an error if this property is not known.
186    pub fn c_parity(&self) -> LadduResult<Parity> {
187        self.c_parity
188            .ok_or_else(|| LadduError::MissingParticleProperty {
189                property: "c_parity",
190            })
191            .clone()
192    }
193    /// Get the particle's intrinsic G-parity
194    ///
195    /// Returns an error if this property is not known.
196    pub fn g_parity(&self) -> LadduResult<Parity> {
197        self.g_parity
198            .ok_or_else(|| LadduError::MissingParticleProperty {
199                property: "g_parity",
200            })
201            .clone()
202    }
203    /// Get the particle's electric charge
204    ///
205    /// Returns an error if this property is not known.
206    pub fn charge(&self) -> LadduResult<Charge> {
207        self.charge
208            .ok_or_else(|| LadduError::MissingParticleProperty { property: "charge" })
209            .clone()
210    }
211    /// Get the particle's isospin
212    ///
213    /// Returns an error if this property is not known.
214    pub fn isospin(&self) -> LadduResult<Isospin> {
215        self.isospin
216            .ok_or_else(|| LadduError::MissingParticleProperty {
217                property: "isospin",
218            })
219            .clone()
220    }
221    /// Get the particle's strangeness
222    ///
223    /// Returns an error if this property is not known.
224    pub fn strangeness(&self) -> LadduResult<i32> {
225        self.strangeness
226            .ok_or_else(|| LadduError::MissingParticleProperty {
227                property: "strangeness",
228            })
229            .clone()
230    }
231    /// Get the particle's charm
232    ///
233    /// Returns an error if this property is not known.
234    pub fn charm(&self) -> LadduResult<i32> {
235        self.charm
236            .ok_or_else(|| LadduError::MissingParticleProperty { property: "charm" })
237            .clone()
238    }
239    /// Get the particle's bottomness
240    ///
241    /// Returns an error if this property is not known.
242    pub fn bottomness(&self) -> LadduResult<i32> {
243        self.bottomness
244            .ok_or_else(|| LadduError::MissingParticleProperty {
245                property: "bottomness",
246            })
247            .clone()
248    }
249    /// Get the particle's topness
250    ///
251    /// Returns an error if this property is not known.
252    pub fn topness(&self) -> LadduResult<i32> {
253        self.topness
254            .ok_or_else(|| LadduError::MissingParticleProperty {
255                property: "topness",
256            })
257            .clone()
258    }
259    /// Get the particle's baryon number
260    ///
261    /// Returns an error if this property is not known.
262    pub fn baryon_number(&self) -> LadduResult<i32> {
263        self.baryon_number
264            .ok_or_else(|| LadduError::MissingParticleProperty {
265                property: "baryon_number",
266            })
267            .clone()
268    }
269    /// Get the particle's electron lepton number
270    ///
271    /// Returns an error if this property is not known.
272    pub fn electron_lepton_number(&self) -> LadduResult<i32> {
273        self.electron_lepton_number
274            .ok_or_else(|| LadduError::MissingParticleProperty {
275                property: "electron_lepton_number",
276            })
277            .clone()
278    }
279    /// Get the particle's muon lepton number
280    ///
281    /// Returns an error if this property is not known.
282    pub fn muon_lepton_number(&self) -> LadduResult<i32> {
283        self.muon_lepton_number
284            .ok_or_else(|| LadduError::MissingParticleProperty {
285                property: "muon_lepton_number",
286            })
287            .clone()
288    }
289    /// Get the particle's tau lepton number
290    ///
291    /// Returns an error if this property is not known.
292    pub fn tau_lepton_number(&self) -> LadduResult<i32> {
293        self.tau_lepton_number
294            .ok_or_else(|| LadduError::MissingParticleProperty {
295                property: "tau_lepton_number",
296            })
297            .clone()
298    }
299    /// Get the particle's statistics
300    ///
301    /// Returns an error if this property is not known.
302    pub fn statistics(&self) -> LadduResult<Statistics> {
303        self.statistics
304            .ok_or_else(|| LadduError::MissingParticleProperty {
305                property: "statistics",
306            })
307            .clone()
308    }
309
310    /// Construct a particle with no specified properties.
311    pub fn unknown() -> Self {
312        Self::default()
313    }
314
315    /// Construct a particle with the given spin and parity.
316    pub fn jp(j: AngularMomentum, p: Parity) -> Self {
317        Self {
318            spin: Some(j),
319            parity: Some(p),
320            statistics: Some(Statistics::from_spin(j)),
321            ..Self::default()
322        }
323    }
324    /// Construct a particle with the given spin, parity, and C-parity.
325    pub fn jpc(j: AngularMomentum, p: Parity, c: Parity) -> Self {
326        Self {
327            spin: Some(j),
328            parity: Some(p),
329            c_parity: Some(c),
330            statistics: Some(Statistics::from_spin(j)),
331            ..Self::default()
332        }
333    }
334    /// Set the particle's name.
335    pub fn with_name(mut self, name: impl Into<String>) -> Self {
336        self.name = Some(name.into());
337        self
338    }
339    /// Set the particle's species.
340    pub fn with_species(mut self, species: impl Into<String>) -> Self {
341        self.species = Some(species.into());
342        self
343    }
344    /// Set the particle's antiparticle species.
345    pub fn with_antiparticle_species(mut self, antiparticle_species: impl Into<String>) -> Self {
346        self.antiparticle_species = Some(antiparticle_species.into());
347        self
348    }
349    /// Set whether the particle is its own antiparticle.
350    pub fn with_self_conjugate(mut self, value: bool) -> Self {
351        self.self_conjugate = Some(value);
352        self
353    }
354    /// Set the particle's spin.
355    pub fn with_spin(mut self, j: AngularMomentum) -> Self {
356        self.spin = Some(j);
357        self.statistics = Some(Statistics::from_spin(j));
358        self
359    }
360    /// Set the particle's intrinsic parity.
361    pub fn with_parity(mut self, p: Parity) -> Self {
362        self.parity = Some(p);
363        self
364    }
365    /// Set the particle's intrinsic C-parity.
366    pub fn with_c_parity(mut self, c: Parity) -> Self {
367        self.c_parity = Some(c);
368        self
369    }
370    /// Set the particle's intrinsic G-parity.
371    pub fn with_g_parity(mut self, g: Parity) -> Self {
372        self.g_parity = Some(g);
373        self
374    }
375    /// Set the particle's electric charge.
376    pub fn with_charge(mut self, q: Charge) -> Self {
377        self.charge = Some(q);
378        self
379    }
380    /// Set the particle's isospin state.
381    pub fn with_isospin(mut self, isospin: Isospin) -> Self {
382        self.isospin = Some(isospin);
383        self
384    }
385    /// Set the particle's total strangeness.
386    pub fn with_strangeness(mut self, s: i32) -> Self {
387        self.strangeness = Some(s);
388        self
389    }
390    /// Set the particle's total charm.
391    pub fn with_charm(mut self, c: i32) -> Self {
392        self.charm = Some(c);
393        self
394    }
395    /// Set the particle's total bottomness.
396    pub fn with_bottomness(mut self, b: i32) -> Self {
397        self.bottomness = Some(b);
398        self
399    }
400    /// Set the particle's total topness.
401    pub fn with_topness(mut self, t: i32) -> Self {
402        self.topness = Some(t);
403        self
404    }
405    /// Set the particle's total baryon number.
406    pub fn with_baryon_number(mut self, b: i32) -> Self {
407        self.baryon_number = Some(b);
408        self
409    }
410    /// Set the particle's electron lepton number.
411    pub fn with_electron_lepton_number(mut self, e: i32) -> Self {
412        self.electron_lepton_number = Some(e);
413        self
414    }
415    /// Set the particle's muon lepton number.
416    pub fn with_muon_lepton_number(mut self, m: i32) -> Self {
417        self.muon_lepton_number = Some(m);
418        self
419    }
420    /// Set the particle's tau lepton number.
421    pub fn with_tau_lepton_number(mut self, t: i32) -> Self {
422        self.tau_lepton_number = Some(t);
423        self
424    }
425    /// Set the particle's statistical nature.
426    ///
427    /// Returns an error if the spin and statistics do not match.
428    pub fn with_statistics(mut self, s: Statistics) -> LadduResult<Self> {
429        if let Some(spin) = self.spin {
430            if Statistics::from_spin(spin) != s {
431                return Err(LadduError::Custom(
432                    "spin and statistics must be consistent".to_string(),
433                ));
434            }
435        }
436        self.statistics = Some(s);
437        Ok(self)
438    }
439
440    /// Returns true if `self` is the antiparticle of `other`.
441    pub fn is_antiparticle_of(&self, other: &ParticleProperties) -> bool {
442        let a_species = self.species.as_ref();
443        let b_species = other.species.as_ref();
444
445        let a_anti = self.antiparticle_species.as_ref();
446        let b_anti = other.antiparticle_species.as_ref();
447
448        match (a_species, b_species, a_anti, b_anti) {
449            (Some(a), Some(b), Some(a_bar), Some(b_bar)) => a_bar == b && b_bar == a,
450            (Some(_), Some(b), Some(a_bar), None) => a_bar == b,
451            (Some(a), Some(_), None, Some(b_bar)) => b_bar == a,
452            _ => false,
453        }
454    }
455}
456
457/// A partial wave defined by a total angular momentum, `J`, an orbital angular momentum, `L`, and
458/// and intrinsic spin, `S`.
459#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
460pub struct PartialWave {
461    /// The total angular momentum of the wave
462    pub j: AngularMomentum,
463    /// The orbital angular momentum of the wave
464    pub l: OrbitalAngularMomentum,
465    /// The spin of the wave
466    pub s: AngularMomentum,
467    /// The spectroscopic label of the wave
468    pub label: String,
469}
470impl PartialWave {
471    /// Construct a new partial wave from the given angular momentum quantum numbers.
472    pub fn new(
473        j: AngularMomentum,
474        l: OrbitalAngularMomentum,
475        s: AngularMomentum,
476    ) -> LadduResult<Self> {
477        PartialWave::validate_coupling(j, l, s)?;
478        let multiplicity = s.value() + 1;
479        Ok(Self {
480            j,
481            l,
482            s,
483            label: format!("{}{}{}", multiplicity, l, j),
484        })
485    }
486    /// Set the spectroscopic label of the wave.
487    pub fn with_label(mut self, label: impl Into<String>) -> Self {
488        self.label = label.into();
489        self
490    }
491    /// Validate the set of angular momentum quantum numbers which define a partial wave.
492    pub fn validate_coupling(
493        j: AngularMomentum,
494        l: OrbitalAngularMomentum,
495        s: AngularMomentum,
496    ) -> LadduResult<()> {
497        let l_twice = 2 * l.value();
498        let s_twice = s.value();
499        let j_twice = j.value();
500        let min = l_twice.abs_diff(s_twice);
501        let max = l_twice + s_twice;
502        if j_twice >= min && j_twice <= max && (j_twice - min).is_multiple_of(2) {
503            Ok(())
504        } else {
505            Err(LadduError::Custom(
506                "j, l, and s must be compatible".to_string(),
507            ))
508        }
509    }
510}
511
512impl Display for PartialWave {
513    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
514        write!(f, "{}", self.label)
515    }
516}
517
518/// A partial wave together with allowed parity and C-parity, if applicable.
519#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
520pub struct AllowedPartialWave {
521    /// The angular quantum numbers of the wave
522    pub wave: PartialWave,
523    /// The allowed parity, if applicable
524    pub parity: Option<Parity>,
525    /// The allowed C-parity, if applicable
526    pub c_parity: Option<Parity>,
527}
528
529impl AllowedPartialWave {
530    /// Take an existing [`PartialWave`] and infer parity and C-parity from its decay products.
531    pub fn new(wave: PartialWave, daughters: (&ParticleProperties, &ParticleProperties)) -> Self {
532        Self {
533            parity: Self::infer_parity(daughters, wave.l),
534            c_parity: Self::infer_c_parity(daughters, wave.l, wave.s),
535            wave,
536        }
537    }
538
539    /// Infer the parity of a state given the parity of its decay products and its orbital angular momentum.
540    pub fn infer_parity(
541        daughters: (&ParticleProperties, &ParticleProperties),
542        l: OrbitalAngularMomentum,
543    ) -> Option<Parity> {
544        let p_a = daughters.0.parity?;
545        let p_b = daughters.1.parity?;
546
547        let value = p_a.value() * p_b.value() * if l.value() & 1 == 0 { 1 } else { -1 };
548
549        Some(if value == 1 {
550            Parity::Positive
551        } else {
552            Parity::Negative
553        })
554    }
555
556    /// Infer the C-parity of a state given the species of its decay products, its orbital angular momentum, and its intrinsic spin.
557    pub fn infer_c_parity(
558        daughters: (&ParticleProperties, &ParticleProperties),
559        l: OrbitalAngularMomentum,
560        s: AngularMomentum,
561    ) -> Option<Parity> {
562        if !daughters.0.is_antiparticle_of(daughters.1) {
563            return None;
564        }
565
566        let exp_twice = 2 * l.value() + s.value();
567
568        if !exp_twice.is_multiple_of(2) {
569            return None;
570        }
571
572        Some(if (exp_twice / 2).is_multiple_of(2) {
573            Parity::Positive
574        } else {
575            Parity::Negative
576        })
577    }
578}
579
580fn validate_projection(spin: AngularMomentum, projection: Projection) -> LadduResult<()> {
581    if projection.value().unsigned_abs() > spin.value() {
582        return Err(LadduError::Custom(
583            "spin projection must satisfy -J <= m <= J".to_string(),
584        ));
585    }
586    if !spin.has_same_parity_as(projection) {
587        return Err(LadduError::Custom(
588            "spin projection must have the same integer or half-integer parity as spin".to_string(),
589        ));
590    }
591    Ok(())
592}