1use std::{collections::BTreeMap, path::Path, sync::Arc};
2
3use bincode_trait_derive::{Decode, Encode};
4use color_eyre::Result;
5use eyre::{Context, eyre};
6use itertools::Itertools;
7use linnet::half_edge::involution::{EdgeIndex, EdgeVec, Orientation};
8use smallvec::{SmallVec, smallvec};
9use spenso::algebra::{algebraic_traits::IsZero, complex::Complex};
10use symbolica::domains::{
11 dual::{DualNumberStructure, HyperDual},
12 float::{Real, RealLike},
13};
14use tracing::{debug, warn};
15use typed_index_collections::{TiVec, ti_vec};
16
17use crate::{
18 GammaLoopContext,
19 cff::{
20 CutCFFIndex,
21 esurface::{
22 Esurface, EsurfaceCollection, EsurfaceID, ExistingEsurfaceId, ExistingThresholds,
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 sampling::maps::SamplingEvaluationError,
38 threshold_multiplier::{
39 ThresholdMultiplierEvaluatorCollection, ThresholdMultiplierInputWorkspace,
40 },
41 },
42 },
43 momentum::{
44 Rotatable, Rotation, SignOrZero, ThreeMomentum,
45 sample::{
46 ExternalFourMomenta, ExternalIndex, LoopIndex, LoopMomenta, MomentumSample,
47 SubspaceData,
48 },
49 signature::LoopSignature,
50 },
51 observables::{
52 GenericThresholdCountertermComponentWeight, ThresholdCountertermComponentOccurrence,
53 },
54 processes::{
55 CutGroupId, EvaluatorBuildTimings, IteratedCtCollection, IteratedThresholdPieces,
56 LUCounterTermData, LUThresholdHelperOutputs, LeftThresholdId, RightThresholdId,
57 SingleThresholdPieces, ThresholdCountertermComponentKind,
58 ThresholdCountertermMetadataRegistry, ThresholdCountertermSide,
59 ThresholdCountertermVariantId, build_derivative_structure,
60 },
61 settings::{GlobalSettings, RuntimeSettings, global::FrozenCompilationMode},
62 subtraction::{
63 RstarTDependenceEvaluator, RstarTDependenceInput, evaluate_integrated_ct_normalisation,
64 evaluate_integrated_ct_normalisation_dual, evaluate_uv_damper, evaluate_uv_damper_dual,
65 overlap_subspace::{self, OverlapGroup, OverlapInput, OverlapKinematics, OverlapStructure},
66 },
67 utils::{
68 ArbPrec, F, FloatLike,
69 hyperdual_utils::{
70 DualOrNot, dualize_dual_t_to_dual_r_t, extract_coefficient_t_duals,
71 extract_t_derivatives, extract_t_derivatives_complex, new_constant,
72 shape_from_cut_cff_index, simple_n_deriv_shape, variable_indices_from_cut_cff_index,
73 },
74 newton_solver::{NewtonIterationResult, RadialRootIdentity},
75 },
76 uv::forest::ParametricIntegrands,
77};
78
79fn zero_dual_or_not_complex<T: FloatLike>(order: usize, zero: &F<T>) -> DualOrNot<Complex<F<T>>> {
80 if order == 0 {
81 DualOrNot::NonDual(Complex::new_re(zero.clone()))
82 } else {
83 DualOrNot::Dual(HyperDual::new(simple_n_deriv_shape(order)))
84 }
85}
86
87fn negate_dual_or_not_complex<T: FloatLike>(
88 value: DualOrNot<Complex<F<T>>>,
89) -> DualOrNot<Complex<F<T>>> {
90 match value {
91 DualOrNot::Dual(dual) => DualOrNot::Dual(-dual),
92 DualOrNot::NonDual(non_dual) => DualOrNot::NonDual(-non_dual),
93 }
94}
95
96fn multiply_dual_or_not_complex<T: FloatLike>(
97 lhs: DualOrNot<Complex<F<T>>>,
98 rhs: &DualOrNot<Complex<F<T>>>,
99) -> DualOrNot<Complex<F<T>>> {
100 match (lhs, rhs) {
101 (DualOrNot::NonDual(lhs), DualOrNot::NonDual(rhs)) => DualOrNot::NonDual(lhs * rhs),
102 (DualOrNot::Dual(lhs), DualOrNot::Dual(rhs)) => DualOrNot::Dual(lhs * rhs.clone()),
103 (DualOrNot::Dual(lhs), DualOrNot::NonDual(rhs)) => {
104 let rhs_dual = new_constant(&lhs, rhs);
105 DualOrNot::Dual(lhs * rhs_dual)
106 }
107 (DualOrNot::NonDual(lhs), DualOrNot::Dual(rhs)) => {
108 let lhs_dual = new_constant(rhs, &lhs);
109 DualOrNot::Dual(lhs_dual * rhs.clone())
110 }
111 }
112}
113
114struct ThresholdHelperEvaluation<T, P> {
115 legacy: Option<T>,
116 pieces: Option<P>,
117}
118
119type ThresholdHelperValue<T> = DualOrNot<Complex<F<T>>>;
120type SingleThresholdHelperEvaluation<T> = ThresholdHelperEvaluation<
121 ThresholdHelperValue<T>,
122 SingleThresholdPieces<ThresholdHelperValue<T>>,
123>;
124type IteratedThresholdHelperEvaluation<T> = ThresholdHelperEvaluation<
125 ThresholdHelperValue<T>,
126 IteratedThresholdPieces<ThresholdHelperValue<T>>,
127>;
128
129struct ThresholdHelperEvaluationContext<'a, T: FloatLike> {
130 helper: &'a mut GenericEvaluator,
131 cut_cff_index: &'a CutCFFIndex,
132 threshold_result: &'a ThresholdHelperValue<T>,
133 outputs: LUThresholdHelperOutputs,
134 evaluation_meta_data: &'a mut EvaluationMetaData,
135}
136
137impl<T, P> ThresholdHelperEvaluation<T, P> {
138 fn into_legacy(self) -> T {
139 self.legacy
140 .expect("legacy LU threshold helper output is unavailable")
141 }
142
143 fn into_pieces(self) -> P {
144 self.pieces
145 .expect("split LU threshold helper outputs are unavailable")
146 }
147}
148
149#[derive(Clone, Debug)]
150struct SingleMultiplierCoefficients<T: FloatLike> {
151 local: F<T>,
152 integrated: F<T>,
153}
154
155#[derive(Clone, Copy, Debug)]
156enum SingleMultiplierComponent {
157 Local,
158 Integrated,
159}
160
161fn single_multiplier_context<'a, S>(
162 component: SingleMultiplierComponent,
163 base: &'a S,
164 root: &'a S,
165) -> (&'a S, &'a S) {
166 match component {
169 SingleMultiplierComponent::Local => (base, root),
170 SingleMultiplierComponent::Integrated => (root, root),
171 }
172}
173
174impl<T: FloatLike> SingleMultiplierCoefficients<T> {
175 fn all_zero(&self) -> bool {
176 self.local.is_zero() && self.integrated.is_zero()
177 }
178}
179
180#[derive(Clone, Debug)]
181struct IteratedMultiplierCoefficients<T: FloatLike> {
182 local_local: PairMultiplierCoefficient<T>,
183 local_integrated: PairMultiplierCoefficient<T>,
184 integrated_local: PairMultiplierCoefficient<T>,
185 integrated_integrated: PairMultiplierCoefficient<T>,
186}
187
188#[derive(Clone, Debug)]
189struct PairMultiplierCoefficient<T: FloatLike> {
190 left: F<T>,
191 right: F<T>,
192}
193
194impl<T: FloatLike> PairMultiplierCoefficient<T> {
195 fn effective(&self) -> F<T> {
196 &self.left * &self.right
197 }
198
199 fn is_zero(&self) -> bool {
200 self.left.is_zero() || self.right.is_zero()
201 }
202}
203
204#[derive(Clone, Copy, Debug)]
205enum IteratedMultiplierComponent {
206 LocalLocal,
207 LocalIntegrated,
208 IntegratedLocal,
209 IntegratedIntegrated,
210}
211
212fn iterated_multiplier_context<'a, S>(
213 component: IteratedMultiplierComponent,
214 base: &'a S,
215 left_root: &'a S,
216 right_root: &'a S,
217 merged_root: &'a S,
218) -> (&'a S, &'a S) {
219 match component {
220 IteratedMultiplierComponent::LocalLocal => (base, merged_root),
221 IteratedMultiplierComponent::LocalIntegrated => (right_root, merged_root),
222 IteratedMultiplierComponent::IntegratedLocal => (left_root, merged_root),
223 IteratedMultiplierComponent::IntegratedIntegrated => (merged_root, merged_root),
224 }
225}
226
227impl<T: FloatLike> IteratedMultiplierCoefficients<T> {
228 fn all_zero(&self) -> bool {
229 self.local_local.is_zero()
230 && self.local_integrated.is_zero()
231 && self.integrated_local.is_zero()
232 && self.integrated_integrated.is_zero()
233 }
234}
235
236#[allow(clippy::too_many_arguments)]
237fn evaluate_single_multiplier_context<T: FloatLike>(
238 collection: &mut ThresholdMultiplierEvaluatorCollection,
239 workspace: &mut ThresholdMultiplierInputWorkspace<T>,
240 variant_id: ThresholdCountertermVariantId,
241 generation_lmb: &LoopMomentumBasis,
242 masses: &EdgeVec<F<T>>,
243 model_values: &[Complex<F<f64>>],
244 additional_values: &[F<T>],
245 effective: &MomentumSample<T>,
246 star: &MomentumSample<T>,
247 evaluation_meta_data: &mut EvaluationMetaData,
248) -> Result<F<T>> {
249 let Some(evaluator_id) = collection.evaluator_id_for_variant(variant_id)? else {
250 return Ok(effective.sample.one());
251 };
252 let values = workspace.bind(
253 collection.layout(),
254 generation_lmb,
255 masses,
256 model_values,
257 additional_values,
258 effective,
259 star,
260 )?;
261 collection.evaluators_mut()[evaluator_id.0].evaluate(values, evaluation_meta_data)
262}
263
264#[allow(clippy::too_many_arguments)]
265fn evaluate_pair_multiplier_context<T: FloatLike>(
266 collection: &mut ThresholdMultiplierEvaluatorCollection,
267 workspace: &mut ThresholdMultiplierInputWorkspace<T>,
268 left_variant_id: ThresholdCountertermVariantId,
269 right_variant_id: ThresholdCountertermVariantId,
270 generation_lmb: &LoopMomentumBasis,
271 masses: &EdgeVec<F<T>>,
272 model_values: &[Complex<F<f64>>],
273 additional_values: &[F<T>],
274 effective: &MomentumSample<T>,
275 star: &MomentumSample<T>,
276 evaluation_meta_data: &mut EvaluationMetaData,
277) -> Result<(F<T>, F<T>)> {
278 let left_evaluator_id = collection.evaluator_id_for_variant(left_variant_id)?;
279 let right_evaluator_id = collection.evaluator_id_for_variant(right_variant_id)?;
280 if left_evaluator_id.is_none() && right_evaluator_id.is_none() {
281 return Ok((effective.sample.one(), effective.sample.one()));
282 }
283
284 let values = workspace.bind(
285 collection.layout(),
286 generation_lmb,
287 masses,
288 model_values,
289 additional_values,
290 effective,
291 star,
292 )?;
293 if let (Some(left_evaluator_id), Some(right_evaluator_id)) =
294 (left_evaluator_id, right_evaluator_id)
295 && left_evaluator_id == right_evaluator_id
296 {
297 let value = collection.evaluators_mut()[left_evaluator_id.0]
300 .evaluate(values, evaluation_meta_data)?;
301 return Ok((value.clone(), value));
302 }
303 let left = if let Some(evaluator_id) = left_evaluator_id {
304 collection.evaluators_mut()[evaluator_id.0].evaluate(values, evaluation_meta_data)?
305 } else {
306 effective.sample.one()
307 };
308 let right = if let Some(evaluator_id) = right_evaluator_id {
311 collection.evaluators_mut()[evaluator_id.0].evaluate(values, evaluation_meta_data)?
312 } else {
313 effective.sample.one()
314 };
315 Ok((left, right))
316}
317
318fn weight_single_multiplier_pieces<T: FloatLike>(
319 pieces: SingleThresholdPieces<DualOrNot<Complex<F<T>>>>,
320 coefficients: &SingleMultiplierCoefficients<T>,
321 order: usize,
322 zero: &F<T>,
323) -> DualOrNot<Complex<F<T>>> {
324 let mut result = zero_dual_or_not_complex(order, zero);
325 if !coefficients.local.is_zero() {
326 result += multiply_dual_or_not_complex(
327 pieces.local,
328 &DualOrNot::NonDual(Complex::new_re(coefficients.local.clone())),
329 );
330 }
331 if !coefficients.integrated.is_zero() {
332 result += multiply_dual_or_not_complex(
333 pieces.integrated,
334 &DualOrNot::NonDual(Complex::new_re(coefficients.integrated.clone())),
335 );
336 }
337 result
338}
339
340fn weight_iterated_multiplier_pieces<T: FloatLike>(
341 pieces: IteratedThresholdPieces<DualOrNot<Complex<F<T>>>>,
342 coefficients: &IteratedMultiplierCoefficients<T>,
343 order: usize,
344 zero: &F<T>,
345) -> DualOrNot<Complex<F<T>>> {
346 let mut result = zero_dual_or_not_complex(order, zero);
347 if !coefficients.local_local.is_zero() {
348 result += multiply_dual_or_not_complex(
349 pieces.local_local,
350 &DualOrNot::NonDual(Complex::new_re(coefficients.local_local.effective())),
351 );
352 }
353 if !coefficients.local_integrated.is_zero() {
354 result += multiply_dual_or_not_complex(
355 pieces.local_integrated,
356 &DualOrNot::NonDual(Complex::new_re(coefficients.local_integrated.effective())),
357 );
358 }
359 if !coefficients.integrated_local.is_zero() {
360 result += multiply_dual_or_not_complex(
361 pieces.integrated_local,
362 &DualOrNot::NonDual(Complex::new_re(coefficients.integrated_local.effective())),
363 );
364 }
365 if !coefficients.integrated_integrated.is_zero() {
366 result += multiply_dual_or_not_complex(
367 pieces.integrated_integrated,
368 &DualOrNot::NonDual(Complex::new_re(
369 coefficients.integrated_integrated.effective(),
370 )),
371 );
372 }
373 result
374}
375
376fn extract_zero_threshold_coefficient_t_dual<
377 T: Clone + spenso::algebra::algebraic_traits::RefZero + Default,
378>(
379 dual: &HyperDual<T>,
380 t_variable: usize,
381) -> HyperDual<T> {
382 let (coefficient_orders, coefficient_duals) = extract_coefficient_t_duals(dual, t_variable);
383 let zero_index = coefficient_orders
384 .iter()
385 .position(|orders| orders.iter().all(|order| *order == 0))
386 .expect("Could not find zero-threshold coefficient in mixed dual");
387
388 coefficient_duals[zero_index].clone()
389}
390
391fn embed_t_dual_in_target_shape<T: FloatLike>(
392 t_dual: &HyperDual<F<T>>,
393 target_shape: &HyperDual<F<T>>,
394 t_variable: Option<usize>,
395) -> HyperDual<F<T>> {
396 match t_variable {
397 Some(t_variable) => {
398 dualize_dual_t_to_dual_r_t(t_dual.clone(), target_shape.clone(), t_variable)
399 }
400 None => new_constant(target_shape, &t_dual.values[0]),
401 }
402}
403
404fn activate_threshold_variable_in_target_shape<T: FloatLike>(
405 dual: &mut HyperDual<F<T>>,
406 variable: Option<usize>,
407) {
408 let Some(variable) = variable else {
409 return;
410 };
411
412 let n_variables = dual.get_shape()[0].len();
413 let mut derivative_shape = vec![0; n_variables];
414 derivative_shape[variable] = 1;
415
416 let derivative_index = dual
417 .get_shape()
418 .iter()
419 .position(|shape| shape == &derivative_shape)
420 .expect("Could not find threshold-variable derivative slot in target shape");
421
422 dual.values[derivative_index] = dual.values[0].one();
423}
424
425fn embed_real_dual_or_not_in_target_shape<T: FloatLike>(
426 value: &DualOrNot<F<T>>,
427 target_shape: &Option<HyperDual<F<T>>>,
428 t_variable: Option<usize>,
429) -> DualOrNot<F<T>> {
430 match (value, target_shape) {
431 (DualOrNot::Dual(dual), Some(target_shape)) => {
432 DualOrNot::Dual(embed_t_dual_in_target_shape(dual, target_shape, t_variable))
433 }
434 (DualOrNot::NonDual(value), Some(target_shape)) => {
435 DualOrNot::Dual(new_constant(target_shape, value))
436 }
437 _ => value.clone(),
438 }
439}
440
441fn embedded_dual_loop_momenta_for_cut_cff_index<T: FloatLike>(
442 sample: &MomentumSample<T>,
443 target_shape: &Option<HyperDual<F<T>>>,
444 t_variable: Option<usize>,
445) -> Option<LoopMomenta<HyperDual<F<T>>>> {
446 match target_shape {
447 Some(target_shape) => Some(match sample.sample.dual_loop_moms.as_ref() {
448 Some(dual_loop_moms) => dual_loop_moms
449 .iter()
450 .map(|momentum| {
451 momentum.map_ref(&|component| {
452 embed_t_dual_in_target_shape(component, target_shape, t_variable)
453 })
454 })
455 .collect(),
456 None => sample
457 .loop_moms()
458 .iter()
459 .map(|momentum| {
460 momentum.map_ref(&|component| new_constant(target_shape, component))
461 })
462 .collect(),
463 }),
464 None => sample.sample.dual_loop_moms.clone(),
465 }
466}
467
468fn extend_plain_helper_real_params<T: FloatLike>(
469 params: &mut Vec<Complex<F<T>>>,
470 value: &DualOrNot<F<T>>,
471 t_variable: Option<usize>,
472) {
473 match value {
474 DualOrNot::Dual(dual) => {
475 if let Some(t_variable) = t_variable {
476 params.extend(
477 extract_t_derivatives(extract_zero_threshold_coefficient_t_dual(
478 dual, t_variable,
479 ))
480 .into_iter()
481 .map(Complex::new_re),
482 );
483 } else {
484 params.push(Complex::new_re(dual.values[0].clone()));
485 }
486 }
487 DualOrNot::NonDual(value) => params.push(Complex::new_re(value.clone())),
488 }
489}
490
491fn extend_extracted_f_params<T: FloatLike>(
492 params: &mut Vec<Complex<F<T>>>,
493 value: &DualOrNot<Complex<F<T>>>,
494 t_variable: Option<usize>,
495) {
496 match value {
497 DualOrNot::Dual(dual) => {
498 if let Some(t_variable) = t_variable {
499 let (_, coefficient_duals) = extract_coefficient_t_duals(dual, t_variable);
500 for coefficient_dual in coefficient_duals {
501 params.extend(extract_t_derivatives_complex(coefficient_dual));
502 }
503 } else {
504 params.extend(dual.values.iter().cloned());
505 }
506 }
507 DualOrNot::NonDual(value) => params.push(value.clone()),
508 }
509}
510
511fn extend_extracted_eta_params<T: FloatLike>(
512 params: &mut Vec<Complex<F<T>>>,
513 value: &DualOrNot<F<T>>,
514 lu_variable: Option<usize>,
515 threshold_variable: Option<usize>,
516) {
517 match value {
518 DualOrNot::Dual(dual) => {
519 if let Some(lu_variable) = lu_variable {
520 let (coefficient_orders, coefficient_duals) =
521 extract_coefficient_t_duals(dual, lu_variable);
522 let projected_threshold_variable = threshold_variable.map(|variable| {
523 if variable > lu_variable {
524 variable - 1
525 } else {
526 variable
527 }
528 });
529 for (orders, coefficient_dual) in
530 coefficient_orders.into_iter().zip(coefficient_duals)
531 {
532 if orders.iter().enumerate().any(|(variable, &order)| {
533 Some(variable) != projected_threshold_variable && order != 0
534 }) {
535 continue;
536 }
537 params.extend(
538 extract_t_derivatives(coefficient_dual)
539 .into_iter()
540 .map(Complex::new_re),
541 );
542 }
543 } else {
544 params.extend(
545 dual.get_shape()
546 .iter()
547 .zip(&dual.values)
548 .filter(|(orders, _)| {
549 !orders.iter().enumerate().any(|(variable, &order)| {
550 Some(variable) != threshold_variable && order != 0
551 })
552 })
553 .map(|(_, value)| Complex::new_re(value.clone())),
554 );
555 }
556 }
557 DualOrNot::NonDual(value) => params.push(Complex::new_re(value.clone())),
558 }
559}
560
561fn evaluate_threshold_helper_single<
562 T: FloatLike + crate::integrands::process::evaluators::GenericEvaluatorFloat,
563>(
564 context: ThresholdHelperEvaluationContext<'_, T>,
565 threshold_params: &ThresholdParams<T>,
566) -> SingleThresholdHelperEvaluation<T> {
567 let ThresholdHelperEvaluationContext {
568 helper,
569 cut_cff_index,
570 threshold_result,
571 outputs,
572 evaluation_meta_data,
573 } = context;
574 let variable_indices = variable_indices_from_cut_cff_index(cut_cff_index);
575 let t_variable = variable_indices.lu_cut;
576 let mut helper_params = Vec::new();
577 extend_extracted_f_params(&mut helper_params, threshold_result, t_variable);
578 extend_extracted_eta_params(
579 &mut helper_params,
580 &threshold_params.esurface_derivative,
581 t_variable,
582 variable_indices
583 .left_threshold
584 .or(variable_indices.right_threshold),
585 );
586 extend_plain_helper_real_params(&mut helper_params, &threshold_params.radius, t_variable);
587 extend_plain_helper_real_params(
588 &mut helper_params,
589 &threshold_params.radius_star,
590 t_variable,
591 );
592 extend_plain_helper_real_params(
593 &mut helper_params,
594 &threshold_params.uv_damp_plus,
595 t_variable,
596 );
597 extend_plain_helper_real_params(
598 &mut helper_params,
599 &threshold_params.uv_damp_minus,
600 t_variable,
601 );
602 extend_plain_helper_real_params(&mut helper_params, &threshold_params.h_function, t_variable);
603
604 debug!(
605 "LU threshold helper input (single): cut_cff_index={:?}, t_variable={:?}, param_count={}",
606 cut_cff_index,
607 t_variable,
608 helper_params.len()
609 );
610 for (idx, value) in helper_params.iter().enumerate() {
611 debug!(
612 "LU threshold helper param (single): cut_cff_index={:?}, index={}, value={}",
613 cut_cff_index, idx, value
614 );
615 }
616
617 let mut result = evaluate_evaluator(helper, &helper_params, evaluation_meta_data);
618
619 match outputs {
620 LUThresholdHelperOutputs::Legacy => {
621 assert_eq!(
622 result.len(),
623 1,
624 "legacy single LU threshold helper must return one combined output",
625 );
626 ThresholdHelperEvaluation {
627 legacy: result.pop(),
628 pieces: None,
629 }
630 }
631 LUThresholdHelperOutputs::Pieces => {
632 assert_eq!(
633 result.len(),
634 2,
635 "single LU threshold helper must return local and integrated pieces",
636 );
637 let integrated = result.pop().unwrap();
638 let local = result.pop().unwrap();
639 ThresholdHelperEvaluation {
640 legacy: None,
641 pieces: Some(SingleThresholdPieces { local, integrated }),
642 }
643 }
644 LUThresholdHelperOutputs::LegacyAndPieces => {
645 assert_eq!(
646 result.len(),
647 3,
648 "recording single LU threshold helper must return legacy, local, and integrated outputs",
649 );
650 let integrated = result.pop().unwrap();
651 let local = result.pop().unwrap();
652 let legacy = result.pop().unwrap();
653 ThresholdHelperEvaluation {
654 legacy: Some(legacy),
655 pieces: Some(SingleThresholdPieces { local, integrated }),
656 }
657 }
658 }
659}
660
661fn evaluate_threshold_helper_iterated<
662 T: FloatLike + crate::integrands::process::evaluators::GenericEvaluatorFloat,
663>(
664 context: ThresholdHelperEvaluationContext<'_, T>,
665 left_threshold_params: &ThresholdParams<T>,
666 right_threshold_params: &ThresholdParams<T>,
667) -> IteratedThresholdHelperEvaluation<T> {
668 let ThresholdHelperEvaluationContext {
669 helper,
670 cut_cff_index,
671 threshold_result,
672 outputs,
673 evaluation_meta_data,
674 } = context;
675 let variable_indices = variable_indices_from_cut_cff_index(cut_cff_index);
676 let t_variable = variable_indices.lu_cut;
677 let mut helper_params = Vec::new();
678 extend_extracted_f_params(&mut helper_params, threshold_result, t_variable);
679 extend_extracted_eta_params(
680 &mut helper_params,
681 &left_threshold_params.esurface_derivative,
682 t_variable,
683 variable_indices.left_threshold,
684 );
685 extend_extracted_eta_params(
686 &mut helper_params,
687 &right_threshold_params.esurface_derivative,
688 t_variable,
689 variable_indices.right_threshold,
690 );
691
692 extend_plain_helper_real_params(
693 &mut helper_params,
694 &left_threshold_params.radius,
695 t_variable,
696 );
697 extend_plain_helper_real_params(
698 &mut helper_params,
699 &left_threshold_params.radius_star,
700 t_variable,
701 );
702 extend_plain_helper_real_params(
703 &mut helper_params,
704 &left_threshold_params.uv_damp_plus,
705 t_variable,
706 );
707 extend_plain_helper_real_params(
708 &mut helper_params,
709 &left_threshold_params.uv_damp_minus,
710 t_variable,
711 );
712 extend_plain_helper_real_params(
713 &mut helper_params,
714 &left_threshold_params.h_function,
715 t_variable,
716 );
717
718 extend_plain_helper_real_params(
719 &mut helper_params,
720 &right_threshold_params.radius,
721 t_variable,
722 );
723 extend_plain_helper_real_params(
724 &mut helper_params,
725 &right_threshold_params.radius_star,
726 t_variable,
727 );
728 extend_plain_helper_real_params(
729 &mut helper_params,
730 &right_threshold_params.uv_damp_plus,
731 t_variable,
732 );
733 extend_plain_helper_real_params(
734 &mut helper_params,
735 &right_threshold_params.uv_damp_minus,
736 t_variable,
737 );
738 extend_plain_helper_real_params(
739 &mut helper_params,
740 &right_threshold_params.h_function,
741 t_variable,
742 );
743
744 debug!(
745 "LU threshold helper input (iterated): cut_cff_index={:?}, t_variable={:?}, param_count={}",
746 cut_cff_index,
747 t_variable,
748 helper_params.len()
749 );
750 for (idx, value) in helper_params.iter().enumerate() {
751 debug!(
752 "LU threshold helper param (iterated): cut_cff_index={:?}, index={}, value={}",
753 cut_cff_index, idx, value
754 );
755 }
756
757 let mut result = evaluate_evaluator(helper, &helper_params, evaluation_meta_data);
758
759 match outputs {
760 LUThresholdHelperOutputs::Legacy => {
761 assert_eq!(
762 result.len(),
763 1,
764 "legacy iterated LU threshold helper must return one combined output",
765 );
766 ThresholdHelperEvaluation {
767 legacy: result.pop(),
768 pieces: None,
769 }
770 }
771 LUThresholdHelperOutputs::Pieces | LUThresholdHelperOutputs::LegacyAndPieces => {
772 let expected = if outputs == LUThresholdHelperOutputs::Pieces {
773 4
774 } else {
775 5
776 };
777 assert_eq!(
778 result.len(),
779 expected,
780 "iterated LU threshold helper must return the configured legacy and/or LL, LI, IL, and II outputs",
781 );
782 let integrated_integrated = result.pop().unwrap();
783 let integrated_local = result.pop().unwrap();
784 let local_integrated = result.pop().unwrap();
785 let local_local = result.pop().unwrap();
786 let legacy = (outputs == LUThresholdHelperOutputs::LegacyAndPieces)
787 .then(|| result.pop().unwrap());
788 ThresholdHelperEvaluation {
789 legacy,
790 pieces: Some(IteratedThresholdPieces {
791 local_local,
792 local_integrated,
793 integrated_local,
794 integrated_integrated,
795 }),
796 }
797 }
798 }
799}
800
801fn real_dual_to_complex<T: FloatLike>(dual: HyperDual<F<T>>) -> HyperDual<Complex<F<T>>> {
802 let shape = dual.get_shape().iter().map(|v| v.to_vec()).collect();
803 let values = dual.values.into_iter().map(Complex::new_re).collect_vec();
804 HyperDual::from_values(shape, values)
805}
806
807fn dualize_external_momenta<T: FloatLike>(
808 shape: &HyperDual<F<T>>,
809 external_moms: &ExternalFourMomenta<F<T>>,
810) -> ExternalFourMomenta<HyperDual<F<T>>> {
811 external_moms
812 .iter()
813 .map(|momentum| momentum.map_ref(&|component| new_constant(shape, component)))
814 .collect()
815}
816
817fn dualize_loop_momenta<T: FloatLike>(
818 shape: &HyperDual<F<T>>,
819 loop_moms: &LoopMomenta<F<T>>,
820) -> LoopMomenta<HyperDual<F<T>>> {
821 loop_moms
822 .iter()
823 .map(|momentum| momentum.map_ref(&|component| new_constant(shape, component)))
824 .collect()
825}
826
827fn dual_shifted_radius<T: FloatLike>(
828 shifted_momenta: &LoopMomenta<HyperDual<F<T>>>,
829 subspace: &SubspaceData,
830) -> HyperDual<F<T>> {
831 let zero = new_constant(
832 &shifted_momenta[LoopIndex(0)].px,
833 &shifted_momenta[LoopIndex(0)].px.values[0].zero(),
834 );
835
836 subspace
837 .iter_lmb_indices()
838 .fold(zero, |acc, loop_index| {
839 acc + shifted_momenta[loop_index].norm_squared()
840 })
841 .sqrt()
842}
843
844fn compute_shift_part_from_dual_momenta_in_subspace<T: FloatLike>(
845 esurface: &Esurface,
846 loop_moms: &LoopMomenta<HyperDual<F<T>>>,
847 external_moms: &ExternalFourMomenta<HyperDual<F<T>>>,
848 subspace: &SubspaceData,
849 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
850 graph: &Graph,
851 masses: &EdgeVec<F<T>>,
852) -> HyperDual<F<T>> {
853 let lmb = subspace.get_lmb(all_lmbs);
854 let zero = new_constant(
855 &external_moms[ExternalIndex(0)].temporal.value,
856 &external_moms[ExternalIndex(0)].temporal.value.values[0].zero(),
857 );
858
859 let full_external_shift = esurface
860 .external_shift
861 .iter()
862 .map(|(index, sign)| {
863 let external_signature = &lmb.edge_signatures[*index].external;
864 let sign = new_constant(&zero, &F::from_f64(*sign as f64));
865 let external_energy = external_signature
866 .try_apply(&external_moms.raw)
867 .map(|momentum| momentum.temporal.value)
868 .unwrap_or_else(|| zero.clone());
869
870 sign * external_energy
871 })
872 .reduce(|acc, value| acc + value)
873 .unwrap_or_else(|| zero.clone());
874
875 let spatial_externals = external_moms
876 .iter()
877 .map(|momentum| momentum.spatial.clone())
878 .collect::<TiVec<ExternalIndex, _>>();
879
880 let remaining_shift = subspace
881 .does_not_contain(&esurface.energies, graph)
882 .map(|index| {
883 let signature = &lmb.edge_signatures[index];
884 let momentum = signature
885 .try_compute_momentum(&loop_moms.0, &spatial_externals.raw)
886 .unwrap_or_else(|| unreachable!());
887 let mass = &masses[index];
888 let lifted_mass = new_constant(&momentum.px, mass);
889
890 (momentum.norm_squared() + lifted_mass.clone() * lifted_mass).sqrt()
891 })
892 .reduce(|acc, value| acc + value)
893 .unwrap_or_else(|| zero.clone());
894
895 full_external_shift + remaining_shift
896}
897
898#[allow(clippy::too_many_arguments)]
899fn compute_self_and_r_derivative_subspace_dual<T: FloatLike>(
900 esurface: &Esurface,
901 radius: &HyperDual<F<T>>,
902 shifted_unit_loops_in_subspace: &LoopMomenta<HyperDual<F<T>>>,
903 center_in_subspace: &LoopMomenta<HyperDual<F<T>>>,
904 external_moms: &ExternalFourMomenta<HyperDual<F<T>>>,
905 real_mass_vector: &EdgeVec<F<T>>,
906 subspace: &SubspaceData,
907 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
908 graph: &Graph,
909) -> (HyperDual<F<T>>, HyperDual<F<T>>) {
910 let spatial_part_of_externals = external_moms
911 .iter()
912 .map(|momentum| momentum.spatial.clone())
913 .collect::<TiVec<ExternalIndex, _>>();
914
915 let loops: LoopMomenta<HyperDual<F<T>>> = shifted_unit_loops_in_subspace
916 .iter_enumerated()
917 .map(|(loop_index, shifted_unit_momenta)| {
918 if subspace.contains_loop_index(loop_index) {
919 shifted_unit_momenta * radius + ¢er_in_subspace[loop_index]
920 } else {
921 shifted_unit_momenta.clone()
922 }
923 })
924 .collect();
925
926 let shift = compute_shift_part_from_dual_momenta_in_subspace(
927 esurface,
928 shifted_unit_loops_in_subspace,
929 external_moms,
930 subspace,
931 all_lmbs,
932 graph,
933 real_mass_vector,
934 );
935
936 let lmb = subspace.get_lmb(all_lmbs);
937 let zero = new_constant(radius, &radius.values[0].zero());
938 let (derivative, energy_sum) = subspace
939 .contains(&esurface.energies, graph)
940 .map(|index| {
941 let signature = &lmb.edge_signatures[index];
942 let momentum = signature
943 .try_compute_momentum(&loops.0, &spatial_part_of_externals.raw)
944 .unwrap_or_else(|| unreachable!());
945 let unit_loop_part = compute_loop_part_subspace_dual(
946 &signature.internal,
947 shifted_unit_loops_in_subspace,
948 subspace,
949 );
950 let mass = &real_mass_vector[index];
951 let lifted_mass = new_constant(&momentum.px, mass);
952 let energy = (momentum.norm_squared() + lifted_mass.clone() * lifted_mass).sqrt();
953 let numerator = momentum * &unit_loop_part;
954
955 (numerator / &energy, energy)
956 })
957 .fold((zero.clone(), zero), |(der_sum, en_sum), (der, en)| {
958 (der_sum + der, en_sum + en)
959 });
960
961 (energy_sum + shift, derivative)
962}
963
964fn compute_loop_part_subspace_dual<T: FloatLike>(
965 loop_signature: &LoopSignature,
966 loop_moms: &LoopMomenta<HyperDual<F<T>>>,
967 subspace: &SubspaceData,
968) -> ThreeMomentum<HyperDual<F<T>>> {
969 let projected: LoopSignature = subspace.project_loop_signature(loop_signature).collect();
970 let zero = new_constant(
971 &loop_moms[LoopIndex(0)].px,
972 &loop_moms[LoopIndex(0)].px.values[0].zero(),
973 );
974 let mut result = ThreeMomentum::new(zero.clone(), zero.clone(), zero);
975
976 for (loop_index, sign) in projected.iter_enumerated() {
977 match sign {
978 SignOrZero::Zero => {}
979 SignOrZero::Plus => result += &loop_moms[loop_index],
980 SignOrZero::Minus => result -= &loop_moms[loop_index],
981 }
982 }
983
984 result
985}
986
987#[derive(Clone, Encode, Decode)]
988#[trait_decode(trait = GammaLoopContext)]
989pub struct LUThresholdHelperEvaluators {
990 pub left_thresholds: TiVec<LeftThresholdId, BTreeMap<CutCFFIndex, GenericEvaluator>>,
991 pub right_thresholds: TiVec<RightThresholdId, BTreeMap<CutCFFIndex, GenericEvaluator>>,
992 pub iterated: IteratedCtCollection<BTreeMap<CutCFFIndex, GenericEvaluator>>,
993}
994
995impl LUThresholdHelperEvaluators {
996 fn for_each_generic_evaluator_mut(
997 &mut self,
998 mut f: impl FnMut(&mut GenericEvaluator) -> color_eyre::Result<()>,
999 ) -> color_eyre::Result<()> {
1000 for evaluators in self.left_thresholds.iter_mut() {
1001 for evaluator in evaluators.values_mut() {
1002 f(evaluator)?;
1003 }
1004 }
1005 for evaluators in self.right_thresholds.iter_mut() {
1006 for evaluator in evaluators.values_mut() {
1007 f(evaluator)?;
1008 }
1009 }
1010 for evaluators in self.iterated.iter_mut() {
1011 for evaluator in evaluators.values_mut() {
1012 f(evaluator)?;
1013 }
1014 }
1015
1016 Ok(())
1017 }
1018}
1019
1020#[derive(Clone, Encode, Decode)]
1021#[trait_decode(trait = GammaLoopContext)]
1022pub struct LUCounterTermEvaluators {
1023 pub left_thresholds_evaluator: TiVec<LeftThresholdId, BTreeMap<CutCFFIndex, EvaluatorStack>>,
1024 pub right_thresholds_evaluator: TiVec<RightThresholdId, BTreeMap<CutCFFIndex, EvaluatorStack>>,
1025 pub iterated_evaluator: IteratedCtCollection<BTreeMap<CutCFFIndex, EvaluatorStack>>,
1026 pub threshold_helpers: LUThresholdHelperEvaluators,
1027 pub threshold_multipliers: Option<ThresholdMultiplierEvaluatorCollection>,
1028 pub residue_from_e_surface_evaluators: Vec<GenericEvaluator>,
1029}
1030
1031impl LUCounterTermEvaluators {
1032 fn residue_evaluators(
1033 parametric_integrands: &ParametricIntegrands,
1034 param_builder: &ParamBuilder,
1035 settings: &GlobalSettings,
1036 orientations: &TiVec<OrientationID, EdgeVec<Orientation>>,
1037 production_orientation_ids: &[OrientationID],
1038 ) -> (BTreeMap<CutCFFIndex, EvaluatorStack>, EvaluatorBuildTimings) {
1039 let mut timings = EvaluatorBuildTimings::default();
1040 let evaluators = parametric_integrands
1041 .integrands
1042 .iter()
1043 .map(|(index, atom)| {
1044 let dual_shape = shape_from_cut_cff_index(index);
1045 let (evaluator, evaluator_timings) = EvaluatorStack::from_integrand_with_timings(
1046 atom,
1047 param_builder,
1048 parametric_integrands.integrands.numerators(),
1049 (!settings.generation.explicit_orientation_sum_only)
1050 .then_some((&orientations.raw, production_orientation_ids)),
1051 dual_shape,
1052 &settings.generation.evaluator,
1053 )
1054 .unwrap();
1055 timings += evaluator_timings;
1056 (*index, evaluator)
1057 })
1058 .collect();
1059
1060 (evaluators, timings)
1061 }
1062
1063 pub(crate) fn generic_compileable_evaluator_count(&self) -> usize {
1064 let left = self
1065 .left_thresholds_evaluator
1066 .iter()
1067 .flat_map(|evaluators| evaluators.values())
1068 .map(EvaluatorStack::generic_evaluator_count)
1069 .sum::<usize>();
1070 let right = self
1071 .right_thresholds_evaluator
1072 .iter()
1073 .flat_map(|evaluators| evaluators.values())
1074 .map(EvaluatorStack::generic_evaluator_count)
1075 .sum::<usize>();
1076 let iterated = self
1077 .iterated_evaluator
1078 .iter()
1079 .flat_map(|evaluators| evaluators.values())
1080 .map(EvaluatorStack::generic_evaluator_count)
1081 .sum::<usize>();
1082
1083 left + right + iterated
1085 }
1086
1087 fn evaluate_residue_from_esurface<T: FloatLike>(
1088 &mut self,
1089 cut_group_id: CutGroupId,
1090 order: usize,
1091 pass_one_result: DualOrNot<Complex<F<T>>>,
1092 cut_esurface: &DualOrNot<F<T>>,
1093 evaluation_meta_data: &mut EvaluationMetaData,
1094 ) -> Complex<F<T>> {
1095 let mut params_for_pass_two = vec![];
1096 match pass_one_result {
1097 DualOrNot::Dual(dual_result) => {
1098 params_for_pass_two.extend_from_slice(&extract_t_derivatives_complex(dual_result));
1099 }
1100 DualOrNot::NonDual(non_dual_result) => {
1101 params_for_pass_two.push(non_dual_result);
1102 }
1103 }
1104
1105 match cut_esurface {
1106 DualOrNot::Dual(dual_e_surface) => {
1107 extract_t_derivatives(dual_e_surface.clone())[1..]
1108 .iter()
1109 .for_each(|value| params_for_pass_two.push(Complex::new_re(value.clone())));
1110 }
1111 DualOrNot::NonDual(non_dual_e_surface) => {
1112 debug!("non dual esurface: {}", non_dual_e_surface);
1113 params_for_pass_two.push(Complex::new_re(non_dual_e_surface.clone()));
1114 }
1115 }
1116
1117 debug!(
1118 "LU pass-two evaluator input: cut_group_id={}, lu_order={}, param_count={}",
1119 cut_group_id.0,
1120 order + 1,
1121 params_for_pass_two.len()
1122 );
1123 for (idx, value) in params_for_pass_two.iter().enumerate() {
1124 debug!(
1125 "LU pass-two evaluator param: cut_group_id={}, lu_order={}, index={}, value={}",
1126 cut_group_id.0,
1127 order + 1,
1128 idx,
1129 value
1130 );
1131 }
1132
1133 evaluate_evaluator_single(
1134 &mut self.residue_from_e_surface_evaluators[order],
1135 ¶ms_for_pass_two,
1136 evaluation_meta_data,
1137 )
1138 }
1139
1140 #[allow(clippy::too_many_arguments)]
1141 pub fn from_atoms(
1142 counterterm_data: &LUCounterTermData,
1143 max_cut_order: usize,
1144 threshold_helpers: LUThresholdHelperEvaluators,
1145 threshold_multipliers: Option<ThresholdMultiplierEvaluatorCollection>,
1146 param_builder: &ParamBuilder,
1147 settings: &GlobalSettings,
1148 orientations: &TiVec<OrientationID, EdgeVec<Orientation>>,
1149 production_orientation_ids: &[OrientationID],
1150 ) -> (Self, EvaluatorBuildTimings) {
1151 let mut timings = EvaluatorBuildTimings::default();
1152 let left_thresholds_evaluator = counterterm_data
1153 .left_atoms
1154 .iter()
1155 .map(|parametric_integrands| {
1156 let (evaluators, evaluator_timings) = Self::residue_evaluators(
1157 parametric_integrands,
1158 param_builder,
1159 settings,
1160 orientations,
1161 production_orientation_ids,
1162 );
1163 timings += evaluator_timings;
1164 evaluators
1165 })
1166 .collect();
1167
1168 let right_thresholds_evaluator = counterterm_data
1169 .right_atoms
1170 .iter()
1171 .map(|parametric_integrands| {
1172 let (evaluators, evaluator_timings) = Self::residue_evaluators(
1173 parametric_integrands,
1174 param_builder,
1175 settings,
1176 orientations,
1177 production_orientation_ids,
1178 );
1179 timings += evaluator_timings;
1180 evaluators
1181 })
1182 .collect();
1183
1184 let iterated_timings = std::cell::Cell::new(EvaluatorBuildTimings::default());
1185 let iterated_evaluator = counterterm_data.iterated.map_ref(|parametric_integrands| {
1186 let (evaluators, evaluator_timings) = Self::residue_evaluators(
1187 parametric_integrands,
1188 param_builder,
1189 settings,
1190 orientations,
1191 production_orientation_ids,
1192 );
1193 let mut timings = iterated_timings.get();
1194 timings += evaluator_timings;
1195 iterated_timings.set(timings);
1196 evaluators
1197 });
1198 timings += iterated_timings.get();
1199
1200 let symbolica_started = std::time::Instant::now();
1201 let pass_two_evaluator = (1..=max_cut_order)
1202 .map(|order| {
1203 build_derivative_structure(order as u8, -1, &settings.generation.evaluator)
1204 })
1205 .collect();
1206
1207 timings.symbolica_time += symbolica_started.elapsed();
1208
1209 if let Some(multipliers) = &threshold_multipliers {
1210 multipliers
1211 .validate(
1212 counterterm_data.left_thresholds.len(),
1213 counterterm_data.right_thresholds.len(),
1214 )
1215 .expect("invalid threshold-multiplier registry generated for LU counterterms");
1216 }
1217
1218 (
1219 LUCounterTermEvaluators {
1220 left_thresholds_evaluator,
1221 right_thresholds_evaluator,
1222 iterated_evaluator,
1223 threshold_helpers,
1224 threshold_multipliers,
1225 residue_from_e_surface_evaluators: pass_two_evaluator,
1226 },
1227 timings,
1228 )
1229 }
1230
1231 pub(crate) fn compile(
1232 &mut self,
1233 path: impl AsRef<Path>,
1234 cut_group_id: CutGroupId,
1235 frozen_mode: &FrozenCompilationMode,
1236 ) -> color_eyre::Result<()> {
1237 for (threshold_id, evaluators) in self.left_thresholds_evaluator.iter_mut_enumerated() {
1238 for (index, evaluator) in evaluators.iter_mut() {
1239 let name = format!(
1240 "cut_group_{}_left_threshold_{}_index_{}",
1241 cut_group_id.0, threshold_id.0, index
1242 );
1243 evaluator.compile(&name, path.as_ref(), frozen_mode)?;
1244 }
1245 }
1246
1247 for (threshold_id, evaluators) in self.right_thresholds_evaluator.iter_mut_enumerated() {
1248 for (index, evaluator) in evaluators.iter_mut() {
1249 let name = format!(
1250 "cut_group_{}_right_threshold_{}_index_{}",
1251 cut_group_id.0, threshold_id.0, index
1252 );
1253 evaluator.compile(&name, path.as_ref(), frozen_mode)?;
1254 }
1255 }
1256
1257 for (iterated_index, evaluators) in self.iterated_evaluator.iter_mut().enumerate() {
1258 for (index, evaluator) in evaluators.iter_mut() {
1259 let name = format!(
1260 "cut_group_{}_iterated_{}_index_{}",
1261 cut_group_id.0, iterated_index, index
1262 );
1263 evaluator.compile(&name, path.as_ref(), frozen_mode)?;
1264 }
1265 }
1266
1267 for (order, pass_to_evaluator) in self
1268 .residue_from_e_surface_evaluators
1269 .iter_mut()
1270 .enumerate()
1271 {
1272 let name = format!("cut_group_{}_pass_two_{}", cut_group_id.0, order);
1273 pass_to_evaluator.compile_external(
1274 path.as_ref().join(&name).with_extension("cpp"),
1275 &name,
1276 path.as_ref().join(&name).with_extension("so"),
1277 frozen_mode,
1278 )?;
1279 }
1280
1281 Ok(())
1282 }
1283
1284 pub(crate) fn for_each_generic_evaluator_mut(
1285 &mut self,
1286 mut f: impl FnMut(&mut GenericEvaluator) -> color_eyre::Result<()>,
1287 ) -> color_eyre::Result<()> {
1288 for evaluators in self.left_thresholds_evaluator.iter_mut() {
1289 for evaluator in evaluators.values_mut() {
1290 evaluator.for_each_generic_evaluator_mut(&mut f)?;
1291 }
1292 }
1293 for evaluators in self.right_thresholds_evaluator.iter_mut() {
1294 for evaluator in evaluators.values_mut() {
1295 evaluator.for_each_generic_evaluator_mut(&mut f)?;
1296 }
1297 }
1298 for evaluators in self.iterated_evaluator.iter_mut() {
1299 for evaluator in evaluators.values_mut() {
1300 evaluator.for_each_generic_evaluator_mut(&mut f)?;
1301 }
1302 }
1303 self.threshold_helpers
1304 .for_each_generic_evaluator_mut(&mut f)?;
1305 if let Some(multipliers) = &mut self.threshold_multipliers {
1306 multipliers.for_each_generic_evaluator_mut(&mut f)?;
1307 }
1308 for pass_to_evaluator in self.residue_from_e_surface_evaluators.iter_mut() {
1309 f(pass_to_evaluator)?;
1310 }
1311
1312 Ok(())
1313 }
1314}
1315
1316type CutThresholds = (
1317 TiVec<LeftThresholdId, Esurface>,
1318 TiVec<RightThresholdId, Esurface>,
1319);
1320
1321#[derive(Clone, Encode, Decode)]
1322#[trait_decode(trait = GammaLoopContext)]
1323pub(crate) struct LUVariantSubspaces {
1324 pub left_variant_ids: TiVec<LeftThresholdId, ThresholdCountertermVariantId>,
1325 pub right_variant_ids: TiVec<RightThresholdId, ThresholdCountertermVariantId>,
1326 pub left: TiVec<LeftThresholdId, SubspaceData>,
1327 pub right: TiVec<RightThresholdId, SubspaceData>,
1328}
1329
1330#[derive(Clone, Encode, Decode)]
1331#[trait_decode(trait = GammaLoopContext)]
1332pub(crate) struct LUCounterTerm {
1333 pub evaluators: TiVec<CutGroupId, LUCounterTermEvaluators>,
1334 pub thresholds: TiVec<CutGroupId, CutThresholds>,
1335 pub subspaces: TiVec<CutGroupId, (SubspaceData, SubspaceData)>,
1336 pub variant_subspaces: Option<TiVec<CutGroupId, LUVariantSubspaces>>,
1338 pub metadata_registry: Option<ThresholdCountertermMetadataRegistry>,
1340 pub active_cut_groups: TiVec<CutGroupId, bool>,
1341 pub active_left_thresholds: TiVec<CutGroupId, TiVec<LeftThresholdId, bool>>,
1342 pub active_right_thresholds: TiVec<CutGroupId, TiVec<RightThresholdId, bool>>,
1343 pub active_iterated_thresholds: TiVec<CutGroupId, IteratedCtCollection<bool>>,
1344 pub rstar_dependence_calculator: TiVec<CutGroupId, RstarTDependenceEvaluator>,
1345}
1346
1347pub(crate) struct LUCountertermEvaluation<T: FloatLike> {
1348 pub total: Complex<F<T>>,
1349 pub components: Option<Vec<GenericThresholdCountertermComponentWeight<T>>>,
1350}
1351
1352#[derive(Clone, Debug, PartialEq, Eq)]
1353struct GlobalOverlapRecord {
1354 cut_group_id: CutGroupId,
1355 side: ThresholdCountertermSide,
1356 local_threshold_id: usize,
1357 variant_id: Option<ThresholdCountertermVariantId>,
1358}
1359
1360#[derive(Debug)]
1364struct ThresholdSolveGroup<T: FloatLike> {
1365 thresholds: EsurfaceCollection,
1366 subspace: SubspaceData,
1367 kinematics: Vec<MomentumSample<T>>,
1368 records: Vec<GlobalOverlapRecord>,
1369 overlap: OverlapStructure,
1370 prefactor_power: usize,
1371}
1372
1373#[derive(Clone, Debug)]
1374pub struct LUSharedOverlaps<T: FloatLike> {
1375 left: OverlapStructure,
1376 right: OverlapStructure,
1377 left_groups: Vec<(Arc<ThresholdSolveGroup<T>>, usize)>,
1380 right_groups: Vec<(Arc<ThresholdSolveGroup<T>>, usize)>,
1381}
1382
1383struct PendingLUCountertermComponent<T: FloatLike> {
1384 component_id: usize,
1385 occurrence: ThresholdCountertermComponentOccurrence,
1386 multiplier_values: SmallVec<[F<T>; 2]>,
1387 effective_multiplier: F<T>,
1388 pass_one: Option<DualOrNot<Complex<F<T>>>>,
1390}
1391
1392impl<T: FloatLike> PendingLUCountertermComponent<T> {
1393 fn finish(
1394 self,
1395 zero: &F<T>,
1396 evaluate_pass_two: impl FnOnce(DualOrNot<Complex<F<T>>>) -> Complex<F<T>>,
1397 ) -> (Complex<F<T>>, GenericThresholdCountertermComponentWeight<T>) {
1398 let Some(pass_one) = self.pass_one else {
1399 let weighted = Complex::new_re(zero.clone());
1400 return (
1401 weighted.clone(),
1402 GenericThresholdCountertermComponentWeight {
1403 component_id: self.component_id,
1404 occurrence: self.occurrence,
1405 multiplier_values: self.multiplier_values,
1406 effective_multiplier: self.effective_multiplier,
1407 bare: None,
1408 weighted,
1409 evaluation_skipped: true,
1410 },
1411 );
1412 };
1413
1414 let bare = evaluate_pass_two(pass_one);
1415 let weighted = &bare * &Complex::new_re(self.effective_multiplier.clone());
1416 (
1417 weighted.clone(),
1418 GenericThresholdCountertermComponentWeight {
1419 component_id: self.component_id,
1420 occurrence: self.occurrence,
1421 multiplier_values: self.multiplier_values,
1422 effective_multiplier: self.effective_multiplier,
1423 bare: Some(bare),
1424 weighted,
1425 evaluation_skipped: false,
1426 },
1427 )
1428 }
1429}
1430
1431#[derive(Clone)]
1432pub struct LUCTKinematicPoint<T: FloatLike> {
1433 pub unrescaled_sample: MomentumSample<T>,
1434 pub dualized_momentum_sample_cache: Vec<MomentumSample<T>>,
1435 pub lu_cut_parameter_cache: Vec<LUParams<T>>,
1436 pub lu_cut_esurface_values: Vec<DualOrNot<F<T>>>,
1437}
1438
1439impl<T: FloatLike> LUCTKinematicPoint<T> {
1440 pub fn unrescaled_sample(&self) -> &MomentumSample<T> {
1441 &self.unrescaled_sample
1442 }
1443
1444 pub fn representative_sample(&self) -> &MomentumSample<T> {
1445 &self.dualized_momentum_sample_cache[0]
1446 }
1447
1448 pub fn sample_for_order(&self, order: usize) -> &MomentumSample<T> {
1449 &self.dualized_momentum_sample_cache[order]
1450 }
1451
1452 pub fn lmb_transform(&self, from: &LoopMomentumBasis, to: &LoopMomentumBasis) -> Self {
1453 let transformed_samples = self
1454 .dualized_momentum_sample_cache
1455 .iter()
1456 .map(|sample| sample.lmb_transform(from, to))
1457 .collect();
1458
1459 Self {
1460 unrescaled_sample: self.unrescaled_sample.clone(),
1461 dualized_momentum_sample_cache: transformed_samples,
1462 lu_cut_parameter_cache: self.lu_cut_parameter_cache.clone(),
1463 lu_cut_esurface_values: self.lu_cut_esurface_values.clone(),
1464 }
1465 }
1466
1467 pub fn non_dual_cut_params(&self) -> LUParams<T> {
1468 self.lu_cut_parameter_cache[0].clone()
1469 }
1470
1471 pub fn cut_params_for_order(&self, order: usize) -> &LUParams<T> {
1472 &self.lu_cut_parameter_cache[order]
1473 }
1474
1475 pub fn cut_params_for_cut_cff_index(&self, cut_cff_index: &CutCFFIndex) -> LUParams<T> {
1476 let lu_order = cut_cff_index
1477 .lu_cut_order
1478 .expect("LU cut parameter lookup requires lu_cut_order")
1479 - 1;
1480 let base_params = self.cut_params_for_order(lu_order).clone();
1481 let target_shape = shape_from_cut_cff_index(cut_cff_index).map(HyperDual::new);
1482 let t_variable = variable_indices_from_cut_cff_index(cut_cff_index).lu_cut;
1483
1484 LUParams {
1485 tstar: embed_real_dual_or_not_in_target_shape(
1486 &base_params.tstar,
1487 &target_shape,
1488 t_variable,
1489 ),
1490 h_function: embed_real_dual_or_not_in_target_shape(
1491 &base_params.h_function,
1492 &target_shape,
1493 t_variable,
1494 ),
1495 }
1496 }
1497
1498 pub fn cut_esurface_for_order(&self, order: usize) -> &DualOrNot<F<T>> {
1499 &self.lu_cut_esurface_values[order]
1500 }
1501
1502 pub fn non_dual_cut_esurface_value(&self) -> F<T> {
1503 match self.cut_esurface_for_order(0) {
1504 DualOrNot::NonDual(value) => value.clone(),
1505 DualOrNot::Dual(_) => {
1506 unreachable!("representative LU cut e-surface value must be non-dual")
1507 }
1508 }
1509 }
1510
1511 pub fn new(unrescaled_sample: MomentumSample<T>) -> Self {
1512 Self {
1513 unrescaled_sample,
1514 dualized_momentum_sample_cache: Vec::new(),
1515 lu_cut_parameter_cache: Vec::new(),
1516 lu_cut_esurface_values: Vec::new(),
1517 }
1518 }
1519}
1520
1521impl LUCounterTerm {
1522 fn radial_root_identity(
1523 graph_name: &str,
1524 cut_group_id: CutGroupId,
1525 side: &str,
1526 overlap_group: usize,
1527 esurface_id: EsurfaceID,
1528 probe_rotation: &Rotation,
1529 ) -> RadialRootIdentity {
1530 RadialRootIdentity::new(format!(
1531 "LU graph '{graph_name}' cut group {} {side} overlap group {overlap_group} E-surface {} probe rotation {}",
1532 cut_group_id.0, esurface_id.0, probe_rotation.method,
1533 ))
1534 }
1535
1536 pub(crate) fn cut_group_is_active(&self, cut_group_id: CutGroupId) -> bool {
1537 self.active_cut_groups[cut_group_id]
1538 }
1539
1540 pub(crate) fn compile(
1541 &mut self,
1542 path: impl AsRef<Path>,
1543 frozen_mode: &FrozenCompilationMode,
1544 ) -> color_eyre::Result<()> {
1545 for (cut_group_id, evaluators) in self.evaluators.iter_mut_enumerated() {
1546 evaluators.compile(path.as_ref(), cut_group_id, frozen_mode)?;
1547 }
1548 Ok(())
1549 }
1550
1551 pub(crate) fn for_each_generic_evaluator_mut(
1552 &mut self,
1553 mut f: impl FnMut(&mut GenericEvaluator) -> color_eyre::Result<()>,
1554 ) -> color_eyre::Result<()> {
1555 for evaluators in self.evaluators.iter_mut() {
1556 evaluators.for_each_generic_evaluator_mut(&mut f)?;
1557 }
1558 Ok(())
1559 }
1560
1561 fn ensure_active_cut_group(&self, cut_group_id: CutGroupId) -> Result<()> {
1562 if self.cut_group_is_active(cut_group_id) {
1563 return Ok(());
1564 }
1565
1566 Err(eyre!(
1567 "Cut group {} was reached at runtime even though generation marked it inactive for the selected orientation subset",
1568 cut_group_id.0
1569 ))
1570 }
1571
1572 fn ensure_active_left_threshold(
1573 &self,
1574 cut_group_id: CutGroupId,
1575 threshold_id: LeftThresholdId,
1576 ) -> Result<()> {
1577 if self.active_left_thresholds[cut_group_id][threshold_id] {
1578 return Ok(());
1579 }
1580
1581 Err(eyre!(
1582 "Left threshold evaluator {} for cut group {} was reached at runtime even though generation marked it inactive for the selected orientation subset",
1583 threshold_id.0,
1584 cut_group_id.0
1585 ))
1586 }
1587
1588 fn ensure_active_right_threshold(
1589 &self,
1590 cut_group_id: CutGroupId,
1591 threshold_id: RightThresholdId,
1592 ) -> Result<()> {
1593 if self.active_right_thresholds[cut_group_id][threshold_id] {
1594 return Ok(());
1595 }
1596
1597 Err(eyre!(
1598 "Right threshold evaluator {} for cut group {} was reached at runtime even though generation marked it inactive for the selected orientation subset",
1599 threshold_id.0,
1600 cut_group_id.0
1601 ))
1602 }
1603
1604 fn ensure_active_iterated_threshold(
1605 &self,
1606 cut_group_id: CutGroupId,
1607 iterated_index: (LeftThresholdId, RightThresholdId),
1608 ) -> Result<()> {
1609 if self.active_iterated_thresholds[cut_group_id][iterated_index] {
1610 return Ok(());
1611 }
1612
1613 Err(eyre!(
1614 "Iterated threshold evaluator ({}, {}) for cut group {} was reached at runtime even though generation marked it inactive for the selected orientation subset",
1615 iterated_index.0.0,
1616 iterated_index.1.0,
1617 cut_group_id.0
1618 ))
1619 }
1620
1621 #[allow(clippy::too_many_arguments)]
1631 pub(crate) fn prepare_shared_overlaps<T: FloatLike>(
1632 &self,
1633 cut_samples: &[(CutGroupId, &MomentumSample<T>)],
1634 graph: &Graph,
1635 masses: &EdgeVec<F<T>>,
1636 reversed_edges: &TiVec<CutGroupId, Vec<EdgeIndex>>,
1637 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
1638 settings: &RuntimeSettings,
1639 probe_rotation: &Rotation,
1640 canonical: Option<&TiVec<CutGroupId, Option<LUSharedOverlaps<ArbPrec>>>>,
1641 ) -> Result<TiVec<CutGroupId, Option<LUSharedOverlaps<T>>>> {
1642 if canonical.is_none() && !probe_rotation.is_identity() {
1643 return Err(eyre!(
1644 "physical overlap centers must be prepared in the canonical identity frame"
1645 ));
1646 }
1647 let mut canonical_groups = canonical.map(|locals| {
1650 locals
1651 .iter()
1652 .flatten()
1653 .flat_map(|local| local.left_groups.iter().chain(&local.right_groups))
1654 .map(|(group, _)| group)
1655 .unique_by(|group| Arc::as_ptr(group))
1656 .collect_vec()
1657 });
1658 if let Some(canonical) = canonical
1659 && (canonical.len() != self.thresholds.len()
1660 || canonical.iter_enumerated().any(|(cut, overlap)| {
1661 overlap.is_some() != cut_samples.iter().any(|(id, _)| *id == cut)
1662 }))
1663 {
1664 return Err(SamplingEvaluationError::UncertainGeometry {
1665 detail: format!(
1666 "physical selector/cut membership differs from canonical graph '{}'",
1667 graph.name
1668 ),
1669 }
1670 .into());
1671 }
1672 let mut partitions = BTreeMap::new();
1673 let e_cm = F::from_f64(settings.kinematics.e_cm);
1674 let existence_threshold = F::from_f64(settings.subtraction.esurface_existence_threshold);
1675 for &(cut_group_id, cut_sample) in cut_samples {
1676 let variants = self
1677 .variant_subspaces
1678 .as_ref()
1679 .map(|data| &data[cut_group_id]);
1680 for side in [
1681 ThresholdCountertermSide::Left,
1682 ThresholdCountertermSide::Right,
1683 ] {
1684 let (thresholds, legacy, subspaces, variant_ids, active) = match side {
1685 ThresholdCountertermSide::Left => (
1686 self.thresholds[cut_group_id]
1687 .0
1688 .iter()
1689 .cloned()
1690 .collect_vec(),
1691 &self.subspaces[cut_group_id].0,
1692 variants.map(|data| data.left.raw.as_slice()),
1693 variants.map(|data| data.left_variant_ids.raw.as_slice()),
1694 self.active_left_thresholds[cut_group_id].raw.as_slice(),
1695 ),
1696 ThresholdCountertermSide::Right => (
1697 self.thresholds[cut_group_id]
1698 .1
1699 .iter()
1700 .cloned()
1701 .collect_vec(),
1702 &self.subspaces[cut_group_id].1,
1703 variants.map(|data| data.right.raw.as_slice()),
1704 variants.map(|data| data.right_variant_ids.raw.as_slice()),
1705 self.active_right_thresholds[cut_group_id].raw.as_slice(),
1706 ),
1707 ThresholdCountertermSide::Amplitude => unreachable!(),
1708 };
1709 for (local_id, surface) in thresholds.into_iter().enumerate() {
1710 if !active[local_id] {
1711 continue;
1712 }
1713 let subspace = subspaces.map_or(legacy, |data| &data[local_id]);
1714 let native = cut_sample
1715 .lmb_transform(&graph.loop_momentum_basis, subspace.get_lmb(all_lmbs));
1716 if !surface
1717 .classify_existence_subspace(
1718 native.loop_moms(),
1719 native.external_moms(),
1720 subspace,
1721 all_lmbs,
1722 graph,
1723 masses,
1724 &reversed_edges[cut_group_id],
1725 &e_cm,
1726 &existence_threshold,
1727 )
1728 .is_existing()
1729 {
1730 continue;
1731 }
1732 let variant_id = variant_ids.map(|ids| ids[local_id]);
1733 let group_id = variant_id.and_then(|id| {
1734 self.metadata_registry
1735 .as_ref()
1736 .and_then(|registry| registry.variants.get(id.0))
1737 .and_then(|variant| variant.group_id)
1738 });
1739 let record = GlobalOverlapRecord {
1740 cut_group_id,
1741 side,
1742 local_threshold_id: local_id,
1743 variant_id,
1744 };
1745 partitions
1746 .entry((group_id, subspace.solve_signature(all_lmbs)))
1747 .or_insert_with(Vec::new)
1748 .push((record, surface, subspace.clone(), native));
1749 }
1750 }
1751 }
1752 let mut groups = Vec::new();
1753 for ((group_id, signature), entries) in partitions {
1754 let subspace = entries[0].2.clone();
1755 let mut thresholds = EsurfaceCollection::new();
1756 let mut kinematics = Vec::new();
1757 let mut records = Vec::new();
1758 let mut prefactor_power = 2;
1759 for (record, surface, native_subspace, native) in entries {
1760 let order = match record.side {
1761 ThresholdCountertermSide::Left => self.evaluators[record.cut_group_id]
1762 .left_thresholds_evaluator
1763 [LeftThresholdId::from(record.local_threshold_id)]
1764 .keys()
1765 .filter_map(|index| index.left_threshold_order)
1766 .max()
1767 .unwrap_or(1),
1768 ThresholdCountertermSide::Right => self.evaluators[record.cut_group_id]
1769 .right_thresholds_evaluator
1770 [RightThresholdId::from(record.local_threshold_id)]
1771 .keys()
1772 .filter_map(|index| index.right_threshold_order)
1773 .max()
1774 .unwrap_or(1),
1775 ThresholdCountertermSide::Amplitude => unreachable!(),
1776 };
1777 let num_right = self.thresholds[record.cut_group_id].1.len();
1778 let iterated_order = self.evaluators[record.cut_group_id]
1779 .iterated_evaluator
1780 .iter()
1781 .enumerate()
1782 .filter(|(index, _)| match record.side {
1783 ThresholdCountertermSide::Left => {
1784 index / num_right == record.local_threshold_id
1785 }
1786 ThresholdCountertermSide::Right => {
1787 index % num_right == record.local_threshold_id
1788 }
1789 ThresholdCountertermSide::Amplitude => false,
1790 })
1791 .flat_map(|(_, evaluator)| evaluator.keys())
1792 .filter_map(|index| match record.side {
1793 ThresholdCountertermSide::Left => index.left_threshold_order,
1794 ThresholdCountertermSide::Right => index.right_threshold_order,
1795 ThresholdCountertermSide::Amplitude => None,
1796 })
1797 .max()
1798 .unwrap_or(1);
1799 let order = order.max(iterated_order);
1800 prefactor_power = prefactor_power.max(2 * (order / 2 + 1));
1803 thresholds.push(surface);
1804 kinematics.push(native.lmb_transform(
1805 native_subspace.get_lmb(all_lmbs),
1806 subspace.get_lmb(all_lmbs),
1807 ));
1808 records.push(record);
1809 }
1810 let overlap = if let Some(groups) = &mut canonical_groups {
1811 let index = groups.iter().position(|group| {
1812 group.records == records
1813 && group.subspace.solve_signature(all_lmbs) == signature
1814 && group.subspace.get_lmb(all_lmbs).loop_edges
1815 == subspace.get_lmb(all_lmbs).loop_edges
1816 && group.prefactor_power == prefactor_power
1817 }).ok_or_else(|| SamplingEvaluationError::UncertainGeometry {
1818 detail: format!("physical overlap membership differs from canonical graph '{}' group {:?}, records {:?}", graph.name, group_id, records),
1819 })?;
1820 groups.remove(index).overlap.clone()
1821 } else {
1822 let solver_kinematics = kinematics
1823 .iter()
1824 .map(|sample| OverlapKinematics {
1825 loop_moms: sample.loop_moms().iter().map(|p| p.to_f64()).collect(),
1826 external_momenta: sample
1827 .external_moms()
1828 .iter()
1829 .map(|p| p.to_f64())
1830 .collect(),
1831 edge_masses: None,
1832 })
1833 .collect_vec();
1834 let existing: ExistingThresholds = thresholds.keys().collect();
1835 let input = OverlapInput {
1836 graph,
1837 settings,
1838 subspace: &subspace,
1839 threshold_subspaces: None,
1840 lmbs: all_lmbs,
1841 thresholds: &thresholds,
1842 edge_masses: masses.iter().map(|(_, mass)| F(mass.to_f64())).collect(),
1843 surface_kinematics: Some(&solver_kinematics),
1844 };
1845 let first = &solver_kinematics[0];
1846 overlap_subspace::find_maximal_overlap(
1847 &input,
1848 &existing,
1849 &first.loop_moms,
1850 &first.external_momenta,
1851 probe_rotation,
1852 )?
1853 };
1854 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect, #center;
1855 graph = %graph.name, group_id = ?group_id,
1856 file.solve_signature = ?signature,
1857 file.instances = ?records.iter().map(|record| (record.cut_group_id, record.side, record.local_threshold_id, record.variant_id)).collect_vec(),
1858 centers = overlap.overlap_groups.len(),
1859 "prepared complete threshold solve group"
1860 );
1861 groups.push(Arc::new(ThresholdSolveGroup {
1862 thresholds,
1863 subspace,
1864 kinematics,
1865 records,
1866 overlap,
1867 prefactor_power,
1868 }));
1869 }
1870 if canonical_groups
1871 .as_ref()
1872 .is_some_and(|groups| !groups.is_empty())
1873 {
1874 return Err(SamplingEvaluationError::UncertainGeometry {
1875 detail: format!(
1876 "physical graph '{}' omitted a canonical threshold solve group",
1877 graph.name
1878 ),
1879 }
1880 .into());
1881 }
1882 let mut result = ti_vec![None; self.thresholds.len()];
1883 for &(cut_group_id, _) in cut_samples {
1884 let mut local = LUSharedOverlaps {
1885 left: OverlapStructure::new_empty(),
1886 right: OverlapStructure::new_empty(),
1887 left_groups: Vec::new(),
1888 right_groups: Vec::new(),
1889 };
1890 for side in [
1891 ThresholdCountertermSide::Left,
1892 ThresholdCountertermSide::Right,
1893 ] {
1894 let (structure, contexts) = if side == ThresholdCountertermSide::Left {
1895 (&mut local.left, &mut local.left_groups)
1896 } else {
1897 (&mut local.right, &mut local.right_groups)
1898 };
1899 structure.existing_esurfaces = groups
1900 .iter()
1901 .flat_map(|group| &group.records)
1902 .filter(|record| record.cut_group_id == cut_group_id && record.side == side)
1903 .map(|record| EsurfaceID::from(record.local_threshold_id))
1904 .sorted()
1905 .collect();
1906 for group in &groups {
1907 let local_id = |id: ExistingEsurfaceId| {
1908 let record = &group.records[group.overlap.existing_esurfaces[id].0];
1909 if record.cut_group_id != cut_group_id || record.side != side {
1910 return None;
1911 }
1912 structure
1913 .existing_esurfaces
1914 .iter_enumerated()
1915 .find_map(|(id, surface)| {
1916 (surface.0 == record.local_threshold_id).then_some(id)
1917 })
1918 };
1919 for (center_index, center) in group.overlap.overlap_groups.iter().enumerate() {
1920 let members = center
1921 .existing_esurfaces
1922 .iter()
1923 .filter_map(|&id| local_id(id))
1924 .collect_vec();
1925 if members.is_empty() {
1926 continue;
1927 }
1928 let complement = center
1929 .complement
1930 .iter()
1931 .filter_map(|&id| local_id(id))
1932 .collect();
1933 structure.overlap_groups.push(OverlapGroup {
1934 existing_esurfaces: members,
1935 complement,
1936 center: center.center.clone(),
1937 });
1938 contexts.push((Arc::clone(group), center_index));
1939 }
1940 }
1941 }
1942 result[cut_group_id] = Some(local);
1945 }
1946 Ok(result)
1947 }
1948
1949 #[allow(clippy::too_many_arguments)]
1950 pub(crate) fn evaluate<T: FloatLike>(
1951 &mut self,
1952 kinematic_point: &LUCTKinematicPoint<T>,
1953 cut_group_id: CutGroupId,
1954 _reversed_edges: &[EdgeIndex],
1955 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
1956 graph: &Graph,
1957 masses: &EdgeVec<F<T>>,
1958 probe_rotation: &Rotation,
1959 settings: &RuntimeSettings,
1960 param_builder: &mut ParamBuilder<f64>,
1961 orientations: SingleOrAllOrientations<'_, OrientationID>,
1962 evaluation_meta_data: &mut EvaluationMetaData,
1963 record_components: bool,
1964 shared_overlaps: Option<&LUSharedOverlaps<T>>,
1965 ) -> Result<LUCountertermEvaluation<T>> {
1966 self.ensure_active_cut_group(cut_group_id)?;
1967
1968 let record_components = record_components && self.metadata_registry.is_some();
1971 let additional_params = settings.additional_params::<T>();
1972
1973 let (left_thresholds_typed, right_thresholds_typed) = &self.thresholds[cut_group_id];
1974
1975 let left_thresholds = TiVec::from_ref(&left_thresholds_typed.raw);
1976 let right_thresholds = TiVec::from_ref(&right_thresholds_typed.raw);
1977
1978 if left_thresholds.is_empty() && right_thresholds.is_empty() {
1979 return Ok(LUCountertermEvaluation {
1980 total: Complex::new_re(kinematic_point.representative_sample().zero()),
1981 components: record_components.then(Vec::new),
1982 });
1983 }
1984
1985 let (legacy_left_subspace, legacy_right_subspace) = &self.subspaces[cut_group_id];
1986 let variant_subspaces = self
1987 .variant_subspaces
1988 .as_ref()
1989 .map(|subspaces| &subspaces[cut_group_id]);
1990 let threshold_helper_outputs = if variant_subspaces.is_some() {
1991 LUThresholdHelperOutputs::Pieces
1992 } else if self.metadata_registry.is_some() {
1993 LUThresholdHelperOutputs::LegacyAndPieces
1994 } else {
1995 LUThresholdHelperOutputs::Legacy
1996 };
1997 let left_subspace = legacy_left_subspace;
1998 let right_subspace = legacy_right_subspace;
1999 let left_threshold_subspaces =
2000 variant_subspaces.map(|subspaces| subspaces.left.raw.as_slice());
2001 let right_threshold_subspaces =
2002 variant_subspaces.map(|subspaces| subspaces.right.raw.as_slice());
2003 let detailed_variant_ids = if record_components {
2004 let registry = self
2005 .metadata_registry
2006 .as_ref()
2007 .expect("record_components requires threshold metadata");
2008 let (left, right) = if let Some(subspaces) = variant_subspaces {
2009 (
2010 subspaces.left_variant_ids.iter().copied().collect_vec(),
2011 subspaces.right_variant_ids.iter().copied().collect_vec(),
2012 )
2013 } else {
2014 (
2015 registry.lu_variant_ids(cut_group_id, ThresholdCountertermSide::Left)?,
2016 registry.lu_variant_ids(cut_group_id, ThresholdCountertermSide::Right)?,
2017 )
2018 };
2019 if left.len() != left_thresholds.len() || right.len() != right_thresholds.len() {
2020 return Err(eyre!(
2021 "graph '{}' cut group {} threshold metadata maps {} left/{} right variants onto {} left/{} right runtime thresholds",
2022 graph.name,
2023 cut_group_id.0,
2024 left.len(),
2025 right.len(),
2026 left_thresholds.len(),
2027 right_thresholds.len(),
2028 ));
2029 }
2030 Some((left, right))
2031 } else {
2032 None
2033 };
2034 let Some(shared) = shared_overlaps else {
2035 return Ok(LUCountertermEvaluation {
2036 total: Complex::new_re(kinematic_point.representative_sample().zero()),
2037 components: record_components.then(Vec::new),
2038 });
2039 };
2040 let left_overlap = &shared.left;
2041 let right_overlap = &shared.right;
2042 debug!("left overlap structure: {}", left_overlap);
2043 debug!("right overlap structure: {}", right_overlap);
2044 let mut radial_root_failed = false;
2045 let left_counterterm_builder = CounterTermBuilder::new(
2046 graph,
2047 settings,
2048 left_thresholds,
2049 kinematic_point.clone(),
2050 left_overlap,
2051 masses,
2052 all_lmbs,
2053 left_subspace,
2054 left_threshold_subspaces,
2055 probe_rotation,
2056 cut_group_id,
2057 &shared.left_groups,
2058 );
2059 let left_overlap_builders = left_overlap
2060 .overlap_groups
2061 .iter()
2062 .enumerate()
2063 .map(|(group_index, group)| {
2064 group
2065 .existing_esurfaces
2066 .iter()
2067 .map(|id| {
2068 left_counterterm_builder
2069 .new_overlap_builder(group_index, left_overlap.existing_esurfaces[*id])
2070 })
2071 .collect_vec()
2072 })
2073 .collect_vec();
2074 let mut left_overlap_solutions = Vec::with_capacity(left_overlap.overlap_groups.len());
2075 for (overlap_group_index, (group, builders)) in left_overlap
2076 .overlap_groups
2077 .iter()
2078 .zip(&left_overlap_builders)
2079 .enumerate()
2080 {
2081 let mut solutions = Vec::with_capacity(builders.len());
2082 for (&id, builder) in group.existing_esurfaces.iter().zip(builders) {
2083 let esurface_id = left_overlap.existing_esurfaces[id];
2084 let identity = Self::radial_root_identity(
2085 &graph.name,
2086 cut_group_id,
2087 "left",
2088 overlap_group_index,
2089 esurface_id,
2090 probe_rotation,
2091 );
2092 match builder.new_esurface_builder(id).solve_rstar(
2093 &mut self.rstar_dependence_calculator[cut_group_id],
2094 &identity,
2095 evaluation_meta_data,
2096 ) {
2097 Some(solution) => solutions.push(solution),
2098 None => {
2099 evaluation_meta_data.record_threshold_counterterm_error(format!(
2100 "LU graph '{}' cut group {} left threshold {} center {} failed radial-root validation",
2101 graph.name, cut_group_id.0, esurface_id.0, overlap_group_index));
2102 radial_root_failed = true;
2103 }
2104 }
2105 }
2106 left_overlap_solutions.push(solutions);
2107 }
2108 let right_counterterm_builder = CounterTermBuilder::new(
2109 graph,
2110 settings,
2111 right_thresholds,
2112 kinematic_point.clone(),
2113 right_overlap,
2114 masses,
2115 all_lmbs,
2116 right_subspace,
2117 right_threshold_subspaces,
2118 probe_rotation,
2119 cut_group_id,
2120 &shared.right_groups,
2121 );
2122 let right_overlap_builders = right_overlap
2123 .overlap_groups
2124 .iter()
2125 .enumerate()
2126 .map(|(group_index, group)| {
2127 group
2128 .existing_esurfaces
2129 .iter()
2130 .map(|id| {
2131 right_counterterm_builder
2132 .new_overlap_builder(group_index, right_overlap.existing_esurfaces[*id])
2133 })
2134 .collect_vec()
2135 })
2136 .collect_vec();
2137 let mut right_overlap_solutions = Vec::with_capacity(right_overlap.overlap_groups.len());
2138 for (overlap_group_index, (group, builders)) in right_overlap
2139 .overlap_groups
2140 .iter()
2141 .zip(&right_overlap_builders)
2142 .enumerate()
2143 {
2144 let mut solutions = Vec::with_capacity(builders.len());
2145 for (&id, builder) in group.existing_esurfaces.iter().zip(builders) {
2146 let esurface_id = right_overlap.existing_esurfaces[id];
2147 let identity = Self::radial_root_identity(
2148 &graph.name,
2149 cut_group_id,
2150 "right",
2151 overlap_group_index,
2152 esurface_id,
2153 probe_rotation,
2154 );
2155 match builder.new_esurface_builder(id).solve_rstar(
2156 &mut self.rstar_dependence_calculator[cut_group_id],
2157 &identity,
2158 evaluation_meta_data,
2159 ) {
2160 Some(solution) => solutions.push(solution),
2161 None => {
2162 evaluation_meta_data.record_threshold_counterterm_error(format!(
2163 "LU graph '{}' cut group {} right threshold {} center {} failed radial-root validation",
2164 graph.name, cut_group_id.0, esurface_id.0, overlap_group_index));
2165 radial_root_failed = true;
2166 }
2167 }
2168 }
2169 right_overlap_solutions.push(solutions);
2170 }
2171 if radial_root_failed {
2172 return Ok(LUCountertermEvaluation {
2173 total: Complex::new_re(F::from_f64(f64::NAN)),
2174 components: None,
2175 });
2176 }
2177
2178 let zero = kinematic_point.representative_sample().zero();
2179 let mut total_result = Complex::new_re(zero.clone());
2180 let mut completed_components = record_components.then(Vec::new);
2181 let mut threshold_multiplier_workspace = self.evaluators[cut_group_id]
2182 .threshold_multipliers
2183 .as_ref()
2184 .map(|collection| {
2185 ThresholdMultiplierInputWorkspace::new(collection.layout(), zero.clone())
2186 });
2187 if threshold_multiplier_workspace.is_some() && variant_subspaces.is_none() {
2188 return Err(eyre!(
2189 "graph '{}' cut group {} has threshold multipliers but no variant/subspace registry",
2190 graph.name,
2191 cut_group_id.0,
2192 ));
2193 }
2194
2195 for order in 0..kinematic_point.lu_cut_parameter_cache.len() {
2196 let mut pending_components = record_components.then(Vec::new);
2197 let mut left_evaluations = zero_dual_or_not_complex(order, &zero);
2198
2199 for (overlap_group_index, solutions_group) in left_overlap_solutions.iter().enumerate()
2200 {
2201 for solution in solutions_group {
2202 let representative_sample = solution.rstar_sample_for_order(order);
2203 let left_threshold_id =
2204 LeftThresholdId::from(representative_sample.get_esurface_id().0);
2205 self.ensure_active_left_threshold(cut_group_id, left_threshold_id)?;
2206
2207 let matching_cut_indices = self.evaluators[cut_group_id]
2208 .left_thresholds_evaluator[left_threshold_id]
2209 .keys()
2210 .filter(|cut_cff_index| {
2211 cut_cff_index.lu_cut_order == Some(order + 1)
2212 && cut_cff_index.right_threshold_order.is_none()
2213 })
2214 .copied()
2215 .collect_vec();
2216
2217 for cut_cff_index in matching_cut_indices {
2218 let variable_indices = variable_indices_from_cut_cff_index(&cut_cff_index);
2219 let lu_cut_params =
2220 kinematic_point.cut_params_for_cut_cff_index(&cut_cff_index);
2221 let sample = solution.rstar_sample_for_cut_cff_index(
2222 &cut_cff_index,
2223 variable_indices.left_threshold,
2224 );
2225 debug!("left threshold parameters");
2226
2227 let left_threshold_params: ThresholdParams<T> =
2228 sample.extract_threshold_parameters(true);
2229 debug!(
2230 "LU left evaluator input: cut_group_id={}, left_threshold_id={}, cut_cff_index={:?}, lu_order={}, r={}, rstar={}",
2231 cut_group_id.0,
2232 left_threshold_id.0,
2233 cut_cff_index,
2234 order + 1,
2235 left_threshold_params.radius,
2236 left_threshold_params.radius_star,
2237 );
2238 let inverse_transformed_sample = sample.get_inverse_transformed_sample();
2239 let detailed_variant_id = detailed_variant_ids
2240 .as_ref()
2241 .map(|(left, _)| left[left_threshold_id.0]);
2242 let multiplier_coefficients = if let Some(workspace) =
2243 threshold_multiplier_workspace.as_mut()
2244 {
2245 let variant_id = variant_subspaces
2246 .expect("multiplier registry requires variant subspaces")
2247 .left_variant_ids[left_threshold_id];
2248 let collection = self.evaluators[cut_group_id]
2249 .threshold_multipliers
2250 .as_mut()
2251 .expect("multiplier workspace requires evaluator collection");
2252 let (effective, star) = single_multiplier_context(
2253 SingleMultiplierComponent::Local,
2254 kinematic_point.representative_sample(),
2255 &inverse_transformed_sample,
2256 );
2257 let local = evaluate_single_multiplier_context(
2258 collection,
2259 workspace,
2260 variant_id,
2261 &graph.loop_momentum_basis,
2262 masses,
2263 param_builder.model_values(),
2264 &additional_params,
2265 effective,
2266 star,
2267 evaluation_meta_data,
2268 )
2269 .with_context(|| {
2270 format!(
2271 "Failed to evaluate local multiplier for graph '{}' cut group {} left variant {}",
2272 graph.name, cut_group_id.0, variant_id.0,
2273 )
2274 })?;
2275 let (effective, star) = single_multiplier_context(
2276 SingleMultiplierComponent::Integrated,
2277 kinematic_point.representative_sample(),
2278 &inverse_transformed_sample,
2279 );
2280 let integrated = evaluate_single_multiplier_context(
2281 collection,
2282 workspace,
2283 variant_id,
2284 &graph.loop_momentum_basis,
2285 masses,
2286 param_builder.model_values(),
2287 &additional_params,
2288 effective,
2289 star,
2290 evaluation_meta_data,
2291 )
2292 .with_context(|| {
2293 format!(
2294 "Failed to evaluate integrated multiplier for graph '{}' cut group {} left variant {}",
2295 graph.name, cut_group_id.0, variant_id.0,
2296 )
2297 })?;
2298 let coefficients = SingleMultiplierCoefficients { local, integrated };
2299 Some(coefficients)
2300 } else if record_components {
2301 let one = kinematic_point.representative_sample().one();
2302 Some(SingleMultiplierCoefficients {
2303 local: one.clone(),
2304 integrated: one,
2305 })
2306 } else {
2307 None
2308 };
2309
2310 if multiplier_coefficients
2311 .as_ref()
2312 .is_some_and(SingleMultiplierCoefficients::all_zero)
2313 {
2314 if let Some(pending) = pending_components.as_mut() {
2315 let coefficients = multiplier_coefficients.as_ref().unwrap();
2316 let variant_id = detailed_variant_id.expect(
2317 "detailed zero multiplier requires a stable left variant ID",
2318 );
2319 let registry = self.metadata_registry.as_ref().unwrap();
2320 for (kind, coefficient) in [
2321 (
2322 ThresholdCountertermComponentKind::Local,
2323 &coefficients.local,
2324 ),
2325 (
2326 ThresholdCountertermComponentKind::Integrated,
2327 &coefficients.integrated,
2328 ),
2329 ] {
2330 pending.push(PendingLUCountertermComponent {
2331 component_id: registry.component_id(
2332 Some(cut_group_id),
2333 kind,
2334 &[variant_id],
2335 )?,
2336 occurrence: ThresholdCountertermComponentOccurrence::LocalUnitarity {
2337 overlap_groups: SmallVec::from_slice(&[
2338 overlap_group_index,
2339 ]),
2340 left_threshold_order: cut_cff_index.left_threshold_order,
2341 right_threshold_order: None,
2342 lu_cut_order: cut_cff_index.lu_cut_order,
2343 },
2344 multiplier_values: smallvec![coefficient.clone()],
2345 effective_multiplier: coefficient.clone(),
2346 pass_one: None,
2347 });
2348 }
2349 }
2350 continue;
2351 }
2352
2353 let params = T::get_parameters(
2354 param_builder,
2355 (false, false),
2356 graph,
2357 &inverse_transformed_sample,
2358 settings.kinematics.externals.get_helicities(),
2359 &additional_params,
2360 Some(&left_threshold_params),
2361 None,
2362 Some(&lu_cut_params),
2363 );
2364
2365 let result_of_this_ct = self.evaluators[cut_group_id]
2366 .left_thresholds_evaluator[left_threshold_id]
2367 .get_mut(&cut_cff_index)
2368 .unwrap()
2369 .evaluate(params, orientations, settings, evaluation_meta_data)
2370 .unwrap()
2371 .pop()
2372 .unwrap();
2373
2374 let result_of_this_ct = multiply_dual_or_not_complex(
2378 result_of_this_ct,
2379 &sample.value_of_multi_channeling_factor,
2380 );
2381
2382 debug!(
2383 "result of left threshold evaluator {:?}: {}",
2384 cut_cff_index, result_of_this_ct
2385 );
2386
2387 let helper_evaluation = evaluate_threshold_helper_single(
2388 ThresholdHelperEvaluationContext {
2389 helper: self.evaluators[cut_group_id]
2390 .threshold_helpers
2391 .left_thresholds[left_threshold_id]
2392 .get_mut(&cut_cff_index)
2393 .unwrap(),
2394 cut_cff_index: &cut_cff_index,
2395 threshold_result: &result_of_this_ct,
2396 outputs: threshold_helper_outputs,
2397 evaluation_meta_data,
2398 },
2399 &left_threshold_params,
2400 );
2401 if let Some(pending) = pending_components.as_mut() {
2402 let helper_completed_pieces = helper_evaluation.into_pieces();
2403 let coefficients = multiplier_coefficients
2404 .as_ref()
2405 .expect("detailed components always materialize identity factors");
2406 let variant_id = detailed_variant_id
2407 .expect("detailed left component requires a stable variant ID");
2408 let registry = self.metadata_registry.as_ref().unwrap();
2409 for (kind, piece, coefficient) in [
2410 (
2411 ThresholdCountertermComponentKind::Local,
2412 helper_completed_pieces.local,
2413 coefficients.local.clone(),
2414 ),
2415 (
2416 ThresholdCountertermComponentKind::Integrated,
2417 helper_completed_pieces.integrated,
2418 coefficients.integrated.clone(),
2419 ),
2420 ] {
2421 let pass_one = (!coefficient.is_zero())
2422 .then(|| negate_dual_or_not_complex(piece));
2423 pending.push(PendingLUCountertermComponent {
2424 component_id: registry.component_id(
2425 Some(cut_group_id),
2426 kind,
2427 &[variant_id],
2428 )?,
2429 occurrence:
2430 ThresholdCountertermComponentOccurrence::LocalUnitarity {
2431 overlap_groups: SmallVec::from_slice(&[
2432 overlap_group_index,
2433 ]),
2434 left_threshold_order: cut_cff_index
2435 .left_threshold_order,
2436 right_threshold_order: None,
2437 lu_cut_order: cut_cff_index.lu_cut_order,
2438 },
2439 multiplier_values: smallvec![coefficient.clone()],
2440 effective_multiplier: coefficient,
2441 pass_one,
2442 });
2443 }
2444 } else {
2445 let helper_completed_result = if let Some(coefficients) =
2446 &multiplier_coefficients
2447 {
2448 weight_single_multiplier_pieces(
2449 helper_evaluation.into_pieces(),
2450 coefficients,
2451 order,
2452 &zero,
2453 )
2454 } else if threshold_helper_outputs == LUThresholdHelperOutputs::Pieces {
2455 let helper_completed_pieces = helper_evaluation.into_pieces();
2456 let mut result = helper_completed_pieces.local;
2457 result += helper_completed_pieces.integrated;
2458 result
2459 } else {
2460 helper_evaluation.into_legacy()
2461 };
2462 left_evaluations += helper_completed_result;
2463 }
2464 }
2465 }
2466 }
2467
2468 let mut right_evaluations = zero_dual_or_not_complex(order, &zero);
2469
2470 for (overlap_group_index, solutions_group) in right_overlap_solutions.iter().enumerate()
2471 {
2472 for solution in solutions_group {
2473 let representative_sample = solution.rstar_sample_for_order(order);
2474 let right_threshold_id =
2475 RightThresholdId::from(representative_sample.get_esurface_id().0);
2476 self.ensure_active_right_threshold(cut_group_id, right_threshold_id)?;
2477
2478 let matching_cut_indices = self.evaluators[cut_group_id]
2479 .right_thresholds_evaluator[right_threshold_id]
2480 .keys()
2481 .filter(|cut_cff_index| {
2482 cut_cff_index.lu_cut_order == Some(order + 1)
2483 && cut_cff_index.left_threshold_order.is_none()
2484 })
2485 .copied()
2486 .collect_vec();
2487
2488 for cut_cff_index in matching_cut_indices {
2489 let variable_indices = variable_indices_from_cut_cff_index(&cut_cff_index);
2490 let lu_cut_params =
2491 kinematic_point.cut_params_for_cut_cff_index(&cut_cff_index);
2492 let sample = solution.rstar_sample_for_cut_cff_index(
2493 &cut_cff_index,
2494 variable_indices.right_threshold,
2495 );
2496 debug!("right threshold parameters");
2497 let right_threshold_params: ThresholdParams<T> =
2498 sample.extract_threshold_parameters(true);
2499 debug!(
2500 "LU right evaluator input: cut_group_id={}, right_threshold_id={}, cut_cff_index={:?}, lu_order={}, r={}, rstar={}",
2501 cut_group_id.0,
2502 right_threshold_id.0,
2503 cut_cff_index,
2504 order + 1,
2505 right_threshold_params.radius,
2506 right_threshold_params.radius_star,
2507 );
2508 let inverse_transformed_sample = sample.get_inverse_transformed_sample();
2509 let detailed_variant_id = detailed_variant_ids
2510 .as_ref()
2511 .map(|(_, right)| right[right_threshold_id.0]);
2512 let multiplier_coefficients = if let Some(workspace) =
2513 threshold_multiplier_workspace.as_mut()
2514 {
2515 let variant_id = variant_subspaces
2516 .expect("multiplier registry requires variant subspaces")
2517 .right_variant_ids[right_threshold_id];
2518 let collection = self.evaluators[cut_group_id]
2519 .threshold_multipliers
2520 .as_mut()
2521 .expect("multiplier workspace requires evaluator collection");
2522 let (effective, star) = single_multiplier_context(
2523 SingleMultiplierComponent::Local,
2524 kinematic_point.representative_sample(),
2525 &inverse_transformed_sample,
2526 );
2527 let local = evaluate_single_multiplier_context(
2528 collection,
2529 workspace,
2530 variant_id,
2531 &graph.loop_momentum_basis,
2532 masses,
2533 param_builder.model_values(),
2534 &additional_params,
2535 effective,
2536 star,
2537 evaluation_meta_data,
2538 )
2539 .with_context(|| {
2540 format!(
2541 "Failed to evaluate local multiplier for graph '{}' cut group {} right variant {}",
2542 graph.name, cut_group_id.0, variant_id.0,
2543 )
2544 })?;
2545 let (effective, star) = single_multiplier_context(
2546 SingleMultiplierComponent::Integrated,
2547 kinematic_point.representative_sample(),
2548 &inverse_transformed_sample,
2549 );
2550 let integrated = evaluate_single_multiplier_context(
2551 collection,
2552 workspace,
2553 variant_id,
2554 &graph.loop_momentum_basis,
2555 masses,
2556 param_builder.model_values(),
2557 &additional_params,
2558 effective,
2559 star,
2560 evaluation_meta_data,
2561 )
2562 .with_context(|| {
2563 format!(
2564 "Failed to evaluate integrated multiplier for graph '{}' cut group {} right variant {}",
2565 graph.name, cut_group_id.0, variant_id.0,
2566 )
2567 })?;
2568 let coefficients = SingleMultiplierCoefficients { local, integrated };
2569 Some(coefficients)
2570 } else if record_components {
2571 let one = kinematic_point.representative_sample().one();
2572 Some(SingleMultiplierCoefficients {
2573 local: one.clone(),
2574 integrated: one,
2575 })
2576 } else {
2577 None
2578 };
2579
2580 if multiplier_coefficients
2581 .as_ref()
2582 .is_some_and(SingleMultiplierCoefficients::all_zero)
2583 {
2584 if let Some(pending) = pending_components.as_mut() {
2585 let coefficients = multiplier_coefficients.as_ref().unwrap();
2586 let variant_id = detailed_variant_id.expect(
2587 "detailed zero multiplier requires a stable right variant ID",
2588 );
2589 let registry = self.metadata_registry.as_ref().unwrap();
2590 for (kind, coefficient) in [
2591 (
2592 ThresholdCountertermComponentKind::Local,
2593 &coefficients.local,
2594 ),
2595 (
2596 ThresholdCountertermComponentKind::Integrated,
2597 &coefficients.integrated,
2598 ),
2599 ] {
2600 pending.push(PendingLUCountertermComponent {
2601 component_id: registry.component_id(
2602 Some(cut_group_id),
2603 kind,
2604 &[variant_id],
2605 )?,
2606 occurrence: ThresholdCountertermComponentOccurrence::LocalUnitarity {
2607 overlap_groups: SmallVec::from_slice(&[
2608 overlap_group_index,
2609 ]),
2610 left_threshold_order: None,
2611 right_threshold_order: cut_cff_index.right_threshold_order,
2612 lu_cut_order: cut_cff_index.lu_cut_order,
2613 },
2614 multiplier_values: smallvec![coefficient.clone()],
2615 effective_multiplier: coefficient.clone(),
2616 pass_one: None,
2617 });
2618 }
2619 }
2620 continue;
2621 }
2622
2623 let params = T::get_parameters(
2624 param_builder,
2625 (false, false),
2626 graph,
2627 &inverse_transformed_sample,
2628 settings.kinematics.externals.get_helicities(),
2629 &additional_params,
2630 None,
2631 Some(&right_threshold_params),
2632 Some(&lu_cut_params),
2633 );
2634
2635 let result_of_this_ct = self.evaluators[cut_group_id]
2636 .right_thresholds_evaluator[right_threshold_id]
2637 .get_mut(&cut_cff_index)
2638 .unwrap()
2639 .evaluate(params, orientations, settings, evaluation_meta_data)
2640 .unwrap()
2641 .pop()
2642 .unwrap();
2643
2644 let result_of_this_ct = multiply_dual_or_not_complex(
2648 result_of_this_ct,
2649 &sample.value_of_multi_channeling_factor,
2650 );
2651
2652 debug!(
2653 "result of right threshold evaluator {:?}: {}",
2654 cut_cff_index, result_of_this_ct
2655 );
2656
2657 let helper_evaluation = evaluate_threshold_helper_single(
2658 ThresholdHelperEvaluationContext {
2659 helper: self.evaluators[cut_group_id]
2660 .threshold_helpers
2661 .right_thresholds[right_threshold_id]
2662 .get_mut(&cut_cff_index)
2663 .unwrap(),
2664 cut_cff_index: &cut_cff_index,
2665 threshold_result: &result_of_this_ct,
2666 outputs: threshold_helper_outputs,
2667 evaluation_meta_data,
2668 },
2669 &right_threshold_params,
2670 );
2671 if let Some(pending) = pending_components.as_mut() {
2672 let helper_completed_pieces = helper_evaluation.into_pieces();
2673 let coefficients = multiplier_coefficients
2674 .as_ref()
2675 .expect("detailed components always materialize identity factors");
2676 let variant_id = detailed_variant_id
2677 .expect("detailed right component requires a stable variant ID");
2678 let registry = self.metadata_registry.as_ref().unwrap();
2679 for (kind, piece, coefficient) in [
2680 (
2681 ThresholdCountertermComponentKind::Local,
2682 helper_completed_pieces.local,
2683 coefficients.local.clone(),
2684 ),
2685 (
2686 ThresholdCountertermComponentKind::Integrated,
2687 helper_completed_pieces.integrated,
2688 coefficients.integrated.clone(),
2689 ),
2690 ] {
2691 let pass_one = (!coefficient.is_zero())
2692 .then(|| negate_dual_or_not_complex(piece));
2693 pending.push(PendingLUCountertermComponent {
2694 component_id: registry.component_id(
2695 Some(cut_group_id),
2696 kind,
2697 &[variant_id],
2698 )?,
2699 occurrence:
2700 ThresholdCountertermComponentOccurrence::LocalUnitarity {
2701 overlap_groups: SmallVec::from_slice(&[
2702 overlap_group_index,
2703 ]),
2704 left_threshold_order: None,
2705 right_threshold_order: cut_cff_index
2706 .right_threshold_order,
2707 lu_cut_order: cut_cff_index.lu_cut_order,
2708 },
2709 multiplier_values: smallvec![coefficient.clone()],
2710 effective_multiplier: coefficient,
2711 pass_one,
2712 });
2713 }
2714 } else {
2715 let helper_completed_result = if let Some(coefficients) =
2716 &multiplier_coefficients
2717 {
2718 weight_single_multiplier_pieces(
2719 helper_evaluation.into_pieces(),
2720 coefficients,
2721 order,
2722 &zero,
2723 )
2724 } else if threshold_helper_outputs == LUThresholdHelperOutputs::Pieces {
2725 let helper_completed_pieces = helper_evaluation.into_pieces();
2726 let mut result = helper_completed_pieces.local;
2727 result += helper_completed_pieces.integrated;
2728 result
2729 } else {
2730 helper_evaluation.into_legacy()
2731 };
2732 right_evaluations += helper_completed_result;
2733 }
2734 }
2735 }
2736 }
2737
2738 let mut cartesian_product_result = zero_dual_or_not_complex(order, &zero);
2739
2740 for (left_overlap_group_index, left_solutions_group) in
2741 left_overlap_solutions.iter().enumerate()
2742 {
2743 for left_solution in left_solutions_group {
2744 let left_sample_representative = left_solution.rstar_sample_for_order(order);
2745 for (right_overlap_group_index, right_solutions_group) in
2746 right_overlap_solutions.iter().enumerate()
2747 {
2748 for right_solution in right_solutions_group {
2749 let right_sample_representative =
2750 right_solution.rstar_sample_for_order(order);
2751 let iterated_index = (
2752 LeftThresholdId::from(
2753 left_sample_representative.get_esurface_id().0,
2754 ),
2755 RightThresholdId::from(
2756 right_sample_representative.get_esurface_id().0,
2757 ),
2758 );
2759 self.ensure_active_iterated_threshold(cut_group_id, iterated_index)?;
2760
2761 let matching_cut_indices = self.evaluators[cut_group_id]
2762 .iterated_evaluator[iterated_index]
2763 .keys()
2764 .filter(|cut_cff_index| {
2765 cut_cff_index.lu_cut_order == Some(order + 1)
2766 })
2767 .copied()
2768 .collect_vec();
2769
2770 for cut_cff_index in matching_cut_indices {
2771 let variable_indices =
2772 variable_indices_from_cut_cff_index(&cut_cff_index);
2773 let lu_cut_params =
2774 kinematic_point.cut_params_for_cut_cff_index(&cut_cff_index);
2775 let sample_left = left_solution.rstar_sample_for_cut_cff_index(
2776 &cut_cff_index,
2777 variable_indices.left_threshold,
2778 );
2779 let sample_right = right_solution.rstar_sample_for_cut_cff_index(
2780 &cut_cff_index,
2781 variable_indices.right_threshold,
2782 );
2783
2784 let left_threshold_params: ThresholdParams<T> =
2785 sample_left.extract_threshold_parameters(false);
2786 let right_threshold_params: ThresholdParams<T> =
2787 sample_right.extract_threshold_parameters(false);
2788 debug!(
2789 "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={}",
2790 cut_group_id.0,
2791 iterated_index.0.0,
2792 iterated_index.1.0,
2793 cut_cff_index,
2794 order + 1,
2795 left_threshold_params.radius,
2796 left_threshold_params.radius_star,
2797 right_threshold_params.radius,
2798 right_threshold_params.radius_star,
2799 );
2800 let inverse_transformed_momentum_sample =
2801 merge_and_inverse_transform(
2802 &sample_left,
2803 &sample_right,
2804 &cut_cff_index,
2805 );
2806 let detailed_pair_variant_ids =
2807 detailed_variant_ids.as_ref().map(|(left, right)| {
2808 (left[iterated_index.0.0], right[iterated_index.1.0])
2809 });
2810 let multiplier_coefficients = if let Some(workspace) =
2811 threshold_multiplier_workspace.as_mut()
2812 {
2813 let variant_subspaces = variant_subspaces
2814 .expect("multiplier registry requires variant subspaces");
2815 let left_variant_id =
2816 variant_subspaces.left_variant_ids[iterated_index.0];
2817 let right_variant_id =
2818 variant_subspaces.right_variant_ids[iterated_index.1];
2819 let left_root = sample_left.get_inverse_transformed_sample();
2820 let right_root = sample_right.get_inverse_transformed_sample();
2821 let collection = self.evaluators[cut_group_id]
2822 .threshold_multipliers
2823 .as_mut()
2824 .expect(
2825 "multiplier workspace requires evaluator collection",
2826 );
2827
2828 let (effective, star) = iterated_multiplier_context(
2829 IteratedMultiplierComponent::LocalLocal,
2830 kinematic_point.representative_sample(),
2831 &left_root,
2832 &right_root,
2833 &inverse_transformed_momentum_sample,
2834 );
2835 let (left, right) = evaluate_pair_multiplier_context(
2836 collection,
2837 workspace,
2838 left_variant_id,
2839 right_variant_id,
2840 &graph.loop_momentum_basis,
2841 masses,
2842 param_builder.model_values(),
2843 &additional_params,
2844 effective,
2845 star,
2846 evaluation_meta_data,
2847 )
2848 .with_context(|| {
2849 format!(
2850 "Failed to evaluate LL multipliers for graph '{}' cut group {} variants ({}, {})",
2851 graph.name,
2852 cut_group_id.0,
2853 left_variant_id.0,
2854 right_variant_id.0,
2855 )
2856 })?;
2857 let local_local = PairMultiplierCoefficient { left, right };
2858
2859 let (effective, star) = iterated_multiplier_context(
2860 IteratedMultiplierComponent::IntegratedLocal,
2861 kinematic_point.representative_sample(),
2862 &left_root,
2863 &right_root,
2864 &inverse_transformed_momentum_sample,
2865 );
2866 let (left, right) = evaluate_pair_multiplier_context(
2867 collection,
2868 workspace,
2869 left_variant_id,
2870 right_variant_id,
2871 &graph.loop_momentum_basis,
2872 masses,
2873 param_builder.model_values(),
2874 &additional_params,
2875 effective,
2876 star,
2877 evaluation_meta_data,
2878 )
2879 .with_context(|| {
2880 format!(
2881 "Failed to evaluate IL multipliers for graph '{}' cut group {} variants ({}, {})",
2882 graph.name,
2883 cut_group_id.0,
2884 left_variant_id.0,
2885 right_variant_id.0,
2886 )
2887 })?;
2888 let integrated_local =
2889 PairMultiplierCoefficient { left, right };
2890
2891 let (effective, star) = iterated_multiplier_context(
2892 IteratedMultiplierComponent::LocalIntegrated,
2893 kinematic_point.representative_sample(),
2894 &left_root,
2895 &right_root,
2896 &inverse_transformed_momentum_sample,
2897 );
2898 let (left, right) = evaluate_pair_multiplier_context(
2899 collection,
2900 workspace,
2901 left_variant_id,
2902 right_variant_id,
2903 &graph.loop_momentum_basis,
2904 masses,
2905 param_builder.model_values(),
2906 &additional_params,
2907 effective,
2908 star,
2909 evaluation_meta_data,
2910 )
2911 .with_context(|| {
2912 format!(
2913 "Failed to evaluate LI multipliers for graph '{}' cut group {} variants ({}, {})",
2914 graph.name,
2915 cut_group_id.0,
2916 left_variant_id.0,
2917 right_variant_id.0,
2918 )
2919 })?;
2920 let local_integrated =
2921 PairMultiplierCoefficient { left, right };
2922
2923 let (effective, star) = iterated_multiplier_context(
2924 IteratedMultiplierComponent::IntegratedIntegrated,
2925 kinematic_point.representative_sample(),
2926 &left_root,
2927 &right_root,
2928 &inverse_transformed_momentum_sample,
2929 );
2930 let (left, right) = evaluate_pair_multiplier_context(
2931 collection,
2932 workspace,
2933 left_variant_id,
2934 right_variant_id,
2935 &graph.loop_momentum_basis,
2936 masses,
2937 param_builder.model_values(),
2938 &additional_params,
2939 effective,
2940 star,
2941 evaluation_meta_data,
2942 )
2943 .with_context(|| {
2944 format!(
2945 "Failed to evaluate II multipliers for graph '{}' cut group {} variants ({}, {})",
2946 graph.name,
2947 cut_group_id.0,
2948 left_variant_id.0,
2949 right_variant_id.0,
2950 )
2951 })?;
2952 let integrated_integrated =
2953 PairMultiplierCoefficient { left, right };
2954
2955 let coefficients = IteratedMultiplierCoefficients {
2956 local_local,
2957 local_integrated,
2958 integrated_local,
2959 integrated_integrated,
2960 };
2961 Some(coefficients)
2962 } else if record_components {
2963 let one = kinematic_point.representative_sample().one();
2964 let identity = || PairMultiplierCoefficient {
2965 left: one.clone(),
2966 right: one.clone(),
2967 };
2968 Some(IteratedMultiplierCoefficients {
2969 local_local: identity(),
2970 local_integrated: identity(),
2971 integrated_local: identity(),
2972 integrated_integrated: identity(),
2973 })
2974 } else {
2975 None
2976 };
2977
2978 if multiplier_coefficients
2979 .as_ref()
2980 .is_some_and(IteratedMultiplierCoefficients::all_zero)
2981 {
2982 if let Some(pending) = pending_components.as_mut() {
2983 let coefficients =
2984 multiplier_coefficients.as_ref().unwrap();
2985 let (left_variant_id, right_variant_id) =
2986 detailed_pair_variant_ids.expect(
2987 "detailed zero multipliers require stable pair variant IDs",
2988 );
2989 let registry = self.metadata_registry.as_ref().unwrap();
2990 for (kind, coefficient) in [
2991 (
2992 ThresholdCountertermComponentKind::LocalLocal,
2993 &coefficients.local_local,
2994 ),
2995 (
2996 ThresholdCountertermComponentKind::LocalIntegrated,
2997 &coefficients.local_integrated,
2998 ),
2999 (
3000 ThresholdCountertermComponentKind::IntegratedLocal,
3001 &coefficients.integrated_local,
3002 ),
3003 (
3004 ThresholdCountertermComponentKind::IntegratedIntegrated,
3005 &coefficients.integrated_integrated,
3006 ),
3007 ] {
3008 pending.push(PendingLUCountertermComponent {
3009 component_id: registry.component_id(
3010 Some(cut_group_id),
3011 kind,
3012 &[left_variant_id, right_variant_id],
3013 )?,
3014 occurrence: ThresholdCountertermComponentOccurrence::LocalUnitarity {
3015 overlap_groups: SmallVec::from_slice(&[
3016 left_overlap_group_index,
3017 right_overlap_group_index,
3018 ]),
3019 left_threshold_order: cut_cff_index.left_threshold_order,
3020 right_threshold_order: cut_cff_index.right_threshold_order,
3021 lu_cut_order: cut_cff_index.lu_cut_order,
3022 },
3023 multiplier_values: smallvec![
3024 coefficient.left.clone(),
3025 coefficient.right.clone(),
3026 ],
3027 effective_multiplier: coefficient.effective(),
3028 pass_one: None,
3029 });
3030 }
3031 }
3032 continue;
3033 }
3034
3035 let multi_channeling_factor = multiply_dual_or_not_complex(
3036 sample_left.value_of_multi_channeling_factor.clone(),
3037 &sample_right.value_of_multi_channeling_factor,
3038 );
3039
3040 let params = T::get_parameters(
3041 param_builder,
3042 (false, false),
3043 graph,
3044 &inverse_transformed_momentum_sample,
3045 settings.kinematics.externals.get_helicities(),
3046 &additional_params,
3047 Some(&left_threshold_params),
3048 Some(&right_threshold_params),
3049 Some(&lu_cut_params),
3050 );
3051
3052 let result_of_this_ct = self.evaluators[cut_group_id]
3053 .iterated_evaluator[iterated_index]
3054 .get_mut(&cut_cff_index)
3055 .unwrap()
3056 .evaluate(params, orientations, settings, evaluation_meta_data)
3057 .unwrap()
3058 .pop()
3059 .unwrap();
3060
3061 let result_of_this_ct = multiply_dual_or_not_complex(
3062 result_of_this_ct,
3063 &multi_channeling_factor,
3064 );
3065 let helper_evaluation = evaluate_threshold_helper_iterated(
3066 ThresholdHelperEvaluationContext {
3067 helper: self.evaluators[cut_group_id]
3068 .threshold_helpers
3069 .iterated[iterated_index]
3070 .get_mut(&cut_cff_index)
3071 .unwrap(),
3072 cut_cff_index: &cut_cff_index,
3073 threshold_result: &result_of_this_ct,
3074 outputs: threshold_helper_outputs,
3075 evaluation_meta_data,
3076 },
3077 &left_threshold_params,
3078 &right_threshold_params,
3079 );
3080 if let Some(pending) = pending_components.as_mut() {
3081 let helper_completed_pieces = helper_evaluation.into_pieces();
3082 let coefficients = multiplier_coefficients.as_ref().expect(
3083 "detailed components always materialize identity factors",
3084 );
3085 let (left_variant_id, right_variant_id) =
3086 detailed_pair_variant_ids.expect(
3087 "detailed pair component requires stable variant IDs",
3088 );
3089 let registry = self.metadata_registry.as_ref().unwrap();
3090 for (kind, piece, coefficient) in [
3091 (
3092 ThresholdCountertermComponentKind::LocalLocal,
3093 helper_completed_pieces.local_local,
3094 &coefficients.local_local,
3095 ),
3096 (
3097 ThresholdCountertermComponentKind::LocalIntegrated,
3098 helper_completed_pieces.local_integrated,
3099 &coefficients.local_integrated,
3100 ),
3101 (
3102 ThresholdCountertermComponentKind::IntegratedLocal,
3103 helper_completed_pieces.integrated_local,
3104 &coefficients.integrated_local,
3105 ),
3106 (
3107 ThresholdCountertermComponentKind::IntegratedIntegrated,
3108 helper_completed_pieces.integrated_integrated,
3109 &coefficients.integrated_integrated,
3110 ),
3111 ] {
3112 let pass_one = (!coefficient.is_zero()).then_some(piece);
3113 pending.push(PendingLUCountertermComponent {
3114 component_id: registry.component_id(
3115 Some(cut_group_id),
3116 kind,
3117 &[left_variant_id, right_variant_id],
3118 )?,
3119 occurrence: ThresholdCountertermComponentOccurrence::LocalUnitarity {
3120 overlap_groups: SmallVec::from_slice(&[
3121 left_overlap_group_index,
3122 right_overlap_group_index,
3123 ]),
3124 left_threshold_order: cut_cff_index.left_threshold_order,
3125 right_threshold_order: cut_cff_index.right_threshold_order,
3126 lu_cut_order: cut_cff_index.lu_cut_order,
3127 },
3128 multiplier_values: smallvec![
3129 coefficient.left.clone(),
3130 coefficient.right.clone(),
3131 ],
3132 effective_multiplier: coefficient.effective(),
3133 pass_one,
3134 });
3135 }
3136 } else {
3137 let helper_completed_result =
3138 if let Some(coefficients) = &multiplier_coefficients {
3139 weight_iterated_multiplier_pieces(
3140 helper_evaluation.into_pieces(),
3141 coefficients,
3142 order,
3143 &zero,
3144 )
3145 } else if threshold_helper_outputs
3146 == LUThresholdHelperOutputs::Pieces
3147 {
3148 let helper_completed_pieces =
3149 helper_evaluation.into_pieces();
3150 let mut result = helper_completed_pieces.local_local;
3151 result += helper_completed_pieces.local_integrated;
3152 result += helper_completed_pieces.integrated_local;
3153 result += helper_completed_pieces.integrated_integrated;
3154 result
3155 } else {
3156 helper_evaluation.into_legacy()
3157 };
3158
3159 cartesian_product_result += helper_completed_result;
3160 }
3161 }
3162 }
3163 }
3164 }
3165 }
3166
3167 let cut_esurface = kinematic_point.cut_esurface_for_order(order);
3168 if let Some(pending_components) = pending_components {
3169 let completed_components = completed_components
3170 .as_mut()
3171 .expect("detailed pending components require completed storage");
3172 for component in pending_components {
3173 let (weighted, completed) = component.finish(&zero, |pass_one| {
3174 self.evaluators[cut_group_id].evaluate_residue_from_esurface(
3175 cut_group_id,
3176 order,
3177 pass_one,
3178 cut_esurface,
3179 evaluation_meta_data,
3180 )
3181 });
3182 total_result += weighted;
3183 completed_components.push(completed);
3184 }
3185 } else {
3186 let mut pass_one_result = left_evaluations;
3187 pass_one_result += right_evaluations;
3188 pass_one_result += negate_dual_or_not_complex(cartesian_product_result);
3189 let pass_one_result = negate_dual_or_not_complex(pass_one_result);
3190 total_result += self.evaluators[cut_group_id].evaluate_residue_from_esurface(
3191 cut_group_id,
3192 order,
3193 pass_one_result,
3194 cut_esurface,
3195 evaluation_meta_data,
3196 );
3197 }
3198 }
3199
3200 Ok(LUCountertermEvaluation {
3201 total: total_result,
3202 components: completed_components,
3203 })
3204 }
3205}
3206
3207struct CounterTermBuilder<'a, T: FloatLike> {
3208 overlap_structure: &'a OverlapStructure,
3209 real_mass_vector: &'a EdgeVec<F<T>>,
3210 e_cm: F<T>,
3211 graph: &'a Graph,
3212 subspace: &'a SubspaceData,
3213 threshold_subspaces: Option<&'a [SubspaceData]>,
3214 all_lmbs: &'a TiVec<LmbIndex, LoopMomentumBasis>,
3215 settings: &'a RuntimeSettings,
3216 esurface_collection: &'a EsurfaceCollection,
3217 kinematic_point: LUCTKinematicPoint<T>,
3218 probe_rotation: &'a Rotation,
3219 cut_group_id: CutGroupId,
3220 shared_groups: &'a [(Arc<ThresholdSolveGroup<T>>, usize)],
3221}
3222
3223impl<'a, T: FloatLike> CounterTermBuilder<'a, T> {
3224 #[allow(clippy::too_many_arguments)]
3225 fn new(
3226 graph: &'a Graph,
3227 settings: &'a RuntimeSettings,
3228 esurface_collection: &'a EsurfaceCollection,
3229 kinematic_point: LUCTKinematicPoint<T>,
3230 overlap_structure: &'a OverlapStructure,
3231 masses: &'a EdgeVec<F<T>>,
3232 all_lmbs: &'a TiVec<LmbIndex, LoopMomentumBasis>,
3233 subspace: &'a SubspaceData,
3234 threshold_subspaces: Option<&'a [SubspaceData]>,
3235 probe_rotation: &'a Rotation,
3236 cut_group_id: CutGroupId,
3237 shared_groups: &'a [(Arc<ThresholdSolveGroup<T>>, usize)],
3238 ) -> Self {
3239 Self {
3240 real_mass_vector: masses,
3241 e_cm: F::from_f64(settings.kinematics.e_cm),
3242 graph,
3243 settings,
3244 esurface_collection,
3245 overlap_structure,
3246 kinematic_point,
3247 all_lmbs,
3248 subspace,
3249 threshold_subspaces,
3250 probe_rotation,
3251 cut_group_id,
3252 shared_groups,
3253 }
3254 }
3255
3256 fn threshold_subspace(&self, esurface_id: EsurfaceID) -> &SubspaceData {
3257 self.threshold_subspaces
3258 .map_or(self.subspace, |subspaces| &subspaces[esurface_id.0])
3259 }
3260
3261 fn new_overlap_builder(
3262 &'a self,
3263 group_index: usize,
3264 selected_esurface: EsurfaceID,
3265 ) -> OverlapBuilder<'a, T> {
3266 let subspace = self.threshold_subspace(selected_esurface);
3267 let (shared_group, shared_center) = &self.shared_groups[group_index];
3268 let transformed_kinematic_point = self.kinematic_point.lmb_transform(
3269 &self.graph.loop_momentum_basis,
3270 subspace.get_lmb(self.all_lmbs),
3271 );
3272 let mut center = transformed_kinematic_point
3275 .representative_sample()
3276 .loop_moms()
3277 .clone();
3278 for momentum in center.0.iter_mut() {
3279 *momentum = ThreeMomentum::new(self.e_cm.zero(), self.e_cm.zero(), self.e_cm.zero());
3280 }
3281 let shared_lmb = shared_group.subspace.get_lmb(self.all_lmbs);
3282 for target_index in subspace.iter_lmb_indices() {
3283 let edge = subspace.get_lmb(self.all_lmbs).loop_edges[target_index];
3284 let source_index = shared_lmb
3285 .loop_edges
3286 .iter_enumerated()
3287 .find_map(|(index, &candidate)| (candidate == edge).then_some(index))
3288 .expect("compatible solve group has the same defining edges");
3289 center[target_index] = shared_group.overlap.overlap_groups[*shared_center].center
3290 [source_index]
3291 .map(&|value| F(T::from_f64_exact_binary(value.0)))
3292 .rotate(self.probe_rotation);
3293 }
3294 let shifted_loop_momenta = transformed_kinematic_point
3295 .representative_sample()
3296 .loop_moms()
3297 - ¢er;
3298 let radius = shifted_loop_momenta
3299 .hyper_radius_squared(subspace.as_subspace_simple())
3300 .sqrt();
3301 OverlapBuilder {
3302 counterterm_builder: self,
3303 subspace,
3304 center,
3305 shifted_loop_momenta,
3306 radius,
3307 transformed_kinematic_point,
3308 shared_group,
3309 shared_center: *shared_center,
3310 }
3311 }
3312}
3313
3314struct OverlapBuilder<'a, T: FloatLike> {
3315 counterterm_builder: &'a CounterTermBuilder<'a, T>,
3316 subspace: &'a SubspaceData,
3317 center: LoopMomenta<F<T>>,
3320 shifted_loop_momenta: LoopMomenta<F<T>>,
3321 radius: F<T>,
3322 transformed_kinematic_point: LUCTKinematicPoint<T>,
3323 shared_group: &'a ThresholdSolveGroup<T>,
3324 shared_center: usize,
3325}
3326
3327impl<'a, T: FloatLike> OverlapBuilder<'a, T> {
3328 fn new_esurface_builder(
3329 &'a self,
3330 existing_esurface_id: ExistingEsurfaceId,
3331 ) -> EsurfaceCTBuilder<'a, T> {
3332 let esurface_id = self
3333 .counterterm_builder
3334 .overlap_structure
3335 .existing_esurfaces[existing_esurface_id];
3336
3337 EsurfaceCTBuilder {
3338 overlap_builder: self,
3339 _existing_esurface_id: existing_esurface_id,
3340 esurface: &self.counterterm_builder.esurface_collection[esurface_id],
3341 esurface_id,
3342 }
3343 }
3344}
3345
3346const MAX_ITERATIONS: usize = 40;
3347
3348struct EsurfaceCTBuilder<'a, T: FloatLike> {
3349 overlap_builder: &'a OverlapBuilder<'a, T>,
3350 _existing_esurface_id: ExistingEsurfaceId,
3351 esurface: &'a Esurface,
3352 esurface_id: EsurfaceID,
3353}
3354
3355impl<'a, T: FloatLike> EsurfaceCTBuilder<'a, T> {
3356 fn solve_rstar(
3357 self,
3358 rstar_t_dependence_evaluator: &mut RstarTDependenceEvaluator,
3359 radial_root_identity: &RadialRootIdentity,
3360 evaluation_metadata: &mut EvaluationMetaData,
3361 ) -> Option<RstarSolution<'a, T>> {
3362 let subspace = self.overlap_builder.subspace;
3363 let graph = self.overlap_builder.counterterm_builder.graph;
3364 let lmbs = self.overlap_builder.counterterm_builder.all_lmbs;
3365 let masses = self.overlap_builder.counterterm_builder.real_mass_vector;
3366
3367 debug!("subspace: {:?}", subspace);
3368
3369 let radius = &self.overlap_builder.radius;
3370 if radius.is_nan() || radius.is_infinite() || radius <= &radius.zero() {
3371 warn!(%radius, "threshold projection requires a finite positive active displacement");
3372 return None;
3373 }
3374 let representative_sample = self
3375 .overlap_builder
3376 .transformed_kinematic_point
3377 .representative_sample();
3378 let mut center_with_fixed_complement = representative_sample.loop_moms().clone();
3379 for loop_index in subspace.iter_lmb_indices() {
3380 center_with_fixed_complement[loop_index] =
3381 self.overlap_builder.center[loop_index].clone();
3382 }
3383
3384 let shared = self.overlap_builder.shared_group;
3385 let shared_lmb = shared.subspace.get_lmb(lmbs);
3386 let shared_center = &shared.overlap.overlap_groups[self.overlap_builder.shared_center];
3387 let center_surface_values = shared_center
3390 .existing_esurfaces
3391 .iter()
3392 .map(|&id| {
3393 let surface_id = shared.overlap.existing_esurfaces[id];
3394 let sample = &shared.kinematics[surface_id.0];
3395 let mut momenta = sample.loop_moms().clone();
3396 for index in shared.subspace.iter_lmb_indices() {
3397 momenta[index] = shared_center.center[index]
3398 .map(&|value| F(T::from_f64_exact_binary(value.0)))
3399 .rotate(self.overlap_builder.counterterm_builder.probe_rotation);
3400 }
3401 let value = shared.thresholds[surface_id].compute_from_momenta(
3402 shared_lmb,
3403 masses,
3404 &momenta,
3405 sample.external_moms(),
3406 );
3407 let valid = esurface_value_is_strictly_inside(
3408 &value,
3409 &self.overlap_builder.counterterm_builder.e_cm,
3410 );
3411 (surface_id, value, valid)
3412 })
3413 .collect_vec();
3414 let all_center_values_valid = center_surface_values
3415 .iter()
3416 .all(|(_, _, is_valid)| *is_valid);
3417
3418 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect, #center;
3419 stage = "lu_threshold_center_values",
3420 graph = %graph.name,
3421 selected_esurface_id = self.esurface_id.0,
3422 rotation_id = %self.overlap_builder.counterterm_builder.probe_rotation.method,
3423 center_provenance = "canonical_identity_center_promoted_and_rotated_natively",
3424 subspace_loop_indices = ?subspace.iter_lmb_indices().collect_vec(),
3425 file.active_center = %format!("{}", self.overlap_builder.center),
3426 file.center_with_fixed_complement = %format!("{}", center_with_fixed_complement),
3427 file.surface_values = %center_surface_values
3428 .iter()
3429 .map(|(esurface_id, value, is_valid)| format!(
3430 "local={} value={:+16e} inside={}",
3431 esurface_id.0,
3432 value,
3433 is_valid,
3434 ))
3435 .join("\n"),
3436 "LU threshold center values"
3437 );
3438
3439 if !all_center_values_valid {
3440 warn!(
3441 graph = %graph.name,
3442 selected_esurface_id = self.esurface_id.0,
3443 rotation_id = %self.overlap_builder.counterterm_builder.probe_rotation.method,
3444 center_provenance = "canonical_identity_center_promoted_and_rotated_natively",
3445 center = %center_with_fixed_complement,
3446 surface_values = %center_surface_values
3447 .iter()
3448 .map(|(esurface_id, value, is_valid)| format!(
3449 "local={} value={:+16e} inside={}",
3450 esurface_id.0,
3451 value,
3452 is_valid,
3453 ))
3454 .join("; "),
3455 "refusing to evaluate an LU threshold counterterm with an invalid probe-frame overlap center"
3456 );
3457 return None;
3458 }
3459
3460 let (raw_alpha_guess, _) = self.esurface.get_radius_guess_subspace(
3461 &self.overlap_builder.shifted_loop_momenta,
3462 self.overlap_builder
3463 .transformed_kinematic_point
3464 .representative_sample()
3465 .external_moms(),
3466 subspace,
3467 lmbs,
3468 graph,
3469 masses,
3470 );
3471
3472 let function = |alpha: &_| {
3473 self.esurface.compute_self_and_r_derivative_subspace(
3474 alpha,
3475 &self.overlap_builder.shifted_loop_momenta,
3476 &self.overlap_builder.center,
3477 self.overlap_builder
3478 .transformed_kinematic_point
3479 .representative_sample()
3480 .external_moms(),
3481 self.overlap_builder.counterterm_builder.real_mass_vector,
3482 subspace,
3483 lmbs,
3484 graph,
3485 )
3486 };
3487
3488 let zero = raw_alpha_guess.zero();
3489 let mut alpha_guess = raw_alpha_guess.clone();
3490 if alpha_guess.is_nan() || alpha_guess.is_infinite() || alpha_guess <= zero {
3491 alpha_guess = zero.one();
3494 }
3495
3496 debug!("initial alpha guess: {:?}", alpha_guess);
3497
3498 let tolerance = F::from_f64(
3502 self.overlap_builder
3503 .counterterm_builder
3504 .settings
3505 .subtraction
3506 .radial_root_residual_tolerance,
3507 );
3508 let mut solution = match evaluation_metadata.radial_root_diagnostics.solve(
3509 radial_root_identity,
3510 &zero,
3511 &alpha_guess,
3512 function,
3513 &tolerance,
3514 MAX_ITERATIONS,
3515 64,
3516 &self.overlap_builder.counterterm_builder.e_cm,
3517 ) {
3518 Ok(solution) => solution,
3519 Err(error) => {
3520 warn!(
3521 graph = %graph.name,
3522 esurface_id = self.esurface_id.0,
3523 rotation_id = %self.overlap_builder.counterterm_builder.probe_rotation.method,
3524 center_provenance = "canonical_identity_center_promoted_and_rotated_natively",
3525 raw_alpha_guess = %raw_alpha_guess,
3526 alpha_guess = %alpha_guess,
3527 error = %error,
3528 "refusing to evaluate a threshold counterterm with an invalid alpha solution"
3529 );
3530 return None;
3531 }
3532 };
3533
3534 debug!("alpha solution: {:?}", solution);
3535 let alpha = solution.solution.clone();
3536 solution.solution *= radius;
3539 solution.derivative_at_solution /= radius;
3540 if [&solution.solution, &solution.derivative_at_solution]
3541 .iter()
3542 .any(|value| value.is_nan() || value.is_infinite() || **value <= zero)
3543 {
3544 warn!(%alpha, %radius, "threshold radial solution is not representable");
3545 return None;
3546 }
3547
3548 let alpha_t_dependence = if rstar_t_dependence_evaluator.supports_t_derivatives() {
3549 let t_star = match &self
3550 .overlap_builder
3551 .transformed_kinematic_point
3552 .non_dual_cut_params()
3553 .tstar
3554 {
3555 DualOrNot::NonDual(t_star) => t_star.clone(),
3556 DualOrNot::Dual(_) => {
3557 unreachable!("representative LU cut parameters must stay non-dual")
3558 }
3559 };
3560
3561 Some(
3562 rstar_t_dependence_evaluator.evaluate_alpha(
3563 RstarTDependenceInput {
3564 t_star: &t_star,
3565 alpha: &alpha,
3566 overlap_center: &self.overlap_builder.center,
3567 subspace,
3568 unrescaled_momentum_sample: self
3569 .overlap_builder
3570 .transformed_kinematic_point
3571 .unrescaled_sample(),
3572 representative_sample,
3573 masses,
3574 threshold_esurface: self.esurface,
3575 lmb: &self
3576 .overlap_builder
3577 .counterterm_builder
3578 .graph
3579 .loop_momentum_basis,
3580 all_lmbs: lmbs,
3581 },
3582 evaluation_metadata,
3583 ),
3584 )
3585 } else {
3586 None
3587 };
3588
3589 Some(RstarSolution {
3590 esurface_ct_builder: self,
3591 solution,
3592 alpha,
3593 alpha_t_dependence,
3594 })
3595 }
3596}
3597
3598struct RstarSolution<'a, T: FloatLike> {
3599 esurface_ct_builder: EsurfaceCTBuilder<'a, T>,
3600 solution: NewtonIterationResult<T>,
3601 alpha: F<T>,
3603 alpha_t_dependence: Option<HyperDual<F<T>>>,
3604}
3605
3606struct DualRstarGeometry<T: FloatLike> {
3607 radius: HyperDual<F<T>>,
3608 radius_star: HyperDual<F<T>>,
3609 esurface_derivative: HyperDual<F<T>>,
3610 rstar_loop_momenta: LoopMomenta<HyperDual<F<T>>>,
3611 external_moms: ExternalFourMomenta<HyperDual<F<T>>>,
3612}
3613
3614impl<'a, T: FloatLike> RstarSolution<'a, T> {
3615 fn subspace(&self) -> &SubspaceData {
3616 self.esurface_ct_builder.overlap_builder.subspace
3617 }
3618
3619 fn max_supported_order(&self) -> usize {
3620 self.alpha_t_dependence
3621 .as_ref()
3622 .map(|dual| dual.values.len().saturating_sub(1))
3623 .unwrap_or(0)
3624 }
3625
3626 fn base_rstar_loop_momenta(&self) -> LoopMomenta<F<T>> {
3627 let subspace = self.subspace();
3628
3629 &self
3630 .esurface_ct_builder
3631 .overlap_builder
3632 .shifted_loop_momenta
3633 .rescale(&self.alpha, subspace.as_subspace_simple())
3634 + &self.esurface_ct_builder.overlap_builder.center
3635 }
3636
3637 fn truncated_alpha_solution(&self, order: usize) -> HyperDual<F<T>> {
3638 let dual_solution = self
3639 .alpha_t_dependence
3640 .as_ref()
3641 .expect("higher-order LU threshold evaluation requires cached alpha(t)");
3642 HyperDual::from_values(
3643 simple_n_deriv_shape(order),
3644 dual_solution.values[..=order].to_vec(),
3645 )
3646 }
3647
3648 fn embedded_truncated_alpha_solution(&self, cut_cff_index: &CutCFFIndex) -> HyperDual<F<T>> {
3649 let lu_order = cut_cff_index
3650 .lu_cut_order
3651 .expect("mixed LU geometry requires lu_cut_order")
3652 - 1;
3653 let target_shape = shape_from_cut_cff_index(cut_cff_index)
3654 .map(HyperDual::new)
3655 .expect("mixed LU geometry requires a dual shape");
3656 let t_variable = variable_indices_from_cut_cff_index(cut_cff_index).lu_cut;
3657 let t_dual = if lu_order == 0 {
3658 HyperDual::from_values(simple_n_deriv_shape(0), vec![self.alpha.clone()])
3659 } else {
3660 self.truncated_alpha_solution(lu_order)
3661 };
3662
3663 embed_t_dual_in_target_shape(&t_dual, &target_shape, t_variable)
3664 }
3665
3666 fn dual_geometry_for_order(&self, order: usize) -> DualRstarGeometry<T> {
3667 let source_sample = self
3668 .esurface_ct_builder
3669 .overlap_builder
3670 .transformed_kinematic_point
3671 .sample_for_order(order);
3672 let dual_loop_momenta = source_sample
3673 .sample
3674 .dual_loop_moms
3675 .as_ref()
3676 .expect("higher-order LU threshold evaluation requires dual loop momenta")
3677 .clone();
3678
3679 self.dual_geometry(
3680 dual_loop_momenta,
3681 self.truncated_alpha_solution(order),
3682 source_sample.external_moms(),
3683 None,
3684 )
3685 }
3686
3687 fn dual_geometry_for_cut_cff_index(
3688 &self,
3689 cut_cff_index: &CutCFFIndex,
3690 threshold_variable: Option<usize>,
3691 ) -> DualRstarGeometry<T> {
3692 let lu_order = cut_cff_index
3693 .lu_cut_order
3694 .expect("mixed LU geometry requires lu_cut_order")
3695 - 1;
3696 let source_sample = self
3697 .esurface_ct_builder
3698 .overlap_builder
3699 .transformed_kinematic_point
3700 .sample_for_order(lu_order);
3701 let target_shape = shape_from_cut_cff_index(cut_cff_index)
3702 .map(HyperDual::new)
3703 .expect("mixed LU geometry requires a dual shape");
3704 let t_variable = variable_indices_from_cut_cff_index(cut_cff_index).lu_cut;
3705 let dual_loop_momenta = embedded_dual_loop_momenta_for_cut_cff_index(
3706 source_sample,
3707 &Some(target_shape.clone()),
3708 t_variable,
3709 )
3710 .expect("mixed LU geometry requires dual loop momenta");
3711
3712 self.dual_geometry(
3713 dual_loop_momenta,
3714 self.embedded_truncated_alpha_solution(cut_cff_index),
3715 source_sample.external_moms(),
3716 threshold_variable,
3717 )
3718 }
3719
3720 fn dual_geometry(
3721 &self,
3722 mut dual_loop_momenta: LoopMomenta<HyperDual<F<T>>>,
3723 mut alpha: HyperDual<F<T>>,
3724 source_externals: &ExternalFourMomenta<F<T>>,
3725 threshold_variable: Option<usize>,
3726 ) -> DualRstarGeometry<T> {
3727 let representative = self
3728 .esurface_ct_builder
3729 .overlap_builder
3730 .transformed_kinematic_point
3731 .representative_sample();
3732 for (momentum, base) in dual_loop_momenta
3735 .0
3736 .iter_mut()
3737 .zip(representative.loop_moms().iter())
3738 {
3739 momentum.px.values[0] = base.px.clone();
3740 momentum.py.values[0] = base.py.clone();
3741 momentum.pz.values[0] = base.pz.clone();
3742 }
3743 let center = dualize_loop_momenta(&alpha, &self.esurface_ct_builder.overlap_builder.center);
3744 let external_moms = dualize_external_momenta(&alpha, source_externals);
3745 let shifted_momenta = dual_loop_momenta
3746 .iter()
3747 .zip(center.iter())
3748 .map(|(momentum, center)| momentum.clone() - center.clone())
3749 .collect::<LoopMomenta<_>>();
3750 let radius = dual_shifted_radius(&shifted_momenta, self.subspace());
3751 let mut radius_star = alpha.clone() * &radius;
3752 if threshold_variable.is_some() {
3755 let mut radial_variation = new_constant(&alpha, &self.alpha.zero());
3756 activate_threshold_variable_in_target_shape(&mut radial_variation, threshold_variable);
3757 radius_star += &radial_variation;
3758 alpha += radial_variation / &radius;
3759 }
3760 let rstar_loop_momenta = shifted_momenta
3761 .rescale(&alpha, self.subspace().as_subspace_simple())
3762 .iter()
3763 .zip(center.iter())
3764 .map(|(momentum, center)| momentum.clone() + center.clone())
3765 .collect();
3766 let (_, alpha_derivative) = compute_self_and_r_derivative_subspace_dual(
3767 self.esurface_ct_builder.esurface,
3768 &alpha,
3769 &shifted_momenta,
3770 ¢er,
3771 &external_moms,
3772 self.esurface_ct_builder
3773 .overlap_builder
3774 .counterterm_builder
3775 .real_mass_vector,
3776 self.subspace(),
3777 self.esurface_ct_builder
3778 .overlap_builder
3779 .counterterm_builder
3780 .all_lmbs,
3781 self.esurface_ct_builder
3782 .overlap_builder
3783 .counterterm_builder
3784 .graph,
3785 );
3786 let esurface_derivative = alpha_derivative / &radius;
3787
3788 DualRstarGeometry {
3789 radius,
3790 radius_star,
3791 esurface_derivative,
3792 rstar_loop_momenta,
3793 external_moms,
3794 }
3795 }
3796
3797 fn non_dual_multichanneling_factor(&self, rstar_sample: &MomentumSample<T>) -> Complex<F<T>> {
3798 let builder = self.esurface_ct_builder.overlap_builder;
3799 let group = builder.shared_group;
3800 let native_lmb = self
3801 .subspace()
3802 .get_lmb(builder.counterterm_builder.all_lmbs);
3803 let group_lmb = group.subspace.get_lmb(builder.counterterm_builder.all_lmbs);
3804 let active_sample = rstar_sample.lmb_transform(native_lmb, group_lmb);
3805 let values: Vec<_> = group
3806 .thresholds
3807 .iter_enumerated()
3808 .map(|(id, surface)| {
3809 let mut momenta = group.kinematics[id.0].loop_moms().clone();
3810 for index in group.subspace.iter_lmb_indices() {
3811 momenta[index] = active_sample.loop_moms()[index].clone();
3812 }
3813 surface
3814 .compute_from_momenta(
3815 group_lmb,
3816 builder.counterterm_builder.real_mass_vector,
3817 &momenta,
3818 group.kinematics[id.0].external_moms(),
3819 )
3820 .powi(group.prefactor_power as i32)
3821 })
3822 .collect();
3823 let weights = group
3824 .overlap
3825 .overlap_groups
3826 .iter()
3827 .map(|center| {
3828 center
3829 .complement
3830 .iter()
3831 .map(|id| &values[group.overlap.existing_esurfaces[*id].0])
3832 .fold(rstar_sample.one(), |weight, value| weight * value)
3833 })
3834 .collect_vec();
3835 let denominator = weights
3836 .iter()
3837 .fold(rstar_sample.zero(), |sum, value| sum + value);
3838 Complex::new_re(weights[builder.shared_center].clone() / denominator)
3839 }
3840
3841 fn dual_multichanneling_factor(&self, geometry: &DualRstarGeometry<T>) -> HyperDual<F<T>> {
3842 let builder = self.esurface_ct_builder.overlap_builder;
3843 let group = builder.shared_group;
3844 let native_lmb = self
3845 .subspace()
3846 .get_lmb(builder.counterterm_builder.all_lmbs);
3847 let group_lmb = group.subspace.get_lmb(builder.counterterm_builder.all_lmbs);
3848 let external_spatial = geometry
3849 .external_moms
3850 .iter()
3851 .map(|p| p.spatial.clone())
3852 .collect();
3853 let current =
3854 geometry
3855 .rstar_loop_momenta
3856 .lmb_transform(native_lmb, group_lmb, &external_spatial);
3857 let values = group
3858 .thresholds
3859 .iter_enumerated()
3860 .map(|(id, surface)| {
3861 let sample = &group.kinematics[id.0];
3862 let (mut momenta, externals) = if group.records[id.0].cut_group_id
3866 == builder.counterterm_builder.cut_group_id
3867 {
3868 (current.clone(), geometry.external_moms.clone())
3869 } else {
3870 (
3871 dualize_loop_momenta(&geometry.radius, sample.loop_moms()),
3872 dualize_external_momenta(&geometry.radius, sample.external_moms()),
3873 )
3874 };
3875 for index in group.subspace.iter_lmb_indices() {
3876 momenta[index] = current[index].clone();
3877 }
3878 let value = surface.compute_from_dual_momenta(
3879 group_lmb,
3880 builder.counterterm_builder.real_mass_vector,
3881 &momenta,
3882 &externals,
3883 );
3884 (0..group.prefactor_power).fold(
3885 new_constant(&value, &value.values[0].one()),
3886 |product, _| product * &value,
3887 )
3888 })
3889 .collect_vec();
3890 let one = new_constant(&geometry.radius, &geometry.radius.values[0].one());
3891 let zero = new_constant(&geometry.radius, &geometry.radius.values[0].zero());
3892 let weights = group
3893 .overlap
3894 .overlap_groups
3895 .iter()
3896 .map(|center| {
3897 center
3898 .complement
3899 .iter()
3900 .map(|id| &values[group.overlap.existing_esurfaces[*id].0])
3901 .fold(one.clone(), |weight, value| weight * value)
3902 })
3903 .collect_vec();
3904 let denominator = weights.iter().fold(zero, |sum, value| sum + value);
3905 weights[builder.shared_center].clone() / denominator
3906 }
3907
3908 fn rstar_sample_for_order<'solution>(
3909 &'solution self,
3910 order: usize,
3911 ) -> RstarSample<'solution, 'a, T> {
3912 let base_rstar_loop_momenta = self.base_rstar_loop_momenta();
3913
3914 if order == 0 {
3915 let mut rstar_sample = self
3916 .esurface_ct_builder
3917 .overlap_builder
3918 .transformed_kinematic_point
3919 .representative_sample()
3920 .clone();
3921 rstar_sample.sample.loop_moms = base_rstar_loop_momenta;
3922 rstar_sample.sample.dual_loop_moms = None;
3923
3924 let radius = self.esurface_ct_builder.overlap_builder.radius.clone();
3925 let radius_star = self.solution.solution.clone();
3926 let esurface_derivative = self.solution.derivative_at_solution.clone();
3927 let e_cm = &self
3928 .esurface_ct_builder
3929 .overlap_builder
3930 .counterterm_builder
3931 .e_cm;
3932 let uv_localisation_settings = &self
3933 .esurface_ct_builder
3934 .overlap_builder
3935 .counterterm_builder
3936 .settings
3937 .subtraction
3938 .local_ct_settings
3939 .uv_localisation;
3940
3941 let threshold_params = ThresholdParams {
3942 radius: DualOrNot::NonDual(radius.clone()),
3943 radius_star: DualOrNot::NonDual(radius_star.clone()),
3944 esurface_derivative: DualOrNot::NonDual(esurface_derivative.clone()),
3945 uv_damp_plus: DualOrNot::NonDual(evaluate_uv_damper(
3946 &radius,
3947 &radius_star,
3948 e_cm,
3949 uv_localisation_settings,
3950 )),
3951 uv_damp_minus: DualOrNot::NonDual(evaluate_uv_damper(
3952 &-radius.clone(),
3953 &radius_star,
3954 e_cm,
3955 uv_localisation_settings,
3956 )),
3957 h_function: DualOrNot::NonDual(evaluate_integrated_ct_normalisation(
3958 &radius,
3959 &radius_star,
3960 e_cm,
3961 &self
3962 .esurface_ct_builder
3963 .overlap_builder
3964 .counterterm_builder
3965 .settings
3966 .subtraction
3967 .integrated_ct_settings,
3968 )),
3969 };
3970
3971 let value_of_multi_channeling_factor =
3972 DualOrNot::NonDual(self.non_dual_multichanneling_factor(&rstar_sample));
3973
3974 return RstarSample {
3975 rstar_solution: self,
3976 rstar_sample,
3977 threshold_params,
3978 value_of_multi_channeling_factor,
3979 };
3980 }
3981
3982 assert!(
3983 order <= self.max_supported_order(),
3984 "requested LU threshold derivative order {} but only {} orders are cached",
3985 order,
3986 self.max_supported_order()
3987 );
3988
3989 let geometry = self.dual_geometry_for_order(order);
3990 let mut rstar_sample = self
3991 .esurface_ct_builder
3992 .overlap_builder
3993 .transformed_kinematic_point
3994 .sample_for_order(order)
3995 .clone();
3996 rstar_sample.sample.loop_moms = base_rstar_loop_momenta;
3997 rstar_sample.sample.dual_loop_moms = Some(geometry.rstar_loop_momenta.clone());
3998
3999 let e_cm = &self
4000 .esurface_ct_builder
4001 .overlap_builder
4002 .counterterm_builder
4003 .e_cm;
4004 let uv_localisation_settings = &self
4005 .esurface_ct_builder
4006 .overlap_builder
4007 .counterterm_builder
4008 .settings
4009 .subtraction
4010 .local_ct_settings
4011 .uv_localisation;
4012
4013 let threshold_params = ThresholdParams {
4014 radius: DualOrNot::Dual(geometry.radius.clone()),
4015 radius_star: DualOrNot::Dual(geometry.radius_star.clone()),
4016 esurface_derivative: DualOrNot::Dual(geometry.esurface_derivative.clone()),
4017 uv_damp_plus: DualOrNot::Dual(evaluate_uv_damper_dual(
4018 &geometry.radius,
4019 &geometry.radius_star,
4020 e_cm,
4021 uv_localisation_settings,
4022 )),
4023 uv_damp_minus: DualOrNot::Dual(evaluate_uv_damper_dual(
4024 &-geometry.radius.clone(),
4025 &geometry.radius_star,
4026 e_cm,
4027 uv_localisation_settings,
4028 )),
4029 h_function: DualOrNot::Dual(evaluate_integrated_ct_normalisation_dual(
4030 &geometry.radius,
4031 &geometry.radius_star,
4032 e_cm,
4033 &self
4034 .esurface_ct_builder
4035 .overlap_builder
4036 .counterterm_builder
4037 .settings
4038 .subtraction
4039 .integrated_ct_settings,
4040 )),
4041 };
4042
4043 let value_of_multi_channeling_factor = DualOrNot::Dual(real_dual_to_complex(
4044 self.dual_multichanneling_factor(&geometry),
4045 ));
4046
4047 RstarSample {
4048 rstar_solution: self,
4049 rstar_sample,
4050 threshold_params,
4051 value_of_multi_channeling_factor,
4052 }
4053 }
4054
4055 fn rstar_sample_for_cut_cff_index<'solution>(
4056 &'solution self,
4057 cut_cff_index: &CutCFFIndex,
4058 threshold_variable: Option<usize>,
4059 ) -> RstarSample<'solution, 'a, T> {
4060 let lu_order = cut_cff_index
4061 .lu_cut_order
4062 .expect("LU threshold sampling requires lu_cut_order")
4063 - 1;
4064
4065 if shape_from_cut_cff_index(cut_cff_index).is_none() {
4066 return self.rstar_sample_for_order(lu_order);
4067 }
4068
4069 let base_rstar_loop_momenta = self.base_rstar_loop_momenta();
4070 let geometry = self.dual_geometry_for_cut_cff_index(cut_cff_index, threshold_variable);
4071 let mut rstar_sample = self
4072 .esurface_ct_builder
4073 .overlap_builder
4074 .transformed_kinematic_point
4075 .sample_for_order(lu_order)
4076 .clone();
4077 rstar_sample.sample.loop_moms = base_rstar_loop_momenta;
4078 rstar_sample.sample.dual_loop_moms = Some(geometry.rstar_loop_momenta.clone());
4079
4080 let e_cm = &self
4081 .esurface_ct_builder
4082 .overlap_builder
4083 .counterterm_builder
4084 .e_cm;
4085 let uv_localisation_settings = &self
4086 .esurface_ct_builder
4087 .overlap_builder
4088 .counterterm_builder
4089 .settings
4090 .subtraction
4091 .local_ct_settings
4092 .uv_localisation;
4093
4094 let threshold_params = ThresholdParams {
4095 radius: DualOrNot::Dual(geometry.radius.clone()),
4096 radius_star: DualOrNot::Dual(geometry.radius_star.clone()),
4097 esurface_derivative: DualOrNot::Dual(geometry.esurface_derivative.clone()),
4098 uv_damp_plus: DualOrNot::Dual(evaluate_uv_damper_dual(
4099 &geometry.radius,
4100 &geometry.radius_star,
4101 e_cm,
4102 uv_localisation_settings,
4103 )),
4104 uv_damp_minus: DualOrNot::Dual(evaluate_uv_damper_dual(
4105 &-geometry.radius.clone(),
4106 &geometry.radius_star,
4107 e_cm,
4108 uv_localisation_settings,
4109 )),
4110 h_function: DualOrNot::Dual(evaluate_integrated_ct_normalisation_dual(
4111 &geometry.radius,
4112 &geometry.radius_star,
4113 e_cm,
4114 &self
4115 .esurface_ct_builder
4116 .overlap_builder
4117 .counterterm_builder
4118 .settings
4119 .subtraction
4120 .integrated_ct_settings,
4121 )),
4122 };
4123
4124 let value_of_multi_channeling_factor = DualOrNot::Dual(real_dual_to_complex(
4125 self.dual_multichanneling_factor(&geometry),
4126 ));
4127
4128 RstarSample {
4129 rstar_solution: self,
4130 rstar_sample,
4131 threshold_params,
4132 value_of_multi_channeling_factor,
4133 }
4134 }
4135}
4136
4137struct RstarSample<'solution, 'a, T: FloatLike> {
4138 rstar_solution: &'solution RstarSolution<'a, T>,
4139 rstar_sample: MomentumSample<T>,
4140 threshold_params: ThresholdParams<T>,
4141 value_of_multi_channeling_factor: DualOrNot<Complex<F<T>>>,
4142}
4143
4144impl<'solution, 'a, T: FloatLike> RstarSample<'solution, 'a, T> {
4145 fn extract_threshold_parameters(&self, is_first_call: bool) -> ThresholdParams<T> {
4146 if is_first_call {
4147 let edges_in_esurface = self
4148 .rstar_solution
4149 .esurface_ct_builder
4150 .esurface
4151 .energies
4152 .iter()
4153 .map(|i| {
4154 self.rstar_solution
4155 .esurface_ct_builder
4156 .overlap_builder
4157 .counterterm_builder
4158 .graph[i]
4159 .0
4160 .name
4161 .clone()
4162 })
4163 .collect_vec();
4164
4165 debug!("esurface_id: {}", self.get_esurface_id().0);
4166 debug!("edges in esurface: {:?}", edges_in_esurface);
4167 debug!("radius: {}", self.threshold_params.radius);
4168 debug!("radius_star: {}", self.threshold_params.radius_star);
4169 debug!(
4170 "esurface_derivative: {}",
4171 self.threshold_params.esurface_derivative
4172 );
4173 debug!("uv_damp_plus: {}", self.threshold_params.uv_damp_plus);
4174 debug!("uv_damp_minus: {}", self.threshold_params.uv_damp_minus);
4175 debug!("h_function: {}", self.threshold_params.h_function);
4176 debug!(
4177 "value of multi-channeling factor: {}",
4178 self.value_of_multi_channeling_factor
4179 );
4180 }
4181
4182 self.threshold_params.clone()
4183 }
4184
4185 fn get_inverse_transformed_sample(&self) -> MomentumSample<T> {
4186 let subspace = self.rstar_solution.subspace();
4187 let current_lmb = subspace.get_lmb(
4188 self.rstar_solution
4189 .esurface_ct_builder
4190 .overlap_builder
4191 .counterterm_builder
4192 .all_lmbs,
4193 );
4194 let target_lmb = &self
4195 .rstar_solution
4196 .esurface_ct_builder
4197 .overlap_builder
4198 .counterterm_builder
4199 .graph
4200 .loop_momentum_basis;
4201
4202 self.rstar_sample.lmb_transform(current_lmb, target_lmb)
4203 }
4204
4205 fn get_esurface_id(&self) -> EsurfaceID {
4206 self.rstar_solution.esurface_ct_builder.esurface_id
4207 }
4208}
4209
4210fn merge_and_inverse_transform<T: FloatLike>(
4211 left_sample: &RstarSample<'_, '_, T>,
4212 right_sample: &RstarSample<'_, '_, T>,
4213 cut_cff_index: &CutCFFIndex,
4214) -> MomentumSample<T> {
4215 let left_builder = left_sample
4216 .rstar_solution
4217 .esurface_ct_builder
4218 .overlap_builder;
4219 let lmbs = left_builder.counterterm_builder.all_lmbs;
4220 let left_subspace = left_sample.rstar_solution.subspace();
4221 let right_subspace = right_sample.rstar_solution.subspace();
4222 assert!(
4223 left_subspace.is_mergable_with(right_subspace, lmbs),
4224 "incompatible physical subspaces for merging samples"
4225 );
4226 let graph_lmb = &left_builder.counterterm_builder.graph.loop_momentum_basis;
4227 let mut merged = left_sample.get_inverse_transformed_sample();
4228 let right = right_sample.get_inverse_transformed_sample();
4229 let base = left_builder
4230 .transformed_kinematic_point
4231 .representative_sample()
4232 .lmb_transform(left_subspace.get_lmb(lmbs), graph_lmb);
4233 for index in (0..merged.loop_moms().0.len()).map(LoopIndex::from) {
4236 merged.sample.loop_moms[index] +=
4237 &right.sample.loop_moms[index] - &base.sample.loop_moms[index];
4238 }
4239 match (
4240 merged.sample.dual_loop_moms.as_mut(),
4241 right.sample.dual_loop_moms.as_ref(),
4242 ) {
4243 (Some(merged_duals), Some(right_duals)) => {
4244 let source = &left_builder.transformed_kinematic_point;
4245 let order = cut_cff_index.lu_cut_order.expect("iterated LU order") - 1;
4246 let base_sample = source.sample_for_order(order);
4247 let target_shape = shape_from_cut_cff_index(cut_cff_index).map(HyperDual::new);
4248 let native_base = embedded_dual_loop_momenta_for_cut_cff_index(
4249 base_sample,
4250 &target_shape,
4251 variable_indices_from_cut_cff_index(cut_cff_index).lu_cut,
4252 )
4253 .expect("iterated dual base matches the residue shape");
4254 let external = dualize_external_momenta(
4255 &merged_duals[LoopIndex::from(0)].px,
4256 base_sample.external_moms(),
4257 );
4258 let base_duals = native_base.lmb_transform(
4259 left_subspace.get_lmb(lmbs),
4260 graph_lmb,
4261 &external.iter().map(|p| p.spatial.clone()).collect(),
4262 );
4263 for index in (0..merged_duals.0.len()).map(LoopIndex::from) {
4264 merged_duals[index] = merged_duals[index].clone() + right_duals[index].clone()
4265 - base_duals[index].clone();
4266 }
4267 }
4268 (None, None) => {}
4269 _ => unreachable!("iterated LU samples must carry matching dual shapes"),
4270 }
4271 merged
4272}
4273
4274#[cfg(test)]
4275#[path = "lu_counterterm_group_tests.rs"]
4276mod group_tests;
4277
4278#[cfg(test)]
4279mod tests {
4280 use super::{
4281 IteratedMultiplierCoefficients, IteratedMultiplierComponent, LUCounterTerm,
4282 PairMultiplierCoefficient, PendingLUCountertermComponent, SingleMultiplierCoefficients,
4283 SingleMultiplierComponent, extend_extracted_eta_params,
4284 extract_zero_threshold_coefficient_t_dual, iterated_multiplier_context,
4285 single_multiplier_context, weight_iterated_multiplier_pieces,
4286 weight_single_multiplier_pieces,
4287 };
4288 use crate::cff::CutCFFIndex;
4289 use crate::observables::events::ThresholdCountertermComponentOccurrence;
4290 use crate::processes::{CutGroupId, IteratedThresholdPieces, SingleThresholdPieces};
4291 use crate::utils::{
4292 ArbPrec, F, FloatLike, f128,
4293 hyperdual_utils::{DualOrNot, new_from_values, shape_from_cut_cff_index},
4294 };
4295 use spenso::algebra::complex::Complex;
4296 use symbolica::domains::dual::HyperDual;
4297 use typed_index_collections::{TiVec, ti_vec};
4298
4299 #[test]
4300 fn inactive_cut_group_guard_reports_runtime_access() {
4301 let counterterm = LUCounterTerm {
4302 evaluators: TiVec::new(),
4303 thresholds: TiVec::new(),
4304 subspaces: TiVec::new(),
4305 variant_subspaces: None,
4306 metadata_registry: None,
4307 rstar_dependence_calculator: TiVec::new(),
4308 active_cut_groups: ti_vec![false],
4309 active_left_thresholds: ti_vec![TiVec::new()],
4310 active_right_thresholds: ti_vec![TiVec::new()],
4311 active_iterated_thresholds: TiVec::new(),
4312 };
4313
4314 let error = counterterm
4315 .ensure_active_cut_group(CutGroupId(0))
4316 .unwrap_err();
4317 assert!(error.to_string().contains("generation marked it inactive"));
4318 }
4319
4320 #[test]
4321 fn zero_threshold_coefficient_lookup_is_explicit() {
4322 let shape = HyperDual::new(
4323 shape_from_cut_cff_index(&CutCFFIndex {
4324 left_threshold_order: Some(2),
4325 right_threshold_order: Some(2),
4326 lu_cut_order: Some(2),
4327 })
4328 .unwrap(),
4329 );
4330 let mixed_dual = new_from_values(
4331 &shape,
4332 &[
4333 F(10.0_f64),
4334 F(11.0_f64),
4335 F(20.0_f64),
4336 F(30.0_f64),
4337 F(40.0_f64),
4338 F(21.0_f64),
4339 F(31.0_f64),
4340 F(41.0_f64),
4341 ],
4342 );
4343
4344 let zero_threshold = extract_zero_threshold_coefficient_t_dual(&mixed_dual, 0);
4345
4346 assert_eq!(zero_threshold.values, vec![F(10.0_f64), F(11.0_f64)]);
4347 }
4348
4349 #[test]
4350 fn iterated_eta_parameters_project_onto_their_own_threshold_axis() {
4351 let cut_cff_index = CutCFFIndex {
4352 left_threshold_order: Some(1),
4353 right_threshold_order: Some(2),
4354 lu_cut_order: Some(1),
4355 };
4356 let shape = HyperDual::new(shape_from_cut_cff_index(&cut_cff_index).unwrap());
4357 let mixed_eta = DualOrNot::Dual(new_from_values(&shape, &[F(10.0_f64), F(20.0_f64)]));
4358
4359 let mut left_params = Vec::new();
4360 extend_extracted_eta_params(&mut left_params, &mixed_eta, None, None);
4361 assert_eq!(
4362 left_params,
4363 vec![Complex::new_re(F(10.0_f64))],
4364 "an order-one left eta must take the zero coefficient of the right threshold axis",
4365 );
4366
4367 let mut right_params = Vec::new();
4368 extend_extracted_eta_params(&mut right_params, &mixed_eta, None, Some(0));
4369 assert_eq!(
4370 right_params,
4371 vec![Complex::new_re(F(10.0_f64)), Complex::new_re(F(20.0_f64)),],
4372 "the right eta must retain derivatives along its own threshold axis",
4373 );
4374 }
4375
4376 #[test]
4377 fn multiplier_contexts_follow_expanded_term_semantics() {
4378 let base = "base";
4379 let left_root = "left_root";
4380 let right_root = "right_root";
4381 let merged_root = "merged_root";
4382
4383 assert_eq!(
4384 single_multiplier_context(SingleMultiplierComponent::Local, &base, &left_root),
4385 (&base, &left_root),
4386 );
4387 assert_eq!(
4388 single_multiplier_context(SingleMultiplierComponent::Integrated, &base, &left_root),
4389 (&left_root, &left_root),
4390 );
4391 assert_eq!(
4392 iterated_multiplier_context(
4393 IteratedMultiplierComponent::LocalLocal,
4394 &base,
4395 &left_root,
4396 &right_root,
4397 &merged_root,
4398 ),
4399 (&base, &merged_root),
4400 );
4401 assert_eq!(
4402 iterated_multiplier_context(
4403 IteratedMultiplierComponent::IntegratedLocal,
4404 &base,
4405 &left_root,
4406 &right_root,
4407 &merged_root,
4408 ),
4409 (&left_root, &merged_root),
4410 );
4411 assert_eq!(
4412 iterated_multiplier_context(
4413 IteratedMultiplierComponent::LocalIntegrated,
4414 &base,
4415 &left_root,
4416 &right_root,
4417 &merged_root,
4418 ),
4419 (&right_root, &merged_root),
4420 );
4421 assert_eq!(
4422 iterated_multiplier_context(
4423 IteratedMultiplierComponent::IntegratedIntegrated,
4424 &base,
4425 &left_root,
4426 &right_root,
4427 &merged_root,
4428 ),
4429 (&merged_root, &merged_root),
4430 );
4431
4432 let left_only_terms = 2;
4435 let right_only_terms = 2;
4436 let paired_terms = 4;
4437 assert_eq!(1 + left_only_terms + right_only_terms + paired_terms, 9);
4438 assert_eq!((1 + left_only_terms) * (1 + right_only_terms), 9);
4439 }
4440
4441 #[test]
4442 fn zero_multiplier_coefficients_skip_unused_pieces() {
4443 let zero = F(0.0_f64);
4444 let single = weight_single_multiplier_pieces(
4445 SingleThresholdPieces {
4446 local: DualOrNot::NonDual(Complex::new_re(F(f64::NAN))),
4447 integrated: DualOrNot::NonDual(Complex::new_re(F(3.0))),
4448 },
4449 &SingleMultiplierCoefficients {
4450 local: F(0.0),
4451 integrated: F(5.0),
4452 },
4453 0,
4454 &zero,
4455 )
4456 .unwrap_real();
4457 assert_eq!(single, Complex::new_re(F(15.0)));
4458
4459 let iterated = weight_iterated_multiplier_pieces(
4460 IteratedThresholdPieces {
4461 local_local: DualOrNot::NonDual(Complex::new_re(F(1.0))),
4462 local_integrated: DualOrNot::NonDual(Complex::new_re(F(f64::NAN))),
4463 integrated_local: DualOrNot::NonDual(Complex::new_re(F(100.0))),
4464 integrated_integrated: DualOrNot::NonDual(Complex::new_re(F(1000.0))),
4465 },
4466 &IteratedMultiplierCoefficients {
4467 local_local: PairMultiplierCoefficient {
4468 left: F(2.0),
4469 right: F(1.0),
4470 },
4471 local_integrated: PairMultiplierCoefficient {
4472 left: F(0.0),
4473 right: F(1.0),
4474 },
4475 integrated_local: PairMultiplierCoefficient {
4476 left: F(5.0),
4477 right: F(1.0),
4478 },
4479 integrated_integrated: PairMultiplierCoefficient {
4480 left: F(7.0),
4481 right: F(1.0),
4482 },
4483 },
4484 0,
4485 &zero,
4486 )
4487 .unwrap_real();
4488 assert_eq!(iterated, Complex::new_re(F(7502.0)));
4489
4490 assert!(
4491 SingleMultiplierCoefficients {
4492 local: zero,
4493 integrated: zero,
4494 }
4495 .all_zero()
4496 );
4497 assert!(
4498 IteratedMultiplierCoefficients {
4499 local_local: PairMultiplierCoefficient {
4500 left: zero,
4501 right: F(1.0),
4502 },
4503 local_integrated: PairMultiplierCoefficient {
4504 left: zero,
4505 right: F(1.0),
4506 },
4507 integrated_local: PairMultiplierCoefficient {
4508 left: zero,
4509 right: F(1.0),
4510 },
4511 integrated_integrated: PairMultiplierCoefficient {
4512 left: zero,
4513 right: F(1.0),
4514 },
4515 }
4516 .all_zero()
4517 );
4518 }
4519
4520 #[test]
4521 fn detailed_component_finishing_skips_exact_zero_and_reconciles_weights() {
4522 let occurrence = ThresholdCountertermComponentOccurrence::LocalUnitarity {
4523 overlap_groups: smallvec::smallvec![2],
4524 left_threshold_order: Some(1),
4525 right_threshold_order: None,
4526 lu_cut_order: Some(2),
4527 };
4528 let nonzero = PendingLUCountertermComponent {
4529 component_id: 4,
4530 occurrence: occurrence.clone(),
4531 multiplier_values: smallvec::smallvec![F(2.0_f64)],
4532 effective_multiplier: F(2.0),
4533 pass_one: Some(DualOrNot::NonDual(Complex::new_re(F(-3.0)))),
4535 };
4536 let mut pass_two_calls = 0;
4537 let (weighted, completed) = nonzero.finish(&F(0.0), |pass_one| {
4538 pass_two_calls += 1;
4539 pass_one.unwrap_real() * Complex::new_re(F(4.0))
4540 });
4541 assert_eq!(pass_two_calls, 1);
4542 assert_eq!(completed.bare, Some(Complex::new_re(F(-12.0))));
4543 assert_eq!(weighted, Complex::new_re(F(-24.0)));
4544 assert_eq!(completed.weighted, weighted);
4545
4546 let skipped = PendingLUCountertermComponent {
4547 component_id: 5,
4548 occurrence,
4549 multiplier_values: smallvec::smallvec![F(0.0_f64)],
4550 effective_multiplier: F(0.0),
4551 pass_one: None,
4552 };
4553 let (skipped_weight, skipped) = skipped.finish(&F(0.0), |_| {
4554 panic!("exact-zero component must not reach pass two")
4555 });
4556 assert!(skipped.evaluation_skipped);
4557 assert!(skipped.bare.is_none());
4558 assert_eq!(skipped_weight, Complex::new_re(F(0.0)));
4559
4560 let decomposition = crate::observables::events::GenericThresholdCountertermEventInfo {
4561 original: Complex::new_re(F(30.0)),
4562 components: vec![completed, skipped],
4563 };
4564 assert_eq!(decomposition.total(), Complex::new_re(F(6.0)));
4565 }
4566
4567 fn assert_detailed_component_precision<T: FloatLike>() {
4568 let value = |value| F(T::from_f64(value));
4569 let component = PendingLUCountertermComponent {
4570 component_id: 0,
4571 occurrence: ThresholdCountertermComponentOccurrence::LocalUnitarity {
4572 overlap_groups: smallvec::smallvec![0],
4573 left_threshold_order: Some(1),
4574 right_threshold_order: None,
4575 lu_cut_order: Some(1),
4576 },
4577 multiplier_values: smallvec::smallvec![value(2.0)],
4578 effective_multiplier: value(2.0),
4579 pass_one: Some(DualOrNot::NonDual(Complex::new_re(value(-3.0)))),
4580 };
4581 let (_, completed) = component.finish(&value(0.0), |pass_one| {
4582 pass_one.unwrap_real() * Complex::new_re(value(4.0))
4583 });
4584
4585 assert_eq!(completed.bare.unwrap().re.0.into_f64(), -12.0);
4586 assert_eq!(completed.weighted.re.0.into_f64(), -24.0);
4587 assert_eq!(completed.effective_multiplier.0.into_f64(), 2.0);
4588 }
4589
4590 #[test]
4591 fn detailed_components_preserve_all_runtime_precision_lanes() {
4592 assert_detailed_component_precision::<f64>();
4593 assert_detailed_component_precision::<f128>();
4594 assert_detailed_component_precision::<ArbPrec>();
4595 }
4596}