1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::quantum::{L, ParticleProperties, S};
6
7mod evaluators;
8
9#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
10pub enum RuleKind {
13 Parity,
18
19 Isospin,
25 IsospinProjection,
29 CParity,
35 GParity,
41 Charge,
45 Strangeness,
52 Charm,
57 Bottomness,
62 Topness,
66 BaryonNumber,
70 ElectronLeptonNumber,
74 MuonLeptonNumber,
78 TauLeptonNumber,
82 LeptonNumber,
90 IdenticalParticleSymmetry,
96
97 ConventionalMesonJpc,
99}
100
101#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
102pub enum RuleMode {
104 Enforce,
106 Ignore {
108 reason: Option<String>,
110 },
111 DiagnoseOnly {
113 reason: Option<String>,
115 },
116}
117
118#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
119pub enum UnknownPolicy {
121 Allow,
123 Reject,
125 Warn,
127}
128
129#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
130pub struct RulePolicy {
132 pub mode: RuleMode,
134 pub unknown: UnknownPolicy,
136}
137
138impl RulePolicy {
139 pub fn enforce() -> Self {
141 Self {
142 mode: RuleMode::Enforce,
143 unknown: UnknownPolicy::Allow,
144 }
145 }
146
147 pub fn enforce_strict() -> Self {
149 Self {
150 mode: RuleMode::Enforce,
151 unknown: UnknownPolicy::Reject,
152 }
153 }
154
155 pub fn enforce_with_unknown_policy(unknown: UnknownPolicy) -> Self {
157 Self {
158 mode: RuleMode::Enforce,
159 unknown,
160 }
161 }
162
163 pub fn ignore(reason: impl Into<String>) -> Self {
165 Self {
166 mode: RuleMode::Ignore {
167 reason: Some(reason.into()),
168 },
169 unknown: UnknownPolicy::Allow,
170 }
171 }
172
173 pub fn ignore_without_reason() -> Self {
175 Self {
176 mode: RuleMode::Ignore { reason: None },
177 unknown: UnknownPolicy::Allow,
178 }
179 }
180
181 pub fn diagnose_only(reason: impl Into<String>) -> Self {
183 Self {
184 mode: RuleMode::DiagnoseOnly {
185 reason: Some(reason.into()),
186 },
187 unknown: UnknownPolicy::Warn,
188 }
189 }
190
191 pub fn diagnose_only_without_reason() -> Self {
193 Self {
194 mode: RuleMode::DiagnoseOnly { reason: None },
195 unknown: UnknownPolicy::Warn,
196 }
197 }
198}
199
200#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
201pub enum RuleOutcome {
203 Pass {
205 message: String,
207 },
208 Fail {
210 message: String,
212 },
213 UnknownAllowed {
215 missing: Vec<String>,
217 message: String,
219 },
220 Warning {
222 missing: Vec<String>,
224 message: String,
226 },
227 Ignored {
229 reason: Option<String>,
231 },
232 Diagnostic {
234 passed: Option<bool>,
236 reason: Option<String>,
238 message: String,
240 },
241}
242
243#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
244pub struct RuleCheck {
246 pub rule: RuleKind,
248 pub outcome: RuleOutcome,
250}
251
252impl RuleCheck {
253 pub fn is_failure(&self) -> bool {
255 matches!(self.outcome, RuleOutcome::Fail { .. })
256 }
257
258 pub fn is_warning(&self) -> bool {
260 matches!(self.outcome, RuleOutcome::Warning { .. })
261 }
262
263 pub fn is_unknown_allowed(&self) -> bool {
265 matches!(self.outcome, RuleOutcome::UnknownAllowed { .. })
266 }
267
268 pub fn is_ignored(&self) -> bool {
270 matches!(self.outcome, RuleOutcome::Ignored { .. })
271 }
272}
273
274#[derive(Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize)]
275pub struct RuleReport {
277 pub checks: Vec<RuleCheck>,
279}
280
281impl RuleReport {
282 pub fn is_allowed(&self) -> bool {
284 self.checks.iter().all(|check| !check.is_failure())
285 }
286
287 pub fn failures(&self) -> impl Iterator<Item = &RuleCheck> {
289 self.checks.iter().filter(|check| check.is_failure())
290 }
291
292 pub fn warnings(&self) -> impl Iterator<Item = &RuleCheck> {
294 self.checks.iter().filter(|check| check.is_warning())
295 }
296
297 pub fn unknowns(&self) -> impl Iterator<Item = &RuleCheck> {
299 self.checks
300 .iter()
301 .filter(|check| check.is_unknown_allowed())
302 }
303
304 pub fn ignored(&self) -> impl Iterator<Item = &RuleCheck> {
306 self.checks.iter().filter(|check| check.is_ignored())
307 }
308
309 pub fn outcome(&self, rule: RuleKind) -> Option<&RuleOutcome> {
311 self.checks
312 .iter()
313 .find(|check| check.rule == rule)
314 .map(|check| &check.outcome)
315 }
316
317 pub fn has_failures(&self) -> bool {
319 self.failures().next().is_some()
320 }
321
322 pub fn has_unknowns(&self) -> bool {
324 self.unknowns().next().is_some()
325 }
326
327 pub fn has_ignored(&self) -> bool {
329 self.ignored().next().is_some()
330 }
331
332 pub fn len(&self) -> usize {
334 self.checks.len()
335 }
336
337 pub fn is_empty(&self) -> bool {
339 self.checks.is_empty()
340 }
341}
342
343#[derive(Clone, Debug, Eq, Hash, PartialEq, Default, Serialize, Deserialize)]
356pub struct RuleSet {
357 policies: BTreeMap<RuleKind, RulePolicy>,
358}
359impl RuleSet {
360 pub fn angular() -> Self {
368 Self::default()
369 }
370
371 pub fn strong() -> Self {
381 Self::angular()
382 .enforce(RuleKind::Parity)
383 .enforce(RuleKind::Isospin)
384 .enforce(RuleKind::IsospinProjection)
385 .enforce(RuleKind::Charge)
386 .enforce(RuleKind::Strangeness)
387 .enforce(RuleKind::Charm)
388 .enforce(RuleKind::Bottomness)
389 .enforce(RuleKind::Topness)
390 .enforce(RuleKind::BaryonNumber)
391 .enforce(RuleKind::IdenticalParticleSymmetry)
392 }
393
394 pub fn electromagnetic() -> Self {
403 Self::angular()
404 .enforce(RuleKind::Parity)
405 .enforce(RuleKind::IsospinProjection)
406 .enforce(RuleKind::Charge)
407 .enforce(RuleKind::Strangeness)
408 .enforce(RuleKind::Charm)
409 .enforce(RuleKind::Bottomness)
410 .enforce(RuleKind::Topness)
411 .enforce(RuleKind::BaryonNumber)
412 .enforce(RuleKind::IdenticalParticleSymmetry)
413 }
414
415 pub fn weak() -> Self {
423 Self::angular()
424 .enforce(RuleKind::Charge)
425 .enforce(RuleKind::BaryonNumber)
426 .enforce(RuleKind::ElectronLeptonNumber)
427 .enforce(RuleKind::MuonLeptonNumber)
428 .enforce(RuleKind::TauLeptonNumber)
429 .enforce(RuleKind::LeptonNumber)
430 .enforce(RuleKind::IdenticalParticleSymmetry)
431 }
432
433 pub fn enforce_mut(&mut self, rule: RuleKind) -> &mut Self {
435 self.set_policy_mut(rule, RulePolicy::enforce())
436 }
437
438 pub fn enforce_strict_mut(&mut self, rule: RuleKind) -> &mut Self {
440 self.set_policy_mut(rule, RulePolicy::enforce_strict())
441 }
442
443 pub fn set_policy_mut(&mut self, rule: RuleKind, policy: RulePolicy) -> &mut Self {
445 self.policies.insert(rule, policy);
446 self
447 }
448
449 pub fn ignore_mut(&mut self, rule: RuleKind, reason: impl Into<String>) -> &mut Self {
451 self.set_policy_mut(rule, RulePolicy::ignore(reason))
452 }
453
454 pub fn ignore_without_reason_mut(&mut self, rule: RuleKind) -> &mut Self {
456 self.set_policy_mut(rule, RulePolicy::ignore_without_reason())
457 }
458
459 pub fn diagnose_only_mut(&mut self, rule: RuleKind, reason: impl Into<String>) -> &mut Self {
461 self.set_policy_mut(rule, RulePolicy::diagnose_only(reason))
462 }
463
464 pub fn diagnose_only_without_reason_mut(&mut self, rule: RuleKind) -> &mut Self {
466 self.set_policy_mut(rule, RulePolicy::diagnose_only_without_reason())
467 }
468
469 pub fn disable_mut(&mut self, rule: RuleKind) -> &mut Self {
471 self.policies.remove(&rule);
472 self
473 }
474
475 pub fn with_unknown_policy_mut(&mut self, rule: RuleKind, unknown: UnknownPolicy) -> &mut Self {
480 self.policies
481 .entry(rule)
482 .or_insert_with(RulePolicy::enforce)
483 .unknown = unknown;
484 self
485 }
486
487 pub fn enforce(mut self, rule: RuleKind) -> Self {
489 self.enforce_mut(rule);
490 self
491 }
492
493 pub fn enforce_strict(mut self, rule: RuleKind) -> Self {
495 self.enforce_strict_mut(rule);
496 self
497 }
498
499 pub fn set_policy(mut self, rule: RuleKind, policy: RulePolicy) -> Self {
501 self.set_policy_mut(rule, policy);
502 self
503 }
504
505 pub fn ignore(mut self, rule: RuleKind, reason: impl Into<String>) -> Self {
507 self.ignore_mut(rule, reason);
508 self
509 }
510
511 pub fn ignore_without_reason(mut self, rule: RuleKind) -> Self {
513 self.ignore_without_reason_mut(rule);
514 self
515 }
516
517 pub fn diagnose_only(mut self, rule: RuleKind, reason: impl Into<String>) -> Self {
519 self.diagnose_only_mut(rule, reason);
520 self
521 }
522
523 pub fn diagnose_only_without_reason(mut self, rule: RuleKind) -> Self {
525 self.diagnose_only_without_reason_mut(rule);
526 self
527 }
528
529 pub fn disable(mut self, rule: RuleKind) -> Self {
531 self.disable_mut(rule);
532 self
533 }
534
535 pub fn with_unknown_policy(mut self, rule: RuleKind, unknown: UnknownPolicy) -> Self {
537 self.with_unknown_policy_mut(rule, unknown);
538 self
539 }
540
541 pub fn policy(&self, rule: RuleKind) -> Option<&RulePolicy> {
543 self.policies.get(&rule)
544 }
545
546 pub fn enabled_rules(&self) -> impl Iterator<Item = RuleKind> + '_ {
548 self.policies.keys().copied()
549 }
550
551 pub fn check(
553 &self,
554 parent: &ParticleProperties,
555 daughters: (&ParticleProperties, &ParticleProperties),
556 l: L,
557 s: S,
558 ) -> bool {
559 self.evaluate(parent, daughters, l, s).is_allowed()
560 }
561
562 pub fn evaluate(
564 &self,
565 parent: &ParticleProperties,
566 daughters: (&ParticleProperties, &ParticleProperties),
567 l: L,
568 s: S,
569 ) -> RuleReport {
570 let input = evaluators::RuleInput {
571 parent,
572 daughters,
573 l,
574 s,
575 };
576 let checks = self
577 .policies
578 .iter()
579 .map(|(&rule, policy)| apply_policy(rule, policy, evaluators::evaluate(rule, input)))
580 .collect();
581 RuleReport { checks }
582 }
583}
584
585#[derive(Clone, Debug, Eq, PartialEq)]
586enum RawRuleOutcome {
587 Pass {
588 message: String,
589 },
590 Fail {
591 message: String,
592 },
593 Unknown {
594 missing: Vec<String>,
595 message: String,
596 },
597}
598
599impl RawRuleOutcome {
600 fn pass(message: impl Into<String>) -> Self {
601 Self::Pass {
602 message: message.into(),
603 }
604 }
605
606 fn fail(message: impl Into<String>) -> Self {
607 Self::Fail {
608 message: message.into(),
609 }
610 }
611
612 fn unknown(missing: impl Into<Vec<String>>, message: impl Into<String>) -> Self {
613 Self::Unknown {
614 missing: missing.into(),
615 message: message.into(),
616 }
617 }
618
619 fn message(&self) -> String {
620 match self {
621 Self::Pass { message } => message.clone(),
622 Self::Fail { message } => message.clone(),
623 Self::Unknown { message, .. } => message.clone(),
624 }
625 }
626
627 fn passed(&self) -> Option<bool> {
628 match self {
629 Self::Pass { .. } => Some(true),
630 Self::Fail { .. } => Some(false),
631 Self::Unknown { .. } => None,
632 }
633 }
634}
635
636fn missing(fields: &[&'static str]) -> Vec<String> {
637 fields.iter().map(|field| (*field).to_string()).collect()
638}
639
640fn apply_policy(rule: RuleKind, policy: &RulePolicy, raw: RawRuleOutcome) -> RuleCheck {
641 let outcome = match &policy.mode {
642 RuleMode::Ignore { reason } => RuleOutcome::Ignored {
643 reason: reason.clone(),
644 },
645
646 RuleMode::DiagnoseOnly { reason } => RuleOutcome::Diagnostic {
647 passed: raw.passed(),
648 reason: reason.clone(),
649 message: raw.message(),
650 },
651
652 RuleMode::Enforce => match raw {
653 RawRuleOutcome::Pass { message } => RuleOutcome::Pass { message },
654
655 RawRuleOutcome::Fail { message } => RuleOutcome::Fail { message },
656
657 RawRuleOutcome::Unknown { missing, message } => match policy.unknown {
658 UnknownPolicy::Allow => RuleOutcome::UnknownAllowed { missing, message },
659 UnknownPolicy::Warn => RuleOutcome::Warning { missing, message },
660 UnknownPolicy::Reject => RuleOutcome::Fail {
661 message: format!("{message}; unknown inputs are rejected by policy"),
662 },
663 },
664 },
665 };
666
667 RuleCheck { rule, outcome }
668}
669
670#[cfg(test)]
671mod tests {
672 use std::collections::BTreeSet;
673
674 use super::*;
675 use crate::{
676 j, l, m,
677 quantum::{
678 AllowedPartialWave, Isospin, J, M, Parity, PartialWave, PartialWaveCandidate,
679 SelectionRules, Statistics,
680 },
681 };
682
683 fn labels(waves: &[AllowedPartialWave]) -> Vec<String> {
684 waves.iter().map(|w| w.wave.label()).collect()
685 }
686
687 fn allowed_labels<'a>(waves: impl Iterator<Item = &'a AllowedPartialWave>) -> Vec<String> {
688 waves.map(|w| w.wave.label()).collect()
689 }
690
691 fn candidate_labels<'a>(
692 candidates: impl Iterator<Item = &'a PartialWaveCandidate>,
693 ) -> Vec<String> {
694 candidates.map(|candidate| candidate.wave.label()).collect()
695 }
696
697 fn outcome(report: &RuleReport, rule: RuleKind) -> &RuleOutcome {
698 report
699 .outcome(rule)
700 .unwrap_or_else(|| panic!("missing outcome for {rule:?}; report was {report:#?}"))
701 }
702
703 fn assert_pass(report: &RuleReport, rule: RuleKind) {
704 assert!(
705 matches!(outcome(report, rule), RuleOutcome::Pass { .. }),
706 "expected {rule:?} to pass; got {:#?}",
707 outcome(report, rule)
708 );
709 }
710
711 fn assert_fail(report: &RuleReport, rule: RuleKind) {
712 assert!(
713 matches!(outcome(report, rule), RuleOutcome::Fail { .. }),
714 "expected {rule:?} to fail; got {:#?}",
715 outcome(report, rule)
716 );
717 }
718
719 fn assert_unknown_allowed(report: &RuleReport, rule: RuleKind, expected_missing: &[&str]) {
720 match outcome(report, rule) {
721 RuleOutcome::UnknownAllowed { missing, .. } => {
722 for field in expected_missing {
723 assert!(
724 missing.iter().any(|missing| missing == field),
725 "expected missing field {field:?}; got {missing:?}"
726 );
727 }
728 }
729 other => panic!("expected {rule:?} to be UnknownAllowed; got {other:#?}"),
730 }
731 }
732
733 fn assert_warning(report: &RuleReport, rule: RuleKind, expected_missing: &[&str]) {
734 match outcome(report, rule) {
735 RuleOutcome::Warning { missing, .. } => {
736 for field in expected_missing {
737 assert!(
738 missing.iter().any(|missing| missing == field),
739 "expected missing field {field:?}; got {missing:?}"
740 );
741 }
742 }
743 other => panic!("expected {rule:?} to be Warning; got {other:#?}"),
744 }
745 }
746
747 fn assert_ignored(report: &RuleReport, rule: RuleKind, expected_reason: Option<&str>) {
748 match outcome(report, rule) {
749 RuleOutcome::Ignored { reason } => {
750 assert_eq!(reason.as_deref(), expected_reason);
751 }
752 other => panic!("expected {rule:?} to be Ignored; got {other:#?}"),
753 }
754 }
755
756 fn assert_diagnostic(
757 report: &RuleReport,
758 rule: RuleKind,
759 expected_passed: Option<bool>,
760 expected_reason: Option<&str>,
761 ) {
762 match outcome(report, rule) {
763 RuleOutcome::Diagnostic { passed, reason, .. } => {
764 assert_eq!(*passed, expected_passed);
765 assert_eq!(reason.as_deref(), expected_reason);
766 }
767 other => panic!("expected {rule:?} to be Diagnostic; got {other:#?}"),
768 }
769 }
770
771 #[allow(clippy::too_many_arguments)]
772 fn add_additives(
773 particle: ParticleProperties,
774 charge: i32,
775 strangeness: i32,
776 charm: i32,
777 bottomness: i32,
778 topness: i32,
779 baryon_number: i32,
780 electron_lepton_number: i32,
781 muon_lepton_number: i32,
782 tau_lepton_number: i32,
783 ) -> ParticleProperties {
784 particle
785 .with_charge(charge)
786 .with_strangeness(strangeness)
787 .unwrap()
788 .with_charm(charm)
789 .unwrap()
790 .with_bottomness(bottomness)
791 .unwrap()
792 .with_topness(topness)
793 .unwrap()
794 .with_baryon_number(baryon_number)
795 .unwrap()
796 .with_electron_lepton_number(electron_lepton_number)
797 .unwrap()
798 .with_muon_lepton_number(muon_lepton_number)
799 .unwrap()
800 .with_tau_lepton_number(tau_lepton_number)
801 .unwrap()
802 }
803
804 fn pion_like(name: &str, anti_name: &str, charge: i32, i3: i32) -> ParticleProperties {
805 ParticleProperties::meson()
806 .with_zero_flavor()
807 .with_name(name)
808 .with_species_names(name, anti_name)
809 .unwrap()
810 .with_spin(j!(0))
811 .with_parity(Parity::Negative)
812 .with_charge(charge)
813 .with_isospin(Isospin::new(j!(1), Some(M::int(i3))).unwrap())
814 .with_g_parity(Parity::Negative)
815 .with_statistics(Statistics::Boson)
816 .unwrap()
817 }
818
819 fn rho_like() -> ParticleProperties {
820 ParticleProperties::meson()
821 .with_zero_flavor()
822 .with_name("rho0")
823 .with_self_conjugate_species("rho0")
824 .unwrap()
825 .with_spin(j!(1))
826 .with_parity(Parity::Negative)
827 .with_c_parity(Parity::Negative)
828 .unwrap()
829 .with_charge(0)
830 .with_isospin(Isospin::new(j!(1), Some(m!(0))).unwrap())
831 .with_g_parity(Parity::Positive)
832 .with_statistics(Statistics::Boson)
833 .unwrap()
834 }
835
836 fn exotic_one_minus_plus() -> ParticleProperties {
837 ParticleProperties::meson()
838 .with_zero_flavor()
839 .with_name("pi1_exotic")
840 .with_self_conjugate_species("pi1_exotic")
841 .unwrap()
842 .with_spin(j!(1))
843 .with_parity(Parity::Negative)
844 .with_c_parity(Parity::Positive)
845 .unwrap()
846 .with_charge(0)
847 .with_isospin(Isospin::new(j!(1), Some(m!(0))).unwrap())
848 .with_statistics(Statistics::Boson)
849 .unwrap()
850 }
851
852 fn identical_boson(spin: J, species: &str) -> ParticleProperties {
853 ParticleProperties::unknown()
854 .with_spin(spin)
855 .with_species(species)
856 .unwrap()
857 .with_statistics(Statistics::Boson)
858 .unwrap()
859 }
860
861 fn identical_fermion(spin: J, species: &str) -> ParticleProperties {
862 ParticleProperties::unknown()
863 .with_spin(spin)
864 .with_species(species)
865 .unwrap()
866 .with_statistics(Statistics::Fermion)
867 .unwrap()
868 }
869
870 const ALL_RULES: [RuleKind; 17] = [
871 RuleKind::Parity,
872 RuleKind::Isospin,
873 RuleKind::IsospinProjection,
874 RuleKind::CParity,
875 RuleKind::GParity,
876 RuleKind::Charge,
877 RuleKind::Strangeness,
878 RuleKind::Charm,
879 RuleKind::Bottomness,
880 RuleKind::Topness,
881 RuleKind::BaryonNumber,
882 RuleKind::ElectronLeptonNumber,
883 RuleKind::MuonLeptonNumber,
884 RuleKind::TauLeptonNumber,
885 RuleKind::LeptonNumber,
886 RuleKind::IdenticalParticleSymmetry,
887 RuleKind::ConventionalMesonJpc,
888 ];
889
890 #[test]
891 fn rule_registry_is_complete_unique_and_named() {
892 let registered: Vec<_> = evaluators::registered_rules().collect();
893 assert_eq!(
894 registered.iter().map(|(rule, _)| *rule).collect::<Vec<_>>(),
895 ALL_RULES,
896 );
897 assert!(registered.iter().all(|(_, name)| !name.is_empty()));
898 assert_eq!(
899 registered
900 .iter()
901 .map(|(rule, _)| *rule)
902 .collect::<BTreeSet<_>>()
903 .len(),
904 ALL_RULES.len(),
905 );
906 }
907
908 #[test]
909 fn every_rule_outcome_obeys_every_policy_state() {
910 let raw_outcomes = [
911 RawRuleOutcome::pass("passed"),
912 RawRuleOutcome::fail("failed"),
913 RawRuleOutcome::unknown(vec!["missing.field".to_string()], "unknown"),
914 ];
915 let modes = [
916 RuleMode::Enforce,
917 RuleMode::Ignore { reason: None },
918 RuleMode::DiagnoseOnly { reason: None },
919 ];
920 let unknown_policies = [
921 UnknownPolicy::Allow,
922 UnknownPolicy::Warn,
923 UnknownPolicy::Reject,
924 ];
925
926 for rule in ALL_RULES {
927 for mode in &modes {
928 for unknown in unknown_policies {
929 for raw in &raw_outcomes {
930 let check = apply_policy(
931 rule,
932 &RulePolicy {
933 mode: mode.clone(),
934 unknown,
935 },
936 raw.clone(),
937 );
938 assert_eq!(check.rule, rule);
939 match (mode, raw, unknown, check.outcome) {
940 (RuleMode::Ignore { .. }, _, _, RuleOutcome::Ignored { .. })
941 | (
942 RuleMode::DiagnoseOnly { .. },
943 _,
944 _,
945 RuleOutcome::Diagnostic { .. },
946 )
947 | (
948 RuleMode::Enforce,
949 RawRuleOutcome::Pass { .. },
950 _,
951 RuleOutcome::Pass { .. },
952 )
953 | (
954 RuleMode::Enforce,
955 RawRuleOutcome::Fail { .. },
956 _,
957 RuleOutcome::Fail { .. },
958 )
959 | (
960 RuleMode::Enforce,
961 RawRuleOutcome::Unknown { .. },
962 UnknownPolicy::Allow,
963 RuleOutcome::UnknownAllowed { .. },
964 )
965 | (
966 RuleMode::Enforce,
967 RawRuleOutcome::Unknown { .. },
968 UnknownPolicy::Warn,
969 RuleOutcome::Warning { .. },
970 )
971 | (
972 RuleMode::Enforce,
973 RawRuleOutcome::Unknown { .. },
974 UnknownPolicy::Reject,
975 RuleOutcome::Fail { .. },
976 ) => {}
977 combination => panic!("unexpected policy result: {combination:?}"),
978 }
979 }
980 }
981 }
982 }
983 }
984
985 #[test]
986 fn rule_presets_preserve_exact_membership_and_order() {
987 let assert_rules = |actual: RuleSet, expected: &[RuleKind]| {
988 assert_eq!(actual.enabled_rules().collect::<Vec<_>>(), expected);
989 };
990 assert_rules(
991 RuleSet::strong(),
992 &[
993 RuleKind::Parity,
994 RuleKind::Isospin,
995 RuleKind::IsospinProjection,
996 RuleKind::Charge,
997 RuleKind::Strangeness,
998 RuleKind::Charm,
999 RuleKind::Bottomness,
1000 RuleKind::Topness,
1001 RuleKind::BaryonNumber,
1002 RuleKind::IdenticalParticleSymmetry,
1003 ],
1004 );
1005 assert_rules(
1006 RuleSet::electromagnetic(),
1007 &[
1008 RuleKind::Parity,
1009 RuleKind::IsospinProjection,
1010 RuleKind::Charge,
1011 RuleKind::Strangeness,
1012 RuleKind::Charm,
1013 RuleKind::Bottomness,
1014 RuleKind::Topness,
1015 RuleKind::BaryonNumber,
1016 RuleKind::IdenticalParticleSymmetry,
1017 ],
1018 );
1019 assert_rules(
1020 RuleSet::weak(),
1021 &[
1022 RuleKind::Charge,
1023 RuleKind::BaryonNumber,
1024 RuleKind::ElectronLeptonNumber,
1025 RuleKind::MuonLeptonNumber,
1026 RuleKind::TauLeptonNumber,
1027 RuleKind::LeptonNumber,
1028 RuleKind::IdenticalParticleSymmetry,
1029 ],
1030 );
1031 }
1032
1033 #[test]
1034 fn representative_rule_policy_and_report_serde_stay_stable() {
1035 let policy = RulePolicy {
1036 mode: RuleMode::DiagnoseOnly {
1037 reason: Some("classification only".to_string()),
1038 },
1039 unknown: UnknownPolicy::Warn,
1040 };
1041 let policy_json = serde_json::to_string(&policy).unwrap();
1042 assert_eq!(
1043 policy_json,
1044 r#"{"mode":{"DiagnoseOnly":{"reason":"classification only"}},"unknown":"Warn"}"#,
1045 );
1046 assert_eq!(
1047 serde_json::from_str::<RulePolicy>(&policy_json).unwrap(),
1048 policy
1049 );
1050
1051 let report = RuleReport {
1052 checks: vec![RuleCheck {
1053 rule: RuleKind::Charge,
1054 outcome: RuleOutcome::Warning {
1055 missing: vec!["daughter_b.charge".to_string()],
1056 message: "charge cannot be checked".to_string(),
1057 },
1058 }],
1059 };
1060 let report_json = serde_json::to_string(&report).unwrap();
1061 assert_eq!(
1062 report_json,
1063 r#"{"checks":[{"rule":"Charge","outcome":{"Warning":{"missing":["daughter_b.charge"],"message":"charge cannot be checked"}}}]}"#,
1064 );
1065 assert_eq!(
1066 serde_json::from_str::<RuleReport>(&report_json).unwrap(),
1067 report
1068 );
1069 }
1070
1071 #[test]
1072 fn rule_set_constructors_build_expected_default_policies() {
1073 let angular = RuleSet::angular();
1074 assert_eq!(angular.enabled_rules().count(), 0);
1075
1076 let strong = RuleSet::strong();
1077 for rule in [
1078 RuleKind::Parity,
1079 RuleKind::Isospin,
1080 RuleKind::IsospinProjection,
1081 RuleKind::Charge,
1082 RuleKind::Strangeness,
1083 RuleKind::Charm,
1084 RuleKind::Bottomness,
1085 RuleKind::Topness,
1086 RuleKind::BaryonNumber,
1087 RuleKind::IdenticalParticleSymmetry,
1088 ] {
1089 assert!(
1090 matches!(strong.policy(rule).unwrap().mode, RuleMode::Enforce),
1091 "strong rules should enforce {rule:?}"
1092 );
1093 }
1094 assert!(strong.policy(RuleKind::CParity).is_none());
1095 assert!(strong.policy(RuleKind::GParity).is_none());
1096 assert!(strong.policy(RuleKind::ConventionalMesonJpc).is_none());
1097
1098 let electromagnetic = RuleSet::electromagnetic();
1099 assert!(electromagnetic.policy(RuleKind::Parity).is_some());
1100 assert!(electromagnetic.policy(RuleKind::Charge).is_some());
1101 assert!(
1102 electromagnetic
1103 .policy(RuleKind::IsospinProjection)
1104 .is_some()
1105 );
1106 assert!(electromagnetic.policy(RuleKind::Isospin).is_none());
1107
1108 let weak = RuleSet::weak();
1109 assert!(weak.policy(RuleKind::Charge).is_some());
1110 assert!(weak.policy(RuleKind::BaryonNumber).is_some());
1111 assert!(weak.policy(RuleKind::ElectronLeptonNumber).is_some());
1112 assert!(weak.policy(RuleKind::MuonLeptonNumber).is_some());
1113 assert!(weak.policy(RuleKind::TauLeptonNumber).is_some());
1114 assert!(weak.policy(RuleKind::LeptonNumber).is_some());
1115 assert!(weak.policy(RuleKind::Parity).is_none());
1116 assert!(weak.policy(RuleKind::Strangeness).is_none());
1117 }
1118
1119 #[test]
1120 fn rule_set_builder_and_mut_methods_configure_the_same_policies() {
1121 let built = RuleSet::angular()
1122 .enforce(RuleKind::Parity)
1123 .enforce_strict(RuleKind::Charge)
1124 .set_policy(
1125 RuleKind::Strangeness,
1126 RulePolicy::enforce_with_unknown_policy(UnknownPolicy::Warn),
1127 )
1128 .ignore(RuleKind::Isospin, "intentional isospin violation")
1129 .ignore_without_reason(RuleKind::GParity)
1130 .diagnose_only(
1131 RuleKind::ConventionalMesonJpc,
1132 "classify exotics without rejecting them",
1133 )
1134 .diagnose_only_without_reason(RuleKind::CParity)
1135 .with_unknown_policy(RuleKind::Bottomness, UnknownPolicy::Reject)
1136 .disable(RuleKind::CParity);
1137
1138 let mut mutated = RuleSet::angular();
1139 mutated
1140 .enforce_mut(RuleKind::Parity)
1141 .enforce_strict_mut(RuleKind::Charge)
1142 .set_policy_mut(
1143 RuleKind::Strangeness,
1144 RulePolicy::enforce_with_unknown_policy(UnknownPolicy::Warn),
1145 )
1146 .ignore_mut(RuleKind::Isospin, "intentional isospin violation")
1147 .ignore_without_reason_mut(RuleKind::GParity)
1148 .diagnose_only_mut(
1149 RuleKind::ConventionalMesonJpc,
1150 "classify exotics without rejecting them",
1151 )
1152 .diagnose_only_without_reason_mut(RuleKind::CParity)
1153 .with_unknown_policy_mut(RuleKind::Bottomness, UnknownPolicy::Reject)
1154 .disable_mut(RuleKind::CParity);
1155
1156 assert_eq!(built, mutated);
1157
1158 assert!(matches!(
1159 built.policy(RuleKind::Parity).unwrap().mode,
1160 RuleMode::Enforce
1161 ));
1162 assert_eq!(
1163 built.policy(RuleKind::Charge).unwrap().unknown,
1164 UnknownPolicy::Reject
1165 );
1166 assert_eq!(
1167 built.policy(RuleKind::Strangeness).unwrap().unknown,
1168 UnknownPolicy::Warn
1169 );
1170 assert!(matches!(
1171 built.policy(RuleKind::Isospin).unwrap().mode,
1172 RuleMode::Ignore { reason: Some(_) }
1173 ));
1174 assert!(matches!(
1175 built.policy(RuleKind::GParity).unwrap().mode,
1176 RuleMode::Ignore { reason: None }
1177 ));
1178 assert!(matches!(
1179 built.policy(RuleKind::ConventionalMesonJpc).unwrap().mode,
1180 RuleMode::DiagnoseOnly { reason: Some(_) }
1181 ));
1182 assert_eq!(
1183 built.policy(RuleKind::Bottomness).unwrap().unknown,
1184 UnknownPolicy::Reject
1185 );
1186 assert!(built.policy(RuleKind::CParity).is_none());
1187 }
1188
1189 #[test]
1190 fn policy_application_distinguishes_unknown_allowed_warning_reject_ignore_and_diagnostic() {
1191 let parent = ParticleProperties::unknown().with_spin(j!(0));
1192 let a = ParticleProperties::unknown().with_spin(j!(0));
1193 let b = ParticleProperties::unknown().with_spin(j!(0));
1194
1195 let rules = RuleSet::angular()
1196 .enforce(RuleKind::Parity)
1197 .set_policy(
1198 RuleKind::Charge,
1199 RulePolicy::enforce_with_unknown_policy(UnknownPolicy::Warn),
1200 )
1201 .enforce_strict(RuleKind::Strangeness)
1202 .ignore(RuleKind::Isospin, "not relevant for this model")
1203 .diagnose_only(
1204 RuleKind::ConventionalMesonJpc,
1205 "only classify the parent assignment",
1206 );
1207
1208 let report = rules.evaluate(&parent, (&a, &b), l!(0), j!(0));
1209
1210 assert!(!report.is_allowed());
1211 assert_eq!(report.len(), 5);
1212 assert!(report.has_failures());
1213 assert!(report.has_unknowns());
1214 assert!(report.has_ignored());
1215 assert_eq!(report.failures().count(), 1);
1216 assert_eq!(report.warnings().count(), 1);
1217 assert_eq!(report.unknowns().count(), 1);
1218 assert_eq!(report.ignored().count(), 1);
1219
1220 assert_unknown_allowed(&report, RuleKind::Parity, &["parent.parity"]);
1221 assert_warning(
1222 &report,
1223 RuleKind::Charge,
1224 &["parent.charge", "daughter_a.charge", "daughter_b.charge"],
1225 );
1226 assert_fail(&report, RuleKind::Strangeness);
1227 assert_ignored(
1228 &report,
1229 RuleKind::Isospin,
1230 Some("not relevant for this model"),
1231 );
1232 assert_diagnostic(
1233 &report,
1234 RuleKind::ConventionalMesonJpc,
1235 None,
1236 Some("only classify the parent assignment"),
1237 );
1238 }
1239
1240 #[test]
1241 fn angular_momentum_helpers_partial_wave_validation_and_inference_work_together() {
1242 assert_eq!(
1243 SelectionRules::coupled_spins(j!(1 / 2), j!(1 / 2)),
1244 vec![j!(0), j!(1)]
1245 );
1246 assert_eq!(
1247 SelectionRules::coupled_spins(j!(1 / 2), j!(1)),
1248 vec![j!(1 / 2), j!(3 / 2)]
1249 );
1250 assert_eq!(
1251 SelectionRules::coupled_spins(j!(1), j!(1)),
1252 vec![j!(0), j!(1), j!(2)]
1253 );
1254
1255 let wave = PartialWave::new(j!(1), l!(1), j!(0)).unwrap();
1256 assert_eq!(wave.label(), "1P1");
1257 assert_eq!(wave.to_string(), "1P1");
1258
1259 assert!(PartialWave::new(j!(1), l!(0), j!(0)).is_err());
1260
1261 let pi_plus = pion_like("pi+", "pi-", 1, 1);
1262 let pi_minus = pion_like("pi-", "pi+", -1, -1);
1263 let allowed = AllowedPartialWave::new(wave, (&pi_plus, &pi_minus));
1264
1265 assert_eq!(allowed.parity, Some(Parity::Negative));
1266 assert_eq!(allowed.c_parity, Some(Parity::Negative));
1267
1268 let non_c_pair = AllowedPartialWave::new(
1269 PartialWave::new(j!(0), l!(0), j!(0)).unwrap(),
1270 (&pi_plus, &pion_like("pi0", "pi0", 0, 0)),
1271 );
1272 assert_eq!(non_c_pair.parity, Some(Parity::Positive));
1273 assert_eq!(non_c_pair.c_parity, None);
1274 }
1275
1276 #[test]
1277 fn complete_strong_plus_c_and_g_rules_pass_for_rho_like_to_charged_pions() {
1278 let parent = rho_like();
1279 let pi_plus = pion_like("pi+", "pi-", 1, 1);
1280 let pi_minus = pion_like("pi-", "pi+", -1, -1);
1281
1282 let rules = RuleSet::strong()
1283 .enforce(RuleKind::CParity)
1284 .enforce(RuleKind::GParity)
1285 .enforce(RuleKind::ElectronLeptonNumber)
1286 .enforce(RuleKind::MuonLeptonNumber)
1287 .enforce(RuleKind::TauLeptonNumber)
1288 .enforce(RuleKind::LeptonNumber);
1289
1290 let report = rules.evaluate(&parent, (&pi_plus, &pi_minus), l!(1), j!(0));
1291
1292 assert!(report.is_allowed());
1293 assert_eq!(report.len(), rules.enabled_rules().count());
1294 assert!(
1295 report
1296 .checks
1297 .iter()
1298 .all(|check| { matches!(check.outcome, RuleOutcome::Pass { .. }) })
1299 );
1300
1301 for rule in rules.enabled_rules() {
1302 assert_pass(&report, rule);
1303 }
1304 }
1305
1306 #[test]
1307 fn nontrivial_quantum_number_rules_report_failures() {
1308 let parent = ParticleProperties::meson()
1309 .with_zero_flavor()
1310 .with_name("bad_parent")
1311 .with_self_conjugate_species("bad_parent")
1312 .unwrap()
1313 .with_spin(j!(1))
1314 .with_parity(Parity::Positive)
1315 .with_c_parity(Parity::Positive)
1316 .unwrap()
1317 .with_charge(0)
1318 .with_isospin(Isospin::new(j!(3), Some(m!(1))).unwrap())
1319 .with_g_parity(Parity::Negative)
1320 .with_statistics(Statistics::Boson)
1321 .unwrap();
1322
1323 let pi_plus = pion_like("pi+", "pi-", 1, 1);
1324 let pi_minus = pion_like("pi-", "pi+", -1, -1);
1325
1326 let rules = RuleSet::angular()
1327 .enforce(RuleKind::Parity)
1328 .enforce(RuleKind::Isospin)
1329 .enforce(RuleKind::IsospinProjection)
1330 .enforce(RuleKind::CParity)
1331 .enforce(RuleKind::GParity);
1332
1333 let report = rules.evaluate(&parent, (&pi_plus, &pi_minus), l!(1), j!(0));
1334
1335 assert!(!report.is_allowed());
1336 assert_eq!(report.failures().count(), 5);
1337
1338 assert_fail(&report, RuleKind::Parity);
1339 assert_fail(&report, RuleKind::Isospin);
1340 assert_fail(&report, RuleKind::IsospinProjection);
1341 assert_fail(&report, RuleKind::CParity);
1342 assert_fail(&report, RuleKind::GParity);
1343 }
1344
1345 #[test]
1346 fn additive_and_lepton_rules_report_passes_failures_and_missing_fields() {
1347 let parent = add_additives(
1348 ParticleProperties::unknown(),
1349 1, 1, 1, 1, 1, 1, 1, 0,
1357 0,
1358 );
1359
1360 let daughter_a = add_additives(ParticleProperties::unknown(), 0, 0, 0, 0, 0, 0, 0, 1, 0);
1361 let daughter_b = add_additives(ParticleProperties::unknown(), 0, 0, 0, 0, 0, 0, 0, 0, 0);
1362
1363 let rules = RuleSet::angular()
1364 .enforce(RuleKind::Charge)
1365 .enforce(RuleKind::Strangeness)
1366 .enforce(RuleKind::Charm)
1367 .enforce(RuleKind::Bottomness)
1368 .enforce(RuleKind::Topness)
1369 .enforce(RuleKind::BaryonNumber)
1370 .enforce(RuleKind::ElectronLeptonNumber)
1371 .enforce(RuleKind::MuonLeptonNumber)
1372 .enforce(RuleKind::TauLeptonNumber)
1373 .enforce(RuleKind::LeptonNumber);
1374
1375 let report = rules.evaluate(&parent, (&daughter_a, &daughter_b), l!(0), j!(0));
1376
1377 assert!(!report.is_allowed());
1378
1379 for rule in [
1380 RuleKind::Charge,
1381 RuleKind::Strangeness,
1382 RuleKind::Charm,
1383 RuleKind::Bottomness,
1384 RuleKind::Topness,
1385 RuleKind::BaryonNumber,
1386 RuleKind::ElectronLeptonNumber,
1387 RuleKind::MuonLeptonNumber,
1388 ] {
1389 assert_fail(&report, rule);
1390 }
1391
1392 assert_pass(&report, RuleKind::TauLeptonNumber);
1393 assert_pass(&report, RuleKind::LeptonNumber);
1394
1395 let parent_missing = ParticleProperties::unknown().with_charge(0);
1396 let a_missing = ParticleProperties::unknown().with_charge(0);
1397 let b_missing = ParticleProperties::unknown();
1398
1399 let unknown_report = RuleSet::angular()
1400 .enforce(RuleKind::Charge)
1401 .enforce(RuleKind::LeptonNumber)
1402 .evaluate(&parent_missing, (&a_missing, &b_missing), l!(0), j!(0));
1403
1404 assert!(unknown_report.is_allowed());
1405 assert_unknown_allowed(&unknown_report, RuleKind::Charge, &["daughter_b.charge"]);
1406 assert_unknown_allowed(
1407 &unknown_report,
1408 RuleKind::LeptonNumber,
1409 &[
1410 "parent.electron_lepton_number",
1411 "daughter_a.electron_lepton_number",
1412 "daughter_b.electron_lepton_number",
1413 ],
1414 );
1415 }
1416
1417 #[test]
1418 fn identical_particle_symmetry_handles_bosons_and_fermions_with_spin_dependence() {
1419 let scalar_a = identical_boson(j!(0), "scalar");
1420 let scalar_b = identical_boson(j!(0), "scalar");
1421
1422 let vector_a = identical_boson(j!(1), "vector");
1423 let vector_b = identical_boson(j!(1), "vector");
1424
1425 let fermion_a = identical_fermion(j!(1 / 2), "fermion");
1426 let fermion_b = identical_fermion(j!(1 / 2), "fermion");
1427
1428 let rules = RuleSet::angular().enforce(RuleKind::IdenticalParticleSymmetry);
1429
1430 let scalar_even_l = rules.evaluate(
1431 &ParticleProperties::unknown(),
1432 (&scalar_a, &scalar_b),
1433 l!(0),
1434 j!(0),
1435 );
1436 assert_pass(&scalar_even_l, RuleKind::IdenticalParticleSymmetry);
1437
1438 let scalar_odd_l = rules.evaluate(
1439 &ParticleProperties::unknown(),
1440 (&scalar_a, &scalar_b),
1441 l!(1),
1442 j!(0),
1443 );
1444 assert_fail(&scalar_odd_l, RuleKind::IdenticalParticleSymmetry);
1445
1446 let vector_s0 = rules.evaluate(
1447 &ParticleProperties::unknown(),
1448 (&vector_a, &vector_b),
1449 l!(0),
1450 j!(0),
1451 );
1452 assert_pass(&vector_s0, RuleKind::IdenticalParticleSymmetry);
1453
1454 let vector_s1 = rules.evaluate(
1455 &ParticleProperties::unknown(),
1456 (&vector_a, &vector_b),
1457 l!(0),
1458 j!(1),
1459 );
1460 assert_fail(&vector_s1, RuleKind::IdenticalParticleSymmetry);
1461
1462 let fermion_s0 = rules.evaluate(
1463 &ParticleProperties::unknown(),
1464 (&fermion_a, &fermion_b),
1465 l!(0),
1466 j!(0),
1467 );
1468 assert_pass(&fermion_s0, RuleKind::IdenticalParticleSymmetry);
1469
1470 let fermion_s1 = rules.evaluate(
1471 &ParticleProperties::unknown(),
1472 (&fermion_a, &fermion_b),
1473 l!(0),
1474 j!(1),
1475 );
1476 assert_fail(&fermion_s1, RuleKind::IdenticalParticleSymmetry);
1477
1478 let different_species = rules.evaluate(
1479 &ParticleProperties::unknown(),
1480 (&identical_boson(j!(0), "a"), &identical_boson(j!(0), "b")),
1481 l!(1),
1482 j!(0),
1483 );
1484 assert_pass(&different_species, RuleKind::IdenticalParticleSymmetry);
1485 }
1486
1487 #[test]
1488 fn c_parity_rule_distinguishes_inferred_c_from_non_inferable_final_states() {
1489 let parent = rho_like();
1490 let pi_plus = pion_like("pi+", "pi-", 1, 1);
1491 let pi_minus = pion_like("pi-", "pi+", -1, -1);
1492 let pi_zero = pion_like("pi0", "pi0", 0, 0);
1493
1494 let rules = RuleSet::angular().enforce(RuleKind::CParity);
1495
1496 let p_wave_report = rules.evaluate(&parent, (&pi_plus, &pi_minus), l!(1), j!(0));
1497 assert_pass(&p_wave_report, RuleKind::CParity);
1498
1499 let s_wave_report = rules.evaluate(&parent, (&pi_plus, &pi_minus), l!(0), j!(0));
1500 assert_fail(&s_wave_report, RuleKind::CParity);
1501
1502 let unknown_report = rules.evaluate(&parent, (&pi_plus, &pi_zero), l!(1), j!(0));
1503 assert!(unknown_report.is_allowed());
1504 assert_unknown_allowed(
1505 &unknown_report,
1506 RuleKind::CParity,
1507 &[
1508 "daughter_a.species",
1509 "daughter_a.antiparticle_species",
1510 "daughter_b.species",
1511 "daughter_b.antiparticle_species",
1512 ],
1513 );
1514 }
1515
1516 #[test]
1517 fn conventional_meson_jpc_can_be_enforced_or_used_as_non_rejecting_diagnostic() {
1518 let conventional = rho_like();
1519 let exotic = exotic_one_minus_plus();
1520
1521 let diagnostic_rules = RuleSet::angular().diagnose_only(
1522 RuleKind::ConventionalMesonJpc,
1523 "flag exotic JPC without rejecting hybrid candidates",
1524 );
1525
1526 let conventional_report =
1527 diagnostic_rules.evaluate(&conventional, (&conventional, &conventional), l!(0), j!(0));
1528 assert!(conventional_report.is_allowed());
1529 assert_diagnostic(
1530 &conventional_report,
1531 RuleKind::ConventionalMesonJpc,
1532 Some(true),
1533 Some("flag exotic JPC without rejecting hybrid candidates"),
1534 );
1535
1536 let exotic_report = diagnostic_rules.evaluate(&exotic, (&exotic, &exotic), l!(0), j!(0));
1537 assert!(exotic_report.is_allowed());
1538 assert_diagnostic(
1539 &exotic_report,
1540 RuleKind::ConventionalMesonJpc,
1541 Some(false),
1542 Some("flag exotic JPC without rejecting hybrid candidates"),
1543 );
1544
1545 let enforced_report = RuleSet::angular()
1546 .enforce(RuleKind::ConventionalMesonJpc)
1547 .evaluate(&exotic, (&exotic, &exotic), l!(0), j!(0));
1548 assert!(!enforced_report.is_allowed());
1549 assert_fail(&enforced_report, RuleKind::ConventionalMesonJpc);
1550
1551 let unknown_report = RuleSet::angular()
1552 .diagnose_only_without_reason(RuleKind::ConventionalMesonJpc)
1553 .evaluate(
1554 &ParticleProperties::unknown(),
1555 (
1556 &ParticleProperties::unknown(),
1557 &ParticleProperties::unknown(),
1558 ),
1559 l!(0),
1560 j!(0),
1561 );
1562 assert_diagnostic(&unknown_report, RuleKind::ConventionalMesonJpc, None, None);
1563 }
1564
1565 #[test]
1566 fn selection_rules_scan_partial_waves_keeps_rejected_candidates_for_diagnostics() {
1567 let parent = ParticleProperties::jp(j!(1), Parity::Positive);
1568 let a = ParticleProperties::jp(j!(1 / 2), Parity::Positive);
1569 let b = ParticleProperties::jp(j!(1 / 2), Parity::Negative);
1570
1571 let angular_scan = SelectionRules::angular(l!(2)).scan_partial_waves(&parent, (&a, &b));
1572
1573 assert!(angular_scan.missing_inputs.is_empty());
1574 assert_eq!(
1575 candidate_labels(angular_scan.candidates.iter()),
1576 vec!["1P1", "3S1", "3P1", "3D1"]
1577 );
1578 assert_eq!(
1579 allowed_labels(angular_scan.allowed()),
1580 vec!["1P1", "3S1", "3P1", "3D1"]
1581 );
1582 assert_eq!(angular_scan.rejected().count(), 0);
1583
1584 let parity_rules = SelectionRules::new(RuleSet::angular().enforce(RuleKind::Parity), l!(2));
1585 let parity_scan = parity_rules.scan_partial_waves(&parent, (&a, &b));
1586
1587 assert_eq!(
1588 candidate_labels(parity_scan.candidates.iter()),
1589 vec!["1P1", "3S1", "3P1", "3D1"]
1590 );
1591 assert_eq!(allowed_labels(parity_scan.allowed()), vec!["1P1", "3P1"]);
1592 assert_eq!(candidate_labels(parity_scan.rejected()), vec!["3S1", "3D1"]);
1593
1594 let allowed = parity_rules.allowed_partial_waves(&parent, (&a, &b));
1595 assert_eq!(labels(&allowed), vec!["1P1", "3P1"]);
1596 }
1597
1598 #[test]
1599 fn selection_rules_report_missing_spin_inputs_and_default_to_strong_l6() {
1600 assert_eq!(SelectionRules::default(), SelectionRules::strong(l!(6)));
1601 assert_eq!(
1602 SelectionRules::electromagnetic(l!(2)),
1603 SelectionRules::new(RuleSet::electromagnetic(), l!(2))
1604 );
1605 assert_eq!(
1606 SelectionRules::weak(l!(3)),
1607 SelectionRules::new(RuleSet::weak(), l!(3))
1608 );
1609
1610 let parent_missing = ParticleProperties::unknown();
1611 let a = ParticleProperties::jp(j!(0), Parity::Negative);
1612 let b = ParticleProperties::jp(j!(0), Parity::Negative);
1613
1614 let scan = SelectionRules::default().scan_partial_waves(&parent_missing, (&a, &b));
1615
1616 assert!(scan.candidates.is_empty());
1617 assert_eq!(scan.missing_inputs, vec!["parent.spin"]);
1618
1619 let parent = ParticleProperties::jp(j!(0), Parity::Positive);
1620 let a_missing = ParticleProperties::unknown();
1621
1622 let scan = SelectionRules::default().scan_partial_waves(&parent, (&a_missing, &b));
1623
1624 assert!(scan.candidates.is_empty());
1625 assert_eq!(scan.missing_inputs, vec!["daughter_a.spin"]);
1626
1627 let b_missing = ParticleProperties::unknown();
1628
1629 let scan = SelectionRules::default().scan_partial_waves(&parent, (&a, &b_missing));
1630
1631 assert!(scan.candidates.is_empty());
1632 assert_eq!(scan.missing_inputs, vec!["daughter_b.spin"]);
1633 }
1634
1635 #[test]
1636 fn strong_rules_find_delta_like_to_nucleon_pion_p_wave() {
1637 let parent = add_additives(
1638 ParticleProperties::jp(j!(3 / 2), Parity::Positive),
1639 1,
1640 0,
1641 0,
1642 0,
1643 0,
1644 1,
1645 0,
1646 0,
1647 0,
1648 );
1649
1650 let nucleon = add_additives(
1651 ParticleProperties::jp(j!(1 / 2), Parity::Positive),
1652 1,
1653 0,
1654 0,
1655 0,
1656 0,
1657 1,
1658 0,
1659 0,
1660 0,
1661 );
1662
1663 let pion = add_additives(
1664 ParticleProperties::jp(j!(0), Parity::Negative),
1665 0,
1666 0,
1667 0,
1668 0,
1669 0,
1670 0,
1671 0,
1672 0,
1673 0,
1674 );
1675
1676 let rules = SelectionRules::new(
1677 RuleSet::angular()
1678 .enforce(RuleKind::Parity)
1679 .enforce(RuleKind::Charge)
1680 .enforce(RuleKind::BaryonNumber),
1681 l!(4),
1682 );
1683
1684 let waves = rules.allowed_partial_waves(&parent, (&nucleon, &pion));
1685
1686 assert_eq!(labels(&waves), vec!["2P3/2"]);
1687 assert_eq!(waves[0].parity, Some(Parity::Positive));
1688 assert_eq!(waves[0].c_parity, None);
1689 }
1690}