1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub mod prelude;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum ParticleFamily {
11 Lepton,
13 Quark,
15 GaugeBoson,
17 ScalarBoson,
19 Baryon,
21 Meson,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum ParticleStatistics {
28 Fermion,
30 Boson,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub enum ParticleKind {
49 Electron,
51 Positron,
53 Muon,
55 Antimuon,
57 Tau,
59 Antitau,
61
62 ElectronNeutrino,
64 ElectronAntineutrino,
66 MuonNeutrino,
68 MuonAntineutrino,
70 TauNeutrino,
72 TauAntineutrino,
74
75 UpQuark,
77 AntiUpQuark,
79 DownQuark,
81 AntiDownQuark,
83 CharmQuark,
85 AntiCharmQuark,
87 StrangeQuark,
89 AntiStrangeQuark,
91 TopQuark,
93 AntiTopQuark,
95 BottomQuark,
97 AntiBottomQuark,
99
100 Photon,
102 Gluon,
104 WPlusBoson,
106 WMinusBoson,
108 ZBoson,
110 HiggsBoson,
112
113 Proton,
115 Antiproton,
117 Neutron,
119 Antineutron,
121
122 PionPlus,
124 PionMinus,
126 PionZero,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
132pub struct ElementaryCharge {
133 pub thirds: i8,
135}
136
137impl ElementaryCharge {
138 #[must_use]
140 pub const fn new_thirds(thirds: i8) -> Self {
141 Self { thirds }
142 }
143
144 #[must_use]
146 pub const fn neutral() -> Self {
147 Self::new_thirds(0)
148 }
149
150 #[must_use]
152 pub const fn positive_one() -> Self {
153 Self::new_thirds(3)
154 }
155
156 #[must_use]
158 pub const fn negative_one() -> Self {
159 Self::new_thirds(-3)
160 }
161
162 #[must_use]
164 pub fn as_elementary_units(self) -> f64 {
165 f64::from(self.thirds) / 3.0
166 }
167
168 #[must_use]
170 pub const fn is_neutral(self) -> bool {
171 self.thirds == 0
172 }
173
174 #[must_use]
176 pub const fn is_positive(self) -> bool {
177 self.thirds > 0
178 }
179
180 #[must_use]
182 pub const fn is_negative(self) -> bool {
183 self.thirds < 0
184 }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
189pub struct Spin {
190 pub doubled: i8,
192}
193
194impl Spin {
195 #[must_use]
197 pub const fn new_doubled(doubled: i8) -> Self {
198 Self { doubled }
199 }
200
201 #[must_use]
203 pub const fn zero() -> Self {
204 Self::new_doubled(0)
205 }
206
207 #[must_use]
209 pub const fn half() -> Self {
210 Self::new_doubled(1)
211 }
212
213 #[must_use]
215 pub const fn one() -> Self {
216 Self::new_doubled(2)
217 }
218
219 #[must_use]
221 pub fn as_units_of_hbar(self) -> f64 {
222 f64::from(self.doubled) / 2.0
223 }
224
225 #[must_use]
227 pub const fn is_integer(self) -> bool {
228 self.doubled % 2 == 0
229 }
230
231 #[must_use]
233 pub const fn is_half_integer(self) -> bool {
234 !self.is_integer()
235 }
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub struct Particle {
241 pub kind: ParticleKind,
243}
244
245#[allow(clippy::trivially_copy_pass_by_ref)]
246impl Particle {
247 #[must_use]
259 pub const fn new(kind: ParticleKind) -> Self {
260 Self { kind }
261 }
262
263 #[must_use]
265 pub const fn family(&self) -> ParticleFamily {
266 family(self.kind)
267 }
268
269 #[must_use]
271 pub const fn charge(&self) -> ElementaryCharge {
272 charge(self.kind)
273 }
274
275 #[must_use]
277 pub const fn spin(&self) -> Spin {
278 spin(self.kind)
279 }
280
281 #[must_use]
283 pub const fn statistics(&self) -> ParticleStatistics {
284 statistics(self.kind)
285 }
286
287 #[must_use]
289 pub const fn antiparticle(&self) -> Option<Self> {
290 match antiparticle(self.kind) {
291 Some(kind) => Some(Self::new(kind)),
292 None => None,
293 }
294 }
295
296 #[must_use]
298 pub const fn rest_mass_mev_c2(&self) -> Option<f64> {
299 rest_mass_mev_c2(self.kind)
300 }
301
302 #[must_use]
304 pub const fn is_antiparticle(&self) -> bool {
305 is_antiparticle(self.kind)
306 }
307
308 #[must_use]
310 pub const fn is_self_antiparticle(&self) -> bool {
311 is_self_antiparticle(self.kind)
312 }
313}
314
315#[must_use]
317pub const fn charge_thirds(kind: ParticleKind) -> i8 {
318 match kind {
319 ParticleKind::Electron
320 | ParticleKind::Muon
321 | ParticleKind::Tau
322 | ParticleKind::WMinusBoson
323 | ParticleKind::Antiproton
324 | ParticleKind::PionMinus => -3,
325 ParticleKind::Positron
326 | ParticleKind::Antimuon
327 | ParticleKind::Antitau
328 | ParticleKind::WPlusBoson
329 | ParticleKind::Proton
330 | ParticleKind::PionPlus => 3,
331 ParticleKind::UpQuark | ParticleKind::CharmQuark | ParticleKind::TopQuark => 2,
332 ParticleKind::AntiUpQuark | ParticleKind::AntiCharmQuark | ParticleKind::AntiTopQuark => -2,
333 ParticleKind::DownQuark | ParticleKind::StrangeQuark | ParticleKind::BottomQuark => -1,
334 ParticleKind::AntiDownQuark
335 | ParticleKind::AntiStrangeQuark
336 | ParticleKind::AntiBottomQuark => 1,
337 ParticleKind::ElectronNeutrino
338 | ParticleKind::ElectronAntineutrino
339 | ParticleKind::MuonNeutrino
340 | ParticleKind::MuonAntineutrino
341 | ParticleKind::TauNeutrino
342 | ParticleKind::TauAntineutrino
343 | ParticleKind::Photon
344 | ParticleKind::Gluon
345 | ParticleKind::ZBoson
346 | ParticleKind::HiggsBoson
347 | ParticleKind::Neutron
348 | ParticleKind::Antineutron
349 | ParticleKind::PionZero => 0,
350 }
351}
352
353#[must_use]
364pub const fn charge(kind: ParticleKind) -> ElementaryCharge {
365 ElementaryCharge::new_thirds(charge_thirds(kind))
366}
367
368#[must_use]
370pub fn charge_in_elementary_units(kind: ParticleKind) -> f64 {
371 charge(kind).as_elementary_units()
372}
373
374#[must_use]
385pub const fn spin(kind: ParticleKind) -> Spin {
386 match kind {
387 ParticleKind::Electron
388 | ParticleKind::Positron
389 | ParticleKind::Muon
390 | ParticleKind::Antimuon
391 | ParticleKind::Tau
392 | ParticleKind::Antitau
393 | ParticleKind::ElectronNeutrino
394 | ParticleKind::ElectronAntineutrino
395 | ParticleKind::MuonNeutrino
396 | ParticleKind::MuonAntineutrino
397 | ParticleKind::TauNeutrino
398 | ParticleKind::TauAntineutrino
399 | ParticleKind::UpQuark
400 | ParticleKind::AntiUpQuark
401 | ParticleKind::DownQuark
402 | ParticleKind::AntiDownQuark
403 | ParticleKind::CharmQuark
404 | ParticleKind::AntiCharmQuark
405 | ParticleKind::StrangeQuark
406 | ParticleKind::AntiStrangeQuark
407 | ParticleKind::TopQuark
408 | ParticleKind::AntiTopQuark
409 | ParticleKind::BottomQuark
410 | ParticleKind::AntiBottomQuark
411 | ParticleKind::Proton
412 | ParticleKind::Antiproton
413 | ParticleKind::Neutron
414 | ParticleKind::Antineutron => Spin::half(),
415 ParticleKind::Photon
416 | ParticleKind::Gluon
417 | ParticleKind::WPlusBoson
418 | ParticleKind::WMinusBoson
419 | ParticleKind::ZBoson => Spin::one(),
420 ParticleKind::HiggsBoson
421 | ParticleKind::PionPlus
422 | ParticleKind::PionMinus
423 | ParticleKind::PionZero => Spin::zero(),
424 }
425}
426
427#[must_use]
429pub const fn statistics(kind: ParticleKind) -> ParticleStatistics {
430 if spin(kind).is_half_integer() {
431 ParticleStatistics::Fermion
432 } else {
433 ParticleStatistics::Boson
434 }
435}
436
437#[must_use]
448pub const fn family(kind: ParticleKind) -> ParticleFamily {
449 match kind {
450 ParticleKind::Electron
451 | ParticleKind::Positron
452 | ParticleKind::Muon
453 | ParticleKind::Antimuon
454 | ParticleKind::Tau
455 | ParticleKind::Antitau
456 | ParticleKind::ElectronNeutrino
457 | ParticleKind::ElectronAntineutrino
458 | ParticleKind::MuonNeutrino
459 | ParticleKind::MuonAntineutrino
460 | ParticleKind::TauNeutrino
461 | ParticleKind::TauAntineutrino => ParticleFamily::Lepton,
462 ParticleKind::UpQuark
463 | ParticleKind::AntiUpQuark
464 | ParticleKind::DownQuark
465 | ParticleKind::AntiDownQuark
466 | ParticleKind::CharmQuark
467 | ParticleKind::AntiCharmQuark
468 | ParticleKind::StrangeQuark
469 | ParticleKind::AntiStrangeQuark
470 | ParticleKind::TopQuark
471 | ParticleKind::AntiTopQuark
472 | ParticleKind::BottomQuark
473 | ParticleKind::AntiBottomQuark => ParticleFamily::Quark,
474 ParticleKind::Photon
475 | ParticleKind::Gluon
476 | ParticleKind::WPlusBoson
477 | ParticleKind::WMinusBoson
478 | ParticleKind::ZBoson => ParticleFamily::GaugeBoson,
479 ParticleKind::HiggsBoson => ParticleFamily::ScalarBoson,
480 ParticleKind::Proton
481 | ParticleKind::Antiproton
482 | ParticleKind::Neutron
483 | ParticleKind::Antineutron => ParticleFamily::Baryon,
484 ParticleKind::PionPlus | ParticleKind::PionMinus | ParticleKind::PionZero => {
485 ParticleFamily::Meson
486 },
487 }
488}
489
490#[must_use]
492pub const fn is_lepton(kind: ParticleKind) -> bool {
493 matches!(family(kind), ParticleFamily::Lepton)
494}
495
496#[must_use]
498pub const fn is_quark(kind: ParticleKind) -> bool {
499 matches!(family(kind), ParticleFamily::Quark)
500}
501
502#[must_use]
504pub const fn is_boson(kind: ParticleKind) -> bool {
505 matches!(statistics(kind), ParticleStatistics::Boson)
506}
507
508#[must_use]
510pub const fn is_baryon(kind: ParticleKind) -> bool {
511 matches!(family(kind), ParticleFamily::Baryon)
512}
513
514#[must_use]
516pub const fn is_meson(kind: ParticleKind) -> bool {
517 matches!(family(kind), ParticleFamily::Meson)
518}
519
520#[must_use]
522pub const fn is_fermion(kind: ParticleKind) -> bool {
523 matches!(statistics(kind), ParticleStatistics::Fermion)
524}
525
526#[must_use]
539#[allow(clippy::unnecessary_wraps)]
540pub const fn antiparticle(kind: ParticleKind) -> Option<ParticleKind> {
541 match kind {
542 ParticleKind::Electron => Some(ParticleKind::Positron),
543 ParticleKind::Positron => Some(ParticleKind::Electron),
544 ParticleKind::Muon => Some(ParticleKind::Antimuon),
545 ParticleKind::Antimuon => Some(ParticleKind::Muon),
546 ParticleKind::Tau => Some(ParticleKind::Antitau),
547 ParticleKind::Antitau => Some(ParticleKind::Tau),
548 ParticleKind::ElectronNeutrino => Some(ParticleKind::ElectronAntineutrino),
549 ParticleKind::ElectronAntineutrino => Some(ParticleKind::ElectronNeutrino),
550 ParticleKind::MuonNeutrino => Some(ParticleKind::MuonAntineutrino),
551 ParticleKind::MuonAntineutrino => Some(ParticleKind::MuonNeutrino),
552 ParticleKind::TauNeutrino => Some(ParticleKind::TauAntineutrino),
553 ParticleKind::TauAntineutrino => Some(ParticleKind::TauNeutrino),
554 ParticleKind::UpQuark => Some(ParticleKind::AntiUpQuark),
555 ParticleKind::AntiUpQuark => Some(ParticleKind::UpQuark),
556 ParticleKind::DownQuark => Some(ParticleKind::AntiDownQuark),
557 ParticleKind::AntiDownQuark => Some(ParticleKind::DownQuark),
558 ParticleKind::CharmQuark => Some(ParticleKind::AntiCharmQuark),
559 ParticleKind::AntiCharmQuark => Some(ParticleKind::CharmQuark),
560 ParticleKind::StrangeQuark => Some(ParticleKind::AntiStrangeQuark),
561 ParticleKind::AntiStrangeQuark => Some(ParticleKind::StrangeQuark),
562 ParticleKind::TopQuark => Some(ParticleKind::AntiTopQuark),
563 ParticleKind::AntiTopQuark => Some(ParticleKind::TopQuark),
564 ParticleKind::BottomQuark => Some(ParticleKind::AntiBottomQuark),
565 ParticleKind::AntiBottomQuark => Some(ParticleKind::BottomQuark),
566 ParticleKind::Photon
567 | ParticleKind::Gluon
568 | ParticleKind::ZBoson
569 | ParticleKind::HiggsBoson => Some(kind),
570 ParticleKind::WPlusBoson => Some(ParticleKind::WMinusBoson),
571 ParticleKind::WMinusBoson => Some(ParticleKind::WPlusBoson),
572 ParticleKind::Proton => Some(ParticleKind::Antiproton),
573 ParticleKind::Antiproton => Some(ParticleKind::Proton),
574 ParticleKind::Neutron => Some(ParticleKind::Antineutron),
575 ParticleKind::Antineutron => Some(ParticleKind::Neutron),
576 ParticleKind::PionPlus => Some(ParticleKind::PionMinus),
577 ParticleKind::PionMinus => Some(ParticleKind::PionPlus),
578 ParticleKind::PionZero => Some(ParticleKind::PionZero),
579 }
580}
581
582#[must_use]
584pub const fn is_antiparticle(kind: ParticleKind) -> bool {
585 matches!(
586 kind,
587 ParticleKind::Positron
588 | ParticleKind::Antimuon
589 | ParticleKind::Antitau
590 | ParticleKind::ElectronAntineutrino
591 | ParticleKind::MuonAntineutrino
592 | ParticleKind::TauAntineutrino
593 | ParticleKind::AntiUpQuark
594 | ParticleKind::AntiDownQuark
595 | ParticleKind::AntiCharmQuark
596 | ParticleKind::AntiStrangeQuark
597 | ParticleKind::AntiTopQuark
598 | ParticleKind::AntiBottomQuark
599 | ParticleKind::WMinusBoson
600 | ParticleKind::Antiproton
601 | ParticleKind::Antineutron
602 | ParticleKind::PionMinus
603 )
604}
605
606#[must_use]
608pub const fn is_self_antiparticle(kind: ParticleKind) -> bool {
609 matches!(
610 kind,
611 ParticleKind::Photon
612 | ParticleKind::Gluon
613 | ParticleKind::ZBoson
614 | ParticleKind::HiggsBoson
615 | ParticleKind::PionZero
616 )
617}
618
619#[must_use]
623pub const fn rest_mass_mev_c2(kind: ParticleKind) -> Option<f64> {
624 match kind {
625 ParticleKind::Electron | ParticleKind::Positron => Some(0.511),
626 ParticleKind::Muon | ParticleKind::Antimuon => Some(105.658),
627 ParticleKind::Tau | ParticleKind::Antitau => Some(1_776.86),
628 ParticleKind::ElectronNeutrino
629 | ParticleKind::ElectronAntineutrino
630 | ParticleKind::MuonNeutrino
631 | ParticleKind::MuonAntineutrino
632 | ParticleKind::TauNeutrino
633 | ParticleKind::TauAntineutrino
634 | ParticleKind::UpQuark
635 | ParticleKind::AntiUpQuark
636 | ParticleKind::DownQuark
637 | ParticleKind::AntiDownQuark
638 | ParticleKind::CharmQuark
639 | ParticleKind::AntiCharmQuark
640 | ParticleKind::StrangeQuark
641 | ParticleKind::AntiStrangeQuark
642 | ParticleKind::TopQuark
643 | ParticleKind::AntiTopQuark
644 | ParticleKind::BottomQuark
645 | ParticleKind::AntiBottomQuark => None,
646 ParticleKind::Photon | ParticleKind::Gluon => Some(0.0),
647 ParticleKind::WPlusBoson | ParticleKind::WMinusBoson => Some(80_379.0),
648 ParticleKind::ZBoson => Some(91_188.0),
649 ParticleKind::HiggsBoson => Some(125_250.0),
650 ParticleKind::Proton | ParticleKind::Antiproton => Some(938.272),
651 ParticleKind::Neutron | ParticleKind::Antineutron => Some(939.565),
652 ParticleKind::PionPlus | ParticleKind::PionMinus => Some(139.570),
653 ParticleKind::PionZero => Some(134.977),
654 }
655}
656
657#[must_use]
659pub const fn has_rest_mass(kind: ParticleKind) -> Option<bool> {
660 match kind {
661 ParticleKind::Photon | ParticleKind::Gluon => Some(false),
662 ParticleKind::Electron
663 | ParticleKind::Positron
664 | ParticleKind::Muon
665 | ParticleKind::Antimuon
666 | ParticleKind::Tau
667 | ParticleKind::Antitau
668 | ParticleKind::WPlusBoson
669 | ParticleKind::WMinusBoson
670 | ParticleKind::ZBoson
671 | ParticleKind::HiggsBoson
672 | ParticleKind::Proton
673 | ParticleKind::Antiproton
674 | ParticleKind::Neutron
675 | ParticleKind::Antineutron
676 | ParticleKind::PionPlus
677 | ParticleKind::PionMinus
678 | ParticleKind::PionZero => Some(true),
679 ParticleKind::ElectronNeutrino
680 | ParticleKind::ElectronAntineutrino
681 | ParticleKind::MuonNeutrino
682 | ParticleKind::MuonAntineutrino
683 | ParticleKind::TauNeutrino
684 | ParticleKind::TauAntineutrino
685 | ParticleKind::UpQuark
686 | ParticleKind::AntiUpQuark
687 | ParticleKind::DownQuark
688 | ParticleKind::AntiDownQuark
689 | ParticleKind::CharmQuark
690 | ParticleKind::AntiCharmQuark
691 | ParticleKind::StrangeQuark
692 | ParticleKind::AntiStrangeQuark
693 | ParticleKind::TopQuark
694 | ParticleKind::AntiTopQuark
695 | ParticleKind::BottomQuark
696 | ParticleKind::AntiBottomQuark => None,
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use super::{
703 ElementaryCharge, Particle, ParticleFamily, ParticleKind, ParticleStatistics, Spin,
704 antiparticle, charge, charge_thirds, family, has_rest_mass, is_antiparticle, is_baryon,
705 is_boson, is_fermion, is_lepton, is_meson, is_quark, is_self_antiparticle,
706 rest_mass_mev_c2, spin, statistics,
707 };
708
709 fn approx_eq(left: f64, right: f64) -> bool {
710 (left - right).abs() < 1.0e-10
711 }
712
713 #[test]
714 fn charge_helpers_cover_common_particles() {
715 assert_eq!(charge_thirds(ParticleKind::Electron), -3);
716 assert_eq!(charge_thirds(ParticleKind::Positron), 3);
717 assert_eq!(charge_thirds(ParticleKind::UpQuark), 2);
718 assert_eq!(charge_thirds(ParticleKind::DownQuark), -1);
719 assert_eq!(charge_thirds(ParticleKind::Photon), 0);
720
721 assert!(approx_eq(
722 charge(ParticleKind::Electron).as_elementary_units(),
723 -1.0
724 ));
725 assert!(approx_eq(
726 charge(ParticleKind::UpQuark).as_elementary_units(),
727 0.666_666_666_7,
728 ));
729 }
730
731 #[test]
732 fn spin_helpers_follow_statistics_rules() {
733 assert_eq!(spin(ParticleKind::Electron), Spin::half());
734 assert_eq!(spin(ParticleKind::Photon), Spin::one());
735 assert_eq!(spin(ParticleKind::HiggsBoson), Spin::zero());
736
737 assert_eq!(
738 statistics(ParticleKind::Electron),
739 ParticleStatistics::Fermion
740 );
741 assert_eq!(statistics(ParticleKind::Photon), ParticleStatistics::Boson);
742 assert_eq!(
743 statistics(ParticleKind::HiggsBoson),
744 ParticleStatistics::Boson
745 );
746 }
747
748 #[test]
749 fn family_helpers_group_particle_kinds() {
750 assert_eq!(family(ParticleKind::Electron), ParticleFamily::Lepton);
751 assert_eq!(family(ParticleKind::UpQuark), ParticleFamily::Quark);
752 assert_eq!(family(ParticleKind::Photon), ParticleFamily::GaugeBoson);
753 assert_eq!(
754 family(ParticleKind::HiggsBoson),
755 ParticleFamily::ScalarBoson
756 );
757 assert_eq!(family(ParticleKind::Proton), ParticleFamily::Baryon);
758 assert_eq!(family(ParticleKind::PionPlus), ParticleFamily::Meson);
759
760 assert!(is_lepton(ParticleKind::Electron));
761 assert!(is_quark(ParticleKind::UpQuark));
762 assert!(is_boson(ParticleKind::Photon));
763 assert!(is_baryon(ParticleKind::Proton));
764 assert!(is_meson(ParticleKind::PionZero));
765 assert!(is_fermion(ParticleKind::Electron));
766 assert!(!is_fermion(ParticleKind::Photon));
767 }
768
769 #[test]
770 fn antimatter_helpers_cover_pairs_and_self_conjugate_particles() {
771 assert_eq!(
772 antiparticle(ParticleKind::Electron),
773 Some(ParticleKind::Positron)
774 );
775 assert_eq!(
776 antiparticle(ParticleKind::Positron),
777 Some(ParticleKind::Electron)
778 );
779 assert_eq!(
780 antiparticle(ParticleKind::Proton),
781 Some(ParticleKind::Antiproton)
782 );
783 assert_eq!(
784 antiparticle(ParticleKind::Photon),
785 Some(ParticleKind::Photon)
786 );
787
788 assert!(is_antiparticle(ParticleKind::Positron));
789 assert!(!is_antiparticle(ParticleKind::Electron));
790 assert!(is_self_antiparticle(ParticleKind::Photon));
791 assert!(is_self_antiparticle(ParticleKind::PionZero));
792 assert!(!is_self_antiparticle(ParticleKind::Electron));
793 }
794
795 #[test]
796 fn rest_mass_metadata_is_small_and_practical() {
797 assert!(matches!(
798 rest_mass_mev_c2(ParticleKind::Electron),
799 Some(mass) if approx_eq(mass, 0.511)
800 ));
801 assert_eq!(rest_mass_mev_c2(ParticleKind::Photon), Some(0.0));
802 assert_eq!(rest_mass_mev_c2(ParticleKind::ElectronNeutrino), None);
803
804 assert_eq!(has_rest_mass(ParticleKind::Photon), Some(false));
805 assert_eq!(has_rest_mass(ParticleKind::Electron), Some(true));
806 }
807
808 #[test]
809 fn particle_wrapper_delegates_to_free_functions() {
810 let electron = Particle::new(ParticleKind::Electron);
811
812 assert_eq!(electron.charge(), ElementaryCharge::negative_one());
813 assert_eq!(electron.family(), ParticleFamily::Lepton);
814 assert!(matches!(
815 electron.antiparticle(),
816 Some(Particle {
817 kind: ParticleKind::Positron
818 })
819 ));
820 }
821}