1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fmt::Display,
4};
5
6#[cfg(test)]
7use std::cmp::Reverse;
8
9use ahash::AHashMap;
10use eyre::{WrapErr, eyre};
11use gammaloop_tracing_filter::LogMessage;
12use idenso::{color::ColorSimplifier, shorthands::schoonschip::Schoonschip};
13use itertools::Itertools;
14use linnet::half_edge::{
15 HedgeGraph, NoData, NodeIndex,
16 algorithms::trace_unfold::{
17 HiddenData, Independence, TraceKey, TraceUnfold, UnfoldedTraceGraph,
18 },
19 involution::{EdgeIndex, Flow, HedgePair},
20 nodestore::{NodeStorageOps, NodeStorageVec},
21 subgraph::{Inclusion, InternalSubGraph, ModifySubSet, SuBitGraph, SubSetLike, SubSetOps},
22};
23use symbolica::{
24 atom::{Atom, AtomCore, FunctionBuilder},
25 function,
26};
27use tracing::debug;
28use vakint::Vakint;
29
30use crate::{
31 debug_tags,
32 graph::{
33 Graph, LMBext, LoopMomentumBasis,
34 cuts::CutSet,
35 parse::string_utils::{ToOrderedSimple, dot_attr_value},
36 },
37 utils::{GS, W_},
38 uv::{
39 ApproximationType, Integrands, RenormalizationPart, Spinney, UVgenerationSettings,
40 UltravioletGraph,
41 approx::{
42 CutStructure, ForestNodeLike, OrientationProjection, Rooted, UVCtx,
43 final_integrand::{FinalIntegrandBuilder, FinalIntegrands},
44 integrated::{Integrated, IntegratedCts},
45 local_3d::{Local3DApproximation, Local3DCts, Localizer},
46 local_4d::{self, Full4dCts, Local4dCts},
47 },
48 export::UVForestNodeExpression,
49 forest::ParametricIntegrands,
50 marker::UvMarker,
51 settings::VakintSettings,
52 },
53};
54use color_eyre::Result;
55use spenso::shadowing::symbolica_utils::{LogPrint, SpensoPrintSettings};
56
57pub struct Wood {
58 pub graph: HedgeGraph<SuBitGraph, Spinney>,
59 pub root: NodeIndex,
60 pub vakint_settings: vakint::VakintSettings,
61 cuts: CutStructure,
62}
63
64impl Independence<HiddenData<SuBitGraph, EdgeIndex>> for Wood {
65 fn independent(
66 &self,
67 a: &HiddenData<SuBitGraph, EdgeIndex>,
68 b: &HiddenData<SuBitGraph, EdgeIndex>,
69 ) -> bool {
70 !a.order.intersects(&b.order)
71 }
72}
73
74impl TraceUnfold<SuBitGraph> for Wood {
75 type EdgeData = SuBitGraph;
76 type HedgeData = NoData;
77 type NodeData = Spinney;
78 type NodeStorage = NodeStorageVec<Spinney>;
79
80 fn graph(
81 &self,
82 ) -> &HedgeGraph<Self::EdgeData, Self::NodeData, Self::HedgeData, Self::NodeStorage> {
83 &self.graph
84 }
85
86 fn key(&self, e: EdgeIndex) -> SuBitGraph {
87 self.graph[e].clone()
88 }
89
90 fn join_factors(&self, target: NodeIndex) -> Option<BTreeSet<SuBitGraph>> {
97 if self.graph[target].n_components() < 2 {
98 return None;
99 }
100
101 let factors = self
102 .graph
103 .iter_crown(target)
104 .filter(|hedge| self.graph.flow(*hedge) == Flow::Sink)
105 .map(|hedge| self.graph[self.graph[&hedge]].clone())
106 .collect::<BTreeSet<_>>();
107 if factors.len() != self.graph[target].n_components() {
108 return None;
109 }
110
111 let mut cover: Option<SuBitGraph> = None;
112 for factor in &factors {
113 if let Some(acc) = &mut cover {
114 if acc.intersects(factor) {
115 return None;
116 }
117 acc.union_with(factor);
118 } else {
119 cover = Some(factor.clone());
120 }
121 }
122
123 (cover.as_ref() == Some(self.graph[target].filter())).then_some(factors)
124 }
125}
126
127impl Wood {
128 pub fn current_given_pair<'a>(
129 &'a self,
130 edge_id: EdgeIndex,
131 order: usize,
132 ) -> (ForestNode<'a>, ForestNode<'a>) {
133 let HedgePair::Paired { source, sink } = self.graph[&edge_id].1 else {
134 panic!("edge in self is not paired");
135 };
136
137 let given = ForestNode {
139 spinney: &self.graph[self.graph.node_id(source)],
140 topo_order: order,
141 };
142
143 let current_for_h = self.graph.node_id(sink);
145
146 let current = ForestNode {
148 spinney: &self.graph[current_for_h],
149 topo_order: order,
150 };
151
152 (current, given)
153 }
154
155 pub(crate) fn new(cuts: CutStructure, graph: &Graph, settings: &UVgenerationSettings) -> Self {
156 let mut subgraph = graph.full_filter();
157 subgraph.subtract_with(&graph.initial_state_cut.left);
158 let mut spinneys = Vec::new();
159
160 for cut in cuts.cuts.iter() {
161 let cut_sub = subgraph.subtract(&cut.union);
162 spinneys.extend(graph.classified_spinneys(
163 &cut_sub,
164 settings,
165 &graph.loop_momentum_basis,
166 ));
167 }
168
169 Self::from_spinneys(spinneys, graph, cuts, &settings.vakint)
170 }
171
172 pub(crate) fn from_spinneys<I: IntoIterator<Item = Spinney>>(
173 s: I,
174 graph: &Graph,
175 cuts: CutStructure,
176 vakint_settings: &VakintSettings,
177 ) -> Self {
178 let mut max_loops = 0;
179 let mut spinneys = BTreeMap::new();
180 for spinney in s {
181 max_loops = max_loops.max(graph.n_loops(spinney.filter()));
182 spinneys.entry(spinney.filter().clone()).or_insert(spinney);
183 }
184 let empty = Spinney::empty(graph);
185 spinneys.entry(empty.filter().clone()).or_insert(empty);
186 let mut vakint_settings = vakint_settings.true_settings();
187 vakint_settings.number_of_terms_in_epsilon_expansion = max_loops as i64 + 1;
190
191 let mut unions = BTreeSet::new();
192 let g: HedgeGraph<_, _> = HedgeGraph::poset(spinneys.into_values());
193 let mut poset = g.map(
194 |_, _, v| v,
195 |_, n, pair, _, e| {
196 let HedgePair::Paired { source, sink } = pair else {
197 return e.map(|_| graph.as_ref().empty_subgraph());
198 };
199 let nsource = n.node_id_ref(source);
200 let nsink = n.node_id_ref(sink);
201
202 let source_subgraph = &n.get_node_data(nsource).subgraph;
203 let sink_subgraph = &n.get_node_data(nsink).subgraph;
204 let reduced_subgraph = sink_subgraph.subtract(source_subgraph).filter;
205 let hairy_source = graph.as_ref().full_crown(source_subgraph);
206
207 if graph.as_ref().bridges_of(&reduced_subgraph).is_empty()
208 && !hairy_source.intersects(&reduced_subgraph)
209 {
210 if !source_subgraph.is_empty() {
213 unions.insert(nsink);
214 }
215 e.map(|_| reduced_subgraph)
216 } else {
217 e.map(|_| sink_subgraph.filter.clone())
218 }
219 },
220 |_, d| d,
221 );
222
223 let mut to_remove: SuBitGraph = poset.empty_subgraph();
224
225 for u in unions {
229 let mut comps: BTreeSet<_> = graph
231 .as_ref()
232 .connected_components(&poset[u].subgraph)
233 .into_iter()
234 .collect();
235 for c in poset.iter_crown(u) {
236 let Flow::Sink = poset.flow(c) else {
237 continue;
238 };
239 let edge_id = poset[&c];
240 if comps.contains(&poset[edge_id]) {
241 comps.remove(&poset[edge_id]);
242 } else {
243 to_remove.add(c);
244 to_remove.add(poset.inv(c));
245 }
246 }
247 }
248
249 poset.delete_hedges(&to_remove);
250 let root = poset
251 .iter_nodes()
252 .find(|(_, _, s)| s.subgraph.is_empty())
253 .map(|(n, _, _)| n);
254
255 Wood {
256 graph: poset,
257 root: root.expect("no empty spinney found"),
258 cuts,
259 vakint_settings,
260 }
261 }
262
263 fn unfold_with_cached_node_label_atoms(self, cache_node_label_atoms: bool) -> Forests {
264 let unfolded = self.trace_unfold::<NodeStorageVec<_>>(self.root);
265 let graph = unfolded.map(|_, _, key| OperationNode { key });
266
267 let mut cuts: Vec<(SuBitGraph, CutSet)> = Vec::new();
268 for c in &self.cuts.cuts {
269 let mut compatible: SuBitGraph = graph.empty_subgraph();
270 for (_, crown, s) in graph.iter_nodes() {
271 if s.is_compatible_with(c) {
272 for h in crown {
273 compatible.add(h);
274 }
275 }
276 }
277 cuts.push((compatible, c.clone()));
278 }
279 let root = graph
280 .iter_nodes()
281 .find(|(_, _, operation)| operation.key.is_empty())
282 .map(|(node, _, _)| node)
283 .expect("no empty trace key found in unfolded hedge-poset forest");
284
285 let forests = Forests {
286 graph,
287 cuts,
288 root,
289 cached_node_label_atoms: cache_node_label_atoms.then(AHashMap::new),
290 compute_store: ComputeStore::default(),
291 wood: self,
292 };
293
294 if cache_node_label_atoms {
295 forests.with_cached_node_label_atoms()
296 } else {
297 forests
298 }
299 }
300
301 pub fn unfold(self) -> Forests {
302 self.unfold_with_cached_node_label_atoms(true)
303 }
304
305 pub fn unfold_uncached(self) -> Forests {
306 self.unfold_with_cached_node_label_atoms(false)
307 }
308}
309
310impl Display for Wood {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 self.graph.dot_impl_fmt(
313 f,
314 &self.graph.full_filter(),
315 "start=2;\n",
316 &|_| None,
317 &|a| Some(format!("label=\"{}\"", a.string_label())),
318 &|v| Some(format!("label=\"{}\"", v.subgraph.string_label())),
319 )
320 }
321}
322#[derive(Default)]
331pub struct ComputeStore {
332 entries: AHashMap<OperationNode, ComputeNode>,
333}
334
335impl ComputeStore {
336 fn get(&self, key: &OperationNode) -> Option<&ComputeNode> {
337 self.entries.get(key)
338 }
339
340 fn require(&self, key: &OperationNode) -> Result<&ComputeNode> {
341 self.get(key)
342 .ok_or_else(|| eyre!("{key} not yet added to compute store"))
343 }
344
345 fn entry(
346 &mut self,
347 key: OperationNode,
348 ) -> std::collections::hash_map::Entry<'_, OperationNode, ComputeNode> {
349 self.entries.entry(key)
350 }
351}
352
353pub struct Forests {
354 pub graph: UnfoldedTraceGraph<Wood, OperationNode, NodeStorageVec<OperationNode>>,
355 pub root: NodeIndex,
356 cuts: Vec<(SuBitGraph, CutSet)>,
358 cached_node_label_atoms: Option<AHashMap<NodeIndex, Atom>>,
359 pub compute_store: ComputeStore,
360 wood: Wood,
361}
362
363#[derive(Clone, Debug, Hash, PartialEq, Eq)]
364pub struct OperationNode {
365 pub key: TraceKey<SuBitGraph, EdgeIndex>,
366}
367
368pub struct ForestNode<'a> {
369 pub spinney: &'a Spinney,
370 pub topo_order: usize,
371}
372
373pub struct OwnedForestNode {
374 pub spinney: Spinney,
375 pub topo_order: usize,
376}
377
378#[cfg(test)]
379struct LocalLeafOperation {
380 op: HiddenData<SuBitGraph, EdgeIndex>,
381 frontier: NodeIndex,
382}
383
384struct UnionReplayState {
385 integrated: NodeIndex,
386 local_edges: Vec<EdgeIndex>,
387}
388
389#[cfg(test)]
390impl LocalLeafOperation {
391 fn new(op: &HiddenData<SuBitGraph, EdgeIndex>, frontier: NodeIndex) -> Self {
392 Self {
393 op: op.clone(),
394 frontier,
395 }
396 }
397}
398
399impl OperationNode {
400 pub fn is_compatible_with(&self, cut: &CutSet) -> bool {
401 self.covers().is_none_or(|c| !c.intersects(&cut.union))
402 }
403
404 pub fn current<'a>(&'a self, wood: &'a Wood, topo_order: usize) -> Option<Vec<ForestNode<'a>>> {
405 if self.key.is_empty() {
406 return None;
407 }
408
409 Some(
410 self.key
411 .iter_leaf_ops()
412 .map(|op| {
413 let HedgePair::Paired { sink, .. } = wood.graph[&op.data].1 else {
414 panic!("edge in trace key is not paired");
415 };
416 let spinney = &wood.graph[wood.graph.node_id(sink)];
417 ForestNode {
418 spinney,
419 topo_order,
420 }
421 })
422 .collect::<Vec<_>>(),
423 )
424 }
425}
426
427impl AsRef<TraceKey<SuBitGraph, EdgeIndex>> for OperationNode {
428 fn as_ref(&self) -> &TraceKey<SuBitGraph, EdgeIndex> {
429 &self.key
430 }
431}
432
433impl LogMessage for ForestNode<'_> {
434 fn log_display(&self) -> String {
435 format!(
436 "subgraph={}, topo_order={}, dod={}",
437 self.spinney.filter().string_label(),
438 self.topo_order,
439 self.spinney.dod
440 )
441 }
442}
443
444impl ForestNodeLike for ForestNode<'_> {
445 fn dod(&self) -> i32 {
446 self.spinney.dod
447 }
448 fn renormalization_scheme(&self) -> crate::uv::ApproximationType {
449 self.spinney.renormalization_scheme
450 }
451 fn lmb(&self) -> &LoopMomentumBasis {
452 &self.spinney.lmb
453 }
454 fn reduced_subgraph(&self, given: &Self) -> SuBitGraph {
455 self.spinney
456 .subgraph
457 .subtract(&given.spinney.subgraph)
458 .filter
459 }
460 fn subgraph(&self) -> &SuBitGraph {
461 self.spinney.filter()
462 }
463 fn topo_order(&self) -> usize {
464 self.topo_order
465 }
466}
467
468impl LogMessage for OwnedForestNode {
469 fn log_display(&self) -> String {
470 format!(
471 "subgraph={}, topo_order={}, dod={}",
472 self.spinney.filter().string_label(),
473 self.topo_order,
474 self.spinney.dod
475 )
476 }
477}
478
479impl ForestNodeLike for OwnedForestNode {
480 fn dod(&self) -> i32 {
481 self.spinney.dod
482 }
483
484 fn renormalization_scheme(&self) -> crate::uv::ApproximationType {
485 self.spinney.renormalization_scheme
486 }
487
488 fn lmb(&self) -> &LoopMomentumBasis {
489 &self.spinney.lmb
490 }
491
492 fn reduced_subgraph(&self, given: &Self) -> SuBitGraph {
493 self.spinney
494 .subgraph
495 .subtract(&given.spinney.subgraph)
496 .filter
497 }
498
499 fn subgraph(&self) -> &SuBitGraph {
500 self.spinney.filter()
501 }
502
503 fn topo_order(&self) -> usize {
504 self.topo_order
505 }
506}
507
508impl Display for OperationNode {
509 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
510 if self.key.is_empty() {
511 write!(f, "∅")
512 } else {
513 self.key.write_foata_like(f, |op| op.string_label())
514 }
515 }
516}
517
518impl OperationNode {
519 fn foata_level_labels(&self) -> String {
520 self.key
521 .iter_levels_top_down()
522 .map(|level| {
523 level
524 .iter_leaf_ops()
525 .map(|op| op.order.string_label())
526 .join(",")
527 })
528 .join(";")
529 }
530
531 pub fn covers(&self) -> Option<SuBitGraph> {
532 let mut acc: Option<SuBitGraph> = None;
533
534 for level in self.key.iter_levels_top_down() {
535 for op in level.iter_leaf_ops() {
536 if let Some(a) = &mut acc {
537 a.union_with(&op.order);
538 } else {
539 acc = Some(op.order.clone());
540 }
541 }
542 }
543
544 acc
545 }
546
547 fn forest_node(&self, graph: &Graph, topo_order: usize) -> OwnedForestNode {
548 let spinney = match self.covers() {
549 Some(cover) if !cover.is_empty() => {
550 let subgraph = InternalSubGraph::cleaned_filter_optimist(cover, graph.as_ref());
551 Spinney::new(subgraph, graph, &graph.loop_momentum_basis)
552 .expect("operation cover should define a valid spinney")
553 }
554 _ => Spinney::empty(graph),
555 };
556
557 OwnedForestNode {
558 spinney,
559 topo_order,
560 }
561 }
562
563 pub fn to_atom(&self) -> Atom {
564 let mut acc = Atom::one();
565
566 let approx = FunctionBuilder::new(GS.uv_approx);
567 let mut levels = self.key.iter_levels_top_down();
568 let Some(first_level) = levels.next() else {
569 return acc;
570 };
571
572 let Some(first_op) = first_level.iter_leaf_ops().next() else {
573 return acc;
574 };
575
576 let mut last = SuBitGraph::empty(first_op.order.size());
577 for l in std::iter::once(first_level).chain(levels) {
578 let last_sym = if last.is_empty() {
579 Atom::Zero
580 } else {
581 last.symbol().to_atom()
582 };
583
584 let mut mul = Atom::one();
585
586 for op in l.iter_leaf_ops() {
587 let new = function!(op.order.symbol(), usize::from(op.data));
588 mul *= approx.clone().add_arg((new - &last_sym) * &acc).finish();
589 last.union_with(&op.order);
590 }
591
592 acc = mul
593 }
594
595 acc
596 }
597
598 }
602
603#[derive(Default)]
604pub struct ComputeNode {
605 local_4d: Option<Local4dCts>,
606 integrated: Option<IntegratedCts>,
607 cuts: AHashMap<CutSet, CutComputation>,
608}
609
610pub struct CutComputation {
611 local_3d: Local3DCts,
612 final_integrands: FinalIntegrands,
613}
614
615impl ComputeNode {
616 fn local_4d(&self, operation: &OperationNode) -> Result<&Local4dCts> {
617 self.local_4d
618 .as_ref()
619 .ok_or_else(|| eyre!("{operation} has no computed local 4D counterterm"))
620 }
621
622 fn integrated(&self, operation: &OperationNode) -> Result<&IntegratedCts> {
623 self.integrated
624 .as_ref()
625 .ok_or_else(|| eyre!("{operation} has no computed integrated counterterm"))
626 }
627
628 fn cut(&self, operation: &OperationNode, cutset: &CutSet) -> Result<&CutComputation> {
629 self.cuts.get(cutset).ok_or_else(|| {
630 eyre!("{operation} has no computed local counterterms for cut {cutset:?}")
631 })
632 }
633}
634
635impl Forests {
636 fn source_spinney(&self, node: NodeIndex) -> &Spinney {
637 &self.wood.graph[self.graph.source_node(node)]
638 }
639
640 fn recursion_input_4d(&self, node: NodeIndex) -> Result<Full4dCts> {
641 let operation = &self.graph[node];
642 let computed = self.compute_store.require(operation)?;
643 if self.graph.is_disjoint_union(node) {
644 return Ok(Full4dCts::from_factorized_local(
645 computed.local_4d(operation)?,
646 ));
647 }
648 Full4dCts::recursion_input(
649 computed.local_4d(operation)?,
650 computed.integrated(operation)?,
651 self.source_spinney(node).renormalization_scheme,
652 node == self.root,
653 )
654 }
655
656 fn cached_node_label_atom(&self, node: NodeIndex) -> Option<Atom> {
657 self.cached_node_label_atoms
658 .as_ref()
659 .and_then(|labels| labels.get(&node))
660 .cloned()
661 }
662
663 fn node_label_atom_factor(
664 &self,
665 frontier: NodeIndex,
666 op: &HiddenData<SuBitGraph, EdgeIndex>,
667 ) -> Atom {
668 let approx = FunctionBuilder::new(GS.uv_approx);
669 let frontier_atom = self.node_label_atom(frontier);
670
671 let current = function!(op.order.symbol(), usize::from(op.data));
672 let argument = if self.graph[frontier].covers().is_none() {
673 current
674 } else {
675 let previous = self.graph[frontier]
676 .covers()
677 .expect("non-empty frontier cover must exist")
678 .symbol();
679 (current - previous) * frontier_atom
680 };
681 approx.add_arg(argument).finish()
682 }
683
684 fn node_label_atom(&self, node: NodeIndex) -> Atom {
685 if self.graph[node].key.is_empty() {
686 return Atom::one();
687 }
688
689 if let Some(cached) = self.cached_node_label_atom(node) {
690 return cached;
691 }
692
693 self.graph
694 .leaf_op_dependency_frontiers(node, &self.wood)
695 .fold(Atom::one(), |acc, (op, frontier)| {
696 acc * self.node_label_atom_factor(frontier, op)
697 })
698 }
699
700 fn with_cached_node_label_atoms(mut self) -> Self {
701 self.cache_node_label_atoms();
702 self
703 }
704
705 fn cache_node_label_atoms(&mut self) {
706 self.cached_node_label_atoms = Some(AHashMap::new());
707 for nidx in self.graph.topo_sort_kahn().unwrap() {
708 let atom = self.node_label_atom(nidx);
709 self.cached_node_label_atoms
710 .as_mut()
711 .expect("node-label cache was initialized")
712 .insert(nidx, atom);
713 }
714 }
715
716 fn node_label(&self, node: NodeIndex) -> String {
717 let key = &self.graph[node];
718 if key.key.is_empty() {
719 return "∅".to_string();
720 }
721
722 let mut foata = String::new();
723 key.key
724 .write_foata_like(&mut foata, |op| op.string_label())
725 .expect("writing a trace key into a string must succeed");
726 let atom = self.node_label_atom(node);
727 format!("{foata}: {}", atom.to_ordered_simple())
728 }
729
730 fn dot_serialize_expr_atom() -> Atom {
731 Atom::var(GS.expr)
732 }
733
734 fn dot_serialize_node_atom_factor(
735 &self,
736 frontier: NodeIndex,
737 op: &HiddenData<SuBitGraph, EdgeIndex>,
738 ) -> Atom {
739 let frontier_depth = self.graph[frontier].key.op_count();
740 let (current, given) = self.wood.current_given_pair(op.data, frontier_depth);
741 function!(
744 GS.uv_approx,
745 UvMarker::subgraph(current.subgraph(), given.subgraph())
746 * self.dot_serialize_node_atom(frontier)
747 )
748 }
749
750 fn dot_serialize_node_atom(&self, node: NodeIndex) -> Atom {
751 if self.graph[node].key.is_empty() {
752 return Self::dot_serialize_expr_atom();
753 }
754
755 self.graph
756 .leaf_op_dependency_frontiers(node, &self.wood)
757 .fold(Atom::one(), |acc, (op, frontier)| {
758 acc * self.dot_serialize_node_atom_factor(frontier, op)
759 })
760 }
761
762 fn dot_serialize_node_attrs(&self, node: NodeIndex) -> String {
763 let key = &self.graph[node];
764 let label = self
765 .dot_serialize_node_atom(node)
766 .printer(SpensoPrintSettings::typst_options())
767 .to_string();
768 let cover = key
769 .covers()
770 .unwrap_or_else(|| self.graph.empty_subgraph())
771 .string_label();
772
773 format!(
774 "label={} foata={} cover={}",
775 dot_attr_value(&label),
776 dot_attr_value(&key.foata_level_labels()),
777 dot_attr_value(&cover),
778 )
779 }
780
781 pub fn dot_serialize(&self) -> String {
782 let mut output = String::new();
783 self.dot_serialize_fmt(&mut output)
784 .expect("writing hedge-poset forest DOT into a string must succeed");
785 output
786 }
787
788 pub fn dot_serialize_fmt(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
789 let attrs: AHashMap<_, _> = self
790 .graph
791 .iter_nodes()
792 .map(|(node, _, key)| (key.clone(), self.dot_serialize_node_attrs(node)))
793 .collect();
794
795 self.graph.dot_impl_fmt(
796 writer,
797 &self.graph.full_filter(),
798 "start=2;\n",
799 &|_| None,
800 &|_| None,
801 &|v| Some(attrs[v].clone()),
802 )
803 }
804
805 fn compatible_topological_order(&self, subset: &SuBitGraph) -> Result<Vec<NodeIndex>> {
806 let mut order = self.graph.topo_sort_kahn_of(subset)?;
807
808 if let Some(root_position) = order.iter().position(|node| *node == self.root) {
809 if root_position != 0 {
810 let root = order.remove(root_position);
811 order.insert(0, root);
812 }
813 } else {
814 order.insert(0, self.root);
815 }
816
817 Ok(order)
818 }
819
820 #[cfg(test)]
821 fn local_leaf_operations(&self, node: NodeIndex) -> Vec<LocalLeafOperation> {
822 let mut leaves = self
823 .graph
824 .leaf_op_dependency_frontiers(node, &self.wood)
825 .map(|(op, frontier)| LocalLeafOperation::new(op, frontier))
826 .collect::<Vec<_>>();
827
828 leaves.sort_by_key(|leaf| {
829 (
830 Reverse(self.graph[leaf.frontier].key.op_count()),
831 leaf.op.order.clone(),
832 usize::from(leaf.op.data),
833 )
834 });
835 leaves
836 }
837
838 fn disconnected_component_nodes(&self, node: NodeIndex) -> Result<Vec<NodeIndex>> {
839 let operation = &self.graph[node];
840 let factors = self
841 .wood
842 .join_factors(self.graph.source_node(node))
843 .ok_or_else(|| eyre!("{operation} has no disconnected component factors"))?;
844 let target_ops = operation
845 .key
846 .iter_levels_top_down()
847 .flat_map(|level| level.iter_leaf_ops())
848 .cloned()
849 .collect::<Vec<_>>();
850
851 for op in &target_ops {
852 let owners = factors
853 .iter()
854 .filter(|factor| factor.includes(&op.order))
855 .count();
856 if owners != 1 {
857 return Err(eyre!(
858 "operation {} in {operation} belongs to {owners} disconnected factors",
859 op.order.string_label()
860 ));
861 }
862 }
863
864 factors
865 .into_iter()
866 .map(|factor| {
867 let key = target_ops
868 .iter()
869 .filter(|op| factor.includes(&op.order))
870 .fold(TraceKey::empty(), |key, op| {
871 key.push(&self.wood, op.clone())
872 });
873 self.graph
874 .iter_nodes()
875 .find_map(|(component, _, candidate)| {
876 (candidate.key == key && self.source_spinney(component).filter() == &factor)
877 .then_some(component)
878 })
879 .ok_or_else(|| {
880 eyre!(
881 "component {} of {operation} is not in the unfolded forest",
882 factor.string_label()
883 )
884 })
885 })
886 .collect()
887 }
888
889 fn operation_node_index(&self, operation: &OperationNode) -> Result<NodeIndex> {
890 let cover = operation.covers();
891 self.graph
892 .iter_nodes()
893 .find_map(|(node, _, candidate)| {
894 let source_matches = cover.as_ref().map_or_else(
895 || self.source_spinney(node).filter().is_empty(),
896 |cover| self.source_spinney(node).filter() == cover,
897 );
898 (candidate == operation && source_matches).then_some(node)
899 })
900 .ok_or_else(|| eyre!("{operation} is not in the unfolded forest"))
901 }
902
903 fn union_replay_states(&self, node: NodeIndex) -> Result<Vec<UnionReplayState>> {
904 let operation = &self.graph[node];
905 if operation.key.is_empty() {
906 return Ok(vec![UnionReplayState {
907 integrated: node,
908 local_edges: Vec::new(),
909 }]);
910 }
911
912 if !self.graph.is_disjoint_union(node) {
913 let (parent, edge) = self
914 .graph
915 .unique_parent(node)
916 .ok_or_else(|| eyre!("{operation} has no unique replay parent"))?;
917 let mut states = self.union_replay_states(parent)?;
918 for state in &mut states {
919 state.local_edges.push(edge);
920 }
921 states.push(UnionReplayState {
922 integrated: node,
923 local_edges: Vec::new(),
924 });
925 return Ok(states);
926 }
927
928 let component_states = self
929 .disconnected_component_nodes(node)?
930 .into_iter()
931 .map(|component| self.union_replay_states(component))
932 .collect::<Result<Vec<_>>>()?;
933 let mut states = Vec::new();
934 for components in component_states
935 .iter()
936 .map(|states| states.iter())
937 .multi_cartesian_product()
938 {
939 let key = TraceKey::try_foata_join(
940 components
941 .iter()
942 .map(|component| &self.graph[component.integrated].key),
943 &self.wood,
944 )
945 .ok_or_else(|| eyre!("cannot join integrated prefixes for {operation}"))?;
946 let mut local_edges = Vec::new();
947 for component in components {
948 local_edges.extend_from_slice(&component.local_edges);
949 }
950 let integrated = self.operation_node_index(&OperationNode { key })?;
951 states.push(UnionReplayState {
952 integrated,
953 local_edges,
954 });
955 }
956
957 Ok(states)
958 }
959
960 fn compute_4d_for_node(
961 &self,
962 node: NodeIndex,
963 graph: &Graph,
964 vakint: &Vakint,
965 settings: &UVgenerationSettings,
966 ) -> Result<(Local4dCts, IntegratedCts)> {
967 let operation = &self.graph[node];
968 if operation.key.is_empty() {
969 return Ok((Local4dCts::root(), IntegratedCts::root()));
970 }
971
972 if self.graph.is_disjoint_union(node) {
973 let components = self.disconnected_component_nodes(node)?;
974 let mut full_components = Vec::with_capacity(components.len());
975 let mut integrated_components = Vec::with_capacity(components.len());
976 for component in components {
977 let component_operation = &self.graph[component];
978 full_components.push(self.recursion_input_4d(component).wrap_err_with(|| {
979 format!("while loading 4D component {component_operation} for {operation}")
980 })?);
981 integrated_components.push(
982 self.compute_store
983 .require(component_operation)?
984 .integrated(component_operation)?,
985 );
986 }
987
988 let depth = graph.n_loops(
989 &operation
990 .covers()
991 .expect("a non-root operation has a cover"),
992 ) + 1;
993 return Ok((
994 Local4dCts::from_full_product(full_components),
995 IntegratedCts::factorized_product(integrated_components, depth)?,
996 ));
997 }
998
999 let (parent, edge) = self
1000 .graph
1001 .unique_parent(node)
1002 .ok_or_else(|| eyre!("{operation} has no unique 4D parent"))?;
1003 let full = self
1004 .recursion_input_4d(parent)
1005 .wrap_err_with(|| format!("while loading the 4D parent for {operation}"))?;
1006 let step_order = self.graph[parent].key.op_count();
1007 let (current, given) = self.wood.current_given_pair(edge, step_order);
1008 let ctx = UVCtx::new(graph, settings);
1009 let integrated_approximation = Integrated::new(vakint, &self.wood.vakint_settings);
1010 let local = local_4d::uv_limit(&full, &ctx, ¤t, &given, ¤t, &given)?;
1011 let integrated = if settings.generate_integrated {
1012 integrated_approximation.run(&local, &ctx, ¤t, &given, ¤t, &given)?
1013 } else {
1014 IntegratedCts::root()
1015 };
1016
1017 Ok((local, integrated))
1018 }
1019
1020 fn local_3d_for_node(
1021 &self,
1022 node: NodeIndex,
1023 graph: &mut Graph,
1024 cutset: &CutSet,
1025 localizer: Localizer<'_>,
1026 settings: &UVgenerationSettings,
1027 ) -> Result<CutComputation> {
1028 let operation = &self.graph[node];
1029 let local_3d = if operation.key.is_empty() {
1030 Local3DCts::root(graph, localizer)?
1031 } else if self.graph.is_disjoint_union(node) {
1032 let mut active_sectors = Vec::new();
1033 for state in self
1034 .union_replay_states(node)?
1035 .into_iter()
1036 .filter(|state| !state.local_edges.is_empty())
1037 {
1038 let integrated_operation = &self.graph[state.integrated];
1039 let mut edges = state.local_edges.into_iter().enumerate();
1040 let (offset, first_edge) = edges
1041 .next()
1042 .expect("a proper integrated prefix has a local suffix");
1043 let step_order = integrated_operation.key.op_count() + offset;
1044 let (current, given) = self.wood.current_given_pair(first_edge, step_order);
1045
1046 let mut sector = if integrated_operation.key.is_empty() {
1049 let root = Local3DCts::root(graph, localizer)?;
1050 Local3DApproximation::new(localizer, graph, settings)
1051 .run_local(&root, ¤t, &given, ¤t, &given)?
1052 } else {
1053 let integrated = self
1054 .compute_store
1055 .require(integrated_operation)?
1056 .integrated(integrated_operation)?;
1057 let prefix_node = ForestNode {
1058 spinney: self.source_spinney(state.integrated),
1059 topo_order: integrated_operation.key.op_count(),
1060 };
1061 Local3DApproximation::new(localizer, graph, settings).run_integrated(
1062 integrated,
1063 &prefix_node,
1064 ¤t,
1065 &given,
1066 ¤t,
1067 &given,
1068 )?
1069 };
1070
1071 for (offset, edge) in edges {
1072 let step_order = integrated_operation.key.op_count() + offset;
1073 let (current, given) = self.wood.current_given_pair(edge, step_order);
1074 sector = Local3DApproximation::new(localizer, graph, settings)
1075 .run_local(§or, ¤t, &given, ¤t, &given)?;
1076 }
1077
1078 active_sectors.extend(
1081 sector
1082 .active_sectors()
1083 .expect("a replayed union sector retains its active subgraph")
1084 .iter()
1085 .cloned(),
1086 );
1087 }
1088
1089 Local3DCts::from_active_sectors(active_sectors)
1090 .wrap_err_with(|| format!("{operation} has no proper integrated prefixes"))?
1091 } else {
1092 let (parent, edge) = self
1093 .graph
1094 .unique_parent(node)
1095 .ok_or_else(|| eyre!("{operation} has no unique local parent"))?;
1096 let parent_operation = &self.graph[parent];
1097 let parent_local = if parent_operation.key.is_empty() {
1100 Local3DCts::root(graph, localizer)?
1101 } else {
1102 self.compute_store
1103 .require(parent_operation)?
1104 .cut(parent_operation, cutset)?
1105 .local_3d
1106 .clone()
1107 };
1108 let parent_integrated = self
1109 .compute_store
1110 .require(parent_operation)?
1111 .integrated(parent_operation)
1112 .wrap_err_with(|| {
1113 format!("while loading integrated parent {parent_operation} for {operation}")
1114 })?;
1115 let step_order = parent_operation.key.op_count();
1116 let (current, given) = self.wood.current_given_pair(edge, step_order);
1117 Local3DApproximation::new(localizer, graph, settings).run(
1120 &parent_local,
1121 parent_integrated,
1122 ¤t,
1123 &given,
1124 ¤t,
1125 &given,
1126 )?
1127 };
1128
1129 let integrated = self
1130 .compute_store
1131 .require(operation)?
1132 .integrated(operation)?;
1133 let forest_node = ForestNode {
1134 spinney: self.source_spinney(node),
1135 topo_order: operation.key.op_count(),
1136 };
1137 let final_integrands = FinalIntegrandBuilder::new(localizer, settings).build_3d(
1138 graph,
1139 &forest_node,
1140 &local_3d,
1141 integrated,
1142 )?;
1143
1144 Ok(CutComputation {
1145 local_3d,
1146 final_integrands,
1147 })
1148 }
1149
1150 pub fn integrate(
1151 &mut self,
1152 graph: &Graph,
1153 vakint: &Vakint,
1154 settings: &UVgenerationSettings,
1155 ) -> Result<()> {
1156 for (order, nidx) in self.graph.topo_sort_kahn()?.into_iter().enumerate() {
1157 debug!(order, nidx=%nidx, key=%self.graph[nidx], "Computing hedge-poset 4D term");
1158 let operation = self.graph[nidx].clone();
1159 let (local_4d, integrated) = self.compute_4d_for_node(nidx, graph, vakint, settings)?;
1160 let cover = operation
1161 .covers()
1162 .unwrap_or_else(|| self.graph.empty_subgraph())
1163 .string_label();
1164 let source = self.source_spinney(nidx).filter().string_label();
1165 debug_tags!(#generation, #uv, #integrated, #graph, #term, #inspect;
1166 stage = "hedge_poset_4d_node_done",
1167 order,
1168 node_index = %nidx,
1169 forest_term = %operation,
1170 cover = %cover,
1171 source = %source,
1172 is_union = self.graph.is_disjoint_union(nidx),
1173 log.local_4d = local_4d.atom(),
1174 log.integrated_pole = integrated.physical_pole_atom(),
1175 log.integrated_finite = integrated.physical_finite_counterterm_atom(),
1176 "Computed hedge-poset 4D node"
1177 );
1178 let computed = self.compute_store.entry(operation).or_default();
1179 computed.local_4d = Some(local_4d);
1180 computed.integrated = Some(integrated);
1181 }
1182
1183 Ok(())
1184 }
1185
1186 pub(crate) fn compute(
1187 &mut self,
1188 graph: &mut Graph,
1189 vakint: &Vakint,
1190 orientation: OrientationProjection<'_>,
1191 settings: &UVgenerationSettings,
1192 ) -> Result<()> {
1193 self.integrate(graph, vakint, settings)?;
1194
1195 for (compatible_subset, cutset) in self.cuts.clone() {
1196 let localizer = Localizer::new(&cutset, orientation);
1197 for (order, nidx) in self
1198 .compatible_topological_order(&compatible_subset)?
1199 .into_iter()
1200 .enumerate()
1201 {
1202 debug!(order, nidx=%nidx, key=%self.graph[nidx], "Computing hedge-poset per-cut term");
1203 let operation = self.graph[nidx].clone();
1204 let cut_computation =
1205 self.local_3d_for_node(nidx, graph, &cutset, localizer, settings)?;
1206 self.compute_store
1207 .entry(operation)
1208 .or_default()
1209 .cuts
1210 .insert(cutset.clone(), cut_computation);
1211 }
1212 }
1213
1214 Ok(())
1215 }
1216
1217 pub(crate) fn orientation_parametric_exprs(
1218 &self,
1219 graph: &Graph,
1220 _settings: &UVgenerationSettings,
1221 ) -> Result<Vec<ParametricIntegrands>> {
1222 let split_momentum_replacements = graph
1223 .iter_edges_of(
1224 &graph
1225 .full_filter()
1226 .subtract(&graph.initial_state_cut)
1227 .subtract(&graph.tree_edges),
1228 )
1229 .filter_map(|(pair, edge_index, _)| {
1230 (!matches!(pair, HedgePair::Unpaired { .. }))
1231 .then(|| GS.split_mom_pattern_simple(edge_index))
1232 })
1233 .collect::<Vec<_>>();
1234 let mut expressions = Vec::with_capacity(self.cuts.len());
1235
1236 for (compatible_subset, cutset) in &self.cuts {
1237 let mut sum: Option<Integrands> = None;
1238 for nidx in self.compatible_topological_order(compatible_subset)? {
1239 let operation = &self.graph[nidx];
1240 let terms: Integrands = self
1241 .compute_store
1242 .require(operation)?
1243 .cut(operation, cutset)?
1244 .final_integrands
1245 .iter()
1246 .map(|(index, integrand)| (*index, integrand.clone().collect_color()))
1247 .collect();
1248 sum = Some(match sum {
1249 Some(sum) => sum.zip_add(&terms).wrap_err_with(|| {
1250 format!("while aggregating hedge-poset term {operation} for cut {cutset:?}")
1251 })?,
1252 None => terms,
1253 });
1254 }
1255
1256 let integrands = sum
1257 .ok_or_else(|| eyre!("No terms in hedge-poset forest for cut {cutset:?}"))?
1258 .map(|integrand| {
1259 integrand
1260 .replace_multiple(&split_momentum_replacements)
1261 .replace(function!(GS.den, W_.a_, W_.b_, W_.c_, W_.d_))
1262 .with(W_.d_)
1263 });
1264 expressions.push(ParametricIntegrands {
1265 integrands,
1266 cuts: cutset.clone(),
1267 });
1268 }
1269
1270 Ok(expressions)
1271 }
1272
1273 pub(crate) fn export_node_expressions(
1274 &self,
1275 forest_index: usize,
1276 post_process: &mut impl FnMut(Atom) -> Atom,
1277 ) -> Result<Vec<UVForestNodeExpression>> {
1278 let (cut_compatible_forest_subset, cutset) = self
1279 .cuts
1280 .first()
1281 .ok_or_else(|| eyre!("No cuts in hedge-poset forest export"))?;
1282 let mut terms = Vec::new();
1283 for (node_index, nidx) in self
1284 .compatible_topological_order(cut_compatible_forest_subset)?
1285 .into_iter()
1286 .enumerate()
1287 {
1288 let operation = &self.graph[nidx];
1289 let computed = self.compute_store.require(operation)?;
1290 let final_integrands = &computed.cut(operation, cutset)?.final_integrands;
1291 let node_key = operation.to_string();
1292 for (term_index, (&residue_index, numerator)) in final_integrands.iter().enumerate() {
1293 terms.push(UVForestNodeExpression {
1294 forest_index,
1295 node_index,
1296 node_key: node_key.clone(),
1297 term_index,
1298 residue_index,
1299 numerator: post_process(numerator.clone()),
1300 });
1301 }
1302 }
1303
1304 Ok(terms)
1305 }
1306
1307 pub(crate) fn renormalization_part_of_ends(
1308 &self,
1309 graph: &Graph,
1310 settings: &UVgenerationSettings,
1311 ) -> Result<RenormalizationPart> {
1312 let mut sum = Atom::Zero;
1313 let marker = UvMarker::new(settings);
1314
1315 let wild = Atom::var(W_.x___);
1316
1317 let replacements =
1318 graph.integrand_replacement(&graph.full_filter(), &graph.loop_momentum_basis, &[wild]);
1319 for (node, mut crown, key) in self.graph.iter_nodes() {
1320 if crown.any(|r| self.graph.flow(r).is_source()) {
1321 continue;
1322 }
1323
1324 let forest_node = key.forest_node(graph, key.key.op_count());
1325 let components = if self.graph.is_disjoint_union(node) {
1329 self.disconnected_component_nodes(node)?
1330 } else {
1331 vec![node]
1332 };
1333 let physical = components.into_iter().try_fold(
1334 Atom::one(),
1335 |product, component| -> Result<Atom> {
1336 let component_key = &self.graph[component];
1337 let integrated = self
1338 .compute_store
1339 .require(component_key)?
1340 .integrated(component_key)?;
1341 let projection = match self.source_spinney(component).renormalization_scheme {
1342 ApproximationType::MUV => integrated.physical_finite_counterterm_atom(),
1343 ApproximationType::PolePart => integrated.physical_pole_atom(),
1344 scheme => {
1345 return Err(eyre!("No terminal counterterm projection for {scheme}"));
1346 }
1347 };
1348 Ok(product * projection)
1349 },
1350 )?;
1351 let atom = marker.prefix(&graph.full_filter(), forest_node.subgraph(), &physical);
1352 debug!(
1353 key=%key,
1354 expr = % atom.expand_num().log_print(None),"Term before simplification"
1355 );
1356 let atom = (&atom
1357 * &graph.global_prefactor.projector
1358 * &graph.global_prefactor.num
1359 * &graph.overall_factor)
1360 .simplify_color()
1361 .expand_num()
1362 .to_dots();
1363
1364 debug!(
1365 key=%key,
1366 expr = % atom.log_print(None),"Term"
1367 );
1368 sum += atom;
1369 }
1370
1371 Ok(RenormalizationPart::new(
1372 sum.replace_multiple(&replacements)
1373 .replace(GS.m_uv_expansion)
1374 .with(GS.m_uv_vacuum),
1375 self.graph.n_nodes(),
1376 ))
1377 }
1378
1379 pub fn debug_walk(&self) {
1380 let mut cover_groups: BTreeMap<SuBitGraph, Vec<NodeIndex>> = BTreeMap::new();
1381
1382 self.graph
1383 .topo_sort_kahn()
1384 .unwrap()
1385 .iter()
1386 .for_each(|nidx| {
1387 let trace_key = &self.graph[*nidx];
1388 cover_groups
1389 .entry(trace_key.covers().unwrap_or(self.graph.empty_subgraph()))
1390 .and_modify(|e| e.push(*nidx))
1391 .or_insert_with(|| vec![*nidx]);
1392
1393 println!("Node {}:{}", nidx, self.node_label(*nidx));
1394 });
1395
1396 println!("edge [constraint=true style=invis];");
1397 for (a, b) in cover_groups.values().tuple_windows() {
1398 println!("{}->{}", a.first().unwrap(), b.first().unwrap())
1399 }
1400 println!("edge [style=solid];");
1401
1402 for (s, g) in cover_groups.iter() {
1403 println!("subgraph group_{} {{rank=same; ", s.string_label());
1404 for n in g {
1405 println!("{};", n.0);
1406 }
1407 println!("}}");
1408 }
1409 }
1410}
1411
1412impl Display for Forests {
1413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1414 let labels: AHashMap<_, _> = self
1415 .graph
1416 .iter_nodes()
1417 .map(|(node, _, key)| (key.clone(), self.node_label(node)))
1418 .collect();
1419 self.graph.dot_impl_fmt(
1420 f,
1421 &self.graph.full_filter(),
1422 "start=2;\n",
1423 &|_| None,
1424 &|_| None,
1425 &|v| Some(format!("label=\"{}\"", labels[v])),
1426 )
1427 }
1428}
1429
1430#[cfg(test)]
1431mod tests {
1432 use crate::{
1433 dot,
1434 graph::{Graph, parse::IntoGraph},
1435 initialisation::test_initialise,
1436 processes::DotExportSettings,
1437 uv::{UltravioletGraph, Wood as OldWood, settings::RenormalizationPrescriptionSettings},
1438 };
1439
1440 use super::*;
1441 use color_eyre::Result;
1442
1443 impl Forests {
1444 fn normalized_node_label(&self, node: NodeIndex) -> String {
1445 let label = self.node_label(node);
1446 let mut normalized = String::with_capacity(label.len());
1447 let mut chars = label.chars().peekable();
1448
1449 while let Some(ch) = chars.next() {
1450 normalized.push(ch);
1451 if ch != '(' {
1452 continue;
1453 }
1454
1455 let mut digits = String::new();
1456 while chars.peek().is_some_and(|next| next.is_ascii_digit()) {
1457 digits.push(chars.next().expect("peeked digit must exist"));
1458 }
1459
1460 if digits.is_empty() || !matches!(chars.peek(), Some(')')) {
1461 normalized.push_str(&digits);
1462 } else {
1463 normalized.push('_');
1464 }
1465 }
1466
1467 normalized
1468 }
1469
1470 fn normalized_node_labels_with_cover(&self, cover_label: &str) -> Vec<String> {
1471 let mut labels = self
1472 .graph
1473 .iter_nodes()
1474 .filter(|(_, _, operation)| {
1475 operation
1476 .covers()
1477 .is_some_and(|cover| cover.string_label() == cover_label)
1478 })
1479 .map(|(node, _, _)| self.normalized_node_label(node))
1480 .collect::<Vec<_>>();
1481 labels.sort();
1482 labels
1483 }
1484 }
1485
1486 #[test]
1487 fn local_leaf_operations_follow_dependency_frontiers() -> Result<()> {
1488 test_initialise().unwrap();
1489 let dumbell: Graph = dot!(
1490 digraph G{
1491 edge [particle="scalar_1"];
1492 v1 -> v2;
1493 v2 -> v2;
1494 v1 -> v1;v1 -> v1;
1495 },"scalars"
1496 )?;
1497
1498 let forests = Wood::new(
1499 CutStructure::empty(&dumbell),
1500 &dumbell,
1501 &UVgenerationSettings::default(),
1502 )
1503 .unfold();
1504
1505 let frontier_label = |operation: &OperationNode| {
1506 operation
1507 .covers()
1508 .map_or_else(|| "∅".to_string(), |cover| cover.string_label())
1509 };
1510 let leaf_labels = |node| {
1511 forests
1512 .local_leaf_operations(node)
1513 .iter()
1514 .map(|leaf| {
1515 (
1516 leaf.op.order.string_label(),
1517 frontier_label(&forests.graph[leaf.frontier]),
1518 )
1519 })
1520 .collect::<Vec<_>>()
1521 };
1522
1523 let root_disconnected = forests
1524 .graph
1525 .iter_nodes()
1526 .find_map(|(node, _, _)| {
1527 forests
1528 .normalized_node_label(node)
1529 .starts_with("{36,F}:")
1530 .then_some(node)
1531 })
1532 .expect("lopsided dumbbell should contain a root disconnected frontier");
1533 assert_eq!(
1534 leaf_labels(root_disconnected),
1535 vec![
1536 ("36".to_string(), "∅".to_string()),
1537 ("F".to_string(), "∅".to_string())
1538 ]
1539 );
1540 let structured_dot = forests.dot_serialize();
1541 let mut join_markers = Vec::new();
1542 for (node, _, _) in forests.graph.iter_nodes() {
1543 for leaf in forests.local_leaf_operations(node) {
1544 let frontier_depth = forests.graph[leaf.frontier].key.op_count();
1545 let (current, given) = forests
1546 .wood
1547 .current_given_pair(leaf.op.data, frontier_depth);
1548 if current.subgraph() == &leaf.op.order {
1549 continue;
1550 }
1551 join_markers.push(
1552 UvMarker::subgraph(current.subgraph(), given.subgraph())
1553 .printer(SpensoPrintSettings::typst_options())
1554 .to_string(),
1555 );
1556 }
1557 }
1558 assert!(
1559 !join_markers.is_empty(),
1560 "lopsided dumbbell must contain a disconnected join"
1561 );
1562 for marker in join_markers {
1563 assert!(
1564 structured_dot.contains(&marker),
1565 "structured DOT must use the runtime current/given marker {marker}"
1566 );
1567 }
1568
1569 let dependent_disconnected = forests
1570 .graph
1571 .iter_nodes()
1572 .find_map(|(node, _, _)| {
1573 forests
1574 .normalized_node_label(node)
1575 .starts_with("{C} · {36,F}:")
1576 .then_some(node)
1577 })
1578 .expect("lopsided dumbbell should contain a C-dependent disconnected frontier");
1579 assert_eq!(
1580 leaf_labels(dependent_disconnected),
1581 vec![
1582 ("F".to_string(), "C".to_string()),
1583 ("36".to_string(), "∅".to_string())
1584 ]
1585 );
1586 let replay_states = forests.union_replay_states(dependent_disconnected)?;
1587 assert_eq!(replay_states.len(), 6);
1588 assert_eq!(
1589 replay_states
1590 .iter()
1591 .filter(|state| !state.local_edges.is_empty())
1592 .count(),
1593 5
1594 );
1595 Ok(())
1596 }
1597
1598 #[test]
1599 fn union_terms_replay_component_paths_from_typed_roots() -> Result<()> {
1600 test_initialise().unwrap();
1601 let mut graph: Graph = dot!(
1602 digraph G{
1603 edge [particle="scalar_1"];
1604 v1 -> v2;
1605 v2 -> v2;
1606 v1 -> v1;v1 -> v1;
1607 },"scalars"
1608 )?;
1609 let settings = UVgenerationSettings::default();
1610 let cut_structure = CutStructure::empty(&graph);
1611 let cutset = cut_structure
1612 .cuts
1613 .first()
1614 .expect("empty cut structure has one cut")
1615 .clone();
1616 let mut forests = Wood::new(cut_structure, &graph, &settings).unfold();
1617
1618 let root_disconnected = forests
1619 .graph
1620 .iter_nodes()
1621 .find_map(|(node, _, _)| {
1622 forests
1623 .normalized_node_label(node)
1624 .starts_with("{36,F}:")
1625 .then_some(node)
1626 })
1627 .expect("lopsided dumbbell should contain a root disconnected frontier");
1628 let dependent_disconnected = forests
1629 .graph
1630 .iter_nodes()
1631 .find_map(|(node, _, _)| {
1632 forests
1633 .normalized_node_label(node)
1634 .starts_with("{C} · {36,F}:")
1635 .then_some(node)
1636 })
1637 .expect("lopsided dumbbell should contain a C-dependent disconnected frontier");
1638 let non_root_frontier = forests
1639 .local_leaf_operations(dependent_disconnected)
1640 .first()
1641 .expect("dependent disconnected operation has a leaf")
1642 .frontier;
1643 assert_eq!(
1644 forests.graph[non_root_frontier]
1645 .covers()
1646 .expect("non-root frontier has a cover")
1647 .string_label(),
1648 "C"
1649 );
1650
1651 let operations = forests
1652 .graph
1653 .iter_nodes()
1654 .map(|(_, _, operation)| operation.clone())
1655 .collect::<Vec<_>>();
1656 for operation in operations {
1658 forests
1659 .compute_store
1660 .entry(operation)
1661 .or_default()
1662 .integrated = Some(IntegratedCts::root());
1663 }
1664
1665 let orientation_pattern = crate::settings::global::OrientationPattern::default();
1666 let localizer = Localizer::new(
1667 &cutset,
1668 OrientationProjection::new(&[], &orientation_pattern),
1669 );
1670 let seed =
1671 forests.local_3d_for_node(forests.root, &mut graph, &cutset, localizer, &settings)?;
1672 let root_store_marker = symbolica::symbol!("root_store_marker");
1673 let frontier_store_marker = symbolica::symbol!("frontier_store_marker");
1674 let marked_computation = |marker| -> Result<CutComputation> {
1676 Ok(CutComputation {
1677 local_3d: seed.local_3d.map(|_| Ok(Atom::var(marker)))?,
1678 final_integrands: seed.final_integrands.clone(),
1679 })
1680 };
1681
1682 let root_operation = forests.graph[forests.root].clone();
1683 forests
1684 .compute_store
1685 .entry(root_operation)
1686 .or_default()
1687 .cuts
1688 .insert(cutset.clone(), marked_computation(root_store_marker)?);
1689 forests
1690 .compute_store
1691 .entry(forests.graph[non_root_frontier].clone())
1692 .or_default()
1693 .cuts
1694 .insert(cutset.clone(), marked_computation(frontier_store_marker)?);
1695
1696 let root_result = forests.local_3d_for_node(
1697 root_disconnected,
1698 &mut graph,
1699 &cutset,
1700 localizer,
1701 &settings,
1702 )?;
1703 assert_eq!(
1704 root_result
1705 .local_3d
1706 .active_sectors()
1707 .expect("a root union keeps its active sectors")
1708 .len(),
1709 3
1710 );
1711 assert!(
1712 root_result
1713 .local_3d
1714 .integrands()
1715 .iter()
1716 .all(|(_, term)| !term.contains_symbol(root_store_marker)),
1717 "an empty dependency frontier must start from the typed root"
1718 );
1719
1720 let frontier_result = forests.local_3d_for_node(
1721 dependent_disconnected,
1722 &mut graph,
1723 &cutset,
1724 localizer,
1725 &settings,
1726 )?;
1727 assert_eq!(
1728 frontier_result
1729 .local_3d
1730 .active_sectors()
1731 .expect("a dependent union keeps its active sectors")
1732 .len(),
1733 5
1734 );
1735 let replay_states = forests.union_replay_states(dependent_disconnected)?;
1736 let (state, (active_subgraph, _)) = replay_states
1737 .iter()
1738 .filter(|state| !state.local_edges.is_empty())
1739 .zip(
1740 frontier_result
1741 .local_3d
1742 .active_sectors()
1743 .expect("a dependent union keeps its active sectors"),
1744 )
1745 .find(|(state, _)| {
1746 state.local_edges.len() == 2
1747 && forests
1748 .normalized_node_label(state.integrated)
1749 .starts_with("{36}:")
1750 })
1751 .expect("the integrated 36 prefix has the two-step C-to-F suffix");
1752 let expected_active = state
1753 .local_edges
1754 .iter()
1755 .enumerate()
1756 .map(|(offset, edge)| {
1757 let step_order = forests.graph[state.integrated].key.op_count() + offset;
1758 let (current, given) = forests.wood.current_given_pair(*edge, step_order);
1759 current.reduced_subgraph(&given)
1760 })
1761 .reduce(|active, reduced| active.union(&reduced))
1762 .expect("the selected replay state has a local suffix");
1763 assert_eq!(active_subgraph, &expected_active);
1764 assert!(
1765 active_subgraph.empty_intersection(forests.source_spinney(state.integrated).filter())
1766 );
1767 assert!(
1768 frontier_result
1769 .local_3d
1770 .integrands()
1771 .iter()
1772 .all(|(_, term)| !term.contains_symbol(root_store_marker)
1773 && !term.contains_symbol(frontier_store_marker)),
1774 "a union must replay every component path from its typed root"
1775 );
1776 Ok(())
1777 }
1778
1779 #[test]
1780 fn heterogeneous_terminal_projects_components_before_multiplying() -> Result<()> {
1781 test_initialise().unwrap();
1782 let graph: Graph = dot!(
1783 digraph G {
1784 num = "1";
1785 projector = "1";
1786 overall_factor = "1";
1787 edge [particle = "scalar_1"];
1788 node [num = "1"];
1789 v1 -> v2;
1790 v1 -> v1;
1791 v2 -> v3;
1792 v2 -> v3;
1793 },
1794 "scalars"
1795 )?;
1796 let settings = UVgenerationSettings {
1797 softct: false,
1798 renormalization_prescription: RenormalizationPrescriptionSettings {
1799 log_divergent: ApproximationType::MUV,
1800 massive_power_divergent: ApproximationType::PolePart,
1801 massless_power_divergent: ApproximationType::PolePart,
1802 ..Default::default()
1803 },
1804 ..Default::default()
1805 };
1806 let mut forests = Wood::new(CutStructure::empty(&graph), &graph, &settings).unfold();
1807 forests.integrate(&graph, crate::utils::vakint()?, &settings)?;
1808
1809 let terminals = forests
1810 .graph
1811 .iter_nodes()
1812 .filter_map(|(node, mut crown, _)| {
1813 (!crown.any(|hedge| forests.graph.flow(hedge).is_source())).then_some(node)
1814 })
1815 .collect::<Vec<_>>();
1816 assert_eq!(terminals.len(), 1);
1817 let terminal = terminals[0];
1818 assert!(forests.graph.is_disjoint_union(terminal));
1819
1820 let components = forests.disconnected_component_nodes(terminal)?;
1821 assert_eq!(components.len(), 2);
1822 let schemes = components
1823 .iter()
1824 .map(|component| forests.source_spinney(*component).renormalization_scheme)
1825 .collect::<Vec<_>>();
1826 assert!(schemes.contains(&ApproximationType::MUV));
1827 assert!(schemes.contains(&ApproximationType::PolePart));
1828
1829 let expected =
1830 components
1831 .iter()
1832 .try_fold(Atom::one(), |product, &component| -> Result<Atom> {
1833 let component_key = &forests.graph[component];
1834 let integrated = forests
1835 .compute_store
1836 .require(component_key)?
1837 .integrated(component_key)?;
1838 let projection = match forests.source_spinney(component).renormalization_scheme
1839 {
1840 ApproximationType::MUV => integrated.physical_finite_counterterm_atom(),
1841 ApproximationType::PolePart => integrated.physical_pole_atom(),
1842 scheme => panic!("unexpected component scheme {scheme}"),
1843 };
1844 Ok(product * projection)
1845 })?;
1846 let terminal_key = &forests.graph[terminal];
1847 let aggregate = forests
1848 .compute_store
1849 .require(terminal_key)?
1850 .integrated(terminal_key)?;
1851 assert_ne!(
1852 expected.expand(),
1853 aggregate.physical_finite_counterterm_atom().expand()
1854 );
1855 assert_ne!(expected.expand(), aggregate.physical_pole_atom().expand());
1856
1857 let wild = Atom::var(W_.x___);
1858 let replacements =
1859 graph.integrand_replacement(&graph.full_filter(), &graph.loop_momentum_basis, &[wild]);
1860 let expected = expected
1861 .simplify_color()
1862 .expand_num()
1863 .to_dots()
1864 .replace_multiple(&replacements)
1865 .replace(GS.m_uv_expansion)
1866 .with(GS.m_uv_vacuum);
1867 let actual = forests
1868 .renormalization_part_of_ends(&graph, &settings)?
1869 .expression;
1870 assert_eq!(actual.expand(), expected.expand());
1871
1872 Ok(())
1873 }
1874
1875 #[test]
1876 fn triple_tadpole() -> Result<()> {
1877 test_initialise().unwrap();
1878 let dumbell: Graph = dot!(
1879 digraph G{
1880 edge [particle="scalar_1"];
1881 v1 -> v2;
1882 v2 -> v3;
1883 v3 -> v3;
1884 v2 -> v2;
1885 v1 -> v1;
1886 },"scalars"
1887 )?;
1888
1889 let spinneys: Vec<_> = dumbell
1890 .spinneys(&dumbell.full_filter())
1891 .into_iter()
1892 .filter_map(|a| Spinney::new(a, &dumbell, &dumbell.loop_momentum_basis))
1893 .collect();
1894 let f = Wood::new(
1895 CutStructure::empty(&dumbell),
1896 &dumbell,
1897 &UVgenerationSettings::default(),
1898 );
1899
1900 println!("{}", f);
1901
1902 insta::assert_snapshot!(
1903 f.graph.n_nodes(),
1904 @"8",
1905 );
1907
1908 for (_, _, d) in f.graph.iter_nodes() {
1909 println!(
1910 "//Node {}: \n{}",
1911 d.subgraph.string_label(),
1912 dumbell.dot(&d.subgraph)
1913 );
1914 }
1915 let _ff = OldWood::from_spinneys(spinneys, &dumbell); let f = f.unfold();
1920 println!("{}", f);
1921 insta::assert_snapshot!(
1922 f.graph.n_nodes(),
1923 @"8");
1924
1925 let three_component_union = f
1926 .graph
1927 .iter_nodes()
1928 .find_map(|(node, _, operation)| {
1929 (operation.key.op_count() == 3
1930 && f.wood
1931 .join_factors(f.graph.source_node(node))
1932 .is_some_and(|factors| factors.len() == 3))
1933 .then_some(node)
1934 })
1935 .expect("triple tadpole should contain a three-component union");
1936 let replay_states = f.union_replay_states(three_component_union)?;
1937 assert_eq!(replay_states.len(), 8);
1938 assert_eq!(
1939 replay_states
1940 .iter()
1941 .filter(|state| !state.local_edges.is_empty())
1942 .count(),
1943 7
1944 );
1945
1946 Ok(())
1947 }
1948
1949 #[test]
1950 fn saclay() -> Result<()> {
1951 test_initialise().unwrap();
1952 let dt: Graph = dot!(digraph GL16{
1953
1954 num = "spenso::g(spenso::coad(8,gammalooprs::hedge(8)),spenso::coad(8,gammalooprs::hedge(11)))"
1955 ext [style=invis];
1956 ext -> 0 [dir=none id=0 particle="g"];
1957 2 -> ext [dir=none id=1 particle="g"];
1958 0 -> 1 -> 2->3->0 [ particle="d"];
1959 1 ->3 [particle = "g"]
1960 })?;
1961
1962 let f = Wood::new(
1963 CutStructure::empty(&dt),
1964 &dt,
1965 &UVgenerationSettings::default(),
1966 );
1967
1968 println!("{}", dt.dot_serialize(&DotExportSettings::default()));
1969
1970 insta::assert_snapshot!(
1971 f.graph.n_nodes(),
1972 @"5",
1973 );
1975
1976 let f = f.unfold();
1977 let structured_dot = f.dot_serialize();
1978 assert!(structured_dot.contains(r#"label="K[#expr S_44⊛0]""#));
1979 assert!(!structured_dot.contains("#T("));
1980 println!("{structured_dot}");
1981 insta::assert_snapshot!(
1982 f.graph.n_nodes(),
1983 @"8");
1984
1985 Ok(())
1986 }
1987
1988 #[test]
1989 fn dumbells() -> Result<()> {
1990 test_initialise().unwrap();
1991 let dumbell: Graph = dot!(
1992 digraph G{
1993 edge [particle="scalar_1"];
1994 v1 -> v2;
1995 v2 -> v2;
1996 v1 -> v1;
1997 },"scalars"
1998 )?;
1999
2000 let spinneys: Vec<_> = dumbell
2001 .spinneys(&dumbell.full_filter())
2002 .into_iter()
2003 .filter_map(|a| Spinney::new(a, &dumbell, &dumbell.loop_momentum_basis))
2004 .collect();
2005 let f = Wood::new(
2006 CutStructure::empty(&dumbell),
2007 &dumbell,
2008 &UVgenerationSettings::default(),
2009 );
2010
2011 println!("{}", f);
2012
2013 insta::assert_snapshot!(
2014 f.graph.n_nodes(),
2015 @"4",
2016 );
2018
2019 for (_, _, d) in f.graph.iter_nodes() {
2020 println!(
2021 "//Node {}: \n{}",
2022 d.subgraph.string_label(),
2023 dumbell.dot(&d.subgraph)
2024 );
2025 }
2026 let _ff = OldWood::from_spinneys(spinneys, &dumbell); let f = f.unfold();
2031 println!("{}", f);
2032 insta::assert_snapshot!(
2033 f.graph.n_nodes(),
2034 @"4");
2035
2036 Ok(())
2037 }
2038
2039 #[test]
2040 fn bugblatter() -> Result<()> {
2041 test_initialise().unwrap();
2042
2043 match dot!(
2044 digraph G{
2045 A1 -> A2 [particle="t"];
2046 A2 -> A3 [particle="t"];
2047 A3 -> A1 [particle="t"];
2048 B1 -> B2 [particle="t"];
2049 B2 -> B3 [particle="t"];
2050 B3 -> B1 [particle="t"];
2051 A1 -> B1 [particle="a"];
2052 A2 -> B2 [particle="a"];
2053 A3 -> B3 [particle="a"];
2054 },"sm"
2055 ) {
2056 Ok(g) => {
2057 let g: Graph = g;
2058 let spinneys: Vec<_> = g
2059 .spinneys(&g.full_filter())
2060 .into_iter()
2061 .filter_map(|a| Spinney::new(a, &g, &g.loop_momentum_basis))
2062 .collect();
2063 let f = Wood::new(
2064 CutStructure::empty(&g),
2065 &g,
2066 &UVgenerationSettings::default(),
2067 );
2068
2069 println!("{}", f);
2070
2071 assert_eq!(
2072 20,
2073 f.graph.n_nodes(),
2074 "Wood does not have correct number of spinneys: \n{}",
2075 f
2076 );
2077
2078 for (_, _, d) in f.graph.iter_nodes() {
2079 println!(
2080 "//Node {}: \n{}",
2081 d.subgraph.string_label(),
2082 g.dot_lmb_of(&d.subgraph, &d.lmb)
2083 );
2084 }
2085 let _ff = OldWood::from_spinneys(spinneys, &g); let f = f.unfold();
2090 f.debug_walk();
2091 println!("{}", f);
2092 assert_eq!(
2093 152,
2094 f.graph.n_nodes(),
2095 "Forest unfolds into the wrong number of terms :\n{}",
2096 f
2097 );
2098
2099 }
2101 Err(e) => {
2102 eprintln!("{}", e);
2103 }
2104 }
2105
2106 Ok(())
2114 }
2115
2116 #[test]
2117 fn mercedes() -> Result<()> {
2118 test_initialise().unwrap();
2119
2120 let mercedes: Graph = dot!(
2121 digraph G{
2122 edge [particle="scalar_1"];
2123 v1 -> v2;
2124 v2 -> v3;
2125 v3 -> v1;
2126 v1 -> v4;
2127 v2 -> v4;
2128 v3 -> v4;
2129 },"scalars"
2130 )?;
2131
2132 let f = Wood::new(
2133 CutStructure::empty(&mercedes),
2134 &mercedes,
2135 &UVgenerationSettings::default(),
2136 );
2137 println!("{}", f);
2138 insta::assert_snapshot!(
2139 f.graph.n_nodes(),
2140 @"2",
2141 );
2142 let f = f.unfold();
2143 println!("{}", f);
2144 insta::assert_snapshot!(
2145 f.graph.n_nodes(),
2146 @"2",
2147 );
2148
2149 Ok(())
2150 }
2151
2152 #[test]
2153 fn sunrise() -> Result<()> {
2154 test_initialise().unwrap();
2155
2156 let sunrise: Graph = dot!( digraph sunrise{
2157 node [num = "1"]
2158 edge [particle=scalar_1]
2159 e [style=invis]
2160 e -> A:0 [ id=3 ]
2161 B:1 -> e [ id=4 ]
2162 A -> B [ id=0 ]
2163 A -> B [ id=1 ]
2164 A -> B [ id=2 ]
2165 },"scalars")?;
2166 let f = Wood::new(
2168 CutStructure::empty(&sunrise),
2169 &sunrise,
2170 &UVgenerationSettings::default(),
2171 );
2172 println!("{}", f);
2173 insta::assert_snapshot!(
2174 f.graph.n_nodes(),
2175 @"5",
2176 );
2177 let f = f.unfold();
2178 println!("{}", f);
2179 insta::assert_snapshot!(
2180 f.graph.n_nodes(),
2181 @"8",
2182 );
2183
2184 Ok(())
2185 }
2186 #[test]
2187 fn dotted_sunrise() -> Result<()> {
2188 test_initialise().unwrap();
2189
2190 let sunrise: Graph = dot!( digraph sunrise{
2191 edge [particle=scalar_1]
2192 e [style=invis]
2193 e -> A:0 [ id=3]
2194 B:1 -> e [ id=4]
2195
2196 A -> C [ id=0]
2197 C -> e
2198 C -> B
2199 A -> B [ id=1]
2200 A -> B [ id=2]
2201 },"scalars")?;
2202 let f = Wood::new(
2204 CutStructure::empty(&sunrise),
2205 &sunrise,
2206 &UVgenerationSettings::default(),
2207 );
2208 println!("{}", f);
2209 insta::assert_snapshot!(
2210 f.graph.n_nodes(),
2211 @"3",
2212 );
2213 let f = f.unfold();
2214 println!("{}", f);
2215 insta::assert_snapshot!(
2216 f.graph.n_nodes(),
2217 @"4",
2218 );
2219
2220 Ok(())
2221 }
2222
2223 #[test]
2224 fn dotted() -> Result<()> {
2225 test_initialise().unwrap();
2226
2227 let sunrise: Graph = dot!( digraph sunrise{
2228 edge [particle=scalar_1]
2229 e [style=invis]
2230 e -> A:0 [ id=4]
2231 B:1 -> e [ id=5]
2232 C:2 -> e [ id=6]
2233
2234 A -> B [ id=0]
2235 B -> C [ id=1]
2236 C -> A [ id=2]
2237 B -> C [ id=3]
2238
2239 },"scalars")?;
2240 let f = Wood::new(
2242 CutStructure::empty(&sunrise),
2243 &sunrise,
2244 &UVgenerationSettings::default(),
2245 );
2246 println!("{}", f);
2247 insta::assert_snapshot!(
2248 f.graph.n_nodes(),
2249 @"3",
2250 );
2251 let f = f.unfold();
2252 println!("{}", f);
2253 insta::assert_snapshot!(
2254 f.graph.n_nodes(),
2255 @"4",
2256 );
2257
2258 Ok(())
2259 }
2260
2261 #[test]
2262 fn spectacles() -> Result<()> {
2263 test_initialise().unwrap();
2264
2265 let mut spectacles: Graph = dot!(
2266 digraph G{
2267 edge [particle="scalar_1"];
2268 v1 -> v2;
2269 v1 -> v2;
2270
2271 v3 -> v4;
2272 v3 -> v4;
2273
2274 v2 -> v3;
2275 v1 -> v4;
2276 },"scalars"
2277 )?;
2278
2279 let settings = UVgenerationSettings::default();
2281 let cut_structure = CutStructure::empty(&spectacles);
2282 let cutset = cut_structure
2283 .cuts
2284 .first()
2285 .expect("empty cut structure has one cut")
2286 .clone();
2287 let f = Wood::new(cut_structure, &spectacles, &settings);
2288 println!("{}", f);
2289 insta::assert_snapshot!(
2290 f.graph.n_nodes(),
2291 @"5",
2292 );
2293 let f = f.unfold();
2294 println!("{}", f);
2295 insta::assert_snapshot!(
2296 f.graph.n_nodes(),
2297 @"8",
2298 );
2299
2300 let (union, edge) = f
2301 .graph
2302 .iter_nodes()
2303 .find_map(|(child, _, _)| {
2304 let (parent, edge) = f.graph.unique_parent(child)?;
2305 (!f.graph.is_disjoint_union(child) && f.graph.is_disjoint_union(parent))
2306 .then_some((parent, edge))
2307 })
2308 .expect("spectacles has a connected child above its disconnected union");
2309 let orientation_pattern = crate::settings::global::OrientationPattern::default();
2310 let localizer = Localizer::new(
2311 &cutset,
2312 OrientationProjection::new(&[], &orientation_pattern),
2313 );
2314 let union_active = f
2315 .union_replay_states(union)?
2316 .into_iter()
2317 .filter(|state| !state.local_edges.is_empty())
2318 .map(|state| {
2319 state
2320 .local_edges
2321 .iter()
2322 .enumerate()
2323 .map(|(offset, edge)| {
2324 let step_order = f.graph[state.integrated].key.op_count() + offset;
2325 let (current, given) = f.wood.current_given_pair(*edge, step_order);
2326 current.reduced_subgraph(&given)
2327 })
2328 .reduce(|active, reduced| active.union(&reduced))
2329 .expect("a proper union replay state has a local suffix")
2330 })
2331 .collect::<Vec<_>>();
2332 assert_eq!(union_active.len(), 3);
2333 let union_local = Local3DCts::from_active_sectors(
2334 union_active
2335 .iter()
2336 .cloned()
2337 .map(|active| (active, Integrands::root()))
2338 .collect(),
2339 )?;
2340
2341 let step_order = f.graph[union].key.op_count();
2342 let (current, given) = f.wood.current_given_pair(edge, step_order);
2343 let reduced = current.reduced_subgraph(&given);
2344 let expected_active = union_active
2345 .iter()
2346 .map(|active| active.union(&reduced))
2347 .chain(std::iter::once(reduced.clone()))
2348 .collect::<Vec<_>>();
2349 let child_local = Local3DApproximation::new(localizer, &mut spectacles, &settings).run(
2350 &union_local,
2351 &IntegratedCts::root(),
2352 ¤t,
2353 &given,
2354 ¤t,
2355 &given,
2356 )?;
2357 let child_active = child_local
2358 .active_sectors()
2359 .expect("a connected child keeps its parent's active sectors")
2360 .iter()
2361 .map(|(active, _)| active.clone())
2362 .collect::<Vec<_>>();
2363 assert_eq!(child_active.len(), 4);
2364 assert_eq!(child_active, expected_active);
2365
2366 Ok(())
2367 }
2368
2369 #[test]
2370 fn basketball() -> Result<()> {
2371 test_initialise().unwrap();
2372
2373 let basketball: Graph = dot!(
2374 digraph G{
2375 edge [particle="scalar_1"];
2376 v1 -> v2;
2377 v1 -> v2;
2378 v1 -> v2;
2379 v1 -> v2;
2380 },"scalars"
2381 )?;
2382
2383 let f = Wood::new(
2384 CutStructure::empty(&basketball),
2385 &basketball,
2386 &UVgenerationSettings::default(),
2387 );
2388 println!("{}", f);
2389 insta::assert_snapshot!(
2390 f.graph.n_nodes(),
2391 @"12",
2392 );
2393 let f = f.unfold();
2394 println!("{}", f.dot_serialize());
2395 insta::assert_snapshot!(
2396 f.graph.n_nodes(),
2397 @"46");
2398 Ok(())
2399 }
2400
2401 #[test]
2402 fn fourloop_b() -> Result<()> {
2403 test_initialise().unwrap();
2404
2405 let fourloop_b: Graph = dot!(
2406 digraph G{
2407 edge [particle="scalar_1"];
2408 v1 -> v2;
2409 v1 -> v2;
2410
2411 v3 -> v4;
2412 v3 -> v4;
2413
2414 v2 -> v3;
2415 v1 -> v3;
2416 v1 -> v4;
2417 },"scalars"
2418 )?;
2419
2420 let f = Wood::new(
2422 CutStructure::empty(&fourloop_b),
2423 &fourloop_b,
2424 &UVgenerationSettings::default(),
2425 );
2426 println!("{}", f);
2427 insta::assert_snapshot!(
2428 f.graph.n_nodes(),
2429 @"14",
2430 );
2431 let f = f.unfold();
2432 println!("{}", f);
2433 insta::assert_snapshot!(
2434 f.graph.n_nodes(),
2435 @"80",
2436 );
2437
2438 Ok(())
2439 }
2440
2441 #[test]
2442 fn four_loop_a() -> Result<()> {
2443 test_initialise().unwrap();
2444
2445 let four_loop_a: Graph = dot!(
2446 digraph G{
2447 edge [particle="scalar_1"];
2448 v1 -> v2;
2449 v1 -> v2;
2450 v2 -> v3;
2451 v3 -> v1;
2452 v1 -> v4;
2453 v2 -> v4;
2454 v3 -> v4;
2455 },"scalars"
2456 )?;
2457
2458 let f = Wood::new(
2459 CutStructure::empty(&four_loop_a),
2460 &four_loop_a,
2461 &UVgenerationSettings::default(),
2462 );
2463 println!("{}", f);
2464 insta::assert_snapshot!(
2465 f.graph.n_nodes(),
2466 @"12",
2467 );
2468 let f = f.unfold();
2469 println!("{}", f);
2470 insta::assert_snapshot!(
2471 f.graph.n_nodes(),
2472 @"60",
2473 );
2474
2475 Ok(())
2476 }
2477
2478 #[test]
2479 fn triple_double_tadpole() -> Result<()> {
2480 test_initialise().unwrap();
2481 let dumbell: Graph = dot!(
2482 digraph G{
2483 edge [particle="scalar_1"];
2484 v1 -> v2;
2485 v2 -> v3;
2486 v3 -> v3;v3 -> v3;
2487 v2 -> v2; v2 -> v2;
2488 v1 -> v1;v1 -> v1;
2489 },"scalars"
2490 )?;
2491
2492 let f = Wood::new(
2493 CutStructure::empty(&dumbell),
2494 &dumbell,
2495 &UVgenerationSettings::default(),
2496 );
2497
2498 insta::assert_snapshot!(
2499 f.graph.n_nodes(),
2500 @"64");
2501
2502 let f = f.unfold_uncached();
2503 insta::assert_snapshot!(
2504 f.graph.n_nodes(),
2505 @"307");
2506
2507 Ok(())
2508 }
2509
2510 mod failing {
2511 use super::*;
2512
2513 #[test]
2514 fn lobsided_double_dumbell() -> Result<()> {
2515 test_initialise().unwrap();
2516 let dumbell: Graph = dot!(
2517 digraph G{
2518 edge [particle="scalar_1"];
2519 v1 -> v2;
2520 v2 -> v2; v1 -> v1;v1 -> v1;
2522 },"scalars"
2523 )?;
2524
2525 let spinneys: Vec<_> = dumbell
2526 .spinneys(&dumbell.full_filter())
2527 .into_iter()
2528 .filter_map(|a| Spinney::new(a, &dumbell, &dumbell.loop_momentum_basis))
2529 .collect();
2530 let f = Wood::new(
2531 CutStructure::empty(&dumbell),
2532 &dumbell,
2533 &UVgenerationSettings::default(),
2534 );
2535
2536 println!("{}", f);
2537
2538 insta::assert_snapshot!(
2539 f.graph.n_nodes(),
2540 @"8",
2541 );
2543
2544 for (_, _, d) in f.graph.iter_nodes() {
2545 println!(
2546 "//Node {}: \n{}",
2547 d.subgraph.string_label(),
2548 dumbell.dot(&d.subgraph)
2549 );
2550 }
2551 let _ff = OldWood::from_spinneys(spinneys, &dumbell); let f = f.unfold();
2556 f.debug_walk();
2557 println!("{}", f);
2558 insta::assert_snapshot!(
2559 f.normalized_node_labels_with_cover("3L").join("\n"),
2560 @r###"
2561{36,F}: T(S_36(_))*T(S_F(_))
2562{3} · {36,F}: T((-1*S_3+S_F(_))*T(S_3(_)))*T(S_36(_))
2563{C} · {36,F}: T((-1*S_C+S_F(_))*T(S_C(_)))*T(S_36(_))
2564"###
2565 );
2566 insta::assert_snapshot!(
2567 f.graph.n_nodes(),
2568 @"12");
2569
2570 let f = Wood::new(
2571 CutStructure::empty(&dumbell),
2572 &dumbell,
2573 &UVgenerationSettings::default(),
2574 )
2575 .unfold_uncached();
2576 assert!(f.compute_store.entries.is_empty());
2577 insta::assert_snapshot!(
2578 f.normalized_node_labels_with_cover("3L").join("\n"),
2579 @r###"
2580{36,F}: T(S_36(_))*T(S_F(_))
2581{3} · {36,F}: T((-1*S_3+S_F(_))*T(S_3(_)))*T(S_36(_))
2582{C} · {36,F}: T((-1*S_C+S_F(_))*T(S_C(_)))*T(S_36(_))
2583"###
2584 );
2585
2586 Ok(())
2587 }
2588
2589 #[test]
2590 fn double_double_dumbell() -> Result<()> {
2591 test_initialise().unwrap();
2592 let dumbell: Graph = dot!(
2593 digraph G{
2594 edge [particle="scalar_1"];
2595 v1 -> v2;
2596 v2 -> v2;v2 -> v2; v1 -> v1;v1 -> v1;
2598 },"scalars"
2599 )?;
2600
2601 let _spinneys: Vec<_> = dumbell
2602 .spinneys(&dumbell.full_filter())
2603 .into_iter()
2604 .map(|a| Spinney::new(a, &dumbell, &dumbell.loop_momentum_basis))
2605 .collect();
2606 let f = Wood::new(
2607 CutStructure::empty(&dumbell),
2608 &dumbell,
2609 &UVgenerationSettings::default(),
2610 );
2611
2612 println!("{}", f);
2613
2614 insta::assert_snapshot!(
2615 f.graph.n_nodes(),
2616 @"16",
2617 );
2619
2620 let f = f.unfold();
2621
2622 insta::assert_snapshot!(
2623 f.graph.n_nodes(),
2624 @"36");
2625
2626 let f = Wood::new(
2627 CutStructure::empty(&dumbell),
2628 &dumbell,
2629 &UVgenerationSettings::default(),
2630 )
2631 .unfold_uncached();
2632
2633 let foata_labels = f
2634 .graph
2635 .iter_nodes()
2636 .map(|(_, _, key)| key.foata_level_labels())
2637 .collect::<Vec<_>>();
2638 assert!(
2639 foata_labels
2640 .iter()
2641 .any(|label| label == "3,36;FU,F" || label == "36,3;FU,F"),
2642 "expected 3;F and 36;FU branch histories to combine, got:\n{}",
2643 foata_labels.join("\n")
2644 );
2645
2646 Ok(())
2647 }
2648 }
2649}