1use std::{collections::BTreeMap, path::Path, sync::Arc};
2
3use bincode_trait_derive::{Decode, Encode};
4use color_eyre::Result;
5use eyre::{Context, eyre};
6use linnet::half_edge::involution::{EdgeVec, Orientation};
7use spenso::algebra::{algebraic_traits::IsZero, complex::Complex};
8use tracing::{debug, instrument, warn};
9use typed_index_collections::{TiVec, ti_vec};
10
11use crate::{
12 GammaLoopContext,
13 cff::{
14 CutCFFIndex,
15 esurface::{
16 Esurface, EsurfaceCollection, EsurfaceID, ExistingEsurfaceId, ExistingEsurfaces,
17 ExistingThresholds, GroupEsurfaceId, RaisedEsurfaceData, RaisedEsurfaceGroup,
18 RaisedEsurfaceId, esurface_value_is_strictly_inside,
19 },
20 expression::OrientationID,
21 },
22 graph::{FeynmanGraph, Graph, GraphGroupPosition, LmbIndex, LoopMomentumBasis},
23 integrands::{
24 evaluation::EvaluationMetaData,
25 process::{
26 GenericEvaluator, ParamBuilder, ThresholdParams,
27 evaluators::{
28 EvaluatorStack, SingleOrAllOrientations, evaluate_evaluator,
29 evaluate_evaluator_single,
30 },
31 threshold_multiplier::{
32 ThresholdMultiplierEvaluatorCollection, ThresholdMultiplierInputWorkspace,
33 },
34 },
35 },
36 model::Model,
37 momentum::{
38 Energy, FourMomentum, Rotation, ThreeMomentum,
39 sample::{
40 BareMomentumSample, ExternalFourMomenta, LoopIndex, LoopMomenta, MomentumSample,
41 SubspaceData,
42 },
43 },
44 processes::{
45 EvaluatorBuildTimings, ResolvedThresholdCountertermVariant, SingleThresholdPieces,
46 ThresholdCountertermComponentKind, ThresholdCountertermMetadataRegistry,
47 ThresholdCountertermSide, ThresholdCountertermVariantId,
48 },
49 settings::{GlobalSettings, RuntimeSettings},
50 subtraction::{
51 evaluate_integrated_ct_normalisation, evaluate_uv_damper,
52 overlap::{
53 OverlapGroup, OverlapInput, OverlapStructure, SingleGraphOverlapData,
54 find_maximal_overlap,
55 },
56 overlap_subspace::{
57 OverlapInput as SubspaceOverlapInput, OverlapKinematics,
58 OverlapStructure as SubspaceOverlapStructure,
59 find_maximal_overlap as find_maximal_subspace_overlap,
60 },
61 },
62 utils::{
63 F, FloatLike, RuntimeCache,
64 hyperdual_utils::{
65 DualOrNot, extract_t_derivatives, extract_t_derivatives_complex, new_constant,
66 shape_from_cut_cff_index, simple_n_deriv_shape,
67 },
68 newton_solver::{NewtonIterationResult, RadialRootDiagnostics, RadialRootIdentity},
69 },
70 uv::Integrands,
71};
72use symbolica::domains::dual::{DualNumberStructure, HyperDual};
73
74const MAX_ITERATIONS: usize = 40;
75
76fn multiply_dual_or_not_complex<T: FloatLike>(
77 lhs: DualOrNot<Complex<F<T>>>,
78 rhs: &DualOrNot<Complex<F<T>>>,
79) -> DualOrNot<Complex<F<T>>> {
80 match (lhs, rhs) {
81 (DualOrNot::NonDual(lhs), DualOrNot::NonDual(rhs)) => DualOrNot::NonDual(lhs * rhs),
82 (DualOrNot::Dual(lhs), DualOrNot::Dual(rhs)) => DualOrNot::Dual(lhs * rhs.clone()),
83 (DualOrNot::Dual(lhs), DualOrNot::NonDual(rhs)) => {
84 let rhs_dual = new_constant(&lhs, rhs);
85 DualOrNot::Dual(lhs * rhs_dual)
86 }
87 (DualOrNot::NonDual(lhs), DualOrNot::Dual(rhs)) => {
88 let lhs_dual = new_constant(rhs, &lhs);
89 DualOrNot::Dual(lhs_dual * rhs.clone())
90 }
91 }
92}
93
94fn combine_completed_threshold_pieces<T: FloatLike>(
95 local: Complex<F<T>>,
96 integrated: Complex<F<T>>,
97 local_multiplier: &F<T>,
98 integrated_multiplier: &F<T>,
99) -> Complex<F<T>> {
100 let mut result = Complex::new_re(local.re.zero());
101 if !local_multiplier.is_zero() {
102 result += local * Complex::new_re(local_multiplier.clone());
103 }
104 if !integrated_multiplier.is_zero() {
105 result += integrated * Complex::new_re(integrated_multiplier.clone());
106 }
107 result
108}
109
110struct EvaluatedAmplitudeThreshold<T: FloatLike> {
111 weighted: Complex<F<T>>,
112 bare_pieces: Option<SingleThresholdPieces<Complex<F<T>>>>,
113}
114
115impl<T: FloatLike> EvaluatedAmplitudeThreshold<T> {
116 fn new(zero: F<T>, record_components: bool) -> Self {
117 Self {
118 weighted: Complex::new_re(zero.clone()),
119 bare_pieces: record_components.then(|| SingleThresholdPieces {
120 local: Complex::new_re(zero.clone()),
121 integrated: Complex::new_re(zero),
122 }),
123 }
124 }
125
126 fn add_completed_pieces(
127 &mut self,
128 local: Complex<F<T>>,
129 integrated: Complex<F<T>>,
130 local_multiplier: &F<T>,
131 integrated_multiplier: &F<T>,
132 ) {
133 if let Some(bare_pieces) = &mut self.bare_pieces {
134 if !local_multiplier.is_zero() {
135 bare_pieces.local += local.clone();
136 }
137 if !integrated_multiplier.is_zero() {
138 bare_pieces.integrated += integrated.clone();
139 }
140 }
141 self.weighted += combine_completed_threshold_pieces(
142 local,
143 integrated,
144 local_multiplier,
145 integrated_multiplier,
146 );
147 }
148
149 fn finish(mut self, local_multiplier: &F<T>, integrated_multiplier: &F<T>) -> Self {
150 if let Some(bare_pieces) = &self.bare_pieces {
151 self.weighted = combine_completed_threshold_pieces(
152 bare_pieces.local.clone(),
153 bare_pieces.integrated.clone(),
154 local_multiplier,
155 integrated_multiplier,
156 );
157 }
158 self
159 }
160}
161
162fn append_amplitude_component_evaluations<T: FloatLike>(
163 components: &mut Option<Vec<AmplitudeCountertermComponentEvaluation<T>>>,
164 evaluation: &EvaluatedAmplitudeThreshold<T>,
165 variant_id: ThresholdCountertermVariantId,
166 esurface_id: RaisedEsurfaceId,
167 overlap_group: usize,
168 local_multiplier: &F<T>,
169 integrated_multiplier: &F<T>,
170) {
171 let Some(components) = components else {
172 return;
173 };
174 let bare_pieces = evaluation
175 .bare_pieces
176 .as_ref()
177 .expect("recorded amplitude components require completed bare pieces");
178 for (kind, multiplier, bare_piece) in [
179 (
180 ThresholdCountertermComponentKind::Local,
181 local_multiplier,
182 &bare_pieces.local,
183 ),
184 (
185 ThresholdCountertermComponentKind::Integrated,
186 integrated_multiplier,
187 &bare_pieces.integrated,
188 ),
189 ] {
190 let evaluation_skipped = multiplier.is_zero();
191 let (bare, weighted) = if evaluation_skipped {
192 (None, Complex::new_re(multiplier.zero()))
193 } else {
194 (
195 Some(bare_piece.clone()),
196 bare_piece.clone() * Complex::new_re(multiplier.clone()),
197 )
198 };
199 components.push(AmplitudeCountertermComponentEvaluation {
200 variant_id,
201 kind,
202 esurface_id,
203 overlap_group,
204 multiplier_value: multiplier.clone(),
205 bare,
206 weighted,
207 evaluation_skipped,
208 });
209 }
210}
211
212#[derive(Clone, Encode, Decode)]
213#[trait_decode(trait = GammaLoopContext)]
214pub struct AmplitudeCountertermData {
215 pub legacy_equivalent: bool,
217 pub overlap: OverlapStructure,
218 pub evaluators: TiVec<RaisedEsurfaceId, AmplitudeCountertermEvaluator>,
219 pub helper_evaluators: Vec<GenericEvaluator>,
220 pub generated_mask: TiVec<RaisedEsurfaceId, bool>,
224 pub active_mask: TiVec<RaisedEsurfaceId, bool>,
229 pub variant_evaluators: TiVec<ThresholdCountertermVariantId, AmplitudeCountertermEvaluator>,
231 pub variant_helper_evaluators: TiVec<ThresholdCountertermVariantId, Vec<GenericEvaluator>>,
232 pub variant_generated_mask: TiVec<ThresholdCountertermVariantId, bool>,
233 pub variant_active_mask: TiVec<ThresholdCountertermVariantId, bool>,
234 pub variant_raised_esurfaces: TiVec<ThresholdCountertermVariantId, RaisedEsurfaceId>,
235 pub variant_subspaces: TiVec<ThresholdCountertermVariantId, SubspaceData>,
236 pub variant_metadata: TiVec<ThresholdCountertermVariantId, ResolvedThresholdCountertermVariant>,
238 pub threshold_multipliers: Option<ThresholdMultiplierEvaluatorCollection>,
240 pub metadata_registry: Option<ThresholdCountertermMetadataRegistry>,
241 pub lmbs: TiVec<LmbIndex, LoopMomentumBasis>,
242 pub raised_data: RaisedEsurfaceData,
243 pub esurface_map: TiVec<GroupEsurfaceId, TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>>,
244 pub local_esurface_exists: TiVec<GroupEsurfaceId, bool>,
245 pub own_group_position: GraphGroupPosition,
246 pub(crate) group_catalogue: RuntimeCache<Arc<AmplitudeOverlapCatalogue>>,
248}
249
250#[derive(Clone)]
251pub(crate) struct AmplitudeOverlapCatalogue {
252 pub graphs: TiVec<GraphGroupPosition, Graph>,
253 pub lmbs: TiVec<LmbIndex, LoopMomentumBasis>,
254 pub variants: Vec<(
255 GraphGroupPosition,
256 ThresholdCountertermVariantId,
257 ResolvedThresholdCountertermVariant,
258 Esurface,
259 )>,
260}
261
262struct PreparedAmplitudeOverlap<T: FloatLike> {
263 thresholds: EsurfaceCollection,
264 threshold_subspaces: Vec<SubspaceData>,
265 threshold_masses: Vec<EdgeVec<F<T>>>,
266 synthetic_variants: TiVec<EsurfaceID, usize>,
267 sample_in_common_lmb: MomentumSample<T>,
268 lmbs: TiVec<LmbIndex, LoopMomentumBasis>,
269 parent_lmb: LmbIndex,
270 overlap: SubspaceOverlapStructure,
271}
272
273impl AmplitudeOverlapCatalogue {
274 fn prepare_group<T: FloatLike>(
275 &self,
276 allowed_instances: &[usize],
277 momentum_sample: &MomentumSample<T>,
278 model: &Model,
279 settings: &RuntimeSettings,
280 rotation: &Rotation,
281 ) -> Result<PreparedAmplitudeOverlap<T>> {
282 let topology = &self.graphs[GraphGroupPosition::from(0)];
287 let native_full_space = allowed_instances
288 .iter()
289 .any(|&instance| self.variants[instance].2.requested_parent_lmb.is_none());
290 let mut lmbs = self.lmbs.clone();
291 let common_subspace = if native_full_space {
292 let parent = LmbIndex::from(lmbs.len());
293 lmbs.push(topology.loop_momentum_basis.clone());
294 SubspaceData::new_with_user_selected_lmb(topology.no_dummy(), parent, topology, &lmbs)?
295 } else {
296 self.variants[allowed_instances[0]].2.subspace.clone()
297 };
298 let common_lmb = common_subspace.get_lmb(&lmbs).clone();
299 let sample_in_common_lmb = if native_full_space {
300 momentum_sample.clone()
301 } else {
302 momentum_sample.lmb_transform(&topology.loop_momentum_basis, &common_lmb)
303 };
304 let e_cm = F::from_f64(settings.kinematics.e_cm);
305 let existence_tolerance = F::from_f64(settings.subtraction.esurface_existence_threshold);
306
307 let mut thresholds = EsurfaceCollection::new();
308 let mut threshold_subspaces = Vec::new();
309 let mut threshold_masses = Vec::new();
310 let mut surface_kinematics = Vec::new();
311 let mut synthetic_variants = TiVec::<EsurfaceID, usize>::new();
312 for &instance in allowed_instances {
313 let (owner, _, metadata, esurface) = &self.variants[instance];
314 let subspace = &metadata.subspace;
315 let native_lmb = subspace.get_lmb(&self.lmbs);
316 let member = &self.graphs[*owner];
317 let native_geometry = metadata.requested_parent_lmb.is_none();
318 if !native_geometry
319 && esurface
320 .energies
321 .iter()
322 .chain(esurface.external_shift.iter().map(|(edge, _)| edge))
323 .chain(member.loop_momentum_basis.loop_edges.iter())
324 .any(|edge| native_lmb.edge_signatures.get(*edge).is_none())
325 {
326 return Err(eyre!(
327 "Amplitude graph '{}' threshold variant '{}' refers to edges absent from master graph '{}' coordinates",
328 member.name,
329 metadata.name,
330 topology.name,
331 ));
332 }
333 let native_subspace = if native_geometry {
334 let parent = LmbIndex::from(lmbs.len());
335 lmbs.push(member.loop_momentum_basis.clone());
336 SubspaceData::new_with_user_selected_lmb(member.no_dummy(), parent, member, &lmbs)?
337 } else {
338 subspace.clone()
339 };
340 let native_sample = if native_geometry {
341 momentum_sample.clone()
342 } else {
343 momentum_sample.lmb_transform(&topology.loop_momentum_basis, native_lmb)
344 };
345 let geometry = if native_geometry { member } else { topology };
346 let masses = member.get_real_mass_vector::<T>(model);
347 let existence = esurface.classify_existence_subspace(
348 native_sample.loop_moms(),
349 native_sample.external_moms(),
350 &native_subspace,
351 &lmbs,
352 geometry,
353 &masses,
354 &[],
355 &e_cm,
356 &existence_tolerance,
357 );
358 if !existence.is_existing() {
359 continue;
360 }
361 let rebased = if native_geometry {
363 native_sample
364 } else {
365 native_sample.lmb_transform(native_lmb, &common_lmb)
366 };
367 let rebased_subspace = if native_geometry {
368 native_subspace
369 } else {
370 SubspaceData::new_from_parent_basis_edges(
371 &subspace.iter_basis_edges(&self.lmbs).collect::<Vec<_>>(),
372 &topology.no_dummy(),
373 common_subspace.parent_lmb_index(),
374 topology,
375 &lmbs,
376 )?
377 };
378 thresholds.push(esurface.clone());
379 threshold_subspaces.push(rebased_subspace);
380 surface_kinematics.push(OverlapKinematics {
381 loop_moms: rebased.loop_moms().iter().map(|mom| mom.to_f64()).collect(),
382 external_momenta: rebased
383 .external_moms()
384 .iter()
385 .map(|mom| mom.to_f64())
386 .collect(),
387 edge_masses: Some(masses.iter().map(|(_, mass)| F(mass.0.to_f64())).collect()),
388 });
389 threshold_masses.push(masses);
390 synthetic_variants.push(instance);
391 }
392 let existing_thresholds: ExistingThresholds = thresholds.keys().collect();
393 let sample_f64 = momentum_sample_to_f64(&sample_in_common_lmb);
394 let overlap = if native_full_space && !thresholds.is_empty() {
395 let raised_data = RaisedEsurfaceData {
398 raised_groups: thresholds
399 .keys()
400 .map(|id| RaisedEsurfaceGroup {
401 esurface_ids: vec![id],
402 max_occurence: 1,
403 })
404 .collect(),
405 pass_two_evaluator: None,
406 };
407 let mut probe_settings = settings.clone();
408 probe_settings
411 .subtraction
412 .overlap_settings
413 .try_origin_all_lmbs = false;
414 if let Some(center) = &mut probe_settings
415 .subtraction
416 .overlap_settings
417 .force_global_center
418 {
419 let rotated = center
420 .iter()
421 .map(|p| ThreeMomentum::new(F(p[0]), F(p[1]), F(p[2])))
422 .collect::<LoopMomenta<_>>()
423 .rotate(rotation);
424 *center = rotated.iter().map(|p| [p.px.0, p.py.0, p.pz.0]).collect();
425 }
426 let input = OverlapInput {
427 graph_data: thresholds
428 .keys()
429 .map(|id| SingleGraphOverlapData {
430 lmb: threshold_subspaces[id.0].get_lmb(&lmbs),
431 esurfaces: &thresholds,
432 raised_data: &raised_data,
433 edge_masses: surface_kinematics[id.0].edge_masses.clone().unwrap(),
434 })
435 .collect(),
436 settings: &probe_settings,
437 group_esurface_map: thresholds
438 .keys()
439 .map(|id| {
440 thresholds
441 .keys()
442 .map(|owner| (owner == id).then_some(RaisedEsurfaceId::from(id.0)))
443 .collect()
444 })
445 .collect(),
446 local_esurface_exists: thresholds
447 .keys()
448 .map(|owner| thresholds.keys().map(|id| owner == id).collect())
449 .collect(),
450 };
451 let overlap = find_maximal_overlap(
452 &input,
453 &thresholds
454 .keys()
455 .map(|id| GroupEsurfaceId::from(id.0))
456 .collect(),
457 sample_f64.external_moms(),
458 )?;
459 Ok(SubspaceOverlapStructure {
460 existing_esurfaces: overlap
461 .existing_esurfaces
462 .iter()
463 .map(|id| EsurfaceID::from(id.0))
464 .collect(),
465 overlap_groups: overlap
466 .overlap_groups
467 .into_iter()
468 .map(|group| crate::subtraction::overlap_subspace::OverlapGroup {
469 existing_esurfaces: group.existing_esurfaces,
470 complement: group.complement,
471 center: group.center,
472 })
473 .collect(),
474 })
475 } else {
476 let overlap_input = SubspaceOverlapInput {
477 graph: topology,
478 settings,
479 subspace: &common_subspace,
480 threshold_subspaces: Some(&threshold_subspaces),
481 lmbs: &lmbs,
482 thresholds: &thresholds,
483 edge_masses: topology.get_real_mass_vector::<f64>(model),
484 surface_kinematics: Some(&surface_kinematics),
485 };
486 find_maximal_subspace_overlap(
487 &overlap_input,
488 &existing_thresholds,
489 sample_f64.loop_moms(),
490 sample_f64.external_moms(),
491 rotation,
492 )
493 }
494 .with_context(|| {
495 format!(
496 "Failed to find shared threshold overlap for amplitude group master '{}'",
497 topology.name,
498 )
499 })?;
500
501 Ok(PreparedAmplitudeOverlap {
502 thresholds,
503 threshold_subspaces,
504 threshold_masses,
505 synthetic_variants,
506 sample_in_common_lmb,
507 lmbs,
508 parent_lmb: common_subspace.parent_lmb_index(),
509 overlap,
510 })
511 }
512}
513
514#[derive(Clone, Encode, Decode)]
515#[trait_decode(trait = GammaLoopContext)]
516pub struct AmplitudeCountertermAtom {
517 pub parametric: Integrands,
518}
519
520impl AmplitudeCountertermAtom {
521 pub(crate) fn is_generated(&self) -> bool {
522 self.parametric.iter().next().is_some()
523 }
524
525 pub(crate) fn zero_like(&self) -> Self {
526 Self {
527 parametric: self.parametric.zero_like(),
528 }
529 }
530
531 #[instrument(skip_all)]
532 pub(crate) fn to_evaluator_with_timings(
533 &self,
534 param_builder: &ParamBuilder,
535 orientations: &TiVec<OrientationID, EdgeVec<Orientation>>,
536 production_orientation_ids: &[OrientationID],
537 global_settings: &GlobalSettings,
538 ) -> (AmplitudeCountertermEvaluator, EvaluatorBuildTimings) {
539 let _progress_guard =
540 crate::processes::enter_detailed_progress_span("Building Threshold CT Evaluator");
541 let mut evaluator_stacks = BTreeMap::new();
542 let mut timings = EvaluatorBuildTimings::default();
543
544 for (index, integrand) in self.parametric.iter() {
545 let dual_shape = shape_from_cut_cff_index(index);
546
547 let (evaluator_stack, evaluator_timings) = EvaluatorStack::from_integrand_with_timings(
550 integrand,
551 param_builder,
552 self.parametric.numerators(),
553 (!global_settings.generation.explicit_orientation_sum_only)
554 .then_some((orientations.as_slice().as_ref(), production_orientation_ids)),
555 dual_shape,
556 &global_settings.generation.evaluator,
557 )
558 .unwrap();
559 timings += evaluator_timings;
560 evaluator_stacks.insert(*index, evaluator_stack);
561 }
562
563 (AmplitudeCountertermEvaluator { evaluator_stacks }, timings)
564 }
565
566 pub(crate) fn new() -> Self {
567 Self {
568 parametric: std::iter::empty().collect(),
569 }
570 }
571}
572
573#[derive(Clone, Encode, Decode)]
574#[trait_decode(trait = GammaLoopContext)]
575pub struct AmplitudeCountertermEvaluator {
576 pub evaluator_stacks: BTreeMap<CutCFFIndex, EvaluatorStack>,
577}
578
579impl AmplitudeCountertermEvaluator {
580 pub(crate) fn generic_evaluator_count(&self) -> usize {
581 self.evaluator_stacks
582 .values()
583 .map(EvaluatorStack::generic_evaluator_count)
584 .sum()
585 }
586}
587
588#[derive(Debug, Clone)]
589pub struct AmplitudeLocalCountertermEvaluation<T: FloatLike> {
590 pub variant_id: Option<ThresholdCountertermVariantId>,
591 pub esurface_id: RaisedEsurfaceId,
592 pub overlap_group: usize,
593 pub value: Complex<F<T>>,
594}
595
596#[derive(Debug, Clone)]
597pub struct AmplitudeCountertermComponentEvaluation<T: FloatLike> {
598 pub variant_id: ThresholdCountertermVariantId,
599 pub kind: ThresholdCountertermComponentKind,
600 pub esurface_id: RaisedEsurfaceId,
601 pub overlap_group: usize,
602 pub multiplier_value: F<T>,
603 pub bare: Option<Complex<F<T>>>,
604 pub weighted: Complex<F<T>>,
605 pub evaluation_skipped: bool,
606}
607
608#[derive(Debug, Clone)]
609pub struct AmplitudeCountertermEvaluation<T: FloatLike> {
610 pub total: Complex<F<T>>,
611 pub local_counterterms: Vec<AmplitudeLocalCountertermEvaluation<T>>,
612 pub components: Option<Vec<AmplitudeCountertermComponentEvaluation<T>>>,
613}
614
615impl AmplitudeCountertermData {
616 fn radial_root_identity(
617 graph_name: &str,
618 overlap_group: usize,
619 raised_esurface_id: RaisedEsurfaceId,
620 rotation: &Rotation,
621 ) -> RadialRootIdentity {
622 RadialRootIdentity::new(format!(
623 "amplitude graph '{graph_name}' overlap group {overlap_group} raised E-surface {} probe rotation {}",
624 raised_esurface_id.0, rotation.method,
625 ))
626 }
627
628 fn variant_radial_root_identity(
629 graph_name: &str,
630 overlap_group: usize,
631 variant_id: ThresholdCountertermVariantId,
632 raised_esurface_id: RaisedEsurfaceId,
633 rotation: &Rotation,
634 ) -> RadialRootIdentity {
635 RadialRootIdentity::new(format!(
636 "amplitude graph '{graph_name}' overlap group {overlap_group} threshold variant {} raised E-surface {} probe rotation {}",
637 variant_id.0, raised_esurface_id.0, rotation.method,
638 ))
639 }
640
641 pub(crate) fn generic_evaluator_count(&self) -> usize {
642 let stack_count = self
643 .evaluators
644 .iter()
645 .map(AmplitudeCountertermEvaluator::generic_evaluator_count)
646 .chain(
647 self.variant_evaluators
648 .iter()
649 .map(AmplitudeCountertermEvaluator::generic_evaluator_count),
650 )
651 .sum::<usize>();
652 let overlap_count = self
653 .overlap
654 .overlap_groups
655 .iter()
656 .map(|group| {
657 group
658 .prefactor_evaluator
659 .as_ref()
660 .map(|evaluators| evaluators.len())
661 .unwrap_or(0)
662 })
663 .sum::<usize>();
664 stack_count
665 + overlap_count
666 + self.helper_evaluators.len()
667 + self
668 .variant_helper_evaluators
669 .iter()
670 .map(Vec::len)
671 .sum::<usize>()
672 }
673
674 pub fn new_empty(own_group_position: GraphGroupPosition) -> Self {
675 Self {
676 legacy_equivalent: true,
677 overlap: OverlapStructure::new_empty(),
678 evaluators: TiVec::new(),
679 helper_evaluators: vec![],
680 generated_mask: TiVec::new(),
681 active_mask: TiVec::new(),
682 variant_evaluators: TiVec::new(),
683 variant_helper_evaluators: TiVec::new(),
684 variant_generated_mask: TiVec::new(),
685 variant_active_mask: TiVec::new(),
686 variant_raised_esurfaces: TiVec::new(),
687 variant_subspaces: TiVec::new(),
688 variant_metadata: TiVec::new(),
689 threshold_multipliers: None,
690 metadata_registry: None,
691 lmbs: TiVec::new(),
692 raised_data: RaisedEsurfaceData {
693 raised_groups: TiVec::new(),
694 pass_two_evaluator: None,
695 },
696 esurface_map: TiVec::new(),
697 local_esurface_exists: TiVec::new(),
698 own_group_position,
699 group_catalogue: RuntimeCache::default(),
700 }
701 }
702
703 fn overlap_catalogue(
704 &self,
705 graph: &Graph,
706 esurfaces: &EsurfaceCollection,
707 ) -> Arc<AmplitudeOverlapCatalogue> {
708 self.group_catalogue.as_ref().cloned().unwrap_or_else(|| {
709 Arc::new(AmplitudeOverlapCatalogue {
710 graphs: std::iter::repeat_with(|| graph.clone())
711 .take(usize::from(self.own_group_position) + 1)
712 .collect(),
713 lmbs: self.lmbs.clone(),
714 variants: self
715 .variant_metadata
716 .iter_enumerated()
717 .filter(|(id, _)| {
718 self.variant_generated_mask[*id] && self.variant_active_mask[*id]
719 })
720 .map(|(id, metadata)| {
721 let raised = self.variant_raised_esurfaces[id];
722 (
723 self.own_group_position,
724 id,
725 metadata.clone(),
726 esurfaces[self.raised_data.raised_groups[raised].esurface_ids[0]]
727 .clone(),
728 )
729 })
730 .collect(),
731 })
732 })
733 }
734
735 fn ensure_active_raised_esurface(&self, raised_esurface_id: RaisedEsurfaceId) -> Result<()> {
736 if self.active_mask[raised_esurface_id] {
737 return Ok(());
738 }
739
740 Err(color_eyre::eyre::eyre!(
741 "Amplitude threshold evaluator {} was reached at runtime even though generation marked it inactive for the selected orientation subset",
742 raised_esurface_id.0
743 ))
744 }
745
746 fn ensure_active_variant(&self, variant_id: ThresholdCountertermVariantId) -> Result<()> {
747 if self.variant_active_mask[variant_id] {
748 return Ok(());
749 }
750
751 Err(eyre!(
752 "Amplitude threshold variant {} was reached at runtime even though generation marked it inactive for the selected orientation subset",
753 variant_id.0,
754 ))
755 }
756
757 fn metadata_variant_for_raised_esurface(
758 &self,
759 raised_esurface_id: RaisedEsurfaceId,
760 ) -> Result<ThresholdCountertermVariantId> {
761 let registry = self.metadata_registry.as_ref().ok_or_else(|| {
762 eyre!("Amplitude threshold metadata was requested without a graph-local registry")
763 })?;
764 let raised_group = &self.raised_data.raised_groups[raised_esurface_id];
765 let mut matching = registry.variants.iter().filter(|variant| {
766 variant.side == ThresholdCountertermSide::Amplitude
767 && variant.threshold_esurface_ids.len() == raised_group.esurface_ids.len()
768 && variant
769 .threshold_esurface_ids
770 .iter()
771 .zip(&raised_group.esurface_ids)
772 .all(|(metadata_id, esurface_id)| *metadata_id == esurface_id.0)
773 });
774 let variant = matching.next().ok_or_else(|| {
775 eyre!(
776 "Amplitude threshold registry has no variant for raised E-surface {}",
777 raised_esurface_id.0,
778 )
779 })?;
780 if matching.next().is_some() {
781 return Err(eyre!(
782 "Amplitude threshold registry has multiple variants for legacy raised E-surface {}",
783 raised_esurface_id.0,
784 ));
785 }
786 Ok(ThresholdCountertermVariantId(variant.variant_id))
787 }
788
789 #[allow(clippy::too_many_arguments)]
790 fn evaluate_variant_multiplier<T: FloatLike>(
791 threshold_multipliers: &mut Option<ThresholdMultiplierEvaluatorCollection>,
792 multiplier_workspace: Option<&mut ThresholdMultiplierInputWorkspace<T>>,
793 variant_id: ThresholdCountertermVariantId,
794 graph: &Graph,
795 edge_masses: &EdgeVec<F<T>>,
796 model_values: &[Complex<F<f64>>],
797 additional_values: &[F<T>],
798 base_sample: &MomentumSample<T>,
799 root_sample: &MomentumSample<T>,
800 evaluation_metadata: &mut EvaluationMetaData,
801 ) -> Result<(F<T>, F<T>)> {
802 let Some(collection) = threshold_multipliers.as_mut() else {
803 return Ok((base_sample.one(), base_sample.one()));
804 };
805 let reference = collection
806 .left_variants()
807 .iter()
808 .find(|reference| reference.variant_id == variant_id)
809 .copied()
810 .ok_or_else(|| {
811 eyre!(
812 "Amplitude threshold variant {} has no multiplier-registry entry",
813 variant_id.0,
814 )
815 })?;
816 let Some(evaluator_id) = reference.evaluator_id else {
817 return Ok((base_sample.one(), base_sample.one()));
818 };
819
820 let workspace = multiplier_workspace.ok_or_else(|| {
821 eyre!(
822 "Amplitude threshold variant {} requires a missing multiplier workspace",
823 variant_id.0,
824 )
825 })?;
826 let local_values = {
827 let layout = collection.layout();
828 workspace.bind(
829 layout,
830 &graph.loop_momentum_basis,
831 edge_masses,
832 model_values,
833 additional_values,
834 base_sample,
835 root_sample,
836 )
837 .with_context(|| {
838 format!(
839 "Failed to bind local threshold multiplier for amplitude graph '{}' variant {}",
840 graph.name, variant_id.0,
841 )
842 })?
843 };
844 let local = collection.evaluators_mut()[evaluator_id.0].evaluate(
845 local_values,
846 evaluation_metadata,
847 )
848 .with_context(|| {
849 format!(
850 "Failed to evaluate local threshold multiplier for amplitude graph '{}' variant {}",
851 graph.name, variant_id.0,
852 )
853 })?;
854
855 let integrated_values = {
856 let layout = collection.layout();
857 workspace.bind(
858 layout,
859 &graph.loop_momentum_basis,
860 edge_masses,
861 model_values,
862 additional_values,
863 root_sample,
864 root_sample,
865 )
866 .with_context(|| {
867 format!(
868 "Failed to bind integrated threshold multiplier for amplitude graph '{}' variant {}",
869 graph.name, variant_id.0,
870 )
871 })?
872 };
873 let integrated = collection.evaluators_mut()[evaluator_id.0].evaluate(
874 integrated_values,
875 evaluation_metadata,
876 )
877 .with_context(|| {
878 format!(
879 "Failed to evaluate integrated threshold multiplier for amplitude graph '{}' variant {}",
880 graph.name, variant_id.0,
881 )
882 })?;
883
884 Ok((local, integrated))
885 }
886
887 pub fn compile(
888 &mut self,
889 path: impl AsRef<Path>,
890 _override_existing: bool,
891 frozen_mode: &crate::settings::global::FrozenCompilationMode,
892 ) -> Result<()> {
893 for (i, e) in self.evaluators.iter_mut_enumerated() {
894 for (cff_index, evaluator_stack) in e.evaluator_stacks.iter_mut() {
895 let order_index = cff_index.left_threshold_order.unwrap() - 1;
896 evaluator_stack.compile(
897 format!("esurface_{}_order_{}", i.0, order_index + 1),
898 path.as_ref(),
899 frozen_mode,
900 )?;
901 }
902 }
903
904 for (variant_id, evaluator) in self.variant_evaluators.iter_mut_enumerated() {
905 for (cff_index, evaluator_stack) in &mut evaluator.evaluator_stacks {
906 let order_index = cff_index.left_threshold_order.unwrap() - 1;
907 evaluator_stack.compile(
908 format!(
909 "threshold_variant_{}_order_{}",
910 variant_id.0,
911 order_index + 1,
912 ),
913 path.as_ref(),
914 frozen_mode,
915 )?;
916 }
917 }
918
919 for (group_index, group) in self.overlap.overlap_groups.iter_mut().enumerate() {
920 if let Some(prefactor_evaluators) = group.prefactor_evaluator.as_mut() {
921 for (order_index, prefactor_evaluator) in
922 prefactor_evaluators.iter_mut().enumerate()
923 {
924 prefactor_evaluator.borrow_mut().compile_external(
925 path.as_ref()
926 .join(format!(
927 "overlap_prefactor_{group_index}_order_{}",
928 order_index + 1
929 ))
930 .with_extension("cpp"),
931 format!("overlap_prefactor_{group_index}_order_{}", order_index + 1),
932 path.as_ref()
933 .join(format!(
934 "overlap_prefactor_{group_index}_order_{}",
935 order_index + 1
936 ))
937 .with_extension("so"),
938 frozen_mode,
939 )?;
940 }
941 }
942 }
943
944 for (order_index, evaluator) in self.helper_evaluators.iter_mut().enumerate() {
945 evaluator.compile_external(
946 path.as_ref()
947 .join(format!("threshold_helper_{order_index}"))
948 .with_extension("cpp"),
949 format!("threshold_helper_{order_index}"),
950 path.as_ref()
951 .join(format!("threshold_helper_{order_index}"))
952 .with_extension("so"),
953 frozen_mode,
954 )?;
955 }
956 for (variant_id, helpers) in self.variant_helper_evaluators.iter_mut_enumerated() {
957 for (order_index, evaluator) in helpers.iter_mut().enumerate() {
958 evaluator.compile_external(
959 path.as_ref()
960 .join(format!(
961 "threshold_variant_{}_helper_{order_index}",
962 variant_id.0,
963 ))
964 .with_extension("cpp"),
965 format!("threshold_variant_{}_helper_{order_index}", variant_id.0,),
966 path.as_ref()
967 .join(format!(
968 "threshold_variant_{}_helper_{order_index}",
969 variant_id.0,
970 ))
971 .with_extension("so"),
972 frozen_mode,
973 )?;
974 }
975 }
976 Ok(())
977 }
978
979 pub(crate) fn for_each_generic_evaluator_mut(
980 &mut self,
981 mut f: impl FnMut(&mut crate::integrands::process::GenericEvaluator) -> Result<()>,
982 ) -> Result<()> {
983 for evaluator in self.evaluators.iter_mut() {
984 for evaluator_stack in evaluator.evaluator_stacks.values_mut() {
985 evaluator_stack.for_each_generic_evaluator_mut(&mut f)?;
986 }
987 }
988 for evaluator in &mut self.variant_evaluators {
989 for evaluator_stack in evaluator.evaluator_stacks.values_mut() {
990 evaluator_stack.for_each_generic_evaluator_mut(&mut f)?;
991 }
992 }
993
994 for group in &mut self.overlap.overlap_groups {
995 if let Some(prefactor_evaluators) = group.prefactor_evaluator.as_mut() {
996 for prefactor_evaluator in prefactor_evaluators.iter_mut() {
997 f(prefactor_evaluator.get_mut())?;
998 }
999 }
1000 }
1001
1002 for evaluator in &mut self.helper_evaluators {
1003 f(evaluator)?;
1004 }
1005 for helpers in &mut self.variant_helper_evaluators {
1006 for evaluator in helpers {
1007 f(evaluator)?;
1008 }
1009 }
1010 if let Some(multipliers) = &mut self.threshold_multipliers {
1011 multipliers.for_each_generic_evaluator_mut(&mut f)?;
1012 }
1013
1014 Ok(())
1015 }
1016
1017 #[allow(clippy::too_many_arguments)]
1018 pub fn evaluate<T: FloatLike>(
1019 &mut self,
1020 momentum_sample: &MomentumSample<T>,
1021 graph: &Graph,
1022 model: &Model,
1023 esurfaces: &EsurfaceCollection,
1024 rotation: &Rotation,
1025 settings: &RuntimeSettings,
1026 param_builder: &mut ParamBuilder<f64>,
1027 orientation: SingleOrAllOrientations<'_, OrientationID>,
1028 evaluation_metadata: &mut EvaluationMetaData,
1029 record_components: bool,
1030 ) -> Result<AmplitudeCountertermEvaluation<T>> {
1031 if record_components && self.metadata_registry.is_none() {
1032 return Err(eyre!(
1033 "Amplitude graph '{}' requested detailed threshold components without metadata",
1034 graph.name,
1035 ));
1036 }
1037 if !self.legacy_equivalent {
1038 return self.evaluate_variants(
1039 momentum_sample,
1040 graph,
1041 model,
1042 esurfaces,
1043 rotation,
1044 settings,
1045 param_builder,
1046 orientation,
1047 evaluation_metadata,
1048 record_components,
1049 );
1050 }
1051
1052 debug!("start evaluate threshold counterterm");
1053 let existing_esurfaces = self
1054 .overlap
1055 .existing_esurfaces
1056 .iter()
1057 .map(|e| e.0)
1058 .collect::<Vec<_>>();
1059 debug!("subtracting esurfaces: {:?}", existing_esurfaces);
1060 debug!("overlap structure\n: {}", self.overlap);
1061
1062 let counter_term_builder = CounterTermBuilder::new(
1063 graph,
1064 model,
1065 rotation,
1066 settings,
1067 esurfaces,
1068 momentum_sample,
1069 &self.overlap,
1070 &self.raised_data,
1071 self.own_group_position,
1072 &self.esurface_map,
1073 );
1074
1075 let mut result = Complex::new_re(momentum_sample.zero());
1076 let mut local_counterterms = Vec::new();
1077 let mut components = record_components.then(Vec::new);
1078 let mut radial_root_failed = false;
1079 let split_helpers = self.metadata_registry.is_some();
1080
1081 for (overlap_group, group) in self.overlap.overlap_groups.iter().enumerate() {
1082 let overlap_builder = counter_term_builder.new_overlap_builder(group);
1083
1084 for existing_esurface_id in group.existing_esurfaces.iter() {
1085 let group_esurface_id = self.overlap.existing_esurfaces[*existing_esurface_id];
1086 if self
1087 .local_esurface_exists
1088 .get(group_esurface_id)
1089 .is_some_and(|exists| !*exists)
1090 {
1091 continue;
1092 }
1093
1094 let Some(esurface_builder) =
1095 overlap_builder.new_esurface_builder(*existing_esurface_id)
1096 else {
1097 continue;
1098 };
1099
1100 let raised_esurface_id = esurface_builder.raised_esurface_id;
1101 self.ensure_active_raised_esurface(raised_esurface_id)?;
1102 let radial_root_identity = Self::radial_root_identity(
1103 &graph.name,
1104 overlap_group,
1105 raised_esurface_id,
1106 rotation,
1107 );
1108 let Some(rstar_solution) = esurface_builder.solve_rstar(
1109 &radial_root_identity,
1110 &mut evaluation_metadata.radial_root_diagnostics,
1111 ) else {
1112 evaluation_metadata.record_threshold_counterterm_error(format!(
1113 "amplitude graph '{}' overlap group {} raised E-surface {} failed center or radial-root validation in probe rotation {}",
1114 graph.name,
1115 overlap_group,
1116 raised_esurface_id.0,
1117 rotation.method,
1118 ));
1119 radial_root_failed = true;
1120 continue;
1121 };
1122 if radial_root_failed {
1123 continue;
1124 }
1125 let single_evaluation = rstar_solution.rstar_samples().evaluate(
1126 param_builder,
1127 orientation,
1128 evaluation_metadata,
1129 &mut self.evaluators[raised_esurface_id],
1130 &mut self.helper_evaluators,
1131 split_helpers,
1132 record_components,
1133 )?;
1134 let single_result = single_evaluation.weighted.clone();
1135
1136 if record_components {
1137 let variant_id = self
1138 .metadata_variant_for_raised_esurface(raised_esurface_id)
1139 .with_context(|| {
1140 format!(
1141 "while recording amplitude graph '{}' overlap group {}",
1142 graph.name, overlap_group,
1143 )
1144 })?;
1145 let one = momentum_sample.one();
1146 append_amplitude_component_evaluations(
1147 &mut components,
1148 &single_evaluation,
1149 variant_id,
1150 raised_esurface_id,
1151 overlap_group,
1152 &one,
1153 &one,
1154 );
1155 }
1156
1157 if !single_result.is_zero() {
1158 debug!(
1163 "Counterterm for esurface {}: {:+16e}",
1164 existing_esurface_id, single_result
1165 );
1166 }
1167
1168 local_counterterms.push(AmplitudeLocalCountertermEvaluation {
1169 variant_id: None,
1170 esurface_id: raised_esurface_id,
1171 overlap_group,
1172 value: single_result.clone(),
1173 });
1174 result += single_result;
1175 }
1176 }
1177 if radial_root_failed {
1178 return Ok(AmplitudeCountertermEvaluation {
1179 total: Complex::new_re(F::from_f64(f64::NAN)),
1180 local_counterterms,
1181 components: None,
1182 });
1183 }
1184 Ok(AmplitudeCountertermEvaluation {
1185 total: result,
1186 local_counterterms,
1187 components,
1188 })
1189 }
1190
1191 #[allow(clippy::too_many_arguments)]
1192 fn evaluate_variants<T: FloatLike>(
1193 &mut self,
1194 momentum_sample: &MomentumSample<T>,
1195 graph: &Graph,
1196 model: &Model,
1197 esurfaces: &EsurfaceCollection,
1198 rotation: &Rotation,
1199 settings: &RuntimeSettings,
1200 param_builder: &mut ParamBuilder<f64>,
1201 orientation: SingleOrAllOrientations<'_, OrientationID>,
1202 evaluation_metadata: &mut EvaluationMetaData,
1203 record_components: bool,
1204 ) -> Result<AmplitudeCountertermEvaluation<T>> {
1205 let catalogue = self.overlap_catalogue(graph, esurfaces);
1206 let mut groups = BTreeMap::new();
1207 let mut explicit = BTreeMap::<usize, (_, GraphGroupPosition, usize)>::new();
1208 for (instance, (owner, variant_id, metadata, _)) in catalogue.variants.iter().enumerate() {
1209 let signature = metadata.subspace.solve_signature(&catalogue.lmbs);
1210 if let Some(label) = metadata.group_id {
1211 if let Some((previous, previous_owner, previous_variant)) = explicit.get(&label) {
1212 if previous != &signature {
1213 return Err(eyre!(
1214 "Amplitude group master '{}' explicit threshold group_id={} has incompatible physical solve cycles: graph '{}' variant {} {:?}; graph '{}' variant {} {:?}",
1215 catalogue.graphs[GraphGroupPosition::from(0)].name,
1216 label,
1217 catalogue.graphs[*previous_owner].name,
1218 previous_variant,
1219 previous,
1220 catalogue.graphs[*owner].name,
1221 variant_id.0,
1222 signature,
1223 ));
1224 }
1225 } else {
1226 explicit.insert(label, (signature.clone(), *owner, variant_id.0));
1227 }
1228 }
1229 groups
1230 .entry((metadata.group_id, signature))
1231 .or_insert_with(Vec::new)
1232 .push(instance);
1233 }
1234
1235 let mut total = Complex::new_re(momentum_sample.zero());
1236 let mut local_counterterms = Vec::new();
1237 let mut components = record_components.then(Vec::new);
1238 for (_, members) in groups {
1239 let evaluation = self.evaluate_variant_group(
1240 momentum_sample,
1241 graph,
1242 model,
1243 esurfaces,
1244 rotation,
1245 settings,
1246 param_builder,
1247 orientation,
1248 evaluation_metadata,
1249 record_components,
1250 &catalogue,
1251 &members,
1252 )?;
1253 total += evaluation.total;
1254 local_counterterms.extend(evaluation.local_counterterms);
1255 if let (Some(all), Some(mut group_components)) =
1256 (components.as_mut(), evaluation.components)
1257 {
1258 all.append(&mut group_components);
1259 }
1260 }
1261 Ok(AmplitudeCountertermEvaluation {
1262 total,
1263 local_counterterms,
1264 components,
1265 })
1266 }
1267
1268 #[allow(clippy::too_many_arguments)]
1269 fn evaluate_variant_group<T: FloatLike>(
1270 &mut self,
1271 momentum_sample: &MomentumSample<T>,
1272 graph: &Graph,
1273 model: &Model,
1274 _esurfaces: &EsurfaceCollection,
1275 rotation: &Rotation,
1276 settings: &RuntimeSettings,
1277 param_builder: &mut ParamBuilder<f64>,
1278 orientation: SingleOrAllOrientations<'_, OrientationID>,
1279 evaluation_metadata: &mut EvaluationMetaData,
1280 record_components: bool,
1281 catalogue: &AmplitudeOverlapCatalogue,
1282 allowed_instances: &[usize],
1283 ) -> Result<AmplitudeCountertermEvaluation<T>> {
1284 if self.variant_evaluators.len() != self.variant_subspaces.len()
1285 || self.variant_evaluators.len() != self.variant_raised_esurfaces.len()
1286 || self.variant_evaluators.len() != self.variant_helper_evaluators.len()
1287 || self.variant_evaluators.len() != self.variant_generated_mask.len()
1288 || self.variant_evaluators.len() != self.variant_active_mask.len()
1289 || self.variant_evaluators.len() != self.variant_metadata.len()
1290 {
1291 return Err(eyre!(
1292 "Amplitude graph '{}' has inconsistent threshold-variant runtime dimensions",
1293 graph.name,
1294 ));
1295 }
1296 if let Some(multipliers) = &self.threshold_multipliers {
1297 multipliers.validate(self.variant_evaluators.len(), 0)?;
1298 if multipliers
1299 .left_variants()
1300 .iter()
1301 .enumerate()
1302 .any(|(variant_id, reference)| reference.variant_id.0 != variant_id)
1303 {
1304 return Err(eyre!(
1305 "Amplitude graph '{}' threshold multiplier references are not ordered by contiguous variant ID",
1306 graph.name,
1307 ));
1308 }
1309 }
1310
1311 let PreparedAmplitudeOverlap {
1312 thresholds,
1313 threshold_subspaces,
1314 threshold_masses,
1315 synthetic_variants,
1316 sample_in_common_lmb,
1317 lmbs,
1318 parent_lmb,
1319 overlap,
1320 } = catalogue.prepare_group(
1321 allowed_instances,
1322 momentum_sample,
1323 model,
1324 settings,
1325 rotation,
1326 )?;
1327 let topology = &catalogue.graphs[GraphGroupPosition::from(0)];
1328 let common_lmb = &lmbs[parent_lmb];
1329 let real_mass_vector = graph.get_real_mass_vector(model);
1330 let e_cm = F::from_f64(settings.kinematics.e_cm);
1331 let max_prefactor_power = synthetic_variants
1332 .iter()
1333 .map(|&instance| {
1334 let order = catalogue.variants[instance]
1335 .2
1336 .raised_esurface_group
1337 .max_occurence;
1338 2 * (order / 2 + 1)
1339 })
1340 .max()
1341 .unwrap_or(2);
1342 let mut total = Complex::new_re(momentum_sample.zero());
1343 let mut local_counterterms = Vec::new();
1344 let mut components = record_components.then(Vec::new);
1345 let mut radial_root_failed = false;
1346 let mut multiplier_workspace = self.threshold_multipliers.as_ref().map(|multipliers| {
1347 ThresholdMultiplierInputWorkspace::new(multipliers.layout(), momentum_sample.zero())
1348 });
1349 let additional_params = settings.additional_params::<T>();
1350
1351 for (overlap_group_index, overlap_group) in overlap.overlap_groups.iter().enumerate() {
1352 for &existing_esurface_id in &overlap_group.existing_esurfaces {
1353 let synthetic_esurface_id = overlap.existing_esurfaces[existing_esurface_id];
1354 let instance = synthetic_variants[synthetic_esurface_id];
1355 let (owner, variant_id, metadata, _) = &catalogue.variants[instance];
1356 if *owner != self.own_group_position {
1357 continue;
1358 }
1359 let variant_id = *variant_id;
1360 self.ensure_active_variant(variant_id)?;
1361 let raised_esurface_id = self.variant_raised_esurfaces[variant_id];
1362 let subspace = &threshold_subspaces[synthetic_esurface_id.0];
1363 let esurface = &thresholds[synthetic_esurface_id];
1364 let native_geometry = metadata.requested_parent_lmb.is_none();
1365 let geometry = if native_geometry { graph } else { topology };
1366 let root_lmb = if native_geometry {
1367 &graph.loop_momentum_basis
1368 } else {
1369 common_lmb
1370 };
1371
1372 let mut center = overlap_group.center.cast::<T>();
1373 for loop_index in (0..center.0.len()).map(LoopIndex::from) {
1374 if !subspace.contains_loop_index(loop_index) {
1375 center[loop_index] = ThreeMomentum::new(
1376 momentum_sample.zero(),
1377 momentum_sample.zero(),
1378 momentum_sample.zero(),
1379 );
1380 }
1381 }
1382 let shifted_loop_momenta = sample_in_common_lmb.loop_moms() - ¢er;
1383 let radius = shifted_loop_momenta
1384 .hyper_radius_squared(subspace.as_subspace_simple())
1385 .sqrt();
1386 let unit_shifted_momenta =
1387 shifted_loop_momenta.rescale(&radius.inv(), subspace.as_subspace_simple());
1388 let (raw_radius_guess, _) = esurface.get_radius_guess_subspace(
1389 &unit_shifted_momenta,
1390 sample_in_common_lmb.external_moms(),
1391 subspace,
1392 &lmbs,
1393 geometry,
1394 &real_mass_vector,
1395 );
1396 let function = |r: &_| {
1397 esurface.compute_self_and_r_derivative_subspace(
1398 r,
1399 &unit_shifted_momenta,
1400 ¢er,
1401 sample_in_common_lmb.external_moms(),
1402 &real_mass_vector,
1403 subspace,
1404 &lmbs,
1405 geometry,
1406 )
1407 };
1408 let zero = raw_radius_guess.zero();
1409 let mut radius_guess = raw_radius_guess.clone();
1410 if radius_guess.is_nan() || radius_guess.is_infinite() || radius_guess <= zero {
1411 radius_guess = e_cm.clone();
1412 }
1413 let identity = Self::variant_radial_root_identity(
1414 &graph.name,
1415 overlap_group_index,
1416 variant_id,
1417 raised_esurface_id,
1418 rotation,
1419 );
1420 let tolerance = F::from_f64(settings.subtraction.radial_root_residual_tolerance);
1421 let solution = match evaluation_metadata.radial_root_diagnostics.solve(
1422 &identity,
1423 &zero,
1424 &radius_guess,
1425 function,
1426 &tolerance,
1427 MAX_ITERATIONS,
1428 64,
1429 &e_cm,
1430 ) {
1431 Ok(solution) => solution,
1432 Err(error) => {
1433 evaluation_metadata.record_threshold_counterterm_error(format!(
1434 "amplitude graph '{}' threshold variant {} overlap group {} failed radial-root validation: {error}",
1435 graph.name, variant_id.0, overlap_group_index,
1436 ));
1437 radial_root_failed = true;
1438 continue;
1439 }
1440 };
1441 if radial_root_failed {
1442 continue;
1443 }
1444
1445 let root_loop_momenta = &unit_shifted_momenta
1446 .rescale(&solution.solution, subspace.as_subspace_simple())
1447 + ¢er;
1448 let mut root_in_common_lmb = sample_in_common_lmb.clone();
1449 root_in_common_lmb.sample.loop_moms = root_loop_momenta;
1450 let root_in_metadata_lmb =
1451 root_in_common_lmb.lmb_transform(common_lmb, &topology.loop_momentum_basis);
1452 let (local_multiplier, integrated_multiplier) = Self::evaluate_variant_multiplier(
1453 &mut self.threshold_multipliers,
1454 multiplier_workspace.as_mut(),
1455 variant_id,
1456 topology,
1457 &real_mass_vector,
1458 param_builder.model_values(),
1459 &additional_params,
1460 momentum_sample,
1461 &root_in_metadata_lmb,
1462 evaluation_metadata,
1463 )?;
1464 let both_zero = local_multiplier.is_zero() && integrated_multiplier.is_zero();
1465 let evaluation = if both_zero {
1466 EvaluatedAmplitudeThreshold::new(momentum_sample.zero(), record_components)
1467 .finish(&local_multiplier, &integrated_multiplier)
1468 } else {
1469 evaluate_generalized_rstar(
1470 graph,
1471 settings,
1472 param_builder,
1473 orientation,
1474 evaluation_metadata,
1475 &real_mass_vector,
1476 &lmbs,
1477 root_lmb,
1478 &root_in_common_lmb,
1479 ¢er,
1480 &unit_shifted_momenta,
1481 &radius,
1482 &solution,
1483 esurface,
1484 synthetic_esurface_id,
1485 raised_esurface_id,
1486 subspace,
1487 &thresholds,
1488 &threshold_subspaces,
1489 &threshold_masses,
1490 &overlap,
1491 overlap_group_index,
1492 max_prefactor_power,
1493 &mut self.variant_evaluators[variant_id],
1494 &mut self.variant_helper_evaluators[variant_id],
1495 &local_multiplier,
1496 &integrated_multiplier,
1497 record_components,
1498 )?
1499 };
1500 let value = evaluation.weighted.clone();
1501 append_amplitude_component_evaluations(
1502 &mut components,
1503 &evaluation,
1504 variant_id,
1505 raised_esurface_id,
1506 overlap_group_index,
1507 &local_multiplier,
1508 &integrated_multiplier,
1509 );
1510
1511 local_counterterms.push(AmplitudeLocalCountertermEvaluation {
1512 variant_id: Some(variant_id),
1513 esurface_id: raised_esurface_id,
1514 overlap_group: overlap_group_index,
1515 value: value.clone(),
1516 });
1517 total += value;
1518 }
1519 }
1520
1521 if radial_root_failed {
1522 return Ok(AmplitudeCountertermEvaluation {
1523 total: Complex::new_re(F::from_f64(f64::NAN)),
1524 local_counterterms,
1525 components: None,
1526 });
1527 }
1528 Ok(AmplitudeCountertermEvaluation {
1529 total,
1530 local_counterterms,
1531 components,
1532 })
1533 }
1534
1535 #[allow(clippy::too_many_arguments)]
1536 fn kinematics_for_variant_approach_grouped<T: FloatLike>(
1537 &self,
1538 momentum_sample: &MomentumSample<T>,
1539 graph: &Graph,
1540 model: &Model,
1541 esurfaces: &EsurfaceCollection,
1542 rotation: &Rotation,
1543 settings: &RuntimeSettings,
1544 ) -> Result<OverlapStructureWithKinematics<T>> {
1545 let catalogue = self.overlap_catalogue(graph, esurfaces);
1546 let mut groups = BTreeMap::new();
1547 for (instance, (_, _, metadata, _)) in catalogue.variants.iter().enumerate() {
1548 groups
1549 .entry((
1550 metadata.group_id,
1551 metadata.subspace.solve_signature(&catalogue.lmbs),
1552 ))
1553 .or_insert_with(Vec::new)
1554 .push(instance);
1555 }
1556 let mut combined = OverlapStructureWithKinematics {
1557 existing_esurfaces: ExistingEsurfaces::new(),
1558 variant_ids: Some(TiVec::new()),
1559 overlap_groups_with_kinematics: Vec::new(),
1560 };
1561 for (_, members) in groups {
1562 let mut group_result = self.kinematics_for_variant_approach(
1563 momentum_sample,
1564 graph,
1565 model,
1566 esurfaces,
1567 rotation,
1568 settings,
1569 &catalogue,
1570 &members,
1571 )?;
1572 let existing_offset = combined.existing_esurfaces.len();
1573 if let (Some(all_variants), Some(group_variants)) =
1574 (&mut combined.variant_ids, group_result.variant_ids.take())
1575 {
1576 all_variants.extend(group_variants);
1577 }
1578 combined
1579 .existing_esurfaces
1580 .extend(group_result.existing_esurfaces);
1581 for mut group in group_result.overlap_groups_with_kinematics {
1582 group.overlap_group.existing_esurfaces = group
1583 .overlap_group
1584 .existing_esurfaces
1585 .into_iter()
1586 .map(|id| ExistingEsurfaceId::from(usize::from(id) + existing_offset))
1587 .collect();
1588 group.overlap_group.complement = group
1589 .overlap_group
1590 .complement
1591 .into_iter()
1592 .map(|id| ExistingEsurfaceId::from(usize::from(id) + existing_offset))
1593 .collect();
1594 combined.overlap_groups_with_kinematics.push(group);
1595 }
1596 }
1597 Ok(combined)
1598 }
1599
1600 #[allow(clippy::too_many_arguments)]
1601 fn kinematics_for_variant_approach<T: FloatLike>(
1602 &self,
1603 momentum_sample: &MomentumSample<T>,
1604 graph: &Graph,
1605 model: &Model,
1606 _esurfaces: &EsurfaceCollection,
1607 rotation: &Rotation,
1608 settings: &RuntimeSettings,
1609 catalogue: &AmplitudeOverlapCatalogue,
1610 allowed_instances: &[usize],
1611 ) -> Result<OverlapStructureWithKinematics<T>> {
1612 if self.variant_evaluators.len() != self.variant_subspaces.len()
1613 || self.variant_evaluators.len() != self.variant_raised_esurfaces.len()
1614 || self.variant_evaluators.len() != self.variant_generated_mask.len()
1615 || self.variant_evaluators.len() != self.variant_active_mask.len()
1616 {
1617 return Err(eyre!(
1618 "Amplitude graph '{}' has inconsistent threshold-variant approach dimensions",
1619 graph.name,
1620 ));
1621 }
1622
1623 let PreparedAmplitudeOverlap {
1624 thresholds,
1625 threshold_subspaces,
1626 synthetic_variants,
1627 sample_in_common_lmb,
1628 lmbs,
1629 parent_lmb,
1630 overlap,
1631 ..
1632 } = catalogue.prepare_group(
1633 allowed_instances,
1634 momentum_sample,
1635 model,
1636 settings,
1637 rotation,
1638 )?;
1639 let topology = &catalogue.graphs[GraphGroupPosition::from(0)];
1640 let common_lmb = &lmbs[parent_lmb];
1641 let common_subspace = &catalogue.variants[allowed_instances[0]].2.subspace;
1642 let real_mass_vector = graph.get_real_mass_vector(model);
1643 let e_cm = F::from_f64(settings.kinematics.e_cm);
1644 let sample_f64 = momentum_sample_to_f64(&sample_in_common_lmb);
1645 let mut existing_esurfaces = ExistingEsurfaces::new();
1646 let mut variant_ids = TiVec::new();
1647 let mut local_ids = BTreeMap::new();
1648 for (synthetic_id, &instance) in synthetic_variants.iter_enumerated() {
1649 let (owner, variant_id, _, _) = &catalogue.variants[instance];
1650 if *owner != self.own_group_position {
1651 continue;
1652 }
1653 let raised = self.variant_raised_esurfaces[*variant_id];
1654 let group_surface = self
1655 .esurface_map
1656 .iter_enumerated()
1657 .find_map(|(id, map)| (map[*owner] == Some(raised)).then_some(id))
1658 .ok_or_else(|| {
1659 eyre!(
1660 "Amplitude graph '{}' variant {} has no group E-surface mapping",
1661 graph.name,
1662 variant_id.0
1663 )
1664 })?;
1665 local_ids.insert(
1666 ExistingEsurfaceId::from(synthetic_id.0),
1667 ExistingEsurfaceId::from(existing_esurfaces.len()),
1668 );
1669 existing_esurfaces.push(group_surface);
1670 variant_ids.push(*variant_id);
1671 }
1672 let mut radial_root_diagnostics = RadialRootDiagnostics::default();
1673 let mut overlap_groups_with_kinematics = Vec::with_capacity(overlap.overlap_groups.len());
1674 for (overlap_group_index, overlap_group) in overlap.overlap_groups.iter().enumerate() {
1675 let mut group_center_in_common_lmb = sample_f64.clone();
1676 for loop_index in common_subspace.iter_lmb_indices() {
1677 group_center_in_common_lmb.sample.loop_moms[loop_index] =
1678 overlap_group.center[loop_index];
1679 }
1680 let group_center = group_center_in_common_lmb
1681 .lmb_transform(common_lmb, &graph.loop_momentum_basis)
1682 .loop_moms()
1683 .clone();
1684 let mut loop_momenta_at_esurface = TiVec::new();
1685 let mut approach_centers_at_esurface = TiVec::new();
1686
1687 for &existing_esurface_id in &overlap_group.existing_esurfaces {
1688 let synthetic_esurface_id = overlap.existing_esurfaces[existing_esurface_id];
1689 let instance = synthetic_variants[synthetic_esurface_id];
1690 let (owner, variant_id, metadata, _) = &catalogue.variants[instance];
1691 if *owner != self.own_group_position {
1692 continue;
1693 }
1694 let variant_id = *variant_id;
1695 self.ensure_active_variant(variant_id)?;
1696 let raised_esurface_id = self.variant_raised_esurfaces[variant_id];
1697 let subspace = &threshold_subspaces[synthetic_esurface_id.0];
1698 let esurface = &thresholds[synthetic_esurface_id];
1699 let native_geometry = metadata.requested_parent_lmb.is_none();
1700 let geometry = if native_geometry { graph } else { topology };
1701 let root_lmb = if native_geometry {
1702 &graph.loop_momentum_basis
1703 } else {
1704 common_lmb
1705 };
1706
1707 let mut projected_center = overlap_group.center.cast::<T>();
1708 for loop_index in (0..projected_center.0.len()).map(LoopIndex::from) {
1709 if !subspace.contains_loop_index(loop_index) {
1710 projected_center[loop_index] = ThreeMomentum::new(
1711 momentum_sample.zero(),
1712 momentum_sample.zero(),
1713 momentum_sample.zero(),
1714 );
1715 }
1716 }
1717 let shifted_loop_momenta = sample_in_common_lmb.loop_moms() - &projected_center;
1718 let radius = shifted_loop_momenta
1719 .hyper_radius_squared(subspace.as_subspace_simple())
1720 .sqrt();
1721 let unit_shifted_momenta =
1722 shifted_loop_momenta.rescale(&radius.inv(), subspace.as_subspace_simple());
1723 let (raw_radius_guess, _) = esurface.get_radius_guess_subspace(
1724 &unit_shifted_momenta,
1725 sample_in_common_lmb.external_moms(),
1726 subspace,
1727 &lmbs,
1728 geometry,
1729 &real_mass_vector,
1730 );
1731 let function = |r: &_| {
1732 esurface.compute_self_and_r_derivative_subspace(
1733 r,
1734 &unit_shifted_momenta,
1735 &projected_center,
1736 sample_in_common_lmb.external_moms(),
1737 &real_mass_vector,
1738 subspace,
1739 &lmbs,
1740 geometry,
1741 )
1742 };
1743 let zero = raw_radius_guess.zero();
1744 let mut radius_guess = raw_radius_guess.clone();
1745 if radius_guess.is_nan() || radius_guess.is_infinite() || radius_guess <= zero {
1746 radius_guess = e_cm.clone();
1747 }
1748 let identity = Self::variant_radial_root_identity(
1749 &graph.name,
1750 overlap_group_index,
1751 variant_id,
1752 raised_esurface_id,
1753 rotation,
1754 );
1755 let tolerance = F::from_f64(settings.subtraction.radial_root_residual_tolerance);
1756 let solution = radial_root_diagnostics
1757 .solve(
1758 &identity,
1759 &zero,
1760 &radius_guess,
1761 function,
1762 &tolerance,
1763 MAX_ITERATIONS,
1764 64,
1765 &e_cm,
1766 )
1767 .map_err(|error| {
1768 eyre!(
1769 "Could not construct threshold-counterterm approach kinematics for amplitude graph '{}' variant {} raised E-surface {} in overlap group {}: {error}",
1770 graph.name,
1771 variant_id.0,
1772 raised_esurface_id.0,
1773 overlap_group_index,
1774 )
1775 })?;
1776
1777 let root_loop_momenta = &unit_shifted_momenta
1778 .rescale(&solution.solution, subspace.as_subspace_simple())
1779 + &projected_center;
1780 let mut root_in_common_lmb = sample_in_common_lmb.clone();
1781 root_in_common_lmb.sample.loop_moms = root_loop_momenta;
1782 let root_in_generation_lmb =
1783 root_in_common_lmb.lmb_transform(root_lmb, &graph.loop_momentum_basis);
1784
1785 let mut full_center_in_common_lmb = sample_in_common_lmb.clone();
1786 for loop_index in subspace.iter_lmb_indices() {
1787 full_center_in_common_lmb.sample.loop_moms[loop_index] =
1788 projected_center[loop_index].clone();
1789 }
1790 let full_center_in_generation_lmb = full_center_in_common_lmb
1791 .lmb_transform(root_lmb, &graph.loop_momentum_basis)
1792 .loop_moms()
1793 .clone();
1794
1795 loop_momenta_at_esurface.push(Some(root_in_generation_lmb));
1796 approach_centers_at_esurface.push(Some(full_center_in_generation_lmb));
1797 }
1798
1799 if loop_momenta_at_esurface.is_empty() {
1801 continue;
1802 }
1803 overlap_groups_with_kinematics.push(OverlapGroupWithKinematics {
1804 overlap_group: OverlapGroup {
1805 existing_esurfaces: overlap_group
1806 .existing_esurfaces
1807 .iter()
1808 .filter_map(|id| local_ids.get(id).copied())
1809 .collect(),
1810 complement: overlap_group
1811 .complement
1812 .iter()
1813 .filter_map(|id| local_ids.get(id).copied())
1814 .collect(),
1815 center: group_center,
1816 prefactor_evaluator: None,
1817 },
1818 loop_momenta_at_esurface,
1819 approach_centers_at_esurface: Some(approach_centers_at_esurface),
1820 });
1821 }
1822
1823 Ok(OverlapStructureWithKinematics {
1824 existing_esurfaces,
1825 variant_ids: Some(variant_ids),
1826 overlap_groups_with_kinematics,
1827 })
1828 }
1829
1830 #[allow(clippy::too_many_arguments)]
1831 pub fn kinematics_for_approach<T: FloatLike>(
1832 &mut self,
1833 momentum_sample: &MomentumSample<T>,
1834 graph: &Graph,
1835 model: &Model,
1836 esurfaces: &EsurfaceCollection,
1837 rotation: &Rotation,
1838 settings: &RuntimeSettings,
1839 ) -> Result<OverlapStructureWithKinematics<T>> {
1840 if !self.legacy_equivalent {
1841 return self.kinematics_for_variant_approach_grouped(
1842 momentum_sample,
1843 graph,
1844 model,
1845 esurfaces,
1846 rotation,
1847 settings,
1848 );
1849 }
1850
1851 let counter_term_builder = CounterTermBuilder::new(
1852 graph,
1853 model,
1854 rotation,
1855 settings,
1856 esurfaces,
1857 momentum_sample,
1858 &self.overlap,
1859 &self.raised_data,
1860 self.own_group_position,
1861 &self.esurface_map,
1862 );
1863
1864 let mut overlap_sturcture_with_kinematics = OverlapStructureWithKinematics {
1865 existing_esurfaces: self.overlap.existing_esurfaces.clone(),
1866 variant_ids: None,
1867 overlap_groups_with_kinematics: Vec::new(),
1868 };
1869 let mut radial_root_diagnostics = RadialRootDiagnostics::default();
1870
1871 for (overlap_group, group) in self.overlap.overlap_groups.iter().enumerate() {
1872 let overlap_builder = counter_term_builder.new_overlap_builder(group);
1873
1874 let mut loop_momenta_at_esurface: TiVec<ExistingEsurfaceId, Option<MomentumSample<T>>> =
1875 ti_vec![];
1876 for existing_esurface_id in group.existing_esurfaces.iter() {
1877 let single_result = overlap_builder
1878 .new_esurface_builder(*existing_esurface_id)
1879 .map(|esurface_builder| -> Result<_> {
1880 self.ensure_active_raised_esurface(esurface_builder.raised_esurface_id)?;
1881 let raised_esurface_id = esurface_builder.raised_esurface_id;
1882 let radial_root_identity = Self::radial_root_identity(
1883 &graph.name,
1884 overlap_group,
1885 raised_esurface_id,
1886 rotation,
1887 );
1888 let rstar_sample = esurface_builder
1889 .solve_rstar(
1890 &radial_root_identity,
1891 &mut radial_root_diagnostics,
1892 )
1893 .ok_or_else(|| {
1894 eyre!(
1895 "Could not construct threshold-counterterm kinematics for raised E-surface {} in probe rotation {}",
1896 raised_esurface_id.0,
1897 rotation.method,
1898 )
1899 })?
1900 .rstar_samples();
1901 Result::Ok(rstar_sample.rstar_sample)
1902 })
1903 .transpose()?;
1904
1905 loop_momenta_at_esurface.push(single_result);
1906 }
1907
1908 overlap_sturcture_with_kinematics
1909 .overlap_groups_with_kinematics
1910 .push(OverlapGroupWithKinematics {
1911 overlap_group: group.clone(),
1912 loop_momenta_at_esurface,
1913 approach_centers_at_esurface: None,
1914 });
1915 }
1916
1917 Ok(overlap_sturcture_with_kinematics)
1918 }
1919}
1920
1921pub struct OverlapGroupWithKinematics<T: FloatLike> {
1922 pub overlap_group: OverlapGroup,
1923 pub loop_momenta_at_esurface: TiVec<ExistingEsurfaceId, Option<MomentumSample<T>>>,
1924 pub approach_centers_at_esurface: Option<TiVec<ExistingEsurfaceId, Option<LoopMomenta<F<T>>>>>,
1927}
1928
1929pub struct OverlapStructureWithKinematics<T: FloatLike> {
1930 pub existing_esurfaces: ExistingEsurfaces,
1931 pub variant_ids: Option<TiVec<ExistingEsurfaceId, ThresholdCountertermVariantId>>,
1934 pub overlap_groups_with_kinematics: Vec<OverlapGroupWithKinematics<T>>,
1935}
1936
1937fn momentum_sample_to_f64<T: FloatLike>(sample: &MomentumSample<T>) -> MomentumSample<f64> {
1938 MomentumSample {
1939 sample: BareMomentumSample {
1940 loop_moms: sample
1941 .loop_moms()
1942 .iter()
1943 .map(|momentum| momentum.map_ref(&F::into_ff64))
1944 .collect(),
1945 dual_loop_moms: None,
1946 loop_mom_cache_id: sample.sample.loop_mom_cache_id,
1947 loop_mom_base_cache_id: sample.sample.loop_mom_base_cache_id,
1948 external_moms: sample
1949 .external_moms()
1950 .iter()
1951 .map(|momentum| momentum.map_ref(&F::into_ff64))
1952 .collect(),
1953 external_mom_cache_id: sample.sample.external_mom_cache_id,
1954 external_mom_base_cache_id: sample.sample.external_mom_base_cache_id,
1955 jacobian: sample.sample.jacobian.into_ff64(),
1956 orientation: sample.sample.orientation,
1957 parameterization_branch: sample.sample.parameterization_branch,
1958 },
1959 }
1960}
1961
1962fn dualize_external_momenta<T: FloatLike>(
1963 shape: &HyperDual<F<T>>,
1964 external_moms: &ExternalFourMomenta<F<T>>,
1965) -> ExternalFourMomenta<HyperDual<F<T>>> {
1966 external_moms
1967 .iter()
1968 .map(|momentum| momentum.map_ref(&|component| new_constant(shape, component)))
1969 .collect()
1970}
1971
1972fn real_dual_to_complex<T: FloatLike>(dual: HyperDual<F<T>>) -> HyperDual<Complex<F<T>>> {
1973 let shape = dual
1974 .get_shape()
1975 .iter()
1976 .map(|entry| entry.to_vec())
1977 .collect();
1978 let values = dual.values.into_iter().map(Complex::new_re).collect();
1979 HyperDual::from_values(shape, values)
1980}
1981
1982fn repeated_product<T>(value: T, power: usize, one: T) -> T
1983where
1984 T: Clone + std::ops::Mul<T, Output = T>,
1985{
1986 (0..power).fold(one, |product, _| product * value.clone())
1987}
1988
1989#[allow(clippy::too_many_arguments)]
1990fn evaluate_generalized_multichanneling_prefactor<T: FloatLike>(
1991 momentum_sample: &MomentumSample<T>,
1992 threshold_masses: &[EdgeVec<F<T>>],
1993 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
1994 thresholds: &EsurfaceCollection,
1995 threshold_subspaces: &[SubspaceData],
1996 overlap: &SubspaceOverlapStructure,
1997 overlap_group_index: usize,
1998 power: usize,
1999) -> DualOrNot<Complex<F<T>>> {
2000 if overlap.overlap_groups.len() < 2 {
2001 return DualOrNot::NonDual(Complex::new_re(momentum_sample.one()));
2002 }
2003
2004 if let Some(dual_loop_momenta) = &momentum_sample.sample.dual_loop_moms {
2005 let shape = &dual_loop_momenta[LoopIndex(0)].px;
2006 let dual_external_momenta =
2007 dualize_external_momenta(shape, momentum_sample.external_moms());
2008 let group_products = overlap
2009 .overlap_groups
2010 .iter()
2011 .map(|group| {
2012 group.complement.iter().fold(
2013 new_constant(shape, &momentum_sample.one()),
2014 |product, &existing_esurface_id| {
2015 let esurface_id = overlap.existing_esurfaces[existing_esurface_id];
2016 let subspace = &threshold_subspaces[esurface_id.0];
2017 let value = thresholds[esurface_id].compute_from_dual_momenta(
2018 subspace.get_lmb(all_lmbs),
2019 &threshold_masses[esurface_id.0],
2020 dual_loop_momenta,
2021 &dual_external_momenta,
2022 );
2023 product
2024 * repeated_product(
2025 value,
2026 power,
2027 new_constant(shape, &momentum_sample.one()),
2028 )
2029 },
2030 )
2031 })
2032 .collect::<Vec<_>>();
2033 let denominator = group_products
2034 .iter()
2035 .skip(1)
2036 .fold(group_products[0].clone(), |sum, product| sum + product);
2037 return DualOrNot::Dual(real_dual_to_complex(
2038 group_products[overlap_group_index].clone() / denominator,
2039 ));
2040 }
2041
2042 let group_products = overlap
2043 .overlap_groups
2044 .iter()
2045 .map(|group| {
2046 group
2047 .complement
2048 .iter()
2049 .map(|&existing_esurface_id| {
2050 let esurface_id = overlap.existing_esurfaces[existing_esurface_id];
2051 let subspace = &threshold_subspaces[esurface_id.0];
2052 let value = thresholds[esurface_id].compute_from_momenta(
2053 subspace.get_lmb(all_lmbs),
2054 &threshold_masses[esurface_id.0],
2055 momentum_sample.loop_moms(),
2056 momentum_sample.external_moms(),
2057 );
2058 repeated_product(value, power, momentum_sample.one())
2059 })
2060 .fold(momentum_sample.one(), |product, value| product * value)
2061 })
2062 .collect::<Vec<_>>();
2063 let denominator = group_products
2064 .iter()
2065 .skip(1)
2066 .fold(group_products[0].clone(), |sum, value| sum + value);
2067 DualOrNot::NonDual(Complex::new_re(
2068 group_products[overlap_group_index].clone() / denominator,
2069 ))
2070}
2071
2072#[allow(clippy::too_many_arguments)]
2073fn evaluate_generalized_rstar<T: FloatLike>(
2074 graph: &Graph,
2075 settings: &RuntimeSettings,
2076 param_builder: &mut ParamBuilder<f64>,
2077 orientations: SingleOrAllOrientations<'_, OrientationID>,
2078 evaluation_metadata: &mut EvaluationMetaData,
2079 real_mass_vector: &EdgeVec<F<T>>,
2080 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
2081 common_lmb: &LoopMomentumBasis,
2082 root_sample: &MomentumSample<T>,
2083 center: &LoopMomenta<F<T>>,
2084 unit_shifted_momenta: &LoopMomenta<F<T>>,
2085 radius: &F<T>,
2086 solution: &NewtonIterationResult<T>,
2087 esurface: &Esurface,
2088 esurface_id: EsurfaceID,
2089 raised_esurface_id: RaisedEsurfaceId,
2090 subspace: &SubspaceData,
2091 thresholds: &EsurfaceCollection,
2092 threshold_subspaces: &[SubspaceData],
2093 threshold_masses: &[EdgeVec<F<T>>],
2094 overlap: &SubspaceOverlapStructure,
2095 overlap_group_index: usize,
2096 prefactor_power: usize,
2097 ct_evaluator: &mut AmplitudeCountertermEvaluator,
2098 helper_evaluators: &mut [GenericEvaluator],
2099 local_multiplier: &F<T>,
2100 integrated_multiplier: &F<T>,
2101 record_components: bool,
2102) -> Result<EvaluatedAmplitudeThreshold<T>> {
2103 let radius_star = &solution.solution;
2104 let e_cm = F::from_f64(settings.kinematics.e_cm);
2105 let uv_settings = &settings.subtraction.local_ct_settings.uv_localisation;
2106 let integrated_settings = &settings.subtraction.integrated_ct_settings;
2107 let uv_damp_plus = evaluate_uv_damper(radius, radius_star, &e_cm, uv_settings);
2108 let uv_damp_minus = evaluate_uv_damper(&-radius, radius_star, &e_cm, uv_settings);
2109 let h_function =
2110 evaluate_integrated_ct_normalisation(radius, radius_star, &e_cm, integrated_settings);
2111 let mut evaluation = EvaluatedAmplitudeThreshold::new(root_sample.zero(), record_components);
2112
2113 for (cut_cff_index, evaluator_stack) in &mut ct_evaluator.evaluator_stacks {
2114 let order_index = cut_cff_index.left_threshold_order.unwrap() - 1;
2115 let (sample_for_order_in_common_lmb, threshold_params) = if order_index == 0 {
2116 (
2117 root_sample.clone(),
2118 ThresholdParams {
2119 radius: DualOrNot::NonDual(radius.clone()),
2120 radius_star: DualOrNot::NonDual(radius_star.clone()),
2121 esurface_derivative: DualOrNot::NonDual(
2122 solution.derivative_at_solution.clone(),
2123 ),
2124 uv_damp_plus: DualOrNot::NonDual(uv_damp_plus.clone()),
2125 uv_damp_minus: DualOrNot::NonDual(uv_damp_minus.clone()),
2126 h_function: DualOrNot::NonDual(h_function.clone()),
2127 },
2128 )
2129 } else {
2130 let dual_shape = HyperDual::<F<T>>::new(simple_n_deriv_shape(order_index));
2131 let dual_radius_star = dual_shape.variable(0, radius_star.clone());
2132 let dual_center = center
2133 .iter()
2134 .map(|momentum| momentum.map_ref(&|value| new_constant(&dual_radius_star, value)))
2135 .collect::<LoopMomenta<_>>();
2136 let dual_loop_momenta = unit_shifted_momenta
2137 .rescale_with_hyper_dual(&dual_radius_star, subspace.as_subspace_simple())
2138 .iter()
2139 .zip(dual_center.iter())
2140 .map(|(momentum, center)| momentum.clone() + center.clone())
2141 .collect::<LoopMomenta<_>>();
2142 let mut sample = root_sample.clone();
2143 sample.sample.dual_loop_moms = Some(dual_loop_momenta);
2144 (
2145 sample,
2146 ThresholdParams {
2147 radius: DualOrNot::Dual(new_constant(&dual_radius_star, radius)),
2148 radius_star: DualOrNot::Dual(dual_radius_star.clone()),
2149 esurface_derivative: DualOrNot::Dual(new_constant(
2150 &dual_radius_star,
2151 &solution.derivative_at_solution,
2152 )),
2153 uv_damp_plus: DualOrNot::Dual(new_constant(&dual_radius_star, &uv_damp_plus)),
2154 uv_damp_minus: DualOrNot::Dual(new_constant(&dual_radius_star, &uv_damp_minus)),
2155 h_function: DualOrNot::Dual(new_constant(&dual_radius_star, &h_function)),
2156 },
2157 )
2158 };
2159
2160 let esurface_derivatives = if order_index == 0 {
2161 DualOrNot::NonDual(solution.derivative_at_solution.clone())
2162 } else {
2163 let dual_shape = HyperDual::<F<T>>::new(simple_n_deriv_shape(order_index + 1));
2164 let dual_radius_star = dual_shape.variable(0, radius_star.clone());
2165 let dual_center = center
2166 .iter()
2167 .map(|momentum| momentum.map_ref(&|value| new_constant(&dual_radius_star, value)))
2168 .collect::<LoopMomenta<_>>();
2169 let dual_loop_momenta = unit_shifted_momenta
2170 .rescale_with_hyper_dual(&dual_radius_star, subspace.as_subspace_simple())
2171 .iter()
2172 .zip(dual_center.iter())
2173 .map(|(momentum, center)| momentum.clone() + center.clone())
2174 .collect::<LoopMomenta<_>>();
2175 let dual_externals =
2176 dualize_external_momenta(&dual_radius_star, root_sample.external_moms());
2177 DualOrNot::Dual(esurface.compute_from_dual_momenta(
2178 common_lmb,
2179 real_mass_vector,
2180 &dual_loop_momenta,
2181 &dual_externals,
2182 ))
2183 };
2184
2185 let sample_for_order =
2186 sample_for_order_in_common_lmb.lmb_transform(common_lmb, &graph.loop_momentum_basis);
2187 let params = T::get_parameters(
2188 param_builder,
2189 (false, false),
2190 graph,
2191 &sample_for_order,
2192 settings.kinematics.externals.get_helicities(),
2193 &settings.additional_params(),
2194 Some(&threshold_params),
2195 None,
2196 None,
2197 );
2198 let pass_one = evaluator_stack
2199 .evaluate(params, orientations, settings, evaluation_metadata)
2200 .expect("Amplitude variant counterterm evaluator stack failed")
2201 .pop()
2202 .unwrap();
2203 let prefactor = evaluate_generalized_multichanneling_prefactor(
2204 &sample_for_order_in_common_lmb,
2205 threshold_masses,
2206 all_lmbs,
2207 thresholds,
2208 threshold_subspaces,
2209 overlap,
2210 overlap_group_index,
2211 prefactor_power,
2212 );
2213 let pass_one = multiply_dual_or_not_complex(pass_one, &prefactor);
2214
2215 let mut helper_params = Vec::new();
2216 match pass_one {
2217 DualOrNot::Dual(dual) => {
2218 helper_params.extend_from_slice(&extract_t_derivatives_complex(dual));
2219 }
2220 DualOrNot::NonDual(value) => helper_params.push(value),
2221 }
2222 match esurface_derivatives {
2223 DualOrNot::Dual(dual) => {
2224 helper_params.extend(
2225 extract_t_derivatives(dual)[1..]
2226 .iter()
2227 .cloned()
2228 .map(Complex::new_re),
2229 );
2230 }
2231 DualOrNot::NonDual(value) => helper_params.push(Complex::new_re(value)),
2232 }
2233 helper_params.push(Complex::new_re(radius.clone()));
2234 helper_params.push(Complex::new_re(radius_star.clone()));
2235 helper_params.push(Complex::new_re(uv_damp_plus.clone()));
2236 helper_params.push(Complex::new_re(uv_damp_minus.clone()));
2237 helper_params.push(Complex::new_re(h_function.clone()));
2238
2239 let mut pieces = evaluate_evaluator(
2240 &mut helper_evaluators[order_index],
2241 &helper_params,
2242 evaluation_metadata,
2243 );
2244 assert_eq!(
2245 pieces.len(),
2246 2,
2247 "amplitude threshold helper must return local and integrated pieces",
2248 );
2249 let integrated = pieces.pop().unwrap().unwrap_real();
2250 let local = pieces.pop().unwrap().unwrap_real();
2251 evaluation.add_completed_pieces(local, integrated, local_multiplier, integrated_multiplier);
2252 }
2253
2254 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
2255 stage = "amplitude_threshold_variant_total",
2256 graph = %graph.name,
2257 esurface_id = esurface_id.0,
2258 raised_esurface_id = raised_esurface_id.0,
2259 overlap_group = overlap_group_index,
2260 result = %format!("{:+16e}", evaluation.weighted),
2261 "amplitude threshold variant total"
2262 );
2263 Ok(evaluation.finish(local_multiplier, integrated_multiplier))
2264}
2265
2266struct CounterTermBuilder<'a, T: FloatLike> {
2267 overlap_structure: &'a OverlapStructure,
2268 raised_data: &'a RaisedEsurfaceData,
2269 own_group_position: GraphGroupPosition,
2270 esurface_map: &'a TiVec<GroupEsurfaceId, TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>>,
2271 real_mass_vector: EdgeVec<F<T>>,
2272 e_cm: F<T>,
2273 graph: &'a Graph,
2274 rotation_for_overlap: &'a Rotation,
2275 settings: &'a RuntimeSettings,
2276 esurface_collection: &'a EsurfaceCollection,
2277 sample: &'a MomentumSample<T>,
2278}
2279
2280impl<'a, T: FloatLike> CounterTermBuilder<'a, T> {
2281 #[allow(clippy::too_many_arguments)]
2282 fn new(
2283 graph: &'a Graph,
2284 model: &'a Model,
2285 rotation_for_overlap: &'a Rotation,
2286 settings: &'a RuntimeSettings,
2287 esurface_collection: &'a EsurfaceCollection,
2288 sample: &'a MomentumSample<T>,
2289 overlap_structure: &'a OverlapStructure,
2290 raised_data: &'a RaisedEsurfaceData,
2291 own_group_position: GraphGroupPosition,
2292 esurface_map: &'a TiVec<
2293 GroupEsurfaceId,
2294 TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>,
2295 >,
2296 ) -> Self {
2297 let real_mass_vector = graph.get_real_mass_vector(model);
2298 let e_cm = F::from_f64(settings.kinematics.e_cm);
2299
2300 Self {
2301 real_mass_vector,
2302 e_cm,
2303 graph,
2304 rotation_for_overlap,
2305 settings,
2306 esurface_collection,
2307 overlap_structure,
2308 raised_data,
2309 sample,
2310 own_group_position,
2311 esurface_map,
2312 }
2313 }
2314
2315 fn new_overlap_builder(&'a self, overlap_group: &'a OverlapGroup) -> OverlapBuilder<'a, T> {
2316 let center = &overlap_group.center;
2317
2318 let (unrotated_center, rotated_center) = (
2321 center.cast(),
2322 center.rotate(self.rotation_for_overlap).cast(),
2323 );
2324
2325 let shifted_loop_momenta = self.sample.loop_moms() - &rotated_center;
2326 let radius = shifted_loop_momenta.hyper_radius_squared(None).sqrt();
2327 let unit_shifted_momenta = shifted_loop_momenta.rescale(&radius.inv(), None);
2328
2329 OverlapBuilder {
2330 counterterm_builder: self,
2331 overlap_group,
2332 rotated_center,
2333 _unrotated_center: unrotated_center,
2334 unit_shifted_momenta,
2335 radius,
2336 }
2337 }
2338}
2339
2340struct OverlapBuilder<'a, T: FloatLike> {
2341 counterterm_builder: &'a CounterTermBuilder<'a, T>,
2342 overlap_group: &'a OverlapGroup,
2343 rotated_center: LoopMomenta<F<T>>,
2345 _unrotated_center: LoopMomenta<F<T>>,
2347 unit_shifted_momenta: LoopMomenta<F<T>>,
2348 radius: F<T>,
2349}
2350
2351impl<'a, T: FloatLike> OverlapBuilder<'a, T> {
2352 fn new_esurface_builder(
2353 &'a self,
2354 existing_esurface_id: ExistingEsurfaceId,
2355 ) -> Option<EsurfaceCTBuilder<'a, T>> {
2356 let group_esurface_id = self
2357 .counterterm_builder
2358 .overlap_structure
2359 .existing_esurfaces[existing_esurface_id];
2360
2361 let raised_esurface_id = self.counterterm_builder.esurface_map[group_esurface_id]
2362 [self.counterterm_builder.own_group_position];
2363
2364 raised_esurface_id.map(|raised_esurface_id| {
2365 let esurface_id = self.counterterm_builder.raised_data.raised_groups
2366 [raised_esurface_id]
2367 .esurface_ids[0];
2368
2369 EsurfaceCTBuilder {
2370 overlap_builder: self,
2371 _existing_esurface_id: existing_esurface_id,
2372 group_esurface_id,
2373 esurface: &self.counterterm_builder.esurface_collection[esurface_id],
2374 esurface_id,
2375 raised_esurface_id,
2376 }
2377 })
2378 }
2379}
2380
2381struct EsurfaceCTBuilder<'a, T: FloatLike> {
2382 overlap_builder: &'a OverlapBuilder<'a, T>,
2383 _existing_esurface_id: ExistingEsurfaceId,
2384 group_esurface_id: GroupEsurfaceId,
2385 esurface: &'a Esurface,
2386 esurface_id: EsurfaceID,
2387 raised_esurface_id: RaisedEsurfaceId,
2388}
2389
2390impl<'a, T: FloatLike> EsurfaceCTBuilder<'a, T> {
2391 fn solve_rstar(
2392 self,
2393 radial_root_identity: &RadialRootIdentity,
2394 radial_root_diagnostics: &mut RadialRootDiagnostics,
2395 ) -> Option<RstarSolution<'a, T>> {
2396 let mut all_center_values_valid = true;
2397 let center_surface_values = self
2398 .overlap_builder
2399 .overlap_group
2400 .existing_esurfaces
2401 .iter()
2402 .map(|&existing_esurface_id| {
2403 let group_esurface_id = self
2404 .overlap_builder
2405 .counterterm_builder
2406 .overlap_structure
2407 .existing_esurfaces[existing_esurface_id];
2408
2409 match self.overlap_builder.counterterm_builder.esurface_map[group_esurface_id]
2410 [self.overlap_builder.counterterm_builder.own_group_position]
2411 {
2412 Some(raised_esurface_id) => {
2413 let esurface_id = self
2414 .overlap_builder
2415 .counterterm_builder
2416 .raised_data
2417 .raised_groups[raised_esurface_id]
2418 .esurface_ids[0];
2419 let esurface =
2420 &self.overlap_builder.counterterm_builder.esurface_collection
2421 [esurface_id];
2422 let value = esurface.compute_from_momenta(
2423 &self
2424 .overlap_builder
2425 .counterterm_builder
2426 .graph
2427 .loop_momentum_basis,
2428 &self.overlap_builder.counterterm_builder.real_mass_vector,
2429 &self.overlap_builder.rotated_center,
2430 self.overlap_builder
2431 .counterterm_builder
2432 .sample
2433 .external_moms(),
2434 );
2435 let is_valid = esurface_value_is_strictly_inside(
2436 &value,
2437 &self.overlap_builder.counterterm_builder.e_cm,
2438 );
2439 all_center_values_valid &= is_valid;
2440 format!(
2441 "existing={} group={} raised={} local={} edges={:?} value={:+16e} inside={}",
2442 usize::from(existing_esurface_id),
2443 group_esurface_id.0,
2444 raised_esurface_id.0,
2445 esurface_id.0,
2446 esurface.energies,
2447 value,
2448 is_valid
2449 )
2450 }
2451 None => {
2452 format!(
2453 "existing={} group={} absent_in_current_graph",
2454 usize::from(existing_esurface_id),
2455 group_esurface_id.0
2456 )
2457 }
2458 }
2459 })
2460 .collect::<Vec<_>>();
2461 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect, #center;
2462 stage = "amplitude_threshold_center_values",
2463 graph = %self.overlap_builder.counterterm_builder.graph.name,
2464 selected_existing_esurface_id = usize::from(self._existing_esurface_id),
2465 selected_group_esurface_id = self.group_esurface_id.0,
2466 selected_raised_esurface_id = self.raised_esurface_id.0,
2467 selected_esurface_id = self.esurface_id.0,
2468 rotation_id = %self.overlap_builder.counterterm_builder.rotation_for_overlap.method,
2469 center_provenance = "identity_frame_rotated_once",
2470 overlap_group_size = self.overlap_builder.overlap_group.existing_esurfaces.len(),
2471 radius = %format!("{:+16e}", self.overlap_builder.radius),
2472 file.rotated_center = %format!("{}", self.overlap_builder.rotated_center),
2473 file.surface_values = %center_surface_values.join("\n"),
2474 "amplitude threshold center values"
2475 );
2476
2477 if !all_center_values_valid {
2478 warn!(
2479 graph = %self.overlap_builder.counterterm_builder.graph.name,
2480 selected_esurface_id = self.esurface_id.0,
2481 rotation_id = %self.overlap_builder.counterterm_builder.rotation_for_overlap.method,
2482 center_provenance = "identity_frame_rotated_once",
2483 center = %self.overlap_builder.rotated_center,
2484 surface_values = %center_surface_values.join("; "),
2485 "refusing to evaluate an amplitude threshold counterterm with an invalid probe-frame overlap center"
2486 );
2487 return None;
2488 }
2489
2490 let (raw_radius_guess, _) = self.esurface.get_radius_guess(
2491 &self.overlap_builder.unit_shifted_momenta,
2492 self.overlap_builder
2493 .counterterm_builder
2494 .sample
2495 .external_moms(),
2496 &self
2497 .overlap_builder
2498 .counterterm_builder
2499 .graph
2500 .loop_momentum_basis,
2501 );
2502
2503 let function = |r: &_| {
2504 self.esurface.compute_self_and_r_derivative(
2505 r,
2506 &self.overlap_builder.unit_shifted_momenta,
2507 &self.overlap_builder.rotated_center,
2508 self.overlap_builder
2509 .counterterm_builder
2510 .sample
2511 .external_moms(),
2512 &self.overlap_builder.counterterm_builder.real_mass_vector,
2513 &self
2514 .overlap_builder
2515 .counterterm_builder
2516 .graph
2517 .loop_momentum_basis,
2518 )
2519 };
2520
2521 let zero = raw_radius_guess.zero();
2522 let mut radius_guess = raw_radius_guess.clone();
2523 if radius_guess.is_nan() || radius_guess.is_infinite() || radius_guess <= zero {
2524 radius_guess = self.overlap_builder.counterterm_builder.e_cm.clone();
2525 }
2526 let tolerance = F::from_f64(
2527 self.overlap_builder
2528 .counterterm_builder
2529 .settings
2530 .subtraction
2531 .radial_root_residual_tolerance,
2532 );
2533 let solution = match radial_root_diagnostics.solve(
2534 radial_root_identity,
2535 &zero,
2536 &radius_guess,
2537 function,
2538 &tolerance,
2539 MAX_ITERATIONS,
2540 64,
2541 &self.overlap_builder.counterterm_builder.e_cm,
2542 ) {
2543 Ok(solution) => solution,
2544 Err(error) => {
2545 warn!(
2546 graph = %self.overlap_builder.counterterm_builder.graph.name,
2547 esurface_id = self.esurface_id.0,
2548 rotation_id = %self.overlap_builder.counterterm_builder.rotation_for_overlap.method,
2549 center_provenance = "identity_frame_rotated_once",
2550 raw_radius_guess = %raw_radius_guess,
2551 radius_guess = %radius_guess,
2552 error = %error,
2553 "refusing to evaluate an amplitude threshold counterterm with an invalid radial solution"
2554 );
2555 return None;
2556 }
2557 };
2558 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
2559 stage = "amplitude_threshold_rstar_solution",
2560 graph = %self.overlap_builder.counterterm_builder.graph.name,
2561 existing_esurface_id = %self._existing_esurface_id,
2562 group_esurface_id = self.group_esurface_id.0,
2563 raised_esurface_id = self.raised_esurface_id.0,
2564 esurface_id = self.esurface_id.0,
2565 rotation_id = %self.overlap_builder.counterterm_builder.rotation_for_overlap.method,
2566 center_provenance = "identity_frame_rotated_once",
2567 radius_guess = %format!("{:+16e}", radius_guess),
2568 radius_star = %format!("{:+16e}", solution.solution),
2569 derivative = %format!("{:+16e}", solution.derivative_at_solution),
2570 error = %format!("{:+16e}", solution.error_of_function),
2571 iterations = solution.num_iterations_used,
2572 nonfinite = false,
2573 "amplitude threshold rstar solution"
2574 );
2575
2576 Some(RstarSolution {
2577 esurface_ct_builder: self,
2578 solution,
2579 })
2580 }
2581}
2582
2583struct RstarSolution<'a, T: FloatLike> {
2584 esurface_ct_builder: EsurfaceCTBuilder<'a, T>,
2585 solution: NewtonIterationResult<T>,
2586}
2587
2588impl<'a, T: FloatLike> RstarSolution<'a, T> {
2589 fn rstar_samples(self) -> RstarSample<'a, T> {
2590 let rstar_loop_momenta = &self
2591 .esurface_ct_builder
2592 .overlap_builder
2593 .unit_shifted_momenta
2594 .rescale(&self.solution.solution, None)
2595 + &self.esurface_ct_builder.overlap_builder.rotated_center;
2596
2597 let mut rstar_sample = self
2598 .esurface_ct_builder
2599 .overlap_builder
2600 .counterterm_builder
2601 .sample
2602 .clone();
2603
2604 rstar_sample.sample.loop_moms = rstar_loop_momenta;
2605
2606 RstarSample {
2607 rstar_solution: self,
2608 rstar_sample,
2609 }
2610 }
2611}
2612
2613struct RstarSample<'a, T: FloatLike> {
2614 rstar_solution: RstarSolution<'a, T>,
2615 rstar_sample: MomentumSample<T>,
2616}
2617
2618impl<'a, T: FloatLike> RstarSample<'a, T> {
2619 #[allow(clippy::too_many_arguments)]
2620 fn evaluate<'b, 'c: 'b>(
2621 self,
2622 param_builder: &mut ParamBuilder<f64>,
2623 orientations: SingleOrAllOrientations<'a, OrientationID>,
2624 evaluation_metadata: &mut EvaluationMetaData,
2625 ct_evaluator: &mut AmplitudeCountertermEvaluator,
2626 helper_evaluators: &mut [GenericEvaluator],
2627 split_helpers: bool,
2628 record_components: bool,
2629 ) -> Result<EvaluatedAmplitudeThreshold<T>> {
2630 assert!(
2631 split_helpers || !record_components,
2632 "recorded legacy amplitude components require split helpers",
2633 );
2634 let esurface_ct_builder = &self.rstar_solution.esurface_ct_builder;
2635 let ct_builder = esurface_ct_builder.overlap_builder.counterterm_builder;
2636
2637 let esurface_id = esurface_ct_builder.esurface_id;
2638
2639 let model_params = param_builder
2640 .model_values()
2641 .iter()
2642 .map(|c| Complex::new(F::from_ff64(c.re), F::from_ff64(c.im)))
2643 .collect::<Vec<_>>();
2644
2645 let radius = self
2646 .rstar_solution
2647 .esurface_ct_builder
2648 .overlap_builder
2649 .radius
2650 .clone();
2651
2652 let radius_star = self.rstar_solution.solution.solution.clone();
2653 let e_cm = &ct_builder.e_cm;
2654 let settings = &ct_builder
2655 .settings
2656 .subtraction
2657 .local_ct_settings
2658 .uv_localisation;
2659
2660 let integrated_settings = &ct_builder.settings.subtraction.integrated_ct_settings;
2661
2662 let uv_damp_plus = evaluate_uv_damper(&radius, &radius_star, e_cm, settings);
2663 let uv_damp_minus = evaluate_uv_damper(&-&radius, &radius_star, e_cm, settings);
2664
2665 debug!("uv_damp_plus: {:?}", uv_damp_plus);
2666 debug!("uv_damp_minus: {:?}", uv_damp_minus);
2667 debug!("radius: {:?}", radius);
2668 debug!("radius_star: {:?}", radius_star);
2669
2670 let h_function =
2671 evaluate_integrated_ct_normalisation(&radius, &radius_star, e_cm, integrated_settings);
2672 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
2673 stage = "amplitude_threshold_prefactors",
2674 graph = %ct_builder.graph.name,
2675 esurface_id = esurface_id.0,
2676 raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
2677 radius = %format!("{:+16e}", radius),
2678 radius_star = %format!("{:+16e}", radius_star),
2679 derivative = %format!(
2680 "{:+16e}",
2681 self.rstar_solution.solution.derivative_at_solution
2682 ),
2683 uv_damp_plus = %format!("{:+16e}", uv_damp_plus),
2684 uv_damp_minus = %format!("{:+16e}", uv_damp_minus),
2685 h_function = %format!("{:+16e}", h_function),
2686 "amplitude threshold prefactors"
2687 );
2688
2689 let debug_diagnostics_enabled = tracing::event_enabled!(tracing::Level::DEBUG);
2690 if debug_diagnostics_enabled {
2691 let coincidence_tolerance = F::from_f64(1.0e-8) * e_cm;
2692 for (candidate_esurface_id, candidate_esurface) in
2693 ct_builder.esurface_collection.iter_enumerated()
2694 {
2695 let value = candidate_esurface.compute_from_momenta(
2696 &ct_builder.graph.loop_momentum_basis,
2697 &ct_builder.real_mass_vector,
2698 self.rstar_sample.loop_moms(),
2699 self.rstar_sample.external_moms(),
2700 );
2701 if value.abs() < coincidence_tolerance {
2702 let candidate_raised_esurface_id = ct_builder
2703 .raised_data
2704 .raised_groups
2705 .iter_enumerated()
2706 .find_map(|(raised_esurface_id, raised_group)| {
2707 raised_group
2708 .esurface_ids
2709 .contains(&candidate_esurface_id)
2710 .then_some(raised_esurface_id.0)
2711 });
2712 let candidate_atom = candidate_esurface.to_atom(&[]).to_string();
2713 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect, #esurface;
2714 stage = "amplitude_threshold_rstar_coincident_esurface",
2715 graph = %ct_builder.graph.name,
2716 selected_esurface_id = esurface_id.0,
2717 selected_raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
2718 candidate_esurface_id = candidate_esurface_id.0,
2719 candidate_raised_esurface_id = ?candidate_raised_esurface_id,
2720 selected = candidate_esurface_id == esurface_id,
2721 value = %format!("{:+16e}", value),
2722 tolerance = %format!("{:+16e}", coincidence_tolerance),
2723 file.atom = %candidate_atom,
2724 "threshold rstar coincident esurface graph={} selected={} raised={} candidate={} candidate_raised={:?} value={} tol={} atom={}",
2725 ct_builder.graph.name,
2726 esurface_id.0,
2727 esurface_ct_builder.raised_esurface_id.0,
2728 candidate_esurface_id.0,
2729 candidate_raised_esurface_id,
2730 format!("{:+16e}", value),
2731 format!("{:+16e}", coincidence_tolerance),
2732 candidate_atom
2733 );
2734 }
2735 }
2736 }
2737
2738 let mut evaluation =
2739 EvaluatedAmplitudeThreshold::new(self.rstar_sample.zero(), record_components);
2740
2741 for (cut_cff_index, evaluator_stack) in ct_evaluator.evaluator_stacks.iter_mut() {
2742 let order_index = cut_cff_index.left_threshold_order.unwrap() - 1;
2743 let (sample_for_order, threshold_params) = if order_index == 0 {
2744 debug!(
2745 "rescaled loop momenta at rstar:\n{}",
2746 self.rstar_sample.loop_moms()
2747 );
2748 (
2749 self.rstar_sample.clone(),
2750 ThresholdParams {
2751 radius: DualOrNot::NonDual(radius.clone()),
2752 radius_star: DualOrNot::NonDual(radius_star.clone()),
2753 esurface_derivative: DualOrNot::NonDual(
2754 self.rstar_solution.solution.derivative_at_solution.clone(),
2755 ),
2756 uv_damp_plus: DualOrNot::NonDual(uv_damp_plus.clone()),
2757 uv_damp_minus: DualOrNot::NonDual(uv_damp_minus.clone()),
2758 h_function: DualOrNot::NonDual(h_function.clone()),
2759 },
2760 )
2761 } else {
2762 let dual_shape = HyperDual::<F<T>>::new(simple_n_deriv_shape(order_index));
2763 let dual_radius_star = dual_shape.variable(0, radius_star.clone());
2764
2765 let dualized_center = self
2766 .rstar_solution
2767 .esurface_ct_builder
2768 .overlap_builder
2769 .rotated_center
2770 .iter()
2771 .map(|momentum| {
2772 momentum.map_ref(&|value| new_constant(&dual_radius_star, value))
2773 })
2774 .collect::<LoopMomenta<_>>();
2775 let dual_loop_momenta = self
2776 .rstar_solution
2777 .esurface_ct_builder
2778 .overlap_builder
2779 .unit_shifted_momenta
2780 .rescale_with_hyper_dual(&dual_radius_star, None)
2781 .iter()
2782 .zip(dualized_center.iter())
2783 .map(|(momentum, center)| momentum.clone() + center.clone())
2784 .collect::<LoopMomenta<_>>();
2785 debug!("rescaled loop momenta at rstar:\n{}", dual_loop_momenta);
2786 let mut sample_with_duals = self.rstar_sample.clone();
2787 sample_with_duals.sample.dual_loop_moms = Some(dual_loop_momenta);
2788
2789 (
2790 sample_with_duals,
2791 ThresholdParams {
2792 radius: DualOrNot::Dual(new_constant(&dual_radius_star, &radius)),
2793 radius_star: DualOrNot::Dual(dual_radius_star.clone()),
2794 esurface_derivative: DualOrNot::Dual(new_constant(
2795 &dual_radius_star,
2796 &self.rstar_solution.solution.derivative_at_solution.clone(),
2797 )),
2798 uv_damp_plus: DualOrNot::Dual(new_constant(
2799 &dual_radius_star,
2800 &uv_damp_plus,
2801 )),
2802 uv_damp_minus: DualOrNot::Dual(new_constant(
2803 &dual_radius_star,
2804 &uv_damp_minus,
2805 )),
2806 h_function: DualOrNot::Dual(new_constant(&dual_radius_star, &h_function)),
2807 },
2808 )
2809 };
2810
2811 let esurface_derivatives = if order_index == 0 {
2812 DualOrNot::NonDual(self.rstar_solution.solution.derivative_at_solution.clone())
2813 } else {
2814 let dual_shape_for_esurface =
2815 HyperDual::<F<T>>::new(simple_n_deriv_shape(order_index + 1));
2816 let dual_radius_star_for_esurface =
2817 dual_shape_for_esurface.variable(0, radius_star.clone());
2818 let dualized_center_for_esurface = self
2819 .rstar_solution
2820 .esurface_ct_builder
2821 .overlap_builder
2822 .rotated_center
2823 .iter()
2824 .map(|momentum| {
2825 momentum
2826 .map_ref(&|value| new_constant(&dual_radius_star_for_esurface, value))
2827 })
2828 .collect::<LoopMomenta<_>>();
2829 let dual_loop_momenta_for_esurface = self
2830 .rstar_solution
2831 .esurface_ct_builder
2832 .overlap_builder
2833 .unit_shifted_momenta
2834 .rescale_with_hyper_dual(&dual_radius_star_for_esurface, None)
2835 .iter()
2836 .zip(dualized_center_for_esurface.iter())
2837 .map(|(momentum, center)| momentum.clone() + center.clone())
2838 .collect::<LoopMomenta<_>>();
2839
2840 let dualized_externals = self
2841 .rstar_sample
2842 .external_moms()
2843 .iter()
2844 .map(|momentum| FourMomentum {
2845 temporal: Energy {
2846 value: new_constant(
2847 &dual_radius_star_for_esurface,
2848 &momentum.temporal.value,
2849 ),
2850 },
2851 spatial: momentum
2852 .spatial
2853 .map_ref(&|value| new_constant(&dual_radius_star_for_esurface, value)),
2854 })
2855 .collect();
2856
2857 DualOrNot::Dual(
2858 self.rstar_solution
2859 .esurface_ct_builder
2860 .esurface
2861 .compute_from_dual_momenta(
2862 &ct_builder.graph.loop_momentum_basis,
2863 &ct_builder.real_mass_vector,
2864 &dual_loop_momenta_for_esurface,
2865 &dualized_externals,
2866 ),
2867 )
2868 };
2869
2870 let params = T::get_parameters(
2871 param_builder,
2872 (false, false),
2873 ct_builder.graph,
2874 &sample_for_order,
2875 ct_builder.settings.kinematics.externals.get_helicities(),
2876 &ct_builder.settings.additional_params(),
2877 Some(&threshold_params),
2878 None,
2879 None,
2880 );
2881 let params_slice = params.as_slice();
2882 let params_nonfinite_count = params_slice
2883 .iter()
2884 .filter(|value| {
2885 value.re.is_nan()
2886 || value.re.is_infinite()
2887 || value.im.is_nan()
2888 || value.im.is_infinite()
2889 })
2890 .count();
2891 let first_params_nonfinite_index = params_slice.iter().position(|value| {
2892 value.re.is_nan()
2893 || value.re.is_infinite()
2894 || value.im.is_nan()
2895 || value.im.is_infinite()
2896 });
2897 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
2898 stage = "amplitude_threshold_pass_one_params",
2899 graph = %ct_builder.graph.name,
2900 esurface_id = esurface_id.0,
2901 raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
2902 order = order_index + 1,
2903 params_len = params_slice.len(),
2904 params_nonfinite_count,
2905 first_params_nonfinite_index = ?first_params_nonfinite_index,
2906 "amplitude threshold pass one params"
2907 );
2908
2909 let pass_one_result = evaluator_stack
2910 .evaluate(
2911 params,
2912 orientations,
2913 ct_builder.settings,
2914 evaluation_metadata,
2915 )
2916 .expect("Amplitude counterterm evaluator stack failed")
2917 .pop()
2918 .unwrap();
2919
2920 let prefactor = self.evaluate_multichanneling_prefactor(
2921 &sample_for_order,
2922 &model_params,
2923 evaluation_metadata,
2924 order_index,
2925 );
2926
2927 let raw_pass_one_is_nonfinite = match &pass_one_result {
2928 DualOrNot::Dual(dual_result) => dual_result.values.iter().any(|value| {
2929 value.re.is_nan()
2930 || value.re.is_infinite()
2931 || value.im.is_nan()
2932 || value.im.is_infinite()
2933 }),
2934 DualOrNot::NonDual(non_dual_result) => {
2935 non_dual_result.re.is_nan()
2936 || non_dual_result.re.is_infinite()
2937 || non_dual_result.im.is_nan()
2938 || non_dual_result.im.is_infinite()
2939 }
2940 };
2941 let prefactor_is_nonfinite = match &prefactor {
2942 DualOrNot::Dual(dual_result) => dual_result.values.iter().any(|value| {
2943 value.re.is_nan()
2944 || value.re.is_infinite()
2945 || value.im.is_nan()
2946 || value.im.is_infinite()
2947 }),
2948 DualOrNot::NonDual(non_dual_result) => {
2949 non_dual_result.re.is_nan()
2950 || non_dual_result.re.is_infinite()
2951 || non_dual_result.im.is_nan()
2952 || non_dual_result.im.is_infinite()
2953 }
2954 };
2955 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
2956 stage = "amplitude_threshold_pass_one_raw",
2957 graph = %ct_builder.graph.name,
2958 esurface_id = esurface_id.0,
2959 raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
2960 order = order_index + 1,
2961 raw_result = %format!("{pass_one_result}"),
2962 prefactor = %format!("{prefactor}"),
2963 raw_nonfinite = raw_pass_one_is_nonfinite,
2964 prefactor_nonfinite = prefactor_is_nonfinite,
2965 "amplitude threshold pass one raw"
2966 );
2967
2968 let pass_one_result = multiply_dual_or_not_complex(pass_one_result, &prefactor);
2969 let pass_one_is_nonfinite = match &pass_one_result {
2970 DualOrNot::Dual(dual_result) => dual_result.values.iter().any(|value| {
2971 value.re.is_nan()
2972 || value.re.is_infinite()
2973 || value.im.is_nan()
2974 || value.im.is_infinite()
2975 }),
2976 DualOrNot::NonDual(non_dual_result) => {
2977 non_dual_result.re.is_nan()
2978 || non_dual_result.re.is_infinite()
2979 || non_dual_result.im.is_nan()
2980 || non_dual_result.im.is_infinite()
2981 }
2982 };
2983 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
2984 stage = "amplitude_threshold_pass_one",
2985 graph = %ct_builder.graph.name,
2986 esurface_id = esurface_id.0,
2987 raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
2988 order = order_index + 1,
2989 prefactor = %format!("{prefactor}"),
2990 result = %format!("{pass_one_result}"),
2991 nonfinite = pass_one_is_nonfinite,
2992 "amplitude threshold pass one"
2993 );
2994
2995 debug!(
2996 "Pass one result for esurface {} order {}: {}",
2997 esurface_id.0,
2998 order_index + 1,
2999 pass_one_result
3000 );
3001
3002 let mut params_for_pass_two = vec![];
3003 match pass_one_result {
3004 DualOrNot::Dual(dual_result) => {
3005 params_for_pass_two
3006 .extend_from_slice(&extract_t_derivatives_complex(dual_result));
3007 }
3008 DualOrNot::NonDual(non_dual_result) => {
3009 params_for_pass_two.push(non_dual_result);
3010 }
3011 }
3012
3013 match esurface_derivatives {
3014 DualOrNot::Dual(dual_e_surface) => {
3015 extract_t_derivatives(dual_e_surface)[1..]
3016 .iter()
3017 .for_each(|value| params_for_pass_two.push(Complex::new_re(value.clone())));
3018 }
3019 DualOrNot::NonDual(non_dual_e_surface) => {
3020 params_for_pass_two.push(Complex::new_re(non_dual_e_surface));
3021 }
3022 }
3023
3024 params_for_pass_two.push(Complex::new_re(radius.clone()));
3025 params_for_pass_two.push(Complex::new_re(radius_star.clone()));
3026 params_for_pass_two.push(Complex::new_re(uv_damp_plus.clone()));
3027 params_for_pass_two.push(Complex::new_re(uv_damp_minus.clone()));
3028 params_for_pass_two.push(Complex::new_re(h_function.clone()));
3029
3030 let pass_two_result = if split_helpers {
3031 let mut pieces = evaluate_evaluator(
3032 &mut helper_evaluators[order_index],
3033 ¶ms_for_pass_two,
3034 evaluation_metadata,
3035 );
3036 assert_eq!(
3037 pieces.len(),
3038 3,
3039 "recording amplitude threshold helper must return legacy, local, and integrated pieces",
3040 );
3041 let integrated = pieces.pop().unwrap().unwrap_real();
3042 let local = pieces.pop().unwrap().unwrap_real();
3043 let legacy = pieces.pop().unwrap().unwrap_real();
3044 if record_components {
3045 let one = self.rstar_sample.one();
3046 let combined = combine_completed_threshold_pieces(
3047 local.clone(),
3048 integrated.clone(),
3049 &one,
3050 &one,
3051 );
3052 evaluation.add_completed_pieces(local, integrated, &one, &one);
3053 combined
3054 } else {
3055 evaluation.weighted += legacy.clone();
3056 legacy
3057 }
3058 } else {
3059 let combined = evaluate_evaluator_single(
3060 &mut helper_evaluators[order_index],
3061 ¶ms_for_pass_two,
3062 evaluation_metadata,
3063 );
3064 evaluation.weighted += combined.clone();
3065 combined
3066 };
3067
3068 debug!(
3069 "Pass two result for esurface {} order {}: {}",
3070 esurface_id.0,
3071 order_index + 1,
3072 pass_two_result
3073 );
3074 let pass_two_is_nonfinite = pass_two_result.re.is_nan()
3075 || pass_two_result.re.is_infinite()
3076 || pass_two_result.im.is_nan()
3077 || pass_two_result.im.is_infinite();
3078 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
3079 stage = "amplitude_threshold_pass_two",
3080 graph = %ct_builder.graph.name,
3081 esurface_id = esurface_id.0,
3082 raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
3083 order = order_index + 1,
3084 result = %format!("{:+16e}", pass_two_result),
3085 nonfinite = pass_two_is_nonfinite,
3086 "amplitude threshold pass two"
3087 );
3088 }
3089
3090 debug!(
3091 ct_eval = format!("{:+16e}", evaluation.weighted),
3092 "esurface {}", esurface_id.0
3093 );
3094 let total_ct_is_nonfinite = evaluation.weighted.re.is_nan()
3095 || evaluation.weighted.re.is_infinite()
3096 || evaluation.weighted.im.is_nan()
3097 || evaluation.weighted.im.is_infinite();
3098 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
3099 stage = "amplitude_threshold_total",
3100 graph = %ct_builder.graph.name,
3101 esurface_id = esurface_id.0,
3102 raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
3103 result = %format!("{:+16e}", evaluation.weighted),
3104 nonfinite = total_ct_is_nonfinite,
3105 "amplitude threshold total"
3106 );
3107
3108 let one = self.rstar_sample.one();
3109 Ok(evaluation.finish(&one, &one))
3110 }
3111
3112 fn evaluate_multichanneling_prefactor(
3113 &self,
3114 momentum_sample: &MomentumSample<T>,
3115 model_params: &[Complex<F<T>>],
3116 evaluation_metadata: &mut EvaluationMetaData,
3117 order_index: usize,
3118 ) -> DualOrNot<Complex<F<T>>> {
3119 let overlap_builder = self.rstar_solution.esurface_ct_builder.overlap_builder;
3120 let overlap = overlap_builder.counterterm_builder.overlap_structure;
3121
3122 if overlap.overlap_groups.len() < 2 {
3123 return DualOrNot::NonDual(Complex::new_re(momentum_sample.one()));
3124 }
3125
3126 let multiplicative_offset = momentum_sample
3127 .sample
3128 .dual_loop_moms
3129 .as_ref()
3130 .map(|dual_loop_moms| dual_loop_moms.first().unwrap().px.values.len())
3131 .unwrap_or(1);
3132 let zero = Complex::new_re(momentum_sample.zero());
3133 let mut params = if let Some(dual_loop_moms) = &momentum_sample.sample.dual_loop_moms {
3134 dual_loop_moms
3135 .iter()
3136 .flat_map(|mom| {
3137 [
3138 mom.px.values.clone(),
3139 mom.py.values.clone(),
3140 mom.pz.values.clone(),
3141 ]
3142 .into_iter()
3143 .flatten()
3144 .map(Complex::new_re)
3145 .collect::<Vec<_>>()
3146 })
3147 .collect::<Vec<_>>()
3148 } else {
3149 momentum_sample
3150 .loop_moms()
3151 .iter()
3152 .flat_map(|momentum| {
3153 [
3154 momentum.px.clone(),
3155 momentum.py.clone(),
3156 momentum.pz.clone(),
3157 ]
3158 .into_iter()
3159 .map(Complex::new_re)
3160 .collect::<Vec<_>>()
3161 })
3162 .collect::<Vec<_>>()
3163 };
3164
3165 params.extend(momentum_sample.external_moms().iter().flat_map(|momentum| {
3166 [
3167 momentum.temporal.value.clone(),
3168 momentum.spatial.px.clone(),
3169 momentum.spatial.py.clone(),
3170 momentum.spatial.pz.clone(),
3171 ]
3172 .into_iter()
3173 .flat_map(|value| {
3174 std::iter::once(Complex::new_re(value))
3175 .chain((1..multiplicative_offset).map(|_| zero.clone()))
3176 .collect::<Vec<_>>()
3177 })
3178 .collect::<Vec<_>>()
3179 }));
3180 params.extend(model_params.iter().cloned().flat_map(|value| {
3181 std::iter::once(value)
3182 .chain((1..multiplicative_offset).map(|_| zero.clone()))
3183 .collect::<Vec<_>>()
3184 }));
3185
3186 let evaluator = overlap_builder
3187 .overlap_group
3188 .prefactor_evaluator
3189 .as_ref()
3190 .unwrap()
3191 .get(order_index)
3192 .expect("missing overlap prefactor evaluator for amplitude threshold order");
3193
3194 evaluate_evaluator(&mut evaluator.borrow_mut(), ¶ms, evaluation_metadata)
3195 .pop()
3196 .expect("overlap prefactor evaluator should return exactly one value")
3197 }
3198}
3199
3200#[cfg(test)]
3201mod tests {
3202 use super::{
3203 AmplitudeCountertermAtom, AmplitudeCountertermData, EvaluatedAmplitudeThreshold,
3204 append_amplitude_component_evaluations, combine_completed_threshold_pieces,
3205 };
3206 use crate::{
3207 cff::{CutCFFIndex, esurface::RaisedEsurfaceId},
3208 dot,
3209 graph::{Graph, GraphGroupPosition, parse::from_dot::IntoGraph},
3210 initialisation::test_initialise,
3211 integrands::{
3212 evaluation::EvaluationMetaData,
3213 process::threshold_multiplier::{
3214 ThresholdMultiplierEvaluatorCollection, ThresholdMultiplierInput,
3215 ThresholdMultiplierInputWorkspace, ThresholdMultiplierLayout,
3216 ThresholdMultiplierPoint,
3217 },
3218 },
3219 momentum::{
3220 FourMomentum, ThreeMomentum,
3221 sample::{BareMomentumSample, ExternalFourMomenta, LoopMomenta, MomentumSample},
3222 },
3223 processes::{EvaluatorSettings, ThresholdCountertermVariantId},
3224 utils::F,
3225 uv::Integrands,
3226 };
3227 use linnet::half_edge::involution::EdgeVec;
3228 use spenso::algebra::complex::Complex;
3229 use symbolica::{atom::Atom, symbol};
3230 use typed_index_collections::ti_vec;
3231
3232 #[test]
3233 fn empty_amplitude_counterterm_atom_is_not_generated() {
3234 let atom = AmplitudeCountertermAtom {
3235 parametric: std::iter::empty().collect(),
3236 };
3237
3238 assert!(!atom.is_generated());
3239 }
3240
3241 #[test]
3242 fn non_empty_amplitude_counterterm_atom_is_generated() {
3243 let atom = AmplitudeCountertermAtom {
3244 parametric: Integrands::from_iter([(
3245 CutCFFIndex::new_all_none(),
3246 Atom::var(symbol!("x")),
3247 )]),
3248 };
3249
3250 assert!(atom.is_generated());
3251 }
3252
3253 #[test]
3254 fn zero_like_amplitude_counterterm_atom_is_zero() {
3255 let atom = AmplitudeCountertermAtom {
3256 parametric: Integrands::from_iter([(
3257 CutCFFIndex::new_all_none(),
3258 Atom::var(symbol!("x")),
3259 )]),
3260 };
3261
3262 let zeroed = atom.zero_like();
3263
3264 assert_eq!(
3265 zeroed.parametric,
3266 Integrands::from_iter([(CutCFFIndex::new_all_none(), Atom::Zero)])
3267 );
3268 }
3269
3270 #[test]
3271 fn inactive_amplitude_esurface_guard_reports_runtime_access() {
3272 let mut data = AmplitudeCountertermData::new_empty(GraphGroupPosition(0));
3273 data.active_mask = ti_vec![false];
3274
3275 let error = data
3276 .ensure_active_raised_esurface(RaisedEsurfaceId(0))
3277 .unwrap_err();
3278 assert!(error.to_string().contains("generation marked it inactive"));
3279 }
3280
3281 #[test]
3282 fn grouped_amplitude_catalogue_keeps_foreign_complements_and_raised_weights() {
3283 use super::{AmplitudeOverlapCatalogue, evaluate_generalized_multichanneling_prefactor};
3284 use crate::{
3285 cff::{
3286 VertexSet,
3287 esurface::{Esurface, EsurfaceID, RaisedEsurfaceGroup},
3288 },
3289 graph::{FeynmanGraph, LMBext, LmbIndex},
3290 momentum::{
3291 Rotation, RotationMethod,
3292 sample::{LoopIndex, SubspaceData},
3293 },
3294 processes::{ResolvedThresholdCountertermVariant, ThresholdCountertermSide},
3295 settings::RuntimeSettings,
3296 utils::{
3297 hyperdual_utils::{DualOrNot, extract_t_derivatives, simple_n_deriv_shape},
3298 load_generic_model,
3299 },
3300 };
3301 use linnet::half_edge::involution::EdgeIndex;
3302 use symbolica::domains::dual::{DualNumberStructure, HyperDual};
3303
3304 test_initialise().unwrap();
3305 let graph: Graph = dot!(digraph shared_threshold_geometry {
3308 ext [style=invis]
3309 node [num=1]
3310 edge [num=1 mass=0]
3311 ext -> a [id=0]
3312 ext -> b [id=1]
3313 ext -> c [id=2]
3314 a -> b [id=3]
3315 b -> c [id=4]
3316 c -> a [id=5 lmb_id=0]
3317 })
3318 .unwrap();
3319 let model = load_generic_model("scalars");
3320 let lmbs = ti_vec![graph.loop_momentum_basis.clone()];
3321 let subspace = SubspaceData::new_from_parent_basis_edges(
3322 &[EdgeIndex::from(5)],
3323 &graph.full_filter(),
3324 LmbIndex::from(0),
3325 &graph,
3326 &lmbs,
3327 )
3328 .unwrap();
3329 let variants = [5, 3, 4]
3330 .into_iter()
3331 .enumerate()
3332 .map(|(owner, edge)| {
3333 let surface = Esurface {
3334 energies: vec![EdgeIndex::from(edge)],
3335 external_shift: [(0, -2), (1, -1)]
3337 .into_iter()
3338 .map(|(edge, sign)| {
3339 (EdgeIndex::from(edge), sign * if owner == 0 { 4 } else { 1 })
3340 })
3341 .collect(),
3342 vertex_set: VertexSet::dummy(),
3343 };
3344 let metadata = ResolvedThresholdCountertermVariant {
3345 name: format!("member_{owner}"),
3346 group_id: Some(0),
3347 cut_group_id: None,
3348 associations: Vec::new(),
3349 side: ThresholdCountertermSide::Amplitude,
3350 threshold_esurface_ids: vec![EsurfaceID::from(0)],
3351 raised_esurface_group: RaisedEsurfaceGroup {
3352 esurface_ids: vec![EsurfaceID::from(0)],
3353 max_occurence: 2,
3354 },
3355 requested_subspace: Some(vec![EdgeIndex::from(5)]),
3356 requested_parent_lmb: Some(vec![EdgeIndex::from(5)]),
3357 resolved_parent_lmb: vec![EdgeIndex::from(5)],
3358 subspace: subspace.clone(),
3359 subspace_loop_count: 1,
3360 multiplier: None,
3361 };
3362 (
3363 GraphGroupPosition::from(owner),
3364 ThresholdCountertermVariantId(0),
3365 metadata,
3366 surface,
3367 )
3368 })
3369 .collect();
3370 let catalogue = AmplitudeOverlapCatalogue {
3371 graphs: ti_vec![graph.clone(), graph.clone(), graph],
3372 lmbs,
3373 variants,
3374 };
3375 let mut sample = MomentumSample {
3376 sample: BareMomentumSample {
3377 loop_moms: [ThreeMomentum::new(F(6.4), F(4.8), F(0.0))]
3378 .into_iter()
3379 .collect(),
3380 dual_loop_moms: None,
3381 loop_mom_cache_id: 0,
3382 loop_mom_base_cache_id: 0,
3383 external_moms: [(2.0 / 3.0, 4.0), (2.0 / 3.0, -8.0), (-4.0 / 3.0, 4.0)]
3384 .into_iter()
3385 .map(|(energy, px)| FourMomentum::from_args(F(energy), F(px), F(0.0), F(0.0)))
3386 .collect(),
3387 external_mom_cache_id: 0,
3388 external_mom_base_cache_id: 0,
3389 jacobian: F(1.0),
3390 orientation: None,
3391 parameterization_branch: None,
3392 },
3393 };
3394 let mut settings = RuntimeSettings::default();
3395 settings.subtraction.overlap_settings.try_origin_all_lmbs = false;
3396 let prepared = catalogue
3397 .prepare_group(
3398 &[0, 1, 2],
3399 &sample,
3400 &model,
3401 &settings,
3402 &Rotation::new(RotationMethod::Identity),
3403 )
3404 .unwrap();
3405 assert_eq!(prepared.overlap.overlap_groups.len(), 2);
3406 assert!(
3407 prepared
3408 .overlap
3409 .overlap_groups
3410 .iter()
3411 .all(|group| group.existing_esurfaces.len() == 2 && group.complement.len() == 1)
3412 );
3413 let weight = |point: &MomentumSample<f64>, group| {
3414 evaluate_generalized_multichanneling_prefactor(
3415 point,
3416 &prepared.threshold_masses,
3417 &catalogue.lmbs,
3418 &prepared.thresholds,
3419 &prepared.threshold_subspaces,
3420 &prepared.overlap,
3421 group,
3422 4,
3423 )
3424 };
3425 let scalar = |point: &MomentumSample<f64>, group| match weight(point, group) {
3426 DualOrNot::NonDual(value) => value.re.0,
3427 DualOrNot::Dual(_) => unreachable!(),
3428 };
3429 let first = scalar(&sample, 0);
3430 assert!(
3431 (first - 0.5).abs() > 0.1,
3432 "foreign complements must not collapse to equal empty products"
3433 );
3434 assert!((first + scalar(&sample, 1) - 1.0).abs() < 1.0e-12);
3435 let complement =
3436 prepared.overlap.existing_esurfaces[prepared.overlap.overlap_groups[0].complement[0]];
3437 let values = [1usize, 2].map(|id| {
3438 prepared.thresholds[EsurfaceID::from(id)]
3439 .compute_from_momenta(
3440 &catalogue.lmbs[LmbIndex::from(0)],
3441 &prepared.threshold_masses[id],
3442 sample.loop_moms(),
3443 sample.external_moms(),
3444 )
3445 .0
3446 .powi(4)
3447 });
3448 assert!((first - values[complement.0 - 1] / (values[0] + values[1])).abs() < 1.0e-12);
3449
3450 let delta = 1.0e-4;
3451 let mut displaced = sample.clone();
3452 displaced.sample.loop_moms[LoopIndex(0)] =
3453 ThreeMomentum::new(F(0.8 * (8.0 + delta)), F(0.6 * (8.0 + delta)), F(0.0));
3454 let plus = scalar(&displaced, 0);
3455 displaced.sample.loop_moms[LoopIndex(0)] =
3456 ThreeMomentum::new(F(0.8 * (8.0 - delta)), F(0.6 * (8.0 - delta)), F(0.0));
3457 let minus = scalar(&displaced, 0);
3458 let shape = HyperDual::<F<f64>>::new(simple_n_deriv_shape(2));
3459 let radius = shape.variable(0, F(8.0));
3460 sample.sample.dual_loop_moms = Some(
3461 [ThreeMomentum::new(
3462 radius.clone() * F(0.8),
3463 radius * F(0.6),
3464 super::new_constant(&shape, &F(0.0)),
3465 )]
3466 .into_iter()
3467 .collect(),
3468 );
3469 let DualOrNot::Dual(dual) = weight(&sample, 0) else {
3470 panic!("raised weight must retain its derivatives")
3471 };
3472 let real = HyperDual::from_values(
3473 dual.get_shape()
3474 .iter()
3475 .map(|entry| entry.to_vec())
3476 .collect(),
3477 dual.values.into_iter().map(|value| value.re).collect(),
3478 );
3479 let derivatives = extract_t_derivatives(real);
3480 assert!(derivatives[1].0.abs() > 1.0e-5);
3481 assert!((derivatives[1].0 - (plus - minus) / (2.0 * delta)).abs() < 1.0e-8);
3482
3483 let native: Graph = dot!(digraph native_implicit_member {
3486 ext [style=invis]
3487 node [num=1]
3488 edge [num=1 mass=0]
3489 ext -> a [id=0]
3490 ext -> b [id=1]
3491 ext -> c [id=2]
3492 a -> b [id=3]
3493 b -> c [id=4]
3494 c -> d [id=5]
3495 d -> a [id=6 lmb_id=0 mass=1]
3496 })
3497 .unwrap();
3498 let mut mixed = catalogue.clone();
3499 let member_lmb = mixed.graphs[GraphGroupPosition(1)]
3500 .generate_loop_momentum_bases_of(&mixed.graphs[GraphGroupPosition(1)].no_dummy())
3501 .into_iter()
3502 .find(|lmb| lmb.loop_edges.raw.as_slice() == [EdgeIndex(3)])
3503 .unwrap();
3504 mixed.graphs[GraphGroupPosition(1)].loop_momentum_basis = member_lmb;
3505 for (_, _, metadata, _) in &mut mixed.variants {
3506 metadata.group_id = None;
3507 }
3508 let mut implicit = mixed.variants[0].2.clone();
3509 implicit.name = "native_default".into();
3510 implicit.requested_subspace = None;
3511 implicit.requested_parent_lmb = None;
3512 mixed.variants.push((
3513 GraphGroupPosition(3),
3514 ThresholdCountertermVariantId(0),
3515 implicit,
3516 Esurface {
3517 energies: vec![EdgeIndex(6)],
3518 external_shift: mixed.variants[1].3.external_shift.clone(),
3519 vertex_set: VertexSet::dummy(),
3520 },
3521 ));
3522 mixed.graphs.push(native.clone());
3523 settings.subtraction.overlap_settings.try_origin_all_lmbs = true;
3524 let mixed_prepared = mixed
3525 .prepare_group(
3526 &[0, 1, 2, 3],
3527 &sample,
3528 &model,
3529 &settings,
3530 &Rotation::new(RotationMethod::Identity),
3531 )
3532 .unwrap();
3533 assert_eq!(mixed_prepared.thresholds.len(), 4);
3534 assert_eq!(mixed_prepared.overlap.overlap_groups.len(), 3);
3535 for id in 0..3 {
3536 assert_eq!(
3537 mixed_prepared.threshold_subspaces[id]
3538 .get_lmb(&mixed_prepared.lmbs)
3539 .edge_signatures,
3540 prepared.threshold_subspaces[id]
3541 .get_lmb(&prepared.lmbs)
3542 .edge_signatures,
3543 "an implicit neighbor must not change explicit master-based surface equations",
3544 );
3545 }
3546 assert_eq!(
3547 mixed_prepared.threshold_subspaces[3]
3548 .get_lmb(&mixed_prepared.lmbs)
3549 .edge_signatures,
3550 native.loop_momentum_basis.edge_signatures
3551 );
3552 let weights = (0..3)
3553 .map(|group| {
3554 evaluate_generalized_multichanneling_prefactor(
3555 &sample,
3556 &mixed_prepared.threshold_masses,
3557 &mixed_prepared.lmbs,
3558 &mixed_prepared.thresholds,
3559 &mixed_prepared.threshold_subspaces,
3560 &mixed_prepared.overlap,
3561 group,
3562 4,
3563 )
3564 })
3565 .map(|weight| match weight {
3566 DualOrNot::Dual(value) => value,
3567 _ => panic!("mixed raised weight lost derivatives"),
3568 })
3569 .collect::<Vec<_>>();
3570 let sum = weights[0].clone() + &weights[1] + &weights[2];
3571 assert!((sum.values[0].re.0 - 1.0).abs() < 1.0e-12);
3572 assert!(
3573 sum.values
3574 .iter()
3575 .skip(1)
3576 .all(|value| value.re.0.abs() < 1.0e-12)
3577 );
3578 assert!(
3579 mixed_prepared
3580 .overlap
3581 .overlap_groups
3582 .iter()
3583 .all(|group| group.existing_esurfaces.len() == 2 && group.complement.len() == 2)
3584 );
3585 }
3586
3587 #[test]
3588 fn generalized_multiplier_uses_component_contexts_after_helper_evaluation() {
3589 test_initialise().unwrap();
3590 let graph: Graph = dot!(digraph amplitude_multiplier_contexts {
3591 ext [style=invis]
3592 v1 [num=1]
3593 v2 [num=1]
3594 edge [num=1 mass=0]
3595 ext -> v1 [id=0]
3596 v1 -> v2 [id=1]
3597 v1 -> v2 [id=2]
3598 v2 -> ext [id=3]
3599 })
3600 .unwrap();
3601 let edge = graph.loop_momentum_basis.loop_edges.raw[0];
3602 let layout = ThresholdMultiplierLayout::new(
3603 Vec::new(),
3604 Vec::new(),
3605 graph.loop_momentum_basis.ext_edges.len(),
3606 (0..graph.n_edges()).collect(),
3607 Vec::new(),
3608 )
3609 .unwrap();
3610 let expression = layout
3611 .parse_expression(
3612 &format!(
3613 "Q3(effective, {}, cind(1)) + 10 * Q3(star, {}, cind(1))",
3614 edge.0, edge.0,
3615 ),
3616 &Default::default(),
3617 )
3618 .unwrap();
3619 let mut collection = Some(
3620 ThresholdMultiplierEvaluatorCollection::build(
3621 layout,
3622 vec![(ThresholdCountertermVariantId(0), Some(expression))],
3623 Vec::new(),
3624 &EvaluatorSettings::default(),
3625 )
3626 .unwrap()
3627 .unwrap(),
3628 );
3629
3630 let make_sample = |loop_scale: f64| MomentumSample {
3631 sample: BareMomentumSample {
3632 loop_moms: (0..graph.loop_momentum_basis.loop_edges.len())
3633 .map(|index| ThreeMomentum::new(F(loop_scale + index as f64), F(0.25), F(-0.5)))
3634 .collect::<LoopMomenta<_>>(),
3635 dual_loop_moms: None,
3636 loop_mom_cache_id: 0,
3637 loop_mom_base_cache_id: 0,
3638 external_moms: (0..graph.loop_momentum_basis.ext_edges.len())
3639 .map(|position| {
3640 FourMomentum::from_args(
3641 F(5.0 + position as f64),
3642 F(0.1 * position as f64),
3643 F(0.0),
3644 F(0.0),
3645 )
3646 })
3647 .collect::<ExternalFourMomenta<_>>(),
3648 external_mom_cache_id: 0,
3649 external_mom_base_cache_id: 0,
3650 jacobian: F(1.0),
3651 orientation: None,
3652 parameterization_branch: None,
3653 },
3654 };
3655 let base = make_sample(1.0);
3656 let root = make_sample(2.0);
3657 let masses = (0..graph.n_edges()).map(|_| F(0.0)).collect::<EdgeVec<_>>();
3658 let mut expected_workspace =
3659 ThresholdMultiplierInputWorkspace::new(collection.as_ref().unwrap().layout(), F(0.0));
3660 let expected_for = |values: &[Complex<F<f64>>]| {
3661 collection
3662 .as_ref()
3663 .unwrap()
3664 .layout()
3665 .inputs()
3666 .iter()
3667 .zip(values)
3668 .filter_map(|(input, value)| match input {
3669 ThresholdMultiplierInput::EdgeMomentum {
3670 point,
3671 edge: input_edge,
3672 component: 1,
3673 } if *input_edge == edge.0 => Some(match point {
3674 ThresholdMultiplierPoint::Effective => value.re,
3675 ThresholdMultiplierPoint::Star => value.re * F(10.0),
3676 }),
3677 _ => None,
3678 })
3679 .fold(F(0.0), |sum, value| sum + value)
3680 };
3681 let expected_local = expected_for(
3682 expected_workspace
3683 .bind(
3684 collection.as_ref().unwrap().layout(),
3685 &graph.loop_momentum_basis,
3686 &masses,
3687 &[],
3688 &[],
3689 &base,
3690 &root,
3691 )
3692 .unwrap()
3693 .as_slice(),
3694 );
3695 let expected_integrated = expected_for(
3696 expected_workspace
3697 .bind(
3698 collection.as_ref().unwrap().layout(),
3699 &graph.loop_momentum_basis,
3700 &masses,
3701 &[],
3702 &[],
3703 &root,
3704 &root,
3705 )
3706 .unwrap()
3707 .as_slice(),
3708 );
3709 let mut workspace =
3710 ThresholdMultiplierInputWorkspace::new(collection.as_ref().unwrap().layout(), F(0.0));
3711 let (local, integrated) = AmplitudeCountertermData::evaluate_variant_multiplier(
3712 &mut collection,
3713 Some(&mut workspace),
3714 ThresholdCountertermVariantId(0),
3715 &graph,
3716 &masses,
3717 &[],
3718 &[],
3719 &base,
3720 &root,
3721 &mut EvaluationMetaData::new_empty(),
3722 )
3723 .unwrap();
3724 assert_eq!(local, expected_local);
3725 assert_eq!(integrated, expected_integrated);
3726 assert_ne!(local, integrated);
3727
3728 let completed = combine_completed_threshold_pieces(
3729 Complex::new_re(F(2.0)),
3730 Complex::new_re(F(3.0)),
3731 &F(5.0),
3732 &F(7.0),
3733 );
3734 assert_eq!(completed, Complex::new_re(F(31.0)));
3735
3736 let mut data = AmplitudeCountertermData::new_empty(GraphGroupPosition(0));
3737 data.threshold_multipliers = collection;
3738 let mut visited = 0;
3739 data.for_each_generic_evaluator_mut(|_| {
3740 visited += 1;
3741 Ok(())
3742 })
3743 .unwrap();
3744 assert_eq!(visited, 1);
3745 assert_eq!(data.generic_evaluator_count(), 0);
3746 }
3747
3748 #[test]
3749 fn exact_zero_multiplier_skips_a_nan_completed_piece() {
3750 let completed = combine_completed_threshold_pieces(
3751 Complex::new_re(F(f64::NAN)),
3752 Complex::new_re(F(3.0)),
3753 &F(0.0),
3754 &F(7.0),
3755 );
3756
3757 assert_eq!(completed, Complex::new_re(F(21.0)));
3758 }
3759
3760 #[test]
3761 fn partial_zero_records_a_skipped_component_without_its_nan_bare_value() {
3762 let local_multiplier = F(0.0);
3763 let integrated_multiplier = F(7.0);
3764 let mut evaluation = EvaluatedAmplitudeThreshold::new(F(0.0), true);
3765 evaluation.add_completed_pieces(
3766 Complex::new_re(F(f64::NAN)),
3767 Complex::new_re(F(3.0)),
3768 &local_multiplier,
3769 &integrated_multiplier,
3770 );
3771 let evaluation = evaluation.finish(&local_multiplier, &integrated_multiplier);
3772 let mut components = Some(Vec::new());
3773 append_amplitude_component_evaluations(
3774 &mut components,
3775 &evaluation,
3776 ThresholdCountertermVariantId(0),
3777 RaisedEsurfaceId(2),
3778 3,
3779 &local_multiplier,
3780 &integrated_multiplier,
3781 );
3782 let components = components.unwrap();
3783
3784 assert_eq!(evaluation.weighted, Complex::new_re(F(21.0)));
3785 assert_eq!(components.len(), 2);
3786 assert!(components[0].evaluation_skipped);
3787 assert!(components[0].bare.is_none());
3788 assert_eq!(components[0].weighted, Complex::new_re(F(0.0)));
3789 assert!(!components[1].evaluation_skipped);
3790 assert_eq!(components[1].bare, Some(Complex::new_re(F(3.0))));
3791 assert_eq!(components[1].weighted, Complex::new_re(F(21.0)));
3792 }
3793}