Skip to main content

gammalooprs/uv/
mod.rs

1use std::{
2    collections::BTreeMap,
3    hash::Hash,
4    ops::{Mul, Neg},
5    sync::Arc,
6};
7
8use crate::{
9    GammaLoopContext, cff::CutCFFIndex, integrands::process::param_builder::FnMapEntry,
10    numerator::aind::Aind, utils::GS, uv::approx::Rooted,
11};
12use bincode_trait_derive::{Decode, Encode};
13use color_eyre::Result;
14use eyre::eyre;
15use itertools::{EitherOrBoth, Itertools};
16use spenso::{
17    network::parsing::ShadowedStructure,
18    structure::{
19        NamedStructure, ToSymbolic,
20        dimension::Dimension,
21        representation::{Minkowski, RepName},
22    },
23};
24use symbolica::{
25    atom::{Atom, AtomCore, AtomView, Symbol},
26    symbol,
27};
28
29use linnet::half_edge::involution::HedgePair;
30
31// use vakint::{EvaluationOrder, LoopNormalizationFactor, Vakint, VakintSettings};
32
33pub(crate) fn spenso_lor(
34    tag: i32,
35    ind: impl Into<Aind>,
36    dim: impl Into<Dimension>,
37) -> ShadowedStructure<Aind> {
38    let mink = Minkowski {}.new_slot(dim, ind);
39    NamedStructure::from_iter([mink], GS.emr_mom, Some(vec![Atom::num(tag)])).into_canonical()
40}
41
42pub(crate) fn spenso_lor_atom(tag: i32, ind: impl Into<Aind>, dim: impl Into<Dimension>) -> Atom {
43    spenso_lor(tag, ind, dim).to_symbolic(None).unwrap()
44}
45
46/// Cut-indexed factorized integrands. UV markers and final tensor replacements
47/// act on these expressions before evaluator construction. Shared tensor and
48/// scalar bodies are retained separately; root maps never multiply or mark them.
49#[derive(Clone, Debug, PartialEq, Eq, Hash, Encode, Decode)]
50#[trait_decode(trait = GammaLoopContext)]
51pub struct Integrands {
52    atoms: BTreeMap<CutCFFIndex, Atom>,
53    numerators: Vec<Arc<FnMapEntry>>,
54    scalars: Vec<Arc<FnMapEntry>>,
55}
56
57impl Integrands {
58    pub fn map<F: FnMut(&Atom) -> Atom>(&self, mut f: F) -> Self {
59        Self {
60            atoms: self.iter().map(|(key, atom)| (*key, f(atom))).collect(),
61            numerators: self.numerators.clone(),
62            scalars: self.scalars.clone(),
63        }
64    }
65
66    pub fn fallible_map<F: FnMut(&Atom) -> Result<Atom>>(&self, mut f: F) -> Result<Self> {
67        Ok(Self {
68            atoms: self
69                .iter()
70                .map(|(key, atom)| Ok((*key, f(atom)?)))
71                .collect::<Result<_>>()?,
72            numerators: self.numerators.clone(),
73            scalars: self.scalars.clone(),
74        })
75    }
76
77    /// Iterate over the factorized expressions passed to evaluators.
78    pub fn iter(&self) -> impl Iterator<Item = (&CutCFFIndex, &Atom)> {
79        self.atoms.iter()
80    }
81
82    pub(crate) fn numerators(&self) -> &[Arc<FnMapEntry>] {
83        &self.numerators
84    }
85
86    pub(crate) fn scalar_symbol() -> Symbol {
87        symbol!("gammalooprs::uv::scalar_coefficient"; Scalar)
88    }
89
90    pub(crate) fn scalar_definitions(&self) -> &[Arc<FnMapEntry>] {
91        &self.scalars
92    }
93
94    /// Scalar bodies contain only arithmetic in formal variables. All physical
95    /// functions, momenta, masses, and energy owners remain in the call arguments,
96    /// where enclosing forest substitutions can still reach them.
97    pub(crate) fn with_scalar_definitions(
98        mut self,
99        scalars: impl IntoIterator<Item = Arc<FnMapEntry>>,
100    ) -> Result<Self> {
101        let head = Self::scalar_symbol();
102        let mut definitions: BTreeMap<Vec<Atom>, Arc<FnMapEntry>> = BTreeMap::new();
103        for entry in scalars {
104            if let Some(existing) = definitions.get(&entry.tags) {
105                if Arc::ptr_eq(existing, &entry) || existing == &entry {
106                    continue;
107                }
108                return Err(eyre!(
109                    "conflicting retained scalar definition for {}",
110                    entry.lhs
111                ));
112            }
113            let parameters = entry
114                .args
115                .iter()
116                .cloned()
117                .map(Atom::from)
118                .collect::<Vec<_>>();
119            if entry.tags.is_empty()
120                || entry.is_alias
121                || entry.tags.iter().any(|tag| tag.contains_symbol(head))
122                || entry.lhs != head.call_args(entry.tags.iter().chain(&parameters))
123                || parameters.iter().enumerate().any(|(index, parameter)| {
124                    !matches!(parameter.as_view(), AtomView::Var(_))
125                        || entry.tags.iter().any(|tag| tag.contains(parameter))
126                        || parameters[..index].contains(parameter)
127                })
128            {
129                return Err(eyre!("invalid retained scalar binding {}", entry.lhs));
130            }
131            let mut invalid = false;
132            entry.rhs.visitor(&mut |view| {
133                let valid = match view {
134                    AtomView::Num(_) | AtomView::Add(_) | AtomView::Mul(_) | AtomView::Pow(_) => {
135                        true
136                    }
137                    AtomView::Var(_) => parameters
138                        .iter()
139                        .any(|parameter| parameter.as_view() == view),
140                    _ => false,
141                };
142                invalid |= !valid;
143                valid
144            });
145            if invalid {
146                return Err(eyre!(
147                    "retained scalar bodies must contain only arithmetic in their formals: {}",
148                    entry.lhs
149                ));
150            }
151            definitions.insert(entry.tags.clone(), entry);
152        }
153        self.scalars = definitions.into_values().collect();
154        Ok(self)
155    }
156
157    /// Prepare bindings once for a complete residue scope. The returned resolver
158    /// shares instantiated scalar bodies across every root and numerator in it.
159    pub(crate) fn scalar_resolver(
160        scalars: impl IntoIterator<Item = Arc<FnMapEntry>>,
161    ) -> Result<impl FnMut(&Atom) -> Result<Atom>> {
162        let validated = Self::from_iter([]).with_scalar_definitions(scalars)?;
163        let mut replacements = BTreeMap::new();
164        for entry in &validated.scalars {
165            replacements
166                .entry(entry.tags.len())
167                .or_insert_with(BTreeMap::new)
168                .insert(
169                    entry.tags.clone(),
170                    (entry.tags.len() + entry.args.len(), entry.replacement()),
171                );
172        }
173        let head = Self::scalar_symbol();
174        let mut cache = BTreeMap::<Atom, Atom>::new();
175        Ok(move |atom: &Atom| -> Result<Atom> {
176            if !atom.contains_symbol(head) {
177                return Ok(atom.clone());
178            }
179            let mut error = None;
180            let result = atom.replace_map_bottom_up(|view, _, out| {
181                let AtomView::Fun(call) = view else {
182                    return;
183                };
184                if call.get_symbol() != head || error.is_some() {
185                    return;
186                }
187                let original = view.to_owned();
188                if let Some(cached) = cache.get(&original) {
189                    **out = cached.clone();
190                    return;
191                }
192                let mut matched = None;
193                for (tag_count, definitions) in &replacements {
194                    let tags = call
195                        .iter()
196                        .take(*tag_count)
197                        .map(|arg| arg.to_owned())
198                        .collect::<Vec<_>>();
199                    if let Some((arity, replacement)) = definitions.get(&tags)
200                        && *arity == call.get_nargs()
201                    {
202                        if matched.is_some() {
203                            error = Some(eyre!("ambiguous retained scalar call {view}"));
204                            return;
205                        }
206                        matched = Some(replacement);
207                    }
208                }
209                let Some(replacement) = matched else {
210                    error = Some(eyre!("unresolved retained scalar call {view}"));
211                    return;
212                };
213                let resolved = view.replace_multiple([replacement]);
214                if resolved.contains_symbol(head) {
215                    error = Some(eyre!("unresolved or cyclic retained scalar call {view}"));
216                    return;
217                }
218                cache.insert(original, resolved.clone());
219                **out = resolved;
220            });
221            if let Some(error) = error {
222                Err(error)
223            } else {
224                Ok(result)
225            }
226        })
227    }
228
229    #[cfg(test)]
230    pub(crate) fn resolve_scalar(&self, atom: &Atom) -> Result<Atom> {
231        Self::scalar_resolver(self.scalars.iter().cloned())?(atom)
232    }
233
234    /// Resolve innermost scalar calls first, without distributing products or
235    /// materializing shared graph numerators. Flat arithmetic definitions cannot
236    /// create new calls; nested calls may occur only in actual arguments.
237    pub(crate) fn resolved_scalars(&self) -> Result<Self> {
238        let mut resolve = Self::scalar_resolver(self.scalars.iter().cloned())?;
239        let mut resolved = self.fallible_map(&mut resolve)?.map_numerators(resolve)?;
240        resolved.scalars.clear();
241        Ok(resolved)
242    }
243
244    /// Replace the complete flat definition store. Equal definitions share one
245    /// entry; a reused call with a different body or binding is an error.
246    pub(crate) fn with_numerators(
247        mut self,
248        numerators: impl IntoIterator<Item = Arc<FnMapEntry>>,
249    ) -> Result<Self> {
250        let family = symbol!("gammalooprs::uv::numerator_family");
251        let mut definitions: BTreeMap<Vec<Atom>, Arc<FnMapEntry>> = BTreeMap::new();
252        for numerator in numerators {
253            let parameters = numerator
254                .args
255                .iter()
256                .cloned()
257                .map(Atom::from)
258                .collect::<Vec<_>>();
259            if numerator.tags.is_empty()
260                || numerator.lhs
261                    != family.call_args(
262                        numerator
263                            .tags
264                            .iter()
265                            .cloned()
266                            .chain(parameters.iter().cloned()),
267                    )
268                // Sequential formal substitution must not capture a fixed tag
269                // or alter another formal key before that key is substituted.
270                || parameters.iter().enumerate().any(|(index, parameter)| {
271                    numerator.tags.iter().any(|tag| tag.contains(parameter))
272                        || parameters[..index].iter().any(|other| {
273                            other.contains(parameter) || parameter.contains(other)
274                        })
275                })
276            {
277                return Err(eyre!(
278                    "invalid retained numerator binding {}",
279                    numerator.lhs
280                ));
281            }
282            if let Some(existing) = definitions.get(&numerator.tags) {
283                if existing != &numerator {
284                    return Err(eyre!(
285                        "conflicting retained numerator definition for {}",
286                        numerator.lhs
287                    ));
288                }
289                continue;
290            }
291            if numerator.rhs.contains_symbol(family) {
292                return Err(eyre!(
293                    "retained numerator definitions must be flat: {} contains a family call",
294                    numerator.lhs
295                ));
296            }
297            definitions.insert(numerator.tags.clone(), numerator);
298        }
299        self.numerators = definitions.into_values().collect();
300        Ok(self)
301    }
302
303    /// Transform shared bodies explicitly, preserving their call signatures and
304    /// the cut-indexed roots. Unchanged bodies retain their existing Arc.
305    pub(crate) fn map_numerators(
306        &self,
307        mut map: impl FnMut(&Atom) -> Result<Atom>,
308    ) -> Result<Self> {
309        let numerators = self
310            .numerators
311            .iter()
312            .map(|entry| {
313                let rhs = map(&entry.rhs)?;
314                Ok(if rhs == entry.rhs {
315                    Arc::clone(entry)
316                } else {
317                    Arc::new(FnMapEntry {
318                        rhs,
319                        ..entry.as_ref().clone()
320                    })
321                })
322            })
323            .collect::<Result<Vec<_>>>()?;
324        self.clone().with_numerators(numerators)
325    }
326
327    /// Resolve scalar arguments and bodies before materializing tensor families.
328    /// A single numerator substitution pass then suffices for its flat store;
329    /// unresolved or cyclic calls must not escape into a semantic view.
330    pub(crate) fn resolved(&self) -> Result<Self> {
331        let validated = self.resolved_scalars()?;
332        let replacements = validated
333            .numerators
334            .iter()
335            .map(|entry| entry.replacement())
336            .collect::<Vec<_>>();
337        let mut resolved = validated.map(|atom| atom.replace_multiple(&replacements));
338        let family = symbol!("gammalooprs::uv::numerator_family");
339        if resolved
340            .iter()
341            .any(|(_, atom)| atom.contains_symbol(family))
342        {
343            return Err(eyre!("unresolved or cyclic retained numerator family call"));
344        }
345        resolved.numerators.clear();
346        Ok(resolved)
347    }
348
349    pub(crate) fn zero_like(&self) -> Self {
350        self.map(|_| Atom::Zero)
351    }
352
353    pub fn checked_zip(
354        &self,
355        other: &Integrands,
356        mut map: impl FnMut(&CutCFFIndex, &Atom, &Atom) -> Result<Atom>,
357    ) -> Result<Integrands> {
358        let atoms = self
359            .iter()
360            .merge_join_by(other.iter(), |(left_key, _), (right_key, _)| {
361                left_key.cmp(right_key)
362            })
363            .map(|pair| match pair {
364                EitherOrBoth::Both((key, left), (_, right)) => Ok((*key, map(key, left, right)?)),
365                EitherOrBoth::Left((key, _)) => {
366                    Err(eyre!("right integrands are missing key {key:?}"))
367                }
368                EitherOrBoth::Right((key, _)) => {
369                    Err(eyre!("left integrands are missing key {key:?}"))
370                }
371            })
372            .collect::<Result<_>>()?;
373        Self {
374            atoms,
375            numerators: Vec::new(),
376            scalars: Vec::new(),
377        }
378        .with_numerators(self.numerators.iter().chain(&other.numerators).cloned())?
379        .with_scalar_definitions(self.scalars.iter().chain(&other.scalars).cloned())
380    }
381
382    pub fn zip_mul(&self, other: &Integrands) -> Result<Integrands> {
383        self.checked_zip(other, |_, v1, v2| Ok(v1 * v2))
384    }
385
386    /// Validate every cut-key shape, then merge each symbolic sum once. Pairwise
387    /// accumulation repeatedly copies the already assembled residue numerator.
388    pub fn zip_add(self, others: impl IntoIterator<Item = Self>) -> Result<Self> {
389        let mut numerators = self.numerators;
390        let mut scalars = self.scalars;
391        let mut terms = self
392            .atoms
393            .into_iter()
394            .map(|(key, atom)| (key, vec![atom]))
395            .collect::<BTreeMap<_, _>>();
396        for other in others {
397            numerators.extend(other.numerators);
398            scalars.extend(other.scalars);
399            for pair in terms
400                .iter_mut()
401                .merge_join_by(other.atoms, |(left, _), (right, _)| (*left).cmp(right))
402            {
403                match pair {
404                    EitherOrBoth::Both((_, terms), (_, atom)) => terms.push(atom),
405                    EitherOrBoth::Left((key, _)) => {
406                        return Err(eyre!("right integrands are missing key {key:?}"));
407                    }
408                    EitherOrBoth::Right((key, _)) => {
409                        return Err(eyre!("left integrands are missing key {key:?}"));
410                    }
411                }
412            }
413        }
414        Self {
415            atoms: terms
416                .into_iter()
417                .map(|(key, terms)| (key, Atom::add_many(terms)))
418                .collect(),
419            numerators: Vec::new(),
420            scalars: Vec::new(),
421        }
422        .with_numerators(numerators)?
423        .with_scalar_definitions(scalars)
424    }
425}
426
427impl FromIterator<(CutCFFIndex, Atom)> for Integrands {
428    fn from_iter<I: IntoIterator<Item = (CutCFFIndex, Atom)>>(iter: I) -> Self {
429        Self {
430            atoms: BTreeMap::from_iter(iter),
431            numerators: Vec::new(),
432            scalars: Vec::new(),
433        }
434    }
435}
436
437impl Mul<Atom> for Integrands {
438    type Output = Self;
439
440    fn mul(self, rhs: Atom) -> Self::Output {
441        self.map(|a| a * &rhs)
442    }
443}
444
445impl Neg for Integrands {
446    type Output = Self;
447
448    fn neg(self) -> Self::Output {
449        self.map(|a| a.neg())
450    }
451}
452
453impl Mul<&Atom> for Integrands {
454    type Output = Self;
455
456    fn mul(self, rhs: &Atom) -> Self::Output {
457        self.map(|a| a * rhs)
458    }
459}
460
461impl Rooted for Integrands {
462    fn root() -> Self {
463        [(CutCFFIndex::new_all_none(), Atom::num(1))]
464            .into_iter()
465            .collect()
466    }
467}
468
469#[allow(dead_code)]
470pub(crate) fn is_not_paired(pair: &HedgePair) -> bool {
471    !pair.is_paired()
472}
473
474pub mod hedge_poset;
475mod marker;
476mod orchestrator;
477#[cfg(test)]
478pub(crate) mod overlap_control;
479pub mod renormalization;
480pub use renormalization::{RenormalizationPart, RenormalizationStats};
481pub mod settings;
482pub use settings::{
483    ApproximationType, CTIdentifier, CTRenormalizationRule, RenormalizationPrescriptionSettings,
484    UVOrchestrator, UVgenerationSettings,
485};
486pub mod uv_graph;
487pub use uv_graph::UltravioletGraph;
488
489pub mod spinney;
490pub use spinney::Spinney;
491
492pub mod poset;
493pub use poset::Poset;
494
495pub mod wood;
496pub use wood::Wood;
497
498pub mod approx;
499
500pub mod export;
501
502pub mod forest;
503pub use forest::Forest;
504
505pub mod profile;
506pub use profile::{UVProfile, UVProfileAnalysis, UVProfilePassFail};
507
508#[cfg(test)]
509mod tests;