Skip to main content

gammalooprs/cff/
generation.rs

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
72/// Generation-local, deterministic LRU retention. Callers charge owned keys,
73/// containers and symbolic payload at insertion; this is not a process RSS cap.
74/// Cache state changes work only, never the admitted plans or their semantics.
75pub(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    // Callers include key construction and accounting, excluding value work.
86    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    // Keep each occurrence's requested capacity, even when source provenance
253    // identifies equal physical energies. Reuse requires canonical topology
254    // and identical per-edge bounds; independent terms must not combine into
255    // a larger Cartesian capacity or redistribute their numerator ownership.
256    // Losing trials keep only their count, so repeated terms can compare the
257    // same complete source capacity without retaining or rebuilding its trees.
258    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        // Native payloads contain rational data and owned trees, not mapped
357        // numerator Atoms. Count owned vector/string capacity once at insertion;
358        // the private tree/EdgeVec containers are cloned to their live length.
359        // No printing, serialization or per-hit traversal is necessary.
360        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/// These disjoint identities share one preparation budget and LRU order.
464#[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        // Charge every Arc payload reachable from this entry independently.
549        // Another preparation may retain the same payload after this is evicted.
550        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        // This report lives in the physical parent's namespace. Canonical
628        // classes are expanded only for degree reporting, never for dispatch;
629        // remote component classes carry no energy of this source.
630        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        // Parse first so a malformed exact rational source returns its
676        // structural error instead of being hidden behind the mapper's
677        // optional convenience API.
678        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        // Exact sources have occurrence-local denominator IDs. Physical
685        // originals retain their base occurrence and their derived factors may
686        // use serial copies; canonical UV classes use their certified pools.
687        // Analyze all physical active edges first so unused, unrelated
688        // candidate groups cannot reject a constant numerator.
689        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        // Each immutable factor-local plan owns both its exact bounds and the
704        // later numerator substitutions. This keeps the numerator factorized
705        // and prevents generation from understating the expression actually
706        // sampled in a residue or contact sector. Rank proposes a bounded set
707        // of plans; the real source map count chooses between them below.
708        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            // Preparing the immutable mapper also certifies its exact signed
837            // diagonal, including affine shifts, before any native CFF trial.
838            // A complete binding cache hit reuses that same certified template.
839            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            // A known contender needs no expression until it wins. Generated
862            // expressions clone their tree containers; count-only loser records
863            // avoid retaining or rebuilding those trees on repeated requests.
864            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            // Native rows are primary; the descending envelope starts with
906            // maximum rank and supplies the remaining deterministic tie break.
907            // Equal scores retain the earlier admitted candidate. This bounded
908            // search does not claim a global minimum.
909            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            // A previous losing key can win against a different proposal set.
936            // Its count was enough to select it; obtain its payload only now.
937            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            // Reuse requires both canonical topology and identical occurrence
954            // capacity; keep the term's assignment plan unchanged. Retain
955            // only the winning native payload. Losing counts and certified
956            // templates have their own bounded retention.
957            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    /// Generate source-owned maps before interning any physical graph surface.
1021    /// Bounded dispatch can discard a raw proposal without leaving graph state.
1022    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            // Cut aliases and tree denominators are external to the CFF graph,
1084            // but their energies remain in the complete numerator. Removing
1085            // their EMR bounds here therefore changes only CFF capacity; in
1086            // particular, an explicitly bounded numerator class remains
1087            // `Some([])` when every bound belongs to this external sector.
1088            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        // Generic edge-index remapping leaves omitted parent edges at zero. A
1139        // contracted edge can still have an exact outer-energy map when its
1140        // source coordinates contain no inner-loop component; restore only
1141        // that source-owned information and leave genuinely inner-dependent
1142        // contracted edges unavailable.
1143        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        // Analyze the factorized numerator compositionally while keeping every
1226        // active energy degree attached to its EMR source edge. Initial-cut
1227        // aliases and structural bridges stay in their separately projected
1228        // external sector. The standalone CFF generator remaps active source
1229        // IDs only after extracting its local graph, so a loop-momentum basis
1230        // never defines numerator ownership.
1231        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                        // GammaLoop's CFF evaluator convention keeps the
1422                        // on-shell-energy factors as one global product
1423                        // 1/prod(-2E_i) for the ordinary CFF denominator sector.
1424                        variant.half_edges.clear();
1425                    }
1426                    // Generalized variants retain their local positive-energy
1427                    // factors. Their branch-relative denominator convention is
1428                    // already consumed while each powered channel is lowered.
1429                    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            // Surface projection is not injective: distinct occurrence-local
1685            // chains can become the same physical chain. The tree shares
1686            // equal branches structurally, so retain their additive count in
1687            // the variant prefactor and still factor chains with equal counts.
1688            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            // GammaLoop cut E-surfaces store initial-state energies on the
1765            // external-shift side with the opposite sign.
1766            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        // Fixed source incidence: two short channels and five serial copies of
1858        // the third. This polynomial diagnostic is independent of reconstruction.
1859        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        // Compare the complete signed contour for every degree distribution,
1935        // including the bounded allocator's proposals. Native row counts and
1936        // proposal ordering may change without changing this public value.
1937        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        // Integrating k0 first gives -k1/[2 E1 (k1²-(E0+E1)²)].
1951        // The remaining Below contour is the negative sum of the simple pole
1952        // at E0+E1 and the fifth-order pole at E2. At (E0,E1,E2)=(2,3,7),
1953        // these independent residues give the exact values below.
1954        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        // Unchanged denominators retain physical OSE symbols, while a relabelled
2201        // owner may retain a literal square root. Compare both at the same two
2202        // positive-energy loop points, after production has resolved surfaces,
2203        // occurrence energies, ownership and convention factors.
2204        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            // Keep the original quartic factor on its retained occurrence; only
2295            // the two new denominator-derived powers may use the serial copies.
2296            let numerator = fixed.clone().pow(4) * derived.clone().pow(2);
2297            let physical_edges = source.physical_energy_edge_index_map().unwrap();
2298            // Copy assignments can tie when neither copy owns the loop basis.
2299            // Exercise both source topologies without constraining tie resolution.
2300            // Compare their complete contours against the independent analytic
2301            // residues below, rather than mock costs or an envelope formula.
2302            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                    // A different, denominator-only request occupies the sole
2315                    // retention slot; the tested bounded request must evict it.
2316                    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                // Independent clockwise Below contours are -5/(32E_b)
2362                // for q^6/D_b^4 and -1/(2E_t) for the attached tadpole.
2363                // The original quartic factor stays on its original occurrence.
2364                let expected = Atom::num(5) / (Atom::num(64) * energy(2) * energy(dotted_owner));
2365                // The exact mapper keeps an unchanged propagator as E(owner),
2366                // while a rewritten equal-momentum denominator uses its literal
2367                // square root. The repeated-pole oracle requires the same positive
2368                // physical mass shell for both dialects.
2369                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        // At unit on-shell energies, Wick rotation reduces the scalar contour
2432        // to the independently integrated Euclidean propagator product.
2433        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        // At unit on-shell energies, Wick rotation reduces the scalar contour
2496        // to the independently integrated Euclidean propagator product.
2497        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        // Discarded raw proposals must not affect any selected physical residue;
2592        // persistent surface numbering and interpolation layout remain internal.
2593        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}