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#[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 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 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 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 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 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 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 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 integrand_vakint = integrand_vakint
514 .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 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 debug_tags!(#uv, #integrated, #vakint, #graph, #dump;
554 reduced = %graph.dot(reduced),
555 "Den to prop for"
556 );
557 for (pair, index, _data) in graph.iter_edges_of(reduced) {
560 if let HedgePair::Paired { source, sink } = pair {
561 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 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 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 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 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 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, 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 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 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 let is_zero = &ed.data.mom - loop_expr;
914 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 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 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]
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}