Skip to main content

gammalooprs/graph/
lmb.rs

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>, //It should have length = to number of externals (not number of independent externals)
45    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        // Cached LMBs are cloned to their live lengths. Allow a word of slack
169        // for the bitset and charge the owned coordinate vectors explicitly.
170        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        // let ext_id = ExternalIndex::from(self.ext_edges.len());
208        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
252/// Helpers for constructing loop-momentum bases and turning them into Symbolica
253/// replacement rules.
254///
255/// The replacement methods decompose an edge momentum into its loop-dependent
256/// and external-flow parts using a [`LoopMomentumBasis`]. The basis-building
257/// methods pick those signatures from spanning forests of a graph or subgraph.
258pub trait LMBext {
259    /// Enumerate all loop-momentum bases induced by spanning forests of
260    /// `subgraph`.
261    ///
262    /// Each spanning forest covering the same nodes as `subgraph` produces one
263    /// basis. Empty subgraphs return an empty list.
264    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    /// Enumerate all loop-momentum bases for the full graph.
276    fn generate_loop_momentum_bases(&self) -> TiVec<LmbIndex, LoopMomentumBasis>;
277
278    /// Replace `EMRmom(edge, ..)` by a UV-recursion-friendly decomposition.
279    ///
280    /// The loop-dependent part stays wrapped in `EMRmom(...)`, but its index is
281    /// rewritten from the concrete edge id to the loop-basis edge selected by
282    /// `lmb`. The external-flow contribution is added explicitly.
283    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    /// Spatial-vector variant of [`Self::uv_wrapped_replacement`].
324    ///
325    /// This uses `Q3` for both the matched pattern and the wrapped loop
326    /// contribution, preserving the represented or indexed Minkowski structure
327    /// on every routed basis vector. Explicit components use `Q(edge,cind(i))`.
328    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    /// Replace `EMRmom(edge, ..)` by the explicit loop-plus-external momentum
369    /// carried by that edge.
370    ///
371    /// `filter_pair` can restrict which edge kinds are rewritten, for example to
372    /// skip split or unpaired half-edge pairs in contexts that only want full
373    /// propagators.
374    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    /// Replace `EMRmom(edge, ..)` by the integrand momentum variables
407    /// `K(...) + P(...)`, i.e. `GS.loop_mom(...) + GS.external_mom(...)`.
408    ///
409    /// Unlike the UV-wrapped replacements, the generated terms are expressed in
410    /// the loop/external variable families used in the integrand rather than in
411    /// `EMRmom`.
412    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    /// Core implementation shared by the public replacement constructors.
444    ///
445    /// For each edge of `subgraph` whose [`HedgePair`] passes `filter_pair`, this
446    /// computes the loop-dependent and external-flow atoms from `lmb` and passes
447    /// them to `rep`.
448    ///
449    /// `rep` is the final replacement builder: it receives the original edge id,
450    /// the loop-dependent atom, and the external-flow atom, and returns the
451    /// `Replacement` inserted into the result vector.
452    ///
453    /// `loop_symbol` and `ext_symbol` select the function symbol used for the
454    /// generated loop and external terms, while `loop_args` and `ext_args` are
455    /// appended to those function calls. When `emr_id` is `true`, the generated
456    /// loop/external indices are the concrete edge ids stored in the basis;
457    /// otherwise they are the compact loop/external basis positions.
458    #[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    /// Build a loop-momentum basis for `subgraph` using `tree` as the spanning
475    /// forest guide and `externals` as the external-flow carriers.
476    ///
477    /// `tree` must cover the same nodes as `subgraph`. `externals` must only
478    /// contain nodes from `subgraph`; it chooses which external edges are treated
479    /// as true external flows and which one in each connected component becomes
480    /// the dependent external.
481    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    /// Construct one canonical loop-momentum basis for `subgraph`.
491    ///
492    /// This uses `subgraph` itself as the forest guide and the full crown of the
493    /// subgraph as its external carriers.
494    fn lmb_of<S: SubGraphLike<Base = SuBitGraph>>(&self, subgraph: &S) -> LoopMomentumBasis;
495
496    /// Construct the canonical loop-momentum basis for the full graph.
497    fn lmb(&self) -> LoopMomentumBasis;
498
499    /// Build the LMB for `outer - shrunken` while each connected component of
500    /// `shrunken` acts as a contracted passage node. When supplied, the parent
501    /// LMB selects a compatible subset of its loop carriers on the contracted
502    /// topology instead of independently choosing a canonical basis.
503    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    /// Construct the canonical shrunken-subgraph LMB using the full crown of
512    /// `outer` as external-flow carriers.
513    fn shrunken_lmb_of(&self, outer: &SuBitGraph, shrunken: &InternalSubGraph)
514    -> LoopMomentumBasis;
515
516    /// Construct a basis for `subgraph` that reuses loop edges from `lmb`
517    /// whenever the induced cut still spans the same connected components.
518    ///
519    /// This is used when descending into a subgraph while keeping its loop
520    /// variables compatible with a parent basis.
521    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    /// Fallible form of [`Self::compatible_sub_lmb`] for callers that must handle
535    /// an unavailable parent-compatible basis without panicking. The default
536    /// preserves compatibility with external trait implementations that only
537    /// implement the original infallible method.
538    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    /// Construct a basis from a chosen cotree of `subgraph`.
555    ///
556    /// The cotree is converted into the corresponding tree by subtracting it
557    /// from `subgraph`, then forwarded to [`Self::lmb_impl`].
558    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    /// Return the empty basis with no loop or external generators.
575    fn empty_lmb(&self) -> LoopMomentumBasis;
576
577    /// Render a DOT graph whose edge labels show the explicit momentum carried by
578    /// each edge according to `lmb`.
579    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        // A fully contracted component can have no node in the remainder.
674        // Remove only its retired crown endpoints; the opposite endpoints at
675        // surviving nodes remain external flows. Unrelated invalid externals
676        // have already been rejected against the original outer footprint.
677        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        // the subgraph may have disconnected components in case the of disjoint graphs in a spinney
785        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                //this is a self-loop
805                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                // let externals = self.full_crown(subgraph);
816
817                return self.lmb_impl(subgraph.included(), &cut_subgraph, externals.clone());
818            }
819
820            //
821        }
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            // Momentum-label rendering assumes a dimensionally valid parent LMB. Preserve the
833            // topology diagnostics without panicking while constructing the fallible error.
834            (
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    /// The true externals (that will flow through the graph (i.e. not dummy)) are those that are both in the subgraph and in the externals
852    fn lmb_impl<S: SubGraphLike + SubSetOps + ModifySubSet<HedgePair> + ModifySubSet<Hedge>>(
853        &self,
854        subgraph: &S,
855        forest_guide: &S, //guide for the forest (can be the full subgraph if no guide necessary), however it must cover the same nodes as subgraph
856        mut externals: S, //externals to consider for the flow, cannot contain non-subgraph nodes
857    ) -> LmbResult<LoopMomentumBasis>
858    where
859        S::Base: ModifySubSet<Hedge> + SubGraphLike,
860    {
861        // println!(
862        //     "//Lmb of subgraph:\n{}\n//Forest_guide:\n{}//Externals:\n{}",
863        //     self.dot(subgraph),
864        //     self.dot(forest_guide),
865        //     self.dot(&externals),
866        // );
867
868        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        // External flows are signed paths (one half-edge per direction). A boundary
876        // flow contains the dependent external; a paired crown flow joins its two
877        // incidences without changing the net external momentum.
878        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            //we keep removing hedges from not_seen until it is empty
896            // we need to get the first root
897            // if the externals are not yet empty then take from them
898            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                // A paired crown edge injects and removes the same momentum within
912                // this component. It cannot balance the net external momentum.
913                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                    // A singleton component has an empty spanning forest after external
926                    // hairs are stripped. Retain its root, including for contact graphs
927                    // and tadpoles, so its external flows still conserve momentum.
928                    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                // println!(
946                //     "//External cover:\n{}//of \n{}",
947                //     self.dot(&external_cover),
948                //     self.dot(&tree.tree_subgraph)
949                // );
950
951                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                            // Both incidences of a crown edge retain the same physical
1005                            // momentum. Join their root paths with opposite signs; the
1006                            // shared part cancels, leaving the path between its ends.
1007                            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            //remove all edges in cover+node_crowns from not_seen and externals
1050            //if the edge is a non-tree, full internal edge then it is a loop edge
1051            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                                // A contracted UV component can turn a retained edge into
1065                                // a tadpole. Its positive generator follows the same source
1066                                // half-edge convention as `SignedCycle::from_cycle` below.
1067                                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        // for (i, e) in external_flows.iter().enumerate() {
1122        //     println!(
1123        //         "//Ext flow {} for {}: \n{}",
1124        //         e.0,
1125        //         ext_edges[ExternalIndex(i)],
1126        //         self.dot(&e.1)
1127        //     );
1128        // }
1129
1130        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                        // if dep_ext.is_some() {
1137                        // external.push(SignOrZero::Zero);
1138                        // }
1139
1140                        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); //This is the dependent momentum
1163                                            } 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); //This is the dependent momentum
1184                                            } 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            // println!("{}", self.dot(&s));
1255            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                // println!("{e}");
1289                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                // println!("{loop_expr}");
1293
1294                // println!("{external_expr}");
1295                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(crate) fn spatial_emr<T: FloatLike>(
1753    //     &self,
1754    //     sample: &BareMomentumSample<T>,
1755    // ) -> Vec<ThreeMomentum<F<T>>> {
1756    //     let three_externals: ExternalThreeMomenta<F<T>> = sample
1757    //         .external_moms
1758    //         .iter()
1759    //         .map(|m| m.spatial.clone())
1760    //         .collect();
1761    //     self.edge_signatures
1762    //         .borrow()
1763    //         .into_iter()
1764    //         .map(|(_, sig)| sig.compute_momentum(&sample.loop_moms, &three_externals))
1765    //         .collect()
1766    // }
1767
1768    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 to_massless_emr<T: FloatLike>(
1807    //     &self,
1808    //     sample: &BareMomentumSample<T>,
1809    // ) -> Vec<FourMomentum<F<T>>> {
1810    //     self.edge_signatures
1811    //         .borrow()
1812    //         .into_iter()
1813    //         .map(|(_, sig)| {
1814    //             sig.compute_four_momentum_from_three(&sample.loop_moms, &sample.external_moms)
1815    //         })
1816    //         .collect()
1817    // }
1818
1819    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            // The outgoing dependent leg carries p3 = p0 + p1 - p2.
1897            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            // layout=neato
2035            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        // The bridge is external to each component; the two self-loop carriers
2286        // must occur only as loop momenta, never also as external coordinates.
2287        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        // Contracting one edge in each triangle preserves two independent loop flows.
2380        // On the quotient graph, conservation equates each retained edge pair and
2381        // leaves the bridge with zero momentum, for any valid choice of loop basis.
2382        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            // The surviving triangle conserves its loop flow at all three
2504            // vertices; each connector retains exactly its physical endpoint.
2505            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        // External-flow carriers can also be internal remainder hedges. After
2519        // contracting only e1, hedge 3 belongs to the old contracted crown but
2520        // remains on e2 at a surviving node, so it must retain its flow slot.
2521        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        // Both parent carriers must survive this contraction; the earlier
2527        // parent used e1, which is now contracted despite its loop remaining.
2528        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        // A crown intersection must not silently discard an unrelated invalid
2551        // external node alongside the legitimately retired component.
2552        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}