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