1use std::{
2 collections::{BTreeMap, HashSet},
3 fmt::Display,
4};
5
6use color_eyre::eyre::Result;
7use colored::Colorize;
8use eyre::eyre;
9use itertools::Itertools;
10use linnet::half_edge::involution::{EdgeIndex, Orientation};
11use rand::Rng;
12use serde::Serialize;
13use spenso::algebra::complex::Complex;
14use symbolica::numerical_integration::MonteCarloRng;
15use tabled::{builder::Builder, settings::Style};
16use tracing::warn;
17use typed_index_collections::TiVec;
18
19use crate::{
20 DependentMomentaConstructor,
21 cff::esurface::{ExistingEsurfaceId, ExistingEsurfaces, GroupEsurfaceId, RaisedEsurfaceId},
22 graph::{FeynmanGraph, GraphGroupPosition, LmbError, lmb::LMBwithEdges},
23 integrands::{
24 evaluation::PreciseEvaluationResult,
25 process::{
26 GraphTerm, OrientationProfileMode, ProcessIntegrandImpl,
27 amplitude::{AmplitudeGraphTerm, AmplitudeIntegrand},
28 cross_section::{CrossSectionGraphTerm, CrossSectionIntegrand},
29 evaluate_profile_momentum_point_precise, orientation_labels_for_graph,
30 },
31 },
32 model::Model,
33 momentum::{
34 ThreeMomentum,
35 sample::{LoopIndex, LoopMomenta, MomentumSample},
36 },
37 observables::events::AdditionalWeightKey,
38 processes::ThresholdCountertermVariantId,
39 settings::{
40 RuntimeSettings, SamplingSettings,
41 runtime::{
42 DiscreteGraphSamplingSettings, DiscreteGraphSamplingType, ParameterizationMapping,
43 ParameterizationMode, ParameterizationSettings,
44 },
45 },
46 subtraction::amplitude_counterterm::OverlapStructureWithKinematics,
47 utils::{
48 ArbPrec, F, FloatLike, box_muller,
49 fitting::{constant_dropped_fit_points, log_log_slope_constant_dropped},
50 },
51};
52
53pub struct IRProfileSetting {
55 pub lambda_exp_start: f64,
56 pub lambda_exp_end: f64,
57 pub steps: usize,
58 pub seed: u64,
59 pub select_limits_and_graphs: Option<String>,
60 pub orientation_mode: OrientationProfileMode,
61 pub show_per_cut_info: bool,
62}
63
64impl AmplitudeGraphTerm {
65 fn enumerate_ir_limits(&self) -> Vec<IrLimit> {
66 let mut limits: HashSet<IrLimit> = HashSet::new();
67
68 let massless_edges: Vec<EdgeIndex> = self
69 .graph
70 .iter_edges_of(&!self.graph.tree_edges.clone())
71 .filter_map(|(_a, b, c)| {
72 if c.data.particle.is_massless() {
73 Some(b)
74 } else {
75 None
76 }
77 })
78 .collect();
79
80 for subset in massless_edges.iter().powerset() {
81 if subset.is_empty() {
82 continue;
83 }
84 let _lmb = match self.lmb_with_loop_edges(subset.as_slice()) {
85 Ok(lmb) => lmb,
86 Err(err) => match err {
87 LmbError::NotLoopEdges { .. } => {
88 continue;
90 }
91 a => panic!("Failed to build IR loop momentum basis for cut graph:\n{a}"),
92 },
93 };
94
95 let ir_limit = IrLimit::new_pure_soft(subset.into_iter().copied().collect());
96
97 limits.insert(ir_limit);
98 }
99
100 limits.into_iter().sorted().collect()
101 }
102}
103
104impl CrossSectionGraphTerm {
105 fn enumerate_ir_limits(&self) -> Vec<IrLimit> {
106 let mut limits: HashSet<IrLimit> = HashSet::new();
107 let loop_count = self.graph.loop_momentum_basis.loop_edges.len();
108
109 for cut_group in self.cut_group_data.cut_groups.iter() {
110 let mut limits_of_cut: HashSet<IrLimit> = HashSet::new();
111
112 let representative_cut_esurface = &self.cut_esurface[*cut_group.cuts.first().unwrap()];
113 let massless_edges_in_cut = representative_cut_esurface
114 .energies
115 .iter()
116 .filter(|edge_id| self.graph[**edge_id].particle.is_massless())
117 .copied()
118 .collect_vec();
119
120 if massless_edges_in_cut.len() >= 2 {
121 let subsets = massless_edges_in_cut
122 .iter()
123 .powerset()
124 .filter(|subset| {
125 subset.len() >= 2
126 && subset.len() <= loop_count
127 && subset.len() < representative_cut_esurface.energies.len()
128 })
129 .collect_vec();
130
131 for subset in subsets {
132 let ir_limit =
133 IrLimit::new_pure_colinear(subset.into_iter().copied().collect());
134 limits_of_cut.insert(ir_limit);
135 }
136 }
137
138 if !massless_edges_in_cut.is_empty() {
139 let subsets = massless_edges_in_cut
140 .iter()
141 .powerset()
142 .filter(|subset| {
143 !subset.is_empty()
144 && subset.len() <= loop_count
145 && subset.len() < representative_cut_esurface.energies.len()
146 })
147 .collect_vec();
148 for subset in subsets {
149 let ir_limit = IrLimit::new_pure_soft(subset.into_iter().copied().collect());
150 limits_of_cut.insert(ir_limit);
151 }
152 }
153
154 for limit in limits_of_cut.drain() {
155 limits.insert(limit);
156 }
157 }
158
159 limits.into_iter().sorted().collect()
160 }
161}
162
163pub struct IrLimitTestReport {
164 pub all_passed: bool,
165 pub results_per_graph: Vec<GraphIRLimitReport>,
166}
167
168pub struct GraphIRLimitReport {
169 pub graph_name: String,
170 pub all_limits_passed: bool,
171 pub cut_definitions: Vec<GraphCutDefinition>,
172 pub single_limit_reports: Vec<SingleLimitReport>,
173}
174
175#[derive(Debug, Clone)]
176pub struct GraphCutDefinition {
177 pub cut_id: usize,
178 pub edges: Vec<EdgeIndex>,
179}
180
181pub struct SingleLimitReport {
182 pub limit_name: String,
183 pub orientation_label: Option<String>,
184 pub ray_fingerprint: Option<ProfileRayFingerprint>,
185 pub passed: bool,
186 pub power_law_fit: PowerLawFit,
187 pub scaling: f64,
188 pub per_cut_reports: Vec<CutLimitReport>,
189 pub display_only_reports: Vec<DisplayOnlyLimitReport>,
190 num_soft: usize,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
195pub struct ProfileRayFingerprint {
196 pub lmb_edges: Vec<usize>,
197 pub digest: String,
198}
199
200type ProfileRayPoint = (F<f64>, Vec<ThreeMomentum<F<f64>>>);
201
202pub struct CutLimitReport {
203 pub cut_id: usize,
204 pub power_law_fit: Option<PowerLawFit>,
205 pub scaling: Option<f64>,
206 pub fit_error: Option<String>,
207}
208
209pub struct DisplayOnlyLimitReport {
210 pub label: String,
211 pub power_law_fit: Option<PowerLawFit>,
212 pub scaling: Option<f64>,
213 pub fit_error: Option<String>,
214}
215
216impl Display for IrLimitTestReport {
217 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218 let overall_status = if self.all_passed {
219 "PASS".green().bold()
220 } else {
221 "FAIL".red().bold()
222 };
223
224 let passed_graphs = self
225 .results_per_graph
226 .iter()
227 .filter(|graph_report| graph_report.all_limits_passed)
228 .count();
229
230 writeln!(
231 f,
232 "IR limit tests: {} ({}/{})",
233 overall_status,
234 passed_graphs,
235 self.results_per_graph.len()
236 )?;
237
238 let mut graph_summary_table = Builder::new();
239 graph_summary_table.push_record(["graph", "status", "passed", "total"]);
240
241 for graph_report in &self.results_per_graph {
242 let graph_status = if graph_report.all_limits_passed {
243 "PASS".green().bold().to_string()
244 } else {
245 "FAIL".red().bold().to_string()
246 };
247
248 let passed_limits = graph_report
249 .single_limit_reports
250 .iter()
251 .filter(|report| report.passed)
252 .count();
253
254 graph_summary_table.push_record([
255 graph_report.graph_name.clone(),
256 graph_status,
257 passed_limits.to_string(),
258 graph_report.single_limit_reports.len().to_string(),
259 ]);
260 }
261
262 writeln!(f, "{}", graph_summary_table.build().with(Style::rounded()))?;
263
264 for graph_report in &self.results_per_graph {
265 writeln!(f)?;
266 writeln!(f, "{graph_report}")?;
267 }
268
269 Ok(())
270 }
271}
272
273impl Display for GraphIRLimitReport {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 let graph_status = if self.all_limits_passed {
276 "PASS".green().bold()
277 } else {
278 "FAIL".red().bold()
279 };
280
281 let passed_limits = self
282 .single_limit_reports
283 .iter()
284 .filter(|report| report.passed)
285 .count();
286
287 writeln!(
288 f,
289 " {} {} ({}/{})",
290 graph_status,
291 self.graph_name.bold(),
292 passed_limits,
293 self.single_limit_reports.len()
294 )?;
295
296 if !self.cut_definitions.is_empty() {
297 render_graph_cut_definitions(f, &self.cut_definitions)?;
298 writeln!(f)?;
299 }
300
301 let mut limit_table = Builder::new();
302 let mut separators_after_data_rows = Vec::new();
303 let mut data_row_count = 0;
304 limit_table.push_record([
305 "status",
306 "limit",
307 "orientation",
308 "item",
309 "scaling",
310 "p",
311 "r_squared",
312 "n_soft",
313 ]);
314
315 for (report_index, report) in self.single_limit_reports.iter().enumerate() {
316 let status = if report.passed {
317 "PASS".green().bold().to_string()
318 } else {
319 "FAIL".red().bold().to_string()
320 };
321
322 limit_table.push_record([
323 status,
324 report.limit_name.clone(),
325 report
326 .orientation_label
327 .clone()
328 .unwrap_or_else(|| "sum".to_string()),
329 "sum".to_string(),
330 format!("{:+.4}", report.scaling),
331 format!("{:+.4}", report.power_law_fit.exponent),
332 format!("{:.4}", report.power_law_fit.r_squared),
333 report.num_soft.to_string(),
334 ]);
335 data_row_count += 1;
336
337 if !report.per_cut_reports.is_empty() || !report.display_only_reports.is_empty() {
338 separators_after_data_rows.push(data_row_count);
339 }
340
341 for cut_report in &report.per_cut_reports {
342 let [item, scaling, exponent, r_squared] = cut_report_display_row(cut_report);
343 limit_table.push_record([
344 "INFO".cyan().bold().to_string(),
345 String::new(),
346 String::new(),
347 item,
348 scaling,
349 exponent,
350 r_squared,
351 String::new(),
352 ]);
353 data_row_count += 1;
354 }
355
356 for display_only_report in &report.display_only_reports {
357 let [item, scaling, exponent, r_squared] =
358 display_only_report_display_row(display_only_report);
359 limit_table.push_record([
360 "INFO".cyan().bold().to_string(),
361 String::new(),
362 String::new(),
363 item,
364 scaling,
365 exponent,
366 r_squared,
367 String::new(),
368 ]);
369 data_row_count += 1;
370 }
371
372 if report_index + 1 < self.single_limit_reports.len() {
373 separators_after_data_rows.push(data_row_count);
374 }
375 }
376
377 let mut table = limit_table.build();
378 table.with(Style::rounded());
379 let rendered =
380 insert_limit_table_separators(table.to_string(), &separators_after_data_rows);
381
382 write!(f, "{rendered}")?;
383
384 Ok(())
385 }
386}
387
388impl Display for SingleLimitReport {
389 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390 let status = if self.passed {
391 "PASS".green().bold()
392 } else {
393 "FAIL".red().bold()
394 };
395
396 write!(
397 f,
398 "{} {}{} | scaling={:+.4} | p={:+.4} | R²={:.4} | n_soft={}",
399 status,
400 self.limit_name,
401 self.orientation_label
402 .as_ref()
403 .map(|label| format!(" @ {label}"))
404 .unwrap_or_default(),
405 self.scaling,
406 self.power_law_fit.exponent,
407 self.power_law_fit.r_squared,
408 self.num_soft
409 )?;
410
411 render_per_cut_reports(f, self, "")?;
412 render_display_only_reports(f, self, "")
413 }
414}
415
416fn render_per_cut_reports(
417 f: &mut std::fmt::Formatter<'_>,
418 report: &SingleLimitReport,
419 indent: &str,
420) -> std::fmt::Result {
421 if report.per_cut_reports.is_empty() {
422 return Ok(());
423 }
424
425 writeln!(f)?;
426 writeln!(
427 f,
428 "\n{indent}per-cut fits for {}{}",
429 report.limit_name,
430 report
431 .orientation_label
432 .as_ref()
433 .map(|label| format!(" @ {label}"))
434 .unwrap_or_default(),
435 )?;
436
437 let mut cut_table = Builder::new();
438 cut_table.push_record(["cut", "scaling", "p", "r_squared"]);
439
440 for cut_report in &report.per_cut_reports {
441 cut_table.push_record(cut_report_display_row(cut_report));
442 }
443
444 write!(f, "{}", cut_table.build().with(Style::rounded()))
445}
446
447fn render_graph_cut_definitions(
448 f: &mut std::fmt::Formatter<'_>,
449 cut_definitions: &[GraphCutDefinition],
450) -> std::fmt::Result {
451 writeln!(f, " cut definitions")?;
452
453 let mut cut_table = Builder::new();
454 cut_table.push_record(["cut", "edges"]);
455
456 for cut_definition in cut_definitions {
457 cut_table.push_record([
458 cut_definition.cut_id.to_string(),
459 cut_definition
460 .edges
461 .iter()
462 .map(ToString::to_string)
463 .join(", "),
464 ]);
465 }
466
467 writeln!(f, "{}", cut_table.build().with(Style::rounded()))
468}
469
470fn render_display_only_reports(
471 f: &mut std::fmt::Formatter<'_>,
472 report: &SingleLimitReport,
473 indent: &str,
474) -> std::fmt::Result {
475 if report.display_only_reports.is_empty() {
476 return Ok(());
477 }
478
479 writeln!(f)?;
480 writeln!(
481 f,
482 "\n{indent}display-only fits for {}{}",
483 report.limit_name,
484 report
485 .orientation_label
486 .as_ref()
487 .map(|label| format!(" @ {label}"))
488 .unwrap_or_default(),
489 )?;
490
491 let mut display_only_table = Builder::new();
492 display_only_table.push_record(["component", "scaling", "p", "r_squared"]);
493
494 for display_only_report in &report.display_only_reports {
495 display_only_table.push_record(display_only_report_display_row(display_only_report));
496 }
497
498 write!(f, "{}", display_only_table.build().with(Style::rounded()))
499}
500
501fn format_missing_fit_item(label: String, fit_error: Option<&str>) -> String {
502 match fit_error {
503 Some(error) => format!("{label} (no fit: {error})"),
504 None => label,
505 }
506}
507
508fn cut_report_display_row(cut_report: &CutLimitReport) -> [String; 4] {
509 let (scaling, exponent, r_squared) = match (&cut_report.power_law_fit, &cut_report.fit_error) {
510 (Some(fit), None) => (
511 format!("{:+.4}", cut_report.scaling.unwrap_or(fit.exponent)),
512 format!("{:+.4}", fit.exponent),
513 format!("{:.4}", fit.r_squared),
514 ),
515 (None, Some(_)) => ("-".to_string(), "-".to_string(), "-".to_string()),
516 _ => ("-".to_string(), "-".to_string(), "-".to_string()),
517 };
518
519 [
520 format_missing_fit_item(
521 format!("cut {}", cut_report.cut_id),
522 cut_report.fit_error.as_deref(),
523 ),
524 scaling,
525 exponent,
526 r_squared,
527 ]
528}
529
530fn display_only_report_display_row(display_only_report: &DisplayOnlyLimitReport) -> [String; 4] {
531 let (scaling, exponent, r_squared) = match (
532 &display_only_report.power_law_fit,
533 &display_only_report.fit_error,
534 ) {
535 (Some(fit), None) => (
536 format!(
537 "{:+.4}",
538 display_only_report.scaling.unwrap_or(fit.exponent)
539 ),
540 format!("{:+.4}", fit.exponent),
541 format!("{:.4}", fit.r_squared),
542 ),
543 (None, Some(_)) => ("-".to_string(), "-".to_string(), "-".to_string()),
544 _ => ("-".to_string(), "-".to_string(), "-".to_string()),
545 };
546
547 [
548 format_missing_fit_item(
549 display_only_report.label.clone(),
550 display_only_report.fit_error.as_deref(),
551 ),
552 scaling,
553 exponent,
554 r_squared,
555 ]
556}
557
558fn insert_limit_table_separators(rendered: String, separators_after_data_rows: &[usize]) -> String {
559 if separators_after_data_rows.is_empty() {
560 return rendered;
561 }
562
563 let lines = rendered.lines().collect_vec();
564 if lines.len() < 4 {
565 return rendered;
566 }
567
568 let line_count = lines.len();
569 let separator_line = lines[2].to_string();
570 let mut next_separator = separators_after_data_rows.iter().copied().peekable();
571 let mut output = Vec::with_capacity(lines.len() + separators_after_data_rows.len());
572 let mut seen_data_rows = 0;
573
574 for (line_index, line) in lines.into_iter().enumerate() {
575 output.push(line.to_string());
576
577 if line_index >= 3 && line_index + 1 < line_count {
578 seen_data_rows += 1;
579 while next_separator.peek().copied() == Some(seen_data_rows) {
580 output.push(separator_line.clone());
581 next_separator.next();
582 }
583 }
584 }
585
586 output.join("\n")
587}
588
589fn ir_profile_completion_entries(
590 limits: Vec<(String, Vec<ProfileLimit>)>,
591) -> Vec<(String, Vec<String>)> {
592 limits
593 .into_iter()
594 .map(|(graph_name, limits)| {
595 (
596 graph_name,
597 limits.into_iter().map(|limit| limit.to_string()).collect(),
598 )
599 })
600 .collect()
601}
602
603fn graph_cut_definitions_for_cross_section_term(
604 graph_term: &CrossSectionGraphTerm,
605) -> Vec<GraphCutDefinition> {
606 graph_term
607 .cuts
608 .iter_enumerated()
609 .map(|(cut_id, cut)| GraphCutDefinition {
610 cut_id: cut_id.into(),
611 edges: graph_term
612 .graph
613 .underlying
614 .iter_edges_of(&cut.cut)
615 .map(|(_, edge_id, _)| edge_id)
616 .sorted()
617 .collect(),
618 })
619 .collect()
620}
621
622fn graph_id_by_name<I: ProcessIntegrandImpl>(integrand: &I, graph_name: &str) -> Option<usize> {
623 (0..integrand.graph_count())
624 .find(|graph_id| integrand.get_graph(*graph_id).name() == graph_name)
625}
626
627fn parse_select_limits_and_graphs<I: ProcessIntegrandImpl>(
628 integrand: &I,
629 input: &str,
630) -> Result<Vec<(String, Vec<ProfileLimit>)>> {
631 input
632 .split(';')
633 .map(|graph_info_string| {
634 let mut parts = graph_info_string.split(' ');
635 let graph_name = parts
636 .next()
637 .ok_or_else(|| eyre!("Expected graph name in select_limits_and_graphs"))?
638 .to_string();
639
640 let Some(graph_id) = graph_id_by_name(integrand, &graph_name) else {
641 return Err(eyre!(
642 "Graph name '{}' in select_limits_and_graphs does not match any graph in the integrand",
643 graph_name
644 ));
645 };
646
647 let profile_limits = parts
648 .map(ProfileLimit::parse_limit)
649 .collect::<Result<Vec<_>, _>>()?;
650
651 if profile_limits.is_empty() {
652 return Err(eyre!(
653 "No limits specified for graph '{}' in select_limits_and_graphs",
654 graph_name
655 ));
656 }
657
658 let loop_number = integrand
659 .get_graph(graph_id)
660 .get_graph()
661 .loop_momentum_basis
662 .loop_edges
663 .len();
664 if profile_limits
665 .iter()
666 .any(|limit| limit.is_valid(loop_number).is_err())
667 {
668 return Err(eyre!(
669 "One or more limits specified for graph '{}' in select_limits_and_graphs are not valid",
670 graph_name
671 ));
672 }
673
674 Ok((graph_name, profile_limits))
675 })
676 .collect::<Result<Vec<_>, _>>()
677}
678
679fn requested_orientations<I: ProcessIntegrandImpl>(
680 integrand: &I,
681 graph_id: usize,
682 settings: &IRProfileSetting,
683) -> Result<Vec<(Option<usize>, Option<String>)>> {
684 if settings.orientation_mode.profiles_per_orientation() {
685 Ok(orientation_labels_for_graph(integrand, graph_id)?
686 .into_iter()
687 .enumerate()
688 .map(|(orientation_id, label)| (Some(orientation_id), Some(label)))
689 .collect())
690 } else {
691 Ok(vec![(None, None)])
692 }
693}
694
695fn build_single_limit_report(
696 ir_limit: &IrLimit,
697 orientation_label: Option<String>,
698 slope: PowerLawFit,
699 per_cut_reports: Vec<CutLimitReport>,
700) -> SingleLimitReport {
701 let num_soft = ir_limit.num_soft();
702 let scaling = slope.exponent + ((num_soft * 3) as f64);
703 SingleLimitReport {
704 limit_name: format!("{}", ir_limit),
705 orientation_label,
706 ray_fingerprint: None,
707 passed: scaling > 0.0,
708 power_law_fit: slope,
709 scaling,
710 per_cut_reports,
711 display_only_reports: Vec::new(),
712 num_soft,
713 }
714}
715
716fn build_threshold_limit_report(
717 threshold_limit: &ThresholdLimit,
718 orientation_label: Option<String>,
719 slope: PowerLawFit,
720 per_cut_reports: Vec<CutLimitReport>,
721) -> SingleLimitReport {
722 let scaling = slope.exponent;
723 SingleLimitReport {
724 limit_name: format!("{}", threshold_limit),
725 orientation_label,
726 ray_fingerprint: None,
727 passed: scaling > 0.0,
728 power_law_fit: slope,
729 scaling,
730 per_cut_reports,
731 display_only_reports: Vec::new(),
732 num_soft: 0,
733 }
734}
735
736fn build_cut_limit_reports(
737 num_soft: usize,
738 cut_fits: Vec<(usize, Result<PowerLawFit>)>,
739) -> Vec<CutLimitReport> {
740 cut_fits
741 .into_iter()
742 .map(|(cut_id, fit)| match fit {
743 Ok(power_law_fit) => CutLimitReport {
744 cut_id,
745 scaling: Some(power_law_fit.exponent + ((num_soft * 3) as f64)),
746 power_law_fit: Some(power_law_fit),
747 fit_error: None,
748 },
749 Err(error) => CutLimitReport {
750 cut_id,
751 scaling: None,
752 power_law_fit: None,
753 fit_error: Some(error.to_string()),
754 },
755 })
756 .collect()
757}
758
759fn build_display_only_limit_reports(
760 component_fits: Vec<(AdditionalWeightKey, Result<PowerLawFit>)>,
761) -> Vec<DisplayOnlyLimitReport> {
762 component_fits
763 .into_iter()
764 .map(|(key, fit)| match fit {
765 Ok(power_law_fit) => DisplayOnlyLimitReport {
766 label: display_only_limit_label(key),
767 scaling: Some(power_law_fit.exponent),
768 power_law_fit: Some(power_law_fit),
769 fit_error: None,
770 },
771 Err(error) => DisplayOnlyLimitReport {
772 label: display_only_limit_label(key),
773 scaling: None,
774 power_law_fit: None,
775 fit_error: Some(error.to_string()),
776 },
777 })
778 .collect()
779}
780
781fn display_only_limit_label(key: AdditionalWeightKey) -> String {
782 match key {
783 AdditionalWeightKey::Original => "original".to_string(),
784 AdditionalWeightKey::AmplitudeThresholdCounterterm {
785 esurface_id,
786 overlap_group,
787 } => format!("ct_{esurface_id}_{overlap_group}"),
788 AdditionalWeightKey::AmplitudeThresholdCountertermVariant {
789 variant_id,
790 esurface_id,
791 overlap_group,
792 } => format!("ct_variant_{variant_id}_{esurface_id}_{overlap_group}"),
793 AdditionalWeightKey::ThresholdCounterterm { subset_index } => {
794 format!("ct_{subset_index}")
795 }
796 AdditionalWeightKey::FullMultiplicativeFactor => "full multiplicative factor".to_string(),
797 }
798}
799
800fn threshold_approach_loop_momenta<T: FloatLike>(
801 overlap_group_center: &LoopMomenta<F<f64>>,
802 threshold_point: &MomentumSample<T>,
803 lambda: &F<T>,
804) -> LoopMomenta<F<T>> {
805 let overlap_group_center = overlap_group_center.cast::<T>();
806 let threshold_loop_momenta = threshold_point.loop_moms();
807 let offset_towards_center =
808 (&overlap_group_center - threshold_loop_momenta).rescale(lambda, None);
809
810 threshold_loop_momenta + &offset_towards_center
811}
812
813fn run_ir_profile<I: ProcessIntegrandImpl>(
814 integrand: &mut I,
815 ir_profile_settings: &IRProfileSetting,
816 model: &Model,
817 enumerate_limits: impl Fn(&I) -> Vec<(String, Vec<ProfileLimit>)>,
818 graph_cut_definitions: impl Fn(&I, usize) -> Vec<GraphCutDefinition>,
819 points_on_threshold: &[OverlapStructureWithKinematics<ArbPrec>],
820 mut test_single_limit: impl FnMut(
821 &mut I,
822 usize,
823 &ProfileLimit,
824 &mut MonteCarloRng,
825 &IRProfileSetting,
826 &Model,
827 &[OverlapStructureWithKinematics<ArbPrec>],
828 ) -> Result<Vec<SingleLimitReport>>,
829) -> Result<IrLimitTestReport> {
830 let mut rng = MonteCarloRng::new(ir_profile_settings.seed, 0);
831 let limits_to_check =
832 if let Some(select_limits_and_graphs) = &ir_profile_settings.select_limits_and_graphs {
833 parse_select_limits_and_graphs(integrand, select_limits_and_graphs)?
834 } else {
835 enumerate_limits(integrand)
836 };
837
838 let mut result = IrLimitTestReport {
839 all_passed: false,
840 results_per_graph: Vec::new(),
841 };
842
843 for (graph_name, limits) in limits_to_check {
844 let Some(graph_id) = graph_id_by_name(integrand, &graph_name) else {
845 return Err(eyre!("Graph name '{}' not found in integrand", graph_name));
846 };
847
848 let mut graph_report = GraphIRLimitReport {
849 graph_name: graph_name.clone(),
850 all_limits_passed: false,
851 cut_definitions: graph_cut_definitions(integrand, graph_id),
852 single_limit_reports: Vec::new(),
853 };
854
855 for limit in limits {
856 graph_report.single_limit_reports.extend(test_single_limit(
857 integrand,
858 graph_id,
859 &limit,
860 &mut rng,
861 ir_profile_settings,
862 model,
863 points_on_threshold,
864 )?);
865 }
866
867 graph_report.all_limits_passed = graph_report
868 .single_limit_reports
869 .iter()
870 .all(|report| report.passed);
871
872 result.results_per_graph.push(graph_report);
873 }
874
875 result.all_passed = result
876 .results_per_graph
877 .iter()
878 .all(|graph_report| graph_report.all_limits_passed);
879
880 Ok(result)
881}
882
883impl AmplitudeIntegrand {
884 pub fn ir_profile_completion_entries(&self) -> Vec<(String, Vec<String>)> {
885 ir_profile_completion_entries(self.enumerate_ir_limits())
886 }
887
888 pub fn test_ir(
889 &mut self,
890 ir_profile_settings: &IRProfileSetting,
891 model: &Model,
892 ) -> Result<IrLimitTestReport> {
893 self.settings.sampling = SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
895 graph_names: Vec::new(),
896 sample_orientations: false,
897 sampling_type: DiscreteGraphSamplingType::Default(ParameterizationSettings {
898 mode: ParameterizationMode::MomentumSpace,
899 mapping: ParameterizationMapping::default(),
900 b: 10.0,
901 power: 1.0,
902 lmb_basis_ids: Default::default(),
903 sampling_channels: Default::default(),
904 }),
905 });
906
907 let previous_generate_events = self.settings.general.generate_events;
908 let previous_store_additional_weights =
909 self.settings.general.store_additional_weights_in_event;
910 self.settings.general.generate_events = true;
911 self.settings.general.store_additional_weights_in_event = true;
912
913 let result = (|| {
914 self.warm_up(model)?;
915
916 let dependent_momenta_constructor = DependentMomentaConstructor::Amplitude(
917 &self.data.graph_terms[0].graph.get_external_signature(),
918 );
919
920 let mut rng = MonteCarloRng::new(ir_profile_settings.seed, 0);
921 let random_loop_momenta = (0..self.data.graph_terms[0]
922 .graph
923 .loop_momentum_basis
924 .loop_edges
925 .len())
926 .map(|_| sample_random_unit_vector(&mut rng))
927 .collect();
928
929 let momentum_sample = MomentumSample::new(
930 random_loop_momenta,
931 0,
932 &self.settings.kinematics.externals,
933 0,
934 F::from_f64(1.0),
935 dependent_momenta_constructor,
936 None,
937 )?;
938
939 let points_on_threshold =
940 self.kinematics_for_threshold_approach(&momentum_sample, model)?;
941
942 run_ir_profile(
943 self,
944 ir_profile_settings,
945 model,
946 Self::enumerate_ir_limits,
947 Self::graph_cut_definitions,
948 &points_on_threshold,
949 Self::test_single_ir_limit_impl,
950 )
951 })();
952
953 if self.settings.general.generate_events != previous_generate_events {
954 self.settings.general.generate_events = previous_generate_events;
955 }
956 if self.settings.general.store_additional_weights_in_event
957 != previous_store_additional_weights
958 {
959 self.settings.general.store_additional_weights_in_event =
960 previous_store_additional_weights;
961 }
962
963 result
964 }
965
966 fn enumerate_ir_limits(&self) -> Vec<(String, Vec<ProfileLimit>)> {
967 self.data
968 .graph_terms
969 .iter()
970 .map(|term| {
971 let graph_name = term.graph.name.clone();
972 let mut limits = term
973 .enumerate_ir_limits()
974 .into_iter()
975 .map(ProfileLimit::Ir)
976 .collect_vec();
977 let threshold_limits = if term.threshold_counterterm.legacy_equivalent {
978 ThresholdLimit::enumerate_from_overlap_structure(
979 &term.threshold_counterterm.overlap.existing_esurfaces,
980 &term.threshold_counterterm.esurface_map,
981 term.threshold_counterterm.own_group_position,
982 )
983 } else {
984 term.threshold_counterterm
985 .variant_raised_esurfaces
986 .iter_enumerated()
987 .filter(|(variant_id, _)| {
988 term.threshold_counterterm.variant_generated_mask[*variant_id]
989 && term.threshold_counterterm.variant_active_mask[*variant_id]
990 })
991 .map(|(variant_id, &esurface_id)| ThresholdLimit {
992 esurface_id,
993 variant_id: Some(variant_id),
994 })
995 .collect()
996 };
997 limits.extend(threshold_limits.into_iter().map(ProfileLimit::Threshold));
998 limits.sort();
999 limits.dedup();
1000 (graph_name, limits)
1001 })
1002 .collect()
1003 }
1004
1005 fn graph_cut_definitions(&self, _graph_id: usize) -> Vec<GraphCutDefinition> {
1006 Vec::new()
1007 }
1008
1009 fn test_single_ir_limit_impl(
1010 &mut self,
1011 graph_id: usize,
1012 profile_limit: &ProfileLimit,
1013 rng: &mut MonteCarloRng,
1014 approach_settings: &IRProfileSetting,
1015 model: &Model,
1016 points_on_threshold: &[OverlapStructureWithKinematics<ArbPrec>],
1017 ) -> Result<Vec<SingleLimitReport>> {
1018 match profile_limit {
1019 ProfileLimit::Ir(ir_limit) => {
1020 let edges_in_limit = ir_limit.get_all_edges()?;
1021 let lmb = self.data.graph_terms[graph_id]
1022 .lmb_with_loop_edges(edges_in_limit.as_slice())?;
1023 let momenta = ir_limit.get_momenta(rng, &self.settings, approach_settings)?;
1024 let non_limit_loops = lmb
1025 .loop_edges
1026 .iter_enumerated()
1027 .filter_map(|(loop_id, edge_id)| {
1028 if !edges_in_limit.contains(edge_id) {
1029 Some(loop_id)
1030 } else {
1031 None
1032 }
1033 })
1034 .collect_vec();
1035
1036 let non_limit_momenta = non_limit_loops
1037 .iter()
1038 .map(|loop_id| (*loop_id, sample_random_unit_vector(rng)))
1039 .collect_vec();
1040
1041 let externals = self.data.graph_terms[graph_id]
1042 .graph
1043 .get_external_signature();
1044
1045 let dependent_momenta_constructor =
1046 DependentMomentaConstructor::Amplitude(&externals);
1047
1048 let loop_number = lmb.loop_edges.len();
1049 let orientations = requested_orientations(self, graph_id, approach_settings)?;
1050 let mut reports = Vec::with_capacity(orientations.len());
1051
1052 for (orientation, orientation_label) in orientations {
1053 let mut limit_data = LimitData { data: Vec::new() };
1054 let mut ray_points = Vec::with_capacity(momenta.len());
1055
1056 for (loop_mom_id, lambda_point) in momenta.iter().cloned().enumerate() {
1057 let mut loop_moms: LoopMomenta<F<_>> = (0..loop_number)
1058 .map(|_| {
1059 ThreeMomentum::new(
1060 F::from_f64(0.0),
1061 F::from_f64(0.0),
1062 F::from_f64(0.0),
1063 )
1064 })
1065 .collect();
1066
1067 for (loop_id, momentum) in non_limit_momenta.iter() {
1068 loop_moms[*loop_id] = *momentum;
1069 }
1070
1071 for tagged_momenta in &lambda_point.momenta {
1072 let edge_id = tagged_momenta.tag;
1073 let loop_id = lmb
1074 .loop_edges
1075 .iter()
1076 .position(|loop_edge| loop_edge == &edge_id)
1077 .unwrap_or_else(|| {
1078 unreachable!("corrupted lmb and ir limit: {}", ir_limit);
1079 });
1080
1081 loop_moms[LoopIndex(loop_id)] = tagged_momenta.momentum;
1082 }
1083
1084 let sample_in_cmb = MomentumSample::new(
1085 loop_moms,
1086 loop_mom_id,
1087 &self.settings.kinematics.externals,
1088 0,
1089 F::from_f64(1.0),
1090 dependent_momenta_constructor,
1091 orientation,
1092 )?;
1093
1094 let sample = sample_in_cmb.lmb_transform(
1095 &lmb,
1096 &self.data.graph_terms[graph_id].graph.loop_momentum_basis,
1097 );
1098 let routed_loop_momenta = sample.loop_moms().iter().cloned().collect_vec();
1099 ray_points.push((lambda_point.lambda, routed_loop_momenta.clone()));
1100
1101 limit_data.data.push(LambdaPointEval {
1102 lambda: lambda_point.lambda,
1103 value: evaluate_profile_momentum_point_arb(
1104 self,
1105 model,
1106 graph_id,
1107 orientation,
1108 routed_loop_momenta,
1109 approach_settings.show_per_cut_info,
1110 )?,
1111 });
1112 }
1113
1114 let power_fits = limit_data.extract_power()?;
1115 let mut report = build_single_limit_report(
1116 ir_limit,
1117 orientation_label,
1118 power_fits.total,
1119 build_cut_limit_reports(ir_limit.num_soft(), power_fits.per_cut),
1120 );
1121 report.ray_fingerprint = Some(profile_ray_fingerprint(
1122 lmb.loop_edges.iter().copied(),
1123 &ray_points,
1124 ));
1125 report.display_only_reports =
1126 build_display_only_limit_reports(power_fits.display_components);
1127 reports.push(report);
1128 }
1129
1130 Ok(reports)
1131 }
1132 ProfileLimit::Threshold(threshold_limit) => {
1133 let overlap_structure = points_on_threshold.get(graph_id).ok_or_else(|| {
1134 eyre!(
1135 "Missing threshold-approach kinematics for amplitude graph {}",
1136 graph_id
1137 )
1138 })?;
1139
1140 let existing_esurface_id = threshold_limit.resolve_existing_esurface_id(
1141 &self.data.graph_terms[graph_id]
1142 .threshold_counterterm
1143 .esurface_map,
1144 self.data.graph_terms[graph_id]
1145 .threshold_counterterm
1146 .own_group_position,
1147 &overlap_structure.existing_esurfaces,
1148 overlap_structure.variant_ids.as_ref(),
1149 &self.data.graph_terms[graph_id]
1150 .threshold_counterterm
1151 .variant_raised_esurfaces,
1152 )?;
1153
1154 let momenta_per_overlap_group = threshold_limit.get_momenta_per_overlap_group(
1155 overlap_structure,
1156 existing_esurface_id,
1157 approach_settings,
1158 )?;
1159 let orientations = requested_orientations(self, graph_id, approach_settings)?;
1160 let mut reports =
1161 Vec::with_capacity(orientations.len() * momenta_per_overlap_group.len());
1162
1163 for (overlap_group_label, momenta) in momenta_per_overlap_group {
1164 for (orientation, orientation_label) in &orientations {
1165 let mut limit_data = LimitData { data: Vec::new() };
1166
1167 for lambda_point in &momenta {
1168 limit_data.data.push(LambdaPointEval {
1169 lambda: lambda_point.lambda.clone(),
1170 value: evaluate_profile_momentum_point_arb(
1171 self,
1172 model,
1173 graph_id,
1174 *orientation,
1175 lambda_point
1176 .loop_momenta
1177 .iter()
1178 .map(|momentum| {
1179 ThreeMomentum::new(
1180 F::from_f64(momentum.px.into_f64()),
1181 F::from_f64(momentum.py.into_f64()),
1182 F::from_f64(momentum.pz.into_f64()),
1183 )
1184 })
1185 .collect_vec(),
1186 approach_settings.show_per_cut_info,
1187 )?,
1188 });
1189 }
1190
1191 let power_fits = limit_data.extract_power()?;
1192 let context_label = Some(match orientation_label.as_deref() {
1193 Some(orientation_label) => {
1194 format!("{overlap_group_label} / {orientation_label}")
1195 }
1196 None => overlap_group_label.clone(),
1197 });
1198
1199 let mut report = build_threshold_limit_report(
1200 threshold_limit,
1201 context_label,
1202 power_fits.total,
1203 build_cut_limit_reports(0, power_fits.per_cut),
1204 );
1205 report.display_only_reports =
1206 build_display_only_limit_reports(power_fits.display_components);
1207 reports.push(report);
1208 }
1209 }
1210
1211 Ok(reports)
1212 }
1213 }
1214 }
1215}
1216
1217impl CrossSectionIntegrand {
1218 pub fn ir_profile_completion_entries(&self) -> Vec<(String, Vec<String>)> {
1219 ir_profile_completion_entries(self.enumerate_ir_limits())
1220 }
1221
1222 pub fn test_ir(
1223 &mut self,
1224 ir_profile_settings: &IRProfileSetting,
1225 model: &Model,
1226 ) -> Result<IrLimitTestReport> {
1227 self.settings.sampling = SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
1229 graph_names: Vec::new(),
1230 sample_orientations: false,
1231 sampling_type: DiscreteGraphSamplingType::Default(ParameterizationSettings {
1232 mode: ParameterizationMode::MomentumSpace,
1233 mapping: ParameterizationMapping::default(),
1234 b: 10.0,
1235 power: 1.0,
1236 lmb_basis_ids: Default::default(),
1237 sampling_channels: Default::default(),
1238 }),
1239 });
1240
1241 let previous_generate_events = self.settings.general.generate_events;
1242 if ir_profile_settings.show_per_cut_info {
1243 self.settings.general.generate_events = true;
1244 }
1245
1246 let points_on_threshold = vec![]; let result = (|| {
1249 self.warm_up(model)?;
1250 run_ir_profile(
1251 self,
1252 ir_profile_settings,
1253 model,
1254 Self::enumerate_ir_limits,
1255 Self::graph_cut_definitions,
1256 &points_on_threshold,
1257 Self::test_single_ir_limit_impl,
1258 )
1259 })();
1260
1261 if self.settings.general.generate_events != previous_generate_events {
1262 self.settings.general.generate_events = previous_generate_events;
1263 }
1264
1265 result
1266 }
1267
1268 fn enumerate_ir_limits(&self) -> Vec<(String, Vec<ProfileLimit>)> {
1269 self.data
1270 .graph_terms
1271 .iter()
1272 .map(|term| {
1273 let graph_name = term.graph.name.clone();
1274 let limits = term
1275 .enumerate_ir_limits()
1276 .into_iter()
1277 .map(ProfileLimit::Ir)
1278 .collect();
1279 (graph_name, limits)
1280 })
1281 .collect()
1282 }
1283
1284 fn graph_cut_definitions(&self, graph_id: usize) -> Vec<GraphCutDefinition> {
1285 graph_cut_definitions_for_cross_section_term(&self.data.graph_terms[graph_id])
1286 }
1287
1288 fn test_single_ir_limit_impl(
1289 &mut self,
1290 graph_id: usize,
1291 profile_limit: &ProfileLimit,
1292 rng: &mut MonteCarloRng,
1293 approach_settings: &IRProfileSetting,
1294 model: &Model,
1295 _points_on_threshold: &[OverlapStructureWithKinematics<ArbPrec>],
1296 ) -> Result<Vec<SingleLimitReport>> {
1297 let ir_limit = match profile_limit {
1298 ProfileLimit::Ir(ir_limit) => ir_limit,
1299 ProfileLimit::Threshold(threshold_limit) => {
1300 return Err(eyre!(
1301 "Threshold limit '{}' is not yet supported in cross-section IR profiling",
1302 threshold_limit
1303 ));
1304 }
1305 };
1306
1307 let edges_in_limit = ir_limit.get_all_edges()?;
1308
1309 let (cut_id, _esurface) = self.data.graph_terms[graph_id]
1311 .cut_esurface
1312 .iter_enumerated()
1313 .find(|(_cut_id, esurface)| {
1314 edges_in_limit
1315 .iter()
1316 .all(|edge| esurface.energies.contains(edge))
1317 })
1318 .ok_or(eyre!(
1319 "could not find cut with all edges of the limit: {}",
1320 ir_limit
1321 ))?;
1322
1323 let cs_cut = &self.data.graph_terms[graph_id].cuts[cut_id];
1324
1325 let edges_to_flip = cs_cut
1326 .cut
1327 .iter_edges(&self.data.graph_terms[graph_id].graph.underlying)
1328 .map(|(or, _)| or)
1329 .zip(
1330 self.data.graph_terms[graph_id]
1331 .graph
1332 .underlying
1333 .iter_edges_of(&cs_cut.cut)
1334 .map(|x| x.1),
1335 )
1336 .filter_map(|(orientation, edge_id)| {
1337 if edges_in_limit.contains(&edge_id) && matches!(orientation, Orientation::Reversed)
1338 {
1339 Some(edge_id)
1340 } else {
1341 None
1342 }
1343 })
1344 .collect_vec();
1345
1346 let lmb = self.data.graph_terms[graph_id].lmb_with_loop_edges(edges_in_limit.as_slice())?;
1347 let momenta = ir_limit.get_momenta(rng, &self.settings, approach_settings)?;
1348 let non_limit_loops = lmb
1349 .loop_edges
1350 .iter_enumerated()
1351 .filter_map(|(loop_id, edge_id)| {
1352 if !edges_in_limit.contains(edge_id) {
1353 Some(loop_id)
1354 } else {
1355 None
1356 }
1357 })
1358 .collect_vec();
1359
1360 let non_limit_momenta = non_limit_loops
1361 .iter()
1362 .map(|loop_id| (*loop_id, sample_random_unit_vector(rng)))
1363 .collect_vec();
1364
1365 let dependent_momenta_constructor = DependentMomentaConstructor::CrossSection;
1366
1367 let loop_number = lmb.loop_edges.len();
1368 let orientations = requested_orientations(self, graph_id, approach_settings)?;
1369 let mut reports = Vec::with_capacity(orientations.len());
1370
1371 for (orientation, orientation_label) in orientations {
1372 let mut limit_data = LimitData { data: Vec::new() };
1373 let mut ray_points = Vec::with_capacity(momenta.len());
1374
1375 for (loop_mom_id, lambda_point) in momenta.iter().cloned().enumerate() {
1376 let mut loop_moms: LoopMomenta<F<_>> = (0..loop_number)
1377 .map(|_| {
1378 ThreeMomentum::new(F::from_f64(0.0), F::from_f64(0.0), F::from_f64(0.0))
1379 })
1380 .collect();
1381
1382 for (loop_id, momentum) in non_limit_momenta.iter() {
1383 loop_moms[*loop_id] = *momentum;
1384 }
1385
1386 for tagged_momenta in &lambda_point.momenta {
1387 let edge_id = tagged_momenta.tag;
1388 let loop_id = lmb
1389 .loop_edges
1390 .iter()
1391 .position(|loop_edge| loop_edge == &edge_id)
1392 .unwrap_or_else(|| {
1393 unreachable!("corrupted lmb and ir limit: {}", ir_limit);
1394 });
1395
1396 loop_moms[LoopIndex(loop_id)] = if edges_to_flip.contains(&edge_id) {
1397 -tagged_momenta.momentum
1398 } else {
1399 tagged_momenta.momentum
1400 };
1401 }
1402
1403 let sample_in_cmb = MomentumSample::new(
1404 loop_moms,
1405 loop_mom_id,
1406 &self.settings.kinematics.externals,
1407 0,
1408 F::from_f64(1.0),
1409 dependent_momenta_constructor,
1410 orientation,
1411 )?;
1412
1413 let sample = sample_in_cmb.lmb_transform(
1414 &lmb,
1415 &self.data.graph_terms[graph_id].graph.loop_momentum_basis,
1416 );
1417 let routed_loop_momenta = sample.loop_moms().iter().cloned().collect_vec();
1418 ray_points.push((lambda_point.lambda, routed_loop_momenta.clone()));
1419
1420 limit_data.data.push(LambdaPointEval {
1421 lambda: lambda_point.lambda,
1422 value: evaluate_profile_momentum_point_arb(
1423 self,
1424 model,
1425 graph_id,
1426 orientation,
1427 routed_loop_momenta,
1428 approach_settings.show_per_cut_info,
1429 )?,
1430 });
1431 }
1432
1433 let power_fits = limit_data.extract_power()?;
1434 let mut report = build_single_limit_report(
1435 ir_limit,
1436 orientation_label,
1437 power_fits.total,
1438 build_cut_limit_reports(ir_limit.num_soft(), power_fits.per_cut),
1439 );
1440 report.ray_fingerprint = Some(profile_ray_fingerprint(
1441 lmb.loop_edges.iter().copied(),
1442 &ray_points,
1443 ));
1444 reports.push(report);
1445 }
1446
1447 Ok(reports)
1448 }
1449}
1450
1451#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1452struct IrLimit {
1453 colinear: Vec<Vec<HardOrSoft>>,
1454 soft: Vec<EdgeIndex>,
1455}
1456
1457#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1458struct ThresholdLimit {
1459 esurface_id: RaisedEsurfaceId,
1460 variant_id: Option<ThresholdCountertermVariantId>,
1461}
1462
1463#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1464enum ProfileLimit {
1465 Ir(IrLimit),
1466 Threshold(ThresholdLimit),
1467}
1468
1469enum MomentumBuilder<T: FloatLike> {
1470 Colinear {
1471 edge_id: EdgeIndex,
1472 x: F<T>,
1473 colinear_direction: ThreeMomentum<F<T>>,
1474 perpendicular_direction: ThreeMomentum<F<T>>,
1475 is_soft: bool,
1476 },
1477 Soft {
1478 edge_id: EdgeIndex,
1479 direction: ThreeMomentum<F<T>>,
1480 },
1481}
1482
1483impl Display for IrLimit {
1484 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1485 for colinear_set in &self.colinear {
1486 write!(f, "C[")?;
1487
1488 let mut iter = colinear_set.iter();
1489 if let Some(last) = iter.next_back() {
1490 for item in iter {
1491 write!(f, "{},", item)?;
1492 }
1493 write!(f, "{}]", last)?;
1494 } else {
1495 write!(f, "]")?;
1496 }
1497 }
1498
1499 for soft in self.soft.iter() {
1500 write!(f, "S({})", soft)?;
1501 }
1502
1503 Ok(())
1504 }
1505}
1506
1507impl Display for ThresholdLimit {
1508 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1509 if let Some(variant_id) = self.variant_id {
1510 write!(f, "T(t{}:v{})", self.esurface_id.0, variant_id.0)
1511 } else {
1512 write!(f, "T(t{})", self.esurface_id.0)
1513 }
1514 }
1515}
1516
1517impl Display for ProfileLimit {
1518 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1519 match self {
1520 ProfileLimit::Ir(ir_limit) => write!(f, "{}", ir_limit),
1521 ProfileLimit::Threshold(threshold_limit) => write!(f, "{}", threshold_limit),
1522 }
1523 }
1524}
1525
1526impl ThresholdLimit {
1527 fn enumerate_from_overlap_structure(
1528 existing_esurfaces: &ExistingEsurfaces,
1529 esurface_map: &TiVec<GroupEsurfaceId, TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>>,
1530 own_group_position: GraphGroupPosition,
1531 ) -> Vec<Self> {
1532 existing_esurfaces
1533 .iter()
1534 .filter_map(|group_esurface_id| {
1535 esurface_map[*group_esurface_id][own_group_position].map(|esurface_id| Self {
1536 esurface_id,
1537 variant_id: None,
1538 })
1539 })
1540 .collect::<HashSet<_>>()
1541 .into_iter()
1542 .sorted()
1543 .collect()
1544 }
1545
1546 fn parse_threshold(threshold: &str) -> Result<Self> {
1547 let mut threshold = String::from(threshold);
1548 threshold = String::from(threshold.trim());
1549
1550 if threshold.len() < 2 {
1551 return Err(eyre!("Threshold must be at least two characters long"));
1552 }
1553
1554 if threshold.remove(0) != 't' {
1555 return Err(eyre!("Threshold must start with 't'"));
1556 }
1557
1558 let (threshold_id, variant_id) = match threshold.split_once(":v") {
1559 Some((threshold_id, variant_id)) => {
1560 if threshold_id.is_empty() || variant_id.is_empty() || variant_id.contains(':') {
1561 return Err(eyre!(
1562 "Generalized threshold must have the form tN:vM, got: t{}",
1563 threshold
1564 ));
1565 }
1566 let threshold_id: usize = threshold_id.parse().map_err(|_| {
1567 eyre!("Threshold must be a valid integer, got: {}", threshold_id)
1568 })?;
1569 let variant_id: usize = variant_id.parse().map_err(|_| {
1570 eyre!(
1571 "Threshold variant must be a valid integer, got: {}",
1572 variant_id
1573 )
1574 })?;
1575 (
1576 threshold_id,
1577 Some(ThresholdCountertermVariantId(variant_id)),
1578 )
1579 }
1580 None => {
1581 let threshold_id: usize = threshold
1582 .parse()
1583 .map_err(|_| eyre!("Threshold must be a valid integer, got: {}", threshold))?;
1584 (threshold_id, None)
1585 }
1586 };
1587
1588 Ok(Self {
1589 esurface_id: RaisedEsurfaceId::from(threshold_id),
1590 variant_id,
1591 })
1592 }
1593
1594 fn resolve_existing_esurface_id(
1595 &self,
1596 esurface_map: &TiVec<GroupEsurfaceId, TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>>,
1597 own_group_position: GraphGroupPosition,
1598 existing_esurfaces: &ExistingEsurfaces,
1599 variant_ids: Option<&TiVec<ExistingEsurfaceId, ThresholdCountertermVariantId>>,
1600 variant_raised_esurfaces: &TiVec<ThresholdCountertermVariantId, RaisedEsurfaceId>,
1601 ) -> Result<ExistingEsurfaceId> {
1602 let Some(variant_ids) = variant_ids else {
1603 if self.variant_id.is_some() {
1604 return Err(eyre!(
1605 "Threshold '{}' selects a generalized variant, but the selected graph uses the legacy threshold-counterterm path",
1606 self,
1607 ));
1608 }
1609 return existing_esurfaces
1610 .iter_enumerated()
1611 .find_map(|(existing_esurface_id, group_esurface_id)| {
1612 esurface_map.get(*group_esurface_id).and_then(|graph_map| {
1613 (graph_map[own_group_position] == Some(self.esurface_id))
1614 .then_some(existing_esurface_id)
1615 })
1616 })
1617 .ok_or_else(|| {
1618 eyre!(
1619 "Threshold '{}' does not exist in the selected overlap structure",
1620 self
1621 )
1622 });
1623 };
1624
1625 if variant_ids.len() != existing_esurfaces.len() {
1626 return Err(eyre!(
1627 "Generalized threshold approach has {} variant IDs for {} existing E-surfaces",
1628 variant_ids.len(),
1629 existing_esurfaces.len(),
1630 ));
1631 }
1632
1633 if let Some(requested_variant_id) = self.variant_id {
1634 let raised_esurface_id = variant_raised_esurfaces
1635 .get(requested_variant_id)
1636 .ok_or_else(|| {
1637 eyre!(
1638 "Threshold '{}' selects variant v{}, but that variant does not exist",
1639 self,
1640 requested_variant_id.0,
1641 )
1642 })?;
1643 if *raised_esurface_id != self.esurface_id {
1644 return Err(eyre!(
1645 "Threshold '{}' selects variant v{}, which belongs to raised E-surface t{}",
1646 self,
1647 requested_variant_id.0,
1648 raised_esurface_id.0,
1649 ));
1650 }
1651 return variant_ids
1652 .iter_enumerated()
1653 .find_map(|(existing_esurface_id, &variant_id)| {
1654 (variant_id == requested_variant_id).then_some(existing_esurface_id)
1655 })
1656 .ok_or_else(|| {
1657 eyre!(
1658 "Threshold '{}' does not exist in the selected generalized overlap structure",
1659 self,
1660 )
1661 });
1662 }
1663
1664 let matching_variants = variant_ids
1665 .iter_enumerated()
1666 .filter_map(|(existing_esurface_id, &variant_id)| {
1667 variant_raised_esurfaces
1668 .get(variant_id)
1669 .filter(|&&raised_esurface_id| raised_esurface_id == self.esurface_id)
1670 .map(|_| (existing_esurface_id, variant_id))
1671 })
1672 .collect_vec();
1673 match matching_variants.as_slice() {
1674 [(existing_esurface_id, _)] => Ok(*existing_esurface_id),
1675 [] => Err(eyre!(
1676 "Threshold '{}' does not exist in the selected generalized overlap structure",
1677 self,
1678 )),
1679 _ => Err(eyre!(
1680 "Threshold '{}' is ambiguous for generalized variants {}; select T(t{}:vN)",
1681 self,
1682 matching_variants
1683 .iter()
1684 .map(|(_, variant_id)| format!("v{}", variant_id.0))
1685 .join(", "),
1686 self.esurface_id.0,
1687 )),
1688 }
1689 }
1690
1691 fn get_momenta_per_overlap_group<T: FloatLike>(
1692 &self,
1693 overlap_structure: &OverlapStructureWithKinematics<T>,
1694 existing_esurface_id: ExistingEsurfaceId,
1695 approach_settings: &IRProfileSetting,
1696 ) -> Result<Vec<(String, Vec<LambdaLoopMomentaPoint<T>>)>> {
1697 let lambda_values = constant_dropped_fit_points(
1698 &F::from_f64(10.0_f64.powf(approach_settings.lambda_exp_start)),
1699 &F::from_f64(10.0_f64.powf(approach_settings.lambda_exp_end)),
1700 approach_settings.steps,
1701 )?;
1702
1703 let mut momenta_per_overlap_group = Vec::new();
1704
1705 for (overlap_group_id, overlap_group_with_kinematics) in overlap_structure
1706 .overlap_groups_with_kinematics
1707 .iter()
1708 .enumerate()
1709 {
1710 let mut contains_existing_esurface = false;
1711 let mut threshold_point = None;
1712 let mut approach_center = None;
1713
1714 for (group_position, (group_existing_esurface_id, maybe_threshold_point)) in
1715 overlap_group_with_kinematics
1716 .overlap_group
1717 .existing_esurfaces
1718 .iter()
1719 .copied()
1720 .zip(
1721 overlap_group_with_kinematics
1722 .loop_momenta_at_esurface
1723 .iter(),
1724 )
1725 .enumerate()
1726 {
1727 if group_existing_esurface_id != existing_esurface_id {
1728 continue;
1729 }
1730
1731 contains_existing_esurface = true;
1732 threshold_point = maybe_threshold_point.as_ref();
1733 approach_center = overlap_group_with_kinematics
1734 .approach_centers_at_esurface
1735 .as_ref()
1736 .and_then(|centers| centers[ExistingEsurfaceId::from(group_position)].as_ref());
1737 break;
1738 }
1739
1740 if !contains_existing_esurface {
1741 continue;
1742 }
1743
1744 let threshold_point = threshold_point.ok_or_else(|| {
1745 eyre!(
1746 "Threshold '{}' is missing stored approach kinematics for overlap group {}",
1747 self,
1748 overlap_group_id
1749 )
1750 })?;
1751
1752 momenta_per_overlap_group.push((
1753 format!("overlap group {}", overlap_group_id),
1754 lambda_values
1755 .iter()
1756 .cloned()
1757 .map(|lambda| LambdaLoopMomentaPoint {
1758 loop_momenta: if let Some(approach_center) = approach_center {
1759 let threshold_loop_momenta = threshold_point.loop_moms();
1760 let offset_towards_center =
1761 (approach_center - threshold_loop_momenta).rescale(&lambda, None);
1762 threshold_loop_momenta + &offset_towards_center
1763 } else {
1764 threshold_approach_loop_momenta(
1765 &overlap_group_with_kinematics.overlap_group.center,
1766 threshold_point,
1767 &lambda,
1768 )
1769 },
1770 lambda,
1771 })
1772 .collect(),
1773 ));
1774 }
1775
1776 if momenta_per_overlap_group.is_empty() {
1777 return Err(eyre!(
1778 "Threshold '{}' does not appear in any overlap group for the selected graph term",
1779 self
1780 ));
1781 }
1782
1783 Ok(momenta_per_overlap_group)
1784 }
1785}
1786
1787impl ProfileLimit {
1788 fn parse_limit(limit: &str) -> Result<Self> {
1789 let mut colinear_sets = Vec::new();
1790 let mut soft_edges = Vec::new();
1791 let mut threshold_limit = None;
1792
1793 let mut char_iter = limit.chars().enumerate();
1794
1795 while let Some((char_position, char)) = char_iter.next() {
1796 match char {
1797 'C' => {
1798 let (_opening_bracket_position, opening_bracket) =
1799 char_iter.next().ok_or_else(|| {
1800 eyre!(
1801 "Expected opening bracket after 'C' at position {}",
1802 char_position
1803 )
1804 })?;
1805
1806 if opening_bracket != '[' {
1807 return Err(eyre!(
1808 "Expected '[' after 'C' at position {}, found '{}'",
1809 char_position,
1810 opening_bracket
1811 ));
1812 }
1813
1814 let mut colinear_set_str = String::new();
1815
1816 let mut closing_bracket_found = false;
1817 for (_next_char_position, next_char) in char_iter.by_ref() {
1818 if next_char == ']' {
1819 closing_bracket_found = true;
1820 break;
1821 }
1822 colinear_set_str.push(next_char);
1823 }
1824
1825 if !closing_bracket_found {
1826 return Err(eyre!(
1827 "Expected closing bracket ']' for colinear set at position {}",
1828 char_position
1829 ));
1830 }
1831
1832 let edges = colinear_set_str.trim().split(',');
1833
1834 let mut colinear_set = Vec::new();
1835
1836 for edge in edges {
1837 let trimmed_edge = edge.trim();
1838 if trimmed_edge.is_empty() {
1839 return Err(eyre!(
1840 "Empty edge found in colinear set at position {}",
1841 char_position
1842 ));
1843 }
1844
1845 if trimmed_edge.starts_with('S') {
1846 let mut trimmed_edge_iter = trimmed_edge.chars().skip(1);
1847 let opening_bracket = trimmed_edge_iter
1848 .next()
1849 .ok_or(eyre!("Expected '(' after 'S' in soft edge at position"))?;
1850
1851 if opening_bracket != '(' {
1852 return Err(eyre!(
1853 "Expected '(' after 'S' in soft edge at position , found ''",
1854 ));
1855 }
1856
1857 let mut edge_str = String::new();
1858 let mut closing_bracket_found = false;
1859
1860 for next_char in trimmed_edge_iter {
1861 if next_char == ')' {
1862 closing_bracket_found = true;
1863 break;
1864 }
1865 edge_str.push(next_char);
1866 }
1867
1868 if !closing_bracket_found {
1869 return Err(eyre!(
1870 "Expected closing bracket ')' for soft edge at position {}",
1871 char_position
1872 ));
1873 }
1874
1875 let edge_index = IrLimit::parse_edge(&edge_str)?;
1876 colinear_set.push(HardOrSoft::Soft(edge_index));
1877 } else {
1878 let edge_index = IrLimit::parse_edge(trimmed_edge)?;
1879 colinear_set.push(HardOrSoft::Hard(edge_index));
1880 }
1881 }
1882 colinear_sets.push(colinear_set);
1883 }
1884 'S' => {
1885 let (_opening_bracket_position, opening_bracket) =
1886 char_iter.next().ok_or_else(|| {
1887 eyre!(
1888 "Expected opening bracket '(' after 'S' at position {}",
1889 char_position
1890 )
1891 })?;
1892
1893 if opening_bracket != '(' {
1894 return Err(eyre!(
1895 "Expected '(' after 'S' at position {}, found '{}'",
1896 char_position,
1897 opening_bracket
1898 ));
1899 }
1900
1901 let mut edge_str = String::new();
1902 let mut closing_bracket_found = false;
1903
1904 for (_next_char_position, next_char) in char_iter.by_ref() {
1905 if next_char == ')' {
1906 closing_bracket_found = true;
1907 break;
1908 }
1909 edge_str.push(next_char);
1910 }
1911
1912 if !closing_bracket_found {
1913 return Err(eyre!(
1914 "Expected closing bracket ')' for soft edge at position {}",
1915 char_position
1916 ));
1917 }
1918
1919 let edge_index = IrLimit::parse_edge(&edge_str)?;
1920 soft_edges.push(edge_index);
1921 }
1922 'T' => {
1923 if threshold_limit.is_some() {
1924 return Err(eyre!("Only one threshold limit can be specified"));
1925 }
1926
1927 let (_opening_bracket_position, opening_bracket) =
1928 char_iter.next().ok_or_else(|| {
1929 eyre!(
1930 "Expected opening bracket '(' after 'T' at position {}",
1931 char_position
1932 )
1933 })?;
1934
1935 if opening_bracket != '(' {
1936 return Err(eyre!(
1937 "Expected '(' after 'T' at position {}, found '{}'",
1938 char_position,
1939 opening_bracket
1940 ));
1941 }
1942
1943 let mut threshold_str = String::new();
1944 let mut closing_bracket_found = false;
1945
1946 for (_next_char_position, next_char) in char_iter.by_ref() {
1947 if next_char == ')' {
1948 closing_bracket_found = true;
1949 break;
1950 }
1951 threshold_str.push(next_char);
1952 }
1953
1954 if !closing_bracket_found {
1955 return Err(eyre!(
1956 "Expected closing bracket ')' for threshold at position {}",
1957 char_position
1958 ));
1959 }
1960
1961 threshold_limit = Some(ThresholdLimit::parse_threshold(&threshold_str)?);
1962 }
1963 _ => {
1964 return Err(eyre!(
1965 "Unexpected character '{}' at position {}",
1966 char,
1967 char_position
1968 ));
1969 }
1970 }
1971 }
1972
1973 if let Some(threshold_limit) = threshold_limit {
1974 if !colinear_sets.is_empty() || !soft_edges.is_empty() {
1975 return Err(eyre!(
1976 "Threshold limits cannot be combined with soft or colinear limits"
1977 ));
1978 }
1979
1980 return Ok(ProfileLimit::Threshold(threshold_limit));
1981 }
1982
1983 let mut ir_limit = IrLimit {
1984 colinear: colinear_sets,
1985 soft: soft_edges,
1986 };
1987
1988 ir_limit.canonize();
1989
1990 Ok(ProfileLimit::Ir(ir_limit))
1991 }
1992
1993 fn is_valid(&self, loop_number: usize) -> Result<()> {
1994 match self {
1995 ProfileLimit::Ir(ir_limit) => ir_limit.is_valid(loop_number),
1996 ProfileLimit::Threshold(_) => Ok(()),
1997 }
1998 }
1999}
2000
2001#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2002enum HardOrSoft {
2003 Hard(EdgeIndex),
2004 Soft(EdgeIndex),
2005}
2006
2007impl HardOrSoft {
2008 fn index(&self) -> EdgeIndex {
2009 match self {
2010 HardOrSoft::Hard(index) => *index,
2011 HardOrSoft::Soft(index) => *index,
2012 }
2013 }
2014}
2015
2016impl Display for HardOrSoft {
2017 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2018 match self {
2019 HardOrSoft::Hard(index) => write!(f, "{}", index),
2020 HardOrSoft::Soft(index) => write!(f, "S({})", index),
2021 }
2022 }
2023}
2024
2025impl IrLimit {
2026 fn canonize(&mut self) {
2027 for colinear_set in &mut self.colinear.iter_mut() {
2028 colinear_set.sort();
2029 }
2030
2031 self.colinear.sort();
2032 self.soft.sort();
2033 }
2034
2035 fn new_pure_colinear(colinear_edges: Vec<EdgeIndex>) -> Self {
2036 let colinear = vec![colinear_edges.into_iter().map(HardOrSoft::Hard).collect()];
2037
2038 let mut result = IrLimit {
2039 colinear,
2040 soft: Vec::new(),
2041 };
2042 result.canonize();
2043 result
2044 }
2045
2046 fn new_pure_soft(soft_edges: Vec<EdgeIndex>) -> Self {
2047 let mut result = IrLimit {
2048 colinear: Vec::new(),
2049 soft: soft_edges,
2050 };
2051 result.canonize();
2052 result
2053 }
2054
2055 fn num_soft(&self) -> usize {
2056 self.colinear
2057 .iter()
2058 .flatten()
2059 .filter(|edge| matches!(edge, HardOrSoft::Soft(_)))
2060 .count()
2061 + self.soft.len()
2062 }
2063
2064 fn check_min_colinear_size(&self) -> bool {
2065 self.colinear
2066 .iter()
2067 .all(|colinear_set| colinear_set.len() >= 2)
2068 }
2069
2070 fn is_valid(&self, loop_number: usize) -> Result<()> {
2071 if !self.check_min_colinear_size() {
2072 return Err(eyre!("colinear sets must have at least two edges"));
2073 }
2074
2075 let all_edges = self.get_all_edges()?;
2076
2077 if all_edges.len() > loop_number {
2078 return Err(eyre!("not enough degrees of freedom to setup IR limit"));
2079 }
2080
2081 Ok(())
2082 }
2083
2084 fn get_all_edges(&self) -> Result<Vec<EdgeIndex>> {
2085 let colinear_edges = self
2086 .colinear
2087 .iter()
2088 .flatten()
2089 .map(HardOrSoft::index)
2090 .collect_vec();
2091 let soft_edges = self.soft.iter().copied().collect_vec();
2092
2093 let all_edges: Vec<EdgeIndex> = colinear_edges
2094 .into_iter()
2095 .chain(soft_edges)
2096 .sorted()
2097 .collect();
2098
2099 let mut unique_edges = all_edges.clone();
2101 unique_edges.dedup();
2102
2103 if unique_edges.len() != all_edges.len() {
2104 return Err(eyre!("Edges specified in ir limit must be unique")); }
2106
2107 Ok(all_edges)
2108 }
2109
2110 fn parse_edge(edge: &str) -> Result<EdgeIndex> {
2111 let mut edge = String::from(edge);
2112 edge = String::from(edge.trim());
2113
2114 if edge.len() < 2 {
2115 return Err(eyre!("Edge must be at least two characters long"));
2116 }
2117
2118 if edge.remove(0) != 'e' {
2119 return Err(eyre!("Edge must start with 'e'"));
2120 }
2121
2122 let edge_id: usize = edge
2123 .parse()
2124 .map_err(|_| eyre!("Edge must be a valid integer, got: {}", edge))?;
2125
2126 Ok(EdgeIndex::from(edge_id))
2127 }
2128
2129 fn get_momentum_builders(&self, rng: &mut MonteCarloRng) -> Vec<MomentumBuilder<f64>> {
2130 let mut momentum_builder = Vec::new();
2131
2132 for colinear_set in &self.colinear {
2133 let direction_for_set: ThreeMomentum<F<f64>> = sample_random_unit_vector(rng);
2134
2135 let x_variables: Vec<F<f64>> = (0..colinear_set.len())
2136 .map(|_| F::from_f64(rng.random::<f64>() * 0.8 + 0.1))
2137 .sorted_by(|a, b| a.partial_cmp(b).unwrap())
2138 .collect_vec();
2139
2140 for (edge, x) in colinear_set.iter().zip(x_variables) {
2141 let edge_id = edge.index();
2142 let direction: ThreeMomentum<F<f64>> = sample_random_unit_vector(rng);
2143
2144 let perpendicular = direction - direction * (direction * direction_for_set);
2145
2146 let perpendicular_norm = perpendicular.norm();
2147 let perpendicular = perpendicular * perpendicular_norm.inv();
2148
2149 let is_soft = matches!(edge, HardOrSoft::Soft(_));
2150
2151 momentum_builder.push(MomentumBuilder::Colinear {
2152 edge_id,
2153 x,
2154 colinear_direction: direction_for_set,
2155 perpendicular_direction: perpendicular,
2156 is_soft,
2157 });
2158 }
2159 }
2160
2161 for soft_edge in &self.soft {
2162 let direction = sample_random_unit_vector(rng);
2163 momentum_builder.push(MomentumBuilder::Soft {
2164 edge_id: *soft_edge,
2165 direction,
2166 });
2167 }
2168
2169 momentum_builder
2170 }
2171
2172 fn get_momenta(
2173 &self,
2174 rng: &mut MonteCarloRng,
2175 settings: &RuntimeSettings,
2176 approach_settings: &IRProfileSetting,
2177 ) -> Result<Vec<LambdaPoint<f64>>> {
2178 let momentum_builders = self.get_momentum_builders(rng);
2179
2180 let lambda_values = constant_dropped_fit_points(
2181 &F::from_f64(10.0_f64.powf(approach_settings.lambda_exp_start)),
2182 &F::from_f64(10.0_f64.powf(approach_settings.lambda_exp_end)),
2183 approach_settings.steps,
2184 )?;
2185
2186 Ok(lambda_values
2187 .into_iter()
2188 .map(|lambda| LambdaPoint {
2189 momenta: momentum_builders
2190 .iter()
2191 .map(|builder| match builder {
2192 MomentumBuilder::Colinear {
2193 edge_id,
2194 x,
2195 colinear_direction,
2196 perpendicular_direction,
2197 is_soft,
2198 } => {
2199 let momentum = if *is_soft {
2200 (colinear_direction * x + perpendicular_direction * lambda)
2201 * F::from_f64(settings.kinematics.e_cm)
2202 * lambda
2203 } else {
2204 (colinear_direction * x + perpendicular_direction * lambda)
2205 * F::from_f64(settings.kinematics.e_cm)
2206 };
2207 TaggedMomenta {
2208 momentum,
2209 tag: *edge_id,
2210 }
2211 }
2212 MomentumBuilder::Soft { edge_id, direction } => {
2213 let momentum =
2214 direction * lambda * F::from_f64(settings.kinematics.e_cm);
2215 TaggedMomenta {
2216 momentum,
2217 tag: *edge_id,
2218 }
2219 }
2220 })
2221 .collect(),
2222 lambda,
2223 })
2224 .collect())
2225 }
2226}
2227
2228fn evaluate_profile_momentum_point_arb<I: ProcessIntegrandImpl>(
2229 integrand: &mut I,
2230 model: &Model,
2231 graph_id: usize,
2232 orientation: Option<usize>,
2233 loop_momenta: Vec<ThreeMomentum<F<f64>>>,
2234 show_per_cut_info: bool,
2235) -> Result<ProfilePointValue> {
2236 match evaluate_profile_momentum_point_precise(
2237 integrand,
2238 model,
2239 graph_id,
2240 orientation,
2241 loop_momenta,
2242 None,
2243 true,
2244 &F::<ArbPrec>::default().one(),
2245 )? {
2246 PreciseEvaluationResult::Arb(result) => {
2247 let zero_complex = Complex::new_re(result.integrand_result.re.zero());
2248 let per_cut = if show_per_cut_info {
2249 let mut per_cut = BTreeMap::new();
2250 for event_group in result.event_groups.iter() {
2251 for event in event_group.iter() {
2252 let entry = per_cut
2253 .entry(event.cut_info.cut_id)
2254 .or_insert_with(|| zero_complex.clone());
2255 *entry += event.weight.clone();
2256 }
2257 }
2258 per_cut
2259 .into_iter()
2260 .map(|(cut_id, weight)| (cut_id, weight.norm_squared().sqrt()))
2261 .collect()
2262 } else {
2263 BTreeMap::new()
2264 };
2265
2266 let mut display_only_components = BTreeMap::new();
2267 for event_group in result.event_groups.iter() {
2268 for event in event_group.iter() {
2269 for (key, weight) in &event.additional_weights.weights {
2270 if !matches!(
2271 key,
2272 AdditionalWeightKey::Original
2273 | AdditionalWeightKey::ThresholdCounterterm { .. }
2274 | AdditionalWeightKey::AmplitudeThresholdCounterterm { .. }
2275 | AdditionalWeightKey::AmplitudeThresholdCountertermVariant { .. }
2276 ) {
2277 continue;
2278 }
2279
2280 let entry = display_only_components
2281 .entry(*key)
2282 .or_insert_with(|| zero_complex.clone());
2283 *entry += weight.clone();
2284 }
2285 }
2286 }
2287 let display_only_components = display_only_components
2288 .into_iter()
2289 .map(|(key, weight)| (key, weight.norm_squared().sqrt()))
2290 .collect();
2291
2292 Ok(ProfilePointValue {
2293 total: result.integrand_result.norm_squared().sqrt(),
2294 per_cut,
2295 display_only_components,
2296 })
2297 }
2298 PreciseEvaluationResult::Double(_) => Err(eyre!(
2299 "IR profiling requested arbitrary precision but received a double-precision result"
2300 )),
2301 PreciseEvaluationResult::Quad(_) => Err(eyre!(
2302 "IR profiling requested arbitrary precision but received a quad-precision result"
2303 )),
2304 }
2305}
2306
2307fn profile_ray_fingerprint(
2308 lmb_edges: impl IntoIterator<Item = EdgeIndex>,
2309 ray_points: &[ProfileRayPoint],
2310) -> ProfileRayFingerprint {
2311 let lmb_edges = lmb_edges.into_iter().map(usize::from).collect_vec();
2312 let mut payload = b"gammaloop-ir-profile-ray-v1".to_vec();
2313 payload.extend_from_slice(&(lmb_edges.len() as u64).to_le_bytes());
2314 for edge in &lmb_edges {
2315 payload.extend_from_slice(&(*edge as u64).to_le_bytes());
2316 }
2317 payload.extend_from_slice(&(ray_points.len() as u64).to_le_bytes());
2318 for (lambda, loop_momenta) in ray_points {
2319 payload.extend_from_slice(&lambda.0.to_bits().to_le_bytes());
2320 payload.extend_from_slice(&(loop_momenta.len() as u64).to_le_bytes());
2321 for momentum in loop_momenta {
2322 for component in [momentum.px.0, momentum.py.0, momentum.pz.0] {
2323 payload.extend_from_slice(&component.to_bits().to_le_bytes());
2324 }
2325 }
2326 }
2327
2328 let mut hash = 0xcbf29ce484222325u64;
2330 for byte in payload {
2331 hash ^= u64::from(byte);
2332 hash = hash.wrapping_mul(0x100000001b3);
2333 }
2334
2335 ProfileRayFingerprint {
2336 lmb_edges,
2337 digest: format!("{hash:016x}"),
2338 }
2339}
2340
2341fn sample_random_unit_vector<T: FloatLike>(rng: &mut MonteCarloRng) -> ThreeMomentum<F<T>> {
2342 let x_1 = F::<T>::from_f64(rng.random::<f64>());
2343 let x_2 = F::<T>::from_f64(rng.random::<f64>());
2344 let x_3 = F::<T>::from_f64(rng.random::<f64>());
2345 let x_4 = F::<T>::from_f64(rng.random::<f64>());
2346
2347 let (k_x, k_y) = box_muller(x_1, x_2);
2348 let k_z = box_muller(x_3, x_4).0;
2349
2350 let unnormalized_momentum = ThreeMomentum::new(k_x, k_y, k_z);
2351 let norm = unnormalized_momentum.norm();
2352 unnormalized_momentum * norm.inv()
2353}
2354
2355#[derive(Clone)]
2356struct TaggedMomenta<T> {
2357 momentum: ThreeMomentum<T>,
2358 tag: EdgeIndex,
2359}
2360
2361#[derive(Clone)]
2362struct LambdaPoint<T: FloatLike> {
2363 lambda: F<T>,
2364 momenta: Vec<TaggedMomenta<F<T>>>,
2365}
2366
2367#[derive(Clone)]
2368struct LambdaLoopMomentaPoint<T: FloatLike> {
2369 lambda: F<T>,
2370 loop_momenta: LoopMomenta<F<T>>,
2371}
2372
2373struct LambdaPointEval<T: FloatLike> {
2374 lambda: F<T>,
2375 value: ProfilePointValue,
2376}
2377
2378struct ProfilePointValue {
2379 total: F<ArbPrec>,
2380 per_cut: BTreeMap<usize, F<ArbPrec>>,
2381 display_only_components: BTreeMap<AdditionalWeightKey, F<ArbPrec>>,
2382}
2383
2384struct LimitData<T: FloatLike> {
2385 data: Vec<LambdaPointEval<T>>,
2386}
2387
2388type PerCutPowerLawFits = Vec<(usize, Result<PowerLawFit>)>;
2389type DisplayComponentPowerLawFits = Vec<(AdditionalWeightKey, Result<PowerLawFit>)>;
2390
2391struct ExtractedPowerFits {
2392 total: PowerLawFit,
2393 per_cut: PerCutPowerLawFits,
2394 display_components: DisplayComponentPowerLawFits,
2395}
2396
2397impl<T: FloatLike> LimitData<T> {
2398 fn extract_power(&self) -> Result<ExtractedPowerFits> {
2399 let x = self
2400 .data
2401 .iter()
2402 .map(|point_eval| F::<ArbPrec>::from_ff64(point_eval.lambda.into_ff64()))
2403 .collect_vec();
2404
2405 let y = self
2406 .data
2407 .iter()
2408 .map(|point_eval| point_eval.value.total.clone())
2409 .collect_vec();
2410
2411 let result = fit_power_law(x.clone(), y.clone())?;
2412
2413 let zero = x
2414 .first()
2415 .map(|value| value.zero())
2416 .unwrap_or_else(|| F::<ArbPrec>::from_f64(0.0));
2417 let cut_fits = self
2418 .data
2419 .iter()
2420 .flat_map(|point_eval| point_eval.value.per_cut.keys().copied())
2421 .unique()
2422 .sorted()
2423 .map(|cut_id| {
2424 let y = self
2425 .data
2426 .iter()
2427 .map(|point_eval| {
2428 point_eval
2429 .value
2430 .per_cut
2431 .get(&cut_id)
2432 .cloned()
2433 .unwrap_or_else(|| zero.clone())
2434 })
2435 .collect_vec();
2436 (cut_id, fit_power_law(x.clone(), y))
2437 })
2438 .collect_vec();
2439
2440 let component_fits = self
2441 .data
2442 .iter()
2443 .flat_map(|point_eval| point_eval.value.display_only_components.keys().copied())
2444 .filter(|key| {
2445 matches!(
2446 key,
2447 AdditionalWeightKey::Original
2448 | AdditionalWeightKey::ThresholdCounterterm { .. }
2449 | AdditionalWeightKey::AmplitudeThresholdCounterterm { .. }
2450 | AdditionalWeightKey::AmplitudeThresholdCountertermVariant { .. }
2451 )
2452 })
2453 .unique()
2454 .sorted()
2455 .map(|key| {
2456 let y = self
2457 .data
2458 .iter()
2459 .map(|point_eval| {
2460 point_eval
2461 .value
2462 .display_only_components
2463 .get(&key)
2464 .cloned()
2465 .unwrap_or_else(|| zero.clone())
2466 })
2467 .collect_vec();
2468 (key, fit_power_law(x.clone(), y))
2469 })
2470 .collect_vec();
2471
2472 if result.r_squared < 0.9 {
2473 warn!("low r^2 value found for input data");
2474 warn!(
2475 "x: {:?}",
2476 x.iter().map(|value| format!("{}", value)).collect_vec()
2477 );
2478 warn!(
2479 "y: {:?}",
2480 y.iter().map(|value| format!("{}", value)).collect_vec()
2481 );
2482 }
2483
2484 Ok(ExtractedPowerFits {
2485 total: result,
2486 per_cut: cut_fits,
2487 display_components: component_fits,
2488 })
2489 }
2490}
2491
2492#[derive(Debug, Clone)]
2493pub struct PowerLawFit {
2494 exponent: f64,
2495 r_squared: f64,
2496}
2497
2498impl PowerLawFit {
2499 pub fn r_squared(&self) -> f64 {
2500 self.r_squared
2501 }
2502}
2503
2504fn fit_power_law(x: Vec<F<ArbPrec>>, y: Vec<F<ArbPrec>>) -> Result<PowerLawFit> {
2505 if x.len() != y.len() {
2506 return Err(eyre!(
2507 "fit_power_law requires x and y to have the same length"
2508 ));
2509 }
2510 if x.len() < 3 {
2511 return Err(eyre!("fit_power_law requires at least three observations"));
2512 }
2513 if x.iter()
2514 .any(|value| value.is_nan() || value.is_infinite() || value <= &value.zero())
2515 {
2516 return Err(eyre!(
2517 "fit_power_law requires strictly positive, finite x values"
2518 ));
2519 }
2520 if y.iter().any(|value| value.is_nan() || value.is_infinite()) {
2521 return Err(eyre!("fit_power_law requires finite y values"));
2522 }
2523
2524 let fit = log_log_slope_constant_dropped(&x, &y)?;
2525
2526 Ok(PowerLawFit {
2527 exponent: fit.slope.into_f64(),
2528 r_squared: fit.r_squared().into_f64(),
2529 })
2530}
2531
2532#[cfg(test)]
2533mod tests {
2534 use super::*;
2535 use typed_index_collections::ti_vec;
2536
2537 #[test]
2538 fn test_display() {
2539 let ir_limit = IrLimit {
2540 colinear: vec![
2541 vec![
2542 HardOrSoft::Hard(EdgeIndex::from(1)),
2543 HardOrSoft::Hard(EdgeIndex::from(2)),
2544 HardOrSoft::Hard(EdgeIndex::from(3)),
2545 ],
2546 vec![
2547 HardOrSoft::Hard(EdgeIndex::from(4)),
2548 HardOrSoft::Soft(EdgeIndex::from(5)),
2549 ],
2550 ],
2551 soft: vec![EdgeIndex::from(6), EdgeIndex::from(7)],
2552 };
2553
2554 let display = ir_limit.to_string();
2555 let expected = "C[e1,e2,e3]C[e4,S(e5)]S(e6)S(e7)";
2556
2557 assert_eq!(display, expected);
2558 }
2559
2560 #[test]
2561 fn routed_ir_ray_fingerprint_is_deterministic_and_serializable() {
2562 let points = vec![
2563 (
2564 F::from_f64(1.0e-2),
2565 vec![
2566 ThreeMomentum::new(F::from_f64(1.0), F::from_f64(2.0), F::from_f64(3.0)),
2567 ThreeMomentum::new(F::from_f64(-1.0), F::from_f64(0.5), F::from_f64(4.0)),
2568 ],
2569 ),
2570 (
2571 F::from_f64(1.0e-3),
2572 vec![
2573 ThreeMomentum::new(F::from_f64(0.1), F::from_f64(0.2), F::from_f64(0.3)),
2574 ThreeMomentum::new(F::from_f64(-1.0), F::from_f64(0.5), F::from_f64(4.0)),
2575 ],
2576 ),
2577 ];
2578 let lmb = [EdgeIndex::from(6), EdgeIndex::from(8)];
2579 let fingerprint = profile_ray_fingerprint(lmb, &points);
2580
2581 assert_eq!(fingerprint, profile_ray_fingerprint(lmb, &points));
2582 assert_eq!(fingerprint.lmb_edges, vec![6, 8]);
2583 assert_eq!(fingerprint.digest.len(), 16);
2584
2585 let mut changed_points = points.clone();
2586 changed_points[0].1[0].px = F::from_f64(1.5);
2587 assert_ne!(
2588 fingerprint.digest,
2589 profile_ray_fingerprint(lmb, &changed_points).digest,
2590 "the digest must identify the actual routed ray, not only its seed"
2591 );
2592 assert_ne!(
2593 fingerprint.digest,
2594 profile_ray_fingerprint([EdgeIndex::from(8), EdgeIndex::from(6)], &points).digest,
2595 "the LMB route is part of the ray identity"
2596 );
2597
2598 let serialized = serde_json::to_value(&fingerprint).unwrap();
2599 assert_eq!(serialized["lmb_edges"], serde_json::json!([6, 8]));
2600 assert_eq!(serialized["digest"], fingerprint.digest);
2601 }
2602
2603 #[test]
2604 fn test_threshold_display() {
2605 let threshold_limit = ThresholdLimit {
2606 esurface_id: RaisedEsurfaceId::from(8usize),
2607 variant_id: None,
2608 };
2609
2610 let display = threshold_limit.to_string();
2611 let expected = "T(t8)";
2612
2613 assert_eq!(display, expected);
2614
2615 let generalized_threshold_limit = ThresholdLimit {
2616 esurface_id: RaisedEsurfaceId::from(8usize),
2617 variant_id: Some(ThresholdCountertermVariantId(3)),
2618 };
2619 assert_eq!(generalized_threshold_limit.to_string(), "T(t8:v3)");
2620 }
2621
2622 #[test]
2623 fn parse_edge() {
2624 let edge_str = "e5";
2625 let edge_index = IrLimit::parse_edge(edge_str).unwrap();
2626 assert_eq!(edge_index, EdgeIndex::from(5));
2627
2628 let invalid_edge_str = "5"; assert!(IrLimit::parse_edge(invalid_edge_str).is_err());
2630
2631 let invalid_edge_str2 = "e"; assert!(IrLimit::parse_edge(invalid_edge_str2).is_err());
2633
2634 let invalid_edge_str3 = "e5a"; assert!(IrLimit::parse_edge(invalid_edge_str3).is_err());
2636 }
2637
2638 #[test]
2639 fn parse_threshold() {
2640 let threshold_str = "t5";
2641 let threshold_limit = ThresholdLimit::parse_threshold(threshold_str).unwrap();
2642 assert_eq!(threshold_limit.esurface_id, RaisedEsurfaceId::from(5usize));
2643 assert_eq!(threshold_limit.variant_id, None);
2644
2645 let generalized = ThresholdLimit::parse_threshold("t5:v2").unwrap();
2646 assert_eq!(generalized.esurface_id, RaisedEsurfaceId::from(5usize));
2647 assert_eq!(
2648 generalized.variant_id,
2649 Some(ThresholdCountertermVariantId(2))
2650 );
2651
2652 let invalid_threshold_str = "5"; assert!(ThresholdLimit::parse_threshold(invalid_threshold_str).is_err());
2654
2655 let invalid_threshold_str2 = "t"; assert!(ThresholdLimit::parse_threshold(invalid_threshold_str2).is_err());
2657
2658 let invalid_threshold_str3 = "t5a"; assert!(ThresholdLimit::parse_threshold(invalid_threshold_str3).is_err());
2660
2661 for invalid in ["t5:v", "t:v2", "t5:v2:v3", "t5:x2"] {
2662 assert!(ThresholdLimit::parse_threshold(invalid).is_err());
2663 }
2664 }
2665
2666 #[test]
2667 fn parse_limit() {
2668 let limit_str = "C[e1,e2,e3]C[e4,S(e5)]S(e6)S(e7)";
2669 let limit = ProfileLimit::parse_limit(limit_str).unwrap();
2670 let ir_limit = match limit {
2671 ProfileLimit::Ir(ir_limit) => ir_limit,
2672 ProfileLimit::Threshold(_) => panic!("Expected an IR limit"),
2673 };
2674
2675 assert_eq!(ir_limit.colinear.len(), 2, "Expected two colinear sets");
2676 assert_eq!(ir_limit.soft.len(), 2, "Expected two soft edges");
2677
2678 assert_eq!(
2679 ir_limit.colinear[0],
2680 vec![
2681 HardOrSoft::Hard(EdgeIndex::from(1)),
2682 HardOrSoft::Hard(EdgeIndex::from(2)),
2683 HardOrSoft::Hard(EdgeIndex::from(3))
2684 ],
2685 "First colinear set does not match"
2686 );
2687 assert_eq!(
2688 ir_limit.colinear[1],
2689 vec![
2690 HardOrSoft::Hard(EdgeIndex::from(4)),
2691 HardOrSoft::Soft(EdgeIndex::from(5))
2692 ],
2693 "Second colinear set does not match"
2694 );
2695
2696 assert_eq!(
2697 ir_limit.soft,
2698 vec![EdgeIndex::from(6), EdgeIndex::from(7)],
2699 "Soft edges do not match"
2700 );
2701
2702 let threshold_limit = ProfileLimit::parse_limit("T(t8)").unwrap();
2703 assert_eq!(
2704 threshold_limit,
2705 ProfileLimit::Threshold(ThresholdLimit {
2706 esurface_id: RaisedEsurfaceId::from(8usize),
2707 variant_id: None,
2708 }),
2709 "Threshold limit does not match"
2710 );
2711
2712 let generalized_threshold_limit = ProfileLimit::parse_limit("T(t8:v3)").unwrap();
2713 assert_eq!(
2714 generalized_threshold_limit,
2715 ProfileLimit::Threshold(ThresholdLimit {
2716 esurface_id: RaisedEsurfaceId::from(8usize),
2717 variant_id: Some(ThresholdCountertermVariantId(3)),
2718 }),
2719 "Generalized threshold-variant limit does not match"
2720 );
2721
2722 let invalid_limit_str = "C[e1,e2,e3]C[e4,S(e5)]S(e6)S(e7, e8)";
2723 assert!(
2724 ProfileLimit::parse_limit(invalid_limit_str).is_err(),
2725 "Expected error"
2726 );
2727
2728 let invalid_limit_str2 = "C[e1,e2,e3C[e4,S(e5)]S(e6)";
2729 assert!(
2730 ProfileLimit::parse_limit(invalid_limit_str2).is_err(),
2731 "Expected error for unmatched brackets"
2732 );
2733
2734 let invalid_limit_str3 = "C[e1,e2,e3]T(e8)";
2735 assert!(
2736 ProfileLimit::parse_limit(invalid_limit_str3).is_err(),
2737 "Expected error for invalid threshold syntax"
2738 );
2739
2740 let invalid_limit_str4 = "C[e1,e2,e3]T(t8)";
2741 assert!(
2742 ProfileLimit::parse_limit(invalid_limit_str4).is_err(),
2743 "Expected error for mixed threshold and IR limit syntax"
2744 );
2745
2746 let invalid_limit_str5 = "T(t8)T(t9)";
2747 assert!(
2748 ProfileLimit::parse_limit(invalid_limit_str5).is_err(),
2749 "Expected error for multiple threshold limits"
2750 );
2751 }
2752
2753 #[test]
2754 fn resolve_existing_esurface_id_for_threshold_limit() {
2755 let threshold_limit = ThresholdLimit {
2756 esurface_id: RaisedEsurfaceId::from(7usize),
2757 variant_id: None,
2758 };
2759 let esurface_map = ti_vec![
2760 ti_vec![
2761 Some(RaisedEsurfaceId::from(5usize)),
2762 Some(RaisedEsurfaceId::from(6usize))
2763 ],
2764 ti_vec![Some(RaisedEsurfaceId::from(7usize)), None],
2765 ];
2766 let existing_esurfaces =
2767 ti_vec![GroupEsurfaceId::from(0usize), GroupEsurfaceId::from(1usize)];
2768
2769 let existing_esurface_id = threshold_limit
2770 .resolve_existing_esurface_id(
2771 &esurface_map,
2772 GraphGroupPosition::from(0usize),
2773 &existing_esurfaces,
2774 None,
2775 &TiVec::new(),
2776 )
2777 .unwrap();
2778
2779 assert_eq!(existing_esurface_id, ExistingEsurfaceId::from(1usize));
2780 }
2781
2782 #[test]
2783 fn resolve_existing_esurface_id_rejects_threshold_missing_from_graph() {
2784 let threshold_limit = ThresholdLimit {
2785 esurface_id: RaisedEsurfaceId::from(9usize),
2786 variant_id: None,
2787 };
2788 let esurface_map = ti_vec![ti_vec![
2789 Some(RaisedEsurfaceId::from(5usize)),
2790 Some(RaisedEsurfaceId::from(6usize))
2791 ]];
2792 let existing_esurfaces = ti_vec![GroupEsurfaceId::from(0usize)];
2793
2794 assert!(
2795 threshold_limit
2796 .resolve_existing_esurface_id(
2797 &esurface_map,
2798 GraphGroupPosition::from(0usize),
2799 &existing_esurfaces,
2800 None,
2801 &TiVec::new(),
2802 )
2803 .is_err()
2804 );
2805 }
2806
2807 #[test]
2808 fn resolve_existing_esurface_id_rejects_threshold_missing_from_overlap() {
2809 let threshold_limit = ThresholdLimit {
2810 esurface_id: RaisedEsurfaceId::from(7usize),
2811 variant_id: None,
2812 };
2813 let esurface_map = ti_vec![ti_vec![Some(RaisedEsurfaceId::from(7usize))]];
2814 let existing_esurfaces = ti_vec![GroupEsurfaceId::from(1usize)];
2815
2816 assert!(
2817 threshold_limit
2818 .resolve_existing_esurface_id(
2819 &esurface_map,
2820 GraphGroupPosition::from(0usize),
2821 &existing_esurfaces,
2822 None,
2823 &TiVec::new(),
2824 )
2825 .is_err()
2826 );
2827 }
2828
2829 #[test]
2830 fn generalized_threshold_resolution_requires_a_variant_when_geometry_is_duplicated() {
2831 let raised_esurface_id = RaisedEsurfaceId::from(7usize);
2832 let esurface_map = ti_vec![ti_vec![Some(raised_esurface_id)]];
2833 let existing_esurfaces =
2834 ti_vec![GroupEsurfaceId::from(0usize), GroupEsurfaceId::from(0usize),];
2835 let variant_ids = ti_vec![
2836 ThresholdCountertermVariantId(0),
2837 ThresholdCountertermVariantId(1),
2838 ];
2839 let variant_raised_esurfaces = ti_vec![raised_esurface_id, raised_esurface_id];
2840
2841 let ambiguous = ThresholdLimit {
2842 esurface_id: raised_esurface_id,
2843 variant_id: None,
2844 }
2845 .resolve_existing_esurface_id(
2846 &esurface_map,
2847 GraphGroupPosition::from(0usize),
2848 &existing_esurfaces,
2849 Some(&variant_ids),
2850 &variant_raised_esurfaces,
2851 )
2852 .unwrap_err();
2853 let message = ambiguous.to_string();
2854 assert!(message.contains("ambiguous"));
2855 assert!(message.contains("v0, v1"));
2856 assert!(message.contains("T(t7:vN)"));
2857
2858 let explicit = ThresholdLimit {
2859 esurface_id: raised_esurface_id,
2860 variant_id: Some(ThresholdCountertermVariantId(1)),
2861 }
2862 .resolve_existing_esurface_id(
2863 &esurface_map,
2864 GraphGroupPosition::from(0usize),
2865 &existing_esurfaces,
2866 Some(&variant_ids),
2867 &variant_raised_esurfaces,
2868 )
2869 .unwrap();
2870 assert_eq!(explicit, ExistingEsurfaceId::from(1usize));
2871
2872 let unique_existing_esurfaces = ti_vec![GroupEsurfaceId::from(0usize)];
2873 let unique_variant_ids = ti_vec![ThresholdCountertermVariantId(1)];
2874 let shorthand = ThresholdLimit {
2875 esurface_id: raised_esurface_id,
2876 variant_id: None,
2877 }
2878 .resolve_existing_esurface_id(
2879 &esurface_map,
2880 GraphGroupPosition::from(0usize),
2881 &unique_existing_esurfaces,
2882 Some(&unique_variant_ids),
2883 &variant_raised_esurfaces,
2884 )
2885 .unwrap();
2886 assert_eq!(shorthand, ExistingEsurfaceId::from(0usize));
2887 }
2888
2889 #[test]
2890 fn enumerate_threshold_limits_from_overlap_structure() {
2891 let esurface_map = ti_vec![
2892 ti_vec![
2893 Some(RaisedEsurfaceId::from(5usize)),
2894 Some(RaisedEsurfaceId::from(8usize))
2895 ],
2896 ti_vec![Some(RaisedEsurfaceId::from(7usize)), None],
2897 ti_vec![
2898 Some(RaisedEsurfaceId::from(5usize)),
2899 Some(RaisedEsurfaceId::from(9usize))
2900 ],
2901 ti_vec![None, Some(RaisedEsurfaceId::from(3usize))],
2902 ];
2903 let existing_esurfaces = ti_vec![
2904 GroupEsurfaceId::from(2usize),
2905 GroupEsurfaceId::from(0usize),
2906 GroupEsurfaceId::from(1usize),
2907 GroupEsurfaceId::from(3usize),
2908 ];
2909
2910 let threshold_limits = ThresholdLimit::enumerate_from_overlap_structure(
2911 &existing_esurfaces,
2912 &esurface_map,
2913 GraphGroupPosition::from(0usize),
2914 );
2915 let threshold_limits_for_other_group = ThresholdLimit::enumerate_from_overlap_structure(
2916 &existing_esurfaces,
2917 &esurface_map,
2918 GraphGroupPosition::from(1usize),
2919 );
2920
2921 assert_eq!(
2922 threshold_limits,
2923 vec![
2924 ThresholdLimit {
2925 esurface_id: RaisedEsurfaceId::from(5usize),
2926 variant_id: None,
2927 },
2928 ThresholdLimit {
2929 esurface_id: RaisedEsurfaceId::from(7usize),
2930 variant_id: None,
2931 },
2932 ]
2933 );
2934 assert_eq!(
2935 threshold_limits_for_other_group,
2936 vec![
2937 ThresholdLimit {
2938 esurface_id: RaisedEsurfaceId::from(3usize),
2939 variant_id: None,
2940 },
2941 ThresholdLimit {
2942 esurface_id: RaisedEsurfaceId::from(8usize),
2943 variant_id: None,
2944 },
2945 ThresholdLimit {
2946 esurface_id: RaisedEsurfaceId::from(9usize),
2947 variant_id: None,
2948 },
2949 ]
2950 );
2951 }
2952
2953 fn test_ir_profile_settings(steps: usize) -> IRProfileSetting {
2954 IRProfileSetting {
2955 lambda_exp_start: -3.0,
2956 lambda_exp_end: -1.0,
2957 steps,
2958 seed: 0,
2959 select_limits_and_graphs: None,
2960 orientation_mode: OrientationProfileMode::Summed,
2961 show_per_cut_info: false,
2962 }
2963 }
2964
2965 fn test_momentum_sample(loop_momenta: Vec<ThreeMomentum<F<f64>>>) -> MomentumSample<f64> {
2966 MomentumSample {
2967 sample: crate::momentum::sample::BareMomentumSample {
2968 loop_moms: loop_momenta.into_iter().collect(),
2969 dual_loop_moms: None,
2970 loop_mom_cache_id: 0,
2971 loop_mom_base_cache_id: 0,
2972 external_moms: ti_vec![],
2973 external_mom_cache_id: 0,
2974 external_mom_base_cache_id: 0,
2975 jacobian: F::from_f64(1.0),
2976 orientation: None,
2977 parameterization_branch: None,
2978 },
2979 }
2980 }
2981
2982 #[test]
2983 fn threshold_approach_loop_momenta_starts_at_stored_threshold_point() {
2984 let overlap_group_center: LoopMomenta<F<f64>> = vec![ThreeMomentum::new(
2985 F::from_f64(5.0),
2986 F::from_f64(7.0),
2987 F::from_f64(9.0),
2988 )]
2989 .into_iter()
2990 .collect();
2991 let threshold_point = test_momentum_sample(vec![ThreeMomentum::new(
2992 F::from_f64(1.0),
2993 F::from_f64(3.0),
2994 F::from_f64(5.0),
2995 )]);
2996
2997 let at_threshold = threshold_approach_loop_momenta(
2998 &overlap_group_center,
2999 &threshold_point,
3000 &F::from_f64(0.0),
3001 );
3002 let halfway = threshold_approach_loop_momenta(
3003 &overlap_group_center,
3004 &threshold_point,
3005 &F::from_f64(0.5),
3006 );
3007 let at_center = threshold_approach_loop_momenta(
3008 &overlap_group_center,
3009 &threshold_point,
3010 &F::from_f64(1.0),
3011 );
3012
3013 assert_eq!(at_threshold, threshold_point.loop_moms().clone());
3014 assert_eq!(
3015 halfway,
3016 vec![ThreeMomentum::new(
3017 F::from_f64(3.0),
3018 F::from_f64(5.0),
3019 F::from_f64(7.0),
3020 )]
3021 .into_iter()
3022 .collect()
3023 );
3024 assert_eq!(at_center, overlap_group_center);
3025 }
3026
3027 #[test]
3028 fn threshold_limit_builds_group_trajectories_for_matching_overlap_groups() {
3029 let threshold_limit = ThresholdLimit {
3030 esurface_id: RaisedEsurfaceId::from(7usize),
3031 variant_id: None,
3032 };
3033 let threshold_point = test_momentum_sample(vec![ThreeMomentum::new(
3034 F::from_f64(1.0),
3035 F::from_f64(2.0),
3036 F::from_f64(3.0),
3037 )]);
3038 let overlap_structure = OverlapStructureWithKinematics {
3039 existing_esurfaces: ti_vec![
3040 GroupEsurfaceId::from(0usize),
3041 GroupEsurfaceId::from(1usize)
3042 ],
3043 variant_ids: None,
3044 overlap_groups_with_kinematics: vec![
3045 crate::subtraction::amplitude_counterterm::OverlapGroupWithKinematics {
3046 overlap_group: crate::subtraction::overlap::OverlapGroup {
3047 existing_esurfaces: vec![ExistingEsurfaceId::from(1usize)],
3048 complement: vec![],
3049 center: vec![ThreeMomentum::new(
3050 F::from_f64(5.0),
3051 F::from_f64(6.0),
3052 F::from_f64(7.0),
3053 )]
3054 .into_iter()
3055 .collect(),
3056 prefactor_evaluator: None,
3057 },
3058 loop_momenta_at_esurface: ti_vec![Some(threshold_point.clone())],
3059 approach_centers_at_esurface: None,
3060 },
3061 crate::subtraction::amplitude_counterterm::OverlapGroupWithKinematics {
3062 overlap_group: crate::subtraction::overlap::OverlapGroup {
3063 existing_esurfaces: vec![ExistingEsurfaceId::from(0usize)],
3064 complement: vec![],
3065 center: vec![ThreeMomentum::new(
3066 F::from_f64(8.0),
3067 F::from_f64(9.0),
3068 F::from_f64(10.0),
3069 )]
3070 .into_iter()
3071 .collect(),
3072 prefactor_evaluator: None,
3073 },
3074 loop_momenta_at_esurface: ti_vec![Some(test_momentum_sample(vec![
3075 ThreeMomentum::new(F::from_f64(2.0), F::from_f64(3.0), F::from_f64(4.0),),
3076 ]))],
3077 approach_centers_at_esurface: None,
3078 },
3079 ],
3080 };
3081
3082 let trajectories = threshold_limit
3083 .get_momenta_per_overlap_group(
3084 &overlap_structure,
3085 ExistingEsurfaceId::from(1usize),
3086 &test_ir_profile_settings(4),
3087 )
3088 .unwrap();
3089
3090 assert_eq!(trajectories.len(), 1);
3091 assert_eq!(trajectories[0].0, "overlap group 0");
3092 assert_eq!(trajectories[0].1.len(), 4);
3093
3094 for lambda_point in &trajectories[0].1 {
3095 assert_eq!(
3096 lambda_point.loop_momenta,
3097 threshold_approach_loop_momenta(
3098 &overlap_structure.overlap_groups_with_kinematics[0]
3099 .overlap_group
3100 .center,
3101 &threshold_point,
3102 &lambda_point.lambda,
3103 )
3104 );
3105 }
3106 }
3107
3108 #[test]
3109 fn generalized_threshold_trajectory_uses_its_full_fixed_complement_center() {
3110 let variant_id = ThresholdCountertermVariantId(2);
3111 let threshold_limit = ThresholdLimit {
3112 esurface_id: RaisedEsurfaceId::from(7usize),
3113 variant_id: Some(variant_id),
3114 };
3115 let threshold_point = test_momentum_sample(vec![
3116 ThreeMomentum::new(F::from_f64(1.0), F::from_f64(2.0), F::from_f64(3.0)),
3117 ThreeMomentum::new(F::from_f64(4.0), F::from_f64(5.0), F::from_f64(6.0)),
3118 ]);
3119 let full_center: LoopMomenta<F<f64>> = vec![
3120 ThreeMomentum::new(F::from_f64(7.0), F::from_f64(8.0), F::from_f64(9.0)),
3121 threshold_point.loop_moms()[LoopIndex::from(1usize)],
3122 ]
3123 .into_iter()
3124 .collect();
3125 let overlap_structure = OverlapStructureWithKinematics {
3126 existing_esurfaces: ti_vec![GroupEsurfaceId::from(0usize)],
3127 variant_ids: Some(ti_vec![variant_id]),
3128 overlap_groups_with_kinematics: vec![
3129 crate::subtraction::amplitude_counterterm::OverlapGroupWithKinematics {
3130 overlap_group: crate::subtraction::overlap::OverlapGroup {
3131 existing_esurfaces: vec![ExistingEsurfaceId::from(0usize)],
3132 complement: vec![],
3133 center: vec![
3136 ThreeMomentum::new(
3137 F::from_f64(20.0),
3138 F::from_f64(20.0),
3139 F::from_f64(20.0),
3140 ),
3141 ThreeMomentum::new(
3142 F::from_f64(30.0),
3143 F::from_f64(30.0),
3144 F::from_f64(30.0),
3145 ),
3146 ]
3147 .into_iter()
3148 .collect(),
3149 prefactor_evaluator: None,
3150 },
3151 loop_momenta_at_esurface: ti_vec![Some(threshold_point.clone())],
3152 approach_centers_at_esurface: Some(ti_vec![Some(full_center.clone())]),
3153 },
3154 ],
3155 };
3156
3157 let trajectories = threshold_limit
3158 .get_momenta_per_overlap_group(
3159 &overlap_structure,
3160 ExistingEsurfaceId::from(0usize),
3161 &test_ir_profile_settings(4),
3162 )
3163 .unwrap();
3164 assert_eq!(trajectories.len(), 1);
3165 for point in &trajectories[0].1 {
3166 let expected = threshold_point.loop_moms()
3167 + &(&full_center - threshold_point.loop_moms()).rescale(&point.lambda, None);
3168 assert_eq!(point.loop_momenta, expected);
3169 assert_eq!(
3170 point.loop_momenta[LoopIndex::from(1usize)],
3171 threshold_point.loop_moms()[LoopIndex::from(1usize)],
3172 "the inactive loop momentum must remain fixed along a projected approach",
3173 );
3174 }
3175 }
3176
3177 #[test]
3178 fn threshold_limit_rejects_group_missing_threshold_kinematics() {
3179 let threshold_limit = ThresholdLimit {
3180 esurface_id: RaisedEsurfaceId::from(7usize),
3181 variant_id: None,
3182 };
3183 let overlap_structure: OverlapStructureWithKinematics<f64> =
3184 OverlapStructureWithKinematics {
3185 existing_esurfaces: ti_vec![GroupEsurfaceId::from(1usize)],
3186 variant_ids: None,
3187 overlap_groups_with_kinematics: vec![
3188 crate::subtraction::amplitude_counterterm::OverlapGroupWithKinematics {
3189 overlap_group: crate::subtraction::overlap::OverlapGroup {
3190 existing_esurfaces: vec![ExistingEsurfaceId::from(0usize)],
3191 complement: vec![],
3192 center: vec![ThreeMomentum::new(
3193 F::from_f64(5.0),
3194 F::from_f64(6.0),
3195 F::from_f64(7.0),
3196 )]
3197 .into_iter()
3198 .collect(),
3199 prefactor_evaluator: None,
3200 },
3201 loop_momenta_at_esurface: ti_vec![None],
3202 approach_centers_at_esurface: None,
3203 },
3204 ],
3205 };
3206
3207 assert!(
3208 threshold_limit
3209 .get_momenta_per_overlap_group(
3210 &overlap_structure,
3211 ExistingEsurfaceId::from(0usize),
3212 &test_ir_profile_settings(3),
3213 )
3214 .is_err()
3215 );
3216 }
3217
3218 #[test]
3219 fn fit_power_law_recovers_known_parameters() {
3220 let exponent = -1.75_f64;
3221 let coefficient = 3.2_f64;
3222 let offset = 0.6_f64;
3223
3224 let x = constant_dropped_fit_points(
3225 &F::<ArbPrec>::from_f64(0.2_f64 * 1.6_f64.powi(7)),
3226 &F::<ArbPrec>::from_f64(0.2_f64),
3227 8,
3228 )
3229 .expect("geometric fit points should be generated");
3230 let y = x
3231 .iter()
3232 .map(|xv| F::<ArbPrec>::from_f64(coefficient * xv.into_f64().powf(exponent) + offset))
3233 .collect::<Vec<_>>();
3234
3235 let fit = fit_power_law(x, y).expect("power-law fit should succeed");
3236
3237 assert!((fit.exponent - exponent).abs() < 1e-10);
3238 assert!(fit.r_squared > 0.999_999);
3239 }
3240
3241 #[test]
3242 fn fit_power_law_rejects_non_geometric_grid() {
3243 let exponent = -1.75_f64;
3244 let coefficient = 3.2_f64;
3245 let offset = 0.6_f64;
3246
3247 let x = [
3248 0.2_f64, 0.37_f64, 0.55_f64, 0.92_f64, 1.3_f64, 1.85_f64, 2.75_f64, 3.6_f64,
3249 ]
3250 .into_iter()
3251 .map(F::<ArbPrec>::from_f64)
3252 .collect::<Vec<_>>();
3253 let y = x
3254 .iter()
3255 .map(|xv| F::<ArbPrec>::from_f64(coefficient * xv.into_f64().powf(exponent) + offset))
3256 .collect::<Vec<_>>();
3257
3258 let fit = fit_power_law(x, y);
3259
3260 assert!(fit.is_err());
3261 }
3262
3263 #[test]
3264 fn negative_cut_scaling_does_not_fail_limit() {
3265 let ir_limit = IrLimit::new_pure_soft(vec![EdgeIndex::from(1)]);
3266 let total_fit = PowerLawFit {
3267 exponent: -2.5,
3268 r_squared: 0.999,
3269 };
3270 let cut_reports = build_cut_limit_reports(
3271 ir_limit.num_soft(),
3272 vec![
3273 (
3274 0,
3275 Ok(PowerLawFit {
3276 exponent: -4.5,
3277 r_squared: 0.999,
3278 }),
3279 ),
3280 (
3281 1,
3282 Ok(PowerLawFit {
3283 exponent: -2.5,
3284 r_squared: 0.999,
3285 }),
3286 ),
3287 ],
3288 );
3289
3290 let report = build_single_limit_report(&ir_limit, None, total_fit, cut_reports);
3291
3292 assert!(report.passed);
3293 assert_eq!(report.per_cut_reports.len(), 2);
3294 assert!(report.per_cut_reports[0].scaling.unwrap() < 0.0);
3295 assert!(report.per_cut_reports[1].scaling.unwrap() > 0.0);
3296 }
3297
3298 #[test]
3299 fn single_limit_display_includes_per_cut_table() {
3300 let ir_limit = IrLimit::new_pure_soft(vec![EdgeIndex::from(1)]);
3301 let total_fit = PowerLawFit {
3302 exponent: -2.5,
3303 r_squared: 0.999,
3304 };
3305 let cut_reports = build_cut_limit_reports(
3306 ir_limit.num_soft(),
3307 vec![(
3308 0,
3309 Ok(PowerLawFit {
3310 exponent: -4.5,
3311 r_squared: 0.999,
3312 }),
3313 )],
3314 );
3315
3316 let report = build_single_limit_report(&ir_limit, None, total_fit, cut_reports);
3317 let rendered = format!("{report}");
3318
3319 assert!(rendered.contains("per-cut fits for"));
3320 assert!(rendered.contains("cut"));
3321 assert!(rendered.contains("r_squared"));
3322 }
3323
3324 #[test]
3325 fn single_limit_display_includes_display_only_table() {
3326 let ir_limit = IrLimit::new_pure_soft(vec![EdgeIndex::from(1)]);
3327 let total_fit = PowerLawFit {
3328 exponent: -2.5,
3329 r_squared: 0.999,
3330 };
3331 let mut report = build_single_limit_report(&ir_limit, None, total_fit, Vec::new());
3332 report.display_only_reports = build_display_only_limit_reports(vec![
3333 (
3334 AdditionalWeightKey::Original,
3335 Ok(PowerLawFit {
3336 exponent: -1.5,
3337 r_squared: 0.995,
3338 }),
3339 ),
3340 (
3341 AdditionalWeightKey::AmplitudeThresholdCounterterm {
3342 esurface_id: 3,
3343 overlap_group: 1,
3344 },
3345 Ok(PowerLawFit {
3346 exponent: -0.5,
3347 r_squared: 0.991,
3348 }),
3349 ),
3350 ]);
3351
3352 let rendered = format!("{report}");
3353
3354 assert!(rendered.contains("display-only fits for"));
3355 assert!(rendered.contains("original"));
3356 assert!(rendered.contains("ct_3_1"));
3357 assert!(!rendered.contains("note"));
3358 }
3359
3360 #[test]
3361 fn single_limit_display_includes_display_only_fit_error_reason() {
3362 let ir_limit = IrLimit::new_pure_soft(vec![EdgeIndex::from(1)]);
3363 let total_fit = PowerLawFit {
3364 exponent: -2.5,
3365 r_squared: 0.999,
3366 };
3367 let mut report = build_single_limit_report(&ir_limit, None, total_fit, Vec::new());
3368 report.display_only_reports = build_display_only_limit_reports(vec![(
3369 AdditionalWeightKey::Original,
3370 Err(eyre!("fit_power_law requires at least three observations")),
3371 )]);
3372
3373 let rendered = format!("{report}");
3374
3375 assert!(
3376 rendered
3377 .contains("original (no fit: fit_power_law requires at least three observations)")
3378 );
3379 }
3380
3381 #[test]
3382 fn graph_limit_display_includes_per_cut_fit_error_reason() {
3383 let ir_limit = IrLimit::new_pure_soft(vec![EdgeIndex::from(1)]);
3384 let total_fit = PowerLawFit {
3385 exponent: -2.5,
3386 r_squared: 0.999,
3387 };
3388 let graph_report = GraphIRLimitReport {
3389 graph_name: "GL0".to_string(),
3390 all_limits_passed: true,
3391 cut_definitions: Vec::new(),
3392 single_limit_reports: vec![build_single_limit_report(
3393 &ir_limit,
3394 None,
3395 total_fit,
3396 build_cut_limit_reports(
3397 ir_limit.num_soft(),
3398 vec![(0, Err(eyre!("fit_power_law requires finite y values")))],
3399 ),
3400 )],
3401 };
3402
3403 let rendered = format!("{graph_report}");
3404
3405 assert!(rendered.contains("cut 0 (no fit: fit_power_law requires finite y values)"));
3406 }
3407
3408 #[test]
3409 fn graph_limit_display_weaves_per_cut_rows_into_limit_table() {
3410 let ir_limit = IrLimit::new_pure_soft(vec![EdgeIndex::from(1)]);
3411 let total_fit = PowerLawFit {
3412 exponent: -2.5,
3413 r_squared: 0.999,
3414 };
3415 let cut_reports = build_cut_limit_reports(
3416 ir_limit.num_soft(),
3417 vec![
3418 (
3419 0,
3420 Ok(PowerLawFit {
3421 exponent: -4.5,
3422 r_squared: 0.999,
3423 }),
3424 ),
3425 (
3426 1,
3427 Ok(PowerLawFit {
3428 exponent: -2.5,
3429 r_squared: 0.999,
3430 }),
3431 ),
3432 ],
3433 );
3434
3435 let mut report = build_single_limit_report(&ir_limit, None, total_fit, cut_reports);
3436 report.display_only_reports = build_display_only_limit_reports(vec![
3437 (
3438 AdditionalWeightKey::Original,
3439 Ok(PowerLawFit {
3440 exponent: -1.5,
3441 r_squared: 0.995,
3442 }),
3443 ),
3444 (
3445 AdditionalWeightKey::AmplitudeThresholdCounterterm {
3446 esurface_id: 3,
3447 overlap_group: 1,
3448 },
3449 Ok(PowerLawFit {
3450 exponent: -0.5,
3451 r_squared: 0.991,
3452 }),
3453 ),
3454 ]);
3455 let second_report = build_single_limit_report(
3456 &ir_limit,
3457 Some("ori-1".to_string()),
3458 PowerLawFit {
3459 exponent: -2.5,
3460 r_squared: 0.999,
3461 },
3462 build_cut_limit_reports(
3463 ir_limit.num_soft(),
3464 vec![(
3465 2,
3466 Ok(PowerLawFit {
3467 exponent: -1.5,
3468 r_squared: 0.995,
3469 }),
3470 )],
3471 ),
3472 );
3473 let graph_report = GraphIRLimitReport {
3474 graph_name: "GL0".to_string(),
3475 all_limits_passed: true,
3476 cut_definitions: vec![
3477 GraphCutDefinition {
3478 cut_id: 0,
3479 edges: vec![EdgeIndex::from(1), EdgeIndex::from(3)],
3480 },
3481 GraphCutDefinition {
3482 cut_id: 1,
3483 edges: vec![EdgeIndex::from(2), EdgeIndex::from(4)],
3484 },
3485 ],
3486 single_limit_reports: vec![report, second_report],
3487 };
3488 let rendered = format!("{graph_report}");
3489
3490 assert!(rendered.contains("cut definitions"));
3491 assert!(rendered.contains("edges"));
3492 assert!(rendered.contains("e1, e3"));
3493 assert!(rendered.find("cut definitions").unwrap() < rendered.find("status").unwrap());
3494 assert!(rendered.contains("item"));
3495 assert!(rendered.contains("cut 0"));
3496 assert!(rendered.contains("original"));
3497 assert!(rendered.contains("ct_3_1"));
3498 assert!(!rendered.contains("note"));
3499 assert!(rendered.contains("sum"));
3500 assert!(rendered.contains("INFO"));
3501 assert!(!rendered.contains("per-cut fits for"));
3502 assert!(rendered.matches('├').count() >= 3);
3503 }
3504
3505 #[test]
3506 fn limit_data_extract_power_includes_display_only_component_fits() {
3507 let lambdas = constant_dropped_fit_points(
3508 &F::<f64>::from_f64(1.0e-3),
3509 &F::<f64>::from_f64(1.0e-1),
3510 5,
3511 )
3512 .unwrap();
3513
3514 let data = lambdas
3515 .into_iter()
3516 .map(|lambda| {
3517 let lambda_f64 = lambda.into_ff64().0;
3518 let mut display_only_components = BTreeMap::new();
3519 display_only_components.insert(
3520 AdditionalWeightKey::Original,
3521 F::<ArbPrec>::from_f64(3.0 * lambda_f64.powf(-1.5) + 0.5),
3522 );
3523 display_only_components.insert(
3524 AdditionalWeightKey::AmplitudeThresholdCounterterm {
3525 esurface_id: 3,
3526 overlap_group: 1,
3527 },
3528 F::<ArbPrec>::from_f64(5.0 * lambda_f64.powf(-0.5) + 1.0),
3529 );
3530 display_only_components.insert(
3531 AdditionalWeightKey::FullMultiplicativeFactor,
3532 F::<ArbPrec>::from_f64(7.0 * lambda_f64.powf(-2.5) + 1.0),
3533 );
3534
3535 LambdaPointEval {
3536 lambda,
3537 value: ProfilePointValue {
3538 total: F::<ArbPrec>::from_f64(2.0 * lambda_f64.powf(-2.0) + 1.0),
3539 per_cut: BTreeMap::new(),
3540 display_only_components,
3541 },
3542 }
3543 })
3544 .collect();
3545
3546 let power_fits = LimitData { data }.extract_power().unwrap();
3547
3548 assert!((power_fits.total.exponent + 2.0).abs() < 1.0e-10);
3549 assert_eq!(power_fits.display_components.len(), 2);
3550
3551 let component_fits = power_fits
3552 .display_components
3553 .into_iter()
3554 .collect::<BTreeMap<_, _>>();
3555 let original_fit = component_fits
3556 .get(&AdditionalWeightKey::Original)
3557 .unwrap()
3558 .as_ref()
3559 .unwrap();
3560 let ct_fit = component_fits
3561 .get(&AdditionalWeightKey::AmplitudeThresholdCounterterm {
3562 esurface_id: 3,
3563 overlap_group: 1,
3564 })
3565 .unwrap()
3566 .as_ref()
3567 .unwrap();
3568
3569 assert!((original_fit.exponent + 1.5).abs() < 1.0e-10);
3570 assert!((ct_fit.exponent + 0.5).abs() < 1.0e-10);
3571 }
3572}