Skip to main content

gammalooprs/uv/
forest.rs

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                    // debug!("")
223                    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            // .replace(GS.dim)
324            // .max_level(0)
325            // .with(4); //.with(Atom::var(GS.dim_epsilon) * (-2) + 4);
326
327            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            // .collect_factors(); Really bad ! Turns
393        }))
394    }
395
396    // pub(crate) fn graphs(&self, graph: &Graph) -> String {
397    //     let mut out = String::new();
398    //     self.dag.nodes.iter().for_each(|(_, a)| {
399    //         writeln!(
400    //             out,
401    //             "//S_{}:\n{}",
402    //             a.data.subgraph.string_label(),
403    //             a.data.dot(graph)
404    //         )
405    //         .unwrap()
406    //     });
407    //     writeln!(
408    //         out,
409    //         "{}",
410    //         self.dag
411    //             .to_dot_impl(&|n| format!("label=S_{}", n.data.subgraph.string_label()))
412    //     )
413    //     .unwrap();
414
415    //     out
416    // }
417}
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}