1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3
4use crate::{
5 LadduPhysicsError, LadduPhysicsResult,
6 quantum::{J, L, M, Parity, Statistics},
7};
8
9#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
11pub enum ExternalId {
12 Code {
14 value: i64,
16 },
17 Label {
19 value: String,
21 },
22}
23
24impl From<&str> for ExternalId {
25 fn from(value: &str) -> Self {
26 Self::label(value)
27 }
28}
29impl From<String> for ExternalId {
30 fn from(value: String) -> Self {
31 Self::label(value)
32 }
33}
34impl From<&String> for ExternalId {
35 fn from(value: &String) -> Self {
36 Self::label(value)
37 }
38}
39impl From<i64> for ExternalId {
40 fn from(value: i64) -> Self {
41 Self::code(value)
42 }
43}
44
45impl ExternalId {
46 pub fn code(value: i64) -> Self {
48 Self::Code { value }
49 }
50
51 pub fn label(value: impl Into<String>) -> Self {
53 Self::Label {
54 value: value.into(),
55 }
56 }
57
58 pub fn code_value(&self) -> Option<i64> {
60 match self {
61 Self::Code { value, .. } => Some(*value),
62 Self::Label { .. } => None,
63 }
64 }
65
66 pub fn label_value(&self) -> Option<&str> {
68 match self {
69 Self::Label { value, .. } => Some(value),
70 Self::Code { .. } => None,
71 }
72 }
73}
74
75#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
77pub struct SpinState {
78 spin: J,
79 projection: M,
80}
81
82impl SpinState {
83 pub fn new(spin: J, projection: M) -> LadduPhysicsResult<Self> {
90 validate_projection(spin, projection)?;
91 Ok(Self { spin, projection })
92 }
93
94 pub const fn spin(self) -> J {
96 self.spin
97 }
98
99 pub const fn projection(self) -> M {
101 self.projection
102 }
103}
104
105#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
107pub struct Isospin {
108 pub isospin: J,
110 pub projection: Option<M>,
112}
113
114impl Isospin {
115 pub fn new(isospin: J, projection: Option<M>) -> LadduPhysicsResult<Self> {
122 if let Some(projection) = projection {
123 validate_projection(isospin, projection)?;
124 }
125 Ok(Self {
126 isospin,
127 projection,
128 })
129 }
130
131 pub fn isospin(self) -> J {
133 self.isospin
134 }
135 pub fn projection(self) -> LadduPhysicsResult<M> {
142 self.projection
143 .ok_or(LadduPhysicsError::MissingParticleProperty {
144 property: "isospin.projection",
145 })
146 }
147}
148
149impl From<J> for Isospin {
150 fn from(value: J) -> Self {
151 Self {
152 isospin: value,
153 projection: None,
154 }
155 }
156}
157
158#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
160pub struct ParticleProperties {
161 pub name: Option<String>,
163 pub species: Option<String>,
165 pub antiparticle_species: Option<String>,
167 pub self_conjugate: Option<bool>,
169 pub spin: Option<J>,
171 pub parity: Option<Parity>,
173 pub c_parity: Option<Parity>,
175 pub g_parity: Option<Parity>,
177 pub charge: Option<i32>,
179 pub isospin: Option<Isospin>,
181 pub strangeness: Option<i32>,
183 pub charm: Option<i32>,
185 pub bottomness: Option<i32>,
187 pub topness: Option<i32>,
189 pub baryon_number: Option<i32>,
191 pub electron_lepton_number: Option<i32>,
193 pub muon_lepton_number: Option<i32>,
195 pub tau_lepton_number: Option<i32>,
197 pub statistics: Option<Statistics>,
199 pub mass: Option<f64>,
201 pub ids: IndexMap<String, ExternalId>,
203}
204
205impl ParticleProperties {
206 pub fn name(&self) -> LadduPhysicsResult<String> {
213 self.name
214 .clone()
215 .ok_or(LadduPhysicsError::MissingParticleProperty { property: "name" })
216 .clone()
217 }
218 pub fn species(&self) -> LadduPhysicsResult<String> {
225 self.species
226 .clone()
227 .ok_or(LadduPhysicsError::MissingParticleProperty {
228 property: "species",
229 })
230 .clone()
231 }
232 pub fn antiparticle_species(&self) -> LadduPhysicsResult<String> {
239 self.antiparticle_species
240 .clone()
241 .ok_or(LadduPhysicsError::MissingParticleProperty {
242 property: "antiparticle_species",
243 })
244 .clone()
245 }
246 pub fn self_conjugate(&self) -> LadduPhysicsResult<bool> {
253 self.self_conjugate
254 .ok_or(LadduPhysicsError::MissingParticleProperty {
255 property: "self_conjugate",
256 })
257 .clone()
258 }
259 pub fn spin(&self) -> LadduPhysicsResult<J> {
266 self.spin
267 .ok_or(LadduPhysicsError::MissingParticleProperty { property: "spin" })
268 .clone()
269 }
270 pub fn parity(&self) -> LadduPhysicsResult<Parity> {
277 self.parity
278 .ok_or(LadduPhysicsError::MissingParticleProperty { property: "parity" })
279 .clone()
280 }
281 pub fn c_parity(&self) -> LadduPhysicsResult<Parity> {
288 self.c_parity
289 .ok_or(LadduPhysicsError::MissingParticleProperty {
290 property: "c_parity",
291 })
292 .clone()
293 }
294 pub fn g_parity(&self) -> LadduPhysicsResult<Parity> {
301 self.g_parity
302 .ok_or(LadduPhysicsError::MissingParticleProperty {
303 property: "g_parity",
304 })
305 .clone()
306 }
307 pub fn charge(&self) -> LadduPhysicsResult<i32> {
314 self.charge
315 .ok_or(LadduPhysicsError::MissingParticleProperty { property: "charge" })
316 .clone()
317 }
318 pub fn isospin(&self) -> LadduPhysicsResult<Isospin> {
325 self.isospin
326 .ok_or(LadduPhysicsError::MissingParticleProperty {
327 property: "isospin",
328 })
329 .clone()
330 }
331 pub fn strangeness(&self) -> LadduPhysicsResult<i32> {
338 self.strangeness
339 .ok_or(LadduPhysicsError::MissingParticleProperty {
340 property: "strangeness",
341 })
342 .clone()
343 }
344 pub fn charm(&self) -> LadduPhysicsResult<i32> {
351 self.charm
352 .ok_or(LadduPhysicsError::MissingParticleProperty { property: "charm" })
353 .clone()
354 }
355 pub fn bottomness(&self) -> LadduPhysicsResult<i32> {
362 self.bottomness
363 .ok_or(LadduPhysicsError::MissingParticleProperty {
364 property: "bottomness",
365 })
366 .clone()
367 }
368 pub fn topness(&self) -> LadduPhysicsResult<i32> {
375 self.topness
376 .ok_or(LadduPhysicsError::MissingParticleProperty {
377 property: "topness",
378 })
379 .clone()
380 }
381 pub fn baryon_number(&self) -> LadduPhysicsResult<i32> {
388 self.baryon_number
389 .ok_or(LadduPhysicsError::MissingParticleProperty {
390 property: "baryon_number",
391 })
392 .clone()
393 }
394 pub fn electron_lepton_number(&self) -> LadduPhysicsResult<i32> {
401 self.electron_lepton_number
402 .ok_or(LadduPhysicsError::MissingParticleProperty {
403 property: "electron_lepton_number",
404 })
405 .clone()
406 }
407 pub fn muon_lepton_number(&self) -> LadduPhysicsResult<i32> {
414 self.muon_lepton_number
415 .ok_or(LadduPhysicsError::MissingParticleProperty {
416 property: "muon_lepton_number",
417 })
418 .clone()
419 }
420 pub fn tau_lepton_number(&self) -> LadduPhysicsResult<i32> {
427 self.tau_lepton_number
428 .ok_or(LadduPhysicsError::MissingParticleProperty {
429 property: "tau_lepton_number",
430 })
431 .clone()
432 }
433 pub fn statistics(&self) -> LadduPhysicsResult<Statistics> {
440 self.statistics
441 .ok_or(LadduPhysicsError::MissingParticleProperty {
442 property: "statistics",
443 })
444 .clone()
445 }
446 pub fn mass(&self) -> LadduPhysicsResult<f64> {
453 self.mass
454 .ok_or(LadduPhysicsError::MissingParticleProperty { property: "mass" })
455 }
456
457 pub fn unknown() -> Self {
459 Self::default()
460 }
461
462 pub fn jp(j: J, p: Parity) -> Self {
465 Self {
466 spin: Some(j),
467 parity: Some(p),
468 statistics: Some(Statistics::from_spin(j)),
469 ..Self::default()
470 }
471 }
472 pub fn jpc(j: J, p: Parity, c: Parity) -> Self {
474 Self {
475 spin: Some(j),
476 parity: Some(p),
477 c_parity: Some(c),
478 statistics: Some(Statistics::from_spin(j)),
479 ..Self::default()
480 }
481 }
482
483 pub fn boson(j: L) -> Self {
485 let mut particle = Self::unknown().with_spin(j.into());
486 particle.baryon_number = Some(0);
487 particle.electron_lepton_number = Some(0);
488 particle.muon_lepton_number = Some(0);
489 particle.tau_lepton_number = Some(0);
490 particle
491 }
492
493 pub fn lepton(e: i32, m: i32, t: i32) -> Self {
495 let mut particle = Self::unknown().with_zero_flavor();
496 particle.baryon_number = Some(0);
497 particle.electron_lepton_number = Some(e);
498 particle.muon_lepton_number = Some(m);
499 particle.tau_lepton_number = Some(t);
500 particle
501 }
502
503 pub fn hadron() -> Self {
506 Self::unknown().with_zero_lepton_numbers()
507 }
508
509 pub fn meson() -> Self {
512 let mut particle = Self::hadron();
513 particle.baryon_number = Some(0);
514 particle
515 }
516
517 pub fn baryon(b: i32) -> Self {
520 let mut particle = Self::hadron();
521 particle.baryon_number = Some(b);
522 particle
523 }
524
525 pub fn with_name(mut self, name: impl Into<String>) -> Self {
527 self.name = Some(name.into());
528 self
529 }
530 pub fn with_species(mut self, species: impl Into<String>) -> LadduPhysicsResult<Self> {
537 let species = species.into();
538
539 if self.self_conjugate == Some(true) {
540 match &self.antiparticle_species {
541 Some(anti) if anti != &species => {
542 return Err(LadduPhysicsError::invalid_relation(
543 "self-conjugate particle cannot have distinct species and antiparticle_species",
544 ));
545 }
546 None => self.antiparticle_species = Some(species.clone()),
547 _ => {}
548 }
549 }
550
551 self.species = Some(species);
552 self.check_invariants()?;
553 Ok(self)
554 }
555 pub fn with_antiparticle_species(
562 mut self,
563 antiparticle_species: impl Into<String>,
564 ) -> LadduPhysicsResult<Self> {
565 let antiparticle_species = antiparticle_species.into();
566
567 if self.self_conjugate == Some(true) {
568 match &self.species {
569 Some(species) if species != &antiparticle_species => {
570 return Err(LadduPhysicsError::invalid_relation(
571 "self-conjugate particle cannot have distinct species and antiparticle_species",
572 ));
573 }
574 None => self.species = Some(antiparticle_species.clone()),
575 _ => {}
576 }
577 }
578
579 self.antiparticle_species = Some(antiparticle_species);
580 self.check_invariants()?;
581 Ok(self)
582 }
583
584 pub fn with_species_names(
593 mut self,
594 species: impl Into<String>,
595 antiparticle_species: impl Into<String>,
596 ) -> LadduPhysicsResult<Self> {
597 let species = species.into();
598 let antiparticle_species = antiparticle_species.into();
599
600 self.species = Some(species.clone());
601 self.antiparticle_species = Some(antiparticle_species.clone());
602 self.self_conjugate = Some(species == antiparticle_species);
603
604 self.fill_zero_additive_qns_if_self_conjugate();
605 self.check_invariants()?;
606
607 Ok(self)
608 }
609
610 pub fn with_self_conjugate(mut self, value: bool) -> LadduPhysicsResult<Self> {
617 if value {
618 if let (Some(species), Some(anti)) = (&self.species, &self.antiparticle_species)
619 && species != anti
620 {
621 return Err(LadduPhysicsError::invalid_relation(
622 "self-conjugate particle cannot have distinct species and antiparticle_species",
623 ));
624 }
625 match (&self.species, &self.antiparticle_species) {
626 (Some(species), None) => self.antiparticle_species = Some(species.clone()),
627 (None, Some(anti)) => self.species = Some(anti.clone()),
628 _ => {}
629 }
630 self.fill_zero_additive_qns_if_self_conjugate();
631 } else {
632 if self.c_parity.is_some() {
633 return Err(LadduPhysicsError::invalid_relation(
634 "non-self-conjugate particles cannot have C-parity",
635 ));
636 }
637 }
638 self.self_conjugate = Some(value);
639
640 self.check_invariants()?;
641 Ok(self)
642 }
643
644 pub fn with_self_conjugate_species(
651 mut self,
652 species: impl Into<String>,
653 ) -> LadduPhysicsResult<Self> {
654 let species = species.into();
655
656 self.species = Some(species.clone());
657 self.antiparticle_species = Some(species);
658 self.self_conjugate = Some(true);
659
660 self.fill_zero_additive_qns_if_self_conjugate();
661 self.check_invariants()?;
662
663 Ok(self)
664 }
665
666 pub fn with_spin(mut self, j: J) -> Self {
668 self.spin = Some(j);
669 self.statistics = Some(Statistics::from_spin(j));
670 self
671 }
672 pub fn with_parity(mut self, p: Parity) -> Self {
674 self.parity = Some(p);
675 self
676 }
677 pub fn with_c_parity(mut self, c: Parity) -> LadduPhysicsResult<Self> {
684 if self.self_conjugate == Some(false) {
685 return Err(LadduPhysicsError::invalid_relation(
686 "C-parity is only applicable to self-conjugate particles",
687 ));
688 }
689
690 self.c_parity = Some(c);
691
692 if self.self_conjugate.is_none() {
693 self = self.with_self_conjugate(true)?;
694 }
695
696 self.check_invariants()?;
697 Ok(self)
698 }
699 pub fn with_g_parity(mut self, g: Parity) -> Self {
701 self.g_parity = Some(g);
702 self
703 }
704 pub fn with_charge(mut self, q: i32) -> Self {
706 self.charge = Some(q);
707 self
708 }
709 pub fn with_isospin(mut self, isospin: Isospin) -> Self {
711 self.isospin = Some(isospin);
712 self
713 }
714 pub fn with_strangeness(self, s: i32) -> LadduPhysicsResult<Self> {
721 self.with_additive_qn("strangeness", |p| &mut p.strangeness, s)
722 }
723 pub fn with_charm(self, c: i32) -> LadduPhysicsResult<Self> {
730 self.with_additive_qn("charm", |p| &mut p.charm, c)
731 }
732 pub fn with_bottomness(self, b: i32) -> LadduPhysicsResult<Self> {
739 self.with_additive_qn("bottomness", |p| &mut p.bottomness, b)
740 }
741 pub fn with_topness(self, t: i32) -> LadduPhysicsResult<Self> {
748 self.with_additive_qn("topness", |p| &mut p.topness, t)
749 }
750 pub fn with_flavor(self, s: i32, c: i32, b: i32, t: i32) -> LadduPhysicsResult<Self> {
757 self.with_strangeness(s)?
758 .with_charm(c)?
759 .with_bottomness(b)?
760 .with_topness(t)
761 }
762 pub fn with_baryon_number(self, b: i32) -> LadduPhysicsResult<Self> {
769 self.with_additive_qn("baryon_number", |p| &mut p.baryon_number, b)
770 }
771 pub fn with_electron_lepton_number(self, e: i32) -> LadduPhysicsResult<Self> {
778 self.with_additive_qn(
779 "electron_lepton_number",
780 |p| &mut p.electron_lepton_number,
781 e,
782 )
783 }
784 pub fn with_muon_lepton_number(self, m: i32) -> LadduPhysicsResult<Self> {
791 self.with_additive_qn("muon_lepton_number", |p| &mut p.muon_lepton_number, m)
792 }
793 pub fn with_tau_lepton_number(self, t: i32) -> LadduPhysicsResult<Self> {
800 self.with_additive_qn("tau_lepton_number", |p| &mut p.tau_lepton_number, t)
801 }
802
803 pub fn with_lepton_numbers(self, e: i32, m: i32, t: i32) -> LadduPhysicsResult<Self> {
810 self.with_electron_lepton_number(e)?
811 .with_muon_lepton_number(m)?
812 .with_tau_lepton_number(t)
813 }
814
815 pub fn with_statistics(mut self, s: Statistics) -> LadduPhysicsResult<Self> {
821 if let Some(spin) = self.spin
822 && Statistics::from_spin(spin) != s
823 {
824 return Err(LadduPhysicsError::invalid_relation(
825 "spin and statistics must be consistent",
826 ));
827 }
828 self.statistics = Some(s);
829 Ok(self)
830 }
831 pub fn with_mass(mut self, mass: f64) -> Self {
833 self.mass = Some(mass);
834 self
835 }
836
837 pub fn with_zero_flavor(mut self) -> Self {
839 self.strangeness = Some(0);
840 self.charm = Some(0);
841 self.bottomness = Some(0);
842 self.topness = Some(0);
843 self
844 }
845
846 pub fn with_zero_lepton_numbers(mut self) -> Self {
848 self.electron_lepton_number = Some(0);
849 self.muon_lepton_number = Some(0);
850 self.tau_lepton_number = Some(0);
851 self
852 }
853
854 pub fn with_zero_additive_quantum_numbers(mut self) -> LadduPhysicsResult<Self> {
861 self.charge = Some(0);
862 self.strangeness = Some(0);
863 self.charm = Some(0);
864 self.bottomness = Some(0);
865 self.topness = Some(0);
866 self.baryon_number = Some(0);
867 self.electron_lepton_number = Some(0);
868 self.muon_lepton_number = Some(0);
869 self.tau_lepton_number = Some(0);
870
871 self.check_invariants()?;
872 Ok(self)
873 }
874
875 pub fn is_antiparticle_of(&self, other: &ParticleProperties) -> bool {
877 let a_species = self.species.as_ref();
878 let b_species = other.species.as_ref();
879
880 let a_anti = self.antiparticle_species.as_ref();
881 let b_anti = other.antiparticle_species.as_ref();
882
883 match (a_species, b_species, a_anti, b_anti) {
884 (Some(a), Some(b), Some(a_bar), Some(b_bar)) => a_bar == b && b_bar == a,
885 (Some(_), Some(b), Some(a_bar), None) => a_bar == b,
886 (Some(a), Some(_), None, Some(b_bar)) => b_bar == a,
887 _ => false,
888 }
889 }
890
891 pub fn ids(&self) -> &IndexMap<String, ExternalId> {
893 &self.ids
894 }
895
896 pub fn id(&self, namespace: &str) -> Option<&ExternalId> {
898 self.ids.get(namespace)
899 }
900
901 pub fn with_id<Id: Into<ExternalId>>(mut self, namespace: &str, id: Id) -> Self {
903 self.ids.insert(namespace.to_string(), id.into());
904 self
905 }
906
907 pub fn with_ids<I, S, Id>(mut self, ids: I) -> Self
909 where
910 I: IntoIterator<Item = (S, Id)>,
911 S: AsRef<str>,
912 Id: Into<ExternalId>,
913 {
914 self.ids = ids
915 .into_iter()
916 .map(|(s, id)| (s.as_ref().to_string(), id.into()))
917 .collect();
918 self
919 }
920}
921
922impl ParticleProperties {
923 fn additive_quantum_number_fields(&self) -> [(&'static str, Option<i32>); 9] {
924 [
925 ("charge", self.charge),
926 ("strangeness", self.strangeness),
927 ("charm", self.charm),
928 ("bottomness", self.bottomness),
929 ("topness", self.topness),
930 ("baryon_number", self.baryon_number),
931 ("electron_lepton_number", self.electron_lepton_number),
932 ("muon_lepton_number", self.muon_lepton_number),
933 ("tau_lepton_number", self.tau_lepton_number),
934 ]
935 }
936
937 fn fill_zero_additive_qns_if_self_conjugate(&mut self) {
938 if self.self_conjugate == Some(true) {
939 self.charge.get_or_insert(0);
940 self.strangeness.get_or_insert(0);
941 self.charm.get_or_insert(0);
942 self.bottomness.get_or_insert(0);
943 self.topness.get_or_insert(0);
944 self.baryon_number.get_or_insert(0);
945 self.electron_lepton_number.get_or_insert(0);
946 self.muon_lepton_number.get_or_insert(0);
947 self.tau_lepton_number.get_or_insert(0);
948 }
949 }
950
951 fn check_self_conjugate_additive_qns(&self) -> LadduPhysicsResult<()> {
952 if self.self_conjugate == Some(true) {
953 for (property, value) in self.additive_quantum_number_fields() {
954 if matches!(value, Some(v) if v != 0) {
955 return Err(LadduPhysicsError::invalid_relation(format!(
956 "self-conjugate particles must have {property} = 0"
957 )));
958 }
959 }
960 }
961 Ok(())
962 }
963
964 fn check_c_parity_allowed(&self) -> LadduPhysicsResult<()> {
965 if self.c_parity.is_some() && self.self_conjugate == Some(false) {
966 return Err(LadduPhysicsError::invalid_relation(
967 "C-parity is only applicable to self-conjugate particles",
968 ));
969 }
970 Ok(())
971 }
972
973 fn check_invariants(&self) -> LadduPhysicsResult<()> {
974 self.check_self_conjugate_additive_qns()?;
975 self.check_c_parity_allowed()?;
976 Ok(())
977 }
978
979 fn with_additive_qn(
980 mut self,
981 property: &'static str,
982 field: fn(&mut Self) -> &mut Option<i32>,
983 value: i32,
984 ) -> LadduPhysicsResult<Self> {
985 if self.self_conjugate == Some(true) && value != 0 {
986 return Err(LadduPhysicsError::invalid_value(
987 property,
988 "0 for self-conjugate particles",
989 value,
990 ));
991 }
992
993 *field(&mut self) = Some(value);
994 self.check_invariants()?;
995 Ok(self)
996 }
997}
998
999fn validate_projection(spin: J, projection: M) -> LadduPhysicsResult<()> {
1000 if projection.doubled().unsigned_abs() > spin.doubled() {
1001 return Err(LadduPhysicsError::invalid_relation(format!(
1002 "spin projection must satisfy -J <= m <= J, got J = {spin}, m = {projection}"
1003 )));
1004 }
1005 if !spin.has_same_parity_as(projection) {
1006 return Err(LadduPhysicsError::invalid_relation(format!(
1007 "spin projection must have the same integer/half-integer parity as spin, got J = {spin}, m = {projection}"
1008 )));
1009 }
1010 Ok(())
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::{ExternalId, ParticleProperties};
1016
1017 #[test]
1018 fn particle_properties_store_external_ids() {
1019 let properties = ParticleProperties::unknown()
1020 .with_id("pdg", ExternalId::code(310))
1021 .with_id("gluex", ExternalId::label("ks-short"));
1022
1023 assert_eq!(properties.ids().len(), 2);
1024 assert_eq!(
1025 properties.id("pdg").and_then(ExternalId::code_value),
1026 Some(310)
1027 );
1028 assert_eq!(
1029 properties.id("gluex").and_then(ExternalId::label_value),
1030 Some("ks-short")
1031 );
1032 assert_eq!(properties.id("missing"), None);
1033
1034 let replaced = properties.with_ids([("geant", ExternalId::code(16))]);
1035 assert_eq!(replaced.ids().len(), 1);
1036 assert_eq!(
1037 replaced.id("geant").and_then(ExternalId::code_value),
1038 Some(16)
1039 );
1040 }
1041}