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