1use std::{collections::BTreeMap, fmt::Display};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6 LadduPhysicsError, LadduPhysicsResult,
7 quantum::{J, L, Parity, ParticleProperties, S, Statistics},
8};
9
10#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
11pub enum RuleKind {
14 Parity,
19
20 Isospin,
26 IsospinProjection,
30 CParity,
36 GParity,
42 Charge,
46 Strangeness,
53 Charm,
58 Bottomness,
63 Topness,
67 BaryonNumber,
71 ElectronLeptonNumber,
75 MuonLeptonNumber,
79 TauLeptonNumber,
83 LeptonNumber,
91 IdenticalParticleSymmetry,
97
98 ConventionalMesonJpc,
100}
101
102#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
103pub enum RuleMode {
105 Enforce,
107 Ignore {
109 reason: Option<String>,
111 },
112 DiagnoseOnly {
114 reason: Option<String>,
116 },
117}
118
119#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
120pub enum UnknownPolicy {
122 Allow,
124 Reject,
126 Warn,
128}
129
130#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
131pub struct RulePolicy {
133 pub mode: RuleMode,
135 pub unknown: UnknownPolicy,
137}
138
139impl RulePolicy {
140 pub fn enforce() -> Self {
142 Self {
143 mode: RuleMode::Enforce,
144 unknown: UnknownPolicy::Allow,
145 }
146 }
147
148 pub fn enforce_strict() -> Self {
150 Self {
151 mode: RuleMode::Enforce,
152 unknown: UnknownPolicy::Reject,
153 }
154 }
155
156 pub fn enforce_with_unknown_policy(unknown: UnknownPolicy) -> Self {
158 Self {
159 mode: RuleMode::Enforce,
160 unknown,
161 }
162 }
163
164 pub fn ignore(reason: impl Into<String>) -> Self {
166 Self {
167 mode: RuleMode::Ignore {
168 reason: Some(reason.into()),
169 },
170 unknown: UnknownPolicy::Allow,
171 }
172 }
173
174 pub fn ignore_without_reason() -> Self {
176 Self {
177 mode: RuleMode::Ignore { reason: None },
178 unknown: UnknownPolicy::Allow,
179 }
180 }
181
182 pub fn diagnose_only(reason: impl Into<String>) -> Self {
184 Self {
185 mode: RuleMode::DiagnoseOnly {
186 reason: Some(reason.into()),
187 },
188 unknown: UnknownPolicy::Warn,
189 }
190 }
191
192 pub fn diagnose_only_without_reason() -> Self {
194 Self {
195 mode: RuleMode::DiagnoseOnly { reason: None },
196 unknown: UnknownPolicy::Warn,
197 }
198 }
199}
200
201#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
202pub enum RuleOutcome {
204 Pass {
206 message: String,
208 },
209 Fail {
211 message: String,
213 },
214 UnknownAllowed {
216 missing: Vec<String>,
218 message: String,
220 },
221 Warning {
223 missing: Vec<String>,
225 message: String,
227 },
228 Ignored {
230 reason: Option<String>,
232 },
233 Diagnostic {
235 passed: Option<bool>,
237 reason: Option<String>,
239 message: String,
241 },
242}
243
244#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
245pub struct RuleCheck {
247 pub rule: RuleKind,
249 pub outcome: RuleOutcome,
251}
252
253impl RuleCheck {
254 pub fn is_failure(&self) -> bool {
256 matches!(self.outcome, RuleOutcome::Fail { .. })
257 }
258
259 pub fn is_warning(&self) -> bool {
261 matches!(self.outcome, RuleOutcome::Warning { .. })
262 }
263
264 pub fn is_unknown_allowed(&self) -> bool {
266 matches!(self.outcome, RuleOutcome::UnknownAllowed { .. })
267 }
268
269 pub fn is_ignored(&self) -> bool {
271 matches!(self.outcome, RuleOutcome::Ignored { .. })
272 }
273}
274
275#[derive(Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize)]
276pub struct RuleReport {
278 pub checks: Vec<RuleCheck>,
280}
281
282impl RuleReport {
283 pub fn is_allowed(&self) -> bool {
285 self.checks.iter().all(|check| !check.is_failure())
286 }
287
288 pub fn failures(&self) -> impl Iterator<Item = &RuleCheck> {
290 self.checks.iter().filter(|check| check.is_failure())
291 }
292
293 pub fn warnings(&self) -> impl Iterator<Item = &RuleCheck> {
295 self.checks.iter().filter(|check| check.is_warning())
296 }
297
298 pub fn unknowns(&self) -> impl Iterator<Item = &RuleCheck> {
300 self.checks
301 .iter()
302 .filter(|check| check.is_unknown_allowed())
303 }
304
305 pub fn ignored(&self) -> impl Iterator<Item = &RuleCheck> {
307 self.checks.iter().filter(|check| check.is_ignored())
308 }
309
310 pub fn outcome(&self, rule: RuleKind) -> Option<&RuleOutcome> {
312 self.checks
313 .iter()
314 .find(|check| check.rule == rule)
315 .map(|check| &check.outcome)
316 }
317
318 pub fn has_failures(&self) -> bool {
320 self.failures().next().is_some()
321 }
322
323 pub fn has_unknowns(&self) -> bool {
325 self.unknowns().next().is_some()
326 }
327
328 pub fn has_ignored(&self) -> bool {
330 self.ignored().next().is_some()
331 }
332
333 pub fn len(&self) -> usize {
335 self.checks.len()
336 }
337
338 pub fn is_empty(&self) -> bool {
340 self.checks.is_empty()
341 }
342}
343
344#[derive(Clone, Debug, Eq, Hash, PartialEq, Default, Serialize, Deserialize)]
357pub struct RuleSet {
358 policies: BTreeMap<RuleKind, RulePolicy>,
359}
360impl RuleSet {
361 pub fn angular() -> Self {
369 Self::default()
370 }
371
372 pub fn strong() -> Self {
382 Self::angular()
383 .enforce(RuleKind::Parity)
384 .enforce(RuleKind::Isospin)
385 .enforce(RuleKind::IsospinProjection)
386 .enforce(RuleKind::Charge)
387 .enforce(RuleKind::Strangeness)
388 .enforce(RuleKind::Charm)
389 .enforce(RuleKind::Bottomness)
390 .enforce(RuleKind::Topness)
391 .enforce(RuleKind::BaryonNumber)
392 .enforce(RuleKind::IdenticalParticleSymmetry)
393 }
394
395 pub fn electromagnetic() -> Self {
404 Self::angular()
405 .enforce(RuleKind::Parity)
406 .enforce(RuleKind::IsospinProjection)
407 .enforce(RuleKind::Charge)
408 .enforce(RuleKind::Strangeness)
409 .enforce(RuleKind::Charm)
410 .enforce(RuleKind::Bottomness)
411 .enforce(RuleKind::Topness)
412 .enforce(RuleKind::BaryonNumber)
413 .enforce(RuleKind::IdenticalParticleSymmetry)
414 }
415
416 pub fn weak() -> Self {
424 Self::angular()
425 .enforce(RuleKind::Charge)
426 .enforce(RuleKind::BaryonNumber)
427 .enforce(RuleKind::ElectronLeptonNumber)
428 .enforce(RuleKind::MuonLeptonNumber)
429 .enforce(RuleKind::TauLeptonNumber)
430 .enforce(RuleKind::LeptonNumber)
431 .enforce(RuleKind::IdenticalParticleSymmetry)
432 }
433
434 pub fn enforce_mut(&mut self, rule: RuleKind) -> &mut Self {
436 self.policies.insert(rule, RulePolicy::enforce());
437 self
438 }
439
440 pub fn enforce_strict_mut(&mut self, rule: RuleKind) -> &mut Self {
442 self.policies.insert(rule, RulePolicy::enforce_strict());
443 self
444 }
445
446 pub fn set_policy_mut(&mut self, rule: RuleKind, policy: RulePolicy) -> &mut Self {
448 self.policies.insert(rule, policy);
449 self
450 }
451
452 pub fn ignore_mut(&mut self, rule: RuleKind, reason: impl Into<String>) -> &mut Self {
454 self.policies.insert(rule, RulePolicy::ignore(reason));
455 self
456 }
457
458 pub fn ignore_without_reason_mut(&mut self, rule: RuleKind) -> &mut Self {
460 self.policies
461 .insert(rule, RulePolicy::ignore_without_reason());
462 self
463 }
464
465 pub fn diagnose_only_mut(&mut self, rule: RuleKind, reason: impl Into<String>) -> &mut Self {
467 self.policies
468 .insert(rule, RulePolicy::diagnose_only(reason));
469 self
470 }
471
472 pub fn diagnose_only_without_reason_mut(&mut self, rule: RuleKind) -> &mut Self {
474 self.policies
475 .insert(rule, RulePolicy::diagnose_only_without_reason());
476 self
477 }
478
479 pub fn disable_mut(&mut self, rule: RuleKind) -> &mut Self {
481 self.policies.remove(&rule);
482 self
483 }
484
485 pub fn with_unknown_policy_mut(&mut self, rule: RuleKind, unknown: UnknownPolicy) -> &mut Self {
490 self.policies
491 .entry(rule)
492 .or_insert_with(RulePolicy::enforce)
493 .unknown = unknown;
494 self
495 }
496
497 pub fn enforce(mut self, rule: RuleKind) -> Self {
499 self.enforce_mut(rule);
500 self
501 }
502
503 pub fn enforce_strict(mut self, rule: RuleKind) -> Self {
505 self.enforce_strict_mut(rule);
506 self
507 }
508
509 pub fn set_policy(mut self, rule: RuleKind, policy: RulePolicy) -> Self {
511 self.set_policy_mut(rule, policy);
512 self
513 }
514
515 pub fn ignore(mut self, rule: RuleKind, reason: impl Into<String>) -> Self {
517 self.ignore_mut(rule, reason);
518 self
519 }
520
521 pub fn ignore_without_reason(mut self, rule: RuleKind) -> Self {
523 self.ignore_without_reason_mut(rule);
524 self
525 }
526
527 pub fn diagnose_only(mut self, rule: RuleKind, reason: impl Into<String>) -> Self {
529 self.diagnose_only_mut(rule, reason);
530 self
531 }
532
533 pub fn diagnose_only_without_reason(mut self, rule: RuleKind) -> Self {
535 self.diagnose_only_without_reason_mut(rule);
536 self
537 }
538
539 pub fn disable(mut self, rule: RuleKind) -> Self {
541 self.disable_mut(rule);
542 self
543 }
544
545 pub fn with_unknown_policy(mut self, rule: RuleKind, unknown: UnknownPolicy) -> Self {
547 self.with_unknown_policy_mut(rule, unknown);
548 self
549 }
550
551 pub fn policy(&self, rule: RuleKind) -> Option<&RulePolicy> {
553 self.policies.get(&rule)
554 }
555
556 pub fn enabled_rules(&self) -> impl Iterator<Item = RuleKind> + '_ {
558 self.policies.keys().copied()
559 }
560
561 pub fn check(
563 &self,
564 parent: &ParticleProperties,
565 daughters: (&ParticleProperties, &ParticleProperties),
566 l: L,
567 s: S,
568 ) -> bool {
569 self.evaluate(parent, daughters, l, s).is_allowed()
570 }
571
572 pub fn evaluate(
574 &self,
575 parent: &ParticleProperties,
576 daughters: (&ParticleProperties, &ParticleProperties),
577 l: L,
578 s: S,
579 ) -> RuleReport {
580 let mut checks = Vec::new();
581
582 for (&rule, policy) in &self.policies {
583 let raw = match rule {
584 RuleKind::Parity => check_parity_raw(parent, daughters, l),
585 RuleKind::Isospin => check_isospin_raw(parent, daughters),
586 RuleKind::IsospinProjection => check_isospin_projection_raw(parent, daughters),
587 RuleKind::CParity => check_c_parity_raw(parent, daughters, l, s),
588 RuleKind::GParity => check_g_parity_raw(parent, daughters),
589 RuleKind::Charge => check_additive_raw(
590 "charge",
591 "charge",
592 "Q",
593 parent.charge,
594 daughters.0.charge,
595 daughters.1.charge,
596 ),
597 RuleKind::Strangeness => check_additive_raw(
598 "strangeness",
599 "strangeness",
600 "S",
601 parent.strangeness,
602 daughters.0.strangeness,
603 daughters.1.strangeness,
604 ),
605 RuleKind::Charm => check_additive_raw(
606 "charm",
607 "charm",
608 "C",
609 parent.charm,
610 daughters.0.charm,
611 daughters.1.charm,
612 ),
613 RuleKind::Bottomness => check_additive_raw(
614 "bottomness",
615 "bottomness",
616 "B'",
617 parent.bottomness,
618 daughters.0.bottomness,
619 daughters.1.bottomness,
620 ),
621 RuleKind::Topness => check_additive_raw(
622 "topness",
623 "topness",
624 "T",
625 parent.topness,
626 daughters.0.topness,
627 daughters.1.topness,
628 ),
629 RuleKind::BaryonNumber => check_additive_raw(
630 "baryon_number",
631 "baryon number",
632 "B",
633 parent.baryon_number,
634 daughters.0.baryon_number,
635 daughters.1.baryon_number,
636 ),
637 RuleKind::ElectronLeptonNumber => check_additive_raw(
638 "electron_lepton_number",
639 "electron-family lepton number",
640 "L_e",
641 parent.electron_lepton_number,
642 daughters.0.electron_lepton_number,
643 daughters.1.electron_lepton_number,
644 ),
645 RuleKind::MuonLeptonNumber => check_additive_raw(
646 "muon_lepton_number",
647 "muon-family lepton number",
648 "L_mu",
649 parent.muon_lepton_number,
650 daughters.0.muon_lepton_number,
651 daughters.1.muon_lepton_number,
652 ),
653 RuleKind::TauLeptonNumber => check_additive_raw(
654 "tau_lepton_number",
655 "tau-family lepton number",
656 "L_tau",
657 parent.tau_lepton_number,
658 daughters.0.tau_lepton_number,
659 daughters.1.tau_lepton_number,
660 ),
661 RuleKind::LeptonNumber => check_total_lepton_number_raw(parent, daughters),
662 RuleKind::IdenticalParticleSymmetry => {
663 check_identical_particle_symmetry_raw(daughters, l, s)
664 }
665 RuleKind::ConventionalMesonJpc => check_conventional_meson_jpc_raw(parent),
666 };
667
668 checks.push(apply_policy(rule, policy, raw));
669 }
670
671 RuleReport { checks }
672 }
673}
674
675#[derive(Clone, Debug, Eq, PartialEq)]
676enum RawRuleOutcome {
677 Pass {
678 message: String,
679 },
680 Fail {
681 message: String,
682 },
683 Unknown {
684 missing: Vec<String>,
685 message: String,
686 },
687}
688
689impl RawRuleOutcome {
690 fn pass(message: impl Into<String>) -> Self {
691 Self::Pass {
692 message: message.into(),
693 }
694 }
695
696 fn fail(message: impl Into<String>) -> Self {
697 Self::Fail {
698 message: message.into(),
699 }
700 }
701
702 fn unknown(missing: impl Into<Vec<String>>, message: impl Into<String>) -> Self {
703 Self::Unknown {
704 missing: missing.into(),
705 message: message.into(),
706 }
707 }
708
709 fn message(&self) -> String {
710 match self {
711 Self::Pass { message } => message.clone(),
712 Self::Fail { message } => message.clone(),
713 Self::Unknown { message, .. } => message.clone(),
714 }
715 }
716
717 fn passed(&self) -> Option<bool> {
718 match self {
719 Self::Pass { .. } => Some(true),
720 Self::Fail { .. } => Some(false),
721 Self::Unknown { .. } => None,
722 }
723 }
724}
725
726fn missing(fields: &[&'static str]) -> Vec<String> {
727 fields.iter().map(|field| (*field).to_string()).collect()
728}
729
730fn apply_policy(rule: RuleKind, policy: &RulePolicy, raw: RawRuleOutcome) -> RuleCheck {
731 let outcome = match &policy.mode {
732 RuleMode::Ignore { reason } => RuleOutcome::Ignored {
733 reason: reason.clone(),
734 },
735
736 RuleMode::DiagnoseOnly { reason } => RuleOutcome::Diagnostic {
737 passed: raw.passed(),
738 reason: reason.clone(),
739 message: raw.message(),
740 },
741
742 RuleMode::Enforce => match raw {
743 RawRuleOutcome::Pass { message } => RuleOutcome::Pass { message },
744
745 RawRuleOutcome::Fail { message } => RuleOutcome::Fail { message },
746
747 RawRuleOutcome::Unknown { missing, message } => match policy.unknown {
748 UnknownPolicy::Allow => RuleOutcome::UnknownAllowed { missing, message },
749 UnknownPolicy::Warn => RuleOutcome::Warning { missing, message },
750 UnknownPolicy::Reject => RuleOutcome::Fail {
751 message: format!("{message}; unknown inputs are rejected by policy"),
752 },
753 },
754 },
755 };
756
757 RuleCheck { rule, outcome }
758}
759
760fn check_parity_raw(
761 parent: &ParticleProperties,
762 daughters: (&ParticleProperties, &ParticleProperties),
763 l: L,
764) -> RawRuleOutcome {
765 let Some(p_parent) = parent.parity else {
766 return RawRuleOutcome::unknown(missing(&["parent.parity"]), "parent parity is unknown");
767 };
768
769 let mut missing_fields = Vec::new();
770
771 if daughters.0.parity.is_none() {
772 missing_fields.push("daughter_a.parity".to_string());
773 }
774
775 if daughters.1.parity.is_none() {
776 missing_fields.push("daughter_b.parity".to_string());
777 }
778
779 if !missing_fields.is_empty() {
780 return RawRuleOutcome::unknown(
781 missing_fields,
782 "final-state parity cannot be inferred because one or both daughter parities are unknown",
783 );
784 }
785
786 let p_final = infer_parity(daughters, l).expect("daughter parities were checked above");
787
788 if p_parent == p_final {
789 RawRuleOutcome::pass(format!(
790 "parity is conserved for L = {} with final parity {:?}",
791 l.value(),
792 p_final,
793 ))
794 } else {
795 RawRuleOutcome::fail(format!(
796 "parity is not conserved for L = {}: parent parity is {:?}, final parity is {:?}",
797 l.value(),
798 p_parent,
799 p_final,
800 ))
801 }
802}
803
804fn check_isospin_raw(
805 parent: &ParticleProperties,
806 daughters: (&ParticleProperties, &ParticleProperties),
807) -> RawRuleOutcome {
808 let Some(i_parent) = parent.isospin else {
809 return RawRuleOutcome::unknown(missing(&["parent.isospin"]), "parent isospin is unknown");
810 };
811
812 let Some(i_a) = daughters.0.isospin else {
813 return RawRuleOutcome::unknown(
814 missing(&["daughter_a.isospin"]),
815 "first daughter isospin is unknown",
816 );
817 };
818
819 let Some(i_b) = daughters.1.isospin else {
820 return RawRuleOutcome::unknown(
821 missing(&["daughter_b.isospin"]),
822 "second daughter isospin is unknown",
823 );
824 };
825
826 if i_parent
827 .isospin()
828 .can_couple_to(i_a.isospin(), i_b.isospin())
829 {
830 RawRuleOutcome::pass("daughter isospins can couple to parent isospin")
831 } else {
832 RawRuleOutcome::fail("daughter isospins cannot couple to parent isospin")
833 }
834}
835
836fn check_isospin_projection_raw(
837 parent: &ParticleProperties,
838 daughters: (&ParticleProperties, &ParticleProperties),
839) -> RawRuleOutcome {
840 let Some(i_parent) = parent.isospin else {
841 return RawRuleOutcome::unknown(missing(&["parent.isospin"]), "parent isospin is unknown");
842 };
843
844 let Some(i_a) = daughters.0.isospin else {
845 return RawRuleOutcome::unknown(
846 missing(&["daughter_a.isospin"]),
847 "first daughter isospin is unknown",
848 );
849 };
850
851 let Some(i_b) = daughters.1.isospin else {
852 return RawRuleOutcome::unknown(
853 missing(&["daughter_b.isospin"]),
854 "second daughter isospin is unknown",
855 );
856 };
857
858 let Some(i3_parent) = i_parent.projection else {
859 return RawRuleOutcome::unknown(
860 missing(&["parent.isospin.projection"]),
861 "parent isospin projection is unknown",
862 );
863 };
864
865 let Some(i3_a) = i_a.projection else {
866 return RawRuleOutcome::unknown(
867 missing(&["daughter_a.isospin.projection"]),
868 "first daughter isospin projection is unknown",
869 );
870 };
871
872 let Some(i3_b) = i_b.projection else {
873 return RawRuleOutcome::unknown(
874 missing(&["daughter_b.isospin.projection"]),
875 "second daughter isospin projection is unknown",
876 );
877 };
878
879 if i3_parent.doubled() == i3_a.doubled() + i3_b.doubled() {
880 RawRuleOutcome::pass("isospin projection is conserved")
881 } else {
882 RawRuleOutcome::fail("isospin projection is not conserved")
883 }
884}
885
886fn check_c_parity_raw(
887 parent: &ParticleProperties,
888 daughters: (&ParticleProperties, &ParticleProperties),
889 l: L,
890 s: S,
891) -> RawRuleOutcome {
892 let Some(c_parent) = parent.c_parity else {
893 return RawRuleOutcome::unknown(
894 missing(&["parent.c_parity"]),
895 "parent C-parity is unknown or not applicable",
896 );
897 };
898
899 let Some(c_final) = infer_c_parity(daughters, l, s) else {
900 return RawRuleOutcome::unknown(
901 missing(&[
902 "daughter_a.species",
903 "daughter_a.antiparticle_species",
904 "daughter_b.species",
905 "daughter_b.antiparticle_species",
906 ]),
907 "final-state C-parity cannot be inferred; this check currently assumes a C-eigenstate particle-antiparticle combination",
908 );
909 };
910
911 if c_parent == c_final {
912 RawRuleOutcome::pass(format!(
913 "C-parity is conserved with inferred C_final = {:?}; assumes a C-eigenstate particle-antiparticle combination",
914 c_final,
915 ))
916 } else {
917 RawRuleOutcome::fail(format!(
918 "C-parity is not conserved: parent C = {:?}, inferred final C = {:?}; assumes a C-eigenstate particle-antiparticle combination",
919 c_parent, c_final,
920 ))
921 }
922}
923
924fn check_g_parity_raw(
925 parent: &ParticleProperties,
926 daughters: (&ParticleProperties, &ParticleProperties),
927) -> RawRuleOutcome {
928 let Some(g_parent) = parent.g_parity else {
929 return RawRuleOutcome::unknown(
930 missing(&["parent.g_parity"]),
931 "parent G-parity is unknown or not applicable",
932 );
933 };
934
935 let Some(g_a) = daughters.0.g_parity else {
936 return RawRuleOutcome::unknown(
937 missing(&["daughter_a.g_parity"]),
938 "first daughter G-parity is unknown or not applicable",
939 );
940 };
941
942 let Some(g_b) = daughters.1.g_parity else {
943 return RawRuleOutcome::unknown(
944 missing(&["daughter_b.g_parity"]),
945 "second daughter G-parity is unknown or not applicable",
946 );
947 };
948
949 let g_final = g_a.value() * g_b.value();
950
951 if g_parent.value() == g_final {
952 RawRuleOutcome::pass("G-parity product check passes")
953 } else {
954 RawRuleOutcome::fail(format!(
955 "G-parity product check fails: parent G = {:?}, daughter product = {}",
956 g_parent, g_final
957 ))
958 }
959}
960
961fn check_additive_raw(
962 field: &'static str,
963 display_name: &'static str,
964 symbol: &'static str,
965 parent: Option<i32>,
966 a: Option<i32>,
967 b: Option<i32>,
968) -> RawRuleOutcome {
969 match (parent, a, b) {
970 (Some(parent), Some(a), Some(b)) => {
971 let final_value = a + b;
972
973 if parent == final_value {
974 RawRuleOutcome::pass(format!("{display_name} is conserved"))
975 } else {
976 RawRuleOutcome::fail(format!(
977 "{display_name} is not conserved: parent {symbol} = {parent}, final {symbol} = {final_value}",
978 ))
979 }
980 }
981
982 _ => {
983 let mut missing_fields = Vec::new();
984
985 if parent.is_none() {
986 missing_fields.push(format!("parent.{field}"));
987 }
988 if a.is_none() {
989 missing_fields.push(format!("daughter_a.{field}"));
990 }
991 if b.is_none() {
992 missing_fields.push(format!("daughter_b.{field}"));
993 }
994
995 RawRuleOutcome::unknown(
996 missing_fields,
997 format!("{display_name} cannot be checked because required values are unknown"),
998 )
999 }
1000 }
1001}
1002
1003fn check_total_lepton_number_raw(
1004 parent: &ParticleProperties,
1005 daughters: (&ParticleProperties, &ParticleProperties),
1006) -> RawRuleOutcome {
1007 let values = [
1008 (
1009 "parent.electron_lepton_number",
1010 parent.electron_lepton_number,
1011 ),
1012 ("parent.muon_lepton_number", parent.muon_lepton_number),
1013 ("parent.tau_lepton_number", parent.tau_lepton_number),
1014 (
1015 "daughter_a.electron_lepton_number",
1016 daughters.0.electron_lepton_number,
1017 ),
1018 (
1019 "daughter_a.muon_lepton_number",
1020 daughters.0.muon_lepton_number,
1021 ),
1022 (
1023 "daughter_a.tau_lepton_number",
1024 daughters.0.tau_lepton_number,
1025 ),
1026 (
1027 "daughter_b.electron_lepton_number",
1028 daughters.1.electron_lepton_number,
1029 ),
1030 (
1031 "daughter_b.muon_lepton_number",
1032 daughters.1.muon_lepton_number,
1033 ),
1034 (
1035 "daughter_b.tau_lepton_number",
1036 daughters.1.tau_lepton_number,
1037 ),
1038 ];
1039
1040 let missing_fields: Vec<String> = values
1041 .iter()
1042 .filter_map(|(name, value)| {
1043 if value.is_none() {
1044 Some((*name).to_string())
1045 } else {
1046 None
1047 }
1048 })
1049 .collect();
1050
1051 if !missing_fields.is_empty() {
1052 return RawRuleOutcome::unknown(
1053 missing_fields,
1054 "total lepton number cannot be checked because required values are unknown",
1055 );
1056 }
1057
1058 let parent_total = parent.electron_lepton_number.unwrap()
1059 + parent.muon_lepton_number.unwrap()
1060 + parent.tau_lepton_number.unwrap();
1061
1062 let daughter_total = daughters.0.electron_lepton_number.unwrap()
1063 + daughters.0.muon_lepton_number.unwrap()
1064 + daughters.0.tau_lepton_number.unwrap()
1065 + daughters.1.electron_lepton_number.unwrap()
1066 + daughters.1.muon_lepton_number.unwrap()
1067 + daughters.1.tau_lepton_number.unwrap();
1068
1069 if parent_total == daughter_total {
1070 RawRuleOutcome::pass("total lepton number is conserved")
1071 } else {
1072 RawRuleOutcome::fail(format!(
1073 "total lepton number is not conserved: parent L = {parent_total}, final L = {daughter_total}",
1074 ))
1075 }
1076}
1077
1078fn check_identical_particle_symmetry_raw(
1079 daughters: (&ParticleProperties, &ParticleProperties),
1080 l: L,
1081 s: S,
1082) -> RawRuleOutcome {
1083 let Some(species_a) = daughters.0.species.as_ref() else {
1084 return RawRuleOutcome::unknown(
1085 missing(&["daughter_a.species"]),
1086 "first daughter species is unknown",
1087 );
1088 };
1089
1090 let Some(species_b) = daughters.1.species.as_ref() else {
1091 return RawRuleOutcome::unknown(
1092 missing(&["daughter_b.species"]),
1093 "second daughter species is unknown",
1094 );
1095 };
1096
1097 if species_a != species_b {
1098 return RawRuleOutcome::pass("daughters are not identical particles");
1099 }
1100
1101 let Some(stats_a) = daughters.0.statistics else {
1102 return RawRuleOutcome::unknown(
1103 missing(&["daughter_a.statistics"]),
1104 "first daughter statistics are unknown",
1105 );
1106 };
1107
1108 let Some(stats_b) = daughters.1.statistics else {
1109 return RawRuleOutcome::unknown(
1110 missing(&["daughter_b.statistics"]),
1111 "second daughter statistics are unknown",
1112 );
1113 };
1114
1115 if stats_a != stats_b {
1116 return RawRuleOutcome::fail(
1117 "identical particles have inconsistent statistics assignments",
1118 );
1119 }
1120
1121 let Some(ja) = daughters.0.spin else {
1122 return RawRuleOutcome::unknown(
1123 missing(&["daughter_a.spin"]),
1124 "first daughter spin is unknown",
1125 );
1126 };
1127
1128 let Some(jb) = daughters.1.spin else {
1129 return RawRuleOutcome::unknown(
1130 missing(&["daughter_b.spin"]),
1131 "second daughter spin is unknown",
1132 );
1133 };
1134
1135 if ja != jb {
1136 return RawRuleOutcome::fail("identical particles have inconsistent spin assignments");
1137 }
1138
1139 if !s.doubled().is_multiple_of(2) {
1140 return RawRuleOutcome::fail(
1141 "two identical particles cannot couple to half-integer total spin",
1142 );
1143 }
1144
1145 let s_integer = s.doubled() / 2;
1146
1147 if s_integer > ja.doubled() {
1148 return RawRuleOutcome::fail(
1149 "coupled spin is incompatible with two identical daughter spins",
1150 );
1151 }
1152
1153 let exchange_exponent = l.value() + ja.doubled() - s_integer;
1158 let exchange_is_symmetric = exchange_exponent.is_multiple_of(2);
1159
1160 let allowed = match stats_a {
1161 Statistics::Boson => exchange_is_symmetric,
1162 Statistics::Fermion => !exchange_is_symmetric,
1163 };
1164
1165 if allowed {
1166 RawRuleOutcome::pass("identical-particle exchange symmetry is satisfied")
1167 } else {
1168 RawRuleOutcome::fail("identical-particle exchange symmetry is violated")
1169 }
1170}
1171
1172fn check_conventional_meson_jpc_raw(parent: &ParticleProperties) -> RawRuleOutcome {
1173 let Some(j) = parent.spin else {
1174 return RawRuleOutcome::unknown(missing(&["parent.spin"]), "parent spin is unknown");
1175 };
1176
1177 let Some(p) = parent.parity else {
1178 return RawRuleOutcome::unknown(missing(&["parent.parity"]), "parent parity is unknown");
1179 };
1180
1181 let Some(c) = parent.c_parity else {
1182 return RawRuleOutcome::unknown(
1183 missing(&["parent.c_parity"]),
1184 "parent C-parity is unknown or not applicable",
1185 );
1186 };
1187
1188 if !j.doubled().is_multiple_of(2) {
1189 return RawRuleOutcome::fail(
1190 "half-integer J is not compatible with a conventional meson assignment",
1191 );
1192 }
1193
1194 let target_j = j.doubled();
1195
1196 let max_l = target_j / 2 + 1;
1207
1208 for l_raw in 0..=max_l {
1209 for s_raw in [0u32, 1u32] {
1210 let l_doubled = 2 * l_raw;
1211 let s_doubled = 2 * s_raw;
1212
1213 let min_j = l_doubled.abs_diff(s_doubled);
1214 let max_j = l_doubled + s_doubled;
1215
1216 let angular_ok = target_j >= min_j && target_j <= max_j;
1217 let parity_ok = p == L::int(l_raw + 1).orbital_parity();
1218 let c_ok = c == L::int(l_raw + s_raw).orbital_parity();
1219
1220 if angular_ok && parity_ok && c_ok {
1221 return RawRuleOutcome::pass(
1222 "J^PC is compatible with a conventional q qbar meson assignment",
1223 );
1224 }
1225 }
1226 }
1227
1228 RawRuleOutcome::fail(format!(
1229 "J^PC = {}{:?}{:?} is exotic for a conventional q qbar meson assignment",
1230 j, p, c,
1231 ))
1232}
1233
1234#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
1237pub struct PartialWave {
1238 pub j: J,
1240 pub l: L,
1242 pub s: S,
1244}
1245impl PartialWave {
1246 pub fn new(j: J, l: L, s: S) -> LadduPhysicsResult<Self> {
1253 PartialWave::validate_coupling(j, l, s)?;
1254 Ok(Self { j, l, s })
1255 }
1256 pub fn label(&self) -> String {
1259 let multiplicity = self.s.doubled() + 1;
1260 format!("{}{}{}", multiplicity, self.l, self.j)
1261 }
1262 pub fn validate_coupling(j: J, l: L, s: S) -> LadduPhysicsResult<()> {
1269 let l_twice = 2 * l.value();
1270 let s_twice = s.doubled();
1271 let j_twice = j.doubled();
1272 let min = l_twice.abs_diff(s_twice);
1273 let max = l_twice + s_twice;
1274 if j_twice >= min && j_twice <= max && (j_twice - min).is_multiple_of(2) {
1275 Ok(())
1276 } else {
1277 Err(LadduPhysicsError::invalid_relation(
1278 "j, l, and s must be compatible",
1279 ))
1280 }
1281 }
1282}
1283
1284impl Display for PartialWave {
1285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1286 write!(f, "{}", self.label())
1287 }
1288}
1289
1290#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
1292pub struct AllowedPartialWave {
1293 pub wave: PartialWave,
1295 pub parity: Option<Parity>,
1297 pub c_parity: Option<Parity>,
1299}
1300
1301impl AllowedPartialWave {
1302 pub fn new(wave: PartialWave, daughters: (&ParticleProperties, &ParticleProperties)) -> Self {
1304 Self {
1305 parity: infer_parity(daughters, wave.l),
1306 c_parity: infer_c_parity(daughters, wave.l, wave.s),
1307 wave,
1308 }
1309 }
1310}
1311
1312fn infer_parity(daughters: (&ParticleProperties, &ParticleProperties), l: L) -> Option<Parity> {
1313 Some(daughters.0.parity? * daughters.1.parity? * l.orbital_parity())
1314}
1315
1316fn infer_c_parity(
1317 daughters: (&ParticleProperties, &ParticleProperties),
1318 l: L,
1319 s: S,
1320) -> Option<Parity> {
1321 daughters.0.is_antiparticle_of(daughters.1).then_some(())?;
1322 let s_doubled = s.doubled();
1323 if !s_doubled.is_multiple_of(2) {
1324 return None;
1325 }
1326 Some(L::int(l.value() + (s_doubled / 2)).orbital_parity())
1327}
1328
1329#[derive(Clone, Debug, Eq, PartialEq)]
1330pub struct PartialWaveCandidate {
1333 pub wave: PartialWave,
1335 pub inferred: AllowedPartialWave,
1337 pub report: RuleReport,
1339}
1340
1341impl PartialWaveCandidate {
1342 pub fn is_allowed(&self) -> bool {
1344 self.report.is_allowed()
1345 }
1346}
1347
1348#[derive(Clone, Debug, Eq, PartialEq, Default)]
1349pub struct PartialWaveScan {
1351 pub candidates: Vec<PartialWaveCandidate>,
1353 pub missing_inputs: Vec<String>,
1355}
1356
1357impl PartialWaveScan {
1358 pub fn allowed(&self) -> impl Iterator<Item = &AllowedPartialWave> {
1360 self.candidates
1361 .iter()
1362 .filter(|candidate| candidate.is_allowed())
1363 .map(|candidate| &candidate.inferred)
1364 }
1365
1366 pub fn rejected(&self) -> impl Iterator<Item = &PartialWaveCandidate> {
1368 self.candidates
1369 .iter()
1370 .filter(|candidate| !candidate.is_allowed())
1371 }
1372
1373 pub fn into_allowed(self) -> Vec<AllowedPartialWave> {
1375 self.candidates
1376 .into_iter()
1377 .filter_map(|candidate| {
1378 if candidate.is_allowed() {
1379 Some(candidate.inferred)
1380 } else {
1381 None
1382 }
1383 })
1384 .collect()
1385 }
1386}
1387
1388#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1400pub struct SelectionRules {
1401 pub rules: RuleSet,
1408 pub max_l: L,
1414}
1415
1416impl Default for SelectionRules {
1417 fn default() -> Self {
1418 Self::strong(L::int(6))
1419 }
1420}
1421
1422impl SelectionRules {
1423 pub fn new(rules: RuleSet, max_l: L) -> Self {
1426 Self { rules, max_l }
1427 }
1428
1429 pub fn angular(max_l: L) -> Self {
1431 Self::new(RuleSet::angular(), max_l)
1432 }
1433
1434 pub fn electromagnetic(max_l: L) -> Self {
1436 Self::new(RuleSet::electromagnetic(), max_l)
1437 }
1438
1439 pub fn weak(max_l: L) -> Self {
1441 Self::new(RuleSet::weak(), max_l)
1442 }
1443
1444 pub fn strong(max_l: L) -> Self {
1446 Self::new(RuleSet::strong(), max_l)
1447 }
1448 pub fn coupled_spins(a: J, b: J) -> Vec<S> {
1456 a.coupled_with(b)
1457 }
1458
1459 pub fn scan_partial_waves(
1462 &self,
1463 parent: &ParticleProperties,
1464 daughters: (&ParticleProperties, &ParticleProperties),
1465 ) -> PartialWaveScan {
1466 let mut missing_inputs = Vec::new();
1467
1468 let Some(parent_j) = parent.spin else {
1469 missing_inputs.push("parent.spin".to_string());
1470 return PartialWaveScan {
1471 candidates: Vec::new(),
1472 missing_inputs,
1473 };
1474 };
1475
1476 let Some(ja) = daughters.0.spin else {
1477 missing_inputs.push("daughter_a.spin".to_string());
1478 return PartialWaveScan {
1479 candidates: Vec::new(),
1480 missing_inputs,
1481 };
1482 };
1483
1484 let Some(jb) = daughters.1.spin else {
1485 missing_inputs.push("daughter_b.spin".to_string());
1486 return PartialWaveScan {
1487 candidates: Vec::new(),
1488 missing_inputs,
1489 };
1490 };
1491
1492 let mut candidates = Vec::new();
1493
1494 for s in Self::coupled_spins(ja, jb) {
1495 for l_raw in 0..=self.max_l.value() {
1496 let l = L::int(l_raw);
1497
1498 let Ok(wave) = PartialWave::new(parent_j, l, s) else {
1499 continue;
1500 };
1501
1502 let report = self.rules.evaluate(parent, daughters, l, s);
1503 let inferred = AllowedPartialWave::new(wave, daughters);
1504
1505 candidates.push(PartialWaveCandidate {
1506 wave,
1507 inferred,
1508 report,
1509 });
1510 }
1511 }
1512
1513 PartialWaveScan {
1514 candidates,
1515 missing_inputs,
1516 }
1517 }
1518
1519 pub fn allowed_partial_waves(
1539 &self,
1540 parent: &ParticleProperties,
1541 daughters: (&ParticleProperties, &ParticleProperties),
1542 ) -> Vec<AllowedPartialWave> {
1543 self.scan_partial_waves(parent, daughters).into_allowed()
1544 }
1545}
1546
1547#[cfg(test)]
1548mod tests {
1549 use super::*;
1550 use crate::{
1551 j, l, m,
1552 quantum::{Isospin, M},
1553 };
1554
1555 fn labels(waves: &[AllowedPartialWave]) -> Vec<String> {
1556 waves.iter().map(|w| w.wave.label()).collect()
1557 }
1558
1559 fn allowed_labels<'a>(waves: impl Iterator<Item = &'a AllowedPartialWave>) -> Vec<String> {
1560 waves.map(|w| w.wave.label()).collect()
1561 }
1562
1563 fn candidate_labels<'a>(
1564 candidates: impl Iterator<Item = &'a PartialWaveCandidate>,
1565 ) -> Vec<String> {
1566 candidates.map(|candidate| candidate.wave.label()).collect()
1567 }
1568
1569 fn outcome(report: &RuleReport, rule: RuleKind) -> &RuleOutcome {
1570 report
1571 .outcome(rule)
1572 .unwrap_or_else(|| panic!("missing outcome for {rule:?}; report was {report:#?}"))
1573 }
1574
1575 fn assert_pass(report: &RuleReport, rule: RuleKind) {
1576 assert!(
1577 matches!(outcome(report, rule), RuleOutcome::Pass { .. }),
1578 "expected {rule:?} to pass; got {:#?}",
1579 outcome(report, rule)
1580 );
1581 }
1582
1583 fn assert_fail(report: &RuleReport, rule: RuleKind) {
1584 assert!(
1585 matches!(outcome(report, rule), RuleOutcome::Fail { .. }),
1586 "expected {rule:?} to fail; got {:#?}",
1587 outcome(report, rule)
1588 );
1589 }
1590
1591 fn assert_unknown_allowed(report: &RuleReport, rule: RuleKind, expected_missing: &[&str]) {
1592 match outcome(report, rule) {
1593 RuleOutcome::UnknownAllowed { missing, .. } => {
1594 for field in expected_missing {
1595 assert!(
1596 missing.iter().any(|missing| missing == field),
1597 "expected missing field {field:?}; got {missing:?}"
1598 );
1599 }
1600 }
1601 other => panic!("expected {rule:?} to be UnknownAllowed; got {other:#?}"),
1602 }
1603 }
1604
1605 fn assert_warning(report: &RuleReport, rule: RuleKind, expected_missing: &[&str]) {
1606 match outcome(report, rule) {
1607 RuleOutcome::Warning { missing, .. } => {
1608 for field in expected_missing {
1609 assert!(
1610 missing.iter().any(|missing| missing == field),
1611 "expected missing field {field:?}; got {missing:?}"
1612 );
1613 }
1614 }
1615 other => panic!("expected {rule:?} to be Warning; got {other:#?}"),
1616 }
1617 }
1618
1619 fn assert_ignored(report: &RuleReport, rule: RuleKind, expected_reason: Option<&str>) {
1620 match outcome(report, rule) {
1621 RuleOutcome::Ignored { reason } => {
1622 assert_eq!(reason.as_deref(), expected_reason);
1623 }
1624 other => panic!("expected {rule:?} to be Ignored; got {other:#?}"),
1625 }
1626 }
1627
1628 fn assert_diagnostic(
1629 report: &RuleReport,
1630 rule: RuleKind,
1631 expected_passed: Option<bool>,
1632 expected_reason: Option<&str>,
1633 ) {
1634 match outcome(report, rule) {
1635 RuleOutcome::Diagnostic { passed, reason, .. } => {
1636 assert_eq!(*passed, expected_passed);
1637 assert_eq!(reason.as_deref(), expected_reason);
1638 }
1639 other => panic!("expected {rule:?} to be Diagnostic; got {other:#?}"),
1640 }
1641 }
1642
1643 #[allow(clippy::too_many_arguments)]
1644 fn add_additives(
1645 particle: ParticleProperties,
1646 charge: i32,
1647 strangeness: i32,
1648 charm: i32,
1649 bottomness: i32,
1650 topness: i32,
1651 baryon_number: i32,
1652 electron_lepton_number: i32,
1653 muon_lepton_number: i32,
1654 tau_lepton_number: i32,
1655 ) -> ParticleProperties {
1656 particle
1657 .with_charge(charge)
1658 .with_strangeness(strangeness)
1659 .unwrap()
1660 .with_charm(charm)
1661 .unwrap()
1662 .with_bottomness(bottomness)
1663 .unwrap()
1664 .with_topness(topness)
1665 .unwrap()
1666 .with_baryon_number(baryon_number)
1667 .unwrap()
1668 .with_electron_lepton_number(electron_lepton_number)
1669 .unwrap()
1670 .with_muon_lepton_number(muon_lepton_number)
1671 .unwrap()
1672 .with_tau_lepton_number(tau_lepton_number)
1673 .unwrap()
1674 }
1675
1676 fn pion_like(name: &str, anti_name: &str, charge: i32, i3: i32) -> ParticleProperties {
1677 ParticleProperties::meson()
1678 .with_zero_flavor()
1679 .with_name(name)
1680 .with_species_names(name, anti_name)
1681 .unwrap()
1682 .with_spin(j!(0))
1683 .with_parity(Parity::Negative)
1684 .with_charge(charge)
1685 .with_isospin(Isospin::new(j!(1), Some(M::int(i3))).unwrap())
1686 .with_g_parity(Parity::Negative)
1687 .with_statistics(Statistics::Boson)
1688 .unwrap()
1689 }
1690
1691 fn rho_like() -> ParticleProperties {
1692 ParticleProperties::meson()
1693 .with_zero_flavor()
1694 .with_name("rho0")
1695 .with_self_conjugate_species("rho0")
1696 .unwrap()
1697 .with_spin(j!(1))
1698 .with_parity(Parity::Negative)
1699 .with_c_parity(Parity::Negative)
1700 .unwrap()
1701 .with_charge(0)
1702 .with_isospin(Isospin::new(j!(1), Some(m!(0))).unwrap())
1703 .with_g_parity(Parity::Positive)
1704 .with_statistics(Statistics::Boson)
1705 .unwrap()
1706 }
1707
1708 fn exotic_one_minus_plus() -> ParticleProperties {
1709 ParticleProperties::meson()
1710 .with_zero_flavor()
1711 .with_name("pi1_exotic")
1712 .with_self_conjugate_species("pi1_exotic")
1713 .unwrap()
1714 .with_spin(j!(1))
1715 .with_parity(Parity::Negative)
1716 .with_c_parity(Parity::Positive)
1717 .unwrap()
1718 .with_charge(0)
1719 .with_isospin(Isospin::new(j!(1), Some(m!(0))).unwrap())
1720 .with_statistics(Statistics::Boson)
1721 .unwrap()
1722 }
1723
1724 fn identical_boson(spin: J, species: &str) -> ParticleProperties {
1725 ParticleProperties::unknown()
1726 .with_spin(spin)
1727 .with_species(species)
1728 .unwrap()
1729 .with_statistics(Statistics::Boson)
1730 .unwrap()
1731 }
1732
1733 fn identical_fermion(spin: J, species: &str) -> ParticleProperties {
1734 ParticleProperties::unknown()
1735 .with_spin(spin)
1736 .with_species(species)
1737 .unwrap()
1738 .with_statistics(Statistics::Fermion)
1739 .unwrap()
1740 }
1741
1742 #[test]
1743 fn rule_set_constructors_build_expected_default_policies() {
1744 let angular = RuleSet::angular();
1745 assert_eq!(angular.enabled_rules().count(), 0);
1746
1747 let strong = RuleSet::strong();
1748 for rule in [
1749 RuleKind::Parity,
1750 RuleKind::Isospin,
1751 RuleKind::IsospinProjection,
1752 RuleKind::Charge,
1753 RuleKind::Strangeness,
1754 RuleKind::Charm,
1755 RuleKind::Bottomness,
1756 RuleKind::Topness,
1757 RuleKind::BaryonNumber,
1758 RuleKind::IdenticalParticleSymmetry,
1759 ] {
1760 assert!(
1761 matches!(strong.policy(rule).unwrap().mode, RuleMode::Enforce),
1762 "strong rules should enforce {rule:?}"
1763 );
1764 }
1765 assert!(strong.policy(RuleKind::CParity).is_none());
1766 assert!(strong.policy(RuleKind::GParity).is_none());
1767 assert!(strong.policy(RuleKind::ConventionalMesonJpc).is_none());
1768
1769 let electromagnetic = RuleSet::electromagnetic();
1770 assert!(electromagnetic.policy(RuleKind::Parity).is_some());
1771 assert!(electromagnetic.policy(RuleKind::Charge).is_some());
1772 assert!(
1773 electromagnetic
1774 .policy(RuleKind::IsospinProjection)
1775 .is_some()
1776 );
1777 assert!(electromagnetic.policy(RuleKind::Isospin).is_none());
1778
1779 let weak = RuleSet::weak();
1780 assert!(weak.policy(RuleKind::Charge).is_some());
1781 assert!(weak.policy(RuleKind::BaryonNumber).is_some());
1782 assert!(weak.policy(RuleKind::ElectronLeptonNumber).is_some());
1783 assert!(weak.policy(RuleKind::MuonLeptonNumber).is_some());
1784 assert!(weak.policy(RuleKind::TauLeptonNumber).is_some());
1785 assert!(weak.policy(RuleKind::LeptonNumber).is_some());
1786 assert!(weak.policy(RuleKind::Parity).is_none());
1787 assert!(weak.policy(RuleKind::Strangeness).is_none());
1788 }
1789
1790 #[test]
1791 fn rule_set_builder_and_mut_methods_configure_the_same_policies() {
1792 let built = RuleSet::angular()
1793 .enforce(RuleKind::Parity)
1794 .enforce_strict(RuleKind::Charge)
1795 .set_policy(
1796 RuleKind::Strangeness,
1797 RulePolicy::enforce_with_unknown_policy(UnknownPolicy::Warn),
1798 )
1799 .ignore(RuleKind::Isospin, "intentional isospin violation")
1800 .ignore_without_reason(RuleKind::GParity)
1801 .diagnose_only(
1802 RuleKind::ConventionalMesonJpc,
1803 "classify exotics without rejecting them",
1804 )
1805 .diagnose_only_without_reason(RuleKind::CParity)
1806 .with_unknown_policy(RuleKind::Bottomness, UnknownPolicy::Reject)
1807 .disable(RuleKind::CParity);
1808
1809 let mut mutated = RuleSet::angular();
1810 mutated
1811 .enforce_mut(RuleKind::Parity)
1812 .enforce_strict_mut(RuleKind::Charge)
1813 .set_policy_mut(
1814 RuleKind::Strangeness,
1815 RulePolicy::enforce_with_unknown_policy(UnknownPolicy::Warn),
1816 )
1817 .ignore_mut(RuleKind::Isospin, "intentional isospin violation")
1818 .ignore_without_reason_mut(RuleKind::GParity)
1819 .diagnose_only_mut(
1820 RuleKind::ConventionalMesonJpc,
1821 "classify exotics without rejecting them",
1822 )
1823 .diagnose_only_without_reason_mut(RuleKind::CParity)
1824 .with_unknown_policy_mut(RuleKind::Bottomness, UnknownPolicy::Reject)
1825 .disable_mut(RuleKind::CParity);
1826
1827 assert_eq!(built, mutated);
1828
1829 assert!(matches!(
1830 built.policy(RuleKind::Parity).unwrap().mode,
1831 RuleMode::Enforce
1832 ));
1833 assert_eq!(
1834 built.policy(RuleKind::Charge).unwrap().unknown,
1835 UnknownPolicy::Reject
1836 );
1837 assert_eq!(
1838 built.policy(RuleKind::Strangeness).unwrap().unknown,
1839 UnknownPolicy::Warn
1840 );
1841 assert!(matches!(
1842 built.policy(RuleKind::Isospin).unwrap().mode,
1843 RuleMode::Ignore { reason: Some(_) }
1844 ));
1845 assert!(matches!(
1846 built.policy(RuleKind::GParity).unwrap().mode,
1847 RuleMode::Ignore { reason: None }
1848 ));
1849 assert!(matches!(
1850 built.policy(RuleKind::ConventionalMesonJpc).unwrap().mode,
1851 RuleMode::DiagnoseOnly { reason: Some(_) }
1852 ));
1853 assert_eq!(
1854 built.policy(RuleKind::Bottomness).unwrap().unknown,
1855 UnknownPolicy::Reject
1856 );
1857 assert!(built.policy(RuleKind::CParity).is_none());
1858 }
1859
1860 #[test]
1861 fn policy_application_distinguishes_unknown_allowed_warning_reject_ignore_and_diagnostic() {
1862 let parent = ParticleProperties::unknown().with_spin(j!(0));
1863 let a = ParticleProperties::unknown().with_spin(j!(0));
1864 let b = ParticleProperties::unknown().with_spin(j!(0));
1865
1866 let rules = RuleSet::angular()
1867 .enforce(RuleKind::Parity)
1868 .set_policy(
1869 RuleKind::Charge,
1870 RulePolicy::enforce_with_unknown_policy(UnknownPolicy::Warn),
1871 )
1872 .enforce_strict(RuleKind::Strangeness)
1873 .ignore(RuleKind::Isospin, "not relevant for this model")
1874 .diagnose_only(
1875 RuleKind::ConventionalMesonJpc,
1876 "only classify the parent assignment",
1877 );
1878
1879 let report = rules.evaluate(&parent, (&a, &b), l!(0), j!(0));
1880
1881 assert!(!report.is_allowed());
1882 assert_eq!(report.len(), 5);
1883 assert!(report.has_failures());
1884 assert!(report.has_unknowns());
1885 assert!(report.has_ignored());
1886 assert_eq!(report.failures().count(), 1);
1887 assert_eq!(report.warnings().count(), 1);
1888 assert_eq!(report.unknowns().count(), 1);
1889 assert_eq!(report.ignored().count(), 1);
1890
1891 assert_unknown_allowed(&report, RuleKind::Parity, &["parent.parity"]);
1892 assert_warning(
1893 &report,
1894 RuleKind::Charge,
1895 &["parent.charge", "daughter_a.charge", "daughter_b.charge"],
1896 );
1897 assert_fail(&report, RuleKind::Strangeness);
1898 assert_ignored(
1899 &report,
1900 RuleKind::Isospin,
1901 Some("not relevant for this model"),
1902 );
1903 assert_diagnostic(
1904 &report,
1905 RuleKind::ConventionalMesonJpc,
1906 None,
1907 Some("only classify the parent assignment"),
1908 );
1909 }
1910
1911 #[test]
1912 fn angular_momentum_helpers_partial_wave_validation_and_inference_work_together() {
1913 assert_eq!(
1914 SelectionRules::coupled_spins(j!(1 / 2), j!(1 / 2)),
1915 vec![j!(0), j!(1)]
1916 );
1917 assert_eq!(
1918 SelectionRules::coupled_spins(j!(1 / 2), j!(1)),
1919 vec![j!(1 / 2), j!(3 / 2)]
1920 );
1921 assert_eq!(
1922 SelectionRules::coupled_spins(j!(1), j!(1)),
1923 vec![j!(0), j!(1), j!(2)]
1924 );
1925
1926 let wave = PartialWave::new(j!(1), l!(1), j!(0)).unwrap();
1927 assert_eq!(wave.label(), "1P1");
1928 assert_eq!(wave.to_string(), "1P1");
1929
1930 assert!(PartialWave::new(j!(1), l!(0), j!(0)).is_err());
1931
1932 let pi_plus = pion_like("pi+", "pi-", 1, 1);
1933 let pi_minus = pion_like("pi-", "pi+", -1, -1);
1934 let allowed = AllowedPartialWave::new(wave, (&pi_plus, &pi_minus));
1935
1936 assert_eq!(allowed.parity, Some(Parity::Negative));
1937 assert_eq!(allowed.c_parity, Some(Parity::Negative));
1938
1939 let non_c_pair = AllowedPartialWave::new(
1940 PartialWave::new(j!(0), l!(0), j!(0)).unwrap(),
1941 (&pi_plus, &pion_like("pi0", "pi0", 0, 0)),
1942 );
1943 assert_eq!(non_c_pair.parity, Some(Parity::Positive));
1944 assert_eq!(non_c_pair.c_parity, None);
1945 }
1946
1947 #[test]
1948 fn complete_strong_plus_c_and_g_rules_pass_for_rho_like_to_charged_pions() {
1949 let parent = rho_like();
1950 let pi_plus = pion_like("pi+", "pi-", 1, 1);
1951 let pi_minus = pion_like("pi-", "pi+", -1, -1);
1952
1953 let rules = RuleSet::strong()
1954 .enforce(RuleKind::CParity)
1955 .enforce(RuleKind::GParity)
1956 .enforce(RuleKind::ElectronLeptonNumber)
1957 .enforce(RuleKind::MuonLeptonNumber)
1958 .enforce(RuleKind::TauLeptonNumber)
1959 .enforce(RuleKind::LeptonNumber);
1960
1961 let report = rules.evaluate(&parent, (&pi_plus, &pi_minus), l!(1), j!(0));
1962
1963 assert!(report.is_allowed());
1964 assert_eq!(report.len(), rules.enabled_rules().count());
1965 assert!(
1966 report
1967 .checks
1968 .iter()
1969 .all(|check| { matches!(check.outcome, RuleOutcome::Pass { .. }) })
1970 );
1971
1972 for rule in rules.enabled_rules() {
1973 assert_pass(&report, rule);
1974 }
1975 }
1976
1977 #[test]
1978 fn nontrivial_quantum_number_rules_report_failures() {
1979 let parent = ParticleProperties::meson()
1980 .with_zero_flavor()
1981 .with_name("bad_parent")
1982 .with_self_conjugate_species("bad_parent")
1983 .unwrap()
1984 .with_spin(j!(1))
1985 .with_parity(Parity::Positive)
1986 .with_c_parity(Parity::Positive)
1987 .unwrap()
1988 .with_charge(0)
1989 .with_isospin(Isospin::new(j!(3), Some(m!(1))).unwrap())
1990 .with_g_parity(Parity::Negative)
1991 .with_statistics(Statistics::Boson)
1992 .unwrap();
1993
1994 let pi_plus = pion_like("pi+", "pi-", 1, 1);
1995 let pi_minus = pion_like("pi-", "pi+", -1, -1);
1996
1997 let rules = RuleSet::angular()
1998 .enforce(RuleKind::Parity)
1999 .enforce(RuleKind::Isospin)
2000 .enforce(RuleKind::IsospinProjection)
2001 .enforce(RuleKind::CParity)
2002 .enforce(RuleKind::GParity);
2003
2004 let report = rules.evaluate(&parent, (&pi_plus, &pi_minus), l!(1), j!(0));
2005
2006 assert!(!report.is_allowed());
2007 assert_eq!(report.failures().count(), 5);
2008
2009 assert_fail(&report, RuleKind::Parity);
2010 assert_fail(&report, RuleKind::Isospin);
2011 assert_fail(&report, RuleKind::IsospinProjection);
2012 assert_fail(&report, RuleKind::CParity);
2013 assert_fail(&report, RuleKind::GParity);
2014 }
2015
2016 #[test]
2017 fn additive_and_lepton_rules_report_passes_failures_and_missing_fields() {
2018 let parent = add_additives(
2019 ParticleProperties::unknown(),
2020 1, 1, 1, 1, 1, 1, 1, 0,
2028 0,
2029 );
2030
2031 let daughter_a = add_additives(ParticleProperties::unknown(), 0, 0, 0, 0, 0, 0, 0, 1, 0);
2032 let daughter_b = add_additives(ParticleProperties::unknown(), 0, 0, 0, 0, 0, 0, 0, 0, 0);
2033
2034 let rules = RuleSet::angular()
2035 .enforce(RuleKind::Charge)
2036 .enforce(RuleKind::Strangeness)
2037 .enforce(RuleKind::Charm)
2038 .enforce(RuleKind::Bottomness)
2039 .enforce(RuleKind::Topness)
2040 .enforce(RuleKind::BaryonNumber)
2041 .enforce(RuleKind::ElectronLeptonNumber)
2042 .enforce(RuleKind::MuonLeptonNumber)
2043 .enforce(RuleKind::TauLeptonNumber)
2044 .enforce(RuleKind::LeptonNumber);
2045
2046 let report = rules.evaluate(&parent, (&daughter_a, &daughter_b), l!(0), j!(0));
2047
2048 assert!(!report.is_allowed());
2049
2050 for rule in [
2051 RuleKind::Charge,
2052 RuleKind::Strangeness,
2053 RuleKind::Charm,
2054 RuleKind::Bottomness,
2055 RuleKind::Topness,
2056 RuleKind::BaryonNumber,
2057 RuleKind::ElectronLeptonNumber,
2058 RuleKind::MuonLeptonNumber,
2059 ] {
2060 assert_fail(&report, rule);
2061 }
2062
2063 assert_pass(&report, RuleKind::TauLeptonNumber);
2064 assert_pass(&report, RuleKind::LeptonNumber);
2065
2066 let parent_missing = ParticleProperties::unknown().with_charge(0);
2067 let a_missing = ParticleProperties::unknown().with_charge(0);
2068 let b_missing = ParticleProperties::unknown();
2069
2070 let unknown_report = RuleSet::angular()
2071 .enforce(RuleKind::Charge)
2072 .enforce(RuleKind::LeptonNumber)
2073 .evaluate(&parent_missing, (&a_missing, &b_missing), l!(0), j!(0));
2074
2075 assert!(unknown_report.is_allowed());
2076 assert_unknown_allowed(&unknown_report, RuleKind::Charge, &["daughter_b.charge"]);
2077 assert_unknown_allowed(
2078 &unknown_report,
2079 RuleKind::LeptonNumber,
2080 &[
2081 "parent.electron_lepton_number",
2082 "daughter_a.electron_lepton_number",
2083 "daughter_b.electron_lepton_number",
2084 ],
2085 );
2086 }
2087
2088 #[test]
2089 fn identical_particle_symmetry_handles_bosons_and_fermions_with_spin_dependence() {
2090 let scalar_a = identical_boson(j!(0), "scalar");
2091 let scalar_b = identical_boson(j!(0), "scalar");
2092
2093 let vector_a = identical_boson(j!(1), "vector");
2094 let vector_b = identical_boson(j!(1), "vector");
2095
2096 let fermion_a = identical_fermion(j!(1 / 2), "fermion");
2097 let fermion_b = identical_fermion(j!(1 / 2), "fermion");
2098
2099 let rules = RuleSet::angular().enforce(RuleKind::IdenticalParticleSymmetry);
2100
2101 let scalar_even_l = rules.evaluate(
2102 &ParticleProperties::unknown(),
2103 (&scalar_a, &scalar_b),
2104 l!(0),
2105 j!(0),
2106 );
2107 assert_pass(&scalar_even_l, RuleKind::IdenticalParticleSymmetry);
2108
2109 let scalar_odd_l = rules.evaluate(
2110 &ParticleProperties::unknown(),
2111 (&scalar_a, &scalar_b),
2112 l!(1),
2113 j!(0),
2114 );
2115 assert_fail(&scalar_odd_l, RuleKind::IdenticalParticleSymmetry);
2116
2117 let vector_s0 = rules.evaluate(
2118 &ParticleProperties::unknown(),
2119 (&vector_a, &vector_b),
2120 l!(0),
2121 j!(0),
2122 );
2123 assert_pass(&vector_s0, RuleKind::IdenticalParticleSymmetry);
2124
2125 let vector_s1 = rules.evaluate(
2126 &ParticleProperties::unknown(),
2127 (&vector_a, &vector_b),
2128 l!(0),
2129 j!(1),
2130 );
2131 assert_fail(&vector_s1, RuleKind::IdenticalParticleSymmetry);
2132
2133 let fermion_s0 = rules.evaluate(
2134 &ParticleProperties::unknown(),
2135 (&fermion_a, &fermion_b),
2136 l!(0),
2137 j!(0),
2138 );
2139 assert_pass(&fermion_s0, RuleKind::IdenticalParticleSymmetry);
2140
2141 let fermion_s1 = rules.evaluate(
2142 &ParticleProperties::unknown(),
2143 (&fermion_a, &fermion_b),
2144 l!(0),
2145 j!(1),
2146 );
2147 assert_fail(&fermion_s1, RuleKind::IdenticalParticleSymmetry);
2148
2149 let different_species = rules.evaluate(
2150 &ParticleProperties::unknown(),
2151 (&identical_boson(j!(0), "a"), &identical_boson(j!(0), "b")),
2152 l!(1),
2153 j!(0),
2154 );
2155 assert_pass(&different_species, RuleKind::IdenticalParticleSymmetry);
2156 }
2157
2158 #[test]
2159 fn c_parity_rule_distinguishes_inferred_c_from_non_inferable_final_states() {
2160 let parent = rho_like();
2161 let pi_plus = pion_like("pi+", "pi-", 1, 1);
2162 let pi_minus = pion_like("pi-", "pi+", -1, -1);
2163 let pi_zero = pion_like("pi0", "pi0", 0, 0);
2164
2165 let rules = RuleSet::angular().enforce(RuleKind::CParity);
2166
2167 let p_wave_report = rules.evaluate(&parent, (&pi_plus, &pi_minus), l!(1), j!(0));
2168 assert_pass(&p_wave_report, RuleKind::CParity);
2169
2170 let s_wave_report = rules.evaluate(&parent, (&pi_plus, &pi_minus), l!(0), j!(0));
2171 assert_fail(&s_wave_report, RuleKind::CParity);
2172
2173 let unknown_report = rules.evaluate(&parent, (&pi_plus, &pi_zero), l!(1), j!(0));
2174 assert!(unknown_report.is_allowed());
2175 assert_unknown_allowed(
2176 &unknown_report,
2177 RuleKind::CParity,
2178 &[
2179 "daughter_a.species",
2180 "daughter_a.antiparticle_species",
2181 "daughter_b.species",
2182 "daughter_b.antiparticle_species",
2183 ],
2184 );
2185 }
2186
2187 #[test]
2188 fn conventional_meson_jpc_can_be_enforced_or_used_as_non_rejecting_diagnostic() {
2189 let conventional = rho_like();
2190 let exotic = exotic_one_minus_plus();
2191
2192 let diagnostic_rules = RuleSet::angular().diagnose_only(
2193 RuleKind::ConventionalMesonJpc,
2194 "flag exotic JPC without rejecting hybrid candidates",
2195 );
2196
2197 let conventional_report =
2198 diagnostic_rules.evaluate(&conventional, (&conventional, &conventional), l!(0), j!(0));
2199 assert!(conventional_report.is_allowed());
2200 assert_diagnostic(
2201 &conventional_report,
2202 RuleKind::ConventionalMesonJpc,
2203 Some(true),
2204 Some("flag exotic JPC without rejecting hybrid candidates"),
2205 );
2206
2207 let exotic_report = diagnostic_rules.evaluate(&exotic, (&exotic, &exotic), l!(0), j!(0));
2208 assert!(exotic_report.is_allowed());
2209 assert_diagnostic(
2210 &exotic_report,
2211 RuleKind::ConventionalMesonJpc,
2212 Some(false),
2213 Some("flag exotic JPC without rejecting hybrid candidates"),
2214 );
2215
2216 let enforced_report = RuleSet::angular()
2217 .enforce(RuleKind::ConventionalMesonJpc)
2218 .evaluate(&exotic, (&exotic, &exotic), l!(0), j!(0));
2219 assert!(!enforced_report.is_allowed());
2220 assert_fail(&enforced_report, RuleKind::ConventionalMesonJpc);
2221
2222 let unknown_report = RuleSet::angular()
2223 .diagnose_only_without_reason(RuleKind::ConventionalMesonJpc)
2224 .evaluate(
2225 &ParticleProperties::unknown(),
2226 (
2227 &ParticleProperties::unknown(),
2228 &ParticleProperties::unknown(),
2229 ),
2230 l!(0),
2231 j!(0),
2232 );
2233 assert_diagnostic(&unknown_report, RuleKind::ConventionalMesonJpc, None, None);
2234 }
2235
2236 #[test]
2237 fn selection_rules_scan_partial_waves_keeps_rejected_candidates_for_diagnostics() {
2238 let parent = ParticleProperties::jp(j!(1), Parity::Positive);
2239 let a = ParticleProperties::jp(j!(1 / 2), Parity::Positive);
2240 let b = ParticleProperties::jp(j!(1 / 2), Parity::Negative);
2241
2242 let angular_scan = SelectionRules::angular(l!(2)).scan_partial_waves(&parent, (&a, &b));
2243
2244 assert!(angular_scan.missing_inputs.is_empty());
2245 assert_eq!(
2246 candidate_labels(angular_scan.candidates.iter()),
2247 vec!["1P1", "3S1", "3P1", "3D1"]
2248 );
2249 assert_eq!(
2250 allowed_labels(angular_scan.allowed()),
2251 vec!["1P1", "3S1", "3P1", "3D1"]
2252 );
2253 assert_eq!(angular_scan.rejected().count(), 0);
2254
2255 let parity_rules = SelectionRules::new(RuleSet::angular().enforce(RuleKind::Parity), l!(2));
2256 let parity_scan = parity_rules.scan_partial_waves(&parent, (&a, &b));
2257
2258 assert_eq!(
2259 candidate_labels(parity_scan.candidates.iter()),
2260 vec!["1P1", "3S1", "3P1", "3D1"]
2261 );
2262 assert_eq!(allowed_labels(parity_scan.allowed()), vec!["1P1", "3P1"]);
2263 assert_eq!(candidate_labels(parity_scan.rejected()), vec!["3S1", "3D1"]);
2264
2265 let allowed = parity_rules.allowed_partial_waves(&parent, (&a, &b));
2266 assert_eq!(labels(&allowed), vec!["1P1", "3P1"]);
2267 }
2268
2269 #[test]
2270 fn selection_rules_report_missing_spin_inputs_and_default_to_strong_l6() {
2271 assert_eq!(SelectionRules::default(), SelectionRules::strong(l!(6)));
2272 assert_eq!(
2273 SelectionRules::electromagnetic(l!(2)),
2274 SelectionRules::new(RuleSet::electromagnetic(), l!(2))
2275 );
2276 assert_eq!(
2277 SelectionRules::weak(l!(3)),
2278 SelectionRules::new(RuleSet::weak(), l!(3))
2279 );
2280
2281 let parent_missing = ParticleProperties::unknown();
2282 let a = ParticleProperties::jp(j!(0), Parity::Negative);
2283 let b = ParticleProperties::jp(j!(0), Parity::Negative);
2284
2285 let scan = SelectionRules::default().scan_partial_waves(&parent_missing, (&a, &b));
2286
2287 assert!(scan.candidates.is_empty());
2288 assert_eq!(scan.missing_inputs, vec!["parent.spin"]);
2289
2290 let parent = ParticleProperties::jp(j!(0), Parity::Positive);
2291 let a_missing = ParticleProperties::unknown();
2292
2293 let scan = SelectionRules::default().scan_partial_waves(&parent, (&a_missing, &b));
2294
2295 assert!(scan.candidates.is_empty());
2296 assert_eq!(scan.missing_inputs, vec!["daughter_a.spin"]);
2297
2298 let b_missing = ParticleProperties::unknown();
2299
2300 let scan = SelectionRules::default().scan_partial_waves(&parent, (&a, &b_missing));
2301
2302 assert!(scan.candidates.is_empty());
2303 assert_eq!(scan.missing_inputs, vec!["daughter_b.spin"]);
2304 }
2305
2306 #[test]
2307 fn strong_rules_find_delta_like_to_nucleon_pion_p_wave() {
2308 let parent = add_additives(
2309 ParticleProperties::jp(j!(3 / 2), Parity::Positive),
2310 1,
2311 0,
2312 0,
2313 0,
2314 0,
2315 1,
2316 0,
2317 0,
2318 0,
2319 );
2320
2321 let nucleon = add_additives(
2322 ParticleProperties::jp(j!(1 / 2), Parity::Positive),
2323 1,
2324 0,
2325 0,
2326 0,
2327 0,
2328 1,
2329 0,
2330 0,
2331 0,
2332 );
2333
2334 let pion = add_additives(
2335 ParticleProperties::jp(j!(0), Parity::Negative),
2336 0,
2337 0,
2338 0,
2339 0,
2340 0,
2341 0,
2342 0,
2343 0,
2344 0,
2345 );
2346
2347 let rules = SelectionRules::new(
2348 RuleSet::angular()
2349 .enforce(RuleKind::Parity)
2350 .enforce(RuleKind::Charge)
2351 .enforce(RuleKind::BaryonNumber),
2352 l!(4),
2353 );
2354
2355 let waves = rules.allowed_partial_waves(&parent, (&nucleon, &pion));
2356
2357 assert_eq!(labels(&waves), vec!["2P3/2"]);
2358 assert_eq!(waves[0].parity, Some(Parity::Positive));
2359 assert_eq!(waves[0].c_parity, None);
2360 }
2361}