Skip to main content

gammalooprs/uv/
uv_graph.rs

1use std::{cell::RefCell, collections::BTreeSet, ops::Deref};
2
3use ahash::{AHashMap, AHashSet};
4use color_eyre::Result;
5use eyre::eyre;
6use idenso::shorthands::schoonschip::Schoonschip;
7use linnet::half_edge::{
8    HedgeGraph, PowersetIterator,
9    involution::{Flow, Hedge, HedgePair},
10    subgraph::{
11        Cycle, InternalSubGraph, ModifySubSet, PairwiseSubSetOps, SuBitGraph, SubGraphLike,
12        SubGraphOps, SubSetLike, SubSetOps, subset::SubSet,
13    },
14};
15use symbolica::{
16    atom::{Atom, AtomCore, Symbol},
17    domains::atom::AtomField,
18    function,
19    poly::series::Series,
20};
21use tracing::debug;
22
23use crate::{
24    graph::{Edge, FeynmanGraph, Graph, HedgeData, LMBext, LoopMomentumBasis, Vertex},
25    integrands::process::param_builder::ParamBuilderGraph,
26    momentum::sample::LoopIndex,
27    numerator::{AppliedFeynmanRule, Numerator},
28    settings::global::MediumSettings,
29    utils::{GS, W_, symbolica_ext::DOD},
30    uv::{ApproximationType, UVgenerationSettings, settings::CTIdentifier},
31};
32
33use super::{Spinney, Wood, spenso_lor_atom};
34
35pub trait UltravioletGraph: LMBext + FeynmanGraph + ParamBuilderGraph {
36    fn n_loops<S: SubGraphLike, E, V, H>(&self, subgraph: &S) -> usize
37    where
38        Self: AsRef<HedgeGraph<E, V, H>>,
39    {
40        self.as_ref().cyclotomatic_number(subgraph)
41    }
42
43    // fn concretize_spinney<S: SubGraphLike<Base = SuBitGraph>>(&self, subgraph: &S) -> Graph;
44
45    fn dummy_less_full_crown<S: SubGraphLike>(&self, subgraph: &S) -> S::Base
46    where
47        S::Base: ModifySubSet<Hedge> + SubGraphOps;
48
49    ///Get the numerator of the graph.
50    /// If multiply_prefactor is true, the numerator is multiplied by the global  prefactor. (just num not projector)
51    fn numerator<S: SubGraphLike + SubSetOps>(
52        &self,
53        subgraph: &S,
54        without: &S,
55    ) -> Numerator<AppliedFeynmanRule>;
56    fn denominator<S: SubGraphLike, T: Fn(&Edge) -> isize>(
57        &self,
58        subgraph: &S,
59        edge_powers: T,
60    ) -> Atom;
61
62    fn boundary_pdg_set<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(
63        &self,
64        subgraph: &S,
65    ) -> BTreeSet<isize>
66    where
67        Self: AsRef<HedgeGraph<E, V, H>>,
68    {
69        let graph = self.as_ref();
70        graph
71            .full_crown(subgraph)
72            .included_iter()
73            .filter_map(|hedge| {
74                let edge_id = graph[&hedge];
75                graph[edge_id].particle_pdg_code().map(|pdg| {
76                    if graph.flow(hedge) == Flow::Source {
77                        -pdg
78                    } else {
79                        pdg
80                    }
81                })
82            })
83            .collect()
84    }
85
86    fn internal_pdg_set<E: UVE, V, H, S: SubGraphLike>(&self, subgraph: &S) -> BTreeSet<isize>
87    where
88        Self: AsRef<HedgeGraph<E, V, H>>,
89    {
90        self.as_ref()
91            .iter_edges_of(subgraph)
92            .filter_map(|(pair, edge_id, _)| {
93                pair.is_paired()
94                    .then(|| self.as_ref()[edge_id].particle_pdg_code())
95                    .flatten()
96            })
97            .collect()
98    }
99
100    fn has_massive_boundary_external<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(
101        &self,
102        subgraph: &S,
103    ) -> bool
104    where
105        Self: AsRef<HedgeGraph<E, V, H>>,
106    {
107        let graph = self.as_ref();
108        graph.full_crown(subgraph).included_iter().any(|hedge| {
109            let edge_id = graph[&hedge];
110            graph[edge_id].is_massive()
111        })
112    }
113
114    fn ct_identifier<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(
115        &self,
116        subgraph: &S,
117    ) -> CTIdentifier
118    where
119        Self: AsRef<HedgeGraph<E, V, H>>,
120    {
121        CTIdentifier::new(
122            self.boundary_pdg_set(subgraph),
123            Some(self.internal_pdg_set(subgraph)),
124        )
125    }
126
127    fn approximation_scheme<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(
128        &self,
129        subgraph: &S,
130        settings: &UVgenerationSettings,
131        dod: i32,
132    ) -> ApproximationType
133    where
134        Self: AsRef<HedgeGraph<E, V, H>>,
135    {
136        settings.approximation_scheme_for(
137            &self.ct_identifier(subgraph),
138            dod,
139            self.has_massive_boundary_external(subgraph),
140        )
141    }
142
143    fn classify_spinney<E: UVE, V, H>(
144        &self,
145        spinney: InternalSubGraph,
146        settings: &UVgenerationSettings,
147        lmb: &LoopMomentumBasis,
148    ) -> Result<Option<Spinney>>
149    where
150        Self: AsRef<HedgeGraph<E, V, H>>,
151    {
152        let dod = self.compute_dod(&spinney.filter);
153        if dod < 0 {
154            return Ok(None);
155        }
156
157        let renormalization_scheme = self.approximation_scheme(&spinney.filter, settings, dod);
158
159        if renormalization_scheme != ApproximationType::Unsubtracted {
160            Spinney::with_scheme(spinney, self, lmb, renormalization_scheme, dod)
161        } else {
162            Ok(None)
163        }
164    }
165
166    fn classified_spinneys<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(
167        &self,
168        subgraph: &S,
169        settings: &UVgenerationSettings,
170        lmb: &LoopMomentumBasis,
171    ) -> Result<Vec<Spinney>>
172    where
173        Self: AsRef<HedgeGraph<E, V, H>>,
174    {
175        if !settings.subtract_uv {
176            return Ok(vec![Spinney::empty(self)]);
177        }
178
179        let graph = self.as_ref();
180        let (connected, disconnected): (Vec<_>, Vec<_>) = self
181            .spinneys(subgraph)
182            .into_iter()
183            .partition(|spinney| graph.connected_components(spinney).len() <= 1);
184
185        let mut classified: Vec<_> = connected
186            .into_iter()
187            .filter_map(|spinney| self.classify_spinney(spinney, settings, lmb).transpose())
188            .collect::<Result<_>>()?;
189        let retained_factors: AHashSet<_> = classified
190            .iter()
191            .map(|spinney| spinney.filter().clone())
192            .collect();
193
194        for spinney in disconnected {
195            let components = graph.connected_components(&spinney);
196            if !components.into_iter().all(|component| {
197                let factor = InternalSubGraph::cleaned_filter_optimist(component, graph);
198                retained_factors.contains(&factor.filter)
199            }) {
200                continue;
201            }
202
203            let dod = self.compute_dod(&spinney.filter);
204            if let Some(spinney) =
205                Spinney::with_scheme(spinney, self, lmb, ApproximationType::MUV, dod)?
206            {
207                classified.push(spinney);
208            }
209        }
210        Ok(classified)
211    }
212
213    fn remove_full_observable_spinney<E, V, H>(
214        &self,
215        spinneys: &mut Vec<Spinney>,
216        medium: &MediumSettings,
217    ) where
218        Self: AsRef<HedgeGraph<E, V, H>>,
219    {
220        if !medium.vacuum_subtraction {
221            return;
222        }
223
224        // Completed thermal weights already carry (1 - V). An operation covering
225        // all loops leaves no medium dependence, even when tree edges survive.
226        // Omit it before either 4D or 3D computation; spinneys contain no bridges.
227        let graph = self.as_ref();
228        let full_observable =
229            InternalSubGraph::cleaned_filter_pessimist(graph.non_bridges(), graph);
230        spinneys
231            .retain(|spinney| spinney.subgraph.is_empty() || spinney.subgraph != full_observable);
232    }
233
234    fn all_cycle_unions<E, V, H, S: SubGraphLike<Base = SuBitGraph>>(
235        &self,
236        subgraph: &S,
237    ) -> AHashSet<InternalSubGraph>
238    where
239        Self: AsRef<HedgeGraph<E, V, H>>,
240    {
241        let ref_graph = self.as_ref();
242        let _init_node = ref_graph.iter_nodes_of(subgraph).next().unwrap().0;
243        let all_subcycles: Vec<_> =
244            Cycle::all_sum_powerset_filter_map(&ref_graph.cycle_basis_of(subgraph).0, &Some)
245                .map(|a| a.into_iter().map(|c| c.internal_graph(ref_graph)).collect())
246                .unwrap();
247
248        // println!("{}", self.base_dot());
249        let spinneys: AHashSet<_> = InternalSubGraph::all_unions_iterative(&all_subcycles);
250
251        spinneys
252    }
253    fn all_limits<E, V, H, S: SubGraphLike>(
254        &self,
255        subgraph: &S,
256        expr: &Atom,
257        expansion: Symbol,
258        lmb: &LoopMomentumBasis,
259    ) -> Vec<(SubSet<LoopIndex>, Series<AtomField>)>
260    where
261        Self: AsRef<HedgeGraph<E, V, H>>,
262    {
263        let mom_reps = self.normal_emr_replacement(subgraph, lmb, &[W_.x___], |_s| true);
264
265        let ose_reps = self.get_ose_replacements();
266        // for x in &mom_reps {
267        //     println!("LMB replacement: {x}");
268        // }
269
270        // for r in &ose_reps {
271        //     println!("ose{r}");
272        // }
273
274        // This separate analytic copy uses native square-root Laurent series:
275        // after UV rescaling an energy invariant can have a pole, beyond the
276        // compact function's ordinary Taylor derivative domain. Lower energies
277        // after OSE substitution without distributing any numerator factors.
278        let expr = expr
279            .replace_multiple(&ose_reps)
280            .replace_multiple(&mom_reps)
281            .replace(function!(GS.broadcasting_sqrt, W_.a_))
282            .with(Atom::var(W_.a_).sqrt())
283            .replace(function!(GS.on_shell_energy, W_.a_, W_.prop_))
284            .with(Atom::var(W_.prop_).sqrt());
285        // .replace_multiple(&q3_reps);
286        let mut loops = PowersetIterator::<LoopIndex>::new(lmb.loop_edges.len() as u8);
287
288        let mut limits = Vec::new();
289
290        loops.next();
291
292        for ls in loops {
293            let mut expr = expr.clone();
294            for l in ls.included_iter() {
295                let e = usize::from(lmb.loop_edges[l]) as i64;
296                expr = expr
297                    .replace(function!(GS.emr_mom, e, W_.x___))
298                    .with(function!(GS.emr_mom, e, W_.x___) / expansion);
299
300                expr /= Atom::var(expansion).pow(3);
301            }
302
303            let series = expr.series(expansion, Atom::Zero, 0).unwrap();
304
305            // expr = series.to_atom().expand();
306
307            limits.push((ls, series));
308        }
309        limits
310    }
311
312    fn wood<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(&self, subgraph: &S) -> Result<Wood>
313    where
314        Self: AsRef<HedgeGraph<E, V, H>>,
315    {
316        self.wood_with_settings(
317            subgraph,
318            &UVgenerationSettings::default(),
319            &self.as_ref().lmb_of(&self.as_ref().full_filter()),
320        )
321    }
322
323    fn wood_with_settings<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(
324        &self,
325        subgraph: &S,
326        settings: &UVgenerationSettings,
327        lmb: &LoopMomentumBasis,
328    ) -> Result<Wood>
329    where
330        Self: AsRef<HedgeGraph<E, V, H>>,
331    {
332        Ok(Wood::from_spinneys(
333            self.classified_spinneys(subgraph, settings, lmb)?,
334            self,
335        ))
336    }
337
338    fn compute_dod<S: SubGraphLike<Base = SuBitGraph> + SubSetOps>(&self, subgraph: &S) -> i32;
339
340    /// Degree of divergence of the simultaneous loop-energy integrations.
341    ///
342    /// The existing UV DOD already contains the numerator and denominator
343    /// scaling. Replacing the four-dimensional measure by one energy measure
344    /// per loop therefore amounts to subtracting three powers per loop.
345    fn compute_energy_dod<E, V, H, S>(&self, subgraph: &S) -> i32
346    where
347        Self: AsRef<HedgeGraph<E, V, H>>,
348        S: SubGraphLike<Base = SuBitGraph> + SubSetOps,
349    {
350        self.compute_dod(subgraph) - 3 * self.n_loops(subgraph) as i32
351    }
352
353    fn local_dod<S: SubGraphLike>(&self, subgraph: &S) -> i32;
354
355    fn spinneys<E, V, H, S: SubGraphLike<Base = SuBitGraph>>(
356        &self,
357        subgraph: &S,
358    ) -> AHashSet<InternalSubGraph>
359    where
360        Self: AsRef<HedgeGraph<E, V, H>>,
361    {
362        let ref_graph: &HedgeGraph<E, V, H> = self.as_ref();
363        debug!(subgraph=%ref_graph.dot(subgraph),"Spinneys of subgraph");
364        let _b: SuBitGraph = ref_graph.empty_subgraph();
365
366        if subgraph.is_empty() {
367            let mut spinneys = AHashSet::new();
368            spinneys.insert(ref_graph.empty_subgraph());
369            return spinneys;
370        }
371
372        let cycles = ref_graph.cycle_basis_of(subgraph).0;
373
374        let _init_node = ref_graph.iter_nodes_of(subgraph).next().unwrap().0;
375        let all_subcycles: Vec<_> = Cycle::all_sum_powerset_filter_map(&cycles, &Some)
376            .map(|a| a.into_iter().map(|c| c.internal_graph(ref_graph)).collect())
377            .unwrap();
378
379        let dod_cache = RefCell::new(AHashMap::new());
380
381        let mut spinneys: AHashSet<_> = InternalSubGraph::all_ops_iterative_filter_map(
382            &all_subcycles,
383            &|a, b| a.union(b),
384            &|union| {
385                // println!("{}", self.as_ref().dot(&union));
386                let cached_dod = dod_cache.borrow().get(&union).copied();
387                let keep = match cached_dod {
388                    Some(keep) => keep,
389                    None => {
390                        let keep = self.local_dod(&union) >= 0;
391                        dod_cache.borrow_mut().insert(union.clone(), keep);
392                        keep
393                    }
394                };
395                if keep { Some(union) } else { None }
396            },
397        );
398
399        spinneys.insert(ref_graph.empty_subgraph());
400
401        spinneys
402    }
403}
404
405impl AsRef<HedgeGraph<Edge, Vertex, HedgeData>> for Graph {
406    fn as_ref(&self) -> &HedgeGraph<Edge, Vertex, HedgeData> {
407        &self.underlying
408    }
409}
410
411impl Graph {
412    /// Reject source integrals whose loop-energy contours do not vanish at
413    /// infinity. Check the existing unpruned cycle-union inventory so this
414    /// source-level contract does not depend on UV-spinney pruning.
415    pub(crate) fn ensure_energy_convergent_cycles<S>(&self, domain: &S) -> Result<()>
416    where
417        S: SubGraphLike<Base = SuBitGraph>,
418    {
419        let numerator = self.production_numerator_atom_for_full_3d_expression();
420        if numerator.is_zero() {
421            return Ok(());
422        }
423        let mut failures = self
424            .all_cycle_unions(domain)
425            .into_iter()
426            .filter(|cycle| !cycle.is_empty())
427            .filter_map(|cycle| {
428                let loop_count = self.n_loops(&cycle.filter);
429                // `compute_dod` deliberately covers only the edge and vertex
430                // rules used by UV classification. Source convergence instead
431                // sees the complete factorized production numerator, including
432                // graph-global factors and momentum dependence stored outside
433                // this cycle. The production helper excludes initial-cut
434                // factors without changing UV DOD semantics or expanding the
435                // numerator.
436                let lmb = self.lmb_of(&cycle.filter);
437                let integrand = &numerator / self.denominator(&cycle.filter, |_| 1);
438                let four_d_dod = self
439                    .uv_rescaled(cycle.filter.included(), loop_count, &lmb, &lmb, &integrand)
440                    .trailing_exponent();
441                // The rescaled source already contains the four-dimensional
442                // loop measure; retain one energy measure per loop instead.
443                let energy_dod = four_d_dod - 3 * loop_count as i32;
444                (energy_dod >= 0)
445                    .then(|| (cycle.string_label(), loop_count, four_d_dod, energy_dod))
446            })
447            .collect::<Vec<_>>();
448        failures.sort();
449
450        if failures.is_empty() {
451            return Ok(());
452        }
453
454        let details = failures
455            .into_iter()
456            .map(|(cycle, loop_count, four_d_dod, energy_dod)| {
457                format!("cycle {cycle}: L={loop_count}, DOD_4D={four_d_dod}, DOD_E={energy_dod}")
458            })
459            .collect::<Vec<_>>()
460            .join("; ");
461        Err(eyre!(
462            "graph `{}` contains a loop-energy nonconvergent integral ({details}); expected DOD_E < 0 for every cycle union",
463            self.name,
464        ))
465    }
466}
467
468impl UltravioletGraph for Graph {
469    fn dummy_less_full_crown<S: SubGraphLike>(&self, subgraph: &S) -> S::Base
470    where
471        S::Base: ModifySubSet<Hedge>,
472    {
473        let a = self.full_crown(subgraph);
474        let mut ac = a.clone();
475
476        a.included_iter().for_each(|a| {
477            if self[self[&a]].is_dummy {
478                ac.sub(a);
479            }
480        });
481
482        ac
483    }
484
485    // fn concretize_spinney<S: SubGraphLike<Base = SuBitGraph>>(&self, s: &S) -> Graph {
486    //     let mut underlying = self.underlying.clone();
487    //     let dod = self.dod(s);
488    //     let cc = underlying.connected_components(s);
489
490    //     for c in cc{
491
492    //     let v = Vertex {
493
494    //     }
495    //     underlying.identify_nodes_without_self_edges(s, node_data_merge);
496    //     };
497    //     }
498
499    //     self.clone()
500    // }
501
502    fn denominator<S: SubGraphLike, T: Fn(&Edge) -> isize>(
503        &self,
504        subgraph: &S,
505        edge_powers: T,
506    ) -> Atom {
507        let mut den = Atom::num(1);
508
509        for (pair, eid, d) in self.underlying.iter_edges_of(subgraph) {
510            if matches!(pair, HedgePair::Paired { .. }) {
511                let m2 = d.data.mass_atom().pow(2);
512                let edge_power = edge_powers(d.data);
513                let is_power_negative = edge_power < 0;
514                let prop_den = GS.den(
515                    usize::from(eid),
516                    function!(GS.emr_mom, usize::from(eid)),
517                    &m2,
518                    spenso_lor_atom(usize::from(eid) as i32, usize::from(eid), GS.dim)
519                        .pow(2)
520                        .to_dots()
521                        - &m2,
522                );
523                for _i in 0..edge_power.abs() {
524                    if is_power_negative {
525                        den /= prop_den.clone();
526                    } else {
527                        den *= prop_den.clone();
528                    }
529                }
530            }
531        }
532
533        den
534    }
535    fn numerator<S: SubGraphLike + SubSetOps>(
536        &self,
537        subgraph: &S,
538        without: &S,
539    ) -> Numerator<AppliedFeynmanRule> {
540        let num = Numerator::default();
541
542        num.fill_in_reduced(self, subgraph, without)
543    }
544
545    fn compute_dod<S: SubGraphLike<Base = SuBitGraph> + SubSetOps>(&self, subgraph: &S) -> i32 {
546        let lmb = self.lmb_of(subgraph);
547        let empty = self.underlying.empty_subgraph();
548        let integrand = self
549            .numerator(subgraph, &empty)
550            .to_d_dim(GS.dim)
551            .get_single_atom()
552            .unwrap()
553            / self.denominator(subgraph, |_| 1);
554        let nloops: usize = self.n_loops(subgraph);
555        self.uv_rescaled(subgraph.included(), nloops, &lmb, &lmb, &integrand)
556            .trailing_exponent()
557    }
558
559    fn local_dod<S: SubGraphLike>(&self, subgraph: &S) -> i32 {
560        let mut dod: i32 = 4 * self.n_loops(subgraph) as i32;
561        for (p, _, e) in self.underlying.iter_edges_of(subgraph) {
562            if p.is_paired() {
563                dod += e.data.dod.deref();
564            }
565        }
566
567        for (_, _, n) in self.underlying.iter_nodes_of(subgraph) {
568            dod += n.dod.deref();
569        }
570
571        dod
572    }
573}
574
575pub trait UVE {
576    fn mass_atom(&self) -> Atom;
577    fn particle_pdg_code(&self) -> Option<isize>;
578    fn is_massive(&self) -> bool;
579    fn is_fermion(&self) -> bool;
580    fn chemical_potential_atom(&self) -> Option<Atom>;
581}