Skip to main content

gammalooprs/processes/
cross_section.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    fmt::{Display, Formatter},
4    fs::{self, File},
5    io::Write,
6    ops::{Index, IndexMut},
7    path::{Path, PathBuf},
8    sync::Mutex,
9};
10
11#[cfg(test)]
12mod gauge_sewing_tests;
13#[cfg(test)]
14mod sewing_tests;
15
16// use bincode::{Decode, Encode};
17use bincode_trait_derive::{Decode, Encode};
18use color_eyre::Result;
19use itertools::Itertools;
20use rayon::{
21    ThreadPool,
22    iter::{IntoParallelRefMutIterator, ParallelIterator},
23};
24use spenso::algebra::algebraic_traits::IsZero;
25use tracing::info;
26use vakint::Vakint;
27
28use crate::{
29    DependentMomentaConstructor, GammaLoopContext, GammaLoopContextContainer,
30    cff::{
31        CffEnergyDegreeBoundReport, CutCFFIndex,
32        esurface::{RaisedEsurfaceData, RaisedEsurfaceGroup, RaisedEsurfaceId},
33        expression::{
34            OrientationID, normalize_cut_edge_support_with_raised_edge_groups,
35            normalize_three_d_expression_cut_support_with_raised_edge_groups,
36        },
37        surface::HybridSurfaceID,
38    },
39    debug_tags, define_index,
40    feyngen::GenerationType,
41    graph::{
42        GraphGroup, GroupId, LMBext, LmbChannelFallback, LmbIndex, LoopMomentumBasis,
43        ThresholdPinchStatus,
44        cuts::{CutSet, LuCutSelection, ResidueSelector},
45        edge::EdgeMass,
46        parse::complete_group_parsing,
47        threshold_counterterms::{ThresholdCountertermMultiplier, ThresholdCountertermVariant},
48    },
49    integrands::process::{
50        GenericEvaluator, LmbMultiChannelingSetup, ParamBuilder,
51        cross_section::CrossSectionIntegrandData, graph_to_group_id_for_group_structure,
52    },
53    model::ArcParticle,
54    momentum::{
55        Helicity,
56        sample::{ExternalIndex, SubspaceData},
57    },
58    processes::{
59        DotExportSettings, EvaluatorSettings, GraphCutSelectionSubject, GraphGenerationStats,
60        GraphGroupSelectionMode, GraphGroupSelectionPlan, GraphGroupSelectionSpec,
61        GraphSelectionSignatureInventory, GraphSelectionSubject, NamedGraphGenerationReport,
62        RaisedCutSignatureInventory,
63    },
64    settings::{
65        GlobalSettings, RuntimeSettings, global::GenerationSettings, runtime::LockedRuntimeSettings,
66    },
67    utils::{
68        DEFAULT_ESURFACE_EXISTENCE_THRESHOLD, F, GS, W_,
69        hyperdual_utils::{shape_from_cut_cff_index, simple_n_deriv_shape},
70    },
71    uv::{
72        approx::{CutStructure, OrientationProjection},
73        forest::ParametricIntegrands,
74    },
75};
76use eyre::{Context, eyre};
77use linnet::half_edge::{
78    involution::{EdgeIndex, EdgeVec, Orientation},
79    subgraph::{
80        HedgeNode, Inclusion, InternalSubGraph, ModifySubSet, OrientedCut, SuBitGraph,
81        SubGraphLike, SubSetLike, SubSetOps,
82    },
83};
84use serde::{Deserialize, Serialize};
85use symbolica::{domains::dual::HyperDual, prelude::*};
86use three_dimensional_reps::GeneratedThreeDExpression;
87use tracing::{debug, warn};
88use typed_index_collections::{TiVec, ti_vec};
89
90use super::generation_progress::{self, GenerationProcessKind, GenerationProgressPhase};
91use super::threshold_counterterms::{
92    ResolvedCutGroupThresholdCounterterms, ResolvedThresholdCountertermAssociation,
93    ResolvedThresholdCountertermVariant, ResolvedThresholdCounterterms, ThresholdCountertermOrigin,
94    ThresholdCountertermSide, ThresholdCountertermVariantId,
95};
96
97use crate::{
98    cff::esurface::{Esurface, EsurfaceID},
99    graph::{ExternalConnection, FeynmanGraph, Graph},
100    integrands::process::{
101        ProcessIntegrand,
102        cross_section::{CrossSectionGraphTerm, CrossSectionIntegrand},
103    },
104    model::Model,
105};
106
107use crate::processes::ProcessDefinition;
108
109#[derive(Clone, Debug, Encode, Decode)]
110pub struct IteratedCtCollection<T> {
111    data: Vec<T>,
112    num_right_thresholds: usize,
113}
114
115#[cfg(test)]
116#[path = "cross_section/ir_safe_threshold_fixture_tests.rs"]
117mod ir_safe_threshold_fixture_tests;
118
119impl<T> IteratedCtCollection<T> {
120    pub(crate) fn new(
121        data: Vec<T>,
122        num_left_thresholds: usize,
123        num_right_thresholds: usize,
124    ) -> Self {
125        let expected_len = num_left_thresholds
126            .checked_mul(num_right_thresholds)
127            .expect("iterated threshold-counterterm dimensions overflow usize");
128        assert_eq!(
129            data.len(),
130            expected_len,
131            "iterated threshold-counterterm data must contain one entry per left/right pair"
132        );
133        Self {
134            data,
135            num_right_thresholds,
136        }
137    }
138
139    pub fn map_ref<U, F>(&self, f: F) -> IteratedCtCollection<U>
140    where
141        F: Fn(&T) -> U,
142    {
143        let data = self.data.iter().map(f).collect();
144        IteratedCtCollection {
145            data,
146            num_right_thresholds: self.num_right_thresholds,
147        }
148    }
149
150    pub(crate) fn iter(&self) -> impl Iterator<Item = &T> {
151        self.data.iter()
152    }
153
154    pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
155        self.data.iter_mut()
156    }
157
158    pub(crate) fn num_right_thresholds(&self) -> usize {
159        self.num_right_thresholds
160    }
161}
162
163impl<T> Index<(LeftThresholdId, RightThresholdId)> for IteratedCtCollection<T> {
164    type Output = T;
165
166    fn index(&self, index: (LeftThresholdId, RightThresholdId)) -> &Self::Output {
167        let (left_id, right_id) = index;
168        &self.data[left_id.0 * self.num_right_thresholds + right_id.0]
169    }
170}
171
172impl<T> IndexMut<(LeftThresholdId, RightThresholdId)> for IteratedCtCollection<T> {
173    fn index_mut(&mut self, index: (LeftThresholdId, RightThresholdId)) -> &mut Self::Output {
174        let (left_id, right_id) = index;
175        &mut self.data[left_id.0 * self.num_right_thresholds + right_id.0]
176    }
177}
178
179#[derive(Clone, Encode, Decode)]
180#[trait_decode(trait = GammaLoopContext)]
181pub struct LUCounterTermData {
182    pub left_thresholds: TiVec<LeftThresholdId, RaisedEsurfaceGroup>,
183    pub right_thresholds: TiVec<RightThresholdId, RaisedEsurfaceGroup>,
184    pub left_variant_ids: TiVec<LeftThresholdId, ThresholdCountertermVariantId>,
185    pub right_variant_ids: TiVec<RightThresholdId, ThresholdCountertermVariantId>,
186    pub left_subspaces: TiVec<LeftThresholdId, SubspaceData>,
187    pub right_subspaces: TiVec<RightThresholdId, SubspaceData>,
188    pub left_atoms: TiVec<LeftThresholdId, ParametricIntegrands>,
189    pub right_atoms: TiVec<RightThresholdId, ParametricIntegrands>,
190    pub iterated: IteratedCtCollection<ParametricIntegrands>,
191}
192
193#[derive(Clone, Debug)]
194pub(crate) struct SingleThresholdPieces<T> {
195    pub(crate) local: T,
196    pub(crate) integrated: T,
197}
198
199#[derive(Clone, Debug)]
200pub(crate) struct IteratedThresholdPieces<T> {
201    pub(crate) local_local: T,
202    pub(crate) local_integrated: T,
203    pub(crate) integrated_local: T,
204    pub(crate) integrated_integrated: T,
205}
206
207#[derive(Clone, Copy, Debug, PartialEq, Eq)]
208pub(crate) enum LUThresholdHelperOutputs {
209    Legacy,
210    Pieces,
211    LegacyAndPieces,
212}
213
214fn max_dual_size_for_cut_cff_indices<'a>(
215    cut_cff_indices: impl Iterator<Item = &'a CutCFFIndex>,
216) -> usize {
217    cut_cff_indices
218        .map(|cut_cff_index| {
219            shape_from_cut_cff_index(cut_cff_index)
220                .map(|shape| HyperDual::<F<f64>>::new(shape).values.len())
221                .unwrap_or(1)
222        })
223        .max()
224        .unwrap_or(1)
225}
226
227define_index! {pub struct RightThresholdId;}
228define_index! {pub struct LeftThresholdId;}
229define_index! {pub struct CutGroupId;}
230define_index! {pub struct TopologicalThresholdId;}
231
232/// Eligibility of one threshold E-surface relative to one side of one physical cut.
233#[derive(Clone, Copy, Debug, PartialEq, Eq)]
234pub enum ThresholdCountertermStatus {
235    /// The E-surface does not depend on any active loop coordinate of the cut-side subspace.
236    NoRadialDependence,
237    /// The zero can only occur at a pinched boundary and has no strictly existing region.
238    AlwaysPinched,
239    /// A strictly existing region is possible, but its boundary includes a pinched limit.
240    CanBecomePinched,
241    /// Current resolved masses and kinematics rigorously exclude a zero over the full cut phase space.
242    ProvenNonExisting,
243    /// A zero is possible, or the available metadata is insufficient to exclude one safely.
244    PotentiallyExisting,
245}
246
247impl ThresholdCountertermStatus {
248    pub fn is_eligible_for_generation(self, check_current_model: bool) -> bool {
249        match self {
250            Self::NoRadialDependence | Self::AlwaysPinched => false,
251            Self::ProvenNonExisting => !check_current_model,
252            Self::CanBecomePinched | Self::PotentiallyExisting => true,
253        }
254    }
255}
256
257/// Persisted topology and invariant-bound metadata for one generated cut-relative CT association.
258#[derive(Clone, Debug, Encode, Decode)]
259#[trait_decode(trait = GammaLoopContext)]
260pub struct ThresholdCountertermAssociation {
261    pub topological_threshold_id: TopologicalThresholdId,
262    pub esurface_id: EsurfaceID,
263    pub cut_boundary_edges: Vec<EdgeIndex>,
264    pub threshold_boundary_edges: Vec<EdgeIndex>,
265    pub invariant_bound_is_applicable: bool,
266}
267
268#[derive(Clone, Debug, Default, Encode, Decode)]
269#[trait_decode(trait = GammaLoopContext)]
270pub struct CutThresholdCountertermAssociations {
271    pub left: Vec<ThresholdCountertermAssociation>,
272    pub right: Vec<ThresholdCountertermAssociation>,
273}
274
275impl ThresholdCountertermAssociation {
276    fn mass_sum(edges: &[EdgeIndex], masses: &EdgeVec<Option<F<f64>>>) -> Option<F<f64>> {
277        edges.iter().try_fold(F(0.0), |sum, edge_id| {
278            masses[*edge_id].as_ref().map(|mass| sum + mass.abs())
279        })
280    }
281
282    fn incoming_invariant_mass(
283        graph: &Graph,
284        runtime_settings: &RuntimeSettings,
285    ) -> Option<F<f64>> {
286        let external_momenta = runtime_settings
287            .kinematics
288            .externals
289            .get_dependent_externals(DependentMomentaConstructor::CrossSection)
290            .ok()?;
291        if external_momenta.is_empty()
292            || external_momenta.len() != graph.get_edges_in_initial_state_cut().len()
293        {
294            return None;
295        }
296
297        let total = external_momenta
298            .into_iter()
299            .reduce(|sum, momentum| sum + momentum)?;
300        let invariant_squared = total.square();
301        if invariant_squared.is_nan()
302            || invariant_squared.is_infinite()
303            || invariant_squared <= F(0.0)
304        {
305            None
306        } else {
307            Some(invariant_squared.sqrt())
308        }
309    }
310
311    fn classify_from_invariant_bounds(
312        &self,
313        cut_boundary_mass: &F<f64>,
314        threshold_boundary_mass: &F<f64>,
315        maximum_cut_invariant: Option<F<f64>>,
316        tolerance: &F<f64>,
317    ) -> ThresholdCountertermStatus {
318        if !self.invariant_bound_is_applicable
319            || cut_boundary_mass.is_nan()
320            || cut_boundary_mass.is_infinite()
321            || threshold_boundary_mass.is_nan()
322            || threshold_boundary_mass.is_infinite()
323            || tolerance.is_nan()
324            || tolerance.is_infinite()
325            || maximum_cut_invariant
326                .as_ref()
327                .is_some_and(|bound| bound.is_nan() || bound.is_infinite())
328        {
329            return ThresholdCountertermStatus::PotentiallyExisting;
330        }
331
332        let minimum_margin = cut_boundary_mass * cut_boundary_mass
333            - threshold_boundary_mass * threshold_boundary_mass;
334        let masses_match = minimum_margin.abs() <= *tolerance;
335        let threshold_minimum_is_reachable = minimum_margin <= *tolerance;
336
337        if masses_match
338            && (self.cut_boundary_edges.len() <= 1 || self.threshold_boundary_edges.len() <= 1)
339        {
340            return ThresholdCountertermStatus::AlwaysPinched;
341        }
342
343        if self.cut_boundary_edges.len() == 1 && self.threshold_boundary_edges.len() == 1 {
344            return ThresholdCountertermStatus::ProvenNonExisting;
345        }
346
347        if self.cut_boundary_edges.len() > 1 && self.threshold_boundary_edges.len() == 1 {
348            let lower_margin = threshold_boundary_mass * threshold_boundary_mass
349                - cut_boundary_mass * cut_boundary_mass;
350            if lower_margin <= *tolerance {
351                return ThresholdCountertermStatus::ProvenNonExisting;
352            }
353        }
354
355        let Some(maximum_cut_invariant) = maximum_cut_invariant else {
356            return if masses_match
357                && self.cut_boundary_edges.len() > 1
358                && self.threshold_boundary_edges.len() > 1
359            {
360                ThresholdCountertermStatus::CanBecomePinched
361            } else {
362                ThresholdCountertermStatus::PotentiallyExisting
363            };
364        };
365
366        let maximum_margin = maximum_cut_invariant * maximum_cut_invariant
367            - threshold_boundary_mass * threshold_boundary_mass;
368        if maximum_margin.abs() <= *tolerance {
369            ThresholdCountertermStatus::AlwaysPinched
370        } else if maximum_margin < -*tolerance {
371            ThresholdCountertermStatus::ProvenNonExisting
372        } else if threshold_minimum_is_reachable
373            && self.cut_boundary_edges.len() > 1
374            && self.threshold_boundary_edges.len() > 1
375        {
376            ThresholdCountertermStatus::CanBecomePinched
377        } else {
378            ThresholdCountertermStatus::PotentiallyExisting
379        }
380    }
381
382    pub fn classify_for_model(
383        &self,
384        graph: &Graph,
385        cut: &CrossSectionCut,
386        model: &Model,
387        param_builder: &ParamBuilder,
388        runtime_settings: &RuntimeSettings,
389        normalized_margin_tolerance: f64,
390    ) -> ThresholdCountertermStatus {
391        if !self.invariant_bound_is_applicable {
392            return ThresholdCountertermStatus::PotentiallyExisting;
393        }
394
395        match graph
396            .classify_threshold_pinch(&self.cut_boundary_edges, &self.threshold_boundary_edges)
397        {
398            ThresholdPinchStatus::Always => {
399                return ThresholdCountertermStatus::AlwaysPinched;
400            }
401            ThresholdPinchStatus::CanBecome | ThresholdPinchStatus::NotProven => {}
402        }
403
404        // Unlike `get_real_mass_vector`, retain unresolved or complex masses as unknown:
405        // replacing either with zero could turn an inconclusive bound into a false rejection.
406        let masses = graph.new_edgevec(|edge, _, _| {
407            if matches!(edge.mass, EdgeMass::Zero) {
408                Some(F(0.0))
409            } else {
410                edge.mass_value::<f64>(model, param_builder)
411                    .and_then(|mass| IsZero::is_zero(&mass.im).then_some(mass.re))
412            }
413        });
414        let Some(cut_boundary_mass) = Self::mass_sum(&self.cut_boundary_edges, &masses) else {
415            return ThresholdCountertermStatus::PotentiallyExisting;
416        };
417        let Some(threshold_boundary_mass) = Self::mass_sum(&self.threshold_boundary_edges, &masses)
418        else {
419            return ThresholdCountertermStatus::PotentiallyExisting;
420        };
421        let e_cm = F(runtime_settings.kinematics.e_cm.abs());
422        // The classified margins are differences of invariant masses squared, so the
423        // dimensionless threshold is normalized by E_cm squared.
424        let normalized_margin_tolerance = if normalized_margin_tolerance.is_finite() {
425            normalized_margin_tolerance.abs()
426        } else {
427            DEFAULT_ESURFACE_EXISTENCE_THRESHOLD
428        };
429        let tolerance = F(normalized_margin_tolerance) * e_cm * e_cm;
430
431        let maximum_cut_invariant = if self.cut_boundary_edges.len() == 1 {
432            Some(cut_boundary_mass)
433        } else {
434            let incoming_invariant = Self::incoming_invariant_mass(graph, runtime_settings);
435            let complement_mass = graph
436                .iter_edges_of(&cut.cut)
437                .map(|(_, edge_id, _)| edge_id)
438                .filter(|edge_id| !self.cut_boundary_edges.contains(edge_id))
439                .try_fold(F(0.0), |sum, edge_id| {
440                    masses[edge_id].as_ref().map(|mass| sum + mass.abs())
441                });
442            incoming_invariant
443                .zip(complement_mass)
444                .and_then(|(invariant, complement_mass)| {
445                    let maximum = invariant - complement_mass;
446                    (maximum > F(0.0)).then_some(maximum)
447                })
448        };
449        self.classify_from_invariant_bounds(
450            &cut_boundary_mass,
451            &threshold_boundary_mass,
452            maximum_cut_invariant,
453            &tolerance,
454        )
455    }
456}
457
458#[derive(Clone)]
459struct TopologicalThresholdCandidate {
460    topological_threshold_id: TopologicalThresholdId,
461    left: SuBitGraph,
462    cut: OrientedCut,
463    right: SuBitGraph,
464    threshold_esurface: Esurface,
465    selected_orientation_esurface_id: Option<EsurfaceID>,
466}
467
468#[derive(Clone)]
469struct ResolvedThresholdCountertermDraft {
470    name: String,
471    group_id: Option<usize>,
472    origin: ThresholdCountertermOrigin,
473    disable: bool,
474    requested_subspace: Option<Vec<EdgeIndex>>,
475    requested_parent_lmb: Option<Vec<EdgeIndex>>,
476    multiplier: Option<ThresholdCountertermMultiplier>,
477    subspace: SubspaceData,
478    status: ThresholdCountertermStatus,
479}
480
481impl ResolvedThresholdCountertermDraft {
482    fn is_compatible_with(
483        &self,
484        other: &Self,
485        all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
486    ) -> bool {
487        self.name == other.name
488            && self.group_id == other.group_id
489            && self.disable == other.disable
490            && self.multiplier == other.multiplier
491            && self
492                .subspace
493                .has_equivalent_embedding(&other.subspace, all_lmbs)
494    }
495}
496
497#[derive(Clone)]
498struct PhysicalThresholdCountertermDraft {
499    cut_id: CutId,
500    cut_group_id: CutGroupId,
501    cut_edges: Vec<EdgeIndex>,
502    side: ThresholdCountertermSide,
503    esurface_id: EsurfaceID,
504    threshold_edges: Vec<EdgeIndex>,
505    raised_threshold_id: RaisedEsurfaceId,
506    raised_esurface_group: RaisedEsurfaceGroup,
507    variants: Vec<ResolvedThresholdCountertermDraft>,
508}
509
510use derive_more::{From, Into};
511#[derive(Clone, Encode, Decode)]
512#[trait_decode(trait = GammaLoopContext)]
513pub struct CrossSection {
514    pub name: String,
515    pub integrand: Option<ProcessIntegrand>,
516    pub supergraphs: Vec<CrossSectionGraph>,
517    pub external_particles: Vec<ArcParticle>,
518    pub external_connections: Vec<ExternalConnection>,
519    pub n_incmoming: usize,
520    pub graph_group_structure: TiVec<GroupId, GraphGroup>,
521}
522
523impl CrossSection {
524    pub fn plan_graph_group_selection(
525        &self,
526        spec: &GraphGroupSelectionSpec,
527    ) -> Result<GraphGroupSelectionPlan> {
528        if spec.mode() == GraphGroupSelectionMode::CrossSectionAmplitudeGraphs {
529            return Err(eyre!(
530                "`select --amplitude-graphs` for cross sections requires process and generation settings context."
531            ));
532        }
533        if spec.has_raised_cut_rules() {
534            return Err(eyre!(
535                "Raised-cut signature selection for cross sections requires process and generation settings context."
536            ));
537        }
538        spec.plan(&self.graph_group_structure, |graph_id| {
539            self.supergraphs.get(graph_id).map(|graph| &graph.graph)
540        })
541    }
542
543    pub fn plan_graph_group_selection_with_context(
544        &self,
545        spec: &GraphGroupSelectionSpec,
546        model: &Model,
547        process_definition: &ProcessDefinition,
548        generation_settings: &GenerationSettings,
549    ) -> Result<GraphGroupSelectionPlan> {
550        match spec.mode() {
551            GraphGroupSelectionMode::MasterGraphs => {
552                spec.plan_with_analysis_contexts(
553                    &self.graph_group_structure,
554                    |graph_id| self.supergraphs.get(graph_id).map(|graph| &graph.graph),
555                    |_master_graph_id, master_graph| {
556                        Ok(vec![GraphSelectionSubject::whole_graph(master_graph)])
557                    },
558                    |master_graph_id, master_graph| {
559                        self.cut_selection_subjects(
560                            master_graph_id,
561                            master_graph,
562                            model,
563                            process_definition,
564                            generation_settings,
565                        )
566                    },
567                    "Graph-group selection structural filters have no graph analysis subjects.",
568                    "Raised-cut signature selection found no process-valid Cutkosky cuts to analyse.",
569                )
570            }
571            GraphGroupSelectionMode::CrossSectionAmplitudeGraphs => {
572                spec.plan_with_analysis_contexts(
573                    &self.graph_group_structure,
574                    |graph_id| self.supergraphs.get(graph_id).map(|graph| &graph.graph),
575                    |master_graph_id, master_graph| {
576                        let cross_section_graph =
577                            self.supergraphs.get(master_graph_id).ok_or_else(|| {
578                                eyre!(
579                                    "Graph group refers to missing master supergraph id {}.",
580                                    master_graph_id
581                                )
582                            })?;
583                        let cuts = cross_section_graph.process_valid_cuts(
584                            model,
585                            process_definition,
586                            generation_settings,
587                        )?;
588                        Ok(cuts
589                            .into_iter()
590                            .flat_map(|cut| cut.amplitude_side_subjects(master_graph))
591                            .collect())
592                    },
593                    |master_graph_id, master_graph| {
594                        self.cut_selection_subjects(
595                            master_graph_id,
596                            master_graph,
597                            model,
598                            process_definition,
599                            generation_settings,
600                        )
601                    },
602                    "`select --amplitude-graphs` found no process-valid Cutkosky cuts to analyse for structural filters.",
603                    "Raised-cut signature selection found no process-valid Cutkosky cuts to analyse.",
604                )
605            }
606        }
607    }
608
609    fn cut_selection_subjects<'a>(
610        &'a self,
611        master_graph_id: usize,
612        master_graph: &'a Graph,
613        model: &Model,
614        process_definition: &ProcessDefinition,
615        generation_settings: &GenerationSettings,
616    ) -> Result<Vec<GraphCutSelectionSubject<'a>>> {
617        let cross_section_graph = self.supergraphs.get(master_graph_id).ok_or_else(|| {
618            eyre!(
619                "Graph group refers to missing master supergraph id {}.",
620                master_graph_id
621            )
622        })?;
623        let cuts = cross_section_graph.process_valid_cuts(
624            model,
625            process_definition,
626            generation_settings,
627        )?;
628        Ok(cuts
629            .into_iter()
630            .map(|cut| GraphCutSelectionSubject::new(master_graph, cut.cut.as_subgraph()))
631            .collect())
632    }
633
634    pub fn amplitude_graph_signature_inventory(
635        &self,
636        model: &Model,
637        process_definition: &ProcessDefinition,
638        generation_settings: &GenerationSettings,
639    ) -> Result<GraphSelectionSignatureInventory> {
640        let subjects = self
641            .graph_group_structure
642            .iter()
643            .flat_map(|group| group.into_iter().next())
644            .map(|master_graph_id| {
645                let cross_section_graph =
646                    self.supergraphs.get(master_graph_id).ok_or_else(|| {
647                        eyre!(
648                            "Graph group refers to missing master supergraph id {}.",
649                            master_graph_id
650                        )
651                    })?;
652                let cuts = cross_section_graph.process_valid_cuts(
653                    model,
654                    process_definition,
655                    generation_settings,
656                )?;
657                Ok(cuts
658                    .into_iter()
659                    .flat_map(|cut| cut.amplitude_side_subjects(&cross_section_graph.graph))
660                    .collect::<Vec<_>>())
661            })
662            .collect::<Result<Vec<_>>>()?
663            .into_iter()
664            .flatten()
665            .collect::<Vec<_>>();
666
667        Ok(GraphSelectionSignatureInventory::from_analysis_subjects(
668            subjects,
669        ))
670    }
671
672    pub fn raised_cut_signature_inventory(
673        &self,
674        model: &Model,
675        process_definition: &ProcessDefinition,
676        generation_settings: &GenerationSettings,
677    ) -> Result<RaisedCutSignatureInventory> {
678        let subjects = self
679            .graph_group_structure
680            .iter()
681            .flat_map(|group| group.into_iter().next())
682            .map(|master_graph_id| {
683                let cross_section_graph =
684                    self.supergraphs.get(master_graph_id).ok_or_else(|| {
685                        eyre!(
686                            "Graph group refers to missing master supergraph id {}.",
687                            master_graph_id
688                        )
689                    })?;
690                let cuts = cross_section_graph.process_valid_cuts(
691                    model,
692                    process_definition,
693                    generation_settings,
694                )?;
695                Ok(cuts
696                    .into_iter()
697                    .map(|cut| {
698                        GraphCutSelectionSubject::new(
699                            &cross_section_graph.graph,
700                            cut.cut.as_subgraph(),
701                        )
702                    })
703                    .collect::<Vec<_>>())
704            })
705            .collect::<Result<Vec<_>>>()?
706            .into_iter()
707            .flatten()
708            .collect::<Vec<_>>();
709
710        Ok(RaisedCutSignatureInventory::from_cut_subjects(subjects))
711    }
712
713    pub fn validate_graph_group_selection_plan(
714        &self,
715        plan: &GraphGroupSelectionPlan,
716    ) -> Result<()> {
717        for &old_group_id in plan.retained_group_ids() {
718            plan.new_group_id_for_old(old_group_id).ok_or_else(|| {
719                eyre!(
720                    "Selection plan is missing compact group id for old group {}.",
721                    old_group_id.0
722                )
723            })?;
724            if old_group_id.0 >= self.graph_group_structure.len() {
725                return Err(eyre!(
726                    "Selection plan refers to missing graph group {}.",
727                    old_group_id.0
728                ));
729            }
730            let group = &self.graph_group_structure[old_group_id];
731            for old_graph_id in group {
732                if old_graph_id >= self.supergraphs.len() {
733                    return Err(eyre!(
734                        "Graph group {} refers to missing graph id {}.",
735                        old_group_id.0,
736                        old_graph_id
737                    ));
738                }
739            }
740            let master = group.master();
741            if master >= self.supergraphs.len() {
742                return Err(eyre!(
743                    "Graph group {} refers to missing master graph id {}.",
744                    old_group_id.0,
745                    master
746                ));
747            }
748        }
749
750        Ok(())
751    }
752
753    pub fn apply_graph_group_selection(&mut self, plan: &GraphGroupSelectionPlan) -> Result<()> {
754        self.validate_graph_group_selection_plan(plan)?;
755
756        let mut old_graph_to_new_group = vec![None; self.supergraphs.len()];
757        let mut old_graph_is_master = vec![false; self.supergraphs.len()];
758        for &old_group_id in plan.retained_group_ids() {
759            let new_group_id = plan.new_group_id_for_old(old_group_id).ok_or_else(|| {
760                eyre!(
761                    "Selection plan is missing compact group id for old group {}.",
762                    old_group_id.0
763                )
764            })?;
765            let group = &self.graph_group_structure[old_group_id];
766            for old_graph_id in group {
767                old_graph_to_new_group[old_graph_id] = Some(new_group_id);
768            }
769            old_graph_is_master[group.master()] = true;
770        }
771
772        let mut new_supergraphs = self
773            .supergraphs
774            .iter()
775            .cloned()
776            .enumerate()
777            .filter_map(|(old_graph_id, mut graph)| {
778                let new_group_id = old_graph_to_new_group[old_graph_id]?;
779                graph.graph.group_id = Some(new_group_id);
780                graph.graph.is_group_master = old_graph_is_master[old_graph_id];
781                if let Some(multi_channeling_setup) = &mut graph.derived_data.multi_channeling_setup
782                {
783                    multi_channeling_setup.graph.group_id = Some(new_group_id);
784                    multi_channeling_setup.graph.is_group_master =
785                        old_graph_is_master[old_graph_id];
786                }
787                Some(graph)
788            })
789            .collect::<Vec<_>>();
790
791        let mut parsed_graphs = new_supergraphs
792            .iter()
793            .map(|graph| graph.graph.clone())
794            .collect::<Vec<_>>();
795        let new_graph_group_structure = complete_group_parsing(&mut parsed_graphs)?;
796        for (graph, parsed_graph) in new_supergraphs.iter_mut().zip(parsed_graphs) {
797            graph.graph.group_id = parsed_graph.group_id;
798            graph.graph.is_group_master = parsed_graph.is_group_master;
799            if let Some(multi_channeling_setup) = &mut graph.derived_data.multi_channeling_setup {
800                multi_channeling_setup.graph.group_id = graph.graph.group_id;
801                multi_channeling_setup.graph.is_group_master = graph.graph.is_group_master;
802            }
803        }
804
805        self.supergraphs = new_supergraphs;
806        self.graph_group_structure = new_graph_group_structure;
807        self.integrand = None;
808        Ok(())
809    }
810
811    fn storage_path(&self, base: &Path) -> PathBuf {
812        base.join(&self.name)
813    }
814
815    pub fn export_standalone(
816        &self,
817        path: impl AsRef<Path>,
818        settings: &crate::processes::StandaloneExportSettings,
819    ) -> Result<()> {
820        if let Some(integrand) = &self.integrand {
821            integrand.export_standalone(path, settings)?;
822        } else {
823            return Err(eyre!(
824                "Cannot export standalone cross section {} without integrand",
825                self.name
826            ));
827        }
828
829        Ok(())
830    }
831
832    #[allow(dead_code)]
833    pub(crate) fn write_dot<W: std::io::Write>(
834        &self,
835        writer: &mut W,
836        settings: &DotExportSettings,
837    ) -> Result<(), std::io::Error> {
838        for graph in &self.supergraphs {
839            graph.write_dot(writer, settings)?;
840            writeln!(writer)?;
841        }
842        Ok(())
843    }
844
845    pub fn write_dot_fmt<W: std::fmt::Write>(
846        &self,
847        writer: &mut W,
848        settings: &DotExportSettings,
849    ) -> Result<(), std::fmt::Error> {
850        for graph in &self.supergraphs {
851            graph.write_dot_fmt(writer, settings)?;
852            writeln!(writer)?;
853        }
854        Ok(())
855    }
856
857    pub(crate) fn new(name: String) -> Self {
858        Self {
859            name,
860            integrand: None,
861            supergraphs: vec![],
862            external_connections: vec![],
863            external_particles: vec![],
864            n_incmoming: 0,
865            graph_group_structure: TiVec::new(),
866        }
867    }
868
869    pub fn from_graph_list(name: String, mut graphs: Vec<Graph>, _model: &Model) -> Result<Self> {
870        let mut cross_section = CrossSection::new(name);
871        cross_section.graph_group_structure = complete_group_parsing(&mut graphs)?;
872        // println!("group structure: {:?}", cross_section.graph_group_structure);
873
874        for group in &cross_section.graph_group_structure {
875            for graph_id in group.into_iter().skip(1) {
876                let graph = &graphs[graph_id];
877                if !graph.threshold_counterterms.autogenerated {
878                    return Err(eyre!(
879                        "Cross-section graph '{}' is a non-master member of graph group '{}', but declares threshold_counterterms metadata. Threshold metadata on non-master cross-section graphs is unsupported: cross-section threshold solve groups are graph-local. Put the metadata on the master graph or import this graph in its own group.",
880                        graph.name,
881                        graphs[group.master()].name,
882                    ));
883                }
884            }
885        }
886
887        for cross_section_graph in graphs {
888            // cross_section_graph.param_builder =  ParamBuilder::new(&cross_section_graph, model);
889            cross_section.add_supergraph(cross_section_graph)?;
890        }
891        Ok(cross_section)
892    }
893
894    pub(crate) fn warm_up(&mut self, model: &Model) -> Result<()> {
895        if let Some(integrand) = &mut self.integrand {
896            integrand.warm_up(model)
897        } else {
898            Err(eyre!(
899                "Cannot warm up amplitude {} without integrand",
900                self.name
901            ))
902        }
903    }
904
905    fn add_supergraph(&mut self, supergraph: Graph) -> Result<()> {
906        if self.external_particles.is_empty() {
907            let external_particles = supergraph.get_external_partcles();
908            if !external_particles.len().is_multiple_of(2) {
909                return Err(eyre!(
910                    "expected even number of externals for forward scattering graph"
911                ));
912            }
913            self.external_particles = external_particles;
914            self.n_incmoming = self.external_particles.len() / 2;
915        } else if self.external_particles != supergraph.get_external_partcles() {
916            return Err(eyre!(
917                "attempt to add supergraph with differnt external particles"
918            ));
919        }
920
921        let cross_section_graph = CrossSectionGraph::new(supergraph);
922        self.supergraphs.push(cross_section_graph);
923
924        // TODO: validate that the graph is compatible
925        Ok(())
926    }
927
928    pub fn preprocess(
929        &mut self,
930        model: &Model,
931        process_definition: &ProcessDefinition,
932        global_settings: &GenerationSettings,
933        runtime_default: LockedRuntimeSettings,
934        generation_pool: &ThreadPool,
935    ) -> Result<Vec<NamedGraphGenerationReport>> {
936        global_settings.validate_for_process(GenerationType::CrossSection)?;
937        let integrand_name = self.name.clone();
938        generation_progress::begin_phase(
939            GenerationProgressPhase::GraphPreprocessing,
940            GenerationProcessKind::CrossSection,
941            &process_definition.folder_name,
942            &integrand_name,
943            self.supergraphs.len(),
944            None,
945        );
946        generation_pool.install(|| {
947            self.supergraphs
948                .par_iter_mut()
949                .map(|supergraph| {
950                    if crate::is_interrupted() {
951                        return Err(eyre!("Generation interrupted by user"));
952                    }
953                    let graph_name = supergraph.graph.name.clone();
954                    generation_progress::graph_started(
955                        GenerationProcessKind::CrossSection,
956                        &integrand_name,
957                        &graph_name,
958                        None,
959                    );
960                    let stats = supergraph.preprocess(
961                        model,
962                        process_definition,
963                        global_settings,
964                        runtime_default,
965                    )?;
966                    if crate::is_interrupted() {
967                        return Err(eyre!("Generation interrupted by user"));
968                    }
969                    generation_progress::graph_finished(
970                        GenerationProcessKind::CrossSection,
971                        &integrand_name,
972                        &graph_name,
973                        &stats,
974                        None,
975                    );
976                    Ok(NamedGraphGenerationReport {
977                        integrand_name: integrand_name.clone(),
978                        graph_name,
979                        stats,
980                    })
981                })
982                .collect::<Result<Vec<_>>>()
983        })
984    }
985
986    pub fn build_integrand(
987        &mut self,
988        model: &Model,
989        process_definition: &ProcessDefinition,
990        global_settings: &GlobalSettings,
991        runtime_default: LockedRuntimeSettings,
992        generation_pool: &ThreadPool,
993    ) -> Result<Vec<NamedGraphGenerationReport>> {
994        let process_name = process_definition.folder_name.as_str();
995        let started = std::time::Instant::now();
996        crate::debug_tags!(#generation, #profile, #graph, #summary;
997            stage = "cross_section_build_integrand_start",
998            integrand = %self.name,
999            graph_count = self.supergraphs.len(),
1000            "Generation timing milestone"
1001        );
1002        if crate::is_interrupted() {
1003            return Err(eyre!("Generation interrupted by user"));
1004        }
1005        let integrand_name = self.name.clone();
1006        let total_cuts = self
1007            .supergraphs
1008            .iter()
1009            .map(|sg| {
1010                sg.derived_data
1011                    .cut_group_data
1012                    .cut_groups
1013                    .iter()
1014                    .map(|cut_group| cut_group.cuts.len())
1015                    .sum::<usize>()
1016            })
1017            .sum();
1018        generation_progress::begin_phase(
1019            GenerationProgressPhase::GraphGeneration,
1020            GenerationProcessKind::CrossSection,
1021            process_name,
1022            &integrand_name,
1023            self.supergraphs.len(),
1024            Some(total_cuts),
1025        );
1026        let mut graph_reports = Vec::new();
1027        let terms = generation_pool.install(|| {
1028            self.supergraphs
1029                .par_iter_mut()
1030                .map(|sg| {
1031                    if crate::is_interrupted() {
1032                        return Err(eyre!("Generation interrupted by user"));
1033                    }
1034                    let graph_started = std::time::Instant::now();
1035                    crate::debug_tags!(#generation, #profile, #graph, #summary;
1036                        stage = "generate_term_for_graph_start",
1037                        integrand = %integrand_name,
1038                        graph = %sg.graph.name,
1039                        "Generation timing milestone"
1040                    );
1041                    generation_progress::graph_started(
1042                        GenerationProcessKind::CrossSection,
1043                        &integrand_name,
1044                        &sg.graph.name,
1045                        Some(sg.cuts.len()),
1046                    );
1047                    let _progress_context_guard = generation_progress::enter_progress_context(
1048                        format!("{} / {} cuts", sg.graph.name, sg.cuts.len()),
1049                    );
1050                    let (term, mut stats) = sg.generate_term_for_graph(model, global_settings)?;
1051                    if crate::is_interrupted() {
1052                        return Err(eyre!("Generation interrupted by user"));
1053                    }
1054                    stats.total_time += graph_started.elapsed();
1055                    crate::debug_tags!(#generation, #profile, #graph, #summary;
1056                        stage = "generate_term_for_graph_done",
1057                        integrand = %integrand_name,
1058                        graph = %sg.graph.name,
1059                        elapsed_ms = graph_started.elapsed().as_secs_f64() * 1000.0,
1060                        "Generation timing milestone"
1061                    );
1062                    generation_progress::graph_finished(
1063                        GenerationProcessKind::CrossSection,
1064                        &integrand_name,
1065                        &sg.graph.name,
1066                        &stats,
1067                        None,
1068                    );
1069                    Ok((
1070                        term,
1071                        NamedGraphGenerationReport {
1072                            integrand_name: integrand_name.clone(),
1073                            graph_name: sg.graph.name.clone(),
1074                            stats,
1075                        },
1076                    ))
1077                })
1078                .collect::<Result<Vec<_>>>()
1079        })?;
1080        if crate::is_interrupted() {
1081            return Err(eyre!("Generation interrupted by user"));
1082        }
1083        for (_, report) in &terms {
1084            graph_reports.push(report.clone());
1085        }
1086        let mut terms = terms.into_iter().map(|(term, _)| term).collect::<Vec<_>>();
1087
1088        for group in self.graph_group_structure.iter() {
1089            let master = group.master();
1090            let mc_of_master = self.supergraphs[master]
1091                .derived_data
1092                .multi_channeling_setup
1093                .as_ref()
1094                .unwrap();
1095
1096            for graph_id in group.into_iter() {
1097                terms[graph_id].multi_channeling_setup = mc_of_master.clone();
1098            }
1099        }
1100
1101        let backend_started = std::time::Instant::now();
1102        let graph_count = terms.len();
1103        crate::debug_tags!(#generation, #profile, #compile, #graph, #summary;
1104            stage = "prepare_runtime_backends_start",
1105            integrand = %self.name,
1106            graph_count,
1107            elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1108            "Generation timing milestone"
1109        );
1110        generation_progress::backend_started(
1111            GenerationProcessKind::CrossSection,
1112            &self.name,
1113            graph_count,
1114        );
1115        let mut cross_section_integrand = CrossSectionIntegrand {
1116            settings: runtime_default.into(),
1117            data: CrossSectionIntegrandData {
1118                compilation: global_settings
1119                    .generation
1120                    .compile
1121                    .frozen_mode(&global_settings.generation.evaluator),
1122                loop_cache_id: 0,
1123                external_cache_id: 0,
1124                base_external_cache_id: 0,
1125                rotations: None,
1126                symmetrize_left_right_states: process_definition.symmetrize_left_right_states,
1127                name: self.name.clone(),
1128                external_connections: self.external_connections.clone(),
1129                n_incoming: self.n_incmoming,
1130                // polarizations,
1131                graph_terms: terms,
1132                graph_group_structure: self.graph_group_structure.clone(),
1133                graph_to_group_id: graph_to_group_id_for_group_structure(
1134                    &self.graph_group_structure,
1135                ),
1136                explicit_orientation_sum_only: global_settings
1137                    .generation
1138                    .explicit_orientation_sum_only,
1139            },
1140            event_processing_runtime: Default::default(),
1141            active_f64_backend: Default::default(),
1142        };
1143        let compile_times = cross_section_integrand
1144            .prepare_runtime_backends_after_generation_with_compile_times()?;
1145        crate::debug_tags!(#generation, #profile, #compile, #graph, #summary;
1146            stage = "prepare_runtime_backends_done",
1147            integrand = %self.name,
1148            graph_count,
1149            elapsed_ms = backend_started.elapsed().as_secs_f64() * 1000.0,
1150            total_elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1151            "Generation timing milestone"
1152        );
1153        generation_progress::backend_finished(
1154            GenerationProcessKind::CrossSection,
1155            &self.name,
1156            backend_started.elapsed(),
1157        );
1158        for (report, compile_time) in graph_reports.iter_mut().zip(compile_times) {
1159            report.stats.evaluator_compile_time += compile_time;
1160            report.stats.total_time += compile_time;
1161        }
1162
1163        self.integrand = Some(ProcessIntegrand::CrossSection(cross_section_integrand));
1164        crate::debug_tags!(#generation, #profile, #graph, #summary;
1165            stage = "cross_section_build_integrand_done",
1166            integrand = %self.name,
1167            graph_count = self.supergraphs.len(),
1168            elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1169            "Generation timing milestone"
1170        );
1171        Ok(graph_reports)
1172    }
1173
1174    pub fn compile(
1175        &mut self,
1176        path: impl AsRef<Path>,
1177        override_existing: bool,
1178        thread_pool: &ThreadPool,
1179    ) -> Result<Vec<NamedGraphGenerationReport>> {
1180        info!("Compiling cross section {}", self.name);
1181        let p = self.storage_path(path.as_ref());
1182
1183        let result = fs::create_dir_all(&p).with_context(|| {
1184            format!(
1185                "Trying to create directory to compile cross section {}",
1186                p.display()
1187            )
1188        });
1189
1190        if override_existing {
1191            result?;
1192        }
1193
1194        if let Some(integrand) = &mut self.integrand {
1195            let compile_times = integrand.compile(&p, override_existing, thread_pool)?;
1196            return Ok(compile_times
1197                .into_iter()
1198                .map(|(graph_name, duration)| NamedGraphGenerationReport {
1199                    integrand_name: self.name.clone(),
1200                    graph_name,
1201                    stats: GraphGenerationStats {
1202                        total_time: duration,
1203                        evaluator_compile_time: duration,
1204                        ..GraphGenerationStats::default()
1205                    },
1206                })
1207                .collect());
1208        }
1209        Ok(Vec::new())
1210    }
1211
1212    pub fn save(&mut self, path: impl AsRef<Path>, override_existing: bool) -> Result<()> {
1213        let p = self.storage_path(path.as_ref());
1214        let r = fs::create_dir_all(&p).with_context(|| {
1215            format!(
1216                "Trying to create directory to save cross section {}",
1217                p.display()
1218            )
1219        });
1220
1221        if override_existing {
1222            r?;
1223        }
1224
1225        let integrand = self.integrand.take();
1226        if let Some(integrand) = &integrand {
1227            integrand.save(&p, override_existing)?;
1228        }
1229
1230        let binary = bincode::encode_to_vec(&(*self), bincode::config::standard())?;
1231        if override_existing {
1232            fs::write(p.join("cs.bin"), &binary)?;
1233        } else {
1234            let mut file = File::create_new(p.join("cs.bin"))?;
1235            file.write_all(&binary)?;
1236        }
1237
1238        self.integrand = integrand;
1239        Ok(())
1240    }
1241
1242    pub(crate) fn load(path: impl AsRef<Path>, context: GammaLoopContextContainer) -> Result<Self> {
1243        let binary = fs::read(path.as_ref().join("cs.bin"))?;
1244        let (mut cs, _): (Self, _) =
1245            bincode::decode_from_slice_with_context(&binary, bincode::config::standard(), context)?;
1246
1247        if path.as_ref().join("integrand").exists() {
1248            let integrand = CrossSectionIntegrand::load(path.as_ref().join("integrand"), context)?;
1249            cs.integrand = Some(ProcessIntegrand::CrossSection(integrand));
1250        }
1251
1252        Ok(cs)
1253    }
1254}
1255
1256#[derive(Clone, bincode::Encode, bincode::Decode)]
1257pub struct CrossSectionCut {
1258    pub cut: OrientedCut,
1259    pub left: SuBitGraph,
1260    pub right: SuBitGraph,
1261}
1262
1263impl CrossSectionCut {
1264    fn amplitude_side_subjects<'a>(&self, graph: &'a Graph) -> [GraphSelectionSubject<'a>; 2] {
1265        let cut_edges = self.cut.as_subgraph();
1266        [
1267            GraphSelectionSubject::cut_side_amplitude_subgraph(
1268                graph,
1269                self.left.subtract(&cut_edges),
1270            ),
1271            GraphSelectionSubject::cut_side_amplitude_subgraph(
1272                graph,
1273                self.right.subtract(&cut_edges),
1274            ),
1275        ]
1276    }
1277
1278    pub(crate) fn is_s_channel(&self, cross_section_graph: &CrossSectionGraph) -> Result<bool> {
1279        let nodes_of_left_cut: Vec<_> = cross_section_graph
1280            .graph
1281            .underlying
1282            .iter_nodes_of(&self.left)
1283            .map(|(nid, _, _)| nid)
1284            .collect();
1285
1286        let left_node = cross_section_graph
1287            .graph
1288            .underlying
1289            .combine_to_single_hedgenode(&nodes_of_left_cut);
1290        let res = left_node.includes(&cross_section_graph.source_nodes)
1291            && cross_section_graph.target_nodes.weakly_disjoint(&left_node);
1292
1293        if !res {
1294            warn!("s channel check wrong");
1295        }
1296
1297        Ok(true)
1298    }
1299
1300    pub(crate) fn is_valid_for_process(
1301        &self,
1302        cross_section_graph: &CrossSectionGraph,
1303        process: &ProcessDefinition,
1304        model: &Model,
1305    ) -> Result<bool> {
1306        if self.is_s_channel(cross_section_graph)? {
1307            let cut_content_builder = self
1308                .cut
1309                .iter_edges(&cross_section_graph.graph.underlying)
1310                .filter_map(|(orientation, edge_data)| {
1311                    Some(if orientation == Orientation::Reversed {
1312                        edge_data.data.particle()?.get_anti_particle(model)
1313                    } else {
1314                        edge_data.data.particle()?.clone()
1315                    })
1316                })
1317                .collect_vec();
1318
1319            let covariant_states = process.covariant_cut_states(model)?;
1320            let any_pdg_list_passes = covariant_states
1321                .iter()
1322                .map(|x| {
1323                    x.iter()
1324                        .map(|pdg| model.get_particle_from_pdg(*pdg as isize))
1325                })
1326                .any(|particle_content| {
1327                    let mut cut_content = cut_content_builder.clone();
1328                    debug!(
1329                        "cut content: {:?}",
1330                        cut_content.iter().map(|p| p.name.clone()).collect_vec()
1331                    );
1332
1333                    for particle in particle_content {
1334                        if let Some(index) = cut_content.iter().position(|p| p == &particle) {
1335                            cut_content.remove(index);
1336                        } else {
1337                            debug!("wrong particles");
1338                            return false;
1339                        }
1340                    }
1341
1342                    let (n_unresolved, unresolved_cut_content) =
1343                        process.unresolved_cut_content(model);
1344
1345                    if cut_content.len() > n_unresolved {
1346                        debug!(" too many unresolved particles");
1347                        return false;
1348                    }
1349
1350                    if !cut_content
1351                        .iter()
1352                        .all(|particle| unresolved_cut_content.contains(particle))
1353                    {
1354                        debug!("wrong unresolved particles");
1355                        return false;
1356                    }
1357
1358                    true
1359                });
1360
1361            if !any_pdg_list_passes {
1362                debug!("wrong pdg list");
1363                return Ok(false);
1364            }
1365
1366            let amplitude_couplings = process.amplitude_filters.get_coupling_orders();
1367            let amplitude_loop_count = process.amplitude_filters.get_loop_count_range();
1368
1369            if let Some((min_loop, max_loop)) = amplitude_loop_count {
1370                let loop_range = min_loop..=max_loop;
1371                let left_internal_subgraph = InternalSubGraph::cleaned_filter_pessimist(
1372                    self.left.clone(),
1373                    &cross_section_graph.graph.underlying,
1374                );
1375
1376                let right_internal_subgraph = InternalSubGraph::cleaned_filter_pessimist(
1377                    self.right.clone(),
1378                    &cross_section_graph.graph.underlying,
1379                );
1380
1381                let left_loop = cross_section_graph
1382                    .graph
1383                    .underlying
1384                    .cyclotomatic_number(&left_internal_subgraph);
1385
1386                let right_loop = cross_section_graph
1387                    .graph
1388                    .underlying
1389                    .cyclotomatic_number(&right_internal_subgraph);
1390
1391                let total_loops = left_loop + right_loop;
1392
1393                if !loop_range.contains(&total_loops) {
1394                    debug!("incorrect loop count");
1395                    return Ok(false);
1396                }
1397            }
1398
1399            if amplitude_couplings.is_some() {
1400                todo!("waiting for update")
1401            }
1402
1403            Ok(true)
1404        } else {
1405            debug!("cut is not s channel");
1406            Ok(false)
1407        }
1408    }
1409}
1410
1411#[derive(
1412    Debug, Clone, Serialize, Decode, Deserialize, From, Into, Hash, PartialEq, Copy, Eq, Encode,
1413)]
1414pub struct CutId(pub usize);
1415
1416impl Display for CutId {
1417    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1418        write!(f, "{}", self.0)
1419    }
1420}
1421
1422#[derive(Clone, Encode, Decode)]
1423#[trait_decode(trait = GammaLoopContext)]
1424pub struct CrossSectionGraph {
1425    pub graph: Graph,
1426    pub source_nodes: HedgeNode,
1427    pub target_nodes: HedgeNode,
1428    pub cuts: TiVec<CutId, CrossSectionCut>,
1429    pub cut_esurface: TiVec<CutId, Esurface>,
1430    pub cut_esurface_id_map: TiVec<CutId, EsurfaceID>,
1431    pub derived_data: CrossSectionDerivedData,
1432}
1433
1434#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1435pub struct CutkoskyCutCount {
1436    pub candidate_st_cuts: usize,
1437    pub multi_edge_candidate_cuts: usize,
1438    pub process_compatible_cuts: usize,
1439    pub selected_cuts: usize,
1440}
1441
1442impl CrossSectionGraph {
1443    pub(crate) fn new(graph: Graph) -> Self {
1444        let (source_node, target_node) = graph.get_source_and_target();
1445
1446        Self {
1447            graph,
1448            source_nodes: source_node,
1449            target_nodes: target_node,
1450            cuts: TiVec::new(),
1451            cut_esurface: TiVec::new(),
1452            cut_esurface_id_map: TiVec::new(),
1453            derived_data: CrossSectionDerivedData::new_empty(),
1454        }
1455    }
1456
1457    pub(crate) fn apply_spin_sum(
1458        &mut self,
1459        model: &Model,
1460        generation_settings: &GenerationSettings,
1461        locked_runtime_settings: &LockedRuntimeSettings,
1462    ) -> Result<()> {
1463        for (extid, hel) in locked_runtime_settings.helicities().iter().enumerate() {
1464            //println!("{extid},{hel:?}");
1465            let eid = self.graph.loop_momentum_basis.ext_edges[ExternalIndex(extid)];
1466
1467            let Some(p) = self.graph.underlying[eid].particle() else {
1468                continue;
1469            };
1470
1471            match hel {
1472                Helicity::Summed => {
1473                    let Some(p) = p.polarization_sum(
1474                        eid,
1475                        false,
1476                        generation_settings.vector_polarization_sum_gauge,
1477                    )?
1478                    else {
1479                        continue;
1480                    };
1481                    self.graph.global_prefactor.projector =
1482                        self.graph.global_prefactor.projector.replace_multiple(&[p]);
1483                }
1484                Helicity::SummedAveraged => {
1485                    let Some(p) = p.polarization_sum(
1486                        eid,
1487                        true,
1488                        generation_settings.vector_polarization_sum_gauge,
1489                    )?
1490                    else {
1491                        continue;
1492                    };
1493                    self.graph.global_prefactor.projector =
1494                        self.graph.global_prefactor.projector.replace_multiple(&[p]);
1495                }
1496                _ => {}
1497            }
1498        }
1499
1500        self.graph.polarizations = self.graph.global_prefactor.polarizations();
1501        self.graph.param_builder = ParamBuilder::new(
1502            &self.graph,
1503            model,
1504            &self.graph.loop_momentum_basis,
1505            &self.graph.param_builder.pairs.additional_params.params,
1506        )?;
1507        Ok(())
1508    }
1509
1510    pub(crate) fn preprocess(
1511        &mut self,
1512        model: &Model,
1513        process_definition: &ProcessDefinition,
1514        settings: &GenerationSettings,
1515        runtime_default: LockedRuntimeSettings,
1516    ) -> Result<GraphGenerationStats> {
1517        let preprocess_started = std::time::Instant::now();
1518        let mut stats = GraphGenerationStats::default();
1519        self.graph.validate_real_masses(model)?;
1520        process_definition.covariant_cut_states(model)?;
1521        self.derived_data.covariant_cut_representatives =
1522            process_definition.covariant_cut_representatives(model);
1523        self.apply_spin_sum(model, settings, &runtime_default)?;
1524        debug_tags!(#generation; "generating cuts");
1525        self.generate_cuts(model, process_definition, settings)?;
1526        debug_tags!(#generation; "generating esurfaces corresponding to cuts");
1527        self.generate_esurface_cuts();
1528        debug_tags!(#generation; "generating cff");
1529        stats.merge_in_place(&self.generate_cff(settings)?);
1530        debug_tags!(#generation; "building lmbs");
1531        self.build_lmbs()?;
1532        debug_tags!(#generation; "building multi channeling channels");
1533
1534        if self.graph.is_group_master {
1535            self.build_multi_channeling_channels(settings.override_lmb_heuristics)?;
1536        }
1537
1538        let vk = crate::utils::vakint()?;
1539        debug_tags!(#generation; "building parametric integrand");
1540        let cff_energy_degree_bound_reports = Mutex::new(Vec::new());
1541        self.build_parametric_integrand(settings, vk, &cff_energy_degree_bound_reports)?;
1542        //self.build_parametric_integrand_cut_groups(settings)?;
1543
1544        let threshold_candidates = self.topological_threshold_candidates(settings)?;
1545        self.derived_data.threshold_candidate_esurface_ids = threshold_candidates
1546            .iter()
1547            .filter_map(|candidate| candidate.selected_orientation_esurface_id)
1548            .sorted()
1549            .dedup()
1550            .collect();
1551        self.derived_data.resolved_threshold_counterterms = None;
1552        self.derived_data.threshold_counterterms = TiVec::new();
1553        self.derived_data.subspace_data = TiVec::new();
1554
1555        self.derived_data.cut_threshold_associations =
1556            ti_vec![CutThresholdCountertermAssociations::default(); self.cuts.len()];
1557
1558        if settings.threshold_subtraction.enable_thresholds {
1559            debug_tags!(#generation, #subtraction; "building threshold counterterm");
1560            self.build_subspace_data()?;
1561            let runtime_settings: RuntimeSettings = runtime_default.into();
1562            if let Err(error) = self.build_threshold_counterterm(
1563                model,
1564                settings,
1565                &runtime_settings,
1566                &threshold_candidates,
1567                vk,
1568                &cff_energy_degree_bound_reports,
1569            ) {
1570                self.derived_data.resolved_threshold_counterterms = None;
1571                self.derived_data.threshold_counterterms = TiVec::new();
1572                self.derived_data.subspace_data = TiVec::new();
1573                self.derived_data.cut_threshold_associations =
1574                    ti_vec![CutThresholdCountertermAssociations::default(); self.cuts.len()];
1575                return Err(error);
1576            }
1577        }
1578
1579        stats.cff_energy_degree_bound_reports = cff_energy_degree_bound_reports
1580            .into_inner()
1581            .unwrap_or_else(|poisoned| poisoned.into_inner());
1582
1583        stats.total_time += preprocess_started.elapsed();
1584        Ok(stats)
1585    }
1586
1587    #[allow(dead_code)]
1588    pub(crate) fn write_dot<W: std::io::Write>(
1589        &self,
1590        writer: &mut W,
1591        settings: &DotExportSettings,
1592    ) -> Result<(), std::io::Error> {
1593        if let Some(graph) = self.graph_with_materialized_threshold_counterterms(settings) {
1594            graph.dot_serialize_io(writer, settings)
1595        } else {
1596            self.graph.dot_serialize_io(writer, settings)
1597        }
1598    }
1599
1600    pub(crate) fn write_dot_fmt<W: std::fmt::Write>(
1601        &self,
1602        writer: &mut W,
1603        settings: &DotExportSettings,
1604    ) -> Result<(), std::fmt::Error> {
1605        if let Some(graph) = self.graph_with_materialized_threshold_counterterms(settings) {
1606            graph.dot_serialize_fmt(writer, settings)
1607        } else {
1608            self.graph.dot_serialize_fmt(writer, settings)
1609        }
1610    }
1611
1612    fn graph_with_materialized_threshold_counterterms(
1613        &self,
1614        settings: &DotExportSettings,
1615    ) -> Option<Graph> {
1616        // Non-master cross-section members cannot accept threshold metadata on reimport.
1617        if !settings.include_autogenerated_fields
1618            || (self.graph.group_id.is_some() && !self.graph.is_group_master)
1619        {
1620            return None;
1621        }
1622        let resolved = self.derived_data.resolved_threshold_counterterms.as_ref()?;
1623        let all_lmbs = self.derived_data.lmbs.as_ref()?;
1624        let mut graph = self.graph.clone();
1625        graph.threshold_counterterms = crate::graph::autogen::Autogen::explicit(
1626            resolved.materialized_spec(&self.graph.threshold_counterterms, all_lmbs),
1627        );
1628        Some(graph)
1629    }
1630
1631    fn generate_cff(&mut self, settings: &GenerationSettings) -> Result<GraphGenerationStats> {
1632        self.graph.set_medium_mode(settings.medium.mode)?;
1633        self.graph.ensure_energy_convergent_cycles(
1634            &self
1635                .graph
1636                .no_dummy()
1637                .subtract(&self.graph.initial_state_cut),
1638        )?;
1639        let canonize_esurface = self
1640            .graph
1641            .get_esurface_canonization(&self.graph.loop_momentum_basis);
1642
1643        let contract_edges = self
1644            .graph
1645            .iter_edges_of(
1646                &self
1647                    .graph
1648                    .tree_edges
1649                    .subtract(&self.graph.initial_state_cut),
1650            )
1651            .map(|x| x.1)
1652            .collect_vec();
1653
1654        // The options provision maps from the complete factorized numerator.
1655        // The numerator itself remains caller-owned and is mapped only when
1656        // the corresponding orientation integrand is assembled.
1657        let options = self.graph.production_cff_3d_expression_options(settings)?;
1658        let mut global_cff = self.graph.generate_3d_expression_for_integrand(
1659            &contract_edges,
1660            &canonize_esurface,
1661            &options,
1662            None,
1663        )?;
1664        let raised_edge_groups = self.graph.get_raised_edge_groups();
1665        normalize_three_d_expression_cut_support_with_raised_edge_groups(
1666            &mut global_cff.expression,
1667            &raised_edge_groups,
1668        );
1669
1670        let generated_esurfaces = &global_cff.expression.surfaces.esurface_cache;
1671        let normalized_generated_esurfaces = generated_esurfaces
1672            .iter()
1673            .map(|esurface| {
1674                Graph::normalize_esurface_with_raised_edge_groups(esurface, &raised_edge_groups)
1675            })
1676            .collect::<TiVec<EsurfaceID, _>>();
1677        let cut_esurface_map = self
1678            .cut_esurface
1679            .iter()
1680            .map(|esurface| {
1681                let normalized =
1682                    Graph::normalize_esurface_with_raised_edge_groups(esurface, &raised_edge_groups);
1683                generated_esurfaces
1684                    .iter()
1685                    .position(|candidate| candidate == esurface)
1686                    .or_else(|| {
1687                        normalized_generated_esurfaces
1688                            .iter()
1689                            .position(|candidate| candidate == &normalized)
1690                    })
1691                    .map(EsurfaceID)
1692                    .ok_or_else(|| {
1693                        eyre!(
1694                            "Cutkosky-cut E-surface {esurface:?} for graph {} is absent from its generated CFF expression catalogue (normalized: {normalized:?})",
1695                            self.graph.name
1696                        )
1697                    })
1698            })
1699            .collect::<Result<_>>()?;
1700
1701        self.cut_esurface_id_map = cut_esurface_map;
1702
1703        let esurface_raised_data = self
1704            .graph
1705            .determine_raised_esurfaces_from_expression(&global_cff.expression);
1706
1707        let (cut_group_data, cut_group_stats) = CutGroupData::new_from_esurface(
1708            &esurface_raised_data,
1709            &self.cut_esurface_id_map,
1710            &settings.evaluator,
1711        );
1712
1713        self.derived_data.global_cff_expression = Some(global_cff);
1714        self.derived_data.cut_group_data = cut_group_data;
1715
1716        Ok(cut_group_stats)
1717    }
1718
1719    pub(crate) fn process_valid_cuts(
1720        &self,
1721        model: &Model,
1722        process_definition: &ProcessDefinition,
1723        settings: &GenerationSettings,
1724    ) -> Result<TiVec<CutId, CrossSectionCut>> {
1725        if !self.cuts.is_empty() {
1726            return Ok(self.cuts.clone());
1727        }
1728        self.compute_process_valid_cuts(model, process_definition, settings)
1729            .map(|(_, cuts)| cuts)
1730    }
1731
1732    pub fn cutkosky_cut_count_for_process(
1733        &self,
1734        model: &Model,
1735        process_definition: &ProcessDefinition,
1736        settings: &GenerationSettings,
1737    ) -> Result<CutkoskyCutCount> {
1738        self.compute_process_valid_cuts(model, process_definition, settings)
1739            .map(|(cut_count, _)| cut_count)
1740    }
1741
1742    fn compute_process_valid_cuts(
1743        &self,
1744        model: &Model,
1745        process_definition: &ProcessDefinition,
1746        settings: &GenerationSettings,
1747    ) -> Result<(CutkoskyCutCount, TiVec<CutId, CrossSectionCut>)> {
1748        let all_st_cuts = self.graph.all_st_cuts_for_cs(
1749            self.source_nodes.clone(),
1750            self.target_nodes.clone(),
1751            &self.graph.get_initial_state_tree(),
1752        );
1753        let candidate_st_cuts = all_st_cuts.len();
1754
1755        let multi_edge_cuts: TiVec<CutId, CrossSectionCut> = all_st_cuts
1756            .into_iter()
1757            .map(|(left, cut, right)| CrossSectionCut { cut, left, right })
1758            .filter(|cut| cut.cut.nedges(&self.graph) > 1)
1759            .collect();
1760        let multi_edge_candidate_cuts = multi_edge_cuts.len();
1761
1762        let mut cuts: TiVec<CutId, CrossSectionCut> = multi_edge_cuts
1763            .into_iter()
1764            .filter_map(
1765                |cut| match cut.is_valid_for_process(self, process_definition, model) {
1766                    Ok(true) => Some(Ok(cut)),
1767                    Ok(false) => None,
1768                    Err(e) => Some(Err(e)),
1769                },
1770            )
1771            .collect::<Result<_>>()?;
1772        let process_compatible_cuts = cuts.len();
1773
1774        cuts.sort_by(|a, b| a.cut.cmp(&b.cut));
1775
1776        if !settings.force_cuts.is_empty() {
1777            let force_cuts_sorted = settings
1778                .force_cuts
1779                .clone()
1780                .into_iter()
1781                .map(|cut_edges| cut_edges.into_iter().sorted().collect_vec())
1782                .collect_vec();
1783
1784            cuts.retain(|cut| {
1785                let edges_in_cut = self
1786                    .graph
1787                    .iter_edges_of(&cut.cut)
1788                    .map(|(_, _, e)| e.data.name.value.clone())
1789                    .sorted()
1790                    .collect_vec();
1791
1792                force_cuts_sorted.contains(&edges_in_cut)
1793            });
1794        }
1795
1796        let cut_count = CutkoskyCutCount {
1797            candidate_st_cuts,
1798            multi_edge_candidate_cuts,
1799            process_compatible_cuts,
1800            selected_cuts: cuts.len(),
1801        };
1802        Ok((cut_count, cuts))
1803    }
1804
1805    fn generate_cuts(
1806        &mut self,
1807        model: &Model,
1808        process_definition: &ProcessDefinition,
1809        settings: &GenerationSettings,
1810    ) -> Result<()> {
1811        debug_tags!(#generation, #profile, #graph;
1812            stage = "cross_section_generate_cuts_start",
1813            graph = %self.graph.name,
1814            "Cut discovery timing milestone"
1815        );
1816        let started = std::time::Instant::now();
1817        let (cut_count, cuts) =
1818            self.compute_process_valid_cuts(model, process_definition, settings)?;
1819        self.cuts = cuts;
1820        debug_tags!(#generation, #profile, #graph;
1821            stage = "cross_section_generate_cuts_done",
1822            graph = %self.graph.name,
1823            st_cut_count = cut_count.candidate_st_cuts,
1824            valid_cut_count = self.cuts.len(),
1825            elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1826            "Cut discovery timing milestone"
1827        );
1828        generation_progress::cuts_discovered(
1829            "",
1830            &self.graph.name,
1831            cut_count.candidate_st_cuts,
1832            self.cuts.len(),
1833        );
1834
1835        Ok(())
1836    }
1837
1838    fn generate_esurface_cuts(&mut self) {
1839        debug!("generating esurfaces for cuts");
1840
1841        let esurfaces: TiVec<CutId, Esurface> = self
1842            .cuts
1843            .iter()
1844            .map(|cut| {
1845                Esurface::new_from_cut_left(
1846                    &self.graph.underlying,
1847                    cut,
1848                    Some(&self.graph.initial_state_cut),
1849                )
1850            })
1851            .collect();
1852
1853        debug!("generated esurfaces {:?}", esurfaces);
1854
1855        self.cut_esurface = esurfaces;
1856    }
1857
1858    pub(crate) fn build_parametric_integrand(
1859        &mut self,
1860        settings: &GenerationSettings,
1861        vakint: &Vakint,
1862        cff_energy_degree_bound_reports: &Mutex<Vec<CffEnergyDegreeBoundReport>>,
1863    ) -> Result<()> {
1864        self.derived_data.cut_paramatric_integrand =
1865            self.build_integrand(settings, vakint, cff_energy_degree_bound_reports)?;
1866        Ok(())
1867    }
1868
1869    fn build_integrand(
1870        &mut self,
1871        settings: &GenerationSettings,
1872        vakint: &Vakint,
1873        cff_energy_degree_bound_reports: &Mutex<Vec<CffEnergyDegreeBoundReport>>,
1874    ) -> Result<TiVec<CutGroupId, ParametricIntegrands>> {
1875        let started = std::time::Instant::now();
1876        crate::debug_tags!(#generation, #profile, #uv, #graph, #summary;
1877            stage = "supergraph_build_integrand_start",
1878            graph = %self.graph.name,
1879            subtract_uv = settings.uv.subtract_uv,
1880            generate_integrated = settings.uv.generate_integrated,
1881            "Generation timing milestone"
1882        );
1883        let max_order = self
1884            .derived_data
1885            .cut_group_data
1886            .cut_groups
1887            .iter()
1888            .map(|cut_group| cut_group.related_esurface_group.max_occurence)
1889            .max()
1890            .unwrap();
1891
1892        self.graph.param_builder.initialize_duals(max_order);
1893        crate::debug_tags!(#generation, #profile, #graph, #summary;
1894            stage = "supergraph_initialize_duals_done",
1895            graph = %self.graph.name,
1896            max_order,
1897            elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1898            "Generation timing milestone"
1899        );
1900
1901        let cuts = self
1902            .derived_data
1903            .cut_group_data
1904            .cut_groups
1905            .iter()
1906            .map(|cuts| CutSet {
1907                residue_selector: ResidueSelector {
1908                    lu: Some(cuts.lu_cut_selection(&self.graph, &self.cuts)),
1909                    left_th_cut: None,
1910                    right_th_cut: None,
1911                },
1912                union: cuts
1913                    .cuts
1914                    .iter()
1915                    .map(|cut_id| self.cuts[*cut_id].cut.as_subgraph())
1916                    .reduce(|cut_1, cut_2| cut_1.union(&cut_2))
1917                    .unwrap_or_else(|| self.graph.empty_subgraph()),
1918                canonicalize_external_shifts: false,
1919            })
1920            .collect();
1921
1922        let cut_structure = CutStructure { cuts };
1923        crate::debug_tags!(#generation, #profile, #uv, #graph, #summary;
1924            stage = "supergraph_cutsets_done",
1925            graph = %self.graph.name,
1926            cut_count = cut_structure.cuts.len(),
1927            elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1928            "Generation timing milestone"
1929        );
1930
1931        let cff_options = self.graph.production_cff_3d_expression_options(settings)?;
1932        let production_expression = self
1933            .derived_data
1934            .global_cff_expression
1935            .as_ref()
1936            .expect("global_cff_expression should have been created");
1937
1938        let orchestration_started = std::time::Instant::now();
1939        let parametric_integrands = settings.uv.orchestrator.parametric_integrands(
1940            &mut self.graph,
1941            cut_structure,
1942            vakint,
1943            OrientationProjection::exact_expression(
1944                production_expression,
1945                &cff_options,
1946                &settings.orientation_pattern,
1947                settings.explicit_orientation_sum_only,
1948            )
1949            .with_vacuum_subtraction(settings.medium.vacuum_subtraction)
1950            .with_energy_degree_bound_reports(cff_energy_degree_bound_reports),
1951            settings,
1952        )?;
1953        crate::debug_tags!(#generation, #profile, #uv, #graph, #summary;
1954            stage = "supergraph_parametric_orchestration_done",
1955            graph = %self.graph.name,
1956            parametric_integrand_count = parametric_integrands.len(),
1957            elapsed_ms = orchestration_started.elapsed().as_secs_f64() * 1000.0,
1958            total_elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1959            "Generation timing milestone"
1960        );
1961
1962        let finalize_started = std::time::Instant::now();
1963        let result = parametric_integrands
1964            .into_iter()
1965            .enumerate()
1966            .map(|(index, integrand)| {
1967                let group = &self.derived_data.cut_group_data.cut_groups[CutGroupId(index)];
1968                let lu_prefactor = group.lu_prefactor(&self.graph, &self.cuts)?;
1969                Ok(integrand.map(|a| a * &lu_prefactor))
1970            })
1971            .collect::<Result<_>>()?;
1972        crate::debug_tags!(#generation, #profile, #uv, #graph, #summary;
1973            stage = "supergraph_build_integrand_done",
1974            graph = %self.graph.name,
1975            elapsed_ms = finalize_started.elapsed().as_secs_f64() * 1000.0,
1976            total_elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1977            "Generation timing milestone"
1978        );
1979        Ok(result)
1980    }
1981
1982    fn threshold_rstar_power(subspace_loop_count: usize, is_on_right: bool) -> Atom {
1983        let radius_star = if is_on_right {
1984            GS.radius_star_right
1985        } else {
1986            GS.radius_star_left
1987        };
1988        Atom::var(radius_star).pow(subspace_loop_count as i32 * 3 - 1)
1989    }
1990
1991    pub(crate) fn single_th_prefactor_helper_atoms(
1992        order: u8,
1993        subspace_loop_count: usize,
1994        is_on_right: bool,
1995        include_integrated: bool,
1996    ) -> (SingleThresholdPieces<Atom>, Atom) {
1997        let loop_3 = subspace_loop_count as i64 * 3;
1998
1999        let i = Atom::i();
2000
2001        let radius = if is_on_right {
2002            Atom::var(GS.radius_right)
2003        } else {
2004            Atom::var(GS.radius_left)
2005        };
2006
2007        let radius_star = if is_on_right {
2008            Atom::var(GS.radius_star_right)
2009        } else {
2010            Atom::var(GS.radius_star_left)
2011        };
2012        let uv_damp_plus = if is_on_right {
2013            Atom::var(GS.uv_damp_plus_right)
2014        } else {
2015            Atom::var(GS.uv_damp_plus_left)
2016        };
2017        let uv_damp_minus = if is_on_right {
2018            Atom::var(GS.uv_damp_minus_right)
2019        } else {
2020            Atom::var(GS.uv_damp_minus_left)
2021        };
2022        let hfunction = if is_on_right {
2023            Atom::var(GS.hfunction_right_th)
2024        } else {
2025            Atom::var(GS.hfunction_left_th)
2026        };
2027
2028        let laurent_coeff_indices = (1..=order).map(|i| -(i as i8));
2029
2030        let mut laurent_coeffs = laurent_coeff_indices.map(|laurent_coeff_index| {
2031            build_derivative_structure_atom(order, laurent_coeff_index)
2032                .replace(GS.rescale_star)
2033                .with(radius_star.clone())
2034        });
2035
2036        let delta_r_plus = &radius - &radius_star;
2037        let delta_r_minus = -&radius - &radius_star;
2038
2039        let jacobian_ratio = (Atom::one() / &radius).pow(loop_3 - 1);
2040
2041        let local_prefactor =
2042            &jacobian_ratio * (uv_damp_plus / &delta_r_plus + uv_damp_minus / &delta_r_minus);
2043
2044        let integrated_prefactor = if include_integrated {
2045            // Runtime subtracts this helper. The left causal surface therefore
2046            // needs -i*pi here to restore +i*pi*delta(eta), and the right its conjugate.
2047            if is_on_right {
2048                i * Atom::var(GS.pi) * &jacobian_ratio * hfunction
2049            } else {
2050                -i * Atom::var(GS.pi) * &jacobian_ratio * hfunction
2051            }
2052        } else {
2053            Atom::zero()
2054        };
2055
2056        let leading_laurent_coeff = laurent_coeffs.next().unwrap();
2057        // Keep the no-directive helper structurally identical to the historical single-output
2058        // lane. Evaluating the split pieces and adding them later changes f64 rounding.
2059        let mut legacy = (local_prefactor.clone() + integrated_prefactor.clone())
2060            * leading_laurent_coeff.clone();
2061        let mut local = local_prefactor * &leading_laurent_coeff;
2062        let integrated = integrated_prefactor * leading_laurent_coeff;
2063
2064        for pow in 2..=order {
2065            let raised = laurent_coeffs.next().unwrap()
2066                * &jacobian_ratio
2067                * (Atom::one() / delta_r_plus.pow(pow as i64)
2068                    + Atom::one() / delta_r_minus.pow(pow as i64));
2069            legacy += raised.clone();
2070            local += raised;
2071        }
2072
2073        debug!(
2074            "Threshold counterterm helper atoms for order {} and loop number {}: local={}, integrated={}, legacy={}",
2075            order, subspace_loop_count, local, integrated, legacy,
2076        );
2077
2078        (SingleThresholdPieces { local, integrated }, legacy)
2079    }
2080
2081    fn iterated_th_prefactor_helper_atoms(
2082        left_order: u8,
2083        right_order: u8,
2084        left_subspace_loop_count: usize,
2085        right_subspace_loop_count: usize,
2086        include_integrated: bool,
2087    ) -> (IteratedThresholdPieces<Atom>, Atom) {
2088        let (left_prefactors, left_legacy) = Self::single_th_prefactor_helper_atoms(
2089            left_order,
2090            left_subspace_loop_count,
2091            false,
2092            include_integrated,
2093        );
2094        let left_local = left_prefactors
2095            .local
2096            .replace(GS.eta)
2097            .with(Atom::var(GS.eta_left));
2098        let left_integrated = left_prefactors
2099            .integrated
2100            .replace(GS.eta)
2101            .with(Atom::var(GS.eta_left));
2102
2103        let (right_prefactors, right_legacy) = Self::single_th_prefactor_helper_atoms(
2104            right_order,
2105            right_subspace_loop_count,
2106            true,
2107            include_integrated,
2108        );
2109        let right_local = right_prefactors
2110            .local
2111            .replace(GS.eta)
2112            .with(Atom::var(GS.eta_right));
2113        let right_integrated = right_prefactors
2114            .integrated
2115            .replace(GS.eta)
2116            .with(Atom::var(GS.eta_right));
2117
2118        // The two single-side helpers start with the same generic `f` and `η` families.
2119        // Iterated subtraction evaluates one bivariate `f` and distinct left/right inverse-map
2120        // derivatives. Expansion exposes every product of univariate `f` derivatives so the
2121        // replacements below can fuse them into the corresponding bivariate derivative.
2122        let fuse = |left: Atom, right: Atom| {
2123            (left * right)
2124                .expand()
2125                .replace_multiple(Self::fuse_left_right_replacement())
2126        };
2127
2128        let pieces = IteratedThresholdPieces {
2129            local_local: fuse(left_local.clone(), right_local.clone()),
2130            local_integrated: fuse(left_local, right_integrated.clone()),
2131            integrated_local: fuse(left_integrated.clone(), right_local),
2132            integrated_integrated: fuse(left_integrated, right_integrated),
2133        };
2134        let legacy = fuse(
2135            left_legacy.replace(GS.eta).with(Atom::var(GS.eta_left)),
2136            right_legacy.replace(GS.eta).with(Atom::var(GS.eta_right)),
2137        );
2138
2139        (pieces, legacy)
2140    }
2141
2142    pub(crate) fn single_th_prefactor_helper_params(order: u8, is_on_right: bool) -> Vec<Atom> {
2143        let radius_star = if is_on_right {
2144            Atom::var(GS.radius_star_right)
2145        } else {
2146            Atom::var(GS.radius_star_left)
2147        };
2148
2149        let uv_damp_plus = if is_on_right {
2150            Atom::var(GS.uv_damp_plus_right)
2151        } else {
2152            Atom::var(GS.uv_damp_plus_left)
2153        };
2154        let uv_damp_minus = if is_on_right {
2155            Atom::var(GS.uv_damp_minus_right)
2156        } else {
2157            Atom::var(GS.uv_damp_minus_left)
2158        };
2159        let hfunction = if is_on_right {
2160            Atom::var(GS.hfunction_right_th)
2161        } else {
2162            Atom::var(GS.hfunction_left_th)
2163        };
2164
2165        let radius = if is_on_right {
2166            Atom::var(GS.radius_right)
2167        } else {
2168            Atom::var(GS.radius_left)
2169        };
2170
2171        let mut params = params_for_derivative_order(order)
2172            .into_iter()
2173            .map(|param| param.replace(GS.rescale_star).with(radius_star.clone()))
2174            .collect_vec();
2175
2176        params.push(radius);
2177        params.push(radius_star);
2178        params.push(uv_damp_plus);
2179        params.push(uv_damp_minus);
2180        params.push(hfunction);
2181        params
2182    }
2183
2184    pub(crate) fn iterated_th_prefactor_helper_params(
2185        left_order: u8,
2186        right_order: u8,
2187    ) -> Vec<Atom> {
2188        let mut iterated_params = params_for_iterated_threshold_ct(left_order, right_order);
2189        let left_radius_star = Atom::var(GS.radius_star_left);
2190        let right_radius_star = Atom::var(GS.radius_star_right);
2191        let left_radius = Atom::var(GS.radius_left);
2192        let right_radius = Atom::var(GS.radius_right);
2193        let left_uv_damp_plus = Atom::var(GS.uv_damp_plus_left);
2194        let left_uv_damp_minus = Atom::var(GS.uv_damp_minus_left);
2195        let right_uv_damp_plus = Atom::var(GS.uv_damp_plus_right);
2196        let right_uv_damp_minus = Atom::var(GS.uv_damp_minus_right);
2197        let left_hfunction = Atom::var(GS.hfunction_left_th);
2198        let right_hfunction = Atom::var(GS.hfunction_right_th);
2199
2200        iterated_params.push(left_radius);
2201        iterated_params.push(left_radius_star);
2202        iterated_params.push(left_uv_damp_plus);
2203        iterated_params.push(left_uv_damp_minus);
2204        iterated_params.push(left_hfunction);
2205        iterated_params.push(right_radius);
2206        iterated_params.push(right_radius_star);
2207        iterated_params.push(right_uv_damp_plus);
2208        iterated_params.push(right_uv_damp_minus);
2209        iterated_params.push(right_hfunction);
2210        iterated_params
2211    }
2212
2213    #[allow(clippy::too_many_arguments)]
2214    pub(crate) fn single_th_helper(
2215        &self,
2216        order: u8,
2217        subspace_loop_count: usize,
2218        is_on_right: bool,
2219        include_integrated: bool,
2220        outputs: LUThresholdHelperOutputs,
2221        dual_shape: Option<Vec<Vec<usize>>>,
2222        optimization_settings: OptimizationSettings,
2223        evaluator_settings: &EvaluatorSettings,
2224    ) -> Result<GenericEvaluator> {
2225        let (pieces, legacy) = Self::single_th_prefactor_helper_atoms(
2226            order,
2227            subspace_loop_count,
2228            is_on_right,
2229            include_integrated,
2230        );
2231        let atoms = match outputs {
2232            LUThresholdHelperOutputs::Legacy => vec![legacy],
2233            LUThresholdHelperOutputs::Pieces => vec![pieces.local, pieces.integrated],
2234            LUThresholdHelperOutputs::LegacyAndPieces => {
2235                vec![legacy, pieces.local, pieces.integrated]
2236            }
2237        };
2238        let params = Self::single_th_prefactor_helper_params(order, is_on_right);
2239
2240        let fn_map = FunctionMap::new();
2241
2242        let evaluator = GenericEvaluator::new_from_raw_params(
2243            atoms,
2244            &params,
2245            &fn_map,
2246            vec![],
2247            optimization_settings,
2248            dual_shape.map(|shape| (shape, Vec::new())),
2249            evaluator_settings,
2250        )?
2251        .into_eager_only();
2252
2253        Ok(evaluator)
2254    }
2255
2256    #[allow(clippy::too_many_arguments)]
2257    pub(crate) fn iterated_th_helper(
2258        &self,
2259        left_order: u8,
2260        right_order: u8,
2261        left_subspace_loop_count: usize,
2262        right_subspace_loop_count: usize,
2263        include_integrated: bool,
2264        outputs: LUThresholdHelperOutputs,
2265        dual_shape: Option<Vec<Vec<usize>>>,
2266        optimization_settings: OptimizationSettings,
2267        evaluator_settings: &EvaluatorSettings,
2268    ) -> Result<GenericEvaluator> {
2269        let (pieces, legacy) = Self::iterated_th_prefactor_helper_atoms(
2270            left_order,
2271            right_order,
2272            left_subspace_loop_count,
2273            right_subspace_loop_count,
2274            include_integrated,
2275        );
2276        let atoms = match outputs {
2277            LUThresholdHelperOutputs::Legacy => vec![legacy],
2278            LUThresholdHelperOutputs::Pieces => vec![
2279                pieces.local_local,
2280                pieces.local_integrated,
2281                pieces.integrated_local,
2282                pieces.integrated_integrated,
2283            ],
2284            LUThresholdHelperOutputs::LegacyAndPieces => vec![
2285                legacy,
2286                pieces.local_local,
2287                pieces.local_integrated,
2288                pieces.integrated_local,
2289                pieces.integrated_integrated,
2290            ],
2291        };
2292
2293        let params = Self::iterated_th_prefactor_helper_params(left_order, right_order);
2294
2295        let fn_map = FunctionMap::new();
2296
2297        let evaluator = GenericEvaluator::new_from_raw_params(
2298            atoms,
2299            &params,
2300            &fn_map,
2301            vec![],
2302            optimization_settings,
2303            dual_shape.map(|shape| (shape, Vec::new())),
2304            evaluator_settings,
2305        )?
2306        .into_eager_only();
2307
2308        Ok(evaluator)
2309    }
2310
2311    fn fuse_left_right_replacement() -> Vec<Replacement> {
2312        let f = symbol!("f");
2313
2314        vec![
2315            Replacement::new(
2316                (function!(f, GS.radius_star_left) * function!(f, GS.radius_star_right))
2317                    .to_pattern(),
2318                function!(f, GS.radius_star_left, GS.radius_star_right),
2319            ),
2320            Replacement::new(
2321                (function!(f, GS.radius_star_left)
2322                    * function!(Symbol::DERIVATIVE, W_.x_, f, GS.radius_star_right))
2323                .to_pattern(),
2324                function!(
2325                    Symbol::DERIVATIVE,
2326                    0,
2327                    W_.x_,
2328                    f,
2329                    GS.radius_star_left,
2330                    GS.radius_star_right
2331                ),
2332            ),
2333            Replacement::new(
2334                (function!(Symbol::DERIVATIVE, W_.x_, f, GS.radius_star_left)
2335                    * function!(f, GS.radius_star_right))
2336                .to_pattern(),
2337                function!(
2338                    Symbol::DERIVATIVE,
2339                    W_.x_,
2340                    0,
2341                    f,
2342                    GS.radius_star_left,
2343                    GS.radius_star_right
2344                ),
2345            ),
2346            Replacement::new(
2347                (function!(Symbol::DERIVATIVE, W_.x_, f, GS.radius_star_left)
2348                    * function!(Symbol::DERIVATIVE, W_.y_, f, GS.radius_star_right))
2349                .to_pattern(),
2350                function!(
2351                    Symbol::DERIVATIVE,
2352                    W_.x_,
2353                    W_.y_,
2354                    f,
2355                    GS.radius_star_left,
2356                    GS.radius_star_right
2357                ),
2358            ),
2359        ]
2360    }
2361    //fn th_prefactor_helper(
2362    //    &self,
2363    //    subspace_loop_count: usize,
2364    //    is_on_right: bool,
2365    //    include_integrated: bool,
2366    //) -> Atom {
2367    //    let radius = if is_on_right {
2368    //        Atom::var(GS.radius_right)
2369    //    } else {
2370    //        Atom::var(GS.radius_left)
2371    //    };
2372
2373    //    let radius_star = if is_on_right {
2374    //        Atom::var(GS.radius_star_right)
2375    //    } else {
2376    //        Atom::var(GS.radius_star_left)
2377    //    };
2378
2379    //    let grad_eta = if is_on_right {
2380    //        Atom::var(GS.deta_right_th)
2381    //    } else {
2382    //        Atom::var(GS.deta_left_th)
2383    //    };
2384
2385    //    let uv_damp_plus = if is_on_right {
2386    //        Atom::var(GS.uv_damp_plus_right)
2387    //    } else {
2388    //        Atom::var(GS.uv_damp_plus_left)
2389    //    };
2390
2391    //    let uv_damp_minus = if is_on_right {
2392    //        Atom::var(GS.uv_damp_minus_right)
2393    //    } else {
2394    //        Atom::var(GS.uv_damp_minus_left)
2395    //    };
2396
2397    //    let h_function = if is_on_right {
2398    //        Atom::var(GS.hfunction_right_th)
2399    //    } else {
2400    //        Atom::var(GS.hfunction_left_th)
2401    //    };
2402
2403    //    let i = if is_on_right { Atom::i() } else { -Atom::i() };
2404    //    let pi = Atom::var(GS.pi);
2405
2406    //    let jacobian_ratio = (&radius_star / &radius).pow(subspace_loop_count as i64 * 3 - 1);
2407
2408    //    let local_prefactor = (&uv_damp_plus / (&radius - &radius_star)
2409    //        + &uv_damp_minus / (-&radius - &radius_star))
2410    //        / &grad_eta
2411    //        * &jacobian_ratio;
2412
2413    //    let integrated_prefactor = if include_integrated {
2414    //        &h_function * &i * &pi * &jacobian_ratio / &grad_eta
2415    //    } else {
2416    //        Atom::new()
2417    //    };
2418
2419    //    debug!(
2420    //        "th prefactor local: {}, integrated: {}",
2421    //        local_prefactor, integrated_prefactor
2422    //    );
2423
2424    //    local_prefactor + integrated_prefactor
2425    //}
2426
2427    fn build_lmbs(&mut self) -> Result<()> {
2428        let mut lmbs: TiVec<LmbIndex, LoopMomentumBasis> = vec![].into();
2429
2430        let externals: SuBitGraph = self.graph.empty_subgraph();
2431        let full_filter = self.graph.full_filter();
2432        let cut_graph = full_filter.subtract(&self.graph.initial_state_cut.right);
2433
2434        for s in self.graph.all_spanning_forests_of(&cut_graph) {
2435            let mut lmb = self.graph.lmb_impl(&full_filter, &s, externals.clone())?;
2436            let mut exts = vec![];
2437
2438            for i in lmb.loop_edges.iter() {
2439                let (_, p) = &self.graph[i];
2440
2441                if self.graph.initial_state_cut.intersects(p) {
2442                    exts.push(*i);
2443                }
2444            }
2445
2446            exts.sort();
2447
2448            for e in exts {
2449                let mut loopid = None;
2450                for (l, s) in lmb.edge_signatures[e].internal.iter_enumerated() {
2451                    if s.is_non_zero() {
2452                        if loopid.is_none() {
2453                            loopid = Some(l);
2454                        } else {
2455                            panic!("external edge has multiple loop momenta")
2456                        }
2457                    }
2458                }
2459                lmb.put_loop_to_ext(loopid.unwrap());
2460            }
2461            let external_momentum_edge_order = self.graph.external_momentum_edge_order();
2462            lmb.canonicalize_external_order(&external_momentum_edge_order);
2463            //lmbs.push(self.graph.lmb_impl(&full_filter, &s, externals.clone()));
2464            lmbs.push(lmb);
2465        }
2466
2467        let sorted_graph_loop_edges = self
2468            .graph
2469            .loop_momentum_basis
2470            .loop_edges
2471            .iter()
2472            .copied()
2473            .sorted()
2474            .collect_vec();
2475
2476        let matching_lmb_index = lmbs.iter_enumerated().find_map(|(lmb_index, lmb)| {
2477            let sorted_loop_edges = lmb.loop_edges.iter().copied().sorted().collect_vec();
2478            (sorted_loop_edges == sorted_graph_loop_edges).then_some(lmb_index)
2479        });
2480
2481        if let Some(matching_lmb_index) = matching_lmb_index {
2482            {
2483                let matching_lmb = &mut lmbs[matching_lmb_index];
2484                for (target_pos, target_edge) in
2485                    self.graph.loop_momentum_basis.loop_edges.iter_enumerated()
2486                {
2487                    let current_pos = matching_lmb
2488                        .loop_edges
2489                        .iter()
2490                        .find_position(|edge| *edge == target_edge)
2491                        .map(|(pos, _)| pos)
2492                        .unwrap();
2493
2494                    if current_pos != target_pos.0 {
2495                        matching_lmb.swap_loops(current_pos.into(), target_pos);
2496                    }
2497                }
2498            }
2499
2500            let matching_lmb_pos: usize = matching_lmb_index.into();
2501            if matching_lmb_pos != 0 {
2502                lmbs.raw[..=matching_lmb_pos].rotate_right(1);
2503            }
2504        } else {
2505            warn!(
2506                "Could not match current graph LMB against generated LMBs for graph {}",
2507                self.graph.name
2508            );
2509        }
2510
2511        let sorted_graph_loop_edges = self
2512            .graph
2513            .loop_momentum_basis
2514            .loop_edges
2515            .iter()
2516            .copied()
2517            .sorted()
2518            .collect_vec();
2519
2520        let matching_lmb_index = lmbs.iter_enumerated().find_map(|(lmb_index, lmb)| {
2521            let sorted_loop_edges = lmb.loop_edges.iter().copied().sorted().collect_vec();
2522            (sorted_loop_edges == sorted_graph_loop_edges).then_some(lmb_index)
2523        });
2524
2525        if let Some(matching_lmb_index) = matching_lmb_index {
2526            {
2527                let matching_lmb = &mut lmbs[matching_lmb_index];
2528                for (target_pos, target_edge) in
2529                    self.graph.loop_momentum_basis.loop_edges.iter_enumerated()
2530                {
2531                    let current_pos = matching_lmb
2532                        .loop_edges
2533                        .iter()
2534                        .find_position(|edge| *edge == target_edge)
2535                        .map(|(pos, _)| pos)
2536                        .unwrap();
2537
2538                    if current_pos != target_pos.0 {
2539                        matching_lmb.swap_loops(current_pos.into(), target_pos);
2540                    }
2541                }
2542            }
2543
2544            let matching_lmb_pos: usize = matching_lmb_index.into();
2545            if matching_lmb_pos != 0 {
2546                lmbs.raw[..=matching_lmb_pos].rotate_right(1);
2547            }
2548        } else {
2549            warn!(
2550                "Could not match current graph LMB against generated LMBs for graph {}",
2551                self.graph.name
2552            );
2553        }
2554
2555        self.derived_data.lmbs = Some(lmbs);
2556        Ok(())
2557    }
2558
2559    fn build_multi_channeling_channels(&mut self, override_lmb_heuristics: bool) -> Result<()> {
2560        let lmbs = self.derived_data.lmbs.as_ref().unwrap();
2561        let channels = if override_lmb_heuristics {
2562            self.graph
2563                .build_multi_channeling_channels(lmbs, override_lmb_heuristics)
2564        } else {
2565            self.build_cross_section_multi_channeling_channels(lmbs)?
2566        };
2567
2568        self.derived_data.multi_channeling_setup = Some(channels);
2569        Ok(())
2570    }
2571
2572    fn build_cross_section_multi_channeling_channels(
2573        &self,
2574        lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
2575    ) -> Result<LmbMultiChannelingSetup> {
2576        let channels = self.select_cross_section_lmb_channel_indices(lmbs)?;
2577        debug!(
2578            "number of lmbs: {}, number of cross-section channels: {}",
2579            lmbs.len(),
2580            channels.len()
2581        );
2582        Ok(LmbMultiChannelingSetup {
2583            master_edge_masses: Default::default(),
2584            sampling_bridge: Default::default(),
2585            sampling_bridge_quad: Default::default(),
2586            sampling_bridge_fixed256: Default::default(),
2587            sampling_bridge_arb: Default::default(),
2588            sampling_source: Default::default(),
2589            sampling_catalogue: Default::default(),
2590            sampling_programs: Default::default(),
2591            lmb_basis_ids: channels,
2592            graph: self.graph.clone(),
2593            all_bases: lmbs.clone(),
2594        })
2595    }
2596
2597    fn select_cross_section_lmb_channel_indices(
2598        &self,
2599        lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
2600    ) -> Result<TiVec<crate::integrands::process::SamplingChannelId, LmbIndex>> {
2601        if self.cuts.is_empty() {
2602            return Ok(self.graph.select_amplitude_lmb_channel_indices(
2603                lmbs,
2604                false,
2605                LmbChannelFallback::CurrentGraphBasis,
2606            ));
2607        }
2608
2609        let mut lmb_index_by_loop_edges = BTreeMap::<Vec<EdgeIndex>, LmbIndex>::new();
2610        for (lmb_index, lmb) in lmbs.iter_enumerated() {
2611            lmb_index_by_loop_edges
2612                .entry(lmb.loop_edges.iter().copied().sorted().collect())
2613                .or_insert(lmb_index);
2614        }
2615
2616        let mut selected_loop_edge_sets = BTreeSet::<Vec<EdgeIndex>>::new();
2617        for cut in &self.cuts {
2618            let cut_edges = cut.cut.as_subgraph();
2619            let cut_edge_ids = self
2620                .graph
2621                .underlying
2622                .iter_edges_of(&cut_edges)
2623                .map(|(_, edge_id, _)| edge_id)
2624                .sorted()
2625                .collect_vec();
2626            let Some(excluded_cut_edge) = self.excluded_cut_edge_for_lmb_channel(&cut_edge_ids)
2627            else {
2628                continue;
2629            };
2630
2631            let fixed_cut_edges = cut_edge_ids
2632                .iter()
2633                .copied()
2634                .filter(|edge_id| *edge_id != excluded_cut_edge)
2635                .collect::<BTreeSet<_>>();
2636            let left_basis_edge_sets =
2637                self.selected_cut_side_lmb_edge_sets(cut.left.subtract(&cut_edges))?;
2638            let right_basis_edge_sets =
2639                self.selected_cut_side_lmb_edge_sets(cut.right.subtract(&cut_edges))?;
2640
2641            for left_basis_edges in &left_basis_edge_sets {
2642                for right_basis_edges in &right_basis_edge_sets {
2643                    let loop_edges = fixed_cut_edges
2644                        .iter()
2645                        .copied()
2646                        .chain(left_basis_edges.iter().copied())
2647                        .chain(right_basis_edges.iter().copied())
2648                        .collect::<BTreeSet<_>>()
2649                        .into_iter()
2650                        .collect_vec();
2651                    selected_loop_edge_sets.insert(loop_edges);
2652                }
2653            }
2654        }
2655
2656        let mut channels = Vec::<LmbIndex>::new();
2657        for loop_edges in selected_loop_edge_sets {
2658            let lmb_index = lmb_index_by_loop_edges.get(&loop_edges).ok_or_else(|| {
2659                eyre!(
2660                    "Could not find a generated cross-section LMB with loop edges [{}] for graph '{}'.",
2661                    loop_edges.iter().map(|edge| edge.to_string()).join(", "),
2662                    self.graph.name
2663                )
2664            })?;
2665            channels.push(*lmb_index);
2666        }
2667
2668        if channels.is_empty() {
2669            Ok(self.graph.select_amplitude_lmb_channel_indices(
2670                lmbs,
2671                false,
2672                LmbChannelFallback::CurrentGraphBasis,
2673            ))
2674        } else {
2675            Ok(channels.into_iter().sorted().dedup().collect())
2676        }
2677    }
2678
2679    fn excluded_cut_edge_for_lmb_channel(&self, cut_edge_ids: &[EdgeIndex]) -> Option<EdgeIndex> {
2680        Self::excluded_cut_edge_for_lmb_channel_in(&self.graph, cut_edge_ids)
2681    }
2682
2683    fn excluded_cut_edge_for_lmb_channel_in(
2684        graph: &Graph,
2685        cut_edge_ids: &[EdgeIndex],
2686    ) -> Option<EdgeIndex> {
2687        cut_edge_ids
2688            .iter()
2689            .copied()
2690            .filter(|edge_id| graph[*edge_id].particle.is_massive())
2691            .min()
2692            .or_else(|| {
2693                cut_edge_ids
2694                    .iter()
2695                    .copied()
2696                    .filter(|edge_id| graph[*edge_id].particle.is_fermion())
2697                    .min()
2698            })
2699            .or_else(|| cut_edge_ids.iter().copied().min())
2700    }
2701
2702    fn selected_cut_side_lmb_edge_sets(
2703        &self,
2704        mut side_subgraph: SuBitGraph,
2705    ) -> Result<Vec<Vec<EdgeIndex>>> {
2706        for (pair, _, edge) in self.graph.underlying.iter_edges() {
2707            if edge.data.is_dummy {
2708                side_subgraph.sub(pair);
2709            }
2710        }
2711
2712        if self.graph.underlying.cyclotomatic_number(&side_subgraph) == 0 {
2713            return Ok(vec![Vec::new()]);
2714        }
2715
2716        let side_lmbs = self.graph.generate_loop_momentum_bases_of(&side_subgraph);
2717        if side_lmbs.is_empty() {
2718            return Err(eyre!(
2719                "Could not generate cut-side LMBs for a non-tree side of graph '{}'.",
2720                self.graph.name
2721            ));
2722        }
2723
2724        let selected_side_lmbs = self.graph.select_amplitude_lmb_channel_indices(
2725            &side_lmbs,
2726            false,
2727            LmbChannelFallback::FirstBasis,
2728        );
2729        let mut edge_sets = BTreeSet::<Vec<EdgeIndex>>::new();
2730        for lmb_index in selected_side_lmbs {
2731            edge_sets.insert(
2732                side_lmbs[lmb_index]
2733                    .loop_edges
2734                    .iter()
2735                    .copied()
2736                    .sorted()
2737                    .collect(),
2738            );
2739        }
2740
2741        if edge_sets.is_empty() {
2742            Ok(vec![Vec::new()])
2743        } else {
2744            Ok(edge_sets.into_iter().collect())
2745        }
2746    }
2747
2748    fn threshold_counterterm_association(
2749        &self,
2750        threshold_candidate: &TopologicalThresholdCandidate,
2751        sandwich: &SuBitGraph,
2752        cut: &OrientedCut,
2753    ) -> ThresholdCountertermAssociation {
2754        let boundary_edges = |boundary: &OrientedCut| {
2755            let boundary_filter = boundary.left.union(&boundary.right).intersection(sandwich);
2756            let edges = self
2757                .graph
2758                .iter_edges_of(&boundary_filter)
2759                .map(|(_, edge_id, _)| edge_id)
2760                .sorted()
2761                .collect();
2762            let has_left_orientation =
2763                boundary_filter.intersection(&boundary.left).n_included() > 0;
2764            let has_right_orientation =
2765                boundary_filter.intersection(&boundary.right).n_included() > 0;
2766            (edges, has_left_orientation != has_right_orientation)
2767        };
2768
2769        let (cut_boundary_edges, cut_bound_is_applicable) = boundary_edges(cut);
2770        let (threshold_boundary_edges, threshold_bound_is_applicable) =
2771            boundary_edges(&threshold_candidate.cut);
2772
2773        ThresholdCountertermAssociation {
2774            topological_threshold_id: threshold_candidate.topological_threshold_id,
2775            esurface_id: threshold_candidate
2776                .selected_orientation_esurface_id
2777                .expect("only selected-orientation threshold instances have associations"),
2778
2779            cut_boundary_edges,
2780            threshold_boundary_edges,
2781            invariant_bound_is_applicable: cut_bound_is_applicable && threshold_bound_is_applicable,
2782        }
2783    }
2784
2785    #[allow(clippy::too_many_arguments)]
2786    fn classify_threshold_counterterm_association(
2787        &self,
2788        association: &ThresholdCountertermAssociation,
2789        cut_id: CutId,
2790        cut: &CrossSectionCut,
2791        subspace: &SubspaceData,
2792        all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
2793        model: &Model,
2794        runtime_settings: &RuntimeSettings,
2795        settings: &GenerationSettings,
2796    ) -> ThresholdCountertermStatus {
2797        let esurface = &self.derived_data.topological_threshold_esurfaces
2798            [association.topological_threshold_id];
2799        if !esurface.has_radial_dependence_in_subspace(subspace, all_lmbs, &self.graph) {
2800            debug!(
2801                "Skipping graph '{}' cut {} threshold E-surface {} after cut-relative classification {:?}",
2802                self.graph.name,
2803                cut_id.0,
2804                association.topological_threshold_id.0,
2805                ThresholdCountertermStatus::NoRadialDependence,
2806            );
2807            return ThresholdCountertermStatus::NoRadialDependence;
2808        }
2809
2810        let structural_status = if association.invariant_bound_is_applicable {
2811            self.graph.classify_threshold_pinch(
2812                &association.cut_boundary_edges,
2813                &association.threshold_boundary_edges,
2814            )
2815        } else {
2816            ThresholdPinchStatus::NotProven
2817        };
2818        if structural_status == ThresholdPinchStatus::Always {
2819            debug!(
2820                "Skipping graph '{}' cut {} threshold E-surface {} after cut-relative classification {:?}",
2821                self.graph.name,
2822                cut_id.0,
2823                association.topological_threshold_id.0,
2824                ThresholdCountertermStatus::AlwaysPinched,
2825            );
2826            return ThresholdCountertermStatus::AlwaysPinched;
2827        }
2828
2829        let status = if settings.threshold_subtraction.check_esurface_at_generation {
2830            association.classify_for_model(
2831                &self.graph,
2832                cut,
2833                model,
2834                &self.graph.param_builder,
2835                runtime_settings,
2836                settings.threshold_subtraction.esurface_existence_threshold,
2837            )
2838        } else {
2839            match structural_status {
2840                ThresholdPinchStatus::CanBecome => ThresholdCountertermStatus::CanBecomePinched,
2841                ThresholdPinchStatus::NotProven => ThresholdCountertermStatus::PotentiallyExisting,
2842                ThresholdPinchStatus::Always => unreachable!(
2843                    "identically pinched threshold associations return before classification"
2844                ),
2845            }
2846        };
2847        if !status
2848            .is_eligible_for_generation(settings.threshold_subtraction.check_esurface_at_generation)
2849        {
2850            debug!(
2851                "Skipping graph '{}' cut {} threshold E-surface {} after cut-relative classification {:?}",
2852                self.graph.name, cut_id.0, association.topological_threshold_id.0, status,
2853            );
2854        }
2855        status
2856    }
2857
2858    fn topological_threshold_candidates(
2859        &mut self,
2860        settings: &GenerationSettings,
2861    ) -> Result<Vec<TopologicalThresholdCandidate>> {
2862        let global_cff = self
2863            .derived_data
2864            .global_cff_expression
2865            .as_ref()
2866            .expect("global_cff_expression should have been created");
2867        // CFF generation retains the full catalogue for 3D UV construction.
2868        // Only requested orientations can activate threshold-counterterm instances.
2869        let selected_orientations = global_cff
2870            .expression
2871            .orientations
2872            .iter()
2873            .filter(|orientation| settings.orientation_pattern.filter(*orientation))
2874            .collect_vec();
2875        let selected_cff_esurface_ids = selected_orientations
2876            .iter()
2877            .flat_map(|orientation| {
2878                orientation
2879                    .iter_denominator_nodes()
2880                    .filter_map(|node| match node.data {
2881                        HybridSurfaceID::Esurface(esurface_id) => Some(esurface_id),
2882                        _ => None,
2883                    })
2884            })
2885            .collect::<BTreeSet<_>>();
2886
2887        let mut candidates = self.graph.all_st_cuts_for_cs(
2888            self.source_nodes.clone(),
2889            self.target_nodes.clone(),
2890            &self.graph.get_initial_state_tree(),
2891        );
2892        candidates.retain(|(_left, cut, _right)| cut.nedges(&self.graph) > 1);
2893        candidates.sort_by(|a, b| a.1.cmp(&b.1));
2894
2895        let mut topological_threshold_esurfaces = TiVec::new();
2896        let candidates: Vec<Result<TopologicalThresholdCandidate>> = candidates
2897            .into_iter()
2898            .map(|(left, cut, right)| {
2899                let threshold_esurface = Esurface::new_from_cut_left(
2900                    &self.graph.underlying,
2901                    &CrossSectionCut {
2902                        cut: cut.clone(),
2903                        left: left.clone(),
2904                        right: right.clone(),
2905                    },
2906                    Some(&self.graph.initial_state_cut),
2907                );
2908                let has_selected_orientation_instance =
2909                    selected_orientations.iter().any(|orientation| {
2910                        let selected_orientation = &orientation.data.orientation;
2911                        self.graph
2912                            .iter_edges_of(&cut)
2913                            .map(|(_, edge_id, _)| edge_id)
2914                            .zip(cut.iter_edges(&self.graph).map(|(direction, _)| direction))
2915                            .all(|(edge_id, direction)| {
2916                                selected_orientation[edge_id] == direction
2917                            })
2918                    });
2919                if !has_selected_orientation_instance {
2920                    debug!(
2921                        "Keeping topology-discovered threshold surface {:?} dormant in graph '{}': no selected-orientation instance occurs",
2922                        threshold_esurface.energies, self.graph.name,
2923                    );
2924                    let topological_threshold_id = TopologicalThresholdId(
2925                        topological_threshold_esurfaces.len(),
2926                    );
2927                    topological_threshold_esurfaces.push(threshold_esurface.clone());
2928                    return Ok(TopologicalThresholdCandidate {
2929                        topological_threshold_id,
2930                        left,
2931                        cut,
2932                        right,
2933                        threshold_esurface,
2934                        selected_orientation_esurface_id: None,
2935                    });
2936                }
2937
2938                let esurface_id = match self
2939                    .graph
2940                    .surface_cache
2941                    .esurface_cache
2942                    .position(|esurface| esurface == &threshold_esurface)
2943                {
2944                    Some(esurface_id) => esurface_id,
2945                    None => {
2946                        return Err(eyre!(
2947                            "Topology-discovered threshold surface {:?} has a selected-orientation instance but is missing from graph '{}' selected CFF surface cache",
2948                            threshold_esurface.energies,
2949                            self.graph.name,
2950                        ));
2951                    }
2952                };
2953                if !selected_cff_esurface_ids.contains(&esurface_id) {
2954                    return Err(eyre!(
2955                        "Topology-discovered threshold surface {:?} has a selected-orientation instance in graph '{}' but E-surface {} does not occur in its selected CFF expression",
2956                        threshold_esurface.energies,
2957                        self.graph.name,
2958                        esurface_id.0,
2959                    ));
2960                }
2961
2962                let topological_threshold_id = TopologicalThresholdId(
2963                    topological_threshold_esurfaces.len(),
2964                );
2965                topological_threshold_esurfaces.push(threshold_esurface.clone());
2966                Ok(TopologicalThresholdCandidate {
2967                    topological_threshold_id,
2968                    left,
2969                    cut,
2970                    right,
2971                    threshold_esurface,
2972                    selected_orientation_esurface_id: Some(esurface_id),
2973                })
2974
2975            })
2976            .collect();
2977        let candidates = candidates.into_iter().collect::<Result<Vec<_>>>()?;
2978        self.derived_data.topological_threshold_esurfaces = topological_threshold_esurfaces;
2979        Ok(candidates)
2980    }
2981
2982    fn physical_cut_edges(&self, cut_id: CutId) -> Vec<EdgeIndex> {
2983        self.graph
2984            .iter_edges_of(&self.cuts[cut_id].cut)
2985            .map(|(_, edge_id, _)| edge_id)
2986            .sorted()
2987            .collect()
2988    }
2989
2990    #[cfg(test)]
2991    fn threshold_energy_edges(&self, esurface_id: EsurfaceID) -> Vec<EdgeIndex> {
2992        self.graph.surface_cache.esurface_cache[esurface_id]
2993            .energies
2994            .iter()
2995            .copied()
2996            .sorted()
2997            .collect()
2998    }
2999
3000    fn cut_group_side_subgraphs(
3001        &self,
3002        cut_group_id: CutGroupId,
3003    ) -> Result<(SuBitGraph, SuBitGraph)> {
3004        let cut_group = &self.derived_data.cut_group_data.cut_groups[cut_group_id];
3005        let smallest_side = |side: ThresholdCountertermSide| {
3006            cut_group
3007                .cuts
3008                .iter()
3009                .map(|cut_id| match side {
3010                    ThresholdCountertermSide::Amplitude => {
3011                        unreachable!("amplitude side is not valid in cross-section resolution")
3012                    }
3013                    ThresholdCountertermSide::Left => self.cuts[*cut_id].left.clone(),
3014                    ThresholdCountertermSide::Right => self.cuts[*cut_id].right.clone(),
3015                })
3016                .min_by_key(|subgraph| subgraph.nedges(&self.graph))
3017                .ok_or_else(|| {
3018                    eyre!(
3019                        "Graph '{}' cut group {} has no {:?} subgraph",
3020                        self.graph.name,
3021                        cut_group_id.0,
3022                        side,
3023                    )
3024                })
3025        };
3026
3027        Ok((
3028            smallest_side(ThresholdCountertermSide::Left)?,
3029            smallest_side(ThresholdCountertermSide::Right)?,
3030        ))
3031    }
3032
3033    fn cut_group_compatible_parent_lmbs(&self, cut_group_id: CutGroupId) -> Result<Vec<LmbIndex>> {
3034        let representative_cut_id = self.derived_data.cut_group_data.cut_groups[cut_group_id]
3035            .cuts
3036            .first()
3037            .copied()
3038            .ok_or_else(|| {
3039                eyre!(
3040                    "Graph '{}' has an empty cut group {} while selecting threshold-counterterm parents",
3041                    self.graph.name,
3042                    cut_group_id.0,
3043                )
3044            })?;
3045        let all_lmbs = self
3046            .derived_data
3047            .lmbs
3048            .as_ref()
3049            .expect("threshold resolution requires loop-momentum bases");
3050        Ok(all_lmbs
3051            .iter_enumerated()
3052            .filter_map(|(lmb_index, lmb)| {
3053                let mut cut_edges = self
3054                    .graph
3055                    .iter_edges_of(&self.cuts[representative_cut_id].cut)
3056                    .map(|(_, edge_id, _)| edge_id)
3057                    .collect_vec();
3058                cut_edges.retain(|edge_id| !lmb.loop_edges.contains(edge_id));
3059                (cut_edges.len() == 1).then_some(lmb_index)
3060            })
3061            .collect())
3062    }
3063
3064    fn configured_threshold_variants(
3065        spec: &crate::graph::threshold_counterterms::ThresholdCountertermSpec,
3066        cut_edges: &[EdgeIndex],
3067        threshold_edges: &[EdgeIndex],
3068    ) -> Vec<(ThresholdCountertermVariant, ThresholdCountertermOrigin)> {
3069        let configured = spec
3070            .cuts
3071            .iter()
3072            .find(|cut| cut.edges == cut_edges)
3073            .and_then(|cut| {
3074                cut.thresholds
3075                    .iter()
3076                    .find(|threshold| threshold.edges == threshold_edges)
3077            })
3078            .filter(|threshold| !threshold.counterterms.is_empty());
3079
3080        match configured {
3081            Some(threshold) => threshold
3082                .counterterms
3083                .iter()
3084                .cloned()
3085                .map(|mut variant| {
3086                    if let Some(multiplier) = &mut variant.multiplier {
3087                        for (name, definition) in &spec.function_map {
3088                            multiplier
3089                                .function_map
3090                                .entry(name.clone())
3091                                .or_insert_with(|| definition.clone());
3092                        }
3093                    }
3094                    (variant, ThresholdCountertermOrigin::Explicit)
3095                })
3096                .collect(),
3097            None => vec![(
3098                ThresholdCountertermVariant {
3099                    name: Some("default".to_string()),
3100                    subspace: None,
3101                    parent_lmb: None,
3102                    disable: false,
3103                    multiplier: None,
3104                    group_id: None,
3105                },
3106                ThresholdCountertermOrigin::Autogenerated,
3107            )],
3108        }
3109    }
3110
3111    fn resolve_threshold_variant_subspace(
3112        &self,
3113        variant: &ThresholdCountertermVariant,
3114        legacy_subspace: &SubspaceData,
3115        containing_subgraph: &SuBitGraph,
3116        context: &str,
3117    ) -> Result<SubspaceData> {
3118        let Some(requested_parent_lmb) = &variant.parent_lmb else {
3119            if variant.subspace.is_some() {
3120                return Err(eyre!(
3121                    "{context} requires a full parent_lmb for its explicit subspace"
3122                ));
3123            }
3124            return Ok(legacy_subspace.clone());
3125        };
3126        let all_lmbs = self
3127            .derived_data
3128            .lmbs
3129            .as_ref()
3130            .expect("threshold resolution requires loop-momentum bases");
3131        let parent_lmb_index = all_lmbs
3132            .iter_enumerated()
3133            .find_map(|(lmb_index, lmb)| {
3134                lmb.loop_edges
3135                    .iter()
3136                    .eq(requested_parent_lmb.iter())
3137                    .then_some(lmb_index)
3138            })
3139            .ok_or_else(|| {
3140                eyre!(
3141                    "{context} requests parent_lmb {:?}, which is not among graph '{}' generated full LMBs",
3142                    requested_parent_lmb,
3143                    self.graph.name,
3144                )
3145            })?;
3146
3147        match &variant.subspace {
3148            Some(requested_basis_edges) => SubspaceData::new_from_parent_basis_edges(
3149                requested_basis_edges,
3150                containing_subgraph,
3151                parent_lmb_index,
3152                &self.graph,
3153                all_lmbs,
3154            )
3155            .or_else(|side_error| {
3156                SubspaceData::new_from_parent_basis_edges(
3157                    requested_basis_edges,
3158                    &self.graph.full_filter(),
3159                    parent_lmb_index,
3160                    &self.graph,
3161                    all_lmbs,
3162                )
3163                .with_context(|| {
3164                    format!("side-projected embedding was rejected with: {side_error:#}")
3165                })
3166            }),
3167            None => SubspaceData::new_with_user_selected_lmb(
3168                containing_subgraph.clone(),
3169                parent_lmb_index,
3170                &self.graph,
3171                all_lmbs,
3172            ),
3173        }
3174        .with_context(|| format!("{context} is incompatible with its requested parent_lmb"))
3175    }
3176
3177    fn threshold_subspaces_are_equivalent(
3178        left: &SubspaceData,
3179        right: &SubspaceData,
3180        all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
3181    ) -> bool {
3182        left.has_equivalent_embedding(right, all_lmbs)
3183    }
3184
3185    fn resolve_threshold_counterterm_directives(
3186        &self,
3187        model: &Model,
3188        settings: &GenerationSettings,
3189        runtime_settings: &RuntimeSettings,
3190        all_possible_thresholds: &[TopologicalThresholdCandidate],
3191    ) -> Result<(
3192        ResolvedThresholdCounterterms,
3193        TiVec<CutId, CutThresholdCountertermAssociations>,
3194    )> {
3195        let all_lmbs = self
3196            .derived_data
3197            .lmbs
3198            .as_ref()
3199            .expect("threshold resolution requires loop-momentum bases");
3200        let mut cut_group_by_cut = vec![None; self.cuts.len()];
3201        for (cut_group_id, cut_group) in self
3202            .derived_data
3203            .cut_group_data
3204            .cut_groups
3205            .iter_enumerated()
3206        {
3207            for cut_id in &cut_group.cuts {
3208                cut_group_by_cut[cut_id.0] = Some(cut_group_id);
3209            }
3210        }
3211
3212        let threshold_raised_data = self.graph.determine_raised_esurfaces_from_expression(
3213            &self
3214                .derived_data
3215                .global_cff_expression
3216                .as_ref()
3217                .expect("global_cff_expression should have been created")
3218                .expression,
3219        );
3220        let mut raised_threshold_ids: TiVec<EsurfaceID, Option<RaisedEsurfaceId>> =
3221            ti_vec![None; self.graph.surface_cache.esurface_cache.len()];
3222        for (raised_threshold_id, raised_group) in
3223            threshold_raised_data.raised_groups.iter_enumerated()
3224        {
3225            for &esurface_id in &raised_group.esurface_ids {
3226                raised_threshold_ids[esurface_id] = Some(raised_threshold_id);
3227            }
3228        }
3229
3230        let mut physical_drafts = Vec::new();
3231        let mut cut_threshold_associations: TiVec<CutId, CutThresholdCountertermAssociations> =
3232            ti_vec![CutThresholdCountertermAssociations::default(); self.cuts.len()];
3233        let mut legacy_equivalent = true;
3234        let mut matched_explicit_cuts = BTreeSet::new();
3235        let mut matched_explicit_thresholds = BTreeSet::new();
3236        let mut explicit_group_members = BTreeMap::<usize, Vec<_>>::new();
3237
3238        for (cut_id, cut) in self.cuts.iter_enumerated() {
3239            let cut_group_id = cut_group_by_cut[cut_id.0].ok_or_else(|| {
3240                eyre!(
3241                    "Physical cut {} in graph '{}' is missing from the cut-group partition",
3242                    cut_id.0,
3243                    self.graph.name,
3244                )
3245            })?;
3246            let cut_edges = self.physical_cut_edges(cut_id);
3247            if self
3248                .graph
3249                .threshold_counterterms
3250                .cuts
3251                .iter()
3252                .any(|configured| configured.edges == cut_edges)
3253            {
3254                matched_explicit_cuts.insert(cut_edges.clone());
3255            }
3256            let (left_containing_subgraph, right_containing_subgraph) =
3257                self.cut_group_side_subgraphs(cut_group_id)?;
3258            let (legacy_left_subspace, legacy_right_subspace) =
3259                &self.derived_data.subspace_data[cut_group_id];
3260
3261            for threshold_candidate in all_possible_thresholds {
3262                let inferred = if cut.left.includes(&threshold_candidate.left) {
3263                    let sandwich = cut.left.intersection(&threshold_candidate.right);
3264                    self.graph.underlying.is_connected(&sandwich).then_some((
3265                        ThresholdCountertermSide::Left,
3266                        sandwich,
3267                        legacy_left_subspace,
3268                        &left_containing_subgraph,
3269                    ))
3270                } else if cut.right.includes(&threshold_candidate.right) {
3271                    let sandwich = cut.right.intersection(&threshold_candidate.left);
3272                    self.graph.underlying.is_connected(&sandwich).then_some((
3273                        ThresholdCountertermSide::Right,
3274                        sandwich,
3275                        legacy_right_subspace,
3276                        &right_containing_subgraph,
3277                    ))
3278                } else {
3279                    None
3280                };
3281                let Some((side, sandwich, legacy_subspace, containing_subgraph)) = inferred else {
3282                    continue;
3283                };
3284
3285                let threshold_edges = threshold_candidate
3286                    .threshold_esurface
3287                    .energies
3288                    .iter()
3289                    .copied()
3290                    .sorted()
3291                    .collect_vec();
3292                if self
3293                    .graph
3294                    .threshold_counterterms
3295                    .cuts
3296                    .iter()
3297                    .find(|configured| configured.edges == cut_edges)
3298                    .is_some_and(|configured| {
3299                        configured
3300                            .thresholds
3301                            .iter()
3302                            .any(|threshold| threshold.edges == threshold_edges)
3303                    })
3304                {
3305                    matched_explicit_thresholds
3306                        .insert((cut_edges.clone(), threshold_edges.clone()));
3307                }
3308                // Explicit groups also constrain dormant, disabled and generation-ineligible
3309                // associations. Resolve their declared cycles before orientation/runtime filters.
3310                for (variant, _) in Self::configured_threshold_variants(
3311                    &self.graph.threshold_counterterms,
3312                    &cut_edges,
3313                    &threshold_edges,
3314                ) {
3315                    let Some(group_id) = variant.group_id else {
3316                        continue;
3317                    };
3318                    let context = format!(
3319                        "graph '{}' cut {:?} threshold {:?} variant '{}'",
3320                        self.graph.name,
3321                        cut_edges.iter().map(|edge| edge.0).collect_vec(),
3322                        threshold_edges.iter().map(|edge| edge.0).collect_vec(),
3323                        variant.name.as_deref().unwrap_or("default"),
3324                    );
3325                    let subspace = self.resolve_threshold_variant_subspace(
3326                        &variant,
3327                        legacy_subspace,
3328                        containing_subgraph,
3329                        &context,
3330                    )?;
3331                    let signature = subspace.solve_signature(all_lmbs);
3332                    let signed_cycles = signature
3333                        .iter()
3334                        .map(|(edge, cycle)| {
3335                            (
3336                                edge.0,
3337                                cycle
3338                                    .iter()
3339                                    .map(|(edge, sign)| (edge.0, sign))
3340                                    .collect_vec(),
3341                            )
3342                        })
3343                        .collect_vec();
3344                    let description = format!(
3345                        "  {context}, cut_group={}, cut={}, side={side:?}, parent={:?}, signed_cycles={signed_cycles:?}, disabled={}",
3346                        cut_group_id.0,
3347                        cut_id.0,
3348                        subspace
3349                            .get_lmb(all_lmbs)
3350                            .loop_edges
3351                            .iter()
3352                            .map(|edge| edge.0)
3353                            .collect_vec(),
3354                        variant.disable,
3355                    );
3356                    explicit_group_members
3357                        .entry(group_id)
3358                        .or_default()
3359                        .push((signature, description));
3360                }
3361                if threshold_candidate
3362                    .selected_orientation_esurface_id
3363                    .is_none()
3364                {
3365                    debug!(
3366                        "Leaving graph '{}' cut {:?} threshold {:?} dormant because it has no selected-orientation instance",
3367                        self.graph.name, cut_edges, threshold_edges,
3368                    );
3369                    continue;
3370                }
3371
3372                let association = self.threshold_counterterm_association(
3373                    threshold_candidate,
3374                    &sandwich,
3375                    &cut.cut,
3376                );
3377                if settings.threshold_subtraction.skip_thresholds_that_are_cuts
3378                    && self
3379                        .cuts
3380                        .iter()
3381                        .any(|physical_cut| physical_cut.cut == threshold_candidate.cut)
3382                {
3383                    continue;
3384                }
3385                let configured_variants = Self::configured_threshold_variants(
3386                    &self.graph.threshold_counterterms,
3387                    &cut_edges,
3388                    &threshold_edges,
3389                );
3390                let mut variants = Vec::with_capacity(configured_variants.len());
3391                for (variant, origin) in configured_variants {
3392                    if !variant.disable
3393                        && variant
3394                            .multiplier
3395                            .as_ref()
3396                            .is_some_and(|multiplier| multiplier.symmetrize)
3397                    {
3398                        unimplemented!(
3399                            "symmetrized threshold-counterterm multipliers are not implemented (graph '{}', cut {:?}, threshold {:?}, variant '{}')",
3400                            self.graph.name,
3401                            cut_edges,
3402                            threshold_edges,
3403                            variant.name.as_deref().unwrap_or("default"),
3404                        );
3405                    }
3406                    let name = variant
3407                        .name
3408                        .clone()
3409                        .unwrap_or_else(|| "default".to_string());
3410                    let context = format!(
3411                        "graph '{}' cut {:?} threshold {:?} variant '{}'",
3412                        self.graph.name,
3413                        cut_edges.iter().map(|edge| edge.0).collect_vec(),
3414                        threshold_edges.iter().map(|edge| edge.0).collect_vec(),
3415                        name,
3416                    );
3417                    if origin == ThresholdCountertermOrigin::Explicit
3418                        && variant.parent_lmb.is_none()
3419                    {
3420                        return Err(eyre!("{context} requires a full parent_lmb"));
3421                    }
3422                    let subspace = self.resolve_threshold_variant_subspace(
3423                        &variant,
3424                        legacy_subspace,
3425                        containing_subgraph,
3426                        &context,
3427                    )?;
3428                    let status = self.classify_threshold_counterterm_association(
3429                        &association,
3430                        cut_id,
3431                        cut,
3432                        &subspace,
3433                        all_lmbs,
3434                        model,
3435                        runtime_settings,
3436                        settings,
3437                    );
3438                    variants.push(ResolvedThresholdCountertermDraft {
3439                        name,
3440                        group_id: variant.group_id,
3441                        origin,
3442                        disable: variant.disable,
3443                        requested_subspace: variant.subspace,
3444                        requested_parent_lmb: variant.parent_lmb,
3445                        multiplier: variant.multiplier,
3446                        subspace,
3447                        status,
3448                    });
3449                }
3450
3451                let is_eligible = |status: ThresholdCountertermStatus| {
3452                    status.is_eligible_for_generation(
3453                        settings.threshold_subtraction.check_esurface_at_generation,
3454                    )
3455                };
3456                let legacy_status = self.classify_threshold_counterterm_association(
3457                    &association,
3458                    cut_id,
3459                    cut,
3460                    legacy_subspace,
3461                    all_lmbs,
3462                    model,
3463                    runtime_settings,
3464                    settings,
3465                );
3466                if is_eligible(legacy_status)
3467                    && (variants.len() != 1
3468                        || variants[0].disable
3469                        || variants[0].group_id.is_some()
3470                        || variants[0].multiplier.is_some()
3471                        || !Self::threshold_subspaces_are_equivalent(
3472                            &variants[0].subspace,
3473                            legacy_subspace,
3474                            all_lmbs,
3475                        ))
3476                {
3477                    legacy_equivalent = false;
3478                }
3479
3480                let has_active_variant = variants
3481                    .iter()
3482                    .any(|variant| !variant.disable && is_eligible(variant.status));
3483                if has_active_variant {
3484                    match side {
3485                        ThresholdCountertermSide::Amplitude => {
3486                            unreachable!("amplitude side is not valid in cross-section resolution")
3487                        }
3488                        ThresholdCountertermSide::Left => cut_threshold_associations[cut_id]
3489                            .left
3490                            .push(association.clone()),
3491                        ThresholdCountertermSide::Right => cut_threshold_associations[cut_id]
3492                            .right
3493                            .push(association.clone()),
3494                    }
3495                }
3496
3497                let raised_threshold_id = raised_threshold_ids[association.esurface_id]
3498                    .ok_or_else(|| {
3499                        eyre!(
3500                            "Graph '{}' threshold E-surface {} is missing from raised-threshold data",
3501                            self.graph.name,
3502                            association.esurface_id.0,
3503                        )
3504                    })?;
3505                physical_drafts.push(PhysicalThresholdCountertermDraft {
3506                    cut_id,
3507                    cut_group_id,
3508                    cut_edges: cut_edges.clone(),
3509                    side,
3510                    esurface_id: association.esurface_id,
3511                    threshold_edges,
3512                    raised_threshold_id,
3513                    raised_esurface_group: threshold_raised_data.raised_groups[raised_threshold_id]
3514                        .clone(),
3515                    variants,
3516                });
3517            }
3518        }
3519
3520        for configured_cut in &self.graph.threshold_counterterms.cuts {
3521            if !matched_explicit_cuts.contains(&configured_cut.edges) {
3522                return Err(eyre!(
3523                    "Graph '{}' threshold_counterterms cut {:?} does not match any current physical cut",
3524                    self.graph.name,
3525                    configured_cut.edges,
3526                ));
3527            }
3528            for configured_threshold in &configured_cut.thresholds {
3529                if !matched_explicit_thresholds.contains(&(
3530                    configured_cut.edges.clone(),
3531                    configured_threshold.edges.clone(),
3532                )) {
3533                    return Err(eyre!(
3534                        "Graph '{}' threshold_counterterms cut {:?} threshold {:?} does not match a topology-discovered association",
3535                        self.graph.name,
3536                        configured_cut.edges,
3537                        configured_threshold.edges,
3538                    ));
3539                }
3540            }
3541        }
3542
3543        // An explicit group id is a user assertion about one solve space. Validate every
3544        // resolved member in its native parent so incompatible cuts can never silently share
3545        // an SOCP center, including members currently disabled or ineligible for generation.
3546        let conflicts = explicit_group_members
3547            .into_iter()
3548            .filter_map(|(group_id, members)| {
3549                if members
3550                    .iter()
3551                    .all(|(signature, _)| signature == &members[0].0)
3552                {
3553                    return None;
3554                }
3555                Some(format!(
3556                    "group_id={group_id} contains incompatible solve subspaces:\n{}",
3557                    members
3558                        .into_iter()
3559                        .map(|(_, description)| description)
3560                        .join("\n"),
3561                ))
3562            })
3563            .collect_vec();
3564        if !conflicts.is_empty() {
3565            return Err(eyre!(
3566                "Graph '{}' has incompatible explicit threshold groups. A group_id may only refine one automatic signed-cycle subspace. All members of each conflicting group:\n{}",
3567                self.graph.name,
3568                conflicts.join("\n"),
3569            ));
3570        }
3571
3572        let mut grouped = BTreeMap::<
3573            (CutGroupId, RaisedEsurfaceId),
3574            Vec<PhysicalThresholdCountertermDraft>,
3575        >::new();
3576        for draft in physical_drafts {
3577            grouped
3578                .entry((draft.cut_group_id, draft.raised_threshold_id))
3579                .or_default()
3580                .push(draft);
3581        }
3582
3583        let mut aggregated = Vec::new();
3584        for ((cut_group_id, raised_threshold_id), mut drafts) in grouped {
3585            let raised_group_is_legacy_equivalent = drafts.iter().all(|draft| {
3586                let legacy_subspace = match draft.side {
3587                    ThresholdCountertermSide::Amplitude => return false,
3588                    ThresholdCountertermSide::Left => {
3589                        &self.derived_data.subspace_data[cut_group_id].0
3590                    }
3591                    ThresholdCountertermSide::Right => {
3592                        &self.derived_data.subspace_data[cut_group_id].1
3593                    }
3594                };
3595                draft.variants.len() == 1
3596                    && !draft.variants[0].disable
3597                    && draft.variants[0].group_id.is_none()
3598                    && draft.variants[0].multiplier.is_none()
3599                    && Self::threshold_subspaces_are_equivalent(
3600                        &draft.variants[0].subspace,
3601                        legacy_subspace,
3602                        all_lmbs,
3603                    )
3604            });
3605            let has_left = drafts
3606                .iter()
3607                .any(|draft| draft.side == ThresholdCountertermSide::Left);
3608            let has_right = drafts
3609                .iter()
3610                .any(|draft| draft.side == ThresholdCountertermSide::Right);
3611            if has_left && has_right {
3612                if raised_group_is_legacy_equivalent {
3613                    // Preserve the legacy left-side precedence for a raised threshold seen on
3614                    // both sides of physical members of one cut group.
3615                    drafts.retain(|draft| draft.side == ThresholdCountertermSide::Left);
3616                } else {
3617                    return Err(eyre!(
3618                        "Graph '{}' cut group {} raised threshold {:?} is inferred on both sides; non-legacy directives must resolve compatibly across raised physical cuts",
3619                        self.graph.name,
3620                        cut_group_id.0,
3621                        threshold_raised_data.raised_groups[raised_threshold_id].esurface_ids,
3622                    ));
3623                }
3624            }
3625
3626            let representative = drafts.first().expect("grouped drafts are non-empty");
3627            if drafts.iter().skip(1).any(|draft| {
3628                draft.variants.len() != representative.variants.len()
3629                    || draft
3630                        .variants
3631                        .iter()
3632                        .zip(&representative.variants)
3633                        .any(|(variant, expected)| !variant.is_compatible_with(expected, all_lmbs))
3634            }) {
3635                let descriptions = drafts.iter().flat_map(|draft| {
3636                    draft.variants.iter().map(|variant| format!(
3637                        "  cut={}, cut_edges={:?}, threshold_edges={:?}, variant='{}', group_id={:?}, parent={:?}, signed_cycles={:?}, disabled={}, multiplier={:?}",
3638                        draft.cut_id.0,
3639                        draft.cut_edges,
3640                        draft.threshold_edges,
3641                        variant.name,
3642                        variant.group_id,
3643                        variant.subspace.get_lmb(all_lmbs).loop_edges,
3644                        variant.subspace.solve_signature(all_lmbs),
3645                        variant.disable,
3646                        variant.multiplier,
3647                    ))
3648                }).join("\n");
3649                return Err(eyre!(
3650                    "Graph '{}' cut group {} raised threshold {:?} resolves incompatibly across physical cuts. Corresponding counterterm variants of one merged raised residue must have identical directives, group_id and signed solve cycles. All physical members:\n{}",
3651                    self.graph.name,
3652                    cut_group_id.0,
3653                    representative.raised_esurface_group.esurface_ids,
3654                    descriptions,
3655                ));
3656            }
3657            aggregated.push(drafts);
3658        }
3659
3660        aggregated.sort_by_key(|drafts| {
3661            let representative = &drafts[0];
3662            (
3663                representative.cut_group_id.0,
3664                match representative.side {
3665                    ThresholdCountertermSide::Amplitude => 2,
3666                    ThresholdCountertermSide::Left => 0,
3667                    ThresholdCountertermSide::Right => 1,
3668                },
3669                representative
3670                    .raised_esurface_group
3671                    .esurface_ids
3672                    .iter()
3673                    .map(|id| id.0)
3674                    .collect_vec(),
3675            )
3676        });
3677
3678        let mut variants =
3679            TiVec::<ThresholdCountertermVariantId, ResolvedThresholdCountertermVariant>::new();
3680        let mut cut_groups = ti_vec![
3681            ResolvedCutGroupThresholdCounterterms::default();
3682            self.derived_data.cut_group_data.cut_groups.len()
3683        ];
3684        let is_eligible = |status: ThresholdCountertermStatus| {
3685            status.is_eligible_for_generation(
3686                settings.threshold_subtraction.check_esurface_at_generation,
3687            )
3688        };
3689
3690        for drafts in aggregated {
3691            let representative = &drafts[0];
3692            let mut active_variant_count = 0;
3693            for variant_index in 0..representative.variants.len() {
3694                let representative_variant = &representative.variants[variant_index];
3695                if representative_variant.disable {
3696                    continue;
3697                }
3698                let eligible_cut_ids = drafts
3699                    .iter()
3700                    .filter(|draft| is_eligible(draft.variants[variant_index].status))
3701                    .map(|draft| draft.cut_id)
3702                    .sorted_by_key(|cut_id| cut_id.0)
3703                    .dedup()
3704                    .collect_vec();
3705                if eligible_cut_ids.is_empty() {
3706                    continue;
3707                }
3708
3709                active_variant_count += 1;
3710                let associations = drafts
3711                    .iter()
3712                    .map(|draft| {
3713                        let resolved_variant = &draft.variants[variant_index];
3714                        let legacy_subspace = match draft.side {
3715                            ThresholdCountertermSide::Amplitude => unreachable!(
3716                                "amplitude side is not valid in cross-section resolution"
3717                            ),
3718                            ThresholdCountertermSide::Left => {
3719                                &self.derived_data.subspace_data[draft.cut_group_id].0
3720                            }
3721                            ThresholdCountertermSide::Right => {
3722                                &self.derived_data.subspace_data[draft.cut_group_id].1
3723                            }
3724                        };
3725                        ResolvedThresholdCountertermAssociation {
3726                            cut_id: Some(draft.cut_id),
3727                            cut_edges: draft.cut_edges.clone(),
3728                            threshold_edges: draft.threshold_edges.clone(),
3729                            esurface_id: draft.esurface_id,
3730                            subspace: resolved_variant.subspace.clone(),
3731                            requires_explicit_parent_lmb: resolved_variant
3732                                .subspace
3733                                .parent_lmb_index()
3734                                != legacy_subspace.parent_lmb_index(),
3735                            eligible: is_eligible(resolved_variant.status),
3736                            origin: resolved_variant.origin,
3737                        }
3738                    })
3739                    .sorted_by_key(|association| {
3740                        (
3741                            association.cut_id.map(|cut_id| cut_id.0),
3742                            association.esurface_id.0,
3743                            association.threshold_edges.clone(),
3744                        )
3745                    })
3746                    .collect();
3747                let parent_lmb_index = representative_variant.subspace.parent_lmb_index();
3748                let variant_id = ThresholdCountertermVariantId::from(variants.len());
3749                variants.push(ResolvedThresholdCountertermVariant {
3750                    name: representative_variant.name.clone(),
3751                    group_id: representative_variant.group_id,
3752                    cut_group_id: Some(representative.cut_group_id),
3753                    associations,
3754                    side: representative.side,
3755                    threshold_esurface_ids: representative
3756                        .raised_esurface_group
3757                        .esurface_ids
3758                        .clone(),
3759                    raised_esurface_group: representative.raised_esurface_group.clone(),
3760                    requested_subspace: representative_variant.requested_subspace.clone(),
3761                    requested_parent_lmb: representative_variant.requested_parent_lmb.clone(),
3762                    resolved_parent_lmb: all_lmbs[parent_lmb_index].loop_edges.clone().into(),
3763                    subspace: representative_variant.subspace.clone(),
3764                    subspace_loop_count: representative_variant.subspace.loopcount(),
3765                    multiplier: representative_variant.multiplier.clone(),
3766                });
3767                match representative.side {
3768                    ThresholdCountertermSide::Amplitude => {
3769                        unreachable!("amplitude side is not valid in cross-section resolution")
3770                    }
3771                    ThresholdCountertermSide::Left => cut_groups[representative.cut_group_id]
3772                        .left
3773                        .push(variant_id),
3774                    ThresholdCountertermSide::Right => cut_groups[representative.cut_group_id]
3775                        .right
3776                        .push(variant_id),
3777                }
3778            }
3779
3780            let legacy_subspace = match representative.side {
3781                ThresholdCountertermSide::Amplitude => {
3782                    unreachable!("amplitude side is not valid in cross-section resolution")
3783                }
3784                ThresholdCountertermSide::Left => {
3785                    &self.derived_data.subspace_data[representative.cut_group_id].0
3786                }
3787                ThresholdCountertermSide::Right => {
3788                    &self.derived_data.subspace_data[representative.cut_group_id].1
3789                }
3790            };
3791            if active_variant_count > 0
3792                && (active_variant_count != 1
3793                    || representative.variants.len() != 1
3794                    || representative.variants[0].multiplier.is_some()
3795                    || representative.variants[0].group_id.is_some()
3796                    || !Self::threshold_subspaces_are_equivalent(
3797                        &representative.variants[0].subspace,
3798                        legacy_subspace,
3799                        all_lmbs,
3800                    ))
3801            {
3802                legacy_equivalent = false;
3803            }
3804        }
3805
3806        for (cut_group_id, cut_group) in cut_groups.iter_enumerated() {
3807            for (left_variant_id, right_variant_id) in cut_group.iter_pairs() {
3808                let left = &variants[left_variant_id];
3809                let right = &variants[right_variant_id];
3810                if !left.subspace.is_mergable_with(&right.subspace, all_lmbs) {
3811                    return Err(eyre!(
3812                        "Graph '{}' cut group {} has incompatible iterated threshold projections. Left variant '{}': associations={:?}, parent={:?}, signed_cycles={:?}; right variant '{}': associations={:?}, parent={:?}, signed_cycles={:?}. Each selected cycle must have zero response on the other variant's defining edges",
3813                        self.graph.name,
3814                        cut_group_id.0,
3815                        left.name,
3816                        left.associations
3817                            .iter()
3818                            .map(|association| (
3819                                &association.cut_edges,
3820                                &association.threshold_edges
3821                            ))
3822                            .collect_vec(),
3823                        left.subspace.get_lmb(all_lmbs).loop_edges,
3824                        left.subspace.solve_signature(all_lmbs),
3825                        right.name,
3826                        right
3827                            .associations
3828                            .iter()
3829                            .map(|association| (
3830                                &association.cut_edges,
3831                                &association.threshold_edges
3832                            ))
3833                            .collect_vec(),
3834                        right.subspace.get_lmb(all_lmbs).loop_edges,
3835                        right.subspace.solve_signature(all_lmbs),
3836                    ));
3837                }
3838            }
3839        }
3840
3841        Ok((
3842            ResolvedThresholdCounterterms {
3843                legacy_equivalent,
3844                variants,
3845                cross_section_cut_groups: cut_groups,
3846            },
3847            cut_threshold_associations,
3848        ))
3849    }
3850
3851    fn build_threshold_counterterm(
3852        &mut self,
3853        model: &Model,
3854        settings: &GenerationSettings,
3855        runtime_settings: &RuntimeSettings,
3856        all_possible_thresholds: &[TopologicalThresholdCandidate],
3857        vakint: &Vakint,
3858        cff_energy_degree_bound_reports: &Mutex<Vec<CffEnergyDegreeBoundReport>>,
3859    ) -> Result<()> {
3860        let (resolved, cut_threshold_associations) = self
3861            .resolve_threshold_counterterm_directives(
3862                model,
3863                settings,
3864                runtime_settings,
3865                all_possible_thresholds,
3866            )?;
3867
3868        let mut cut_structure = vec![];
3869        let expression_esurfaces = &self
3870            .derived_data
3871            .global_cff_expression
3872            .as_ref()
3873            .expect("global_cff_expression should have been created")
3874            .expression
3875            .surfaces
3876            .esurface_cache;
3877        for (cut_group_id, cut_group) in self
3878            .derived_data
3879            .cut_group_data
3880            .cut_groups
3881            .iter_enumerated()
3882        {
3883            let resolved_group = &resolved.cross_section_cut_groups[cut_group_id];
3884            let lu_cut_selection = cut_group.lu_cut_selection(&self.graph, &self.cuts);
3885            let cutkosky_cut_union = cut_group
3886                .cuts
3887                .iter()
3888                .map(|cut_id| self.cuts[*cut_id].cut.as_subgraph())
3889                .reduce(|a, b| a.union(&b))
3890                .unwrap_or(self.graph.empty_subgraph());
3891
3892            let add_threshold_group_to_union =
3893                |base: SuBitGraph, raised_group: &RaisedEsurfaceGroup| {
3894                    let representative_esurface =
3895                        &expression_esurfaces[raised_group.esurface_ids[0]];
3896
3897                    representative_esurface
3898                        .energies
3899                        .iter()
3900                        .map(|edge_id| self.graph.get_edge_subgraph(*edge_id))
3901                        .fold(base, |acc, subgraph| acc.union(&subgraph))
3902                };
3903
3904            for &variant_id in &resolved_group.left {
3905                let raised_esurface_group = &resolved.variants[variant_id].raised_esurface_group;
3906                let esurface_cut_union =
3907                    add_threshold_group_to_union(cutkosky_cut_union.clone(), raised_esurface_group);
3908
3909                cut_structure.push(CutSet {
3910                    residue_selector: ResidueSelector {
3911                        lu: Some(lu_cut_selection.clone()),
3912                        left_th_cut: Some(raised_esurface_group.clone()),
3913                        right_th_cut: None,
3914                    },
3915                    union: esurface_cut_union,
3916                    canonicalize_external_shifts: false,
3917                });
3918            }
3919
3920            for &variant_id in &resolved_group.right {
3921                let raised_esurface_group = &resolved.variants[variant_id].raised_esurface_group;
3922                let esurface_cut_union =
3923                    add_threshold_group_to_union(cutkosky_cut_union.clone(), raised_esurface_group);
3924
3925                cut_structure.push(CutSet {
3926                    residue_selector: ResidueSelector {
3927                        lu: Some(lu_cut_selection.clone()),
3928                        left_th_cut: None,
3929                        right_th_cut: Some(raised_esurface_group.clone()),
3930                    },
3931                    union: esurface_cut_union,
3932                    canonicalize_external_shifts: false,
3933                });
3934            }
3935
3936            for (left_variant_id, right_variant_id) in resolved_group.iter_pairs() {
3937                let left_raised_esurface_group =
3938                    &resolved.variants[left_variant_id].raised_esurface_group;
3939                let right_raised_esurface_group =
3940                    &resolved.variants[right_variant_id].raised_esurface_group;
3941                let esurface_cut_union = add_threshold_group_to_union(
3942                    add_threshold_group_to_union(
3943                        cutkosky_cut_union.clone(),
3944                        left_raised_esurface_group,
3945                    ),
3946                    right_raised_esurface_group,
3947                );
3948
3949                cut_structure.push(CutSet {
3950                    residue_selector: ResidueSelector {
3951                        lu: Some(lu_cut_selection.clone()),
3952                        left_th_cut: Some(left_raised_esurface_group.clone()),
3953                        right_th_cut: Some(right_raised_esurface_group.clone()),
3954                    },
3955                    union: esurface_cut_union,
3956                    canonicalize_external_shifts: false,
3957                });
3958            }
3959        }
3960
3961        let cut_structure = CutStructure {
3962            cuts: cut_structure,
3963        };
3964
3965        let cff_options = self.graph.production_cff_3d_expression_options(settings)?;
3966        let production_expression = self
3967            .derived_data
3968            .global_cff_expression
3969            .as_ref()
3970            .expect("global_cff_expression should have been created");
3971
3972        let mut threshold_counterterms = settings
3973            .uv
3974            .orchestrator
3975            .parametric_integrands(
3976                &mut self.graph,
3977                cut_structure,
3978                vakint,
3979                OrientationProjection::exact_expression(
3980                    production_expression,
3981                    &cff_options,
3982                    &settings.orientation_pattern,
3983                    settings.explicit_orientation_sum_only,
3984                )
3985                .with_vacuum_subtraction(settings.medium.vacuum_subtraction)
3986                .with_energy_degree_bound_reports(cff_energy_degree_bound_reports),
3987                settings,
3988            )?
3989            .into_iter();
3990
3991        let mut result = TiVec::<CutGroupId, LUCounterTermData>::new();
3992        for (cut_group_id, _) in self
3993            .derived_data
3994            .cut_group_data
3995            .cut_groups
3996            .iter_enumerated()
3997        {
3998            let resolved_group = &resolved.cross_section_cut_groups[cut_group_id];
3999            let lu_prefactor = self.derived_data.cut_group_data.cut_groups[cut_group_id]
4000                .lu_prefactor(&self.graph, &self.cuts)?;
4001            let left_variant_ids: TiVec<LeftThresholdId, ThresholdCountertermVariantId> =
4002                resolved_group.left.iter().copied().collect();
4003            let right_variant_ids: TiVec<RightThresholdId, ThresholdCountertermVariantId> =
4004                resolved_group.right.iter().copied().collect();
4005            let left_thresholds = left_variant_ids
4006                .iter()
4007                .map(|variant_id| resolved.variants[*variant_id].raised_esurface_group.clone())
4008                .collect();
4009            let right_thresholds = right_variant_ids
4010                .iter()
4011                .map(|variant_id| resolved.variants[*variant_id].raised_esurface_group.clone())
4012                .collect();
4013            let left_subspaces = left_variant_ids
4014                .iter()
4015                .map(|variant_id| resolved.variants[*variant_id].subspace.clone())
4016                .collect();
4017            let right_subspaces = right_variant_ids
4018                .iter()
4019                .map(|variant_id| resolved.variants[*variant_id].subspace.clone())
4020                .collect();
4021
4022            let mut left_atoms = TiVec::<LeftThresholdId, _>::new();
4023            let mut right_atoms = TiVec::<RightThresholdId, _>::new();
4024            let mut iterated_atoms = vec![];
4025
4026            for variant_id in &left_variant_ids {
4027                let variant = &resolved.variants[*variant_id];
4028                let rstar_pow = Self::threshold_rstar_power(variant.subspace_loop_count, false);
4029                let counterterm = threshold_counterterms.next().ok_or_else(|| {
4030                    eyre!(
4031                        "Threshold orchestrator returned too few single-left integrands for graph '{}' cut group {}",
4032                        self.graph.name,
4033                        cut_group_id.0,
4034                    )
4035                })?;
4036                left_atoms.push(counterterm.map(|x| x * &lu_prefactor * &rstar_pow));
4037            }
4038
4039            for variant_id in &right_variant_ids {
4040                let variant = &resolved.variants[*variant_id];
4041                let rstar_pow = Self::threshold_rstar_power(variant.subspace_loop_count, true);
4042                let counterterm = threshold_counterterms.next().ok_or_else(|| {
4043                    eyre!(
4044                        "Threshold orchestrator returned too few single-right integrands for graph '{}' cut group {}",
4045                        self.graph.name,
4046                        cut_group_id.0,
4047                    )
4048                })?;
4049                right_atoms.push(counterterm.map(|x| x * &lu_prefactor * &rstar_pow));
4050            }
4051
4052            for (left_variant_id, right_variant_id) in resolved_group.iter_pairs() {
4053                let left_variant = &resolved.variants[left_variant_id];
4054                let right_variant = &resolved.variants[right_variant_id];
4055                let left_rstar_pow =
4056                    Self::threshold_rstar_power(left_variant.subspace_loop_count, false);
4057                let right_rstar_pow =
4058                    Self::threshold_rstar_power(right_variant.subspace_loop_count, true);
4059                let counterterm = threshold_counterterms.next().ok_or_else(|| {
4060                    eyre!(
4061                        "Threshold orchestrator returned too few iterated integrands for graph '{}' cut group {}",
4062                        self.graph.name,
4063                        cut_group_id.0,
4064                    )
4065                })?;
4066                iterated_atoms.push(
4067                    counterterm.map(|x| x * &lu_prefactor * &left_rstar_pow * &right_rstar_pow),
4068                );
4069            }
4070
4071            let iterated_collection =
4072                IteratedCtCollection::new(iterated_atoms, left_atoms.len(), right_atoms.len());
4073
4074            let counterterm_data = LUCounterTermData {
4075                left_thresholds,
4076                right_thresholds,
4077                left_variant_ids,
4078                right_variant_ids,
4079                left_subspaces,
4080                right_subspaces,
4081                left_atoms,
4082                right_atoms,
4083                iterated: iterated_collection,
4084            };
4085            result.push(counterterm_data);
4086        }
4087        if threshold_counterterms.next().is_some() {
4088            return Err(eyre!(
4089                "Threshold orchestrator returned too many integrands for graph '{}'",
4090                self.graph.name,
4091            ));
4092        }
4093
4094        let max_dual_size =
4095            max_dual_size_for_cut_cff_indices(result.iter().flat_map(|counterterm_data| {
4096                counterterm_data
4097                    .left_atoms
4098                    .iter()
4099                    .chain(counterterm_data.right_atoms.iter())
4100                    .chain(counterterm_data.iterated.iter())
4101                    .flat_map(|integrands| integrands.integrands.iter().map(|(index, _)| index))
4102            }));
4103        self.graph.param_builder.initialize_duals(max_dual_size);
4104
4105        self.derived_data.cut_threshold_associations = cut_threshold_associations;
4106        self.derived_data.threshold_counterterms = result;
4107        self.derived_data.resolved_threshold_counterterms = Some(resolved);
4108        Ok(())
4109    }
4110
4111    fn build_subspace_data(&mut self) -> Result<()> {
4112        let all_lmbs = self.derived_data.lmbs.as_ref().unwrap();
4113
4114        let subspace_data = self
4115            .derived_data
4116            .cut_group_data
4117            .cut_groups
4118            .iter_enumerated()
4119            .map(|(cut_group_id, _)| {
4120                let valid_subspace_lmbs =
4121                    self.cut_group_compatible_parent_lmbs(cut_group_id)?;
4122                let (smallest_left_subgraph, smallest_right_subgraph) =
4123                    self.cut_group_side_subgraphs(cut_group_id)?;
4124
4125                let mut possible_subspaces = Vec::new();
4126                let mut rejected_lmbs = Vec::new();
4127                for lmb_index in valid_subspace_lmbs {
4128                    let left = SubspaceData::new_with_user_selected_lmb(
4129                        smallest_left_subgraph.clone(),
4130                        lmb_index,
4131                        &self.graph,
4132                        all_lmbs,
4133                    );
4134                    let right = SubspaceData::new_with_user_selected_lmb(
4135                        smallest_right_subgraph.clone(),
4136                        lmb_index,
4137                        &self.graph,
4138                        all_lmbs,
4139                    );
4140
4141                    match (left, right) {
4142                        (Ok(left), Ok(right)) if left.is_mergable_with(&right, all_lmbs) => {
4143                            possible_subspaces.push((left, right));
4144                        }
4145                        (Ok(left), Ok(right)) => rejected_lmbs.push(format!(
4146                            "LMB {} produced non-disjoint subspaces: left={:?}, right={:?}",
4147                            usize::from(lmb_index),
4148                            left.iter_lmb_indices().collect_vec(),
4149                            right.iter_lmb_indices().collect_vec(),
4150                        )),
4151                        (left, right) => rejected_lmbs.push(format!(
4152                            "LMB {}: left={}, right={}",
4153                            usize::from(lmb_index),
4154                            left.err()
4155                                .map(|error| format!("{error:#}"))
4156                                .unwrap_or_else(|| "compatible".to_string()),
4157                            right
4158                                .err()
4159                                .map(|error| format!("{error:#}"))
4160                                .unwrap_or_else(|| "compatible".to_string()),
4161                        )),
4162                    }
4163                }
4164
4165                possible_subspaces.sort_by_key(|(left, right)| {
4166                    (
4167                        left.iter_basis_edges(all_lmbs).collect_vec(),
4168                        right.iter_basis_edges(all_lmbs).collect_vec(),
4169                    )
4170                });
4171
4172                possible_subspaces.first().cloned().ok_or_else(|| {
4173                    eyre!(
4174                        "No topology-compatible parent LMB found for graph '{}' cut group {}. Rejections:\n{}",
4175                        self.graph.name,
4176                        cut_group_id.0,
4177                        rejected_lmbs.join("\n"),
4178                    )
4179                })
4180            })
4181            .collect::<Result<_>>()?;
4182
4183        let _: () = self.derived_data.subspace_data = subspace_data;
4184        Ok(())
4185    }
4186
4187    fn generate_term_for_graph(
4188        &self,
4189        _model: &Model,
4190        settings: &GlobalSettings,
4191    ) -> Result<(CrossSectionGraphTerm, GraphGenerationStats)> {
4192        CrossSectionGraphTerm::from_cross_section_graph(self, settings)
4193    }
4194}
4195
4196#[derive(Clone, Encode, Decode)]
4197#[trait_decode(trait = GammaLoopContext)]
4198pub struct CrossSectionDerivedData {
4199    /// Observable labels of the physical states represented by covariant
4200    /// Goldstone/ghost cuts; explicit unphysical diagnostic states stay intact.
4201    pub covariant_cut_representatives: BTreeMap<isize, isize>,
4202    pub orientations: Option<TiVec<OrientationID, EdgeVec<Orientation>>>,
4203    pub cut_paramatric_integrand: TiVec<CutGroupId, ParametricIntegrands>,
4204    pub global_cff_expression: Option<
4205        GeneratedThreeDExpression<crate::cff::esurface::Esurface, crate::cff::hsurface::Hsurface>,
4206    >,
4207    pub lmbs: Option<TiVec<LmbIndex, LoopMomentumBasis>>,
4208    pub multi_channeling_setup: Option<LmbMultiChannelingSetup>,
4209    pub threshold_counterterms: TiVec<CutGroupId, LUCounterTermData>,
4210    pub resolved_threshold_counterterms: Option<ResolvedThresholdCounterterms>,
4211    pub topological_threshold_esurfaces: TiVec<TopologicalThresholdId, Esurface>,
4212    /// Graph-level inventory of topology-discovered threshold E-surfaces that have an instance
4213    /// in the selected generation orientations. Orientation-excluded topology identities remain
4214    /// available while resolving directives, but have no CFF E-surface ID to store here.
4215    pub threshold_candidate_esurface_ids: Vec<EsurfaceID>,
4216
4217    /// Exact generated left/right threshold associations for each physical cut. Runtime
4218    /// evaluators aggregate these into cut groups, but display and model reclassification
4219    /// must retain the physical-cut-relative eligibility information.
4220    pub cut_threshold_associations: TiVec<CutId, CutThresholdCountertermAssociations>,
4221    pub subspace_data: TiVec<CutGroupId, (SubspaceData, SubspaceData)>,
4222    pub cut_group_data: CutGroupData,
4223}
4224
4225/// Partition and residue-derivative metadata for all Cutkosky cuts, including order-one cuts.
4226#[derive(Clone, Encode, Decode, Debug)]
4227#[trait_decode(trait = GammaLoopContext)]
4228pub struct CutGroupData {
4229    pub cut_groups: TiVec<CutGroupId, CutGroup>,
4230    pub dual_shapes: Vec<Vec<Vec<usize>>>,
4231    pub pass_two_evaluators: Vec<GenericEvaluator>,
4232}
4233
4234/// Physical cuts that share one normalized E-surface residue, whether ordinary or raised.
4235#[derive(Clone, Encode, Decode, Debug)]
4236#[trait_decode(trait = GammaLoopContext)]
4237pub struct CutGroup {
4238    pub cuts: Vec<CutId>,
4239    pub related_esurface_group: RaisedEsurfaceGroup,
4240}
4241
4242impl CutGroup {
4243    pub(crate) fn lu_prefactor(
4244        &self,
4245        graph: &Graph,
4246        cuts: &TiVec<CutId, CrossSectionCut>,
4247    ) -> Result<Atom> {
4248        let loop_number =
4249            graph.cyclotomatic_number(&graph.full_filter()) - graph.initial_state_cut.nedges(graph);
4250
4251        let loop_3 = loop_number as i64 * 3;
4252        // The inverse UFO vertices already supply the Hermitian-partner
4253        // tensors/couplings. Marking the RHS gives (-1)^(V_R+I_R), and its
4254        // anti-causal virtual contours give (-1)^L_R: together (-1)^C_R.
4255        // Retain cut hairs when counting components, including a contact RHS.
4256        let component_parities = self
4257            .cuts
4258            .iter()
4259            .map(|cut| {
4260                graph
4261                    .underlying
4262                    .count_connected_components(&cuts[*cut].right)
4263                    % 2
4264            })
4265            .unique()
4266            .collect_vec();
4267        let [parity] = component_parities.as_slice() else {
4268            return Err(eyre!(
4269                "Graph '{}' has incompatible RHS marking signs in raised cut group {:?}",
4270                graph.name,
4271                self.cuts
4272            ));
4273        };
4274        // CFF retains the cut propagator numerators, including their i factors.
4275        // The residue conversion supplies 2πi, making the complete physical
4276        // cut i/(q²-m²+i0) -> 2π δ_+(q²-m²) real. For connected RHS, this
4277        // combines with marking to -2πi, independent of cut multiplicity.
4278        let energy_conservation_delta_factor =
4279            Atom::num(2) * Atom::var(GS.pi) * Atom::i() * Atom::num(-1).pow(*parity as i64);
4280
4281        crate::debug_tags!(#generation, #normalization, #lu, #graph, #summary;
4282            stage = "cross_section_lu_prefactor",
4283            graph = %graph.name,
4284            loop_number,
4285            loop_3,
4286            "Cross-section LU prefactor normalization"
4287        );
4288
4289        let tstar = Atom::var(GS.rescale_star);
4290        let tsrat_pow = tstar.pow(loop_3);
4291        let hfunction = Atom::var(GS.hfunction_lu_cut);
4292        Ok(tsrat_pow * hfunction * energy_conservation_delta_factor)
4293    }
4294
4295    pub(crate) fn lu_cut_selection(
4296        &self,
4297        graph: &Graph,
4298        cuts: &TiVec<CutId, CrossSectionCut>,
4299    ) -> LuCutSelection {
4300        let raised_edge_groups = graph.get_raised_edge_groups();
4301        let cut_edge_alternatives = self
4302            .cuts
4303            .iter()
4304            .map(|cut_id| {
4305                let cut_edges = graph
4306                    .iter_edges_of(&cuts[*cut_id].cut.as_subgraph())
4307                    .map(|(_, edge_id, _)| edge_id)
4308                    .collect_vec();
4309                normalize_cut_edge_support_with_raised_edge_groups(&cut_edges, &raised_edge_groups)
4310            })
4311            .collect();
4312        LuCutSelection {
4313            raised_group: self.related_esurface_group.clone(),
4314            cut_edge_alternatives,
4315        }
4316    }
4317}
4318
4319impl Default for CutGroupData {
4320    fn default() -> Self {
4321        Self::new()
4322    }
4323}
4324
4325impl CutGroupData {
4326    pub fn new() -> Self {
4327        CutGroupData {
4328            cut_groups: TiVec::new(),
4329            dual_shapes: vec![],
4330            pass_two_evaluators: vec![],
4331        }
4332    }
4333
4334    pub fn new_from_esurface(
4335        raised_esurface_data: &RaisedEsurfaceData,
4336        cut_esurface_map: &TiVec<CutId, EsurfaceID>,
4337        evaluator_settings: &EvaluatorSettings,
4338    ) -> (Self, GraphGenerationStats) {
4339        let mut stats = GraphGenerationStats::default();
4340        let mut groups = TiVec::new();
4341
4342        for (_raised_esurface_id, raised_esurface_group) in
4343            raised_esurface_data.raised_groups.iter_enumerated()
4344        {
4345            let cuts = cut_esurface_map
4346                .iter_enumerated()
4347                .filter_map(|(cut_id, esurface_id)| {
4348                    raised_esurface_group
4349                        .esurface_ids
4350                        .contains(esurface_id)
4351                        .then_some(cut_id)
4352                })
4353                .collect::<Vec<_>>();
4354
4355            if cuts.is_empty() {
4356                continue;
4357            }
4358
4359            groups.push(CutGroup {
4360                cuts,
4361                related_esurface_group: raised_esurface_group.clone(),
4362            });
4363        }
4364
4365        let global_max_occurence = groups
4366            .iter()
4367            .map(|group| group.related_esurface_group.max_occurence)
4368            .max()
4369            .unwrap_or_else(|| {
4370                println!("corrupted groups");
4371                panic!();
4372            });
4373
4374        let dual_shapes = (1..global_max_occurence)
4375            .map(simple_n_deriv_shape)
4376            .collect();
4377
4378        let pass_two_evaluators = (1..=global_max_occurence)
4379            .map(|i| {
4380                let evaluator_started = std::time::Instant::now();
4381                let evaluator = build_derivative_structure(i as u8, -1, evaluator_settings);
4382                stats.evaluator_symbolica_time += evaluator_started.elapsed();
4383                stats.evaluator_count += 1;
4384                evaluator
4385            })
4386            .collect();
4387
4388        (
4389            Self {
4390                cut_groups: groups,
4391                dual_shapes,
4392                pass_two_evaluators,
4393            },
4394            stats,
4395        )
4396    }
4397}
4398
4399impl CrossSectionDerivedData {
4400    fn new_empty() -> Self {
4401        Self {
4402            covariant_cut_representatives: BTreeMap::new(),
4403            orientations: None,
4404            global_cff_expression: None,
4405            cut_paramatric_integrand: TiVec::new(),
4406            lmbs: None,
4407            multi_channeling_setup: None,
4408            threshold_counterterms: TiVec::new(),
4409            resolved_threshold_counterterms: None,
4410            topological_threshold_esurfaces: TiVec::new(),
4411            threshold_candidate_esurface_ids: Vec::new(),
4412
4413            cut_threshold_associations: TiVec::new(),
4414            subspace_data: TiVec::new(),
4415            cut_group_data: CutGroupData::new(),
4416        }
4417    }
4418}
4419
4420pub(crate) fn build_derivative_structure_atom(
4421    singularity_order: u8,
4422    laurent_coefficient: i8,
4423) -> Atom {
4424    assert!(
4425        laurent_coefficient <= -1,
4426        "only laurent coefficients up to -1 are supported"
4427    );
4428
4429    assert!(
4430        singularity_order >= 1,
4431        "eta order must be at least 1, got {singularity_order}"
4432    );
4433    assert!(
4434        singularity_order >= -laurent_coefficient as u8,
4435        "eta order must be at least the negative of the laurent coefficient, got {singularity_order} for laurent coefficient {laurent_coefficient}"
4436    );
4437
4438    let order = singularity_order as i32;
4439    let laurent_coefficient = laurent_coefficient as i32;
4440    let f = symbol!("f");
4441
4442    let expansion = function!(GS.eta, GS.rescale)
4443        .series(GS.rescale, Atom::var(GS.rescale_star), (order, 1))
4444        .unwrap()
4445        .to_atom()
4446        .replace(function!(GS.eta, GS.rescale_star))
4447        .min_level(0)
4448        .max_level(Some(0))
4449        .with(0);
4450
4451    let mut expression_to_derive = function!(f, GS.rescale)
4452        * expansion.pow(-order)
4453        * (GS.rescale - GS.rescale_star).pow(order);
4454
4455    for _ in 1..=(order + laurent_coefficient) {
4456        expression_to_derive = expression_to_derive.derivative(GS.rescale);
4457    }
4458
4459    expression_to_derive = expression_to_derive
4460        .replace(GS.rescale - GS.rescale_star)
4461        .with(parse!("delta_t"));
4462
4463    let polynomial_in_delta_t = expression_to_derive
4464        .series(symbol!("delta_t"), Atom::num(0), (0, 1))
4465        .unwrap();
4466
4467    let factorial_prefactor = (2..=(order + laurent_coefficient)).product::<i32>();
4468    debug!("factorial prefactor: {}", factorial_prefactor);
4469    let mut expression_to_derive = polynomial_in_delta_t.to_atom() / Atom::num(factorial_prefactor);
4470
4471    expression_to_derive = expression_to_derive
4472        .replace(GS.rescale)
4473        .with(GS.rescale_star);
4474
4475    expression_to_derive
4476}
4477
4478pub(crate) fn build_derivative_structure(
4479    singularity_order: u8,
4480    laurent_coefficient: i8,
4481    evaluator_settings: &EvaluatorSettings,
4482) -> GenericEvaluator {
4483    let expression_to_derive =
4484        build_derivative_structure_atom(singularity_order, laurent_coefficient);
4485
4486    let params = params_for_derivative_order(singularity_order);
4487
4488    GenericEvaluator::new_from_raw_params(
4489        [expression_to_derive],
4490        &params,
4491        &FunctionMap::default(),
4492        vec![],
4493        evaluator_settings.optimization_settings(),
4494        None,
4495        evaluator_settings,
4496    )
4497    .unwrap()
4498    .into_eager_only()
4499}
4500
4501fn ordered_f_derivative_params(
4502    base: Atom,
4503    derivative_vars: &[Symbol],
4504    derivative_shape: Option<&Vec<Vec<usize>>>,
4505) -> Vec<Atom> {
4506    derivative_shape
4507        .cloned()
4508        .unwrap_or_else(|| vec![vec![0; derivative_vars.len()]])
4509        .into_iter()
4510        .map(|orders| {
4511            debug_assert_eq!(orders.len(), derivative_vars.len());
4512
4513            let mut param = base.clone();
4514            for (derivative_var, order) in derivative_vars.iter().zip(orders) {
4515                for _ in 0..order {
4516                    param = param.derivative(*derivative_var);
4517                }
4518            }
4519
4520            param
4521        })
4522        .collect()
4523}
4524
4525pub(crate) fn params_for_derivative_order(singularity_order: u8) -> Vec<Atom> {
4526    let f = symbol!("f");
4527
4528    let f_0 = function!(f, GS.rescale_star);
4529    let eta_1 = function!(GS.eta, GS.rescale_star).derivative(GS.rescale_star);
4530
4531    let f_derivative_shape = shape_from_cut_cff_index(&CutCFFIndex {
4532        left_threshold_order: None,
4533        right_threshold_order: None,
4534        lu_cut_order: Some(singularity_order as usize),
4535    });
4536
4537    let f_parameters =
4538        ordered_f_derivative_params(f_0, &[GS.rescale_star], f_derivative_shape.as_ref());
4539    let mut eta_params = vec![eta_1.clone()];
4540
4541    for _ in 2..=singularity_order {
4542        let next_eta = eta_params.last().unwrap().derivative(GS.rescale_star);
4543        eta_params.push(next_eta);
4544    }
4545
4546    let mut result = vec![];
4547    result.extend(f_parameters);
4548    result.extend(eta_params);
4549    result
4550}
4551
4552pub(crate) fn params_for_iterated_threshold_ct(
4553    left_singularit_order: u8,
4554    right_singularity_order: u8,
4555) -> Vec<Atom> {
4556    let f = symbol!("f");
4557
4558    let cut_cff_index = CutCFFIndex {
4559        left_threshold_order: Some(left_singularit_order as usize),
4560        right_threshold_order: Some(right_singularity_order as usize),
4561        lu_cut_order: None,
4562    };
4563    let f_derivative_shape = shape_from_cut_cff_index(&cut_cff_index);
4564
4565    let f_base = function!(f, GS.radius_star_left, GS.radius_star_right);
4566    let derivative_vars = match (left_singularit_order > 1, right_singularity_order > 1) {
4567        (true, true) => vec![GS.radius_star_left, GS.radius_star_right],
4568        (true, false) => vec![GS.radius_star_left],
4569        (false, true) => vec![GS.radius_star_right],
4570        (false, false) => vec![],
4571    };
4572
4573    let eta_left_d1 = function!(GS.eta_left, GS.radius_star_left).derivative(GS.radius_star_left);
4574    let eta_right_d1 =
4575        function!(GS.eta_right, GS.radius_star_right).derivative(GS.radius_star_right);
4576
4577    let mut eta_left_params = vec![eta_left_d1.clone()];
4578    let mut eta_right_params = vec![eta_right_d1.clone()];
4579
4580    for _ in 2..=left_singularit_order {
4581        let next_eta_left = eta_left_params
4582            .last()
4583            .unwrap()
4584            .derivative(GS.radius_star_left);
4585
4586        eta_left_params.push(next_eta_left);
4587    }
4588
4589    for _ in 2..=right_singularity_order {
4590        let next_eta_right = eta_right_params
4591            .last()
4592            .unwrap()
4593            .derivative(GS.radius_star_right);
4594
4595        eta_right_params.push(next_eta_right);
4596    }
4597
4598    let f_product =
4599        ordered_f_derivative_params(f_base, &derivative_vars, f_derivative_shape.as_ref());
4600
4601    let mut result = vec![];
4602    result.extend(f_product);
4603    result.extend(eta_left_params);
4604    result.extend(eta_right_params);
4605    result
4606}
4607
4608#[cfg(test)]
4609mod tests {
4610    use std::{
4611        fs,
4612        path::PathBuf,
4613        time::{SystemTime, UNIX_EPOCH},
4614    };
4615
4616    use symbolica::{atom::AtomCore, function, symbol};
4617
4618    use crate::{
4619        cff::CutCFFIndex,
4620        dot,
4621        graph::parse::from_dot::IntoGraph,
4622        initialisation::test_initialise,
4623        model::InputParamCard,
4624        processes::cross_section::{EsurfaceID, RaisedEsurfaceData, RaisedEsurfaceGroup},
4625        settings::global::{GenerationSettings, OrientationPattern},
4626        utils::{GS, load_generic_model},
4627    };
4628    use linnet::half_edge::{
4629        involution::EdgeIndex,
4630        subgraph::{OrientedCut, SuBitGraph},
4631    };
4632
4633    fn threshold_association(
4634        cut_boundary_size: usize,
4635        threshold_boundary_size: usize,
4636    ) -> super::ThresholdCountertermAssociation {
4637        super::ThresholdCountertermAssociation {
4638            topological_threshold_id: super::TopologicalThresholdId(0),
4639            esurface_id: super::EsurfaceID(0),
4640            cut_boundary_edges: (0..cut_boundary_size).map(EdgeIndex::from).collect(),
4641            threshold_boundary_edges: (0..threshold_boundary_size)
4642                .map(|index| EdgeIndex::from(cut_boundary_size + index))
4643                .collect(),
4644            invariant_bound_is_applicable: true,
4645        }
4646    }
4647
4648    #[test]
4649    fn cut_groups_include_all_physical_cuts_in_a_raised_group() {
4650        test_initialise().unwrap();
4651        let raised_esurface_data = RaisedEsurfaceData {
4652            raised_groups: [
4653                RaisedEsurfaceGroup {
4654                    esurface_ids: vec![EsurfaceID(1), EsurfaceID(0)],
4655                    max_occurence: 1,
4656                },
4657                RaisedEsurfaceGroup {
4658                    esurface_ids: vec![EsurfaceID(2)],
4659                    max_occurence: 1,
4660                },
4661            ]
4662            .into_iter()
4663            .collect(),
4664            pass_two_evaluator: None,
4665        };
4666        let cut_esurface_map = [EsurfaceID(0), EsurfaceID(0)].into_iter().collect();
4667
4668        let (cut_group_data, _) = super::CutGroupData::new_from_esurface(
4669            &raised_esurface_data,
4670            &cut_esurface_map,
4671            &super::EvaluatorSettings::default(),
4672        );
4673
4674        let mut cut_partitions: Vec<Vec<super::CutId>> = cut_group_data
4675            .cut_groups
4676            .iter()
4677            .filter(|group| !group.cuts.is_empty())
4678            .map(|group| {
4679                let mut cuts = group.cuts.clone();
4680                cuts.sort_unstable_by_key(|cut| cut.0);
4681                cuts
4682            })
4683            .collect();
4684        cut_partitions.sort_unstable_by(|left, right| {
4685            left.iter()
4686                .map(|cut| cut.0)
4687                .cmp(right.iter().map(|cut| cut.0))
4688        });
4689        assert_eq!(cut_partitions, vec![vec![super::CutId(0), super::CutId(1)]]);
4690    }
4691
4692    #[test]
4693    fn scalar_cut_residue_keeps_the_propagator_i_exactly_once() {
4694        use crate::graph::{FeynmanGraph, Graph};
4695        use symbolica::atom::Atom;
4696
4697        test_initialise().unwrap();
4698        // This one-line cut is a residue-boundary diagnostic, not a 1->1
4699        // scattering-rate fixture. It deliberately bypasses the physical
4700        // process selector's requirement of at least two final particles.
4701        let graph: Graph = dot!(
4702            digraph scalar_cut_line {
4703                node [num=1];
4704                edge [particle="scalar_0", num="1𝑖"];
4705                ext [style=invis];
4706                ext -> left [is_cut=0];
4707                right -> ext [is_cut=0];
4708                left -> right;
4709            }, "scalars"
4710        )
4711        .unwrap();
4712        let mut graph = super::CrossSectionGraph::new(graph);
4713        graph.cuts = graph
4714            .graph
4715            .all_st_cuts_for_cs(
4716                graph.source_nodes.clone(),
4717                graph.target_nodes.clone(),
4718                &graph.graph.get_initial_state_tree(),
4719            )
4720            .into_iter()
4721            .map(|(left, cut, right)| super::CrossSectionCut { cut, left, right })
4722            .collect();
4723        assert_eq!(graph.cuts.len(), 1);
4724        let group = super::CutGroup {
4725            cuts: vec![super::CutId(0)],
4726            related_esurface_group: RaisedEsurfaceGroup {
4727                esurface_ids: vec![EsurfaceID(0)],
4728                max_occurence: 1,
4729            },
4730        };
4731        let conversion = group
4732            .lu_prefactor(&graph.graph, &graph.cuts)
4733            .unwrap()
4734            .replace(Atom::var(GS.rescale_star))
4735            .with(Atom::one())
4736            .replace(Atom::var(GS.hfunction_lu_cut))
4737            .with(Atom::one());
4738        let two_pi = Atom::num(2) * Atom::var(GS.pi);
4739        assert_eq!(conversion, -&two_pi * Atom::i());
4740
4741        // i/(x+i0)-i/(x-i0)=2*pi*delta(x): the normalized pole
4742        // coefficient -2*pi*i multiplies the retained numerator i once.
4743        assert_eq!(&conversion * Atom::i(), two_pi);
4744        for energy in [1, 2, 5] {
4745            let q0 = Atom::var(symbol!("scalar_cut_residue_q0"));
4746            let energy = Atom::num(energy);
4747            let positive_pole_residue = (Atom::i() / ((&q0 - &energy) * (&q0 + &energy))
4748                * (&q0 - &energy))
4749                .replace(q0)
4750                .with(energy.to_pattern());
4751            // The delta_+ energy measure is positive pi/E. Inserting an
4752            // extra cut-line i or reversing the residue sign violates this.
4753            assert_eq!(
4754                &conversion * positive_pole_residue,
4755                Atom::var(GS.pi) / energy
4756            );
4757        }
4758    }
4759
4760    #[test]
4761    fn singleton_boundary_classification_uses_generic_mass_hierarchy() {
4762        use super::ThresholdCountertermStatus;
4763        use crate::utils::F;
4764
4765        let association = threshold_association(1, 2);
4766        let tolerance = F(1.0e-12);
4767
4768        assert_eq!(
4769            association.classify_from_invariant_bounds(&F(3.0), &F(4.0), Some(F(3.0)), &tolerance,),
4770            ThresholdCountertermStatus::ProvenNonExisting,
4771        );
4772        assert_eq!(
4773            association.classify_from_invariant_bounds(&F(4.0), &F(4.0), Some(F(4.0)), &tolerance,),
4774            ThresholdCountertermStatus::AlwaysPinched,
4775        );
4776        assert_eq!(
4777            association.classify_from_invariant_bounds(&F(5.0), &F(4.0), Some(F(5.0)), &tolerance,),
4778            ThresholdCountertermStatus::PotentiallyExisting,
4779        );
4780
4781        let fixed_association = threshold_association(1, 1);
4782        assert_eq!(
4783            fixed_association.classify_from_invariant_bounds(
4784                &F(3.0),
4785                &F(4.0),
4786                Some(F(3.0)),
4787                &tolerance,
4788            ),
4789            ThresholdCountertermStatus::ProvenNonExisting,
4790        );
4791        assert_eq!(
4792            fixed_association.classify_from_invariant_bounds(
4793                &F(5.0),
4794                &F(4.0),
4795                Some(F(5.0)),
4796                &tolerance,
4797            ),
4798            ThresholdCountertermStatus::ProvenNonExisting,
4799        );
4800    }
4801
4802    #[test]
4803    fn current_model_filter_is_controlled_only_by_generation_setting() {
4804        use super::ThresholdCountertermStatus;
4805
4806        assert!(ThresholdCountertermStatus::ProvenNonExisting.is_eligible_for_generation(false));
4807        assert!(!ThresholdCountertermStatus::ProvenNonExisting.is_eligible_for_generation(true));
4808        assert!(!ThresholdCountertermStatus::AlwaysPinched.is_eligible_for_generation(false));
4809        assert!(!ThresholdCountertermStatus::NoRadialDependence.is_eligible_for_generation(false));
4810        assert!(ThresholdCountertermStatus::PotentiallyExisting.is_eligible_for_generation(true));
4811    }
4812
4813    #[test]
4814    fn multiparticle_boundary_classification_is_conservative() {
4815        use super::ThresholdCountertermStatus;
4816        use crate::utils::F;
4817
4818        let association = threshold_association(2, 2);
4819        let tolerance = F(1.0e-12);
4820
4821        assert_eq!(
4822            association.classify_from_invariant_bounds(&F(4.0), &F(4.0), Some(F(8.0)), &tolerance,),
4823            ThresholdCountertermStatus::CanBecomePinched,
4824        );
4825        assert_eq!(
4826            association.classify_from_invariant_bounds(&F(3.0), &F(5.0), Some(F(4.0)), &tolerance,),
4827            ThresholdCountertermStatus::ProvenNonExisting,
4828        );
4829        assert_eq!(
4830            association.classify_from_invariant_bounds(&F(3.0), &F(5.0), Some(F(5.0)), &tolerance,),
4831            ThresholdCountertermStatus::AlwaysPinched,
4832        );
4833        assert_eq!(
4834            association.classify_from_invariant_bounds(&F(3.0), &F(5.0), Some(F(8.0)), &tolerance,),
4835            ThresholdCountertermStatus::CanBecomePinched,
4836        );
4837        assert_eq!(
4838            association.classify_from_invariant_bounds(&F(5.0), &F(3.0), Some(F(8.0)), &tolerance,),
4839            ThresholdCountertermStatus::PotentiallyExisting,
4840        );
4841        assert_eq!(
4842            association.classify_from_invariant_bounds(&F(3.0), &F(5.0), None, &tolerance,),
4843            ThresholdCountertermStatus::PotentiallyExisting,
4844        );
4845    }
4846
4847    #[test]
4848    fn invariant_bound_rejects_mixed_or_unresolved_inputs_conservatively() {
4849        use super::ThresholdCountertermStatus;
4850        use crate::utils::F;
4851
4852        let mut association = threshold_association(2, 2);
4853        let tolerance = F(1.0e-12);
4854        association.invariant_bound_is_applicable = false;
4855        assert_eq!(
4856            association.classify_from_invariant_bounds(&F(3.0), &F(5.0), Some(F(4.0)), &tolerance,),
4857            ThresholdCountertermStatus::PotentiallyExisting,
4858        );
4859
4860        association.invariant_bound_is_applicable = true;
4861        assert_eq!(
4862            association.classify_from_invariant_bounds(
4863                &F(f64::NAN),
4864                &F(5.0),
4865                Some(F(4.0)),
4866                &tolerance,
4867            ),
4868            ThresholdCountertermStatus::PotentiallyExisting,
4869        );
4870
4871        let masses: linnet::half_edge::involution::EdgeVec<Option<F<f64>>> =
4872            vec![Some(F(3.0)), None].into();
4873        assert_eq!(
4874            super::ThresholdCountertermAssociation::mass_sum(
4875                &[EdgeIndex::from(0), EdgeIndex::from(1)],
4876                &masses,
4877            ),
4878            None,
4879        );
4880    }
4881
4882    #[test]
4883    fn singleton_threshold_must_lie_strictly_inside_available_interval() {
4884        use super::ThresholdCountertermStatus;
4885        use crate::utils::F;
4886
4887        let association = threshold_association(2, 1);
4888        let tolerance = F(1.0e-12);
4889
4890        assert_eq!(
4891            association.classify_from_invariant_bounds(&F(5.0), &F(4.0), Some(F(9.0)), &tolerance,),
4892            ThresholdCountertermStatus::ProvenNonExisting,
4893        );
4894        assert_eq!(
4895            association.classify_from_invariant_bounds(&F(3.0), &F(5.0), Some(F(9.0)), &tolerance,),
4896            ThresholdCountertermStatus::PotentiallyExisting,
4897        );
4898        assert_eq!(
4899            association.classify_from_invariant_bounds(&F(3.0), &F(9.0), Some(F(9.0)), &tolerance,),
4900            ThresholdCountertermStatus::AlwaysPinched,
4901        );
4902    }
4903
4904    fn fresh_temp_dir(name: &str) -> PathBuf {
4905        let unique = SystemTime::now()
4906            .duration_since(UNIX_EPOCH)
4907            .unwrap()
4908            .as_nanos();
4909        let path = std::env::temp_dir().join(format!(
4910            "gammalooprs-{name}-{}-{unique}",
4911            std::process::id()
4912        ));
4913        fs::create_dir_all(&path).unwrap();
4914        path
4915    }
4916
4917    #[test]
4918    fn non_master_cross_section_threshold_metadata_is_rejected_at_import() {
4919        test_initialise().unwrap();
4920        let model = load_generic_model("sm");
4921        let mut master: crate::graph::Graph =
4922            include_str!("../../../../tests/resources/graphs/ir_safe_thresholds/GL638.dot")
4923                .into_graph(&model)
4924                .unwrap();
4925        master.name = "threshold_group_master".to_string();
4926        master.group_id = Some(super::GroupId(0));
4927        master.is_group_master = true;
4928        let mut member = master.clone();
4929        member.name = "threshold_group_member".to_string();
4930        member.is_group_master = false;
4931        member.threshold_counterterms.autogenerated = false;
4932
4933        let error = match super::CrossSection::from_graph_list(
4934            "threshold_group".to_string(),
4935            vec![master, member],
4936            &model,
4937        ) {
4938            Ok(_) => panic!("non-master threshold metadata must not be interpreted graph-locally"),
4939            Err(error) => error.to_string(),
4940        };
4941        assert!(error.contains("threshold_group_member"));
4942        assert!(error.contains("threshold_group_master"));
4943        assert!(error.contains("non-master cross-section graphs is unsupported"));
4944    }
4945
4946    #[test]
4947    fn cross_section_storage_path_stays_inside_process_folder() {
4948        let temp = fresh_temp_dir("cross-section-storage-path");
4949        let cross_section = super::CrossSection::new("NLO".to_string());
4950
4951        assert_eq!(cross_section.storage_path(&temp), temp.join("NLO"));
4952        fs::remove_dir_all(temp).unwrap();
4953    }
4954
4955    #[test]
4956    fn selected_orientation_keeps_only_selected_threshold_instances_active() {
4957        use crate::graph::FeynmanGraph;
4958
4959        std::thread::Builder::new()
4960            .name("selected-orientation-threshold-test".to_string())
4961            .stack_size(64 * 1024 * 1024)
4962            .spawn(|| {
4963                test_initialise().unwrap();
4964                let model = load_generic_model("sm");
4965                let graph =
4966                    include_str!("../../../../tests/resources/graphs/ir_safe_thresholds/GL638.dot")
4967                        .into_graph(&model)
4968                        .unwrap();
4969                let mut cross_section = super::CrossSectionGraph::new(graph);
4970                let target_cut_edges =
4971                    vec![EdgeIndex::from(2), EdgeIndex::from(4), EdgeIndex::from(12)];
4972                let (left, cut, right) = cross_section
4973                    .graph
4974                    .all_st_cuts_for_cs(
4975                        cross_section.source_nodes.clone(),
4976                        cross_section.target_nodes.clone(),
4977                        &cross_section.graph.get_initial_state_tree(),
4978                    )
4979                    .into_iter()
4980                    .find(|(_, candidate, _)| {
4981                        let mut edges = cross_section
4982                            .graph
4983                            .iter_edges_of(candidate)
4984                            .map(|(_, edge_id, _)| edge_id)
4985                            .collect::<Vec<_>>();
4986                        edges.sort();
4987                        edges == target_cut_edges
4988                    })
4989                    .expect("GL638 must contain the target physical cut");
4990                cross_section
4991                    .cuts
4992                    .push(super::CrossSectionCut { cut, left, right });
4993                let settings = GenerationSettings {
4994                    orientation_pattern: OrientationPattern::from_user_pattern(
4995                        "(+,+,+,+,-,-,-,+,-,0,+,0,+,-,+)",
4996                    )
4997                    .unwrap(),
4998                    ..Default::default()
4999                };
5000                cross_section.generate_esurface_cuts();
5001                cross_section.generate_cff(&settings).unwrap();
5002
5003                let topology_candidates = cross_section
5004                    .topological_threshold_candidates(&settings)
5005                    .unwrap();
5006                let selected_orientation_energy_sets = topology_candidates
5007                    .iter()
5008                    .filter_map(|candidate| {
5009                        candidate
5010                            .selected_orientation_esurface_id
5011                            .map(|esurface_id| cross_section.threshold_energy_edges(esurface_id))
5012                    })
5013                    .collect::<Vec<_>>();
5014                for expected in [
5015                    vec![EdgeIndex::from(7), EdgeIndex::from(8)],
5016                    vec![EdgeIndex::from(8), EdgeIndex::from(12), EdgeIndex::from(14)],
5017                    vec![EdgeIndex::from(5), EdgeIndex::from(10)],
5018                    vec![EdgeIndex::from(5), EdgeIndex::from(12), EdgeIndex::from(13)],
5019                ] {
5020                    assert!(selected_orientation_energy_sets.contains(&expected));
5021                }
5022                let orientation_excluded_edges = vec![
5023                    EdgeIndex::from(8),
5024                    EdgeIndex::from(10),
5025                    EdgeIndex::from(13),
5026                    EdgeIndex::from(14),
5027                ];
5028                assert!(!selected_orientation_energy_sets.contains(&orientation_excluded_edges));
5029                assert!(topology_candidates.iter().any(|candidate| {
5030                    candidate.threshold_esurface.energies == orientation_excluded_edges
5031                        && candidate.selected_orientation_esurface_id.is_none()
5032                }));
5033
5034                // Orientation selection may leave topology-wide candidates dormant, but a surface
5035                // required by a selected-orientation instance must still exist in the CFF cache.
5036                cross_section.graph.surface_cache.esurface_cache.raw.clear();
5037                let error = cross_section
5038                    .topological_threshold_candidates(&settings)
5039                    .err()
5040                    .expect("a corrupted selected CFF cache must be rejected");
5041                assert!(
5042                    error
5043                        .to_string()
5044                        .contains("has a selected-orientation instance but is missing")
5045                );
5046            })
5047            .unwrap()
5048            .join()
5049            .unwrap();
5050    }
5051
5052    #[test]
5053    fn orientation_excluded_explicit_threshold_is_dormant_not_unmatched() {
5054        std::thread::Builder::new()
5055            .name("dormant-orientation-threshold-test".to_string())
5056            .stack_size(64 * 1024 * 1024)
5057            .spawn(|| {
5058                use crate::graph::{
5059                    FeynmanGraph,
5060                    autogen::Autogen,
5061                    threshold_counterterms::{
5062                        ThresholdCountertermCut, ThresholdCountertermMultiplier,
5063                        ThresholdCountertermSpec, ThresholdCountertermThreshold,
5064                        ThresholdCountertermVariant,
5065                    },
5066                };
5067
5068                test_initialise().unwrap();
5069                let model = load_generic_model("sm");
5070                let graph =
5071                    include_str!("../../../../tests/resources/graphs/ir_safe_thresholds/GL638.dot")
5072                        .into_graph(&model)
5073                        .unwrap();
5074                let mut cross_section = super::CrossSectionGraph::new(graph);
5075                let target_cut_edges =
5076                    vec![EdgeIndex::from(2), EdgeIndex::from(4), EdgeIndex::from(12)];
5077                let (left, cut, right) = cross_section
5078                    .graph
5079                    .all_st_cuts_for_cs(
5080                        cross_section.source_nodes.clone(),
5081                        cross_section.target_nodes.clone(),
5082                        &cross_section.graph.get_initial_state_tree(),
5083                    )
5084                    .into_iter()
5085                    .find(|(_, candidate, _)| {
5086                        let mut edges = cross_section
5087                            .graph
5088                            .iter_edges_of(candidate)
5089                            .map(|(_, edge_id, _)| edge_id)
5090                            .collect::<Vec<_>>();
5091                        edges.sort();
5092                        edges == target_cut_edges
5093                    })
5094                    .expect("GL638 must contain the target physical cut");
5095                cross_section
5096                    .cuts
5097                    .push(super::CrossSectionCut { cut, left, right });
5098
5099                let dormant_threshold_edges = vec![EdgeIndex::from(5), EdgeIndex::from(10)];
5100                cross_section.graph.threshold_counterterms =
5101                    Autogen::explicit(ThresholdCountertermSpec {
5102                        schema_version: 1,
5103                        function_map: Default::default(),
5104                        cuts: vec![ThresholdCountertermCut {
5105                            edges: target_cut_edges,
5106                            thresholds: vec![ThresholdCountertermThreshold {
5107                                edges: dormant_threshold_edges.clone(),
5108                                counterterms: vec![ThresholdCountertermVariant {
5109                                    group_id: None,
5110                                    name: Some("dormant".to_string()),
5111                                    subspace: None,
5112                                    parent_lmb: Some(vec![
5113                                        EdgeIndex(3),
5114                                        EdgeIndex(4),
5115                                        EdgeIndex(7),
5116                                        EdgeIndex(10),
5117                                    ]),
5118                                    disable: false,
5119                                    multiplier: Some(ThresholdCountertermMultiplier {
5120                                        expression: "1".to_string(),
5121                                        function_map: Default::default(),
5122                                        symmetrize: true,
5123                                        opaque_derivatives: true,
5124                                    }),
5125                                }],
5126                            }],
5127                        }],
5128                    });
5129
5130                // This orientation contains the physical cut and both left thresholds, but excludes the
5131                // right-side (5,10) threshold. Its explicit directive must remain matched and dormant.
5132                let settings = GenerationSettings {
5133                    orientation_pattern: OrientationPattern::from_user_pattern(
5134                        "(+,+,+,+,-,+,-,+,-,0,+,0,+,-,+)",
5135                    )
5136                    .unwrap(),
5137                    ..Default::default()
5138                };
5139                cross_section.generate_esurface_cuts();
5140                cross_section.generate_cff(&settings).unwrap();
5141                cross_section.build_lmbs().unwrap();
5142                cross_section.build_subspace_data().unwrap();
5143                let topology_candidates = cross_section
5144                    .topological_threshold_candidates(&settings)
5145                    .unwrap();
5146                assert!(topology_candidates.iter().any(|candidate| {
5147                    candidate.threshold_esurface.energies == dormant_threshold_edges
5148                        && candidate.selected_orientation_esurface_id.is_none()
5149                }));
5150
5151                let (resolved, _) = cross_section
5152                    .resolve_threshold_counterterm_directives(
5153                        &model,
5154                        &settings,
5155                        &crate::settings::RuntimeSettings::default(),
5156                        &topology_candidates,
5157                    )
5158                    .unwrap();
5159                assert!(resolved.variants.iter().all(|variant| {
5160                    variant.name != "dormant"
5161                        && variant.associations.iter().all(|association| {
5162                            association.threshold_edges != dormant_threshold_edges
5163                        })
5164                }));
5165                assert!(
5166                    resolved
5167                        .cross_section_cut_groups
5168                        .iter()
5169                        .any(|cut_group| !cut_group.right.is_empty()),
5170                    "the orientation retains its other right-side threshold"
5171                );
5172            })
5173            .unwrap()
5174            .join()
5175            .unwrap();
5176    }
5177
5178    #[test]
5179    fn cross_section_lmb_cut_edge_exclusion_prefers_massive_then_fermion_then_id() {
5180        test_initialise().unwrap();
5181        let mut model = load_generic_model("sm");
5182        let mut parameters = InputParamCard::from_str(
5183            include_str!("../../../../assets/models/json/sm/restrict_light_quarks_massless.json")
5184                .to_owned(),
5185            "json",
5186        )
5187        .unwrap();
5188        model.simplify(&mut parameters).unwrap();
5189        // Keep the light fermion massless to distinguish mass priority from spin priority.
5190        let graph = dot!(
5191            digraph cut_edge_priority {
5192                edge [num=1]
5193                node [num=1]
5194                A -> B [id=0 particle="g"]
5195                A -> B [id=1 particle="d"]
5196                A -> B [id=2 particle="t" mass=1]
5197                A -> B [id=3 particle="a"]
5198                A -> B [id=4 mass=0]
5199            },
5200            &model
5201        )
5202        .unwrap();
5203
5204        assert_eq!(
5205            super::CrossSectionGraph::excluded_cut_edge_for_lmb_channel_in(
5206                &graph,
5207                &[EdgeIndex::from(0), EdgeIndex::from(1), EdgeIndex::from(2)]
5208            ),
5209            Some(EdgeIndex::from(2))
5210        );
5211        assert_eq!(
5212            super::CrossSectionGraph::excluded_cut_edge_for_lmb_channel_in(
5213                &graph,
5214                &[EdgeIndex::from(0), EdgeIndex::from(1), EdgeIndex::from(3)]
5215            ),
5216            Some(EdgeIndex::from(1))
5217        );
5218        assert_eq!(
5219            super::CrossSectionGraph::excluded_cut_edge_for_lmb_channel_in(
5220                &graph,
5221                &[EdgeIndex::from(0), EdgeIndex::from(3)]
5222            ),
5223            Some(EdgeIndex::from(0))
5224        );
5225
5226        // A structural zero and a DOT-overridden mass are resolved information for current-model
5227        // invariant bounds. Keep genuinely unresolved and complex masses conservative, but do not
5228        // let a massless edge disable a proof independent of model parameters.
5229        assert_eq!(
5230            graph.underlying[EdgeIndex::from(2)]
5231                .particle
5232                .mass_atom()
5233                .to_string(),
5234            "1"
5235        );
5236        let empty: SuBitGraph = graph.underlying.empty_subgraph();
5237        let cut = super::CrossSectionCut {
5238            cut: OrientedCut {
5239                left: empty.clone(),
5240                right: empty.clone(),
5241            },
5242            left: empty.clone(),
5243            right: empty,
5244        };
5245        let association = super::ThresholdCountertermAssociation {
5246            topological_threshold_id: super::TopologicalThresholdId(0),
5247            esurface_id: super::EsurfaceID(0),
5248            cut_boundary_edges: vec![EdgeIndex::from(4)],
5249            threshold_boundary_edges: vec![EdgeIndex::from(2)],
5250            invariant_bound_is_applicable: true,
5251        };
5252        assert_eq!(
5253            association.classify_for_model(
5254                &graph,
5255                &cut,
5256                &crate::model::Model::default(),
5257                &graph.param_builder,
5258                &crate::settings::RuntimeSettings::default(),
5259                1.0e-7,
5260            ),
5261            super::ThresholdCountertermStatus::ProvenNonExisting,
5262        );
5263    }
5264
5265    #[test]
5266    fn iterated_counterterms_use_the_right_threshold_count_as_row_stride() {
5267        let two_by_three = super::IteratedCtCollection::new((0..6).collect(), 2, 3);
5268        assert_eq!(
5269            two_by_three[(
5270                super::LeftThresholdId::from(0),
5271                super::RightThresholdId::from(0)
5272            )],
5273            0
5274        );
5275        assert_eq!(
5276            two_by_three[(
5277                super::LeftThresholdId::from(0),
5278                super::RightThresholdId::from(2)
5279            )],
5280            2
5281        );
5282        assert_eq!(
5283            two_by_three[(
5284                super::LeftThresholdId::from(1),
5285                super::RightThresholdId::from(0)
5286            )],
5287            3
5288        );
5289        assert_eq!(
5290            two_by_three[(
5291                super::LeftThresholdId::from(1),
5292                super::RightThresholdId::from(2)
5293            )],
5294            5
5295        );
5296
5297        let three_by_two = super::IteratedCtCollection::new((0..6).collect(), 3, 2);
5298        assert_eq!(
5299            three_by_two[(
5300                super::LeftThresholdId::from(0),
5301                super::RightThresholdId::from(1)
5302            )],
5303            1
5304        );
5305        assert_eq!(
5306            three_by_two[(
5307                super::LeftThresholdId::from(1),
5308                super::RightThresholdId::from(0)
5309            )],
5310            2
5311        );
5312        assert_eq!(
5313            three_by_two[(
5314                super::LeftThresholdId::from(2),
5315                super::RightThresholdId::from(1)
5316            )],
5317            5
5318        );
5319    }
5320
5321    #[test]
5322    fn directive_resolution_defaults_and_explicit_lists_are_authoritative() {
5323        use crate::{
5324            graph::threshold_counterterms::{
5325                ThresholdCountertermCut, ThresholdCountertermSpec, ThresholdCountertermThreshold,
5326                ThresholdCountertermVariant,
5327            },
5328            processes::threshold_counterterms::ThresholdCountertermOrigin,
5329        };
5330
5331        let cut_edges = vec![EdgeIndex::from(2), EdgeIndex::from(4)];
5332        let threshold_edges = vec![EdgeIndex::from(7), EdgeIndex::from(8)];
5333        let mut spec = ThresholdCountertermSpec {
5334            schema_version: 1,
5335            function_map: Default::default(),
5336            cuts: vec![ThresholdCountertermCut {
5337                edges: cut_edges.clone(),
5338                thresholds: vec![ThresholdCountertermThreshold {
5339                    edges: threshold_edges.clone(),
5340                    counterterms: Vec::new(),
5341                }],
5342            }],
5343        };
5344
5345        let implicit = super::CrossSectionGraph::configured_threshold_variants(
5346            &spec,
5347            &cut_edges,
5348            &threshold_edges,
5349        );
5350        assert_eq!(implicit.len(), 1);
5351        assert_eq!(implicit[0].0.name.as_deref(), Some("default"));
5352        assert_eq!(implicit[0].1, ThresholdCountertermOrigin::Autogenerated);
5353
5354        spec.cuts[0].thresholds[0].counterterms = vec![
5355            ThresholdCountertermVariant {
5356                group_id: None,
5357                name: Some("disabled".to_string()),
5358                subspace: None,
5359                parent_lmb: Some(vec![EdgeIndex(7), EdgeIndex(8)]),
5360                disable: true,
5361                multiplier: None,
5362            },
5363            ThresholdCountertermVariant {
5364                group_id: None,
5365                name: Some("duplicate".to_string()),
5366                subspace: Some(vec![EdgeIndex::from(7)]),
5367                parent_lmb: Some(vec![EdgeIndex(7), EdgeIndex(8)]),
5368                disable: false,
5369                multiplier: None,
5370            },
5371        ];
5372        let explicit = super::CrossSectionGraph::configured_threshold_variants(
5373            &spec,
5374            &cut_edges,
5375            &threshold_edges,
5376        );
5377        assert_eq!(explicit.len(), 2);
5378        assert!(explicit[0].0.disable);
5379        assert_eq!(explicit[1].0.name.as_deref(), Some("duplicate"));
5380        assert!(
5381            explicit
5382                .iter()
5383                .all(|(_, origin)| *origin == ThresholdCountertermOrigin::Explicit)
5384        );
5385    }
5386
5387    #[test]
5388    fn explicit_group_conflicts_report_all_members_across_cut_sides() {
5389        use itertools::Itertools;
5390
5391        std::thread::Builder::new()
5392            .name("threshold-group-conflicts".to_string())
5393            .stack_size(64 * 1024 * 1024)
5394            .spawn(|| {
5395                use crate::graph::{
5396                    FeynmanGraph,
5397                    autogen::Autogen,
5398                    threshold_counterterms::{
5399                        ThresholdCountertermCut, ThresholdCountertermSpec,
5400                        ThresholdCountertermThreshold, ThresholdCountertermVariant,
5401                    },
5402                };
5403
5404                test_initialise().unwrap();
5405                let model = load_generic_model("sm");
5406                let graph =
5407                    include_str!("../../../../tests/resources/graphs/ir_safe_thresholds/GL297.dot")
5408                        .into_graph(&model)
5409                        .unwrap();
5410                let mut graph = super::CrossSectionGraph::new(graph);
5411                let declarations = [
5412                    ([2, 4, 9], [5, 7], [4, 5, 9, 11], 5, "left"),
5413                    ([2, 6, 7], [3, 9], [3, 6, 7, 11], 3, "right"),
5414                ];
5415                graph.cuts = graph
5416                    .graph
5417                    .all_st_cuts_for_cs(
5418                        graph.source_nodes.clone(),
5419                        graph.target_nodes.clone(),
5420                        &graph.graph.get_initial_state_tree(),
5421                    )
5422                    .into_iter()
5423                    .filter(|(_, cut, _)| {
5424                        let edges = graph
5425                            .graph
5426                            .iter_edges_of(cut)
5427                            .map(|(_, edge, _)| edge.0)
5428                            .sorted()
5429                            .collect_vec();
5430                        declarations.iter().any(|(cut, ..)| edges.as_slice() == cut)
5431                    })
5432                    .map(|(left, cut, right)| super::CrossSectionCut { cut, left, right })
5433                    .collect();
5434                assert_eq!(graph.cuts.len(), 2);
5435                graph.graph.threshold_counterterms = Autogen::explicit(ThresholdCountertermSpec {
5436                    schema_version: 1,
5437                    function_map: Default::default(),
5438                    cuts: declarations
5439                        .into_iter()
5440                        .map(
5441                            |(cut, threshold, parent, active, side)| ThresholdCountertermCut {
5442                                edges: cut.into_iter().map(EdgeIndex).collect(),
5443                                thresholds: vec![ThresholdCountertermThreshold {
5444                                    edges: threshold.into_iter().map(EdgeIndex).collect(),
5445                                    counterterms: (0..2)
5446                                        .map(|group_id| ThresholdCountertermVariant {
5447                                            name: Some(format!("{side}_group_{group_id}")),
5448                                            group_id: Some(group_id),
5449                                            subspace: Some(vec![EdgeIndex(active)]),
5450                                            parent_lmb: Some(
5451                                                parent.into_iter().map(EdgeIndex).collect(),
5452                                            ),
5453                                            disable: true,
5454                                            multiplier: None,
5455                                        })
5456                                        .collect(),
5457                                }],
5458                            },
5459                        )
5460                        .collect(),
5461                });
5462                let settings = GenerationSettings {
5463                    orientation_pattern: OrientationPattern::from_user_pattern(
5464                        "(+,+,-,+,+,+,+,-,0,-,0,-,-,+,+)",
5465                    )
5466                    .unwrap(),
5467                    ..Default::default()
5468                };
5469                graph.generate_esurface_cuts();
5470                graph.generate_cff(&settings).unwrap();
5471                graph.build_lmbs().unwrap();
5472                graph.build_subspace_data().unwrap();
5473                let topology = graph.topological_threshold_candidates(&settings).unwrap();
5474                let error = graph
5475                    .resolve_threshold_counterterm_directives(
5476                        &model,
5477                        &settings,
5478                        &crate::settings::RuntimeSettings::default(),
5479                        &topology,
5480                    )
5481                    .expect_err("explicit groups cannot join different cut-side cycles");
5482                let diagnostic = error.to_string();
5483                for expected in [
5484                    "group_id=0",
5485                    "group_id=1",
5486                    "left_group_0",
5487                    "right_group_0",
5488                    "left_group_1",
5489                    "right_group_1",
5490                    "signed_cycles=",
5491                    "parent=",
5492                    "cut [2, 4, 9]",
5493                    "cut [2, 6, 7]",
5494                    "disabled=true",
5495                ] {
5496                    assert!(
5497                        diagnostic.contains(expected),
5498                        "missing {expected}: {diagnostic}"
5499                    );
5500                }
5501            })
5502            .unwrap()
5503            .join()
5504            .unwrap();
5505    }
5506
5507    #[test]
5508    fn raised_member_compatibility_uses_directives_and_embedding_not_provenance() {
5509        use crate::{
5510            graph::{Graph, LMBext},
5511            momentum::sample::SubspaceData,
5512            processes::threshold_counterterms::ThresholdCountertermOrigin,
5513        };
5514
5515        test_initialise().unwrap();
5516        let graph: Graph = dot!(digraph raised_compatibility {
5517            ext [style=invis]
5518            edge [num=1 mass=0]
5519            node [num=1]
5520            ext->v1:0 [id=0]
5521            v1->v2 [id=1]
5522            v2->v1 [id=2]
5523            v2->v3 [id=3]
5524            v3->v2 [id=4]
5525            ext->v3:1 [id=5]
5526        })
5527        .unwrap();
5528        let all_lmbs = graph.generate_loop_momentum_bases();
5529        let subspaces = all_lmbs
5530            .iter_enumerated()
5531            .filter_map(|(lmb_index, _)| {
5532                SubspaceData::new_from_parent_basis_edges(
5533                    &[EdgeIndex(1)],
5534                    &graph.full_filter(),
5535                    lmb_index,
5536                    &graph,
5537                    &all_lmbs,
5538                )
5539                .ok()
5540            })
5541            .collect::<Vec<_>>();
5542        let (subspace, raised_subspace) = subspaces
5543            .iter()
5544            .enumerate()
5545            .flat_map(|(left_index, left)| {
5546                subspaces
5547                    .iter()
5548                    .skip(left_index + 1)
5549                    .map(move |right| (left, right))
5550            })
5551            .find(|(left, right)| {
5552                left.parent_lmb_index() != right.parent_lmb_index()
5553                    && left.has_equivalent_embedding(right, &all_lmbs)
5554            })
5555            .expect("raised fixture must have equivalent distinct parent embeddings");
5556        let requested = subspace.iter_basis_edges(&all_lmbs).collect::<Vec<_>>();
5557        let first = super::ResolvedThresholdCountertermDraft {
5558            name: "v".to_string(),
5559            group_id: None,
5560            origin: ThresholdCountertermOrigin::Explicit,
5561            disable: false,
5562            requested_subspace: Some(requested),
5563            requested_parent_lmb: Some(subspace.get_lmb(&all_lmbs).loop_edges.raw.clone()),
5564            multiplier: None,
5565            subspace: subspace.clone(),
5566            status: super::ThresholdCountertermStatus::PotentiallyExisting,
5567        };
5568        let mut raised_member = first.clone();
5569        raised_member.origin = ThresholdCountertermOrigin::Autogenerated;
5570        raised_member.status = super::ThresholdCountertermStatus::ProvenNonExisting;
5571        raised_member.requested_subspace = None;
5572        raised_member.requested_parent_lmb = Some(
5573            raised_subspace
5574                .get_lmb(&all_lmbs)
5575                .loop_edges
5576                .iter()
5577                .copied()
5578                .collect(),
5579        );
5580        raised_member.subspace = raised_subspace.clone();
5581
5582        // Provenance and syntactic requests live on each physical declaration. Raised-member
5583        // compatibility is semantic, so equivalent resolved embeddings remain compatible.
5584        assert!(
5585            super::CrossSectionGraph::threshold_subspaces_are_equivalent(
5586                subspace,
5587                raised_subspace,
5588                &all_lmbs,
5589            )
5590        );
5591        assert!(first.is_compatible_with(&raised_member, &all_lmbs));
5592        raised_member.name = "different".to_string();
5593        assert!(!first.is_compatible_with(&raised_member, &all_lmbs));
5594    }
5595
5596    #[test]
5597    fn threshold_radial_power_uses_each_variant_loop_count() {
5598        let one_loop = super::CrossSectionGraph::threshold_rstar_power(1, false);
5599        let two_loop = super::CrossSectionGraph::threshold_rstar_power(2, true);
5600
5601        assert!(
5602            (one_loop - super::Atom::var(GS.radius_star_left).pow(2))
5603                .expand()
5604                .is_zero()
5605        );
5606        assert!(
5607            (two_loop - super::Atom::var(GS.radius_star_right).pow(5))
5608                .expand()
5609                .is_zero()
5610        );
5611    }
5612
5613    #[test]
5614    fn max_dual_size_for_cut_cff_indices_tracks_mixed_threshold_shapes() {
5615        let cut_cff_indices = [
5616            CutCFFIndex {
5617                left_threshold_order: None,
5618                right_threshold_order: None,
5619                lu_cut_order: Some(2),
5620            },
5621            CutCFFIndex {
5622                left_threshold_order: Some(2),
5623                right_threshold_order: Some(2),
5624                lu_cut_order: Some(2),
5625            },
5626        ];
5627
5628        assert_eq!(
5629            super::max_dual_size_for_cut_cff_indices(cut_cff_indices.iter()),
5630            8
5631        );
5632    }
5633
5634    #[test]
5635    fn single_threshold_params_follow_effective_f_then_eta_contract() {
5636        let params = super::params_for_derivative_order(3);
5637        let f = symbol!("f");
5638        let f_base = function!(f, GS.rescale_star);
5639        let eta_base = function!(GS.eta, GS.rescale_star);
5640
5641        let expected = [
5642            f_base.clone(),
5643            f_base.clone().derivative(GS.rescale_star),
5644            f_base
5645                .derivative(GS.rescale_star)
5646                .derivative(GS.rescale_star),
5647            eta_base.clone().derivative(GS.rescale_star),
5648            eta_base
5649                .clone()
5650                .derivative(GS.rescale_star)
5651                .derivative(GS.rescale_star),
5652            eta_base
5653                .derivative(GS.rescale_star)
5654                .derivative(GS.rescale_star)
5655                .derivative(GS.rescale_star),
5656        ]
5657        .into_iter()
5658        .map(|atom| atom.to_string())
5659        .collect::<Vec<_>>();
5660
5661        let actual = params
5662            .iter()
5663            .map(|atom| atom.to_string())
5664            .collect::<Vec<_>>();
5665
5666        assert_eq!(actual, expected);
5667    }
5668
5669    #[test]
5670    fn iterated_threshold_f_params_follow_mixed_left_right_shape_order() {
5671        let params = super::params_for_iterated_threshold_ct(2, 2);
5672        let f = symbol!("f");
5673        let base = function!(f, GS.radius_star_left, GS.radius_star_right);
5674        let expected = [
5675            base.clone(),
5676            base.clone().derivative(GS.radius_star_left),
5677            base.clone().derivative(GS.radius_star_right),
5678            base.derivative(GS.radius_star_left)
5679                .derivative(GS.radius_star_right),
5680        ]
5681        .into_iter()
5682        .map(|atom| atom.to_string())
5683        .collect::<Vec<_>>();
5684        let actual = params[..expected.len()]
5685            .iter()
5686            .map(|atom| atom.to_string())
5687            .collect::<Vec<_>>();
5688
5689        assert_eq!(actual, expected);
5690    }
5691
5692    #[test]
5693    fn iterated_threshold_params_append_left_then_right_eta_families() {
5694        let params = super::params_for_iterated_threshold_ct(2, 2);
5695
5696        let expected = [
5697            function!(GS.eta_left, GS.radius_star_left).derivative(GS.radius_star_left),
5698            function!(GS.eta_left, GS.radius_star_left)
5699                .derivative(GS.radius_star_left)
5700                .derivative(GS.radius_star_left),
5701            function!(GS.eta_right, GS.radius_star_right).derivative(GS.radius_star_right),
5702            function!(GS.eta_right, GS.radius_star_right)
5703                .derivative(GS.radius_star_right)
5704                .derivative(GS.radius_star_right),
5705        ]
5706        .into_iter()
5707        .map(|atom| atom.to_string())
5708        .collect::<Vec<_>>();
5709
5710        let actual = params[4..]
5711            .iter()
5712            .map(|atom| atom.to_string())
5713            .collect::<Vec<_>>();
5714
5715        assert_eq!(actual, expected);
5716    }
5717
5718    #[test]
5719    fn iterated_threshold_helpers_match_signed_nonlinear_pole_distributions() {
5720        use symbolica::atom::Atom;
5721
5722        test_initialise().unwrap();
5723        // For x=r-r*, take f=2+7x+11x²/2 and eta=2x+3x²/2+5x³/6.
5724        // Direct meromorphic residues of f/eta^n for n=1,2,3 are 1,1,-3/4.
5725        // The remaining Laurent coefficients are (1/2) for n=2 and
5726        // (5/16,1/4) for n=3. At r=2,r*=1 and dampers (3,5), their
5727        // principal-value helpers are 4/3,17/9,-89/216 before r^(1-3L).
5728        // These fixed scalar jets are not a graph or loop-momentum numerator.
5729        let residues = [Atom::one(), Atom::one(), Atom::num((-3, 4))];
5730        let principal_values = [Atom::num((4, 3)), Atom::num((17, 9)), Atom::num((-89, 216))];
5731        let f_jets = [2_i64, 7, 11];
5732        let eta_jets = [2_i64, 3, 5];
5733        let f = symbol!("f");
5734        for left_order in 1..=3_u8 {
5735            for right_order in 1..=3_u8 {
5736                let (_, helper) = super::CrossSectionGraph::iterated_th_prefactor_helper_atoms(
5737                    left_order,
5738                    right_order,
5739                    1,
5740                    2,
5741                    true,
5742                );
5743                let mut sampled = helper;
5744                // A separable bivariate test function fixes every mixed jet
5745                // independently of the production parameter-layout helpers.
5746                for (left_degree, left_jet) in f_jets.iter().enumerate() {
5747                    for (right_degree, right_jet) in f_jets.iter().enumerate() {
5748                        let mut derivative =
5749                            function!(f, GS.radius_star_left, GS.radius_star_right);
5750                        for _ in 0..left_degree {
5751                            derivative = derivative.derivative(GS.radius_star_left);
5752                        }
5753                        for _ in 0..right_degree {
5754                            derivative = derivative.derivative(GS.radius_star_right);
5755                        }
5756                        sampled = sampled
5757                            .replace(derivative)
5758                            .with(Atom::num(left_jet * right_jet));
5759                    }
5760                }
5761                for (eta, radius_star) in [
5762                    (GS.eta_left, GS.radius_star_left),
5763                    (GS.eta_right, GS.radius_star_right),
5764                ] {
5765                    let mut derivative = function!(eta, radius_star);
5766                    for jet in eta_jets {
5767                        derivative = derivative.derivative(radius_star);
5768                        sampled = sampled.replace(derivative.clone()).with(Atom::num(jet));
5769                    }
5770                }
5771                for (parameter, value) in [
5772                    (GS.radius_left, 2),
5773                    (GS.radius_right, 2),
5774                    (GS.radius_star_left, 1),
5775                    (GS.radius_star_right, 1),
5776                    (GS.uv_damp_plus_left, 3),
5777                    (GS.uv_damp_plus_right, 3),
5778                    (GS.uv_damp_minus_left, 5),
5779                    (GS.uv_damp_minus_right, 5),
5780                    (GS.pi, 3),
5781                ] {
5782                    sampled = sampled.replace(parameter).with(Atom::num(value));
5783                }
5784                for (left_integrated, right_integrated) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
5785                    let actual = sampled
5786                        .replace(GS.hfunction_left_th)
5787                        .with(Atom::num(left_integrated))
5788                        .replace(GS.hfunction_right_th)
5789                        .with(Atom::num(right_integrated));
5790                    // Sokhotski-Plemelj fixes the helper signs because runtime
5791                    // subtracts each single CT and adds their intersection.
5792                    // The mixed terms have opposite imaginary signs, while
5793                    // the two delta terms multiply to positive pi².
5794                    let left = (&principal_values[left_order as usize - 1]
5795                        - Atom::i()
5796                            * Atom::num(3 * left_integrated)
5797                            * &residues[left_order as usize - 1])
5798                        * Atom::num((1, 4));
5799                    let right = (&principal_values[right_order as usize - 1]
5800                        + Atom::i()
5801                            * Atom::num(3 * right_integrated)
5802                            * &residues[right_order as usize - 1])
5803                        * Atom::num((1, 32));
5804                    assert_eq!(
5805                        actual,
5806                        left * right,
5807                        "orders {left_order}x{right_order}, integrated {left_integrated}/{right_integrated}"
5808                    );
5809                }
5810            }
5811        }
5812    }
5813
5814    #[test]
5815    fn iterated_threshold_helper_atom_matches_its_left_right_parameter_families() {
5816        std::thread::Builder::new()
5817            .name("iterated-threshold-helper-test".to_string())
5818            .stack_size(16 * 1024 * 1024)
5819            .spawn(|| {
5820                test_initialise().unwrap();
5821
5822                let mut fn_map = super::FunctionMap::new();
5823                fn_map
5824                    .add_aliases([(
5825                        GS.pi.into(),
5826                        super::Atom::num(
5827                            super::Rational::try_from(std::f64::consts::PI).unwrap(),
5828                        ),
5829                    )])
5830                    .unwrap();
5831
5832                for left_order in 1..=3 {
5833                    for right_order in 1..=3 {
5834                        let (pieces, legacy) =
5835                            super::CrossSectionGraph::iterated_th_prefactor_helper_atoms(
5836                            left_order,
5837                            right_order,
5838                            1,
5839                            1,
5840                            true,
5841                        );
5842                        let params = super::CrossSectionGraph::iterated_th_prefactor_helper_params(
5843                            left_order,
5844                            right_order,
5845                        );
5846
5847                        super::GenericEvaluator::new_from_raw_params(
5848                            [
5849                                legacy,
5850                                pieces.local_local,
5851                                pieces.local_integrated,
5852                                pieces.integrated_local,
5853                                pieces.integrated_integrated,
5854                            ],
5855                            &params,
5856                            &fn_map,
5857                            vec![],
5858                            super::OptimizationSettings::default(),
5859                            None,
5860                            &super::EvaluatorSettings::default(),
5861                        )
5862                        .unwrap_or_else(|error| {
5863                            panic!(
5864                                "iterated {left_order}x{right_order} threshold helper atom and parameter families must agree: {error}"
5865                            )
5866                        });
5867                    }
5868                }
5869            })
5870            .expect("iterated threshold-helper test thread must start")
5871            .join()
5872            .expect("iterated threshold-helper test thread must finish successfully");
5873    }
5874
5875    #[test]
5876    fn single_threshold_helper_keeps_raised_poles_in_the_local_piece() {
5877        let (with_integrated, legacy) =
5878            super::CrossSectionGraph::single_th_prefactor_helper_atoms(3, 1, false, true);
5879        let (without_integrated, _) =
5880            super::CrossSectionGraph::single_th_prefactor_helper_atoms(3, 1, false, false);
5881
5882        assert!(
5883            (with_integrated.local.clone() - without_integrated.local)
5884                .expand()
5885                .is_zero()
5886        );
5887        assert!(without_integrated.integrated.is_zero());
5888        assert!(!with_integrated.integrated.is_zero());
5889        assert!(
5890            (legacy - with_integrated.local - with_integrated.integrated)
5891                .expand()
5892                .is_zero()
5893        );
5894    }
5895
5896    #[test]
5897    fn iterated_threshold_helper_pieces_sum_to_the_legacy_product() {
5898        let (left, _) =
5899            super::CrossSectionGraph::single_th_prefactor_helper_atoms(3, 1, false, true);
5900        let left_local = left
5901            .local
5902            .replace(GS.eta)
5903            .with(super::Atom::var(GS.eta_left));
5904        let left_integrated = left
5905            .integrated
5906            .replace(GS.eta)
5907            .with(super::Atom::var(GS.eta_left));
5908        let (right, _) =
5909            super::CrossSectionGraph::single_th_prefactor_helper_atoms(2, 1, true, true);
5910        let right_local = right
5911            .local
5912            .replace(GS.eta)
5913            .with(super::Atom::var(GS.eta_right));
5914        let right_integrated = right
5915            .integrated
5916            .replace(GS.eta)
5917            .with(super::Atom::var(GS.eta_right));
5918
5919        let legacy_product = ((left_local + left_integrated) * (right_local + right_integrated))
5920            .expand()
5921            .replace_multiple(super::CrossSectionGraph::fuse_left_right_replacement());
5922        let (pieces, legacy) =
5923            super::CrossSectionGraph::iterated_th_prefactor_helper_atoms(3, 2, 1, 1, true);
5924        let piece_sum = pieces.local_local
5925            + pieces.local_integrated
5926            + pieces.integrated_local
5927            + pieces.integrated_integrated;
5928
5929        assert!((legacy_product - &legacy).expand().is_zero());
5930        assert!((legacy - piece_sum).expand().is_zero());
5931    }
5932
5933    #[test]
5934    fn iterated_threshold_f_params_follow_active_single_axis_shape_order() {
5935        let params = super::params_for_iterated_threshold_ct(1, 3);
5936        let f = symbol!("f");
5937        let base = function!(f, GS.radius_star_left, GS.radius_star_right);
5938        let expected = [
5939            base.clone(),
5940            base.clone().derivative(GS.radius_star_right),
5941            base.derivative(GS.radius_star_right)
5942                .derivative(GS.radius_star_right),
5943        ]
5944        .into_iter()
5945        .map(|atom| atom.to_string())
5946        .collect::<Vec<_>>();
5947        let actual = params[..expected.len()]
5948            .iter()
5949            .map(|atom| atom.to_string())
5950            .collect::<Vec<_>>();
5951
5952        assert_eq!(actual, expected);
5953    }
5954}