Skip to main content

gammalooprs/subtraction/
lu_counterterm.rs

1use core::f64;
2use std::{collections::BTreeMap, path::Path};
3
4use bincode_trait_derive::{Decode, Encode};
5use color_eyre::Result;
6use eyre::eyre;
7use itertools::Itertools;
8use linnet::half_edge::involution::{EdgeIndex, EdgeVec, Orientation};
9use spenso::algebra::complex::Complex;
10use symbolica::domains::{
11    dual::{DualNumberStructure, HyperDual},
12    float::{Real, RealLike},
13};
14use tracing::{debug, warn};
15use typed_index_collections::TiVec;
16
17use crate::{
18    GammaLoopContext,
19    cff::{
20        CutCFFIndex,
21        esurface::{
22            Esurface, EsurfaceCollection, EsurfaceID, ExistingEsurfaceId,
23            esurface_value_is_strictly_inside,
24        },
25        expression::OrientationID,
26    },
27    graph::{Graph, LmbIndex, LoopMomentumBasis},
28    integrands::{
29        evaluation::EvaluationMetaData,
30        process::{
31            GenericEvaluator, ParamBuilder, ThresholdParams,
32            evaluators::{
33                EvaluatorStack, SingleOrAllOrientations, evaluate_evaluator,
34                evaluate_evaluator_single,
35            },
36            param_builder::LUParams,
37        },
38    },
39    momentum::{
40        Rotation, SignOrZero, ThreeMomentum,
41        sample::{
42            ExternalFourMomenta, ExternalIndex, LoopIndex, LoopMomenta, MomentumSample,
43            SubspaceData,
44        },
45        signature::LoopSignature,
46    },
47    processes::{
48        CutGroupId, EvaluatorBuildTimings, IteratedCtCollection, LUCounterTermData,
49        LeftThresholdId, RightThresholdId, build_derivative_structure,
50    },
51    settings::{GlobalSettings, RuntimeSettings, global::FrozenCompilationMode},
52    subtraction::{
53        RstarTDependenceEvaluator, RstarTDependenceInput, evaluate_integrated_ct_normalisation,
54        evaluate_integrated_ct_normalisation_dual, evaluate_uv_damper, evaluate_uv_damper_dual,
55        overlap_subspace::{self, OverlapGroup, OverlapInput, OverlapStructure},
56    },
57    utils::{
58        F, FloatLike,
59        hyperdual_utils::{
60            DualOrNot, dualize_dual_t_to_dual_r_t, extract_coefficient_t_duals,
61            extract_t_derivatives, extract_t_derivatives_complex, new_constant,
62            shape_from_cut_cff_index, simple_n_deriv_shape, variable_indices_from_cut_cff_index,
63        },
64        newton_solver::{NewtonIterationResult, RadialRootDiagnostics, RadialRootIdentity},
65    },
66};
67
68fn zero_dual_or_not_complex<T: FloatLike>(order: usize, zero: &F<T>) -> DualOrNot<Complex<F<T>>> {
69    if order == 0 {
70        DualOrNot::NonDual(Complex::new_re(zero.clone()))
71    } else {
72        DualOrNot::Dual(HyperDual::new(simple_n_deriv_shape(order)))
73    }
74}
75
76fn negate_dual_or_not_complex<T: FloatLike>(
77    value: DualOrNot<Complex<F<T>>>,
78) -> DualOrNot<Complex<F<T>>> {
79    match value {
80        DualOrNot::Dual(dual) => DualOrNot::Dual(-dual),
81        DualOrNot::NonDual(non_dual) => DualOrNot::NonDual(-non_dual),
82    }
83}
84
85fn multiply_dual_or_not_complex<T: FloatLike>(
86    lhs: DualOrNot<Complex<F<T>>>,
87    rhs: &DualOrNot<Complex<F<T>>>,
88) -> DualOrNot<Complex<F<T>>> {
89    match (lhs, rhs) {
90        (DualOrNot::NonDual(lhs), DualOrNot::NonDual(rhs)) => DualOrNot::NonDual(lhs * rhs),
91        (DualOrNot::Dual(lhs), DualOrNot::Dual(rhs)) => DualOrNot::Dual(lhs * rhs.clone()),
92        (DualOrNot::Dual(lhs), DualOrNot::NonDual(rhs)) => {
93            let rhs_dual = new_constant(&lhs, rhs);
94            DualOrNot::Dual(lhs * rhs_dual)
95        }
96        (DualOrNot::NonDual(lhs), DualOrNot::Dual(rhs)) => {
97            let lhs_dual = new_constant(rhs, &lhs);
98            DualOrNot::Dual(lhs_dual * rhs.clone())
99        }
100    }
101}
102
103fn plain_t_dual_or_scalar_complex<T: FloatLike>(
104    value: &DualOrNot<Complex<F<T>>>,
105    t_variable: Option<usize>,
106) -> DualOrNot<Complex<F<T>>> {
107    match value {
108        DualOrNot::Dual(dual) => {
109            if let Some(t_variable) = t_variable {
110                DualOrNot::Dual(extract_zero_threshold_coefficient_t_dual(dual, t_variable))
111            } else {
112                DualOrNot::NonDual(dual.values[0].clone())
113            }
114        }
115        DualOrNot::NonDual(value) => DualOrNot::NonDual(value.clone()),
116    }
117}
118
119fn extract_zero_threshold_coefficient_t_dual<
120    T: Clone + spenso::algebra::algebraic_traits::RefZero + Default,
121>(
122    dual: &HyperDual<T>,
123    t_variable: usize,
124) -> HyperDual<T> {
125    let (coefficient_orders, coefficient_duals) = extract_coefficient_t_duals(dual, t_variable);
126    let zero_index = coefficient_orders
127        .iter()
128        .position(|orders| orders.iter().all(|order| *order == 0))
129        .expect("Could not find zero-threshold coefficient in mixed dual");
130
131    coefficient_duals[zero_index].clone()
132}
133
134fn embed_t_dual_in_target_shape<T: FloatLike>(
135    t_dual: &HyperDual<F<T>>,
136    target_shape: &HyperDual<F<T>>,
137    t_variable: Option<usize>,
138) -> HyperDual<F<T>> {
139    match t_variable {
140        Some(t_variable) => {
141            dualize_dual_t_to_dual_r_t(t_dual.clone(), target_shape.clone(), t_variable)
142        }
143        None => new_constant(target_shape, &t_dual.values[0]),
144    }
145}
146
147fn activate_threshold_variable_in_target_shape<T: FloatLike>(
148    dual: &mut HyperDual<F<T>>,
149    variable: Option<usize>,
150) {
151    let Some(variable) = variable else {
152        return;
153    };
154
155    let n_variables = dual.get_shape()[0].len();
156    let mut derivative_shape = vec![0; n_variables];
157    derivative_shape[variable] = 1;
158
159    let derivative_index = dual
160        .get_shape()
161        .iter()
162        .position(|shape| shape == &derivative_shape)
163        .expect("Could not find threshold-variable derivative slot in target shape");
164
165    dual.values[derivative_index] = dual.values[0].one();
166}
167
168fn embed_real_dual_or_not_in_target_shape<T: FloatLike>(
169    value: &DualOrNot<F<T>>,
170    target_shape: &Option<HyperDual<F<T>>>,
171    t_variable: Option<usize>,
172) -> DualOrNot<F<T>> {
173    match (value, target_shape) {
174        (DualOrNot::Dual(dual), Some(target_shape)) => {
175            DualOrNot::Dual(embed_t_dual_in_target_shape(dual, target_shape, t_variable))
176        }
177        (DualOrNot::NonDual(value), Some(target_shape)) => {
178            DualOrNot::Dual(new_constant(target_shape, value))
179        }
180        _ => value.clone(),
181    }
182}
183
184fn embedded_dual_loop_momenta_for_cut_cff_index<T: FloatLike>(
185    sample: &MomentumSample<T>,
186    target_shape: &Option<HyperDual<F<T>>>,
187    t_variable: Option<usize>,
188) -> Option<LoopMomenta<HyperDual<F<T>>>> {
189    match target_shape {
190        Some(target_shape) => Some(match sample.sample.dual_loop_moms.as_ref() {
191            Some(dual_loop_moms) => dual_loop_moms
192                .iter()
193                .map(|momentum| {
194                    momentum.map_ref(&|component| {
195                        embed_t_dual_in_target_shape(component, target_shape, t_variable)
196                    })
197                })
198                .collect(),
199            None => sample
200                .loop_moms()
201                .iter()
202                .map(|momentum| {
203                    momentum.map_ref(&|component| new_constant(target_shape, component))
204                })
205                .collect(),
206        }),
207        None => sample.sample.dual_loop_moms.clone(),
208    }
209}
210
211fn extend_plain_helper_real_params<T: FloatLike>(
212    params: &mut Vec<Complex<F<T>>>,
213    value: &DualOrNot<F<T>>,
214    t_variable: Option<usize>,
215) {
216    match value {
217        DualOrNot::Dual(dual) => {
218            if let Some(t_variable) = t_variable {
219                params.extend(
220                    extract_t_derivatives(extract_zero_threshold_coefficient_t_dual(
221                        dual, t_variable,
222                    ))
223                    .into_iter()
224                    .map(Complex::new_re),
225                );
226            } else {
227                params.push(Complex::new_re(dual.values[0].clone()));
228            }
229        }
230        DualOrNot::NonDual(value) => params.push(Complex::new_re(value.clone())),
231    }
232}
233
234fn extend_extracted_f_params<T: FloatLike>(
235    params: &mut Vec<Complex<F<T>>>,
236    value: &DualOrNot<Complex<F<T>>>,
237    t_variable: Option<usize>,
238) {
239    match value {
240        DualOrNot::Dual(dual) => {
241            if let Some(t_variable) = t_variable {
242                let (_, coefficient_duals) = extract_coefficient_t_duals(dual, t_variable);
243                for coefficient_dual in coefficient_duals {
244                    params.extend(extract_t_derivatives_complex(coefficient_dual));
245                }
246            } else {
247                params.extend(dual.values.iter().cloned());
248            }
249        }
250        DualOrNot::NonDual(value) => params.push(value.clone()),
251    }
252}
253
254fn extend_extracted_eta_params<T: FloatLike>(
255    params: &mut Vec<Complex<F<T>>>,
256    value: &DualOrNot<F<T>>,
257    t_variable: Option<usize>,
258) {
259    match value {
260        DualOrNot::Dual(dual) => {
261            if let Some(t_variable) = t_variable {
262                let (_, coefficient_duals) = extract_coefficient_t_duals(dual, t_variable);
263                for coefficient_dual in coefficient_duals {
264                    params.extend(
265                        extract_t_derivatives(coefficient_dual)
266                            .into_iter()
267                            .map(Complex::new_re),
268                    );
269                }
270            } else {
271                params.extend(dual.values.iter().cloned().map(Complex::new_re));
272            }
273        }
274        DualOrNot::NonDual(value) => params.push(Complex::new_re(value.clone())),
275    }
276}
277
278fn evaluate_threshold_helper_single<
279    T: FloatLike + crate::integrands::process::evaluators::GenericEvaluatorFloat,
280>(
281    helper: &mut GenericEvaluator,
282    cut_cff_index: &CutCFFIndex,
283    threshold_result: &DualOrNot<Complex<F<T>>>,
284    threshold_params: &ThresholdParams<T>,
285    evaluation_meta_data: &mut EvaluationMetaData,
286    record_primary_timing: bool,
287) -> DualOrNot<Complex<F<T>>> {
288    let t_variable = variable_indices_from_cut_cff_index(cut_cff_index).lu_cut;
289    let mut helper_params = Vec::new();
290    extend_extracted_f_params(&mut helper_params, threshold_result, t_variable);
291    extend_extracted_eta_params(
292        &mut helper_params,
293        &threshold_params.esurface_derivative,
294        t_variable,
295    );
296    extend_plain_helper_real_params(&mut helper_params, &threshold_params.radius, t_variable);
297    extend_plain_helper_real_params(
298        &mut helper_params,
299        &threshold_params.radius_star,
300        t_variable,
301    );
302    extend_plain_helper_real_params(
303        &mut helper_params,
304        &threshold_params.uv_damp_plus,
305        t_variable,
306    );
307    extend_plain_helper_real_params(
308        &mut helper_params,
309        &threshold_params.uv_damp_minus,
310        t_variable,
311    );
312    extend_plain_helper_real_params(&mut helper_params, &threshold_params.h_function, t_variable);
313
314    debug!(
315        "LU threshold helper input (single): cut_cff_index={:?}, t_variable={:?}, param_count={}",
316        cut_cff_index,
317        t_variable,
318        helper_params.len()
319    );
320    for (idx, value) in helper_params.iter().enumerate() {
321        debug!(
322            "LU threshold helper param (single): cut_cff_index={:?}, index={}, value={}",
323            cut_cff_index, idx, value
324        );
325    }
326
327    let mut result = evaluate_evaluator(
328        helper,
329        &helper_params,
330        evaluation_meta_data,
331        record_primary_timing,
332    );
333
334    debug_assert_eq!(result.len(), 1);
335    result.pop().unwrap()
336}
337
338fn evaluate_threshold_helper_iterated<
339    T: FloatLike + crate::integrands::process::evaluators::GenericEvaluatorFloat,
340>(
341    helper: &mut GenericEvaluator,
342    cut_cff_index: &CutCFFIndex,
343    threshold_result: &DualOrNot<Complex<F<T>>>,
344    left_threshold_params: &ThresholdParams<T>,
345    right_threshold_params: &ThresholdParams<T>,
346    evaluation_meta_data: &mut EvaluationMetaData,
347    record_primary_timing: bool,
348) -> DualOrNot<Complex<F<T>>> {
349    let t_variable = variable_indices_from_cut_cff_index(cut_cff_index).lu_cut;
350    let mut helper_params = Vec::new();
351    extend_extracted_f_params(&mut helper_params, threshold_result, t_variable);
352    extend_extracted_eta_params(
353        &mut helper_params,
354        &left_threshold_params.esurface_derivative,
355        t_variable,
356    );
357    extend_extracted_eta_params(
358        &mut helper_params,
359        &right_threshold_params.esurface_derivative,
360        t_variable,
361    );
362
363    extend_plain_helper_real_params(
364        &mut helper_params,
365        &left_threshold_params.radius,
366        t_variable,
367    );
368    extend_plain_helper_real_params(
369        &mut helper_params,
370        &left_threshold_params.radius_star,
371        t_variable,
372    );
373    extend_plain_helper_real_params(
374        &mut helper_params,
375        &left_threshold_params.uv_damp_plus,
376        t_variable,
377    );
378    extend_plain_helper_real_params(
379        &mut helper_params,
380        &left_threshold_params.uv_damp_minus,
381        t_variable,
382    );
383    extend_plain_helper_real_params(
384        &mut helper_params,
385        &left_threshold_params.h_function,
386        t_variable,
387    );
388
389    extend_plain_helper_real_params(
390        &mut helper_params,
391        &right_threshold_params.radius,
392        t_variable,
393    );
394    extend_plain_helper_real_params(
395        &mut helper_params,
396        &right_threshold_params.radius_star,
397        t_variable,
398    );
399    extend_plain_helper_real_params(
400        &mut helper_params,
401        &right_threshold_params.uv_damp_plus,
402        t_variable,
403    );
404    extend_plain_helper_real_params(
405        &mut helper_params,
406        &right_threshold_params.uv_damp_minus,
407        t_variable,
408    );
409    extend_plain_helper_real_params(
410        &mut helper_params,
411        &right_threshold_params.h_function,
412        t_variable,
413    );
414
415    debug!(
416        "LU threshold helper input (iterated): cut_cff_index={:?}, t_variable={:?}, param_count={}",
417        cut_cff_index,
418        t_variable,
419        helper_params.len()
420    );
421    for (idx, value) in helper_params.iter().enumerate() {
422        debug!(
423            "LU threshold helper param (iterated): cut_cff_index={:?}, index={}, value={}",
424            cut_cff_index, idx, value
425        );
426    }
427
428    let mut result = evaluate_evaluator(
429        helper,
430        &helper_params,
431        evaluation_meta_data,
432        record_primary_timing,
433    );
434
435    debug_assert_eq!(result.len(), 1);
436    result.pop().unwrap()
437}
438
439fn real_dual_to_complex<T: FloatLike>(dual: HyperDual<F<T>>) -> HyperDual<Complex<F<T>>> {
440    let shape = dual.get_shape().iter().map(|v| v.to_vec()).collect();
441    let values = dual.values.into_iter().map(Complex::new_re).collect_vec();
442    HyperDual::from_values(shape, values)
443}
444
445fn dualize_external_momenta<T: FloatLike>(
446    shape: &HyperDual<F<T>>,
447    external_moms: &ExternalFourMomenta<F<T>>,
448) -> ExternalFourMomenta<HyperDual<F<T>>> {
449    external_moms
450        .iter()
451        .map(|momentum| momentum.map_ref(&|component| new_constant(shape, component)))
452        .collect()
453}
454
455fn dualize_loop_momenta<T: FloatLike>(
456    shape: &HyperDual<F<T>>,
457    loop_moms: &LoopMomenta<F<T>>,
458) -> LoopMomenta<HyperDual<F<T>>> {
459    loop_moms
460        .iter()
461        .map(|momentum| momentum.map_ref(&|component| new_constant(shape, component)))
462        .collect()
463}
464
465fn dual_shifted_radius<T: FloatLike>(
466    shifted_momenta: &LoopMomenta<HyperDual<F<T>>>,
467    subspace: &SubspaceData,
468) -> HyperDual<F<T>> {
469    let zero = new_constant(
470        &shifted_momenta[LoopIndex(0)].px,
471        &shifted_momenta[LoopIndex(0)].px.values[0].zero(),
472    );
473
474    subspace
475        .iter_lmb_indices()
476        .fold(zero, |acc, loop_index| {
477            acc + shifted_momenta[loop_index].norm_squared()
478        })
479        .sqrt()
480}
481
482fn compute_shift_part_from_dual_momenta_in_subspace<T: FloatLike>(
483    esurface: &Esurface,
484    loop_moms: &LoopMomenta<HyperDual<F<T>>>,
485    external_moms: &ExternalFourMomenta<HyperDual<F<T>>>,
486    subspace: &SubspaceData,
487    all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
488    graph: &Graph,
489    masses: &EdgeVec<F<T>>,
490) -> HyperDual<F<T>> {
491    let lmb = subspace.get_lmb(all_lmbs);
492    let zero = new_constant(
493        &external_moms[ExternalIndex(0)].temporal.value,
494        &external_moms[ExternalIndex(0)].temporal.value.values[0].zero(),
495    );
496
497    let full_external_shift = esurface
498        .external_shift
499        .iter()
500        .map(|(index, sign)| {
501            let external_signature = &lmb.edge_signatures[*index].external;
502            let sign = new_constant(&zero, &F::from_f64(*sign as f64));
503            let external_energy = external_signature
504                .try_apply(&external_moms.raw)
505                .map(|momentum| momentum.temporal.value)
506                .unwrap_or_else(|| zero.clone());
507
508            sign * external_energy
509        })
510        .reduce(|acc, value| acc + value)
511        .unwrap_or_else(|| zero.clone());
512
513    let spatial_externals = external_moms
514        .iter()
515        .map(|momentum| momentum.spatial.clone())
516        .collect::<TiVec<ExternalIndex, _>>();
517
518    let remaining_shift = subspace
519        .does_not_contain(&esurface.energies, graph)
520        .map(|index| {
521            let signature = &lmb.edge_signatures[index];
522            let momentum = signature
523                .try_compute_momentum(&loop_moms.0, &spatial_externals.raw)
524                .unwrap_or_else(|| unreachable!());
525            let mass = &masses[index];
526            let lifted_mass = new_constant(&momentum.px, mass);
527
528            (momentum.norm_squared() + lifted_mass.clone() * lifted_mass).sqrt()
529        })
530        .reduce(|acc, value| acc + value)
531        .unwrap_or_else(|| zero.clone());
532
533    full_external_shift + remaining_shift
534}
535
536#[allow(clippy::too_many_arguments)]
537fn compute_self_and_r_derivative_subspace_dual<T: FloatLike>(
538    esurface: &Esurface,
539    radius: &HyperDual<F<T>>,
540    shifted_unit_loops_in_subspace: &LoopMomenta<HyperDual<F<T>>>,
541    center_in_subspace: &LoopMomenta<HyperDual<F<T>>>,
542    external_moms: &ExternalFourMomenta<HyperDual<F<T>>>,
543    real_mass_vector: &EdgeVec<F<T>>,
544    subspace: &SubspaceData,
545    all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
546    graph: &Graph,
547) -> (HyperDual<F<T>>, HyperDual<F<T>>) {
548    let spatial_part_of_externals = external_moms
549        .iter()
550        .map(|momentum| momentum.spatial.clone())
551        .collect::<TiVec<ExternalIndex, _>>();
552
553    let loops: LoopMomenta<HyperDual<F<T>>> = shifted_unit_loops_in_subspace
554        .iter_enumerated()
555        .map(|(loop_index, shifted_unit_momenta)| {
556            if subspace.contains_loop_index(loop_index) {
557                shifted_unit_momenta * radius + &center_in_subspace[loop_index]
558            } else {
559                shifted_unit_momenta.clone()
560            }
561        })
562        .collect();
563
564    let shift = compute_shift_part_from_dual_momenta_in_subspace(
565        esurface,
566        shifted_unit_loops_in_subspace,
567        external_moms,
568        subspace,
569        all_lmbs,
570        graph,
571        real_mass_vector,
572    );
573
574    let lmb = subspace.get_lmb(all_lmbs);
575    let zero = new_constant(radius, &radius.values[0].zero());
576    let (derivative, energy_sum) = subspace
577        .contains(&esurface.energies, graph)
578        .map(|index| {
579            let signature = &lmb.edge_signatures[index];
580            let momentum = signature
581                .try_compute_momentum(&loops.0, &spatial_part_of_externals.raw)
582                .unwrap_or_else(|| unreachable!());
583            let unit_loop_part = compute_loop_part_subspace_dual(
584                &signature.internal,
585                shifted_unit_loops_in_subspace,
586                subspace,
587            );
588            let mass = &real_mass_vector[index];
589            let lifted_mass = new_constant(&momentum.px, mass);
590            let energy = (momentum.norm_squared() + lifted_mass.clone() * lifted_mass).sqrt();
591            let numerator = momentum * &unit_loop_part;
592
593            (numerator / &energy, energy)
594        })
595        .fold((zero.clone(), zero), |(der_sum, en_sum), (der, en)| {
596            (der_sum + der, en_sum + en)
597        });
598
599    (energy_sum + shift, derivative)
600}
601
602fn compute_loop_part_subspace_dual<T: FloatLike>(
603    loop_signature: &LoopSignature,
604    loop_moms: &LoopMomenta<HyperDual<F<T>>>,
605    subspace: &SubspaceData,
606) -> ThreeMomentum<HyperDual<F<T>>> {
607    let projected: LoopSignature = subspace.project_loop_signature(loop_signature).collect();
608    let zero = new_constant(
609        &loop_moms[LoopIndex(0)].px,
610        &loop_moms[LoopIndex(0)].px.values[0].zero(),
611    );
612    let mut result = ThreeMomentum::new(zero.clone(), zero.clone(), zero);
613
614    for (loop_index, sign) in projected.iter_enumerated() {
615        match sign {
616            SignOrZero::Zero => {}
617            SignOrZero::Plus => result += &loop_moms[loop_index],
618            SignOrZero::Minus => result -= &loop_moms[loop_index],
619        }
620    }
621
622    result
623}
624
625#[derive(Clone, Encode, Decode)]
626#[trait_decode(trait = GammaLoopContext)]
627pub struct LUThresholdHelperEvaluators {
628    pub left_thresholds: TiVec<LeftThresholdId, BTreeMap<CutCFFIndex, GenericEvaluator>>,
629    pub right_thresholds: TiVec<RightThresholdId, BTreeMap<CutCFFIndex, GenericEvaluator>>,
630    pub iterated: IteratedCtCollection<BTreeMap<CutCFFIndex, GenericEvaluator>>,
631}
632
633impl LUThresholdHelperEvaluators {
634    fn for_each_generic_evaluator_mut(
635        &mut self,
636        mut f: impl FnMut(&mut GenericEvaluator) -> color_eyre::Result<()>,
637    ) -> color_eyre::Result<()> {
638        for evaluators in self.left_thresholds.iter_mut() {
639            for evaluator in evaluators.values_mut() {
640                f(evaluator)?;
641            }
642        }
643        for evaluators in self.right_thresholds.iter_mut() {
644            for evaluator in evaluators.values_mut() {
645                f(evaluator)?;
646            }
647        }
648        for evaluators in self.iterated.iter_mut() {
649            for evaluator in evaluators.values_mut() {
650                f(evaluator)?;
651            }
652        }
653
654        Ok(())
655    }
656}
657
658#[derive(Clone, Encode, Decode)]
659#[trait_decode(trait = GammaLoopContext)]
660pub struct LUCounterTermEvaluators {
661    pub left_thresholds_evaluator: TiVec<LeftThresholdId, BTreeMap<CutCFFIndex, EvaluatorStack>>,
662    pub right_thresholds_evaluator: TiVec<RightThresholdId, BTreeMap<CutCFFIndex, EvaluatorStack>>,
663    pub iterated_evaluator: IteratedCtCollection<BTreeMap<CutCFFIndex, EvaluatorStack>>,
664    pub threshold_helpers: LUThresholdHelperEvaluators,
665    pub residue_from_e_surface_evaluators: Vec<GenericEvaluator>,
666}
667
668impl LUCounterTermEvaluators {
669    pub(crate) fn generic_compileable_evaluator_count(&self) -> usize {
670        let left = self
671            .left_thresholds_evaluator
672            .iter()
673            .flat_map(|evaluators| evaluators.values())
674            .map(EvaluatorStack::generic_evaluator_count)
675            .sum::<usize>();
676        let right = self
677            .right_thresholds_evaluator
678            .iter()
679            .flat_map(|evaluators| evaluators.values())
680            .map(EvaluatorStack::generic_evaluator_count)
681            .sum::<usize>();
682        let iterated = self
683            .iterated_evaluator
684            .iter()
685            .flat_map(|evaluators| evaluators.values())
686            .map(EvaluatorStack::generic_evaluator_count)
687            .sum::<usize>();
688
689        // Ignore eager-only helper evaluators and pass-two evaluators.
690        left + right + iterated
691    }
692
693    pub fn from_atoms(
694        counterterm_data: &LUCounterTermData,
695        max_cut_order: usize,
696        threshold_helpers: LUThresholdHelperEvaluators,
697        param_builder: &ParamBuilder,
698        settings: &GlobalSettings,
699        orientations: &TiVec<OrientationID, EdgeVec<Orientation>>,
700    ) -> (Self, EvaluatorBuildTimings) {
701        let mut timings = EvaluatorBuildTimings::default();
702        let left_thresholds_evaluator = counterterm_data
703            .left_atoms
704            .iter()
705            .map(|parametric_integrands| {
706                parametric_integrands
707                    .integrands
708                    .iter()
709                    .map(|(i, atom)| {
710                        let dual_shape = shape_from_cut_cff_index(i);
711
712                        let (evaluator, evaluator_timings) = EvaluatorStack::new_with_timings(
713                            std::slice::from_ref(atom),
714                            param_builder,
715                            &orientations.raw,
716                            dual_shape,
717                            &settings.generation.evaluator,
718                        )
719                        .unwrap();
720                        timings += evaluator_timings;
721                        (*i, evaluator)
722                    })
723                    .collect()
724            })
725            .collect();
726
727        let right_thresholds_evaluator = counterterm_data
728            .right_atoms
729            .iter()
730            .map(|parametric_integrands| {
731                parametric_integrands
732                    .integrands
733                    .iter()
734                    .map(|(i, atom)| {
735                        let dual_shape = shape_from_cut_cff_index(i);
736
737                        let (evaluator, evaluator_timings) = EvaluatorStack::new_with_timings(
738                            std::slice::from_ref(atom),
739                            param_builder,
740                            &orientations.raw,
741                            dual_shape,
742                            &settings.generation.evaluator,
743                        )
744                        .unwrap();
745                        timings += evaluator_timings;
746                        (*i, evaluator)
747                    })
748                    .collect()
749            })
750            .collect();
751
752        let iterated_timings = std::cell::Cell::new(EvaluatorBuildTimings::default());
753        let iterated_evaluator = counterterm_data.iterated.map_ref(|parametric_integrands| {
754            parametric_integrands
755                .integrands
756                .iter()
757                .map(|(i, atom)| {
758                    let dual_shape = shape_from_cut_cff_index(i);
759
760                    let (evaluator, evaluator_timings) = EvaluatorStack::new_with_timings(
761                        std::slice::from_ref(atom),
762                        param_builder,
763                        &orientations.raw,
764                        dual_shape,
765                        &settings.generation.evaluator,
766                    )
767                    .unwrap();
768                    let mut timings = iterated_timings.get();
769                    timings += evaluator_timings;
770                    iterated_timings.set(timings);
771                    (*i, evaluator)
772                })
773                .collect()
774        });
775        timings += iterated_timings.get();
776
777        let symbolica_started = std::time::Instant::now();
778        let pass_two_evaluator = (1..=max_cut_order)
779            .map(|order| {
780                build_derivative_structure(order as u8, -1, &settings.generation.evaluator)
781            })
782            .collect();
783
784        timings.symbolica_time += symbolica_started.elapsed();
785
786        (
787            LUCounterTermEvaluators {
788                left_thresholds_evaluator,
789                right_thresholds_evaluator,
790                iterated_evaluator,
791                threshold_helpers,
792                residue_from_e_surface_evaluators: pass_two_evaluator,
793            },
794            timings,
795        )
796    }
797
798    pub(crate) fn compile(
799        &mut self,
800        path: impl AsRef<Path>,
801        cut_group_id: CutGroupId,
802        frozen_mode: &FrozenCompilationMode,
803    ) -> color_eyre::Result<()> {
804        for (threshold_id, evaluators) in self.left_thresholds_evaluator.iter_mut_enumerated() {
805            for (index, evaluator) in evaluators.iter_mut() {
806                let name = format!(
807                    "cut_group_{}_left_threshold_{}_index_{}",
808                    cut_group_id.0, threshold_id.0, index
809                );
810                evaluator.compile(&name, path.as_ref(), frozen_mode)?;
811            }
812        }
813
814        for (threshold_id, evaluators) in self.right_thresholds_evaluator.iter_mut_enumerated() {
815            for (index, evaluator) in evaluators.iter_mut() {
816                let name = format!(
817                    "cut_group_{}_right_threshold_{}_index_{}",
818                    cut_group_id.0, threshold_id.0, index
819                );
820                evaluator.compile(&name, path.as_ref(), frozen_mode)?;
821            }
822        }
823
824        for (iterated_index, evaluators) in self.iterated_evaluator.iter_mut().enumerate() {
825            for (index, evaluator) in evaluators.iter_mut() {
826                let name = format!(
827                    "cut_group_{}_iterated_{}_index_{}",
828                    cut_group_id.0, iterated_index, index
829                );
830                evaluator.compile(&name, path.as_ref(), frozen_mode)?;
831            }
832        }
833
834        for (order, pass_to_evaluator) in self
835            .residue_from_e_surface_evaluators
836            .iter_mut()
837            .enumerate()
838        {
839            let name = format!("cut_group_{}_pass_two_{}", cut_group_id.0, order);
840            pass_to_evaluator.compile_external(
841                path.as_ref().join(&name).with_extension("cpp"),
842                &name,
843                path.as_ref().join(&name).with_extension("so"),
844                frozen_mode,
845            )?;
846        }
847
848        Ok(())
849    }
850
851    pub(crate) fn for_each_generic_evaluator_mut(
852        &mut self,
853        mut f: impl FnMut(&mut GenericEvaluator) -> color_eyre::Result<()>,
854    ) -> color_eyre::Result<()> {
855        for evaluators in self.left_thresholds_evaluator.iter_mut() {
856            for evaluator in evaluators.values_mut() {
857                evaluator.for_each_generic_evaluator_mut(&mut f)?;
858            }
859        }
860        for evaluators in self.right_thresholds_evaluator.iter_mut() {
861            for evaluator in evaluators.values_mut() {
862                evaluator.for_each_generic_evaluator_mut(&mut f)?;
863            }
864        }
865        for evaluators in self.iterated_evaluator.iter_mut() {
866            for evaluator in evaluators.values_mut() {
867                evaluator.for_each_generic_evaluator_mut(&mut f)?;
868            }
869        }
870        self.threshold_helpers
871            .for_each_generic_evaluator_mut(&mut f)?;
872        for pass_to_evaluator in self.residue_from_e_surface_evaluators.iter_mut() {
873            f(pass_to_evaluator)?;
874        }
875
876        Ok(())
877    }
878}
879
880type CutThresholds = (
881    TiVec<LeftThresholdId, Esurface>,
882    TiVec<RightThresholdId, Esurface>,
883);
884
885#[derive(Clone, Encode, Decode)]
886#[trait_decode(trait = GammaLoopContext)]
887pub(crate) struct LUCounterTerm {
888    pub evaluators: TiVec<CutGroupId, LUCounterTermEvaluators>,
889    pub thresholds: TiVec<CutGroupId, CutThresholds>,
890    pub subspaces: TiVec<CutGroupId, (SubspaceData, SubspaceData)>,
891    pub active_cut_groups: TiVec<CutGroupId, bool>,
892    pub active_left_thresholds: TiVec<CutGroupId, TiVec<LeftThresholdId, bool>>,
893    pub active_right_thresholds: TiVec<CutGroupId, TiVec<RightThresholdId, bool>>,
894    pub active_iterated_thresholds: TiVec<CutGroupId, IteratedCtCollection<bool>>,
895    pub rstar_dependence_calculator: TiVec<CutGroupId, RstarTDependenceEvaluator>,
896}
897
898pub struct LUCTKinematicPoint<T: FloatLike> {
899    pub unrescaled_sample: MomentumSample<T>,
900    pub dualized_momentum_sample_cache: Vec<MomentumSample<T>>,
901    pub lu_cut_parameter_cache: Vec<LUParams<T>>,
902    pub lu_cut_esurface_values: Vec<DualOrNot<F<T>>>,
903}
904
905impl<T: FloatLike> LUCTKinematicPoint<T> {
906    pub fn unrescaled_sample(&self) -> &MomentumSample<T> {
907        &self.unrescaled_sample
908    }
909
910    pub fn representative_sample(&self) -> &MomentumSample<T> {
911        &self.dualized_momentum_sample_cache[0]
912    }
913
914    pub fn sample_for_order(&self, order: usize) -> &MomentumSample<T> {
915        &self.dualized_momentum_sample_cache[order]
916    }
917
918    pub fn lmb_transform(&self, from: &LoopMomentumBasis, to: &LoopMomentumBasis) -> Self {
919        let transformed_samples = self
920            .dualized_momentum_sample_cache
921            .iter()
922            .map(|sample| sample.lmb_transform(from, to))
923            .collect();
924
925        Self {
926            unrescaled_sample: self.unrescaled_sample.clone(),
927            dualized_momentum_sample_cache: transformed_samples,
928            lu_cut_parameter_cache: self.lu_cut_parameter_cache.clone(),
929            lu_cut_esurface_values: self.lu_cut_esurface_values.clone(),
930        }
931    }
932
933    pub fn non_dual_cut_params(&self) -> LUParams<T> {
934        self.lu_cut_parameter_cache[0].clone()
935    }
936
937    pub fn cut_params_for_order(&self, order: usize) -> &LUParams<T> {
938        &self.lu_cut_parameter_cache[order]
939    }
940
941    pub fn cut_params_for_cut_cff_index(&self, cut_cff_index: &CutCFFIndex) -> LUParams<T> {
942        let lu_order = cut_cff_index
943            .lu_cut_order
944            .expect("LU cut parameter lookup requires lu_cut_order")
945            - 1;
946        let base_params = self.cut_params_for_order(lu_order).clone();
947        let target_shape = shape_from_cut_cff_index(cut_cff_index).map(HyperDual::new);
948        let t_variable = variable_indices_from_cut_cff_index(cut_cff_index).lu_cut;
949
950        LUParams {
951            tstar: embed_real_dual_or_not_in_target_shape(
952                &base_params.tstar,
953                &target_shape,
954                t_variable,
955            ),
956            h_function: embed_real_dual_or_not_in_target_shape(
957                &base_params.h_function,
958                &target_shape,
959                t_variable,
960            ),
961        }
962    }
963
964    pub fn cut_esurface_for_order(&self, order: usize) -> &DualOrNot<F<T>> {
965        &self.lu_cut_esurface_values[order]
966    }
967
968    pub fn non_dual_cut_esurface_value(&self) -> F<T> {
969        match self.cut_esurface_for_order(0) {
970            DualOrNot::NonDual(value) => value.clone(),
971            DualOrNot::Dual(_) => {
972                unreachable!("representative LU cut e-surface value must be non-dual")
973            }
974        }
975    }
976
977    pub fn new(unrescaled_sample: MomentumSample<T>) -> Self {
978        Self {
979            unrescaled_sample,
980            dualized_momentum_sample_cache: Vec::new(),
981            lu_cut_parameter_cache: Vec::new(),
982            lu_cut_esurface_values: Vec::new(),
983        }
984    }
985}
986
987impl LUCounterTerm {
988    fn radial_root_identity(
989        graph_name: &str,
990        cut_group_id: CutGroupId,
991        side: &str,
992        overlap_group: usize,
993        esurface_id: EsurfaceID,
994        probe_rotation: &Rotation,
995    ) -> RadialRootIdentity {
996        RadialRootIdentity::new(format!(
997            "LU graph '{graph_name}' cut group {} {side} overlap group {overlap_group} E-surface {} probe rotation {}",
998            cut_group_id.0, esurface_id.0, probe_rotation.method,
999        ))
1000    }
1001
1002    pub(crate) fn cut_group_is_active(&self, cut_group_id: CutGroupId) -> bool {
1003        self.active_cut_groups[cut_group_id]
1004    }
1005
1006    pub(crate) fn compile(
1007        &mut self,
1008        path: impl AsRef<Path>,
1009        frozen_mode: &FrozenCompilationMode,
1010    ) -> color_eyre::Result<()> {
1011        for (cut_group_id, evaluators) in self.evaluators.iter_mut_enumerated() {
1012            evaluators.compile(path.as_ref(), cut_group_id, frozen_mode)?;
1013        }
1014        Ok(())
1015    }
1016
1017    pub(crate) fn for_each_generic_evaluator_mut(
1018        &mut self,
1019        mut f: impl FnMut(&mut GenericEvaluator) -> color_eyre::Result<()>,
1020    ) -> color_eyre::Result<()> {
1021        for evaluators in self.evaluators.iter_mut() {
1022            evaluators.for_each_generic_evaluator_mut(&mut f)?;
1023        }
1024        Ok(())
1025    }
1026
1027    fn ensure_active_cut_group(&self, cut_group_id: CutGroupId) -> Result<()> {
1028        if self.cut_group_is_active(cut_group_id) {
1029            return Ok(());
1030        }
1031
1032        Err(eyre!(
1033            "Cut group {} was reached at runtime even though generation marked it inactive for the selected orientation subset",
1034            cut_group_id.0
1035        ))
1036    }
1037
1038    fn ensure_active_left_threshold(
1039        &self,
1040        cut_group_id: CutGroupId,
1041        threshold_id: LeftThresholdId,
1042    ) -> Result<()> {
1043        if self.active_left_thresholds[cut_group_id][threshold_id] {
1044            return Ok(());
1045        }
1046
1047        Err(eyre!(
1048            "Left threshold evaluator {} for cut group {} was reached at runtime even though generation marked it inactive for the selected orientation subset",
1049            threshold_id.0,
1050            cut_group_id.0
1051        ))
1052    }
1053
1054    fn ensure_active_right_threshold(
1055        &self,
1056        cut_group_id: CutGroupId,
1057        threshold_id: RightThresholdId,
1058    ) -> Result<()> {
1059        if self.active_right_thresholds[cut_group_id][threshold_id] {
1060            return Ok(());
1061        }
1062
1063        Err(eyre!(
1064            "Right threshold evaluator {} for cut group {} was reached at runtime even though generation marked it inactive for the selected orientation subset",
1065            threshold_id.0,
1066            cut_group_id.0
1067        ))
1068    }
1069
1070    fn ensure_active_iterated_threshold(
1071        &self,
1072        cut_group_id: CutGroupId,
1073        iterated_index: (LeftThresholdId, RightThresholdId),
1074    ) -> Result<()> {
1075        if self.active_iterated_thresholds[cut_group_id][iterated_index] {
1076            return Ok(());
1077        }
1078
1079        Err(eyre!(
1080            "Iterated threshold evaluator ({}, {}) for cut group {} was reached at runtime even though generation marked it inactive for the selected orientation subset",
1081            iterated_index.0.0,
1082            iterated_index.1.0,
1083            cut_group_id.0
1084        ))
1085    }
1086
1087    #[allow(clippy::too_many_arguments)]
1088    pub(crate) fn evaluate<T: FloatLike>(
1089        &mut self,
1090        kinematic_point: &LUCTKinematicPoint<T>,
1091        cut_group_id: CutGroupId,
1092        reversed_edges: &[EdgeIndex],
1093        all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
1094        graph: &Graph,
1095        masses: &EdgeVec<F<T>>,
1096        probe_rotation: &Rotation,
1097        settings: &RuntimeSettings,
1098        param_builder: &mut ParamBuilder<f64>,
1099        orientations: SingleOrAllOrientations<'_, OrientationID>,
1100        evaluation_meta_data: &mut EvaluationMetaData,
1101        record_primary_timing: bool,
1102    ) -> Result<Complex<F<T>>> {
1103        self.ensure_active_cut_group(cut_group_id)?;
1104
1105        let (left_thresholds_typed, right_thresholds_typed) = &self.thresholds[cut_group_id];
1106
1107        let left_thresholds = TiVec::from_ref(&left_thresholds_typed.raw);
1108        let right_thresholds = TiVec::from_ref(&right_thresholds_typed.raw);
1109
1110        if left_thresholds.is_empty() && right_thresholds.is_empty() {
1111            return Ok(Complex::new_re(
1112                kinematic_point.representative_sample().zero(),
1113            ));
1114        }
1115
1116        let (left_subspace, right_subspace) = &self.subspaces[cut_group_id];
1117        let (sample_left_transformed, sample_right_transformed) = (
1118            kinematic_point
1119                .lmb_transform(&graph.loop_momentum_basis, left_subspace.get_lmb(all_lmbs)),
1120            kinematic_point
1121                .lmb_transform(&graph.loop_momentum_basis, right_subspace.get_lmb(all_lmbs)),
1122        );
1123
1124        debug!("possible left thresholds: {}", left_thresholds.len());
1125        debug!("possible right thresholds: {}", right_thresholds.len());
1126
1127        let masses_f64: EdgeVec<F<f64>> = masses.iter().map(|(_, m)| F(m.to_f64())).collect();
1128        let sample_left_transformed_f64 = sample_left_transformed
1129            .representative_sample()
1130            .loop_moms()
1131            .iter()
1132            .map(|lm| lm.to_f64())
1133            .collect();
1134        let sample_right_transformed_f64 = sample_right_transformed
1135            .representative_sample()
1136            .loop_moms()
1137            .iter()
1138            .map(|lm| lm.to_f64())
1139            .collect();
1140        let external_moms_f64 = kinematic_point
1141            .representative_sample()
1142            .external_moms()
1143            .iter()
1144            .map(|em| em.to_f64())
1145            .collect();
1146
1147        let e_cm = F::from_f64(settings.kinematics.e_cm);
1148        let esurface_existence_threshold =
1149            F::from_f64(settings.subtraction.esurface_existence_threshold);
1150
1151        let left_existing_esurfaces = self.thresholds[cut_group_id]
1152            .0
1153            .iter_enumerated()
1154            .filter_map(|(left_id, esurface)| {
1155                let classification = esurface.classify_existence_subspace(
1156                    sample_left_transformed.representative_sample().loop_moms(),
1157                    kinematic_point.representative_sample().external_moms(),
1158                    left_subspace,
1159                    all_lmbs,
1160                    graph,
1161                    masses,
1162                    reversed_edges,
1163                    &e_cm,
1164                    &esurface_existence_threshold,
1165                );
1166                debug!(
1167                    graph = %graph.name,
1168                    side = "left",
1169                    esurface_id = left_id.0,
1170                    status = classification.label(),
1171                    normalized_margin = ?classification.normalized_margin(),
1172                    non_existing_reason = ?classification.non_existing_reason(),
1173                    "classified LU threshold surface"
1174                );
1175                if classification.is_existing() {
1176                    Some(EsurfaceID::from(left_id.0))
1177                } else {
1178                    None
1179                }
1180            })
1181            .collect::<TiVec<ExistingEsurfaceId, _>>();
1182
1183        let right_existing_esurfaces = self.thresholds[cut_group_id]
1184            .1
1185            .iter_enumerated()
1186            .filter_map(|(right_id, esurface)| {
1187                let classification = esurface.classify_existence_subspace(
1188                    sample_right_transformed.representative_sample().loop_moms(),
1189                    kinematic_point.representative_sample().external_moms(),
1190                    right_subspace,
1191                    all_lmbs,
1192                    graph,
1193                    masses,
1194                    reversed_edges,
1195                    &e_cm,
1196                    &esurface_existence_threshold,
1197                );
1198                debug!(
1199                    graph = %graph.name,
1200                    side = "right",
1201                    esurface_id = right_id.0,
1202                    status = classification.label(),
1203                    normalized_margin = ?classification.normalized_margin(),
1204                    non_existing_reason = ?classification.non_existing_reason(),
1205                    "classified LU threshold surface"
1206                );
1207                if classification.is_existing() {
1208                    Some(EsurfaceID::from(right_id.0))
1209                } else {
1210                    None
1211                }
1212            })
1213            .collect::<TiVec<ExistingEsurfaceId, _>>();
1214
1215        debug!(
1216            "number of thresholds on the left: {}",
1217            left_existing_esurfaces.len()
1218        );
1219        debug!(
1220            "number of thresholds on the right: {}",
1221            right_existing_esurfaces.len()
1222        );
1223
1224        if left_existing_esurfaces.is_empty() && right_existing_esurfaces.is_empty() {
1225            return Ok(Complex::new_re(
1226                kinematic_point.representative_sample().zero(),
1227            ));
1228        }
1229
1230        let left_overlap_input = OverlapInput {
1231            graph,
1232            subspace: left_subspace,
1233            settings,
1234            edge_masses: masses_f64.clone(),
1235            lmbs: all_lmbs,
1236            thresholds: left_thresholds,
1237        };
1238
1239        let right_overlap_input = OverlapInput {
1240            graph,
1241            subspace: right_subspace,
1242            settings,
1243            edge_masses: masses_f64,
1244            lmbs: all_lmbs,
1245            thresholds: right_thresholds,
1246        };
1247
1248        let left_overlap = match overlap_subspace::find_maximal_overlap(
1249            &left_overlap_input,
1250            &left_existing_esurfaces,
1251            &sample_left_transformed_f64,
1252            &external_moms_f64,
1253            probe_rotation,
1254        ) {
1255            Ok(left_overlap) => left_overlap,
1256            Err(error) => {
1257                evaluation_meta_data.record_threshold_counterterm_error(format!(
1258                    "LU graph '{}' cut group {} left subspace failed overlap-center construction in probe rotation {}: {}",
1259                    graph.name,
1260                    cut_group_id.0,
1261                    probe_rotation.method,
1262                    error,
1263                ));
1264                return Ok(Complex::new_re(F::from_f64(f64::NAN)));
1265            }
1266        };
1267
1268        let right_overlap = match overlap_subspace::find_maximal_overlap(
1269            &right_overlap_input,
1270            &right_existing_esurfaces,
1271            &sample_right_transformed_f64,
1272            &external_moms_f64,
1273            probe_rotation,
1274        ) {
1275            Ok(right_overlap) => right_overlap,
1276            Err(error) => {
1277                evaluation_meta_data.record_threshold_counterterm_error(format!(
1278                    "LU graph '{}' cut group {} right subspace failed overlap-center construction in probe rotation {}: {}",
1279                    graph.name,
1280                    cut_group_id.0,
1281                    probe_rotation.method,
1282                    error,
1283                ));
1284                return Ok(Complex::new_re(F::from_f64(f64::NAN)));
1285            }
1286        };
1287
1288        debug!("left overlap structure: {}", left_overlap);
1289
1290        debug!("right overlap structure: {}", right_overlap);
1291
1292        let left_counterterm_builder = CounterTermBuilder::new(
1293            graph,
1294            settings,
1295            left_overlap_input.thresholds,
1296            sample_left_transformed,
1297            &left_overlap,
1298            masses,
1299            all_lmbs,
1300            left_subspace,
1301            probe_rotation,
1302        );
1303
1304        let left_overlap_builders = left_overlap
1305            .overlap_groups
1306            .iter()
1307            .map(|overlap_group| left_counterterm_builder.new_overlap_builder(overlap_group))
1308            .collect_vec();
1309
1310        let mut radial_root_failed = false;
1311        let mut left_overlap_solutions = Vec::with_capacity(left_overlap_builders.len());
1312        for (overlap_group_index, overlap_builder) in left_overlap_builders.iter().enumerate() {
1313            let mut group_solutions =
1314                Vec::with_capacity(overlap_builder.overlap_group.existing_esurfaces.len());
1315            for &existing_esurface_id in &overlap_builder.overlap_group.existing_esurfaces {
1316                let esurface_id = left_overlap.existing_esurfaces[existing_esurface_id];
1317                let radial_root_identity = Self::radial_root_identity(
1318                    &graph.name,
1319                    cut_group_id,
1320                    "left",
1321                    overlap_group_index,
1322                    esurface_id,
1323                    probe_rotation,
1324                );
1325                let Some(solution) = overlap_builder
1326                    .new_esurface_builder(existing_esurface_id)
1327                    .solve_rstar(
1328                        &mut self.rstar_dependence_calculator[cut_group_id],
1329                        &radial_root_identity,
1330                        &mut evaluation_meta_data.radial_root_diagnostics,
1331                    )
1332                else {
1333                    evaluation_meta_data.record_threshold_counterterm_error(format!(
1334                        "LU graph '{}' cut group {} left overlap group {} E-surface {} failed center or radial-root validation in probe rotation {}",
1335                        graph.name,
1336                        cut_group_id.0,
1337                        overlap_group_index,
1338                        esurface_id.0,
1339                        probe_rotation.method,
1340                    ));
1341                    radial_root_failed = true;
1342                    continue;
1343                };
1344                group_solutions.push(solution);
1345            }
1346            left_overlap_solutions.push(group_solutions);
1347        }
1348
1349        let right_counterterm_builder = CounterTermBuilder::new(
1350            graph,
1351            settings,
1352            right_overlap_input.thresholds,
1353            sample_right_transformed,
1354            &right_overlap,
1355            masses,
1356            all_lmbs,
1357            right_subspace,
1358            probe_rotation,
1359        );
1360
1361        let right_overlap_builders = right_overlap
1362            .overlap_groups
1363            .iter()
1364            .map(|overlap_group| right_counterterm_builder.new_overlap_builder(overlap_group))
1365            .collect_vec();
1366
1367        let mut right_overlap_solutions = Vec::with_capacity(right_overlap_builders.len());
1368        for (overlap_group_index, overlap_builder) in right_overlap_builders.iter().enumerate() {
1369            let mut group_solutions =
1370                Vec::with_capacity(overlap_builder.overlap_group.existing_esurfaces.len());
1371            for &existing_esurface_id in &overlap_builder.overlap_group.existing_esurfaces {
1372                let esurface_id = right_overlap.existing_esurfaces[existing_esurface_id];
1373                let radial_root_identity = Self::radial_root_identity(
1374                    &graph.name,
1375                    cut_group_id,
1376                    "right",
1377                    overlap_group_index,
1378                    esurface_id,
1379                    probe_rotation,
1380                );
1381                let Some(solution) = overlap_builder
1382                    .new_esurface_builder(existing_esurface_id)
1383                    .solve_rstar(
1384                        &mut self.rstar_dependence_calculator[cut_group_id],
1385                        &radial_root_identity,
1386                        &mut evaluation_meta_data.radial_root_diagnostics,
1387                    )
1388                else {
1389                    evaluation_meta_data.record_threshold_counterterm_error(format!(
1390                        "LU graph '{}' cut group {} right overlap group {} E-surface {} failed center or radial-root validation in probe rotation {}",
1391                        graph.name,
1392                        cut_group_id.0,
1393                        overlap_group_index,
1394                        esurface_id.0,
1395                        probe_rotation.method,
1396                    ));
1397                    radial_root_failed = true;
1398                    continue;
1399                };
1400                group_solutions.push(solution);
1401            }
1402            right_overlap_solutions.push(group_solutions);
1403        }
1404
1405        if radial_root_failed {
1406            return Ok(Complex::new_re(F::from_f64(f64::NAN)));
1407        }
1408
1409        let zero = kinematic_point.representative_sample().zero();
1410        let mut total_result = Complex::new_re(zero.clone());
1411
1412        for order in 0..kinematic_point.lu_cut_parameter_cache.len() {
1413            let mut left_evaluations = zero_dual_or_not_complex(order, &zero);
1414
1415            for solutions_group in left_overlap_solutions.iter() {
1416                for solution in solutions_group {
1417                    let representative_sample = solution.rstar_sample_for_order(order);
1418                    let left_threshold_id =
1419                        LeftThresholdId::from(representative_sample.get_esurface_id().0);
1420                    self.ensure_active_left_threshold(cut_group_id, left_threshold_id)?;
1421
1422                    let matching_cut_indices = self.evaluators[cut_group_id]
1423                        .left_thresholds_evaluator[left_threshold_id]
1424                        .keys()
1425                        .filter(|cut_cff_index| {
1426                            cut_cff_index.lu_cut_order == Some(order + 1)
1427                                && cut_cff_index.right_threshold_order.is_none()
1428                        })
1429                        .copied()
1430                        .collect_vec();
1431
1432                    for cut_cff_index in matching_cut_indices {
1433                        let variable_indices = variable_indices_from_cut_cff_index(&cut_cff_index);
1434                        let lu_cut_params =
1435                            kinematic_point.cut_params_for_cut_cff_index(&cut_cff_index);
1436                        let sample = solution.rstar_sample_for_cut_cff_index(
1437                            &cut_cff_index,
1438                            variable_indices.left_threshold,
1439                        );
1440                        debug!("left threshold parameters");
1441
1442                        let left_threshold_params: ThresholdParams<T> =
1443                            sample.extract_threshold_parameters(true);
1444                        debug!(
1445                            "LU left evaluator input: cut_group_id={}, left_threshold_id={}, cut_cff_index={:?}, lu_order={}, r={}, rstar={}",
1446                            cut_group_id.0,
1447                            left_threshold_id.0,
1448                            cut_cff_index,
1449                            order + 1,
1450                            left_threshold_params.radius,
1451                            left_threshold_params.radius_star,
1452                        );
1453                        let inverse_transformed_sample = sample.get_inverse_transformed_sample();
1454
1455                        let params = T::get_parameters(
1456                            param_builder,
1457                            (false, false),
1458                            graph,
1459                            &inverse_transformed_sample,
1460                            settings.kinematics.externals.get_helicities(),
1461                            &settings.additional_params(),
1462                            Some(&left_threshold_params),
1463                            None,
1464                            Some(&lu_cut_params),
1465                        );
1466
1467                        let result_of_this_ct = self.evaluators[cut_group_id]
1468                            .left_thresholds_evaluator[left_threshold_id]
1469                            .get_mut(&cut_cff_index)
1470                            .unwrap()
1471                            .evaluate(
1472                                params,
1473                                orientations,
1474                                settings,
1475                                evaluation_meta_data,
1476                                record_primary_timing,
1477                            )
1478                            .unwrap()
1479                            .pop()
1480                            .unwrap();
1481
1482                        debug!(
1483                            "result of left threshold evaluator {:?}: {}",
1484                            cut_cff_index, result_of_this_ct
1485                        );
1486
1487                        let helper_completed_result = evaluate_threshold_helper_single(
1488                            self.evaluators[cut_group_id]
1489                                .threshold_helpers
1490                                .left_thresholds[left_threshold_id]
1491                                .get_mut(&cut_cff_index)
1492                                .unwrap(),
1493                            &cut_cff_index,
1494                            &result_of_this_ct,
1495                            &left_threshold_params,
1496                            evaluation_meta_data,
1497                            record_primary_timing,
1498                        );
1499
1500                        let multi_channeling_factor = plain_t_dual_or_scalar_complex(
1501                            &sample.value_of_multi_channeling_factor,
1502                            variable_indices.lu_cut,
1503                        );
1504
1505                        left_evaluations += multiply_dual_or_not_complex(
1506                            helper_completed_result,
1507                            &multi_channeling_factor,
1508                        );
1509                    }
1510                }
1511            }
1512
1513            let mut right_evaluations = zero_dual_or_not_complex(order, &zero);
1514
1515            for solutions_group in right_overlap_solutions.iter() {
1516                for solution in solutions_group {
1517                    let representative_sample = solution.rstar_sample_for_order(order);
1518                    let right_threshold_id =
1519                        RightThresholdId::from(representative_sample.get_esurface_id().0);
1520                    self.ensure_active_right_threshold(cut_group_id, right_threshold_id)?;
1521
1522                    let matching_cut_indices = self.evaluators[cut_group_id]
1523                        .right_thresholds_evaluator[right_threshold_id]
1524                        .keys()
1525                        .filter(|cut_cff_index| {
1526                            cut_cff_index.lu_cut_order == Some(order + 1)
1527                                && cut_cff_index.left_threshold_order.is_none()
1528                        })
1529                        .copied()
1530                        .collect_vec();
1531
1532                    for cut_cff_index in matching_cut_indices {
1533                        let variable_indices = variable_indices_from_cut_cff_index(&cut_cff_index);
1534                        let lu_cut_params =
1535                            kinematic_point.cut_params_for_cut_cff_index(&cut_cff_index);
1536                        let sample = solution.rstar_sample_for_cut_cff_index(
1537                            &cut_cff_index,
1538                            variable_indices.right_threshold,
1539                        );
1540                        debug!("right threshold parameters");
1541                        let right_threshold_params: ThresholdParams<T> =
1542                            sample.extract_threshold_parameters(true);
1543                        debug!(
1544                            "LU right evaluator input: cut_group_id={}, right_threshold_id={}, cut_cff_index={:?}, lu_order={}, r={}, rstar={}",
1545                            cut_group_id.0,
1546                            right_threshold_id.0,
1547                            cut_cff_index,
1548                            order + 1,
1549                            right_threshold_params.radius,
1550                            right_threshold_params.radius_star,
1551                        );
1552                        let inverse_transformed_sample = sample.get_inverse_transformed_sample();
1553
1554                        let params = T::get_parameters(
1555                            param_builder,
1556                            (false, false),
1557                            graph,
1558                            &inverse_transformed_sample,
1559                            settings.kinematics.externals.get_helicities(),
1560                            &settings.additional_params(),
1561                            None,
1562                            Some(&right_threshold_params),
1563                            Some(&lu_cut_params),
1564                        );
1565
1566                        let result_of_this_ct = self.evaluators[cut_group_id]
1567                            .right_thresholds_evaluator[right_threshold_id]
1568                            .get_mut(&cut_cff_index)
1569                            .unwrap()
1570                            .evaluate(
1571                                params,
1572                                orientations,
1573                                settings,
1574                                evaluation_meta_data,
1575                                record_primary_timing,
1576                            )
1577                            .unwrap()
1578                            .pop()
1579                            .unwrap();
1580
1581                        debug!(
1582                            "result of right threshold evaluator {:?}: {}",
1583                            cut_cff_index, result_of_this_ct
1584                        );
1585
1586                        let helper_completed_result = evaluate_threshold_helper_single(
1587                            self.evaluators[cut_group_id]
1588                                .threshold_helpers
1589                                .right_thresholds[right_threshold_id]
1590                                .get_mut(&cut_cff_index)
1591                                .unwrap(),
1592                            &cut_cff_index,
1593                            &result_of_this_ct,
1594                            &right_threshold_params,
1595                            evaluation_meta_data,
1596                            record_primary_timing,
1597                        );
1598
1599                        let multi_channeling_factor = plain_t_dual_or_scalar_complex(
1600                            &sample.value_of_multi_channeling_factor,
1601                            variable_indices.lu_cut,
1602                        );
1603
1604                        right_evaluations += multiply_dual_or_not_complex(
1605                            helper_completed_result,
1606                            &multi_channeling_factor,
1607                        );
1608                    }
1609                }
1610            }
1611
1612            let mut cartesian_product_result = zero_dual_or_not_complex(order, &zero);
1613
1614            for left_solutions_group in left_overlap_solutions.iter() {
1615                for left_solution in left_solutions_group {
1616                    let left_sample_representative = left_solution.rstar_sample_for_order(order);
1617                    for right_solutions_group in right_overlap_solutions.iter() {
1618                        for right_solution in right_solutions_group {
1619                            let right_sample_representative =
1620                                right_solution.rstar_sample_for_order(order);
1621                            let iterated_index = (
1622                                LeftThresholdId::from(
1623                                    left_sample_representative.get_esurface_id().0,
1624                                ),
1625                                RightThresholdId::from(
1626                                    right_sample_representative.get_esurface_id().0,
1627                                ),
1628                            );
1629                            self.ensure_active_iterated_threshold(cut_group_id, iterated_index)?;
1630
1631                            let matching_cut_indices = self.evaluators[cut_group_id]
1632                                .iterated_evaluator[iterated_index]
1633                                .keys()
1634                                .filter(|cut_cff_index| {
1635                                    cut_cff_index.lu_cut_order == Some(order + 1)
1636                                })
1637                                .copied()
1638                                .collect_vec();
1639
1640                            for cut_cff_index in matching_cut_indices {
1641                                let variable_indices =
1642                                    variable_indices_from_cut_cff_index(&cut_cff_index);
1643                                let lu_cut_params =
1644                                    kinematic_point.cut_params_for_cut_cff_index(&cut_cff_index);
1645                                let sample_left = left_solution.rstar_sample_for_cut_cff_index(
1646                                    &cut_cff_index,
1647                                    variable_indices.left_threshold,
1648                                );
1649                                let sample_right = right_solution.rstar_sample_for_cut_cff_index(
1650                                    &cut_cff_index,
1651                                    variable_indices.right_threshold,
1652                                );
1653
1654                                let left_threshold_params: ThresholdParams<T> =
1655                                    sample_left.extract_threshold_parameters(false);
1656                                let right_threshold_params: ThresholdParams<T> =
1657                                    sample_right.extract_threshold_parameters(false);
1658                                debug!(
1659                                    "LU iterated evaluator input: cut_group_id={}, left_threshold_id={}, right_threshold_id={}, cut_cff_index={:?}, lu_order={}, left_r={}, left_rstar={}, right_r={}, right_rstar={}",
1660                                    cut_group_id.0,
1661                                    iterated_index.0.0,
1662                                    iterated_index.1.0,
1663                                    cut_cff_index,
1664                                    order + 1,
1665                                    left_threshold_params.radius,
1666                                    left_threshold_params.radius_star,
1667                                    right_threshold_params.radius,
1668                                    right_threshold_params.radius_star,
1669                                );
1670                                let multi_channeling_factor = plain_t_dual_or_scalar_complex(
1671                                    &multiply_dual_or_not_complex(
1672                                        sample_left.value_of_multi_channeling_factor.clone(),
1673                                        &sample_right.value_of_multi_channeling_factor,
1674                                    ),
1675                                    variable_indices.lu_cut,
1676                                );
1677                                let inverse_transformed_momentum_sample =
1678                                    merge_and_inverse_transform(&sample_left, &sample_right);
1679
1680                                let params = T::get_parameters(
1681                                    param_builder,
1682                                    (false, false),
1683                                    graph,
1684                                    &inverse_transformed_momentum_sample,
1685                                    settings.kinematics.externals.get_helicities(),
1686                                    &settings.additional_params(),
1687                                    Some(&left_threshold_params),
1688                                    Some(&right_threshold_params),
1689                                    Some(&lu_cut_params),
1690                                );
1691
1692                                let result_of_this_ct = self.evaluators[cut_group_id]
1693                                    .iterated_evaluator[iterated_index]
1694                                    .get_mut(&cut_cff_index)
1695                                    .unwrap()
1696                                    .evaluate(
1697                                        params,
1698                                        orientations,
1699                                        settings,
1700                                        evaluation_meta_data,
1701                                        record_primary_timing,
1702                                    )
1703                                    .unwrap()
1704                                    .pop()
1705                                    .unwrap();
1706
1707                                let helper_completed_result = evaluate_threshold_helper_iterated(
1708                                    self.evaluators[cut_group_id].threshold_helpers.iterated
1709                                        [iterated_index]
1710                                        .get_mut(&cut_cff_index)
1711                                        .unwrap(),
1712                                    &cut_cff_index,
1713                                    &result_of_this_ct,
1714                                    &left_threshold_params,
1715                                    &right_threshold_params,
1716                                    evaluation_meta_data,
1717                                    record_primary_timing,
1718                                );
1719
1720                                cartesian_product_result += multiply_dual_or_not_complex(
1721                                    helper_completed_result,
1722                                    &multi_channeling_factor,
1723                                );
1724                            }
1725                        }
1726                    }
1727                }
1728            }
1729
1730            let mut pass_one_result = left_evaluations.clone();
1731            pass_one_result += right_evaluations;
1732            pass_one_result += negate_dual_or_not_complex(cartesian_product_result);
1733            let pass_one_result = negate_dual_or_not_complex(pass_one_result);
1734
1735            let mut params_for_pass_two = vec![];
1736            match pass_one_result {
1737                DualOrNot::Dual(dual_result) => {
1738                    params_for_pass_two
1739                        .extend_from_slice(&extract_t_derivatives_complex(dual_result));
1740                }
1741                DualOrNot::NonDual(non_dual_result) => {
1742                    params_for_pass_two.push(non_dual_result);
1743                }
1744            }
1745
1746            match kinematic_point.cut_esurface_for_order(order).clone() {
1747                DualOrNot::Dual(dual_e_surface) => {
1748                    extract_t_derivatives(dual_e_surface)[1..]
1749                        .iter()
1750                        .for_each(|value| params_for_pass_two.push(Complex::new_re(value.clone())));
1751                }
1752                DualOrNot::NonDual(non_dual_e_surface) => {
1753                    debug!("non dual esurface: {}", non_dual_e_surface);
1754                    params_for_pass_two.push(Complex::new_re(non_dual_e_surface));
1755                }
1756            }
1757
1758            debug!(
1759                "LU pass-two evaluator input: cut_group_id={}, lu_order={}, param_count={}",
1760                cut_group_id.0,
1761                order + 1,
1762                params_for_pass_two.len()
1763            );
1764            for (idx, value) in params_for_pass_two.iter().enumerate() {
1765                debug!(
1766                    "LU pass-two evaluator param: cut_group_id={}, lu_order={}, index={}, value={}",
1767                    cut_group_id.0,
1768                    order + 1,
1769                    idx,
1770                    value
1771                );
1772            }
1773
1774            let pass_two_result = evaluate_evaluator_single(
1775                &mut self.evaluators[cut_group_id].residue_from_e_surface_evaluators[order],
1776                &params_for_pass_two,
1777                evaluation_meta_data,
1778                record_primary_timing,
1779            );
1780
1781            total_result += pass_two_result;
1782        }
1783
1784        Ok(total_result)
1785    }
1786}
1787
1788struct CounterTermBuilder<'a, T: FloatLike> {
1789    overlap_structure: &'a OverlapStructure,
1790    real_mass_vector: &'a EdgeVec<F<T>>,
1791    e_cm: F<T>,
1792    graph: &'a Graph,
1793    subspace: &'a SubspaceData,
1794    all_lmbs: &'a TiVec<LmbIndex, LoopMomentumBasis>,
1795    settings: &'a RuntimeSettings,
1796    esurface_collection: &'a EsurfaceCollection,
1797    transformed_kinematic_point: LUCTKinematicPoint<T>,
1798    probe_rotation: &'a Rotation,
1799}
1800
1801impl<'a, T: FloatLike> CounterTermBuilder<'a, T> {
1802    #[allow(clippy::too_many_arguments)]
1803    fn new(
1804        graph: &'a Graph,
1805        settings: &'a RuntimeSettings,
1806        esurface_collection: &'a EsurfaceCollection,
1807        transformed_kinematic_point: LUCTKinematicPoint<T>,
1808        overlap_structure: &'a OverlapStructure,
1809        masses: &'a EdgeVec<F<T>>,
1810        all_lmbs: &'a TiVec<LmbIndex, LoopMomentumBasis>,
1811        subspace: &'a SubspaceData,
1812        probe_rotation: &'a Rotation,
1813    ) -> Self {
1814        let e_cm = F::from_f64(settings.kinematics.e_cm);
1815
1816        Self {
1817            real_mass_vector: masses,
1818            e_cm,
1819            graph,
1820            settings,
1821            esurface_collection,
1822            overlap_structure,
1823            transformed_kinematic_point,
1824            all_lmbs,
1825            subspace,
1826            probe_rotation,
1827        }
1828    }
1829
1830    fn new_overlap_builder(&'a self, overlap_group: &'a OverlapGroup) -> OverlapBuilder<'a, T> {
1831        let subspace = self.subspace;
1832
1833        let center = overlap_group.center.cast();
1834
1835        let shifted_loop_momenta = self
1836            .transformed_kinematic_point
1837            .representative_sample()
1838            .loop_moms()
1839            - &center;
1840
1841        let radius = shifted_loop_momenta
1842            .hyper_radius_squared(Some(&subspace.iter_lmb_indices().collect_vec()))
1843            .sqrt();
1844        let unit_shifted_momenta = shifted_loop_momenta.rescale(
1845            &radius.inv(),
1846            Some(&subspace.iter_lmb_indices().collect_vec()),
1847        );
1848
1849        OverlapBuilder {
1850            counterterm_builder: self,
1851            overlap_group,
1852            center,
1853            unit_shifted_momenta,
1854            radius,
1855        }
1856    }
1857}
1858
1859struct OverlapBuilder<'a, T: FloatLike> {
1860    counterterm_builder: &'a CounterTermBuilder<'a, T>,
1861    overlap_group: &'a OverlapGroup,
1862    /// Solver-derived centers already belong to the current probe and cut-side LMB frame.
1863    center: LoopMomenta<F<T>>,
1864    unit_shifted_momenta: LoopMomenta<F<T>>,
1865    radius: F<T>,
1866}
1867
1868impl<'a, T: FloatLike> OverlapBuilder<'a, T> {
1869    fn new_esurface_builder(
1870        &'a self,
1871        existing_esurface_id: ExistingEsurfaceId,
1872    ) -> EsurfaceCTBuilder<'a, T> {
1873        let esurface_id = self
1874            .counterterm_builder
1875            .overlap_structure
1876            .existing_esurfaces[existing_esurface_id];
1877
1878        EsurfaceCTBuilder {
1879            overlap_builder: self,
1880            _existing_esurface_id: existing_esurface_id,
1881            esurface: &self.counterterm_builder.esurface_collection[esurface_id],
1882            esurface_id,
1883        }
1884    }
1885}
1886
1887const MAX_ITERATIONS: usize = 40;
1888
1889struct EsurfaceCTBuilder<'a, T: FloatLike> {
1890    overlap_builder: &'a OverlapBuilder<'a, T>,
1891    _existing_esurface_id: ExistingEsurfaceId,
1892    esurface: &'a Esurface,
1893    esurface_id: EsurfaceID,
1894}
1895
1896impl<'a, T: FloatLike> EsurfaceCTBuilder<'a, T> {
1897    fn solve_rstar(
1898        self,
1899        rstar_t_dependence_evaluator: &mut RstarTDependenceEvaluator,
1900        radial_root_identity: &RadialRootIdentity,
1901        radial_root_diagnostics: &mut RadialRootDiagnostics,
1902    ) -> Option<RstarSolution<'a, T>> {
1903        let subspace = self.overlap_builder.counterterm_builder.subspace;
1904        let graph = self.overlap_builder.counterterm_builder.graph;
1905        let lmbs = self.overlap_builder.counterterm_builder.all_lmbs;
1906        let masses = self.overlap_builder.counterterm_builder.real_mass_vector;
1907
1908        debug!("subspace: {:?}", subspace);
1909
1910        let representative_sample = self
1911            .overlap_builder
1912            .counterterm_builder
1913            .transformed_kinematic_point
1914            .representative_sample();
1915        let mut center_with_fixed_complement = representative_sample.loop_moms().clone();
1916        for loop_index in subspace.iter_lmb_indices() {
1917            center_with_fixed_complement[loop_index] =
1918                self.overlap_builder.center[loop_index].clone();
1919        }
1920
1921        let center_surface_values = self
1922            .overlap_builder
1923            .overlap_group
1924            .existing_esurfaces
1925            .iter()
1926            .map(|&existing_esurface_id| {
1927                let esurface_id = self
1928                    .overlap_builder
1929                    .counterterm_builder
1930                    .overlap_structure
1931                    .existing_esurfaces[existing_esurface_id];
1932                let esurface =
1933                    &self.overlap_builder.counterterm_builder.esurface_collection[esurface_id];
1934                let value = esurface.compute_from_momenta(
1935                    subspace.get_lmb(lmbs),
1936                    masses,
1937                    &center_with_fixed_complement,
1938                    representative_sample.external_moms(),
1939                );
1940                let is_valid = esurface_value_is_strictly_inside(
1941                    &value,
1942                    &self.overlap_builder.counterterm_builder.e_cm,
1943                );
1944                (esurface_id, value, is_valid)
1945            })
1946            .collect_vec();
1947        let all_center_values_valid = center_surface_values
1948            .iter()
1949            .all(|(_, _, is_valid)| *is_valid);
1950
1951        crate::debug_tags!(#integration, #subtraction, #threshold, #inspect, #center;
1952            stage = "lu_threshold_center_values",
1953            graph = %graph.name,
1954            selected_esurface_id = self.esurface_id.0,
1955            rotation_id = %self.overlap_builder.counterterm_builder.probe_rotation.method,
1956            center_provenance = "current_probe_cut_lmb_frame",
1957            subspace_loop_indices = ?subspace.iter_lmb_indices().collect_vec(),
1958            file.active_center = %format!("{}", self.overlap_builder.center),
1959            file.center_with_fixed_complement = %format!("{}", center_with_fixed_complement),
1960            file.surface_values = %center_surface_values
1961                .iter()
1962                .map(|(esurface_id, value, is_valid)| format!(
1963                    "local={} value={:+16e} inside={}",
1964                    esurface_id.0,
1965                    value,
1966                    is_valid,
1967                ))
1968                .join("\n"),
1969            "LU threshold center values"
1970        );
1971
1972        if !all_center_values_valid {
1973            warn!(
1974                graph = %graph.name,
1975                selected_esurface_id = self.esurface_id.0,
1976                rotation_id = %self.overlap_builder.counterterm_builder.probe_rotation.method,
1977                center_provenance = "current_probe_cut_lmb_frame",
1978                center = %center_with_fixed_complement,
1979                surface_values = %center_surface_values
1980                    .iter()
1981                    .map(|(esurface_id, value, is_valid)| format!(
1982                        "local={} value={:+16e} inside={}",
1983                        esurface_id.0,
1984                        value,
1985                        is_valid,
1986                    ))
1987                    .join("; "),
1988                "refusing to evaluate an LU threshold counterterm with an invalid probe-frame overlap center"
1989            );
1990            return None;
1991        }
1992
1993        let (raw_radius_guess, _) = self.esurface.get_radius_guess_subspace(
1994            &self.overlap_builder.unit_shifted_momenta,
1995            self.overlap_builder
1996                .counterterm_builder
1997                .transformed_kinematic_point
1998                .representative_sample()
1999                .external_moms(),
2000            subspace,
2001            lmbs,
2002            graph,
2003            masses,
2004        );
2005
2006        let function = |r: &_| {
2007            self.esurface.compute_self_and_r_derivative_subspace(
2008                r,
2009                &self.overlap_builder.unit_shifted_momenta,
2010                &self.overlap_builder.center,
2011                self.overlap_builder
2012                    .counterterm_builder
2013                    .transformed_kinematic_point
2014                    .representative_sample()
2015                    .external_moms(),
2016                self.overlap_builder.counterterm_builder.real_mass_vector,
2017                subspace,
2018                lmbs,
2019                graph,
2020            )
2021        };
2022
2023        let zero = raw_radius_guess.zero();
2024        let mut radius_guess = raw_radius_guess.clone();
2025        if radius_guess.is_nan() || radius_guess.is_infinite() || radius_guess <= zero {
2026            radius_guess = self.overlap_builder.counterterm_builder.e_cm.clone();
2027        }
2028
2029        debug!("initial radius guess: {:?}", radius_guess);
2030
2031        // Some residual is expected when Newton stagnates at the representable value nearest the
2032        // root. Direct convergence uses the active precision; the shared diagnostics may also
2033        // certify a residual-limited higher-precision result against the preceding precision.
2034        let tolerance = F::from_f64(
2035            self.overlap_builder
2036                .counterterm_builder
2037                .settings
2038                .subtraction
2039                .radial_root_residual_tolerance,
2040        );
2041        let solution = match radial_root_diagnostics.solve(
2042            radial_root_identity,
2043            &zero,
2044            &radius_guess,
2045            function,
2046            &tolerance,
2047            MAX_ITERATIONS,
2048            64,
2049            &self.overlap_builder.counterterm_builder.e_cm,
2050        ) {
2051            Ok(solution) => solution,
2052            Err(error) => {
2053                warn!(
2054                    graph = %graph.name,
2055                    esurface_id = self.esurface_id.0,
2056                    rotation_id = %self.overlap_builder.counterterm_builder.probe_rotation.method,
2057                    center_provenance = "current_probe_cut_lmb_frame",
2058                    raw_radius_guess = %raw_radius_guess,
2059                    radius_guess = %radius_guess,
2060                    error = %error,
2061                    "refusing to evaluate a threshold counterterm with an invalid radial solution"
2062                );
2063                return None;
2064            }
2065        };
2066
2067        debug!("r* solution: {:?}", solution);
2068
2069        let t_dependent_solution = if rstar_t_dependence_evaluator.supports_t_derivatives() {
2070            let t_star = match &self
2071                .overlap_builder
2072                .counterterm_builder
2073                .transformed_kinematic_point
2074                .non_dual_cut_params()
2075                .tstar
2076            {
2077                DualOrNot::NonDual(t_star) => t_star.clone(),
2078                DualOrNot::Dual(_) => {
2079                    unreachable!("representative LU cut parameters must stay non-dual")
2080                }
2081            };
2082
2083            Some(
2084                rstar_t_dependence_evaluator.evaluate(RstarTDependenceInput {
2085                    t_star: &t_star,
2086                    radius_star: &solution.solution,
2087                    overlap_center: &self.overlap_builder.center,
2088                    subspace,
2089                    unrescaled_momentum_sample: self
2090                        .overlap_builder
2091                        .counterterm_builder
2092                        .transformed_kinematic_point
2093                        .unrescaled_sample(),
2094                    masses,
2095                    threshold_esurface: self.esurface,
2096                    lmb: &self
2097                        .overlap_builder
2098                        .counterterm_builder
2099                        .graph
2100                        .loop_momentum_basis,
2101                    all_lmbs: lmbs,
2102                }),
2103            )
2104        } else {
2105            None
2106        };
2107
2108        Some(RstarSolution {
2109            esurface_ct_builder: self,
2110            solution,
2111            t_dependent_solution,
2112        })
2113    }
2114}
2115
2116struct RstarSolution<'a, T: FloatLike> {
2117    esurface_ct_builder: EsurfaceCTBuilder<'a, T>,
2118    solution: NewtonIterationResult<T>,
2119    t_dependent_solution: Option<HyperDual<F<T>>>,
2120}
2121
2122struct DualRstarGeometry<T: FloatLike> {
2123    radius: HyperDual<F<T>>,
2124    radius_star: HyperDual<F<T>>,
2125    esurface_derivative: HyperDual<F<T>>,
2126    rstar_loop_momenta: LoopMomenta<HyperDual<F<T>>>,
2127    external_moms: ExternalFourMomenta<HyperDual<F<T>>>,
2128}
2129
2130impl<'a, T: FloatLike> RstarSolution<'a, T> {
2131    fn max_supported_order(&self) -> usize {
2132        self.t_dependent_solution
2133            .as_ref()
2134            .map(|dual| dual.values.len().saturating_sub(1))
2135            .unwrap_or(0)
2136    }
2137
2138    fn base_rstar_loop_momenta(&self) -> LoopMomenta<F<T>> {
2139        let subspace = self
2140            .esurface_ct_builder
2141            .overlap_builder
2142            .counterterm_builder
2143            .subspace;
2144
2145        &self
2146            .esurface_ct_builder
2147            .overlap_builder
2148            .unit_shifted_momenta
2149            .rescale(&self.solution.solution, subspace.as_subspace_simple())
2150            + &self.esurface_ct_builder.overlap_builder.center
2151    }
2152
2153    fn truncated_rstar_solution(&self, order: usize) -> HyperDual<F<T>> {
2154        let dual_solution = self
2155            .t_dependent_solution
2156            .as_ref()
2157            .expect("higher-order LU threshold evaluation requires cached r_star(t)");
2158        HyperDual::from_values(
2159            simple_n_deriv_shape(order),
2160            dual_solution.values[..=order].to_vec(),
2161        )
2162    }
2163
2164    fn embedded_truncated_rstar_solution(&self, cut_cff_index: &CutCFFIndex) -> HyperDual<F<T>> {
2165        let lu_order = cut_cff_index
2166            .lu_cut_order
2167            .expect("mixed LU geometry requires lu_cut_order")
2168            - 1;
2169        let target_shape = shape_from_cut_cff_index(cut_cff_index)
2170            .map(HyperDual::new)
2171            .expect("mixed LU geometry requires a dual shape");
2172        let t_variable = variable_indices_from_cut_cff_index(cut_cff_index).lu_cut;
2173        let t_dual = if lu_order == 0 {
2174            HyperDual::from_values(
2175                simple_n_deriv_shape(0),
2176                vec![self.solution.solution.clone()],
2177            )
2178        } else {
2179            self.truncated_rstar_solution(lu_order)
2180        };
2181
2182        embed_t_dual_in_target_shape(&t_dual, &target_shape, t_variable)
2183    }
2184
2185    fn dual_geometry_for_order(&self, order: usize) -> DualRstarGeometry<T> {
2186        let source_sample = self
2187            .esurface_ct_builder
2188            .overlap_builder
2189            .counterterm_builder
2190            .transformed_kinematic_point
2191            .sample_for_order(order);
2192        let dual_loop_momenta = source_sample
2193            .sample
2194            .dual_loop_moms
2195            .as_ref()
2196            .expect("higher-order LU threshold evaluation requires dual loop momenta")
2197            .clone();
2198
2199        let radius_star = self.truncated_rstar_solution(order);
2200        let center = dualize_loop_momenta(
2201            &radius_star,
2202            &self.esurface_ct_builder.overlap_builder.center,
2203        );
2204        let external_moms = dualize_external_momenta(&radius_star, source_sample.external_moms());
2205
2206        let shifted_momenta = dual_loop_momenta
2207            .iter()
2208            .zip(center.iter())
2209            .map(|(momentum, center)| momentum.clone() - center.clone())
2210            .collect::<LoopMomenta<_>>();
2211
2212        let radius = dual_shifted_radius(
2213            &shifted_momenta,
2214            self.esurface_ct_builder
2215                .overlap_builder
2216                .counterterm_builder
2217                .subspace,
2218        );
2219        let inverse_radius = new_constant(&radius, &radius.values[0].one()) / radius.clone();
2220        let unit_shifted_momenta = shifted_momenta.rescale(
2221            &inverse_radius,
2222            self.esurface_ct_builder
2223                .overlap_builder
2224                .counterterm_builder
2225                .subspace
2226                .as_subspace_simple(),
2227        );
2228
2229        let rstar_loop_momenta = unit_shifted_momenta
2230            .rescale(
2231                &radius_star,
2232                self.esurface_ct_builder
2233                    .overlap_builder
2234                    .counterterm_builder
2235                    .subspace
2236                    .as_subspace_simple(),
2237            )
2238            .iter()
2239            .zip(center.iter())
2240            .map(|(momentum, center)| momentum.clone() + center.clone())
2241            .collect();
2242
2243        let (_, esurface_derivative) = compute_self_and_r_derivative_subspace_dual(
2244            self.esurface_ct_builder.esurface,
2245            &radius_star,
2246            &unit_shifted_momenta,
2247            &center,
2248            &external_moms,
2249            self.esurface_ct_builder
2250                .overlap_builder
2251                .counterterm_builder
2252                .real_mass_vector,
2253            self.esurface_ct_builder
2254                .overlap_builder
2255                .counterterm_builder
2256                .subspace,
2257            self.esurface_ct_builder
2258                .overlap_builder
2259                .counterterm_builder
2260                .all_lmbs,
2261            self.esurface_ct_builder
2262                .overlap_builder
2263                .counterterm_builder
2264                .graph,
2265        );
2266
2267        DualRstarGeometry {
2268            radius,
2269            radius_star,
2270            esurface_derivative,
2271            rstar_loop_momenta,
2272            external_moms,
2273        }
2274    }
2275
2276    fn dual_geometry_for_cut_cff_index(
2277        &self,
2278        cut_cff_index: &CutCFFIndex,
2279        threshold_variable: Option<usize>,
2280    ) -> DualRstarGeometry<T> {
2281        let lu_order = cut_cff_index
2282            .lu_cut_order
2283            .expect("mixed LU geometry requires lu_cut_order")
2284            - 1;
2285        let source_sample = self
2286            .esurface_ct_builder
2287            .overlap_builder
2288            .counterterm_builder
2289            .transformed_kinematic_point
2290            .sample_for_order(lu_order);
2291        let target_shape = shape_from_cut_cff_index(cut_cff_index)
2292            .map(HyperDual::new)
2293            .expect("mixed LU geometry requires a dual shape");
2294        let t_variable = variable_indices_from_cut_cff_index(cut_cff_index).lu_cut;
2295        let dual_loop_momenta = embedded_dual_loop_momenta_for_cut_cff_index(
2296            source_sample,
2297            &Some(target_shape.clone()),
2298            t_variable,
2299        )
2300        .expect("mixed LU geometry requires dual loop momenta");
2301
2302        let mut radius_star = self.embedded_truncated_rstar_solution(cut_cff_index);
2303        activate_threshold_variable_in_target_shape(&mut radius_star, threshold_variable);
2304        let center = dualize_loop_momenta(
2305            &radius_star,
2306            &self.esurface_ct_builder.overlap_builder.center,
2307        );
2308        let external_moms = dualize_external_momenta(&radius_star, source_sample.external_moms());
2309
2310        let shifted_momenta = dual_loop_momenta
2311            .iter()
2312            .zip(center.iter())
2313            .map(|(momentum, center)| momentum.clone() - center.clone())
2314            .collect::<LoopMomenta<_>>();
2315
2316        let radius = dual_shifted_radius(
2317            &shifted_momenta,
2318            self.esurface_ct_builder
2319                .overlap_builder
2320                .counterterm_builder
2321                .subspace,
2322        );
2323        let inverse_radius = new_constant(&radius, &radius.values[0].one()) / radius.clone();
2324        let unit_shifted_momenta = shifted_momenta.rescale(
2325            &inverse_radius,
2326            self.esurface_ct_builder
2327                .overlap_builder
2328                .counterterm_builder
2329                .subspace
2330                .as_subspace_simple(),
2331        );
2332
2333        let rstar_loop_momenta = unit_shifted_momenta
2334            .rescale(
2335                &radius_star,
2336                self.esurface_ct_builder
2337                    .overlap_builder
2338                    .counterterm_builder
2339                    .subspace
2340                    .as_subspace_simple(),
2341            )
2342            .iter()
2343            .zip(center.iter())
2344            .map(|(momentum, center)| momentum.clone() + center.clone())
2345            .collect();
2346
2347        let (_, esurface_derivative) = compute_self_and_r_derivative_subspace_dual(
2348            self.esurface_ct_builder.esurface,
2349            &radius_star,
2350            &unit_shifted_momenta,
2351            &center,
2352            &external_moms,
2353            self.esurface_ct_builder
2354                .overlap_builder
2355                .counterterm_builder
2356                .real_mass_vector,
2357            self.esurface_ct_builder
2358                .overlap_builder
2359                .counterterm_builder
2360                .subspace,
2361            self.esurface_ct_builder
2362                .overlap_builder
2363                .counterterm_builder
2364                .all_lmbs,
2365            self.esurface_ct_builder
2366                .overlap_builder
2367                .counterterm_builder
2368                .graph,
2369        );
2370
2371        DualRstarGeometry {
2372            radius,
2373            radius_star,
2374            esurface_derivative,
2375            rstar_loop_momenta,
2376            external_moms,
2377        }
2378    }
2379
2380    fn non_dual_multichanneling_factor(&self, rstar_sample: &MomentumSample<T>) -> Complex<F<T>> {
2381        let subspace = self
2382            .esurface_ct_builder
2383            .overlap_builder
2384            .counterterm_builder
2385            .subspace;
2386
2387        let multi_channeling_denominator = self
2388            .esurface_ct_builder
2389            .overlap_builder
2390            .counterterm_builder
2391            .overlap_structure
2392            .overlap_groups
2393            .iter()
2394            .map(|group| {
2395                group
2396                    .complement
2397                    .iter()
2398                    .map(|existing_esurface_id| {
2399                        let esurface_id = self
2400                            .esurface_ct_builder
2401                            .overlap_builder
2402                            .counterterm_builder
2403                            .overlap_structure
2404                            .existing_esurfaces[*existing_esurface_id];
2405                        let esurface = &self
2406                            .esurface_ct_builder
2407                            .overlap_builder
2408                            .counterterm_builder
2409                            .esurface_collection[esurface_id];
2410                        let esurface_value = esurface.compute_from_momenta(
2411                            subspace.get_lmb(
2412                                self.esurface_ct_builder
2413                                    .overlap_builder
2414                                    .counterterm_builder
2415                                    .all_lmbs,
2416                            ),
2417                            self.esurface_ct_builder
2418                                .overlap_builder
2419                                .counterterm_builder
2420                                .real_mass_vector,
2421                            rstar_sample.loop_moms(),
2422                            rstar_sample.external_moms(),
2423                        );
2424
2425                        &esurface_value * &esurface_value
2426                    })
2427                    .fold(rstar_sample.one(), |acc, value| acc * value)
2428            })
2429            .fold(rstar_sample.zero(), |acc, value| acc + value);
2430
2431        let multichanneling_numerator = self
2432            .esurface_ct_builder
2433            .overlap_builder
2434            .overlap_group
2435            .complement
2436            .iter()
2437            .map(|existing_esurface_id| {
2438                let esurface_id = self
2439                    .esurface_ct_builder
2440                    .overlap_builder
2441                    .counterterm_builder
2442                    .overlap_structure
2443                    .existing_esurfaces[*existing_esurface_id];
2444                let esurface = &self
2445                    .esurface_ct_builder
2446                    .overlap_builder
2447                    .counterterm_builder
2448                    .esurface_collection[esurface_id];
2449                let esurface_value = esurface.compute_from_momenta(
2450                    subspace.get_lmb(
2451                        self.esurface_ct_builder
2452                            .overlap_builder
2453                            .counterterm_builder
2454                            .all_lmbs,
2455                    ),
2456                    self.esurface_ct_builder
2457                        .overlap_builder
2458                        .counterterm_builder
2459                        .real_mass_vector,
2460                    rstar_sample.loop_moms(),
2461                    rstar_sample.external_moms(),
2462                );
2463
2464                &esurface_value * &esurface_value
2465            })
2466            .fold(rstar_sample.one(), |acc, value| acc * value);
2467
2468        Complex::new_re(multichanneling_numerator / multi_channeling_denominator)
2469    }
2470
2471    fn dual_multichanneling_factor(&self, geometry: &DualRstarGeometry<T>) -> HyperDual<F<T>> {
2472        let subspace = self
2473            .esurface_ct_builder
2474            .overlap_builder
2475            .counterterm_builder
2476            .subspace;
2477        let lmb = subspace.get_lmb(
2478            self.esurface_ct_builder
2479                .overlap_builder
2480                .counterterm_builder
2481                .all_lmbs,
2482        );
2483        let zero = new_constant(&geometry.radius, &geometry.radius.values[0].zero());
2484        let one = new_constant(&geometry.radius, &geometry.radius.values[0].one());
2485
2486        let multi_channeling_denominator = self
2487            .esurface_ct_builder
2488            .overlap_builder
2489            .counterterm_builder
2490            .overlap_structure
2491            .overlap_groups
2492            .iter()
2493            .map(|group| {
2494                group
2495                    .complement
2496                    .iter()
2497                    .map(|existing_esurface_id| {
2498                        let esurface_id = self
2499                            .esurface_ct_builder
2500                            .overlap_builder
2501                            .counterterm_builder
2502                            .overlap_structure
2503                            .existing_esurfaces[*existing_esurface_id];
2504                        let esurface = &self
2505                            .esurface_ct_builder
2506                            .overlap_builder
2507                            .counterterm_builder
2508                            .esurface_collection[esurface_id];
2509                        let esurface_value = esurface.compute_from_dual_momenta(
2510                            lmb,
2511                            self.esurface_ct_builder
2512                                .overlap_builder
2513                                .counterterm_builder
2514                                .real_mass_vector,
2515                            &geometry.rstar_loop_momenta,
2516                            &geometry.external_moms,
2517                        );
2518
2519                        esurface_value.clone() * esurface_value
2520                    })
2521                    .fold(one.clone(), |acc, value| acc * value)
2522            })
2523            .fold(zero.clone(), |acc, value| acc + value);
2524
2525        let multi_channeling_numerator = self
2526            .esurface_ct_builder
2527            .overlap_builder
2528            .overlap_group
2529            .complement
2530            .iter()
2531            .map(|existing_esurface_id| {
2532                let esurface_id = self
2533                    .esurface_ct_builder
2534                    .overlap_builder
2535                    .counterterm_builder
2536                    .overlap_structure
2537                    .existing_esurfaces[*existing_esurface_id];
2538                let esurface = &self
2539                    .esurface_ct_builder
2540                    .overlap_builder
2541                    .counterterm_builder
2542                    .esurface_collection[esurface_id];
2543                let esurface_value = esurface.compute_from_dual_momenta(
2544                    lmb,
2545                    self.esurface_ct_builder
2546                        .overlap_builder
2547                        .counterterm_builder
2548                        .real_mass_vector,
2549                    &geometry.rstar_loop_momenta,
2550                    &geometry.external_moms,
2551                );
2552
2553                esurface_value.clone() * esurface_value
2554            })
2555            .fold(one, |acc, value| acc * value);
2556
2557        multi_channeling_numerator / multi_channeling_denominator
2558    }
2559
2560    fn rstar_sample_for_order<'solution>(
2561        &'solution self,
2562        order: usize,
2563    ) -> RstarSample<'solution, 'a, T> {
2564        let base_rstar_loop_momenta = self.base_rstar_loop_momenta();
2565
2566        if order == 0 {
2567            let mut rstar_sample = self
2568                .esurface_ct_builder
2569                .overlap_builder
2570                .counterterm_builder
2571                .transformed_kinematic_point
2572                .representative_sample()
2573                .clone();
2574            rstar_sample.sample.loop_moms = base_rstar_loop_momenta;
2575            rstar_sample.sample.dual_loop_moms = None;
2576
2577            let radius = self.esurface_ct_builder.overlap_builder.radius.clone();
2578            let radius_star = self.solution.solution.clone();
2579            let esurface_derivative = self.solution.derivative_at_solution.clone();
2580            let e_cm = &self
2581                .esurface_ct_builder
2582                .overlap_builder
2583                .counterterm_builder
2584                .e_cm;
2585            let uv_localisation_settings = &self
2586                .esurface_ct_builder
2587                .overlap_builder
2588                .counterterm_builder
2589                .settings
2590                .subtraction
2591                .local_ct_settings
2592                .uv_localisation;
2593
2594            let threshold_params = ThresholdParams {
2595                radius: DualOrNot::NonDual(radius.clone()),
2596                radius_star: DualOrNot::NonDual(radius_star.clone()),
2597                esurface_derivative: DualOrNot::NonDual(esurface_derivative.clone()),
2598                uv_damp_plus: DualOrNot::NonDual(evaluate_uv_damper(
2599                    &radius,
2600                    &radius_star,
2601                    e_cm,
2602                    uv_localisation_settings,
2603                )),
2604                uv_damp_minus: DualOrNot::NonDual(evaluate_uv_damper(
2605                    &-radius.clone(),
2606                    &radius_star,
2607                    e_cm,
2608                    uv_localisation_settings,
2609                )),
2610                h_function: DualOrNot::NonDual(evaluate_integrated_ct_normalisation(
2611                    &radius,
2612                    &radius_star,
2613                    e_cm,
2614                    &self
2615                        .esurface_ct_builder
2616                        .overlap_builder
2617                        .counterterm_builder
2618                        .settings
2619                        .subtraction
2620                        .integrated_ct_settings,
2621                )),
2622            };
2623
2624            let value_of_multi_channeling_factor =
2625                DualOrNot::NonDual(self.non_dual_multichanneling_factor(&rstar_sample));
2626
2627            return RstarSample {
2628                rstar_solution: self,
2629                rstar_sample,
2630                threshold_params,
2631                value_of_multi_channeling_factor,
2632            };
2633        }
2634
2635        assert!(
2636            order <= self.max_supported_order(),
2637            "requested LU threshold derivative order {} but only {} orders are cached",
2638            order,
2639            self.max_supported_order()
2640        );
2641
2642        let geometry = self.dual_geometry_for_order(order);
2643        let mut rstar_sample = self
2644            .esurface_ct_builder
2645            .overlap_builder
2646            .counterterm_builder
2647            .transformed_kinematic_point
2648            .sample_for_order(order)
2649            .clone();
2650        rstar_sample.sample.loop_moms = base_rstar_loop_momenta;
2651        rstar_sample.sample.dual_loop_moms = Some(geometry.rstar_loop_momenta.clone());
2652
2653        let e_cm = &self
2654            .esurface_ct_builder
2655            .overlap_builder
2656            .counterterm_builder
2657            .e_cm;
2658        let uv_localisation_settings = &self
2659            .esurface_ct_builder
2660            .overlap_builder
2661            .counterterm_builder
2662            .settings
2663            .subtraction
2664            .local_ct_settings
2665            .uv_localisation;
2666
2667        let threshold_params = ThresholdParams {
2668            radius: DualOrNot::Dual(geometry.radius.clone()),
2669            radius_star: DualOrNot::Dual(geometry.radius_star.clone()),
2670            esurface_derivative: DualOrNot::Dual(geometry.esurface_derivative.clone()),
2671            uv_damp_plus: DualOrNot::Dual(evaluate_uv_damper_dual(
2672                &geometry.radius,
2673                &geometry.radius_star,
2674                e_cm,
2675                uv_localisation_settings,
2676            )),
2677            uv_damp_minus: DualOrNot::Dual(evaluate_uv_damper_dual(
2678                &-geometry.radius.clone(),
2679                &geometry.radius_star,
2680                e_cm,
2681                uv_localisation_settings,
2682            )),
2683            h_function: DualOrNot::Dual(evaluate_integrated_ct_normalisation_dual(
2684                &geometry.radius,
2685                &geometry.radius_star,
2686                e_cm,
2687                &self
2688                    .esurface_ct_builder
2689                    .overlap_builder
2690                    .counterterm_builder
2691                    .settings
2692                    .subtraction
2693                    .integrated_ct_settings,
2694            )),
2695        };
2696
2697        let value_of_multi_channeling_factor = DualOrNot::Dual(real_dual_to_complex(
2698            self.dual_multichanneling_factor(&geometry),
2699        ));
2700
2701        RstarSample {
2702            rstar_solution: self,
2703            rstar_sample,
2704            threshold_params,
2705            value_of_multi_channeling_factor,
2706        }
2707    }
2708
2709    fn rstar_sample_for_cut_cff_index<'solution>(
2710        &'solution self,
2711        cut_cff_index: &CutCFFIndex,
2712        threshold_variable: Option<usize>,
2713    ) -> RstarSample<'solution, 'a, T> {
2714        let lu_order = cut_cff_index
2715            .lu_cut_order
2716            .expect("LU threshold sampling requires lu_cut_order")
2717            - 1;
2718
2719        if shape_from_cut_cff_index(cut_cff_index).is_none() {
2720            return self.rstar_sample_for_order(lu_order);
2721        }
2722
2723        let base_rstar_loop_momenta = self.base_rstar_loop_momenta();
2724        let geometry = self.dual_geometry_for_cut_cff_index(cut_cff_index, threshold_variable);
2725        let mut rstar_sample = self
2726            .esurface_ct_builder
2727            .overlap_builder
2728            .counterterm_builder
2729            .transformed_kinematic_point
2730            .sample_for_order(lu_order)
2731            .clone();
2732        rstar_sample.sample.loop_moms = base_rstar_loop_momenta;
2733        rstar_sample.sample.dual_loop_moms = Some(geometry.rstar_loop_momenta.clone());
2734
2735        let e_cm = &self
2736            .esurface_ct_builder
2737            .overlap_builder
2738            .counterterm_builder
2739            .e_cm;
2740        let uv_localisation_settings = &self
2741            .esurface_ct_builder
2742            .overlap_builder
2743            .counterterm_builder
2744            .settings
2745            .subtraction
2746            .local_ct_settings
2747            .uv_localisation;
2748
2749        let threshold_params = ThresholdParams {
2750            radius: DualOrNot::Dual(geometry.radius.clone()),
2751            radius_star: DualOrNot::Dual(geometry.radius_star.clone()),
2752            esurface_derivative: DualOrNot::Dual(geometry.esurface_derivative.clone()),
2753            uv_damp_plus: DualOrNot::Dual(evaluate_uv_damper_dual(
2754                &geometry.radius,
2755                &geometry.radius_star,
2756                e_cm,
2757                uv_localisation_settings,
2758            )),
2759            uv_damp_minus: DualOrNot::Dual(evaluate_uv_damper_dual(
2760                &-geometry.radius.clone(),
2761                &geometry.radius_star,
2762                e_cm,
2763                uv_localisation_settings,
2764            )),
2765            h_function: DualOrNot::Dual(evaluate_integrated_ct_normalisation_dual(
2766                &geometry.radius,
2767                &geometry.radius_star,
2768                e_cm,
2769                &self
2770                    .esurface_ct_builder
2771                    .overlap_builder
2772                    .counterterm_builder
2773                    .settings
2774                    .subtraction
2775                    .integrated_ct_settings,
2776            )),
2777        };
2778
2779        let value_of_multi_channeling_factor = DualOrNot::Dual(real_dual_to_complex(
2780            self.dual_multichanneling_factor(&geometry),
2781        ));
2782
2783        RstarSample {
2784            rstar_solution: self,
2785            rstar_sample,
2786            threshold_params,
2787            value_of_multi_channeling_factor,
2788        }
2789    }
2790}
2791
2792struct RstarSample<'solution, 'a, T: FloatLike> {
2793    rstar_solution: &'solution RstarSolution<'a, T>,
2794    rstar_sample: MomentumSample<T>,
2795    threshold_params: ThresholdParams<T>,
2796    value_of_multi_channeling_factor: DualOrNot<Complex<F<T>>>,
2797}
2798
2799impl<'solution, 'a, T: FloatLike> RstarSample<'solution, 'a, T> {
2800    fn extract_threshold_parameters(&self, is_first_call: bool) -> ThresholdParams<T> {
2801        if is_first_call {
2802            let edges_in_esurface = self
2803                .rstar_solution
2804                .esurface_ct_builder
2805                .esurface
2806                .energies
2807                .iter()
2808                .map(|i| {
2809                    self.rstar_solution
2810                        .esurface_ct_builder
2811                        .overlap_builder
2812                        .counterterm_builder
2813                        .graph[i]
2814                        .0
2815                        .name
2816                        .clone()
2817                })
2818                .collect_vec();
2819
2820            debug!("esurface_id: {}", self.get_esurface_id().0);
2821            debug!("edges in esurface: {:?}", edges_in_esurface);
2822            debug!("radius: {}", self.threshold_params.radius);
2823            debug!("radius_star: {}", self.threshold_params.radius_star);
2824            debug!(
2825                "esurface_derivative: {}",
2826                self.threshold_params.esurface_derivative
2827            );
2828            debug!("uv_damp_plus: {}", self.threshold_params.uv_damp_plus);
2829            debug!("uv_damp_minus: {}", self.threshold_params.uv_damp_minus);
2830            debug!("h_function: {}", self.threshold_params.h_function);
2831            debug!(
2832                "value of multi-channeling factor: {}",
2833                self.value_of_multi_channeling_factor
2834            );
2835        }
2836
2837        self.threshold_params.clone()
2838    }
2839
2840    fn get_inverse_transformed_sample(&self) -> MomentumSample<T> {
2841        let subspace = self
2842            .rstar_solution
2843            .esurface_ct_builder
2844            .overlap_builder
2845            .counterterm_builder
2846            .subspace;
2847        let current_lmb = subspace.get_lmb(
2848            self.rstar_solution
2849                .esurface_ct_builder
2850                .overlap_builder
2851                .counterterm_builder
2852                .all_lmbs,
2853        );
2854        let target_lmb = &self
2855            .rstar_solution
2856            .esurface_ct_builder
2857            .overlap_builder
2858            .counterterm_builder
2859            .graph
2860            .loop_momentum_basis;
2861
2862        self.rstar_sample.lmb_transform(current_lmb, target_lmb)
2863    }
2864
2865    fn get_esurface_id(&self) -> EsurfaceID {
2866        self.rstar_solution.esurface_ct_builder.esurface_id
2867    }
2868}
2869
2870fn merge_and_inverse_transform<T: FloatLike>(
2871    left_sample: &RstarSample<'_, '_, T>,
2872    right_sample: &RstarSample<'_, '_, T>,
2873) -> MomentumSample<T> {
2874    let left_subspace = left_sample
2875        .rstar_solution
2876        .esurface_ct_builder
2877        .overlap_builder
2878        .counterterm_builder
2879        .subspace;
2880
2881    let right_subspace = right_sample
2882        .rstar_solution
2883        .esurface_ct_builder
2884        .overlap_builder
2885        .counterterm_builder
2886        .subspace;
2887
2888    assert!(
2889        left_subspace.is_mergable_with(right_subspace),
2890        "incompatible subspaces for merging samples"
2891    );
2892
2893    let mut merged_sample = left_sample.rstar_sample.clone();
2894    for lmb_index in right_subspace.iter_lmb_indices() {
2895        let right_momentum = right_sample.rstar_sample.loop_moms()[lmb_index].clone();
2896        merged_sample.sample.loop_moms[lmb_index] = right_momentum;
2897    }
2898
2899    match (
2900        merged_sample.sample.dual_loop_moms.as_mut(),
2901        right_sample.rstar_sample.sample.dual_loop_moms.as_ref(),
2902    ) {
2903        (Some(merged_dual_loop_moms), Some(right_dual_loop_moms)) => {
2904            for lmb_index in right_subspace.iter_lmb_indices() {
2905                merged_dual_loop_moms[lmb_index] = right_dual_loop_moms[lmb_index].clone();
2906            }
2907        }
2908        (None, None) => {}
2909        _ => {
2910            unreachable!("iterated LU samples must either both carry dual loop momenta or neither")
2911        }
2912    }
2913
2914    let current_lmb = left_subspace.get_lmb(
2915        left_sample
2916            .rstar_solution
2917            .esurface_ct_builder
2918            .overlap_builder
2919            .counterterm_builder
2920            .all_lmbs,
2921    );
2922
2923    let target_lmb = &left_sample
2924        .rstar_solution
2925        .esurface_ct_builder
2926        .overlap_builder
2927        .counterterm_builder
2928        .graph
2929        .loop_momentum_basis;
2930
2931    merged_sample.lmb_transform(current_lmb, target_lmb)
2932}
2933
2934#[cfg(test)]
2935mod tests {
2936    use super::{LUCounterTerm, extract_zero_threshold_coefficient_t_dual};
2937    use crate::cff::CutCFFIndex;
2938    use crate::processes::CutGroupId;
2939    use crate::utils::{
2940        F,
2941        hyperdual_utils::{new_from_values, shape_from_cut_cff_index},
2942    };
2943    use symbolica::domains::dual::HyperDual;
2944    use typed_index_collections::{TiVec, ti_vec};
2945
2946    #[test]
2947    fn inactive_cut_group_guard_reports_runtime_access() {
2948        let counterterm = LUCounterTerm {
2949            evaluators: TiVec::new(),
2950            thresholds: TiVec::new(),
2951            subspaces: TiVec::new(),
2952            rstar_dependence_calculator: TiVec::new(),
2953            active_cut_groups: ti_vec![false],
2954            active_left_thresholds: ti_vec![TiVec::new()],
2955            active_right_thresholds: ti_vec![TiVec::new()],
2956            active_iterated_thresholds: TiVec::new(),
2957        };
2958
2959        let error = counterterm
2960            .ensure_active_cut_group(CutGroupId(0))
2961            .unwrap_err();
2962        assert!(error.to_string().contains("generation marked it inactive"));
2963    }
2964
2965    #[test]
2966    fn zero_threshold_coefficient_lookup_is_explicit() {
2967        let shape = HyperDual::new(
2968            shape_from_cut_cff_index(&CutCFFIndex {
2969                left_threshold_order: Some(2),
2970                right_threshold_order: Some(2),
2971                lu_cut_order: Some(2),
2972            })
2973            .unwrap(),
2974        );
2975        let mixed_dual = new_from_values(
2976            &shape,
2977            &[
2978                F(10.0_f64),
2979                F(11.0_f64),
2980                F(20.0_f64),
2981                F(30.0_f64),
2982                F(40.0_f64),
2983                F(21.0_f64),
2984                F(31.0_f64),
2985                F(41.0_f64),
2986            ],
2987        );
2988
2989        let zero_threshold = extract_zero_threshold_coefficient_t_dual(&mixed_dual, 0);
2990
2991        assert_eq!(zero_threshold.values, vec![F(10.0_f64), F(11.0_f64)]);
2992    }
2993}