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