1use std::collections::BTreeMap;
2
3use color_eyre::Result;
4use eyre::{WrapErr, eyre};
5use gammaloop_tracing_filter::debug_instrument;
6use idenso::{
7 color::{ColorSimplifier, ColorSimplifySettings},
8 dirac::{AGS, GammaSimplifier, GammaSimplifySettings},
9 representations::Bispinor,
10 shorthands::{
11 UndoShorthands,
12 chain::Chain,
13 metric::MetricSimplifier,
14 schoonschip::{Schoonschip, SchoonschipSettings},
15 },
16 tensor::{SymbolicNetExt, SymbolicNetParse},
17};
18
19use linnet::half_edge::{
20 HedgeGraph,
21 builder::HedgeGraphBuilder,
22 involution::{EdgeIndex, HedgePair},
23 subgraph::{ModifySubSet, SuBitGraph, SubGraphLike, SubSetLike, SubSetOps},
24};
25use spenso::{
26 network::{
27 graph::NetworkEdge,
28 library::symbolic::ETS,
29 parsing::{
30 ParseSettings, SchoonschipExpansionMode, ShorthandParsing, StructureInferenceMode,
31 },
32 tags::SPENSO_TAG,
33 },
34 shadowing::TensorCollectExt,
35 structure::{
36 representation::{Minkowski, RepName},
37 slot::{DummyAind, IsAbstractSlot, ParseableAind, Slot},
38 },
39};
40use symbolica::{
41 atom::{AliasedAtom, Atom, AtomCore, AtomView, FunctionBuilder},
42 domains::{atom::AtomField, integer::Z, rational::Q},
43 function,
44 id::Replacement,
45 parse, parse_lit,
46 poly::{PolyVariable, series::Series},
47 symbol,
48};
49use symbolica_utils::ReplaceBuilderExt;
50use vakint::{Vakint, VakintExpression, vakint_symbol};
51
52use crate::{
53 debug_tags,
54 graph::{LMBext, LoopMomentumBasis},
55 numerator::aind::Aind,
56 utils::{GS, W_},
57 uv::{
58 ApproximationType, UltravioletGraph,
59 approx::{
60 ForestNodeLike, Rooted, UVCtx,
61 local_4d::{FourDSector, Local4dCts},
62 },
63 marker::{UvMarker, UvOperation},
64 settings::VakintSettings,
65 uv_graph::UVE,
66 },
67};
68
69#[derive(Clone, Debug, PartialEq, Eq, Hash)]
74pub(crate) struct IntegratedCts {
75 expansion: Series<AtomField>,
76 scale_power: i64,
77}
78
79impl IntegratedCts {
80 pub(crate) fn factorized_product<'a>(
81 factors: impl IntoIterator<Item = &'a Self>,
82 depth: usize,
83 ) -> Result<Self> {
84 let mut factors = factors.into_iter();
85 let first = factors
86 .next()
87 .ok_or_else(|| eyre!("a factorized integrated counterterm cannot be empty"))?;
88 let mut pole = truncate(&first.expansion, false);
89 let mut finite_counterterm = -truncate(&first.expansion, true);
90 let mut scale_power = first.scale_power;
91
92 for factor in factors {
93 pole *= truncate(&factor.expansion, false);
94 finite_counterterm *= -truncate(&factor.expansion, true);
95 scale_power += factor.scale_power;
96 }
97
98 Ok(Self {
99 expansion: series(&(pole - finite_counterterm), depth)?,
102 scale_power,
103 })
104 }
105
106 fn projected_atom(&self, finite: bool) -> Atom {
107 truncate(&self.expansion, finite)
112 * Atom::var(GS.integrated_loop_scale).pow(self.scale_power)
113 }
114
115 pub(crate) fn pole_atom(&self) -> Atom {
116 self.projected_atom(false)
117 }
118
119 pub(crate) fn finite_counterterm_atom(&self) -> Atom {
120 -self.projected_atom(true)
121 }
122
123 pub(crate) fn physical_pole_atom(&self) -> Atom {
124 self.pole_atom()
125 .replace(GS.integrated_loop_scale)
126 .with(Atom::one())
127 }
128
129 pub(crate) fn physical_finite_counterterm_atom(&self) -> Atom {
130 self.finite_counterterm_atom()
131 .replace(GS.integrated_loop_scale)
132 .with(Atom::one())
133 }
134}
135
136fn series(expr: &Atom, depth: usize) -> Result<Series<AtomField>> {
137 Ok(expr.series(GS.dim_epsilon, 0, depth)?)
138}
139
140fn truncate(series: &Series<AtomField>, finite: bool) -> Atom {
141 let mut truncated = Atom::Zero;
142
143 for (power, p) in series.terms() {
144 if (power >= 0) == finite {
145 truncated += p * Atom::var(GS.dim_epsilon).pow(power);
146 }
147 }
148
149 truncated
150}
151
152impl Rooted for IntegratedCts {
153 fn root() -> Self {
154 Self {
155 expansion: series(&Atom::Zero, 1).expect("zero has a Laurent expansion"),
156 scale_power: 0,
157 }
158 }
159}
160
161fn simplify(integrand: &Atom) -> Result<Atom> {
162 if integrand.contains_symbol(AGS.sigma) {
163 return Err(eyre!(
164 "Sigma tensors are not supported by d-dimensional analytic UV numerator algebra"
165 ));
166 }
167 let integrand =
171 if integrand.contains_symbol(AGS.gamma) || integrand.contains_symbol(SPENSO_TAG.trace) {
172 let mut used = integrand.get_all_symbols(true);
179 let mut aliases = BTreeMap::<Atom, Atom>::new();
180 let mut serial = 0usize;
181 let protected = integrand.replace_map(|arg, _context, out| {
182 if matches!(arg, AtomView::Fun(fun) if fun.get_symbol() == GS.den)
186 || (matches!(arg, AtomView::Fun(fun)
187 if fun.get_symbol() == SPENSO_TAG.dot || fun.get_symbol() == ETS.metric)
188 && !arg.contains_symbol(AGS.gamma)
189 && !arg.contains_symbol(SPENSO_TAG.trace)
190 && arg
191 .parse_to_symbolic_net::<Aind>(&ParseSettings {
192 shorthand_parsing: ShorthandParsing::expand_all(),
193 ..Default::default()
194 })
195 .is_ok_and(|network| network.graph.dangling_indices().is_empty()))
196 {
197 let alias = aliases.entry(arg.to_owned()).or_insert_with(|| {
198 loop {
199 let symbol = symbol!(&format!("gammaloop::uv_scalar_product_{serial}"));
200 serial += 1;
201 if used.insert(symbol) {
202 break Atom::var(symbol);
203 }
204 }
205 });
206 **out = alias.clone();
207 }
208 });
209 let mut protected = AliasedAtom::from(protected);
210 for (original, alias) in aliases {
211 protected.register_alias(alias, original);
212 }
213 let prepared = protected
214 .get_root()
215 .parse_to_symbolic_net::<Aind>(&ParseSettings {
216 shorthand_parsing: ShorthandParsing::Expand {
217 schoonschip: SchoonschipExpansionMode::full(),
218 trace: false,
219 chain: true,
220 },
221 ..Default::default()
222 })
223 .map_err(|error| eyre!("invalid analytic UV spin tensor notation: {error}"))?
224 .simple_execute::<()>()?;
225 protected.map_root(|_| prepared).into_inner()
226 } else {
227 integrand.clone()
228 };
229 let collected = integrand
230 .collect_rep(Minkowski {}.into())
231 .simplify_metrics()
232 .collect_rep((Bispinor {}).into())
233 .collect_gamma_chains();
234 debug_tags!(#uv,#integrated,#collect;log.expr = collected, "After gamma chain collection");
235
236 let schoonschip = collected
237 .schoonschip_with_settings(&SchoonschipSettings {
238 simplify_chain_like_functions: true,
239 schoonschip_rank1_tensors: true,
240 ..Default::default()
241 })
242 .normalize_chains();
243 debug_tags!(#uv, #integrated, #profile, #trace, #start, #collect;
244 log.expr = schoonschip,
245 "After gamma schoonschip"
246 );
247 let collected = schoonschip.simplify_metrics().collect_gamma_chains();
253 debug_tags!(#uv, #integrated, #profile, #trace, #start, #collect;
254 log.expr = collected,
255 "After gamma collection"
256 );
257
258 let simplified = collected
259 .simplify_gamma_with(GammaSimplifySettings::canonical())
260 .collect_rep(Minkowski {}.into())
261 .expand_num();
262 debug_tags!(#uv, #integrated, #vakint, #profile, #trace, #start, #gamma;
263 log.expr = simplified,
264 "After gamma simplification"
265 );
266 let schoonschipped = simplified.schoonschip_net::<Aind>()?;
267 debug_tags!(#uv, #integrated, #vakint, #profile, #trace,#schoonschip, #start;
268 log.expr = schoonschipped,
269 "After Schoonschip net"
270 );
271 let dotted = schoonschipped.to_dots().normalize_dots();
272 debug_tags!(#uv, #integrated, #vakint, #profile, #trace, #dots;
273 log.expr = dotted,
274 "After dots"
275 );
276
277 if dotted
278 .replace(function!(
279 SPENSO_TAG.trace,
280 Bispinor {}.to_symbolic([W_.d_]),
281 W_.x___
282 ))
283 .matches()
284 {
285 return Err(eyre!(
286 "an unresolved Dirac trace remains after d-dimensional UV numerator algebra"
287 ));
288 }
289 if dotted
290 .replace(function!(
291 SPENSO_TAG.trace,
292 Minkowski {}.to_symbolic([W_.d_]),
293 W_.x___
294 ))
295 .matches()
296 {
297 return Err(eyre!(
298 "an unresolved Lorentz trace remains after d-dimensional UV numerator algebra"
299 ));
300 }
301 Ok(dotted)
302}
303
304pub(crate) struct Integrated<'a> {
305 pub vakint: &'a Vakint,
306 pub vakint_settings: &'a vakint::VakintSettings,
307}
308
309impl Integrated<'_> {
310 pub(crate) fn new<'a>(
311 vakint: &'a Vakint,
312 vakint_settings: &'a vakint::VakintSettings,
313 ) -> Integrated<'a> {
314 Integrated {
315 vakint,
316 vakint_settings,
317 }
318 }
319
320 pub(crate) fn run<S: super::ForestNodeLike, M: super::ForestNodeLike>(
321 &self,
322 integrand: &Local4dCts,
323 ctx: &UVCtx<'_>,
324 current: &S,
325 given: &S,
326 marker_current: &M,
327 marker_given: &M,
328 ) -> Result<IntegratedCts> {
329 let graph = ctx.graph;
330
331 let n_loops = graph.n_loops(current.subgraph());
332
333 let scheme = current.renormalization_scheme();
334 match scheme {
335 ApproximationType::MUV | ApproximationType::PolePart | ApproximationType::IR => {
336 let mut integrated = Atom::Zero;
340 for sector in integrand.active_sectors() {
341 let physical = sector
342 .atom
343 .replace(GS.integrated_loop_scale)
344 .with(Atom::one());
345 let simplified = simplify(&physical)?;
346 if !simplified.is_zero() {
347 integrated += self
348 .integrate(&simplified, sector, ctx, current, given)
349 .wrap_err_with(|| {
350 format!(
351 "while integrating {scheme} counterterm {} with retained component frames {:?}",
352 current.subgraph().string_label(),
353 sector.active_components,
354 )
355 })?;
356 }
357 }
358 let marker = UvMarker::new(ctx.settings);
359 let integrated = marker.apply(
360 UvOperation::Integrate,
361 marker_current.subgraph(),
362 marker_given.subgraph(),
363 &integrated,
364 );
365 let expansion_depth =
366 usize::try_from(self.vakint_settings.number_of_terms_in_epsilon_expansion)
367 .wrap_err("Vakint epsilon expansion depth must be nonnegative")?;
368 let expanded = series(&integrated, expansion_depth.max(n_loops + 1))?.map_coeff(
369 |coefficient| {
370 marker.apply(
371 UvOperation::Series,
372 marker_current.subgraph(),
373 marker_given.subgraph(),
374 coefficient,
375 )
376 },
377 );
378 let expansion = expanded.map_coeff(|coefficient| {
379 marker.apply(
380 UvOperation::Truncate,
381 marker_current.subgraph(),
382 marker_given.subgraph(),
383 coefficient,
384 )
385 });
386
387 Ok(IntegratedCts {
390 expansion,
391 scale_power: 4 * n_loops as i64,
392 })
393 }
394 ApproximationType::OS => Err(eyre!("Not yet implemented OS")),
395 ApproximationType::Unsubtracted => {
396 panic!("should have been kept out of the wood");
397 }
398 }
399 }
400
401 #[debug_instrument(
402 current = %current.log_display(),
403 given = %given.log_display(),
404 reduced,
405 )]
406 fn integrate<S: ForestNodeLike>(
407 &self,
408 integrand: &Atom,
409 sector: &FourDSector,
410 ctx: &UVCtx<'_>,
411 current: &S,
412 given: &S,
413 ) -> Result<Atom> {
414 let graph = ctx.graph;
415 let reduced = current.reduced_subgraph(given);
416 let settings = ctx.settings;
417 let reduced_label = reduced.string_label();
418 tracing::Span::current().record("reduced", reduced_label.as_str());
419 debug_tags!(#uv, #integrated, #vakint, #trace, #input;
420 log.integrand = integrand,
421 "Integrating and truncating"
422 );
423 let integrand_vakint = to_vakint_integrand(
428 integrand,
429 graph,
430 current.subgraph(),
431 given.subgraph(),
432 §or.active_components,
433 &settings.vakint,
434 false,
435 )?;
436
437 let propagator = function!(
438 vakint::symbols::S.prop,
439 W_.a_,
440 W_.b_,
441 W_.mom_,
442 W_.mass_,
443 W_.e_
444 )
445 .to_pattern();
446 let retained_loop_count = sector
447 .active_components
448 .iter()
449 .flat_map(|(_, _, lmb)| lmb.loop_edges.iter())
450 .collect::<std::collections::HashSet<_>>()
451 .len();
452 let mut res = Atom::Zero;
453 for source_term in integrand_vakint.0 {
454 let mut loop_factors = std::collections::BTreeMap::<i64, Atom>::new();
458 let mut masses = std::collections::HashSet::new();
459 let mut independent = true;
460 for matched in source_term.integral.pattern_match(&propagator, None, None) {
461 let momentum = &matched[&W_.mom_];
462 let loop_id = [1, -1].into_iter().find_map(|sign| {
463 let momentum = momentum * Atom::num(sign);
464 let AtomView::Fun(vector) = momentum.as_view() else {
465 return None;
466 };
467 (vector.get_symbol() == vakint::symbols::S.k && vector.get_nargs() == 1)
468 .then(|| i64::try_from(vector.get(0)).ok())
469 .flatten()
470 });
471 let Some(loop_id) = loop_id else {
472 independent = false;
473 break;
474 };
475 masses.insert(matched[&W_.mass_].clone());
476 *loop_factors.entry(loop_id).or_insert_with(Atom::one) *= propagator
477 .replace_wildcards(&matched)
478 .map_err(|error| eyre!(error))?;
479 }
480 let factorized = independent
481 && retained_loop_count > 1
482 && loop_factors.len() == retained_loop_count
483 && masses.len() > 1;
484 let loop_ids = loop_factors.keys().copied().collect::<Vec<_>>();
485 let stages = if factorized {
486 loop_factors
487 .into_iter()
488 .map(|(loop_id, propagators)| {
489 (
490 function!(vakint::symbols::S.topo, propagators),
491 loop_ids
492 .iter()
493 .copied()
494 .filter(|id| *id != loop_id)
495 .collect::<Vec<_>>(),
496 )
497 })
498 .collect::<Vec<_>>()
499 } else {
500 vec![(source_term.integral.clone(), Vec::new())]
501 };
502 let mut coefficient = source_term.numerator.clone();
503 if factorized {
504 let normalization = self
508 .vakint_settings
509 .get_integral_normalization_factor_atom()?;
510 coefficient *= normalization
511 .replace(vakint::symbols::S.n_loops.to_pattern())
512 .with(Atom::num(retained_loop_count))
513 / normalization
514 .replace(vakint::symbols::S.n_loops.to_pattern())
515 .with(Atom::one())
516 .pow(retained_loop_count);
517 }
518 for (integral, spectators) in stages {
522 let stage_loop_count = if factorized { 1 } else { retained_loop_count };
523 let external = function!(vakint::symbols::S.p, W_.i_, W_.x___).to_pattern();
524 let mut used_ids = coefficient
525 .pattern_match(&external, None, None)
526 .map(|matched| i64::try_from(matched[&W_.i_].as_view()))
527 .collect::<Result<std::collections::HashSet<_>, _>>()
528 .map_err(|error| eyre!(error))?;
529 let mut next_id = 0i64;
530 let mut restore_spectators = Vec::new();
531 for spectator in spectators {
532 while used_ids.contains(&next_id) {
533 next_id = next_id.checked_add(1).ok_or_else(|| {
534 eyre!("no free external momentum ID for vacuum spectator")
535 })?;
536 }
537 used_ids.insert(next_id);
538 let source = function!(vakint::symbols::S.k, spectator, W_.x___);
539 let target = function!(vakint::symbols::S.p, next_id, W_.x___);
540 coefficient = coefficient
541 .replace(source.to_pattern())
542 .with(target.to_pattern());
543 restore_spectators.push(Replacement::new(target, source));
544 }
545 let mut stage_term = source_term.clone();
546 stage_term.integral = integral;
547 stage_term.numerator = coefficient;
548 let mut integrand_vakint = VakintExpression(vec![stage_term]);
549 let mut same_momentum = std::collections::BTreeMap::<Atom, Vec<_>>::new();
550 for matched in integrand_vakint.0[0]
551 .integral
552 .pattern_match(&propagator, None, None)
553 {
554 let momentum = &matched[&W_.mom_];
555 same_momentum
556 .entry(momentum.clone().min(-momentum))
557 .or_default()
558 .push((
559 usize::try_from(matched[&W_.a_].as_view())
560 .map_err(|error| eyre!(error))?,
561 matched[&W_.mass_].clone(),
562 matched[&W_.e_].clone(),
563 ));
564 }
565 for (momentum, propagators) in same_momentum {
566 let (_, first_mass, _) = &propagators[0];
567 if propagators.iter().all(|(_, mass, _)| mass == first_mass)
568 || momentum.contains_symbol(vakint::symbols::S.p)
569 || propagators.iter().any(|(_, mass, power)| {
570 mass.contains_symbol(W_.x_)
571 || !i64::try_from(power.as_view()).is_ok_and(|power| power > 0)
572 })
573 {
574 continue;
575 }
576
577 let loop_square = Atom::var(W_.x_);
584 let denominator = propagators
585 .iter()
586 .fold(Atom::one(), |product, (_, mass, power)| {
587 product * (&loop_square - mass).pow(-power)
588 });
589 let fractions = denominator
590 .try_to_rational_polynomial::<_, _, u16>(&Q, &Z, [W_.x_])?
591 .apart_factored_denominators(0);
592 for term in std::mem::take(&mut integrand_vakint.0) {
593 let powers = term
594 .integral
595 .pattern_match(&propagator, None, None)
596 .map(|matched| {
597 Ok((
598 usize::try_from(matched[&W_.a_].as_view())
599 .map_err(|error| eyre!(error))?,
600 matched[&W_.e_].clone(),
601 ))
602 })
603 .collect::<Result<std::collections::BTreeMap<_, _>>>()?;
604 let graph = vakint::graph::Graph::new_from_atom(
605 term.integral.as_view(),
606 *powers.keys().next_back().unwrap(),
607 )?;
608 for (coefficient, denominator, power) in &fractions {
609 let denominator = denominator.to_expression();
610 let constant = denominator.replace(W_.x_).with(Atom::Zero);
611 let leading = denominator.replace(W_.x_).with(Atom::one()) - &constant;
612 let (survivor, _, _) = propagators.iter().find(|(_, mass, _)| {
613 (&denominator - &leading * (&loop_square - mass)).expand().is_zero()
614 }).ok_or_else(|| eyre!("vacuum partial fraction has an unexpected denominator {denominator}"))?;
615 let coefficient = coefficient.to_expression() / leading.pow(*power);
616 if coefficient.contains_symbol(W_.x_) {
617 return Err(eyre!(
618 "vacuum partial fraction retained loop dependence in its coefficient"
619 ));
620 }
621 let mut contracted = graph.clone();
622 for (id, _, _) in &propagators {
623 if id != survivor {
624 contracted.contract_edges(&[*id].into_iter().collect());
625 }
626 }
627 if contracted.to_symbolica_graph(false).num_loops() != stage_loop_count
632 {
633 return Err(eyre!(
634 "vacuum partial fraction changed the retained {stage_loop_count}-loop integration domain for component {reduced_label}"
635 ));
636 }
637 let mut fraction = term.clone();
638 fraction.numerator *= coefficient;
639 fraction.integral = function!(
640 vakint::symbols::S.topo,
641 contracted
642 .edges
643 .values()
644 .fold(Atom::one(), |product, edge| {
645 product
646 * function!(
647 vakint::symbols::S.prop,
648 edge.id,
649 function!(
650 vakint::symbols::S.edge,
651 edge.left_node_id,
652 edge.right_node_id
653 ),
654 &edge.momentum,
655 &edge.mass,
656 if edge.id == *survivor {
657 Atom::num(*power)
658 } else {
659 powers[&edge.id].clone()
660 }
661 )
662 })
663 );
664 integrand_vakint.0.push(fraction);
665 }
666 }
667 }
668 integrand_vakint.0.retain(|term| {
672 let propagators = term
673 .integral
674 .pattern_match(&propagator, None, None)
675 .map(|matched| {
676 matched[&W_.mass_].is_zero()
677 && matched[&W_.mom_].contains_symbol(vakint::symbols::S.k)
678 })
679 .collect::<Vec<_>>();
680 if propagators.is_empty()
681 || propagators.iter().any(|massless_loop| !massless_loop)
682 || term.integral.contains_symbol(vakint::symbols::S.p)
683 {
684 return true;
685 }
686 let mut pending = vec![term.numerator.as_view()];
687 let mut scaleless = true;
688 while let Some(view) = pending.pop() {
689 if !view.contains_symbol(vakint::symbols::S.k) {
690 continue;
691 }
692 match view {
693 AtomView::Add(sum) => pending.extend(sum.iter()),
694 AtomView::Mul(product) => pending.extend(product.iter()),
695 AtomView::Pow(power) => {
696 let (base, exponent) = power.get_base_exp();
697 if i64::try_from(exponent).is_ok_and(|power| power >= 0) {
698 pending.push(base);
699 } else {
700 scaleless = false;
701 break;
702 }
703 }
704 AtomView::Fun(function)
705 if function.get_symbol() == vakint::symbols::S.k
706 && function.get_nargs() > 0
707 && usize::try_from(function.get(0)).is_ok() => {}
708 AtomView::Fun(function)
709 if function.get_symbol() == vakint::symbols::S.dot
710 && function.get_nargs() == 2
711 && function.iter().all(|argument| {
712 matches!(argument, AtomView::Fun(vector)
713 if [vakint::symbols::S.k, vakint::symbols::S.p]
714 .contains(&vector.get_symbol())
715 && vector.get_nargs() == 1
716 && usize::try_from(vector.get(0)).is_ok())
717 }) => {}
718 _ => {
719 scaleless = false;
720 break;
721 }
722 }
723 }
724 if scaleless {
725 debug_tags!(#uv, #integrated, #vakint, #trace;
726 stage = "certified_scaleless_vacuum",
727 log.integral = term.integral,
728 log.numerator = term.numerator,
729 "Dropping a massless vacuum term with polynomial loop numerator"
730 );
731 }
732 !scaleless
733 });
734 if integrand_vakint.0.is_empty() {
735 coefficient = Atom::Zero;
736 break;
737 }
738
739 for (term_index, t) in integrand_vakint.0.iter().enumerate() {
740 debug_tags!(#uv,#integrated,#vakint,#trace,#to_vakint;
741 term_index = %term_index,
742 log.integral = t.integral,
743 log.numerator = t.numerator,
744 "Vakint term as input"
745 );
746 }
747 debug_tags!(#uv,#integrated,#vakint;settings = ?&self.vakint_settings,"Vakint args");
748
749 let mut mass_aliases = std::collections::BTreeMap::new();
758 let mut used_symbols = integrand_vakint
759 .0
760 .iter()
761 .flat_map(|term| [&term.integral, &term.numerator])
762 .flat_map(|atom| atom.get_all_symbols(true))
763 .collect::<std::collections::HashSet<_>>();
764 let mut mass_serial = 0;
765 for term in &mut integrand_vakint.0 {
766 let mut replacements = Vec::new();
767 for matched in term.integral.pattern_match(&propagator, None, None) {
768 let mass = &matched[&W_.mass_];
769 if mass.contains_symbol(vakint::symbols::S.k)
770 || mass.contains_symbol(vakint::symbols::S.p)
771 {
772 return Err(eyre!(
773 "a vacuum propagator mass must be independent of momentum"
774 ));
775 }
776 if mass.is_zero()
777 || matches!(mass.as_view(), AtomView::Var(_))
778 || matches!(mass.as_view(), AtomView::Pow(power)
779 if matches!(power.get_base_exp().0, AtomView::Var(_))
780 && power.get_base_exp().1 == Atom::num(2).as_view())
781 {
782 continue;
783 }
784 let alias = mass_aliases.entry(mass.clone()).or_insert_with(|| {
785 loop {
786 let symbol = vakint_symbol!(format!(
787 "integrated_mass_squared_{mass_serial}"
788 ));
789 mass_serial += 1;
790 if used_symbols.insert(symbol) {
791 break Atom::var(symbol);
792 }
793 }
794 });
795 replacements.push(Replacement::new(
796 propagator
797 .replace_wildcards(&matched)
798 .map_err(|error| eyre!(error))?,
799 function!(
800 vakint::symbols::S.prop,
801 &matched[&W_.a_],
802 &matched[&W_.b_],
803 &matched[&W_.mom_],
804 &*alias,
805 &matched[&W_.e_]
806 ),
807 ));
808 }
809 term.integral = term.integral.replace_multiple(&replacements);
810 }
811 integrand_vakint.canonicalize(
812 self.vakint_settings,
813 &self.vakint.topologies,
814 false,
815 )?;
816 for (term_index, t) in integrand_vakint.0.iter().enumerate() {
817 debug_tags!(#uv,#integrated,#vakint,#trace,#canonicalize;
818 term_index = %term_index,
819 log.integral = t.integral,
820 log.numerator = t.numerator,
821 "Vakint term after canonicalization"
822 );
823 }
824 integrand_vakint.tensor_reduce(self.vakint, self.vakint_settings)?;
825 for (term_index, t) in integrand_vakint.0.iter().enumerate() {
826 debug_tags!(#uv,#integrated,#vakint,#trace,#tensor_reduce;
827 term_index = %term_index,
828 log.integral = t.integral,
829 log.numerator = t.numerator,
830 "Vakint term after tensor reduction"
831 );
832 }
833 for (term_index, term) in integrand_vakint.0.iter_mut().enumerate() {
834 term.numerator =
835 self.simplify_projected_numerator(&term.numerator, current.topo_order())?;
836 debug_tags!(#uv, #integrated, #vakint, #trace;
837 stage = "projected_numerator_after_d_dimensional_algebra",
838 term_index = %term_index,
839 log.numerator = term.numerator,
840 "Completed projected numerator algebra before Laurent expansion"
841 );
842 }
843 integrand_vakint.evaluate_integral(self.vakint, self.vakint_settings)?;
844 for (term_index, t) in integrand_vakint.0.iter().enumerate() {
845 debug_tags!(#uv,#integrated,#vakint,#trace,#evaluate;
846 term_index = %term_index,
847 log.integral = t.integral,
848 log.numerator = t.numerator,
849 "Vakint term after evaluation"
850 );
851 }
852
853 coefficient = Atom::from(integrand_vakint).replace_multiple(
854 mass_aliases
855 .into_iter()
856 .map(|(mass, alias)| Replacement::new(alias, mass))
857 .collect::<Vec<_>>(),
858 );
859 if coefficient.contains_symbol(vakint::symbols::S.k) {
860 return Err(eyre!(
861 "integrated vacuum factor retained an active loop momentum"
862 ));
863 }
864 coefficient = coefficient.replace_multiple(&restore_spectators);
865 }
866 res += coefficient;
867 }
868
869 debug_tags!(#uv,#integrated,#vakint,#trace,#raw;
870 log.res = res,
871 "Raw post vakint "
872 );
873
874 let mut res = Self::restore_numerator(res, current.topo_order());
875
876 res = Self::dimensionally_regularized(&res.simplify_metrics().metric_shorthand_to_dot());
877
878 debug_tags!(#uv, #integrated, #vakint, #inspect, #trace, #replace;
879 log.res = res,
880 "Replaced post vakint "
881 );
882
883 let bispinor_rep = Bispinor {}.into();
886 let after_chainify = res.chainify(bispinor_rep);
887 debug_tags!(#uv, #integrated, #vakint, #profile, #trace, #chainify;
888 log.expr = after_chainify,
889 "Integrated UV chain cleanup after chainify"
890 );
891
892 let after_collect_chains = after_chainify.collect_chains(bispinor_rep);
893 debug_tags!(#uv, #integrated, #vakint, #profile, #trace, #collect;
894 log.expr = after_collect_chains,
895 "Integrated UV chain cleanup after collect_chains"
896 );
897
898 res = after_collect_chains.undo_single_length();
899 debug_tags!(#uv, #integrated, #vakint, #profile, #trace, #undo_single_length;
900 log.expr = res,
901 "Integrated UV chain cleanup after undo_single_length"
902 );
903
904 Ok(res.expand())
909 }
910
911 fn simplify_projected_numerator(
912 &self,
913 numerator: &Atom,
914 topology_order: usize,
915 ) -> Result<Atom> {
916 let numerator = Vakint::convert_to_dot_notation(self.vakint_settings, numerator.as_view())?;
921 let numerator = Self::restore_numerator(numerator, topology_order);
922 let numerator = simplify(&numerator)?;
923 Self::ensure_resolved_lorentz_contractions(&numerator)?;
924 let numerator = Self::dimensionally_regularized(&numerator)
925 .undo_schoonschip::<Aind>()?
926 .undo_chain::<Aind>()?
927 .undo_trace::<Aind>()?
928 .metric_shorthand_to_dot();
929 Ok(Self::to_vakint_numerator(&numerator))
930 }
931
932 fn ensure_resolved_lorentz_contractions(numerator: &Atom) -> Result<()> {
933 let minkowski = Minkowski {}.to_symbolic([GS.dim]).get_symbol().unwrap();
934 if !numerator.contains_symbol(minkowski) {
935 return Ok(());
936 }
937 let explicit = numerator
942 .parse_to_symbolic_net::<Aind>(&ParseSettings {
943 shorthand_parsing: ShorthandParsing::Expand {
944 schoonschip: SchoonschipExpansionMode::full(),
945 trace: false,
946 chain: true,
947 },
948 ..Default::default()
949 })
950 .map_err(|error| eyre!("invalid analytic UV Lorentz tensor notation: {error}"))?
951 .simple_execute::<()>()?
952 .expand();
953 let terms = if let AtomView::Add(sum) = explicit.as_view() {
954 sum.iter().collect::<Vec<_>>()
955 } else {
956 vec![explicit.as_view()]
957 };
958 let settings = ParseSettings {
959 shorthand_parsing: ShorthandParsing::Opaque {
960 inference: StructureInferenceMode::Fast,
961 },
962 ..Default::default()
963 };
964 for term in terms {
965 let mut unresolved_inner_product = false;
966 let tensors = term.replace_map(|view, _, out| {
967 if !matches!(view, AtomView::Num(_)) && !view.contains_symbol(minkowski) {
968 **out = Atom::one();
972 return;
973 }
974 match view {
975 AtomView::Fun(f) => {
976 if [SPENSO_TAG.dot, ETS.metric].contains(&f.get_symbol())
977 && view.contains_symbol(minkowski)
978 && !f
979 .iter()
980 .all(|arg| Slot::<Minkowski, Aind>::try_from(arg).is_ok())
981 {
982 unresolved_inner_product = true;
986 }
987 let momentum = [
988 GS.emr_mom,
989 GS.loop_mom,
990 vakint::symbols::S.p,
991 vakint::symbols::S.k,
992 ]
993 .contains(&f.get_symbol());
994 **out = if !momentum
995 && f.iter()
996 .any(|arg| Slot::<Minkowski, Aind>::try_from(arg).is_ok())
997 {
998 view.to_owned()
999 } else {
1000 Atom::one()
1001 };
1002 }
1003 AtomView::Var(_) => **out = Atom::one(),
1004 _ => {}
1005 }
1006 });
1007 if unresolved_inner_product {
1008 return Err(eyre!(
1009 "an unresolved compact Lorentz inner product remains after d-dimensional UV numerator algebra"
1010 ));
1011 }
1012 let network = tensors
1013 .parse_to_symbolic_net::<Aind>(&settings)
1014 .map_err(|error| {
1015 eyre!("invalid residual d-dimensional tensor structure: {error}")
1016 })?;
1017 let external = network.graph.dangling_indices();
1018 for (pair, _, edge) in network.graph.graph.iter_edges() {
1019 if pair.is_paired()
1020 && let NetworkEdge::Slot(slot) = edge.data
1021 && slot.rep_name().symbol() == minkowski
1022 && !external.contains(slot)
1023 {
1024 return Err(eyre!(
1025 "an unresolved internal Lorentz tensor contraction remains after d-dimensional UV numerator algebra; a d-dimensional tensor-operator reduction is required before analytic integration"
1026 ));
1027 }
1028 }
1029 }
1030 Ok(())
1031 }
1032
1033 fn dimensionally_regularized(numerator: &Atom) -> Atom {
1034 let minkowski = Minkowski {}.to_symbolic([GS.dim]).get_symbol().unwrap();
1035 let dimension = Atom::var(GS.dim);
1036 let regulated = Atom::num(4) - Atom::num(2) * Atom::var(GS.dim_epsilon);
1037 numerator.replace_map(|view, _, out| {
1038 if view.get_symbol() == Some(minkowski) {
1039 **out = view.to_owned();
1043 } else if view == dimension.as_view() {
1044 **out = regulated.clone();
1045 }
1046 })
1047 }
1048
1049 fn restore_numerator(mut res: Atom, topology_order: usize) -> Atom {
1050 let mink = Minkowski {}.new_rep(GS.dim);
1053 let mut projected_indices = std::collections::BTreeMap::new();
1054 res = res.replace_map(|view, _, out| {
1055 if matches!(view, AtomView::Fun(index) if index.get_symbol() == vakint::symbols::S.tensor_index) {
1056 let replacement = projected_indices.entry(view.to_owned()).or_insert_with(|| loop {
1057 let candidate = mink.to_symbolic([Aind::new_dummy().to_atom()]);
1058 if res.pattern_match(&candidate.to_pattern(), None, None).next().is_none() {
1059 break candidate;
1060 }
1061 });
1062 **out = replacement.clone();
1063 }
1064 });
1065
1066 res = res
1067 .replace(function!(vakint::symbols::S.tensor, W_.x_, W_.x___))
1070 .with(Atom::var(W_.x_))
1071 .replace(parse_lit!(vakint::cl2))
1072 .with(parse_lit!(cl2))
1073 .replace(parse_lit!(vakint::sqrt3))
1074 .with(parse_lit!(sqrt(3)));
1075
1076 let vk_metric = vakint_symbol!("g");
1077
1078 res = res
1080 .replace(vakint::symbols::S.p.call_args([W_.i_, W_.j_]))
1081 .when(W_.j_.filter(|r| r.is_integer().is_true()))
1082 .with(
1083 vakint::symbols::S.p.call_args([
1084 Atom::var(W_.i_),
1085 mink.to_symbolic([GS
1086 .uvaind
1087 .call_args([Atom::num(topology_order), Atom::var(W_.j_)])]),
1088 ]),
1089 )
1090 .replace(
1091 vakint::symbols::S
1092 .p
1093 .call_args([Atom::var(W_.i_), vakint::symbols::S.dot_dummy_ind(W_.j_)]),
1094 )
1095 .when(W_.j_.filter(|r| r.is_integer().is_true()))
1096 .with(
1097 vakint::symbols::S.p.call_args([
1098 Atom::var(W_.i_),
1099 mink.to_symbolic([GS
1100 .uvaind
1101 .call_args([Atom::num(topology_order), Atom::var(W_.j_)])]),
1102 ]),
1103 )
1104 .replace(vakint::symbols::S.p.call_args([W_.x__]))
1105 .with(GS.emr_mom.call_args([W_.x__]));
1106 res = res
1107 .replace(function!(vk_metric, W_.x_, W_.y_) * function!(GS.emr_mom, W_.x___, W_.x_))
1108 .with(function!(GS.emr_mom, W_.x___, W_.y_))
1109 .replace(function!(
1110 vk_metric,
1111 vakint::symbols::S.dot_dummy_ind(W_.x_),
1112 W_.y_
1113 ))
1114 .when(W_.x_.filter(|r| r.is_integer().is_true()))
1115 .with(function!(
1116 vk_metric,
1117 mink.to_symbolic([GS
1118 .uvaind
1119 .call_args([Atom::num(topology_order), Atom::var(W_.x_)])]),
1120 W_.y_
1121 ))
1122 .replace(function!(
1123 vk_metric,
1124 W_.x_,
1125 vakint::symbols::S.dot_dummy_ind(W_.y_)
1126 ))
1127 .when(W_.y_.filter(|r| r.is_integer().is_true()))
1128 .with(function!(
1129 vk_metric,
1130 mink.to_symbolic([GS
1131 .uvaind
1132 .call_args([Atom::num(topology_order), Atom::var(W_.y_)])]),
1133 W_.x_
1134 ))
1135 .replace(function!(vk_metric, W_.x_, W_.y_))
1136 .with(function!(ETS.metric, W_.x_, W_.y_));
1137
1138 res = res.replace(vakint::symbols::S.cmplx_i).with(Atom::i());
1139
1140 res
1141 }
1142
1143 fn to_vakint_numerator(numerator: &Atom) -> Atom {
1144 let numerator = numerator
1145 .replace(function!(GS.loop_mom, W_.x___))
1146 .with(function!(vakint::symbols::S.k, W_.x___))
1147 .replace(function!(GS.emr_mom, W_.x___))
1148 .with(function!(vakint::symbols::S.p, W_.x___))
1149 .replace(function!(
1150 SPENSO_TAG.dot,
1151 function!(W_.a_, W_.a___, Minkowski {}.new_rep(GS.dim).to_symbolic([])),
1152 function!(W_.b_, W_.b___, Minkowski {}.new_rep(GS.dim).to_symbolic([]))
1153 ))
1154 .with(vakint::symbols::S.dot(function!(W_.a_, W_.a___), function!(W_.b_, W_.b___)))
1155 .replace(function!(
1156 ETS.metric,
1157 Minkowski {}.to_symbolic([W_.a__]),
1158 Minkowski {}.to_symbolic([W_.b__])
1159 ))
1160 .with(function!(
1161 vakint::symbols::S.metric,
1162 Minkowski {}.to_symbolic([W_.a__]),
1163 Minkowski {}.to_symbolic([W_.b__])
1164 ));
1165 numerator.replace_map(|view, _, out| {
1170 let AtomView::Fun(tensor) = view else { return };
1171 if [
1172 vakint::symbols::S.k,
1173 vakint::symbols::S.p,
1174 vakint::symbols::S.g,
1175 vakint::symbols::S.dot,
1176 vakint::symbols::S.tensor,
1177 ]
1178 .contains(&tensor.get_symbol())
1179 {
1180 **out = view.to_owned();
1181 return;
1182 }
1183 let slots = tensor
1184 .iter()
1185 .filter(|argument| Slot::<Minkowski, Aind>::try_from(*argument).is_ok())
1186 .collect::<Vec<_>>();
1187 if !slots.is_empty() {
1188 let mut wrapped = FunctionBuilder::new(vakint::symbols::S.tensor).add_arg(view);
1189 for slot in slots {
1190 wrapped = wrapped.add_arg(slot);
1191 }
1192 **out = wrapped.finish();
1193 }
1194 })
1195 }
1196}
1197
1198#[debug_instrument]
1199pub(crate) fn to_vakint_integrand<
1200 E: UVE,
1201 V,
1202 H,
1203 S: SubGraphLike + SubSetLike<Base = SuBitGraph>,
1204 SS: SubGraphLike,
1205>(
1206 integrand: &Atom,
1207 graph: &HedgeGraph<E, V, H>,
1208 reduced: &S,
1209 dependent_subgraph: &SS,
1210 active_components: &[(SuBitGraph, SuBitGraph, LoopMomentumBasis)],
1211 settings: &VakintSettings,
1212 substitute_masses_to_m_uv: bool,
1213) -> Result<VakintExpression> {
1214 let reduced_label = reduced.string_label();
1215 let dependent_subgraph_label = dependent_subgraph.string_label();
1216 let source_lmbs = active_components
1217 .iter()
1218 .map(|(_, _, lmb)| lmb)
1219 .collect::<Vec<_>>();
1220 let mut retained_loop_edges = source_lmbs
1221 .iter()
1222 .flat_map(|lmb| lmb.loop_edges.iter().copied())
1223 .collect::<Vec<_>>();
1224 retained_loop_edges.sort();
1225 retained_loop_edges.dedup();
1226 let mut integrand_vakint = GS
1231 .erase_uv_momentum_provenance(integrand)
1232 .simplify_color_with(ColorSimplifySettings {
1233 simplify_non_color: false,
1234 ..Default::default()
1235 })
1236 .undo_schoonschip::<Aind>()?
1237 .undo_chain::<Aind>()?
1238 .undo_trace::<Aind>()?;
1239 debug_tags!(#uv, #integrated, #vakint, #trace;
1240 stage = "to_vakint_integrand_after_undo_shorthands",
1241 reduced = %reduced_label,
1242 dependent_subgraph = %dependent_subgraph_label,
1243 substitute_masses_to_m_uv = substitute_masses_to_m_uv,
1244 log.integrand = integrand_vakint,
1245 "Vakint trace after undo shorthands"
1246 );
1247 integrand_vakint = integrand_vakint.simplify_metrics();
1264 debug_tags!(#uv, #integrated, #vakint, #trace;
1265 stage = "to_vakint_integrand_after_simplify_metrics",
1266 reduced = %reduced_label,
1267 dependent_subgraph = %dependent_subgraph_label,
1268 log.integrand = integrand_vakint,
1269 "Vakint trace after metric simplification"
1270 );
1271
1272 let denominator_pattern = function!(GS.den, W_.x___).to_pattern();
1277 let denominators = integrand_vakint
1278 .pattern_match(&denominator_pattern, None, None)
1279 .map(|matched| denominator_pattern.replace_wildcards(&matched).unwrap())
1280 .collect::<std::collections::HashSet<_>>();
1281 integrand_vakint = integrand_vakint
1282 .coefficient_list::<i32>(&denominators.into_iter().collect::<Vec<_>>())
1283 .into_iter()
1284 .map(|(denominator, numerator)| denominator * numerator)
1285 .sum();
1286 let mut propagator_id = 1;
1287 let mut propagator_replacements = Vec::new();
1288 let mut incidence_replacements = Vec::new();
1289 let mut assigned_edges = std::collections::HashSet::new();
1290
1291 let vk_prop = vakint::symbols::S.prop;
1292 let vk_edge = vakint_symbol!("edge");
1293 let vk_topo = vakint_symbol!("topo");
1294
1295 debug_tags!(#uv, #integrated, #vakint, #graph, #dump;
1301 reduced = %graph.dot(reduced),
1302 "Den to prop for"
1303 );
1304 for (owners, source_scope, _) in active_components {
1307 let mut contracted_nodes = std::collections::HashMap::new();
1312 for prefix in graph.connected_components(&source_scope.subtract(owners)) {
1313 let mut nodes = graph.iter_nodes_of(&prefix).map(|(node, _, _)| node);
1314 if let Some(first) = nodes.next() {
1315 for node in nodes {
1316 contracted_nodes.insert(node, first);
1317 }
1318 }
1319 }
1320 for (pair, index, _data) in graph.iter_edges_of(owners) {
1321 if let HedgePair::Paired { source, sink } = pair {
1322 if !assigned_edges.insert(index) {
1323 return Err(eyre!(
1324 "Vakint denominator owner {index} belongs to multiple active components"
1325 ));
1326 }
1327 let source = graph.node_id(source);
1331 let sink = graph.node_id(sink);
1332 let contracted_source = contracted_nodes.get(&source).copied().unwrap_or(source);
1333 let contracted_sink = contracted_nodes.get(&sink).copied().unwrap_or(sink);
1334 propagator_replacements.push(Replacement::new(
1335 function!(
1336 GS.den,
1337 usize::from(index) as i64,
1338 W_.mom_,
1339 W_.mass_,
1340 W_.x___
1341 ),
1342 function!(
1343 vk_prop,
1344 propagator_id,
1345 function!(vk_edge, usize::from(source), usize::from(sink)),
1346 W_.mom_,
1347 if substitute_masses_to_m_uv {
1348 Atom::var(GS.m_uv_vacuum).pow(2)
1349 } else {
1350 Atom::var(W_.mass_)
1351 },
1352 1
1353 ),
1354 ));
1355 incidence_replacements.push(Replacement::new(
1356 function!(
1357 vk_prop,
1358 propagator_id,
1359 function!(vk_edge, usize::from(source), usize::from(sink)),
1360 W_.x___
1361 ),
1362 function!(
1363 vk_prop,
1364 propagator_id,
1365 function!(
1366 vk_edge,
1367 usize::from(contracted_source),
1368 usize::from(contracted_sink)
1369 ),
1370 W_.x___
1371 ),
1372 ));
1373 propagator_id += 1;
1374 }
1375 }
1376 }
1377 integrand_vakint = integrand_vakint
1380 .replace_multiple(&propagator_replacements)
1381 .replace(function!(vk_prop, W_.x___, 1).pow(Atom::var(W_.e_)))
1382 .with(function!(vk_prop, W_.x___, -Atom::var(W_.e_)));
1383 debug_tags!(#uv, #integrated, #vakint, #trace;
1384 stage = "to_vakint_integrand_after_den_to_prop",
1385 reduced = %reduced_label,
1386 dependent_subgraph = %dependent_subgraph_label,
1387 log.integrand = integrand_vakint,
1388 "Vakint trace after denominator-to-propagator conversion"
1389 );
1390
1391 debug_tags!(#uv, #integrated, #vakint, #graph, #dump;
1392 reduced = %graph.dot(dependent_subgraph),
1393 "Shrinking each component's omitted prefix for vakint"
1394 );
1395 integrand_vakint = integrand_vakint.replace_multiple(&incidence_replacements);
1397 if integrand_vakint.contains_symbol(GS.den) {
1398 return Err(eyre!(
1399 "Vakint component {reduced_label} contains a denominator without an active owner"
1400 ));
1401 }
1402 debug_tags!(#uv, #integrated, #vakint, #trace;
1403 stage = "to_vakint_integrand_after_shrink_subgraph",
1404 reduced = %reduced_label,
1405 dependent_subgraph = %dependent_subgraph_label,
1406 log.integrand = integrand_vakint,
1407 "Vakint trace after shrinking subgraph"
1408 );
1409
1410 integrand_vakint = integrand_vakint
1413 .replace(function!(
1414 vk_prop,
1415 W_.x_,
1416 function!(vk_edge, W_.a_, W_.b_),
1417 -Atom::var(W_.y_),
1418 W_.e___
1419 ))
1420 .repeat()
1421 .with(function!(
1422 vk_prop,
1423 W_.x_,
1424 function!(vk_edge, W_.b_, W_.a_),
1425 W_.y_,
1426 W_.e___
1427 ));
1428
1429 integrand_vakint = integrand_vakint
1431 .replace(
1432 function!(
1433 vk_prop,
1434 W_.x_,
1435 function!(vk_edge, W_.a_, W_.b_),
1436 W_.x___,
1437 W_.e_
1438 ) * function!(
1439 vk_prop,
1440 W_.y_,
1441 function!(vk_edge, W_.b_, W_.c_),
1442 W_.x___,
1443 W_.f_
1444 ),
1445 )
1446 .repeat()
1447 .with(function!(
1448 vk_prop,
1449 W_.y_,
1450 function!(vk_edge, W_.a_, W_.c_),
1451 W_.x___,
1452 W_.e_ + W_.f_
1453 ));
1454 debug_tags!(#uv, #integrated, #vakint, #trace;
1455 stage = "to_vakint_integrand_after_flip_fuse",
1456 reduced = %reduced_label,
1457 dependent_subgraph = %dependent_subgraph_label,
1458 log.integrand = integrand_vakint,
1459 "Vakint trace after edge flip and fuse"
1460 );
1461
1462 let vakint_input_atom = integrand_vakint
1476 .replace(function!(vk_prop, W_.x__))
1477 .with(function!(vk_topo, function!(vk_prop, W_.x__)))
1478 .replace(function!(vk_topo, W_.x_) * function!(vk_topo, W_.y_))
1479 .repeat()
1480 .with(function!(vk_topo, W_.x_ * W_.y_));
1481 debug_tags!(#uv, #integrated, #vakint, #trace;
1482 stage = "to_vakint_integrand_before_split_terms",
1483 reduced = %reduced_label,
1484 dependent_subgraph = %dependent_subgraph_label,
1485 log.integrand = vakint_input_atom,
1486 "Vakint trace before split terms"
1487 );
1488
1489 let mut a = VakintExpression::try_from(vakint_input_atom)
1490 .wrap_err("could not split integrand into Vakint terms")?;
1491
1492 for (term_index, t) in a.0.iter_mut().enumerate() {
1493 debug_tags!(#uv, #integrated, #vakint, #inspect, #trace;
1494 stage = "to_vakint_integrand_term_initial",
1495 term_index = %term_index,
1496 reduced = %reduced_label,
1497 dependent_subgraph = %dependent_subgraph_label,
1498 log.integral = t.integral,
1499 log.numerator = t.numerator,
1500 "Starting integral"
1501 );
1502
1503 let mut graph = HedgeGraphBuilder::new();
1504 let pat = function!(
1506 vk_prop,
1507 W_.a_,
1508 function!(vakint::symbols::S.edge, W_.i_, W_.j_),
1509 W_.c_,
1510 W_.d_,
1511 W_.e_
1512 )
1513 .to_pattern();
1514 let mut nodemap = std::collections::HashMap::new();
1515
1516 struct ContractibleEdge {
1517 mom: Atom,
1518 mass_squared: Atom,
1519 power: i32,
1520 }
1521
1522 for m in t.integral.pattern_match(&pat, None, None) {
1523 let i: usize = m[&W_.i_].as_view().try_into().unwrap();
1524 let j: usize = m[&W_.j_].as_view().try_into().unwrap();
1525 nodemap.entry(i).or_insert_with(|| graph.add_node(()));
1526 nodemap.entry(j).or_insert_with(|| graph.add_node(()));
1527
1528 graph.add_edge(
1529 nodemap[&i],
1530 nodemap[&j],
1531 ContractibleEdge {
1532 mom: m[&W_.c_].clone(),
1533 mass_squared: m[&W_.d_].clone(),
1534 power: m[&W_.e_].as_view().try_into().unwrap(),
1535 },
1536 false,
1537 );
1538 }
1539
1540 let mut system = vec![];
1541 let mut momentum_variables = vec![];
1542
1543 let mut graph: HedgeGraph<ContractibleEdge, ()> = graph.build();
1544 let uncontracted_propagator_count =
1545 graph.iter_edges().filter(|(p, _, _)| p.is_paired()).count();
1546 let uncontracted_propagator_power_sum = graph
1547 .iter_edges()
1548 .filter_map(|(p, _, e)| p.is_paired().then_some(e.data.power))
1549 .sum::<i32>();
1550
1551 while let Some(same_mass_two_bond) = graph.a_bond(&|c| {
1552 let mut count = 0;
1553 let mut mass_squared = None;
1554 for (_, _, d) in graph.iter_edges_of(c) {
1555 count += 1;
1556
1557 if let Some(m) = &mass_squared
1558 && m != &d.data.mass_squared
1559 {
1560 return false;
1561 } else {
1562 mass_squared = Some(d.data.mass_squared.clone());
1563 }
1564 if count > 2 {
1565 return false;
1566 }
1567 }
1568 count == 2
1569 }) {
1570 let mut iter = same_mass_two_bond.included_iter();
1571 let first = graph[&iter.next().unwrap()];
1572 let second = graph[&iter.next().unwrap()];
1573 graph[first].power += graph[second].power;
1574 let mut to_contract: SuBitGraph = graph.empty_subgraph();
1575 to_contract.add(graph[&second].1);
1576 graph.contract_subgraph(&to_contract, ());
1577 }
1578 let mut nodes_to_merge = vec![];
1579
1580 for c in graph.connected_components(&graph.full_filter()) {
1581 let Some((nid, _, _)) = graph.iter_nodes_of(&c).next() else {
1582 continue;
1583 };
1584 nodes_to_merge.push(nid);
1585 }
1586
1587 if !nodes_to_merge.is_empty() {
1588 graph.identify_nodes(&nodes_to_merge, ());
1589 }
1590
1591 graph.forget_identification_history();
1592 debug_tags!(#uv, #integrated, #vakint, #graph, #dump;
1593 log.graph = %graph.base_dot(),
1594 "Graph"
1595 );
1596
1597 let mut new_integral: Atom = 1.into();
1598 for (p, eid, e) in graph.iter_edges() {
1599 let HedgePair::Paired { source, sink } = p else {
1600 continue;
1601 };
1602 new_integral *= function!(
1603 vk_prop,
1604 eid.0 + 1, function!(
1606 vakint::symbols::S.edge,
1607 graph.node_id(source).0,
1608 graph.node_id(sink).0
1609 ),
1610 &e.data.mom,
1611 &e.data
1612 .mass_squared
1613 .replace(GS.m_uv_expansion)
1614 .with(GS.m_uv_vacuum),
1615 e.data.power
1616 )
1617 }
1618
1619 t.integral = function!(vakint::symbols::S.topo, new_integral);
1621 let nloops = graph.cyclotomatic_number(&graph.full_filter());
1622 if nloops != retained_loop_edges.len() {
1627 return Err(eyre!(
1628 "Vakint term {term_index} for component {reduced_label} after prefix {dependent_subgraph_label} has {nloops} loop generators, expected {} retained active generators {:?}; a changed integration domain requires an explicit scaleless or factorization certificate",
1629 retained_loop_edges.len(),
1630 retained_loop_edges,
1631 ));
1632 }
1633 let contracted_propagator_count =
1634 graph.iter_edges().filter(|(p, _, _)| p.is_paired()).count();
1635 let contracted_propagator_power_sum = graph
1636 .iter_edges()
1637 .filter_map(|(p, _, e)| p.is_paired().then_some(e.data.power))
1638 .sum::<i32>();
1639 debug_tags!(#uv, #integrated, #vakint, #trace;
1640 stage = "to_vakint_integrand_term_after_graph_rebuild",
1641 term_index = %term_index,
1642 reduced = %reduced_label,
1643 dependent_subgraph = %dependent_subgraph_label,
1644 nloops = nloops,
1645 uncontracted_propagator_count = uncontracted_propagator_count,
1646 uncontracted_propagator_power_sum = uncontracted_propagator_power_sum,
1647 contracted_propagator_count = contracted_propagator_count,
1648 contracted_propagator_power_sum = contracted_propagator_power_sum,
1649 log.integral = t.integral,
1650 log.numerator = t.numerator,
1651 "Vakint trace"
1652 );
1653
1654 let lmb = graph.lmb();
1655 let mom_pat = function!(GS.emr_mom, W_.a_).to_pattern();
1656 for (p, e, ed) in graph.iter_edges() {
1657 if p.is_paired() {
1658 let loop_expr = lmb.loop_atom::<Atom>(e, GS.loop_mom, &[], false);
1660
1661 ed.data
1662 .mom
1663 .pattern_match(&mom_pat, None, None)
1664 .for_each(|m| {
1665 if usize::try_from(m[&W_.a_].as_view()).is_ok_and(|edge| {
1673 let edge = EdgeIndex(edge);
1674 source_lmbs.iter().any(|lmb| lmb.ext_from(edge).is_some())
1675 && !source_lmbs.iter().any(|lmb| lmb.loop_edges.contains(&edge))
1676 }) {
1677 return;
1678 }
1679 let var = mom_pat.replace_wildcards(&m).unwrap();
1680 if !momentum_variables.iter().any(|existing| existing == &var) {
1681 momentum_variables.push(var);
1682 }
1683 });
1684
1685 let is_zero = &ed.data.mom - loop_expr;
1689 system.push(is_zero);
1691 }
1692 }
1693
1694 let add_additional_args = [
1695 Replacement::new(
1696 function!(GS.emr_mom, W_.i_).to_pattern(),
1697 function!(GS.emr_mom, W_.i_, W_.a___),
1698 )
1699 .allow_new_wildcards_on_rhs(true),
1700 Replacement::new(
1701 function!(GS.loop_mom, W_.i_).to_pattern(),
1702 function!(GS.loop_mom, W_.i_, W_.a___),
1703 )
1704 .allow_new_wildcards_on_rhs(true),
1705 ];
1706 let momentum_solution =
1707 VakintMomentumSolution::solve(&system, &momentum_variables, &add_additional_args)
1708 .wrap_err("could not solve momentum system for Vakint integrand")?;
1709 t.numerator = momentum_solution.rewrite_numerator(&t.numerator);
1710 t.integral = momentum_solution.rewrite_integral(&t.integral, &add_additional_args);
1711 momentum_solution.ensure_free_variables_eliminated(
1712 term_index,
1713 "integral",
1714 &t.integral,
1715 &add_additional_args,
1716 )?;
1717 momentum_solution.ensure_free_variables_eliminated(
1718 term_index,
1719 "numerator",
1720 &t.numerator,
1721 &add_additional_args,
1722 )?;
1723 let retained_hard_coordinates = retained_loop_edges
1724 .iter()
1725 .map(|edge| function!(GS.emr_mom, usize::from(*edge)))
1726 .collect::<Vec<_>>();
1727 momentum_solution.ensure_variables_eliminated(
1728 term_index,
1729 "numerator",
1730 &t.numerator,
1731 &retained_hard_coordinates,
1732 &add_additional_args,
1733 )?;
1734 debug_tags!(#uv, #integrated, #vakint, #trace;
1735 stage = "to_vakint_integrand_term_after_momentum_solve",
1736 term_index = %term_index,
1737 reduced = %reduced_label,
1738 dependent_subgraph = %dependent_subgraph_label,
1739 log.integral = t.integral,
1740 log.numerator = t.numerator,
1741 "Vakint trace"
1742 );
1743
1744 let additional_normalization = parse!(&settings.additional_normalization);
1751 t.numerator *= additional_normalization.clone().pow(nloops);
1752 debug_tags!(#uv, #integrated, #vakint, #trace;
1753 stage = "to_vakint_integrand_term_after_loop_normalization",
1754 term_index = %term_index,
1755 reduced = %reduced_label,
1756 dependent_subgraph = %dependent_subgraph_label,
1757 nloops = nloops,
1758 log.additional_normalization = additional_normalization,
1759 log.numerator = t.numerator,
1760 "Vakint trace after loop normalization"
1761 );
1762
1763 t.numerator = t.numerator.metric_shorthand_to_dot();
1766 debug_tags!(#uv, #integrated, #vakint, #trace;
1767 stage = "to_vakint_integrand_term_after_metric_shorthand_to_dot",
1768 term_index = %term_index,
1769 reduced = %reduced_label,
1770 dependent_subgraph = %dependent_subgraph_label,
1771 log.integral = t.integral,
1772 log.numerator = t.numerator,
1773 "Vakint trace"
1774 );
1775 t.integral = t
1776 .integral
1777 .replace(function!(GS.loop_mom, W_.x___))
1778 .with(function!(vakint::symbols::S.k, W_.x___))
1779 .replace(function!(GS.emr_mom, W_.x___))
1780 .with(function!(vakint::symbols::S.p, W_.x___));
1781 t.numerator = Integrated::to_vakint_numerator(&t.numerator);
1782 debug_tags!(#uv, #integrated, #vakint, #trace;
1783 stage = "to_vakint_integrand_term_after_vakint_symbols",
1784 term_index = %term_index,
1785 reduced = %reduced_label,
1786 dependent_subgraph = %dependent_subgraph_label,
1787 log.integral = t.integral,
1788 log.numerator = t.numerator,
1789 "Vakint trace"
1790 );
1791 }
1792
1793 Ok(a)
1794}
1795
1796struct VakintMomentumSolution {
1797 replacements: Vec<Replacement>,
1798 free_variables: Vec<Atom>,
1799}
1800
1801impl VakintMomentumSolution {
1802 fn solve(
1803 system: &[Atom],
1804 variables: &[Atom],
1805 add_additional_args: &[Replacement],
1806 ) -> Result<Self> {
1807 if variables.is_empty() {
1808 return Ok(Self {
1809 replacements: vec![],
1810 free_variables: vec![],
1811 });
1812 }
1813 if system.is_empty() {
1814 return Ok(Self {
1815 replacements: vec![],
1816 free_variables: variables.to_vec(),
1817 });
1818 }
1819
1820 let solutions = Atom::solve(system)
1821 .wrt_with_exponent::<u8, _>(variables)
1822 .map_err(|source| eyre!("{source}"))?;
1823 let [solution] = solutions.iter().as_slice() else {
1824 return Err(eyre!(
1825 "expected one Vakint momentum solution, got {} branches",
1826 solutions.len()
1827 ));
1828 };
1829 let solution = variables
1830 .iter()
1831 .map(|variable| {
1832 let polynomial_variable =
1833 PolyVariable::try_from(variable.clone()).map_err(|source| eyre!("{source}"))?;
1834 if solution.free_variables().contains(&polynomial_variable) {
1835 Ok(variable.clone())
1836 } else {
1837 solution.get(&polynomial_variable).cloned().ok_or_else(|| {
1838 eyre!("Vakint momentum solution has no value for {polynomial_variable}")
1839 })
1840 }
1841 })
1842 .collect::<Result<Vec<_>>>()?;
1843 Ok(Self::from_solution(
1844 &solution,
1845 variables,
1846 add_additional_args,
1847 ))
1848 }
1849
1850 fn from_solution(
1851 solution: &[Atom],
1852 variables: &[Atom],
1853 add_additional_args: &[Replacement],
1854 ) -> Self {
1855 debug_assert_eq!(solution.len(), variables.len());
1856 let mut replacements = vec![];
1857 let mut free_variables = vec![];
1858
1859 for index in (0..variables.len()).rev() {
1860 let replacement = &solution[index];
1861 let variable = &variables[index];
1862 let replacement = replacement.replace_multiple(&replacements);
1863 if replacement == variable {
1864 free_variables.push(variable.clone());
1865 } else {
1866 replacements.push(Replacement::new(
1867 variable.replace_multiple(add_additional_args).to_pattern(),
1868 replacement
1869 .replace_multiple(add_additional_args)
1870 .to_pattern(),
1871 ));
1872 }
1873 }
1874
1875 Self {
1876 replacements,
1877 free_variables,
1878 }
1879 }
1880
1881 fn ensure_free_variables_eliminated(
1882 &self,
1883 term_index: usize,
1884 expression_kind: &str,
1885 expression: &Atom,
1886 add_additional_args: &[Replacement],
1887 ) -> Result<()> {
1888 self.ensure_variables_eliminated(
1889 term_index,
1890 expression_kind,
1891 expression,
1892 self.free_variables.iter(),
1893 add_additional_args,
1894 )
1895 }
1896
1897 fn ensure_variables_eliminated<'a>(
1898 &self,
1899 term_index: usize,
1900 expression_kind: &str,
1901 expression: &Atom,
1902 variables: impl IntoIterator<Item = &'a Atom>,
1903 add_additional_args: &[Replacement],
1904 ) -> Result<()> {
1905 for variable in variables {
1906 let bare_pattern = variable.to_pattern();
1907 let indexed_pattern = variable.replace_multiple(add_additional_args).to_pattern();
1908 if expression
1909 .pattern_match(&bare_pattern, None, None)
1910 .next()
1911 .is_some()
1912 || expression
1913 .pattern_match(&indexed_pattern, None, None)
1914 .next()
1915 .is_some()
1916 {
1917 return Err(eyre!(
1918 "Underdetermined Vakint momentum solve left free variable {} in {} of term {}",
1919 variable,
1920 expression_kind,
1921 term_index
1922 ));
1923 }
1924 }
1925 Ok(())
1926 }
1927
1928 fn rewrite_numerator(&self, numerator: &Atom) -> Atom {
1929 numerator
1930 .replace_multiple(&self.replacements)
1931 .normalize_dots()
1932 }
1933
1934 fn rewrite_integral(&self, integral: &Atom, add_additional_args: &[Replacement]) -> Atom {
1935 let free_variable_zero_replacements =
1936 self.free_variable_zero_replacements(add_additional_args);
1937 Self::normalize_topology_momenta(
1938 integral
1939 .replace_multiple(&self.replacements)
1940 .replace_multiple(&free_variable_zero_replacements),
1941 )
1942 }
1943
1944 fn free_variable_zero_replacements(
1945 &self,
1946 add_additional_args: &[Replacement],
1947 ) -> Vec<Replacement> {
1948 let mut replacements = Vec::with_capacity(2 * self.free_variables.len());
1949 for variable in &self.free_variables {
1950 replacements.push(Replacement::new(variable.to_pattern(), Atom::Zero));
1951 let indexed_variable = variable.replace_multiple(add_additional_args);
1952 if indexed_variable != *variable {
1953 replacements.push(Replacement::new(indexed_variable.to_pattern(), Atom::Zero));
1954 }
1955 }
1956 replacements
1957 }
1958
1959 fn normalize_topology_momenta(expression: Atom) -> Atom {
1960 if !expression
1961 .replace(function!(vakint::symbols::S.topo, W_.x_))
1962 .matches()
1963 {
1964 return expression;
1965 }
1966
1967 expression
1968 .replace(function!(
1969 vakint::symbols::S.prop,
1970 W_.edgeid_,
1971 W_.x_,
1972 W_.mom_,
1973 W_.mass_,
1974 W_.prop_
1975 ))
1976 .with_map(|matches| {
1977 function!(
1978 vakint::symbols::S.prop,
1979 matches.get(W_.edgeid_).unwrap().to_atom(),
1980 matches.get(W_.x_).unwrap().to_atom(),
1981 matches.get(W_.mom_).unwrap().to_atom().expand(),
1982 matches.get(W_.mass_).unwrap().to_atom(),
1983 matches.get(W_.prop_).unwrap().to_atom()
1984 )
1985 })
1986 }
1987}
1988
1989#[cfg(test)]
1990mod tests {
1991 use crate::{initialisation::test_initialise, utils::symbolica_ext::Q_I};
1992
1993 use super::*;
1994
1995 #[test]
1996 fn analytic_spin_simplification_preserves_shared_color_factor() {
1997 use idenso::{bis, coad, cof, color_f, color_idx, color_t, gamma};
1998 use spenso::{mink, trace, trace_sym};
1999
2000 test_initialise().unwrap();
2001 let a = coad!(8, color_a);
2002 let b = coad!(8, color_b);
2003 let c = coad!(8, color_c);
2004 let color = trace_sym!(cof!(3), color_t!(&a), color_t!(&b), color_t!(&c))
2005 + Atom::num(2) * color_f!(&a, &b, &c) * color_idx!(2, cof!(3));
2006 let spin = trace!(
2007 bis!(4),
2008 gamma!(mink!(4, mu)),
2009 gamma!(mink!(4, nu)),
2010 gamma!(mink!(4, rho)),
2011 gamma!(mink!(4, sigma))
2012 );
2013 let reduced_spin = simplify(&spin).unwrap();
2014 assert!(!reduced_spin.is_zero());
2015 assert!(!reduced_spin.contains_symbol(AGS.gamma));
2016 assert_eq!(simplify(&(&color * spin)).unwrap(), color * reduced_spin);
2019 }
2020
2021 #[test]
2022 fn analytic_spin_expansion_preserves_powered_scalar_products() {
2023 test_initialise().unwrap();
2024 let mink = Minkowski {}.new_rep(GS.dim);
2025 let mu = mink.to_symbolic([Aind::new_dummy().to_atom()]);
2026 let nu = mink.to_symbolic([Aind::new_dummy().to_atom()]);
2027 let bis = Bispinor {}.new_rep(4);
2028 let a = bis.to_symbolic([Aind::new_dummy().to_atom()]);
2029 let b = bis.to_symbolic([Aind::new_dummy().to_atom()]);
2030 let p = function!(GS.loop_mom, 1, mink.to_symbolic([]));
2031 let k = function!(GS.loop_mom, 2, mink.to_symbolic([]));
2032 let spin = function!(AGS.gamma, &a, &b, &mu) * function!(AGS.gamma, &b, &a, &nu);
2033 for product in [spenso::dot!(&p, &k), spenso::g!(&p, &k)] {
2034 for power in [2, 3] {
2035 let expected =
2038 Atom::num(4) * spenso::g!(&mu, &nu) * spenso::dot!(&p, &k).pow(power);
2039 assert_eq!(
2040 simplify(&(product.pow(power) * &spin))
2041 .unwrap()
2042 .normalize_dots(),
2043 expected
2044 );
2045 assert_eq!(
2048 simplify(&(product.pow(power) * &spin * spenso::g!(&mu, &p)))
2049 .unwrap()
2050 .normalize_dots(),
2051 Atom::num(4) * function!(GS.loop_mom, 1, &nu) * spenso::dot!(&p, &k).pow(power)
2052 );
2053 assert_eq!(
2054 simplify(
2055 &(product.pow(power) * &spin * spenso::g!(&mu, &p) * spenso::g!(&nu, &k)),
2056 )
2057 .unwrap()
2058 .normalize_dots(),
2059 Atom::num(4) * spenso::dot!(&p, &k).pow(power + 1)
2060 );
2061 }
2062 }
2063 }
2064
2065 #[test]
2066 fn projected_dirac_algebra_precedes_single_and_double_pole_expansion() {
2067 test_initialise().unwrap();
2068 let vakint = Vakint::new().unwrap();
2069 let epsilon = Atom::var(GS.dim_epsilon);
2070 let radial = vakint::symbols::S.dot(
2071 function!(vakint::symbols::S.k, 1),
2072 function!(vakint::symbols::S.k, 2),
2073 );
2074 let mink = Minkowski {}.new_rep(GS.dim);
2075 let mu = mink.to_symbolic([Aind::new_dummy().to_atom()]);
2076 let nu = mink.to_symbolic([Aind::new_dummy().to_atom()]);
2077 let bis = Bispinor {}.new_rep(4);
2078 let a = bis.to_symbolic([Aind::new_dummy().to_atom()]);
2079 let b = bis.to_symbolic([Aind::new_dummy().to_atom()]);
2080 let c = bis.to_symbolic([Aind::new_dummy().to_atom()]);
2081
2082 for project_onto_tensor_integrals in [true, false] {
2083 for use_dot_product_notation in [false, true] {
2084 let settings = vakint::VakintSettings {
2085 use_dot_product_notation,
2086 project_onto_tensor_integrals,
2087 ..VakintSettings::default().true_settings()
2088 };
2089 let integrated = Integrated::new(&vakint, &settings);
2090 for closed in [false, true] {
2091 let numerator = function!(GS.loop_mom, 1, &mu)
2094 * function!(GS.loop_mom, 2, &nu)
2095 * function!(AGS.gamma, &a, &b, &mu)
2096 * function!(AGS.gamma, &b, if closed { &a } else { &c }, &nu);
2097 let mut term = vakint::VakintTerm {
2098 integral: vakint::vakint_parse!("topo(I2L(mUVsq,1,1,1))").unwrap(),
2099 numerator: Integrated::to_vakint_numerator(&numerator),
2100 vectors: vec![("k".into(), 1), ("k".into(), 2)],
2101 };
2102 term.tensor_reduce(&vakint, &settings).unwrap();
2103 let spin = if closed {
2104 Atom::num(4)
2105 } else {
2106 function!(ETS.metric, &a, &c)
2107 };
2108 let late_four_dimensional = simplify(
2109 &Integrated::restore_numerator(
2110 Vakint::convert_to_dot_notation(&settings, term.numerator.as_view())
2111 .unwrap(),
2112 0,
2113 )
2114 .replace(GS.dim)
2115 .with(4),
2116 )
2117 .unwrap();
2118 assert!(
2119 !truncate(
2120 &series(
2121 &(late_four_dimensional
2122 .replace(radial.to_pattern())
2123 .with(epsilon.pow(-1))
2124 - &spin * epsilon.pow(-1)),
2125 1,
2126 )
2127 .unwrap(),
2128 true,
2129 )
2130 .is_zero(),
2131 "the old late-4D contraction must miss a nonzero finite term"
2132 );
2133 for evanescent_power in 0..=2 {
2134 let projected = &term.numerator
2135 * (Atom::var(GS.dim) - Atom::num(4)).pow(evanescent_power);
2136 let resolved = integrated
2137 .simplify_projected_numerator(&projected, 0)
2138 .unwrap();
2139 assert!(!resolved.contains_symbol(AGS.gamma));
2140 assert!(!resolved.contains_symbol(SPENSO_TAG.trace));
2141 assert!(!resolved.contains_symbol(GS.dim));
2142 let coefficient = &spin * (Atom::num(-2) * &epsilon).pow(evanescent_power);
2143 for pole in [
2144 epsilon.pow(-1) + Atom::num(5) + Atom::num(7) * &epsilon,
2145 Atom::num(2) * epsilon.pow(-2)
2146 + Atom::num(3) * epsilon.pow(-1)
2147 + Atom::num(5)
2148 + Atom::num(7) * &epsilon,
2149 ] {
2150 let evaluated = resolved
2155 .replace(radial.to_pattern())
2156 .with(pole.to_pattern());
2157 assert!(
2158 series(&(evaluated - &coefficient * pole), 2)
2159 .unwrap()
2160 .to_atom()
2161 .is_zero(),
2162 "project={project_onto_tensor_integrals}, closed={closed}, dot={use_dot_product_notation}, evanescent power={evanescent_power}"
2163 );
2164 }
2165 }
2166 }
2167 }
2168 }
2169 }
2170
2171 #[test]
2172 fn projected_dirac_numerator_matches_scalar_master_before_backend_truncation() {
2173 test_initialise().unwrap();
2174 let vakint = Vakint::new().unwrap();
2175 let mut results = Vec::new();
2176 for project_onto_tensor_integrals in [true, false] {
2177 let settings = vakint::VakintSettings {
2178 number_of_terms_in_epsilon_expansion: 3,
2181 project_onto_tensor_integrals,
2182 ..VakintSettings::default().true_settings()
2183 };
2184 let integrated = Integrated::new(&vakint, &settings);
2185 let mink = Minkowski {}.new_rep(GS.dim);
2186 let mu = mink.to_symbolic([Aind::new_dummy().to_atom()]);
2187 let nu = mink.to_symbolic([Aind::new_dummy().to_atom()]);
2188 let bis = Bispinor {}.new_rep(4);
2189 let a = bis.to_symbolic([Aind::new_dummy().to_atom()]);
2190 let b = bis.to_symbolic([Aind::new_dummy().to_atom()]);
2191 let numerator = function!(GS.loop_mom, 1, &mu)
2192 * function!(GS.loop_mom, 2, &nu)
2193 * function!(AGS.gamma, &a, &b, &mu)
2194 * function!(AGS.gamma, &b, &a, &nu);
2195 let mut projected = vakint::VakintTerm {
2196 integral: vakint::vakint_parse!("topo(I2L(mUVsq,1,1,1))").unwrap(),
2197 numerator: Integrated::to_vakint_numerator(&numerator),
2198 vectors: vec![("k".into(), 1), ("k".into(), 2)],
2199 };
2200 projected.tensor_reduce(&vakint, &settings).unwrap();
2201 let d_minus_four = Atom::var(GS.dim) - Atom::num(4);
2202 projected.numerator = integrated
2203 .simplify_projected_numerator(
2204 &(&projected.numerator * (&d_minus_four + d_minus_four.pow(2))),
2205 0,
2206 )
2207 .unwrap();
2208 let epsilon = Atom::var(GS.dim_epsilon);
2209 let mut scalar = vakint::VakintTerm {
2210 numerator: Atom::num(4)
2211 * (Atom::num(-2) * &epsilon + Atom::num(4) * epsilon.pow(2))
2212 * vakint::symbols::S.dot(
2213 function!(vakint::symbols::S.k, 1),
2214 function!(vakint::symbols::S.k, 2),
2215 ),
2216 ..projected.clone()
2217 };
2218 let resolved_input = projected.numerator.clone();
2219 assert!(
2223 (&resolved_input - &scalar.numerator)
2224 .try_to_rational_polynomial::<_, _, u32>(&*Q_I, &*Q_I, None)
2225 .expect("the analytic scalar input difference is rational over Q(i)")
2226 .numerator
2227 .is_zero(),
2228 "project={project_onto_tensor_integrals}: resolved input {resolved_input} differs from scalar oracle {}",
2229 scalar.numerator
2230 );
2231 projected.evaluate_integral(&vakint, &settings).unwrap();
2232 scalar.evaluate_integral(&vakint, &settings).unwrap();
2233 assert!(
2234 !truncate(&series(&scalar.numerator, 1).unwrap(), true).is_zero(),
2235 "the independently integrated evanescent scalar master has a nonzero finite term"
2236 );
2237 let difference = series(&(&projected.numerator - &scalar.numerator), 1)
2238 .unwrap()
2239 .to_atom();
2240 assert!(
2244 difference.expand().is_zero(),
2245 "project={project_onto_tensor_integrals}: resolved input {resolved_input}; projected result {}; scalar result {}; difference {difference}; expanded difference {}",
2246 projected.numerator,
2247 scalar.numerator,
2248 difference.expand()
2249 );
2250 results.push(projected.numerator);
2251 }
2252 assert!(
2253 series(&(&results[0] - &results[1]), 1)
2254 .unwrap()
2255 .to_atom()
2256 .expand()
2257 .is_zero()
2258 );
2259 }
2260
2261 #[test]
2262 fn scalar_dimensions_are_substituted_inside_functions_but_not_lorentz_slots() {
2263 test_initialise().unwrap();
2264 let slot = Minkowski {}
2265 .new_rep(GS.dim)
2266 .to_symbolic([Aind::new_dummy().to_atom()]);
2267 let logarithm = symbolica::symbol!("log");
2268 let tensor = symbolica::symbol!("dimension_test_tensor");
2269 let input = function!(logarithm, GS.dim) * function!(tensor, &slot);
2270 let expected = function!(
2271 logarithm,
2272 Atom::num(4) - Atom::num(2) * Atom::var(GS.dim_epsilon)
2273 ) * function!(tensor, &slot);
2274 assert_eq!(Integrated::dimensionally_regularized(&input), expected);
2275 }
2276
2277 #[test]
2278 fn analytic_uv_spin_algebra_rejects_unsupported_residual_contractions() {
2279 test_initialise().unwrap();
2280 let bis = Bispinor {}.new_rep(4);
2281 let a = bis.to_symbolic([Aind::new_dummy().to_atom()]);
2282 let b = bis.to_symbolic([Aind::new_dummy().to_atom()]);
2283 let mu = Minkowski {}
2284 .new_rep(GS.dim)
2285 .to_symbolic([Aind::new_dummy().to_atom()]);
2286 let nu = Minkowski {}
2287 .new_rep(GS.dim)
2288 .to_symbolic([Aind::new_dummy().to_atom()]);
2289 let unknown = function!(
2290 symbolica::symbol!("unsupported_dirac_factor"),
2291 SPENSO_TAG.chain_in,
2292 SPENSO_TAG.chain_out
2293 );
2294 let gamma = function!(AGS.gamma, SPENSO_TAG.chain_in, SPENSO_TAG.chain_out, &mu);
2295 for (input, message) in [
2296 (function!(AGS.sigma, &a, &b, &mu, &nu), "Sigma tensors"),
2297 (
2298 function!(SPENSO_TAG.trace, bis.to_symbolic([]), &unknown),
2299 "unresolved Dirac trace",
2300 ),
2301 (
2302 function!(
2303 SPENSO_TAG.trace,
2304 Minkowski {}.new_rep(GS.dim).to_symbolic([]),
2305 &unknown
2306 ),
2307 "unresolved Lorentz trace",
2308 ),
2309 ] {
2310 assert!(simplify(&input).unwrap_err().to_string().contains(message));
2311 }
2312 let residual = function!(SPENSO_TAG.chain, &a, &b, &gamma, &unknown, &gamma);
2313 assert!(
2314 Integrated::ensure_resolved_lorentz_contractions(&simplify(&residual).unwrap())
2315 .is_err()
2316 );
2317 let open = simplify(&function!(AGS.gamma, &a, &b, &mu)).unwrap();
2319 assert!(Integrated::ensure_resolved_lorentz_contractions(&open).is_ok());
2320 }
2321
2322 #[test]
2323 fn residual_tensor_contractions_are_checked_per_analytic_branch() {
2324 test_initialise().unwrap();
2325 let bis = Bispinor {}.new_rep(4);
2326 let [a, b, c, d] =
2327 std::array::from_fn::<_, 4, _>(|_| bis.to_symbolic([Aind::new_dummy().to_atom()]));
2328 let mink = Minkowski {}.new_rep(GS.dim);
2329 let [mu, nu, rho] =
2330 std::array::from_fn::<_, 3, _>(|_| mink.to_symbolic([Aind::new_dummy().to_atom()]));
2331 let left = function!(AGS.gamma, &a, &b, &mu);
2332 let right = function!(AGS.gamma, &c, &d, &mu);
2333 assert!(Integrated::ensure_resolved_lorentz_contractions(&(&left * &right)).is_err());
2334 let compact = function!(
2335 SPENSO_TAG.dot,
2336 function!(AGS.gamma, &a, &b, mink.to_symbolic([])),
2337 function!(AGS.gamma, &c, &d, mink.to_symbolic([]))
2338 );
2339 assert!(Integrated::ensure_resolved_lorentz_contractions(&compact).is_err());
2340 let same_chain = function!(
2341 SPENSO_TAG.dot,
2342 function!(AGS.gamma, &a, &b, mink.to_symbolic([])),
2343 function!(AGS.gamma, &b, &c, mink.to_symbolic([]))
2344 );
2345 let same_chain = simplify(&same_chain).unwrap();
2346 assert!(
2347 (&same_chain - Atom::var(GS.dim) * function!(ETS.metric, &a, &c))
2348 .expand()
2349 .is_zero(),
2350 "a compact contraction within one gamma chain must close to d times its spinor identity: {same_chain}"
2351 );
2352 assert!(Integrated::ensure_resolved_lorentz_contractions(&same_chain).is_ok());
2353 let free =
2354 &left * function!(AGS.gamma, &c, &d, &nu) + function!(AGS.gamma, &a, &b, &nu) * &right;
2355 assert!(Integrated::ensure_resolved_lorentz_contractions(&free).is_ok());
2356 assert!(
2357 Integrated::ensure_resolved_lorentz_contractions(&function!(ETS.metric, &mu, &nu))
2358 .is_ok()
2359 );
2360 let ambiguous = function!(
2361 SPENSO_TAG.dot,
2362 function!(
2363 AGS.sigma,
2364 &a,
2365 &b,
2366 mink.to_symbolic([]),
2367 mink.to_symbolic([])
2368 ),
2369 function!(
2370 AGS.sigma,
2371 &c,
2372 &d,
2373 mink.to_symbolic([]),
2374 mink.to_symbolic([])
2375 )
2376 );
2377 assert!(Integrated::ensure_resolved_lorentz_contractions(&ambiguous).is_err());
2378 let slash = function!(
2379 AGS.gamma,
2380 &a,
2381 &b,
2382 function!(GS.emr_mom, 0, mink.to_symbolic([]))
2383 );
2384 assert!(Integrated::ensure_resolved_lorentz_contractions(&slash).is_ok());
2385 let slash_product = &slash
2386 * function!(
2387 AGS.gamma,
2388 &c,
2389 &d,
2390 function!(GS.emr_mom, 1, mink.to_symbolic([]))
2391 );
2392 assert!(Integrated::ensure_resolved_lorentz_contractions(&slash_product).is_ok());
2393 assert!(
2394 Integrated::ensure_resolved_lorentz_contractions(
2395 &(&left / (Atom::var(GS.dim) - Atom::one()))
2396 )
2397 .is_ok()
2398 );
2399
2400 let factors = [&mu, &nu, &rho]
2404 .map(|index| function!(AGS.gamma, SPENSO_TAG.chain_in, SPENSO_TAG.chain_out, index));
2405 let operator = function!(
2406 SPENSO_TAG.chain,
2407 &a,
2408 &b,
2409 &factors[0],
2410 &factors[1],
2411 &factors[2]
2412 ) * function!(
2413 SPENSO_TAG.chain,
2414 &c,
2415 &d,
2416 &factors[0],
2417 &factors[1],
2418 &factors[2]
2419 );
2420 assert!(Integrated::ensure_resolved_lorentz_contractions(&operator).is_err());
2421 let closed = function!(
2422 SPENSO_TAG.trace,
2423 bis.to_symbolic([]),
2424 &factors[0],
2425 &factors[1],
2426 &factors[2],
2427 &factors[0],
2428 &factors[1],
2429 &factors[2]
2430 );
2431 let closed = Integrated::dimensionally_regularized(&simplify(&closed).unwrap());
2432 let epsilon = Atom::var(GS.dim_epsilon);
2433 assert_eq!(
2434 truncate(&series(&(closed / epsilon), 0).unwrap(), true),
2435 Atom::num(32)
2436 );
2437 }
2438
2439 #[test]
2440 fn integrated_counterterm_projects_one_laurent_expansion() {
2441 test_initialise().unwrap();
2442
2443 let epsilon = Atom::var(GS.dim_epsilon);
2444 let expansion = series(
2445 &(Atom::num(2) * epsilon.pow(-2)
2446 + Atom::num(3) * epsilon.pow(-1)
2447 + Atom::num(5)
2448 + Atom::num(7) * &epsilon),
2449 2,
2450 )
2451 .unwrap();
2452 let integrated = IntegratedCts {
2453 expansion,
2454 scale_power: 4,
2455 };
2456 let scale = Atom::var(GS.integrated_loop_scale).pow(4);
2457
2458 assert_eq!(
2459 integrated.pole_atom().expand(),
2460 ((Atom::num(2) * epsilon.pow(-2) + Atom::num(3) * epsilon.pow(-1)) * &scale).expand()
2461 );
2462 assert_eq!(
2463 integrated.finite_counterterm_atom().expand(),
2464 (-(Atom::num(5) + Atom::num(7) * epsilon) * scale).expand()
2465 );
2466 assert_eq!(
2467 integrated.physical_finite_counterterm_atom(),
2468 -(Atom::num(5) + Atom::num(7) * Atom::var(GS.dim_epsilon))
2469 );
2470 }
2471
2472 #[test]
2473 fn finite_projection_retains_child_epsilon_terms_until_parent_poles_multiply() {
2474 test_initialise().unwrap();
2475 let epsilon = Atom::var(GS.dim_epsilon);
2476 let child = IntegratedCts {
2477 expansion: series(
2478 &(Atom::num(2) + Atom::num(3) * &epsilon + Atom::num(5) * epsilon.pow(2)),
2479 3,
2480 )
2481 .unwrap(),
2482 scale_power: 4,
2483 };
2484 let parent_poles = epsilon.pow(-2) + Atom::num(7) * epsilon.pow(-1);
2485 let nested = IntegratedCts {
2486 expansion: series(
2487 &(child.physical_finite_counterterm_atom() * parent_poles),
2488 1,
2489 )
2490 .unwrap(),
2491 scale_power: 8,
2492 };
2493 let finite = nested.physical_finite_counterterm_atom();
2495 assert_eq!(
2496 finite.replace(GS.dim_epsilon).with(Atom::Zero),
2497 Atom::num(26)
2498 );
2499 assert!(
2500 (finite - Atom::num(26) - Atom::num(35) * &epsilon)
2501 .expand()
2502 .is_zero()
2503 );
2504 assert_eq!(
2505 nested.physical_pole_atom(),
2506 -Atom::num(2) * epsilon.pow(-2) - Atom::num(17) * epsilon.pow(-1),
2507 );
2508 }
2509
2510 #[test]
2511 fn factorized_product_projects_each_component() {
2512 test_initialise().unwrap();
2513
2514 let epsilon = Atom::var(GS.dim_epsilon);
2515 let factors = [2, 3, 5].map(|finite| IntegratedCts {
2516 expansion: series(&(epsilon.pow(-1) + Atom::num(finite)), 1).unwrap(),
2517 scale_power: 0,
2518 });
2519 let product = IntegratedCts::factorized_product(&factors[..2], 1).unwrap();
2520
2521 assert_eq!(product.physical_pole_atom(), epsilon.pow(-2));
2522 assert_eq!(product.physical_finite_counterterm_atom(), Atom::num(6));
2523
2524 let product = IntegratedCts::factorized_product(&factors, 1).unwrap();
2525 assert_eq!(product.physical_pole_atom(), epsilon.pow(-3));
2526 assert_eq!(product.physical_finite_counterterm_atom(), Atom::num(-30));
2527 }
2528
2529 #[test]
2554 fn vakint_dot_conversion_keeps_loop_momentum_tagged_until_to_dots() {
2555 test_initialise().unwrap();
2556
2557 let mink = Minkowski {}.new_rep(GS.dim).to_symbolic([]);
2558 let numerator = function!(
2559 ETS.metric,
2560 function!(GS.emr_mom, 0, mink.clone()),
2561 function!(GS.loop_mom, 1, mink.clone())
2562 );
2563
2564 let converted = numerator
2565 .simplify_metrics()
2566 .to_dots()
2567 .replace(function!(GS.loop_mom, W_.x___))
2568 .with(function!(vakint::symbols::S.k, W_.x___))
2569 .replace(function!(GS.emr_mom, W_.x___))
2570 .with(function!(vakint::symbols::S.p, W_.x___))
2571 .replace(function!(
2572 SPENSO_TAG.dot,
2573 function!(W_.a_, W_.a___, Minkowski {}.new_rep(GS.dim).to_symbolic([])),
2574 function!(W_.b_, W_.b___, Minkowski {}.new_rep(GS.dim).to_symbolic([]))
2575 ))
2576 .with(vakint::symbols::S.dot(function!(W_.a_, W_.a___), function!(W_.b_, W_.b___)));
2577
2578 assert_eq!(
2579 converted,
2580 vakint::symbols::S.dot(
2581 function!(vakint::symbols::S.p, 0),
2582 function!(vakint::symbols::S.k, 1)
2583 )
2584 );
2585 }
2586
2587 #[test]
2588 fn underdetermined_vakint_momentum_solve_tracks_free_variables() {
2589 test_initialise().unwrap();
2590
2591 let q0 = function!(GS.emr_mom, 0);
2592 let q1 = function!(GS.emr_mom, 1);
2593 let q2 = function!(GS.emr_mom, 2);
2594 let k0 = function!(GS.loop_mom, 0);
2595 let k1 = function!(GS.loop_mom, 1);
2596 let edge_3 = -&q0 - &q1 - &q2;
2597 let system = vec![&q0 - &k0, &edge_3 - &k1];
2598 let variables = vec![q0.clone(), q1.clone(), q2.clone()];
2599
2600 let solution = VakintMomentumSolution::solve(&system, &variables, &[]).unwrap();
2601
2602 assert_eq!(solution.free_variables, vec![q2]);
2603 assert!(
2604 solution
2605 .ensure_free_variables_eliminated(0, "numerator", &solution.free_variables[0], &[])
2606 .is_err()
2607 );
2608 }
2609
2610 #[test]
2611 fn underdetermined_vakint_momentum_solve_projects_topology() {
2612 test_initialise().unwrap();
2613
2614 let q1 = function!(GS.emr_mom, 1);
2615 let q2 = function!(GS.emr_mom, 2);
2616 let k0 = function!(GS.loop_mom, 0);
2617 let system = vec![-&q1 - &q2 - &k0];
2618 let variables = vec![q1.clone(), q2.clone()];
2619 let topology = function!(
2620 vakint::symbols::S.topo,
2621 function!(
2622 vakint::symbols::S.prop,
2623 1,
2624 function!(vakint::symbols::S.edge, 0, 0),
2625 -&q1 - &q2,
2626 GS.m_uv_vacuum,
2627 1
2628 )
2629 );
2630
2631 let solution = VakintMomentumSolution::solve(&system, &variables, &[]).unwrap();
2632
2633 solution
2634 .ensure_free_variables_eliminated(0, "numerator", &Atom::from(1), &[])
2635 .unwrap();
2636 assert_eq!(
2637 solution.rewrite_integral(&topology, &[]),
2638 function!(
2639 vakint::symbols::S.topo,
2640 function!(
2641 vakint::symbols::S.prop,
2642 1,
2643 function!(vakint::symbols::S.edge, 0, 0),
2644 k0,
2645 GS.m_uv_vacuum,
2646 1
2647 )
2648 )
2649 );
2650 }
2651
2652 #[test]
2653 fn empty_vakint_momentum_solve_treats_variables_as_free() {
2654 test_initialise().unwrap();
2655
2656 let q0 = function!(GS.emr_mom, 0);
2657 let no_variables = VakintMomentumSolution::solve(&[], &[], &[]).unwrap();
2658 let free_variable =
2659 VakintMomentumSolution::solve(&[], std::slice::from_ref(&q0), &[]).unwrap();
2660
2661 assert!(no_variables.free_variables.is_empty());
2662 assert_eq!(free_variable.free_variables, vec![q0]);
2663 }
2664
2665 #[test]
2666 fn vakint_preparation_reduces_color_before_opening_traces() {
2667 use crate::{
2668 dot,
2669 graph::{Graph, parse::IntoGraph},
2670 };
2671 use idenso::{coad, cof, color_cas, color_f, color_idx, color_t};
2672 use spenso::{g, trace_sym};
2673
2674 test_initialise().unwrap();
2675 let graph: Graph = dot!(digraph color_vakint_tadpole {
2676 edge [num=1 mass=1]
2677 node [num=1]
2678 incoming [style=invis]
2679 outgoing [style=invis]
2680 incoming -> a [id=0]
2681 a -> a [id=1 lmb_id=0]
2682 a -> outgoing [id=2]
2683 })
2684 .unwrap();
2685 let [a, b, c, d] = [
2686 "color_vakint_a",
2687 "color_vakint_b",
2688 "color_vakint_c",
2689 "color_vakint_d",
2690 ]
2691 .map(|name| coad!(8, Atom::var(symbol!(name))));
2692 let trace = trace_sym!(cof!(3), color_t!(&a), color_t!(&b), color_t!(&c));
2693 let index = color_idx!(2, cof!(3));
2694 let color = color_f!(&d, &b, &c) * (trace + Atom::i() / 2 * color_f!(&a, &b, &c) * &index);
2695 let coefficient = parse!("(color_x+color_y)^3*(color_u+color_v)");
2696 let denominator = function!(
2697 GS.den,
2698 1,
2699 function!(GS.emr_mom, 1),
2700 Atom::var(GS.m_uv_vacuum).pow(2)
2701 );
2702 let actual = to_vakint_integrand(
2703 &(color * &coefficient / denominator.pow(2)),
2704 &graph,
2705 &graph.full_filter(),
2706 &graph.empty_subgraph::<SuBitGraph>(),
2707 &[(
2708 graph.full_filter(),
2709 graph.full_filter(),
2710 graph.loop_momentum_basis.clone(),
2711 )],
2712 &VakintSettings {
2713 additional_normalization: "1".to_string(),
2714 ..Default::default()
2715 },
2716 true,
2717 )
2718 .unwrap();
2719
2720 let expected = Atom::i() / 2 * color_cas!(2, coad!(8)) * index * g!(&a, &d) * coefficient;
2723 assert_eq!(actual.0.len(), 1);
2724 assert_eq!(actual.0[0].numerator, expected);
2725 }
2726
2727 #[test]
2728 fn vakint_conversion_preserves_squared_masses() {
2729 use crate::{
2730 dot,
2731 graph::{Graph, parse::IntoGraph},
2732 };
2733
2734 test_initialise().unwrap();
2735 let graph: Graph = dot!(digraph vakint_mass_tadpole {
2736 edge [num=1 mass=2]
2737 node [num=1]
2738 incoming [style=invis]
2739 outgoing [style=invis]
2740 incoming -> a [id=0]
2741 a -> a [id=1 lmb_id=0]
2742 a -> outgoing [id=2]
2743 })
2744 .unwrap();
2745 let momentum = function!(GS.emr_mom, 1);
2746 let indexed_momentum =
2747 function!(GS.emr_mom, 1, Minkowski {}.new_rep(GS.dim).to_symbolic([]));
2748 let settings = VakintSettings {
2749 additional_normalization: "1".to_string(),
2750 ..Default::default()
2751 };
2752 for mass in [Atom::num(2), parse!("m_phys")] {
2755 let mass_squared = mass.pow(2);
2756 let denominator = function!(
2757 GS.den,
2758 1,
2759 &momentum,
2760 &mass_squared,
2761 function!(SPENSO_TAG.dot, &indexed_momentum, &indexed_momentum) - &mass_squared
2762 );
2763 for substitute_masses_to_m_uv in [false, true] {
2764 let actual = to_vakint_integrand(
2765 &denominator.pow(-3),
2766 &graph,
2767 &graph.full_filter(),
2768 &graph.empty_subgraph::<SuBitGraph>(),
2769 &[(
2770 graph.full_filter(),
2771 graph.full_filter(),
2772 graph.loop_momentum_basis.clone(),
2773 )],
2774 &settings,
2775 substitute_masses_to_m_uv,
2776 )
2777 .unwrap();
2778 assert_eq!(actual.0.len(), 1);
2779 assert_eq!(actual.0[0].numerator, Atom::one());
2780 assert_eq!(
2781 actual.0[0].integral,
2782 function!(
2783 vakint::symbols::S.topo,
2784 function!(
2785 vakint::symbols::S.prop,
2786 1,
2787 function!(vakint::symbols::S.edge, 0, 0),
2788 function!(vakint::symbols::S.k, 0),
2789 if substitute_masses_to_m_uv {
2790 Atom::var(GS.m_uv_vacuum).pow(2)
2791 } else {
2792 mass_squared.clone()
2793 },
2794 3
2795 )
2796 )
2797 );
2798 }
2799 }
2800 }
2801
2802 #[test]
2803 fn vakint_conversion_rejects_a_lost_active_loop() {
2804 use crate::{
2805 dot,
2806 graph::{Graph, parse::IntoGraph},
2807 };
2808
2809 test_initialise().unwrap();
2810 let graph: Graph = dot!(digraph cancelled_tadpole_loop {
2811 edge [num=1 mass=2]
2812 node [num=1]
2813 a -> a [id=0 lmb_id=0]
2814 a -> a [id=1 lmb_id=1]
2815 })
2816 .unwrap();
2817 let remaining = graph.get_edge_subgraph(EdgeIndex(1));
2818 let input = graph.denominator(&remaining, |_| -3);
2822 let error = to_vakint_integrand(
2823 &input,
2824 &graph,
2825 &graph.full_filter(),
2826 &graph.empty_subgraph::<SuBitGraph>(),
2827 &[(
2828 graph.full_filter(),
2829 graph.full_filter(),
2830 graph.loop_momentum_basis.clone(),
2831 )],
2832 &VakintSettings::default(),
2833 false,
2834 )
2835 .unwrap_err();
2836 assert!(
2837 error
2838 .to_string()
2839 .contains("has 1 loop generators, expected 2")
2840 );
2841 let remaining_lmb = graph.lmb_of(&remaining);
2842 let converted = to_vakint_integrand(
2843 &input,
2844 &graph,
2845 &graph.full_filter(),
2846 &graph.empty_subgraph::<SuBitGraph>(),
2847 &[(
2848 graph.full_filter(),
2849 graph.full_filter(),
2850 remaining_lmb.clone(),
2851 )],
2852 &VakintSettings::default(),
2853 false,
2854 )
2855 .unwrap();
2856 assert_eq!(converted.0.len(), 1);
2857 }
2858
2859 #[test]
2860 fn affine_vakint_routing_keeps_external_momenta_fixed() {
2861 use crate::{
2862 dot,
2863 graph::{Graph, parse::IntoGraph},
2864 };
2865
2866 test_initialise().unwrap();
2867 let graph: Graph = dot!(digraph affine_vakint_tadpole {
2868 edge [num=1 mass=1]
2869 node [num=1]
2870 incoming [style=invis]
2871 outgoing [style=invis]
2872 incoming -> a [id=0]
2873 a -> a [id=1 lmb_id=0]
2874 a -> outgoing [id=2]
2875 })
2876 .unwrap();
2877 let hard = function!(GS.emr_mom, 1) - function!(GS.emr_mom, 0);
2878 let minkowski = Minkowski {}.new_rep(GS.dim).to_symbolic([]);
2879 let indexed_hard =
2880 function!(GS.emr_mom, 1, &minkowski) - function!(GS.emr_mom, 0, &minkowski);
2881 let numerator = function!(
2882 SPENSO_TAG.dot,
2883 function!(GS.emr_mom, 0, &minkowski),
2884 &indexed_hard
2885 ) * function!(
2886 SPENSO_TAG.dot,
2887 function!(GS.emr_mom, 2, &minkowski),
2888 &indexed_hard
2889 );
2890 let settings = VakintSettings {
2891 additional_normalization: "1".to_string(),
2892 ..Default::default()
2893 };
2894 let mut child_frame = graph.empty_lmb();
2898 child_frame.ext_edges.push(EdgeIndex(1));
2899 for source_lmbs in [
2900 vec![&graph.loop_momentum_basis],
2901 vec![&child_frame, &graph.loop_momentum_basis],
2902 ] {
2903 for sign in [-1, 1] {
2904 let denominator = function!(
2905 GS.den,
2906 1,
2907 Atom::num(sign) * &hard,
2908 Atom::var(GS.m_uv_vacuum).pow(2),
2909 function!(SPENSO_TAG.dot, &indexed_hard, &indexed_hard)
2910 - Atom::var(GS.m_uv_vacuum).pow(2)
2911 );
2912 let actual = to_vakint_integrand(
2913 &(&numerator / denominator.pow(3)),
2914 &graph,
2915 &graph.full_filter(),
2916 &graph.empty_subgraph::<SuBitGraph>(),
2917 &source_lmbs
2918 .iter()
2919 .map(|lmb| {
2920 let owners = if lmb.loop_edges.is_empty() {
2921 graph.empty_subgraph::<SuBitGraph>()
2922 } else {
2923 graph.full_filter()
2924 };
2925 (owners.clone(), owners, (*lmb).clone())
2926 })
2927 .collect::<Vec<_>>(),
2928 &settings,
2929 true,
2930 )
2931 .unwrap();
2932 let actual = Atom::from(actual)
2935 .replace(function!(vakint::symbols::S.topo, W_.x_))
2936 .with(W_.x_)
2937 .replace(function!(
2938 vakint::symbols::S.prop,
2939 W_.a_,
2940 W_.b_,
2941 W_.c_,
2942 W_.d_,
2943 W_.e_
2944 ))
2945 .with(
2946 (function!(vakint::symbols::S.dot, W_.c_, W_.c_) - Atom::var(W_.d_))
2947 .pow(-Atom::var(W_.e_)),
2948 );
2949 let expected_numerator = function!(
2950 vakint::symbols::S.dot,
2951 function!(vakint::symbols::S.p, 0),
2952 function!(vakint::symbols::S.k, 0)
2953 ) * function!(
2954 vakint::symbols::S.dot,
2955 function!(vakint::symbols::S.p, 2),
2956 function!(vakint::symbols::S.k, 0)
2957 );
2958 let radial = function!(
2959 vakint::symbols::S.dot,
2960 function!(vakint::symbols::S.k, 0),
2961 function!(vakint::symbols::S.k, 0)
2962 ) - Atom::var(GS.m_uv_vacuum).pow(2);
2963 assert!(
2964 (actual.collect_factors()
2965 - (expected_numerator / radial.pow(3)).collect_factors())
2966 .is_zero(),
2967 "the complete integrand must retain the fixed external momenta for either D(H) spelling"
2968 );
2969 }
2970 }
2971 }
2972
2973 #[test]
2974 fn nested_active_sunset_keeps_its_two_loop_incidence() {
2975 use crate::{
2976 dot,
2977 graph::{Graph, parse::IntoGraph},
2978 momentum::sample::LoopIndex,
2979 };
2980
2981 test_initialise().unwrap();
2982 let graph: Graph = dot!(digraph nested_active_sunset {
2983 edge [num=1 mass=1]
2984 node [num=1]
2985 ext [style=invis]
2986 ext -> a:0 [id=0]
2987 d:1 -> ext [id=1]
2988 a -> b [id=2]
2989 b -> c [id=3 lmb_id=0]
2990 b -> c [id=4]
2991 c -> d [id=5]
2992 a -> d [id=6 lmb_id=2]
2993 b -> c [id=7 lmb_id=1]
2994 })
2995 .unwrap();
2996 let full = graph
2997 .full_filter()
2998 .subtract(&graph.external_filter::<SuBitGraph>());
2999 let mut child = graph.empty_subgraph::<SuBitGraph>();
3000 for edge in [3, 4, 7] {
3001 child.add(graph[&EdgeIndex(edge)].1);
3002 }
3003 let quotient = full.subtract(&child);
3004 let child_lmb = graph
3005 .try_compatible_sub_lmb(
3006 &child,
3007 graph.dummy_less_full_crown(&child),
3008 &graph.loop_momentum_basis,
3009 )
3010 .unwrap();
3011 let mut quotient_lmb = graph.loop_momentum_basis.clone();
3012 for index in (0..quotient_lmb.loop_edges.len()).rev() {
3013 let index = LoopIndex(index);
3014 if child_lmb
3015 .loop_edges
3016 .contains("ient_lmb.loop_edges[index])
3017 {
3018 quotient_lmb.put_loop_to_ext(index);
3019 }
3020 }
3021 let [k, l, p] = [3, 7, 6].map(|edge| function!(GS.emr_mom, edge));
3022 let mass_squared = Atom::var(GS.m_uv_vacuum).pow(2);
3023 let child_denominators = function!(GS.den, 3, &k, &mass_squared)
3024 * function!(GS.den, 7, &l, &mass_squared)
3025 * function!(GS.den, 4, -&k - &l, &mass_squared).pow(3);
3026 let quotient_denominators = function!(GS.den, 2, -&p, &mass_squared)
3027 * function!(GS.den, 5, -&p, &mass_squared)
3028 * function!(GS.den, 6, &p, &mass_squared);
3029 let numerator = parse!("(a+b)*(c+d)");
3030 for child_active in [true, false] {
3031 let mut components = Vec::new();
3032 let input = if child_active {
3033 components.push((child.clone(), child.clone(), child_lmb.clone()));
3034 &numerator / (&child_denominators * "ient_denominators)
3035 } else {
3036 &numerator / "ient_denominators
3037 };
3038 components.push((quotient.clone(), full.clone(), quotient_lmb.clone()));
3039 let converted = to_vakint_integrand(
3040 &input,
3041 &graph,
3042 &full,
3043 &child,
3044 &components,
3045 &VakintSettings::default(),
3046 false,
3047 )
3048 .unwrap();
3049 assert_eq!(converted.0.len(), 1);
3050 let term = &converted.0[0];
3051 assert_eq!(term.numerator, numerator);
3052 let propagator = function!(
3053 vakint::symbols::S.prop,
3054 W_.a_,
3055 W_.b_,
3056 W_.mom_,
3057 W_.mass_,
3058 W_.e_
3059 )
3060 .to_pattern();
3061 let mut powers = term
3062 .integral
3063 .pattern_match(&propagator, None, None)
3064 .map(|matched| {
3065 assert_eq!(matched[&W_.mass_], mass_squared);
3066 assert!(!matched[&W_.mom_].contains_symbol(vakint::symbols::S.p));
3067 i32::try_from(matched[&W_.e_].as_view()).unwrap()
3068 })
3069 .collect::<Vec<_>>();
3070 powers.sort();
3071 assert_eq!(
3072 powers,
3073 if child_active {
3074 vec![1, 1, 3, 3]
3075 } else {
3076 vec![3]
3077 }
3078 );
3079 }
3080 }
3081
3082 #[test]
3083 fn nested_vacuum_denominators_preserve_incidence_and_factorized_numerators() {
3084 use crate::{
3085 dot,
3086 graph::{Graph, parse::IntoGraph},
3087 };
3088
3089 test_initialise().unwrap();
3090 let graph: Graph = dot!(digraph nested_vacuum_bubble {
3091 edge [num=1 mass=1]
3092 node [num=1]
3093 incoming [style=invis]
3094 outgoing [style=invis]
3095 incoming -> a [id=0]
3096 b -> outgoing [id=1]
3097 a -> b [id=2 lmb_id=0]
3098 a -> b [id=3]
3099 })
3100 .unwrap();
3101 let denominators = [(2, 1), (3, -1)].map(|(edge, sign)| {
3102 function!(
3103 GS.den,
3104 edge,
3105 Atom::num(sign) * function!(GS.emr_mom, 2),
3106 Atom::var(GS.m_uv_vacuum).pow(2)
3107 )
3108 });
3109 let numerator = parse!("(a+b)*(c+d)");
3110 let [first, second] = [parse!("s"), parse!("t")];
3111 for base_power in [1, 127] {
3114 let input = &numerator / (&denominators[0] * denominators[1].pow(base_power))
3115 * (&first / &denominators[1] + &second / denominators[1].pow(2));
3116 let actual = to_vakint_integrand(
3117 &input,
3118 &graph,
3119 &graph.full_filter(),
3120 &graph.empty_subgraph::<SuBitGraph>(),
3121 &[(
3122 graph.full_filter(),
3123 graph.full_filter(),
3124 graph.loop_momentum_basis.clone(),
3125 )],
3126 &VakintSettings {
3127 additional_normalization: "1".to_string(),
3129 ..VakintSettings::default()
3130 },
3131 true,
3132 )
3133 .unwrap();
3134 let actual = Atom::from(actual)
3135 .replace(function!(vakint::symbols::S.topo, W_.x_))
3136 .with(W_.x_)
3137 .replace(function!(
3138 vakint::symbols::S.prop,
3139 W_.a_,
3140 W_.b_,
3141 W_.c_,
3142 W_.d_,
3143 W_.e_
3144 ))
3145 .with(
3146 (function!(vakint::symbols::S.dot, W_.c_, W_.c_) - Atom::var(W_.d_))
3147 .pow(-Atom::var(W_.e_)),
3148 );
3149 let radial = function!(
3150 vakint::symbols::S.dot,
3151 function!(vakint::symbols::S.k, 0),
3152 function!(vakint::symbols::S.k, 0)
3153 ) - Atom::var(GS.m_uv_vacuum).pow(2);
3154 let expected = &numerator
3155 * (&first / radial.pow(base_power + 2) + &second / radial.pow(base_power + 3));
3156 assert_eq!(actual.collect_factors(), expected.collect_factors());
3157 }
3158 }
3159}