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::{
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    /// Apply a semantic substitution to roots and each retained body once.
66    /// Multiplicative cut prefactors continue to use the root-only `map`.
67    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                    // debug!("")
263                    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            // .replace(GS.dim)
369            // .max_level(0)
370            // .with(4); //.with(Atom::var(GS.dim_epsilon) * (-2) + 4);
371
372            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            // .collect_factors(); Really bad ! Turns
436        })
437    }
438
439    // pub(crate) fn graphs(&self, graph: &Graph) -> String {
440    //     let mut out = String::new();
441    //     self.dag.nodes.iter().for_each(|(_, a)| {
442    //         writeln!(
443    //             out,
444    //             "//S_{}:\n{}",
445    //             a.data.subgraph.string_label(),
446    //             a.data.dot(graph)
447    //         )
448    //         .unwrap()
449    //     });
450    //     writeln!(
451    //         out,
452    //         "{}",
453    //         self.dag
454    //             .to_dot_impl(&|n| format!("label=S_{}", n.data.subgraph.string_label()))
455    //     )
456    //     .unwrap();
457
458    //     out
459    // }
460}
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}