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.0,
1606 ghost_number,
1607 lepton_number: 0,
1608 y_charge: 0,
1609 goldstone,
1610 }
1611 }
1612
1613 fn raised_test_graph() -> Result<Graph> {
1614 let mut graph: Graph = dot!(
1615 digraph raised {
1616 edge [particle=scalar_1]
1617 ext [style=invis]
1618 ext -> A [id=3]
1619 B -> ext [id=4]
1620 A -> B [id=0]
1621 A -> B [id=1]
1622 A -> B [id=2]
1623 A -> B [id=5]
1624 A -> B [id=6]
1625 },
1626 scalar_model()
1627 )?;
1628
1629 graph.loop_momentum_basis = LoopMomentumBasis {
1630 tree: graph.underlying.empty_subgraph(),
1631 loop_edges: vec![
1632 EdgeIndex::from(0),
1633 EdgeIndex::from(1),
1634 EdgeIndex::from(2),
1635 EdgeIndex::from(5),
1636 EdgeIndex::from(6),
1637 ]
1638 .into(),
1639 ext_edges: vec![EdgeIndex::from(3), EdgeIndex::from(4)].into(),
1640 edge_signatures: graph
1641 .underlying
1642 .new_edgevec(|_, edge_id, _| match edge_id.0 {
1643 0 => LoopExtSignature::from((vec![1], vec![])),
1644 1 => LoopExtSignature::from((vec![-1], vec![])),
1645 2 => LoopExtSignature::from((vec![0], vec![])),
1646 5 => LoopExtSignature::from((vec![1], vec![])),
1647 6 => LoopExtSignature::from((vec![-1], vec![])),
1648 _ => LoopExtSignature::from((vec![0], vec![])),
1649 }),
1650 };
1651
1652 for edge_id in [0, 1, 2] {
1653 graph.underlying[EdgeIndex::from(edge_id)].mass = EdgeMass::Zero;
1654 }
1655 Ok(graph)
1656 }
1657
1658 fn tree_raised_test_graph() -> Result<Graph> {
1659 let mut graph: Graph = dot!(
1660 digraph tree_raised {
1661 edge [particle=scalar_1]
1662 ext [style=invis]
1663 ext -> A [id=3]
1664 B -> ext [id=4]
1665 A -> B [id=0]
1666 A -> B [id=1]
1667 A -> B [id=2]
1668 },
1669 scalar_model()
1670 )?;
1671
1672 graph.loop_momentum_basis = LoopMomentumBasis {
1673 tree: graph.underlying.empty_subgraph(),
1674 loop_edges: vec![EdgeIndex::from(2)].into(),
1675 ext_edges: vec![EdgeIndex::from(3), EdgeIndex::from(4)].into(),
1676 edge_signatures: graph
1677 .underlying
1678 .new_edgevec(|_, edge_id, _| match edge_id.0 {
1679 0 => LoopExtSignature::from((vec![0], vec![1])),
1680 1 => LoopExtSignature::from((vec![0], vec![-1])),
1681 2 => LoopExtSignature::from((vec![1], vec![])),
1682 _ => LoopExtSignature::from((vec![0], vec![])),
1683 }),
1684 };
1685
1686 Ok(graph)
1687 }
1688
1689 fn cycle_test_graph() -> Result<Graph> {
1690 dot!(
1691 digraph cycle_test {
1692 node [num="1"]
1693 A -> B [particle=scalar_0, id=0]
1694 B -> C [particle=scalar_1, id=1]
1695 C -> A [particle=scalar_0, id=2]
1696 B -> D [particle=scalar_2, id=3]
1697 D -> C [particle=scalar_2, id=4]
1698 },
1699 scalar_model()
1700 )
1701 }
1702
1703 fn vertex_rule(name: &str) -> ArcVertexRule {
1704 ArcVertexRule(Arc::new(VertexRule {
1705 name: name.into(),
1706 couplings: Vec::new(),
1707 lorentz_structures: Vec::new(),
1708 particles: Vec::new(),
1709 color_structures: ColorStructure {
1710 color_structure: Vec::new(),
1711 },
1712 dod: 0,
1713 }))
1714 }
1715
1716 fn graph_with_vertex_rules(name: &str, vertex_names: &[&str]) -> Result<Graph> {
1717 let mut graph: Graph = "digraph vertex_test {
1718 node [num=\"1\"]
1719 edge [particle=scalar_1]
1720 A -> B [id=0]
1721 B -> C [id=1]
1722 C -> A [id=2]
1723 }"
1724 .into_graph(scalar_model())?;
1725 graph.name = name.to_string();
1726 let vertex_rules = vertex_names
1727 .iter()
1728 .map(|name| vertex_rule(name))
1729 .collect::<Vec<_>>();
1730 for ((_, _, vertex), vertex_rule) in graph
1731 .underlying
1732 .iter_nodes_mut()
1733 .zip(vertex_rules.into_iter().cycle())
1734 {
1735 vertex.vertex_rule = Some(vertex_rule);
1736 }
1737 Ok(graph)
1738 }
1739
1740 fn particle_test_graph(name: &str) -> Result<Graph> {
1741 let mut graph: Graph = "digraph particle_test {
1742 node [num=\"1\"]
1743 A -> B [particle=scalar_0, id=0]
1744 B -> C [particle=scalar_1, id=1]
1745 C -> A [particle=scalar_2, id=2]
1746 }"
1747 .into_graph(scalar_model())?;
1748 graph.name = name.to_string();
1749 Ok(graph)
1750 }
1751
1752 fn subgraph_from_edge_ids(graph: &Graph, edge_ids: &[usize]) -> SuBitGraph {
1753 let wanted = edge_ids.iter().copied().collect::<BTreeSet<_>>();
1754 let mut subgraph: SuBitGraph = graph.underlying.empty_subgraph();
1755 for (pair, edge_id, _) in graph.underlying.iter_edges() {
1756 if wanted.contains(&edge_id.0) {
1757 subgraph.add(pair);
1758 }
1759 }
1760 subgraph
1761 }
1762
1763 fn vertex_test_subjects_by_graph_id(
1764 graph_id: usize,
1765 graph: &Graph,
1766 ) -> Result<Vec<GraphSelectionSubject<'_>>> {
1767 let edge_ids = if graph_id == 0 { &[0][..] } else { &[1][..] };
1768 Ok(vec![GraphSelectionSubject::subgraph(
1769 graph,
1770 subgraph_from_edge_ids(graph, edge_ids),
1771 )])
1772 }
1773
1774 fn raised_cut_test_subjects_by_graph_id(
1775 graph_id: usize,
1776 graph: &Graph,
1777 ) -> Result<Vec<GraphCutSelectionSubject<'_>>> {
1778 let edge_ids = if graph_id == 0 { &[0][..] } else { &[2][..] };
1779 Ok(vec![GraphCutSelectionSubject::new(
1780 graph,
1781 subgraph_from_edge_ids(graph, edge_ids),
1782 )])
1783 }
1784
1785 fn raised_any_test_subjects_by_graph_id(
1786 graph_id: usize,
1787 graph: &Graph,
1788 ) -> Result<Vec<GraphSelectionSubject<'_>>> {
1789 let subject = if graph_id == 0 {
1790 GraphSelectionSubject::whole_graph(graph)
1791 } else {
1792 GraphSelectionSubject::subgraph(graph, subgraph_from_edge_ids(graph, &[0, 2]))
1793 };
1794 Ok(vec![subject])
1795 }
1796
1797 #[test]
1798 fn raised_signature_parses_and_canonicalizes() {
1799 assert_eq!(
1800 "[2,3,4]",
1801 RaisedPropagatorSignature::from_str("[3,2,4]")
1802 .unwrap()
1803 .canonical()
1804 );
1805 assert_eq!(
1806 "[]",
1807 RaisedPropagatorSignature::from_str("[]")
1808 .unwrap()
1809 .canonical()
1810 );
1811 assert_eq!(
1812 "[2]",
1813 RaisedPropagatorSignature::from_str("[2,]")
1814 .unwrap()
1815 .canonical()
1816 );
1817 assert_eq!(
1818 "ANY_RAISING",
1819 RaisedPropagatorSignature::from_str("ANY_RAISING")
1820 .unwrap()
1821 .canonical()
1822 );
1823 assert!(RaisedPropagatorSignature::from_str("[1]").is_err());
1824 }
1825
1826 #[test]
1827 fn any_raising_matches_any_non_empty_raised_signature() -> Result<()> {
1828 let any = RaisedPropagatorSignature::from_str("ANY_RAISING")?;
1829 let none = RaisedPropagatorSignature::from_str("[]")?;
1830 let raised = RaisedPropagatorSignature::from_str("[2]")?;
1831
1832 assert!(any.matches(&raised));
1833 assert!(!any.matches(&none));
1834 assert!(raised.matches(&raised));
1835 assert!(!raised.matches(&RaisedPropagatorSignature::from_str("[3]")?));
1836
1837 Ok(())
1838 }
1839
1840 #[test]
1841 fn raised_signature_groups_up_to_sign_and_splits_by_mass_scope() -> Result<()> {
1842 let graph = raised_test_graph()?;
1843
1844 assert_eq!(
1845 "[2,2]",
1846 graph
1847 .raised_propagator_signature(RaisedPropagatorScope::All)
1848 .canonical()
1849 );
1850 assert_eq!(
1851 "[2]",
1852 graph
1853 .raised_propagator_signature(RaisedPropagatorScope::Massless)
1854 .canonical()
1855 );
1856 assert_eq!(
1857 "[2]",
1858 graph
1859 .raised_propagator_signature(RaisedPropagatorScope::Massive)
1860 .canonical()
1861 );
1862
1863 Ok(())
1864 }
1865
1866 #[test]
1867 fn subgraph_raised_signature_only_uses_edges_in_view() -> Result<()> {
1868 let graph = raised_test_graph()?;
1869 let repeated_side =
1870 GraphSelectionSubject::subgraph(&graph, subgraph_from_edge_ids(&graph, &[0, 1, 2]));
1871 let cut_edge_removed_side =
1872 GraphSelectionSubject::subgraph(&graph, subgraph_from_edge_ids(&graph, &[0, 2]));
1873
1874 assert_eq!(
1875 "[2]",
1876 repeated_side
1877 .raised_propagator_signature(RaisedPropagatorScope::All)
1878 .canonical()
1879 );
1880 assert_eq!(
1881 "[]",
1882 cut_edge_removed_side
1883 .raised_propagator_signature(RaisedPropagatorScope::All)
1884 .canonical()
1885 );
1886
1887 Ok(())
1888 }
1889
1890 #[test]
1891 fn cut_side_amplitude_raised_signature_includes_tree_like_internal_edges() -> Result<()> {
1892 let graph = tree_raised_test_graph()?;
1893 let tree_raised_edges = subgraph_from_edge_ids(&graph, &[0, 1]);
1894 let normal_subject = GraphSelectionSubject::subgraph(&graph, tree_raised_edges.clone());
1895 let cut_side_subject =
1896 GraphSelectionSubject::cut_side_amplitude_subgraph(&graph, tree_raised_edges.clone());
1897
1898 assert_eq!(
1899 "[]",
1900 normal_subject
1901 .raised_propagator_signature(RaisedPropagatorScope::All)
1902 .canonical()
1903 );
1904 assert_eq!(
1905 "[2]",
1906 cut_side_subject
1907 .raised_propagator_signature(RaisedPropagatorScope::All)
1908 .canonical()
1909 );
1910 assert_eq!(
1911 "[]",
1912 GraphCutSelectionSubject::new(&graph, tree_raised_edges)
1913 .raised_cut_signature(RaisedPropagatorScope::All)
1914 .canonical()
1915 );
1916
1917 Ok(())
1918 }
1919
1920 #[test]
1921 fn raised_cut_signature_counts_touched_raised_groups_once() -> Result<()> {
1922 let graph = raised_test_graph()?;
1923
1924 assert_eq!(
1925 "[2]",
1926 GraphCutSelectionSubject::new(&graph, subgraph_from_edge_ids(&graph, &[0]))
1927 .raised_cut_signature(RaisedPropagatorScope::All)
1928 .canonical()
1929 );
1930 assert_eq!(
1931 "[2]",
1932 GraphCutSelectionSubject::new(&graph, subgraph_from_edge_ids(&graph, &[0, 1]))
1933 .raised_cut_signature(RaisedPropagatorScope::All)
1934 .canonical()
1935 );
1936 assert_eq!(
1937 "[2,2]",
1938 GraphCutSelectionSubject::new(&graph, subgraph_from_edge_ids(&graph, &[0, 5]))
1939 .raised_cut_signature(RaisedPropagatorScope::All)
1940 .canonical()
1941 );
1942 assert_eq!(
1943 "[]",
1944 GraphCutSelectionSubject::new(&graph, subgraph_from_edge_ids(&graph, &[2]))
1945 .raised_cut_signature(RaisedPropagatorScope::All)
1946 .canonical()
1947 );
1948
1949 Ok(())
1950 }
1951
1952 #[test]
1953 fn raised_cut_signature_respects_mass_scope() -> Result<()> {
1954 let graph = raised_test_graph()?;
1955 let cut_subject =
1956 GraphCutSelectionSubject::new(&graph, subgraph_from_edge_ids(&graph, &[0, 5]));
1957
1958 assert_eq!(
1959 "[2,2]",
1960 cut_subject
1961 .raised_cut_signature(RaisedPropagatorScope::All)
1962 .canonical()
1963 );
1964 assert_eq!(
1965 "[2]",
1966 cut_subject
1967 .raised_cut_signature(RaisedPropagatorScope::Massless)
1968 .canonical()
1969 );
1970 assert_eq!(
1971 "[2]",
1972 cut_subject
1973 .raised_cut_signature(RaisedPropagatorScope::Massive)
1974 .canonical()
1975 );
1976
1977 Ok(())
1978 }
1979
1980 #[test]
1981 fn vertex_signature_preserves_multiplicity() {
1982 let signature = VertexSignature::parse("[V_9,V_6,V_9]").unwrap();
1983 assert_eq!(signature.0.get("V_9"), Some(&2));
1984 assert_eq!(signature.0.get("V_6"), Some(&1));
1985 assert_eq!(signature.canonical(), "[V_6,V_9,V_9]");
1986 assert_eq!(
1987 VertexSignature::parse("[V_1,]").unwrap().canonical(),
1988 "[V_1]"
1989 );
1990 assert!(VertexSignature::parse("[]").is_err());
1991 }
1992
1993 #[test]
1994 fn particle_signature_parses_names_pdgs_antiparticles_and_tuples() {
1995 let model = minimal_model();
1996 assert_eq!(
1997 "[11,21]",
1998 ParticleSignature::parse("[e+,g]", &model)
1999 .unwrap()
2000 .canonical()
2001 );
2002 assert_eq!(
2003 "[11,21]",
2004 ParticleSignature::parse("(e-,-21,)", &model)
2005 .unwrap()
2006 .canonical()
2007 );
2008 assert!(ParticleSignature::parse("[]", &model).is_err());
2009 assert!(ParticleSignature::parse("[0]", &model).is_err());
2010 }
2011
2012 #[test]
2013 fn cycle_signature_parses_and_canonicalizes() {
2014 let model = minimal_model();
2015 assert_eq!(
2016 "[(3)]",
2017 CycleSignature::parse("[(3)]", &model).unwrap().canonical()
2018 );
2019 assert_eq!(
2020 "[(3)]",
2021 CycleSignature::parse("[(3,),]", &model)
2022 .unwrap()
2023 .canonical()
2024 );
2025 assert_eq!(
2026 "[(3,21),(4,22)]",
2027 CycleSignature::parse("[(3,21), (4,22)]", &model)
2028 .unwrap()
2029 .canonical()
2030 );
2031 assert_eq!(
2032 "[(fermion,ghost,goldstone)]",
2033 CycleSignature::parse("[(fermion,ghost,goldstone)]", &model)
2034 .unwrap()
2035 .canonical()
2036 );
2037 }
2038
2039 #[test]
2040 fn cycle_requirements_include_simple_cycles_beyond_basis() -> Result<()> {
2041 let graph = cycle_test_graph()?;
2042 let scalar_0 = scalar_model().try_get_particle("scalar_0")?.pdg_code.abs();
2043 let scalar_1 = scalar_model().try_get_particle("scalar_1")?.pdg_code.abs();
2044 let scalar_2 = scalar_model().try_get_particle("scalar_2")?.pdg_code.abs();
2045 let requirements = graph
2046 .cycle_requirements()
2047 .into_iter()
2048 .map(|requirement| requirement.to_string())
2049 .collect::<BTreeSet<_>>();
2050
2051 let basis_cycle_a = format!("({},{})", scalar_0.min(scalar_1), scalar_0.max(scalar_1));
2052 let basis_cycle_b = format!("({},{})", scalar_1.min(scalar_2), scalar_1.max(scalar_2));
2053 let combined_cycle = format!("({},{})", scalar_0.min(scalar_2), scalar_0.max(scalar_2));
2054
2055 assert!(
2056 requirements.contains(&basis_cycle_a),
2057 "missing first basis cycle in {requirements:?}"
2058 );
2059 assert!(
2060 requirements.contains(&basis_cycle_b),
2061 "missing second basis cycle in {requirements:?}"
2062 );
2063 assert!(
2064 requirements.contains(&combined_cycle),
2065 "missing combined simple cycle in {requirements:?}"
2066 );
2067
2068 Ok(())
2069 }
2070
2071 #[test]
2072 fn subgraph_cycles_are_broken_by_removed_edges() -> Result<()> {
2073 let graph = cycle_test_graph()?;
2074 let full_subject = GraphSelectionSubject::whole_graph(&graph);
2075 let cut_side_subject =
2076 GraphSelectionSubject::subgraph(&graph, subgraph_from_edge_ids(&graph, &[0, 1]));
2077
2078 assert!(!full_subject.cycle_requirements().is_empty());
2079 assert!(cut_side_subject.cycle_requirements().is_empty());
2080
2081 Ok(())
2082 }
2083
2084 #[test]
2085 fn cycle_signature_rejects_antiparticles() {
2086 let model = minimal_model();
2087 assert!(CycleSignature::parse("[(-3)]", &model).is_err());
2088 assert!(CycleSignature::parse("[(e+)]", &model).is_err());
2089 }
2090
2091 #[test]
2092 fn cycle_category_requirement_matches_particles_directly() {
2093 let model = minimal_model();
2094 let gluon = model.try_get_particle_from_pdg(21).unwrap();
2095 let strange = model.try_get_particle_from_pdg(3).unwrap();
2096 let requirement =
2097 CycleRequirement::new(vec![CycleMatcher::Pdg(3), CycleMatcher::Pdg(21)]).unwrap();
2098 assert!(cycle_matches_requirement(
2099 &[strange.clone(), gluon.clone()],
2100 &requirement
2101 ));
2102 let fermion_requirement = CycleRequirement::new(vec![CycleMatcher::Fermion]).unwrap();
2103 assert!(cycle_matches_requirement(&[strange], &fermion_requirement));
2104 assert!(!cycle_matches_requirement(&[gluon], &fermion_requirement));
2105 }
2106
2107 #[test]
2108 fn subgraph_vertex_signature_counts_only_vertices_in_view() -> Result<()> {
2109 let graph = graph_with_vertex_rules("g0", &["V_A", "V_B", "V_C"])?;
2110 let subject = GraphSelectionSubject::subgraph(&graph, subgraph_from_edge_ids(&graph, &[0]));
2111 let counts = subject.vertex_rule_name_counts();
2112
2113 assert_eq!(counts.values().copied().sum::<usize>(), 2);
2114 assert_eq!(
2115 GraphSelectionSubject::whole_graph(&graph)
2116 .vertex_rule_name_counts()
2117 .values()
2118 .copied()
2119 .sum::<usize>(),
2120 3
2121 );
2122
2123 Ok(())
2124 }
2125
2126 #[test]
2127 fn subgraph_particle_signature_counts_only_particles_in_view() -> Result<()> {
2128 let graph = particle_test_graph("g0")?;
2129 let subject = GraphSelectionSubject::subgraph(&graph, subgraph_from_edge_ids(&graph, &[0]));
2130 let scalar_0 = scalar_model().try_get_particle("scalar_0")?.pdg_code.abs();
2131 let scalar_1 = scalar_model().try_get_particle("scalar_1")?.pdg_code.abs();
2132
2133 assert!(subject.particle_pdgs().contains(&scalar_0));
2134 assert!(!subject.particle_pdgs().contains(&scalar_1));
2135 assert!(
2136 GraphSelectionSubject::whole_graph(&graph)
2137 .particle_pdgs()
2138 .contains(&scalar_1)
2139 );
2140
2141 Ok(())
2142 }
2143
2144 #[test]
2145 fn selection_spec_combines_rules_with_and_and_respects_vertex_multiplicity() -> Result<()> {
2146 let mut graphs = vec![
2147 graph_with_vertex_rules("g0", &["V_A", "V_A", "V_B"])?,
2148 graph_with_vertex_rules("g1", &["V_A", "V_B", "V_C"])?,
2149 graph_with_vertex_rules("g2", &["V_A", "V_A", "V_C"])?,
2150 ];
2151 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2152
2153 let spec = GraphGroupSelectionSpec::new()
2154 .with_vertex_signatures(
2155 SelectionPolarity::With,
2156 vec![VertexSignature::parse("[V_A,V_A]")?],
2157 )
2158 .with_vertex_signatures(
2159 SelectionPolarity::Without,
2160 vec![VertexSignature::parse("[V_C]")?],
2161 );
2162 let plan = spec.plan(&graph_group_structure, |graph_id| graphs.get(graph_id))?;
2163
2164 assert_eq!(plan.retained_group_ids(), &[GroupId(0)]);
2165 assert_eq!(plan.report().kept_master_graphs, vec!["g0".to_string()]);
2166 assert_eq!(
2167 plan.report().removed_master_graphs,
2168 vec!["g1".to_string(), "g2".to_string()]
2169 );
2170
2171 Ok(())
2172 }
2173
2174 #[test]
2175 fn graph_name_selection_supports_without_polarity() -> Result<()> {
2176 let mut graphs = vec![
2177 graph_with_vertex_rules("g0", &["V_A"])?,
2178 graph_with_vertex_rules("g1", &["V_B"])?,
2179 graph_with_vertex_rules("g2", &["V_C"])?,
2180 ];
2181 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2182
2183 let spec = GraphGroupSelectionSpec::new()
2184 .with_master_graph_names_polarity(SelectionPolarity::Without, vec!["g1".to_string()]);
2185 let plan = spec.plan(&graph_group_structure, |graph_id| graphs.get(graph_id))?;
2186
2187 assert_eq!(plan.report().kept_master_graphs, vec!["g0", "g2"]);
2188 assert_eq!(plan.report().removed_master_graphs, vec!["g1"]);
2189
2190 Ok(())
2191 }
2192
2193 #[test]
2194 fn master_graph_name_selection_rejects_members_and_preserves_group_order() -> Result<()> {
2195 let mut graphs = vec![
2196 graph_with_vertex_rules("master_0", &["V_A"])?,
2197 graph_with_vertex_rules("member_0", &["V_A"])?,
2198 graph_with_vertex_rules("master_1", &["V_B"])?,
2199 ];
2200 graphs[0].group_id = Some(GroupId(0));
2201 graphs[0].is_group_master = true;
2202 graphs[1].group_id = Some(GroupId(0));
2203 graphs[1].is_group_master = false;
2204 graphs[2].group_id = Some(GroupId(1));
2205 graphs[2].is_group_master = true;
2206 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2207
2208 let member_error =
2209 GraphGroupSelectionSpec::from_master_graph_names(vec!["member_0".to_string()])
2210 .plan(&graph_group_structure, |graph_id| graphs.get(graph_id))
2211 .unwrap_err();
2212 assert!(
2213 member_error.chain().any(|cause| cause
2214 .to_string()
2215 .contains("is not the master graph of its group; use 'master_0' instead")),
2216 "{member_error:?}"
2217 );
2218
2219 let plan = GraphGroupSelectionSpec::from_master_graph_names(vec![
2220 "master_1".to_string(),
2221 "master_0".to_string(),
2222 ])
2223 .plan(&graph_group_structure, |graph_id| graphs.get(graph_id))?;
2224 assert_eq!(plan.retained_group_ids(), &[GroupId(0), GroupId(1)]);
2225 assert_eq!(plan.report().kept_master_graphs, ["master_0", "master_1"]);
2226
2227 Ok(())
2228 }
2229
2230 #[test]
2231 fn with_graph_names_are_authoritative_over_vetoes() -> Result<()> {
2232 let mut graphs = vec![
2233 graph_with_vertex_rules("g0", &["V_A"])?,
2234 graph_with_vertex_rules("g1", &["V_B"])?,
2235 ];
2236 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2237
2238 let spec = GraphGroupSelectionSpec::new()
2239 .with_master_graph_names(vec!["g1".to_string()])
2240 .with_vertex_signatures(
2241 SelectionPolarity::Without,
2242 vec![VertexSignature::parse("[V_B]")?],
2243 );
2244 let plan = spec.plan(&graph_group_structure, |graph_id| graphs.get(graph_id))?;
2245
2246 assert_eq!(plan.report().kept_master_graphs, vec!["g0", "g1"]);
2247 assert!(plan.report().removed_master_graphs.is_empty());
2248
2249 Ok(())
2250 }
2251
2252 #[test]
2253 fn graph_name_selection_rejects_conflicting_constraints() -> Result<()> {
2254 let mut graphs = vec![
2255 graph_with_vertex_rules("g0", &["V_A"])?,
2256 graph_with_vertex_rules("g1", &["V_B"])?,
2257 ];
2258 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2259
2260 let spec = GraphGroupSelectionSpec::new()
2261 .with_master_graph_names(vec!["g1".to_string()])
2262 .with_master_graph_names_polarity(SelectionPolarity::Without, vec!["g1".to_string()]);
2263 let err = spec
2264 .plan(&graph_group_structure, |graph_id| graphs.get(graph_id))
2265 .unwrap_err();
2266 let message = format!("{err}");
2267
2268 assert!(message.contains("Contradictory graph-name selection"));
2269 assert!(message.contains("g1"));
2270 assert!(message.contains("--with-graph-names"));
2271 assert!(message.contains("--without-graph-names"));
2272
2273 Ok(())
2274 }
2275
2276 #[test]
2277 fn particle_selection_matches_required_sets() -> Result<()> {
2278 let mut graphs = vec![
2279 particle_test_graph("g0")?,
2280 graph_with_vertex_rules("g1", &["V_A"])?,
2281 ];
2282 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2283 let scalar_0 = scalar_model()
2284 .try_get_particle("scalar_0")?
2285 .name
2286 .to_string();
2287 let scalar_1 = scalar_model()
2288 .try_get_particle("scalar_1")?
2289 .name
2290 .to_string();
2291 let scalar_2 = scalar_model()
2292 .try_get_particle("scalar_2")?
2293 .name
2294 .to_string();
2295
2296 let spec = GraphGroupSelectionSpec::new()
2297 .with_particle_signatures(
2298 SelectionPolarity::With,
2299 vec![ParticleSignature::parse(
2300 &format!("[{scalar_0},{scalar_1}]"),
2301 scalar_model(),
2302 )?],
2303 )
2304 .with_particle_signatures(
2305 SelectionPolarity::Without,
2306 vec![ParticleSignature::parse(
2307 &format!("[{scalar_2}]"),
2308 scalar_model(),
2309 )?],
2310 );
2311
2312 let err = spec
2313 .plan(&graph_group_structure, |graph_id| graphs.get(graph_id))
2314 .unwrap_err();
2315 assert!(
2316 err.to_string()
2317 .contains("Graph-group selection would remove all graph groups"),
2318 "{err:?}"
2319 );
2320
2321 let spec = GraphGroupSelectionSpec::new().with_particle_signatures(
2322 SelectionPolarity::With,
2323 vec![ParticleSignature::parse(
2324 &format!("({scalar_0},{scalar_1})"),
2325 scalar_model(),
2326 )?],
2327 );
2328 let plan = spec.plan(&graph_group_structure, |graph_id| graphs.get(graph_id))?;
2329 assert_eq!(plan.report().kept_master_graphs, vec!["g0"]);
2330
2331 Ok(())
2332 }
2333
2334 #[test]
2335 fn structural_selection_matches_any_analysis_subject_and_without_vetoes() -> Result<()> {
2336 let mut graphs = vec![
2337 graph_with_vertex_rules("g0", &["V_A", "V_B", "V_C"])?,
2338 graph_with_vertex_rules("g1", &["V_D", "V_E", "V_F"])?,
2339 ];
2340 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2341 let with_spec = GraphGroupSelectionSpec::new()
2342 .with_master_graph_names(vec!["g0".to_string(), "g1".to_string()])
2343 .with_vertex_signatures(
2344 SelectionPolarity::With,
2345 vec![VertexSignature::parse("[V_A]")?],
2346 );
2347 let without_spec = GraphGroupSelectionSpec::new().with_vertex_signatures(
2348 SelectionPolarity::Without,
2349 vec![VertexSignature::parse("[V_A]")?],
2350 );
2351
2352 let with_plan = with_spec.plan_with_analysis_contexts(
2353 &graph_group_structure,
2354 |graph_id| graphs.get(graph_id),
2355 vertex_test_subjects_by_graph_id,
2356 |_graph_id, _graph| Ok(Vec::new()),
2357 "no subjects",
2358 "no cuts",
2359 )?;
2360 assert_eq!(with_plan.report().kept_master_graphs, vec!["g0", "g1"]);
2361
2362 let without_plan = without_spec.plan_with_analysis_contexts(
2363 &graph_group_structure,
2364 |graph_id| graphs.get(graph_id),
2365 vertex_test_subjects_by_graph_id,
2366 |_graph_id, _graph| Ok(Vec::new()),
2367 "no subjects",
2368 "no cuts",
2369 )?;
2370 assert_eq!(without_plan.report().kept_master_graphs, vec!["g1"]);
2371
2372 Ok(())
2373 }
2374
2375 #[test]
2376 fn raised_propagator_selection_supports_any_raising_wildcard() -> Result<()> {
2377 let mut graphs = vec![raised_test_graph()?, raised_test_graph()?];
2378 graphs[0].name = "g0".to_string();
2379 graphs[1].name = "g1".to_string();
2380 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2381 let any = RaisedPropagatorSignature::from_str("ANY_RAISING")?;
2382 let with_spec = GraphGroupSelectionSpec::new().with_raised_propagator_signatures(
2383 SelectionPolarity::With,
2384 RaisedPropagatorScope::All,
2385 vec![any.clone()],
2386 );
2387 let without_spec = GraphGroupSelectionSpec::new().with_raised_propagator_signatures(
2388 SelectionPolarity::Without,
2389 RaisedPropagatorScope::All,
2390 vec![any],
2391 );
2392
2393 let with_plan = with_spec.plan_with_analysis_contexts(
2394 &graph_group_structure,
2395 |graph_id| graphs.get(graph_id),
2396 raised_any_test_subjects_by_graph_id,
2397 |_graph_id, _graph| Ok(Vec::new()),
2398 "no graph subjects",
2399 "no cuts",
2400 )?;
2401 assert_eq!(with_plan.report().kept_master_graphs, vec!["g0"]);
2402
2403 let without_plan = without_spec.plan_with_analysis_contexts(
2404 &graph_group_structure,
2405 |graph_id| graphs.get(graph_id),
2406 raised_any_test_subjects_by_graph_id,
2407 |_graph_id, _graph| Ok(Vec::new()),
2408 "no graph subjects",
2409 "no cuts",
2410 )?;
2411 assert_eq!(without_plan.report().kept_master_graphs, vec!["g1"]);
2412
2413 Ok(())
2414 }
2415
2416 #[test]
2417 fn raised_cut_selection_matches_any_cut_and_without_vetoes() -> Result<()> {
2418 let mut graphs = vec![raised_test_graph()?, raised_test_graph()?];
2419 graphs[0].name = "g0".to_string();
2420 graphs[1].name = "g1".to_string();
2421 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2422 let with_spec = GraphGroupSelectionSpec::new().with_raised_cut_signatures(
2423 SelectionPolarity::With,
2424 RaisedPropagatorScope::All,
2425 vec![RaisedPropagatorSignature::from_str("[2]")?],
2426 );
2427 let without_spec = GraphGroupSelectionSpec::new().with_raised_cut_signatures(
2428 SelectionPolarity::Without,
2429 RaisedPropagatorScope::All,
2430 vec![RaisedPropagatorSignature::from_str("[2]")?],
2431 );
2432
2433 let with_plan = with_spec.plan_with_analysis_contexts(
2434 &graph_group_structure,
2435 |graph_id| graphs.get(graph_id),
2436 |_graph_id, graph| Ok(vec![GraphSelectionSubject::whole_graph(graph)]),
2437 raised_cut_test_subjects_by_graph_id,
2438 "no graph subjects",
2439 "no cuts",
2440 )?;
2441 assert_eq!(with_plan.report().kept_master_graphs, vec!["g0"]);
2442
2443 let without_plan = without_spec.plan_with_analysis_contexts(
2444 &graph_group_structure,
2445 |graph_id| graphs.get(graph_id),
2446 |_graph_id, graph| Ok(vec![GraphSelectionSubject::whole_graph(graph)]),
2447 raised_cut_test_subjects_by_graph_id,
2448 "no graph subjects",
2449 "no cuts",
2450 )?;
2451 assert_eq!(without_plan.report().kept_master_graphs, vec!["g1"]);
2452
2453 Ok(())
2454 }
2455
2456 #[test]
2457 fn raised_cut_selection_supports_any_raising_wildcard() -> Result<()> {
2458 let mut graphs = vec![raised_test_graph()?, raised_test_graph()?];
2459 graphs[0].name = "g0".to_string();
2460 graphs[1].name = "g1".to_string();
2461 let graph_group_structure = complete_group_parsing(&mut graphs)?;
2462 let any = RaisedPropagatorSignature::from_str("ANY_RAISING")?;
2463 let with_spec = GraphGroupSelectionSpec::new().with_raised_cut_signatures(
2464 SelectionPolarity::With,
2465 RaisedPropagatorScope::All,
2466 vec![any.clone()],
2467 );
2468 let without_spec = GraphGroupSelectionSpec::new().with_raised_cut_signatures(
2469 SelectionPolarity::Without,
2470 RaisedPropagatorScope::All,
2471 vec![any],
2472 );
2473
2474 let with_plan = with_spec.plan_with_analysis_contexts(
2475 &graph_group_structure,
2476 |graph_id| graphs.get(graph_id),
2477 |_graph_id, graph| Ok(vec![GraphSelectionSubject::whole_graph(graph)]),
2478 raised_cut_test_subjects_by_graph_id,
2479 "no graph subjects",
2480 "no cuts",
2481 )?;
2482 assert_eq!(with_plan.report().kept_master_graphs, vec!["g0"]);
2483
2484 let without_plan = without_spec.plan_with_analysis_contexts(
2485 &graph_group_structure,
2486 |graph_id| graphs.get(graph_id),
2487 |_graph_id, graph| Ok(vec![GraphSelectionSubject::whole_graph(graph)]),
2488 raised_cut_test_subjects_by_graph_id,
2489 "no graph subjects",
2490 "no cuts",
2491 )?;
2492 assert_eq!(without_plan.report().kept_master_graphs, vec!["g1"]);
2493
2494 Ok(())
2495 }
2496}