1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fmt::Display,
4 sync::Arc,
5};
6
7#[cfg(test)]
8use crate::uv::Integrands;
9#[cfg(test)]
10use crate::{
11 cff::expression::OrientationID,
12 uv::approx::direct_3d::{DirectCoordinateFrame, DirectResidueBranches, DirectSector},
13};
14#[cfg(test)]
15use std::cmp::Reverse;
16
17use ahash::AHashMap;
18use eyre::{WrapErr, eyre};
19use gammaloop_tracing_filter::LogMessage;
20use idenso::{color::ColorSimplifier, shorthands::schoonschip::Schoonschip};
21use itertools::Itertools;
22use linnet::half_edge::{
23 HedgeGraph, NoData, NodeIndex,
24 algorithms::{
25 DirectionBasis,
26 trace_unfold::{HiddenData, Independence, TraceKey, TraceUnfold, UnfoldedTraceGraph},
27 },
28 involution::{EdgeIndex, Flow, HedgePair},
29 nodestore::{NodeStorageOps, NodeStorageVec},
30 subgraph::{Inclusion, InternalSubGraph, ModifySubSet, SuBitGraph, SubSetLike, SubSetOps},
31};
32use symbolica::{
33 atom::{Atom, AtomCore, FunctionBuilder},
34 function,
35};
36use tracing::debug;
37use vakint::Vakint;
38
39use crate::{
40 debug_tags,
41 graph::{
42 Graph, LMBext, LoopMomentumBasis,
43 cuts::CutSet,
44 parse::string_utils::{ToOrderedSimple, dot_attr_value},
45 },
46 settings::global::GenerationSettings,
47 utils::{GS, W_},
48 uv::{
49 ApproximationType, RenormalizationPart, Spinney, UVgenerationSettings, UltravioletGraph,
50 approx::{
51 CutStructure, ForestNodeLike, OrientationProjection, Rooted, UVCtx,
52 direct_3d::{Direct3dApproximation, Direct3dCts},
53 final_integrand::{FinalIntegrandBuilder, FinalIntegrands},
54 integrated::{Integrated, IntegratedCts},
55 local_3d::{Local3DCts, Localizer},
56 local_4d::{self, Full4dCts, Local4dCts},
57 projected_4d::{Local4dProjectionContext, Projected4dApproximation},
58 },
59 export::{
60 UVForestLocalProvenance, UVForestNodeExpression, UVForestNodeProvenance,
61 UVForestProvenance,
62 },
63 forest::ParametricIntegrands,
64 marker::UvMarker,
65 },
66};
67use color_eyre::Result;
68use spenso::shadowing::symbolica_utils::{LogPrint, SpensoPrintSettings};
69
70pub struct Wood {
71 pub graph: HedgeGraph<SuBitGraph, Spinney>,
72 pub root: NodeIndex,
73 pub vakint_settings: vakint::VakintSettings,
74 cuts: CutStructure,
75}
76
77impl Independence<HiddenData<SuBitGraph, EdgeIndex>> for Wood {
78 fn independent(
79 &self,
80 a: &HiddenData<SuBitGraph, EdgeIndex>,
81 b: &HiddenData<SuBitGraph, EdgeIndex>,
82 ) -> bool {
83 !a.order.intersects(&b.order)
84 }
85}
86
87impl TraceUnfold<SuBitGraph> for Wood {
88 type EdgeData = SuBitGraph;
89 type HedgeData = NoData;
90 type NodeData = Spinney;
91 type NodeStorage = NodeStorageVec<Spinney>;
92
93 fn graph(
94 &self,
95 ) -> &HedgeGraph<Self::EdgeData, Self::NodeData, Self::HedgeData, Self::NodeStorage> {
96 &self.graph
97 }
98
99 fn key(&self, e: EdgeIndex) -> SuBitGraph {
100 self.graph[e].clone()
101 }
102
103 fn join_factors(&self, target: NodeIndex) -> Option<BTreeSet<SuBitGraph>> {
110 if self.graph[target].n_components() < 2 {
111 return None;
112 }
113
114 let factors = self
115 .graph
116 .iter_crown(target)
117 .filter(|hedge| self.graph.flow(*hedge) == Flow::Sink)
118 .map(|hedge| self.graph[self.graph[&hedge]].clone())
119 .collect::<BTreeSet<_>>();
120 if factors.len() != self.graph[target].n_components() {
121 return None;
122 }
123 if factors.iter().any(|factor| {
124 !self
125 .graph
126 .iter_nodes()
127 .any(|(_, _, spinney)| spinney.n_components() == 1 && spinney.filter() == factor)
128 }) {
129 return None;
130 }
131
132 let mut cover: Option<SuBitGraph> = None;
133 for factor in &factors {
134 if let Some(acc) = &mut cover {
135 if acc.intersects(factor) {
136 return None;
137 }
138 acc.union_with(factor);
139 } else {
140 cover = Some(factor.clone());
141 }
142 }
143
144 (cover.as_ref() == Some(self.graph[target].filter())).then_some(factors)
145 }
146}
147
148impl Wood {
149 pub fn current_given_pair<'a>(
150 &'a self,
151 edge_id: EdgeIndex,
152 order: usize,
153 ) -> (ForestNode<'a>, ForestNode<'a>) {
154 let HedgePair::Paired { source, sink } = self.graph[&edge_id].1 else {
155 panic!("edge in self is not paired");
156 };
157
158 let given = ForestNode {
160 spinney: &self.graph[self.graph.node_id(source)],
161 topo_order: order,
162 };
163
164 let current_for_h = self.graph.node_id(sink);
166
167 let current = ForestNode {
169 spinney: &self.graph[current_for_h],
170 topo_order: order,
171 };
172
173 (current, given)
174 }
175
176 pub(crate) fn new(
177 cuts: CutStructure,
178 graph: &Graph,
179 settings: &GenerationSettings,
180 ) -> Result<Self> {
181 let mut subgraph = graph.full_filter();
182 subgraph.subtract_with(&graph.initial_state_cut.left);
183 let mut spinneys = Vec::new();
184
185 for (cut_index, cut) in cuts.cuts.iter().enumerate() {
186 let cut_sub = subgraph.subtract(&cut.union);
187 spinneys.extend(
188 graph
189 .classified_spinneys(&cut_sub, &settings.uv, &graph.loop_momentum_basis)
190 .wrap_err_with(|| {
191 format!(
192 "graph '{}' cut {cut_index}: failed to classify UV counterterm components",
193 graph.name
194 )
195 })?,
196 );
197 }
198 graph.remove_full_observable_spinney(&mut spinneys, &settings.medium);
199
200 Ok(Self::from_spinneys(spinneys, graph, cuts, &settings.uv))
201 }
202
203 pub(crate) fn from_spinneys<I: IntoIterator<Item = Spinney>>(
204 s: I,
205 graph: &Graph,
206 cuts: CutStructure,
207 settings: &UVgenerationSettings,
208 ) -> Self {
209 let mut max_loops = 0;
210 let mut spinneys = BTreeMap::new();
211 for spinney in s {
212 max_loops = max_loops.max(graph.n_loops(spinney.filter()));
213 spinneys.entry(spinney.filter().clone()).or_insert(spinney);
214 }
215 let empty = Spinney::empty(graph);
216 spinneys.entry(empty.filter().clone()).or_insert(empty);
217 let mut vakint_settings = settings.vakint.true_settings();
218 vakint_settings.project_onto_tensor_integrals =
219 settings.project_integrated_uv_cts_onto_tensor_integrals;
220 vakint_settings.number_of_terms_in_epsilon_expansion = max_loops as i64 + 1;
223
224 let mut unions = BTreeSet::new();
225 let g: HedgeGraph<_, _> = HedgeGraph::poset(spinneys.into_values());
226 let mut poset = g.map(
227 |_, _, v| v,
228 |_, n, pair, _, e| {
229 let HedgePair::Paired { source, sink } = pair else {
230 return e.map(|_| graph.as_ref().empty_subgraph());
231 };
232 let nsource = n.node_id_ref(source);
233 let nsink = n.node_id_ref(sink);
234
235 let source_subgraph = &n.get_node_data(nsource).subgraph;
236 let sink_subgraph = &n.get_node_data(nsink).subgraph;
237 let reduced_subgraph = sink_subgraph.subtract(source_subgraph).filter;
238 let hairy_source = graph.as_ref().full_crown(source_subgraph);
239
240 if graph.as_ref().bridges_of(&reduced_subgraph).is_empty()
241 && !hairy_source.intersects(&reduced_subgraph)
242 {
243 if !source_subgraph.is_empty() {
246 unions.insert(nsink);
247 }
248 e.map(|_| reduced_subgraph)
249 } else {
250 e.map(|_| sink_subgraph.filter.clone())
251 }
252 },
253 |_, d| d,
254 );
255
256 let mut to_remove: SuBitGraph = poset.empty_subgraph();
257 let classified_filters = poset
258 .iter_nodes()
259 .map(|(_, _, spinney)| spinney.filter().clone())
260 .collect::<BTreeSet<_>>();
261
262 for u in unions {
272 let collective_scope = poset[u].subgraph.filter.clone();
273 let comps: BTreeSet<_> = graph
274 .as_ref()
275 .connected_components(&collective_scope)
276 .into_iter()
277 .collect();
278 let incoming = poset
279 .iter_crown(u)
280 .filter(|c| poset.flow(*c) == Flow::Sink)
281 .collect::<Vec<_>>();
282 let supplied = incoming
283 .iter()
284 .map(|c| &poset[poset[c]])
285 .filter(|factor| comps.contains(*factor))
286 .cloned()
287 .collect::<BTreeSet<_>>();
288 let independently_classified = comps
289 .iter()
290 .all(|component| classified_filters.contains(component));
291 if independently_classified && supplied == comps {
292 for c in incoming {
293 if comps.contains(&poset[poset[&c]]) {
294 continue;
295 }
296 to_remove.add(c);
297 to_remove.add(poset.inv(c));
298 }
299 } else {
300 for c in incoming {
301 let edge = poset[&c];
302 poset[edge] = collective_scope.clone();
303 }
304 }
305 }
306
307 poset.delete_hedges(&to_remove);
308 let root = poset
309 .iter_nodes()
310 .find(|(_, _, s)| s.subgraph.is_empty())
311 .map(|(n, _, _)| n);
312
313 Wood {
314 graph: poset,
315 root: root.expect("no empty spinney found"),
316 cuts,
317 vakint_settings,
318 }
319 }
320
321 fn unfold_with_cached_node_label_atoms(self, cache_node_label_atoms: bool) -> Forests {
322 let unfolded = self.trace_unfold::<NodeStorageVec<_>>(self.root);
323 let graph = unfolded.map(|_, _, key| OperationNode { key });
324
325 let mut cuts: Vec<(SuBitGraph, CutSet)> = Vec::new();
326 for c in &self.cuts.cuts {
327 let mut compatible: SuBitGraph = graph.empty_subgraph();
328 for (_, crown, s) in graph.iter_nodes() {
329 if s.is_compatible_with(c) {
330 for h in crown {
331 compatible.add(h);
332 }
333 }
334 }
335 cuts.push((compatible, c.clone()));
336 }
337 let root = graph
338 .iter_nodes()
339 .find(|(_, _, operation)| operation.key.is_empty())
340 .map(|(node, _, _)| node)
341 .expect("no empty trace key found in unfolded hedge-poset forest");
342
343 let forests = Forests {
344 graph,
345 cuts,
346 root,
347 cached_node_label_atoms: cache_node_label_atoms.then(AHashMap::new),
348 compute_store: ComputeStore::default(),
349 wood: self,
350 };
351
352 if cache_node_label_atoms {
353 forests.with_cached_node_label_atoms()
354 } else {
355 forests
356 }
357 }
358
359 pub fn unfold(self) -> Forests {
360 self.unfold_with_cached_node_label_atoms(true)
361 }
362
363 pub fn unfold_uncached(self) -> Forests {
364 self.unfold_with_cached_node_label_atoms(false)
365 }
366}
367
368impl Display for Wood {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 self.graph.dot_impl_fmt(
371 f,
372 &self.graph.full_filter(),
373 "start=2;\n",
374 &|_| None,
375 &|a| Some(format!("label=\"{}\"", a.string_label())),
376 &|v| Some(format!("label=\"{}\"", v.subgraph.string_label())),
377 )
378 }
379}
380#[derive(Default)]
389pub struct ComputeStore {
390 entries: AHashMap<OperationNode, ComputeNode>,
391}
392
393impl ComputeStore {
394 fn get(&self, key: &OperationNode) -> Option<&ComputeNode> {
395 self.entries.get(key)
396 }
397
398 fn require(&self, key: &OperationNode) -> Result<&ComputeNode> {
399 self.get(key)
402 .ok_or_else(|| eyre!("{key} not yet added to compute store"))
403 }
404
405 fn entry(
406 &mut self,
407 key: OperationNode,
408 ) -> std::collections::hash_map::Entry<'_, OperationNode, ComputeNode> {
409 self.entries.entry(key)
410 }
411}
412
413pub struct Forests {
414 pub graph: UnfoldedTraceGraph<Wood, OperationNode, NodeStorageVec<OperationNode>>,
415 pub root: NodeIndex,
416 cuts: Vec<(SuBitGraph, CutSet)>,
418 cached_node_label_atoms: Option<AHashMap<NodeIndex, Atom>>,
419 pub compute_store: ComputeStore,
420 wood: Wood,
421}
422
423#[derive(Clone, Debug, Hash, PartialEq, Eq)]
424pub struct OperationNode {
425 pub key: TraceKey<SuBitGraph, EdgeIndex>,
426}
427
428pub struct ForestNode<'a> {
429 pub spinney: &'a Spinney,
430 pub topo_order: usize,
431}
432
433pub struct OwnedForestNode {
434 pub spinney: Spinney,
435 pub topo_order: usize,
436}
437
438#[cfg(test)]
439struct LocalLeafOperation {
440 op: HiddenData<SuBitGraph, EdgeIndex>,
441 frontier: NodeIndex,
442}
443
444struct UnionReplayState {
445 integrated: NodeIndex,
446 local_edges: Vec<EdgeIndex>,
447}
448
449#[cfg(test)]
450impl LocalLeafOperation {
451 fn new(op: &HiddenData<SuBitGraph, EdgeIndex>, frontier: NodeIndex) -> Self {
452 Self {
453 op: op.clone(),
454 frontier,
455 }
456 }
457}
458
459impl OperationNode {
460 pub fn is_compatible_with(&self, cut: &CutSet) -> bool {
461 self.covers().is_none_or(|c| !c.intersects(&cut.union))
462 }
463
464 pub fn current<'a>(&'a self, wood: &'a Wood, topo_order: usize) -> Option<Vec<ForestNode<'a>>> {
465 if self.key.is_empty() {
466 return None;
467 }
468
469 Some(
470 self.key
471 .iter_leaf_ops()
472 .map(|op| {
473 let HedgePair::Paired { sink, .. } = wood.graph[&op.data].1 else {
474 panic!("edge in trace key is not paired");
475 };
476 let spinney = &wood.graph[wood.graph.node_id(sink)];
477 ForestNode {
478 spinney,
479 topo_order,
480 }
481 })
482 .collect::<Vec<_>>(),
483 )
484 }
485}
486
487impl AsRef<TraceKey<SuBitGraph, EdgeIndex>> for OperationNode {
488 fn as_ref(&self) -> &TraceKey<SuBitGraph, EdgeIndex> {
489 &self.key
490 }
491}
492
493impl LogMessage for ForestNode<'_> {
494 fn log_display(&self) -> String {
495 format!(
496 "subgraph={}, topo_order={}, dod={}",
497 self.spinney.filter().string_label(),
498 self.topo_order,
499 self.spinney.dod
500 )
501 }
502}
503
504impl ForestNodeLike for ForestNode<'_> {
505 fn dod(&self) -> i32 {
506 self.spinney.dod
507 }
508 fn renormalization_scheme(&self) -> crate::uv::ApproximationType {
509 self.spinney.renormalization_scheme
510 }
511 fn lmb(&self) -> &LoopMomentumBasis {
512 &self.spinney.lmb
513 }
514 fn reduced_subgraph(&self, given: &Self) -> SuBitGraph {
515 self.spinney
516 .subgraph
517 .subtract(&given.spinney.subgraph)
518 .filter
519 }
520 fn subgraph(&self) -> &SuBitGraph {
521 self.spinney.filter()
522 }
523 fn topo_order(&self) -> usize {
524 self.topo_order
525 }
526}
527
528impl LogMessage for OwnedForestNode {
529 fn log_display(&self) -> String {
530 format!(
531 "subgraph={}, topo_order={}, dod={}",
532 self.spinney.filter().string_label(),
533 self.topo_order,
534 self.spinney.dod
535 )
536 }
537}
538
539impl ForestNodeLike for OwnedForestNode {
540 fn dod(&self) -> i32 {
541 self.spinney.dod
542 }
543
544 fn renormalization_scheme(&self) -> crate::uv::ApproximationType {
545 self.spinney.renormalization_scheme
546 }
547
548 fn lmb(&self) -> &LoopMomentumBasis {
549 &self.spinney.lmb
550 }
551
552 fn reduced_subgraph(&self, given: &Self) -> SuBitGraph {
553 self.spinney
554 .subgraph
555 .subtract(&given.spinney.subgraph)
556 .filter
557 }
558
559 fn subgraph(&self) -> &SuBitGraph {
560 self.spinney.filter()
561 }
562
563 fn topo_order(&self) -> usize {
564 self.topo_order
565 }
566}
567
568impl Display for OperationNode {
569 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570 if self.key.is_empty() {
571 write!(f, "∅")
572 } else {
573 self.key.write_foata_like(f, |op| op.string_label())
574 }
575 }
576}
577
578impl OperationNode {
579 fn foata_level_labels(&self) -> String {
580 self.key
581 .iter_levels_top_down()
582 .map(|level| {
583 level
584 .iter_leaf_ops()
585 .map(|op| op.order.string_label())
586 .join(",")
587 })
588 .join(";")
589 }
590
591 pub fn covers(&self) -> Option<SuBitGraph> {
592 let mut acc: Option<SuBitGraph> = None;
593
594 for level in self.key.iter_levels_top_down() {
595 for op in level.iter_leaf_ops() {
596 if let Some(a) = &mut acc {
597 a.union_with(&op.order);
598 } else {
599 acc = Some(op.order.clone());
600 }
601 }
602 }
603
604 acc
605 }
606
607 fn forest_node(&self, graph: &Graph, topo_order: usize) -> Result<OwnedForestNode> {
608 let spinney = match self.covers() {
609 Some(cover) if !cover.is_empty() => {
610 let subgraph = InternalSubGraph::cleaned_filter_optimist(cover, graph.as_ref());
611 Spinney::new(subgraph, graph, &graph.loop_momentum_basis)
612 .wrap_err_with(|| {
613 format!(
614 "graph '{}': failed to construct forest node for operation {self}",
615 graph.name
616 )
617 })?
618 .ok_or_else(|| {
619 eyre!(
620 "graph '{}': operation {self} cover has negative degree of divergence",
621 graph.name
622 )
623 })?
624 }
625 _ => Spinney::empty(graph),
626 };
627
628 Ok(OwnedForestNode {
629 spinney,
630 topo_order,
631 })
632 }
633
634 pub fn to_atom(&self) -> Atom {
635 let mut acc = Atom::one();
636
637 let approx = FunctionBuilder::new(GS.uv_approx);
638 let mut levels = self.key.iter_levels_top_down();
639 let Some(first_level) = levels.next() else {
640 return acc;
641 };
642
643 let Some(first_op) = first_level.iter_leaf_ops().next() else {
644 return acc;
645 };
646
647 let mut last = SuBitGraph::empty(first_op.order.size());
648 for l in std::iter::once(first_level).chain(levels) {
649 let last_sym = if last.is_empty() {
650 Atom::Zero
651 } else {
652 last.symbol().to_atom()
653 };
654
655 let mut mul = Atom::one();
656
657 for op in l.iter_leaf_ops() {
658 let new = function!(op.order.symbol(), usize::from(op.data));
659 mul *= approx.clone().add_arg((new - &last_sym) * &acc).finish();
660 last.union_with(&op.order);
661 }
662
663 acc = mul
664 }
665
666 acc
667 }
668
669 }
673
674#[derive(Default)]
675pub struct ComputeNode {
676 local_4d: Option<Local4dCts>,
677 integrated: Option<IntegratedCts>,
678 cuts: AHashMap<CutSet, CutComputation>,
679}
680
681pub struct CutComputation {
682 local_3d: Local3DCts,
683 final_integrands: FinalIntegrands,
684}
685
686impl ComputeNode {
687 fn local_4d(&self, operation: &OperationNode) -> Result<&Local4dCts> {
688 self.local_4d
689 .as_ref()
690 .ok_or_else(|| eyre!("{operation} has no computed local 4D counterterm"))
691 }
692
693 fn integrated(&self, operation: &OperationNode) -> Result<&IntegratedCts> {
694 self.integrated
695 .as_ref()
696 .ok_or_else(|| eyre!("{operation} has no computed integrated counterterm"))
697 }
698
699 fn cut(&self, operation: &OperationNode, cutset: &CutSet) -> Result<&CutComputation> {
700 self.cuts.get(cutset).ok_or_else(|| {
701 eyre!("{operation} has no computed local counterterms for cut {cutset:?}")
702 })
703 }
704}
705
706impl Forests {
707 fn source_spinney(&self, node: NodeIndex) -> &Spinney {
708 &self.wood.graph[self.graph.source_node(node)]
709 }
710
711 fn recursion_input_4d(&self, node: NodeIndex) -> Result<Full4dCts> {
712 let operation = &self.graph[node];
718 let computed = self.compute_store.require(operation)?;
719 if self.graph.is_disjoint_union(node) {
720 return Ok(Full4dCts::from_factorized_local(
721 computed.local_4d(operation)?,
722 ));
723 }
724 Full4dCts::recursion_input(
725 computed.local_4d(operation)?,
726 computed.integrated(operation)?,
727 self.source_spinney(node).renormalization_scheme,
728 node == self.root,
729 &self.source_spinney(node).lmb,
730 )
731 }
732
733 fn cached_node_label_atom(&self, node: NodeIndex) -> Option<Atom> {
734 self.cached_node_label_atoms
735 .as_ref()
736 .and_then(|labels| labels.get(&node))
737 .cloned()
738 }
739
740 fn node_label_atom_factor(
741 &self,
742 frontier: NodeIndex,
743 op: &HiddenData<SuBitGraph, EdgeIndex>,
744 ) -> Atom {
745 let approx = FunctionBuilder::new(GS.uv_approx);
746 let frontier_atom = self.node_label_atom(frontier);
747
748 let current = function!(op.order.symbol(), usize::from(op.data));
749 let argument = if self.graph[frontier].covers().is_none() {
750 current
751 } else {
752 let previous = self.graph[frontier]
753 .covers()
754 .expect("non-empty frontier cover must exist")
755 .symbol();
756 (current - previous) * frontier_atom
757 };
758 approx.add_arg(argument).finish()
759 }
760
761 fn node_label_atom(&self, node: NodeIndex) -> Atom {
762 if self.graph[node].key.is_empty() {
763 return Atom::one();
764 }
765
766 if let Some(cached) = self.cached_node_label_atom(node) {
767 return cached;
768 }
769
770 self.graph
771 .leaf_op_dependency_frontiers(node, &self.wood)
772 .fold(Atom::one(), |acc, (op, frontier)| {
773 acc * self.node_label_atom_factor(frontier, op)
774 })
775 }
776
777 fn with_cached_node_label_atoms(mut self) -> Self {
778 self.cache_node_label_atoms();
779 self
780 }
781
782 fn cache_node_label_atoms(&mut self) {
783 self.cached_node_label_atoms = Some(AHashMap::new());
784 for nidx in self
785 .graph
786 .topo_sort_kahn(DirectionBasis::Underlying)
787 .unwrap()
788 {
789 let atom = self.node_label_atom(nidx);
790 self.cached_node_label_atoms
791 .as_mut()
792 .expect("node-label cache was initialized")
793 .insert(nidx, atom);
794 }
795 }
796
797 fn node_label(&self, node: NodeIndex) -> String {
798 let key = &self.graph[node];
799 if key.key.is_empty() {
800 return "∅".to_string();
801 }
802
803 let mut foata = String::new();
804 key.key
805 .write_foata_like(&mut foata, |op| op.string_label())
806 .expect("writing a trace key into a string must succeed");
807 let atom = self.node_label_atom(node);
808 format!("{foata}: {}", atom.to_ordered_simple())
809 }
810
811 fn dot_serialize_expr_atom() -> Atom {
812 Atom::var(GS.expr)
813 }
814
815 fn dot_serialize_node_atom_factor(
816 &self,
817 frontier: NodeIndex,
818 op: &HiddenData<SuBitGraph, EdgeIndex>,
819 ) -> Atom {
820 let frontier_depth = self.graph[frontier].key.op_count();
821 let (current, given) = self.wood.current_given_pair(op.data, frontier_depth);
822 function!(
825 GS.uv_approx,
826 UvMarker::subgraph(current.subgraph(), given.subgraph())
827 * self.dot_serialize_node_atom(frontier)
828 )
829 }
830
831 fn dot_serialize_node_atom(&self, node: NodeIndex) -> Atom {
832 if self.graph[node].key.is_empty() {
833 return Self::dot_serialize_expr_atom();
834 }
835
836 self.graph
837 .leaf_op_dependency_frontiers(node, &self.wood)
838 .fold(Atom::one(), |acc, (op, frontier)| {
839 acc * self.dot_serialize_node_atom_factor(frontier, op)
840 })
841 }
842
843 fn dot_serialize_node_attrs(&self, node: NodeIndex) -> String {
844 let key = &self.graph[node];
845 let label = self
846 .dot_serialize_node_atom(node)
847 .printer(SpensoPrintSettings::typst_options())
848 .to_string();
849 let cover = key
850 .covers()
851 .unwrap_or_else(|| self.graph.empty_subgraph())
852 .string_label();
853
854 format!(
855 "label={} foata={} cover={}",
856 dot_attr_value(&label),
857 dot_attr_value(&key.foata_level_labels()),
858 dot_attr_value(&cover),
859 )
860 }
861
862 pub fn dot_serialize(&self) -> String {
863 let mut output = String::new();
864 self.dot_serialize_fmt(&mut output)
865 .expect("writing hedge-poset forest DOT into a string must succeed");
866 output
867 }
868
869 pub fn dot_serialize_fmt(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
870 let attrs: AHashMap<_, _> = self
871 .graph
872 .iter_nodes()
873 .map(|(node, _, key)| (key.clone(), self.dot_serialize_node_attrs(node)))
874 .collect();
875
876 self.graph.dot_impl_fmt(
877 writer,
878 &self.graph.full_filter(),
879 "start=2;\n",
880 &|_| None,
881 &|_| None,
882 &|v| Some(attrs[v].clone()),
883 )
884 }
885
886 fn compatible_topological_order(&self, subset: &SuBitGraph) -> Result<Vec<NodeIndex>> {
887 let mut order = self
888 .graph
889 .topo_sort_kahn_of(subset, DirectionBasis::Underlying)?;
890
891 if let Some(root_position) = order.iter().position(|node| *node == self.root) {
892 if root_position != 0 {
893 let root = order.remove(root_position);
894 order.insert(0, root);
895 }
896 } else {
897 order.insert(0, self.root);
898 }
899
900 Ok(order)
901 }
902
903 #[cfg(test)]
904 fn local_leaf_operations(&self, node: NodeIndex) -> Vec<LocalLeafOperation> {
905 let mut leaves = self
906 .graph
907 .leaf_op_dependency_frontiers(node, &self.wood)
908 .map(|(op, frontier)| LocalLeafOperation::new(op, frontier))
909 .collect::<Vec<_>>();
910
911 leaves.sort_by_key(|leaf| {
912 (
913 Reverse(self.graph[leaf.frontier].key.op_count()),
914 leaf.op.order.clone(),
915 usize::from(leaf.op.data),
916 )
917 });
918 leaves
919 }
920
921 fn disconnected_component_nodes(&self, node: NodeIndex) -> Result<Vec<NodeIndex>> {
922 let operation = &self.graph[node];
923 let factors = self
924 .wood
925 .join_factors(self.graph.source_node(node))
926 .ok_or_else(|| eyre!("{operation} has no disconnected component factors"))?;
927 let target_ops = operation
928 .key
929 .iter_levels_top_down()
930 .flat_map(|level| level.iter_leaf_ops())
931 .cloned()
932 .collect::<Vec<_>>();
933
934 for op in &target_ops {
935 let owners = factors
936 .iter()
937 .filter(|factor| factor.includes(&op.order))
938 .count();
939 if owners != 1 {
940 return Err(eyre!(
941 "operation {} in {operation} belongs to {owners} disconnected factors",
942 op.order.string_label()
943 ));
944 }
945 }
946
947 factors
948 .into_iter()
949 .map(|factor| {
950 let key = target_ops
951 .iter()
952 .filter(|op| factor.includes(&op.order))
953 .fold(TraceKey::empty(), |key, op| {
954 key.push(&self.wood, op.clone())
955 });
956 self.graph
957 .iter_nodes()
958 .find_map(|(component, _, candidate)| {
959 (candidate.key == key && self.source_spinney(component).filter() == &factor)
960 .then_some(component)
961 })
962 .ok_or_else(|| {
963 eyre!(
964 "component {} of {operation} is not in the unfolded forest",
965 factor.string_label()
966 )
967 })
968 })
969 .collect()
970 }
971
972 fn operation_node_index(&self, operation: &OperationNode) -> Result<NodeIndex> {
973 let cover = operation.covers();
974 self.graph
975 .iter_nodes()
976 .find_map(|(node, _, candidate)| {
977 let source_matches = cover.as_ref().map_or_else(
978 || self.source_spinney(node).filter().is_empty(),
979 |cover| self.source_spinney(node).filter() == cover,
980 );
981 (candidate == operation && source_matches).then_some(node)
982 })
983 .ok_or_else(|| eyre!("{operation} is not in the unfolded forest"))
984 }
985
986 fn union_replay_states(&self, node: NodeIndex) -> Result<Vec<UnionReplayState>> {
987 let operation = &self.graph[node];
988 if operation.key.is_empty() {
989 return Ok(vec![UnionReplayState {
990 integrated: node,
991 local_edges: Vec::new(),
992 }]);
993 }
994
995 if !self.graph.is_disjoint_union(node) {
996 let (parent, edge) = self
997 .graph
998 .unique_parent(node)
999 .ok_or_else(|| eyre!("{operation} has no unique replay parent"))?;
1000 let mut states = self.union_replay_states(parent)?;
1001 for state in &mut states {
1002 state.local_edges.push(edge);
1003 }
1004 states.push(UnionReplayState {
1005 integrated: node,
1006 local_edges: Vec::new(),
1007 });
1008 return Ok(states);
1009 }
1010
1011 let component_states = self
1012 .disconnected_component_nodes(node)?
1013 .into_iter()
1014 .map(|component| self.union_replay_states(component))
1015 .collect::<Result<Vec<_>>>()?;
1016 let mut states = Vec::new();
1017 for components in component_states
1018 .iter()
1019 .map(|states| states.iter())
1020 .multi_cartesian_product()
1021 {
1022 let key = TraceKey::try_foata_join(
1023 components
1024 .iter()
1025 .map(|component| &self.graph[component.integrated].key),
1026 &self.wood,
1027 )
1028 .ok_or_else(|| eyre!("cannot join integrated prefixes for {operation}"))?;
1029 let mut local_edges = Vec::new();
1030 for component in components {
1031 local_edges.extend_from_slice(&component.local_edges);
1032 }
1033 let integrated = self.operation_node_index(&OperationNode { key })?;
1034 states.push(UnionReplayState {
1035 integrated,
1036 local_edges,
1037 });
1038 }
1039
1040 Ok(states)
1041 }
1042
1043 fn compute_4d_for_node(
1044 &self,
1045 node: NodeIndex,
1046 graph: &Graph,
1047 vakint: &Vakint,
1048 settings: &UVgenerationSettings,
1049 ) -> Result<(Local4dCts, IntegratedCts)> {
1050 let operation = &self.graph[node];
1051 if operation.key.is_empty() {
1052 return Ok((Local4dCts::root(), IntegratedCts::root()));
1053 }
1054
1055 if self.graph.is_disjoint_union(node) {
1056 let components = self.disconnected_component_nodes(node)?;
1057 let mut full_components = Vec::with_capacity(components.len());
1058 let mut integrated_components = Vec::with_capacity(components.len());
1059 for component in components {
1060 let component_operation = &self.graph[component];
1061 full_components.push(self.recursion_input_4d(component).wrap_err_with(|| {
1062 format!("while loading 4D component {component_operation} for {operation}")
1063 })?);
1064 integrated_components.push(
1065 self.compute_store
1066 .require(component_operation)?
1067 .integrated(component_operation)?,
1068 );
1069 }
1070
1071 let depth = graph.n_loops(
1072 &operation
1073 .covers()
1074 .expect("a non-root operation has a cover"),
1075 ) + 1;
1076 return Ok((
1077 Local4dCts::from_full_product(full_components),
1078 IntegratedCts::factorized_product(integrated_components, depth)?,
1079 ));
1080 }
1081
1082 let (parent, edge) = self
1083 .graph
1084 .unique_parent(node)
1085 .ok_or_else(|| eyre!("{operation} has no unique 4D parent"))?;
1086 let full = self
1087 .recursion_input_4d(parent)
1088 .wrap_err_with(|| format!("while loading the 4D parent for {operation}"))?;
1089 let step_order = self.graph[parent].key.op_count();
1090 let (current, given) = self.wood.current_given_pair(edge, step_order);
1091 let ctx = UVCtx::new(graph, settings);
1092 let integrated_approximation = Integrated::new(vakint, &self.wood.vakint_settings);
1093 let local = local_4d::uv_limit(&full, &ctx, ¤t, &given, ¤t, &given)?;
1094 let integrated = if settings.generate_integrated {
1095 integrated_approximation.run(&local, &ctx, ¤t, &given, ¤t, &given)?
1096 } else {
1097 IntegratedCts::root()
1098 };
1099
1100 Ok((local, integrated))
1101 }
1102
1103 fn local_3d_for_node(
1104 &self,
1105 node: NodeIndex,
1106 graph: &mut Graph,
1107 cutset: &CutSet,
1108 localizer: Localizer<'_>,
1109 settings: &UVgenerationSettings,
1110 projection_context: &mut Local4dProjectionContext,
1111 ) -> Result<CutComputation> {
1112 let operation = &self.graph[node];
1113 let forest_node = ForestNode {
1114 spinney: self.source_spinney(node),
1115 topo_order: operation.key.op_count(),
1116 };
1117 let local_3d = if operation.key.is_empty() {
1118 Local3DCts::Direct(Direct3dCts::root(graph, localizer)?)
1119 } else if settings.local_uv_cts_from_expanded_4d_integrands {
1120 let local_4d = self.compute_store.require(operation)?.local_4d(operation)?;
1125 Local3DCts::Projected4d(
1126 Projected4dApproximation::new(localizer, graph, settings)
1127 .project_local_4d(local_4d, projection_context)?,
1128 )
1129 } else if self.graph.is_disjoint_union(node) {
1130 let mut active_sectors = Vec::new();
1134 for state in self
1135 .union_replay_states(node)?
1136 .into_iter()
1137 .filter(|state| !state.local_edges.is_empty())
1138 {
1139 let integrated_operation = &self.graph[state.integrated];
1140 let mut edges = state.local_edges.into_iter().enumerate();
1141 let (offset, first_edge) = edges
1142 .next()
1143 .expect("a proper integrated prefix has a local suffix");
1144 let step_order = integrated_operation.key.op_count() + offset;
1145 let (current, given) = self.wood.current_given_pair(first_edge, step_order);
1146
1147 let mut sector = if integrated_operation.key.is_empty() {
1150 let root = Direct3dCts::root(graph, localizer)?;
1151 Direct3dApproximation::new(localizer, graph, settings)
1152 .run_local(&root, ¤t, &given, ¤t, &given)?
1153 } else {
1154 let components = if self.graph.is_disjoint_union(state.integrated) {
1160 self.disconnected_component_nodes(state.integrated)?
1161 } else {
1162 vec![state.integrated]
1163 };
1164 let finite_components = components
1165 .into_iter()
1166 .map(|component| {
1167 let operation = &self.graph[component];
1168 let integrated = self
1169 .compute_store
1170 .require(operation)?
1171 .integrated(operation)?;
1172 Ok((
1173 integrated.physical_finite_counterterm_atom(),
1174 ForestNode {
1175 spinney: self.source_spinney(component),
1176 topo_order: operation.key.op_count(),
1177 },
1178 ))
1179 })
1180 .collect::<Result<Vec<_>>>()?;
1181 let finite_counterterms = finite_components
1182 .iter()
1183 .map(|(coefficient, owner)| (coefficient, owner))
1184 .collect::<Vec<_>>();
1185 let prefix_node = ForestNode {
1190 spinney: self.source_spinney(state.integrated),
1191 topo_order: integrated_operation.key.op_count(),
1192 };
1193 Direct3dApproximation::new(localizer, graph, settings).run_integrated(
1194 &finite_counterterms,
1195 &prefix_node,
1196 ¤t,
1197 &given,
1198 ¤t,
1199 &given,
1200 )?
1201 };
1202
1203 for (offset, edge) in edges {
1204 let step_order = integrated_operation.key.op_count() + offset;
1205 let (current, given) = self.wood.current_given_pair(edge, step_order);
1206 sector = Direct3dApproximation::new(localizer, graph, settings)
1207 .run_local(§or, ¤t, &given, ¤t, &given)?;
1208 }
1209
1210 active_sectors.extend(sector.sectors()?.iter().cloned());
1213 }
1214
1215 Local3DCts::Direct(
1216 Direct3dCts::from_sectors(active_sectors)
1217 .wrap_err_with(|| format!("{operation} has no proper integrated prefixes"))?,
1218 )
1219 } else {
1220 let (parent, edge) = self
1221 .graph
1222 .unique_parent(node)
1223 .ok_or_else(|| eyre!("{operation} has no unique local parent"))?;
1224 let parent_operation = &self.graph[parent];
1225 let root;
1228 let parent_local = if parent_operation.key.is_empty() {
1229 root = Direct3dCts::root(graph, localizer)?;
1230 &root
1231 } else {
1232 self.compute_store
1233 .require(parent_operation)?
1234 .cut(parent_operation, cutset)?
1235 .local_3d
1236 .direct()?
1237 };
1238 let parent_integrated = self
1239 .compute_store
1240 .require(parent_operation)?
1241 .integrated(parent_operation)
1242 .wrap_err_with(|| {
1243 format!("while loading integrated parent {parent_operation} for {operation}")
1244 })?;
1245 let step_order = parent_operation.key.op_count();
1246 let (current, given) = self.wood.current_given_pair(edge, step_order);
1247 Local3DCts::Direct(Direct3dApproximation::new(localizer, graph, settings).run(
1251 parent_local,
1252 parent_integrated,
1253 ¤t,
1254 &given,
1255 ¤t,
1256 &given,
1257 )?)
1258 };
1259
1260 let integrated = self
1261 .compute_store
1262 .require(operation)?
1263 .integrated(operation)?;
1264 let final_builder = FinalIntegrandBuilder::new(localizer, settings);
1265 let final_integrands = match &local_3d {
1266 Local3DCts::Direct(direct) => {
1267 final_builder.build_direct(graph, &forest_node, direct, integrated)?
1268 }
1269 Local3DCts::Projected4d(projected) => {
1270 final_builder.build_projected(graph, &forest_node, projected, integrated)?
1271 }
1272 };
1273
1274 Ok(CutComputation {
1275 local_3d,
1276 final_integrands,
1277 })
1278 }
1279
1280 pub fn integrate(
1281 &mut self,
1282 graph: &Graph,
1283 vakint: &Vakint,
1284 settings: &UVgenerationSettings,
1285 ) -> Result<()> {
1286 for (order, nidx) in self
1287 .graph
1288 .topo_sort_kahn(DirectionBasis::Underlying)?
1289 .into_iter()
1290 .enumerate()
1291 {
1292 debug!(order, nidx=%nidx, key=%self.graph[nidx], "Computing hedge-poset 4D term");
1293 let operation = self.graph[nidx].clone();
1294 let (local_4d, integrated) = self.compute_4d_for_node(nidx, graph, vakint, settings)?;
1295 let cover = operation
1296 .covers()
1297 .unwrap_or_else(|| self.graph.empty_subgraph())
1298 .string_label();
1299 let source = self.source_spinney(nidx).filter().string_label();
1300 debug_tags!(#generation, #uv, #integrated, #graph, #term, #inspect;
1301 stage = "hedge_poset_4d_node_done",
1302 order,
1303 node_index = %nidx,
1304 forest_term = %operation,
1305 cover = %cover,
1306 source = %source,
1307 is_union = self.graph.is_disjoint_union(nidx),
1308 log.local_4d = local_4d.atom(),
1309 log.integrated_pole = integrated.physical_pole_atom(),
1310 log.integrated_finite = integrated.physical_finite_counterterm_atom(),
1311 "Computed hedge-poset 4D node"
1312 );
1313 let computed = self.compute_store.entry(operation).or_default();
1314 computed.local_4d = Some(local_4d);
1315 computed.integrated = Some(integrated);
1316 }
1317
1318 Ok(())
1319 }
1320
1321 pub(crate) fn compute(
1322 &mut self,
1323 graph: &mut Graph,
1324 vakint: &Vakint,
1325 orientation: OrientationProjection<'_>,
1326 settings: &UVgenerationSettings,
1327 ) -> Result<()> {
1328 if settings.generate_integrated || settings.local_uv_cts_from_expanded_4d_integrands {
1329 self.integrate(graph, vakint, settings)?;
1332 } else {
1333 let zero = IntegratedCts::root();
1339 for (_, _, operation) in self.graph.iter_nodes() {
1340 self.compute_store
1341 .entry(operation.clone())
1342 .or_default()
1343 .integrated = Some(zero.clone());
1344 }
1345 }
1346 let mut projection_context = Local4dProjectionContext::default();
1347
1348 for (compatible_subset, cutset) in self.cuts.clone() {
1349 let localizer = Localizer::new(&cutset, orientation);
1350 for (order, nidx) in self
1351 .compatible_topological_order(&compatible_subset)?
1352 .into_iter()
1353 .enumerate()
1354 {
1355 debug!(order, nidx=%nidx, key=%self.graph[nidx], "Computing hedge-poset per-cut term");
1356 let operation = self.graph[nidx].clone();
1357 let cut_computation = self.local_3d_for_node(
1361 nidx,
1362 graph,
1363 &cutset,
1364 localizer,
1365 settings,
1366 &mut projection_context,
1367 )?;
1368 self.compute_store
1369 .entry(operation)
1370 .or_default()
1371 .cuts
1372 .insert(cutset.clone(), cut_computation);
1373 }
1374 }
1375
1376 Ok(())
1377 }
1378
1379 pub(crate) fn orientation_parametric_exprs(
1380 &self,
1381 graph: &Graph,
1382 _settings: &UVgenerationSettings,
1383 ) -> Result<Vec<ParametricIntegrands>> {
1384 let split_momentum_replacements = graph
1385 .iter_edges_of(
1386 &graph
1387 .full_filter()
1388 .subtract(&graph.initial_state_cut)
1389 .subtract(&graph.tree_edges),
1390 )
1391 .filter_map(|(pair, edge_index, _)| {
1392 (!matches!(pair, HedgePair::Unpaired { .. }))
1393 .then(|| GS.split_mom_pattern_simple(edge_index))
1394 })
1395 .collect::<Vec<_>>();
1396 let mut expressions = Vec::with_capacity(self.cuts.len());
1397
1398 for (compatible_subset, cutset) in &self.cuts {
1399 let mut sum: Option<FinalIntegrands> = None;
1400 for nidx in self.compatible_topological_order(compatible_subset)? {
1401 let operation = &self.graph[nidx];
1402 let terms = self
1403 .compute_store
1404 .require(operation)?
1405 .cut(operation, cutset)?
1406 .final_integrands
1407 .map_expressions(|integrand| Ok(integrand.clone().collect_color()))?;
1408 sum = Some(match sum {
1409 Some(sum) => sum.zip_add(terms).wrap_err_with(|| {
1410 format!("while aggregating hedge-poset term {operation} for cut {cutset:?}")
1411 })?,
1412 None => terms,
1413 });
1414 }
1415 let integrands = sum
1421 .ok_or_else(|| eyre!("No terms in hedge-poset forest for cut {cutset:?}"))?
1422 .map_expressions(|integrand| {
1423 Ok(integrand
1424 .replace_multiple(&split_momentum_replacements)
1425 .replace(function!(GS.den, W_.a_, W_.b_, W_.c_, W_.d_))
1426 .with(W_.d_))
1427 })?;
1428 expressions.push(ParametricIntegrands::from_final(integrands, cutset.clone()));
1429 }
1430
1431 Ok(expressions)
1432 }
1433
1434 pub(crate) fn export_node_expressions(
1435 &self,
1436 forest_index: usize,
1437 post_process: &mut impl FnMut(Atom) -> Atom,
1438 ) -> Result<Vec<UVForestNodeExpression>> {
1439 let (cut_compatible_forest_subset, cutset) = self
1440 .cuts
1441 .first()
1442 .ok_or_else(|| eyre!("No cuts in hedge-poset forest export"))?;
1443 let mut terms = Vec::new();
1444 for (node_index, nidx) in self
1445 .compatible_topological_order(cut_compatible_forest_subset)?
1446 .into_iter()
1447 .enumerate()
1448 {
1449 let operation = &self.graph[nidx];
1450 let computed = self.compute_store.require(operation)?;
1451 let cut_computation = computed.cut(operation, cutset)?;
1452 let final_integrands = cut_computation
1453 .final_integrands
1454 .clone()
1455 .into_integrands()
1456 .resolved()?
1457 .map(|numerator| post_process(numerator.clone()));
1458 let node_key = operation.to_string();
1459 let provenance = self.node_export_3d_provenance(nidx, &cut_computation.local_3d);
1460 for (term_index, (&residue_index, numerator)) in final_integrands.iter().enumerate() {
1461 terms.push(UVForestNodeExpression {
1462 forest_index,
1463 node_index,
1464 node_key: node_key.clone(),
1465 term_index,
1466 residue_index,
1467 numerator: numerator.clone(),
1468 forest_provenance: Some(provenance.clone()),
1469 });
1470 }
1471 }
1472
1473 Ok(terms)
1474 }
1475
1476 pub(crate) fn export_4d_node_expressions(
1477 &self,
1478 forest_index: usize,
1479 ) -> Result<Vec<UVForestNodeExpression>> {
1480 let (cut_compatible_forest_subset, _) = self
1481 .cuts
1482 .first()
1483 .ok_or_else(|| eyre!("No cuts in hedge-poset forest export"))?;
1484 self.compatible_topological_order(cut_compatible_forest_subset)?
1485 .into_iter()
1486 .enumerate()
1487 .map(|(node_index, nidx)| {
1488 let operation = &self.graph[nidx];
1489 let local_4d = self.compute_store.require(operation)?.local_4d(operation)?;
1490 Ok(UVForestNodeExpression {
1491 forest_index,
1492 node_index,
1493 node_key: operation.to_string(),
1494 term_index: 0,
1495 residue_index: crate::cff::CutCFFIndex::new_all_none(),
1496 numerator: local_4d.atom().clone(),
1497 forest_provenance: Some(self.node_export_4d_provenance(nidx, local_4d)),
1498 })
1499 })
1500 .collect()
1501 }
1502
1503 fn node_parent_keys(&self, node: NodeIndex) -> Vec<String> {
1504 let mut parent_keys = self
1505 .graph
1506 .parents(node)
1507 .map(|(parent, _)| self.graph[parent].to_string())
1508 .collect::<Vec<_>>();
1509 parent_keys.sort();
1510 parent_keys.dedup();
1511 parent_keys
1512 }
1513
1514 fn node_export_4d_provenance(
1515 &self,
1516 node: NodeIndex,
1517 local_4d: &Local4dCts,
1518 ) -> Arc<UVForestProvenance> {
1519 Arc::new(UVForestProvenance {
1520 node: UVForestNodeProvenance {
1521 parent_keys: self.node_parent_keys(node),
1522 local: UVForestLocalProvenance::FourD {
1523 components: local_4d.provenance_components().to_vec(),
1524 branches: local_4d.provenance_branches().to_vec(),
1525 },
1526 },
1527 })
1528 }
1529
1530 fn node_export_3d_provenance(
1531 &self,
1532 node: NodeIndex,
1533 local_3d: &Local3DCts,
1534 ) -> Arc<UVForestProvenance> {
1535 Arc::new(UVForestProvenance {
1536 node: UVForestNodeProvenance {
1537 parent_keys: self.node_parent_keys(node),
1538 local: UVForestLocalProvenance::ThreeD {
1539 projection_paths: local_3d.projection_paths(),
1540 },
1541 },
1542 })
1543 }
1544
1545 pub(crate) fn renormalization_part_of_ends(
1546 &self,
1547 graph: &Graph,
1548 settings: &UVgenerationSettings,
1549 ) -> Result<RenormalizationPart> {
1550 let mut sum = Atom::Zero;
1551 let marker = UvMarker::new(settings);
1552
1553 let wild = Atom::var(W_.x___);
1554
1555 let replacements =
1556 graph.integrand_replacement(&graph.full_filter(), &graph.loop_momentum_basis, &[wild]);
1557 for (node, mut crown, key) in self.graph.iter_nodes() {
1558 if crown.any(|r| self.graph.flow(r).is_source()) {
1559 continue;
1560 }
1561
1562 let forest_node = key.forest_node(graph, key.key.op_count())?;
1563 let components = if self.graph.is_disjoint_union(node) {
1567 self.disconnected_component_nodes(node)?
1568 } else {
1569 vec![node]
1570 };
1571 let physical = components.into_iter().try_fold(
1572 Atom::one(),
1573 |product, component| -> Result<Atom> {
1574 let component_key = &self.graph[component];
1575 let integrated = self
1576 .compute_store
1577 .require(component_key)?
1578 .integrated(component_key)?;
1579 let projection = match self.source_spinney(component).renormalization_scheme {
1580 ApproximationType::MUV | ApproximationType::IR => {
1581 integrated.physical_finite_counterterm_atom()
1582 }
1583 ApproximationType::PolePart => integrated.physical_pole_atom(),
1584 scheme => {
1585 return Err(eyre!("No terminal counterterm projection for {scheme}"));
1586 }
1587 };
1588 Ok(product * projection)
1589 },
1590 )?;
1591 let atom = marker.prefix(&graph.full_filter(), forest_node.subgraph(), &physical);
1592 debug!(
1593 key=%key,
1594 expr = % atom.expand_num().log_print(None),"Term before simplification"
1595 );
1596 let atom = (&atom
1597 * &graph.global_prefactor.projector
1598 * &graph.global_prefactor.num
1599 * &graph.overall_factor)
1600 .simplify_color()
1601 .expand_num()
1602 .to_dots();
1603
1604 debug!(
1605 key=%key,
1606 expr = % atom.log_print(None),"Term"
1607 );
1608 sum += atom;
1609 }
1610
1611 Ok(RenormalizationPart::new(
1612 sum.replace_multiple(&replacements)
1613 .replace(GS.m_uv_expansion)
1614 .with(GS.m_uv_vacuum),
1615 self.graph.n_nodes(),
1616 ))
1617 }
1618
1619 pub fn debug_walk(&self) {
1620 let mut cover_groups: BTreeMap<SuBitGraph, Vec<NodeIndex>> = BTreeMap::new();
1621
1622 self.graph
1623 .topo_sort_kahn(DirectionBasis::Underlying)
1624 .unwrap()
1625 .iter()
1626 .for_each(|nidx| {
1627 let trace_key = &self.graph[*nidx];
1628 cover_groups
1629 .entry(trace_key.covers().unwrap_or(self.graph.empty_subgraph()))
1630 .and_modify(|e| e.push(*nidx))
1631 .or_insert_with(|| vec![*nidx]);
1632
1633 println!("Node {}:{}", nidx, self.node_label(*nidx));
1634 });
1635
1636 println!("edge [constraint=true style=invis];");
1637 for (a, b) in cover_groups.values().tuple_windows() {
1638 println!("{}->{}", a.first().unwrap(), b.first().unwrap())
1639 }
1640 println!("edge [style=solid];");
1641
1642 for (s, g) in cover_groups.iter() {
1643 println!("subgraph group_{} {{rank=same; ", s.string_label());
1644 for n in g {
1645 println!("{};", n.0);
1646 }
1647 println!("}}");
1648 }
1649 }
1650}
1651
1652impl Display for Forests {
1653 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1654 let labels: AHashMap<_, _> = self
1655 .graph
1656 .iter_nodes()
1657 .map(|(node, _, key)| (key.clone(), self.node_label(node)))
1658 .collect();
1659 self.graph.dot_impl_fmt(
1660 f,
1661 &self.graph.full_filter(),
1662 "start=2;\n",
1663 &|_| None,
1664 &|_| None,
1665 &|v| Some(format!("label=\"{}\"", labels[v])),
1666 )
1667 }
1668}
1669
1670#[cfg(test)]
1671mod tests {
1672 use crate::{
1673 dot,
1674 graph::{FeynmanGraph, Graph, parse::IntoGraph},
1675 initialisation::test_initialise,
1676 processes::DotExportSettings,
1677 uv::{
1678 UltravioletGraph, Wood as OldWood, approx::local_4d::Local4dBranch,
1679 settings::RenormalizationPrescriptionSettings,
1680 },
1681 };
1682
1683 use super::*;
1684 use color_eyre::Result;
1685
1686 impl Forests {
1687 fn normalized_node_label(&self, node: NodeIndex) -> String {
1688 let label = self.node_label(node);
1689 let mut normalized = String::with_capacity(label.len());
1690 let mut chars = label.chars().peekable();
1691
1692 while let Some(ch) = chars.next() {
1693 normalized.push(ch);
1694 if ch != '(' {
1695 continue;
1696 }
1697
1698 let mut digits = String::new();
1699 while chars.peek().is_some_and(|next| next.is_ascii_digit()) {
1700 digits.push(chars.next().expect("peeked digit must exist"));
1701 }
1702
1703 if digits.is_empty() || !matches!(chars.peek(), Some(')')) {
1704 normalized.push_str(&digits);
1705 } else {
1706 normalized.push('_');
1707 }
1708 }
1709
1710 normalized
1711 }
1712
1713 fn normalized_node_labels_with_cover(&self, cover_label: &str) -> Vec<String> {
1714 let mut labels = self
1715 .graph
1716 .iter_nodes()
1717 .filter(|(_, _, operation)| {
1718 operation
1719 .covers()
1720 .is_some_and(|cover| cover.string_label() == cover_label)
1721 })
1722 .map(|(node, _, _)| self.normalized_node_label(node))
1723 .collect::<Vec<_>>();
1724 labels.sort();
1725 labels
1726 }
1727 }
1728
1729 #[test]
1730 fn three_d_compute_skips_unused_four_d_atoms_when_integration_is_disabled() -> Result<()> {
1731 test_initialise().unwrap();
1732 for project_local_4d in [false, true] {
1733 let mut graph: Graph = dot!(
1734 digraph scalar_self_energy {
1735 edge [particle="scalar_1"];
1736 node [num=1];
1737 ext [style=invis];
1738 ext -> a;
1739 b -> ext;
1740 a -> b;
1741 a -> b;
1742 },
1743 "scalars"
1744 )?;
1745 let settings = GenerationSettings {
1746 uv: UVgenerationSettings {
1747 generate_integrated: false,
1748 local_uv_cts_from_expanded_4d_integrands: project_local_4d,
1749 ..Default::default()
1750 },
1751 ..Default::default()
1752 };
1753 let orientation_pattern = crate::settings::global::OrientationPattern::default();
1754 let options = graph.denominator_only_cff_3d_expression_options();
1755 let production = graph.generate_3d_expression_for_integrand(
1756 &[],
1757 &graph.get_esurface_canonization(&graph.loop_momentum_basis),
1758 &options,
1759 Some(&Atom::one()),
1760 )?;
1761 let mut forests = Wood::new(CutStructure::empty(&graph), &graph, &settings)?.unfold();
1762
1763 forests.compute(
1764 &mut graph,
1765 crate::utils::vakint()?,
1766 OrientationProjection::exact_expression(
1767 &production,
1768 &options,
1769 &orientation_pattern,
1770 true,
1771 ),
1772 &settings.uv,
1773 )?;
1774
1775 assert_eq!(forests.compute_store.entries.len(), forests.graph.n_nodes());
1776 for (_, _, operation) in forests.graph.iter_nodes() {
1777 let computed = forests.compute_store.require(operation)?;
1778 assert_eq!(
1779 computed.local_4d.is_some(),
1780 project_local_4d,
1781 "4D coefficients must be constructed exactly when the projected route needs them for {operation}"
1782 );
1783 let integrated = computed.integrated(operation)?;
1784 assert!(integrated.physical_pole_atom().is_zero());
1785 assert!(integrated.physical_finite_counterterm_atom().is_zero());
1786 assert!(
1787 !computed.cuts.is_empty(),
1788 "both routes must compute a cut for {operation}"
1789 );
1790 assert!(
1791 computed
1792 .cuts
1793 .values()
1794 .all(|cut| cut.final_integrands.iter().next().is_some()),
1795 "every computed cut must retain a 3D final integrand for {operation}"
1796 );
1797 }
1798 }
1799
1800 Ok(())
1801 }
1802
1803 #[test]
1804 fn vacuum_subtraction_prunes_only_full_observable_spinneys() -> Result<()> {
1805 use crate::settings::global::MediumMode;
1806
1807 test_initialise()?;
1808 let graphs: Vec<Graph> = dot!(
1809 digraph sunrise {
1810 edge [particle="scalar_1"];
1811 v1 -> v2; v1 -> v2; v1 -> v2;
1812 }
1813 digraph dumbbell {
1814 edge [particle="scalar_1"];
1815 v1 -> v2;
1816 v2 -> v2;
1817 v1 -> v1; v1 -> v1;
1818 }
1819 digraph sunrise_with_bridge {
1820 edge [particle="scalar_1"];
1821 ext [style=invis];
1822 v1 -> v2; v1 -> v2; v1 -> v2;
1823 v2 -> v3;
1824 ext -> v3; v3 -> ext;
1825 }, "scalars"
1826 )?;
1827 for graph in graphs {
1828 let full = InternalSubGraph::cleaned_filter_pessimist(
1830 graph.full_filter().subtract(&graph.tree_edges),
1831 graph.as_ref(),
1832 );
1833 for subtract_uv in [false, true] {
1834 let mut settings = GenerationSettings::default();
1835 settings.uv.subtract_uv = subtract_uv;
1836 let ordinary = Wood::new(CutStructure::empty(&graph), &graph, &settings)?;
1837 let all = ordinary
1838 .graph
1839 .iter_nodes()
1840 .map(|(_, _, spinney)| spinney.filter().clone())
1841 .collect::<BTreeSet<_>>();
1842 let proper = all
1843 .iter()
1844 .filter(|filter| **filter != full.filter)
1845 .cloned()
1846 .collect::<BTreeSet<_>>();
1847 assert!(proper.iter().any(|filter| filter.is_empty()));
1848 if subtract_uv {
1849 assert!(all.contains(&full.filter), "{}", graph.name);
1850 assert!(proper.len() > 1, "{}", graph.name);
1851 }
1852 for mode in [
1853 MediumMode::Vacuum,
1854 MediumMode::ThermodynamicEquilibrium,
1855 MediumMode::ZeroTemperatureEquilibrium,
1856 ] {
1857 settings.medium.mode = mode;
1858 for vacuum_subtraction in [false, true] {
1859 if mode == MediumMode::Vacuum && vacuum_subtraction {
1860 continue; }
1862 settings.medium.vacuum_subtraction = vacuum_subtraction;
1863 let expected = if vacuum_subtraction { &proper } else { &all };
1864 let wood = Wood::new(CutStructure::empty(&graph), &graph, &settings)?;
1865 assert_eq!(
1866 &wood
1867 .graph
1868 .iter_nodes()
1869 .map(|(_, _, spinney)| spinney.filter().clone())
1870 .collect::<BTreeSet<_>>(),
1871 expected,
1872 "{}: {mode:?}, subtract_uv={subtract_uv}, vacuum_subtraction={vacuum_subtraction}",
1873 graph.name
1874 );
1875 let mut spinneys = graph.classified_spinneys(
1877 &graph.full_filter(),
1878 &settings.uv,
1879 &graph.loop_momentum_basis,
1880 )?;
1881 graph.remove_full_observable_spinney(&mut spinneys, &settings.medium);
1882 assert_eq!(
1883 &spinneys
1884 .iter()
1885 .map(|spinney| spinney.filter().clone())
1886 .collect::<BTreeSet<_>>(),
1887 expected,
1888 "legacy {}: {mode:?}, subtract_uv={subtract_uv}, vacuum_subtraction={vacuum_subtraction}",
1889 graph.name
1890 );
1891 }
1892 }
1893 }
1894 }
1895 Ok(())
1896 }
1897
1898 #[test]
1899 fn vacuum_subtraction_commutes_with_local_and_integrated_forests() -> Result<()> {
1900 use crate::{
1901 integrands::process::param_builder::ParamBuilderGraph,
1902 numerator::{
1903 soft_energy_certificate::{EnergyZeroOutcome, SoftEnergyAlgebra},
1904 symbolica_ext::NumeratorAtomExt,
1905 },
1906 settings::global::MediumMode,
1907 };
1908 use symbolica::atom::AtomView;
1909 use three_dimensional_reps::thermal::OCCUPATION_FORM;
1910 test_initialise()?;
1911 let graphs: Vec<Graph> = dot!(
1912 digraph nested {
1913 edge [particle="scalar_1"]; node [num=1];
1914 v1 -> v2; v1 -> v2; v1 -> v2;
1915 v1 -> v1;
1916 }
1917 digraph disjoint {
1918 edge [particle="scalar_1"]; node [num=1];
1919 v1 -> v2; v1 -> v2;
1920 v3 -> v4; v3 -> v4;
1921 v2 -> v3; v1 -> v4;
1922 }, "scalars"
1923 )?;
1924 for graph in graphs {
1925 for medium in [
1926 MediumMode::ThermodynamicEquilibrium,
1927 MediumMode::ZeroTemperatureEquilibrium,
1928 ] {
1929 for generate_integrated in [false, true] {
1930 let mut settings = GenerationSettings::default();
1931 settings.medium.mode = medium;
1932 settings.medium.vacuum_subtraction = true;
1933 settings.uv.softct = false;
1934 settings.uv.generate_integrated = generate_integrated;
1935 let mut graph = graph.clone();
1936 let options = graph.production_cff_3d_expression_options(&settings)?;
1937 let contract =
1938 graph.paired_edges(&graph.tree_edges.subtract(&graph.initial_state_cut));
1939 let production = graph
1940 .generate_3d_expression_for_integrand(&contract, &None, &options, None)?;
1941 let mut results = Vec::new();
1942 for early_subtraction in [false, true] {
1943 let projection = OrientationProjection::exact_expression(
1944 &production,
1945 &options,
1946 &settings.orientation_pattern,
1947 true,
1948 )
1949 .with_vacuum_subtraction(early_subtraction);
1950 let mut forests =
1953 Wood::new(CutStructure::empty(&graph), &graph, &settings)?.unfold();
1954 forests.compute(
1955 &mut graph,
1956 crate::utils::vakint()?,
1957 projection,
1958 &settings.uv,
1959 )?;
1960 results.push(forests.orientation_parametric_exprs(&graph, &settings.uv)?);
1961 }
1962 assert_eq!(results[0].len(), results[1].len());
1963 for (late, early) in results[0].iter().zip(&results[1]) {
1964 let late = late.integrands.resolved()?;
1965 let early = early.integrands.resolved()?;
1966 assert_eq!(late.iter().count(), early.iter().count());
1967 for ((late_key, raw), (early_key, actual)) in late.iter().zip(early.iter())
1968 {
1969 assert_eq!(late_key, early_key);
1970 let vacuum = graph.make_thermal_distributions_explicit(
1971 raw,
1972 MediumMode::Vacuum,
1973 graph.iter_edges().map(|(_, edge, _)| edge),
1974 )?;
1975 assert!(
1976 raw.is_zero() || raw.contains_symbol(GS.thermal_weight_wrapper)
1977 );
1978 assert!(
1979 actual.is_zero()
1980 || actual.contains_symbol(GS.thermal_weight_wrapper)
1981 );
1982 let difference = (actual - (raw - vacuum))
1985 .replace(function!(GS.thermal_weight_wrapper, W_.a_))
1986 .with(W_.a_)
1987 .replace_multiple(&*OCCUPATION_FORM)
1988 .expand();
1989 let mut occupations = BTreeSet::new();
1992 difference.visitor(&mut |view| {
1993 if let AtomView::Fun(call) = view
1994 && call.get_symbol() == GS.thermal_distribution
1995 {
1996 occupations.insert(view.to_owned());
1997 return false;
1998 }
1999 true
2000 });
2001 let coefficients = difference
2002 .coefficient_list_exact(&occupations.into_iter().collect_vec())
2003 .expect("scalar difference must be polynomial in occupations");
2004 for (occupation, coefficient) in coefficients {
2005 let certificate =
2006 SoftEnergyAlgebra::default().scalar_zero(&coefficient)?;
2007 assert!(
2008 matches!(certificate, EnergyZeroOutcome::Zero(_)),
2009 "{}: {medium:?}, integrated={generate_integrated}, residue={late_key:?}, occupation={occupation}: {certificate:?}: {coefficient}",
2010 graph.name
2011 );
2012 }
2013 }
2014 }
2015 }
2016 }
2017 }
2018 Ok(())
2019 }
2020
2021 #[test]
2022 fn local_leaf_operations_follow_dependency_frontiers() -> Result<()> {
2023 test_initialise().unwrap();
2024 let dumbell: Graph = dot!(
2025 digraph G{
2026 edge [particle="scalar_1"];
2027 v1 -> v2;
2028 v2 -> v2;
2029 v1 -> v1;v1 -> v1;
2030 },"scalars"
2031 )?;
2032
2033 let forests = Wood::new(
2034 CutStructure::empty(&dumbell),
2035 &dumbell,
2036 &GenerationSettings::default(),
2037 )?
2038 .unfold();
2039
2040 let frontier_label = |operation: &OperationNode| {
2041 operation
2042 .covers()
2043 .map_or_else(|| "∅".to_string(), |cover| cover.string_label())
2044 };
2045 let leaf_labels = |node| {
2046 forests
2047 .local_leaf_operations(node)
2048 .iter()
2049 .map(|leaf| {
2050 (
2051 leaf.op.order.string_label(),
2052 frontier_label(&forests.graph[leaf.frontier]),
2053 )
2054 })
2055 .collect::<Vec<_>>()
2056 };
2057
2058 let root_disconnected = forests
2059 .graph
2060 .iter_nodes()
2061 .find_map(|(node, _, _)| {
2062 forests
2063 .normalized_node_label(node)
2064 .starts_with("{36,F}:")
2065 .then_some(node)
2066 })
2067 .expect("lopsided dumbbell should contain a root disconnected frontier");
2068 assert_eq!(
2069 leaf_labels(root_disconnected),
2070 vec![
2071 ("36".to_string(), "∅".to_string()),
2072 ("F".to_string(), "∅".to_string())
2073 ]
2074 );
2075 let structured_dot = forests.dot_serialize();
2076 let mut join_markers = Vec::new();
2077 for (node, _, _) in forests.graph.iter_nodes() {
2078 for leaf in forests.local_leaf_operations(node) {
2079 let frontier_depth = forests.graph[leaf.frontier].key.op_count();
2080 let (current, given) = forests
2081 .wood
2082 .current_given_pair(leaf.op.data, frontier_depth);
2083 if current.subgraph() == &leaf.op.order {
2084 continue;
2085 }
2086 join_markers.push(
2087 UvMarker::subgraph(current.subgraph(), given.subgraph())
2088 .printer(SpensoPrintSettings::typst_options())
2089 .to_string(),
2090 );
2091 }
2092 }
2093 assert!(
2094 !join_markers.is_empty(),
2095 "lopsided dumbbell must contain a disconnected join"
2096 );
2097 for marker in join_markers {
2098 assert!(
2099 structured_dot.contains(&marker),
2100 "structured DOT must use the runtime current/given marker {marker}"
2101 );
2102 }
2103
2104 let dependent_disconnected = forests
2105 .graph
2106 .iter_nodes()
2107 .find_map(|(node, _, _)| {
2108 forests
2109 .normalized_node_label(node)
2110 .starts_with("{C} · {36,F}:")
2111 .then_some(node)
2112 })
2113 .expect("lopsided dumbbell should contain a C-dependent disconnected frontier");
2114 assert_eq!(
2115 leaf_labels(dependent_disconnected),
2116 vec![
2117 ("F".to_string(), "C".to_string()),
2118 ("36".to_string(), "∅".to_string())
2119 ]
2120 );
2121 let replay_states = forests.union_replay_states(dependent_disconnected)?;
2122 assert_eq!(replay_states.len(), 6);
2123 assert_eq!(
2124 replay_states
2125 .iter()
2126 .filter(|state| !state.local_edges.is_empty())
2127 .count(),
2128 5
2129 );
2130 Ok(())
2131 }
2132
2133 #[test]
2134 fn union_terms_project_factorized_typed_4d_values() -> Result<()> {
2135 test_initialise().unwrap();
2136 let graph: Graph = dot!(
2137 digraph G{
2138 edge [particle="scalar_1"];
2139 v1 -> v2;
2140 v2 -> v2;
2141 v1 -> v1;v1 -> v1;
2142 },"scalars"
2143 )?;
2144 let settings = GenerationSettings {
2147 uv: UVgenerationSettings {
2148 generate_integrated: false,
2149 local_uv_cts_from_expanded_4d_integrands: true,
2150 ..Default::default()
2151 },
2152 ..Default::default()
2153 };
2154 let cut_structure = CutStructure::empty(&graph);
2155 let mut forests = Wood::new(cut_structure, &graph, &settings)?.unfold();
2156 forests.integrate(&graph, crate::utils::vakint()?, &settings.uv)?;
2157
2158 let unions = forests
2159 .graph
2160 .iter_nodes()
2161 .filter_map(|(node, _, operation)| {
2162 (forests.graph.is_disjoint_union(node) && !operation.key.is_empty()).then_some(node)
2163 })
2164 .collect::<Vec<_>>();
2165 assert!(!unions.is_empty());
2166
2167 for union in unions {
2168 let components = forests.disconnected_component_nodes(union)?;
2169 let expected = Local4dCts::from_full_product(
2170 components
2171 .into_iter()
2172 .map(|component| forests.recursion_input_4d(component))
2173 .collect::<Result<Vec<_>>>()?,
2174 );
2175 let operation = &forests.graph[union];
2176 let local = forests
2177 .compute_store
2178 .require(operation)?
2179 .local_4d(operation)?;
2180
2181 assert!((local.atom().collect_factors() - expected.atom().collect_factors()).is_zero());
2184 }
2185 Ok(())
2186 }
2187
2188 #[test]
2189 fn union_terms_replay_component_paths_from_typed_roots() -> Result<()> {
2190 use crate::graph::feynman_graph::FeynmanGraph;
2191
2192 test_initialise().unwrap();
2193 let mut graph: Graph = dot!(
2194 digraph G{
2195 edge [particle="scalar_1"];
2196 v1 -> v2;
2197 v2 -> v2;
2198 v1 -> v1;v1 -> v1;
2199 },"scalars"
2200 )?;
2201 let settings = GenerationSettings::default();
2202 let cut_structure = CutStructure::empty(&graph);
2203 let cutset = cut_structure
2204 .cuts
2205 .first()
2206 .expect("empty cut structure has one cut")
2207 .clone();
2208 let mut forests = Wood::new(cut_structure, &graph, &settings)?.unfold();
2209
2210 let root_disconnected = forests
2211 .graph
2212 .iter_nodes()
2213 .find_map(|(node, _, _)| {
2214 forests
2215 .normalized_node_label(node)
2216 .starts_with("{36,F}:")
2217 .then_some(node)
2218 })
2219 .expect("lopsided dumbbell should contain a root disconnected frontier");
2220 let dependent_disconnected = forests
2221 .graph
2222 .iter_nodes()
2223 .find_map(|(node, _, _)| {
2224 forests
2225 .normalized_node_label(node)
2226 .starts_with("{C} · {36,F}:")
2227 .then_some(node)
2228 })
2229 .expect("lopsided dumbbell should contain a C-dependent disconnected frontier");
2230 let non_root_frontier = forests
2231 .local_leaf_operations(dependent_disconnected)
2232 .first()
2233 .expect("dependent disconnected operation has a leaf")
2234 .frontier;
2235 assert_eq!(
2236 forests.graph[non_root_frontier]
2237 .covers()
2238 .expect("non-root frontier has a cover")
2239 .string_label(),
2240 "C"
2241 );
2242
2243 let operations = forests
2244 .graph
2245 .iter_nodes()
2246 .map(|(_, _, operation)| operation.clone())
2247 .collect::<Vec<_>>();
2248 for operation in operations {
2250 forests
2251 .compute_store
2252 .entry(operation)
2253 .or_default()
2254 .integrated = Some(IntegratedCts::root());
2255 }
2256
2257 let orientation_pattern = crate::settings::global::OrientationPattern::default();
2258 let options = graph.denominator_only_cff_3d_expression_options();
2259 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
2260 let contract_edges = graph
2261 .iter_edges_of(&graph.tree_edges)
2262 .map(|(_, edge, _)| edge)
2263 .collect_vec();
2264 let production = graph.generate_3d_expression_for_integrand(
2265 &contract_edges,
2266 &canonization,
2267 &options,
2268 Some(&Atom::one()),
2269 )?;
2270 let localizer = Localizer::new(
2271 &cutset,
2272 OrientationProjection::exact_expression(
2273 &production,
2274 &options,
2275 &orientation_pattern,
2276 false,
2277 ),
2278 );
2279 let seed = forests.local_3d_for_node(
2280 forests.root,
2281 &mut graph,
2282 &cutset,
2283 localizer,
2284 &settings.uv,
2285 &mut Local4dProjectionContext::default(),
2286 )?;
2287 let root_store_marker = symbolica::symbol!("root_store_marker");
2288 let frontier_store_marker = symbolica::symbol!("frontier_store_marker");
2289 let marked_computation = |marker| -> Result<CutComputation> {
2291 Ok(CutComputation {
2292 local_3d: seed.local_3d.map(|_| Ok(Atom::var(marker)))?,
2293 final_integrands: seed.final_integrands.clone(),
2294 })
2295 };
2296
2297 let root_operation = forests.graph[forests.root].clone();
2298 forests
2299 .compute_store
2300 .entry(root_operation)
2301 .or_default()
2302 .cuts
2303 .insert(cutset.clone(), marked_computation(root_store_marker)?);
2304 forests
2305 .compute_store
2306 .entry(forests.graph[non_root_frontier].clone())
2307 .or_default()
2308 .cuts
2309 .insert(cutset.clone(), marked_computation(frontier_store_marker)?);
2310
2311 let root_result = forests.local_3d_for_node(
2312 root_disconnected,
2313 &mut graph,
2314 &cutset,
2315 localizer,
2316 &settings.uv,
2317 &mut Local4dProjectionContext::default(),
2318 )?;
2319 assert!(
2320 root_result
2321 .local_3d
2322 .direct()?
2323 .branches()?
2324 .iter_keys()
2325 .all(|(_, integrands)| integrands
2326 .iter()
2327 .all(|(_, term)| !term.contains_symbol(root_store_marker))),
2328 "an empty dependency frontier must start from the typed root"
2329 );
2330
2331 let frontier_result = forests.local_3d_for_node(
2332 dependent_disconnected,
2333 &mut graph,
2334 &cutset,
2335 localizer,
2336 &settings.uv,
2337 &mut Local4dProjectionContext::default(),
2338 )?;
2339 let replay_states = forests.union_replay_states(dependent_disconnected)?;
2340 let state = replay_states
2341 .iter()
2342 .find(|state| {
2343 state.local_edges.len() == 2
2344 && forests
2345 .normalized_node_label(state.integrated)
2346 .starts_with("{36}:")
2347 })
2348 .expect("the integrated 36 prefix has the two-step C-to-F suffix");
2349 let expected_active = state
2350 .local_edges
2351 .iter()
2352 .enumerate()
2353 .map(|(offset, edge)| {
2354 let step_order = forests.graph[state.integrated].key.op_count() + offset;
2355 let (current, given) = forests.wood.current_given_pair(*edge, step_order);
2356 current.reduced_subgraph(&given)
2357 })
2358 .reduce(|active, reduced| active.union(&reduced))
2359 .expect("the selected replay state has a local suffix");
2360 assert!(
2361 expected_active.empty_intersection(forests.source_spinney(state.integrated).filter())
2362 );
2363 let sector = frontier_result
2364 .local_3d
2365 .direct()?
2366 .sectors()?
2367 .iter()
2368 .find(|sector| sector.active_subgraph == expected_active)
2369 .expect("the replay frontier contains the expected active sector");
2370 assert!(
2371 !sector.projection_paths.is_empty()
2372 && sector.projection_paths.iter().all(|path| {
2373 path.steps.len() == state.local_edges.len()
2374 && path
2375 .steps
2376 .windows(2)
2377 .all(|pair| pair[0].topo_order <= pair[1].topo_order)
2378 }),
2379 "each disconnected sector must retain its own ordered local replay path"
2380 );
2381 assert!(
2382 frontier_result
2383 .local_3d
2384 .direct()?
2385 .branches()?
2386 .iter_keys()
2387 .all(|(_, integrands)| integrands.iter().all(|(_, term)| {
2388 !term.contains_symbol(root_store_marker)
2389 && !term.contains_symbol(frontier_store_marker)
2390 })),
2391 "a union must replay every component path from its typed root"
2392 );
2393 Ok(())
2394 }
2395
2396 #[test]
2397 fn disconnected_soft_union_is_the_product_of_completed_component_atoms() -> Result<()> {
2398 test_initialise().unwrap();
2399 let mut graph: Graph =
2400 include_str!("../../../../tests/resources/graphs/local_ir_dod2_scalar_spectacles.dot")
2401 .into_graph(&crate::utils::load_generic_model("scalars"))?;
2402 let settings = GenerationSettings {
2403 uv: UVgenerationSettings {
2404 generate_integrated: false,
2405 renormalization_prescription: RenormalizationPrescriptionSettings {
2406 log_divergent: ApproximationType::IR,
2407 massive_power_divergent: ApproximationType::IR,
2408 massless_power_divergent: ApproximationType::IR,
2409 ..Default::default()
2410 },
2411 ..Default::default()
2412 },
2413 ..Default::default()
2414 };
2415 let mut forests = Wood::new(CutStructure::empty(&graph), &graph, &settings)?.unfold();
2416 forests.integrate(&graph, crate::utils::vakint()?, &settings.uv)?;
2417
2418 let union = forests
2419 .graph
2420 .iter_nodes()
2421 .find_map(|(node, _, _)| forests.graph.is_disjoint_union(node).then_some(node))
2422 .expect("the spectacles fixture must contain a disconnected UV union");
2423 assert_eq!(
2424 forests.source_spinney(union).renormalization_scheme,
2425 ApproximationType::MUV,
2426 "a disconnected union carries only the neutral aggregate scheme"
2427 );
2428 let components = forests.disconnected_component_nodes(union)?;
2429 assert_eq!(components.len(), 2);
2430 assert!(components.iter().all(|component| {
2431 forests.source_spinney(*component).renormalization_scheme == ApproximationType::IR
2432 }));
2433
2434 let expected =
2435 components
2436 .iter()
2437 .try_fold(Atom::one(), |product, component| -> Result<Atom> {
2438 let completed = forests.recursion_input_4d(*component)?;
2439 assert!(
2440 !completed.atom().is_zero(),
2441 "each disconnected soft component must be completed before the join"
2442 );
2443 Ok(product * completed.atom())
2444 })?;
2445 let operation = &forests.graph[union];
2446 let actual = forests
2447 .compute_store
2448 .require(operation)?
2449 .local_4d(operation)?;
2450 assert_eq!(actual.atom(), &expected);
2451 let provenance = forests.node_export_4d_provenance(union, actual);
2452 assert_eq!(provenance.node.parent_keys.len(), 2);
2453 let UVForestLocalProvenance::FourD {
2454 components: provenance_components,
2455 branches,
2456 } = &provenance.node.local
2457 else {
2458 panic!("a computed 4D union must carry 4D provenance")
2459 };
2460 assert_eq!(provenance_components.len(), 2);
2461 assert!(provenance_components.iter().all(|component| {
2462 component.scheme == ApproximationType::IR
2463 && !component.route_loop_edges.is_empty()
2464 && !component.route_signatures.is_empty()
2465 }));
2466 assert_eq!(branches.len(), 1);
2467 assert_eq!(branches[0].branch, Local4dBranch::Combined);
2468 assert!(branches[0].byte_size > 0);
2469
2470 let cutset = CutSet::empty(graph.n_hedges());
2475 let orientation_pattern = crate::settings::global::OrientationPattern::default();
2476 let options = graph.production_cff_3d_expression_options(&Default::default())?;
2477 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
2478 let contract_edges = graph
2481 .iter_edges_of(
2482 &graph
2483 .tree_edges
2484 .subtract(&graph.initial_state_cut)
2485 .subtract(&graph.external_filter::<SuBitGraph>()),
2486 )
2487 .map(|(_, edge, _)| edge)
2488 .collect::<Vec<_>>();
2489 let mut production = graph.generate_3d_expression_for_integrand(
2490 &contract_edges,
2491 &canonization,
2492 &options,
2493 None,
2494 )?;
2495 assert!(
2496 !production.expression.orientations.is_empty(),
2497 "the spectacles parent CFF has at least one orientation"
2498 );
2499 production.expression.orientations.truncate(1);
2500 let localizer = Localizer::new(
2501 &cutset,
2502 OrientationProjection::exact_expression(
2503 &production,
2504 &options,
2505 &orientation_pattern,
2506 false,
2507 ),
2508 );
2509 let union_3d = forests.local_3d_for_node(
2510 union,
2511 &mut graph,
2512 &cutset,
2513 localizer,
2514 &settings.uv,
2515 &mut Local4dProjectionContext::default(),
2516 )?;
2517 let sectors = union_3d
2518 .local_3d
2519 .direct()?
2520 .sectors()
2521 .expect("a disconnected local 3D union retains its replay sectors");
2522 assert_eq!(sectors.len(), 3);
2523 let combined_sectors = sectors
2524 .iter()
2525 .map(DirectSector::combine)
2526 .collect::<Result<Vec<_>>>()?;
2527 let nonzero_sectors = combined_sectors
2528 .iter()
2529 .filter(|branches| {
2530 branches
2531 .iter_keys()
2532 .any(|(_, integrands)| integrands.iter().any(|(_, atom)| !atom.is_zero()))
2533 })
2534 .collect::<Vec<_>>();
2535 assert_eq!(
2536 nonzero_sectors.len(),
2537 1,
2538 "zero integrated prefixes must leave only the two-local-component cross sector"
2539 );
2540 let cross = nonzero_sectors[0];
2541
2542 let assert_same_branches = |left: &DirectResidueBranches,
2543 right: &DirectResidueBranches,
2544 label: &str|
2545 -> Result<()> {
2546 let left = left.iter_keys().collect::<Vec<_>>();
2547 let right = right.iter_keys().collect::<Vec<_>>();
2548 assert_eq!(
2549 left.len(),
2550 right.len(),
2551 "{label} differs in residue-map count"
2552 );
2553 for ((left_key, left), (right_key, right)) in left.into_iter().zip(right) {
2554 assert_eq!(
2555 left_key, right_key,
2556 "{label} changed the complete residue map"
2557 );
2558 let left = left.resolved()?;
2561 let right = right.resolved()?;
2562 left.checked_zip(&right, |residue, left, right| {
2563 let difference = (left - right).together().cancel();
2566 assert!(
2567 difference.is_zero(),
2568 "{label} differs in residue {residue:?}: {difference}"
2569 );
2570 Ok(Atom::Zero)
2571 })?;
2572 }
2573 Ok(())
2574 };
2575 assert_same_branches(
2576 &union_3d.local_3d.direct()?.branches()?,
2577 cross,
2578 "the disconnected aggregate and its cross sector",
2579 )?;
2580
2581 let component_edges = components
2582 .iter()
2583 .map(|component| {
2584 let (parent, edge) = forests
2585 .graph
2586 .unique_parent(*component)
2587 .expect("a primitive disconnected component has one root edge");
2588 assert!(
2589 forests.graph[parent].key.is_empty(),
2590 "the spectacles components must both be primitive"
2591 );
2592 edge
2593 })
2594 .collect::<Vec<_>>();
2595 let mut replay = |edges: &[EdgeIndex]| -> Result<Direct3dCts> {
2596 let mut local = Direct3dCts::root(&graph, localizer)?;
2597 for (order, edge) in edges.iter().enumerate() {
2598 let (current, given) = forests.wood.current_given_pair(*edge, order);
2599 local = Direct3dApproximation::new(localizer, &mut graph, &settings.uv)
2600 .run_local(&local, ¤t, &given, ¤t, &given)?;
2601 }
2602 assert!(local.branches()?.iter_keys().all(|(_, integrands)| {
2603 integrands
2604 .iter()
2605 .all(|(_, atom)| !atom.contains_symbol(GS.rescale))
2606 }));
2607 Ok(local)
2608 };
2609 let forward = replay(&component_edges)?;
2610 let reverse = replay(&component_edges.iter().rev().copied().collect::<Vec<_>>())?;
2611 assert_same_branches(
2612 &forward.branches()?,
2613 cross,
2614 "forward component-local replay and the produced cross sector",
2615 )?;
2616 assert_same_branches(
2617 &reverse.branches()?,
2618 cross,
2619 "reverse component-local replay and the produced cross sector",
2620 )?;
2621 Ok(())
2622 }
2623
2624 #[test]
2625 fn heterogeneous_terminal_projects_components_before_multiplying() -> Result<()> {
2626 test_initialise().unwrap();
2627 let graph: Graph = dot!(
2628 digraph G {
2629 num = "1";
2630 projector = "1";
2631 overall_factor = "1";
2632 edge [particle = "scalar_1"];
2633 node [num = "1"];
2634 v1 -> v2;
2635 v1 -> v1;
2636 v2 -> v3;
2637 v2 -> v3;
2638 },
2639 "scalars"
2640 )?;
2641 let settings = GenerationSettings {
2642 uv: UVgenerationSettings {
2643 softct: false,
2644 renormalization_prescription: RenormalizationPrescriptionSettings {
2645 log_divergent: ApproximationType::MUV,
2646 massive_power_divergent: ApproximationType::PolePart,
2647 massless_power_divergent: ApproximationType::PolePart,
2648 ..Default::default()
2649 },
2650 ..Default::default()
2651 },
2652 ..Default::default()
2653 };
2654 let mut forests = Wood::new(CutStructure::empty(&graph), &graph, &settings)?.unfold();
2655 forests.integrate(&graph, crate::utils::vakint()?, &settings.uv)?;
2656
2657 let terminals = forests
2658 .graph
2659 .iter_nodes()
2660 .filter_map(|(node, mut crown, _)| {
2661 (!crown.any(|hedge| forests.graph.flow(hedge).is_source())).then_some(node)
2662 })
2663 .collect::<Vec<_>>();
2664 assert_eq!(terminals.len(), 1);
2665 let terminal = terminals[0];
2666 assert!(forests.graph.is_disjoint_union(terminal));
2667
2668 let components = forests.disconnected_component_nodes(terminal)?;
2669 assert_eq!(components.len(), 2);
2670 let schemes = components
2671 .iter()
2672 .map(|component| forests.source_spinney(*component).renormalization_scheme)
2673 .collect::<Vec<_>>();
2674 assert!(schemes.contains(&ApproximationType::MUV));
2675 assert!(schemes.contains(&ApproximationType::PolePart));
2676
2677 let expected =
2678 components
2679 .iter()
2680 .try_fold(Atom::one(), |product, &component| -> Result<Atom> {
2681 let component_key = &forests.graph[component];
2682 let integrated = forests
2683 .compute_store
2684 .require(component_key)?
2685 .integrated(component_key)?;
2686 let projection = match forests.source_spinney(component).renormalization_scheme
2687 {
2688 ApproximationType::MUV => integrated.physical_finite_counterterm_atom(),
2689 ApproximationType::PolePart => integrated.physical_pole_atom(),
2690 scheme => panic!("unexpected component scheme {scheme}"),
2691 };
2692 Ok(product * projection)
2693 })?;
2694 let terminal_key = &forests.graph[terminal];
2695 let aggregate = forests
2696 .compute_store
2697 .require(terminal_key)?
2698 .integrated(terminal_key)?;
2699 assert_ne!(
2700 expected.expand(),
2701 aggregate.physical_finite_counterterm_atom().expand()
2702 );
2703 assert_ne!(expected.expand(), aggregate.physical_pole_atom().expand());
2704
2705 let wild = Atom::var(W_.x___);
2706 let replacements =
2707 graph.integrand_replacement(&graph.full_filter(), &graph.loop_momentum_basis, &[wild]);
2708 let expected = expected
2709 .simplify_color()
2710 .expand_num()
2711 .to_dots()
2712 .replace_multiple(&replacements)
2713 .replace(GS.m_uv_expansion)
2714 .with(GS.m_uv_vacuum);
2715 let actual = forests
2716 .renormalization_part_of_ends(&graph, &settings.uv)?
2717 .expression;
2718 assert_eq!(actual.expand(), expected.expand());
2719
2720 Ok(())
2721 }
2722
2723 #[test]
2724 fn triple_tadpole() -> Result<()> {
2725 test_initialise().unwrap();
2726 let dumbell: Graph = dot!(
2727 digraph G{
2728 edge [particle="scalar_1"];
2729 v1 -> v2;
2730 v2 -> v3;
2731 v3 -> v3;
2732 v2 -> v2;
2733 v1 -> v1;
2734 },"scalars"
2735 )?;
2736
2737 let spinneys: Vec<_> = dumbell
2738 .spinneys(&dumbell.full_filter())
2739 .into_iter()
2740 .filter_map(|a| Spinney::new(a, &dumbell, &dumbell.loop_momentum_basis).transpose())
2741 .collect::<Result<_>>()?;
2742 let f = Wood::new(
2743 CutStructure::empty(&dumbell),
2744 &dumbell,
2745 &GenerationSettings::default(),
2746 )?;
2747
2748 println!("{}", f);
2749
2750 insta::assert_snapshot!(
2751 f.graph.n_nodes(),
2752 @"8",
2753 );
2755
2756 let three_component_target = f
2757 .graph
2758 .iter_nodes()
2759 .find_map(|(node, _, spinney)| (spinney.n_components() == 3).then_some(node))
2760 .expect("triple tadpole should contain its fully factorized target");
2761 let target_filter = f.graph[three_component_target].filter().clone();
2762 let components = dumbell
2763 .as_ref()
2764 .connected_components(&target_filter)
2765 .into_iter()
2766 .collect::<BTreeSet<_>>();
2767 assert_eq!(components.len(), 3);
2768 let incoming = f
2769 .graph
2770 .iter_crown(three_component_target)
2771 .filter(|hedge| f.graph.flow(*hedge) == Flow::Sink)
2772 .map(|hedge| f.graph[f.graph[&hedge]].clone())
2773 .collect::<BTreeSet<_>>();
2774 assert_eq!(incoming, components);
2775 assert!(
2776 !incoming.contains(&target_filter),
2777 "a fully supplied product must not retain a duplicate atomic full-target transition"
2778 );
2779
2780 for (_, _, d) in f.graph.iter_nodes() {
2781 println!(
2782 "//Node {}: \n{}",
2783 d.subgraph.string_label(),
2784 dumbell.dot(&d.subgraph)
2785 );
2786 }
2787 let _ff = OldWood::from_spinneys(spinneys, &dumbell); let f = f.unfold();
2792 println!("{}", f);
2793 insta::assert_snapshot!(
2794 f.graph.n_nodes(),
2795 @"8");
2796
2797 let three_component_union = f
2798 .graph
2799 .iter_nodes()
2800 .find_map(|(node, _, operation)| {
2801 (operation.key.op_count() == 3
2802 && f.wood
2803 .join_factors(f.graph.source_node(node))
2804 .is_some_and(|factors| factors.len() == 3))
2805 .then_some(node)
2806 })
2807 .expect("triple tadpole should contain a three-component union");
2808 assert_eq!(
2809 f.disconnected_component_nodes(three_component_union)?.len(),
2810 3
2811 );
2812 let replay_states = f.union_replay_states(three_component_union)?;
2813 assert_eq!(replay_states.len(), 8);
2814 assert_eq!(
2815 replay_states
2816 .iter()
2817 .filter(|state| !state.local_edges.is_empty())
2818 .count(),
2819 7
2820 );
2821
2822 Ok(())
2823 }
2824
2825 #[test]
2826 fn saclay() -> Result<()> {
2827 test_initialise().unwrap();
2828 let dt: Graph = dot!(digraph GL16{
2829
2830 num = "spenso::g(spenso::coad(8,gammalooprs::hedge(8)),spenso::coad(8,gammalooprs::hedge(11)))"
2831 ext [style=invis];
2832 ext -> 0 [dir=none id=0 particle="g"];
2833 2 -> ext [dir=none id=1 particle="g"];
2834 0 -> 1 -> 2->3->0 [ particle="d"];
2835 1 ->3 [particle = "g"]
2836 })?;
2837
2838 let f = Wood::new(
2839 CutStructure::empty(&dt),
2840 &dt,
2841 &GenerationSettings::default(),
2842 )?;
2843
2844 println!("{}", dt.dot_serialize(&DotExportSettings::default()));
2845
2846 insta::assert_snapshot!(
2847 f.graph.n_nodes(),
2848 @"5",
2849 );
2851
2852 let f = f.unfold();
2853 let structured_dot = f.dot_serialize();
2854 assert!(structured_dot.contains(r#"label="K[#expr S_44⊛0]""#));
2855 assert!(!structured_dot.contains("#T("));
2856 println!("{structured_dot}");
2857 insta::assert_snapshot!(
2858 f.graph.n_nodes(),
2859 @"8");
2860
2861 Ok(())
2862 }
2863
2864 #[test]
2865 fn dumbells() -> Result<()> {
2866 test_initialise().unwrap();
2867 let dumbell: Graph = dot!(
2868 digraph G{
2869 edge [particle="scalar_1"];
2870 v1 -> v2;
2871 v2 -> v2;
2872 v1 -> v1;
2873 },"scalars"
2874 )?;
2875
2876 let spinneys: Vec<_> = dumbell
2877 .spinneys(&dumbell.full_filter())
2878 .into_iter()
2879 .filter_map(|a| Spinney::new(a, &dumbell, &dumbell.loop_momentum_basis).transpose())
2880 .collect::<Result<_>>()?;
2881 let f = Wood::new(
2882 CutStructure::empty(&dumbell),
2883 &dumbell,
2884 &GenerationSettings::default(),
2885 )?;
2886
2887 println!("{}", f);
2888
2889 insta::assert_snapshot!(
2890 f.graph.n_nodes(),
2891 @"4",
2892 );
2894
2895 for (_, _, d) in f.graph.iter_nodes() {
2896 println!(
2897 "//Node {}: \n{}",
2898 d.subgraph.string_label(),
2899 dumbell.dot(&d.subgraph)
2900 );
2901 }
2902 let _ff = OldWood::from_spinneys(spinneys, &dumbell); let f = f.unfold();
2907 println!("{}", f);
2908 insta::assert_snapshot!(
2909 f.graph.n_nodes(),
2910 @"4");
2911
2912 Ok(())
2913 }
2914
2915 #[test]
2916 fn bugblatter() -> Result<()> {
2917 test_initialise().unwrap();
2918
2919 match dot!(
2920 digraph G{
2921 A1 -> A2 [particle="t"];
2922 A2 -> A3 [particle="t"];
2923 A3 -> A1 [particle="t"];
2924 B1 -> B2 [particle="t"];
2925 B2 -> B3 [particle="t"];
2926 B3 -> B1 [particle="t"];
2927 A1 -> B1 [particle="a"];
2928 A2 -> B2 [particle="a"];
2929 A3 -> B3 [particle="a"];
2930 },"sm"
2931 ) {
2932 Ok(g) => {
2933 let g: Graph = g;
2934 let spinneys: Vec<_> = g
2935 .spinneys(&g.full_filter())
2936 .into_iter()
2937 .filter_map(|a| Spinney::new(a, &g, &g.loop_momentum_basis).transpose())
2938 .collect::<Result<_>>()?;
2939 let f = Wood::new(CutStructure::empty(&g), &g, &GenerationSettings::default())?;
2940
2941 println!("{}", f);
2942
2943 assert_eq!(
2944 20,
2945 f.graph.n_nodes(),
2946 "Wood does not have correct number of spinneys: \n{}",
2947 f
2948 );
2949
2950 for (_, _, d) in f.graph.iter_nodes() {
2951 println!(
2952 "//Node {}: \n{}",
2953 d.subgraph.string_label(),
2954 g.dot_lmb_of(&d.subgraph, &d.lmb)
2955 );
2956 }
2957 let _ff = OldWood::from_spinneys(spinneys, &g); let f = f.unfold();
2962 f.debug_walk();
2963 println!("{}", f);
2964 assert_eq!(
2965 152,
2966 f.graph.n_nodes(),
2967 "Forest unfolds into the wrong number of terms :\n{}",
2968 f
2969 );
2970
2971 }
2973 Err(e) => {
2974 eprintln!("{}", e);
2975 }
2976 }
2977
2978 Ok(())
2986 }
2987
2988 #[test]
2989 fn mercedes() -> Result<()> {
2990 test_initialise().unwrap();
2991
2992 let mercedes: Graph = dot!(
2993 digraph G{
2994 edge [particle="scalar_1"];
2995 v1 -> v2;
2996 v2 -> v3;
2997 v3 -> v1;
2998 v1 -> v4;
2999 v2 -> v4;
3000 v3 -> v4;
3001 },"scalars"
3002 )?;
3003
3004 let f = Wood::new(
3005 CutStructure::empty(&mercedes),
3006 &mercedes,
3007 &GenerationSettings::default(),
3008 )?;
3009 println!("{}", f);
3010 insta::assert_snapshot!(
3011 f.graph.n_nodes(),
3012 @"2",
3013 );
3014 let f = f.unfold();
3015 println!("{}", f);
3016 insta::assert_snapshot!(
3017 f.graph.n_nodes(),
3018 @"2",
3019 );
3020
3021 Ok(())
3022 }
3023
3024 #[test]
3025 fn sunrise() -> Result<()> {
3026 test_initialise().unwrap();
3027
3028 let sunrise: Graph = dot!( digraph sunrise{
3029 node [num = "1"]
3030 edge [particle=scalar_1]
3031 e [style=invis]
3032 e -> A:0 [ id=3 ]
3033 B:1 -> e [ id=4 ]
3034 A -> B [ id=0 ]
3035 A -> B [ id=1 ]
3036 A -> B [ id=2 ]
3037 },"scalars")?;
3038 let f = Wood::new(
3040 CutStructure::empty(&sunrise),
3041 &sunrise,
3042 &GenerationSettings::default(),
3043 )?;
3044 println!("{}", f);
3045 insta::assert_snapshot!(
3046 f.graph.n_nodes(),
3047 @"5",
3048 );
3049 let f = f.unfold();
3050 println!("{}", f);
3051 insta::assert_snapshot!(
3052 f.graph.n_nodes(),
3053 @"8",
3054 );
3055
3056 Ok(())
3057 }
3058 #[test]
3059 fn dotted_sunrise() -> Result<()> {
3060 test_initialise().unwrap();
3061
3062 let sunrise: Graph = dot!( digraph sunrise{
3063 edge [particle=scalar_1]
3064 e [style=invis]
3065 e -> A:0 [ id=3]
3066 B:1 -> e [ id=4]
3067
3068 A -> C [ id=0]
3069 C -> e
3070 C -> B
3071 A -> B [ id=1]
3072 A -> B [ id=2]
3073 },"scalars")?;
3074 let f = Wood::new(
3076 CutStructure::empty(&sunrise),
3077 &sunrise,
3078 &GenerationSettings::default(),
3079 )?;
3080 println!("{}", f);
3081 insta::assert_snapshot!(
3082 f.graph.n_nodes(),
3083 @"3",
3084 );
3085 let f = f.unfold();
3086 println!("{}", f);
3087 insta::assert_snapshot!(
3088 f.graph.n_nodes(),
3089 @"4",
3090 );
3091
3092 Ok(())
3093 }
3094
3095 #[test]
3096 fn dotted() -> Result<()> {
3097 test_initialise().unwrap();
3098
3099 let sunrise: Graph = dot!( digraph sunrise{
3100 edge [particle=scalar_1]
3101 e [style=invis]
3102 e -> A:0 [ id=4]
3103 B:1 -> e [ id=5]
3104 C:2 -> e [ id=6]
3105
3106 A -> B [ id=0]
3107 B -> C [ id=1]
3108 C -> A [ id=2]
3109 B -> C [ id=3]
3110
3111 },"scalars")?;
3112 let f = Wood::new(
3114 CutStructure::empty(&sunrise),
3115 &sunrise,
3116 &GenerationSettings::default(),
3117 )?;
3118 println!("{}", f);
3119 insta::assert_snapshot!(
3120 f.graph.n_nodes(),
3121 @"3",
3122 );
3123 let f = f.unfold();
3124 println!("{}", f);
3125 insta::assert_snapshot!(
3126 f.graph.n_nodes(),
3127 @"4",
3128 );
3129
3130 Ok(())
3131 }
3132
3133 #[test]
3134 fn spectacles() -> Result<()> {
3135 use crate::graph::feynman_graph::FeynmanGraph;
3136
3137 test_initialise().unwrap();
3138
3139 let mut spectacles: Graph = dot!(
3140 digraph G{
3141 edge [particle="scalar_1"];
3142 v1 -> v2;
3143 v1 -> v2;
3144
3145 v3 -> v4;
3146 v3 -> v4;
3147
3148 v2 -> v3;
3149 v1 -> v4;
3150 },"scalars"
3151 )?;
3152
3153 let settings = GenerationSettings::default();
3155 let cut_structure = CutStructure::empty(&spectacles);
3156 let cutset = cut_structure
3157 .cuts
3158 .first()
3159 .expect("empty cut structure has one cut")
3160 .clone();
3161 let f = Wood::new(cut_structure, &spectacles, &settings)?;
3162 println!("{}", f);
3163 let mut f = f.unfold();
3164 println!("{}", f);
3165
3166 let (union, edge) = f
3167 .graph
3168 .iter_nodes()
3169 .find_map(|(child, _, _)| {
3170 let (parent, edge) = f.graph.unique_parent(child)?;
3171 (!f.graph.is_disjoint_union(child) && f.graph.is_disjoint_union(parent))
3172 .then_some((parent, edge))
3173 })
3174 .expect("spectacles has a connected child above its disconnected union");
3175 let operations = f
3176 .graph
3177 .iter_nodes()
3178 .map(|(_, _, operation)| operation.clone())
3179 .collect::<Vec<_>>();
3180 for operation in operations {
3182 f.compute_store.entry(operation).or_default().integrated = Some(IntegratedCts::root());
3183 }
3184 let orientation_pattern = crate::settings::global::OrientationPattern::default();
3185 let options = spectacles.denominator_only_cff_3d_expression_options();
3188 let canonization = spectacles.get_esurface_canonization(&spectacles.loop_momentum_basis);
3189 let contract_edges = spectacles.paired_edges(
3190 &spectacles
3191 .tree_edges
3192 .subtract(&spectacles.initial_state_cut),
3193 );
3194 let production = spectacles.generate_3d_expression_for_integrand(
3195 &contract_edges,
3196 &canonization,
3197 &options,
3198 None,
3199 )?;
3200 assert!(!production.expression.orientations.is_empty());
3201 let localizer = Localizer::new(
3202 &cutset,
3203 OrientationProjection::exact_expression(
3204 &production,
3205 &options,
3206 &orientation_pattern,
3207 false,
3208 ),
3209 );
3210 let replay_states = f
3211 .union_replay_states(union)?
3212 .into_iter()
3213 .filter(|state| !state.local_edges.is_empty())
3214 .collect::<Vec<_>>();
3215 let union_active = replay_states
3216 .iter()
3217 .map(|state| {
3218 state
3219 .local_edges
3220 .iter()
3221 .enumerate()
3222 .map(|(offset, edge)| {
3223 let step_order = f.graph[state.integrated].key.op_count() + offset;
3224 let (current, given) = f.wood.current_given_pair(*edge, step_order);
3225 current.reduced_subgraph(&given)
3226 })
3227 .reduce(|active, reduced| active.union(&reduced))
3228 .expect("a proper union replay state has a local suffix")
3229 })
3230 .collect::<Vec<_>>();
3231 let computed = f.local_3d_for_node(
3232 union,
3233 &mut spectacles,
3234 &cutset,
3235 localizer,
3236 &settings.uv,
3237 &mut Local4dProjectionContext::default(),
3238 )?;
3239 let sectors = computed.local_3d.direct()?.sectors()?;
3240 assert_eq!(
3241 sectors.len(),
3242 replay_states.len(),
3243 "every proper integrated prefix must produce one disconnected replay sector"
3244 );
3245 assert_eq!(
3246 sectors
3247 .iter()
3248 .map(|sector| sector.active_subgraph.clone())
3249 .collect::<Vec<_>>(),
3250 union_active,
3251 "the completed union must retain the active component support of every replay path"
3252 );
3253 assert!(
3254 computed.final_integrands.iter().next().is_some(),
3255 "the successfully replayed disconnected union must reach final-integrand construction"
3256 );
3257 let union_local = Direct3dCts::from_sectors(
3258 union_active
3259 .iter()
3260 .cloned()
3261 .map(|active_subgraph| {
3262 let coordinate_frames = spectacles
3263 .as_ref()
3264 .connected_components(&active_subgraph)
3265 .into_iter()
3266 .filter(|component| spectacles.n_loops(component) != 0)
3267 .map(|component| DirectCoordinateFrame {
3268 lmb: spectacles.lmb_of(&component),
3269 active_subgraph: component,
3270 })
3271 .collect();
3272 Ok(DirectSector {
3273 active_subgraph,
3274 coordinate_frames,
3275 active: DirectResidueBranches::production(
3276 OrientationID(0),
3277 Integrands::root(),
3278 )?,
3279 frozen_integrands: Integrands::root(),
3280 projection_paths: Vec::new(),
3281 })
3282 })
3283 .collect::<Result<_>>()?,
3284 )?;
3285
3286 let step_order = f.graph[union].key.op_count();
3287 let (current, given) = f.wood.current_given_pair(edge, step_order);
3288 let reduced = current.reduced_subgraph(&given);
3289 let numerator = spectacles
3290 .numerator(&reduced, given.subgraph())
3291 .get_single_atom()
3292 .expect("the scalar reduced numerator is available");
3293 assert!(!numerator.contains_symbol(GS.emr_mom));
3294 let expected = union_local
3299 .branches()?
3300 .materialize(false)?
3301 .resolved()?
3302 .map(|atom| -(atom * &numerator));
3303 let given_internal =
3304 InternalSubGraph::try_new(given.subgraph().clone(), spectacles.as_ref())
3305 .expect("the connected child has an internal given prefix");
3306 let quotient_rank = spectacles
3307 .shrunken_sub_lmb(
3308 current.subgraph(),
3309 &given_internal,
3310 spectacles.dummy_stripped_external_flows_of(current.subgraph()),
3311 None,
3312 )?
3313 .loop_edges
3314 .len();
3315 let expected_frames = union_local
3316 .sectors()?
3317 .iter()
3318 .map(|sector| {
3319 let retained_region = sector.active_subgraph.intersection(given.subgraph());
3320 let mut retained = sector
3321 .coordinate_frames
3322 .iter()
3323 .filter(|frame| retained_region.includes(&frame.active_subgraph))
3324 .flat_map(|frame| frame.lmb.loop_edges.iter().copied())
3325 .collect::<Vec<_>>();
3326 retained.sort();
3327 retained.dedup();
3328 let expected_rank = retained.len() + quotient_rank;
3329 (retained, expected_rank)
3330 })
3331 .collect::<Vec<_>>();
3332 let external: SuBitGraph = spectacles.external_filter();
3333 let full_internal = spectacles.full_filter().subtract(&external);
3334 let canonical_route = if current.subgraph() == &full_internal
3335 && spectacles.n_loops(&full_internal) == spectacles.loop_momentum_basis.loop_edges.len()
3336 {
3337 spectacles.loop_momentum_basis.clone()
3338 } else {
3339 spectacles.try_compatible_sub_lmb(
3340 current.subgraph(),
3341 spectacles.dummy_less_full_crown(current.subgraph()),
3342 &spectacles.loop_momentum_basis,
3343 )?
3344 };
3345 let expected_active = union_active
3346 .iter()
3347 .map(|active| active.union(&reduced))
3348 .collect::<Vec<_>>();
3349 let child_local = Direct3dApproximation::new(localizer, &mut spectacles, &settings.uv)
3350 .run(
3351 &union_local,
3352 &IntegratedCts::root(),
3353 ¤t,
3354 &given,
3355 ¤t,
3356 &given,
3357 )?;
3358 let actual = child_local.branches()?.materialize(false)?.resolved()?;
3359 assert_eq!(
3360 actual.map(|atom| atom.collect_factors()),
3361 expected.map(|atom| atom.collect_factors()),
3362 "the connected child must retain every complete source value across its enclosing Taylor operation",
3363 );
3364 let child_sectors = child_local.sectors()?;
3365 let child_active = child_sectors
3366 .iter()
3367 .map(|sector| sector.active_subgraph.clone())
3368 .collect::<Vec<_>>();
3369 assert_eq!(child_active.len(), 3);
3370 assert_eq!(child_active, expected_active);
3371 for (sector, (retained, expected_rank)) in child_sectors.iter().zip(expected_frames.iter())
3372 {
3373 assert_eq!(
3374 sector.coordinate_frames.len(),
3375 1,
3376 "one connected enclosing Taylor operation must merge every contained child frame"
3377 );
3378 let child_carriers = sector
3379 .coordinate_frames
3380 .iter()
3381 .flat_map(|frame| frame.lmb.loop_edges.iter())
3382 .copied()
3383 .collect::<BTreeSet<_>>();
3384 assert!(
3385 retained.iter().all(|edge| child_carriers.contains(edge)),
3386 "the connected child must retain every carrier from the contained union frames"
3387 );
3388 assert!(
3389 child_carriers.iter().all(|edge| {
3390 spectacles.loop_momentum_basis.edge_signatures[*edge]
3391 .external
3392 .iter()
3393 .all(|sign| sign.is_zero())
3394 }),
3395 "the connected child route must be homogeneous in graph-external momenta"
3396 );
3397 assert_eq!(
3398 sector
3399 .coordinate_frames
3400 .iter()
3401 .map(|frame| frame.lmb.loop_edges.len())
3402 .sum::<usize>(),
3403 *expected_rank,
3404 "the connected child rank is the retained prefix rank plus its quotient rank"
3405 );
3406 let canonical_carriers = canonical_route
3407 .loop_edges
3408 .iter()
3409 .copied()
3410 .collect::<BTreeSet<_>>();
3411 let canonical_is_admissible = retained
3412 .iter()
3413 .all(|edge| canonical_carriers.contains(edge))
3414 && canonical_carriers.len() == *expected_rank
3415 && canonical_carriers.iter().all(|edge| {
3416 spectacles.loop_momentum_basis.edge_signatures[*edge]
3417 .external
3418 .iter()
3419 .all(|sign| sign.is_zero())
3420 });
3421 if canonical_is_admissible {
3422 assert_eq!(
3423 child_carriers, canonical_carriers,
3424 "an admissible graph-canonical route must win before equivalent fallbacks"
3425 );
3426 }
3427 }
3428
3429 Ok(())
3430 }
3431
3432 #[test]
3433 fn collective_two_component_region_requires_independently_divergent_factors() -> Result<()> {
3434 test_initialise().unwrap();
3435
3436 let graph: Graph = dot!(
3441 digraph G {
3442 node [num = "1"];
3443 edge [particle = "scalar_1"];
3444 a0 -> a1 [id = 0, dod = "-1", num = "gammalooprs::Q(0,spenso::mink(4,0))"];
3445 a0 -> a1 [id = 1, dod = "-1", num = "gammalooprs::Q(1,spenso::mink(4,0))"];
3446 a1 -> b0 [id = 2];
3447 b0 -> b1 [id = 3];
3448 b1 -> b2 [id = 4];
3449 b2 -> b0 [id = 5];
3450 },
3451 "scalars"
3452 )?;
3453
3454 let settings = GenerationSettings {
3455 uv: UVgenerationSettings {
3456 generate_integrated: false,
3457 ..Default::default()
3458 },
3459 ..Default::default()
3460 };
3461 let wood = Wood::new(CutStructure::empty(&graph), &graph, &settings)?;
3462 let collective = graph
3463 .spinneys(&graph.full_filter())
3464 .into_iter()
3465 .find(|spinney| graph.as_ref().connected_components(spinney).len() == 2)
3466 .expect("the raw cycle inventory contains the collective region");
3467 let collective_filter = collective.filter;
3468 let components = graph
3469 .as_ref()
3470 .connected_components(&collective_filter)
3471 .into_iter()
3472 .collect::<BTreeSet<_>>();
3473 assert_eq!(components.len(), 2);
3474 assert_eq!(graph.compute_dod(&collective_filter), 0);
3475 assert_eq!(
3476 components
3477 .iter()
3478 .map(|component| graph.compute_dod(component))
3479 .sorted()
3480 .collect::<Vec<_>>(),
3481 vec![-2, 2]
3482 );
3483
3484 let root = OwnedForestNode {
3485 spinney: Spinney::empty(&graph),
3486 topo_order: 0,
3487 };
3488 let divergent_filter = components
3489 .iter()
3490 .find(|component| graph.compute_dod(*component) >= 0)
3491 .expect("the bubble is the independently divergent component")
3492 .clone();
3493 let divergent = OwnedForestNode {
3494 spinney: Spinney::new(
3495 InternalSubGraph::cleaned_filter_optimist(divergent_filter, graph.as_ref()),
3496 &graph,
3497 &graph.loop_momentum_basis,
3498 )?
3499 .expect("the divergent component has a compatible source chart"),
3500 topo_order: 0,
3501 };
3502 let collective_source = OwnedForestNode {
3503 spinney: Spinney::new(
3504 InternalSubGraph::cleaned_filter_optimist(
3505 collective_filter.clone(),
3506 graph.as_ref(),
3507 ),
3508 &graph,
3509 &graph.loop_momentum_basis,
3510 )?
3511 .expect("the collective region has a compatible source chart"),
3512 topo_order: 1,
3513 };
3514 let ctx = UVCtx::new(&graph, &settings.uv);
3515 let input = Full4dCts::from_factorized_local(&Local4dCts::root());
3516 let prefix = local_4d::uv_limit(&input, &ctx, &divergent, &root, &divergent, &root)?;
3517 let atomic = local_4d::uv_limit(
3521 &input,
3522 &ctx,
3523 &collective_source,
3524 &root,
3525 &collective_source,
3526 &root,
3527 )?;
3528 let nested = local_4d::uv_limit(
3529 &Full4dCts::from_factorized_local(&prefix),
3530 &ctx,
3531 &collective_source,
3532 &divergent,
3533 &collective_source,
3534 &divergent,
3535 )?;
3536 let expected = atomic.atom() + nested.atom();
3537 let mut forests = wood.unfold();
3538 forests.integrate(&graph, crate::utils::vakint()?, &settings.uv)?;
3539 let actual = forests
3540 .graph
3541 .iter_nodes()
3542 .filter(|(_, _, operation)| operation.covers().as_ref() == Some(&collective_filter))
3543 .try_fold(Atom::Zero, |sum, (_, _, operation)| -> Result<_> {
3544 Ok(sum
3545 + forests
3546 .compute_store
3547 .require(operation)?
3548 .local_4d(operation)?
3549 .atom())
3550 })?;
3551 let replacements = graph.integrand_replacement(
3555 &graph.full_filter(),
3556 &graph.loop_momentum_basis,
3557 &[Atom::var(W_.x___)],
3558 );
3559 let [actual, expected] = [actual, expected].map(|expression| {
3560 GS.erase_uv_momentum_provenance(&expression)
3561 .replace(GS.den(W_.a_, W_.mom_, W_.mass_, W_.prop_))
3562 .with(W_.prop_)
3563 .replace_multiple(&replacements)
3564 .normalize_dots()
3565 .collect_factors()
3566 });
3567 assert!(
3568 !atomic.atom().is_zero(),
3569 "the atomic Taylor term must be nonzero"
3570 );
3571 assert!((actual - expected).collect_factors().is_zero());
3572
3573 assert!(
3574 forests
3575 .wood
3576 .graph
3577 .iter_nodes()
3578 .all(|(_, _, spinney)| { spinney.filter() != &collective_filter }),
3579 "a convergent connected factor cannot enter a disconnected UV spinney"
3580 );
3581 let convergent = components
3582 .iter()
3583 .find(|component| graph.compute_dod(*component) < 0)
3584 .expect("the collective region has one convergent component");
3585 let divergent = components
3586 .iter()
3587 .find(|component| graph.compute_dod(*component) >= 0)
3588 .expect("the collective region has one independently divergent component");
3589 assert!(
3590 forests
3591 .wood
3592 .graph
3593 .iter_nodes()
3594 .all(|(_, _, spinney)| spinney.filter() != convergent)
3595 );
3596 assert!(
3597 forests
3598 .wood
3599 .graph
3600 .iter_nodes()
3601 .any(|(_, _, spinney)| spinney.filter() == divergent)
3602 );
3603
3604 assert!(
3605 forests.graph.iter_nodes().all(|(_, _, operation)| {
3606 operation.covers().as_ref() != Some(&collective_filter)
3607 }),
3608 "neither atomic nor nested collective counterterms may bypass factor selection"
3609 );
3610 let mut cut = CutSet::empty(graph.n_hedges());
3611 cut.union = convergent.clone();
3612 assert!(
3613 forests.graph.iter_nodes().any(|(_, _, operation)| {
3614 operation.covers().as_ref() == Some(divergent) && operation.is_compatible_with(&cut)
3615 }),
3616 "a disjoint independently divergent prefix remains cut-compatible"
3617 );
3618 cut.union = divergent.clone();
3619 assert!(
3620 forests
3621 .graph
3622 .iter_nodes()
3623 .filter(|(_, _, operation)| { operation.covers().as_ref() == Some(divergent) })
3624 .all(|(_, _, operation)| !operation.is_compatible_with(&cut)),
3625 "cutting the retained divergent factor must exclude its counterterms"
3626 );
3627
3628 Ok(())
3629 }
3630
3631 #[test]
3632 fn collective_three_component_region_keeps_only_its_divergent_join() -> Result<()> {
3633 test_initialise().unwrap();
3634
3635 let graph: Graph = dot!(
3644 digraph G {
3645 node [num = "1"];
3646 edge [particle = "scalar_1"];
3647 a0 -> a1 [id = 0, dod = "-1", num = "gammalooprs::Q(0,spenso::mink(4,0))"];
3648 a0 -> a1 [id = 1, dod = "-1", num = "gammalooprs::Q(1,spenso::mink(4,0))"];
3649 a1 -> b0 [id = 2];
3650 b0 -> b1 [id = 3, dod = "-1", num = "gammalooprs::Q(3,spenso::mink(4,1))"];
3651 b0 -> b1 [id = 4, dod = "-1", num = "gammalooprs::Q(4,spenso::mink(4,1))"];
3652 b1 -> c0 [id = 5];
3653 c0 -> c1 [id = 6];
3654 c1 -> c2 [id = 7];
3655 c2 -> c0 [id = 8];
3656 },
3657 "scalars"
3658 )?;
3659
3660 let settings = GenerationSettings {
3661 uv: UVgenerationSettings {
3662 generate_integrated: false,
3663 ..Default::default()
3664 },
3665 ..Default::default()
3666 };
3667 let wood = Wood::new(CutStructure::empty(&graph), &graph, &settings)?;
3668 let collective = graph
3669 .spinneys(&graph.full_filter())
3670 .into_iter()
3671 .find(|spinney| graph.as_ref().connected_components(spinney).len() == 3)
3672 .expect("the raw cycle inventory contains the collective region");
3673 let collective_filter = collective.filter;
3674 let components = graph
3675 .as_ref()
3676 .connected_components(&collective_filter)
3677 .into_iter()
3678 .collect::<BTreeSet<_>>();
3679 assert_eq!(components.len(), 3);
3680 assert_eq!(graph.compute_dod(&collective_filter), 2);
3681 assert_eq!(
3682 components
3683 .iter()
3684 .map(|component| graph.compute_dod(component))
3685 .sorted()
3686 .collect::<Vec<_>>(),
3687 vec![-2, 2, 2]
3688 );
3689 assert!(
3690 wood.graph.iter_nodes().all(|(_, _, spinney)| {
3691 graph
3692 .as_ref()
3693 .connected_components(spinney.filter())
3694 .iter()
3695 .all(|component| graph.compute_dod(component) >= 0)
3696 }),
3697 "a convergent connected factor cannot enter a disconnected UV spinney"
3698 );
3699 assert!(
3700 components.iter().all(|component| {
3701 graph.compute_dod(&collective_filter.subtract(component)) >= 0
3702 })
3703 );
3704 let retained_components = components
3705 .iter()
3706 .filter(|component| {
3707 wood.graph
3708 .iter_nodes()
3709 .any(|(_, _, spinney)| spinney.filter() == *component)
3710 })
3711 .cloned()
3712 .collect::<BTreeSet<_>>();
3713 assert_eq!(
3714 retained_components.len(),
3715 2,
3716 "complete complement coverage must not stand in for independent component counterterms"
3717 );
3718 assert!(
3719 retained_components
3720 .iter()
3721 .all(|component| graph.compute_dod(component) >= 0)
3722 );
3723 let retained_union = retained_components
3724 .iter()
3725 .cloned()
3726 .reduce(|left, right| left.union(&right))
3727 .unwrap();
3728 let join = wood
3729 .graph
3730 .iter_nodes()
3731 .find_map(|(node, _, spinney)| (spinney.filter() == &retained_union).then_some(node))
3732 .expect("the two independently divergent bubbles retain their union");
3733 assert_eq!(wood.join_factors(join), Some(retained_components));
3734
3735 let root = OwnedForestNode {
3736 spinney: Spinney::empty(&graph),
3737 topo_order: 0,
3738 };
3739 let root_local = Local4dCts::root();
3740 let input = Full4dCts::from_factorized_local(&root_local);
3741 let ctx = UVCtx::new(&graph, &settings.uv);
3742 let convergent = components
3743 .iter()
3744 .find(|component| graph.compute_dod(*component) < 0)
3745 .expect("the triangle is the convergent component");
3746 let replacements = graph.integrand_replacement(
3750 &graph.full_filter(),
3751 &graph.loop_momentum_basis,
3752 &[Atom::var(W_.x___)],
3753 );
3754 let mut independent = Vec::<(OwnedForestNode, Local4dCts)>::new();
3755 for component in components
3756 .iter()
3757 .filter(|component| graph.compute_dod(*component) >= 0)
3758 {
3759 let source = OwnedForestNode {
3760 spinney: Spinney::new(
3761 InternalSubGraph::cleaned_filter_optimist(component.clone(), graph.as_ref()),
3762 &graph,
3763 &graph.loop_momentum_basis,
3764 )?
3765 .expect("each divergent bubble has a compatible source chart"),
3766 topo_order: 0,
3767 };
3768 let local = local_4d::uv_limit(&input, &ctx, &source, &root, &source, &root)?;
3769 let mixed = OwnedForestNode {
3770 spinney: Spinney::new(
3771 InternalSubGraph::cleaned_filter_optimist(
3772 component.union(convergent),
3773 graph.as_ref(),
3774 ),
3775 &graph,
3776 &graph.loop_momentum_basis,
3777 )?
3778 .expect("the bubble-triangle collective region is logarithmic"),
3779 topo_order: 1,
3780 };
3781 let atomic = local_4d::uv_limit(&input, &ctx, &mixed, &root, &mixed, &root)?;
3782 let nested = local_4d::uv_limit(
3783 &Full4dCts::from_factorized_local(&local),
3784 &ctx,
3785 &mixed,
3786 &source,
3787 &mixed,
3788 &source,
3789 )?;
3790 assert!(
3791 !atomic.atom().is_zero(),
3792 "the mixed atomic Taylor term must be nonzero"
3793 );
3794 let mixed_remainder = GS
3799 .erase_uv_momentum_provenance(&(atomic.atom() + nested.atom()))
3800 .replace(GS.den(W_.a_, W_.mom_, W_.mass_, W_.prop_))
3801 .with(W_.prop_)
3802 .replace_multiple(&replacements)
3803 .normalize_dots()
3804 .collect_factors();
3805 assert!(
3806 mixed_remainder.is_zero(),
3807 "mixed collective Taylor terms must cancel"
3808 );
3809 independent.push((source, local));
3810 }
3811 let joint_scope = independent.iter().fold(
3812 graph.empty_subgraph::<SuBitGraph>(),
3813 |scope, (source, _)| scope.union(source.subgraph()),
3814 );
3815 let joint = OwnedForestNode {
3816 spinney: Spinney::new(
3817 InternalSubGraph::cleaned_filter_optimist(joint_scope, graph.as_ref()),
3818 &graph,
3819 &graph.loop_momentum_basis,
3820 )?
3821 .expect("the disjoint divergent bubbles have a compatible joint source chart"),
3822 topo_order: 1,
3823 };
3824 let product = Local4dCts::from_full_product(
3825 independent
3826 .iter()
3827 .map(|(_, local)| Full4dCts::from_factorized_local(local)),
3828 );
3829 let collective_source = OwnedForestNode {
3830 spinney: Spinney::new(
3831 InternalSubGraph::cleaned_filter_optimist(
3832 collective_filter.clone(),
3833 graph.as_ref(),
3834 ),
3835 &graph,
3836 &graph.loop_momentum_basis,
3837 )?
3838 .expect("the collective region has a compatible source chart"),
3839 topo_order: 2,
3840 };
3841 let mut expected = Atom::Zero;
3846 for (given, local) in std::iter::once((&root, &root_local))
3847 .chain(independent.iter().map(|(source, local)| (source, local)))
3848 .chain(std::iter::once((&joint, &product)))
3849 {
3850 let term = local_4d::uv_limit(
3851 &Full4dCts::from_factorized_local(local),
3852 &ctx,
3853 &collective_source,
3854 given,
3855 &collective_source,
3856 given,
3857 )?;
3858 if given.subgraph().is_empty() {
3859 assert!(
3860 !term.atom().is_zero(),
3861 "the full atomic Taylor term must be nonzero"
3862 );
3863 }
3864 expected += term.atom();
3865 }
3866 let mut forests = wood.unfold();
3867 forests.integrate(&graph, crate::utils::vakint()?, &settings.uv)?;
3868 let actual = forests
3869 .graph
3870 .iter_nodes()
3871 .filter(|(_, _, operation)| operation.covers().as_ref() == Some(&collective_filter))
3872 .try_fold(Atom::Zero, |sum, (_, _, operation)| -> Result<_> {
3873 Ok(sum
3874 + forests
3875 .compute_store
3876 .require(operation)?
3877 .local_4d(operation)?
3878 .atom())
3879 })?;
3880 let [actual, expected] = [actual, expected].map(|expression| {
3881 GS.erase_uv_momentum_provenance(&expression)
3882 .replace(GS.den(W_.a_, W_.mom_, W_.mass_, W_.prop_))
3883 .with(W_.prop_)
3884 .replace_multiple(&replacements)
3885 .normalize_dots()
3886 });
3887 assert!(
3888 (actual - expected).collect_factors().cancel().is_zero(),
3889 "the complete collective Taylor value must equal -T_U(1-T_A)(1-T_B)I"
3890 );
3891
3892 assert!(
3893 forests.graph.iter_nodes().all(|(_, _, operation)| {
3894 operation.covers().is_none_or(|scope| {
3895 graph
3896 .as_ref()
3897 .connected_components(&scope)
3898 .iter()
3899 .all(|component| graph.compute_dod(component) >= 0)
3900 })
3901 }),
3902 "no atomic or nested collective operation may include the convergent triangle"
3903 );
3904 assert!(
3905 forests.graph.iter_nodes().any(|(node, _, operation)| {
3906 operation.covers().as_ref() == Some(&retained_union)
3907 && forests.graph.is_disjoint_union(node)
3908 }),
3909 "the complete independently divergent cover remains a factorized join"
3910 );
3911
3912 Ok(())
3913 }
3914
3915 #[test]
3916 fn spectacles_typed_4d_local_construction_matches_uv_limit() -> Result<()> {
3917 test_initialise().unwrap();
3918
3919 let spectacles: Graph = dot!(
3920 digraph G{
3921 edge [particle="scalar_1"];
3922 v1 -> v2;
3923 v1 -> v2;
3924
3925 v3 -> v4;
3926 v3 -> v4;
3927
3928 v2 -> v3;
3929 v1 -> v4;
3930 },"scalars"
3931 )?;
3932
3933 let settings = GenerationSettings {
3935 uv: UVgenerationSettings {
3936 generate_integrated: false,
3937 local_uv_cts_from_expanded_4d_integrands: true,
3938 ..Default::default()
3939 },
3940 ..Default::default()
3941 };
3942 let cut_structure = CutStructure::empty(&spectacles);
3943 let f = Wood::new(cut_structure, &spectacles, &settings)?;
3944 println!("{}", f);
3945 let mut f = f.unfold();
3946 println!("{}", f);
3947
3948 let (union, child, edge) = f
3949 .graph
3950 .iter_nodes()
3951 .find_map(|(child, _, _)| {
3952 let (parent, edge) = f.graph.unique_parent(child)?;
3953 (!f.graph.is_disjoint_union(child) && f.graph.is_disjoint_union(parent))
3954 .then_some((parent, child, edge))
3955 })
3956 .expect("spectacles has a connected child above its disconnected union");
3957 f.integrate(&spectacles, crate::utils::vakint()?, &settings.uv)?;
3958
3959 let step_order = f.graph[union].key.op_count();
3960 let (current, given) = f.wood.current_given_pair(edge, step_order);
3961 let expected = local_4d::uv_limit(
3962 &f.recursion_input_4d(union)?,
3963 &UVCtx::new(&spectacles, &settings.uv),
3964 ¤t,
3965 &given,
3966 ¤t,
3967 &given,
3968 )?;
3969 let operation = &f.graph[child];
3970 let local = f.compute_store.require(operation)?.local_4d(operation)?;
3971 assert!((local.atom().collect_factors() - expected.atom().collect_factors()).is_zero());
3972
3973 Ok(())
3974 }
3975
3976 #[test]
3977 fn basketball() -> Result<()> {
3978 test_initialise().unwrap();
3979
3980 let basketball: Graph = dot!(
3981 digraph G{
3982 edge [particle="scalar_1"];
3983 v1 -> v2;
3984 v1 -> v2;
3985 v1 -> v2;
3986 v1 -> v2;
3987 },"scalars"
3988 )?;
3989
3990 let f = Wood::new(
3991 CutStructure::empty(&basketball),
3992 &basketball,
3993 &GenerationSettings::default(),
3994 )?;
3995 println!("{}", f);
3996 insta::assert_snapshot!(
3997 f.graph.n_nodes(),
3998 @"12",
3999 );
4000 let f = f.unfold();
4001 println!("{}", f.dot_serialize());
4002 insta::assert_snapshot!(
4003 f.graph.n_nodes(),
4004 @"46");
4005 Ok(())
4006 }
4007
4008 #[test]
4009 fn fourloop_b() -> Result<()> {
4010 test_initialise().unwrap();
4011
4012 let fourloop_b: Graph = dot!(
4013 digraph G{
4014 edge [particle="scalar_1"];
4015 v1 -> v2;
4016 v1 -> v2;
4017
4018 v3 -> v4;
4019 v3 -> v4;
4020
4021 v2 -> v3;
4022 v1 -> v3;
4023 v1 -> v4;
4024 },"scalars"
4025 )?;
4026
4027 let f = Wood::new(
4029 CutStructure::empty(&fourloop_b),
4030 &fourloop_b,
4031 &GenerationSettings::default(),
4032 )?;
4033 println!("{}", f);
4034 insta::assert_snapshot!(
4035 f.graph.n_nodes(),
4036 @"14",
4037 );
4038 let f = f.unfold();
4039 println!("{}", f);
4040 insta::assert_snapshot!(
4041 f.graph.n_nodes(),
4042 @"80",
4043 );
4044
4045 Ok(())
4046 }
4047
4048 #[test]
4049 fn four_loop_a() -> Result<()> {
4050 test_initialise().unwrap();
4051
4052 let four_loop_a: Graph = dot!(
4053 digraph G{
4054 edge [particle="scalar_1"];
4055 v1 -> v2;
4056 v1 -> v2;
4057 v2 -> v3;
4058 v3 -> v1;
4059 v1 -> v4;
4060 v2 -> v4;
4061 v3 -> v4;
4062 },"scalars"
4063 )?;
4064
4065 let f = Wood::new(
4066 CutStructure::empty(&four_loop_a),
4067 &four_loop_a,
4068 &GenerationSettings::default(),
4069 )?;
4070 println!("{}", f);
4071 insta::assert_snapshot!(
4072 f.graph.n_nodes(),
4073 @"12",
4074 );
4075 let f = f.unfold();
4076 println!("{}", f);
4077 insta::assert_snapshot!(
4078 f.graph.n_nodes(),
4079 @"60",
4080 );
4081
4082 Ok(())
4083 }
4084
4085 #[test]
4086 fn triple_double_tadpole() -> Result<()> {
4087 test_initialise().unwrap();
4088 let dumbell: Graph = dot!(
4089 digraph G{
4090 edge [particle="scalar_1"];
4091 v1 -> v2;
4092 v2 -> v3;
4093 v3 -> v3;v3 -> v3;
4094 v2 -> v2; v2 -> v2;
4095 v1 -> v1;v1 -> v1;
4096 },"scalars"
4097 )?;
4098
4099 let f = Wood::new(
4100 CutStructure::empty(&dumbell),
4101 &dumbell,
4102 &GenerationSettings::default(),
4103 )?;
4104
4105 insta::assert_snapshot!(
4106 f.graph.n_nodes(),
4107 @"64");
4108
4109 let f = f.unfold_uncached();
4110 insta::assert_snapshot!(
4111 f.graph.n_nodes(),
4112 @"307");
4113
4114 Ok(())
4115 }
4116
4117 mod failing {
4118 use super::*;
4119
4120 #[test]
4121 fn lobsided_double_dumbell() -> Result<()> {
4122 test_initialise().unwrap();
4123 let dumbell: Graph = dot!(
4124 digraph G{
4125 edge [particle="scalar_1"];
4126 v1 -> v2;
4127 v2 -> v2; v1 -> v1;v1 -> v1;
4129 },"scalars"
4130 )?;
4131
4132 let spinneys: Vec<_> = dumbell
4133 .spinneys(&dumbell.full_filter())
4134 .into_iter()
4135 .filter_map(|a| Spinney::new(a, &dumbell, &dumbell.loop_momentum_basis).transpose())
4136 .collect::<Result<_>>()?;
4137 let f = Wood::new(
4138 CutStructure::empty(&dumbell),
4139 &dumbell,
4140 &GenerationSettings::default(),
4141 )?;
4142
4143 println!("{}", f);
4144
4145 insta::assert_snapshot!(
4146 f.graph.n_nodes(),
4147 @"8",
4148 );
4150
4151 for (_, _, d) in f.graph.iter_nodes() {
4152 println!(
4153 "//Node {}: \n{}",
4154 d.subgraph.string_label(),
4155 dumbell.dot(&d.subgraph)
4156 );
4157 }
4158 let _ff = OldWood::from_spinneys(spinneys, &dumbell); let f = f.unfold();
4163 f.debug_walk();
4164 println!("{}", f);
4165 insta::assert_snapshot!(
4166 f.normalized_node_labels_with_cover("3L").join("\n"),
4167 @r###"
4168{36,F}: Approx(S_36(_))*Approx(S_F(_))
4169{3} · {36,F}: Approx((-1*S_3+S_F(_))*Approx(S_3(_)))*Approx(S_36(_))
4170{C} · {36,F}: Approx((-1*S_C+S_F(_))*Approx(S_C(_)))*Approx(S_36(_))
4171"###
4172 );
4173 insta::assert_snapshot!(
4174 f.graph.n_nodes(),
4175 @"12");
4176
4177 let f = Wood::new(
4178 CutStructure::empty(&dumbell),
4179 &dumbell,
4180 &GenerationSettings::default(),
4181 )?
4182 .unfold_uncached();
4183 assert!(f.compute_store.entries.is_empty());
4184 insta::assert_snapshot!(
4185 f.normalized_node_labels_with_cover("3L").join("\n"),
4186 @r###"
4187{36,F}: Approx(S_36(_))*Approx(S_F(_))
4188{3} · {36,F}: Approx((-1*S_3+S_F(_))*Approx(S_3(_)))*Approx(S_36(_))
4189{C} · {36,F}: Approx((-1*S_C+S_F(_))*Approx(S_C(_)))*Approx(S_36(_))
4190"###
4191 );
4192
4193 Ok(())
4194 }
4195
4196 #[test]
4197 fn double_double_dumbell() -> Result<()> {
4198 test_initialise().unwrap();
4199 let dumbell: Graph = dot!(
4200 digraph G{
4201 edge [particle="scalar_1"];
4202 v1 -> v2;
4203 v2 -> v2;v2 -> v2; v1 -> v1;v1 -> v1;
4205 },"scalars"
4206 )?;
4207
4208 let _spinneys: Vec<_> = dumbell
4209 .spinneys(&dumbell.full_filter())
4210 .into_iter()
4211 .filter_map(|a| Spinney::new(a, &dumbell, &dumbell.loop_momentum_basis).transpose())
4212 .collect::<Result<_>>()?;
4213 let f = Wood::new(
4214 CutStructure::empty(&dumbell),
4215 &dumbell,
4216 &GenerationSettings::default(),
4217 )?;
4218
4219 println!("{}", f);
4220
4221 insta::assert_snapshot!(
4222 f.graph.n_nodes(),
4223 @"16",
4224 );
4226
4227 let f = f.unfold();
4228
4229 insta::assert_snapshot!(
4230 f.graph.n_nodes(),
4231 @"36");
4232
4233 let f = Wood::new(
4234 CutStructure::empty(&dumbell),
4235 &dumbell,
4236 &GenerationSettings::default(),
4237 )?
4238 .unfold_uncached();
4239
4240 let foata_labels = f
4241 .graph
4242 .iter_nodes()
4243 .map(|(_, _, key)| key.foata_level_labels())
4244 .collect::<Vec<_>>();
4245 assert!(
4246 foata_labels
4247 .iter()
4248 .any(|label| label == "3,36;FU,F" || label == "36,3;FU,F"),
4249 "expected 3;F and 36;FU branch histories to combine, got:\n{}",
4250 foata_labels.join("\n")
4251 );
4252
4253 Ok(())
4254 }
4255 }
4256}
4257
4258#[cfg(test)]
4259#[path = "overlap_poles.rs"]
4260pub(crate) mod overlap_poles;