1use std::collections::BTreeMap;
2use std::fmt::Display;
3use std::time::Duration;
4
5use bincode::{Decode, Encode};
6use colored::Colorize;
7use serde::{Deserialize, Serialize};
8use spenso::algebra::complex::Complex;
9use tabled::{
10 Table, Tabled,
11 builder::Builder,
12 settings::{
13 Alignment, Modify, Panel, Style,
14 object::{Columns, Object, Rows},
15 style::{HorizontalLine, On, VerticalLine},
16 themes::BorderCorrection,
17 },
18};
19
20use crate::observables::{
21 AdditionalWeightKey, EventGroupList, GenericEventGroupList, HistogramAccumulatorState,
22 HistogramMedianPosition, HistogramSnapshot, ObservablePhase, ObservableSnapshotBundle,
23 ObservableValueTransform,
24 events::{format_complex_generic, format_optional_real_generic, format_real_generic},
25};
26use crate::{
27 integrands::process::{
28 sampling::context::SamplingProposalPolicies, sampling::reference::ReferenceMoments,
29 },
30 settings::runtime::{IntegrationStatisticsSnapshot, Precision},
31 utils::{
32 ArbPrec, F, FloatLike, duration_from_secs_f64_saturating, f128, format_evaluation_time,
33 newton_solver::RadialRootDiagnostics, normalize_tabled_separator_rows,
34 },
35};
36
37#[derive(Clone, Debug)]
38pub struct GraphEvaluationResult<T: FloatLike> {
39 pub integrand_result: Complex<F<T>>,
40 pub absolute_integrand_result: Option<Complex<F<T>>>,
43 pub(crate) channel_norm_sum: Option<F<T>>,
47 pub reference_moments: Option<ReferenceMoments<T>>,
48 pub event_groups: GenericEventGroupList<T>,
49 pub event_processing_time: Duration,
50 pub generated_event_count: usize,
51 pub accepted_event_count: usize,
52}
53
54impl<T: FloatLike> GraphEvaluationResult<T> {
55 pub fn zero(zero: F<T>) -> Self {
56 Self {
57 integrand_result: Complex::new_re(zero),
58 absolute_integrand_result: None,
59 channel_norm_sum: None,
60 reference_moments: None,
61 event_groups: GenericEventGroupList::default(),
62 event_processing_time: Duration::ZERO,
63 generated_event_count: 0,
64 accepted_event_count: 0,
65 }
66 }
67
68 pub fn merge_in_place(&mut self, mut other: Self) {
69 self.integrand_result += other.integrand_result;
70 if let Some(absolute) = other.absolute_integrand_result {
71 if let Some(current) = &mut self.absolute_integrand_result {
72 *current += absolute;
73 } else {
74 self.absolute_integrand_result = Some(absolute);
75 }
76 }
77 if let Some(norm) = other.channel_norm_sum {
78 if let Some(current) = &mut self.channel_norm_sum {
79 *current += norm;
80 } else {
81 self.channel_norm_sum = Some(norm);
82 }
83 }
84 if let Some(moments) = other.reference_moments {
85 if let Some(current) = &mut self.reference_moments {
86 current.merge_in_place(moments);
87 } else {
88 self.reference_moments = Some(moments);
89 }
90 }
91 self.event_groups.append(&mut other.event_groups);
92 self.event_processing_time += other.event_processing_time;
93 self.generated_event_count += other.generated_event_count;
94 self.accepted_event_count += other.accepted_event_count;
95 }
96
97 pub(crate) fn apply_sampling_factor(&mut self, factor: F<T>) {
101 if let Some(absolute) = &mut self.absolute_integrand_result {
102 *absolute *= factor.abs();
103 }
104 if let Some(norm) = &mut self.channel_norm_sum {
105 *norm *= factor.abs();
106 }
107 if let Some(moments) = &mut self.reference_moments {
108 moments.rescale(&factor);
109 }
110 let factor = Complex::new_re(factor);
111 self.integrand_result *= &factor;
112 crate::integrands::process::apply_full_event_multiplicative_factor_precise(
113 &mut self.event_groups,
114 &factor,
115 );
116 }
117}
118
119#[derive(Clone, Serialize, Debug)]
121pub struct EvaluationResult {
122 pub integrand_result: Complex<F<f64>>,
127 #[serde(skip_serializing_if = "Option::is_none")]
130 pub absolute_integrand_result: Option<Complex<F<f64>>>,
131 pub parameterization_jacobian: Option<F<f64>>,
132 pub integrator_weight: F<f64>,
134 pub event_groups: EventGroupList,
135 pub evaluation_metadata: EvaluationMetaData,
136}
137
138#[derive(Clone, Serialize, Debug)]
139pub struct EvaluationResultOutput {
140 pub integrand_result: Complex<F<f64>>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub absolute_integrand_result: Option<Complex<F<f64>>>,
143 pub parameterization_jacobian: Option<F<f64>>,
144 pub integrator_weight: F<f64>,
145 pub event_groups: EventGroupList,
146 pub evaluation_metadata: Option<EvaluationMetaData>,
147}
148
149#[derive(Clone, Debug)]
150pub struct GenericEvaluationResult<T: FloatLike> {
151 pub(crate) reference_moments: Option<ReferenceMoments<T>>,
154 pub integrand_result: Complex<F<T>>,
155 pub absolute_integrand_result: Option<Complex<F<T>>>,
156 pub parameterization_jacobian: Option<F<T>>,
157 pub integrator_weight: F<T>,
158 pub event_groups: GenericEventGroupList<T>,
159 pub evaluation_metadata: EvaluationMetaData,
160}
161
162#[derive(Clone, Debug)]
163pub struct GenericEvaluationResultOutput<T: FloatLike> {
164 pub integrand_result: Complex<F<T>>,
165 pub absolute_integrand_result: Option<Complex<F<T>>>,
166 pub parameterization_jacobian: Option<F<T>>,
167 pub integrator_weight: F<T>,
168 pub event_groups: GenericEventGroupList<T>,
169 pub evaluation_metadata: Option<EvaluationMetaData>,
170}
171
172impl<T: FloatLike> GenericEvaluationResult<T> {
173 pub(crate) fn try_into_f64(self) -> eyre::Result<EvaluationResult> {
176 use eyre::WrapErr;
177
178 let one = self.integrator_weight.one();
179 let remaining_factor =
180 self.parameterization_jacobian.as_ref().unwrap_or(&one) * &self.integrator_weight;
181 eyre::ensure!(
182 remaining_factor.0.is_finite(),
183 "nonfinite remaining native integration factor {remaining_factor}"
184 );
185 let check = |value: &F<T>, remaining: Option<&F<T>>| -> eyre::Result<()> {
190 let reported = value.into_f64();
191 if value.0.is_finite() {
192 if !reported.is_finite() {
193 return Err(eyre::eyre!(
194 "native value {value} overflows the f64 integration/reporting boundary"
195 ));
196 }
197 if reported == 0.0 && value != &value.zero() {
198 let complete = remaining.map(|factor| value * factor);
199 if complete.as_ref().is_none_or(|weighted| {
200 !weighted.0.is_finite()
206 || weighted.abs() >= F::<T>::from_f64(f64::MIN_POSITIVE)
207 }) {
208 return Err(eyre::eyre!(
209 "native value {value} rounds to zero before remaining factor {}; complete contribution {} cannot be preserved at the f64 integration/reporting boundary",
210 remaining.map_or_else(
211 || "separately stored factorized component".to_owned(),
212 ToString::to_string
213 ),
214 complete
215 .as_ref()
216 .map_or_else(|| "not combined".to_owned(), ToString::to_string)
217 ));
218 }
219 }
220 }
221 Ok(())
222 };
223 check(&self.integrator_weight, None).wrap_err("integrator_weight")?;
226 if let Some(jacobian) = &self.parameterization_jacobian {
227 check(jacobian, None).wrap_err("parameterization_jacobian")?;
228 }
229 for (field, weight) in std::iter::once(("integrand_result", &self.integrand_result)).chain(
230 self.absolute_integrand_result
231 .iter()
232 .map(|weight| ("absolute_integrand_result", weight)),
233 ) {
234 for (component, value) in [("re", &weight.re), ("im", &weight.im)] {
235 check(value, Some(&remaining_factor))
236 .wrap_err_with(|| format!("{field}.{component}"))?;
237 }
238 }
239 for (group_index, group) in self.event_groups.iter().enumerate() {
240 for (event_index, event) in group.iter().enumerate() {
241 for (component, value) in [("re", &event.weight.re), ("im", &event.weight.im)] {
243 check(value, Some(&one)).wrap_err_with(|| {
244 format!("event_groups[{group_index}][{event_index}].weight.{component}")
245 })?;
246 }
247 let component_factor = event
252 .additional_weights
253 .weights
254 .get(&AdditionalWeightKey::FullMultiplicativeFactor)
255 .filter(|factor| factor.re.0.is_finite() && factor.im.0.is_finite())
256 .map(|factor| {
257 let real = factor.re.abs();
258 let imag = factor.im.abs();
259 if real > imag { real } else { imag }
260 });
261 for (key, weight) in &event.additional_weights.weights {
262 let remaining = if *key == AdditionalWeightKey::FullMultiplicativeFactor {
263 None
264 } else {
265 component_factor.as_ref()
266 };
267 for (component, value) in [("re", &weight.re), ("im", &weight.im)] {
268 check(value, remaining).wrap_err_with(|| {
269 format!("event_groups[{group_index}][{event_index}].additional_weights[{key:?}].{component}")
270 })?;
271 }
272 }
273 }
274 }
275 Ok(EvaluationResult {
276 integrand_result: Complex::new(
277 self.integrand_result.re.into_ff64(),
278 self.integrand_result.im.into_ff64(),
279 ),
280 absolute_integrand_result: self
281 .absolute_integrand_result
282 .map(|value| Complex::new(value.re.into_ff64(), value.im.into_ff64())),
283 parameterization_jacobian: self
284 .parameterization_jacobian
285 .map(|value| value.into_ff64()),
286 integrator_weight: self.integrator_weight.into_ff64(),
287 event_groups: self.event_groups.to_f64(),
288 evaluation_metadata: self.evaluation_metadata,
289 })
290 }
291
292 pub fn into_output(self, minimal_output: bool) -> GenericEvaluationResultOutput<T> {
293 GenericEvaluationResultOutput {
294 integrand_result: self.integrand_result,
295 absolute_integrand_result: self.absolute_integrand_result,
296 parameterization_jacobian: self.parameterization_jacobian,
297 integrator_weight: self.integrator_weight,
298 event_groups: self.event_groups,
299 evaluation_metadata: (!minimal_output).then_some(self.evaluation_metadata),
300 }
301 }
302}
303
304#[derive(Clone, Debug)]
305pub enum PreciseEvaluationResultOutput {
306 Double(GenericEvaluationResultOutput<f64>),
307 Quad(GenericEvaluationResultOutput<f128>),
308 Arb(GenericEvaluationResultOutput<ArbPrec>),
309}
310
311#[derive(Clone, Debug)]
312pub enum PreciseEvaluationResult {
313 Double(GenericEvaluationResult<f64>),
314 Quad(GenericEvaluationResult<f128>),
315 Arb(GenericEvaluationResult<ArbPrec>),
316}
317
318impl PreciseEvaluationResult {
319 pub(crate) fn try_into_f64(self) -> eyre::Result<EvaluationResult> {
320 match self {
321 Self::Double(result) => result.try_into_f64(),
322 Self::Quad(result) => result.try_into_f64(),
323 Self::Arb(result) => result.try_into_f64(),
324 }
325 }
326
327 pub fn into_output(self, minimal_output: bool) -> PreciseEvaluationResultOutput {
328 match self {
329 PreciseEvaluationResult::Double(result) => {
330 PreciseEvaluationResultOutput::Double(result.into_output(minimal_output))
331 }
332 PreciseEvaluationResult::Quad(result) => {
333 PreciseEvaluationResultOutput::Quad(result.into_output(minimal_output))
334 }
335 PreciseEvaluationResult::Arb(result) => {
336 PreciseEvaluationResultOutput::Arb(result.into_output(minimal_output))
337 }
338 }
339 }
340}
341
342impl PreciseEvaluationResultOutput {
343 pub fn precision(&self) -> Precision {
344 match self {
345 PreciseEvaluationResultOutput::Double(_) => Precision::Double,
346 PreciseEvaluationResultOutput::Quad(_) => Precision::Quad,
347 PreciseEvaluationResultOutput::Arb(_) => Precision::Arb,
348 }
349 }
350
351 pub fn evaluation_metadata(&self) -> Option<&EvaluationMetaData> {
352 match self {
353 PreciseEvaluationResultOutput::Double(result) => result.evaluation_metadata.as_ref(),
354 PreciseEvaluationResultOutput::Quad(result) => result.evaluation_metadata.as_ref(),
355 PreciseEvaluationResultOutput::Arb(result) => result.evaluation_metadata.as_ref(),
356 }
357 }
358}
359
360impl EvaluationResult {
361 pub fn zero() -> Self {
362 Self {
363 integrand_result: Complex::new_zero(),
364 absolute_integrand_result: None,
365 parameterization_jacobian: None,
366 integrator_weight: F(0.0),
367 event_groups: EventGroupList::default(),
368 evaluation_metadata: EvaluationMetaData::new_empty(),
369 }
370 }
371
372 pub fn into_output(self, minimal_output: bool) -> EvaluationResultOutput {
373 EvaluationResultOutput {
374 integrand_result: self.integrand_result,
375 absolute_integrand_result: self.absolute_integrand_result,
376 parameterization_jacobian: self.parameterization_jacobian,
377 integrator_weight: self.integrator_weight,
378 event_groups: self.event_groups,
379 evaluation_metadata: (!minimal_output).then_some(self.evaluation_metadata),
380 }
381 }
382}
383
384#[allow(clippy::too_many_arguments)]
385fn fmt_evaluation_result_output<T: FloatLike>(
386 f: &mut std::fmt::Formatter<'_>,
387 precision: Option<Precision>,
388 integrand_result: &Complex<F<T>>,
389 absolute_integrand_result: Option<&Complex<F<T>>>,
390 parameterization_jacobian: Option<&F<T>>,
391 integrator_weight: &F<T>,
392 event_groups: &GenericEventGroupList<T>,
393 evaluation_metadata: Option<&EvaluationMetaData>,
394) -> std::fmt::Result {
395 let mut summary_rows = Vec::with_capacity(5);
396 if let Some(precision) = precision {
397 summary_rows.push(EvaluationSummaryRow {
398 field: "precision".to_string(),
399 value: precision.to_string(),
400 });
401 }
402 summary_rows.extend([
403 EvaluationSummaryRow {
404 field: "integrand result".to_string(),
405 value: format_complex_generic(integrand_result),
406 },
407 EvaluationSummaryRow {
408 field: "parameterization jacobian".to_string(),
409 value: format_optional_real_generic(parameterization_jacobian),
410 },
411 EvaluationSummaryRow {
412 field: "integrator weight".to_string(),
413 value: format_real_generic(integrator_weight),
414 },
415 EvaluationSummaryRow {
416 field: "event groups".to_string(),
417 value: format_count(event_groups.len()),
418 },
419 ]);
420
421 if let Some(absolute) = absolute_integrand_result {
422 summary_rows.push(EvaluationSummaryRow {
423 field: "absolute physical contribution (|Re|, |Im|)".to_string(),
424 value: format!(
425 "({}, {})",
426 format_real_generic(&absolute.re),
427 format_real_generic(&absolute.im)
428 ),
429 });
430 }
431 writeln!(f, "{}", "Evaluation result".bold().bright_green())?;
432 writeln!(f, "{}", Table::new(summary_rows).with(Style::rounded()))?;
433 if let Some(metadata) = evaluation_metadata {
434 writeln!(f)?;
435 write!(f, "{metadata}")?;
436 }
437
438 if !event_groups.is_empty() {
439 writeln!(f)?;
440 writeln!(
441 f,
442 "{}",
443 format!(
444 "Generated {} event group(s)",
445 format_count(event_groups.len())
446 )
447 .bold()
448 .bright_cyan()
449 )?;
450 write!(f, "{event_groups}")?;
451 }
452
453 Ok(())
454}
455
456fn fmt_sample_result<D: Display>(
457 f: &mut std::fmt::Formatter<'_>,
458 evaluation: &D,
459 observables: &ObservableSnapshotBundle,
460) -> std::fmt::Result {
461 write!(f, "{evaluation}")?;
462
463 if let Some(observables_table) = summarize_observables(observables) {
464 writeln!(f)?;
465 writeln!(f)?;
466 writeln!(f, "{}", "Observable snapshots".bold().bright_magenta())?;
467 write!(f, "{observables_table}")?;
468 }
469
470 Ok(())
471}
472
473fn fmt_batch_result<D: Display>(
474 f: &mut std::fmt::Formatter<'_>,
475 samples: &[D],
476 observables: &ObservableSnapshotBundle,
477 numerical_stability: Option<&NumericalStabilityHistograms>,
478) -> std::fmt::Result {
479 let summary_rows = [EvaluationSummaryRow {
480 field: "samples".to_string(),
481 value: format_count(samples.len()),
482 }];
483
484 writeln!(f, "{}", "Batch evaluation result".bold().bright_green())?;
485 writeln!(f, "{}", Table::new(summary_rows).with(Style::rounded()))?;
486
487 if let Some(observables_table) = summarize_observables(observables) {
488 writeln!(f)?;
489 writeln!(f, "{}", "Observable snapshots".bold().bright_magenta())?;
490 writeln!(f, "{observables_table}")?;
491 }
492
493 if let Some(stability_table) = numerical_stability.and_then(summarize_numerical_stability) {
494 writeln!(f)?;
495 writeln!(f, "{}", "Numerical stability".bold().bright_magenta())?;
496 writeln!(f, "{stability_table}")?;
497 }
498
499 for (sample_index, sample) in samples.iter().enumerate() {
500 writeln!(f)?;
501 if sample_index > 0 {
502 writeln!(f)?;
503 }
504 writeln!(
505 f,
506 "{}",
507 format!("Sample {sample_index}").bold().bright_cyan()
508 )?;
509 write!(f, "{sample}")?;
510 }
511
512 Ok(())
513}
514
515impl Display for EvaluationResultOutput {
516 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517 fmt_evaluation_result_output(
518 f,
519 None,
520 &self.integrand_result,
521 self.absolute_integrand_result.as_ref(),
522 self.parameterization_jacobian.as_ref(),
523 &self.integrator_weight,
524 &self.event_groups,
525 self.evaluation_metadata.as_ref(),
526 )
527 }
528}
529
530impl<T: FloatLike> Display for GenericEvaluationResultOutput<T> {
531 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
532 fmt_evaluation_result_output(
533 f,
534 None,
535 &self.integrand_result,
536 self.absolute_integrand_result.as_ref(),
537 self.parameterization_jacobian.as_ref(),
538 &self.integrator_weight,
539 &self.event_groups,
540 self.evaluation_metadata.as_ref(),
541 )
542 }
543}
544
545impl Display for PreciseEvaluationResultOutput {
546 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547 match self {
548 PreciseEvaluationResultOutput::Double(result) => fmt_evaluation_result_output(
549 f,
550 Some(Precision::Double),
551 &result.integrand_result,
552 result.absolute_integrand_result.as_ref(),
553 result.parameterization_jacobian.as_ref(),
554 &result.integrator_weight,
555 &result.event_groups,
556 result.evaluation_metadata.as_ref(),
557 ),
558 PreciseEvaluationResultOutput::Quad(result) => fmt_evaluation_result_output(
559 f,
560 Some(Precision::Quad),
561 &result.integrand_result,
562 result.absolute_integrand_result.as_ref(),
563 result.parameterization_jacobian.as_ref(),
564 &result.integrator_weight,
565 &result.event_groups,
566 result.evaluation_metadata.as_ref(),
567 ),
568 PreciseEvaluationResultOutput::Arb(result) => fmt_evaluation_result_output(
569 f,
570 Some(Precision::Arb),
571 &result.integrand_result,
572 result.absolute_integrand_result.as_ref(),
573 result.parameterization_jacobian.as_ref(),
574 &result.integrator_weight,
575 &result.event_groups,
576 result.evaluation_metadata.as_ref(),
577 ),
578 }
579 }
580}
581
582impl Display for SampleEvaluationResult {
583 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584 write!(f, "{}", self.evaluation)
585 }
586}
587
588#[derive(Clone, Serialize, Debug)]
589pub struct SampleEvaluationResult {
590 pub evaluation: EvaluationResultOutput,
591}
592
593#[derive(Clone, Debug)]
594pub struct PreciseSampleEvaluationResult {
595 pub evaluation: PreciseEvaluationResultOutput,
596}
597
598#[derive(Clone, Serialize, Debug)]
599pub struct SingleSampleEvaluationResult {
600 pub sample: SampleEvaluationResult,
602 pub observables: ObservableSnapshotBundle,
606}
607
608#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Hash)]
609pub enum NumericalStabilityLevel {
610 Double,
611 Quad,
612 ArbPrec,
613}
614
615impl NumericalStabilityLevel {
616 pub fn all() -> [Self; 3] {
617 [Self::Double, Self::Quad, Self::ArbPrec]
618 }
619
620 pub fn key(self) -> &'static str {
621 match self {
622 Self::Double => "Double",
623 Self::Quad => "Quad",
624 Self::ArbPrec => "ArbPrec",
625 }
626 }
627
628 pub fn short_label(self) -> &'static str {
629 match self {
630 Self::Double => "f64",
631 Self::Quad => "f128",
632 Self::ArbPrec => "arb",
633 }
634 }
635
636 fn from_precision(precision: Precision) -> Self {
637 match precision {
638 Precision::Double => Self::Double,
639 Precision::Quad => Self::Quad,
640 Precision::Arb => Self::ArbPrec,
641 }
642 }
643
644 fn histogram_range(self) -> (f64, f64, usize) {
645 match self {
646 Self::Double => (-1.0, 17.0, 72),
647 Self::Quad => (-1.0, 34.0, 140),
648 Self::ArbPrec => (-1.0, 1000.0, 100),
649 }
650 }
651}
652
653impl Display for NumericalStabilityLevel {
654 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
655 f.write_str(self.key())
656 }
657}
658
659#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
660pub struct NumericalStabilityHistograms {
661 pub histograms: BTreeMap<String, HistogramSnapshot>,
662}
663
664impl NumericalStabilityHistograms {
665 pub fn as_observable_snapshot_bundle(&self) -> ObservableSnapshotBundle {
666 ObservableSnapshotBundle {
667 histograms: self.histograms.clone(),
668 }
669 }
670
671 pub fn processed_sample_count(&self, level: NumericalStabilityLevel) -> usize {
672 self.histograms
673 .get(level.key())
674 .map(|histogram| histogram.sample_count)
675 .unwrap_or_default()
676 }
677
678 pub fn median(&self, level: NumericalStabilityLevel) -> Option<NumericalStabilityMedian> {
679 let histogram = self.histograms.get(level.key())?;
680 NumericalStabilityMedian::from_histogram(histogram)
681 }
682}
683
684#[derive(Clone, Copy, Debug, PartialEq)]
685pub enum NumericalStabilityMedian {
686 Underflow { log10_lower_bound: f64 },
687 InRange { log10_relative_accuracy: f64 },
688 Overflow { log10_upper_bound: f64 },
689}
690
691impl NumericalStabilityMedian {
692 fn from_histogram(histogram: &HistogramSnapshot) -> Option<Self> {
693 match histogram.approximate_weighted_median_position()? {
694 HistogramMedianPosition::Underflow => Some(Self::Underflow {
695 log10_lower_bound: -histogram.x_min?,
696 }),
697 HistogramMedianPosition::InRange(log_accuracy) => Some(Self::InRange {
698 log10_relative_accuracy: -log_accuracy,
699 }),
700 HistogramMedianPosition::Overflow => Some(Self::Overflow {
701 log10_upper_bound: -histogram.x_max?,
702 }),
703 }
704 }
705
706 pub fn formatted_relative_accuracy(self) -> String {
707 match self {
708 Self::Underflow { log10_lower_bound } => {
709 format!(">{}", format_stability_scientific(log10_lower_bound))
710 }
711 Self::InRange {
712 log10_relative_accuracy,
713 } => format_stability_scientific(log10_relative_accuracy),
714 Self::Overflow { log10_upper_bound } => {
715 format!("<={}", format_stability_scientific(log10_upper_bound))
716 }
717 }
718 }
719}
720
721fn format_stability_scientific(log10_value: f64) -> String {
722 if !log10_value.is_finite() {
723 return "N/A".to_string();
724 }
725 let mut exponent = log10_value.floor();
727 let mut mantissa = (10.0_f64.powf(log10_value - exponent) * 10.0).round() / 10.0;
728 if mantissa >= 10.0 {
729 mantissa = 1.0;
730 exponent += 1.0;
731 }
732 format!("{mantissa:.1}e{exponent:.0}")
733}
734
735#[derive(Clone, Debug, Serialize, Deserialize)]
736pub(crate) struct NumericalStabilityHistogramAccumulator {
737 histograms: BTreeMap<String, HistogramAccumulatorState>,
738}
739
740impl Default for NumericalStabilityHistogramAccumulator {
741 fn default() -> Self {
742 Self::new_empty()
743 }
744}
745
746impl NumericalStabilityHistogramAccumulator {
747 fn new_empty() -> Self {
748 let histograms = NumericalStabilityLevel::all()
749 .into_iter()
750 .map(|level| {
751 let (x_min, x_max, n_bins) = level.histogram_range();
752 (
753 level.key().to_string(),
754 HistogramAccumulatorState::continuous(
755 format!("Numerical stability {}", level.key()),
756 "-log10(relative accuracy)".to_string(),
757 ObservablePhase::Real,
758 ObservableValueTransform::Identity,
759 x_min,
760 x_max,
761 false,
762 false,
763 n_bins,
764 ),
765 )
766 })
767 .collect();
768 Self { histograms }
769 }
770
771 fn fill_from_stability_results(&mut self, stability_results: &[StabilityResult]) {
772 for result in stability_results {
773 let level = NumericalStabilityLevel::from_precision(result.precision);
774 let Some(histogram) = self.histograms.get_mut(level.key()) else {
775 continue;
776 };
777 let value = match (
778 result.estimated_decimal_digits,
779 result.estimated_relative_accuracy,
780 ) {
781 (Some(digits), _) if digits.0.is_finite() => digits.0,
782 (None, Some(accuracy)) if accuracy.0 == 0.0 => {
783 let (_, x_max, _) = level.histogram_range();
784 x_max + 1.0
785 }
786 _ => continue,
787 };
788 histogram
789 .fill_continuous_sample(&[(value, 1.0)])
790 .expect("numerical stability histograms are fixed continuous histograms");
791 }
792 }
793
794 fn merge_in_place(&mut self, other: &Self) {
795 for (name, histogram) in &mut self.histograms {
796 let mut other_histogram = other
797 .histograms
798 .get(name)
799 .unwrap_or_else(|| panic!("missing numerical stability histogram '{name}'"))
800 .clone();
801 histogram
802 .merge_in_place(&mut other_histogram)
803 .expect("fixed numerical stability histograms must be compatible");
804 }
805 }
806
807 fn merged(&self, other: &Self) -> Self {
808 let mut merged = self.clone();
809 merged.merge_in_place(other);
810 merged
811 }
812
813 pub(crate) fn snapshot(&self) -> NumericalStabilityHistograms {
814 NumericalStabilityHistograms {
815 histograms: self
816 .histograms
817 .iter()
818 .map(|(name, histogram)| (name.clone(), histogram.snapshot()))
819 .collect(),
820 }
821 }
822}
823
824#[derive(Clone, Serialize, Debug)]
825pub struct BatchSampleEvaluationResult {
826 pub samples: Vec<SampleEvaluationResult>,
828 pub observables: ObservableSnapshotBundle,
832 pub numerical_stability: Option<NumericalStabilityHistograms>,
834}
835
836#[derive(Clone, Debug)]
837pub struct RawBatchEvaluationResult {
838 pub samples: Vec<EvaluationResult>,
839 pub statistics: StatisticsCounter,
840}
841
842#[derive(Clone, Debug)]
843pub struct PreciseSingleSampleEvaluationResult {
844 pub sample: PreciseSampleEvaluationResult,
846 pub observables: ObservableSnapshotBundle,
848}
849
850#[derive(Clone, Debug)]
851pub struct PreciseBatchSampleEvaluationResult {
852 pub samples: Vec<PreciseSampleEvaluationResult>,
854 pub observables: ObservableSnapshotBundle,
856}
857
858#[derive(Clone, Debug)]
859pub struct RawPreciseBatchEvaluationResult {
860 pub samples: Vec<PreciseEvaluationResult>,
861}
862
863impl Display for PreciseSampleEvaluationResult {
864 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
865 write!(f, "{}", self.evaluation)
866 }
867}
868
869impl Display for SingleSampleEvaluationResult {
870 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
871 fmt_sample_result(f, &self.sample.evaluation, &self.observables)
872 }
873}
874
875impl Display for BatchSampleEvaluationResult {
876 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
877 fmt_batch_result(
878 f,
879 &self.samples,
880 &self.observables,
881 self.numerical_stability.as_ref(),
882 )
883 }
884}
885
886impl Display for PreciseSingleSampleEvaluationResult {
887 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
888 fmt_sample_result(f, &self.sample.evaluation, &self.observables)
889 }
890}
891
892impl Display for PreciseBatchSampleEvaluationResult {
893 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
894 fmt_batch_result(f, &self.samples, &self.observables, None)
895 }
896}
897
898#[derive(Tabled)]
899struct EvaluationSummaryRow {
900 field: String,
901 value: String,
902}
903
904#[derive(Tabled)]
905struct HistogramSummaryRow {
906 name: String,
907 bins: usize,
908 #[tabled(rename = "in-range")]
909 in_range_entries: String,
910 phase: String,
911 range: String,
912 underflow: String,
913 overflow: String,
914}
915
916#[derive(Tabled)]
917struct StabilitySummaryRow {
918 level: String,
919 relative_accuracy: String,
920 time: String,
921 status: String,
922}
923
924#[derive(Tabled)]
925struct NumericalStabilitySummaryRow {
926 level: String,
927 median: String,
928 samples: String,
929 range: String,
930 overflow: String,
931}
932
933fn format_duration(duration: Duration) -> String {
934 format_evaluation_time(duration)
935}
936
937fn format_count(value: usize) -> String {
938 if value < 1_000 {
939 return value.to_string();
940 }
941
942 let value = value as f64;
943 for (scale, suffix) in [
944 (1_000_000_000_f64, "B"),
945 (1_000_000_f64, "M"),
946 (1_000_f64, "K"),
947 ] {
948 if value >= scale {
949 let scaled = value / scale;
950 let precision = if scaled >= 100.0 {
951 0
952 } else if scaled >= 10.0 {
953 1
954 } else {
955 2
956 };
957 return format!("{scaled:.precision$}{suffix}");
958 }
959 }
960
961 value.round().to_string()
962}
963
964fn format_percentage(value: f64, significant_digits: usize) -> String {
965 if !value.is_finite() {
966 return "None".red().to_string();
967 }
968
969 if value == 0.0 {
970 let decimals = significant_digits.saturating_sub(1);
971 return format!("{:.*}%", decimals, 0.0);
972 }
973
974 let abs_value = value.abs();
975 let exponent = abs_value.log10().floor() as i32;
976 if exponent < -2 || exponent >= significant_digits as i32 {
977 return format!("{:.*e}%", significant_digits.saturating_sub(1), value);
978 }
979
980 let decimals = (significant_digits as i32 - exponent - 1).max(0) as usize;
981 format!("{value:.decimals$}%")
982}
983
984fn format_status_header(label: &str) -> String {
985 format!("{label:<7}").blue().bold().to_string()
986}
987
988fn format_status_key(label: &str) -> String {
989 format!("{label} : ")
990}
991
992fn pad_status_value(value: impl Display, width: usize) -> String {
993 format!("{value:<width$}")
994}
995
996fn status_group_separator() -> VerticalLine<On, On, ()> {
997 VerticalLine::new('│').top('┬').bottom('┴')
998}
999
1000fn summarize_observables(observables: &ObservableSnapshotBundle) -> Option<String> {
1001 if observables.histograms.is_empty() {
1002 return None;
1003 }
1004
1005 let rows = observables
1006 .histograms
1007 .iter()
1008 .map(|(name, histogram)| HistogramSummaryRow {
1009 name: name.clone(),
1010 bins: histogram.bins.len(),
1011 in_range_entries: format_count(histogram.statistics.in_range_entry_count),
1012 phase: format!("{:?}", histogram.phase).to_lowercase(),
1013 range: match histogram.kind {
1014 crate::observables::HistogramSnapshotKind::Continuous => format!(
1015 "[{:+.16e}, {:+.16e}]",
1016 histogram.x_min.unwrap_or_default(),
1017 histogram.x_max.unwrap_or_default()
1018 ),
1019 crate::observables::HistogramSnapshotKind::Discrete => {
1020 let min = histogram.discrete_min_bin_id.unwrap_or_default();
1021 let max = min + histogram.bins.len() as isize - 1;
1022 format!("[{}, {}]", min, max)
1023 }
1024 },
1025 underflow: format_count(histogram.underflow_bin.entry_count),
1026 overflow: format_count(histogram.overflow_bin.entry_count),
1027 })
1028 .collect::<Vec<_>>();
1029
1030 Some(Table::new(rows).with(Style::rounded()).to_string())
1031}
1032
1033fn summarize_numerical_stability(stability: &NumericalStabilityHistograms) -> Option<String> {
1034 if stability.histograms.is_empty() {
1035 return None;
1036 }
1037
1038 let rows = NumericalStabilityLevel::all()
1039 .into_iter()
1040 .filter_map(|level| {
1041 let histogram = stability.histograms.get(level.key())?;
1042 Some(NumericalStabilitySummaryRow {
1043 level: level.key().to_string(),
1044 median: stability
1045 .median(level)
1046 .map(NumericalStabilityMedian::formatted_relative_accuracy)
1047 .unwrap_or_else(|| "N/A".to_string()),
1048 samples: format_count(histogram.sample_count),
1049 range: format!(
1050 "[{:+.0}, {:+.0}]",
1051 histogram.x_min.unwrap_or_default(),
1052 histogram.x_max.unwrap_or_default()
1053 ),
1054 overflow: format_count(histogram.overflow_bin.entry_count),
1055 })
1056 })
1057 .collect::<Vec<_>>();
1058
1059 Some(Table::new(rows).with(Style::rounded()).to_string())
1060}
1061
1062#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
1064pub enum StabilityFailureReason {
1065 ErrorThreshold,
1066 ZeroError,
1067 WeightThreshold,
1068}
1069
1070#[derive(Clone, Debug, Serialize)]
1071pub struct RotatedEvaluation {
1072 pub rotation: String,
1073 pub result: Complex<F<f64>>,
1074}
1075
1076#[derive(Clone, Debug, Serialize)]
1077pub struct StabilityResult {
1078 pub precision: Precision,
1079 pub estimated_relative_accuracy: Option<F<f64>>,
1080 pub estimated_decimal_digits: Option<F<f64>>,
1083 pub status: StabilityStatus,
1084 pub total_time: Duration,
1085}
1086
1087#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
1088pub enum StabilityStatus {
1089 Unknown,
1090 Stable(usize),
1091 Unstable(usize),
1092}
1093
1094impl StabilityStatus {
1095 pub fn from_sample_count(sample_count: usize, accepted_as_stable: bool) -> Self {
1096 if sample_count <= 1 {
1097 Self::Unknown
1098 } else if accepted_as_stable {
1099 Self::Stable(sample_count)
1100 } else {
1101 Self::Unstable(sample_count)
1102 }
1103 }
1104
1105 pub fn sample_count(&self) -> usize {
1106 match self {
1107 Self::Unknown => 1,
1108 Self::Stable(sample_count) | Self::Unstable(sample_count) => *sample_count,
1109 }
1110 }
1111
1112 fn styled_label(&self) -> String {
1113 let label = self.to_string();
1114 match self {
1115 Self::Unknown => label.yellow().to_string(),
1116 Self::Stable(_) => label.green().to_string(),
1117 Self::Unstable(_) => label.red().to_string(),
1118 }
1119 }
1120}
1121
1122impl Display for StabilityStatus {
1123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1124 match self {
1125 Self::Unknown => write!(f, "Unknown(1 sample)"),
1126 Self::Stable(sample_count) => write!(f, "Stable({sample_count} samples)"),
1127 Self::Unstable(sample_count) => write!(f, "Unstable({sample_count} samples)"),
1128 }
1129 }
1130}
1131
1132#[derive(Clone, Debug, Serialize)]
1133pub struct LoopMomentaEscalationMetrics {
1134 pub sum_norm: f64,
1135 pub threshold: f64,
1136}
1137
1138#[derive(Clone, Serialize, Debug)]
1140pub struct EvaluationMetaData {
1141 pub total_timing: Duration,
1142 pub integrand_evaluation_time: Duration,
1146 pub canonical_physical_preparation_time: Duration,
1150 pub evaluator_evaluation_time: Duration,
1153 pub parameterization_time: Duration,
1157 pub canonical_sampling_preparation_time: Duration,
1161 pub event_processing_time: Duration,
1162 pub generated_event_count: usize,
1163 pub accepted_event_count: usize,
1164 pub relative_instability_error: Complex<F<f64>>,
1165 pub is_nan: bool,
1166 pub loop_momenta_escalation: Option<LoopMomentaEscalationMetrics>,
1167 pub stability_results: Vec<StabilityResult>,
1168 #[serde(skip_serializing_if = "Option::is_none")]
1169 pub(crate) threshold_counterterm_error: Option<String>,
1170 #[serde(skip)]
1171 pub(crate) radial_root_diagnostics: RadialRootDiagnostics,
1172 #[serde(skip)]
1173 pub(crate) sampling_proposal_policies: SamplingProposalPolicies,
1174}
1175
1176impl Display for EvaluationMetaData {
1177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1178 let rows = vec![
1179 EvaluationSummaryRow {
1180 field: "nan".to_string(),
1181 value: self.is_nan.to_string(),
1182 },
1183 EvaluationSummaryRow {
1184 field: "generated events".to_string(),
1185 value: format_count(self.generated_event_count),
1186 },
1187 EvaluationSummaryRow {
1188 field: "accepted events".to_string(),
1189 value: format_count(self.accepted_event_count),
1190 },
1191 EvaluationSummaryRow {
1192 field: "parameterization time".to_string(),
1193 value: format_duration(self.parameterization_time),
1194 },
1195 EvaluationSummaryRow {
1196 field: "canonical sampling (included)".to_string(),
1197 value: format_duration(self.canonical_sampling_preparation_time),
1198 },
1199 EvaluationSummaryRow {
1200 field: "integrand evaluation time".to_string(),
1201 value: format_duration(self.integrand_evaluation_time),
1202 },
1203 EvaluationSummaryRow {
1204 field: "canonical physical (included)".to_string(),
1205 value: format_duration(self.canonical_physical_preparation_time),
1206 },
1207 EvaluationSummaryRow {
1208 field: "evaluator evaluation time".to_string(),
1209 value: format_duration(self.evaluator_evaluation_time),
1210 },
1211 EvaluationSummaryRow {
1212 field: "event processing time".to_string(),
1213 value: format_duration(self.event_processing_time),
1214 },
1215 EvaluationSummaryRow {
1216 field: "total evaluation time".to_string(),
1217 value: format_duration(self.total_timing),
1218 },
1219 ];
1220 writeln!(f, "{}", "Evaluation metadata".bold().bright_yellow())?;
1221 writeln!(f, "{}", Table::new(rows).with(Style::rounded()))?;
1222
1223 if !self.stability_results.is_empty() {
1224 let stability_rows = self
1225 .stability_results
1226 .iter()
1227 .map(|result| StabilitySummaryRow {
1228 level: result.precision.to_string(),
1229 relative_accuracy: format_optional_real_generic(
1230 result.estimated_relative_accuracy.as_ref(),
1231 ),
1232 time: format_duration(result.total_time),
1233 status: result.status.styled_label(),
1234 })
1235 .collect::<Vec<_>>();
1236 writeln!(f)?;
1237 writeln!(f, "{}", "Stability results".bold().bright_magenta())?;
1238 write!(f, "{}", Table::new(stability_rows).with(Style::rounded()))?;
1239 }
1240
1241 Ok(())
1242 }
1243}
1244
1245impl EvaluationMetaData {
1246 pub(crate) fn new_empty() -> Self {
1247 Self {
1248 total_timing: Duration::ZERO,
1249 integrand_evaluation_time: Duration::ZERO,
1250 canonical_physical_preparation_time: Duration::ZERO,
1251 evaluator_evaluation_time: Duration::ZERO,
1252 parameterization_time: Duration::ZERO,
1253 canonical_sampling_preparation_time: Duration::ZERO,
1254 event_processing_time: Duration::ZERO,
1255 generated_event_count: 0,
1256 accepted_event_count: 0,
1257 relative_instability_error: Complex::new_zero(),
1258 is_nan: false,
1259 loop_momenta_escalation: None,
1260 stability_results: Vec::new(),
1261 threshold_counterterm_error: None,
1262 radial_root_diagnostics: RadialRootDiagnostics::default(),
1263 sampling_proposal_policies: SamplingProposalPolicies::default(),
1264 }
1265 }
1266
1267 pub(crate) fn clear_threshold_counterterm_error(&mut self) {
1268 self.threshold_counterterm_error = None;
1269 }
1270
1271 pub(crate) fn record_threshold_counterterm_error(&mut self, error: impl Into<String>) {
1272 if self.threshold_counterterm_error.is_none() {
1273 self.threshold_counterterm_error = Some(error.into());
1274 }
1275 }
1276
1277 pub(crate) fn final_precision(&self) -> Option<Precision> {
1278 self.stability_results.last().map(|result| result.precision)
1279 }
1280}
1281
1282#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
1284pub struct StatisticsCounter {
1285 pub num_evals: usize,
1286 num_sample_points: usize,
1287 sum_integrand_evaluation_time: Duration,
1288 sum_evaluator_evaluation_time: Duration,
1289 sum_parameterization_time: Duration,
1290 sum_event_time: Duration,
1291 sum_integrator_overhead_time: Duration,
1292 sum_total_evaluation_time: Duration,
1293 sum_relative_instability_error: (F<f64>, F<f64>),
1294 num_double_precision_evals: usize,
1295 num_quadruple_precision_evals: usize,
1296 num_arb_precision_evals: usize,
1297 num_nan_evals: usize,
1298 num_nan_or_unstable_evals: usize,
1299 sum_generated_event_count: usize,
1300 sum_accepted_event_count: usize,
1301 #[bincode(with_serde)]
1302 numerical_stability: NumericalStabilityHistogramAccumulator,
1303}
1304
1305impl StatisticsCounter {
1306 pub(crate) fn from_evaluation_results(data: &[EvaluationResult]) -> Self {
1308 data.iter().fold(
1309 StatisticsCounter::new_empty(),
1310 |mut accumulator, data_entry| {
1311 accumulator.sum_integrand_evaluation_time +=
1312 data_entry.evaluation_metadata.integrand_evaluation_time;
1313 accumulator.sum_evaluator_evaluation_time +=
1314 data_entry.evaluation_metadata.evaluator_evaluation_time;
1315 accumulator.sum_parameterization_time +=
1316 data_entry.evaluation_metadata.parameterization_time;
1317 accumulator.sum_event_time += data_entry.evaluation_metadata.event_processing_time;
1318 accumulator.sum_relative_instability_error.0 +=
1319 data_entry.evaluation_metadata.relative_instability_error.re;
1320 accumulator.sum_relative_instability_error.1 +=
1321 data_entry.evaluation_metadata.relative_instability_error.im;
1322 accumulator.sum_total_evaluation_time +=
1323 data_entry.evaluation_metadata.total_timing;
1324 accumulator.sum_generated_event_count +=
1325 data_entry.evaluation_metadata.generated_event_count;
1326 accumulator.sum_accepted_event_count +=
1327 data_entry.evaluation_metadata.accepted_event_count;
1328 accumulator
1329 .numerical_stability
1330 .fill_from_stability_results(&data_entry.evaluation_metadata.stability_results);
1331
1332 accumulator.num_evals += 1;
1333 match data_entry
1334 .evaluation_metadata
1335 .final_precision()
1336 .unwrap_or(Precision::Double)
1337 {
1338 Precision::Double => accumulator.num_double_precision_evals += 1,
1339 Precision::Quad => accumulator.num_quadruple_precision_evals += 1,
1340 Precision::Arb => accumulator.num_arb_precision_evals += 1,
1341 }
1343
1344 if data_entry.evaluation_metadata.is_nan {
1345 accumulator.num_nan_evals += 1;
1346 }
1347 if data_entry.evaluation_metadata.is_nan
1348 || data_entry
1349 .evaluation_metadata
1350 .stability_results
1351 .last()
1352 .map(|result| matches!(result.status, StabilityStatus::Unstable(_)))
1353 .unwrap_or(false)
1354 {
1355 accumulator.num_nan_or_unstable_evals += 1;
1356 }
1357
1358 accumulator
1359 },
1360 )
1361 }
1362
1363 pub(crate) fn merged(&self, other: &Self) -> Self {
1365 Self {
1366 sum_integrand_evaluation_time: self.sum_integrand_evaluation_time
1367 + other.sum_integrand_evaluation_time,
1368 sum_evaluator_evaluation_time: self.sum_evaluator_evaluation_time
1369 + other.sum_evaluator_evaluation_time,
1370 sum_parameterization_time: self.sum_parameterization_time
1371 + other.sum_parameterization_time,
1372 sum_integrator_overhead_time: self.sum_integrator_overhead_time
1373 + other.sum_integrator_overhead_time,
1374 sum_relative_instability_error: (
1375 self.sum_relative_instability_error.0 + other.sum_relative_instability_error.0,
1376 self.sum_relative_instability_error.1 + other.sum_relative_instability_error.1,
1377 ),
1378 num_evals: self.num_evals + other.num_evals,
1379 num_sample_points: self.num_sample_points + other.num_sample_points,
1380 num_double_precision_evals: (self.num_double_precision_evals
1381 + other.num_double_precision_evals),
1382 num_quadruple_precision_evals: (self.num_quadruple_precision_evals
1383 + other.num_quadruple_precision_evals),
1384 num_arb_precision_evals: self.num_arb_precision_evals + other.num_arb_precision_evals,
1385 sum_total_evaluation_time: self.sum_total_evaluation_time
1386 + other.sum_total_evaluation_time,
1387 num_nan_evals: self.num_nan_evals + other.num_nan_evals,
1388 num_nan_or_unstable_evals: self.num_nan_or_unstable_evals
1389 + other.num_nan_or_unstable_evals,
1390 sum_event_time: self.sum_event_time + other.sum_event_time,
1391 sum_generated_event_count: self.sum_generated_event_count
1392 + other.sum_generated_event_count,
1393 sum_accepted_event_count: self.sum_accepted_event_count
1394 + other.sum_accepted_event_count,
1395 numerical_stability: self.numerical_stability.merged(&other.numerical_stability),
1396 }
1397 }
1398
1399 pub(crate) fn new_empty() -> Self {
1400 Self {
1401 sum_integrand_evaluation_time: Duration::ZERO,
1402 sum_evaluator_evaluation_time: Duration::ZERO,
1403 sum_parameterization_time: Duration::ZERO,
1404 sum_event_time: Duration::ZERO,
1405 sum_integrator_overhead_time: Duration::ZERO,
1406 sum_relative_instability_error: (F(0.0), F(0.0)),
1407 sum_total_evaluation_time: Duration::ZERO,
1408 num_evals: 0,
1409 num_sample_points: 0,
1410 num_double_precision_evals: 0,
1411 num_quadruple_precision_evals: 0,
1412 num_arb_precision_evals: 0,
1413 num_nan_evals: 0,
1414 num_nan_or_unstable_evals: 0,
1415 sum_generated_event_count: 0,
1416 sum_accepted_event_count: 0,
1417 numerical_stability: NumericalStabilityHistogramAccumulator::new_empty(),
1418 }
1419 }
1420
1421 #[allow(dead_code)]
1423 pub(crate) fn merge_list(list: Vec<Self>) -> Self {
1424 if let Some(merged) = list.into_iter().reduce(|acc, x| acc.merged(&x)) {
1425 merged
1426 } else {
1427 Self::new_empty()
1428 }
1429 }
1430
1431 fn avg_duration(sum: Duration, count: usize) -> Duration {
1432 if count == 0 {
1433 return Duration::ZERO;
1434 }
1435
1436 duration_from_secs_f64_saturating(sum.as_secs_f64() / count as f64)
1437 }
1438
1439 fn eval_percentage(&self, count: usize) -> f64 {
1440 if self.num_evals == 0 {
1441 0.0
1442 } else {
1443 count as f64 / self.num_evals as f64 * 100.0
1444 }
1445 }
1446
1447 pub(crate) fn add_integrator_overhead(&mut self, duration: Duration, sample_points: usize) {
1448 self.sum_integrator_overhead_time += duration;
1449 self.num_sample_points += sample_points;
1450 }
1451
1452 pub(crate) fn get_avg_total_timing(&self) -> Duration {
1454 Self::avg_duration(self.sum_total_evaluation_time, self.num_evals)
1455 }
1456
1457 pub(crate) fn get_avg_integrand_timing(&self) -> Duration {
1459 Self::avg_duration(self.sum_integrand_evaluation_time, self.num_evals)
1460 }
1461
1462 pub(crate) fn get_avg_evaluator_timing(&self) -> Duration {
1464 Self::avg_duration(self.sum_evaluator_evaluation_time, self.num_evals)
1465 }
1466
1467 pub(crate) fn get_avg_param_timing(&self) -> Duration {
1469 Self::avg_duration(self.sum_parameterization_time, self.num_evals)
1470 }
1471
1472 pub(crate) fn get_avg_event_timing(&self) -> Duration {
1473 Self::avg_duration(self.sum_event_time, self.num_evals)
1474 }
1475
1476 pub(crate) fn get_avg_integrator_overhead_timing(&self) -> Duration {
1477 Self::avg_duration(self.sum_integrator_overhead_time, self.num_sample_points)
1478 }
1479
1480 #[allow(dead_code)]
1482 pub(crate) fn get_avg_instabillity_error(&self) -> (F<f64>, F<f64>) {
1483 if self.num_evals == 0 {
1484 return (
1485 self.sum_relative_instability_error.0.zero(),
1486 self.sum_relative_instability_error.1.zero(),
1487 );
1488 }
1489
1490 (
1491 self.sum_relative_instability_error.0
1492 / self
1493 .sum_relative_instability_error
1494 .0
1495 .from_usize(self.num_evals),
1496 self.sum_relative_instability_error.1
1497 / self
1498 .sum_relative_instability_error
1499 .1
1500 .from_usize(self.num_evals),
1501 )
1502 }
1503
1504 pub(crate) fn get_percentage_f64(&self) -> f64 {
1506 self.eval_percentage(self.num_double_precision_evals)
1507 }
1508
1509 pub(crate) fn get_percentage_f128(&self) -> f64 {
1511 self.eval_percentage(self.num_quadruple_precision_evals)
1512 }
1513
1514 pub(crate) fn get_percentage_arb(&self) -> f64 {
1515 self.eval_percentage(self.num_arb_precision_evals)
1516 }
1517
1518 pub(crate) fn get_percentage_nan(&self) -> f64 {
1519 self.eval_percentage(self.num_nan_evals)
1520 }
1521
1522 pub(crate) fn get_percentage_nan_or_unstable(&self) -> f64 {
1523 self.eval_percentage(self.num_nan_or_unstable_evals)
1524 }
1525
1526 pub(crate) fn selection_efficiency_percentage(&self) -> Option<f64> {
1527 if self.sum_generated_event_count == 0 {
1528 None
1529 } else {
1530 Some(
1531 self.sum_accepted_event_count as f64 / self.sum_generated_event_count as f64
1532 * 100.0,
1533 )
1534 }
1535 }
1536
1537 pub fn numerical_stability_snapshot(&self) -> NumericalStabilityHistograms {
1538 self.numerical_stability.snapshot()
1539 }
1540
1541 pub(crate) fn snapshot(&self) -> IntegrationStatisticsSnapshot {
1542 IntegrationStatisticsSnapshot {
1543 num_evals: self.num_evals,
1544 average_total_time_seconds: self.get_avg_total_timing().as_secs_f64(),
1545 average_parameterization_time_seconds: self.get_avg_param_timing().as_secs_f64(),
1546 average_integrand_time_seconds: self.get_avg_integrand_timing().as_secs_f64(),
1547 average_evaluator_time_seconds: self.get_avg_evaluator_timing().as_secs_f64(),
1548 average_observable_time_seconds: self.get_avg_event_timing().as_secs_f64(),
1549 average_integrator_time_seconds: self
1550 .get_avg_integrator_overhead_timing()
1551 .as_secs_f64(),
1552 f64_percentage: self.get_percentage_f64(),
1553 f128_percentage: self.get_percentage_f128(),
1554 arb_percentage: self.get_percentage_arb(),
1555 nan_percentage: self.get_percentage_nan(),
1556 nan_or_unstable_percentage: self.get_percentage_nan_or_unstable(),
1557 generated_event_count: self.sum_generated_event_count,
1558 accepted_event_count: self.sum_accepted_event_count,
1559 selection_efficiency_percentage: self.selection_efficiency_percentage(),
1560 }
1561 }
1562
1563 pub(crate) fn build_status_table(&self) -> Table {
1564 let time_integrand_formatted = format_evaluation_time(self.get_avg_integrand_timing());
1565 let time_evaluators_formatted = format_evaluation_time(self.get_avg_evaluator_timing());
1566 let param_time_formatted = format_evaluation_time(self.get_avg_param_timing());
1567 let event_time_formatted = format_evaluation_time(self.get_avg_event_timing());
1568 let integrator_time_formatted =
1569 format_evaluation_time(self.get_avg_integrator_overhead_timing());
1570 let total_time = format_evaluation_time(self.get_avg_total_timing());
1571 let selection_efficiency = self.selection_efficiency_percentage();
1572 let selection_efficiency_display = selection_efficiency
1573 .map(|value| {
1574 pad_status_value(format_percentage(value, 3), 9)
1575 .green()
1576 .to_string()
1577 })
1578 .unwrap_or_else(|| pad_status_value("N/A", 9));
1579 let nan_or_unstable = self.get_percentage_nan_or_unstable();
1580 let nan_or_unstable_display = if nan_or_unstable > 0.0 {
1581 pad_status_value(format_percentage(nan_or_unstable, 2), 9)
1582 .red()
1583 .to_string()
1584 } else {
1585 pad_status_value(format_percentage(nan_or_unstable, 2), 9)
1586 .green()
1587 .to_string()
1588 };
1589 let mut table = Builder::new();
1590 table.push_record([
1591 format_status_header("timing"),
1592 format_status_key("total"),
1593 pad_status_value(total_time, 9).green().to_string(),
1594 format_status_key("param"),
1595 pad_status_value(param_time_formatted, 9)
1596 .green()
1597 .to_string(),
1598 format_status_key("itg"),
1599 pad_status_value(time_integrand_formatted, 9)
1600 .green()
1601 .to_string(),
1602 format_status_key("evaluators"),
1603 pad_status_value(time_evaluators_formatted, 9)
1604 .green()
1605 .to_string(),
1606 ]);
1607 table.push_record([
1608 format_status_header("evals"),
1609 format_status_key("f64"),
1610 pad_status_value(format!("{:.2}%", self.get_percentage_f64()), 9)
1611 .green()
1612 .to_string(),
1613 format_status_key("f128"),
1614 pad_status_value(format!("{:.2}%", self.get_percentage_f128()), 9)
1615 .green()
1616 .to_string(),
1617 format_status_key("arb"),
1618 pad_status_value(format!("{:.2}%", self.get_percentage_arb()), 9)
1619 .green()
1620 .to_string(),
1621 format_status_key("nans+unstable"),
1622 nan_or_unstable_display.clone(),
1623 ]);
1624 table.push_record([
1625 format_status_header("events"),
1626 format_status_key("evts #"),
1627 pad_status_value(format_count(self.sum_generated_event_count), 9)
1628 .green()
1629 .to_string(),
1630 format_status_key("sel. %"),
1631 selection_efficiency_display,
1632 format_status_key("obs"),
1633 pad_status_value(event_time_formatted, 9)
1634 .green()
1635 .to_string(),
1636 format_status_key("integrator"),
1637 pad_status_value(integrator_time_formatted, 9)
1638 .green()
1639 .to_string(),
1640 ]);
1641
1642 let mut table = table.build();
1643 table.with(Panel::header(
1644 "Integration statistics".bold().green().to_string(),
1645 ));
1646 table.with(
1647 Style::rounded()
1648 .remove_horizontals()
1649 .verticals([
1650 (1, status_group_separator()),
1651 (3, status_group_separator()),
1652 (5, status_group_separator()),
1653 (7, status_group_separator()),
1654 ])
1655 .horizontals([(
1656 1,
1657 HorizontalLine::new('─')
1658 .intersection('┬')
1659 .left('├')
1660 .right('┤'),
1661 )])
1662 .remove_vertical(),
1663 );
1664 table.with(BorderCorrection::span());
1665 table.with(Modify::new(Rows::new(0..1)).with(Alignment::center()));
1666 table.with(Modify::new(Rows::new(1..)).with(Alignment::left()));
1667 for column in [1usize, 3, 5, 7] {
1668 table.with(
1669 Modify::new(Rows::new(1..).intersect(Columns::one(column)))
1670 .with(Alignment::right()),
1671 );
1672 }
1673 table
1674 }
1675
1676 pub(crate) fn render_status_table(&self) -> String {
1677 normalize_tabled_separator_rows(&self.build_status_table().to_string())
1678 }
1679}
1680
1681#[cfg(test)]
1682mod tests {
1683 use std::time::Duration;
1684
1685 use crate::settings::runtime::Precision;
1686
1687 use super::{
1688 BatchSampleEvaluationResult, EvaluationMetaData, EvaluationResult,
1689 NumericalStabilityHistogramAccumulator, NumericalStabilityLevel, NumericalStabilityMedian,
1690 StabilityResult, StabilityStatus, StatisticsCounter,
1691 };
1692
1693 #[test]
1694 fn status_table_renders_three_rows_without_header() {
1695 let stats = StatisticsCounter {
1696 num_evals: 10,
1697 num_sample_points: 10,
1698 sum_integrand_evaluation_time: Duration::from_micros(414),
1699 sum_evaluator_evaluation_time: Duration::from_micros(279),
1700 sum_parameterization_time: Duration::from_nanos(5_800),
1701 sum_event_time: Duration::ZERO,
1702 sum_integrator_overhead_time: Duration::from_micros(110),
1703 sum_total_evaluation_time: Duration::from_micros(462),
1704 sum_relative_instability_error: (0.0.into(), 0.0.into()),
1705 num_double_precision_evals: 10,
1706 num_quadruple_precision_evals: 0,
1707 num_arb_precision_evals: 0,
1708 num_nan_evals: 0,
1709 num_nan_or_unstable_evals: 0,
1710 sum_generated_event_count: 0,
1711 sum_accepted_event_count: 0,
1712 numerical_stability: NumericalStabilityHistogramAccumulator::new_empty(),
1713 };
1714
1715 let rendered = stats.render_status_table();
1716 let lines = rendered.lines().collect::<Vec<_>>();
1717
1718 assert!(
1719 rendered.starts_with('╭') && rendered.ends_with('╯'),
1720 "{rendered}"
1721 );
1722 assert_eq!(lines.len(), 7, "{rendered}");
1723 assert!(rendered.contains("Integration statistics"), "{rendered}");
1724 assert!(lines[2].contains('┬'), "{rendered}");
1725 assert!(lines[6].contains('┴'), "{rendered}");
1726 assert_eq!(lines[3].matches('│').count(), 6, "{rendered}");
1727 assert_eq!(lines[4].matches('│').count(), 6, "{rendered}");
1728 assert_eq!(lines[5].matches('│').count(), 6, "{rendered}");
1729 assert!(rendered.contains("timing"), "{rendered}");
1730 assert!(rendered.contains("evals"), "{rendered}");
1731 assert!(rendered.contains("events"), "{rendered}");
1732 assert!(rendered.contains("nans+unstable :"), "{rendered}");
1733 assert!(rendered.contains("integrator :"), "{rendered}");
1734 }
1735
1736 #[test]
1737 fn statistics_snapshot_reports_selection_efficiency_from_event_counts() {
1738 let stats = StatisticsCounter {
1739 num_evals: 2,
1740 num_sample_points: 2,
1741 sum_integrand_evaluation_time: Duration::ZERO,
1742 sum_evaluator_evaluation_time: Duration::ZERO,
1743 sum_parameterization_time: Duration::ZERO,
1744 sum_event_time: Duration::ZERO,
1745 sum_integrator_overhead_time: Duration::ZERO,
1746 sum_total_evaluation_time: Duration::ZERO,
1747 sum_relative_instability_error: (0.0.into(), 0.0.into()),
1748 num_double_precision_evals: 2,
1749 num_quadruple_precision_evals: 0,
1750 num_arb_precision_evals: 0,
1751 num_nan_evals: 0,
1752 num_nan_or_unstable_evals: 0,
1753 sum_generated_event_count: 10,
1754 sum_accepted_event_count: 4,
1755 numerical_stability: NumericalStabilityHistogramAccumulator::new_empty(),
1756 };
1757
1758 let snapshot = stats.snapshot();
1759
1760 assert_eq!(snapshot.generated_event_count, 10);
1761 assert_eq!(snapshot.accepted_event_count, 4);
1762 assert_eq!(snapshot.selection_efficiency_percentage, Some(40.0));
1763
1764 let rendered = stats.render_status_table();
1765 assert!(rendered.contains("sel. % : 40.0%"), "{rendered}");
1766 }
1767
1768 #[test]
1769 fn evaluation_metadata_renders_stability_status_with_sample_counts() {
1770 let metadata = EvaluationMetaData {
1771 stability_results: vec![
1772 StabilityResult {
1773 precision: Precision::Double,
1774 estimated_relative_accuracy: Some(1.0e-5.into()),
1775 estimated_decimal_digits: Some(5.0.into()),
1776 status: StabilityStatus::Stable(3),
1777 total_time: Duration::from_micros(120),
1778 },
1779 StabilityResult {
1780 precision: Precision::Quad,
1781 estimated_relative_accuracy: None,
1782 estimated_decimal_digits: None,
1783 status: StabilityStatus::Unknown,
1784 total_time: Duration::from_micros(240),
1785 },
1786 ],
1787 ..EvaluationMetaData::new_empty()
1788 };
1789
1790 let rendered = metadata.to_string();
1791 assert!(rendered.contains("Stable(3 samples)"), "{rendered}");
1792 assert!(rendered.contains("Unknown(1 sample)"), "{rendered}");
1793 assert!(rendered.contains("None"), "{rendered}");
1794 }
1795
1796 #[test]
1797 fn numerical_stability_histograms_aggregate_stability_results() {
1798 let mut first = EvaluationResult::zero();
1799 first.evaluation_metadata.stability_results = vec![
1800 StabilityResult {
1801 precision: Precision::Double,
1802 estimated_relative_accuracy: Some(1.0e-8.into()),
1803 estimated_decimal_digits: Some(8.0.into()),
1804 status: StabilityStatus::Stable(3),
1805 total_time: Duration::ZERO,
1806 },
1807 StabilityResult {
1808 precision: Precision::Quad,
1809 estimated_relative_accuracy: Some(0.0.into()),
1810 estimated_decimal_digits: None,
1811 status: StabilityStatus::Stable(3),
1812 total_time: Duration::ZERO,
1813 },
1814 ];
1815
1816 let stats = StatisticsCounter::from_evaluation_results(&[first]);
1817 let stability = stats.numerical_stability_snapshot();
1818 let double_histogram = stability
1819 .histograms
1820 .get(NumericalStabilityLevel::Double.key())
1821 .expect("double histogram");
1822 let quad_histogram = stability
1823 .histograms
1824 .get(NumericalStabilityLevel::Quad.key())
1825 .expect("quad histogram");
1826
1827 assert_eq!(double_histogram.sample_count, 1);
1828 assert_eq!(double_histogram.statistics.in_range_entry_count, 1);
1829 assert_eq!(quad_histogram.sample_count, 1);
1830 assert_eq!(quad_histogram.overflow_bin.entry_count, 1);
1831 let Some(NumericalStabilityMedian::InRange {
1832 log10_relative_accuracy,
1833 }) = stability.median(NumericalStabilityLevel::Double)
1834 else {
1835 panic!("double median should be populated in range");
1836 };
1837 assert!((-9.0..-7.0).contains(&log10_relative_accuracy));
1838 assert_eq!(
1839 stability
1840 .median(NumericalStabilityLevel::Quad)
1841 .map(|median| median.formatted_relative_accuracy()),
1842 Some("<=1.0e-34".to_string())
1843 );
1844 assert_eq!(stability.median(NumericalStabilityLevel::ArbPrec), None);
1845 }
1846
1847 #[test]
1848 fn arb_stability_median_renders_unrepresentable_relative_accuracy() {
1849 for accuracy in [0.0_f64, 1.0e-320] {
1850 let mut result = EvaluationResult::zero();
1851 result
1852 .evaluation_metadata
1853 .stability_results
1854 .push(StabilityResult {
1855 precision: Precision::Arb,
1856 estimated_relative_accuracy: Some(accuracy.into()),
1857 estimated_decimal_digits: (accuracy != 0.0).then(|| (-accuracy.log10()).into()),
1858 status: StabilityStatus::Stable(1),
1859 total_time: Duration::ZERO,
1860 });
1861 let statistics = StatisticsCounter::from_evaluation_results(&[result]);
1862 let rendered = statistics
1863 .numerical_stability_snapshot()
1864 .median(NumericalStabilityLevel::ArbPrec)
1865 .unwrap()
1866 .formatted_relative_accuracy();
1867 if accuracy == 0.0 {
1868 assert_eq!(rendered, "<=1.0e-1000");
1869 } else {
1870 let (mantissa, exponent) = rendered.split_once('e').unwrap();
1871 assert!(mantissa.parse::<f64>().unwrap() > 0.0, "{rendered}");
1872 assert!(
1873 (-330..=-310).contains(&exponent.parse::<i32>().unwrap()),
1874 "{rendered}"
1875 );
1876 }
1877 }
1878 }
1879
1880 #[test]
1881 fn numerical_stability_overflow_includes_exact_upper_boundary() {
1882 for (precision, level, digits, expected) in [
1883 (
1884 Precision::Double,
1885 NumericalStabilityLevel::Double,
1886 17,
1887 "<=1.0e-17",
1888 ),
1889 (
1890 Precision::Quad,
1891 NumericalStabilityLevel::Quad,
1892 34,
1893 "<=1.0e-34",
1894 ),
1895 (
1896 Precision::Arb,
1897 NumericalStabilityLevel::ArbPrec,
1898 1000,
1899 "<=1.0e-1000",
1900 ),
1901 ] {
1902 let mut result = EvaluationResult::zero();
1903 result
1904 .evaluation_metadata
1905 .stability_results
1906 .push(StabilityResult {
1907 precision,
1908 estimated_relative_accuracy: Some(10.0_f64.powi(-digits).into()),
1909 estimated_decimal_digits: Some(f64::from(digits).into()),
1910 status: StabilityStatus::Stable(2),
1911 total_time: Duration::ZERO,
1912 });
1913 let statistics = StatisticsCounter::from_evaluation_results(&[result]);
1914 let rendered = statistics
1915 .numerical_stability_snapshot()
1916 .median(level)
1917 .unwrap()
1918 .formatted_relative_accuracy();
1919 assert_eq!(rendered, expected);
1920 }
1921 }
1922
1923 #[test]
1924 fn batch_result_omits_numerical_stability_when_minimal() {
1925 let rendered = BatchSampleEvaluationResult {
1926 samples: Vec::new(),
1927 observables: Default::default(),
1928 numerical_stability: None,
1929 }
1930 .to_string();
1931
1932 assert!(!rendered.contains("Numerical stability"), "{rendered}");
1933 }
1934
1935 #[test]
1936 fn zero_eval_statistics_snapshot_and_render_are_sanitized() {
1937 let stats = StatisticsCounter::new_empty();
1938 let snapshot = stats.snapshot();
1939
1940 assert_eq!(snapshot.num_evals, 0);
1941 assert_eq!(snapshot.average_total_time_seconds, 0.0);
1942 assert_eq!(snapshot.average_parameterization_time_seconds, 0.0);
1943 assert_eq!(snapshot.average_integrand_time_seconds, 0.0);
1944 assert_eq!(snapshot.average_evaluator_time_seconds, 0.0);
1945 assert_eq!(snapshot.average_observable_time_seconds, 0.0);
1946 assert_eq!(snapshot.average_integrator_time_seconds, 0.0);
1947 assert_eq!(snapshot.f64_percentage, 0.0);
1948 assert_eq!(snapshot.f128_percentage, 0.0);
1949 assert_eq!(snapshot.arb_percentage, 0.0);
1950 assert_eq!(snapshot.nan_percentage, 0.0);
1951 assert_eq!(snapshot.nan_or_unstable_percentage, 0.0);
1952 assert_eq!(snapshot.selection_efficiency_percentage, None);
1953
1954 let rendered = stats.render_status_table();
1955 assert!(rendered.contains("sel. % : N/A"), "{rendered}");
1956 assert!(rendered.contains("f64 : 0.00%"), "{rendered}");
1957 assert!(rendered.contains("nans+unstable : 0.0%"), "{rendered}");
1958 assert!(rendered.contains("integrator : 0.00 µs"), "{rendered}");
1959 }
1960}