Skip to main content

gammalooprs/integrands/process/
mod.rs

1use std::collections::BTreeSet;
2use std::fs;
3use std::path::Path;
4
5use crate::cff::expression::OrientationID;
6use crate::graph::{FeynmanGraph, Graph, GraphGroup, GroupId, LmbIndex, LoopMomentumBasis};
7use crate::integrands::evaluation::{
8    EvaluationMetaData, EvaluationResult, GenericEvaluationResult, GraphEvaluationResult,
9    LoopMomentaEscalationMetrics, PreciseEvaluationResult, RawBatchEvaluationResult,
10    RawPreciseBatchEvaluationResult, RotatedEvaluation, StabilityFailureReason, StabilityResult,
11    StabilityStatus, StatisticsCounter,
12};
13use crate::model::Model;
14use crate::momentum::sample::{BareMomentumSample, LoopMomenta, MomentumSample};
15use crate::momentum::{Rotation, ThreeMomentum};
16use crate::observables::{
17    AdditionalWeightKey, EventProcessingRuntime, GenericEvent, HistogramProcessInfo,
18    ObservableAccumulatorBundle, ObservableFileFormat, ObservableSnapshotBundle,
19};
20use crate::processes::{GraphGroupSelectionSpec, StandaloneExportSettings};
21use crate::utils::{
22    ArbPrec, F, FloatLike, f128, format_for_compare_digits, get_n_dim_for_n_loop_momenta,
23    global_inv_parameterize,
24};
25use bincode_trait_derive::{Decode, Encode};
26use color_eyre::owo_colors::OwoColorize;
27use colored::Colorize;
28use derive_more::{From, Into};
29use enum_dispatch::enum_dispatch;
30use eyre::{Context, eyre};
31use gammaloop_sample::{DiscreteGraphSample, GammaLoopSample, parameterize};
32use itertools::Itertools;
33use linnet::half_edge::involution::EdgeVec;
34use linnet::half_edge::involution::Orientation;
35use linnet::half_edge::subgraph::{SubSetLike, subset::SubSet};
36use momtrop::SampleGenerator;
37use serde::{Deserialize, Serialize};
38use smallvec::SmallVec;
39use spenso::algebra::algebraic_traits::IsZero;
40use spenso::algebra::complex::Complex;
41use std::sync::Once;
42use std::time::{Duration, Instant};
43use symbolica::numerical_integration::{ContinuousGrid, DiscreteGrid, Grid, Sample};
44use tracing::{debug, warn};
45use typed_index_collections::TiVec;
46pub mod amplitude;
47pub mod cache_debugging;
48pub mod cross_section;
49pub mod gammaloop_sample;
50pub mod ir;
51use crate::{
52    DependentMomentaConstructor, GammaLoopContext, settings::RuntimeSettings,
53    settings::runtime::DiscreteGraphSamplingSettings, settings::runtime::DiscreteGraphSamplingType,
54    settings::runtime::IntegratorSettings, settings::runtime::LmbChannelWeight,
55    settings::runtime::ParameterizationMode, settings::runtime::ParameterizationSettings,
56    settings::runtime::Precision, settings::runtime::SamplingSettings,
57    settings::runtime::StabilityLevelSetting, settings::runtime::StabilitySettings,
58};
59use color_eyre::Result;
60
61pub mod evaluators;
62pub use evaluators::ActiveF64Backend;
63pub use evaluators::{GenericEvaluator, GenericEvaluatorFloat};
64
65pub mod param_builder;
66pub use param_builder::{ParamBuilder, ParamValuePairs, ThresholdParams, UpdateAndGetParams};
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
69pub enum OrientationProfileMode {
70    #[default]
71    Summed,
72    PerOrientation,
73}
74
75impl OrientationProfileMode {
76    pub fn profiles_per_orientation(self) -> bool {
77        matches!(self, Self::PerOrientation)
78    }
79}
80
81/// Direct input for evaluating an integrand at explicit spatial loop momenta.
82///
83/// `loop_momenta` contains one `(px, py, pz)` value per independent loop momentum;
84/// energies are reconstructed by the integrand and external momenta come from its
85/// kinematics settings. The selected graph or graph group determines the required
86/// loop count and evaluation rejects a different number of entries.
87#[derive(Debug, Clone)]
88pub struct MomentumSpaceEvaluationInput {
89    /// Spatial loop momenta in loop-basis order.
90    pub loop_momenta: Vec<ThreeMomentum<F<f64>>>,
91    /// Multiplicative integration weight associated with this explicit point.
92    pub integrator_weight: F<f64>,
93    /// Optional graph selected within a non-discrete sampling setup.
94    pub graph_id: Option<usize>,
95    /// Optional graph group selected within a discrete sampling setup.
96    pub group_id: Option<GroupId>,
97    /// Optional orientation within the selected graph or graph group.
98    pub orientation: Option<usize>,
99    /// Optional loop-momentum-basis channel selected by discrete sampling.
100    pub channel_id: Option<ChannelIndex>,
101}
102
103#[derive(Clone, Debug)]
104pub(crate) struct RuntimeCache<T>(Option<T>);
105
106impl<T> Default for RuntimeCache<T> {
107    fn default() -> Self {
108        Self(None)
109    }
110}
111
112impl<T> RuntimeCache<T> {
113    pub(crate) fn invalidate(&mut self) {
114        self.0 = None;
115    }
116
117    pub(crate) fn set(&mut self, value: T) {
118        self.0 = Some(value);
119    }
120
121    pub(crate) fn take(&mut self) -> Option<T> {
122        self.0.take()
123    }
124
125    pub(crate) fn as_ref(&self) -> Option<&T> {
126        self.0.as_ref()
127    }
128
129    pub(crate) fn as_mut(&mut self) -> Option<&mut T> {
130        self.0.as_mut()
131    }
132}
133
134impl<T> bincode::Encode for RuntimeCache<T> {
135    fn encode<E: bincode::enc::Encoder>(
136        &self,
137        _encoder: &mut E,
138    ) -> std::result::Result<(), bincode::error::EncodeError> {
139        Ok(())
140    }
141}
142
143impl<C, T> bincode::Decode<C> for RuntimeCache<T> {
144    fn decode<D: bincode::de::Decoder<Context = C>>(
145        _decoder: &mut D,
146    ) -> std::result::Result<Self, bincode::error::DecodeError> {
147        Ok(Self::default())
148    }
149}
150
151#[derive(Clone, Encode, Decode)]
152#[trait_decode(trait = GammaLoopContext)]
153#[enum_dispatch(HasIntegrand)]
154pub enum ProcessIntegrand {
155    Amplitude(amplitude::AmplitudeIntegrand),
156    CrossSection(cross_section::CrossSectionIntegrand),
157}
158
159fn discrete_sampling_type_name(sampling_type: &DiscreteGraphSamplingType) -> &'static str {
160    match sampling_type {
161        DiscreteGraphSamplingType::Default(_) => "default",
162        DiscreteGraphSamplingType::MultiChanneling(_) => "multi_channeling",
163        DiscreteGraphSamplingType::TropicalSampling(_) => "tropical",
164        DiscreteGraphSamplingType::DiscreteMultiChanneling(_) => "discrete_multi_channeling",
165    }
166}
167
168pub(crate) fn discrete_sampling_depth_for_settings(
169    settings: &DiscreteGraphSamplingSettings,
170) -> usize {
171    let orientation_depth = usize::from(settings.sample_orientations);
172    match &settings.sampling_type {
173        DiscreteGraphSamplingType::DiscreteMultiChanneling(_) => 2 + orientation_depth,
174        _ => 1 + orientation_depth,
175    }
176}
177
178fn invalid_discrete_sampling_depth_error(
179    settings: &DiscreteGraphSamplingSettings,
180    actual_depth: usize,
181) -> eyre::Report {
182    let mut axes = vec!["graph group"];
183    if settings.sample_orientations {
184        axes.push("orientation");
185    }
186    if matches!(
187        settings.sampling_type,
188        DiscreteGraphSamplingType::DiscreteMultiChanneling(_)
189    ) {
190        axes.push("channel");
191    }
192
193    eyre!(
194        "This integrand uses discrete graph sampling (sample_orientations = {}, sampling_type = {}), so x-space evaluation requires {} discrete dimensions [{}], but got {}.",
195        settings.sample_orientations,
196        discrete_sampling_type_name(&settings.sampling_type),
197        axes.len(),
198        axes.join(", "),
199        actual_depth
200    )
201}
202
203pub(crate) fn resolve_discrete_selection_for_sampling(
204    sampling: &SamplingSettings,
205    discrete_dimensions: &[usize],
206    group_count: usize,
207    mut orientation_count_for_group: impl FnMut(GroupId) -> Option<usize>,
208    mut channel_count_for_group: impl FnMut(GroupId) -> Option<usize>,
209) -> Result<(Option<GroupId>, Option<usize>, Option<ChannelIndex>)> {
210    match sampling {
211        SamplingSettings::Default(_) | SamplingSettings::MultiChanneling(_) => {
212            if !discrete_dimensions.is_empty() {
213                return Err(eyre!(
214                    "This integrand does not use discrete graph sampling; expected no discrete dimensions, got {:?}.",
215                    discrete_dimensions
216                ));
217            }
218            Ok((None, None, None))
219        }
220        SamplingSettings::DiscreteGraphs(settings) => {
221            let expected_depth = discrete_sampling_depth_for_settings(settings);
222            if discrete_dimensions.len() != expected_depth {
223                return Err(invalid_discrete_sampling_depth_error(
224                    settings,
225                    discrete_dimensions.len(),
226                ));
227            }
228
229            let group_id = GroupId(discrete_dimensions[0]);
230            if group_id.0 >= group_count {
231                return Err(eyre!(
232                    "Discrete graph group index {} is out of range; the integrand has {} groups.",
233                    group_id.0,
234                    group_count
235                ));
236            }
237
238            let orientation = if settings.sample_orientations {
239                let orientation = discrete_dimensions[1];
240                let orientation_count = orientation_count_for_group(group_id).ok_or_else(|| {
241                    eyre!(
242                        "Could not determine orientation count for group {}.",
243                        group_id.0
244                    )
245                })?;
246                if orientation >= orientation_count {
247                    return Err(eyre!(
248                        "Orientation {} is out of range for graph group {}; the group has {} orientations.",
249                        orientation,
250                        group_id.0,
251                        orientation_count
252                    ));
253                }
254                Some(orientation)
255            } else {
256                None
257            };
258
259            let channel = match &settings.sampling_type {
260                DiscreteGraphSamplingType::DiscreteMultiChanneling(_) => {
261                    let channel_index = *discrete_dimensions.last().expect("validated depth");
262                    let channel_count = channel_count_for_group(group_id).ok_or_else(|| {
263                        eyre!(
264                            "Could not determine channel count for group {}.",
265                            group_id.0
266                        )
267                    })?;
268                    if channel_index >= channel_count {
269                        return Err(eyre!(
270                            "Channel {} is out of range for graph group {}; the group has {} channels.",
271                            channel_index,
272                            group_id.0,
273                            channel_count
274                        ));
275                    }
276                    Some(ChannelIndex::from(channel_index))
277                }
278                _ => None,
279            };
280
281            Ok((Some(group_id), orientation, channel))
282        }
283    }
284}
285
286impl ProcessIntegrand {
287    pub fn clone_with_selected_graph_groups(&self, graph_names: &[String]) -> Result<Self> {
288        if graph_names.is_empty() {
289            return Ok(self.clone());
290        }
291        if !matches!(
292            self.get_settings().sampling,
293            SamplingSettings::DiscreteGraphs(_)
294        ) {
295            return Err(eyre!(
296                "Runtime graph-group selection requires graphs = 'monte_carlo'."
297            ));
298        }
299        let mut unique_names = BTreeSet::new();
300        for graph_name in graph_names {
301            if !unique_names.insert(graph_name) {
302                return Err(eyre!(
303                    "Runtime graph-group selection contains duplicate graph name '{}'.",
304                    graph_name
305                ));
306            }
307        }
308
309        let selection = GraphGroupSelectionSpec::from_master_graph_names(graph_names.to_vec());
310        let mut selected = match self {
311            Self::Amplitude(integrand) => {
312                let plan = selection.plan(&integrand.data.graph_group_structure, |graph_id| {
313                    integrand
314                        .data
315                        .graph_terms
316                        .get(graph_id)
317                        .map(|term| &term.graph)
318                })?;
319                Self::Amplitude(integrand.clone_with_graph_group_selection(&plan)?)
320            }
321            Self::CrossSection(integrand) => {
322                let plan = selection.plan(&integrand.data.graph_group_structure, |graph_id| {
323                    integrand
324                        .data
325                        .graph_terms
326                        .get(graph_id)
327                        .map(|term| &term.graph)
328                })?;
329                Self::CrossSection(integrand.clone_with_graph_group_selection(&plan)?)
330            }
331        };
332        let selected_names = selected
333            .graph_group_master_names()
334            .into_iter()
335            .map(str::to_string)
336            .collect();
337        let SamplingSettings::DiscreteGraphs(sampling) = &mut selected.get_mut_settings().sampling
338        else {
339            unreachable!("validated graph sampling before constructing the selected view")
340        };
341        sampling.graph_names = selected_names;
342        Ok(selected)
343    }
344
345    pub fn resume_fingerprint(&self) -> Result<String> {
346        let mut bytes = match self {
347            Self::Amplitude(integrand) => {
348                bincode::encode_to_vec(&integrand.data, bincode::config::standard())
349            }
350            Self::CrossSection(integrand) => {
351                bincode::encode_to_vec(&integrand.data, bincode::config::standard())
352            }
353        }
354        .map_err(|err| eyre!("Could not serialize integrand fingerprint payload: {err}"))?;
355
356        let mut hash = 0xcbf29ce484222325u64;
357        for byte in self
358            .variant_tag()
359            .as_bytes()
360            .iter()
361            .copied()
362            .chain(bytes.drain(..))
363        {
364            hash ^= u64::from(byte);
365            hash = hash.wrapping_mul(0x100000001b3);
366        }
367
368        Ok(format!("{hash:016x}"))
369    }
370
371    fn variant_tag(&self) -> &'static str {
372        match self {
373            Self::Amplitude(_) => "amplitude",
374            Self::CrossSection(_) => "cross_section",
375        }
376    }
377
378    pub fn kind_name(&self) -> &'static str {
379        self.variant_tag()
380    }
381
382    pub fn export_standalone(
383        &self,
384        path: impl AsRef<Path>,
385        settings: &StandaloneExportSettings,
386    ) -> Result<()> {
387        match self {
388            Self::Amplitude(a) => a.export_standalone(path, settings),
389            Self::CrossSection(a) => a.export_standalone(path, settings),
390        }
391    }
392
393    pub fn warm_up(&mut self, model: &Model) -> Result<()> {
394        let settings = self.get_settings();
395        if matches!(
396            &settings.sampling,
397            SamplingSettings::DiscreteGraphs(settings) if settings.sample_orientations
398        ) {
399            if !matches!(
400                settings.general.evaluator_method,
401                evaluators::EvaluatorMethod::SingleParametric
402            ) {
403                return Err(eyre!(
404                    "Monte Carlo sampling over orientations requires general.evaluator_method=SingleParametric; got {:?}.",
405                    settings.general.evaluator_method
406                ));
407            }
408            warn!("Monte Carlo sampling over orientations is using the SingleParametric evaluator");
409        }
410
411        match self {
412            Self::Amplitude(a) => a.warm_up(model),
413            Self::CrossSection(a) => a.warm_up(model),
414        }
415    }
416
417    pub fn frozen_compilation(&self) -> &crate::settings::global::FrozenCompilationMode {
418        match self {
419            Self::Amplitude(a) => a.frozen_compilation(),
420            Self::CrossSection(a) => a.frozen_compilation(),
421        }
422    }
423
424    pub fn active_f64_backend(&self) -> ActiveF64Backend {
425        match self {
426            Self::Amplitude(a) => a.active_f64_backend(),
427            Self::CrossSection(a) => a.active_f64_backend(),
428        }
429    }
430
431    pub(crate) fn activate_runtime_backends_after_load(
432        &mut self,
433        allow_symjit_fallback: bool,
434    ) -> Result<Option<String>> {
435        match self {
436            Self::Amplitude(a) => a.activate_runtime_backends_after_load(allow_symjit_fallback),
437            Self::CrossSection(a) => a.activate_runtime_backends_after_load(allow_symjit_fallback),
438        }
439    }
440
441    pub(crate) fn save(&self, path: impl AsRef<Path>, override_existing: bool) -> Result<()> {
442        let path = path.as_ref().join("integrand");
443
444        let r = fs::create_dir_all(&path).with_context(|| {
445            format!(
446                "Trying to create directory to save amplitude {}",
447                path.display()
448            )
449        });
450        if override_existing {
451            r?;
452        }
453        match self {
454            ProcessIntegrand::Amplitude(integrand) => integrand.save(path, override_existing),
455            ProcessIntegrand::CrossSection(integrand) => integrand.save(path, override_existing),
456        }
457    }
458
459    pub(crate) fn compile(
460        &mut self,
461        path: impl AsRef<Path>,
462        override_existing: bool,
463        thread_pool: &rayon::ThreadPool,
464    ) -> Result<Vec<(String, std::time::Duration)>> {
465        let path = path.as_ref().join("integrand");
466
467        let r = fs::create_dir_all(&path).with_context(|| {
468            format!(
469                "Trying to create directory to save amplitude {}",
470                path.display()
471            )
472        });
473        if override_existing {
474            r?;
475        }
476        match self {
477            ProcessIntegrand::Amplitude(integrand) => {
478                integrand.compile(path, override_existing, thread_pool)
479            }
480            ProcessIntegrand::CrossSection(integrand) => {
481                integrand.compile(path, override_existing, thread_pool)
482            }
483        }
484    }
485
486    pub fn get_settings(&self) -> &RuntimeSettings {
487        match self {
488            ProcessIntegrand::Amplitude(integrand) => &integrand.settings,
489            ProcessIntegrand::CrossSection(integrand) => &integrand.settings,
490        }
491    }
492
493    pub fn get_mut_settings(&mut self) -> &mut RuntimeSettings {
494        match self {
495            ProcessIntegrand::Amplitude(integrand) => {
496                integrand.invalidate_event_processing_runtime();
497                &mut integrand.settings
498            }
499            ProcessIntegrand::CrossSection(integrand) => {
500                integrand.invalidate_event_processing_runtime();
501                &mut integrand.settings
502            }
503        }
504    }
505
506    pub fn graph_count(&self) -> usize {
507        match self {
508            ProcessIntegrand::Amplitude(integrand) => integrand.data.graph_terms.len(),
509            ProcessIntegrand::CrossSection(integrand) => integrand.data.graph_terms.len(),
510        }
511    }
512
513    pub fn graph_group_count(&self) -> usize {
514        match self {
515            ProcessIntegrand::Amplitude(integrand) => integrand.data.graph_group_structure.len(),
516            ProcessIntegrand::CrossSection(integrand) => integrand.data.graph_group_structure.len(),
517        }
518    }
519
520    pub fn graph_group_master_names(&self) -> Vec<&str> {
521        match self {
522            ProcessIntegrand::Amplitude(integrand) => integrand
523                .data
524                .graph_group_structure
525                .iter()
526                .map(|group| {
527                    integrand.data.graph_terms[group.master()]
528                        .graph
529                        .name
530                        .as_str()
531                })
532                .collect(),
533            ProcessIntegrand::CrossSection(integrand) => integrand
534                .data
535                .graph_group_structure
536                .iter()
537                .map(|group| {
538                    integrand.data.graph_terms[group.master()]
539                        .graph
540                        .name
541                        .as_str()
542                })
543                .collect(),
544        }
545    }
546
547    pub fn find_graph_id_by_name(&self, graph_name: &str) -> Option<usize> {
548        match self {
549            ProcessIntegrand::Amplitude(integrand) => integrand
550                .data
551                .graph_terms
552                .iter()
553                .position(|term| term.graph.name == graph_name),
554            ProcessIntegrand::CrossSection(integrand) => integrand
555                .data
556                .graph_terms
557                .iter()
558                .position(|term| term.graph.name == graph_name),
559        }
560    }
561
562    pub fn graph_name_by_id(&self, graph_id: usize) -> Option<&str> {
563        match self {
564            ProcessIntegrand::Amplitude(integrand) => integrand
565                .data
566                .graph_terms
567                .get(graph_id)
568                .map(|term| term.graph.name.as_str()),
569            ProcessIntegrand::CrossSection(integrand) => integrand
570                .data
571                .graph_terms
572                .get(graph_id)
573                .map(|term| term.graph.name.as_str()),
574        }
575    }
576
577    pub fn graph_group_id_by_graph_id(&self, graph_id: usize) -> Option<usize> {
578        self.find_group_id_containing_graph(graph_id)
579            .map(usize::from)
580    }
581
582    pub fn cut_edge_ids(&self, graph_id: usize, cut_id: usize) -> Option<Vec<usize>> {
583        match self {
584            ProcessIntegrand::Amplitude(integrand) => {
585                (cut_id == 0 && graph_id < integrand.data.graph_terms.len()).then(Vec::new)
586            }
587            ProcessIntegrand::CrossSection(integrand) => {
588                let graph_term = integrand.data.graph_terms.get(graph_id)?;
589                let cut = graph_term.cuts.get(crate::processes::CutId::from(cut_id))?;
590                Some(
591                    graph_term
592                        .graph
593                        .underlying
594                        .iter_edges_of(&cut.cut)
595                        .map(|(_, edge_id, _)| edge_id.0)
596                        .sorted()
597                        .collect(),
598                )
599            }
600        }
601    }
602
603    pub fn lmb_sample_id_for_channel(
604        &self,
605        graph_id: usize,
606        lmb_channel_id: usize,
607        parameterization_settings: &ParameterizationSettings,
608    ) -> Result<Option<usize>> {
609        match self {
610            ProcessIntegrand::Amplitude(integrand) => {
611                let Some(graph_term) = integrand.data.graph_terms.get(graph_id) else {
612                    return Ok(None);
613                };
614                Ok(Some(usize::from(
615                    graph_term.multi_channeling_setup.effective_channel_lmb_id(
616                        ChannelIndex::from(lmb_channel_id),
617                        &graph_term.multi_channeling_setup.graph.name,
618                        parameterization_settings,
619                    )?,
620                )))
621            }
622            ProcessIntegrand::CrossSection(integrand) => {
623                let Some(graph_term) = integrand.data.graph_terms.get(graph_id) else {
624                    return Ok(None);
625                };
626                Ok(Some(usize::from(
627                    graph_term.multi_channeling_setup.effective_channel_lmb_id(
628                        ChannelIndex::from(lmb_channel_id),
629                        &graph_term.multi_channeling_setup.graph.name,
630                        parameterization_settings,
631                    )?,
632                )))
633            }
634        }
635    }
636
637    fn find_group_id_containing_graph(&self, graph_id: usize) -> Option<GroupId> {
638        match self {
639            ProcessIntegrand::Amplitude(integrand) => integrand
640                .data
641                .graph_group_structure
642                .iter_enumerated()
643                .find_map(|(group_id, group)| {
644                    group.into_iter().contains(&graph_id).then_some(group_id)
645                }),
646            ProcessIntegrand::CrossSection(integrand) => integrand
647                .data
648                .graph_group_structure
649                .iter_enumerated()
650                .find_map(|(group_id, group)| {
651                    group.into_iter().contains(&graph_id).then_some(group_id)
652                }),
653        }
654    }
655
656    pub fn resolve_group_id_by_master_name(&self, graph_name: &str) -> Result<GroupId> {
657        match self.find_graph_id_by_name(graph_name) {
658            Some(graph_id) => {
659                let group_id = self
660                    .find_group_id_containing_graph(graph_id)
661                    .ok_or_else(|| {
662                        eyre!("Could not find graph group for graph '{}'.", graph_name)
663                    })?;
664                let master_graph_name = match self {
665                    ProcessIntegrand::Amplitude(integrand) => {
666                        &integrand.data.graph_terms
667                            [integrand.data.graph_group_structure[group_id].master()]
668                        .graph
669                        .name
670                    }
671                    ProcessIntegrand::CrossSection(integrand) => {
672                        &integrand.data.graph_terms
673                            [integrand.data.graph_group_structure[group_id].master()]
674                        .graph
675                        .name
676                    }
677                };
678                if master_graph_name != graph_name {
679                    return Err(eyre!(
680                        "Graph '{}' is not the master graph of its group; use '{}' instead.",
681                        graph_name,
682                        master_graph_name
683                    ));
684                }
685                Ok(group_id)
686            }
687            None => Err(eyre!(
688                "Unknown graph '{}' in momentum-space evaluation.",
689                graph_name
690            )),
691        }
692    }
693
694    pub fn resolve_discrete_selection(
695        &self,
696        discrete_dimensions: &[usize],
697    ) -> Result<(Option<GroupId>, Option<usize>, Option<ChannelIndex>)> {
698        let group_count = match self {
699            ProcessIntegrand::Amplitude(integrand) => integrand.data.graph_group_structure.len(),
700            ProcessIntegrand::CrossSection(integrand) => integrand.data.graph_group_structure.len(),
701        };
702
703        resolve_discrete_selection_for_sampling(
704            &self.get_settings().sampling,
705            discrete_dimensions,
706            group_count,
707            |group_id| self.group_orientation_count(group_id),
708            |group_id| self.group_channel_count(group_id),
709        )
710    }
711
712    pub fn expected_x_space_dimension(&self, discrete_dimensions: &[usize]) -> Result<usize> {
713        let settings = self.get_settings();
714        let (group_id, _, _) = self.resolve_discrete_selection(discrete_dimensions)?;
715        if matches!(
716            &settings.sampling,
717            SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
718                sampling_type: DiscreteGraphSamplingType::TropicalSampling(_),
719                ..
720            })
721        ) {
722            let group_id = group_id.ok_or_else(|| {
723                eyre!("Tropical sampling requires a discrete graph-group selection.")
724            })?;
725            let (loop_number, n_edges) = match self {
726                ProcessIntegrand::Amplitude(integrand) => {
727                    let master_graph = &integrand.data.graph_terms
728                        [integrand.data.graph_group_structure[group_id].master()];
729                    (
730                        master_graph.get_graph().get_loop_number(),
731                        master_graph.get_graph().iter_loop_edges().count(),
732                    )
733                }
734                ProcessIntegrand::CrossSection(integrand) => {
735                    let master_graph = &integrand.data.graph_terms
736                        [integrand.data.graph_group_structure[group_id].master()];
737                    (
738                        master_graph.get_graph().get_loop_number(),
739                        master_graph.get_graph().iter_loop_edges().count(),
740                    )
741                }
742            };
743            return Ok(get_n_dim_for_n_loop_momenta(
744                &settings.sampling,
745                loop_number,
746                Some(n_edges),
747            ));
748        }
749
750        let loop_number = match self {
751            ProcessIntegrand::Amplitude(integrand) => {
752                integrand.data.graph_terms[0].graph.get_loop_number()
753            }
754            ProcessIntegrand::CrossSection(integrand) => {
755                integrand.data.graph_terms[0].graph.get_loop_number()
756            }
757        };
758        Ok(get_n_dim_for_n_loop_momenta(
759            &settings.sampling,
760            loop_number,
761            None,
762        ))
763    }
764
765    pub fn discrete_sampling_depth(&self) -> usize {
766        match &self.get_settings().sampling {
767            SamplingSettings::Default(_) | SamplingSettings::MultiChanneling(_) => 0,
768            SamplingSettings::DiscreteGraphs(settings) => {
769                discrete_sampling_depth_for_settings(settings)
770            }
771        }
772    }
773
774    pub fn group_orientation_count(&self, group_id: GroupId) -> Option<usize> {
775        match self {
776            ProcessIntegrand::Amplitude(integrand) => Some(
777                integrand.data.graph_terms[integrand.data.graph_group_structure[group_id].master()]
778                    .get_num_orientations(),
779            ),
780            ProcessIntegrand::CrossSection(integrand) => Some(
781                integrand.data.graph_terms[integrand.data.graph_group_structure[group_id].master()]
782                    .get_num_orientations(),
783            ),
784        }
785    }
786
787    pub fn group_channel_count(&self, group_id: GroupId) -> Option<usize> {
788        let parameterization_settings = self
789            .get_settings()
790            .sampling
791            .get_parameterization_settings()
792            .unwrap_or_default();
793        match self {
794            ProcessIntegrand::Amplitude(integrand) => Some(
795                integrand.data.graph_terms[integrand.data.graph_group_structure[group_id].master()]
796                    .get_num_channels(&parameterization_settings),
797            ),
798            ProcessIntegrand::CrossSection(integrand) => Some(
799                integrand.data.graph_terms[integrand.data.graph_group_structure[group_id].master()]
800                    .get_num_channels(&parameterization_settings),
801            ),
802        }
803    }
804
805    pub fn graph_orientation_count(&self, graph_id: usize) -> Option<usize> {
806        match self {
807            ProcessIntegrand::Amplitude(integrand) => integrand
808                .data
809                .graph_terms
810                .get(graph_id)
811                .map(|term| term.get_num_orientations()),
812            ProcessIntegrand::CrossSection(integrand) => integrand
813                .data
814                .graph_terms
815                .get(graph_id)
816                .map(|term| term.get_num_orientations()),
817        }
818    }
819
820    pub fn evaluate_momentum_configuration(
821        &mut self,
822        model: &Model,
823        input: &MomentumSpaceEvaluationInput,
824        use_arb_prec: bool,
825    ) -> Result<EvaluationResult> {
826        match self {
827            ProcessIntegrand::Amplitude(integrand) => evaluate_momentum_configuration(
828                integrand,
829                model,
830                input,
831                input.integrator_weight,
832                use_arb_prec,
833                Complex::new_zero(),
834            ),
835            ProcessIntegrand::CrossSection(integrand) => evaluate_momentum_configuration(
836                integrand,
837                model,
838                input,
839                input.integrator_weight,
840                use_arb_prec,
841                Complex::new_zero(),
842            ),
843        }
844    }
845
846    pub fn evaluate_sample_precise(
847        &mut self,
848        sample: &Sample<F<f64>>,
849        model: &Model,
850        wgt: F<f64>,
851        use_arb_prec: bool,
852        max_eval: Complex<F<f64>>,
853    ) -> Result<PreciseEvaluationResult> {
854        match self {
855            ProcessIntegrand::Amplitude(integrand) => {
856                evaluate_sample_precise(integrand, model, sample, wgt, use_arb_prec, max_eval)
857            }
858            ProcessIntegrand::CrossSection(integrand) => {
859                evaluate_sample_precise(integrand, model, sample, wgt, use_arb_prec, max_eval)
860            }
861        }
862    }
863
864    pub fn evaluate_momentum_configuration_precise(
865        &mut self,
866        model: &Model,
867        input: &MomentumSpaceEvaluationInput,
868        use_arb_prec: bool,
869    ) -> Result<PreciseEvaluationResult> {
870        match self {
871            ProcessIntegrand::Amplitude(integrand) => evaluate_momentum_configuration_precise(
872                integrand,
873                model,
874                input,
875                input.integrator_weight,
876                use_arb_prec,
877                Complex::new_zero(),
878            ),
879            ProcessIntegrand::CrossSection(integrand) => evaluate_momentum_configuration_precise(
880                integrand,
881                model,
882                input,
883                input.integrator_weight,
884                use_arb_prec,
885                Complex::new_zero(),
886            ),
887        }
888    }
889
890    pub fn evaluate_samples_raw(
891        &mut self,
892        model: &Model,
893        samples: &[Sample<F<f64>>],
894        iter: usize,
895        use_arb_prec: bool,
896        stop_on_interrupt: bool,
897        max_eval: Complex<F<f64>>,
898    ) -> Result<RawBatchEvaluationResult> {
899        let mut results = Vec::with_capacity(samples.len());
900        for sample in samples {
901            if stop_on_interrupt && crate::is_interrupted() {
902                break;
903            }
904            let mut result = match self {
905                ProcessIntegrand::Amplitude(integrand) => evaluate_sample(
906                    integrand,
907                    model,
908                    sample,
909                    sample.get_weight(),
910                    iter,
911                    use_arb_prec,
912                    max_eval,
913                ),
914                ProcessIntegrand::CrossSection(integrand) => evaluate_sample(
915                    integrand,
916                    model,
917                    sample,
918                    sample.get_weight(),
919                    iter,
920                    use_arb_prec,
921                    max_eval,
922                ),
923            }?;
924
925            self.process_evaluation_result(&result);
926            maybe_discard_generated_events_in_result(self.get_settings(), &mut result);
927            results.push(result);
928            if stop_on_interrupt && crate::is_interrupted() {
929                break;
930            }
931        }
932
933        Ok(RawBatchEvaluationResult {
934            statistics: StatisticsCounter::from_evaluation_results(&results),
935            samples: results,
936        })
937    }
938
939    pub fn evaluate_momentum_configurations_raw(
940        &mut self,
941        model: &Model,
942        inputs: &[MomentumSpaceEvaluationInput],
943        use_arb_prec: bool,
944    ) -> Result<RawBatchEvaluationResult> {
945        let mut results = Vec::with_capacity(inputs.len());
946        for input in inputs {
947            let mut result = match self {
948                ProcessIntegrand::Amplitude(integrand) => evaluate_momentum_configuration(
949                    integrand,
950                    model,
951                    input,
952                    input.integrator_weight,
953                    use_arb_prec,
954                    Complex::new_zero(),
955                ),
956                ProcessIntegrand::CrossSection(integrand) => evaluate_momentum_configuration(
957                    integrand,
958                    model,
959                    input,
960                    input.integrator_weight,
961                    use_arb_prec,
962                    Complex::new_zero(),
963                ),
964            }?;
965
966            self.process_evaluation_result(&result);
967            maybe_discard_generated_events_in_result(self.get_settings(), &mut result);
968            results.push(result);
969        }
970
971        Ok(RawBatchEvaluationResult {
972            statistics: StatisticsCounter::from_evaluation_results(&results),
973            samples: results,
974        })
975    }
976
977    pub fn evaluate_samples_precise_raw(
978        &mut self,
979        model: &Model,
980        samples: &[Sample<F<f64>>],
981        use_arb_prec: bool,
982        max_eval: Complex<F<f64>>,
983    ) -> Result<RawPreciseBatchEvaluationResult> {
984        let mut results = Vec::with_capacity(samples.len());
985        for sample in samples {
986            results.push(self.evaluate_sample_precise(
987                sample,
988                model,
989                sample.get_weight(),
990                use_arb_prec,
991                max_eval,
992            )?);
993        }
994
995        Ok(RawPreciseBatchEvaluationResult { samples: results })
996    }
997
998    pub fn evaluate_momentum_configurations_precise_raw(
999        &mut self,
1000        model: &Model,
1001        inputs: &[MomentumSpaceEvaluationInput],
1002        use_arb_prec: bool,
1003    ) -> Result<RawPreciseBatchEvaluationResult> {
1004        let mut results = Vec::with_capacity(inputs.len());
1005        for input in inputs {
1006            results.push(self.evaluate_momentum_configuration_precise(
1007                model,
1008                input,
1009                use_arb_prec,
1010            )?);
1011        }
1012
1013        Ok(RawPreciseBatchEvaluationResult { samples: results })
1014    }
1015
1016    pub fn process_evaluation_result(&mut self, result: &EvaluationResult) {
1017        match self {
1018            ProcessIntegrand::Amplitude(integrand) => {
1019                process_evaluation_result_runtime(integrand, result)
1020            }
1021            ProcessIntegrand::CrossSection(integrand) => {
1022                process_evaluation_result_runtime(integrand, result)
1023            }
1024        }
1025    }
1026
1027    pub fn merge_event_processing_runtime(&mut self, other: &mut Self) -> Result<()> {
1028        match (self, other) {
1029            (ProcessIntegrand::Amplitude(lhs), ProcessIntegrand::Amplitude(rhs)) => {
1030                merge_event_processing_runtime(lhs, rhs)
1031            }
1032            (ProcessIntegrand::CrossSection(lhs), ProcessIntegrand::CrossSection(rhs)) => {
1033                merge_event_processing_runtime(lhs, rhs)
1034            }
1035            _ => Err(eyre!(
1036                "Cannot merge event-processing runtime for incompatible process integrands."
1037            )),
1038        }
1039    }
1040
1041    pub fn update_event_processing_runtime(&mut self, iter: usize) {
1042        match self {
1043            ProcessIntegrand::Amplitude(integrand) => {
1044                update_event_processing_runtime(integrand, iter)
1045            }
1046            ProcessIntegrand::CrossSection(integrand) => {
1047                update_event_processing_runtime(integrand, iter)
1048            }
1049        }
1050    }
1051
1052    pub fn observable_accumulator_bundle(&self) -> Option<ObservableAccumulatorBundle> {
1053        match self {
1054            ProcessIntegrand::Amplitude(integrand) => observable_accumulator_bundle(integrand),
1055            ProcessIntegrand::CrossSection(integrand) => observable_accumulator_bundle(integrand),
1056        }
1057    }
1058
1059    pub fn has_observables(&self) -> bool {
1060        match self {
1061            ProcessIntegrand::Amplitude(integrand) => integrand
1062                .event_processing_runtime()
1063                .is_some_and(EventProcessingRuntime::has_observables),
1064            ProcessIntegrand::CrossSection(integrand) => integrand
1065                .event_processing_runtime()
1066                .is_some_and(EventProcessingRuntime::has_observables),
1067        }
1068    }
1069
1070    pub fn observable_snapshot_bundle(&self) -> Option<ObservableSnapshotBundle> {
1071        match self {
1072            ProcessIntegrand::Amplitude(integrand) => observable_snapshot_bundle(integrand),
1073            ProcessIntegrand::CrossSection(integrand) => observable_snapshot_bundle(integrand),
1074        }
1075    }
1076
1077    pub fn build_observable_snapshots_for_result(
1078        &self,
1079        result: &EvaluationResult,
1080    ) -> Option<ObservableSnapshotBundle> {
1081        match self {
1082            ProcessIntegrand::Amplitude(integrand) => {
1083                build_observable_snapshots_for_result(integrand, result)
1084            }
1085            ProcessIntegrand::CrossSection(integrand) => {
1086                build_observable_snapshots_for_result(integrand, result)
1087            }
1088        }
1089    }
1090
1091    pub fn build_observable_snapshots_for_precise_result(
1092        &self,
1093        result: &PreciseEvaluationResult,
1094    ) -> Option<ObservableSnapshotBundle> {
1095        match self {
1096            ProcessIntegrand::Amplitude(integrand) => match result {
1097                PreciseEvaluationResult::Double(result) => {
1098                    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1099                }
1100                PreciseEvaluationResult::Quad(result) => {
1101                    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1102                }
1103                PreciseEvaluationResult::Arb(result) => {
1104                    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1105                }
1106            },
1107            ProcessIntegrand::CrossSection(integrand) => match result {
1108                PreciseEvaluationResult::Double(result) => {
1109                    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1110                }
1111                PreciseEvaluationResult::Quad(result) => {
1112                    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1113                }
1114                PreciseEvaluationResult::Arb(result) => {
1115                    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1116                }
1117            },
1118        }
1119    }
1120
1121    pub fn write_observable_snapshots(
1122        &self,
1123        path: impl AsRef<Path>,
1124        format: ObservableFileFormat,
1125    ) -> Result<()> {
1126        let Some(bundle) = self.observable_snapshot_bundle() else {
1127            return Ok(());
1128        };
1129
1130        write_observable_snapshot_bundle(&bundle, path.as_ref(), format)
1131    }
1132
1133    pub fn restore_observable_snapshot_bundle(
1134        &mut self,
1135        bundle: &ObservableSnapshotBundle,
1136    ) -> Result<()> {
1137        match self {
1138            ProcessIntegrand::Amplitude(integrand) => {
1139                restore_observable_snapshot_bundle(integrand, bundle)
1140            }
1141            ProcessIntegrand::CrossSection(integrand) => {
1142                restore_observable_snapshot_bundle(integrand, bundle)
1143            }
1144        }
1145    }
1146}
1147
1148fn format_orientation_label(signature: &EdgeVec<Orientation>) -> String {
1149    signature
1150        .iter()
1151        .map(|(_, orientation)| match *orientation {
1152            Orientation::Default => '+',
1153            Orientation::Reversed => '-',
1154            Orientation::Undirected => '0',
1155        })
1156        .collect()
1157}
1158
1159pub(crate) fn resolve_visible_orientation_id(
1160    orientation_filter: &SubSet<OrientationID>,
1161    visible_orientation_id: usize,
1162) -> Option<OrientationID> {
1163    if orientation_filter.is_full() {
1164        Some(OrientationID::from(visible_orientation_id))
1165    } else {
1166        orientation_filter
1167            .included_iter()
1168            .nth(visible_orientation_id)
1169    }
1170}
1171
1172pub(crate) fn filtered_orientation_count(
1173    orientation_filter: &SubSet<OrientationID>,
1174    orientations: &TiVec<OrientationID, EdgeVec<Orientation>>,
1175) -> usize {
1176    if orientation_filter.is_full() {
1177        orientations.len()
1178    } else {
1179        orientation_filter.included_iter().count()
1180    }
1181}
1182
1183pub(crate) fn orientation_labels_for_graph<I: ProcessIntegrandImpl>(
1184    integrand: &I,
1185    graph_id: usize,
1186) -> Result<Vec<String>> {
1187    let group_id = integrand
1188        .graph_group_id_for_graph(graph_id)
1189        .map(GroupId)
1190        .ok_or_else(|| {
1191            eyre!(
1192                "Unknown graph '{}' while resolving orientation labels.",
1193                graph_id
1194            )
1195        })?;
1196    let master = integrand.get_master_graph(group_id);
1197    Ok((0..master.get_num_orientations())
1198        .map(|orientation_id| {
1199            master
1200                .orientation_label(orientation_id)
1201                .unwrap_or_else(|| format!("#{}", orientation_id))
1202        })
1203        .collect())
1204}
1205
1206pub(crate) fn evaluate_profile_momentum_point<I: ProcessIntegrandImpl>(
1207    integrand: &mut I,
1208    model: &Model,
1209    graph_id: usize,
1210    orientation: Option<usize>,
1211    loop_momenta: Vec<ThreeMomentum<F<f64>>>,
1212    use_arb_prec: bool,
1213) -> Result<EvaluationResult> {
1214    let input = MomentumSpaceEvaluationInput {
1215        loop_momenta,
1216        integrator_weight: F(1.0),
1217        graph_id: Some(graph_id),
1218        group_id: None,
1219        orientation,
1220        channel_id: None,
1221    };
1222    evaluate_momentum_configuration(
1223        integrand,
1224        model,
1225        &input,
1226        F(1.0),
1227        use_arb_prec,
1228        Complex::new_re(F(100.0 * integrand.get_settings().kinematics.e_cm)),
1229    )
1230}
1231
1232pub(crate) fn evaluate_profile_momentum_point_precise<I: ProcessIntegrandImpl>(
1233    integrand: &mut I,
1234    model: &Model,
1235    graph_id: usize,
1236    orientation: Option<usize>,
1237    loop_momenta: Vec<ThreeMomentum<F<f64>>>,
1238    use_arb_prec: bool,
1239) -> Result<PreciseEvaluationResult> {
1240    let input = MomentumSpaceEvaluationInput {
1241        loop_momenta,
1242        integrator_weight: F(1.0),
1243        graph_id: Some(graph_id),
1244        group_id: None,
1245        orientation,
1246        channel_id: None,
1247    };
1248    evaluate_momentum_configuration_precise(
1249        integrand,
1250        model,
1251        &input,
1252        F(1.0),
1253        use_arb_prec,
1254        Complex::new_re(F(100.0 * integrand.get_settings().kinematics.e_cm)),
1255    )
1256}
1257
1258fn format_lmb_channel_label(edge_ids: &[usize]) -> String {
1259    let mut sorted = edge_ids.to_vec();
1260    sorted.sort_unstable();
1261    format!(
1262        "({})",
1263        sorted
1264            .iter()
1265            .map(|edge_id| edge_id.to_string())
1266            .collect_vec()
1267            .join(",")
1268    )
1269}
1270
1271pub(crate) fn histogram_process_info_for_integrand<I: ProcessIntegrandImpl>(
1272    integrand: &I,
1273) -> Result<HistogramProcessInfo> {
1274    let parameterization_settings = integrand
1275        .get_settings()
1276        .sampling
1277        .get_parameterization_settings()
1278        .unwrap_or_default();
1279    let graph_names = (0..integrand.graph_count())
1280        .map(|graph_id| integrand.get_graph(graph_id).name())
1281        .collect_vec();
1282    let graph_to_group_id = (0..integrand.graph_count())
1283        .map(|graph_id| {
1284            integrand
1285                .graph_group_id_for_graph(graph_id)
1286                .unwrap_or_else(|| {
1287                    panic!(
1288                        "graph {} is missing a graph-group mapping for histogram process info",
1289                        graph_id
1290                    )
1291                })
1292        })
1293        .collect_vec();
1294    let graph_group_master_names = integrand
1295        .get_group_structure()
1296        .iter_enumerated()
1297        .map(|(group_id, _)| integrand.get_master_graph(group_id).name())
1298        .collect_vec();
1299    let orientation_labels_by_group = integrand
1300        .get_group_structure()
1301        .iter_enumerated()
1302        .map(|(group_id, _)| {
1303            let master = integrand.get_master_graph(group_id);
1304            (0..master.get_num_orientations())
1305                .map(|orientation_id| {
1306                    master
1307                        .orientation_label(orientation_id)
1308                        .unwrap_or_else(|| format!("#{}", orientation_id))
1309                })
1310                .collect_vec()
1311        })
1312        .collect_vec();
1313    let lmb_channel_labels_by_group = integrand
1314        .get_group_structure()
1315        .iter_enumerated()
1316        .map(|(group_id, _)| {
1317            let master = integrand.get_master_graph(group_id);
1318            (0..master.get_num_channels(&parameterization_settings))
1319                .map(|channel_id| {
1320                    Ok(master
1321                        .lmb_channel_label(
1322                            ChannelIndex::from(channel_id),
1323                            &parameterization_settings,
1324                        )?
1325                        .unwrap_or_else(|| format!("#{}", channel_id)))
1326                })
1327                .collect::<Result<Vec<_>>>()
1328        })
1329        .collect::<Result<Vec<_>>>()?;
1330    Ok(HistogramProcessInfo {
1331        graph_names,
1332        graph_to_group_id,
1333        graph_group_master_names,
1334        orientation_labels_by_group,
1335        lmb_channel_labels_by_group,
1336    })
1337}
1338
1339pub(crate) fn graph_to_group_id_for_group_structure(
1340    group_structure: &TiVec<GroupId, GraphGroup>,
1341) -> Vec<usize> {
1342    group_structure
1343        .iter_enumerated()
1344        .flat_map(|(group_id, group)| {
1345            group
1346                .into_iter()
1347                .map(move |graph_id| (graph_id, group_id.0))
1348        })
1349        .sorted_by_key(|(graph_id, _)| *graph_id)
1350        .map(|(_, group_id)| group_id)
1351        .collect_vec()
1352}
1353
1354pub(crate) struct PreparedBufferedEvent<T: FloatLike> {
1355    pub(crate) buffered_event: Option<GenericEvent<T>>,
1356    pub(crate) selectors_pass: bool,
1357    pub(crate) event_processing_time: Duration,
1358    pub(crate) generated_event_count: usize,
1359    pub(crate) accepted_event_count: usize,
1360}
1361
1362impl<T: FloatLike> Default for PreparedBufferedEvent<T> {
1363    fn default() -> Self {
1364        Self {
1365            buffered_event: None,
1366            selectors_pass: true,
1367            event_processing_time: Duration::ZERO,
1368            generated_event_count: 0,
1369            accepted_event_count: 0,
1370        }
1371    }
1372}
1373
1374pub(crate) fn prepare_buffered_event<T: FloatLike>(
1375    settings: &RuntimeSettings,
1376    rotation: &Rotation,
1377    event_processing_runtime: Option<&mut EventProcessingRuntime>,
1378    build_event: impl FnOnce() -> Result<GenericEvent<T>>,
1379) -> Result<PreparedBufferedEvent<T>> {
1380    let needs_selector_events = event_processing_runtime
1381        .as_ref()
1382        .is_some_and(|runtime| runtime.has_selectors());
1383    let should_buffer_event = rotation.is_identity() && settings.should_buffer_generated_events();
1384    let should_build_event = if rotation.is_identity() {
1385        settings.should_generate_events()
1386    } else {
1387        needs_selector_events
1388    };
1389
1390    if !should_build_event {
1391        return Ok(PreparedBufferedEvent::default());
1392    }
1393
1394    let build_start = Instant::now();
1395    let mut event = build_event()?;
1396    let mut event_processing_time = build_start.elapsed();
1397    let generated_event_count = usize::from(rotation.is_identity());
1398
1399    let selector_start = Instant::now();
1400    let selectors_pass = if let Some(runtime) = event_processing_runtime {
1401        if rotation.is_identity() {
1402            runtime.process_event(&mut event)
1403        } else {
1404            runtime.process_event_for_selectors(&mut event)
1405        }
1406    } else {
1407        true
1408    };
1409    event_processing_time += selector_start.elapsed();
1410
1411    let buffered_event = if selectors_pass && should_buffer_event {
1412        Some(event)
1413    } else {
1414        None
1415    };
1416
1417    // Count selector-accepted physical events even when they are processed only
1418    // transiently for selectors and are not retained in the returned event buffer.
1419    let accepted_event_count = usize::from(rotation.is_identity() && selectors_pass);
1420
1421    Ok(PreparedBufferedEvent {
1422        accepted_event_count,
1423        buffered_event,
1424        selectors_pass,
1425        event_processing_time,
1426        generated_event_count,
1427    })
1428}
1429
1430fn process_evaluation_result_runtime<I: ProcessIntegrandImpl>(
1431    integrand: &mut I,
1432    result: &EvaluationResult,
1433) {
1434    if let Some(runtime) = integrand.event_processing_runtime_mut()
1435        && runtime.has_observables()
1436    {
1437        runtime.process_event_groups(&result.event_groups);
1438    }
1439}
1440
1441fn maybe_discard_generated_events_in_result(
1442    settings: &RuntimeSettings,
1443    result: &mut EvaluationResult,
1444) {
1445    if !settings.should_return_generated_events() {
1446        result.event_groups.clear();
1447    }
1448}
1449
1450fn merge_event_processing_runtime<I: ProcessIntegrandImpl>(
1451    integrand: &mut I,
1452    other: &mut I,
1453) -> Result<()> {
1454    match (
1455        integrand.event_processing_runtime_mut(),
1456        other.event_processing_runtime_mut(),
1457    ) {
1458        (Some(lhs), Some(rhs)) => lhs.merge_samples(rhs),
1459        _ => Ok(()),
1460    }
1461}
1462
1463fn update_event_processing_runtime<I: ProcessIntegrandImpl>(integrand: &mut I, iter: usize) {
1464    if let Some(runtime) = integrand.event_processing_runtime_mut() {
1465        runtime.update_results(iter);
1466    }
1467}
1468
1469fn observable_accumulator_bundle<I: ProcessIntegrandImpl>(
1470    integrand: &I,
1471) -> Option<ObservableAccumulatorBundle> {
1472    integrand
1473        .event_processing_runtime()
1474        .filter(|runtime| runtime.has_observables())
1475        .map(EventProcessingRuntime::accumulator_bundle)
1476}
1477
1478fn observable_snapshot_bundle<I: ProcessIntegrandImpl>(
1479    integrand: &I,
1480) -> Option<ObservableSnapshotBundle> {
1481    integrand
1482        .event_processing_runtime()
1483        .filter(|runtime| runtime.has_observables())
1484        .map(EventProcessingRuntime::snapshot_bundle)
1485}
1486
1487fn build_observable_snapshots_for_result<I: ProcessIntegrandImpl>(
1488    integrand: &I,
1489    result: &EvaluationResult,
1490) -> Option<ObservableSnapshotBundle> {
1491    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1492}
1493
1494fn build_observable_snapshots_for_event_groups<I: ProcessIntegrandImpl, T: FloatLike>(
1495    integrand: &I,
1496    event_groups: &crate::observables::GenericEventGroupList<T>,
1497) -> Option<ObservableSnapshotBundle> {
1498    let runtime = integrand.event_processing_runtime()?;
1499    if !runtime.has_observables() {
1500        return None;
1501    }
1502
1503    let mut runtime = runtime.cleared_observable_clone();
1504    runtime.process_event_groups(event_groups);
1505    Some(runtime.snapshot_bundle())
1506}
1507
1508fn restore_observable_snapshot_bundle<I: ProcessIntegrandImpl>(
1509    integrand: &mut I,
1510    bundle: &ObservableSnapshotBundle,
1511) -> Result<()> {
1512    let runtime = integrand.event_processing_runtime_mut().ok_or_else(|| {
1513        eyre!("Cannot restore observables before the integrand has been warmed up")
1514    })?;
1515    if !runtime.has_observables() {
1516        return Err(eyre!(
1517            "Cannot restore observable snapshots for an integrand without configured observables"
1518        ));
1519    }
1520    runtime.restore_snapshot_bundle(bundle)
1521}
1522
1523fn full_event_multiplicative_factor(
1524    parameterization_jacobian: Option<F<f64>>,
1525    integrator_weight: F<f64>,
1526) -> Complex<F<f64>> {
1527    let jacobian = parameterization_jacobian.unwrap_or(F(1.0));
1528    Complex::new_re(jacobian * integrator_weight)
1529}
1530
1531fn apply_full_event_multiplicative_factor(
1532    event_groups: &mut crate::observables::EventGroupList,
1533    full_factor: &Complex<F<f64>>,
1534) {
1535    for event_group in event_groups.iter_mut() {
1536        for event in event_group.iter_mut() {
1537            event.weight *= full_factor;
1538            if !event.additional_weights.weights.is_empty() {
1539                event
1540                    .additional_weights
1541                    .weights
1542                    .entry(AdditionalWeightKey::FullMultiplicativeFactor)
1543                    .and_modify(|value| *value *= full_factor)
1544                    .or_insert_with(|| *full_factor);
1545            }
1546        }
1547    }
1548}
1549
1550fn full_event_multiplicative_factor_precise<T: FloatLike>(
1551    parameterization_jacobian: Option<F<T>>,
1552    integrator_weight: F<T>,
1553) -> Complex<F<T>> {
1554    let jacobian = parameterization_jacobian.unwrap_or_else(|| integrator_weight.one());
1555    Complex::new_re(jacobian * integrator_weight)
1556}
1557
1558fn apply_full_event_multiplicative_factor_precise<T: FloatLike>(
1559    event_groups: &mut crate::observables::GenericEventGroupList<T>,
1560    full_factor: &Complex<F<T>>,
1561) {
1562    for event_group in event_groups.iter_mut() {
1563        for event in event_group.iter_mut() {
1564            event.weight *= full_factor.clone();
1565
1566            if !event.additional_weights.weights.is_empty() {
1567                event.additional_weights.weights.insert(
1568                    AdditionalWeightKey::FullMultiplicativeFactor,
1569                    full_factor.clone(),
1570                );
1571            }
1572        }
1573    }
1574}
1575
1576pub(crate) fn write_observable_snapshot_bundle(
1577    bundle: &ObservableSnapshotBundle,
1578    path: &Path,
1579    format: ObservableFileFormat,
1580) -> Result<()> {
1581    match format {
1582        ObservableFileFormat::None => Ok(()),
1583        ObservableFileFormat::Hwu => bundle.write_hwu_file(path),
1584        ObservableFileFormat::Json => bundle.to_json_file(path),
1585    }
1586}
1587
1588#[derive(Debug, Clone, Copy)]
1589pub enum IntegrandType {
1590    Amplitude,
1591    CrossSection,
1592}
1593
1594fn create_stability_iterator(
1595    settings: &StabilitySettings,
1596    use_arb_prec: bool,
1597) -> Vec<StabilityLevelSetting> {
1598    if use_arb_prec {
1599        // Force arbitrary precision, reusing the configured Arb thresholds when available.
1600        if let Some(arb_settings_position) = settings
1601            .levels
1602            .iter()
1603            .position(|stability_level_setting| stability_level_setting.precision == Precision::Arb)
1604        {
1605            vec![settings.levels[arb_settings_position]]
1606        } else {
1607            vec![StabilityLevelSetting {
1608                precision: Precision::Arb,
1609                required_precision_for_re: 1e-5,
1610                required_precision_for_im: 1e-5,
1611                escalate_for_large_weight_threshold: -1.,
1612            }]
1613        }
1614    } else {
1615        settings.levels.clone()
1616    }
1617}
1618
1619#[inline]
1620fn complex_from_f64<T: FloatLike>(value: &Complex<F<f64>>) -> Complex<F<T>> {
1621    Complex::new(F::<T>::from_ff64(value.re), F::<T>::from_ff64(value.im))
1622}
1623
1624#[inline]
1625fn complex_to_f64<T: FloatLike>(value: &Complex<F<T>>) -> Complex<F<f64>> {
1626    Complex::new(value.re.into_ff64(), value.im.into_ff64())
1627}
1628
1629type StabilityCheckResult<T> = (
1630    Complex<F<T>>,
1631    Option<F<T>>,
1632    bool,
1633    Option<StabilityFailureReason>,
1634);
1635
1636#[inline]
1637fn stability_check<T: FloatLike>(
1638    _settings: &RuntimeSettings,
1639    results: &[Complex<F<T>>],
1640    stability_settings: &StabilityLevelSetting,
1641    max_eval: Complex<F<T>>,
1642    wgt: F<T>,
1643    is_final_level: bool,
1644    escalate_if_exact_zero: bool,
1645) -> StabilityCheckResult<T> {
1646    if results.len() == 1 {
1647        return (results[0].clone(), None, true, None);
1648    }
1649
1650    if !is_final_level
1651        && results.iter().any(|result| {
1652            result.re.is_nan()
1653                || result.re.is_infinite()
1654                || result.im.is_nan()
1655                || result.im.is_infinite()
1656        })
1657    {
1658        return (
1659            results[0].clone(),
1660            None,
1661            false,
1662            Some(StabilityFailureReason::ErrorThreshold),
1663        );
1664    }
1665
1666    let average = results
1667        .iter()
1668        .skip(1)
1669        .fold(results[0].clone(), |acc, x| acc + x)
1670        / F::<T>::from_f64(results.len() as f64);
1671
1672    let errors = results.iter().map(|res| {
1673        let error_re = if IsZero::is_zero(&res.re) && IsZero::is_zero(&average.re) {
1674            F::<T>::from_f64(0.0)
1675        } else {
1676            ((&res.re - &average.re) / &average.re).abs()
1677        };
1678        let error_im = if IsZero::is_zero(&res.im) && IsZero::is_zero(&average.im) {
1679            F::<T>::from_f64(0.0)
1680        } else {
1681            ((&res.im - &average.im) / &average.im).abs()
1682        };
1683        Complex::new(error_re, error_im)
1684    });
1685    let mut estimated_relative_accuracy = average.re.zero();
1686
1687    let mut unstable_reason = None;
1688    let mut unstable_sample = None;
1689    for (index, error) in errors.enumerate() {
1690        estimated_relative_accuracy =
1691            estimated_relative_accuracy.max(error.re.clone().max(error.im.clone()));
1692        if !is_final_level
1693            && escalate_if_exact_zero
1694            && error.re == F::<T>::from_f64(0.0)
1695            && error.im == F::<T>::from_f64(0.0)
1696        {
1697            unstable_reason = Some(StabilityFailureReason::ZeroError);
1698            unstable_sample = Some(index);
1699            break;
1700        }
1701
1702        if error.re > F::<T>::from_f64(stability_settings.required_precision_for_re)
1703            || error.im > F::<T>::from_f64(stability_settings.required_precision_for_im)
1704        {
1705            unstable_reason = Some(StabilityFailureReason::ErrorThreshold);
1706            unstable_sample = Some(index);
1707            break;
1708        }
1709    }
1710
1711    if let Some(unstable_index) = unstable_sample {
1712        let unstable_point = &results[unstable_index];
1713
1714        let ((real_formatted, rotated_real_formatted), (imag_formatted, rotated_imag_formatted)) = (
1715            format_for_compare_digits(
1716                average.re.clone().into_ff64(),
1717                unstable_point.re.clone().into_ff64(),
1718            ),
1719            format_for_compare_digits(
1720                average.im.clone().into_ff64(),
1721                unstable_point.im.clone().into_ff64(),
1722            ),
1723        );
1724
1725        debug!("{}", "\nUnstable point detected:".red());
1726        debug!("\taverage result: {} + {}i", real_formatted, imag_formatted,);
1727        debug!(
1728            "\trotated result: {} + {}i",
1729            rotated_real_formatted, rotated_imag_formatted,
1730        );
1731    }
1732
1733    let stable = unstable_sample.is_none();
1734
1735    let below_wgt_threshold = if stability_settings.escalate_for_large_weight_threshold > 0.
1736        && max_eval.is_non_zero()
1737    {
1738        average.re.abs() * wgt.clone()
1739            < F::<T>::from_f64(stability_settings.escalate_for_large_weight_threshold) * max_eval.re
1740            || average.im.abs() * wgt
1741                < F::<T>::from_f64(stability_settings.escalate_for_large_weight_threshold)
1742                    * max_eval.im
1743    } else {
1744        true
1745    };
1746
1747    let weight_reason = if stable && !below_wgt_threshold {
1748        Some(StabilityFailureReason::WeightThreshold)
1749    } else {
1750        None
1751    };
1752
1753    (
1754        average,
1755        Some(estimated_relative_accuracy),
1756        stable && below_wgt_threshold,
1757        unstable_reason.or(weight_reason),
1758    )
1759}
1760
1761#[inline]
1762fn stability_check_on_norm<T: FloatLike>(
1763    _settings: &RuntimeSettings,
1764    results: &[Complex<F<T>>],
1765    stability_settings: &StabilityLevelSetting,
1766    max_eval: Complex<F<T>>,
1767    wgt: F<T>,
1768    is_final_level: bool,
1769    escalate_if_exact_zero: bool,
1770) -> StabilityCheckResult<T> {
1771    if results.len() == 1 {
1772        return (results[0].clone(), None, true, None);
1773    }
1774
1775    if !is_final_level
1776        && results.iter().any(|result| {
1777            result.re.is_nan()
1778                || result.re.is_infinite()
1779                || result.im.is_nan()
1780                || result.im.is_infinite()
1781        })
1782    {
1783        return (
1784            results[0].clone(),
1785            None,
1786            false,
1787            Some(StabilityFailureReason::ErrorThreshold),
1788        );
1789    }
1790
1791    let average = results.iter().fold(F::<T>::from_f64(0.0), |acc, x| {
1792        acc + x.norm_squared().sqrt()
1793    }) / F::<T>::from_f64(results.len() as f64);
1794
1795    let errors = results.iter().map(|res| {
1796        let res = res.norm_squared().sqrt();
1797        if IsZero::is_zero(&res) && IsZero::is_zero(&average) {
1798            (F::<T>::from_f64(0.0), true) // true zero is fishy -> upgrade to next precision
1799        } else {
1800            (((res - average.clone()) / average.clone()).abs(), false)
1801        }
1802    });
1803    let mut estimated_relative_accuracy = average.zero();
1804
1805    let mut unstable_reason = None;
1806    let mut unstable_sample = None;
1807    for (index, (error, result_is_exact_zero)) in errors.enumerate() {
1808        estimated_relative_accuracy = estimated_relative_accuracy.max(error.clone());
1809        if !is_final_level
1810            && error == F::<T>::from_f64(0.0)
1811            && result_is_exact_zero
1812            && escalate_if_exact_zero
1813        {
1814            unstable_reason = Some(StabilityFailureReason::ZeroError);
1815            unstable_sample = Some(index);
1816            break;
1817        }
1818
1819        if error > F::<T>::from_f64(stability_settings.required_precision_for_re) {
1820            unstable_reason = Some(StabilityFailureReason::ErrorThreshold);
1821            unstable_sample = Some(index);
1822            break;
1823        }
1824    }
1825
1826    if let Some(unstable_index) = unstable_sample {
1827        let unstable_point = &results[unstable_index];
1828
1829        let (real_formatted, rotated_real_formatted) = format_for_compare_digits(
1830            average.clone().into_ff64(),
1831            unstable_point.re.clone().into_ff64(),
1832        );
1833
1834        debug!("{}", "\nUnstable point detected:".red());
1835        debug!("\tnormed average result: {}", real_formatted,);
1836        debug!("\tnormed rotated result: {}", rotated_real_formatted,);
1837    }
1838
1839    let stable = unstable_sample.is_none();
1840
1841    let below_wgt_threshold =
1842        if stability_settings.escalate_for_large_weight_threshold > 0. && max_eval.is_non_zero() {
1843            average.abs() * wgt
1844                < F::<T>::from_f64(stability_settings.escalate_for_large_weight_threshold)
1845                    * max_eval.norm_squared().sqrt()
1846        } else {
1847            true
1848        };
1849
1850    let weight_reason = if stable && !below_wgt_threshold {
1851        Some(StabilityFailureReason::WeightThreshold)
1852    } else {
1853        None
1854    };
1855
1856    (
1857        results[0].clone(),
1858        Some(estimated_relative_accuracy),
1859        stable && below_wgt_threshold,
1860        unstable_reason.or(weight_reason),
1861    )
1862}
1863
1864#[derive(Debug, Clone)]
1865pub struct StabilityLevelResult {
1866    pub result: Complex<F<f64>>,
1867    pub graph_result: GraphEvaluationResult<f64>,
1868    pub stability_level_used: Precision,
1869    pub estimated_relative_accuracy: Option<F<f64>>,
1870    pub sample_count: usize,
1871    pub total_time: Duration,
1872    pub parameterization_time: Duration,
1873    pub parameterization_jacobian: Option<F<f64>>,
1874    pub integrand_evaluation_time: Duration,
1875    pub evaluator_evaluation_time: Duration,
1876    pub is_stable: bool,
1877    pub instability_reason: Option<StabilityFailureReason>,
1878    pub rotated_results: Vec<RotatedEvaluation>,
1879}
1880
1881#[derive(Debug, Clone)]
1882struct PreciseStabilityLevelResult<T: FloatLike> {
1883    pub result: Complex<F<T>>,
1884    pub graph_result: GraphEvaluationResult<T>,
1885    pub stability_level_used: Precision,
1886    pub estimated_relative_accuracy: Option<F<T>>,
1887    pub sample_count: usize,
1888    pub total_time: Duration,
1889    pub parameterization_time: Duration,
1890    pub parameterization_jacobian: Option<F<T>>,
1891    pub integrand_evaluation_time: Duration,
1892    pub evaluator_evaluation_time: Duration,
1893    pub is_stable: bool,
1894    pub instability_reason: Option<StabilityFailureReason>,
1895    pub rotated_results: Vec<RotatedEvaluation>,
1896}
1897
1898impl<T: FloatLike> PreciseStabilityLevelResult<T> {
1899    fn into_f64(self) -> StabilityLevelResult {
1900        StabilityLevelResult {
1901            result: complex_to_f64(&self.result),
1902            graph_result: self.graph_result.into_f64(),
1903            stability_level_used: self.stability_level_used,
1904            estimated_relative_accuracy: self
1905                .estimated_relative_accuracy
1906                .map(|value| value.into_ff64()),
1907            sample_count: self.sample_count,
1908            total_time: self.total_time,
1909            parameterization_time: self.parameterization_time,
1910            parameterization_jacobian: self
1911                .parameterization_jacobian
1912                .map(|value| value.into_ff64()),
1913            integrand_evaluation_time: self.integrand_evaluation_time,
1914            evaluator_evaluation_time: self.evaluator_evaluation_time,
1915            is_stable: self.is_stable,
1916            instability_reason: self.instability_reason,
1917            rotated_results: self.rotated_results,
1918        }
1919    }
1920}
1921
1922#[derive(
1923    Debug, Clone, Copy, From, Into, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize,
1924)]
1925pub struct ChannelIndex(usize);
1926
1927type LmbChannelSamples<T> = TiVec<ChannelIndex, (MomentumSample<T>, F<T>)>;
1928
1929/// Helper struct for the LMB multi-channeling setup
1930#[derive(Clone, Encode, Decode)]
1931#[trait_decode(trait = GammaLoopContext)]
1932pub struct LmbMultiChannelingSetup {
1933    pub channels: TiVec<ChannelIndex, LmbIndex>,
1934    /// Canonical group-master graph used to resolve group-level channel overrides.
1935    pub graph: Graph,
1936    pub all_bases: TiVec<LmbIndex, LoopMomentumBasis>,
1937}
1938
1939pub(crate) struct LmbChannelWeightingSettings<'a, T: FloatLike> {
1940    pub(crate) graph_name: &'a str,
1941    pub(crate) model: &'a Model,
1942    pub(crate) alpha: &'a F<T>,
1943    pub(crate) channel_weight: LmbChannelWeight,
1944    pub(crate) parameterization_settings: &'a ParameterizationSettings,
1945    pub(crate) e_cm: f64,
1946}
1947
1948impl<'a, T: FloatLike> Copy for LmbChannelWeightingSettings<'a, T> {}
1949
1950impl<'a, T: FloatLike> Clone for LmbChannelWeightingSettings<'a, T> {
1951    fn clone(&self) -> Self {
1952        *self
1953    }
1954}
1955
1956impl LmbMultiChannelingSetup {
1957    fn validate_lmb_basis_id(&self, basis_id: usize, graph_name: &str) -> Result<LmbIndex> {
1958        if basis_id >= self.all_bases.len() {
1959            return Err(eyre!(
1960                "Requested LMB basis id {} is out of range for graph '{}'; the graph has {} generated LMB bases.",
1961                basis_id,
1962                graph_name,
1963                self.all_bases.len()
1964            ));
1965        }
1966        Ok(LmbIndex::from(basis_id))
1967    }
1968
1969    pub fn effective_channels(
1970        &self,
1971        graph_name: &str,
1972        parameterization_settings: &ParameterizationSettings,
1973    ) -> Result<Vec<LmbIndex>> {
1974        if let Some(basis_ids) = parameterization_settings.lmb_basis_ids.get(graph_name) {
1975            basis_ids
1976                .iter()
1977                .copied()
1978                .map(|basis_id| self.validate_lmb_basis_id(basis_id, graph_name))
1979                .collect()
1980        } else {
1981            Ok(self.channels.iter().copied().collect())
1982        }
1983    }
1984
1985    pub fn effective_channel_count(
1986        &self,
1987        graph_name: &str,
1988        parameterization_settings: &ParameterizationSettings,
1989    ) -> usize {
1990        parameterization_settings
1991            .lmb_basis_ids
1992            .get(graph_name)
1993            .map_or_else(|| self.channels.len(), Vec::len)
1994    }
1995
1996    pub fn effective_channel_lmb_id(
1997        &self,
1998        channel_index: ChannelIndex,
1999        graph_name: &str,
2000        parameterization_settings: &ParameterizationSettings,
2001    ) -> Result<LmbIndex> {
2002        if let Some(basis_ids) = parameterization_settings.lmb_basis_ids.get(graph_name) {
2003            let basis_id = basis_ids.get(channel_index.0).ok_or_else(|| {
2004                eyre!(
2005                    "Requested LMB channel {} is out of range for graph '{}'; the graph has {} effective LMB channels.",
2006                    channel_index.0,
2007                    graph_name,
2008                    basis_ids.len()
2009                )
2010            })?;
2011            self.validate_lmb_basis_id(*basis_id, graph_name)
2012        } else {
2013            self.channels.get(channel_index).copied().ok_or_else(|| {
2014                eyre!(
2015                    "Requested LMB channel {} is out of range for graph '{}'; the graph has {} effective LMB channels.",
2016                    channel_index.0,
2017                    graph_name,
2018                    self.channels.len()
2019                )
2020            })
2021        }
2022    }
2023
2024    pub fn effective_channel_edge_ids(
2025        &self,
2026        channel_index: ChannelIndex,
2027        graph_name: &str,
2028        parameterization_settings: &ParameterizationSettings,
2029    ) -> Result<SmallVec<[usize; 4]>> {
2030        Ok(self.all_bases[self.effective_channel_lmb_id(
2031            channel_index,
2032            graph_name,
2033            parameterization_settings,
2034        )?]
2035        .loop_edges
2036        .iter()
2037        .map(|edge_id| edge_id.0)
2038        .collect())
2039    }
2040
2041    pub fn selected_lmb_basis_id(
2042        &self,
2043        graph_name: &str,
2044        parameterization_settings: &ParameterizationSettings,
2045    ) -> Result<LmbIndex> {
2046        let effective_channels = self.effective_channels(graph_name, parameterization_settings)?;
2047        effective_channels.first().copied().ok_or_else(|| {
2048            eyre!(
2049                "Could not select a default LMB basis for graph '{}'; the optimized LMB subset is empty.",
2050                graph_name
2051            )
2052        })
2053    }
2054
2055    fn reinterpret_loop_momenta_for_lmb_impl<T: FloatLike>(
2056        &self,
2057        lmb_index: LmbIndex,
2058        momentum_sample: &BareMomentumSample<T>,
2059        loop_mom_cache_id: usize,
2060    ) -> BareMomentumSample<T> {
2061        let channel_lmb = &self.all_bases[lmb_index];
2062        let new_loop_moms: LoopMomenta<F<T>> = self
2063            .graph
2064            .loop_momentum_basis
2065            .loop_edges
2066            .iter()
2067            .map(|&edge_index| {
2068                let signature_of_edge_channel_lmb = &channel_lmb.edge_signatures[edge_index];
2069
2070                signature_of_edge_channel_lmb
2071                    .internal
2072                    .apply_typed(&momentum_sample.loop_moms)
2073                    + signature_of_edge_channel_lmb
2074                        .external
2075                        .apply(&momentum_sample.external_moms.raw)
2076                        .spatial
2077            })
2078            .collect();
2079
2080        BareMomentumSample {
2081            loop_moms: new_loop_moms,
2082            dual_loop_moms: momentum_sample.dual_loop_moms.clone().map(|_dlm| todo!()),
2083            loop_mom_cache_id,
2084            loop_mom_base_cache_id: momentum_sample.loop_mom_base_cache_id,
2085            external_mom_cache_id: momentum_sample.external_mom_cache_id,
2086            external_mom_base_cache_id: momentum_sample.external_mom_base_cache_id,
2087            external_moms: momentum_sample.external_moms.clone(),
2088            jacobian: momentum_sample.jacobian.clone(),
2089            orientation: momentum_sample.orientation,
2090            parameterization_branch: momentum_sample.parameterization_branch,
2091        }
2092    }
2093
2094    pub(crate) fn reinterpret_loop_momenta_for_lmb<T: FloatLike>(
2095        &self,
2096        lmb_index: LmbIndex,
2097        momentum_sample: &MomentumSample<T>,
2098        loop_mom_cache_id: usize,
2099    ) -> MomentumSample<T> {
2100        MomentumSample {
2101            sample: self.reinterpret_loop_momenta_for_lmb_impl(
2102                lmb_index,
2103                &momentum_sample.sample,
2104                loop_mom_cache_id,
2105            ),
2106        }
2107    }
2108
2109    /// Note this increments the loop_mom_cache_id of all of the returned BareMomentumSample
2110    #[allow(dead_code)]
2111    pub(crate) fn reinterpret_loop_momenta_and_compute_prefactor_all_channels<T: FloatLike>(
2112        &self,
2113        momentum_sample: &MomentumSample<T>,
2114        weighting_settings: LmbChannelWeightingSettings<'_, T>,
2115        cache: bool,
2116    ) -> Result<LmbChannelSamples<T>> {
2117        let mut loop_mom_cache_id = momentum_sample.sample.loop_mom_cache_id;
2118        let effective_channels = self.effective_channels(
2119            weighting_settings.graph_name,
2120            weighting_settings.parameterization_settings,
2121        )?;
2122        effective_channels
2123            .iter()
2124            .enumerate()
2125            .map(|(channel_index, _)| {
2126                if cache {
2127                    loop_mom_cache_id += 1;
2128                }
2129                self.reinterpret_loop_momenta_and_compute_prefactor(
2130                    ChannelIndex::from(channel_index),
2131                    momentum_sample,
2132                    loop_mom_cache_id,
2133                    weighting_settings,
2134                )
2135            })
2136            .collect()
2137    }
2138
2139    /// This function is used to do do LMB multi-channeling without fully switching to a different lmb
2140    /// for each channel. The momenta provided are reinterpreted as loop momenta of the lmb corresponding to the channel_index.
2141    /// Then we transform these loop momenta to the fixed lmb of the graph. The prefactor is immediately computed for the requested channel
2142    ///
2143    /// Note this increments the loop_mom_cache_id of the returned BareMomentumSample
2144    pub(crate) fn reinterpret_loop_momenta_and_compute_prefactor<T: FloatLike>(
2145        &self,
2146        channel_index: ChannelIndex,
2147        momentum_sample: &MomentumSample<T>,
2148        loop_mom_cache_id: usize,
2149        weighting_settings: LmbChannelWeightingSettings<'_, T>,
2150    ) -> Result<(MomentumSample<T>, F<T>)> {
2151        let lmb_index = self.effective_channel_lmb_id(
2152            channel_index,
2153            weighting_settings.graph_name,
2154            weighting_settings.parameterization_settings,
2155        )?;
2156        let sample = MomentumSample {
2157            sample: self.reinterpret_loop_momenta_for_lmb_impl(
2158                lmb_index,
2159                &momentum_sample.sample,
2160                loop_mom_cache_id,
2161            ), // uuid: momentum_sample.uuid,
2162        };
2163
2164        let prefactor =
2165            self.compute_prefactor_impl(channel_index, lmb_index, &sample, weighting_settings)?;
2166
2167        Ok((sample, prefactor))
2168    }
2169
2170    /// Computes the prefactor for the given channel index and momentum sample.
2171    pub(crate) fn compute_prefactor_impl<T: FloatLike>(
2172        &self,
2173        channel_index: ChannelIndex,
2174        selected_lmb: LmbIndex,
2175        momentum_sample: &MomentumSample<T>,
2176        weighting_settings: LmbChannelWeightingSettings<'_, T>,
2177    ) -> Result<F<T>> {
2178        let effective_channels = self.effective_channels(
2179            weighting_settings.graph_name,
2180            weighting_settings.parameterization_settings,
2181        )?;
2182        if usize::from(channel_index) >= effective_channels.len() {
2183            return Err(eyre!(
2184                "Requested LMB channel {} is out of range for graph '{}'; the graph has {} effective LMB channels.",
2185                usize::from(channel_index),
2186                weighting_settings.graph_name,
2187                effective_channels.len()
2188            ));
2189        }
2190
2191        match weighting_settings.channel_weight {
2192            LmbChannelWeight::Ose => Ok(self.compute_ose_prefactor_impl(
2193                selected_lmb,
2194                &effective_channels,
2195                momentum_sample,
2196                weighting_settings.model,
2197                weighting_settings.alpha,
2198            )),
2199            LmbChannelWeight::InverseJacobian => Ok(self.compute_inverse_jacobian_prefactor_impl(
2200                selected_lmb,
2201                &effective_channels,
2202                momentum_sample,
2203                weighting_settings.parameterization_settings,
2204                weighting_settings.e_cm,
2205            )),
2206        }
2207    }
2208
2209    fn compute_ose_prefactor_impl<T: FloatLike>(
2210        &self,
2211        selected_lmb: LmbIndex,
2212        effective_channels: &[LmbIndex],
2213        momentum_sample: &MomentumSample<T>,
2214        model: &Model,
2215        alpha: &F<T>,
2216    ) -> F<T> {
2217        let all_energies = self.graph.get_energy_cache(
2218            model,
2219            &momentum_sample.sample.loop_moms,
2220            &momentum_sample.sample.external_moms,
2221            &self.graph.loop_momentum_basis,
2222        );
2223
2224        let mut numerator = momentum_sample.zero();
2225
2226        let denominators = effective_channels
2227            .iter()
2228            .map(|&lmb_index| {
2229                let channel_product = self.all_bases[lmb_index]
2230                    .loop_edges
2231                    .iter()
2232                    .map(|&edge_index| &all_energies[edge_index])
2233                    .fold(momentum_sample.one(), |product, energy| product * energy)
2234                    .powf(&-alpha);
2235
2236                if selected_lmb == lmb_index {
2237                    numerator = channel_product.clone();
2238                }
2239
2240                channel_product
2241            })
2242            .fold(momentum_sample.zero(), |sum, summand| sum + summand);
2243
2244        numerator / denominators
2245    }
2246
2247    fn compute_inverse_jacobian_prefactor_impl<T: FloatLike>(
2248        &self,
2249        selected_lmb: LmbIndex,
2250        effective_channels: &[LmbIndex],
2251        momentum_sample: &MomentumSample<T>,
2252        parameterization_settings: &ParameterizationSettings,
2253        e_cm: f64,
2254    ) -> F<T> {
2255        let mut numerator = momentum_sample.zero();
2256        let e_cm = F::<T>::from_f64(e_cm);
2257
2258        if matches!(
2259            parameterization_settings.mode,
2260            ParameterizationMode::SphericalProductCommonRadial
2261        ) {
2262            let product_settings = ParameterizationSettings {
2263                mode: ParameterizationMode::Spherical,
2264                mapping: parameterization_settings.mapping.clone(),
2265                b: parameterization_settings.b,
2266                power: parameterization_settings.power,
2267                lmb_basis_ids: Default::default(),
2268            };
2269            let common_radial_settings = ParameterizationSettings {
2270                mode: ParameterizationMode::SphericalCommonRadial,
2271                mapping: parameterization_settings.mapping.clone(),
2272                b: parameterization_settings.b,
2273                power: parameterization_settings.power,
2274                lmb_basis_ids: Default::default(),
2275            };
2276            let sampled_branch = momentum_sample.sample.parameterization_branch;
2277            let denominator = effective_channels
2278                .iter()
2279                .map(|&lmb_index| {
2280                    let basis_momenta = self.basis_momenta_for_lmb(lmb_index, momentum_sample);
2281                    let (_, product_inverse_jacobian) =
2282                        global_inv_parameterize(&basis_momenta, e_cm.clone(), &product_settings);
2283                    let (_, common_radial_inverse_jacobian) = global_inv_parameterize(
2284                        &basis_momenta,
2285                        e_cm.clone(),
2286                        &common_radial_settings,
2287                    );
2288
2289                    if selected_lmb == lmb_index {
2290                        numerator = match sampled_branch {
2291                            Some(0) => product_inverse_jacobian.clone(),
2292                            Some(1) => common_radial_inverse_jacobian.clone(),
2293                            _ => {
2294                                product_inverse_jacobian.clone()
2295                                    + common_radial_inverse_jacobian.clone()
2296                            }
2297                        };
2298                    }
2299
2300                    product_inverse_jacobian + common_radial_inverse_jacobian
2301                })
2302                .fold(momentum_sample.zero(), |sum, summand| sum + summand);
2303
2304            return if denominator.is_zero() {
2305                momentum_sample.zero()
2306            } else {
2307                numerator / denominator
2308            };
2309        }
2310
2311        let denominator = effective_channels
2312            .iter()
2313            .map(|&lmb_index| {
2314                let basis_momenta = self.basis_momenta_for_lmb(lmb_index, momentum_sample);
2315                let (_, inverse_jacobian) = global_inv_parameterize(
2316                    &basis_momenta,
2317                    e_cm.clone(),
2318                    parameterization_settings,
2319                );
2320
2321                if selected_lmb == lmb_index {
2322                    numerator = inverse_jacobian.clone();
2323                }
2324
2325                inverse_jacobian
2326            })
2327            .fold(momentum_sample.zero(), |sum, summand| sum + summand);
2328
2329        if denominator.is_zero() {
2330            momentum_sample.zero()
2331        } else {
2332            numerator / denominator
2333        }
2334    }
2335
2336    fn basis_momenta_for_lmb<T: FloatLike>(
2337        &self,
2338        lmb_index: LmbIndex,
2339        momentum_sample: &MomentumSample<T>,
2340    ) -> Vec<ThreeMomentum<F<T>>> {
2341        self.all_bases[lmb_index]
2342            .loop_edges
2343            .iter()
2344            .map(|&edge_index| {
2345                let edge_signature = &self.graph.loop_momentum_basis.edge_signatures[edge_index];
2346
2347                edge_signature
2348                    .internal
2349                    .apply_typed(&momentum_sample.sample.loop_moms)
2350                    + edge_signature
2351                        .external
2352                        .apply(&momentum_sample.sample.external_moms.raw)
2353                        .spatial
2354            })
2355            .collect()
2356    }
2357}
2358
2359pub trait ProcessIntegrandImpl {
2360    type G: GraphTerm;
2361
2362    fn warm_up(&mut self, model: &Model) -> Result<()>;
2363    fn get_rotations(&self) -> impl Iterator<Item = &Rotation>;
2364
2365    fn increment_loop_cache_id(&mut self, val: usize);
2366    fn loop_cache_id(&self) -> usize;
2367
2368    fn increment_external_cache_id(&mut self, val: usize);
2369    fn external_cache_id(&self) -> usize;
2370
2371    /// Signal that external momenta configuration has actually changed
2372    /// This increments the external cache ID to invalidate cached computations
2373    ///
2374    /// # Usage
2375    /// Call this method when:
2376    /// - You change the external momenta values in your Monte Carlo sampling
2377    /// - You switch to a different kinematic configuration
2378    /// - You want to force recomputation of cached polarizations/external quantities
2379    ///
2380    /// # Example
2381    /// ```rust,ignore
2382    /// // When you change external momenta in your sampling
2383    /// integrand.signal_external_momenta_changed();
2384    /// let new_sample = parameterize(&sample_point, &mut integrand)?;
2385    ///
2386    /// // Rotations will automatically get new cache IDs
2387    /// let rotated_samples = evaluate_all_rotations(&new_sample, &mut integrand, true)?;
2388    ///
2389    /// // Next iteration - revert to base configuration to reuse cache
2390    /// integrand.revert_to_base_external_cache_id();
2391    /// let next_sample = parameterize(&next_sample_point, &mut integrand)?;
2392    /// ```
2393    fn signal_external_momenta_changed(&mut self) {
2394        self.increment_external_cache_id(1);
2395    }
2396
2397    /// Get the current external cache ID for reuse (doesn't increment)
2398    ///
2399    /// This returns the current cache ID without incrementing it, allowing
2400    /// new samples to reuse cached computations for the same external
2401    /// momenta configuration.
2402    fn get_current_external_cache_id(&self) -> usize {
2403        self.external_cache_id()
2404    }
2405
2406    /// Revert to the base external cache ID for the current configuration
2407    ///
2408    /// This allows new samples to reuse the base cache ID when they represent
2409    /// the same underlying external momenta configuration (e.g., after rotations)
2410    ///
2411    /// # Usage
2412    /// Call this method when you want to create a new sample that represents
2413    /// the same base external momenta configuration as before, allowing reuse
2414    /// of cached polarizations and other external-dependent computations.
2415    ///
2416    /// # Example Cache Flow
2417    /// ```text
2418    /// 1. Initial sample:           cache_id = 0, base_cache_id = 0
2419    /// 2. Rotation 1:               cache_id = 1, base_cache_id = 0
2420    /// 3. Rotation 2:               cache_id = 2, base_cache_id = 0
2421    /// 4. revert_to_base():         cache_id = 0, base_cache_id = 0  // Reuse cache!
2422    /// 5. signal_changed():         cache_id = 3, base_cache_id = 3  // New base config
2423    /// 6. Rotation of new config:   cache_id = 4, base_cache_id = 3
2424    /// 7. revert_to_base():         cache_id = 3, base_cache_id = 3  // Reuse new base
2425    /// ```
2426    fn revert_to_base_external_cache_id(&mut self);
2427
2428    /// Check if external momenta caching is beneficial
2429    ///
2430    /// Returns true if the same external cache ID has been used multiple times,
2431    /// indicating that caching is providing benefits.
2432    fn is_external_caching_beneficial(&self) -> bool {
2433        // Simple heuristic: if we've created samples without incrementing
2434        // cache ID, then caching is being used
2435        self.external_cache_id() < self.loop_cache_id()
2436    }
2437
2438    /// Force a cache consistency check with detailed reporting
2439    fn debug_cache_state(&self, context: &str) {
2440        if std::env::var("GAMMALOOP_DEBUG_CACHE").is_ok() {
2441            let validation = self.validate_cache_consistency();
2442            let stats = self.get_cache_stats();
2443
2444            tracing::info!("🔍 DEBUG CACHE STATE at {}", context);
2445            tracing::info!("   Validation: {}", validation);
2446            tracing::info!("   Statistics: {}", stats);
2447
2448            if !validation.is_valid {
2449                tracing::error!("   ❌ CACHE INCONSISTENCY DETECTED!");
2450                panic!(
2451                    "Cache corruption at {}: {}",
2452                    context, validation.diagnostics
2453                );
2454            }
2455
2456            if validation.has_rotations {
2457                tracing::info!(
2458                    "   🔄 {} rotation variants from base cache_id {}",
2459                    validation.current_external_cache_id - validation.base_external_cache_id,
2460                    validation.base_external_cache_id
2461                );
2462            }
2463
2464            if stats.efficiency_ratio < 0.3 {
2465                tracing::warn!(
2466                    "   ⚠️ Very low cache efficiency: {:.1}%",
2467                    stats.efficiency_ratio * 100.0
2468                );
2469            } else if stats.efficiency_ratio > 0.8 {
2470                tracing::info!(
2471                    "   ✅ Excellent cache efficiency: {:.1}%",
2472                    stats.efficiency_ratio * 100.0
2473                );
2474            }
2475        }
2476    }
2477
2478    /// Get the base external cache ID for the current configuration
2479    fn get_base_external_cache_id(&self) -> usize;
2480
2481    /// Validate cache ID consistency and return diagnostics
2482    fn validate_cache_consistency(&self) -> CacheValidationResult {
2483        let current_id = self.external_cache_id();
2484        let base_id = self.get_base_external_cache_id();
2485        let loop_id = self.loop_cache_id();
2486
2487        let is_valid = base_id <= current_id;
2488        let has_rotations = current_id > base_id;
2489        let cache_efficiency = if loop_id > 0 {
2490            1.0 - (current_id as f64 / loop_id as f64)
2491        } else {
2492            0.0
2493        };
2494
2495        CacheValidationResult {
2496            is_valid,
2497            current_external_cache_id: current_id,
2498            base_external_cache_id: base_id,
2499            loop_cache_id: loop_id,
2500            has_rotations,
2501            cache_efficiency,
2502            diagnostics: if is_valid {
2503                "Cache IDs are consistent".to_string()
2504            } else {
2505                format!(
2506                    "ERROR: Base cache ID ({}) > Current cache ID ({})",
2507                    base_id, current_id
2508                )
2509            },
2510        }
2511    }
2512
2513    /// Get cache usage statistics for monitoring
2514    fn get_cache_stats(&self) -> CacheStats {
2515        let validation = self.validate_cache_consistency();
2516        CacheStats {
2517            total_external_increments: validation.current_external_cache_id,
2518            total_loop_increments: validation.loop_cache_id,
2519            base_configurations: validation.base_external_cache_id + 1,
2520            rotational_variants: validation.current_external_cache_id
2521                - validation.base_external_cache_id,
2522            efficiency_ratio: validation.cache_efficiency,
2523        }
2524    }
2525
2526    fn get_group_masters(&self) -> impl Iterator<Item = &Self::G>;
2527
2528    fn get_terms_mut(&mut self) -> impl Iterator<Item = &mut Self::G>;
2529    fn graph_count(&self) -> usize;
2530    fn get_settings(&self) -> &RuntimeSettings;
2531    fn get_master_graph(&self, group_id: GroupId) -> &Self::G;
2532    fn get_graph(&self, graph_id: usize) -> &Self::G;
2533    fn get_graph_mut(&mut self, graph_id: usize) -> &mut Self::G;
2534    fn graph_group_id_for_graph(&self, graph_id: usize) -> Option<usize>;
2535    fn get_group(&self, group_id: GroupId) -> &GraphGroup;
2536    fn get_group_structure(&self) -> &TiVec<GroupId, GraphGroup>;
2537    fn get_dependent_momenta_constructor(&self) -> DependentMomentaConstructor<'_>;
2538    fn take_event_processing_runtime(&mut self) -> Option<EventProcessingRuntime> {
2539        None
2540    }
2541    fn restore_event_processing_runtime(&mut self, _runtime: Option<EventProcessingRuntime>) {}
2542    fn event_processing_runtime(&self) -> Option<&EventProcessingRuntime> {
2543        None
2544    }
2545    fn event_processing_runtime_mut(&mut self) -> Option<&mut EventProcessingRuntime> {
2546        None
2547    }
2548    fn groups_default_sample_events_by_graph_group(&self) -> bool {
2549        false
2550    }
2551
2552    // fn get_builder_cache(&self) -> &ParamBuilder<f64>;
2553}
2554
2555fn get_global_dimension_if_exists<I: ProcessIntegrandImpl>(integrand: &I) -> Option<usize> {
2556    if integrand
2557        .get_settings()
2558        .sampling
2559        .get_parameterization_settings()
2560        .is_none()
2561    {
2562        None
2563    } else {
2564        Some(
2565            integrand
2566                .get_master_graph(GroupId(0))
2567                .get_graph()
2568                .get_loop_number()
2569                * 3,
2570        )
2571    }
2572}
2573
2574pub trait GraphTerm {
2575    fn evaluate<T: FloatLike>(
2576        &mut self,
2577        sample: &MomentumSample<T>,
2578        context: GraphTermEvaluationContext<'_, '_, T>,
2579    ) -> Result<GraphEvaluationResult<T>>;
2580
2581    fn name(&self) -> String;
2582    fn orientation_label(&self, orientation_id: usize) -> Option<String>;
2583    fn lmb_channel_label(
2584        &self,
2585        channel_id: ChannelIndex,
2586        parameterization_settings: &ParameterizationSettings,
2587    ) -> Result<Option<String>>;
2588
2589    fn warm_up(&mut self, settings: &RuntimeSettings, model: &Model) -> Result<()>;
2590    fn get_graph(&self) -> &Graph;
2591    fn get_num_channels(&self, parameterization_settings: &ParameterizationSettings) -> usize;
2592    fn get_num_orientations(&self) -> usize;
2593    fn selected_lmb_basis_id(
2594        &self,
2595        parameterization_settings: &ParameterizationSettings,
2596    ) -> Result<LmbIndex>;
2597    fn get_tropical_sampler(&self) -> &SampleGenerator<3>;
2598    fn get_mut_param_builder(&mut self) -> &mut ParamBuilder<f64>;
2599    fn get_real_mass_vector(&self) -> EdgeVec<Option<F<f64>>>;
2600}
2601
2602struct EvaluationContext<'a, 'm> {
2603    model: &'a Model,
2604    settings: &'a RuntimeSettings,
2605    rotation: &'a Rotation,
2606    evaluation_metadata: &'m mut EvaluationMetaData,
2607    record_primary_timing: bool,
2608}
2609
2610pub struct GraphTermEvaluationContext<'a, 'm, T: FloatLike> {
2611    pub model: &'a Model,
2612    pub settings: &'a RuntimeSettings,
2613    pub event_processing_runtime: Option<&'m mut EventProcessingRuntime>,
2614    pub rotation: &'a Rotation,
2615    pub evaluation_metadata: &'m mut EvaluationMetaData,
2616    pub record_primary_timing: bool,
2617    pub channel_id: Option<(ChannelIndex, F<T>, LmbChannelWeight)>,
2618    pub lmb_basis_id: Option<LmbIndex>,
2619}
2620
2621fn evaluate_graph_term<T: FloatLike, I: ProcessIntegrandImpl>(
2622    integrand: &mut I,
2623    graph_id: usize,
2624    sample: &MomentumSample<T>,
2625    context: &mut EvaluationContext<'_, '_>,
2626    channel_id: Option<(ChannelIndex, F<T>, LmbChannelWeight)>,
2627    lmb_basis_id: Option<LmbIndex>,
2628) -> Result<GraphEvaluationResult<T>> {
2629    let mut event_processing_runtime = integrand.take_event_processing_runtime();
2630    let result = {
2631        let graph_context = GraphTermEvaluationContext {
2632            model: context.model,
2633            settings: context.settings,
2634            event_processing_runtime: event_processing_runtime.as_mut(),
2635            rotation: context.rotation,
2636            evaluation_metadata: context.evaluation_metadata,
2637            record_primary_timing: context.record_primary_timing,
2638            channel_id,
2639            lmb_basis_id,
2640        };
2641        integrand
2642            .get_graph_mut(graph_id)
2643            .evaluate(sample, graph_context)
2644    };
2645    integrand.restore_event_processing_runtime(event_processing_runtime);
2646    let mut result = result?;
2647    let graph_group_id = integrand.graph_group_id_for_graph(graph_id);
2648    for event_group in result.event_groups.iter_mut() {
2649        for event in event_group.iter_mut() {
2650            event.cut_info.graph_id = graph_id;
2651            event.cut_info.graph_group_id = graph_group_id;
2652        }
2653    }
2654    Ok(result)
2655}
2656
2657fn selected_lmb_basis_for_default_sampling<I: ProcessIntegrandImpl>(
2658    integrand: &I,
2659    graph_id: usize,
2660    use_lmb_basis: bool,
2661) -> Result<Option<LmbIndex>> {
2662    if !use_lmb_basis {
2663        return Ok(None);
2664    }
2665
2666    let parameterization_settings = integrand
2667        .get_settings()
2668        .sampling
2669        .get_parameterization_settings()
2670        .expect("Default LMB-basis sampling requires a parameterization.");
2671    let group_id = integrand
2672        .graph_group_id_for_graph(graph_id)
2673        .map(GroupId)
2674        .ok_or_else(|| {
2675            eyre!(
2676                "Could not determine graph group for graph {} while selecting an LMB basis.",
2677                graph_id
2678            )
2679        })?;
2680
2681    Ok(Some(
2682        integrand
2683            .get_master_graph(group_id)
2684            .selected_lmb_basis_id(&parameterization_settings)?,
2685    ))
2686}
2687
2688fn evaluate_graph_group<T: FloatLike, I: ProcessIntegrandImpl>(
2689    integrand: &mut I,
2690    group_id: GroupId,
2691    sample: &DiscreteGraphSample<T>,
2692    context: &mut EvaluationContext<'_, '_>,
2693    zero: &F<T>,
2694) -> Result<GraphEvaluationResult<T>> {
2695    let group = integrand.get_group(group_id).into_iter().collect_vec();
2696
2697    let mut result = GraphEvaluationResult::zero(zero.clone());
2698    let mut grouped_events = crate::observables::GenericEventGroup::default();
2699
2700    for graph_id in group {
2701        let graph_term_result = match sample {
2702            DiscreteGraphSample::Default {
2703                sample,
2704                use_lmb_basis,
2705            } => {
2706                let lmb_basis_id =
2707                    selected_lmb_basis_for_default_sampling(integrand, graph_id, *use_lmb_basis)?;
2708                evaluate_graph_term(integrand, graph_id, sample, context, None, lmb_basis_id)
2709            }
2710            DiscreteGraphSample::DiscreteMultiChanneling {
2711                alpha,
2712                channel_weight,
2713                channel_id,
2714                sample,
2715            } => evaluate_graph_term(
2716                integrand,
2717                graph_id,
2718                sample,
2719                context,
2720                Some((*channel_id, alpha.clone(), *channel_weight)),
2721                None,
2722            ),
2723            DiscreteGraphSample::MultiChanneling {
2724                alpha,
2725                channel_weight,
2726                sample,
2727            } => {
2728                let parameterization_settings = context
2729                    .settings
2730                    .sampling
2731                    .get_parameterization_settings()
2732                    .expect("LMB multichanneling requires a parameterization.");
2733                let num_channels = integrand
2734                    .get_master_graph(group_id)
2735                    .get_num_channels(&parameterization_settings);
2736                (0..num_channels)
2737                    .map(ChannelIndex::from)
2738                    .map(|channel_index| {
2739                        evaluate_graph_term(
2740                            integrand,
2741                            graph_id,
2742                            sample,
2743                            context,
2744                            Some((channel_index, alpha.clone(), *channel_weight)),
2745                            None,
2746                        )
2747                    })
2748                    .try_fold(
2749                        GraphEvaluationResult::zero(zero.clone()),
2750                        |mut sum, term| {
2751                            sum.merge_in_place(term?);
2752                            Ok::<GraphEvaluationResult<T>, eyre::Report>(sum)
2753                        },
2754                    )
2755            }
2756            DiscreteGraphSample::Tropical(sample) => {
2757                let master_graph = integrand.get_master_graph(group_id).get_graph();
2758
2759                let energy_cache = master_graph.get_energy_cache(
2760                    context.model,
2761                    sample.loop_moms(),
2762                    sample.external_moms(),
2763                    &master_graph.loop_momentum_basis,
2764                );
2765
2766                let prefactor = master_graph
2767                    .iter_loop_edges()
2768                    .map(|(_, edge_index, _)| edge_index)
2769                    .zip(
2770                        integrand
2771                            .get_master_graph(group_id)
2772                            .get_tropical_sampler()
2773                            .iter_edge_weights(),
2774                    )
2775                    .fold(sample.one(), |product, (edge_id, weight)| {
2776                        let energy = &energy_cache[edge_id];
2777                        product * energy.powf(&F::from_f64(2. * weight))
2778                    });
2779
2780                let mut graph_result =
2781                    evaluate_graph_term(integrand, graph_id, sample, context, None, None)?;
2782                graph_result.integrand_result *= Complex::new_re(prefactor);
2783                Ok(graph_result)
2784            }
2785        }?;
2786
2787        let mut graph_term_result = graph_term_result;
2788        for mut event_group in graph_term_result.event_groups.drain(..) {
2789            grouped_events.append(&mut event_group);
2790        }
2791        result.merge_in_place(graph_term_result);
2792    }
2793
2794    if !grouped_events.is_empty() {
2795        result.event_groups.push(grouped_events);
2796    }
2797
2798    Ok(result)
2799}
2800
2801fn evaluate_all_rotations<T: FloatLike, I: ProcessIntegrandImpl>(
2802    integrand: &mut I,
2803    model: &Model,
2804    gammaloop_sample: &GammaLoopSample<T>,
2805    evaluation_metadata: &mut EvaluationMetaData,
2806    is_primary_stability_level: bool,
2807    record_rotated_results: bool,
2808) -> Result<(Vec<GraphEvaluationResult<T>>, usize, Vec<RotatedEvaluation>)> {
2809    let rotations = integrand.get_rotations().cloned().collect_vec();
2810
2811    let cache = integrand.get_settings().general.enable_cache;
2812
2813    let mut loop_mom_cache_id = integrand.loop_cache_id();
2814    let mut external_mom_cache_id = integrand.external_cache_id();
2815
2816    // rotate the momenta for the stability tests.
2817    let gammaloop_samples: Vec<_> = rotations
2818        .iter()
2819        .map(|rotation| {
2820            if rotation.is_identity() {
2821                return gammaloop_sample.clone();
2822            }
2823            if cache {
2824                loop_mom_cache_id += 1;
2825                external_mom_cache_id += 1;
2826            }
2827            gammaloop_sample.rotate(rotation, loop_mom_cache_id, external_mom_cache_id)
2828        })
2829        .collect();
2830
2831    let primary_rotation_index = rotations
2832        .iter()
2833        .position(Rotation::is_identity)
2834        .unwrap_or(0);
2835    let mut original_call_timed = false;
2836    let mut evaluation_results: Vec<GraphEvaluationResult<T>> =
2837        Vec::with_capacity(gammaloop_samples.len());
2838    for (rotation_index, (gammaloop_sample, rotation)) in
2839        gammaloop_samples.iter().zip(rotations.iter()).enumerate()
2840    {
2841        debug!("Evaluating rotation: {}", rotation.method);
2842        let record_primary_timing = is_primary_stability_level
2843            && !original_call_timed
2844            && rotation_index == primary_rotation_index;
2845
2846        let result = evaluate_single(
2847            integrand,
2848            model,
2849            gammaloop_sample,
2850            rotation,
2851            evaluation_metadata,
2852            record_primary_timing,
2853        )?;
2854
2855        if record_primary_timing {
2856            original_call_timed = true;
2857        }
2858        evaluation_results.push(result);
2859    }
2860
2861    for result in &evaluation_results {
2862        evaluation_metadata.event_processing_time += result.event_processing_time;
2863    }
2864
2865    if cache {
2866        integrand.increment_loop_cache_id(rotations.len());
2867        integrand.increment_external_cache_id(rotations.len());
2868        // After evaluating all rotations, revert to base cache ID to enable cache reuse
2869        // for subsequent sample points with the same base external momenta
2870        integrand.revert_to_base_external_cache_id();
2871    }
2872
2873    let rotated_results = if record_rotated_results {
2874        rotations
2875            .iter()
2876            .zip(evaluation_results.iter())
2877            .map(|(rotation, result)| RotatedEvaluation {
2878                rotation: rotation.method.to_string(),
2879                result: complex_to_f64(&result.integrand_result),
2880            })
2881            .collect()
2882    } else {
2883        Vec::new()
2884    };
2885
2886    Ok((evaluation_results, primary_rotation_index, rotated_results))
2887}
2888
2889struct StabilityEvaluationContext<'a, 'm> {
2890    model: &'a Model,
2891    source: &'a EvaluationSource<'a>,
2892    stability_level: &'a StabilityLevelSetting,
2893    max_eval: &'a Complex<F<f64>>,
2894    wgt: F<f64>,
2895    check_on_norm: bool,
2896    is_final_level: bool,
2897    is_primary_stability_level: bool,
2898    evaluation_metadata: &'m mut EvaluationMetaData,
2899    record_rotated_results: bool,
2900    precision_label: &'static str,
2901    escalate_if_exact_zero: bool,
2902}
2903
2904fn evaluate_stability_level_precise<T: FloatLike, I: ProcessIntegrandImpl>(
2905    integrand: &mut I,
2906    context: &mut StabilityEvaluationContext<'_, '_>,
2907) -> Result<PreciseStabilityLevelResult<T>> {
2908    let level_start = Instant::now();
2909    let (gammaloop_sample, parameterization_time) =
2910        context.source.build_gamma_sample::<T, I>(integrand)?;
2911    debug!("{} parameterization succeeded", context.precision_label);
2912    debug!(
2913        "jacobian: {:+16e}",
2914        gammaloop_sample.get_default_sample().jacobian()
2915    );
2916
2917    let integrand_time_before = context.evaluation_metadata.integrand_evaluation_time;
2918    let evaluator_time_before = context.evaluation_metadata.evaluator_evaluation_time;
2919    let (graph_results, primary_rotation_index, rotated_results) = evaluate_all_rotations(
2920        integrand,
2921        context.model,
2922        &gammaloop_sample,
2923        context.evaluation_metadata,
2924        context.is_primary_stability_level,
2925        context.record_rotated_results,
2926    )?;
2927    let threshold_counterterm_failed = context
2928        .evaluation_metadata
2929        .threshold_counterterm_error
2930        .is_some();
2931    if context.is_final_level
2932        && let Some(threshold_error) = &context.evaluation_metadata.threshold_counterterm_error
2933    {
2934        return Err(eyre!(
2935            "threshold-counterterm evaluation remained invalid after the final {} stability level: {}",
2936            context.stability_level.precision,
2937            threshold_error,
2938        ));
2939    }
2940    let results = graph_results
2941        .iter()
2942        .map(|result| result.integrand_result.clone())
2943        .collect_vec();
2944
2945    let max_eval = complex_from_f64::<T>(context.max_eval);
2946    let wgt = F::<T>::from_ff64(context.wgt);
2947
2948    let (average_result, estimated_relative_accuracy, is_stable, instability_reason) =
2949        if context.check_on_norm {
2950            stability_check_on_norm(
2951                integrand.get_settings(),
2952                &results,
2953                context.stability_level,
2954                max_eval,
2955                wgt,
2956                context.is_final_level,
2957                context.escalate_if_exact_zero,
2958            )
2959        } else {
2960            stability_check(
2961                integrand.get_settings(),
2962                &results,
2963                context.stability_level,
2964                max_eval,
2965                wgt,
2966                context.is_final_level,
2967                context.escalate_if_exact_zero,
2968            )
2969        };
2970
2971    let mut graph_result = graph_results[primary_rotation_index].clone();
2972    graph_result.integrand_result = average_result.clone();
2973
2974    Ok(PreciseStabilityLevelResult {
2975        result: average_result,
2976        graph_result,
2977        stability_level_used: context.stability_level.precision,
2978        estimated_relative_accuracy,
2979        sample_count: results.len(),
2980        total_time: level_start.elapsed(),
2981        parameterization_time,
2982        parameterization_jacobian: match context.source {
2983            EvaluationSource::XSpace(_) => Some(gammaloop_sample.get_default_sample().jacobian()),
2984            EvaluationSource::Momentum(_) => None,
2985        },
2986        integrand_evaluation_time: context
2987            .evaluation_metadata
2988            .integrand_evaluation_time
2989            .saturating_sub(integrand_time_before),
2990        evaluator_evaluation_time: context
2991            .evaluation_metadata
2992            .evaluator_evaluation_time
2993            .saturating_sub(evaluator_time_before),
2994        is_stable: is_stable && !threshold_counterterm_failed,
2995        instability_reason,
2996        rotated_results,
2997    })
2998}
2999
3000fn evaluate_stability_level<T: FloatLike, I: ProcessIntegrandImpl>(
3001    integrand: &mut I,
3002    context: &mut StabilityEvaluationContext<'_, '_>,
3003) -> Result<StabilityLevelResult> {
3004    evaluate_stability_level_precise::<T, I>(integrand, context)
3005        .map(PreciseStabilityLevelResult::into_f64)
3006}
3007
3008/// Result of cache validation checks
3009#[derive(Debug, Clone)]
3010pub struct CacheValidationResult {
3011    pub is_valid: bool,
3012    pub current_external_cache_id: usize,
3013    pub base_external_cache_id: usize,
3014    pub loop_cache_id: usize,
3015    pub has_rotations: bool,
3016    pub cache_efficiency: f64,
3017    pub diagnostics: String,
3018}
3019
3020impl std::fmt::Display for CacheValidationResult {
3021    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3022        write!(
3023            f,
3024            "Cache Validation: {} | Current: {} | Base: {} | Loop: {} | Rotations: {} | Efficiency: {:.1}%",
3025            if self.is_valid { "✓" } else { "✗" },
3026            self.current_external_cache_id,
3027            self.base_external_cache_id,
3028            self.loop_cache_id,
3029            if self.has_rotations { "Yes" } else { "No" },
3030            self.cache_efficiency * 100.0
3031        )
3032    }
3033}
3034
3035/// Cache usage statistics
3036#[derive(Debug, Clone)]
3037pub struct CacheStats {
3038    pub total_external_increments: usize,
3039    pub total_loop_increments: usize,
3040    pub base_configurations: usize,
3041    pub rotational_variants: usize,
3042    pub efficiency_ratio: f64,
3043}
3044
3045impl std::fmt::Display for CacheStats {
3046    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3047        write!(
3048            f,
3049            "Cache Stats: {} base configs, {} rotations, {} loop increments, {:.1}% efficiency",
3050            self.base_configurations,
3051            self.rotational_variants,
3052            self.total_loop_increments,
3053            self.efficiency_ratio * 100.0
3054        )
3055    }
3056}
3057
3058/// Helper macro for cache debugging
3059#[macro_export]
3060macro_rules! debug_cache {
3061    ($integrand:expr, $msg:expr) => {
3062        let validation = $integrand.validate_cache_consistency();
3063        tracing::debug!("{}: {}", $msg, validation);
3064    };
3065}
3066
3067/// Helper macro for cache monitoring with custom conditions
3068#[macro_export]
3069macro_rules! monitor_cache {
3070    ($integrand:expr, $condition:expr, $msg:expr) => {
3071        if $condition {
3072            let stats = $integrand.get_cache_stats();
3073            tracing::info!("{}: {}", $msg, stats);
3074        }
3075    };
3076}
3077
3078/// Helper macro for missed cache hit detection in debug mode
3079#[macro_export]
3080macro_rules! debug_cache_search {
3081    ($integrand:expr, $msg:expr) => {
3082        if std::env::var("GAMMALOOP_DEBUG_CACHE").is_ok() {
3083            let validation = $integrand.validate_cache_consistency();
3084            if !validation.is_valid {
3085                tracing::error!(
3086                    "CACHE CORRUPTION DETECTED at {}: {}",
3087                    $msg,
3088                    validation.diagnostics
3089                );
3090            } else {
3091                tracing::debug!("Cache search at {}: {}", $msg, validation);
3092            }
3093        }
3094    };
3095}
3096
3097/// Helper macro for cache efficiency warnings
3098#[macro_export]
3099macro_rules! warn_cache_efficiency {
3100    ($integrand:expr, $threshold:expr, $msg:expr) => {
3101        let stats = $integrand.get_cache_stats();
3102        if stats.efficiency_ratio < $threshold {
3103            tracing::warn!(
3104                "⚠️ Low cache efficiency at {}: {:.1}% (threshold: {:.1}%)",
3105                $msg,
3106                stats.efficiency_ratio * 100.0,
3107                $threshold * 100.0
3108            );
3109            tracing::warn!("   Cache stats: {}", stats);
3110        }
3111    };
3112}
3113
3114fn evaluate_single<T: FloatLike, I: ProcessIntegrandImpl>(
3115    integrand: &mut I,
3116    model: &Model,
3117    gammaloop_sample: &GammaLoopSample<T>,
3118    rotation: &Rotation,
3119    evaluation_metadata: &mut EvaluationMetaData,
3120    record_primary_timing: bool,
3121) -> Result<GraphEvaluationResult<T>> {
3122    let settings = integrand.get_settings().clone();
3123    let zero = gammaloop_sample.get_default_sample().zero();
3124    let loop_cache_shift = 0;
3125    let cache = integrand.get_settings().general.enable_cache;
3126
3127    let start_integrand_timing = if record_primary_timing {
3128        Some(std::time::Instant::now())
3129    } else {
3130        None
3131    };
3132    let mut context = EvaluationContext {
3133        model,
3134        settings: &settings,
3135        rotation,
3136        evaluation_metadata,
3137        record_primary_timing,
3138    };
3139    let result = (|| -> Result<GraphEvaluationResult<T>> {
3140        let result = match &gammaloop_sample {
3141            GammaLoopSample::Default {
3142                sample,
3143                use_lmb_basis,
3144            } => {
3145                if integrand.groups_default_sample_events_by_graph_group() {
3146                    integrand
3147                        .get_group_structure()
3148                        .iter_enumerated()
3149                        .map(|(group_id, _)| group_id)
3150                        .collect_vec()
3151                        .into_iter()
3152                        .try_fold(
3153                            GraphEvaluationResult::zero(zero.clone()),
3154                            |mut sum, group_id| {
3155                                let group_result = evaluate_graph_group(
3156                                    integrand,
3157                                    group_id,
3158                                    &DiscreteGraphSample::Default {
3159                                        sample: sample.clone(),
3160                                        use_lmb_basis: *use_lmb_basis,
3161                                    },
3162                                    &mut context,
3163                                    &zero,
3164                                )?;
3165                                sum.merge_in_place(group_result);
3166                                Ok::<GraphEvaluationResult<T>, eyre::Report>(sum)
3167                            },
3168                        )?
3169                } else {
3170                    (0..integrand.graph_count()).try_fold(
3171                        GraphEvaluationResult::zero(zero.clone()),
3172                        |mut sum, graph_id| {
3173                            let lmb_basis_id = selected_lmb_basis_for_default_sampling(
3174                                integrand,
3175                                graph_id,
3176                                *use_lmb_basis,
3177                            )?;
3178                            let graph_result = evaluate_graph_term(
3179                                integrand,
3180                                graph_id,
3181                                sample,
3182                                &mut context,
3183                                None,
3184                                lmb_basis_id,
3185                            )?;
3186                            sum.merge_in_place(graph_result);
3187                            Ok::<GraphEvaluationResult<T>, eyre::Report>(sum)
3188                        },
3189                    )?
3190                }
3191            }
3192            GammaLoopSample::Graph { graph_id, sample } => {
3193                evaluate_graph_term(integrand, *graph_id, sample, &mut context, None, None)?
3194            }
3195            GammaLoopSample::MultiChanneling {
3196                alpha,
3197                channel_weight,
3198                sample,
3199            } => (0..integrand.graph_count()).try_fold(
3200                GraphEvaluationResult::zero(zero.clone()),
3201                |mut sum, graph_id| {
3202                    let parameterization_settings = context
3203                        .settings
3204                        .sampling
3205                        .get_parameterization_settings()
3206                        .expect("LMB multichanneling requires a parameterization.");
3207                    let num_channels = integrand
3208                        .get_graph(graph_id)
3209                        .get_num_channels(&parameterization_settings);
3210                    let graph_result = (0..num_channels).map(ChannelIndex::from).try_fold(
3211                        GraphEvaluationResult::zero(zero.clone()),
3212                        |mut channel_sum, channel_index| {
3213                            let channel_result = evaluate_graph_term(
3214                                integrand,
3215                                graph_id,
3216                                sample,
3217                                &mut context,
3218                                Some((channel_index, alpha.clone(), *channel_weight)),
3219                                None,
3220                            )?;
3221                            channel_sum.merge_in_place(channel_result);
3222                            Ok::<GraphEvaluationResult<T>, eyre::Report>(channel_sum)
3223                        },
3224                    )?;
3225                    sum.merge_in_place(graph_result);
3226                    Ok::<GraphEvaluationResult<T>, eyre::Report>(sum)
3227                },
3228            )?,
3229            GammaLoopSample::DiscreteGraph { group_id, sample } => {
3230                evaluate_graph_group(integrand, *group_id, sample, &mut context, &zero)?
3231            }
3232        };
3233
3234        if cache {
3235            integrand.increment_loop_cache_id(loop_cache_shift);
3236        }
3237
3238        Ok(result)
3239    })();
3240    if record_primary_timing {
3241        context.evaluation_metadata.integrand_evaluation_time = context
3242            .evaluation_metadata
3243            .integrand_evaluation_time
3244            .saturating_add(
3245                start_integrand_timing
3246                    .expect("integrand timing start should exist")
3247                    .elapsed(),
3248            );
3249    }
3250
3251    result
3252}
3253
3254fn create_grid_for_graph<G: GraphTerm>(
3255    graph_term: &G,
3256    settings: &DiscreteGraphSamplingSettings,
3257    integrator_settings: &IntegratorSettings,
3258) -> Grid<F<f64>> {
3259    match &settings.sampling_type {
3260        DiscreteGraphSamplingType::Default(_) | DiscreteGraphSamplingType::MultiChanneling(_) => {
3261            let continuous_grid = create_default_continous_grid(graph_term, integrator_settings);
3262
3263            if settings.sample_orientations {
3264                let continuous_grids = (0..graph_term.get_num_orientations())
3265                    .map(|_| Some(continuous_grid.clone()))
3266                    .collect();
3267
3268                Grid::Discrete(DiscreteGrid::new(
3269                    continuous_grids,
3270                    F(integrator_settings.max_prob_ratio),
3271                    integrator_settings.train_on_avg,
3272                ))
3273            } else {
3274                continuous_grid
3275            }
3276        }
3277        DiscreteGraphSamplingType::DiscreteMultiChanneling(multichanneling_settings) => {
3278            let continuous_grid = create_default_continous_grid(graph_term, integrator_settings);
3279            let lmb_channel_grid = Grid::Discrete(DiscreteGrid::new(
3280                (0..graph_term
3281                    .get_num_channels(&multichanneling_settings.parameterization_settings))
3282                    .map(|_| Some(continuous_grid.clone()))
3283                    .collect_vec(),
3284                F(integrator_settings.max_prob_ratio),
3285                integrator_settings.train_on_avg,
3286            ));
3287
3288            if settings.sample_orientations {
3289                Grid::Discrete(DiscreteGrid::new(
3290                    (0..graph_term.get_num_orientations())
3291                        .map(|_| Some(lmb_channel_grid.clone()))
3292                        .collect(),
3293                    F(integrator_settings.max_prob_ratio),
3294                    integrator_settings.train_on_avg,
3295                ))
3296            } else {
3297                lmb_channel_grid
3298            }
3299        }
3300
3301        DiscreteGraphSamplingType::TropicalSampling(_) => {
3302            let dimension = get_n_dim_for_n_loop_momenta(
3303                &SamplingSettings::DiscreteGraphs(settings.clone()),
3304                graph_term.get_graph().get_loop_number(),
3305                Some(graph_term.get_graph().iter_loop_edges().count()),
3306            );
3307
3308            let continious_grid = Grid::Continuous(ContinuousGrid::new(
3309                dimension,
3310                integrator_settings.n_bins,
3311                integrator_settings.min_samples_for_update,
3312                integrator_settings.bin_number_evolution.clone(),
3313                integrator_settings.train_on_avg,
3314            ));
3315
3316            if settings.sample_orientations {
3317                let continuous_grids = (0..graph_term.get_num_orientations())
3318                    .map(|_| Some(continious_grid.clone()))
3319                    .collect();
3320
3321                Grid::Discrete(DiscreteGrid::new(
3322                    continuous_grids,
3323                    F(integrator_settings.max_prob_ratio),
3324                    integrator_settings.train_on_avg,
3325                ))
3326            } else {
3327                continious_grid
3328            }
3329        }
3330    }
3331}
3332
3333fn create_default_continous_grid<G: GraphTerm>(
3334    graph_term: &G,
3335    integrator_settings: &IntegratorSettings,
3336) -> Grid<F<f64>> {
3337    Grid::Continuous(ContinuousGrid::new(
3338        graph_term.get_graph().get_loop_number() * 3,
3339        integrator_settings.n_bins,
3340        integrator_settings.min_samples_for_update,
3341        integrator_settings.bin_number_evolution.clone(),
3342        integrator_settings.train_on_avg,
3343    ))
3344}
3345
3346fn create_grid<I: ProcessIntegrandImpl>(integrand: &I) -> Grid<F<f64>> {
3347    let settings = integrand.get_settings();
3348    match &settings.sampling {
3349        SamplingSettings::Default(_) => Grid::Continuous(ContinuousGrid::new(
3350            get_global_dimension_if_exists(integrand).unwrap(),
3351            settings.integrator.n_bins,
3352            settings.integrator.min_samples_for_update,
3353            settings.integrator.bin_number_evolution.clone(),
3354            settings.integrator.train_on_avg,
3355        )),
3356        SamplingSettings::MultiChanneling(_) => Grid::Continuous(ContinuousGrid::new(
3357            get_global_dimension_if_exists(integrand).unwrap(),
3358            settings.integrator.n_bins,
3359            settings.integrator.min_samples_for_update,
3360            settings.integrator.bin_number_evolution.clone(),
3361            settings.integrator.train_on_avg,
3362        )),
3363        SamplingSettings::DiscreteGraphs(discrete_graph_sampling_settings) => {
3364            Grid::Discrete(DiscreteGrid::new(
3365                integrand
3366                    .get_group_masters()
3367                    .map(|term| {
3368                        Some(create_grid_for_graph(
3369                            term,
3370                            discrete_graph_sampling_settings,
3371                            &settings.integrator,
3372                        ))
3373                    })
3374                    .collect(),
3375                F(settings.integrator.max_prob_ratio),
3376                settings.integrator.train_on_avg,
3377            ))
3378        }
3379    }
3380}
3381
3382#[derive(Clone, Copy)]
3383enum EvaluationSource<'a> {
3384    XSpace(&'a Sample<F<f64>>),
3385    Momentum(&'a MomentumSpaceEvaluationInput),
3386}
3387
3388impl<'a> EvaluationSource<'a> {
3389    fn build_gamma_sample<T: FloatLike, I: ProcessIntegrandImpl>(
3390        &self,
3391        integrand: &mut I,
3392    ) -> Result<(GammaLoopSample<T>, Duration)> {
3393        match self {
3394            EvaluationSource::XSpace(sample) => {
3395                let before_parameterization = std::time::Instant::now();
3396                let sample = parameterize::<T, I>(sample, integrand)?;
3397                Ok((sample, before_parameterization.elapsed()))
3398            }
3399            EvaluationSource::Momentum(input) => Ok((
3400                build_direct_gamma_sample::<T, I>(integrand, input)?,
3401                Duration::ZERO,
3402            )),
3403        }
3404    }
3405
3406    fn loop_norm_sum<I: ProcessIntegrandImpl>(&self, integrand: &mut I) -> Result<F<f64>> {
3407        match self {
3408            EvaluationSource::XSpace(sample) => {
3409                let sample = parameterize::<f64, I>(sample, integrand)?;
3410                Ok(sum_loop_norms(
3411                    sample.get_default_sample().loop_moms().0.iter(),
3412                ))
3413            }
3414            EvaluationSource::Momentum(input) => Ok(sum_loop_norms(input.loop_momenta.iter())),
3415        }
3416    }
3417
3418    fn debug_sample<I: ProcessIntegrandImpl>(
3419        &self,
3420        integrand: &mut I,
3421    ) -> Result<GammaLoopSample<f64>> {
3422        self.build_gamma_sample::<f64, I>(integrand)
3423            .map(|(sample, _)| sample)
3424    }
3425}
3426
3427fn sum_loop_norms<'a>(loop_momenta: impl Iterator<Item = &'a ThreeMomentum<F<f64>>>) -> F<f64> {
3428    loop_momenta.fold(F(0.0), |acc, momentum| acc + momentum.norm())
3429}
3430
3431fn build_direct_gamma_sample<T: FloatLike, I: ProcessIntegrandImpl>(
3432    integrand: &mut I,
3433    input: &MomentumSpaceEvaluationInput,
3434) -> Result<GammaLoopSample<T>> {
3435    let expected_loop_count = if let Some(graph_id) = input.graph_id {
3436        let group_id = integrand
3437            .get_group_structure()
3438            .iter_enumerated()
3439            .find_map(|(group_id, group)| group.into_iter().contains(&graph_id).then_some(group_id))
3440            .ok_or_else(|| eyre!("Unknown graph '{}' in momentum-space evaluation.", graph_id))?;
3441        integrand
3442            .get_master_graph(group_id)
3443            .get_graph()
3444            .get_loop_number()
3445    } else if let Some(group_id) = input.group_id {
3446        if group_id.0 >= integrand.get_group_structure().len() {
3447            return Err(eyre!(
3448                "Unknown graph group '{}' in momentum-space evaluation.",
3449                group_id.0
3450            ));
3451        }
3452        integrand
3453            .get_master_graph(group_id)
3454            .get_graph()
3455            .get_loop_number()
3456    } else {
3457        integrand
3458            .get_group_masters()
3459            .next()
3460            .map(|graph| graph.get_graph().get_loop_number())
3461            .ok_or_else(|| eyre!("Cannot evaluate an integrand with no graph terms."))?
3462    };
3463
3464    if input.loop_momenta.len() != expected_loop_count {
3465        return Err(eyre!(
3466            "Expected {} loop momenta in momentum-space evaluation, got {}.",
3467            expected_loop_count,
3468            input.loop_momenta.len()
3469        ));
3470    }
3471
3472    let loop_momenta = input
3473        .loop_momenta
3474        .iter()
3475        .map(|momentum| {
3476            ThreeMomentum::new(
3477                F::<T>::from_ff64(momentum.px),
3478                F::<T>::from_ff64(momentum.py),
3479                F::<T>::from_ff64(momentum.pz),
3480            )
3481        })
3482        .collect::<LoopMomenta<F<T>>>();
3483    let sample = MomentumSample::new(
3484        loop_momenta,
3485        integrand.loop_cache_id(),
3486        &integrand.get_settings().kinematics.externals,
3487        integrand.get_current_external_cache_id(),
3488        F::<T>::from_f64(1.0),
3489        integrand.get_dependent_momenta_constructor(),
3490        input.orientation,
3491    )?;
3492
3493    if let Some(graph_id) = input.graph_id {
3494        if input.group_id.is_some() || input.channel_id.is_some() {
3495            return Err(eyre!(
3496                "Explicit graph selection is mutually exclusive with discrete graph/channel selections in momentum-space evaluation."
3497            ));
3498        }
3499        return Ok(GammaLoopSample::Graph { graph_id, sample });
3500    }
3501
3502    match &integrand.get_settings().sampling {
3503        SamplingSettings::Default(_) | SamplingSettings::MultiChanneling(_) => {
3504            if input.group_id.is_some() || input.channel_id.is_some() {
3505                return Err(eyre!(
3506                    "Discrete graph/channel selections are not supported for this sampling mode."
3507                ));
3508            }
3509
3510            Ok(GammaLoopSample::Default {
3511                sample,
3512                use_lmb_basis: false,
3513            })
3514        }
3515        SamplingSettings::DiscreteGraphs(settings) => {
3516            let Some(group_id) = input.group_id else {
3517                if input.orientation.is_some() || input.channel_id.is_some() {
3518                    return Err(eyre!(
3519                        "Explicit orientation or channel selections require selecting a graph group in momentum-space evaluation."
3520                    ));
3521                }
3522                return Ok(GammaLoopSample::Default {
3523                    sample,
3524                    use_lmb_basis: false,
3525                });
3526            };
3527            let discrete_sample = match &settings.sampling_type {
3528                DiscreteGraphSamplingType::Default(_) => {
3529                    if input.channel_id.is_some() {
3530                        return Err(eyre!(
3531                            "Channel selection is not available for this discrete-graph sampling mode."
3532                        ));
3533                    }
3534                    DiscreteGraphSample::Default {
3535                        sample,
3536                        use_lmb_basis: false,
3537                    }
3538                }
3539                DiscreteGraphSamplingType::MultiChanneling(multichanneling_settings) => {
3540                    if input.channel_id.is_some() {
3541                        return Err(eyre!(
3542                            "Channel selection is not available for this discrete-graph sampling mode."
3543                        ));
3544                    }
3545                    DiscreteGraphSample::MultiChanneling {
3546                        alpha: F::from_f64(multichanneling_settings.alpha),
3547                        channel_weight: multichanneling_settings.channel_weight,
3548                        sample,
3549                    }
3550                }
3551                DiscreteGraphSamplingType::TropicalSampling(_) => {
3552                    if input.channel_id.is_some() {
3553                        return Err(eyre!(
3554                            "Channel selection is not available for tropical discrete-graph sampling."
3555                        ));
3556                    }
3557                    DiscreteGraphSample::Tropical(sample)
3558                }
3559                DiscreteGraphSamplingType::DiscreteMultiChanneling(multichanneling_settings) => {
3560                    let channel_id = input.channel_id.ok_or_else(|| {
3561                        eyre!(
3562                            "Momentum-space evaluation for discrete multichanneling requires selecting a channel."
3563                        )
3564                    })?;
3565                    DiscreteGraphSample::DiscreteMultiChanneling {
3566                        alpha: F::from_f64(multichanneling_settings.alpha),
3567                        channel_weight: multichanneling_settings.channel_weight,
3568                        channel_id,
3569                        sample,
3570                    }
3571                }
3572            };
3573
3574            Ok(GammaLoopSample::DiscreteGraph {
3575                group_id,
3576                sample: discrete_sample,
3577            })
3578        }
3579    }
3580}
3581
3582fn log_rotated_samples<I: ProcessIntegrandImpl>(
3583    integrand: &mut I,
3584    gammaloop_sample: &GammaLoopSample<f64>,
3585    level_result: &StabilityLevelResult,
3586) {
3587    let mut loop_mom_cache_id = integrand.loop_cache_id();
3588    let mut external_mom_cache_id = integrand.external_cache_id();
3589    let mut shift = 0;
3590
3591    let rotated_samples: Vec<_> = integrand
3592        .get_rotations()
3593        .map(|rotation| {
3594            if rotation.is_identity() {
3595                return gammaloop_sample.clone();
3596            }
3597            loop_mom_cache_id += 1;
3598            shift += 1;
3599            external_mom_cache_id += 1;
3600            gammaloop_sample.rotate(rotation, loop_mom_cache_id, external_mom_cache_id)
3601        })
3602        .collect();
3603    integrand.increment_external_cache_id(shift);
3604    integrand.increment_loop_cache_id(shift);
3605
3606    for (sample, result) in rotated_samples
3607        .iter()
3608        .zip(level_result.rotated_results.iter())
3609    {
3610        let default_sample = sample.get_default_sample();
3611        debug!(
3612            "loop_moms: {}, external_moms: {}",
3613            format!("{}", default_sample.loop_moms()).blue(),
3614            format!("{:?}", default_sample.external_moms()).blue()
3615        );
3616
3617        debug!(
3618            "result of current level: {}",
3619            format!("{:16e}", result.result).blue()
3620        );
3621    }
3622}
3623
3624fn evaluate_from_source<I: ProcessIntegrandImpl>(
3625    integrand: &mut I,
3626    model: &Model,
3627    source: EvaluationSource<'_>,
3628    wgt: F<f64>,
3629    use_arb_prec: bool,
3630    max_eval: Complex<F<f64>>,
3631) -> Result<EvaluationResult> {
3632    let start_eval = std::time::Instant::now();
3633    let mut escalate_if_exact_zero = integrand.get_settings().stability.escalate_if_exact_zero;
3634    if escalate_if_exact_zero
3635        && integrand
3636            .get_settings()
3637            .selectors
3638            .values()
3639            .any(|selector| selector.active)
3640    {
3641        warn_selectors_disable_zero_once();
3642        escalate_if_exact_zero = false;
3643    }
3644    let mut evaluation_metadata = EvaluationMetaData::new_empty();
3645    let (stability_iterator, loop_momenta_escalation) =
3646        stability_iterator_for_source(integrand, &source, use_arb_prec);
3647
3648    let mut results_of_stability_levels = Vec::with_capacity(stability_iterator.len());
3649
3650    let total_levels = stability_iterator.len();
3651    for (level_index, stability_level) in stability_iterator.into_iter().enumerate() {
3652        evaluation_metadata.clear_threshold_counterterm_error();
3653        let is_final_level = level_index + 1 == total_levels;
3654        let record_rotated_results = integrand
3655            .get_settings()
3656            .stability
3657            .recording
3658            .map(|recording| recording.record_rotated_results)
3659            .unwrap_or(false);
3660        let is_primary_stability_level = level_index == 0;
3661        let mut context = StabilityEvaluationContext {
3662            model,
3663            source: &source,
3664            stability_level: &stability_level,
3665            max_eval: &max_eval,
3666            wgt,
3667            check_on_norm: integrand.get_settings().stability.check_on_norm,
3668            is_final_level,
3669            is_primary_stability_level,
3670            evaluation_metadata: &mut evaluation_metadata,
3671            record_rotated_results,
3672            precision_label: match stability_level.precision {
3673                Precision::Double => "f64",
3674                Precision::Quad => "f128",
3675                Precision::Arb => "ArbPrec",
3676            },
3677            escalate_if_exact_zero,
3678        };
3679        let result_of_level = match stability_level.precision {
3680            Precision::Double => evaluate_stability_level::<f64, I>(integrand, &mut context),
3681            Precision::Quad => evaluate_stability_level::<f128, I>(integrand, &mut context),
3682            Precision::Arb => evaluate_stability_level::<ArbPrec, I>(integrand, &mut context),
3683        }?;
3684
3685        let is_stable = result_of_level.is_stable;
3686        results_of_stability_levels.push(result_of_level);
3687
3688        if is_stable {
3689            break;
3690        } else {
3691            debug!("unstable at level: {}", stability_level.precision);
3692            if let Ok(gammaloop_sample) = source.debug_sample(integrand) {
3693                let level_result = results_of_stability_levels
3694                    .last()
3695                    .expect("stability level result missing");
3696                log_rotated_samples(integrand, &gammaloop_sample, level_result);
3697            } else {
3698                debug!("failed to reconstruct sample for instability logging");
3699            }
3700        }
3701    }
3702
3703    debug!("result at each level:");
3704    for level_result in results_of_stability_levels.iter() {
3705        debug!(
3706            "level: {}. result: {}",
3707            format!("{}", level_result.stability_level_used).green(),
3708            format!("{:16e}", level_result.result).blue()
3709        );
3710    }
3711
3712    if let Some(stability_level_result) = results_of_stability_levels.last().cloned() {
3713        let re_is_nan = stability_level_result.result.re.is_nan()
3714            || stability_level_result.result.re.is_infinite();
3715        let im_is_nan = stability_level_result.result.im.is_nan()
3716            || stability_level_result.result.im.is_infinite();
3717        let is_nan = re_is_nan || im_is_nan;
3718        if is_nan {
3719            match source {
3720                EvaluationSource::XSpace(sample) => {
3721                    warn!(
3722                        stage = "process_final_nonfinite_sample",
3723                        ?sample,
3724                        result = %stability_level_result.result,
3725                        re_is_nan,
3726                        im_is_nan,
3727                        "final process evaluation is nonfinite"
3728                    );
3729                }
3730                EvaluationSource::Momentum(input) => {
3731                    warn!(
3732                        stage = "process_final_nonfinite_sample",
3733                        graph_id = ?input.graph_id,
3734                        group_id = ?input.group_id,
3735                        orientation = ?input.orientation,
3736                        channel_id = ?input.channel_id,
3737                        loop_momenta = ?input.loop_momenta,
3738                        result = %stability_level_result.result,
3739                        re_is_nan,
3740                        im_is_nan,
3741                        "final process evaluation is nonfinite"
3742                    );
3743                }
3744            }
3745        }
3746
3747        let stability_results = results_of_stability_levels
3748            .iter()
3749            .map(|level| StabilityResult {
3750                precision: level.stability_level_used,
3751                estimated_relative_accuracy: level.estimated_relative_accuracy,
3752                status: StabilityStatus::from_sample_count(level.sample_count, level.is_stable),
3753                total_time: level.total_time,
3754            })
3755            .collect();
3756
3757        evaluation_metadata.total_timing = start_eval.elapsed();
3758        evaluation_metadata.parameterization_time = stability_level_result.parameterization_time;
3759        evaluation_metadata.generated_event_count =
3760            stability_level_result.graph_result.generated_event_count;
3761        evaluation_metadata.accepted_event_count =
3762            stability_level_result.graph_result.accepted_event_count;
3763        evaluation_metadata.relative_instability_error = Complex::new_zero();
3764        evaluation_metadata.is_nan = is_nan;
3765        evaluation_metadata.loop_momenta_escalation = loop_momenta_escalation;
3766        evaluation_metadata.stability_results = stability_results;
3767
3768        let nanless_result = if re_is_nan && !im_is_nan {
3769            Complex::new(F(0.0), stability_level_result.result.im)
3770        } else if im_is_nan && !re_is_nan {
3771            Complex::new(stability_level_result.result.re, F(0.0))
3772        } else if im_is_nan && re_is_nan {
3773            Complex::new(F(0.0), F(0.0))
3774        } else {
3775            stability_level_result.result
3776        };
3777        let mut event_groups = stability_level_result.graph_result.event_groups;
3778        let parameterization_jacobian = stability_level_result.parameterization_jacobian;
3779        let full_factor = full_event_multiplicative_factor(parameterization_jacobian, wgt);
3780        apply_full_event_multiplicative_factor(&mut event_groups, &full_factor);
3781        Ok(EvaluationResult {
3782            integrand_result: nanless_result,
3783            parameterization_jacobian,
3784            integrator_weight: wgt,
3785            event_groups,
3786            evaluation_metadata,
3787        })
3788    } else {
3789        println!("Evaluation failed at all stability levels");
3790        Ok(EvaluationResult {
3791            integrand_result: Complex::new(F(0.0), F(0.0)),
3792            parameterization_jacobian: None,
3793            integrator_weight: wgt,
3794            event_groups: Default::default(),
3795            evaluation_metadata: EvaluationMetaData {
3796                total_timing: Duration::ZERO,
3797                integrand_evaluation_time: Duration::ZERO,
3798                evaluator_evaluation_time: Duration::ZERO,
3799                parameterization_time: Duration::ZERO,
3800                event_processing_time: Duration::ZERO,
3801                generated_event_count: 0,
3802                accepted_event_count: 0,
3803                relative_instability_error: Complex::new(F(0.0), F(0.0)),
3804                is_nan: true,
3805                loop_momenta_escalation: None,
3806                stability_results: Vec::new(),
3807                threshold_counterterm_error: None,
3808                radial_root_diagnostics: Default::default(),
3809            },
3810        })
3811    }
3812}
3813
3814fn stability_iterator_for_source<I: ProcessIntegrandImpl>(
3815    integrand: &mut I,
3816    source: &EvaluationSource<'_>,
3817    use_arb_prec: bool,
3818) -> (
3819    Vec<StabilityLevelSetting>,
3820    Option<LoopMomentaEscalationMetrics>,
3821) {
3822    let mut stability_iterator =
3823        create_stability_iterator(&integrand.get_settings().stability, use_arb_prec);
3824    let escalation_factor = integrand
3825        .get_settings()
3826        .stability
3827        .loop_momenta_norm_escalation_factor;
3828    let record_loop_momenta_escalation = integrand
3829        .get_settings()
3830        .stability
3831        .recording
3832        .map(|recording| recording.record_loop_momenta_escalation)
3833        .unwrap_or(false);
3834    let mut loop_momenta_escalation = None;
3835    if escalation_factor > 0.0
3836        && stability_iterator.len() > 1
3837        && let Ok(sum_norm) = source.loop_norm_sum(integrand)
3838    {
3839        let threshold =
3840            F::<f64>::from_f64(escalation_factor * integrand.get_settings().kinematics.e_cm);
3841        if record_loop_momenta_escalation {
3842            loop_momenta_escalation = Some(LoopMomentaEscalationMetrics {
3843                sum_norm: sum_norm.0,
3844                threshold: threshold.0,
3845            });
3846        }
3847        if sum_norm > threshold {
3848            let escalated_level_index = stability_iterator
3849                .iter()
3850                .position(|level| level.precision == Precision::Quad)
3851                .or_else(|| stability_iterator.len().checked_sub(1));
3852            if let Some(level_index) = escalated_level_index {
3853                stability_iterator = stability_iterator[level_index..].to_vec();
3854            }
3855        }
3856    }
3857
3858    (stability_iterator, loop_momenta_escalation)
3859}
3860
3861fn finalize_precise_evaluation_result<T: FloatLike>(
3862    result: PreciseStabilityLevelResult<T>,
3863    integrator_weight: F<f64>,
3864    evaluation_metadata: EvaluationMetaData,
3865) -> GenericEvaluationResult<T> {
3866    let re_is_nan = result.result.re.is_nan() || result.result.re.is_infinite();
3867    let im_is_nan = result.result.im.is_nan() || result.result.im.is_infinite();
3868    let nanless_result = if re_is_nan && !im_is_nan {
3869        Complex::new(result.result.re.zero(), result.result.im)
3870    } else if im_is_nan && !re_is_nan {
3871        Complex::new(result.result.re, result.result.im.zero())
3872    } else if re_is_nan && im_is_nan {
3873        Complex::new(result.result.re.zero(), result.result.im.zero())
3874    } else {
3875        result.result
3876    };
3877
3878    let mut event_groups = result.graph_result.event_groups;
3879    let integrator_weight = F::<T>::from_ff64(integrator_weight);
3880    let parameterization_jacobian = result.parameterization_jacobian;
3881    let full_factor = full_event_multiplicative_factor_precise(
3882        parameterization_jacobian.clone(),
3883        integrator_weight.clone(),
3884    );
3885    apply_full_event_multiplicative_factor_precise(&mut event_groups, &full_factor);
3886
3887    GenericEvaluationResult {
3888        integrand_result: nanless_result,
3889        parameterization_jacobian,
3890        integrator_weight,
3891        event_groups,
3892        evaluation_metadata,
3893    }
3894}
3895
3896fn evaluate_from_source_precise<I: ProcessIntegrandImpl>(
3897    integrand: &mut I,
3898    model: &Model,
3899    source: EvaluationSource<'_>,
3900    wgt: F<f64>,
3901    use_arb_prec: bool,
3902    max_eval: Complex<F<f64>>,
3903) -> Result<crate::integrands::evaluation::PreciseEvaluationResult> {
3904    let base_result = evaluate_from_source(integrand, model, source, wgt, use_arb_prec, max_eval)?;
3905    let final_precision = base_result
3906        .evaluation_metadata
3907        .final_precision()
3908        .unwrap_or(Precision::Double);
3909    let base_metadata = base_result.evaluation_metadata.clone();
3910    let mut escalate_if_exact_zero = integrand.get_settings().stability.escalate_if_exact_zero;
3911    if escalate_if_exact_zero
3912        && integrand
3913            .get_settings()
3914            .selectors
3915            .values()
3916            .any(|selector| selector.active)
3917    {
3918        escalate_if_exact_zero = false;
3919    }
3920
3921    match final_precision {
3922        Precision::Double => Ok(
3923            crate::integrands::evaluation::PreciseEvaluationResult::Double(
3924                GenericEvaluationResult {
3925                    integrand_result: base_result.integrand_result,
3926                    parameterization_jacobian: base_result.parameterization_jacobian,
3927                    integrator_weight: base_result.integrator_weight,
3928                    event_groups: base_result.event_groups,
3929                    evaluation_metadata: base_metadata,
3930                },
3931            ),
3932        ),
3933        Precision::Quad => {
3934            let (stability_iterator, _) =
3935                stability_iterator_for_source(integrand, &source, use_arb_prec);
3936            let stability_level = stability_iterator
3937                .into_iter()
3938                .rev()
3939                .find(|level| level.precision == Precision::Quad)
3940                .ok_or_else(|| {
3941                    eyre!(
3942                        "Quad precision was selected for the final result, but no quad stability level is configured."
3943                    )
3944            })?;
3945            let mut evaluation_metadata = EvaluationMetaData::new_empty();
3946            evaluation_metadata.radial_root_diagnostics =
3947                base_metadata.radial_root_diagnostics.clone();
3948            evaluation_metadata
3949                .radial_root_diagnostics
3950                .restart_precision_pass();
3951            let mut context = StabilityEvaluationContext {
3952                model,
3953                source: &source,
3954                stability_level: &stability_level,
3955                max_eval: &max_eval,
3956                wgt,
3957                check_on_norm: integrand.get_settings().stability.check_on_norm,
3958                is_final_level: true,
3959                is_primary_stability_level: true,
3960                evaluation_metadata: &mut evaluation_metadata,
3961                record_rotated_results: false,
3962                precision_label: "f128",
3963                escalate_if_exact_zero,
3964            };
3965            let result = evaluate_stability_level_precise::<f128, I>(integrand, &mut context)?;
3966            Ok(
3967                crate::integrands::evaluation::PreciseEvaluationResult::Quad(
3968                    finalize_precise_evaluation_result(result, wgt, base_metadata),
3969                ),
3970            )
3971        }
3972        Precision::Arb => {
3973            let (stability_iterator, _) =
3974                stability_iterator_for_source(integrand, &source, use_arb_prec);
3975            let stability_level = stability_iterator
3976                .into_iter()
3977                .rev()
3978                .find(|level| level.precision == Precision::Arb)
3979                .ok_or_else(|| {
3980                    eyre!(
3981                        "Arbitrary precision was selected for the final result, but no Arb precision stability level is configured."
3982                    )
3983            })?;
3984            let mut evaluation_metadata = EvaluationMetaData::new_empty();
3985            evaluation_metadata.radial_root_diagnostics =
3986                base_metadata.radial_root_diagnostics.clone();
3987            evaluation_metadata
3988                .radial_root_diagnostics
3989                .restart_precision_pass();
3990            let mut context = StabilityEvaluationContext {
3991                model,
3992                source: &source,
3993                stability_level: &stability_level,
3994                max_eval: &max_eval,
3995                wgt,
3996                check_on_norm: integrand.get_settings().stability.check_on_norm,
3997                is_final_level: true,
3998                is_primary_stability_level: true,
3999                evaluation_metadata: &mut evaluation_metadata,
4000                record_rotated_results: false,
4001                precision_label: "ArbPrec",
4002                escalate_if_exact_zero,
4003            };
4004            let result = evaluate_stability_level_precise::<ArbPrec, I>(integrand, &mut context)?;
4005            Ok(crate::integrands::evaluation::PreciseEvaluationResult::Arb(
4006                finalize_precise_evaluation_result(result, wgt, base_metadata),
4007            ))
4008        }
4009    }
4010}
4011
4012fn warn_selectors_disable_zero_once() {
4013    static ONCE: Once = Once::new();
4014    ONCE.call_once(|| {
4015        warn!(
4016            "disabling `stability.escalate_if_exact_zero` during evaluation because selectors can legitimately zero the event weight"
4017        );
4018    });
4019}
4020
4021fn evaluate_sample<I: ProcessIntegrandImpl>(
4022    integrand: &mut I,
4023    model: &Model,
4024    sample: &Sample<F<f64>>,
4025    wgt: F<f64>,
4026    _iter: usize,
4027    use_arb_prec: bool,
4028    max_eval: Complex<F<f64>>,
4029) -> Result<EvaluationResult> {
4030    evaluate_from_source(
4031        integrand,
4032        model,
4033        EvaluationSource::XSpace(sample),
4034        wgt,
4035        use_arb_prec,
4036        max_eval,
4037    )
4038}
4039
4040fn evaluate_sample_precise<I: ProcessIntegrandImpl>(
4041    integrand: &mut I,
4042    model: &Model,
4043    sample: &Sample<F<f64>>,
4044    wgt: F<f64>,
4045    use_arb_prec: bool,
4046    max_eval: Complex<F<f64>>,
4047) -> Result<crate::integrands::evaluation::PreciseEvaluationResult> {
4048    evaluate_from_source_precise(
4049        integrand,
4050        model,
4051        EvaluationSource::XSpace(sample),
4052        wgt,
4053        use_arb_prec,
4054        max_eval,
4055    )
4056}
4057
4058fn evaluate_momentum_configuration<I: ProcessIntegrandImpl>(
4059    integrand: &mut I,
4060    model: &Model,
4061    input: &MomentumSpaceEvaluationInput,
4062    wgt: F<f64>,
4063    use_arb_prec: bool,
4064    max_eval: Complex<F<f64>>,
4065) -> Result<EvaluationResult> {
4066    evaluate_from_source(
4067        integrand,
4068        model,
4069        EvaluationSource::Momentum(input),
4070        wgt,
4071        use_arb_prec,
4072        max_eval,
4073    )
4074}
4075
4076fn evaluate_momentum_configuration_precise<I: ProcessIntegrandImpl>(
4077    integrand: &mut I,
4078    model: &Model,
4079    input: &MomentumSpaceEvaluationInput,
4080    wgt: F<f64>,
4081    use_arb_prec: bool,
4082    max_eval: Complex<F<f64>>,
4083) -> Result<crate::integrands::evaluation::PreciseEvaluationResult> {
4084    evaluate_from_source_precise(
4085        integrand,
4086        model,
4087        EvaluationSource::Momentum(input),
4088        wgt,
4089        use_arb_prec,
4090        max_eval,
4091    )
4092}
4093
4094#[cfg(test)]
4095mod tests {
4096    use super::{
4097        ChannelIndex, LmbChannelWeightingSettings, LmbMultiChannelingSetup, RuntimeCache,
4098        create_stability_iterator, filtered_orientation_count, resolve_visible_orientation_id,
4099    };
4100    use crate::cff::expression::OrientationID;
4101    use crate::{
4102        dot,
4103        graph::{Graph, LMBext, LmbIndex, LoopMomentumBasis, parse::from_dot::IntoGraph},
4104        initialisation::test_initialise,
4105        momentum::{
4106            ThreeMomentum,
4107            sample::{BareMomentumSample, ExternalFourMomenta, LoopMomenta, MomentumSample},
4108            signature::LoopExtSignature,
4109        },
4110        settings::runtime::{
4111            LmbChannelWeight, ParameterizationSettings, Precision, StabilitySettings,
4112        },
4113        utils::{F, load_generic_model},
4114    };
4115    use linnet::half_edge::{
4116        involution::{EdgeIndex, EdgeVec, Orientation},
4117        subgraph::{ModifySubSet, SubSetLike, subset::SubSet},
4118    };
4119    use std::sync::OnceLock;
4120    use typed_index_collections::TiVec;
4121
4122    #[test]
4123    fn runtime_cache_serializes_as_empty() {
4124        let encoded = bincode::encode_to_vec(
4125            RuntimeCache::<usize>::default(),
4126            bincode::config::standard(),
4127        )
4128        .expect("runtime cache should encode");
4129        assert!(encoded.is_empty());
4130
4131        let (decoded, consumed): (RuntimeCache<usize>, usize) =
4132            bincode::decode_from_slice(&encoded, bincode::config::standard())
4133                .expect("runtime cache should decode");
4134        assert_eq!(consumed, 0);
4135        assert!(decoded.0.is_none());
4136    }
4137
4138    #[test]
4139    fn arbitrary_precision_override_prefers_the_configured_arb_level() {
4140        let mut settings = StabilitySettings::default();
4141        let arb_level = settings
4142            .levels
4143            .iter_mut()
4144            .find(|level| level.precision == Precision::Arb)
4145            .expect("default stability settings should include Arb");
4146        arb_level.required_precision_for_re = 2.5e-7;
4147        let configured_arb_level = *arb_level;
4148
4149        assert_eq!(
4150            create_stability_iterator(&settings, true),
4151            vec![configured_arb_level]
4152        );
4153        assert_eq!(create_stability_iterator(&settings, false), settings.levels);
4154    }
4155
4156    #[test]
4157    fn filtered_orientation_helpers_map_visible_indices_into_subset_order() {
4158        let orientations = TiVec::<OrientationID, EdgeVec<Orientation>>::from_iter([
4159            EdgeVec::from_iter([Orientation::Default]),
4160            EdgeVec::from_iter([Orientation::Reversed]),
4161            EdgeVec::from_iter([Orientation::Undirected]),
4162            EdgeVec::from_iter([Orientation::Default]),
4163        ]);
4164        let mut filter = SubSet::empty(orientations.len());
4165        filter.add(OrientationID(1));
4166        filter.add(OrientationID(3));
4167
4168        assert_eq!(filtered_orientation_count(&filter, &orientations), 2);
4169        assert_eq!(
4170            resolve_visible_orientation_id(&filter, 0),
4171            Some(OrientationID(1))
4172        );
4173        assert_eq!(
4174            resolve_visible_orientation_id(&filter, 1),
4175            Some(OrientationID(3))
4176        );
4177        assert_eq!(resolve_visible_orientation_id(&filter, 2), None);
4178    }
4179
4180    #[test]
4181    fn effective_lmb_basis_ids_use_graph_override_or_optimized_channels() {
4182        test_initialise().unwrap();
4183        static GRAPH: OnceLock<Graph> = OnceLock::new();
4184        let graph = GRAPH
4185            .get_or_init(|| {
4186                dot!(
4187                    digraph lmb_basis_selection {
4188                        edge [num=1 mass=0]
4189                        node [num=1]
4190                        A -> B [id=0]
4191                        A -> B [id=1]
4192                        A -> B [id=2]
4193                    }
4194                )
4195                .unwrap()
4196            })
4197            .clone();
4198        let lmb = |edge_id| LoopMomentumBasis {
4199            tree: graph.underlying.empty_subgraph(),
4200            loop_edges: vec![EdgeIndex::from(edge_id)].into(),
4201            ext_edges: Vec::new().into(),
4202            edge_signatures: graph.underlying.new_edgevec(|_, _, _| {
4203                LoopExtSignature::from((Vec::<isize>::new(), Vec::<isize>::new()))
4204            }),
4205        };
4206        let all_bases = vec![lmb(0), lmb(1), lmb(2)].into();
4207        let setup = LmbMultiChannelingSetup {
4208            channels: vec![LmbIndex::from(2), LmbIndex::from(0)].into(),
4209            graph,
4210            all_bases,
4211        };
4212        let default_settings = ParameterizationSettings::default();
4213        let override_settings = ParameterizationSettings {
4214            lmb_basis_ids: std::collections::BTreeMap::from([("G".to_string(), vec![1])]),
4215            ..Default::default()
4216        };
4217        let out_of_range_settings = ParameterizationSettings {
4218            lmb_basis_ids: std::collections::BTreeMap::from([("G".to_string(), vec![3])]),
4219            ..Default::default()
4220        };
4221
4222        assert_eq!(
4223            setup.selected_lmb_basis_id("G", &default_settings).unwrap(),
4224            LmbIndex::from(2)
4225        );
4226        assert_eq!(
4227            setup
4228                .selected_lmb_basis_id("G", &override_settings)
4229                .unwrap(),
4230            LmbIndex::from(1)
4231        );
4232        assert_eq!(
4233            setup.effective_channels("G", &override_settings).unwrap(),
4234            vec![LmbIndex::from(1)]
4235        );
4236        assert_eq!(setup.effective_channel_count("G", &override_settings), 1);
4237        assert_eq!(
4238            setup
4239                .effective_channel_lmb_id(ChannelIndex::from(0), "G", &override_settings)
4240                .unwrap(),
4241            LmbIndex::from(1)
4242        );
4243        assert_eq!(
4244            setup
4245                .effective_channel_edge_ids(ChannelIndex::from(0), "G", &override_settings)
4246                .unwrap()
4247                .as_slice(),
4248            &[1]
4249        );
4250        assert!(
4251            setup
4252                .selected_lmb_basis_id("G", &out_of_range_settings)
4253                .is_err()
4254        );
4255    }
4256
4257    #[test]
4258    fn lmb_channel_prefactors_form_a_partition_of_unity() {
4259        test_initialise().unwrap();
4260        let mut graph: Graph = dot!(
4261            digraph lmb_prefactor_partition {
4262                edge [num=1 mass=0]
4263                node [num=1]
4264                ext [style=invis]
4265                ext -> A [id=0]
4266                A -> B [id=1]
4267                A -> B [id=2]
4268                B -> ext [id=3]
4269            }
4270        )
4271        .unwrap();
4272        let all_bases = graph.generate_loop_momentum_bases();
4273        assert!(all_bases.len() >= 2);
4274        graph.loop_momentum_basis = all_bases[LmbIndex::from(0)].clone();
4275        let setup = LmbMultiChannelingSetup {
4276            channels: vec![LmbIndex::from(0), LmbIndex::from(1)].into(),
4277            graph: graph.clone(),
4278            all_bases,
4279        };
4280        let loop_moms: LoopMomenta<F<f64>> = graph
4281            .loop_momentum_basis
4282            .loop_edges
4283            .iter()
4284            .enumerate()
4285            .map(|(index, _)| {
4286                let offset = index as f64;
4287                ThreeMomentum::new(F(0.4 + offset), F(-0.3), F(0.2 - offset))
4288            })
4289            .collect();
4290        let external_moms: ExternalFourMomenta<F<f64>> =
4291            (0..graph.loop_momentum_basis.ext_edges.len())
4292                .map(|_| [F(0.0), F(0.0), F(0.0), F(0.0)].into())
4293                .collect();
4294        let sample = MomentumSample {
4295            sample: BareMomentumSample {
4296                loop_moms,
4297                dual_loop_moms: None,
4298                loop_mom_cache_id: 0,
4299                loop_mom_base_cache_id: 0,
4300                external_moms,
4301                external_mom_cache_id: 0,
4302                external_mom_base_cache_id: 0,
4303                jacobian: F(1.0),
4304                orientation: None,
4305                parameterization_branch: None,
4306            },
4307        };
4308        let model = load_generic_model("sm");
4309        let parameterization_settings = ParameterizationSettings::default();
4310        let alpha = F(1.3);
4311
4312        for channel_weight in [LmbChannelWeight::Ose, LmbChannelWeight::InverseJacobian] {
4313            let weighting_settings = LmbChannelWeightingSettings {
4314                graph_name: "G",
4315                model: &model,
4316                alpha: &alpha,
4317                channel_weight,
4318                parameterization_settings: &parameterization_settings,
4319                e_cm: 1.0,
4320            };
4321            let sum = [ChannelIndex::from(0), ChannelIndex::from(1)]
4322                .into_iter()
4323                .map(|channel_index| {
4324                    let selected_lmb = setup
4325                        .effective_channel_lmb_id(channel_index, "G", &parameterization_settings)
4326                        .unwrap();
4327                    setup
4328                        .compute_prefactor_impl(
4329                            channel_index,
4330                            selected_lmb,
4331                            &sample,
4332                            weighting_settings,
4333                        )
4334                        .unwrap()
4335                })
4336                .fold(F(0.0), |sum, weight| sum + weight);
4337
4338            let difference = (sum - sum.one()).abs();
4339            assert!(difference <= sum.epsilon() * sum.from_usize(16));
4340        }
4341    }
4342}