1use std::fmt::Display;
2
3use bincode_trait_derive::{Decode, Encode};
4use derive_more::{From, Into};
5use itertools::Itertools;
6use linnet::half_edge::{
7 HedgeGraph, HedgeGraphError, NoData,
8 involution::{EdgeData, EdgeIndex, EdgeVec, Flow, Hedge, HedgePair, Orientation},
9 subgraph::{
10 Inclusion, InternalSubGraph, ModifySubSet, SuBitGraph, SubGraphLike, SubGraphOps,
11 SubSetLike, SubSetOps, cycle::SignedCycle,
12 },
13 tree::SimpleTraversalTree,
14};
15use serde::{Deserialize, Serialize};
16use symbolica::{
17 atom::{Atom, AtomCore, AtomOrView, FunctionBuilder, Symbol},
18 function,
19 id::Replacement,
20 printer::PrintOptions,
21 symbol,
22};
23use tabled::{builder::Builder, settings::Style};
24use thiserror::Error;
25use typed_index_collections::TiVec;
26
27use crate::{
28 integrands::process::{amplitude::AmplitudeGraphTerm, cross_section::CrossSectionGraphTerm},
29 momentum::{
30 SignOrZero,
31 sample::{ExternalIndex, LoopIndex},
32 signature::{LoopExtSignature, SignatureLike},
33 },
34 utils::{GS, W_},
35};
36
37use super::Graph;
38
39#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Encode, Decode)]
40pub struct LoopMomentumBasis {
41 pub tree: SuBitGraph,
42 pub loop_edges: TiVec<LoopIndex, EdgeIndex>,
43 pub ext_edges: TiVec<ExternalIndex, EdgeIndex>, pub edge_signatures: EdgeVec<LoopExtSignature>,
45}
46
47pub type LmbResult<T> = std::result::Result<T, LmbError>;
48
49#[derive(Debug, Error)]
50pub enum LmbError {
51 #[error(
52 "loop edges specified are not actual loop edges in the graph:{loop_edges}:\n{loop_edges_dot}"
53 )]
54 NotLoopEdges {
55 loop_edges: String,
56 loop_edges_dot: String,
57 },
58 #[error("externals\n{externals_dot}\ncontain non-subgraph nodes:\n{subgraph_dot}\n")]
59 ExternalsOutsideSubgraph {
60 externals_dot: String,
61 subgraph_dot: String,
62 },
63 #[error(
64 "external cover is empty for externals\n{externals_dot}\nand subgraph\n{subgraph_dot}\n"
65 )]
66 EmptyExternalCover {
67 externals_dot: String,
68 subgraph_dot: String,
69 },
70 #[error(
71 "forest guide\n{forest_guide_dot}\ndoes not cover the same nodes as subgraph\n{subgraph_dot}\n"
72 )]
73 ForestGuideMismatch {
74 forest_guide_dot: String,
75 subgraph_dot: String,
76 },
77 #[error(
78 "failed to trace external flow from hedge {hedge} to dependent root {root} in tree\n{tree_dot}\n"
79 )]
80 ExternalFlowPathMissing {
81 hedge: Hedge,
82 root: Hedge,
83 tree_dot: String,
84 },
85 #[error("failed to get cycle for source hedge {hedge} in tree:\n{tree_dot}\n")]
86 MissingCycle { hedge: Hedge, tree_dot: String },
87 #[error(
88 "no loop-momentum basis compatible with the parent basis was found for subgraph\n{subgraph_dot}\nparent basis\n{parent_lmb_dot}"
89 )]
90 NoCompatibleSubLmb {
91 subgraph_dot: String,
92 parent_lmb_dot: String,
93 },
94 #[error("failed to get cycle from tree:{is_circuit}\n{cycle_dot}\n{cover_dot}")]
95 InvalidCycle {
96 is_circuit: bool,
97 cycle_dot: String,
98 cover_dot: String,
99 },
100 #[error("split edge on full graph")]
101 SplitEdgeOnFullGraph,
102 #[error("failed to build edge-signature vector")]
103 EdgeSignatureVector(#[source] HedgeGraphError),
104 #[error(
105 "shrunken subgraph is not contained in outer graph\nouter:\n{outer_dot}\nshrunken:\n{shrunken_dot}"
106 )]
107 ShrunkenOutsideOuter {
108 outer_dot: String,
109 shrunken_dot: String,
110 },
111 #[error("invalid shrunken internal subgraph:\n{shrunken_dot}")]
112 InvalidShrunkenSubgraph { shrunken_dot: String },
113 #[error(
114 "failed to build loop momentum basis after shrinking subgraph\nouter:\n{outer_dot}\nshrunken:\n{shrunken_dot}\nremainder:\n{remainder_dot}"
115 )]
116 NoShrunkenLmb {
117 outer_dot: String,
118 shrunken_dot: String,
119 remainder_dot: String,
120 #[source]
121 source: Box<LmbError>,
122 },
123}
124
125impl Display for LoopMomentumBasis {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 let mut signature = Builder::new();
128 let mut title = vec!["edge".to_string()];
129 let mut table = Builder::new();
130
131 table.push_column(["Loop id", "edge id"]);
132 for (i, item) in self.loop_edges.iter_enumerated() {
133 title.push(format!("L{i} {item}"));
134 table.push_column(&[i.to_string(), item.to_string()]);
135 }
136 table.build().with(Style::rounded()).fmt(f)?;
137 writeln!(f)?;
138 let mut table = Builder::new();
139
140 table.push_column(["External id", "edge id"]);
141 for (i, item) in self.ext_edges.iter_enumerated() {
142 title.push(format!("Ext{i} {item}"));
143 table.push_column(&[i.to_string(), item.to_string()]);
144 }
145 table.build().with(Style::rounded()).fmt(f)?;
146 writeln!(f)?;
147 signature.push_record(&title);
148 for (i, s) in &self.edge_signatures {
149 let mut signs = s
150 .internal
151 .iter()
152 .chain(s.external.iter())
153 .map(|s| s.to_string())
154 .collect_vec();
155 signs.insert(0, i.to_string());
156 signature.push_record(signs);
157 }
158 signature.build().with(Style::rounded()).fmt(f)?;
159 writeln!(f)?;
160
161 Ok(())
162 }
163}
164
165impl LoopMomentumBasis {
166 pub(crate) fn ext_from(&self, eid: EdgeIndex) -> Option<ExternalIndex> {
167 self.ext_edges
168 .iter()
169 .position(|&e| e == eid)
170 .map(ExternalIndex)
171 }
172 pub(crate) fn swap_loops(&mut self, i: LoopIndex, j: LoopIndex) {
173 self.loop_edges.swap(i, j);
174 self.edge_signatures = self
175 .edge_signatures
176 .iter()
177 .map(|(eid, a)| {
178 let mut a = a.clone();
179 a.swap_loops(i, j);
180 (eid, a)
181 })
182 .collect();
183 }
184
185 pub(crate) fn put_loop_to_ext(&mut self, i: LoopIndex) {
186 let a = self.loop_edges.remove(i);
187 self.ext_edges.push(a);
189 self.edge_signatures
190 .iter_mut()
191 .for_each(|(_, s)| s.put_loop_to_ext(i));
192 }
193 pub(crate) fn canonicalize_external_order(&mut self, external_edge_order: &[EdgeIndex]) {
194 if external_edge_order.is_empty() {
195 return;
196 }
197
198 let current_ext_edges = self.ext_edges.clone();
199 let mut ordered_ext_edges = external_edge_order.to_vec();
200 ordered_ext_edges.extend(
201 current_ext_edges
202 .iter()
203 .copied()
204 .filter(|edge| !external_edge_order.contains(edge))
205 .sorted(),
206 );
207
208 if ordered_ext_edges == current_ext_edges.raw {
209 return;
210 }
211
212 for (_, signature) in self.edge_signatures.iter_mut() {
213 let mut expanded_external = vec![SignOrZero::Zero; ordered_ext_edges.len()];
214
215 for (old_slot, edge) in current_ext_edges.iter_enumerated() {
216 let Some(new_slot) = ordered_ext_edges
217 .iter()
218 .position(|ordered_edge| ordered_edge == edge)
219 else {
220 continue;
221 };
222 expanded_external[new_slot] = signature.external[old_slot];
223 }
224
225 signature.external = SignatureLike::from_iter(expanded_external);
226 }
227
228 self.ext_edges = ordered_ext_edges.into();
229 }
230}
231
232pub trait LMBext {
239 fn generate_loop_momentum_bases_of<S: SubGraphLike>(
245 &self,
246 subgraph: &S,
247 ) -> TiVec<LmbIndex, LoopMomentumBasis>
248 where
249 S::Base: SubGraphLike<Base = S::Base>
250 + SubSetOps
251 + Clone
252 + ModifySubSet<HedgePair>
253 + ModifySubSet<Hedge>;
254
255 fn generate_loop_momentum_bases(&self) -> TiVec<LmbIndex, LoopMomentumBasis>;
257
258 fn uv_wrapped_replacement<'a, S: SubSetLike, I>(
264 &self,
265 subgraph: &S,
266 lmb: &LoopMomentumBasis,
267 rep_args: &'a [I],
268 ) -> Vec<Replacement>
269 where
270 &'a I: Into<AtomOrView<'a>>,
271 {
272 self.replacement_impl(
273 |e, a, b| {
274 Replacement::new(
275 FunctionBuilder::new(GS.emr_mom)
276 .add_arg(usize::from(e))
277 .add_args(rep_args)
278 .finish()
279 .to_pattern(),
280 (a.replace(function!(GS.emr_mom, W_.x_))
281 .allow_new_wildcards_on_rhs(true)
282 .with(
283 FunctionBuilder::new(GS.emr_mom)
284 .add_arg(W_.x_)
285 .add_args(rep_args)
286 .finish(),
287 )
288 + b)
289 .to_pattern(),
290 )
291 },
292 subgraph,
293 lmb,
294 GS.emr_mom,
295 GS.emr_mom,
296 &[],
297 rep_args,
298 HedgePair::is_paired,
299 true,
300 )
301 }
302
303 fn uv_spatial_wrapped_replacement<'a, S: SubSetLike, I>(
308 &self,
309 subgraph: &S,
310 lmb: &LoopMomentumBasis,
311 rep_args: &'a [I],
312 ) -> Vec<Replacement>
313 where
314 &'a I: Into<AtomOrView<'a>>,
315 {
316 self.replacement_impl(
317 |e, a, b| {
318 Replacement::new(
319 FunctionBuilder::new(GS.emr_vec)
320 .add_arg(usize::from(e))
321 .add_args(rep_args)
322 .finish()
323 .to_pattern(),
324 (a.replace(function!(GS.emr_vec, W_.x_))
325 .allow_new_wildcards_on_rhs(true)
326 .with(
327 FunctionBuilder::new(GS.emr_vec)
328 .add_arg(W_.x_)
329 .add_args(rep_args)
330 .finish(),
331 )
332 + b)
333 .to_pattern(),
334 )
335 },
336 subgraph,
337 lmb,
338 GS.emr_vec,
339 GS.emr_vec,
340 &[],
341 rep_args,
342 HedgePair::is_paired,
343 true,
344 )
345 }
346
347 fn normal_emr_replacement<'a, S: SubSetLike, I>(
354 &self,
355 subgraph: &S,
356 lmb: &LoopMomentumBasis,
357 rep_args: &'a [I],
358 filter_pair: fn(&HedgePair) -> bool,
359 ) -> Vec<Replacement>
360 where
361 &'a I: Into<AtomOrView<'a>>,
362 {
363 self.replacement_impl(
364 |e, a, b| {
365 Replacement::new(
366 FunctionBuilder::new(GS.emr_mom)
367 .add_arg(usize::from(e))
368 .add_args(rep_args)
369 .finish()
370 .to_pattern(),
371 (a + b).to_pattern(),
372 )
373 },
374 subgraph,
375 lmb,
376 GS.emr_mom,
377 GS.emr_mom,
378 rep_args,
379 rep_args,
380 filter_pair,
381 true,
382 )
383 }
384
385 fn integrand_replacement<'a, S: SubSetLike, I>(
392 &self,
393 subgraph: &S,
394 lmb: &LoopMomentumBasis,
395 rep_args: &'a [I],
396 ) -> Vec<Replacement>
397 where
398 &'a I: Into<AtomOrView<'a>>,
399 {
400 self.replacement_impl(
401 |e, a, b| {
402 Replacement::new(
403 FunctionBuilder::new(GS.emr_mom)
404 .add_arg(usize::from(e))
405 .add_args(rep_args)
406 .finish()
407 .to_pattern(),
408 (a + b).to_pattern(),
409 )
410 },
411 subgraph,
412 lmb,
413 GS.loop_mom,
414 GS.external_mom,
415 rep_args,
416 rep_args,
417 no_filter,
418 false,
419 )
420 }
421
422 #[allow(clippy::too_many_arguments)]
438 fn replacement_impl<'a, S: SubSetLike, I>(
439 &self,
440 rep: impl Fn(EdgeIndex, Atom, Atom) -> Replacement,
441 subgraph: &S,
442 lmb: &LoopMomentumBasis,
443 loop_symbol: Symbol,
444 ext_symbol: Symbol,
445 loop_args: &'a [I],
446 ext_args: &'a [I],
447 filter_pair: fn(&HedgePair) -> bool,
448 emr_id: bool,
449 ) -> Vec<Replacement>
450 where
451 &'a I: Into<AtomOrView<'a>>;
452
453 fn lmb_impl<S: SubGraphLike + SubSetOps + ModifySubSet<HedgePair> + ModifySubSet<Hedge>>(
461 &self,
462 subgraph: &S,
463 tree: &S,
464 externals: S,
465 ) -> LmbResult<LoopMomentumBasis>
466 where
467 S::Base: ModifySubSet<Hedge> + SubGraphLike;
468
469 fn lmb_of<S: SubGraphLike<Base = SuBitGraph>>(&self, subgraph: &S) -> LoopMomentumBasis;
474
475 fn lmb(&self) -> LoopMomentumBasis;
477
478 fn shrunken_sub_lmb(
481 &self,
482 outer: &SuBitGraph,
483 shrunken: &InternalSubGraph,
484 externals: SuBitGraph,
485 ) -> LmbResult<LoopMomentumBasis>;
486
487 fn shrunken_lmb_of(&self, outer: &SuBitGraph, shrunken: &InternalSubGraph)
490 -> LoopMomentumBasis;
491
492 fn compatible_sub_lmb<S: SubGraphLike>(
498 &self,
499 subgraph: &S,
500 externals: S::Base,
501 lmb: &LoopMomentumBasis,
502 ) -> LoopMomentumBasis
503 where
504 S::Base: SubGraphLike<Base = S::Base>
505 + SubSetOps
506 + Clone
507 + ModifySubSet<HedgePair>
508 + ModifySubSet<Hedge>;
509
510 fn try_compatible_sub_lmb<S: SubGraphLike>(
515 &self,
516 subgraph: &S,
517 externals: S::Base,
518 lmb: &LoopMomentumBasis,
519 ) -> LmbResult<LoopMomentumBasis>
520 where
521 S::Base: SubGraphLike<Base = S::Base>
522 + SubSetOps
523 + Clone
524 + ModifySubSet<HedgePair>
525 + ModifySubSet<Hedge>,
526 {
527 Ok(self.compatible_sub_lmb(subgraph, externals, lmb))
528 }
529
530 fn cotree_lmb<
535 S: SubGraphLike + SubSetOps + SubGraphOps + ModifySubSet<HedgePair> + ModifySubSet<Hedge>,
536 >(
537 &self,
538 subgraph: &S,
539 cotree: &S,
540 externals: S,
541 ) -> LoopMomentumBasis
542 where
543 S::Base: ModifySubSet<Hedge> + SubGraphLike,
544 {
545 let tree = subgraph.subtract(cotree);
546 self.lmb_impl(subgraph, &tree, externals)
547 .unwrap_or_else(|err| panic!("Failed to build cotree loop momentum basis:\n{err}"))
548 }
549
550 fn empty_lmb(&self) -> LoopMomentumBasis;
552
553 fn dot_lmb_of<S: SubGraphLike>(&self, subgraph: &S, lmb: &LoopMomentumBasis) -> String;
556}
557
558pub(crate) fn no_filter(_pair: &HedgePair) -> bool {
559 true
560}
561
562impl<E, V, H> LMBext for HedgeGraph<E, V, H> {
563 fn empty_lmb(&self) -> LoopMomentumBasis {
564 LoopMomentumBasis {
565 tree: SuBitGraph::empty(0),
566 loop_edges: vec![].into(),
567 ext_edges: vec![].into(),
568 edge_signatures: self.new_edgevec(|_, _, _| LoopExtSignature::from((vec![], vec![]))),
569 }
570 }
571 fn lmb(&self) -> LoopMomentumBasis {
572 self.lmb_of(&self.full_filter())
573 }
574
575 fn shrunken_sub_lmb(
576 &self,
577 outer: &SuBitGraph,
578 shrunken: &InternalSubGraph,
579 externals: SuBitGraph,
580 ) -> LmbResult<LoopMomentumBasis> {
581 let graph_size = self.n_hedges();
582 let outer_dot = || {
583 if outer.size() == graph_size {
584 self.dot(outer)
585 } else {
586 format!(
587 "invalid outer size {}, expected {}; label {}",
588 outer.size(),
589 graph_size,
590 outer.string_label()
591 )
592 }
593 };
594 let shrunken_dot = || {
595 if shrunken.size() == graph_size {
596 self.dot(shrunken)
597 } else {
598 format!(
599 "invalid shrunken size {}, expected {}; label {}",
600 shrunken.size(),
601 graph_size,
602 shrunken.string_label()
603 )
604 }
605 };
606
607 if shrunken.size() != graph_size || !shrunken.valid(self) {
608 return Err(LmbError::InvalidShrunkenSubgraph {
609 shrunken_dot: shrunken_dot(),
610 });
611 }
612
613 if outer.size() != graph_size || !outer.includes(&shrunken.filter) {
614 return Err(LmbError::ShrunkenOutsideOuter {
615 outer_dot: outer_dot(),
616 shrunken_dot: shrunken_dot(),
617 });
618 }
619
620 if shrunken.is_empty() {
621 return self.lmb_impl(outer, outer, externals);
622 }
623
624 let remainder = outer.subtract(&shrunken.filter);
625 let contracted_externals = externals.subtract(&shrunken.filter);
626 let mut contracted = self.to_ref();
627
628 for component in self.connected_components(shrunken) {
629 let Some(root) = component.included_iter().next() else {
630 continue;
631 };
632 let node_data = &self[self.node_id(root)];
633 contracted.identify_nodes_of_subgraph_without_self_edges::<_, SuBitGraph>(
634 &component, node_data,
635 );
636 }
637
638 contracted
639 .lmb_impl(&remainder, &remainder, contracted_externals)
640 .map_err(|source| LmbError::NoShrunkenLmb {
641 outer_dot: outer_dot(),
642 shrunken_dot: shrunken_dot(),
643 remainder_dot: self.dot(&remainder),
644 source: Box::new(source),
645 })
646 }
647
648 fn shrunken_lmb_of(
649 &self,
650 outer: &SuBitGraph,
651 shrunken: &InternalSubGraph,
652 ) -> LoopMomentumBasis {
653 let externals = self.full_crown(outer);
654 self.shrunken_sub_lmb(outer, shrunken, externals)
655 .unwrap_or_else(|err| {
656 panic!("Failed to build shrunken-subgraph loop momentum basis:\n{err}")
657 })
658 }
659
660 fn dot_lmb_of<S: SubGraphLike>(&self, subgraph: &S, lmb: &LoopMomentumBasis) -> String {
661 let reps = self.normal_emr_replacement::<_, Atom>(subgraph, lmb, &[], no_filter);
662
663 let emrgraph = self.map_data_ref(
664 |_, _, _| "",
665 |_, e, _, _| {
666 EdgeData::new(
667 GS.emr_mom
668 .call_args([usize::from(e)])
669 .replace_multiple(&reps)
670 .printer(PrintOptions {
671 color_builtin_symbols: false,
672 color_top_level_sum: false,
673 bracket_level_colors: None,
674 ..Default::default()
675 })
676 .to_string(),
677 Orientation::Default,
678 )
679 },
680 |_, _| NoData {},
681 );
682 emrgraph.dot_label(subgraph)
683 }
684
685 fn lmb_of<S: SubGraphLike<Base = SuBitGraph>>(&self, subgraph: &S) -> LoopMomentumBasis {
686 if subgraph.is_empty() {
687 self.empty_lmb()
688 } else {
689 let external = self.full_crown(subgraph);
690 self.lmb_impl(subgraph.included(), subgraph.included(), external)
691 .unwrap_or_else(|err| {
692 panic!("Failed to build loop momentum basis for subgraph:\n{err}")
693 })
694 }
695 }
696
697 fn compatible_sub_lmb<S: SubGraphLike>(
698 &self,
699 subgraph: &S,
700 externals: S::Base,
701 lmb: &LoopMomentumBasis,
702 ) -> LoopMomentumBasis
703 where
704 S::Base: SubGraphLike<Base = S::Base>
705 + SubSetOps
706 + Clone
707 + ModifySubSet<HedgePair>
708 + ModifySubSet<Hedge>,
709 {
710 self.try_compatible_sub_lmb(subgraph, externals, lmb)
711 .unwrap_or_else(|err| {
712 panic!("Failed to build compatible subgraph loop momentum basis:\n{err}")
713 })
714 }
715
716 fn try_compatible_sub_lmb<S: SubGraphLike>(
717 &self,
718 subgraph: &S,
719 externals: S::Base,
720 lmb: &LoopMomentumBasis,
721 ) -> LmbResult<LoopMomentumBasis>
722 where
723 S::Base: SubGraphLike<Base = S::Base>
724 + SubSetOps
725 + Clone
726 + ModifySubSet<HedgePair>
727 + ModifySubSet<Hedge>,
728 {
729 let n_loops = self.cyclotomatic_number(subgraph);
730 if n_loops == 0 {
731 return Ok(self.empty_lmb());
732 }
733
734 let components = self.count_connected_components(subgraph);
736
737 for v in lmb
738 .loop_edges
739 .iter()
740 .filter(|e| {
741 let (_, p) = &self[*e];
742 subgraph.includes(p)
743 })
744 .combinations(n_loops)
745 {
746 let mut cut_subgraph = subgraph.included().clone();
747
748 for eid in v {
749 let (_, p) = &self[eid];
750 let HedgePair::Paired { source, sink } = p else {
751 continue;
752 };
753
754 if self.node_id(*source) == self.node_id(*sink) {
756 continue;
757 }
758 cut_subgraph.sub(*p);
759 }
760
761 if self.count_connected_components(&cut_subgraph) == components
762 && self.number_of_nodes_in_subgraph(&cut_subgraph)
763 == self.number_of_nodes_in_subgraph(subgraph)
764 {
765 return self.lmb_impl(subgraph.included(), &cut_subgraph, externals.clone());
768 }
769
770 }
772
773 let full_graph = self.full_filter();
774 let parent_lmb_has_full_loop_dimension =
775 lmb.loop_edges.len() == self.cyclotomatic_number(&full_graph);
776 let (subgraph_dot, parent_lmb_dot) = if parent_lmb_has_full_loop_dimension {
777 (
778 self.dot_lmb_of(subgraph, lmb),
779 self.dot_lmb_of(&full_graph, lmb),
780 )
781 } else {
782 (
785 self.dot(subgraph),
786 format!(
787 "parent loop edges {:?}; expected {} loops for\n{}",
788 lmb.loop_edges,
789 self.cyclotomatic_number(&full_graph),
790 self.dot(&full_graph),
791 ),
792 )
793 };
794
795 Err(LmbError::NoCompatibleSubLmb {
796 subgraph_dot,
797 parent_lmb_dot,
798 })
799 }
800
801 fn lmb_impl<S: SubGraphLike + SubSetOps + ModifySubSet<HedgePair> + ModifySubSet<Hedge>>(
803 &self,
804 subgraph: &S,
805 forest_guide: &S, mut externals: S, ) -> LmbResult<LoopMomentumBasis>
808 where
809 S::Base: ModifySubSet<Hedge> + SubGraphLike,
810 {
811 if subgraph.is_empty() {
819 return Ok(self.empty_lmb());
820 };
821
822 let mut not_seen = subgraph.clone();
823 let mut forest_edge: SuBitGraph = self.empty_subgraph();
824
825 let external_edge_order = self
828 .iter_edges_of(&externals)
829 .map(|(_, edge_id, _)| edge_id)
830 .unique()
831 .collect_vec();
832
833 let mut external_flows: TiVec<ExternalIndex, _> = vec![].into();
834 let mut ext_edges: TiVec<ExternalIndex, EdgeIndex> = vec![].into();
835
836 let mut loop_edges: TiVec<LoopIndex, EdgeIndex> = vec![].into();
837 let mut cycles = vec![];
838
839 loop {
840 let Some(mut root) = not_seen.included_iter().next() else {
841 break;
842 };
843
844 let tree = if let Some(external_root) = externals.included_iter().next() {
848 root = external_root;
849 let root_node = self.node_id(root);
850 let subgraph_tree =
851 SimpleTraversalTree::depth_first_traverse(self, subgraph, &root_node, None)
852 .map_err(|_| LmbError::ExternalsOutsideSubgraph {
853 externals_dot: self.dot(&externals),
854 subgraph_dot: self.dot(subgraph),
855 })?;
856
857 let external_cover = subgraph_tree.covers(&externals);
858
859 root = external_cover.included_iter().next_back().ok_or_else(|| {
860 LmbError::EmptyExternalCover {
861 externals_dot: self.dot(&externals),
862 subgraph_dot: self.dot(subgraph),
863 }
864 })?;
865 let root_node = self.node_id(root);
866 let tree =
867 SimpleTraversalTree::depth_first_traverse(self, forest_guide, &root_node, None)
868 .map_err(|_| LmbError::ForestGuideMismatch {
869 forest_guide_dot: self.dot(forest_guide),
870 subgraph_dot: self.dot(subgraph),
871 })?; debug_assert_eq!(
874 subgraph_tree.covers(subgraph),
875 tree.covers(subgraph),
876 "Forest guide \n{}\n,does not cover the same nodes as subgraph \n{}\n",
877 self.dot(forest_guide),
878 self.dot(subgraph)
879 );
880
881 for (p, e, _) in self.iter_edges_of(&external_cover) {
888 let mut path_to_dep: S = self.empty_subgraph();
889
890 match p {
891 HedgePair::Split {
892 source,
893 sink,
894 split,
895 } => {
896 let hedge = match split {
897 Flow::Sink => sink,
898 Flow::Source => source,
899 };
900 let ext_sign: SignOrZero = split.into();
901 path_to_dep.add(root);
902
903 if hedge != root {
904 let ext = tree.hedge_parent(hedge, self.as_ref());
905 if let Some(ext) = ext {
906 for h in tree.ancestor_iter_hedge(ext, self.as_ref()).step_by(2)
907 {
908 path_to_dep.add(h);
909 }
910 }
911 }
912 external_flows.push((ext_sign, path_to_dep));
913 ext_edges.push(e);
914 }
915 HedgePair::Unpaired { hedge, flow } => {
916 let ext_sign: SignOrZero = flow.into();
917
918 path_to_dep.add(root);
919 if hedge != root {
920 if self.node_id(hedge) == root_node {
921 } else {
922 let ext = tree.hedge_parent(hedge, self.as_ref()).ok_or_else(
923 || LmbError::ExternalFlowPathMissing {
924 hedge,
925 root,
926 tree_dot: self.dot(&tree.tree_subgraph),
927 },
928 )?;
929
930 for h in tree.ancestor_iter_hedge(ext, self.as_ref()).step_by(2)
931 {
932 path_to_dep.add(h);
933 }
934 }
935 }
936 ext_edges.push(e);
937 external_flows.push((ext_sign, path_to_dep));
938 }
939 HedgePair::Paired { source, .. } => {
940 path_to_dep.add(root);
941
942 let ext_sign: SignOrZero = Flow::Source.into();
943 if source != root {
944 let ext = tree.hedge_parent(source, self.as_ref());
945 if let Some(ext) = ext {
946 for h in tree.ancestor_iter_hedge(ext, self.as_ref()).step_by(2)
947 {
948 path_to_dep.add(h);
949 }
950 }
951 }
952 external_flows.push((ext_sign, path_to_dep));
953 ext_edges.push(e);
954 }
955 }
956 }
957
958 tree
959 } else {
960 let root_node = self.node_id(root);
961 if forest_guide.is_empty() {
962 SimpleTraversalTree::empty(self)
963 } else {
964 SimpleTraversalTree::depth_first_traverse(self, forest_guide, &root_node, None)
965 .map_err(|_| LmbError::ForestGuideMismatch {
966 forest_guide_dot: self.dot(forest_guide),
967 subgraph_dot: self.dot(subgraph),
968 })?
969 }
970 };
971
972 forest_edge.union_with(&tree.tree_subgraph);
973
974 let mut cover = tree.covers(subgraph);
975
976 for i in self.iter_crown(self.node_id(root)) {
977 if subgraph.includes(&i) {
978 cover.add(i);
979 }
980 }
981 for (p, e, _) in self.iter_edges_of(&cover) {
984 match p {
985 HedgePair::Paired { source, sink } => {
986 for h in self.iter_crown(self.node_id(sink)) {
987 not_seen.sub(h);
988 externals.sub(h);
989 }
990 for h in self.iter_crown(self.node_id(source)) {
991 not_seen.sub(h);
992 externals.sub(h);
993 }
994 if !tree.tree_subgraph.includes(&p) {
995 let cycle = tree.get_cycle(source, self).ok_or_else(|| {
996 LmbError::MissingCycle {
997 hedge: source,
998 tree_dot: self.dot(&tree.tree_subgraph),
999 }
1000 })?;
1001 let cycle_is_circuit = cycle.is_circuit(self);
1002 let cycle_dot = self.dot(&cycle.filter);
1003 cycles.push(SignedCycle::from_cycle(cycle, source, self).ok_or_else(
1004 || LmbError::InvalidCycle {
1005 is_circuit: cycle_is_circuit,
1006 cycle_dot,
1007 cover_dot: self.dot(&cover),
1008 },
1009 )?);
1010 loop_edges.push(e);
1011 }
1012 }
1013 HedgePair::Split {
1014 source,
1015 sink,
1016 split,
1017 } => match split {
1018 Flow::Sink => {
1019 for h in self.iter_crown(self.node_id(sink)) {
1020 not_seen.sub(h);
1021 externals.sub(h);
1022 }
1023 }
1024 Flow::Source => {
1025 for h in self.iter_crown(self.node_id(source)) {
1026 not_seen.sub(h);
1027 externals.sub(h);
1028 }
1029 }
1030 },
1031 HedgePair::Unpaired { hedge, .. } => {
1032 for h in self.iter_crown(self.node_id(hedge)) {
1033 not_seen.sub(h);
1034 externals.sub(h);
1035 }
1036 }
1037 }
1038 }
1039 }
1040 let signature = self
1050 .new_edgevec_from_iter(
1051 self.iter_edges()
1052 .map(|(p, eid, _)| -> LmbResult<_> {
1053 let mut internal = vec![];
1054 let mut external = vec![];
1055 let empty_internal = vec![SignOrZero::Zero; cycles.len()];
1060 let empty_external = vec![SignOrZero::Zero; external_flows.len()];
1061
1062 match p {
1063 HedgePair::Paired { source, sink } => {
1064 if subgraph.includes(&p) {
1065 for l in &cycles {
1066 if l.filter.includes(&source) {
1067 internal.push(SignOrZero::Plus);
1068 } else if l.filter.includes(&sink) {
1069 internal.push(SignOrZero::Minus);
1070 } else {
1071 internal.push(SignOrZero::Zero);
1072 }
1073 }
1074 } else {
1075 internal = empty_internal;
1076 }
1077 if subgraph.intersects(&p) {
1078 for (i, (s, e)) in external_flows.iter_enumerated() {
1079 if ext_edges[i] == eid {
1080 if e.includes(&source) || e.includes(&sink) {
1081 external.push(SignOrZero::Zero); } else {
1083 external.push(SignOrZero::Plus);
1084 }
1085 } else if e.includes(&source) {
1086 external.push(*s * SignOrZero::Minus);
1087 } else if e.includes(&sink) {
1088 external.push(*s * SignOrZero::Plus);
1089 } else {
1090 external.push(SignOrZero::Zero);
1091 }
1092 }
1093 } else {
1094 external = empty_external;
1095 }
1096 }
1097 HedgePair::Unpaired { hedge, flow } => {
1098 if subgraph.includes(&hedge) {
1099 for (i, (s, e)) in external_flows.iter_enumerated() {
1100 if ext_edges[i] == eid {
1101 if e.includes(&hedge) {
1102 external.push(SignOrZero::Zero); } else {
1104 external.push(SignOrZero::Plus);
1105 }
1106 } else if e.includes(&hedge) {
1107 match flow {
1108 Flow::Source => {
1109 external.push(*s * SignOrZero::Minus)
1110 }
1111 Flow::Sink => external.push(*s * SignOrZero::Plus),
1112 }
1113 } else {
1114 external.push(SignOrZero::Zero);
1115 }
1116 }
1117 } else {
1118 external = empty_external;
1119 if externals.includes(&hedge)
1120 && let Some((e, _)) =
1121 ext_edges.iter().find_position(|a| *a == &eid)
1122 {
1123 external[e] = SignOrZero::Plus;
1124 };
1125 }
1126 internal = empty_internal;
1127 }
1128 HedgePair::Split { .. } => {
1129 return Err(LmbError::SplitEdgeOnFullGraph);
1130 }
1131 }
1132
1133 Ok(LoopExtSignature {
1134 internal: SignatureLike::from_iter(internal),
1135 external: SignatureLike::from_iter(external),
1136 })
1137 })
1138 .collect::<LmbResult<Vec<_>>>()?,
1139 )
1140 .map_err(LmbError::EdgeSignatureVector)?;
1141
1142 let mut lmb = LoopMomentumBasis {
1143 tree: forest_edge,
1144 edge_signatures: signature,
1145 ext_edges,
1146 loop_edges,
1147 };
1148 lmb.canonicalize_external_order(&external_edge_order);
1149
1150 Ok(lmb)
1151 }
1152
1153 fn generate_loop_momentum_bases_of<S: SubGraphLike>(
1154 &self,
1155 subgraph: &S,
1156 ) -> TiVec<LmbIndex, LoopMomentumBasis>
1157 where
1158 S::Base: SubGraphLike<Base = S::Base>
1159 + SubSetOps
1160 + Clone
1161 + ModifySubSet<HedgePair>
1162 + ModifySubSet<Hedge>,
1163 {
1164 let Some(_) = subgraph.included_iter().next() else {
1165 return vec![].into();
1166 };
1167
1168 let mut lmbs: TiVec<LmbIndex, LoopMomentumBasis> = vec![].into();
1169
1170 let externals = self.full_crown(subgraph);
1171
1172 for s in self.all_spanning_forests_of(subgraph) {
1173 lmbs.push(
1175 self.lmb_impl(subgraph.included(), &s, externals.clone())
1176 .unwrap_or_else(|err| {
1177 panic!("Failed to build loop momentum basis from spanning forest:\n{err}")
1178 }),
1179 );
1180 }
1181 lmbs
1182 }
1183
1184 fn generate_loop_momentum_bases(&self) -> TiVec<LmbIndex, LoopMomentumBasis> {
1185 self.generate_loop_momentum_bases_of(&self.full_filter())
1186 }
1187
1188 #[allow(clippy::too_many_arguments)]
1189 fn replacement_impl<'a, S: SubSetLike, I>(
1190 &self,
1191 rep: impl Fn(EdgeIndex, Atom, Atom) -> Replacement,
1192 subgraph: &S,
1193 lmb: &LoopMomentumBasis,
1194 loop_symbol: Symbol,
1195 ext_symbol: Symbol,
1196 loop_args: &'a [I],
1197 ext_args: &'a [I],
1198 filter_pair: fn(&HedgePair) -> bool,
1199 emr_id: bool,
1200 ) -> Vec<Replacement>
1201 where
1202 &'a I: Into<AtomOrView<'a>>,
1203 {
1204 let mut reps = vec![];
1205 for (p, e, _) in self.iter_edges_of(subgraph) {
1206 if filter_pair(&p) {
1207 let loop_expr = lmb.loop_atom(e, loop_symbol, loop_args, emr_id);
1209 let external_expr = lmb.ext_atom(e, ext_symbol, ext_args, emr_id);
1210
1211 reps.push(rep(e, loop_expr, external_expr))
1215 }
1216 }
1217
1218 reps
1219 }
1220}
1221
1222pub trait LMBwithEdges<E: ?Sized> {
1223 fn lmb_with_loop_edges(&self, lmb_edges: &E) -> LmbResult<LoopMomentumBasis>;
1224}
1225
1226impl LMBwithEdges<SuBitGraph> for Graph {
1227 fn lmb_with_loop_edges(&self, lmb_edges: &SuBitGraph) -> LmbResult<LoopMomentumBasis> {
1228 let full_filter = self.full_filter();
1229 let externals = self.internal_crown(&full_filter);
1230 let cut_graph = full_filter.subtract(lmb_edges);
1231
1232 self.lmb_impl(&full_filter, &cut_graph, externals)
1233 }
1234}
1235impl LMBwithEdges<[EdgeIndex]> for Graph {
1236 fn lmb_with_loop_edges(&self, lmb_edges: &[EdgeIndex]) -> LmbResult<LoopMomentumBasis> {
1237 let mut lmb_edges_subgraph: SuBitGraph = self.empty_subgraph();
1238
1239 for e in lmb_edges.iter() {
1240 lmb_edges_subgraph.add(self[e].1);
1241 }
1242 self.lmb_with_loop_edges(&lmb_edges_subgraph)
1243 }
1244}
1245
1246impl LMBwithEdges<[&EdgeIndex]> for Graph {
1247 fn lmb_with_loop_edges(&self, lmb_edges: &[&EdgeIndex]) -> LmbResult<LoopMomentumBasis> {
1248 let mut lmb_edges_subgraph: SuBitGraph = self.empty_subgraph();
1249
1250 for e in lmb_edges.iter() {
1251 lmb_edges_subgraph.add(self[*e].1);
1252 }
1253 self.lmb_with_loop_edges(&lmb_edges_subgraph)
1254 }
1255}
1256
1257impl LMBwithEdges<[&EdgeIndex]> for CrossSectionGraphTerm {
1258 fn lmb_with_loop_edges(&self, lmb_edges: &[&EdgeIndex]) -> LmbResult<LoopMomentumBasis> {
1259 Ok(self
1260 .lmbs
1261 .iter_enumerated()
1262 .find(|(_, lmb)| lmb_edges.iter().all(|edge| lmb.loop_edges.contains(edge)))
1263 .ok_or(LmbError::NotLoopEdges {
1264 loop_edges: lmb_edges.iter().map(|a| a.to_string()).join(","),
1265 loop_edges_dot: self.graph.debug_dot(),
1266 })?
1267 .1
1268 .clone())
1269 }
1270}
1271
1272impl LMBwithEdges<[EdgeIndex]> for CrossSectionGraphTerm {
1273 fn lmb_with_loop_edges(&self, lmb_edges: &[EdgeIndex]) -> LmbResult<LoopMomentumBasis> {
1274 Ok(self
1275 .lmbs
1276 .iter_enumerated()
1277 .find(|(_, lmb)| lmb_edges.iter().all(|edge| lmb.loop_edges.contains(edge)))
1278 .ok_or(LmbError::NotLoopEdges {
1279 loop_edges: lmb_edges.iter().map(|a| a.to_string()).join(","),
1280 loop_edges_dot: self.graph.debug_dot(),
1281 })?
1282 .1
1283 .clone())
1284 }
1285}
1286
1287impl LMBwithEdges<[&EdgeIndex]> for AmplitudeGraphTerm {
1288 fn lmb_with_loop_edges(&self, lmb_edges: &[&EdgeIndex]) -> LmbResult<LoopMomentumBasis> {
1289 Ok(self
1290 .lmbs
1291 .iter_enumerated()
1292 .find(|(_, lmb)| lmb_edges.iter().all(|edge| lmb.loop_edges.contains(edge)))
1293 .ok_or(LmbError::NotLoopEdges {
1294 loop_edges: lmb_edges.iter().map(|a| a.to_string()).join(","),
1295 loop_edges_dot: self.graph.debug_dot(),
1296 })?
1297 .1
1298 .clone())
1299 }
1300}
1301
1302impl LMBwithEdges<[EdgeIndex]> for AmplitudeGraphTerm {
1303 fn lmb_with_loop_edges(&self, lmb_edges: &[EdgeIndex]) -> LmbResult<LoopMomentumBasis> {
1304 Ok(self
1305 .lmbs
1306 .iter_enumerated()
1307 .find(|(_, lmb)| lmb_edges.iter().all(|edge| lmb.loop_edges.contains(edge)))
1308 .ok_or(LmbError::NotLoopEdges {
1309 loop_edges: lmb_edges.iter().map(|a| a.to_string()).join(","),
1310 loop_edges_dot: self.graph.debug_dot(),
1311 })?
1312 .1
1313 .clone())
1314 }
1315}
1316
1317impl LMBext for Graph {
1318 fn dot_lmb_of<S: SubGraphLike>(&self, subgraph: &S, lmb: &LoopMomentumBasis) -> String {
1319 self.underlying.dot_lmb_of(subgraph, lmb)
1320 }
1321
1322 fn generate_loop_momentum_bases(&self) -> TiVec<LmbIndex, LoopMomentumBasis> {
1323 self.generate_loop_momentum_bases_of(&self.underlying.full_filter())
1324 }
1325
1326 fn lmb(&self) -> LoopMomentumBasis {
1327 self.lmb_of(&self.underlying.full_filter())
1328 }
1329
1330 fn shrunken_sub_lmb(
1331 &self,
1332 outer: &SuBitGraph,
1333 shrunken: &InternalSubGraph,
1334 externals: SuBitGraph,
1335 ) -> LmbResult<LoopMomentumBasis> {
1336 let mut lmb = self
1337 .underlying
1338 .shrunken_sub_lmb(outer, shrunken, externals)?;
1339 self.canonicalize_lmb_external_order(&mut lmb);
1340 Ok(lmb)
1341 }
1342
1343 fn shrunken_lmb_of(
1344 &self,
1345 outer: &SuBitGraph,
1346 shrunken: &InternalSubGraph,
1347 ) -> LoopMomentumBasis {
1348 let mut lmb = self.underlying.shrunken_lmb_of(outer, shrunken);
1349 self.canonicalize_lmb_external_order(&mut lmb);
1350 lmb
1351 }
1352
1353 fn empty_lmb(&self) -> LoopMomentumBasis {
1354 self.underlying.empty_lmb()
1355 }
1356 fn generate_loop_momentum_bases_of<S: SubGraphLike>(
1357 &self,
1358 subgraph: &S,
1359 ) -> TiVec<LmbIndex, LoopMomentumBasis>
1360 where
1361 S::Base: SubGraphLike<Base = S::Base>
1362 + SubSetOps
1363 + Clone
1364 + ModifySubSet<HedgePair>
1365 + ModifySubSet<Hedge>,
1366 {
1367 let Some(_) = subgraph.included_iter().next() else {
1368 return vec![].into();
1369 };
1370
1371 let externals = self.dummy_stripped_external_flows_of(subgraph);
1372 let mut lmbs: TiVec<LmbIndex, LoopMomentumBasis> = vec![].into();
1373 for forest in self.underlying.all_spanning_forests_of(subgraph) {
1374 let mut lmb = self
1375 .underlying
1376 .lmb_impl(subgraph.included(), &forest, externals.clone())
1377 .unwrap_or_else(|err| {
1378 panic!("Failed to build loop momentum basis from spanning forest:\n{err}")
1379 });
1380 self.canonicalize_lmb_external_order(&mut lmb);
1381 lmbs.push(lmb);
1382 }
1383 lmbs
1384 }
1385
1386 fn replacement_impl<'a, S: SubSetLike, I>(
1387 &self,
1388 rep: impl Fn(EdgeIndex, Atom, Atom) -> Replacement,
1389 subgraph: &S,
1390 lmb: &LoopMomentumBasis,
1391 loop_symbol: Symbol,
1392 ext_symbol: Symbol,
1393 loop_args: &'a [I],
1394 ext_args: &'a [I],
1395 filter_pair: fn(&HedgePair) -> bool,
1396 emr_id: bool,
1397 ) -> Vec<Replacement>
1398 where
1399 &'a I: Into<AtomOrView<'a>>,
1400 {
1401 self.underlying.replacement_impl(
1402 rep,
1403 subgraph,
1404 lmb,
1405 loop_symbol,
1406 ext_symbol,
1407 loop_args,
1408 ext_args,
1409 filter_pair,
1410 emr_id,
1411 )
1412 }
1413
1414 fn lmb_impl<S: SubGraphLike + SubSetOps + ModifySubSet<HedgePair> + ModifySubSet<Hedge>>(
1415 &self,
1416 subgraph: &S,
1417 tree: &S,
1418 externals: S,
1419 ) -> LmbResult<LoopMomentumBasis>
1420 where
1421 S::Base: ModifySubSet<Hedge> + SubGraphLike,
1422 {
1423 let mut lmb = self.underlying.lmb_impl(subgraph, tree, externals)?;
1424 self.canonicalize_lmb_external_order(&mut lmb);
1425 Ok(lmb)
1426 }
1427
1428 fn lmb_of<S: SubGraphLike<Base = SuBitGraph>>(&self, subgraph: &S) -> LoopMomentumBasis {
1429 let mut lmb = self.underlying.lmb_of(subgraph);
1430 self.canonicalize_lmb_external_order(&mut lmb);
1431 lmb
1432 }
1433
1434 fn compatible_sub_lmb<S: SubGraphLike>(
1435 &self,
1436 subgraph: &S,
1437 externals: S::Base,
1438 lmb: &LoopMomentumBasis,
1439 ) -> LoopMomentumBasis
1440 where
1441 S::Base: SubGraphLike<Base = S::Base>
1442 + SubSetOps
1443 + Clone
1444 + ModifySubSet<HedgePair>
1445 + ModifySubSet<Hedge>,
1446 {
1447 self.try_compatible_sub_lmb(subgraph, externals, lmb)
1448 .unwrap_or_else(|err| {
1449 panic!("Failed to build compatible subgraph loop momentum basis:\n{err}")
1450 })
1451 }
1452
1453 fn try_compatible_sub_lmb<S: SubGraphLike>(
1454 &self,
1455 subgraph: &S,
1456 externals: S::Base,
1457 lmb: &LoopMomentumBasis,
1458 ) -> LmbResult<LoopMomentumBasis>
1459 where
1460 S::Base: SubGraphLike<Base = S::Base>
1461 + SubSetOps
1462 + Clone
1463 + ModifySubSet<HedgePair>
1464 + ModifySubSet<Hedge>,
1465 {
1466 let mut sub_lmb = self
1467 .underlying
1468 .try_compatible_sub_lmb(subgraph, externals, lmb)?;
1469 self.canonicalize_lmb_external_order(&mut sub_lmb);
1470 Ok(sub_lmb)
1471 }
1472}
1473
1474impl LMBext for &Graph {
1475 fn dot_lmb_of<S: SubGraphLike>(&self, subgraph: &S, lmb: &LoopMomentumBasis) -> String {
1476 self.underlying.dot_lmb_of(subgraph, lmb)
1477 }
1478
1479 fn lmb(&self) -> LoopMomentumBasis {
1480 self.lmb_of(&self.underlying.full_filter())
1481 }
1482
1483 fn shrunken_sub_lmb(
1484 &self,
1485 outer: &SuBitGraph,
1486 shrunken: &InternalSubGraph,
1487 externals: SuBitGraph,
1488 ) -> LmbResult<LoopMomentumBasis> {
1489 let mut lmb = self
1490 .underlying
1491 .shrunken_sub_lmb(outer, shrunken, externals)?;
1492 self.canonicalize_lmb_external_order(&mut lmb);
1493 Ok(lmb)
1494 }
1495
1496 fn shrunken_lmb_of(
1497 &self,
1498 outer: &SuBitGraph,
1499 shrunken: &InternalSubGraph,
1500 ) -> LoopMomentumBasis {
1501 let mut lmb = self.underlying.shrunken_lmb_of(outer, shrunken);
1502 self.canonicalize_lmb_external_order(&mut lmb);
1503 lmb
1504 }
1505
1506 fn empty_lmb(&self) -> LoopMomentumBasis {
1507 self.underlying.empty_lmb()
1508 }
1509 fn generate_loop_momentum_bases_of<S: SubGraphLike>(
1510 &self,
1511 subgraph: &S,
1512 ) -> TiVec<LmbIndex, LoopMomentumBasis>
1513 where
1514 S::Base: SubGraphLike<Base = S::Base>
1515 + SubSetOps
1516 + Clone
1517 + ModifySubSet<HedgePair>
1518 + ModifySubSet<Hedge>,
1519 {
1520 let Some(_) = subgraph.included_iter().next() else {
1521 return vec![].into();
1522 };
1523
1524 let externals = self.dummy_stripped_external_flows_of(subgraph);
1525 let mut lmbs: TiVec<LmbIndex, LoopMomentumBasis> = vec![].into();
1526 for forest in self.underlying.all_spanning_forests_of(subgraph) {
1527 let mut lmb = self
1528 .underlying
1529 .lmb_impl(subgraph.included(), &forest, externals.clone())
1530 .unwrap_or_else(|err| {
1531 panic!("Failed to build loop momentum basis from spanning forest:\n{err}")
1532 });
1533 self.canonicalize_lmb_external_order(&mut lmb);
1534 lmbs.push(lmb);
1535 }
1536 lmbs
1537 }
1538
1539 fn generate_loop_momentum_bases(&self) -> TiVec<LmbIndex, LoopMomentumBasis> {
1540 self.generate_loop_momentum_bases_of(&self.underlying.full_filter())
1541 }
1542
1543 fn replacement_impl<'a, S: SubSetLike, I>(
1544 &self,
1545 rep: impl Fn(EdgeIndex, Atom, Atom) -> Replacement,
1546 subgraph: &S,
1547 lmb: &LoopMomentumBasis,
1548 loop_symbol: Symbol,
1549 ext_symbol: Symbol,
1550 loop_args: &'a [I],
1551 ext_args: &'a [I],
1552 filter_pair: fn(&HedgePair) -> bool,
1553 emr_id: bool,
1554 ) -> Vec<Replacement>
1555 where
1556 &'a I: Into<AtomOrView<'a>>,
1557 {
1558 self.underlying.replacement_impl(
1559 rep,
1560 subgraph,
1561 lmb,
1562 loop_symbol,
1563 ext_symbol,
1564 loop_args,
1565 ext_args,
1566 filter_pair,
1567 emr_id,
1568 )
1569 }
1570
1571 fn lmb_impl<S: SubGraphLike + SubSetOps + ModifySubSet<HedgePair> + ModifySubSet<Hedge>>(
1572 &self,
1573 subgraph: &S,
1574 tree: &S,
1575 externals: S,
1576 ) -> LmbResult<LoopMomentumBasis>
1577 where
1578 S::Base: ModifySubSet<Hedge> + SubGraphLike,
1579 {
1580 let mut lmb = self.underlying.lmb_impl(subgraph, tree, externals)?;
1581 self.canonicalize_lmb_external_order(&mut lmb);
1582 Ok(lmb)
1583 }
1584
1585 fn lmb_of<S: SubGraphLike<Base = SuBitGraph>>(&self, subgraph: &S) -> LoopMomentumBasis {
1586 let mut lmb = self.underlying.lmb_of(subgraph);
1587 self.canonicalize_lmb_external_order(&mut lmb);
1588 lmb
1589 }
1590
1591 fn compatible_sub_lmb<S: SubGraphLike>(
1592 &self,
1593 subgraph: &S,
1594 externals: S::Base,
1595 lmb: &LoopMomentumBasis,
1596 ) -> LoopMomentumBasis
1597 where
1598 S::Base: SubGraphLike<Base = S::Base>
1599 + SubSetOps
1600 + Clone
1601 + ModifySubSet<HedgePair>
1602 + ModifySubSet<Hedge>,
1603 {
1604 self.try_compatible_sub_lmb(subgraph, externals, lmb)
1605 .unwrap_or_else(|err| {
1606 panic!("Failed to build compatible subgraph loop momentum basis:\n{err}")
1607 })
1608 }
1609
1610 fn try_compatible_sub_lmb<S: SubGraphLike>(
1611 &self,
1612 subgraph: &S,
1613 externals: S::Base,
1614 lmb: &LoopMomentumBasis,
1615 ) -> LmbResult<LoopMomentumBasis>
1616 where
1617 S::Base: SubGraphLike<Base = S::Base>
1618 + SubSetOps
1619 + Clone
1620 + ModifySubSet<HedgePair>
1621 + ModifySubSet<Hedge>,
1622 {
1623 let mut sub_lmb = self
1624 .underlying
1625 .try_compatible_sub_lmb(subgraph, externals, lmb)?;
1626 self.canonicalize_lmb_external_order(&mut sub_lmb);
1627 Ok(sub_lmb)
1628 }
1629}
1630
1631impl LoopMomentumBasis {
1632 pub fn map_to(&self, other: &Self) -> Vec<Atom> {
1633 let selfmom = symbol!("K");
1634 let othermom = symbol!("L");
1635 let mut sys = vec![];
1636
1637 for (l, e) in self.loop_edges.iter_enumerated() {
1638 sys.push(
1639 other.loop_atom::<Atom>(*e, othermom, &[], false)
1640 + other.ext_atom::<Atom>(*e, othermom, &[], false)
1641 - selfmom.call_args([l.0]),
1642 )
1643 }
1644
1645 let mut vars = vec![];
1646
1647 for (l, _) in other.loop_edges.iter_enumerated() {
1648 vars.push(othermom.call_args([l.0]))
1649 }
1650
1651 Atom::solve_linear_system::<u8, _, _>(&sys, &vars).unwrap()
1652 }
1653 pub fn loop_atom<'a, I>(
1670 &self,
1671 edge_id: EdgeIndex,
1672 mom_symbol: Symbol,
1673 additional_args: &'a [I],
1674 emr_id: bool,
1675 ) -> Atom
1676 where
1677 &'a I: Into<AtomOrView<'a>>,
1678 {
1679 self.edge_signatures[edge_id].loop_atom(mom_symbol, additional_args, |l| {
1680 Atom::num(if emr_id {
1681 usize::from(self.loop_edges[l])
1682 } else {
1683 usize::from(l)
1684 } as i64)
1685 })
1686 }
1687
1688 pub fn ext_atom<'a, I>(
1689 &self,
1690 edge_id: EdgeIndex,
1691 mom_symbol: Symbol,
1692 additional_args: &'a [I],
1693 emr_id: bool,
1694 ) -> Atom
1695 where
1696 &'a I: Into<AtomOrView<'a>>,
1697 {
1698 self.edge_signatures[edge_id].ext_atom(mom_symbol, additional_args, |l| {
1699 Atom::num(if emr_id {
1700 usize::from(self.ext_edges[l])
1701 } else {
1702 usize::from(l)
1703 } as i64)
1704 })
1705 }
1706
1707 pub(crate) fn edges_are_raised(&self, edge_1: EdgeIndex, edge_2: EdgeIndex) -> bool {
1721 let sig_1 = &self.edge_signatures[edge_1];
1722 let sig_2 = &self.edge_signatures[edge_2];
1723 sig_1.equality_up_to_sign(sig_2)
1724 }
1725}
1726
1727#[derive(
1728 Debug,
1729 Clone,
1730 Serialize,
1731 Deserialize,
1732 bincode::Encode,
1733 bincode::Decode,
1734 Copy,
1735 Hash,
1736 From,
1737 Into,
1738 Eq,
1739 PartialEq,
1740 Ord,
1741 PartialOrd,
1742)]
1743pub struct LmbIndex(usize);
1744
1745#[cfg(test)]
1746pub mod test {
1747
1748 use insta::assert_snapshot;
1749 use linnet::{
1750 half_edge::{
1751 involution::{EdgeIndex, Hedge},
1752 subgraph::{Inclusion, InternalSubGraph, ModifySubSet, SuBitGraph, SubSetOps},
1753 },
1754 parser::DotGraph,
1755 };
1756
1757 use crate::{
1758 dot,
1759 graph::{FeynmanGraph, Graph, LMBext, LmbError, parse::IntoGraph},
1760 initialisation::test_initialise,
1761 momentum::SignOrZero,
1762 };
1763
1764 static SHRUNKEN_LMB_TEST_INIT: std::sync::Once = std::sync::Once::new();
1765 static SHRUNKEN_LMB_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1766
1767 #[test]
1768 fn lmb_for_dummy() {
1769 test_initialise().unwrap();
1770 let gs: Vec<Graph> = dot!(
1771 digraph dxda{
1772 ext [style=invis]
1773 node[num=1]
1774 ext->v1:0[id=0 is_dummy=true]
1775 ext->v1:1[id=1 ]
1776 ext->v1:2[id=2 ]
1777 }
1778
1779 digraph aa{
1780 ext [style=invis]
1781 node[num=1]
1782 ext->v1:0[id=0 is_dummy=true]
1783 ext->v1:1[id=1 is_dummy=true]
1784 v1->v2
1785 ext->v2:2[id=2 ]
1786 }
1787 )
1788 .unwrap();
1789
1790 for g in gs {
1791 insta::with_settings!({
1792 snapshot_suffix=>g.name.to_string(),
1793 }, {
1794 insta::assert_snapshot!(g.dot_lmb_of(&g.full_filter(), &g.loop_momentum_basis));
1795 });
1796 }
1797 }
1798
1799 #[test]
1800 fn generated_lmbs_do_not_use_dummy_external_carriers() {
1801 test_initialise().unwrap();
1802 let g: Graph = dot!(digraph{
1803 ext [style=invis]
1804 edge[num=1 mass=1]
1805 node[num=1]
1806 ext->v1:0[id=0 is_dummy=true]
1807 ext->v1:1[id=1]
1808 v1->v2[id=2]
1809 v2->v1[id=3]
1810 ext->v2:2[id=4]
1811 })
1812 .unwrap();
1813
1814 let lmbs = g.generate_loop_momentum_bases_of(&g.no_dummy());
1815 assert!(!lmbs.is_empty());
1816
1817 for lmb in lmbs {
1818 assert_eq!(
1819 lmb.ext_edges[crate::momentum::sample::ExternalIndex(0)],
1820 EdgeIndex::from(0)
1821 );
1822
1823 for edge_id in [1, 2, 3, 4].map(EdgeIndex::from) {
1824 assert_eq!(
1825 lmb.edge_signatures[edge_id].external
1826 [crate::momentum::sample::ExternalIndex(0)],
1827 SignOrZero::Zero,
1828 "non-dummy edge {edge_id} uses the dummy external as a generated LMB carrier"
1829 );
1830 }
1831 }
1832 }
1833
1834 #[test]
1835 fn complicated() {
1836 test_initialise().unwrap();
1837 let g: Graph = dot!(digraph{
1838
1839 edge[num=1 mass=1]
1840 node[num=1]
1841
1842 e[style=invis]
1843
1844 a->c
1845
1846 a->e
1847 a->e
1848 b->c
1849 d->c
1850 d->e
1851 b->e
1852 a->b->d->a
1853 b->b1
1854 b1->b2
1855 b1->b2
1856 b1->b2
1857 b2->e
1858 })
1859 .unwrap();
1860 assert_snapshot!(g.dot_lmb_of(&g.full_filter(), &g.loop_momentum_basis));
1861 assert_snapshot!(&g.loop_momentum_basis.to_string());
1862 let _g = g.generate_loop_momentum_bases_of(&g.full_filter());
1863 }
1864 #[test]
1865 fn disconnected() {
1866 test_initialise().unwrap();
1867 let g: Graph = dot!(digraph{
1868 e [style=invis]
1870 edge[num=1 mass=1]
1871 node[num=1]
1872 e->v1
1873 e->v1
1874 e->v1
1875 e->v1
1876 v1->v1
1877
1878 e->v2
1879 e->v2
1880 e->v2
1881
1882 v3->v3
1883 v3->v4
1884 v4->v4
1885
1886
1887 e->v5
1888 e->v5->v6
1889 v6->v7
1890 v6->v7
1891 e->v7
1892 })
1893 .unwrap();
1894 assert_snapshot!(g.dot_lmb_of(&g.full_filter(), &g.loop_momentum_basis));
1895 assert_snapshot!(&g.loop_momentum_basis.to_string());
1896
1897 let g: Graph = dot!(digraph{
1898
1899 edge[num=1 mass=1]
1900 node[num=1]
1901 a->b
1902 a->b
1903 a->b
1904
1905
1906 c->e
1907 e->d
1908 c->d
1909 c->d
1910 })
1911 .unwrap();
1912
1913 assert_eq!(g.generate_loop_momentum_bases().len(), 15);
1914 }
1915 #[test]
1916 fn subgraph_with_exts_in_loop() {
1917 test_initialise().unwrap();
1918 let g: Graph = dot!(digraph{
1919 edge[num=1 mass=1]
1920 node[num=1]
1921 v3:0->v4:1
1922 v3:3->v4:2
1923 v3:4->v4:5
1924 })
1925 .unwrap();
1926
1927 let mut sub = g.full_filter();
1928 sub.sub(Hedge(0));
1929 sub.sub(Hedge(1));
1930 let lmb = g.lmb_of(&sub);
1931 assert_snapshot!(g.dot_lmb_of(&g.full_filter(), &lmb));
1932 assert_snapshot!(&lmb.to_string());
1933
1934 let lmb = g.lmb_impl(&sub, &sub, g.full_crown(&sub)).unwrap();
1935 assert_snapshot!(g.dot_lmb_of(&g.full_filter(), &lmb));
1936 assert_snapshot!(&lmb.to_string());
1937 }
1938
1939 #[test]
1940 fn compatible_sub_lmb() {
1941 test_initialise().unwrap();
1942 let g: DotGraph = linnet::dot!(
1943 digraph{
1944
1945 node[num=1]
1946
1947 v1->v2
1948 v2->v3
1949 v1->v2
1950 v2->v3
1951 v3:s->v1:s
1952 v1:s->v3:s
1953
1954 }
1955 )
1956 .unwrap();
1957
1958 let subgraph: SuBitGraph = g.compass_subgraph(Some(dot_parser::ast::CompassPt::S));
1959
1960 let lmb = g.lmb_of(&subgraph);
1961 assert_snapshot!(g.dot_lmb_of(&subgraph, &lmb));
1962 assert_snapshot!(lmb.to_string());
1963
1964 let mut incompatible_parent_lmb = lmb.clone();
1965 incompatible_parent_lmb.loop_edges.clear();
1966 let unavailable =
1967 g.try_compatible_sub_lmb(&subgraph, g.full_crown(&subgraph), &incompatible_parent_lmb);
1968 assert!(matches!(
1969 unavailable,
1970 Err(LmbError::NoCompatibleSubLmb { .. })
1971 ));
1972
1973 let g: DotGraph = linnet::dot!(
1974 digraph dxda{
1975 e1 [style=invis]
1976 e2 [style=invis]
1977 e3 [style=invis]
1978 e4 [style=invis]
1979 node[num=1]
1980 e1->v1:0:n[id=0]
1981 e2->v1:1[id=1 ]
1982 v1:s->v2:s
1983 v2:s->v3:s
1984 v3->v1
1985 v1:s->v3:s
1986 e4->v3
1987 e3->v2:2[id=2 ]
1988 }
1989
1990 )
1991 .unwrap();
1992
1993 let subgraph: SuBitGraph = g.compass_subgraph(Some(dot_parser::ast::CompassPt::S));
1994
1995 let dummy: SuBitGraph = g.compass_subgraph(Some(dot_parser::ast::CompassPt::N));
1996 let non_dummy = g.full_filter().subtract(&dummy);
1997 let lmb = g.lmb_of(&non_dummy);
1998 let non_dummy_sub_ext = g.full_crown(&subgraph).subtract(&dummy);
1999
2000 let sub_lmb = g
2001 .try_compatible_sub_lmb(&subgraph, non_dummy_sub_ext, &lmb)
2002 .unwrap();
2003
2004 assert_snapshot!(g.dot_lmb_of(&non_dummy, &lmb));
2005 assert_snapshot!(g.dot_lmb_of(&subgraph, &sub_lmb));
2006
2007 let g: DotGraph = linnet::dot!(
2008 digraph {
2009 0:0:s -> 0:1:s [id=0];
2010 0:2 -> 1:3 [id=1];
2011 1:4:s -> 1:5:s [id=2];
2012 }
2013
2014 )
2015 .unwrap();
2016
2017 let subgraph: SuBitGraph = g.compass_subgraph(Some(dot_parser::ast::CompassPt::S));
2018 let non_dummy = g.full_filter();
2019 let lmb = g.lmb_of(&non_dummy);
2020 let sub_lmb = g.compatible_sub_lmb(&subgraph, non_dummy, &lmb);
2021 assert_snapshot!(g.dot_lmb_of(&subgraph, &sub_lmb));
2022 }
2023
2024 #[test]
2025 fn shrunken_connected_subgraph() {
2026 let _guard = SHRUNKEN_LMB_TEST_LOCK.lock().unwrap();
2027 SHRUNKEN_LMB_TEST_INIT.call_once(|| test_initialise().unwrap());
2028 let g: Graph = dot!(digraph{
2029 edge[num=1 mass=1]
2030 node[num=1]
2031
2032 a:0->b:1[id=0]
2033 b:2->c:3[id=1]
2034 c:4->a:5[id=2]
2035 })
2036 .unwrap();
2037
2038 let outer = g.full_filter();
2039 let mut shrunken_filter: SuBitGraph = g.empty_subgraph();
2040 let shrunken_edge = EdgeIndex::from(0);
2041 shrunken_filter.add(g[&shrunken_edge].1);
2042 let shrunken =
2043 InternalSubGraph::try_new(shrunken_filter, &g.underlying).expect("valid subgraph");
2044 let remainder = outer.subtract(&shrunken.filter);
2045
2046 let lmb = g.shrunken_lmb_of(&outer, &shrunken);
2047
2048 assert!(
2049 !lmb.loop_edges
2050 .iter()
2051 .any(|edge| shrunken.filter.includes(&g[edge].1))
2052 );
2053 assert_snapshot!(g.dot_lmb_of(&remainder, &lmb));
2054 }
2055
2056 #[test]
2057 fn shrunken_disconnected_subgraph() {
2058 let _guard = SHRUNKEN_LMB_TEST_LOCK.lock().unwrap();
2059 SHRUNKEN_LMB_TEST_INIT.call_once(|| test_initialise().unwrap());
2060 let g: Graph = dot!(digraph{
2061 edge[num=1 mass=1]
2062 node[num=1]
2063
2064 a:0->b:1[id=0]
2065 b:2->c:3[id=1]
2066 c:4->a:5[id=2]
2067
2068 d:6->e:7[id=3]
2069 e:8->f:9[id=4]
2070 f:10->d:11[id=5]
2071
2072 c:12->d:13[id=6]
2073 })
2074 .unwrap();
2075
2076 let outer = g.full_filter();
2077 let mut shrunken_filter: SuBitGraph = g.empty_subgraph();
2078 for edge in [EdgeIndex::from(0), EdgeIndex::from(3)] {
2079 shrunken_filter.add(g[&edge].1);
2080 }
2081 let shrunken =
2082 InternalSubGraph::try_new(shrunken_filter, &g.underlying).expect("valid subgraph");
2083 let remainder = outer.subtract(&shrunken.filter);
2084
2085 let lmb = g.shrunken_lmb_of(&outer, &shrunken);
2086
2087 assert!(
2088 !lmb.loop_edges
2089 .iter()
2090 .any(|edge| shrunken.filter.includes(&g[edge].1))
2091 );
2092 assert_snapshot!(g.dot_lmb_of(&remainder, &lmb));
2093 }
2094
2095 #[test]
2096 fn shrunken_edge_outside_outer_errors() {
2097 let _guard = SHRUNKEN_LMB_TEST_LOCK.lock().unwrap();
2098 SHRUNKEN_LMB_TEST_INIT.call_once(|| test_initialise().unwrap());
2099 let g: Graph = dot!(digraph{
2100 edge[num=1 mass=1]
2101 node[num=1]
2102
2103 a:0->b:1[id=0]
2104 b:2->c:3[id=1]
2105 c:4->a:5[id=2]
2106 })
2107 .unwrap();
2108
2109 let mut shrunken_filter: SuBitGraph = g.empty_subgraph();
2110 let shrunken_edge = EdgeIndex::from(0);
2111 shrunken_filter.add(g[&shrunken_edge].1);
2112 let shrunken =
2113 InternalSubGraph::try_new(shrunken_filter, &g.underlying).expect("valid subgraph");
2114 let outer = g.full_filter().subtract(&shrunken.filter);
2115
2116 let result = g.shrunken_sub_lmb(&outer, &shrunken, g.full_crown(&outer));
2117
2118 assert!(matches!(result, Err(LmbError::ShrunkenOutsideOuter { .. })));
2119 }
2120}