1use crate::{
2 GammaLoopContext, debug_tags,
3 graph::{Graph, LMBext, cuts::CutSet, parse::string_utils::dot_attr_value},
4 utils::{GS, W_},
5 uv::{
6 ApproximationType, Integrands,
7 approx::{
8 CutStructure, ForestNodeLike, OrientationProjection, final_integrand::FinalIntegrands,
9 local_3d::Localizer, projected_4d::Local4dProjectionContext,
10 },
11 marker::UvMarker,
12 settings::FinalIntegrandDimension,
13 },
14};
15use bincode_trait_derive::{Decode, Encode};
16use color_eyre::Result;
17use eyre::{WrapErr, eyre};
18use gammaloop_tracing_filter::{LogMessage, debug_instrument};
19use idenso::{color::ColorSimplifier, shorthands::schoonschip::Schoonschip};
20use spenso::shadowing::symbolica_utils::LogPrint;
21
22use symbolica::atom::{Atom, AtomCore};
23
24use linnet::half_edge::{
25 involution::HedgePair,
26 subgraph::{SubSetLike, SubSetOps},
27};
28use vakint::Vakint;
29
30use super::{
31 RenormalizationPart, UVgenerationSettings,
32 approx::Approximation,
33 export::UVForestNodeExpression,
34 poset::{DAG, DagNode},
35};
36
37pub struct CutForests {
38 pub cuts: CutStructure,
39 pub forests: Vec<Forest>,
40 pub settings: Vec<vakint::VakintSettings>,
41}
42
43#[derive(Clone, Encode, Decode)]
44#[trait_decode(trait = GammaLoopContext)]
45pub struct ParametricIntegrands {
46 pub integrands: Integrands,
47 pub cuts: CutSet,
48}
49
50impl ParametricIntegrands {
51 pub(crate) fn from_final(integrands: FinalIntegrands, cuts: CutSet) -> Self {
52 Self {
53 integrands: integrands.into_integrands(),
54 cuts,
55 }
56 }
57
58 pub fn map<F: FnMut(Atom) -> Atom>(self, mut map: F) -> Self {
59 Self {
60 integrands: self.integrands.map(|atom| map(atom.clone())),
61 cuts: self.cuts,
62 }
63 }
64
65 pub(crate) fn map_expressions(
68 self,
69 mut map: impl FnMut(&Atom) -> Result<Atom>,
70 ) -> Result<Self> {
71 Ok(Self {
72 integrands: self
73 .integrands
74 .fallible_map(&mut map)?
75 .map_numerators(map)?,
76 cuts: self.cuts,
77 })
78 }
79
80 pub fn zero_like(&self) -> Self {
81 Self {
82 integrands: self.integrands.zero_like(),
83 cuts: self.cuts.clone(),
84 }
85 }
86}
87
88impl CutForests {
89 #[debug_instrument(graph = %graph.log_display())]
90 pub(crate) fn compute(
91 &mut self,
92 graph: &mut Graph,
93 vakint: &Vakint,
94 orientation: OrientationProjection<'_>,
95 settings: &UVgenerationSettings,
96 ) -> Result<()> {
97 let mut projection_context = Local4dProjectionContext::default();
98 for ((forest, cuts), vakint_settings) in &mut self
99 .forests
100 .iter_mut()
101 .zip(self.cuts.cuts.iter())
102 .zip(self.settings.iter())
103 {
104 let localizer = Localizer::new(cuts, orientation);
105 debug_tags!(#forest,#uv;
106 n_terms = %forest.n_terms(),
107 "Computing cut forest");
108 forest.compute(
109 graph,
110 (vakint, vakint_settings),
111 localizer,
112 settings,
113 &mut projection_context,
114 )?;
115 }
116 Ok(())
117 }
118 #[debug_instrument(graph = %graph.log_display())]
119 pub(crate) fn orientation_parametric_exprs(
120 self,
121 graph: &Graph,
122 _settings: &UVgenerationSettings,
123 ) -> Result<Vec<ParametricIntegrands>> {
124 let started = std::time::Instant::now();
125 let CutForests {
126 cuts,
127 forests,
128 settings: _vakint_settings,
129 } = self;
130 debug_tags!(#generation, #profile, #uv, #graph, #summary;
131 stage = "orientation_parametric_exprs_start",
132 forest_count = forests.len(),
133 "Building orientation parametric integrands"
134 );
135 let mut exprs = vec![];
136
137 for (forest, cuts) in forests.iter().zip(cuts.cuts.into_iter()) {
138 exprs.push(ParametricIntegrands::from_final(
139 forest.orientation_parametric_expr(graph)?,
140 cuts,
141 ));
142 }
143 debug_tags!(#generation, #profile, #uv, #graph, #summary;
144 stage = "orientation_parametric_exprs_done",
145 result_count = exprs.len(),
146 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
147 "Built orientation parametric integrands"
148 );
149
150 Ok(exprs)
151 }
152}
153
154pub struct Forest {
155 pub dag: DAG<Approximation, DagNode, ()>,
156}
157
158impl Forest {
159 pub(crate) fn n_terms(&self) -> usize {
160 self.dag.nodes.len()
161 }
162
163 pub(crate) fn dot_serialize_for_export(&mut self, name: &str) -> String {
164 self.dag.compute_topological_order();
165 self.dag
166 .to_dot_impl(&|node| {
167 let order = node.order.unwrap_or_default() as usize;
168 format!(
169 "label={}",
170 dot_attr_value(&Self::export_node_key(order, &node.data))
171 )
172 })
173 .replacen("digraph Poset {", &format!("digraph {name} {{"), 1)
174 }
175
176 pub(crate) fn export_node_expressions(
177 &self,
178 graph: &Graph,
179 forest_index: usize,
180 post_process: &mut impl FnMut(Atom) -> Atom,
181 ) -> Result<Vec<UVForestNodeExpression>> {
182 let mut nodes = self.dag.nodes.values().collect::<Vec<_>>();
183 nodes.sort_by_key(|node| node.data.topo_order);
184
185 let mut terms = Vec::new();
186 for node in nodes {
187 let node_index = node.data.topo_order;
188 let node_key = Self::export_node_key(node_index, &node.data);
189 let final_integrand = node
190 .data
191 .final_integrand(graph)?
192 .clone()
193 .into_integrands()
194 .resolved()?
195 .map(|numerator| post_process(numerator.clone()));
196 for (term_index, (&residue_index, numerator)) in final_integrand.iter().enumerate() {
197 terms.push(UVForestNodeExpression {
198 forest_index,
199 node_index,
200 node_key: node_key.clone(),
201 term_index,
202 residue_index,
203 numerator: numerator.clone(),
204 forest_provenance: None,
205 });
206 }
207 }
208
209 Ok(terms)
210 }
211
212 fn export_node_key(node_index: usize, approximation: &Approximation) -> String {
213 format!(
214 "legacy:{node_index}:S_{}",
215 approximation.spinney.filter().string_label()
216 )
217 }
218
219 #[allow(clippy::too_many_arguments)]
220 #[debug_instrument(graph = %graph.log_display(), forest_terms = self.n_terms())]
221 pub(crate) fn compute(
222 &mut self,
223 graph: &mut Graph,
224 vakint: (&Vakint, &vakint::VakintSettings),
225 localizer: Localizer<'_>,
226 settings: &UVgenerationSettings,
227 projection_context: &mut Local4dProjectionContext,
228 ) -> Result<()> {
229 let started = std::time::Instant::now();
230 debug_tags!(#generation, #profile, #uv, #graph, #summary;
231 stage = "forest_compute_start",
232 generate_integrated = settings.generate_integrated,
233 final_integrand = %settings.final_integrand,
234 "Computing UV forest"
235 );
236 let order = self.dag.compute_topological_order();
237
238 for (i, n) in order.into_iter().enumerate() {
239 let node_started = std::time::Instant::now();
240 let parent_count = self.dag.nodes[n].parents.len();
241 let dod = self.dag.nodes[n].data.spinney.dod;
242 debug_tags!(#generation, #profile, #uv, #graph, #term, #summary;
243 stage = "forest_node_start",
244 node = ?n,
245 topo_index = i,
246 parent_count,
247 dod,
248 "Computing UV forest node"
249 );
250 match self.dag.nodes[n].parents.len() {
251 0 => {
252 self.dag.nodes[n].data.topo_order = i;
253 let root_started = std::time::Instant::now();
254 self.dag.nodes[n].data.root(graph, localizer, settings)?;
255 debug_tags!(#generation, #profile, #uv, #graph, #term, #summary;
256 stage = "forest_node_root_done",
257 elapsed_ms = root_started.elapsed().as_secs_f64() * 1000.0,
258 "Computed root UV forest node"
259 );
260 }
261 1 => {
262 let parent_id = self.dag.nodes[n].parents[0];
264 let [current, parent] =
265 &mut self.dag.nodes.get_disjoint_mut([n, parent_id]).unwrap();
266
267 let Some(a) = &parent.data.simple_approx else {
268 panic!("Should have computed the simple approx");
269 };
270 current.data.simple_approx =
271 Some(a.dependent(current.data.spinney.subgraph.clone()));
272
273 debug_tags!(#generation, #profile, #uv, #graph, #term, #summary;
274 stage = "computing forest node",
275 simple = %current.data.simple_approx.as_ref().map(|a| a.expr(&graph.full_filter()).to_string()).unwrap(),
276 log.current = current.data.spinney,
277 log.given = parent.data.spinney,
278 "Computing UV forest node single"
279 );
280
281 current.data.topo_order = i;
282 current
283 .data
284 .compute_4d(graph, vakint, &parent.data, settings)?;
285
286 match settings.final_integrand {
287 FinalIntegrandDimension::FourD => {
288 debug_tags!(#generation, #profile, #uv, #graph, #term, #summary;
289 "Skipping local UV forest node"
290 );
291 continue;
292 }
293 FinalIntegrandDimension::ThreeD => {
294 current.data.compute_3d(
295 &parent.data,
296 graph,
297 localizer,
298 settings,
299 projection_context,
300 )?;
301 }
302 }
303 }
304 _ => {
305 unimplemented!(
306 "Union not implemented for UV forest node {n:?} with {parent_count} parents in diagram '{}'",
307 graph.name,
308 );
309 }
310 }
311 debug_tags!(#generation, #profile, #uv, #graph, #term, #summary;
312 stage = "forest_node_done",
313 elapsed_ms = node_started.elapsed().as_secs_f64() * 1000.0,
314 "Computed UV forest node"
315 );
316 }
317 debug_tags!(#generation, #profile, #uv, #graph, #summary;
318 stage = "forest_compute_done",
319 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
320 "Computed UV forest"
321 );
322 Ok(())
323 }
324
325 pub(crate) fn renormalization_part_of_ends(
326 &self,
327 graph: &Graph,
328 settings: &UVgenerationSettings,
329 ) -> Result<RenormalizationPart> {
330 let mut sum = Atom::Zero;
331 let marker = UvMarker::new(settings);
332
333 let wild = Atom::var(W_.x___);
334
335 let replacements =
336 graph.integrand_replacement(&graph.full_filter(), &graph.loop_momentum_basis, &[wild]);
337 for (_, n) in &self.dag.nodes {
338 if !n.children.is_empty() {
339 continue;
340 }
341
342 let integrated = n.data.integrated(graph)?;
343 let physical = match n.data.renormalization_scheme() {
344 ApproximationType::MUV | ApproximationType::IR => {
345 integrated.physical_finite_counterterm_atom()
346 }
347 ApproximationType::PolePart => integrated.physical_pole_atom(),
348 scheme => return Err(eyre!("No terminal counterterm projection for {scheme}")),
349 };
350 let atom = marker.prefix(&graph.full_filter(), n.data.subgraph(), &physical);
351
352 debug_tags!(#generation, #uv, #graph, #term;
353 forest_term = %n.data.simple_display(graph),
354 log.expr = atom,
355 "Term before simplification"
356 );
357
358 let atom = &atom
359 * &graph.global_prefactor.projector
360 * &graph.global_prefactor.num
361 * &graph.overall_factor;
362 debug_tags!(#generation, #uv, #inspect, #dump;
363 log.expression = atom,
364 dod = n.data.spinney.dod,
365 "Dumped pole part color simplification input"
366 );
367 let atom = atom.simplify_color().expand_num().to_dots();
368 debug_tags!(#generation, #uv, #graph, #term;
373 forest_term=%
374 n.data
375 .simple_approx
376 .as_ref()
377 .unwrap()
378 .expr(&graph.full_filter()),
379 expr = % atom.log_print(None),"Term"
380 );
381 sum += atom;
382 }
383
384 Ok(RenormalizationPart::legacy(
385 sum.replace_multiple(&replacements)
386 .replace(GS.m_uv_expansion)
387 .with(GS.m_uv_vacuum),
388 ))
389 }
390
391 #[debug_instrument(graph = %graph.log_display())]
392 pub(crate) fn orientation_parametric_expr(&self, graph: &Graph) -> Result<FinalIntegrands> {
393 let mut sum: Option<FinalIntegrands> = None;
394
395 for (_, n) in &self.dag.nodes {
396 debug_tags!(#generation, #uv, #graph, #term;
397 dod = %n.data.dod(),
398 log.graph = %graph.dot_lmb_of(&n.data.spinney.subgraph,&n.data.spinney.lmb),
399 simple = %n.data.simple_display(graph),"Terms"
400 );
401 let terms = n
402 .data
403 .final_integrand(graph)?
404 .map_expressions(|integrand| Ok(integrand.clone().collect_color()))?;
405 sum = Some(match sum {
406 Some(sum) => sum.zip_add(terms).wrap_err_with(|| {
407 format!(
408 "while aggregating legacy UV forest term {}",
409 n.data.simple_display(graph)
410 )
411 })?,
412 None => terms,
413 });
414 }
415
416 let sum = sum.ok_or(eyre!("No terms in forest"))?;
417 let split_momentum_replacements = graph
418 .iter_edges_of(
419 &graph
420 .full_filter()
421 .subtract(&graph.initial_state_cut)
422 .subtract(&graph.tree_edges),
423 )
424 .filter_map(|(pair, edge_index, _)| {
425 (!matches!(pair, HedgePair::Unpaired { .. }))
426 .then(|| GS.split_mom_pattern_simple(edge_index))
427 })
428 .collect::<Vec<_>>();
429
430 sum.map_expressions(|integrand| {
431 Ok(integrand
432 .replace_multiple(&split_momentum_replacements)
433 .replace(GS.den(W_.a_, W_.b_, W_.c_, W_.d_))
434 .with(W_.d_))
435 })
437 }
438
439 }
461
462#[cfg(test)]
463mod tests {
464 use std::sync::Arc;
465
466 use super::ParametricIntegrands;
467 use crate::{
468 cff::CutCFFIndex, graph::cuts::CutSet, integrands::process::param_builder::FnMapEntry,
469 uv::Integrands,
470 };
471 use symbolica::{
472 atom::{Atom, AtomCore},
473 function, symbol,
474 };
475
476 #[test]
477 fn semantic_maps_substitute_shared_bodies_while_prefactors_multiply_roots()
478 -> color_eyre::Result<()> {
479 crate::initialisation::test_initialise()?;
480 let family = symbol!("gammalooprs::uv::numerator_family");
481 let parameter = symbol!("uv_forest_test::parameter");
482 let scope = Atom::var(symbol!("uv_forest_test::scope"));
483 let coordinate = Atom::var(symbol!("uv_forest_test::coordinate"));
484 let definition = Arc::new(FnMapEntry {
485 inlining: symbolica::evaluate::InliningPolicy::Always,
486 is_alias: false,
487 lhs: function!(family, &scope, parameter),
488 rhs: Atom::var(parameter) + &coordinate,
489 args: vec![parameter.into()],
490 tags: vec![scope.clone()],
491 });
492 let cut = CutCFFIndex::new_all_none();
493 let original = ParametricIntegrands {
494 integrands: Integrands::from_iter([(cut, &coordinate * function!(family, &scope, 2))])
495 .with_numerators([Arc::clone(&definition)])?,
496 cuts: CutSet::empty(3),
497 };
498 let multiplied = original.clone().map(|atom| Atom::num(3) * atom);
499 assert!(Arc::ptr_eq(
500 &multiplied.integrands.numerators()[0],
501 &definition
502 ));
503 assert_eq!(
504 multiplied.integrands.resolved()?,
505 Integrands::from_iter([(
506 cut,
507 Atom::num(3) * &coordinate * (Atom::num(2) + &coordinate)
508 )])
509 );
510
511 let mut visited = 0;
512 let substituted = original.map_expressions(|atom| {
513 visited += 1;
514 Ok(atom
515 .replace(coordinate.to_pattern())
516 .with(Atom::num(2) * &coordinate))
517 })?;
518 assert_eq!(visited, 2, "visit the root and shared body once each");
519 assert_eq!(substituted.cuts, CutSet::empty(3));
520 assert_eq!(
521 substituted.integrands.resolved()?,
522 Integrands::from_iter([(
523 cut,
524 Atom::num(2) * &coordinate * (Atom::num(2) + Atom::num(2) * &coordinate)
525 )])
526 );
527 Ok(())
528 }
529
530 #[test]
531 fn zero_like_preserves_shape_and_cuts() {
532 let integrands = ParametricIntegrands {
533 integrands: Integrands::from_iter([
534 (CutCFFIndex::new_all_none(), Atom::var(symbol!("x"))),
535 (
536 CutCFFIndex {
537 left_threshold_order: Some(1),
538 right_threshold_order: None,
539 lu_cut_order: None,
540 },
541 Atom::num(7),
542 ),
543 ]),
544 cuts: CutSet::empty(3),
545 };
546
547 let zeroed = integrands.zero_like();
548
549 assert_eq!(
550 zeroed.integrands,
551 Integrands::from_iter([
552 (CutCFFIndex::new_all_none(), Atom::Zero),
553 (
554 CutCFFIndex {
555 left_threshold_order: Some(1),
556 right_threshold_order: None,
557 lu_cut_order: None,
558 },
559 Atom::Zero,
560 ),
561 ]),
562 );
563 assert_eq!(zeroed.cuts, CutSet::empty(3));
564 }
565}