1use 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 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 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 }
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 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)]
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 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 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 #[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}