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};
24use 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 pub initial_state_cut: OrientedCut,
70 pub polarizations: Vec<(PolDef, Atom)>,
71 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
81pub 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 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 let source_node_vec = source_nodes.into_iter().collect_vec();
515 let target_node_vec = target_nodes.into_iter().collect_vec();
516
517 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 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 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}