1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fmt::Display,
4 hash::{Hash, Hasher},
5 sync::Arc,
6 time::{Duration, Instant},
7};
8
9use bincode_trait_derive::{Decode, Encode};
10use linnet::half_edge::{
11 involution::{EdgeIndex, Hedge},
12 subgraph::{SubGraphLike, SubSetLike, SubSetOps},
13};
14use serde::{Deserialize, Serialize};
15use symbolica::{
16 atom::{Atom, AtomCore, FunctionBuilder},
17 symbol,
18};
19
20use crate::{
21 cff::{
22 expression::{
23 GammaLoopOrientationExpression, OrientationExpression, OrientationID,
24 OrientationSelector, ThreeDExpression,
25 normalize_cut_edge_support_with_raised_edge_groups,
26 normalize_three_d_expression_cut_support_with_raised_edge_groups,
27 },
28 generation::{ExactCffPreparationKey, PreparationKey, PreparationValue},
29 orientations::GraphOrientation,
30 surface::{GammaLoopLinearEnergyExpr, GammaLoopSurfaceCache, LinearEnergyExpr},
31 },
32 graph::{
33 ExactUvSubLmbFrame, FeynmanGraph, FourDDenominator, Graph, GraphThreeDSource,
34 LoopMomentumBasis,
35 cuts::CutSet,
36 get_cff_inverse_energy_product_impl,
37 three_d_source::{ExactSourceEnergyMapper, ExactSourceMappingContext},
38 },
39 numerator::energy_degree::{EnergyPowerAssignmentPlan, PlannedEnergyExpression},
40 settings::global::OrientationPattern,
41 utils::GS,
42 uv::{
43 Integrands,
44 approx::{local_4d::CanonicalUvDenominatorClass, projected_4d::Local4dProjectionContext},
45 },
46};
47use color_eyre::Result;
48use three_dimensional_reps::{
49 CffEnergyFactorOwnership, CffGlobalPrefactorSign, Generate3DExpressionOptions,
50 GeneratedThreeDExpression, thermal::VACUUM_SUBTRACTION,
51};
52
53pub mod orientations;
54pub mod esurface;
56pub mod expression;
57pub mod generation;
58pub mod hsurface;
59pub mod surface;
60pub mod tree;
61mod vertex_set;
62pub(crate) use vertex_set::VertexSet;
63
64pub(crate) struct CFFOrientationTerm {
65 pub(crate) expression: Atom,
66 pub(crate) orientation: OrientationExpression,
67 pub(crate) production_orientation_id: Option<OrientationID>,
68}
69
70pub struct CFFTerm {
71 pub(crate) orientations: Vec<CFFOrientationTerm>,
74 exact_source_numerator: Option<Arc<PlannedExactSourceNumerator>>,
75}
76
77#[derive(PartialEq, Eq, Hash)]
78struct ExactNumeratorTemplateBinding {
79 context: ExactSourceMappingContext,
80 assignment: Arc<EnergyPowerAssignmentPlan>,
81}
82
83#[derive(Clone)]
86pub(crate) struct ExactNumeratorTemplateKey {
87 binding: Arc<ExactNumeratorTemplateBinding>,
88 hash: u64,
89}
90
91impl PartialEq for ExactNumeratorTemplateKey {
92 fn eq(&self, other: &Self) -> bool {
93 self.hash == other.hash && self.binding == other.binding
94 }
95}
96
97impl Eq for ExactNumeratorTemplateKey {}
98
99impl Hash for ExactNumeratorTemplateKey {
100 fn hash<H: Hasher>(&self, state: &mut H) {
101 self.hash.hash(state);
102 }
103}
104
105#[derive(Clone)]
108struct ExactNumeratorTemplateIdentity(Arc<()>);
109
110impl PartialEq for ExactNumeratorTemplateIdentity {
111 fn eq(&self, other: &Self) -> bool {
112 Arc::ptr_eq(&self.0, &other.0)
113 }
114}
115
116impl Eq for ExactNumeratorTemplateIdentity {}
117
118impl Hash for ExactNumeratorTemplateIdentity {
119 fn hash<H: Hasher>(&self, state: &mut H) {
120 Arc::as_ptr(&self.0).hash(state);
121 }
122}
123
124#[derive(Clone, PartialEq, Eq, Hash)]
125pub(crate) struct ExactNumeratorRowKey {
126 template: ExactNumeratorTemplateIdentity,
127 node: usize,
128 samples: Vec<Atom>,
129}
130
131pub(crate) struct PlannedExactSourceNumerator {
132 mapper: Arc<ExactSourceEnergyMapper>,
133 binding: ExactNumeratorTemplateKey,
134 identity: ExactNumeratorTemplateIdentity,
135 template: PlannedEnergyExpression,
136 parameters: Vec<Atom>,
137 dependencies: Vec<Vec<usize>>,
138 node_ends: Vec<usize>,
139 constants: Vec<Option<Atom>>,
140}
141
142impl PlannedExactSourceNumerator {
143 fn prepare_parametric(
148 &self,
149 orientations: &[CFFOrientationTerm],
150 scope: &Atom,
151 ) -> Result<(Atom, Vec<Atom>, Vec<Vec<Atom>>)> {
152 let coefficient = symbol!("gammalooprs::uv::numerator_coefficient"; Scalar);
153 let mut captured = false;
154 let _ = self.template.map(&mut |_, factor, _| {
155 let _ = factor.replace_map(|view, _, _| {
156 if let symbolica::atom::AtomView::Fun(function) = view
157 && function.get_symbol() == coefficient
158 && function.get_nargs() == 2
159 && function.get(0) == scope.as_view()
160 {
161 captured = true;
162 }
163 });
164 Ok::<_, std::convert::Infallible>(Atom::one())
165 });
166 if captured {
167 return Err(eyre::eyre!(
168 "exact numerator coefficient scope {scope} is already in use"
169 ));
170 }
171 let required = &self.dependencies[0];
172 let Some(first) = orientations.first() else {
173 return Err(eyre::eyre!("exact numerator family has no residue rows"));
174 };
175 let loop_count = first.orientation.loop_energy_map.len();
176 let edge_count = first.orientation.edge_energy_map.len();
177 let mut support = BTreeMap::new();
178 for (row, orientation) in orientations.iter().enumerate() {
179 let orientation = &orientation.orientation;
180 if orientation.loop_energy_map.len() != loop_count
181 || orientation.edge_energy_map.len() != edge_count
182 {
183 return Err(eyre::eyre!(
184 "exact numerator family has inconsistent affine map lengths"
185 ));
186 }
187 self.mapper.sample_energies(
190 &orientation.loop_energy_map,
191 &orientation.edge_energy_map,
192 required,
193 )?;
194 for &slot in required {
195 let energy = if slot < loop_count {
196 &orientation.loop_energy_map[slot]
197 } else {
198 &orientation.edge_energy_map[slot - loop_count]
199 }
200 .clone()
201 .canonical();
202 let coefficients = energy
203 .internal_terms
204 .iter()
205 .map(|(edge, value)| (0, usize::from(*edge), value))
206 .chain(
207 energy
208 .external_terms
209 .iter()
210 .map(|(edge, value)| (1, usize::from(*edge), value)),
211 )
212 .chain([
213 (2, 0, &energy.uniform_scale_coeff),
214 (3, 0, &energy.constant),
215 ]);
216 for (kind, edge, value) in coefficients {
217 let value = Atom::num(value.clone());
218 if !value.is_zero() {
219 support
220 .entry((slot, kind, edge))
221 .or_insert_with(|| vec![Atom::Zero; orientations.len()])[row] = value;
222 }
223 }
224 }
225 }
226 let mut parameters = Vec::with_capacity(support.len());
227 let mut rows = vec![Vec::with_capacity(support.len()); orientations.len()];
228 let mut samples = required
229 .iter()
230 .copied()
231 .map(|slot| (slot, Atom::Zero))
232 .collect::<BTreeMap<_, _>>();
233 for ((slot, kind, edge), values) in support {
234 let mut unit = LinearEnergyExpr::zero();
235 match kind {
236 0 => unit.internal_terms.push((EdgeIndex(edge), 1.into())),
237 1 => unit.external_terms.push((EdgeIndex(edge), 1.into())),
238 2 => unit.uniform_scale_coeff = 1.into(),
239 3 => unit.constant = 1.into(),
240 _ => unreachable!(),
241 }
242 let basis = unit
245 .to_atom_gs(&[])
246 .replace_multiple(self.mapper.exact_ose_replacements());
247 let parameter = coefficient.call_args([scope.clone(), Atom::num(parameters.len())]);
250 *samples.get_mut(&slot).unwrap() += ¶meter * basis;
251 parameters.push(parameter);
252 for (arguments, value) in rows.iter_mut().zip(values) {
253 arguments.push(value);
254 }
255 }
256 let mapped = self.map_template(&self.template, &samples, &mut 0, &mut None);
257 Ok((
258 self.mapper.set_inactive_loop_energies_to_zero(mapped),
259 parameters,
260 rows,
261 ))
262 }
263
264 fn prepare(
267 mapper: Arc<ExactSourceEnergyMapper>,
268 assignment: Arc<EnergyPowerAssignmentPlan>,
269 context: Option<&mut Local4dProjectionContext>,
270 ) -> Result<(Arc<Self>, Duration, Duration)> {
271 let started = Instant::now();
272 let binding = ExactNumeratorTemplateBinding {
273 context: mapper.mapping_context()?,
274 assignment,
275 };
276 let mut hash = std::hash::DefaultHasher::new();
277 binding.hash(&mut hash);
278 let key = ExactNumeratorTemplateKey {
279 binding: Arc::new(binding),
280 hash: hash.finish(),
281 };
282 let cache_key = PreparationKey::Numerator(key.clone());
283 let mut context = context;
284 if let Some(context) = context.as_deref_mut()
285 && let Some(PreparationValue::Numerator(template)) =
286 context.preparations.get(&cache_key)
287 {
288 let template = Arc::clone(template);
289 drop(cache_key);
290 drop(key);
291 let cache_time = started.elapsed();
292 context.preparations.cache_time += cache_time;
293 crate::debug_tags!(#generation, #uv, #local, #four_d, #profile;
294 stage = "numerator_template",
295 cache_hit = true,
296 elapsed_ms = cache_time.as_secs_f64() * 1000.0,
297 cache_work_ms = cache_time.as_secs_f64() * 1000.0,
298 template_build_ms = 0.0,
299 "Reused immutable exact-source numerator template"
300 );
301 return Ok((template, Duration::ZERO, cache_time));
302 }
303 let build_started = Instant::now();
304 let (template, parameters) = mapper.prepare_numerator(&key.binding.assignment)?;
305 let mut prepared = Self {
306 mapper,
307 binding: key.clone(),
308 identity: ExactNumeratorTemplateIdentity(Arc::new(())),
309 template,
310 parameters,
311 dependencies: Vec::new(),
312 node_ends: Vec::new(),
313 constants: Vec::new(),
314 };
315 Self::parameter_dependencies(
316 &prepared.template,
317 &prepared.parameters,
318 &mut prepared.dependencies,
319 &mut prepared.node_ends,
320 &mut prepared.constants,
321 );
322 let build_time = build_started.elapsed();
323 let bytes = prepared.accounted_bytes()?;
324 let prepared = Arc::new(prepared);
325 if let Some(context) = context.as_deref_mut() {
326 context.numerator_template_builds += 1;
327 context.preparations.insert(
328 cache_key,
329 PreparationValue::Numerator(Arc::clone(&prepared)),
330 bytes,
331 );
332 }
333 let elapsed = started.elapsed();
334 let cache_time = elapsed.saturating_sub(build_time);
335 if let Some(context) = context {
336 context.preparations.cache_time += cache_time;
337 }
338 crate::debug_tags!(#generation, #uv, #local, #four_d, #profile;
339 stage = "numerator_template",
340 cache_hit = false,
341 template_nodes = prepared.dependencies.len(),
342 parameters = prepared.parameters.len(),
343 accounted_bytes = bytes,
344 elapsed_ms = elapsed.as_secs_f64() * 1000.0,
345 cache_work_ms = cache_time.as_secs_f64() * 1000.0,
346 template_build_ms = build_time.as_secs_f64() * 1000.0,
347 "Prepared immutable exact-source numerator template"
348 );
349 Ok((prepared, build_time, cache_time))
350 }
351
352 fn parameter_dependencies(
353 expression: &PlannedEnergyExpression,
354 parameters: &[Atom],
355 nodes: &mut Vec<Vec<usize>>,
356 node_ends: &mut Vec<usize>,
357 constants: &mut Vec<Option<Atom>>,
358 ) -> usize {
359 let node = nodes.len();
360 nodes.push(Vec::new());
361 node_ends.push(0);
362 constants.push(None);
363 let (dependencies, constant) = match expression {
364 PlannedEnergyExpression::Factor { expression, .. } => {
365 let mut used = BTreeSet::new();
366 let _ = expression.replace_map(|view, _, _| {
367 if let Some(index) = parameters
368 .iter()
369 .position(|parameter| parameter.as_view() == view)
370 {
371 used.insert(index);
372 }
373 });
374 let constant = used.is_empty().then(|| expression.clone());
375 (used, constant)
376 }
377 PlannedEnergyExpression::Add(children)
378 | PlannedEnergyExpression::Mul(children)
379 | PlannedEnergyExpression::MultilinearFunction {
380 arguments: children,
381 ..
382 } => {
383 let children = children
384 .iter()
385 .map(|child| {
386 Self::parameter_dependencies(child, parameters, nodes, node_ends, constants)
387 })
388 .collect::<Vec<_>>();
389 let dependencies = children
390 .iter()
391 .flat_map(|child| nodes[*child].iter().copied())
392 .collect::<BTreeSet<_>>();
393 let constant = dependencies.is_empty().then(|| match expression {
394 PlannedEnergyExpression::Add(_) => {
395 children.iter().fold(Atom::Zero, |sum, child| {
396 sum + constants[*child].as_ref().unwrap()
397 })
398 }
399 PlannedEnergyExpression::Mul(_) => {
400 children.iter().fold(Atom::one(), |product, child| {
401 product * constants[*child].as_ref().unwrap()
402 })
403 }
404 PlannedEnergyExpression::MultilinearFunction { symbol, .. } => {
405 let mut builder = FunctionBuilder::new(*symbol);
406 for child in &children {
407 builder =
408 builder.add_arg(constants[*child].as_ref().unwrap().as_view());
409 }
410 builder.finish()
411 }
412 _ => unreachable!(),
413 });
414 (dependencies, constant)
415 }
416 PlannedEnergyExpression::Repeat { base, exponent } => {
417 let child =
418 Self::parameter_dependencies(base, parameters, nodes, node_ends, constants);
419 (
420 nodes[child].iter().copied().collect(),
421 constants[child]
422 .as_ref()
423 .map(|constant| constant.pow(*exponent as u64)),
424 )
425 }
426 };
427 nodes[node] = dependencies.iter().copied().collect();
428 node_ends[node] = nodes.len();
429 constants[node] = constant;
430 node
431 }
432
433 fn accounted_bytes(&self) -> Result<usize> {
434 Ok(std::mem::size_of::<Self>()
437 + 128
438 + 2 * std::mem::size_of::<PreparationKey>()
439 + std::mem::size_of::<PreparationValue>()
440 + self.binding.binding.context.accounted_bytes()
441 + self.mapper.accounted_bytes()?
442 + self.binding.binding.assignment.accounted_bytes()
443 + self.template.accounted_bytes()
444 + self.parameters.capacity() * std::mem::size_of::<Atom>()
445 + self
446 .parameters
447 .iter()
448 .map(|atom| atom.as_view().get_byte_size())
449 .sum::<usize>()
450 + self.dependencies.capacity() * std::mem::size_of::<Vec<usize>>()
451 + self
452 .dependencies
453 .iter()
454 .map(|indices| indices.capacity() * std::mem::size_of::<usize>())
455 .sum::<usize>()
456 + self.node_ends.capacity() * std::mem::size_of::<usize>()
457 + self.constants.capacity() * std::mem::size_of::<Option<Atom>>()
458 + self
459 .constants
460 .iter()
461 .flatten()
462 .map(|atom| atom.as_view().get_byte_size())
463 .sum::<usize>())
464 }
465
466 #[cfg(test)]
467 fn sample(
468 &self,
469 orientation: &OrientationExpression,
470 context: Option<&mut Local4dProjectionContext>,
471 ) -> Result<Atom> {
472 let started = Instant::now();
473 let required = &self.dependencies[0];
474 let values = self.mapper.sample_energies(
475 &orientation.loop_energy_map,
476 &orientation.edge_energy_map,
477 required,
478 )?;
479 let samples = required
480 .iter()
481 .copied()
482 .zip(values)
483 .collect::<BTreeMap<_, _>>();
484 let mut cache = context.map(|context| &mut context.numerator_rows);
485 let mapped = self.map_template(&self.template, &samples, &mut 0, &mut cache);
486 let mapped = self.mapper.set_inactive_loop_energies_to_zero(mapped);
487 crate::debug_tags!(#generation, #uv, #local, #four_d, #profile;
488 stage = "numerator_row_mapping",
489 required_energies = required.len(),
490 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
491 "Mapped one exact-source residue sample"
492 );
493 Ok(mapped)
494 }
495
496 fn map_template(
497 &self,
498 expression: &PlannedEnergyExpression,
499 samples: &BTreeMap<usize, Atom>,
500 next_node: &mut usize,
501 cache: &mut Option<&mut generation::GenerationCache<ExactNumeratorRowKey, Atom>>,
502 ) -> Atom {
503 let node = *next_node;
504 *next_node += 1;
505 if let Some(constant) = &self.constants[node] {
506 *next_node = self.node_ends[node];
507 return constant.clone();
508 }
509 let lookup_started = cache.as_ref().map(|_| Instant::now());
510 let key = cache.as_ref().map(|_| ExactNumeratorRowKey {
511 template: self.identity.clone(),
512 node,
513 samples: self.dependencies[node]
514 .iter()
515 .map(|index| samples[index].clone())
516 .collect(),
517 });
518 if let Some(cache) = cache.as_deref_mut() {
519 let value = cache.get(key.as_ref().unwrap()).cloned();
520 if let Some(value) = value {
521 *next_node = self.node_ends[node];
522 drop(key);
523 cache.cache_time += lookup_started.unwrap().elapsed();
524 return value;
525 }
526 cache.cache_time += lookup_started.unwrap().elapsed();
527 }
528 let mapped = match expression {
529 PlannedEnergyExpression::Factor { expression, .. } => {
530 let dependencies = &self.dependencies[node];
531 if dependencies.is_empty() {
532 return expression.clone();
533 }
534 expression.replace_map(|view, _, output| {
535 for index in dependencies {
536 if self.parameters[*index].as_view() == view {
537 **output = samples[index].clone();
538 return;
539 }
540 }
541 })
542 }
543 PlannedEnergyExpression::Add(children) => {
544 children.iter().fold(Atom::Zero, |sum, child| {
545 sum + self.map_template(child, samples, next_node, cache)
546 })
547 }
548 PlannedEnergyExpression::Mul(children) => {
549 children.iter().fold(Atom::one(), |product, child| {
550 product * self.map_template(child, samples, next_node, cache)
551 })
552 }
553 PlannedEnergyExpression::MultilinearFunction { symbol, arguments } => {
554 let mut builder = FunctionBuilder::new(*symbol);
555 for child in arguments {
556 builder = builder.add_arg(self.map_template(child, samples, next_node, cache));
557 }
558 builder.finish()
559 }
560 PlannedEnergyExpression::Repeat { base, exponent } => self
561 .map_template(base, samples, next_node, cache)
562 .pow(*exponent as u64),
563 };
564 if let Some(cache) = cache.as_deref_mut() {
565 let insertion_started = Instant::now();
566 let key = key.unwrap();
567 let bytes = 2
568 * (std::mem::size_of::<ExactNumeratorRowKey>()
569 + key.samples.capacity() * std::mem::size_of::<Atom>()
570 + key
571 .samples
572 .iter()
573 .map(|atom| atom.as_view().get_byte_size())
574 .sum::<usize>())
575 + std::mem::size_of::<Atom>()
576 + mapped.as_view().get_byte_size()
577 + 96;
578 if cache.can_retain(bytes) {
579 cache.insert(key, mapped.clone(), bytes);
580 } else {
581 drop(key);
582 }
583 cache.cache_time += insertion_started.elapsed();
584 }
585 mapped
586 }
587}
588
589impl CFFTerm {
590 pub(crate) fn prepare_exact_source_numerator(
591 &self,
592 scope: &Atom,
593 ) -> Result<(Atom, Vec<Atom>, Vec<Vec<Atom>>)> {
594 self.exact_source_numerator
595 .as_ref()
596 .ok_or_else(|| eyre::eyre!("ordinary CFF term has no exact-source numerator plan"))?
597 .prepare_parametric(&self.orientations, scope)
598 }
599
600 #[cfg(test)]
601 pub(crate) fn map_exact_source_atom(
602 &self,
603 orientation: &OrientationExpression,
604 atom: &Atom,
605 ) -> Result<Atom> {
606 let planned = self
607 .exact_source_numerator
608 .as_ref()
609 .ok_or_else(|| eyre::eyre!("ordinary CFF term has no exact-source numerator map"))?;
610 planned.mapper.map_numerator(
611 &orientation.loop_energy_map,
612 &orientation.edge_energy_map,
613 atom,
614 )
615 }
616
617 #[cfg(test)]
618 pub(crate) fn map_exact_source_physical_loop_lift_energies(
619 &self,
620 orientation: &OrientationExpression,
621 physical_edges: impl IntoIterator<Item = EdgeIndex>,
622 ) -> Result<Vec<(EdgeIndex, Atom)>> {
623 let planned = self
624 .exact_source_numerator
625 .as_ref()
626 .ok_or_else(|| eyre::eyre!("ordinary CFF term has no exact-source numerator map"))?;
627 planned.mapper.map_physical_owner_loop_lift_energies(
628 &orientation.loop_energy_map,
629 &orientation.edge_energy_map,
630 physical_edges,
631 )
632 }
633
634 #[cfg(test)]
635 pub(crate) fn map_exact_source_numerator(
636 &self,
637 orientation: &OrientationExpression,
638 context: Option<&mut Local4dProjectionContext>,
639 ) -> Result<Atom> {
640 let planned = self
641 .exact_source_numerator
642 .as_ref()
643 .ok_or_else(|| eyre::eyre!("ordinary CFF term has no exact-source numerator plan"))?;
644 planned.sample(orientation, context)
645 }
646
647 pub fn expression_with_selectors(&self) -> Atom {
648 self.orientations
649 .iter()
650 .map(|term| {
651 let selector = term.production_orientation_id.map_or_else(
652 || term.orientation.data.orientation.orientation_thetas(),
653 OrientationID::atom,
654 );
655 term.expression.clone() * selector
656 })
657 .reduce(|left, right| left + right)
658 .unwrap_or(Atom::Zero)
659 }
660}
661
662#[derive(
663 Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Encode, Decode, Serialize, Deserialize,
664)]
665pub struct CutCFFIndex {
667 pub left_threshold_order: Option<usize>,
668 pub right_threshold_order: Option<usize>,
669 pub lu_cut_order: Option<usize>,
670}
671
672impl CutCFFIndex {
673 pub fn new_all_none() -> Self {
674 Self {
675 left_threshold_order: None,
676 right_threshold_order: None,
677 lu_cut_order: None,
678 }
679 }
680}
681
682impl Display for CutCFFIndex {
683 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
684 let mut parts = vec![];
685 if let Some(order) = self.lu_cut_order {
686 parts.push(format!("lu_cut_{}", order));
687 }
688
689 if let Some(order) = self.left_threshold_order {
690 parts.push(format!("left_th_{}", order));
691 }
692
693 if let Some(order) = self.right_threshold_order {
694 parts.push(format!("right_th_{}", order));
695 }
696
697 if parts.is_empty() {
698 write!(f, "")
699 } else {
700 write!(f, "{}", parts.join("_"))
701 }
702 }
703}
704
705#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
707pub enum CffEnergyBoundSourceKind {
708 PhysicalGraph,
710 ExactFourD,
712}
713
714#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
722pub struct CffEnergyDegreeBoundReport {
723 pub source_kind: CffEnergyBoundSourceKind,
724 pub physical_parent_bounds: Vec<(usize, usize)>,
725 pub assigned_cff_source_bounds: Vec<(usize, usize)>,
726}
727
728pub struct CutCFF {
729 pub terms: BTreeMap<CutCFFIndex, CFFTerm>,
730 pub(crate) energy_degree_bound_report: CffEnergyDegreeBoundReport,
731 production_prefactor_bridge: CffGlobalPrefactorSign,
735}
736
737impl CutCFF {
738 pub(crate) fn with_vacuum_subtraction(mut self, enabled: bool) -> Self {
741 if enabled {
742 for term in self.terms.values_mut() {
743 for orientation in &mut term.orientations {
744 orientation.expression = orientation
745 .expression
746 .replace_multiple(&*VACUUM_SUBTRACTION);
747 }
748 }
749 }
750 self
751 }
752
753 fn gamma_loop_prefactor_conversion<E, H>(
773 generated: &GeneratedThreeDExpression<E, H>,
774 ) -> CffGlobalPrefactorSign {
775 let retained_positive_energy_factors =
776 generated.energy_factor_ownership == CffEnergyFactorOwnership::VariantLocal;
777 generated.energy_factor_components.iter().fold(
778 CffGlobalPrefactorSign::default(),
779 |conversion, component| {
780 conversion
781 .product(CffGlobalPrefactorSign::from_exponent(
782 component.internal_edge_ids.len()
783 * usize::from(retained_positive_energy_factors),
784 ))
785 .product(component.source_frame)
786 },
787 )
788 }
789
790 pub(crate) const fn production_prefactor_factor(&self) -> i64 {
791 self.production_prefactor_bridge.factor()
792 }
793
794 pub fn expression_with_selectors(&self) -> Integrands {
795 let production_prefactor = Atom::num(self.production_prefactor_factor());
796 self.terms
797 .iter()
798 .map(|(index, term)| {
799 (
800 *index,
801 term.expression_with_selectors() * &production_prefactor,
802 )
803 })
804 .collect()
805 }
806}
807
808#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
809enum CutCffResidueAxis {
810 RightThreshold,
811 LeftThreshold,
812 LuCut,
813}
814
815impl CutCffResidueAxis {
816 fn set_order(self, index: &mut CutCFFIndex, order: usize) {
817 match self {
818 Self::RightThreshold => index.right_threshold_order = Some(order),
819 Self::LeftThreshold => index.left_threshold_order = Some(order),
820 Self::LuCut => index.lu_cut_order = Some(order),
821 }
822 }
823}
824
825fn apply_indexed_residue_selection<F>(
826 residues: Vec<(CutCFFIndex, ThreeDExpression<OrientationID>)>,
827 axis: CutCffResidueAxis,
828 mut select: F,
829) -> Vec<(CutCFFIndex, ThreeDExpression<OrientationID>)>
830where
831 F: FnMut(ThreeDExpression<OrientationID>) -> Vec<ThreeDExpression<OrientationID>>,
832{
833 residues
834 .into_iter()
835 .flat_map(|(index, expression)| {
836 select(expression)
837 .into_iter()
838 .enumerate()
839 .map(move |(i, residue)| {
840 let mut new_index = index;
841 axis.set_order(&mut new_index, i + 1);
842 (new_index, residue)
847 })
848 })
849 .collect()
850}
851
852fn select_indexed_cff_residues(
853 cff: ThreeDExpression<OrientationID>,
854 cutset: &CutSet,
855) -> Result<Vec<(CutCFFIndex, ThreeDExpression<OrientationID>)>> {
856 if let Some(lu) = cutset.residue_selector.lu.as_ref()
857 && (lu.cut_edge_alternatives.is_empty()
858 || lu.cut_edge_alternatives.iter().any(Vec::is_empty))
859 {
860 return Err(eyre::eyre!(
861 "LU residue selection requires at least one non-empty physical cut alternative"
862 ));
863 }
864 for group in cutset
865 .residue_selector
866 .right_th_cut
867 .iter()
868 .chain(&cutset.residue_selector.left_th_cut)
869 .chain(cutset.residue_selector.lu.iter().map(|lu| &lu.raised_group))
870 {
871 if group.esurface_ids.is_empty() || group.max_occurence == 0 {
872 return Err(eyre::eyre!(
873 "CFF residue selection requires a nonempty surface group and a positive residue order"
874 ));
875 }
876 if let Some(id) = group
877 .esurface_ids
878 .iter()
879 .find(|id| cff.surfaces.esurface_cache.get(**id).is_none())
880 {
881 return Err(eyre::eyre!(
882 "CFF residue selection references missing E-surface {id:?}"
883 ));
884 }
885 }
886 let mut residues = vec![(CutCFFIndex::new_all_none(), cff)];
891
892 if let Some(right_threshold) = cutset.residue_selector.right_th_cut.as_ref() {
893 residues = apply_indexed_residue_selection(
894 residues,
895 CutCffResidueAxis::RightThreshold,
896 |expression| expression.select_esurface_residue(right_threshold),
897 );
898 }
899
900 if let Some(left_threshold) = cutset.residue_selector.left_th_cut.as_ref() {
901 residues = apply_indexed_residue_selection(
902 residues,
903 CutCffResidueAxis::LeftThreshold,
904 |expression| expression.select_esurface_residue(left_threshold),
908 );
909 }
910
911 if let Some(lu) = cutset.residue_selector.lu.as_ref() {
912 residues =
913 apply_indexed_residue_selection(residues, CutCffResidueAxis::LuCut, |expression| {
914 expression
919 .restrict_to_cut_alternatives(&lu.cut_edge_alternatives)
920 .select_esurface_residue(&lu.raised_group)
921 });
922 }
923
924 Ok(residues)
925}
926
927impl Graph {
928 pub(crate) fn cff_from_production_expression(
929 &self,
930 production: &GeneratedThreeDExpression<esurface::Esurface, hsurface::Hsurface>,
931 cutset: &CutSet,
932 orientation_pattern: &OrientationPattern,
933 ) -> Result<CutCFF> {
934 let production_prefactor_bridge = CutCFF::gamma_loop_prefactor_conversion(production);
935 let mut cff = production.expression.clone();
936 normalize_three_d_expression_cut_support_with_raised_edge_groups(
937 &mut cff,
938 &self.get_raised_edge_groups(),
939 );
940 let residues = select_indexed_cff_residues(cff, cutset)?;
941 let contract_subgraph = self.tree_edges.subtract(&self.initial_state_cut);
942 let contract_edges = self
943 .iter_edges_of(&contract_subgraph)
944 .filter_map(|(pair, edge_id, _)| pair.is_paired().then_some(edge_id))
945 .collect::<Vec<_>>();
946 let graph_without_is_cut = self
947 .underlying
948 .full_filter()
949 .subtract(&self.initial_state_cut.left)
950 .subtract(&self.initial_state_cut.right);
951 let cff_loop_number = self
952 .get_loop_number()
953 .saturating_sub(self.cyclotomatic_number(&contract_subgraph));
954 let cff_phase = Atom::i().pow(cff_loop_number as i64);
955 let cff_normalization = cff_phase / (Atom::var(GS.pi) * 2).pow(3 * cff_loop_number as i64);
956 let cff_energy_factor = match production.energy_factor_ownership {
957 CffEnergyFactorOwnership::GlobalSourceProduct => {
958 get_cff_inverse_energy_product_impl(self, &graph_without_is_cut, &contract_edges)
959 }
960 CffEnergyFactorOwnership::VariantLocal => Atom::num(1),
961 };
962
963 let mut terms = BTreeMap::new();
964 for (cut_cff_index, expr) in residues {
965 let replacement_rules = if cutset.canonicalize_external_shifts {
966 expr.surfaces
967 .get_all_replacements_gs_in_lmb(&[], &self.loop_momentum_basis)
968 } else {
969 expr.surfaces.get_all_replacements_gs(&[])
970 };
971 let mut cff_term = CFFTerm {
972 orientations: Vec::new(),
973 exact_source_numerator: None,
974 };
975 for (orientation_index, orientation) in expr.orientations.into_iter().enumerate() {
976 if !orientation_pattern.filter_orientation(&orientation.data.orientation) {
977 continue;
978 }
979 let expression = orientation
980 .to_atom_gs()
981 .replace_multiple(&replacement_rules)
982 * &cff_energy_factor
983 * &cff_normalization;
984 cff_term.orientations.push(CFFOrientationTerm {
985 expression,
986 orientation,
987 production_orientation_id: Some(OrientationID(orientation_index)),
988 });
989 }
990 terms.insert(cut_cff_index, cff_term);
991 }
992
993 Ok(CutCFF {
994 terms,
995 energy_degree_bound_report: CffEnergyDegreeBoundReport {
996 source_kind: CffEnergyBoundSourceKind::PhysicalGraph,
997 physical_parent_bounds: production.source_energy_degree_bounds.clone(),
998 assigned_cff_source_bounds: production.source_energy_degree_bounds.clone(),
999 },
1000 production_prefactor_bridge,
1001 })
1002 }
1003
1004 #[cfg(test)]
1005 pub(crate) fn cff_from_4d_denominators(
1006 &mut self,
1007 denominators: &[FourDDenominator],
1008 cutset: &CutSet,
1009 options: &Generate3DExpressionOptions,
1010 analysis_numerator: &Atom,
1011 ) -> Result<(CutCFF, linnet::half_edge::subgraph::SuBitGraph)> {
1012 self.cff_from_4d_denominators_in_uv_edges(
1013 denominators,
1014 [],
1015 cutset,
1016 options,
1017 analysis_numerator,
1018 None,
1019 )
1020 }
1021
1022 #[cfg(test)]
1023 pub(crate) fn cff_from_4d_denominators_in_uv_edges(
1024 &mut self,
1025 denominators: &[FourDDenominator],
1026 uv_edges: impl IntoIterator<Item = EdgeIndex>,
1027 cutset: &CutSet,
1028 options: &Generate3DExpressionOptions,
1029 analysis_numerator: &Atom,
1030 context: Option<&mut Local4dProjectionContext>,
1031 ) -> Result<(CutCFF, linnet::half_edge::subgraph::SuBitGraph)> {
1032 self.cff_from_4d_denominators_in_uv_edges_and_boundaries(
1033 denominators,
1034 uv_edges,
1035 [],
1036 cutset,
1037 options,
1038 analysis_numerator,
1039 context,
1040 )
1041 }
1042
1043 #[cfg(test)]
1046 #[allow(clippy::too_many_arguments)]
1047 pub(crate) fn cff_from_4d_denominators_in_uv_edges_and_boundaries(
1048 &mut self,
1049 denominators: &[FourDDenominator],
1050 uv_edges: impl IntoIterator<Item = EdgeIndex>,
1051 uv_boundary_hedges: impl IntoIterator<Item = Hedge>,
1052 cutset: &CutSet,
1053 options: &Generate3DExpressionOptions,
1054 analysis_numerator: &Atom,
1055 context: Option<&mut Local4dProjectionContext>,
1056 ) -> Result<(CutCFF, linnet::half_edge::subgraph::SuBitGraph)> {
1057 self.cff_from_4d_denominators_in_uv_coordinates(
1058 denominators,
1059 uv_edges,
1060 uv_boundary_hedges,
1061 None,
1062 cutset,
1063 options,
1064 analysis_numerator,
1065 context,
1066 &[],
1067 )
1068 }
1069
1070 #[allow(clippy::too_many_arguments)]
1074 pub(crate) fn cff_from_4d_denominators_in_uv_sub_lmb(
1075 &mut self,
1076 denominators: &[FourDDenominator],
1077 uv_edges: impl IntoIterator<Item = EdgeIndex>,
1078 uv_boundary_hedges: impl IntoIterator<Item = Hedge>,
1079 sub_lmb: &LoopMomentumBasis,
1080 frame: ExactUvSubLmbFrame,
1081 cutset: &CutSet,
1082 options: &Generate3DExpressionOptions,
1083 analysis_numerator: &Atom,
1084 context: Option<&mut Local4dProjectionContext>,
1085 classes: &[CanonicalUvDenominatorClass],
1086 ) -> Result<(CutCFF, linnet::half_edge::subgraph::SuBitGraph)> {
1087 self.cff_from_4d_denominators_in_uv_coordinates(
1088 denominators,
1089 uv_edges,
1090 uv_boundary_hedges,
1091 Some((sub_lmb, frame)),
1092 cutset,
1093 options,
1094 analysis_numerator,
1095 context,
1096 classes,
1097 )
1098 }
1099
1100 #[allow(clippy::too_many_arguments)]
1101 fn cff_from_4d_denominators_in_uv_coordinates(
1102 &mut self,
1103 denominators: &[FourDDenominator],
1104 uv_edges: impl IntoIterator<Item = EdgeIndex>,
1105 uv_boundary_hedges: impl IntoIterator<Item = Hedge>,
1106 coordinates: Option<(&LoopMomentumBasis, ExactUvSubLmbFrame)>,
1107 cutset: &CutSet,
1108 options: &Generate3DExpressionOptions,
1109 analysis_numerator: &Atom,
1110 context: Option<&mut Local4dProjectionContext>,
1111 classes: &[CanonicalUvDenominatorClass],
1112 ) -> Result<(CutCFF, linnet::half_edge::subgraph::SuBitGraph)> {
1113 let (
1114 generated,
1115 physical_surfaces,
1116 physical_energy_edges,
1117 exact_source_numerator,
1118 inverse_energy_product,
1119 cff_loop_number,
1120 contract_subgraph,
1121 energy_degree_bound_report,
1122 physical_cut_support_edges,
1123 production_prefactor_bridge,
1124 ) = {
1125 let mut context = context;
1126 let source_started = Instant::now();
1127 let key = Arc::new(ExactCffPreparationKey {
1128 denominators: denominators.to_vec(),
1129 uv_edges: uv_edges
1130 .into_iter()
1131 .collect::<BTreeSet<_>>()
1132 .into_iter()
1133 .collect(),
1134 boundary_hedges: uv_boundary_hedges
1135 .into_iter()
1136 .collect::<BTreeSet<_>>()
1137 .into_iter()
1138 .collect(),
1139 coordinates: coordinates.map(|(lmb, frame)| (lmb.clone(), frame)),
1140 classes: classes.to_vec(),
1141 numerator: analysis_numerator.clone(),
1142 options: options.clone(),
1143 });
1144 let cache_key = PreparationKey::Source(Arc::clone(&key));
1145 let cached = context
1146 .as_deref_mut()
1147 .and_then(|context| context.preparations.get(&cache_key))
1148 .map(|entry| match entry {
1149 PreparationValue::Source(preparation) => Arc::clone(preparation),
1150 _ => unreachable!("preparation key and value kinds agree"),
1151 });
1152 let cache_hit = cached.is_some();
1153 let mut source_build_time = Duration::ZERO;
1154 let mut analysis_preparation_time = Duration::ZERO;
1155 let preparation = if let Some(preparation) = cached {
1156 drop(cache_key);
1157 drop(key);
1158 preparation
1159 } else {
1160 let reconstruction_started = Instant::now();
1161 let source = if let Some((sub_lmb, frame)) = &key.coordinates {
1162 GraphThreeDSource::from_exact_denominators_in_uv_sub_lmb(
1163 self,
1164 &key.denominators,
1165 key.uv_edges.iter().copied(),
1166 key.boundary_hedges.iter().copied(),
1167 sub_lmb,
1168 *frame,
1169 )?
1170 } else {
1171 GraphThreeDSource::from_exact_denominators_in_uv_edges_and_boundaries(
1172 self,
1173 &key.denominators,
1174 key.uv_edges.iter().copied(),
1175 key.boundary_hedges.iter().copied(),
1176 )?
1177 };
1178 source_build_time = reconstruction_started.elapsed();
1179 let preparation_started = Instant::now();
1180 let preparation = Arc::new(self.prepare_3d_expression_for_4d_term(
1181 &source,
1182 options,
1183 analysis_numerator,
1184 classes,
1185 )?);
1186 analysis_preparation_time = preparation_started.elapsed();
1187 if let Some(context) = context.as_deref_mut() {
1188 let bytes = key.accounted_bytes()
1189 + preparation.accounted_bytes()?
1190 + 2 * std::mem::size_of::<PreparationKey>()
1191 + std::mem::size_of::<PreparationValue>()
1192 + 128;
1193 context.source_preparation_builds += 1;
1194 context.preparations.insert(
1195 cache_key,
1196 PreparationValue::Source(Arc::clone(&preparation)),
1197 bytes,
1198 );
1199 }
1200 drop(source);
1201 drop(key);
1202 preparation
1203 };
1204 let request_time = source_started.elapsed();
1205 let cache_time =
1206 request_time.saturating_sub(source_build_time + analysis_preparation_time);
1207 if let Some(context) = context.as_deref_mut() {
1208 context.preparations.cache_time += cache_time;
1209 }
1210 crate::debug_tags!(#generation, #uv, #local, #four_d, #profile;
1211 stage = "source_reconstruction",
1212 graph = %self.name,
1213 denominator_occurrences = denominators.len(),
1214 classes = classes.len(),
1215 cache_hit,
1216 elapsed_ms = source_build_time.as_secs_f64() * 1000.0,
1217 analysis_preparation_ms = analysis_preparation_time.as_secs_f64() * 1000.0,
1218 request_elapsed_ms = request_time.as_secs_f64() * 1000.0,
1219 cache_work_ms = cache_time.as_secs_f64() * 1000.0,
1220 "Prepared certified exact denominator source incidence and numerator analysis"
1221 );
1222 let (generated, exact_source_numerator, _, energy_degree_bound_report) =
1223 self.generate_3d_expression_for_4d_term(&preparation, context)?;
1224 let physical_surfaces = generated
1225 .expression
1226 .surfaces
1227 .linear_surface_cache
1228 .iter()
1229 .map(|surface| {
1230 surface
1231 .expression
1232 .internal_terms
1233 .iter()
1234 .all(|(edge, _)| {
1235 preparation
1236 .physical_surface_edges
1237 .contains(&usize::from(*edge))
1238 })
1239 .then(|| {
1240 let mut surface = surface.clone();
1241 surface.expression = surface.expression.remap_energy_edges(
1242 &preparation.physical_energy_edges.internal,
1243 &BTreeMap::new(),
1244 );
1245 surface
1246 })
1247 })
1248 .collect::<Vec<_>>();
1249 let physical_energy_edges = preparation.physical_energy_edges.clone();
1250 let production_prefactor_bridge = CutCFF::gamma_loop_prefactor_conversion(&generated);
1251 (
1252 generated,
1253 physical_surfaces,
1254 physical_energy_edges,
1255 exact_source_numerator,
1256 preparation.inverse_energy_product.clone(),
1257 preparation.active_loop_count,
1258 preparation.contract_subgraph.clone(),
1259 energy_degree_bound_report,
1260 preparation.physical_cut_support_edges.clone(),
1261 production_prefactor_bridge,
1262 )
1263 };
1264 let (generated, surface_ownership) =
1265 self.convert_4d_expression_surfaces(generated, &physical_surfaces)?;
1266 let energy_factor_ownership = generated.energy_factor_ownership;
1267 crate::debug_tags!(#generation, #cff, #inspect;
1271 denominator_count = denominators.len(),
1272 production_prefactor_bridge = ?production_prefactor_bridge,
1273 aggregate_ownership = ?energy_factor_ownership,
1274 components = ?generated.energy_factor_components,
1275 physical_energy_edges = ?physical_energy_edges,
1276 surface_ownership = ?surface_ownership,
1277 "Exact CFF energy-factor component metadata: context={:?}, bridge={}, ownership={:?}, components={:?}, physical_energy_edges={:?}",
1278 options.cff_generation_context,
1279 production_prefactor_bridge.factor(),
1280 energy_factor_ownership,
1281 generated.energy_factor_components,
1282 physical_energy_edges,
1283 );
1284 let mut cff = generated.expression;
1285 let physical_edges = |edge: linnet::half_edge::involution::EdgeIndex| {
1290 let edge_id = usize::from(edge);
1291 if edge_id < physical_energy_edges.orientation_edge_count {
1292 return Ok(vec![edge]);
1293 }
1294 physical_cut_support_edges
1295 .get(&edge_id)
1296 .cloned()
1297 .ok_or_else(|| {
1298 eyre::eyre!("exact CFF cut support contains unmapped occurrence edge {edge_id}")
1299 })
1300 };
1301 let raised_edge_groups = self.get_raised_edge_groups();
1302 let retain_physical_support_with_raised_representatives = |support: &mut Vec<
1303 linnet::half_edge::involution::EdgeIndex,
1304 >| {
1305 let representatives =
1306 normalize_cut_edge_support_with_raised_edge_groups(support, &raised_edge_groups);
1307 support.extend(representatives);
1308 support.sort_unstable();
1309 support.dedup();
1310 };
1311 for orientation in cff.orientations.iter_mut() {
1312 for variant in &mut orientation.variants {
1313 let mut denominator_edges = variant
1314 .denominator_edges
1315 .iter()
1316 .copied()
1317 .map(&physical_edges)
1318 .collect::<Result<Vec<_>>>()?
1319 .into_iter()
1320 .flatten()
1321 .collect::<Vec<_>>();
1322 retain_physical_support_with_raised_representatives(&mut denominator_edges);
1323 variant.denominator_edges = denominator_edges;
1324 variant.denominator_edge_support_signs =
1325 std::mem::take(&mut variant.denominator_edge_support_signs)
1326 .into_iter()
1327 .try_fold(BTreeMap::new(), |mut mapped, (support, sign)| {
1328 let mut support = support
1329 .into_iter()
1330 .map(&physical_edges)
1331 .collect::<Result<Vec<_>>>()?
1332 .into_iter()
1333 .flatten()
1334 .collect::<Vec<_>>();
1335 retain_physical_support_with_raised_representatives(&mut support);
1336 *mapped.entry(support).or_insert(1) *= sign;
1337 Ok::<_, color_eyre::Report>(mapped)
1338 })?;
1339 }
1340 }
1341 let residues = select_indexed_cff_residues(cff, cutset)?;
1342 let cff_phase = Atom::i().pow(cff_loop_number as i64);
1343 let cff_normalization = cff_phase / (Atom::var(GS.pi) * 2).pow(3 * cff_loop_number as i64);
1344 let mut terms = BTreeMap::new();
1345 for (cut_cff_index, expr) in residues {
1346 let cff_energy_factor = match energy_factor_ownership {
1347 CffEnergyFactorOwnership::GlobalSourceProduct => inverse_energy_product.clone(),
1348 CffEnergyFactorOwnership::VariantLocal => Atom::num(1),
1349 };
1350 crate::debug_tags!(#generation, #cff, #inspect;
1351 cut_index = ?cut_cff_index,
1352 log.cff_energy_factor = cff_energy_factor,
1353 "Exact CFF component-local energy-factor bridge"
1354 );
1355 let replacement_rules = if cutset.canonicalize_external_shifts {
1356 expr.surfaces
1357 .get_all_replacements_gs_in_lmb(&[], &self.loop_momentum_basis)
1358 } else {
1359 expr.surfaces.get_all_replacements_gs(&[])
1360 };
1361 let mut cff_term = CFFTerm {
1362 orientations: Vec::new(),
1363 exact_source_numerator: Some(exact_source_numerator.clone()),
1364 };
1365 for orientation in expr.orientations {
1366 let expression = orientation
1367 .to_atom_gs()
1368 .replace_multiple(&replacement_rules)
1369 .replace_multiple(exact_source_numerator.mapper.exact_ose_replacements())
1370 * &cff_energy_factor
1371 * &cff_normalization;
1372 cff_term.orientations.push(CFFOrientationTerm {
1377 expression,
1378 orientation,
1379 production_orientation_id: None,
1380 });
1381 }
1382 terms.insert(cut_cff_index, cff_term);
1383 }
1384 Ok((
1385 CutCFF {
1386 terms,
1387 energy_degree_bound_report,
1388 production_prefactor_bridge,
1389 },
1390 contract_subgraph,
1391 ))
1392 }
1393
1394 pub fn cff<S: SubGraphLike + SubSetLike>(
1395 &mut self,
1396 contract_subgraph: &S,
1397 cutset: &CutSet,
1398 orientation_pattern: &OrientationPattern,
1399 options: &Generate3DExpressionOptions,
1400 analysis_numerator: Option<&Atom>,
1401 ) -> Result<CutCFF> {
1402 let mut contract_edges = vec![];
1403
1404 for (p, eid, _) in self.iter_edges_of(contract_subgraph) {
1405 if p.is_paired() {
1406 contract_edges.push(eid);
1407 }
1408 }
1409 contract_edges.sort_unstable();
1410 contract_edges.dedup();
1411
1412 let canonize_esurface = self.get_esurface_canonization(&self.loop_momentum_basis);
1413
1414 let generated = self.generate_3d_expression_for_integrand(
1419 &contract_edges,
1420 &canonize_esurface,
1421 options,
1422 analysis_numerator,
1423 )?;
1424 self.cff_from_generated_expression(
1425 generated,
1426 contract_subgraph,
1427 cutset,
1428 orientation_pattern,
1429 options,
1430 )
1431 }
1432
1433 pub(crate) fn cff_from_generated_expression<S: SubGraphLike + SubSetLike>(
1434 &self,
1435 generated: GeneratedThreeDExpression<esurface::Esurface, hsurface::Hsurface>,
1436 contract_subgraph: &S,
1437 cutset: &CutSet,
1438 orientation_pattern: &OrientationPattern,
1439 options: &Generate3DExpressionOptions,
1440 ) -> Result<CutCFF> {
1441 let mut contract_edges = self
1442 .iter_edges_of(contract_subgraph)
1443 .filter_map(|(pair, edge, _)| pair.is_paired().then_some(edge))
1444 .collect::<Vec<_>>();
1445 contract_edges.sort_unstable();
1446 contract_edges.dedup();
1447 let energy_factor_ownership = generated.energy_factor_ownership;
1448 let source_energy_degree_bounds = generated.source_energy_degree_bounds.clone();
1449 let production_prefactor_bridge = CutCFF::gamma_loop_prefactor_conversion(&generated);
1450 let mut cff = generated.expression;
1451 normalize_three_d_expression_cut_support_with_raised_edge_groups(
1452 &mut cff,
1453 &self.get_raised_edge_groups(),
1454 );
1455
1456 let residues = select_indexed_cff_residues(cff, cutset)?;
1457
1458 let graph_without_is_cut = self
1461 .underlying
1462 .full_filter()
1463 .subtract(&self.initial_state_cut.left)
1464 .subtract(&self.initial_state_cut.right);
1465 let cff_loop_number = self
1469 .get_loop_number()
1470 .saturating_sub(self.cyclotomatic_number(contract_subgraph));
1471 let cff_phase = Atom::i().pow(cff_loop_number as i64);
1472 let cff_normalization = cff_phase / (Atom::var(GS.pi) * 2).pow(3 * cff_loop_number as i64);
1473 let cff_energy_factor = match energy_factor_ownership {
1474 CffEnergyFactorOwnership::GlobalSourceProduct => {
1475 get_cff_inverse_energy_product_impl(self, &graph_without_is_cut, &contract_edges)
1476 }
1477 CffEnergyFactorOwnership::VariantLocal => Atom::num(1),
1478 };
1479 crate::debug_tags!(#cff, #trace;
1480 stage = "graph_cff_normalization",
1481 graph = %self.name,
1482 cff_loop_number = cff_loop_number,
1483 production_prefactor_bridge = ?production_prefactor_bridge,
1484 log.cff_normalization = cff_normalization,
1485 "Graph CFF normalization: graph={}, context={:?}, loops={}, bridge={}",
1486 self.name,
1487 options.cff_generation_context,
1488 cff_loop_number,
1489 production_prefactor_bridge.factor(),
1490 );
1491
1492 let mut terms = BTreeMap::new();
1493
1494 for (cut_cff_index, expr) in residues {
1495 let replacement_rules = if cutset.canonicalize_external_shifts {
1496 expr.surfaces
1497 .get_all_replacements_gs_in_lmb(&[], &self.loop_momentum_basis)
1498 } else {
1499 expr.surfaces.get_all_replacements_gs(&[])
1500 };
1501 let mut cff_term = CFFTerm {
1502 orientations: vec![],
1503 exact_source_numerator: None,
1504 };
1505 for orientation in expr.orientations.iter().filter(|orientation| {
1506 orientation_pattern.filter_orientation(&orientation.data.orientation)
1507 }) {
1508 let eta_expr = orientation.to_atom_gs();
1509 let mut ose_expr = eta_expr.replace_multiple(&replacement_rules);
1510 ose_expr *= &cff_energy_factor;
1511 ose_expr *= cff_normalization.clone();
1512
1513 crate::debug_tags!(#cff, #trace;
1514 stage = "graph_cff_term_expr",
1515 graph = %self.name,
1516 cut_index = ?cut_cff_index,
1517 log.expr = ose_expr,
1518 "Graph CFF term expression"
1519 );
1520 cff_term.orientations.push(CFFOrientationTerm {
1522 expression: ose_expr,
1523 orientation: orientation.clone(),
1524 production_orientation_id: None,
1525 });
1526 }
1527 terms.insert(cut_cff_index, cff_term);
1528 }
1529
1530 let cut_cff = CutCFF {
1531 terms,
1532 energy_degree_bound_report: CffEnergyDegreeBoundReport {
1533 source_kind: CffEnergyBoundSourceKind::PhysicalGraph,
1534 physical_parent_bounds: source_energy_degree_bounds.clone(),
1535 assigned_cff_source_bounds: source_energy_degree_bounds,
1536 },
1537 production_prefactor_bridge,
1538 };
1539 Ok(cut_cff)
1540 }
1541}
1542
1543#[cfg(test)]
1544mod tests {
1545 use std::{collections::BTreeSet, fmt::Write};
1546
1547 use super::*;
1548 use crate::{
1549 dot,
1550 graph::{LMBext, parse::IntoGraph},
1551 initialisation::test_initialise,
1552 integrands::{
1553 evaluation::EvaluationMetaData,
1554 process::{
1555 GenericEvaluatorFloat,
1556 evaluators::{EvaluatorStack, SingleOrAllOrientations},
1557 },
1558 },
1559 momentum::{
1560 ThreeMomentum,
1561 sample::{BareMomentumSample, ExternalFourMomenta, LoopMomenta, MomentumSample},
1562 },
1563 processes::EvaluatorSettings,
1564 settings::RuntimeSettings,
1565 utils::{ArbPrec, F, FloatLike, W_, cut_energy},
1566 uv::{
1567 UltravioletGraph,
1568 approx::{OrientationProjection, local_3d::Localizer, local_4d::Full4dCts},
1569 },
1570 };
1571 use itertools::Itertools;
1572 use linnet::half_edge::involution::{EdgeIndex, EdgeVec, Orientation};
1573 use linnet::half_edge::subgraph::{
1574 InternalSubGraph, ModifySubSet, SuBitGraph, SubSetLike, SubSetOps, subset::SubSet,
1575 };
1576 use spenso::algebra::{algebraic_traits::IsZero, complex::Complex};
1577 use symbolica::{
1578 atom::FunctionBuilder,
1579 domains::{
1580 float::{Complex as SymComplex, Real},
1581 integer::IntegerRing,
1582 rational::{Fraction, Rational},
1583 },
1584 evaluate::ExpressionEvaluator,
1585 function,
1586 id::Replacement,
1587 };
1588 use three_dimensional_reps::{
1589 OrientationData, OrientationID, ThreeDGraphSource, repeated_groups,
1590 };
1591 use typed_index_collections::TiVec;
1592
1593 #[test]
1594 fn exact_numerator_templates_and_subtrees_are_retention_invariant() -> Result<()> {
1595 test_initialise()?;
1596 let mut graph: Graph = dot!(digraph exact_template_retention {
1597 edge [num=1 mass=1]
1598 node [num=1]
1599 a -> b [id=0 lmb_id=0]
1600 a -> b [id=1]
1601 })?;
1602 let denominators = [EdgeIndex(0), EdgeIndex(1)].map(|source_edge| FourDDenominator {
1603 source_edge,
1604 momentum: FunctionBuilder::new(GS.emr_mom)
1605 .add_arg(usize::from(source_edge))
1606 .finish(),
1607 mass_squared: Atom::one(),
1608 full_expr: Atom::one(),
1609 });
1610 let q0 = GS.emr_mom(EdgeIndex(0), GS.cind(0));
1613 let q1 = GS.emr_mom(EdgeIndex(1), GS.cind(0));
1614 let numerator = (&q0 + Atom::num(3)) * (&q1 + Atom::num(2));
1615 let cutset = CutSet::empty(graph.n_hedges());
1616 let options = graph.denominator_only_cff_3d_expression_options();
1617 let mut context = Local4dProjectionContext::default();
1618 let (cff, _) = graph.cff_from_4d_denominators_in_uv_edges(
1619 &denominators,
1620 [],
1621 &cutset,
1622 &options,
1623 &numerator,
1624 Some(&mut context),
1625 )?;
1626 let term = cff.terms.values().next().unwrap();
1627 let prepared = term.exact_source_numerator.as_ref().unwrap();
1628 assert!(!term.orientations.is_empty());
1629 let direct = term
1630 .orientations
1631 .iter()
1632 .map(|orientation| {
1633 prepared
1634 .mapper
1635 .map_planned_numerator(
1636 &orientation.orientation.loop_energy_map,
1637 &orientation.orientation.edge_energy_map,
1638 &prepared.binding.binding.assignment,
1639 )
1640 .map(|atom| atom.collect_factors())
1641 })
1642 .collect::<Result<Vec<_>>>()?;
1643
1644 for phase in ["cold", "warm", "disabled", "evicted"] {
1645 if phase == "disabled" {
1646 context.preparations = generation::GenerationCache::new(0, 0);
1647 context.numerator_rows = generation::GenerationCache::new(0, 0);
1648 } else if phase == "evicted" {
1649 context.preparations = generation::GenerationCache::new(48 * 1024 * 1024, 1);
1650 context.numerator_rows = generation::GenerationCache::new(16 * 1024 * 1024, 1);
1651 }
1652 let (template, _, _) = PlannedExactSourceNumerator::prepare(
1653 prepared.mapper.clone(),
1654 prepared.binding.binding.assignment.clone(),
1655 Some(&mut context),
1656 )?;
1657 if phase == "evicted" {
1658 let other = graph.cff_from_4d_denominators_in_uv_edges(
1661 &denominators,
1662 [],
1663 &cutset,
1664 &options,
1665 &(&q0 + Atom::one()),
1666 None,
1667 )?;
1668 assert!(!other.0.terms.is_empty());
1669 let other = other
1670 .0
1671 .terms
1672 .values()
1673 .next()
1674 .unwrap()
1675 .exact_source_numerator
1676 .as_ref()
1677 .unwrap();
1678 PlannedExactSourceNumerator::prepare(
1679 Arc::clone(&other.mapper),
1680 Arc::clone(&other.binding.binding.assignment),
1681 Some(&mut context),
1682 )?;
1683 }
1684 for (orientation, expected) in term.orientations.iter().zip(&direct) {
1685 let mapped = template.sample(&orientation.orientation, Some(&mut context))?;
1686 assert_eq!(mapped.collect_factors(), *expected, "{phase}");
1687 }
1688 }
1689 Ok(())
1690 }
1691
1692 #[test]
1693 fn exact_source_parametric_family_retains_affine_sampling_rows() -> Result<()> {
1694 test_initialise()?;
1695 let mut graph: Graph = dot!(digraph exact_parametric_rows {
1696 edge [num=1 mass=1]
1697 node [num=1]
1698 a -> b [id=0 lmb_id=0]
1699 a -> b [id=1]
1700 })?;
1701 let denominators = [EdgeIndex(0), EdgeIndex(1)].map(|source_edge| FourDDenominator {
1702 source_edge,
1703 momentum: FunctionBuilder::new(GS.emr_mom)
1704 .add_arg(usize::from(source_edge))
1705 .finish(),
1706 mass_squared: Atom::one(),
1707 full_expr: Atom::one(),
1708 });
1709 let numerator = (GS.emr_mom(EdgeIndex(0), GS.cind(0)).pow(2) + Atom::one())
1710 * (GS.emr_mom(EdgeIndex(1), GS.cind(0)) + Atom::num(2)).pow(2);
1711 let cutset = CutSet::empty(graph.n_hedges());
1712 let options = graph.denominator_only_cff_3d_expression_options();
1713 let (cff, _) =
1714 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &numerator)?;
1715 let term = cff.terms.values().next().unwrap();
1716 let scope = crate::uv::approx::direct_3d::DirectResidueBranches::numerator_scope().1;
1717 let (body, parameters, rows) = term.prepare_exact_source_numerator(&scope)?;
1718 assert_eq!(rows.len(), term.orientations.len());
1719 assert!(!parameters.is_empty());
1720 for (orientation, arguments) in term.orientations.iter().zip(&rows) {
1721 assert!(
1722 arguments
1723 .iter()
1724 .all(|value| matches!(value.as_view(), symbolica::atom::AtomView::Num(_)))
1725 );
1726 let bound =
1727 body.replace_multiple(parameters.iter().zip(arguments).map(
1728 |(parameter, value)| Replacement::new(parameter.to_pattern(), value.clone()),
1729 ));
1730 assert_eq!(
1731 bound,
1732 term.map_exact_source_numerator(&orientation.orientation, None)?
1733 );
1734 }
1735
1736 let mut synthetic = CFFTerm {
1741 orientations: Vec::new(),
1742 exact_source_numerator: term.exact_source_numerator.clone(),
1743 };
1744 for row in 0..3 {
1745 let mut orientation = term.orientations[0].orientation.clone();
1746 for (slot, energy) in orientation
1747 .loop_energy_map
1748 .iter_mut()
1749 .chain(&mut orientation.edge_energy_map)
1750 .enumerate()
1751 {
1752 *energy = if row == 0 {
1753 LinearEnergyExpr::zero()
1754 } else {
1755 LinearEnergyExpr {
1756 internal_terms: vec![
1757 (EdgeIndex(0), (row as i64).into()),
1758 (EdgeIndex(1), (-2).into()),
1759 ],
1760 external_terms: vec![(EdgeIndex(17), (3, 2).into())],
1761 uniform_scale_coeff: ((slot + row) as i64).into(),
1762 constant: 7.into(),
1763 }
1764 };
1765 }
1766 synthetic.orientations.push(CFFOrientationTerm {
1767 expression: Atom::one(),
1768 orientation,
1769 production_orientation_id: Some(crate::cff::expression::OrientationID(0)),
1770 });
1771 }
1772 let scope = crate::uv::approx::direct_3d::DirectResidueBranches::numerator_scope().1;
1773 let (body, parameters, rows) = synthetic.prepare_exact_source_numerator(&scope)?;
1774 assert_eq!(
1775 rows.len(),
1776 3,
1777 "distinct full maps on one host must retain independent rows"
1778 );
1779 assert!(body.contains_symbol(GS.numerator_sampling_scale));
1780 assert!(rows[0].iter().all(Atom::is_zero));
1781 assert_ne!(rows[1], rows[2]);
1782 for (orientation, arguments) in synthetic.orientations.iter().zip(rows) {
1783 let bound = body.replace_multiple(
1784 parameters
1785 .iter()
1786 .zip(arguments)
1787 .map(|(parameter, value)| Replacement::new(parameter.to_pattern(), value)),
1788 );
1789 assert_eq!(
1790 bound,
1791 synthetic.map_exact_source_numerator(&orientation.orientation, None)?
1792 );
1793 }
1794 Ok(())
1795 }
1796
1797 #[test]
1798 fn owned_source_preparation_preserves_assignment_and_coefficient() -> Result<()> {
1799 test_initialise()?;
1800 let mut graph: Graph = dot!(digraph owned_source_retention {
1801 edge [num=1 mass=1]
1802 node [num=1]
1803 a -> b [id=0 lmb_id=0]
1804 a -> b [id=1]
1805 })?;
1806 let denominators = [EdgeIndex(0), EdgeIndex(1)].map(|source_edge| FourDDenominator {
1807 source_edge,
1808 momentum: FunctionBuilder::new(GS.emr_mom)
1809 .add_arg(usize::from(source_edge))
1810 .finish(),
1811 mass_squared: Atom::one(),
1812 full_expr: Atom::one(),
1813 });
1814 let energy = GS.emr_mom(EdgeIndex(0), GS.cind(0));
1815 let numerators = [energy.pow(2) + Atom::one(), energy + Atom::num(2)];
1816 let cutset = CutSet::empty(graph.n_hedges());
1817 let options = graph.denominator_only_cff_3d_expression_options();
1818 let mut context = Local4dProjectionContext::default();
1819 let mut expected = Vec::new();
1820 for phase in ["cold", "warm", "disabled", "evicted"] {
1821 if phase == "disabled" {
1822 context.preparations = generation::GenerationCache::new(0, 0);
1823 } else if phase == "evicted" {
1824 context.preparations = generation::GenerationCache::new(48 * 1024 * 1024, 1);
1825 }
1826 for (index, numerator) in numerators.iter().enumerate() {
1827 let (cff, _) = graph.cff_from_4d_denominators_in_uv_edges(
1828 &denominators,
1829 [],
1830 &cutset,
1831 &options,
1832 numerator,
1833 Some(&mut context),
1834 )?;
1835 let prefactor = Atom::num(cff.production_prefactor_factor());
1836 let mut coefficient = Atom::Zero;
1837 for term in cff.terms.values() {
1838 for orientation in &term.orientations {
1839 coefficient += &prefactor
1840 * &orientation.expression
1841 * term.map_exact_source_numerator(
1842 &orientation.orientation,
1843 Some(&mut context),
1844 )?;
1845 }
1846 }
1847 let observed = coefficient.collect_factors();
1848 if phase == "cold" {
1849 expected.push(observed);
1850 } else {
1851 assert_eq!(observed, expected[index], "{phase}, numerator {index}");
1852 }
1853 }
1854 }
1855 Ok(())
1856 }
1857
1858 #[test]
1859 fn cff_term_selects_duplicate_physical_orientations_by_production_map_key() {
1860 let orientation = || OrientationExpression {
1861 data: OrientationData::new(EdgeVec::from_iter([Orientation::Default])),
1862 loop_energy_map: Vec::new(),
1863 edge_energy_map: Vec::new(),
1864 variants: Vec::new(),
1865 };
1866 let keyed = CFFTerm {
1867 orientations: vec![
1868 CFFOrientationTerm {
1869 expression: Atom::num(2),
1870 orientation: orientation(),
1871 production_orientation_id: Some(OrientationID(0)),
1872 },
1873 CFFOrientationTerm {
1874 expression: Atom::num(3),
1875 orientation: orientation(),
1876 production_orientation_id: Some(OrientationID(1)),
1877 },
1878 ],
1879 exact_source_numerator: None,
1880 }
1881 .expression_with_selectors();
1882
1883 assert_eq!(OrientationID(0).select(&keyed).expand(), Atom::num(2));
1884 assert_eq!(OrientationID(1).select(&keyed).expand(), Atom::num(3));
1885 assert_eq!(
1886 keyed
1887 .replace(function!(OrientationID::symbol(), W_.a_))
1888 .with(Atom::one())
1889 .expand(),
1890 Atom::num(5),
1891 "explicit summation must retain every distinct map key even when their physical directions coincide",
1892 );
1893
1894 let source_local = CFFTerm {
1895 orientations: vec![CFFOrientationTerm {
1896 expression: Atom::num(7),
1897 orientation: orientation(),
1898 production_orientation_id: None,
1899 }],
1900 exact_source_numerator: None,
1901 }
1902 .expression_with_selectors();
1903 assert_eq!(
1904 source_local,
1905 Atom::num(7) * GS.sign_theta(GS.sign(EdgeIndex(0))),
1906 "a source-local term without a production map key must retain its physical selector",
1907 );
1908 }
1909
1910 #[test]
1911 fn lu_residue_selection_rejects_missing_physical_cut_support() {
1912 let mut cutset = CutSet::empty(0);
1913 cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
1914 raised_group: crate::cff::esurface::RaisedEsurfaceGroup {
1915 esurface_ids: Vec::new(),
1916 max_occurence: 1,
1917 },
1918 cut_edge_alternatives: Vec::new(),
1919 });
1920
1921 assert!(select_indexed_cff_residues(ThreeDExpression::new_empty(), &cutset).is_err());
1922
1923 let mut expression = ThreeDExpression::new_empty();
1924 expression.surfaces.esurface_cache.push(esurface::Esurface {
1925 energies: vec![EdgeIndex(0)],
1926 external_shift: Vec::new(),
1927 vertex_set: VertexSet::dummy(),
1928 });
1929 for (esurface_ids, max_occurence) in [
1930 (Vec::new(), 1),
1931 (vec![esurface::EsurfaceID(0)], 0),
1932 (vec![esurface::EsurfaceID(1)], 1),
1933 ] {
1934 let group = esurface::RaisedEsurfaceGroup {
1935 esurface_ids,
1936 max_occurence,
1937 };
1938 for axis in [
1939 CutCffResidueAxis::RightThreshold,
1940 CutCffResidueAxis::LeftThreshold,
1941 CutCffResidueAxis::LuCut,
1942 ] {
1943 let mut cutset = CutSet::empty(0);
1944 match axis {
1945 CutCffResidueAxis::RightThreshold => {
1946 cutset.residue_selector.right_th_cut = Some(group.clone())
1947 }
1948 CutCffResidueAxis::LeftThreshold => {
1949 cutset.residue_selector.left_th_cut = Some(group.clone())
1950 }
1951 CutCffResidueAxis::LuCut => {
1952 cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
1953 raised_group: group.clone(),
1954 cut_edge_alternatives: vec![vec![EdgeIndex(0)]],
1955 })
1956 }
1957 }
1958 assert!(
1959 select_indexed_cff_residues(expression.clone(), &cutset).is_err(),
1960 "malformed residue group must return an error for {axis:?}"
1961 );
1962 }
1963 }
1964 }
1965
1966 #[test]
1967 fn exact_child_sub_lmb_retains_parent_loop_crown_shift() -> Result<()> {
1968 test_initialise()?;
1969 let mut graph: Graph = dot!(digraph exact_child_sub_lmb {
1970 edge [num=1 mass=1]
1971 node [num=1]
1972 incoming [style=invis]
1973 outgoing [style=invis]
1974
1975 incoming -> v3 [id=0]
1976 v0 -> v1 [id=1 lmb_id=0]
1977 v0 -> v1 [id=2]
1978 v3 -> v0 [id=3 lmb_id=1]
1979 v1 -> v2 [id=4]
1980 v2 -> v3 [id=5]
1981 v2 -> outgoing [id=6]
1982 })?;
1983 let uv_edges = [EdgeIndex(1), EdgeIndex(2)];
1984 let uv_filter = graph
1985 .get_edge_subgraph(uv_edges[0])
1986 .union(&graph.get_edge_subgraph(uv_edges[1]));
1987 let uv_subgraph = InternalSubGraph::cleaned_filter_optimist(uv_filter, graph.as_ref());
1988 let crown = graph.dummy_stripped_external_flows_of(&uv_subgraph);
1989 let boundary_hedges = crown.included_iter().collect::<Vec<_>>();
1990 let sub_lmb =
1991 graph.try_compatible_sub_lmb(&uv_subgraph, crown, &graph.loop_momentum_basis)?;
1992 assert_eq!(sub_lmb.loop_edges.len(), 1);
1993 assert!(sub_lmb.ext_edges.contains(&EdgeIndex(4)));
1994
1995 let denominators = uv_edges.map(|edge| FourDDenominator {
1996 source_edge: edge,
1997 momentum: FunctionBuilder::new(GS.emr_mom)
1998 .add_arg(usize::from(edge))
1999 .finish(),
2000 mass_squared: graph.underlying[edge].particle.mass_atom().pow(2),
2001 full_expr: Atom::one(),
2002 });
2003 assert!(
2004 GraphThreeDSource::from_exact_denominators_in_uv_sub_lmb(
2005 &graph,
2006 &denominators,
2007 uv_edges,
2008 boundary_hedges.iter().copied().take(1),
2009 &sub_lmb,
2010 ExactUvSubLmbFrame::RetainedPhysicalCrown,
2011 )
2012 .is_err(),
2013 "a proper child source must not omit part of its non-dummy crown",
2014 );
2015 {
2016 let source = GraphThreeDSource::from_exact_denominators_in_uv_sub_lmb(
2017 &graph,
2018 &denominators,
2019 uv_edges,
2020 boundary_hedges.iter().copied(),
2021 &sub_lmb,
2022 ExactUvSubLmbFrame::RetainedPhysicalCrown,
2023 )?;
2024 let parsed = source.to_three_d_parsed_graph()?;
2025 assert_eq!(parsed.loop_names.len(), 1);
2026 let local_to_occurrence = source
2027 .energy_edge_index_map(&parsed)
2028 .expect("the exact child source exposes occurrence energies")
2029 .internal;
2030 let occurrence_to_owner = source
2031 .physical_energy_edge_index_map()
2032 .expect("the exact child source exposes physical owners")
2033 .internal;
2034 let shifted_local_edge = local_to_occurrence
2035 .iter()
2036 .find_map(|(local, occurrence)| {
2037 (occurrence_to_owner.get(occurrence) == Some(&usize::from(uv_edges[1])))
2038 .then_some(*local)
2039 })
2040 .expect("the second child denominator retains its source occurrence");
2041 let shifted_signature = &parsed.internal_edges[shifted_local_edge].signature;
2042 assert!(
2043 shifted_signature
2044 .external_signature
2045 .iter()
2046 .any(|coefficient| *coefficient != 0),
2047 "D2 must retain the parent-loop crown momentum as a child external shift",
2048 );
2049 }
2050
2051 let options = graph.denominator_only_cff_3d_expression_options();
2052 let cutset = CutSet::empty(graph.n_hedges());
2053 let (exact, _) = graph.cff_from_4d_denominators_in_uv_sub_lmb(
2054 &denominators,
2055 uv_edges,
2056 boundary_hedges,
2057 &sub_lmb,
2058 ExactUvSubLmbFrame::RetainedPhysicalCrown,
2059 &cutset,
2060 &options,
2061 &Atom::one(),
2062 None,
2063 &[],
2064 )?;
2065 let exact_term = exact
2066 .terms
2067 .get(&CutCFFIndex::new_all_none())
2068 .expect("the exact child has one uncut sector");
2069 let exact_sum = exact_term.orientations.iter().try_fold(
2070 Atom::Zero,
2071 |sum, orientation| -> Result<Atom> {
2072 Ok(sum
2073 + &orientation.expression
2074 * exact_term.map_exact_source_numerator(&orientation.orientation, None)?)
2075 },
2076 )? * Atom::num(exact.production_prefactor_factor());
2077
2078 let mut child: Graph = dot!(digraph ordinary_child {
2079 edge [num=1 mass=1]
2080 node [num=1]
2081 incoming [style=invis]
2082 outgoing [style=invis]
2083
2084 incoming -> a [id=2]
2085 a -> b [id=0 lmb_id=0]
2086 a -> b [id=1]
2087 b -> outgoing [id=3]
2088 })?;
2089 let child_options = child.denominator_only_cff_3d_expression_options();
2090 let child_cutset = CutSet::empty(child.n_hedges());
2091 let child_contract = child.empty_subgraph::<SuBitGraph>();
2092 let ordinary = child.cff(
2093 &child_contract,
2094 &child_cutset,
2095 &OrientationPattern::default(),
2096 &child_options,
2097 None,
2098 )?;
2099 let ordinary_term = ordinary
2100 .terms
2101 .get(&CutCFFIndex::new_all_none())
2102 .expect("the standalone child has one uncut sector");
2103 let mut ordinary_sum = ordinary_term
2104 .orientations
2105 .iter()
2106 .fold(Atom::Zero, |sum, orientation| sum + &orientation.expression)
2107 * Atom::num(ordinary.production_prefactor_factor());
2108 ordinary_sum = ordinary_sum.replace_multiple(
2109 [(0, 1), (1, 2), (2, 3), (3, 4)]
2110 .into_iter()
2111 .flat_map(|(child_edge, parent_edge)| {
2112 [
2113 Replacement::new(
2114 function!(GS.ose, child_edge, W_.x___).to_pattern(),
2115 function!(GS.ose, parent_edge, W_.x___).to_pattern(),
2116 ),
2117 Replacement::new(
2118 function!(GS.emr_mom, child_edge, W_.x___).to_pattern(),
2119 function!(GS.emr_mom, parent_edge, W_.x___).to_pattern(),
2120 ),
2121 ]
2122 })
2123 .collect::<Vec<_>>(),
2124 );
2125 assert_eq!(
2126 exact_sum, ordinary_sum,
2127 "the parent-source child CFF must equal its standalone ordinary representation"
2128 );
2129 Ok(())
2130 }
2131
2132 #[test]
2133 fn exact_child_sub_lmb_keeps_remote_cut_and_crown_emrs_factorized() -> Result<()> {
2134 test_initialise()?;
2135 let mut graph: Graph = dot!(
2136 digraph exact_child_lu_aliases {
2137 num = 1
2138 edge [particle="scalar_1" num=1]
2139 node [num=1]
2140
2141 ext0 [style=invis is_cut=0]
2142 v2 -> ext0 [id=0]
2143 ext0 -> v3
2144 v0 -> v1 [id=1 lmb_id=0]
2145 v0 -> v1 [id=2]
2146 v3 -> v0 [id=3 lmb_id=1]
2147 v1 -> v2 [id=4]
2148 v2 -> v3 [id=5]
2149 },
2150 "scalars"
2151 )?;
2152 let uv_edges = [EdgeIndex(1), EdgeIndex(2)];
2153 let uv_filter = graph
2154 .get_edge_subgraph(uv_edges[0])
2155 .union(&graph.get_edge_subgraph(uv_edges[1]));
2156 let uv_subgraph = InternalSubGraph::cleaned_filter_optimist(uv_filter, graph.as_ref());
2157 let crown = graph.dummy_stripped_external_flows_of(&uv_subgraph);
2158 let boundary_hedges = crown.included_iter().collect::<Vec<_>>();
2159 assert!(
2160 boundary_hedges
2161 .iter()
2162 .all(|hedge| graph.underlying[hedge] != EdgeIndex(0)),
2163 "the remote Cutkosky carrier is not part of the physical child crown",
2164 );
2165 let sub_lmb =
2166 graph.try_compatible_sub_lmb(&uv_subgraph, crown, &graph.loop_momentum_basis)?;
2167 assert!(
2168 sub_lmb.edge_signatures[EdgeIndex(0)]
2169 .external
2170 .iter()
2171 .all(|coefficient| *coefficient == crate::momentum::SignOrZero::Zero),
2172 "the remote cut must have no child external coordinate",
2173 );
2174 let denominators = uv_edges.map(|edge| FourDDenominator {
2175 source_edge: edge,
2176 momentum: FunctionBuilder::new(GS.emr_mom)
2177 .add_arg(usize::from(edge))
2178 .finish(),
2179 mass_squared: graph.underlying[edge].particle.mass_atom().pow(2),
2180 full_expr: Atom::one(),
2181 });
2182 {
2183 let source = GraphThreeDSource::from_exact_denominators_in_uv_sub_lmb(
2184 &graph,
2185 &denominators,
2186 uv_edges,
2187 boundary_hedges.iter().copied(),
2188 &sub_lmb,
2189 ExactUvSubLmbFrame::RetainedPhysicalCrown,
2190 )?;
2191 let parsed = source.to_three_d_parsed_graph()?;
2192 assert_eq!(parsed.loop_names.len(), 1);
2193 assert!(
2194 parsed.initial_state_cut_edges.is_empty(),
2195 "a factorized UV child must not import its parent's Cutkosky carrier",
2196 );
2197
2198 let options = graph.denominator_only_cff_3d_expression_options();
2199 let shell_energy = GS.emr_mom(EdgeIndex(1), GS.cind(0));
2200 let crown_energy = GS.emr_mom(EdgeIndex(3), GS.cind(0));
2201 let analysis_numerator = &shell_energy * &crown_energy;
2202 let preparation = graph.prepare_3d_expression_for_4d_term(
2203 &source,
2204 &options,
2205 &analysis_numerator,
2206 &[],
2207 )?;
2208 let (generated, mapper, plan, report) =
2209 graph.generate_3d_expression_for_4d_term(&preparation, None)?;
2210 let mapper = &mapper.mapper;
2211 assert_eq!(report.physical_parent_bounds, vec![(1, 1)]);
2212 assert_eq!(
2213 report.assigned_cff_source_bounds,
2214 plan.energy_degree_bounds()
2215 );
2216 assert!(!generated.expression.orientations.is_empty());
2217 for orientation in &generated.expression.orientations {
2218 let mapped_shell = mapper.map_numerator(
2219 &orientation.loop_energy_map,
2220 &orientation.edge_energy_map,
2221 &shell_energy,
2222 )?;
2223 assert_ne!(mapped_shell, shell_energy);
2224 assert_eq!(
2225 mapper.map_numerator(
2226 &orientation.loop_energy_map,
2227 &orientation.edge_energy_map,
2228 &analysis_numerator,
2229 )?,
2230 mapped_shell * &crown_energy,
2231 "the shell energy is CFF-mapped while the crown energy stays factorized",
2232 );
2233 let factorized_parent =
2234 GS.emr_mom(EdgeIndex(0), GS.cind(0)) + GS.emr_mom(EdgeIndex(3), GS.cind(0));
2235 assert_eq!(
2236 mapper.map_numerator(
2237 &orientation.loop_energy_map,
2238 &orientation.edge_energy_map,
2239 &factorized_parent,
2240 )?,
2241 factorized_parent,
2242 "remote cut and crown EMRs must remain T-inert for the outer graph",
2243 );
2244 }
2245 }
2246
2247 let options = graph.denominator_only_cff_3d_expression_options();
2248 let cutset = CutSet::empty(graph.n_hedges());
2249 graph.cff_from_4d_denominators_in_uv_sub_lmb(
2250 &denominators,
2251 uv_edges,
2252 boundary_hedges,
2253 &sub_lmb,
2254 ExactUvSubLmbFrame::RetainedPhysicalCrown,
2255 &cutset,
2256 &options,
2257 &Atom::one(),
2258 None,
2259 &[],
2260 )?;
2261 Ok(())
2262 }
2263
2264 #[test]
2265 fn exact_child_signed_maps_keep_owner_invariant_production_hosts() -> Result<()> {
2266 test_initialise()?;
2267 let mut graph: Graph = dot!(digraph exact_child_orientation_projection {
2268 edge [num=1 mass=1]
2269 node [num=1]
2270 incoming [style=invis]
2271 outgoing [style=invis]
2272
2273 incoming -> v3 [id=0]
2274 v0 -> v1 [id=1 lmb_id=0]
2275 v0 -> v1 [id=2]
2276 v3 -> v0 [id=3 lmb_id=1]
2277 v1 -> v2 [id=4]
2278 v2 -> v3 [id=5]
2279 v2 -> outgoing [id=6]
2280 })?;
2281 let uv_edges = [EdgeIndex(1), EdgeIndex(2)];
2282 let uv_filter = graph
2283 .get_edge_subgraph(uv_edges[0])
2284 .union(&graph.get_edge_subgraph(uv_edges[1]));
2285 let uv_subgraph = InternalSubGraph::cleaned_filter_optimist(uv_filter, graph.as_ref());
2286 let crown = graph.dummy_stripped_external_flows_of(&uv_subgraph);
2287 let boundary_hedges = crown.included_iter().collect::<Vec<_>>();
2288 let sub_lmb =
2289 graph.try_compatible_sub_lmb(&uv_subgraph, crown, &graph.loop_momentum_basis)?;
2290 let options = graph.denominator_only_cff_3d_expression_options();
2291 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
2292 let production =
2293 graph.generate_3d_expression_for_integrand(&[], &canonization, &options, None)?;
2294 let pattern = OrientationPattern::default();
2295 let cutset = CutSet::empty(graph.n_hedges());
2296 let localizer = Localizer::new(
2297 &cutset,
2298 OrientationProjection::exact_expression(&production, &options, &pattern, true),
2299 );
2300 let source_denominator = |edge| FourDDenominator {
2301 source_edge: edge,
2302 momentum: FunctionBuilder::new(GS.emr_mom)
2303 .add_arg(usize::from(edge))
2304 .finish(),
2305 mass_squared: graph.underlying[edge].particle.mass_atom().pow(2),
2306 full_expr: Atom::one(),
2307 };
2308 let denominators = uv_edges.map(source_denominator);
2309 let mut opposite_denominators = denominators.clone();
2310 opposite_denominators[0].momentum = -opposite_denominators[0].momentum.clone();
2311 let physical_host_classes =
2312 |denominators: &[FourDDenominator]| -> Result<BTreeSet<Vec<OrientationID>>> {
2313 let source = GraphThreeDSource::from_exact_denominators_in_uv_sub_lmb(
2314 &graph,
2315 denominators,
2316 uv_edges,
2317 boundary_hedges.iter().copied(),
2318 &sub_lmb,
2319 ExactUvSubLmbFrame::RetainedPhysicalCrown,
2320 )?;
2321 let preparation = graph.prepare_3d_expression_for_4d_term(
2322 &source,
2323 &options,
2324 &Atom::one(),
2325 &[],
2326 )?;
2327 let (child, _, _, _) =
2328 graph.generate_3d_expression_for_4d_term(&preparation, None)?;
2329 child
2330 .expression
2331 .orientations
2332 .iter()
2333 .map(|child_orientation| {
2334 let candidates = localizer.source_selector_representatives(
2335 &graph,
2336 child_orientation,
2337 &source.contract_subgraph(),
2338 )?;
2339 assert!(
2340 !candidates.is_empty(),
2341 "every exact child residue must extend to a production map using only its physical graph-edge prefix; denominators={denominators:?}, child={:?}",
2342 child_orientation.data,
2343 );
2344 Ok(candidates)
2345 })
2346 .collect()
2347 };
2348
2349 let ordinary_hosts = physical_host_classes(&denominators)?;
2350 assert_eq!(
2351 physical_host_classes(&opposite_denominators)?,
2352 ordinary_hosts,
2353 "D(Q) and D(-Q) must induce identical physical orientation constraints",
2354 );
2355 Ok(())
2356 }
2357
2358 #[test]
2359 fn exact_unexpanded_cff_matches_ordinary_for_cross_loop_factorized_energy() -> Result<()> {
2360 test_initialise()?;
2361 let mut graph: Graph = dot!(
2362 digraph exact_unexpanded_cross_loop_factorized_energy {
2363 num = 1
2364 edge [particle="scalar_1" num=1]
2365 node [num=1]
2366
2367 ext0 [style=invis is_cut=0]
2368 v2 -> ext0 [id=0]
2369 ext0 -> v3
2370 v0 -> v1 [id=1 lmb_id=0]
2371 v0 -> v1 [id=2]
2372 v3 -> v0 [id=3 lmb_id=1]
2373 v1 -> v2 [id=4]
2374 v2 -> v3 [id=5]
2375 },
2376 "scalars"
2377 )?;
2378 let numerator = GS.emr_mom(EdgeIndex(1), GS.cind(0))
2379 * GS.emr_mom(EdgeIndex(3), GS.cind(0)).pow(2)
2380 * GS.emr_mom(EdgeIndex(4), GS.cind(0))
2381 * GS.emr_mom(EdgeIndex(5), GS.cind(0));
2382 let options = graph.denominator_only_cff_3d_expression_options();
2383 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
2384 let production = graph.generate_3d_expression_for_integrand(
2385 &[],
2386 &canonization,
2387 &options,
2388 Some(&numerator),
2389 )?;
2390 let cutset = CutSet::empty(graph.n_hedges());
2391 let ordinary = graph.cff_from_production_expression(
2392 &production,
2393 &cutset,
2394 &OrientationPattern::default(),
2395 )?;
2396 let cograph = graph.full_filter().subtract(&graph.initial_state_cut);
2397 let denominators = Full4dCts::from_coefficient(&Atom::one(), &graph, &cograph)
2398 .terms()?
2399 .into_iter()
2400 .next()
2401 .expect("the unexpanded graph has one exact denominator term")
2402 .denominators;
2403 let (exact, _) =
2404 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &numerator)?;
2405
2406 assert_eq!(
2407 ordinary.energy_degree_bound_report.physical_parent_bounds,
2408 exact.energy_degree_bound_report.physical_parent_bounds
2409 );
2410 assert_eq!(
2411 ordinary
2412 .energy_degree_bound_report
2413 .assigned_cff_source_bounds,
2414 exact.energy_degree_bound_report.assigned_cff_source_bounds,
2415 "a one-occurrence exact source must preserve physical energy IDs"
2416 );
2417 let index = CutCFFIndex::new_all_none();
2418 let ordinary_term = ordinary
2419 .terms
2420 .get(&index)
2421 .expect("the ordinary uncut sector exists");
2422 let exact_term = exact
2423 .terms
2424 .get(&index)
2425 .expect("the exact uncut sector exists");
2426 let ordinary_sum = ordinary_term
2427 .orientations
2428 .iter()
2429 .map(|orientation| {
2430 orientation.expression.clone()
2431 * numerator
2432 .replace_multiple(orientation.orientation.energy_replacements_gs(&graph))
2433 })
2434 .fold(Atom::Zero, |sum, term| sum + term)
2435 * Atom::num(ordinary.production_prefactor_factor());
2436 let exact_sum = exact_term
2437 .orientations
2438 .iter()
2439 .map(|orientation| {
2440 Ok(orientation.expression.clone()
2441 * exact_term.map_exact_source_numerator(&orientation.orientation, None)?)
2442 })
2443 .collect::<Result<Vec<_>>>()?
2444 .into_iter()
2445 .fold(Atom::Zero, |sum, term| sum + term)
2446 * Atom::num(exact.production_prefactor_factor());
2447
2448 let mass_squared = Atom::num(Rational::from((4, 9)));
2449 let external_energy = Atom::num(Rational::from((5, 2)));
2450 let external_edges = graph
2451 .external_momentum_edge_order()
2452 .into_iter()
2453 .collect::<BTreeSet<_>>();
2454 let evaluate_arb = |mut expression: Atom| -> Result<Complex<F<ArbPrec>>> {
2455 expression = expression
2456 .replace(Atom::var(GS.numerator_sampling_scale))
2457 .with(Atom::num(Rational::from((13, 10))))
2458 .replace(function!(GS.tree_denom_wrapper, W_.x_))
2459 .with(W_.x_)
2460 .replace(GS.den(W_.a_, W_.b_, W_.c_, W_.d_))
2461 .with(W_.d_);
2462 for edge in 0..graph.underlying.n_edges() {
2463 let edge = EdgeIndex(edge);
2464 expression = expression
2465 .replace(graph.underlying[edge].particle.mass_atom())
2466 .with(mass_squared.clone().sqrt());
2467 if external_edges.contains(&edge) {
2468 expression = expression
2469 .replace(GS.emr_mom(edge, GS.cind(0)))
2470 .with(external_energy.clone());
2471 for spatial_index in 1..=3 {
2472 expression = expression
2473 .replace(GS.emr_mom(edge, GS.cind(spatial_index)))
2474 .with(Atom::Zero)
2475 .replace(GS.emr_vec(edge, GS.cind(spatial_index)))
2476 .with(Atom::Zero);
2477 }
2478 } else {
2479 let spatial = match usize::from(edge) {
2480 1 => Atom::num(Rational::from((4, 3))),
2481 2 => Atom::num(Rational::from((-7, 12))),
2482 3..=5 => Atom::num(Rational::from((3, 4))),
2483 _ => Atom::Zero,
2484 };
2485 let on_shell_energy = (mass_squared.clone() + spatial.clone().pow(2)).sqrt();
2486 expression = expression
2487 .replace(GS.emr_mom(edge, GS.cind(1)))
2488 .with(spatial.clone())
2489 .replace(GS.emr_vec(edge, GS.cind(1)))
2490 .with(spatial)
2491 .replace(GS.ose(edge))
2492 .with(on_shell_energy.clone())
2493 .replace(cut_energy(edge))
2494 .with(on_shell_energy);
2495 for spatial_index in 2..=3 {
2496 expression = expression
2497 .replace(GS.emr_mom(edge, GS.cind(spatial_index)))
2498 .with(Atom::Zero)
2499 .replace(GS.emr_vec(edge, GS.cind(spatial_index)))
2500 .with(Atom::Zero);
2501 }
2502 }
2503 }
2504 let parameters = [Atom::var(GS.pi)];
2505 let rational: ExpressionEvaluator<SymComplex<Fraction<IntegerRing>>> =
2506 expression.evaluator(¶meters).build().map_err(|error| {
2507 eyre::eyre!("failed to build source-identity evaluator: {error}")
2508 })?;
2509 let mut arb: ExpressionEvaluator<Complex<F<ArbPrec>>> =
2510 rational.map_coeff(&|coefficient| {
2511 Complex::new(F::from(&coefficient.re), F::from(&coefficient.im))
2512 });
2513 let zero = F(ArbPrec::default());
2514 Ok(arb.evaluate_single(&[Complex::new(zero.clone().pi(), zero)]))
2515 };
2516 let ordinary_value = evaluate_arb(ordinary_sum)?;
2517 let exact_value = evaluate_arb(exact_sum)?;
2518 let distance = (ordinary_value.clone() - exact_value.clone()).norm().re;
2519 let ordinary_norm = ordinary_value.clone().norm().re;
2520 let exact_norm = exact_value.clone().norm().re;
2521 let scale = if ordinary_norm > exact_norm {
2522 ordinary_norm
2523 } else {
2524 exact_norm
2525 };
2526 let relative_distance = if scale.is_zero() {
2527 distance
2528 } else {
2529 distance / scale
2530 };
2531 let tolerance = F(ArbPrec::default().epsilon()).sqrt().sqrt().sqrt();
2534 assert!(
2535 relative_distance <= tolerance,
2536 "source-preserving exact CFF differs from ordinary CFF: ordinary={ordinary_value:e}, exact={exact_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
2537 );
2538 Ok(())
2539 }
2540
2541 #[test]
2542 fn triangle_affine_energy_identity_survives_production_mapping() -> Result<()> {
2543 test_initialise()?;
2544 let graph: Graph = dot!(
2545 digraph diagnostic_triangle_affine_energy_identity {
2546 num = 1
2547 edge [particle="scalar_1" num=1]
2548 node [num=1]
2549
2550 ext [style=invis]
2551 v0;
2552 v1;
2553 v2;
2554 ext -> v0 [id=0]
2555 ext -> v1 [id=1]
2556 v2 -> ext [id=2]
2557 v0 -> v2 [id=3 lmb_id=0]
2558 v1 -> v0 [id=4]
2559 v2 -> v1 [id=5]
2560 },
2561 "scalars"
2562 )?;
2563 let q = GS.emr_mom(EdgeIndex(3), GS.cind(0));
2564 let shifted = GS.emr_mom(EdgeIndex(4), GS.cind(0));
2565 let external = GS.emr_mom(EdgeIndex(0), GS.cind(0));
2566 let squared = q.clone().pow(2);
2567 let affine = q * (shifted + external);
2568 let build_sum = |numerator: &Atom| -> Result<Atom> {
2569 let mut graph = graph.clone();
2570 let options = graph.denominator_only_cff_3d_expression_options();
2571 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
2572 let production = graph.generate_3d_expression_for_integrand(
2573 &[],
2574 &canonization,
2575 &options,
2576 Some(numerator),
2577 )?;
2578 let cff = graph.cff_from_production_expression(
2579 &production,
2580 &CutSet::empty(graph.n_hedges()),
2581 &OrientationPattern::default(),
2582 )?;
2583 let term = cff
2584 .terms
2585 .get(&CutCFFIndex::new_all_none())
2586 .expect("the triangle has one uncut CFF sector");
2587 Ok(term
2588 .orientations
2589 .iter()
2590 .map(|orientation| {
2591 orientation.expression.clone()
2592 * numerator.replace_multiple(
2593 orientation.orientation.energy_replacements_gs(&graph),
2594 )
2595 })
2596 .fold(Atom::Zero, |sum, term| sum + term)
2597 * Atom::num(cff.production_prefactor_factor()))
2598 };
2599 let evaluate_arb = |mut expression: Atom| -> Result<Complex<F<ArbPrec>>> {
2600 expression = expression
2601 .replace(function!(GS.tree_denom_wrapper, W_.x_))
2602 .with(W_.x_)
2603 .replace(GS.den(W_.a_, W_.b_, W_.c_, W_.d_))
2604 .with(W_.d_);
2605 for (edge, energy) in [
2606 (EdgeIndex(0), Atom::num(Rational::from((7, 10)))),
2607 (EdgeIndex(1), Atom::num(Rational::from((-23, 100)))),
2608 (EdgeIndex(2), Atom::num(Rational::from((47, 100)))),
2609 ] {
2610 expression = expression
2611 .replace(GS.emr_mom(edge, GS.cind(0)))
2612 .with(energy);
2613 }
2614 let mass_squared = Atom::num(1);
2615 for (edge, spatial) in [
2616 (EdgeIndex(3), [(31, 100), (-47, 100), (83, 100)]),
2617 (EdgeIndex(4), [(20, 100), (-39, 100), (78, 100)]),
2618 (EdgeIndex(5), [(24, 100), (-46, 100), (87, 100)]),
2619 ] {
2620 let energy = spatial
2621 .into_iter()
2622 .fold(mass_squared.clone(), |sum, value| {
2623 sum + Atom::num(Rational::from(value)).pow(2)
2624 })
2625 .sqrt();
2626 expression = expression
2627 .replace(GS.ose(edge))
2628 .with(energy.clone())
2629 .replace(cut_energy(edge))
2630 .with(energy);
2631 }
2632 let parameters = [Atom::var(GS.pi)];
2633 let rational: ExpressionEvaluator<SymComplex<Fraction<IntegerRing>>> = expression
2634 .evaluator(¶meters)
2635 .build()
2636 .map_err(|error| eyre::eyre!("failed to build triangle evaluator: {error}"))?;
2637 let mut arb: ExpressionEvaluator<Complex<F<ArbPrec>>> =
2638 rational.map_coeff(&|coefficient| {
2639 Complex::new(F::from(&coefficient.re), F::from(&coefficient.im))
2640 });
2641 let zero = F(ArbPrec::default());
2642 let pi = zero.clone().pi();
2643 Ok(arb.evaluate_single(&[Complex::new(pi, zero)]))
2644 };
2645 let squared_sum = build_sum(&squared)?;
2646 let affine_sum = build_sum(&affine)?;
2647 let squared_value = evaluate_arb(squared_sum.clone())?;
2648 let affine_value = evaluate_arb(affine_sum.clone())?;
2649 let distance = (squared_value.clone() - affine_value.clone()).norm().re;
2650 let scale = {
2651 let squared_norm = squared_value.clone().norm().re;
2652 let affine_norm = affine_value.clone().norm().re;
2653 if squared_norm > affine_norm {
2654 squared_norm
2655 } else {
2656 affine_norm
2657 }
2658 };
2659 let relative_distance = if scale.is_zero() {
2660 distance
2661 } else {
2662 distance / scale
2663 };
2664 let tolerance = F(ArbPrec::default().epsilon()).sqrt().sqrt().sqrt();
2665 assert!(
2666 relative_distance <= tolerance,
2667 "GammaLoop's mapped triangle CFF violates Q3^0=Q4^0+Q0^0: squared={squared_value:e}, affine={affine_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
2668 );
2669
2670 let rational = |numerator: i64, denominator: i64| {
2671 F::<ArbPrec>::from(&Rational::from((numerator, denominator)))
2672 };
2673 let loop_moms: LoopMomenta<F<ArbPrec>> = [ThreeMomentum::new(
2674 rational(31, 100),
2675 rational(-47, 100),
2676 rational(83, 100),
2677 )]
2678 .into_iter()
2679 .collect();
2680 let external_moms: ExternalFourMomenta<F<ArbPrec>> = [
2681 [
2682 rational(7, 10),
2683 rational(11, 100),
2684 rational(-8, 100),
2685 rational(5, 100),
2686 ]
2687 .into(),
2688 [
2689 rational(-23, 100),
2690 rational(-4, 100),
2691 rational(7, 100),
2692 rational(-9, 100),
2693 ]
2694 .into(),
2695 [
2696 rational(47, 100),
2697 rational(7, 100),
2698 rational(-1, 100),
2699 rational(-4, 100),
2700 ]
2701 .into(),
2702 ]
2703 .into_iter()
2704 .collect();
2705 let sample = MomentumSample {
2706 sample: BareMomentumSample {
2707 loop_moms,
2708 dual_loop_moms: None,
2709 loop_mom_cache_id: 0,
2710 loop_mom_base_cache_id: 0,
2711 external_moms,
2712 external_mom_cache_id: 0,
2713 external_mom_base_cache_id: 0,
2714 jacobian: rational(1, 1),
2715 orientation: None,
2716 parameterization_branch: None,
2717 },
2718 };
2719 let orientations = TiVec::<OrientationID, EdgeVec<Orientation>>::new();
2720 let orientation_filter = SubSet::full(orientations.len());
2721 let runtime_settings = RuntimeSettings::default();
2722 let evaluate_stack = |expression: &Atom| -> Result<Complex<F<ArbPrec>>> {
2723 let mut param_builder = graph.param_builder.clone();
2724 let (mut evaluator, _) = EvaluatorStack::new_explicit_sum_with_timings(
2725 std::slice::from_ref(expression),
2726 ¶m_builder,
2727 &[],
2728 None,
2729 &EvaluatorSettings::default(),
2730 )?;
2731 let input = <ArbPrec as GenericEvaluatorFloat>::get_parameters(
2732 &mut param_builder,
2733 (false, false),
2734 &graph,
2735 &sample,
2736 &[],
2737 &[],
2738 None,
2739 None,
2740 None,
2741 );
2742 Ok(evaluator
2743 .evaluate(
2744 input,
2745 SingleOrAllOrientations::All {
2746 all: &orientations,
2747 filter: &orientation_filter,
2748 },
2749 &runtime_settings,
2750 &mut EvaluationMetaData::new_empty(),
2751 )?
2752 .pop()
2753 .expect("the triangle evaluator should return one value")
2754 .unwrap_real())
2755 };
2756 let squared_stack_value = evaluate_stack(&squared_sum)?;
2757 let affine_stack_value = evaluate_stack(&affine_sum)?;
2758
2759 for (comparison, left, right) in [
2760 (
2761 "squared direct vs production stack",
2762 &squared_value,
2763 &squared_stack_value,
2764 ),
2765 (
2766 "affine direct vs production stack",
2767 &affine_value,
2768 &affine_stack_value,
2769 ),
2770 (
2771 "squared vs affine production stack",
2772 &squared_stack_value,
2773 &affine_stack_value,
2774 ),
2775 ] {
2776 let distance = (left.clone() - right.clone()).norm().re;
2777 let left_norm = left.clone().norm().re;
2778 let right_norm = right.clone().norm().re;
2779 let scale = if left_norm > right_norm {
2780 left_norm
2781 } else {
2782 right_norm
2783 };
2784 let relative_distance = if scale.is_zero() {
2785 distance
2786 } else {
2787 distance / scale
2788 };
2789 assert!(
2790 relative_distance <= tolerance,
2791 "{comparison} failed: left={left:e}, right={right:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
2792 );
2793 }
2794 Ok(())
2795 }
2796
2797 #[test]
2798 fn indexed_residue_selection_tracks_axes_independent_of_raised_order() -> Result<()> {
2799 let raised_group = crate::cff::esurface::RaisedEsurfaceGroup {
2800 esurface_ids: vec![crate::cff::esurface::EsurfaceID::from(0)],
2801 max_occurence: 2,
2802 };
2803 for selected_axis_count in 0usize..=3 {
2804 let mut cutset = CutSet::empty(1);
2805 if selected_axis_count >= 1 {
2806 cutset.residue_selector.right_th_cut = Some(raised_group.clone());
2807 }
2808 if selected_axis_count >= 2 {
2809 cutset.residue_selector.left_th_cut = Some(raised_group.clone());
2810 }
2811 if selected_axis_count >= 3 {
2812 cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
2813 raised_group: raised_group.clone(),
2814 cut_edge_alternatives: vec![vec![EdgeIndex(0)]],
2815 });
2816 }
2817
2818 let mut expression = ThreeDExpression::new_empty();
2819 expression.surfaces.esurface_cache.push(esurface::Esurface {
2820 energies: vec![EdgeIndex(0)],
2821 external_shift: Vec::new(),
2822 vertex_set: VertexSet::dummy(),
2823 });
2824 let residues = select_indexed_cff_residues(expression, &cutset)?;
2825 assert_eq!(residues.len(), 1usize << selected_axis_count);
2826 assert!(residues.iter().all(|(index, _)| {
2827 let present_axes = [
2828 index.right_threshold_order,
2829 index.left_threshold_order,
2830 index.lu_cut_order,
2831 ]
2832 .into_iter()
2833 .flatten()
2834 .count();
2835 present_axes == selected_axis_count
2836 }));
2837 if selected_axis_count != 0 {
2838 assert!(
2839 residues.iter().any(|(index, _)| {
2840 [
2841 index.right_threshold_order,
2842 index.left_threshold_order,
2843 index.lu_cut_order,
2844 ]
2845 .into_iter()
2846 .flatten()
2847 .any(|order| order == 2)
2848 }),
2849 "raised order two must retain the same selected-axis parity"
2850 );
2851 }
2852 }
2853 Ok(())
2854 }
2855
2856 #[test]
2857 fn exact_cff_keeps_dotted_same_edge_occurrences() -> Result<()> {
2858 test_initialise()?;
2859 let mut graph: Graph = dot!(digraph exact_dotted {
2860 edge [num=1 mass=1]
2861 node [num=1]
2862 incoming [style=invis]
2863 outgoing [style=invis]
2864
2865 incoming -> a [id=2]
2866 a -> b [id=0 lmb_id=0]
2867 a -> b [id=1]
2868 b -> outgoing [id=3]
2869 })?;
2870 let edge = EdgeIndex::from(0);
2871 let momentum = FunctionBuilder::new(GS.emr_mom)
2872 .add_arg(usize::from(edge))
2873 .finish();
2874 let mass_squared = graph.underlying[edge].particle.mass_atom().pow(2);
2875 let denominators = [
2876 FourDDenominator {
2877 source_edge: edge,
2878 momentum: momentum.clone(),
2879 mass_squared: mass_squared.clone(),
2880 full_expr: Atom::var(symbolica::symbol!("exact_cff_test::first")),
2881 },
2882 FourDDenominator {
2883 source_edge: edge,
2884 momentum,
2885 mass_squared: mass_squared.clone(),
2886 full_expr: Atom::var(symbolica::symbol!("exact_cff_test::second")),
2887 },
2888 ];
2889 let options = graph.denominator_only_cff_3d_expression_options();
2890 let cutset = CutSet::empty(graph.n_hedges());
2891
2892 let (cff, _) =
2893 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &Atom::one())?;
2894 assert_eq!(cff.terms.len(), 1);
2895 let on_shell_energy = (1..=3)
2896 .fold(mass_squared, |norm_squared, spatial_index| {
2897 norm_squared + GS.emr_mom(edge, GS.cind(spatial_index)).pow(2)
2898 })
2899 .sqrt();
2900 let expected_orientation_sum =
2904 Atom::i() / (Atom::num(32) * Atom::var(GS.pi).pow(3) * on_shell_energy.pow(3));
2905 let terms = &cff
2906 .terms
2907 .values()
2908 .next()
2909 .expect("the empty cutset has one CFF term")
2910 .orientations;
2911 let orientation_sum = terms
2912 .iter()
2913 .fold(Atom::zero(), |sum, term| sum + &term.expression)
2914 .replace(GS.ose(edge))
2915 .with(on_shell_energy);
2916 assert!(
2917 (orientation_sum * Atom::num(cff.production_prefactor_factor())
2918 - expected_orientation_sum)
2919 .together()
2920 .is_zero()
2921 );
2922 Ok(())
2923 }
2924
2925 #[test]
2926 fn exact_cff_uncancelled_powered_denominator_matches_lower_source() -> Result<()> {
2927 test_initialise()?;
2928 let mut graph: Graph = dot!(digraph exact_powered_identity {
2929 edge [num=1 mass=1]
2930 node [num=1]
2931
2932 a -> b [id=0 lmb_id=0]
2933 a -> b [id=1]
2934 })?;
2935 let edge = EdgeIndex(0);
2936 let momentum = FunctionBuilder::new(GS.emr_mom)
2937 .add_arg(usize::from(edge))
2938 .finish();
2939 let mass_squared = graph.underlying[edge].particle.mass_atom().pow(2);
2940 let full_expr = GS.emr_mom(edge, GS.cind(0)).pow(2)
2941 - (1..=3).fold(mass_squared.clone(), |norm_squared, spatial_index| {
2942 norm_squared + GS.emr_mom(edge, GS.cind(spatial_index)).pow(2)
2943 });
2944 let denominator = FourDDenominator {
2945 source_edge: edge,
2946 momentum: momentum.clone(),
2947 mass_squared: mass_squared.clone(),
2948 full_expr: full_expr.clone(),
2949 };
2950 let spectator_edge = EdgeIndex(1);
2951 let spectator_momentum = FunctionBuilder::new(GS.emr_mom)
2952 .add_arg(usize::from(spectator_edge))
2953 .finish();
2954 let spectator_mass_squared = graph.underlying[spectator_edge].particle.mass_atom().pow(2);
2955 let spectator = FourDDenominator {
2956 source_edge: spectator_edge,
2957 momentum: spectator_momentum,
2958 mass_squared: spectator_mass_squared.clone(),
2959 full_expr: GS.emr_mom(spectator_edge, GS.cind(0)).pow(2)
2960 - (1..=3).fold(spectator_mass_squared, |norm_squared, spatial_index| {
2961 norm_squared + GS.emr_mom(spectator_edge, GS.cind(spatial_index)).pow(2)
2962 }),
2963 };
2964 let denominators = [spectator, denominator.clone(), denominator];
2965 let retained_constant = Atom::var(symbolica::symbol!("exact_cff_test::retained_factor"));
2966 let retained_factor = GS.emr_mom(edge, GS.cind(0)) + &retained_constant;
2967 let numerator =
2968 GS.den(usize::from(edge), momentum, mass_squared, full_expr) * &retained_factor;
2969 let options = graph.denominator_only_cff_3d_expression_options();
2970 let cutset = CutSet::empty(graph.n_hedges());
2971 let exact_terms = |cff: &CutCFF| -> Result<Vec<Atom>> {
2972 let prefactor = Atom::num(cff.production_prefactor_factor());
2973 cff.terms
2974 .values()
2975 .flat_map(|term| {
2976 term.orientations.iter().map(|orientation| {
2977 Ok(orientation.expression.clone()
2978 * term.map_exact_source_numerator(&orientation.orientation, None)?
2979 * &prefactor)
2980 })
2981 })
2982 .collect::<Result<Vec<_>>>()
2983 };
2984
2985 let (powered, _) =
2986 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &numerator)?;
2987 let powered_terms = exact_terms(&powered)?;
2988
2989 let mut reversed_denominators = denominators.clone();
2993 for denominator in &mut reversed_denominators[1..] {
2994 denominator.momentum = -denominator.momentum.clone();
2995 }
2996 let (reversed, _) = graph.cff_from_4d_denominators(
2997 &reversed_denominators,
2998 &cutset,
2999 &options,
3000 &numerator,
3001 )?;
3002 let reversed_terms = exact_terms(&reversed)?;
3003 let (lower, _) = graph.cff_from_4d_denominators(
3004 &denominators[..2],
3005 &cutset,
3006 &options,
3007 &retained_factor,
3008 )?;
3009 let lower_terms = exact_terms(&lower)?;
3010 let denominator_only = GS.den(
3011 usize::from(edge),
3012 denominators[1].momentum.clone(),
3013 denominators[1].mass_squared.clone(),
3014 denominators[1].full_expr.clone(),
3015 );
3016 let (powered_scalar, _) =
3017 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &denominator_only)?;
3018 let powered_scalar_terms = exact_terms(&powered_scalar)?;
3019 let (lower_scalar, _) =
3020 graph.cff_from_4d_denominators(&denominators[..2], &cutset, &options, &Atom::one())?;
3021 let lower_scalar_terms = exact_terms(&lower_scalar)?;
3022 let contract: SuBitGraph = graph.empty_subgraph();
3023 let ordinary = graph.cff(
3024 &contract,
3025 &cutset,
3026 &OrientationPattern::default(),
3027 &options,
3028 Some(&retained_factor),
3029 )?;
3030 let ordinary_prefactor = Atom::num(ordinary.production_prefactor_factor());
3031 let ordinary_terms = ordinary
3032 .terms
3033 .values()
3034 .flat_map(|term| {
3035 term.orientations.iter().map(|orientation| {
3036 orientation.expression.clone()
3037 * retained_factor.replace_multiple(
3038 orientation.orientation.energy_replacements_gs(&graph),
3039 )
3040 * &ordinary_prefactor
3041 })
3042 })
3043 .collect::<Vec<_>>();
3044
3045 let mut quintic_denominators = vec![denominators[0].clone()];
3049 quintic_denominators.extend((0..5).map(|_| denominators[1].clone()));
3050 let retained_quartic = retained_factor.pow(4);
3051 let quintic_numerator = GS.den(
3052 usize::from(edge),
3053 denominators[1].momentum.clone(),
3054 denominators[1].mass_squared.clone(),
3055 denominators[1].full_expr.clone(),
3056 ) * &retained_quartic;
3057 let (quintic, _) = graph.cff_from_4d_denominators(
3058 &quintic_denominators,
3059 &cutset,
3060 &options,
3061 &quintic_numerator,
3062 )?;
3063 let quintic_terms = exact_terms(&quintic)?;
3064 let mut reversed_quintic_denominators = quintic_denominators.clone();
3065 for denominator in &mut reversed_quintic_denominators[1..] {
3066 denominator.momentum = -denominator.momentum.clone();
3067 }
3068 let (reversed_quintic, _) = graph.cff_from_4d_denominators(
3069 &reversed_quintic_denominators,
3070 &cutset,
3071 &options,
3072 &quintic_numerator,
3073 )?;
3074 let reversed_quintic_terms = exact_terms(&reversed_quintic)?;
3075 let (quartic, _) = graph.cff_from_4d_denominators(
3076 &quintic_denominators[..5],
3077 &cutset,
3078 &options,
3079 &retained_quartic,
3080 )?;
3081 let quartic_terms = exact_terms(&quartic)?;
3082
3083 let fixed_point = |mut expression: Atom, spatial: &Atom, energy: &Atom, constant: &Atom| {
3084 for source_edge in [edge, spectator_edge] {
3085 expression = expression
3086 .replace(GS.emr_mom(source_edge, GS.cind(1)))
3087 .with(spatial.clone())
3088 .replace(GS.ose(source_edge))
3089 .with(energy.clone());
3090 for spatial_index in 2..=3 {
3091 expression = expression
3092 .replace(GS.emr_mom(source_edge, GS.cind(spatial_index)))
3093 .with(Atom::Zero);
3094 }
3095 }
3096 expression
3097 .replace(retained_constant.clone())
3098 .with(constant.clone())
3099 };
3100 let evaluate_arb = |expression: Atom| -> Result<Complex<F<ArbPrec>>> {
3101 let parameters = [Atom::var(GS.pi)];
3102 let rational: ExpressionEvaluator<SymComplex<Fraction<IntegerRing>>> = expression
3103 .evaluator(¶meters)
3104 .build()
3105 .map_err(|error| eyre::eyre!("failed to build odd-routing oracle: {error}"))?;
3106 let mut arb: ExpressionEvaluator<Complex<F<ArbPrec>>> =
3107 rational.map_coeff(&|coefficient| {
3108 Complex::new(F::from(&coefficient.re), F::from(&coefficient.im))
3109 });
3110 Ok(arb.evaluate_single(&[Complex::new(
3111 F(ArbPrec::default()).pi(),
3112 F(ArbPrec::default()),
3113 )]))
3114 };
3115 let evaluate_terms = |terms: &[Atom],
3120 unwrap_denominator: bool,
3121 spatial: &Atom,
3122 energy: &Atom,
3123 constant: &Atom|
3124 -> Result<Complex<F<ArbPrec>>> {
3125 let zero = F(ArbPrec::default());
3126 terms
3127 .iter()
3128 .try_fold(Complex::new(zero.clone(), zero), |sum, expression| {
3129 let expression = if unwrap_denominator {
3130 expression
3131 .replace(GS.den(W_.a_, W_.b_, W_.c_, W_.d_))
3132 .with(W_.d_)
3133 } else {
3134 expression.clone()
3135 };
3136 Ok(sum + evaluate_arb(fixed_point(expression, spatial, energy, constant))?)
3137 })
3138 };
3139 let tolerance = F(ArbPrec::default().epsilon()).sqrt().sqrt().sqrt();
3140 let points = [
3141 (Atom::Zero, Atom::one(), Atom::num(2)),
3142 (
3143 Atom::num(symbolica::domains::rational::Rational::from((3, 4))),
3144 Atom::num(symbolica::domains::rational::Rational::from((5, 4))),
3145 Atom::num(symbolica::domains::rational::Rational::from((7, 3))),
3146 ),
3147 ];
3148 for (spatial, energy, constant) in points {
3149 let powered_value = evaluate_terms(&powered_terms, true, &spatial, &energy, &constant)?;
3150 let reversed_value =
3151 evaluate_terms(&reversed_terms, true, &spatial, &energy, &constant)?;
3152 let lower_value = evaluate_terms(&lower_terms, false, &spatial, &energy, &constant)?;
3153 let powered_scalar_value =
3154 evaluate_terms(&powered_scalar_terms, true, &spatial, &energy, &constant)?;
3155 let lower_scalar_value =
3156 evaluate_terms(&lower_scalar_terms, false, &spatial, &energy, &constant)?;
3157 for (label, candidate, reference) in [
3158 (
3159 "uncancelled D*(Q0+c)/D^3",
3160 powered_value.clone(),
3161 lower_value,
3162 ),
3163 (
3164 "uncancelled scalar D/D^3",
3165 powered_scalar_value,
3166 lower_scalar_value,
3167 ),
3168 ] {
3169 let distance = (candidate.clone() - reference.clone()).norm().re;
3170 let candidate_norm = candidate.clone().norm().re;
3171 let reference_norm = reference.clone().norm().re;
3172 let scale = if candidate_norm > reference_norm {
3173 candidate_norm
3174 } else {
3175 reference_norm
3176 };
3177 let relative_distance = if scale.is_zero() {
3178 distance
3179 } else {
3180 distance / scale
3181 };
3182 assert!(
3183 relative_distance <= tolerance,
3184 "{label} differs from its independently generated lower source at spatial={spatial}, c={constant}: candidate={candidate:e}, lower={reference:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
3185 );
3186 }
3187 let distance = (powered_value.clone() - reversed_value.clone()).norm().re;
3188 let powered_norm = powered_value.clone().norm().re;
3189 let reversed_norm = reversed_value.clone().norm().re;
3190 let scale = if powered_norm > reversed_norm {
3191 powered_norm
3192 } else {
3193 reversed_norm
3194 };
3195 let relative_distance = if scale.is_zero() {
3196 distance
3197 } else {
3198 distance / scale
3199 };
3200 assert!(
3201 relative_distance <= tolerance,
3202 "globally reversed D(-Q) component changes odd physical Q0+c numerator at spatial={spatial}, c={constant}: powered={powered_value:e}, reversed={reversed_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
3203 );
3204 let ordinary_value =
3205 evaluate_terms(&ordinary_terms, false, &spatial, &energy, &constant)?;
3206 let lower_value = evaluate_terms(&lower_terms, false, &spatial, &energy, &constant)?;
3207 let normalization_distance = (lower_value.clone() - ordinary_value.clone()).norm().re;
3208 let lower_norm = lower_value.clone().norm().re;
3209 let ordinary_norm = ordinary_value.clone().norm().re;
3210 let normalization_scale = if lower_norm > ordinary_norm {
3211 lower_norm
3212 } else {
3213 ordinary_norm
3214 };
3215 let normalization_relative_distance = if normalization_scale.is_zero() {
3216 normalization_distance
3217 } else {
3218 normalization_distance / normalization_scale
3219 };
3220 assert!(
3221 normalization_relative_distance <= tolerance,
3222 "exact and ordinary (Q0+c)/(D7*D8) CFF sums differ at spatial={spatial}, c={constant}: exact={lower_value:e}, ordinary={ordinary_value:e}, relative delta={normalization_relative_distance:e}, tolerance={tolerance:e}"
3223 );
3224
3225 let quintic_value = evaluate_terms(&quintic_terms, true, &spatial, &energy, &constant)?;
3226 let reversed_quintic_value =
3227 evaluate_terms(&reversed_quintic_terms, true, &spatial, &energy, &constant)?;
3228 let quartic_value =
3229 evaluate_terms(&quartic_terms, false, &spatial, &energy, &constant)?;
3230 for (label, candidate) in [
3231 ("uncancelled", quintic_value),
3232 ("globally reversed", reversed_quintic_value),
3233 ] {
3234 let distance = (candidate.clone() - quartic_value.clone()).norm().re;
3235 let candidate_norm = candidate.clone().norm().re;
3236 let quartic_norm = quartic_value.clone().norm().re;
3237 let scale = if candidate_norm > quartic_norm {
3238 candidate_norm
3239 } else {
3240 quartic_norm
3241 };
3242 let relative_distance = if scale.is_zero() {
3243 distance
3244 } else {
3245 distance / scale
3246 };
3247 assert!(
3248 relative_distance <= tolerance,
3249 "{label} D*(Q0+c)^4/D^5 differs from (Q0+c)^4/D^4 at spatial={spatial}, c={constant}: candidate={candidate:e}, lower={quartic_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
3250 );
3251 }
3252 }
3253 Ok(())
3254 }
3255
3256 #[test]
3257 fn exact_cff_uncancelled_powered_denominator_matches_lower_lu_residues() -> Result<()> {
3258 test_initialise()?;
3259 let mut graph: Graph = dot!(digraph exact_powered_lu_identity {
3260 edge [num=1 mass=1]
3261 node [num=1]
3262 incoming [style=invis]
3263 outgoing [style=invis]
3264
3265 incoming -> v1 [id=0]
3266 v1 -> v2 [id=1 lmb_id=0]
3267 v2 -> v3 [id=2]
3268 v1 -> v3 [id=3]
3269 v3 -> outgoing [id=4]
3270 })?;
3271 let edge = EdgeIndex(1);
3272 let momentum = FunctionBuilder::new(GS.emr_mom)
3273 .add_arg(usize::from(edge))
3274 .finish();
3275 let mass_squared = graph.underlying[edge].particle.mass_atom().pow(2);
3276 let full_expr = GS.emr_mom(edge, GS.cind(0)).pow(2)
3277 - (1..=3).fold(mass_squared.clone(), |norm_squared, spatial_index| {
3278 norm_squared + GS.emr_mom(edge, GS.cind(spatial_index)).pow(2)
3279 });
3280 let denominator = FourDDenominator {
3281 source_edge: edge,
3282 momentum: momentum.clone(),
3283 mass_squared: mass_squared.clone(),
3284 full_expr: full_expr.clone(),
3285 };
3286 let spectator_edge = EdgeIndex(3);
3287 let spectator_mass_squared = graph.underlying[spectator_edge].particle.mass_atom().pow(2);
3288 let spectator = FourDDenominator {
3289 source_edge: spectator_edge,
3290 momentum: FunctionBuilder::new(GS.emr_mom)
3291 .add_arg(usize::from(spectator_edge))
3292 .finish(),
3293 mass_squared: spectator_mass_squared.clone(),
3294 full_expr: GS.emr_mom(spectator_edge, GS.cind(0)).pow(2)
3295 - (1..=3).fold(spectator_mass_squared, |norm_squared, spatial_index| {
3296 norm_squared + GS.emr_mom(spectator_edge, GS.cind(spatial_index)).pow(2)
3297 }),
3298 };
3299 let denominators = [spectator, denominator.clone(), denominator];
3300 let retained_constant = Atom::var(symbolica::symbol!("exact_cff_test::lu_retained_factor"));
3301 let retained_factor = GS.emr_mom(edge, GS.cind(0)) + &retained_constant;
3302 let numerator =
3303 GS.den(usize::from(edge), momentum, mass_squared, full_expr) * &retained_factor;
3304 let options = graph.denominator_only_cff_3d_expression_options();
3305 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
3306 let production = graph.generate_3d_expression_for_integrand(
3307 &[EdgeIndex(2)],
3308 &canonization,
3309 &options,
3310 Some(&retained_factor),
3311 )?;
3312 let mut lu_cut = graph
3313 .determine_raised_esurfaces_from_expression(&production.expression)
3314 .raised_groups
3315 .into_iter()
3316 .find(|group| {
3317 group.esurface_ids.iter().any(|esurface_id| {
3318 !production.expression.surfaces.esurface_cache[*esurface_id]
3319 .external_shift
3320 .is_empty()
3321 })
3322 })
3323 .expect("the lower production CFF contains an ordinary physical LU surface");
3324 lu_cut.max_occurence = 2;
3325 let mut cutset = CutSet::empty(graph.n_hedges());
3326 cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
3327 raised_group: lu_cut.clone(),
3328 cut_edge_alternatives: lu_cut
3329 .esurface_ids
3330 .iter()
3331 .map(|esurface_id| {
3332 production.expression.surfaces.esurface_cache[*esurface_id]
3333 .energies
3334 .clone()
3335 })
3336 .collect(),
3337 });
3338
3339 let (powered, _) =
3340 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &numerator)?;
3341 let (lower, _) = graph.cff_from_4d_denominators(
3342 &denominators[..2],
3343 &cutset,
3344 &options,
3345 &retained_factor,
3346 )?;
3347 let mut contract: SuBitGraph = graph.empty_subgraph();
3348 contract.add(graph[&EdgeIndex(2)].1);
3349 let ordinary = graph.cff(
3350 &contract,
3351 &cutset,
3352 &OrientationPattern::default(),
3353 &options,
3354 Some(&retained_factor),
3355 )?;
3356 assert_eq!(
3357 powered.terms.keys().collect::<Vec<_>>(),
3358 lower.terms.keys().collect::<Vec<_>>(),
3359 "powered and lower exact sources must expose the same LU residues"
3360 );
3361 assert_eq!(
3362 lower.terms.keys().collect::<Vec<_>>(),
3363 ordinary.terms.keys().collect::<Vec<_>>(),
3364 "exact and ordinary sources must expose the same LU residues"
3365 );
3366
3367 let points = [
3368 (
3369 Atom::Zero,
3370 Atom::one(),
3371 Atom::num(3) / 4,
3372 Atom::num(5) / 4,
3373 Atom::num(7),
3374 Atom::num(2),
3375 ),
3376 (
3377 Atom::num(3) / 4,
3378 Atom::num(5) / 4,
3379 Atom::Zero,
3380 Atom::one(),
3381 Atom::num(11),
3382 Atom::num(7) / 3,
3383 ),
3384 ];
3385 for index in lower.terms.keys() {
3386 let powered_term = powered.terms.get(index).expect("powered residue exists");
3387 let powered_sum = powered_term
3388 .orientations
3389 .iter()
3390 .map(|orientation| {
3391 Ok(orientation.expression.clone()
3392 * powered_term
3393 .map_exact_source_numerator(&orientation.orientation, None)?)
3394 })
3395 .collect::<Result<Vec<_>>>()?
3396 .into_iter()
3397 .fold(Atom::Zero, |sum, term| sum + term)
3398 * Atom::num(powered.production_prefactor_factor());
3399 let powered_sum = powered_sum
3400 .replace(GS.den(W_.a_, W_.b_, W_.c_, W_.d_))
3401 .with(W_.d_);
3402 let lower_term = lower.terms.get(index).expect("lower residue exists");
3403 let lower_sum = lower_term
3404 .orientations
3405 .iter()
3406 .map(|orientation| {
3407 Ok(orientation.expression.clone()
3408 * lower_term.map_exact_source_numerator(&orientation.orientation, None)?)
3409 })
3410 .collect::<Result<Vec<_>>>()?
3411 .into_iter()
3412 .fold(Atom::Zero, |sum, term| sum + term)
3413 * Atom::num(lower.production_prefactor_factor());
3414 let ordinary_sum = ordinary
3415 .terms
3416 .get(index)
3417 .expect("ordinary residue exists")
3418 .orientations
3419 .iter()
3420 .map(|orientation| {
3421 orientation.expression.clone()
3422 * retained_factor.replace_multiple(
3423 orientation.orientation.energy_replacements_gs(&graph),
3424 )
3425 })
3426 .fold(Atom::Zero, |sum, term| sum + term)
3427 * Atom::num(ordinary.production_prefactor_factor());
3428
3429 for (q, eq, q3, e3, external_energy, constant) in &points {
3430 let fixed_point = |mut expression: Atom| {
3431 for (source_edge, spatial, energy) in [(edge, q, eq), (spectator_edge, q3, e3)]
3432 {
3433 expression = expression
3434 .replace(GS.emr_mom(source_edge, GS.cind(1)))
3435 .with(spatial.clone())
3436 .replace(GS.ose(source_edge))
3437 .with(energy.clone());
3438 for spatial_index in 2..=3 {
3439 expression = expression
3440 .replace(GS.emr_mom(source_edge, GS.cind(spatial_index)))
3441 .with(Atom::Zero);
3442 }
3443 }
3444 for external_edge in [EdgeIndex(0), EdgeIndex(4)] {
3445 expression = expression
3446 .replace(GS.emr_mom(external_edge, GS.cind(0)))
3447 .with(external_energy.clone());
3448 }
3449 expression
3450 .replace(retained_constant.clone())
3451 .with(constant.clone())
3452 };
3453 let powered_value = fixed_point(powered_sum.clone());
3454 let lower_value = fixed_point(lower_sum.clone());
3455 let ordinary_value = fixed_point(ordinary_sum.clone());
3456 let powered_difference = (powered_value - &lower_value).together();
3457 assert!(
3458 powered_difference.is_zero(),
3459 "uncancelled powered and lower exact LU residues differ for index {index}: {powered_difference}"
3460 );
3461 let ordinary_difference = (ordinary_value - &lower_value).together();
3462 assert!(
3463 ordinary_difference.is_zero(),
3464 "exact lower and ordinary LU residues differ for index {index}: {ordinary_difference}"
3465 );
3466 if index.lu_cut_order == Some(2) {
3467 assert!(
3468 lower_value.together().is_zero(),
3469 "the artificially raised second-order LU residue must vanish for the lower source"
3470 );
3471 }
3472 }
3473 }
3474 Ok(())
3475 }
3476
3477 #[test]
3478 fn exact_cff_alias_edges_share_physical_numerator_energy() -> Result<()> {
3479 test_initialise()?;
3480 let mut graph: Graph = dot!(digraph exact_alias_energy_identity {
3481 edge [num=1 mass=1]
3482 node [num=1]
3483 incoming [style=invis]
3484 outgoing [style=invis]
3485
3486 incoming -> v1 [id=0]
3487 v1 -> v2 [id=1 lmb_id=0]
3488 v2 -> v3 [id=2]
3489 v1 -> v3 [id=3]
3490 v3 -> outgoing [id=4]
3491 })?;
3492 let alias_edges = [EdgeIndex(1), EdgeIndex(2)];
3493 let spectator_edge = EdgeIndex(3);
3494 let denominators = [spectator_edge, alias_edges[0], alias_edges[1]].map(|edge| {
3495 let momentum = FunctionBuilder::new(GS.emr_mom)
3496 .add_arg(usize::from(edge))
3497 .finish();
3498 let mass_squared = graph.underlying[edge].particle.mass_atom().pow(2);
3499 FourDDenominator {
3500 source_edge: edge,
3501 momentum,
3502 mass_squared: mass_squared.clone(),
3503 full_expr: GS.emr_mom(edge, GS.cind(0)).pow(2)
3504 - (1..=3).fold(mass_squared, |norm_squared, spatial_index| {
3505 norm_squared + GS.emr_mom(edge, GS.cind(spatial_index)).pow(2)
3506 }),
3507 }
3508 });
3509 let retained_constant =
3510 Atom::var(symbolica::symbol!("exact_cff_test::alias_retained_factor"));
3511 let numerator = (GS.emr_mom(alias_edges[0], GS.cind(0)) + &retained_constant)
3512 * (GS.emr_mom(alias_edges[1], GS.cind(0)) + &retained_constant + 1);
3513 let options = graph.denominator_only_cff_3d_expression_options();
3514 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
3515 let production = graph.generate_3d_expression_for_integrand(
3516 &[],
3517 &canonization,
3518 &options,
3519 Some(&numerator),
3520 )?;
3521 let mut lu_cut = graph
3522 .determine_raised_esurfaces_from_expression(&production.expression)
3523 .raised_groups
3524 .into_iter()
3525 .find(|group| {
3526 group.esurface_ids.iter().any(|esurface_id| {
3527 !production.expression.surfaces.esurface_cache[*esurface_id]
3528 .external_shift
3529 .is_empty()
3530 })
3531 })
3532 .expect("the production CFF contains a physical LU surface");
3533 lu_cut.max_occurence = 2;
3534 let mut cutset = CutSet::empty(graph.n_hedges());
3535 cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
3536 raised_group: lu_cut.clone(),
3537 cut_edge_alternatives: lu_cut
3538 .esurface_ids
3539 .iter()
3540 .map(|esurface_id| {
3541 production.expression.surfaces.esurface_cache[*esurface_id]
3542 .energies
3543 .clone()
3544 })
3545 .collect(),
3546 });
3547
3548 let (exact, _) =
3549 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &numerator)?;
3550 let contract: SuBitGraph = graph.empty_subgraph();
3551 let ordinary = graph.cff(
3552 &contract,
3553 &cutset,
3554 &OrientationPattern::default(),
3555 &options,
3556 Some(&numerator),
3557 )?;
3558 assert_eq!(
3559 exact.terms.keys().collect::<Vec<_>>(),
3560 ordinary.terms.keys().collect::<Vec<_>>(),
3561 "exact and ordinary alias sources must expose the same LU residues"
3562 );
3563
3564 let points = [
3565 (
3566 Atom::Zero,
3567 Atom::one(),
3568 Atom::num(3) / 4,
3569 Atom::num(5) / 4,
3570 Atom::num(7),
3571 Atom::num(2),
3572 ),
3573 (
3574 Atom::num(3) / 4,
3575 Atom::num(5) / 4,
3576 Atom::Zero,
3577 Atom::one(),
3578 Atom::num(11),
3579 Atom::num(7) / 3,
3580 ),
3581 ];
3582 for index in ordinary.terms.keys() {
3583 let exact_term = exact.terms.get(index).expect("exact residue exists");
3584 let exact_sum = exact_term
3585 .orientations
3586 .iter()
3587 .map(|orientation| {
3588 Ok(orientation.expression.clone()
3589 * exact_term.map_exact_source_numerator(&orientation.orientation, None)?)
3590 })
3591 .collect::<Result<Vec<_>>>()?
3592 .into_iter()
3593 .fold(Atom::Zero, |sum, term| sum + term)
3594 * Atom::num(exact.production_prefactor_factor());
3595 let ordinary_sum = ordinary
3596 .terms
3597 .get(index)
3598 .expect("ordinary residue exists")
3599 .orientations
3600 .iter()
3601 .map(|orientation| {
3602 orientation.expression.clone()
3603 * numerator.replace_multiple(
3604 orientation.orientation.energy_replacements_gs(&graph),
3605 )
3606 })
3607 .fold(Atom::Zero, |sum, term| sum + term)
3608 * Atom::num(ordinary.production_prefactor_factor());
3609
3610 for (alias_spatial, alias_energy, spectator_spatial, spectator_energy, external, c) in
3611 &points
3612 {
3613 let fixed_point = |mut expression: Atom| {
3614 for edge in alias_edges {
3615 expression = expression
3616 .replace(GS.emr_mom(edge, GS.cind(1)))
3617 .with(alias_spatial.clone())
3618 .replace(GS.ose(edge))
3619 .with(alias_energy.clone());
3620 }
3621 expression = expression
3622 .replace(GS.emr_mom(spectator_edge, GS.cind(1)))
3623 .with(spectator_spatial.clone())
3624 .replace(GS.ose(spectator_edge))
3625 .with(spectator_energy.clone());
3626 for edge in [alias_edges[0], alias_edges[1], spectator_edge] {
3627 for spatial_index in 2..=3 {
3628 expression = expression
3629 .replace(GS.emr_mom(edge, GS.cind(spatial_index)))
3630 .with(Atom::Zero);
3631 }
3632 }
3633 for external_edge in [EdgeIndex(0), EdgeIndex(4)] {
3634 expression = expression
3635 .replace(GS.emr_mom(external_edge, GS.cind(0)))
3636 .with(external.clone());
3637 }
3638 expression
3639 .replace(retained_constant.clone())
3640 .with(c.clone())
3641 };
3642 let difference =
3643 (fixed_point(exact_sum.clone()) - fixed_point(ordinary_sum.clone())).together();
3644 assert!(
3645 difference.is_zero(),
3646 "exact alias-edge and ordinary LU residues differ for index {index}: {difference}"
3647 );
3648 }
3649 }
3650 Ok(())
3651 }
3652
3653 #[test]
3654 fn exact_cff_cubic_uv_rewrite_matches_production_convention() -> Result<()> {
3655 test_initialise()?;
3656 let mut graph: Graph = dot!(digraph exact_cubic_uv_rewrite {
3657 edge [num=1 mass=1]
3658 node [num=1]
3659 incoming [style=invis]
3660 outgoing [style=invis]
3661
3662 incoming -> v5 [id=0]
3663 v0 -> v2 [id=1 lmb_id=0]
3664 v3 -> v0 [id=2]
3665 v0 -> v5 [id=3]
3666 v2 -> v1 [id=4]
3667 v1 -> v3 [id=5 lmb_id=1]
3668 v1 -> v4 [id=6]
3669 v2 -> v3 [id=7]
3670 v4 -> outgoing [id=8]
3671 })?;
3672 let owner_edges = [EdgeIndex(4), EdgeIndex(5), EdgeIndex(7)];
3673 assert_eq!(
3674 owner_edges
3675 .iter()
3676 .map(|edge| &graph.loop_momentum_basis.edge_signatures[*edge])
3677 .collect::<BTreeSet<_>>()
3678 .len(),
3679 owner_edges.len(),
3680 "the exact UV owners must have genuinely different physical routings"
3681 );
3682
3683 let carrier = EdgeIndex(5);
3684 let momentum = FunctionBuilder::new(GS.emr_mom)
3685 .add_arg(usize::from(carrier))
3686 .finish();
3687 let mass_squared = Atom::var(GS.m_uv_expansion).pow(2);
3688 let full_expr = GS.emr_mom(carrier, GS.cind(0)).pow(2)
3689 - (1..=3).fold(mass_squared.clone(), |norm_squared, spatial_index| {
3690 norm_squared + GS.emr_mom(carrier, GS.cind(spatial_index)).pow(2)
3691 });
3692 let denominator_records = owner_edges
3693 .into_iter()
3694 .zip([1, 1, -1])
3695 .map(|(source_edge, routing_sign)| FourDDenominator {
3696 source_edge,
3697 momentum: Atom::num(routing_sign) * &momentum,
3698 mass_squared: mass_squared.clone(),
3699 full_expr: full_expr.clone(),
3700 })
3701 .collect::<Vec<_>>();
3702 let mut reference_orientation_sum: Option<Atom> = None;
3703 let mut reference_scaled_quadratic_sum: Option<Atom> = None;
3704 for denominators in denominator_records.iter().cloned().permutations(3) {
3705 let source = GraphThreeDSource::from_exact_denominators_in_uv_edges(
3706 &graph,
3707 &denominators,
3708 owner_edges,
3709 )?;
3710 let parsed = source.to_three_d_parsed_graph()?;
3711 let validation = three_dimensional_reps::validate_parsed_graph(&parsed);
3712 assert!(
3713 validation.ok,
3714 "the cubic UV source must remain momentum-balanced: {validation:?}"
3715 );
3716 let nodes = parsed
3717 .internal_edges
3718 .iter()
3719 .flat_map(|edge| [edge.tail, edge.head])
3720 .collect::<BTreeSet<_>>();
3721 assert_eq!(
3722 parsed.internal_edges.len() + 1 - nodes.len(),
3723 1,
3724 "the cubic UV source must retain one topological loop"
3725 );
3726 assert_eq!(parsed.loop_names.len(), 1);
3727 assert!(
3728 parsed.external_edges.is_empty(),
3729 "zero-denominator cograph components must not create causal surfaces"
3730 );
3731
3732 let options = graph.denominator_only_cff_3d_expression_options();
3733 let cutset = CutSet::empty(graph.n_hedges());
3734 let (cff, _) = graph.cff_from_4d_denominators_in_uv_edges(
3735 &denominators,
3736 owner_edges,
3737 &cutset,
3738 &options,
3739 &Atom::one(),
3740 None,
3741 )?;
3742 assert_eq!(cff.terms.len(), 1);
3743 let term = cff
3744 .terms
3745 .values()
3746 .next()
3747 .expect("the empty cutset has one exact CFF term");
3748 let orientation_sum = term
3749 .orientations
3750 .iter()
3751 .fold(Atom::Zero, |sum, orientation| sum + &orientation.expression)
3752 * Atom::num(cff.production_prefactor_factor());
3753 let energy = (1..=3)
3754 .fold(mass_squared.clone(), |norm_squared, spatial_index| {
3755 norm_squared + GS.emr_mom(carrier, GS.cind(spatial_index)).pow(2)
3756 })
3757 .sqrt();
3758 let production_contour = -Atom::num(3) * Atom::i()
3765 / (Atom::num(128) * Atom::var(GS.pi).pow(3) * energy.pow(5));
3766 let difference = (&orientation_sum - &production_contour).together();
3767 assert!(
3768 difference.is_zero(),
3769 "the cubic exact UV CFF differs from the production convention: {difference}"
3770 );
3771 if let Some(reference) = &reference_orientation_sum {
3772 let provenance_difference = (&orientation_sum - reference).together();
3773 assert!(
3774 provenance_difference.is_zero(),
3775 "the bare cubic exact UV CFF depends on denominator-factor order: {provenance_difference}"
3776 );
3777 } else {
3778 reference_orientation_sum = Some(orientation_sum.clone());
3779 }
3780
3781 let common_energy_denominator = energy.pow(5);
3785 let scaled_orientation_sum = (&orientation_sum * &common_energy_denominator).together();
3786 let quadratic_numerator = GS.emr_mom(carrier, GS.cind(0)).pow(2);
3787 let (quadratic_cff, _) = graph.cff_from_4d_denominators_in_uv_edges(
3788 &denominators,
3789 owner_edges,
3790 &cutset,
3791 &options,
3792 &quadratic_numerator,
3793 None,
3794 )?;
3795 let scaled_quadratic_sum = quadratic_cff
3796 .terms
3797 .values()
3798 .flat_map(|term| {
3799 term.orientations.iter().map(|orientation| {
3800 let scaled_kernel =
3801 (&orientation.expression * &common_energy_denominator).together();
3802 Ok(scaled_kernel
3803 * term.map_exact_source_numerator(&orientation.orientation, None)?)
3804 })
3805 })
3806 .collect::<Result<Vec<_>>>()?
3807 .into_iter()
3808 .fold(Atom::Zero, |sum, term| sum + term)
3809 * Atom::num(quadratic_cff.production_prefactor_factor());
3810 let fixed_hard_energy = |owner: EdgeIndex| {
3811 FunctionBuilder::new(GS.emr_mom)
3812 .add_arg(GS.uv_momentum_provenance_tag(
3813 Atom::num(usize::from(owner) as i64).as_view(),
3814 false,
3815 momentum.as_view(),
3816 ))
3817 .add_arg(GS.cind(0))
3818 .finish()
3819 };
3820 let tagged_quadratic_numerator =
3821 fixed_hard_energy(owner_edges[0]) * fixed_hard_energy(owner_edges[1]);
3822 let (tagged_quadratic_cff, _) = graph.cff_from_4d_denominators_in_uv_edges(
3823 &denominators,
3824 owner_edges,
3825 &cutset,
3826 &options,
3827 &tagged_quadratic_numerator,
3828 None,
3829 )?;
3830 let scaled_tagged_quadratic_sum = tagged_quadratic_cff
3831 .terms
3832 .values()
3833 .flat_map(|term| {
3834 term.orientations.iter().map(|orientation| {
3835 let scaled_kernel =
3836 (&orientation.expression * &common_energy_denominator).together();
3837 Ok(scaled_kernel
3838 * term.map_exact_source_numerator(&orientation.orientation, None)?)
3839 })
3840 })
3841 .collect::<Result<Vec<_>>>()?
3842 .into_iter()
3843 .fold(Atom::Zero, |sum, term| sum + term)
3844 * Atom::num(tagged_quadratic_cff.production_prefactor_factor());
3845 let tagged_difference = (scaled_tagged_quadratic_sum.collect_factors()
3848 - scaled_quadratic_sum.collect_factors())
3849 .collect_factors();
3850 assert!(
3851 tagged_difference.is_zero(),
3852 "two fixed provenance factors carrying the same hard Q must reproduce Q0^2: {tagged_difference}",
3853 );
3854 if let Some(reference) = &reference_scaled_quadratic_sum {
3859 let provenance_difference = (scaled_quadratic_sum.collect_factors()
3860 - reference.collect_factors())
3861 .collect_factors();
3862 assert!(
3863 provenance_difference.is_zero(),
3864 "the q0^2 cubic exact UV CFF depends on denominator-factor order: {provenance_difference}"
3865 );
3866 } else {
3867 reference_scaled_quadratic_sum = Some(scaled_quadratic_sum.clone());
3868 }
3869
3870 let spatial_norm = (1..=3).fold(Atom::Zero, |norm_squared, spatial_index| {
3871 norm_squared + GS.emr_mom(carrier, GS.cind(spatial_index)).pow(2)
3872 });
3873 let momentum_squared_numerator = &quadratic_numerator - &spatial_norm;
3874 let (momentum_squared_cff, _) = graph.cff_from_4d_denominators_in_uv_edges(
3875 &denominators,
3876 owner_edges,
3877 &cutset,
3878 &options,
3879 &momentum_squared_numerator,
3880 None,
3881 )?;
3882 let scaled_momentum_squared_sum = momentum_squared_cff
3883 .terms
3884 .values()
3885 .flat_map(|term| {
3886 term.orientations.iter().map(|orientation| {
3887 let scaled_kernel =
3888 (&orientation.expression * &common_energy_denominator).together();
3889 Ok(scaled_kernel
3890 * term.map_exact_source_numerator(&orientation.orientation, None)?)
3891 })
3892 })
3893 .collect::<Result<Vec<_>>>()?
3894 .into_iter()
3895 .fold(Atom::Zero, |sum, term| sum + term)
3896 * Atom::num(momentum_squared_cff.production_prefactor_factor());
3897 let momentum_squared_reference =
3898 &scaled_quadratic_sum - &spatial_norm * &scaled_orientation_sum;
3899 let momentum_squared_difference =
3903 (scaled_momentum_squared_sum.expand_num().collect_factors()
3904 - momentum_squared_reference.expand_num().collect_factors())
3905 .collect_factors()
3906 .expand_num();
3907 assert!(
3908 momentum_squared_difference.is_zero(),
3909 "the full momentum-square numerator must preserve CFF linearity between its temporal and spatial factors: {momentum_squared_difference}"
3910 );
3911 }
3912 Ok(())
3913 }
3914
3915 #[test]
3916 fn exact_theta_equal_channel_temporal_numerators_are_owner_invariant() -> Result<()> {
3917 test_initialise()?;
3918 let mut graph: Graph = dot!(digraph exact_theta_equal_channel_numerator {
3919 edge [num=1 mass=1]
3920 node [num=1]
3921
3922 a -> b [id=0 lmb_id=0]
3923 a -> c [id=1 lmb_id=1]
3924 a -> d [id=2]
3925 b -> c [id=3]
3926 b -> d [id=4]
3927 })?;
3928 let q0 = FunctionBuilder::new(GS.emr_mom).add_arg(0).finish();
3929 let q1 = FunctionBuilder::new(GS.emr_mom).add_arg(1).finish();
3930 let momenta = [q0.clone(), q1.clone(), -&q0 - &q1, -&q1, &q0 + &q1];
3931 let denominators = momenta
3932 .into_iter()
3933 .enumerate()
3934 .map(|(owner, momentum)| FourDDenominator {
3935 source_edge: EdgeIndex(owner),
3936 momentum,
3937 mass_squared: Atom::one(),
3938 full_expr: Atom::one(),
3939 })
3940 .collect::<Vec<_>>();
3941 let tagged_temporal = |owner: usize, hard: &Atom| {
3942 FunctionBuilder::new(GS.emr_mom)
3943 .add_arg(GS.uv_momentum_provenance_tag(
3944 Atom::num(owner as i64).as_view(),
3945 false,
3946 hard.as_view(),
3947 ))
3948 .add_arg(GS.cind(0))
3949 .finish()
3950 };
3951 let options = graph.denominator_only_cff_3d_expression_options();
3952 let cutset = CutSet::empty(graph.n_hedges());
3953 let exact_cff = |graph: &mut Graph, numerator: &Atom| -> Result<CutCFF> {
3954 Ok(graph
3955 .cff_from_4d_denominators(&denominators, &cutset, &options, numerator)?
3956 .0)
3957 };
3958 let exact_sum = |cff: &CutCFF| -> Result<Atom> {
3959 Ok(cff
3960 .terms
3961 .values()
3962 .flat_map(|term| {
3963 term.orientations.iter().map(|orientation| {
3964 Ok(orientation.expression.clone()
3965 * term.map_exact_source_numerator(&orientation.orientation, None)?)
3966 })
3967 })
3968 .collect::<Result<Vec<_>>>()?
3969 .into_iter()
3970 .fold(Atom::Zero, |sum, term| sum + term)
3971 * Atom::num(cff.production_prefactor_factor()))
3972 };
3973 let exact_unit_cff = exact_cff(&mut graph, &Atom::one())?;
3974 let exact_unit = exact_sum(&exact_unit_cff)?;
3975 let q1_squared = tagged_temporal(1, &q1).pow(2);
3976 let q3_squared = tagged_temporal(3, &(-q1)).pow(2);
3977 let q1_cff = exact_cff(&mut graph, &q1_squared)?;
3978 let q3_cff = exact_cff(&mut graph, &q3_squared)?;
3979 for (cff, owner, numerator) in [
3982 (&q1_cff, EdgeIndex(1), &q1_squared),
3983 (&q3_cff, EdgeIndex(3), &q3_squared),
3984 ] {
3985 assert_eq!(
3986 cff.energy_degree_bound_report.physical_parent_bounds,
3987 vec![(usize::from(owner), 2)],
3988 );
3989 for term in cff.terms.values() {
3990 let planned = term.exact_source_numerator.as_ref().unwrap();
3991 for orientation in &term.orientations {
3992 assert_eq!(
3993 term.map_exact_source_numerator(&orientation.orientation, None)?,
3994 planned.mapper.map_numerator(
3995 &orientation.orientation.loop_energy_map,
3996 &orientation.orientation.edge_energy_map,
3997 numerator,
3998 )?,
3999 "the original quadratic factor must use its own base sample",
4000 );
4001 }
4002 }
4003 }
4004 let empty: linnet::half_edge::subgraph::SuBitGraph = graph.empty_subgraph();
4005 let ordinary = graph.cff(
4006 &empty,
4007 &cutset,
4008 &OrientationPattern::default(),
4009 &options,
4010 Some(&Atom::one()),
4011 )?;
4012 let ordinary_unit = ordinary
4013 .terms
4014 .values()
4015 .flat_map(|term| &term.orientations)
4016 .fold(Atom::Zero, |sum, orientation| sum + &orientation.expression)
4017 * Atom::num(ordinary.production_prefactor_factor());
4018 let mut fixed_unit_difference = &exact_unit - &ordinary_unit;
4019 for (edge, x, energy) in [
4020 (
4021 EdgeIndex(0),
4022 Atom::num(2) / Atom::num(3),
4023 (Atom::num(13) / Atom::num(9)).sqrt(),
4024 ),
4025 (
4026 EdgeIndex(1),
4027 Atom::num(3) / Atom::num(4),
4028 Atom::num(5) / Atom::num(4),
4029 ),
4030 ] {
4031 fixed_unit_difference = fixed_unit_difference
4032 .replace(GS.emr_mom(edge, GS.cind(1)))
4033 .with(x)
4034 .replace(GS.emr_mom(edge, GS.cind(2)))
4035 .with(Atom::Zero)
4036 .replace(GS.emr_mom(edge, GS.cind(3)))
4037 .with(Atom::Zero)
4038 .replace(GS.ose(edge))
4039 .with(energy);
4040 }
4041 let combined_energy = (Atom::num(433) / Atom::num(144)).sqrt();
4042 fixed_unit_difference = fixed_unit_difference
4043 .replace(GS.ose(EdgeIndex(2)))
4044 .with(combined_energy.clone())
4045 .replace(GS.ose(EdgeIndex(3)))
4046 .with(Atom::num(5) / Atom::num(4))
4047 .replace(GS.ose(EdgeIndex(4)))
4048 .with(combined_energy);
4049 assert!(
4050 fixed_unit_difference.together().is_zero(),
4051 "the reconstructed and ordinary unit-numerator theta CFFs must agree: {fixed_unit_difference}",
4052 );
4053 for [q0x, q1x] in [
4054 [Atom::num(2) / Atom::num(3), Atom::num(3) / Atom::num(4)],
4055 [Atom::num(4) / Atom::num(5), Atom::num(5) / Atom::num(6)],
4056 ] {
4057 let q0_energy = (Atom::one() + q0x.clone().pow(2)).sqrt();
4058 let q1_energy = (Atom::one() + q1x.clone().pow(2)).sqrt();
4059 let fix = |mut expression: Atom| {
4060 for (edge, x, energy) in [
4061 (EdgeIndex(0), q0x.clone(), q0_energy.clone()),
4062 (EdgeIndex(1), q1x.clone(), q1_energy.clone()),
4063 ] {
4064 expression = expression
4065 .replace(GS.emr_mom(edge, GS.cind(1)))
4066 .with(x)
4067 .replace(GS.emr_mom(edge, GS.cind(2)))
4068 .with(Atom::Zero)
4069 .replace(GS.emr_mom(edge, GS.cind(3)))
4070 .with(Atom::Zero)
4071 .replace(GS.ose(edge))
4072 .with(energy);
4073 }
4074 expression
4075 };
4076 let mut sums = Vec::new();
4077 for cff in [&q1_cff, &q3_cff] {
4078 let mut sum = Atom::Zero;
4079 for term in cff.terms.values() {
4080 for orientation in &term.orientations {
4081 let carrier = fix(orientation.expression.clone()).together();
4082 let numerator =
4083 fix(term.map_exact_source_numerator(&orientation.orientation, None)?)
4084 .together();
4085 sum += carrier * numerator;
4090 }
4091 }
4092 sums.push(sum * Atom::num(cff.production_prefactor_factor()));
4093 }
4094 assert!(
4095 !sums[0].together().is_zero(),
4096 "the temporal theta contour must exercise a nonzero numerator contribution",
4097 );
4098 let difference = (&sums[0] - &sums[1]).together();
4099 assert!(
4100 difference.is_zero(),
4101 "equal physical temporal numerators must agree after their complete independent residue sums: {difference}",
4102 );
4103 }
4104 Ok(())
4105 }
4106
4107 #[test]
4108 fn exact_lu_cut_matches_ordinary_cff_per_residue() -> Result<()> {
4109 test_initialise()?;
4110 let mut graph: Graph = dot!(digraph exact_lu_cut_identity {
4111 edge [num=1 mass=1]
4112 node [num=1]
4113 incoming [style=invis]
4114 outgoing [style=invis]
4115
4116 incoming -> v1 [id=0]
4117 v1 -> v2 [id=1 lmb_id=0]
4118 v1 -> v2 [id=2]
4119 v2 -> outgoing [id=3]
4120 })?;
4121 let options = graph.denominator_only_cff_3d_expression_options();
4122 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
4123 let denominators = [1, 2].map(|edge| {
4124 let edge = EdgeIndex(edge);
4125 FourDDenominator {
4126 source_edge: edge,
4127 momentum: FunctionBuilder::new(GS.emr_mom)
4128 .add_arg(usize::from(edge))
4129 .finish(),
4130 mass_squared: graph.underlying[edge].particle.mass_atom().pow(2),
4131 full_expr: Atom::one(),
4132 }
4133 });
4134 let contract: SuBitGraph = graph.empty_subgraph();
4135
4136 for (expected_order, numerator) in [
4137 (1, Atom::one()),
4138 (1, GS.emr_mom(EdgeIndex(1), GS.cind(0)).pow(4)),
4139 ] {
4140 let production = graph.generate_3d_expression_for_integrand(
4141 &[],
4142 &canonization,
4143 &options,
4144 Some(&numerator),
4145 )?;
4146 let raised_groups = graph
4147 .determine_raised_esurfaces_from_expression(&production.expression)
4148 .raised_groups;
4149 let lu_cut = raised_groups
4150 .into_iter()
4151 .find(|group| {
4152 group.max_occurence == expected_order
4153 && group.esurface_ids.iter().any(|esurface_id| {
4154 !production.expression.surfaces.esurface_cache[*esurface_id]
4155 .external_shift
4156 .is_empty()
4157 })
4158 })
4159 .expect("the production CFF contains the requested physical LU surface");
4160 let mut cutset = CutSet::empty(graph.n_hedges());
4161 cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
4162 raised_group: lu_cut.clone(),
4163 cut_edge_alternatives: lu_cut
4164 .esurface_ids
4165 .iter()
4166 .map(|esurface_id| {
4167 production.expression.surfaces.esurface_cache[*esurface_id]
4168 .energies
4169 .clone()
4170 })
4171 .collect(),
4172 });
4173 let ordinary = graph.cff(
4174 &contract,
4175 &cutset,
4176 &OrientationPattern::default(),
4177 &options,
4178 Some(&numerator),
4179 )?;
4180 let (exact, _) =
4181 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &numerator)?;
4182 assert_eq!(
4183 exact.terms.keys().collect::<Vec<_>>(),
4184 ordinary.terms.keys().collect::<Vec<_>>()
4185 );
4186 for (index, ordinary_term) in &ordinary.terms {
4187 let mut ordinary_sum = ordinary_term
4188 .orientations
4189 .iter()
4190 .map(|orientation| {
4191 orientation.expression.clone()
4192 * numerator.replace_multiple(
4193 orientation.orientation.energy_replacements_gs(&graph),
4194 )
4195 })
4196 .fold(Atom::Zero, |sum, term| sum + term)
4197 * Atom::num(ordinary.production_prefactor_factor());
4198 for edge in [EdgeIndex(1), EdgeIndex(2)] {
4199 let on_shell_energy = (1..=3)
4200 .fold(
4201 graph.underlying[edge].particle.mass_atom().pow(2),
4202 |norm_squared, spatial_index| {
4203 norm_squared + GS.emr_mom(edge, GS.cind(spatial_index)).pow(2)
4204 },
4205 )
4206 .sqrt();
4207 ordinary_sum = ordinary_sum.replace(GS.ose(edge)).with(on_shell_energy);
4208 }
4209 let exact_term = exact
4210 .terms
4211 .get(index)
4212 .expect("exact and ordinary residue keys agree");
4213 let mut exact_sum = exact_term
4214 .orientations
4215 .iter()
4216 .map(|orientation| {
4217 Ok(orientation.expression.clone()
4218 * exact_term
4219 .map_exact_source_numerator(&orientation.orientation, None)?)
4220 })
4221 .collect::<Result<Vec<_>>>()?
4222 .into_iter()
4223 .fold(Atom::Zero, |sum, term| sum + term)
4224 * Atom::num(exact.production_prefactor_factor());
4225 for edge in [EdgeIndex(1), EdgeIndex(2)] {
4226 let on_shell_energy = (1..=3)
4227 .fold(
4228 graph.underlying[edge].particle.mass_atom().pow(2),
4229 |norm_squared, spatial_index| {
4230 norm_squared + GS.emr_mom(edge, GS.cind(spatial_index)).pow(2)
4231 },
4232 )
4233 .sqrt();
4234 exact_sum = exact_sum.replace(GS.ose(edge)).with(on_shell_energy);
4235 }
4236 let difference = (exact_sum.collect_factors() - ordinary_sum.collect_factors())
4237 .collect_factors();
4238 assert!(
4239 difference.is_zero(),
4240 "exact and ordinary LU residues differ for maximum order {expected_order}, index {index}: {difference}"
4241 );
4242 }
4243 }
4244 Ok(())
4245 }
4246
4247 #[test]
4248 fn exact_uncut_cubic_one_loop_source_matches_ordinary_cff() -> Result<()> {
4249 test_initialise()?;
4250 let mut graph: Graph = dot!(digraph exact_uncut_cubic_one_loop {
4251 edge [num=1 mass=1]
4252 node [num=1]
4253
4254 a -> b [id=0 lmb_id=0]
4255 b -> c [id=1]
4256 c -> a [id=2]
4257 })?;
4258 let options = graph.denominator_only_cff_3d_expression_options();
4259 let numerator = GS.emr_mom(EdgeIndex(0), GS.cind(0)).pow(2);
4260 let cutset = CutSet::empty(graph.n_hedges());
4261 let contract: SuBitGraph = graph.empty_subgraph();
4262 let ordinary = graph.cff(
4263 &contract,
4264 &cutset,
4265 &OrientationPattern::default(),
4266 &options,
4267 Some(&numerator),
4268 )?;
4269 let source_momentum = FunctionBuilder::new(GS.emr_mom)
4270 .add_arg(usize::from(EdgeIndex(0)))
4271 .finish();
4272 let denominators = [(EdgeIndex(0), 1), (EdgeIndex(1), -1), (EdgeIndex(2), -1)].map(
4273 |(source_edge, sign)| FourDDenominator {
4274 source_edge,
4275 momentum: Atom::num(sign) * &source_momentum,
4276 mass_squared: graph.underlying[source_edge].particle.mass_atom().pow(2),
4277 full_expr: Atom::one(),
4278 },
4279 );
4280 let (exact, _) =
4281 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &numerator)?;
4282 assert_eq!(
4283 exact.terms.keys().collect::<Vec<_>>(),
4284 ordinary.terms.keys().collect::<Vec<_>>()
4285 );
4286
4287 let index = CutCFFIndex::new_all_none();
4288 let ordinary_sum = ordinary
4289 .terms
4290 .get(&index)
4291 .expect("the ordinary uncut source exists")
4292 .orientations
4293 .iter()
4294 .map(|orientation| {
4295 orientation.expression.clone()
4296 * numerator
4297 .replace_multiple(orientation.orientation.energy_replacements_gs(&graph))
4298 })
4299 .fold(Atom::Zero, |sum, term| sum + term)
4300 * Atom::num(ordinary.production_prefactor_factor());
4301 let exact_term = exact
4302 .terms
4303 .get(&index)
4304 .expect("the exact uncut source exists");
4305 let exact_sum = exact_term
4306 .orientations
4307 .iter()
4308 .map(|orientation| {
4309 Ok(orientation.expression.clone()
4310 * exact_term.map_exact_source_numerator(&orientation.orientation, None)?)
4311 })
4312 .collect::<Result<Vec<_>>>()?
4313 .into_iter()
4314 .fold(Atom::Zero, |sum, term| sum + term)
4315 * Atom::num(exact.production_prefactor_factor());
4316
4317 let evaluate_arb = |mut expression: Atom| -> Result<Complex<F<ArbPrec>>> {
4318 for edge in 0..3 {
4319 let edge = EdgeIndex(edge);
4320 expression = expression
4321 .replace(GS.emr_mom(edge, GS.cind(1)))
4322 .with(Atom::num(Rational::from((3, 4))))
4323 .replace(GS.emr_mom(edge, GS.cind(2)))
4324 .with(Atom::Zero)
4325 .replace(GS.emr_mom(edge, GS.cind(3)))
4326 .with(Atom::Zero)
4327 .replace(GS.ose(edge))
4328 .with(Atom::num(Rational::from((5, 4))));
4329 }
4330 let parameters = [Atom::var(GS.pi)];
4331 let rational: ExpressionEvaluator<SymComplex<Fraction<IntegerRing>>> =
4332 expression.evaluator(¶meters).build().map_err(|error| {
4333 eyre::eyre!("failed to build cubic one-loop evaluator: {error}")
4334 })?;
4335 let mut arb: ExpressionEvaluator<Complex<F<ArbPrec>>> =
4336 rational.map_coeff(&|coefficient| {
4337 Complex::new(F::from(&coefficient.re), F::from(&coefficient.im))
4338 });
4339 Ok(arb.evaluate_single(&[Complex::new(
4340 F(ArbPrec::default()).pi(),
4341 F(ArbPrec::default()),
4342 )]))
4343 };
4344 let ordinary_value = evaluate_arb(ordinary_sum)?;
4345 let exact_value = evaluate_arb(exact_sum)?;
4346 let distance = (exact_value.clone() - ordinary_value.clone()).norm().re;
4347 let exact_norm = exact_value.clone().norm().re;
4348 let ordinary_norm = ordinary_value.clone().norm().re;
4349 let scale = if exact_norm > ordinary_norm {
4350 exact_norm
4351 } else {
4352 ordinary_norm
4353 };
4354 let relative_distance = if scale.is_zero() {
4355 distance
4356 } else {
4357 distance / scale
4358 };
4359 let tolerance = F(ArbPrec::default().epsilon()).sqrt().sqrt().sqrt();
4360 assert!(
4361 relative_distance <= tolerance,
4362 "energy-convergent D(Q)D(-Q)^2 source differs from ordinary CFF: exact={exact_value:e}, ordinary={ordinary_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
4363 );
4364 Ok(())
4365 }
4366
4367 #[test]
4368 fn exact_raised_lu_cut_matches_ordinary_cff_per_residue() -> Result<()> {
4369 test_initialise()?;
4370 let mut graph: Graph = dot!(digraph exact_raised_lu_cut_identity {
4371 edge [num=1 mass=1]
4372 node [num=1]
4373 incoming [style=invis]
4374 outgoing [style=invis]
4375
4376 incoming -> v1 [id=0]
4377 v1 -> v2 [id=1 lmb_id=0]
4378 v2 -> v3 [id=2]
4379 v1 -> v3 [id=3]
4380 v3 -> outgoing [id=4]
4381 })?;
4382 let options = graph.denominator_only_cff_3d_expression_options();
4383 let numerator = GS.emr_mom(EdgeIndex(1), GS.cind(0)).pow(2);
4384 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
4385 let production = graph.generate_3d_expression_for_integrand(
4386 &[],
4387 &canonization,
4388 &options,
4389 Some(&numerator),
4390 )?;
4391 let lu_cut = graph
4392 .determine_raised_esurfaces_from_expression(&production.expression)
4393 .raised_groups
4394 .into_iter()
4395 .find(|group| {
4396 group.max_occurence > 1
4397 && group.esurface_ids.iter().any(|esurface_id| {
4398 !production.expression.surfaces.esurface_cache[*esurface_id]
4399 .external_shift
4400 .is_empty()
4401 })
4402 })
4403 .expect("the production CFF contains a physical raised LU surface");
4404 let mut cutset = CutSet::empty(graph.n_hedges());
4405 cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
4406 raised_group: lu_cut.clone(),
4407 cut_edge_alternatives: lu_cut
4408 .esurface_ids
4409 .iter()
4410 .map(|esurface_id| {
4411 production.expression.surfaces.esurface_cache[*esurface_id]
4412 .energies
4413 .clone()
4414 })
4415 .collect(),
4416 });
4417 let contract: SuBitGraph = graph.empty_subgraph();
4418 let ordinary = graph.cff(
4419 &contract,
4420 &cutset,
4421 &OrientationPattern::default(),
4422 &options,
4423 Some(&numerator),
4424 )?;
4425 let denominators = [1, 2, 3].map(|edge| {
4426 let edge = EdgeIndex(edge);
4427 FourDDenominator {
4428 source_edge: edge,
4429 momentum: FunctionBuilder::new(GS.emr_mom)
4430 .add_arg(usize::from(edge))
4431 .finish(),
4432 mass_squared: graph.underlying[edge].particle.mass_atom().pow(2),
4433 full_expr: Atom::one(),
4434 }
4435 });
4436 let (exact, _) =
4437 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &numerator)?;
4438 assert_eq!(
4439 exact.terms.keys().collect::<Vec<_>>(),
4440 ordinary.terms.keys().collect::<Vec<_>>()
4441 );
4442 for (index, ordinary_term) in &ordinary.terms {
4443 let mut ordinary_sum = ordinary_term
4444 .orientations
4445 .iter()
4446 .map(|orientation| {
4447 orientation.expression.clone()
4448 * numerator.replace_multiple(
4449 orientation.orientation.energy_replacements_gs(&graph),
4450 )
4451 })
4452 .fold(Atom::Zero, |sum, term| sum + term)
4453 * Atom::num(ordinary.production_prefactor_factor());
4454 for edge in [EdgeIndex(1), EdgeIndex(2), EdgeIndex(3)] {
4455 let on_shell_energy = (1..=3)
4456 .fold(
4457 graph.underlying[edge].particle.mass_atom().pow(2),
4458 |norm_squared, spatial_index| {
4459 norm_squared + GS.emr_mom(edge, GS.cind(spatial_index)).pow(2)
4460 },
4461 )
4462 .sqrt();
4463 ordinary_sum = ordinary_sum.replace(GS.ose(edge)).with(on_shell_energy);
4464 }
4465 let exact_term = exact
4466 .terms
4467 .get(index)
4468 .expect("exact and ordinary residue keys agree");
4469 let exact_sum = exact_term
4470 .orientations
4471 .iter()
4472 .map(|orientation| {
4473 Ok(orientation.expression.clone()
4474 * exact_term.map_exact_source_numerator(&orientation.orientation, None)?)
4475 })
4476 .collect::<Result<Vec<_>>>()?
4477 .into_iter()
4478 .fold(Atom::Zero, |sum, term| sum + term)
4479 * Atom::num(exact.production_prefactor_factor());
4480 let mut difference = exact_sum - ordinary_sum;
4481 for edge in (0..graph.underlying.n_edges()).map(EdgeIndex) {
4482 for spatial_index in 1..=3 {
4483 difference = difference
4484 .replace(GS.emr_mom(edge, GS.cind(spatial_index)))
4485 .with(Atom::Zero);
4486 }
4487 difference = difference.replace(GS.ose(edge)).with(Atom::one());
4488 }
4489 for external_edge in [EdgeIndex(0), EdgeIndex(4)] {
4490 difference = difference
4491 .replace(GS.emr_mom(external_edge, GS.cind(0)))
4492 .with(Atom::num(7));
4493 }
4494 let difference = difference.together();
4495 assert!(
4496 difference.is_zero(),
4497 "exact and ordinary raised LU residues differ for index {index}: {difference}"
4498 );
4499 }
4500 Ok(())
4501 }
4502
4503 #[test]
4504 fn exact_cff_keeps_opposite_source_routing_without_a_sign_bridge() -> Result<()> {
4505 test_initialise()?;
4506 let mut graph: Graph = dot!(digraph exact_opposite_routing {
4507 edge [num=1 mass=1]
4508 node [num=1]
4509
4510 a -> b [id=0 lmb_id=0]
4511 a -> b [id=1]
4512 })?;
4513 let momentum = FunctionBuilder::new(GS.emr_mom).add_arg(0).finish();
4514 let mass_squared = Atom::var(GS.m_uv_expansion).pow(2);
4515 let denominators = [
4516 FourDDenominator {
4517 source_edge: EdgeIndex(0),
4518 momentum: momentum.clone(),
4519 mass_squared: mass_squared.clone(),
4520 full_expr: Atom::var(symbolica::symbol!("exact_cff_test::positive_routing")),
4521 },
4522 FourDDenominator {
4523 source_edge: EdgeIndex(1),
4524 momentum: -momentum,
4525 mass_squared: mass_squared.clone(),
4526 full_expr: Atom::var(symbolica::symbol!("exact_cff_test::negative_routing")),
4527 },
4528 ];
4529 let options = graph.denominator_only_cff_3d_expression_options();
4530 let cutset = CutSet::empty(graph.n_hedges());
4531
4532 let (cff, _) =
4533 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &Atom::one())?;
4534 assert_eq!(cff.terms.len(), 1);
4535 let on_shell_energy = (1..=3)
4536 .fold(mass_squared, |norm_squared, spatial_index| {
4537 norm_squared + GS.emr_mom(EdgeIndex(0), GS.cind(spatial_index)).pow(2)
4538 })
4539 .sqrt();
4540 let expected_orientation_sum =
4541 Atom::i() / (Atom::num(32) * Atom::var(GS.pi).pow(3) * on_shell_energy.pow(3));
4542 let terms = &cff
4543 .terms
4544 .values()
4545 .next()
4546 .expect("the empty cutset has one CFF term")
4547 .orientations;
4548
4549 let orientation_sum = terms
4550 .iter()
4551 .fold(Atom::zero(), |sum, term| sum + &term.expression)
4552 .replace(GS.ose(EdgeIndex(0)))
4553 .with(on_shell_energy.clone());
4554 assert!(
4555 (orientation_sum * Atom::num(cff.production_prefactor_factor())
4556 - expected_orientation_sum)
4557 .together()
4558 .is_zero()
4559 );
4560
4561 Ok(())
4562 }
4563
4564 #[test]
4565 fn exact_cff_separates_uv_topology_from_the_cograph() -> Result<()> {
4566 test_initialise()?;
4567 let mut graph: Graph = dot!(digraph exact_uv_cograph {
4568 edge [num=1 mass=1]
4569 node [num=1]
4570
4571 a -> b [id=0 lmb_id=0]
4572 a -> b [id=1]
4573 b -> c [id=4]
4574 c -> d [id=2 lmb_id=1]
4575 c -> d [id=3]
4576 })?;
4577 let mut denominators = Vec::new();
4578 for edge in [EdgeIndex::from(0), EdgeIndex::from(1)] {
4579 denominators.push(FourDDenominator {
4580 source_edge: edge,
4581 momentum: FunctionBuilder::new(GS.emr_mom)
4582 .add_arg(usize::from(edge))
4583 .finish(),
4584 mass_squared: Atom::var(GS.m_uv_expansion).pow(2),
4585 full_expr: Atom::var(symbolica::symbol!("exact_cff_test::uv_full")),
4586 });
4587 }
4588 for edge in [EdgeIndex::from(2), EdgeIndex::from(3)] {
4589 denominators.push(FourDDenominator {
4590 source_edge: edge,
4591 momentum: FunctionBuilder::new(GS.emr_mom)
4592 .add_arg(usize::from(edge))
4593 .finish(),
4594 mass_squared: graph.underlying[edge].particle.mass_atom().pow(2),
4595 full_expr: Atom::var(symbolica::symbol!("exact_cff_test::cograph_full")),
4596 });
4597 }
4598 let options = graph.denominator_only_cff_3d_expression_options();
4599 let cutset = CutSet::empty(graph.n_hedges());
4600
4601 let (cff, _) =
4602 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &Atom::one())?;
4603 assert!(cff.terms.values().any(|term| !term.orientations.is_empty()));
4604 Ok(())
4605 }
4606
4607 #[test]
4608 fn exact_cff_lu_residue_factorizes_from_quadratic_cubic_spectator() -> Result<()> {
4609 test_initialise()?;
4610 let mut graph: Graph = dot!(digraph exact_lu_cubic_product {
4611 edge [num=1 mass=1]
4612 node [num=1]
4613
4614 a -> b [id=0 lmb_id=0]
4615 a -> b [id=1]
4616 b -> c [id=4]
4617 c -> d [id=2 lmb_id=1]
4618 c -> d [id=3]
4619 })?;
4620 let options = graph.denominator_only_cff_3d_expression_options();
4621 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
4622 let production = graph.generate_3d_expression_for_integrand(
4623 &[EdgeIndex(4)],
4624 &canonization,
4625 &options,
4626 Some(&Atom::one()),
4627 )?;
4628 let lu_surface = graph
4629 .determine_raised_esurfaces_from_expression(&production.expression)
4630 .raised_groups
4631 .into_iter()
4632 .find(|group| {
4633 group.esurface_ids.iter().any(|surface_id| {
4634 production.expression.surfaces.esurface_cache[*surface_id]
4635 .energies
4636 .iter()
4637 .copied()
4638 .collect::<BTreeSet<_>>()
4639 == BTreeSet::from([EdgeIndex(0), EdgeIndex(1)])
4640 })
4641 })
4642 .expect("the cograph bubble supplies an LU residue surface");
4643 let mut lu_cutset = CutSet::empty(graph.n_hedges());
4644 lu_cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
4645 raised_group: lu_surface,
4646 cut_edge_alternatives: vec![vec![EdgeIndex(0), EdgeIndex(1)]],
4647 });
4648
4649 let cograph_denominators = [EdgeIndex(0), EdgeIndex(1)].map(|edge| FourDDenominator {
4650 source_edge: edge,
4651 momentum: FunctionBuilder::new(GS.emr_mom)
4652 .add_arg(usize::from(edge))
4653 .finish(),
4654 mass_squared: graph.underlying[edge].particle.mass_atom().pow(2),
4655 full_expr: Atom::one(),
4656 });
4657 let uv_edge = EdgeIndex(2);
4658 let uv_momentum = FunctionBuilder::new(GS.emr_mom)
4659 .add_arg(usize::from(uv_edge))
4660 .finish();
4661 let uv_mass_squared = Atom::var(GS.m_uv_expansion).pow(2);
4662 let uv_full_expr = GS.emr_mom(uv_edge, GS.cind(0)).pow(2)
4663 - (1..=3).fold(uv_mass_squared.clone(), |norm_squared, spatial_index| {
4664 norm_squared + GS.emr_mom(uv_edge, GS.cind(spatial_index)).pow(2)
4665 });
4666 let uv_denominators = [1, 1, -1].map(|sign| FourDDenominator {
4667 source_edge: uv_edge,
4668 momentum: Atom::num(sign) * &uv_momentum,
4669 mass_squared: uv_mass_squared.clone(),
4670 full_expr: uv_full_expr.clone(),
4671 });
4672 let quadratic_numerator = GS.emr_mom(uv_edge, GS.cind(0)).pow(2);
4673 let combined_denominators = cograph_denominators
4674 .iter()
4675 .cloned()
4676 .chain(uv_denominators.iter().cloned())
4677 .collect::<Vec<_>>();
4678
4679 let combined_source =
4680 GraphThreeDSource::from_exact_denominators(&graph, &combined_denominators)?;
4681 let cograph_source =
4682 GraphThreeDSource::from_exact_denominators(&graph, &cograph_denominators)?;
4683 let uv_source = GraphThreeDSource::from_exact_denominators(&graph, &uv_denominators)?;
4684 assert_eq!(
4685 combined_source.active_loop_count(),
4686 cograph_source.active_loop_count() + uv_source.active_loop_count(),
4687 "the source phase must factor between the independent loop components",
4688 );
4689 let (combined, _) = graph.cff_from_4d_denominators(
4690 &combined_denominators,
4691 &lu_cutset,
4692 &options,
4693 &quadratic_numerator,
4694 )?;
4695 let (cograph, _) = graph.cff_from_4d_denominators(
4696 &cograph_denominators,
4697 &lu_cutset,
4698 &options,
4699 &Atom::one(),
4700 )?;
4701 let (uv, _) = graph.cff_from_4d_denominators(
4702 &uv_denominators,
4703 &CutSet::empty(graph.n_hedges()),
4704 &options,
4705 &quadratic_numerator,
4706 )?;
4707 let cff_sum = |cff: &CutCFF| -> Result<Atom> {
4708 let term = cff
4709 .terms
4710 .values()
4711 .next()
4712 .expect("the exact source retains its requested residue sector");
4713 Ok(term
4714 .orientations
4715 .iter()
4716 .map(|orientation| {
4717 Ok(orientation.expression.clone()
4718 * term.map_exact_source_numerator(&orientation.orientation, None)?)
4719 })
4720 .collect::<Result<Vec<_>>>()?
4721 .into_iter()
4722 .fold(Atom::Zero, |sum, term| sum + term)
4723 * Atom::num(cff.production_prefactor_factor()))
4724 };
4725 let combined_sum = cff_sum(&combined)?;
4726 let cograph_sum = cff_sum(&cograph)?;
4727 let uv_sum = cff_sum(&uv)?;
4728
4729 let empty_cutset = CutSet::empty(graph.n_hedges());
4730 let (combined_uncut, _) = graph.cff_from_4d_denominators(
4731 &combined_denominators,
4732 &empty_cutset,
4733 &options,
4734 &quadratic_numerator,
4735 )?;
4736 let (cograph_uncut, _) = graph.cff_from_4d_denominators(
4737 &cograph_denominators,
4738 &empty_cutset,
4739 &options,
4740 &Atom::one(),
4741 )?;
4742 let combined_uncut_sum = cff_sum(&combined_uncut)?;
4743 let cograph_uncut_sum = cff_sum(&cograph_uncut)?;
4744 let uncut_difference = (combined_uncut_sum.collect_factors()
4746 - (&cograph_uncut_sum * &uv_sum).collect_factors())
4747 .collect_factors();
4748 assert!(
4749 uncut_difference.is_zero(),
4750 "an uncut exact source must factorize between its independent rational components: difference={uncut_difference}, combined={combined_uncut_sum} (bridge={}), cograph={cograph_uncut_sum} (bridge={}), spectator={uv_sum} (bridge={})",
4751 combined_uncut.production_prefactor_factor(),
4752 cograph_uncut.production_prefactor_factor(),
4753 uv.production_prefactor_factor(),
4754 );
4755 let difference = (combined_sum.collect_factors()
4756 - (cograph_sum * uv_sum).collect_factors())
4757 .collect_factors();
4758 assert!(
4759 difference.is_zero(),
4760 "an LU residue in one exact component must factorize from a quadratic cubic spectator: {difference}"
4761 );
4762 Ok(())
4763 }
4764
4765 #[test]
4766 fn selected_highest_pole_commutes_with_factorized_cubic_numerator() -> Result<()> {
4767 test_initialise()?;
4768 let mut graph: Graph = dot!(digraph selected_cubic_repeated_channel {
4769 num = 1
4770 edge [particle="scalar_1" num=1]
4771 node [num=1]
4772
4773 ext0 [style=invis is_cut=0]
4774 v2 -> ext0 [id=0]
4775 ext0 -> v3
4776 v0 -> v1 [id=1 lmb_id=0]
4777 v0 -> v1 [id=2]
4778 v3 -> v0 [id=3 lmb_id=1]
4779 v1 -> v2 [id=4]
4780 v2 -> v3 [id=5]
4781 }, "scalars")?;
4782 let options = graph.denominator_only_cff_3d_expression_options();
4783 let numerator =
4787 GS.emr_mom(EdgeIndex(3), GS.cind(0)).pow(2) * GS.emr_mom(EdgeIndex(4), GS.cind(0));
4788 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
4789 let production = graph.generate_3d_expression_for_integrand(
4790 &[],
4791 &canonization,
4792 &options,
4793 Some(&numerator),
4794 )?;
4795 let raised_group = graph
4796 .determine_raised_esurfaces_from_expression(&production.expression)
4797 .raised_groups
4798 .into_iter()
4799 .find(|group| {
4800 group.max_occurence == 2
4801 && group.esurface_ids.iter().any(|surface_id| {
4802 !production.expression.surfaces.esurface_cache[*surface_id]
4803 .external_shift
4804 .is_empty()
4805 })
4806 })
4807 .expect("the repeated outer channel supplies a second-order LU surface");
4808 let mut cutset = CutSet::empty(graph.n_hedges());
4809 cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
4810 cut_edge_alternatives: raised_group
4811 .esurface_ids
4812 .iter()
4813 .map(|surface_id| {
4814 production.expression.surfaces.esurface_cache[*surface_id]
4815 .energies
4816 .clone()
4817 })
4818 .collect(),
4819 raised_group,
4820 });
4821 let contracted = graph
4822 .get_edge_subgraph(EdgeIndex(1))
4823 .union(&graph.get_edge_subgraph(EdgeIndex(2)));
4824 let contract_subgraph = contracted
4825 .union(&graph.tree_edges)
4826 .subtract(&graph.initial_state_cut);
4827 let pattern = OrientationPattern::default();
4828 let generalized = graph.cff(
4829 &contract_subgraph,
4830 &cutset,
4831 &pattern,
4832 &options,
4833 Some(&numerator),
4834 )?;
4835 let ordinary = graph.cff(
4836 &contract_subgraph,
4837 &cutset,
4838 &pattern,
4839 &options,
4840 Some(&Atom::one()),
4841 )?;
4842 let highest_pole = |cff: &CutCFF| {
4843 let term = cff
4844 .terms
4845 .iter()
4846 .find(|(index, _)| index.lu_cut_order == Some(2))
4847 .expect("the selected CFF retains its maximum LU order")
4848 .1;
4849 term.orientations
4850 .iter()
4851 .map(|orientation| {
4852 &orientation.expression
4853 * numerator.replace_multiple(
4854 orientation.orientation.energy_replacements_gs(&graph),
4855 )
4856 })
4857 .fold(Atom::Zero, |sum, term| sum + term)
4858 * Atom::num(cff.production_prefactor_factor())
4859 };
4860 let difference = (highest_pole(&generalized).collect_factors()
4861 - highest_pole(&ordinary).collect_factors())
4862 .collect_factors();
4863 assert!(
4864 difference.is_zero(),
4865 "the maximum-order LU residue must commute with its factorized cubic numerator: {difference}",
4866 );
4867 Ok(())
4868 }
4869
4870 #[test]
4871 fn selected_lower_pole_obeys_repeated_channel_polynomial_division() -> Result<()> {
4872 test_initialise()?;
4873 let mut graph: Graph = dot!(digraph selected_cubic_repeated_channel_contact {
4874 num = 1
4875 edge [particle="scalar_1" num=1]
4876 node [num=1]
4877
4878 ext0 [style=invis is_cut=0]
4879 v2 -> ext0 [id=0]
4880 ext0 -> v3
4881 v0 -> v1 [id=1 lmb_id=0]
4882 v0 -> v1 [id=2]
4883 v3 -> v0 [id=3 lmb_id=1]
4884 v1 -> v2 [id=4]
4885 v2 -> v3 [id=5]
4886 }, "scalars")?;
4887 let options = graph.denominator_only_cff_3d_expression_options();
4888 let repeated_energy = GS.emr_mom(EdgeIndex(3), GS.cind(0));
4889 let alias_energy = GS.emr_mom(EdgeIndex(4), GS.cind(0));
4890 let numerator = repeated_energy.pow(2) * &alias_energy;
4891 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
4892 let production = graph.generate_3d_expression_for_integrand(
4893 &[],
4894 &canonization,
4895 &options,
4896 Some(&numerator),
4897 )?;
4898 let raised_group = graph
4899 .determine_raised_esurfaces_from_expression(&production.expression)
4900 .raised_groups
4901 .into_iter()
4902 .find(|group| {
4903 group.max_occurence == 2
4904 && group.esurface_ids.iter().any(|surface_id| {
4905 !production.expression.surfaces.esurface_cache[*surface_id]
4906 .external_shift
4907 .is_empty()
4908 })
4909 })
4910 .expect("the repeated outer channel supplies a second-order LU surface");
4911 let mut cutset = CutSet::empty(graph.n_hedges());
4912 cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
4913 cut_edge_alternatives: raised_group
4914 .esurface_ids
4915 .iter()
4916 .map(|surface_id| {
4917 production.expression.surfaces.esurface_cache[*surface_id]
4918 .energies
4919 .clone()
4920 })
4921 .collect(),
4922 raised_group,
4923 });
4924 let contracted = graph
4925 .get_edge_subgraph(EdgeIndex(1))
4926 .union(&graph.get_edge_subgraph(EdgeIndex(2)));
4927 let contract_subgraph = contracted
4928 .union(&graph.tree_edges)
4929 .subtract(&graph.initial_state_cut);
4930 let pattern = OrientationPattern::default();
4931 let generalized = graph.cff(
4932 &contract_subgraph,
4933 &cutset,
4934 &pattern,
4935 &options,
4936 Some(&numerator),
4937 )?;
4938
4939 let remainder_numerator = GS.ose(EdgeIndex(3)).pow(2) * &alias_energy;
4947 let remainder = graph.cff(
4948 &contract_subgraph,
4949 &cutset,
4950 &pattern,
4951 &options,
4952 Some(&remainder_numerator),
4953 )?;
4954 let mut contact_contract = contract_subgraph.clone();
4955 contact_contract.add(graph[&EdgeIndex(3)].1);
4956 let contact = graph.cff(
4957 &contact_contract,
4958 &cutset,
4959 &pattern,
4960 &options,
4961 Some(&alias_energy),
4962 )?;
4963 let selected_lu1 = |cff: &CutCFF, mapped_numerator: &Atom| {
4964 cff.terms
4965 .iter()
4966 .filter(|(index, _)| index.lu_cut_order == Some(1))
4967 .flat_map(|(_, term)| {
4968 term.orientations.iter().map(|orientation| {
4969 &orientation.expression
4970 * mapped_numerator.replace_multiple(
4971 orientation.orientation.energy_replacements_gs(&graph),
4972 )
4973 })
4974 })
4975 .fold(Atom::Zero, |sum, term| sum + term)
4976 * Atom::num(cff.production_prefactor_factor())
4977 };
4978 let difference = (selected_lu1(&generalized, &numerator).collect_factors()
4979 - (selected_lu1(&remainder, &remainder_numerator)
4980 + selected_lu1(&contact, &alias_energy))
4981 .collect_factors())
4982 .replace(GS.ose(EdgeIndex(4)))
4983 .with(GS.ose(EdgeIndex(3)))
4984 .collect_factors();
4985 assert!(
4986 difference.is_zero(),
4987 "the first-order LU residue must preserve exact polynomial division on a repeated channel: {difference}",
4988 );
4989 Ok(())
4990 }
4991
4992 #[test]
4993 fn exact_raised_lu_residue_factorizes_from_quadratic_cubic_spectator() -> Result<()> {
4994 test_initialise()?;
4995 let mut graph: Graph = dot!(digraph exact_raised_lu_cubic_product {
4996 edge [num=1 mass=1]
4997 node [num=1]
4998
4999 incoming [style=invis]
5000 outgoing [style=invis]
5001
5002 incoming -> v1 [id=0]
5003 v1 -> v2 [id=1 lmb_id=0]
5004 v2 -> v3 [id=2]
5005 v1 -> v3 [id=3]
5006 v1 -> u [id=5 lmb_id=1]
5007 u -> v1 [id=6]
5008 v3 -> outgoing [id=4]
5009 })?;
5010 let options = graph.denominator_only_cff_3d_expression_options();
5011 let cograph_edge = EdgeIndex(1);
5012 let cograph_numerator = GS.emr_mom(cograph_edge, GS.cind(0)).pow(2);
5013 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
5014 let production = graph.generate_3d_expression_for_integrand(
5015 &[EdgeIndex(5), EdgeIndex(6)],
5016 &canonization,
5017 &options,
5018 Some(&cograph_numerator),
5019 )?;
5020 let lu_surface = graph
5021 .determine_raised_esurfaces_from_expression(&production.expression)
5022 .raised_groups
5023 .into_iter()
5024 .find(|group| {
5025 group.max_occurence == 2
5026 && group.esurface_ids.iter().any(|surface_id| {
5027 production.expression.surfaces.esurface_cache[*surface_id]
5028 .energies
5029 .iter()
5030 .copied()
5031 .collect::<BTreeSet<_>>()
5032 .is_subset(&BTreeSet::from([EdgeIndex(1), EdgeIndex(2), EdgeIndex(3)]))
5033 })
5034 })
5035 .expect("the quadratic cograph supplies a second-order LU surface");
5036 let mut lu_cutset = CutSet::empty(graph.n_hedges());
5037 let physical_cut_support = production.expression.surfaces.esurface_cache
5038 [lu_surface.esurface_ids[0]]
5039 .energies
5040 .clone();
5041 lu_cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
5042 cut_edge_alternatives: vec![physical_cut_support; lu_surface.esurface_ids.len()],
5045 raised_group: lu_surface,
5046 });
5047
5048 let cograph_denominators =
5049 [EdgeIndex(1), EdgeIndex(2), EdgeIndex(3)].map(|edge| FourDDenominator {
5050 source_edge: edge,
5051 momentum: FunctionBuilder::new(GS.emr_mom)
5052 .add_arg(usize::from(edge))
5053 .finish(),
5054 mass_squared: graph.underlying[edge].particle.mass_atom().pow(2),
5055 full_expr: Atom::one(),
5056 });
5057 let uv_edge = EdgeIndex(5);
5058 let uv_momentum = FunctionBuilder::new(GS.emr_mom)
5059 .add_arg(usize::from(uv_edge))
5060 .finish();
5061 let uv_mass_squared = Atom::var(GS.m_uv_expansion).pow(2);
5062 let uv_full_expr = GS.emr_mom(uv_edge, GS.cind(0)).pow(2)
5063 - (1..=3).fold(uv_mass_squared.clone(), |norm_squared, spatial_index| {
5064 norm_squared + GS.emr_mom(uv_edge, GS.cind(spatial_index)).pow(2)
5065 });
5066 let uv_denominators = [(EdgeIndex(5), 1), (EdgeIndex(6), -1), (EdgeIndex(6), -1)].map(
5070 |(source_edge, sign)| FourDDenominator {
5071 source_edge,
5072 momentum: Atom::num(sign) * &uv_momentum,
5073 mass_squared: uv_mass_squared.clone(),
5074 full_expr: uv_full_expr.clone(),
5075 },
5076 );
5077 let uv_numerator = GS.emr_mom(uv_edge, GS.cind(0)).pow(2);
5078 let combined_numerator = &cograph_numerator * &uv_numerator;
5079 let combined_denominators = cograph_denominators
5080 .iter()
5081 .cloned()
5082 .chain(uv_denominators.iter().cloned())
5083 .collect::<Vec<_>>();
5084 let uv_edges = [EdgeIndex(5), EdgeIndex(6)];
5085
5086 let (combined, _) = graph.cff_from_4d_denominators_in_uv_edges(
5087 &combined_denominators,
5088 uv_edges,
5089 &lu_cutset,
5090 &options,
5091 &combined_numerator,
5092 None,
5093 )?;
5094 let (cograph, _) = graph.cff_from_4d_denominators(
5095 &cograph_denominators,
5096 &lu_cutset,
5097 &options,
5098 &cograph_numerator,
5099 )?;
5100 let (uv, _) = graph.cff_from_4d_denominators_in_uv_edges(
5101 &uv_denominators,
5102 uv_edges,
5103 &CutSet::empty(graph.n_hedges()),
5104 &options,
5105 &uv_numerator,
5106 None,
5107 )?;
5108 assert_eq!(
5109 combined.terms.keys().collect::<BTreeSet<_>>(),
5110 cograph.terms.keys().collect::<BTreeSet<_>>(),
5111 "tensoring an uncut UV spectator must preserve every raised cograph residue order"
5112 );
5113 assert_eq!(
5114 combined
5115 .terms
5116 .keys()
5117 .map(|index| index.lu_cut_order)
5118 .collect::<BTreeSet<_>>(),
5119 BTreeSet::from([Some(1), Some(2)])
5120 );
5121
5122 let exact_sum = |cff: &CutCFF, index: &CutCFFIndex| -> Result<Atom> {
5123 let term = cff
5124 .terms
5125 .get(index)
5126 .expect("the requested exact-CFF residue sector exists");
5127 Ok(term
5128 .orientations
5129 .iter()
5130 .map(|orientation| {
5131 Ok(orientation.expression.clone()
5132 * term.map_exact_source_numerator(&orientation.orientation, None)?)
5133 })
5134 .collect::<Result<Vec<_>>>()?
5135 .into_iter()
5136 .fold(Atom::Zero, |sum, term| sum + term)
5137 * Atom::num(cff.production_prefactor_factor()))
5138 };
5139 let ordinary_sum = |graph: &Graph, cff: &CutCFF, index: &CutCFFIndex, numerator: &Atom| {
5140 cff.terms
5141 .get(index)
5142 .expect("the requested ordinary-CFF residue sector exists")
5143 .orientations
5144 .iter()
5145 .map(|orientation| {
5146 orientation.expression.clone()
5147 * numerator
5148 .replace_multiple(orientation.orientation.energy_replacements_gs(graph))
5149 })
5150 .fold(Atom::Zero, |sum, term| sum + term)
5151 * Atom::num(cff.production_prefactor_factor())
5152 };
5153 let uv_index = CutCFFIndex::new_all_none();
5154 let uv_sum = exact_sum(&uv, &uv_index)?;
5155 let graph_mass_squared = (0..graph.underlying.n_edges())
5156 .map(|edge| {
5157 graph.underlying[EdgeIndex(edge)]
5158 .particle
5159 .mass_atom()
5160 .pow(2)
5161 })
5162 .collect::<Vec<_>>();
5163 let external_edges = graph
5164 .external_momentum_edge_order()
5165 .into_iter()
5166 .collect::<BTreeSet<_>>();
5167 let representative_esurface_id = lu_cutset
5168 .residue_selector
5169 .lu
5170 .as_ref()
5171 .expect("the raised LU selector exists")
5172 .raised_group
5173 .esurface_ids[0];
5174 let representative_esurface =
5175 &production.expression.surfaces.esurface_cache[representative_esurface_id];
5176 let rescale_star = Atom::one();
5177 let cograph_spatial_momentum = Atom::num(Rational::from((3, 4)));
5178 let uv_spatial_momentum = Atom::num(Rational::from((4, 3)));
5179 let base_spatial_momentum = |edge: EdgeIndex| {
5180 if uv_edges.contains(&edge) {
5181 uv_spatial_momentum.clone()
5182 } else {
5183 cograph_spatial_momentum.clone()
5184 }
5185 };
5186 let scaled_on_shell_energy = |edge: EdgeIndex, rescale: Atom| {
5187 (graph_mass_squared[usize::from(edge)].clone()
5188 + (base_spatial_momentum(edge) * rescale).pow(2))
5189 .sqrt()
5190 };
5191 let root_energy_sum = representative_esurface
5192 .energies
5193 .iter()
5194 .map(|edge| scaled_on_shell_energy(*edge, rescale_star.clone()))
5195 .fold(Atom::Zero, |sum, energy| sum + energy);
5196 let external_shift_coefficient = representative_esurface
5197 .external_shift
5198 .iter()
5199 .map(|(_, coefficient)| *coefficient)
5200 .sum::<i64>();
5201 assert_ne!(
5202 external_shift_coefficient, 0,
5203 "the raised LU surface must have a nonzero external shift"
5204 );
5205 let external_energy = -root_energy_sum / Atom::num(external_shift_coefficient);
5206 let rescale_expression = |mut expression: Atom| {
5207 let rescale = Atom::var(GS.rescale);
5208 expression = expression
5209 .replace(Atom::var(GS.m_uv_expansion))
5210 .with(Atom::one());
5211 for edge in 0..graph_mass_squared.len() {
5212 let edge = EdgeIndex(edge);
5213 if external_edges.contains(&edge) {
5214 expression = expression
5215 .replace(GS.emr_mom(edge, GS.cind(0)))
5216 .with(external_energy.clone());
5217 for spatial_index in 1..=3 {
5218 expression = expression
5219 .replace(GS.emr_mom(edge, GS.cind(spatial_index)))
5220 .with(Atom::Zero);
5221 }
5222 } else {
5223 let on_shell_energy = scaled_on_shell_energy(edge, rescale.clone());
5224 expression = expression
5225 .replace(GS.emr_mom(edge, GS.cind(1)))
5226 .with(base_spatial_momentum(edge) * &rescale)
5227 .replace(GS.ose(edge))
5228 .with(on_shell_energy.clone())
5229 .replace(cut_energy(edge))
5230 .with(on_shell_energy);
5231 for spatial_index in 2..=3 {
5232 expression = expression
5233 .replace(GS.emr_mom(edge, GS.cind(spatial_index)))
5234 .with(Atom::Zero);
5235 }
5236 }
5237 }
5238 expression
5239 };
5240 let eta = rescale_expression(representative_esurface.to_atom(&[]));
5241 assert!(
5242 eta.replace(GS.rescale)
5243 .with(rescale_star.clone())
5244 .expand()
5245 .is_zero(),
5246 "the raised-LU spectator oracle must be evaluated at its selected radial root"
5247 );
5248 let value_and_t_derivative = |expression: Atom| -> Result<[Atom; 2]> {
5249 let jet = rescale_expression(expression)
5250 .series(GS.rescale, rescale_star.clone(), 1)
5251 .map_err(|error| eyre::eyre!("failed to build raised-LU t jet: {error}"))?
5252 .to_atom();
5253 Ok([
5254 jet.replace(GS.rescale).with(rescale_star.clone()),
5255 jet.derivative(GS.rescale)
5256 .replace(GS.rescale)
5257 .with(rescale_star.clone()),
5258 ])
5259 };
5260 let evaluate_arb = |expression: Atom| -> Result<Complex<F<ArbPrec>>> {
5261 let parameters = [Atom::var(GS.pi)];
5262 let rational: ExpressionEvaluator<SymComplex<Fraction<IntegerRing>>> =
5263 expression.evaluator(¶meters).build().map_err(|error| {
5264 eyre::eyre!("failed to build raised-LU spectator evaluator: {error}")
5265 })?;
5266 let mut arb: ExpressionEvaluator<Complex<F<ArbPrec>>> =
5267 rational.map_coeff(&|coefficient| {
5268 Complex::new(F::from(&coefficient.re), F::from(&coefficient.im))
5269 });
5270 Ok(arb.evaluate_single(&[Complex::new(
5271 F::<ArbPrec>::from(&Rational::from(0)).pi(),
5272 F::<ArbPrec>::from(&Rational::from(0)),
5273 )]))
5274 };
5275 let tolerance = F(ArbPrec::default().epsilon()).sqrt().sqrt().sqrt();
5278 let mut failures = Vec::new();
5279 for index in cograph.terms.keys() {
5280 let combined_jet = value_and_t_derivative(exact_sum(&combined, index)?)?;
5281 let factorized_jet = value_and_t_derivative(exact_sum(&cograph, index)? * &uv_sum)?;
5282 for (jet_component, combined_expression, factorized_expression) in
5283 ["value", "first t derivative"]
5284 .into_iter()
5285 .zip(combined_jet)
5286 .zip(factorized_jet)
5287 .map(|((component, combined), factorized)| (component, combined, factorized))
5288 {
5289 let combined_value = evaluate_arb(combined_expression)?;
5290 let factorized_value = evaluate_arb(factorized_expression)?;
5291 let distance = (combined_value.clone() - factorized_value.clone())
5292 .norm()
5293 .re;
5294 let combined_norm = combined_value.clone().norm().re;
5295 let factorized_norm = factorized_value.clone().norm().re;
5296 let scale = if combined_norm > factorized_norm {
5297 combined_norm
5298 } else {
5299 factorized_norm
5300 };
5301 let relative_distance = if scale.is_zero() {
5302 distance
5303 } else {
5304 distance / scale
5305 };
5306 if !([
5307 &combined_value.re,
5308 &combined_value.im,
5309 &factorized_value.re,
5310 &factorized_value.im,
5311 ]
5312 .into_iter()
5313 .all(symbolica::domains::float::SingleFloat::is_finite)
5314 && symbolica::domains::float::SingleFloat::is_finite(&relative_distance)
5315 && relative_distance <= tolerance)
5316 {
5317 failures.push(format!(
5318 "raised LU residue {index} {jet_component} does not factorize from its quadratic cubic UV spectator: combined={combined_value:e}, factorized={factorized_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
5319 ));
5320 }
5321 }
5322 }
5323 assert!(
5324 failures.is_empty(),
5325 "raised-LU cubic spectator factorization failures:\n{}",
5326 failures.join("\n")
5327 );
5328
5329 assert_eq!(
5335 graph.loop_momentum_basis.edge_signatures[EdgeIndex(1)],
5336 graph.loop_momentum_basis.edge_signatures[EdgeIndex(2)],
5337 "the outer cograph must contain two owner-distinct edges with the same k routing"
5338 );
5339 let outer_numerator = [EdgeIndex(1), EdgeIndex(2), EdgeIndex(3)]
5340 .into_iter()
5341 .map(|edge| GS.emr_mom(edge, GS.cind(0)) + Atom::one())
5342 .fold(Atom::one(), |product, factor| product * factor);
5343 let retained_dotted_factor =
5344 Atom::num(2) * GS.emr_mom(cograph_edge, GS.cind(0)) * &outer_numerator;
5345 let (exact_outer, _) = graph.cff_from_4d_denominators(
5346 &cograph_denominators,
5347 &lu_cutset,
5348 &options,
5349 &retained_dotted_factor,
5350 )?;
5351 let mut uv_contract: SuBitGraph = graph.empty_subgraph();
5352 for edge in uv_edges {
5353 uv_contract.add(graph[&edge].1);
5354 }
5355 let ordinary_outer = graph.cff(
5356 &uv_contract,
5357 &lu_cutset,
5358 &OrientationPattern::default(),
5359 &options,
5360 Some(&retained_dotted_factor),
5361 )?;
5362 assert_eq!(
5363 exact_outer.terms.keys().collect::<BTreeSet<_>>(),
5364 ordinary_outer.terms.keys().collect::<BTreeSet<_>>(),
5365 "exact and ordinary alias cographs must expose both raised LU orders"
5366 );
5367 failures.clear();
5368 for index in ordinary_outer.terms.keys() {
5369 let exact_jet = value_and_t_derivative(exact_sum(&exact_outer, index)?)?;
5370 let ordinary_term = ordinary_outer
5371 .terms
5372 .get(index)
5373 .expect("the ordinary alias-cograph residue exists");
5374 let ordinary_sum = ordinary_term
5375 .orientations
5376 .iter()
5377 .map(|orientation| {
5378 orientation.expression.clone()
5379 * retained_dotted_factor.replace_multiple(
5380 orientation.orientation.energy_replacements_gs(&graph),
5381 )
5382 })
5383 .fold(Atom::Zero, |sum, term| sum + term)
5384 * Atom::num(ordinary_outer.production_prefactor_factor());
5385 let ordinary_jet = value_and_t_derivative(ordinary_sum)?;
5386 for (jet_component, exact_expression, ordinary_expression) in
5387 ["value", "first t derivative"]
5388 .into_iter()
5389 .zip(exact_jet)
5390 .zip(ordinary_jet)
5391 .map(|((component, exact), ordinary)| (component, exact, ordinary))
5392 {
5393 let exact_value = evaluate_arb(exact_expression)?;
5394 let ordinary_value = evaluate_arb(ordinary_expression)?;
5395 let distance = (exact_value.clone() - ordinary_value.clone()).norm().re;
5396 let exact_norm = exact_value.clone().norm().re;
5397 let ordinary_norm = ordinary_value.clone().norm().re;
5398 let scale = if exact_norm > ordinary_norm {
5399 exact_norm
5400 } else {
5401 ordinary_norm
5402 };
5403 let relative_distance = if scale.is_zero() {
5404 distance
5405 } else {
5406 distance / scale
5407 };
5408 if !([
5409 &exact_value.re,
5410 &exact_value.im,
5411 &ordinary_value.re,
5412 &ordinary_value.im,
5413 ]
5414 .into_iter()
5415 .all(symbolica::domains::float::SingleFloat::is_finite)
5416 && symbolica::domains::float::SingleFloat::is_finite(&relative_distance)
5417 && relative_distance <= tolerance)
5418 {
5419 failures.push(format!(
5420 "raised alias residue {index} {jet_component} differs between exact and ordinary cograph CFF: exact={exact_value:e}, ordinary={ordinary_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
5421 ));
5422 }
5423 }
5424 }
5425 assert!(
5426 failures.is_empty(),
5427 "raised-alias exact-vs-ordinary failures:\n{}",
5428 failures.join("\n")
5429 );
5430
5431 let denominator_atom = GS.den(
5432 usize::from(uv_edge),
5433 uv_momentum.clone(),
5434 uv_mass_squared.clone(),
5435 uv_full_expr.clone(),
5436 );
5437 let dotted_numerator = &denominator_atom * &retained_dotted_factor;
5438 let lower_denominators = uv_denominators[..2]
5441 .iter()
5442 .cloned()
5443 .chain(cograph_denominators.iter().cloned())
5444 .collect::<Vec<_>>();
5445 let dotted_denominators = uv_denominators
5446 .iter()
5447 .cloned()
5448 .chain(cograph_denominators.iter().cloned())
5449 .collect::<Vec<_>>();
5450
5451 let mut cograph_contract: SuBitGraph = graph.empty_subgraph();
5458 for edge in [EdgeIndex(1), EdgeIndex(2), EdgeIndex(3)] {
5459 cograph_contract.add(graph[&edge].1);
5460 }
5461 failures.clear();
5462 for (uv_component, odd_uv_numerator) in [
5463 ("temporal", GS.emr_mom(uv_edge, GS.cind(0))),
5464 ("spatial", GS.emr_mom(uv_edge, GS.cind(1))),
5465 ] {
5466 let odd_combined_numerator = &retained_dotted_factor * &odd_uv_numerator;
5467 let (odd_uv_source, _) = graph.cff_from_4d_denominators_in_uv_edges(
5468 &uv_denominators[..2],
5469 uv_edges,
5470 &CutSet::empty(graph.n_hedges()),
5471 &options,
5472 &odd_uv_numerator,
5473 None,
5474 )?;
5475 let (odd_combined_source, _) = graph.cff_from_4d_denominators_in_uv_edges(
5476 &lower_denominators,
5477 uv_edges,
5478 &lu_cutset,
5479 &options,
5480 &odd_combined_numerator,
5481 None,
5482 )?;
5483 let ordinary_uv = graph.cff(
5484 &cograph_contract,
5485 &CutSet::empty(graph.n_hedges()),
5486 &OrientationPattern::default(),
5487 &options,
5488 Some(&odd_uv_numerator),
5489 )?;
5490 let exact_uv_sum = exact_sum(&odd_uv_source, &uv_index)?;
5491 let ordinary_uv_sum = ordinary_sum(&graph, &ordinary_uv, &uv_index, &odd_uv_numerator);
5492 for index in exact_outer.terms.keys() {
5493 let combined_jet = value_and_t_derivative(exact_sum(&odd_combined_source, index)?)?;
5494 let exact_factorized_jet =
5495 value_and_t_derivative(exact_sum(&exact_outer, index)? * &exact_uv_sum)?;
5496 let ordinary_factorized_jet = value_and_t_derivative(
5497 ordinary_sum(&graph, &ordinary_outer, index, &retained_dotted_factor)
5498 * &ordinary_uv_sum,
5499 )?;
5500 for (jet_component, combined_expression, exact_expression, ordinary_expression) in
5501 ["value", "first t derivative"]
5502 .into_iter()
5503 .zip(combined_jet)
5504 .zip(exact_factorized_jet)
5505 .zip(ordinary_factorized_jet)
5506 .map(|(((component, combined), exact), ordinary)| {
5507 (component, combined, exact, ordinary)
5508 })
5509 {
5510 let combined_value = evaluate_arb(combined_expression)?;
5511 for (reference, reference_expression) in [
5512 ("factorized exact", exact_expression),
5513 ("factorized ordinary", ordinary_expression),
5514 ] {
5515 let reference_value = evaluate_arb(reference_expression)?;
5516 let distance = (combined_value.clone() - reference_value.clone()).norm().re;
5517 let combined_norm = combined_value.clone().norm().re;
5518 let reference_norm = reference_value.clone().norm().re;
5519 let scale = if combined_norm > reference_norm {
5520 combined_norm
5521 } else {
5522 reference_norm
5523 };
5524 let relative_distance = if scale.is_zero() {
5525 distance
5526 } else {
5527 distance / scale
5528 };
5529 if !([
5530 &combined_value.re,
5531 &combined_value.im,
5532 &reference_value.re,
5533 &reference_value.im,
5534 ]
5535 .into_iter()
5536 .all(symbolica::domains::float::SingleFloat::is_finite)
5537 && symbolica::domains::float::SingleFloat::is_finite(
5538 &relative_distance,
5539 )
5540 && relative_distance <= tolerance)
5541 {
5542 failures.push(format!(
5543 "raised LU residue {index} {jet_component} differs from its {reference} odd-UV-{uv_component} source: combined={combined_value:e}, reference={reference_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
5544 ));
5545 }
5546 }
5547 }
5548 }
5549 }
5550 assert!(
5551 failures.is_empty(),
5552 "raised-LU odd-UV-momentum factorization failures:\n{}",
5553 failures.join("\n")
5554 );
5555
5556 let (retained_source, _) = graph.cff_from_4d_denominators_in_uv_edges(
5557 &lower_denominators,
5558 uv_edges,
5559 &lu_cutset,
5560 &options,
5561 &retained_dotted_factor,
5562 None,
5563 )?;
5564 let (dotted_source, _) = graph.cff_from_4d_denominators_in_uv_edges(
5565 &dotted_denominators,
5566 uv_edges,
5567 &lu_cutset,
5568 &options,
5569 &dotted_numerator,
5570 None,
5571 )?;
5572 assert_eq!(
5573 retained_source.terms.keys().collect::<BTreeSet<_>>(),
5574 dotted_source.terms.keys().collect::<BTreeSet<_>>(),
5575 "powered and lower exact sources must expose the same raised LU orders"
5576 );
5577 failures.clear();
5578 for index in retained_source.terms.keys() {
5579 let dotted_sum = exact_sum(&dotted_source, index)?
5580 .replace(GS.den(W_.a_, W_.b_, W_.c_, W_.d_))
5581 .with(W_.d_);
5582 let retained_sum = exact_sum(&retained_source, index)?;
5583 let dotted_jet = value_and_t_derivative(dotted_sum)?;
5584 let retained_jet = value_and_t_derivative(retained_sum)?;
5585 for (jet_component, dotted_expression, retained_expression) in
5586 ["value", "first t derivative"]
5587 .into_iter()
5588 .zip(dotted_jet)
5589 .zip(retained_jet)
5590 .map(|((component, dotted), retained)| (component, dotted, retained))
5591 {
5592 let dotted_value = evaluate_arb(dotted_expression)?;
5593 let retained_value = evaluate_arb(retained_expression)?;
5594 let distance = (dotted_value.clone() - retained_value.clone()).norm().re;
5595 let dotted_norm = dotted_value.clone().norm().re;
5596 let retained_norm = retained_value.clone().norm().re;
5597 let scale = if dotted_norm > retained_norm {
5598 dotted_norm
5599 } else {
5600 retained_norm
5601 };
5602 let relative_distance = if scale.is_zero() {
5603 distance
5604 } else {
5605 distance / scale
5606 };
5607 if !([
5608 &dotted_value.re,
5609 &dotted_value.im,
5610 &retained_value.re,
5611 &retained_value.im,
5612 ]
5613 .into_iter()
5614 .all(symbolica::domains::float::SingleFloat::is_finite)
5615 && symbolica::domains::float::SingleFloat::is_finite(&relative_distance)
5616 && relative_distance <= tolerance)
5617 {
5618 failures.push(format!(
5619 "raised LU residue {index} {jet_component} differs between D_UV*F/D_UV^3 and F/D_UV^2: dotted={dotted_value:e}, lower={retained_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
5620 ));
5621 }
5622 }
5623 }
5624 assert!(
5625 failures.is_empty(),
5626 "raised-LU powered-alias identity failures:\n{}",
5627 failures.join("\n")
5628 );
5629
5630 let cograph_temporal_numerator = GS.emr_mom(cograph_edge, GS.cind(0)) * &outer_numerator;
5636 let cograph_spatial_numerator = GS.emr_mom(cograph_edge, GS.cind(1)) * &outer_numerator;
5637 let uv_temporal_numerator = GS.emr_mom(uv_edge, GS.cind(0)).pow(2);
5638 let uv_spatial_numerator =
5639 GS.emr_mom(uv_edge, GS.cind(0)) * GS.emr_mom(uv_edge, GS.cind(1));
5640 let cross_component_numerator = &cograph_temporal_numerator * &uv_temporal_numerator
5641 - &cograph_spatial_numerator * &uv_spatial_numerator;
5642 let (cross_component_source, _) = graph.cff_from_4d_denominators_in_uv_edges(
5643 &dotted_denominators,
5644 uv_edges,
5645 &lu_cutset,
5646 &options,
5647 &cross_component_numerator,
5648 None,
5649 )?;
5650 let (cograph_temporal, _) = graph.cff_from_4d_denominators(
5651 &cograph_denominators,
5652 &lu_cutset,
5653 &options,
5654 &cograph_temporal_numerator,
5655 )?;
5656 let (cograph_spatial, _) = graph.cff_from_4d_denominators(
5657 &cograph_denominators,
5658 &lu_cutset,
5659 &options,
5660 &cograph_spatial_numerator,
5661 )?;
5662 let (uv_temporal, _) = graph.cff_from_4d_denominators_in_uv_edges(
5663 &uv_denominators,
5664 uv_edges,
5665 &CutSet::empty(graph.n_hedges()),
5666 &options,
5667 &uv_temporal_numerator,
5668 None,
5669 )?;
5670 let (uv_spatial, _) = graph.cff_from_4d_denominators_in_uv_edges(
5671 &uv_denominators,
5672 uv_edges,
5673 &CutSet::empty(graph.n_hedges()),
5674 &options,
5675 &uv_spatial_numerator,
5676 None,
5677 )?;
5678 let uv_temporal_sum = exact_sum(&uv_temporal, &uv_index)?;
5679 let uv_spatial_sum = exact_sum(&uv_spatial, &uv_index)?;
5680 assert_eq!(
5681 cross_component_source.terms.keys().collect::<BTreeSet<_>>(),
5682 cograph_temporal.terms.keys().collect::<BTreeSet<_>>(),
5683 "the cross-component source must preserve both raised cograph orders"
5684 );
5685 failures.clear();
5686 for index in cograph_temporal.terms.keys() {
5687 let combined_jet = value_and_t_derivative(exact_sum(&cross_component_source, index)?)?;
5688 let factorized_jet = value_and_t_derivative(
5689 exact_sum(&cograph_temporal, index)? * &uv_temporal_sum
5690 - exact_sum(&cograph_spatial, index)? * &uv_spatial_sum,
5691 )?;
5692 for (jet_component, combined_expression, factorized_expression) in
5693 ["value", "first t derivative"]
5694 .into_iter()
5695 .zip(combined_jet)
5696 .zip(factorized_jet)
5697 .map(|((component, combined), factorized)| (component, combined, factorized))
5698 {
5699 let combined_value = evaluate_arb(combined_expression)?;
5700 let factorized_value = evaluate_arb(factorized_expression)?;
5701 let distance = (combined_value.clone() - factorized_value.clone())
5702 .norm()
5703 .re;
5704 let combined_norm = combined_value.clone().norm().re;
5705 let factorized_norm = factorized_value.clone().norm().re;
5706 let scale = if combined_norm > factorized_norm {
5707 combined_norm
5708 } else {
5709 factorized_norm
5710 };
5711 let relative_distance = if scale.is_zero() {
5712 distance
5713 } else {
5714 distance / scale
5715 };
5716 if !([
5717 &combined_value.re,
5718 &combined_value.im,
5719 &factorized_value.re,
5720 &factorized_value.im,
5721 ]
5722 .into_iter()
5723 .all(symbolica::domains::float::SingleFloat::is_finite)
5724 && symbolica::domains::float::SingleFloat::is_finite(&relative_distance)
5725 && relative_distance <= tolerance)
5726 {
5727 failures.push(format!(
5728 "raised LU residue {index} {jet_component} does not factorize for Q_UV^0 (Q_UV^0 Q_outer^0-Q_UV^1 Q_outer^1): combined={combined_value:e}, factorized={factorized_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
5729 ));
5730 }
5731 }
5732 }
5733 assert!(
5734 failures.is_empty(),
5735 "raised-LU cross-component factorization failures:\n{}",
5736 failures.join("\n")
5737 );
5738 Ok(())
5739 }
5740
5741 #[test]
5742 fn exact_cff_quartic_lu_component_factorizes_from_muv_triangle() -> Result<()> {
5743 test_initialise()?;
5744 let mut graph: Graph = dot!(
5745 digraph exact_quartic_lu_muv_product {
5746 node [num=1]
5747
5748 a -> b [id=0 lmb_id=0 particle=scalar_1]
5749 a -> b [id=1 particle=scalar_2]
5750 b -> c [id=4 particle=scalar_0]
5751 c -> d [id=2 lmb_id=1 particle=scalar_0]
5752 c -> d [id=3 particle=scalar_0]
5753 c -> d [id=5 particle=scalar_0]
5754 },
5755 "scalars"
5756 )?;
5757 let options = graph.denominator_only_cff_3d_expression_options();
5758 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
5759 let production = graph.generate_3d_expression_for_integrand(
5760 &[EdgeIndex(4)],
5761 &canonization,
5762 &options,
5763 Some(&Atom::one()),
5764 )?;
5765 let lu_surface = graph
5766 .determine_raised_esurfaces_from_expression(&production.expression)
5767 .raised_groups
5768 .into_iter()
5769 .find(|group| {
5770 group.esurface_ids.iter().any(|surface_id| {
5771 production.expression.surfaces.esurface_cache[*surface_id]
5772 .energies
5773 .iter()
5774 .copied()
5775 .collect::<BTreeSet<_>>()
5776 == BTreeSet::from([EdgeIndex(0), EdgeIndex(1)])
5777 })
5778 })
5779 .expect("the nonrepeated cograph bubble supplies an LU residue surface");
5780 let mut lu_cutset = CutSet::empty(graph.n_hedges());
5781 lu_cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
5782 raised_group: lu_surface,
5783 cut_edge_alternatives: vec![vec![EdgeIndex(0), EdgeIndex(1)]],
5784 });
5785
5786 let cograph_denominators = [EdgeIndex(0), EdgeIndex(1)].map(|edge| FourDDenominator {
5787 source_edge: edge,
5788 momentum: FunctionBuilder::new(GS.emr_mom)
5789 .add_arg(usize::from(edge))
5790 .finish(),
5791 mass_squared: graph.underlying[edge].particle.mass_atom().pow(2),
5792 full_expr: Atom::one(),
5793 });
5794 let muv_mass_squared = Atom::var(GS.m_uv_expansion).pow(2);
5795 let muv_denominators =
5796 [EdgeIndex(2), EdgeIndex(3), EdgeIndex(5)].map(|edge| FourDDenominator {
5797 source_edge: edge,
5798 momentum: FunctionBuilder::new(GS.emr_mom)
5799 .add_arg(usize::from(edge))
5800 .finish(),
5801 mass_squared: muv_mass_squared.clone(),
5802 full_expr: Atom::one(),
5803 });
5804 let quartic_numerator = GS.emr_mom(EdgeIndex(0), GS.cind(0)).pow(4);
5805 let combined_denominators = cograph_denominators
5806 .iter()
5807 .cloned()
5808 .chain(muv_denominators.iter().cloned())
5809 .collect::<Vec<_>>();
5810 let uv_edges = [EdgeIndex(2), EdgeIndex(3), EdgeIndex(5)];
5811
5812 let (combined, _) = graph.cff_from_4d_denominators_in_uv_edges(
5813 &combined_denominators,
5814 uv_edges,
5815 &lu_cutset,
5816 &options,
5817 &quartic_numerator,
5818 None,
5819 )?;
5820 let (cograph, _) = graph.cff_from_4d_denominators(
5821 &cograph_denominators,
5822 &lu_cutset,
5823 &options,
5824 &quartic_numerator,
5825 )?;
5826 let (muv, _) = graph.cff_from_4d_denominators_in_uv_edges(
5827 &muv_denominators,
5828 uv_edges,
5829 &CutSet::empty(graph.n_hedges()),
5830 &options,
5831 &Atom::one(),
5832 None,
5833 )?;
5834 let exact_sum = |cff: &CutCFF| -> Result<Atom> {
5835 Ok(cff
5836 .terms
5837 .values()
5838 .flat_map(|term| {
5839 term.orientations.iter().map(|orientation| {
5840 Ok(orientation.expression.clone()
5841 * term.map_exact_source_numerator(&orientation.orientation, None)?)
5842 })
5843 })
5844 .collect::<Result<Vec<_>>>()?
5845 .into_iter()
5846 .fold(Atom::Zero, |sum, term| sum + term)
5847 * Atom::num(cff.production_prefactor_factor()))
5848 };
5849 let combined_sum = exact_sum(&combined)?;
5850 let isolated_product = exact_sum(&cograph)? * exact_sum(&muv)?;
5851 let evaluate_arb = |expression: Atom| -> Result<Complex<F<ArbPrec>>> {
5852 let mut parameters = vec![Atom::var(GS.pi), Atom::var(GS.m_uv_expansion)];
5853 let zero = F::<ArbPrec>::from(&Rational::from(0));
5854 let mut values = vec![
5855 Complex::new(zero.clone().pi(), zero.clone()),
5856 Complex::new_re(F::from(&Rational::from((7, 5)))),
5857 ];
5858 for edge in 0..graph.underlying.n_edges() {
5859 let edge = EdgeIndex(edge);
5860 parameters.extend([GS.ose(edge), cut_energy(edge)]);
5861 let edge_value = F::from(&Rational::from((edge.0 as i64 + 2, 3)));
5862 values.extend([
5863 Complex::new_re(edge_value.clone()),
5864 Complex::new_re(edge_value),
5865 ]);
5866 for component in 0..=3 {
5867 parameters.push(GS.emr_mom(edge, GS.cind(component)));
5868 values.push(Complex::new_re(F::from(&Rational::from((
5869 edge.0 as i64 + component as i64 + 1,
5870 component as i64 + 2,
5871 )))));
5872 }
5873 }
5874 let rational: ExpressionEvaluator<SymComplex<Fraction<IntegerRing>>> =
5875 expression.evaluator(¶meters).build().map_err(|error| {
5876 eyre::eyre!("failed to build mixed-component evaluator: {error}")
5877 })?;
5878 let mut arb: ExpressionEvaluator<Complex<F<ArbPrec>>> =
5879 rational.map_coeff(&|coefficient| {
5880 Complex::new(F::from(&coefficient.re), F::from(&coefficient.im))
5881 });
5882 Ok(arb.evaluate_single(&values))
5883 };
5884 let combined_value = evaluate_arb(combined_sum)?;
5885 let isolated_value = evaluate_arb(isolated_product)?;
5886 let distance = (combined_value.clone() - isolated_value.clone()).norm().re;
5887 let combined_norm = combined_value.clone().norm().re;
5888 let isolated_norm = isolated_value.clone().norm().re;
5889 let scale = if combined_norm > isolated_norm {
5890 combined_norm
5891 } else {
5892 isolated_norm
5893 };
5894 let relative_distance = distance / scale;
5895 let tolerance = F(ArbPrec::default().epsilon()).sqrt().sqrt().sqrt();
5896 assert!(
5897 relative_distance <= tolerance,
5898 "a nonrepeated quartic LU component must factorize from its pure MUV triangle: combined={combined_value:e}, isolated={isolated_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
5899 );
5900 Ok(())
5901 }
5902
5903 #[test]
5904 fn connected_parent_uv_limit_matches_uncorrected_child_times_outer() -> Result<()> {
5905 test_initialise()?;
5906 let check = |label: &str,
5907 mut graph: Graph,
5908 expected_loop_count: usize,
5909 selected_lu_support: Option<&[EdgeIndex]>|
5910 -> Result<()> {
5911 let uv_edges = [EdgeIndex(1), EdgeIndex(2)];
5912 let mut uv_contract: SuBitGraph = graph.empty_subgraph();
5913 for edge in uv_edges {
5914 uv_contract.add(graph[&edge].1);
5915 }
5916 let uv_subgraph =
5917 InternalSubGraph::cleaned_filter_optimist(uv_contract.clone(), graph.as_ref());
5918 assert_eq!(graph.get_loop_number(), expected_loop_count);
5919 assert_eq!(graph.n_loops(&uv_contract), 1);
5920
5921 let options = graph.denominator_only_cff_3d_expression_options();
5922 let cutset = CutSet::empty(graph.n_hedges());
5923 let pattern = OrientationPattern::default();
5924 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
5925 let root_contract = graph.empty_subgraph::<SuBitGraph>();
5926 let root = graph.cff(&root_contract, &cutset, &pattern, &options, None)?;
5927 let outer = graph.cff(&uv_contract, &cutset, &pattern, &options, None)?;
5928
5929 let crown = graph.dummy_stripped_external_flows_of(&uv_subgraph);
5930 let child_lmb = graph.try_compatible_sub_lmb(
5931 &uv_subgraph,
5932 crown.clone(),
5933 &graph.loop_momentum_basis,
5934 )?;
5935 assert_eq!(child_lmb.loop_edges.len(), 1);
5936 let muv_mass_squared = Atom::var(GS.m_uv_expansion).pow(2);
5937 let child_denominators = uv_edges.map(|edge| FourDDenominator {
5938 source_edge: edge,
5939 momentum: FunctionBuilder::new(GS.emr_mom)
5940 .add_arg(usize::from(edge))
5941 .finish(),
5942 mass_squared: muv_mass_squared.clone(),
5943 full_expr: Atom::one(),
5944 });
5945 let (child, _) = graph.cff_from_4d_denominators_in_uv_sub_lmb(
5946 &child_denominators,
5947 uv_edges,
5948 crown.included_iter(),
5949 &child_lmb,
5950 ExactUvSubLmbFrame::RetainedPhysicalCrown,
5951 &cutset,
5952 &options,
5953 &Atom::one(),
5954 None,
5955 &[],
5956 )?;
5957
5958 let ordinary_raw_sum = |cff: &CutCFF| {
5959 cff.terms
5960 .values()
5961 .flat_map(|term| &term.orientations)
5962 .fold(Atom::Zero, |sum, orientation| sum + &orientation.expression)
5963 };
5964 let exact_raw_sum = |cff: &CutCFF| -> Result<Atom> {
5965 Ok(cff
5966 .terms
5967 .values()
5968 .flat_map(|term| {
5969 term.orientations.iter().map(|orientation| {
5970 Ok(orientation.expression.clone()
5971 * term
5972 .map_exact_source_numerator(&orientation.orientation, None)?)
5973 })
5974 })
5975 .collect::<Result<Vec<_>>>()?
5976 .into_iter()
5977 .fold(Atom::Zero, |sum, term| sum + term))
5978 };
5979 let root_bridge = root.production_prefactor_factor();
5980 let outer_bridge = outer.production_prefactor_factor();
5981 let child_bridge = child.production_prefactor_factor();
5982 let parent_raw = ordinary_raw_sum(&root);
5983 let components_raw = ordinary_raw_sum(&outer) * exact_raw_sum(&child)?;
5984
5985 let relative_bridge = root_bridge * outer_bridge * child_bridge;
5992 let connected_frame = components_raw.clone() * Atom::num(relative_bridge);
5993 let parent = parent_raw.clone() * Atom::num(root_bridge);
5994 let factorized = components_raw.clone() * Atom::num(outer_bridge * child_bridge);
5995
5996 let zero = F::<ArbPrec>::from(&Rational::from(0));
5997 let one = F::<ArbPrec>::from(&Rational::from(1));
5998 let muv_mass = F::<ArbPrec>::from(&Rational::from((7, 5)));
5999 let outer_spatial = F::<ArbPrec>::from(&Rational::from((3, 7)));
6000 let extra_outer_spatial = F::<ArbPrec>::from(&Rational::from((2, 5)));
6001 let mut parameters = vec![Atom::var(GS.pi), Atom::var(GS.m_uv_expansion)];
6002 let mut edge_parameters = Vec::new();
6003 for edge in 0..graph.underlying.n_edges() {
6004 let edge = EdgeIndex(edge);
6005 parameters.extend([GS.ose(edge), cut_energy(edge), GS.emr_mom(edge, GS.cind(0))]);
6006 for component in 1..=3 {
6007 parameters.push(GS.emr_mom(edge, GS.cind(component)));
6008 }
6009 edge_parameters.push(edge);
6010 }
6011 let values_at = |scale: &F<ArbPrec>| {
6012 let child_spatial = [scale.clone(), outer_spatial.clone() - scale];
6013 let mut values = vec![
6014 Complex::new(zero.clone().pi(), zero.clone()),
6015 Complex::new_re(muv_mass.clone()),
6016 ];
6017 for edge in &edge_parameters {
6018 let child_index = uv_edges.iter().position(|candidate| candidate == edge);
6019 let (energy, spatial) = if let Some(child_index) = child_index {
6020 let spatial = child_spatial[child_index].clone();
6021 (
6022 (&muv_mass * &muv_mass + &spatial * &spatial).sqrt(),
6023 spatial,
6024 )
6025 } else if let Some(spatial) = match (expected_loop_count, *edge) {
6026 (_, EdgeIndex(3)) => Some(outer_spatial.clone()),
6027 (2, EdgeIndex(4)) => Some(-&outer_spatial),
6028 (3, EdgeIndex(4)) => Some(&extra_outer_spatial - &outer_spatial),
6029 (3, EdgeIndex(5)) => Some(extra_outer_spatial.clone()),
6030 (3, EdgeIndex(6)) => Some(-&extra_outer_spatial),
6031 _ => None,
6032 } {
6033 ((&one + &spatial * &spatial).sqrt(), spatial)
6034 } else {
6035 (F::<ArbPrec>::from(&Rational::from(2)), zero.clone())
6036 };
6037 values.extend([
6038 Complex::new_re(energy.clone()),
6039 Complex::new_re(energy.clone()),
6040 Complex::new_re(energy),
6041 ]);
6042 for component in 1..=3 {
6043 values.push(Complex::new_re(if component == 1 {
6044 spatial.clone()
6045 } else {
6046 zero.clone()
6047 }));
6048 }
6049 }
6050 values
6051 };
6052 let evaluate_arb =
6053 |expression: Atom, values: &[Complex<F<ArbPrec>>]| -> Result<Complex<F<ArbPrec>>> {
6054 let rational: ExpressionEvaluator<SymComplex<Fraction<IntegerRing>>> =
6055 expression.evaluator(¶meters).build().map_err(|error| {
6056 eyre::eyre!(
6057 "failed to build {label} connected-parent UV-bridge evaluator: {error}"
6058 )
6059 })?;
6060 let mut arb: ExpressionEvaluator<Complex<F<ArbPrec>>> =
6061 rational.map_coeff(&|coefficient| {
6062 Complex::new(F::from(&coefficient.re), F::from(&coefficient.im))
6063 });
6064 Ok(arb.evaluate_single(values))
6065 };
6066 let scale = F::<ArbPrec>::from(&Rational::from(10).pow(45));
6067 let uv_values = values_at(&scale);
6068 let measure = &scale * &scale * &scale;
6069 let parent_raw = evaluate_arb(parent_raw, &uv_values)? * &measure;
6070 let connected_frame = evaluate_arb(connected_frame, &uv_values)? * &measure;
6071 let parent = evaluate_arb(parent, &uv_values)? * &measure;
6072 let factorized = evaluate_arb(factorized, &uv_values)? * &measure;
6073 let raw_distance = (parent_raw.clone() - connected_frame.clone()).norm().re;
6074 let parent_raw_norm = parent_raw.clone().norm().re;
6075 let connected_frame_norm = connected_frame.clone().norm().re;
6076 let raw_norm = if parent_raw_norm > connected_frame_norm {
6077 parent_raw_norm
6078 } else {
6079 connected_frame_norm
6080 };
6081 let raw_relative_distance = if raw_norm.is_zero() {
6082 raw_distance
6083 } else {
6084 raw_distance / raw_norm
6085 };
6086 let distance = (parent.clone() - factorized.clone()).norm().re;
6087 let parent_norm = parent.clone().norm().re;
6088 let factorized_norm = factorized.clone().norm().re;
6089 let norm = if parent_norm > factorized_norm {
6090 parent_norm
6091 } else {
6092 factorized_norm
6093 };
6094 let relative_distance = if norm.is_zero() {
6095 distance
6096 } else {
6097 distance / norm.clone()
6098 };
6099 let tolerance = F(ArbPrec::default().epsilon()).sqrt().sqrt().sqrt();
6100 assert!(
6101 raw_relative_distance <= tolerance,
6102 "{label}: raw connected-parent UV frame differs from raw child times outer after one relative/root bridge: parent={parent_raw:e}, connected frame={connected_frame:e}, bridge={relative_bridge}, relative delta={raw_relative_distance:e}, tolerance={tolerance:e}",
6103 );
6104 assert!(
6105 relative_distance <= tolerance,
6106 "{label}: connected parent UV limit differs from its retained-crown exact child times reduced outer: parent={parent:e}, factorized={factorized:e}, standalone bridge ratio={relative_bridge}, relative delta={relative_distance:e}, tolerance={tolerance:e}",
6107 );
6108 if let Some(lu_support) = selected_lu_support {
6109 let quartic_owner = EdgeIndex(3);
6110 let outer_momentum_squared = GS.emr_mom(quartic_owner, GS.cind(0)).pow(2)
6111 - (1..=3)
6112 .map(|component| GS.emr_mom(quartic_owner, GS.cind(component)).pow(2))
6113 .fold(Atom::Zero, |sum, component| sum + component);
6114 let quartic_numerator = outer_momentum_squared.pow(2);
6115 let fixed_selected_spatial_quartic = (1..=3)
6121 .map(|component| GS.emr_mom(quartic_owner, GS.cind(component)).pow(2))
6122 .fold(Atom::Zero, |sum, component| sum - component)
6123 .pow(2);
6124 let root_generated = graph.generate_3d_expression_for_integrand(
6125 &[],
6126 &canonization,
6127 &options,
6128 Some(&quartic_numerator),
6129 )?;
6130 let target_support = lu_support.iter().copied().collect::<BTreeSet<_>>();
6131 let lu_surface = graph
6132 .determine_raised_esurfaces_from_expression(&root_generated.expression)
6133 .raised_groups
6134 .into_iter()
6135 .find(|group| {
6136 group.esurface_ids.iter().any(|surface_id| {
6137 root_generated.expression.surfaces.esurface_cache[*surface_id]
6138 .energies
6139 .iter()
6140 .copied()
6141 .collect::<BTreeSet<_>>()
6142 .eq(&target_support)
6143 })
6144 })
6145 .ok_or_else(|| {
6146 eyre::eyre!(
6147 "{label}: no selected LU surface has physical support {target_support:?}"
6148 )
6149 })?;
6150 let mut selected_cutset = CutSet::empty(graph.n_hedges());
6151 selected_cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
6152 raised_group: lu_surface,
6153 cut_edge_alternatives: vec![lu_support.to_vec()],
6154 });
6155 let variant_root = graph.cff(
6156 &root_contract,
6157 &selected_cutset,
6158 &pattern,
6159 &options,
6160 Some(&quartic_numerator),
6161 )?;
6162 let variant_outer = graph.cff(
6163 &uv_contract,
6164 &selected_cutset,
6165 &pattern,
6166 &options,
6167 Some(&quartic_numerator),
6168 )?;
6169 let fixed_order_one_raw_sum = |cff: &CutCFF| {
6170 cff.terms
6171 .iter()
6172 .filter(|(index, _)| index.lu_cut_order == Some(1))
6173 .flat_map(|(_, term)| {
6174 term.orientations.iter().map(|orientation| {
6175 orientation.expression.clone()
6176 * fixed_selected_spatial_quartic.clone()
6177 })
6178 })
6179 .fold(Atom::Zero, |sum, term| sum + term)
6180 };
6181 let variant_parent_raw = fixed_order_one_raw_sum(&variant_root);
6182 let variant_components_raw =
6183 fixed_order_one_raw_sum(&variant_outer) * exact_raw_sum(&child)?;
6184 let variant_connected_frame =
6185 variant_components_raw.clone() * Atom::num(relative_bridge);
6186 let variant_parent = variant_parent_raw.clone() * Atom::num(root_bridge);
6187 let variant_factorized =
6188 variant_components_raw * Atom::num(outer_bridge * child_bridge);
6189 let variant_parent_raw = evaluate_arb(variant_parent_raw, &uv_values)? * &measure;
6190 let variant_connected_frame =
6191 evaluate_arb(variant_connected_frame, &uv_values)? * &measure;
6192 let variant_parent = evaluate_arb(variant_parent, &uv_values)? * &measure;
6193 let variant_factorized = evaluate_arb(variant_factorized, &uv_values)? * &measure;
6194 let relative_delta = |left: &Complex<F<ArbPrec>>, right: &Complex<F<ArbPrec>>| {
6195 let distance = (left.clone() - right.clone()).norm().re;
6196 let left_norm = left.clone().norm().re;
6197 let right_norm = right.clone().norm().re;
6198 let scale = if left_norm > right_norm {
6199 left_norm
6200 } else {
6201 right_norm
6202 };
6203 if scale.is_zero() {
6204 distance
6205 } else {
6206 distance / scale
6207 }
6208 };
6209 let raw_delta = relative_delta(&variant_parent_raw, &variant_connected_frame);
6210 let production_delta = relative_delta(&variant_parent, &variant_factorized);
6211 assert!(
6212 raw_delta <= tolerance,
6213 "{label}: selected-LU quartic raw connected frame differs from child times reduced outer: parent={variant_parent_raw:e}, connected frame={variant_connected_frame:e}, relative delta={raw_delta:e}, tolerance={tolerance:e}",
6214 );
6215 assert!(
6216 production_delta <= tolerance,
6217 "{label}: selected-LU quartic connected-parent UV limit differs from independently bridged child times reduced outer: parent={variant_parent:e}, factorized={variant_factorized:e}, relative delta={production_delta:e}, tolerance={tolerance:e}",
6218 );
6219 }
6220 Ok(())
6221 };
6222
6223 check(
6224 "opposite outer parity",
6225 dot!(digraph connected_parent_uv_bridge {
6226 edge [num=1 mass=1]
6227 node [num=1]
6228 incoming [style=invis]
6229 outgoing [style=invis]
6230
6231 incoming -> a [id=0]
6232 a -> u [id=1 lmb_id=0]
6233 a -> u [id=2]
6234 u -> b [id=3 lmb_id=1]
6235 a -> b [id=4]
6236 b -> outgoing [id=5]
6237 })?,
6238 2,
6239 None,
6240 )?;
6241 check(
6242 "GL24-matching outer parity",
6243 dot!(digraph connected_parent_uv_bridge_three_loop {
6244 edge [num=1 mass=1]
6245 node [num=1]
6246 incoming [style=invis]
6247 outgoing [style=invis]
6248
6249 incoming -> a [id=0]
6250 a -> u [id=1 lmb_id=0]
6251 a -> u [id=2]
6252 u -> b [id=3 lmb_id=1]
6253 a -> b [id=4]
6254 b -> c [id=5 lmb_id=2]
6255 a -> c [id=6]
6256 c -> outgoing [id=7]
6257 })?,
6258 3,
6259 Some(&[EdgeIndex(5), EdgeIndex(6)]),
6260 )?;
6261 Ok(())
6262 }
6263
6264 #[test]
6265 fn gl24_selected_quartic_residue_uses_the_reduced_outer_frame() -> Result<()> {
6266 test_initialise()?;
6267 let mut graph: Graph = dot!(digraph gl24_selected_zero_sample {
6268 edge [num=1 mass=1]
6269 node [num=1]
6270
6271 v0 -> v1 [id=0 is_cut=0]
6272 v0 -> v3 [id=1 lmb_id=0]
6273 v0 -> v5 [id=2]
6274 v1 -> v2 [id=3 lmb_id=1]
6275 v1 -> v3 [id=4]
6276 v2 -> v4 [id=5 lmb_id=2]
6277 v2 -> v4 [id=6]
6278 v3 -> v5 [id=7]
6279 v4 -> v5 [id=8 mass=2]
6280 })?;
6281 assert_eq!(graph.get_loop_number(), 3);
6282 assert_eq!(graph.get_edges_in_initial_state_cut(), [EdgeIndex(0)]);
6283
6284 let uv_edges = [EdgeIndex(5), EdgeIndex(6)];
6285 let mut uv_contract: SuBitGraph = graph.empty_subgraph();
6286 for edge in uv_edges {
6287 uv_contract.add(graph[&edge].1);
6288 }
6289 let production_contract = graph.tree_edges.subtract(&graph.initial_state_cut);
6290 let production_contract_edges = graph.paired_edges(&production_contract);
6291 let outer_contract = uv_contract.union(&production_contract);
6292 let options = graph.denominator_only_cff_3d_expression_options();
6293 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
6294 let quartic_owner = EdgeIndex(1);
6295 let momentum_squared = GS.emr_mom(quartic_owner, GS.cind(0)).pow(2)
6296 - (1..=3)
6297 .map(|component| GS.emr_mom(quartic_owner, GS.cind(component)).pow(2))
6298 .fold(Atom::Zero, |sum, component| sum + component);
6299 let quartic_numerator = momentum_squared.pow(2);
6300 let root_generated = graph.generate_3d_expression_for_integrand(
6301 &production_contract_edges,
6302 &canonization,
6303 &options,
6304 Some(&quartic_numerator),
6305 )?;
6306 let target_support = [EdgeIndex(3), EdgeIndex(4)]
6307 .into_iter()
6308 .collect::<BTreeSet<_>>();
6309 let lu_surface = graph
6310 .determine_raised_esurfaces_from_expression(&root_generated.expression)
6311 .raised_groups
6312 .into_iter()
6313 .find(|group| {
6314 group.esurface_ids.iter().any(|surface_id| {
6315 root_generated.expression.surfaces.esurface_cache[*surface_id]
6316 .energies
6317 .iter()
6318 .copied()
6319 .collect::<BTreeSet<_>>()
6320 .eq(&target_support)
6321 })
6322 })
6323 .ok_or_else(|| eyre::eyre!("GL24 q3/q4 LU surface was not generated"))?;
6324 let mut cutset = CutSet::empty(graph.n_hedges());
6325 cutset.residue_selector.lu = Some(crate::graph::cuts::LuCutSelection {
6326 raised_group: lu_surface,
6327 cut_edge_alternatives: vec![vec![EdgeIndex(3), EdgeIndex(4)]],
6328 });
6329 let pattern = OrientationPattern::default();
6330 let root = graph.cff(
6331 &production_contract,
6332 &cutset,
6333 &pattern,
6334 &options,
6335 Some(&quartic_numerator),
6336 )?;
6337 let outer = graph.cff(
6338 &outer_contract,
6339 &cutset,
6340 &pattern,
6341 &options,
6342 Some(&quartic_numerator),
6343 )?;
6344 let mapped_order_one = |cff: &CutCFF| {
6349 cff.terms
6350 .iter()
6351 .filter(|(index, _)| index.lu_cut_order == Some(1))
6352 .flat_map(|(_, term)| &term.orientations)
6353 .fold(Atom::Zero, |sum, orientation| {
6354 sum + &orientation.expression
6355 * quartic_numerator.replace_multiple(
6356 orientation.orientation.energy_replacements_gs(&graph),
6357 )
6358 })
6359 };
6360 let root_raw = mapped_order_one(&root);
6361 let outer_raw = mapped_order_one(&outer);
6362 assert!(!root_raw.is_zero() && !outer_raw.is_zero());
6363
6364 let uv_subgraph =
6365 InternalSubGraph::cleaned_filter_optimist(uv_contract.clone(), graph.as_ref());
6366 let crown = graph.dummy_stripped_external_flows_of(&uv_subgraph);
6367 let child_lmb = graph.try_compatible_sub_lmb(
6368 &uv_subgraph,
6369 crown.clone(),
6370 &graph.loop_momentum_basis,
6371 )?;
6372 let muv_mass_squared = Atom::var(GS.m_uv_expansion).pow(2);
6373 let child_denominators = uv_edges.map(|edge| FourDDenominator {
6374 source_edge: edge,
6375 momentum: FunctionBuilder::new(GS.emr_mom)
6376 .add_arg(usize::from(edge))
6377 .finish(),
6378 mass_squared: muv_mass_squared.clone(),
6379 full_expr: Atom::one(),
6380 });
6381 let child_cutset = CutSet::empty(graph.n_hedges());
6382 let (child, _) = graph.cff_from_4d_denominators_in_uv_sub_lmb(
6383 &child_denominators,
6384 uv_edges,
6385 crown.included_iter(),
6386 &child_lmb,
6387 ExactUvSubLmbFrame::RetainedPhysicalCrown,
6388 &child_cutset,
6389 &options,
6390 &Atom::one(),
6391 None,
6392 &[],
6393 )?;
6394 let exact_raw = |cff: &CutCFF| -> Result<Atom> {
6395 Ok(cff
6396 .terms
6397 .values()
6398 .flat_map(|term| {
6399 term.orientations.iter().map(|orientation| {
6400 Ok(orientation.expression.clone()
6401 * term.map_exact_source_numerator(&orientation.orientation, None)?)
6402 })
6403 })
6404 .collect::<Result<Vec<_>>>()?
6405 .into_iter()
6406 .fold(Atom::Zero, |sum, term| sum + term))
6407 };
6408 let child_raw = exact_raw(&child)?;
6409 let components_raw = outer_raw * child_raw;
6410 let root_production = root_raw.clone() * Atom::num(root.production_prefactor_factor());
6411 let components_production = components_raw.clone()
6412 * Atom::num(outer.production_prefactor_factor() * child.production_prefactor_factor());
6413
6414 let zero = F::<ArbPrec>::from(&Rational::from(0));
6415 let one = F::<ArbPrec>::from(&Rational::from(1));
6416 let muv_mass = F::<ArbPrec>::from(&Rational::from((7, 5)));
6417 let k0 = F::<ArbPrec>::from(&Rational::from((3, 7)));
6418 let k1 = F::<ArbPrec>::from(&Rational::from((2, 5)));
6419 let mut parameters = vec![Atom::var(GS.pi), Atom::var(GS.m_uv_expansion)];
6420 for edge in 0..graph.underlying.n_edges() {
6421 let edge = EdgeIndex(edge);
6422 parameters.extend([GS.ose(edge), cut_energy(edge), GS.emr_mom(edge, GS.cind(0))]);
6423 for component in 1..=3 {
6424 parameters.push(GS.emr_mom(edge, GS.cind(component)));
6425 }
6426 }
6427 let values_at = |scale: &F<ArbPrec>| {
6428 let mut values = vec![
6429 Complex::new(zero.clone().pi(), zero.clone()),
6430 Complex::new_re(muv_mass.clone()),
6431 ];
6432 for edge in 0..graph.underlying.n_edges() {
6433 let edge = EdgeIndex(edge);
6434 let spatial = match edge {
6435 EdgeIndex(1) => k0.clone(),
6436 EdgeIndex(2) => -&k0,
6437 EdgeIndex(3) | EdgeIndex(8) => k1.clone(),
6438 EdgeIndex(4) => -&k1,
6439 EdgeIndex(5) => scale.clone(),
6440 EdgeIndex(6) => &k1 - scale,
6441 EdgeIndex(7) => &k0 - &k1,
6442 _ => zero.clone(),
6443 };
6444 let mass = match edge {
6445 EdgeIndex(5) | EdgeIndex(6) => muv_mass.clone(),
6446 EdgeIndex(8) => F::<ArbPrec>::from(&Rational::from(2)),
6447 _ => one.clone(),
6448 };
6449 let energy = if edge == EdgeIndex(0) {
6450 F::<ArbPrec>::from(&Rational::from(2))
6451 } else {
6452 (&mass * &mass + &spatial * &spatial).sqrt()
6453 };
6454 values.extend([
6455 Complex::new_re(energy.clone()),
6456 Complex::new_re(energy.clone()),
6457 Complex::new_re(energy),
6458 ]);
6459 for component in 1..=3 {
6460 values.push(Complex::new_re(if component == 1 {
6461 spatial.clone()
6462 } else {
6463 zero.clone()
6464 }));
6465 }
6466 }
6467 values
6468 };
6469 let evaluate_arb =
6470 |expression: Atom, values: &[Complex<F<ArbPrec>>]| -> Result<Complex<F<ArbPrec>>> {
6471 let rational: ExpressionEvaluator<SymComplex<Fraction<IntegerRing>>> =
6472 expression.evaluator(¶meters).build()?;
6473 let mut arb: ExpressionEvaluator<Complex<F<ArbPrec>>> =
6474 rational.map_coeff(&|coefficient| {
6475 Complex::new(F::from(&coefficient.re), F::from(&coefficient.im))
6476 });
6477 Ok(arb.evaluate_single(values))
6478 };
6479 let scale = F::<ArbPrec>::from(&Rational::from(10).pow(45));
6480 let values = values_at(&scale);
6481 let measure = &scale * &scale * &scale;
6482 let root_raw = evaluate_arb(root_raw, &values)? * &measure;
6483 let components_raw = evaluate_arb(components_raw, &values)? * &measure;
6484 let root_production = evaluate_arb(root_production, &values)? * &measure;
6485 let components_production = evaluate_arb(components_production, &values)? * &measure;
6486 assert!(
6487 !components_production.clone().norm().re.is_zero(),
6488 "the factorized GL24 quartic residue must provide a nonzero UV-limit oracle"
6489 );
6490 let relative_delta = |left: &Complex<F<ArbPrec>>, right: &Complex<F<ArbPrec>>| {
6491 let distance = (left.clone() - right.clone()).norm().re;
6492 let left_norm = left.clone().norm().re;
6493 let right_norm = right.clone().norm().re;
6494 let norm = if left_norm > right_norm {
6495 left_norm
6496 } else {
6497 right_norm
6498 };
6499 if norm.is_zero() {
6500 distance
6501 } else {
6502 distance / norm
6503 }
6504 };
6505 let tolerance = F(ArbPrec::default().epsilon()).sqrt().sqrt().sqrt();
6506 let relative_bridge = root.production_prefactor_factor()
6507 * outer.production_prefactor_factor()
6508 * child.production_prefactor_factor();
6509 let raw_delta = relative_delta(
6510 &root_raw,
6511 &(&components_raw * F::<ArbPrec>::from(&Rational::from(relative_bridge))),
6512 );
6513 let production_delta = relative_delta(&root_production, &components_production);
6514 assert!(
6515 raw_delta <= tolerance,
6516 "GL24 complete selected quartic residue does not carry the relative parent/outer/child frame: root={root_raw:e}, child*outer={components_raw:e}, relative delta={raw_delta:e}, tolerance={tolerance:e}",
6517 );
6518 assert!(
6519 production_delta <= tolerance,
6520 "GL24 complete selected quartic residue differs from child times reduced outer: root={root_production:e}, child*outer={components_production:e}, relative delta={production_delta:e}, tolerance={tolerance:e}",
6521 );
6522 Ok(())
6523 }
6524
6525 #[test]
6526 fn exact_original_source_matches_production_cff_after_projection() -> Result<()> {
6527 test_initialise()?;
6528 let mut graph: Graph = dot!(digraph exact_root {
6529 edge [num=1 mass=1]
6530 node [num=1]
6531 incoming [style=invis]
6532 outgoing [style=invis]
6533
6534 incoming -> a [id=2]
6535 a -> b [id=0 lmb_id=0]
6536 a -> b [id=1]
6537 b -> outgoing [id=3]
6538 })?;
6539 let options = graph.denominator_only_cff_3d_expression_options();
6540 let cutset = CutSet::empty(graph.n_hedges());
6541 let contract: SuBitGraph = graph.empty_subgraph();
6542 let ordinary = graph.cff(
6543 &contract,
6544 &cutset,
6545 &OrientationPattern::default(),
6546 &options,
6547 None,
6548 )?;
6549 let denominators = [EdgeIndex::from(0), EdgeIndex::from(1)].map(|edge| FourDDenominator {
6550 source_edge: edge,
6551 momentum: FunctionBuilder::new(GS.emr_mom)
6552 .add_arg(usize::from(edge))
6553 .finish(),
6554 mass_squared: graph.underlying[edge].particle.mass_atom().pow(2),
6555 full_expr: Atom::one(),
6556 });
6557 let (exact, _) =
6558 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &Atom::one())?;
6559 let ordinary_sum = ordinary
6560 .terms
6561 .values()
6562 .flat_map(|term| &term.orientations)
6563 .fold(Atom::Zero, |sum, orientation| sum + &orientation.expression)
6564 * Atom::num(ordinary.production_prefactor_factor());
6565 let exact_sum = exact
6566 .terms
6567 .values()
6568 .flat_map(|term| &term.orientations)
6569 .fold(Atom::Zero, |sum, orientation| sum + &orientation.expression)
6570 * Atom::num(exact.production_prefactor_factor());
6571 let rational =
6578 |numerator: i64, denominator: i64| Atom::num(numerator) / Atom::num(denominator);
6579 for (spatial_0, energy_0, spatial_1, energy_1, external_energy) in [
6580 (
6581 Atom::Zero,
6582 Atom::one(),
6583 Atom::Zero,
6584 Atom::one(),
6585 rational(1, 3),
6586 ),
6587 (
6588 rational(3, 4),
6589 rational(5, 4),
6590 rational(4, 3),
6591 rational(5, 3),
6592 rational(2, 5),
6593 ),
6594 ] {
6595 let at_point = |mut expression: Atom| {
6596 for (edge, spatial, energy) in [
6597 (EdgeIndex(0), spatial_0.clone(), energy_0.clone()),
6598 (EdgeIndex(1), spatial_1.clone(), energy_1.clone()),
6599 ] {
6600 expression = expression
6601 .replace(GS.ose(edge))
6602 .with(energy)
6603 .replace(GS.emr_mom(edge, GS.cind(1)))
6604 .with(spatial);
6605 for spatial_index in 2..=3 {
6606 expression = expression
6607 .replace(GS.emr_mom(edge, GS.cind(spatial_index)))
6608 .with(Atom::Zero);
6609 }
6610 }
6611 expression
6612 .replace(GS.emr_mom(EdgeIndex(3), GS.cind(0)))
6613 .with(external_energy.clone())
6614 .expand()
6615 };
6616 let difference =
6617 (at_point(exact_sum.clone()) - at_point(ordinary_sum.clone())).expand();
6618 assert!(
6619 difference.is_zero(),
6620 "source-local canonical orientations must reproduce the ordinary CFF at exact on-shell points: {difference}"
6621 );
6622 }
6623 Ok(())
6624 }
6625
6626 #[test]
6627 fn exact_nonliteral_physical_mass_alias_keeps_repeated_frame_and_arb_value() -> Result<()> {
6628 test_initialise()?;
6629 let mut graph: Graph = dot!(
6630 digraph exact_nonliteral_physical_mass_alias {
6631 node [num=1]
6632 a -> b [id=0 lmb_id=0 particle=scalar_1]
6633 a -> b [id=1 particle=scalar_1]
6634 },
6635 "scalars"
6636 )?;
6637 let edge = EdgeIndex(0);
6638 let momentum = FunctionBuilder::new(GS.emr_mom)
6639 .add_arg(usize::from(edge))
6640 .finish();
6641 let mass_squared = graph.underlying[edge].particle.mass_atom().pow(2);
6642 let numerator = GS.emr_mom(edge, GS.cind(0)).pow(2)
6643 - (1..=3).fold(mass_squared.clone(), |norm_squared, spatial_index| {
6644 norm_squared + GS.emr_mom(edge, GS.cind(spatial_index)).pow(2)
6645 });
6646 let literal = [EdgeIndex(0), EdgeIndex(1)].map(|source_edge| FourDDenominator {
6647 source_edge,
6648 momentum: FunctionBuilder::new(GS.emr_mom)
6649 .add_arg(usize::from(source_edge))
6650 .finish(),
6651 mass_squared: graph.underlying[source_edge].particle.mass_atom().pow(2),
6652 full_expr: Atom::one(),
6653 });
6654 let mut nonliteral = literal.clone();
6655 nonliteral[1].momentum = -momentum;
6656
6657 let options = graph.denominator_only_cff_3d_expression_options();
6658 let cutset = CutSet::empty(graph.n_hedges());
6659 let (literal_cff, _) =
6660 graph.cff_from_4d_denominators(&literal, &cutset, &options, &numerator)?;
6661 let (nonliteral_cff, _) =
6662 graph.cff_from_4d_denominators(&nonliteral, &cutset, &options, &numerator)?;
6663 let exact_sum = |cff: &CutCFF| -> Result<Atom> {
6664 Ok(cff
6665 .terms
6666 .values()
6667 .flat_map(|term| {
6668 term.orientations.iter().map(|orientation| {
6669 Ok(orientation.expression.clone()
6670 * term.map_exact_source_numerator(&orientation.orientation, None)?)
6671 })
6672 })
6673 .collect::<Result<Vec<_>>>()?
6674 .into_iter()
6675 .fold(Atom::Zero, |sum, term| sum + term)
6676 * Atom::num(cff.production_prefactor_factor()))
6677 };
6678 let fixed_point = |mut expression: Atom| {
6679 expression = expression
6680 .replace(graph.underlying[edge].particle.mass_atom())
6681 .with(Atom::one());
6682 for (source_edge, spatial) in [
6683 (EdgeIndex(0), Atom::num(Rational::from((3, 4)))),
6684 (EdgeIndex(1), Atom::num(Rational::from((-3, 4)))),
6685 ] {
6686 expression = expression
6687 .replace(GS.emr_mom(source_edge, GS.cind(1)))
6688 .with(spatial)
6689 .replace(GS.ose(source_edge))
6690 .with(Atom::num(Rational::from((5, 4))));
6691 for spatial_index in 2..=3 {
6692 expression = expression
6693 .replace(GS.emr_mom(source_edge, GS.cind(spatial_index)))
6694 .with(Atom::Zero);
6695 }
6696 }
6697 expression
6698 };
6699 let evaluate_arb = |expression: Atom| -> Result<Complex<F<ArbPrec>>> {
6700 let parameters = [Atom::var(GS.pi)];
6701 let rational: ExpressionEvaluator<SymComplex<Fraction<IntegerRing>>> = expression
6702 .evaluator(¶meters)
6703 .build()
6704 .map_err(|error| eyre::eyre!("failed to build Q-spelling evaluator: {error}"))?;
6705 let mut arb: ExpressionEvaluator<Complex<F<ArbPrec>>> =
6706 rational.map_coeff(&|coefficient| {
6707 Complex::new(F::from(&coefficient.re), F::from(&coefficient.im))
6708 });
6709 let zero = F(ArbPrec::default());
6710 Ok(arb.evaluate_single(&[Complex::new(zero.clone().pi(), zero)]))
6711 };
6712 let literal_value = evaluate_arb(fixed_point(exact_sum(&literal_cff)?))?;
6713 let nonliteral_value = evaluate_arb(fixed_point(exact_sum(&nonliteral_cff)?))?;
6714 assert!(!literal_value.clone().norm().re.is_zero());
6715 let distance = (literal_value.clone() - nonliteral_value.clone()).norm().re;
6716 let literal_norm = literal_value.clone().norm().re;
6717 let nonliteral_norm = nonliteral_value.clone().norm().re;
6718 let scale = if literal_norm > nonliteral_norm {
6719 literal_norm
6720 } else {
6721 nonliteral_norm
6722 };
6723 let relative_distance = distance / scale;
6724 let tolerance = F(ArbPrec::default().epsilon()).sqrt().sqrt().sqrt();
6725 assert!(
6726 relative_distance <= tolerance,
6727 "Q(1) and algebraically equivalent -Q(0) sources differ: literal={literal_value:e}, nonliteral={nonliteral_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}"
6728 );
6729 Ok(())
6730 }
6731
6732 #[test]
6733 fn exact_two_loop_source_matches_the_signed_theta_contour() -> Result<()> {
6734 test_initialise()?;
6735 let mut graph: Graph = dot!(digraph exact_two_loop_prefactor {
6736 edge [num=1 mass=1]
6737 node [num=1]
6738
6739 a -> b [id=0 lmb_id=0]
6740 a -> b [id=1 lmb_id=1]
6741 b -> a [id=2]
6742 })?;
6743 let options = graph.denominator_only_cff_3d_expression_options();
6744 let cutset = CutSet::empty(graph.n_hedges());
6745 let contract: SuBitGraph = graph.empty_subgraph();
6746 let ordinary = graph.cff(
6747 &contract,
6748 &cutset,
6749 &OrientationPattern::default(),
6750 &options,
6751 None,
6752 )?;
6753 let denominators = [0, 1, 2].map(|edge| {
6754 let edge = EdgeIndex::from(edge);
6755 FourDDenominator {
6756 source_edge: edge,
6757 momentum: FunctionBuilder::new(GS.emr_mom)
6758 .add_arg(usize::from(edge))
6759 .finish(),
6760 mass_squared: graph.underlying[edge].particle.mass_atom().pow(2),
6761 full_expr: Atom::one(),
6762 }
6763 });
6764 let (exact, _) =
6765 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &Atom::one())?;
6766
6767 for (route, cff) in [("ordinary", ordinary), ("exact", exact)] {
6768 let mut actual = cff
6769 .terms
6770 .values()
6771 .flat_map(|term| &term.orientations)
6772 .fold(Atom::Zero, |sum, orientation| sum + &orientation.expression)
6773 * Atom::num(cff.production_prefactor_factor())
6774 * (Atom::num(2) * Atom::var(GS.pi)).pow(6)
6775 / Atom::i().pow(2);
6776 for edge in (0..3).map(EdgeIndex) {
6777 actual = actual.replace(GS.ose(edge)).with(1);
6778 for component in 1..=3 {
6779 actual = actual.replace(GS.emr_mom(edge, GS.cind(component))).with(0);
6780 }
6781 }
6782 assert!(
6785 (actual + Atom::one() / Atom::num(12)).together().is_zero(),
6786 "the complete {route} two-loop contour must equal its independent integral"
6787 );
6788 }
6789 Ok(())
6790 }
6791
6792 #[test]
6793 fn unused_rank_capacity_preserves_complete_one_loop_cff() -> Result<()> {
6794 test_initialise()?;
6795 let mut graph: Graph = dot!(digraph production_rank_capacity_conversion {
6796 edge [num=1 mass=1]
6797 node [num=1]
6798
6799 a -> b [id=0 lmb_id=0]
6800 a -> b [id=1]
6801 })?;
6802 let ordinary_options = graph.denominator_only_cff_3d_expression_options();
6803 let mut generalized_options = ordinary_options.clone();
6804 generalized_options.energy_degree_bounds = Some(vec![(0, 2)]);
6805 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
6806 let ordinary_generated = graph.generate_3d_expression_for_integrand(
6807 &[],
6808 &canonization,
6809 &ordinary_options,
6810 None,
6811 )?;
6812 let generalized_generated = graph.generate_3d_expression_for_integrand(
6813 &[],
6814 &canonization,
6815 &generalized_options,
6816 None,
6817 )?;
6818 let cutset = CutSet::empty(graph.n_hedges());
6819 let pattern = OrientationPattern::default();
6820 let ordinary =
6821 graph.cff_from_production_expression(&ordinary_generated, &cutset, &pattern)?;
6822 let generalized =
6823 graph.cff_from_production_expression(&generalized_generated, &cutset, &pattern)?;
6824 let raw_sum = |cff: &CutCFF| {
6825 cff.terms
6826 .values()
6827 .flat_map(|term| &term.orientations)
6828 .fold(Atom::Zero, |sum, orientation| sum + &orientation.expression)
6829 };
6830 let common_energy = Atom::var(symbolica::symbol!(
6831 "cff_test::production_rank_capacity_energy"
6832 ));
6833 let specialize_equal_energies = |expression: Atom| {
6834 expression
6835 .replace(GS.ose(EdgeIndex(0)))
6836 .with(common_energy.clone())
6837 .replace(GS.ose(EdgeIndex(1)))
6838 .with(common_energy.clone())
6839 };
6840 let ordinary_value = specialize_equal_energies(raw_sum(&ordinary))
6841 * Atom::num(ordinary.production_prefactor_factor());
6842 let generalized_typed_value = specialize_equal_energies(raw_sum(&generalized))
6843 * Atom::num(generalized.production_prefactor_factor());
6844
6845 assert!(
6846 (ordinary_value - generalized_typed_value)
6847 .together()
6848 .is_zero(),
6849 "declaring unused rank-two capacity must not change a scalar rational CFF",
6850 );
6851 Ok(())
6852 }
6853
6854 #[test]
6855 fn unused_rank_capacity_preserves_complete_two_loop_cff() -> Result<()> {
6856 test_initialise()?;
6857 let mut graph: Graph = dot!(digraph two_loop_rank_capacity_conversion {
6858 edge [num=1 mass=1]
6859 node [num=1]
6860
6861 a -> b [id=0 lmb_id=0]
6862 a -> b [id=1 lmb_id=1]
6863 a -> b [id=2]
6864 })?;
6865 let ordinary_options = graph.denominator_only_cff_3d_expression_options();
6866 let mut generalized_options = ordinary_options.clone();
6867 generalized_options.energy_degree_bounds = Some(vec![(0, 2)]);
6868 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
6869 let ordinary_generated = graph.generate_3d_expression_for_integrand(
6870 &[],
6871 &canonization,
6872 &ordinary_options,
6873 None,
6874 )?;
6875 let generalized_generated = graph.generate_3d_expression_for_integrand(
6876 &[],
6877 &canonization,
6878 &generalized_options,
6879 None,
6880 )?;
6881 let cutset = CutSet::empty(graph.n_hedges());
6882 let pattern = OrientationPattern::default();
6883 let ordinary =
6884 graph.cff_from_production_expression(&ordinary_generated, &cutset, &pattern)?;
6885 let generalized =
6886 graph.cff_from_production_expression(&generalized_generated, &cutset, &pattern)?;
6887 let raw_sum = |cff: &CutCFF| {
6888 cff.terms
6889 .values()
6890 .flat_map(|term| &term.orientations)
6891 .fold(Atom::Zero, |sum, orientation| sum + &orientation.expression)
6892 };
6893 let specialize = |mut expression: Atom| {
6894 for (edge, energy) in [(0, 2), (1, 3), (2, 5)] {
6895 expression = expression
6896 .replace(GS.ose(EdgeIndex(edge)))
6897 .with(Atom::num(energy));
6898 }
6899 expression
6900 };
6901 let ordinary_value =
6902 specialize(raw_sum(&ordinary)) * Atom::num(ordinary.production_prefactor_factor());
6903 let generalized_raw = specialize(raw_sum(&generalized));
6904 let typed_value = &generalized_raw * Atom::num(generalized.production_prefactor_factor());
6905 assert!(
6906 (ordinary_value - typed_value).together().is_zero(),
6907 "unused rank capacity must preserve the scalar two-loop CFF",
6908 );
6909 Ok(())
6910 }
6911
6912 #[test]
6913 fn exact_cff_powered_rational_identities_match_at_arb_precision() -> Result<()> {
6914 test_initialise()?;
6915 let mut graph: Graph = dot!(digraph exact_powered_rational_identities {
6916 edge [num=1 mass=1]
6917 node [num=1]
6918 incoming [style=invis]
6919 outgoing [style=invis]
6920
6921 incoming -> a [id=0]
6922 a -> x [id=1 lmb_id=0]
6923 x -> y [id=2]
6924 b -> y [id=3]
6925 a -> b [id=4]
6926 b -> outgoing [id=5]
6927 })?;
6928 let repeated_edges = [EdgeIndex(1), EdgeIndex(2), EdgeIndex(3)];
6929 let spectator_edge = EdgeIndex(4);
6930 let carrier = repeated_edges[0];
6931 let carrier_momentum = FunctionBuilder::new(GS.emr_mom)
6932 .add_arg(usize::from(carrier))
6933 .finish();
6934 let mass_squared = graph.underlying[carrier].particle.mass_atom().pow(2);
6935 let full_expr = GS.emr_mom(carrier, GS.cind(0)).pow(2)
6936 - (1..=3).fold(mass_squared.clone(), |norm_squared, spatial_index| {
6937 norm_squared + GS.emr_mom(carrier, GS.cind(spatial_index)).pow(2)
6938 });
6939 let repeated_denominators = repeated_edges
6940 .into_iter()
6941 .zip([1, 1, -1])
6942 .map(|(source_edge, routing_sign)| FourDDenominator {
6943 source_edge,
6944 momentum: Atom::num(routing_sign) * &carrier_momentum,
6945 mass_squared: mass_squared.clone(),
6946 full_expr: full_expr.clone(),
6947 })
6948 .collect::<Vec<_>>();
6949 let spectator_mass_squared = graph.underlying[spectator_edge].particle.mass_atom().pow(2);
6950 let spectator = FourDDenominator {
6951 source_edge: spectator_edge,
6952 momentum: FunctionBuilder::new(GS.emr_mom)
6953 .add_arg(usize::from(spectator_edge))
6954 .finish(),
6955 mass_squared: spectator_mass_squared.clone(),
6956 full_expr: GS.emr_mom(spectator_edge, GS.cind(0)).pow(2)
6957 - (1..=3).fold(spectator_mass_squared, |norm_squared, spatial_index| {
6958 norm_squared + GS.emr_mom(spectator_edge, GS.cind(spatial_index)).pow(2)
6959 }),
6960 };
6961 let powered_denominators = std::iter::once(spectator.clone())
6962 .chain(repeated_denominators.iter().cloned())
6963 .collect::<Vec<_>>();
6964 let lower_denominators = [
6965 std::iter::once(spectator.clone())
6966 .chain([
6967 repeated_denominators[0].clone(),
6968 repeated_denominators[2].clone(),
6969 ])
6970 .collect::<Vec<_>>(),
6971 std::iter::once(spectator)
6972 .chain([repeated_denominators[2].clone()])
6973 .collect::<Vec<_>>(),
6974 ];
6975 assert_eq!(
6976 powered_denominators
6977 .iter()
6978 .skip(1)
6979 .map(|denominator| denominator.source_edge)
6980 .collect::<BTreeSet<_>>()
6981 .len(),
6982 3,
6983 "the cubic denominator must retain three distinct provenance owners"
6984 );
6985 assert_eq!(
6986 repeated_denominators[2].momentum,
6987 -carrier_momentum.clone(),
6988 "one occurrence must exercise the even D(-Q) denominator routing"
6989 );
6990
6991 let options = graph.denominator_only_cff_3d_expression_options();
6992 let mut production_options = options.clone();
6993 production_options.energy_degree_bounds = None;
6994 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
6995 let production_contract_edges =
6996 graph.paired_edges(&graph.tree_edges.subtract(&graph.initial_state_cut));
6997 let production = graph.generate_3d_expression_for_integrand(
6998 &production_contract_edges,
6999 &canonization,
7000 &production_options,
7001 None,
7002 )?;
7003 let lu_surface = graph
7004 .determine_raised_esurfaces_from_expression(&production.expression)
7005 .raised_groups
7006 .into_iter()
7007 .find(|group| {
7008 group.esurface_ids.iter().any(|esurface_id| {
7009 !production.expression.surfaces.esurface_cache[*esurface_id]
7010 .external_shift
7011 .is_empty()
7012 })
7013 })
7014 .expect("the dotted bubble contains an external-shift LU surface");
7015 assert!(lu_surface.esurface_ids.iter().all(|esurface_id| {
7016 let alternative = &production.expression.surfaces.esurface_cache[*esurface_id].energies;
7017 alternative.contains(&spectator_edge)
7018 && alternative.iter().any(|edge| repeated_edges.contains(edge))
7019 }));
7020
7021 let uncut = CutSet::empty(graph.n_hedges());
7022 let exact_sum = |graph: &mut Graph,
7023 denominators: &[FourDDenominator],
7024 numerator: &Atom|
7025 -> Result<Atom> {
7026 let (cff, _) =
7027 graph.cff_from_4d_denominators(denominators, &uncut, &options, numerator)?;
7028 let prefactor = Atom::num(cff.production_prefactor_factor());
7029 let mut sum = Atom::Zero;
7030 for term in cff.terms.values() {
7031 for orientation in &term.orientations {
7032 sum += &orientation.expression
7033 * term.map_exact_source_numerator(&orientation.orientation, None)?;
7034 }
7035 }
7036 Ok(sum.replace(GS.den(W_.a_, W_.b_, W_.c_, W_.d_)).with(W_.d_) * prefactor)
7037 };
7038 let graph_mass_squared = (0..graph.underlying.n_edges())
7039 .map(|edge| {
7040 graph.underlying[EdgeIndex(edge)]
7041 .particle
7042 .mass_atom()
7043 .pow(2)
7044 })
7045 .collect::<Vec<_>>();
7046 let external_edges = graph
7047 .external_momentum_edge_order()
7048 .into_iter()
7049 .collect::<BTreeSet<_>>();
7050 let representative_esurface =
7051 &production.expression.surfaces.esurface_cache[lu_surface.esurface_ids[0]];
7052 let rescale_star = Atom::num(Rational::from((3, 4)));
7053 let external_spatial_momentum = Atom::num(Rational::from((25, 12)));
7054 let scaled_spatial_momentum = |edge: EdgeIndex, rescale: Atom| {
7055 if edge == spectator_edge {
7056 &external_spatial_momentum - rescale
7057 } else {
7058 rescale
7059 }
7060 };
7061 let scaled_on_shell_energy = |edge: EdgeIndex, rescale: Atom| {
7062 (graph_mass_squared[usize::from(edge)].clone()
7063 + scaled_spatial_momentum(edge, rescale).pow(2))
7064 .sqrt()
7065 };
7066 let root_energy_sum = representative_esurface
7067 .energies
7068 .iter()
7069 .map(|edge| scaled_on_shell_energy(*edge, rescale_star.clone()))
7070 .fold(Atom::Zero, |sum, energy| sum + energy);
7071 let external_shift_coefficient = representative_esurface
7072 .external_shift
7073 .iter()
7074 .map(|(_, coefficient)| *coefficient)
7075 .sum::<i64>();
7076 assert_ne!(
7077 external_shift_coefficient, 0,
7078 "the selected LU surface must have a nonzero net external shift"
7079 );
7080 let external_energy = -root_energy_sum / Atom::num(external_shift_coefficient);
7081 let rescale_expression = |mut expression: Atom| {
7082 let rescale = Atom::var(GS.rescale);
7083 for edge in 0..graph_mass_squared.len() {
7084 let edge = EdgeIndex(edge);
7085 if external_edges.contains(&edge) {
7086 expression = expression
7087 .replace(GS.emr_mom(edge, GS.cind(0)))
7088 .with(external_energy.clone())
7089 .replace(GS.emr_mom(edge, GS.cind(1)))
7090 .with(external_spatial_momentum.clone());
7091 } else {
7092 expression = expression
7093 .replace(GS.emr_mom(edge, GS.cind(1)))
7094 .with(scaled_spatial_momentum(edge, rescale.clone()))
7095 .replace(GS.ose(edge))
7096 .with(scaled_on_shell_energy(edge, rescale.clone()))
7097 .replace(cut_energy(edge))
7098 .with(scaled_on_shell_energy(edge, rescale.clone()));
7099 }
7100 for spatial_index in 2..=3 {
7101 expression = expression
7102 .replace(GS.emr_mom(edge, GS.cind(spatial_index)))
7103 .with(Atom::Zero);
7104 }
7105 }
7106 expression
7107 };
7108 let eta = rescale_expression(representative_esurface.to_atom(&[]));
7109 assert!(
7110 eta.replace(GS.rescale)
7111 .with(rescale_star.clone())
7112 .expand()
7113 .is_zero(),
7114 "the exact test point must lie on the selected LU surface"
7115 );
7116 let raised_laurent_residue = |expression: Atom| -> Result<Atom> {
7117 Ok(rescale_expression(expression)
7121 .series(GS.rescale, rescale_star.clone(), 0)
7122 .map_err(|error| eyre::eyre!("failed to expand selected LU residue: {error}"))?
7123 .coefficient(Rational::from(-1))
7124 .expect("requested coefficient is within series precision"))
7125 };
7126 let evaluate_arb = |mut expression: Atom| -> Result<Complex<F<ArbPrec>>> {
7127 for (edge, mass_squared) in graph_mass_squared.iter().enumerate() {
7128 let edge = EdgeIndex(edge);
7129 let on_shell_energy = (1..=3)
7130 .fold(mass_squared.clone(), |norm_squared, spatial_index| {
7131 norm_squared + GS.emr_mom(edge, GS.cind(spatial_index)).pow(2)
7132 })
7133 .sqrt();
7134 expression = expression.replace(GS.ose(edge)).with(on_shell_energy);
7135 }
7136 let mut parameters = vec![Atom::var(GS.pi)];
7137 parameters.extend(
7138 (0..graph_mass_squared.len())
7139 .flat_map(|edge| (0..=3).map(move |component| (edge, component)))
7140 .map(|(edge, component)| GS.emr_mom(EdgeIndex(edge), GS.cind(component))),
7141 );
7142 let rational: ExpressionEvaluator<SymComplex<Fraction<IntegerRing>>> = expression
7143 .evaluator(¶meters)
7144 .build()
7145 .map_err(|error| eyre::eyre!("failed to build exact-CFF evaluator: {error}"))?;
7146 let mut arb: ExpressionEvaluator<Complex<F<ArbPrec>>> =
7147 rational.map_coeff(&|coefficient| {
7148 Complex::new(F::from(&coefficient.re), F::from(&coefficient.im))
7149 });
7150 let zero = F(ArbPrec::default());
7151 let values = parameters
7152 .iter()
7153 .enumerate()
7154 .map(|(index, _)| {
7155 if index == 0 {
7156 Complex::new(zero.clone().pi(), zero.clone())
7157 } else {
7158 Complex::new(
7159 F::<ArbPrec>::from(&Rational::from((170 + 37 * index as i64, 1000))),
7160 zero.clone(),
7161 )
7162 }
7163 })
7164 .collect::<Vec<_>>();
7165 Ok(arb.evaluate_single(&values))
7166 };
7167 let tolerance = F(ArbPrec::default().epsilon()).sqrt().sqrt().sqrt();
7170 let mut failures = Vec::new();
7171 for (power, lower) in [1_i64, 2].into_iter().zip(&lower_denominators) {
7172 let denominator_numerator = GS
7173 .den(
7174 usize::from(carrier),
7175 &carrier_momentum,
7176 &mass_squared,
7177 &full_expr,
7178 )
7179 .pow(power);
7180 let powered_uncut =
7181 exact_sum(&mut graph, &powered_denominators, &denominator_numerator)?;
7182 let lower_uncut = exact_sum(&mut graph, lower, &Atom::one())?;
7183 let comparisons = [
7184 (
7185 "uncut rational function",
7186 powered_uncut.clone(),
7187 lower_uncut.clone(),
7188 ),
7189 (
7190 "selected LU Laurent residue",
7191 raised_laurent_residue(powered_uncut)?,
7192 raised_laurent_residue(lower_uncut)?,
7193 ),
7194 ];
7195 for (comparison, powered_expression, lower_expression) in comparisons {
7196 let powered_value = evaluate_arb(powered_expression)?;
7197 let lower_value = evaluate_arb(lower_expression)?;
7198 assert!(
7199 [
7200 &powered_value.re,
7201 &powered_value.im,
7202 &lower_value.re,
7203 &lower_value.im
7204 ]
7205 .into_iter()
7206 .all(symbolica::domains::float::SingleFloat::is_finite),
7207 "{comparison} must evaluate to finite Arb values: powered={powered_value:e}, lower={lower_value:e}"
7208 );
7209 assert!(
7210 !lower_value.clone().norm().re.is_zero(),
7211 "{comparison} must provide a nonzero identity oracle"
7212 );
7213 let distance = (powered_value.clone() - lower_value.clone()).norm().re;
7214 let powered_norm = powered_value.norm().re;
7215 let lower_norm = lower_value.norm().re;
7216 let scale = if powered_norm > lower_norm {
7217 powered_norm
7218 } else {
7219 lower_norm
7220 };
7221 let relative_distance = if scale.is_zero() {
7222 distance.clone()
7223 } else {
7224 distance.clone() / scale
7225 };
7226 if !(symbolica::domains::float::SingleFloat::is_finite(&relative_distance)
7227 && relative_distance <= tolerance)
7228 {
7229 failures.push(format!(
7230 "D(Q)^{power}/D(Q)^3 differs from 1/D(Q)^{} for {comparison}: powered={powered_value:e}, lower={lower_value:e}, relative delta={relative_distance:e}, tolerance={tolerance:e}",
7231 3 - power
7232 ));
7233 }
7234 }
7235 }
7236 assert!(
7237 failures.is_empty(),
7238 "exact-CFF powered rational identity failures:\n{}",
7239 failures.join("\n")
7240 );
7241 Ok(())
7242 }
7243
7244 #[test]
7245 fn scalar_amplitude_phases_match_independent_bubble_contours() -> Result<()> {
7246 test_initialise()?;
7247 let model = crate::utils::load_generic_model("scalars");
7248 let coupling = model.get_coupling("SCALAR_COUPLING");
7249 let lambda: Atom = model.get_parameter("lam").name.into();
7250 assert_eq!(coupling.value, Some(Complex::new(0.0, -1.0)));
7251 assert_eq!(
7252 coupling
7253 .expression
7254 .replace(lambda.to_pattern())
7255 .with(Atom::one()),
7256 -Atom::i(),
7257 );
7258 let [coupling_symbol, coupling_expression] = coupling.rep_rule();
7259 let unit_phases = [
7266 Atom::one(),
7267 Atom::i(),
7268 -Atom::one(),
7269 -Atom::i(),
7270 Atom::one(),
7271 ];
7272 for (loops, unit_phase) in unit_phases.into_iter().enumerate() {
7273 let mut dot = format!(
7274 "digraph scalar_phase_{loops} {{ edge [particle=scalar_1]; \
7275 incoming0 [style=invis]; incoming1 [style=invis]; \
7276 outgoing0 [style=invis]; outgoing1 [style=invis]; \
7277 incoming0 -> a0 [id={}]; incoming1 -> a0 [id={}]; \
7278 a{loops} -> outgoing0 [id={}]; a{loops} -> outgoing1 [id={}];",
7279 2 * loops,
7280 2 * loops + 1,
7281 2 * loops + 2,
7282 2 * loops + 3,
7283 );
7284 for bubble in 0..loops {
7285 write!(
7286 dot,
7287 "a{bubble} -> a{} [id={} lmb_id={bubble}]; \
7288 a{bubble} -> a{} [id={} particle=scalar_2];",
7289 bubble + 1,
7290 2 * bubble,
7291 bubble + 1,
7292 2 * bubble + 1,
7293 )?;
7294 }
7295 dot.push('}');
7296 let physical: Graph = dot.into_graph(&model)?;
7297 assert_eq!(physical.get_loop_number(), loops);
7298 let unit = physical
7299 .with_global_numerator_only(format!("scalar_unit_phase_{loops}"), Atom::one());
7300 let expected_unit = unit_phase / Atom::num(12).pow(loops as i64);
7301 let expected_physical = (if loops.is_multiple_of(2) {
7304 -Atom::i()
7305 } else {
7306 Atom::i()
7307 }) / Atom::num(12).pow(loops as i64);
7308 for (mut graph, expected) in [(unit, expected_unit), (physical, expected_physical)] {
7309 let numerator = graph
7310 .production_numerator_atom_for_full_3d_expression()
7311 .replace(coupling_symbol.to_pattern())
7312 .with(coupling_expression.to_pattern())
7313 .replace(lambda.to_pattern())
7314 .with(Atom::one());
7315 let options = graph.denominator_only_cff_3d_expression_options();
7316 let cutset = CutSet::empty(graph.n_hedges());
7317 let production = graph.generate_3d_expression_for_integrand(
7318 &[],
7319 &graph.get_esurface_canonization(&graph.loop_momentum_basis),
7320 &options,
7321 Some(&numerator),
7322 )?;
7323 let stored = graph.cff_from_production_expression(
7324 &production,
7325 &cutset,
7326 &OrientationPattern::default(),
7327 )?;
7328 let direct = graph.cff(
7329 &graph.empty_subgraph::<SuBitGraph>(),
7330 &cutset,
7331 &OrientationPattern::default(),
7332 &options,
7333 Some(&numerator),
7334 )?;
7335 for (route, cff) in [("stored", stored), ("direct", direct)] {
7336 let mut actual = cff
7337 .terms
7338 .values()
7339 .flat_map(|term| &term.orientations)
7340 .fold(Atom::zero(), |sum, term| sum + &term.expression)
7341 * Atom::num(cff.production_prefactor_factor())
7342 * &numerator
7343 * (Atom::num(2) * Atom::var(GS.pi)).pow(3 * loops as i64);
7344 for edge in 0..2 * loops + 4 {
7345 let id = EdgeIndex(edge);
7346 actual = actual
7347 .replace(GS.ose(id))
7348 .with(Atom::num(1 + edge as i64 % 2))
7349 .replace(GS.emr_mom(id, GS.cind(0)))
7350 .with(Atom::zero())
7351 .replace(cut_energy(id))
7352 .with(Atom::zero());
7353 }
7354 assert_eq!(
7355 actual, expected,
7356 "{} {route}: physical contour differs from its exact residue value",
7357 graph.name,
7358 );
7359 }
7360 }
7361 }
7362 Ok(())
7363 }
7364
7365 #[test]
7366 fn thermal_mass_cycles_match_partial_fraction_contours() -> Result<()> {
7367 use crate::integrands::process::param_builder::ParamBuilderGraph;
7368 use three_dimensional_reps::MediumMode;
7369
7370 test_initialise()?;
7371 let scalars = crate::utils::load_generic_model("scalars");
7375 let sm = crate::utils::load_generic_model("sm");
7376 let b = "particle=\"b\"";
7377 let media = [
7378 (&scalars, "", MediumMode::ThermodynamicEquilibrium, (0, 1)),
7379 (&sm, b, MediumMode::ThermodynamicEquilibrium, (3, 2)),
7380 (&sm, b, MediumMode::ThermodynamicEquilibrium, (-3, 2)),
7381 (&sm, b, MediumMode::ZeroTemperatureEquilibrium, (3, 2)),
7382 (&sm, b, MediumMode::ZeroTemperatureEquilibrium, (-3, 2)),
7383 ];
7384 let even_cases =
7385 itertools::iproduct!(media, [&[1_i64][..], &[1, 2], &[1, 2, 3], &[1, 1, 2]])
7386 .map(|(medium, masses)| (medium, masses, false, false));
7387 let odd_cases = itertools::iproduct!(
7388 [b, "particle=\"b~\""],
7389 [
7390 MediumMode::ThermodynamicEquilibrium,
7391 MediumMode::ZeroTemperatureEquilibrium,
7392 ],
7393 [(0, 1), (3, 2), (-3, 2)],
7394 [false, true]
7395 )
7396 .map(|(particle, mode, mu, reversed)| {
7397 ((&sm, particle, mode, mu), &[1_i64, 2][..], true, reversed)
7398 });
7399 for ((model, particle, mode, mu), masses, odd, reversed) in even_cases.chain(odd_cases) {
7400 if !mode.is_finite_temperature() && masses == [1, 1, 2] {
7401 continue;
7404 }
7405 let fermion = !particle.is_empty();
7406 let mut dot = format!(
7407 "digraph thermal_mass_cycle {{ params=\"m1;m2;m3\"; \
7408 edge [num=1 {particle}]; node [num=1];",
7409 );
7410 for (edge, mass) in masses.iter().enumerate() {
7411 let next = (edge + 1) % masses.len();
7412 let (source, sink) = if reversed { (next, edge) } else { (edge, next) };
7413 write!(dot, "v{source} -> v{sink} [id={edge} mass=\"m{mass}\"];")?;
7414 }
7415 dot.push('}');
7416 let mut graph: Graph = dot.into_graph(model)?;
7417 let numerator = if odd {
7418 let energy = GS.emr_mom(EdgeIndex(0), GS.cind(0));
7421 if reversed { -energy } else { energy }
7422 } else {
7423 Atom::one()
7424 };
7425 let mut options = graph.denominator_only_cff_3d_expression_options();
7426 options.medium_mode = mode;
7427 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
7428 let production = graph.generate_3d_expression_for_integrand(
7429 &[],
7430 &canonization,
7431 &options,
7432 Some(&numerator),
7433 )?;
7434 let cff = graph.cff_from_production_expression(
7435 &production,
7436 &CutSet::empty(graph.n_hedges()),
7437 &OrientationPattern::default(),
7438 )?;
7439 let sum = cff.terms.values().flat_map(|term| &term.orientations).fold(
7440 Atom::zero(),
7441 |sum, term| {
7442 sum + &term.expression
7443 * numerator
7444 .replace_multiple(term.orientation.energy_replacements_gs(&graph))
7445 },
7446 ) * Atom::num(cff.production_prefactor_factor());
7447 let sum = sum * (Atom::num(2) * Atom::var(GS.pi)).pow(3) / Atom::i();
7450 let mut explicit = graph
7451 .make_thermal_distributions_explicit(&sum, mode, graph.iter_edge_ids())?
7452 .replace(function!(GS.thermal_weight_wrapper, W_.a_))
7453 .with(W_.a_);
7454 if fermion {
7455 explicit = model
7458 .apply_parameter_replacement_rules(&explicit)
7459 .replace(Atom::from(model.get_parameter("muB").name))
7460 .with(Atom::num(3) * Atom::num(mu))
7461 .replace(Atom::from(model.get_parameter("muQ").name))
7462 .with(Atom::zero());
7463 }
7464 for (edge, mass) in masses.iter().enumerate() {
7465 let energy = (Atom::num(mass * mass) + Atom::num((9, 25))).sqrt();
7468 explicit = explicit.replace(GS.ose(EdgeIndex(edge))).with(energy);
7469 }
7470 let rational: ExpressionEvaluator<SymComplex<Fraction<IntegerRing>>> = explicit
7471 .evaluator(&[Atom::var(GS.inverse_temperature)])
7472 .function_map(graph.param_builder.fn_map.clone())
7473 .build()
7474 .map_err(|error| eyre::eyre!("thermal mass-cycle evaluator: {error}"))?;
7475 let mut evaluator: ExpressionEvaluator<Complex<F<f64>>> =
7476 rational.map_coeff(&|coefficient| {
7477 Complex::new(F::from(&coefficient.re), F::from(&coefficient.im))
7478 });
7479 let mu = mu.0 as f64 / mu.1 as f64;
7480 let betas = if mode.is_finite_temperature() {
7481 &[0.7_f64, 2.0][..]
7482 } else {
7483 &[f64::INFINITY][..]
7484 };
7485 for &beta in betas {
7486 let medium = |mass: i64| {
7489 let energy = ((mass * mass) as f64 + 0.36).sqrt();
7490 if !fermion {
7491 let arg = beta * energy / 2.0;
7492 return (
7493 -1.0 / (2.0 * energy * arg.tanh()),
7494 1.0 / (4.0 * energy.powi(3) * arg.tanh())
7495 + beta / (8.0 * energy.powi(2) * arg.sinh().powi(2)),
7496 );
7497 }
7498 let occupation = |x: f64| {
7500 if beta.is_infinite() {
7501 if x < 0.0 { 1.0 } else { 0.0 }
7502 } else {
7503 1.0 / ((beta * x).exp() + 1.0)
7504 }
7505 };
7506 let [particles, antiparticles] =
7507 [occupation(energy - mu), occupation(energy + mu)];
7508 if odd {
7509 let charge = if particle == b { 1.0 } else { -1.0 };
7510 let axis = if reversed { -1.0 } else { 1.0 };
7511 return (charge * axis * (particles - antiparticles) / 2.0, f64::NAN);
7512 }
7513 if beta.is_infinite() {
7514 let open = energy > mu.abs();
7517 return (if open { -1.0 / (2.0 * energy) } else { 0.0 }, f64::NAN);
7518 }
7519 let vacant = 1.0 - particles - antiparticles;
7520 let slope = vacant / (2.0 * energy.powi(2))
7521 - beta
7522 * (particles * (1.0 - particles)
7523 + antiparticles * (1.0 - antiparticles))
7524 / (2.0 * energy);
7525 (-vacant / (2.0 * energy), slope / (2.0 * energy))
7526 };
7527 let vacuum = |mass: i64| {
7528 let energy = ((mass * mass) as f64 + 0.36).sqrt();
7529 (-1.0 / (2.0 * energy), 1.0 / (4.0 * energy.powi(3)))
7530 };
7531 let partial_fractions = |contour: &dyn Fn(i64) -> (f64, f64)| {
7534 if masses == [1, 1, 2] {
7535 (contour(1).1 * (1.0 - 4.0) - (contour(1).0 - contour(2).0)) / 9.0
7538 } else {
7539 masses
7540 .iter()
7541 .map(|&mass| {
7542 contour(mass).0
7543 / masses
7544 .iter()
7545 .filter(|&&other| other != mass)
7546 .map(|&other| (mass * mass - other * other) as f64)
7547 .product::<f64>()
7548 })
7549 .sum()
7550 }
7551 };
7552 let expected = partial_fractions(&medium);
7553 let scale = if odd {
7558 1.0 / 3.0
7559 } else {
7560 expected.abs().max(partial_fractions(&vacuum).abs())
7561 };
7562 let actual = evaluator.evaluate_single(&[Complex::new(F(beta), F(0.0))]);
7563 assert!(
7564 actual.im.0.abs() < 1.0e-13 && (actual.re.0 - expected).abs() < 1.0e-11 * scale,
7565 "{mode:?}, {particle}, mu={mu}, masses={masses:?}, beta={beta}, odd={odd}, reversed={reversed}: signed CFF={actual}, partial fractions={expected}"
7566 );
7567 }
7568 }
7569 Ok(())
7570 }
7571
7572 #[test]
7573 fn ordinary_terminal_sources_match_signed_contours() -> Result<()> {
7574 test_initialise()?;
7575 let tadpole: Graph = dot!(digraph ordinary_unit_tadpole {
7576 edge [particle="scalar_1" num=1]
7577 node [num=1]
7578 incoming [style=invis]
7579 outgoing [style=invis]
7580 a -> a [id=0 lmb_id=0]
7581 incoming -> a [id=1]
7582 a -> outgoing [id=2]
7583 }, "scalars")?;
7584 let attached: Graph = dot!(digraph ordinary_attached_tadpole_bubble {
7585 edge [particle="scalar_1" num=1]
7586 node [num=1]
7587 incoming [style=invis]
7588 outgoing [style=invis]
7589 a -> a [id=0 lmb_id=0]
7590 a -> b [id=1 lmb_id=1]
7591 a -> b [id=2 particle="scalar_2"]
7592 incoming -> a [id=3]
7593 a -> outgoing [id=4]
7594 }, "scalars")?;
7595 let mut failures = Vec::new();
7596 for (label, mut graph, energies) in [
7597 ("unit tadpole", tadpole, vec![2_i64]),
7598 (
7599 "attached tadpole and nonrepeated bubble",
7600 attached,
7601 vec![2, 3, 5],
7602 ),
7603 ] {
7604 let parsed = graph.to_three_d_parsed_graph()?;
7605 assert!(repeated_groups(&parsed).is_empty());
7606 let loop_count = graph.get_loop_number();
7607 assert_eq!(loop_count, if energies.len() == 1 { 1 } else { 2 });
7608 let et = Atom::num(energies[0]);
7612 let mut contour = -Atom::one() / (Atom::num(2) * et);
7613 if energies.len() == 3 {
7614 let eb = Atom::num(energies[1]);
7615 let ec = Atom::num(energies[2]);
7616 let bubble: Atom = -(Atom::one()
7617 / (Atom::num(2) * &eb * (eb.clone().pow(2) - ec.clone().pow(2)))
7618 + Atom::one() / (Atom::num(2) * &ec * (ec.clone().pow(2) - eb.clone().pow(2))));
7619 contour *= bubble;
7620 }
7621 let contour = contour.together();
7622 for context in [
7623 three_dimensional_reps::CffGenerationContext::Standalone,
7624 three_dimensional_reps::CffGenerationContext::EmbeddedCffFactor,
7625 ] {
7626 let mut options = graph.denominator_only_cff_3d_expression_options();
7627 options.cff_generation_context = context;
7628 let raw = three_dimensional_reps::generate_3d_expression(&graph, &options)?;
7629 let raw_bridge = CutCFF::gamma_loop_prefactor_conversion(&raw).product(
7630 CffGlobalPrefactorSign::from_exponent(
7631 parsed.denominator_internal_edge_ids().len(),
7632 ),
7633 );
7634 let mut raw_signed = raw
7635 .expression
7636 .orientations
7637 .iter()
7638 .fold(Atom::zero(), |sum, orientation| sum + orientation.to_atom())
7639 .replace_multiple(raw.expression.surfaces.get_all_replacements(&[]))
7640 * Atom::num(raw_bridge.factor());
7641 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
7642 let production = graph.generate_3d_expression_for_integrand(
7643 &[],
7644 &canonization,
7645 &options,
7646 Some(&Atom::one()),
7647 )?;
7648 let cff = graph.cff_from_production_expression(
7649 &production,
7650 &CutSet::empty(graph.n_hedges()),
7651 &OrientationPattern::default(),
7652 )?;
7653 let term_sum = cff
7654 .terms
7655 .values()
7656 .flat_map(|term| &term.orientations)
7657 .fold(Atom::zero(), |sum, term| sum + &term.expression)
7658 * Atom::num(cff.production_prefactor_factor());
7659 let selector_sum = cff
7660 .expression_with_selectors()
7661 .iter()
7662 .fold(Atom::zero(), |sum, (_, term)| sum + term)
7663 .replace(function!(OrientationID::symbol(), W_.a_))
7664 .with(Atom::one());
7665 assert!(
7666 (selector_sum - &term_sum).together().is_zero(),
7667 "production selector summation must not introduce an undocumented average"
7668 );
7669 let mut gamma_signed = term_sum
7670 * (Atom::num(2) * Atom::var(GS.pi)).pow(3 * loop_count as i64)
7671 / Atom::i().pow(loop_count as i64);
7672 for (edge, energy) in energies.iter().enumerate() {
7673 raw_signed = raw_signed
7674 .replace(three_dimensional_reps::symbols::ose_atom_from_index(
7675 EdgeIndex(edge),
7676 ))
7677 .with(Atom::num(*energy));
7678 gamma_signed = gamma_signed
7679 .replace(GS.ose(EdgeIndex(edge)))
7680 .with(Atom::num(*energy));
7681 }
7682 let raw_signed = raw_signed.together();
7683 let gamma_signed = gamma_signed.together();
7684 assert!(
7685 (raw_signed.clone() - &gamma_signed).is_zero(),
7686 "{label} {context:?}: raw bridge {raw_signed} differs from full GammaLoop bridge {gamma_signed}"
7687 );
7688 if !(gamma_signed.clone() - &contour).is_zero() {
7689 failures.push(format!(
7690 "{label} {context:?}: signed CFF={gamma_signed}, exact contour={contour}"
7691 ));
7692 }
7693 }
7694 }
7695 assert!(
7696 failures.is_empty(),
7697 "ordinary terminal contour mismatches:\n{}",
7698 failures.join("\n")
7699 );
7700 Ok(())
7701 }
7702}