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::{CutStructure, ForestNodeLike, OrientationProjection, local_3d::Localizer},
8 marker::UvMarker,
9 settings::FinalIntegrandDimension,
10 },
11};
12use bincode_trait_derive::{Decode, Encode};
13use color_eyre::Result;
14use eyre::{WrapErr, eyre};
15use gammaloop_tracing_filter::{LogMessage, debug_instrument};
16use idenso::{color::ColorSimplifier, shorthands::schoonschip::Schoonschip};
17use spenso::shadowing::symbolica_utils::LogPrint;
18
19use symbolica::atom::{Atom, AtomCore};
20
21use linnet::half_edge::{
22 involution::HedgePair,
23 subgraph::{SubSetLike, SubSetOps},
24};
25use vakint::Vakint;
26
27use super::{
28 RenormalizationPart, UVgenerationSettings,
29 approx::Approximation,
30 export::UVForestNodeExpression,
31 poset::{DAG, DagNode},
32};
33
34pub struct CutForests {
35 pub cuts: CutStructure,
36 pub forests: Vec<Forest>,
37 pub settings: Vec<vakint::VakintSettings>,
38}
39
40#[derive(Clone, Encode, Decode)]
41#[trait_decode(trait = GammaLoopContext)]
42pub struct ParametricIntegrands {
43 pub integrands: Integrands,
44 pub cuts: CutSet,
45}
46
47impl ParametricIntegrands {
48 pub fn map<F: FnMut(Atom) -> Atom>(self, mut map: F) -> Self {
49 Self {
50 integrands: self.integrands.map(|atom| map(atom.clone())),
51 cuts: self.cuts,
52 }
53 }
54
55 pub fn zero_like(&self) -> Self {
56 Self {
57 integrands: self.integrands.map(|_| Atom::Zero),
58 cuts: self.cuts.clone(),
59 }
60 }
61}
62
63impl CutForests {
64 #[debug_instrument(graph = %graph.log_display())]
65 pub(crate) fn compute(
66 &mut self,
67 graph: &mut Graph,
68 vakint: &Vakint,
69 orientation: OrientationProjection<'_>,
70 settings: &UVgenerationSettings,
71 ) -> Result<()> {
72 for ((forest, cuts), vakint_settings) in &mut self
73 .forests
74 .iter_mut()
75 .zip(self.cuts.cuts.iter())
76 .zip(self.settings.iter())
77 {
78 let localizer = Localizer::new(cuts, orientation);
79 debug_tags!(#forest,#uv;
80 n_terms = %forest.n_terms(),
81 "Computing cut forest");
82 forest.compute(graph, (vakint, vakint_settings), localizer, settings)?;
83 }
84 Ok(())
85 }
86 #[debug_instrument(graph = %graph.log_display())]
87 pub(crate) fn orientation_parametric_exprs(
88 self,
89 graph: &Graph,
90 _settings: &UVgenerationSettings,
91 ) -> Result<Vec<ParametricIntegrands>> {
92 let started = std::time::Instant::now();
93 let CutForests {
94 cuts,
95 forests,
96 settings: _vakint_settings,
97 } = self;
98 debug_tags!(#generation, #profile, #uv, #graph, #summary;
99 stage = "orientation_parametric_exprs_start",
100 forest_count = forests.len(),
101 "Building orientation parametric integrands"
102 );
103 let mut exprs = vec![];
104
105 for (forest, cuts) in forests.iter().zip(cuts.cuts.into_iter()) {
106 exprs.push(ParametricIntegrands {
107 integrands: forest.orientation_parametric_expr(graph)?,
108 cuts,
109 });
110 }
111 debug_tags!(#generation, #profile, #uv, #graph, #summary;
112 stage = "orientation_parametric_exprs_done",
113 result_count = exprs.len(),
114 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
115 "Built orientation parametric integrands"
116 );
117
118 Ok(exprs)
119 }
120}
121
122pub struct Forest {
123 pub dag: DAG<Approximation, DagNode, ()>,
124}
125
126impl Forest {
127 pub(crate) fn n_terms(&self) -> usize {
128 self.dag.nodes.len()
129 }
130
131 pub(crate) fn dot_serialize_for_export(&mut self, name: &str) -> String {
132 self.dag.compute_topological_order();
133 self.dag
134 .to_dot_impl(&|node| {
135 let order = node.order.unwrap_or_default() as usize;
136 format!(
137 "label={}",
138 dot_attr_value(&Self::export_node_key(order, &node.data))
139 )
140 })
141 .replacen("digraph Poset {", &format!("digraph {name} {{"), 1)
142 }
143
144 pub(crate) fn export_node_expressions(
145 &self,
146 graph: &Graph,
147 forest_index: usize,
148 post_process: &mut impl FnMut(Atom) -> Atom,
149 ) -> Result<Vec<UVForestNodeExpression>> {
150 let mut nodes = self.dag.nodes.values().collect::<Vec<_>>();
151 nodes.sort_by_key(|node| node.data.topo_order);
152
153 let mut terms = Vec::new();
154 for node in nodes {
155 let node_index = node.data.topo_order;
156 let node_key = Self::export_node_key(node_index, &node.data);
157 let final_integrand = node.data.final_integrand(graph)?;
158 for (term_index, (&residue_index, numerator)) in final_integrand.iter().enumerate() {
159 terms.push(UVForestNodeExpression {
160 forest_index,
161 node_index,
162 node_key: node_key.clone(),
163 term_index,
164 residue_index,
165 numerator: post_process(numerator.clone()),
166 });
167 }
168 }
169
170 Ok(terms)
171 }
172
173 fn export_node_key(node_index: usize, approximation: &Approximation) -> String {
174 format!(
175 "legacy:{node_index}:S_{}",
176 approximation.spinney.filter().string_label()
177 )
178 }
179
180 #[allow(clippy::too_many_arguments)]
181 #[debug_instrument(graph = %graph.log_display(), forest_terms = self.n_terms())]
182 pub(crate) fn compute(
183 &mut self,
184 graph: &mut Graph,
185 vakint: (&Vakint, &vakint::VakintSettings),
186 localizer: Localizer<'_>,
187 settings: &UVgenerationSettings,
188 ) -> Result<()> {
189 let started = std::time::Instant::now();
190 debug_tags!(#generation, #profile, #uv, #graph, #summary;
191 stage = "forest_compute_start",
192 generate_integrated = settings.generate_integrated,
193 final_integrand = %settings.final_integrand,
194 "Computing UV forest"
195 );
196 let order = self.dag.compute_topological_order();
197
198 for (i, n) in order.into_iter().enumerate() {
199 let node_started = std::time::Instant::now();
200 let parent_count = self.dag.nodes[n].parents.len();
201 let dod = self.dag.nodes[n].data.spinney.dod;
202 debug_tags!(#generation, #profile, #uv, #graph, #term, #summary;
203 stage = "forest_node_start",
204 node = ?n,
205 topo_index = i,
206 parent_count,
207 dod,
208 "Computing UV forest node"
209 );
210 match self.dag.nodes[n].parents.len() {
211 0 => {
212 self.dag.nodes[n].data.topo_order = i;
213 let root_started = std::time::Instant::now();
214 self.dag.nodes[n].data.root(graph, localizer, settings)?;
215 debug_tags!(#generation, #profile, #uv, #graph, #term, #summary;
216 stage = "forest_node_root_done",
217 elapsed_ms = root_started.elapsed().as_secs_f64() * 1000.0,
218 "Computed root UV forest node"
219 );
220 }
221 1 => {
222 let parent_id = self.dag.nodes[n].parents[0];
224 let [current, parent] =
225 &mut self.dag.nodes.get_disjoint_mut([n, parent_id]).unwrap();
226
227 let Some(a) = &parent.data.simple_approx else {
228 panic!("Should have computed the simple approx");
229 };
230 current.data.simple_approx =
231 Some(a.dependent(current.data.spinney.subgraph.clone()));
232
233 debug_tags!(#generation, #profile, #uv, #graph, #term, #summary;
234 stage = "computing forest node",
235 simple = %current.data.simple_approx.as_ref().map(|a| a.expr(&graph.full_filter()).to_string()).unwrap(),
236 log.current = current.data.spinney,
237 log.given = parent.data.spinney,
238 "Computing UV forest node single"
239 );
240
241 current.data.topo_order = i;
242 current
243 .data
244 .compute_4d(graph, vakint, &parent.data, settings)?;
245
246 match settings.final_integrand {
247 FinalIntegrandDimension::FourD => {
248 debug_tags!(#generation, #profile, #uv, #graph, #term, #summary;
249 "Skipping local UV forest node"
250 );
251 continue;
252 }
253 FinalIntegrandDimension::ThreeD => {
254 current
255 .data
256 .compute_3d(&parent.data, graph, localizer, settings)?;
257 }
258 }
259 }
260 _ => {
261 unimplemented!(
262 "Union not implemented for UV forest node {n:?} with {parent_count} parents in diagram '{}'",
263 graph.name,
264 );
265 }
266 }
267 debug_tags!(#generation, #profile, #uv, #graph, #term, #summary;
268 stage = "forest_node_done",
269 elapsed_ms = node_started.elapsed().as_secs_f64() * 1000.0,
270 "Computed UV forest node"
271 );
272 }
273 debug_tags!(#generation, #profile, #uv, #graph, #summary;
274 stage = "forest_compute_done",
275 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
276 "Computed UV forest"
277 );
278 Ok(())
279 }
280
281 pub(crate) fn renormalization_part_of_ends(
282 &self,
283 graph: &Graph,
284 settings: &UVgenerationSettings,
285 ) -> Result<RenormalizationPart> {
286 let mut sum = Atom::Zero;
287 let marker = UvMarker::new(settings);
288
289 let wild = Atom::var(W_.x___);
290
291 let replacements =
292 graph.integrand_replacement(&graph.full_filter(), &graph.loop_momentum_basis, &[wild]);
293 for (_, n) in &self.dag.nodes {
294 if !n.children.is_empty() {
295 continue;
296 }
297
298 let integrated = n.data.integrated(graph)?;
299 let physical = match n.data.renormalization_scheme() {
300 ApproximationType::MUV => integrated.physical_finite_counterterm_atom(),
301 ApproximationType::PolePart => integrated.physical_pole_atom(),
302 scheme => return Err(eyre!("No terminal counterterm projection for {scheme}")),
303 };
304 let atom = marker.prefix(&graph.full_filter(), n.data.subgraph(), &physical);
305
306 let expanded_atom = atom.expand_num();
307 debug_tags!(#generation, #uv, #graph, #term;
308 forest_term = %n.data.simple_display(graph),
309 log.expr = expanded_atom,
310 "Term before simplification"
311 );
312
313 let atom = &atom
314 * &graph.global_prefactor.projector
315 * &graph.global_prefactor.num
316 * &graph.overall_factor;
317 debug_tags!(#generation, #uv, #inspect, #dump;
318 log.expression = atom,
319 dod = n.data.spinney.dod,
320 "Dumped pole part color simplification input"
321 );
322 let atom = atom.simplify_color().expand_num().to_dots();
323 debug_tags!(#generation, #uv, #graph, #term;
328 forest_term=%
329 n.data
330 .simple_approx
331 .as_ref()
332 .unwrap()
333 .expr(&graph.full_filter()),
334 expr = % atom.log_print(None),"Term"
335 );
336 sum += atom;
337 }
338
339 Ok(RenormalizationPart::legacy(
340 sum.replace_multiple(&replacements)
341 .replace(GS.m_uv_expansion)
342 .with(GS.m_uv_vacuum),
343 ))
344 }
345
346 #[debug_instrument(graph = %graph.log_display())]
347 pub(crate) fn orientation_parametric_expr(&self, graph: &Graph) -> Result<Integrands> {
348 let mut sum: Option<Integrands> = None;
349
350 for (_, n) in &self.dag.nodes {
351 debug_tags!(#generation, #uv, #graph, #term;
352 dod = %n.data.dod(),
353 log.graph = %graph.dot_lmb_of(&n.data.spinney.subgraph,&n.data.spinney.lmb),
354 simple = %n.data.simple_display(graph),"Terms"
355 );
356 let terms = n
357 .data
358 .final_integrand(graph)?
359 .iter()
360 .map(|(cut_index, integrand)| (*cut_index, integrand.clone().collect_color()))
361 .collect();
362 sum = Some(match sum {
363 Some(sum) => sum.zip_add(&terms).wrap_err_with(|| {
364 format!(
365 "while aggregating legacy UV forest term {}",
366 n.data.simple_display(graph)
367 )
368 })?,
369 None => terms,
370 });
371 }
372
373 let sum = sum.ok_or(eyre!("No terms in forest"))?;
374 let split_momentum_replacements = graph
375 .iter_edges_of(
376 &graph
377 .full_filter()
378 .subtract(&graph.initial_state_cut)
379 .subtract(&graph.tree_edges),
380 )
381 .filter_map(|(pair, edge_index, _)| {
382 (!matches!(pair, HedgePair::Unpaired { .. }))
383 .then(|| GS.split_mom_pattern_simple(edge_index))
384 })
385 .collect::<Vec<_>>();
386
387 Ok(sum.map(|integrand| {
388 integrand
389 .replace_multiple(&split_momentum_replacements)
390 .replace(GS.den(W_.a_, W_.b_, W_.c_, W_.d_))
391 .with(W_.d_)
392 }))
394 }
395
396 }
418
419#[cfg(test)]
420mod tests {
421 use super::ParametricIntegrands;
422 use crate::{cff::CutCFFIndex, graph::cuts::CutSet, uv::Integrands};
423 use symbolica::{atom::Atom, symbol};
424
425 #[test]
426 fn zero_like_preserves_shape_and_cuts() {
427 let integrands = ParametricIntegrands {
428 integrands: Integrands::from_iter([
429 (CutCFFIndex::new_all_none(), Atom::var(symbol!("x"))),
430 (
431 CutCFFIndex {
432 left_threshold_order: Some(1),
433 right_threshold_order: None,
434 lu_cut_order: None,
435 },
436 Atom::num(7),
437 ),
438 ]),
439 cuts: CutSet::empty(3),
440 };
441
442 let zeroed = integrands.zero_like();
443
444 assert_eq!(
445 zeroed.integrands,
446 Integrands::from_iter([
447 (CutCFFIndex::new_all_none(), Atom::Zero),
448 (
449 CutCFFIndex {
450 left_threshold_order: Some(1),
451 right_threshold_order: None,
452 lu_cut_order: None,
453 },
454 Atom::Zero,
455 ),
456 ]),
457 );
458 assert_eq!(zeroed.cuts, CutSet::empty(3));
459 }
460}