Skip to main content

gammalooprs/uv/approx/
integrated.rs

1use color_eyre::Result;
2use eyre::{WrapErr, eyre};
3use gammaloop_tracing_filter::debug_instrument;
4use idenso::{
5    color::ColorSimplifier,
6    dirac::{GammaSimplifier, GammaSimplifySettings},
7    representations::Bispinor,
8    shorthands::{
9        UndoShorthands,
10        chain::Chain,
11        metric::MetricSimplifier,
12        schoonschip::{Schoonschip, SchoonschipSettings},
13    },
14};
15
16use linnet::half_edge::{
17    HedgeGraph, NodeIndex,
18    builder::HedgeGraphBuilder,
19    involution::HedgePair,
20    subgraph::{ModifySubSet, SuBitGraph, SubGraphLike, SubSetLike},
21};
22use spenso::{
23    network::{library::symbolic::ETS, tags::SPENSO_TAG},
24    shadowing::TensorCollectExt,
25    structure::representation::{Minkowski, RepName},
26};
27use symbolica::{
28    atom::{Atom, AtomCore},
29    domains::atom::AtomField,
30    function,
31    id::Replacement,
32    parse, parse_lit,
33    poly::series::Series,
34    solve::SolveError,
35};
36use symbolica_utils::ReplaceBuilderExt;
37use vakint::{Vakint, VakintExpression, vakint_symbol};
38
39use crate::{
40    debug_tags,
41    graph::LMBext,
42    numerator::aind::Aind,
43    utils::{GS, W_},
44    uv::{
45        ApproximationType, UltravioletGraph,
46        approx::{ForestNodeLike, Rooted, UVCtx, local_4d::Local4dCts},
47        marker::{UvMarker, UvOperation},
48        settings::VakintSettings,
49        uv_graph::UVE,
50    },
51};
52
53/// Laurent projections of an integrated counterterm.
54///
55/// Connected values store their canonical expansion. Factorized values encode
56/// componentwise pole and signed nonnegative-power products under the same accessors.
57#[derive(Clone, Debug, PartialEq, Eq, Hash)]
58pub(crate) struct IntegratedCts {
59    expansion: Series<AtomField>,
60    scale_power: i64,
61}
62
63impl IntegratedCts {
64    pub(crate) fn factorized_product<'a>(
65        factors: impl IntoIterator<Item = &'a Self>,
66        depth: usize,
67    ) -> Result<Self> {
68        let mut factors = factors.into_iter();
69        let first = factors
70            .next()
71            .ok_or_else(|| eyre!("a factorized integrated counterterm cannot be empty"))?;
72        let mut pole = truncate(&first.expansion, false);
73        let mut finite_counterterm = -truncate(&first.expansion, true);
74        let mut scale_power = first.scale_power;
75
76        for factor in factors {
77            pole *= truncate(&factor.expansion, false);
78            finite_counterterm *= -truncate(&factor.expansion, true);
79            scale_power += factor.scale_power;
80        }
81
82        Ok(Self {
83            // The public projections add the finite-counterterm sign, so store
84            // its negative while keeping the pole product unchanged.
85            expansion: series(&(pole - finite_counterterm), depth)?,
86            scale_power,
87        })
88    }
89
90    fn projected_atom(&self, finite: bool) -> Atom {
91        truncate(&self.expansion, finite)
92            * Atom::var(GS.integrated_loop_scale).pow(self.scale_power)
93    }
94
95    pub(crate) fn pole_atom(&self) -> Atom {
96        self.projected_atom(false)
97    }
98
99    pub(crate) fn finite_counterterm_atom(&self) -> Atom {
100        -self.projected_atom(true)
101    }
102
103    pub(crate) fn physical_pole_atom(&self) -> Atom {
104        self.pole_atom()
105            .replace(GS.integrated_loop_scale)
106            .with(Atom::one())
107    }
108
109    pub(crate) fn physical_finite_counterterm_atom(&self) -> Atom {
110        self.finite_counterterm_atom()
111            .replace(GS.integrated_loop_scale)
112            .with(Atom::one())
113    }
114}
115
116fn series(expr: &Atom, depth: usize) -> Result<Series<AtomField>> {
117    Ok(expr.series(GS.dim_epsilon, 0, depth)?)
118}
119
120fn truncate(series: &Series<AtomField>, finite: bool) -> Atom {
121    let mut truncated = Atom::Zero;
122
123    for (power, p) in series.terms() {
124        if (power >= 0) == finite {
125            truncated += p * Atom::var(GS.dim_epsilon).pow(power);
126        }
127    }
128
129    truncated
130}
131
132impl Rooted for IntegratedCts {
133    fn root() -> Self {
134        Self {
135            expansion: series(&Atom::Zero, 1).expect("zero has a Laurent expansion"),
136            scale_power: 0,
137        }
138    }
139}
140
141fn simplify(integrand: &Atom) -> Result<Atom> {
142    let collected = integrand
143        .collect_rep(Minkowski {}.into())
144        .simplify_metrics()
145        .collect_rep((Bispinor {}).into())
146        .collect_gamma_chains();
147    debug_tags!(#uv,#integrated,#collect;log.expr = collected, "After gamma chain collection");
148
149    let schoonschip = collected
150        .schoonschip_with_settings(&SchoonschipSettings {
151            simplify_chain_like_functions: true,
152            schoonschip_rank1_tensors: true,
153            ..Default::default()
154        })
155        .normalize_chains();
156    debug_tags!(#uv, #integrated, #profile, #trace, #start, #collect;
157        log.expr = schoonschip,
158        "After gamma schoonschip"
159    );
160    let collected = schoonschip
161        .collect_chains_and_traces()
162        .simplify_metrics()
163        .collect_gamma_chains()
164        .collect_color()
165        .collect_factors();
166    debug_tags!(#uv, #integrated, #profile, #trace, #start, #collect;
167        log.expr = collected,
168        "After gamma collection"
169    );
170
171    let simplified = collected
172        .simplify_gamma_with(GammaSimplifySettings::canonical())
173        .collect_rep(Minkowski {}.into())
174        .expand_num();
175    debug_tags!(#uv, #integrated, #vakint, #profile, #trace, #start, #gamma;
176        log.expr = simplified,
177        "After gamma simplification"
178    );
179    let schoonschipped = simplified.schoonschip_net::<Aind>();
180    debug_tags!(#uv, #integrated, #vakint, #profile, #trace,#schoonschip, #start;
181        log.expr = schoonschipped,
182        "After Schoonschip net"
183    );
184    let dotted = schoonschipped.to_dots().normalize_dots();
185    debug_tags!(#uv, #integrated, #vakint, #profile, #trace, #dots;
186        log.expr = dotted,
187        "After dots"
188    );
189
190    Ok(dotted)
191}
192
193pub(crate) struct Integrated<'a> {
194    pub vakint: &'a Vakint,
195    pub vakint_settings: &'a vakint::VakintSettings,
196}
197
198impl Integrated<'_> {
199    pub(crate) fn new<'a>(
200        vakint: &'a Vakint,
201        vakint_settings: &'a vakint::VakintSettings,
202    ) -> Integrated<'a> {
203        Integrated {
204            vakint,
205            vakint_settings,
206        }
207    }
208
209    pub(crate) fn run<S: super::ForestNodeLike, M: super::ForestNodeLike>(
210        &self,
211        integrand: &Local4dCts,
212        ctx: &UVCtx<'_>,
213        current: &S,
214        given: &S,
215        marker_current: &M,
216        marker_given: &M,
217    ) -> Result<IntegratedCts> {
218        let graph = ctx.graph;
219
220        let n_loops = graph.n_loops(current.subgraph());
221
222        let scheme = current.renormalization_scheme();
223        match scheme {
224            ApproximationType::MUV | ApproximationType::PolePart => {
225                let integrand = integrand
226                    .atom()
227                    .replace(GS.integrated_loop_scale)
228                    .with(Atom::one());
229                let simplified = simplify(&integrand)?;
230                let marker = UvMarker::new(ctx.settings);
231                let integrated = marker.apply(
232                    UvOperation::Integrate,
233                    marker_current.subgraph(),
234                    marker_given.subgraph(),
235                    &self.integrate(&simplified, ctx, current, given)?,
236                );
237                let expansion_depth =
238                    usize::try_from(self.vakint_settings.number_of_terms_in_epsilon_expansion)
239                        .wrap_err("Vakint epsilon expansion depth must be nonnegative")?;
240                let expanded = series(&integrated, expansion_depth.max(n_loops + 1))?.map_coeff(
241                    |coefficient| {
242                        marker.apply(
243                            UvOperation::Series,
244                            marker_current.subgraph(),
245                            marker_given.subgraph(),
246                            coefficient,
247                        )
248                    },
249                );
250                let expansion = expanded.map_coeff(|coefficient| {
251                    marker.apply(
252                        UvOperation::Truncate,
253                        marker_current.subgraph(),
254                        marker_given.subgraph(),
255                        coefficient,
256                    )
257                });
258
259                // Retain the consumed loop measures for subsequent UV rescalings. Keep this
260                // marker independent of mUV so enclosing limits still rescale the vacuum mass.
261                Ok(IntegratedCts {
262                    expansion,
263                    scale_power: 4 * n_loops as i64,
264                })
265            }
266            ApproximationType::IR => Err(eyre!("Not yet implemented IR")),
267            ApproximationType::VaccuumLimit => Err(eyre!("Not yet implemented VaccuumLimit")),
268            ApproximationType::OS => Err(eyre!("Not yet implemented OS")),
269            ApproximationType::Unsubtracted => {
270                panic!("should have been kept out of the wood");
271            }
272        }
273    }
274
275    #[debug_instrument(
276        current = %current.log_display(),
277        given = %given.log_display(),
278        reduced,
279    )]
280    fn integrate<S: ForestNodeLike>(
281        &self,
282        integrand: &Atom,
283        ctx: &UVCtx<'_>,
284        current: &S,
285        given: &S,
286    ) -> Result<Atom> {
287        let graph = ctx.graph;
288        let reduced = current.reduced_subgraph(given);
289        let settings = ctx.settings;
290        let reduced_label = reduced.string_label();
291        tracing::Span::current().record("reduced", reduced_label.as_str());
292        debug_tags!(#uv, #integrated, #vakint, #trace, #input;
293            log.integrand = integrand,
294            "Integrating and truncating"
295        );
296        // Match the vacuum result to the unintegrated CFF loop measure through
297        // Vakint's configured per-loop normalization hook. Forest-subtraction
298        // signs are folded into the expression at the integrated CT composition
299        // sites.
300        let mut integrand_vakint = to_vakint_integrand(
301            integrand,
302            graph,
303            current.subgraph(),
304            given.subgraph(),
305            &settings.vakint,
306            true,
307        )?;
308
309        for (term_index, t) in integrand_vakint.0.iter().enumerate() {
310            debug_tags!(#uv,#integrated,#vakint,#trace,#to_vakint;
311                term_index = %term_index,
312                log.integral = t.integral,
313                log.numerator = t.numerator,
314                "Vakint term as input"
315            );
316        }
317        debug_tags!(#uv,#integrated,#vakint;settings = ?&self.vakint_settings,"Vakint args");
318
319        // let mut res = vakint
320        //     .0
321        //     .evaluate(&vakint.1, integrand_vakint.as_view())
322        //     .unwrap();
323
324        integrand_vakint.canonicalize(self.vakint_settings, &self.vakint.topologies, false)?;
325        for (term_index, t) in integrand_vakint.0.iter().enumerate() {
326            debug_tags!(#uv,#integrated,#vakint,#trace,#canonicalize;
327                term_index = %term_index,
328                log.integral = t.integral,
329                log.numerator = t.numerator,
330                "Vakint term after canonicalization"
331            );
332        }
333        integrand_vakint.tensor_reduce(self.vakint, self.vakint_settings)?;
334        for (term_index, t) in integrand_vakint.0.iter().enumerate() {
335            debug_tags!(#uv,#integrated,#vakint,#trace,#tensor_reduce;
336                term_index = %term_index,
337                log.integral = t.integral,
338                log.numerator = t.numerator,
339                "Vakint term after tensor reduction"
340            );
341        }
342        integrand_vakint.evaluate_integral(self.vakint, self.vakint_settings)?;
343        for (term_index, t) in integrand_vakint.0.iter().enumerate() {
344            debug_tags!(#uv,#integrated,#vakint,#trace,#evaluate;
345                term_index = %term_index,
346                log.integral = t.integral,
347                log.numerator = t.numerator,
348                "Vakint term after evaluation"
349            );
350        }
351
352        let mut res: Atom = integrand_vakint.into();
353
354        debug_tags!(#uv,#integrated,#vakint,#trace,#raw;
355            log.res = res,
356            "Raw post vakint "
357        );
358
359        res = res
360            .replace(parse_lit!(vakint::cl2))
361            .with(parse_lit!(cl2))
362            .replace(parse_lit!(vakint::sqrt3))
363            .with(parse_lit!(sqrt(3)));
364
365        let vk_metric = vakint_symbol!("g");
366        let mink = Minkowski {}.new_rep(GS.dim);
367
368        // apply metric
369        res = res
370            .replace(vakint::symbols::S.p.call_args([W_.i_, W_.j_]))
371            .when(W_.j_.filter(|r| r.is_integer()))
372            .with(
373                vakint::symbols::S.p.call_args([
374                    Atom::var(W_.i_),
375                    mink.to_symbolic([GS
376                        .uvaind
377                        .call_args([Atom::num(current.topo_order()), Atom::var(W_.j_)])]),
378                ]),
379            )
380            .replace(
381                vakint::symbols::S
382                    .p
383                    .call_args([Atom::var(W_.i_), vakint::symbols::S.dot_dummy_ind(W_.j_)]),
384            )
385            .when(W_.j_.filter(|r| r.is_integer()))
386            .with(
387                vakint::symbols::S.p.call_args([
388                    Atom::var(W_.i_),
389                    mink.to_symbolic([GS
390                        .uvaind
391                        .call_args([Atom::num(current.topo_order()), Atom::var(W_.j_)])]),
392                ]),
393            )
394            .replace(vakint::symbols::S.p.call_args([W_.x__]))
395            .with(GS.emr_mom.call_args([W_.x__]));
396        res = res
397            .replace(function!(vk_metric, W_.x_, W_.y_) * function!(GS.emr_mom, W_.x___, W_.x_))
398            .with(function!(GS.emr_mom, W_.x___, W_.y_))
399            .replace(function!(
400                vk_metric,
401                vakint::symbols::S.dot_dummy_ind(W_.x_),
402                W_.y_
403            ))
404            .when(W_.x_.filter(|r| r.is_integer()))
405            .with(function!(
406                vk_metric,
407                mink.to_symbolic([GS
408                    .uvaind
409                    .call_args([Atom::num(current.topo_order()), Atom::var(W_.x_)])]),
410                W_.y_
411            ))
412            .replace(function!(
413                vk_metric,
414                W_.x_,
415                vakint::symbols::S.dot_dummy_ind(W_.y_)
416            ))
417            .when(W_.y_.filter(|r| r.is_integer()))
418            .with(function!(
419                vk_metric,
420                mink.to_symbolic([GS
421                    .uvaind
422                    .call_args([Atom::num(current.topo_order()), Atom::var(W_.y_)])]),
423                W_.x_
424            ))
425            .replace(function!(vk_metric, W_.x_, W_.y_))
426            .with(function!(ETS.metric, W_.x_, W_.y_));
427
428        res = res.replace(vakint::symbols::S.cmplx_i).with(Atom::i());
429
430        res = res
431            .simplify_metrics()
432            .metric_shorthand_to_dot()
433            .replace(GS.dim)
434            .max_level(0)
435            .with(Atom::var(GS.dim_epsilon) * (-2) + 4);
436
437        debug_tags!(#uv, #integrated, #vakint, #inspect, #trace, #replace;
438            log.res = res,
439            "Replaced post vakint "
440        );
441
442        // This strips as many dummies as possible after undoing chains and traces,
443        // so that terms can merge later on.
444        let bispinor_rep = Bispinor {}.into();
445        let after_chainify = res.chainify(bispinor_rep);
446        debug_tags!(#uv, #integrated, #vakint, #profile, #trace, #chainify;
447            log.expr = after_chainify,
448            "Integrated UV chain cleanup after chainify"
449        );
450
451        let after_collect_chains = after_chainify.collect_chains(bispinor_rep);
452        debug_tags!(#uv, #integrated, #vakint, #profile, #trace, #collect;
453            log.expr = after_collect_chains,
454            "Integrated UV chain cleanup after collect_chains"
455        );
456
457        res = after_collect_chains.undo_single_length();
458        debug_tags!(#uv, #integrated, #vakint, #profile, #trace, #undo_single_length;
459            log.expr = res,
460            "Integrated UV chain cleanup after undo_single_length"
461        );
462
463        if res
464            .replace(GS.dim)
465            .max_level(0)
466            .match_iter()
467            .next()
468            .is_some()
469        {
470            panic!(
471                "The t_arg should not contain dim after expansion, found {}",
472                res
473            );
474        }
475
476        // println!("\nIntegrated CT:\n{}\n", res);
477        Ok(res)
478    }
479}
480
481#[debug_instrument]
482pub(crate) fn to_vakint_integrand<
483    E: UVE,
484    V,
485    H,
486    S: SubGraphLike + SubSetLike<Base = SuBitGraph>,
487    SS: SubGraphLike,
488>(
489    integrand: &Atom,
490    graph: &HedgeGraph<E, V, H>,
491    reduced: &S,
492    dependent_subgraph: &SS,
493    settings: &VakintSettings,
494    substitute_masses_to_m_uv: bool,
495) -> Result<VakintExpression> {
496    let reduced_label = reduced.string_label();
497    let dependent_subgraph_label = dependent_subgraph.string_label();
498    let mut integrand_vakint = integrand
499        .undo_schoonschip::<Aind>()
500        .undo_chain::<Aind>()
501        .undo_trace::<Aind>();
502    debug_tags!(#uv, #integrated, #vakint, #trace;
503        stage = "to_vakint_integrand_after_undo_shorthands",
504        reduced = %reduced_label,
505        dependent_subgraph = %dependent_subgraph_label,
506        substitute_masses_to_m_uv = substitute_masses_to_m_uv,
507        log.integrand = integrand_vakint,
508        "Vakint trace after undo shorthands"
509    );
510    //Atom::Zero
511
512    // strip the momentum wrapper from the denominator
513    integrand_vakint = integrand_vakint
514        // .replace(function!(
515        //     GS.den,
516        //     W_.prop_,
517        //     function!(GS.emr_mom, W_.prop_, W_.mom_),
518        //     W_.x__
519        // ))
520        // .with(function!(GS.den, W_.prop_, W_.mom_, W_.x__))
521        .expand();
522    debug_tags!(#uv, #integrated, #vakint, #trace;
523        stage = "to_vakint_integrand_after_den_strip_expand",
524        reduced = %reduced_label,
525        dependent_subgraph = %dependent_subgraph_label,
526        log.integrand = integrand_vakint,
527        "Vakint trace after denominator strip and expand"
528    );
529
530    // Nested counterterms can expose a boundary metric next to the propagator
531    // metric of the reduced graph. Contract those metric-only structures before
532    // the expression is split into Vakint terms.
533    integrand_vakint = integrand_vakint.simplify_metrics();
534    debug_tags!(#uv, #integrated, #vakint, #trace;
535        stage = "to_vakint_integrand_after_simplify_metrics",
536        reduced = %reduced_label,
537        dependent_subgraph = %dependent_subgraph_label,
538        log.integrand = integrand_vakint,
539        "Vakint trace after metric simplification"
540    );
541
542    let mut propagator_id = 1;
543
544    let vk_prop = vakint::symbols::S.prop;
545    let vk_edge = vakint_symbol!("edge");
546    let vk_topo = vakint_symbol!("topo");
547
548    // let contracted_nodes: BTreeSet<NodeIndex> = graph
549    //     .iter_nodes_of(dependent_subgraph)
550    //     .map(|(nid, c, v)| nid)
551    //     .collect();
552
553    debug_tags!(#uv, #integrated, #vakint, #graph, #dump;
554        reduced = %graph.dot(reduced),
555        "Den to prop for"
556    );
557    // let first = contracted_nodes.first();
558
559    for (pair, index, _data) in graph.iter_edges_of(reduced) {
560        if let HedgePair::Paired { source, sink } = pair {
561            // let source = if contracted_nodes.contains(&graph.node_id(source)) {
562            //     first.unwrap().clone()
563            // } else {
564            //     graph.node_id(source)
565            // };
566            // let sink = if contracted_nodes.contains(&graph.node_id(sink)) {
567            //     first.unwrap().clone()
568            // } else {
569            //     graph.node_id(sink)
570            // };
571            integrand_vakint = integrand_vakint
572                .replace(function!(
573                    GS.den,
574                    usize::from(index) as i64,
575                    W_.mom_,
576                    W_.mass_,
577                    W_.x___
578                ))
579                .with(function!(
580                    vk_prop,
581                    propagator_id,
582                    function!(
583                        vk_edge,
584                        usize::from(graph.node_id(source)),
585                        usize::from(graph.node_id(sink))
586                    ),
587                    W_.mom_,
588                    if substitute_masses_to_m_uv {
589                        GS.m_uv_vacuum
590                    } else {
591                        W_.mass_
592                    },
593                    1
594                ))
595                .replace(function!(vk_prop, W_.x___, 1).pow(Atom::var(W_.e_)))
596                .with(function!(vk_prop, W_.x___, -Atom::var(W_.e_)));
597            propagator_id += 1;
598        }
599    }
600    debug_tags!(#uv, #integrated, #vakint, #trace;
601        stage = "to_vakint_integrand_after_den_to_prop",
602        reduced = %reduced_label,
603        dependent_subgraph = %dependent_subgraph_label,
604        log.integrand = integrand_vakint,
605        "Vakint trace after denominator-to-propagator conversion"
606    );
607
608    let mut first: Option<NodeIndex> = None;
609
610    debug_tags!(#uv, #integrated, #vakint, #graph, #dump;
611        reduced = %graph.dot(dependent_subgraph),
612        "Shrinking subgraph for vakint"
613    );
614    // shrink vertices of the subgraph
615    for (id, _crown, _data) in graph.iter_nodes_of(dependent_subgraph) {
616        debug_tags!(#uv, #integrated, #vakint, #graph, #inspect;
617            id = %id,
618            "Shrinking Node"
619        );
620
621        if let Some(first) = first {
622            integrand_vakint = integrand_vakint
623                .replace(function!(
624                    vk_prop,
625                    W_.x_,
626                    function!(vk_edge, id.0, W_.y_),
627                    W_.x___
628                ))
629                .with(function!(
630                    vk_prop,
631                    W_.x_,
632                    function!(vk_edge, first.0, W_.y_),
633                    W_.x___
634                ))
635                .replace(function!(
636                    vk_prop,
637                    W_.x_,
638                    function!(vk_edge, W_.y_, id.0),
639                    W_.x___
640                ))
641                .with(function!(
642                    vk_prop,
643                    W_.x_,
644                    function!(vk_edge, W_.y_, first.0),
645                    W_.x___
646                ))
647        } else {
648            first = Some(id);
649        }
650    }
651    debug_tags!(#uv, #integrated, #vakint, #trace;
652        stage = "to_vakint_integrand_after_shrink_subgraph",
653        reduced = %reduced_label,
654        dependent_subgraph = %dependent_subgraph_label,
655        log.integrand = integrand_vakint,
656        "Vakint trace after shrinking subgraph"
657    );
658
659    // flip edges to positive momentum
660    // FIXME: how will this work for sums of momenta?
661    integrand_vakint = integrand_vakint
662        .replace(function!(
663            vk_prop,
664            W_.x_,
665            function!(vk_edge, W_.a_, W_.b_),
666            -Atom::var(W_.y_),
667            W_.e___
668        ))
669        .repeat()
670        .with(function!(
671            vk_prop,
672            W_.x_,
673            function!(vk_edge, W_.b_, W_.a_),
674            W_.y_,
675            W_.e___
676        ));
677
678    // fuse raised edges
679    integrand_vakint = integrand_vakint
680        .replace(
681            function!(
682                vk_prop,
683                W_.x_,
684                function!(vk_edge, W_.a_, W_.b_),
685                W_.x___,
686                W_.e_
687            ) * function!(
688                vk_prop,
689                W_.y_,
690                function!(vk_edge, W_.b_, W_.c_),
691                W_.x___,
692                W_.f_
693            ),
694        )
695        .repeat()
696        .with(function!(
697            vk_prop,
698            W_.y_,
699            function!(vk_edge, W_.a_, W_.c_),
700            W_.x___,
701            W_.e_ + W_.f_
702        ));
703    debug_tags!(#uv, #integrated, #vakint, #trace;
704        stage = "to_vakint_integrand_after_flip_fuse",
705        reduced = %reduced_label,
706        dependent_subgraph = %dependent_subgraph_label,
707        log.integrand = integrand_vakint,
708        "Vakint trace after edge flip and fuse"
709    );
710
711    // println!(
712    //     "Integrand pre vakint: {:}",
713    //     VakintExpression::try_from(
714    //         integrand_vakint
715    //             .replace(function!(vk_prop, W_.x__))
716    //             .with(function!(vk_topo, function!(vk_prop, W_.x__)))
717    //             .replace(function!(vk_topo, W_.x_) * function!(vk_topo, W_.y_))
718    //             .repeat()
719    //             .with(function!(vk_topo, W_.x_ * W_.y_))
720    //     )
721    //     .unwrap()
722    // );
723
724    let vakint_input_atom = integrand_vakint
725        .replace(function!(vk_prop, W_.x__))
726        .with(function!(vk_topo, function!(vk_prop, W_.x__)))
727        .replace(function!(vk_topo, W_.x_) * function!(vk_topo, W_.y_))
728        .repeat()
729        .with(function!(vk_topo, W_.x_ * W_.y_));
730    debug_tags!(#uv, #integrated, #vakint, #trace;
731        stage = "to_vakint_integrand_before_split_terms",
732        reduced = %reduced_label,
733        dependent_subgraph = %dependent_subgraph_label,
734        log.integrand = vakint_input_atom,
735        "Vakint trace before split terms"
736    );
737
738    let mut a = VakintExpression::try_from(vakint_input_atom)
739        .wrap_err("could not split integrand into Vakint terms")?;
740
741    for (term_index, t) in a.0.iter_mut().enumerate() {
742        debug_tags!(#uv, #integrated, #vakint, #inspect, #trace;
743            stage = "to_vakint_integrand_term_initial",
744            term_index = %term_index,
745            reduced = %reduced_label,
746            dependent_subgraph = %dependent_subgraph_label,
747            log.integral = t.integral,
748            log.numerator = t.numerator,
749            "Starting integral"
750        );
751
752        let mut graph = HedgeGraphBuilder::new();
753        //prop(<id>, edge(<node_source>,<node_sink>), <mom>, <mass>, <power>)
754        let pat = function!(
755            vk_prop,
756            W_.a_,
757            function!(vakint::symbols::S.edge, W_.i_, W_.j_),
758            W_.c_,
759            W_.d_,
760            W_.e_
761        )
762        .to_pattern();
763        let mut nodemap = std::collections::HashMap::new();
764
765        struct ContractibleEdge {
766            mom: Atom,
767            mass: Atom,
768            power: i32,
769        }
770
771        for m in t.integral.pattern_match(&pat, None, None) {
772            let i: usize = m[&W_.i_].as_view().try_into().unwrap();
773            let j: usize = m[&W_.j_].as_view().try_into().unwrap();
774            nodemap.entry(i).or_insert_with(|| graph.add_node(()));
775            nodemap.entry(j).or_insert_with(|| graph.add_node(()));
776
777            graph.add_edge(
778                nodemap[&i],
779                nodemap[&j],
780                ContractibleEdge {
781                    mom: m[&W_.c_].clone(),
782                    mass: m[&W_.d_].clone(),
783                    power: m[&W_.e_].as_view().try_into().unwrap(),
784                },
785                false,
786            );
787        }
788
789        let mut system = vec![];
790        let mut momentum_variables = vec![];
791
792        let mut graph: HedgeGraph<ContractibleEdge, ()> = graph.build();
793        let uncontracted_propagator_count =
794            graph.iter_edges().filter(|(p, _, _)| p.is_paired()).count();
795        let uncontracted_propagator_power_sum = graph
796            .iter_edges()
797            .filter_map(|(p, _, e)| p.is_paired().then_some(e.data.power))
798            .sum::<i32>();
799
800        while let Some(same_mass_two_bond) = graph.a_bond(&|c| {
801            let mut count = 0;
802            let mut mass = None;
803            for (_, _, d) in graph.iter_edges_of(c) {
804                count += 1;
805
806                if let Some(m) = &mass
807                    && m != &d.data.mass
808                {
809                    return false;
810                } else {
811                    mass = Some(d.data.mass.clone());
812                }
813                if count > 2 {
814                    return false;
815                }
816            }
817            count == 2
818        }) {
819            let mut iter = same_mass_two_bond.included_iter();
820            let first = graph[&iter.next().unwrap()];
821            let second = graph[&iter.next().unwrap()];
822            graph[first].power += graph[second].power;
823            let mut to_contract: SuBitGraph = graph.empty_subgraph();
824            to_contract.add(graph[&second].1);
825            graph.contract_subgraph(&to_contract, ());
826        }
827        let mut nodes_to_merge = vec![];
828
829        for c in graph.connected_components(&graph.full_filter()) {
830            let Some((nid, _, _)) = graph.iter_nodes_of(&c).next() else {
831                continue;
832            };
833            nodes_to_merge.push(nid);
834        }
835
836        if !nodes_to_merge.is_empty() {
837            graph.identify_nodes(&nodes_to_merge, ());
838        }
839
840        graph.forget_identification_history();
841        debug_tags!(#uv, #integrated, #vakint, #graph, #dump;
842            log.graph = %graph.base_dot(),
843            "Graph"
844        );
845
846        let mut new_integral: Atom = 1.into();
847        for (p, eid, e) in graph.iter_edges() {
848            let HedgePair::Paired { source, sink } = p else {
849                continue;
850            };
851            new_integral *= function!(
852                vk_prop,
853                eid.0 + 1, //vakint propagator ids are 1-indexed
854                function!(
855                    vakint::symbols::S.edge,
856                    graph.node_id(source).0,
857                    graph.node_id(sink).0
858                ),
859                &e.data.mom,
860                &e.data
861                    .mass
862                    .pow(2)
863                    .replace(GS.m_uv_expansion)
864                    .with(GS.m_uv_vacuum),
865                e.data.power
866            )
867        }
868
869        // println!("{}->{}", t.integral, new_integral);
870        t.integral = function!(vakint::symbols::S.topo, new_integral);
871        let nloops = graph.cyclotomatic_number(&graph.full_filter());
872        let contracted_propagator_count =
873            graph.iter_edges().filter(|(p, _, _)| p.is_paired()).count();
874        let contracted_propagator_power_sum = graph
875            .iter_edges()
876            .filter_map(|(p, _, e)| p.is_paired().then_some(e.data.power))
877            .sum::<i32>();
878        debug_tags!(#uv, #integrated, #vakint, #trace;
879            stage = "to_vakint_integrand_term_after_graph_rebuild",
880            term_index = %term_index,
881            reduced = %reduced_label,
882            dependent_subgraph = %dependent_subgraph_label,
883            nloops = nloops,
884            uncontracted_propagator_count = uncontracted_propagator_count,
885            uncontracted_propagator_power_sum = uncontracted_propagator_power_sum,
886            contracted_propagator_count = contracted_propagator_count,
887            contracted_propagator_power_sum = contracted_propagator_power_sum,
888            log.integral = t.integral,
889            log.numerator = t.numerator,
890            "Vakint trace"
891        );
892
893        let lmb = graph.lmb();
894        let mom_pat = function!(GS.emr_mom, W_.a_).to_pattern();
895        for (p, e, ed) in graph.iter_edges() {
896            if p.is_paired() {
897                // println!("{e}");
898                let loop_expr = lmb.loop_atom::<Atom>(e, GS.loop_mom, &[], false);
899
900                ed.data
901                    .mom
902                    .pattern_match(&mom_pat, None, None)
903                    .for_each(|m| {
904                        let var = mom_pat.replace_wildcards(&m).unwrap();
905                        if !momentum_variables.iter().any(|existing| existing == &var) {
906                            momentum_variables.push(var);
907                        }
908                    });
909
910                // println!("{loop_expr}");
911
912                // println!("{external_expr}");
913                let is_zero = &ed.data.mom - loop_expr;
914                // println!("Momentum check for edge {}: {}", e, is_zero);
915                system.push(is_zero);
916            }
917        }
918
919        let add_additional_args = [
920            Replacement::new(
921                function!(GS.emr_mom, W_.i_).to_pattern(),
922                function!(GS.emr_mom, W_.i_, W_.a___),
923            )
924            .allow_new_wildcards_on_rhs(true),
925            Replacement::new(
926                function!(GS.loop_mom, W_.i_).to_pattern(),
927                function!(GS.loop_mom, W_.i_, W_.a___),
928            )
929            .allow_new_wildcards_on_rhs(true),
930        ];
931        let momentum_solution =
932            VakintMomentumSolution::solve(&system, &momentum_variables, &add_additional_args)
933                .wrap_err("could not solve momentum system for Vakint integrand")?;
934        t.numerator = momentum_solution.rewrite_numerator(&t.numerator);
935        t.integral = momentum_solution.rewrite_integral(&t.integral, &add_additional_args);
936        momentum_solution.ensure_free_variables_eliminated(
937            term_index,
938            "integral",
939            &t.integral,
940            &add_additional_args,
941        )?;
942        debug_tags!(#uv, #integrated, #vakint, #trace;
943            stage = "to_vakint_integrand_term_after_momentum_solve",
944            term_index = %term_index,
945            reduced = %reduced_label,
946            dependent_subgraph = %dependent_subgraph_label,
947            log.integral = t.integral,
948            log.numerator = t.numerator,
949            "Vakint trace"
950        );
951
952        // debug!(
953        //     "Graph from vakint expression:\n{}\n{}",
954        //     graph.dot_lmb_of(&graph.full_filter(), &graph.lmb()),
955        //     term
956        // );
957
958        let additional_normalization = parse!(&settings.additional_normalization);
959        t.numerator *= additional_normalization.clone().pow(nloops);
960        debug_tags!(#uv, #integrated, #vakint, #trace;
961            stage = "to_vakint_integrand_term_after_loop_normalization",
962            term_index = %term_index,
963            reduced = %reduced_label,
964            dependent_subgraph = %dependent_subgraph_label,
965            nloops = nloops,
966            log.additional_normalization = additional_normalization,
967            log.numerator = t.numerator,
968            "Vakint trace after loop normalization"
969        );
970
971        // Vakint needs explicit tensor indices; only translate metric shorthands
972        // to dot notation here, without reintroducing Schoonschip rank-1 factors.
973        t.numerator = t.numerator.metric_shorthand_to_dot();
974        debug_tags!(#uv, #integrated, #vakint, #trace;
975            stage = "to_vakint_integrand_term_after_metric_shorthand_to_dot",
976            term_index = %term_index,
977            reduced = %reduced_label,
978            dependent_subgraph = %dependent_subgraph_label,
979            log.integral = t.integral,
980            log.numerator = t.numerator,
981            "Vakint trace"
982        );
983        t.integral = t
984            .integral
985            .replace(function!(GS.loop_mom, W_.x___))
986            .with(function!(vakint::symbols::S.k, W_.x___))
987            .replace(function!(GS.emr_mom, W_.x___))
988            .with(function!(vakint::symbols::S.p, W_.x___));
989        t.numerator = t
990            .numerator
991            .replace(function!(GS.loop_mom, W_.x___))
992            .with(function!(vakint::symbols::S.k, W_.x___))
993            .replace(function!(GS.emr_mom, W_.x___))
994            .with(function!(vakint::symbols::S.p, W_.x___))
995            .replace(function!(
996                SPENSO_TAG.dot,
997                function!(W_.a_, W_.a___, Minkowski {}.new_rep(GS.dim).to_symbolic([])),
998                function!(W_.b_, W_.b___, Minkowski {}.new_rep(GS.dim).to_symbolic([]))
999            ))
1000            .with(vakint::symbols::S.dot(function!(W_.a_, W_.a___), function!(W_.b_, W_.b___)))
1001            .replace(function!(
1002                ETS.metric,
1003                Minkowski {}.to_symbolic([W_.a__]),
1004                Minkowski {}.to_symbolic([W_.b__])
1005            ))
1006            .with(function!(
1007                vakint::symbols::S.metric,
1008                Minkowski {}.to_symbolic([W_.a__]),
1009                Minkowski {}.to_symbolic([W_.b__])
1010            ));
1011        debug_tags!(#uv, #integrated, #vakint, #trace;
1012            stage = "to_vakint_integrand_term_after_vakint_symbols",
1013            term_index = %term_index,
1014            reduced = %reduced_label,
1015            dependent_subgraph = %dependent_subgraph_label,
1016            log.integral = t.integral,
1017            log.numerator = t.numerator,
1018            "Vakint trace"
1019        );
1020    }
1021
1022    Ok(a)
1023}
1024
1025struct VakintMomentumSolution {
1026    replacements: Vec<Replacement>,
1027    free_variables: Vec<Atom>,
1028}
1029
1030impl VakintMomentumSolution {
1031    fn solve(
1032        system: &[Atom],
1033        variables: &[Atom],
1034        add_additional_args: &[Replacement],
1035    ) -> Result<Self> {
1036        if variables.is_empty() {
1037            return Ok(Self {
1038                replacements: vec![],
1039                free_variables: vec![],
1040            });
1041        }
1042        if system.is_empty() {
1043            return Ok(Self {
1044                replacements: vec![],
1045                free_variables: variables.to_vec(),
1046            });
1047        }
1048
1049        match Atom::solve_linear_system::<u8, _, _>(system, variables) {
1050            Ok(solution) => Ok(Self::from_solution(
1051                &solution,
1052                variables,
1053                add_additional_args,
1054            )),
1055            Err(SolveError::Underdetermined {
1056                partial_solution, ..
1057            }) => Ok(Self::from_solution(
1058                &partial_solution,
1059                variables,
1060                add_additional_args,
1061            )),
1062            Err(source) => Err(eyre!("{source}")),
1063        }
1064    }
1065
1066    fn from_solution(
1067        solution: &[Atom],
1068        variables: &[Atom],
1069        add_additional_args: &[Replacement],
1070    ) -> Self {
1071        debug_assert_eq!(solution.len(), variables.len());
1072        let mut replacements = vec![];
1073        let mut free_variables = vec![];
1074
1075        for index in (0..variables.len()).rev() {
1076            let replacement = &solution[index];
1077            let variable = &variables[index];
1078            let replacement = replacement.replace_multiple(&replacements);
1079            if replacement == variable {
1080                free_variables.push(variable.clone());
1081            } else {
1082                replacements.push(Replacement::new(
1083                    variable.replace_multiple(add_additional_args).to_pattern(),
1084                    replacement
1085                        .replace_multiple(add_additional_args)
1086                        .to_pattern(),
1087                ));
1088            }
1089        }
1090
1091        Self {
1092            replacements,
1093            free_variables,
1094        }
1095    }
1096
1097    fn ensure_free_variables_eliminated(
1098        &self,
1099        term_index: usize,
1100        expression_kind: &str,
1101        expression: &Atom,
1102        add_additional_args: &[Replacement],
1103    ) -> Result<()> {
1104        self.ensure_variables_eliminated(
1105            term_index,
1106            expression_kind,
1107            expression,
1108            self.free_variables.iter(),
1109            add_additional_args,
1110        )
1111    }
1112
1113    fn ensure_variables_eliminated<'a>(
1114        &self,
1115        term_index: usize,
1116        expression_kind: &str,
1117        expression: &Atom,
1118        variables: impl IntoIterator<Item = &'a Atom>,
1119        add_additional_args: &[Replacement],
1120    ) -> Result<()> {
1121        for variable in variables {
1122            let bare_pattern = variable.to_pattern();
1123            let indexed_pattern = variable.replace_multiple(add_additional_args).to_pattern();
1124            if expression
1125                .pattern_match(&bare_pattern, None, None)
1126                .next()
1127                .is_some()
1128                || expression
1129                    .pattern_match(&indexed_pattern, None, None)
1130                    .next()
1131                    .is_some()
1132            {
1133                return Err(eyre!(
1134                    "Underdetermined Vakint momentum solve left free variable {} in {} of term {}",
1135                    variable,
1136                    expression_kind,
1137                    term_index
1138                ));
1139            }
1140        }
1141        Ok(())
1142    }
1143
1144    fn rewrite_numerator(&self, numerator: &Atom) -> Atom {
1145        numerator
1146            .replace_multiple(&self.replacements)
1147            .normalize_dots()
1148    }
1149
1150    fn rewrite_integral(&self, integral: &Atom, add_additional_args: &[Replacement]) -> Atom {
1151        let free_variable_zero_replacements =
1152            self.free_variable_zero_replacements(add_additional_args);
1153        Self::normalize_topology_momenta(
1154            integral
1155                .replace_multiple(&self.replacements)
1156                .replace_multiple(&free_variable_zero_replacements),
1157        )
1158    }
1159
1160    fn free_variable_zero_replacements(
1161        &self,
1162        add_additional_args: &[Replacement],
1163    ) -> Vec<Replacement> {
1164        let mut replacements = Vec::with_capacity(2 * self.free_variables.len());
1165        for variable in &self.free_variables {
1166            replacements.push(Replacement::new(variable.to_pattern(), Atom::Zero));
1167            let indexed_variable = variable.replace_multiple(add_additional_args);
1168            if indexed_variable != *variable {
1169                replacements.push(Replacement::new(indexed_variable.to_pattern(), Atom::Zero));
1170            }
1171        }
1172        replacements
1173    }
1174
1175    fn normalize_topology_momenta(expression: Atom) -> Atom {
1176        if !expression
1177            .replace(function!(vakint::symbols::S.topo, W_.x_))
1178            .matches()
1179        {
1180            return expression;
1181        }
1182
1183        expression
1184            .replace(function!(
1185                vakint::symbols::S.prop,
1186                W_.edgeid_,
1187                W_.x_,
1188                W_.mom_,
1189                W_.mass_,
1190                W_.prop_
1191            ))
1192            .with_map(|matches| {
1193                function!(
1194                    vakint::symbols::S.prop,
1195                    matches.get(W_.edgeid_).unwrap().to_atom(),
1196                    matches.get(W_.x_).unwrap().to_atom(),
1197                    matches.get(W_.mom_).unwrap().to_atom().expand(),
1198                    matches.get(W_.mass_).unwrap().to_atom(),
1199                    matches.get(W_.prop_).unwrap().to_atom()
1200                )
1201            })
1202    }
1203}
1204
1205#[cfg(test)]
1206mod tests {
1207    use crate::initialisation::test_initialise;
1208
1209    use super::*;
1210
1211    #[test]
1212    fn integrated_counterterm_projects_one_laurent_expansion() {
1213        test_initialise().unwrap();
1214
1215        let epsilon = Atom::var(GS.dim_epsilon);
1216        let expansion = series(
1217            &(Atom::num(2) * epsilon.pow(-2)
1218                + Atom::num(3) * epsilon.pow(-1)
1219                + Atom::num(5)
1220                + Atom::num(7) * &epsilon),
1221            2,
1222        )
1223        .unwrap();
1224        let integrated = IntegratedCts {
1225            expansion,
1226            scale_power: 4,
1227        };
1228        let scale = Atom::var(GS.integrated_loop_scale).pow(4);
1229
1230        assert_eq!(
1231            integrated.pole_atom().expand(),
1232            ((Atom::num(2) * epsilon.pow(-2) + Atom::num(3) * epsilon.pow(-1)) * &scale).expand()
1233        );
1234        assert_eq!(
1235            integrated.finite_counterterm_atom().expand(),
1236            (-(Atom::num(5) + Atom::num(7) * epsilon) * scale).expand()
1237        );
1238        assert_eq!(
1239            integrated.physical_finite_counterterm_atom(),
1240            -(Atom::num(5) + Atom::num(7) * Atom::var(GS.dim_epsilon))
1241        );
1242    }
1243
1244    #[test]
1245    fn factorized_product_projects_each_component() {
1246        test_initialise().unwrap();
1247
1248        let epsilon = Atom::var(GS.dim_epsilon);
1249        let factors = [2, 3, 5].map(|finite| IntegratedCts {
1250            expansion: series(&(epsilon.pow(-1) + Atom::num(finite)), 1).unwrap(),
1251            scale_power: 0,
1252        });
1253        let product = IntegratedCts::factorized_product(&factors[..2], 1).unwrap();
1254
1255        assert_eq!(product.physical_pole_atom(), epsilon.pow(-2));
1256        assert_eq!(product.physical_finite_counterterm_atom(), Atom::num(6));
1257
1258        let product = IntegratedCts::factorized_product(&factors, 1).unwrap();
1259        assert_eq!(product.physical_pole_atom(), epsilon.pow(-3));
1260        assert_eq!(product.physical_finite_counterterm_atom(), Atom::num(-30));
1261    }
1262
1263    // #[test]
1264    // fn integrated_triangle_norm_is_euclidean() {
1265    //     test_initialise().unwrap();
1266
1267    //     let edge = EdgeIndex(7);
1268    //     let euclidean_norm = GS.emr_mom(edge, GS.cind(1)).pow(2)
1269    //         + GS.emr_mom(edge, GS.cind(2)).pow(2)
1270    //         + GS.emr_mom(edge, GS.cind(3)).pow(2);
1271    //     let minkowski_norm = Minkowski {}
1272    //         .new_rep(4)
1273    //         .inner_product(GS.emr_vec(edge), GS.emr_vec(edge));
1274
1275    //     assert_eq!(
1276    //         euclidean_norm,
1277    //         GS.emr_mom(edge, GS.cind(1)).pow(2)
1278    //             + GS.emr_mom(edge, GS.cind(2)).pow(2)
1279    //             + GS.emr_mom(edge, GS.cind(3)).pow(2)
1280    //     );
1281    //     assert_ne!(euclidean_norm, minkowski_norm);
1282    // }
1283
1284    #[test]
1285    fn vakint_dot_conversion_keeps_loop_momentum_tagged_until_to_dots() {
1286        test_initialise().unwrap();
1287
1288        let mink = Minkowski {}.new_rep(GS.dim).to_symbolic([]);
1289        let numerator = function!(
1290            ETS.metric,
1291            function!(GS.emr_mom, 0, mink.clone()),
1292            function!(GS.loop_mom, 1, mink.clone())
1293        );
1294
1295        let converted = numerator
1296            .simplify_metrics()
1297            .to_dots()
1298            .replace(function!(GS.loop_mom, W_.x___))
1299            .with(function!(vakint::symbols::S.k, W_.x___))
1300            .replace(function!(GS.emr_mom, W_.x___))
1301            .with(function!(vakint::symbols::S.p, W_.x___))
1302            .replace(function!(
1303                SPENSO_TAG.dot,
1304                function!(W_.a_, W_.a___, Minkowski {}.new_rep(GS.dim).to_symbolic([])),
1305                function!(W_.b_, W_.b___, Minkowski {}.new_rep(GS.dim).to_symbolic([]))
1306            ))
1307            .with(vakint::symbols::S.dot(function!(W_.a_, W_.a___), function!(W_.b_, W_.b___)));
1308
1309        assert_eq!(
1310            converted,
1311            vakint::symbols::S.dot(
1312                function!(vakint::symbols::S.p, 0),
1313                function!(vakint::symbols::S.k, 1)
1314            )
1315        );
1316    }
1317
1318    #[test]
1319    fn underdetermined_vakint_momentum_solve_tracks_free_variables() {
1320        test_initialise().unwrap();
1321
1322        let q0 = function!(GS.emr_mom, 0);
1323        let q1 = function!(GS.emr_mom, 1);
1324        let q2 = function!(GS.emr_mom, 2);
1325        let k0 = function!(GS.loop_mom, 0);
1326        let k1 = function!(GS.loop_mom, 1);
1327        let edge_3 = -&q0 - &q1 - &q2;
1328        let system = vec![&q0 - &k0, &edge_3 - &k1];
1329        let variables = vec![q0.clone(), q1.clone(), q2.clone()];
1330
1331        let solution = VakintMomentumSolution::solve(&system, &variables, &[]).unwrap();
1332
1333        assert_eq!(solution.free_variables, vec![q2]);
1334        assert!(
1335            solution
1336                .ensure_free_variables_eliminated(0, "numerator", &solution.free_variables[0], &[])
1337                .is_err()
1338        );
1339    }
1340
1341    #[test]
1342    fn underdetermined_vakint_momentum_solve_projects_topology() {
1343        test_initialise().unwrap();
1344
1345        let q1 = function!(GS.emr_mom, 1);
1346        let q2 = function!(GS.emr_mom, 2);
1347        let k0 = function!(GS.loop_mom, 0);
1348        let system = vec![-&q1 - &q2 - &k0];
1349        let variables = vec![q1.clone(), q2.clone()];
1350        let topology = function!(
1351            vakint::symbols::S.topo,
1352            function!(
1353                vakint::symbols::S.prop,
1354                1,
1355                function!(vakint::symbols::S.edge, 0, 0),
1356                -&q1 - &q2,
1357                GS.m_uv_vacuum,
1358                1
1359            )
1360        );
1361
1362        let solution = VakintMomentumSolution::solve(&system, &variables, &[]).unwrap();
1363
1364        solution
1365            .ensure_free_variables_eliminated(0, "numerator", &Atom::from(1), &[])
1366            .unwrap();
1367        assert_eq!(
1368            solution.rewrite_integral(&topology, &[]),
1369            function!(
1370                vakint::symbols::S.topo,
1371                function!(
1372                    vakint::symbols::S.prop,
1373                    1,
1374                    function!(vakint::symbols::S.edge, 0, 0),
1375                    k0,
1376                    GS.m_uv_vacuum,
1377                    1
1378                )
1379            )
1380        );
1381    }
1382
1383    #[test]
1384    fn empty_vakint_momentum_solve_treats_variables_as_free() {
1385        test_initialise().unwrap();
1386
1387        let q0 = function!(GS.emr_mom, 0);
1388        let no_variables = VakintMomentumSolution::solve(&[], &[], &[]).unwrap();
1389        let free_variable =
1390            VakintMomentumSolution::solve(&[], std::slice::from_ref(&q0), &[]).unwrap();
1391
1392        assert!(no_variables.free_variables.is_empty());
1393        assert_eq!(free_variable.free_variables, vec![q0]);
1394    }
1395}