Skip to main content

gammalooprs/integrands/process/
mod.rs

1use std::collections::{BTreeMap, 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, LoopIndex, LoopMomenta, MomentumSample};
15use crate::momentum::{Rotation, ThreeMomentum};
16use crate::observables::{
17    AdditionalWeightKey, EventProcessingRuntime, GenericEvent, HistogramProcessInfo,
18    ObservableAccumulatorBundle, ObservableFileFormat, ObservableSnapshotBundle,
19};
20use crate::processes::{CutGroupId, GraphGroupSelectionSpec, StandaloneExportSettings};
21use crate::subtraction::lu_counterterm::LUSharedOverlaps;
22use crate::utils::{
23    ArbPrec, F, FloatLike, RuntimeCache, SamplingFloat, SamplingPrecision, f128,
24    format_for_compare_digits, get_n_dim_for_n_loop_momenta,
25};
26use bincode_trait_derive::{Decode, Encode};
27use color_eyre::owo_colors::OwoColorize;
28use colored::Colorize;
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(crate) mod fermi_surface;
50pub mod gammaloop_sample;
51pub mod ir;
52pub mod sampling;
53use crate::{
54    DependentMomentaConstructor, GammaLoopContext,
55    settings::RuntimeSettings,
56    settings::runtime::DiscreteGraphSamplingSettings,
57    settings::runtime::DiscreteGraphSamplingType,
58    settings::runtime::IntegratorSettings,
59    settings::runtime::ParameterizationSettings,
60    settings::runtime::Precision,
61    settings::runtime::StabilityLevelSetting,
62    settings::runtime::StabilitySettings,
63    settings::runtime::{IntegratedPhase, SamplingChannelWeight, SamplingSettings},
64};
65use color_eyre::Result;
66use sampling::context::PreparedLUHost;
67use sampling::selection::SamplingChannelPrograms;
68pub use sampling::{
69    CompiledSamplingChannel, CompiledSamplingMap, GaussianReferenceFunction,
70    ImplicitSurfaceContextPreparer, ImplicitSurfaceRadialContextEvaluator,
71    ImplicitSurfaceRadialEvaluator, ImplicitSurfaceRadialMap, PreparedSurfaceStatus,
72    ReferenceMoments, ReferenceSampleEvaluation, ReferenceSamplingReport,
73    ResolvedNamedSamplingChannel, ResolvedSamplingBlock, ResolvedSamplingChannelSelection,
74    SamplingCatalogueEntry, SamplingChannelBridge, SamplingChannelBridgeAcceptanceReport,
75    SamplingChannelBridgeError, SamplingChannelBridgeEvaluation, SamplingChannelCatalogue,
76    SamplingChannelCompileContext, SamplingChannelCompileError, SamplingChannelId,
77    SamplingChannelInspection, SamplingChannelPreset, SamplingChannelRuntimeContexts,
78    SamplingChannelScore, SamplingChannelSelector, SamplingCoverageReport, SamplingCutSide,
79    SamplingDualValue, SamplingExpressionEvaluator, SamplingGeometryKey, SamplingJacobian,
80    SamplingMapAcceptanceReport, SamplingMapAffine, SamplingMapComponent, SamplingMapComposition,
81    SamplingMapContextTransform, SamplingMapContract, SamplingMapDefinition, SamplingMapEmbedding,
82    SamplingMapEvaluation, SamplingMapKernel, SamplingMapPoint, SamplingMomentumSampleContext,
83    SamplingPartition, SamplingPartitionMode, SamplingScoreFunction, SamplingSelectionError,
84    SamplingSupport, SharedEnergyJointGeometry, SharedEnergyJointMap, SurfaceRadialMap,
85    SurfaceRadialPoint, build_sampling_channel_catalogue,
86    build_sampling_channel_catalogue_with_surfaces,
87    build_sampling_channel_catalogue_with_surfaces_and_coverage, explicitly_selected_graphs,
88    graph_channel_definitions, resolve_sampling_channel_selection,
89    resolve_sampling_channel_selection_replacing_default,
90};
91
92pub mod evaluators;
93pub use evaluators::ActiveF64Backend;
94pub use evaluators::{GenericEvaluator, GenericEvaluatorFloat};
95
96pub mod param_builder;
97pub use param_builder::{ParamBuilder, ParamValuePairs, ThresholdParams, UpdateAndGetParams};
98
99pub mod threshold_multiplier;
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
102pub enum OrientationProfileMode {
103    #[default]
104    Summed,
105    PerOrientation,
106}
107
108impl OrientationProfileMode {
109    pub fn profiles_per_orientation(self) -> bool {
110        matches!(self, Self::PerOrientation)
111    }
112}
113
114/// Direct input for evaluating an integrand at explicit spatial loop momenta.
115///
116/// `loop_momenta` contains one `(px, py, pz)` value per independent loop momentum;
117/// energies are reconstructed by the integrand and external momenta come from its
118/// kinematics settings. The selected graph or graph group determines the required
119/// loop count and evaluation rejects a different number of entries.
120#[derive(Debug, Clone)]
121pub struct MomentumSpaceEvaluationInput {
122    /// Spatial loop momenta in loop-basis order.
123    pub loop_momenta: Vec<ThreeMomentum<F<f64>>>,
124    /// Multiplicative integration weight associated with this explicit point.
125    pub integrator_weight: F<f64>,
126    /// Optional graph selected within a non-discrete sampling setup.
127    pub graph_id: Option<usize>,
128    /// Optional graph group selected within a discrete sampling setup.
129    pub group_id: Option<GroupId>,
130    /// Optional orientation within the selected graph or graph group.
131    pub orientation: Option<usize>,
132    /// Optional canonical sampling channel selected by discrete sampling.
133    pub channel_id: Option<SamplingChannelId>,
134}
135
136#[derive(Clone, Encode, Decode)]
137#[trait_decode(trait = GammaLoopContext)]
138#[enum_dispatch(HasIntegrand)]
139pub enum ProcessIntegrand {
140    Amplitude(amplitude::AmplitudeIntegrand),
141    CrossSection(cross_section::CrossSectionIntegrand),
142}
143
144fn discrete_sampling_type_name(sampling_type: &DiscreteGraphSamplingType) -> &'static str {
145    match sampling_type {
146        DiscreteGraphSamplingType::Default(_) => "default",
147        DiscreteGraphSamplingType::MultiChanneling(_) => "multi_channeling",
148        DiscreteGraphSamplingType::TropicalSampling(_) => "tropical",
149        DiscreteGraphSamplingType::SamplingMultiChanneling(_) => "sampling_multi_channeling",
150    }
151}
152
153pub(crate) fn discrete_sampling_depth_for_settings(
154    settings: &DiscreteGraphSamplingSettings,
155) -> usize {
156    let orientation_depth = usize::from(settings.sample_orientations);
157    match &settings.sampling_type {
158        DiscreteGraphSamplingType::SamplingMultiChanneling(_) => 2 + orientation_depth,
159        _ => 1 + orientation_depth,
160    }
161}
162
163fn invalid_discrete_sampling_depth_error(
164    settings: &DiscreteGraphSamplingSettings,
165    actual_depth: usize,
166) -> eyre::Report {
167    let mut axes = vec!["graph group"];
168    if settings.sample_orientations {
169        axes.push("orientation");
170    }
171    if matches!(
172        settings.sampling_type,
173        DiscreteGraphSamplingType::SamplingMultiChanneling(_)
174    ) {
175        axes.push("channel");
176    }
177
178    eyre!(
179        "This integrand uses discrete graph sampling (sample_orientations = {}, sampling_type = {}), so x-space evaluation requires {} discrete dimensions [{}], but got {}.",
180        settings.sample_orientations,
181        discrete_sampling_type_name(&settings.sampling_type),
182        axes.len(),
183        axes.join(", "),
184        actual_depth
185    )
186}
187
188pub(crate) fn resolve_discrete_selection_for_sampling(
189    sampling: &SamplingSettings,
190    discrete_dimensions: &[usize],
191    group_count: usize,
192    mut orientation_count_for_group: impl FnMut(GroupId) -> Option<usize>,
193    mut channel_count_for_group: impl FnMut(GroupId) -> Result<Option<usize>>,
194) -> Result<(Option<GroupId>, Option<usize>, Option<SamplingChannelId>)> {
195    match sampling {
196        SamplingSettings::Default(_) | SamplingSettings::MultiChanneling(_) => {
197            if !discrete_dimensions.is_empty() {
198                return Err(eyre!(
199                    "This integrand does not use discrete graph sampling; expected no discrete dimensions, got {:?}.",
200                    discrete_dimensions
201                ));
202            }
203            Ok((None, None, None))
204        }
205        SamplingSettings::DiscreteGraphs(settings) => {
206            let expected_depth = discrete_sampling_depth_for_settings(settings);
207            if discrete_dimensions.len() != expected_depth {
208                return Err(invalid_discrete_sampling_depth_error(
209                    settings,
210                    discrete_dimensions.len(),
211                ));
212            }
213
214            let group_id = GroupId(discrete_dimensions[0]);
215            if group_id.0 >= group_count {
216                return Err(eyre!(
217                    "Discrete graph group index {} is out of range; the integrand has {} groups.",
218                    group_id.0,
219                    group_count
220                ));
221            }
222
223            let orientation = if settings.sample_orientations {
224                let orientation = discrete_dimensions[1];
225                let orientation_count = orientation_count_for_group(group_id).ok_or_else(|| {
226                    eyre!(
227                        "Could not determine orientation count for group {}.",
228                        group_id.0
229                    )
230                })?;
231                if orientation >= orientation_count {
232                    return Err(eyre!(
233                        "Orientation {} is out of range for graph group {}; the group has {} orientations.",
234                        orientation,
235                        group_id.0,
236                        orientation_count
237                    ));
238                }
239                Some(orientation)
240            } else {
241                None
242            };
243
244            let channel = match &settings.sampling_type {
245                DiscreteGraphSamplingType::SamplingMultiChanneling(_) => {
246                    let channel_index = *discrete_dimensions.last().expect("validated depth");
247                    let channel_count = channel_count_for_group(group_id)?.ok_or_else(|| {
248                        eyre!(
249                            "Could not determine channel count for group {}.",
250                            group_id.0
251                        )
252                    })?;
253                    if channel_index >= channel_count {
254                        return Err(eyre!(
255                            "Channel {} is out of range for graph group {}; the group has {} channels.",
256                            channel_index,
257                            group_id.0,
258                            channel_count
259                        ));
260                    }
261                    Some(SamplingChannelId::from(channel_index))
262                }
263                _ => None,
264            };
265
266            Ok((Some(group_id), orientation, channel))
267        }
268    }
269}
270
271impl ProcessIntegrand {
272    pub fn clone_with_selected_graph_groups(&self, graph_names: &[String]) -> Result<Self> {
273        if graph_names.is_empty() {
274            return Ok(self.clone());
275        }
276        if !matches!(
277            self.get_settings().sampling,
278            SamplingSettings::DiscreteGraphs(_)
279        ) {
280            return Err(eyre!(
281                "Runtime graph-group selection requires graphs = 'monte_carlo'."
282            ));
283        }
284        let mut unique_names = BTreeSet::new();
285        for graph_name in graph_names {
286            if !unique_names.insert(graph_name) {
287                return Err(eyre!(
288                    "Runtime graph-group selection contains duplicate graph name '{}'.",
289                    graph_name
290                ));
291            }
292        }
293
294        let selection = GraphGroupSelectionSpec::from_master_graph_names(graph_names.to_vec());
295        let mut selected = match self {
296            Self::Amplitude(integrand) => {
297                let plan = selection.plan(&integrand.data.graph_group_structure, |graph_id| {
298                    integrand
299                        .data
300                        .graph_terms
301                        .get(graph_id)
302                        .map(|term| &term.graph)
303                })?;
304                Self::Amplitude(integrand.clone_with_graph_group_selection(&plan)?)
305            }
306            Self::CrossSection(integrand) => {
307                let plan = selection.plan(&integrand.data.graph_group_structure, |graph_id| {
308                    integrand
309                        .data
310                        .graph_terms
311                        .get(graph_id)
312                        .map(|term| &term.graph)
313                })?;
314                Self::CrossSection(integrand.clone_with_graph_group_selection(&plan)?)
315            }
316        };
317        let selected_names = selected
318            .graph_group_master_names()
319            .into_iter()
320            .map(str::to_string)
321            .collect();
322        let SamplingSettings::DiscreteGraphs(sampling) = &mut selected.get_mut_settings().sampling
323        else {
324            unreachable!("validated graph sampling before constructing the selected view")
325        };
326        sampling.graph_names = selected_names;
327        Ok(selected)
328    }
329
330    pub fn resume_fingerprint(&self) -> Result<String> {
331        let mut bytes = match self {
332            Self::Amplitude(integrand) => {
333                bincode::encode_to_vec(&integrand.data, bincode::config::standard())
334            }
335            Self::CrossSection(integrand) => {
336                bincode::encode_to_vec(&integrand.data, bincode::config::standard())
337            }
338        }
339        .map_err(|err| eyre!("Could not serialize integrand fingerprint payload: {err}"))?;
340
341        let mut hash = 0xcbf29ce484222325u64;
342        for byte in self
343            .variant_tag()
344            .as_bytes()
345            .iter()
346            .copied()
347            .chain(bytes.drain(..))
348        {
349            hash ^= u64::from(byte);
350            hash = hash.wrapping_mul(0x100000001b3);
351        }
352
353        Ok(format!("{hash:016x}"))
354    }
355
356    fn variant_tag(&self) -> &'static str {
357        match self {
358            Self::Amplitude(_) => "amplitude",
359            Self::CrossSection(_) => "cross_section",
360        }
361    }
362
363    pub fn kind_name(&self) -> &'static str {
364        self.variant_tag()
365    }
366
367    pub fn export_standalone(
368        &self,
369        path: impl AsRef<Path>,
370        settings: &StandaloneExportSettings,
371    ) -> Result<()> {
372        match self {
373            Self::Amplitude(a) => a.export_standalone(path, settings),
374            Self::CrossSection(a) => a.export_standalone(path, settings),
375        }
376    }
377
378    pub fn warm_up(&mut self, model: &Model) -> Result<()> {
379        let settings = self.get_mut_settings();
380        if matches!(
381            &settings.sampling,
382            SamplingSettings::DiscreteGraphs(settings) if settings.sample_orientations
383        ) {
384            if !matches!(
385                settings.general.evaluator_method,
386                evaluators::EvaluatorMethod::SingleParametric
387            ) {
388                return Err(eyre!(
389                    "Monte Carlo sampling over orientations requires general.evaluator_method=SingleParametric; got {:?}.",
390                    settings.general.evaluator_method
391                ));
392            }
393            warn!("Monte Carlo sampling over orientations is using the SingleParametric evaluator");
394        }
395
396        match self {
397            Self::Amplitude(a) => a.warm_up(model),
398            Self::CrossSection(a) => a.warm_up(model),
399        }
400    }
401
402    pub fn frozen_compilation(&self) -> &crate::settings::global::FrozenCompilationMode {
403        match self {
404            Self::Amplitude(a) => a.frozen_compilation(),
405            Self::CrossSection(a) => a.frozen_compilation(),
406        }
407    }
408
409    pub fn active_f64_backend(&self) -> ActiveF64Backend {
410        match self {
411            Self::Amplitude(a) => a.active_f64_backend(),
412            Self::CrossSection(a) => a.active_f64_backend(),
413        }
414    }
415
416    pub(crate) fn activate_runtime_backends_after_load(
417        &mut self,
418        allow_symjit_fallback: bool,
419    ) -> Result<Option<String>> {
420        match self {
421            Self::Amplitude(a) => a.activate_runtime_backends_after_load(allow_symjit_fallback),
422            Self::CrossSection(a) => a.activate_runtime_backends_after_load(allow_symjit_fallback),
423        }
424    }
425
426    pub(crate) fn save(&self, path: impl AsRef<Path>, override_existing: bool) -> Result<()> {
427        let path = path.as_ref().join("integrand");
428
429        let r = fs::create_dir_all(&path).with_context(|| {
430            format!(
431                "Trying to create directory to save amplitude {}",
432                path.display()
433            )
434        });
435        if override_existing {
436            r?;
437        }
438        match self {
439            ProcessIntegrand::Amplitude(integrand) => integrand.save(path, override_existing),
440            ProcessIntegrand::CrossSection(integrand) => integrand.save(path, override_existing),
441        }
442    }
443
444    pub(crate) fn compile(
445        &mut self,
446        path: impl AsRef<Path>,
447        override_existing: bool,
448        thread_pool: &rayon::ThreadPool,
449    ) -> Result<Vec<(String, std::time::Duration)>> {
450        let path = path.as_ref().join("integrand");
451
452        let r = fs::create_dir_all(&path).with_context(|| {
453            format!(
454                "Trying to create directory to save amplitude {}",
455                path.display()
456            )
457        });
458        if override_existing {
459            r?;
460        }
461        match self {
462            ProcessIntegrand::Amplitude(integrand) => {
463                integrand.compile(path, override_existing, thread_pool)
464            }
465            ProcessIntegrand::CrossSection(integrand) => {
466                integrand.compile(path, override_existing, thread_pool)
467            }
468        }
469    }
470
471    pub fn get_settings(&self) -> &RuntimeSettings {
472        match self {
473            ProcessIntegrand::Amplitude(integrand) => &integrand.settings,
474            ProcessIntegrand::CrossSection(integrand) => &integrand.settings,
475        }
476    }
477
478    /// Invalidate runtime caches before exposing mutable settings. Call
479    /// `warm_up` again before evaluating samples through sampling channels.
480    pub fn get_mut_settings(&mut self) -> &mut RuntimeSettings {
481        match self {
482            ProcessIntegrand::Amplitude(integrand) => {
483                integrand.invalidate_runtime_caches();
484                &mut integrand.settings
485            }
486            ProcessIntegrand::CrossSection(integrand) => {
487                integrand.invalidate_runtime_caches();
488                &mut integrand.settings
489            }
490        }
491    }
492
493    pub fn graph_count(&self) -> usize {
494        match self {
495            ProcessIntegrand::Amplitude(integrand) => integrand.data.graph_terms.len(),
496            ProcessIntegrand::CrossSection(integrand) => integrand.data.graph_terms.len(),
497        }
498    }
499
500    pub fn graph_group_count(&self) -> usize {
501        match self {
502            ProcessIntegrand::Amplitude(integrand) => integrand.data.graph_group_structure.len(),
503            ProcessIntegrand::CrossSection(integrand) => integrand.data.graph_group_structure.len(),
504        }
505    }
506
507    pub fn graph_group_master_names(&self) -> Vec<&str> {
508        match self {
509            ProcessIntegrand::Amplitude(integrand) => integrand
510                .data
511                .graph_group_structure
512                .iter()
513                .map(|group| {
514                    integrand.data.graph_terms[group.master()]
515                        .graph
516                        .name
517                        .as_str()
518                })
519                .collect(),
520            ProcessIntegrand::CrossSection(integrand) => integrand
521                .data
522                .graph_group_structure
523                .iter()
524                .map(|group| {
525                    integrand.data.graph_terms[group.master()]
526                        .graph
527                        .name
528                        .as_str()
529                })
530                .collect(),
531        }
532    }
533
534    pub fn find_graph_id_by_name(&self, graph_name: &str) -> Option<usize> {
535        match self {
536            ProcessIntegrand::Amplitude(integrand) => integrand
537                .data
538                .graph_terms
539                .iter()
540                .position(|term| term.graph.name == graph_name),
541            ProcessIntegrand::CrossSection(integrand) => integrand
542                .data
543                .graph_terms
544                .iter()
545                .position(|term| term.graph.name == graph_name),
546        }
547    }
548
549    pub fn graph_name_by_id(&self, graph_id: usize) -> Option<&str> {
550        match self {
551            ProcessIntegrand::Amplitude(integrand) => integrand
552                .data
553                .graph_terms
554                .get(graph_id)
555                .map(|term| term.graph.name.as_str()),
556            ProcessIntegrand::CrossSection(integrand) => integrand
557                .data
558                .graph_terms
559                .get(graph_id)
560                .map(|term| term.graph.name.as_str()),
561        }
562    }
563
564    pub fn graph_group_id_by_graph_id(&self, graph_id: usize) -> Option<usize> {
565        self.find_group_id_containing_graph(graph_id)
566            .map(usize::from)
567    }
568
569    pub fn cut_edge_ids(&self, graph_id: usize, cut_id: usize) -> Option<Vec<usize>> {
570        match self {
571            ProcessIntegrand::Amplitude(integrand) => {
572                (cut_id == 0 && graph_id < integrand.data.graph_terms.len()).then(Vec::new)
573            }
574            ProcessIntegrand::CrossSection(integrand) => {
575                let graph_term = integrand.data.graph_terms.get(graph_id)?;
576                let cut = graph_term.cuts.get(crate::processes::CutId::from(cut_id))?;
577                Some(
578                    graph_term
579                        .graph
580                        .underlying
581                        .iter_edges_of(&cut.cut)
582                        .map(|(_, edge_id, _)| edge_id.0)
583                        .sorted()
584                        .collect(),
585                )
586            }
587        }
588    }
589
590    pub fn lmb_sample_id_for_channel(
591        &self,
592        graph_id: usize,
593        sampling_channel_id: usize,
594        parameterization_settings: &ParameterizationSettings,
595    ) -> Result<Option<usize>> {
596        match self {
597            ProcessIntegrand::Amplitude(integrand) => {
598                let Some(graph_term) = integrand.data.graph_terms.get(graph_id) else {
599                    return Ok(None);
600                };
601                Ok(graph_term
602                    .multi_channeling_setup
603                    .sampling_channel_lmb_id(
604                        SamplingChannelId::from(sampling_channel_id),
605                        &graph_term.multi_channeling_setup.graph.name,
606                        parameterization_settings,
607                    )?
608                    .map(usize::from))
609            }
610            ProcessIntegrand::CrossSection(integrand) => {
611                let Some(graph_term) = integrand.data.graph_terms.get(graph_id) else {
612                    return Ok(None);
613                };
614                Ok(graph_term
615                    .multi_channeling_setup
616                    .sampling_channel_lmb_id(
617                        SamplingChannelId::from(sampling_channel_id),
618                        &graph_term.multi_channeling_setup.graph.name,
619                        parameterization_settings,
620                    )?
621                    .map(usize::from))
622            }
623        }
624    }
625
626    fn find_group_id_containing_graph(&self, graph_id: usize) -> Option<GroupId> {
627        match self {
628            ProcessIntegrand::Amplitude(integrand) => integrand
629                .data
630                .graph_group_structure
631                .iter_enumerated()
632                .find_map(|(group_id, group)| {
633                    group.into_iter().contains(&graph_id).then_some(group_id)
634                }),
635            ProcessIntegrand::CrossSection(integrand) => integrand
636                .data
637                .graph_group_structure
638                .iter_enumerated()
639                .find_map(|(group_id, group)| {
640                    group.into_iter().contains(&graph_id).then_some(group_id)
641                }),
642        }
643    }
644
645    pub fn resolve_group_id_by_master_name(&self, graph_name: &str) -> Result<GroupId> {
646        match self.find_graph_id_by_name(graph_name) {
647            Some(graph_id) => {
648                let group_id = self
649                    .find_group_id_containing_graph(graph_id)
650                    .ok_or_else(|| {
651                        eyre!("Could not find graph group for graph '{}'.", graph_name)
652                    })?;
653                let master_graph_name = match self {
654                    ProcessIntegrand::Amplitude(integrand) => {
655                        &integrand.data.graph_terms
656                            [integrand.data.graph_group_structure[group_id].master()]
657                        .graph
658                        .name
659                    }
660                    ProcessIntegrand::CrossSection(integrand) => {
661                        &integrand.data.graph_terms
662                            [integrand.data.graph_group_structure[group_id].master()]
663                        .graph
664                        .name
665                    }
666                };
667                if master_graph_name != graph_name {
668                    return Err(eyre!(
669                        "Graph '{}' is not the master graph of its group; use '{}' instead.",
670                        graph_name,
671                        master_graph_name
672                    ));
673                }
674                Ok(group_id)
675            }
676            None => Err(eyre!(
677                "Unknown graph '{}' in momentum-space evaluation.",
678                graph_name
679            )),
680        }
681    }
682
683    pub fn resolve_discrete_selection(
684        &self,
685        discrete_dimensions: &[usize],
686    ) -> Result<(Option<GroupId>, Option<usize>, Option<SamplingChannelId>)> {
687        let group_count = match self {
688            ProcessIntegrand::Amplitude(integrand) => integrand.data.graph_group_structure.len(),
689            ProcessIntegrand::CrossSection(integrand) => integrand.data.graph_group_structure.len(),
690        };
691
692        resolve_discrete_selection_for_sampling(
693            &self.get_settings().sampling,
694            discrete_dimensions,
695            group_count,
696            |group_id| self.group_orientation_count(group_id),
697            |group_id| Ok(self.group_channel_count(group_id)),
698        )
699    }
700
701    pub fn expected_x_space_dimension(&self, discrete_dimensions: &[usize]) -> Result<usize> {
702        let settings = self.get_settings();
703        let (group_id, _, _) = self.resolve_discrete_selection(discrete_dimensions)?;
704        if matches!(
705            &settings.sampling,
706            SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
707                sampling_type: DiscreteGraphSamplingType::TropicalSampling(_),
708                ..
709            })
710        ) {
711            let group_id = group_id.ok_or_else(|| {
712                eyre!("Tropical sampling requires a discrete graph-group selection.")
713            })?;
714            let (loop_number, n_edges) = match self {
715                ProcessIntegrand::Amplitude(integrand) => {
716                    let master_graph = &integrand.data.graph_terms
717                        [integrand.data.graph_group_structure[group_id].master()];
718                    (
719                        master_graph.get_graph().get_loop_number(),
720                        master_graph.get_graph().iter_loop_edges().count(),
721                    )
722                }
723                ProcessIntegrand::CrossSection(integrand) => {
724                    let master_graph = &integrand.data.graph_terms
725                        [integrand.data.graph_group_structure[group_id].master()];
726                    (
727                        master_graph.get_graph().get_loop_number(),
728                        master_graph.get_graph().iter_loop_edges().count(),
729                    )
730                }
731            };
732            return Ok(get_n_dim_for_n_loop_momenta(
733                &settings.sampling,
734                loop_number,
735                Some(n_edges),
736            ));
737        }
738
739        let loop_number = match self {
740            ProcessIntegrand::Amplitude(integrand) => {
741                integrand.data.graph_terms[0].graph.get_loop_number()
742            }
743            ProcessIntegrand::CrossSection(integrand) => {
744                integrand.data.graph_terms[0].graph.get_loop_number()
745            }
746        };
747        Ok(get_n_dim_for_n_loop_momenta(
748            &settings.sampling,
749            loop_number,
750            None,
751        ))
752    }
753
754    pub fn discrete_sampling_depth(&self) -> usize {
755        match &self.get_settings().sampling {
756            SamplingSettings::Default(_) | SamplingSettings::MultiChanneling(_) => 0,
757            SamplingSettings::DiscreteGraphs(settings) => {
758                discrete_sampling_depth_for_settings(settings)
759            }
760        }
761    }
762
763    pub fn group_orientation_count(&self, group_id: GroupId) -> Option<usize> {
764        match self {
765            ProcessIntegrand::Amplitude(integrand) => Some(
766                integrand.data.graph_terms[integrand.data.graph_group_structure[group_id].master()]
767                    .get_num_orientations(),
768            ),
769            ProcessIntegrand::CrossSection(integrand) => Some(
770                integrand.data.graph_terms[integrand.data.graph_group_structure[group_id].master()]
771                    .get_num_orientations(),
772            ),
773        }
774    }
775
776    /// Return the canonical sampling-channel IDs for a graph group.
777    ///
778    /// The IDs come directly from the master graph's single catalogue. This
779    /// helper is intended for saved-state acceptance callers constructing
780    /// explicit `(group, orientation, channel)` selections; callers must pass
781    /// the same parameterization settings used by the loaded state.
782    pub fn group_sampling_channel_ids(
783        &self,
784        group_id: GroupId,
785        parameterization_settings: &ParameterizationSettings,
786    ) -> Result<Vec<SamplingChannelId>> {
787        let group = match self {
788            ProcessIntegrand::Amplitude(integrand) => {
789                integrand.data.graph_group_structure.get(group_id)
790            }
791            ProcessIntegrand::CrossSection(integrand) => {
792                integrand.data.graph_group_structure.get(group_id)
793            }
794        }
795        .ok_or_else(|| eyre!("Unknown graph group {}.", group_id.0))?;
796        let master = group.master();
797        match self {
798            ProcessIntegrand::Amplitude(integrand) => {
799                integrand.data.graph_terms[master].sampling_channel_ids(parameterization_settings)
800            }
801            ProcessIntegrand::CrossSection(integrand) => {
802                integrand.data.graph_terms[master].sampling_channel_ids(parameterization_settings)
803            }
804        }
805    }
806
807    pub fn group_channel_count(&self, group_id: GroupId) -> Option<usize> {
808        let parameterization_settings = self
809            .get_settings()
810            .sampling
811            .get_parameterization_settings()
812            .unwrap_or_default();
813        self.group_sampling_channel_ids(group_id, &parameterization_settings)
814            .ok()
815            .map(|channel_ids| channel_ids.len())
816    }
817
818    pub fn graph_orientation_count(&self, graph_id: usize) -> Option<usize> {
819        match self {
820            ProcessIntegrand::Amplitude(integrand) => integrand
821                .data
822                .graph_terms
823                .get(graph_id)
824                .map(|term| term.get_num_orientations()),
825            ProcessIntegrand::CrossSection(integrand) => integrand
826                .data
827                .graph_terms
828                .get(graph_id)
829                .map(|term| term.get_num_orientations()),
830        }
831    }
832
833    pub fn evaluate_momentum_configuration(
834        &mut self,
835        model: &Model,
836        input: &MomentumSpaceEvaluationInput,
837        use_arb_prec: bool,
838    ) -> Result<EvaluationResult> {
839        match self {
840            ProcessIntegrand::Amplitude(integrand) => evaluate_momentum_configuration(
841                integrand,
842                model,
843                input,
844                input.integrator_weight,
845                use_arb_prec,
846                Complex::new_zero(),
847            ),
848            ProcessIntegrand::CrossSection(integrand) => evaluate_momentum_configuration(
849                integrand,
850                model,
851                input,
852                input.integrator_weight,
853                use_arb_prec,
854                Complex::new_zero(),
855            ),
856        }
857    }
858
859    pub fn evaluate_sample_precise(
860        &mut self,
861        sample: &Sample<F<f64>>,
862        model: &Model,
863        wgt: F<f64>,
864        use_arb_prec: bool,
865        max_eval: Complex<F<f64>>,
866    ) -> Result<PreciseEvaluationResult> {
867        match self {
868            ProcessIntegrand::Amplitude(integrand) => {
869                evaluate_sample_precise(integrand, model, sample, wgt, use_arb_prec, max_eval)
870            }
871            ProcessIntegrand::CrossSection(integrand) => {
872                evaluate_sample_precise(integrand, model, sample, wgt, use_arb_prec, max_eval)
873            }
874        }
875    }
876
877    /// Evaluate a normalized reference function through the real process
878    /// parameterization, retaining its Jacobian and integrator weight.
879    ///
880    /// This is intentionally a test-only style overlay: graph routing,
881    /// orientation/channel selection and all sampling maps are still exercised,
882    /// while the physical graph evaluation is replaced by `reference`.
883    pub fn evaluate_reference_sample(
884        &mut self,
885        sample: &Sample<F<f64>>,
886        reference: &GaussianReferenceFunction,
887    ) -> Result<EvaluationResult> {
888        match self {
889            ProcessIntegrand::Amplitude(integrand) => {
890                Ok(evaluate_reference_sample(integrand, sample, reference)?.evaluation)
891            }
892            ProcessIntegrand::CrossSection(integrand) => {
893                Ok(evaluate_reference_sample(integrand, sample, reference)?.evaluation)
894            }
895        }
896    }
897
898    /// Evaluate a normalized reference function from unit-cube coordinates.
899    ///
900    /// This convenience entry point is intended for saved-state acceptance
901    /// harnesses: callers can load a process normally, generate deterministic
902    /// coordinates (for example Halton points), and exercise the complete
903    /// process parameterization without constructing Symbolica `Sample`
904    /// values themselves. The coordinate batch is interpreted as an equally
905    /// weighted quadrature rule, so each sample has unit inverse-density weight
906    /// and the report averages once over the `N` draws; the
907    /// canonical sampling channel selected by
908    /// the loaded settings remains responsible for its map and Jacobian.
909    pub fn evaluate_reference_coordinates(
910        &mut self,
911        coordinates: &[Vec<f64>],
912        reference: &GaussianReferenceFunction,
913    ) -> Result<ReferenceSamplingReport> {
914        if coordinates.is_empty() {
915            return Err(eyre!(
916                "reference acceptance coordinate batch needs at least one sample"
917            ));
918        }
919        let samples = coordinates
920            .iter()
921            .enumerate()
922            .map(|(sample_index, coordinate)| {
923                if coordinate.iter().any(|value| !value.is_finite()) {
924                    return Err(eyre!(
925                        "reference acceptance sample {sample_index} contains a non-finite coordinate"
926                    ));
927                }
928                Ok(Sample::Continuous(
929                    F(1.0),
930                    coordinate.iter().copied().map(F).collect(),
931                ))
932            })
933            .collect::<Result<Vec<_>>>()?;
934        self.evaluate_reference_samples(&samples, reference)
935    }
936
937    /// Evaluate deterministic coordinates under one explicit discrete
938    /// selection path. `discrete_indices` is ordered from the outermost
939    /// `Sample::Discrete` selector to the innermost one, matching the
940    /// canonical graph/orientation/channel order used by the process sampler.
941    /// The selection itself is never re-enumerated or filtered here; callers
942    /// must supply IDs obtained from the loaded process' canonical catalogue.
943    /// Unit discrete weights make this a partition contribution, which need
944    /// not integrate to one. A complete acceptance estimator must sum the
945    /// per-draw contributions or use MC samples with reciprocal selection
946    /// probabilities. Do not add report errors for channels sharing the same
947    /// coordinates: their contributions must be combined before squaring.
948    pub fn evaluate_reference_discrete_coordinates(
949        &mut self,
950        discrete_indices: &[usize],
951        coordinates: &[Vec<f64>],
952        reference: &GaussianReferenceFunction,
953    ) -> Result<ReferenceSamplingReport> {
954        if coordinates.is_empty() {
955            return Err(eyre!(
956                "reference acceptance coordinate batch needs at least one sample"
957            ));
958        }
959        let samples = coordinates
960            .iter()
961            .enumerate()
962            .map(|(sample_index, coordinate)| {
963                if coordinate.iter().any(|value| !value.is_finite()) {
964                    return Err(eyre!(
965                        "reference acceptance sample {sample_index} contains a non-finite coordinate"
966                    ));
967                }
968                let mut sample = Sample::Continuous(
969                    F(1.0),
970                    coordinate.iter().copied().map(F).collect(),
971                );
972                for index in discrete_indices.iter().rev() {
973                    sample = Sample::Discrete(F(1.0), *index, Some(Box::new(sample)));
974                }
975                Ok(sample)
976            })
977            .collect::<Result<Vec<_>>>()?;
978        self.evaluate_reference_samples(&samples, reference)
979    }
980
981    /// Evaluate a batch of samples with a normalized reference function while
982    /// retaining the process maps, Jacobians and sampling-grid weights.
983    pub fn evaluate_reference_samples(
984        &mut self,
985        samples: &[Sample<F<f64>>],
986        reference: &GaussianReferenceFunction,
987    ) -> Result<ReferenceSamplingReport> {
988        if samples.is_empty() {
989            return Err(eyre!("reference acceptance needs at least one sample"));
990        }
991        let evaluations = samples
992            .iter()
993            .map(|sample| self.evaluate_reference_sample_detailed(sample, reference))
994            .collect::<Result<Vec<_>>>()?;
995        let report = ReferenceSamplingReport::from_evaluations(evaluations, reference);
996        if report.finite_sample_count != report.sample_count
997            || !report.normalization.is_finite()
998            || !report.normalization_stderr.is_finite()
999            || !report.second_moment.is_finite()
1000            || !report.second_moment_stderr.is_finite()
1001        {
1002            return Err(eyre!(
1003                "reference acceptance has invalid mapped contributions or statistics: {} finite draws out of {}",
1004                report.finite_sample_count,
1005                report.sample_count
1006            ));
1007        }
1008        Ok(report)
1009    }
1010
1011    /// Detailed variant used by acceptance harnesses that also check raw
1012    /// momentum moments.  The public scalar method above remains convenient
1013    /// for callers interested only in the mapped value.
1014    pub fn evaluate_reference_sample_detailed(
1015        &mut self,
1016        sample: &Sample<F<f64>>,
1017        reference: &GaussianReferenceFunction,
1018    ) -> Result<ReferenceSampleEvaluation> {
1019        match self {
1020            ProcessIntegrand::Amplitude(integrand) => {
1021                evaluate_reference_sample(integrand, sample, reference)
1022            }
1023            ProcessIntegrand::CrossSection(integrand) => {
1024                evaluate_reference_sample(integrand, sample, reference)
1025            }
1026        }
1027    }
1028
1029    pub fn evaluate_momentum_configuration_precise(
1030        &mut self,
1031        model: &Model,
1032        input: &MomentumSpaceEvaluationInput,
1033        use_arb_prec: bool,
1034    ) -> Result<PreciseEvaluationResult> {
1035        match self {
1036            ProcessIntegrand::Amplitude(integrand) => evaluate_momentum_configuration_precise(
1037                integrand,
1038                model,
1039                input,
1040                input.integrator_weight,
1041                use_arb_prec,
1042                Complex::new_zero(),
1043            ),
1044            ProcessIntegrand::CrossSection(integrand) => evaluate_momentum_configuration_precise(
1045                integrand,
1046                model,
1047                input,
1048                input.integrator_weight,
1049                use_arb_prec,
1050                Complex::new_zero(),
1051            ),
1052        }
1053    }
1054
1055    pub fn evaluate_samples_raw(
1056        &mut self,
1057        target: EvaluationTarget<'_>,
1058        samples: &[Sample<F<f64>>],
1059        _iter: usize,
1060        use_arb_prec: bool,
1061        stop_on_interrupt: bool,
1062        max_eval: Complex<F<f64>>,
1063    ) -> Result<RawBatchEvaluationResult> {
1064        self.evaluate_samples_raw_with_estimate(
1065            target,
1066            samples,
1067            _iter,
1068            use_arb_prec,
1069            stop_on_interrupt,
1070            max_eval,
1071            None,
1072        )
1073    }
1074
1075    #[allow(clippy::too_many_arguments)]
1076    pub fn evaluate_samples_raw_with_estimate(
1077        &mut self,
1078        target: EvaluationTarget<'_>,
1079        samples: &[Sample<F<f64>>],
1080        _iter: usize,
1081        use_arb_prec: bool,
1082        stop_on_interrupt: bool,
1083        max_eval: Complex<F<f64>>,
1084        integral_estimate: Option<(f64, f64)>,
1085    ) -> Result<RawBatchEvaluationResult> {
1086        let mut results = Vec::with_capacity(samples.len());
1087        for sample in samples {
1088            if stop_on_interrupt && crate::is_interrupted() {
1089                break;
1090            }
1091            macro_rules! evaluate {
1092                ($integrand:expr) => {
1093                    evaluate_from_source_precise_with_estimate(
1094                        $integrand,
1095                        target,
1096                        EvaluationSource::XSpace(sample),
1097                        sample.get_weight(),
1098                        use_arb_prec,
1099                        max_eval,
1100                        integral_estimate,
1101                        None,
1102                    )
1103                };
1104            }
1105            let precise = match self {
1106                ProcessIntegrand::Amplitude(integrand) => evaluate!(integrand),
1107                ProcessIntegrand::CrossSection(integrand) => evaluate!(integrand),
1108            }?;
1109            let mut result = match target {
1110                EvaluationTarget::Reference(reference) => reference.integration_report(precise)?,
1111                EvaluationTarget::Physical(_) => precise.try_into_f64()?,
1112                #[cfg(test)]
1113                EvaluationTarget::SamplingLaw(_) => precise.try_into_f64()?,
1114            };
1115
1116            self.process_evaluation_result(&result);
1117            maybe_discard_generated_events_in_result(self.get_settings(), &mut result);
1118            results.push(result);
1119            if stop_on_interrupt && crate::is_interrupted() {
1120                break;
1121            }
1122        }
1123
1124        Ok(RawBatchEvaluationResult {
1125            statistics: StatisticsCounter::from_evaluation_results(&results),
1126            samples: results,
1127        })
1128    }
1129
1130    pub fn evaluate_momentum_configurations_raw(
1131        &mut self,
1132        model: &Model,
1133        inputs: &[MomentumSpaceEvaluationInput],
1134        use_arb_prec: bool,
1135    ) -> Result<RawBatchEvaluationResult> {
1136        let mut results = Vec::with_capacity(inputs.len());
1137        for input in inputs {
1138            let mut result = match self {
1139                ProcessIntegrand::Amplitude(integrand) => evaluate_momentum_configuration(
1140                    integrand,
1141                    model,
1142                    input,
1143                    input.integrator_weight,
1144                    use_arb_prec,
1145                    Complex::new_zero(),
1146                ),
1147                ProcessIntegrand::CrossSection(integrand) => evaluate_momentum_configuration(
1148                    integrand,
1149                    model,
1150                    input,
1151                    input.integrator_weight,
1152                    use_arb_prec,
1153                    Complex::new_zero(),
1154                ),
1155            }?;
1156
1157            self.process_evaluation_result(&result);
1158            maybe_discard_generated_events_in_result(self.get_settings(), &mut result);
1159            results.push(result);
1160        }
1161
1162        Ok(RawBatchEvaluationResult {
1163            statistics: StatisticsCounter::from_evaluation_results(&results),
1164            samples: results,
1165        })
1166    }
1167
1168    pub fn evaluate_samples_precise_raw(
1169        &mut self,
1170        model: &Model,
1171        samples: &[Sample<F<f64>>],
1172        use_arb_prec: bool,
1173        max_eval: Complex<F<f64>>,
1174    ) -> Result<RawPreciseBatchEvaluationResult> {
1175        let mut results = Vec::with_capacity(samples.len());
1176        for sample in samples {
1177            results.push(self.evaluate_sample_precise(
1178                sample,
1179                model,
1180                sample.get_weight(),
1181                use_arb_prec,
1182                max_eval,
1183            )?);
1184        }
1185
1186        Ok(RawPreciseBatchEvaluationResult { samples: results })
1187    }
1188
1189    pub fn evaluate_momentum_configurations_precise_raw(
1190        &mut self,
1191        model: &Model,
1192        inputs: &[MomentumSpaceEvaluationInput],
1193        use_arb_prec: bool,
1194    ) -> Result<RawPreciseBatchEvaluationResult> {
1195        let mut results = Vec::with_capacity(inputs.len());
1196        for input in inputs {
1197            results.push(self.evaluate_momentum_configuration_precise(
1198                model,
1199                input,
1200                use_arb_prec,
1201            )?);
1202        }
1203
1204        Ok(RawPreciseBatchEvaluationResult { samples: results })
1205    }
1206
1207    pub fn process_evaluation_result(&mut self, result: &EvaluationResult) {
1208        match self {
1209            ProcessIntegrand::Amplitude(integrand) => {
1210                process_evaluation_result_runtime(integrand, result)
1211            }
1212            ProcessIntegrand::CrossSection(integrand) => {
1213                process_evaluation_result_runtime(integrand, result)
1214            }
1215        }
1216    }
1217
1218    pub fn merge_event_processing_runtime(&mut self, other: &mut Self) -> Result<()> {
1219        match (self, other) {
1220            (ProcessIntegrand::Amplitude(lhs), ProcessIntegrand::Amplitude(rhs)) => {
1221                merge_event_processing_runtime(lhs, rhs)
1222            }
1223            (ProcessIntegrand::CrossSection(lhs), ProcessIntegrand::CrossSection(rhs)) => {
1224                merge_event_processing_runtime(lhs, rhs)
1225            }
1226            _ => Err(eyre!(
1227                "Cannot merge event-processing runtime for incompatible process integrands."
1228            )),
1229        }
1230    }
1231
1232    pub fn update_event_processing_runtime(&mut self, iter: usize) {
1233        match self {
1234            ProcessIntegrand::Amplitude(integrand) => {
1235                update_event_processing_runtime(integrand, iter)
1236            }
1237            ProcessIntegrand::CrossSection(integrand) => {
1238                update_event_processing_runtime(integrand, iter)
1239            }
1240        }
1241    }
1242
1243    pub fn observable_accumulator_bundle(&self) -> Option<ObservableAccumulatorBundle> {
1244        match self {
1245            ProcessIntegrand::Amplitude(integrand) => observable_accumulator_bundle(integrand),
1246            ProcessIntegrand::CrossSection(integrand) => observable_accumulator_bundle(integrand),
1247        }
1248    }
1249
1250    pub fn has_observables(&self) -> bool {
1251        match self {
1252            ProcessIntegrand::Amplitude(integrand) => integrand
1253                .event_processing_runtime()
1254                .is_some_and(EventProcessingRuntime::has_observables),
1255            ProcessIntegrand::CrossSection(integrand) => integrand
1256                .event_processing_runtime()
1257                .is_some_and(EventProcessingRuntime::has_observables),
1258        }
1259    }
1260
1261    pub fn observable_snapshot_bundle(&self) -> Option<ObservableSnapshotBundle> {
1262        match self {
1263            ProcessIntegrand::Amplitude(integrand) => observable_snapshot_bundle(integrand),
1264            ProcessIntegrand::CrossSection(integrand) => observable_snapshot_bundle(integrand),
1265        }
1266    }
1267
1268    pub fn build_observable_snapshots_for_result(
1269        &self,
1270        result: &EvaluationResult,
1271    ) -> Option<ObservableSnapshotBundle> {
1272        match self {
1273            ProcessIntegrand::Amplitude(integrand) => {
1274                build_observable_snapshots_for_result(integrand, result)
1275            }
1276            ProcessIntegrand::CrossSection(integrand) => {
1277                build_observable_snapshots_for_result(integrand, result)
1278            }
1279        }
1280    }
1281
1282    pub fn build_observable_snapshots_for_precise_result(
1283        &self,
1284        result: &PreciseEvaluationResult,
1285    ) -> Option<ObservableSnapshotBundle> {
1286        match self {
1287            ProcessIntegrand::Amplitude(integrand) => match result {
1288                PreciseEvaluationResult::Double(result) => {
1289                    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1290                }
1291                PreciseEvaluationResult::Quad(result) => {
1292                    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1293                }
1294                PreciseEvaluationResult::Arb(result) => {
1295                    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1296                }
1297            },
1298            ProcessIntegrand::CrossSection(integrand) => match result {
1299                PreciseEvaluationResult::Double(result) => {
1300                    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1301                }
1302                PreciseEvaluationResult::Quad(result) => {
1303                    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1304                }
1305                PreciseEvaluationResult::Arb(result) => {
1306                    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1307                }
1308            },
1309        }
1310    }
1311
1312    pub fn write_observable_snapshots(
1313        &self,
1314        path: impl AsRef<Path>,
1315        format: ObservableFileFormat,
1316    ) -> Result<()> {
1317        let Some(bundle) = self.observable_snapshot_bundle() else {
1318            return Ok(());
1319        };
1320
1321        write_observable_snapshot_bundle(&bundle, path.as_ref(), format)
1322    }
1323
1324    pub fn restore_observable_snapshot_bundle(
1325        &mut self,
1326        bundle: &ObservableSnapshotBundle,
1327    ) -> Result<()> {
1328        match self {
1329            ProcessIntegrand::Amplitude(integrand) => {
1330                restore_observable_snapshot_bundle(integrand, bundle)
1331            }
1332            ProcessIntegrand::CrossSection(integrand) => {
1333                restore_observable_snapshot_bundle(integrand, bundle)
1334            }
1335        }
1336    }
1337}
1338
1339fn format_orientation_label(signature: &EdgeVec<Orientation>) -> String {
1340    signature
1341        .iter()
1342        .map(|(_, orientation)| match *orientation {
1343            Orientation::Default => '+',
1344            Orientation::Reversed => '-',
1345            Orientation::Undirected => '0',
1346        })
1347        .collect()
1348}
1349
1350pub(crate) fn resolve_visible_orientation_id(
1351    orientation_filter: &SubSet<OrientationID>,
1352    visible_orientation_id: usize,
1353) -> Option<OrientationID> {
1354    if orientation_filter.is_full() {
1355        Some(OrientationID::from(visible_orientation_id))
1356    } else {
1357        orientation_filter
1358            .included_iter()
1359            .nth(visible_orientation_id)
1360    }
1361}
1362
1363pub(crate) fn filtered_orientation_count(
1364    orientation_filter: &SubSet<OrientationID>,
1365    orientations: &TiVec<OrientationID, EdgeVec<Orientation>>,
1366) -> usize {
1367    if orientation_filter.is_full() {
1368        orientations.len()
1369    } else {
1370        orientation_filter.included_iter().count()
1371    }
1372}
1373
1374pub(crate) fn orientation_labels_for_graph<I: ProcessIntegrandImpl>(
1375    integrand: &I,
1376    graph_id: usize,
1377) -> Result<Vec<String>> {
1378    if graph_id >= integrand.graph_count() {
1379        return Err(eyre!(
1380            "Unknown graph '{}' while resolving orientation labels.",
1381            graph_id
1382        ));
1383    }
1384    let graph = integrand.get_graph(graph_id);
1385    Ok((0..graph.get_num_orientations())
1386        .map(|orientation_id| {
1387            graph
1388                .orientation_label(orientation_id)
1389                .unwrap_or_else(|| format!("#{}", orientation_id))
1390        })
1391        .collect())
1392}
1393
1394#[allow(clippy::too_many_arguments)]
1395pub(crate) fn evaluate_profile_momentum_point_precise<I: ProcessIntegrandImpl>(
1396    integrand: &mut I,
1397    model: &Model,
1398    graph_id: usize,
1399    orientation: Option<usize>,
1400    loop_momenta: Vec<ThreeMomentum<F<f64>>>,
1401    candidate_lmb: Option<&LoopMomentumBasis>,
1402    use_arb_prec: bool,
1403    weight: &F<ArbPrec>,
1404) -> Result<PreciseEvaluationResult> {
1405    let input = MomentumSpaceEvaluationInput {
1406        loop_momenta,
1407        integrator_weight: F(1.0),
1408        graph_id: Some(graph_id),
1409        group_id: None,
1410        orientation,
1411        channel_id: None,
1412    };
1413    evaluate_from_source_precise_with_estimate(
1414        integrand,
1415        EvaluationTarget::Physical(model),
1416        EvaluationSource::Momentum {
1417            input: &input,
1418            candidate_lmb,
1419        },
1420        F(1.0),
1421        use_arb_prec,
1422        Complex::new_re(F(100.0 * integrand.get_settings().kinematics.e_cm)),
1423        None,
1424        Some(weight),
1425    )
1426}
1427
1428fn format_sampling_channel_label(edge_ids: &[usize]) -> String {
1429    let mut sorted = edge_ids.to_vec();
1430    sorted.sort_unstable();
1431    format!(
1432        "({})",
1433        sorted
1434            .iter()
1435            .map(|edge_id| edge_id.to_string())
1436            .collect_vec()
1437            .join(",")
1438    )
1439}
1440
1441pub(crate) fn histogram_process_info_for_integrand<I: ProcessIntegrandImpl>(
1442    integrand: &I,
1443) -> Result<HistogramProcessInfo> {
1444    let parameterization_settings = integrand
1445        .get_settings()
1446        .sampling
1447        .get_parameterization_settings()
1448        .unwrap_or_default();
1449    let graph_names = (0..integrand.graph_count())
1450        .map(|graph_id| integrand.get_graph(graph_id).name())
1451        .collect_vec();
1452    let graph_to_group_id = (0..integrand.graph_count())
1453        .map(|graph_id| {
1454            integrand
1455                .graph_group_id_for_graph(graph_id)
1456                .unwrap_or_else(|| {
1457                    panic!(
1458                        "graph {} is missing a graph-group mapping for histogram process info",
1459                        graph_id
1460                    )
1461                })
1462        })
1463        .collect_vec();
1464    let graph_group_master_names = integrand
1465        .get_group_structure()
1466        .iter_enumerated()
1467        .map(|(group_id, _)| integrand.get_master_graph(group_id).name())
1468        .collect_vec();
1469    let orientation_labels_by_group = integrand
1470        .get_group_structure()
1471        .iter_enumerated()
1472        .map(|(group_id, _)| {
1473            let master = integrand.get_master_graph(group_id);
1474            (0..master.get_num_orientations())
1475                .map(|orientation_id| {
1476                    master
1477                        .orientation_label(orientation_id)
1478                        .unwrap_or_else(|| format!("#{}", orientation_id))
1479                })
1480                .collect_vec()
1481        })
1482        .collect_vec();
1483    let sampling_channel_labels_by_group = integrand
1484        .get_group_structure()
1485        .iter_enumerated()
1486        .map(|(group_id, _)| {
1487            let master = integrand.get_master_graph(group_id);
1488            let channel_ids = master.sampling_channel_ids(&parameterization_settings)?;
1489            channel_ids
1490                .into_iter()
1491                .map(|channel_id| {
1492                    Ok(master
1493                        .sampling_channel_label(channel_id, &parameterization_settings)?
1494                        .unwrap_or_else(|| format!("#{}", channel_id.index())))
1495                })
1496                .collect::<Result<Vec<_>>>()
1497        })
1498        .collect::<Result<Vec<_>>>()?;
1499    Ok(HistogramProcessInfo {
1500        graph_names,
1501        graph_to_group_id,
1502        graph_group_master_names,
1503        orientation_labels_by_group,
1504        sampling_channel_labels_by_group,
1505    })
1506}
1507
1508pub(crate) fn graph_to_group_id_for_group_structure(
1509    group_structure: &TiVec<GroupId, GraphGroup>,
1510) -> Vec<usize> {
1511    group_structure
1512        .iter_enumerated()
1513        .flat_map(|(group_id, group)| {
1514            group
1515                .into_iter()
1516                .map(move |graph_id| (graph_id, group_id.0))
1517        })
1518        .sorted_by_key(|(graph_id, _)| *graph_id)
1519        .map(|(_, group_id)| group_id)
1520        .collect_vec()
1521}
1522
1523pub(crate) struct PreparedBufferedEvent<T: FloatLike> {
1524    pub(crate) buffered_event: Option<GenericEvent<T>>,
1525    pub(crate) selectors_pass: bool,
1526    pub(crate) event_processing_time: Duration,
1527    pub(crate) generated_event_count: usize,
1528    pub(crate) accepted_event_count: usize,
1529}
1530
1531impl<T: FloatLike> Default for PreparedBufferedEvent<T> {
1532    fn default() -> Self {
1533        Self {
1534            buffered_event: None,
1535            selectors_pass: true,
1536            event_processing_time: Duration::ZERO,
1537            generated_event_count: 0,
1538            accepted_event_count: 0,
1539        }
1540    }
1541}
1542
1543pub(crate) fn prepare_buffered_event<T: FloatLike>(
1544    settings: &RuntimeSettings,
1545    rotation: &Rotation,
1546    event_processing_runtime: Option<&mut EventProcessingRuntime>,
1547    build_event: impl FnOnce() -> Result<GenericEvent<T>>,
1548) -> Result<PreparedBufferedEvent<T>> {
1549    let needs_selector_events = event_processing_runtime
1550        .as_ref()
1551        .is_some_and(|runtime| runtime.has_selectors());
1552    let should_buffer_event = rotation.is_identity() && settings.should_buffer_generated_events();
1553    let should_build_event = if rotation.is_identity() {
1554        settings.should_generate_events()
1555    } else {
1556        needs_selector_events
1557    };
1558
1559    if !should_build_event {
1560        return Ok(PreparedBufferedEvent::default());
1561    }
1562
1563    let build_start = Instant::now();
1564    let mut event = build_event()?;
1565    let mut event_processing_time = build_start.elapsed();
1566    let generated_event_count = usize::from(rotation.is_identity());
1567
1568    let selector_start = Instant::now();
1569    let selectors_pass = if let Some(runtime) = event_processing_runtime {
1570        if rotation.is_identity() {
1571            runtime.process_event(&mut event)
1572        } else {
1573            runtime.process_event_for_selectors(&mut event)
1574        }
1575    } else {
1576        true
1577    };
1578    event_processing_time += selector_start.elapsed();
1579
1580    let buffered_event = if selectors_pass && should_buffer_event {
1581        Some(event)
1582    } else {
1583        None
1584    };
1585
1586    // Count selector-accepted physical events even when they are processed only
1587    // transiently for selectors and are not retained in the returned event buffer.
1588    let accepted_event_count = usize::from(rotation.is_identity() && selectors_pass);
1589
1590    Ok(PreparedBufferedEvent {
1591        accepted_event_count,
1592        buffered_event,
1593        selectors_pass,
1594        event_processing_time,
1595        generated_event_count,
1596    })
1597}
1598
1599fn process_evaluation_result_runtime<I: ProcessIntegrandImpl>(
1600    integrand: &mut I,
1601    result: &EvaluationResult,
1602) {
1603    if let Some(runtime) = integrand.event_processing_runtime_mut()
1604        && runtime.has_observables()
1605    {
1606        runtime.process_event_groups(&result.event_groups);
1607    }
1608}
1609
1610fn maybe_discard_generated_events_in_result(
1611    settings: &RuntimeSettings,
1612    result: &mut EvaluationResult,
1613) {
1614    if !settings.should_return_generated_events() {
1615        result.event_groups.clear();
1616    }
1617}
1618
1619fn merge_event_processing_runtime<I: ProcessIntegrandImpl>(
1620    integrand: &mut I,
1621    other: &mut I,
1622) -> Result<()> {
1623    match (
1624        integrand.event_processing_runtime_mut(),
1625        other.event_processing_runtime_mut(),
1626    ) {
1627        (Some(lhs), Some(rhs)) => lhs.merge_samples(rhs),
1628        _ => Ok(()),
1629    }
1630}
1631
1632fn update_event_processing_runtime<I: ProcessIntegrandImpl>(integrand: &mut I, iter: usize) {
1633    if let Some(runtime) = integrand.event_processing_runtime_mut() {
1634        runtime.update_results(iter);
1635    }
1636}
1637
1638fn observable_accumulator_bundle<I: ProcessIntegrandImpl>(
1639    integrand: &I,
1640) -> Option<ObservableAccumulatorBundle> {
1641    integrand
1642        .event_processing_runtime()
1643        .filter(|runtime| runtime.has_observables())
1644        .map(EventProcessingRuntime::accumulator_bundle)
1645}
1646
1647fn observable_snapshot_bundle<I: ProcessIntegrandImpl>(
1648    integrand: &I,
1649) -> Option<ObservableSnapshotBundle> {
1650    integrand
1651        .event_processing_runtime()
1652        .filter(|runtime| runtime.has_observables())
1653        .map(EventProcessingRuntime::snapshot_bundle)
1654}
1655
1656fn build_observable_snapshots_for_result<I: ProcessIntegrandImpl>(
1657    integrand: &I,
1658    result: &EvaluationResult,
1659) -> Option<ObservableSnapshotBundle> {
1660    build_observable_snapshots_for_event_groups(integrand, &result.event_groups)
1661}
1662
1663fn build_observable_snapshots_for_event_groups<I: ProcessIntegrandImpl, T: FloatLike>(
1664    integrand: &I,
1665    event_groups: &crate::observables::GenericEventGroupList<T>,
1666) -> Option<ObservableSnapshotBundle> {
1667    let runtime = integrand.event_processing_runtime()?;
1668    if !runtime.has_observables() {
1669        return None;
1670    }
1671
1672    let mut runtime = runtime.cleared_observable_clone();
1673    runtime.process_event_groups(event_groups);
1674    Some(runtime.snapshot_bundle())
1675}
1676
1677fn restore_observable_snapshot_bundle<I: ProcessIntegrandImpl>(
1678    integrand: &mut I,
1679    bundle: &ObservableSnapshotBundle,
1680) -> Result<()> {
1681    let runtime = integrand.event_processing_runtime_mut().ok_or_else(|| {
1682        eyre!("Cannot restore observables before the integrand has been warmed up")
1683    })?;
1684    if !runtime.has_observables() {
1685        return Err(eyre!(
1686            "Cannot restore observable snapshots for an integrand without configured observables"
1687        ));
1688    }
1689    runtime.restore_snapshot_bundle(bundle)
1690}
1691
1692fn full_event_multiplicative_factor_precise<T: FloatLike>(
1693    parameterization_jacobian: Option<F<T>>,
1694    integrator_weight: F<T>,
1695) -> Complex<F<T>> {
1696    let jacobian = parameterization_jacobian.unwrap_or_else(|| integrator_weight.one());
1697    Complex::new_re(jacobian * integrator_weight)
1698}
1699
1700pub(super) fn apply_full_event_multiplicative_factor_precise<T: FloatLike>(
1701    event_groups: &mut crate::observables::GenericEventGroupList<T>,
1702    full_factor: &Complex<F<T>>,
1703) {
1704    for event_group in event_groups.iter_mut() {
1705        for event in event_group.iter_mut() {
1706            event.apply_multiplicative_factor(full_factor);
1707
1708            if !event.additional_weights.weights.is_empty() {
1709                event
1710                    .additional_weights
1711                    .weights
1712                    .entry(AdditionalWeightKey::FullMultiplicativeFactor)
1713                    .and_modify(|value| *value *= full_factor.clone())
1714                    .or_insert_with(|| full_factor.clone());
1715            }
1716        }
1717    }
1718}
1719
1720pub(crate) fn write_observable_snapshot_bundle(
1721    bundle: &ObservableSnapshotBundle,
1722    path: &Path,
1723    format: ObservableFileFormat,
1724) -> Result<()> {
1725    match format {
1726        ObservableFileFormat::None => Ok(()),
1727        ObservableFileFormat::Hwu => bundle.write_hwu_file(path),
1728        ObservableFileFormat::Json => bundle.to_json_file(path),
1729    }
1730}
1731
1732#[derive(Debug, Clone, Copy)]
1733pub enum IntegrandType {
1734    Amplitude,
1735    CrossSection,
1736}
1737
1738fn create_stability_iterator(
1739    settings: &StabilitySettings,
1740    use_arb_prec: bool,
1741) -> Vec<StabilityLevelSetting> {
1742    if use_arb_prec {
1743        // Force arbitrary precision, reusing the configured Arb thresholds when available.
1744        if let Some(arb_settings_position) = settings
1745            .levels
1746            .iter()
1747            .position(|stability_level_setting| stability_level_setting.precision == Precision::Arb)
1748        {
1749            vec![settings.levels[arb_settings_position]]
1750        } else {
1751            vec![StabilityLevelSetting::default_arb()]
1752        }
1753    } else {
1754        settings.levels.clone()
1755    }
1756}
1757
1758#[inline]
1759fn complex_from_f64<T: FloatLike>(value: &Complex<F<f64>>) -> Complex<F<T>> {
1760    Complex::new(F::<T>::from_ff64(value.re), F::<T>::from_ff64(value.im))
1761}
1762
1763#[inline]
1764fn complex_to_f64<T: FloatLike>(value: &Complex<F<T>>) -> Complex<F<f64>> {
1765    Complex::new(value.re.into_ff64(), value.im.into_ff64())
1766}
1767
1768type StabilityCheckResult<T> = (
1769    Complex<F<T>>,
1770    Option<F<T>>,
1771    bool,
1772    Option<StabilityFailureReason>,
1773);
1774
1775#[inline]
1776#[cfg(test)]
1777fn stability_check<T: FloatLike>(
1778    ecm_scale: Option<&F<T>>,
1779    results: &[Complex<F<T>>],
1780    stability_settings: &StabilityLevelSetting,
1781    max_eval: Complex<F<T>>,
1782    wgt: F<T>,
1783    is_final_level: bool,
1784    escalate_if_exact_zero: bool,
1785) -> StabilityCheckResult<T> {
1786    stability_check_with_estimate(
1787        ecm_scale,
1788        results,
1789        stability_settings,
1790        max_eval,
1791        wgt,
1792        is_final_level,
1793        escalate_if_exact_zero,
1794        None,
1795        0.0,
1796    )
1797}
1798
1799#[inline]
1800#[allow(clippy::too_many_arguments)]
1801fn stability_check_with_estimate<T: FloatLike>(
1802    ecm_scale: Option<&F<T>>,
1803    results: &[Complex<F<T>>],
1804    stability_settings: &StabilityLevelSetting,
1805    max_eval: Complex<F<T>>,
1806    wgt: F<T>,
1807    is_final_level: bool,
1808    escalate_if_exact_zero: bool,
1809    integral_estimate: Option<(f64, f64)>,
1810    min_abs_wgt_for_escalation: f64,
1811) -> StabilityCheckResult<T> {
1812    stability_check_components_with_estimate(
1813        ecm_scale,
1814        results,
1815        stability_settings,
1816        max_eval,
1817        wgt,
1818        is_final_level,
1819        escalate_if_exact_zero,
1820        true,
1821        true,
1822        integral_estimate,
1823        min_abs_wgt_for_escalation,
1824    )
1825}
1826
1827#[inline]
1828#[cfg(test)]
1829#[allow(clippy::too_many_arguments)]
1830fn stability_check_components<T: FloatLike>(
1831    ecm_scale: Option<&F<T>>,
1832    results: &[Complex<F<T>>],
1833    stability_settings: &StabilityLevelSetting,
1834    max_eval: Complex<F<T>>,
1835    wgt: F<T>,
1836    is_final_level: bool,
1837    escalate_if_exact_zero: bool,
1838    check_real: bool,
1839    check_imag: bool,
1840) -> StabilityCheckResult<T> {
1841    stability_check_components_with_estimate(
1842        ecm_scale,
1843        results,
1844        stability_settings,
1845        max_eval,
1846        wgt,
1847        is_final_level,
1848        escalate_if_exact_zero,
1849        check_real,
1850        check_imag,
1851        None,
1852        0.0,
1853    )
1854}
1855
1856#[inline]
1857#[allow(clippy::too_many_arguments)]
1858fn stability_check_components_with_estimate<T: FloatLike>(
1859    ecm_scale: Option<&F<T>>,
1860    results: &[Complex<F<T>>],
1861    stability_settings: &StabilityLevelSetting,
1862    max_eval: Complex<F<T>>,
1863    wgt: F<T>,
1864    is_final_level: bool,
1865    escalate_if_exact_zero: bool,
1866    check_real: bool,
1867    check_imag: bool,
1868    integral_estimate: Option<(f64, f64)>,
1869    min_abs_wgt_for_escalation: f64,
1870) -> StabilityCheckResult<T> {
1871    // Nonfinite probes cannot establish stability, even at the final precision.
1872    if results.iter().any(|result| {
1873        (check_real && (result.re.is_nan() || result.re.is_infinite()))
1874            || (check_imag && (result.im.is_nan() || result.im.is_infinite()))
1875    }) {
1876        return (
1877            results[0].clone(),
1878            None,
1879            false,
1880            Some(StabilityFailureReason::ErrorThreshold),
1881        );
1882    }
1883
1884    if results.len() == 1 {
1885        return (results[0].clone(), None, true, None);
1886    }
1887
1888    let average = results
1889        .iter()
1890        .skip(1)
1891        .fold(results[0].clone(), |acc, x| acc + x)
1892        / F::<T>::from_f64(results.len() as f64);
1893
1894    // Bound the reported rotation average without cancellation, applying the
1895    // remaining weight before any normal-range test. Native values and their
1896    // measured relative errors remain intact; only suppressed final components
1897    // may waive a relative-error failure at binary64's normal boundary.
1898    let weighted_absolute_average = results
1899        .iter()
1900        .fold(Complex::new_re(average.re.zero()), |sum, result| {
1901            sum + Complex::new((&result.re * &wgt).abs(), (&result.im * &wgt).abs())
1902        })
1903        / F::<T>::from_f64(results.len() as f64);
1904    let minimum_normal = F::<T>::from_f64(f64::MIN_POSITIVE);
1905    let real_underflow = check_real
1906        && weighted_absolute_average.re.0.is_finite()
1907        && weighted_absolute_average.re < minimum_normal;
1908    let imag_underflow = check_imag
1909        && weighted_absolute_average.im.0.is_finite()
1910        && weighted_absolute_average.im < minimum_normal;
1911
1912    let errors = results
1913        .iter()
1914        .map(|res| {
1915            let error_re =
1916                if !check_real || (IsZero::is_zero(&res.re) && IsZero::is_zero(&average.re)) {
1917                    F::<T>::from_f64(0.0)
1918                } else {
1919                    ((&res.re - &average.re) / &average.re).abs()
1920                };
1921            let error_im =
1922                if !check_imag || (IsZero::is_zero(&res.im) && IsZero::is_zero(&average.im)) {
1923                    F::<T>::from_f64(0.0)
1924                } else {
1925                    ((&res.im - &average.im) / &average.im).abs()
1926                };
1927            Complex::new(error_re, error_im)
1928        })
1929        .collect::<Vec<_>>();
1930    let estimated_relative_accuracy = errors.iter().fold(average.re.zero(), |max, error| {
1931        max.max(error.re.clone().max(error.im.clone()))
1932    });
1933
1934    // Do not spend higher precision on a point whose complete weighted probe
1935    // contribution is negligible compared with a well-resolved integral. The
1936    // estimate is supplied by the previous integration iteration and is
1937    // accepted only when its standard error is at most ten percent. The
1938    // integrator supplies the corresponding absolute-component estimate.
1939    // A discrepancy below 1e-2 is treated as insufficient evidence that the
1940    // order of magnitude is reliable, so it must not permit this waiver.
1941    let small_weight_waiver = |max_weight: F<T>| {
1942        min_abs_wgt_for_escalation > 0.0
1943            && !is_final_level
1944            && integral_estimate.is_some_and(|(estimate, error)| {
1945                estimate.is_finite()
1946                    && error.is_finite()
1947                    && error >= 0.0
1948                    && estimate != 0.0
1949                    && error <= 0.10 * estimate.abs()
1950                    && estimated_relative_accuracy >= F::<T>::from_f64(1.0e-2)
1951                    && max_weight.0.is_finite()
1952                    && max_weight < F::<T>::from_f64(min_abs_wgt_for_escalation * estimate.abs())
1953            })
1954    };
1955    let current_max_weight = results
1956        .iter()
1957        .fold(wgt.abs() * average.re.zero(), |max, result| {
1958            let magnitude = if check_real && check_imag {
1959                result.re.abs().max(result.im.abs())
1960            } else if check_real {
1961                result.re.abs()
1962            } else if check_imag {
1963                result.im.abs()
1964            } else {
1965                result.re.zero()
1966            } * wgt.abs();
1967            max.max(magnitude)
1968        });
1969    let small_weight_waiver = small_weight_waiver(current_max_weight);
1970
1971    let mut unstable_reason = None;
1972    let mut unstable_sample = None;
1973    for (index, error) in errors.into_iter().enumerate() {
1974        if !is_final_level
1975            && escalate_if_exact_zero
1976            && !small_weight_waiver
1977            && (!check_real || error.re == F::<T>::from_f64(0.0))
1978            && (!check_imag || error.im == F::<T>::from_f64(0.0))
1979        {
1980            unstable_reason = Some(StabilityFailureReason::ZeroError);
1981            unstable_sample = Some(index);
1982            break;
1983        }
1984
1985        // The optional allowance is dimensionless relative to E_cm, after
1986        // every sample factor. Preserve the measured relative discrepancy;
1987        // another component's scale must not supply the allowance.
1988        let absolute_error = Complex::new(
1989            ((&results[index].re - &average.re) * &wgt).abs(),
1990            ((&results[index].im - &average.im) * &wgt).abs(),
1991        );
1992        let real_absolute = stability_settings.ecm_relative_tolerance_for_re > 0.0
1993            && wgt.0.is_finite()
1994            && weighted_absolute_average.re.0.is_finite()
1995            && absolute_error.re.0.is_finite()
1996            && ecm_scale.is_some_and(|scale| {
1997                scale.0.is_finite()
1998                    && scale > &scale.zero()
1999                    && &absolute_error.re / scale
2000                        <= F::<T>::from_f64(stability_settings.ecm_relative_tolerance_for_re)
2001            });
2002        let imag_absolute = stability_settings.ecm_relative_tolerance_for_im > 0.0
2003            && wgt.0.is_finite()
2004            && weighted_absolute_average.im.0.is_finite()
2005            && absolute_error.im.0.is_finite()
2006            && ecm_scale.is_some_and(|scale| {
2007                scale.0.is_finite()
2008                    && scale > &scale.zero()
2009                    && &absolute_error.im / scale
2010                        <= F::<T>::from_f64(stability_settings.ecm_relative_tolerance_for_im)
2011            });
2012        if (check_real
2013            && error.re > F::<T>::from_f64(stability_settings.required_precision_for_re)
2014            && !real_underflow
2015            && !real_absolute
2016            && !small_weight_waiver)
2017            || (check_imag
2018                && error.im > F::<T>::from_f64(stability_settings.required_precision_for_im)
2019                && !imag_underflow
2020                && !imag_absolute
2021                && !small_weight_waiver)
2022        {
2023            unstable_reason = Some(StabilityFailureReason::ErrorThreshold);
2024            unstable_sample = Some(index);
2025            break;
2026        }
2027    }
2028
2029    if let Some(unstable_index) = unstable_sample {
2030        let unstable_point = &results[unstable_index];
2031
2032        let ((real_formatted, rotated_real_formatted), (imag_formatted, rotated_imag_formatted)) = (
2033            format_for_compare_digits(
2034                average.re.clone().into_ff64(),
2035                unstable_point.re.clone().into_ff64(),
2036            ),
2037            format_for_compare_digits(
2038                average.im.clone().into_ff64(),
2039                unstable_point.im.clone().into_ff64(),
2040            ),
2041        );
2042
2043        debug!("{}", "\nUnstable point detected:".red());
2044        debug!("\taverage result: {} + {}i", real_formatted, imag_formatted,);
2045        debug!(
2046            "\trotated result: {} + {}i",
2047            rotated_real_formatted, rotated_imag_formatted,
2048        );
2049    }
2050
2051    let stable = unstable_sample.is_none();
2052
2053    let has_active_max = (check_real && !IsZero::is_zero(&max_eval.re))
2054        || (check_imag && !IsZero::is_zero(&max_eval.im));
2055    let below_wgt_threshold =
2056        if stability_settings.escalate_for_large_weight_threshold > 0. && has_active_max {
2057            (check_real
2058                && average.re.abs() * wgt.clone()
2059                    < F::<T>::from_f64(stability_settings.escalate_for_large_weight_threshold)
2060                        * max_eval.re)
2061                || (check_imag
2062                    && average.im.abs() * wgt
2063                        < F::<T>::from_f64(stability_settings.escalate_for_large_weight_threshold)
2064                            * max_eval.im)
2065        } else {
2066            true
2067        };
2068
2069    let weight_reason = if stable && !below_wgt_threshold && !small_weight_waiver {
2070        Some(StabilityFailureReason::WeightThreshold)
2071    } else {
2072        None
2073    };
2074
2075    (
2076        average,
2077        Some(estimated_relative_accuracy),
2078        stable && (below_wgt_threshold || small_weight_waiver),
2079        unstable_reason.or(weight_reason),
2080    )
2081}
2082
2083#[inline]
2084#[cfg(test)]
2085fn stability_check_on_norm<T: FloatLike>(
2086    ecm_scale: Option<&F<T>>,
2087    results: &[Complex<F<T>>],
2088    stability_settings: &StabilityLevelSetting,
2089    max_eval: Complex<F<T>>,
2090    wgt: F<T>,
2091    is_final_level: bool,
2092    escalate_if_exact_zero: bool,
2093) -> StabilityCheckResult<T> {
2094    stability_check_on_norm_components_with_estimate(
2095        ecm_scale,
2096        results,
2097        stability_settings,
2098        max_eval,
2099        wgt,
2100        is_final_level,
2101        escalate_if_exact_zero,
2102        true,
2103        true,
2104        None,
2105        0.0,
2106    )
2107}
2108
2109#[inline]
2110#[allow(clippy::too_many_arguments)]
2111fn stability_check_on_norm_components_with_estimate<T: FloatLike>(
2112    ecm_scale: Option<&F<T>>,
2113    results: &[Complex<F<T>>],
2114    stability_settings: &StabilityLevelSetting,
2115    max_eval: Complex<F<T>>,
2116    wgt: F<T>,
2117    is_final_level: bool,
2118    escalate_if_exact_zero: bool,
2119    check_real: bool,
2120    check_imag: bool,
2121    integral_estimate: Option<(f64, f64)>,
2122    min_abs_wgt_for_escalation: f64,
2123) -> StabilityCheckResult<T> {
2124    // Nonfinite probes cannot establish stability, even at the final precision.
2125    if results.iter().any(|result| {
2126        (check_real && (result.re.is_nan() || result.re.is_infinite()))
2127            || (check_imag && (result.im.is_nan() || result.im.is_infinite()))
2128    }) {
2129        return (
2130            results[0].clone(),
2131            None,
2132            false,
2133            Some(StabilityFailureReason::ErrorThreshold),
2134        );
2135    }
2136
2137    if results.len() == 1 {
2138        return (results[0].clone(), None, true, None);
2139    }
2140
2141    let component_magnitude = |x: &Complex<F<T>>| match (check_real, check_imag) {
2142        (true, false) => x.re.abs(),
2143        (false, true) => x.im.abs(),
2144        (true, true) => x.re.hypot(&x.im),
2145        (false, false) => x.re.zero(),
2146    };
2147    let magnitudes = results.iter().map(component_magnitude).collect_vec();
2148    let count = F::<T>::from_f64(results.len() as f64);
2149    // Divide before summing so finite near-MAX probes have a finite mean.
2150    // A true norm overflow must trigger precision rescue, never a NaN accuracy
2151    // whose comparison with the tolerance would silently accept the sample.
2152    let average = magnitudes
2153        .iter()
2154        .fold(F::<T>::from_f64(0.0), |acc, x| acc + x / &count);
2155    if !average.0.is_finite() || magnitudes.iter().any(|value| !value.0.is_finite()) {
2156        return (
2157            results[0].clone(),
2158            None,
2159            false,
2160            Some(StabilityFailureReason::ErrorThreshold),
2161        );
2162    }
2163
2164    // The norm owner returns the primary probe, so bound it as well as the
2165    // average. Scaled component magnitudes avoid squaring tiny weighted values.
2166    let weighted_magnitude = |result: &Complex<F<T>>| (component_magnitude(result) * &wgt).abs();
2167    let weighted_absolute_average = results.iter().fold(average.zero(), |sum, result| {
2168        sum + weighted_magnitude(result) / &count
2169    });
2170    let primary_magnitude = weighted_magnitude(&results[0]);
2171    let minimum_normal = F::<T>::from_f64(f64::MIN_POSITIVE);
2172    let underflow = weighted_absolute_average.0.is_finite()
2173        && primary_magnitude.0.is_finite()
2174        && weighted_absolute_average < minimum_normal
2175        && primary_magnitude < minimum_normal;
2176
2177    // The E_cm-relative allowance must bound every component against the
2178    // primary value returned by this owner, independently of its norm.
2179    let absolute_agreement = (stability_settings.ecm_relative_tolerance_for_re > 0.0
2180        || stability_settings.ecm_relative_tolerance_for_im > 0.0)
2181        && wgt.0.is_finite()
2182        && weighted_absolute_average.0.is_finite()
2183        && ecm_scale.is_some_and(|scale| scale.0.is_finite() && scale > &scale.zero())
2184        && results.iter().all(|result| {
2185            let error_re = ((&result.re - &results[0].re) * &wgt).abs();
2186            let error_im = ((&result.im - &results[0].im) * &wgt).abs();
2187            (!check_real
2188                || (error_re.0.is_finite()
2189                    && error_re / ecm_scale.unwrap()
2190                        <= F::<T>::from_f64(stability_settings.ecm_relative_tolerance_for_re)))
2191                && (!check_imag
2192                    || (error_im.0.is_finite()
2193                        && error_im / ecm_scale.unwrap()
2194                            <= F::<T>::from_f64(stability_settings.ecm_relative_tolerance_for_im)))
2195        });
2196
2197    let errors = magnitudes
2198        .iter()
2199        .map(|res| {
2200            if IsZero::is_zero(res) && IsZero::is_zero(&average) {
2201                (F::<T>::from_f64(0.0), true) // true zero is fishy -> upgrade to next precision
2202            } else {
2203                (((res - &average) / &average).abs(), false)
2204            }
2205        })
2206        .collect::<Vec<_>>();
2207    if errors.iter().any(|(error, _)| !error.0.is_finite()) {
2208        return (
2209            results[0].clone(),
2210            None,
2211            false,
2212            Some(StabilityFailureReason::ErrorThreshold),
2213        );
2214    }
2215    let estimated_relative_accuracy = errors
2216        .iter()
2217        .fold(average.zero(), |max, (error, _)| max.max(error.clone()));
2218
2219    // Low probe discrepancy cannot establish the order of magnitude.
2220    let small_weight_waiver = |max_weight: F<T>| {
2221        min_abs_wgt_for_escalation > 0.0
2222            && !is_final_level
2223            && integral_estimate.is_some_and(|(estimate, error)| {
2224                estimate.is_finite()
2225                    && error.is_finite()
2226                    && error >= 0.0
2227                    && estimate != 0.0
2228                    && error <= 0.10 * estimate.abs()
2229                    && estimated_relative_accuracy >= F::<T>::from_f64(1.0e-2)
2230                    && max_weight.0.is_finite()
2231                    && max_weight < F::<T>::from_f64(min_abs_wgt_for_escalation * estimate.abs())
2232            })
2233    };
2234    let current_max_weight = results.iter().fold(average.zero(), |max, result| {
2235        max.max(weighted_magnitude(result).abs())
2236    });
2237    let small_weight_waiver = small_weight_waiver(current_max_weight);
2238
2239    let mut unstable_reason = None;
2240    let mut unstable_sample = None;
2241    for (index, (error, result_is_exact_zero)) in errors.into_iter().enumerate() {
2242        if !is_final_level
2243            && error == F::<T>::from_f64(0.0)
2244            && result_is_exact_zero
2245            && escalate_if_exact_zero
2246            && !small_weight_waiver
2247        {
2248            unstable_reason = Some(StabilityFailureReason::ZeroError);
2249            unstable_sample = Some(index);
2250            break;
2251        }
2252
2253        if error > F::<T>::from_f64(stability_settings.required_precision_for_re)
2254            && !underflow
2255            && !absolute_agreement
2256            && !small_weight_waiver
2257        {
2258            unstable_reason = Some(StabilityFailureReason::ErrorThreshold);
2259            unstable_sample = Some(index);
2260            break;
2261        }
2262    }
2263
2264    if let Some(unstable_index) = unstable_sample {
2265        let unstable_point = &results[unstable_index];
2266
2267        let (real_formatted, rotated_real_formatted) = format_for_compare_digits(
2268            average.clone().into_ff64(),
2269            unstable_point.re.clone().into_ff64(),
2270        );
2271
2272        debug!("{}", "\nUnstable point detected:".red());
2273        debug!("\tnormed average result: {}", real_formatted,);
2274        debug!("\tnormed rotated result: {}", rotated_real_formatted,);
2275    }
2276
2277    let stable = unstable_sample.is_none();
2278
2279    let max_magnitude = component_magnitude(&max_eval);
2280    let below_wgt_threshold = if stability_settings.escalate_for_large_weight_threshold > 0.
2281        && max_magnitude != max_magnitude.zero()
2282    {
2283        average.abs() * wgt.abs()
2284            < F::<T>::from_f64(stability_settings.escalate_for_large_weight_threshold)
2285                * max_magnitude
2286    } else {
2287        true
2288    };
2289
2290    let weight_reason = if stable && !below_wgt_threshold && !small_weight_waiver {
2291        Some(StabilityFailureReason::WeightThreshold)
2292    } else {
2293        None
2294    };
2295
2296    (
2297        results[0].clone(),
2298        Some(estimated_relative_accuracy),
2299        stable && (below_wgt_threshold || small_weight_waiver),
2300        unstable_reason.or(weight_reason),
2301    )
2302}
2303
2304#[derive(Debug, Clone)]
2305struct PreciseStabilityLevelResult<T: FloatLike> {
2306    pub result: Complex<F<T>>,
2307    pub graph_result: GraphEvaluationResult<T>,
2308    pub stability_level_used: Precision,
2309    pub estimated_relative_accuracy: Option<F<T>>,
2310    pub sample_count: usize,
2311    pub total_time: Duration,
2312    pub parameterization_jacobian: Option<F<T>>,
2313    pub is_stable: bool,
2314    pub rotated_results: Vec<RotatedEvaluation>,
2315}
2316
2317impl<T: FloatLike> PreciseStabilityLevelResult<T> {
2318    fn stability_result(&self) -> StabilityResult {
2319        // Preserve the exponent before a nonzero Arb estimate can underflow to f64 zero.
2320        let estimated_decimal_digits = self
2321            .estimated_relative_accuracy
2322            .as_ref()
2323            .filter(|value| value.is_non_zero() && !value.is_nan() && !value.is_infinite())
2324            .map(|value| (-value.abs().log10()).into_ff64());
2325        StabilityResult {
2326            precision: self.stability_level_used,
2327            estimated_relative_accuracy: self
2328                .estimated_relative_accuracy
2329                .as_ref()
2330                .map(F::into_ff64),
2331            estimated_decimal_digits,
2332            status: if self.sample_count == 0 {
2333                StabilityStatus::Unstable(0)
2334            } else {
2335                StabilityStatus::from_sample_count(self.sample_count, self.is_stable)
2336            },
2337            total_time: self.total_time,
2338        }
2339    }
2340}
2341
2342/// Helper struct for the LMB multi-channeling setup
2343#[derive(Clone, Encode, Decode)]
2344#[trait_decode(trait = GammaLoopContext)]
2345pub struct LmbMultiChannelingSetup {
2346    pub lmb_basis_ids: TiVec<SamplingChannelId, LmbIndex>,
2347    /// Canonical group-master graph used to resolve group-level channel overrides.
2348    pub graph: Graph,
2349    pub all_bases: TiVec<LmbIndex, LoopMomentumBasis>,
2350    /// Current masses of the master graph; member masses never define its partition.
2351    pub(crate) master_edge_masses: RuntimeCache<EdgeVec<Complex<F<f64>>>>,
2352    pub(crate) sampling_bridge:
2353        RuntimeCache<Result<SamplingChannelBridge, sampling::maps::SamplingEvaluationError>>,
2354    pub(crate) sampling_bridge_quad:
2355        RuntimeCache<Result<SamplingChannelBridge<f128>, sampling::maps::SamplingEvaluationError>>,
2356    pub(crate) sampling_bridge_fixed256: RuntimeCache<
2357        Result<SamplingChannelBridge<SamplingFloat>, sampling::maps::SamplingEvaluationError>,
2358    >,
2359    pub(crate) sampling_bridge_arb: RuntimeCache<
2360        Result<SamplingChannelBridge<ArbPrec>, sampling::maps::SamplingEvaluationError>,
2361    >,
2362    /// One fixed source precision and strictest accuracy budget for the whole
2363    /// integrand epoch. RuntimeCache contributes no bytes to saved states.
2364    pub(crate) sampling_source: RuntimeCache<(SamplingPrecision, f64)>,
2365    pub(crate) sampling_catalogue: RuntimeCache<SamplingChannelCatalogue>,
2366    pub(crate) sampling_programs: RuntimeCache<Vec<SamplingChannelPrograms>>,
2367}
2368
2369impl LmbMultiChannelingSetup {
2370    /// Refresh master-owned mass evaluators independently of member parameter slots.
2371    /// Keep complex values until the selected score decides whether it supports them.
2372    pub(crate) fn warm_up_masses(
2373        &mut self,
2374        settings: &RuntimeSettings,
2375        model: &Model,
2376    ) -> Result<()> {
2377        self.master_edge_masses.invalidate();
2378        let parameters = &mut self.graph.param_builder;
2379        parameters.m_uv_value(Complex::new_re(F(settings.general.m_uv)));
2380        parameters.renormalization_localization_scale_value(Complex::new_re(F(settings
2381            .general
2382            .renormalization_localization_scale)));
2383        parameters.mu_r_sq_value(Complex::new_re(F(settings.general.mu_r_sq())));
2384        parameters.numerator_sampling_scale_value(Complex::new_re(F(settings
2385            .general
2386            .numerator_sampling_scale)));
2387        parameters.update_model_values(model)?;
2388        self.master_edge_masses
2389            .set(self.graph.new_edgevec(|edge, _, _| {
2390                edge.mass_value(model, &self.graph.param_builder)
2391                    .unwrap_or_else(|| Complex::new_re(F(0.0)))
2392            }));
2393        Ok(())
2394    }
2395
2396    /// Borrow the bridge compiled in the current successful warmup epoch, or
2397    /// replay that precision's cached numerical failure into the stability loop.
2398    /// Explicit constructors remain fresh and never populate this runtime cache.
2399    pub fn sampling_bridge<T: FloatLike>(&self) -> Result<&SamplingChannelBridge<T>> {
2400        T::sampling_bridge_cache(self).as_ref().ok_or_else(|| {
2401            eyre!(
2402                "sampling bridge for graph '{}' is not initialized; call warm_up after loading or changing runtime settings, model parameters, or graph routing",
2403                self.graph.name
2404            )
2405        })?.as_ref().map_err(|error| {
2406            eyre::Report::new(error.clone()).wrap_err(format!(
2407                "sampling binding for graph '{}' at {} precision",
2408                self.graph.name, T::sampling_precision()
2409            ))
2410        })
2411    }
2412
2413    /// Invalidate all numerical bindings and their single canonical program epoch.
2414    pub(crate) fn invalidate_sampling(&mut self) {
2415        self.sampling_bridge.invalidate();
2416        self.sampling_bridge_quad.invalidate();
2417        self.sampling_bridge_fixed256.invalidate();
2418        self.sampling_bridge_arb.invalidate();
2419        self.sampling_source.invalidate();
2420        self.sampling_catalogue.invalidate();
2421        self.sampling_programs.invalidate();
2422    }
2423
2424    /// Expand a graph-resolved selection into the canonical catalogue used by
2425    /// inspection and map construction. Resolve with the actual graph name
2426    /// before invoking this method (the setup may be shared by a graph group).
2427    pub fn sampling_channel_catalogue(
2428        &self,
2429        resolved: &ResolvedSamplingChannelSelection,
2430        parameterization_settings: &ParameterizationSettings,
2431    ) -> Result<SamplingChannelCatalogue> {
2432        self.sampling_channel_catalogue_with_surfaces(resolved, parameterization_settings, &[])
2433    }
2434
2435    /// Resolve the canonical catalogue and return its read-only inspection
2436    /// view.  This is useful to CLI/Python diagnostics and intentionally does
2437    /// not compile maps, prepare kinematics, or enumerate a second channel
2438    /// domain.
2439    pub fn inspect_sampling_channels(
2440        &self,
2441        resolved: &ResolvedSamplingChannelSelection,
2442        parameterization_settings: &ParameterizationSettings,
2443    ) -> Result<SamplingChannelInspection> {
2444        Ok(self
2445            .sampling_channel_catalogue(resolved, parameterization_settings)?
2446            .inspection())
2447    }
2448
2449    /// Report the conservative full-domain and elementary soft coverage of
2450    /// the canonical catalogue.  This is an inspection operation only: it
2451    /// does not discover physical E-surfaces or construct a second channel
2452    /// enumeration.
2453    pub fn sampling_channel_coverage_report(
2454        &self,
2455        resolved: &ResolvedSamplingChannelSelection,
2456        parameterization_settings: &ParameterizationSettings,
2457    ) -> Result<SamplingCoverageReport> {
2458        let catalogue = self.sampling_channel_catalogue(resolved, parameterization_settings)?;
2459        let all_lmbs = self
2460            .all_bases
2461            .iter_enumerated()
2462            .map(|(basis_id, basis)| {
2463                (
2464                    usize::from(basis_id),
2465                    basis.loop_edges.iter().map(|edge| edge.0).collect(),
2466                )
2467            })
2468            .collect::<Vec<_>>();
2469        let massless_edges = self
2470            .graph
2471            .underlying
2472            .iter_edges()
2473            .filter_map(|(_, edge_id, edge)| edge.data.particle.is_massless().then_some(edge_id.0))
2474            .collect::<Vec<_>>();
2475        Ok(catalogue.coverage_report(&all_lmbs, &massless_edges))
2476    }
2477
2478    /// Resolve the same selection while supplying E-surface candidates already
2479    /// enumerated in the master graph frame. Surface existence and geometry are
2480    /// still prepared per cut/orientation before compilation.
2481    pub fn sampling_channel_catalogue_with_surfaces(
2482        &self,
2483        resolved: &ResolvedSamplingChannelSelection,
2484        parameterization_settings: &ParameterizationSettings,
2485        surface_edges: &[Vec<usize>],
2486    ) -> Result<SamplingChannelCatalogue> {
2487        let mut all_lmbs = self
2488            .all_bases
2489            .iter_enumerated()
2490            .map(|(basis_id, basis)| {
2491                (
2492                    usize::from(basis_id),
2493                    basis.loop_edges.iter().map(|edge| edge.0).collect(),
2494                )
2495            })
2496            .collect::<Vec<_>>();
2497        let explicit_basis_ids = parameterization_settings
2498            .lmb_basis_ids
2499            .get(&self.graph.name);
2500        let optimized_lmbs = if let Some(basis_ids) = explicit_basis_ids {
2501            for &basis_id in basis_ids {
2502                self.validate_lmb_basis_id(basis_id, &self.graph.name)?;
2503            }
2504            // Explicit basis selection is exact and ordered. Automatic soft-coverage
2505            // augmentation applies only when the user has not selected the bases.
2506            all_lmbs = basis_ids.iter().map(|&id| all_lmbs[id].clone()).collect();
2507            for selector in &resolved.selectors {
2508                if let SamplingChannelSelector::Lmb(edges) = selector
2509                    && !all_lmbs.iter().any(|(_, candidate)| candidate == edges)
2510                {
2511                    let matching_ids = self
2512                        .all_bases
2513                        .iter_enumerated()
2514                        .filter_map(|(id, basis)| {
2515                            basis
2516                                .loop_edges
2517                                .iter()
2518                                .map(|edge| edge.0)
2519                                .eq(edges.iter().copied())
2520                                .then_some(usize::from(id))
2521                        })
2522                        .collect::<Vec<_>>();
2523                    if !matching_ids.is_empty() {
2524                        return Err(SamplingSelectionError::ExcludedLmb {
2525                            graph: resolved.graph_name.clone(),
2526                            edges: edges.clone(),
2527                            basis_ids: matching_ids,
2528                            available: all_lmbs,
2529                        }
2530                        .into());
2531                    }
2532                }
2533            }
2534            basis_ids.clone()
2535        } else {
2536            self.lmb_basis_ids
2537                .iter()
2538                .map(|basis| usize::from(*basis))
2539                .collect()
2540        };
2541        let parent_lmb = self
2542            .graph
2543            .loop_momentum_basis
2544            .loop_edges
2545            .iter()
2546            .map(|edge| edge.0)
2547            .collect::<Vec<_>>();
2548        let massless_edges = self
2549            .graph
2550            .underlying
2551            .iter_edges()
2552            .filter_map(|(_, edge_id, edge)| edge.data.particle.is_massless().then_some(edge_id.0))
2553            .collect::<Vec<_>>();
2554        Ok(build_sampling_channel_catalogue_with_surfaces_and_coverage(
2555            resolved,
2556            &all_lmbs,
2557            &optimized_lmbs,
2558            surface_edges,
2559            &parent_lmb,
2560            if explicit_basis_ids.is_some() {
2561                &[]
2562            } else {
2563                &massless_edges
2564            },
2565        )?)
2566    }
2567
2568    /// Compile the selected catalogue against prepared master-graph
2569    /// kinematics. The context binds each surface's native equation and frame;
2570    /// any cut/side centers, radii and `t*` are prepared from its declared prior
2571    /// blocks inside the compiled map. This rejects missing or stale physical
2572    /// prerequisites before a mapped sample reaches the graph evaluator.
2573    pub fn compile_sampling_channels<T: FloatLike>(
2574        &self,
2575        catalogue: &SamplingChannelCatalogue,
2576        programs: &[SamplingChannelPrograms],
2577        context: &SamplingChannelCompileContext<T>,
2578    ) -> Result<Vec<CompiledSamplingChannel<T>>> {
2579        if context.master_graph != self.graph.name {
2580            return Err(eyre!(
2581                "sampling channel master graph `{}` does not match LMB setup graph `{}`",
2582                context.master_graph,
2583                self.graph.name
2584            ));
2585        }
2586        let mut context = context.clone();
2587        if let Some(masses) = self.master_edge_masses.as_ref() {
2588            context.edge_masses = masses
2589                .iter()
2590                .map(|(edge, mass)| (edge.0, mass.map_ref(|value| F::<T>::from_ff64(*value))))
2591                .collect();
2592        }
2593        catalogue.compile(&context, programs)
2594    }
2595
2596    /// Compile channels after preparing the external data needed by every
2597    /// selected-LMB-to-parent affine routing.  This is the required entry
2598    /// point when a channel catalogue contains more than the parent LMB;
2599    /// callers must pass external data from the same solved cut/orientation
2600    /// context as the map geometry.
2601    pub fn compile_sampling_channels_with_external<T: FloatLike>(
2602        &self,
2603        catalogue: &SamplingChannelCatalogue,
2604        programs: &[SamplingChannelPrograms],
2605        context: &SamplingChannelCompileContext<T>,
2606        external_momenta: &[[T; 4]],
2607    ) -> Result<Vec<CompiledSamplingChannel<T>>> {
2608        let mut context = context.clone();
2609        for (basis_id, edges) in catalogue.lmb_basis_entries() {
2610            if edges != context.parent_lmb.as_slice() {
2611                context.lmb_frame_maps.insert(
2612                    basis_id,
2613                    self.lmb_frame_map(
2614                        &self.all_bases[LmbIndex::from(basis_id)],
2615                        external_momenta,
2616                    )?,
2617                );
2618            }
2619        }
2620        for channel in catalogue.named_entries() {
2621            let edges = match &channel.map {
2622                SamplingMapDefinition::Lmb(edges) => edges,
2623                _ => &channel.definition.parent_lmb,
2624            };
2625            if edges == context.parent_lmb.as_slice() {
2626                continue;
2627            }
2628            let basis = self.sampling_parent_lmb(edges)?;
2629            context
2630                .lmb_frame_maps_by_edges
2631                .insert(edges.clone(), self.lmb_frame_map(&basis, external_momenta)?);
2632        }
2633        self.compile_sampling_channels(catalogue, programs, &context)
2634    }
2635
2636    /// Compile the selected channels and bind them to the raw-frame bridge.
2637    /// The process sampler supplies the prepared frame and external data.
2638    pub fn compile_sampling_channel_bridge<T: FloatLike>(
2639        &self,
2640        catalogue: &SamplingChannelCatalogue,
2641        programs: &[SamplingChannelPrograms],
2642        context: &SamplingChannelCompileContext<T>,
2643    ) -> Result<SamplingChannelBridge<T>> {
2644        let channels = self.compile_sampling_channels(catalogue, programs, context)?;
2645        let mode = match context.parameterization_settings.sampling_channels.weight {
2646            SamplingChannelWeight::MapDensity | SamplingChannelWeight::InverseJacobian => {
2647                SamplingPartitionMode::MapDensity
2648            }
2649            SamplingChannelWeight::Ose => SamplingPartitionMode::Ose,
2650            SamplingChannelWeight::SingularityProxy => SamplingPartitionMode::SingularityProxy,
2651        };
2652        SamplingChannelBridge::new_with_partition_mode(channels, mode).map_err(Into::into)
2653    }
2654
2655    /// External-data variant of [`Self::compile_sampling_channel_bridge`].
2656    pub fn compile_sampling_channel_bridge_with_external<T: FloatLike>(
2657        &self,
2658        catalogue: &SamplingChannelCatalogue,
2659        programs: &[SamplingChannelPrograms],
2660        context: &SamplingChannelCompileContext<T>,
2661        external_momenta: &[[T; 4]],
2662    ) -> Result<SamplingChannelBridge<T>> {
2663        let channels = self.compile_sampling_channels_with_external(
2664            catalogue,
2665            programs,
2666            context,
2667            external_momenta,
2668        )?;
2669        let mode = match context.parameterization_settings.sampling_channels.weight {
2670            SamplingChannelWeight::MapDensity | SamplingChannelWeight::InverseJacobian => {
2671                SamplingPartitionMode::MapDensity
2672            }
2673            SamplingChannelWeight::Ose => SamplingPartitionMode::Ose,
2674            SamplingChannelWeight::SingularityProxy => SamplingPartitionMode::SingularityProxy,
2675        };
2676        SamplingChannelBridge::new_with_partition_mode(channels, mode).map_err(Into::into)
2677    }
2678
2679    fn validate_lmb_basis_id(&self, basis_id: usize, graph_name: &str) -> Result<LmbIndex> {
2680        if basis_id >= self.all_bases.len() {
2681            return Err(eyre!(
2682                "Requested LMB basis id {} is out of range for graph '{}'; the graph has {} generated LMB bases.",
2683                basis_id,
2684                graph_name,
2685                self.all_bases.len()
2686            ));
2687        }
2688        Ok(LmbIndex::from(basis_id))
2689    }
2690
2691    /// Build the one canonical catalogue used by all channel-count and LMB
2692    /// lookup helpers during the runtime migration.  The old generated basis
2693    /// list is only input data for this catalogue; it is never enumerated as a
2694    /// second channel universe.
2695    pub fn canonical_sampling_catalogue(
2696        &self,
2697        graph_name: &str,
2698        parameterization_settings: &ParameterizationSettings,
2699    ) -> Result<SamplingChannelCatalogue> {
2700        let resolved = resolve_sampling_channel_selection(
2701            graph_name,
2702            &parameterization_settings.sampling_channels,
2703        )?;
2704        self.sampling_channel_catalogue(&resolved, parameterization_settings)
2705    }
2706
2707    /// Return the stable IDs from the one canonical catalogue.
2708    pub fn sampling_channel_ids(
2709        &self,
2710        graph_name: &str,
2711        parameterization_settings: &ParameterizationSettings,
2712    ) -> Result<Vec<SamplingChannelId>> {
2713        let catalogue = self.canonical_sampling_catalogue(graph_name, parameterization_settings)?;
2714        Ok((0..catalogue.entries.len())
2715            .map(SamplingChannelId::from)
2716            .collect())
2717    }
2718
2719    pub fn sampling_channel_label(
2720        &self,
2721        channel_id: SamplingChannelId,
2722        graph_name: &str,
2723        parameterization_settings: &ParameterizationSettings,
2724    ) -> Result<String> {
2725        let catalogue = self.canonical_sampling_catalogue(graph_name, parameterization_settings)?;
2726        let entry = catalogue.entries.get(channel_id.index()).ok_or_else(|| {
2727            eyre!(
2728                "Requested sampling channel {} is out of range for graph '{}'",
2729                channel_id.index(),
2730                graph_name
2731            )
2732        })?;
2733        Ok(match entry {
2734            SamplingCatalogueEntry::Lmb {
2735                basis_id, edges, ..
2736            } => {
2737                format!("lmb[{basis_id}] {:?}", edges)
2738            }
2739            SamplingCatalogueEntry::Surface { edges, .. } => format!("surface:{edges:?}"),
2740            SamplingCatalogueEntry::Named(channel) => channel.name.clone(),
2741        })
2742    }
2743
2744    pub fn sampling_channel_is_lmb(
2745        &self,
2746        channel_id: SamplingChannelId,
2747        graph_name: &str,
2748        parameterization_settings: &ParameterizationSettings,
2749    ) -> Result<bool> {
2750        let catalogue = self.canonical_sampling_catalogue(graph_name, parameterization_settings)?;
2751        let entry = catalogue.entries.get(channel_id.index()).ok_or_else(|| {
2752            eyre!(
2753                "Requested sampling channel {} is out of range for graph '{}'",
2754                channel_id.index(),
2755                graph_name
2756            )
2757        })?;
2758        Ok(matches!(entry, SamplingCatalogueEntry::Lmb { .. }))
2759    }
2760
2761    /// Return the generated LMB behind a canonical channel when that channel
2762    /// is an LMB entry. Graph-aware channels intentionally have no LMB index;
2763    /// callers that only expose LMB metadata can skip those entries without
2764    /// creating a second channel enumeration.
2765    pub fn sampling_channel_lmb_id(
2766        &self,
2767        channel_id: SamplingChannelId,
2768        graph_name: &str,
2769        parameterization_settings: &ParameterizationSettings,
2770    ) -> Result<Option<LmbIndex>> {
2771        let catalogue = self.canonical_sampling_catalogue(graph_name, parameterization_settings)?;
2772        let entry = catalogue.entries.get(channel_id.index()).ok_or_else(|| {
2773            eyre!(
2774                "Requested sampling channel {} is out of range for graph '{}'",
2775                channel_id.index(),
2776                graph_name
2777            )
2778        })?;
2779        match entry {
2780            SamplingCatalogueEntry::Lmb { basis_id, .. } => {
2781                self.validate_lmb_basis_id(*basis_id, graph_name).map(Some)
2782            }
2783            SamplingCatalogueEntry::Surface { .. } | SamplingCatalogueEntry::Named(_) => Ok(None),
2784        }
2785    }
2786
2787    /// Resolve a canonical sampling channel to its generated LMB basis.
2788    ///
2789    /// This strict accessor is intentionally named in terms of the canonical
2790    /// sampling catalogue.  It is only valid for channels whose map is an LMB;
2791    /// graph-aware channels must be evaluated through the sampling bridge.
2792    pub fn sampling_channel_lmb_basis_id(
2793        &self,
2794        channel_id: SamplingChannelId,
2795        graph_name: &str,
2796        parameterization_settings: &ParameterizationSettings,
2797    ) -> Result<LmbIndex> {
2798        self.sampling_channel_lmb_id(channel_id, graph_name, parameterization_settings)?
2799            .ok_or_else(|| {
2800                eyre!(
2801                    "Sampling channel {} for graph '{}' is graph-aware and has no generated LMB basis; use the canonical sampling bridge for this channel.",
2802                    channel_id.index(), graph_name
2803                )
2804            })
2805    }
2806
2807    pub fn sampling_channel_edge_ids(
2808        &self,
2809        channel_id: SamplingChannelId,
2810        graph_name: &str,
2811        parameterization_settings: &ParameterizationSettings,
2812    ) -> Result<SmallVec<[usize; 4]>> {
2813        let lmb_index = self
2814            .sampling_channel_lmb_id(channel_id, graph_name, parameterization_settings)?
2815            .ok_or_else(|| {
2816                eyre!(
2817                    "Sampling channel {} for graph '{}' is graph-aware and has no generated LMB basis; use the canonical sampling bridge for this channel.",
2818                    channel_id.index(), graph_name
2819                )
2820            })?;
2821        Ok(self.all_bases[lmb_index]
2822            .loop_edges
2823            .iter()
2824            .map(|edge_id| edge_id.0)
2825            .collect())
2826    }
2827
2828    pub fn selected_lmb_basis_id(
2829        &self,
2830        graph_name: &str,
2831        parameterization_settings: &ParameterizationSettings,
2832    ) -> Result<LmbIndex> {
2833        let channels = self.canonical_lmb_basis_entries(graph_name, parameterization_settings)?;
2834        channels.first().map(|(_, basis_id)| *basis_id).ok_or_else(|| {
2835            eyre!(
2836                "Could not select a default LMB basis for graph '{}'; the optimized LMB subset is empty.",
2837                graph_name
2838            )
2839        })
2840    }
2841
2842    /// Resolve generated bases for default sampling's LMB choice. IDs retain
2843    /// their catalogue positions; this view never creates another channel axis.
2844    /// Multichannel sampling uses the compiled bridge for every channel.
2845    fn canonical_lmb_basis_entries(
2846        &self,
2847        graph_name: &str,
2848        parameterization_settings: &ParameterizationSettings,
2849    ) -> Result<Vec<(SamplingChannelId, LmbIndex)>> {
2850        let catalogue = self.canonical_sampling_catalogue(graph_name, parameterization_settings)?;
2851        let mut channels = Vec::with_capacity(catalogue.entries.len());
2852        for (index, entry) in catalogue.entries.iter().enumerate() {
2853            match entry {
2854                SamplingCatalogueEntry::Lmb { basis_id, .. } => channels.push((
2855                    SamplingChannelId::from(index),
2856                    self.validate_lmb_basis_id(*basis_id, graph_name)?,
2857                )),
2858                SamplingCatalogueEntry::Surface { .. } | SamplingCatalogueEntry::Named(_) => {
2859                    return Err(eyre!(
2860                        "LMB-only weighting was requested for graph '{graph_name}', but canonical sampling channel {} is graph-aware; use the advanced discrete sampling route for this selection",
2861                        index
2862                    ));
2863                }
2864            }
2865        }
2866        Ok(channels)
2867    }
2868
2869    /// Resolve a complete user-ordered parent from a generated edge set without
2870    /// rebuilding its tree or external routing. Reorder a clone only: canonical
2871    /// catalogue IDs and the generated basis order are immutable.
2872    pub(crate) fn sampling_parent_lmb(&self, edges: &[usize]) -> Result<LoopMomentumBasis> {
2873        let selected = edges.iter().copied().collect::<BTreeSet<_>>();
2874        if edges.len() != self.graph.loop_momentum_basis.loop_edges.len()
2875            || selected.len() != edges.len()
2876        {
2877            return Err(eyre!(
2878                "sampling parent requires {} distinct loop edges, received {edges:?}",
2879                self.graph.loop_momentum_basis.loop_edges.len()
2880            ));
2881        }
2882        let candidates = self
2883            .all_bases
2884            .iter()
2885            .filter(|basis| {
2886                basis.loop_edges.len() == edges.len()
2887                    && basis
2888                        .loop_edges
2889                        .iter()
2890                        .all(|edge| selected.contains(&edge.0))
2891            })
2892            .collect_vec();
2893        let [basis] = candidates.as_slice() else {
2894            return Err(eyre!(
2895                "sampling parent {edges:?} matches {} generated bases for graph '{}'; expected one complete basis, available {:?}",
2896                candidates.len(),
2897                self.graph.name,
2898                self.all_bases
2899                    .iter()
2900                    .map(|basis| &basis.loop_edges)
2901                    .collect_vec()
2902            ));
2903        };
2904        let mut basis = (*basis).clone();
2905        for (index, edge) in edges.iter().enumerate() {
2906            let current = basis
2907                .loop_edges
2908                .iter_enumerated()
2909                .find(|(_, candidate)| candidate.0 == *edge)
2910                .unwrap()
2911                .0;
2912            basis.swap_loops(LoopIndex(index), current);
2913        }
2914        Ok(basis)
2915    }
2916
2917    /// Build the exact affine routing from one generated LMB into this setup's
2918    /// parent loop frame.  The integer edge signatures provide the linear
2919    /// block matrix; the supplied external momenta provide its translation.
2920    /// Keeping this operation on the graph-aware setup ensures that a compiled
2921    /// channel never mistakes selected-LMB coordinates for parent-frame
2922    /// coordinates.
2923    pub fn lmb_frame_map<T: FloatLike>(
2924        &self,
2925        channel_lmb: &LoopMomentumBasis,
2926        external_momenta: &[[T; 4]],
2927    ) -> Result<SamplingMapAffine<T>> {
2928        let parent_loop_edges = &self.graph.loop_momentum_basis.loop_edges;
2929        let channel_loop_count = channel_lmb.loop_edges.len();
2930        if parent_loop_edges.len() != channel_loop_count {
2931            return Err(eyre!(
2932                "cannot route LMB {:?} with {} loop blocks into parent frame with {} blocks",
2933                &channel_lmb.loop_edges,
2934                channel_loop_count,
2935                parent_loop_edges.len()
2936            ));
2937        }
2938        let dimension = 3 * channel_loop_count;
2939        let zero = F::<T>::default().zero();
2940        let mut matrix = vec![vec![zero.0.clone(); dimension]; dimension];
2941        let mut translation = vec![zero.0.clone(); dimension];
2942        for (parent_block, &edge_index) in parent_loop_edges.iter().enumerate() {
2943            let signature = &channel_lmb.edge_signatures[edge_index];
2944            let internal = signature.internal.to_momtrop_format();
2945            if internal.len() != channel_loop_count {
2946                return Err(eyre!(
2947                    "LMB {:?} edge {} has internal signature length {}, expected {}",
2948                    &channel_lmb.loop_edges,
2949                    edge_index.0,
2950                    internal.len(),
2951                    channel_loop_count
2952                ));
2953            }
2954            let external = signature.external.to_momtrop_format();
2955            if external.len() != external_momenta.len() {
2956                return Err(eyre!(
2957                    "LMB {:?} edge {} has external signature length {}, but {} external momenta were supplied",
2958                    &channel_lmb.loop_edges,
2959                    edge_index.0,
2960                    external.len(),
2961                    external_momenta.len()
2962                ));
2963            }
2964            for component in 0..3 {
2965                let row = 3 * parent_block + component;
2966                for (channel_block, coefficient) in internal.iter().enumerate() {
2967                    matrix[row][3 * channel_block + component] =
2968                        zero.from_i64(*coefficient as i64).0;
2969                }
2970                translation[row] = external
2971                    .iter()
2972                    .zip(external_momenta)
2973                    .map(|(coefficient, momentum)| {
2974                        zero.from_i64(*coefficient as i64) * F(momentum[component + 1].clone())
2975                    })
2976                    .fold(zero.clone(), |sum, value| sum + value)
2977                    .0;
2978            }
2979        }
2980        SamplingMapAffine::new(matrix, translation)
2981    }
2982
2983    fn reinterpret_loop_momenta_for_lmb_impl<T: FloatLike>(
2984        &self,
2985        lmb_index: LmbIndex,
2986        momentum_sample: &BareMomentumSample<T>,
2987        loop_mom_cache_id: usize,
2988    ) -> BareMomentumSample<T> {
2989        let channel_lmb = &self.all_bases[lmb_index];
2990        let new_loop_moms: LoopMomenta<F<T>> = self
2991            .graph
2992            .loop_momentum_basis
2993            .loop_edges
2994            .iter()
2995            .map(|&edge_index| {
2996                let signature_of_edge_channel_lmb = &channel_lmb.edge_signatures[edge_index];
2997                let loop_part = signature_of_edge_channel_lmb
2998                    .internal
2999                    .apply_typed(&momentum_sample.loop_moms);
3000                if momentum_sample.external_moms.is_empty() {
3001                    loop_part
3002                } else {
3003                    loop_part
3004                        + signature_of_edge_channel_lmb
3005                            .external
3006                            .apply(&momentum_sample.external_moms.raw)
3007                            .spatial
3008                }
3009            })
3010            .collect();
3011
3012        BareMomentumSample {
3013            loop_moms: new_loop_moms,
3014            dual_loop_moms: momentum_sample.dual_loop_moms.clone().map(|_dlm| todo!()),
3015            loop_mom_cache_id,
3016            loop_mom_base_cache_id: momentum_sample.loop_mom_base_cache_id,
3017            external_mom_cache_id: momentum_sample.external_mom_cache_id,
3018            external_mom_base_cache_id: momentum_sample.external_mom_base_cache_id,
3019            external_moms: momentum_sample.external_moms.clone(),
3020            jacobian: momentum_sample.jacobian.clone(),
3021            orientation: momentum_sample.orientation,
3022            parameterization_branch: momentum_sample.parameterization_branch,
3023        }
3024    }
3025
3026    pub(crate) fn reinterpret_loop_momenta_for_lmb<T: FloatLike>(
3027        &self,
3028        lmb_index: LmbIndex,
3029        momentum_sample: &MomentumSample<T>,
3030        loop_mom_cache_id: usize,
3031    ) -> MomentumSample<T> {
3032        MomentumSample {
3033            sample: self.reinterpret_loop_momenta_for_lmb_impl(
3034                lmb_index,
3035                &momentum_sample.sample,
3036                loop_mom_cache_id,
3037            ),
3038        }
3039    }
3040}
3041
3042pub trait ProcessIntegrandImpl {
3043    type G: GraphTerm;
3044
3045    fn warm_up(&mut self, model: &Model) -> Result<()>;
3046
3047    /// Natural E_cm scale of the returned physical quantity. Custom graphs
3048    /// declare their integrated energy dimension; raw inputs omit spatial measure.
3049    fn stability_reference_scale<T: FloatLike>(
3050        &self,
3051        graph_id: usize,
3052        sample: &MomentumSample<T>,
3053        missing_measure_dimension: i32,
3054    ) -> Result<F<T>> {
3055        let settings = self.get_settings();
3056        let dimension = settings.stability.integrated_energy_dimension.ok_or_else(|| {
3057            eyre!("E_cm-relative physical stability requires `stability.integrated_energy_dimension` (after flux normalization, before output-unit conversion)")
3058        })?;
3059        let exponent = dimension.checked_sub(missing_measure_dimension).ok_or_else(|| {
3060            eyre!("stability energy dimension overflows after subtracting the missing spatial measure")
3061        })?;
3062        let mut scale = F::<T>::from_f64(settings.kinematics.e_cm).powi(exponent);
3063        if matches!(
3064            self.get_dependent_momenta_constructor(),
3065            DependentMomentaConstructor::CrossSection
3066        ) && !settings.general.disable_flux_factor
3067            && sample.external_moms().len() == 2
3068        {
3069            let graph = self.get_graph(graph_id).get_graph();
3070            let unit = settings
3071                .general
3072                .integral_unit
3073                .resolve_for_cross_section(graph.initial_state_cut.iter_edges(graph).count());
3074            scale *= cross_section::barn_conversion_factor(unit, sample.one());
3075        }
3076        Ok(scale)
3077    }
3078
3079    /// Choose the complete numerical proposal before any draw or physical retry.
3080    /// This conservative admission floor is not a pointwise map-error bound;
3081    /// density/root checks still fail explicitly when the fixed law is unresolved.
3082    fn sampling_source_policy(&self) -> Result<(SamplingPrecision, f64)> {
3083        let settings = self.get_settings();
3084        let budget = GammaLoopSample::<ArbPrec>::source_accuracy_budget(settings)?;
3085        let floor = F::<f128>::default().epsilon().sqrt().into_ff64().0;
3086        let parameterization = settings
3087            .sampling
3088            .get_parameterization_settings()
3089            .ok_or_else(|| eyre!("sampling source policy requires a channel parameterization"))?;
3090        let quad_eligible = budget >= floor
3091            && self.graph_count() > 0
3092            && parameterization.sampling_channels.weight != SamplingChannelWeight::SingularityProxy
3093            && (0..self.graph_count()).all(|id| {
3094                self.get_graph(id)
3095                    .sampling_setup()
3096                    .sampling_catalogue
3097                    .as_ref()
3098                    .is_some_and(SamplingChannelCatalogue::supports_fixed_quad_source)
3099            });
3100        let externals = settings
3101            .kinematics
3102            .externals
3103            .get_dependent_externals::<ArbPrec>(self.get_dependent_momenta_constructor())?;
3104        let mut inputs = externals
3105            .iter()
3106            .flat_map(|momentum| {
3107                [
3108                    momentum.temporal.value.clone(),
3109                    momentum.spatial.px.clone(),
3110                    momentum.spatial.py.clone(),
3111                    momentum.spatial.pz.clone(),
3112                ]
3113            })
3114            .collect_vec();
3115        let scale = F::<ArbPrec>::from_f64(settings.kinematics.e_cm);
3116        let b = F::<ArbPrec>::from_f64(parameterization.b);
3117        inputs.extend([
3118            scale.square(),
3119            &scale * &b,
3120            scale,
3121            b,
3122            F::<ArbPrec>::from_f64(parameterization.power),
3123            F::<ArbPrec>::from_f64(settings.lu_h_function.sigma),
3124        ]);
3125        for id in 0..self.graph_count() {
3126            for mass in self
3127                .get_graph(id)
3128                .get_real_mass_vector()?
3129                .iter()
3130                .filter_map(|(_, mass)| mass.as_ref())
3131            {
3132                let mass = F::<ArbPrec>::from_ff64(*mass);
3133                inputs.extend([mass.square(), mass]);
3134            }
3135        }
3136        if inputs
3137            .iter()
3138            .any(|value| value.is_nan() || value.is_infinite())
3139        {
3140            return Err(eyre!(
3141                "sampling source has nonfinite fixed kinematic, mass or parameterization input"
3142            ));
3143        }
3144        let quad_representable = inputs
3145            .iter()
3146            .all(|value| F::<f128>::from_arb(&value.0).is_ok());
3147        let fixed256_floor = F::<SamplingFloat>::default().epsilon().sqrt().into_ff64().0;
3148        Ok((
3149            if quad_eligible && quad_representable {
3150                SamplingPrecision::Quad
3151            } else if budget >= fixed256_floor
3152                && inputs
3153                    .iter()
3154                    .all(|value| F::<SamplingFloat>::from_arb(&value.0).is_ok())
3155            {
3156                SamplingPrecision::Fixed256
3157            } else {
3158                SamplingPrecision::Arb
3159            },
3160            budget,
3161        ))
3162    }
3163
3164    /// Compile fixed graph geometry after process warmup has prepared masses
3165    /// and improved externals. Publish only when the fixed canonical precision
3166    /// has every bridge valid. Native bindings remain available for component
3167    /// diagnostics; they do not define physical retry proposals. Radial roots
3168    /// and conditional cut contexts remain point-dependent runtime data.
3169    /// A failed canonical binding invalidates the epoch.
3170    fn warm_up_sampling(&mut self) -> Result<()> {
3171        for graph in self.get_terms_mut() {
3172            graph.sampling_setup_mut().invalidate_sampling();
3173        }
3174        if !self.get_settings().sampling.uses_sampling_channels() {
3175            return Ok(());
3176        }
3177        let parameterization = self
3178            .get_settings()
3179            .sampling
3180            .get_parameterization_settings()
3181            .expect("sampling channels require a parameterization");
3182        let prepared = (0..self.graph_count())
3183            .map(|id| {
3184                let graph = self.get_graph(id);
3185                let setup = graph.sampling_setup();
3186                let resolved = resolve_sampling_channel_selection(
3187                    &setup.graph.name,
3188                    &parameterization.sampling_channels,
3189                )?;
3190                // Inspect already resolved blocks without requiring a successful
3191                // binding at another precision; the original source will retain
3192                // one fixed canonical map for every physical rescue lane.
3193                let catalogue = setup.sampling_channel_catalogue(&resolved, &parameterization)?;
3194                let programs = catalogue.compile_programs(
3195                    3 * graph.get_graph().get_loop_number(),
3196                    &self.get_settings().lu_h_function,
3197                )?;
3198                Ok((catalogue, programs))
3199            })
3200            .collect::<Result<Vec<_>>>()?;
3201        for (graph, (catalogue, programs)) in self.get_terms_mut().zip(prepared) {
3202            let setup = graph.sampling_setup_mut();
3203            setup.sampling_catalogue.set(catalogue);
3204            setup.sampling_programs.set(programs);
3205        }
3206        let result = (|| {
3207            let (precision, budget) = self.sampling_source_policy()?;
3208            match precision {
3209                SamplingPrecision::Quad => self.prepare_sampling_precision::<f128>(),
3210                SamplingPrecision::Fixed256 => self.prepare_sampling_precision::<SamplingFloat>(),
3211                SamplingPrecision::Arb => self.prepare_sampling_precision::<ArbPrec>(),
3212                SamplingPrecision::Double => unreachable!("Double is never a canonical source"),
3213            }?;
3214            for graph in self.get_terms_mut() {
3215                graph
3216                    .sampling_setup_mut()
3217                    .sampling_source
3218                    .set((precision, budget));
3219            }
3220            crate::debug_tags!(#sampling;
3221                stage = "sampling_source_warmup", precision = %precision, accuracy_budget = budget,
3222                "selected fixed source for the complete integrand epoch"
3223            );
3224            Ok(())
3225        })();
3226        if result.is_ok() {
3227            // Preserve the configured native component APIs and their cached
3228            // numerical diagnostics, without requiring a second proposal law
3229            // to be usable before evaluating the canonical one.
3230            let precisions = self
3231                .get_settings()
3232                .stability
3233                .levels
3234                .iter()
3235                .map(|level| level.precision)
3236                .unique()
3237                .collect_vec();
3238            for precision in precisions {
3239                let native = match precision {
3240                    Precision::Double => self.prepare_sampling_precision::<f64>(),
3241                    Precision::Quad => self.prepare_sampling_precision::<f128>(),
3242                    Precision::Arb => self.prepare_sampling_precision::<ArbPrec>(),
3243                };
3244                if let Err(error) = native {
3245                    crate::debug_tags!(#sampling;
3246                        stage = "sampling_native_component_binding", precision = %precision,
3247                        error = %error, "native component binding unavailable; production uses the canonical draw"
3248                    );
3249                }
3250            }
3251        }
3252        if result.is_err() {
3253            // An epoch with no usable canonical binding publishes neither a
3254            // catalogue nor partial bindings, irrespective of native availability.
3255            for graph in self.get_terms_mut() {
3256                graph.sampling_setup_mut().invalidate_sampling();
3257            }
3258        }
3259        result
3260    }
3261
3262    /// Bind a configured or explicitly requested precision once per warmup epoch,
3263    /// retaining typed numerical failures as well as successes. Programs and
3264    /// canonical IDs are reused; geometry comes from the same native
3265    /// improved external data consumed by physical evaluation. Worker clones own
3266    /// their evaluator buffers, and a precision request never reparses metadata.
3267    fn prepare_sampling_precision<T: FloatLike>(&mut self) -> Result<()> {
3268        if !self.get_settings().sampling.uses_sampling_channels() {
3269            return Ok(());
3270        }
3271        if (0..self.graph_count()).all(|id| {
3272            T::sampling_bridge_cache(self.get_graph(id).sampling_setup())
3273                .as_ref()
3274                .is_some()
3275        }) {
3276            return (0..self.graph_count()).try_for_each(|id| {
3277                self.get_graph(id)
3278                    .sampling_setup()
3279                    .sampling_bridge::<T>()
3280                    .map(|_| ())
3281            });
3282        }
3283        let parameterization = self
3284            .get_settings()
3285            .sampling
3286            .get_parameterization_settings()
3287            .expect("sampling channels require a parameterization");
3288        let externals = self
3289            .get_settings()
3290            .kinematics
3291            .externals
3292            .get_dependent_externals::<T>(self.get_dependent_momenta_constructor())?;
3293        let external_momenta = externals
3294            .iter()
3295            .map(|momentum| {
3296                [
3297                    momentum.temporal.value.0.clone(),
3298                    momentum.spatial.px.0.clone(),
3299                    momentum.spatial.py.0.clone(),
3300                    momentum.spatial.pz.0.clone(),
3301                ]
3302            })
3303            .collect_vec();
3304        // The materialized draw, map consistency and physical host adoption
3305        // share one accuracy budget; native physical retries need no map binding.
3306        let (source_precision, source_budget) = self
3307            .get_graph(0)
3308            .sampling_setup()
3309            .sampling_source
3310            .as_ref()
3311            .copied()
3312            .map_or_else(|| self.sampling_source_policy(), Ok)?;
3313        let density_tolerance = if source_precision == T::sampling_precision() {
3314            source_budget
3315        } else {
3316            GammaLoopSample::<T>::relative_accuracy_budget(self.get_settings())
3317        };
3318        let bridges = (0..self.graph_count()).map(|id| {
3319            let graph = self.get_graph(id);
3320            let setup = graph.sampling_setup();
3321            if T::sampling_bridge_cache(setup).as_ref().is_some() { return Ok(None); }
3322            let catalogue = setup.sampling_catalogue.as_ref().ok_or_else(|| eyre!(
3323                "sampling catalogue for graph '{}' is not initialized; call warm_up after loading or changing runtime settings, model parameters, or graph routing", graph.name()
3324            ))?;
3325            let programs = setup.sampling_programs.as_ref().ok_or_else(|| eyre!(
3326                "sampling programs for graph '{}' are not initialized; call warm_up", graph.name()
3327            ))?;
3328            let bridge = graph.bind_sampling_bridge(catalogue, programs, &parameterization, self.get_settings(), &external_momenta, None)
3329                .and_then(|bridge| bridge.with_relative_density_tolerance(density_tolerance));
3330            match bridge {
3331                Ok(bridge) => Ok(Some(Ok(bridge))),
3332                Err(error) => match error.downcast_ref::<sampling::maps::SamplingEvaluationError>() {
3333                    Some(error) => Ok(Some(Err(error.clone()))),
3334                    None => Err(error),
3335                },
3336            }
3337        }).collect::<Result<Vec<_>>>()?;
3338        for (graph, binding) in self.get_terms_mut().zip(bridges) {
3339            if let Some(binding) = binding {
3340                crate::debug_tags!(#sampling;
3341                    stage = "sampling_bridge_warmup", graph = %graph.name(),
3342                    channels = ?binding.as_ref().ok().map(|bridge| bridge.channels().len()),
3343                    precision = std::any::type_name::<T>(), valid = binding.is_ok(),
3344                    "prepared graph sampling binding"
3345                );
3346                T::sampling_bridge_cache_mut(graph.sampling_setup_mut()).set(binding);
3347            }
3348        }
3349        (0..self.graph_count()).try_for_each(|id| {
3350            self.get_graph(id)
3351                .sampling_setup()
3352                .sampling_bridge::<T>()
3353                .map(|_| ())
3354        })
3355    }
3356
3357    fn get_rotations(&self) -> impl Iterator<Item = &Rotation>;
3358
3359    fn increment_loop_cache_id(&mut self, val: usize);
3360    fn loop_cache_id(&self) -> usize;
3361
3362    fn increment_external_cache_id(&mut self, val: usize);
3363    fn external_cache_id(&self) -> usize;
3364
3365    /// Signal that external momenta configuration has actually changed
3366    /// This increments the external cache ID to invalidate cached computations
3367    ///
3368    /// # Usage
3369    /// Call this method when:
3370    /// - You change the external momenta values in your Monte Carlo sampling
3371    /// - You switch to a different kinematic configuration
3372    /// - You want to force recomputation of cached polarizations/external quantities
3373    ///
3374    /// # Example
3375    /// ```rust,ignore
3376    /// // When you change external momenta in your sampling
3377    /// integrand.signal_external_momenta_changed();
3378    /// let new_sample = parameterize(&sample_point, &mut integrand, &mut first_draw_metadata)?;
3379    ///
3380    /// // Rotations will automatically get new cache IDs
3381    /// let rotated_samples = evaluate_all_rotations(&new_sample, &mut integrand, true)?;
3382    ///
3383    /// // Next iteration - revert to base configuration to reuse cache
3384    /// integrand.revert_to_base_external_cache_id();
3385    /// let next_sample = parameterize(&next_sample_point, &mut integrand, &mut next_draw_metadata)?;
3386    /// ```
3387    fn signal_external_momenta_changed(&mut self) {
3388        self.increment_external_cache_id(1);
3389    }
3390
3391    /// Get the current external cache ID for reuse (doesn't increment)
3392    ///
3393    /// This returns the current cache ID without incrementing it, allowing
3394    /// new samples to reuse cached computations for the same external
3395    /// momenta configuration.
3396    fn get_current_external_cache_id(&self) -> usize {
3397        self.external_cache_id()
3398    }
3399
3400    /// Revert to the base external cache ID for the current configuration
3401    ///
3402    /// This allows new samples to reuse the base cache ID when they represent
3403    /// the same underlying external momenta configuration (e.g., after rotations)
3404    ///
3405    /// # Usage
3406    /// Call this method when you want to create a new sample that represents
3407    /// the same base external momenta configuration as before, allowing reuse
3408    /// of cached polarizations and other external-dependent computations.
3409    ///
3410    /// # Example Cache Flow
3411    /// ```text
3412    /// 1. Initial sample:           cache_id = 0, base_cache_id = 0
3413    /// 2. Rotation 1:               cache_id = 1, base_cache_id = 0
3414    /// 3. Rotation 2:               cache_id = 2, base_cache_id = 0
3415    /// 4. revert_to_base():         cache_id = 0, base_cache_id = 0  // Reuse cache!
3416    /// 5. signal_changed():         cache_id = 3, base_cache_id = 3  // New base config
3417    /// 6. Rotation of new config:   cache_id = 4, base_cache_id = 3
3418    /// 7. revert_to_base():         cache_id = 3, base_cache_id = 3  // Reuse new base
3419    /// ```
3420    fn revert_to_base_external_cache_id(&mut self);
3421
3422    /// Check if external momenta caching is beneficial
3423    ///
3424    /// Returns true if the same external cache ID has been used multiple times,
3425    /// indicating that caching is providing benefits.
3426    fn is_external_caching_beneficial(&self) -> bool {
3427        // Simple heuristic: if we've created samples without incrementing
3428        // cache ID, then caching is being used
3429        self.external_cache_id() < self.loop_cache_id()
3430    }
3431
3432    /// Force a cache consistency check with detailed reporting
3433    fn debug_cache_state(&self, context: &str) {
3434        if std::env::var("GAMMALOOP_DEBUG_CACHE").is_ok() {
3435            let validation = self.validate_cache_consistency();
3436            let stats = self.get_cache_stats();
3437
3438            tracing::info!("🔍 DEBUG CACHE STATE at {}", context);
3439            tracing::info!("   Validation: {}", validation);
3440            tracing::info!("   Statistics: {}", stats);
3441
3442            if !validation.is_valid {
3443                tracing::error!("   ❌ CACHE INCONSISTENCY DETECTED!");
3444                panic!(
3445                    "Cache corruption at {}: {}",
3446                    context, validation.diagnostics
3447                );
3448            }
3449
3450            if validation.has_rotations {
3451                tracing::info!(
3452                    "   🔄 {} rotation variants from base cache_id {}",
3453                    validation.current_external_cache_id - validation.base_external_cache_id,
3454                    validation.base_external_cache_id
3455                );
3456            }
3457
3458            if stats.efficiency_ratio < 0.3 {
3459                tracing::warn!(
3460                    "   ⚠️ Very low cache efficiency: {:.1}%",
3461                    stats.efficiency_ratio * 100.0
3462                );
3463            } else if stats.efficiency_ratio > 0.8 {
3464                tracing::info!(
3465                    "   ✅ Excellent cache efficiency: {:.1}%",
3466                    stats.efficiency_ratio * 100.0
3467                );
3468            }
3469        }
3470    }
3471
3472    /// Get the base external cache ID for the current configuration
3473    fn get_base_external_cache_id(&self) -> usize;
3474
3475    /// Validate cache ID consistency and return diagnostics
3476    fn validate_cache_consistency(&self) -> CacheValidationResult {
3477        let current_id = self.external_cache_id();
3478        let base_id = self.get_base_external_cache_id();
3479        let loop_id = self.loop_cache_id();
3480
3481        let is_valid = base_id <= current_id;
3482        let has_rotations = current_id > base_id;
3483        let cache_efficiency = if loop_id > 0 {
3484            1.0 - (current_id as f64 / loop_id as f64)
3485        } else {
3486            0.0
3487        };
3488
3489        CacheValidationResult {
3490            is_valid,
3491            current_external_cache_id: current_id,
3492            base_external_cache_id: base_id,
3493            loop_cache_id: loop_id,
3494            has_rotations,
3495            cache_efficiency,
3496            diagnostics: if is_valid {
3497                "Cache IDs are consistent".to_string()
3498            } else {
3499                format!(
3500                    "ERROR: Base cache ID ({}) > Current cache ID ({})",
3501                    base_id, current_id
3502                )
3503            },
3504        }
3505    }
3506
3507    /// Get cache usage statistics for monitoring
3508    fn get_cache_stats(&self) -> CacheStats {
3509        let validation = self.validate_cache_consistency();
3510        CacheStats {
3511            total_external_increments: validation.current_external_cache_id,
3512            total_loop_increments: validation.loop_cache_id,
3513            base_configurations: validation.base_external_cache_id + 1,
3514            rotational_variants: validation.current_external_cache_id
3515                - validation.base_external_cache_id,
3516            efficiency_ratio: validation.cache_efficiency,
3517        }
3518    }
3519
3520    fn get_group_masters(&self) -> impl Iterator<Item = &Self::G>;
3521
3522    fn get_terms_mut(&mut self) -> impl Iterator<Item = &mut Self::G>;
3523    fn graph_count(&self) -> usize;
3524    fn get_settings(&self) -> &RuntimeSettings;
3525    fn get_master_graph(&self, group_id: GroupId) -> &Self::G;
3526    fn get_graph(&self, graph_id: usize) -> &Self::G;
3527    fn get_graph_mut(&mut self, graph_id: usize) -> &mut Self::G;
3528    fn graph_group_id_for_graph(&self, graph_id: usize) -> Option<usize>;
3529    fn get_group(&self, group_id: GroupId) -> &GraphGroup;
3530    fn get_group_structure(&self) -> &TiVec<GroupId, GraphGroup>;
3531    fn get_dependent_momenta_constructor(&self) -> DependentMomentaConstructor<'_>;
3532    fn take_event_processing_runtime(&mut self) -> Option<EventProcessingRuntime> {
3533        None
3534    }
3535    fn restore_event_processing_runtime(&mut self, _runtime: Option<EventProcessingRuntime>) {}
3536    fn event_processing_runtime(&self) -> Option<&EventProcessingRuntime> {
3537        None
3538    }
3539    fn event_processing_runtime_mut(&mut self) -> Option<&mut EventProcessingRuntime> {
3540        None
3541    }
3542    fn groups_default_sample_events_by_graph_group(&self) -> bool {
3543        false
3544    }
3545
3546    fn uses_explicit_orientation_sum_only(&self) -> bool {
3547        false
3548    }
3549
3550    // fn get_builder_cache(&self) -> &ParamBuilder<f64>;
3551}
3552
3553pub(crate) fn validate_process_runtime_settings(
3554    settings: &RuntimeSettings,
3555    explicit_orientation_sum_only: bool,
3556) -> Result<()> {
3557    if let Some(parameterization) = settings.sampling.get_parameterization_settings() {
3558        let alpha = parameterization.sampling_channels.alpha;
3559        if !alpha.is_finite() || alpha < 0.0 {
3560            return Err(eyre!(
3561                "sampling.alpha must be finite and nonnegative; got {alpha}"
3562            ));
3563        }
3564    }
3565    for (index, level) in settings.stability.levels.iter().enumerate() {
3566        for (component, tolerance) in [
3567            ("re", level.ecm_relative_tolerance_for_re),
3568            ("im", level.ecm_relative_tolerance_for_im),
3569        ] {
3570            if !tolerance.is_finite() || tolerance < 0.0 {
3571                return Err(eyre!(
3572                    "`runtime.stability.levels[{index}].ecm_relative_tolerance_for_{component}` must be finite and nonnegative; got {tolerance}"
3573                ));
3574            }
3575        }
3576    }
3577    if settings.stability.levels.iter().any(|level| {
3578        level.ecm_relative_tolerance_for_re > 0.0 || level.ecm_relative_tolerance_for_im > 0.0
3579    }) && (!settings.kinematics.e_cm.is_finite() || settings.kinematics.e_cm <= 0.0)
3580    {
3581        return Err(eyre!(
3582            "E_cm-relative stability requires finite positive `runtime.kinematics.e_cm`"
3583        ));
3584    }
3585    if settings.general.use_ltd {
3586        return Err(eyre!(
3587            "`runtime.general.use_ltd = true` is reserved for deferred proper-LTD support; the current evaluation backend is CFF"
3588        ));
3589    }
3590
3591    // The shared process parameter layout always includes M, even when unused.
3592    if settings.general.numerator_sampling_scale == 0.0 {
3593        return Err(eyre!(
3594            "`runtime.general.numerator_sampling_scale` must be nonzero for the auxiliary sampling scale M"
3595        ));
3596    }
3597
3598    if !explicit_orientation_sum_only {
3599        return Ok(());
3600    }
3601
3602    if settings.general.orientation_pat.pat.is_some() {
3603        return Err(eyre!(
3604            "`global.generation.explicit_orientation_sum_only = true` already contains the complete orientation sum; `runtime.general.orientation_pat` must be unset"
3605        ));
3606    }
3607
3608    if let SamplingSettings::DiscreteGraphs(discrete_settings) = &settings.sampling
3609        && discrete_settings.sample_orientations
3610    {
3611        return Err(eyre!(
3612            "`global.generation.explicit_orientation_sum_only = true` does not support runtime individual-orientation Monte Carlo sampling; set `sampling.sample_orientations = false`"
3613        ));
3614    }
3615
3616    Ok(())
3617}
3618
3619fn validate_orientation_catalog_group<'a>(
3620    group_id: GroupId,
3621    catalogs: impl IntoIterator<Item = (String, Vec<&'a str>)>,
3622) -> Result<()> {
3623    let mut catalogs = catalogs.into_iter();
3624    let Some((master_name, master_catalog)) = catalogs.next() else {
3625        return Ok(());
3626    };
3627    for (graph_name, catalog) in catalogs {
3628        if catalog != master_catalog {
3629            let first_difference = master_catalog
3630                .iter()
3631                .zip_longest(&catalog)
3632                .position(|entry| match entry {
3633                    itertools::EitherOrBoth::Both(left, right) => left != right,
3634                    itertools::EitherOrBoth::Left(_) | itertools::EitherOrBoth::Right(_) => true,
3635                })
3636                .unwrap_or_default();
3637            return Err(eyre!(
3638                "Runtime orientation Monte Carlo cannot use graph group {} because graph '{}' and master '{}' have different exact residue-map catalogs ({} versus {} selected maps; first difference at channel {}). Disable `sampling.sample_orientations` so each graph explicitly sums its own complete map catalog.",
3639                group_id.0,
3640                graph_name,
3641                master_name,
3642                catalog.len(),
3643                master_catalog.len(),
3644                first_difference,
3645            ));
3646        }
3647    }
3648    Ok(())
3649}
3650
3651pub(crate) fn validate_group_orientation_catalogs<G: GraphTerm>(
3652    settings: &RuntimeSettings,
3653    graph_terms: &[G],
3654    groups: &TiVec<GroupId, GraphGroup>,
3655) -> Result<()> {
3656    let SamplingSettings::DiscreteGraphs(discrete_settings) = &settings.sampling else {
3657        return Ok(());
3658    };
3659    if !discrete_settings.sample_orientations {
3660        return Ok(());
3661    }
3662
3663    for (group_id, group) in groups.iter_enumerated() {
3664        let master_id = group.master();
3665        validate_orientation_catalog_group(
3666            group_id,
3667            std::iter::once(master_id)
3668                .chain(group.into_iter().filter(|graph_id| *graph_id != master_id))
3669                .map(|graph_id| {
3670                    let graph = &graph_terms[graph_id];
3671                    (graph.name(), graph.selected_production_orientation_keys())
3672                }),
3673        )?;
3674    }
3675    Ok(())
3676}
3677
3678fn get_global_dimension_if_exists<I: ProcessIntegrandImpl>(integrand: &I) -> Option<usize> {
3679    if integrand
3680        .get_settings()
3681        .sampling
3682        .get_parameterization_settings()
3683        .is_none()
3684    {
3685        None
3686    } else {
3687        Some(
3688            integrand
3689                .get_master_graph(GroupId(0))
3690                .get_graph()
3691                .get_loop_number()
3692                * 3,
3693        )
3694    }
3695}
3696
3697pub trait GraphTerm {
3698    /// Prepare physical center decisions at the original Arb point. Geometry
3699    /// is channel-independent for the same selector-accepted cut set; explicit
3700    /// channel selectors retain their source metadata. Amplitudes own their
3701    /// separate overlap preparation.
3702    fn prepare_physical_overlaps(
3703        &mut self,
3704        _sample: &MomentumSample<ArbPrec>,
3705        _context: GraphTermEvaluationContext<'_, '_, ArbPrec>,
3706    ) -> Result<Option<TiVec<CutGroupId, Option<LUSharedOverlaps<ArbPrec>>>>> {
3707        Ok(None)
3708    }
3709
3710    fn evaluate<T: FloatLike>(
3711        &mut self,
3712        sample: &MomentumSample<T>,
3713        context: GraphTermEvaluationContext<'_, '_, T>,
3714    ) -> Result<GraphEvaluationResult<T>>;
3715
3716    fn name(&self) -> String;
3717    fn orientation_label(&self, orientation_id: usize) -> Option<String>;
3718    fn sampling_channel_label(
3719        &self,
3720        channel_id: SamplingChannelId,
3721        parameterization_settings: &ParameterizationSettings,
3722    ) -> Result<Option<String>>;
3723
3724    fn warm_up(&mut self, settings: &RuntimeSettings, model: &Model) -> Result<()>;
3725    fn get_graph(&self) -> &Graph;
3726    fn sampling_setup(&self) -> &LmbMultiChannelingSetup;
3727    fn sampling_setup_mut(&mut self) -> &mut LmbMultiChannelingSetup;
3728    fn get_num_orientations(&self) -> usize;
3729    fn production_orientation_keys(&self) -> &[String];
3730    fn selected_production_orientation_keys(&self) -> Vec<&str>;
3731    fn selected_lmb_basis_id(
3732        &self,
3733        parameterization_settings: &ParameterizationSettings,
3734    ) -> Result<LmbIndex>;
3735    fn get_tropical_sampler(&self) -> &SampleGenerator<3>;
3736    fn get_mut_param_builder(&mut self) -> &mut ParamBuilder<f64>;
3737    fn get_real_mass_vector(&self) -> Result<EdgeVec<Option<F<f64>>>>;
3738
3739    /// Compile the canonical full-frame sampling bridge for this graph.
3740    /// Process implementations own the graph-specific external signature and
3741    /// LMB setup; the sampler only supplies the already-resolved kinematics.
3742    fn compile_sampling_bridge<T: FloatLike>(
3743        &self,
3744        parameterization_settings: &ParameterizationSettings,
3745        settings: &RuntimeSettings,
3746        external_momenta: &[[T; 4]],
3747        orientation: Option<usize>,
3748    ) -> Result<SamplingChannelBridge<T>> {
3749        let resolved = resolve_sampling_channel_selection(
3750            &self.sampling_setup().graph.name,
3751            &parameterization_settings.sampling_channels,
3752        )?;
3753        let catalogue = self
3754            .sampling_setup()
3755            .sampling_channel_catalogue(&resolved, parameterization_settings)?;
3756        let programs = catalogue.compile_programs(
3757            3 * self.get_graph().get_loop_number(),
3758            &settings.lu_h_function,
3759        )?;
3760        self.bind_sampling_bridge(
3761            &catalogue,
3762            &programs,
3763            parameterization_settings,
3764            settings,
3765            external_momenta,
3766            orientation,
3767        )
3768    }
3769
3770    /// Bind the already resolved catalogue and compiled programs to native
3771    /// graph geometry. Canonical warmup and native component diagnostics reuse
3772    /// this one definition.
3773    fn bind_sampling_bridge<T: FloatLike>(
3774        &self,
3775        catalogue: &SamplingChannelCatalogue,
3776        programs: &[SamplingChannelPrograms],
3777        parameterization_settings: &ParameterizationSettings,
3778        settings: &RuntimeSettings,
3779        external_momenta: &[[T; 4]],
3780        orientation: Option<usize>,
3781    ) -> Result<SamplingChannelBridge<T>>;
3782
3783    fn sampling_channel_is_lmb(
3784        &self,
3785        channel_id: SamplingChannelId,
3786        parameterization_settings: &ParameterizationSettings,
3787    ) -> Result<bool>;
3788    fn sampling_channel_ids(
3789        &self,
3790        parameterization_settings: &ParameterizationSettings,
3791    ) -> Result<Vec<SamplingChannelId>>;
3792}
3793
3794#[derive(Clone, Copy)]
3795pub enum EvaluationTarget<'a> {
3796    Physical(&'a Model),
3797    Reference(&'a GaussianReferenceFunction),
3798    #[cfg(test)]
3799    SamplingLaw(&'a SamplingLawProbe<'a>),
3800}
3801
3802/// A fixed normalized density and controlled body failure for exercising the
3803/// real original-source retry owner. It never reads the current proposal's
3804/// Jacobian or partition, and is absent from production builds.
3805#[cfg(test)]
3806pub struct SamplingLawProbe<'a> {
3807    reference: &'a LmbMultiChannelingSetup,
3808    channel_id: SamplingChannelId,
3809    radial_coordinate: usize,
3810    retry_lower_half: bool,
3811    sign_coordinate: Option<usize>,
3812    alternating_graph_sign: bool,
3813    calls: [std::sync::atomic::AtomicUsize; 3],
3814}
3815
3816#[cfg(test)]
3817impl SamplingLawProbe<'_> {
3818    fn evaluate<T: FloatLike>(
3819        &self,
3820        sample: &MomentumSample<T>,
3821        rotation: &Rotation,
3822        canonical: Option<&DiscreteGraphSample<ArbPrec>>,
3823        graph_id: usize,
3824    ) -> Result<GraphEvaluationResult<T>> {
3825        let canonical =
3826            canonical.expect("production sampling probe receives the retained canonical row");
3827        let expected = canonical.sample.materialize::<T>()?.rotate(
3828            rotation,
3829            sample.sample.loop_mom_cache_id,
3830            sample.sample.external_mom_cache_id,
3831        );
3832        assert_eq!(
3833            sample.loop_moms(),
3834            expected.loop_moms(),
3835            "physical retry changed the canonical point"
3836        );
3837        assert_eq!(sample.external_moms(), expected.external_moms());
3838        assert_eq!(
3839            sample.jacobian(),
3840            expected.jacobian(),
3841            "physical retry changed the combined canonical factor"
3842        );
3843        let precision = T::sampling_precision();
3844        let index = match precision {
3845            SamplingPrecision::Double => 0,
3846            SamplingPrecision::Quad => 1,
3847            SamplingPrecision::Arb => 2,
3848            SamplingPrecision::Fixed256 => unreachable!("source precision is not a physical lane"),
3849        };
3850        self.calls[index].fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3851        let point = sample
3852            .loop_moms()
3853            .iter()
3854            .flat_map(|momentum| {
3855                let momentum = rotation.inverse_rotate_three(momentum);
3856                [momentum.px.0, momentum.py.0, momentum.pz.0]
3857            })
3858            .collect_vec();
3859        let mut result = GraphEvaluationResult::zero(sample.zero());
3860        if let Some(reference) = self.reference.sampling_bridge::<T>()?.channels()
3861            [self.channel_id.index()]
3862        .map
3863        .inverse(&point)?
3864        {
3865            let half = sample.one() / sample.one().from_usize(2);
3866            result.integrand_result.re = if self.retry_lower_half
3867                && precision == SamplingPrecision::Double
3868                && F(reference.coordinates[self.radial_coordinate].clone()) < half
3869            {
3870                sample.zero() / sample.zero()
3871            } else {
3872                F(reference.inverse_jacobian)
3873            };
3874            if let Some(axis) = self.sign_coordinate {
3875                if F(reference.coordinates[axis].clone()) < half {
3876                    result.integrand_result.re = -result.integrand_result.re;
3877                }
3878                result.integrand_result.im = -result.integrand_result.re.clone();
3879            }
3880            if self.alternating_graph_sign && graph_id % 2 == 1 {
3881                result.integrand_result = -result.integrand_result;
3882            }
3883        }
3884        Ok(result)
3885    }
3886}
3887
3888struct EvaluationContext<'a, 'm> {
3889    target: EvaluationTarget<'a>,
3890    settings: &'a RuntimeSettings,
3891    rotation: &'a Rotation,
3892    evaluation_metadata: &'m mut EvaluationMetaData,
3893}
3894
3895pub struct GraphTermEvaluationContext<'a, 'm, T: FloatLike> {
3896    pub model: &'a Model,
3897    pub settings: &'a RuntimeSettings,
3898    pub event_processing_runtime: Option<&'m mut EventProcessingRuntime>,
3899    pub rotation: &'a Rotation,
3900    pub evaluation_metadata: &'m mut EvaluationMetaData,
3901    /// The canonical channel which mapped this point into the parent frame.
3902    /// Its sampling partition is applied outside the physical graph evaluation.
3903    pub sampling_channel: Option<SamplingChannelId>,
3904    pub graph_id: usize,
3905    pub(crate) prepared_lu_hosts: &'m [PreparedLUHost<T>],
3906    pub(crate) canonical_sample: Option<&'a DiscreteGraphSample<ArbPrec>>,
3907    pub(crate) sampling_accuracy_budget: f64,
3908}
3909
3910/// Evaluate one graph term using the canonical sampling channel contract.
3911fn evaluate_graph_term<T: FloatLike, I: ProcessIntegrandImpl>(
3912    integrand: &mut I,
3913    graph_id: usize,
3914    sample: &MomentumSample<T>,
3915    context: &mut EvaluationContext<'_, '_>,
3916    sampling_channel: Option<(SamplingChannelId, &[PreparedLUHost<T>])>,
3917    canonical_sample: Option<&DiscreteGraphSample<ArbPrec>>,
3918) -> Result<GraphEvaluationResult<T>> {
3919    if let Some((channel_id, hosts)) = sampling_channel {
3920        let graph = integrand.get_graph(graph_id);
3921        graph
3922            .sampling_setup()
3923            .sampling_catalogue
3924            .as_ref()
3925            .and_then(|catalogue| catalogue.entries.get(channel_id.index()))
3926            .ok_or_else(|| {
3927                eyre!(
3928                    "sampling channel {} is out of range for graph '{}'",
3929                    channel_id.index(),
3930                    graph.name()
3931                )
3932            })?;
3933        if hosts.iter().any(|host| {
3934            host.source.graph_id != graph_id
3935                || host.source.generating_channel != channel_id
3936                || host.source.target_channel != channel_id
3937        }) {
3938            return Err(eyre!(
3939                "selected LU host records do not belong to graph-group master {graph_id} and generating channel {channel_id:?}"
3940            ));
3941        }
3942        // Fixed canonical preparation owns the completed parent-frame map and
3943        // its selected host records. Physical lanes only adopt these records;
3944        // native map binding is not a prerequisite for evaluating the point.
3945    }
3946    // Every actual target and evaluator call counts, including failed attempts
3947    // and probe rotations. Physical diagnostic/event selection stays separate.
3948    // Host adoption is sampling work even though it runs inside this body.
3949    let sampling_before = context.evaluation_metadata.parameterization_time;
3950    let started = Instant::now();
3951    let result = (|| -> Result<GraphEvaluationResult<T>> {
3952        let model = match context.target {
3953            EvaluationTarget::Physical(model) => model,
3954            #[cfg(test)]
3955            EvaluationTarget::SamplingLaw(probe) => {
3956                return probe.evaluate(sample, context.rotation, canonical_sample, graph_id);
3957            }
3958            EvaluationTarget::Reference(reference) => {
3959                // A reference is defined in the original raw frame, including its
3960                // nonzero center. Stability rotations must not rotate the target
3961                // relative to the integration point.
3962                let raw_momenta = sample
3963                    .loop_moms()
3964                    .0
3965                    .iter()
3966                    .map(|momentum| context.rotation.inverse_rotate_three(momentum))
3967                    .collect::<LoopMomenta<_>>();
3968                let mut value = reference.evaluate(&raw_momenta)?
3969                    / sample.zero().from_usize(integrand.graph_count());
3970                if sample.sample.orientation.is_some() {
3971                    value /= sample
3972                        .zero()
3973                        .from_usize(integrand.get_graph(graph_id).get_num_orientations());
3974                }
3975                let radius_squared = raw_momenta.0.iter().fold(sample.zero(), |sum, momentum| {
3976                    sum + momentum.px.square() + momentum.py.square() + momentum.pz.square()
3977                });
3978                let mut result = GraphEvaluationResult::zero(sample.zero());
3979                result.reference_moments = Some(ReferenceMoments {
3980                    second_moment: value.clone() * radius_squared,
3981                    jacobian_min: 1.0,
3982                    jacobian_max: 1.0,
3983                });
3984                result.integrand_result = Complex::new_re(value);
3985                return Ok(result);
3986            }
3987        };
3988        let mut event_processing_runtime = integrand.take_event_processing_runtime();
3989        let result = {
3990            let graph_context = GraphTermEvaluationContext {
3991                model,
3992                settings: context.settings,
3993                event_processing_runtime: event_processing_runtime.as_mut(),
3994                rotation: context.rotation,
3995                evaluation_metadata: context.evaluation_metadata,
3996                sampling_channel: sampling_channel.map(|(channel_id, _)| channel_id),
3997                graph_id,
3998                prepared_lu_hosts: sampling_channel.map_or(&[], |(_, hosts)| hosts),
3999                canonical_sample,
4000                sampling_accuracy_budget: GammaLoopSample::<T>::relative_accuracy_budget(
4001                    context.settings,
4002                ),
4003            };
4004            integrand
4005                .get_graph_mut(graph_id)
4006                .evaluate(sample, graph_context)
4007        };
4008        integrand.restore_event_processing_runtime(event_processing_runtime);
4009        let mut result = result?;
4010        let graph_group_id = integrand.graph_group_id_for_graph(graph_id);
4011        for event_group in result.event_groups.iter_mut() {
4012            for event in event_group.iter_mut() {
4013                event.cut_info.graph_id = graph_id;
4014                event.cut_info.graph_group_id = graph_group_id;
4015            }
4016        }
4017        Ok(result)
4018    })();
4019    let nested_sampling = context.evaluation_metadata.parameterization_time - sampling_before;
4020    context.evaluation_metadata.integrand_evaluation_time +=
4021        started.elapsed().saturating_sub(nested_sampling);
4022    result
4023}
4024
4025fn selected_lmb_basis_for_default_sampling<I: ProcessIntegrandImpl>(
4026    integrand: &I,
4027    graph_id: usize,
4028    use_lmb_basis: bool,
4029) -> Result<Option<LmbIndex>> {
4030    if !use_lmb_basis {
4031        return Ok(None);
4032    }
4033
4034    let parameterization_settings = integrand
4035        .get_settings()
4036        .sampling
4037        .get_parameterization_settings()
4038        .expect("Default LMB-basis sampling requires a parameterization.");
4039    let group_id = integrand
4040        .graph_group_id_for_graph(graph_id)
4041        .map(GroupId)
4042        .ok_or_else(|| {
4043            eyre!(
4044                "Could not determine graph group for graph {} while selecting an LMB basis.",
4045                graph_id
4046            )
4047        })?;
4048
4049    Ok(Some(
4050        integrand
4051            .get_master_graph(group_id)
4052            .selected_lmb_basis_id(&parameterization_settings)?,
4053    ))
4054}
4055
4056fn evaluate_all_rotations<T: FloatLike, I: ProcessIntegrandImpl>(
4057    integrand: &mut I,
4058    target: EvaluationTarget<'_>,
4059    gammaloop_sample: &GammaLoopSample<T>,
4060    evaluation_metadata: &mut EvaluationMetaData,
4061    record_rotated_results: bool,
4062    canonical_sample: Option<&GammaLoopSample<ArbPrec>>,
4063) -> Result<(Vec<GraphEvaluationResult<T>>, usize, Vec<RotatedEvaluation>)> {
4064    let rotations = integrand.get_rotations().cloned().collect_vec();
4065
4066    let cache = integrand.get_settings().general.enable_cache;
4067
4068    let mut loop_mom_cache_id = integrand.loop_cache_id();
4069    let mut external_mom_cache_id = integrand.external_cache_id();
4070
4071    // rotate the momenta for the stability tests.
4072    let gammaloop_samples: Vec<_> = rotations
4073        .iter()
4074        .map(|rotation| {
4075            if rotation.is_identity() {
4076                return gammaloop_sample.clone();
4077            }
4078            if cache {
4079                loop_mom_cache_id += gammaloop_sample.row_count();
4080                external_mom_cache_id += 1;
4081            }
4082            gammaloop_sample.rotate(rotation, loop_mom_cache_id, external_mom_cache_id)
4083        })
4084        .collect();
4085
4086    if cache {
4087        integrand.increment_loop_cache_id(rotations.len() * gammaloop_sample.row_count());
4088        integrand.increment_external_cache_id(rotations.len());
4089        // External configurations are shared by subsequent draws; restore the
4090        // base even if a later physical rotation returns a typed retry error.
4091        integrand.revert_to_base_external_cache_id();
4092    }
4093
4094    let primary_rotation_index = rotations
4095        .iter()
4096        .position(Rotation::is_identity)
4097        .unwrap_or(0);
4098    let mut evaluation_results: Vec<GraphEvaluationResult<T>> =
4099        Vec::with_capacity(gammaloop_samples.len());
4100    for (gammaloop_sample, rotation) in gammaloop_samples.iter().zip(rotations.iter()) {
4101        debug!("Evaluating rotation: {}", rotation.method);
4102        let result = evaluate_single(
4103            integrand,
4104            target,
4105            gammaloop_sample,
4106            rotation,
4107            evaluation_metadata,
4108            canonical_sample,
4109        )?;
4110
4111        evaluation_results.push(result);
4112    }
4113
4114    for result in &evaluation_results {
4115        evaluation_metadata.event_processing_time += result.event_processing_time;
4116    }
4117
4118    let rotated_results = if record_rotated_results {
4119        rotations
4120            .iter()
4121            .zip(evaluation_results.iter())
4122            .map(|(rotation, result)| RotatedEvaluation {
4123                rotation: rotation.method.to_string(),
4124                result: complex_to_f64(&result.integrand_result),
4125            })
4126            .collect()
4127    } else {
4128        Vec::new()
4129    };
4130
4131    Ok((evaluation_results, primary_rotation_index, rotated_results))
4132}
4133
4134struct StabilityEvaluationContext<'a, 'm> {
4135    target: EvaluationTarget<'a>,
4136    source: &'a EvaluationSource<'a>,
4137    stability_level: &'a StabilityLevelSetting,
4138    max_eval: &'a Complex<F<f64>>,
4139    wgt: F<f64>,
4140    remaining_weight: Option<&'a F<ArbPrec>>,
4141    check_on_norm: bool,
4142    is_final_level: bool,
4143    evaluation_metadata: &'m mut EvaluationMetaData,
4144    record_rotated_results: bool,
4145    precision_label: &'static str,
4146    escalate_if_exact_zero: bool,
4147    check_real: bool,
4148    check_imag: bool,
4149    integral_estimate: Option<(f64, f64)>,
4150    min_abs_wgt_for_escalation: f64,
4151}
4152
4153impl StabilityEvaluationContext<'_, '_> {
4154    fn ecm_reference_scale<T: FloatLike, I: ProcessIntegrandImpl>(
4155        &self,
4156        integrand: &I,
4157        sample: &GammaLoopSample<T>,
4158    ) -> Result<Option<F<T>>> {
4159        if self.stability_level.ecm_relative_tolerance_for_re > 0.0
4160            || self.stability_level.ecm_relative_tolerance_for_im > 0.0
4161        {
4162            let e_cm = integrand.get_settings().kinematics.e_cm;
4163            if !e_cm.is_finite() || e_cm <= 0.0 {
4164                return Err(eyre!(
4165                    "E_cm-relative stability requires finite positive E_cm"
4166                ));
4167            }
4168            let momentum_sample = sample.get_default_sample();
4169            let missing_measure_dimension = if self.source.is_x_space() {
4170                0
4171            } else {
4172                let loops = momentum_sample.loop_moms().0.len();
4173                if sample
4174                    .groups
4175                    .iter()
4176                    .flat_map(|(_, rows)| rows)
4177                    .any(|row| row.sample.loop_moms().0.len() != loops)
4178                {
4179                    return Err(eyre!(
4180                        "E_cm-relative stability cannot compare a raw momentum sum with different spatial dimensions"
4181                    ));
4182                }
4183                i32::try_from(loops)?.checked_mul(3).ok_or_else(|| {
4184                    eyre!("missing spatial measure dimension exceeds the supported integer range")
4185                })?
4186            };
4187            let scale = match self.target {
4188                EvaluationTarget::Physical(_) => integrand.stability_reference_scale(
4189                    sample.groups[0].1[0].graph_id,
4190                    momentum_sample,
4191                    missing_measure_dimension,
4192                )?,
4193                EvaluationTarget::Reference(_) => {
4194                    F::<T>::from_f64(integrand.get_settings().kinematics.e_cm)
4195                        .powi(-missing_measure_dimension)
4196                }
4197                #[cfg(test)]
4198                EvaluationTarget::SamplingLaw(_) => momentum_sample.one(),
4199            };
4200            if !scale.0.is_finite() || scale <= momentum_sample.zero() {
4201                return Err(eyre!(
4202                    "E_cm-relative stability scale must be finite and positive"
4203                ));
4204            }
4205            Ok(Some(scale))
4206        } else {
4207            Ok(None)
4208        }
4209    }
4210}
4211
4212fn evaluate_stability_level_precise<T: FloatLike, I: ProcessIntegrandImpl>(
4213    integrand: &mut I,
4214    context: &mut StabilityEvaluationContext<'_, '_>,
4215) -> Result<PreciseStabilityLevelResult<T>> {
4216    let level_start = Instant::now();
4217    let evaluated = if context
4218        .evaluation_metadata
4219        .threshold_counterterm_error
4220        .is_some()
4221    {
4222        None
4223    } else {
4224        let evaluated = (|| {
4225            let sample = context
4226                .source
4227                .build_gamma_sample::<T, I>(integrand, context.evaluation_metadata)?;
4228            let ecm_scale = context.ecm_reference_scale(integrand, &sample)?;
4229            debug!("{} parameterization succeeded", context.precision_label);
4230            debug!("jacobian: {:+16e}", sample.get_default_sample().jacobian());
4231            let rotations = evaluate_all_rotations(
4232                integrand,
4233                context.target,
4234                &sample,
4235                context.evaluation_metadata,
4236                context.record_rotated_results,
4237                context.source.canonical_sample(),
4238            )?;
4239            Ok::<_, eyre::Report>((sample, ecm_scale, rotations))
4240        })();
4241        match evaluated {
4242            Ok(evaluated) => Some(evaluated),
4243            Err(error)
4244                if matches!(context.target, EvaluationTarget::Physical(_))
4245                    && matches!(
4246                        error.downcast_ref::<sampling::maps::SamplingEvaluationError>(),
4247                        Some(
4248                            sampling::maps::SamplingEvaluationError::UncertifiedOverlap { .. }
4249                                | sampling::maps::SamplingEvaluationError::UncertifiedRoot { .. }
4250                        )
4251                    ) =>
4252            {
4253                // A failed native pass may have populated only part of its
4254                // point caches; use fresh identities for the following lane.
4255                integrand.increment_loop_cache_id(integrand.get_rotations().count() + 1);
4256                integrand.revert_to_base_external_cache_id();
4257                context
4258                    .evaluation_metadata
4259                    .record_threshold_counterterm_error(format!("{error:#}"));
4260                None
4261            }
4262            Err(error) => return Err(error),
4263        }
4264    };
4265    if context
4266        .evaluation_metadata
4267        .threshold_counterterm_error
4268        .is_some()
4269    {
4270        // An uncertified cut/overlap invalidates the whole sample, including
4271        // partial channels and events. Retry native precision through the same
4272        // driver; an exhausted failure remains explicitly marked NaN.
4273        let mut graph_result = GraphEvaluationResult::zero(F::<T>::from_f64(0.0));
4274        graph_result.integrand_result = Complex::new(F::from_f64(f64::NAN), F::from_f64(f64::NAN));
4275        return Ok(PreciseStabilityLevelResult {
4276            result: graph_result.integrand_result.clone(),
4277            graph_result,
4278            stability_level_used: context.stability_level.precision,
4279            estimated_relative_accuracy: None,
4280            sample_count: 0,
4281            total_time: level_start.elapsed(),
4282            parameterization_jacobian: context.source.is_x_space().then(|| F::from_f64(1.0)),
4283            is_stable: false,
4284            rotated_results: Vec::new(),
4285        });
4286    }
4287    let (gammaloop_sample, ecm_scale, (graph_results, primary_rotation_index, rotated_results)) =
4288        evaluated.expect("a successful level has evaluated rotations");
4289    let results = graph_results
4290        .iter()
4291        .map(|result| result.integrand_result.clone())
4292        .collect_vec();
4293
4294    let mut max_eval = complex_from_f64::<T>(context.max_eval);
4295    if matches!(context.target, EvaluationTarget::Reference(_)) {
4296        // Integration reports a second observable in Im. Its scale must not
4297        // alter the Gaussian value's existing independent stability check.
4298        max_eval.im = max_eval.im.zero();
4299    }
4300    let mut wgt = F::<T>::from_ff64(context.wgt);
4301    if let Some(remaining_weight) = context.remaining_weight {
4302        // A profiler applies its measure after native evaluation. Include it
4303        // in every stability bound before accepting an underflowed component.
4304        wgt *= F::<T>::from_arb(&remaining_weight.0)?;
4305    }
4306
4307    let (average_result, mut estimated_relative_accuracy, mut is_stable, _instability_reason) =
4308        if context.check_on_norm {
4309            stability_check_on_norm_components_with_estimate(
4310                ecm_scale.as_ref(),
4311                &results,
4312                context.stability_level,
4313                max_eval,
4314                wgt.clone(),
4315                context.is_final_level,
4316                context.escalate_if_exact_zero,
4317                context.check_real,
4318                context.check_imag,
4319                context.integral_estimate,
4320                context.min_abs_wgt_for_escalation,
4321            )
4322        } else {
4323            stability_check_components_with_estimate(
4324                ecm_scale.as_ref(),
4325                &results,
4326                context.stability_level,
4327                max_eval,
4328                wgt.clone(),
4329                context.is_final_level,
4330                context.escalate_if_exact_zero,
4331                context.check_real,
4332                context.check_imag,
4333                context.integral_estimate,
4334                context.min_abs_wgt_for_escalation,
4335            )
4336        };
4337
4338    let mut graph_result = graph_results[primary_rotation_index].clone();
4339    graph_result.integrand_result = average_result.clone();
4340    if graph_result.absolute_integrand_result.is_some() {
4341        // The physical absolute integral is a separate observable: cancellation
4342        // between different channel points must not hide an unstable absolute
4343        // contribution. Reuse the configured component/norm criterion, without
4344        // borrowing the signed integral's maximum-weight escalation scale.
4345        // Helicity amplitudes acquire a common phase under rotation. Their
4346        // cancellation guard therefore uses sum_c |z_c|, while the public
4347        // |Re|/|Im| report remains the primary probe's physical components.
4348        let check_channel_norm = graph_result.channel_norm_sum.is_some();
4349        let absolute_results = graph_results
4350            .iter()
4351            .map(|result| {
4352                if check_channel_norm {
4353                    result.channel_norm_sum.clone().map(Complex::new_re)
4354                } else {
4355                    result.absolute_integrand_result.clone()
4356                }
4357                .ok_or_else(|| {
4358                    eyre!("sampling-channel absolute contribution missing from a rotation")
4359                })
4360            })
4361            .collect::<Result<Vec<_>>>()?;
4362        let (absolute_check_real, absolute_check_imag) = if check_channel_norm {
4363            (true, false)
4364        } else {
4365            (context.check_real, context.check_imag)
4366        };
4367        let mut absolute_level = *context.stability_level;
4368        if check_channel_norm {
4369            // Either component can carry the norm after a phase rotation; a
4370            // one-component absolute allowance must not relax this monitor.
4371            absolute_level.ecm_relative_tolerance_for_re = absolute_level
4372                .ecm_relative_tolerance_for_re
4373                .min(absolute_level.ecm_relative_tolerance_for_im);
4374        }
4375        let check = if context.check_on_norm {
4376            stability_check_on_norm_components_with_estimate::<T>
4377        } else {
4378            stability_check_components_with_estimate::<T>
4379        };
4380        let (absolute, accuracy, stable, _) = check(
4381            ecm_scale.as_ref(),
4382            &absolute_results,
4383            &absolute_level,
4384            Complex::new_re(average_result.re.zero()),
4385            wgt.clone(),
4386            context.is_final_level,
4387            context.escalate_if_exact_zero,
4388            absolute_check_real,
4389            absolute_check_imag,
4390            context.integral_estimate,
4391            context.min_abs_wgt_for_escalation,
4392        );
4393        if !check_channel_norm {
4394            graph_result.absolute_integrand_result = Some(absolute);
4395        }
4396        estimated_relative_accuracy = estimated_relative_accuracy
4397            .into_iter()
4398            .chain(accuracy)
4399            .reduce(|left, right| left.max(right));
4400        is_stable &= stable;
4401    }
4402    if matches!(context.target, EvaluationTarget::Reference(_)) {
4403        // The Gaussian value and its raw-momentum moment are independent
4404        // observables. Reuse the scalar stability owner for the moment too;
4405        // agreement of the value alone cannot certify rotation invariance.
4406        let moments = graph_results
4407            .iter()
4408            .map(|result| {
4409                result
4410                    .reference_moments
4411                    .as_ref()
4412                    .map(|moments| Complex::new_re(moments.second_moment.clone()))
4413                    .ok_or_else(|| eyre!("reference acceptance produced no mapped moment"))
4414            })
4415            .collect::<Result<Vec<_>>>()?;
4416        let mut moment_level = *context.stability_level;
4417        moment_level.required_precision_for_re = moment_level
4418            .required_precision_for_re
4419            .min(moment_level.required_precision_for_im);
4420        moment_level.required_precision_for_im = moment_level.required_precision_for_re;
4421        // Like its relative precision, the independent moment uses the stricter
4422        // component allowance: a Re-only allowance does not relax this oracle.
4423        moment_level.ecm_relative_tolerance_for_re = moment_level
4424            .ecm_relative_tolerance_for_re
4425            .min(moment_level.ecm_relative_tolerance_for_im);
4426        moment_level.ecm_relative_tolerance_for_im = moment_level.ecm_relative_tolerance_for_re;
4427        // The raw second moment has two additional energy dimensions. Its
4428        // independent oracle uses E_cm^2 times the reference normalization scale.
4429        let moment_scale = ecm_scale.as_ref().map(|scale| {
4430            scale * F::<T>::from_f64(integrand.get_settings().kinematics.e_cm).square()
4431        });
4432        if moment_scale
4433            .as_ref()
4434            .is_some_and(|scale| !scale.0.is_finite() || scale <= &scale.zero())
4435        {
4436            return Err(eyre!(
4437                "E_cm-relative reference-moment scale must be finite and positive"
4438            ));
4439        }
4440        let (moment, accuracy, stable, _) = stability_check_with_estimate(
4441            moment_scale.as_ref(),
4442            &moments,
4443            &moment_level,
4444            Complex::new_re(average_result.re.zero()),
4445            wgt.clone(),
4446            context.is_final_level,
4447            context.escalate_if_exact_zero,
4448            context.integral_estimate,
4449            context.min_abs_wgt_for_escalation,
4450        );
4451        graph_result
4452            .reference_moments
4453            .as_mut()
4454            .unwrap()
4455            .second_moment = moment.re;
4456        estimated_relative_accuracy = estimated_relative_accuracy
4457            .into_iter()
4458            .chain(accuracy)
4459            .reduce(|left, right| left.max(right));
4460        is_stable &= stable;
4461    }
4462
4463    Ok(PreciseStabilityLevelResult {
4464        result: average_result,
4465        graph_result,
4466        stability_level_used: context.stability_level.precision,
4467        estimated_relative_accuracy,
4468        sample_count: results.len(),
4469        total_time: level_start.elapsed(),
4470        parameterization_jacobian: context
4471            .source
4472            .is_x_space()
4473            .then(|| gammaloop_sample.get_default_sample().one()),
4474        is_stable,
4475        rotated_results,
4476    })
4477}
4478
4479/// Result of cache validation checks
4480#[derive(Debug, Clone)]
4481pub struct CacheValidationResult {
4482    pub is_valid: bool,
4483    pub current_external_cache_id: usize,
4484    pub base_external_cache_id: usize,
4485    pub loop_cache_id: usize,
4486    pub has_rotations: bool,
4487    pub cache_efficiency: f64,
4488    pub diagnostics: String,
4489}
4490
4491impl std::fmt::Display for CacheValidationResult {
4492    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4493        write!(
4494            f,
4495            "Cache Validation: {} | Current: {} | Base: {} | Loop: {} | Rotations: {} | Efficiency: {:.1}%",
4496            if self.is_valid { "✓" } else { "✗" },
4497            self.current_external_cache_id,
4498            self.base_external_cache_id,
4499            self.loop_cache_id,
4500            if self.has_rotations { "Yes" } else { "No" },
4501            self.cache_efficiency * 100.0
4502        )
4503    }
4504}
4505
4506/// Cache usage statistics
4507#[derive(Debug, Clone)]
4508pub struct CacheStats {
4509    pub total_external_increments: usize,
4510    pub total_loop_increments: usize,
4511    pub base_configurations: usize,
4512    pub rotational_variants: usize,
4513    pub efficiency_ratio: f64,
4514}
4515
4516impl std::fmt::Display for CacheStats {
4517    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4518        write!(
4519            f,
4520            "Cache Stats: {} base configs, {} rotations, {} loop increments, {:.1}% efficiency",
4521            self.base_configurations,
4522            self.rotational_variants,
4523            self.total_loop_increments,
4524            self.efficiency_ratio * 100.0
4525        )
4526    }
4527}
4528
4529/// Helper macro for cache debugging
4530#[macro_export]
4531macro_rules! debug_cache {
4532    ($integrand:expr, $msg:expr) => {
4533        let validation = $integrand.validate_cache_consistency();
4534        tracing::debug!("{}: {}", $msg, validation);
4535    };
4536}
4537
4538/// Helper macro for cache monitoring with custom conditions
4539#[macro_export]
4540macro_rules! monitor_cache {
4541    ($integrand:expr, $condition:expr, $msg:expr) => {
4542        if $condition {
4543            let stats = $integrand.get_cache_stats();
4544            tracing::info!("{}: {}", $msg, stats);
4545        }
4546    };
4547}
4548
4549/// Helper macro for missed cache hit detection in debug mode
4550#[macro_export]
4551macro_rules! debug_cache_search {
4552    ($integrand:expr, $msg:expr) => {
4553        if std::env::var("GAMMALOOP_DEBUG_CACHE").is_ok() {
4554            let validation = $integrand.validate_cache_consistency();
4555            if !validation.is_valid {
4556                tracing::error!(
4557                    "CACHE CORRUPTION DETECTED at {}: {}",
4558                    $msg,
4559                    validation.diagnostics
4560                );
4561            } else {
4562                tracing::debug!("Cache search at {}: {}", $msg, validation);
4563            }
4564        }
4565    };
4566}
4567
4568/// Helper macro for cache efficiency warnings
4569#[macro_export]
4570macro_rules! warn_cache_efficiency {
4571    ($integrand:expr, $threshold:expr, $msg:expr) => {
4572        let stats = $integrand.get_cache_stats();
4573        if stats.efficiency_ratio < $threshold {
4574            tracing::warn!(
4575                "⚠️ Low cache efficiency at {}: {:.1}% (threshold: {:.1}%)",
4576                $msg,
4577                stats.efficiency_ratio * 100.0,
4578                $threshold * 100.0
4579            );
4580            tracing::warn!("   Cache stats: {}", stats);
4581        }
4582    };
4583}
4584
4585fn evaluate_single<T: FloatLike, I: ProcessIntegrandImpl>(
4586    integrand: &mut I,
4587    target: EvaluationTarget<'_>,
4588    gammaloop_sample: &GammaLoopSample<T>,
4589    rotation: &Rotation,
4590    evaluation_metadata: &mut EvaluationMetaData,
4591    canonical_sample: Option<&GammaLoopSample<ArbPrec>>,
4592) -> Result<GraphEvaluationResult<T>> {
4593    let settings = integrand.get_settings().clone();
4594    let zero = gammaloop_sample.get_default_sample().zero();
4595    let mut context = EvaluationContext {
4596        target,
4597        settings: &settings,
4598        rotation,
4599        evaluation_metadata,
4600    };
4601    let mut result = GraphEvaluationResult::zero(zero.clone());
4602    let mut absolute = Complex::new_re(zero.clone());
4603    let mut channel_norm_sum = (matches!(target, EvaluationTarget::Physical(_))
4604        && settings.stability.check_on_norm
4605        && !matches!(
4606            integrand.get_dependent_momenta_constructor(),
4607            DependentMomentaConstructor::CrossSection
4608        ))
4609    .then(|| zero.clone());
4610    let mut has_sampling_channels = false;
4611    for (group_index, (group_id, rows)) in gammaloop_sample.groups.iter().enumerate() {
4612        let mut grouped_events = crate::observables::GenericEventGroup::default();
4613        let mut channel_values = BTreeMap::new();
4614        for (row_index, row) in rows.iter().enumerate() {
4615            let canonical_row =
4616                canonical_sample.map(|sample| &sample.groups[group_index].1[row_index]);
4617            let mut graph_result = evaluate_graph_term(
4618                integrand,
4619                row.graph_id,
4620                &row.sample,
4621                &mut context,
4622                row.channel_id
4623                    .map(|id| (id, row.prepared_lu_hosts.as_slice())),
4624                canonical_row,
4625            )?;
4626            // Preserve tropical's integrand-only compensation. All ordinary
4627            // map/partition factors reach integrand, events and reference moments
4628            // once through the same per-row owner, including explicit sums.
4629            graph_result.integrand_result *= Complex::new_re(row.integrand_prefactor.clone());
4630            graph_result.apply_sampling_factor(row.sample.jacobian());
4631            has_sampling_channels |= row.channel_id.is_some();
4632            // The source owner prepares one master point per channel and clones
4633            // it across group members. Sum their complete physical bodies here;
4634            // only distinct channel points contribute separate absolute values.
4635            *channel_values
4636                .entry(row.channel_id)
4637                .or_insert_with(|| Complex::new_re(zero.clone())) +=
4638                graph_result.integrand_result.clone();
4639            if group_id.is_some() {
4640                for mut events in graph_result.event_groups.drain(..) {
4641                    grouped_events.append(&mut events);
4642                }
4643            }
4644            result.merge_in_place(graph_result);
4645        }
4646        if !grouped_events.is_empty() {
4647            result.event_groups.push(grouped_events);
4648        }
4649        for value in channel_values.into_values() {
4650            if let Some(norm) = &mut channel_norm_sum {
4651                *norm += value.re.hypot(&value.im);
4652            }
4653            absolute += Complex::new(value.re.abs(), value.im.abs());
4654        }
4655    }
4656    result.channel_norm_sum = channel_norm_sum.map(|norm| {
4657        if has_sampling_channels {
4658            norm
4659        } else {
4660            result
4661                .integrand_result
4662                .re
4663                .hypot(&result.integrand_result.im)
4664        }
4665    });
4666    result.absolute_integrand_result = Some(if has_sampling_channels {
4667        absolute
4668    } else {
4669        Complex::new(
4670            result.integrand_result.re.abs(),
4671            result.integrand_result.im.abs(),
4672        )
4673    });
4674    Ok(result)
4675}
4676
4677fn create_grid_for_graph<G: GraphTerm>(
4678    graph_term: &G,
4679    settings: &DiscreteGraphSamplingSettings,
4680    integrator_settings: &IntegratorSettings,
4681) -> Grid<F<f64>> {
4682    match &settings.sampling_type {
4683        DiscreteGraphSamplingType::Default(_) | DiscreteGraphSamplingType::MultiChanneling(_) => {
4684            let continuous_grid = create_default_continous_grid(graph_term, integrator_settings);
4685
4686            if settings.sample_orientations {
4687                let continuous_grids = (0..graph_term.get_num_orientations())
4688                    .map(|_| Some(continuous_grid.clone()))
4689                    .collect();
4690
4691                Grid::Discrete(
4692                    DiscreteGrid::new(
4693                        continuous_grids,
4694                        F(integrator_settings.max_prob_ratio),
4695                        integrator_settings.train_on_avg,
4696                    )
4697                    .expect("orientation sampling requires at least one orientation"),
4698                )
4699            } else {
4700                continuous_grid
4701            }
4702        }
4703        DiscreteGraphSamplingType::SamplingMultiChanneling(multichanneling_settings) => {
4704            let continuous_grid = create_default_continous_grid(graph_term, integrator_settings);
4705            let channel_count = graph_term
4706                .sampling_channel_ids(&multichanneling_settings.parameterization_settings)
4707                .map(|channel_ids| channel_ids.len())
4708                .unwrap_or_else(|error| panic!("cannot build the sampling channel grid: {error}"));
4709            let lmb_channel_grid = Grid::Discrete(
4710                DiscreteGrid::new(
4711                    (0..channel_count)
4712                        .map(|_| Some(continuous_grid.clone()))
4713                        .collect_vec(),
4714                    F(integrator_settings.max_prob_ratio),
4715                    integrator_settings.train_on_avg,
4716                )
4717                .expect("discrete multichanneling requires at least one LMB channel"),
4718            );
4719
4720            if settings.sample_orientations {
4721                Grid::Discrete(
4722                    DiscreteGrid::new(
4723                        (0..graph_term.get_num_orientations())
4724                            .map(|_| Some(lmb_channel_grid.clone()))
4725                            .collect(),
4726                        F(integrator_settings.max_prob_ratio),
4727                        integrator_settings.train_on_avg,
4728                    )
4729                    .expect("orientation sampling requires at least one orientation"),
4730                )
4731            } else {
4732                lmb_channel_grid
4733            }
4734        }
4735
4736        DiscreteGraphSamplingType::TropicalSampling(_) => {
4737            let dimension = get_n_dim_for_n_loop_momenta(
4738                &SamplingSettings::DiscreteGraphs(settings.clone()),
4739                graph_term.get_graph().get_loop_number(),
4740                Some(graph_term.get_graph().iter_loop_edges().count()),
4741            );
4742
4743            let continious_grid = Grid::Continuous(
4744                ContinuousGrid::new(
4745                    dimension,
4746                    integrator_settings.n_bins,
4747                    integrator_settings.min_samples_for_update,
4748                    integrator_settings.bin_number_evolution.clone(),
4749                    integrator_settings.train_on_avg,
4750                )
4751                .expect("tropical sampling requires valid continuous-grid settings"),
4752            );
4753
4754            if settings.sample_orientations {
4755                let continuous_grids = (0..graph_term.get_num_orientations())
4756                    .map(|_| Some(continious_grid.clone()))
4757                    .collect();
4758
4759                Grid::Discrete(
4760                    DiscreteGrid::new(
4761                        continuous_grids,
4762                        F(integrator_settings.max_prob_ratio),
4763                        integrator_settings.train_on_avg,
4764                    )
4765                    .expect("orientation sampling requires at least one orientation"),
4766                )
4767            } else {
4768                continious_grid
4769            }
4770        }
4771    }
4772}
4773
4774fn create_default_continous_grid<G: GraphTerm>(
4775    graph_term: &G,
4776    integrator_settings: &IntegratorSettings,
4777) -> Grid<F<f64>> {
4778    Grid::Continuous(
4779        ContinuousGrid::new(
4780            graph_term.get_graph().get_loop_number() * 3,
4781            integrator_settings.n_bins,
4782            integrator_settings.min_samples_for_update,
4783            integrator_settings.bin_number_evolution.clone(),
4784            integrator_settings.train_on_avg,
4785        )
4786        .expect("graph integration requires valid continuous-grid settings"),
4787    )
4788}
4789
4790fn create_grid<I: ProcessIntegrandImpl>(integrand: &I) -> Grid<F<f64>> {
4791    let settings = integrand.get_settings();
4792    match &settings.sampling {
4793        SamplingSettings::Default(_) => Grid::Continuous(
4794            ContinuousGrid::new(
4795                get_global_dimension_if_exists(integrand).unwrap(),
4796                settings.integrator.n_bins,
4797                settings.integrator.min_samples_for_update,
4798                settings.integrator.bin_number_evolution.clone(),
4799                settings.integrator.train_on_avg,
4800            )
4801            .expect("process integration requires valid continuous-grid settings"),
4802        ),
4803        SamplingSettings::MultiChanneling(_) => Grid::Continuous(
4804            ContinuousGrid::new(
4805                get_global_dimension_if_exists(integrand).unwrap(),
4806                settings.integrator.n_bins,
4807                settings.integrator.min_samples_for_update,
4808                settings.integrator.bin_number_evolution.clone(),
4809                settings.integrator.train_on_avg,
4810            )
4811            .expect("multichannel integration requires valid continuous-grid settings"),
4812        ),
4813        SamplingSettings::DiscreteGraphs(discrete_graph_sampling_settings) => Grid::Discrete(
4814            DiscreteGrid::new(
4815                integrand
4816                    .get_group_masters()
4817                    .map(|term| {
4818                        Some(create_grid_for_graph(
4819                            term,
4820                            discrete_graph_sampling_settings,
4821                            &settings.integrator,
4822                        ))
4823                    })
4824                    .collect(),
4825                F(settings.integrator.max_prob_ratio),
4826                settings.integrator.train_on_avg,
4827            )
4828            .expect("discrete graph sampling requires at least one graph group"),
4829        ),
4830    }
4831}
4832
4833#[derive(Clone, Copy)]
4834enum EvaluationSource<'a> {
4835    XSpace(&'a Sample<F<f64>>),
4836    Momentum {
4837        input: &'a MomentumSpaceEvaluationInput,
4838        candidate_lmb: Option<&'a LoopMomentumBasis>,
4839    },
4840    /// Borrowed from the one original-source preparation outside the physical
4841    /// retry stack. Retain source kind for reporting, not for native remapping.
4842    Prepared {
4843        sample: &'a GammaLoopSample<ArbPrec>,
4844        original: &'a EvaluationSource<'a>,
4845    },
4846}
4847
4848impl<'a> EvaluationSource<'a> {
4849    fn canonical_sample(&self) -> Option<&'a GammaLoopSample<ArbPrec>> {
4850        match self {
4851            Self::Prepared { sample, .. } => Some(sample),
4852            _ => None,
4853        }
4854    }
4855
4856    fn is_x_space(&self) -> bool {
4857        match self {
4858            Self::XSpace(_) => true,
4859            Self::Momentum { .. } => false,
4860            Self::Prepared { original, .. } => original.is_x_space(),
4861        }
4862    }
4863
4864    /// Prepare every participating row once at the fixed proposal precision.
4865    /// Failure ends before physical bodies: choosing another map precision after
4866    /// inspecting the active point would change the generated proposal law.
4867    fn prepare_draw<I: ProcessIntegrandImpl>(
4868        &self,
4869        integrand: &mut I,
4870        metadata: &mut EvaluationMetaData,
4871    ) -> Result<Option<GammaLoopSample<ArbPrec>>> {
4872        if matches!(self, Self::Prepared { .. }) {
4873            return Ok(None);
4874        }
4875        let started = Instant::now();
4876        let history = std::mem::take(&mut metadata.radial_root_diagnostics);
4877        metadata.sampling_proposal_policies.begin_collection();
4878        let result = (|| {
4879            let precision = if integrand.get_settings().sampling.uses_sampling_channels()
4880                && !matches!(self, Self::Momentum { input, .. } if input.channel_id.is_none())
4881            {
4882                integrand
4883                    .get_graph(0)
4884                    .sampling_setup()
4885                    .sampling_source
4886                    .as_ref()
4887                    .ok_or_else(|| eyre!("fixed sampling source is not initialized; call warm_up"))?
4888                    .0
4889            } else {
4890                SamplingPrecision::Arb
4891            };
4892            match (self, precision) {
4893                (Self::XSpace(sample), SamplingPrecision::Quad) => {
4894                    parameterize::<f128, I>(sample, integrand, metadata)?.into_canonical(
4895                        &integrand.get_settings().kinematics.externals,
4896                        integrand.get_dependent_momenta_constructor(),
4897                    )
4898                }
4899                (
4900                    Self::Momentum {
4901                        input,
4902                        candidate_lmb,
4903                    },
4904                    SamplingPrecision::Quad,
4905                ) => {
4906                    integrand.prepare_sampling_precision::<f128>()?;
4907                    build_direct_gamma_sample::<f128, I>(
4908                        integrand,
4909                        input,
4910                        *candidate_lmb,
4911                        metadata,
4912                    )?
4913                    .into_canonical(
4914                        &integrand.get_settings().kinematics.externals,
4915                        integrand.get_dependent_momenta_constructor(),
4916                    )
4917                }
4918                (Self::XSpace(sample), SamplingPrecision::Fixed256) => {
4919                    parameterize::<SamplingFloat, I>(sample, integrand, metadata)?.into_canonical(
4920                        &integrand.get_settings().kinematics.externals,
4921                        integrand.get_dependent_momenta_constructor(),
4922                    )
4923                }
4924                (
4925                    Self::Momentum {
4926                        input,
4927                        candidate_lmb,
4928                    },
4929                    SamplingPrecision::Fixed256,
4930                ) => {
4931                    integrand.prepare_sampling_precision::<SamplingFloat>()?;
4932                    build_direct_gamma_sample::<SamplingFloat, I>(
4933                        integrand,
4934                        input,
4935                        *candidate_lmb,
4936                        metadata,
4937                    )?
4938                    .into_canonical(
4939                        &integrand.get_settings().kinematics.externals,
4940                        integrand.get_dependent_momenta_constructor(),
4941                    )
4942                }
4943                (Self::XSpace(sample), SamplingPrecision::Arb) => {
4944                    parameterize::<ArbPrec, I>(sample, integrand, metadata)
4945                }
4946                (
4947                    Self::Momentum {
4948                        input,
4949                        candidate_lmb,
4950                    },
4951                    SamplingPrecision::Arb,
4952                ) => {
4953                    if input.channel_id.is_some() {
4954                        integrand.prepare_sampling_precision::<ArbPrec>()?;
4955                    }
4956                    build_direct_gamma_sample::<ArbPrec, I>(
4957                        integrand,
4958                        input,
4959                        *candidate_lmb,
4960                        metadata,
4961                    )
4962                }
4963                _ => unreachable!("prepared sources and Double source policies are excluded"),
4964            }
4965        })();
4966        // Canonical roots and foreign inverses never consume native physical
4967        // retry occurrences. Preserve the discrete decisions and inclusive cost.
4968        metadata.radial_root_diagnostics = history;
4969        metadata.sampling_proposal_policies.seal();
4970        let elapsed = started.elapsed();
4971        metadata.canonical_sampling_preparation_time += elapsed;
4972        metadata.parameterization_time += elapsed;
4973        result
4974            .map(Some)
4975            .wrap_err("canonical sampling preparation failed at the fixed source precision")
4976    }
4977
4978    fn build_gamma_sample<T: FloatLike, I: ProcessIntegrandImpl>(
4979        &self,
4980        integrand: &mut I,
4981        metadata: &mut EvaluationMetaData,
4982    ) -> Result<GammaLoopSample<T>> {
4983        // Raw callers outside the stability driver still use the same fixed
4984        // proposal preparation; the production driver retains its output once.
4985        let prepared = if !matches!(self, Self::Prepared { .. }) {
4986            self.prepare_draw(integrand, metadata)?
4987        } else {
4988            None
4989        };
4990        let started = Instant::now();
4991        let result = (|| {
4992            let mut sample = prepared
4993                .as_ref()
4994                .or(self.canonical_sample())
4995                .expect("every original source has a canonical row")
4996                .materialize::<T>(GammaLoopSample::<T>::relative_accuracy_budget(
4997                    integrand.get_settings(),
4998                ))?;
4999            // Distinct graph/channel rows may share one evaluator cache. Give
5000            // every completed point its own identity, including base identities
5001            // used by rotation-aware caches; retries start with a fresh range.
5002            let first_id = integrand.loop_cache_id();
5003            for (index, row) in sample
5004                .groups
5005                .iter_mut()
5006                .flat_map(|(_, rows)| rows)
5007                .enumerate()
5008            {
5009                row.sample.sample.loop_mom_cache_id = first_id + index;
5010                row.sample.sample.loop_mom_base_cache_id = first_id + index;
5011            }
5012            if integrand.get_settings().general.enable_cache {
5013                integrand.increment_loop_cache_id(sample.row_count());
5014            }
5015            Ok(sample)
5016        })();
5017        metadata.parameterization_time += started.elapsed();
5018        result
5019    }
5020
5021    fn loop_norm_sum<I: ProcessIntegrandImpl>(
5022        &self,
5023        integrand: &mut I,
5024        metadata: &mut EvaluationMetaData,
5025    ) -> Result<F<f64>> {
5026        if let Self::Momentum { input, .. } = self {
5027            return Ok(sum_loop_norms(input.loop_momenta.iter()));
5028        }
5029        // Inspect only canonical momenta: a norm prepass must not reject an
5030        // otherwise usable Arb draw because its Jacobian is outside f64 range.
5031        let prepared = if self.canonical_sample().is_none() {
5032            self.prepare_draw(integrand, metadata)?
5033        } else {
5034            None
5035        };
5036        let sample = self
5037            .canonical_sample()
5038            .or(prepared.as_ref())
5039            .expect("mapped source has prepared rows");
5040        let started = Instant::now();
5041        let zero = sample.get_default_sample().zero();
5042        let largest = sample
5043            .groups
5044            .iter()
5045            .flat_map(|(_, rows)| rows)
5046            .map(|row| {
5047                row.sample
5048                    .loop_moms()
5049                    .0
5050                    .iter()
5051                    .fold(zero.clone(), |sum, momentum| sum + momentum.norm())
5052            })
5053            .fold(zero.clone(), |a, b| if a > b { a } else { b });
5054        metadata.parameterization_time += started.elapsed();
5055        Ok(largest.into_ff64())
5056    }
5057
5058    fn debug_sample<I: ProcessIntegrandImpl>(
5059        &self,
5060        integrand: &mut I,
5061        metadata: &mut EvaluationMetaData,
5062    ) -> Result<GammaLoopSample<f64>> {
5063        self.build_gamma_sample::<f64, I>(integrand, metadata)
5064    }
5065}
5066
5067fn sum_loop_norms<'a>(loop_momenta: impl Iterator<Item = &'a ThreeMomentum<F<f64>>>) -> F<f64> {
5068    loop_momenta.fold(F(0.0), |acc, momentum| acc + momentum.norm())
5069}
5070
5071fn build_direct_gamma_sample<T: FloatLike, I: ProcessIntegrandImpl>(
5072    integrand: &mut I,
5073    input: &MomentumSpaceEvaluationInput,
5074    candidate_lmb: Option<&LoopMomentumBasis>,
5075    metadata: &mut EvaluationMetaData,
5076) -> Result<GammaLoopSample<T>> {
5077    if integrand.uses_explicit_orientation_sum_only() && input.orientation.is_some() {
5078        return Err(eyre!(
5079            "`global.generation.explicit_orientation_sum_only = true` represents all orientations as a single summed contribution, so explicit orientation selection is not supported"
5080        ));
5081    }
5082
5083    let expected_loop_count = if let Some(graph_id) = input.graph_id {
5084        let group_id = integrand
5085            .get_group_structure()
5086            .iter_enumerated()
5087            .find_map(|(group_id, group)| group.into_iter().contains(&graph_id).then_some(group_id))
5088            .ok_or_else(|| eyre!("Unknown graph '{}' in momentum-space evaluation.", graph_id))?;
5089        integrand
5090            .get_master_graph(group_id)
5091            .get_graph()
5092            .get_loop_number()
5093    } else if let Some(group_id) = input.group_id {
5094        if group_id.0 >= integrand.get_group_structure().len() {
5095            return Err(eyre!(
5096                "Unknown graph group '{}' in momentum-space evaluation.",
5097                group_id.0
5098            ));
5099        }
5100        integrand
5101            .get_master_graph(group_id)
5102            .get_graph()
5103            .get_loop_number()
5104    } else {
5105        integrand
5106            .get_group_masters()
5107            .next()
5108            .map(|graph| graph.get_graph().get_loop_number())
5109            .ok_or_else(|| eyre!("Cannot evaluate an integrand with no graph terms."))?
5110    };
5111
5112    if input.loop_momenta.len() != expected_loop_count {
5113        return Err(eyre!(
5114            "Expected {} loop momenta in momentum-space evaluation, got {}.",
5115            expected_loop_count,
5116            input.loop_momenta.len()
5117        ));
5118    }
5119
5120    // A direct input already denotes a binary64 point. Preserve those exact
5121    // tokens in every lane; the decimal conversion policy for settings is unchanged.
5122    let loop_momenta = input
5123        .loop_momenta
5124        .iter()
5125        .map(|momentum| {
5126            ThreeMomentum::new(
5127                F(T::from_f64_exact_binary(momentum.px.0)),
5128                F(T::from_f64_exact_binary(momentum.py.0)),
5129                F(T::from_f64_exact_binary(momentum.pz.0)),
5130            )
5131        })
5132        .collect::<LoopMomenta<F<T>>>();
5133    let sample = MomentumSample::new(
5134        loop_momenta,
5135        integrand.loop_cache_id(),
5136        &integrand.get_settings().kinematics.externals,
5137        integrand.get_current_external_cache_id(),
5138        F::<T>::from_f64(1.0),
5139        integrand.get_dependent_momenta_constructor(),
5140        input.orientation,
5141    )?;
5142
5143    if let Some(graph_id) = input.graph_id {
5144        if input.group_id.is_some() || input.channel_id.is_some() {
5145            return Err(eyre!(
5146                "Explicit graph selection is mutually exclusive with discrete graph/channel selections in momentum-space evaluation."
5147            ));
5148        }
5149        // Profile rays use a candidate basis. Convert only after the exact
5150        // input tokens have reached the fixed source precision, and target this
5151        // graph rather than its group master. The canonical row then owns the
5152        // completed point for every native materialization and stability rotation.
5153        let sample = if let Some(from) = candidate_lmb {
5154            let to = &integrand
5155                .get_graph(graph_id)
5156                .get_graph()
5157                .loop_momentum_basis;
5158            sample.lmb_transform(from, to)
5159        } else {
5160            sample
5161        };
5162        return Ok(GammaLoopSample {
5163            groups: vec![(
5164                None,
5165                vec![DiscreteGraphSample {
5166                    graph_id,
5167                    channel_id: None,
5168                    prepared_lu_hosts: vec![],
5169                    physical_overlaps: None,
5170                    integrand_prefactor: sample.one(),
5171                    sample,
5172                }],
5173            )],
5174        });
5175    }
5176
5177    if candidate_lmb.is_some() {
5178        return Err(eyre!(
5179            "Candidate LMB evaluation requires explicit graph selection."
5180        ));
5181    }
5182
5183    match &integrand.get_settings().sampling {
5184        SamplingSettings::Default(_) | SamplingSettings::MultiChanneling(_) => {
5185            if input.group_id.is_some() || input.channel_id.is_some() {
5186                return Err(eyre!(
5187                    "Discrete graph/channel selections are not supported for this sampling mode."
5188                ));
5189            }
5190
5191            GammaLoopSample::from_default(integrand, sample, false, None)
5192        }
5193        SamplingSettings::DiscreteGraphs(settings) => {
5194            let Some(group_id) = input.group_id else {
5195                if input.orientation.is_some() || input.channel_id.is_some() {
5196                    return Err(eyre!(
5197                        "Explicit orientation or channel selections require selecting a graph group in momentum-space evaluation."
5198                    ));
5199                }
5200                return GammaLoopSample::from_default(integrand, sample, false, None);
5201            };
5202            match &settings.sampling_type {
5203                DiscreteGraphSamplingType::Default(_) => {
5204                    if input.channel_id.is_some() {
5205                        return Err(eyre!(
5206                            "Channel selection is not available for this discrete-graph sampling mode."
5207                        ));
5208                    }
5209                    GammaLoopSample::from_default(integrand, sample, false, Some(group_id))
5210                }
5211                DiscreteGraphSamplingType::MultiChanneling(_) => {
5212                    if input.channel_id.is_some() {
5213                        return Err(eyre!(
5214                            "Channel selection is not available for this discrete-graph sampling mode."
5215                        ));
5216                    }
5217                    // Direct momentum input already contains the desired
5218                    // parent-frame point; only unit-cube samples replay maps.
5219                    GammaLoopSample::from_default(integrand, sample, false, Some(group_id))
5220                }
5221                DiscreteGraphSamplingType::TropicalSampling(_) => {
5222                    if input.channel_id.is_some() {
5223                        return Err(eyre!(
5224                            "Channel selection is not available for tropical discrete-graph sampling."
5225                        ));
5226                    }
5227                    GammaLoopSample::from_tropical(integrand, group_id, sample)
5228                }
5229                DiscreteGraphSamplingType::SamplingMultiChanneling(_) => {
5230                    let Some(channel_id) = input.channel_id else {
5231                        // An unselected raw point evaluates the complete physical
5232                        // graph without a proposal partition or artificial channel.
5233                        return GammaLoopSample::from_default(
5234                            integrand,
5235                            sample,
5236                            false,
5237                            Some(group_id),
5238                        );
5239                    };
5240                    let graph = integrand.get_master_graph(group_id);
5241                    let bridge = graph.sampling_setup().sampling_bridge::<T>()?;
5242                    let mut contexts = SamplingChannelRuntimeContexts::for_draw(
5243                        bridge.channels().len(),
5244                        integrand.get_group(group_id).master(),
5245                        channel_id,
5246                        metadata,
5247                    );
5248                    let mapped = bridge.inverse_with_runtime_contexts(
5249                        channel_id,
5250                        &sample
5251                            .loop_moms()
5252                            .0
5253                            .iter()
5254                            .flat_map(|momentum| {
5255                                [
5256                                    momentum.px.0.clone(),
5257                                    momentum.py.0.clone(),
5258                                    momentum.pz.0.clone(),
5259                                ]
5260                            })
5261                            .collect::<Vec<_>>(),
5262                        &mut contexts,
5263                    )?
5264                    .ok_or_else(|| eyre!(
5265                        "raw momentum point is outside sampling channel {}; select a full-support sibling for direct-momentum evaluation",
5266                        channel_id.0,
5267                    ))?;
5268                    let partition_weight =
5269                        mapped.partition.weight(channel_id.0).ok_or_else(|| {
5270                            eyre!(
5271                                "sampling channel partition has no weight for channel {}",
5272                                channel_id.0
5273                            )
5274                        })?;
5275                    if !partition_weight.is_finite() || partition_weight <= partition_weight.zero()
5276                    {
5277                        return Err(eyre!(
5278                            "sampling channel partition has invalid weight {partition_weight}"
5279                        ));
5280                    }
5281                    let mut sample = sample;
5282                    // Direct inputs have no parameterization Jacobian. Retain
5283                    // only the selected partition in the same per-row factor.
5284                    sample.sample.jacobian = F(partition_weight);
5285                    GammaLoopSample::from_selected(
5286                        integrand,
5287                        group_id,
5288                        channel_id,
5289                        sample,
5290                        mapped.prepared_lu_hosts,
5291                    )
5292                }
5293            }
5294        }
5295    }
5296}
5297
5298fn log_rotated_samples<I: ProcessIntegrandImpl>(
5299    integrand: &mut I,
5300    gammaloop_sample: &GammaLoopSample<f64>,
5301    rotated_results: &[RotatedEvaluation],
5302) {
5303    let mut loop_mom_cache_id = integrand.loop_cache_id();
5304    let mut external_mom_cache_id = integrand.external_cache_id();
5305    let mut shift = 0;
5306
5307    let rotated_samples: Vec<_> = integrand
5308        .get_rotations()
5309        .map(|rotation| {
5310            if rotation.is_identity() {
5311                return gammaloop_sample.clone();
5312            }
5313            loop_mom_cache_id += gammaloop_sample.row_count();
5314            shift += 1;
5315            external_mom_cache_id += 1;
5316            gammaloop_sample.rotate(rotation, loop_mom_cache_id, external_mom_cache_id)
5317        })
5318        .collect();
5319    integrand.increment_external_cache_id(shift);
5320    integrand.increment_loop_cache_id(shift * gammaloop_sample.row_count());
5321
5322    for (sample, result) in rotated_samples.iter().zip(rotated_results.iter()) {
5323        let default_sample = sample.get_default_sample();
5324        debug!(
5325            "loop_moms: {}, external_moms: {}",
5326            format!("{}", default_sample.loop_moms()).blue(),
5327            format!("{:?}", default_sample.external_moms()).blue()
5328        );
5329
5330        debug!(
5331            "result of current level: {}",
5332            format!("{:16e}", result.result).blue()
5333        );
5334    }
5335}
5336
5337fn evaluate_from_source_precise<I: ProcessIntegrandImpl>(
5338    integrand: &mut I,
5339    target: EvaluationTarget<'_>,
5340    source: EvaluationSource<'_>,
5341    wgt: F<f64>,
5342    use_arb_prec: bool,
5343    max_eval: Complex<F<f64>>,
5344) -> Result<PreciseEvaluationResult> {
5345    evaluate_from_source_precise_with_estimate(
5346        integrand,
5347        target,
5348        source,
5349        wgt,
5350        use_arb_prec,
5351        max_eval,
5352        None,
5353        None,
5354    )
5355}
5356
5357#[allow(clippy::too_many_arguments)]
5358fn evaluate_from_source_precise_with_estimate<I: ProcessIntegrandImpl>(
5359    integrand: &mut I,
5360    target: EvaluationTarget<'_>,
5361    source: EvaluationSource<'_>,
5362    wgt: F<f64>,
5363    use_arb_prec: bool,
5364    max_eval: Complex<F<f64>>,
5365    integral_estimate: Option<(f64, f64)>,
5366    remaining_weight: Option<&F<ArbPrec>>,
5367) -> Result<PreciseEvaluationResult> {
5368    let stability = &integrand.get_settings().stability;
5369    if matches!(target, EvaluationTarget::Physical(_))
5370        && stability.integrated_energy_dimension.is_none()
5371        && stability.levels.iter().any(|level| {
5372            level.ecm_relative_tolerance_for_re > 0.0 || level.ecm_relative_tolerance_for_im > 0.0
5373        })
5374    {
5375        return Err(eyre!(
5376            "E_cm-relative physical stability requires `stability.integrated_energy_dimension` (after flux normalization, before output-unit conversion)"
5377        ));
5378    }
5379    let start_eval = std::time::Instant::now();
5380    let mut escalate_if_exact_zero = integrand.get_settings().stability.escalate_if_exact_zero;
5381    if escalate_if_exact_zero
5382        && integrand
5383            .get_settings()
5384            .selectors
5385            .values()
5386            .any(|selector| selector.active)
5387    {
5388        warn_selectors_disable_zero_once();
5389        escalate_if_exact_zero = false;
5390    }
5391    let mut evaluation_metadata = EvaluationMetaData::new_empty();
5392    if matches!(target, EvaluationTarget::Reference(_))
5393        && matches!(&integrand.get_settings().sampling, SamplingSettings::DiscreteGraphs(settings)
5394            if matches!(settings.sampling_type, DiscreteGraphSamplingType::TropicalSampling(_)))
5395    {
5396        return Err(eyre!(
5397            "reference acceptance does not support tropical sampling"
5398        ));
5399    }
5400    let prepared = (|| {
5401        let mut anchor = source.prepare_draw(integrand, &mut evaluation_metadata)?;
5402        if let (Some(sample), EvaluationTarget::Physical(model)) = (&mut anchor, target) {
5403            sample.prepare_physical_overlaps(integrand, model, &mut evaluation_metadata)?;
5404        }
5405        Ok::<_, eyre::Report>(anchor)
5406    })();
5407    let (anchor, preparation_error) = match prepared {
5408        Ok(anchor) => (anchor, None),
5409        Err(error)
5410            if matches!(target, EvaluationTarget::Physical(_))
5411                && matches!(
5412                    error.downcast_ref::<sampling::maps::SamplingEvaluationError>(),
5413                    Some(
5414                        sampling::maps::SamplingEvaluationError::UncertifiedOverlap { .. }
5415                            | sampling::maps::SamplingEvaluationError::UncertifiedRoot { .. }
5416                    )
5417                ) =>
5418        {
5419            // The canonical proposal/center authority must not be changed in a
5420            // native retry. An unresolved preparation prevents every physical
5421            // lane; preserve its diagnostic without evaluating partial bodies.
5422            (None, Some(format!("{error:#}")))
5423        }
5424        Err(error) => return Err(error),
5425    };
5426    let original_source = source;
5427    let source = match &anchor {
5428        Some(sample) => EvaluationSource::Prepared {
5429            sample,
5430            original: &original_source,
5431        },
5432        None => original_source,
5433    };
5434    let (stability_iterator, loop_momenta_escalation) = if preparation_error.is_some() {
5435        (
5436            create_stability_iterator(&integrand.get_settings().stability, use_arb_prec),
5437            None,
5438        )
5439    } else {
5440        stability_iterator_for_source(integrand, &source, use_arb_prec, &mut evaluation_metadata)
5441    };
5442
5443    let mut final_result = None;
5444
5445    let mut stability_results = Vec::with_capacity(stability_iterator.len());
5446    let mut sampling_failures = Vec::new();
5447    let total_levels = stability_iterator.len();
5448    for (level_index, stability_level) in stability_iterator.into_iter().enumerate() {
5449        evaluation_metadata.clear_threshold_counterterm_error();
5450        if let Some(error) = &preparation_error {
5451            evaluation_metadata.record_threshold_counterterm_error(error.clone());
5452        }
5453        let is_final_level = level_index + 1 == total_levels;
5454        let record_rotated_results = integrand
5455            .get_settings()
5456            .stability
5457            .recording
5458            .map(|recording| recording.record_rotated_results)
5459            .unwrap_or(false);
5460        // A fixed-helicity amplitude changes by a global phase under spatial
5461        // rotations. Norm stability therefore checks its full complex value,
5462        // independently of which component the integrator accumulates.
5463        let check_both_components = !matches!(target, EvaluationTarget::Physical(_))
5464            || (integrand.get_settings().stability.check_on_norm
5465                && !matches!(
5466                    integrand.get_dependent_momenta_constructor(),
5467                    DependentMomentaConstructor::CrossSection
5468                ));
5469        let mut context = StabilityEvaluationContext {
5470            target,
5471            source: &source,
5472            stability_level: &stability_level,
5473            max_eval: &max_eval,
5474            wgt,
5475            remaining_weight,
5476            check_on_norm: integrand.get_settings().stability.check_on_norm,
5477            is_final_level,
5478            evaluation_metadata: &mut evaluation_metadata,
5479            record_rotated_results,
5480            precision_label: match stability_level.precision {
5481                Precision::Double => "f64",
5482                Precision::Quad => "f128",
5483                Precision::Arb => "ArbPrec",
5484            },
5485            escalate_if_exact_zero,
5486            check_real: check_both_components
5487                || !matches!(
5488                    integrand.get_settings().integrator.integrated_phase,
5489                    IntegratedPhase::Imag
5490                ),
5491            check_imag: check_both_components
5492                || !matches!(
5493                    integrand.get_settings().integrator.integrated_phase,
5494                    IntegratedPhase::Real
5495                ),
5496            integral_estimate: matches!(target, EvaluationTarget::Physical(_))
5497                .then_some(integral_estimate)
5498                .flatten(),
5499            min_abs_wgt_for_escalation: integrand
5500                .get_settings()
5501                .stability
5502                .min_abs_wgt_for_escalation,
5503        };
5504        let level_start = Instant::now();
5505        let mut is_stable = false;
5506        let mut rotated_results = Vec::new();
5507        macro_rules! evaluate_native_level {
5508            ($scalar:ty, $variant:ident) => {
5509                evaluate_stability_level_precise::<$scalar, I>(integrand, &mut context).map(
5510                    |mut result| {
5511                        is_stable = result.is_stable;
5512                        stability_results.push(result.stability_result());
5513                        debug!(
5514                            "level: {}. result: {}",
5515                            format!("{}", result.stability_level_used).green(),
5516                            format!("{:16e}", result.result).blue()
5517                        );
5518                        rotated_results = std::mem::take(&mut result.rotated_results);
5519                        PreciseEvaluationResult::$variant(finalize_precise_evaluation_result(
5520                            result,
5521                            wgt,
5522                            evaluation_metadata.clone(),
5523                        ))
5524                    },
5525                )
5526            };
5527        }
5528        let result_of_level = match stability_level.precision {
5529            Precision::Double => evaluate_native_level!(f64, Double),
5530            Precision::Quad => evaluate_native_level!(f128, Quad),
5531            Precision::Arb => evaluate_native_level!(ArbPrec, Arb),
5532        };
5533        let result_of_level = match result_of_level {
5534            Ok(result) => result,
5535            Err(error)
5536                if error
5537                    .downcast_ref::<sampling::maps::SamplingEvaluationError>()
5538                    .is_some() =>
5539            {
5540                stability_results.push(StabilityResult {
5541                    precision: stability_level.precision,
5542                    estimated_relative_accuracy: None,
5543                    estimated_decimal_digits: None,
5544                    status: StabilityStatus::Unstable(0),
5545                    total_time: level_start.elapsed(),
5546                });
5547                crate::debug_tags!(#sampling;
5548                    stage = "native_sampling_retry", precision = %stability_level.precision,
5549                    error = %error, final_level = is_final_level,
5550                    "sampling reconstruction failed at this precision"
5551                );
5552                // A partial rotation/channel pass may have populated numerical
5553                // caches. Start the next reconstruction with fresh point IDs;
5554                // dropped GraphEvaluationResults contain all its uncommitted events.
5555                integrand.increment_loop_cache_id(integrand.get_rotations().count() + 1);
5556                integrand.revert_to_base_external_cache_id();
5557                sampling_failures.push(format!("{}: {error:#}", stability_level.precision));
5558                if is_final_level {
5559                    return Err(error.wrap_err(format!(
5560                        "sampling reconstruction failed; attempts [{}]; numerical errors [{}]",
5561                        stability_results
5562                            .iter()
5563                            .map(|result| format!("{}: {:?}", result.precision, result.status))
5564                            .join("; "),
5565                        sampling_failures.join("; ")
5566                    )));
5567                }
5568                continue;
5569            }
5570            Err(error) => return Err(error),
5571        };
5572
5573        if matches!(target, EvaluationTarget::Reference(_)) && is_final_level && !is_stable {
5574            return Err(eyre!(
5575                "reference value or moment remained unstable after the final {} precision level",
5576                stability_level.precision
5577            ));
5578        }
5579        final_result = Some(result_of_level);
5580
5581        if is_stable {
5582            break;
5583        } else {
5584            debug!("unstable at level: {}", stability_level.precision);
5585            if evaluation_metadata.threshold_counterterm_error.is_some() {
5586                debug!("threshold preparation or evaluation requires precision escalation");
5587            } else if let Ok(gammaloop_sample) =
5588                source.debug_sample(integrand, &mut evaluation_metadata)
5589            {
5590                log_rotated_samples(integrand, &gammaloop_sample, &rotated_results);
5591            } else {
5592                debug!("failed to reconstruct sample for instability logging");
5593            }
5594        }
5595    }
5596
5597    let mut result = final_result.ok_or_else(|| eyre!("no stability level was evaluated"))?;
5598    let metadata = match &mut result {
5599        PreciseEvaluationResult::Double(result) => &mut result.evaluation_metadata,
5600        PreciseEvaluationResult::Quad(result) => &mut result.evaluation_metadata,
5601        PreciseEvaluationResult::Arb(result) => &mut result.evaluation_metadata,
5602    };
5603    metadata.total_timing = start_eval.elapsed();
5604    // A last unstable-level debug replay occurs after its result was cloned.
5605    // Copy the sole source owner's accumulated counters, never one lane's time.
5606    metadata.parameterization_time = evaluation_metadata.parameterization_time;
5607    metadata.canonical_sampling_preparation_time =
5608        evaluation_metadata.canonical_sampling_preparation_time;
5609    metadata.integrand_evaluation_time = evaluation_metadata.integrand_evaluation_time;
5610    metadata.loop_momenta_escalation = loop_momenta_escalation;
5611    metadata.stability_results = stability_results;
5612    Ok(result)
5613}
5614
5615fn evaluate_from_source<I: ProcessIntegrandImpl>(
5616    integrand: &mut I,
5617    model: &Model,
5618    source: EvaluationSource<'_>,
5619    wgt: F<f64>,
5620    use_arb_prec: bool,
5621    max_eval: Complex<F<f64>>,
5622) -> Result<EvaluationResult> {
5623    evaluate_from_source_precise(
5624        integrand,
5625        EvaluationTarget::Physical(model),
5626        source,
5627        wgt,
5628        use_arb_prec,
5629        max_eval,
5630    )?
5631    .try_into_f64()
5632}
5633
5634fn stability_iterator_for_source<I: ProcessIntegrandImpl>(
5635    integrand: &mut I,
5636    source: &EvaluationSource<'_>,
5637    use_arb_prec: bool,
5638    metadata: &mut EvaluationMetaData,
5639) -> (
5640    Vec<StabilityLevelSetting>,
5641    Option<LoopMomentaEscalationMetrics>,
5642) {
5643    let mut stability_iterator =
5644        create_stability_iterator(&integrand.get_settings().stability, use_arb_prec);
5645    let escalation_factor = integrand
5646        .get_settings()
5647        .stability
5648        .loop_momenta_norm_escalation_factor;
5649    let record_loop_momenta_escalation = integrand
5650        .get_settings()
5651        .stability
5652        .recording
5653        .map(|recording| recording.record_loop_momenta_escalation)
5654        .unwrap_or(false);
5655    let mut loop_momenta_escalation = None;
5656    if escalation_factor > 0.0
5657        && stability_iterator.len() > 1
5658        && let Ok(sum_norm) = source.loop_norm_sum(integrand, metadata)
5659    {
5660        let threshold =
5661            F::<f64>::from_f64(escalation_factor * integrand.get_settings().kinematics.e_cm);
5662        if record_loop_momenta_escalation {
5663            loop_momenta_escalation = Some(LoopMomentaEscalationMetrics {
5664                sum_norm: sum_norm.0,
5665                threshold: threshold.0,
5666            });
5667        }
5668        if sum_norm > threshold {
5669            let escalated_level_index = stability_iterator
5670                .iter()
5671                .position(|level| level.precision == Precision::Quad)
5672                .or_else(|| stability_iterator.len().checked_sub(1));
5673            if let Some(level_index) = escalated_level_index {
5674                stability_iterator = stability_iterator[level_index..].to_vec();
5675            }
5676        }
5677    }
5678
5679    (stability_iterator, loop_momenta_escalation)
5680}
5681
5682fn finalize_precise_evaluation_result<T: FloatLike>(
5683    result: PreciseStabilityLevelResult<T>,
5684    integrator_weight: F<f64>,
5685    mut evaluation_metadata: EvaluationMetaData,
5686) -> GenericEvaluationResult<T> {
5687    let re_is_nan = result.result.re.is_nan()
5688        || result.result.re.is_infinite()
5689        || result
5690            .graph_result
5691            .absolute_integrand_result
5692            .as_ref()
5693            .is_some_and(|value| value.re.is_nan() || value.re.is_infinite());
5694    let im_is_nan = result.result.im.is_nan()
5695        || result.result.im.is_infinite()
5696        || result
5697            .graph_result
5698            .absolute_integrand_result
5699            .as_ref()
5700            .is_some_and(|value| value.im.is_nan() || value.im.is_infinite());
5701    if re_is_nan || im_is_nan {
5702        warn!(
5703            stage = "process_final_nonfinite_sample",
5704            result = %result.result,
5705            re_is_nan,
5706            im_is_nan,
5707            "process evaluation is nonfinite"
5708        );
5709    }
5710    evaluation_metadata.generated_event_count = result.graph_result.generated_event_count;
5711    evaluation_metadata.accepted_event_count = result.graph_result.accepted_event_count;
5712    evaluation_metadata.relative_instability_error = Complex::new_zero();
5713    evaluation_metadata.is_nan = re_is_nan || im_is_nan;
5714    let nanless_result = if re_is_nan && !im_is_nan {
5715        Complex::new(result.result.re.zero(), result.result.im)
5716    } else if im_is_nan && !re_is_nan {
5717        Complex::new(result.result.re, result.result.im.zero())
5718    } else if re_is_nan && im_is_nan {
5719        Complex::new(result.result.re.zero(), result.result.im.zero())
5720    } else {
5721        result.result
5722    };
5723
5724    let mut event_groups = result.graph_result.event_groups;
5725    let integrator_weight = F::<T>::from_ff64(integrator_weight);
5726    let parameterization_jacobian = result.parameterization_jacobian;
5727    let full_factor = full_event_multiplicative_factor_precise(
5728        parameterization_jacobian.clone(),
5729        integrator_weight.clone(),
5730    );
5731    apply_full_event_multiplicative_factor_precise(&mut event_groups, &full_factor);
5732
5733    GenericEvaluationResult {
5734        reference_moments: result.graph_result.reference_moments,
5735        integrand_result: nanless_result,
5736        absolute_integrand_result: result.graph_result.absolute_integrand_result.map(|value| {
5737            Complex::new(
5738                if re_is_nan { value.re.zero() } else { value.re },
5739                if im_is_nan { value.im.zero() } else { value.im },
5740            )
5741        }),
5742        parameterization_jacobian,
5743        integrator_weight,
5744        event_groups,
5745        evaluation_metadata,
5746    }
5747}
5748
5749fn warn_selectors_disable_zero_once() {
5750    static ONCE: Once = Once::new();
5751    ONCE.call_once(|| {
5752        warn!(
5753            "disabling `stability.escalate_if_exact_zero` during evaluation because selectors can legitimately zero the event weight"
5754        );
5755    });
5756}
5757
5758fn evaluate_sample<I: ProcessIntegrandImpl>(
5759    integrand: &mut I,
5760    model: &Model,
5761    sample: &Sample<F<f64>>,
5762    wgt: F<f64>,
5763    _iter: usize,
5764    use_arb_prec: bool,
5765    max_eval: Complex<F<f64>>,
5766) -> Result<EvaluationResult> {
5767    evaluate_from_source(
5768        integrand,
5769        model,
5770        EvaluationSource::XSpace(sample),
5771        wgt,
5772        use_arb_prec,
5773        max_eval,
5774    )
5775}
5776
5777fn evaluate_reference_sample<I: ProcessIntegrandImpl>(
5778    integrand: &mut I,
5779    sample: &Sample<F<f64>>,
5780    reference: &GaussianReferenceFunction,
5781) -> Result<ReferenceSampleEvaluation> {
5782    let result = evaluate_from_source_precise(
5783        integrand,
5784        EvaluationTarget::Reference(reference),
5785        EvaluationSource::XSpace(sample),
5786        sample.get_weight(),
5787        false,
5788        Complex::new_zero(),
5789    )?;
5790    // Select precision once in the common loop, then narrow its value and
5791    // moment together at the existing f64 acceptance reporting boundary.
5792    macro_rules! report {
5793        ($result:expr) => {{
5794            let mut result = $result;
5795            if result.evaluation_metadata.is_nan {
5796                return Err(eyre!(
5797                    "reference acceptance cannot report a nonfinite finalized value"
5798                ));
5799            }
5800            let moments = result
5801                .reference_moments
5802                .take()
5803                .ok_or_else(|| eyre!("reference acceptance produced no mapped moment"))?
5804                .try_into_f64()?;
5805            Ok(ReferenceSampleEvaluation {
5806                evaluation: result.try_into_f64()?,
5807                moments,
5808            })
5809        }};
5810    }
5811    match result {
5812        PreciseEvaluationResult::Double(result) => report!(result),
5813        PreciseEvaluationResult::Quad(result) => report!(result),
5814        PreciseEvaluationResult::Arb(result) => report!(result),
5815    }
5816}
5817
5818fn evaluate_sample_precise<I: ProcessIntegrandImpl>(
5819    integrand: &mut I,
5820    model: &Model,
5821    sample: &Sample<F<f64>>,
5822    wgt: F<f64>,
5823    use_arb_prec: bool,
5824    max_eval: Complex<F<f64>>,
5825) -> Result<crate::integrands::evaluation::PreciseEvaluationResult> {
5826    evaluate_from_source_precise(
5827        integrand,
5828        EvaluationTarget::Physical(model),
5829        EvaluationSource::XSpace(sample),
5830        wgt,
5831        use_arb_prec,
5832        max_eval,
5833    )
5834}
5835
5836fn evaluate_momentum_configuration<I: ProcessIntegrandImpl>(
5837    integrand: &mut I,
5838    model: &Model,
5839    input: &MomentumSpaceEvaluationInput,
5840    wgt: F<f64>,
5841    use_arb_prec: bool,
5842    max_eval: Complex<F<f64>>,
5843) -> Result<EvaluationResult> {
5844    evaluate_from_source(
5845        integrand,
5846        model,
5847        EvaluationSource::Momentum {
5848            input,
5849            candidate_lmb: None,
5850        },
5851        wgt,
5852        use_arb_prec,
5853        max_eval,
5854    )
5855}
5856
5857fn evaluate_momentum_configuration_precise<I: ProcessIntegrandImpl>(
5858    integrand: &mut I,
5859    model: &Model,
5860    input: &MomentumSpaceEvaluationInput,
5861    wgt: F<f64>,
5862    use_arb_prec: bool,
5863    max_eval: Complex<F<f64>>,
5864) -> Result<crate::integrands::evaluation::PreciseEvaluationResult> {
5865    evaluate_from_source_precise(
5866        integrand,
5867        EvaluationTarget::Physical(model),
5868        EvaluationSource::Momentum {
5869            input,
5870            candidate_lmb: None,
5871        },
5872        wgt,
5873        use_arb_prec,
5874        max_eval,
5875    )
5876}
5877
5878#[cfg(test)]
5879pub(crate) mod tests {
5880    use super::{
5881        GraphTerm, LmbMultiChannelingSetup, RuntimeCache, SamplingChannelCompileContext,
5882        SamplingChannelId, SamplingSelectionError, create_stability_iterator,
5883        filtered_orientation_count, resolve_sampling_channel_selection,
5884        resolve_visible_orientation_id, validate_orientation_catalog_group,
5885        validate_process_runtime_settings,
5886    };
5887    use crate::cff::expression::OrientationID;
5888    use crate::{
5889        dot,
5890        graph::{
5891            FeynmanGraph, Graph, GroupId, LMBext, LmbIndex, LoopMomentumBasis,
5892            parse::from_dot::IntoGraph,
5893        },
5894        initialisation::test_initialise,
5895        momentum::{
5896            ThreeMomentum,
5897            sample::{BareMomentumSample, ExternalFourMomenta, LoopMomenta, MomentumSample},
5898            signature::LoopExtSignature,
5899        },
5900        settings::{
5901            RuntimeSettings,
5902            global::OrientationPattern,
5903            runtime::{
5904                DiscreteGraphSamplingSettings, DiscreteGraphSamplingType, HFunctionSettings,
5905                MultiChannelingSettings, ParameterizationSettings, Precision,
5906                SamplingChannelDefinition, SamplingChannelSelection, SamplingSettings,
5907                StabilitySettings,
5908            },
5909        },
5910        utils::F,
5911    };
5912    use linnet::half_edge::{
5913        involution::{EdgeIndex, EdgeVec, Orientation},
5914        subgraph::{ModifySubSet, SubSetLike, subset::SubSet},
5915    };
5916    use std::sync::OnceLock;
5917    use typed_index_collections::TiVec;
5918
5919    /// Use the existing generated scalar cut fixture for a fixed-source body
5920    /// retry. Its normalized q target makes a missing/doubled J*w observable.
5921    pub(crate) fn check_fixed_quad_source_transport(
5922        runtime: &mut super::ProcessIntegrand,
5923        model: &crate::model::Model,
5924        source: &symbolica::numerical_integration::Sample<F<f64>>,
5925    ) -> color_eyre::Result<()> {
5926        use super::{
5927            EvaluationSource, EvaluationTarget, GraphTerm, ProcessIntegrand, ProcessIntegrandImpl,
5928            SamplingLawProbe, evaluate_from_source_precise,
5929        };
5930        use crate::{
5931            integrands::evaluation::EvaluationMetaData,
5932            settings::runtime::{Precision, StabilityLevelSetting},
5933            utils::{ArbPrec, QuadFloat, SamplingFloat, SamplingPrecision},
5934        };
5935        use spenso::algebra::complex::Complex;
5936        use std::sync::atomic::{AtomicUsize, Ordering};
5937        let original_sampling = runtime.get_settings().sampling.clone();
5938        for fixed256 in [false, true] {
5939            runtime.get_mut_settings().sampling = original_sampling.clone();
5940            if fixed256 {
5941                let mut parser: crate::settings::runtime::SamplingSettingsParser =
5942                    toml::from_str(&toml::to_string(&original_sampling)?)?;
5943                parser.sampling_channel_weight =
5944                    crate::settings::runtime::SamplingChannelWeight::SingularityProxy;
5945                for definitions in parser.channel_definitions.values_mut() {
5946                    for definition in definitions.values_mut() {
5947                        definition.singularity_proxy = Some("1".into());
5948                    }
5949                }
5950                runtime.get_mut_settings().sampling = toml::from_str(&toml::to_string(&parser)?)?;
5951            }
5952            let expected_source = if fixed256 {
5953                SamplingPrecision::Fixed256
5954            } else {
5955                SamplingPrecision::Quad
5956            };
5957            let settings = runtime.get_mut_settings();
5958            settings.stability.levels = vec![
5959                StabilityLevelSetting::default_double(),
5960                StabilityLevelSetting::default_quad(),
5961                StabilityLevelSetting::default_arb(),
5962            ];
5963            settings.stability.rotation_axis.clear();
5964            settings.stability.escalate_if_exact_zero = false;
5965            settings.stability.loop_momenta_norm_escalation_factor = 0.0;
5966            runtime.warm_up(model)?;
5967            let ProcessIntegrand::CrossSection(integrand) = runtime else {
5968                unreachable!("generated cut fixture")
5969            };
5970            let policy = integrand
5971                .get_graph(0)
5972                .sampling_setup()
5973                .sampling_source
5974                .as_ref()
5975                .copied()
5976                .unwrap();
5977            assert_eq!(policy.0, expected_source);
5978            let saved_catalogue = integrand
5979                .get_graph(0)
5980                .sampling_setup()
5981                .sampling_catalogue
5982                .as_ref()
5983                .unwrap()
5984                .clone();
5985            integrand
5986                .get_graph_mut(0)
5987                .sampling_setup_mut()
5988                .sampling_catalogue
5989                .as_mut()
5990                .unwrap()
5991                .entries
5992                .push(
5993                    super::sampling::selection::SamplingCatalogueEntry::Surface {
5994                        edges: vec![1, 2],
5995                        parent_lmb: vec![1],
5996                    },
5997                );
5998            assert_eq!(
5999                integrand.sampling_source_policy()?.0,
6000                SamplingPrecision::Fixed256
6001            );
6002            integrand
6003                .get_graph_mut(0)
6004                .sampling_setup_mut()
6005                .sampling_catalogue
6006                .set(saved_catalogue);
6007            let saved_externals = integrand.settings.kinematics.externals.clone();
6008            let mut external_values = saved_externals.get_dependent_externals::<ArbPrec>(
6009                integrand.get_dependent_momenta_constructor(),
6010            )?;
6011            external_values[crate::momentum::sample::ExternalIndex(0)]
6012                .spatial
6013                .px = F::<ArbPrec>::default().from_usize(2).powi(2000);
6014            let crate::settings::runtime::kinematic::Externals::Constant { arb_cache, .. } =
6015                &mut integrand.settings.kinematics.externals;
6016            arb_cache.set(external_values);
6017            assert_eq!(
6018                integrand.sampling_source_policy()?.0,
6019                SamplingPrecision::Fixed256
6020            );
6021            integrand.settings.kinematics.externals = saved_externals;
6022            assert_eq!(integrand.sampling_source_policy()?, policy);
6023            let mut metadata = EvaluationMetaData::new_empty();
6024            let expected = if fixed256 {
6025                super::parameterize::<SamplingFloat, _>(source, integrand, &mut metadata)?
6026                    .into_canonical(
6027                        &integrand.get_settings().kinematics.externals,
6028                        integrand.get_dependent_momenta_constructor(),
6029                    )?
6030            } else {
6031                super::parameterize::<QuadFloat, _>(source, integrand, &mut metadata)?
6032                    .into_canonical(
6033                        &integrand.get_settings().kinematics.externals,
6034                        integrand.get_dependent_momenta_constructor(),
6035                    )?
6036            };
6037            let source_kind = EvaluationSource::XSpace(source);
6038            let anchor = source_kind.prepare_draw(integrand, &mut metadata)?.unwrap();
6039            assert_eq!(format!("{anchor:?}"), format!("{expected:?}"));
6040            if !fixed256 {
6041                use super::{
6042                    GaussianReferenceFunction, MomentumSpaceEvaluationInput,
6043                    StabilityEvaluationContext,
6044                };
6045                use crate::settings::runtime::IntegralUnit;
6046
6047                // Exercise the real source/target scale owners with this already
6048                // generated one-incoming cut and its explicitly selected channel.
6049                let original_settings = integrand.settings.clone();
6050                let mut level = StabilityLevelSetting::default_arb();
6051                level.ecm_relative_tolerance_for_re = 1e-4;
6052                level.ecm_relative_tolerance_for_im = 1e-4;
6053                integrand.settings.stability.levels = vec![level];
6054                integrand.settings.stability.integrated_energy_dimension = None;
6055                let error = evaluate_from_source_precise(
6056                    integrand,
6057                    EvaluationTarget::Physical(model),
6058                    source_kind,
6059                    F(1.0),
6060                    false,
6061                    Complex::new_zero(),
6062                )
6063                .unwrap_err();
6064                assert!(error.to_string().contains("integrated_energy_dimension"));
6065
6066                let default_sample = anchor.get_default_sample();
6067                assert_eq!(default_sample.external_moms().len(), 1);
6068                let loops = default_sample.loop_moms().0.len();
6069                let direct = MomentumSpaceEvaluationInput {
6070                    loop_momenta: default_sample
6071                        .loop_moms()
6072                        .iter()
6073                        .map(|momentum| {
6074                            ThreeMomentum::new(
6075                                momentum.px.into_ff64(),
6076                                momentum.py.into_ff64(),
6077                                momentum.pz.into_ff64(),
6078                            )
6079                        })
6080                        .collect(),
6081                    integrator_weight: F(1.0),
6082                    graph_id: None,
6083                    group_id: Some(GroupId(0)),
6084                    orientation: None,
6085                    channel_id: Some(SamplingChannelId(0)),
6086                };
6087                integrand.settings.sampling =
6088                    SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
6089                        sampling_type: DiscreteGraphSamplingType::SamplingMultiChanneling(
6090                            MultiChannelingSettings {
6091                                parameterization_settings: original_settings
6092                                    .sampling
6093                                    .get_parameterization_settings()
6094                                    .unwrap(),
6095                            },
6096                        ),
6097                        ..Default::default()
6098                    });
6099                integrand.warm_up(model)?;
6100                let raw_source = EvaluationSource::Momentum {
6101                    input: &direct,
6102                    candidate_lmb: None,
6103                };
6104                let raw = raw_source.prepare_draw(integrand, &mut metadata)?.unwrap();
6105                let prepared_raw = EvaluationSource::Prepared {
6106                    sample: &raw,
6107                    original: &raw_source,
6108                };
6109                assert!(!prepared_raw.is_x_space());
6110                let reference_function =
6111                    GaussianReferenceFunction::centered(original_settings.kinematics.e_cm, loops)?;
6112                let maximum = Complex::new_zero();
6113                for energy in [
6114                    original_settings.kinematics.e_cm,
6115                    2.0 * original_settings.kinematics.e_cm,
6116                ] {
6117                    integrand.settings.kinematics.e_cm = energy;
6118                    let e_cm = F::<ArbPrec>::from_f64(energy);
6119                    for (evaluation_source, sample, missing) in [
6120                        (&source_kind, &anchor, 0),
6121                        (&prepared_raw, &raw, 3 * loops as i32),
6122                    ] {
6123                        let mut context = StabilityEvaluationContext {
6124                            target: EvaluationTarget::Reference(&reference_function),
6125                            source: evaluation_source,
6126                            stability_level: &level,
6127                            max_eval: &maximum,
6128                            wgt: F(1.0),
6129                            remaining_weight: None,
6130                            check_on_norm: false,
6131                            is_final_level: true,
6132                            evaluation_metadata: &mut metadata,
6133                            record_rotated_results: false,
6134                            precision_label: "ArbPrec",
6135                            escalate_if_exact_zero: false,
6136                            check_real: true,
6137                            check_imag: true,
6138                            integral_estimate: None,
6139                            min_abs_wgt_for_escalation: 0.0,
6140                        };
6141                        integrand.settings.stability.integrated_energy_dimension = None;
6142                        assert_eq!(
6143                            context.ecm_reference_scale(integrand, sample)?.unwrap(),
6144                            e_cm.powi(-missing)
6145                        );
6146                        context.target = EvaluationTarget::Physical(model);
6147                        assert!(context.ecm_reference_scale(integrand, sample).is_err());
6148                        for (disabled_flux, dimension) in [(false, 1), (true, 2)] {
6149                            integrand.settings.general.disable_flux_factor = disabled_flux;
6150                            integrand.settings.stability.integrated_energy_dimension =
6151                                Some(dimension);
6152                            // The actual decay branch has no barn conversion,
6153                            // including when an explicit barn unit was supplied.
6154                            for unit in [
6155                                IntegralUnit::None,
6156                                IntegralUnit::Picobarn,
6157                                IntegralUnit::Femtobarn,
6158                            ] {
6159                                integrand.settings.general.integral_unit = unit;
6160                                assert_eq!(
6161                                    context.ecm_reference_scale(integrand, sample)?.unwrap(),
6162                                    e_cm.powi(dimension - missing)
6163                                );
6164                            }
6165                        }
6166                    }
6167                }
6168                integrand.settings = original_settings.clone();
6169                integrand.settings.stability.levels = vec![level];
6170                integrand.settings.stability.integrated_energy_dimension = None;
6171                integrand.warm_up(model)?;
6172                let reference_result = evaluate_from_source_precise(
6173                    integrand,
6174                    EvaluationTarget::Reference(&reference_function),
6175                    source_kind,
6176                    F(1.0),
6177                    false,
6178                    Complex::new_zero(),
6179                )?;
6180                assert!(match &reference_result {
6181                    super::PreciseEvaluationResult::Double(result) =>
6182                        result.reference_moments.is_some(),
6183                    super::PreciseEvaluationResult::Quad(result) =>
6184                        result.reference_moments.is_some(),
6185                    super::PreciseEvaluationResult::Arb(result) =>
6186                        result.reference_moments.is_some(),
6187                });
6188                reference_result.try_into_f64()?;
6189                integrand.settings = original_settings;
6190                integrand.warm_up(model)?;
6191            }
6192            let reference = integrand.get_graph(0).sampling_setup().clone();
6193            let probe = SamplingLawProbe {
6194                reference: &reference,
6195                channel_id: SamplingChannelId(0),
6196                radial_coordinate: 0,
6197                retry_lower_half: true,
6198                sign_coordinate: None,
6199                alternating_graph_sign: false,
6200                calls: std::array::from_fn(|_| AtomicUsize::new(0)),
6201            };
6202            let value = evaluate_from_source_precise(
6203                integrand,
6204                EvaluationTarget::SamplingLaw(&probe),
6205                EvaluationSource::XSpace(source),
6206                F(1.0),
6207                false,
6208                Complex::new_zero(),
6209            )?
6210            .try_into_f64()?;
6211            assert_eq!(
6212                value.evaluation_metadata.final_precision(),
6213                Some(Precision::Quad)
6214            );
6215            assert_eq!(probe.calls[0].load(Ordering::Relaxed), 1);
6216            assert_eq!(probe.calls[1].load(Ordering::Relaxed), 1);
6217            assert_eq!(probe.calls[2].load(Ordering::Relaxed), 0);
6218            assert!((value.integrand_result.re.0 - 1.0).abs() < 1.0e-8);
6219            let prepared = EvaluationSource::Prepared {
6220                sample: &anchor,
6221                original: &source_kind,
6222            };
6223            let arb_sample = prepared.build_gamma_sample::<ArbPrec, _>(integrand, &mut metadata)?;
6224            let arb_value = super::evaluate_single(
6225                integrand,
6226                EvaluationTarget::SamplingLaw(&probe),
6227                &arb_sample,
6228                &crate::momentum::Rotation::new(crate::momentum::RotationMethod::Identity),
6229                &mut metadata,
6230                Some(&anchor),
6231            )?;
6232            assert_eq!(probe.calls[2].load(Ordering::Relaxed), 1);
6233            assert!((arb_value.integrand_result.re.into_ff64().0 - 1.0).abs() < 1.0e-8);
6234            for calls in &probe.calls {
6235                calls.store(0, Ordering::Relaxed);
6236            }
6237            assert!(
6238                integrand
6239                    .get_graph(0)
6240                    .sampling_setup()
6241                    .sampling_bridge::<ArbPrec>()
6242                    .is_ok()
6243            );
6244            // A signed normalized density q(k) has integral zero and absolute
6245            // integral one when its reference radial CDF selects the sign.
6246            // Pair u with 1-u: two distinct channel points cancel in the signed
6247            // estimator, while their absolute contributions must still add.
6248            let mut absolute_runtime = integrand.clone();
6249            let catalogue = absolute_runtime
6250                .get_graph_mut(0)
6251                .sampling_setup_mut()
6252                .sampling_catalogue
6253                .as_mut()
6254                .unwrap();
6255            catalogue.entries.push(catalogue.entries[0].clone());
6256            let mut signed_probe = SamplingLawProbe {
6257                reference: &reference,
6258                channel_id: SamplingChannelId(0),
6259                radial_coordinate: 0,
6260                retry_lower_half: false,
6261                sign_coordinate: Some(0),
6262                alternating_graph_sign: false,
6263                calls: std::array::from_fn(|_| AtomicUsize::new(0)),
6264            };
6265            let (selection, original_coordinates) =
6266                super::gammaloop_sample::unwrap_sample::<f64>(source);
6267            for lower in [0.125, 0.25, 0.375] {
6268                let mut rows = Vec::new();
6269                for (channel, radial) in [lower, 1.0 - lower].into_iter().enumerate() {
6270                    let mut coordinates = original_coordinates.clone();
6271                    coordinates[0] = F(radial);
6272                    let source = symbolica::numerical_integration::Sample::Uniform(
6273                        F(1.0),
6274                        selection.clone(),
6275                        coordinates,
6276                    );
6277                    let mut draw = EvaluationSource::XSpace(&source)
6278                        .prepare_draw(&mut absolute_runtime, &mut metadata)?
6279                        .unwrap();
6280                    let mut row = draw.groups[0].1.remove(0);
6281                    row.sample.sample.jacobian /= row.sample.one().from_usize(2);
6282                    row.channel_id = Some(SamplingChannelId(channel));
6283                    row.prepared_lu_hosts.clear();
6284                    rows.push(row);
6285                }
6286                let draw = super::GammaLoopSample {
6287                    groups: vec![(Some(GroupId(0)), rows)],
6288                };
6289                let identity =
6290                    crate::momentum::Rotation::new(crate::momentum::RotationMethod::Identity);
6291                let value = super::evaluate_single(
6292                    &mut absolute_runtime,
6293                    EvaluationTarget::SamplingLaw(&signed_probe),
6294                    &draw,
6295                    &identity,
6296                    &mut metadata,
6297                    Some(&draw),
6298                )?;
6299                assert!(value.integrand_result.re.into_ff64().0.abs() < 1.0e-8);
6300                assert!(value.integrand_result.im.into_ff64().0.abs() < 1.0e-8);
6301                let absolute = value.absolute_integrand_result.unwrap();
6302                assert!((absolute.re.into_ff64().0 - 1.0).abs() < 1.0e-8);
6303                assert!((absolute.im.into_ff64().0 - 1.0).abs() < 1.0e-8);
6304                // Selected-channel MC has probability 1/2. Its individual
6305                // absolute weighted samples are also one, not two or one-half.
6306                for row in &draw.groups[0].1 {
6307                    let selected = super::GammaLoopSample {
6308                        groups: vec![(Some(GroupId(0)), vec![row.clone()])],
6309                    };
6310                    let value = super::evaluate_single(
6311                        &mut absolute_runtime,
6312                        EvaluationTarget::SamplingLaw(&signed_probe),
6313                        &selected,
6314                        &identity,
6315                        &mut metadata,
6316                        Some(&selected),
6317                    )?;
6318                    assert!(
6319                        (2.0 * value.absolute_integrand_result.unwrap().re.into_ff64().0 - 1.0)
6320                            .abs()
6321                            < 1.0e-8
6322                    );
6323                }
6324                // Opposite graph pieces at the SAME physical channel point
6325                // cancel before abs, including the imaginary component.
6326                let mut grouped = absolute_runtime.clone();
6327                grouped
6328                    .data
6329                    .graph_terms
6330                    .push(grouped.data.graph_terms[0].clone());
6331                grouped.data.graph_to_group_id.push(0);
6332                let mut grouped_draw = draw.clone();
6333                for row in &draw.groups[0].1 {
6334                    let mut partner = row.clone();
6335                    partner.graph_id = 1;
6336                    grouped_draw.groups[0].1.push(partner);
6337                }
6338                signed_probe.alternating_graph_sign = true;
6339                let canceled = super::evaluate_single(
6340                    &mut grouped,
6341                    EvaluationTarget::SamplingLaw(&signed_probe),
6342                    &grouped_draw,
6343                    &identity,
6344                    &mut metadata,
6345                    Some(&grouped_draw),
6346                )?;
6347                signed_probe.alternating_graph_sign = false;
6348                assert_eq!(canceled.integrand_result, Complex::new_re(draw.zero()));
6349                assert_eq!(
6350                    canceled.absolute_integrand_result,
6351                    Some(Complex::new_re(draw.zero()))
6352                );
6353            }
6354            // Exercise the pre-stability numerical failure boundary on this
6355            // generated physical cut without requiring a platform-dependent
6356            // Clarabel termination status. The original proposal stays fixed.
6357            let healthy_setup = integrand.get_graph(0).sampling_setup().clone();
6358            let overlap_failure = super::sampling::maps::SamplingEvaluationError::UncertifiedOverlap {
6359                detail: "Threshold SOCP returned AlmostPrimalInfeasible without a strictly interior center".into(),
6360            };
6361            let setup = integrand.get_graph_mut(0).sampling_setup_mut();
6362            if fixed256 {
6363                setup.sampling_bridge_fixed256.set(Err(overlap_failure));
6364            } else {
6365                setup.sampling_bridge_quad.set(Err(overlap_failure));
6366            }
6367            let failed = evaluate_from_source_precise(
6368                integrand,
6369                EvaluationTarget::Physical(model),
6370                EvaluationSource::XSpace(source),
6371                F(1.0),
6372                false,
6373                Complex::new_zero(),
6374            )?
6375            .try_into_f64()?;
6376            assert!(failed.evaluation_metadata.is_nan);
6377            assert!(failed.event_groups.is_empty());
6378            assert_eq!(
6379                failed.evaluation_metadata.final_precision(),
6380                Some(Precision::Arb)
6381            );
6382            assert_eq!(
6383                failed
6384                    .evaluation_metadata
6385                    .stability_results
6386                    .iter()
6387                    .map(|result| (result.precision, result.status.clone()))
6388                    .collect::<Vec<_>>(),
6389                [Precision::Double, Precision::Quad, Precision::Arb]
6390                    .into_iter()
6391                    .map(|precision| (
6392                        precision,
6393                        crate::integrands::evaluation::StabilityStatus::Unstable(0)
6394                    ))
6395                    .collect::<Vec<_>>()
6396            );
6397            assert!(
6398                failed
6399                    .evaluation_metadata
6400                    .threshold_counterterm_error
6401                    .as_ref()
6402                    .unwrap()
6403                    .contains("AlmostPrimalInfeasible")
6404            );
6405            assert!(
6406                serde_json::to_string(&failed.evaluation_metadata)?
6407                    .contains("AlmostPrimalInfeasible")
6408            );
6409            assert_eq!(
6410                failed.evaluation_metadata.evaluator_evaluation_time,
6411                std::time::Duration::ZERO
6412            );
6413            *integrand.get_graph_mut(0).sampling_setup_mut() = healthy_setup.clone();
6414            let following = evaluate_from_source_precise(
6415                integrand,
6416                EvaluationTarget::Physical(model),
6417                EvaluationSource::XSpace(source),
6418                F(1.0),
6419                false,
6420                Complex::new_zero(),
6421            )?
6422            .try_into_f64()?;
6423            assert!(!following.evaluation_metadata.is_nan);
6424            assert!(
6425                following
6426                    .evaluation_metadata
6427                    .threshold_counterterm_error
6428                    .is_none()
6429            );
6430            assert!(following.integrand_result.re.0.is_finite());
6431            assert!(following.integrand_result.im.0.is_finite());
6432            // Missing fixed-source metadata is a programming/configuration
6433            // error, not a failed numerical sample to record and skip.
6434            integrand
6435                .get_graph_mut(0)
6436                .sampling_setup_mut()
6437                .sampling_source = RuntimeCache::default();
6438            let malformed = evaluate_from_source_precise(
6439                integrand,
6440                EvaluationTarget::Physical(model),
6441                EvaluationSource::XSpace(source),
6442                F(1.0),
6443                false,
6444                Complex::new_zero(),
6445            )
6446            .unwrap_err();
6447            assert!(format!("{malformed:#}").contains("fixed sampling source is not initialized"));
6448            *integrand.get_graph_mut(0).sampling_setup_mut() = healthy_setup;
6449
6450            let unavailable = super::sampling::maps::SamplingEvaluationError::Unrepresentable {
6451                operation: "test-only fixed source poison",
6452                detail: "cannot redraw at Arb".into(),
6453            };
6454            let setup = integrand.get_graph_mut(0).sampling_setup_mut();
6455            if fixed256 {
6456                setup.sampling_bridge_fixed256.set(Err(unavailable));
6457            } else {
6458                setup.sampling_bridge_quad.set(Err(unavailable));
6459            }
6460            let failure = evaluate_from_source_precise(
6461                integrand,
6462                EvaluationTarget::SamplingLaw(&probe),
6463                EvaluationSource::XSpace(source),
6464                F(1.0),
6465                false,
6466                Complex::new_zero(),
6467            )
6468            .unwrap_err();
6469            assert!(format!("{failure:#}").contains("fixed source precision"));
6470            assert!(
6471                probe
6472                    .calls
6473                    .iter()
6474                    .all(|calls| calls.load(Ordering::Relaxed) == 0)
6475            );
6476            let input = super::MomentumSpaceEvaluationInput {
6477                loop_momenta: vec![ThreeMomentum::new(F(0.1), F(0.2), F(0.3))],
6478                integrator_weight: F(1.0),
6479                graph_id: Some(0),
6480                group_id: None,
6481                orientation: None,
6482                channel_id: None,
6483            };
6484            let raw = EvaluationSource::Momentum {
6485                input: &input,
6486                candidate_lmb: None,
6487            }
6488            .prepare_draw(integrand, &mut metadata)?
6489            .unwrap();
6490            assert_eq!(
6491                raw.get_default_sample().loop_moms().0[0].px,
6492                F(0.1).to_arb_exact()?
6493            );
6494            let selected_input = super::MomentumSpaceEvaluationInput {
6495                channel_id: Some(SamplingChannelId(0)),
6496                ..input
6497            };
6498            assert!(
6499                EvaluationSource::Momentum {
6500                    input: &selected_input,
6501                    candidate_lmb: None
6502                }
6503                .prepare_draw(integrand, &mut metadata)
6504                .is_err()
6505            );
6506        }
6507        runtime.get_mut_settings().sampling = original_sampling;
6508        // Fixed inputs and accuracy select an epoch, never an active point.
6509        let floor = F::<QuadFloat>::default().epsilon().sqrt().into_ff64().0;
6510        let settings = runtime.get_mut_settings();
6511        for level in &mut settings.stability.levels {
6512            level.required_precision_for_re = floor * 10.0;
6513            level.required_precision_for_im = floor * 10.0;
6514        }
6515        runtime.warm_up(model)?;
6516        let ProcessIntegrand::CrossSection(integrand) = runtime else {
6517            unreachable!()
6518        };
6519        let (precision, budget) = integrand.sampling_source_policy()?;
6520        assert!(budget >= floor);
6521        assert_eq!(precision, SamplingPrecision::Quad);
6522        runtime.get_mut_settings().stability.levels[2].required_precision_for_re = 1.0e-20;
6523        runtime.warm_up(model)?;
6524        let ProcessIntegrand::CrossSection(integrand) = runtime else {
6525            unreachable!()
6526        };
6527        assert_eq!(
6528            integrand
6529                .get_graph(0)
6530                .sampling_setup()
6531                .sampling_source
6532                .as_ref()
6533                .unwrap()
6534                .0,
6535            SamplingPrecision::Fixed256
6536        );
6537        let fixed256_floor = F::<SamplingFloat>::default().epsilon().sqrt().into_ff64().0;
6538        runtime.get_mut_settings().stability.levels[2].required_precision_for_re = fixed256_floor;
6539        runtime.warm_up(model)?;
6540        let ProcessIntegrand::CrossSection(integrand) = runtime else {
6541            unreachable!()
6542        };
6543        assert_eq!(
6544            integrand.sampling_source_policy()?.0,
6545            SamplingPrecision::Arb
6546        );
6547        assert!(
6548            integrand
6549                .get_graph(0)
6550                .sampling_setup()
6551                .sampling_bridge_fixed256
6552                .as_ref()
6553                .is_none()
6554        );
6555        Ok(())
6556    }
6557
6558    /// Exercise source-private lifetime boundaries from the generated host fixture.
6559    pub(crate) fn check_host_source_transport<I: super::ProcessIntegrandImpl>(
6560        integrand: &mut I,
6561        model: &crate::model::Model,
6562        source: &symbolica::numerical_integration::Sample<F<f64>>,
6563        graph_id: usize,
6564        channel_id: SamplingChannelId,
6565    ) -> color_eyre::Result<()> {
6566        use super::{EvaluationSource, gammaloop_sample::unwrap_sample};
6567        use crate::{
6568            integrands::evaluation::EvaluationMetaData,
6569            momentum::{Rotatable, Rotation, RotationMethod},
6570            utils::{QuadFloat, newton_solver::RadialRootDiagnostics},
6571        };
6572        use symbolica::numerical_integration::Sample;
6573
6574        assert_eq!(
6575            integrand
6576                .get_graph(0)
6577                .sampling_setup()
6578                .sampling_source
6579                .as_ref()
6580                .unwrap()
6581                .0,
6582            super::SamplingPrecision::Fixed256,
6583        );
6584        integrand.prepare_sampling_precision::<super::ArbPrec>()?;
6585        let mut metadata = EvaluationMetaData::new_empty();
6586        // Seed an existing physical occurrence so successful and failed source
6587        // preparation/debug operations must preserve nonempty history as well.
6588        metadata
6589            .radial_root_diagnostics
6590            .solve(
6591                &crate::utils::newton_solver::RadialRootIdentity::new(
6592                    "existing physical occurrence".into(),
6593                ),
6594                &F(0.0),
6595                &F(2.0),
6596                |r| (*r - F(1.0), F(1.0)),
6597                &F(1.0e-8),
6598                32,
6599                8,
6600                &F(1.0),
6601            )
6602            .unwrap();
6603        let original_source = EvaluationSource::XSpace(source);
6604        let mut anchor = original_source
6605            .prepare_draw(integrand, &mut metadata)?
6606            .unwrap();
6607        let sampling_preparation_time = metadata.canonical_sampling_preparation_time;
6608        assert!(sampling_preparation_time > std::time::Duration::ZERO);
6609        assert_eq!(metadata.parameterization_time, sampling_preparation_time);
6610        anchor.prepare_physical_overlaps(integrand, model, &mut metadata)?;
6611        // Clear only the test control's retained host so the physical owner must
6612        // solve its original Arb cut equation independently at this same point.
6613        let mut independent = anchor.clone();
6614        for (_, rows) in &mut independent.groups {
6615            for row in rows {
6616                row.prepared_lu_hosts.clear();
6617                row.physical_overlaps = None;
6618            }
6619        }
6620        let mut independent_metadata = EvaluationMetaData::new_empty();
6621        independent.prepare_physical_overlaps(integrand, model, &mut independent_metadata)?;
6622        let identity = Rotation::new(RotationMethod::Identity);
6623        let adopted_arb = super::evaluate_single(
6624            integrand,
6625            super::EvaluationTarget::Physical(model),
6626            &anchor,
6627            &identity,
6628            &mut EvaluationMetaData::new_empty(),
6629            Some(&anchor),
6630        )?;
6631        let independent_arb = super::evaluate_single(
6632            integrand,
6633            super::EvaluationTarget::Physical(model),
6634            &independent,
6635            &identity,
6636            &mut independent_metadata,
6637            Some(&independent),
6638        )?;
6639        for (adopted, solved) in [
6640            (
6641                &adopted_arb.integrand_result.re,
6642                &independent_arb.integrand_result.re,
6643            ),
6644            (
6645                &adopted_arb.integrand_result.im,
6646                &independent_arb.integrand_result.im,
6647            ),
6648        ] {
6649            let scale = adopted
6650                .abs()
6651                .max(solved.abs())
6652                .max(adopted.one() / adopted.from_usize(10).powi(25));
6653            assert!((adopted - solved).abs() <= scale / adopted.from_usize(10).powi(12));
6654        }
6655        assert_eq!(metadata.generated_event_count, 0);
6656        assert_eq!(metadata.accepted_event_count, 0);
6657        let preparation_time = metadata.canonical_physical_preparation_time;
6658        let prepared_source = EvaluationSource::Prepared {
6659            sample: &anchor,
6660            original: &original_source,
6661        };
6662        assert!(
6663            prepared_source
6664                .prepare_draw(integrand, &mut metadata)?
6665                .is_none()
6666        );
6667        let original = prepared_source.build_gamma_sample::<f64, _>(integrand, &mut metadata)?;
6668        assert_eq!(
6669            metadata.canonical_sampling_preparation_time,
6670            sampling_preparation_time
6671        );
6672        let prepared_lu_hosts = &original.groups[0].1[0].prepared_lu_hosts;
6673        assert_eq!(original.groups[0].1[0].channel_id, Some(channel_id));
6674        assert!(!prepared_lu_hosts.is_empty());
6675        for host in prepared_lu_hosts {
6676            assert_eq!(host.source.graph_id, graph_id);
6677            assert_eq!(host.source.generating_channel, channel_id);
6678            assert_eq!(host.source.target_channel, channel_id);
6679        }
6680        let history = format!("{:?}", metadata.radial_root_diagnostics);
6681        assert_ne!(history, format!("{:?}", RadialRootDiagnostics::default()));
6682        let rotation = Rotation::new(RotationMethod::Pi2Z);
6683        let rotated = original.rotate(&rotation, 17, 23);
6684        let canonical_overlaps = anchor.groups[0].1[0].physical_overlaps.as_ref().unwrap();
6685        assert!(std::sync::Arc::ptr_eq(
6686            canonical_overlaps,
6687            rotated.groups[0].1[0].physical_overlaps.as_ref().unwrap(),
6688        ));
6689        let retained = &rotated.groups[0].1[0].prepared_lu_hosts;
6690        assert_eq!(retained.len(), prepared_lu_hosts.len());
6691        for (before, after) in prepared_lu_hosts.iter().zip(retained) {
6692            assert_eq!(before.plan, after.plan);
6693            assert_eq!(before.source, after.source);
6694            assert_eq!(before.prior, after.prior);
6695            assert_eq!(before.solution.solution, after.solution.solution);
6696            assert_eq!(
6697                before.solution.derivative_at_solution,
6698                after.solution.derivative_at_solution
6699            );
6700            assert_eq!(
6701                before.solution.error_of_function,
6702                after.solution.error_of_function
6703            );
6704            assert_eq!(
6705                before.solution.num_iterations_used,
6706                after.solution.num_iterations_used
6707            );
6708            for t in [F(0.0), F(1.0), before.solution.solution] {
6709                assert_eq!(before.ray.evaluate(&t), after.ray.evaluate(&t));
6710            }
6711        }
6712        let original_momenta = original.get_default_sample().loop_moms();
6713        let rotated_momenta = rotated.get_default_sample().loop_moms();
6714        assert!(original_momenta.0.iter().any(|p| p != &p.rotate(&rotation)));
6715        for (before, after) in original_momenta.0.iter().zip(&rotated_momenta.0) {
6716            assert_eq!(*after, before.rotate(&rotation));
6717        }
6718        assert_eq!(rotated.get_default_sample().sample.loop_mom_cache_id, 17);
6719
6720        // Successful physical adoption must also respect a nonidentity probe.
6721        // Keep the original source/host payload and compare the complete raised
6722        // estimator and normal identity/probe event policy, without another
6723        // generation or grid.
6724        let mut baseline_metadata = metadata.clone();
6725        let baseline = super::evaluate_single(
6726            integrand,
6727            super::EvaluationTarget::Physical(model),
6728            &original,
6729            &Rotation::new(RotationMethod::Identity),
6730            &mut baseline_metadata,
6731            Some(&anchor),
6732        )?;
6733        let mut rotated_metadata = metadata.clone();
6734        let physical_rotated = super::evaluate_single(
6735            integrand,
6736            super::EvaluationTarget::Physical(model),
6737            &rotated,
6738            &rotation,
6739            &mut rotated_metadata,
6740            Some(&anchor),
6741        )?;
6742        assert_eq!(
6743            baseline_metadata.canonical_physical_preparation_time,
6744            preparation_time
6745        );
6746        assert_eq!(
6747            rotated_metadata.canonical_physical_preparation_time,
6748            preparation_time
6749        );
6750        // Both identity and stability-probe evaluator work contributes to E;
6751        // preparing or adopting the retained source does not repeat C_S.
6752        for physical_metadata in [&baseline_metadata, &rotated_metadata] {
6753            assert!(
6754                physical_metadata.evaluator_evaluation_time > metadata.evaluator_evaluation_time
6755            );
6756            assert_eq!(
6757                physical_metadata.canonical_sampling_preparation_time,
6758                sampling_preparation_time
6759            );
6760        }
6761        for result in [&baseline, &physical_rotated] {
6762            assert!(result.integrand_result.re.0.is_finite());
6763            assert!(result.integrand_result.im.0.is_finite());
6764            assert!(result.integrand_result.re.0.abs() + result.integrand_result.im.0.abs() > 0.0);
6765        }
6766        for (a, b) in [
6767            (
6768                baseline.integrand_result.re.0,
6769                physical_rotated.integrand_result.re.0,
6770            ),
6771            (
6772                baseline.integrand_result.im.0,
6773                physical_rotated.integrand_result.im.0,
6774            ),
6775        ] {
6776            assert!(
6777                (a - b).abs() <= 1.0e-8 * a.abs().max(b.abs()).max(1.0e-25),
6778                "rotated physical contribution {b} != {a}"
6779            );
6780        }
6781        let baseline_events = baseline
6782            .event_groups
6783            .iter()
6784            .flat_map(|group| group.iter())
6785            .collect::<Vec<_>>();
6786        assert!(!baseline_events.is_empty());
6787        assert!(
6788            baseline_events
6789                .iter()
6790                .all(|event| event.weight.re.0.is_finite() && event.weight.im.0.is_finite())
6791        );
6792        // prepare_buffered_event intentionally retains/counts identity events
6793        // only; a rotated probe builds transient selector data when needed.
6794        // The surrounding fixture compares identity selected/direct event weights.
6795        assert!(physical_rotated.event_groups.is_empty());
6796        assert_eq!(physical_rotated.generated_event_count, 0);
6797        assert_eq!(physical_rotated.accepted_event_count, 0);
6798
6799        // Losing a canonical accepted cut is not a request to choose a new
6800        // overlap. The real physical boundary must reject it before CT work.
6801        let mut incomplete = anchor.clone();
6802        let overlaps = std::sync::Arc::make_mut(
6803            incomplete.groups[0].1[0]
6804                .physical_overlaps
6805                .as_mut()
6806                .unwrap(),
6807        );
6808        let accepted = overlaps.iter_mut().find(|entry| entry.is_some()).unwrap();
6809        *accepted = None;
6810        let mut incomplete_metadata = metadata.clone();
6811        let error = super::evaluate_single(
6812            integrand,
6813            super::EvaluationTarget::Physical(model),
6814            &original,
6815            &Rotation::new(RotationMethod::Identity),
6816            &mut incomplete_metadata,
6817            Some(&incomplete),
6818        )
6819        .unwrap_err();
6820        assert!(
6821            error
6822                .downcast_ref::<super::sampling::maps::SamplingEvaluationError>()
6823                .is_some(),
6824            "{error:?}"
6825        );
6826        assert!(
6827            error.to_string().contains("selector/cut membership"),
6828            "{error:?}"
6829        );
6830        assert_eq!(
6831            incomplete_metadata.canonical_physical_preparation_time,
6832            preparation_time
6833        );
6834
6835        // A claimed selected record with another graph ID is corruption,
6836        // not an ordinary graph-group member for which no authority is supplied.
6837        let mut corrupted = original.clone();
6838        let claimed_hosts = &mut corrupted.groups[0].1[0].prepared_lu_hosts;
6839        claimed_hosts[0].source.graph_id = graph_id.wrapping_add(1);
6840        let reference =
6841            super::GaussianReferenceFunction::new(1.0, vec![0.0; 3 * original_momenta.0.len()])?;
6842        let error = super::evaluate_single(
6843            integrand,
6844            super::EvaluationTarget::Reference(&reference),
6845            &corrupted,
6846            &rotation,
6847            &mut metadata,
6848            None,
6849        )
6850        .unwrap_err();
6851        assert!(
6852            error.to_string().contains("graph-group master"),
6853            "{error:?}"
6854        );
6855        assert_eq!(history, format!("{:?}", metadata.radial_root_diagnostics));
6856
6857        // A valid alternate parent still does not belong to the selected
6858        // immutable channel. Exercise actual physical adoption, where this
6859        // mismatch and a forged active representative must fail before roots/events.
6860        for forge_parent in [true, false] {
6861            let mut corrupted = original.clone();
6862            let claimed_hosts = &mut corrupted.groups[0].1[0].prepared_lu_hosts;
6863            let plan = std::sync::Arc::make_mut(&mut claimed_hosts[0].plan);
6864            let expected = if forge_parent {
6865                assert!(plan.parent_lmb.len() >= 2);
6866                plan.parent_lmb.swap(0, 1);
6867                "selected channel definition"
6868            } else {
6869                plan.representative_cut_id.0 = plan.representative_cut_id.0.wrapping_add(1);
6870                "incompatible active representative"
6871            };
6872            let error = super::evaluate_single(
6873                integrand,
6874                super::EvaluationTarget::Physical(model),
6875                &corrupted,
6876                &rotation,
6877                &mut metadata,
6878                None,
6879            )
6880            .unwrap_err();
6881            assert!(error.to_string().contains(expected), "{error:?}");
6882            assert_eq!(history, format!("{:?}", metadata.radial_root_diagnostics));
6883        }
6884
6885        // Norm/debug now consume this same retained draw and cannot prepare
6886        // roots or advance the physical retry occurrence/history owner.
6887        let before_time = metadata.parameterization_time;
6888        prepared_source.loop_norm_sum(integrand, &mut metadata)?;
6889        assert_eq!(history, format!("{:?}", metadata.radial_root_diagnostics));
6890        prepared_source.debug_sample(integrand, &mut metadata)?;
6891        assert_eq!(history, format!("{:?}", metadata.radial_root_diagnostics));
6892        let mut large_factor = anchor.clone();
6893        large_factor.groups[0].1[0].sample.sample.jacobian *=
6894            F::<crate::utils::ArbPrec>::from_f64(2.0).powi(2048);
6895        let large_factor_source = EvaluationSource::Prepared {
6896            sample: &large_factor,
6897            original: &original_source,
6898        };
6899        assert_eq!(
6900            large_factor_source.loop_norm_sum(integrand, &mut metadata)?,
6901            prepared_source.loop_norm_sum(integrand, &mut metadata)?
6902        );
6903        assert!(
6904            large_factor_source
6905                .debug_sample(integrand, &mut metadata)
6906                .is_err()
6907        );
6908        assert_eq!(history, format!("{:?}", metadata.radial_root_diagnostics));
6909        let (selection, mut coordinates) = unwrap_sample::<f64>(source);
6910        coordinates[0] = F(f64::NAN);
6911        let invalid = Sample::Uniform(F(1.0), selection, coordinates);
6912        assert!(
6913            EvaluationSource::XSpace(&invalid)
6914                .loop_norm_sum(integrand, &mut metadata)
6915                .is_err()
6916        );
6917        assert_eq!(history, format!("{:?}", metadata.radial_root_diagnostics));
6918        assert!(
6919            EvaluationSource::XSpace(&invalid)
6920                .debug_sample(integrand, &mut metadata)
6921                .is_err()
6922        );
6923        assert_eq!(history, format!("{:?}", metadata.radial_root_diagnostics));
6924        assert!(metadata.parameterization_time >= before_time);
6925
6926        // Quad materializes the original canonical record directly; no earlier
6927        // Double geometry is promoted and no map/root is replayed.
6928        let quad = prepared_source.build_gamma_sample::<QuadFloat, _>(integrand, &mut metadata)?;
6929        let native = &quad.groups[0].1[0].prepared_lu_hosts;
6930        assert_eq!(native.len(), prepared_lu_hosts.len());
6931        for (before, after) in prepared_lu_hosts.iter().zip(native) {
6932            assert_eq!(before.plan, after.plan);
6933            assert_eq!(before.source, after.source);
6934            assert_eq!(before.prior.len(), after.prior.len());
6935        }
6936        let mut direct_momenta = original_momenta.0.clone();
6937        direct_momenta[0].px = F(0.1);
6938        let direct = super::MomentumSpaceEvaluationInput {
6939            loop_momenta: direct_momenta,
6940            integrator_weight: F(1.0),
6941            graph_id: Some(graph_id),
6942            group_id: None,
6943            channel_id: None,
6944            orientation: None,
6945        };
6946        let raw_source = EvaluationSource::Momentum {
6947            input: &direct,
6948            candidate_lmb: None,
6949        };
6950        let mut raw_anchor = raw_source.prepare_draw(integrand, &mut metadata)?.unwrap();
6951        raw_anchor.prepare_physical_overlaps(integrand, model, &mut metadata)?;
6952        assert!(
6953            raw_anchor
6954                .groups
6955                .iter()
6956                .flat_map(|(_, rows)| rows)
6957                .all(|row| {
6958                    row.channel_id.is_none()
6959                        && row.prepared_lu_hosts.is_empty()
6960                        && row.physical_overlaps.is_some()
6961                })
6962        );
6963        let raw_prepared = EvaluationSource::Prepared {
6964            sample: &raw_anchor,
6965            original: &raw_source,
6966        };
6967        let raw_quad = raw_prepared.build_gamma_sample::<QuadFloat, _>(integrand, &mut metadata)?;
6968        assert_eq!(
6969            raw_quad.get_default_sample().loop_moms().0[0].px.0,
6970            <QuadFloat as crate::utils::FloatLike>::from_f64_exact_binary(0.1)
6971        );
6972        assert!(
6973            raw_quad
6974                .groups
6975                .iter()
6976                .flat_map(|(_, rows)| rows)
6977                .all(|row| row.channel_id.is_none() && row.prepared_lu_hosts.is_empty())
6978        );
6979        // The UV/IR profile path enters through this same source owner. Large
6980        // exact input tokens make affine shifts smaller than binary64 spacing;
6981        // compare every physical edge after the production canonical routing.
6982        let selected_graph = integrand.get_graph(graph_id).get_graph().clone();
6983        let profile_input = super::MomentumSpaceEvaluationInput {
6984            loop_momenta: direct
6985                .loop_momenta
6986                .iter()
6987                .map(|_| {
6988                    ThreeMomentum::new(F(2_f64.powi(80)), F(-2_f64.powi(81)), F(2_f64.powi(82)))
6989                })
6990                .collect(),
6991            ..direct
6992        };
6993        let unrouted_source = EvaluationSource::Momentum {
6994            input: &profile_input,
6995            candidate_lmb: None,
6996        };
6997        let unrouted = unrouted_source
6998            .prepare_draw(integrand, &mut metadata)?
6999            .unwrap();
7000        let input_sample = unrouted.get_default_sample();
7001        let external_spatial = input_sample
7002            .sample
7003            .external_moms
7004            .iter()
7005            .map(|momentum| momentum.spatial.clone())
7006            .collect::<crate::momentum::sample::ExternalThreeMomenta<_>>();
7007        // Cross-section preprocessing promotes initial-state cut loops to
7008        // external momenta. Reuse its physical basis catalogue: a fresh spanning
7009        // tree enumeration would reintroduce those external loops.
7010        let setup = integrand.get_graph(graph_id).sampling_setup();
7011        assert_eq!(
7012            setup.graph.loop_momentum_basis,
7013            selected_graph.loop_momentum_basis
7014        );
7015        let candidates = setup.all_bases.clone();
7016        assert!(!candidates.is_empty());
7017        assert!(candidates.iter().any(|candidate| {
7018            candidate.loop_edges != selected_graph.loop_momentum_basis.loop_edges
7019        }));
7020        for candidate in &candidates {
7021            assert_eq!(candidate.loop_edges.len(), profile_input.loop_momenta.len());
7022            let profile_source = EvaluationSource::Momentum {
7023                input: &profile_input,
7024                candidate_lmb: Some(candidate),
7025            };
7026            let canonical = profile_source
7027                .prepare_draw(integrand, &mut metadata)?
7028                .unwrap();
7029            assert_eq!(canonical.groups[0].1[0].graph_id, graph_id);
7030            let prepared = EvaluationSource::Prepared {
7031                sample: &canonical,
7032                original: &profile_source,
7033            };
7034            let native =
7035                prepared.build_gamma_sample::<super::ArbPrec, _>(integrand, &mut metadata)?;
7036            let routed = native.get_default_sample();
7037            for (_, edge_id, _) in selected_graph.underlying.iter_edges() {
7038                let expected: ThreeMomentum<F<super::ArbPrec>> = candidate.edge_signatures[edge_id]
7039                    .compute_momentum(input_sample.loop_moms(), &external_spatial);
7040                let actual: ThreeMomentum<F<super::ArbPrec>> =
7041                    selected_graph.loop_momentum_basis.edge_signatures[edge_id]
7042                        .compute_momentum(routed.loop_moms(), &external_spatial);
7043                assert_eq!(
7044                    actual, expected,
7045                    "canonical profile source changed edge {edge_id:?}"
7046                );
7047            }
7048        }
7049        Ok(())
7050    }
7051
7052    /// Reuse the generated kite fixture while retaining the real source,
7053    /// partition and native stability owners. The final target alone is a probe.
7054    pub(super) fn check_joint_policy_source_replay(
7055        integrand: &super::ProcessIntegrand,
7056        model: &crate::model::Model,
7057        joint_id: super::SamplingChannelId,
7058        cube: &[f64],
7059    ) -> color_eyre::Result<()> {
7060        use super::{
7061            EvaluationSource, EvaluationTarget, GraphTerm, ProcessIntegrand, ProcessIntegrandImpl,
7062            SamplingLawProbe, evaluate_from_source_precise,
7063        };
7064        use crate::{
7065            integrands::evaluation::StabilityStatus,
7066            settings::runtime::{
7067                Precision, SamplingChannelWeight, SamplingSettingsParser, StabilityLevelSetting,
7068                SumMode,
7069            },
7070            utils::{ArbPrec, SamplingFloat, SamplingPrecision},
7071        };
7072        use spenso::algebra::complex::Complex;
7073        use std::sync::atomic::{AtomicUsize, Ordering};
7074        use symbolica::numerical_integration::Sample;
7075
7076        let mut runtime = integrand.clone();
7077        let mut parser: SamplingSettingsParser =
7078            toml::from_str(&toml::to_string(&runtime.get_settings().sampling)?)?;
7079        parser.graphs = SumMode::MonteCarlo;
7080        parser.orientations = SumMode::Summed;
7081        parser.sampling_channels = SumMode::MonteCarlo;
7082        parser.sampling_channel_weight = SamplingChannelWeight::SingularityProxy;
7083        for definitions in parser.channel_definitions.values_mut() {
7084            for definition in definitions.values_mut() {
7085                definition.singularity_proxy = Some("1".into());
7086            }
7087        }
7088        runtime.get_mut_settings().sampling = toml::from_str(&toml::to_string(&parser)?)?;
7089        let stability = &mut runtime.get_mut_settings().stability;
7090        stability.rotation_axis.clear();
7091        stability.escalate_if_exact_zero = false;
7092        stability.loop_momenta_norm_escalation_factor = 0.0;
7093        stability.levels = vec![
7094            StabilityLevelSetting::default_double(),
7095            StabilityLevelSetting::default_quad(),
7096        ];
7097        runtime.warm_up(model)?;
7098        let ProcessIntegrand::Amplitude(amplitude) = &mut runtime else {
7099            unreachable!("the fixture is the generated kite amplitude")
7100        };
7101        amplitude.prepare_sampling_precision::<ArbPrec>()?;
7102        let reference = amplitude.get_graph(0).sampling_setup().clone();
7103        assert_eq!(reference.sampling_bridge::<f64>()?.channels().len(), 2);
7104        let ordinary_name = reference
7105            .sampling_bridge::<f64>()?
7106            .channels()
7107            .iter()
7108            .find(|channel| channel.contract().support == super::SamplingSupport::Full)
7109            .unwrap()
7110            .name
7111            .clone();
7112        let mut probe = SamplingLawProbe {
7113            reference: &reference,
7114            channel_id: joint_id,
7115            radial_coordinate: 3,
7116            retry_lower_half: true,
7117            sign_coordinate: None,
7118            alternating_graph_sign: false,
7119            calls: std::array::from_fn(|_| AtomicUsize::new(0)),
7120        };
7121        for graph in amplitude.get_terms_mut() {
7122            let unavailable = super::sampling::maps::SamplingEvaluationError::Unrepresentable {
7123                operation: "test-only native map poison",
7124                detail: "physical retry must consume the canonical draw".into(),
7125            };
7126            graph
7127                .sampling_setup_mut()
7128                .sampling_bridge
7129                .set(Err(unavailable.clone()));
7130            graph
7131                .sampling_setup_mut()
7132                .sampling_bridge_quad
7133                .set(Err(unavailable));
7134        }
7135        for radial in [0.25, 0.75] {
7136            for calls in &probe.calls {
7137                calls.store(0, Ordering::Relaxed);
7138            }
7139            let mut coordinates = cube.to_vec();
7140            coordinates[3] = radial;
7141            // The outer weight includes the inverse channel probability 2.
7142            // Both support-gated constant proxies equal one on q_rho's patch.
7143            let source = Sample::Discrete(
7144                F(2.0),
7145                0,
7146                Some(Box::new(Sample::Discrete(
7147                    F(1.0),
7148                    joint_id.index(),
7149                    Some(Box::new(Sample::Continuous(
7150                        F(1.0),
7151                        coordinates.into_iter().map(F).collect(),
7152                    ))),
7153                ))),
7154            );
7155            let ProcessIntegrand::Amplitude(amplitude) = &mut runtime else {
7156                unreachable!()
7157            };
7158            let result = evaluate_from_source_precise(
7159                amplitude,
7160                EvaluationTarget::SamplingLaw(&probe),
7161                EvaluationSource::XSpace(&source),
7162                F(2.0),
7163                false,
7164                Complex::new_zero(),
7165            )?
7166            .try_into_f64()?;
7167            let retried = radial < 0.5;
7168            assert!(
7169                !result.evaluation_metadata.is_nan
7170                    && result.integrand_result.re.0.is_finite()
7171                    && result.integrand_result.im.0.is_finite(),
7172                "nonfinite final retry result at radial {radial}: {result:?}"
7173            );
7174            assert_eq!(
7175                probe.calls[0].load(Ordering::Relaxed),
7176                1,
7177                "Double body call count at radial {radial}: {:?}",
7178                result.evaluation_metadata.stability_results
7179            );
7180            assert_eq!(
7181                probe.calls[1].load(Ordering::Relaxed),
7182                usize::from(retried),
7183                "Quad body call count at radial {radial}: {:?}",
7184                result.evaluation_metadata.stability_results
7185            );
7186            assert_eq!(
7187                probe.calls[2].load(Ordering::Relaxed),
7188                0,
7189                "canonical preparation must not evaluate the target"
7190            );
7191            assert_eq!(
7192                result.evaluation_metadata.final_precision(),
7193                Some(if retried {
7194                    Precision::Quad
7195                } else {
7196                    Precision::Double
7197                })
7198            );
7199            assert_eq!(
7200                result.evaluation_metadata.stability_results.len(),
7201                1 + usize::from(retried)
7202            );
7203            if retried {
7204                let first = &result.evaluation_metadata.stability_results[0];
7205                assert_eq!(first.precision, Precision::Double);
7206                // A single identity probe is reported as Unknown regardless
7207                // of its acceptance. Body calls and the two native levels
7208                // above establish the intended nonfinite-Double retry.
7209                assert_eq!(first.status, StabilityStatus::Unknown);
7210            }
7211            assert!(
7212                (result.integrand_result.re.0 * result.integrator_weight.0 - 1.0).abs() < 1.0e-8
7213            );
7214            let policies = result
7215                .evaluation_metadata
7216                .sampling_proposal_policies
7217                .clone();
7218            assert!(!policies.is_collecting());
7219            assert_eq!(policies.len(), 1);
7220            let mut replay_metadata = result.evaluation_metadata.clone();
7221            let before = replay_metadata.parameterization_time;
7222            EvaluationSource::XSpace(&source).debug_sample(amplitude, &mut replay_metadata)?;
7223            assert!(replay_metadata.parameterization_time >= before);
7224            assert_eq!(
7225                replay_metadata.integrand_evaluation_time,
7226                result.evaluation_metadata.integrand_evaluation_time
7227            );
7228            assert_eq!(replay_metadata.sampling_proposal_policies, policies);
7229
7230            let mut quad_only = runtime.clone();
7231            quad_only.get_mut_settings().stability.levels =
7232                vec![StabilityLevelSetting::default_quad()];
7233            quad_only.warm_up(model)?;
7234            let ProcessIntegrand::Amplitude(amplitude) = &mut quad_only else {
7235                unreachable!()
7236            };
7237            let direct = evaluate_from_source_precise(
7238                amplitude,
7239                EvaluationTarget::SamplingLaw(&probe),
7240                EvaluationSource::XSpace(&source),
7241                F(2.0),
7242                false,
7243                Complex::new_zero(),
7244            )?
7245            .try_into_f64()?;
7246            assert!((result.integrand_result.re.0 - direct.integrand_result.re.0).abs() < 1.0e-10);
7247            assert_eq!(
7248                result.evaluation_metadata.sampling_proposal_policies,
7249                direct.evaluation_metadata.sampling_proposal_policies
7250            );
7251
7252            // An active-point norm may choose Quad immediately, but cannot
7253            // bypass the canonical phase or initialize policy in that lane.
7254            let mut norm_escalated = runtime.clone();
7255            norm_escalated
7256                .get_mut_settings()
7257                .stability
7258                .loop_momenta_norm_escalation_factor = 1.0e-6;
7259            norm_escalated.warm_up(model)?;
7260            let ProcessIntegrand::Amplitude(amplitude) = &mut norm_escalated else {
7261                unreachable!()
7262            };
7263            let escalated = evaluate_from_source_precise(
7264                amplitude,
7265                EvaluationTarget::SamplingLaw(&probe),
7266                EvaluationSource::XSpace(&source),
7267                F(2.0),
7268                false,
7269                Complex::new_zero(),
7270            )?
7271            .try_into_f64()?;
7272            assert_eq!(
7273                escalated.evaluation_metadata.final_precision(),
7274                Some(Precision::Quad)
7275            );
7276            assert_eq!(escalated.evaluation_metadata.stability_results.len(), 1);
7277            assert_eq!(
7278                escalated.evaluation_metadata.sampling_proposal_policies,
7279                policies
7280            );
7281            assert_eq!(escalated.integrand_result, direct.integrand_result);
7282        }
7283
7284        // Ordinary channels also retain one fixed canonical proposal, while
7285        // requiring no joint decisions even if unused joint definitions remain.
7286        parser.default_channel_selection = vec![ordinary_name];
7287        parser.channel_selection.clear();
7288        runtime.get_mut_settings().sampling = toml::from_str(&toml::to_string(&parser)?)?;
7289        runtime.warm_up(model)?;
7290        let ProcessIntegrand::Amplitude(amplitude) = &mut runtime else {
7291            unreachable!()
7292        };
7293        assert_eq!(
7294            amplitude.sampling_source_policy()?.0,
7295            SamplingPrecision::Fixed256
7296        );
7297        assert!(
7298            amplitude
7299                .get_graph(0)
7300                .sampling_setup()
7301                .sampling_bridge::<SamplingFloat>()
7302                .is_ok()
7303        );
7304        probe.retry_lower_half = false;
7305        let source = Sample::Uniform(F(1.0), vec![0, 0], cube.iter().copied().map(F).collect());
7306        let result = evaluate_from_source_precise(
7307            amplitude,
7308            EvaluationTarget::SamplingLaw(&probe),
7309            EvaluationSource::XSpace(&source),
7310            F(1.0),
7311            false,
7312            Complex::new_zero(),
7313        )?
7314        .try_into_f64()?;
7315        assert!(
7316            result
7317                .evaluation_metadata
7318                .sampling_proposal_policies
7319                .is_empty()
7320        );
7321        assert!(
7322            amplitude
7323                .get_graph(0)
7324                .sampling_setup()
7325                .sampling_bridge::<SamplingFloat>()
7326                .is_ok()
7327        );
7328        // A failed fixed proposal cannot choose a native map as a fallback,
7329        // regardless of which physical precision would otherwise be usable.
7330        for calls in &probe.calls {
7331            calls.store(0, Ordering::Relaxed);
7332        }
7333        amplitude
7334            .get_graph_mut(0)
7335            .sampling_setup_mut()
7336            .sampling_bridge_fixed256
7337            .set(Err(
7338                super::sampling::maps::SamplingEvaluationError::Unrepresentable {
7339                    operation: "test-only canonical map poison",
7340                    detail: "fixed proposal unavailable".into(),
7341                },
7342            ));
7343        let failure = evaluate_from_source_precise(
7344            amplitude,
7345            EvaluationTarget::SamplingLaw(&probe),
7346            EvaluationSource::XSpace(&source),
7347            F(1.0),
7348            false,
7349            Complex::new_zero(),
7350        )
7351        .unwrap_err();
7352        assert!(
7353            format!("{failure:#}").contains("fixed source precision"),
7354            "{failure:?}"
7355        );
7356        assert!(
7357            probe
7358                .calls
7359                .iter()
7360                .all(|calls| calls.load(Ordering::Relaxed) == 0)
7361        );
7362        Ok(())
7363    }
7364
7365    #[test]
7366    fn active_dependent_retry_changes_a_normalized_radial_law() {
7367        // Even identical unit determinants and inverse densities do not fix a
7368        // law when an active-dependent retry changes a continuous chart. Circle
7369        // maps x and x+delta are each uniform, but switching only x<delta leaves
7370        // [0,delta) empty. A normalized narrow target has mean zero rather than
7371        // one although the same-point J*q identity holds exactly in both maps.
7372        for denominator in [32, 1024, 16384] {
7373            let delta = 1.0 / f64::from(denominator);
7374            let original = 0.5 * delta;
7375            let switched = original + delta;
7376            assert!(original < delta && switched >= delta);
7377            assert_eq!(delta * (1.0 / delta), 1.0);
7378            assert_eq!(delta * 0.0, 0.0);
7379        }
7380
7381        // For f=q_rho, uniform R in (0,rho) has unit weight. If only the
7382        // lower-half draws replay with 2rho, they stay inside f's support but
7383        // receive weight two. This oracle is independent of map code.
7384        let retained: f64 = [0.25, 0.75].into_iter().map(|_| 1.0).sum::<f64>() / 2.0;
7385        let switched = [0.25, 0.75]
7386            .into_iter()
7387            .map(|u| {
7388                let radius = if u < 0.5 { 2.0 } else { 1.0 };
7389                let reference_density = if radius * u < 1.0 { 1.0 } else { 0.0 };
7390                radius * reference_density
7391            })
7392            .sum::<f64>()
7393            / 2.0;
7394        assert_eq!(retained, 1.0);
7395        assert_eq!(switched, 1.5);
7396        // Equal channel probabilities and a full ordinary sibling with its
7397        // unchanged unit conditional mean give a complete mean of 5/4.
7398        assert_eq!((switched + 1.0) / 2.0, 1.25);
7399    }
7400
7401    #[test]
7402    fn precise_stability_reporting_preserves_underflowed_estimates() {
7403        use super::{PreciseStabilityLevelResult, Precision};
7404        use crate::{
7405            integrands::evaluation::{
7406                EvaluationResult, GraphEvaluationResult, NumericalStabilityLevel,
7407                NumericalStabilityMedian, StatisticsCounter,
7408            },
7409            utils::ArbPrec,
7410        };
7411        use spenso::algebra::complex::Complex;
7412        use std::time::Duration;
7413
7414        let one = F::<ArbPrec>::default().one();
7415        for exponent in [400, 700] {
7416            // Construct a nonzero estimate in its active precision; the public
7417            // relative value may underflow, but its decimal exponent must survive.
7418            let level = PreciseStabilityLevelResult {
7419                result: Complex::new_re(one.clone()),
7420                graph_result: GraphEvaluationResult::zero(one.zero()),
7421                stability_level_used: Precision::Arb,
7422                estimated_relative_accuracy: Some(one.from_usize(10).powi(-exponent)),
7423                sample_count: 2,
7424                total_time: Duration::ZERO,
7425                parameterization_jacobian: None,
7426                is_stable: true,
7427                rotated_results: Vec::new(),
7428            };
7429            let mut evaluation = EvaluationResult::zero();
7430            evaluation
7431                .evaluation_metadata
7432                .stability_results
7433                .push(level.stability_result());
7434            let metadata = serde_json::to_value(&evaluation.evaluation_metadata).unwrap();
7435            let reported = metadata["stability_results"][0]["estimated_decimal_digits"]
7436                .as_f64()
7437                .unwrap();
7438            assert!((reported - f64::from(exponent)).abs() < 1.0e-10);
7439            let statistics = StatisticsCounter::from_evaluation_results(&[evaluation]);
7440            let Some(NumericalStabilityMedian::InRange {
7441                log10_relative_accuracy,
7442            }) = statistics
7443                .numerical_stability_snapshot()
7444                .median(NumericalStabilityLevel::ArbPrec)
7445            else {
7446                panic!("a finite precision estimate must retain its histogram value");
7447            };
7448            assert!((log10_relative_accuracy + f64::from(exponent)).abs() < 11.0);
7449        }
7450    }
7451
7452    #[test]
7453    fn stability_checks_reject_nonfinite_probes_at_every_level() {
7454        use super::{StabilityFailureReason, StabilityLevelSetting};
7455        use spenso::algebra::complex::Complex;
7456
7457        let level = StabilityLevelSetting::default_double();
7458        for check_on_norm in [false, true] {
7459            let check = if check_on_norm {
7460                super::stability_check_on_norm::<f64>
7461            } else {
7462                super::stability_check::<f64>
7463            };
7464            for is_final_level in [false, true] {
7465                for nonfinite in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
7466                    for invalid in [(nonfinite, 2.0), (2.0, nonfinite)] {
7467                        for probes in [
7468                            vec![invalid, (2.0, 3.0)],
7469                            vec![(2.0, 3.0), invalid],
7470                            vec![invalid],
7471                        ] {
7472                            let results = probes
7473                                .into_iter()
7474                                .map(|(re, im)| Complex::new(F(re), F(im)))
7475                                .collect::<Vec<_>>();
7476                            let (result, accuracy, stable, reason) = check(
7477                                None,
7478                                &results,
7479                                &level,
7480                                Complex::new_zero(),
7481                                F(1.0),
7482                                is_final_level,
7483                                false,
7484                            );
7485                            assert!(
7486                                !stable,
7487                                "nonfinite probe accepted: norm={check_on_norm}, final={is_final_level}, probes={results:?}"
7488                            );
7489                            assert!(accuracy.is_none());
7490                            assert_eq!(reason, Some(StabilityFailureReason::ErrorThreshold));
7491                            // Preserve even a nonfinite primary for the existing
7492                            // validity flag and later component sanitization.
7493                            assert_eq!(result.re.0.to_bits(), results[0].re.0.to_bits());
7494                            assert_eq!(result.im.0.to_bits(), results[0].im.0.to_bits());
7495                        }
7496                    }
7497                }
7498            }
7499        }
7500    }
7501
7502    #[test]
7503    fn real_only_stability_ignores_inactive_imaginary_probe() {
7504        use super::StabilityFailureReason;
7505        use crate::settings::runtime::StabilityLevelSetting;
7506        use spenso::algebra::complex::Complex;
7507
7508        let level = StabilityLevelSetting::default_double();
7509        let results = vec![Complex::new(F(2.0), F(1.0)), Complex::new(F(2.0), F(2.0))];
7510        let (_, _, real_stable, _) = super::stability_check_components(
7511            None,
7512            &results,
7513            &level,
7514            Complex::new(F(10.0), F(10.0)),
7515            F(1.0),
7516            false,
7517            false,
7518            true,
7519            false,
7520        );
7521        let (_, _, both_stable, reason) = super::stability_check_components(
7522            None,
7523            &results,
7524            &level,
7525            Complex::new(F(10.0), F(10.0)),
7526            F(1.0),
7527            false,
7528            false,
7529            true,
7530            true,
7531        );
7532        assert!(real_stable);
7533        assert!(!both_stable);
7534        assert_eq!(reason, Some(StabilityFailureReason::ErrorThreshold));
7535
7536        let nonfinite_im = vec![Complex::new(F(2.0), F(f64::NAN))];
7537        let (_, _, stable, _) = super::stability_check_components(
7538            None,
7539            &nonfinite_im,
7540            &level,
7541            Complex::new(F(10.0), F(10.0)),
7542            F(1.0),
7543            true,
7544            false,
7545            true,
7546            false,
7547        );
7548        assert!(stable);
7549    }
7550
7551    #[test]
7552    fn negligible_weight_waiver_requires_ten_percent_absolute_estimate() {
7553        use super::StabilityFailureReason;
7554        use crate::settings::runtime::StabilityLevelSetting;
7555        use spenso::algebra::complex::Complex;
7556
7557        let level = StabilityLevelSetting::default_double();
7558        let results = vec![Complex::new_re(F(1.0)), Complex::new_re(F(1.1))];
7559        let (_, _, waived, reason) = super::stability_check_components_with_estimate(
7560            None,
7561            &results,
7562            &level,
7563            Complex::new_zero(),
7564            F(1.0),
7565            false,
7566            false,
7567            true,
7568            false,
7569            Some((10.0, 0.5)),
7570            0.2,
7571        );
7572        assert!(waived);
7573        assert_eq!(reason, None);
7574
7575        let (_, _, unstable, reason) = super::stability_check_components_with_estimate(
7576            None,
7577            &results,
7578            &level,
7579            Complex::new_zero(),
7580            F(1.0),
7581            false,
7582            false,
7583            true,
7584            false,
7585            Some((10.0, 1.1)),
7586            0.2,
7587        );
7588        assert!(!unstable);
7589        assert_eq!(reason, Some(StabilityFailureReason::ErrorThreshold));
7590
7591        let (_, _, unstable, reason) = super::stability_check_components_with_estimate(
7592            None,
7593            &[Complex::new_re(F(1.0)), Complex::new_re(F(1.001))],
7594            &level,
7595            Complex::new_zero(),
7596            F(1.0),
7597            false,
7598            false,
7599            true,
7600            false,
7601            Some((10.0, 0.5)),
7602            0.2,
7603        );
7604        assert!(!unstable);
7605        assert_eq!(reason, Some(StabilityFailureReason::ErrorThreshold));
7606    }
7607
7608    #[test]
7609    fn stability_checks_preserve_finite_controls_at_every_level() {
7610        use super::{StabilityFailureReason, StabilityLevelSetting, StabilityStatus};
7611        use spenso::algebra::complex::Complex;
7612
7613        let level = StabilityLevelSetting::default_double();
7614        for check_on_norm in [false, true] {
7615            let check = if check_on_norm {
7616                super::stability_check_on_norm::<f64>
7617            } else {
7618                super::stability_check::<f64>
7619            };
7620            for is_final_level in [false, true] {
7621                for count in [1, 2] {
7622                    for (re, im) in [(0.0, 0.0), (2.0, 3.0)] {
7623                        let results = vec![Complex::new(F(re), F(im)); count];
7624                        let (result, accuracy, stable, reason) = check(
7625                            None,
7626                            &results,
7627                            &level,
7628                            Complex::new_zero(),
7629                            F(1.0),
7630                            is_final_level,
7631                            false,
7632                        );
7633                        assert_eq!(result, results[0]);
7634                        assert!(stable);
7635                        assert_eq!(reason, None);
7636                        if count == 1 {
7637                            assert!(accuracy.is_none());
7638                            assert_eq!(
7639                                StabilityStatus::from_sample_count(count, stable),
7640                                StabilityStatus::Unknown
7641                            );
7642                        } else {
7643                            assert_eq!(accuracy, Some(F(0.0)));
7644                        }
7645                    }
7646                }
7647
7648                let results = [Complex::new(F(2.0), F(3.0)), Complex::new(F(4.0), F(6.0))];
7649                let (result, accuracy, stable, reason) = check(
7650                    None,
7651                    &results,
7652                    &level,
7653                    Complex::new_zero(),
7654                    F(1.0),
7655                    is_final_level,
7656                    false,
7657                );
7658                assert!(!stable);
7659                assert!(accuracy.unwrap() > F(level.required_precision_for_re));
7660                assert_eq!(reason, Some(StabilityFailureReason::ErrorThreshold));
7661                assert_eq!(
7662                    result,
7663                    if check_on_norm {
7664                        results[0]
7665                    } else {
7666                        Complex::new(F(3.0), F(4.5))
7667                    }
7668                );
7669            }
7670        }
7671    }
7672
7673    #[test]
7674    fn stability_checks_bound_complete_underflow_without_cancellation() {
7675        use crate::{settings::runtime::StabilityLevelSetting, utils::ArbPrec};
7676        use spenso::algebra::complex::Complex;
7677
7678        let level = StabilityLevelSetting::default_arb();
7679        let one = F::<ArbPrec>::default().one();
7680        let minimum = F::<ArbPrec>::from_f64(f64::MIN_POSITIVE);
7681        // One probe is normal-sized, but the cancellation-free bound on the
7682        // returned average is 7/8 of MIN_POSITIVE, as in GL638 sample 3559.
7683        let probes = [
7684            Complex::new_re(&minimum / one.from_usize(2)),
7685            Complex::new_re(-&minimum * one.from_usize(5) / one.from_usize(4)),
7686        ];
7687        let average = (&probes[0] + &probes[1]) / one.from_usize(2);
7688        for norm in [false, true] {
7689            let check = if norm {
7690                super::stability_check_on_norm::<ArbPrec>
7691            } else {
7692                super::stability_check::<ArbPrec>
7693            };
7694            for (weight, expected) in [
7695                (one.clone(), true),
7696                (one.from_usize(2), false),
7697                (one.from_usize(10).powi(100), false),
7698            ] {
7699                let (result, accuracy, stable, _) = check(
7700                    None,
7701                    &probes,
7702                    &level,
7703                    Complex::new_re(one.zero()),
7704                    weight,
7705                    true,
7706                    false,
7707                );
7708                assert_eq!(stable, expected);
7709                assert!(accuracy.unwrap() > F::from_f64(level.required_precision_for_re));
7710                assert_eq!(
7711                    result,
7712                    if norm {
7713                        probes[0].clone()
7714                    } else {
7715                        average.clone()
7716                    }
7717                );
7718            }
7719            // Norm checking returns the primary rather than the average, so a
7720            // normal-sized primary cannot inherit the average's waiver.
7721            assert_eq!(
7722                check(
7723                    None,
7724                    &[probes[1].clone(), probes[0].clone()],
7725                    &level,
7726                    Complex::new_re(one.zero()),
7727                    one.clone(),
7728                    true,
7729                    false,
7730                )
7731                .2,
7732                !norm,
7733            );
7734            for invalid_weight in [f64::NAN, f64::INFINITY] {
7735                assert!(
7736                    !check(
7737                        None,
7738                        &probes,
7739                        &level,
7740                        Complex::new_re(one.zero()),
7741                        F::from_f64(invalid_weight),
7742                        true,
7743                        false,
7744                    )
7745                    .2
7746                );
7747            }
7748        }
7749        // A small signed average does not bound large cancelling probes.
7750        assert!(
7751            !super::stability_check(
7752                None,
7753                &[Complex::new_re(one.clone()), Complex::new_re(-one.clone())],
7754                &level,
7755                Complex::new_re(one.zero()),
7756                one,
7757                true,
7758                false,
7759            )
7760            .2
7761        );
7762    }
7763
7764    #[test]
7765    fn stability_underflow_waivers_are_componentwise() {
7766        use crate::{settings::runtime::StabilityLevelSetting, utils::ArbPrec};
7767        use spenso::algebra::complex::Complex;
7768
7769        let level = StabilityLevelSetting::default_arb();
7770        let one = F::<ArbPrec>::default().one();
7771        let tiny = F::<ArbPrec>::from_f64(f64::MIN_POSITIVE) / one.from_usize(4);
7772        for swap in [false, true] {
7773            for (second_normal, expected) in [(1, true), (2, false)] {
7774                let mut probes = [
7775                    Complex::new(tiny.clone(), one.clone()),
7776                    Complex::new(-tiny.clone(), one.from_usize(second_normal)),
7777                ];
7778                if swap {
7779                    for probe in &mut probes {
7780                        std::mem::swap(&mut probe.re, &mut probe.im);
7781                    }
7782                }
7783                assert_eq!(
7784                    super::stability_check(
7785                        None,
7786                        &probes,
7787                        &level,
7788                        Complex::new_re(one.zero()),
7789                        one.clone(),
7790                        true,
7791                        false,
7792                    )
7793                    .2,
7794                    expected,
7795                );
7796            }
7797        }
7798    }
7799
7800    #[test]
7801    fn stability_ecm_tolerance_preserves_energy_and_unit_scaling() {
7802        use crate::settings::runtime::{IntegralUnit, StabilityLevelSetting};
7803        use crate::utils::ArbPrec;
7804        use spenso::algebra::complex::Complex;
7805
7806        let one = F::<ArbPrec>::default().one();
7807        let mut level = StabilityLevelSetting::default_arb();
7808        level.ecm_relative_tolerance_for_re = 1e-5;
7809        for energy in [600, 1200] {
7810            // Cross sections, widths and a scalar two-loop amplitude have
7811            // different dimensions; the decision concerns a dimensionless ratio.
7812            for dimension in [-6, -2, 0, 1, 2] {
7813                for missing_measure in [0, 6] {
7814                    for unit in [
7815                        IntegralUnit::None,
7816                        IntegralUnit::Picobarn,
7817                        IntegralUnit::Femtobarn,
7818                    ] {
7819                        let scale = one.from_usize(energy).powi(dimension - missing_measure)
7820                            * super::cross_section::barn_conversion_factor(unit, one.clone());
7821                        let small = &scale / one.from_usize(1_000_000);
7822                        let probes = [
7823                            Complex::new_re(small.clone()),
7824                            Complex::new_re(&small * one.from_usize(3)),
7825                        ];
7826                        for norm in [false, true] {
7827                            let check = if norm {
7828                                super::stability_check_on_norm::<ArbPrec>
7829                            } else {
7830                                super::stability_check::<ArbPrec>
7831                            };
7832                            for (weight, expected) in [(1, true), (-1, true), (100, false)] {
7833                                let (result, accuracy, stable, _) = check(
7834                                    Some(&scale),
7835                                    &probes,
7836                                    &level,
7837                                    Complex::new_re(one.zero()),
7838                                    one.from_i64(weight),
7839                                    true,
7840                                    false,
7841                                );
7842                                assert_eq!(stable, expected);
7843                                assert!(
7844                                    accuracy.unwrap()
7845                                        > F::from_f64(level.required_precision_for_re)
7846                                );
7847                                assert_eq!(
7848                                    result.re,
7849                                    if norm {
7850                                        small.clone()
7851                                    } else {
7852                                        &small * one.from_usize(2)
7853                                    }
7854                                );
7855                            }
7856                        }
7857                    }
7858                }
7859            }
7860        }
7861        // A body below binary64 range must be multiplied in native precision:
7862        // its huge outer factor can put the disagreement well above the allowance.
7863        let tiny = one.from_usize(10).powi(-1000);
7864        let probes = [
7865            Complex::new_re(tiny.clone()),
7866            Complex::new_re(&tiny * one.from_usize(3)),
7867        ];
7868        level.ecm_relative_tolerance_for_re = 1e-201;
7869        assert!(
7870            !super::stability_check(
7871                Some(&one),
7872                &probes,
7873                &level,
7874                Complex::new_re(one.zero()),
7875                one.from_usize(10).powi(800),
7876                true,
7877                false
7878            )
7879            .2
7880        );
7881    }
7882
7883    #[test]
7884    fn stability_ecm_tolerance_bounds_the_returned_components() {
7885        use crate::settings::runtime::StabilityLevelSetting;
7886        use spenso::algebra::complex::Complex;
7887
7888        let scale = F(1.0);
7889        let mut level = StabilityLevelSetting::default_double();
7890        let probes = [Complex::new(F(1.0), F(0.0)), Complex::new(F(3.0), F(0.0))];
7891        for (allowance, expected_component, expected_norm) in [
7892            (0.0, false, false),
7893            (0.5, false, false),
7894            (1.0, true, false),
7895            (2.0, true, true),
7896        ] {
7897            level.ecm_relative_tolerance_for_re = allowance;
7898            for (norm, expected, returned) in
7899                [(false, expected_component, 2.0), (true, expected_norm, 1.0)]
7900            {
7901                let check = if norm {
7902                    super::stability_check_on_norm::<f64>
7903                } else {
7904                    super::stability_check::<f64>
7905                };
7906                let (value, accuracy, stable, _) = check(
7907                    Some(&scale),
7908                    &probes,
7909                    &level,
7910                    Complex::new_zero(),
7911                    F(1.0),
7912                    true,
7913                    false,
7914                );
7915                assert_eq!(stable, expected);
7916                assert_eq!(value.re, F(returned));
7917                assert!(accuracy.unwrap() > F(level.required_precision_for_re));
7918            }
7919        }
7920        // A real allowance cannot excuse an independently unstable imaginary
7921        // component; swapping Re and Im swaps the applicable setting too.
7922        for swap in [false, true] {
7923            let mut mixed = [Complex::new(F(1.0), F(4.0)), Complex::new(F(3.0), F(8.0))];
7924            level.ecm_relative_tolerance_for_re = 1.0;
7925            level.ecm_relative_tolerance_for_im = 0.0;
7926            if swap {
7927                for probe in &mut mixed {
7928                    std::mem::swap(&mut probe.re, &mut probe.im);
7929                }
7930                std::mem::swap(
7931                    &mut level.ecm_relative_tolerance_for_re,
7932                    &mut level.ecm_relative_tolerance_for_im,
7933                );
7934            }
7935            assert!(
7936                !super::stability_check(
7937                    Some(&scale),
7938                    &mixed,
7939                    &level,
7940                    Complex::new_zero(),
7941                    F(1.0),
7942                    true,
7943                    false
7944                )
7945                .2
7946            );
7947        }
7948        level.ecm_relative_tolerance_for_re = 0.5;
7949        let cancelling = [Complex::new_re(F(1.0)), Complex::new_re(F(-1.0))];
7950        assert!(
7951            !super::stability_check(
7952                Some(&scale),
7953                &cancelling,
7954                &level,
7955                Complex::new_zero(),
7956                F(1.0),
7957                true,
7958                false
7959            )
7960            .2
7961        );
7962        for check in [
7963            super::stability_check::<f64>,
7964            super::stability_check_on_norm::<f64>,
7965        ] {
7966            level.ecm_relative_tolerance_for_re = 10.0;
7967            for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
7968                assert!(
7969                    !check(
7970                        Some(&scale),
7971                        &probes,
7972                        &level,
7973                        Complex::new_zero(),
7974                        F(invalid),
7975                        true,
7976                        false
7977                    )
7978                    .2
7979                );
7980                assert!(
7981                    !check(
7982                        Some(&F(invalid)),
7983                        &probes,
7984                        &level,
7985                        Complex::new_zero(),
7986                        F(1.0),
7987                        true,
7988                        false
7989                    )
7990                    .2
7991                );
7992            }
7993        }
7994        // Finite differences do not make overflowing complete products safe.
7995        level.required_precision_for_re = 0.0;
7996        let large = [
7997            Complex::new_re(F(1e200)),
7998            Complex::new_re(F(1e200 * (1.0 - 1e-14))),
7999        ];
8000        assert!(
8001            !super::stability_check(
8002                Some(&F(1e308)),
8003                &large,
8004                &level,
8005                Complex::new_zero(),
8006                F(1e110),
8007                true,
8008                false
8009            )
8010            .2
8011        );
8012    }
8013
8014    #[test]
8015    fn stability_ecm_runtime_validation_precedes_orientation_checks() {
8016        let mut settings = RuntimeSettings::default();
8017        for component in ["re", "im"] {
8018            for invalid in [-1.0, f64::NAN, f64::INFINITY] {
8019                let level = &mut settings.stability.levels[0];
8020                level.ecm_relative_tolerance_for_re = 0.0;
8021                level.ecm_relative_tolerance_for_im = 0.0;
8022                if component == "re" {
8023                    level.ecm_relative_tolerance_for_re = invalid;
8024                } else {
8025                    level.ecm_relative_tolerance_for_im = invalid;
8026                }
8027                for explicit in [false, true] {
8028                    let error = validate_process_runtime_settings(&settings, explicit).unwrap_err();
8029                    assert!(
8030                        error
8031                            .to_string()
8032                            .contains(&format!("levels[0].ecm_relative_tolerance_for_{component}"))
8033                    );
8034                }
8035            }
8036        }
8037        settings.stability.levels[0].ecm_relative_tolerance_for_re = 1e-100;
8038        settings.stability.levels[0].ecm_relative_tolerance_for_im = 0.0;
8039        for invalid in [0.0, -1.0, f64::NAN, f64::INFINITY] {
8040            settings.kinematics.e_cm = invalid;
8041            assert!(
8042                validate_process_runtime_settings(&settings, false)
8043                    .unwrap_err()
8044                    .to_string()
8045                    .contains("kinematics.e_cm")
8046            );
8047        }
8048        settings.kinematics.e_cm = 600.0;
8049        // The physical dimension is admitted at its target boundary; generic
8050        // warmup also serves reference functions with their known dimensions.
8051        assert!(settings.stability.integrated_energy_dimension.is_none());
8052        validate_process_runtime_settings(&settings, false).unwrap();
8053    }
8054
8055    #[test]
8056    fn precise_event_normalization_preserves_prior_factors_and_partial_weights() {
8057        use crate::{
8058            observables::{
8059                AdditionalWeightKey, GenericEvent, GenericEventGroup, GenericEventGroupList,
8060            },
8061            utils::ArbPrec,
8062        };
8063        use spenso::algebra::complex::Complex;
8064
8065        let one = F::<ArbPrec>::default().one();
8066        let original = Complex::new_re(one.from_usize(3));
8067        let counterterm = Complex::new_re(-one.clone());
8068        let prior_factor = Complex::new_re(one.from_usize(5));
8069        let mut event = GenericEvent::<ArbPrec> {
8070            weight: (&original + &counterterm) * &prior_factor,
8071            ..Default::default()
8072        };
8073        event.additional_weights.weights.extend([
8074            (AdditionalWeightKey::Original, original.clone()),
8075            (
8076                AdditionalWeightKey::ThresholdCounterterm { subset_index: 0 },
8077                counterterm.clone(),
8078            ),
8079            (
8080                AdditionalWeightKey::FullMultiplicativeFactor,
8081                prior_factor.clone(),
8082            ),
8083        ]);
8084        let mut events = GenericEventGroupList(vec![GenericEventGroup(vec![event])]);
8085        let final_factor = super::full_event_multiplicative_factor_precise(
8086            Some(one.from_usize(7)),
8087            one.from_usize(11),
8088        );
8089        super::apply_full_event_multiplicative_factor_precise(&mut events, &final_factor);
8090        let event = &events[0][0];
8091        let weights = &event.additional_weights.weights;
8092        assert_eq!(weights[&AdditionalWeightKey::Original], original);
8093        assert_eq!(
8094            weights[&AdditionalWeightKey::ThresholdCounterterm { subset_index: 0 }],
8095            counterterm
8096        );
8097        assert_eq!(
8098            weights[&AdditionalWeightKey::FullMultiplicativeFactor],
8099            &prior_factor * &final_factor
8100        );
8101        assert_eq!(
8102            event.weight,
8103            (&original + &counterterm) * &weights[&AdditionalWeightKey::FullMultiplicativeFactor]
8104        );
8105        assert_eq!(event.weight, Complex::new_re(one.from_usize(770)));
8106    }
8107
8108    #[test]
8109    fn native_sampling_product_and_precise_reporting_preserve_full_range() {
8110        use crate::{
8111            integrands::evaluation::{
8112                EvaluationMetaData, GraphEvaluationResult, PreciseEvaluationResult,
8113            },
8114            integrands::process::sampling::reference::ReferenceMoments,
8115            observables::{GenericEvent, GenericEventGroup, GenericEventGroupList},
8116            settings::runtime::Precision,
8117            utils::ArbPrec,
8118        };
8119        use spenso::algebra::complex::Complex;
8120        use std::time::Duration;
8121
8122        let one = F::<ArbPrec>::default().one();
8123        let large = one.from_usize(10).powi(400);
8124        let small = large.clone().inv();
8125        assert!(large.clone().into_ff64().0.is_infinite());
8126        assert_eq!(small.clone().into_ff64().0, 0.0);
8127        let mut graph = GraphEvaluationResult::zero(one.zero());
8128        graph.integrand_result = Complex::new_re(small.clone());
8129        graph.reference_moments = Some(ReferenceMoments {
8130            second_moment: small.clone(),
8131            jacobian_min: 1.0,
8132            jacobian_max: 1.0,
8133        });
8134        graph.event_groups = GenericEventGroupList(vec![GenericEventGroup(vec![GenericEvent {
8135            weight: Complex::new_re(small.clone()),
8136            ..Default::default()
8137        }])]);
8138        // This is the same single owner used by selected and summed physical
8139        // maps and reference moments, before any reporting conversion.
8140        graph.apply_sampling_factor(large.clone());
8141        assert!((&graph.integrand_result.re - &one).abs() < one.epsilon() * one.from_usize(4));
8142        assert_eq!(graph.event_groups[0][0].weight, graph.integrand_result);
8143        assert_eq!(
8144            graph.reference_moments.as_ref().unwrap().second_moment,
8145            graph.integrand_result.re
8146        );
8147        let level = super::PreciseStabilityLevelResult {
8148            result: graph.integrand_result.clone(),
8149            graph_result: graph,
8150            stability_level_used: Precision::Arb,
8151            estimated_relative_accuracy: None,
8152            sample_count: 1,
8153            total_time: Duration::ZERO,
8154            parameterization_jacobian: Some(one.clone()),
8155            is_stable: true,
8156            rotated_results: Vec::new(),
8157        };
8158        let mut metadata = EvaluationMetaData::new_empty();
8159        metadata.canonical_sampling_preparation_time = Duration::from_micros(7);
8160        metadata.parameterization_time = Duration::from_micros(7) + Duration::from_micros(11);
8161        metadata.integrand_evaluation_time = Duration::from_micros(19) + Duration::from_micros(23);
8162        let result = super::finalize_precise_evaluation_result(level.clone(), F(1.0), metadata);
8163        assert_eq!(
8164            result.evaluation_metadata.parameterization_time,
8165            Duration::from_micros(18)
8166        );
8167        assert_eq!(
8168            result
8169                .evaluation_metadata
8170                .canonical_sampling_preparation_time,
8171            Duration::from_micros(7)
8172        );
8173        assert_eq!(
8174            result.evaluation_metadata.integrand_evaluation_time,
8175            Duration::from_micros(42)
8176        );
8177        assert_eq!(
8178            result.clone().try_into_f64().unwrap().integrand_result.re,
8179            F(1.0)
8180        );
8181        assert_eq!(result.event_groups[0][0].weight, result.integrand_result);
8182        // Cancellation can leave a finite signed result while the physical
8183        // absolute channel sum exceeds the ordinary reporting range.
8184        let mut excessive_absolute = result.clone();
8185        excessive_absolute.absolute_integrand_result = Some(Complex::new_re(large.clone()));
8186        assert!(excessive_absolute.try_into_f64().is_err());
8187        for extreme in [large.clone(), small.clone()] {
8188            let mut extreme_level = level.clone();
8189            extreme_level.result = Complex::new_re(extreme.clone());
8190            extreme_level.graph_result.integrand_result = extreme_level.result.clone();
8191            let precise = PreciseEvaluationResult::Arb(super::finalize_precise_evaluation_result(
8192                extreme_level,
8193                F(1.0),
8194                EvaluationMetaData::new_empty(),
8195            ));
8196            let PreciseEvaluationResult::Arb(value) = &precise else {
8197                unreachable!()
8198            };
8199            assert_eq!(value.integrand_result.re, extreme);
8200            assert!(!value.evaluation_metadata.is_nan);
8201            if extreme == large {
8202                assert!(
8203                    format!("{:#}", precise.try_into_f64().unwrap_err())
8204                        .contains("f64 integration/reporting boundary")
8205                );
8206            } else {
8207                assert_eq!(precise.try_into_f64().unwrap().integrand_result.re, F(0.0));
8208            }
8209        }
8210        // Correct final rounding is allowed for signed and absolute values,
8211        // including fully weighted event totals, while precise output stays native.
8212        let mut underflow = result.clone();
8213        underflow.integrand_result = Complex::new(small.clone(), -small.clone());
8214        underflow.absolute_integrand_result = Some(Complex::new(small.clone(), small.clone()));
8215        underflow.event_groups[0][0].weight = underflow.integrand_result.clone();
8216        let rounded = underflow.clone().try_into_f64().unwrap();
8217        assert_eq!(rounded.integrand_result, Complex::new_zero());
8218        assert_eq!(rounded.absolute_integrand_result, Some(Complex::new_zero()));
8219        assert_eq!(rounded.event_groups[0][0].weight, Complex::new_zero());
8220        assert_eq!(underflow.integrand_result.re, small);
8221
8222        let mut subnormal = result.clone();
8223        subnormal.integrand_result = Complex::new_re(one.from_usize(10).powi(320).inv());
8224        let subnormal_value = subnormal.integrand_result.re.into_ff64();
8225        assert!(subnormal_value.0 > 0.0 && subnormal_value.0 < f64::MIN_POSITIVE);
8226        assert_eq!(
8227            subnormal.try_into_f64().unwrap().integrand_result.re,
8228            subnormal_value
8229        );
8230
8231        // An outer-grid factor can rescue an otherwise underflowed intermediate;
8232        // reject that loss rather than silently returning a zero observation.
8233        underflow.integrator_weight = one.from_usize(10).powi(100);
8234        let error = format!("{:#}", underflow.clone().try_into_f64().unwrap_err());
8235        assert!(error.contains("integrand_result.re"));
8236        assert!(error.contains("complete contribution"));
8237        underflow.integrand_result = Complex::new_re(one.zero());
8238        assert!(
8239            format!("{:#}", underflow.clone().try_into_f64().unwrap_err())
8240                .contains("absolute_integrand_result.re")
8241        );
8242        underflow.absolute_integrand_result = None;
8243        // Event totals are already weighted; do not apply the outer weight twice.
8244        assert_eq!(
8245            underflow.clone().try_into_f64().unwrap().event_groups[0][0].weight,
8246            Complex::new_zero()
8247        );
8248        underflow.integrator_weight = one.clone();
8249        underflow.parameterization_jacobian = Some(one.from_usize(10).powi(100));
8250        underflow.integrand_result = Complex::new_im(-small.clone());
8251        assert!(
8252            format!("{:#}", underflow.clone().try_into_f64().unwrap_err())
8253                .contains("integrand_result.im")
8254        );
8255
8256        // Separately reported factors must themselves survive conversion: a
8257        // final underflow does not excuse an infinite serialized Jacobian.
8258        let mut unrepresentable_factor = result.clone();
8259        unrepresentable_factor.integrand_result = Complex::new_re(&small * &small);
8260        unrepresentable_factor.parameterization_jacobian = Some(large.clone());
8261        assert!(
8262            format!(
8263                "{:#}",
8264                unrepresentable_factor.clone().try_into_f64().unwrap_err()
8265            )
8266            .contains("parameterization_jacobian")
8267        );
8268        unrepresentable_factor.parameterization_jacobian = Some(small.clone());
8269        assert!(
8270            format!(
8271                "{:#}",
8272                unrepresentable_factor.clone().try_into_f64().unwrap_err()
8273            )
8274            .contains("parameterization_jacobian")
8275        );
8276        unrepresentable_factor.parameterization_jacobian = None;
8277        unrepresentable_factor.integrator_weight = large.clone();
8278        assert!(
8279            format!(
8280                "{:#}",
8281                unrepresentable_factor.clone().try_into_f64().unwrap_err()
8282            )
8283            .contains("integrator_weight")
8284        );
8285        unrepresentable_factor.integrator_weight = small.clone();
8286        assert!(
8287            format!("{:#}", unrepresentable_factor.try_into_f64().unwrap_err())
8288                .contains("integrator_weight")
8289        );
8290
8291        // Auxiliary event entries deliberately remain factorized. Their raw
8292        // components cannot be rounded away before the stored multiplier acts.
8293        let mut auxiliary = result.clone();
8294        auxiliary.event_groups[0][0]
8295            .additional_weights
8296            .weights
8297            .insert(
8298                crate::observables::AdditionalWeightKey::Original,
8299                Complex::new_re(small.clone()),
8300            );
8301        assert!(
8302            format!("{:#}", auxiliary.clone().try_into_f64().unwrap_err())
8303                .contains("additional_weights[Original].re")
8304        );
8305
8306        let full_factor_key = crate::observables::AdditionalWeightKey::FullMultiplicativeFactor;
8307        auxiliary.event_groups[0][0]
8308            .additional_weights
8309            .weights
8310            .insert(full_factor_key, Complex::new_re(one.clone()));
8311        assert_eq!(
8312            auxiliary.clone().try_into_f64().unwrap().event_groups[0][0]
8313                .additional_weights
8314                .weights[&crate::observables::AdditionalWeightKey::Original],
8315            Complex::new_zero(),
8316        );
8317        // An unknown component of a complex multiplier cannot certify a tail,
8318        // even when its other component is finite and small.
8319        auxiliary.event_groups[0][0]
8320            .additional_weights
8321            .weights
8322            .insert(
8323                full_factor_key,
8324                Complex::new(F::<ArbPrec>::from_f64(f64::NAN), one.clone()),
8325            );
8326        assert!(
8327            format!("{:#}", auxiliary.clone().try_into_f64().unwrap_err())
8328                .contains("additional_weights[Original].re")
8329        );
8330        // A complex multiplier can promote this tiny real component into a
8331        // meaningful imaginary contribution. It must survive before rounding.
8332        auxiliary.event_groups[0][0]
8333            .additional_weights
8334            .weights
8335            .insert(
8336                full_factor_key,
8337                Complex::new_im(one.from_usize(10).powi(100)),
8338            );
8339        assert!(
8340            format!("{:#}", auxiliary.try_into_f64().unwrap_err())
8341                .contains("additional_weights[Original].re")
8342        );
8343
8344        // A cancelling total does not make individually unrepresentable event
8345        // weights representable. Native APIs retain them; ordinary output errors.
8346        let mut cancelling_events = result;
8347        cancelling_events.event_groups[0].0 = vec![
8348            GenericEvent {
8349                weight: Complex::new_re(large.clone()),
8350                ..Default::default()
8351            },
8352            GenericEvent {
8353                weight: Complex::new_re(-large),
8354                ..Default::default()
8355            },
8356        ];
8357        assert!(cancelling_events.try_into_f64().is_err());
8358    }
8359
8360    #[test]
8361    fn complex_norm_stability_retains_finite_extreme_scales() {
8362        use spenso::algebra::complex::Complex;
8363
8364        let level = crate::settings::runtime::StabilityLevelSetting::default_double();
8365        for scale in [1e-200, 1.0, 1e200] {
8366            let primary = Complex::new_im(F(scale));
8367            for (rotated_scale, weight, stable) in [
8368                (scale, 1.0, true),
8369                (scale, -1.0, true),
8370                (2.0 * scale, 1.0, false),
8371                (2.0 * scale, -1.0, false),
8372            ] {
8373                let (result, accuracy, actual_stable, _) = super::stability_check_on_norm(
8374                    None,
8375                    &[primary, Complex::new_re(F(rotated_scale))],
8376                    &level,
8377                    Complex::new_zero(),
8378                    F(weight),
8379                    false,
8380                    false,
8381                );
8382                assert_eq!(result, primary);
8383                assert_eq!(actual_stable, stable, "scale={scale}");
8384                assert!(accuracy.unwrap().0.is_finite());
8385                if !stable {
8386                    // A tiny but normal binary64 contribution cannot pass merely
8387                    // because squaring its components would underflow to zero.
8388                    assert!(accuracy.unwrap().0 > 0.3);
8389                }
8390            }
8391        }
8392        for (probes, stable) in [
8393            ([Complex::new_re(F(1e308)); 2], true),
8394            (
8395                [Complex::new_re(F(1e308)), Complex::new_re(F(9e307))],
8396                false,
8397            ),
8398            ([Complex::new(F(f64::MAX), F(f64::MAX)); 2], false),
8399        ] {
8400            let (_, accuracy, actual_stable, _) = super::stability_check_on_norm(
8401                None,
8402                &probes,
8403                &level,
8404                Complex::new_zero(),
8405                F(1.0),
8406                false,
8407                false,
8408            );
8409            assert_eq!(actual_stable, stable, "probes={probes:?}");
8410            assert!(accuracy.is_none_or(|value| value.0.is_finite()));
8411        }
8412    }
8413
8414    #[test]
8415    fn amplitude_channel_norm_preserves_helicity_phase_covariance() {
8416        use spenso::algebra::complex::Complex;
8417
8418        let level = crate::settings::runtime::StabilityLevelSetting::default_double();
8419        let mut signed = Vec::new();
8420        let mut absolute = Vec::new();
8421        let mut channel_norm = Vec::new();
8422        for phase in [
8423            Complex::new(F(1.0), F(0.0)),
8424            Complex::new(
8425                F(std::f64::consts::FRAC_1_SQRT_2),
8426                F(std::f64::consts::FRAC_1_SQRT_2),
8427            ),
8428        ] {
8429            // Orthogonal channels have a phase-dependent componentwise absolute
8430            // sum: (1, 1) rotates to (sqrt(2), sqrt(2)), although both physical
8431            // channel norms and their signed total norm remain unchanged.
8432            let channels = [phase, Complex::new_im(F(1.0)) * phase];
8433            signed.push(channels[0] + channels[1]);
8434            absolute.push(channels.iter().fold(Complex::new_zero(), |sum, value| {
8435                sum + Complex::new(value.re.abs(), value.im.abs())
8436            }));
8437            channel_norm.push(Complex::new_re(
8438                channels
8439                    .iter()
8440                    .fold(F(0.0), |sum, value| sum + value.re.hypot(&value.im)),
8441            ));
8442        }
8443        for (probes, stable) in [(&signed, true), (&channel_norm, true), (&absolute, false)] {
8444            assert_eq!(
8445                super::stability_check_on_norm(
8446                    None,
8447                    probes,
8448                    &level,
8449                    Complex::new_zero(),
8450                    F(1.0),
8451                    false,
8452                    false,
8453                )
8454                .2,
8455                stable
8456            );
8457        }
8458        assert_eq!(absolute[0], Complex::new(F(1.0), F(1.0)));
8459        assert_eq!(channel_norm[0], Complex::new_re(F(2.0)));
8460
8461        // A private norm monitor follows the same real map factor as the
8462        // physical result, using its magnitude even for a negative factor.
8463        let mut result = super::GraphEvaluationResult::zero(F(0.0));
8464        result.integrand_result = signed[0];
8465        result.absolute_integrand_result = Some(absolute[0]);
8466        result.channel_norm_sum = Some(channel_norm[0].re);
8467        result.apply_sampling_factor(F(-3.0));
8468        assert_eq!(result.integrand_result, Complex::new(F(-3.0), F(-3.0)));
8469        assert_eq!(
8470            result.absolute_integrand_result,
8471            Some(Complex::new(F(3.0), F(3.0)))
8472        );
8473        assert_eq!(result.channel_norm_sum, Some(F(6.0)));
8474        let other = result.clone();
8475        result.merge_in_place(other);
8476        assert_eq!(result.channel_norm_sum, Some(F(12.0)));
8477    }
8478
8479    #[test]
8480    fn absolute_channel_instability_cannot_hide_behind_signed_cancellation() {
8481        use crate::settings::runtime::StabilityLevelSetting;
8482        use spenso::algebra::complex::Complex;
8483
8484        let mut level = StabilityLevelSetting::default_double();
8485        level.ecm_relative_tolerance_for_re = 0.1;
8486        level.ecm_relative_tolerance_for_im = 0.1;
8487        let signed = [Complex::new_zero(), Complex::new_zero()];
8488        let absolute = [Complex::new(F(2.0), F(4.0)), Complex::new(F(3.0), F(6.0))];
8489        for check in [
8490            super::stability_check::<f64>,
8491            super::stability_check_on_norm::<f64>,
8492        ] {
8493            assert!(
8494                check(
8495                    Some(&F(1.0)),
8496                    &signed,
8497                    &level,
8498                    Complex::new_zero(),
8499                    F(1.0),
8500                    false,
8501                    false
8502                )
8503                .2
8504            );
8505            assert!(
8506                !check(
8507                    Some(&F(1.0)),
8508                    &absolute,
8509                    &level,
8510                    Complex::new_zero(),
8511                    F(1.0),
8512                    false,
8513                    false
8514                )
8515                .2
8516            );
8517        }
8518        // Equal and opposite channels can keep the signed total exactly zero
8519        // while their individual norms change. The private amplitude monitor
8520        // must still reject this variation.
8521        let channel_norm = [Complex::new_re(F(2.0)), Complex::new_re(F(3.0))];
8522        assert!(
8523            !super::stability_check_on_norm(
8524                None,
8525                &channel_norm,
8526                &level,
8527                Complex::new_zero(),
8528                F(1.0),
8529                false,
8530                false,
8531            )
8532            .2
8533        );
8534    }
8535
8536    #[test]
8537    fn explicit_orientation_sum_rejects_runtime_filters_and_ltd() {
8538        let mut settings = RuntimeSettings::default();
8539        settings.general.orientation_pat = OrientationPattern::from_user_pattern("(+)").unwrap();
8540        let error = validate_process_runtime_settings(&settings, true).unwrap_err();
8541        assert!(error.to_string().contains("orientation_pat` must be unset"));
8542
8543        validate_process_runtime_settings(&settings, false).unwrap();
8544
8545        settings.general.orientation_pat = OrientationPattern::default();
8546        settings.general.use_ltd = true;
8547        let error = validate_process_runtime_settings(&settings, false).unwrap_err();
8548        assert!(error.to_string().contains("deferred proper-LTD support"));
8549    }
8550
8551    #[test]
8552    fn numerator_sampling_scale_requires_nonzero_runtime_value() {
8553        let mut settings = RuntimeSettings::default();
8554        validate_process_runtime_settings(&settings, false).unwrap();
8555
8556        for scale in [0.0, -0.0] {
8557            settings.general.numerator_sampling_scale = scale;
8558            for explicit_orientation_sum_only in [false, true] {
8559                let error =
8560                    validate_process_runtime_settings(&settings, explicit_orientation_sum_only)
8561                        .unwrap_err();
8562                assert!(error.to_string().contains("sampling scale M"));
8563            }
8564        }
8565
8566        settings.general.numerator_sampling_scale = -2.0;
8567        validate_process_runtime_settings(&settings, false).unwrap();
8568    }
8569
8570    #[test]
8571    fn grouped_orientation_sampling_requires_identical_exact_map_catalogs() {
8572        let master = ["O[+0]|M[0]", "O[+0]|M[1]"];
8573        validate_orientation_catalog_group(
8574            GroupId(3),
8575            [
8576                ("master".to_string(), master.to_vec()),
8577                ("matching".to_string(), master.to_vec()),
8578            ],
8579        )
8580        .unwrap();
8581
8582        let error = validate_orientation_catalog_group(
8583            GroupId(3),
8584            [
8585                ("master".to_string(), master.to_vec()),
8586                ("different".to_string(), vec!["O[+0]|M[0]", "O[+0]|M[2]"]),
8587            ],
8588        )
8589        .unwrap_err();
8590        assert!(
8591            error
8592                .to_string()
8593                .contains("different exact residue-map catalogs")
8594        );
8595        assert!(error.to_string().contains("first difference at channel 1"));
8596    }
8597
8598    #[test]
8599    fn runtime_cache_serializes_as_empty() {
8600        let encoded = bincode::encode_to_vec(
8601            RuntimeCache::<usize>::default(),
8602            bincode::config::standard(),
8603        )
8604        .expect("runtime cache should encode");
8605        assert!(encoded.is_empty());
8606
8607        let (decoded, consumed): (RuntimeCache<usize>, usize) =
8608            bincode::decode_from_slice(&encoded, bincode::config::standard())
8609                .expect("runtime cache should decode");
8610        assert_eq!(consumed, 0);
8611        assert!(decoded.as_ref().is_none());
8612        let mut source = RuntimeCache::default();
8613        source.set((super::Precision::Quad, 1.0e-13));
8614        let encoded_source = bincode::encode_to_vec(&source, bincode::config::standard()).unwrap();
8615        assert!(encoded_source.is_empty());
8616        let (decoded_source, consumed): (RuntimeCache<(super::Precision, f64)>, usize) =
8617            bincode::decode_from_slice(&encoded_source, bincode::config::standard()).unwrap();
8618        assert_eq!(consumed, 0);
8619        assert!(decoded_source.as_ref().is_none());
8620        // Cached preparation failures are as transient as compiled bridges;
8621        // neither payload needs a codec or may enter a saved state.
8622        let mut failure: RuntimeCache<
8623            Result<super::SamplingChannelBridge, super::sampling::maps::SamplingEvaluationError>,
8624        > = RuntimeCache::default();
8625        failure.set(Err(
8626            super::sampling::maps::SamplingEvaluationError::UncertainGeometry {
8627                detail: "numeric binding fixture".into(),
8628            },
8629        ));
8630        assert!(
8631            bincode::encode_to_vec(&failure, bincode::config::standard())
8632                .unwrap()
8633                .is_empty()
8634        );
8635    }
8636
8637    #[test]
8638    fn arbitrary_precision_override_prefers_the_configured_arb_level() {
8639        let mut settings = StabilitySettings::default();
8640        let arb_level = settings
8641            .levels
8642            .iter_mut()
8643            .find(|level| level.precision == Precision::Arb)
8644            .expect("default stability settings should include Arb");
8645        arb_level.required_precision_for_re = 2.5e-7;
8646        let configured_arb_level = *arb_level;
8647
8648        assert_eq!(
8649            create_stability_iterator(&settings, true),
8650            vec![configured_arb_level]
8651        );
8652        assert_eq!(create_stability_iterator(&settings, false), settings.levels);
8653    }
8654
8655    #[test]
8656    fn filtered_orientation_helpers_map_visible_indices_into_subset_order() {
8657        let orientations = TiVec::<OrientationID, EdgeVec<Orientation>>::from_iter([
8658            EdgeVec::from_iter([Orientation::Default]),
8659            EdgeVec::from_iter([Orientation::Reversed]),
8660            EdgeVec::from_iter([Orientation::Undirected]),
8661            EdgeVec::from_iter([Orientation::Default]),
8662        ]);
8663        let mut filter = SubSet::empty(orientations.len());
8664        filter.add(OrientationID(1));
8665        filter.add(OrientationID(3));
8666
8667        assert_eq!(filtered_orientation_count(&filter, &orientations), 2);
8668        assert_eq!(
8669            resolve_visible_orientation_id(&filter, 0),
8670            Some(OrientationID(1))
8671        );
8672        assert_eq!(
8673            resolve_visible_orientation_id(&filter, 1),
8674            Some(OrientationID(3))
8675        );
8676        assert_eq!(resolve_visible_orientation_id(&filter, 2), None);
8677    }
8678
8679    #[test]
8680    fn discrete_acceptance_selection_decodes_canonical_channel_id() {
8681        let settings = SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
8682            sample_orientations: true,
8683            sampling_type: DiscreteGraphSamplingType::SamplingMultiChanneling(
8684                MultiChannelingSettings::default(),
8685            ),
8686            ..Default::default()
8687        });
8688
8689        let selected = super::resolve_discrete_selection_for_sampling(
8690            &settings,
8691            &[2, 5, 7],
8692            3,
8693            |_| Some(6),
8694            |_| Ok::<Option<usize>, eyre::Report>(Some(8)),
8695        )
8696        .unwrap();
8697        assert_eq!(selected.0, Some(GroupId(2)));
8698        assert_eq!(selected.1, Some(5));
8699        assert_eq!(selected.2, Some(SamplingChannelId::from(7)));
8700
8701        let error = super::resolve_discrete_selection_for_sampling(
8702            &settings,
8703            &[2, 5, 8],
8704            3,
8705            |_| Some(6),
8706            |_| Ok::<Option<usize>, eyre::Report>(Some(8)),
8707        )
8708        .unwrap_err();
8709        assert!(error.to_string().contains("Channel 8 is out of range"));
8710    }
8711
8712    #[test]
8713    fn sampling_channel_basis_ids_use_graph_override_or_optimized_channels() {
8714        test_initialise().unwrap();
8715        static GRAPH: OnceLock<Graph> = OnceLock::new();
8716        let graph = GRAPH
8717            .get_or_init(|| {
8718                dot!(
8719                    digraph lmb_basis_selection {
8720                        edge [num=1 mass=0]
8721                        node [num=1]
8722                        A -> B [id=0]
8723                        A -> B [id=1]
8724                        A -> B [id=2]
8725                    }
8726                )
8727                .unwrap()
8728            })
8729            .clone();
8730        let lmb = |edge_id| LoopMomentumBasis {
8731            tree: graph.underlying.empty_subgraph(),
8732            loop_edges: vec![EdgeIndex::from(edge_id)].into(),
8733            ext_edges: Vec::new().into(),
8734            edge_signatures: graph.underlying.new_edgevec(|_, _, _| {
8735                LoopExtSignature::from((Vec::<isize>::new(), Vec::<isize>::new()))
8736            }),
8737        };
8738        let all_bases = vec![lmb(0), lmb(1), lmb(2)].into();
8739        let setup = LmbMultiChannelingSetup {
8740            master_edge_masses: Default::default(),
8741            sampling_bridge: Default::default(),
8742            sampling_bridge_quad: Default::default(),
8743            sampling_bridge_fixed256: Default::default(),
8744            sampling_bridge_arb: Default::default(),
8745            sampling_source: Default::default(),
8746            sampling_catalogue: Default::default(),
8747            sampling_programs: Default::default(),
8748            lmb_basis_ids: vec![LmbIndex::from(2), LmbIndex::from(0)].into(),
8749            graph,
8750            all_bases,
8751        };
8752        let default_settings = ParameterizationSettings::default();
8753        let override_settings = ParameterizationSettings {
8754            lmb_basis_ids: std::collections::BTreeMap::from([(setup.graph.name.clone(), vec![1])]),
8755            ..Default::default()
8756        };
8757        let out_of_range_settings = ParameterizationSettings {
8758            lmb_basis_ids: std::collections::BTreeMap::from([(setup.graph.name.clone(), vec![3])]),
8759            ..Default::default()
8760        };
8761
8762        assert_eq!(
8763            setup
8764                .selected_lmb_basis_id(&setup.graph.name, &default_settings)
8765                .unwrap(),
8766            LmbIndex::from(2)
8767        );
8768        assert_eq!(
8769            setup
8770                .selected_lmb_basis_id(&setup.graph.name, &override_settings)
8771                .unwrap(),
8772            LmbIndex::from(1)
8773        );
8774        assert_eq!(
8775            setup
8776                .sampling_channel_ids(&setup.graph.name, &override_settings)
8777                .unwrap(),
8778            vec![SamplingChannelId::from(0)]
8779        );
8780        // The canonical IDs retain their one-domain ordering while resolving
8781        // to the generated LMB basis shown in diagnostics.
8782        assert_eq!(
8783            [SamplingChannelId::from(0)]
8784                .into_iter()
8785                .map(|channel_id| {
8786                    setup
8787                        .sampling_channel_lmb_id(channel_id, &setup.graph.name, &override_settings)
8788                        .unwrap()
8789                })
8790                .collect::<Vec<_>>(),
8791            vec![Some(LmbIndex::from(1))]
8792        );
8793        assert_eq!(
8794            setup
8795                .sampling_channel_lmb_basis_id(
8796                    SamplingChannelId::from(0),
8797                    &setup.graph.name,
8798                    &override_settings,
8799                )
8800                .unwrap(),
8801            LmbIndex::from(1)
8802        );
8803        assert_eq!(
8804            setup
8805                .sampling_channel_edge_ids(
8806                    SamplingChannelId::from(0),
8807                    &setup.graph.name,
8808                    &override_settings,
8809                )
8810                .unwrap()
8811                .as_slice(),
8812            &[1]
8813        );
8814        assert!(
8815            setup
8816                .selected_lmb_basis_id(&setup.graph.name, &out_of_range_settings)
8817                .is_err()
8818        );
8819
8820        for preset in ["auto:lmb", "auto:optimized_lmb", "auto:surfaces"] {
8821            let mut ordered_settings = ParameterizationSettings {
8822                lmb_basis_ids: std::collections::BTreeMap::from([(
8823                    setup.graph.name.clone(),
8824                    vec![2, 1],
8825                )]),
8826                ..Default::default()
8827            };
8828            ordered_settings.sampling_channels.default_channel_selection = vec![preset.into()];
8829            let ids = setup
8830                .sampling_channel_ids(&setup.graph.name, &ordered_settings)
8831                .unwrap();
8832            let edges = ids
8833                .into_iter()
8834                .map(|id| {
8835                    setup
8836                        .sampling_channel_edge_ids(id, &setup.graph.name, &ordered_settings)
8837                        .unwrap()
8838                        .to_vec()
8839                })
8840                .collect::<Vec<_>>();
8841            assert_eq!(
8842                edges,
8843                vec![vec![2], vec![1]],
8844                "{preset} must preserve the exact user override"
8845            );
8846        }
8847
8848        let mut explicit_settings = override_settings.clone();
8849        explicit_settings
8850            .sampling_channels
8851            .default_channel_selection = vec!["lmb(1)".into()];
8852        assert_eq!(
8853            setup
8854                .selected_lmb_basis_id(&setup.graph.name, &explicit_settings)
8855                .unwrap(),
8856            LmbIndex::from(1)
8857        );
8858        explicit_settings
8859            .sampling_channels
8860            .default_channel_selection = vec!["lmb(2)".into()];
8861        let error = setup
8862            .canonical_sampling_catalogue(&setup.graph.name, &explicit_settings)
8863            .unwrap_err();
8864        assert!(
8865            matches!(error.downcast_ref::<SamplingSelectionError>(), Some(SamplingSelectionError::ExcludedLmb { edges, basis_ids, .. }) if edges == &[2] && basis_ids == &[2])
8866        );
8867        explicit_settings
8868            .sampling_channels
8869            .default_channel_selection = vec!["lmb(99)".into()];
8870        let error = setup
8871            .canonical_sampling_catalogue(&setup.graph.name, &explicit_settings)
8872            .unwrap_err();
8873        assert!(
8874            matches!(error.downcast_ref::<SamplingSelectionError>(), Some(SamplingSelectionError::MissingLmb { edges, .. }) if edges == &[99])
8875        );
8876
8877        explicit_settings
8878            .lmb_basis_ids
8879            .insert(setup.graph.name.clone(), vec![2, 1]);
8880        explicit_settings
8881            .sampling_channels
8882            .default_channel_selection = vec!["lmb(1)".into(), "lmb(2)".into()];
8883        let catalogue = setup
8884            .canonical_sampling_catalogue(&setup.graph.name, &explicit_settings)
8885            .unwrap();
8886        assert_eq!(
8887            catalogue
8888                .lmb_basis_entries()
8889                .map(|(id, _)| id)
8890                .collect::<Vec<_>>(),
8891            vec![1, 2]
8892        );
8893
8894        let mut physical_settings = ParameterizationSettings::default();
8895        let parent_lmb = setup
8896            .graph
8897            .loop_momentum_basis
8898            .loop_edges
8899            .iter()
8900            .map(|edge| edge.0)
8901            .collect::<Vec<_>>();
8902        physical_settings
8903            .sampling_channels
8904            .default_channel_selection = vec!["cut".into()];
8905        physical_settings
8906            .sampling_channels
8907            .channel_definitions
8908            .entry(setup.graph.name.clone())
8909            .or_default()
8910            .insert(
8911                "cut".into(),
8912                SamplingChannelDefinition {
8913                    channel_weight: None,
8914                    around: "phase_space(cut(0))".into(),
8915                    subspace_lmb: parent_lmb.clone(),
8916                    parent_lmb,
8917                    on_cut: vec![],
8918                    singularity_proxy: None,
8919                    radial_profile: None,
8920                },
8921            );
8922        let catalogue = setup
8923            .canonical_sampling_catalogue(&setup.graph.name, &physical_settings)
8924            .unwrap();
8925        let dimension = 3 * setup.graph.get_loop_number();
8926        let programs = catalogue
8927            .compile_programs(dimension, &HFunctionSettings::default())
8928            .unwrap();
8929        let context = SamplingChannelCompileContext::<f64>::new(
8930            setup.graph.name.clone(),
8931            setup
8932                .graph
8933                .loop_momentum_basis
8934                .loop_edges
8935                .iter()
8936                .map(|edge| edge.0)
8937                .collect(),
8938            physical_settings,
8939            1.0,
8940            setup.graph.get_loop_number(),
8941        );
8942        // Physical labels alone do not provide their native cut geometry.
8943        assert!(
8944            setup
8945                .compile_sampling_channel_bridge(&catalogue, &programs, &context)
8946                .is_err()
8947        );
8948
8949        // Graph-aware entries occupy the same canonical channel axis as LMB
8950        // entries.  In particular, inserting a named channel before the
8951        // generated LMBs must not make channel id 1 resolve as basis 1 by
8952        // positional filtering.
8953        let mut mixed_settings = ParameterizationSettings::default();
8954        mixed_settings.sampling_channels.default_channel_selection =
8955            vec!["named".into(), "auto:lmb".into()];
8956        mixed_settings
8957            .sampling_channels
8958            .channel_definitions
8959            .entry(setup.graph.name.clone())
8960            .or_default()
8961            .insert(
8962                "named".into(),
8963                SamplingChannelDefinition {
8964                    channel_weight: None,
8965                    around: "lmb(0)".into(),
8966                    subspace_lmb: Vec::new(),
8967                    parent_lmb: vec![0],
8968                    on_cut: Vec::new(),
8969                    singularity_proxy: None,
8970                    radial_profile: None,
8971                },
8972            );
8973        assert_eq!(
8974            setup
8975                .sampling_channel_ids(&setup.graph.name, &mixed_settings)
8976                .unwrap(),
8977            vec![
8978                SamplingChannelId::from(0),
8979                SamplingChannelId::from(1),
8980                SamplingChannelId::from(2),
8981                SamplingChannelId::from(3),
8982            ]
8983        );
8984        assert!(
8985            setup
8986                .sampling_channel_lmb_basis_id(
8987                    SamplingChannelId::from(0),
8988                    &setup.graph.name,
8989                    &mixed_settings,
8990                )
8991                .is_err()
8992        );
8993        assert_eq!(
8994            setup
8995                .sampling_channel_lmb_id(
8996                    SamplingChannelId::from(1),
8997                    &setup.graph.name,
8998                    &mixed_settings,
8999                )
9000                .unwrap(),
9001            Some(LmbIndex::from(0))
9002        );
9003    }
9004
9005    #[test]
9006    fn canonical_lmb_density_partition_matches_inverse_jacobians() {
9007        test_initialise().unwrap();
9008        let mut graph: Graph = dot!(
9009            digraph lmb_prefactor_partition {
9010                edge [num=1 mass=0]
9011                node [num=1]
9012                ext [style=invis]
9013                ext -> A [id=0]
9014                A -> B [id=1]
9015                A -> B [id=2]
9016                B -> ext [id=3]
9017            }
9018        )
9019        .unwrap();
9020        let all_bases = graph.generate_loop_momentum_bases();
9021        assert!(all_bases.len() >= 2);
9022        graph.loop_momentum_basis = all_bases[LmbIndex::from(0)].clone();
9023        let mut setup = LmbMultiChannelingSetup {
9024            master_edge_masses: Default::default(),
9025            sampling_bridge: Default::default(),
9026            sampling_bridge_quad: Default::default(),
9027            sampling_bridge_fixed256: Default::default(),
9028            sampling_bridge_arb: Default::default(),
9029            sampling_source: Default::default(),
9030            sampling_catalogue: Default::default(),
9031            sampling_programs: Default::default(),
9032            lmb_basis_ids: vec![LmbIndex::from(0), LmbIndex::from(1)].into(),
9033            graph: graph.clone(),
9034            all_bases,
9035        };
9036        let loop_moms: LoopMomenta<F<f64>> = graph
9037            .loop_momentum_basis
9038            .loop_edges
9039            .iter()
9040            .enumerate()
9041            .map(|(index, _)| {
9042                let offset = index as f64;
9043                ThreeMomentum::new(F(0.4 + offset), F(-0.3), F(0.2 - offset))
9044            })
9045            .collect();
9046        let external_moms: ExternalFourMomenta<F<f64>> =
9047            (0..graph.loop_momentum_basis.ext_edges.len())
9048                .map(|_| [F(0.0), F(0.2), F(-0.3), F(0.4)].into())
9049                .collect();
9050        let sample = MomentumSample {
9051            sample: BareMomentumSample {
9052                loop_moms,
9053                dual_loop_moms: None,
9054                loop_mom_cache_id: 0,
9055                loop_mom_base_cache_id: 0,
9056                external_moms,
9057                external_mom_cache_id: 0,
9058                external_mom_base_cache_id: 0,
9059                jacobian: F(1.0),
9060                orientation: None,
9061                parameterization_branch: None,
9062            },
9063        };
9064        let mut parameterization_settings = ParameterizationSettings::default();
9065        parameterization_settings
9066            .sampling_channels
9067            .default_channel_selection = vec!["auto:lmb".into()];
9068        let resolved = resolve_sampling_channel_selection(
9069            &graph.name,
9070            &parameterization_settings.sampling_channels,
9071        )
9072        .unwrap();
9073        let context = SamplingChannelCompileContext::new(
9074            graph.name.clone(),
9075            graph
9076                .loop_momentum_basis
9077                .loop_edges
9078                .iter()
9079                .map(|edge| edge.0)
9080                .collect(),
9081            parameterization_settings.clone(),
9082            1.0,
9083            sample.loop_moms().0.len(),
9084        );
9085        let external = sample
9086            .external_moms()
9087            .iter()
9088            .map(|momentum| {
9089                [
9090                    momentum.temporal.value.0,
9091                    momentum.spatial.px.0,
9092                    momentum.spatial.py.0,
9093                    momentum.spatial.pz.0,
9094                ]
9095            })
9096            .collect::<Vec<_>>();
9097        let bridge = setup
9098            .compile_sampling_channel_bridge_with_external(
9099                &setup
9100                    .sampling_channel_catalogue(&resolved, &context.parameterization_settings)
9101                    .unwrap(),
9102                &setup
9103                    .sampling_channel_catalogue(&resolved, &context.parameterization_settings)
9104                    .unwrap()
9105                    .compile_programs(3 * context.n_loop_momenta, &HFunctionSettings::default())
9106                    .unwrap(),
9107                &context,
9108                &external,
9109            )
9110            .unwrap();
9111        let raw = sample
9112            .loop_moms()
9113            .iter()
9114            .flat_map(|momentum| [momentum.px.0, momentum.py.0, momentum.pz.0])
9115            .collect::<Vec<_>>();
9116        // Check map-density weights against independent inverse parameterizations
9117        // in every generated LMB at the same parent momentum point. This retains
9118        // the former prefactor partition's pointwise normalization test without
9119        // keeping a separate LMB weighting implementation in production.
9120        let scores = setup
9121            .all_bases
9122            .iter()
9123            .map(|basis| {
9124                let momenta = basis
9125                    .loop_edges
9126                    .iter()
9127                    .map(|edge| {
9128                        let signature = &graph.loop_momentum_basis.edge_signatures[*edge];
9129                        signature.internal.apply_typed(sample.loop_moms())
9130                            + signature
9131                                .external
9132                                .apply(&sample.external_moms().raw)
9133                                .spatial
9134                    })
9135                    .collect::<Vec<_>>();
9136                crate::utils::global_inv_parameterize(&momenta, F(1.0), &parameterization_settings)
9137                    .1
9138                    .0
9139            })
9140            .collect::<Vec<_>>();
9141        let total = scores.iter().sum::<f64>();
9142        for (channel_id, score) in scores.iter().enumerate().take(bridge.channels().len()) {
9143            let evaluation = bridge
9144                .inverse(SamplingChannelId::from(channel_id), &raw)
9145                .unwrap()
9146                .expect("full-support channel inverse");
9147            assert!((evaluation.partition.weights.iter().sum::<f64>() - 1.0).abs() < 1.0e-14);
9148            let actual = evaluation.partition.weight(channel_id).unwrap();
9149            let expected = score / total;
9150            assert!(
9151                (actual - expected).abs() < 1.0e-14,
9152                "channel {channel_id}: {actual} != {expected}"
9153            );
9154        }
9155        // The master owns OSE masses even if a member/context supplies different ones.
9156        // Refresh twice to catch a stale runtime cache after a model/mass change.
9157        let model = crate::utils::load_generic_model("sm");
9158        let mut context = context;
9159        context.parameterization_settings.sampling_channels.weight =
9160            crate::settings::runtime::SamplingChannelWeight::Ose;
9161        context.edge_masses = graph
9162            .new_edgevec(|_, _, _| spenso::algebra::complex::Complex::new_re(F(1000.0)))
9163            .iter()
9164            .map(|(edge, mass)| (edge.0, *mass))
9165            .collect();
9166        for mass in [2.0, 5.0] {
9167            let edge = *setup.all_bases[LmbIndex::from(0)]
9168                .loop_edges
9169                .first()
9170                .unwrap();
9171            setup.graph.underlying[edge].mass = crate::graph::edge::EdgeMass::Value(
9172                spenso::algebra::complex::Complex::new_re(F(mass)),
9173            );
9174            setup
9175                .warm_up_masses(&RuntimeSettings::default(), &model)
9176                .unwrap();
9177            assert_eq!(setup.master_edge_masses.as_ref().unwrap()[edge].re, F(mass));
9178            assert_ne!(graph.get_real_mass_vector::<f64>(&model)[edge], F(mass));
9179            let catalogue = setup
9180                .sampling_channel_catalogue(&resolved, &context.parameterization_settings)
9181                .unwrap();
9182            let programs = catalogue
9183                .compile_programs(3 * context.n_loop_momenta, &HFunctionSettings::default())
9184                .unwrap();
9185            let bridge = setup
9186                .compile_sampling_channel_bridge_with_external(
9187                    &catalogue, &programs, &context, &external,
9188                )
9189                .unwrap();
9190            let energies = setup.graph.get_energy_cache(
9191                &model,
9192                sample.loop_moms(),
9193                sample.external_moms(),
9194                &setup.graph.loop_momentum_basis,
9195            );
9196            let scores = setup
9197                .all_bases
9198                .iter()
9199                .map(|basis| {
9200                    basis
9201                        .loop_edges
9202                        .iter()
9203                        .map(|edge| energies[*edge].0)
9204                        .product::<f64>()
9205                        .powf(-3.0)
9206                })
9207                .collect::<Vec<_>>();
9208            let partition = bridge.partition(&raw).unwrap();
9209            let total = scores.iter().sum::<f64>();
9210            for (actual, score) in partition.weights.iter().zip(scores) {
9211                assert!((actual - score / total).abs() < 1e-13);
9212            }
9213        }
9214    }
9215
9216    #[test]
9217    fn process_catalogue_bridge_integrates_normalized_gaussian() {
9218        test_initialise().unwrap();
9219        let mut graph: Graph = dot!(
9220            digraph process_sampling_acceptance {
9221                edge [num=1 mass=0]
9222                node [num=1]
9223                ext [style=invis]
9224                ext -> A [id=0]
9225                A -> B [id=1]
9226                A -> B [id=2]
9227                B -> ext [id=3]
9228            }
9229        )
9230        .unwrap();
9231        let generated_bases = graph.generate_loop_momentum_bases();
9232        assert!(!generated_bases.is_empty());
9233        graph.loop_momentum_basis = generated_bases[LmbIndex::from(0)].clone();
9234        let all_bases = vec![graph.loop_momentum_basis.clone()].into();
9235        let parent_lmb = graph
9236            .loop_momentum_basis
9237            .loop_edges
9238            .iter()
9239            .map(|edge| edge.0)
9240            .collect::<Vec<_>>();
9241        let setup = LmbMultiChannelingSetup {
9242            master_edge_masses: Default::default(),
9243            sampling_bridge: Default::default(),
9244            sampling_bridge_quad: Default::default(),
9245            sampling_bridge_fixed256: Default::default(),
9246            sampling_bridge_arb: Default::default(),
9247            sampling_source: Default::default(),
9248            sampling_catalogue: Default::default(),
9249            sampling_programs: Default::default(),
9250            lmb_basis_ids: vec![LmbIndex::from(0)].into(),
9251            graph: graph.clone(),
9252            all_bases,
9253        };
9254
9255        let selection = SamplingChannelSelection::default();
9256        let resolved = resolve_sampling_channel_selection(&graph.name, &selection).unwrap();
9257        let parameterization_settings = ParameterizationSettings {
9258            sampling_channels: selection,
9259            ..Default::default()
9260        };
9261        let context = SamplingChannelCompileContext::new(
9262            graph.name.clone(),
9263            parent_lmb,
9264            parameterization_settings,
9265            100.0,
9266            graph.loop_momentum_basis.loop_edges.len(),
9267        );
9268        let bridge = setup
9269            .compile_sampling_channel_bridge(
9270                &setup
9271                    .sampling_channel_catalogue(&resolved, &context.parameterization_settings)
9272                    .unwrap(),
9273                &setup
9274                    .sampling_channel_catalogue(&resolved, &context.parameterization_settings)
9275                    .unwrap()
9276                    .compile_programs(3 * context.n_loop_momenta, &HFunctionSettings::default())
9277                    .unwrap(),
9278                &context,
9279            )
9280            .unwrap();
9281        assert_eq!(
9282            setup
9283                .sampling_channel_ids(&graph.name, &context.parameterization_settings)
9284                .unwrap()
9285                .len(),
9286            bridge.channels().len(),
9287            "omitted defaults must resolve identically for channel counts and compiled maps"
9288        );
9289        assert_eq!(bridge.channels().len(), 1);
9290
9291        let dimensions = bridge.dimensions();
9292        let width = 1.5;
9293        let normalisation =
9294            (2.0 * std::f64::consts::PI * width * width).powf(-0.5 * dimensions as f64);
9295        let samples = 2048usize;
9296        let mut integral = 0.0;
9297        for sample in 1..=samples {
9298            let coordinates = (0..dimensions)
9299                .map(|axis| {
9300                    let base = [2_u64, 3, 5, 7, 11, 13, 17, 19][axis];
9301                    let mut index = sample;
9302                    let mut fraction = 1.0;
9303                    let mut value = 0.0;
9304                    while index > 0 {
9305                        fraction /= base as f64;
9306                        value += fraction * (index as u64 % base) as f64;
9307                        index /= base as usize;
9308                    }
9309                    value
9310                })
9311                .collect::<Vec<_>>();
9312            let evaluation = bridge
9313                .forward(SamplingChannelId::from(0), &coordinates)
9314                .unwrap();
9315            assert!((evaluation.partition.weights.iter().sum::<f64>() - 1.0).abs() < 1.0e-14);
9316            let radius_squared = evaluation
9317                .raw_coordinates
9318                .iter()
9319                .map(|component| component * component)
9320                .sum::<f64>();
9321            integral += normalisation
9322                * (-0.5 * radius_squared / width.powi(2)).exp()
9323                * evaluation.map.jacobian;
9324        }
9325        integral /= samples as f64;
9326        assert!(
9327            (integral - 1.0).abs() < 5.0e-2,
9328            "process sampling bridge Gaussian integral = {integral}"
9329        );
9330    }
9331}