1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fmt::{self, Display},
4 str::FromStr,
5};
6
7use color_eyre::Result;
8use eyre::{Context, eyre};
9use itertools::Itertools;
10use linnet::half_edge::{
11 involution::EdgeIndex,
12 subgraph::{Cycle, ModifySubSet, SuBitGraph, SubSetLike, SubSetOps},
13};
14use typed_index_collections::TiVec;
15
16use crate::{
17 graph::{Graph, GraphGroup, GroupId, edge::EdgeMass},
18 model::{ArcParticle, Model},
19};
20
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub enum GraphGroupSelectionMode {
23 #[default]
24 MasterGraphs,
25 CrossSectionAmplitudeGraphs,
26}
27
28#[derive(Debug, Clone, Default, PartialEq, Eq)]
29pub struct GraphGroupSelectionSpec {
30 rules: Vec<GraphGroupSelectionRule>,
31 mode: GraphGroupSelectionMode,
32}
33
34impl GraphGroupSelectionSpec {
35 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn from_master_graph_names(graph_names: Vec<String>) -> Self {
40 Self::new().with_master_graph_names(graph_names)
41 }
42
43 pub fn with_mode(mut self, mode: GraphGroupSelectionMode) -> Self {
44 self.mode = mode;
45 self
46 }
47
48 pub fn mode(&self) -> GraphGroupSelectionMode {
49 self.mode
50 }
51
52 pub fn with_master_graph_names(self, graph_names: Vec<String>) -> Self {
53 self.with_master_graph_names_polarity(SelectionPolarity::With, graph_names)
54 }
55
56 pub fn with_master_graph_names_polarity(
57 mut self,
58 polarity: SelectionPolarity,
59 graph_names: Vec<String>,
60 ) -> Self {
61 if !graph_names.is_empty() {
62 self.rules.push(GraphGroupSelectionRule::MasterGraphNames {
63 polarity,
64 graph_names,
65 });
66 }
67 self
68 }
69
70 pub fn with_raised_propagator_signatures(
71 mut self,
72 polarity: SelectionPolarity,
73 scope: RaisedPropagatorScope,
74 signatures: Vec<RaisedPropagatorSignature>,
75 ) -> Self {
76 if !signatures.is_empty() {
77 self.rules.push(GraphGroupSelectionRule::RaisedPropagators {
78 polarity,
79 scope,
80 signatures,
81 });
82 }
83 self
84 }
85
86 pub fn with_raised_cut_signatures(
87 mut self,
88 polarity: SelectionPolarity,
89 scope: RaisedPropagatorScope,
90 signatures: Vec<RaisedPropagatorSignature>,
91 ) -> Self {
92 if !signatures.is_empty() {
93 self.rules.push(GraphGroupSelectionRule::RaisedCuts {
94 polarity,
95 scope,
96 signatures,
97 });
98 }
99 self
100 }
101
102 pub fn with_cycle_signatures(
103 mut self,
104 polarity: SelectionPolarity,
105 signatures: Vec<CycleSignature>,
106 ) -> Self {
107 if !signatures.is_empty() {
108 self.rules.push(GraphGroupSelectionRule::Cycles {
109 polarity,
110 signatures,
111 });
112 }
113 self
114 }
115
116 pub fn with_vertex_signatures(
117 mut self,
118 polarity: SelectionPolarity,
119 signatures: Vec<VertexSignature>,
120 ) -> Self {
121 if !signatures.is_empty() {
122 self.rules.push(GraphGroupSelectionRule::Vertices {
123 polarity,
124 signatures,
125 });
126 }
127 self
128 }
129
130 pub fn with_particle_signatures(
131 mut self,
132 polarity: SelectionPolarity,
133 signatures: Vec<ParticleSignature>,
134 ) -> Self {
135 if !signatures.is_empty() {
136 self.rules.push(GraphGroupSelectionRule::Particles {
137 polarity,
138 signatures,
139 });
140 }
141 self
142 }
143
144 pub fn is_empty(&self) -> bool {
145 self.rules.is_empty()
146 }
147
148 pub fn has_raised_cut_rules(&self) -> bool {
149 self.rules
150 .iter()
151 .any(GraphGroupSelectionRule::uses_cut_analysis)
152 }
153
154 pub(crate) fn has_graph_analysis_rules(&self) -> bool {
155 self.rules
156 .iter()
157 .any(GraphGroupSelectionRule::uses_graph_analysis)
158 }
159
160 pub fn plan<'a, F>(
161 &self,
162 graph_group_structure: &TiVec<GroupId, GraphGroup>,
163 graph_by_id: F,
164 ) -> Result<GraphGroupSelectionPlan>
165 where
166 F: FnMut(usize) -> Option<&'a Graph>,
167 {
168 self.plan_with_analysis_contexts(
169 graph_group_structure,
170 graph_by_id,
171 |_master_graph_id, master_graph| {
172 Ok(vec![GraphSelectionSubject::whole_graph(master_graph)])
173 },
174 |_master_graph_id, _master_graph| Ok(Vec::new()),
175 "Graph-group selection structural filters have no graph analysis subjects.",
176 "Graph-group selection raised-cut filters require cross-section Cutkosky cuts.",
177 )
178 }
179
180 pub(crate) fn plan_with_analysis_contexts<'a, F, S, C>(
181 &self,
182 graph_group_structure: &TiVec<GroupId, GraphGroup>,
183 mut graph_by_id: F,
184 mut analysis_subjects_by_master_id: S,
185 mut cut_subjects_by_master_id: C,
186 no_structural_subjects_message: &str,
187 no_cut_subjects_message: &str,
188 ) -> Result<GraphGroupSelectionPlan>
189 where
190 F: FnMut(usize) -> Option<&'a Graph>,
191 S: FnMut(usize, &'a Graph) -> Result<Vec<GraphSelectionSubject<'a>>>,
192 C: FnMut(usize, &'a Graph) -> Result<Vec<GraphCutSelectionSubject<'a>>>,
193 {
194 if self.rules.is_empty() {
195 return Err(eyre!("No graph-group selection rules were provided."));
196 }
197 if graph_group_structure.is_empty() {
198 return Err(eyre!("Cannot select graph groups from an empty integrand."));
199 }
200
201 let candidates = graph_group_structure
202 .iter_enumerated()
203 .map(|(group_id, group)| {
204 let master_graph_id = group.master();
205 let master_graph = graph_by_id(master_graph_id).ok_or_else(|| {
206 eyre!(
207 "Graph group {} refers to missing master graph id {}.",
208 group_id.0,
209 master_graph_id
210 )
211 })?;
212 let graph_names = group
213 .into_iter()
214 .map(|graph_id| {
215 graph_by_id(graph_id)
216 .map(|graph| graph.name.clone())
217 .ok_or_else(|| {
218 eyre!(
219 "Graph group {} refers to missing graph id {}.",
220 group_id.0,
221 graph_id
222 )
223 })
224 })
225 .collect::<Result<Vec<_>>>()?;
226 let analysis_subjects =
227 analysis_subjects_by_master_id(master_graph_id, master_graph)?;
228 let cut_subjects = if self.has_raised_cut_rules() {
229 cut_subjects_by_master_id(master_graph_id, master_graph)?
230 } else {
231 Vec::new()
232 };
233 Ok(GraphGroupSelectionCandidate {
234 group_id,
235 master_graph_id,
236 master_graph_name: master_graph.name.clone(),
237 analysis_subjects,
238 cut_subjects,
239 graph_names,
240 })
241 })
242 .collect::<Result<Vec<_>>>()?;
243
244 if self.has_graph_analysis_rules()
245 && candidates
246 .iter()
247 .all(|candidate| candidate.analysis_subjects.is_empty())
248 {
249 return Err(eyre!(no_structural_subjects_message.to_string()));
250 }
251 if self.has_raised_cut_rules()
252 && candidates
253 .iter()
254 .all(|candidate| candidate.cut_subjects.is_empty())
255 {
256 return Err(eyre!(no_cut_subjects_message.to_string()));
257 }
258
259 let mut master_name_to_group = BTreeMap::<String, GroupId>::new();
260 for candidate in &candidates {
261 if let Some(previous) =
262 master_name_to_group.insert(candidate.master_graph_name.clone(), candidate.group_id)
263 {
264 return Err(eyre!(
265 "Master graph name '{}' is ambiguous: it appears in groups {} and {}.",
266 candidate.master_graph_name,
267 previous.0,
268 candidate.group_id.0
269 ));
270 }
271 }
272
273 let authoritative_group_ids =
274 self.authoritative_master_graph_group_ids(&candidates, &master_name_to_group)?;
275 let mut forbidden_master_graph_group_ids = BTreeSet::new();
276 for rule in &self.rules {
277 let GraphGroupSelectionRule::MasterGraphNames {
278 polarity: SelectionPolarity::Without,
279 graph_names,
280 } = rule
281 else {
282 continue;
283 };
284 forbidden_master_graph_group_ids.extend(
285 GraphGroupSelectionRule::resolve_master_graph_name_set(
286 graph_names,
287 &candidates,
288 &master_name_to_group,
289 )?,
290 );
291 }
292 let conflicting_master_graph_names = candidates
293 .iter()
294 .filter(|candidate| {
295 authoritative_group_ids.contains(&candidate.group_id)
296 && forbidden_master_graph_group_ids.contains(&candidate.group_id)
297 })
298 .map(|candidate| candidate.master_graph_name.as_str())
299 .collect::<Vec<_>>();
300 if !conflicting_master_graph_names.is_empty() {
301 return Err(eyre!(
302 "Contradictory graph-name selection: master graph(s) {} appear in both --with-graph-names and --without-graph-names.",
303 conflicting_master_graph_names.join(", ")
304 ));
305 }
306 let non_authoritative_rules = self
307 .rules
308 .iter()
309 .filter(|rule| !rule.is_authoritative_master_graph_name_rule())
310 .collect::<Vec<_>>();
311
312 let mut retained_group_ids = if non_authoritative_rules.is_empty() {
313 authoritative_group_ids.clone()
314 } else {
315 candidates
316 .iter()
317 .map(|candidate| candidate.group_id)
318 .collect::<BTreeSet<_>>()
319 };
320 for rule in non_authoritative_rules {
321 let rule_groups = rule.resolve(&candidates, &master_name_to_group)?;
322 retained_group_ids = retained_group_ids
323 .intersection(&rule_groups)
324 .copied()
325 .collect::<BTreeSet<_>>();
326 }
327 retained_group_ids.extend(authoritative_group_ids);
328
329 if retained_group_ids.is_empty() {
330 return Err(eyre!(
331 "Graph-group selection would remove all graph groups."
332 ));
333 }
334
335 let retained_group_ids = candidates
336 .iter()
337 .filter_map(|candidate| {
338 retained_group_ids
339 .contains(&candidate.group_id)
340 .then_some(candidate.group_id)
341 })
342 .collect::<Vec<_>>();
343 let old_to_new_group_id = retained_group_ids
344 .iter()
345 .enumerate()
346 .map(|(new_group_id, old_group_id)| (*old_group_id, GroupId(new_group_id)))
347 .collect::<BTreeMap<_, _>>();
348 let kept_master_graphs = candidates
349 .iter()
350 .filter(|candidate| old_to_new_group_id.contains_key(&candidate.group_id))
351 .map(|candidate| candidate.master_graph_name.clone())
352 .collect::<Vec<_>>();
353 let removed_master_graphs = candidates
354 .iter()
355 .filter(|candidate| !old_to_new_group_id.contains_key(&candidate.group_id))
356 .map(|candidate| candidate.master_graph_name.clone())
357 .collect::<Vec<_>>();
358 let removed_graphs = candidates
359 .iter()
360 .filter(|candidate| !old_to_new_group_id.contains_key(&candidate.group_id))
361 .flat_map(|candidate| candidate.graph_names.iter().cloned())
362 .collect::<Vec<_>>();
363
364 Ok(GraphGroupSelectionPlan {
365 retained_group_ids,
366 old_to_new_group_id,
367 report: GraphGroupSelectionReport {
368 kept_master_graphs,
369 removed_master_graphs,
370 removed_graphs,
371 },
372 })
373 }
374
375 fn authoritative_master_graph_group_ids<'a>(
376 &self,
377 candidates: &[GraphGroupSelectionCandidate<'a>],
378 master_name_to_group: &BTreeMap<String, GroupId>,
379 ) -> Result<BTreeSet<GroupId>> {
380 let mut group_ids = BTreeSet::new();
381 for rule in self
382 .rules
383 .iter()
384 .filter(|rule| rule.is_authoritative_master_graph_name_rule())
385 {
386 group_ids.extend(
387 rule.resolve_authoritative_master_graph_names(candidates, master_name_to_group)?,
388 );
389 }
390 Ok(group_ids)
391 }
392}
393
394#[derive(Debug, Clone, PartialEq, Eq)]
395pub struct GraphGroupSelectionPlan {
396 retained_group_ids: Vec<GroupId>,
397 old_to_new_group_id: BTreeMap<GroupId, GroupId>,
398 report: GraphGroupSelectionReport,
399}
400
401impl GraphGroupSelectionPlan {
402 pub fn retained_group_ids(&self) -> &[GroupId] {
403 &self.retained_group_ids
404 }
405
406 pub fn new_group_id_for_old(&self, group_id: GroupId) -> Option<GroupId> {
407 self.old_to_new_group_id.get(&group_id).copied()
408 }
409
410 pub fn report(&self) -> &GraphGroupSelectionReport {
411 &self.report
412 }
413}
414
415#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct GraphGroupSelectionReport {
417 pub kept_master_graphs: Vec<String>,
418 pub removed_master_graphs: Vec<String>,
419 pub removed_graphs: Vec<String>,
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
423pub enum SelectionPolarity {
424 With,
425 Without,
426}
427
428impl SelectionPolarity {
429 fn keep_if_match(self, matched: bool) -> bool {
430 match self {
431 Self::With => matched,
432 Self::Without => !matched,
433 }
434 }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438pub enum RaisedPropagatorScope {
439 All,
440 Massive,
441 Massless,
442}
443
444#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
445pub enum RaisedPropagatorSignature {
446 Exact(Vec<usize>),
447 AnyRaising,
448}
449
450impl RaisedPropagatorSignature {
451 pub const ANY_RAISING_KEYWORD: &'static str = "ANY_RAISING";
452
453 pub fn new(mut multiplicities: Vec<usize>) -> Result<Self> {
454 if let Some(invalid) = multiplicities
455 .iter()
456 .find(|multiplicity| **multiplicity < 2)
457 {
458 return Err(eyre!(
459 "Raised-propagator signatures only accept multiplicities >= 2; found {}.",
460 invalid
461 ));
462 }
463 multiplicities.sort_unstable();
464 Ok(Self::Exact(multiplicities))
465 }
466
467 pub fn canonical(&self) -> String {
468 self.to_string()
469 }
470
471 fn matches(&self, actual: &Self) -> bool {
472 match self {
473 Self::Exact(_) => self == actual,
474 Self::AnyRaising => {
475 matches!(actual, Self::Exact(multiplicities) if !multiplicities.is_empty())
476 }
477 }
478 }
479}
480
481impl Display for RaisedPropagatorSignature {
482 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483 match self {
484 Self::Exact(multiplicities) => write!(f, "[{}]", multiplicities.iter().join(",")),
485 Self::AnyRaising => write!(f, "{}", Self::ANY_RAISING_KEYWORD),
486 }
487 }
488}
489
490impl FromStr for RaisedPropagatorSignature {
491 type Err = eyre::Report;
492
493 fn from_str(raw: &str) -> Result<Self> {
494 if raw.trim().eq_ignore_ascii_case(Self::ANY_RAISING_KEYWORD) {
495 return Ok(Self::AnyRaising);
496 }
497 let content = bracket_content(raw, '[', ']')
498 .with_context(|| format!("Invalid raised-propagator signature '{raw}'"))?;
499 if content.trim().is_empty() {
500 return Ok(Self::Exact(Vec::new()));
501 }
502 let entries = parse_comma_separated_list(content)
503 .with_context(|| format!("Invalid raised-propagator signature '{raw}'"))?;
504 let multiplicities = entries
505 .into_iter()
506 .map(|value| {
507 value.parse::<usize>().map_err(|_| {
508 eyre!("Invalid raised-propagator multiplicity '{value}' in '{raw}'.")
509 })
510 })
511 .collect::<Result<Vec<_>>>()?;
512 Self::new(multiplicities)
513 }
514}
515
516#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
517pub struct CycleSignature(Vec<CycleRequirement>);
518
519impl CycleSignature {
520 pub fn new(requirements: Vec<CycleRequirement>) -> Result<Self> {
521 if requirements.is_empty() {
522 return Err(eyre!(
523 "Cycle signatures require at least one cycle requirement."
524 ));
525 }
526 let mut seen = BTreeSet::new();
527 for requirement in &requirements {
528 if !seen.insert(requirement.clone()) {
529 return Err(eyre!(
530 "Duplicate cycle requirement '{}' is ambiguous; specify each required cycle once.",
531 requirement
532 ));
533 }
534 }
535 Ok(Self(requirements))
536 }
537
538 pub fn parse(raw: &str, model: &Model) -> Result<Self> {
539 let content = bracket_content(raw, '[', ']')
540 .with_context(|| format!("Invalid cycle signature '{raw}'"))?;
541 let requirements = parse_cycle_requirements(content, model)
542 .with_context(|| format!("Invalid cycle signature '{raw}'"))?;
543 Self::new(requirements)
544 }
545
546 pub fn canonical(&self) -> String {
547 self.to_string()
548 }
549}
550
551impl Display for CycleSignature {
552 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553 write!(f, "[{}]", self.0.iter().join(","))
554 }
555}
556
557#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
558pub struct CycleRequirement(Vec<CycleMatcher>);
559
560impl CycleRequirement {
561 pub fn new(mut matchers: Vec<CycleMatcher>) -> Result<Self> {
562 if matchers.is_empty() {
563 return Err(eyre!("Cycle requirements cannot be empty."));
564 }
565 matchers.sort();
566 matchers.dedup();
567 Ok(Self(matchers))
568 }
569}
570
571impl Display for CycleRequirement {
572 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
573 write!(f, "({})", self.0.iter().join(","))
574 }
575}
576
577#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
578pub enum CycleMatcher {
579 Pdg(isize),
580 Fermion,
581 Ghost,
582 Goldstone,
583}
584
585impl CycleMatcher {
586 fn matches(&self, particle: &ArcParticle) -> bool {
587 match self {
588 Self::Pdg(pdg) => particle.pdg_code.abs() == *pdg,
589 Self::Fermion => particle.is_fermion(),
590 Self::Ghost => particle.is_ghost(),
591 Self::Goldstone => particle.is_goldstone(),
592 }
593 }
594}
595
596impl Display for CycleMatcher {
597 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598 match self {
599 Self::Pdg(pdg) => write!(f, "{pdg}"),
600 Self::Fermion => f.write_str("fermion"),
601 Self::Ghost => f.write_str("ghost"),
602 Self::Goldstone => f.write_str("goldstone"),
603 }
604 }
605}
606
607#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
608pub struct VertexSignature(BTreeMap<String, usize>);
609
610impl VertexSignature {
611 pub fn new(vertex_rule_names: Vec<String>) -> Result<Self> {
612 if vertex_rule_names.is_empty() {
613 return Err(eyre!("Vertex signatures require at least one vertex rule."));
614 }
615 let mut counts = BTreeMap::<String, usize>::new();
616 for vertex_rule_name in vertex_rule_names {
617 if vertex_rule_name.trim().is_empty() {
618 return Err(eyre!("Vertex rule names cannot be empty."));
619 }
620 *counts.entry(vertex_rule_name).or_default() += 1;
621 }
622 Ok(Self(counts))
623 }
624
625 pub fn parse(raw: &str) -> Result<Self> {
626 let content = bracket_content(raw, '[', ']')
627 .with_context(|| format!("Invalid vertex signature '{raw}'"))?;
628 let names = parse_comma_separated_identifiers(content)
629 .with_context(|| format!("Invalid vertex signature '{raw}'"))?;
630 Self::new(names)
631 }
632
633 pub fn vertex_rule_names(&self) -> impl Iterator<Item = &str> {
634 self.0.keys().map(String::as_str)
635 }
636
637 pub fn canonical(&self) -> String {
638 self.to_string()
639 }
640}
641
642impl Display for VertexSignature {
643 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
644 let names = self
645 .0
646 .iter()
647 .flat_map(|(name, count)| std::iter::repeat_n(name, *count))
648 .join(",");
649 write!(f, "[{names}]")
650 }
651}
652
653#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
654pub struct ParticleSignature(BTreeSet<isize>);
655
656impl ParticleSignature {
657 pub fn new(pdgs: impl IntoIterator<Item = isize>) -> Result<Self> {
658 let pdgs = pdgs.into_iter().collect::<BTreeSet<_>>();
659 if pdgs.is_empty() {
660 return Err(eyre!("Particle signatures require at least one particle."));
661 }
662 Ok(Self(pdgs))
663 }
664
665 pub fn parse(raw: &str, model: &Model) -> Result<Self> {
666 let raw = raw.trim();
667 let content = if raw.starts_with('[') {
668 bracket_content(raw, '[', ']')
669 } else {
670 bracket_content(raw, '(', ')')
671 }
672 .with_context(|| format!("Invalid particle signature '{raw}'"))?;
673 let tokens = parse_comma_separated_list(content)
674 .with_context(|| format!("Invalid particle signature '{raw}'"))?;
675 let pdgs = tokens
676 .into_iter()
677 .map(|token| parse_particle_signature_pdg(&token, model))
678 .collect::<Result<Vec<_>>>()?;
679 Self::new(pdgs)
680 }
681
682 pub fn canonical(&self) -> String {
683 self.to_string()
684 }
685}
686
687impl Display for ParticleSignature {
688 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
689 write!(f, "[{}]", self.0.iter().join(","))
690 }
691}
692
693#[derive(Debug, Clone, PartialEq, Eq)]
694pub struct GraphSelectionSignatureInventory {
695 pub raised_all: Vec<String>,
696 pub raised_massive: Vec<String>,
697 pub raised_massless: Vec<String>,
698 pub cycles: Vec<String>,
699 pub vertices: Vec<String>,
700}
701
702impl GraphSelectionSignatureInventory {
703 pub fn empty() -> Self {
704 Self {
705 raised_all: vec!["[]".to_string(), "[2]".to_string()],
706 raised_massive: vec!["[]".to_string(), "[2]".to_string()],
707 raised_massless: vec!["[]".to_string(), "[2]".to_string()],
708 cycles: Vec::new(),
709 vertices: Vec::new(),
710 }
711 }
712
713 pub fn from_master_graphs<'a>(graphs: impl IntoIterator<Item = &'a Graph>) -> Self {
714 Self::from_analysis_subjects(graphs.into_iter().map(GraphSelectionSubject::whole_graph))
715 }
716
717 pub(crate) fn from_analysis_subjects<'a>(
718 subjects: impl IntoIterator<Item = GraphSelectionSubject<'a>>,
719 ) -> Self {
720 let mut raised_all = BTreeSet::from(["[]".to_string(), "[2]".to_string()]);
721 let mut raised_massive = BTreeSet::from(["[]".to_string(), "[2]".to_string()]);
722 let mut raised_massless = BTreeSet::from(["[]".to_string(), "[2]".to_string()]);
723 let mut cycles = BTreeSet::new();
724 let mut vertices = BTreeSet::new();
725
726 for subject in subjects {
727 raised_all.insert(
728 subject
729 .raised_propagator_signature(RaisedPropagatorScope::All)
730 .canonical(),
731 );
732 raised_massive.insert(
733 subject
734 .raised_propagator_signature(RaisedPropagatorScope::Massive)
735 .canonical(),
736 );
737 raised_massless.insert(
738 subject
739 .raised_propagator_signature(RaisedPropagatorScope::Massless)
740 .canonical(),
741 );
742 cycles.extend(
743 subject
744 .cycle_requirements()
745 .into_iter()
746 .map(|requirement| CycleSignature(vec![requirement]).canonical()),
747 );
748 if let Some(signature) = subject.vertex_signature() {
749 vertices.insert(signature.canonical());
750 }
751 }
752
753 Self {
754 raised_all: raised_all.into_iter().collect(),
755 raised_massive: raised_massive.into_iter().collect(),
756 raised_massless: raised_massless.into_iter().collect(),
757 cycles: cycles.into_iter().collect(),
758 vertices: vertices.into_iter().collect(),
759 }
760 }
761}
762
763#[derive(Debug, Clone, PartialEq, Eq)]
764pub struct RaisedCutSignatureInventory {
765 pub all: Vec<String>,
766 pub massive: Vec<String>,
767 pub massless: Vec<String>,
768}
769
770impl RaisedCutSignatureInventory {
771 pub fn empty() -> Self {
772 Self {
773 all: vec!["[]".to_string(), "[2]".to_string()],
774 massive: vec!["[]".to_string(), "[2]".to_string()],
775 massless: vec!["[]".to_string(), "[2]".to_string()],
776 }
777 }
778
779 pub(crate) fn from_cut_subjects<'a>(
780 subjects: impl IntoIterator<Item = GraphCutSelectionSubject<'a>>,
781 ) -> Self {
782 let mut all = BTreeSet::from(["[]".to_string(), "[2]".to_string()]);
783 let mut massive = BTreeSet::from(["[]".to_string(), "[2]".to_string()]);
784 let mut massless = BTreeSet::from(["[]".to_string(), "[2]".to_string()]);
785
786 for subject in subjects {
787 all.insert(
788 subject
789 .raised_cut_signature(RaisedPropagatorScope::All)
790 .canonical(),
791 );
792 massive.insert(
793 subject
794 .raised_cut_signature(RaisedPropagatorScope::Massive)
795 .canonical(),
796 );
797 massless.insert(
798 subject
799 .raised_cut_signature(RaisedPropagatorScope::Massless)
800 .canonical(),
801 );
802 }
803
804 Self {
805 all: all.into_iter().collect(),
806 massive: massive.into_iter().collect(),
807 massless: massless.into_iter().collect(),
808 }
809 }
810}
811
812#[derive(Debug, Clone, PartialEq, Eq)]
813enum GraphGroupSelectionRule {
814 MasterGraphNames {
815 polarity: SelectionPolarity,
816 graph_names: Vec<String>,
817 },
818 RaisedPropagators {
819 polarity: SelectionPolarity,
820 scope: RaisedPropagatorScope,
821 signatures: Vec<RaisedPropagatorSignature>,
822 },
823 RaisedCuts {
824 polarity: SelectionPolarity,
825 scope: RaisedPropagatorScope,
826 signatures: Vec<RaisedPropagatorSignature>,
827 },
828 Cycles {
829 polarity: SelectionPolarity,
830 signatures: Vec<CycleSignature>,
831 },
832 Vertices {
833 polarity: SelectionPolarity,
834 signatures: Vec<VertexSignature>,
835 },
836 Particles {
837 polarity: SelectionPolarity,
838 signatures: Vec<ParticleSignature>,
839 },
840}
841
842impl GraphGroupSelectionRule {
843 fn uses_graph_analysis(&self) -> bool {
844 matches!(
845 self,
846 Self::RaisedPropagators { .. }
847 | Self::Cycles { .. }
848 | Self::Vertices { .. }
849 | Self::Particles { .. }
850 )
851 }
852
853 fn uses_cut_analysis(&self) -> bool {
854 matches!(self, Self::RaisedCuts { .. })
855 }
856
857 fn is_authoritative_master_graph_name_rule(&self) -> bool {
858 matches!(
859 self,
860 Self::MasterGraphNames {
861 polarity: SelectionPolarity::With,
862 ..
863 }
864 )
865 }
866
867 fn resolve_authoritative_master_graph_names(
868 &self,
869 candidates: &[GraphGroupSelectionCandidate],
870 master_name_to_group: &BTreeMap<String, GroupId>,
871 ) -> Result<BTreeSet<GroupId>> {
872 let Self::MasterGraphNames {
873 polarity: SelectionPolarity::With,
874 graph_names,
875 } = self
876 else {
877 return Ok(BTreeSet::new());
878 };
879 Self::resolve_master_graph_name_set(graph_names, candidates, master_name_to_group)
880 }
881
882 fn resolve(
883 &self,
884 candidates: &[GraphGroupSelectionCandidate],
885 master_name_to_group: &BTreeMap<String, GroupId>,
886 ) -> Result<BTreeSet<GroupId>> {
887 match self {
888 Self::MasterGraphNames {
889 polarity,
890 graph_names,
891 } => {
892 let matched = Self::resolve_master_graph_name_set(
893 graph_names,
894 candidates,
895 master_name_to_group,
896 )?;
897 Ok(candidates
898 .iter()
899 .filter_map(|candidate| {
900 polarity
901 .keep_if_match(matched.contains(&candidate.group_id))
902 .then_some(candidate.group_id)
903 })
904 .collect())
905 }
906 Self::RaisedPropagators {
907 polarity,
908 scope,
909 signatures,
910 } => matching_candidates(candidates, |candidate| {
911 let matched = candidate.analysis_subjects.iter().any(|subject| {
912 let actual = subject.raised_propagator_signature(*scope);
913 signatures
914 .iter()
915 .any(|signature| signature.matches(&actual))
916 });
917 polarity.keep_if_match(matched)
918 }),
919 Self::RaisedCuts {
920 polarity,
921 scope,
922 signatures,
923 } => matching_candidates(candidates, |candidate| {
924 let matched = candidate.cut_subjects.iter().any(|subject| {
925 let actual = subject.raised_cut_signature(*scope);
926 signatures
927 .iter()
928 .any(|signature| signature.matches(&actual))
929 });
930 polarity.keep_if_match(matched)
931 }),
932 Self::Cycles {
933 polarity,
934 signatures,
935 } => matching_candidates(candidates, |candidate| {
936 let matched = candidate.analysis_subjects.iter().any(|subject| {
937 signatures
938 .iter()
939 .any(|signature| subject.matches_cycle_signature(signature))
940 });
941 polarity.keep_if_match(matched)
942 }),
943 Self::Vertices {
944 polarity,
945 signatures,
946 } => matching_candidates(candidates, |candidate| {
947 let matched = candidate.analysis_subjects.iter().any(|subject| {
948 let counts = subject.vertex_rule_name_counts();
949 signatures.iter().any(|signature| {
950 signature.0.iter().all(|(name, required_count)| {
951 counts.get(name).copied().unwrap_or_default() >= *required_count
952 })
953 })
954 });
955 polarity.keep_if_match(matched)
956 }),
957 Self::Particles {
958 polarity,
959 signatures,
960 } => matching_candidates(candidates, |candidate| {
961 let matched = candidate.analysis_subjects.iter().any(|subject| {
962 let pdgs = subject.particle_pdgs();
963 signatures
964 .iter()
965 .any(|signature| signature.0.iter().all(|pdg| pdgs.contains(pdg)))
966 });
967 polarity.keep_if_match(matched)
968 }),
969 }
970 }
971
972 fn resolve_master_graph_name_set(
973 graph_names: &[String],
974 candidates: &[GraphGroupSelectionCandidate],
975 master_name_to_group: &BTreeMap<String, GroupId>,
976 ) -> Result<BTreeSet<GroupId>> {
977 if graph_names.is_empty() {
978 return Err(eyre!(
979 "Graph-name selection requires at least one graph name."
980 ));
981 }
982
983 let mut seen = BTreeSet::<&str>::new();
984 for graph_name in graph_names {
985 if !seen.insert(graph_name.as_str()) {
986 return Err(eyre!(
987 "Duplicate graph name '{}' in graph-name selection.",
988 graph_name
989 ));
990 }
991 }
992
993 graph_names
994 .iter()
995 .map(|graph_name| {
996 resolve_master_graph_name(graph_name, candidates, master_name_to_group)
997 .with_context(|| {
998 format!(
999 "Available master graphs are: {}",
1000 master_name_to_group.keys().join(", ")
1001 )
1002 })
1003 })
1004 .collect::<Result<BTreeSet<_>>>()
1005 }
1006}
1007
1008#[derive(Clone)]
1009struct GraphGroupSelectionCandidate<'a> {
1010 group_id: GroupId,
1011 master_graph_id: usize,
1012 master_graph_name: String,
1013 analysis_subjects: Vec<GraphSelectionSubject<'a>>,
1014 cut_subjects: Vec<GraphCutSelectionSubject<'a>>,
1015 graph_names: Vec<String>,
1016}
1017
1018#[derive(Clone)]
1019pub(crate) struct GraphSelectionSubject<'a> {
1020 graph: &'a Graph,
1021 subgraph: Option<SuBitGraph>,
1022 raised_edge_policy: RaisedEdgePolicy,
1023}
1024
1025#[derive(Clone, Copy, PartialEq, Eq)]
1026enum RaisedEdgePolicy {
1027 LoopDependentOnly,
1028 AllInternalInSubject,
1029}
1030
1031impl<'a> GraphSelectionSubject<'a> {
1032 pub(crate) fn whole_graph(graph: &'a Graph) -> Self {
1033 Self {
1034 graph,
1035 subgraph: None,
1036 raised_edge_policy: RaisedEdgePolicy::LoopDependentOnly,
1037 }
1038 }
1039
1040 #[cfg(test)]
1041 pub(crate) fn subgraph(graph: &'a Graph, subgraph: SuBitGraph) -> Self {
1042 Self {
1043 graph,
1044 subgraph: Some(subgraph),
1045 raised_edge_policy: RaisedEdgePolicy::LoopDependentOnly,
1046 }
1047 }
1048
1049 pub(crate) fn cut_side_amplitude_subgraph(graph: &'a Graph, subgraph: SuBitGraph) -> Self {
1050 Self {
1051 graph,
1052 subgraph: Some(subgraph),
1053 raised_edge_policy: RaisedEdgePolicy::AllInternalInSubject,
1054 }
1055 }
1056
1057 fn raised_edge_subgraph(&self) -> SuBitGraph {
1058 match &self.subgraph {
1059 Some(subgraph) => subgraph
1060 .subtract(&self.graph.external_filter::<SuBitGraph>())
1061 .subtract(&self.graph.initial_state_cut.left)
1062 .subtract(&self.graph.initial_state_cut.right),
1063 None => self.graph.underlying.full_filter(),
1064 }
1065 }
1066
1067 fn internal_edge_subgraph(&self) -> SuBitGraph {
1068 let mut subgraph = match &self.subgraph {
1069 Some(subgraph) => subgraph.clone(),
1070 None => self.graph.underlying.full_filter(),
1071 }
1072 .subtract(&self.graph.external_filter::<SuBitGraph>())
1073 .subtract(&self.graph.initial_state_cut.left)
1074 .subtract(&self.graph.initial_state_cut.right);
1075
1076 for (pair, _, edge) in self.graph.underlying.iter_edges() {
1077 if edge.data.is_dummy {
1078 subgraph.sub(pair);
1079 }
1080 }
1081 subgraph
1082 }
1083
1084 fn vertex_subgraph(&self) -> Option<SuBitGraph> {
1085 self.subgraph
1086 .as_ref()
1087 .map(|_| self.internal_edge_subgraph())
1088 }
1089}
1090
1091#[derive(Clone)]
1092pub(crate) struct GraphCutSelectionSubject<'a> {
1093 graph: &'a Graph,
1094 cut_edges: SuBitGraph,
1095}
1096
1097impl<'a> GraphCutSelectionSubject<'a> {
1098 pub(crate) fn new(graph: &'a Graph, cut_edges: SuBitGraph) -> Self {
1099 Self { graph, cut_edges }
1100 }
1101
1102 fn raised_cut_signature(&self, scope: RaisedPropagatorScope) -> RaisedPropagatorSignature {
1103 let cut_edge_ids = self
1104 .graph
1105 .underlying
1106 .iter_edges_of(&self.cut_edges)
1107 .map(|(_, edge_id, _)| edge_id)
1108 .collect::<BTreeSet<_>>();
1109
1110 let multiplicities = GraphSelectionSubject::whole_graph(self.graph)
1111 .raised_edge_groups()
1112 .into_iter()
1113 .filter(|group| group.len() > 1)
1114 .filter(|group| raised_group_matches_scope(self.graph, group, scope))
1115 .filter(|group| group.iter().any(|edge| cut_edge_ids.contains(edge)))
1116 .map(|group| group.len())
1117 .collect::<Vec<_>>();
1118
1119 RaisedPropagatorSignature::new(multiplicities)
1120 .expect("group lengths are always valid raised-cut multiplicities")
1121 }
1122}
1123
1124trait GraphSelectionAnalysis {
1125 fn raised_propagator_signature(
1126 &self,
1127 scope: RaisedPropagatorScope,
1128 ) -> RaisedPropagatorSignature;
1129 fn cycle_requirements(&self) -> BTreeSet<CycleRequirement>;
1130 fn cycle_particle_sets(&self) -> Vec<Vec<ArcParticle>>;
1131 fn matches_cycle_signature(&self, signature: &CycleSignature) -> bool;
1132 fn vertex_rule_name_counts(&self) -> BTreeMap<String, usize>;
1133 fn particle_pdgs(&self) -> BTreeSet<isize>;
1134}
1135
1136impl GraphSelectionAnalysis for Graph {
1137 fn raised_propagator_signature(
1138 &self,
1139 scope: RaisedPropagatorScope,
1140 ) -> RaisedPropagatorSignature {
1141 GraphSelectionSubject::whole_graph(self).raised_propagator_signature(scope)
1142 }
1143
1144 fn cycle_requirements(&self) -> BTreeSet<CycleRequirement> {
1145 GraphSelectionSubject::whole_graph(self).cycle_requirements()
1146 }
1147
1148 fn cycle_particle_sets(&self) -> Vec<Vec<ArcParticle>> {
1149 GraphSelectionSubject::whole_graph(self).cycle_particle_sets()
1150 }
1151
1152 fn matches_cycle_signature(&self, signature: &CycleSignature) -> bool {
1153 GraphSelectionSubject::whole_graph(self).matches_cycle_signature(signature)
1154 }
1155
1156 fn vertex_rule_name_counts(&self) -> BTreeMap<String, usize> {
1157 GraphSelectionSubject::whole_graph(self).vertex_rule_name_counts()
1158 }
1159
1160 fn particle_pdgs(&self) -> BTreeSet<isize> {
1161 GraphSelectionSubject::whole_graph(self).particle_pdgs()
1162 }
1163}
1164
1165impl GraphSelectionAnalysis for GraphSelectionSubject<'_> {
1166 fn raised_propagator_signature(
1167 &self,
1168 scope: RaisedPropagatorScope,
1169 ) -> RaisedPropagatorSignature {
1170 let multiplicities = self
1171 .raised_edge_groups()
1172 .into_iter()
1173 .filter(|group| group.len() > 1)
1174 .filter(|group| raised_group_matches_scope(self.graph, group, scope))
1175 .map(|group| group.len())
1176 .collect::<Vec<_>>();
1177 RaisedPropagatorSignature::new(multiplicities)
1178 .expect("group lengths are always valid raised-propagator multiplicities")
1179 }
1180
1181 fn cycle_requirements(&self) -> BTreeSet<CycleRequirement> {
1182 self.cycle_particle_sets()
1183 .into_iter()
1184 .filter_map(cycle_requirement)
1185 .collect()
1186 }
1187
1188 fn cycle_particle_sets(&self) -> Vec<Vec<ArcParticle>> {
1189 self.internal_simple_cycles()
1190 .into_iter()
1191 .filter_map(|cycle| cycle_particles(self.graph, &cycle))
1192 .collect()
1193 }
1194
1195 fn matches_cycle_signature(&self, signature: &CycleSignature) -> bool {
1196 let cycle_particle_sets = self.cycle_particle_sets();
1197 signature.0.iter().all(|requirement| {
1198 cycle_particle_sets
1199 .iter()
1200 .any(|particles| cycle_matches_requirement(particles, requirement))
1201 })
1202 }
1203
1204 fn vertex_rule_name_counts(&self) -> BTreeMap<String, usize> {
1205 let mut counts = BTreeMap::<String, usize>::new();
1206 if let Some(subgraph) = self.vertex_subgraph() {
1207 for (_, _, vertex) in self.graph.underlying.iter_nodes_of(&subgraph) {
1208 if let Some(vertex_rule) = &vertex.vertex_rule {
1209 *counts.entry(vertex_rule.name.to_string()).or_default() += 1;
1210 }
1211 }
1212 } else {
1213 for (_, _, vertex) in self.graph.underlying.iter_nodes() {
1214 if let Some(vertex_rule) = &vertex.vertex_rule {
1215 *counts.entry(vertex_rule.name.to_string()).or_default() += 1;
1216 }
1217 }
1218 }
1219 counts
1220 }
1221
1222 fn particle_pdgs(&self) -> BTreeSet<isize> {
1223 self.graph
1224 .underlying
1225 .iter_edges_of(&self.internal_edge_subgraph())
1226 .filter_map(|(_, edge_id, _)| self.graph[edge_id].particle())
1227 .map(|particle| particle.pdg_code.abs())
1228 .collect()
1229 }
1230}
1231
1232trait GraphCycleSelectionAnalysis {
1233 fn internal_simple_cycles(&self) -> Vec<Cycle>;
1234}
1235
1236impl GraphSelectionSubject<'_> {
1237 fn raised_edge_groups(&self) -> Vec<Vec<EdgeIndex>> {
1238 let mut result = Vec::<Vec<EdgeIndex>>::new();
1239 let subgraph = self.raised_edge_subgraph();
1240
1241 for (_, edge_index, edge) in self.graph.underlying.iter_edges_of(&subgraph) {
1242 let loop_independent = self.graph.loop_momentum_basis.edge_signatures[edge_index]
1243 .internal
1244 .iter()
1245 .all(|sign| sign.is_zero());
1246 if edge.data.is_dummy
1247 || (self.raised_edge_policy == RaisedEdgePolicy::LoopDependentOnly
1248 && loop_independent)
1249 {
1250 continue;
1251 }
1252
1253 let group_position = result.iter().position(|group| {
1254 group.iter().all(|edge| {
1255 self.graph
1256 .loop_momentum_basis
1257 .edges_are_raised(*edge, edge_index)
1258 && self.graph[edge_index].mass == self.graph[*edge].mass
1259 })
1260 });
1261
1262 if let Some(pos) = group_position {
1263 result[pos].push(edge_index);
1264 } else {
1265 result.push(vec![edge_index]);
1266 }
1267 }
1268
1269 result.iter_mut().for_each(|group| group.sort());
1270 result
1271 }
1272
1273 fn vertex_signature(&self) -> Option<VertexSignature> {
1274 let counts = self.vertex_rule_name_counts();
1275 if counts.is_empty() {
1276 None
1277 } else {
1278 Some(VertexSignature(counts))
1279 }
1280 }
1281}
1282
1283impl GraphCycleSelectionAnalysis for GraphSelectionSubject<'_> {
1284 fn internal_simple_cycles(&self) -> Vec<Cycle> {
1285 let subgraph = self.internal_edge_subgraph();
1286 if self.graph.underlying.cyclotomatic_number(&subgraph) == 0 {
1287 return Vec::new();
1288 }
1289 let basis = self.graph.underlying.cycle_basis_of(&subgraph).0;
1290 Cycle::all_sum_powerset_filter_map(&basis, &|mut cycle| {
1291 if cycle.is_circuit(&self.graph.underlying) {
1292 cycle.loop_count = Some(1);
1293 Some(cycle)
1294 } else {
1295 None
1296 }
1297 })
1298 .map(|cycles| cycles.into_iter().collect())
1299 .unwrap_or_default()
1300 }
1301}
1302
1303fn raised_group_matches_scope(
1304 graph: &Graph,
1305 group: &[EdgeIndex],
1306 scope: RaisedPropagatorScope,
1307) -> bool {
1308 match scope {
1309 RaisedPropagatorScope::All => true,
1310 RaisedPropagatorScope::Massive => group
1311 .iter()
1312 .all(|edge| !matches!(graph[*edge].mass, EdgeMass::Zero)),
1313 RaisedPropagatorScope::Massless => group
1314 .iter()
1315 .all(|edge| matches!(graph[*edge].mass, EdgeMass::Zero)),
1316 }
1317}
1318
1319fn cycle_particles(graph: &Graph, cycle: &Cycle) -> Option<Vec<ArcParticle>> {
1320 let mut particles = BTreeMap::<EdgeIndex, ArcParticle>::new();
1321 for hedge in cycle.filter.included_iter() {
1322 let edge_id = graph.underlying[&hedge];
1323 if graph[edge_id].is_dummy {
1324 continue;
1325 }
1326 let particle = graph[edge_id].particle()?;
1327 particles.insert(edge_id, particle);
1328 }
1329 Some(particles.into_values().collect())
1330}
1331
1332fn cycle_requirement(particles: Vec<ArcParticle>) -> Option<CycleRequirement> {
1333 let matchers = particles
1334 .into_iter()
1335 .map(|particle| CycleMatcher::Pdg(particle.pdg_code.abs()))
1336 .collect::<Vec<_>>();
1337 CycleRequirement::new(matchers).ok()
1338}
1339
1340fn cycle_matches_requirement(particles: &[ArcParticle], requirement: &CycleRequirement) -> bool {
1341 if particles.is_empty() {
1342 return false;
1343 }
1344 let mut matched_requirements = vec![false; requirement.0.len()];
1345 for particle in particles {
1346 let mut particle_matched = false;
1347 for (index, matcher) in requirement.0.iter().enumerate() {
1348 if matcher.matches(particle) {
1349 matched_requirements[index] = true;
1350 particle_matched = true;
1351 }
1352 }
1353 if !particle_matched {
1354 return false;
1355 }
1356 }
1357 matched_requirements.into_iter().all(|matched| matched)
1358}
1359
1360fn matching_candidates(
1361 candidates: &[GraphGroupSelectionCandidate],
1362 predicate: impl Fn(&GraphGroupSelectionCandidate) -> bool,
1363) -> Result<BTreeSet<GroupId>> {
1364 Ok(candidates
1365 .iter()
1366 .filter(|candidate| predicate(candidate))
1367 .map(|candidate| candidate.group_id)
1368 .collect())
1369}
1370
1371fn resolve_master_graph_name(
1372 graph_name: &str,
1373 candidates: &[GraphGroupSelectionCandidate],
1374 master_name_to_group: &BTreeMap<String, GroupId>,
1375) -> Result<GroupId> {
1376 if let Some(group_id) = master_name_to_group.get(graph_name) {
1377 return Ok(*group_id);
1378 }
1379
1380 let containing_groups = candidates
1381 .iter()
1382 .filter(|candidate| candidate.graph_names.iter().any(|name| name == graph_name))
1383 .collect::<Vec<_>>();
1384 match containing_groups.as_slice() {
1385 [] => Err(eyre!("Unknown graph '{}'.", graph_name)),
1386 [candidate] => Err(eyre!(
1387 "Graph '{}' is not the master graph of its group; use '{}' instead.",
1388 graph_name,
1389 candidate.master_graph_name
1390 )),
1391 many => Err(eyre!(
1392 "Graph name '{}' is ambiguous across graph groups with masters: {}.",
1393 graph_name,
1394 many.iter()
1395 .map(|candidate| format!(
1396 "{} (group {}, master graph id {})",
1397 candidate.master_graph_name, candidate.group_id.0, candidate.master_graph_id
1398 ))
1399 .join(", ")
1400 )),
1401 }
1402}
1403
1404fn bracket_content(raw: &str, open: char, close: char) -> Result<&str> {
1405 let raw = raw.trim();
1406 if !raw.starts_with(open) || !raw.ends_with(close) {
1407 return Err(eyre!("Expected value enclosed by '{open}' and '{close}'."));
1408 }
1409 Ok(&raw[open.len_utf8()..raw.len() - close.len_utf8()])
1410}
1411
1412fn parse_comma_separated_identifiers(content: &str) -> Result<Vec<String>> {
1413 parse_comma_separated_list(content)
1414}
1415
1416fn parse_comma_separated_list(content: &str) -> Result<Vec<String>> {
1417 let trimmed = content.trim();
1418 if trimmed.is_empty() {
1419 return Ok(Vec::new());
1420 }
1421
1422 let tokens = trimmed.split(',').map(str::trim).collect::<Vec<_>>();
1423 let last_index = tokens.len().saturating_sub(1);
1424 tokens
1425 .into_iter()
1426 .enumerate()
1427 .filter_map(|(index, token)| {
1428 if token.is_empty() {
1429 if index == last_index {
1430 None
1431 } else {
1432 Some(Err(eyre!("Empty entry in comma-separated list.")))
1433 }
1434 } else {
1435 Some(Ok(token.to_string()))
1436 }
1437 })
1438 .collect()
1439}
1440
1441fn parse_cycle_requirements(content: &str, model: &Model) -> Result<Vec<CycleRequirement>> {
1442 let mut requirements = Vec::new();
1443 let mut rest = content.trim();
1444 while !rest.is_empty() {
1445 if !rest.starts_with('(') {
1446 return Err(eyre!("Expected '(' at '{}'.", rest));
1447 }
1448 let close = rest
1449 .find(')')
1450 .ok_or_else(|| eyre!("Unclosed cycle requirement in '{content}'."))?;
1451 let tuple_content = &rest[1..close];
1452 let matchers = parse_cycle_matchers(tuple_content, model)?;
1453 requirements.push(CycleRequirement::new(matchers)?);
1454 rest = rest[close + 1..].trim_start();
1455 if rest.is_empty() {
1456 break;
1457 }
1458 if !rest.starts_with(',') {
1459 return Err(eyre!(
1460 "Expected ',' after cycle requirement in '{content}'."
1461 ));
1462 }
1463 rest = rest[1..].trim_start();
1464 if rest.is_empty() {
1465 break;
1466 }
1467 }
1468 Ok(requirements)
1469}
1470
1471fn parse_cycle_matchers(content: &str, model: &Model) -> Result<Vec<CycleMatcher>> {
1472 let trimmed = content.trim();
1473 if trimmed.is_empty() {
1474 return Err(eyre!("Cycle requirements cannot be empty."));
1475 }
1476 let tokens = trimmed
1477 .split(',')
1478 .map(str::trim)
1479 .filter(|token| !token.is_empty())
1480 .map(str::to_string)
1481 .collect::<Vec<_>>();
1482 if tokens.is_empty() {
1483 return Err(eyre!("Cycle requirements cannot be empty."));
1484 }
1485 tokens
1486 .into_iter()
1487 .map(|token| parse_cycle_matcher(&token, model))
1488 .collect()
1489}
1490
1491fn parse_cycle_matcher(token: &str, model: &Model) -> Result<CycleMatcher> {
1492 match token {
1493 "fermion" => return Ok(CycleMatcher::Fermion),
1494 "ghost" => return Ok(CycleMatcher::Ghost),
1495 "goldstone" => return Ok(CycleMatcher::Goldstone),
1496 _ => {}
1497 }
1498 if let Ok(pdg) = token.parse::<isize>() {
1499 if pdg < 0 {
1500 return Err(eyre!(
1501 "Cycle signatures use particle PDGs only; specify {} instead of anti-particle PDG {}.",
1502 pdg.abs(),
1503 pdg
1504 ));
1505 }
1506 model.try_get_particle_from_pdg(pdg)?;
1507 return Ok(CycleMatcher::Pdg(pdg));
1508 }
1509
1510 let particle = model.try_get_particle(token)?;
1511 if particle.pdg_code < 0 || particle.name.as_str() != token {
1512 return Err(eyre!(
1513 "Cycle signatures use particles only; specify '{}' instead of anti-particle '{}'.",
1514 particle.get_anti_particle(model).name,
1515 token
1516 ));
1517 }
1518 Ok(CycleMatcher::Pdg(particle.pdg_code))
1519}
1520
1521fn parse_particle_signature_pdg(token: &str, model: &Model) -> Result<isize> {
1522 if let Ok(pdg) = token.parse::<isize>() {
1523 if pdg == 0 {
1524 return Err(eyre!("Particle signatures do not accept PDG 0."));
1525 }
1526 let abs_pdg = pdg.abs();
1527 model
1528 .try_get_particle_from_pdg(abs_pdg)
1529 .or_else(|_| model.try_get_particle_from_pdg(-abs_pdg))?;
1530 return Ok(abs_pdg);
1531 }
1532
1533 let particle = model.try_get_particle(token)?;
1534 Ok(particle.pdg_code.abs())
1535}
1536
1537#[cfg(test)]
1538mod tests {
1539 use super::*;
1540 use std::sync::{Arc, OnceLock};
1541
1542 use crate::{
1543 dot,
1544 graph::{
1545 Graph, LoopMomentumBasis,
1546 edge::EdgeMass,
1547 parse::{IntoGraph, complete_group_parsing},
1548 },
1549 initialisation::test_initialise,
1550 model::{ArcVertexRule, ColorStructure, ParameterName, Particle, UFOSymbol, VertexRule},
1551 momentum::signature::LoopExtSignature,
1552 };
1553
1554 fn scalar_model() -> &'static Model {
1555 static MODEL: OnceLock<Model> = OnceLock::new();
1556 MODEL.get_or_init(|| {
1557 test_initialise().expect("test initialization should succeed");
1558 crate::utils::load_generic_model("scalars")
1559 })
1560 }
1561
1562 fn minimal_model() -> Model {
1563 let mut model = Model::default();
1564 for particle in [
1565 test_particle(3, "s", "s~", 2, 3, false, 0),
1566 test_particle(-3, "s~", "s", 2, -3, false, 0),
1567 test_particle(4, "c", "c~", 2, 3, false, 0),
1568 test_particle(11, "e-", "e+", 2, 1, false, 0),
1569 test_particle(-11, "e+", "e-", 2, 1, false, 0),
1570 test_particle(21, "g", "g", 3, 8, false, 0),
1571 test_particle(22, "a", "a", 3, 1, false, 0),
1572 test_particle(25, "h", "h", 1, 1, true, 0),
1573 test_particle(82, "ghG", "ghG~", 1, 8, false, 1),
1574 ] {
1575 model
1576 .particle_pdg_to_position
1577 .insert(particle.pdg_code, model.particles.len());
1578 model
1579 .particle_name_to_position
1580 .insert(particle.name.clone(), model.particles.len());
1581 model.particles.push(ArcParticle(Arc::new(particle)));
1582 }
1583 model
1584 }
1585
1586 fn test_particle(
1587 pdg_code: isize,
1588 name: &str,
1589 antiname: &str,
1590 spin: isize,
1591 color: isize,
1592 goldstone: bool,
1593 ghost_number: isize,
1594 ) -> Particle {
1595 Particle {
1596 pdg_code,
1597 name: name.into(),
1598 antiname: antiname.into(),
1599 spin,
1600 color,
1601 mass: ParameterName(UFOSymbol::zero()),
1602 width: ParameterName(UFOSymbol::zero()),
1603 texname: name.into(),
1604 antitexname: antiname.into(),
1605 charge: 0.into(),
1606 ghost_number,
1607 lepton_number: 0,
1608 y_charge: None,
1609 y_charge_right: None,
1610 goldstone,
1611 chemical_potential: None,
1612 }
1613 }
1614
1615 fn raised_test_graph() -> Result<Graph> {
1616 let mut graph: Graph = dot!(
1617 digraph raised {
1618 edge [particle=scalar_1]
1619 ext [style=invis]
1620 ext -> A [id=3]
1621 B -> ext [id=4]
1622 A -> B [id=0]
1623 A -> B [id=1]
1624 A -> B [id=2]
1625 A -> B [id=5]
1626 A -> B [id=6]
1627 },
1628 scalar_model()
1629 )?;
1630
1631 graph.loop_momentum_basis = LoopMomentumBasis {
1632 tree: graph.underlying.empty_subgraph(),
1633 loop_edges: vec![
1634 EdgeIndex::from(0),
1635 EdgeIndex::from(1),
1636 EdgeIndex::from(2),
1637 EdgeIndex::from(5),
1638 EdgeIndex::from(6),
1639 ]
1640 .into(),
1641 ext_edges: vec![EdgeIndex::from(3), EdgeIndex::from(4)].into(),
1642 edge_signatures: graph
1643 .underlying
1644 .new_edgevec(|_, edge_id, _| match edge_id.0 {
1645 0 => LoopExtSignature::from((vec![1], vec![])),
1646 1 => LoopExtSignature::from((vec![-1], vec![])),
1647 2 => LoopExtSignature::from((vec![0], vec![])),
1648 5 => LoopExtSignature::from((vec![1], vec![])),
1649 6 => LoopExtSignature::from((vec![-1], vec![])),
1650 _ => LoopExtSignature::from((vec![0], vec![])),
1651 }),
1652 };
1653
1654 for edge_id in [0, 1, 2] {
1655 graph.underlying[EdgeIndex::from(edge_id)].mass = EdgeMass::Zero;
1656 }
1657 Ok(graph)
1658 }
1659
1660 fn tree_raised_test_graph() -> Result<Graph> {
1661 let mut graph: Graph = dot!(
1662 digraph tree_raised {
1663 edge [particle=scalar_1]
1664 ext [style=invis]
1665 ext -> A [id=3]
1666 B -> ext [id=4]
1667 A -> B [id=0]
1668 A -> B [id=1]
1669 A -> B [id=2]
1670 },
1671 scalar_model()
1672 )?;
1673
1674 graph.loop_momentum_basis = LoopMomentumBasis {
1675 tree: graph.underlying.empty_subgraph(),
1676 loop_edges: vec![EdgeIndex::from(2)].into(),
1677 ext_edges: vec![EdgeIndex::from(3), EdgeIndex::from(4)].into(),
1678 edge_signatures: graph
1679 .underlying
1680 .new_edgevec(|_, edge_id, _| match edge_id.0 {
1681 0 => LoopExtSignature::from((vec![0], vec![1])),
1682 1 => LoopExtSignature::from((vec![0], vec![-1])),
1683 2 => LoopExtSignature::from((vec![1], vec![])),
1684 _ => LoopExtSignature::from((vec![0], vec![])),
1685 }),
1686 };
1687
1688 Ok(graph)
1689 }
1690
1691 fn cycle_test_graph() -> Result<Graph> {
1692 dot!(
1693 digraph cycle_test {
1694 node [num="1"]
1695 A -> B [particle=scalar_0, id=0]
1696 B -> C [particle=scalar_1, id=1]
1697 C -> A [particle=scalar_0, id=2]
1698 B -> D [particle=scalar_2, id=3]
1699 D -> C [particle=scalar_2, id=4]
1700 },
1701 scalar_model()
1702 )
1703 }
1704
1705 fn vertex_rule(name: &str) -> ArcVertexRule {
1706 ArcVertexRule(Arc::new(VertexRule {
1707 name: name.into(),
1708 couplings: Vec::new(),
1709 lorentz_structures: Vec::new(),
1710 particles: Vec::new(),
1711 color_structures: ColorStructure {
1712 color_structure: Vec::new(),
1713 },
1714 dod: 0,
1715 }))
1716 }
1717
1718 fn graph_with_vertex_rules(name: &str, vertex_names: &[&str]) -> Result<Graph> {
1719 let mut graph: Graph = "digraph vertex_test {
1720 node [num=\"1\"]
1721 edge [particle=scalar_1]
1722 A -> B [id=0]
1723 B -> C [id=1]
1724 C -> A [id=2]
1725 }"
1726 .into_graph(scalar_model())?;
1727 graph.name = name.to_string();
1728 let vertex_rules = vertex_names
1729 .iter()
1730 .map(|name| vertex_rule(name))
1731 .collect::<Vec<_>>();
1732 for ((_, _, vertex), vertex_rule) in graph
1733 .underlying
1734 .iter_nodes_mut()
1735 .zip(vertex_rules.into_iter().cycle())
1736 {
1737 vertex.vertex_rule = Some(vertex_rule);
1738 }
1739 Ok(graph)
1740 }
1741
1742 fn particle_test_graph(name: &str) -> Result<Graph> {
1743 let mut graph: Graph = "digraph particle_test {
1744 node [num=\"1\"]
1745 A -> B [particle=scalar_0, id=0]
1746 B -> C [particle=scalar_1, id=1]
1747 C -> A [particle=scalar_2, id=2]
1748 }"
1749 .into_graph(scalar_model())?;
1750 graph.name = name.to_string();
1751 Ok(graph)
1752 }
1753
1754 fn subgraph_from_edge_ids(graph: &Graph, edge_ids: &[usize]) -> SuBitGraph {
1755 let wanted = edge_ids.iter().copied().collect::<BTreeSet<_>>();
1756 let mut subgraph: SuBitGraph = graph.underlying.empty_subgraph();
1757 for (pair, edge_id, _) in graph.underlying.iter_edges() {
1758 if wanted.contains(&edge_id.0) {
1759 subgraph.add(pair);
1760 }
1761 }
1762 subgraph
1763 }
1764
1765 fn vertex_test_subjects_by_graph_id(
1766 graph_id: usize,
1767 graph: &Graph,
1768 ) -> Result<Vec<GraphSelectionSubject<'_>>> {
1769 let edge_ids = if graph_id == 0 { &[0][..] } else { &[1][..] };
1770 Ok(vec![GraphSelectionSubject::subgraph(
1771 graph,
1772 subgraph_from_edge_ids(graph, edge_ids),
1773 )])
1774 }
1775
1776 fn raised_cut_test_subjects_by_graph_id(
1777 graph_id: usize,
1778 graph: &Graph,
1779 ) -> Result<Vec<GraphCutSelectionSubject<'_>>> {
1780 let edge_ids = if graph_id == 0 { &[0][..] } else { &[2][..] };
1781 Ok(vec![GraphCutSelectionSubject::new(
1782 graph,
1783 subgraph_from_edge_ids(graph, edge_ids),
1784 )])
1785 }
1786
1787 fn raised_any_test_subjects_by_graph_id(
1788 graph_id: usize,
1789 graph: &Graph,
1790 ) -> Result<Vec<GraphSelectionSubject<'_>>> {
1791 let subject = if graph_id == 0 {
1792 GraphSelectionSubject::whole_graph(graph)
1793 } else {
1794 GraphSelectionSubject::subgraph(graph, subgraph_from_edge_ids(graph, &[0, 2]))
1795 };
1796 Ok(vec![subject])
1797 }
1798
1799 #[test]
1800 fn raised_signature_parses_and_canonicalizes() {
1801 assert_eq!(
1802 "[2,3,4]",
1803 RaisedPropagatorSignature::from_str("[3,2,4]")
1804 .unwrap()
1805 .canonical()
1806 );
1807 assert_eq!(
1808 "[]",
1809 RaisedPropagatorSignature::from_str("[]")
1810 .unwrap()
1811 .canonical()
1812 );
1813 assert_eq!(
1814 "[2]",
1815 RaisedPropagatorSignature::from_str("[2,]")
1816 .unwrap()
1817 .canonical()
1818 );
1819 assert_eq!(
1820 "ANY_RAISING",
1821 RaisedPropagatorSignature::from_str("ANY_RAISING")
1822 .unwrap()
1823 .canonical()
1824 );
1825 assert!(RaisedPropagatorSignature::from_str("[1]").is_err());
1826 }
1827
1828 #[test]
1829 fn any_raising_matches_any_non_empty_raised_signature() -> Result<()> {
1830 let any = RaisedPropagatorSignature::from_str("ANY_RAISING")?;
1831 let none = RaisedPropagatorSignature::from_str("[]")?;
1832 let raised = RaisedPropagatorSignature::from_str("[2]")?;
1833
1834 assert!(any.matches(&raised));
1835 assert!(!any.matches(&none));
1836 assert!(raised.matches(&raised));
1837 assert!(!raised.matches(&RaisedPropagatorSignature::from_str("[3]")?));
1838
1839 Ok(())
1840 }
1841
1842 #[test]
1843 fn raised_signature_groups_up_to_sign_and_splits_by_mass_scope() -> Result<()> {
1844 let graph = raised_test_graph()?;
1845
1846 assert_eq!(
1847 "[2,2]",
1848 graph
1849 .raised_propagator_signature(RaisedPropagatorScope::All)
1850 .canonical()
1851 );
1852 assert_eq!(
1853 "[2]",
1854 graph
1855 .raised_propagator_signature(RaisedPropagatorScope::Massless)
1856 .canonical()
1857 );
1858 assert_eq!(
1859 "[2]",
1860 graph
1861 .raised_propagator_signature(RaisedPropagatorScope::Massive)
1862 .canonical()
1863 );
1864
1865 Ok(())
1866 }
1867
1868 #[test]
1869 fn subgraph_raised_signature_only_uses_edges_in_view() -> Result<()> {
1870 let graph = raised_test_graph()?;
1871 let repeated_side =
1872 GraphSelectionSubject::subgraph(&graph, subgraph_from_edge_ids(&graph, &[0, 1, 2]));
1873 let cut_edge_removed_side =
1874 GraphSelectionSubject::subgraph(&graph, subgraph_from_edge_ids(&graph, &[0, 2]));
1875
1876 assert_eq!(
1877 "[2]",
1878 repeated_side
1879 .raised_propagator_signature(RaisedPropagatorScope::All)
1880 .canonical()
1881 );
1882 assert_eq!(
1883 "[]",
1884 cut_edge_removed_side
1885 .raised_propagator_signature(RaisedPropagatorScope::All)
1886 .canonical()
1887 );
1888
1889 Ok(())
1890 }
1891
1892 #[test]
1893 fn cut_side_amplitude_raised_signature_includes_tree_like_internal_edges() -> Result<()> {
1894 let graph = tree_raised_test_graph()?;
1895 let tree_raised_edges = subgraph_from_edge_ids(&graph, &[0, 1]);
1896 let normal_subject = GraphSelectionSubject::subgraph(&graph, tree_raised_edges.clone());
1897 let cut_side_subject =
1898 GraphSelectionSubject::cut_side_amplitude_subgraph(&graph, tree_raised_edges.clone());
1899
1900 assert_eq!(
1901 "[]",
1902 normal_subject
1903 .raised_propagator_signature(RaisedPropagatorScope::All)
1904 .canonical()
1905 );
1906 assert_eq!(
1907 "[2]",
1908 cut_side_subject
1909 .raised_propagator_signature(RaisedPropagatorScope::All)
1910 .canonical()
1911 );
1912 assert_eq!(
1913 "[]",
1914 GraphCutSelectionSubject::new(&graph, tree_raised_edges)
1915 .raised_cut_signature(RaisedPropagatorScope::All)
1916 .canonical()
1917 );
1918
1919 Ok(())
1920 }
1921
1922 #[test]
1923 fn raised_cut_signature_counts_touched_raised_groups_once() -> Result<()> {
1924 let graph = raised_test_graph()?;
1925
1926 assert_eq!(
1927 "[2]",
1928 GraphCutSelectionSubject::new(&graph, subgraph_from_edge_ids(&graph, &[0]))
1929 .raised_cut_signature(RaisedPropagatorScope::All)
1930 .canonical()
1931 );
1932 assert_eq!(
1933 "[2]",
1934 GraphCutSelectionSubject::new(&graph, subgraph_from_edge_ids(&graph, &[0, 1]))
1935 .raised_cut_signature(RaisedPropagatorScope::All)
1936 .canonical()
1937 );
1938 assert_eq!(
1939 "[2,2]",
1940 GraphCutSelectionSubject::new(&graph, subgraph_from_edge_ids(&graph, &[0, 5]))
1941 .raised_cut_signature(RaisedPropagatorScope::All)
1942 .canonical()
1943 );
1944 assert_eq!(
1945 "[]",
1946 GraphCutSelectionSubject::new(&graph, subgraph_from_edge_ids(&graph, &[2]))
1947 .raised_cut_signature(RaisedPropagatorScope::All)
1948 .canonical()
1949 );
1950
1951 Ok(())
1952 }
1953
1954 #[test]
1955 fn raised_cut_signature_respects_mass_scope() -> Result<()> {
1956 let graph = raised_test_graph()?;
1957 let cut_subject =
1958 GraphCutSelectionSubject::new(&graph, subgraph_from_edge_ids(&graph, &[0, 5]));
1959
1960 assert_eq!(
1961 "[2,2]",
1962 cut_subject
1963 .raised_cut_signature(RaisedPropagatorScope::All)
1964 .canonical()
1965 );
1966 assert_eq!(
1967 "[2]",
1968 cut_subject
1969 .raised_cut_signature(RaisedPropagatorScope::Massless)
1970 .canonical()
1971 );
1972 assert_eq!(
1973 "[2]",
1974 cut_subject
1975 .raised_cut_signature(RaisedPropagatorScope::Massive)
1976 .canonical()
1977 );
1978
1979 Ok(())
1980 }
1981
1982 #[test]
1983 fn vertex_signature_preserves_multiplicity() {
1984 let signature = VertexSignature::parse("[V_9,V_6,V_9]").unwrap();
1985 assert_eq!(signature.0.get("V_9"), Some(&2));
1986 assert_eq!(signature.0.get("V_6"), Some(&1));
1987 assert_eq!(signature.canonical(), "[V_6,V_9,V_9]");
1988 assert_eq!(
1989 VertexSignature::parse("[V_1,]").unwrap().canonical(),
1990 "[V_1]"
1991 );
1992 assert!(VertexSignature::parse("[]").is_err());
1993 }
1994
1995 #[test]
1996 fn particle_signature_parses_names_pdgs_antiparticles_and_tuples() {
1997 let model = minimal_model();
1998 assert_eq!(
1999 "[11,21]",
2000 ParticleSignature::parse("[e+,g]", &model)
2001 .unwrap()
2002 .canonical()
2003 );
2004 assert_eq!(
2005 "[11,21]",
2006 ParticleSignature::parse("(e-,-21,)", &model)
2007 .unwrap()
2008 .canonical()
2009 );
2010 assert!(ParticleSignature::parse("[]", &model).is_err());
2011 assert!(ParticleSignature::parse("[0]", &model).is_err());
2012 }
2013
2014 #[test]
2015 fn cycle_signature_parses_and_canonicalizes() {
2016 let model = minimal_model();
2017 assert_eq!(
2018 "[(3)]",
2019 CycleSignature::parse("[(3)]", &model).unwrap().canonical()
2020 );
2021 assert_eq!(
2022 "[(3)]",
2023 CycleSignature::parse("[(3,),]", &model)
2024 .unwrap()
2025 .canonical()
2026 );
2027 assert_eq!(
2028 "[(3,21),(4,22)]",
2029 CycleSignature::parse("[(3,21), (4,22)]", &model)
2030 .unwrap()
2031 .canonical()
2032 );
2033 assert_eq!(
2034 "[(fermion,ghost,goldstone)]",
2035 CycleSignature::parse("[(fermion,ghost,goldstone)]", &model)
2036 .unwrap()
2037 .canonical()
2038 );
2039 }
2040
2041 #[test]
2042 fn cycle_requirements_include_simple_cycles_beyond_basis() -> Result<()> {
2043 let graph = cycle_test_graph()?;
2044 let scalar_0 = scalar_model().try_get_particle("scalar_0")?.pdg_code.abs();
2045 let scalar_1 = scalar_model().try_get_particle("scalar_1")?.pdg_code.abs();
2046 let scalar_2 = scalar_model().try_get_particle("scalar_2")?.pdg_code.abs();
2047 let requirements = graph
2048 .cycle_requirements()
2049 .into_iter()
2050 .map(|requirement| requirement.to_string())
2051 .collect::<BTreeSet<_>>();
2052
2053 let basis_cycle_a = format!("({},{})", scalar_0.min(scalar_1), scalar_0.max(scalar_1));
2054 let basis_cycle_b = format!("({},{})", scalar_1.min(scalar_2), scalar_1.max(scalar_2));
2055 let combined_cycle = format!("({},{})", scalar_0.min(scalar_2), scalar_0.max(scalar_2));
2056
2057 assert!(
2058 requirements.contains(&basis_cycle_a),
2059 "missing first basis cycle in {requirements:?}"
2060 );
2061 assert!(
2062 requirements.contains(&basis_cycle_b),
2063 "missing second basis cycle in {requirements:?}"
2064 );
2065 assert!(
2066 requirements.contains(&combined_cycle),
2067 "missing combined simple cycle in {requirements:?}"
2068 );
2069
2070 Ok(())
2071 }
2072
2073 #[test]
2074 fn subgraph_cycles_are_broken_by_removed_edges() -> Result<()> {
2075 let graph = cycle_test_graph()?;
2076 let full_subject = GraphSelectionSubject::whole_graph(&graph);
2077 let cut_side_subject =
2078 GraphSelectionSubject::subgraph(&graph, subgraph_from_edge_ids(&graph, &[0, 1]));
2079
2080 assert!(!full_subject.cycle_requirements().is_empty());
2081 assert!(cut_side_subject.cycle_requirements().is_empty());
2082
2083 Ok(())
2084 }
2085
2086 #[test]
2087 fn cycle_signature_rejects_antiparticles() {
2088 let model = minimal_model();
2089 assert!(CycleSignature::parse("[(-3)]", &model).is_err());
2090 assert!(CycleSignature::parse("[(e+)]", &model).is_err());
2091 }
2092
2093 #[test]
2094 fn cycle_category_requirement_matches_particles_directly() {
2095 let model = minimal_model();
2096 let gluon = model.try_get_particle_from_pdg(21).unwrap();
2097 let strange = model.try_get_particle_from_pdg(3).unwrap();
2098 let requirement =
2099 CycleRequirement::new(vec![CycleMatcher::Pdg(3), CycleMatcher::Pdg(21)]).unwrap();
2100 assert!(cycle_matches_requirement(
2101 &[strange.clone(), gluon.clone()],
2102 &requirement
2103 ));
2104 let fermion_requirement = CycleRequirement::new(vec![CycleMatcher::Fermion]).unwrap();
2105 assert!(cycle_matches_requirement(&[strange], &fermion_requirement));
2106 assert!(!cycle_matches_requirement(&[gluon], &fermion_requirement));
2107 }
2108
2109 #[test]
2110 fn subgraph_vertex_signature_counts_only_vertices_in_view() -> Result<()> {
2111 let graph = graph_with_vertex_rules("g0", &["V_A", "V_B", "V_C"])?;
2112 let subject = GraphSelectionSubject::subgraph(&graph, subgraph_from_edge_ids(&graph, &[0]));
2113 let counts = subject.vertex_rule_name_counts();
2114
2115 assert_eq!(counts.values().copied().sum::<usize>(), 2);
2116 assert_eq!(
2117 GraphSelectionSubject::whole_graph(&graph)
2118 .vertex_rule_name_counts()
2119 .values()
2120 .copied()
2121 .sum::<usize>(),
2122 3
2123 );
2124
2125 Ok(())
2126 }
2127
2128 #[test]
2129 fn subgraph_particle_signature_counts_only_particles_in_view() -> Result<()> {
2130 let graph = particle_test_graph("g0")?;
2131 let subject = GraphSelectionSubject::subgraph(&graph, subgraph_from_edge_ids(&graph, &[0]));
2132 let scalar_0 = scalar_model().try_get_particle("scalar_0")?.pdg_code.abs();
2133 let scalar_1 = scalar_model().try_get_particle("scalar_1")?.pdg_code.abs();
2134
2135 assert!(subject.particle_pdgs().contains(&scalar_0));
2136 assert!(!subject.particle_pdgs().contains(&scalar_1));
2137 assert!(
2138 GraphSelectionSubject::whole_graph(&graph)
2139 .particle_pdgs()
2140 .contains(&scalar_1)
2141 );
2142
2143 Ok(())
2144 }
2145
2146 #[test]
2147 fn selection_spec_combines_rules_with_and_and_respects_vertex_multiplicity() -> Result<()> {
2148 let mut graphs = vec![
2149 graph_with_vertex_rules("g0", &["V_A", "V_A", "V_B"])?,
2150 graph_with_vertex_rules("g1", &["V_A", "V_B", "V_C"])?,
2151 graph_with_vertex_rules("g2", &["V_A", "V_A", "V_C"])?,
2152 ];
2153 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2154
2155 let spec = GraphGroupSelectionSpec::new()
2156 .with_vertex_signatures(
2157 SelectionPolarity::With,
2158 vec![VertexSignature::parse("[V_A,V_A]")?],
2159 )
2160 .with_vertex_signatures(
2161 SelectionPolarity::Without,
2162 vec![VertexSignature::parse("[V_C]")?],
2163 );
2164 let plan = spec.plan(&graph_group_structure, |graph_id| graphs.get(graph_id))?;
2165
2166 assert_eq!(plan.retained_group_ids(), &[GroupId(0)]);
2167 assert_eq!(plan.report().kept_master_graphs, vec!["g0".to_string()]);
2168 assert_eq!(
2169 plan.report().removed_master_graphs,
2170 vec!["g1".to_string(), "g2".to_string()]
2171 );
2172
2173 Ok(())
2174 }
2175
2176 #[test]
2177 fn graph_name_selection_supports_without_polarity() -> Result<()> {
2178 let mut graphs = vec![
2179 graph_with_vertex_rules("g0", &["V_A"])?,
2180 graph_with_vertex_rules("g1", &["V_B"])?,
2181 graph_with_vertex_rules("g2", &["V_C"])?,
2182 ];
2183 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2184
2185 let spec = GraphGroupSelectionSpec::new()
2186 .with_master_graph_names_polarity(SelectionPolarity::Without, vec!["g1".to_string()]);
2187 let plan = spec.plan(&graph_group_structure, |graph_id| graphs.get(graph_id))?;
2188
2189 assert_eq!(plan.report().kept_master_graphs, vec!["g0", "g2"]);
2190 assert_eq!(plan.report().removed_master_graphs, vec!["g1"]);
2191
2192 Ok(())
2193 }
2194
2195 #[test]
2196 fn master_graph_name_selection_rejects_members_and_preserves_group_order() -> Result<()> {
2197 let mut graphs = vec![
2198 graph_with_vertex_rules("master_0", &["V_A"])?,
2199 graph_with_vertex_rules("member_0", &["V_A"])?,
2200 graph_with_vertex_rules("master_1", &["V_B"])?,
2201 ];
2202 graphs[0].group_id = Some(GroupId(0));
2203 graphs[0].is_group_master = true;
2204 graphs[1].group_id = Some(GroupId(0));
2205 graphs[1].is_group_master = false;
2206 graphs[2].group_id = Some(GroupId(1));
2207 graphs[2].is_group_master = true;
2208 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2209
2210 let member_error =
2211 GraphGroupSelectionSpec::from_master_graph_names(vec!["member_0".to_string()])
2212 .plan(&graph_group_structure, |graph_id| graphs.get(graph_id))
2213 .unwrap_err();
2214 assert!(
2215 member_error.chain().any(|cause| cause
2216 .to_string()
2217 .contains("is not the master graph of its group; use 'master_0' instead")),
2218 "{member_error:?}"
2219 );
2220
2221 let plan = GraphGroupSelectionSpec::from_master_graph_names(vec![
2222 "master_1".to_string(),
2223 "master_0".to_string(),
2224 ])
2225 .plan(&graph_group_structure, |graph_id| graphs.get(graph_id))?;
2226 assert_eq!(plan.retained_group_ids(), &[GroupId(0), GroupId(1)]);
2227 assert_eq!(plan.report().kept_master_graphs, ["master_0", "master_1"]);
2228
2229 Ok(())
2230 }
2231
2232 #[test]
2233 fn with_graph_names_are_authoritative_over_vetoes() -> Result<()> {
2234 let mut graphs = vec![
2235 graph_with_vertex_rules("g0", &["V_A"])?,
2236 graph_with_vertex_rules("g1", &["V_B"])?,
2237 ];
2238 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2239
2240 let spec = GraphGroupSelectionSpec::new()
2241 .with_master_graph_names(vec!["g1".to_string()])
2242 .with_vertex_signatures(
2243 SelectionPolarity::Without,
2244 vec![VertexSignature::parse("[V_B]")?],
2245 );
2246 let plan = spec.plan(&graph_group_structure, |graph_id| graphs.get(graph_id))?;
2247
2248 assert_eq!(plan.report().kept_master_graphs, vec!["g0", "g1"]);
2249 assert!(plan.report().removed_master_graphs.is_empty());
2250
2251 Ok(())
2252 }
2253
2254 #[test]
2255 fn graph_name_selection_rejects_conflicting_constraints() -> Result<()> {
2256 let mut graphs = vec![
2257 graph_with_vertex_rules("g0", &["V_A"])?,
2258 graph_with_vertex_rules("g1", &["V_B"])?,
2259 ];
2260 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2261
2262 let spec = GraphGroupSelectionSpec::new()
2263 .with_master_graph_names(vec!["g1".to_string()])
2264 .with_master_graph_names_polarity(SelectionPolarity::Without, vec!["g1".to_string()]);
2265 let err = spec
2266 .plan(&graph_group_structure, |graph_id| graphs.get(graph_id))
2267 .unwrap_err();
2268 let message = format!("{err}");
2269
2270 assert!(message.contains("Contradictory graph-name selection"));
2271 assert!(message.contains("g1"));
2272 assert!(message.contains("--with-graph-names"));
2273 assert!(message.contains("--without-graph-names"));
2274
2275 Ok(())
2276 }
2277
2278 #[test]
2279 fn particle_selection_matches_required_sets() -> Result<()> {
2280 let mut graphs = vec![
2281 particle_test_graph("g0")?,
2282 graph_with_vertex_rules("g1", &["V_A"])?,
2283 ];
2284 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2285 let scalar_0 = scalar_model()
2286 .try_get_particle("scalar_0")?
2287 .name
2288 .to_string();
2289 let scalar_1 = scalar_model()
2290 .try_get_particle("scalar_1")?
2291 .name
2292 .to_string();
2293 let scalar_2 = scalar_model()
2294 .try_get_particle("scalar_2")?
2295 .name
2296 .to_string();
2297
2298 let spec = GraphGroupSelectionSpec::new()
2299 .with_particle_signatures(
2300 SelectionPolarity::With,
2301 vec![ParticleSignature::parse(
2302 &format!("[{scalar_0},{scalar_1}]"),
2303 scalar_model(),
2304 )?],
2305 )
2306 .with_particle_signatures(
2307 SelectionPolarity::Without,
2308 vec![ParticleSignature::parse(
2309 &format!("[{scalar_2}]"),
2310 scalar_model(),
2311 )?],
2312 );
2313
2314 let err = spec
2315 .plan(&graph_group_structure, |graph_id| graphs.get(graph_id))
2316 .unwrap_err();
2317 assert!(
2318 err.to_string()
2319 .contains("Graph-group selection would remove all graph groups"),
2320 "{err:?}"
2321 );
2322
2323 let spec = GraphGroupSelectionSpec::new().with_particle_signatures(
2324 SelectionPolarity::With,
2325 vec![ParticleSignature::parse(
2326 &format!("({scalar_0},{scalar_1})"),
2327 scalar_model(),
2328 )?],
2329 );
2330 let plan = spec.plan(&graph_group_structure, |graph_id| graphs.get(graph_id))?;
2331 assert_eq!(plan.report().kept_master_graphs, vec!["g0"]);
2332
2333 Ok(())
2334 }
2335
2336 #[test]
2337 fn structural_selection_matches_any_analysis_subject_and_without_vetoes() -> Result<()> {
2338 let mut graphs = vec![
2339 graph_with_vertex_rules("g0", &["V_A", "V_B", "V_C"])?,
2340 graph_with_vertex_rules("g1", &["V_D", "V_E", "V_F"])?,
2341 ];
2342 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2343 let with_spec = GraphGroupSelectionSpec::new()
2344 .with_master_graph_names(vec!["g0".to_string(), "g1".to_string()])
2345 .with_vertex_signatures(
2346 SelectionPolarity::With,
2347 vec![VertexSignature::parse("[V_A]")?],
2348 );
2349 let without_spec = GraphGroupSelectionSpec::new().with_vertex_signatures(
2350 SelectionPolarity::Without,
2351 vec![VertexSignature::parse("[V_A]")?],
2352 );
2353
2354 let with_plan = with_spec.plan_with_analysis_contexts(
2355 &graph_group_structure,
2356 |graph_id| graphs.get(graph_id),
2357 vertex_test_subjects_by_graph_id,
2358 |_graph_id, _graph| Ok(Vec::new()),
2359 "no subjects",
2360 "no cuts",
2361 )?;
2362 assert_eq!(with_plan.report().kept_master_graphs, vec!["g0", "g1"]);
2363
2364 let without_plan = without_spec.plan_with_analysis_contexts(
2365 &graph_group_structure,
2366 |graph_id| graphs.get(graph_id),
2367 vertex_test_subjects_by_graph_id,
2368 |_graph_id, _graph| Ok(Vec::new()),
2369 "no subjects",
2370 "no cuts",
2371 )?;
2372 assert_eq!(without_plan.report().kept_master_graphs, vec!["g1"]);
2373
2374 Ok(())
2375 }
2376
2377 #[test]
2378 fn raised_propagator_selection_supports_any_raising_wildcard() -> Result<()> {
2379 let mut graphs = vec![raised_test_graph()?, raised_test_graph()?];
2380 graphs[0].name = "g0".to_string();
2381 graphs[1].name = "g1".to_string();
2382 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2383 let any = RaisedPropagatorSignature::from_str("ANY_RAISING")?;
2384 let with_spec = GraphGroupSelectionSpec::new().with_raised_propagator_signatures(
2385 SelectionPolarity::With,
2386 RaisedPropagatorScope::All,
2387 vec![any.clone()],
2388 );
2389 let without_spec = GraphGroupSelectionSpec::new().with_raised_propagator_signatures(
2390 SelectionPolarity::Without,
2391 RaisedPropagatorScope::All,
2392 vec![any],
2393 );
2394
2395 let with_plan = with_spec.plan_with_analysis_contexts(
2396 &graph_group_structure,
2397 |graph_id| graphs.get(graph_id),
2398 raised_any_test_subjects_by_graph_id,
2399 |_graph_id, _graph| Ok(Vec::new()),
2400 "no graph subjects",
2401 "no cuts",
2402 )?;
2403 assert_eq!(with_plan.report().kept_master_graphs, vec!["g0"]);
2404
2405 let without_plan = without_spec.plan_with_analysis_contexts(
2406 &graph_group_structure,
2407 |graph_id| graphs.get(graph_id),
2408 raised_any_test_subjects_by_graph_id,
2409 |_graph_id, _graph| Ok(Vec::new()),
2410 "no graph subjects",
2411 "no cuts",
2412 )?;
2413 assert_eq!(without_plan.report().kept_master_graphs, vec!["g1"]);
2414
2415 Ok(())
2416 }
2417
2418 #[test]
2419 fn raised_cut_selection_matches_any_cut_and_without_vetoes() -> Result<()> {
2420 let mut graphs = vec![raised_test_graph()?, raised_test_graph()?];
2421 graphs[0].name = "g0".to_string();
2422 graphs[1].name = "g1".to_string();
2423 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2424 let with_spec = GraphGroupSelectionSpec::new().with_raised_cut_signatures(
2425 SelectionPolarity::With,
2426 RaisedPropagatorScope::All,
2427 vec![RaisedPropagatorSignature::from_str("[2]")?],
2428 );
2429 let without_spec = GraphGroupSelectionSpec::new().with_raised_cut_signatures(
2430 SelectionPolarity::Without,
2431 RaisedPropagatorScope::All,
2432 vec![RaisedPropagatorSignature::from_str("[2]")?],
2433 );
2434
2435 let with_plan = with_spec.plan_with_analysis_contexts(
2436 &graph_group_structure,
2437 |graph_id| graphs.get(graph_id),
2438 |_graph_id, graph| Ok(vec![GraphSelectionSubject::whole_graph(graph)]),
2439 raised_cut_test_subjects_by_graph_id,
2440 "no graph subjects",
2441 "no cuts",
2442 )?;
2443 assert_eq!(with_plan.report().kept_master_graphs, vec!["g0"]);
2444
2445 let without_plan = without_spec.plan_with_analysis_contexts(
2446 &graph_group_structure,
2447 |graph_id| graphs.get(graph_id),
2448 |_graph_id, graph| Ok(vec![GraphSelectionSubject::whole_graph(graph)]),
2449 raised_cut_test_subjects_by_graph_id,
2450 "no graph subjects",
2451 "no cuts",
2452 )?;
2453 assert_eq!(without_plan.report().kept_master_graphs, vec!["g1"]);
2454
2455 Ok(())
2456 }
2457
2458 #[test]
2459 fn raised_cut_selection_supports_any_raising_wildcard() -> Result<()> {
2460 let mut graphs = vec![raised_test_graph()?, raised_test_graph()?];
2461 graphs[0].name = "g0".to_string();
2462 graphs[1].name = "g1".to_string();
2463 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2464 let any = RaisedPropagatorSignature::from_str("ANY_RAISING")?;
2465 let with_spec = GraphGroupSelectionSpec::new().with_raised_cut_signatures(
2466 SelectionPolarity::With,
2467 RaisedPropagatorScope::All,
2468 vec![any.clone()],
2469 );
2470 let without_spec = GraphGroupSelectionSpec::new().with_raised_cut_signatures(
2471 SelectionPolarity::Without,
2472 RaisedPropagatorScope::All,
2473 vec![any],
2474 );
2475
2476 let with_plan = with_spec.plan_with_analysis_contexts(
2477 &graph_group_structure,
2478 |graph_id| graphs.get(graph_id),
2479 |_graph_id, graph| Ok(vec![GraphSelectionSubject::whole_graph(graph)]),
2480 raised_cut_test_subjects_by_graph_id,
2481 "no graph subjects",
2482 "no cuts",
2483 )?;
2484 assert_eq!(with_plan.report().kept_master_graphs, vec!["g0"]);
2485
2486 let without_plan = without_spec.plan_with_analysis_contexts(
2487 &graph_group_structure,
2488 |graph_id| graphs.get(graph_id),
2489 |_graph_id, graph| Ok(vec![GraphSelectionSubject::whole_graph(graph)]),
2490 raised_cut_test_subjects_by_graph_id,
2491 "no graph subjects",
2492 "no cuts",
2493 )?;
2494 assert_eq!(without_plan.report().kept_master_graphs, vec!["g1"]);
2495
2496 Ok(())
2497 }
2498}