Skip to main content

gammaloop_api/
integrand_info.rs

1use std::fmt;
2
3use color_eyre::Result;
4use eyre::{eyre, Context};
5use gammalooprs::{
6    cff::esurface::EsurfaceExistenceStatus,
7    graph::{FeynmanGraph, Graph},
8    integrands::process::{ActiveF64Backend, LmbMultiChannelingSetup, ParamBuilder},
9    model::Model,
10    processes::{
11        Amplitude, CrossSection, CrossSectionCut, CutId, ProcessCollection,
12        ThresholdCountertermAssociation, ThresholdCountertermStatus,
13    },
14    settings::{global::FrozenCompilationMode, runtime::ParameterizationSettings, RuntimeSettings},
15    utils::F,
16    DependentMomentaConstructor,
17};
18use linnet::half_edge::involution::{EdgeVec, Orientation};
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21
22use crate::state::State;
23
24#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
25#[serde(rename_all = "snake_case")]
26pub enum IntegrandKind {
27    Amplitude,
28    CrossSection,
29}
30
31impl fmt::Display for IntegrandKind {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Self::Amplitude => f.write_str("amplitude"),
35            Self::CrossSection => f.write_str("cross section"),
36        }
37    }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
41pub struct IntegrandGraphInfo {
42    pub graph_id: usize,
43    pub name: String,
44    pub is_master: bool,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
48pub struct IntegrandOrientationInfo {
49    pub orientation_id: usize,
50    pub signature: Vec<i8>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
54pub struct IntegrandLoopMomentumBasisInfo {
55    pub basis_id: usize,
56    pub channel_id: Option<usize>,
57    pub edge_ids: Vec<usize>,
58    pub matches_generation_basis: bool,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
62pub struct IntegrandCutInfo {
63    pub cut_id: usize,
64    pub edge_ids: Vec<usize>,
65    pub raising_power: usize,
66    pub left_thresholds: Vec<IntegrandCutThresholdInfo>,
67    pub right_thresholds: Vec<IntegrandCutThresholdInfo>,
68}
69
70#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
71#[serde(rename_all = "snake_case")]
72pub enum IntegrandThresholdStatus {
73    NoRadialDependence,
74    AlwaysPinched,
75    CanBecomePinched,
76    ProvenNonExisting,
77    PotentiallyExisting,
78}
79
80impl IntegrandThresholdStatus {
81    pub fn as_str(self) -> &'static str {
82        match self {
83            Self::NoRadialDependence => "no_radial_dependence",
84            Self::AlwaysPinched => "always_pinched",
85            Self::CanBecomePinched => "can_become_pinched",
86            Self::ProvenNonExisting => "proven_non_existing",
87            Self::PotentiallyExisting => "potentially_existing",
88        }
89    }
90
91    pub fn is_currently_viable(self) -> bool {
92        matches!(self, Self::CanBecomePinched | Self::PotentiallyExisting)
93    }
94
95    pub fn can_become_pinched(self) -> bool {
96        self == Self::CanBecomePinched
97    }
98}
99
100impl From<ThresholdCountertermStatus> for IntegrandThresholdStatus {
101    fn from(value: ThresholdCountertermStatus) -> Self {
102        match value {
103            ThresholdCountertermStatus::NoRadialDependence => Self::NoRadialDependence,
104            ThresholdCountertermStatus::AlwaysPinched => Self::AlwaysPinched,
105            ThresholdCountertermStatus::CanBecomePinched => Self::CanBecomePinched,
106            ThresholdCountertermStatus::ProvenNonExisting => Self::ProvenNonExisting,
107            ThresholdCountertermStatus::PotentiallyExisting => Self::PotentiallyExisting,
108        }
109    }
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
113pub struct IntegrandCutThresholdInfo {
114    pub esurface_id: usize,
115    pub status: IntegrandThresholdStatus,
116    pub cut_boundary_edge_ids: Vec<usize>,
117    pub threshold_boundary_edge_ids: Vec<usize>,
118    pub invariant_bound_is_applicable: bool,
119}
120
121impl IntegrandCutThresholdInfo {
122    fn from_association(
123        association: &ThresholdCountertermAssociation,
124        graph: &Graph,
125        cut: &CrossSectionCut,
126        model: &Model,
127        param_builder: &ParamBuilder,
128        settings: &RuntimeSettings,
129    ) -> Self {
130        Self {
131            esurface_id: association.esurface_id.0,
132            status: association
133                .classify_for_model(
134                    graph,
135                    cut,
136                    model,
137                    param_builder,
138                    settings,
139                    settings.subtraction.esurface_existence_threshold,
140                )
141                .into(),
142            cut_boundary_edge_ids: association
143                .cut_boundary_edges
144                .iter()
145                .map(|edge_id| edge_id.0)
146                .collect(),
147            threshold_boundary_edge_ids: association
148                .threshold_boundary_edges
149                .iter()
150                .map(|edge_id| edge_id.0)
151                .collect(),
152            invariant_bound_is_applicable: association.invariant_bound_is_applicable,
153        }
154    }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
158pub struct IntegrandActiveThresholdCutInfo {
159    pub cut_id: usize,
160    pub can_become_pinched: bool,
161}
162
163#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
164#[serde(rename_all = "snake_case")]
165pub enum IntegrandEsurfaceClassification {
166    NonExisting,
167    Pinched,
168    Existing,
169}
170
171impl IntegrandEsurfaceClassification {
172    pub fn as_str(self) -> &'static str {
173        match self {
174            Self::NonExisting => "non_existing",
175            Self::Pinched => "pinched",
176            Self::Existing => "existing",
177        }
178    }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
182pub struct IntegrandThresholdEsurfaceInfo {
183    pub esurface_id: usize,
184    pub representative_graph_id: usize,
185    pub edge_ids: Vec<usize>,
186    pub classification: Option<IntegrandEsurfaceClassification>,
187    pub active_cuts: Vec<IntegrandActiveThresholdCutInfo>,
188}
189
190impl IntegrandCutInfo {
191    fn from_cross_section_cut(
192        graph_term: &gammalooprs::integrands::process::cross_section::CrossSectionGraphTerm,
193        cut_id: CutId,
194        cut: &CrossSectionCut,
195        raising_power: usize,
196        model: &Model,
197        param_builder: &ParamBuilder,
198        settings: &RuntimeSettings,
199    ) -> Self {
200        let associations = &graph_term.cut_threshold_associations[cut_id];
201        let threshold_info = |association| {
202            IntegrandCutThresholdInfo::from_association(
203                association,
204                &graph_term.graph,
205                cut,
206                model,
207                param_builder,
208                settings,
209            )
210        };
211
212        Self {
213            // Retain `CutId` for internal indexing and convert only at the API boundary.
214            cut_id: usize::from(cut_id),
215            edge_ids: cut_edge_ids(&graph_term.graph, cut),
216            raising_power,
217            left_thresholds: associations.left.iter().map(&threshold_info).collect(),
218            right_thresholds: associations.right.iter().map(threshold_info).collect(),
219        }
220    }
221
222    fn active_threshold_cut(&self, esurface_id: usize) -> Option<IntegrandActiveThresholdCutInfo> {
223        let mut is_currently_viable = false;
224        let mut can_become_pinched = false;
225
226        for threshold in self
227            .left_thresholds
228            .iter()
229            .chain(self.right_thresholds.iter())
230            .filter(|threshold| threshold.esurface_id == esurface_id)
231        {
232            is_currently_viable |= threshold.status.is_currently_viable();
233            can_become_pinched |= threshold.status.can_become_pinched();
234        }
235
236        is_currently_viable.then_some(IntegrandActiveThresholdCutInfo {
237            cut_id: self.cut_id,
238            can_become_pinched,
239        })
240    }
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
244pub struct IntegrandGraphGroupInfo {
245    pub group_id: usize,
246    pub graphs: Vec<IntegrandGraphInfo>,
247    pub orientation_edge_ids: Vec<usize>,
248    pub orientations: Vec<IntegrandOrientationInfo>,
249    pub loop_momentum_bases: Vec<IntegrandLoopMomentumBasisInfo>,
250    pub threshold_esurface_ids: Vec<usize>,
251    pub threshold_esurfaces: Vec<IntegrandThresholdEsurfaceInfo>,
252    pub cuts: Vec<IntegrandCutInfo>,
253}
254
255fn threshold_esurface_edge_ids(
256    esurfaces: &gammalooprs::cff::esurface::EsurfaceCollection,
257    esurface_id: usize,
258) -> Vec<usize> {
259    esurfaces
260        .iter()
261        .nth(esurface_id)
262        .expect("threshold esurface id should resolve in collection")
263        .energies
264        .iter()
265        .map(|edge_id| edge_id.0)
266        .collect()
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
270pub struct IntegrandInfo {
271    /// Numeric identifier of the process that owns the integrand.
272    pub process_id: usize,
273    /// Persistent folder name of the owning process.
274    pub process_name: String,
275    /// Canonical name of the resolved integrand.
276    pub integrand_name: String,
277    /// Whether the integrand represents an amplitude or cross section.
278    pub kind: IntegrandKind,
279    /// Compilation mode frozen into the generated integrand.
280    pub generation_compilation: FrozenCompilationMode,
281    /// Floating-point evaluator backend currently active for the integrand.
282    pub active_f64_backend: ActiveF64Backend,
283    /// Number of amplitude graphs or cross-section supergraphs.
284    pub graph_count: usize,
285    /// Number of graph groups in the generated integrand.
286    pub graph_group_count: usize,
287    /// Bincode size of the saved integrand record, excluding its compiled evaluator.
288    pub record_size_bytes: usize,
289    /// Per-group graphs, orientations, loop-momentum bases, thresholds, and cuts.
290    pub graph_groups: Vec<IntegrandGraphGroupInfo>,
291}
292
293pub(crate) fn collect_integrand_info(
294    state: &State,
295    process_id: usize,
296    integrand_name: &str,
297) -> Result<IntegrandInfo> {
298    let process = &state.process_list.processes[process_id];
299    let resolved = process.get_integrand(integrand_name)?;
300    let generated = resolved.require_generated()?;
301    let generation_compilation = generated.frozen_compilation().clone();
302    let active_f64_backend = generated.active_f64_backend();
303
304    match (generated, &process.collection) {
305        (
306            gammalooprs::integrands::process::ProcessIntegrand::Amplitude(integrand),
307            ProcessCollection::Amplitudes(amplitudes),
308        ) => {
309            let amplitude = amplitudes.get(&resolved.canonical_name).ok_or_else(|| {
310                eyre!(
311                    "Could not resolve amplitude record '{}' in process #{} ({})",
312                    resolved.canonical_name,
313                    process.definition.process_id,
314                    process.definition.folder_name
315                )
316            })?;
317            let model = state.resolve_model_for_integrand(
318                process.definition.process_id,
319                &resolved.canonical_name,
320            )?;
321            Ok(IntegrandInfo {
322                process_id: process.definition.process_id,
323                process_name: process.definition.folder_name.clone(),
324                integrand_name: resolved.canonical_name,
325                kind: IntegrandKind::Amplitude,
326                generation_compilation: generation_compilation.clone(),
327                active_f64_backend,
328                graph_count: amplitude.graphs.len(),
329                graph_group_count: amplitude.graph_group_structure.len(),
330                record_size_bytes: integrand_record_size_from_amplitude(amplitude)?,
331                graph_groups: amplitude_graph_groups(integrand, &model)?,
332            })
333        }
334        (
335            gammalooprs::integrands::process::ProcessIntegrand::CrossSection(integrand),
336            ProcessCollection::CrossSections(cross_sections),
337        ) => {
338            let cross_section = cross_sections
339                .get(&resolved.canonical_name)
340                .ok_or_else(|| {
341                    eyre!(
342                        "Could not resolve cross-section record '{}' in process #{} ({})",
343                        resolved.canonical_name,
344                        process.definition.process_id,
345                        process.definition.folder_name
346                    )
347                })?;
348            let model = state.resolve_model_for_integrand(
349                process.definition.process_id,
350                &resolved.canonical_name,
351            )?;
352            Ok(IntegrandInfo {
353                process_id: process.definition.process_id,
354                process_name: process.definition.folder_name.clone(),
355                integrand_name: resolved.canonical_name,
356                kind: IntegrandKind::CrossSection,
357                generation_compilation,
358                active_f64_backend,
359                graph_count: cross_section.supergraphs.len(),
360                graph_group_count: cross_section.graph_group_structure.len(),
361                record_size_bytes: integrand_record_size_from_cross_section(cross_section)?,
362                graph_groups: cross_section_graph_groups(integrand, &model)?,
363            })
364        }
365        _ => Err(eyre!(
366            "Process/integrand type mismatch for '{}' in process #{} ({})",
367            resolved.canonical_name,
368            process.definition.process_id,
369            process.definition.folder_name
370        )),
371    }
372}
373
374fn orientation_signature(orientation: &EdgeVec<Orientation>) -> Vec<i8> {
375    orientation
376        .iter()
377        .map(|(_, orientation)| match *orientation {
378            Orientation::Default => 1,
379            Orientation::Reversed => -1,
380            Orientation::Undirected => 0,
381        })
382        .collect()
383}
384
385fn orientation_edge_ids(orientation: &EdgeVec<Orientation>) -> Vec<usize> {
386    orientation.iter().map(|(edge_id, _)| edge_id.0).collect()
387}
388
389fn cut_edge_ids(graph: &Graph, cut: &CrossSectionCut) -> Vec<usize> {
390    cut.cut
391        .iter_edges(&graph.underlying)
392        .map(|(_, edge)| {
393            graph
394                .underlying
395                .iter_edges()
396                .find_map(|(_, edge_id, edge_data)| {
397                    (edge_data.data.name == edge.data.name).then_some(edge_id.0)
398                })
399                .expect("cut edge should resolve in graph")
400        })
401        .collect()
402}
403
404fn cut_raising_powers(
405    graph: &gammalooprs::integrands::process::cross_section::CrossSectionGraphTerm,
406) -> typed_index_collections::TiVec<CutId, usize> {
407    let mut raising_powers: typed_index_collections::TiVec<CutId, usize> =
408        vec![1; graph.cuts.len()].into();
409    for cut_group in graph.cut_group_data.cut_groups.iter() {
410        let raising_power = cut_group.related_esurface_group.max_occurence;
411        for cut_id in &cut_group.cuts {
412            raising_powers[*cut_id] = raising_power;
413        }
414    }
415    raising_powers
416}
417
418fn lmb_channel_ids(
419    lmbs: &typed_index_collections::TiVec<
420        gammalooprs::graph::LmbIndex,
421        gammalooprs::graph::LoopMomentumBasis,
422    >,
423    multi_channeling_setup: &LmbMultiChannelingSetup,
424    graph_name: &str,
425    parameterization_settings: &ParameterizationSettings,
426) -> Result<Vec<Option<usize>>> {
427    let mut channel_ids = vec![None; lmbs.len()];
428    for (channel_id, lmb_index) in multi_channeling_setup
429        .effective_channels(graph_name, parameterization_settings)?
430        .into_iter()
431        .enumerate()
432    {
433        channel_ids[usize::from(lmb_index)] = Some(channel_id);
434    }
435    Ok(channel_ids)
436}
437
438fn amplitude_graph_groups(
439    integrand: &gammalooprs::integrands::process::amplitude::AmplitudeIntegrand,
440    model: &Model,
441) -> Result<Vec<IntegrandGraphGroupInfo>> {
442    let parameterization_settings = integrand
443        .settings
444        .sampling
445        .get_parameterization_settings()
446        .unwrap_or_default();
447    let external_momenta = integrand
448        .settings
449        .kinematics
450        .externals
451        .get_dependent_externals::<f64>(DependentMomentaConstructor::Amplitude(
452            &integrand.data.external_signature,
453        ))
454        .context("While constructing amplitude external momenta for integrand information")?;
455    let e_cm = F(integrand.settings.kinematics.e_cm);
456    let existence_tolerance = F(integrand.settings.subtraction.esurface_existence_threshold);
457    let real_mass_vectors = integrand
458        .data
459        .graph_terms
460        .iter()
461        .map(|graph_term| graph_term.graph.get_real_mass_vector::<f64>(model))
462        .collect::<Vec<_>>();
463    integrand
464        .data
465        .graph_group_structure
466        .iter_enumerated()
467        .map(|(group_id, group)| {
468            let master_graph_id = group
469                .into_iter()
470                .next()
471                .expect("graph group should not be empty");
472            let master_graph = &integrand.data.graph_terms[master_graph_id];
473            let channel_ids = lmb_channel_ids(
474                &master_graph.lmbs,
475                &master_graph.multi_channeling_setup,
476                &master_graph.graph.name,
477                &parameterization_settings,
478            )?;
479            // Amplitude overlap centers are solved from the canonical union across all members
480            // of the graph group. Report that same union rather than the master graph's local
481            // raised-surface numbering.
482            let threshold_esurfaces = integrand.data.group_derived_data[group_id]
483                .esurface_map
484                .iter_enumerated()
485                .filter_map(|(group_esurface_id, raised_esurface_map)| {
486                    let (representative_graph_id, raised_esurface_id) = raised_esurface_map
487                        .iter_enumerated()
488                        .filter_map(|(graph_group_position, raised_esurface_id)| {
489                            raised_esurface_id.map(|raised_esurface_id| {
490                                let graph_id = group[graph_group_position];
491                                (graph_id, raised_esurface_id)
492                            })
493                        })
494                        .find(|(graph_id, raised_esurface_id)| {
495                            integrand.data.graph_terms[*graph_id]
496                                .threshold_counterterm
497                                .generated_mask[*raised_esurface_id]
498                        })?;
499                    let graph_term = &integrand.data.graph_terms[representative_graph_id];
500                    let local_esurface_id =
501                        graph_term.threshold_counterterm.raised_data.raised_groups
502                            [raised_esurface_id]
503                            .esurface_ids[0];
504
505                    let mut classification = IntegrandEsurfaceClassification::NonExisting;
506                    for (graph_group_position, raised_esurface_id) in raised_esurface_map
507                        .iter_enumerated()
508                        .filter_map(|(graph_group_position, raised_esurface_id)| {
509                            raised_esurface_id.map(|raised_esurface_id| {
510                                (graph_group_position, raised_esurface_id)
511                            })
512                        })
513                    {
514                        let graph_id = group[graph_group_position];
515                        let candidate = &integrand.data.graph_terms[graph_id];
516                        let candidate_esurface_id =
517                            candidate.threshold_counterterm.raised_data.raised_groups
518                                [raised_esurface_id]
519                                .esurface_ids[0];
520                        let candidate_status = candidate.esurfaces[candidate_esurface_id]
521                            .existence_status(
522                                &external_momenta,
523                                &candidate.graph.loop_momentum_basis,
524                                &real_mass_vectors[graph_id],
525                                &e_cm,
526                                &existence_tolerance,
527                            );
528                        match candidate_status {
529                            EsurfaceExistenceStatus::Existing => {
530                                classification = IntegrandEsurfaceClassification::Existing;
531                                break;
532                            }
533                            EsurfaceExistenceStatus::Pinched => {
534                                classification = IntegrandEsurfaceClassification::Pinched;
535                            }
536                            EsurfaceExistenceStatus::NonExisting => {}
537                        }
538                    }
539
540                    Some(IntegrandThresholdEsurfaceInfo {
541                        esurface_id: group_esurface_id.0,
542                        representative_graph_id,
543                        edge_ids: threshold_esurface_edge_ids(
544                            &graph_term.esurfaces,
545                            local_esurface_id.0,
546                        ),
547                        classification: Some(classification),
548                        active_cuts: Vec::new(),
549                    })
550                })
551                .collect::<Vec<_>>();
552            let threshold_esurface_ids = threshold_esurfaces
553                .iter()
554                .map(|threshold| threshold.esurface_id)
555                .collect();
556            Ok(IntegrandGraphGroupInfo {
557                group_id: usize::from(group_id),
558                graphs: group
559                    .into_iter()
560                    .map(|graph_id| IntegrandGraphInfo {
561                        graph_id,
562                        name: integrand.data.graph_terms[graph_id].graph.name.clone(),
563                        is_master: graph_id == master_graph_id,
564                    })
565                    .collect(),
566                orientation_edge_ids: master_graph
567                    .orientations
568                    .first()
569                    .map(orientation_edge_ids)
570                    .unwrap_or_default(),
571                orientations: master_graph
572                    .orientations
573                    .iter()
574                    .enumerate()
575                    .map(|(orientation_id, orientation)| IntegrandOrientationInfo {
576                        orientation_id,
577                        signature: orientation_signature(orientation),
578                    })
579                    .collect(),
580                loop_momentum_bases: master_graph
581                    .lmbs
582                    .iter_enumerated()
583                    .map(|(basis_id, lmb)| IntegrandLoopMomentumBasisInfo {
584                        basis_id: usize::from(basis_id),
585                        channel_id: channel_ids[usize::from(basis_id)],
586                        edge_ids: lmb.loop_edges.iter().map(|edge_id| edge_id.0).collect(),
587                        matches_generation_basis: lmb.loop_edges.len()
588                            == master_graph.graph.loop_momentum_basis.loop_edges.len()
589                            && lmb.loop_edges.iter().all(|edge_id| {
590                                master_graph
591                                    .graph
592                                    .loop_momentum_basis
593                                    .loop_edges
594                                    .contains(edge_id)
595                            }),
596                    })
597                    .collect(),
598                threshold_esurface_ids,
599                threshold_esurfaces,
600                cuts: Vec::new(),
601            })
602        })
603        .collect()
604}
605
606fn cross_section_graph_groups(
607    integrand: &gammalooprs::integrands::process::cross_section::CrossSectionIntegrand,
608    model: &Model,
609) -> Result<Vec<IntegrandGraphGroupInfo>> {
610    let parameterization_settings = integrand
611        .settings
612        .sampling
613        .get_parameterization_settings()
614        .unwrap_or_default();
615    integrand
616        .data
617        .graph_group_structure
618        .iter()
619        .enumerate()
620        .map(|(group_id, group)| {
621            let master_graph_id = group
622                .into_iter()
623                .next()
624                .expect("graph group should not be empty");
625            let master_graph = &integrand.data.graph_terms[master_graph_id];
626            let mut active_model_param_builder: ParamBuilder =
627                master_graph.graph.param_builder.clone();
628            active_model_param_builder.update_model_values(model);
629            let channel_ids = lmb_channel_ids(
630                &master_graph.lmbs,
631                &master_graph.multi_channeling_setup,
632                &master_graph.graph.name,
633                &parameterization_settings,
634            )?;
635            let cut_raising_powers = cut_raising_powers(master_graph);
636
637            let cuts = master_graph
638                .cuts
639                .iter_enumerated()
640                .map(|(cut_id, cut)| {
641                    IntegrandCutInfo::from_cross_section_cut(
642                        master_graph,
643                        cut_id,
644                        cut,
645                        cut_raising_powers[cut_id],
646                        model,
647                        &active_model_param_builder,
648                        &integrand.settings,
649                    )
650                })
651                .collect::<Vec<_>>();
652
653            let threshold_esurface_ids = master_graph
654                .threshold_candidate_esurface_ids
655                .iter()
656                .map(|esurface_id| esurface_id.0)
657                .collect::<Vec<_>>();
658
659            let threshold_esurfaces = threshold_esurface_ids
660                .iter()
661                .copied()
662                .map(|esurface_id| {
663                    let active_cuts = cuts
664                        .iter()
665                        .filter_map(|cut| cut.active_threshold_cut(esurface_id))
666                        .collect();
667
668                    IntegrandThresholdEsurfaceInfo {
669                        esurface_id,
670                        representative_graph_id: master_graph_id,
671                        edge_ids: threshold_esurface_edge_ids(
672                            &master_graph.graph.surface_cache.esurface_cache,
673                            esurface_id,
674                        ),
675                        classification: None,
676                        active_cuts,
677                    }
678                })
679                .collect::<Vec<_>>();
680
681            Ok(IntegrandGraphGroupInfo {
682                group_id,
683                graphs: group
684                    .into_iter()
685                    .map(|graph_id| IntegrandGraphInfo {
686                        graph_id,
687                        name: integrand.data.graph_terms[graph_id].graph.name.clone(),
688                        is_master: graph_id == master_graph_id,
689                    })
690                    .collect(),
691                orientation_edge_ids: master_graph
692                    .orientations
693                    .first()
694                    .map(orientation_edge_ids)
695                    .unwrap_or_default(),
696                orientations: master_graph
697                    .orientations
698                    .iter()
699                    .enumerate()
700                    .map(|(orientation_id, orientation)| IntegrandOrientationInfo {
701                        orientation_id,
702                        signature: orientation_signature(orientation),
703                    })
704                    .collect(),
705                loop_momentum_bases: master_graph
706                    .lmbs
707                    .iter_enumerated()
708                    .map(|(basis_id, lmb)| IntegrandLoopMomentumBasisInfo {
709                        basis_id: usize::from(basis_id),
710                        channel_id: channel_ids[usize::from(basis_id)],
711                        edge_ids: lmb.loop_edges.iter().map(|edge_id| edge_id.0).collect(),
712                        matches_generation_basis: lmb.loop_edges.len()
713                            == master_graph.graph.loop_momentum_basis.loop_edges.len()
714                            && lmb.loop_edges.iter().all(|edge_id| {
715                                master_graph
716                                    .graph
717                                    .loop_momentum_basis
718                                    .loop_edges
719                                    .contains(edge_id)
720                            }),
721                    })
722                    .collect(),
723                threshold_esurface_ids,
724                threshold_esurfaces,
725                cuts,
726            })
727        })
728        .collect()
729}
730
731fn integrand_record_size_from_amplitude(amplitude: &Amplitude) -> Result<usize> {
732    let mut record = amplitude.clone();
733    record.integrand = None;
734    let encoded =
735        bincode::encode_to_vec(&record, bincode::config::standard()).with_context(|| {
736            format!(
737                "While serializing amplitude '{}' for integrand info",
738                amplitude.name
739            )
740        })?;
741    Ok(encoded.len())
742}
743
744fn integrand_record_size_from_cross_section(cross_section: &CrossSection) -> Result<usize> {
745    let mut record = cross_section.clone();
746    record.integrand = None;
747    let encoded =
748        bincode::encode_to_vec(&record, bincode::config::standard()).with_context(|| {
749            format!(
750                "While serializing cross-section '{}' for integrand info",
751                cross_section.name
752            )
753        })?;
754    Ok(encoded.len())
755}