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#[derive(Debug, Clone)]
121pub struct MomentumSpaceEvaluationInput {
122 pub loop_momenta: Vec<ThreeMomentum<F<f64>>>,
124 pub integrator_weight: F<f64>,
126 pub graph_id: Option<usize>,
128 pub group_id: Option<GroupId>,
130 pub orientation: Option<usize>,
132 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 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 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, ¶meterization_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 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 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 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 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 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(¶meterization_settings)?;
1489 channel_ids
1490 .into_iter()
1491 .map(|channel_id| {
1492 Ok(master
1493 .sampling_channel_label(channel_id, ¶meterization_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 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 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 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 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 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 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 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 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 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 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) } 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 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 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#[derive(Clone, Encode, Decode)]
2344#[trait_decode(trait = GammaLoopContext)]
2345pub struct LmbMultiChannelingSetup {
2346 pub lmb_basis_ids: TiVec<SamplingChannelId, LmbIndex>,
2347 pub graph: Graph,
2349 pub all_bases: TiVec<LmbIndex, LoopMomentumBasis>,
2350 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 ¶meterization_settings.sampling_channels,
2703 )?;
2704 self.sampling_channel_catalogue(&resolved, parameterization_settings)
2705 }
2706
2707 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 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 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 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 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 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 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 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 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 ¶meterization.sampling_channels,
3189 )?;
3190 let catalogue = setup.sampling_channel_catalogue(&resolved, ¶meterization)?;
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 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 for graph in self.get_terms_mut() {
3256 graph.sampling_setup_mut().invalidate_sampling();
3257 }
3258 }
3259 result
3260 }
3261
3262 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 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, ¶meterization, 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 fn signal_external_momenta_changed(&mut self) {
3388 self.increment_external_cache_id(1);
3389 }
3390
3391 fn get_current_external_cache_id(&self) -> usize {
3397 self.external_cache_id()
3398 }
3399
3400 fn revert_to_base_external_cache_id(&mut self);
3421
3422 fn is_external_caching_beneficial(&self) -> bool {
3427 self.external_cache_id() < self.loop_cache_id()
3430 }
3431
3432 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 fn get_base_external_cache_id(&self) -> usize;
3474
3475 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 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 }
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 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 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 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 ¶meterization_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 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#[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 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
3910fn 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 }
3946 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 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(¶meterization_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 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 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 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 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 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 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 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 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 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 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 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#[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#[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#[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#[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#[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#[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 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 *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 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 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 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 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 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 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 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 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 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 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 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 (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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert!(
8944 setup
8945 .compile_sampling_channel_bridge(&catalogue, &programs, &context)
8946 .is_err()
8947 );
8948
8949 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 ¶meterization_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 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), ¶meterization_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 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}