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 #[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 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 pub process_id: usize,
362 pub process_name: String,
364 pub integrand_name: String,
366 pub kind: IntegrandKind,
368 pub generation_compilation: FrozenCompilationMode,
370 pub active_f64_backend: ActiveF64Backend,
372 pub graph_count: usize,
374 pub graph_group_count: usize,
376 pub record_size_bytes: usize,
378 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 ¶meterization_settings,
571 )?;
572 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 ¶meterization_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}