1use crate::{
2 quantum::{PartialWave, ParticleProperties},
3 AllowedPartialWave, AngularMomentum, OrbitalAngularMomentum, Parity, Statistics,
4};
5
6#[derive(Clone, Debug, Eq, Hash, PartialEq, Default)]
17pub struct RuleSet {
18 pub parity: bool,
23
24 pub isospin: bool,
30
31 pub isospin_projection: bool,
35
36 pub c_parity: bool,
42
43 pub g_parity: bool,
49
50 pub charge: bool,
54
55 pub strangeness: bool,
62
63 pub charm: bool,
68
69 pub bottomness: bool,
74
75 pub topness: bool,
79
80 pub baryon_number: bool,
84
85 pub electron_lepton_number: bool,
89
90 pub muon_lepton_number: bool,
94
95 pub tau_lepton_number: bool,
99
100 pub lepton_number: bool,
108
109 pub identical_particle_symmetry: bool,
115}
116impl RuleSet {
117 pub fn angular() -> Self {
125 Self::default()
126 }
127
128 pub fn strong() -> Self {
138 Self {
139 parity: true,
140 isospin: true,
141 isospin_projection: true,
142 charge: true,
143 strangeness: true,
144 charm: true,
145 bottomness: true,
146 topness: true,
147 baryon_number: true,
148 identical_particle_symmetry: true,
149 ..Default::default()
150 }
151 }
152
153 pub fn electromagnetic() -> Self {
162 Self {
163 parity: true,
164 isospin_projection: true,
165 charge: true,
166 strangeness: true,
167 charm: true,
168 bottomness: true,
169 topness: true,
170 baryon_number: true,
171 identical_particle_symmetry: true,
172 ..Default::default()
173 }
174 }
175
176 pub fn weak() -> Self {
184 Self {
185 charge: true,
186 baryon_number: true,
187 electron_lepton_number: true,
188 muon_lepton_number: true,
189 tau_lepton_number: true,
190 lepton_number: true,
191 identical_particle_symmetry: true,
192 ..Default::default()
193 }
194 }
195
196 pub fn check(
210 &self,
211 parent: &ParticleProperties,
212 daughters: (&ParticleProperties, &ParticleProperties),
213 l: OrbitalAngularMomentum,
214 s: AngularMomentum,
215 ) -> bool {
216 (!self.parity || Self::check_parity(parent, daughters, l).unwrap_or(true))
217 && (!self.isospin || Self::check_isospin(parent, daughters).unwrap_or(true))
218 && (!self.isospin_projection
219 || Self::check_isospin_projection(parent, daughters).unwrap_or(true))
220 && (!self.c_parity || Self::check_c_parity(parent, daughters, l, s).unwrap_or(true))
221 && (!self.g_parity || Self::check_g_parity(parent, daughters).unwrap_or(true))
222 && self.check_additives(parent, daughters).unwrap_or(true)
223 && (!self.identical_particle_symmetry
224 || Self::check_identical_particle_symmetry(daughters, l).unwrap_or(true))
225 }
226 fn check_parity(
227 parent: &ParticleProperties,
228 daughters: (&ParticleProperties, &ParticleProperties),
229 l: OrbitalAngularMomentum,
230 ) -> Option<bool> {
231 let p_parent = parent.parity?;
232 let p_a = daughters.0.parity?;
233 let p_b = daughters.1.parity?;
234 let sign = if l.value() & 1 == 0 { 1 } else { -1 };
235 Some(p_parent.value() == p_a.value() * p_b.value() * sign)
236 }
237 fn check_isospin(
238 parent: &ParticleProperties,
239 daughters: (&ParticleProperties, &ParticleProperties),
240 ) -> Option<bool> {
241 let i_parent = parent.isospin?;
242 let i_a = daughters.0.isospin?;
243 let i_b = daughters.1.isospin?;
244 Some(
245 i_parent
246 .isospin()
247 .can_couple_to(i_a.isospin(), i_b.isospin()),
248 )
249 }
250 fn check_isospin_projection(
251 parent: &ParticleProperties,
252 daughters: (&ParticleProperties, &ParticleProperties),
253 ) -> Option<bool> {
254 let i_parent = parent.isospin?;
255 let i_a = daughters.0.isospin?;
256 let i_b = daughters.1.isospin?;
257 let i3_parent = i_parent.projection?;
258 let i3_a = i_a.projection?;
259 let i3_b = i_b.projection?;
260 Some(i3_parent.value() == i3_a.value() + i3_b.value())
261 }
262 fn check_c_parity(
263 parent: &ParticleProperties,
264 daughters: (&ParticleProperties, &ParticleProperties),
265 l: OrbitalAngularMomentum,
266 s: AngularMomentum,
267 ) -> Option<bool> {
268 let c_parent = parent.c_parity?;
269 if !daughters.0.is_antiparticle_of(daughters.1) {
270 return None;
271 }
272 let exp_twice = 2 * l.value() + s.value();
273 if !exp_twice.is_multiple_of(2) {
274 return Some(false);
275 }
276 let c_final = if (exp_twice / 2).is_multiple_of(2) {
277 Parity::Positive
278 } else {
279 Parity::Negative
280 };
281 Some(c_parent == c_final)
282 }
283 fn check_g_parity(
284 parent: &ParticleProperties,
285 daughters: (&ParticleProperties, &ParticleProperties),
286 ) -> Option<bool> {
287 let g_parent = parent.g_parity?;
288 let g_a = daughters.0.g_parity?;
289 let g_b = daughters.1.g_parity?;
290 Some(g_parent.value() == g_a.value() * g_b.value())
291 }
292 fn check_additives(
293 &self,
294 parent: &ParticleProperties,
295 daughters: (&ParticleProperties, &ParticleProperties),
296 ) -> Option<bool> {
297 let mut unknown = false;
298
299 macro_rules! check_conserved {
300 ($enabled:expr, $parent:expr, $a:expr, $b:expr) => {
301 if $enabled {
302 match ($parent, $a, $b) {
303 (Some(parent), Some(a), Some(b)) => {
304 if parent != a + b {
305 return Some(false);
306 }
307 }
308
309 _ => {
310 unknown = true;
311 }
312 }
313 }
314 };
315 }
316
317 check_conserved!(
318 self.charge,
319 parent.charge.map(|q| q.value()),
320 daughters.0.charge.map(|q| q.value()),
321 daughters.1.charge.map(|q| q.value())
322 );
323
324 check_conserved!(
325 self.strangeness,
326 parent.strangeness,
327 daughters.0.strangeness,
328 daughters.1.strangeness
329 );
330
331 check_conserved!(
332 self.charm,
333 parent.charm,
334 daughters.0.charm,
335 daughters.1.charm
336 );
337
338 check_conserved!(
339 self.bottomness,
340 parent.bottomness,
341 daughters.0.bottomness,
342 daughters.1.bottomness
343 );
344
345 check_conserved!(
346 self.topness,
347 parent.topness,
348 daughters.0.topness,
349 daughters.1.topness
350 );
351
352 check_conserved!(
353 self.baryon_number,
354 parent.baryon_number,
355 daughters.0.baryon_number,
356 daughters.1.baryon_number
357 );
358
359 check_conserved!(
360 self.electron_lepton_number,
361 parent.electron_lepton_number,
362 daughters.0.electron_lepton_number,
363 daughters.1.electron_lepton_number
364 );
365
366 check_conserved!(
367 self.muon_lepton_number,
368 parent.muon_lepton_number,
369 daughters.0.muon_lepton_number,
370 daughters.1.muon_lepton_number
371 );
372
373 check_conserved!(
374 self.tau_lepton_number,
375 parent.tau_lepton_number,
376 daughters.0.tau_lepton_number,
377 daughters.1.tau_lepton_number
378 );
379
380 if self.lepton_number {
381 match (
382 parent.electron_lepton_number,
383 parent.muon_lepton_number,
384 parent.tau_lepton_number,
385 daughters.0.electron_lepton_number,
386 daughters.0.muon_lepton_number,
387 daughters.0.tau_lepton_number,
388 daughters.1.electron_lepton_number,
389 daughters.1.muon_lepton_number,
390 daughters.1.tau_lepton_number,
391 ) {
392 (
393 Some(parent_e),
394 Some(parent_mu),
395 Some(parent_tau),
396 Some(a_e),
397 Some(a_mu),
398 Some(a_tau),
399 Some(b_e),
400 Some(b_mu),
401 Some(b_tau),
402 ) => {
403 let parent_total = parent_e + parent_mu + parent_tau;
404
405 let daughter_total = a_e + a_mu + a_tau + b_e + b_mu + b_tau;
406
407 if parent_total != daughter_total {
408 return Some(false);
409 }
410 }
411
412 _ => {
413 unknown = true;
414 }
415 }
416 }
417
418 if unknown {
419 None
420 } else {
421 Some(true)
422 }
423 }
424 fn check_identical_particle_symmetry(
425 daughters: (&ParticleProperties, &ParticleProperties),
426 l: OrbitalAngularMomentum,
427 ) -> Option<bool> {
428 let sp_a = daughters.0.species.as_ref()?;
429 let sp_b = daughters.1.species.as_ref()?;
430 if sp_a != sp_b {
431 return Some(true);
432 }
433 let stats_a = daughters.0.statistics?;
434 let stats_b = daughters.0.statistics?;
435 if stats_a != stats_b {
436 return Some(false);
437 }
438 if stats_a == Statistics::Boson
439 && daughters.0.spin.map(|x| x.value()) == Some(0)
440 && daughters.1.spin.map(|x| x.value()) == Some(0)
441 {
442 if l.value().is_multiple_of(2) {
443 return Some(true);
444 }
445 return Some(false);
446 }
447 None
448 }
449}
450
451#[derive(Clone, Debug, Eq, Hash, PartialEq)]
463pub struct SelectionRules {
464 pub max_l: OrbitalAngularMomentum,
470
471 pub rules: RuleSet,
478}
479impl Default for SelectionRules {
480 fn default() -> Self {
481 Self {
482 max_l: OrbitalAngularMomentum::integer(6),
483 rules: RuleSet::strong(),
484 }
485 }
486}
487impl SelectionRules {
488 pub fn coupled_spins(a: AngularMomentum, b: AngularMomentum) -> Vec<AngularMomentum> {
496 let min = a.value().abs_diff(b.value());
497 let max = a.value() + b.value();
498 (min..=max)
499 .step_by(2)
500 .map(AngularMomentum::half_integer)
501 .collect()
502 }
503 pub fn allowed_partial_waves(
523 &self,
524 parent: &ParticleProperties,
525 daughters: (&ParticleProperties, &ParticleProperties),
526 ) -> Vec<AllowedPartialWave> {
527 let Some(parent_j) = parent.spin else {
528 return vec![];
529 };
530 let Some(ja) = daughters.0.spin else {
531 return vec![];
532 };
533 let Some(jb) = daughters.1.spin else {
534 return vec![];
535 };
536 let mut out = Vec::new();
537 for s in Self::coupled_spins(ja, jb) {
538 for l_raw in 0..=self.max_l.value() {
539 let l = OrbitalAngularMomentum::integer(l_raw);
540 let wave = PartialWave::new(parent_j, l, s);
541 if let Ok(wave) = wave {
542 if self.rules.check(parent, daughters, l, s) {
544 out.push(AllowedPartialWave::new(wave, daughters));
545 }
546 }
547 }
548 }
549 out
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use crate::{Charge, Isospin, Projection};
557
558 fn j(twice: u32) -> AngularMomentum {
559 AngularMomentum::half_integer(twice)
560 }
561
562 fn l(value: u32) -> OrbitalAngularMomentum {
563 OrbitalAngularMomentum::integer(value)
564 }
565
566 fn q(thirds: i32) -> Charge {
567 Charge::third_integer(thirds)
568 }
569
570 fn labels(waves: &[AllowedPartialWave]) -> Vec<String> {
571 waves.iter().map(|w| w.wave.label.clone()).collect()
572 }
573
574 #[test]
575 fn coupled_spins_include_all_allowed_values() {
576 assert_eq!(SelectionRules::coupled_spins(j(1), j(1)), vec![j(0), j(2)]);
577 assert_eq!(SelectionRules::coupled_spins(j(1), j(2)), vec![j(1), j(3)]);
578 assert_eq!(
579 SelectionRules::coupled_spins(j(2), j(2)),
580 vec![j(0), j(2), j(4)]
581 );
582 }
583
584 #[test]
585 fn parity_check_uses_both_daughter_parities() {
586 let parent = ParticleProperties::jp(j(0), Parity::Positive);
587 let a = ParticleProperties::jp(j(0), Parity::Positive);
588 let b = ParticleProperties::jp(j(0), Parity::Negative);
589 assert_eq!(RuleSet::check_parity(&parent, (&a, &b), l(0)), Some(false));
590 assert_eq!(RuleSet::check_parity(&parent, (&a, &b), l(1)), Some(true));
591 }
592
593 #[test]
594 fn parity_check_returns_none_when_required_values_are_unknown() {
595 let parent = ParticleProperties::unknown();
596 let a = ParticleProperties::jp(j(0), Parity::Positive);
597 let b = ParticleProperties::jp(j(0), Parity::Negative);
598 assert_eq!(RuleSet::check_parity(&parent, (&a, &b), l(0)), None);
599 }
600
601 #[test]
602 fn additive_checks_reject_any_known_violation() {
603 let rules = RuleSet {
604 charge: true,
605 strangeness: true,
606 baryon_number: true,
607 ..RuleSet::default()
608 };
609 let parent = ParticleProperties::unknown()
610 .with_charge(q(0))
611 .with_strangeness(0)
612 .with_baryon_number(0);
613 let a = ParticleProperties::unknown()
614 .with_charge(q(3))
615 .with_strangeness(0)
616 .with_baryon_number(0);
617 let b = ParticleProperties::unknown()
618 .with_charge(q(0))
619 .with_strangeness(0)
620 .with_baryon_number(0);
621 assert_eq!(rules.check_additives(&parent, (&a, &b)), Some(false));
622 }
623
624 #[test]
625 fn additive_checks_return_none_for_unknowns_only_when_no_violation_is_known() {
626 let rules = RuleSet {
627 charge: true,
628 strangeness: true,
629 ..RuleSet::default()
630 };
631 let parent = ParticleProperties::unknown().with_charge(q(0));
632 let a = ParticleProperties::unknown().with_charge(q(3));
633 let b = ParticleProperties::unknown().with_charge(q(-3));
634 assert_eq!(rules.check_additives(&parent, (&a, &b)), None);
635 }
636
637 #[test]
638 fn additive_checks_return_some_true_when_all_enabled_checks_pass() {
639 let rules = RuleSet {
640 charge: true,
641 strangeness: true,
642 baryon_number: true,
643 ..RuleSet::default()
644 };
645 let parent = ParticleProperties::unknown()
646 .with_charge(q(0))
647 .with_strangeness(0)
648 .with_baryon_number(0);
649 let a = ParticleProperties::unknown()
650 .with_charge(q(3))
651 .with_strangeness(1)
652 .with_baryon_number(0);
653 let b = ParticleProperties::unknown()
654 .with_charge(q(-3))
655 .with_strangeness(-1)
656 .with_baryon_number(0);
657 assert_eq!(rules.check_additives(&parent, (&a, &b)), Some(true));
658 }
659
660 #[test]
661
662 fn total_lepton_number_can_pass_when_individual_flavors_change() {
663 let rules = RuleSet {
664 lepton_number: true,
665 electron_lepton_number: false,
666 muon_lepton_number: false,
667 tau_lepton_number: false,
668 ..RuleSet::default()
669 };
670 let parent = ParticleProperties::unknown()
671 .with_electron_lepton_number(1)
672 .with_muon_lepton_number(0)
673 .with_tau_lepton_number(0);
674 let a = ParticleProperties::unknown()
675 .with_electron_lepton_number(0)
676 .with_muon_lepton_number(1)
677 .with_tau_lepton_number(0);
678 let b = ParticleProperties::unknown()
679 .with_electron_lepton_number(0)
680 .with_muon_lepton_number(0)
681 .with_tau_lepton_number(0);
682 assert_eq!(rules.check_additives(&parent, (&a, &b)), Some(true));
683 }
684
685 #[test]
686 fn individual_lepton_number_can_reject_flavor_change() {
687 let rules = RuleSet {
688 lepton_number: true,
689 electron_lepton_number: true,
690 muon_lepton_number: true,
691 tau_lepton_number: true,
692 ..RuleSet::default()
693 };
694 let parent = ParticleProperties::unknown()
695 .with_electron_lepton_number(1)
696 .with_muon_lepton_number(0)
697 .with_tau_lepton_number(0);
698 let a = ParticleProperties::unknown()
699 .with_electron_lepton_number(0)
700 .with_muon_lepton_number(1)
701 .with_tau_lepton_number(0);
702 let b = ParticleProperties::unknown()
703 .with_electron_lepton_number(0)
704 .with_muon_lepton_number(0)
705 .with_tau_lepton_number(0);
706 assert_eq!(rules.check_additives(&parent, (&a, &b)), Some(false));
707 }
708
709 #[test]
710 fn isospin_coupling_accepts_allowed_parent_isospin() {
711 let parent = ParticleProperties::unknown().with_isospin(Isospin::new(j(2), None).unwrap()); let a = ParticleProperties::unknown().with_isospin(Isospin::new(j(1), None).unwrap()); let b = ParticleProperties::unknown().with_isospin(Isospin::new(j(1), None).unwrap()); assert_eq!(RuleSet::check_isospin(&parent, (&a, &b)), Some(true));
715 }
716
717 #[test]
718 fn isospin_coupling_rejects_disallowed_parent_isospin() {
719 let parent = ParticleProperties::unknown().with_isospin(Isospin::new(j(4), None).unwrap()); let a = ParticleProperties::unknown().with_isospin(Isospin::new(j(1), None).unwrap()); let b = ParticleProperties::unknown().with_isospin(Isospin::new(j(1), None).unwrap()); assert_eq!(RuleSet::check_isospin(&parent, (&a, &b)), Some(false));
723 }
724
725 #[test]
726 fn isospin_projection_checks_i3_conservation() {
727 let parent = ParticleProperties::unknown()
728 .with_isospin(Isospin::new(j(2), Some(Projection::integer(0))).unwrap());
729 let a = ParticleProperties::unknown()
730 .with_isospin(Isospin::new(j(1), Some(Projection::half_integer(1))).unwrap());
731 let b = ParticleProperties::unknown()
732 .with_isospin(Isospin::new(j(1), Some(Projection::half_integer(-1))).unwrap());
733 assert_eq!(
734 RuleSet::check_isospin_projection(&parent, (&a, &b)),
735 Some(true)
736 );
737 }
738
739 #[test]
740 fn c_parity_uses_l_plus_s_for_particle_antiparticle_pair() {
741 let parent = ParticleProperties::jpc(j(2), Parity::Negative, Parity::Negative);
742 let a = ParticleProperties::jp(j(0), Parity::Negative)
743 .with_species("pi+")
744 .with_antiparticle_species("pi-");
745 let b = ParticleProperties::jp(j(0), Parity::Negative)
746 .with_species("pi-")
747 .with_antiparticle_species("pi+");
748 assert_eq!(
750 RuleSet::check_c_parity(&parent, (&a, &b), l(1), j(0)),
751 Some(true)
752 );
753 assert_eq!(
755 RuleSet::check_c_parity(&parent, (&a, &b), l(0), j(0)),
756 Some(false)
757 );
758 }
759
760 #[test]
761 fn g_parity_checks_product_of_daughter_g_parities() {
762 let parent = ParticleProperties::unknown().with_g_parity(Parity::Positive);
763 let a = ParticleProperties::unknown().with_g_parity(Parity::Negative);
764 let b = ParticleProperties::unknown().with_g_parity(Parity::Negative);
765 assert_eq!(RuleSet::check_g_parity(&parent, (&a, &b)), Some(true));
766 }
767
768 #[test]
769 fn identical_spin_zero_bosons_require_even_l() {
770 let a = ParticleProperties::jp(j(0), Parity::Negative)
771 .with_species("pi0")
772 .with_statistics(Statistics::Boson)
773 .unwrap();
774 let b = ParticleProperties::jp(j(0), Parity::Negative)
775 .with_species("pi0")
776 .with_statistics(Statistics::Boson)
777 .unwrap();
778 assert_eq!(
779 RuleSet::check_identical_particle_symmetry((&a, &b), l(0)),
780 Some(true)
781 );
782 assert_eq!(
783 RuleSet::check_identical_particle_symmetry((&a, &b), l(1)),
784 Some(false)
785 );
786 }
787
788 #[test]
789 fn selection_rules_find_delta_to_n_pi_p_wave() {
790 let parent = ParticleProperties::jp(j(3), Parity::Positive)
791 .with_charge(q(3))
792 .with_baryon_number(1);
793 let nucleon = ParticleProperties::jp(j(1), Parity::Positive)
794 .with_charge(q(3))
795 .with_baryon_number(1);
796 let pion = ParticleProperties::jp(j(0), Parity::Negative)
797 .with_charge(q(0))
798 .with_baryon_number(0);
799 let rules = SelectionRules {
800 max_l: l(4),
801 rules: RuleSet {
802 parity: true,
803 charge: true,
804 baryon_number: true,
805 ..RuleSet::angular()
806 },
807 };
808 let waves = rules.allowed_partial_waves(&parent, (&nucleon, &pion));
809 assert_eq!(labels(&waves), vec!["2P3/2"]);
810 }
811
812 #[test]
813 fn angular_only_selection_rules_include_all_l_s_couplings() {
814 let parent = ParticleProperties::jp(j(2), Parity::Positive); let a = ParticleProperties::jp(j(1), Parity::Positive); let b = ParticleProperties::jp(j(1), Parity::Negative); let rules = SelectionRules {
818 max_l: l(2),
819 rules: RuleSet::angular(),
820 };
821 let waves = rules.allowed_partial_waves(&parent, (&a, &b));
822 let got = labels(&waves);
823 assert_eq!(got, vec!["1P1", "3S1", "3P1", "3D1"]);
824 }
825
826 #[test]
827 fn strong_parity_filter_removes_wrong_l_values() {
828 let parent = ParticleProperties::jp(j(2), Parity::Positive); let a = ParticleProperties::jp(j(1), Parity::Positive);
830 let b = ParticleProperties::jp(j(1), Parity::Negative);
831 let rules = SelectionRules {
832 max_l: l(2),
833 rules: RuleSet {
834 parity: true,
835 ..RuleSet::angular()
836 },
837 };
838 let waves = rules.allowed_partial_waves(&parent, (&a, &b));
839 let got = labels(&waves);
840 assert_eq!(got, vec!["1P1", "3P1"]);
842 }
843
844 #[test]
845 fn allowed_partial_waves_returns_empty_when_spin_information_is_missing() {
846 let parent = ParticleProperties::unknown();
847 let a = ParticleProperties::jp(j(0), Parity::Negative);
848 let b = ParticleProperties::jp(j(0), Parity::Negative);
849 let rules = SelectionRules::default();
850 assert!(rules.allowed_partial_waves(&parent, (&a, &b)).is_empty());
851 }
852}