1use std::{collections::BTreeMap, path::Path, slice};
2
3use bincode_trait_derive::{Decode, Encode};
4use color_eyre::Result;
5use eyre::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 GroupEsurfaceId, RaisedEsurfaceData, RaisedEsurfaceId,
18 esurface_value_is_strictly_inside,
19 },
20 expression::OrientationID,
21 },
22 graph::{FeynmanGraph, Graph, GraphGroupPosition},
23 integrands::{
24 evaluation::EvaluationMetaData,
25 process::{
26 GenericEvaluator, ParamBuilder, ThresholdParams,
27 evaluators::{
28 EvaluatorStack, SingleOrAllOrientations, evaluate_evaluator,
29 evaluate_evaluator_single,
30 },
31 },
32 },
33 model::Model,
34 momentum::{
35 Energy, FourMomentum, Rotation,
36 sample::{LoopMomenta, MomentumSample},
37 },
38 processes::EvaluatorBuildTimings,
39 settings::{GlobalSettings, RuntimeSettings},
40 subtraction::{
41 evaluate_integrated_ct_normalisation, evaluate_uv_damper,
42 overlap::{OverlapGroup, OverlapStructure},
43 },
44 utils::{
45 F, FloatLike,
46 hyperdual_utils::{
47 DualOrNot, extract_t_derivatives, extract_t_derivatives_complex, new_constant,
48 shape_from_cut_cff_index, simple_n_deriv_shape,
49 },
50 newton_solver::{NewtonIterationResult, RadialRootDiagnostics, RadialRootIdentity},
51 },
52 uv::Integrands,
53};
54use symbolica::domains::dual::HyperDual;
55
56const MAX_ITERATIONS: usize = 40;
57
58fn multiply_dual_or_not_complex<T: FloatLike>(
59 lhs: DualOrNot<Complex<F<T>>>,
60 rhs: &DualOrNot<Complex<F<T>>>,
61) -> DualOrNot<Complex<F<T>>> {
62 match (lhs, rhs) {
63 (DualOrNot::NonDual(lhs), DualOrNot::NonDual(rhs)) => DualOrNot::NonDual(lhs * rhs),
64 (DualOrNot::Dual(lhs), DualOrNot::Dual(rhs)) => DualOrNot::Dual(lhs * rhs.clone()),
65 (DualOrNot::Dual(lhs), DualOrNot::NonDual(rhs)) => {
66 let rhs_dual = new_constant(&lhs, rhs);
67 DualOrNot::Dual(lhs * rhs_dual)
68 }
69 (DualOrNot::NonDual(lhs), DualOrNot::Dual(rhs)) => {
70 let lhs_dual = new_constant(rhs, &lhs);
71 DualOrNot::Dual(lhs_dual * rhs.clone())
72 }
73 }
74}
75
76#[derive(Clone, Encode, Decode)]
77#[trait_decode(trait = GammaLoopContext)]
78pub struct AmplitudeCountertermData {
79 pub overlap: OverlapStructure,
80 pub evaluators: TiVec<RaisedEsurfaceId, AmplitudeCountertermEvaluator>,
81 pub helper_evaluators: Vec<GenericEvaluator>,
82 pub generated_mask: TiVec<RaisedEsurfaceId, bool>,
86 pub active_mask: TiVec<RaisedEsurfaceId, bool>,
91 pub raised_data: RaisedEsurfaceData,
92 pub esurface_map: TiVec<GroupEsurfaceId, TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>>,
93 pub local_esurface_exists: TiVec<GroupEsurfaceId, bool>,
94 pub own_group_position: GraphGroupPosition,
95}
96
97#[derive(Clone, Encode, Decode)]
98#[trait_decode(trait = GammaLoopContext)]
99pub struct AmplitudeCountertermAtom {
100 pub parametric: Integrands,
101}
102
103impl AmplitudeCountertermAtom {
104 pub(crate) fn is_generated(&self) -> bool {
105 self.parametric.iter().next().is_some()
106 }
107
108 pub(crate) fn zero_like(&self) -> Self {
109 Self {
110 parametric: self.parametric.map(|_| symbolica::atom::Atom::Zero),
111 }
112 }
113
114 #[instrument(skip_all)]
115 pub(crate) fn to_evaluator_with_timings(
116 &self,
117 param_builder: &ParamBuilder,
118 orientations: &TiVec<OrientationID, EdgeVec<Orientation>>,
119 global_settings: &GlobalSettings,
120 ) -> (AmplitudeCountertermEvaluator, EvaluatorBuildTimings) {
121 let _progress_guard =
122 crate::processes::enter_detailed_progress_span("Building Threshold CT Evaluator");
123 let mut evaluator_stacks = BTreeMap::new();
124 let mut timings = EvaluatorBuildTimings::default();
125
126 for (index, integrand) in self.parametric.iter() {
127 let dual_shape = shape_from_cut_cff_index(index);
128
129 let (evaluator_stack, evaluator_timings) = EvaluatorStack::new_with_timings(
130 slice::from_ref(integrand),
131 param_builder,
132 orientations.as_slice().as_ref(),
133 dual_shape,
134 &global_settings.generation.evaluator,
135 )
136 .unwrap();
137 timings += evaluator_timings;
138 evaluator_stacks.insert(*index, evaluator_stack);
139 }
140
141 (AmplitudeCountertermEvaluator { evaluator_stacks }, timings)
142 }
143
144 pub(crate) fn new() -> Self {
145 Self {
146 parametric: std::iter::empty().collect(),
147 }
148 }
149}
150
151#[derive(Clone, Encode, Decode)]
152#[trait_decode(trait = GammaLoopContext)]
153pub struct AmplitudeCountertermEvaluator {
154 pub evaluator_stacks: BTreeMap<CutCFFIndex, EvaluatorStack>,
155}
156
157impl AmplitudeCountertermEvaluator {
158 pub(crate) fn generic_evaluator_count(&self) -> usize {
159 self.evaluator_stacks
160 .values()
161 .map(EvaluatorStack::generic_evaluator_count)
162 .sum()
163 }
164}
165
166#[derive(Debug, Clone)]
167pub struct AmplitudeLocalCountertermEvaluation<T: FloatLike> {
168 pub esurface_id: RaisedEsurfaceId,
169 pub overlap_group: usize,
170 pub value: Complex<F<T>>,
171}
172
173#[derive(Debug, Clone)]
174pub struct AmplitudeCountertermEvaluation<T: FloatLike> {
175 pub total: Complex<F<T>>,
176 pub local_counterterms: Vec<AmplitudeLocalCountertermEvaluation<T>>,
177}
178
179impl AmplitudeCountertermData {
180 fn radial_root_identity(
181 graph_name: &str,
182 overlap_group: usize,
183 raised_esurface_id: RaisedEsurfaceId,
184 rotation: &Rotation,
185 ) -> RadialRootIdentity {
186 RadialRootIdentity::new(format!(
187 "amplitude graph '{graph_name}' overlap group {overlap_group} raised E-surface {} probe rotation {}",
188 raised_esurface_id.0, rotation.method,
189 ))
190 }
191
192 pub(crate) fn generic_evaluator_count(&self) -> usize {
193 let stack_count = self
194 .evaluators
195 .iter()
196 .map(AmplitudeCountertermEvaluator::generic_evaluator_count)
197 .sum::<usize>();
198 let overlap_count = self
199 .overlap
200 .overlap_groups
201 .iter()
202 .map(|group| {
203 group
204 .prefactor_evaluator
205 .as_ref()
206 .map(|evaluators| evaluators.len())
207 .unwrap_or(0)
208 })
209 .sum::<usize>();
210 stack_count + overlap_count + self.helper_evaluators.len()
211 }
212
213 pub fn new_empty(own_group_position: GraphGroupPosition) -> Self {
214 Self {
215 overlap: OverlapStructure::new_empty(),
216 evaluators: TiVec::new(),
217 helper_evaluators: vec![],
218 generated_mask: TiVec::new(),
219 active_mask: TiVec::new(),
220 raised_data: RaisedEsurfaceData {
221 raised_groups: TiVec::new(),
222 pass_two_evaluator: None,
223 },
224 esurface_map: TiVec::new(),
225 local_esurface_exists: TiVec::new(),
226 own_group_position,
227 }
228 }
229
230 fn ensure_active_raised_esurface(&self, raised_esurface_id: RaisedEsurfaceId) -> Result<()> {
231 if self.active_mask[raised_esurface_id] {
232 return Ok(());
233 }
234
235 Err(color_eyre::eyre::eyre!(
236 "Amplitude threshold evaluator {} was reached at runtime even though generation marked it inactive for the selected orientation subset",
237 raised_esurface_id.0
238 ))
239 }
240
241 pub fn compile(
242 &mut self,
243 path: impl AsRef<Path>,
244 _override_existing: bool,
245 frozen_mode: &crate::settings::global::FrozenCompilationMode,
246 ) -> Result<()> {
247 for (i, e) in self.evaluators.iter_mut_enumerated() {
248 for (cff_index, evaluator_stack) in e.evaluator_stacks.iter_mut() {
249 let order_index = cff_index.left_threshold_order.unwrap() - 1;
250 evaluator_stack.compile(
251 format!("esurface_{}_order_{}", i.0, order_index + 1),
252 path.as_ref(),
253 frozen_mode,
254 )?;
255 }
256 }
257
258 for (group_index, group) in self.overlap.overlap_groups.iter_mut().enumerate() {
259 if let Some(prefactor_evaluators) = group.prefactor_evaluator.as_mut() {
260 for (order_index, prefactor_evaluator) in
261 prefactor_evaluators.iter_mut().enumerate()
262 {
263 prefactor_evaluator.borrow_mut().compile_external(
264 path.as_ref()
265 .join(format!(
266 "overlap_prefactor_{group_index}_order_{}",
267 order_index + 1
268 ))
269 .with_extension("cpp"),
270 format!("overlap_prefactor_{group_index}_order_{}", order_index + 1),
271 path.as_ref()
272 .join(format!(
273 "overlap_prefactor_{group_index}_order_{}",
274 order_index + 1
275 ))
276 .with_extension("so"),
277 frozen_mode,
278 )?;
279 }
280 }
281 }
282
283 for (order_index, evaluator) in self.helper_evaluators.iter_mut().enumerate() {
284 evaluator.compile_external(
285 path.as_ref()
286 .join(format!("threshold_helper_{order_index}"))
287 .with_extension("cpp"),
288 format!("threshold_helper_{order_index}"),
289 path.as_ref()
290 .join(format!("threshold_helper_{order_index}"))
291 .with_extension("so"),
292 frozen_mode,
293 )?;
294 }
295 Ok(())
296 }
297
298 pub(crate) fn for_each_generic_evaluator_mut(
299 &mut self,
300 mut f: impl FnMut(&mut crate::integrands::process::GenericEvaluator) -> Result<()>,
301 ) -> Result<()> {
302 for evaluator in self.evaluators.iter_mut() {
303 for evaluator_stack in evaluator.evaluator_stacks.values_mut() {
304 evaluator_stack.for_each_generic_evaluator_mut(&mut f)?;
305 }
306 }
307
308 for group in &mut self.overlap.overlap_groups {
309 if let Some(prefactor_evaluators) = group.prefactor_evaluator.as_mut() {
310 for prefactor_evaluator in prefactor_evaluators.iter_mut() {
311 f(prefactor_evaluator.get_mut())?;
312 }
313 }
314 }
315
316 for evaluator in &mut self.helper_evaluators {
317 f(evaluator)?;
318 }
319
320 Ok(())
321 }
322
323 #[allow(clippy::too_many_arguments)]
324 pub fn evaluate<T: FloatLike>(
325 &mut self,
326 momentum_sample: &MomentumSample<T>,
327 graph: &Graph,
328 model: &Model,
329 esurfaces: &EsurfaceCollection,
330 rotation: &Rotation,
331 settings: &RuntimeSettings,
332 param_builder: &mut ParamBuilder<f64>,
333 orientation: SingleOrAllOrientations<'_, OrientationID>,
334 evaluation_metadata: &mut EvaluationMetaData,
335 record_primary_timing: bool,
336 ) -> Result<AmplitudeCountertermEvaluation<T>> {
337 debug!("start evaluate threshold counterterm");
338 let existing_esurfaces = self
339 .overlap
340 .existing_esurfaces
341 .iter()
342 .map(|e| e.0)
343 .collect::<Vec<_>>();
344 debug!("subtracting esurfaces: {:?}", existing_esurfaces);
345 debug!("overlap structure\n: {}", self.overlap);
346
347 let counter_term_builder = CounterTermBuilder::new(
348 graph,
349 model,
350 rotation,
351 settings,
352 esurfaces,
353 momentum_sample,
354 &self.overlap,
355 &self.raised_data,
356 self.own_group_position,
357 &self.esurface_map,
358 );
359
360 let mut result = Complex::new_re(momentum_sample.zero());
361 let mut local_counterterms = Vec::new();
362 let mut radial_root_failed = false;
363
364 for (overlap_group, group) in self.overlap.overlap_groups.iter().enumerate() {
365 let overlap_builder = counter_term_builder.new_overlap_builder(group);
366
367 for existing_esurface_id in group.existing_esurfaces.iter() {
368 let group_esurface_id = self.overlap.existing_esurfaces[*existing_esurface_id];
369 if self
370 .local_esurface_exists
371 .get(group_esurface_id)
372 .is_some_and(|exists| !*exists)
373 {
374 continue;
375 }
376
377 let Some(esurface_builder) =
378 overlap_builder.new_esurface_builder(*existing_esurface_id)
379 else {
380 continue;
381 };
382
383 let raised_esurface_id = esurface_builder.raised_esurface_id;
384 self.ensure_active_raised_esurface(raised_esurface_id)?;
385 let radial_root_identity = Self::radial_root_identity(
386 &graph.name,
387 overlap_group,
388 raised_esurface_id,
389 rotation,
390 );
391 let Some(rstar_solution) = esurface_builder.solve_rstar(
392 &radial_root_identity,
393 &mut evaluation_metadata.radial_root_diagnostics,
394 ) else {
395 evaluation_metadata.record_threshold_counterterm_error(format!(
396 "amplitude graph '{}' overlap group {} raised E-surface {} failed center or radial-root validation in probe rotation {}",
397 graph.name,
398 overlap_group,
399 raised_esurface_id.0,
400 rotation.method,
401 ));
402 radial_root_failed = true;
403 continue;
404 };
405 if radial_root_failed {
406 continue;
407 }
408 let single_result = rstar_solution.rstar_samples().evaluate(
409 param_builder,
410 orientation,
411 evaluation_metadata,
412 record_primary_timing,
413 &mut self.evaluators[raised_esurface_id],
414 &mut self.helper_evaluators,
415 )?;
416
417 if !single_result.is_zero() {
418 debug!(
423 "Counterterm for esurface {}: {:+16e}",
424 existing_esurface_id, single_result
425 );
426 }
427
428 local_counterterms.push(AmplitudeLocalCountertermEvaluation {
429 esurface_id: raised_esurface_id,
430 overlap_group,
431 value: single_result.clone(),
432 });
433 result += single_result;
434 }
435 }
436 if radial_root_failed {
437 return Ok(AmplitudeCountertermEvaluation {
438 total: Complex::new_re(F::from_f64(f64::NAN)),
439 local_counterterms,
440 });
441 }
442 Ok(AmplitudeCountertermEvaluation {
443 total: result,
444 local_counterterms,
445 })
446 }
447
448 #[allow(clippy::too_many_arguments)]
449 pub fn kinematics_for_approach<T: FloatLike>(
450 &mut self,
451 momentum_sample: &MomentumSample<T>,
452 graph: &Graph,
453 model: &Model,
454 esurfaces: &EsurfaceCollection,
455 rotation: &Rotation,
456 settings: &RuntimeSettings,
457 ) -> Result<OverlapStructureWithKinematics<T>> {
458 let counter_term_builder = CounterTermBuilder::new(
459 graph,
460 model,
461 rotation,
462 settings,
463 esurfaces,
464 momentum_sample,
465 &self.overlap,
466 &self.raised_data,
467 self.own_group_position,
468 &self.esurface_map,
469 );
470
471 let mut overlap_sturcture_with_kinematics = OverlapStructureWithKinematics {
472 existing_esurfaces: self.overlap.existing_esurfaces.clone(),
473 overlap_groups_with_kinematics: Vec::new(),
474 };
475 let mut radial_root_diagnostics = RadialRootDiagnostics::default();
476
477 for (overlap_group, group) in self.overlap.overlap_groups.iter().enumerate() {
478 let overlap_builder = counter_term_builder.new_overlap_builder(group);
479
480 let mut loop_momenta_at_esurface: TiVec<ExistingEsurfaceId, Option<MomentumSample<T>>> =
481 ti_vec![];
482 for existing_esurface_id in group.existing_esurfaces.iter() {
483 let single_result = overlap_builder
484 .new_esurface_builder(*existing_esurface_id)
485 .map(|esurface_builder| -> Result<_> {
486 self.ensure_active_raised_esurface(esurface_builder.raised_esurface_id)?;
487 let raised_esurface_id = esurface_builder.raised_esurface_id;
488 let radial_root_identity = Self::radial_root_identity(
489 &graph.name,
490 overlap_group,
491 raised_esurface_id,
492 rotation,
493 );
494 let rstar_sample = esurface_builder
495 .solve_rstar(
496 &radial_root_identity,
497 &mut radial_root_diagnostics,
498 )
499 .ok_or_else(|| {
500 eyre!(
501 "Could not construct threshold-counterterm kinematics for raised E-surface {} in probe rotation {}",
502 raised_esurface_id.0,
503 rotation.method,
504 )
505 })?
506 .rstar_samples();
507 Result::Ok(rstar_sample.rstar_sample)
508 })
509 .transpose()?;
510
511 loop_momenta_at_esurface.push(single_result);
512 }
513
514 overlap_sturcture_with_kinematics
515 .overlap_groups_with_kinematics
516 .push(OverlapGroupWithKinematics {
517 overlap_group: group.clone(),
518 loop_momenta_at_esurface,
519 });
520 }
521
522 Ok(overlap_sturcture_with_kinematics)
523 }
524}
525
526pub struct OverlapGroupWithKinematics<T: FloatLike> {
527 pub overlap_group: OverlapGroup,
528 pub loop_momenta_at_esurface: TiVec<ExistingEsurfaceId, Option<MomentumSample<T>>>,
529}
530
531pub struct OverlapStructureWithKinematics<T: FloatLike> {
532 pub existing_esurfaces: ExistingEsurfaces,
533 pub overlap_groups_with_kinematics: Vec<OverlapGroupWithKinematics<T>>,
534}
535
536struct CounterTermBuilder<'a, T: FloatLike> {
537 overlap_structure: &'a OverlapStructure,
538 raised_data: &'a RaisedEsurfaceData,
539 own_group_position: GraphGroupPosition,
540 esurface_map: &'a TiVec<GroupEsurfaceId, TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>>,
541 real_mass_vector: EdgeVec<F<T>>,
542 e_cm: F<T>,
543 graph: &'a Graph,
544 rotation_for_overlap: &'a Rotation,
545 settings: &'a RuntimeSettings,
546 esurface_collection: &'a EsurfaceCollection,
547 sample: &'a MomentumSample<T>,
548}
549
550impl<'a, T: FloatLike> CounterTermBuilder<'a, T> {
551 #[allow(clippy::too_many_arguments)]
552 fn new(
553 graph: &'a Graph,
554 model: &'a Model,
555 rotation_for_overlap: &'a Rotation,
556 settings: &'a RuntimeSettings,
557 esurface_collection: &'a EsurfaceCollection,
558 sample: &'a MomentumSample<T>,
559 overlap_structure: &'a OverlapStructure,
560 raised_data: &'a RaisedEsurfaceData,
561 own_group_position: GraphGroupPosition,
562 esurface_map: &'a TiVec<
563 GroupEsurfaceId,
564 TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>,
565 >,
566 ) -> Self {
567 let real_mass_vector = graph.get_real_mass_vector(model);
568 let e_cm = F::from_f64(settings.kinematics.e_cm);
569
570 Self {
571 real_mass_vector,
572 e_cm,
573 graph,
574 rotation_for_overlap,
575 settings,
576 esurface_collection,
577 overlap_structure,
578 raised_data,
579 sample,
580 own_group_position,
581 esurface_map,
582 }
583 }
584
585 fn new_overlap_builder(&'a self, overlap_group: &'a OverlapGroup) -> OverlapBuilder<'a, T> {
586 let center = &overlap_group.center;
587
588 let (unrotated_center, rotated_center) = (
591 center.cast(),
592 center.rotate(self.rotation_for_overlap).cast(),
593 );
594
595 let shifted_loop_momenta = self.sample.loop_moms() - &rotated_center;
596 let radius = shifted_loop_momenta.hyper_radius_squared(None).sqrt();
597 let unit_shifted_momenta = shifted_loop_momenta.rescale(&radius.inv(), None);
598
599 OverlapBuilder {
600 counterterm_builder: self,
601 overlap_group,
602 rotated_center,
603 _unrotated_center: unrotated_center,
604 unit_shifted_momenta,
605 radius,
606 }
607 }
608}
609
610struct OverlapBuilder<'a, T: FloatLike> {
611 counterterm_builder: &'a CounterTermBuilder<'a, T>,
612 overlap_group: &'a OverlapGroup,
613 rotated_center: LoopMomenta<F<T>>,
615 _unrotated_center: LoopMomenta<F<T>>,
617 unit_shifted_momenta: LoopMomenta<F<T>>,
618 radius: F<T>,
619}
620
621impl<'a, T: FloatLike> OverlapBuilder<'a, T> {
622 fn new_esurface_builder(
623 &'a self,
624 existing_esurface_id: ExistingEsurfaceId,
625 ) -> Option<EsurfaceCTBuilder<'a, T>> {
626 let group_esurface_id = self
627 .counterterm_builder
628 .overlap_structure
629 .existing_esurfaces[existing_esurface_id];
630
631 let raised_esurface_id = self.counterterm_builder.esurface_map[group_esurface_id]
632 [self.counterterm_builder.own_group_position];
633
634 raised_esurface_id.map(|raised_esurface_id| {
635 let esurface_id = self.counterterm_builder.raised_data.raised_groups
636 [raised_esurface_id]
637 .esurface_ids[0];
638
639 EsurfaceCTBuilder {
640 overlap_builder: self,
641 _existing_esurface_id: existing_esurface_id,
642 group_esurface_id,
643 esurface: &self.counterterm_builder.esurface_collection[esurface_id],
644 esurface_id,
645 raised_esurface_id,
646 }
647 })
648 }
649}
650
651struct EsurfaceCTBuilder<'a, T: FloatLike> {
652 overlap_builder: &'a OverlapBuilder<'a, T>,
653 _existing_esurface_id: ExistingEsurfaceId,
654 group_esurface_id: GroupEsurfaceId,
655 esurface: &'a Esurface,
656 esurface_id: EsurfaceID,
657 raised_esurface_id: RaisedEsurfaceId,
658}
659
660impl<'a, T: FloatLike> EsurfaceCTBuilder<'a, T> {
661 fn solve_rstar(
662 self,
663 radial_root_identity: &RadialRootIdentity,
664 radial_root_diagnostics: &mut RadialRootDiagnostics,
665 ) -> Option<RstarSolution<'a, T>> {
666 let mut all_center_values_valid = true;
667 let center_surface_values = self
668 .overlap_builder
669 .overlap_group
670 .existing_esurfaces
671 .iter()
672 .map(|&existing_esurface_id| {
673 let group_esurface_id = self
674 .overlap_builder
675 .counterterm_builder
676 .overlap_structure
677 .existing_esurfaces[existing_esurface_id];
678
679 match self.overlap_builder.counterterm_builder.esurface_map[group_esurface_id]
680 [self.overlap_builder.counterterm_builder.own_group_position]
681 {
682 Some(raised_esurface_id) => {
683 let esurface_id = self
684 .overlap_builder
685 .counterterm_builder
686 .raised_data
687 .raised_groups[raised_esurface_id]
688 .esurface_ids[0];
689 let esurface =
690 &self.overlap_builder.counterterm_builder.esurface_collection
691 [esurface_id];
692 let value = esurface.compute_from_momenta(
693 &self
694 .overlap_builder
695 .counterterm_builder
696 .graph
697 .loop_momentum_basis,
698 &self.overlap_builder.counterterm_builder.real_mass_vector,
699 &self.overlap_builder.rotated_center,
700 self.overlap_builder
701 .counterterm_builder
702 .sample
703 .external_moms(),
704 );
705 let is_valid = esurface_value_is_strictly_inside(
706 &value,
707 &self.overlap_builder.counterterm_builder.e_cm,
708 );
709 all_center_values_valid &= is_valid;
710 format!(
711 "existing={} group={} raised={} local={} edges={:?} value={:+16e} inside={}",
712 usize::from(existing_esurface_id),
713 group_esurface_id.0,
714 raised_esurface_id.0,
715 esurface_id.0,
716 esurface.energies,
717 value,
718 is_valid
719 )
720 }
721 None => {
722 format!(
723 "existing={} group={} absent_in_current_graph",
724 usize::from(existing_esurface_id),
725 group_esurface_id.0
726 )
727 }
728 }
729 })
730 .collect::<Vec<_>>();
731 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect, #center;
732 stage = "amplitude_threshold_center_values",
733 graph = %self.overlap_builder.counterterm_builder.graph.name,
734 selected_existing_esurface_id = usize::from(self._existing_esurface_id),
735 selected_group_esurface_id = self.group_esurface_id.0,
736 selected_raised_esurface_id = self.raised_esurface_id.0,
737 selected_esurface_id = self.esurface_id.0,
738 rotation_id = %self.overlap_builder.counterterm_builder.rotation_for_overlap.method,
739 center_provenance = "identity_frame_rotated_once",
740 overlap_group_size = self.overlap_builder.overlap_group.existing_esurfaces.len(),
741 radius = %format!("{:+16e}", self.overlap_builder.radius),
742 file.rotated_center = %format!("{}", self.overlap_builder.rotated_center),
743 file.surface_values = %center_surface_values.join("\n"),
744 "amplitude threshold center values"
745 );
746
747 if !all_center_values_valid {
748 warn!(
749 graph = %self.overlap_builder.counterterm_builder.graph.name,
750 selected_esurface_id = self.esurface_id.0,
751 rotation_id = %self.overlap_builder.counterterm_builder.rotation_for_overlap.method,
752 center_provenance = "identity_frame_rotated_once",
753 center = %self.overlap_builder.rotated_center,
754 surface_values = %center_surface_values.join("; "),
755 "refusing to evaluate an amplitude threshold counterterm with an invalid probe-frame overlap center"
756 );
757 return None;
758 }
759
760 let (raw_radius_guess, _) = self.esurface.get_radius_guess(
761 &self.overlap_builder.unit_shifted_momenta,
762 self.overlap_builder
763 .counterterm_builder
764 .sample
765 .external_moms(),
766 &self
767 .overlap_builder
768 .counterterm_builder
769 .graph
770 .loop_momentum_basis,
771 );
772
773 let function = |r: &_| {
774 self.esurface.compute_self_and_r_derivative(
775 r,
776 &self.overlap_builder.unit_shifted_momenta,
777 &self.overlap_builder.rotated_center,
778 self.overlap_builder
779 .counterterm_builder
780 .sample
781 .external_moms(),
782 &self.overlap_builder.counterterm_builder.real_mass_vector,
783 &self
784 .overlap_builder
785 .counterterm_builder
786 .graph
787 .loop_momentum_basis,
788 )
789 };
790
791 let zero = raw_radius_guess.zero();
792 let mut radius_guess = raw_radius_guess.clone();
793 if radius_guess.is_nan() || radius_guess.is_infinite() || radius_guess <= zero {
794 radius_guess = self.overlap_builder.counterterm_builder.e_cm.clone();
795 }
796 let tolerance = F::from_f64(
797 self.overlap_builder
798 .counterterm_builder
799 .settings
800 .subtraction
801 .radial_root_residual_tolerance,
802 );
803 let solution = match radial_root_diagnostics.solve(
804 radial_root_identity,
805 &zero,
806 &radius_guess,
807 function,
808 &tolerance,
809 MAX_ITERATIONS,
810 64,
811 &self.overlap_builder.counterterm_builder.e_cm,
812 ) {
813 Ok(solution) => solution,
814 Err(error) => {
815 warn!(
816 graph = %self.overlap_builder.counterterm_builder.graph.name,
817 esurface_id = self.esurface_id.0,
818 rotation_id = %self.overlap_builder.counterterm_builder.rotation_for_overlap.method,
819 center_provenance = "identity_frame_rotated_once",
820 raw_radius_guess = %raw_radius_guess,
821 radius_guess = %radius_guess,
822 error = %error,
823 "refusing to evaluate an amplitude threshold counterterm with an invalid radial solution"
824 );
825 return None;
826 }
827 };
828 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
829 stage = "amplitude_threshold_rstar_solution",
830 graph = %self.overlap_builder.counterterm_builder.graph.name,
831 existing_esurface_id = %self._existing_esurface_id,
832 group_esurface_id = self.group_esurface_id.0,
833 raised_esurface_id = self.raised_esurface_id.0,
834 esurface_id = self.esurface_id.0,
835 rotation_id = %self.overlap_builder.counterterm_builder.rotation_for_overlap.method,
836 center_provenance = "identity_frame_rotated_once",
837 radius_guess = %format!("{:+16e}", radius_guess),
838 radius_star = %format!("{:+16e}", solution.solution),
839 derivative = %format!("{:+16e}", solution.derivative_at_solution),
840 error = %format!("{:+16e}", solution.error_of_function),
841 iterations = solution.num_iterations_used,
842 nonfinite = false,
843 "amplitude threshold rstar solution"
844 );
845
846 Some(RstarSolution {
847 esurface_ct_builder: self,
848 solution,
849 })
850 }
851}
852
853struct RstarSolution<'a, T: FloatLike> {
854 esurface_ct_builder: EsurfaceCTBuilder<'a, T>,
855 solution: NewtonIterationResult<T>,
856}
857
858impl<'a, T: FloatLike> RstarSolution<'a, T> {
859 fn rstar_samples(self) -> RstarSample<'a, T> {
860 let rstar_loop_momenta = &self
861 .esurface_ct_builder
862 .overlap_builder
863 .unit_shifted_momenta
864 .rescale(&self.solution.solution, None)
865 + &self.esurface_ct_builder.overlap_builder.rotated_center;
866
867 let mut rstar_sample = self
868 .esurface_ct_builder
869 .overlap_builder
870 .counterterm_builder
871 .sample
872 .clone();
873
874 rstar_sample.sample.loop_moms = rstar_loop_momenta;
875
876 RstarSample {
877 rstar_solution: self,
878 rstar_sample,
879 }
880 }
881}
882
883struct RstarSample<'a, T: FloatLike> {
884 rstar_solution: RstarSolution<'a, T>,
885 rstar_sample: MomentumSample<T>,
886}
887
888impl<'a, T: FloatLike> RstarSample<'a, T> {
889 fn evaluate<'b, 'c: 'b>(
890 self,
891 param_builder: &mut ParamBuilder<f64>,
892 orientations: SingleOrAllOrientations<'a, OrientationID>,
893 evaluation_metadata: &mut EvaluationMetaData,
894 record_primary_timing: bool,
895 ct_evaluator: &mut AmplitudeCountertermEvaluator,
896 helper_evaluators: &mut [GenericEvaluator],
897 ) -> Result<Complex<F<T>>> {
898 let esurface_ct_builder = &self.rstar_solution.esurface_ct_builder;
899 let ct_builder = esurface_ct_builder.overlap_builder.counterterm_builder;
900
901 let esurface_id = esurface_ct_builder.esurface_id;
902
903 let model_params = param_builder
904 .model_values()
905 .iter()
906 .map(|c| Complex::new(F::from_ff64(c.re), F::from_ff64(c.im)))
907 .collect::<Vec<_>>();
908
909 let radius = self
910 .rstar_solution
911 .esurface_ct_builder
912 .overlap_builder
913 .radius
914 .clone();
915
916 let radius_star = self.rstar_solution.solution.solution.clone();
917 let e_cm = &ct_builder.e_cm;
918 let settings = &ct_builder
919 .settings
920 .subtraction
921 .local_ct_settings
922 .uv_localisation;
923
924 let integrated_settings = &ct_builder.settings.subtraction.integrated_ct_settings;
925
926 let uv_damp_plus = evaluate_uv_damper(&radius, &radius_star, e_cm, settings);
927 let uv_damp_minus = evaluate_uv_damper(&-&radius, &radius_star, e_cm, settings);
928
929 debug!("uv_damp_plus: {:?}", uv_damp_plus);
930 debug!("uv_damp_minus: {:?}", uv_damp_minus);
931 debug!("radius: {:?}", radius);
932 debug!("radius_star: {:?}", radius_star);
933
934 let h_function =
935 evaluate_integrated_ct_normalisation(&radius, &radius_star, e_cm, integrated_settings);
936 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
937 stage = "amplitude_threshold_prefactors",
938 graph = %ct_builder.graph.name,
939 esurface_id = esurface_id.0,
940 raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
941 radius = %format!("{:+16e}", radius),
942 radius_star = %format!("{:+16e}", radius_star),
943 derivative = %format!(
944 "{:+16e}",
945 self.rstar_solution.solution.derivative_at_solution
946 ),
947 uv_damp_plus = %format!("{:+16e}", uv_damp_plus),
948 uv_damp_minus = %format!("{:+16e}", uv_damp_minus),
949 h_function = %format!("{:+16e}", h_function),
950 "amplitude threshold prefactors"
951 );
952
953 let debug_diagnostics_enabled = tracing::event_enabled!(tracing::Level::DEBUG);
954 if debug_diagnostics_enabled {
955 let coincidence_tolerance = F::from_f64(1.0e-8) * e_cm;
956 for (candidate_esurface_id, candidate_esurface) in
957 ct_builder.esurface_collection.iter_enumerated()
958 {
959 let value = candidate_esurface.compute_from_momenta(
960 &ct_builder.graph.loop_momentum_basis,
961 &ct_builder.real_mass_vector,
962 self.rstar_sample.loop_moms(),
963 self.rstar_sample.external_moms(),
964 );
965 if value.abs() < coincidence_tolerance {
966 let candidate_raised_esurface_id = ct_builder
967 .raised_data
968 .raised_groups
969 .iter_enumerated()
970 .find_map(|(raised_esurface_id, raised_group)| {
971 raised_group
972 .esurface_ids
973 .contains(&candidate_esurface_id)
974 .then_some(raised_esurface_id.0)
975 });
976 let candidate_atom = candidate_esurface.to_atom(&[]).to_string();
977 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect, #esurface;
978 stage = "amplitude_threshold_rstar_coincident_esurface",
979 graph = %ct_builder.graph.name,
980 selected_esurface_id = esurface_id.0,
981 selected_raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
982 candidate_esurface_id = candidate_esurface_id.0,
983 candidate_raised_esurface_id = ?candidate_raised_esurface_id,
984 selected = candidate_esurface_id == esurface_id,
985 value = %format!("{:+16e}", value),
986 tolerance = %format!("{:+16e}", coincidence_tolerance),
987 file.atom = %candidate_atom,
988 "threshold rstar coincident esurface graph={} selected={} raised={} candidate={} candidate_raised={:?} value={} tol={} atom={}",
989 ct_builder.graph.name,
990 esurface_id.0,
991 esurface_ct_builder.raised_esurface_id.0,
992 candidate_esurface_id.0,
993 candidate_raised_esurface_id,
994 format!("{:+16e}", value),
995 format!("{:+16e}", coincidence_tolerance),
996 candidate_atom
997 );
998 }
999 }
1000 }
1001
1002 let mut total_ct = Complex::new_re(self.rstar_sample.zero());
1003
1004 for (cut_cff_index, evaluator_stack) in ct_evaluator.evaluator_stacks.iter_mut() {
1005 let order_index = cut_cff_index.left_threshold_order.unwrap() - 1;
1006 let (sample_for_order, threshold_params) = if order_index == 0 {
1007 debug!(
1008 "rescaled loop momenta at rstar:\n{}",
1009 self.rstar_sample.loop_moms()
1010 );
1011 (
1012 self.rstar_sample.clone(),
1013 ThresholdParams {
1014 radius: DualOrNot::NonDual(radius.clone()),
1015 radius_star: DualOrNot::NonDual(radius_star.clone()),
1016 esurface_derivative: DualOrNot::NonDual(
1017 self.rstar_solution.solution.derivative_at_solution.clone(),
1018 ),
1019 uv_damp_plus: DualOrNot::NonDual(uv_damp_plus.clone()),
1020 uv_damp_minus: DualOrNot::NonDual(uv_damp_minus.clone()),
1021 h_function: DualOrNot::NonDual(h_function.clone()),
1022 },
1023 )
1024 } else {
1025 let dual_shape = HyperDual::<F<T>>::new(simple_n_deriv_shape(order_index));
1026 let dual_radius_star = dual_shape.variable(0, radius_star.clone());
1027
1028 let dualized_center = self
1029 .rstar_solution
1030 .esurface_ct_builder
1031 .overlap_builder
1032 .rotated_center
1033 .iter()
1034 .map(|momentum| {
1035 momentum.map_ref(&|value| new_constant(&dual_radius_star, value))
1036 })
1037 .collect::<LoopMomenta<_>>();
1038 let dual_loop_momenta = self
1039 .rstar_solution
1040 .esurface_ct_builder
1041 .overlap_builder
1042 .unit_shifted_momenta
1043 .rescale_with_hyper_dual(&dual_radius_star, None)
1044 .iter()
1045 .zip(dualized_center.iter())
1046 .map(|(momentum, center)| momentum.clone() + center.clone())
1047 .collect::<LoopMomenta<_>>();
1048 debug!("rescaled loop momenta at rstar:\n{}", dual_loop_momenta);
1049 let mut sample_with_duals = self.rstar_sample.clone();
1050 sample_with_duals.sample.dual_loop_moms = Some(dual_loop_momenta);
1051
1052 (
1053 sample_with_duals,
1054 ThresholdParams {
1055 radius: DualOrNot::Dual(new_constant(&dual_radius_star, &radius)),
1056 radius_star: DualOrNot::Dual(dual_radius_star.clone()),
1057 esurface_derivative: DualOrNot::Dual(new_constant(
1058 &dual_radius_star,
1059 &self.rstar_solution.solution.derivative_at_solution.clone(),
1060 )),
1061 uv_damp_plus: DualOrNot::Dual(new_constant(
1062 &dual_radius_star,
1063 &uv_damp_plus,
1064 )),
1065 uv_damp_minus: DualOrNot::Dual(new_constant(
1066 &dual_radius_star,
1067 &uv_damp_minus,
1068 )),
1069 h_function: DualOrNot::Dual(new_constant(&dual_radius_star, &h_function)),
1070 },
1071 )
1072 };
1073
1074 let esurface_derivatives = if order_index == 0 {
1075 DualOrNot::NonDual(self.rstar_solution.solution.derivative_at_solution.clone())
1076 } else {
1077 let dual_shape_for_esurface =
1078 HyperDual::<F<T>>::new(simple_n_deriv_shape(order_index + 1));
1079 let dual_radius_star_for_esurface =
1080 dual_shape_for_esurface.variable(0, radius_star.clone());
1081 let dualized_center_for_esurface = self
1082 .rstar_solution
1083 .esurface_ct_builder
1084 .overlap_builder
1085 .rotated_center
1086 .iter()
1087 .map(|momentum| {
1088 momentum
1089 .map_ref(&|value| new_constant(&dual_radius_star_for_esurface, value))
1090 })
1091 .collect::<LoopMomenta<_>>();
1092 let dual_loop_momenta_for_esurface = self
1093 .rstar_solution
1094 .esurface_ct_builder
1095 .overlap_builder
1096 .unit_shifted_momenta
1097 .rescale_with_hyper_dual(&dual_radius_star_for_esurface, None)
1098 .iter()
1099 .zip(dualized_center_for_esurface.iter())
1100 .map(|(momentum, center)| momentum.clone() + center.clone())
1101 .collect::<LoopMomenta<_>>();
1102
1103 let dualized_externals = self
1104 .rstar_sample
1105 .external_moms()
1106 .iter()
1107 .map(|momentum| FourMomentum {
1108 temporal: Energy {
1109 value: new_constant(
1110 &dual_radius_star_for_esurface,
1111 &momentum.temporal.value,
1112 ),
1113 },
1114 spatial: momentum
1115 .spatial
1116 .map_ref(&|value| new_constant(&dual_radius_star_for_esurface, value)),
1117 })
1118 .collect();
1119
1120 DualOrNot::Dual(
1121 self.rstar_solution
1122 .esurface_ct_builder
1123 .esurface
1124 .compute_from_dual_momenta(
1125 &ct_builder.graph.loop_momentum_basis,
1126 &ct_builder.real_mass_vector,
1127 &dual_loop_momenta_for_esurface,
1128 &dualized_externals,
1129 ),
1130 )
1131 };
1132
1133 let params = T::get_parameters(
1134 param_builder,
1135 (false, false),
1136 ct_builder.graph,
1137 &sample_for_order,
1138 ct_builder.settings.kinematics.externals.get_helicities(),
1139 &ct_builder.settings.additional_params(),
1140 Some(&threshold_params),
1141 None,
1142 None,
1143 );
1144 let params_slice = params.as_slice();
1145 let params_nonfinite_count = params_slice
1146 .iter()
1147 .filter(|value| {
1148 value.re.is_nan()
1149 || value.re.is_infinite()
1150 || value.im.is_nan()
1151 || value.im.is_infinite()
1152 })
1153 .count();
1154 let first_params_nonfinite_index = params_slice.iter().position(|value| {
1155 value.re.is_nan()
1156 || value.re.is_infinite()
1157 || value.im.is_nan()
1158 || value.im.is_infinite()
1159 });
1160 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
1161 stage = "amplitude_threshold_pass_one_params",
1162 graph = %ct_builder.graph.name,
1163 esurface_id = esurface_id.0,
1164 raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
1165 order = order_index + 1,
1166 params_len = params_slice.len(),
1167 params_nonfinite_count,
1168 first_params_nonfinite_index = ?first_params_nonfinite_index,
1169 "amplitude threshold pass one params"
1170 );
1171
1172 let pass_one_result = evaluator_stack
1173 .evaluate(
1174 params,
1175 orientations,
1176 ct_builder.settings,
1177 evaluation_metadata,
1178 record_primary_timing,
1179 )
1180 .expect("Amplitude counterterm evaluator stack failed")
1181 .pop()
1182 .unwrap();
1183
1184 let prefactor = self.evaluate_multichanneling_prefactor(
1185 &sample_for_order,
1186 &model_params,
1187 evaluation_metadata,
1188 record_primary_timing,
1189 order_index,
1190 );
1191
1192 let raw_pass_one_is_nonfinite = match &pass_one_result {
1193 DualOrNot::Dual(dual_result) => dual_result.values.iter().any(|value| {
1194 value.re.is_nan()
1195 || value.re.is_infinite()
1196 || value.im.is_nan()
1197 || value.im.is_infinite()
1198 }),
1199 DualOrNot::NonDual(non_dual_result) => {
1200 non_dual_result.re.is_nan()
1201 || non_dual_result.re.is_infinite()
1202 || non_dual_result.im.is_nan()
1203 || non_dual_result.im.is_infinite()
1204 }
1205 };
1206 let prefactor_is_nonfinite = match &prefactor {
1207 DualOrNot::Dual(dual_result) => dual_result.values.iter().any(|value| {
1208 value.re.is_nan()
1209 || value.re.is_infinite()
1210 || value.im.is_nan()
1211 || value.im.is_infinite()
1212 }),
1213 DualOrNot::NonDual(non_dual_result) => {
1214 non_dual_result.re.is_nan()
1215 || non_dual_result.re.is_infinite()
1216 || non_dual_result.im.is_nan()
1217 || non_dual_result.im.is_infinite()
1218 }
1219 };
1220 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
1221 stage = "amplitude_threshold_pass_one_raw",
1222 graph = %ct_builder.graph.name,
1223 esurface_id = esurface_id.0,
1224 raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
1225 order = order_index + 1,
1226 raw_result = %format!("{pass_one_result}"),
1227 prefactor = %format!("{prefactor}"),
1228 raw_nonfinite = raw_pass_one_is_nonfinite,
1229 prefactor_nonfinite = prefactor_is_nonfinite,
1230 "amplitude threshold pass one raw"
1231 );
1232
1233 let pass_one_result = multiply_dual_or_not_complex(pass_one_result, &prefactor);
1234 let pass_one_is_nonfinite = match &pass_one_result {
1235 DualOrNot::Dual(dual_result) => dual_result.values.iter().any(|value| {
1236 value.re.is_nan()
1237 || value.re.is_infinite()
1238 || value.im.is_nan()
1239 || value.im.is_infinite()
1240 }),
1241 DualOrNot::NonDual(non_dual_result) => {
1242 non_dual_result.re.is_nan()
1243 || non_dual_result.re.is_infinite()
1244 || non_dual_result.im.is_nan()
1245 || non_dual_result.im.is_infinite()
1246 }
1247 };
1248 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
1249 stage = "amplitude_threshold_pass_one",
1250 graph = %ct_builder.graph.name,
1251 esurface_id = esurface_id.0,
1252 raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
1253 order = order_index + 1,
1254 prefactor = %format!("{prefactor}"),
1255 result = %format!("{pass_one_result}"),
1256 nonfinite = pass_one_is_nonfinite,
1257 "amplitude threshold pass one"
1258 );
1259
1260 debug!(
1261 "Pass one result for esurface {} order {}: {}",
1262 esurface_id.0,
1263 order_index + 1,
1264 pass_one_result
1265 );
1266
1267 let mut params_for_pass_two = vec![];
1268 match pass_one_result {
1269 DualOrNot::Dual(dual_result) => {
1270 params_for_pass_two
1271 .extend_from_slice(&extract_t_derivatives_complex(dual_result));
1272 }
1273 DualOrNot::NonDual(non_dual_result) => {
1274 params_for_pass_two.push(non_dual_result);
1275 }
1276 }
1277
1278 match esurface_derivatives {
1279 DualOrNot::Dual(dual_e_surface) => {
1280 extract_t_derivatives(dual_e_surface)[1..]
1281 .iter()
1282 .for_each(|value| params_for_pass_two.push(Complex::new_re(value.clone())));
1283 }
1284 DualOrNot::NonDual(non_dual_e_surface) => {
1285 params_for_pass_two.push(Complex::new_re(non_dual_e_surface));
1286 }
1287 }
1288
1289 params_for_pass_two.push(Complex::new_re(radius.clone()));
1290 params_for_pass_two.push(Complex::new_re(radius_star.clone()));
1291 params_for_pass_two.push(Complex::new_re(uv_damp_plus.clone()));
1292 params_for_pass_two.push(Complex::new_re(uv_damp_minus.clone()));
1293 params_for_pass_two.push(Complex::new_re(h_function.clone()));
1294
1295 let pass_two_result = evaluate_evaluator_single(
1296 &mut helper_evaluators[order_index],
1297 ¶ms_for_pass_two,
1298 evaluation_metadata,
1299 record_primary_timing,
1300 );
1301
1302 debug!(
1303 "Pass two result for esurface {} order {}: {}",
1304 esurface_id.0,
1305 order_index + 1,
1306 pass_two_result
1307 );
1308 let pass_two_is_nonfinite = pass_two_result.re.is_nan()
1309 || pass_two_result.re.is_infinite()
1310 || pass_two_result.im.is_nan()
1311 || pass_two_result.im.is_infinite();
1312 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
1313 stage = "amplitude_threshold_pass_two",
1314 graph = %ct_builder.graph.name,
1315 esurface_id = esurface_id.0,
1316 raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
1317 order = order_index + 1,
1318 result = %format!("{:+16e}", pass_two_result),
1319 nonfinite = pass_two_is_nonfinite,
1320 "amplitude threshold pass two"
1321 );
1322
1323 total_ct += pass_two_result;
1324 }
1325
1326 debug!(
1327 ct_eval = format!("{:+16e}", total_ct),
1328 "esurface {}", esurface_id.0
1329 );
1330 let total_ct_is_nonfinite = total_ct.re.is_nan()
1331 || total_ct.re.is_infinite()
1332 || total_ct.im.is_nan()
1333 || total_ct.im.is_infinite();
1334 crate::debug_tags!(#integration, #subtraction, #threshold, #inspect;
1335 stage = "amplitude_threshold_total",
1336 graph = %ct_builder.graph.name,
1337 esurface_id = esurface_id.0,
1338 raised_esurface_id = esurface_ct_builder.raised_esurface_id.0,
1339 result = %format!("{:+16e}", total_ct),
1340 nonfinite = total_ct_is_nonfinite,
1341 "amplitude threshold total"
1342 );
1343
1344 Ok(total_ct)
1345 }
1346
1347 fn evaluate_multichanneling_prefactor(
1348 &self,
1349 momentum_sample: &MomentumSample<T>,
1350 model_params: &[Complex<F<T>>],
1351 evaluation_metadata: &mut EvaluationMetaData,
1352 record_primary_timing: bool,
1353 order_index: usize,
1354 ) -> DualOrNot<Complex<F<T>>> {
1355 let overlap_builder = self.rstar_solution.esurface_ct_builder.overlap_builder;
1356 let overlap = overlap_builder.counterterm_builder.overlap_structure;
1357
1358 if overlap.overlap_groups.len() < 2 {
1359 return DualOrNot::NonDual(Complex::new_re(momentum_sample.one()));
1360 }
1361
1362 let multiplicative_offset = momentum_sample
1363 .sample
1364 .dual_loop_moms
1365 .as_ref()
1366 .map(|dual_loop_moms| dual_loop_moms.first().unwrap().px.values.len())
1367 .unwrap_or(1);
1368 let zero = Complex::new_re(momentum_sample.zero());
1369 let mut params = if let Some(dual_loop_moms) = &momentum_sample.sample.dual_loop_moms {
1370 dual_loop_moms
1371 .iter()
1372 .flat_map(|mom| {
1373 [
1374 mom.px.values.clone(),
1375 mom.py.values.clone(),
1376 mom.pz.values.clone(),
1377 ]
1378 .into_iter()
1379 .flatten()
1380 .map(Complex::new_re)
1381 .collect::<Vec<_>>()
1382 })
1383 .collect::<Vec<_>>()
1384 } else {
1385 momentum_sample
1386 .loop_moms()
1387 .iter()
1388 .flat_map(|momentum| {
1389 [
1390 momentum.px.clone(),
1391 momentum.py.clone(),
1392 momentum.pz.clone(),
1393 ]
1394 .into_iter()
1395 .map(Complex::new_re)
1396 .collect::<Vec<_>>()
1397 })
1398 .collect::<Vec<_>>()
1399 };
1400
1401 params.extend(momentum_sample.external_moms().iter().flat_map(|momentum| {
1402 [
1403 momentum.temporal.value.clone(),
1404 momentum.spatial.px.clone(),
1405 momentum.spatial.py.clone(),
1406 momentum.spatial.pz.clone(),
1407 ]
1408 .into_iter()
1409 .flat_map(|value| {
1410 std::iter::once(Complex::new_re(value))
1411 .chain((1..multiplicative_offset).map(|_| zero.clone()))
1412 .collect::<Vec<_>>()
1413 })
1414 .collect::<Vec<_>>()
1415 }));
1416 params.extend(model_params.iter().cloned().flat_map(|value| {
1417 std::iter::once(value)
1418 .chain((1..multiplicative_offset).map(|_| zero.clone()))
1419 .collect::<Vec<_>>()
1420 }));
1421
1422 let evaluator = overlap_builder
1423 .overlap_group
1424 .prefactor_evaluator
1425 .as_ref()
1426 .unwrap()
1427 .get(order_index)
1428 .expect("missing overlap prefactor evaluator for amplitude threshold order");
1429
1430 evaluate_evaluator(
1431 &mut evaluator.borrow_mut(),
1432 ¶ms,
1433 evaluation_metadata,
1434 record_primary_timing,
1435 )
1436 .pop()
1437 .expect("overlap prefactor evaluator should return exactly one value")
1438 }
1439}
1440
1441#[cfg(test)]
1442mod tests {
1443 use super::{AmplitudeCountertermAtom, AmplitudeCountertermData};
1444 use crate::{
1445 cff::{CutCFFIndex, esurface::RaisedEsurfaceId},
1446 graph::GraphGroupPosition,
1447 uv::Integrands,
1448 };
1449 use symbolica::{atom::Atom, symbol};
1450 use typed_index_collections::ti_vec;
1451
1452 #[test]
1453 fn empty_amplitude_counterterm_atom_is_not_generated() {
1454 let atom = AmplitudeCountertermAtom {
1455 parametric: std::iter::empty().collect(),
1456 };
1457
1458 assert!(!atom.is_generated());
1459 }
1460
1461 #[test]
1462 fn non_empty_amplitude_counterterm_atom_is_generated() {
1463 let atom = AmplitudeCountertermAtom {
1464 parametric: Integrands::from_iter([(
1465 CutCFFIndex::new_all_none(),
1466 Atom::var(symbol!("x")),
1467 )]),
1468 };
1469
1470 assert!(atom.is_generated());
1471 }
1472
1473 #[test]
1474 fn zero_like_amplitude_counterterm_atom_is_zero() {
1475 let atom = AmplitudeCountertermAtom {
1476 parametric: Integrands::from_iter([(
1477 CutCFFIndex::new_all_none(),
1478 Atom::var(symbol!("x")),
1479 )]),
1480 };
1481
1482 let zeroed = atom.zero_like();
1483
1484 assert_eq!(
1485 zeroed.parametric,
1486 Integrands::from_iter([(CutCFFIndex::new_all_none(), Atom::Zero)])
1487 );
1488 }
1489
1490 #[test]
1491 fn inactive_amplitude_esurface_guard_reports_runtime_access() {
1492 let mut data = AmplitudeCountertermData::new_empty(GraphGroupPosition(0));
1493 data.active_mask = ti_vec![false];
1494
1495 let error = data
1496 .ensure_active_raised_esurface(RaisedEsurfaceId(0))
1497 .unwrap_err();
1498 assert!(error.to_string().contains("generation marked it inactive"));
1499 }
1500}