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