Skip to main content

gammalooprs/subtraction/
mod.rs

1use bincode_trait_derive::{Decode, Encode};
2use eyre::Result;
3use itertools::Itertools;
4use linnet::half_edge::involution::EdgeVec;
5use spenso::algebra::complex::Complex;
6use symbolica::{
7    atom::{Atom, AtomCore, Symbol},
8    domains::{
9        dual::{DualNumberStructure, HyperDual},
10        float::Real,
11    },
12    evaluate::{FunctionMap, OptimizationSettings},
13    function, parse,
14    poly::PolyVariable,
15    symbol,
16};
17use tracing::debug;
18use typed_index_collections::TiVec;
19
20use crate::{
21    GammaLoopContext,
22    cff::esurface::Esurface,
23    graph::{LmbIndex, LoopMomentumBasis},
24    integrands::{
25        evaluation::EvaluationMetaData,
26        process::{GenericEvaluator, evaluators::evaluate_evaluator},
27    },
28    momentum::{
29        Energy, FourMomentum,
30        sample::{MomentumSample, SubspaceData},
31    },
32    processes::EvaluatorSettings,
33    settings::runtime::{
34        IntegratedCounterTermRange, IntegratedCounterTermSettings, UVLocalisationSettings,
35    },
36    utils::{
37        self, F, FloatLike, GS,
38        hyperdual_utils::{DualOrNot, new_constant, simple_n_deriv_shape},
39    },
40};
41
42pub mod amplitude_counterterm;
43pub mod lu_counterterm;
44pub mod overlap;
45pub mod overlap_subspace;
46
47fn evaluate_uv_damper<T: FloatLike>(
48    radius: &F<T>,
49    radius_star: &F<T>,
50    e_cm: &F<T>,
51    settings: &UVLocalisationSettings,
52) -> F<T> {
53    if settings.force_uv_dampers_to_one {
54        return radius.one();
55    }
56
57    let normalizing_scale = match settings.dynamic_width {
58        true => radius_star,
59        false => e_cm,
60    };
61
62    let delta_r = radius - radius_star;
63    let sliver_width = F::from_f64(settings.sliver_width) * normalizing_scale;
64
65    if delta_r.abs() > sliver_width || (settings.smooth_sliver && delta_r.abs() == sliver_width) {
66        return radius.zero();
67    }
68
69    let delta_r_sq = &delta_r * &delta_r;
70    let width = F::from_f64(settings.gaussian_width) * normalizing_scale;
71    let width_sq = &width * &width;
72
73    let mut exponent = -&delta_r_sq / width_sq;
74    if settings.smooth_sliver {
75        // This even bump is one at the pole and flat to all orders at the edge.
76        let gap = sliver_width.square() - &delta_r_sq;
77        exponent -= delta_r_sq / gap;
78    }
79    exponent.exp()
80}
81
82fn evaluate_uv_damper_dual<T: FloatLike>(
83    radius: &HyperDual<F<T>>,
84    radius_star: &HyperDual<F<T>>,
85    e_cm: &F<T>,
86    settings: &UVLocalisationSettings,
87) -> HyperDual<F<T>> {
88    if settings.force_uv_dampers_to_one {
89        return new_constant(radius, &radius.values[0].one());
90    }
91
92    let normalizing_scale = if settings.dynamic_width {
93        radius_star.clone()
94    } else {
95        new_constant(radius, e_cm)
96    };
97
98    let delta_r = radius.clone() - radius_star.clone();
99    let sliver_width =
100        new_constant(radius, &F::from_f64(settings.sliver_width)) * normalizing_scale.clone();
101
102    if delta_r.values[0].abs() > sliver_width.values[0]
103        || (settings.smooth_sliver && delta_r.values[0].abs() == sliver_width.values[0])
104    {
105        return new_constant(radius, &radius.values[0].zero());
106    }
107
108    let delta_r_sq = delta_r.clone() * delta_r;
109    let width = new_constant(radius, &F::from_f64(settings.gaussian_width)) * normalizing_scale;
110    let width_sq = width.clone() * width;
111
112    let mut exponent = -delta_r_sq.clone() / width_sq;
113    if settings.smooth_sliver {
114        let gap = sliver_width.clone() * sliver_width - delta_r_sq.clone();
115        exponent -= delta_r_sq / gap;
116    }
117    exponent.exp()
118}
119
120fn evaluate_integrated_ct_normalisation<T: FloatLike>(
121    radius: &F<T>,
122    radius_star: &F<T>,
123    _e_cm: &F<T>,
124    settings: &IntegratedCounterTermSettings,
125) -> F<T> {
126    match &settings.range {
127        IntegratedCounterTermRange::Infinite {
128            h_function_settings,
129        } => {
130            // The helper's inverse radial measure leaves dr, so this density must
131            // integrate to one in r: dr / r_star = d(r / r_star).
132            let h = utils::h(&(radius / radius_star), None, None, h_function_settings);
133            h * (radius_star).inv()
134        }
135        IntegratedCounterTermRange::Compact {} => {
136            todo!();
137        }
138    }
139}
140
141fn evaluate_integrated_ct_normalisation_dual<T: FloatLike>(
142    radius: &HyperDual<F<T>>,
143    radius_star: &HyperDual<F<T>>,
144    _e_cm: &F<T>,
145    settings: &IntegratedCounterTermSettings,
146) -> HyperDual<F<T>> {
147    match &settings.range {
148        IntegratedCounterTermRange::Infinite {
149            h_function_settings,
150        } => {
151            let h = utils::h_dual(
152                &(radius.clone() / radius_star.clone()),
153                None,
154                None,
155                h_function_settings,
156            );
157            h / radius_star.clone()
158        }
159        IntegratedCounterTermRange::Compact {} => {
160            todo!();
161        }
162    }
163}
164
165#[derive(Clone, Encode, Decode)]
166#[trait_decode(trait = GammaLoopContext)]
167pub(crate) struct RstarTDependenceEvaluator {
168    dual_shape_for_esurface_evaluation: Vec<Vec<usize>>,
169    implicit_function_theorem: Option<GenericEvaluator>,
170}
171
172impl RstarTDependenceEvaluator {
173    pub(crate) fn supports_t_derivatives(&self) -> bool {
174        self.implicit_function_theorem.is_some()
175    }
176
177    fn evaluate_alpha<T: FloatLike>(
178        &mut self,
179        input: RstarTDependenceInput<'_, T>,
180        evaluation_metadata: &mut EvaluationMetaData,
181    ) -> HyperDual<F<T>> {
182        let RstarTDependenceInput {
183            t_star,
184            alpha,
185            overlap_center,
186            subspace,
187            unrescaled_momentum_sample,
188            representative_sample,
189            masses,
190            threshold_esurface,
191            lmb,
192            all_lmbs,
193        } = input;
194        debug!("t-star: {}", t_star);
195        debug!("projection alpha: {}", alpha);
196
197        let dual = HyperDual::new(self.dual_shape_for_esurface_evaluation.clone());
198        let dual_alpha = dual.variable(0, alpha.clone());
199        let dual_t = dual.variable(1, t_star.clone());
200
201        let rescale_tstar = unrescaled_momentum_sample
202            .loop_moms()
203            .rescale_with_hyper_dual(&dual_t, None);
204
205        let dualized_externals_three_momenta = unrescaled_momentum_sample
206            .external_moms()
207            .iter()
208            .map(|fm: &FourMomentum<F<T>>| fm.spatial.map_ref(&|x| new_constant(&dual, x)))
209            .collect();
210
211        let mut lmb_transform = rescale_tstar.lmb_transform(
212            lmb,
213            subspace.get_lmb(all_lmbs),
214            &dualized_externals_three_momenta,
215        );
216
217        // The derivative ray comes from raw generation data, while its base is
218        // the actual transformed representative used by the projection solve.
219        // Retain every derivative, aligning the base before differentiating eta.
220        for (momentum, base) in lmb_transform
221            .0
222            .iter_mut()
223            .zip(representative_sample.loop_moms().iter())
224        {
225            momentum.px.values[0] = base.px.clone();
226            momentum.py.values[0] = base.py.clone();
227            momentum.pz.values[0] = base.pz.clone();
228        }
229
230        let dualized_overlap_center = overlap_center
231            .iter()
232            .map(|momentum| momentum.map_ref(&|x| new_constant(&dual, x)))
233            .collect::<crate::momentum::sample::LoopMomenta<_>>();
234
235        let shifted_t_dependent_momenta = lmb_transform
236            .iter()
237            .zip(dualized_overlap_center.iter())
238            .map(|(momentum, center)| momentum.clone() - center.clone())
239            .collect::<crate::momentum::sample::LoopMomenta<_>>();
240
241        let dualized_external_fourmomenta = dualized_externals_three_momenta
242            .into_iter()
243            .zip(unrescaled_momentum_sample.external_moms())
244            .map(|(spatial, fm)| FourMomentum {
245                spatial,
246                temporal: Energy {
247                    value: new_constant(&dual, &fm.temporal.value),
248                },
249            })
250            .collect();
251
252        // The same IFT backend differentiates the dimensionless root. Keep the
253        // raw active displacement so null directions never enter a normalization.
254        let rescale_rstar = shifted_t_dependent_momenta
255            .rescale(&dual_alpha, subspace.as_subspace_simple())
256            .iter()
257            .zip(dualized_overlap_center.iter())
258            .map(|(momentum, center)| momentum.clone() + center.clone())
259            .collect::<crate::momentum::sample::LoopMomenta<_>>();
260
261        let dual_esurface = threshold_esurface.compute_from_dual_momenta(
262            subspace.get_lmb(all_lmbs),
263            masses,
264            &rescale_rstar,
265            &dualized_external_fourmomenta,
266        );
267
268        debug!("Dual e-surface: {}", dual_esurface);
269
270        // HyperDual stores Taylor coefficients, whereas the generated IFT
271        // parameters are ordinary mixed partial derivatives.
272        let params = dual_esurface.values[1..]
273            .iter()
274            .zip(dual_esurface.get_shape().into_iter().skip(1))
275            .map(|(coefficient, orders)| {
276                let derivative = orders
277                    .iter()
278                    .flat_map(|order| 2..=*order)
279                    .fold(coefficient.clone(), |value, factor| {
280                        value * coefficient.from_usize(factor)
281                    });
282                Complex::new_re(derivative)
283            })
284            .collect_vec();
285
286        debug!("Parameters for implicit function theorem: {:#?}", params);
287
288        let result = evaluate_evaluator(
289            self.implicit_function_theorem
290                .as_mut()
291                .expect("alpha(t) evaluator requested without t-derivative support"),
292            &params,
293            evaluation_metadata,
294        )
295        .into_iter()
296        .map(DualOrNot::unwrap_real)
297        .collect_vec();
298
299        debug!("Result from implicit function theorem: {:#?}", result);
300
301        let mut dual_values = vec![alpha.clone()];
302        let mut n_factorial = alpha.one();
303        for (i, result) in result.into_iter().enumerate() {
304            n_factorial *= alpha.from_usize(i + 1);
305            dual_values.push(result.re / &n_factorial);
306        }
307
308        HyperDual::from_values(simple_n_deriv_shape(dual_values.len() - 1), dual_values)
309    }
310}
311
312pub(crate) struct RstarTDependenceInput<'a, T: FloatLike> {
313    pub t_star: &'a F<T>,
314    pub alpha: &'a F<T>,
315    pub overlap_center: &'a crate::momentum::sample::LoopMomenta<F<T>>,
316    pub subspace: &'a SubspaceData,
317    pub unrescaled_momentum_sample: &'a MomentumSample<T>,
318    pub representative_sample: &'a MomentumSample<T>,
319    pub masses: &'a EdgeVec<F<T>>,
320    pub threshold_esurface: &'a Esurface,
321    pub lmb: &'a LoopMomentumBasis,
322    pub all_lmbs: &'a TiVec<LmbIndex, LoopMomentumBasis>,
323}
324
325// Use the chain rule to express derivatives of the projection root in terms of
326// the mixed derivatives of eta(root(t), t). The neutral symbolic program names
327// that scalar r_star; runtime supplies alpha and forms the physical radius later.
328pub(crate) fn generate_rstar_t_dependence_evaluator(
329    num_t_derivatives: usize,
330) -> Result<RstarTDependenceEvaluator> {
331    if num_t_derivatives == 0 {
332        return Ok(RstarTDependenceEvaluator {
333            dual_shape_for_esurface_evaluation: Vec::new(),
334            implicit_function_theorem: None,
335        });
336    }
337
338    let t = symbol!("t");
339
340    let rstar = parse!("r_star(t)");
341    let e_surface = function!(GS.eta, rstar.clone(), Atom::var(t));
342
343    let mut rstar_derivatives = vec![];
344    for i in 0..num_t_derivatives {
345        if i == 0 {
346            rstar_derivatives.push(rstar.derivative(t));
347        } else {
348            rstar_derivatives.push(rstar_derivatives.last().unwrap().derivative(t));
349        }
350    }
351
352    let mut equations = vec![];
353    for i in 0..num_t_derivatives {
354        if i == 0 {
355            equations.push(e_surface.derivative(t));
356        } else {
357            equations.push(equations.last().unwrap().derivative(t));
358        }
359    }
360
361    let mut solutions = equations
362        .iter()
363        .zip(&rstar_derivatives)
364        .map(|(eq, variable)| {
365            let solutions = Atom::solve(std::slice::from_ref(eq))
366                .wrt_with_exponent::<u8, _>(std::slice::from_ref(variable))
367                .unwrap();
368            let [solution] = solutions.iter().as_slice() else {
369                panic!(
370                    "expected one implicit-function solution, got {} branches",
371                    solutions.len()
372                );
373            };
374            assert!(
375                solution.free_variables().is_empty(),
376                "implicit-function solution is underdetermined"
377            );
378            let variable = PolyVariable::try_from(variable.clone()).unwrap();
379            solution.get(&variable).cloned().unwrap()
380        })
381        .collect_vec();
382
383    for i in 1..solutions.len() {
384        for j in 0..i {
385            solutions[i] = solutions[i]
386                .replace(rstar_derivatives[j].clone())
387                .with(solutions[j].clone());
388        }
389    }
390
391    // dual shape is for e-surface derivatives, implict function theorem should NOT be dualized with this
392    let mut dual_shape = vec![vec![0, 0]];
393    let mut params = vec![];
394    for i in 1..=num_t_derivatives {
395        let mut eta_derivatives_at_this_order = vec![];
396
397        let mut current_r_derivative_counter = 0;
398        let mut current_t_derivative_counter = i;
399
400        loop {
401            dual_shape.push(vec![
402                current_r_derivative_counter,
403                current_t_derivative_counter,
404            ]);
405            let eta_derivative = function!(
406                Symbol::DERIVATIVE,
407                current_r_derivative_counter,
408                current_t_derivative_counter,
409                GS.eta,
410                rstar.clone(),
411                Atom::var(t)
412            );
413
414            eta_derivatives_at_this_order.push(eta_derivative);
415
416            if current_t_derivative_counter == 0 {
417                break;
418            }
419            current_r_derivative_counter += 1;
420            current_t_derivative_counter -= 1;
421        }
422
423        params.extend(eta_derivatives_at_this_order);
424    }
425
426    let fn_map = FunctionMap::new();
427    let fn_map_entries = vec![];
428    let implict_function_theorem = GenericEvaluator::new_from_raw_params(
429        solutions,
430        &params,
431        &fn_map,
432        fn_map_entries,
433        OptimizationSettings::default(),
434        None,
435        &EvaluatorSettings::default(),
436    )?;
437
438    Ok(RstarTDependenceEvaluator {
439        dual_shape_for_esurface_evaluation: dual_shape,
440        implicit_function_theorem: Some(implict_function_theorem),
441    })
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447    use crate::{
448        integrands::process::GenericEvaluatorFloat,
449        processes::cross_section::CrossSectionGraph,
450        settings::runtime::{HFunction, HFunctionSettings},
451        utils::hyperdual_utils::new_from_values,
452    };
453
454    #[test]
455    fn integrated_ct_profiles_preserve_the_radial_residue() {
456        for (function, power) in [
457            (HFunction::Exponential, None),
458            (HFunction::PolyExponential, None),
459            (HFunction::PolyExponential, Some(4)),
460            (HFunction::PolyExponential, Some(16)),
461            // This profile's p=0 normalization is a focused regression
462            // check. Higher powers need log-space quadrature because their
463            // integrable x -> 0 peak is too narrow for a uniform grid.
464            (HFunction::PolyLeftRightExponential, None),
465        ] {
466            for sigma in [0.5, 2.0] {
467                let settings = IntegratedCounterTermSettings {
468                    range: IntegratedCounterTermRange::Infinite {
469                        h_function_settings: HFunctionSettings {
470                            function: function.clone(),
471                            sigma,
472                            power,
473                            ..Default::default()
474                        },
475                    },
476                };
477                for radius_star in [0.3, 3.0] {
478                    // The generated helper cancels r^(3L-1) from the measure.
479                    // Gaussian profiles are negligible after 8 sigma. The
480                    // left/right exponential has only exponential (rather than
481                    // Gaussian) tails, so integrate it over a wider finite
482                    // window before comparing its analytic normalization.
483                    let tail_multiple = if function == HFunction::PolyLeftRightExponential {
484                        64.0
485                    } else {
486                        8.0
487                    };
488                    let step = tail_multiple * sigma * radius_star / 2048.0;
489                    let integral: f64 = (0..2048)
490                        .map(|i| {
491                            evaluate_integrated_ct_normalisation(
492                                &F((i as f64 + 0.5) * step),
493                                &F(radius_star),
494                                &F(1.0),
495                                &settings,
496                            )
497                            .0
498                        })
499                        .sum::<f64>()
500                        * step;
501                    let tolerance = if function == HFunction::PolyLeftRightExponential {
502                        1.0e-8
503                    } else {
504                        2.0e-12
505                    };
506                    assert!(
507                        (integral - 1.0).abs() < tolerance,
508                        "{function:?}, power={power:?}, sigma={sigma}, r_star={radius_star}: {integral}"
509                    );
510                }
511            }
512        }
513    }
514
515    #[test]
516    fn integrated_ct_profile_dual_matches_normalized_density_derivatives() {
517        let sigma = 0.6_f64;
518        let radius_star = 1.7_f64;
519        let settings = IntegratedCounterTermSettings {
520            range: IntegratedCounterTermRange::Infinite {
521                h_function_settings: HFunctionSettings {
522                    function: HFunction::Exponential,
523                    sigma,
524                    ..Default::default()
525                },
526            },
527        };
528        let shape = HyperDual::new(simple_n_deriv_shape(2));
529        let dual_star = new_from_values(&shape, &[F(radius_star), F(1.0), F(0.0)]);
530        for radius in [0.2_f64, 1.4, 4.0] {
531            let dual_radius = new_from_values(&shape, &[F(radius), F(0.0), F(0.0)]);
532            let actual = evaluate_integrated_ct_normalisation_dual(
533                &dual_radius,
534                &dual_star,
535                &F(1.0),
536                &settings,
537            );
538            // Analytic half-Gaussian density and its first two r_star derivatives.
539            let x = radius / (sigma * radius_star);
540            let density =
541                2.0 * (-x * x).exp() / (std::f64::consts::PI.sqrt() * sigma * radius_star);
542            let log_derivative = (2.0 * x * x - 1.0) / radius_star;
543            let second_log_derivative = (1.0 - 6.0 * x * x) / radius_star.powi(2);
544            let expected = [
545                density,
546                density * log_derivative,
547                density * (log_derivative.powi(2) + second_log_derivative) / 2.0,
548            ];
549            for (actual, expected) in actual.values.iter().zip(expected) {
550                assert!((actual.0 - expected).abs() < 2.0e-13 * expected.abs().max(1.0));
551            }
552        }
553    }
554
555    #[test]
556    fn uv_smooth_sliver_preserves_pole_symmetry_and_hard_mode() {
557        for dynamic_width in [false, true] {
558            let settings = UVLocalisationSettings {
559                smooth_sliver: true,
560                sliver_width: 0.5,
561                dynamic_width,
562                ..Default::default()
563            };
564            let radius_star = F(2.0_f64);
565            let e_cm = F(4.0_f64);
566            let half_width = if dynamic_width { 1.0 } else { 2.0 };
567            assert_eq!(
568                evaluate_uv_damper(&radius_star, &radius_star, &e_cm, &settings),
569                F(1.0)
570            );
571            for fraction in [0.25, 0.75] {
572                let delta = F(fraction * half_width);
573                assert_eq!(
574                    evaluate_uv_damper(&(radius_star + delta), &radius_star, &e_cm, &settings),
575                    evaluate_uv_damper(&(radius_star - delta), &radius_star, &e_cm, &settings)
576                );
577            }
578            let hard = UVLocalisationSettings {
579                smooth_sliver: false,
580                ..settings.clone()
581            };
582            for sign in [-1.0, 1.0] {
583                let edge = F(radius_star.0 + sign * half_width);
584                assert_eq!(
585                    evaluate_uv_damper(&edge, &radius_star, &e_cm, &settings),
586                    F(0.0)
587                );
588                // The existing hard sliver includes its endpoints.
589                assert_eq!(
590                    evaluate_uv_damper(&edge, &radius_star, &e_cm, &hard),
591                    F((-0.25_f64).exp())
592                );
593            }
594            let forced = UVLocalisationSettings {
595                force_uv_dampers_to_one: true,
596                ..settings
597            };
598            assert_eq!(
599                evaluate_uv_damper(&F(100.0), &radius_star, &e_cm, &forced),
600                F(1.0)
601            );
602        }
603    }
604
605    #[test]
606    fn uv_smooth_sliver_dual_matches_derivatives_and_flat_boundary() {
607        let shape = HyperDual::new(simple_n_deriv_shape(2));
608        let radius_star = new_from_values(&shape, &[F(2.0_f64), F(0.7), F(0.0)]);
609        for dynamic_width in [false, true] {
610            let settings = UVLocalisationSettings {
611                smooth_sliver: true,
612                sliver_width: 0.5,
613                dynamic_width,
614                ..Default::default()
615            };
616            let scale = if dynamic_width { 2.0 } else { 4.0 };
617            let scale_derivative = if dynamic_width { 0.7 } else { 0.0 };
618            for u in [-0.4_f64, 0.0, 0.125] {
619                let radius = new_from_values(&shape, &[F(2.0 + u * scale), F(0.3), F(0.0)]);
620                let actual = evaluate_uv_damper_dual(&radius, &radius_star, &F(4.0), &settings);
621                // Differentiate the dimensionless even profile, including S(t).
622                let gap = 0.25 - u * u;
623                let value = (-u * u - u * u / gap).exp();
624                let log_first = -2.0 * u - 0.5 * u / gap.powi(2);
625                let log_second = -2.0 - 0.5 / gap.powi(2) - 2.0 * u * u / gap.powi(3);
626                let u_first = (-0.4 - u * scale_derivative) / scale;
627                let u_second = -2.0 * scale_derivative * u_first / scale;
628                let expected = [
629                    value,
630                    value * log_first * u_first,
631                    value
632                        * ((log_first.powi(2) + log_second) * u_first.powi(2)
633                            + log_first * u_second)
634                        / 2.0,
635                ];
636                let scalar = evaluate_uv_damper(
637                    &radius.values[0],
638                    &radius_star.values[0],
639                    &F(4.0),
640                    &settings,
641                );
642                assert!((actual.values[0].0 - scalar.0).abs() < 2.0e-14);
643                for (actual, expected) in actual.values.iter().zip(expected) {
644                    assert!((actual.0 - expected).abs() < 2.0e-13 * expected.abs().max(1.0));
645                }
646            }
647            for u in [-0.6_f64, -0.5, -0.4995, 0.4995, 0.5, 0.6] {
648                let radius = new_from_values(&shape, &[F(2.0 + u * scale), F(0.3), F(0.0)]);
649                let actual = evaluate_uv_damper_dual(&radius, &radius_star, &F(4.0), &settings);
650                for coefficient in actual.values {
651                    assert!(coefficient.0.abs() < 1.0e-190);
652                    if u.abs() >= 0.5 {
653                        assert_eq!(coefficient, F(0.0));
654                    }
655                }
656            }
657            let forced = UVLocalisationSettings {
658                force_uv_dampers_to_one: true,
659                ..settings
660            };
661            let radius = new_from_values(&shape, &[F(100.0), F(0.3), F(0.0)]);
662            assert_eq!(
663                evaluate_uv_damper_dual(&radius, &radius_star, &F(4.0), &forced).values,
664                vec![F(1.0), F(0.0), F(0.0)]
665            );
666        }
667    }
668
669    #[test]
670    fn uv_smooth_sliver_helper_preserves_signed_radial_principal_value() {
671        crate::initialisation::test_initialise().unwrap();
672        let settings = UVLocalisationSettings {
673            smooth_sliver: true,
674            sliver_width: 1.5,
675            ..Default::default()
676        };
677        for loop_count in [1, 2] {
678            let (pieces, _) =
679                CrossSectionGraph::single_th_prefactor_helper_atoms(1, loop_count, false, false);
680            let mut helper = GenericEvaluator::new_from_raw_params(
681                [pieces.local],
682                &CrossSectionGraph::single_th_prefactor_helper_params(1, false),
683                &FunctionMap::new(),
684                vec![],
685                OptimizationSettings::default(),
686                None,
687                &EvaluatorSettings::default(),
688            )
689            .unwrap()
690            .into_eager_only();
691            for radius_star in [0.5, 2.0] {
692                // Equal midpoint bins pair around the pole. W > r_star in the
693                // first case requires the helper's negative-radius mirror term.
694                let step = 1.0 / 2048.0;
695                let steps = ((radius_star + settings.sliver_width) / step) as usize;
696                let mut integral = 0.0;
697                for i in 0..steps {
698                    let radius = F((i as f64 + 0.5) * step);
699                    let plus = evaluate_uv_damper(&radius, &F(radius_star), &F(1.0), &settings);
700                    let minus = evaluate_uv_damper(&(-radius), &F(radius_star), &F(1.0), &settings);
701                    let params = [F(1.0), F(1.0), radius, F(radius_star), plus, minus, F(0.0)]
702                        .map(Complex::new_re);
703                    let value = f64::get_evaluator_single(&mut helper)(&params);
704                    integral += value.re.0 * radius.0.powi(3 * loop_count as i32 - 1) * step;
705                }
706                assert!(
707                    integral.abs() < 1.0e-10,
708                    "{loop_count} loops, r_star={radius_star}: PV={integral}"
709                );
710            }
711        }
712    }
713
714    #[test]
715    fn uv_damper_can_be_forced_to_one() {
716        let settings = UVLocalisationSettings {
717            force_uv_dampers_to_one: true,
718            ..Default::default()
719        };
720
721        assert_eq!(
722            evaluate_uv_damper(&F(100.0_f64), &F(1.0_f64), &F(1.0_f64), &settings),
723            F(1.0_f64)
724        );
725    }
726
727    #[test]
728    fn uv_damper_dual_can_be_forced_to_constant_one() {
729        let settings = UVLocalisationSettings {
730            force_uv_dampers_to_one: true,
731            ..Default::default()
732        };
733        let shape = HyperDual::new(simple_n_deriv_shape(1));
734        let radius = new_from_values(&shape, &[F(100.0_f64), F(5.0_f64)]);
735        let radius_star = new_from_values(&shape, &[F(1.0_f64), F(3.0_f64)]);
736
737        let damper = evaluate_uv_damper_dual(&radius, &radius_star, &F(1.0_f64), &settings);
738
739        assert_eq!(damper.values, vec![F(1.0_f64), F(0.0_f64)]);
740    }
741
742    #[test]
743    fn test_rstar_t_dependence_evaluator() {
744        generate_rstar_t_dependence_evaluator(3).unwrap();
745    }
746
747    #[test]
748    fn test_rstar_t_dependence_evaluator_zero_derivatives() {
749        let evaluator = generate_rstar_t_dependence_evaluator(0).unwrap();
750        assert!(!evaluator.supports_t_derivatives());
751    }
752}