1use std::{
2 collections::{BTreeMap, BTreeSet, VecDeque},
3 hash::Hash,
4 mem::{size_of, size_of_val},
5 sync::Arc,
6 time::{Duration, Instant},
7};
8
9use crate::utils::GS;
10use crate::{
11 cff::{
12 VertexSet,
13 hsurface::{Hsurface, HsurfaceID},
14 surface::{
15 HybridSurfaceID, LinearEnergyExpr, LinearSurface, LinearSurfaceID, LinearSurfaceKind,
16 },
17 },
18 graph::{
19 ExactUvSubLmbFrame, FeynmanGraph, FourDDenominator, Graph, GraphThreeDSource,
20 LoopMomentumBasis,
21 },
22 numerator::energy_degree::EnergyPowerAssignmentPlan,
23 settings::global::{GenerationSettings, UniformNumeratorSamplingScale},
24 uv::approx::{
25 local_4d::{CanonicalUvDenominatorClass, CanonicalUvSector, FourDSector},
26 projected_4d::Local4dProjectionContext,
27 },
28};
29use ahash::{AHashMap, HashSet};
30use color_eyre::Result;
31use itertools::Itertools;
32use linnet::half_edge::{
33 involution::{EdgeIndex, Hedge},
34 subgraph::{SuBitGraph, SubSetLike},
35};
36use linnet::num_traits::SignOrZero;
37use symbolica::atom::{Atom, AtomCore, AtomView};
38use symbolica::domains::rational::Rational;
39use three_dimensional_reps::{
40 CffEnergyFactorOwnership, CffGenerationContext, EnergyEdgeIndexMap,
41 Generate3DExpressionOptions, GeneratedThreeDExpression, NumeratorSamplingScaleMode,
42 ParsedGraph, RepresentationMode, ThreeDGraphSource,
43 tree::{NodeId, Tree},
44};
45
46use tracing::debug;
47
48use super::{
49 CffEnergyBoundSourceKind, CffEnergyDegreeBoundReport, PlannedExactSourceNumerator,
50 esurface::{Esurface, EsurfaceID, ExternalShift},
51 expression::CFFExpression,
52};
53
54#[derive(Debug, Clone)]
55pub struct ShiftRewrite {
56 pub dependent_momentum: EdgeIndex,
57 pub dependent_momentum_expr: ExternalShift,
58}
59
60#[derive(Debug, Clone, Copy)]
61struct SurfaceMapEntry {
62 surface_id: HybridSurfaceID,
63 sign: i64,
64}
65
66struct RemappedDenominatorTree {
67 prefactor: i64,
68 surface_signs: BTreeMap<HybridSurfaceID, i64>,
69 denominator: Tree<HybridSurfaceID>,
70}
71
72pub(crate) struct GenerationCache<K, V> {
76 entries: AHashMap<K, GenerationCacheEntry<V>>,
77 recency: BTreeMap<u64, K>,
78 clock: u64,
79 max_bytes: usize,
80 max_entries: usize,
81 retained_bytes: usize,
82 pub(crate) hits: usize,
83 pub(crate) misses: usize,
84 pub(crate) evictions: usize,
85 pub(crate) cache_time: Duration,
87}
88
89struct GenerationCacheEntry<V> {
90 value: V,
91 bytes: usize,
92 used: u64,
93}
94
95impl<K: Clone + Eq + Hash, V> GenerationCache<K, V> {
96 pub(crate) fn new(max_bytes: usize, max_entries: usize) -> Self {
97 Self {
98 entries: AHashMap::new(),
99 recency: BTreeMap::new(),
100 clock: 0,
101 max_bytes,
102 max_entries,
103 retained_bytes: 0,
104 hits: 0,
105 misses: 0,
106 evictions: 0,
107 cache_time: Duration::ZERO,
108 }
109 }
110
111 pub(crate) fn get(&mut self, key: &K) -> Option<&V> {
112 let Some(entry) = self.entries.get_mut(key) else {
113 self.misses += 1;
114 return None;
115 };
116 self.hits += 1;
117 self.clock = self
118 .clock
119 .checked_add(1)
120 .expect("generation cache access counter exhausted");
121 let retained_key = self
122 .recency
123 .remove(&entry.used)
124 .expect("LRU index and cache entries agree");
125 entry.used = self.clock;
126 self.recency.insert(self.clock, retained_key);
127 Some(&entry.value)
128 }
129
130 pub(crate) fn insert(&mut self, key: K, value: V, accounted_bytes: usize) {
131 if let Some(previous) = self.entries.remove(&key) {
132 self.retained_bytes -= previous.bytes;
133 self.recency.remove(&previous.used);
134 }
135 if self.max_entries == 0 || self.max_bytes == 0 || accounted_bytes > self.max_bytes {
136 return;
137 }
138 while self.entries.len() >= self.max_entries
139 || accounted_bytes > self.max_bytes - self.retained_bytes
140 {
141 let Some((_, oldest)) = self.recency.pop_first() else {
142 break;
143 };
144 let removed = self
145 .entries
146 .remove(&oldest)
147 .expect("LRU index and cache entries agree");
148 self.retained_bytes -= removed.bytes;
149 self.evictions += 1;
150 }
151 self.clock = self
152 .clock
153 .checked_add(1)
154 .expect("generation cache access counter exhausted");
155 self.retained_bytes += accounted_bytes;
156 self.recency.insert(self.clock, key.clone());
157 self.entries.insert(
158 key,
159 GenerationCacheEntry {
160 value,
161 bytes: accounted_bytes,
162 used: self.clock,
163 },
164 );
165 }
166
167 pub(crate) fn clear(&mut self) {
168 let started = Instant::now();
169 self.entries.clear();
170 self.recency.clear();
171 self.retained_bytes = 0;
172 self.cache_time += started.elapsed();
173 }
174
175 pub(crate) fn len(&self) -> usize {
176 self.entries.len()
177 }
178 pub(crate) fn can_retain(&self, accounted_bytes: usize) -> bool {
179 self.max_entries != 0 && self.max_bytes != 0 && accounted_bytes <= self.max_bytes
180 }
181 pub(crate) fn retained_bytes(&self) -> usize {
182 self.retained_bytes
183 }
184}
185
186#[derive(Clone, PartialEq, Eq, Hash)]
187struct ExactCffGenerationKey {
188 topology: ParsedGraph,
189 internal_energy_edges: BTreeMap<usize, usize>,
190 external_energy_edges: BTreeMap<usize, usize>,
191 orientation_edge_count: usize,
192 options: Generate3DExpressionOptions,
193}
194
195struct ExactCffGenerationEntry {
196 count: usize,
197 payload: Option<GeneratedThreeDExpression>,
198}
199
200impl ExactCffGenerationKey {
201 fn accounted_bytes(&self) -> usize {
202 let graph = &self.topology;
203 size_of::<Self>()
204 + ExactCffGenerationCache::vector_bytes(&graph.internal_edges)
205 + graph
206 .internal_edges
207 .iter()
208 .map(|edge| {
209 edge.label.capacity()
210 + edge.mass_key.as_ref().map_or(0, String::capacity)
211 + ExactCffGenerationCache::vector_bytes(&edge.signature.loop_signature)
212 + ExactCffGenerationCache::vector_bytes(&edge.signature.external_signature)
213 })
214 .sum::<usize>()
215 + ExactCffGenerationCache::vector_bytes(&graph.external_edges)
216 + graph
217 .external_edges
218 .iter()
219 .map(|edge| {
220 edge.label.capacity()
221 + ExactCffGenerationCache::vector_bytes(&edge.external_coefficients)
222 })
223 .sum::<usize>()
224 + ExactCffGenerationCache::vector_bytes(&graph.initial_state_cut_edges)
225 + ExactCffGenerationCache::vector_bytes(&graph.loop_names)
226 + ExactCffGenerationCache::vector_bytes(&graph.external_names)
227 + graph
228 .loop_names
229 .iter()
230 .chain(&graph.external_names)
231 .map(String::capacity)
232 .sum::<usize>()
233 + graph
234 .node_name_to_internal
235 .keys()
236 .map(|name| size_of::<String>() + name.capacity() + size_of::<usize>() + 32)
237 .sum::<usize>()
238 + (self.internal_energy_edges.len() + self.external_energy_edges.len())
239 * (2 * size_of::<usize>() + 32)
240 + self
241 .options
242 .energy_degree_bounds
243 .as_ref()
244 .map_or(0, ExactCffGenerationCache::vector_bytes)
245 + ExactCffGenerationCache::vector_bytes(
246 &self.options.preserve_internal_edges_as_four_d_denominators,
247 )
248 }
249}
250
251pub(crate) struct ExactCffGenerationCache {
252 entries: GenerationCache<ExactCffGenerationKey, ExactCffGenerationEntry>,
259 pub(crate) native_generations: usize,
260}
261
262impl Default for ExactCffGenerationCache {
263 fn default() -> Self {
264 Self {
265 entries: GenerationCache::new(64 * 1024 * 1024, 4096),
266 native_generations: 0,
267 }
268 }
269}
270
271impl ExactCffGenerationCache {
272 pub(crate) fn statistics(&self) -> (usize, usize, usize, usize) {
273 (
274 self.entries.hits,
275 self.entries.misses,
276 self.entries.evictions,
277 self.entries.retained_bytes(),
278 )
279 }
280
281 pub(crate) fn len(&self) -> usize {
282 self.entries
283 .entries
284 .values()
285 .filter(|entry| entry.value.payload.is_some())
286 .count()
287 }
288
289 fn count(&mut self, key: &ExactCffGenerationKey) -> Option<usize> {
290 self.entries.get(key).map(|entry| entry.count)
291 }
292
293 fn record_count(&mut self, key: ExactCffGenerationKey, count: usize) {
294 let bytes = key.accounted_bytes() * 2 + size_of::<ExactCffGenerationEntry>() + 64;
295 self.entries.insert(
296 key,
297 ExactCffGenerationEntry {
298 count,
299 payload: None,
300 },
301 bytes,
302 );
303 }
304
305 fn record_payload(&mut self, key: ExactCffGenerationKey, payload: &GeneratedThreeDExpression) {
306 let count = payload.expression.orientations.len();
307 let bytes = key.accounted_bytes() * 2
308 + size_of::<ExactCffGenerationEntry>()
309 + 64
310 + Self::payload_bytes(payload);
311 if bytes > self.entries.max_bytes {
312 self.record_count(key, count);
313 } else {
314 self.entries.insert(
315 key,
316 ExactCffGenerationEntry {
317 count,
318 payload: Some(payload.clone()),
319 },
320 bytes,
321 );
322 }
323 }
324
325 fn rational_bytes(value: &Rational) -> usize {
326 [value.numerator_ref(), value.denominator_ref()]
327 .into_iter()
328 .map(|integer| {
329 if let symbolica::domains::integer::Integer::Large(value) = integer {
330 value.significant_bits().div_ceil(8) as usize
331 } else {
332 0
333 }
334 })
335 .sum()
336 }
337
338 fn vector_bytes<T>(values: &Vec<T>) -> usize {
339 values.capacity() * size_of::<T>()
340 }
341
342 fn energy_bytes(energy: &LinearEnergyExpr) -> usize {
343 Self::vector_bytes(&energy.internal_terms)
344 + Self::vector_bytes(&energy.external_terms)
345 + energy
346 .internal_terms
347 .iter()
348 .chain(&energy.external_terms)
349 .map(|(_, coefficient)| Self::rational_bytes(coefficient))
350 .sum::<usize>()
351 + Self::rational_bytes(&energy.constant)
352 + Self::rational_bytes(&energy.uniform_scale_coeff)
353 }
354
355 fn payload_bytes(payload: &GeneratedThreeDExpression) -> usize {
356 let expression = &payload.expression;
361 size_of_val(payload)
362 + Self::vector_bytes(&payload.source_energy_degree_bounds)
363 + Self::vector_bytes(&payload.energy_factor_components)
364 + payload
365 .energy_factor_components
366 .iter()
367 .map(|component| Self::vector_bytes(&component.internal_edge_ids))
368 .sum::<usize>()
369 + expression.surfaces.linear_surface_cache.capacity() * size_of::<LinearSurface>()
370 + expression
371 .surfaces
372 .linear_surface_cache
373 .iter()
374 .map(|surface| Self::energy_bytes(&surface.expression))
375 .sum::<usize>()
376 + Self::vector_bytes(&expression.residual_denominators)
377 + expression
378 .residual_denominators
379 .iter()
380 .map(|denominator| denominator.origin.as_ref().map_or(0, String::capacity))
381 .sum::<usize>()
382 + expression.orientations.capacity()
383 * size_of::<three_dimensional_reps::expression::OrientationExpression>()
384 + expression
385 .orientations
386 .iter()
387 .map(|orientation| {
388 orientation.data.orientation.iter().count()
389 * size_of::<linnet::half_edge::involution::Orientation>()
390 + orientation.data.label.as_ref().map_or(0, String::capacity)
391 + Self::vector_bytes(&orientation.loop_energy_map)
392 + Self::vector_bytes(&orientation.edge_energy_map)
393 + orientation
394 .loop_energy_map
395 .iter()
396 .chain(&orientation.edge_energy_map)
397 .map(Self::energy_bytes)
398 .sum::<usize>()
399 + Self::vector_bytes(&orientation.variants)
400 + orientation
401 .variants
402 .iter()
403 .map(|variant| {
404 variant.origin.as_ref().map_or(0, String::capacity)
405 + Self::rational_bytes(&variant.prefactor)
406 + Self::vector_bytes(&variant.half_edges)
407 + Self::vector_bytes(&variant.denominator_edges)
408 + Self::vector_bytes(&variant.numerator_surfaces)
409 + variant.denominator_surface_signs.len()
410 * (size_of::<(HybridSurfaceID, i64)>() + 32)
411 + variant
412 .denominator_edge_support_signs
413 .keys()
414 .map(|support| {
415 size_of::<(Vec<EdgeIndex>, i64)>()
416 + Self::vector_bytes(support)
417 + 32
418 })
419 .sum::<usize>()
420 + variant
421 .denominator
422 .iter_nodes()
423 .map(|node| {
424 size_of_val(node) + Self::vector_bytes(&node.children)
425 })
426 .sum::<usize>()
427 })
428 .sum::<usize>()
429 })
430 .sum::<usize>()
431 }
432
433 fn generation_key(
434 parsed: &ParsedGraph,
435 energy_edges: &EnergyEdgeIndexMap,
436 options: &Generate3DExpressionOptions,
437 ) -> ExactCffGenerationKey {
438 let mut topology = parsed.clone();
439 topology.node_name_to_internal = topology
440 .internal_edges
441 .iter()
442 .flat_map(|edge| [edge.tail, edge.head])
443 .chain(
444 topology
445 .external_edges
446 .iter()
447 .flat_map(|edge| edge.source.into_iter().chain(edge.destination)),
448 )
449 .collect::<std::collections::BTreeSet<_>>()
450 .into_iter()
451 .map(|node| (format!("__gammaloop_exact_node_{node}"), node))
452 .collect();
453 ExactCffGenerationKey {
454 topology,
455 internal_energy_edges: energy_edges.internal.clone(),
456 external_energy_edges: energy_edges.external.clone(),
457 orientation_edge_count: energy_edges.orientation_edge_count,
458 options: options.clone(),
459 }
460 }
461}
462
463#[derive(Clone, PartialEq, Eq, Hash)]
465pub(crate) enum PreparationKey {
466 Canonical(Arc<FourDSector>),
467 Source(Arc<ExactCffPreparationKey>),
468 Numerator(super::ExactNumeratorTemplateKey),
469}
470
471pub(crate) enum PreparationValue {
472 Canonical(Arc<CanonicalUvSector>),
473 Source(Arc<ExactCffGenerationPreparation>),
474 Numerator(Arc<super::PlannedExactSourceNumerator>),
475}
476
477#[derive(PartialEq, Eq, Hash)]
478pub(crate) struct ExactCffPreparationKey {
479 pub(super) denominators: Vec<FourDDenominator>,
480 pub(super) uv_edges: Vec<EdgeIndex>,
481 pub(super) boundary_hedges: Vec<Hedge>,
482 pub(super) coordinates: Option<(LoopMomentumBasis, ExactUvSubLmbFrame)>,
483 pub(super) classes: Vec<CanonicalUvDenominatorClass>,
484 pub(super) numerator: Atom,
485 pub(super) options: Generate3DExpressionOptions,
486}
487
488impl ExactCffPreparationKey {
489 pub(super) fn accounted_bytes(&self) -> usize {
490 size_of::<Self>()
491 + self.denominators.capacity() * size_of::<FourDDenominator>()
492 + self
493 .denominators
494 .iter()
495 .map(FourDDenominator::accounted_bytes)
496 .sum::<usize>()
497 + self.uv_edges.capacity() * size_of::<EdgeIndex>()
498 + self.boundary_hedges.capacity() * size_of::<Hedge>()
499 + self
500 .coordinates
501 .as_ref()
502 .map_or(0, |(lmb, _)| lmb.accounted_bytes())
503 + self.classes.capacity() * size_of::<CanonicalUvDenominatorClass>()
504 + self
505 .classes
506 .iter()
507 .map(CanonicalUvDenominatorClass::accounted_bytes)
508 .sum::<usize>()
509 + self.numerator.as_view().get_byte_size()
510 + self
511 .options
512 .energy_degree_bounds
513 .as_ref()
514 .map_or(0, ExactCffGenerationCache::vector_bytes)
515 + ExactCffGenerationCache::vector_bytes(
516 &self.options.preserve_internal_edges_as_four_d_denominators,
517 )
518 }
519}
520
521pub(crate) struct ExactCffGenerationPreparation {
522 parsed: ParsedGraph,
523 energy_edges: EnergyEdgeIndexMap,
524 source_options: Generate3DExpressionOptions,
525 exact_source_energy_mapper: Arc<crate::graph::three_d_source::ExactSourceEnergyMapper>,
526 energy_assignment_plans: Vec<Arc<EnergyPowerAssignmentPlan>>,
527 physical_energy_degree_bounds: Vec<(usize, usize)>,
528 pub(super) physical_energy_edges: EnergyEdgeIndexMap,
529 pub(super) physical_cut_support_edges: BTreeMap<usize, Vec<EdgeIndex>>,
530 pub(super) physical_surface_edges: BTreeSet<usize>,
531 pub(super) inverse_energy_product: Atom,
532 pub(super) active_loop_count: usize,
533 pub(super) contract_subgraph: SuBitGraph,
534}
535
536impl ThreeDGraphSource for ExactCffGenerationPreparation {
537 fn to_three_d_parsed_graph(&self) -> three_dimensional_reps::graph_io::Result<ParsedGraph> {
538 Ok(self.parsed.clone())
539 }
540
541 fn energy_edge_index_map(&self, _: &ParsedGraph) -> Option<EnergyEdgeIndexMap> {
542 Some(self.energy_edges.clone())
543 }
544}
545
546impl ExactCffGenerationPreparation {
547 pub(super) fn accounted_bytes(&self) -> Result<usize> {
548 Ok(size_of::<Self>()
551 + 128
552 + ExactCffGenerationKey {
553 topology: self.parsed.clone(),
554 internal_energy_edges: self.energy_edges.internal.clone(),
555 external_energy_edges: self.energy_edges.external.clone(),
556 orientation_edge_count: self.energy_edges.orientation_edge_count,
557 options: self.source_options.clone(),
558 }
559 .accounted_bytes()
560 + self.exact_source_energy_mapper.accounted_bytes()?
561 + self.energy_assignment_plans.capacity() * size_of::<Arc<EnergyPowerAssignmentPlan>>()
562 + self
563 .energy_assignment_plans
564 .iter()
565 .map(|plan| 32 + plan.accounted_bytes())
566 .sum::<usize>()
567 + self.physical_energy_degree_bounds.capacity() * size_of::<(usize, usize)>()
568 + (self.physical_energy_edges.internal.len()
569 + self.physical_energy_edges.external.len())
570 * 64
571 + self
572 .physical_cut_support_edges
573 .values()
574 .map(|edges| 64 + edges.capacity() * size_of::<EdgeIndex>())
575 .sum::<usize>()
576 + self.physical_surface_edges.len() * 48
577 + self.inverse_energy_product.as_view().get_byte_size()
578 + self.contract_subgraph.size().div_ceil(8)
579 + 32)
580 }
581}
582
583impl Graph {
584 pub(crate) fn prepare_3d_expression_for_4d_term(
585 &self,
586 source: &GraphThreeDSource<'_>,
587 options: &Generate3DExpressionOptions,
588 analysis_numerator: &Atom,
589 classes: &[CanonicalUvDenominatorClass],
590 ) -> Result<ExactCffGenerationPreparation> {
591 let preparation_started = Instant::now();
592 let source_options = options.clone();
593 let initial_state_cut_edges = self
594 .iter_edges_of(&self.initial_state_cut)
595 .map(|(_, edge_id, _)| edge_id)
596 .collect::<HashSet<_>>();
597 let bridge_edges = self
598 .iter_edges_of(&self.tree_edges)
599 .map(|(_, edge_id, _)| edge_id)
600 .filter(|edge_id| !initial_state_cut_edges.contains(edge_id))
601 .collect::<HashSet<_>>();
602 let excluded_numerator_edges = initial_state_cut_edges
603 .iter()
604 .chain(&bridge_edges)
605 .copied()
606 .chain(source.factorized_external_emr_edges())
607 .collect::<HashSet<_>>();
608 let contracted_source = source.contract_subgraph();
609 for (_, edge_id, _) in self.iter_edges_of(&contracted_source) {
610 if !bridge_edges.contains(&edge_id) {
611 continue;
612 }
613 let Some(coordinates) = source.reconstructible_outer_loop_coordinates(edge_id) else {
614 return Err(eyre::eyre!(
615 "contracted bridge edge {} retains an inner-loop energy coordinate and cannot be projected independently of the exact CFF source",
616 usize::from(edge_id),
617 ));
618 };
619 if coordinates.iter().any(|coordinate| *coordinate != 0) {
620 return Err(eyre::eyre!(
621 "contracted bridge edge {} has nonzero outer-loop coordinates {:?}; a CFF-external tree edge must have a purely external affine energy",
622 usize::from(edge_id),
623 coordinates,
624 ));
625 }
626 }
627 let degree_analysis_started = Instant::now();
631 let (prepared_numerator, fixed_affine_blocks) =
632 source.prepare_affine_numerator(analysis_numerator)?;
633 let analysis_numerator = &prepared_numerator;
634 let physical_report_numerator = analysis_numerator.replace_map(|view, _, output| {
635 if let AtomView::Fun(denominator) = view
636 && denominator.get_symbol() == GS.den
637 && denominator.get_nargs() == 4
638 {
639 **output = denominator.get(3).to_owned();
640 }
641 });
642 let physical_report_numerator = physical_report_numerator.replace_map(|view, _, output| {
643 let AtomView::Fun(momentum) = view else {
644 return;
645 };
646 if momentum.get_symbol() != GS.emr_mom || momentum.get_nargs() < 2 {
647 return;
648 }
649 let Some(id) = GS.uv_class_data(momentum.get(0)) else {
650 return;
651 };
652 **output = classes
653 .iter()
654 .find(|class| class.id == id)
655 .map_or(Atom::Zero, |class| {
656 class.momentum_with_indices(
657 &momentum
658 .iter()
659 .skip(1)
660 .map(|index| index.to_owned())
661 .collect::<Vec<_>>(),
662 )
663 });
664 });
665 let physical_energy_degree_bounds = self
666 .automatic_numerator_energy_degree_bounds_in_atoms_excluding_with_min_degree(
667 [&physical_report_numerator],
668 excluded_numerator_edges.iter().copied(),
669 1,
670 )
671 .map_err(|error| {
672 eyre::eyre!("could not analyze numerator in physical EMR energy variables: {error}")
673 })?;
674 let degree_analysis_ms = degree_analysis_started.elapsed().as_secs_f64() * 1000.0;
675 let source_mapping_started = Instant::now();
679 let parsed = source.to_three_d_parsed_graph()?;
680 let energy_edges = source
681 .energy_edge_index_map(&parsed)
682 .expect("exact 4D source has an occurrence-local energy map");
683 let exact_source_energy_mapper = source.exact_source_energy_mapper(classes)?;
684 let mut candidates = exact_source_energy_mapper
690 .equivalent_energy_candidates(
691 physical_energy_degree_bounds
692 .iter()
693 .map(|(edge, _)| EdgeIndex(*edge)),
694 )
695 .map_err(|error| {
696 eyre::eyre!(
697 "could not certify exact 4D CFF numerator energies for graph `{}`: {error}",
698 self.name,
699 )
700 })?;
701 candidates.fixed_affine_blocks = fixed_affine_blocks;
702 let source_mapping_ms = source_mapping_started.elapsed().as_secs_f64() * 1000.0;
703 let allocation_started = Instant::now();
709 let energy_assignment_plans = self
710 .plan_numerator_energy_assignment_proposals_in_atom_excluding(
711 analysis_numerator,
712 excluded_numerator_edges.iter().copied(),
713 &candidates,
714 )
715 .map_err(|error| {
716 eyre::eyre!(
717 "could not plan exact 4D CFF numerator energies for graph `{}`: {error}",
718 self.name,
719 )
720 })?;
721 let allocation_ms = allocation_started.elapsed().as_secs_f64() * 1000.0;
722 debug!(
723 graph = %self.name,
724 physical_energy_degree_bounds = ?physical_energy_degree_bounds,
725 equivalent_energy_candidates = ?candidates,
726 candidate_bounds = ?energy_assignment_plans.iter().map(|plan| plan.energy_degree_bounds()).collect::<Vec<_>>(),
727 "planned factorized exact-CFF numerator energy assignment proposals"
728 );
729 let preparation = ExactCffGenerationPreparation {
730 parsed,
731 energy_edges,
732 source_options,
733 exact_source_energy_mapper: Arc::new(exact_source_energy_mapper),
734 energy_assignment_plans: energy_assignment_plans.into_iter().map(Arc::new).collect(),
735 physical_energy_degree_bounds,
736 physical_energy_edges: source
737 .physical_energy_edge_index_map()
738 .expect("exact source has a physical energy projection"),
739 physical_cut_support_edges: source
740 .physical_cut_support_edge_index_map()
741 .expect("exact source has a cut-support projection"),
742 physical_surface_edges: source.physical_surface_energy_edges(),
743 inverse_energy_product: source
744 .exact_inverse_energy_product()
745 .expect("exact source has an inverse energy product"),
746 active_loop_count: source.active_loop_count(),
747 contract_subgraph: source.contract_subgraph(),
748 };
749 crate::debug_tags!(#generation, #uv, #local, #four_d, #cff, #profile;
750 stage = "exact_cff_preparation",
751 graph = %self.name,
752 elapsed_ms = preparation_started.elapsed().as_secs_f64() * 1000.0,
753 degree_analysis_ms,
754 source_mapping_ms,
755 allocation_ms,
756 proposal_count = preparation.energy_assignment_plans.len(),
757 "Prepared factorized energy assignments and certified source mapping"
758 );
759 Ok(preparation)
760 }
761
762 pub(crate) fn generate_3d_expression_for_4d_term(
763 &self,
764 preparation: &ExactCffGenerationPreparation,
765 mut context: Option<&mut Local4dProjectionContext>,
766 ) -> Result<(
767 GeneratedThreeDExpression,
768 Arc<PlannedExactSourceNumerator>,
769 Arc<EnergyPowerAssignmentPlan>,
770 CffEnergyDegreeBoundReport,
771 )> {
772 let ExactCffGenerationPreparation {
773 parsed,
774 energy_edges,
775 source_options,
776 exact_source_energy_mapper,
777 energy_assignment_plans,
778 physical_energy_degree_bounds,
779 ..
780 } = preparation;
781 let mut source_options = source_options.clone();
782 let selection_started = std::time::Instant::now();
783 let mut certificate_time = std::time::Duration::ZERO;
784 let mut native_times = BTreeMap::new();
785 let mut template_build_times = BTreeMap::new();
786 let mut template_time = Duration::ZERO;
787 let generate = |source_options: &Generate3DExpressionOptions| {
788 crate::debug_tags!(#generation, #uv, #local, #four_d, #cff, #profile;
789 graph = %self.name,
790 term_local_bounds = ?source_options.energy_degree_bounds,
791 file.parsed_source = ?parsed,
792 "Generating exact CFF at its term-local capacity"
793 );
794 three_dimensional_reps::generate_3d_expression(preparation, source_options).map_err(
795 |error| {
796 eyre::eyre!(
797 "generalized CFF expression generation failed for exact 4D source in graph `{}` with physical EMR bounds {:?} and term-local exact-occurrence bounds {:?}: {error}\n{}",
798 self.name,
799 physical_energy_degree_bounds,
800 source_options.energy_degree_bounds,
801 three_d_source_summary(parsed),
802 )
803 },
804 )
805 };
806 type Candidate = (
807 usize,
808 Vec<usize>,
809 Arc<PlannedExactSourceNumerator>,
810 Option<GeneratedThreeDExpression>,
811 );
812 let mut selected: Option<Candidate> = None;
813 let mut pending = VecDeque::from(energy_assignment_plans.clone());
814 let mut seen_bounds = Vec::new();
815 let mut challenged = false;
816 loop {
817 if pending.is_empty() && !challenged {
818 challenged = true;
819 if let Some((_, _, best, _)) = &selected
820 && let Some(challenger) = best
821 .binding
822 .binding
823 .assignment
824 .placement_challenger(&seen_bounds)?
825 {
826 pending.push_back(Arc::new(challenger));
827 }
828 }
829 let Some(plan) = pending.pop_front() else {
830 break;
831 };
832 let proposal = seen_bounds.len();
833 let certificate_started = std::time::Instant::now();
834 exact_source_energy_mapper.certify_assignment(&plan)?;
835 certificate_time += certificate_started.elapsed();
836 let (numerator, build_time, cache_time) = PlannedExactSourceNumerator::prepare(
840 Arc::clone(exact_source_energy_mapper),
841 Arc::clone(&plan),
842 context.as_deref_mut(),
843 )?;
844 template_time += build_time + cache_time;
845 template_build_times.insert(plan.energy_degree_bounds().to_vec(), build_time);
846 seen_bounds.push(plan.energy_degree_bounds().to_vec());
847 let mut rank_envelope = plan
848 .energy_degree_bounds()
849 .iter()
850 .map(|(_, degree)| *degree)
851 .collect::<Vec<_>>();
852 rank_envelope.sort_unstable_by(|left, right| right.cmp(left));
853 source_options.energy_degree_bounds = Some(plan.energy_degree_bounds().to_vec());
854 let key =
855 ExactCffGenerationCache::generation_key(parsed, energy_edges, &source_options);
856 let known_count = context
857 .as_deref_mut()
858 .map(|context| &mut context.generation_cache)
859 .and_then(|cache| cache.count(&key));
860 let started = std::time::Instant::now();
861 let generated = if known_count.is_some() {
865 None
866 } else {
867 if let Some(cache) = context
868 .as_deref_mut()
869 .map(|context| &mut context.generation_cache)
870 {
871 cache.native_generations += 1;
872 }
873 let native_started = std::time::Instant::now();
874 let generated = generate(&source_options)?;
875 native_times.insert(
876 plan.energy_degree_bounds().to_vec(),
877 native_started.elapsed(),
878 );
879 Some(generated)
880 };
881 let map_count = known_count.unwrap_or_else(|| {
882 generated
883 .as_ref()
884 .expect("an unknown candidate was freshly generated")
885 .expression
886 .orientations
887 .len()
888 });
889 if known_count.is_none()
890 && let Some(cache) = context
891 .as_deref_mut()
892 .map(|context| &mut context.generation_cache)
893 {
894 cache.record_count(key, map_count);
895 }
896 crate::debug_tags!(#generation, #uv, #local, #four_d, #cff, #profile;
897 graph = %self.name,
898 proposal,
899 native_source_maps = map_count,
900 bounds = ?source_options.energy_degree_bounds,
901 count_memo_hit = known_count.is_some(),
902 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
903 "Scored bounded exact-CFF assignment proposal"
904 );
905 if selected
910 .as_ref()
911 .is_none_or(|(best_count, best_envelope, _, _)| {
912 (map_count, &rank_envelope) < (*best_count, best_envelope)
913 })
914 {
915 selected = Some((map_count, rank_envelope, numerator, generated));
916 }
917 }
918 let (selected_count, _, numerator, generated) =
919 selected.expect("rank planning always provides a baseline assignment");
920 let energy_assignment_plan = Arc::clone(&numerator.binding.binding.assignment);
921 source_options.energy_degree_bounds =
922 Some(energy_assignment_plan.energy_degree_bounds().to_vec());
923 let key = ExactCffGenerationCache::generation_key(parsed, energy_edges, &source_options);
924 let cached = context
925 .as_deref_mut()
926 .map(|context| &mut context.generation_cache)
927 .and_then(|cache| cache.entries.get(&key))
928 .and_then(|entry| entry.payload.as_ref());
929 let cache_hit = cached.is_some();
930 let generated = if let Some(generated) = generated {
931 generated
932 } else if let Some(cached) = cached {
933 cached.clone()
934 } else {
935 if let Some(cache) = context
938 .as_deref_mut()
939 .map(|context| &mut context.generation_cache)
940 {
941 cache.native_generations += 1;
942 }
943 let native_started = std::time::Instant::now();
944 let generated = generate(&source_options)?;
945 *native_times
946 .entry(energy_assignment_plan.energy_degree_bounds().to_vec())
947 .or_default() += native_started.elapsed();
948 generated
949 };
950 debug_assert_eq!(generated.expression.orientations.len(), selected_count);
951 if !cache_hit && let Some(context) = context {
952 let cache = &mut context.generation_cache;
953 cache.record_payload(key, &generated);
958 }
959 let winning_native_time = native_times
960 .get(energy_assignment_plan.energy_degree_bounds())
961 .copied()
962 .unwrap_or_default();
963 let native_time = native_times.values().copied().sum::<std::time::Duration>();
964 let winning_template_build_time = template_build_times
965 .get(energy_assignment_plan.energy_degree_bounds())
966 .copied()
967 .unwrap_or_default();
968 let template_build_time = template_build_times.values().copied().sum::<Duration>();
969 let selection_time = selection_started.elapsed();
970 crate::debug_tags!(#generation, #uv, #local, #four_d, #cff, #profile;
971 stage = "exact_cff_assignment_selection",
972 graph = %self.name,
973 admitted_candidates = seen_bounds.len(),
974 native_rows = selected_count,
975 candidate_certificate_ms = certificate_time.as_secs_f64() * 1000.0,
976 winning_native_ms = winning_native_time.as_secs_f64() * 1000.0,
977 losing_native_ms = (native_time - winning_native_time).as_secs_f64() * 1000.0,
978 winning_template_build_ms = winning_template_build_time.as_secs_f64() * 1000.0,
979 losing_template_build_ms = (template_build_time - winning_template_build_time).as_secs_f64() * 1000.0,
980 selection_overhead_ms = selection_time.saturating_sub(winning_native_time).saturating_sub(certificate_time).saturating_sub(template_time).as_secs_f64() * 1000.0,
981 native_payload_cache_hit = cache_hit,
982 "Selected a certified exact CFF assignment; losing trials count as selection overhead"
983 );
984 let energy_degree_bound_report = CffEnergyDegreeBoundReport {
985 source_kind: CffEnergyBoundSourceKind::ExactFourD,
986 physical_parent_bounds: physical_energy_degree_bounds.clone(),
987 assigned_cff_source_bounds: energy_assignment_plan.energy_degree_bounds().to_vec(),
988 };
989 Ok((
990 generated,
991 numerator,
992 energy_assignment_plan,
993 energy_degree_bound_report,
994 ))
995 }
996
997 pub fn generate_3d_expression_for_integrand(
998 &mut self,
999 contract_edges: &[EdgeIndex],
1000 canonize_esurface: &Option<ShiftRewrite>,
1001 options: &Generate3DExpressionOptions,
1002 analysis_numerator: Option<&Atom>,
1003 ) -> Result<GeneratedThreeDExpression<Esurface, Hsurface>> {
1004 let generated = self.generate_raw_3d_expression_for_integrand(
1005 contract_edges,
1006 options,
1007 analysis_numerator,
1008 )?;
1009 let initial_state_cut_edges = self
1010 .iter_edges_of(&self.initial_state_cut)
1011 .map(|(_, edge, _)| edge)
1012 .collect::<Vec<_>>();
1013 self.convert_generated_expression_surfaces(
1014 generated,
1015 canonize_esurface,
1016 &initial_state_cut_edges,
1017 )
1018 }
1019
1020 pub(crate) fn generate_raw_3d_expression_for_integrand(
1023 &self,
1024 contract_edges: &[EdgeIndex],
1025 options: &Generate3DExpressionOptions,
1026 analysis_numerator: Option<&Atom>,
1027 ) -> Result<GeneratedThreeDExpression> {
1028 let request_started = Instant::now();
1029 let initial_state_cut_edges = self
1030 .iter_edges_of(&self.initial_state_cut)
1031 .map(|(_, edge_id, _)| edge_id)
1032 .collect_vec();
1033 let source_contract_edges =
1034 source_contract_edges_for_3d_expression(contract_edges, &initial_state_cut_edges);
1035 if !source_contract_edges.is_empty() {
1036 debug!(
1037 "contracting {} internal edges before generalized 3D expression generation",
1038 source_contract_edges.len()
1039 );
1040 }
1041 let reconstruction_started = Instant::now();
1042 let source = GraphThreeDSource::new(self, &source_contract_edges)?;
1043 let reconstruction_time = reconstruction_started.elapsed();
1044 let bridge_edges = self
1045 .iter_edges_of(&self.tree_edges)
1046 .map(|(_, edge_id, _)| edge_id)
1047 .filter(|edge_id| !initial_state_cut_edges.contains(edge_id))
1048 .collect::<HashSet<_>>();
1049 for edge_id in source_contract_edges
1050 .iter()
1051 .filter(|edge_id| bridge_edges.contains(edge_id))
1052 {
1053 let Some(coordinates) = source.reconstructible_outer_loop_coordinates(*edge_id) else {
1054 return Err(eyre::eyre!(
1055 "contracted bridge edge {} retains an inner-loop energy coordinate and cannot be projected independently of the CFF source",
1056 usize::from(*edge_id),
1057 ));
1058 };
1059 if coordinates.iter().any(|coordinate| *coordinate != 0) {
1060 return Err(eyre::eyre!(
1061 "contracted bridge edge {} has nonzero outer-loop coordinates {:?}; a CFF-external tree edge must have a purely external affine energy",
1062 usize::from(*edge_id),
1063 coordinates,
1064 ));
1065 }
1066 }
1067 let mut source_options = options.clone();
1068 if let Some(numerator) = analysis_numerator {
1069 source_options.energy_degree_bounds = Some(
1070 self.automatic_numerator_energy_degree_bounds_in_atoms_excluding_with_min_degree(
1071 [numerator],
1072 initial_state_cut_edges.iter().chain(&bridge_edges).copied(),
1073 1,
1074 )?,
1075 );
1076 debug!(
1077 graph = %self.name,
1078 bounds = ?source_options.energy_degree_bounds,
1079 "using source-edge numerator energy-degree bounds"
1080 );
1081 }
1082 if let Some(bounds) = &mut source_options.energy_degree_bounds {
1083 bounds.retain(|(edge_id, _)| {
1089 let edge_id = EdgeIndex(*edge_id);
1090 !initial_state_cut_edges.contains(&edge_id) && !bridge_edges.contains(&edge_id)
1091 });
1092 }
1093 for &(edge_id, degree) in source_options
1094 .energy_degree_bounds
1095 .as_deref()
1096 .unwrap_or(&[])
1097 {
1098 let edge_id = EdgeIndex(edge_id);
1099 if !source_contract_edges.contains(&edge_id) {
1100 continue;
1101 }
1102 if source
1103 .reconstructible_outer_loop_coordinates(edge_id)
1104 .is_some()
1105 {
1106 return Err(eyre::eyre!(
1107 "numerator energy-degree bound {degree} for shrunken EMR edge {} cannot yet be supplied to generalized CFF generation: its exact outer affine energy is retained for numerator evaluation, but the standalone generator has no numerator-only/shrunken-edge bound channel",
1108 usize::from(edge_id),
1109 ));
1110 }
1111 return Err(eyre::eyre!(
1112 "numerator energy-degree bound {degree} for shrunken EMR edge {} retains contracted inner-loop dependence and cannot be represented in the outer CFF source",
1113 usize::from(edge_id),
1114 ));
1115 }
1116 let preparation_time = request_started
1117 .elapsed()
1118 .saturating_sub(reconstruction_time);
1119 let native_started = Instant::now();
1120 let mut generated = {
1121 let result = three_dimensional_reps::generate_3d_expression(&source, &source_options);
1122 result.map_err(|error| {
1123 let source_summary = source
1124 .to_three_d_parsed_graph()
1125 .map(|parsed| three_d_source_summary(&parsed))
1126 .unwrap_or_else(|source_error| {
1127 format!("failed to rebuild 3D source summary: {source_error}")
1128 });
1129 eyre::eyre!(
1130 "generalized CFF expression generation failed for graph `{}` with source-edge numerator energy-degree bounds {:?}: {error}\n{source_summary}",
1131 self.name,
1132 source_options.energy_degree_bounds,
1133 )
1134 })
1135 }?;
1136 let native_time = native_started.elapsed();
1137
1138 for edge_id in &source_contract_edges {
1144 let Some(coordinates) = source.reconstructible_outer_loop_coordinates(*edge_id) else {
1145 continue;
1146 };
1147 let signature = &self.loop_momentum_basis.edge_signatures[*edge_id];
1148 for orientation in generated.expression.orientations.iter_mut() {
1149 if coordinates.len() != orientation.loop_energy_map.len() {
1150 return Err(eyre::eyre!(
1151 "contracted edge {} has {} outer coordinates for {} generated loop-energy maps",
1152 usize::from(*edge_id),
1153 coordinates.len(),
1154 orientation.loop_energy_map.len(),
1155 ));
1156 }
1157 let mut edge_energy = coordinates.iter().zip(&orientation.loop_energy_map).fold(
1158 LinearEnergyExpr::zero(),
1159 |sum, (coefficient, loop_energy)| {
1160 sum + loop_energy.clone().scale_rational(coefficient.clone())
1161 },
1162 );
1163 for (external_edge, sign) in self
1164 .loop_momentum_basis
1165 .ext_edges
1166 .iter()
1167 .zip(&signature.external)
1168 {
1169 let coefficient = match sign {
1170 SignOrZero::Zero => 0,
1171 SignOrZero::Plus => 1,
1172 SignOrZero::Minus => -1,
1173 };
1174 edge_energy =
1175 edge_energy + LinearEnergyExpr::external(*external_edge, coefficient);
1176 }
1177 let edge_energy_slot = orientation
1178 .edge_energy_map
1179 .get_mut(usize::from(*edge_id))
1180 .ok_or_else(|| {
1181 eyre::eyre!(
1182 "contracted edge {} is outside the generated parent energy map",
1183 usize::from(*edge_id),
1184 )
1185 })?;
1186 *edge_energy_slot = edge_energy;
1187 }
1188 }
1189
1190 let elapsed = request_started.elapsed();
1191 crate::debug_tags!(#generation, #cff, #profile;
1192 stage = "raw_cff_generation",
1193 graph = %self.name,
1194 native_source_maps = generated.expression.orientations.len(),
1195 elapsed_ms = elapsed.as_secs_f64() * 1000.0,
1196 source_reconstruction_ms = reconstruction_time.as_secs_f64() * 1000.0,
1197 preparation_ms = preparation_time.as_secs_f64() * 1000.0,
1198 native_generation_ms = native_time.as_secs_f64() * 1000.0,
1199 postprocessing_ms = elapsed.saturating_sub(reconstruction_time + preparation_time + native_time).as_secs_f64() * 1000.0,
1200 "Generated a raw CFF request before surface conversion"
1201 );
1202 Ok(generated)
1203 }
1204
1205 pub fn production_cff_3d_expression_options(
1206 &self,
1207 settings: &GenerationSettings,
1208 ) -> Result<Generate3DExpressionOptions> {
1209 let mut options = self.cff_3d_expression_options(numerator_sampling_scale_mode(
1210 settings.uniform_numerator_sampling_scale,
1211 ))?;
1212 options.medium_mode = settings.medium.mode;
1213 Ok(options)
1214 }
1215
1216 pub fn cff_3d_expression_options(
1217 &self,
1218 numerator_sampling_scale: NumeratorSamplingScaleMode,
1219 ) -> Result<Generate3DExpressionOptions> {
1220 let numerator = self.production_numerator_atom_for_full_3d_expression();
1221 let cff_external_edges = self
1222 .iter_edges_of(&self.initial_state_cut)
1223 .chain(self.iter_edges_of(&self.tree_edges))
1224 .map(|(_, edge_id, _)| edge_id);
1225 let energy_degree_bounds = self
1232 .automatic_numerator_energy_degree_bounds_in_atoms_excluding_with_min_degree(
1233 [&numerator],
1234 cff_external_edges,
1235 1,
1236 )?;
1237 debug!(
1238 graph = %self.name,
1239 bounds = ?energy_degree_bounds,
1240 "using production CFF source-edge numerator energy-degree bounds"
1241 );
1242 Ok(Generate3DExpressionOptions {
1243 medium_mode: Default::default(),
1244 representation: RepresentationMode::Cff,
1245 cff_generation_context: CffGenerationContext::Standalone,
1246 energy_degree_bounds: Some(energy_degree_bounds),
1247 numerator_sampling_scale,
1248 preserve_internal_edges_as_four_d_denominators: Vec::new(),
1249 })
1250 }
1251
1252 #[cfg(test)]
1253 pub(crate) fn denominator_only_cff_3d_expression_options(&self) -> Generate3DExpressionOptions {
1254 Generate3DExpressionOptions {
1255 medium_mode: Default::default(),
1256 representation: RepresentationMode::Cff,
1257 cff_generation_context: CffGenerationContext::Standalone,
1258 energy_degree_bounds: Some(Vec::new()),
1259 numerator_sampling_scale: NumeratorSamplingScaleMode::None,
1260 preserve_internal_edges_as_four_d_denominators: Vec::new(),
1261 }
1262 }
1263
1264 pub(crate) fn convert_generated_expression_surfaces(
1265 &mut self,
1266 generated: GeneratedThreeDExpression,
1267 canonize_esurface: &Option<ShiftRewrite>,
1268 initial_state_cut_edges: &[EdgeIndex],
1269 ) -> Result<GeneratedThreeDExpression<Esurface, Hsurface>> {
1270 Ok(self
1271 .convert_generated_expression_surfaces_impl(
1272 generated,
1273 canonize_esurface,
1274 initial_state_cut_edges,
1275 None,
1276 )?
1277 .0)
1278 }
1279
1280 pub(crate) fn convert_4d_expression_surfaces(
1281 &mut self,
1282 generated: GeneratedThreeDExpression,
1283 physical_surfaces: &[Option<LinearSurface>],
1284 ) -> Result<(
1285 GeneratedThreeDExpression<Esurface, Hsurface>,
1286 BTreeMap<EsurfaceID, CffEnergyFactorOwnership>,
1287 )> {
1288 let canonize_esurface = self.get_esurface_canonization(&self.loop_momentum_basis);
1289 let initial_state_cut_edges = self
1290 .iter_edges_of(&self.initial_state_cut)
1291 .map(|(_, edge_id, _)| edge_id)
1292 .collect_vec();
1293 self.convert_generated_expression_surfaces_impl(
1294 generated,
1295 &canonize_esurface,
1296 &initial_state_cut_edges,
1297 Some(physical_surfaces),
1298 )
1299 }
1300
1301 fn convert_generated_expression_surfaces_impl(
1302 &mut self,
1303 generated: GeneratedThreeDExpression,
1304 canonize_esurface: &Option<ShiftRewrite>,
1305 initial_state_cut_edges: &[EdgeIndex],
1306 physical_surfaces: Option<&[Option<LinearSurface>]>,
1307 ) -> Result<(
1308 GeneratedThreeDExpression<Esurface, Hsurface>,
1309 BTreeMap<EsurfaceID, CffEnergyFactorOwnership>,
1310 )> {
1311 let GeneratedThreeDExpression {
1312 mut expression,
1313 energy_factor_ownership,
1314 energy_factor_components,
1315 source_energy_degree_bounds,
1316 source_frame,
1317 } = generated;
1318 if !expression.residual_denominators.is_empty() {
1319 return Err(eyre::eyre!(
1320 "GammaLoop production CFF conversion received residual four-dimensional denominator edges [{}]; production callers must contract tree edges and attach their projected denominators separately",
1321 expression
1322 .residual_denominators
1323 .iter()
1324 .map(|denominator| denominator.edge_id.0)
1325 .join(", "),
1326 ));
1327 }
1328 let mut linear_surface_components = BTreeMap::new();
1329 for (surface_id, surface) in expression.surfaces.linear_surface_cache.iter_enumerated() {
1330 let mut surface_component = None;
1331 for (edge_id, _) in &surface.expression.internal_terms {
1332 for (component_index, component) in energy_factor_components.iter().enumerate() {
1333 if !component.internal_edge_ids.contains(&usize::from(*edge_id)) {
1334 continue;
1335 }
1336 if let Some(previous) = surface_component
1337 && previous != component_index
1338 {
1339 return Err(eyre::eyre!(
1340 "generated causal surface {surface_id:?} spans disconnected energy-factor components"
1341 ));
1342 }
1343 surface_component = Some(component_index);
1344 }
1345 }
1346 if let Some(component_index) = surface_component {
1347 linear_surface_components.insert(surface_id, component_index);
1348 }
1349 }
1350 let mut linear_surface_map = BTreeMap::<LinearSurfaceID, SurfaceMapEntry>::new();
1351 let mut retained_linear_surfaces = Vec::new();
1352 for (linear_surface_id, surface) in
1353 expression.surfaces.linear_surface_cache.iter_enumerated()
1354 {
1355 let physical = physical_surfaces
1356 .and_then(|surfaces| surfaces.get(usize::from(linear_surface_id)))
1357 .and_then(Option::as_ref);
1358 if physical_surfaces.is_some() && physical.is_none() {
1359 retained_linear_surfaces.push((linear_surface_id, surface.clone()));
1360 } else {
1361 let converted = self.intern_generated_linear_surface(
1362 physical.unwrap_or(surface),
1363 canonize_esurface,
1364 initial_state_cut_edges,
1365 )?;
1366 linear_surface_map.insert(linear_surface_id, converted);
1367 }
1368 }
1369 let mut surface_cache = self.surface_cache.clone();
1370 for (source_id, surface) in retained_linear_surfaces {
1371 let target_id = LinearSurfaceID(surface_cache.linear_surface_cache.len());
1372 surface_cache.linear_surface_cache.push(surface);
1373 linear_surface_map.insert(
1374 source_id,
1375 SurfaceMapEntry {
1376 surface_id: HybridSurfaceID::Linear(target_id),
1377 sign: 1,
1378 },
1379 );
1380 }
1381 let mut mapped_surface_components = BTreeMap::<EsurfaceID, usize>::new();
1382 for (source_id, component_index) in linear_surface_components {
1383 let Some(SurfaceMapEntry {
1384 surface_id: HybridSurfaceID::Esurface(target_id),
1385 ..
1386 }) = linear_surface_map.get(&source_id)
1387 else {
1388 continue;
1389 };
1390 if let Some(previous) = mapped_surface_components.insert(*target_id, component_index)
1391 && previous != component_index
1392 {
1393 return Err(eyre::eyre!(
1394 "physical causal surface {target_id:?} merges disconnected energy-factor components"
1395 ));
1396 }
1397 }
1398 let mapped_surface_ownership = mapped_surface_components
1399 .into_iter()
1400 .map(|(surface_id, component_index)| {
1401 (
1402 surface_id,
1403 energy_factor_components[component_index].ownership,
1404 )
1405 })
1406 .collect();
1407 for orientation in expression.orientations.iter_mut() {
1408 let mut remapped_variants = Vec::with_capacity(orientation.variants.len());
1409 for mut variant in std::mem::take(&mut orientation.variants) {
1410 let signed_denominators =
1411 remap_denominator_tree_surface_ids(&variant.denominator, &linear_surface_map);
1412 for surface_id in &mut variant.numerator_surfaces {
1413 let numerator_sign =
1414 remap_generated_surface_id(surface_id, &linear_surface_map);
1415 if numerator_sign < 0 {
1416 variant.prefactor = -variant.prefactor;
1417 }
1418 }
1419 match energy_factor_ownership {
1420 CffEnergyFactorOwnership::GlobalSourceProduct => {
1421 variant.half_edges.clear();
1425 }
1426 CffEnergyFactorOwnership::VariantLocal => {}
1430 }
1431 for remapped_denominator in signed_denominators {
1432 let mut signed_variant = variant.clone();
1433 signed_variant.denominator = remapped_denominator.denominator;
1434 signed_variant.denominator_surface_signs = remapped_denominator.surface_signs;
1435 signed_variant.prefactor *= Rational::from(remapped_denominator.prefactor);
1436 remapped_variants.push(signed_variant);
1437 }
1438 }
1439 orientation.variants = remapped_variants;
1440 }
1441
1442 Ok((
1443 GeneratedThreeDExpression {
1444 expression: CFFExpression {
1445 orientations: expression.orientations,
1446 surfaces: surface_cache,
1447 residual_denominators: Vec::new(),
1448 },
1449 energy_factor_ownership,
1450 energy_factor_components,
1451 source_energy_degree_bounds,
1452 source_frame,
1453 },
1454 mapped_surface_ownership,
1455 ))
1456 }
1457
1458 fn intern_generated_linear_surface(
1459 &mut self,
1460 surface: &LinearSurface,
1461 canonize_esurface: &Option<ShiftRewrite>,
1462 initial_state_cut_edges: &[EdgeIndex],
1463 ) -> Result<SurfaceMapEntry> {
1464 if surface.numerator_only
1465 && (!surface.expression.uniform_scale_coeff.is_zero()
1466 || !surface.expression.constant.is_zero())
1467 {
1468 let surface_id =
1469 Into::<LinearSurfaceID>::into(self.surface_cache.linear_surface_cache.len());
1470 self.surface_cache
1471 .linear_surface_cache
1472 .push(surface.clone());
1473 return Ok(SurfaceMapEntry {
1474 surface_id: HybridSurfaceID::Linear(surface_id),
1475 sign: 1,
1476 });
1477 }
1478
1479 if !surface.expression.uniform_scale_coeff.is_zero()
1480 || !surface.expression.constant.is_zero()
1481 {
1482 return Err(eyre::eyre!(
1483 "generalized CFF production cannot convert non-homogeneous linear surface {:?}",
1484 surface
1485 ));
1486 }
1487
1488 let mut positive_energies = Vec::new();
1489 let mut negative_energies = Vec::new();
1490 let mut external_shift = Vec::new();
1491 collect_linear_surface_terms(
1492 &surface.expression.internal_terms,
1493 initial_state_cut_edges,
1494 &mut positive_energies,
1495 &mut negative_energies,
1496 &mut external_shift,
1497 )?;
1498 for (edge_id, coeff) in &surface.expression.external_terms {
1499 let coeff = integer_coeff(coeff)?;
1500 external_shift.push((*edge_id, coeff));
1501 }
1502 positive_energies.sort();
1503 negative_energies.sort();
1504 external_shift.sort_by_key(|(edge_id, _)| *edge_id);
1505
1506 let (surface_id, sign) = match surface.kind {
1507 LinearSurfaceKind::Esurface => {
1508 let mut sign = 1;
1509 if positive_energies.is_empty() && !negative_energies.is_empty() {
1510 sign = -1;
1511 positive_energies = negative_energies;
1512 negative_energies = Vec::new();
1513 for (_, coeff) in &mut external_shift {
1514 *coeff = -*coeff;
1515 }
1516 }
1517 if !negative_energies.is_empty() {
1518 return Err(eyre::eyre!(
1519 "generalized 3D production cannot convert E-surface with mixed-sign internal terms {:?}",
1520 surface
1521 ));
1522 }
1523 let mut esurface = Esurface {
1524 energies: positive_energies,
1525 external_shift,
1526 vertex_set: VertexSet::dummy(),
1527 };
1528 if let Some(shift_rewrite) = canonize_esurface {
1529 esurface.canonicalize_shift(shift_rewrite);
1530 }
1531
1532 let esurface_id = self
1533 .surface_cache
1534 .esurface_cache
1535 .position(|existing| *existing == esurface)
1536 .unwrap_or_else(|| {
1537 self.surface_cache.esurface_cache.push(esurface);
1538 Into::<EsurfaceID>::into(self.surface_cache.esurface_cache.len() - 1)
1539 });
1540
1541 (HybridSurfaceID::Esurface(esurface_id), sign)
1542 }
1543 LinearSurfaceKind::Hsurface => {
1544 let hsurface = Hsurface {
1545 positive_energies,
1546 negative_energies,
1547 external_shift,
1548 vertex_set: VertexSet::dummy(),
1549 };
1550 let hsurface_id = self
1551 .surface_cache
1552 .hsurface_cache
1553 .position(|existing| existing == &hsurface)
1554 .unwrap_or_else(|| {
1555 self.surface_cache.hsurface_cache.push(hsurface);
1556 Into::<HsurfaceID>::into(self.surface_cache.hsurface_cache.len() - 1)
1557 });
1558
1559 (HybridSurfaceID::Hsurface(hsurface_id), 1)
1560 }
1561 };
1562 Ok(SurfaceMapEntry { surface_id, sign })
1563 }
1564}
1565
1566fn three_d_source_summary(parsed: &three_dimensional_reps::ParsedGraph) -> String {
1567 let internal_edges = parsed
1568 .internal_edges
1569 .iter()
1570 .map(|edge| {
1571 format!(
1572 "edge {}: {} -> {}, loops={:?}, externals={:?}",
1573 edge.edge_id,
1574 edge.tail,
1575 edge.head,
1576 edge.signature.loop_signature,
1577 edge.signature.external_signature
1578 )
1579 })
1580 .join("; ");
1581 format!(
1582 "3D source has {} loop names {:?}, {} internal edges [{}]",
1583 parsed.loop_names.len(),
1584 parsed.loop_names,
1585 parsed.internal_edges.len(),
1586 internal_edges
1587 )
1588}
1589
1590fn source_contract_edges_for_3d_expression(
1591 contract_edges: &[EdgeIndex],
1592 initial_state_cut_edges: &[EdgeIndex],
1593) -> Vec<EdgeIndex> {
1594 let non_contractible_edges = initial_state_cut_edges
1595 .iter()
1596 .copied()
1597 .collect::<HashSet<_>>();
1598 contract_edges
1599 .iter()
1600 .copied()
1601 .filter(|edge_id| !non_contractible_edges.contains(edge_id))
1602 .collect_vec()
1603}
1604
1605fn remap_generated_surface_id(
1606 surface_id: &mut HybridSurfaceID,
1607 linear_surface_map: &BTreeMap<LinearSurfaceID, SurfaceMapEntry>,
1608) -> i64 {
1609 if let HybridSurfaceID::Linear(linear_surface_id) = surface_id {
1610 let entry = *linear_surface_map
1611 .get(linear_surface_id)
1612 .expect("all generated linear surfaces should have been interned");
1613 *surface_id = entry.surface_id;
1614 entry.sign
1615 } else {
1616 1
1617 }
1618}
1619
1620fn remap_denominator_tree_surface_ids(
1621 denominator: &Tree<HybridSurfaceID>,
1622 linear_surface_map: &BTreeMap<LinearSurfaceID, SurfaceMapEntry>,
1623) -> Vec<RemappedDenominatorTree> {
1624 let node_signs = denominator
1625 .iter_nodes()
1626 .map(|node| {
1627 let sign = if let HybridSurfaceID::Linear(linear_surface_id) = node.data {
1628 linear_surface_map
1629 .get(&linear_surface_id)
1630 .map(|entry| entry.sign)
1631 .unwrap_or(1)
1632 } else {
1633 1
1634 };
1635 (node.node_id, sign)
1636 })
1637 .collect::<BTreeMap<_, _>>();
1638
1639 let mut chains_by_signature =
1640 BTreeMap::<(i64, Vec<(HybridSurfaceID, i64)>), BTreeMap<Vec<HybridSurfaceID>, usize>>::new(
1641 );
1642 for leaf in denominator.get_bottom_layer() {
1643 let mut sign = 1;
1644 let mut surface_signs = BTreeMap::<HybridSurfaceID, i64>::new();
1645 let mut chain = Vec::new();
1646 let mut current = Some(leaf);
1647 while let Some(node_id) = current {
1648 let mut surface_id = denominator.get_node(node_id).data;
1649 let node_sign = node_signs.get(&node_id).copied().unwrap_or(1);
1650 sign *= node_sign;
1651 remap_generated_surface_id(&mut surface_id, linear_surface_map);
1652 if surface_id != HybridSurfaceID::Unit {
1653 if node_sign < 0 {
1654 let entry = surface_signs.entry(surface_id).or_insert(1);
1655 *entry *= node_sign;
1656 if *entry > 0 {
1657 surface_signs.remove(&surface_id);
1658 }
1659 }
1660 chain.push(surface_id);
1661 }
1662 current = denominator.get_node(node_id).parent;
1663 }
1664 chain.reverse();
1665 chains_by_signature
1666 .entry((sign, surface_signs.into_iter().collect()))
1667 .or_default()
1668 .entry(chain)
1669 .and_modify(|multiplicity| *multiplicity += 1)
1670 .or_insert(1);
1671 }
1672
1673 if chains_by_signature.is_empty() {
1674 return vec![RemappedDenominatorTree {
1675 prefactor: 1,
1676 surface_signs: BTreeMap::new(),
1677 denominator: Tree::from_root(HybridSurfaceID::Unit),
1678 }];
1679 }
1680
1681 chains_by_signature
1682 .into_iter()
1683 .flat_map(|((prefactor_sign, surface_signs), chains)| {
1684 let mut chains_by_multiplicity = BTreeMap::<usize, Vec<Vec<HybridSurfaceID>>>::new();
1689 for (chain, multiplicity) in chains {
1690 chains_by_multiplicity
1691 .entry(multiplicity)
1692 .or_default()
1693 .push(chain);
1694 }
1695 chains_by_multiplicity
1696 .into_iter()
1697 .map(move |(multiplicity, chains)| RemappedDenominatorTree {
1698 prefactor: prefactor_sign
1699 * i64::try_from(multiplicity)
1700 .expect("denominator-chain multiplicity should fit in i64"),
1701 surface_signs: surface_signs.iter().copied().collect(),
1702 denominator: denominator_tree_from_chains(&chains),
1703 })
1704 })
1705 .collect()
1706}
1707
1708fn denominator_tree_from_chains(chains: &[Vec<HybridSurfaceID>]) -> Tree<HybridSurfaceID> {
1709 if chains.is_empty() || chains.iter().all(Vec::is_empty) {
1710 return Tree::from_root(HybridSurfaceID::Unit);
1711 }
1712
1713 let mut tree = Tree::from_root(HybridSurfaceID::Unit);
1714 for chain in chains {
1715 if chain.is_empty() {
1716 insert_terminal_unit_if_missing(&mut tree, NodeId::root());
1717 continue;
1718 }
1719 let mut parent = NodeId::root();
1720 for surface_id in chain {
1721 let existing_child = tree
1722 .get_node(parent)
1723 .children
1724 .iter()
1725 .copied()
1726 .find(|child| tree.get_node(*child).data == *surface_id);
1727 if let Some(child) = existing_child {
1728 parent = child;
1729 continue;
1730 }
1731 let child = NodeId(tree.get_num_nodes());
1732 tree.insert_node(parent, *surface_id);
1733 parent = child;
1734 }
1735 insert_terminal_unit_if_missing(&mut tree, parent);
1736 }
1737 tree
1738}
1739
1740fn insert_terminal_unit_if_missing(tree: &mut Tree<HybridSurfaceID>, parent: NodeId) {
1741 let has_terminal_unit = tree
1742 .get_node(parent)
1743 .children
1744 .iter()
1745 .any(|child| tree.get_node(*child).data == HybridSurfaceID::Unit);
1746 if !has_terminal_unit {
1747 tree.insert_node(parent, HybridSurfaceID::Unit);
1748 }
1749}
1750
1751fn collect_linear_surface_terms(
1752 terms: &[(EdgeIndex, Rational)],
1753 initial_state_cut_edges: &[EdgeIndex],
1754 positive_energies: &mut Vec<EdgeIndex>,
1755 negative_energies: &mut Vec<EdgeIndex>,
1756 external_shift: &mut Vec<(EdgeIndex, i64)>,
1757) -> Result<()> {
1758 for (edge_id, coeff) in terms {
1759 let coeff = integer_coeff(coeff)?;
1760 if coeff == 0 {
1761 continue;
1762 }
1763 if initial_state_cut_edges.contains(edge_id) {
1764 external_shift.push((*edge_id, -coeff));
1767 continue;
1768 }
1769
1770 let target = if coeff > 0 {
1771 &mut *positive_energies
1772 } else {
1773 &mut *negative_energies
1774 };
1775 for _ in 0..coeff.unsigned_abs() {
1776 target.push(*edge_id);
1777 }
1778 }
1779 Ok(())
1780}
1781
1782fn integer_coeff(coeff: &Rational) -> Result<i64> {
1783 coeff
1784 .is_integer()
1785 .then(|| coeff.numerator_ref().to_i64())
1786 .flatten()
1787 .ok_or_else(|| eyre::eyre!("expected integer linear-surface coefficient, found {coeff}"))
1788}
1789
1790fn numerator_sampling_scale_mode(
1791 setting: UniformNumeratorSamplingScale,
1792) -> NumeratorSamplingScaleMode {
1793 match setting {
1794 UniformNumeratorSamplingScale::None => NumeratorSamplingScaleMode::None,
1795 UniformNumeratorSamplingScale::BeyondQuadratic => {
1796 NumeratorSamplingScaleMode::BeyondQuadratic
1797 }
1798 UniformNumeratorSamplingScale::All => NumeratorSamplingScaleMode::All,
1799 }
1800}
1801
1802#[cfg(test)]
1803mod tests {
1804 use super::*;
1805 use crate::{
1806 cff::{expression::GammaLoopOrientationExpression, surface::GammaLoopSurfaceCache},
1807 dot,
1808 graph::{
1809 ExactUvSubLmbFrame, FeynmanGraph, FourDDenominator,
1810 cuts::{CutSet, LuCutSelection},
1811 parse::from_dot::IntoGraph,
1812 },
1813 initialisation::test_initialise,
1814 settings::global::{GenerationSettings, OrientationPattern},
1815 utils::GS,
1816 uv::uv_graph::UVE,
1817 };
1818 use linnet::half_edge::subgraph::SubSetOps;
1819 use symbolica::atom::{AtomCore, FunctionBuilder};
1820
1821 #[test]
1822 fn generation_cache_obeys_byte_entry_and_access_order_limits() {
1823 let mut cache = GenerationCache::new(10, 2);
1824 cache.insert(1, "first", 4);
1825 cache.insert(2, "second", 4);
1826 assert_eq!(cache.get(&1), Some(&"first"));
1827 cache.insert(3, "third", 4);
1828 assert_eq!(cache.get(&2), None);
1829 assert_eq!(cache.get(&1), Some(&"first"));
1830 assert_eq!(cache.get(&3), Some(&"third"));
1831
1832 cache.insert(4, "oversized", 11);
1833 assert_eq!(cache.get(&4), None);
1834 cache.insert(3, "replacement", 7);
1835 assert_eq!(cache.get(&1), None);
1836 assert_eq!(cache.get(&3), Some(&"replacement"));
1837 cache.clear();
1838 assert_eq!(cache.get(&3), None);
1839
1840 let mut disabled = GenerationCache::new(0, 10);
1841 disabled.insert(1, "uncached", 0);
1842 assert_eq!(disabled.get(&1), None);
1843 }
1844
1845 #[test]
1846 fn canonical_sunset_allocator_matches_native_small_oracle() -> Result<()> {
1847 use crate::{
1848 cff::surface::GammaLoopLinearEnergyExpr,
1849 numerator::energy_degree::{EnergyPowerAnalyzer, EquivalentEnergyCandidates},
1850 utils::symbols::UvDenominatorClassId,
1851 };
1852 use three_dimensional_reps::{
1853 CffGlobalPrefactorSign, MomentumSignature, graph_io::ParsedGraphInternalEdge,
1854 };
1855
1856 test_initialise()?;
1857 let parsed = ParsedGraph {
1860 internal_edges: [(0, 1, vec![1, 0]), (0, 1, vec![-1, 1])]
1861 .into_iter()
1862 .chain((1..6).map(|tail| (tail, (tail + 1) % 6, vec![0, 1])))
1863 .enumerate()
1864 .map(
1865 |(edge_id, (tail, head, loop_signature))| ParsedGraphInternalEdge {
1866 edge_id,
1867 tail,
1868 head,
1869 label: format!("q{edge_id}"),
1870 mass_key: Some("m_uv".into()),
1871 signature: MomentumSignature {
1872 loop_signature,
1873 external_signature: Vec::new(),
1874 },
1875 had_pow: false,
1876 },
1877 )
1878 .collect(),
1879 external_edges: Vec::new(),
1880 initial_state_cut_edges: Vec::new(),
1881 loop_names: vec!["K0".into(), "K1".into()],
1882 external_names: Vec::new(),
1883 node_name_to_internal: (0..6).map(|node| (format!("n{node}"), node)).collect(),
1884 };
1885 let mut options = Generate3DExpressionOptions {
1886 cff_generation_context: CffGenerationContext::EmbeddedCffFactor,
1887 ..Default::default()
1888 };
1889 let mut requests = Vec::new();
1890 for a in 0..=5 {
1891 for b in 0..=5 - a {
1892 for c in 0..=5 - a - b {
1893 for d in 0..=5 - a - b - c {
1894 let loads = [a, b, c, d, 5 - a - b - c - d];
1895 let bounds = std::iter::once((0, 1))
1896 .chain(loads.iter().enumerate().filter_map(|(index, degree)| {
1897 (*degree > 0).then_some((index + 2, *degree))
1898 }))
1899 .collect::<Vec<_>>();
1900 requests.push((5, bounds));
1901 }
1902 }
1903 }
1904 }
1905 requests.push((7, vec![(0, 1), (2, 7)]));
1906
1907 let classes = [UvDenominatorClassId(0), UvDenominatorClassId(1)];
1908 let mut candidates = EquivalentEnergyCandidates::try_from_source_occurrences([])?;
1909 candidates.add_uv_classes([(classes[0], vec![0]), (classes[1], (2..7).collect())])?;
1910 let q0 = FunctionBuilder::new(GS.emr_mom)
1911 .add_arg(GS.uv_class_ref(classes[0]))
1912 .add_arg(GS.cind(0))
1913 .finish();
1914 let q1 = FunctionBuilder::new(GS.emr_mom)
1915 .add_arg(GS.uv_class_ref(classes[1]))
1916 .add_arg(GS.cind(0))
1917 .finish();
1918 let analyzer = EnergyPowerAnalyzer::for_physical_emr_edges([]);
1919 for degree in [5, 7] {
1920 let plans =
1921 analyzer.plan_atom_assignment_proposals(&(&q0 * q1.pow(degree)), &candidates)?;
1922 assert!(!plans.is_empty());
1923 let seen = plans
1924 .iter()
1925 .map(|plan| plan.energy_degree_bounds().to_vec())
1926 .collect::<Vec<_>>();
1927 for plan in plans {
1928 requests.push((degree, plan.energy_degree_bounds().to_vec()));
1929 if let Some(challenger) = plan.placement_challenger(&seen)? {
1930 requests.push((degree, challenger.energy_degree_bounds().to_vec()));
1931 }
1932 }
1933 }
1934 let energy_replacements = (0..7)
1938 .map(|edge| {
1939 symbolica::id::Replacement::new(
1940 GS.ose(EdgeIndex(edge)).to_pattern(),
1941 Atom::num(match edge {
1942 0 => 2,
1943 1 => 3,
1944 _ => 7,
1945 })
1946 .to_pattern(),
1947 )
1948 })
1949 .collect::<Vec<_>>();
1950 for (degree, bounds) in requests {
1955 options.energy_degree_bounds = Some(bounds.clone());
1956 let generated = three_dimensional_reps::generate_3d_expression(&parsed, &options)?;
1957 let surfaces = generated.expression.surfaces.get_all_replacements_gs(&[]);
1958 let mut contour = Atom::Zero;
1959 for orientation in &generated.expression.orientations {
1960 let numerator = bounds
1961 .iter()
1962 .map(|(edge, exponent)| {
1963 orientation.edge_energy_map[*edge]
1964 .to_atom_gs(&[])
1965 .replace_multiple(&energy_replacements)
1966 .pow(*exponent as u64)
1967 })
1968 .product::<Atom>();
1969 contour += orientation
1970 .to_atom_gs()
1971 .replace_multiple(&surfaces)
1972 .replace_multiple(&energy_replacements)
1973 * numerator;
1974 }
1975 let source_sign =
1976 generated
1977 .energy_factor_components
1978 .iter()
1979 .fold(1, |sign, component| {
1980 sign * CffGlobalPrefactorSign::from_exponent(
1981 component.internal_edge_ids.len(),
1982 )
1983 .product(component.source_frame)
1984 .factor()
1985 });
1986 contour *= Atom::num(source_sign);
1987 let expected = match degree {
1988 5 => Atom::num(1213) / Atom::num(16_387_080_192i64),
1989 7 => -Atom::num(365) / Atom::num(47_775_744),
1990 _ => unreachable!(),
1991 };
1992 assert_eq!(contour, expected, "degree {degree}, bounds {bounds:?}");
1993 }
1994 Ok(())
1995 }
1996
1997 #[test]
1998 fn projected_denominator_chains_preserve_occurrence_multiplicity() {
1999 let local_surfaces = [0, 1, 2, 3].map(LinearSurfaceID);
2000 let physical_surfaces = [
2001 HybridSurfaceID::Esurface(EsurfaceID(0)),
2002 HybridSurfaceID::Esurface(EsurfaceID(1)),
2003 HybridSurfaceID::Esurface(EsurfaceID(2)),
2004 ];
2005 let denominator = denominator_tree_from_chains(&[
2006 vec![HybridSurfaceID::Linear(local_surfaces[0])],
2007 vec![HybridSurfaceID::Linear(local_surfaces[1])],
2008 vec![HybridSurfaceID::Linear(local_surfaces[2])],
2009 vec![HybridSurfaceID::Linear(local_surfaces[3])],
2010 ]);
2011 let surface_map = BTreeMap::from([
2012 (
2013 local_surfaces[0],
2014 SurfaceMapEntry {
2015 surface_id: physical_surfaces[0],
2016 sign: 1,
2017 },
2018 ),
2019 (
2020 local_surfaces[1],
2021 SurfaceMapEntry {
2022 surface_id: physical_surfaces[0],
2023 sign: 1,
2024 },
2025 ),
2026 (
2027 local_surfaces[2],
2028 SurfaceMapEntry {
2029 surface_id: physical_surfaces[1],
2030 sign: 1,
2031 },
2032 ),
2033 (
2034 local_surfaces[3],
2035 SurfaceMapEntry {
2036 surface_id: physical_surfaces[2],
2037 sign: 1,
2038 },
2039 ),
2040 ]);
2041
2042 let remapped = remap_denominator_tree_surface_ids(&denominator, &surface_map);
2043 let actual = remapped.iter().fold(Atom::new(), |sum, term| {
2044 sum + Atom::num(term.prefactor) * term.denominator.to_atom_inv()
2045 });
2046 let expected = Atom::num(2) / Atom::from(physical_surfaces[0])
2047 + Atom::num(1) / Atom::from(physical_surfaces[1])
2048 + Atom::num(1) / Atom::from(physical_surfaces[2]);
2049
2050 assert!(
2051 (actual.clone() - expected).expand().is_zero(),
2052 "projecting distinct local chains onto one physical chain lost multiplicity: {actual}"
2053 );
2054
2055 let negative_map = BTreeMap::from([
2056 (
2057 local_surfaces[0],
2058 SurfaceMapEntry {
2059 surface_id: physical_surfaces[0],
2060 sign: -1,
2061 },
2062 ),
2063 (
2064 local_surfaces[1],
2065 SurfaceMapEntry {
2066 surface_id: physical_surfaces[0],
2067 sign: -1,
2068 },
2069 ),
2070 ]);
2071 let negative_denominator = denominator_tree_from_chains(&[
2072 vec![HybridSurfaceID::Linear(local_surfaces[0])],
2073 vec![HybridSurfaceID::Linear(local_surfaces[1])],
2074 ]);
2075 let negative = remap_denominator_tree_surface_ids(&negative_denominator, &negative_map);
2076 let value = negative.iter().fold(Atom::Zero, |sum, term| {
2077 sum + Atom::num(term.prefactor) * term.denominator.to_atom_inv()
2078 });
2079 assert!(
2080 (value + Atom::num(2) / Atom::from(physical_surfaces[0]))
2081 .together()
2082 .is_zero()
2083 );
2084 }
2085
2086 #[test]
2087 fn exact_cff_cache_preserves_occurrence_capacities() -> Result<()> {
2088 test_initialise()?;
2089 let graph: Graph = dot!(digraph exact_occurrence_cache {
2090 edge [num=1 mass=1]
2091 node [num=1]
2092 a -> b [id=0 lmb_id=0]
2093 b -> a [id=1]
2094 })?;
2095 let owners = [EdgeIndex(0), EdgeIndex(1)];
2096 let denominators = owners.map(|source_edge| FourDDenominator {
2097 source_edge,
2098 momentum: FunctionBuilder::new(GS.emr_mom)
2099 .add_arg(usize::from(source_edge))
2100 .finish(),
2101 mass_squared: Atom::one(),
2102 full_expr: Atom::one(),
2103 });
2104 let source = GraphThreeDSource::from_exact_denominators_in_uv_sub_lmb(
2105 &graph,
2106 &denominators,
2107 owners,
2108 [],
2109 &graph.loop_momentum_basis,
2110 ExactUvSubLmbFrame::TaylorVacuum,
2111 )?;
2112 let options = graph.denominator_only_cff_3d_expression_options();
2113 let left = GS.emr_mom(owners[0], GS.cind(0));
2114 let right = GS.emr_mom(owners[1], GS.cind(0));
2115 let numerators = [left.clone().pow(2), right.clone().pow(2), left * right];
2116 let mut cache = Local4dProjectionContext::default();
2117 for numerator in numerators.iter().cycle().take(2 * numerators.len()) {
2118 let mut values = Vec::new();
2119 for generation_cache in [Some(&mut cache), None] {
2120 let preparation =
2121 graph.prepare_3d_expression_for_4d_term(&source, &options, numerator, &[])?;
2122 let (generated, mapper, plan, _) =
2123 graph.generate_3d_expression_for_4d_term(&preparation, generation_cache)?;
2124 let mapper = &mapper.mapper;
2125 let surfaces = generated.expression.surfaces.get_all_replacements_gs(&[]);
2126 let mut value = Atom::Zero;
2127 for orientation in &generated.expression.orientations {
2128 value += orientation.to_atom_gs().replace_multiple(&surfaces)
2129 * mapper.map_planned_numerator(
2130 &orientation.loop_energy_map,
2131 &orientation.edge_energy_map,
2132 &plan,
2133 )?;
2134 }
2135 values.push(value.replace_multiple(mapper.exact_ose_replacements()));
2136 }
2137 assert!(
2138 !values[0].collect_factors().is_zero(),
2139 "the occurrence-capacity requests must exercise nonzero residues"
2140 );
2141 assert!(
2142 (values[0].collect_factors() - values[1].collect_factors())
2143 .collect_factors()
2144 .is_zero(),
2145 "cached and uncached complete residues must agree for each independent numerator capacity: {numerator}"
2146 );
2147 }
2148 Ok(())
2149 }
2150
2151 #[test]
2152 fn exact_cff_batch_reuses_owner_relabelled_sub_lmb_topology() -> Result<()> {
2153 test_initialise()?;
2154 let graph: Graph = dot!(digraph exact_owner_relabelled_cache {
2155 edge [num=1 mass=1]
2156 node [num=1]
2157
2158 a -> b [id=0 lmb_id=0]
2159 a -> b [id=1]
2160 })?;
2161 let denominators = [EdgeIndex(0), EdgeIndex(1)].map(|source_edge| FourDDenominator {
2162 source_edge,
2163 momentum: FunctionBuilder::new(GS.emr_mom)
2164 .add_arg(usize::from(source_edge))
2165 .finish(),
2166 mass_squared: graph.underlying[source_edge].mass_atom().pow(2),
2167 full_expr: Atom::one(),
2168 });
2169 let mut relabelled = denominators.clone();
2170 relabelled[1].source_edge = EdgeIndex(0);
2171 let options = graph.denominator_only_cff_3d_expression_options();
2172
2173 let mut cache = crate::uv::approx::projected_4d::Local4dProjectionContext::default();
2174 let cutset = CutSet::empty(graph.n_hedges());
2175 let mut values = Vec::new();
2176 for denominators in [&denominators, &relabelled] {
2177 for generation_cache in [Some(&mut cache), None] {
2178 let (cff, _) = graph.clone().cff_from_4d_denominators_in_uv_sub_lmb(
2179 denominators,
2180 [EdgeIndex(0), EdgeIndex(1)],
2181 [],
2182 &graph.loop_momentum_basis,
2183 ExactUvSubLmbFrame::TaylorVacuum,
2184 &cutset,
2185 &options,
2186 &Atom::one(),
2187 generation_cache,
2188 &[],
2189 )?;
2190 let mut value = Atom::Zero;
2191 for term in cff.terms.values() {
2192 for orientation in &term.orientations {
2193 value += &orientation.expression
2194 * term.map_exact_source_numerator(&orientation.orientation, None)?;
2195 }
2196 }
2197 values.push(value * Atom::num(cff.production_prefactor_factor()));
2198 }
2199 }
2200 for qx in [Atom::num(3) / Atom::num(4), Atom::num(4) / Atom::num(3)] {
2205 let energy = (Atom::one() + qx.clone().pow(2)).sqrt();
2206 let at_point = |value: &Atom| {
2207 let mut value = value.clone();
2208 for (edge, spatial_x) in [(EdgeIndex(0), qx.clone()), (EdgeIndex(1), -&qx)] {
2209 value = value
2210 .replace(GS.emr_mom(edge, GS.cind(1)))
2211 .with(spatial_x)
2212 .replace(GS.emr_mom(edge, GS.cind(2)))
2213 .with(Atom::Zero)
2214 .replace(GS.emr_mom(edge, GS.cind(3)))
2215 .with(Atom::Zero)
2216 .replace(GS.ose(edge))
2217 .with(energy.clone());
2218 }
2219 value.collect_factors()
2220 };
2221 let expected = at_point(&values[0]);
2222 assert!(
2223 !expected.is_zero(),
2224 "the scalar bubble residue must be nonzero"
2225 );
2226 assert!(
2227 values
2228 .iter()
2229 .skip(1)
2230 .all(|value| (at_point(value) - &expected).together().is_zero()),
2231 "cached and uncached complete residues must agree under compatible owner relabelling at qx={qx}"
2232 );
2233 }
2234
2235 Ok(())
2236 }
2237
2238 #[test]
2239 fn bounded_exact_cff_dispatch_preserves_owner_contour() -> Result<()> {
2240 use crate::cff::{
2241 expression::GammaLoopOrientationExpression, surface::GammaLoopSurfaceCache,
2242 };
2243 use three_dimensional_reps::CffGlobalPrefactorSign;
2244
2245 test_initialise()?;
2246 let graph: Graph = dot!(digraph bounded_exact_dispatch {
2247 edge [num=1 mass=2]
2248 node [num=1]
2249
2250 a -> b [id=0 lmb_id=0]
2251 b -> a [id=1]
2252 a -> a [id=2 lmb_id=1]
2253 })?;
2254 let owners = [EdgeIndex(0), EdgeIndex(1), EdgeIndex(2)];
2255 let base = owners.map(|source_edge| FourDDenominator {
2256 source_edge,
2257 momentum: graph.loop_momentum_basis.loop_atom::<Atom>(
2258 source_edge,
2259 GS.emr_mom,
2260 &[],
2261 true,
2262 ),
2263 mass_squared: graph.underlying[source_edge].mass_atom().pow(2),
2264 full_expr: Atom::one(),
2265 });
2266 let options = Generate3DExpressionOptions {
2267 cff_generation_context: CffGenerationContext::EmbeddedCffFactor,
2268 numerator_sampling_scale: NumeratorSamplingScaleMode::None,
2269 ..graph.denominator_only_cff_3d_expression_options()
2270 };
2271 for dotted_owner in [0, 1] {
2272 let mut denominators = base.to_vec();
2273 denominators.extend([base[dotted_owner].clone(), base[dotted_owner].clone()]);
2274 let source = GraphThreeDSource::from_exact_denominators_in_uv_sub_lmb(
2275 &graph,
2276 &denominators,
2277 owners,
2278 [],
2279 &graph.loop_momentum_basis,
2280 ExactUvSubLmbFrame::TaylorVacuum,
2281 )?;
2282 let tagged = |derived| {
2283 FunctionBuilder::new(GS.emr_mom)
2284 .add_arg(GS.uv_momentum_provenance_tag(
2285 Atom::num(dotted_owner as i64).as_view(),
2286 derived,
2287 base[dotted_owner].momentum.as_view(),
2288 ))
2289 .add_arg(GS.cind(0))
2290 .finish()
2291 };
2292 let fixed = tagged(false);
2293 let derived = tagged(true);
2294 let numerator = fixed.clone().pow(4) * derived.clone().pow(2);
2297 let physical_edges = source.physical_energy_edge_index_map().unwrap();
2298 let mut cache = Local4dProjectionContext::default();
2303 for phase in ["fresh", "cached", "uncached", "disabled", "evicted"] {
2304 if phase == "disabled" {
2305 cache.generation_cache.entries = GenerationCache::new(0, 0);
2306 } else if phase == "evicted" {
2307 cache.generation_cache.entries = GenerationCache::new(64 * 1024 * 1024, 1);
2308 let parsed = source.to_three_d_parsed_graph()?;
2309 let key = ExactCffGenerationCache::generation_key(
2310 &parsed,
2311 &source.energy_edge_index_map(&parsed).unwrap(),
2312 &options,
2313 );
2314 cache.generation_cache.record_count(key, 0);
2317 }
2318 let preparation =
2319 graph.prepare_3d_expression_for_4d_term(&source, &options, &numerator, &[])?;
2320 let (generated, mapper, plan, _) = graph.generate_3d_expression_for_4d_term(
2321 &preparation,
2322 (phase != "uncached").then_some(&mut cache),
2323 )?;
2324 let mapper = &mapper.mapper;
2325
2326 let replacements = generated.expression.surfaces.get_all_replacements_gs(&[]);
2327 let mut contour = Atom::Zero;
2328 for orientation in &generated.expression.orientations {
2329 let mapped = mapper.map_planned_numerator(
2330 &orientation.loop_energy_map,
2331 &orientation.edge_energy_map,
2332 &plan,
2333 )?;
2334 contour += orientation
2335 .to_atom_gs()
2336 .replace_multiple(&replacements)
2337 .replace_multiple(mapper.exact_ose_replacements())
2338 * mapped;
2339 }
2340 let source_sign =
2341 generated
2342 .energy_factor_components
2343 .iter()
2344 .fold(1, |sign, component| {
2345 sign * CffGlobalPrefactorSign::from_exponent(
2346 component.internal_edge_ids.len(),
2347 )
2348 .product(component.source_frame)
2349 .factor()
2350 });
2351 contour *= Atom::num(source_sign);
2352 let energy = |physical| {
2353 let occurrence = physical_edges
2354 .internal
2355 .iter()
2356 .find_map(|(occurrence, owner)| (*owner == physical).then_some(*occurrence))
2357 .unwrap();
2358 crate::utils::ose_atom_from_index(EdgeIndex(occurrence))
2359 .replace_multiple(mapper.exact_ose_replacements())
2360 };
2361 let expected = Atom::num(5) / (Atom::num(64) * energy(2) * energy(dotted_owner));
2365 let physical_shell = |mut expression: Atom| {
2370 for physical in owners {
2371 let on_shell_energy = (1..=3)
2372 .fold(
2373 graph.underlying[physical].mass_atom().pow(2),
2374 |square, spatial_index| {
2375 square
2376 + graph
2377 .loop_momentum_basis
2378 .loop_atom::<Atom>(
2379 physical,
2380 GS.emr_mom,
2381 &[GS.cind(spatial_index)],
2382 true,
2383 )
2384 .pow(2)
2385 },
2386 )
2387 .sqrt();
2388 expression = expression.replace(GS.ose(physical)).with(on_shell_energy);
2389 }
2390 expression
2391 };
2392 let difference = (physical_shell(contour) - physical_shell(expected)).together();
2393 assert!(
2394 difference.is_zero(),
2395 "dotted owner {dotted_owner}, {phase}: dispatch changed the analytic contour: {difference}"
2396 );
2397 }
2398 }
2399 Ok(())
2400 }
2401
2402 #[test]
2403 fn direct_cff_matches_signed_bubble_and_contracted_unit_contours() -> Result<()> {
2404 test_initialise()?;
2405 let mut graph: Graph = dot!(digraph direct_duplicate_sign {
2406 edge [num=1 mass=1]
2407 node [num=1]
2408
2409 a -> b [id=0 lmb_id=0]
2410 a -> b [id=1]
2411 })?;
2412 let options = graph.denominator_only_cff_3d_expression_options();
2413 let cutset = CutSet::empty(graph.n_hedges());
2414 let contract: linnet::half_edge::subgraph::SuBitGraph = graph.empty_subgraph();
2415 let cff = graph.cff(
2416 &contract,
2417 &cutset,
2418 &OrientationPattern::default(),
2419 &options,
2420 None,
2421 )?;
2422 let mut value = cff
2423 .terms
2424 .values()
2425 .flat_map(|term| &term.orientations)
2426 .fold(Atom::Zero, |sum, orientation| sum + &orientation.expression)
2427 * Atom::num(cff.production_prefactor_factor());
2428 for edge in 0..2 {
2429 value = value.replace(GS.ose(EdgeIndex(edge))).with(1);
2430 }
2431 let signed_contour = value * (Atom::num(2) * Atom::var(GS.pi)).pow(3) / Atom::i().pow(1);
2434 assert!(
2435 (signed_contour - Atom::one() / Atom::num(4))
2436 .together()
2437 .is_zero()
2438 );
2439
2440 let contracted_loop = graph
2441 .get_edge_subgraph(EdgeIndex(0))
2442 .union(&graph.get_edge_subgraph(EdgeIndex(1)));
2443 assert_eq!(graph.cyclotomatic_number(&contracted_loop), 1);
2444 let reduced = graph.cff(
2445 &contracted_loop,
2446 &cutset,
2447 &OrientationPattern::default(),
2448 &options,
2449 None,
2450 )?;
2451 let reduced_value = reduced
2452 .terms
2453 .values()
2454 .flat_map(|term| &term.orientations)
2455 .fold(Atom::Zero, |sum, orientation| sum + &orientation.expression)
2456 * Atom::num(reduced.production_prefactor_factor());
2457 assert_eq!(
2458 reduced_value,
2459 Atom::one(),
2460 "contracting the only loop must leave the unit zero-loop source"
2461 );
2462 Ok(())
2463 }
2464
2465 #[test]
2466 fn two_loop_cff_matches_signed_theta_contour() -> Result<()> {
2467 test_initialise()?;
2468 let mut graph: Graph = dot!(digraph direct_two_loop_sign {
2469 edge [num=1 mass=1]
2470 node [num=1]
2471
2472 a -> b [id=0 lmb_id=0]
2473 a -> b [id=1 lmb_id=1]
2474 b -> a [id=2]
2475 })?;
2476 let options = graph.denominator_only_cff_3d_expression_options();
2477 let cutset = CutSet::empty(graph.n_hedges());
2478 let contract: linnet::half_edge::subgraph::SuBitGraph = graph.empty_subgraph();
2479 let cff = graph.cff(
2480 &contract,
2481 &cutset,
2482 &OrientationPattern::default(),
2483 &options,
2484 None,
2485 )?;
2486 let mut value = cff
2487 .terms
2488 .values()
2489 .flat_map(|term| &term.orientations)
2490 .fold(Atom::Zero, |sum, orientation| sum + &orientation.expression)
2491 * Atom::num(cff.production_prefactor_factor());
2492 for edge in 0..3 {
2493 value = value.replace(GS.ose(EdgeIndex(edge))).with(1);
2494 }
2495 let signed_contour = value * (Atom::num(2) * Atom::var(GS.pi)).pow(6) / Atom::i().pow(2);
2498 assert!(
2499 (signed_contour - -Atom::one() / Atom::num(12))
2500 .together()
2501 .is_zero()
2502 );
2503
2504 Ok(())
2505 }
2506
2507 #[test]
2508 fn contracted_raised_generation_rejects_absent_emr_bound_without_aliasing() -> Result<()> {
2509 test_initialise()?;
2510 let graph: Graph = dot!(
2511 digraph factorized_source_bounds {
2512 edge [pdg=1000 num=1 mass=0]
2513 node [num=1]
2514 ext_in [style=invis]
2515 ext_out [style=invis]
2516 ext_in -> A [id=4]
2517 C -> ext_out [id=5]
2518 A -> B [id=0 num="Q(0,spenso::cind(0))+1"]
2519 B -> C [id=1 num="Q(1,spenso::cind(0))+2"]
2520 C -> D [id=2]
2521 D -> A [id=3]
2522 },
2523 "scalars"
2524 )?;
2525 let options = graph.production_cff_3d_expression_options(&GenerationSettings::default())?;
2526 assert_eq!(options.energy_degree_bounds, Some(vec![(0, 1), (1, 1)]));
2527
2528 let numerator = graph.production_numerator_atom_for_full_3d_expression();
2529 let mut contracted_graph = graph.clone();
2530 let error = contracted_graph
2531 .generate_3d_expression_for_integrand(
2532 &[EdgeIndex(0)],
2533 &None,
2534 &options,
2535 Some(&numerator),
2536 )
2537 .expect_err("a bound owned by a shrunken EMR edge must not be silently reassigned");
2538 let error = format!("{error:#}");
2539 assert!(
2540 error.contains("shrunken EMR edge 0")
2541 && error.contains("no numerator-only/shrunken-edge bound channel"),
2542 "unexpected error: {error}"
2543 );
2544 Ok(())
2545 }
2546
2547 #[test]
2548 fn selected_raw_cff_matches_normal_generation_after_discarding_a_trial() -> Result<()> {
2549 test_initialise()?;
2550 let mut graph: Graph = dot!(digraph selected_raw_cff {
2551 edge [num=1 mass=1]
2552 node [num=1]
2553 a -> b [id=0 lmb_id=0]
2554 a -> b [id=1]
2555 b -> b [id=2 lmb_id=1]
2556 })?;
2557 let mut ordinary_graph = graph.clone();
2558 let contract = graph.get_edge_subgraph(EdgeIndex(2));
2559 let options = Generate3DExpressionOptions {
2560 cff_generation_context: CffGenerationContext::EmbeddedCffFactor,
2561 ..graph.denominator_only_cff_3d_expression_options()
2562 };
2563 let numerator = GS.emr_mom(EdgeIndex(0), GS.cind(0)).pow(2);
2564 let discarded = graph.generate_raw_3d_expression_for_integrand(
2565 &[EdgeIndex(2)],
2566 &options,
2567 Some(&GS.emr_mom(EdgeIndex(0), GS.cind(0)).pow(3)),
2568 )?;
2569 drop(discarded);
2570 let selected = graph.generate_raw_3d_expression_for_integrand(
2571 &[EdgeIndex(2)],
2572 &options,
2573 Some(&numerator),
2574 )?;
2575 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
2576 let selected = graph.convert_generated_expression_surfaces(selected, &canonization, &[])?;
2577 let cutset = CutSet::empty(graph.n_hedges());
2578 let pattern = OrientationPattern::default();
2579 let selected = graph
2580 .cff_from_generated_expression(selected, &contract, &cutset, &pattern, &options)?;
2581 let ordinary =
2582 ordinary_graph.cff(&contract, &cutset, &pattern, &options, Some(&numerator))?;
2583 assert_eq!(
2584 selected.energy_degree_bound_report,
2585 ordinary.energy_degree_bound_report
2586 );
2587 assert_eq!(
2588 selected.terms.keys().collect::<Vec<_>>(),
2589 ordinary.terms.keys().collect::<Vec<_>>()
2590 );
2591 for (index, selected_term) in &selected.terms {
2594 let ordinary_term = &ordinary.terms[index];
2595 let values = [
2596 (
2597 selected_term,
2598 selected.production_prefactor_factor(),
2599 &graph,
2600 ),
2601 (
2602 ordinary_term,
2603 ordinary.production_prefactor_factor(),
2604 &ordinary_graph,
2605 ),
2606 ]
2607 .map(|(term, prefactor, graph)| {
2608 term.orientations
2609 .iter()
2610 .fold(Atom::Zero, |sum, orientation| {
2611 sum + &orientation.expression
2612 * numerator.replace_multiple(
2613 orientation.orientation.energy_replacements_gs(graph),
2614 )
2615 })
2616 * Atom::num(prefactor)
2617 });
2618 assert!(
2619 (values[0].collect_factors() - values[1].collect_factors())
2620 .collect_factors()
2621 .is_zero(),
2622 "discarded trials must leave the complete physical residue unchanged at {index}"
2623 );
2624 }
2625 Ok(())
2626 }
2627
2628 #[test]
2629 fn raised_lu_cff_preserves_complete_production_residues() -> Result<()> {
2630 test_initialise()?;
2631 let mut graph: Graph = dot!(digraph raised_lu {
2632 edge [num=1 mass=1]
2633 node [num=1]
2634 incoming [style=invis]
2635 outgoing [style=invis]
2636
2637 incoming -> v1 [id=0]
2638 v1 -> v2 [id=1 lmb_id=0]
2639 v2 -> v3 [id=2]
2640 v1 -> v3 [id=3]
2641 v3 -> outgoing [id=4]
2642 })?;
2643 let options = graph.denominator_only_cff_3d_expression_options();
2644 let numerator = GS.emr_mom(EdgeIndex(1), GS.cind(0)).pow(2);
2645 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
2646 let generated = graph.generate_3d_expression_for_integrand(
2647 &[],
2648 &canonization,
2649 &options,
2650 Some(&numerator),
2651 )?;
2652 let production = &generated.expression;
2653 let lu_cut = graph
2654 .determine_raised_esurfaces_from_expression(production)
2655 .raised_groups
2656 .into_iter()
2657 .find(|group| {
2658 group.max_occurence > 1
2659 && group.esurface_ids.iter().any(|esurface_id| {
2660 !production.surfaces.esurface_cache[*esurface_id]
2661 .external_shift
2662 .is_empty()
2663 })
2664 })
2665 .expect("raised production CFF should contain a physical repeated LU surface");
2666 let mut cutset = CutSet::empty(graph.n_hedges());
2667 cutset.residue_selector.lu = Some(LuCutSelection {
2668 raised_group: lu_cut.clone(),
2669 cut_edge_alternatives: lu_cut
2670 .esurface_ids
2671 .iter()
2672 .map(|esurface_id| {
2673 production.surfaces.esurface_cache[*esurface_id]
2674 .energies
2675 .clone()
2676 })
2677 .collect(),
2678 });
2679 let direct = graph.cff(
2680 &cutset.union,
2681 &cutset,
2682 &OrientationPattern::default(),
2683 &options,
2684 Some(&numerator),
2685 )?;
2686
2687 assert_eq!(direct.terms.len(), lu_cut.max_occurence);
2688 assert!(direct.terms.keys().all(|index| {
2689 index.lu_cut_order.is_some()
2690 && index.left_threshold_order.is_none()
2691 && index.right_threshold_order.is_none()
2692 }));
2693 assert!(direct.terms.iter().any(|(index, term)| {
2694 index.lu_cut_order == Some(2) && !term.orientations.is_empty()
2695 }));
2696 let stored = graph.cff_from_production_expression(
2697 &generated,
2698 &cutset,
2699 &OrientationPattern::default(),
2700 )?;
2701 for (index, term) in &direct.terms {
2702 let value = |term: &crate::cff::CFFTerm, prefactor| {
2703 term.orientations
2704 .iter()
2705 .fold(Atom::Zero, |sum, orientation| {
2706 sum + &orientation.expression
2707 * numerator.replace_multiple(
2708 orientation.orientation.energy_replacements_gs(&graph),
2709 )
2710 })
2711 * Atom::num(prefactor)
2712 };
2713 assert!(
2714 (value(term, direct.production_prefactor_factor()).collect_factors()
2715 - value(&stored.terms[index], stored.production_prefactor_factor())
2716 .collect_factors())
2717 .is_zero(),
2718 "selected LU generation must preserve the complete stored production residue"
2719 );
2720 }
2721 Ok(())
2722 }
2723
2724 #[test]
2725 fn exact_massless_raised_lu_cff_retains_selected_residue() -> Result<()> {
2726 test_initialise()?;
2727 let mut graph: Graph = dot!(digraph exact_massless_raised_lu {
2728 edge [num=1 mass="UFO::ZERO"]
2729 node [num=1]
2730 incoming [style=invis]
2731 outgoing [style=invis]
2732
2733 incoming -> v1 [id=0]
2734 v1 -> v2 [id=1 lmb_id=0]
2735 v2 -> v3 [id=2]
2736 v1 -> v3 [id=3]
2737 v3 -> outgoing [id=4]
2738 })?;
2739 let options = graph.denominator_only_cff_3d_expression_options();
2740 let numerator = GS.emr_mom(EdgeIndex(1), GS.cind(0)).pow(2);
2741 let canonization = graph.get_esurface_canonization(&graph.loop_momentum_basis);
2742 let production = graph
2743 .generate_3d_expression_for_integrand(&[], &canonization, &options, Some(&numerator))?
2744 .expression;
2745 let lu_cut = graph
2746 .determine_raised_esurfaces_from_expression(&production)
2747 .raised_groups
2748 .into_iter()
2749 .find(|group| {
2750 group.max_occurence > 1
2751 && group.esurface_ids.iter().any(|esurface_id| {
2752 !production.surfaces.esurface_cache[*esurface_id]
2753 .external_shift
2754 .is_empty()
2755 })
2756 })
2757 .expect("raised production CFF should contain a physical repeated LU surface");
2758 let mut cutset = CutSet::empty(graph.n_hedges());
2759 cutset.residue_selector.lu = Some(LuCutSelection {
2760 raised_group: lu_cut.clone(),
2761 cut_edge_alternatives: lu_cut
2762 .esurface_ids
2763 .iter()
2764 .map(|esurface_id| {
2765 production.surfaces.esurface_cache[*esurface_id]
2766 .energies
2767 .clone()
2768 })
2769 .collect(),
2770 });
2771 let denominators = [1, 2, 3].map(|edge| {
2772 let edge = EdgeIndex(edge);
2773 FourDDenominator {
2774 source_edge: edge,
2775 momentum: FunctionBuilder::new(GS.emr_mom)
2776 .add_arg(usize::from(edge))
2777 .finish(),
2778 mass_squared: graph.underlying[edge].mass_atom().pow(2),
2779 full_expr: Atom::one(),
2780 }
2781 });
2782 assert!(
2783 denominators
2784 .iter()
2785 .all(|denominator| denominator.mass_squared.is_zero()),
2786 "UFO::ZERO masses must be normalized before exact surface projection"
2787 );
2788 let source = GraphThreeDSource::from_exact_denominators(&graph, &denominators)?;
2789 let mapper = source
2790 .exact_source_energy_mapper(&[])
2791 .expect("exact source has a physical EMR mapper");
2792 let candidates = mapper.equivalent_energy_candidates([EdgeIndex(1)])?;
2793 let plan = graph.plan_numerator_energy_assignment_in_atom_excluding(
2794 &numerator,
2795 std::iter::empty(),
2796 &candidates,
2797 )?;
2798 let assigned_occurrence = source
2799 .physical_energy_edge_index_map()
2800 .expect("exact source has a physical occurrence map")
2801 .internal
2802 .into_iter()
2803 .find_map(|(occurrence, owner)| (owner == 1).then_some(occurrence))
2804 .expect("the physical numerator carrier has one exact occurrence");
2805 assert_eq!(
2806 plan.energy_degree_bounds(),
2807 &[(assigned_occurrence, 2)],
2808 "canonical exact-edge IDs may change, but the quadratic bound must remain on the occurrence owned by physical edge 1",
2809 );
2810
2811 let (exact, _) =
2812 graph.cff_from_4d_denominators(&denominators, &cutset, &options, &numerator)?;
2813 assert_eq!(
2814 exact.energy_degree_bound_report,
2815 CffEnergyDegreeBoundReport {
2816 source_kind: CffEnergyBoundSourceKind::ExactFourD,
2817 physical_parent_bounds: vec![(1, 2)],
2818 assigned_cff_source_bounds: plan.energy_degree_bounds().to_vec(),
2819 },
2820 "exact CFF diagnostics must keep physical-parent and assigned occurrence bounds distinct",
2821 );
2822 assert_eq!(exact.terms.len(), lu_cut.max_occurence);
2823 let selected = exact
2824 .terms
2825 .iter()
2826 .find(|(index, _)| index.lu_cut_order == Some(2))
2827 .expect("the exact raised CFF should retain its second LU residue");
2828 let selected_value = selected
2829 .1
2830 .orientations
2831 .iter()
2832 .map(|orientation| {
2833 Ok(&orientation.expression
2834 * selected
2835 .1
2836 .map_exact_source_numerator(&orientation.orientation, None)?)
2837 })
2838 .collect::<Result<Vec<_>>>()?
2839 .into_iter()
2840 .sum::<Atom>()
2841 * Atom::num(exact.production_prefactor_factor());
2842 assert!(
2843 !selected_value.collect_factors().is_zero(),
2844 "the complete mapped exact second-order LU residue must remain nonzero"
2845 );
2846 Ok(())
2847 }
2848}