Skip to main content

gammalooprs/uv/
profile.rs

1//! UV Profile Analysis Module
2//!
3//! This module provides functionality for analyzing ultraviolet behavior of loop integrands
4//! by evaluating them at different momentum scalings and computing degrees of divergence.
5
6use std::collections::BTreeMap;
7use std::path::Path;
8use std::sync::{Arc, Mutex};
9
10use crate::DependentMomentaConstructor;
11use crate::cff::expression::OrientationData;
12use crate::cff::orientations::GraphOrientation;
13use crate::graph::parse::string_utils::ToOrderedSimple;
14use crate::graph::{Graph, LmbIndex};
15use crate::integrands::evaluation::EvaluationResult;
16use crate::model::Model;
17use crate::momentum::ThreeMomentum;
18use crate::momentum::sample::{ExternalIndex, LoopIndex};
19use crate::processes::{Amplitude, AmplitudeGraph, CrossSection};
20use crate::settings::RuntimeSettings;
21use crate::utils::F;
22use crate::uv::UltravioletGraph;
23use crate::{
24    graph::LoopMomentumBasis,
25    integrands::process::{
26        OrientationProfileMode, ProcessIntegrand, evaluate_profile_momentum_point,
27        orientation_labels_for_graph,
28    },
29};
30use color_eyre::{Result, eyre::Context};
31use colored::Colorize;
32use eyre::eyre;
33use itertools::Itertools;
34use linnet::half_edge::PowersetIterator;
35use linnet::half_edge::involution::{EdgeIndex, SignOrZero};
36use linnet::half_edge::subgraph::subset::SubSet;
37use linnet::half_edge::subgraph::{SuBitGraph, SubSetLike, SubSetOps};
38use linnet::half_edge::tree::SimpleTraversalTree;
39use rand::Rng;
40use rayon::prelude::*;
41use serde::Serialize;
42use symbolica::domains::atom::AtomField;
43use symbolica::numerical_integration::MonteCarloRng;
44use symbolica::poly::series::Series;
45use symbolica::symbol;
46use tabled::{
47    Table, Tabled,
48    builder::Builder,
49    settings::{Modify, Span, Style},
50};
51use tracing::{debug, info_span, instrument};
52use tracing_indicatif::{span_ext::IndicatifSpanExt, style::ProgressStyle};
53use typed_index_collections::TiVec;
54
55type ExternalMomenta = TiVec<ExternalIndex, ThreeMomentum<F<f64>>>;
56type LoopMomentumSample = TiVec<LoopIndex, ThreeMomentum<F<f64>>>;
57const UV_PROFILE_RETRY_MAX_DOD: f64 = -0.9;
58
59pub struct ProfileSettings {
60    pub n_points: usize,
61    pub min_scale_exponent: f64,
62    pub max_scale_exponent: f64,
63    pub seed: u64,
64    pub use_f128: bool,
65    pub analyse_analytically: bool,
66    pub orientation_mode: OrientationProfileMode,
67    pub fixed_uv_ray: Option<UVProfileFixedRay>,
68}
69
70impl Default for ProfileSettings {
71    fn default() -> Self {
72        ProfileSettings {
73            n_points: 15,
74            min_scale_exponent: 3.0,
75            max_scale_exponent: 6.0,
76            seed: 42,
77            analyse_analytically: false,
78            use_f128: false,
79            orientation_mode: OrientationProfileMode::Summed,
80            fixed_uv_ray: None,
81        }
82    }
83}
84
85#[derive(Debug, Clone)]
86pub struct UVProfileFixedRay {
87    directions: Vec<[f64; 3]>,
88    norms: Vec<f64>,
89}
90
91impl UVProfileFixedRay {
92    pub fn from_flat_components(directions: &[f64], norms: &[f64]) -> Result<Self> {
93        if directions.is_empty() {
94            return Err(eyre!(
95                "Fixed UV ray directions cannot be empty when the option is used."
96            ));
97        }
98        if !directions.len().is_multiple_of(3) {
99            return Err(eyre!(
100                "Fixed UV ray directions must contain a multiple of 3 components, got {}.",
101                directions.len()
102            ));
103        }
104        if norms.is_empty() {
105            return Err(eyre!(
106                "Fixed UV ray norms cannot be empty when directions are supplied."
107            ));
108        }
109
110        let directions = directions
111            .as_chunks::<3>()
112            .0
113            .iter()
114            .map(|direction| {
115                let norm = (direction[0] * direction[0]
116                    + direction[1] * direction[1]
117                    + direction[2] * direction[2])
118                    .sqrt();
119                if norm == 0.0 {
120                    return Err(eyre!(
121                        "Fixed UV ray directions cannot contain a zero vector."
122                    ));
123                }
124                Ok([
125                    direction[0] / norm,
126                    direction[1] / norm,
127                    direction[2] / norm,
128                ])
129            })
130            .collect::<Result<Vec<_>>>()?;
131
132        if norms.iter().any(|norm| !norm.is_finite() || *norm <= 0.0) {
133            return Err(eyre!(
134                "Fixed UV ray norms must be finite and strictly positive."
135            ));
136        }
137
138        Ok(Self {
139            directions,
140            norms: norms.to_vec(),
141        })
142    }
143
144    fn sample(&self, loop_count: usize) -> Result<LoopMomentumSample> {
145        if self.directions.len() != 1 && self.directions.len() != loop_count {
146            return Err(eyre!(
147                "Fixed UV ray directions contain {} ray(s), but this LMB has {} loop variable(s). Use either one direction or one direction per loop variable.",
148                self.directions.len(),
149                loop_count
150            ));
151        }
152        if self.norms.len() != 1 && self.norms.len() != loop_count {
153            return Err(eyre!(
154                "Fixed UV ray norms contain {} value(s), but this LMB has {} loop variable(s). Use either one norm or one norm per loop variable.",
155                self.norms.len(),
156                loop_count
157            ));
158        }
159
160        (0..loop_count)
161            .map(|i| {
162                let direction = self.directions[if self.directions.len() == 1 { 0 } else { i }];
163                let norm = self.norms[if self.norms.len() == 1 { 0 } else { i }];
164                Ok(ThreeMomentum {
165                    px: F(norm * direction[0]),
166                    py: F(norm * direction[1]),
167                    pz: F(norm * direction[2]),
168                })
169            })
170            .collect()
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::UVProfileFixedRay;
177
178    #[test]
179    fn fixed_uv_ray_repeats_single_direction_and_norm() {
180        let ray = UVProfileFixedRay::from_flat_components(&[0.0, 0.0, 2.0], &[3.0]).unwrap();
181        let sample = ray.sample(2).unwrap();
182
183        assert_eq!(sample.len(), 2);
184        for momentum in &sample {
185            assert_eq!(momentum.px.0, 0.0);
186            assert_eq!(momentum.py.0, 0.0);
187            assert_eq!(momentum.pz.0, 3.0);
188        }
189    }
190
191    #[test]
192    fn fixed_uv_ray_rejects_invalid_input_shapes() {
193        assert!(UVProfileFixedRay::from_flat_components(&[1.0, 0.0], &[1.0]).is_err());
194        assert!(UVProfileFixedRay::from_flat_components(&[0.0, 0.0, 0.0], &[1.0]).is_err());
195        assert!(UVProfileFixedRay::from_flat_components(&[1.0, 0.0, 0.0], &[0.0]).is_err());
196    }
197}
198
199pub fn logspace(start: f64, stop: f64, num: usize, base: f64) -> Vec<f64> {
200    let log_start = start;
201    let log_stop = stop;
202    let step = (log_stop - log_start) / (num - 1) as f64;
203
204    (0..num)
205        .map(|i| {
206            let exponent = log_start + step * i as f64;
207            base.powf(exponent)
208        })
209        .collect()
210}
211
212fn lmb_seed(base_seed: u64, graph_id: usize, lmb_index: usize) -> u64 {
213    base_seed
214        .wrapping_add((graph_id as u64).wrapping_mul(0x9E3779B97F4A7C15))
215        .wrapping_add(lmb_index as u64)
216}
217
218struct UVProfileRunner<'a> {
219    integrand: &'a Arc<Mutex<ProcessIntegrand>>,
220    scales: &'a [f64],
221    externals: &'a ExternalMomenta,
222    model: &'a Model,
223    settings: &'a RuntimeSettings,
224    profile_settings: &'a ProfileSettings,
225    base_seed: u64,
226}
227
228pub trait UVProfileable {
229    fn profile(
230        &mut self,
231        model: &Model,
232        // settings: &RuntimeSettings,
233        profile_settings: &ProfileSettings,
234    ) -> Result<UVProfile>;
235}
236
237impl UVProfileable for Amplitude {
238    #[instrument(skip_all)]
239    fn profile(
240        &mut self,
241        model: &Model,
242        // settings: &RuntimeSettings,
243        profile_settings: &ProfileSettings,
244    ) -> Result<UVProfile> {
245        let scales = logspace(
246            profile_settings.min_scale_exponent,
247            profile_settings.max_scale_exponent,
248            profile_settings.n_points,
249            10.0,
250        );
251
252        let settings = self
253            .integrand
254            .as_ref()
255            .ok_or(eyre!("Integrand Not built yet"))?
256            .get_settings()
257            .clone();
258        let externals: ExternalMomenta = settings
259            .kinematics
260            .externals
261            .get_dependent_externals(DependentMomentaConstructor::Amplitude(
262                &self.external_signature,
263            ))
264            .unwrap()
265            .into_iter()
266            .map(|a| a.spatial)
267            .collect();
268
269        let base_seed = profile_settings.seed;
270        let integrand = Arc::new(Mutex::new(self.integrand.take().unwrap()));
271
272        let profile_span = info_span!("Profiling graphs", indicatif.pb_show = true);
273        profile_span.pb_set_style(&ProgressStyle::with_template(
274            "{wide_bar} {pos}/{len} {msg}",
275        )?);
276        profile_span.pb_set_length(self.graphs.len() as u64);
277        profile_span.pb_set_message("Profiling graphs");
278        profile_span.pb_set_finish_message("all graphs profiled");
279        let _profile_span_enter = profile_span.enter();
280
281        let runner = UVProfileRunner {
282            integrand: &integrand,
283            scales: &scales,
284            externals: &externals,
285            model,
286            settings: &settings,
287            profile_settings,
288            base_seed,
289        };
290
291        let per_graph = self
292            .graphs
293            .par_iter()
294            .enumerate()
295            .map(|(i, g)| {
296                let res = runner.sample_graph(i, g)?;
297                profile_span.pb_inc(1);
298                Ok(res)
299            })
300            .collect::<Result<Vec<_>>>()?;
301
302        drop(_profile_span_enter);
303        drop(profile_span);
304
305        let integrand = Arc::try_unwrap(integrand)
306            .map_err(|_| color_eyre::eyre::eyre!("integrand still shared"))?
307            .into_inner()
308            .expect("integrand mutex poisoned");
309        self.integrand = Some(integrand);
310
311        Ok(UVProfile {
312            per_graph,
313            scales,
314            allow_vanishing_missing_fits: false,
315        })
316        // results.push((inspect_res, analytic_res));
317    }
318}
319
320impl UVProfileable for CrossSection {
321    #[instrument(skip_all)]
322    fn profile(&mut self, model: &Model, profile_settings: &ProfileSettings) -> Result<UVProfile> {
323        let scales = logspace(
324            profile_settings.min_scale_exponent,
325            profile_settings.max_scale_exponent,
326            profile_settings.n_points,
327            10.0,
328        );
329
330        let integrand = self
331            .integrand
332            .as_ref()
333            .ok_or(eyre!("Integrand Not built yet"))?;
334        let settings = integrand.get_settings().clone();
335        let graph_inputs = match integrand {
336            ProcessIntegrand::CrossSection(cross_section) => cross_section
337                .data
338                .graph_terms
339                .iter()
340                .map(|graph_term| (graph_term.graph.clone(), graph_term.lmbs.clone()))
341                .collect::<Vec<_>>(),
342            ProcessIntegrand::Amplitude(_) => {
343                unreachable!("cross-section UV profiling expects cross-section integrands")
344            }
345        };
346        let externals: ExternalMomenta = settings
347            .kinematics
348            .externals
349            .get_dependent_externals(DependentMomentaConstructor::CrossSection)
350            .unwrap()
351            .into_iter()
352            .map(|a| a.spatial)
353            .collect();
354
355        let base_seed = profile_settings.seed;
356        let integrand = Arc::new(Mutex::new(self.integrand.take().unwrap()));
357
358        let profile_span = info_span!("Profiling graphs", indicatif.pb_show = true);
359        profile_span.pb_set_style(&ProgressStyle::with_template(
360            "{wide_bar} {pos}/{len} {msg}",
361        )?);
362        profile_span.pb_set_length(graph_inputs.len() as u64);
363        profile_span.pb_set_message("Profiling graphs");
364        profile_span.pb_set_finish_message("all graphs profiled");
365        let _profile_span_enter = profile_span.enter();
366
367        let runner = UVProfileRunner {
368            integrand: &integrand,
369            scales: &scales,
370            externals: &externals,
371            model,
372            settings: &settings,
373            profile_settings,
374            base_seed,
375        };
376
377        let per_graph = graph_inputs
378            .par_iter()
379            .enumerate()
380            .map(|(i, (graph, lmbs))| {
381                let res = runner.sample_cross_section_graph(i, graph, lmbs)?;
382                profile_span.pb_inc(1);
383                Ok(res)
384            })
385            .collect::<Result<Vec<_>>>()?;
386
387        drop(_profile_span_enter);
388        drop(profile_span);
389
390        let integrand = Arc::try_unwrap(integrand)
391            .map_err(|_| color_eyre::eyre::eyre!("integrand still shared"))?
392            .into_inner()
393            .expect("integrand mutex poisoned");
394        self.integrand = Some(integrand);
395
396        Ok(UVProfile {
397            per_graph,
398            scales,
399            allow_vanishing_missing_fits: true,
400        })
401    }
402}
403
404pub struct UVProfile {
405    pub per_graph: Vec<UVSamplingResult>,
406    pub scales: Vec<f64>,
407    pub allow_vanishing_missing_fits: bool,
408}
409
410impl UVProfile {
411    pub fn analyse(&self) -> UVProfileAnalysis {
412        let graphs = self
413            .per_graph
414            .iter()
415            .enumerate()
416            .map(|(graph_index, graph)| {
417                let lmbs: Vec<UVProfileLmbAnalysis> = graph
418                    .per_lmb
419                    .iter()
420                    .enumerate()
421                    .map(|(lmb_index, lmb)| {
422                        let lmb_label = lmb_label(&lmb.lmb);
423                        let subsets: Vec<UVProfileSubsetAnalysis> = lmb
424                            .per_subsets
425                            .iter()
426                            .enumerate()
427                            .map(|(subset_index, (subset, subset_result))| {
428                                let mut not_included: SubSet<LoopIndex> =
429                                    SubSet::full(subset.size());
430                                not_included.subtract_with(subset);
431                                let free: Vec<EdgeIndex> = subset
432                                    .included_iter()
433                                    .map(|loop_index| lmb.lmb.loop_edges[loop_index])
434                                    .collect();
435
436                                let fixed: Vec<EdgeIndex> = not_included
437                                    .included_iter()
438                                    .map(|loop_index| lmb.lmb.loop_edges[loop_index])
439                                    .collect();
440                                let analysis = subset_result.analyse(&self.scales);
441                                let per_orientation_inspect_entries =
442                                    analysis.per_orientation_inspect_entries();
443                                let analytic_entries =
444                                    analysis.analytic.as_ref().and_then(|analytic| {
445                                        let entries = analytic
446                                            .per_orientation
447                                            .iter()
448                                            .map(|(orientation, orientation_analysis)| {
449                                                let (orientation_edges, orientation_signs) =
450                                                    orientation_signs(orientation);
451                                                UVProfileAnalyticEntry {
452                                                    graph_index,
453                                                    lmb_index,
454                                                    subset_index,
455                                                    fixed: fixed.clone(),
456                                                    free: free.clone(),
457                                                    orientation_edges,
458                                                    orientation_signs,
459                                                    is_constant: orientation_analysis.is_constant,
460                                                    leading_coef: orientation_analysis
461                                                        .leading_coef
462                                                        .to_string(),
463                                                }
464                                            })
465                                            .collect::<Vec<_>>();
466                                        if entries.is_empty() {
467                                            None
468                                        } else {
469                                            Some(entries)
470                                        }
471                                    });
472                                UVProfileSubsetAnalysis {
473                                    subset_index,
474                                    fixed,
475                                    free,
476                                    initial_dod: subset_result.initial_dod,
477                                    analysis,
478                                    per_orientation_inspect_entries,
479                                    analytic_entries,
480                                }
481                            })
482                            .collect();
483
484                        UVProfileLmbAnalysis {
485                            lmb_index,
486                            lmb_label,
487                            subsets,
488                        }
489                    })
490                    .collect();
491
492                UVProfileGraphAnalysis { graph_index, lmbs }
493            })
494            .collect();
495
496        UVProfileAnalysis {
497            scales: self.scales.clone(),
498            graphs,
499            allow_vanishing_missing_fits: self.allow_vanishing_missing_fits,
500        }
501    }
502
503    pub fn write_profile_data<P: AsRef<Path>>(
504        &self,
505        _settings: &ProfileSettings,
506        out_dir: P,
507    ) -> Result<()> {
508        self.analyse().write_profile_data(out_dir)
509    }
510
511    pub fn write_typst_bundle<P: AsRef<Path>>(
512        &self,
513        settings: &ProfileSettings,
514        out_dir: P,
515    ) -> Result<()> {
516        self.write_profile_data(settings, out_dir)
517    }
518
519    pub fn pass_fail(&self, max_dod: f64, _settings: &ProfileSettings) -> UVProfilePassFail {
520        self.analyse().pass_fail(max_dod)
521    }
522}
523
524#[derive(Debug, Clone, Serialize)]
525pub struct UVProfileAnalysis {
526    pub scales: Vec<f64>,
527    pub graphs: Vec<UVProfileGraphAnalysis>,
528    #[serde(skip_serializing)]
529    pub allow_vanishing_missing_fits: bool,
530}
531
532#[derive(Debug, Clone, Serialize)]
533pub struct UVProfileGraphAnalysis {
534    pub graph_index: usize,
535    pub lmbs: Vec<UVProfileLmbAnalysis>,
536}
537
538#[derive(Debug, Clone, Serialize)]
539pub struct UVProfileLmbAnalysis {
540    pub lmb_index: usize,
541    pub lmb_label: String,
542    pub subsets: Vec<UVProfileSubsetAnalysis>,
543}
544
545#[derive(Debug, Clone, Serialize)]
546pub struct UVProfileSubsetAnalysis {
547    pub subset_index: usize,
548    pub fixed: Vec<EdgeIndex>,
549    pub free: Vec<EdgeIndex>,
550    pub initial_dod: i32,
551    // pub subset_label: String,
552    pub analysis: Analysis,
553    pub per_orientation_inspect_entries: Option<Vec<UVProfileOrientationInspectEntry>>,
554    pub analytic_entries: Option<Vec<UVProfileAnalyticEntry>>,
555}
556
557impl UVProfileSubsetAnalysis {
558    pub fn estimated_dod(&self) -> Option<i64> {
559        self.analysis
560            .inspect_level
561            .as_ref()
562            .map(|analysis| analysis.estimated_dod)
563    }
564
565    pub fn bare_dod_matches_estimate(&self) -> bool {
566        self.estimated_dod() == Some(i64::from(self.initial_dod))
567    }
568}
569
570// #[derive(Debug, Clone, Serialize)]
571// pub struct UVProfilePoint {
572//     norm:f64,
573// }
574
575#[derive(Debug, Clone, Serialize)]
576pub struct UVProfileSummary {
577    pub subset_count: usize,
578    pub slope_min: Option<f64>,
579    pub slope_max: Option<f64>,
580    pub dod_min: Option<i64>,
581    pub dod_max: Option<i64>,
582    pub stable_min: Option<f64>,
583    pub stable_max: Option<f64>,
584}
585
586#[derive(Debug, Clone, Serialize)]
587pub struct UVProfilePassFail {
588    pub max_dod: f64,
589    pub total: usize,
590    pub failed: usize,
591    pub failures: Vec<UVProfileFailure>,
592}
593
594#[derive(Debug, Clone, Serialize)]
595pub struct UVProfileFailure {
596    pub graph_index: usize,
597    pub lmb_index: usize,
598    pub fixed: Vec<EdgeIndex>,
599    pub free: Vec<EdgeIndex>,
600    pub orientation_label: Option<String>,
601    pub reason: String,
602}
603
604#[derive(Debug, Clone, Serialize)]
605pub struct UVProfileAnalyticEntry {
606    pub graph_index: usize,
607    pub lmb_index: usize,
608    pub subset_index: usize,
609    pub fixed: Vec<EdgeIndex>,
610    pub free: Vec<EdgeIndex>,
611    pub orientation_edges: Vec<String>,
612    pub orientation_signs: Vec<String>,
613    pub is_constant: bool,
614    pub leading_coef: String,
615}
616
617#[derive(Debug, Clone, Serialize)]
618pub struct UVProfileOrientationInspectEntry {
619    pub orientation_label: String,
620    pub analysis: Option<InspectAnalysis>,
621}
622
623#[derive(Tabled)]
624struct UVProfileSubsetRow {
625    #[tabled(rename = "fixed")]
626    fixed: String,
627    #[tabled(rename = "→ ∞")]
628    free: String,
629    #[tabled(rename = "slope")]
630    slope: String,
631    #[tabled(rename = "r2")]
632    r_squared: String,
633    #[tabled(rename = "DOD")]
634    estimated_dod: String,
635    #[tabled(rename = "bare DOD")]
636    initial_dod: String,
637    #[tabled(rename = "inspect")]
638    inspect: String,
639}
640
641#[derive(Tabled)]
642struct UVProfileOrientationSubsetRow {
643    #[tabled(rename = "fixed")]
644    fixed: String,
645    #[tabled(rename = "→ ∞")]
646    free: String,
647    #[tabled(rename = "orientation")]
648    orientation_label: String,
649    #[tabled(rename = "slope")]
650    slope: String,
651    #[tabled(rename = "r2")]
652    r_squared: String,
653    #[tabled(rename = "DOD")]
654    estimated_dod: String,
655    #[tabled(rename = "bare DOD")]
656    initial_dod: String,
657    #[tabled(rename = "inspect")]
658    inspect: String,
659}
660
661#[derive(Debug, Clone, Serialize)]
662struct FitResult {
663    slope: f64,
664    points: Vec<f64>,
665    points_detail: Vec<EvaluationResult>,
666    intercept: f64,
667    r_squared: f64,
668}
669
670impl UVProfileAnalysis {
671    pub fn tables_per_graph(&self, max_dod: f64) -> Vec<Table> {
672        self.graphs
673            .iter()
674            .map(|graph| {
675                let rows = graph
676                    .lmbs
677                    .iter()
678                    .flat_map(|lmb| {
679                        lmb.subsets.iter().map(|subset| {
680                            let (slope, r_squared, estimated_dod) =
681                                match &subset.analysis.inspect_level {
682                                    Some(analysis) => {
683                                        let r2_text = format!("{:.3}", analysis.result.r_squared);
684                                        let r2_text = if analysis.result.r_squared >= 0.99 {
685                                            r2_text.green()
686                                        } else {
687                                            r2_text.red()
688                                        }
689                                        .to_string();
690
691                                        let dod = analysis.estimated_dod;
692                                        let dod_text = dod.to_string();
693                                        let dod_text = if (dod as f64) <= max_dod {
694                                            dod_text.green()
695                                        } else {
696                                            dod_text.red()
697                                        }
698                                        .to_string();
699
700                                        (format!("{:.6}", analysis.result.slope), r2_text, dod_text)
701                                    }
702                                    None => ("-".to_string(), "-".to_string(), "-".to_string()),
703                                };
704
705                            UVProfileSubsetRow {
706                                fixed: format!(
707                                    "{{{}}}",
708                                    subset.fixed.iter().map(ToString::to_string).join(",")
709                                ),
710                                free: format!(
711                                    "{{{}}}",
712                                    subset.free.iter().map(ToString::to_string).join(",")
713                                ),
714                                slope,
715                                r_squared,
716                                estimated_dod,
717                                initial_dod: if subset.initial_dod >= 0 {
718                                    subset.initial_dod.to_string().red().to_string()
719                                } else {
720                                    subset.initial_dod.to_string().green().to_string()
721                                },
722                                inspect: inspect_retry_label(
723                                    subset.analysis.inspect_level.as_ref(),
724                                ),
725                            }
726                        })
727                    })
728                    .collect::<Vec<_>>();
729
730                let mut table = Table::new(rows);
731                table.with(Style::rounded());
732                table
733            })
734            .collect()
735    }
736
737    pub fn analytic_tables_per_graph(&self) -> Vec<Option<Table>> {
738        self.graphs
739            .iter()
740            .map(|graph| {
741                let mut groups: Vec<Vec<&UVProfileAnalyticEntry>> = Vec::new();
742                let mut orientation_headers: Option<Vec<String>> = None;
743
744                for lmb in &graph.lmbs {
745                    for subset in &lmb.subsets {
746                        if let Some(entries) = &subset.analytic_entries {
747                            if orientation_headers.is_none() {
748                                orientation_headers =
749                                    entries.first().map(|entry| entry.orientation_edges.clone());
750                            }
751                            groups.push(entries.iter().collect());
752                        }
753                    }
754                }
755
756                if groups.is_empty() {
757                    return None;
758                }
759
760                let mut builder = Builder::new();
761                let mut header = vec!["fixed".to_string(), "→ ∞".to_string()];
762                header.extend(orientation_headers.unwrap_or_default());
763                header.extend(["const".to_string(), "leading coef".to_string()]);
764                builder.push_record(header);
765
766                let mut span_ops: Vec<(usize, usize, usize)> = Vec::new();
767                let mut row_index = 1;
768
769                for group in groups {
770                    let span_len = group.len();
771                    let start_row = row_index;
772
773                    for (i, entry) in group.into_iter().enumerate() {
774                        let fixed = if i == 0 {
775                            format!(
776                                "{{{}}}",
777                                entry.fixed.iter().map(ToString::to_string).join(",")
778                            )
779                        } else {
780                            String::new()
781                        };
782                        let free = if i == 0 {
783                            format!(
784                                "{{{}}}",
785                                entry.free.iter().map(ToString::to_string).join(",")
786                            )
787                        } else {
788                            String::new()
789                        };
790                        let mut row = vec![fixed, free];
791                        row.extend(entry.orientation_signs.iter().cloned());
792                        row.extend([entry.is_constant.to_string(), "".to_string()]);
793                        builder.push_record(row);
794                        row_index += 1;
795                    }
796
797                    if span_len > 1 {
798                        span_ops.push((start_row, 0, span_len));
799                        span_ops.push((start_row, 1, span_len));
800                    }
801                }
802
803                let mut table = builder.build();
804                for (row, col, span_len) in span_ops {
805                    table.with(Modify::new((row, col)).with(Span::row(span_len as isize)));
806                }
807                table.with(Style::rounded());
808                Some(table)
809            })
810            .collect()
811    }
812
813    pub fn per_orientation_tables_per_graph(&self, max_dod: f64) -> Vec<Option<Table>> {
814        self.graphs
815            .iter()
816            .map(|graph| {
817                let rows = graph
818                    .lmbs
819                    .iter()
820                    .flat_map(|lmb| {
821                        lmb.subsets.iter().flat_map(|subset| {
822                            subset
823                                .per_orientation_inspect_entries
824                                .iter()
825                                .flatten()
826                                .map(|entry| {
827                                    let (slope, r_squared, estimated_dod) = match &entry.analysis {
828                                        Some(analysis) => {
829                                            let r2_text =
830                                                format!("{:.3}", analysis.result.r_squared);
831                                            let r2_text = if analysis.result.r_squared >= 0.99 {
832                                                r2_text.green()
833                                            } else {
834                                                r2_text.red()
835                                            }
836                                            .to_string();
837
838                                            let dod = analysis.estimated_dod;
839                                            let dod_text = dod.to_string();
840                                            let dod_text = if (dod as f64) <= max_dod {
841                                                dod_text.green()
842                                            } else {
843                                                dod_text.red()
844                                            }
845                                            .to_string();
846
847                                            (
848                                                format!("{:.6}", analysis.result.slope),
849                                                r2_text,
850                                                dod_text,
851                                            )
852                                        }
853                                        None => ("-".to_string(), "-".to_string(), "-".to_string()),
854                                    };
855
856                                    UVProfileOrientationSubsetRow {
857                                        fixed: format!(
858                                            "{{{}}}",
859                                            subset.fixed.iter().map(ToString::to_string).join(",")
860                                        ),
861                                        free: format!(
862                                            "{{{}}}",
863                                            subset.free.iter().map(ToString::to_string).join(",")
864                                        ),
865                                        orientation_label: entry.orientation_label.clone(),
866                                        slope,
867                                        r_squared,
868                                        estimated_dod,
869                                        initial_dod: if subset.initial_dod >= 0 {
870                                            subset.initial_dod.to_string().red().to_string()
871                                        } else {
872                                            subset.initial_dod.to_string().green().to_string()
873                                        },
874                                        inspect: inspect_retry_label(entry.analysis.as_ref()),
875                                    }
876                                })
877                                .collect::<Vec<_>>()
878                        })
879                    })
880                    .collect::<Vec<_>>();
881
882                if rows.is_empty() {
883                    None
884                } else {
885                    let mut table = Table::new(rows);
886                    table.with(Style::rounded());
887                    Some(table)
888                }
889            })
890            .collect()
891    }
892
893    pub fn write_profile_data<P: AsRef<Path>>(&self, out_dir: P) -> Result<()> {
894        let out_dir = out_dir.as_ref();
895        std::fs::create_dir_all(out_dir).context("failed to create UV profile output directory")?;
896
897        let json_path = out_dir.join("uv_profile.json");
898        let json = serde_json::to_string_pretty(self)
899            .context("failed to serialize UV profile analysis to JSON")?;
900        std::fs::write(&json_path, json).context("failed to write UV profile JSON output")?;
901
902        Ok(())
903    }
904
905    pub fn pass_fail(&self, max_dod: f64) -> UVProfilePassFail {
906        let mut failures = Vec::new();
907        let mut total = 0;
908
909        for graph in &self.graphs {
910            for lmb in &graph.lmbs {
911                for subset in &lmb.subsets {
912                    let per_orientation_reason_count = subset
913                        .analysis
914                        .per_orientation_inspect
915                        .as_ref()
916                        .map(|entries| {
917                            entries
918                                .iter()
919                                .filter(|entry| {
920                                    inspect_failure_reason(
921                                        entry.analysis.as_ref(),
922                                        entry.inspect_fit_status,
923                                        max_dod,
924                                        self.allow_vanishing_missing_fits,
925                                    )
926                                    .is_some()
927                                })
928                                .count()
929                        })
930                        .unwrap_or(0);
931                    let all_orientations_pass = subset.analysis.per_orientation_inspect.is_some()
932                        && per_orientation_reason_count == 0;
933
934                    total += 1;
935                    let reason = inspect_failure_reason(
936                        subset.analysis.inspect_level.as_ref(),
937                        subset.analysis.inspect_fit_status,
938                        max_dod,
939                        self.allow_vanishing_missing_fits,
940                    );
941
942                    if let Some(reason) = reason.filter(|_| !all_orientations_pass) {
943                        failures.push(UVProfileFailure {
944                            graph_index: graph.graph_index,
945                            lmb_index: lmb.lmb_index,
946                            fixed: subset.fixed.clone(),
947                            free: subset.free.clone(),
948                            orientation_label: None,
949                            reason: reason.to_string(),
950                        });
951                    }
952
953                    for entry in subset.analysis.per_orientation_inspect.iter().flatten() {
954                        total += 1;
955                        let reason = inspect_failure_reason(
956                            entry.analysis.as_ref(),
957                            entry.inspect_fit_status,
958                            max_dod,
959                            self.allow_vanishing_missing_fits,
960                        );
961
962                        if let Some(reason) = reason {
963                            failures.push(UVProfileFailure {
964                                graph_index: graph.graph_index,
965                                lmb_index: lmb.lmb_index,
966                                fixed: subset.fixed.clone(),
967                                free: subset.free.clone(),
968                                orientation_label: Some(entry.orientation_label.clone()),
969                                reason: reason.to_string(),
970                            });
971                        }
972                    }
973                }
974            }
975        }
976
977        UVProfilePassFail {
978            max_dod,
979            total,
980            failed: failures.len(),
981            failures,
982        }
983    }
984}
985
986fn inspect_failure_reason(
987    analysis: Option<&InspectAnalysis>,
988    fit_status: InspectFitStatus,
989    max_dod: f64,
990    allow_vanishing_missing_fits: bool,
991) -> Option<&'static str> {
992    match analysis {
993        None if allow_vanishing_missing_fits && fit_status.missing_fit_is_vanishing() => None,
994        None => Some("missing_fit"),
995        Some(analysis) if analysis.result.slope > max_dod || analysis.result.slope.is_nan() => {
996            Some("dod_exceeds_threshold")
997        }
998        _ => None,
999    }
1000}
1001
1002fn lmb_label(lmb: &LoopMomentumBasis) -> String {
1003    let edges: Vec<String> = lmb.loop_edges.iter().map(|edge| edge.to_string()).collect();
1004    format!("loop_edges=[{}]", edges.join(","))
1005}
1006
1007fn orientation_signs(orientation: &OrientationData) -> (Vec<String>, Vec<String>) {
1008    let edges = orientation
1009        .orientation
1010        .iter()
1011        .map(|(edge, _)| edge.to_string())
1012        .collect::<Vec<_>>();
1013    let signs = orientation
1014        .orientation
1015        .iter()
1016        .map(|(_, sign)| SignOrZero::from(*sign).to_string())
1017        .collect::<Vec<_>>();
1018    (edges, signs)
1019}
1020
1021pub struct UVSamplingResult {
1022    pub per_lmb: Vec<LMBResult>,
1023}
1024
1025impl<'a> UVProfileRunner<'a> {
1026    fn sample_graph(&self, graph_id: usize, g: &AmplitudeGraph) -> Result<UVSamplingResult> {
1027        let lmbs = g.derived_data.lmbs.as_ref().unwrap();
1028        let integrand_expr = &g.derived_data.all_mighty_integrand;
1029        let analytic_orientations: Vec<_> = g
1030            .derived_data
1031            .cff_expression
1032            .as_ref()
1033            .unwrap()
1034            .orientations
1035            .iter()
1036            .collect();
1037        let orientation_labels = if self
1038            .profile_settings
1039            .orientation_mode
1040            .profiles_per_orientation()
1041        {
1042            let integrand = self.integrand.lock().expect("integrand mutex poisoned");
1043            match &*integrand {
1044                ProcessIntegrand::Amplitude(amplitude) => {
1045                    Some(orientation_labels_for_graph(amplitude, graph_id)?)
1046                }
1047                ProcessIntegrand::CrossSection(_) => {
1048                    unreachable!("UV profiling expects amplitudes")
1049                }
1050            }
1051        } else {
1052            None
1053        };
1054        let lmb_refs: Vec<_> = lmbs.iter().enumerate().collect();
1055
1056        let lmb_span = info_span!(
1057            "Profiling loop momentum bases",
1058            indicatif.pb_show = true,
1059            graph_id = graph_id
1060        );
1061        lmb_span.pb_set_style(
1062            &ProgressStyle::with_template("{wide_bar} {pos}/{len} {msg}")
1063                .expect("invalid progress bar template"),
1064        );
1065        lmb_span.pb_set_length(lmb_refs.len() as u64);
1066        lmb_span.pb_set_message("Profiling loop momentum bases");
1067        lmb_span.pb_set_finish_message("all loop momentum bases profiled");
1068        let _lmb_span_enter = lmb_span.enter();
1069
1070        let per_lmb = lmb_refs
1071            .par_iter()
1072            .map(|(lmb_index, lmb)| {
1073                let mut res = self.sample_lmb(
1074                    graph_id,
1075                    &g.graph,
1076                    *lmb_index,
1077                    lmb,
1078                    orientation_labels.as_deref(),
1079                )?;
1080
1081                if self.profile_settings.analyse_analytically {
1082                    let orientation_limits: Vec<(
1083                        SubSet<LoopIndex>,
1084                        OrientationData,
1085                        Series<AtomField>,
1086                    )> = analytic_orientations
1087                        .par_iter()
1088                        .map(|o| {
1089                            let oatom = o.data.orientation.select(integrand_expr);
1090                            g.graph
1091                                .all_limits(&g.graph.full_filter(), &oatom, symbol!("lambd"), lmb)
1092                                .into_iter()
1093                                .map(|(l, v)| (l, o.data.clone(), v))
1094                                .collect::<Vec<_>>()
1095                        })
1096                        .reduce(Vec::new, |mut acc, mut v| {
1097                            acc.append(&mut v);
1098                            acc
1099                        });
1100
1101                    for (l, odata, v) in orientation_limits {
1102                        let subset = res
1103                            .per_subsets
1104                            .get_mut(&l)
1105                            .expect("subset missing for orientation limits");
1106                        let analytic = subset.analytic.get_or_insert_with(|| AnalyticResult {
1107                            per_orientations: BTreeMap::new(),
1108                        });
1109                        analytic.per_orientations.insert(odata, v);
1110                    }
1111                }
1112                lmb_span.pb_inc(1);
1113                Ok(res)
1114            })
1115            .collect::<Result<Vec<_>>>()?;
1116
1117        drop(_lmb_span_enter);
1118        drop(lmb_span);
1119
1120        Ok(UVSamplingResult { per_lmb })
1121    }
1122
1123    fn sample_cross_section_graph(
1124        &self,
1125        graph_id: usize,
1126        graph: &Graph,
1127        lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
1128    ) -> Result<UVSamplingResult> {
1129        if self.profile_settings.analyse_analytically {
1130            return Err(eyre!(
1131                "Analytic UV profiling is not implemented for cross sections"
1132            ));
1133        }
1134
1135        let orientation_labels = if self
1136            .profile_settings
1137            .orientation_mode
1138            .profiles_per_orientation()
1139        {
1140            let integrand = self.integrand.lock().expect("integrand mutex poisoned");
1141            match &*integrand {
1142                ProcessIntegrand::CrossSection(cross_section) => {
1143                    Some(orientation_labels_for_graph(cross_section, graph_id)?)
1144                }
1145                ProcessIntegrand::Amplitude(_) => {
1146                    unreachable!("cross-section UV profiling expects cross sections")
1147                }
1148            }
1149        } else {
1150            None
1151        };
1152        let lmb_refs: Vec<_> = lmbs.iter().enumerate().collect();
1153
1154        let lmb_span = info_span!(
1155            "Profiling loop momentum bases",
1156            indicatif.pb_show = true,
1157            graph_id = graph_id
1158        );
1159        lmb_span.pb_set_style(
1160            &ProgressStyle::with_template("{wide_bar} {pos}/{len} {msg}")
1161                .expect("invalid progress bar template"),
1162        );
1163        lmb_span.pb_set_length(lmb_refs.len() as u64);
1164        lmb_span.pb_set_message("Profiling loop momentum bases");
1165        lmb_span.pb_set_finish_message("all loop momentum bases profiled");
1166        let _lmb_span_enter = lmb_span.enter();
1167
1168        let per_lmb = lmb_refs
1169            .par_iter()
1170            .map(|(lmb_index, lmb)| {
1171                let res = self.sample_lmb(
1172                    graph_id,
1173                    graph,
1174                    *lmb_index,
1175                    lmb,
1176                    orientation_labels.as_deref(),
1177                )?;
1178                lmb_span.pb_inc(1);
1179                Ok(res)
1180            })
1181            .collect::<Result<Vec<_>>>()?;
1182
1183        drop(_lmb_span_enter);
1184        drop(lmb_span);
1185
1186        Ok(UVSamplingResult { per_lmb })
1187    }
1188}
1189
1190pub struct LMBResult {
1191    pub(crate) lmb: LoopMomentumBasis,
1192    pub(crate) per_subsets: BTreeMap<SubSet<LoopIndex>, SubSetResult>,
1193}
1194
1195impl<'a> UVProfileRunner<'a> {
1196    fn sample_lmb(
1197        &self,
1198        graph_id: usize,
1199        graph: &Graph,
1200        lmb_index: usize,
1201        lmb: &LoopMomentumBasis,
1202        orientation_labels: Option<&[String]>,
1203    ) -> Result<LMBResult> {
1204        let sample: LoopMomentumSample =
1205            if let Some(fixed_uv_ray) = &self.profile_settings.fixed_uv_ray {
1206                fixed_uv_ray.sample(lmb.loop_edges.len())?
1207            } else {
1208                let mut rng = MonteCarloRng::new(lmb_seed(self.base_seed, graph_id, lmb_index), 0);
1209                lmb.loop_edges
1210                    .iter()
1211                    .map(|_| ThreeMomentum {
1212                        px: F(rng.random_range(
1213                            -self.settings.kinematics.e_cm..self.settings.kinematics.e_cm,
1214                        )),
1215                        py: F(rng.random_range(
1216                            -self.settings.kinematics.e_cm..self.settings.kinematics.e_cm,
1217                        )),
1218                        pz: F(rng.random_range(
1219                            -self.settings.kinematics.e_cm..self.settings.kinematics.e_cm,
1220                        )),
1221                    })
1222                    .collect()
1223            };
1224
1225        let mut loops = PowersetIterator::<LoopIndex>::new(lmb.loop_edges.len() as u8);
1226        loops.next();
1227
1228        let subsets: Vec<_> = loops.collect();
1229        let subset_span = info_span!(
1230            "Profiling subsets",
1231            indicatif.pb_show = true,
1232            graph_id = graph_id,
1233            lmb_index = lmb_index
1234        );
1235        subset_span.pb_set_style(
1236            &ProgressStyle::with_template("{wide_bar} {pos}/{len} {msg}")
1237                .expect("invalid progress bar template"),
1238        );
1239        subset_span.pb_set_length(subsets.len() as u64);
1240        subset_span.pb_set_message("Profiling subsets");
1241        let _subset_span_enter = subset_span.enter();
1242
1243        let per_subsets_vec: Vec<(SubSet<LoopIndex>, SubSetResult)> = subsets
1244            .into_par_iter()
1245            .map_init(
1246                || {
1247                    self.integrand
1248                        .lock()
1249                        .expect("integrand mutex poisoned")
1250                        .clone()
1251                },
1252                |integrand, ls| {
1253                    let res = self.sample_subset(
1254                        integrand,
1255                        SubsetSampleInput {
1256                            graph_id,
1257                            graph,
1258                            subset: &ls,
1259                            lmb,
1260                            sample: &sample,
1261                            orientation_labels,
1262                        },
1263                    )?;
1264                    subset_span.pb_inc(1);
1265                    Ok((ls, res))
1266                },
1267            )
1268            .collect::<Result<Vec<_>>>()?;
1269        let per_subsets = per_subsets_vec.into_iter().collect();
1270
1271        drop(_subset_span_enter);
1272        drop(subset_span);
1273
1274        Ok(LMBResult {
1275            lmb: lmb.clone(),
1276            per_subsets,
1277        })
1278    }
1279}
1280
1281pub struct SubSetResult {
1282    pub(crate) initial_dod: i32,
1283    pub(crate) inspect: InspectSamples,
1284    pub(crate) analytic: Option<AnalyticResult>,
1285}
1286
1287#[derive(Debug, Clone, Default)]
1288pub(crate) struct InspectSamples {
1289    pub(crate) summed: Vec<InspectResult>,
1290    pub(crate) summed_used_arb_prec_retry: bool,
1291    pub(crate) per_orientation: Vec<OrientationInspectSamples>,
1292}
1293
1294#[derive(Debug, Clone)]
1295pub(crate) struct OrientationInspectSamples {
1296    pub(crate) label: String,
1297    pub(crate) inspect: Vec<InspectResult>,
1298    pub(crate) used_arb_prec_retry: bool,
1299}
1300
1301#[derive(Debug, Clone)]
1302struct InspectRun {
1303    inspect: Vec<InspectResult>,
1304    used_arb_prec_retry: bool,
1305}
1306
1307#[derive(Clone, Copy)]
1308struct SubsetOrientationInput<'a> {
1309    graph_id: usize,
1310    subset: &'a SubSet<LoopIndex>,
1311    lmb: &'a LoopMomentumBasis,
1312    sample: &'a LoopMomentumSample,
1313    orientation: Option<usize>,
1314}
1315
1316struct SubsetSampleInput<'a> {
1317    graph_id: usize,
1318    graph: &'a Graph,
1319    subset: &'a SubSet<LoopIndex>,
1320    lmb: &'a LoopMomentumBasis,
1321    sample: &'a LoopMomentumSample,
1322    orientation_labels: Option<&'a [String]>,
1323}
1324
1325impl<'a> UVProfileRunner<'a> {
1326    fn sample_subset(
1327        &self,
1328        integrand: &mut ProcessIntegrand,
1329        input: SubsetSampleInput<'_>,
1330    ) -> Result<SubSetResult> {
1331        let mut subgraph: SuBitGraph = input.graph.empty_subgraph();
1332        for l in input.subset.included_iter() {
1333            let eid = input.lmb.loop_edges[l];
1334            let cut = input.graph[&eid].1.any_hedge();
1335            let root_node = input.graph.node_id(cut);
1336
1337            let tree = SimpleTraversalTree::depth_first_traverse(
1338                input.graph,
1339                &input.lmb.tree,
1340                &root_node,
1341                None,
1342            )
1343            .unwrap();
1344            subgraph.union_with(
1345                &tree
1346                    .get_cycle(cut, input.graph.underlying.as_ref())
1347                    .unwrap()
1348                    .filter,
1349            );
1350        }
1351
1352        let initial_dod = input.graph.compute_dod(&subgraph);
1353        let inspect = if let Some(orientation_labels) = input.orientation_labels {
1354            let per_orientation = orientation_labels
1355                .iter()
1356                .enumerate()
1357                .map(|(orientation_id, label)| {
1358                    let inspect = self.sample_subset_orientation(
1359                        integrand,
1360                        SubsetOrientationInput {
1361                            graph_id: input.graph_id,
1362                            subset: input.subset,
1363                            lmb: input.lmb,
1364                            sample: input.sample,
1365                            orientation: Some(orientation_id),
1366                        },
1367                    )?;
1368                    Ok(OrientationInspectSamples {
1369                        label: label.clone(),
1370                        inspect: inspect.inspect,
1371                        used_arb_prec_retry: inspect.used_arb_prec_retry,
1372                    })
1373                })
1374                .collect::<Result<Vec<_>>>()?;
1375            let summed = sum_orientation_inspect_samples(&per_orientation);
1376            InspectSamples {
1377                summed,
1378                summed_used_arb_prec_retry: per_orientation
1379                    .iter()
1380                    .any(|orientation| orientation.used_arb_prec_retry),
1381                per_orientation,
1382            }
1383        } else {
1384            let inspect = self.sample_subset_orientation(
1385                integrand,
1386                SubsetOrientationInput {
1387                    graph_id: input.graph_id,
1388                    subset: input.subset,
1389                    lmb: input.lmb,
1390                    sample: input.sample,
1391                    orientation: None,
1392                },
1393            )?;
1394            InspectSamples {
1395                summed: inspect.inspect,
1396                summed_used_arb_prec_retry: inspect.used_arb_prec_retry,
1397                per_orientation: Vec::new(),
1398            }
1399        };
1400        let analytic = None;
1401
1402        Ok(SubSetResult {
1403            inspect,
1404            initial_dod,
1405            analytic,
1406        })
1407    }
1408
1409    fn sample_subset_orientation(
1410        &self,
1411        integrand: &mut ProcessIntegrand,
1412        input: SubsetOrientationInput<'_>,
1413    ) -> Result<InspectRun> {
1414        let inspect = self.sample_subset_orientation_with_precision(
1415            integrand,
1416            input,
1417            self.profile_settings.use_f128,
1418        )?;
1419        if !self.profile_settings.use_f128
1420            && inspect_results_need_arbprec_retry(&inspect, self.scales)
1421        {
1422            return Ok(InspectRun {
1423                inspect: self.sample_subset_orientation_with_precision(integrand, input, true)?,
1424                used_arb_prec_retry: true,
1425            });
1426        }
1427        Ok(InspectRun {
1428            inspect,
1429            used_arb_prec_retry: false,
1430        })
1431    }
1432
1433    fn sample_subset_orientation_with_precision(
1434        &self,
1435        integrand: &mut ProcessIntegrand,
1436        input: SubsetOrientationInput<'_>,
1437        use_arb_prec: bool,
1438    ) -> Result<Vec<InspectResult>> {
1439        let n_included = input.subset.n_included() as i32;
1440        self.scales
1441            .iter()
1442            .map(|s| {
1443                let prefactor = s.powi(3 * n_included);
1444                let mut scaled_sample = input.sample.clone();
1445                for l in input.subset.included_iter() {
1446                    scaled_sample[l] = scaled_sample[l].map_ref(&|a| a * F(*s));
1447                }
1448                let loop_momenta = input
1449                    .lmb
1450                    .loop_edges
1451                    .iter()
1452                    .map(|edge| {
1453                        input.lmb.edge_signatures[*edge]
1454                            .compute_momentum(&scaled_sample, self.externals)
1455                    })
1456                    .collect::<Vec<_>>();
1457
1458                let inspect_res_eval = evaluate_momentum_space_point(
1459                    integrand,
1460                    self.model,
1461                    loop_momenta,
1462                    input.graph_id,
1463                    input.orientation,
1464                    use_arb_prec,
1465                )?;
1466
1467                Ok(InspectResult {
1468                    result: inspect_res_eval,
1469                    prefactor,
1470                })
1471            })
1472            .collect()
1473    }
1474}
1475
1476impl SubSetResult {
1477    pub fn analyse_inspect(&self, scales: &[f64]) -> Option<InspectAnalysis> {
1478        analyse_inspect_results(
1479            &self.inspect.summed,
1480            scales,
1481            self.inspect.summed_used_arb_prec_retry,
1482        )
1483    }
1484
1485    pub fn analyse(&self, scales: &[f64]) -> Analysis {
1486        Analysis {
1487            inspect_level: self.analyse_inspect(scales),
1488            inspect_fit_status: InspectFitStatus::from_results(&self.inspect.summed),
1489            per_orientation_inspect: (!self.inspect.per_orientation.is_empty()).then(|| {
1490                self.inspect
1491                    .per_orientation
1492                    .iter()
1493                    .map(|orientation| OrientationInspectAnalysis {
1494                        orientation_label: orientation.label.clone(),
1495                        analysis: analyse_inspect_results(
1496                            &orientation.inspect,
1497                            scales,
1498                            orientation.used_arb_prec_retry,
1499                        ),
1500                        inspect_fit_status: InspectFitStatus::from_results(&orientation.inspect),
1501                    })
1502                    .collect()
1503            }),
1504            analytic: self.analyse_analytic(),
1505        }
1506    }
1507
1508    pub fn analyse_analytic(&self) -> Option<AnalyticAnalysis> {
1509        //             .derived_data
1510        //             .cff_expression
1511        //             .as_ref()
1512        //             .unwrap()
1513        //             .orientations
1514        //             .iter()
1515        //             .enumerate()
1516        //         {
1517        //             let expansion = symbol!("lambd");
1518
1519        //             for (ls, res) in &analytic_res[i_lmb][i] {
1520        //                 // print!("res:{res}");
1521        //                 let l = res.coefficient_list::<i8>(&[Atom::var(expansion)]);
1522
1523        //                 println!(
1524        //                     "In the limit of {:?} going to infinity for orientation \n{}:",
1525        //                     ls.included_iter()
1526        //                         .map(|l| lmb.loop_edges[l].to_string())
1527        //                         .collecqt::<Vec<_>>(),
1528        //                     o.data
1529        //                 );
1530        //                 if l.is_empty() {
1531        //                     println!("\tFull cancellation to order 1");
1532        //                 }
1533        //                 for (t, a) in l {
1534        //                     println!("\t{}: {}", t, a);
1535        //                 }
1536        //             }
1537        //         }
1538
1539        Some(AnalyticAnalysis {
1540            per_orientation: self
1541                .analytic
1542                .as_ref()?
1543                .per_orientations
1544                .par_iter()
1545                .map(|(k, v)| {
1546                    (
1547                        k.clone(),
1548                        OrientationAnalyticAnalysis {
1549                            is_constant: v.is_constant(),
1550                            leading_coef: v.lcoeff().to_ordered_simple(),
1551                        },
1552                    )
1553                })
1554                .collect(),
1555        })
1556    }
1557}
1558
1559fn analyse_inspect_results(
1560    inspect: &[InspectResult],
1561    scales: &[f64],
1562    used_arb_prec_retry: bool,
1563) -> Option<InspectAnalysis> {
1564    let result = log_log_slope(inspect, scales)?;
1565    let dod = result.slope.round() as i64;
1566    Some(InspectAnalysis {
1567        result,
1568        estimated_dod: dod,
1569        used_arb_prec_retry,
1570    })
1571}
1572
1573fn inspect_results_need_arbprec_retry(inspect: &[InspectResult], scales: &[f64]) -> bool {
1574    match analyse_inspect_results(inspect, scales, false) {
1575        None => true,
1576        Some(analysis) => {
1577            analysis.result.slope.is_nan() || analysis.result.slope > UV_PROFILE_RETRY_MAX_DOD
1578        }
1579    }
1580}
1581
1582fn log_log_slope(inspect: &[InspectResult], scales: &[f64]) -> Option<FitResult> {
1583    let mut valid_samples = Vec::new();
1584    let mut sum_x = 0.0;
1585    let mut sum_y = 0.0;
1586    let mut sum_xy = 0.0;
1587    let mut sum_x2 = 0.0;
1588    let mut points = vec![];
1589    let mut points_detail = vec![];
1590
1591    for (x, s) in inspect.iter().zip(scales) {
1592        let norm = x.magnitude();
1593        if norm <= 0.0 {
1594            debug!("{s}:\t{}", x.result.evaluation_metadata);
1595            continue;
1596        }
1597        if !norm.is_finite() {
1598            continue;
1599        }
1600        points_detail.push(x.result.clone());
1601        points.push(norm);
1602        let y = (norm).log10();
1603        let x = s.log10();
1604        if !y.is_finite() || !x.is_finite() {
1605            continue;
1606        }
1607        valid_samples.push((x, y));
1608        sum_x += x;
1609        sum_y += y;
1610        sum_xy += x * y;
1611        sum_x2 += x * x;
1612    }
1613
1614    if valid_samples.len() < 2 {
1615        return None;
1616    }
1617
1618    let n = valid_samples.len() as f64;
1619    let denominator = n * sum_x2 - sum_x * sum_x;
1620    if denominator.abs() < 1e-15 {
1621        return None;
1622    }
1623
1624    let slope = (n * sum_xy - sum_x * sum_y) / denominator;
1625    let intercept = (sum_y - slope * sum_x) / n;
1626
1627    let y_mean = sum_y / n;
1628    let mut ss_tot = 0.0;
1629    let mut ss_res = 0.0;
1630    for (x, y) in &valid_samples {
1631        let y_pred = intercept + slope * x;
1632        ss_tot += (y - y_mean).powi(2);
1633        ss_res += (y - y_pred).powi(2);
1634    }
1635
1636    let r_squared = if ss_tot > 1e-15 {
1637        1.0 - ss_res / ss_tot
1638    } else {
1639        0.0
1640    };
1641
1642    Some(FitResult {
1643        points,
1644        points_detail,
1645        slope,
1646        intercept,
1647        r_squared,
1648    })
1649}
1650
1651fn sum_orientation_inspect_samples(
1652    per_orientation: &[OrientationInspectSamples],
1653) -> Vec<InspectResult> {
1654    let Some((first, rest)) = per_orientation.split_first() else {
1655        return Vec::new();
1656    };
1657
1658    (0..first.inspect.len())
1659        .map(|point_index| {
1660            let mut summed = first.inspect[point_index].clone();
1661            for orientation in rest {
1662                summed.result.integrand_result +=
1663                    orientation.inspect[point_index].result.integrand_result;
1664            }
1665            summed
1666        })
1667        .collect()
1668}
1669
1670fn inspect_retry_label(analysis: Option<&InspectAnalysis>) -> String {
1671    match analysis {
1672        Some(analysis) if analysis.used_arb_prec_retry => "arb retry".yellow().to_string(),
1673        Some(_) => String::new(),
1674        None => "-".to_string(),
1675    }
1676}
1677
1678fn evaluate_momentum_space_point(
1679    integrand: &mut ProcessIntegrand,
1680    model: &Model,
1681    loop_momenta: Vec<ThreeMomentum<F<f64>>>,
1682    graph_id: usize,
1683    orientation: Option<usize>,
1684    use_arb_prec: bool,
1685) -> Result<EvaluationResult> {
1686    match integrand {
1687        ProcessIntegrand::Amplitude(amplitude) => evaluate_profile_momentum_point(
1688            amplitude,
1689            model,
1690            graph_id,
1691            orientation,
1692            loop_momenta,
1693            use_arb_prec,
1694        ),
1695        ProcessIntegrand::CrossSection(cross_section) => evaluate_profile_momentum_point(
1696            cross_section,
1697            model,
1698            graph_id,
1699            orientation,
1700            loop_momenta,
1701            use_arb_prec,
1702        ),
1703    }
1704}
1705
1706#[derive(Debug, Clone)]
1707pub struct InspectResult {
1708    pub(crate) result: EvaluationResult,
1709    pub(crate) prefactor: f64,
1710}
1711
1712impl InspectResult {
1713    fn magnitude(&self) -> f64 {
1714        self.result.integrand_result.norm_squared().sqrt().0 * self.prefactor
1715    }
1716}
1717
1718#[derive(Debug, Clone, Copy, Default)]
1719struct InspectFitStatus {
1720    finite_samples: usize,
1721    positive_finite_samples: usize,
1722}
1723
1724impl InspectFitStatus {
1725    fn from_results(inspect: &[InspectResult]) -> Self {
1726        inspect.iter().fold(Self::default(), |mut status, result| {
1727            let norm = result.magnitude();
1728            if norm.is_finite() {
1729                status.finite_samples += 1;
1730                if norm > 0.0 {
1731                    status.positive_finite_samples += 1;
1732                }
1733            }
1734            status
1735        })
1736    }
1737
1738    fn missing_fit_is_vanishing(self) -> bool {
1739        self.finite_samples > self.positive_finite_samples && self.positive_finite_samples < 2
1740    }
1741}
1742
1743#[derive(Debug, Clone, Serialize)]
1744pub struct Analysis {
1745    ///Is None if the fit hasn't worked
1746    inspect_level: Option<InspectAnalysis>,
1747    #[serde(skip_serializing)]
1748    inspect_fit_status: InspectFitStatus,
1749    #[serde(skip_serializing)]
1750    per_orientation_inspect: Option<Vec<OrientationInspectAnalysis>>,
1751    ///Is None if the analytic analysis is disabled
1752    #[serde(skip_serializing)]
1753    analytic: Option<AnalyticAnalysis>,
1754}
1755
1756impl Analysis {
1757    fn per_orientation_inspect_entries(&self) -> Option<Vec<UVProfileOrientationInspectEntry>> {
1758        self.per_orientation_inspect.as_ref().map(|entries| {
1759            entries
1760                .iter()
1761                .map(|entry| UVProfileOrientationInspectEntry {
1762                    orientation_label: entry.orientation_label.clone(),
1763                    analysis: entry.analysis.clone(),
1764                })
1765                .collect()
1766        })
1767    }
1768}
1769
1770#[derive(Debug, Clone)]
1771struct OrientationInspectAnalysis {
1772    orientation_label: String,
1773    analysis: Option<InspectAnalysis>,
1774    inspect_fit_status: InspectFitStatus,
1775}
1776
1777#[derive(Debug, Clone, Serialize)]
1778pub struct AnalyticAnalysis {
1779    per_orientation: BTreeMap<OrientationData, OrientationAnalyticAnalysis>,
1780}
1781
1782#[derive(Debug, Clone, Serialize)]
1783pub struct OrientationAnalyticAnalysis {
1784    is_constant: bool,
1785    leading_coef: String,
1786}
1787
1788#[derive(Debug, Clone, Serialize)]
1789pub struct InspectAnalysis {
1790    result: FitResult,
1791    estimated_dod: i64,
1792    used_arb_prec_retry: bool,
1793}
1794
1795pub struct AnalyticResult {
1796    pub(crate) per_orientations: BTreeMap<OrientationData, Series<AtomField>>,
1797}