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 poly::PolyVariable,
21 printer::PrintOptions,
22 symbol,
23};
24use tabled::{builder::Builder, settings::Style};
25use thiserror::Error;
26use typed_index_collections::TiVec;
27
28use crate::{
29 integrands::process::{amplitude::AmplitudeGraphTerm, cross_section::CrossSectionGraphTerm},
30 momentum::{
31 SignOrZero,
32 sample::{ExternalIndex, LoopIndex},
33 signature::{LoopExtSignature, SignatureLike},
34 },
35 utils::{GS, W_},
36};
37
38use super::Graph;
39
40#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Encode, Decode)]
41pub struct LoopMomentumBasis {
42 pub tree: SuBitGraph,
43 pub loop_edges: TiVec<LoopIndex, EdgeIndex>,
44 pub ext_edges: TiVec<ExternalIndex, EdgeIndex>, pub edge_signatures: EdgeVec<LoopExtSignature>,
46}
47
48pub type LmbResult<T> = std::result::Result<T, LmbError>;
49
50#[derive(Debug, Error)]
51pub enum LmbError {
52 #[error(
53 "loop edges specified are not actual loop edges in the graph:{loop_edges}:\n{loop_edges_dot}"
54 )]
55 NotLoopEdges {
56 loop_edges: String,
57 loop_edges_dot: String,
58 },
59 #[error("externals\n{externals_dot}\ncontain non-subgraph nodes:\n{subgraph_dot}\n")]
60 ExternalsOutsideSubgraph {
61 externals_dot: String,
62 subgraph_dot: String,
63 },
64 #[error(
65 "external cover is empty for externals\n{externals_dot}\nand subgraph\n{subgraph_dot}\n"
66 )]
67 EmptyExternalCover {
68 externals_dot: String,
69 subgraph_dot: String,
70 },
71 #[error(
72 "forest guide\n{forest_guide_dot}\ndoes not cover the same nodes as subgraph\n{subgraph_dot}\n"
73 )]
74 ForestGuideMismatch {
75 forest_guide_dot: String,
76 subgraph_dot: String,
77 },
78 #[error(
79 "failed to trace external flow from hedge {hedge} to dependent root {root} in tree\n{tree_dot}\n"
80 )]
81 ExternalFlowPathMissing {
82 hedge: Hedge,
83 root: Hedge,
84 tree_dot: String,
85 },
86 #[error("failed to get cycle for source hedge {hedge} in tree:\n{tree_dot}\n")]
87 MissingCycle { hedge: Hedge, tree_dot: String },
88 #[error(
89 "no loop-momentum basis compatible with the parent basis was found for subgraph\n{subgraph_dot}\nparent basis\n{parent_lmb_dot}"
90 )]
91 NoCompatibleSubLmb {
92 subgraph_dot: String,
93 parent_lmb_dot: String,
94 },
95 #[error("failed to get cycle from tree:{is_circuit}\n{cycle_dot}\n{cover_dot}")]
96 InvalidCycle {
97 is_circuit: bool,
98 cycle_dot: String,
99 cover_dot: String,
100 },
101 #[error("split edge on full graph")]
102 SplitEdgeOnFullGraph,
103 #[error("failed to build edge-signature vector")]
104 EdgeSignatureVector(#[source] HedgeGraphError),
105 #[error(
106 "shrunken subgraph is not contained in outer graph\nouter:\n{outer_dot}\nshrunken:\n{shrunken_dot}"
107 )]
108 ShrunkenOutsideOuter {
109 outer_dot: String,
110 shrunken_dot: String,
111 },
112 #[error("invalid shrunken internal subgraph:\n{shrunken_dot}")]
113 InvalidShrunkenSubgraph { shrunken_dot: String },
114 #[error(
115 "failed to build loop momentum basis after shrinking subgraph\nouter:\n{outer_dot}\nshrunken:\n{shrunken_dot}\nremainder:\n{remainder_dot}"
116 )]
117 NoShrunkenLmb {
118 outer_dot: String,
119 shrunken_dot: String,
120 remainder_dot: String,
121 #[source]
122 source: Box<LmbError>,
123 },
124}
125
126impl Display for LoopMomentumBasis {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 let mut signature = Builder::new();
129 let mut title = vec!["edge".to_string()];
130 let mut table = Builder::new();
131
132 table.push_column(["Loop id", "edge id"]);
133 for (i, item) in self.loop_edges.iter_enumerated() {
134 title.push(format!("L{i} {item}"));
135 table.push_column(&[i.to_string(), item.to_string()]);
136 }
137 table.build().with(Style::rounded()).fmt(f)?;
138 writeln!(f)?;
139 let mut table = Builder::new();
140
141 table.push_column(["External id", "edge id"]);
142 for (i, item) in self.ext_edges.iter_enumerated() {
143 title.push(format!("Ext{i} {item}"));
144 table.push_column(&[i.to_string(), item.to_string()]);
145 }
146 table.build().with(Style::rounded()).fmt(f)?;
147 writeln!(f)?;
148 signature.push_record(&title);
149 for (i, s) in &self.edge_signatures {
150 let mut signs = s
151 .internal
152 .iter()
153 .chain(s.external.iter())
154 .map(|s| s.to_string())
155 .collect_vec();
156 signs.insert(0, i.to_string());
157 signature.push_record(signs);
158 }
159 signature.build().with(Style::rounded()).fmt(f)?;
160 writeln!(f)?;
161
162 Ok(())
163 }
164}
165
166impl LoopMomentumBasis {
167 pub(crate) fn accounted_bytes(&self) -> usize {
168 std::mem::size_of::<Self>()
171 + self.tree.size().div_ceil(8)
172 + 32
173 + (self.loop_edges.capacity() + self.ext_edges.capacity())
174 * std::mem::size_of::<EdgeIndex>()
175 + self.edge_signatures.capacity() * std::mem::size_of::<LoopExtSignature>()
176 + self
177 .edge_signatures
178 .iter()
179 .map(|(_, signature)| {
180 (signature.internal.len() + signature.external.len())
181 * std::mem::size_of::<SignOrZero>()
182 })
183 .sum::<usize>()
184 }
185
186 pub(crate) fn ext_from(&self, eid: EdgeIndex) -> Option<ExternalIndex> {
187 self.ext_edges
188 .iter()
189 .position(|&e| e == eid)
190 .map(ExternalIndex)
191 }
192 pub(crate) fn swap_loops(&mut self, i: LoopIndex, j: LoopIndex) {
193 self.loop_edges.swap(i, j);
194 self.edge_signatures = self
195 .edge_signatures
196 .iter()
197 .map(|(eid, a)| {
198 let mut a = a.clone();
199 a.swap_loops(i, j);
200 (eid, a)
201 })
202 .collect();
203 }
204
205 pub(crate) fn put_loop_to_ext(&mut self, i: LoopIndex) {
206 let a = self.loop_edges.remove(i);
207 self.ext_edges.push(a);
209 self.edge_signatures
210 .iter_mut()
211 .for_each(|(_, s)| s.put_loop_to_ext(i));
212 }
213 pub(crate) fn canonicalize_external_order(&mut self, external_edge_order: &[EdgeIndex]) {
214 if external_edge_order.is_empty() {
215 return;
216 }
217
218 let current_ext_edges = self.ext_edges.clone();
219 let mut ordered_ext_edges = external_edge_order.to_vec();
220 ordered_ext_edges.extend(
221 current_ext_edges
222 .iter()
223 .copied()
224 .filter(|edge| !external_edge_order.contains(edge))
225 .sorted(),
226 );
227
228 if ordered_ext_edges == current_ext_edges.raw {
229 return;
230 }
231
232 for (_, signature) in self.edge_signatures.iter_mut() {
233 let mut expanded_external = vec![SignOrZero::Zero; ordered_ext_edges.len()];
234
235 for (old_slot, edge) in current_ext_edges.iter_enumerated() {
236 let Some(new_slot) = ordered_ext_edges
237 .iter()
238 .position(|ordered_edge| ordered_edge == edge)
239 else {
240 continue;
241 };
242 expanded_external[new_slot] = signature.external[old_slot];
243 }
244
245 signature.external = SignatureLike::from_iter(expanded_external);
246 }
247
248 self.ext_edges = ordered_ext_edges.into();
249 }
250}
251
252pub trait LMBext {
259 fn generate_loop_momentum_bases_of<S: SubGraphLike>(
265 &self,
266 subgraph: &S,
267 ) -> TiVec<LmbIndex, LoopMomentumBasis>
268 where
269 S::Base: SubGraphLike<Base = S::Base>
270 + SubSetOps
271 + Clone
272 + ModifySubSet<HedgePair>
273 + ModifySubSet<Hedge>;
274
275 fn generate_loop_momentum_bases(&self) -> TiVec<LmbIndex, LoopMomentumBasis>;
277
278 fn uv_wrapped_replacement<'a, S: SubSetLike, I>(
284 &self,
285 subgraph: &S,
286 lmb: &LoopMomentumBasis,
287 rep_args: &'a [I],
288 ) -> Vec<Replacement>
289 where
290 &'a I: Into<AtomOrView<'a>>,
291 {
292 self.replacement_impl(
293 |e, a, b| {
294 Replacement::new(
295 FunctionBuilder::new(GS.emr_mom)
296 .add_arg(usize::from(e))
297 .add_args(rep_args)
298 .finish()
299 .to_pattern(),
300 (a.replace(function!(GS.emr_mom, W_.x_))
301 .allow_new_wildcards_on_rhs(true)
302 .with(
303 FunctionBuilder::new(GS.emr_mom)
304 .add_arg(W_.x_)
305 .add_args(rep_args)
306 .finish(),
307 )
308 + b)
309 .to_pattern(),
310 )
311 },
312 subgraph,
313 lmb,
314 GS.emr_mom,
315 GS.emr_mom,
316 &[],
317 rep_args,
318 HedgePair::is_paired,
319 true,
320 )
321 }
322
323 fn uv_spatial_wrapped_replacement<'a, S: SubSetLike, I>(
329 &self,
330 subgraph: &S,
331 lmb: &LoopMomentumBasis,
332 rep_args: &'a [I],
333 ) -> Vec<Replacement>
334 where
335 &'a I: Into<AtomOrView<'a>>,
336 {
337 self.replacement_impl(
338 |e, a, b| {
339 Replacement::new(
340 FunctionBuilder::new(GS.emr_vec)
341 .add_arg(usize::from(e))
342 .add_args(rep_args)
343 .finish()
344 .to_pattern(),
345 (a.replace(function!(GS.emr_vec, W_.a_))
346 .allow_new_wildcards_on_rhs(true)
347 .with(
348 FunctionBuilder::new(GS.emr_vec)
349 .add_arg(W_.a_)
350 .add_args(rep_args)
351 .finish(),
352 )
353 + b)
354 .to_pattern(),
355 )
356 },
357 subgraph,
358 lmb,
359 GS.emr_vec,
360 GS.emr_vec,
361 &[],
362 rep_args,
363 HedgePair::is_paired,
364 true,
365 )
366 }
367
368 fn normal_emr_replacement<'a, S: SubSetLike, I>(
375 &self,
376 subgraph: &S,
377 lmb: &LoopMomentumBasis,
378 rep_args: &'a [I],
379 filter_pair: fn(&HedgePair) -> bool,
380 ) -> Vec<Replacement>
381 where
382 &'a I: Into<AtomOrView<'a>>,
383 {
384 self.replacement_impl(
385 |e, a, b| {
386 Replacement::new(
387 FunctionBuilder::new(GS.emr_mom)
388 .add_arg(usize::from(e))
389 .add_args(rep_args)
390 .finish()
391 .to_pattern(),
392 (a + b).to_pattern(),
393 )
394 },
395 subgraph,
396 lmb,
397 GS.emr_mom,
398 GS.emr_mom,
399 rep_args,
400 rep_args,
401 filter_pair,
402 true,
403 )
404 }
405
406 fn integrand_replacement<'a, S: SubSetLike, I>(
413 &self,
414 subgraph: &S,
415 lmb: &LoopMomentumBasis,
416 rep_args: &'a [I],
417 ) -> Vec<Replacement>
418 where
419 &'a I: Into<AtomOrView<'a>>,
420 {
421 self.replacement_impl(
422 |e, a, b| {
423 Replacement::new(
424 FunctionBuilder::new(GS.emr_mom)
425 .add_arg(usize::from(e))
426 .add_args(rep_args)
427 .finish()
428 .to_pattern(),
429 (a + b).to_pattern(),
430 )
431 },
432 subgraph,
433 lmb,
434 GS.loop_mom,
435 GS.external_mom,
436 rep_args,
437 rep_args,
438 no_filter,
439 false,
440 )
441 }
442
443 #[allow(clippy::too_many_arguments)]
459 fn replacement_impl<'a, S: SubSetLike, I>(
460 &self,
461 rep: impl Fn(EdgeIndex, Atom, Atom) -> Replacement,
462 subgraph: &S,
463 lmb: &LoopMomentumBasis,
464 loop_symbol: Symbol,
465 ext_symbol: Symbol,
466 loop_args: &'a [I],
467 ext_args: &'a [I],
468 filter_pair: fn(&HedgePair) -> bool,
469 emr_id: bool,
470 ) -> Vec<Replacement>
471 where
472 &'a I: Into<AtomOrView<'a>>;
473
474 fn lmb_impl<S: SubGraphLike + SubSetOps + ModifySubSet<HedgePair> + ModifySubSet<Hedge>>(
482 &self,
483 subgraph: &S,
484 tree: &S,
485 externals: S,
486 ) -> LmbResult<LoopMomentumBasis>
487 where
488 S::Base: ModifySubSet<Hedge> + SubGraphLike;
489
490 fn lmb_of<S: SubGraphLike<Base = SuBitGraph>>(&self, subgraph: &S) -> LoopMomentumBasis;
495
496 fn lmb(&self) -> LoopMomentumBasis;
498
499 fn shrunken_sub_lmb(
504 &self,
505 outer: &SuBitGraph,
506 shrunken: &InternalSubGraph,
507 externals: SuBitGraph,
508 parent_lmb: Option<&LoopMomentumBasis>,
509 ) -> LmbResult<LoopMomentumBasis>;
510
511 fn shrunken_lmb_of(&self, outer: &SuBitGraph, shrunken: &InternalSubGraph)
514 -> LoopMomentumBasis;
515
516 fn compatible_sub_lmb<S: SubGraphLike>(
522 &self,
523 subgraph: &S,
524 externals: S::Base,
525 lmb: &LoopMomentumBasis,
526 ) -> LoopMomentumBasis
527 where
528 S::Base: SubGraphLike<Base = S::Base>
529 + SubSetOps
530 + Clone
531 + ModifySubSet<HedgePair>
532 + ModifySubSet<Hedge>;
533
534 fn try_compatible_sub_lmb<S: SubGraphLike>(
539 &self,
540 subgraph: &S,
541 externals: S::Base,
542 lmb: &LoopMomentumBasis,
543 ) -> LmbResult<LoopMomentumBasis>
544 where
545 S::Base: SubGraphLike<Base = S::Base>
546 + SubSetOps
547 + Clone
548 + ModifySubSet<HedgePair>
549 + ModifySubSet<Hedge>,
550 {
551 Ok(self.compatible_sub_lmb(subgraph, externals, lmb))
552 }
553
554 fn cotree_lmb<
559 S: SubGraphLike + SubSetOps + SubGraphOps + ModifySubSet<HedgePair> + ModifySubSet<Hedge>,
560 >(
561 &self,
562 subgraph: &S,
563 cotree: &S,
564 externals: S,
565 ) -> LoopMomentumBasis
566 where
567 S::Base: ModifySubSet<Hedge> + SubGraphLike,
568 {
569 let tree = subgraph.subtract(cotree);
570 self.lmb_impl(subgraph, &tree, externals)
571 .unwrap_or_else(|err| panic!("Failed to build cotree loop momentum basis:\n{err}"))
572 }
573
574 fn empty_lmb(&self) -> LoopMomentumBasis;
576
577 fn dot_lmb_of<S: SubGraphLike>(&self, subgraph: &S, lmb: &LoopMomentumBasis) -> String;
580}
581
582pub(crate) fn no_filter(_pair: &HedgePair) -> bool {
583 true
584}
585
586impl<E, V, H> LMBext for HedgeGraph<E, V, H> {
587 fn empty_lmb(&self) -> LoopMomentumBasis {
588 LoopMomentumBasis {
589 tree: SuBitGraph::empty(0),
590 loop_edges: vec![].into(),
591 ext_edges: vec![].into(),
592 edge_signatures: self.new_edgevec(|_, _, _| LoopExtSignature::from((vec![], vec![]))),
593 }
594 }
595 fn lmb(&self) -> LoopMomentumBasis {
596 self.lmb_of(&self.full_filter())
597 }
598
599 fn shrunken_sub_lmb(
600 &self,
601 outer: &SuBitGraph,
602 shrunken: &InternalSubGraph,
603 externals: SuBitGraph,
604 parent_lmb: Option<&LoopMomentumBasis>,
605 ) -> LmbResult<LoopMomentumBasis> {
606 let graph_size = self.n_hedges();
607 let outer_dot = || {
608 if outer.size() == graph_size {
609 self.dot(outer)
610 } else {
611 format!(
612 "invalid outer size {}, expected {}; label {}",
613 outer.size(),
614 graph_size,
615 outer.string_label()
616 )
617 }
618 };
619 let shrunken_dot = || {
620 if shrunken.size() == graph_size {
621 self.dot(shrunken)
622 } else {
623 format!(
624 "invalid shrunken size {}, expected {}; label {}",
625 shrunken.size(),
626 graph_size,
627 shrunken.string_label()
628 )
629 }
630 };
631
632 if shrunken.size() != graph_size || !shrunken.valid(self) {
633 return Err(LmbError::InvalidShrunkenSubgraph {
634 shrunken_dot: shrunken_dot(),
635 });
636 }
637
638 if outer.size() != graph_size || !outer.includes(&shrunken.filter) {
639 return Err(LmbError::ShrunkenOutsideOuter {
640 outer_dot: outer_dot(),
641 shrunken_dot: shrunken_dot(),
642 });
643 }
644
645 if !outer.union(&self.full_crown(outer)).includes(&externals) {
646 return Err(LmbError::ExternalsOutsideSubgraph {
647 externals_dot: self.dot(&externals),
648 subgraph_dot: outer_dot(),
649 });
650 }
651
652 if shrunken.is_empty() {
653 return match parent_lmb {
654 Some(parent_lmb) => self.try_compatible_sub_lmb(outer, externals, parent_lmb),
655 None => self.lmb_impl(outer, outer, externals),
656 };
657 }
658
659 let remainder = outer.subtract(&shrunken.filter);
660 let mut contracted = self.to_ref();
661
662 for component in self.connected_components(shrunken) {
663 let Some(root) = component.included_iter().next() else {
664 continue;
665 };
666 let node_data = &self[self.node_id(root)];
667 contracted.identify_nodes_of_subgraph_without_self_edges::<_, SuBitGraph>(
668 &component, node_data,
669 );
670 }
671 contracted.forget_identification_history();
672
673 let retired_crown = self
678 .full_crown(shrunken)
679 .subtract(&contracted.full_crown(&remainder).union(&remainder));
680 let contracted_externals = externals
681 .subtract(&shrunken.filter)
682 .subtract(&retired_crown);
683
684 match parent_lmb {
685 Some(parent_lmb) => {
686 contracted.try_compatible_sub_lmb(&remainder, contracted_externals, parent_lmb)
687 }
688 None => contracted.lmb_impl(&remainder, &remainder, contracted_externals),
689 }
690 .map_err(|source| LmbError::NoShrunkenLmb {
691 outer_dot: outer_dot(),
692 shrunken_dot: shrunken_dot(),
693 remainder_dot: self.dot(&remainder),
694 source: Box::new(source),
695 })
696 }
697
698 fn shrunken_lmb_of(
699 &self,
700 outer: &SuBitGraph,
701 shrunken: &InternalSubGraph,
702 ) -> LoopMomentumBasis {
703 let externals = self.full_crown(outer);
704 self.shrunken_sub_lmb(outer, shrunken, externals, None)
705 .unwrap_or_else(|err| {
706 panic!("Failed to build shrunken-subgraph loop momentum basis:\n{err}")
707 })
708 }
709
710 fn dot_lmb_of<S: SubGraphLike>(&self, subgraph: &S, lmb: &LoopMomentumBasis) -> String {
711 let reps = self.normal_emr_replacement::<_, Atom>(subgraph, lmb, &[], no_filter);
712
713 let emrgraph = self.map_data_ref(
714 |_, _, _| "",
715 |_, e, _, _| {
716 EdgeData::new(
717 GS.emr_mom
718 .call_args([usize::from(e)])
719 .replace_multiple(&reps)
720 .printer(PrintOptions {
721 color_builtin_symbols: false,
722 color_top_level_sum: false,
723 bracket_level_colors: None,
724 ..Default::default()
725 })
726 .to_string(),
727 Orientation::Default,
728 )
729 },
730 |_, _| NoData {},
731 );
732 emrgraph.dot_label(subgraph)
733 }
734
735 fn lmb_of<S: SubGraphLike<Base = SuBitGraph>>(&self, subgraph: &S) -> LoopMomentumBasis {
736 if subgraph.is_empty() {
737 self.empty_lmb()
738 } else {
739 let external = self.full_crown(subgraph);
740 self.lmb_impl(subgraph.included(), subgraph.included(), external)
741 .unwrap_or_else(|err| {
742 panic!("Failed to build loop momentum basis for subgraph:\n{err}")
743 })
744 }
745 }
746
747 fn compatible_sub_lmb<S: SubGraphLike>(
748 &self,
749 subgraph: &S,
750 externals: S::Base,
751 lmb: &LoopMomentumBasis,
752 ) -> LoopMomentumBasis
753 where
754 S::Base: SubGraphLike<Base = S::Base>
755 + SubSetOps
756 + Clone
757 + ModifySubSet<HedgePair>
758 + ModifySubSet<Hedge>,
759 {
760 self.try_compatible_sub_lmb(subgraph, externals, lmb)
761 .unwrap_or_else(|err| {
762 panic!("Failed to build compatible subgraph loop momentum basis:\n{err}")
763 })
764 }
765
766 fn try_compatible_sub_lmb<S: SubGraphLike>(
767 &self,
768 subgraph: &S,
769 externals: S::Base,
770 lmb: &LoopMomentumBasis,
771 ) -> LmbResult<LoopMomentumBasis>
772 where
773 S::Base: SubGraphLike<Base = S::Base>
774 + SubSetOps
775 + Clone
776 + ModifySubSet<HedgePair>
777 + ModifySubSet<Hedge>,
778 {
779 let n_loops = self.cyclotomatic_number(subgraph);
780 if n_loops == 0 {
781 return Ok(self.empty_lmb());
782 }
783
784 let components = self.count_connected_components(subgraph);
786
787 for v in lmb
788 .loop_edges
789 .iter()
790 .filter(|e| {
791 let (_, p) = &self[*e];
792 subgraph.includes(p)
793 })
794 .combinations(n_loops)
795 {
796 let mut cut_subgraph = subgraph.included().clone();
797
798 for eid in v {
799 let (_, p) = &self[eid];
800 let HedgePair::Paired { source, sink } = p else {
801 continue;
802 };
803
804 if self.node_id(*source) == self.node_id(*sink) {
806 continue;
807 }
808 cut_subgraph.sub(*p);
809 }
810
811 if self.count_connected_components(&cut_subgraph) == components
812 && self.number_of_nodes_in_subgraph(&cut_subgraph)
813 == self.number_of_nodes_in_subgraph(subgraph)
814 {
815 return self.lmb_impl(subgraph.included(), &cut_subgraph, externals.clone());
818 }
819
820 }
822
823 let full_graph = self.full_filter();
824 let parent_lmb_has_full_loop_dimension =
825 lmb.loop_edges.len() == self.cyclotomatic_number(&full_graph);
826 let (subgraph_dot, parent_lmb_dot) = if parent_lmb_has_full_loop_dimension {
827 (
828 self.dot_lmb_of(subgraph, lmb),
829 self.dot_lmb_of(&full_graph, lmb),
830 )
831 } else {
832 (
835 self.dot(subgraph),
836 format!(
837 "parent loop edges {:?}; expected {} loops for\n{}",
838 lmb.loop_edges,
839 self.cyclotomatic_number(&full_graph),
840 self.dot(&full_graph),
841 ),
842 )
843 };
844
845 Err(LmbError::NoCompatibleSubLmb {
846 subgraph_dot,
847 parent_lmb_dot,
848 })
849 }
850
851 fn lmb_impl<S: SubGraphLike + SubSetOps + ModifySubSet<HedgePair> + ModifySubSet<Hedge>>(
853 &self,
854 subgraph: &S,
855 forest_guide: &S, mut externals: S, ) -> LmbResult<LoopMomentumBasis>
858 where
859 S::Base: ModifySubSet<Hedge> + SubGraphLike,
860 {
861 if subgraph.is_empty() {
869 return Ok(self.empty_lmb());
870 };
871
872 let mut not_seen = subgraph.clone();
873 let mut forest_edge: SuBitGraph = self.empty_subgraph();
874
875 let external_edge_order = self
879 .iter_edges_of(&externals)
880 .map(|(_, edge_id, _)| edge_id)
881 .unique()
882 .collect_vec();
883
884 let mut external_flows: TiVec<ExternalIndex, _> = vec![].into();
885 let mut ext_edges: TiVec<ExternalIndex, EdgeIndex> = vec![].into();
886
887 let mut loop_edges: TiVec<LoopIndex, EdgeIndex> = vec![].into();
888 let mut cycles = vec![];
889
890 loop {
891 let Some(mut root) = not_seen.included_iter().next() else {
892 break;
893 };
894
895 let tree = if let Some(external_root) = externals.included_iter().next() {
899 root = external_root;
900 let root_node = self.node_id(root);
901 let subgraph_tree =
902 SimpleTraversalTree::depth_first_traverse(self, subgraph, &root_node, None)
903 .map_err(|_| LmbError::ExternalsOutsideSubgraph {
904 externals_dot: self.dot(&externals),
905 subgraph_dot: self.dot(subgraph),
906 })?;
907
908 let external_cover = subgraph_tree.covers(&externals);
909 let subgraph_cover = subgraph_tree.covers(subgraph);
910
911 root = external_cover
914 .included_iter()
915 .rfind(|hedge| {
916 self.is_dangling(*hedge) || !external_cover.includes(&self.inv(*hedge))
917 })
918 .or_else(|| external_cover.included_iter().next_back())
919 .ok_or_else(|| LmbError::EmptyExternalCover {
920 externals_dot: self.dot(&externals),
921 subgraph_dot: self.dot(subgraph),
922 })?;
923 let root_node = self.node_id(root);
924 let tree = if subgraph_tree.tree_subgraph.is_empty() {
925 subgraph_tree
929 } else {
930 SimpleTraversalTree::depth_first_traverse(self, forest_guide, &root_node, None)
931 .map_err(|_| LmbError::ForestGuideMismatch {
932 forest_guide_dot: self.dot(forest_guide),
933 subgraph_dot: self.dot(subgraph),
934 })?
935 };
936
937 debug_assert_eq!(
938 subgraph_cover,
939 tree.covers(subgraph),
940 "Forest guide \n{}\n,does not cover the same nodes as subgraph \n{}\n",
941 self.dot(forest_guide),
942 self.dot(subgraph)
943 );
944
945 for (p, e, _) in self.iter_edges_of(&external_cover) {
952 let mut path_to_dep: S = self.empty_subgraph();
953
954 match p {
955 HedgePair::Split {
956 source,
957 sink,
958 split,
959 } => {
960 let hedge = match split {
961 Flow::Sink => sink,
962 Flow::Source => source,
963 };
964 let ext_sign: SignOrZero = split.into();
965 path_to_dep.add(root);
966
967 if hedge != root {
968 let ext = tree.hedge_parent(hedge, self.as_ref());
969 if let Some(ext) = ext {
970 for h in tree.ancestor_iter_hedge(ext, self.as_ref()).step_by(2)
971 {
972 path_to_dep.add(h);
973 }
974 }
975 }
976 external_flows.push((ext_sign, path_to_dep));
977 ext_edges.push(e);
978 }
979 HedgePair::Unpaired { hedge, flow } => {
980 let ext_sign: SignOrZero = flow.into();
981
982 path_to_dep.add(root);
983 if hedge != root {
984 if self.node_id(hedge) == root_node {
985 } else {
986 let ext = tree.hedge_parent(hedge, self.as_ref()).ok_or_else(
987 || LmbError::ExternalFlowPathMissing {
988 hedge,
989 root,
990 tree_dot: self.dot(&tree.tree_subgraph),
991 },
992 )?;
993
994 for h in tree.ancestor_iter_hedge(ext, self.as_ref()).step_by(2)
995 {
996 path_to_dep.add(h);
997 }
998 }
999 }
1000 ext_edges.push(e);
1001 external_flows.push((ext_sign, path_to_dep));
1002 }
1003 HedgePair::Paired { source, sink } => {
1004 for (hedge, reverse) in [(source, false), (sink, true)] {
1008 if let Some(ext) = tree.hedge_parent(hedge, self.as_ref()) {
1009 for h in tree.ancestor_iter_hedge(ext, self.as_ref()).step_by(2)
1010 {
1011 let h = if reverse { self.inv(h) } else { h };
1012 if path_to_dep.includes(&self.inv(h)) {
1013 path_to_dep.sub(self.inv(h));
1014 } else {
1015 path_to_dep.add(h);
1016 }
1017 }
1018 }
1019 }
1020 external_flows.push((Flow::Source.into(), path_to_dep));
1021 ext_edges.push(e);
1022 }
1023 }
1024 }
1025
1026 tree
1027 } else {
1028 let root_node = self.node_id(root);
1029 if forest_guide.is_empty() {
1030 SimpleTraversalTree::empty(self)
1031 } else {
1032 SimpleTraversalTree::depth_first_traverse(self, forest_guide, &root_node, None)
1033 .map_err(|_| LmbError::ForestGuideMismatch {
1034 forest_guide_dot: self.dot(forest_guide),
1035 subgraph_dot: self.dot(subgraph),
1036 })?
1037 }
1038 };
1039
1040 forest_edge.union_with(&tree.tree_subgraph);
1041
1042 let mut cover = tree.covers(subgraph);
1043
1044 for i in self.iter_crown(self.node_id(root)) {
1045 if subgraph.includes(&i) {
1046 cover.add(i);
1047 }
1048 }
1049 for (p, e, _) in self.iter_edges_of(&cover) {
1052 match p {
1053 HedgePair::Paired { source, sink } => {
1054 for h in self.iter_crown(self.node_id(sink)) {
1055 not_seen.sub(h);
1056 externals.sub(h);
1057 }
1058 for h in self.iter_crown(self.node_id(source)) {
1059 not_seen.sub(h);
1060 externals.sub(h);
1061 }
1062 if !tree.tree_subgraph.includes(&p) {
1063 let signed_cycle = if self.node_id(source) == self.node_id(sink) {
1064 let mut filter: SuBitGraph = self.empty_subgraph();
1068 filter.add(source);
1069 SignedCycle {
1070 filter,
1071 loop_count: Some(1),
1072 }
1073 } else {
1074 let cycle = tree.get_cycle(source, self).ok_or_else(|| {
1075 LmbError::MissingCycle {
1076 hedge: source,
1077 tree_dot: self.dot(&tree.tree_subgraph),
1078 }
1079 })?;
1080 let cycle_is_circuit = cycle.is_circuit(self);
1081 let cycle_dot = self.dot(&cycle.filter);
1082 SignedCycle::from_cycle(cycle, source, self).ok_or_else(|| {
1083 LmbError::InvalidCycle {
1084 is_circuit: cycle_is_circuit,
1085 cycle_dot,
1086 cover_dot: self.dot(&cover),
1087 }
1088 })?
1089 };
1090 cycles.push(signed_cycle);
1091 loop_edges.push(e);
1092 }
1093 }
1094 HedgePair::Split {
1095 source,
1096 sink,
1097 split,
1098 } => match split {
1099 Flow::Sink => {
1100 for h in self.iter_crown(self.node_id(sink)) {
1101 not_seen.sub(h);
1102 externals.sub(h);
1103 }
1104 }
1105 Flow::Source => {
1106 for h in self.iter_crown(self.node_id(source)) {
1107 not_seen.sub(h);
1108 externals.sub(h);
1109 }
1110 }
1111 },
1112 HedgePair::Unpaired { hedge, .. } => {
1113 for h in self.iter_crown(self.node_id(hedge)) {
1114 not_seen.sub(h);
1115 externals.sub(h);
1116 }
1117 }
1118 }
1119 }
1120 }
1121 let signature = self
1131 .new_edgevec_from_iter(
1132 self.iter_edges()
1133 .map(|(p, eid, _)| -> LmbResult<_> {
1134 let mut internal = vec![];
1135 let mut external = vec![];
1136 let empty_internal = vec![SignOrZero::Zero; cycles.len()];
1141 let empty_external = vec![SignOrZero::Zero; external_flows.len()];
1142
1143 match p {
1144 HedgePair::Paired { source, sink } => {
1145 if subgraph.includes(&p) {
1146 for l in &cycles {
1147 if l.filter.includes(&source) {
1148 internal.push(SignOrZero::Plus);
1149 } else if l.filter.includes(&sink) {
1150 internal.push(SignOrZero::Minus);
1151 } else {
1152 internal.push(SignOrZero::Zero);
1153 }
1154 }
1155 } else {
1156 internal = empty_internal;
1157 }
1158 if subgraph.intersects(&p) {
1159 for (i, (s, e)) in external_flows.iter_enumerated() {
1160 if ext_edges[i] == eid {
1161 if e.includes(&source) || e.includes(&sink) {
1162 external.push(SignOrZero::Zero); } else {
1164 external.push(SignOrZero::Plus);
1165 }
1166 } else if e.includes(&source) {
1167 external.push(*s * SignOrZero::Minus);
1168 } else if e.includes(&sink) {
1169 external.push(*s * SignOrZero::Plus);
1170 } else {
1171 external.push(SignOrZero::Zero);
1172 }
1173 }
1174 } else {
1175 external = empty_external;
1176 }
1177 }
1178 HedgePair::Unpaired { hedge, flow } => {
1179 if subgraph.includes(&hedge) {
1180 for (i, (s, e)) in external_flows.iter_enumerated() {
1181 if ext_edges[i] == eid {
1182 if e.includes(&hedge) {
1183 external.push(SignOrZero::Zero); } else {
1185 external.push(SignOrZero::Plus);
1186 }
1187 } else if e.includes(&hedge) {
1188 match flow {
1189 Flow::Source => {
1190 external.push(*s * SignOrZero::Minus)
1191 }
1192 Flow::Sink => external.push(*s * SignOrZero::Plus),
1193 }
1194 } else {
1195 external.push(SignOrZero::Zero);
1196 }
1197 }
1198 } else {
1199 external = empty_external;
1200 if externals.includes(&hedge)
1201 && let Some((e, _)) =
1202 ext_edges.iter().find_position(|a| *a == &eid)
1203 {
1204 external[e] = SignOrZero::Plus;
1205 };
1206 }
1207 internal = empty_internal;
1208 }
1209 HedgePair::Split { .. } => {
1210 return Err(LmbError::SplitEdgeOnFullGraph);
1211 }
1212 }
1213
1214 Ok(LoopExtSignature {
1215 internal: SignatureLike::from_iter(internal),
1216 external: SignatureLike::from_iter(external),
1217 })
1218 })
1219 .collect::<LmbResult<Vec<_>>>()?,
1220 )
1221 .map_err(LmbError::EdgeSignatureVector)?;
1222
1223 let mut lmb = LoopMomentumBasis {
1224 tree: forest_edge,
1225 edge_signatures: signature,
1226 ext_edges,
1227 loop_edges,
1228 };
1229 lmb.canonicalize_external_order(&external_edge_order);
1230
1231 Ok(lmb)
1232 }
1233
1234 fn generate_loop_momentum_bases_of<S: SubGraphLike>(
1235 &self,
1236 subgraph: &S,
1237 ) -> TiVec<LmbIndex, LoopMomentumBasis>
1238 where
1239 S::Base: SubGraphLike<Base = S::Base>
1240 + SubSetOps
1241 + Clone
1242 + ModifySubSet<HedgePair>
1243 + ModifySubSet<Hedge>,
1244 {
1245 let Some(_) = subgraph.included_iter().next() else {
1246 return vec![].into();
1247 };
1248
1249 let mut lmbs: TiVec<LmbIndex, LoopMomentumBasis> = vec![].into();
1250
1251 let externals = self.full_crown(subgraph);
1252
1253 for s in self.all_spanning_forests_of(subgraph) {
1254 lmbs.push(
1256 self.lmb_impl(subgraph.included(), &s, externals.clone())
1257 .unwrap_or_else(|err| {
1258 panic!("Failed to build loop momentum basis from spanning forest:\n{err}")
1259 }),
1260 );
1261 }
1262 lmbs
1263 }
1264
1265 fn generate_loop_momentum_bases(&self) -> TiVec<LmbIndex, LoopMomentumBasis> {
1266 self.generate_loop_momentum_bases_of(&self.full_filter())
1267 }
1268
1269 #[allow(clippy::too_many_arguments)]
1270 fn replacement_impl<'a, S: SubSetLike, I>(
1271 &self,
1272 rep: impl Fn(EdgeIndex, Atom, Atom) -> Replacement,
1273 subgraph: &S,
1274 lmb: &LoopMomentumBasis,
1275 loop_symbol: Symbol,
1276 ext_symbol: Symbol,
1277 loop_args: &'a [I],
1278 ext_args: &'a [I],
1279 filter_pair: fn(&HedgePair) -> bool,
1280 emr_id: bool,
1281 ) -> Vec<Replacement>
1282 where
1283 &'a I: Into<AtomOrView<'a>>,
1284 {
1285 let mut reps = vec![];
1286 for (p, e, _) in self.iter_edges_of(subgraph) {
1287 if filter_pair(&p) {
1288 let loop_expr = lmb.loop_atom(e, loop_symbol, loop_args, emr_id);
1290 let external_expr = lmb.ext_atom(e, ext_symbol, ext_args, emr_id);
1291
1292 reps.push(rep(e, loop_expr, external_expr))
1296 }
1297 }
1298
1299 reps
1300 }
1301}
1302
1303pub trait LMBwithEdges<E: ?Sized> {
1304 fn lmb_with_loop_edges(&self, lmb_edges: &E) -> LmbResult<LoopMomentumBasis>;
1305}
1306
1307impl LMBwithEdges<SuBitGraph> for Graph {
1308 fn lmb_with_loop_edges(&self, lmb_edges: &SuBitGraph) -> LmbResult<LoopMomentumBasis> {
1309 let full_filter = self.full_filter();
1310 let externals = self.internal_crown(&full_filter);
1311 let cut_graph = full_filter.subtract(lmb_edges);
1312
1313 self.lmb_impl(&full_filter, &cut_graph, externals)
1314 }
1315}
1316impl LMBwithEdges<[EdgeIndex]> for Graph {
1317 fn lmb_with_loop_edges(&self, lmb_edges: &[EdgeIndex]) -> LmbResult<LoopMomentumBasis> {
1318 let mut lmb_edges_subgraph: SuBitGraph = self.empty_subgraph();
1319
1320 for e in lmb_edges.iter() {
1321 lmb_edges_subgraph.add(self[e].1);
1322 }
1323 self.lmb_with_loop_edges(&lmb_edges_subgraph)
1324 }
1325}
1326
1327impl LMBwithEdges<[&EdgeIndex]> for Graph {
1328 fn lmb_with_loop_edges(&self, lmb_edges: &[&EdgeIndex]) -> LmbResult<LoopMomentumBasis> {
1329 let mut lmb_edges_subgraph: SuBitGraph = self.empty_subgraph();
1330
1331 for e in lmb_edges.iter() {
1332 lmb_edges_subgraph.add(self[*e].1);
1333 }
1334 self.lmb_with_loop_edges(&lmb_edges_subgraph)
1335 }
1336}
1337
1338impl LMBwithEdges<[&EdgeIndex]> for CrossSectionGraphTerm {
1339 fn lmb_with_loop_edges(&self, lmb_edges: &[&EdgeIndex]) -> LmbResult<LoopMomentumBasis> {
1340 Ok(self
1341 .lmbs
1342 .iter_enumerated()
1343 .find(|(_, lmb)| lmb_edges.iter().all(|edge| lmb.loop_edges.contains(edge)))
1344 .ok_or(LmbError::NotLoopEdges {
1345 loop_edges: lmb_edges.iter().map(|a| a.to_string()).join(","),
1346 loop_edges_dot: self.graph.debug_dot(),
1347 })?
1348 .1
1349 .clone())
1350 }
1351}
1352
1353impl LMBwithEdges<[EdgeIndex]> for CrossSectionGraphTerm {
1354 fn lmb_with_loop_edges(&self, lmb_edges: &[EdgeIndex]) -> LmbResult<LoopMomentumBasis> {
1355 Ok(self
1356 .lmbs
1357 .iter_enumerated()
1358 .find(|(_, lmb)| lmb_edges.iter().all(|edge| lmb.loop_edges.contains(edge)))
1359 .ok_or(LmbError::NotLoopEdges {
1360 loop_edges: lmb_edges.iter().map(|a| a.to_string()).join(","),
1361 loop_edges_dot: self.graph.debug_dot(),
1362 })?
1363 .1
1364 .clone())
1365 }
1366}
1367
1368impl LMBwithEdges<[&EdgeIndex]> for AmplitudeGraphTerm {
1369 fn lmb_with_loop_edges(&self, lmb_edges: &[&EdgeIndex]) -> LmbResult<LoopMomentumBasis> {
1370 Ok(self
1371 .lmbs
1372 .iter_enumerated()
1373 .find(|(_, lmb)| lmb_edges.iter().all(|edge| lmb.loop_edges.contains(edge)))
1374 .ok_or(LmbError::NotLoopEdges {
1375 loop_edges: lmb_edges.iter().map(|a| a.to_string()).join(","),
1376 loop_edges_dot: self.graph.debug_dot(),
1377 })?
1378 .1
1379 .clone())
1380 }
1381}
1382
1383impl LMBwithEdges<[EdgeIndex]> for AmplitudeGraphTerm {
1384 fn lmb_with_loop_edges(&self, lmb_edges: &[EdgeIndex]) -> LmbResult<LoopMomentumBasis> {
1385 Ok(self
1386 .lmbs
1387 .iter_enumerated()
1388 .find(|(_, lmb)| lmb_edges.iter().all(|edge| lmb.loop_edges.contains(edge)))
1389 .ok_or(LmbError::NotLoopEdges {
1390 loop_edges: lmb_edges.iter().map(|a| a.to_string()).join(","),
1391 loop_edges_dot: self.graph.debug_dot(),
1392 })?
1393 .1
1394 .clone())
1395 }
1396}
1397
1398impl LMBext for Graph {
1399 fn dot_lmb_of<S: SubGraphLike>(&self, subgraph: &S, lmb: &LoopMomentumBasis) -> String {
1400 self.underlying.dot_lmb_of(subgraph, lmb)
1401 }
1402
1403 fn generate_loop_momentum_bases(&self) -> TiVec<LmbIndex, LoopMomentumBasis> {
1404 self.generate_loop_momentum_bases_of(&self.underlying.full_filter())
1405 }
1406
1407 fn lmb(&self) -> LoopMomentumBasis {
1408 self.lmb_of(&self.underlying.full_filter())
1409 }
1410
1411 fn shrunken_sub_lmb(
1412 &self,
1413 outer: &SuBitGraph,
1414 shrunken: &InternalSubGraph,
1415 externals: SuBitGraph,
1416 parent_lmb: Option<&LoopMomentumBasis>,
1417 ) -> LmbResult<LoopMomentumBasis> {
1418 let mut lmb = self
1419 .underlying
1420 .shrunken_sub_lmb(outer, shrunken, externals, parent_lmb)?;
1421 self.canonicalize_lmb_external_order(&mut lmb);
1422 Ok(lmb)
1423 }
1424
1425 fn shrunken_lmb_of(
1426 &self,
1427 outer: &SuBitGraph,
1428 shrunken: &InternalSubGraph,
1429 ) -> LoopMomentumBasis {
1430 let mut lmb = self.underlying.shrunken_lmb_of(outer, shrunken);
1431 self.canonicalize_lmb_external_order(&mut lmb);
1432 lmb
1433 }
1434
1435 fn empty_lmb(&self) -> LoopMomentumBasis {
1436 self.underlying.empty_lmb()
1437 }
1438 fn generate_loop_momentum_bases_of<S: SubGraphLike>(
1439 &self,
1440 subgraph: &S,
1441 ) -> TiVec<LmbIndex, LoopMomentumBasis>
1442 where
1443 S::Base: SubGraphLike<Base = S::Base>
1444 + SubSetOps
1445 + Clone
1446 + ModifySubSet<HedgePair>
1447 + ModifySubSet<Hedge>,
1448 {
1449 let Some(_) = subgraph.included_iter().next() else {
1450 return vec![].into();
1451 };
1452
1453 let externals = self.dummy_stripped_external_flows_of(subgraph);
1454 let mut lmbs: TiVec<LmbIndex, LoopMomentumBasis> = vec![].into();
1455 for forest in self.underlying.all_spanning_forests_of(subgraph) {
1456 let mut lmb = self
1457 .underlying
1458 .lmb_impl(subgraph.included(), &forest, externals.clone())
1459 .unwrap_or_else(|err| {
1460 panic!("Failed to build loop momentum basis from spanning forest:\n{err}")
1461 });
1462 self.canonicalize_lmb_external_order(&mut lmb);
1463 lmbs.push(lmb);
1464 }
1465 lmbs
1466 }
1467
1468 fn replacement_impl<'a, S: SubSetLike, I>(
1469 &self,
1470 rep: impl Fn(EdgeIndex, Atom, Atom) -> Replacement,
1471 subgraph: &S,
1472 lmb: &LoopMomentumBasis,
1473 loop_symbol: Symbol,
1474 ext_symbol: Symbol,
1475 loop_args: &'a [I],
1476 ext_args: &'a [I],
1477 filter_pair: fn(&HedgePair) -> bool,
1478 emr_id: bool,
1479 ) -> Vec<Replacement>
1480 where
1481 &'a I: Into<AtomOrView<'a>>,
1482 {
1483 self.underlying.replacement_impl(
1484 rep,
1485 subgraph,
1486 lmb,
1487 loop_symbol,
1488 ext_symbol,
1489 loop_args,
1490 ext_args,
1491 filter_pair,
1492 emr_id,
1493 )
1494 }
1495
1496 fn lmb_impl<S: SubGraphLike + SubSetOps + ModifySubSet<HedgePair> + ModifySubSet<Hedge>>(
1497 &self,
1498 subgraph: &S,
1499 tree: &S,
1500 externals: S,
1501 ) -> LmbResult<LoopMomentumBasis>
1502 where
1503 S::Base: ModifySubSet<Hedge> + SubGraphLike,
1504 {
1505 let mut lmb = self.underlying.lmb_impl(subgraph, tree, externals)?;
1506 self.canonicalize_lmb_external_order(&mut lmb);
1507 Ok(lmb)
1508 }
1509
1510 fn lmb_of<S: SubGraphLike<Base = SuBitGraph>>(&self, subgraph: &S) -> LoopMomentumBasis {
1511 let mut lmb = self.underlying.lmb_of(subgraph);
1512 self.canonicalize_lmb_external_order(&mut lmb);
1513 lmb
1514 }
1515
1516 fn compatible_sub_lmb<S: SubGraphLike>(
1517 &self,
1518 subgraph: &S,
1519 externals: S::Base,
1520 lmb: &LoopMomentumBasis,
1521 ) -> LoopMomentumBasis
1522 where
1523 S::Base: SubGraphLike<Base = S::Base>
1524 + SubSetOps
1525 + Clone
1526 + ModifySubSet<HedgePair>
1527 + ModifySubSet<Hedge>,
1528 {
1529 self.try_compatible_sub_lmb(subgraph, externals, lmb)
1530 .unwrap_or_else(|err| {
1531 panic!("Failed to build compatible subgraph loop momentum basis:\n{err}")
1532 })
1533 }
1534
1535 fn try_compatible_sub_lmb<S: SubGraphLike>(
1536 &self,
1537 subgraph: &S,
1538 externals: S::Base,
1539 lmb: &LoopMomentumBasis,
1540 ) -> LmbResult<LoopMomentumBasis>
1541 where
1542 S::Base: SubGraphLike<Base = S::Base>
1543 + SubSetOps
1544 + Clone
1545 + ModifySubSet<HedgePair>
1546 + ModifySubSet<Hedge>,
1547 {
1548 let mut sub_lmb = self
1549 .underlying
1550 .try_compatible_sub_lmb(subgraph, externals, lmb)?;
1551 self.canonicalize_lmb_external_order(&mut sub_lmb);
1552 Ok(sub_lmb)
1553 }
1554}
1555
1556impl LMBext for &Graph {
1557 fn dot_lmb_of<S: SubGraphLike>(&self, subgraph: &S, lmb: &LoopMomentumBasis) -> String {
1558 self.underlying.dot_lmb_of(subgraph, lmb)
1559 }
1560
1561 fn lmb(&self) -> LoopMomentumBasis {
1562 self.lmb_of(&self.underlying.full_filter())
1563 }
1564
1565 fn shrunken_sub_lmb(
1566 &self,
1567 outer: &SuBitGraph,
1568 shrunken: &InternalSubGraph,
1569 externals: SuBitGraph,
1570 parent_lmb: Option<&LoopMomentumBasis>,
1571 ) -> LmbResult<LoopMomentumBasis> {
1572 let mut lmb = self
1573 .underlying
1574 .shrunken_sub_lmb(outer, shrunken, externals, parent_lmb)?;
1575 self.canonicalize_lmb_external_order(&mut lmb);
1576 Ok(lmb)
1577 }
1578
1579 fn shrunken_lmb_of(
1580 &self,
1581 outer: &SuBitGraph,
1582 shrunken: &InternalSubGraph,
1583 ) -> LoopMomentumBasis {
1584 let mut lmb = self.underlying.shrunken_lmb_of(outer, shrunken);
1585 self.canonicalize_lmb_external_order(&mut lmb);
1586 lmb
1587 }
1588
1589 fn empty_lmb(&self) -> LoopMomentumBasis {
1590 self.underlying.empty_lmb()
1591 }
1592 fn generate_loop_momentum_bases_of<S: SubGraphLike>(
1593 &self,
1594 subgraph: &S,
1595 ) -> TiVec<LmbIndex, LoopMomentumBasis>
1596 where
1597 S::Base: SubGraphLike<Base = S::Base>
1598 + SubSetOps
1599 + Clone
1600 + ModifySubSet<HedgePair>
1601 + ModifySubSet<Hedge>,
1602 {
1603 let Some(_) = subgraph.included_iter().next() else {
1604 return vec![].into();
1605 };
1606
1607 let externals = self.dummy_stripped_external_flows_of(subgraph);
1608 let mut lmbs: TiVec<LmbIndex, LoopMomentumBasis> = vec![].into();
1609 for forest in self.underlying.all_spanning_forests_of(subgraph) {
1610 let mut lmb = self
1611 .underlying
1612 .lmb_impl(subgraph.included(), &forest, externals.clone())
1613 .unwrap_or_else(|err| {
1614 panic!("Failed to build loop momentum basis from spanning forest:\n{err}")
1615 });
1616 self.canonicalize_lmb_external_order(&mut lmb);
1617 lmbs.push(lmb);
1618 }
1619 lmbs
1620 }
1621
1622 fn generate_loop_momentum_bases(&self) -> TiVec<LmbIndex, LoopMomentumBasis> {
1623 self.generate_loop_momentum_bases_of(&self.underlying.full_filter())
1624 }
1625
1626 fn replacement_impl<'a, S: SubSetLike, I>(
1627 &self,
1628 rep: impl Fn(EdgeIndex, Atom, Atom) -> Replacement,
1629 subgraph: &S,
1630 lmb: &LoopMomentumBasis,
1631 loop_symbol: Symbol,
1632 ext_symbol: Symbol,
1633 loop_args: &'a [I],
1634 ext_args: &'a [I],
1635 filter_pair: fn(&HedgePair) -> bool,
1636 emr_id: bool,
1637 ) -> Vec<Replacement>
1638 where
1639 &'a I: Into<AtomOrView<'a>>,
1640 {
1641 self.underlying.replacement_impl(
1642 rep,
1643 subgraph,
1644 lmb,
1645 loop_symbol,
1646 ext_symbol,
1647 loop_args,
1648 ext_args,
1649 filter_pair,
1650 emr_id,
1651 )
1652 }
1653
1654 fn lmb_impl<S: SubGraphLike + SubSetOps + ModifySubSet<HedgePair> + ModifySubSet<Hedge>>(
1655 &self,
1656 subgraph: &S,
1657 tree: &S,
1658 externals: S,
1659 ) -> LmbResult<LoopMomentumBasis>
1660 where
1661 S::Base: ModifySubSet<Hedge> + SubGraphLike,
1662 {
1663 let mut lmb = self.underlying.lmb_impl(subgraph, tree, externals)?;
1664 self.canonicalize_lmb_external_order(&mut lmb);
1665 Ok(lmb)
1666 }
1667
1668 fn lmb_of<S: SubGraphLike<Base = SuBitGraph>>(&self, subgraph: &S) -> LoopMomentumBasis {
1669 let mut lmb = self.underlying.lmb_of(subgraph);
1670 self.canonicalize_lmb_external_order(&mut lmb);
1671 lmb
1672 }
1673
1674 fn compatible_sub_lmb<S: SubGraphLike>(
1675 &self,
1676 subgraph: &S,
1677 externals: S::Base,
1678 lmb: &LoopMomentumBasis,
1679 ) -> LoopMomentumBasis
1680 where
1681 S::Base: SubGraphLike<Base = S::Base>
1682 + SubSetOps
1683 + Clone
1684 + ModifySubSet<HedgePair>
1685 + ModifySubSet<Hedge>,
1686 {
1687 self.try_compatible_sub_lmb(subgraph, externals, lmb)
1688 .unwrap_or_else(|err| {
1689 panic!("Failed to build compatible subgraph loop momentum basis:\n{err}")
1690 })
1691 }
1692
1693 fn try_compatible_sub_lmb<S: SubGraphLike>(
1694 &self,
1695 subgraph: &S,
1696 externals: S::Base,
1697 lmb: &LoopMomentumBasis,
1698 ) -> LmbResult<LoopMomentumBasis>
1699 where
1700 S::Base: SubGraphLike<Base = S::Base>
1701 + SubSetOps
1702 + Clone
1703 + ModifySubSet<HedgePair>
1704 + ModifySubSet<Hedge>,
1705 {
1706 let mut sub_lmb = self
1707 .underlying
1708 .try_compatible_sub_lmb(subgraph, externals, lmb)?;
1709 self.canonicalize_lmb_external_order(&mut sub_lmb);
1710 Ok(sub_lmb)
1711 }
1712}
1713
1714impl LoopMomentumBasis {
1715 pub fn map_to(&self, other: &Self) -> Vec<Atom> {
1716 let selfmom = symbol!("K");
1717 let othermom = symbol!("L");
1718 let mut sys = vec![];
1719
1720 for (l, e) in self.loop_edges.iter_enumerated() {
1721 sys.push(
1722 other.loop_atom::<Atom>(*e, othermom, &[], false)
1723 + other.ext_atom::<Atom>(*e, othermom, &[], false)
1724 - selfmom.call_args([l.0]),
1725 )
1726 }
1727
1728 let mut vars = vec![];
1729
1730 for (l, _) in other.loop_edges.iter_enumerated() {
1731 vars.push(othermom.call_args([l.0]))
1732 }
1733
1734 let solutions = Atom::solve(&sys).wrt_with_exponent::<u8, _>(&vars).unwrap();
1735 let [solution] = solutions.iter().as_slice() else {
1736 panic!(
1737 "expected one loop-momentum basis solution, got {} branches",
1738 solutions.len()
1739 );
1740 };
1741 assert!(
1742 solution.free_variables().is_empty(),
1743 "loop-momentum basis solution is underdetermined"
1744 );
1745 vars.iter()
1746 .map(|variable| {
1747 let variable = PolyVariable::try_from(variable.clone()).unwrap();
1748 solution.get(&variable).cloned().unwrap()
1749 })
1750 .collect()
1751 }
1752 pub fn loop_atom<'a, I>(
1769 &self,
1770 edge_id: EdgeIndex,
1771 mom_symbol: Symbol,
1772 additional_args: &'a [I],
1773 emr_id: bool,
1774 ) -> Atom
1775 where
1776 &'a I: Into<AtomOrView<'a>>,
1777 {
1778 self.edge_signatures[edge_id].loop_atom(mom_symbol, additional_args, |l| {
1779 Atom::num(if emr_id {
1780 usize::from(self.loop_edges[l])
1781 } else {
1782 usize::from(l)
1783 } as i64)
1784 })
1785 }
1786
1787 pub fn ext_atom<'a, I>(
1788 &self,
1789 edge_id: EdgeIndex,
1790 mom_symbol: Symbol,
1791 additional_args: &'a [I],
1792 emr_id: bool,
1793 ) -> Atom
1794 where
1795 &'a I: Into<AtomOrView<'a>>,
1796 {
1797 self.edge_signatures[edge_id].ext_atom(mom_symbol, additional_args, |l| {
1798 Atom::num(if emr_id {
1799 usize::from(self.ext_edges[l])
1800 } else {
1801 usize::from(l)
1802 } as i64)
1803 })
1804 }
1805
1806 pub(crate) fn edges_are_raised(&self, edge_1: EdgeIndex, edge_2: EdgeIndex) -> bool {
1820 let sig_1 = &self.edge_signatures[edge_1];
1821 let sig_2 = &self.edge_signatures[edge_2];
1822 sig_1.equality_up_to_sign(sig_2)
1823 }
1824}
1825
1826#[derive(
1827 Debug,
1828 Clone,
1829 Serialize,
1830 Deserialize,
1831 bincode::Encode,
1832 bincode::Decode,
1833 Copy,
1834 Hash,
1835 From,
1836 Into,
1837 Eq,
1838 PartialEq,
1839 Ord,
1840 PartialOrd,
1841)]
1842pub struct LmbIndex(usize);
1843
1844#[cfg(test)]
1845pub mod test {
1846
1847 use insta::assert_snapshot;
1848 use linnet::{
1849 half_edge::{
1850 involution::{EdgeIndex, Hedge},
1851 subgraph::{Inclusion, InternalSubGraph, ModifySubSet, SuBitGraph, SubSetOps},
1852 },
1853 parser::DotGraph,
1854 };
1855
1856 use crate::{
1857 dot,
1858 graph::{FeynmanGraph, Graph, LMBext, LmbError, parse::IntoGraph},
1859 initialisation::test_initialise,
1860 momentum::SignOrZero,
1861 };
1862
1863 static SHRUNKEN_LMB_TEST_INIT: std::sync::Once = std::sync::Once::new();
1864 static SHRUNKEN_LMB_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1865
1866 #[test]
1867 fn generated_singleton_lmbs_conserve_external_momentum() {
1868 test_initialise().unwrap();
1869 let graphs: Vec<Graph> = dot!(
1870 digraph contact {
1871 ext [style=invis]
1872 node[num=1]
1873 ext->v:0[id=0]
1874 ext->v:1[id=1]
1875 v:2->ext[id=2]
1876 v:3->ext[id=3]
1877 }
1878 digraph tadpole {
1879 ext [style=invis]
1880 node[num=1]
1881 ext->v:0[id=0]
1882 ext->v:1[id=1]
1883 v:2->ext[id=2]
1884 v:3->ext[id=3]
1885 v->v[id=4 num=1 mass=1]
1886 }
1887 )
1888 .unwrap();
1889
1890 for (loops, graph) in graphs.iter().enumerate() {
1891 let lmbs = graph.generate_loop_momentum_bases();
1892 let lmb = lmbs
1893 .first()
1894 .expect("a singleton graph has a valid momentum basis");
1895 assert_eq!(lmb.loop_edges.len(), loops);
1896 for (edge, expected) in [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [1, 1, -1, 0]]
1898 .into_iter()
1899 .enumerate()
1900 {
1901 let signature = &lmb.edge_signatures[EdgeIndex::from(edge)];
1902 assert!(
1903 signature
1904 .internal
1905 .iter()
1906 .all(|sign| *sign == SignOrZero::Zero)
1907 );
1908 assert_eq!(
1909 signature
1910 .external
1911 .iter()
1912 .map(|sign| *sign * 1_i32)
1913 .collect::<Vec<_>>(),
1914 expected
1915 );
1916 }
1917 if loops == 1 {
1918 let signature = &lmb.edge_signatures[EdgeIndex::from(4)];
1919 assert_eq!(
1920 signature.internal.iter().copied().collect::<Vec<_>>(),
1921 [SignOrZero::Plus]
1922 );
1923 assert!(
1924 signature
1925 .external
1926 .iter()
1927 .all(|sign| *sign == SignOrZero::Zero)
1928 );
1929 }
1930 }
1931 }
1932
1933 #[test]
1934 fn lmb_for_dummy() {
1935 test_initialise().unwrap();
1936 let gs: Vec<Graph> = dot!(
1937 digraph dxda{
1938 ext [style=invis]
1939 node[num=1]
1940 ext->v1:0[id=0 is_dummy=true]
1941 ext->v1:1[id=1 ]
1942 ext->v1:2[id=2 ]
1943 }
1944
1945 digraph aa{
1946 ext [style=invis]
1947 node[num=1]
1948 ext->v1:0[id=0 is_dummy=true]
1949 ext->v1:1[id=1 is_dummy=true]
1950 v1->v2
1951 ext->v2:2[id=2 ]
1952 }
1953 )
1954 .unwrap();
1955
1956 for g in gs {
1957 insta::with_settings!({
1958 snapshot_suffix=>g.name.to_string(),
1959 }, {
1960 insta::assert_snapshot!(g.dot_lmb_of(&g.full_filter(), &g.loop_momentum_basis));
1961 });
1962 }
1963 }
1964
1965 #[test]
1966 fn generated_lmbs_do_not_use_dummy_external_carriers() {
1967 test_initialise().unwrap();
1968 let g: Graph = dot!(digraph{
1969 ext [style=invis]
1970 edge[num=1 mass=1]
1971 node[num=1]
1972 ext->v1:0[id=0 is_dummy=true]
1973 ext->v1:1[id=1]
1974 v1->v2[id=2]
1975 v2->v1[id=3]
1976 ext->v2:2[id=4]
1977 })
1978 .unwrap();
1979
1980 let lmbs = g.generate_loop_momentum_bases_of(&g.no_dummy());
1981 assert!(!lmbs.is_empty());
1982
1983 for lmb in lmbs {
1984 assert_eq!(
1985 lmb.ext_edges[crate::momentum::sample::ExternalIndex(0)],
1986 EdgeIndex::from(0)
1987 );
1988
1989 for edge_id in [1, 2, 3, 4].map(EdgeIndex::from) {
1990 assert_eq!(
1991 lmb.edge_signatures[edge_id].external
1992 [crate::momentum::sample::ExternalIndex(0)],
1993 SignOrZero::Zero,
1994 "non-dummy edge {edge_id} uses the dummy external as a generated LMB carrier"
1995 );
1996 }
1997 }
1998 }
1999
2000 #[test]
2001 fn complicated() {
2002 test_initialise().unwrap();
2003 let g: Graph = dot!(digraph{
2004
2005 edge[num=1 mass=1]
2006 node[num=1]
2007
2008 e[style=invis]
2009
2010 a->c
2011
2012 a->e
2013 a->e
2014 b->c
2015 d->c
2016 d->e
2017 b->e
2018 a->b->d->a
2019 b->b1
2020 b1->b2
2021 b1->b2
2022 b1->b2
2023 b2->e
2024 })
2025 .unwrap();
2026 assert_snapshot!(g.dot_lmb_of(&g.full_filter(), &g.loop_momentum_basis));
2027 assert_snapshot!(&g.loop_momentum_basis.to_string());
2028 let _g = g.generate_loop_momentum_bases_of(&g.full_filter());
2029 }
2030 #[test]
2031 fn disconnected() {
2032 test_initialise().unwrap();
2033 let g: Graph = dot!(digraph{
2034 e [style=invis]
2036 edge[num=1 mass=1]
2037 node[num=1]
2038 e->v1
2039 e->v1
2040 e->v1
2041 e->v1
2042 v1->v1
2043
2044 e->v2
2045 e->v2
2046 e->v2
2047
2048 v3->v3
2049 v3->v4
2050 v4->v4
2051
2052
2053 e->v5
2054 e->v5->v6
2055 v6->v7
2056 v6->v7
2057 e->v7
2058 })
2059 .unwrap();
2060 assert_snapshot!(g.dot_lmb_of(&g.full_filter(), &g.loop_momentum_basis));
2061 assert_snapshot!(&g.loop_momentum_basis.to_string());
2062
2063 let g: Graph = dot!(digraph{
2064
2065 edge[num=1 mass=1]
2066 node[num=1]
2067 a->b
2068 a->b
2069 a->b
2070
2071
2072 c->e
2073 e->d
2074 c->d
2075 c->d
2076 })
2077 .unwrap();
2078
2079 assert_eq!(g.generate_loop_momentum_bases().len(), 15);
2080 }
2081 #[test]
2082 fn subgraph_with_exts_in_loop() {
2083 test_initialise().unwrap();
2084 let g: Graph = dot!(digraph{
2085 edge[num=1 mass=1]
2086 node[num=1]
2087 v3:0->v4:1
2088 v3:3->v4:2
2089 v3:4->v4:5
2090 })
2091 .unwrap();
2092
2093 let mut sub = g.full_filter();
2094 sub.sub(Hedge(0));
2095 sub.sub(Hedge(1));
2096 let lmb = g.lmb_of(&sub);
2097 assert_snapshot!(g.dot_lmb_of(&g.full_filter(), &lmb));
2098 assert_snapshot!(&lmb.to_string());
2099
2100 let lmb = g.lmb_impl(&sub, &sub, g.full_crown(&sub)).unwrap();
2101 assert_snapshot!(g.dot_lmb_of(&g.full_filter(), &lmb));
2102 assert_snapshot!(&lmb.to_string());
2103 }
2104
2105 #[test]
2106 fn paired_crown_sub_lmbs_preserve_full_graph_momenta() {
2107 test_initialise().unwrap();
2108 let banana: Graph = dot!(digraph paired_crown_banana {
2109 edge[num=1 mass=1]
2110 node[num=1]
2111 incoming[style=invis]
2112 outgoing[style=invis]
2113 incoming -> a:0 [id=0]
2114 b:1 -> outgoing [id=5]
2115 a:2 -> b:3 [id=1 lmb_id=0]
2116 a:4 -> b:5 [id=2 lmb_id=1]
2117 b:6 -> a:7 [id=3 lmb_id=2]
2118 a:8 -> b:9 [id=4]
2119 })
2120 .unwrap();
2121 let common_path: Graph = dot!(digraph paired_crown_common_path {
2122 edge[num=1 mass=1]
2123 node[num=1]
2124 incoming[style=invis]
2125 outgoing[style=invis]
2126 incoming -> root [id=0]
2127 root -> c [id=1]
2128 c -> a [id=2]
2129 c -> b [id=3]
2130 a -> b [id=4 lmb_id=0]
2131 root -> outgoing [id=5]
2132 a -> b [id=6 lmb_id=1]
2133 })
2134 .unwrap();
2135 let vacuum: Graph = dot!(digraph paired_crown_vacuum {
2136 edge[num=1 mass=1]
2137 node[num=1]
2138 a -> b [id=0 lmb_id=0]
2139 a -> b [id=1 lmb_id=1]
2140 b -> a [id=2 lmb_id=2]
2141 a -> b [id=3]
2142 })
2143 .unwrap();
2144
2145 for (graph, edge_sets) in [
2146 (banana, vec![vec![1, 2], vec![1, 2, 3]]),
2147 (common_path, vec![vec![1, 2, 3, 4]]),
2148 (vacuum, vec![vec![0, 1], vec![0, 1, 2]]),
2149 ] {
2150 let physical = &graph.loop_momentum_basis;
2151 for edges in edge_sets {
2152 let mut subgraph = graph.empty_subgraph::<SuBitGraph>();
2153 for edge in &edges {
2154 subgraph.union_with(&graph.get_edge_subgraph(EdgeIndex::from(*edge)));
2155 }
2156 let mut bases = graph.generate_loop_momentum_bases_of(&subgraph).raw;
2157 bases.push(
2158 graph
2159 .try_compatible_sub_lmb(&subgraph, graph.full_crown(&subgraph), physical)
2160 .unwrap(),
2161 );
2162 for basis in bases {
2163 for edge in &edges {
2164 let edge = EdgeIndex::from(*edge);
2165 let signature = &basis.edge_signatures[edge];
2166 let mut routed =
2167 vec![0; physical.loop_edges.len() + physical.ext_edges.len()];
2168 for (carrier, coefficient) in basis
2169 .loop_edges
2170 .iter()
2171 .zip(signature.internal.iter())
2172 .chain(basis.ext_edges.iter().zip(signature.external.iter()))
2173 {
2174 let physical_carrier = &physical.edge_signatures[*carrier];
2175 for (value, sign) in routed.iter_mut().zip(
2176 physical_carrier
2177 .internal
2178 .iter()
2179 .chain(physical_carrier.external.iter()),
2180 ) {
2181 *value += *coefficient * (*sign * 1_i32);
2182 }
2183 }
2184 let expected = &physical.edge_signatures[edge];
2185 assert_eq!(
2186 routed,
2187 expected
2188 .internal
2189 .iter()
2190 .chain(expected.external.iter())
2191 .map(|sign| *sign * 1_i32)
2192 .collect::<Vec<_>>(),
2193 "{} subgraph {edges:?}, loops {:?}, edge {edge}: the sub-LMB must retain every physical crown momentum",
2194 graph.name,
2195 basis.loop_edges,
2196 );
2197 }
2198 }
2199 }
2200 }
2201 }
2202
2203 #[test]
2204 fn compatible_sub_lmb() {
2205 test_initialise().unwrap();
2206 let g: DotGraph = linnet::dot!(
2207 digraph{
2208
2209 node[num=1]
2210
2211 v1->v2
2212 v2->v3
2213 v1->v2
2214 v2->v3
2215 v3:s->v1:s
2216 v1:s->v3:s
2217
2218 }
2219 )
2220 .unwrap();
2221
2222 let subgraph: SuBitGraph = g.compass_subgraph(Some(dot_parser::ast::CompassPt::S));
2223
2224 let lmb = g.lmb_of(&subgraph);
2225 assert_snapshot!(g.dot_lmb_of(&subgraph, &lmb));
2226 assert_snapshot!(lmb.to_string());
2227
2228 let mut incompatible_parent_lmb = lmb.clone();
2229 incompatible_parent_lmb.loop_edges.clear();
2230 let unavailable =
2231 g.try_compatible_sub_lmb(&subgraph, g.full_crown(&subgraph), &incompatible_parent_lmb);
2232 assert!(matches!(
2233 unavailable,
2234 Err(LmbError::NoCompatibleSubLmb { .. })
2235 ));
2236
2237 let g: DotGraph = linnet::dot!(
2238 digraph dxda{
2239 e1 [style=invis]
2240 e2 [style=invis]
2241 e3 [style=invis]
2242 e4 [style=invis]
2243 node[num=1]
2244 e1->v1:0:n[id=0]
2245 e2->v1:1[id=1 ]
2246 v1:s->v2:s
2247 v2:s->v3:s
2248 v3->v1
2249 v1:s->v3:s
2250 e4->v3
2251 e3->v2:2[id=2 ]
2252 }
2253
2254 )
2255 .unwrap();
2256
2257 let subgraph: SuBitGraph = g.compass_subgraph(Some(dot_parser::ast::CompassPt::S));
2258
2259 let dummy: SuBitGraph = g.compass_subgraph(Some(dot_parser::ast::CompassPt::N));
2260 let non_dummy = g.full_filter().subtract(&dummy);
2261 let lmb = g.lmb_of(&non_dummy);
2262 let non_dummy_sub_ext = g.full_crown(&subgraph).subtract(&dummy);
2263
2264 let sub_lmb = g
2265 .try_compatible_sub_lmb(&subgraph, non_dummy_sub_ext, &lmb)
2266 .unwrap();
2267
2268 assert_snapshot!(g.dot_lmb_of(&non_dummy, &lmb));
2269 assert_snapshot!(g.dot_lmb_of(&subgraph, &sub_lmb));
2270
2271 let g: DotGraph = linnet::dot!(
2272 digraph {
2273 0:0:s -> 0:1:s [id=0];
2274 0:2 -> 1:3 [id=1];
2275 1:4:s -> 1:5:s [id=2];
2276 }
2277
2278 )
2279 .unwrap();
2280
2281 let subgraph: SuBitGraph = g.compass_subgraph(Some(dot_parser::ast::CompassPt::S));
2282 let non_dummy = g.full_filter();
2283 let lmb = g.lmb_of(&non_dummy);
2284 let sub_lmb = g.compatible_sub_lmb(&subgraph, g.full_crown(&subgraph), &lmb);
2285 assert_eq!(
2288 sub_lmb.loop_edges.raw,
2289 vec![EdgeIndex::from(0), EdgeIndex::from(2)]
2290 );
2291 for (edge, expected) in [(0, [1, 0]), (2, [0, 1])] {
2292 let signature = &sub_lmb.edge_signatures[EdgeIndex::from(edge)];
2293 assert_eq!(
2294 signature
2295 .internal
2296 .iter()
2297 .map(|sign| *sign * 1_i32)
2298 .collect::<Vec<_>>(),
2299 expected
2300 );
2301 assert!(
2302 signature
2303 .external
2304 .iter()
2305 .all(|sign| *sign == SignOrZero::Zero)
2306 );
2307 }
2308 assert_snapshot!(g.dot_lmb_of(&subgraph, &sub_lmb));
2309 }
2310
2311 #[test]
2312 fn shrunken_connected_subgraph() {
2313 let _guard = SHRUNKEN_LMB_TEST_LOCK.lock().unwrap();
2314 SHRUNKEN_LMB_TEST_INIT.call_once(|| test_initialise().unwrap());
2315 let g: Graph = dot!(digraph{
2316 edge[num=1 mass=1]
2317 node[num=1]
2318
2319 a:0->b:1[id=0]
2320 b:2->c:3[id=1]
2321 c:4->a:5[id=2]
2322 })
2323 .unwrap();
2324
2325 let outer = g.full_filter();
2326 let mut shrunken_filter: SuBitGraph = g.empty_subgraph();
2327 let shrunken_edge = EdgeIndex::from(0);
2328 shrunken_filter.add(g[&shrunken_edge].1);
2329 let shrunken =
2330 InternalSubGraph::try_new(shrunken_filter, &g.underlying).expect("valid subgraph");
2331 let remainder = outer.subtract(&shrunken.filter);
2332
2333 let lmb = g.shrunken_lmb_of(&outer, &shrunken);
2334
2335 assert!(
2336 !lmb.loop_edges
2337 .iter()
2338 .any(|edge| shrunken.filter.includes(&g[edge].1))
2339 );
2340 assert_snapshot!(g.dot_lmb_of(&remainder, &lmb));
2341 }
2342
2343 #[test]
2344 fn shrunken_disconnected_subgraph() {
2345 let _guard = SHRUNKEN_LMB_TEST_LOCK.lock().unwrap();
2346 SHRUNKEN_LMB_TEST_INIT.call_once(|| test_initialise().unwrap());
2347 let g: Graph = dot!(digraph{
2348 edge[num=1 mass=1]
2349 node[num=1]
2350
2351 a:0->b:1[id=0]
2352 b:2->c:3[id=1]
2353 c:4->a:5[id=2]
2354
2355 d:6->e:7[id=3]
2356 e:8->f:9[id=4]
2357 f:10->d:11[id=5]
2358
2359 c:12->d:13[id=6]
2360 })
2361 .unwrap();
2362
2363 let outer = g.full_filter();
2364 let mut shrunken_filter: SuBitGraph = g.empty_subgraph();
2365 for edge in [EdgeIndex::from(0), EdgeIndex::from(3)] {
2366 shrunken_filter.add(g[&edge].1);
2367 }
2368 let shrunken =
2369 InternalSubGraph::try_new(shrunken_filter, &g.underlying).expect("valid subgraph");
2370 let remainder = outer.subtract(&shrunken.filter);
2371
2372 let lmb = g.shrunken_lmb_of(&outer, &shrunken);
2373
2374 assert!(
2375 !lmb.loop_edges
2376 .iter()
2377 .any(|edge| shrunken.filter.includes(&g[edge].1))
2378 );
2379 let first = &lmb.edge_signatures[EdgeIndex(1)].internal;
2383 let second = &lmb.edge_signatures[EdgeIndex(4)].internal;
2384 assert_eq!(first, &lmb.edge_signatures[EdgeIndex(2)].internal);
2385 assert_eq!(second, &lmb.edge_signatures[EdgeIndex(5)].internal);
2386 assert!(
2387 lmb.edge_signatures[EdgeIndex(6)]
2388 .internal
2389 .iter()
2390 .all(|sign| *sign == SignOrZero::Zero)
2391 );
2392 for (_, edge, _) in g.iter_edges_of(&remainder) {
2393 assert!(
2394 lmb.edge_signatures[edge]
2395 .external
2396 .iter()
2397 .all(|sign| *sign == SignOrZero::Zero)
2398 );
2399 }
2400 let first_norm: i64 = first.iter().map(|sign| (*sign * 1_i64).pow(2)).sum();
2401 let second_norm: i64 = second.iter().map(|sign| (*sign * 1_i64).pow(2)).sum();
2402 let overlap: i64 = first
2403 .iter()
2404 .zip(second.iter())
2405 .map(|(left, right)| (*left * 1_i64) * (*right * 1_i64))
2406 .sum();
2407 assert!(
2408 first_norm * second_norm > overlap.pow(2),
2409 "both surviving cycle flows must be independent"
2410 );
2411 }
2412
2413 #[test]
2414 fn shrunken_edge_outside_outer_errors() {
2415 let _guard = SHRUNKEN_LMB_TEST_LOCK.lock().unwrap();
2416 SHRUNKEN_LMB_TEST_INIT.call_once(|| test_initialise().unwrap());
2417 let g: Graph = dot!(digraph{
2418 edge[num=1 mass=1]
2419 node[num=1]
2420
2421 a:0->b:1[id=0]
2422 b:2->c:3[id=1]
2423 c:4->a:5[id=2]
2424 })
2425 .unwrap();
2426
2427 let mut shrunken_filter: SuBitGraph = g.empty_subgraph();
2428 let shrunken_edge = EdgeIndex::from(0);
2429 shrunken_filter.add(g[&shrunken_edge].1);
2430 let shrunken =
2431 InternalSubGraph::try_new(shrunken_filter, &g.underlying).expect("valid subgraph");
2432 let outer = g.full_filter().subtract(&shrunken.filter);
2433
2434 let result = g.shrunken_sub_lmb(&outer, &shrunken, g.full_crown(&outer), None);
2435
2436 assert!(matches!(result, Err(LmbError::ShrunkenOutsideOuter { .. })));
2437 }
2438
2439 #[test]
2440 fn shrunken_whole_component_retains_only_surviving_external_endpoints() {
2441 use linnet::half_edge::subgraph::SubSetLike;
2442
2443 let _guard = SHRUNKEN_LMB_TEST_LOCK.lock().unwrap();
2444 SHRUNKEN_LMB_TEST_INIT.call_once(|| test_initialise().unwrap());
2445 let g: Graph = dot!(digraph {
2446 edge[num=1 mass=1]
2447 node[num=1]
2448 b:17 -> a:0 [id=0]
2449 a:1 -> e:2 [id=1]
2450 a:3 -> f:4 [id=2]
2451 b:5 -> c:6 [id=3]
2452 b:7 -> d:8 [id=4]
2453 c:9 -> d:10 [id=5]
2454 c:11 -> f:12 [id=6]
2455 d:13 -> e:14 [id=7]
2456 e:15 -> f:16 [id=8]
2457 x:18 -> y:19 [id=9]
2458 })
2459 .unwrap();
2460 let mut outer: SuBitGraph = g.empty_subgraph();
2461 let mut shrunken_filter: SuBitGraph = g.empty_subgraph();
2462 for edge in [1, 2, 8].map(EdgeIndex::from) {
2463 shrunken_filter.add(g[&edge].1);
2464 }
2465 outer.union_with(&shrunken_filter);
2466 for edge in [3, 4, 5].map(EdgeIndex::from) {
2467 outer.add(g[&edge].1);
2468 }
2469 let shrunken = InternalSubGraph::try_new(shrunken_filter, &g.underlying).unwrap();
2470 let remainder = outer.subtract(&shrunken.filter);
2471 let externals = g.full_crown(&outer);
2472 let parent = g.lmb_impl(&outer, &outer, externals.clone()).unwrap();
2473 assert_eq!(parent.loop_edges.len(), 2);
2474
2475 let mut contracted = g.underlying.to_ref();
2476 let root = shrunken.included_iter().next().unwrap();
2477 contracted.identify_nodes_of_subgraph_without_self_edges::<_, SuBitGraph>(
2478 &shrunken,
2479 &g.underlying[g.node_id(root)],
2480 );
2481 contracted.forget_identification_history();
2482 let surviving_crown = contracted.full_crown(&remainder);
2483 assert_eq!(
2484 surviving_crown.included_iter().collect::<Vec<_>>(),
2485 [Hedge(11), Hedge(13), Hedge(17)]
2486 );
2487 assert!(matches!(
2488 contracted.lmb_impl(&remainder, &remainder, externals.clone()),
2489 Err(LmbError::ExternalsOutsideSubgraph { .. })
2490 ));
2491
2492 for parent_lmb in [None, Some(&parent)] {
2493 let lmb = g
2494 .shrunken_sub_lmb(&outer, &shrunken, externals.clone(), parent_lmb)
2495 .unwrap();
2496 assert_eq!(lmb.loop_edges.len(), 1);
2497 assert!(
2498 remainder.includes(&g[&lmb.loop_edges[crate::momentum::sample::LoopIndex(0)]].1)
2499 );
2500 let mut external_edges = lmb.ext_edges.iter().copied().collect::<Vec<_>>();
2501 external_edges.sort();
2502 assert_eq!(external_edges, [EdgeIndex(0), EdgeIndex(6), EdgeIndex(7)]);
2503 let loop_rows = [3, 4, 5].map(|edge| {
2506 *lmb.edge_signatures[EdgeIndex(edge)]
2507 .internal
2508 .iter()
2509 .next()
2510 .unwrap()
2511 * 1_i32
2512 });
2513 assert_eq!(loop_rows[0] + loop_rows[1], 0);
2514 assert_eq!(-loop_rows[0] + loop_rows[2], 0);
2515 assert_eq!(-loop_rows[1] - loop_rows[2], 0);
2516 }
2517
2518 let mut partial_filter: SuBitGraph = g.empty_subgraph();
2522 partial_filter.add(g[&EdgeIndex(1)].1);
2523 let partial = InternalSubGraph::try_new(partial_filter, &g.underlying).unwrap();
2524 let mut internal_externals = externals.clone();
2525 internal_externals.add(Hedge(3));
2526 let mut partial_parent_guide = outer.clone();
2529 for edge in [2, 3].map(EdgeIndex::from) {
2530 partial_parent_guide.sub(g[&edge].1);
2531 }
2532 let partial_parent = g
2533 .lmb_impl(&outer, &partial_parent_guide, internal_externals.clone())
2534 .unwrap();
2535 let mut partial_carriers = partial_parent
2536 .loop_edges
2537 .iter()
2538 .copied()
2539 .collect::<Vec<_>>();
2540 partial_carriers.sort();
2541 assert_eq!(partial_carriers, [EdgeIndex(2), EdgeIndex(3)]);
2542 for parent_lmb in [None, Some(&partial_parent)] {
2543 let lmb = g
2544 .shrunken_sub_lmb(&outer, &partial, internal_externals.clone(), parent_lmb)
2545 .unwrap();
2546 assert_eq!(lmb.loop_edges.len(), 2);
2547 assert!(lmb.ext_edges.contains(&EdgeIndex(2)));
2548 }
2549
2550 let mut invalid_externals = externals;
2553 invalid_externals.add(Hedge(18));
2554 let invalid = g.shrunken_sub_lmb(&outer, &shrunken, invalid_externals, None);
2555 assert!(matches!(
2556 invalid,
2557 Err(LmbError::ExternalsOutsideSubgraph { .. })
2558 ));
2559 }
2560}