Skip to main content

gammalooprs/cff/
mod.rs

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