1use std::fmt::Display;
2use std::time::Duration;
3
4use bincode::{Decode, Encode};
5use colored::Colorize;
6use serde::{Deserialize, Serialize};
7use spenso::algebra::complex::Complex;
8use tabled::{
9 Table, Tabled,
10 builder::Builder,
11 settings::{
12 Alignment, Modify, Panel, Style,
13 object::{Columns, Object, Rows},
14 style::{HorizontalLine, On, VerticalLine},
15 themes::BorderCorrection,
16 },
17};
18
19use crate::observables::{
20 EventGroupList, GenericEventGroupList, ObservableSnapshotBundle,
21 events::{format_complex_generic, format_optional_real_generic, format_real_generic},
22};
23use crate::{
24 settings::runtime::{IntegrationStatisticsSnapshot, Precision},
25 utils::{
26 ArbPrec, F, FloatLike, duration_from_secs_f64_saturating, f128, format_evaluation_time,
27 newton_solver::RadialRootDiagnostics, normalize_tabled_separator_rows,
28 },
29};
30
31#[derive(Clone, Debug)]
32pub struct GraphEvaluationResult<T: FloatLike> {
33 pub integrand_result: Complex<F<T>>,
34 pub event_groups: GenericEventGroupList<T>,
35 pub event_processing_time: Duration,
36 pub generated_event_count: usize,
37 pub accepted_event_count: usize,
38}
39
40impl<T: FloatLike> GraphEvaluationResult<T> {
41 pub fn zero(zero: F<T>) -> Self {
42 Self {
43 integrand_result: Complex::new_re(zero),
44 event_groups: GenericEventGroupList::default(),
45 event_processing_time: Duration::ZERO,
46 generated_event_count: 0,
47 accepted_event_count: 0,
48 }
49 }
50
51 pub fn merge_in_place(&mut self, mut other: Self) {
52 self.integrand_result += other.integrand_result;
53 self.event_groups.append(&mut other.event_groups);
54 self.event_processing_time += other.event_processing_time;
55 self.generated_event_count += other.generated_event_count;
56 self.accepted_event_count += other.accepted_event_count;
57 }
58
59 pub fn into_f64(self) -> GraphEvaluationResult<f64> {
60 GraphEvaluationResult {
61 integrand_result: Complex::new(
62 self.integrand_result.re.into_ff64(),
63 self.integrand_result.im.into_ff64(),
64 ),
65 event_groups: self.event_groups.to_f64(),
66 event_processing_time: self.event_processing_time,
67 generated_event_count: self.generated_event_count,
68 accepted_event_count: self.accepted_event_count,
69 }
70 }
71}
72
73#[derive(Clone, Serialize, Debug)]
75pub struct EvaluationResult {
76 pub integrand_result: Complex<F<f64>>,
78 pub parameterization_jacobian: Option<F<f64>>,
79 pub integrator_weight: F<f64>,
81 pub event_groups: EventGroupList,
82 pub evaluation_metadata: EvaluationMetaData,
83}
84
85#[derive(Clone, Serialize, Debug)]
86pub struct EvaluationResultOutput {
87 pub integrand_result: Complex<F<f64>>,
88 pub parameterization_jacobian: Option<F<f64>>,
89 pub integrator_weight: F<f64>,
90 pub event_groups: EventGroupList,
91 pub evaluation_metadata: Option<EvaluationMetaData>,
92}
93
94#[derive(Clone, Debug)]
95pub struct GenericEvaluationResult<T: FloatLike> {
96 pub integrand_result: Complex<F<T>>,
97 pub parameterization_jacobian: Option<F<T>>,
98 pub integrator_weight: F<T>,
99 pub event_groups: GenericEventGroupList<T>,
100 pub evaluation_metadata: EvaluationMetaData,
101}
102
103#[derive(Clone, Debug)]
104pub struct GenericEvaluationResultOutput<T: FloatLike> {
105 pub integrand_result: Complex<F<T>>,
106 pub parameterization_jacobian: Option<F<T>>,
107 pub integrator_weight: F<T>,
108 pub event_groups: GenericEventGroupList<T>,
109 pub evaluation_metadata: Option<EvaluationMetaData>,
110}
111
112impl<T: FloatLike> GenericEvaluationResult<T> {
113 pub fn into_output(self, minimal_output: bool) -> GenericEvaluationResultOutput<T> {
114 GenericEvaluationResultOutput {
115 integrand_result: self.integrand_result,
116 parameterization_jacobian: self.parameterization_jacobian,
117 integrator_weight: self.integrator_weight,
118 event_groups: self.event_groups,
119 evaluation_metadata: (!minimal_output).then_some(self.evaluation_metadata),
120 }
121 }
122}
123
124#[derive(Clone, Debug)]
125pub enum PreciseEvaluationResultOutput {
126 Double(GenericEvaluationResultOutput<f64>),
127 Quad(GenericEvaluationResultOutput<f128>),
128 Arb(GenericEvaluationResultOutput<ArbPrec>),
129}
130
131#[derive(Clone, Debug)]
132pub enum PreciseEvaluationResult {
133 Double(GenericEvaluationResult<f64>),
134 Quad(GenericEvaluationResult<f128>),
135 Arb(GenericEvaluationResult<ArbPrec>),
136}
137
138impl PreciseEvaluationResult {
139 pub fn into_output(self, minimal_output: bool) -> PreciseEvaluationResultOutput {
140 match self {
141 PreciseEvaluationResult::Double(result) => {
142 PreciseEvaluationResultOutput::Double(result.into_output(minimal_output))
143 }
144 PreciseEvaluationResult::Quad(result) => {
145 PreciseEvaluationResultOutput::Quad(result.into_output(minimal_output))
146 }
147 PreciseEvaluationResult::Arb(result) => {
148 PreciseEvaluationResultOutput::Arb(result.into_output(minimal_output))
149 }
150 }
151 }
152}
153
154impl PreciseEvaluationResultOutput {
155 pub fn precision(&self) -> Precision {
156 match self {
157 PreciseEvaluationResultOutput::Double(_) => Precision::Double,
158 PreciseEvaluationResultOutput::Quad(_) => Precision::Quad,
159 PreciseEvaluationResultOutput::Arb(_) => Precision::Arb,
160 }
161 }
162
163 pub fn evaluation_metadata(&self) -> Option<&EvaluationMetaData> {
164 match self {
165 PreciseEvaluationResultOutput::Double(result) => result.evaluation_metadata.as_ref(),
166 PreciseEvaluationResultOutput::Quad(result) => result.evaluation_metadata.as_ref(),
167 PreciseEvaluationResultOutput::Arb(result) => result.evaluation_metadata.as_ref(),
168 }
169 }
170}
171
172impl EvaluationResult {
173 pub fn zero() -> Self {
174 Self {
175 integrand_result: Complex::new_zero(),
176 parameterization_jacobian: None,
177 integrator_weight: F(0.0),
178 event_groups: EventGroupList::default(),
179 evaluation_metadata: EvaluationMetaData::new_empty(),
180 }
181 }
182
183 pub fn into_output(self, minimal_output: bool) -> EvaluationResultOutput {
184 EvaluationResultOutput {
185 integrand_result: self.integrand_result,
186 parameterization_jacobian: self.parameterization_jacobian,
187 integrator_weight: self.integrator_weight,
188 event_groups: self.event_groups,
189 evaluation_metadata: (!minimal_output).then_some(self.evaluation_metadata),
190 }
191 }
192}
193
194fn fmt_evaluation_result_output<T: FloatLike>(
195 f: &mut std::fmt::Formatter<'_>,
196 precision: Option<Precision>,
197 integrand_result: &Complex<F<T>>,
198 parameterization_jacobian: Option<&F<T>>,
199 integrator_weight: &F<T>,
200 event_groups: &GenericEventGroupList<T>,
201 evaluation_metadata: Option<&EvaluationMetaData>,
202) -> std::fmt::Result {
203 let mut summary_rows = Vec::with_capacity(5);
204 if let Some(precision) = precision {
205 summary_rows.push(EvaluationSummaryRow {
206 field: "precision".to_string(),
207 value: precision.to_string(),
208 });
209 }
210 summary_rows.extend([
211 EvaluationSummaryRow {
212 field: "integrand result".to_string(),
213 value: format_complex_generic(integrand_result),
214 },
215 EvaluationSummaryRow {
216 field: "parameterization jacobian".to_string(),
217 value: format_optional_real_generic(parameterization_jacobian),
218 },
219 EvaluationSummaryRow {
220 field: "integrator weight".to_string(),
221 value: format_real_generic(integrator_weight),
222 },
223 EvaluationSummaryRow {
224 field: "event groups".to_string(),
225 value: format_count(event_groups.len()),
226 },
227 ]);
228
229 writeln!(f, "{}", "Evaluation result".bold().bright_green())?;
230 writeln!(f, "{}", Table::new(summary_rows).with(Style::rounded()))?;
231 if let Some(metadata) = evaluation_metadata {
232 writeln!(f)?;
233 write!(f, "{metadata}")?;
234 }
235
236 if !event_groups.is_empty() {
237 writeln!(f)?;
238 writeln!(
239 f,
240 "{}",
241 format!(
242 "Generated {} event group(s)",
243 format_count(event_groups.len())
244 )
245 .bold()
246 .bright_cyan()
247 )?;
248 write!(f, "{event_groups}")?;
249 }
250
251 Ok(())
252}
253
254fn fmt_sample_result<D: Display>(
255 f: &mut std::fmt::Formatter<'_>,
256 evaluation: &D,
257 observables: &ObservableSnapshotBundle,
258) -> std::fmt::Result {
259 write!(f, "{evaluation}")?;
260
261 if let Some(observables_table) = summarize_observables(observables) {
262 writeln!(f)?;
263 writeln!(f)?;
264 writeln!(f, "{}", "Observable snapshots".bold().bright_magenta())?;
265 write!(f, "{observables_table}")?;
266 }
267
268 Ok(())
269}
270
271fn fmt_batch_result<D: Display>(
272 f: &mut std::fmt::Formatter<'_>,
273 samples: &[D],
274 observables: &ObservableSnapshotBundle,
275) -> std::fmt::Result {
276 let summary_rows = [EvaluationSummaryRow {
277 field: "samples".to_string(),
278 value: format_count(samples.len()),
279 }];
280
281 writeln!(f, "{}", "Batch evaluation result".bold().bright_green())?;
282 writeln!(f, "{}", Table::new(summary_rows).with(Style::rounded()))?;
283
284 if let Some(observables_table) = summarize_observables(observables) {
285 writeln!(f)?;
286 writeln!(f, "{}", "Observable snapshots".bold().bright_magenta())?;
287 writeln!(f, "{observables_table}")?;
288 }
289
290 for (sample_index, sample) in samples.iter().enumerate() {
291 writeln!(f)?;
292 if sample_index > 0 {
293 writeln!(f)?;
294 }
295 writeln!(
296 f,
297 "{}",
298 format!("Sample {sample_index}").bold().bright_cyan()
299 )?;
300 write!(f, "{sample}")?;
301 }
302
303 Ok(())
304}
305
306impl Display for EvaluationResultOutput {
307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308 fmt_evaluation_result_output(
309 f,
310 None,
311 &self.integrand_result,
312 self.parameterization_jacobian.as_ref(),
313 &self.integrator_weight,
314 &self.event_groups,
315 self.evaluation_metadata.as_ref(),
316 )
317 }
318}
319
320impl<T: FloatLike> Display for GenericEvaluationResultOutput<T> {
321 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322 fmt_evaluation_result_output(
323 f,
324 None,
325 &self.integrand_result,
326 self.parameterization_jacobian.as_ref(),
327 &self.integrator_weight,
328 &self.event_groups,
329 self.evaluation_metadata.as_ref(),
330 )
331 }
332}
333
334impl Display for PreciseEvaluationResultOutput {
335 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336 match self {
337 PreciseEvaluationResultOutput::Double(result) => fmt_evaluation_result_output(
338 f,
339 Some(Precision::Double),
340 &result.integrand_result,
341 result.parameterization_jacobian.as_ref(),
342 &result.integrator_weight,
343 &result.event_groups,
344 result.evaluation_metadata.as_ref(),
345 ),
346 PreciseEvaluationResultOutput::Quad(result) => fmt_evaluation_result_output(
347 f,
348 Some(Precision::Quad),
349 &result.integrand_result,
350 result.parameterization_jacobian.as_ref(),
351 &result.integrator_weight,
352 &result.event_groups,
353 result.evaluation_metadata.as_ref(),
354 ),
355 PreciseEvaluationResultOutput::Arb(result) => fmt_evaluation_result_output(
356 f,
357 Some(Precision::Arb),
358 &result.integrand_result,
359 result.parameterization_jacobian.as_ref(),
360 &result.integrator_weight,
361 &result.event_groups,
362 result.evaluation_metadata.as_ref(),
363 ),
364 }
365 }
366}
367
368impl Display for SampleEvaluationResult {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 write!(f, "{}", self.evaluation)
371 }
372}
373
374#[derive(Clone, Serialize, Debug)]
375pub struct SampleEvaluationResult {
376 pub evaluation: EvaluationResultOutput,
377}
378
379#[derive(Clone, Debug)]
380pub struct PreciseSampleEvaluationResult {
381 pub evaluation: PreciseEvaluationResultOutput,
382}
383
384#[derive(Clone, Serialize, Debug)]
385pub struct SingleSampleEvaluationResult {
386 pub sample: SampleEvaluationResult,
388 pub observables: ObservableSnapshotBundle,
392}
393
394#[derive(Clone, Serialize, Debug)]
395pub struct BatchSampleEvaluationResult {
396 pub samples: Vec<SampleEvaluationResult>,
398 pub observables: ObservableSnapshotBundle,
402}
403
404#[derive(Clone, Debug)]
405pub struct RawBatchEvaluationResult {
406 pub samples: Vec<EvaluationResult>,
407 pub statistics: StatisticsCounter,
408}
409
410#[derive(Clone, Debug)]
411pub struct PreciseSingleSampleEvaluationResult {
412 pub sample: PreciseSampleEvaluationResult,
414 pub observables: ObservableSnapshotBundle,
416}
417
418#[derive(Clone, Debug)]
419pub struct PreciseBatchSampleEvaluationResult {
420 pub samples: Vec<PreciseSampleEvaluationResult>,
422 pub observables: ObservableSnapshotBundle,
424}
425
426#[derive(Clone, Debug)]
427pub struct RawPreciseBatchEvaluationResult {
428 pub samples: Vec<PreciseEvaluationResult>,
429}
430
431impl Display for PreciseSampleEvaluationResult {
432 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
433 write!(f, "{}", self.evaluation)
434 }
435}
436
437impl Display for SingleSampleEvaluationResult {
438 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439 fmt_sample_result(f, &self.sample.evaluation, &self.observables)
440 }
441}
442
443impl Display for BatchSampleEvaluationResult {
444 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445 fmt_batch_result(f, &self.samples, &self.observables)
446 }
447}
448
449impl Display for PreciseSingleSampleEvaluationResult {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 fmt_sample_result(f, &self.sample.evaluation, &self.observables)
452 }
453}
454
455impl Display for PreciseBatchSampleEvaluationResult {
456 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457 fmt_batch_result(f, &self.samples, &self.observables)
458 }
459}
460
461#[derive(Tabled)]
462struct EvaluationSummaryRow {
463 field: String,
464 value: String,
465}
466
467#[derive(Tabled)]
468struct HistogramSummaryRow {
469 name: String,
470 bins: usize,
471 #[tabled(rename = "in-range")]
472 in_range_entries: String,
473 phase: String,
474 range: String,
475 underflow: String,
476 overflow: String,
477}
478
479#[derive(Tabled)]
480struct StabilitySummaryRow {
481 level: String,
482 relative_accuracy: String,
483 time: String,
484 status: String,
485}
486
487fn format_duration(duration: Duration) -> String {
488 format_evaluation_time(duration)
489}
490
491fn format_count(value: usize) -> String {
492 if value < 1_000 {
493 return value.to_string();
494 }
495
496 let value = value as f64;
497 for (scale, suffix) in [
498 (1_000_000_000_f64, "B"),
499 (1_000_000_f64, "M"),
500 (1_000_f64, "K"),
501 ] {
502 if value >= scale {
503 let scaled = value / scale;
504 let precision = if scaled >= 100.0 {
505 0
506 } else if scaled >= 10.0 {
507 1
508 } else {
509 2
510 };
511 return format!("{scaled:.precision$}{suffix}");
512 }
513 }
514
515 value.round().to_string()
516}
517
518fn format_percentage(value: f64, significant_digits: usize) -> String {
519 if !value.is_finite() {
520 return "None".red().to_string();
521 }
522
523 if value == 0.0 {
524 let decimals = significant_digits.saturating_sub(1);
525 return format!("{:.*}%", decimals, 0.0);
526 }
527
528 let abs_value = value.abs();
529 let exponent = abs_value.log10().floor() as i32;
530 if exponent < -2 || exponent >= significant_digits as i32 {
531 return format!("{:.*e}%", significant_digits.saturating_sub(1), value);
532 }
533
534 let decimals = (significant_digits as i32 - exponent - 1).max(0) as usize;
535 format!("{value:.decimals$}%")
536}
537
538fn format_status_header(label: &str) -> String {
539 format!("{label:<7}").blue().bold().to_string()
540}
541
542fn format_status_key(label: &str) -> String {
543 format!("{label} : ")
544}
545
546fn pad_status_value(value: impl Display, width: usize) -> String {
547 format!("{value:<width$}")
548}
549
550fn status_group_separator() -> VerticalLine<On, On, ()> {
551 VerticalLine::new('│').top('┬').bottom('┴')
552}
553
554fn summarize_observables(observables: &ObservableSnapshotBundle) -> Option<String> {
555 if observables.histograms.is_empty() {
556 return None;
557 }
558
559 let rows = observables
560 .histograms
561 .iter()
562 .map(|(name, histogram)| HistogramSummaryRow {
563 name: name.clone(),
564 bins: histogram.bins.len(),
565 in_range_entries: format_count(histogram.statistics.in_range_entry_count),
566 phase: format!("{:?}", histogram.phase).to_lowercase(),
567 range: match histogram.kind {
568 crate::observables::HistogramSnapshotKind::Continuous => format!(
569 "[{:+.16e}, {:+.16e}]",
570 histogram.x_min.unwrap_or_default(),
571 histogram.x_max.unwrap_or_default()
572 ),
573 crate::observables::HistogramSnapshotKind::Discrete => {
574 let min = histogram.discrete_min_bin_id.unwrap_or_default();
575 let max = min + histogram.bins.len() as isize - 1;
576 format!("[{}, {}]", min, max)
577 }
578 },
579 underflow: format_count(histogram.underflow_bin.entry_count),
580 overflow: format_count(histogram.overflow_bin.entry_count),
581 })
582 .collect::<Vec<_>>();
583
584 Some(Table::new(rows).with(Style::rounded()).to_string())
585}
586
587#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
589pub enum StabilityFailureReason {
590 ErrorThreshold,
591 ZeroError,
592 WeightThreshold,
593}
594
595#[derive(Clone, Debug, Serialize)]
596pub struct RotatedEvaluation {
597 pub rotation: String,
598 pub result: Complex<F<f64>>,
599}
600
601#[derive(Clone, Debug, Serialize)]
602pub struct StabilityResult {
603 pub precision: Precision,
604 pub estimated_relative_accuracy: Option<F<f64>>,
605 pub status: StabilityStatus,
606 pub total_time: Duration,
607}
608
609#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
610pub enum StabilityStatus {
611 Unknown,
612 Stable(usize),
613 Unstable(usize),
614}
615
616impl StabilityStatus {
617 pub fn from_sample_count(sample_count: usize, accepted_as_stable: bool) -> Self {
618 if sample_count <= 1 {
619 Self::Unknown
620 } else if accepted_as_stable {
621 Self::Stable(sample_count)
622 } else {
623 Self::Unstable(sample_count)
624 }
625 }
626
627 pub fn sample_count(&self) -> usize {
628 match self {
629 Self::Unknown => 1,
630 Self::Stable(sample_count) | Self::Unstable(sample_count) => *sample_count,
631 }
632 }
633
634 fn styled_label(&self) -> String {
635 let label = self.to_string();
636 match self {
637 Self::Unknown => label.yellow().to_string(),
638 Self::Stable(_) => label.green().to_string(),
639 Self::Unstable(_) => label.red().to_string(),
640 }
641 }
642}
643
644impl Display for StabilityStatus {
645 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
646 match self {
647 Self::Unknown => write!(f, "Unknown(1 sample)"),
648 Self::Stable(sample_count) => write!(f, "Stable({sample_count} samples)"),
649 Self::Unstable(sample_count) => write!(f, "Unstable({sample_count} samples)"),
650 }
651 }
652}
653
654#[derive(Clone, Debug, Serialize)]
655pub struct LoopMomentaEscalationMetrics {
656 pub sum_norm: f64,
657 pub threshold: f64,
658}
659
660#[derive(Clone, Serialize, Debug)]
662pub struct EvaluationMetaData {
663 pub total_timing: Duration,
664 pub integrand_evaluation_time: Duration,
665 pub evaluator_evaluation_time: Duration,
666 pub parameterization_time: Duration,
667 pub event_processing_time: Duration,
668 pub generated_event_count: usize,
669 pub accepted_event_count: usize,
670 pub relative_instability_error: Complex<F<f64>>,
671 pub is_nan: bool,
672 pub loop_momenta_escalation: Option<LoopMomentaEscalationMetrics>,
673 pub stability_results: Vec<StabilityResult>,
674 #[serde(skip_serializing_if = "Option::is_none")]
675 pub(crate) threshold_counterterm_error: Option<String>,
676 #[serde(skip)]
677 pub(crate) radial_root_diagnostics: RadialRootDiagnostics,
678}
679
680impl Display for EvaluationMetaData {
681 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682 let rows = vec![
683 EvaluationSummaryRow {
684 field: "nan".to_string(),
685 value: self.is_nan.to_string(),
686 },
687 EvaluationSummaryRow {
688 field: "generated events".to_string(),
689 value: format_count(self.generated_event_count),
690 },
691 EvaluationSummaryRow {
692 field: "accepted events".to_string(),
693 value: format_count(self.accepted_event_count),
694 },
695 EvaluationSummaryRow {
696 field: "parameterization time".to_string(),
697 value: format_duration(self.parameterization_time),
698 },
699 EvaluationSummaryRow {
700 field: "integrand evaluation time".to_string(),
701 value: format_duration(self.integrand_evaluation_time),
702 },
703 EvaluationSummaryRow {
704 field: "evaluator evaluation time".to_string(),
705 value: format_duration(self.evaluator_evaluation_time),
706 },
707 EvaluationSummaryRow {
708 field: "event processing time".to_string(),
709 value: format_duration(self.event_processing_time),
710 },
711 EvaluationSummaryRow {
712 field: "total evaluation time".to_string(),
713 value: format_duration(self.total_timing),
714 },
715 ];
716 writeln!(f, "{}", "Evaluation metadata".bold().bright_yellow())?;
717 writeln!(f, "{}", Table::new(rows).with(Style::rounded()))?;
718
719 if !self.stability_results.is_empty() {
720 let stability_rows = self
721 .stability_results
722 .iter()
723 .map(|result| StabilitySummaryRow {
724 level: result.precision.to_string(),
725 relative_accuracy: format_optional_real_generic(
726 result.estimated_relative_accuracy.as_ref(),
727 ),
728 time: format_duration(result.total_time),
729 status: result.status.styled_label(),
730 })
731 .collect::<Vec<_>>();
732 writeln!(f)?;
733 writeln!(f, "{}", "Stability results".bold().bright_magenta())?;
734 write!(f, "{}", Table::new(stability_rows).with(Style::rounded()))?;
735 }
736
737 Ok(())
738 }
739}
740
741impl EvaluationMetaData {
742 pub(crate) fn new_empty() -> Self {
743 Self {
744 total_timing: Duration::ZERO,
745 integrand_evaluation_time: Duration::ZERO,
746 evaluator_evaluation_time: Duration::ZERO,
747 parameterization_time: Duration::ZERO,
748 event_processing_time: Duration::ZERO,
749 generated_event_count: 0,
750 accepted_event_count: 0,
751 relative_instability_error: Complex::new_zero(),
752 is_nan: false,
753 loop_momenta_escalation: None,
754 stability_results: Vec::new(),
755 threshold_counterterm_error: None,
756 radial_root_diagnostics: RadialRootDiagnostics::default(),
757 }
758 }
759
760 pub(crate) fn clear_threshold_counterterm_error(&mut self) {
761 self.threshold_counterterm_error = None;
762 }
763
764 pub(crate) fn record_threshold_counterterm_error(&mut self, error: impl Into<String>) {
765 if self.threshold_counterterm_error.is_none() {
766 self.threshold_counterterm_error = Some(error.into());
767 }
768 }
769
770 pub(crate) fn final_precision(&self) -> Option<Precision> {
771 self.stability_results.last().map(|result| result.precision)
772 }
773}
774
775#[derive(Debug, Copy, Clone, Serialize, Deserialize, Encode, Decode)]
777pub struct StatisticsCounter {
778 pub num_evals: usize,
779 num_sample_points: usize,
780 sum_integrand_evaluation_time: Duration,
781 sum_evaluator_evaluation_time: Duration,
782 sum_parameterization_time: Duration,
783 sum_event_time: Duration,
784 sum_integrator_overhead_time: Duration,
785 sum_total_evaluation_time: Duration,
786 sum_relative_instability_error: (F<f64>, F<f64>),
787 num_double_precision_evals: usize,
788 num_quadruple_precision_evals: usize,
789 num_arb_precision_evals: usize,
790 num_nan_evals: usize,
791 num_nan_or_unstable_evals: usize,
792 sum_generated_event_count: usize,
793 sum_accepted_event_count: usize,
794}
795
796impl StatisticsCounter {
797 pub(crate) fn from_evaluation_results(data: &[EvaluationResult]) -> Self {
799 data.iter().fold(
800 StatisticsCounter::new_empty(),
801 |mut accumulator, data_entry| {
802 accumulator.sum_integrand_evaluation_time +=
803 data_entry.evaluation_metadata.integrand_evaluation_time;
804 accumulator.sum_evaluator_evaluation_time +=
805 data_entry.evaluation_metadata.evaluator_evaluation_time;
806 accumulator.sum_parameterization_time +=
807 data_entry.evaluation_metadata.parameterization_time;
808 accumulator.sum_event_time += data_entry.evaluation_metadata.event_processing_time;
809 accumulator.sum_relative_instability_error.0 +=
810 data_entry.evaluation_metadata.relative_instability_error.re;
811 accumulator.sum_relative_instability_error.1 +=
812 data_entry.evaluation_metadata.relative_instability_error.im;
813 accumulator.sum_total_evaluation_time +=
814 data_entry.evaluation_metadata.total_timing;
815 accumulator.sum_generated_event_count +=
816 data_entry.evaluation_metadata.generated_event_count;
817 accumulator.sum_accepted_event_count +=
818 data_entry.evaluation_metadata.accepted_event_count;
819
820 accumulator.num_evals += 1;
821 match data_entry
822 .evaluation_metadata
823 .final_precision()
824 .unwrap_or(Precision::Double)
825 {
826 Precision::Double => accumulator.num_double_precision_evals += 1,
827 Precision::Quad => accumulator.num_quadruple_precision_evals += 1,
828 Precision::Arb => accumulator.num_arb_precision_evals += 1,
829 }
831
832 if data_entry.evaluation_metadata.is_nan {
833 accumulator.num_nan_evals += 1;
834 }
835 if data_entry.evaluation_metadata.is_nan
836 || data_entry
837 .evaluation_metadata
838 .stability_results
839 .last()
840 .map(|result| matches!(result.status, StabilityStatus::Unstable(_)))
841 .unwrap_or(false)
842 {
843 accumulator.num_nan_or_unstable_evals += 1;
844 }
845
846 accumulator
847 },
848 )
849 }
850
851 pub(crate) fn merged(&self, other: &Self) -> Self {
853 Self {
854 sum_integrand_evaluation_time: self.sum_integrand_evaluation_time
855 + other.sum_integrand_evaluation_time,
856 sum_evaluator_evaluation_time: self.sum_evaluator_evaluation_time
857 + other.sum_evaluator_evaluation_time,
858 sum_parameterization_time: self.sum_parameterization_time
859 + other.sum_parameterization_time,
860 sum_integrator_overhead_time: self.sum_integrator_overhead_time
861 + other.sum_integrator_overhead_time,
862 sum_relative_instability_error: (
863 self.sum_relative_instability_error.0 + other.sum_relative_instability_error.0,
864 self.sum_relative_instability_error.1 + other.sum_relative_instability_error.1,
865 ),
866 num_evals: self.num_evals + other.num_evals,
867 num_sample_points: self.num_sample_points + other.num_sample_points,
868 num_double_precision_evals: (self.num_double_precision_evals
869 + other.num_double_precision_evals),
870 num_quadruple_precision_evals: (self.num_quadruple_precision_evals
871 + other.num_quadruple_precision_evals),
872 num_arb_precision_evals: self.num_arb_precision_evals + other.num_arb_precision_evals,
873 sum_total_evaluation_time: self.sum_total_evaluation_time
874 + other.sum_total_evaluation_time,
875 num_nan_evals: self.num_nan_evals + other.num_nan_evals,
876 num_nan_or_unstable_evals: self.num_nan_or_unstable_evals
877 + other.num_nan_or_unstable_evals,
878 sum_event_time: self.sum_event_time + other.sum_event_time,
879 sum_generated_event_count: self.sum_generated_event_count
880 + other.sum_generated_event_count,
881 sum_accepted_event_count: self.sum_accepted_event_count
882 + other.sum_accepted_event_count,
883 }
884 }
885
886 pub(crate) fn new_empty() -> Self {
887 Self {
888 sum_integrand_evaluation_time: Duration::ZERO,
889 sum_evaluator_evaluation_time: Duration::ZERO,
890 sum_parameterization_time: Duration::ZERO,
891 sum_event_time: Duration::ZERO,
892 sum_integrator_overhead_time: Duration::ZERO,
893 sum_relative_instability_error: (F(0.0), F(0.0)),
894 sum_total_evaluation_time: Duration::ZERO,
895 num_evals: 0,
896 num_sample_points: 0,
897 num_double_precision_evals: 0,
898 num_quadruple_precision_evals: 0,
899 num_arb_precision_evals: 0,
900 num_nan_evals: 0,
901 num_nan_or_unstable_evals: 0,
902 sum_generated_event_count: 0,
903 sum_accepted_event_count: 0,
904 }
905 }
906
907 #[allow(dead_code)]
909 pub(crate) fn merge_list(list: Vec<Self>) -> Self {
910 if let Some(merged) = list.into_iter().reduce(|acc, x| acc.merged(&x)) {
911 merged
912 } else {
913 Self::new_empty()
914 }
915 }
916
917 fn avg_duration(sum: Duration, count: usize) -> Duration {
918 if count == 0 {
919 return Duration::ZERO;
920 }
921
922 duration_from_secs_f64_saturating(sum.as_secs_f64() / count as f64)
923 }
924
925 fn eval_percentage(&self, count: usize) -> f64 {
926 if self.num_evals == 0 {
927 0.0
928 } else {
929 count as f64 / self.num_evals as f64 * 100.0
930 }
931 }
932
933 pub(crate) fn add_integrator_overhead(&mut self, duration: Duration, sample_points: usize) {
934 self.sum_integrator_overhead_time += duration;
935 self.num_sample_points += sample_points;
936 }
937
938 pub(crate) fn get_avg_total_timing(&self) -> Duration {
940 Self::avg_duration(self.sum_total_evaluation_time, self.num_evals)
941 }
942
943 pub(crate) fn get_avg_integrand_timing(&self) -> Duration {
945 Self::avg_duration(self.sum_integrand_evaluation_time, self.num_evals)
946 }
947
948 pub(crate) fn get_avg_evaluator_timing(&self) -> Duration {
950 Self::avg_duration(self.sum_evaluator_evaluation_time, self.num_evals)
951 }
952
953 pub(crate) fn get_avg_param_timing(&self) -> Duration {
955 Self::avg_duration(self.sum_parameterization_time, self.num_evals)
956 }
957
958 pub(crate) fn get_avg_event_timing(&self) -> Duration {
959 Self::avg_duration(self.sum_event_time, self.num_evals)
960 }
961
962 pub(crate) fn get_avg_integrator_overhead_timing(&self) -> Duration {
963 Self::avg_duration(self.sum_integrator_overhead_time, self.num_sample_points)
964 }
965
966 #[allow(dead_code)]
968 pub(crate) fn get_avg_instabillity_error(&self) -> (F<f64>, F<f64>) {
969 if self.num_evals == 0 {
970 return (
971 self.sum_relative_instability_error.0.zero(),
972 self.sum_relative_instability_error.1.zero(),
973 );
974 }
975
976 (
977 self.sum_relative_instability_error.0
978 / self
979 .sum_relative_instability_error
980 .0
981 .from_usize(self.num_evals),
982 self.sum_relative_instability_error.1
983 / self
984 .sum_relative_instability_error
985 .1
986 .from_usize(self.num_evals),
987 )
988 }
989
990 pub(crate) fn get_percentage_f64(&self) -> f64 {
992 self.eval_percentage(self.num_double_precision_evals)
993 }
994
995 pub(crate) fn get_percentage_f128(&self) -> f64 {
997 self.eval_percentage(self.num_quadruple_precision_evals)
998 }
999
1000 pub(crate) fn get_percentage_arb(&self) -> f64 {
1001 self.eval_percentage(self.num_arb_precision_evals)
1002 }
1003
1004 pub(crate) fn get_percentage_nan(&self) -> f64 {
1005 self.eval_percentage(self.num_nan_evals)
1006 }
1007
1008 pub(crate) fn get_percentage_nan_or_unstable(&self) -> f64 {
1009 self.eval_percentage(self.num_nan_or_unstable_evals)
1010 }
1011
1012 pub(crate) fn selection_efficiency_percentage(&self) -> Option<f64> {
1013 if self.sum_generated_event_count == 0 {
1014 None
1015 } else {
1016 Some(
1017 self.sum_accepted_event_count as f64 / self.sum_generated_event_count as f64
1018 * 100.0,
1019 )
1020 }
1021 }
1022
1023 pub(crate) fn snapshot(&self) -> IntegrationStatisticsSnapshot {
1024 IntegrationStatisticsSnapshot {
1025 num_evals: self.num_evals,
1026 average_total_time_seconds: self.get_avg_total_timing().as_secs_f64(),
1027 average_parameterization_time_seconds: self.get_avg_param_timing().as_secs_f64(),
1028 average_integrand_time_seconds: self.get_avg_integrand_timing().as_secs_f64(),
1029 average_evaluator_time_seconds: self.get_avg_evaluator_timing().as_secs_f64(),
1030 average_observable_time_seconds: self.get_avg_event_timing().as_secs_f64(),
1031 average_integrator_time_seconds: self
1032 .get_avg_integrator_overhead_timing()
1033 .as_secs_f64(),
1034 f64_percentage: self.get_percentage_f64(),
1035 f128_percentage: self.get_percentage_f128(),
1036 arb_percentage: self.get_percentage_arb(),
1037 nan_percentage: self.get_percentage_nan(),
1038 nan_or_unstable_percentage: self.get_percentage_nan_or_unstable(),
1039 generated_event_count: self.sum_generated_event_count,
1040 accepted_event_count: self.sum_accepted_event_count,
1041 selection_efficiency_percentage: self.selection_efficiency_percentage(),
1042 }
1043 }
1044
1045 pub(crate) fn build_status_table(&self) -> Table {
1046 let time_integrand_formatted = format_evaluation_time(self.get_avg_integrand_timing());
1047 let time_evaluators_formatted = format_evaluation_time(self.get_avg_evaluator_timing());
1048 let param_time_formatted = format_evaluation_time(self.get_avg_param_timing());
1049 let event_time_formatted = format_evaluation_time(self.get_avg_event_timing());
1050 let integrator_time_formatted =
1051 format_evaluation_time(self.get_avg_integrator_overhead_timing());
1052 let total_time = format_evaluation_time(self.get_avg_total_timing());
1053 let selection_efficiency = self.selection_efficiency_percentage();
1054 let selection_efficiency_display = selection_efficiency
1055 .map(|value| {
1056 pad_status_value(format_percentage(value, 3), 9)
1057 .green()
1058 .to_string()
1059 })
1060 .unwrap_or_else(|| pad_status_value("N/A", 9));
1061 let nan_or_unstable = self.get_percentage_nan_or_unstable();
1062 let nan_or_unstable_display = if nan_or_unstable > 0.0 {
1063 pad_status_value(format_percentage(nan_or_unstable, 2), 9)
1064 .red()
1065 .to_string()
1066 } else {
1067 pad_status_value(format_percentage(nan_or_unstable, 2), 9)
1068 .green()
1069 .to_string()
1070 };
1071 let mut table = Builder::new();
1072 table.push_record([
1073 format_status_header("timing"),
1074 format_status_key("total"),
1075 pad_status_value(total_time, 9).green().to_string(),
1076 format_status_key("param"),
1077 pad_status_value(param_time_formatted, 9)
1078 .green()
1079 .to_string(),
1080 format_status_key("itg"),
1081 pad_status_value(time_integrand_formatted, 9)
1082 .green()
1083 .to_string(),
1084 format_status_key("evaluators"),
1085 pad_status_value(time_evaluators_formatted, 9)
1086 .green()
1087 .to_string(),
1088 ]);
1089 table.push_record([
1090 format_status_header("evals"),
1091 format_status_key("f64"),
1092 pad_status_value(format!("{:.2}%", self.get_percentage_f64()), 9)
1093 .green()
1094 .to_string(),
1095 format_status_key("f128"),
1096 pad_status_value(format!("{:.2}%", self.get_percentage_f128()), 9)
1097 .green()
1098 .to_string(),
1099 format_status_key("arb"),
1100 pad_status_value(format!("{:.2}%", self.get_percentage_arb()), 9)
1101 .green()
1102 .to_string(),
1103 format_status_key("nans+unstable"),
1104 nan_or_unstable_display.clone(),
1105 ]);
1106 table.push_record([
1107 format_status_header("events"),
1108 format_status_key("evts #"),
1109 pad_status_value(format_count(self.sum_generated_event_count), 9)
1110 .green()
1111 .to_string(),
1112 format_status_key("sel. %"),
1113 selection_efficiency_display,
1114 format_status_key("obs"),
1115 pad_status_value(event_time_formatted, 9)
1116 .green()
1117 .to_string(),
1118 format_status_key("integrator"),
1119 pad_status_value(integrator_time_formatted, 9)
1120 .green()
1121 .to_string(),
1122 ]);
1123
1124 let mut table = table.build();
1125 table.with(Panel::header(
1126 "Integration statistics".bold().green().to_string(),
1127 ));
1128 table.with(
1129 Style::rounded()
1130 .remove_horizontals()
1131 .verticals([
1132 (1, status_group_separator()),
1133 (3, status_group_separator()),
1134 (5, status_group_separator()),
1135 (7, status_group_separator()),
1136 ])
1137 .horizontals([(
1138 1,
1139 HorizontalLine::new('─')
1140 .intersection('┬')
1141 .left('├')
1142 .right('┤'),
1143 )])
1144 .remove_vertical(),
1145 );
1146 table.with(BorderCorrection::span());
1147 table.with(Modify::new(Rows::new(0..1)).with(Alignment::center()));
1148 table.with(Modify::new(Rows::new(1..)).with(Alignment::left()));
1149 for column in [1usize, 3, 5, 7] {
1150 table.with(
1151 Modify::new(Rows::new(1..).intersect(Columns::one(column)))
1152 .with(Alignment::right()),
1153 );
1154 }
1155 table
1156 }
1157
1158 pub(crate) fn render_status_table(&self) -> String {
1159 normalize_tabled_separator_rows(&self.build_status_table().to_string())
1160 }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165 use std::time::Duration;
1166
1167 use crate::settings::runtime::Precision;
1168
1169 use super::{EvaluationMetaData, StabilityResult, StabilityStatus, StatisticsCounter};
1170
1171 #[test]
1172 fn status_table_renders_three_rows_without_header() {
1173 let stats = StatisticsCounter {
1174 num_evals: 10,
1175 num_sample_points: 10,
1176 sum_integrand_evaluation_time: Duration::from_micros(414),
1177 sum_evaluator_evaluation_time: Duration::from_micros(279),
1178 sum_parameterization_time: Duration::from_nanos(5_800),
1179 sum_event_time: Duration::ZERO,
1180 sum_integrator_overhead_time: Duration::from_micros(110),
1181 sum_total_evaluation_time: Duration::from_micros(462),
1182 sum_relative_instability_error: (0.0.into(), 0.0.into()),
1183 num_double_precision_evals: 10,
1184 num_quadruple_precision_evals: 0,
1185 num_arb_precision_evals: 0,
1186 num_nan_evals: 0,
1187 num_nan_or_unstable_evals: 0,
1188 sum_generated_event_count: 0,
1189 sum_accepted_event_count: 0,
1190 };
1191
1192 let rendered = stats.render_status_table();
1193 let lines = rendered.lines().collect::<Vec<_>>();
1194
1195 assert!(
1196 rendered.starts_with('╭') && rendered.ends_with('╯'),
1197 "{rendered}"
1198 );
1199 assert_eq!(lines.len(), 7, "{rendered}");
1200 assert!(rendered.contains("Integration statistics"), "{rendered}");
1201 assert!(lines[2].contains('┬'), "{rendered}");
1202 assert!(lines[6].contains('┴'), "{rendered}");
1203 assert_eq!(lines[3].matches('│').count(), 6, "{rendered}");
1204 assert_eq!(lines[4].matches('│').count(), 6, "{rendered}");
1205 assert_eq!(lines[5].matches('│').count(), 6, "{rendered}");
1206 assert!(rendered.contains("timing"), "{rendered}");
1207 assert!(rendered.contains("evals"), "{rendered}");
1208 assert!(rendered.contains("events"), "{rendered}");
1209 assert!(rendered.contains("nans+unstable :"), "{rendered}");
1210 assert!(rendered.contains("integrator :"), "{rendered}");
1211 }
1212
1213 #[test]
1214 fn statistics_snapshot_reports_selection_efficiency_from_event_counts() {
1215 let stats = StatisticsCounter {
1216 num_evals: 2,
1217 num_sample_points: 2,
1218 sum_integrand_evaluation_time: Duration::ZERO,
1219 sum_evaluator_evaluation_time: Duration::ZERO,
1220 sum_parameterization_time: Duration::ZERO,
1221 sum_event_time: Duration::ZERO,
1222 sum_integrator_overhead_time: Duration::ZERO,
1223 sum_total_evaluation_time: Duration::ZERO,
1224 sum_relative_instability_error: (0.0.into(), 0.0.into()),
1225 num_double_precision_evals: 2,
1226 num_quadruple_precision_evals: 0,
1227 num_arb_precision_evals: 0,
1228 num_nan_evals: 0,
1229 num_nan_or_unstable_evals: 0,
1230 sum_generated_event_count: 10,
1231 sum_accepted_event_count: 4,
1232 };
1233
1234 let snapshot = stats.snapshot();
1235
1236 assert_eq!(snapshot.generated_event_count, 10);
1237 assert_eq!(snapshot.accepted_event_count, 4);
1238 assert_eq!(snapshot.selection_efficiency_percentage, Some(40.0));
1239
1240 let rendered = stats.render_status_table();
1241 assert!(rendered.contains("sel. % : 40.0%"), "{rendered}");
1242 }
1243
1244 #[test]
1245 fn evaluation_metadata_renders_stability_status_with_sample_counts() {
1246 let metadata = EvaluationMetaData {
1247 stability_results: vec![
1248 StabilityResult {
1249 precision: Precision::Double,
1250 estimated_relative_accuracy: Some(1.0e-5.into()),
1251 status: StabilityStatus::Stable(3),
1252 total_time: Duration::from_micros(120),
1253 },
1254 StabilityResult {
1255 precision: Precision::Quad,
1256 estimated_relative_accuracy: None,
1257 status: StabilityStatus::Unknown,
1258 total_time: Duration::from_micros(240),
1259 },
1260 ],
1261 ..EvaluationMetaData::new_empty()
1262 };
1263
1264 let rendered = metadata.to_string();
1265 assert!(rendered.contains("Stable(3 samples)"), "{rendered}");
1266 assert!(rendered.contains("Unknown(1 sample)"), "{rendered}");
1267 assert!(rendered.contains("None"), "{rendered}");
1268 }
1269
1270 mod failing {
1271 use super::*;
1272
1273 #[test]
1274 fn zero_eval_statistics_snapshot_and_render_are_sanitized() {
1275 let stats = StatisticsCounter::new_empty();
1276 let snapshot = stats.snapshot();
1277
1278 assert_eq!(snapshot.num_evals, 0);
1279 assert_eq!(snapshot.average_total_time_seconds, 0.0);
1280 assert_eq!(snapshot.average_parameterization_time_seconds, 0.0);
1281 assert_eq!(snapshot.average_integrand_time_seconds, 0.0);
1282 assert_eq!(snapshot.average_evaluator_time_seconds, 0.0);
1283 assert_eq!(snapshot.average_observable_time_seconds, 0.0);
1284 assert_eq!(snapshot.average_integrator_time_seconds, 0.0);
1285 assert_eq!(snapshot.f64_percentage, 0.0);
1286 assert_eq!(snapshot.f128_percentage, 0.0);
1287 assert_eq!(snapshot.arb_percentage, 0.0);
1288 assert_eq!(snapshot.nan_percentage, 0.0);
1289 assert_eq!(snapshot.nan_or_unstable_percentage, 0.0);
1290 assert_eq!(snapshot.selection_efficiency_percentage, None);
1291
1292 let rendered = stats.render_status_table();
1293 assert!(rendered.contains("sel. % : N/A"), "{rendered}");
1294 assert!(rendered.contains("f64 : 0.00%"), "{rendered}");
1295 assert!(rendered.contains("nans+unstable : 0.0%"), "{rendered}");
1296 assert!(rendered.contains("integrator : 0.00 ns"), "{rendered}");
1297 }
1298 }
1299}