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