Skip to main content

gammalooprs/processes/
selection.rs

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}