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