1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fmt::{Display, Formatter},
4 fs::{self, File},
5 io::Write,
6 ops::{Index, IndexMut},
7 path::{Path, PathBuf},
8};
9
10use ahash::HashMap;
11use bincode_trait_derive::{Decode, Encode};
13use color_eyre::Result;
14use itertools::Itertools;
15use rayon::{
16 ThreadPool,
17 iter::{IntoParallelRefMutIterator, ParallelIterator},
18};
19use spenso::algebra::algebraic_traits::IsZero;
20use tracing::info;
21use vakint::Vakint;
22
23use crate::{
24 DependentMomentaConstructor, GammaLoopContext, GammaLoopContextContainer,
25 cff::{
26 CutCFFIndex,
27 esurface::{RaisedEsurfaceData, RaisedEsurfaceGroup, RaisedEsurfaceId},
28 expression::{CFFExpression, OrientationID},
29 },
30 debug_tags, define_index,
31 graph::{
32 GraphGroup, GroupId, LMBext, LmbChannelFallback, LmbIndex, LoopMomentumBasis,
33 ThresholdPinchStatus,
34 cuts::{CutSet, ResidueSelector},
35 edge::EdgeMass,
36 parse::complete_group_parsing,
37 },
38 integrands::process::{
39 GenericEvaluator, LmbMultiChannelingSetup, ParamBuilder,
40 cross_section::CrossSectionIntegrandData, graph_to_group_id_for_group_structure,
41 },
42 model::ArcParticle,
43 momentum::{
44 Helicity,
45 sample::{ExternalIndex, SubspaceData},
46 },
47 processes::{
48 DotExportSettings, EvaluatorSettings, GraphCutSelectionSubject, GraphGenerationStats,
49 GraphGroupSelectionMode, GraphGroupSelectionPlan, GraphGroupSelectionSpec,
50 GraphSelectionSignatureInventory, GraphSelectionSubject, NamedGraphGenerationReport,
51 RaisedCutSignatureInventory,
52 },
53 settings::{
54 GlobalSettings, RuntimeSettings, global::GenerationSettings, runtime::LockedRuntimeSettings,
55 },
56 utils::{
57 DEFAULT_ESURFACE_EXISTENCE_THRESHOLD, F, GS, W_,
58 hyperdual_utils::{shape_from_cut_cff_index, simple_n_deriv_shape},
59 },
60 uv::{
61 approx::{CutStructure, OrientationProjection},
62 forest::ParametricIntegrands,
63 },
64};
65use eyre::{Context, eyre};
66use linnet::half_edge::{
67 involution::{EdgeIndex, EdgeVec, Orientation},
68 subgraph::{
69 HedgeNode, Inclusion, InternalSubGraph, ModifySubSet, OrientedCut, SuBitGraph,
70 SubGraphLike, SubSetLike, SubSetOps,
71 },
72};
73use serde::{Deserialize, Serialize};
74use symbolica::{domains::dual::HyperDual, prelude::*};
75use tracing::{debug, warn};
76use typed_index_collections::{TiVec, ti_vec};
77
78use super::generation_progress::{self, GenerationProcessKind, GenerationProgressPhase};
79
80use crate::{
81 cff::esurface::{Esurface, EsurfaceID},
82 graph::{ExternalConnection, FeynmanGraph, Graph},
83 integrands::process::{
84 ProcessIntegrand,
85 cross_section::{CrossSectionGraphTerm, CrossSectionIntegrand},
86 },
87 model::Model,
88};
89
90use crate::processes::ProcessDefinition;
91
92#[derive(Clone, Debug, Encode, Decode)]
93pub struct IteratedCtCollection<T> {
94 data: Vec<T>,
95 num_right_thresholds: usize,
96}
97
98impl<T> IteratedCtCollection<T> {
99 pub(crate) fn new(
100 data: Vec<T>,
101 num_left_thresholds: usize,
102 num_right_thresholds: usize,
103 ) -> Self {
104 let expected_len = num_left_thresholds
105 .checked_mul(num_right_thresholds)
106 .expect("iterated threshold-counterterm dimensions overflow usize");
107 assert_eq!(
108 data.len(),
109 expected_len,
110 "iterated threshold-counterterm data must contain one entry per left/right pair"
111 );
112 Self {
113 data,
114 num_right_thresholds,
115 }
116 }
117
118 pub fn map_ref<U, F>(&self, f: F) -> IteratedCtCollection<U>
119 where
120 F: Fn(&T) -> U,
121 {
122 let data = self.data.iter().map(f).collect();
123 IteratedCtCollection {
124 data,
125 num_right_thresholds: self.num_right_thresholds,
126 }
127 }
128
129 pub(crate) fn iter(&self) -> impl Iterator<Item = &T> {
130 self.data.iter()
131 }
132
133 pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
134 self.data.iter_mut()
135 }
136
137 pub(crate) fn num_right_thresholds(&self) -> usize {
138 self.num_right_thresholds
139 }
140}
141
142impl<T> Index<(LeftThresholdId, RightThresholdId)> for IteratedCtCollection<T> {
143 type Output = T;
144
145 fn index(&self, index: (LeftThresholdId, RightThresholdId)) -> &Self::Output {
146 let (left_id, right_id) = index;
147 &self.data[left_id.0 * self.num_right_thresholds + right_id.0]
148 }
149}
150
151impl<T> IndexMut<(LeftThresholdId, RightThresholdId)> for IteratedCtCollection<T> {
152 fn index_mut(&mut self, index: (LeftThresholdId, RightThresholdId)) -> &mut Self::Output {
153 let (left_id, right_id) = index;
154 &mut self.data[left_id.0 * self.num_right_thresholds + right_id.0]
155 }
156}
157
158#[derive(Clone, Encode, Decode)]
159#[trait_decode(trait = GammaLoopContext)]
160pub struct LUCounterTermData {
161 pub left_thresholds: TiVec<LeftThresholdId, RaisedEsurfaceGroup>,
162 pub right_thresholds: TiVec<RightThresholdId, RaisedEsurfaceGroup>,
163 pub left_atoms: TiVec<LeftThresholdId, ParametricIntegrands>,
164 pub right_atoms: TiVec<RightThresholdId, ParametricIntegrands>,
165 pub iterated: IteratedCtCollection<ParametricIntegrands>,
166}
167
168fn max_dual_size_for_cut_cff_indices<'a>(
169 cut_cff_indices: impl Iterator<Item = &'a CutCFFIndex>,
170) -> usize {
171 cut_cff_indices
172 .map(|cut_cff_index| {
173 shape_from_cut_cff_index(cut_cff_index)
174 .map(|shape| HyperDual::<F<f64>>::new(shape).values.len())
175 .unwrap_or(1)
176 })
177 .max()
178 .unwrap_or(1)
179}
180
181define_index! {pub struct RightThresholdId;}
182define_index! {pub struct LeftThresholdId;}
183define_index! {pub struct CutGroupId;}
184
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub enum ThresholdCountertermStatus {
188 NoRadialDependence,
190 AlwaysPinched,
192 CanBecomePinched,
194 ProvenNonExisting,
196 PotentiallyExisting,
198}
199
200impl ThresholdCountertermStatus {
201 pub fn is_eligible_for_generation(self, check_current_model: bool) -> bool {
202 match self {
203 Self::NoRadialDependence | Self::AlwaysPinched => false,
204 Self::ProvenNonExisting => !check_current_model,
205 Self::CanBecomePinched | Self::PotentiallyExisting => true,
206 }
207 }
208}
209
210#[derive(Clone, Debug, Encode, Decode)]
212#[trait_decode(trait = GammaLoopContext)]
213pub struct ThresholdCountertermAssociation {
214 pub esurface_id: EsurfaceID,
215 pub cut_boundary_edges: Vec<EdgeIndex>,
216 pub threshold_boundary_edges: Vec<EdgeIndex>,
217 pub invariant_bound_is_applicable: bool,
218}
219
220#[derive(Clone, Debug, Default, Encode, Decode)]
221#[trait_decode(trait = GammaLoopContext)]
222pub struct CutThresholdCountertermAssociations {
223 pub left: Vec<ThresholdCountertermAssociation>,
224 pub right: Vec<ThresholdCountertermAssociation>,
225}
226
227impl ThresholdCountertermAssociation {
228 fn mass_sum(edges: &[EdgeIndex], masses: &EdgeVec<Option<F<f64>>>) -> Option<F<f64>> {
229 edges.iter().try_fold(F(0.0), |sum, edge_id| {
230 masses[*edge_id].as_ref().map(|mass| sum + mass.abs())
231 })
232 }
233
234 fn incoming_invariant_mass(
235 graph: &Graph,
236 runtime_settings: &RuntimeSettings,
237 ) -> Option<F<f64>> {
238 let external_momenta = runtime_settings
239 .kinematics
240 .externals
241 .get_dependent_externals(DependentMomentaConstructor::CrossSection)
242 .ok()?;
243 if external_momenta.is_empty()
244 || external_momenta.len() != graph.get_edges_in_initial_state_cut().len()
245 {
246 return None;
247 }
248
249 let total = external_momenta
250 .into_iter()
251 .reduce(|sum, momentum| sum + momentum)?;
252 let invariant_squared = total.square();
253 if invariant_squared.is_nan()
254 || invariant_squared.is_infinite()
255 || invariant_squared <= F(0.0)
256 {
257 None
258 } else {
259 Some(invariant_squared.sqrt())
260 }
261 }
262
263 fn classify_from_invariant_bounds(
264 &self,
265 cut_boundary_mass: &F<f64>,
266 threshold_boundary_mass: &F<f64>,
267 maximum_cut_invariant: Option<F<f64>>,
268 tolerance: &F<f64>,
269 ) -> ThresholdCountertermStatus {
270 if !self.invariant_bound_is_applicable
271 || cut_boundary_mass.is_nan()
272 || cut_boundary_mass.is_infinite()
273 || threshold_boundary_mass.is_nan()
274 || threshold_boundary_mass.is_infinite()
275 || tolerance.is_nan()
276 || tolerance.is_infinite()
277 || maximum_cut_invariant
278 .as_ref()
279 .is_some_and(|bound| bound.is_nan() || bound.is_infinite())
280 {
281 return ThresholdCountertermStatus::PotentiallyExisting;
282 }
283
284 let minimum_margin = cut_boundary_mass * cut_boundary_mass
285 - threshold_boundary_mass * threshold_boundary_mass;
286 let masses_match = minimum_margin.abs() <= *tolerance;
287 let threshold_minimum_is_reachable = minimum_margin <= *tolerance;
288
289 if masses_match
290 && (self.cut_boundary_edges.len() <= 1 || self.threshold_boundary_edges.len() <= 1)
291 {
292 return ThresholdCountertermStatus::AlwaysPinched;
293 }
294
295 if self.cut_boundary_edges.len() == 1 && self.threshold_boundary_edges.len() == 1 {
296 return ThresholdCountertermStatus::ProvenNonExisting;
297 }
298
299 if self.cut_boundary_edges.len() > 1 && self.threshold_boundary_edges.len() == 1 {
300 let lower_margin = threshold_boundary_mass * threshold_boundary_mass
301 - cut_boundary_mass * cut_boundary_mass;
302 if lower_margin <= *tolerance {
303 return ThresholdCountertermStatus::ProvenNonExisting;
304 }
305 }
306
307 let Some(maximum_cut_invariant) = maximum_cut_invariant else {
308 return if masses_match
309 && self.cut_boundary_edges.len() > 1
310 && self.threshold_boundary_edges.len() > 1
311 {
312 ThresholdCountertermStatus::CanBecomePinched
313 } else {
314 ThresholdCountertermStatus::PotentiallyExisting
315 };
316 };
317
318 let maximum_margin = maximum_cut_invariant * maximum_cut_invariant
319 - threshold_boundary_mass * threshold_boundary_mass;
320 if maximum_margin.abs() <= *tolerance {
321 ThresholdCountertermStatus::AlwaysPinched
322 } else if maximum_margin < -*tolerance {
323 ThresholdCountertermStatus::ProvenNonExisting
324 } else if threshold_minimum_is_reachable
325 && self.cut_boundary_edges.len() > 1
326 && self.threshold_boundary_edges.len() > 1
327 {
328 ThresholdCountertermStatus::CanBecomePinched
329 } else {
330 ThresholdCountertermStatus::PotentiallyExisting
331 }
332 }
333
334 pub fn classify_for_model(
335 &self,
336 graph: &Graph,
337 cut: &CrossSectionCut,
338 model: &Model,
339 param_builder: &ParamBuilder,
340 runtime_settings: &RuntimeSettings,
341 normalized_margin_tolerance: f64,
342 ) -> ThresholdCountertermStatus {
343 if !self.invariant_bound_is_applicable {
344 return ThresholdCountertermStatus::PotentiallyExisting;
345 }
346
347 match graph
348 .classify_threshold_pinch(&self.cut_boundary_edges, &self.threshold_boundary_edges)
349 {
350 ThresholdPinchStatus::Always => {
351 return ThresholdCountertermStatus::AlwaysPinched;
352 }
353 ThresholdPinchStatus::CanBecome | ThresholdPinchStatus::NotProven => {}
354 }
355
356 let masses = graph.new_edgevec(|edge, _, _| {
359 if matches!(edge.mass, EdgeMass::Zero) {
360 Some(F(0.0))
361 } else {
362 edge.mass_value::<f64>(model, param_builder)
363 .and_then(|mass| IsZero::is_zero(&mass.im).then_some(mass.re))
364 }
365 });
366 let Some(cut_boundary_mass) = Self::mass_sum(&self.cut_boundary_edges, &masses) else {
367 return ThresholdCountertermStatus::PotentiallyExisting;
368 };
369 let Some(threshold_boundary_mass) = Self::mass_sum(&self.threshold_boundary_edges, &masses)
370 else {
371 return ThresholdCountertermStatus::PotentiallyExisting;
372 };
373 let e_cm = F(runtime_settings.kinematics.e_cm.abs());
374 let normalized_margin_tolerance = if normalized_margin_tolerance.is_finite() {
377 normalized_margin_tolerance.abs()
378 } else {
379 DEFAULT_ESURFACE_EXISTENCE_THRESHOLD
380 };
381 let tolerance = F(normalized_margin_tolerance) * e_cm * e_cm;
382
383 let maximum_cut_invariant = if self.cut_boundary_edges.len() == 1 {
384 Some(cut_boundary_mass)
385 } else {
386 let incoming_invariant = Self::incoming_invariant_mass(graph, runtime_settings);
387 let complement_mass = graph
388 .iter_edges_of(&cut.cut)
389 .map(|(_, edge_id, _)| edge_id)
390 .filter(|edge_id| !self.cut_boundary_edges.contains(edge_id))
391 .try_fold(F(0.0), |sum, edge_id| {
392 masses[edge_id].as_ref().map(|mass| sum + mass.abs())
393 });
394 incoming_invariant
395 .zip(complement_mass)
396 .and_then(|(invariant, complement_mass)| {
397 let maximum = invariant - complement_mass;
398 (maximum > F(0.0)).then_some(maximum)
399 })
400 };
401 self.classify_from_invariant_bounds(
402 &cut_boundary_mass,
403 &threshold_boundary_mass,
404 maximum_cut_invariant,
405 &tolerance,
406 )
407 }
408}
409
410#[derive(Clone)]
411struct TopologicalThresholdCandidate {
412 left: SuBitGraph,
413 cut: OrientedCut,
414 right: SuBitGraph,
415 esurface_id: EsurfaceID,
416}
417
418use derive_more::{From, Into};
419#[derive(Clone, Encode, Decode)]
420#[trait_decode(trait = GammaLoopContext)]
421pub struct CrossSection {
422 pub name: String,
423 pub integrand: Option<ProcessIntegrand>,
424 pub supergraphs: Vec<CrossSectionGraph>,
425 pub external_particles: Vec<ArcParticle>,
426 pub external_connections: Vec<ExternalConnection>,
427 pub n_incmoming: usize,
428 pub graph_group_structure: TiVec<GroupId, GraphGroup>,
429}
430
431impl CrossSection {
432 pub fn plan_graph_group_selection(
433 &self,
434 spec: &GraphGroupSelectionSpec,
435 ) -> Result<GraphGroupSelectionPlan> {
436 if spec.mode() == GraphGroupSelectionMode::CrossSectionAmplitudeGraphs {
437 return Err(eyre!(
438 "`select --amplitude-graphs` for cross sections requires process and generation settings context."
439 ));
440 }
441 if spec.has_raised_cut_rules() {
442 return Err(eyre!(
443 "Raised-cut signature selection for cross sections requires process and generation settings context."
444 ));
445 }
446 spec.plan(&self.graph_group_structure, |graph_id| {
447 self.supergraphs.get(graph_id).map(|graph| &graph.graph)
448 })
449 }
450
451 pub fn plan_graph_group_selection_with_context(
452 &self,
453 spec: &GraphGroupSelectionSpec,
454 model: &Model,
455 process_definition: &ProcessDefinition,
456 generation_settings: &GenerationSettings,
457 ) -> Result<GraphGroupSelectionPlan> {
458 match spec.mode() {
459 GraphGroupSelectionMode::MasterGraphs => {
460 spec.plan_with_analysis_contexts(
461 &self.graph_group_structure,
462 |graph_id| self.supergraphs.get(graph_id).map(|graph| &graph.graph),
463 |_master_graph_id, master_graph| {
464 Ok(vec![GraphSelectionSubject::whole_graph(master_graph)])
465 },
466 |master_graph_id, master_graph| {
467 self.cut_selection_subjects(
468 master_graph_id,
469 master_graph,
470 model,
471 process_definition,
472 generation_settings,
473 )
474 },
475 "Graph-group selection structural filters have no graph analysis subjects.",
476 "Raised-cut signature selection found no process-valid Cutkosky cuts to analyse.",
477 )
478 }
479 GraphGroupSelectionMode::CrossSectionAmplitudeGraphs => {
480 spec.plan_with_analysis_contexts(
481 &self.graph_group_structure,
482 |graph_id| self.supergraphs.get(graph_id).map(|graph| &graph.graph),
483 |master_graph_id, master_graph| {
484 let cross_section_graph =
485 self.supergraphs.get(master_graph_id).ok_or_else(|| {
486 eyre!(
487 "Graph group refers to missing master supergraph id {}.",
488 master_graph_id
489 )
490 })?;
491 let cuts = cross_section_graph.process_valid_cuts(
492 model,
493 process_definition,
494 generation_settings,
495 )?;
496 Ok(cuts
497 .into_iter()
498 .flat_map(|cut| cut.amplitude_side_subjects(master_graph))
499 .collect())
500 },
501 |master_graph_id, master_graph| {
502 self.cut_selection_subjects(
503 master_graph_id,
504 master_graph,
505 model,
506 process_definition,
507 generation_settings,
508 )
509 },
510 "`select --amplitude-graphs` found no process-valid Cutkosky cuts to analyse for structural filters.",
511 "Raised-cut signature selection found no process-valid Cutkosky cuts to analyse.",
512 )
513 }
514 }
515 }
516
517 fn cut_selection_subjects<'a>(
518 &'a self,
519 master_graph_id: usize,
520 master_graph: &'a Graph,
521 model: &Model,
522 process_definition: &ProcessDefinition,
523 generation_settings: &GenerationSettings,
524 ) -> Result<Vec<GraphCutSelectionSubject<'a>>> {
525 let cross_section_graph = self.supergraphs.get(master_graph_id).ok_or_else(|| {
526 eyre!(
527 "Graph group refers to missing master supergraph id {}.",
528 master_graph_id
529 )
530 })?;
531 let cuts = cross_section_graph.process_valid_cuts(
532 model,
533 process_definition,
534 generation_settings,
535 )?;
536 Ok(cuts
537 .into_iter()
538 .map(|cut| GraphCutSelectionSubject::new(master_graph, cut.cut.as_subgraph()))
539 .collect())
540 }
541
542 pub fn amplitude_graph_signature_inventory(
543 &self,
544 model: &Model,
545 process_definition: &ProcessDefinition,
546 generation_settings: &GenerationSettings,
547 ) -> Result<GraphSelectionSignatureInventory> {
548 let subjects = self
549 .graph_group_structure
550 .iter()
551 .flat_map(|group| group.into_iter().next())
552 .map(|master_graph_id| {
553 let cross_section_graph =
554 self.supergraphs.get(master_graph_id).ok_or_else(|| {
555 eyre!(
556 "Graph group refers to missing master supergraph id {}.",
557 master_graph_id
558 )
559 })?;
560 let cuts = cross_section_graph.process_valid_cuts(
561 model,
562 process_definition,
563 generation_settings,
564 )?;
565 Ok(cuts
566 .into_iter()
567 .flat_map(|cut| cut.amplitude_side_subjects(&cross_section_graph.graph))
568 .collect::<Vec<_>>())
569 })
570 .collect::<Result<Vec<_>>>()?
571 .into_iter()
572 .flatten()
573 .collect::<Vec<_>>();
574
575 Ok(GraphSelectionSignatureInventory::from_analysis_subjects(
576 subjects,
577 ))
578 }
579
580 pub fn raised_cut_signature_inventory(
581 &self,
582 model: &Model,
583 process_definition: &ProcessDefinition,
584 generation_settings: &GenerationSettings,
585 ) -> Result<RaisedCutSignatureInventory> {
586 let subjects = self
587 .graph_group_structure
588 .iter()
589 .flat_map(|group| group.into_iter().next())
590 .map(|master_graph_id| {
591 let cross_section_graph =
592 self.supergraphs.get(master_graph_id).ok_or_else(|| {
593 eyre!(
594 "Graph group refers to missing master supergraph id {}.",
595 master_graph_id
596 )
597 })?;
598 let cuts = cross_section_graph.process_valid_cuts(
599 model,
600 process_definition,
601 generation_settings,
602 )?;
603 Ok(cuts
604 .into_iter()
605 .map(|cut| {
606 GraphCutSelectionSubject::new(
607 &cross_section_graph.graph,
608 cut.cut.as_subgraph(),
609 )
610 })
611 .collect::<Vec<_>>())
612 })
613 .collect::<Result<Vec<_>>>()?
614 .into_iter()
615 .flatten()
616 .collect::<Vec<_>>();
617
618 Ok(RaisedCutSignatureInventory::from_cut_subjects(subjects))
619 }
620
621 pub fn validate_graph_group_selection_plan(
622 &self,
623 plan: &GraphGroupSelectionPlan,
624 ) -> Result<()> {
625 for &old_group_id in plan.retained_group_ids() {
626 plan.new_group_id_for_old(old_group_id).ok_or_else(|| {
627 eyre!(
628 "Selection plan is missing compact group id for old group {}.",
629 old_group_id.0
630 )
631 })?;
632 if old_group_id.0 >= self.graph_group_structure.len() {
633 return Err(eyre!(
634 "Selection plan refers to missing graph group {}.",
635 old_group_id.0
636 ));
637 }
638 let group = &self.graph_group_structure[old_group_id];
639 for old_graph_id in group {
640 if old_graph_id >= self.supergraphs.len() {
641 return Err(eyre!(
642 "Graph group {} refers to missing graph id {}.",
643 old_group_id.0,
644 old_graph_id
645 ));
646 }
647 }
648 let master = group.master();
649 if master >= self.supergraphs.len() {
650 return Err(eyre!(
651 "Graph group {} refers to missing master graph id {}.",
652 old_group_id.0,
653 master
654 ));
655 }
656 }
657
658 Ok(())
659 }
660
661 pub fn apply_graph_group_selection(&mut self, plan: &GraphGroupSelectionPlan) -> Result<()> {
662 self.validate_graph_group_selection_plan(plan)?;
663
664 let mut old_graph_to_new_group = vec![None; self.supergraphs.len()];
665 let mut old_graph_is_master = vec![false; self.supergraphs.len()];
666 for &old_group_id in plan.retained_group_ids() {
667 let new_group_id = plan.new_group_id_for_old(old_group_id).ok_or_else(|| {
668 eyre!(
669 "Selection plan is missing compact group id for old group {}.",
670 old_group_id.0
671 )
672 })?;
673 let group = &self.graph_group_structure[old_group_id];
674 for old_graph_id in group {
675 old_graph_to_new_group[old_graph_id] = Some(new_group_id);
676 }
677 old_graph_is_master[group.master()] = true;
678 }
679
680 let mut new_supergraphs = self
681 .supergraphs
682 .iter()
683 .cloned()
684 .enumerate()
685 .filter_map(|(old_graph_id, mut graph)| {
686 let new_group_id = old_graph_to_new_group[old_graph_id]?;
687 graph.graph.group_id = Some(new_group_id);
688 graph.graph.is_group_master = old_graph_is_master[old_graph_id];
689 if let Some(multi_channeling_setup) = &mut graph.derived_data.multi_channeling_setup
690 {
691 multi_channeling_setup.graph.group_id = Some(new_group_id);
692 multi_channeling_setup.graph.is_group_master =
693 old_graph_is_master[old_graph_id];
694 }
695 Some(graph)
696 })
697 .collect::<Vec<_>>();
698
699 let mut parsed_graphs = new_supergraphs
700 .iter()
701 .map(|graph| graph.graph.clone())
702 .collect::<Vec<_>>();
703 let new_graph_group_structure = complete_group_parsing(&mut parsed_graphs)?;
704 for (graph, parsed_graph) in new_supergraphs.iter_mut().zip(parsed_graphs) {
705 graph.graph.group_id = parsed_graph.group_id;
706 graph.graph.is_group_master = parsed_graph.is_group_master;
707 if let Some(multi_channeling_setup) = &mut graph.derived_data.multi_channeling_setup {
708 multi_channeling_setup.graph.group_id = graph.graph.group_id;
709 multi_channeling_setup.graph.is_group_master = graph.graph.is_group_master;
710 }
711 }
712
713 self.supergraphs = new_supergraphs;
714 self.graph_group_structure = new_graph_group_structure;
715 self.integrand = None;
716 Ok(())
717 }
718
719 fn storage_path(&self, base: &Path) -> PathBuf {
720 base.join(&self.name)
721 }
722
723 pub fn export_standalone(
724 &self,
725 path: impl AsRef<Path>,
726 settings: &crate::processes::StandaloneExportSettings,
727 ) -> Result<()> {
728 if let Some(integrand) = &self.integrand {
729 integrand.export_standalone(path, settings)?;
730 } else {
731 return Err(eyre!(
732 "Cannot export standalone cross section {} without integrand",
733 self.name
734 ));
735 }
736
737 Ok(())
738 }
739
740 #[allow(dead_code)]
741 pub(crate) fn write_dot<W: std::io::Write>(
742 &self,
743 writer: &mut W,
744 settings: &DotExportSettings,
745 ) -> Result<(), std::io::Error> {
746 for graph in &self.supergraphs {
747 graph.write_dot(writer, settings)?;
748 writeln!(writer)?;
749 }
750 Ok(())
751 }
752
753 pub fn write_dot_fmt<W: std::fmt::Write>(
754 &self,
755 writer: &mut W,
756 settings: &DotExportSettings,
757 ) -> Result<(), std::fmt::Error> {
758 for graph in &self.supergraphs {
759 graph.write_dot_fmt(writer, settings)?;
760 writeln!(writer)?;
761 }
762 Ok(())
763 }
764
765 pub(crate) fn new(name: String) -> Self {
766 Self {
767 name,
768 integrand: None,
769 supergraphs: vec![],
770 external_connections: vec![],
771 external_particles: vec![],
772 n_incmoming: 0,
773 graph_group_structure: TiVec::new(),
774 }
775 }
776
777 pub fn from_graph_list(name: String, mut graphs: Vec<Graph>, _model: &Model) -> Result<Self> {
778 let mut cross_section = CrossSection::new(name);
779 cross_section.graph_group_structure = complete_group_parsing(&mut graphs)?;
780 for cross_section_graph in graphs {
783 cross_section.add_supergraph(cross_section_graph)?;
785 }
786 Ok(cross_section)
787 }
788
789 pub(crate) fn warm_up(&mut self, model: &Model) -> Result<()> {
790 if let Some(integrand) = &mut self.integrand {
791 integrand.warm_up(model)
792 } else {
793 Err(eyre!(
794 "Cannot warm up amplitude {} without integrand",
795 self.name
796 ))
797 }
798 }
799
800 fn add_supergraph(&mut self, supergraph: Graph) -> Result<()> {
801 if self.external_particles.is_empty() {
802 let external_particles = supergraph.get_external_partcles();
803 if !external_particles.len().is_multiple_of(2) {
804 return Err(eyre!(
805 "expected even number of externals for forward scattering graph"
806 ));
807 }
808 self.external_particles = external_particles;
809 self.n_incmoming = self.external_particles.len() / 2;
810 } else if self.external_particles != supergraph.get_external_partcles() {
811 return Err(eyre!(
812 "attempt to add supergraph with differnt external particles"
813 ));
814 }
815
816 let cross_section_graph = CrossSectionGraph::new(supergraph);
817 self.supergraphs.push(cross_section_graph);
818
819 Ok(())
821 }
822
823 pub fn preprocess(
824 &mut self,
825 model: &Model,
826 process_definition: &ProcessDefinition,
827 global_settings: &GenerationSettings,
828 runtime_default: LockedRuntimeSettings,
829 generation_pool: &ThreadPool,
830 ) -> Result<Vec<NamedGraphGenerationReport>> {
831 let integrand_name = self.name.clone();
832 generation_progress::begin_phase(
833 GenerationProgressPhase::GraphPreprocessing,
834 GenerationProcessKind::CrossSection,
835 &process_definition.folder_name,
836 &integrand_name,
837 self.supergraphs.len(),
838 None,
839 );
840 generation_pool.install(|| {
841 self.supergraphs
842 .par_iter_mut()
843 .map(|supergraph| {
844 if crate::is_interrupted() {
845 return Err(eyre!("Generation interrupted by user"));
846 }
847 let graph_name = supergraph.graph.name.clone();
848 generation_progress::graph_started(
849 GenerationProcessKind::CrossSection,
850 &integrand_name,
851 &graph_name,
852 None,
853 );
854 let stats = supergraph.preprocess(
855 model,
856 process_definition,
857 global_settings,
858 runtime_default,
859 )?;
860 if crate::is_interrupted() {
861 return Err(eyre!("Generation interrupted by user"));
862 }
863 generation_progress::graph_finished(
864 GenerationProcessKind::CrossSection,
865 &integrand_name,
866 &graph_name,
867 &stats,
868 None,
869 );
870 Ok(NamedGraphGenerationReport {
871 integrand_name: integrand_name.clone(),
872 graph_name,
873 stats,
874 })
875 })
876 .collect::<Result<Vec<_>>>()
877 })
878 }
879
880 pub fn build_integrand(
881 &mut self,
882 model: &Model,
883 process_name: &str,
884 global_settings: &GlobalSettings,
885 runtime_default: LockedRuntimeSettings,
886 generation_pool: &ThreadPool,
887 ) -> Result<Vec<NamedGraphGenerationReport>> {
888 let started = std::time::Instant::now();
889 crate::debug_tags!(#generation, #profile, #graph, #summary;
890 stage = "cross_section_build_integrand_start",
891 integrand = %self.name,
892 graph_count = self.supergraphs.len(),
893 "Generation timing milestone"
894 );
895 if crate::is_interrupted() {
896 return Err(eyre!("Generation interrupted by user"));
897 }
898 let integrand_name = self.name.clone();
899 let total_cuts = self
900 .supergraphs
901 .iter()
902 .map(|sg| {
903 sg.derived_data
904 .cut_group_data
905 .cut_groups
906 .iter()
907 .map(|cut_group| cut_group.cuts.len())
908 .sum::<usize>()
909 })
910 .sum();
911 generation_progress::begin_phase(
912 GenerationProgressPhase::GraphGeneration,
913 GenerationProcessKind::CrossSection,
914 process_name,
915 &integrand_name,
916 self.supergraphs.len(),
917 Some(total_cuts),
918 );
919 let mut graph_reports = Vec::new();
920 let terms = generation_pool.install(|| {
921 self.supergraphs
922 .par_iter_mut()
923 .map(|sg| {
924 if crate::is_interrupted() {
925 return Err(eyre!("Generation interrupted by user"));
926 }
927 let graph_started = std::time::Instant::now();
928 crate::debug_tags!(#generation, #profile, #graph, #summary;
929 stage = "generate_term_for_graph_start",
930 integrand = %integrand_name,
931 graph = %sg.graph.name,
932 "Generation timing milestone"
933 );
934 generation_progress::graph_started(
935 GenerationProcessKind::CrossSection,
936 &integrand_name,
937 &sg.graph.name,
938 Some(sg.cuts.len()),
939 );
940 let _progress_context_guard = generation_progress::enter_progress_context(
941 format!("{} / {} cuts", sg.graph.name, sg.cuts.len()),
942 );
943 let (term, mut stats) = sg.generate_term_for_graph(model, global_settings)?;
944 if crate::is_interrupted() {
945 return Err(eyre!("Generation interrupted by user"));
946 }
947 stats.total_time += graph_started.elapsed();
948 crate::debug_tags!(#generation, #profile, #graph, #summary;
949 stage = "generate_term_for_graph_done",
950 integrand = %integrand_name,
951 graph = %sg.graph.name,
952 elapsed_ms = graph_started.elapsed().as_secs_f64() * 1000.0,
953 "Generation timing milestone"
954 );
955 generation_progress::graph_finished(
956 GenerationProcessKind::CrossSection,
957 &integrand_name,
958 &sg.graph.name,
959 &stats,
960 None,
961 );
962 Ok((
963 term,
964 NamedGraphGenerationReport {
965 integrand_name: integrand_name.clone(),
966 graph_name: sg.graph.name.clone(),
967 stats,
968 },
969 ))
970 })
971 .collect::<Result<Vec<_>>>()
972 })?;
973 if crate::is_interrupted() {
974 return Err(eyre!("Generation interrupted by user"));
975 }
976 for (_, report) in &terms {
977 graph_reports.push(report.clone());
978 }
979 let mut terms = terms.into_iter().map(|(term, _)| term).collect::<Vec<_>>();
980
981 for group in self.graph_group_structure.iter() {
982 let master = group.master();
983 let mc_of_master = self.supergraphs[master]
984 .derived_data
985 .multi_channeling_setup
986 .as_ref()
987 .unwrap();
988
989 for graph_id in group.into_iter() {
990 terms[graph_id].multi_channeling_setup = mc_of_master.clone();
991 }
992 }
993
994 let backend_started = std::time::Instant::now();
995 let graph_count = terms.len();
996 crate::debug_tags!(#generation, #profile, #compile, #graph, #summary;
997 stage = "prepare_runtime_backends_start",
998 integrand = %self.name,
999 graph_count,
1000 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1001 "Generation timing milestone"
1002 );
1003 generation_progress::backend_started(
1004 GenerationProcessKind::CrossSection,
1005 &self.name,
1006 graph_count,
1007 );
1008 let mut cross_section_integrand = CrossSectionIntegrand {
1009 settings: runtime_default.into(),
1010 data: CrossSectionIntegrandData {
1011 compilation: global_settings
1012 .generation
1013 .compile
1014 .frozen_mode(&global_settings.generation.evaluator),
1015 loop_cache_id: 0,
1016 external_cache_id: 0,
1017 base_external_cache_id: 0,
1018 rotations: None,
1019 name: self.name.clone(),
1020 external_connections: self.external_connections.clone(),
1021 n_incoming: self.n_incmoming,
1022 graph_terms: terms,
1024 graph_group_structure: self.graph_group_structure.clone(),
1025 graph_to_group_id: graph_to_group_id_for_group_structure(
1026 &self.graph_group_structure,
1027 ),
1028 },
1029 event_processing_runtime: Default::default(),
1030 active_f64_backend: Default::default(),
1031 };
1032 let compile_times = cross_section_integrand
1033 .prepare_runtime_backends_after_generation_with_compile_times()?;
1034 crate::debug_tags!(#generation, #profile, #compile, #graph, #summary;
1035 stage = "prepare_runtime_backends_done",
1036 integrand = %self.name,
1037 graph_count,
1038 elapsed_ms = backend_started.elapsed().as_secs_f64() * 1000.0,
1039 total_elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1040 "Generation timing milestone"
1041 );
1042 generation_progress::backend_finished(
1043 GenerationProcessKind::CrossSection,
1044 &self.name,
1045 backend_started.elapsed(),
1046 );
1047 for (report, compile_time) in graph_reports.iter_mut().zip(compile_times) {
1048 report.stats.evaluator_compile_time += compile_time;
1049 report.stats.total_time += compile_time;
1050 }
1051
1052 self.integrand = Some(ProcessIntegrand::CrossSection(cross_section_integrand));
1053 crate::debug_tags!(#generation, #profile, #graph, #summary;
1054 stage = "cross_section_build_integrand_done",
1055 integrand = %self.name,
1056 graph_count = self.supergraphs.len(),
1057 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1058 "Generation timing milestone"
1059 );
1060 Ok(graph_reports)
1061 }
1062
1063 pub fn compile(
1064 &mut self,
1065 path: impl AsRef<Path>,
1066 override_existing: bool,
1067 thread_pool: &ThreadPool,
1068 ) -> Result<Vec<NamedGraphGenerationReport>> {
1069 info!("Compiling cross section {}", self.name);
1070 let p = self.storage_path(path.as_ref());
1071
1072 let result = fs::create_dir_all(&p).with_context(|| {
1073 format!(
1074 "Trying to create directory to compile cross section {}",
1075 p.display()
1076 )
1077 });
1078
1079 if override_existing {
1080 result?;
1081 }
1082
1083 if let Some(integrand) = &mut self.integrand {
1084 let compile_times = integrand.compile(&p, override_existing, thread_pool)?;
1085 return Ok(compile_times
1086 .into_iter()
1087 .map(|(graph_name, duration)| NamedGraphGenerationReport {
1088 integrand_name: self.name.clone(),
1089 graph_name,
1090 stats: GraphGenerationStats {
1091 total_time: duration,
1092 evaluator_compile_time: duration,
1093 ..GraphGenerationStats::default()
1094 },
1095 })
1096 .collect());
1097 }
1098 Ok(Vec::new())
1099 }
1100
1101 pub fn save(&mut self, path: impl AsRef<Path>, override_existing: bool) -> Result<()> {
1102 let p = self.storage_path(path.as_ref());
1103 let r = fs::create_dir_all(&p).with_context(|| {
1104 format!(
1105 "Trying to create directory to save cross section {}",
1106 p.display()
1107 )
1108 });
1109
1110 if override_existing {
1111 r?;
1112 }
1113
1114 let integrand = self.integrand.take();
1115 if let Some(integrand) = &integrand {
1116 integrand.save(&p, override_existing)?;
1117 }
1118
1119 let binary = bincode::encode_to_vec(&(*self), bincode::config::standard())?;
1120 if override_existing {
1121 fs::write(p.join("cs.bin"), &binary)?;
1122 } else {
1123 let mut file = File::create_new(p.join("cs.bin"))?;
1124 file.write_all(&binary)?;
1125 }
1126
1127 self.integrand = integrand;
1128 Ok(())
1129 }
1130
1131 pub(crate) fn load(path: impl AsRef<Path>, context: GammaLoopContextContainer) -> Result<Self> {
1132 let binary = fs::read(path.as_ref().join("cs.bin"))?;
1133 let (mut cs, _): (Self, _) =
1134 bincode::decode_from_slice_with_context(&binary, bincode::config::standard(), context)?;
1135
1136 if path.as_ref().join("integrand").exists() {
1137 let integrand = CrossSectionIntegrand::load(path.as_ref().join("integrand"), context)?;
1138 cs.integrand = Some(ProcessIntegrand::CrossSection(integrand));
1139 }
1140
1141 Ok(cs)
1142 }
1143}
1144
1145#[derive(Clone, bincode::Encode, bincode::Decode)]
1146pub struct CrossSectionCut {
1147 pub cut: OrientedCut,
1148 pub left: SuBitGraph,
1149 pub right: SuBitGraph,
1150}
1151
1152impl CrossSectionCut {
1153 fn amplitude_side_subjects<'a>(&self, graph: &'a Graph) -> [GraphSelectionSubject<'a>; 2] {
1154 let cut_edges = self.cut.as_subgraph();
1155 [
1156 GraphSelectionSubject::cut_side_amplitude_subgraph(
1157 graph,
1158 self.left.subtract(&cut_edges),
1159 ),
1160 GraphSelectionSubject::cut_side_amplitude_subgraph(
1161 graph,
1162 self.right.subtract(&cut_edges),
1163 ),
1164 ]
1165 }
1166
1167 pub(crate) fn is_s_channel(&self, cross_section_graph: &CrossSectionGraph) -> Result<bool> {
1168 let nodes_of_left_cut: Vec<_> = cross_section_graph
1169 .graph
1170 .underlying
1171 .iter_nodes_of(&self.left)
1172 .map(|(nid, _, _)| nid)
1173 .collect();
1174
1175 let left_node = cross_section_graph
1176 .graph
1177 .underlying
1178 .combine_to_single_hedgenode(&nodes_of_left_cut);
1179 let res = left_node.includes(&cross_section_graph.source_nodes)
1180 && cross_section_graph.target_nodes.weakly_disjoint(&left_node);
1181
1182 if !res {
1183 warn!("s channel check wrong");
1184 }
1185
1186 Ok(true)
1187 }
1188
1189 pub(crate) fn is_valid_for_process(
1190 &self,
1191 cross_section_graph: &CrossSectionGraph,
1192 process: &ProcessDefinition,
1193 model: &Model,
1194 ) -> Result<bool> {
1195 if self.is_s_channel(cross_section_graph)? {
1196 let cut_content_builder = self
1197 .cut
1198 .iter_edges(&cross_section_graph.graph.underlying)
1199 .filter_map(|(orientation, edge_data)| {
1200 Some(if orientation == Orientation::Reversed {
1201 edge_data.data.particle()?.get_anti_particle(model)
1202 } else {
1203 edge_data.data.particle()?.clone()
1204 })
1205 })
1206 .collect_vec();
1207
1208 let any_pdg_list_passes = process
1209 .final_pdgs_lists
1210 .iter()
1211 .map(|x| {
1212 x.iter()
1213 .map(|pdg| model.get_particle_from_pdg(*pdg as isize))
1214 })
1215 .any(|particle_content| {
1216 let mut cut_content = cut_content_builder.clone();
1217 debug!(
1218 "cut content: {:?}",
1219 cut_content.iter().map(|p| p.name.clone()).collect_vec()
1220 );
1221
1222 for particle in particle_content {
1223 if let Some(index) = cut_content.iter().position(|p| p == &particle) {
1224 cut_content.remove(index);
1225 } else {
1226 debug!("wrong particles");
1227 return false;
1228 }
1229 }
1230
1231 let (n_unresolved, unresolved_cut_content) =
1232 process.unresolved_cut_content(model);
1233
1234 if cut_content.len() > n_unresolved {
1235 debug!(" too many unresolved particles");
1236 return false;
1237 }
1238
1239 if !cut_content
1240 .iter()
1241 .all(|particle| unresolved_cut_content.contains(particle))
1242 {
1243 debug!("wrong unresolved particles");
1244 return false;
1245 }
1246
1247 true
1248 });
1249
1250 if !any_pdg_list_passes {
1251 debug!("wrong pdg list");
1252 return Ok(false);
1253 }
1254
1255 let amplitude_couplings = process.amplitude_filters.get_coupling_orders();
1256 let amplitude_loop_count = process.amplitude_filters.get_loop_count_range();
1257
1258 if let Some((min_loop, max_loop)) = amplitude_loop_count {
1259 let loop_range = min_loop..=max_loop;
1260 let left_internal_subgraph = InternalSubGraph::cleaned_filter_pessimist(
1261 self.left.clone(),
1262 &cross_section_graph.graph.underlying,
1263 );
1264
1265 let right_internal_subgraph = InternalSubGraph::cleaned_filter_pessimist(
1266 self.right.clone(),
1267 &cross_section_graph.graph.underlying,
1268 );
1269
1270 let left_loop = cross_section_graph
1271 .graph
1272 .underlying
1273 .cyclotomatic_number(&left_internal_subgraph);
1274
1275 let right_loop = cross_section_graph
1276 .graph
1277 .underlying
1278 .cyclotomatic_number(&right_internal_subgraph);
1279
1280 let total_loops = left_loop + right_loop;
1281
1282 if !loop_range.contains(&total_loops) {
1283 debug!("incorrect loop count");
1284 return Ok(false);
1285 }
1286 }
1287
1288 if amplitude_couplings.is_some() {
1289 todo!("waiting for update")
1290 }
1291
1292 Ok(true)
1293 } else {
1294 debug!("cut is not s channel");
1295 Ok(false)
1296 }
1297 }
1298}
1299
1300#[derive(
1301 Debug, Clone, Serialize, Decode, Deserialize, From, Into, Hash, PartialEq, Copy, Eq, Encode,
1302)]
1303pub struct CutId(pub usize);
1304
1305impl Display for CutId {
1306 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1307 write!(f, "{}", self.0)
1308 }
1309}
1310
1311#[derive(Clone, Encode, Decode)]
1312#[trait_decode(trait = GammaLoopContext)]
1313pub struct CrossSectionGraph {
1314 pub graph: Graph,
1315 pub source_nodes: HedgeNode,
1316 pub target_nodes: HedgeNode,
1317 pub cuts: TiVec<CutId, CrossSectionCut>,
1318 pub cut_esurface: TiVec<CutId, Esurface>,
1319 pub cut_esurface_id_map: TiVec<CutId, EsurfaceID>,
1320 pub derived_data: CrossSectionDerivedData,
1321}
1322
1323impl CrossSectionGraph {
1324 pub(crate) fn new(graph: Graph) -> Self {
1325 let (source_node, target_node) = graph.get_source_and_target();
1326
1327 Self {
1328 graph,
1329 source_nodes: source_node,
1330 target_nodes: target_node,
1331 cuts: TiVec::new(),
1332 cut_esurface: TiVec::new(),
1333 cut_esurface_id_map: TiVec::new(),
1334 derived_data: CrossSectionDerivedData::new_empty(),
1335 }
1336 }
1337
1338 pub(crate) fn apply_spin_sum(
1339 &mut self,
1340 model: &Model,
1341 generation_settings: &GenerationSettings,
1342 locked_runtime_settings: &LockedRuntimeSettings,
1343 ) -> Result<()> {
1344 for (extid, hel) in locked_runtime_settings.helicities().iter().enumerate() {
1345 let eid = self.graph.loop_momentum_basis.ext_edges[ExternalIndex(extid)];
1347
1348 let Some(p) = self.graph.underlying[eid].particle() else {
1349 continue;
1350 };
1351
1352 match hel {
1353 Helicity::Summed => {
1354 let Some(p) = p.polarization_sum(
1355 eid,
1356 false,
1357 generation_settings.vector_polarization_sum_gauge,
1358 )?
1359 else {
1360 continue;
1361 };
1362 self.graph.global_prefactor.projector =
1363 self.graph.global_prefactor.projector.replace_multiple(&[p]);
1364 }
1365 Helicity::SummedAveraged => {
1366 let Some(p) = p.polarization_sum(
1367 eid,
1368 true,
1369 generation_settings.vector_polarization_sum_gauge,
1370 )?
1371 else {
1372 continue;
1373 };
1374 self.graph.global_prefactor.projector =
1375 self.graph.global_prefactor.projector.replace_multiple(&[p]);
1376 }
1377 _ => {}
1378 }
1379 }
1380
1381 self.graph.polarizations = self.graph.global_prefactor.polarizations();
1382 self.graph.param_builder = ParamBuilder::new(
1383 &self.graph,
1384 model,
1385 &self.graph.loop_momentum_basis,
1386 &self.graph.param_builder.pairs.additional_params.params,
1387 );
1388 Ok(())
1389 }
1390
1391 pub(crate) fn preprocess(
1392 &mut self,
1393 model: &Model,
1394 process_definition: &ProcessDefinition,
1395 settings: &GenerationSettings,
1396 runtime_default: LockedRuntimeSettings,
1397 ) -> Result<GraphGenerationStats> {
1398 let preprocess_started = std::time::Instant::now();
1399 let mut stats = GraphGenerationStats::default();
1400 self.apply_spin_sum(model, settings, &runtime_default)?;
1401 debug_tags!(#generation; "generating cuts");
1402 self.generate_cuts(model, process_definition, settings)?;
1403 debug_tags!(#generation; "generating esurfaces corresponding to cuts");
1404 self.generate_esurface_cuts();
1405 debug_tags!(#generation; "generating cff");
1406 stats.merge_in_place(&self.generate_cff(settings)?);
1407 debug_tags!(#generation; "building lmbs");
1408 self.build_lmbs()?;
1409 debug_tags!(#generation; "building multi channeling channels");
1410
1411 if self.graph.is_group_master {
1412 self.build_multi_channeling_channels(settings.override_lmb_heuristics)?;
1413 }
1414
1415 let vk = crate::utils::vakint()?;
1416 debug_tags!(#generation; "building parametric integrand");
1417 self.build_parametric_integrand(settings, vk)?;
1418 let threshold_candidates = self.topological_threshold_candidates()?;
1421 self.derived_data.threshold_candidate_esurface_ids = threshold_candidates
1422 .iter()
1423 .map(|candidate| candidate.esurface_id)
1424 .sorted()
1425 .dedup()
1426 .collect();
1427 self.derived_data.cut_threshold_associations =
1428 ti_vec![CutThresholdCountertermAssociations::default(); self.cuts.len()];
1429
1430 if settings.threshold_subtraction.enable_thresholds {
1431 debug_tags!(#generation, #subtraction; "building threshold counterterm");
1432 self.build_subspace_data()?;
1433 let runtime_settings: RuntimeSettings = runtime_default.into();
1434 self.build_threshold_counterterm(
1435 model,
1436 settings,
1437 &runtime_settings,
1438 &threshold_candidates,
1439 vk,
1440 )?;
1441 }
1442
1443 stats.total_time += preprocess_started.elapsed();
1444 Ok(stats)
1445 }
1446
1447 #[allow(dead_code)]
1448 pub(crate) fn write_dot<W: std::io::Write>(
1449 &self,
1450 writer: &mut W,
1451 settings: &DotExportSettings,
1452 ) -> Result<(), std::io::Error> {
1453 self.graph.dot_serialize_io(writer, settings)
1454 }
1455
1456 pub(crate) fn write_dot_fmt<W: std::fmt::Write>(
1457 &self,
1458 writer: &mut W,
1459 settings: &DotExportSettings,
1460 ) -> Result<(), std::fmt::Error> {
1461 self.graph.dot_serialize_fmt(writer, settings)
1462 }
1463
1464 fn generate_cff(&mut self, settings: &GenerationSettings) -> Result<GraphGenerationStats> {
1465 let canonize_esurface = self
1466 .graph
1467 .get_esurface_canonization(&self.graph.loop_momentum_basis);
1468
1469 let contract_edges = self
1470 .graph
1471 .iter_edges_of(
1472 &self
1473 .graph
1474 .tree_edges
1475 .subtract(&self.graph.initial_state_cut),
1476 )
1477 .map(|x| x.1)
1478 .collect_vec();
1479
1480 let global_cff = self.graph.generate_cff(
1481 &contract_edges,
1482 &canonize_esurface,
1483 &settings.orientation_pattern,
1484 )?;
1485
1486 let cut_esurface_map = self
1487 .cut_esurface
1488 .iter()
1489 .map(|esurface| {
1490 if let Some(pos) = self
1491 .graph
1492 .surface_cache
1493 .esurface_cache
1494 .iter()
1495 .position(|e_sf| e_sf == esurface)
1496 .map(Into::<EsurfaceID>::into)
1497 {
1498 pos
1499 } else {
1500 let pos = self.graph.surface_cache.esurface_cache.len();
1501 self.graph
1502 .surface_cache
1503 .esurface_cache
1504 .push(esurface.clone());
1505 EsurfaceID(pos)
1506 }
1507 })
1508 .collect();
1509
1510 self.cut_esurface_id_map = cut_esurface_map;
1511
1512 let esurface_raised_data = self
1513 .graph
1514 .determine_raised_esurfaces_from_expression(&global_cff);
1515
1516 let (cut_group_data, cut_group_stats) = CutGroupData::new_from_esurface(
1517 &esurface_raised_data,
1518 &self.cut_esurface_id_map,
1519 &settings.evaluator,
1520 );
1521
1522 self.derived_data.global_cff_expression = Some(global_cff);
1523 self.derived_data.cut_group_data = cut_group_data;
1524
1525 Ok(cut_group_stats)
1526 }
1527
1528 pub(crate) fn process_valid_cuts(
1529 &self,
1530 model: &Model,
1531 process_definition: &ProcessDefinition,
1532 settings: &GenerationSettings,
1533 ) -> Result<TiVec<CutId, CrossSectionCut>> {
1534 if !self.cuts.is_empty() {
1535 return Ok(self.cuts.clone());
1536 }
1537 self.compute_process_valid_cuts(model, process_definition, settings)
1538 .map(|(_, cuts)| cuts)
1539 }
1540
1541 fn compute_process_valid_cuts(
1542 &self,
1543 model: &Model,
1544 process_definition: &ProcessDefinition,
1545 settings: &GenerationSettings,
1546 ) -> Result<(usize, TiVec<CutId, CrossSectionCut>)> {
1547 let all_st_cuts = self.graph.all_st_cuts_for_cs(
1548 self.source_nodes.clone(),
1549 self.target_nodes.clone(),
1550 &self.graph.get_initial_state_tree().0,
1551 );
1552 let num_st_cuts = all_st_cuts.len();
1553
1554 let mut cuts: TiVec<CutId, CrossSectionCut> = all_st_cuts
1555 .into_iter()
1556 .map(|(left, cut, right)| CrossSectionCut { cut, left, right })
1557 .filter(|cut| cut.cut.nedges(&self.graph) > 1)
1558 .filter_map(
1559 |cut| match cut.is_valid_for_process(self, process_definition, model) {
1560 Ok(true) => Some(Ok(cut)),
1561 Ok(false) => None,
1562 Err(e) => Some(Err(e)),
1563 },
1564 )
1565 .collect::<Result<_>>()?;
1566
1567 cuts.sort_by(|a, b| a.cut.cmp(&b.cut));
1568
1569 if !settings.force_cuts.is_empty() {
1570 let force_cuts_sorted = settings
1571 .force_cuts
1572 .clone()
1573 .into_iter()
1574 .map(|cut_edges| cut_edges.into_iter().sorted().collect_vec())
1575 .collect_vec();
1576
1577 cuts.retain(|cut| {
1578 let edges_in_cut = self
1579 .graph
1580 .iter_edges_of(&cut.cut)
1581 .map(|(_, _, e)| e.data.name.value.clone())
1582 .sorted()
1583 .collect_vec();
1584
1585 force_cuts_sorted.contains(&edges_in_cut)
1586 });
1587 }
1588
1589 Ok((num_st_cuts, cuts))
1590 }
1591
1592 fn generate_cuts(
1593 &mut self,
1594 model: &Model,
1595 process_definition: &ProcessDefinition,
1596 settings: &GenerationSettings,
1597 ) -> Result<()> {
1598 debug_tags!(#generation, #profile, #graph;
1599 stage = "cross_section_generate_cuts_start",
1600 graph = %self.graph.name,
1601 "Cut discovery timing milestone"
1602 );
1603 let started = std::time::Instant::now();
1604 let (num_st_cuts, cuts) =
1605 self.compute_process_valid_cuts(model, process_definition, settings)?;
1606 self.cuts = cuts;
1607 debug_tags!(#generation, #profile, #graph;
1608 stage = "cross_section_generate_cuts_done",
1609 graph = %self.graph.name,
1610 st_cut_count = num_st_cuts,
1611 valid_cut_count = self.cuts.len(),
1612 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1613 "Cut discovery timing milestone"
1614 );
1615 generation_progress::cuts_discovered("", &self.graph.name, num_st_cuts, self.cuts.len());
1616
1617 Ok(())
1618 }
1619
1620 fn generate_esurface_cuts(&mut self) {
1621 debug!("generating esurfaces for cuts");
1622
1623 let esurfaces: TiVec<CutId, Esurface> = self
1624 .cuts
1625 .iter()
1626 .map(|cut| {
1627 Esurface::new_from_cut_left(
1628 &self.graph.underlying,
1629 cut,
1630 Some(&self.graph.initial_state_cut),
1631 )
1632 })
1633 .collect();
1634
1635 debug!("generated esurfaces {:?}", esurfaces);
1636
1637 self.cut_esurface = esurfaces;
1638 }
1639
1640 pub(crate) fn build_parametric_integrand(
1641 &mut self,
1642 settings: &GenerationSettings,
1643 vakint: &Vakint,
1644 ) -> Result<()> {
1645 self.derived_data.cut_paramatric_integrand = self.build_integrand(settings, vakint)?;
1646 Ok(())
1647 }
1648
1649 fn build_integrand(
1650 &mut self,
1651 settings: &GenerationSettings,
1652 vakint: &Vakint,
1653 ) -> Result<TiVec<CutGroupId, ParametricIntegrands>> {
1654 let started = std::time::Instant::now();
1655 crate::debug_tags!(#generation, #profile, #uv, #graph, #summary;
1656 stage = "supergraph_build_integrand_start",
1657 graph = %self.graph.name,
1658 subtract_uv = settings.uv.subtract_uv,
1659 generate_integrated = settings.uv.generate_integrated,
1660 "Generation timing milestone"
1661 );
1662 let max_order = self
1663 .derived_data
1664 .cut_group_data
1665 .cut_groups
1666 .iter()
1667 .map(|cut_group| cut_group.related_esurface_group.max_occurence)
1668 .max()
1669 .unwrap();
1670
1671 self.graph.param_builder.initialize_duals(max_order);
1672 crate::debug_tags!(#generation, #profile, #graph, #summary;
1673 stage = "supergraph_initialize_duals_done",
1674 graph = %self.graph.name,
1675 max_order,
1676 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1677 "Generation timing milestone"
1678 );
1679
1680 let cuts = self
1681 .derived_data
1682 .cut_group_data
1683 .cut_groups
1684 .iter()
1685 .map(|cuts| CutSet {
1686 residue_selector: ResidueSelector {
1687 lu_cut: Some(cuts.related_esurface_group.clone()),
1688 left_th_cut: None,
1689 right_th_cut: None,
1690 },
1691 union: cuts
1692 .cuts
1693 .iter()
1694 .map(|cut_id| self.cuts[*cut_id].cut.as_subgraph())
1695 .reduce(|cut_1, cut_2| cut_1.union(&cut_2))
1696 .unwrap_or_else(|| self.graph.empty_subgraph()),
1697 canonicalize_external_shifts: false,
1698 })
1699 .collect();
1700
1701 let cut_structure = CutStructure { cuts };
1702 crate::debug_tags!(#generation, #profile, #uv, #graph, #summary;
1703 stage = "supergraph_cutsets_done",
1704 graph = %self.graph.name,
1705 cut_count = cut_structure.cuts.len(),
1706 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1707 "Generation timing milestone"
1708 );
1709
1710 let valid_orientations: Vec<_> = self
1711 .derived_data
1712 .global_cff_expression
1713 .as_ref()
1714 .expect("global_cff_expression should have been created")
1715 .orientations
1716 .iter()
1717 .map(|orientation| orientation.data.orientation.clone())
1718 .collect();
1719
1720 let lu_prefactor = self.lu_prefactor_helper();
1721
1722 let orchestration_started = std::time::Instant::now();
1723 let parametric_integrands = settings.uv.orchestrator.parametric_integrands(
1724 &mut self.graph,
1725 cut_structure,
1726 vakint,
1727 OrientationProjection::new(&valid_orientations, &settings.orientation_pattern),
1728 &settings.uv,
1729 )?;
1730 crate::debug_tags!(#generation, #profile, #uv, #graph, #summary;
1731 stage = "supergraph_parametric_orchestration_done",
1732 graph = %self.graph.name,
1733 parametric_integrand_count = parametric_integrands.len(),
1734 elapsed_ms = orchestration_started.elapsed().as_secs_f64() * 1000.0,
1735 total_elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1736 "Generation timing milestone"
1737 );
1738
1739 let finalize_started = std::time::Instant::now();
1740 let result = parametric_integrands
1741 .into_iter()
1742 .map(|integrand| integrand.map(|a| a * &lu_prefactor))
1743 .collect();
1744 crate::debug_tags!(#generation, #profile, #uv, #graph, #summary;
1745 stage = "supergraph_build_integrand_done",
1746 graph = %self.graph.name,
1747 elapsed_ms = finalize_started.elapsed().as_secs_f64() * 1000.0,
1748 total_elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1749 "Generation timing milestone"
1750 );
1751 Ok(result)
1752 }
1753
1754 fn lu_prefactor_helper(&self) -> Atom {
1755 let loop_number = self.graph.cyclotomatic_number(&self.graph.full_filter())
1756 - self.graph.initial_state_cut.nedges(&self.graph);
1757
1758 let loop_3 = loop_number as i64 * 3;
1759 let energy_conservation_delta_factor = Atom::num(2) * Atom::var(GS.pi);
1760
1761 crate::debug_tags!(#generation, #normalization, #lu, #graph, #summary;
1762 stage = "cross_section_lu_prefactor",
1763 graph = %self.graph.name,
1764 loop_number,
1765 loop_3,
1766 "Cross-section LU prefactor normalization"
1767 );
1768
1769 let tstar = Atom::var(GS.rescale_star);
1770 let tsrat_pow = tstar.pow(loop_3);
1771 let hfunction = Atom::var(GS.hfunction_lu_cut);
1772 tsrat_pow * hfunction * energy_conservation_delta_factor
1773 }
1774
1775 fn single_th_prefactor_helper_atom(
1776 order: u8,
1777 subspace_loop_count: usize,
1778 is_on_right: bool,
1779 include_integrated: bool,
1780 ) -> Atom {
1781 let loop_3 = subspace_loop_count as i64 * 3;
1782
1783 let i = Atom::i();
1784
1785 let radius = if is_on_right {
1786 Atom::var(GS.radius_right)
1787 } else {
1788 Atom::var(GS.radius_left)
1789 };
1790
1791 let radius_star = if is_on_right {
1792 Atom::var(GS.radius_star_right)
1793 } else {
1794 Atom::var(GS.radius_star_left)
1795 };
1796 let uv_damp_plus = if is_on_right {
1797 Atom::var(GS.uv_damp_plus_right)
1798 } else {
1799 Atom::var(GS.uv_damp_plus_left)
1800 };
1801 let uv_damp_minus = if is_on_right {
1802 Atom::var(GS.uv_damp_minus_right)
1803 } else {
1804 Atom::var(GS.uv_damp_minus_left)
1805 };
1806 let hfunction = if is_on_right {
1807 Atom::var(GS.hfunction_right_th)
1808 } else {
1809 Atom::var(GS.hfunction_left_th)
1810 };
1811
1812 let laurent_coeff_indices = (1..=order).map(|i| -(i as i8));
1813
1814 let mut laurent_coeffs = laurent_coeff_indices.map(|laurent_coeff_index| {
1815 build_derivative_structure_atom(order, laurent_coeff_index)
1816 .replace(GS.rescale_star)
1817 .with(radius_star.clone())
1818 });
1819
1820 let delta_r_plus = &radius - &radius_star;
1821 let delta_r_minus = -&radius - &radius_star;
1822
1823 let jacobian_ratio = (Atom::one() / &radius).pow(loop_3 - 1);
1824
1825 let local_prefactor =
1826 &jacobian_ratio * (uv_damp_plus / &delta_r_plus + uv_damp_minus / &delta_r_minus);
1827
1828 let integrated_prefactor = if include_integrated {
1829 if is_on_right {
1830 -i * Atom::var(GS.pi) * &jacobian_ratio * hfunction
1831 } else {
1832 i * Atom::var(GS.pi) * &jacobian_ratio * hfunction
1833 }
1834 } else {
1835 Atom::zero()
1836 };
1837
1838 let mut result = (local_prefactor + integrated_prefactor) * laurent_coeffs.next().unwrap();
1839
1840 for pow in 2..=order {
1841 result += laurent_coeffs.next().unwrap()
1842 * &jacobian_ratio
1843 * (Atom::one() / delta_r_plus.pow(pow as i64)
1844 + Atom::one() / delta_r_minus.pow(pow as i64));
1845 }
1846
1847 debug!(
1848 "Threshold counterterm helper atom for order {} and loop number {}: {}",
1849 order, subspace_loop_count, result
1850 );
1851
1852 result
1853 }
1854
1855 fn iterated_th_prefactor_helper_atom(
1856 left_order: u8,
1857 right_order: u8,
1858 left_subspace_loop_count: usize,
1859 right_subspace_loop_count: usize,
1860 include_integrated: bool,
1861 ) -> Atom {
1862 let left_prefactor = Self::single_th_prefactor_helper_atom(
1863 left_order,
1864 left_subspace_loop_count,
1865 false,
1866 include_integrated,
1867 )
1868 .replace(GS.eta)
1869 .with(Atom::var(GS.eta_left));
1870 let right_prefactor = Self::single_th_prefactor_helper_atom(
1871 right_order,
1872 right_subspace_loop_count,
1873 true,
1874 include_integrated,
1875 )
1876 .replace(GS.eta)
1877 .with(Atom::var(GS.eta_right));
1878
1879 let mut product = (left_prefactor * right_prefactor).expand();
1880
1881 product = product.replace_multiple(Self::fuse_left_right_replacement());
1886 product
1887 }
1888
1889 fn single_th_prefactor_helper_params(
1890 order: u8,
1891 _subspace_loop_count: usize,
1892 is_on_right: bool,
1893 ) -> Vec<Atom> {
1894 let radius_star = if is_on_right {
1895 Atom::var(GS.radius_star_right)
1896 } else {
1897 Atom::var(GS.radius_star_left)
1898 };
1899
1900 let uv_damp_plus = if is_on_right {
1901 Atom::var(GS.uv_damp_plus_right)
1902 } else {
1903 Atom::var(GS.uv_damp_plus_left)
1904 };
1905 let uv_damp_minus = if is_on_right {
1906 Atom::var(GS.uv_damp_minus_right)
1907 } else {
1908 Atom::var(GS.uv_damp_minus_left)
1909 };
1910 let hfunction = if is_on_right {
1911 Atom::var(GS.hfunction_right_th)
1912 } else {
1913 Atom::var(GS.hfunction_left_th)
1914 };
1915
1916 let radius = if is_on_right {
1917 Atom::var(GS.radius_right)
1918 } else {
1919 Atom::var(GS.radius_left)
1920 };
1921
1922 let mut params = params_for_derivative_order(order)
1923 .into_iter()
1924 .map(|param| param.replace(GS.rescale_star).with(radius_star.clone()))
1925 .collect_vec();
1926
1927 params.push(radius);
1928 params.push(radius_star);
1929 params.push(uv_damp_plus);
1930 params.push(uv_damp_minus);
1931 params.push(hfunction);
1932 params
1933 }
1934
1935 fn iterated_th_prefactor_helper_params(left_order: u8, right_order: u8) -> Vec<Atom> {
1936 let mut iterated_params = params_for_iterated_threshold_ct(left_order, right_order);
1937 let left_radius_star = Atom::var(GS.radius_star_left);
1938 let right_radius_star = Atom::var(GS.radius_star_right);
1939 let left_radius = Atom::var(GS.radius_left);
1940 let right_radius = Atom::var(GS.radius_right);
1941 let left_uv_damp_plus = Atom::var(GS.uv_damp_plus_left);
1942 let left_uv_damp_minus = Atom::var(GS.uv_damp_minus_left);
1943 let right_uv_damp_plus = Atom::var(GS.uv_damp_plus_right);
1944 let right_uv_damp_minus = Atom::var(GS.uv_damp_minus_right);
1945 let left_hfunction = Atom::var(GS.hfunction_left_th);
1946 let right_hfunction = Atom::var(GS.hfunction_right_th);
1947
1948 iterated_params.push(left_radius);
1949 iterated_params.push(left_radius_star);
1950 iterated_params.push(left_uv_damp_plus);
1951 iterated_params.push(left_uv_damp_minus);
1952 iterated_params.push(left_hfunction);
1953 iterated_params.push(right_radius);
1954 iterated_params.push(right_radius_star);
1955 iterated_params.push(right_uv_damp_plus);
1956 iterated_params.push(right_uv_damp_minus);
1957 iterated_params.push(right_hfunction);
1958 iterated_params
1959 }
1960
1961 #[allow(clippy::too_many_arguments)]
1962 pub(crate) fn single_th_helper(
1963 &self,
1964 order: u8,
1965 subspace_loop_count: usize,
1966 is_on_right: bool,
1967 include_integrated: bool,
1968 dual_shape: Option<Vec<Vec<usize>>>,
1969 optimization_settings: OptimizationSettings,
1970 evaluator_settings: &EvaluatorSettings,
1971 ) -> Result<GenericEvaluator> {
1972 let atom = Self::single_th_prefactor_helper_atom(
1973 order,
1974 subspace_loop_count,
1975 is_on_right,
1976 include_integrated,
1977 );
1978 let params =
1979 Self::single_th_prefactor_helper_params(order, subspace_loop_count, is_on_right);
1980
1981 let mut fn_map = FunctionMap::new();
1982 fn_map
1983 .add_aliases([(
1984 GS.pi.into(),
1985 Atom::num(Rational::try_from(std::f64::consts::PI).unwrap()),
1986 )])
1987 .unwrap();
1988
1989 let evaluator = GenericEvaluator::new_from_raw_params(
1990 [atom],
1991 ¶ms,
1992 &fn_map,
1993 vec![],
1994 optimization_settings,
1995 dual_shape,
1996 evaluator_settings,
1997 )?
1998 .into_eager_only();
1999
2000 Ok(evaluator)
2001 }
2002
2003 #[allow(clippy::too_many_arguments)]
2004 pub(crate) fn iterated_th_helper(
2005 &self,
2006 left_order: u8,
2007 right_order: u8,
2008 left_subspace_loop_count: usize,
2009 right_subspace_loop_count: usize,
2010 include_integrated: bool,
2011 dual_shape: Option<Vec<Vec<usize>>>,
2012 optimization_settings: OptimizationSettings,
2013 evaluator_settings: &EvaluatorSettings,
2014 ) -> Result<GenericEvaluator> {
2015 let atom = Self::iterated_th_prefactor_helper_atom(
2016 left_order,
2017 right_order,
2018 left_subspace_loop_count,
2019 right_subspace_loop_count,
2020 include_integrated,
2021 );
2022
2023 let params = Self::iterated_th_prefactor_helper_params(left_order, right_order);
2024
2025 let mut fn_map = FunctionMap::new();
2026 fn_map
2027 .add_aliases([(
2028 GS.pi.into(),
2029 Atom::num(Rational::try_from(std::f64::consts::PI).unwrap()),
2030 )])
2031 .unwrap();
2032
2033 let evaluator = GenericEvaluator::new_from_raw_params(
2034 [atom],
2035 ¶ms,
2036 &fn_map,
2037 vec![],
2038 optimization_settings,
2039 dual_shape,
2040 evaluator_settings,
2041 )?
2042 .into_eager_only();
2043
2044 Ok(evaluator)
2045 }
2046
2047 fn fuse_left_right_replacement() -> Vec<Replacement> {
2048 let f = symbol!("f");
2049
2050 vec![
2051 Replacement::new(
2052 (function!(f, GS.radius_star_left) * function!(f, GS.radius_star_right))
2053 .to_pattern(),
2054 function!(f, GS.radius_star_left, GS.radius_star_right),
2055 ),
2056 Replacement::new(
2057 (function!(f, GS.radius_star_left)
2058 * function!(Symbol::DERIVATIVE, W_.x_, f, GS.radius_star_right))
2059 .to_pattern(),
2060 function!(
2061 Symbol::DERIVATIVE,
2062 0,
2063 W_.x_,
2064 f,
2065 GS.radius_star_left,
2066 GS.radius_star_right
2067 ),
2068 ),
2069 Replacement::new(
2070 (function!(Symbol::DERIVATIVE, W_.x_, f, GS.radius_star_left)
2071 * function!(f, GS.radius_star_right))
2072 .to_pattern(),
2073 function!(
2074 Symbol::DERIVATIVE,
2075 W_.x_,
2076 0,
2077 f,
2078 GS.radius_star_left,
2079 GS.radius_star_right
2080 ),
2081 ),
2082 Replacement::new(
2083 (function!(Symbol::DERIVATIVE, W_.x_, f, GS.radius_star_left)
2084 * function!(Symbol::DERIVATIVE, W_.y_, f, GS.radius_star_right))
2085 .to_pattern(),
2086 function!(
2087 Symbol::DERIVATIVE,
2088 W_.x_,
2089 W_.y_,
2090 f,
2091 GS.radius_star_left,
2092 GS.radius_star_right
2093 ),
2094 ),
2095 ]
2096 }
2097 fn build_lmbs(&mut self) -> Result<()> {
2164 let mut lmbs: TiVec<LmbIndex, LoopMomentumBasis> = vec![].into();
2165
2166 let externals: SuBitGraph = self.graph.empty_subgraph();
2167 let full_filter = self.graph.full_filter();
2168 let cut_graph = full_filter.subtract(&self.graph.initial_state_cut.right);
2169
2170 for s in self.graph.all_spanning_forests_of(&cut_graph) {
2171 let mut lmb = self.graph.lmb_impl(&full_filter, &s, externals.clone())?;
2172 let mut exts = vec![];
2173
2174 for i in lmb.loop_edges.iter() {
2175 let (_, p) = &self.graph[i];
2176
2177 if self.graph.initial_state_cut.intersects(p) {
2178 exts.push(*i);
2179 }
2180 }
2181
2182 exts.sort();
2183
2184 for e in exts {
2185 let mut loopid = None;
2186 for (l, s) in lmb.edge_signatures[e].internal.iter_enumerated() {
2187 if s.is_non_zero() {
2188 if loopid.is_none() {
2189 loopid = Some(l);
2190 } else {
2191 panic!("external edge has multiple loop momenta")
2192 }
2193 }
2194 }
2195 lmb.put_loop_to_ext(loopid.unwrap());
2196 }
2197 let external_momentum_edge_order = self.graph.external_momentum_edge_order();
2198 lmb.canonicalize_external_order(&external_momentum_edge_order);
2199 lmbs.push(lmb);
2201 }
2202
2203 let sorted_graph_loop_edges = self
2204 .graph
2205 .loop_momentum_basis
2206 .loop_edges
2207 .iter()
2208 .copied()
2209 .sorted()
2210 .collect_vec();
2211
2212 let matching_lmb_index = lmbs.iter_enumerated().find_map(|(lmb_index, lmb)| {
2213 let sorted_loop_edges = lmb.loop_edges.iter().copied().sorted().collect_vec();
2214 (sorted_loop_edges == sorted_graph_loop_edges).then_some(lmb_index)
2215 });
2216
2217 if let Some(matching_lmb_index) = matching_lmb_index {
2218 {
2219 let matching_lmb = &mut lmbs[matching_lmb_index];
2220 for (target_pos, target_edge) in
2221 self.graph.loop_momentum_basis.loop_edges.iter_enumerated()
2222 {
2223 let current_pos = matching_lmb
2224 .loop_edges
2225 .iter()
2226 .find_position(|edge| *edge == target_edge)
2227 .map(|(pos, _)| pos)
2228 .unwrap();
2229
2230 if current_pos != target_pos.0 {
2231 matching_lmb.swap_loops(current_pos.into(), target_pos);
2232 }
2233 }
2234 }
2235
2236 let matching_lmb_pos: usize = matching_lmb_index.into();
2237 if matching_lmb_pos != 0 {
2238 lmbs.raw[..=matching_lmb_pos].rotate_right(1);
2239 }
2240 } else {
2241 warn!(
2242 "Could not match current graph LMB against generated LMBs for graph {}",
2243 self.graph.name
2244 );
2245 }
2246
2247 let sorted_graph_loop_edges = self
2248 .graph
2249 .loop_momentum_basis
2250 .loop_edges
2251 .iter()
2252 .copied()
2253 .sorted()
2254 .collect_vec();
2255
2256 let matching_lmb_index = lmbs.iter_enumerated().find_map(|(lmb_index, lmb)| {
2257 let sorted_loop_edges = lmb.loop_edges.iter().copied().sorted().collect_vec();
2258 (sorted_loop_edges == sorted_graph_loop_edges).then_some(lmb_index)
2259 });
2260
2261 if let Some(matching_lmb_index) = matching_lmb_index {
2262 {
2263 let matching_lmb = &mut lmbs[matching_lmb_index];
2264 for (target_pos, target_edge) in
2265 self.graph.loop_momentum_basis.loop_edges.iter_enumerated()
2266 {
2267 let current_pos = matching_lmb
2268 .loop_edges
2269 .iter()
2270 .find_position(|edge| *edge == target_edge)
2271 .map(|(pos, _)| pos)
2272 .unwrap();
2273
2274 if current_pos != target_pos.0 {
2275 matching_lmb.swap_loops(current_pos.into(), target_pos);
2276 }
2277 }
2278 }
2279
2280 let matching_lmb_pos: usize = matching_lmb_index.into();
2281 if matching_lmb_pos != 0 {
2282 lmbs.raw[..=matching_lmb_pos].rotate_right(1);
2283 }
2284 } else {
2285 warn!(
2286 "Could not match current graph LMB against generated LMBs for graph {}",
2287 self.graph.name
2288 );
2289 }
2290
2291 self.derived_data.lmbs = Some(lmbs);
2292 Ok(())
2293 }
2294
2295 fn build_multi_channeling_channels(&mut self, override_lmb_heuristics: bool) -> Result<()> {
2296 let lmbs = self.derived_data.lmbs.as_ref().unwrap();
2297 let channels = if override_lmb_heuristics {
2298 self.graph
2299 .build_multi_channeling_channels(lmbs, override_lmb_heuristics)
2300 } else {
2301 self.build_cross_section_multi_channeling_channels(lmbs)?
2302 };
2303
2304 self.derived_data.multi_channeling_setup = Some(channels);
2305 Ok(())
2306 }
2307
2308 fn build_cross_section_multi_channeling_channels(
2309 &self,
2310 lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
2311 ) -> Result<LmbMultiChannelingSetup> {
2312 let channels = self.select_cross_section_lmb_channel_indices(lmbs)?;
2313 debug!(
2314 "number of lmbs: {}, number of cross-section channels: {}",
2315 lmbs.len(),
2316 channels.len()
2317 );
2318 Ok(LmbMultiChannelingSetup {
2319 channels,
2320 graph: self.graph.clone(),
2321 all_bases: lmbs.clone(),
2322 })
2323 }
2324
2325 fn select_cross_section_lmb_channel_indices(
2326 &self,
2327 lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
2328 ) -> Result<TiVec<crate::integrands::process::ChannelIndex, LmbIndex>> {
2329 if self.cuts.is_empty() {
2330 return Ok(self.graph.select_amplitude_lmb_channel_indices(
2331 lmbs,
2332 false,
2333 LmbChannelFallback::CurrentGraphBasis,
2334 ));
2335 }
2336
2337 let mut lmb_index_by_loop_edges = BTreeMap::<Vec<EdgeIndex>, LmbIndex>::new();
2338 for (lmb_index, lmb) in lmbs.iter_enumerated() {
2339 lmb_index_by_loop_edges
2340 .entry(lmb.loop_edges.iter().copied().sorted().collect())
2341 .or_insert(lmb_index);
2342 }
2343
2344 let mut selected_loop_edge_sets = BTreeSet::<Vec<EdgeIndex>>::new();
2345 for cut in &self.cuts {
2346 let cut_edges = cut.cut.as_subgraph();
2347 let cut_edge_ids = self
2348 .graph
2349 .underlying
2350 .iter_edges_of(&cut_edges)
2351 .map(|(_, edge_id, _)| edge_id)
2352 .sorted()
2353 .collect_vec();
2354 let Some(excluded_cut_edge) = self.excluded_cut_edge_for_lmb_channel(&cut_edge_ids)
2355 else {
2356 continue;
2357 };
2358
2359 let fixed_cut_edges = cut_edge_ids
2360 .iter()
2361 .copied()
2362 .filter(|edge_id| *edge_id != excluded_cut_edge)
2363 .collect::<BTreeSet<_>>();
2364 let left_basis_edge_sets =
2365 self.selected_cut_side_lmb_edge_sets(cut.left.subtract(&cut_edges))?;
2366 let right_basis_edge_sets =
2367 self.selected_cut_side_lmb_edge_sets(cut.right.subtract(&cut_edges))?;
2368
2369 for left_basis_edges in &left_basis_edge_sets {
2370 for right_basis_edges in &right_basis_edge_sets {
2371 let loop_edges = fixed_cut_edges
2372 .iter()
2373 .copied()
2374 .chain(left_basis_edges.iter().copied())
2375 .chain(right_basis_edges.iter().copied())
2376 .collect::<BTreeSet<_>>()
2377 .into_iter()
2378 .collect_vec();
2379 selected_loop_edge_sets.insert(loop_edges);
2380 }
2381 }
2382 }
2383
2384 let mut channels = Vec::<LmbIndex>::new();
2385 for loop_edges in selected_loop_edge_sets {
2386 let lmb_index = lmb_index_by_loop_edges.get(&loop_edges).ok_or_else(|| {
2387 eyre!(
2388 "Could not find a generated cross-section LMB with loop edges [{}] for graph '{}'.",
2389 loop_edges.iter().map(|edge| edge.to_string()).join(", "),
2390 self.graph.name
2391 )
2392 })?;
2393 channels.push(*lmb_index);
2394 }
2395
2396 if channels.is_empty() {
2397 Ok(self.graph.select_amplitude_lmb_channel_indices(
2398 lmbs,
2399 false,
2400 LmbChannelFallback::CurrentGraphBasis,
2401 ))
2402 } else {
2403 Ok(channels.into_iter().sorted().dedup().collect())
2404 }
2405 }
2406
2407 fn excluded_cut_edge_for_lmb_channel(&self, cut_edge_ids: &[EdgeIndex]) -> Option<EdgeIndex> {
2408 Self::excluded_cut_edge_for_lmb_channel_in(&self.graph, cut_edge_ids)
2409 }
2410
2411 fn excluded_cut_edge_for_lmb_channel_in(
2412 graph: &Graph,
2413 cut_edge_ids: &[EdgeIndex],
2414 ) -> Option<EdgeIndex> {
2415 cut_edge_ids
2416 .iter()
2417 .copied()
2418 .filter(|edge_id| graph[*edge_id].particle.is_massive())
2419 .min()
2420 .or_else(|| {
2421 cut_edge_ids
2422 .iter()
2423 .copied()
2424 .filter(|edge_id| graph[*edge_id].particle.is_fermion())
2425 .min()
2426 })
2427 .or_else(|| cut_edge_ids.iter().copied().min())
2428 }
2429
2430 fn selected_cut_side_lmb_edge_sets(
2431 &self,
2432 mut side_subgraph: SuBitGraph,
2433 ) -> Result<Vec<Vec<EdgeIndex>>> {
2434 for (pair, _, edge) in self.graph.underlying.iter_edges() {
2435 if edge.data.is_dummy {
2436 side_subgraph.sub(pair);
2437 }
2438 }
2439
2440 if self.graph.underlying.cyclotomatic_number(&side_subgraph) == 0 {
2441 return Ok(vec![Vec::new()]);
2442 }
2443
2444 let side_lmbs = self.graph.generate_loop_momentum_bases_of(&side_subgraph);
2445 if side_lmbs.is_empty() {
2446 return Err(eyre!(
2447 "Could not generate cut-side LMBs for a non-tree side of graph '{}'.",
2448 self.graph.name
2449 ));
2450 }
2451
2452 let selected_side_lmbs = self.graph.select_amplitude_lmb_channel_indices(
2453 &side_lmbs,
2454 false,
2455 LmbChannelFallback::FirstBasis,
2456 );
2457 let mut edge_sets = BTreeSet::<Vec<EdgeIndex>>::new();
2458 for lmb_index in selected_side_lmbs {
2459 edge_sets.insert(
2460 side_lmbs[lmb_index]
2461 .loop_edges
2462 .iter()
2463 .copied()
2464 .sorted()
2465 .collect(),
2466 );
2467 }
2468
2469 if edge_sets.is_empty() {
2470 Ok(vec![Vec::new()])
2471 } else {
2472 Ok(edge_sets.into_iter().collect())
2473 }
2474 }
2475
2476 fn threshold_counterterm_association(
2477 &self,
2478 threshold_candidate: &TopologicalThresholdCandidate,
2479 sandwich: &SuBitGraph,
2480 cut: &OrientedCut,
2481 ) -> ThresholdCountertermAssociation {
2482 let boundary_edges = |boundary: &OrientedCut| {
2483 let boundary_filter = boundary.left.union(&boundary.right).intersection(sandwich);
2484 let edges = self
2485 .graph
2486 .iter_edges_of(&boundary_filter)
2487 .map(|(_, edge_id, _)| edge_id)
2488 .sorted()
2489 .collect();
2490 let has_left_orientation =
2491 boundary_filter.intersection(&boundary.left).n_included() > 0;
2492 let has_right_orientation =
2493 boundary_filter.intersection(&boundary.right).n_included() > 0;
2494 (edges, has_left_orientation != has_right_orientation)
2495 };
2496
2497 let (cut_boundary_edges, cut_bound_is_applicable) = boundary_edges(cut);
2498 let (threshold_boundary_edges, threshold_bound_is_applicable) =
2499 boundary_edges(&threshold_candidate.cut);
2500
2501 ThresholdCountertermAssociation {
2502 esurface_id: threshold_candidate.esurface_id,
2503 cut_boundary_edges,
2504 threshold_boundary_edges,
2505 invariant_bound_is_applicable: cut_bound_is_applicable && threshold_bound_is_applicable,
2506 }
2507 }
2508
2509 #[allow(clippy::too_many_arguments)]
2510 fn classify_threshold_counterterm_association(
2511 &self,
2512 association: &ThresholdCountertermAssociation,
2513 cut_id: CutId,
2514 cut: &CrossSectionCut,
2515 subspace: &SubspaceData,
2516 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
2517 model: &Model,
2518 runtime_settings: &RuntimeSettings,
2519 settings: &GenerationSettings,
2520 ) -> ThresholdCountertermStatus {
2521 let esurface = &self.graph.surface_cache.esurface_cache[association.esurface_id];
2522 if !esurface.has_radial_dependence_in_subspace(subspace, all_lmbs, &self.graph) {
2523 debug!(
2524 "Skipping graph '{}' cut {} threshold E-surface {} after cut-relative classification {:?}",
2525 self.graph.name,
2526 cut_id.0,
2527 association.esurface_id.0,
2528 ThresholdCountertermStatus::NoRadialDependence,
2529 );
2530 return ThresholdCountertermStatus::NoRadialDependence;
2531 }
2532
2533 let structural_status = if association.invariant_bound_is_applicable {
2534 self.graph.classify_threshold_pinch(
2535 &association.cut_boundary_edges,
2536 &association.threshold_boundary_edges,
2537 )
2538 } else {
2539 ThresholdPinchStatus::NotProven
2540 };
2541 if structural_status == ThresholdPinchStatus::Always {
2542 debug!(
2543 "Skipping graph '{}' cut {} threshold E-surface {} after cut-relative classification {:?}",
2544 self.graph.name,
2545 cut_id.0,
2546 association.esurface_id.0,
2547 ThresholdCountertermStatus::AlwaysPinched,
2548 );
2549 return ThresholdCountertermStatus::AlwaysPinched;
2550 }
2551
2552 let status = if settings.threshold_subtraction.check_esurface_at_generation {
2553 association.classify_for_model(
2554 &self.graph,
2555 cut,
2556 model,
2557 &self.graph.param_builder,
2558 runtime_settings,
2559 settings.threshold_subtraction.esurface_existence_threshold,
2560 )
2561 } else {
2562 match structural_status {
2563 ThresholdPinchStatus::CanBecome => ThresholdCountertermStatus::CanBecomePinched,
2564 ThresholdPinchStatus::NotProven => ThresholdCountertermStatus::PotentiallyExisting,
2565 ThresholdPinchStatus::Always => unreachable!(
2566 "identically pinched threshold associations return before classification"
2567 ),
2568 }
2569 };
2570 if !status
2571 .is_eligible_for_generation(settings.threshold_subtraction.check_esurface_at_generation)
2572 {
2573 debug!(
2574 "Skipping graph '{}' cut {} threshold E-surface {} after cut-relative classification {:?}",
2575 self.graph.name, cut_id.0, association.esurface_id.0, status,
2576 );
2577 }
2578 status
2579 }
2580
2581 fn topological_threshold_candidates(&self) -> Result<Vec<TopologicalThresholdCandidate>> {
2582 let mut candidates = self.graph.all_st_cuts_for_cs(
2583 self.source_nodes.clone(),
2584 self.target_nodes.clone(),
2585 &self.graph.get_initial_state_tree().0,
2586 );
2587 candidates.retain(|(_left, cut, _right)| cut.nedges(&self.graph) > 1);
2588 candidates.sort_by(|a, b| a.1.cmp(&b.1));
2589
2590 candidates
2591 .into_iter()
2592 .map(|(left, cut, right)| {
2593 let threshold_esurface = Esurface::new_from_cut_left(
2594 &self.graph.underlying,
2595 &CrossSectionCut {
2596 cut: cut.clone(),
2597 left: left.clone(),
2598 right: right.clone(),
2599 },
2600 Some(&self.graph.initial_state_cut),
2601 );
2602 let esurface_id = self
2603 .graph
2604 .surface_cache
2605 .esurface_cache
2606 .position(|esurface| esurface == &threshold_esurface)
2607 .ok_or_else(|| {
2608 eyre!(
2609 "Topology-discovered threshold surface {:?} is missing from graph '{}' CFF surface cache",
2610 threshold_esurface.energies,
2611 self.graph.name,
2612 )
2613 })?;
2614 Ok(TopologicalThresholdCandidate {
2615 left,
2616 cut,
2617 right,
2618 esurface_id,
2619 })
2620 })
2621 .collect()
2622 }
2623
2624 fn build_threshold_counterterm(
2625 &mut self,
2626 model: &Model,
2627 settings: &GenerationSettings,
2628 runtime_settings: &RuntimeSettings,
2629 all_possible_thresholds: &[TopologicalThresholdCandidate],
2630 vakint: &Vakint,
2631 ) -> Result<()> {
2632 let mut cut_threshold_associations: TiVec<CutId, CutThresholdCountertermAssociations> =
2635 ti_vec![CutThresholdCountertermAssociations::default(); self.cuts.len()];
2636
2637 let mut cut_group_by_cut = vec![None; self.cuts.len()];
2638 for (cut_group_id, cut_group) in self
2639 .derived_data
2640 .cut_group_data
2641 .cut_groups
2642 .iter_enumerated()
2643 {
2644 for cut_id in &cut_group.cuts {
2645 cut_group_by_cut[cut_id.0] = Some(cut_group_id);
2646 }
2647 }
2648 let all_lmbs = self
2649 .derived_data
2650 .lmbs
2651 .as_ref()
2652 .expect("threshold generation requires loop-momentum bases");
2653
2654 let subtraction_threshold_candidates = all_possible_thresholds
2659 .iter()
2660 .filter(|threshold_candidate| {
2661 !settings.threshold_subtraction.skip_thresholds_that_are_cuts
2662 || !self
2663 .cuts
2664 .iter()
2665 .any(|physical_cut| physical_cut.cut == threshold_candidate.cut)
2666 })
2667 .collect_vec();
2668
2669 for (cut_id, cut) in self.cuts.iter_enumerated() {
2670 let cut_group_id = cut_group_by_cut[cut_id.0].ok_or_else(|| {
2671 eyre!(
2672 "Physical cut {} in graph '{}' is missing from the cut-group partition",
2673 cut_id.0,
2674 self.graph.name,
2675 )
2676 })?;
2677 let (left_subspace, right_subspace) = &self.derived_data.subspace_data[cut_group_id];
2678
2679 for threshold_candidate in &subtraction_threshold_candidates {
2680 if cut.left.includes(&threshold_candidate.left) {
2682 let sandwich = cut.left.intersection(&threshold_candidate.right);
2683
2684 if self.graph.underlying.is_connected(&sandwich) {
2687 let association = self.threshold_counterterm_association(
2688 threshold_candidate,
2689 &sandwich,
2690 &cut.cut,
2691 );
2692 let status = self.classify_threshold_counterterm_association(
2693 &association,
2694 cut_id,
2695 cut,
2696 left_subspace,
2697 all_lmbs,
2698 model,
2699 runtime_settings,
2700 settings,
2701 );
2702 if status.is_eligible_for_generation(
2703 settings.threshold_subtraction.check_esurface_at_generation,
2704 ) {
2705 cut_threshold_associations[cut_id].left.push(association);
2706 }
2707 }
2708 } else if cut.right.includes(&threshold_candidate.right) {
2709 let sandwich = cut.right.intersection(&threshold_candidate.left);
2710 if self.graph.underlying.is_connected(&sandwich) {
2711 let association = self.threshold_counterterm_association(
2717 threshold_candidate,
2718 &sandwich,
2719 &cut.cut,
2720 );
2721 let status = self.classify_threshold_counterterm_association(
2722 &association,
2723 cut_id,
2724 cut,
2725 right_subspace,
2726 all_lmbs,
2727 model,
2728 runtime_settings,
2729 settings,
2730 );
2731 if status.is_eligible_for_generation(
2732 settings.threshold_subtraction.check_esurface_at_generation,
2733 ) {
2734 cut_threshold_associations[cut_id].right.push(association);
2735 }
2736 }
2737 }
2738 }
2739 }
2740
2741 let left_cut_threshold_data: TiVec<CutId, Vec<EsurfaceID>> = cut_threshold_associations
2742 .iter()
2743 .map(|associations| {
2744 associations
2745 .left
2746 .iter()
2747 .map(|association| association.esurface_id)
2748 .collect()
2749 })
2750 .collect();
2751 let right_cut_threshold_data: TiVec<CutId, Vec<EsurfaceID>> = cut_threshold_associations
2752 .iter()
2753 .map(|associations| {
2754 associations
2755 .right
2756 .iter()
2757 .map(|association| association.esurface_id)
2758 .collect()
2759 })
2760 .collect();
2761 self.derived_data.cut_threshold_associations = cut_threshold_associations;
2762
2763 let threshold_raised_data = self.graph.determine_raised_esurfaces_from_expression(
2764 self.derived_data
2765 .global_cff_expression
2766 .as_ref()
2767 .expect("global_cff_expression should have been created"),
2768 );
2769 let mut raised_threshold_ids: TiVec<EsurfaceID, Option<RaisedEsurfaceId>> =
2770 ti_vec![None; self.graph.surface_cache.esurface_cache.len()];
2771
2772 for (raised_threshold_id, raised_group) in
2773 threshold_raised_data.raised_groups.iter_enumerated()
2774 {
2775 for &esurface_id in &raised_group.esurface_ids {
2776 raised_threshold_ids[esurface_id] = Some(raised_threshold_id);
2777 }
2778 }
2779
2780 let raised_threshold_ids: TiVec<EsurfaceID, RaisedEsurfaceId> = raised_threshold_ids
2781 .into_iter()
2782 .map(|raised_threshold_id| {
2783 raised_threshold_id
2784 .expect("every esurface should belong to exactly one raised threshold group")
2785 })
2786 .collect();
2787
2788 let collect_raised_threshold_groups = |threshold_ids: Vec<EsurfaceID>| {
2789 let mut groups = Vec::new();
2790 for esurface_id in threshold_ids.into_iter().sorted().dedup() {
2791 let raised_threshold_id = raised_threshold_ids[esurface_id];
2792 let raised_group = threshold_raised_data.raised_groups[raised_threshold_id].clone();
2793 if !groups.contains(&raised_group) {
2794 groups.push(raised_group);
2795 }
2796 }
2797 groups
2798 };
2799
2800 let mut left_cut_group_threshold_data: TiVec<
2801 CutGroupId,
2802 TiVec<LeftThresholdId, RaisedEsurfaceGroup>,
2803 > = TiVec::new();
2804
2805 let mut right_cut_group_threshold_data: TiVec<
2806 CutGroupId,
2807 TiVec<RightThresholdId, RaisedEsurfaceGroup>,
2808 > = TiVec::new();
2809
2810 for cut_group in self.derived_data.cut_group_data.cut_groups.iter() {
2811 let left_thresholds = collect_raised_threshold_groups(
2812 cut_group
2813 .cuts
2814 .iter()
2815 .flat_map(|cut_id| left_cut_threshold_data[*cut_id].iter().copied())
2816 .collect(),
2817 );
2818
2819 let mut right_thresholds = collect_raised_threshold_groups(
2820 cut_group
2821 .cuts
2822 .iter()
2823 .flat_map(|cut_id| right_cut_threshold_data[*cut_id].iter().copied())
2824 .collect(),
2825 );
2826
2827 right_thresholds.retain(|raised_group| !left_thresholds.contains(raised_group));
2828
2829 left_cut_group_threshold_data.push(left_thresholds.into());
2830 right_cut_group_threshold_data.push(right_thresholds.into());
2831 }
2832
2833 let mut cut_structure = vec![];
2834
2835 for (cut_group_id, cut_group) in self
2836 .derived_data
2837 .cut_group_data
2838 .cut_groups
2839 .iter_enumerated()
2840 {
2841 let left_thresholds = &left_cut_group_threshold_data[cut_group_id];
2842 let right_thresholds = &right_cut_group_threshold_data[cut_group_id];
2843
2844 let cutkosky_cut_union = cut_group
2845 .cuts
2846 .iter()
2847 .map(|cut_id| self.cuts[*cut_id].cut.as_subgraph())
2848 .reduce(|a, b| a.union(&b))
2849 .unwrap_or(self.graph.empty_subgraph());
2850
2851 let add_threshold_group_to_union =
2852 |base: SuBitGraph, raised_group: &RaisedEsurfaceGroup| {
2853 let representative_esurface =
2854 &self.graph.surface_cache.esurface_cache[raised_group.esurface_ids[0]];
2855
2856 representative_esurface
2857 .energies
2858 .iter()
2859 .map(|edge_id| self.graph.get_edge_subgraph(*edge_id))
2860 .fold(base, |acc, subgraph| acc.union(&subgraph))
2861 };
2862
2863 for raised_esurface_group in left_thresholds {
2864 let esurface_cut_union =
2865 add_threshold_group_to_union(cutkosky_cut_union.clone(), raised_esurface_group);
2866
2867 cut_structure.push(CutSet {
2868 residue_selector: ResidueSelector {
2869 lu_cut: Some(cut_group.related_esurface_group.clone()),
2870 left_th_cut: Some(raised_esurface_group.clone()),
2871 right_th_cut: None,
2872 },
2873 union: esurface_cut_union,
2874 canonicalize_external_shifts: false,
2875 });
2876 }
2877
2878 for raised_esurface_group in right_thresholds {
2879 let esurface_cut_union =
2880 add_threshold_group_to_union(cutkosky_cut_union.clone(), raised_esurface_group);
2881
2882 cut_structure.push(CutSet {
2883 residue_selector: ResidueSelector {
2884 lu_cut: Some(cut_group.related_esurface_group.clone()),
2885 left_th_cut: None,
2886 right_th_cut: Some(raised_esurface_group.clone()),
2887 },
2888 union: esurface_cut_union,
2889 canonicalize_external_shifts: false,
2890 });
2891 }
2892
2893 for (left_raised_esurface_group, right_raised_esurface_group) in left_thresholds
2894 .iter()
2895 .cartesian_product(right_thresholds.iter())
2896 {
2897 let esurface_cut_union = add_threshold_group_to_union(
2898 add_threshold_group_to_union(
2899 cutkosky_cut_union.clone(),
2900 left_raised_esurface_group,
2901 ),
2902 right_raised_esurface_group,
2903 );
2904
2905 cut_structure.push(CutSet {
2906 residue_selector: ResidueSelector {
2907 lu_cut: Some(cut_group.related_esurface_group.clone()),
2908 left_th_cut: Some(left_raised_esurface_group.clone()),
2909 right_th_cut: Some(right_raised_esurface_group.clone()),
2910 },
2911 union: esurface_cut_union,
2912 canonicalize_external_shifts: false,
2913 });
2914 }
2915 }
2916
2917 let cut_structure = CutStructure {
2918 cuts: cut_structure,
2919 };
2920
2921 let valid_orientations: Vec<_> = self
2922 .derived_data
2923 .global_cff_expression
2924 .as_ref()
2925 .expect("global_cff_expression should have been created")
2926 .orientations
2927 .iter()
2928 .map(|orientation| orientation.data.orientation.clone())
2929 .collect();
2930
2931 let mut threshold_counterterms = settings
2932 .uv
2933 .orchestrator
2934 .parametric_integrands(
2935 &mut self.graph,
2936 cut_structure,
2937 vakint,
2938 OrientationProjection::new(&valid_orientations, &settings.orientation_pattern),
2939 &settings.uv,
2940 )?
2941 .into_iter();
2942
2943 let lu_prefactor = self.lu_prefactor_helper();
2944
2945 let mut result = TiVec::<CutGroupId, LUCounterTermData>::new();
2946 for (cut_group_id, _cut_group) in self
2947 .derived_data
2948 .cut_group_data
2949 .cut_groups
2950 .iter_enumerated()
2951 {
2952 let (left_subspace, right_subspace) = &self.derived_data.subspace_data[cut_group_id];
2953
2954 let left_rstar_pow =
2955 Atom::var(GS.radius_star_left).pow(left_subspace.loopcount() as i32 * 3 - 1);
2956
2957 let right_rstar_pow =
2958 Atom::var(GS.radius_star_right).pow(right_subspace.loopcount() as i32 * 3 - 1);
2959
2960 let mut left_atoms = TiVec::<LeftThresholdId, _>::new();
2961 let mut right_atoms = TiVec::<RightThresholdId, _>::new();
2962 let mut iterated_atoms = vec![];
2963
2964 for _ in 0..left_cut_group_threshold_data[cut_group_id].len() {
2965 left_atoms.push(
2966 threshold_counterterms
2967 .next()
2968 .unwrap()
2969 .map(|x| x * &lu_prefactor * &left_rstar_pow),
2970 );
2971 }
2972
2973 for _ in 0..right_cut_group_threshold_data[cut_group_id].len() {
2974 right_atoms.push(
2975 threshold_counterterms
2976 .next()
2977 .unwrap()
2978 .map(|x| x * &lu_prefactor * &right_rstar_pow),
2979 );
2980 }
2981
2982 for _ in 0..(left_cut_group_threshold_data[cut_group_id].len()
2983 * right_cut_group_threshold_data[cut_group_id].len())
2984 {
2985 iterated_atoms.push(
2986 threshold_counterterms
2987 .next()
2988 .unwrap()
2989 .map(|x| x * &lu_prefactor * &left_rstar_pow * &right_rstar_pow),
2990 );
2991 }
2992
2993 let iterated_collection =
2994 IteratedCtCollection::new(iterated_atoms, left_atoms.len(), right_atoms.len());
2995
2996 let counterterm_data = LUCounterTermData {
2997 left_thresholds: left_cut_group_threshold_data[cut_group_id].clone(),
2998 right_thresholds: right_cut_group_threshold_data[cut_group_id].clone(),
2999 left_atoms,
3000 right_atoms,
3001 iterated: iterated_collection,
3002 };
3003 result.push(counterterm_data);
3004 }
3005
3006 let max_dual_size =
3007 max_dual_size_for_cut_cff_indices(result.iter().flat_map(|counterterm_data| {
3008 counterterm_data
3009 .left_atoms
3010 .iter()
3011 .chain(counterterm_data.right_atoms.iter())
3012 .chain(counterterm_data.iterated.iter())
3013 .flat_map(|integrands| integrands.integrands.iter().map(|(index, _)| index))
3014 }));
3015 self.graph.param_builder.initialize_duals(max_dual_size);
3016
3017 self.derived_data.threshold_counterterms = result;
3018 Ok(())
3019 }
3020
3021 fn build_subspace_data(&mut self) -> Result<()> {
3022 let all_lmbs = self.derived_data.lmbs.as_ref().unwrap();
3023
3024 let subspace_data = self
3025 .derived_data
3026 .cut_group_data
3027 .cut_groups
3028 .iter_enumerated()
3029 .map(|(cut_group_id, cut_group)| {
3030 let representative_cut_id = cut_group.cuts.first().copied().ok_or_else(|| {
3031 eyre!(
3032 "Graph '{}' has an empty cut group {} while building threshold-counterterm subspaces",
3033 self.graph.name,
3034 cut_group_id.0,
3035 )
3036 })?;
3037 let valid_subspace_lmbs = all_lmbs
3038 .iter_enumerated()
3039 .filter_map(|(index, lmb)| {
3040 let mut edges_in_cut = self
3041 .graph
3042 .underlying
3043 .iter_edges_of(&self.cuts[representative_cut_id].cut)
3044 .map(|(_, e, _)| e)
3045 .collect_vec();
3046
3047 edges_in_cut.retain(|e| !lmb.loop_edges.contains(e));
3048 if edges_in_cut.len() == 1 {
3049 Some(index)
3050 } else {
3051 None
3052 }
3053 })
3054 .collect_vec();
3055
3056 let left_subgraphs = cut_group
3057 .cuts
3058 .iter()
3059 .map(|cut_id| self.cuts[*cut_id].left.clone())
3060 .sorted_by(|subgraph_a, subgraph_b| {
3061 let n_edges_a = subgraph_a.nedges(&self.graph);
3062 let n_edges_b = subgraph_b.nedges(&self.graph);
3063 n_edges_a.cmp(&n_edges_b)
3064 })
3065 .collect_vec();
3066
3067 let right_subgraphs = cut_group
3068 .cuts
3069 .iter()
3070 .map(|cut_id| self.cuts[*cut_id].right.clone())
3071 .sorted_by(|subgraph_a, subgraph_b| {
3072 let n_edges_a = subgraph_a.nedges(&self.graph);
3073 let n_edges_b = subgraph_b.nedges(&self.graph);
3074 n_edges_a.cmp(&n_edges_b)
3075 })
3076 .collect_vec();
3077
3078 let smallest_left_subgraph = left_subgraphs.first().cloned().ok_or_else(|| {
3079 eyre!(
3080 "Graph '{}' cut group {} has no left subgraph",
3081 self.graph.name,
3082 cut_group_id.0,
3083 )
3084 })?;
3085 let smallest_right_subgraph = right_subgraphs.first().cloned().ok_or_else(|| {
3086 eyre!(
3087 "Graph '{}' cut group {} has no right subgraph",
3088 self.graph.name,
3089 cut_group_id.0,
3090 )
3091 })?;
3092
3093 let mut possible_subspaces = Vec::new();
3094 let mut rejected_lmbs = Vec::new();
3095 for lmb_index in valid_subspace_lmbs {
3096 let left = SubspaceData::new_with_user_selected_lmb(
3097 smallest_left_subgraph.clone(),
3098 lmb_index,
3099 &self.graph,
3100 all_lmbs,
3101 );
3102 let right = SubspaceData::new_with_user_selected_lmb(
3103 smallest_right_subgraph.clone(),
3104 lmb_index,
3105 &self.graph,
3106 all_lmbs,
3107 );
3108
3109 match (left, right) {
3110 (Ok(left), Ok(right)) if left.is_mergable_with(&right) => {
3111 possible_subspaces.push((left, right));
3112 }
3113 (Ok(left), Ok(right)) => rejected_lmbs.push(format!(
3114 "LMB {} produced non-disjoint subspaces: left={:?}, right={:?}",
3115 usize::from(lmb_index),
3116 left.iter_lmb_indices().collect_vec(),
3117 right.iter_lmb_indices().collect_vec(),
3118 )),
3119 (left, right) => rejected_lmbs.push(format!(
3120 "LMB {}: left={}, right={}",
3121 usize::from(lmb_index),
3122 left.err()
3123 .map(|error| format!("{error:#}"))
3124 .unwrap_or_else(|| "compatible".to_string()),
3125 right
3126 .err()
3127 .map(|error| format!("{error:#}"))
3128 .unwrap_or_else(|| "compatible".to_string()),
3129 )),
3130 }
3131 }
3132
3133 possible_subspaces.sort_by_key(|(left, right)| {
3134 (
3135 left.iter_basis_edges(all_lmbs).collect_vec(),
3136 right.iter_basis_edges(all_lmbs).collect_vec(),
3137 )
3138 });
3139
3140 possible_subspaces.first().cloned().ok_or_else(|| {
3141 eyre!(
3142 "No topology-compatible parent LMB found for graph '{}' cut group {}. Rejections:\n{}",
3143 self.graph.name,
3144 cut_group_id.0,
3145 rejected_lmbs.join("\n"),
3146 )
3147 })
3148 })
3149 .collect::<Result<_>>()?;
3150
3151 let _: () = self.derived_data.subspace_data = subspace_data;
3152 Ok(())
3153 }
3154
3155 fn generate_term_for_graph(
3156 &self,
3157 _model: &Model,
3158 settings: &GlobalSettings,
3159 ) -> Result<(CrossSectionGraphTerm, GraphGenerationStats)> {
3160 CrossSectionGraphTerm::from_cross_section_graph(self, settings)
3161 }
3162}
3163
3164#[derive(Clone, Encode, Decode)]
3165#[trait_decode(trait = GammaLoopContext)]
3166pub struct CrossSectionDerivedData {
3167 pub orientations: Option<TiVec<OrientationID, EdgeVec<Orientation>>>,
3168 pub cut_paramatric_integrand: TiVec<CutGroupId, ParametricIntegrands>,
3169 pub global_cff_expression: Option<CFFExpression<OrientationID>>,
3170 pub lmbs: Option<TiVec<LmbIndex, LoopMomentumBasis>>,
3171 pub multi_channeling_setup: Option<LmbMultiChannelingSetup>,
3172 pub threshold_counterterms: TiVec<CutGroupId, LUCounterTermData>,
3173 pub threshold_candidate_esurface_ids: Vec<EsurfaceID>,
3176 pub cut_threshold_associations: TiVec<CutId, CutThresholdCountertermAssociations>,
3180 pub subspace_data: TiVec<CutGroupId, (SubspaceData, SubspaceData)>,
3181 pub cut_group_data: CutGroupData,
3182}
3183
3184#[derive(Clone, Encode, Decode, Debug)]
3186#[trait_decode(trait = GammaLoopContext)]
3187pub struct CutGroupData {
3188 pub cut_groups: TiVec<CutGroupId, CutGroup>,
3189 pub dual_shapes: Vec<Vec<Vec<usize>>>,
3190 pub pass_two_evaluators: Vec<GenericEvaluator>,
3191}
3192
3193#[derive(Clone, Encode, Decode, Debug)]
3195#[trait_decode(trait = GammaLoopContext)]
3196pub struct CutGroup {
3197 pub cuts: Vec<CutId>,
3198 pub related_esurface_group: RaisedEsurfaceGroup,
3199}
3200
3201impl Default for CutGroupData {
3202 fn default() -> Self {
3203 Self::new()
3204 }
3205}
3206
3207impl CutGroupData {
3208 pub fn new() -> Self {
3209 CutGroupData {
3210 cut_groups: TiVec::new(),
3211 dual_shapes: vec![],
3212 pass_two_evaluators: vec![],
3213 }
3214 }
3215
3216 pub fn new_from_esurface(
3217 raised_esurface_data: &RaisedEsurfaceData,
3218 cut_esurface_map: &TiVec<CutId, EsurfaceID>,
3219 evaluator_settings: &EvaluatorSettings,
3220 ) -> (Self, GraphGenerationStats) {
3221 let mut stats = GraphGenerationStats::default();
3222 let reversed_map = cut_esurface_map
3223 .iter_enumerated()
3224 .map(|(cut_id, &esurface_id)| (esurface_id, cut_id))
3225 .collect::<HashMap<EsurfaceID, CutId>>();
3226
3227 let mut groups = TiVec::new();
3228
3229 for (_raised_esurface_id, raised_esurface_group) in
3230 raised_esurface_data.raised_groups.iter_enumerated()
3231 {
3232 if cut_esurface_map.contains(&raised_esurface_group.esurface_ids[0]) {
3233 let cuts = raised_esurface_group
3234 .esurface_ids
3235 .iter()
3236 .map(|esurface_id| reversed_map[esurface_id])
3237 .collect::<Vec<_>>();
3238
3239 let cut_group = CutGroup {
3240 cuts,
3241 related_esurface_group: raised_esurface_group.clone(),
3242 };
3243
3244 groups.push(cut_group);
3245 } else {
3246 continue;
3247 }
3248 }
3249
3250 let global_max_occurence = groups
3251 .iter()
3252 .map(|group| group.related_esurface_group.max_occurence)
3253 .max()
3254 .unwrap_or_else(|| {
3255 println!("corrupted groups");
3256 panic!();
3257 });
3258
3259 let dual_shapes = (1..global_max_occurence)
3260 .map(simple_n_deriv_shape)
3261 .collect();
3262
3263 let pass_two_evaluators = (1..=global_max_occurence)
3264 .map(|i| {
3265 let evaluator_started = std::time::Instant::now();
3266 let evaluator = build_derivative_structure(i as u8, -1, evaluator_settings);
3267 stats.evaluator_symbolica_time += evaluator_started.elapsed();
3268 stats.evaluator_count += 1;
3269 evaluator
3270 })
3271 .collect();
3272
3273 (
3274 Self {
3275 cut_groups: groups,
3276 dual_shapes,
3277 pass_two_evaluators,
3278 },
3279 stats,
3280 )
3281 }
3282}
3283
3284impl CrossSectionDerivedData {
3285 fn new_empty() -> Self {
3286 Self {
3287 orientations: None,
3288 global_cff_expression: None,
3289 cut_paramatric_integrand: TiVec::new(),
3290 lmbs: None,
3291 multi_channeling_setup: None,
3292 threshold_counterterms: TiVec::new(),
3293 threshold_candidate_esurface_ids: Vec::new(),
3294 cut_threshold_associations: TiVec::new(),
3295 subspace_data: TiVec::new(),
3296 cut_group_data: CutGroupData::new(),
3297 }
3298 }
3299}
3300
3301pub(crate) fn build_derivative_structure_atom(
3302 singularity_order: u8,
3303 laurent_coefficient: i8,
3304) -> Atom {
3305 assert!(
3306 laurent_coefficient <= -1,
3307 "only laurent coefficients up to -1 are supported"
3308 );
3309
3310 assert!(
3311 singularity_order >= 1,
3312 "eta order must be at least 1, got {singularity_order}"
3313 );
3314 assert!(
3315 singularity_order >= -laurent_coefficient as u8,
3316 "eta order must be at least the negative of the laurent coefficient, got {singularity_order} for laurent coefficient {laurent_coefficient}"
3317 );
3318
3319 let order = singularity_order as i32;
3320 let laurent_coefficient = laurent_coefficient as i32;
3321 let f = symbol!("f");
3322
3323 let expansion = function!(GS.eta, GS.rescale)
3324 .series(GS.rescale, Atom::var(GS.rescale_star), (order, 1))
3325 .unwrap()
3326 .to_atom()
3327 .replace(function!(GS.eta, GS.rescale_star))
3328 .level_range((0, Some(0)))
3329 .with(0);
3330
3331 let mut expression_to_derive = function!(f, GS.rescale)
3332 * expansion.pow(-order)
3333 * (GS.rescale - GS.rescale_star).pow(order);
3334
3335 for _ in 1..=(order + laurent_coefficient) {
3336 expression_to_derive = expression_to_derive.derivative(GS.rescale);
3337 }
3338
3339 expression_to_derive = expression_to_derive
3340 .replace(GS.rescale - GS.rescale_star)
3341 .with(parse!("delta_t"));
3342
3343 let polynomial_in_delta_t = expression_to_derive
3344 .series(symbol!("delta_t"), Atom::num(0), (0, 1))
3345 .unwrap();
3346
3347 let factorial_prefactor = (2..=(order + laurent_coefficient)).product::<i32>();
3348 debug!("factorial prefactor: {}", factorial_prefactor);
3349 let mut expression_to_derive = polynomial_in_delta_t.to_atom() / Atom::num(factorial_prefactor);
3350
3351 expression_to_derive = expression_to_derive
3352 .replace(GS.rescale)
3353 .with(GS.rescale_star);
3354
3355 expression_to_derive
3356}
3357
3358pub(crate) fn build_derivative_structure(
3359 singularity_order: u8,
3360 laurent_coefficient: i8,
3361 evaluator_settings: &EvaluatorSettings,
3362) -> GenericEvaluator {
3363 let expression_to_derive =
3364 build_derivative_structure_atom(singularity_order, laurent_coefficient);
3365
3366 let params = params_for_derivative_order(singularity_order);
3367
3368 GenericEvaluator::new_from_raw_params(
3369 [expression_to_derive],
3370 ¶ms,
3371 &FunctionMap::default(),
3372 vec![],
3373 evaluator_settings.optimization_settings(),
3374 None,
3375 evaluator_settings,
3376 )
3377 .unwrap()
3378 .into_eager_only()
3379}
3380
3381fn ordered_f_derivative_params(
3382 base: Atom,
3383 derivative_vars: &[Symbol],
3384 derivative_shape: Option<&Vec<Vec<usize>>>,
3385) -> Vec<Atom> {
3386 derivative_shape
3387 .cloned()
3388 .unwrap_or_else(|| vec![vec![0; derivative_vars.len()]])
3389 .into_iter()
3390 .map(|orders| {
3391 debug_assert_eq!(orders.len(), derivative_vars.len());
3392
3393 let mut param = base.clone();
3394 for (derivative_var, order) in derivative_vars.iter().zip(orders) {
3395 for _ in 0..order {
3396 param = param.derivative(*derivative_var);
3397 }
3398 }
3399
3400 param
3401 })
3402 .collect()
3403}
3404
3405pub(crate) fn params_for_derivative_order(singularity_order: u8) -> Vec<Atom> {
3406 let f = symbol!("f");
3407
3408 let f_0 = function!(f, GS.rescale_star);
3409 let eta_1 = function!(GS.eta, GS.rescale_star).derivative(GS.rescale_star);
3410
3411 let f_derivative_shape = shape_from_cut_cff_index(&CutCFFIndex {
3412 left_threshold_order: None,
3413 right_threshold_order: None,
3414 lu_cut_order: Some(singularity_order as usize),
3415 });
3416
3417 let f_parameters =
3418 ordered_f_derivative_params(f_0, &[GS.rescale_star], f_derivative_shape.as_ref());
3419 let mut eta_params = vec![eta_1.clone()];
3420
3421 for _ in 2..=singularity_order {
3422 let next_eta = eta_params.last().unwrap().derivative(GS.rescale_star);
3423 eta_params.push(next_eta);
3424 }
3425
3426 let mut result = vec![];
3427 result.extend(f_parameters);
3428 result.extend(eta_params);
3429 result
3430}
3431
3432pub(crate) fn params_for_iterated_threshold_ct(
3433 left_singularit_order: u8,
3434 right_singularity_order: u8,
3435) -> Vec<Atom> {
3436 let f = symbol!("f");
3437
3438 let cut_cff_index = CutCFFIndex {
3439 left_threshold_order: Some(left_singularit_order as usize),
3440 right_threshold_order: Some(right_singularity_order as usize),
3441 lu_cut_order: None,
3442 };
3443 let f_derivative_shape = shape_from_cut_cff_index(&cut_cff_index);
3444
3445 let f_base = function!(f, GS.radius_star_left, GS.radius_star_right);
3446 let derivative_vars = match (left_singularit_order > 1, right_singularity_order > 1) {
3447 (true, true) => vec![GS.radius_star_left, GS.radius_star_right],
3448 (true, false) => vec![GS.radius_star_left],
3449 (false, true) => vec![GS.radius_star_right],
3450 (false, false) => vec![],
3451 };
3452
3453 let eta_left_d1 = function!(GS.eta_left, GS.radius_star_left).derivative(GS.radius_star_left);
3454 let eta_right_d1 =
3455 function!(GS.eta_right, GS.radius_star_right).derivative(GS.radius_star_right);
3456
3457 let mut eta_left_params = vec![eta_left_d1.clone()];
3458 let mut eta_right_params = vec![eta_right_d1.clone()];
3459
3460 for _ in 2..=left_singularit_order {
3461 let next_eta_left = eta_left_params
3462 .last()
3463 .unwrap()
3464 .derivative(GS.radius_star_left);
3465
3466 eta_left_params.push(next_eta_left);
3467 }
3468
3469 for _ in 2..=right_singularity_order {
3470 let next_eta_right = eta_right_params
3471 .last()
3472 .unwrap()
3473 .derivative(GS.radius_star_right);
3474
3475 eta_right_params.push(next_eta_right);
3476 }
3477
3478 let f_product =
3479 ordered_f_derivative_params(f_base, &derivative_vars, f_derivative_shape.as_ref());
3480
3481 let mut result = vec![];
3482 result.extend(f_product);
3483 result.extend(eta_left_params);
3484 result.extend(eta_right_params);
3485 result
3486}
3487
3488#[cfg(test)]
3489mod tests {
3490 use std::{
3491 fs,
3492 path::PathBuf,
3493 time::{SystemTime, UNIX_EPOCH},
3494 };
3495
3496 use symbolica::{atom::AtomCore, function, symbol};
3497
3498 use crate::{
3499 cff::CutCFFIndex, dot, graph::parse::from_dot::IntoGraph, initialisation::test_initialise,
3500 utils::GS,
3501 };
3502 use linnet::half_edge::{
3503 involution::EdgeIndex,
3504 subgraph::{OrientedCut, SuBitGraph},
3505 };
3506
3507 fn threshold_association(
3508 cut_boundary_size: usize,
3509 threshold_boundary_size: usize,
3510 ) -> super::ThresholdCountertermAssociation {
3511 super::ThresholdCountertermAssociation {
3512 esurface_id: crate::cff::esurface::EsurfaceID(0),
3513 cut_boundary_edges: (0..cut_boundary_size).map(EdgeIndex::from).collect(),
3514 threshold_boundary_edges: (0..threshold_boundary_size)
3515 .map(|index| EdgeIndex::from(cut_boundary_size + index))
3516 .collect(),
3517 invariant_bound_is_applicable: true,
3518 }
3519 }
3520
3521 #[test]
3522 fn singleton_boundary_classification_uses_generic_mass_hierarchy() {
3523 use super::ThresholdCountertermStatus;
3524 use crate::utils::F;
3525
3526 let association = threshold_association(1, 2);
3527 let tolerance = F(1.0e-12);
3528
3529 assert_eq!(
3530 association.classify_from_invariant_bounds(&F(3.0), &F(4.0), Some(F(3.0)), &tolerance,),
3531 ThresholdCountertermStatus::ProvenNonExisting,
3532 );
3533 assert_eq!(
3534 association.classify_from_invariant_bounds(&F(4.0), &F(4.0), Some(F(4.0)), &tolerance,),
3535 ThresholdCountertermStatus::AlwaysPinched,
3536 );
3537 assert_eq!(
3538 association.classify_from_invariant_bounds(&F(5.0), &F(4.0), Some(F(5.0)), &tolerance,),
3539 ThresholdCountertermStatus::PotentiallyExisting,
3540 );
3541
3542 let fixed_association = threshold_association(1, 1);
3543 assert_eq!(
3544 fixed_association.classify_from_invariant_bounds(
3545 &F(3.0),
3546 &F(4.0),
3547 Some(F(3.0)),
3548 &tolerance,
3549 ),
3550 ThresholdCountertermStatus::ProvenNonExisting,
3551 );
3552 assert_eq!(
3553 fixed_association.classify_from_invariant_bounds(
3554 &F(5.0),
3555 &F(4.0),
3556 Some(F(5.0)),
3557 &tolerance,
3558 ),
3559 ThresholdCountertermStatus::ProvenNonExisting,
3560 );
3561 }
3562
3563 #[test]
3564 fn current_model_filter_is_controlled_only_by_generation_setting() {
3565 use super::ThresholdCountertermStatus;
3566
3567 assert!(ThresholdCountertermStatus::ProvenNonExisting.is_eligible_for_generation(false));
3568 assert!(!ThresholdCountertermStatus::ProvenNonExisting.is_eligible_for_generation(true));
3569 assert!(!ThresholdCountertermStatus::AlwaysPinched.is_eligible_for_generation(false));
3570 assert!(!ThresholdCountertermStatus::NoRadialDependence.is_eligible_for_generation(false));
3571 assert!(ThresholdCountertermStatus::PotentiallyExisting.is_eligible_for_generation(true));
3572 }
3573
3574 #[test]
3575 fn multiparticle_boundary_classification_is_conservative() {
3576 use super::ThresholdCountertermStatus;
3577 use crate::utils::F;
3578
3579 let association = threshold_association(2, 2);
3580 let tolerance = F(1.0e-12);
3581
3582 assert_eq!(
3583 association.classify_from_invariant_bounds(&F(4.0), &F(4.0), Some(F(8.0)), &tolerance,),
3584 ThresholdCountertermStatus::CanBecomePinched,
3585 );
3586 assert_eq!(
3587 association.classify_from_invariant_bounds(&F(3.0), &F(5.0), Some(F(4.0)), &tolerance,),
3588 ThresholdCountertermStatus::ProvenNonExisting,
3589 );
3590 assert_eq!(
3591 association.classify_from_invariant_bounds(&F(3.0), &F(5.0), Some(F(5.0)), &tolerance,),
3592 ThresholdCountertermStatus::AlwaysPinched,
3593 );
3594 assert_eq!(
3595 association.classify_from_invariant_bounds(&F(3.0), &F(5.0), Some(F(8.0)), &tolerance,),
3596 ThresholdCountertermStatus::CanBecomePinched,
3597 );
3598 assert_eq!(
3599 association.classify_from_invariant_bounds(&F(5.0), &F(3.0), Some(F(8.0)), &tolerance,),
3600 ThresholdCountertermStatus::PotentiallyExisting,
3601 );
3602 assert_eq!(
3603 association.classify_from_invariant_bounds(&F(3.0), &F(5.0), None, &tolerance,),
3604 ThresholdCountertermStatus::PotentiallyExisting,
3605 );
3606 }
3607
3608 #[test]
3609 fn invariant_bound_rejects_mixed_or_unresolved_inputs_conservatively() {
3610 use super::ThresholdCountertermStatus;
3611 use crate::utils::F;
3612
3613 let mut association = threshold_association(2, 2);
3614 let tolerance = F(1.0e-12);
3615 association.invariant_bound_is_applicable = false;
3616 assert_eq!(
3617 association.classify_from_invariant_bounds(&F(3.0), &F(5.0), Some(F(4.0)), &tolerance,),
3618 ThresholdCountertermStatus::PotentiallyExisting,
3619 );
3620
3621 association.invariant_bound_is_applicable = true;
3622 assert_eq!(
3623 association.classify_from_invariant_bounds(
3624 &F(f64::NAN),
3625 &F(5.0),
3626 Some(F(4.0)),
3627 &tolerance,
3628 ),
3629 ThresholdCountertermStatus::PotentiallyExisting,
3630 );
3631
3632 let masses: linnet::half_edge::involution::EdgeVec<Option<F<f64>>> =
3633 vec![Some(F(3.0)), None].into();
3634 assert_eq!(
3635 super::ThresholdCountertermAssociation::mass_sum(
3636 &[EdgeIndex::from(0), EdgeIndex::from(1)],
3637 &masses,
3638 ),
3639 None,
3640 );
3641 }
3642
3643 #[test]
3644 fn singleton_threshold_must_lie_strictly_inside_available_interval() {
3645 use super::ThresholdCountertermStatus;
3646 use crate::utils::F;
3647
3648 let association = threshold_association(2, 1);
3649 let tolerance = F(1.0e-12);
3650
3651 assert_eq!(
3652 association.classify_from_invariant_bounds(&F(5.0), &F(4.0), Some(F(9.0)), &tolerance,),
3653 ThresholdCountertermStatus::ProvenNonExisting,
3654 );
3655 assert_eq!(
3656 association.classify_from_invariant_bounds(&F(3.0), &F(5.0), Some(F(9.0)), &tolerance,),
3657 ThresholdCountertermStatus::PotentiallyExisting,
3658 );
3659 assert_eq!(
3660 association.classify_from_invariant_bounds(&F(3.0), &F(9.0), Some(F(9.0)), &tolerance,),
3661 ThresholdCountertermStatus::AlwaysPinched,
3662 );
3663 }
3664
3665 fn fresh_temp_dir(name: &str) -> PathBuf {
3666 let unique = SystemTime::now()
3667 .duration_since(UNIX_EPOCH)
3668 .unwrap()
3669 .as_nanos();
3670 let path = std::env::temp_dir().join(format!(
3671 "gammalooprs-{name}-{}-{unique}",
3672 std::process::id()
3673 ));
3674 fs::create_dir_all(&path).unwrap();
3675 path
3676 }
3677
3678 #[test]
3679 fn cross_section_storage_path_stays_inside_process_folder() {
3680 let temp = fresh_temp_dir("cross-section-storage-path");
3681 let cross_section = super::CrossSection::new("NLO".to_string());
3682
3683 assert_eq!(cross_section.storage_path(&temp), temp.join("NLO"));
3684 fs::remove_dir_all(temp).unwrap();
3685 }
3686
3687 #[test]
3688 fn cross_section_lmb_cut_edge_exclusion_prefers_massive_then_fermion_then_id() {
3689 test_initialise().unwrap();
3690 let graph = dot!(
3691 digraph cut_edge_priority {
3692 edge [num=1]
3693 node [num=1]
3694 A -> B [id=0 particle="g"]
3695 A -> B [id=1 particle="d"]
3696 A -> B [id=2 particle="t" mass=1]
3697 A -> B [id=3 particle="a"]
3698 A -> B [id=4 mass=0]
3699 }
3700 )
3701 .unwrap();
3702
3703 assert_eq!(
3704 super::CrossSectionGraph::excluded_cut_edge_for_lmb_channel_in(
3705 &graph,
3706 &[EdgeIndex::from(0), EdgeIndex::from(1), EdgeIndex::from(2)]
3707 ),
3708 Some(EdgeIndex::from(2))
3709 );
3710 assert_eq!(
3711 super::CrossSectionGraph::excluded_cut_edge_for_lmb_channel_in(
3712 &graph,
3713 &[EdgeIndex::from(0), EdgeIndex::from(1), EdgeIndex::from(3)]
3714 ),
3715 Some(EdgeIndex::from(1))
3716 );
3717 assert_eq!(
3718 super::CrossSectionGraph::excluded_cut_edge_for_lmb_channel_in(
3719 &graph,
3720 &[EdgeIndex::from(0), EdgeIndex::from(3)]
3721 ),
3722 Some(EdgeIndex::from(0))
3723 );
3724
3725 assert_eq!(
3729 graph.underlying[EdgeIndex::from(2)]
3730 .particle
3731 .mass_atom()
3732 .to_string(),
3733 "1"
3734 );
3735 let empty: SuBitGraph = graph.underlying.empty_subgraph();
3736 let cut = super::CrossSectionCut {
3737 cut: OrientedCut {
3738 left: empty.clone(),
3739 right: empty.clone(),
3740 },
3741 left: empty.clone(),
3742 right: empty,
3743 };
3744 let association = super::ThresholdCountertermAssociation {
3745 esurface_id: crate::cff::esurface::EsurfaceID(0),
3746 cut_boundary_edges: vec![EdgeIndex::from(4)],
3747 threshold_boundary_edges: vec![EdgeIndex::from(2)],
3748 invariant_bound_is_applicable: true,
3749 };
3750 assert_eq!(
3751 association.classify_for_model(
3752 &graph,
3753 &cut,
3754 &crate::model::Model::default(),
3755 &graph.param_builder,
3756 &crate::settings::RuntimeSettings::default(),
3757 1.0e-7,
3758 ),
3759 super::ThresholdCountertermStatus::ProvenNonExisting,
3760 );
3761 }
3762
3763 #[test]
3764 fn iterated_counterterms_use_the_right_threshold_count_as_row_stride() {
3765 let two_by_three = super::IteratedCtCollection::new((0..6).collect(), 2, 3);
3766 assert_eq!(
3767 two_by_three[(
3768 super::LeftThresholdId::from(0),
3769 super::RightThresholdId::from(0)
3770 )],
3771 0
3772 );
3773 assert_eq!(
3774 two_by_three[(
3775 super::LeftThresholdId::from(0),
3776 super::RightThresholdId::from(2)
3777 )],
3778 2
3779 );
3780 assert_eq!(
3781 two_by_three[(
3782 super::LeftThresholdId::from(1),
3783 super::RightThresholdId::from(0)
3784 )],
3785 3
3786 );
3787 assert_eq!(
3788 two_by_three[(
3789 super::LeftThresholdId::from(1),
3790 super::RightThresholdId::from(2)
3791 )],
3792 5
3793 );
3794
3795 let three_by_two = super::IteratedCtCollection::new((0..6).collect(), 3, 2);
3796 assert_eq!(
3797 three_by_two[(
3798 super::LeftThresholdId::from(0),
3799 super::RightThresholdId::from(1)
3800 )],
3801 1
3802 );
3803 assert_eq!(
3804 three_by_two[(
3805 super::LeftThresholdId::from(1),
3806 super::RightThresholdId::from(0)
3807 )],
3808 2
3809 );
3810 assert_eq!(
3811 three_by_two[(
3812 super::LeftThresholdId::from(2),
3813 super::RightThresholdId::from(1)
3814 )],
3815 5
3816 );
3817 }
3818
3819 #[test]
3820 fn max_dual_size_for_cut_cff_indices_tracks_mixed_threshold_shapes() {
3821 let cut_cff_indices = [
3822 CutCFFIndex {
3823 left_threshold_order: None,
3824 right_threshold_order: None,
3825 lu_cut_order: Some(2),
3826 },
3827 CutCFFIndex {
3828 left_threshold_order: Some(2),
3829 right_threshold_order: Some(2),
3830 lu_cut_order: Some(2),
3831 },
3832 ];
3833
3834 assert_eq!(
3835 super::max_dual_size_for_cut_cff_indices(cut_cff_indices.iter()),
3836 8
3837 );
3838 }
3839
3840 #[test]
3841 fn single_threshold_params_follow_effective_f_then_eta_contract() {
3842 let params = super::params_for_derivative_order(3);
3843 let f = symbol!("f");
3844 let f_base = function!(f, GS.rescale_star);
3845 let eta_base = function!(GS.eta, GS.rescale_star);
3846
3847 let expected = [
3848 f_base.clone(),
3849 f_base.clone().derivative(GS.rescale_star),
3850 f_base
3851 .derivative(GS.rescale_star)
3852 .derivative(GS.rescale_star),
3853 eta_base.clone().derivative(GS.rescale_star),
3854 eta_base
3855 .clone()
3856 .derivative(GS.rescale_star)
3857 .derivative(GS.rescale_star),
3858 eta_base
3859 .derivative(GS.rescale_star)
3860 .derivative(GS.rescale_star)
3861 .derivative(GS.rescale_star),
3862 ]
3863 .into_iter()
3864 .map(|atom| atom.to_string())
3865 .collect::<Vec<_>>();
3866
3867 let actual = params
3868 .iter()
3869 .map(|atom| atom.to_string())
3870 .collect::<Vec<_>>();
3871
3872 assert_eq!(actual, expected);
3873 }
3874
3875 #[test]
3876 fn iterated_threshold_f_params_follow_mixed_left_right_shape_order() {
3877 let params = super::params_for_iterated_threshold_ct(2, 2);
3878 let f = symbol!("f");
3879 let base = function!(f, GS.radius_star_left, GS.radius_star_right);
3880 let expected = [
3881 base.clone(),
3882 base.clone().derivative(GS.radius_star_left),
3883 base.clone().derivative(GS.radius_star_right),
3884 base.derivative(GS.radius_star_left)
3885 .derivative(GS.radius_star_right),
3886 ]
3887 .into_iter()
3888 .map(|atom| atom.to_string())
3889 .collect::<Vec<_>>();
3890 let actual = params[..expected.len()]
3891 .iter()
3892 .map(|atom| atom.to_string())
3893 .collect::<Vec<_>>();
3894
3895 assert_eq!(actual, expected);
3896 }
3897
3898 #[test]
3899 fn iterated_threshold_params_append_left_then_right_eta_families() {
3900 let params = super::params_for_iterated_threshold_ct(2, 2);
3901
3902 let expected = [
3903 function!(GS.eta_left, GS.radius_star_left).derivative(GS.radius_star_left),
3904 function!(GS.eta_left, GS.radius_star_left)
3905 .derivative(GS.radius_star_left)
3906 .derivative(GS.radius_star_left),
3907 function!(GS.eta_right, GS.radius_star_right).derivative(GS.radius_star_right),
3908 function!(GS.eta_right, GS.radius_star_right)
3909 .derivative(GS.radius_star_right)
3910 .derivative(GS.radius_star_right),
3911 ]
3912 .into_iter()
3913 .map(|atom| atom.to_string())
3914 .collect::<Vec<_>>();
3915
3916 let actual = params[4..]
3917 .iter()
3918 .map(|atom| atom.to_string())
3919 .collect::<Vec<_>>();
3920
3921 assert_eq!(actual, expected);
3922 }
3923
3924 #[test]
3925 fn iterated_threshold_helper_atom_matches_its_left_right_parameter_families() {
3926 test_initialise().unwrap();
3927
3928 let mut fn_map = super::FunctionMap::new();
3929 fn_map
3930 .add_aliases([(
3931 GS.pi.into(),
3932 super::Atom::num(super::Rational::try_from(std::f64::consts::PI).unwrap()),
3933 )])
3934 .unwrap();
3935
3936 for left_order in 1..=3 {
3937 for right_order in 1..=3 {
3938 let atom = super::CrossSectionGraph::iterated_th_prefactor_helper_atom(
3939 left_order,
3940 right_order,
3941 1,
3942 1,
3943 true,
3944 );
3945 let params = super::CrossSectionGraph::iterated_th_prefactor_helper_params(
3946 left_order,
3947 right_order,
3948 );
3949
3950 super::GenericEvaluator::new_from_raw_params(
3951 [atom],
3952 ¶ms,
3953 &fn_map,
3954 vec![],
3955 super::OptimizationSettings::default(),
3956 None,
3957 &super::EvaluatorSettings::default(),
3958 )
3959 .unwrap_or_else(|error| {
3960 panic!(
3961 "iterated {left_order}x{right_order} threshold helper atom and parameter families must agree: {error}"
3962 )
3963 });
3964 }
3965 }
3966 }
3967
3968 #[test]
3969 fn iterated_threshold_f_params_follow_active_single_axis_shape_order() {
3970 let params = super::params_for_iterated_threshold_ct(1, 3);
3971 let f = symbol!("f");
3972 let base = function!(f, GS.radius_star_left, GS.radius_star_right);
3973 let expected = [
3974 base.clone(),
3975 base.clone().derivative(GS.radius_star_right),
3976 base.derivative(GS.radius_star_right)
3977 .derivative(GS.radius_star_right),
3978 ]
3979 .into_iter()
3980 .map(|atom| atom.to_string())
3981 .collect::<Vec<_>>();
3982 let actual = params[..expected.len()]
3983 .iter()
3984 .map(|atom| atom.to_string())
3985 .collect::<Vec<_>>();
3986
3987 assert_eq!(actual, expected);
3988 }
3989}