Skip to main content

gammalooprs/graph/
mod.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    ops::Index,
4};
5
6use ahash::AHashSet;
7use bincode_trait_derive::{Decode, Encode};
8use gammaloop_tracing_filter::LogMessage;
9use itertools::Itertools;
10use linnet::{
11    half_edge::{
12        HedgeGraph,
13        involution::{EdgeData, EdgeIndex, Hedge, HedgePair},
14        subgraph::{
15            HedgeNode, Inclusion, ModifySubSet, OrientedCut, SuBitGraph, SubGraphLike, SubSetLike,
16            SubSetOps,
17        },
18    },
19    parser::DotGraph,
20};
21use tracing::debug;
22
23use rand::{Rng, SeedableRng, rngs::SmallRng};
24// use petgraph::Direction::Outgoing;
25use spenso::shadowing::symbolica_utils::LogPrint;
26use symbolica::atom::{Atom, AtomCore};
27use tracing::warn;
28use typed_index_collections::TiVec;
29
30use crate::{
31    cff::surface::SurfaceCache,
32    define_index,
33    feyngen::diagram_generator::evaluate_overall_factor,
34    integrands::process::{LmbMultiChannelingSetup, ParamBuilder, SamplingChannelId},
35    momentum::{Dep, ExternalMomenta, PolDef, sample::ExternalIndex},
36    numerator::GlobalPrefactor,
37    processes::DotExportSettings,
38    settings::runtime::kinematic::{Externals, improvement::PhaseSpaceImprovementSettings},
39    utils::{F, Length, ose_atom_from_index},
40    uv::{UltravioletGraph, uv_graph::UVE},
41};
42
43pub(crate) mod attribute_warnings;
44pub mod autogen;
45pub mod cuts;
46pub mod global;
47pub mod threshold_counterterms;
48
49#[derive(Clone, Copy, bincode_trait_derive::Encode, bincode_trait_derive::Decode, Default)]
50pub struct VertexOrder(pub u8);
51
52define_index! {pub struct GroupId;}
53
54#[derive(Clone, bincode_trait_derive::Encode, bincode_trait_derive::Decode)]
55#[trait_decode(trait = crate::GammaLoopContext)]
56pub struct Graph {
57    pub overall_factor: Atom,
58    pub name: String,
59    pub group_id: Option<GroupId>,
60    pub is_group_master: bool,
61    pub tree_edges: SuBitGraph,
62    pub underlying: HedgeGraph<Edge, Vertex, HedgeData>,
63    pub loop_momentum_basis: LoopMomentumBasis,
64    pub param_builder: ParamBuilder,
65    pub global_prefactor: GlobalPrefactor,
66    pub surface_cache: SurfaceCache,
67    /// The cross section initial state cut
68    /// Only relevant for cross sections, but stored here for the parsing
69    pub initial_state_cut: OrientedCut,
70    pub polarizations: Vec<(PolDef, Atom)>,
71    /// Compact graph-local threshold-counterterm directives from the DOT input.
72    pub threshold_counterterms: autogen::Autogen<threshold_counterterms::ThresholdCountertermSpec>,
73}
74
75impl LogMessage for Graph {
76    fn log_display(&self) -> String {
77        self.name.to_string()
78    }
79}
80
81// impl Deref for Graph {
82//     type Target = HedgeGraph<Edge, Vertex>;
83// }
84
85pub mod feynman_graph;
86pub use feynman_graph::FeynmanGraph;
87pub mod ext;
88
89#[derive(Clone, Copy)]
90pub(crate) enum LmbChannelFallback {
91    CurrentGraphBasis,
92    FirstBasis,
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub(crate) enum ThresholdPinchStatus {
97    Always,
98    CanBecome,
99    NotProven,
100}
101
102impl Graph {
103    pub(crate) fn with_global_numerator_only(&self, name: String, numerator: Atom) -> Self {
104        let mut graph = self.clone();
105        graph.name = name;
106        graph.overall_factor = Atom::one();
107        graph.global_prefactor.num = numerator;
108        graph.global_prefactor.projector = Atom::one();
109        graph.polarizations.clear();
110        graph.underlying = self.underlying.map_data_ref(
111            |_, _, vertex| {
112                let mut vertex = vertex.clone();
113                vertex.num = autogen::Autogen::explicit(Atom::one());
114                vertex
115            },
116            |_, _, _, edge| {
117                edge.map(|edge| {
118                    let mut edge = edge.clone();
119                    edge.num = autogen::Autogen::explicit(Atom::one());
120                    edge
121                })
122            },
123            |_, hedge_data| hedge_data.clone(),
124        );
125        graph
126    }
127
128    pub fn debug_dot(&self) -> String {
129        DotGraph::from(self).debug_dot()
130    }
131
132    pub fn debug_dot_with_settings(&self, settings: &DotExportSettings) -> String {
133        self.to_dot_graph_with_settings(settings).debug_dot()
134    }
135
136    pub fn pretty_dot(&self) -> String {
137        self.dot_impl(
138            &self.full_filter(),
139            "",
140            &|_a| None,
141            &|e| Some(format!("label=\"{}\"", e.num.log_print(None))),
142            &|v| Some(format!("label=\"{}\"", v.num.log_print(None))),
143        )
144    }
145
146    pub(crate) fn global_atom(&self) -> Atom {
147        &self.global_prefactor.num
148            * &self.global_prefactor.projector
149            * evaluate_overall_factor(self.overall_factor.as_view())
150    }
151
152    pub(crate) fn production_numerator_atom_for_full_3d_expression(&self) -> Atom {
153        let reduced = self.full_filter().subtract(&self.initial_state_cut);
154        self.numerator(&reduced, &self.empty_subgraph())
155            .get_single_atom()
156            .expect("Graph numerator should be available")
157            * self.global_atom()
158    }
159
160    pub(crate) fn external_momentum_edge_order(&self) -> Vec<EdgeIndex> {
161        if self.initial_state_cut.nedges(&self.underlying) == 0 {
162            let external_filter: SuBitGraph = self.external_filter();
163            external_filter
164                .included_iter()
165                .sorted()
166                .map(|hedge| self.underlying[&hedge])
167                .collect_vec()
168        } else {
169            self.initial_state_cut
170                .iter_left_hedges()
171                .sorted()
172                .map(|hedge| self.underlying[&hedge])
173                .collect_vec()
174        }
175    }
176
177    pub(crate) fn canonicalize_lmb_external_order(&self, lmb: &mut LoopMomentumBasis) {
178        lmb.canonicalize_external_order(&self.external_momentum_edge_order());
179    }
180
181    pub(crate) fn dummy_stripped_external_flows_of<S: SubGraphLike>(&self, subgraph: &S) -> S::Base
182    where
183        S::Base: ModifySubSet<HedgePair> + ModifySubSet<Hedge>,
184    {
185        let mut externals = self.underlying.full_crown(subgraph);
186        for (pair, _, edge) in self.underlying.iter_edges() {
187            if edge.data.is_dummy {
188                externals.sub(pair);
189            }
190        }
191        externals
192    }
193
194    pub(crate) fn random_externals(&self, seed: u64) -> Externals {
195        let mut rng = SmallRng::seed_from_u64(seed);
196        let mom_range = -10.0..10.0;
197
198        let mut momenta = vec![ExternalMomenta::Dependent(Dep::Dep)];
199        let mut helicities = vec![];
200        let ext: SuBitGraph = self.external_filter();
201
202        for (_, _, d) in self.iter_edges_of(&ext) {
203            let hel = d.data.random_helicity(seed);
204            helicities.push(hel);
205            if helicities.len() == 2 {
206                continue;
207            }
208            let mom = ExternalMomenta::Independent([
209                F(rng.random_range(mom_range.clone())),
210                F(rng.random_range(mom_range.clone())),
211                F(rng.random_range(mom_range.clone())),
212                F(rng.random_range(mom_range.clone())),
213            ]);
214
215            momenta.push(mom);
216        }
217        Externals::Constant {
218            momenta,
219            helicities,
220            improvement_settings: PhaseSpaceImprovementSettings::default(),
221            f_64_cache: None,
222            f_128_cache: None,
223            arb_cache: Default::default(),
224        }
225    }
226
227    // pub(crate) fn new(
228    //     name: SmartString<LazyCompact>,
229    //     multiplicity: Atom,
230    //     underlying: HedgeGraph<Edge, Vertex, NumHedgeData>,
231    // ) -> Result<Self> {
232    //     Ok(Self {
233    //         name: name.to_string(),
234    //         overall_factor: multiplicity,
235    //         loop_momentum_basis: underlying.lmb(&underlying.full_filter()),
236    //         group_id: None,
237    //         is_group_master: false,
238    //         underlying,
239    //         global_prefactor: GlobalPrefactor::default(),
240    //         polarizations: vec![],
241    //     })
242    // }
243
244    pub(crate) fn build_multi_channeling_channels(
245        &self,
246        lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
247        override_lmb_heuristics: bool,
248    ) -> LmbMultiChannelingSetup {
249        let channels = self.select_amplitude_lmb_channel_indices(
250            lmbs,
251            override_lmb_heuristics,
252            LmbChannelFallback::CurrentGraphBasis,
253        );
254
255        debug!(
256            "number of lmbs: {}, number of channels: {}",
257            lmbs.len(),
258            channels.len()
259        );
260
261        LmbMultiChannelingSetup {
262            master_edge_masses: Default::default(),
263            sampling_bridge: Default::default(),
264            sampling_bridge_quad: Default::default(),
265            sampling_bridge_fixed256: Default::default(),
266            sampling_bridge_arb: Default::default(),
267            sampling_source: Default::default(),
268            sampling_catalogue: Default::default(),
269            sampling_programs: Default::default(),
270            lmb_basis_ids: channels,
271            graph: self.clone(),
272            all_bases: lmbs.clone(),
273        }
274    }
275
276    pub(crate) fn select_amplitude_lmb_channel_indices(
277        &self,
278        lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
279        override_lmb_heuristics: bool,
280        fallback: LmbChannelFallback,
281    ) -> TiVec<SamplingChannelId, LmbIndex> {
282        if override_lmb_heuristics {
283            return lmbs
284                .iter_enumerated()
285                .map(|(lmb_index, _)| lmb_index)
286                .collect();
287        }
288
289        let Some((_, first_lmb)) = lmbs.iter_enumerated().next() else {
290            return TiVec::new();
291        };
292        let num_loops = first_lmb.loop_edges.len();
293        if num_loops == 0 {
294            return self.fallback_lmb_channel_indices(lmbs, fallback);
295        }
296
297        let mut universe = BTreeMap::<Vec<EdgeIndex>, usize>::new();
298        let mut candidate_covers = Vec::<(LmbIndex, BTreeSet<usize>)>::new();
299        for (lmb_index, lmb) in lmbs.iter_enumerated() {
300            let mut cover = BTreeSet::<usize>::new();
301            let massless_loop_edges = lmb
302                .loop_edges
303                .iter()
304                .copied()
305                .filter(|edge_id| self.underlying[*edge_id].particle.is_massless())
306                .collect_vec();
307
308            for mut combination in massless_loop_edges.into_iter().combinations(num_loops) {
309                combination.sort();
310                let next_universe_id = universe.len();
311                let universe_id = *universe.entry(combination).or_insert(next_universe_id);
312                cover.insert(universe_id);
313            }
314
315            if !cover.is_empty() {
316                candidate_covers.push((lmb_index, cover));
317            }
318        }
319
320        if universe.is_empty() {
321            return self.fallback_lmb_channel_indices(lmbs, fallback);
322        }
323
324        let channels = Self::minimum_lmb_set_cover(universe.len(), &candidate_covers);
325        if channels.is_empty() {
326            self.fallback_lmb_channel_indices(lmbs, fallback)
327        } else {
328            channels.into_iter().sorted().collect()
329        }
330    }
331
332    fn fallback_lmb_channel_indices(
333        &self,
334        lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
335        fallback: LmbChannelFallback,
336    ) -> TiVec<SamplingChannelId, LmbIndex> {
337        let fallback_index = match fallback {
338            LmbChannelFallback::CurrentGraphBasis => lmbs
339                .iter_enumerated()
340                .find(|(_, lmb)| lmb.loop_edges == self.loop_momentum_basis.loop_edges)
341                .map(|(lmb_index, _)| lmb_index)
342                .or_else(|| {
343                    warn!(
344                        "lmb not in the list of all lmb, falling back to the first generated basis"
345                    );
346                    lmbs.iter_enumerated()
347                        .next()
348                        .map(|(lmb_index, _)| lmb_index)
349                }),
350            LmbChannelFallback::FirstBasis => lmbs
351                .iter_enumerated()
352                .next()
353                .map(|(lmb_index, _)| lmb_index),
354        };
355
356        fallback_index.into_iter().collect()
357    }
358
359    fn minimum_lmb_set_cover(
360        universe_len: usize,
361        candidate_covers: &[(LmbIndex, BTreeSet<usize>)],
362    ) -> Vec<LmbIndex> {
363        let mut candidates_by_element = vec![Vec::<usize>::new(); universe_len];
364        for (candidate_pos, (_, cover)) in candidate_covers.iter().enumerate() {
365            for element in cover {
366                candidates_by_element[*element].push(candidate_pos);
367            }
368        }
369
370        let remaining = (0..universe_len).collect::<BTreeSet<_>>();
371        let mut chosen = Vec::<usize>::new();
372        let mut best = None;
373        Self::search_minimum_lmb_set_cover(
374            remaining,
375            &mut chosen,
376            &mut best,
377            candidate_covers,
378            &candidates_by_element,
379        );
380        best.unwrap_or_default()
381    }
382
383    fn search_minimum_lmb_set_cover(
384        remaining: BTreeSet<usize>,
385        chosen: &mut Vec<usize>,
386        best: &mut Option<Vec<LmbIndex>>,
387        candidate_covers: &[(LmbIndex, BTreeSet<usize>)],
388        candidates_by_element: &[Vec<usize>],
389    ) {
390        if remaining.is_empty() {
391            let mut candidate = chosen
392                .iter()
393                .map(|candidate_pos| candidate_covers[*candidate_pos].0)
394                .collect_vec();
395            candidate.sort();
396            let better = best.as_ref().is_none_or(|current_best| {
397                candidate.len() < current_best.len()
398                    || (candidate.len() == current_best.len() && candidate < *current_best)
399            });
400            if better {
401                *best = Some(candidate);
402            }
403            return;
404        }
405
406        if let Some(current_best) = best.as_ref()
407            && chosen.len() >= current_best.len()
408        {
409            return;
410        }
411
412        let Some(element) = remaining
413            .iter()
414            .copied()
415            .min_by_key(|element| candidates_by_element[*element].len())
416        else {
417            return;
418        };
419        if candidates_by_element[element].is_empty() {
420            return;
421        }
422
423        for candidate_pos in &candidates_by_element[element] {
424            if chosen.contains(candidate_pos) {
425                continue;
426            }
427            let cover = &candidate_covers[*candidate_pos].1;
428            if cover.is_disjoint(&remaining) {
429                continue;
430            }
431
432            let mut next_remaining = remaining.clone();
433            for covered_element in cover {
434                next_remaining.remove(covered_element);
435            }
436            chosen.push(*candidate_pos);
437            Self::search_minimum_lmb_set_cover(
438                next_remaining,
439                chosen,
440                best,
441                candidate_covers,
442                candidates_by_element,
443            );
444            chosen.pop();
445        }
446    }
447
448    pub(crate) fn get_edge_subgraph(&self, edge: EdgeIndex) -> SuBitGraph {
449        let mut subgraph: SuBitGraph = self.underlying.empty_subgraph();
450
451        match self[&edge].1 {
452            HedgePair::Paired { source, sink } => {
453                subgraph.add(source);
454                subgraph.add(sink);
455            }
456            HedgePair::Unpaired { hedge, .. } => {
457                subgraph.add(hedge);
458            }
459            HedgePair::Split { .. } => unreachable!(),
460        }
461
462        subgraph
463    }
464
465    pub(crate) fn iter_loop_edges(
466        &self,
467    ) -> impl Iterator<Item = (HedgePair, EdgeIndex, EdgeData<&Edge>)> {
468        self.underlying.iter_edges().filter(|(_, edge_index, _)| {
469            self.loop_momentum_basis.edge_signatures[*edge_index]
470                .internal
471                .iter()
472                .any(|sign| sign.is_sign())
473        })
474    }
475
476    pub(crate) fn iter_non_loop_edges(
477        &self,
478    ) -> impl Iterator<Item = (HedgePair, EdgeIndex, EdgeData<&Edge>)> {
479        self.underlying.iter_edges().filter(|(_, edge_index, _)| {
480            self.loop_momentum_basis.edge_signatures[*edge_index]
481                .internal
482                .iter()
483                .all(|sign| sign.is_zero())
484        })
485    }
486
487    pub(crate) fn get_source_and_target(&self) -> (HedgeNode, HedgeNode) {
488        let mut source_nodes = AHashSet::new();
489        let mut target_nodes = AHashSet::new();
490
491        for (hedge_pair, _, _) in self.underlying.iter_edges_of(&self.initial_state_cut) {
492            match hedge_pair {
493                HedgePair::Split { source, sink, .. } => {
494                    let source_node = self.underlying.node_id(sink);
495                    let sink_node = self.underlying.node_id(source);
496
497                    source_nodes.insert(source_node);
498                    target_nodes.insert(sink_node);
499                }
500                _ => {
501                    unreachable!();
502                }
503            }
504        }
505
506        // They don't need to be the same!
507        // assert_eq!(
508        //     source_nodes.len(),
509        //     target_nodes.len(),
510        //     "The number of source and target nodes should be the same{}",
511        //     self.debug_dot()
512        // );
513
514        let source_node_vec = source_nodes.into_iter().collect_vec();
515        let target_node_vec = target_nodes.into_iter().collect_vec();
516
517        //println!("source nodes: {:?}", source_node_vec);
518        //println!("target nodes: {:?}", target_node_vec);
519        //panic!("stop");
520
521        let source_node = self
522            .underlying
523            .combine_to_single_hedgenode(&source_node_vec);
524
525        let target_node = self
526            .underlying
527            .combine_to_single_hedgenode(&target_node_vec);
528
529        (source_node, target_node)
530    }
531
532    pub(crate) fn edge_name_to_index(&self, name: &str) -> Option<EdgeIndex> {
533        for (_, edge_index, edge_data) in self.underlying.iter_edges() {
534            if edge_data.data.name.value == name {
535                return Some(edge_index);
536            }
537        }
538
539        None
540    }
541
542    pub(crate) fn get_initial_state_tree(&self) -> SuBitGraph {
543        let full_is_cut = self.initial_state_cut.as_subgraph();
544        let full_graph_without_initial_state_cut =
545            self.underlying.full_filter().subtract(&full_is_cut);
546        let mut result: SuBitGraph = self.underlying.empty_subgraph();
547
548        for (pair, edge_id, _) in self
549            .underlying
550            .iter_edges_of(&full_graph_without_initial_state_cut)
551        {
552            if let HedgePair::Paired { source, sink } = pair {
553                let loop_signature = &self.loop_momentum_basis.edge_signatures[edge_id];
554                if loop_signature.internal.iter().all(|sign| sign.is_zero()) {
555                    // A fixed-momentum bridge can also join two internal loop components.
556                    // Amputate only an endpoint attached to the initial-state cut.
557                    let initial_state_node = [source, sink]
558                        .into_iter()
559                        .map(|hedge| self.underlying.node_id(hedge))
560                        .find(|node| {
561                            self.underlying
562                                .iter_crown(*node)
563                                .any(|hedge| full_is_cut.includes(&hedge))
564                        });
565                    if let Some(node) = initial_state_node {
566                        for hedge in self.underlying.iter_crown(node) {
567                            result.add(hedge);
568                        }
569                    }
570                }
571            }
572        }
573
574        result
575    }
576
577    pub(crate) fn get_raised_edge_groups(&self) -> Vec<Vec<EdgeIndex>> {
578        let mut result = Vec::<Vec<EdgeIndex>>::new();
579
580        for (_, edge_index, _) in self.iter_loop_edges() {
581            let group_position = result.iter().position(|group| {
582                group.iter().all(|e| {
583                    self.loop_momentum_basis.edges_are_raised(*e, edge_index)
584                        && self[edge_index].particle.mass_atom() == self[*e].particle.mass_atom()
585                })
586            });
587
588            if let Some(pos) = group_position {
589                result[pos].push(edge_index);
590            } else {
591                result.push(vec![edge_index]);
592            }
593        }
594
595        result.iter_mut().for_each(|group| group.sort());
596
597        result
598    }
599
600    pub fn get_edges_in_initial_state_cut(&self) -> Vec<EdgeIndex> {
601        self.initial_state_cut
602            .iter_left_hedges()
603            .map(|hedge| self.underlying[&hedge])
604            .collect_vec()
605    }
606    pub(crate) fn classify_threshold_pinch(
607        &self,
608        cut_boundary_edges: &[EdgeIndex],
609        threshold_boundary_edges: &[EdgeIndex],
610    ) -> ThresholdPinchStatus {
611        // The caller has already intersected both cut boundaries with the connected sandwich.
612        // These edge sets therefore carry the same graph-relative information that the former
613        // `is_always_pinch` implementation derived from the two cuts and sandwich internally.
614        // Keep every mass representation symbolic here. A non-zero difference is not a
615        // model-independent conclusion; resolved values handle that case during generation.
616        let boundary_mass_sum = |edges: &[EdgeIndex]| {
617            edges
618                .iter()
619                .map(|edge_id| self[*edge_id].mass_atom())
620                .fold(Atom::new(), |sum, mass| sum + mass)
621        };
622
623        let mass_sums_are_identical = (boundary_mass_sum(cut_boundary_edges)
624            - boundary_mass_sum(threshold_boundary_edges))
625        .expand()
626        .is_zero();
627
628        if !mass_sums_are_identical {
629            return ThresholdPinchStatus::NotProven;
630        }
631
632        if cut_boundary_edges.len() > 1 && threshold_boundary_edges.len() > 1 {
633            ThresholdPinchStatus::CanBecome
634        } else {
635            ThresholdPinchStatus::Always
636        }
637    }
638}
639
640pub mod edge;
641pub mod parse;
642pub use autogen::Autogen;
643pub use edge::Edge;
644pub mod hedge_data;
645pub use hedge_data::HedgeData;
646pub(crate) mod three_d_source;
647pub(crate) use three_d_source::{ExactUvSubLmbFrame, FourDDenominator, GraphThreeDSource};
648pub mod vertex;
649pub use vertex::Vertex;
650
651pub mod lmb;
652pub use lmb::{LMBext, LmbError, LmbIndex, LoopMomentumBasis};
653
654#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
655pub struct GraphGroup {
656    master: usize,
657    remaining: Vec<usize>,
658}
659
660impl GraphGroup {
661    pub(crate) fn master(&self) -> usize {
662        self.master
663    }
664
665    pub(crate) fn iter_enumerated(&self) -> impl Iterator<Item = (GraphGroupPosition, usize)> + '_ {
666        self.into_iter()
667            .enumerate()
668            .map(|(i, graph_id)| (GraphGroupPosition(i), graph_id))
669    }
670
671    pub(crate) fn find_position(&self, graph_id: usize) -> Option<GraphGroupPosition> {
672        self.into_iter()
673            .position(|id| id == graph_id)
674            .map(GraphGroupPosition)
675    }
676}
677
678impl<'a> IntoIterator for &'a GraphGroup {
679    type Item = usize;
680    type IntoIter = std::iter::Chain<
681        std::array::IntoIter<usize, 1>,
682        std::iter::Copied<std::slice::Iter<'a, usize>>,
683    >;
684
685    fn into_iter(self) -> Self::IntoIter {
686        [self.master]
687            .into_iter()
688            .chain(self.remaining.iter().copied())
689    }
690}
691
692impl Length for GraphGroup {
693    fn len(&self) -> usize {
694        1 + self.remaining.len()
695    }
696}
697
698impl Index<GraphGroupPosition> for GraphGroup {
699    type Output = usize;
700
701    fn index(&self, index: GraphGroupPosition) -> &Self::Output {
702        if index.0 == 0 {
703            &self.master
704        } else {
705            &self.remaining[index.0 - 1]
706        }
707    }
708}
709
710define_index! {pub struct GraphGroupPosition;}
711
712#[derive(
713    Debug, Copy, Clone, PartialEq, Eq, bincode_trait_derive::Encode, bincode_trait_derive::Decode,
714)]
715#[trait_decode(trait = symbolica::state::HasStateMap)]
716pub struct ExternalConnection {
717    pub incoming_index: ExternalIndex,
718    pub outgoing_index: ExternalIndex,
719}
720
721pub fn get_cff_inverse_energy_product_impl<E, V, H, S: SubSetLike>(
722    graph: &HedgeGraph<E, V, H>,
723    subgraph: &S,
724    contract_edges: &[EdgeIndex],
725) -> Atom {
726    Atom::num(1)
727        / graph
728            .iter_edges_of(subgraph)
729            .filter_map(|(pair, edge_index, _)| match pair {
730                HedgePair::Paired { .. } => {
731                    if contract_edges.contains(&edge_index) {
732                        None
733                    } else {
734                        Some(-Atom::num(2) * ose_atom_from_index(edge_index))
735                    }
736                }
737                _ => None,
738            })
739            .reduce(|acc, x| acc * x)
740            .unwrap_or_else(|| Atom::num(1))
741}
742
743#[cfg(test)]
744mod tests {
745    use super::*;
746    use crate::{
747        dot, graph::parse::from_dot::IntoGraph, initialisation::test_initialise,
748        momentum::signature::LoopExtSignature,
749    };
750    use std::sync::OnceLock;
751
752    fn test_lmb(graph: &Graph, loop_edges: &[usize]) -> LoopMomentumBasis {
753        LoopMomentumBasis {
754            tree: graph.underlying.empty_subgraph(),
755            loop_edges: loop_edges.iter().copied().map(EdgeIndex::from).collect(),
756            ext_edges: Vec::new().into(),
757            edge_signatures: graph.underlying.new_edgevec(|_, _, _| {
758                LoopExtSignature::from((Vec::<isize>::new(), Vec::<isize>::new()))
759            }),
760        }
761    }
762
763    fn selector_test_graph() -> Graph {
764        static GRAPH: OnceLock<Graph> = OnceLock::new();
765        GRAPH
766            .get_or_init(|| {
767                test_initialise().unwrap();
768                dot!(
769                    digraph lmb_selector {
770                        edge [num=1 mass=0]
771                        node [num=1]
772                        A -> B [id=0]
773                        A -> B [id=1]
774                        A -> B [id=2]
775                        A -> B [id=3]
776                        A -> B [id=4 mass=1]
777                    }
778                )
779                .unwrap()
780            })
781            .clone()
782    }
783
784    #[test]
785    fn exact_lmb_set_cover_prefers_smallest_deterministic_solution() {
786        let candidates = [
787            (LmbIndex::from(0), BTreeSet::from([0])),
788            (LmbIndex::from(1), BTreeSet::from([1])),
789            (LmbIndex::from(2), BTreeSet::from([0, 1])),
790            (LmbIndex::from(3), BTreeSet::from([0, 1])),
791        ];
792
793        assert_eq!(
794            Graph::minimum_lmb_set_cover(2, &candidates),
795            vec![LmbIndex::from(2)]
796        );
797    }
798
799    #[test]
800    fn amplitude_lmb_selector_covers_massless_combinations_and_skips_duplicates() {
801        let mut graph = selector_test_graph();
802        graph.loop_momentum_basis = test_lmb(&graph, &[0, 1]);
803        let lmbs: TiVec<LmbIndex, LoopMomentumBasis> = vec![
804            test_lmb(&graph, &[0, 1]),
805            test_lmb(&graph, &[2, 3]),
806            test_lmb(&graph, &[0, 2]),
807            test_lmb(&graph, &[0, 1]),
808            test_lmb(&graph, &[0, 4]),
809        ]
810        .into();
811
812        let selected = graph.select_amplitude_lmb_channel_indices(
813            &lmbs,
814            false,
815            LmbChannelFallback::CurrentGraphBasis,
816        );
817
818        assert_eq!(
819            selected.into_iter().collect_vec(),
820            vec![LmbIndex::from(0), LmbIndex::from(1), LmbIndex::from(2)]
821        );
822    }
823
824    #[test]
825    fn amplitude_lmb_selector_override_keeps_all_bases() {
826        let graph = selector_test_graph();
827        let lmbs: TiVec<LmbIndex, LoopMomentumBasis> = vec![
828            test_lmb(&graph, &[0, 1]),
829            test_lmb(&graph, &[2, 3]),
830            test_lmb(&graph, &[0, 4]),
831        ]
832        .into();
833
834        let selected = graph.select_amplitude_lmb_channel_indices(
835            &lmbs,
836            true,
837            LmbChannelFallback::CurrentGraphBasis,
838        );
839
840        assert_eq!(
841            selected.into_iter().collect_vec(),
842            vec![LmbIndex::from(0), LmbIndex::from(1), LmbIndex::from(2)]
843        );
844    }
845
846    #[test]
847    fn amplitude_lmb_selector_falls_back_when_no_massless_combination_exists() {
848        let mut graph = selector_test_graph();
849        graph.loop_momentum_basis = test_lmb(&graph, &[0, 4]);
850        let lmbs: TiVec<LmbIndex, LoopMomentumBasis> =
851            vec![test_lmb(&graph, &[1, 4]), test_lmb(&graph, &[0, 4])].into();
852
853        let selected_current = graph.select_amplitude_lmb_channel_indices(
854            &lmbs,
855            false,
856            LmbChannelFallback::CurrentGraphBasis,
857        );
858        let selected_first = graph.select_amplitude_lmb_channel_indices(
859            &lmbs,
860            false,
861            LmbChannelFallback::FirstBasis,
862        );
863
864        assert_eq!(
865            selected_current.into_iter().collect_vec(),
866            vec![LmbIndex::from(1)]
867        );
868        assert_eq!(
869            selected_first.into_iter().collect_vec(),
870            vec![LmbIndex::from(0)]
871        );
872    }
873
874    #[test]
875    fn threshold_pinch_classification_distinguishes_fixed_and_multiparticle_boundaries() {
876        let graph = selector_test_graph();
877
878        assert_eq!(
879            graph.classify_threshold_pinch(&[EdgeIndex::from(0)], &[EdgeIndex::from(1)],),
880            ThresholdPinchStatus::Always,
881        );
882        assert_eq!(
883            graph.classify_threshold_pinch(
884                &[EdgeIndex::from(0), EdgeIndex::from(1)],
885                &[EdgeIndex::from(2), EdgeIndex::from(3)],
886            ),
887            ThresholdPinchStatus::CanBecome,
888        );
889        assert_eq!(
890            graph.classify_threshold_pinch(&[EdgeIndex::from(0)], &[EdgeIndex::from(4)],),
891            ThresholdPinchStatus::NotProven,
892        );
893    }
894}