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 + ¢er_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 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 ¶ms_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 - ¢er;
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 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 ¢er_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 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 ¢er,
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 ¢er,
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}