Skip to main content

gammalooprs/integrate/
mod.rs

1#![allow(dead_code)]
2
3//! This module steers the integration process.
4//! It contains two main ways of integrating the integrand.
5//! The havana_integrate function is mostly used for local runs.
6//! The master node in combination with batch_integrate is for distributed runs.
7
8mod display;
9mod render_ratatui;
10mod render_tabled;
11mod status_update;
12
13use crate::integrands::process::EvaluationTarget;
14use bincode::Decode;
15use bincode::Encode;
16use color_eyre::{Report, Result};
17use colored::Colorize;
18use itertools::Itertools;
19use itertools::izip;
20use linnet::half_edge::involution::{EdgeVec, Orientation};
21use rayon::ThreadPoolBuilder;
22use serde::Deserialize;
23use serde::Serialize;
24use spenso::algebra::algebraic_traits::IsZero;
25use symbolica::domains::float::Constructible;
26use symbolica::numerical_integration::{
27    DiscreteGrid, Grid, MonteCarloRng, Sample, StatisticsAccumulator,
28};
29
30use crate::Integrand;
31use crate::graph::GroupId;
32use crate::integrands::HasIntegrand;
33use crate::integrands::evaluation::StatisticsCounter;
34use crate::integrands::evaluation::{EvaluationMetaData, EvaluationResult};
35use crate::integrands::process::GaussianReferenceFunction;
36use crate::integrands::process::{GraphTerm, ProcessIntegrand};
37use crate::model::{Model, SerializableInputParamCard};
38use crate::observables::{
39    EventGroupList, ObservableAccumulatorBundle, ObservableFileFormat, ObservableSnapshotBundle,
40};
41use crate::settings::IntegratorSettings;
42use crate::settings::RuntimeSettings;
43use crate::settings::runtime::{
44    AbsoluteIntegrationResult, ComponentDiscreteBreakdown, DiscreteBreakdown,
45    DiscreteBreakdownEntry, DiscreteCoordinate, DiscreteGraphSamplingType, IntegralEstimate,
46    IntegratedPhase, IntegrationResult, IntegrationTableComponentResult, MaxWeightInfoEntry,
47    SamplingSettings, SlotIntegrationResult,
48};
49use crate::utils;
50use crate::utils::F;
51use crate::{
52    clear_iteration_abort_request, is_interrupted, is_iteration_abort_requested, set_interrupted,
53};
54use rayon::prelude::*;
55pub use render_ratatui::RatatuiDashboardState;
56pub use render_tabled::TabledRenderOptions;
57use spenso::algebra::complex::Complex;
58pub use status_update::{
59    ContributionSortMode, IntegrationStatusKind, IntegrationStatusPhaseDisplay,
60    IntegrationStatusViewOptions, StatusUpdate,
61};
62use status_update::{
63    StatusUpdateBuildRequest, build_saved_status_update, build_status_update,
64    evaluate_target_accuracy,
65};
66use std::fs;
67use std::io::Write;
68use std::path::Path;
69use std::path::PathBuf;
70use std::time::Duration;
71use std::time::Instant;
72use tabled::{
73    Tabled,
74    builder::Builder,
75    settings::{
76        Alignment, Modify, Panel, Style,
77        object::{Columns, Rows},
78        style::HorizontalLine,
79    },
80};
81#[allow(unused_imports)]
82use tracing::{debug, error, info, trace, warn};
83
84#[derive(Clone, Copy, Debug, Default)]
85pub struct WorkspaceSnapshotControl {
86    pub write_iteration_archives: bool,
87}
88
89#[derive(Clone, Copy, Debug)]
90pub struct IterationBatchingSettings {
91    pub batch_size: Option<usize>,
92    pub batch_timing_seconds: f64,
93    pub min_time_between_status_updates_seconds: f64,
94    pub emit_live_status_updates: bool,
95    pub emit_initial_status_update: bool,
96}
97
98impl Default for IterationBatchingSettings {
99    fn default() -> Self {
100        Self {
101            batch_size: None,
102            batch_timing_seconds: 5.0,
103            min_time_between_status_updates_seconds: 0.0,
104            emit_live_status_updates: true,
105            emit_initial_status_update: true,
106        }
107    }
108}
109
110pub struct IntegrationSlot {
111    pub meta: SlotMeta,
112    pub settings: RuntimeSettings,
113    pub model: Model,
114    pub integrand: Integrand,
115    pub target: Option<Complex<F<f64>>>,
116    /// Acceptance target retained independently of worker-local process clones.
117    pub reference_gaussian: Option<GaussianReferenceFunction>,
118}
119
120impl IntegrationSlot {
121    pub fn new(
122        meta: SlotMeta,
123        settings: RuntimeSettings,
124        model: Model,
125        integrand: Integrand,
126        target: Option<Complex<F<f64>>>,
127    ) -> Self {
128        Self {
129            meta,
130            settings,
131            model,
132            integrand,
133            target,
134            reference_gaussian: None,
135        }
136    }
137}
138
139pub struct HavanaIntegrateRequest {
140    pub slots: Vec<IntegrationSlot>,
141    pub sampling_correlation_mode: SamplingCorrelationMode,
142    pub n_cores: usize,
143    pub state: Option<IntegrationState>,
144    pub workspace: Option<PathBuf>,
145    pub output_control: WorkspaceSnapshotControl,
146    pub batching: IterationBatchingSettings,
147    pub view_options: IntegrationStatusViewOptions,
148}
149
150// const N_INTEGRAND_ACCUMULATORS: usize = 2;
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
153pub struct SlotMeta {
154    pub process_name: String,
155    pub integrand_name: String,
156}
157
158impl SlotMeta {
159    pub fn key(&self) -> String {
160        format!("{}@{}", self.process_name, self.integrand_name)
161    }
162}
163
164#[derive(Serialize, Deserialize, Clone)]
165pub struct IntegrationWorkspaceManifest {
166    #[serde(default)]
167    pub version: u32,
168    pub slots: Vec<SlotMeta>,
169    pub targets: Vec<Option<Complex<F<f64>>>>,
170    pub effective_model_parameters: Vec<SerializableInputParamCard<F<f64>>>,
171    pub integrand_fingerprints: Vec<String>,
172    /// Empty for physical workspaces; otherwise one resolved descriptor per slot.
173    #[serde(default, skip_serializing_if = "Vec::is_empty")]
174    pub reference_gaussians: Vec<Option<GaussianReferenceFunction>>,
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub reference_observable_convention: Option<String>,
177    pub training_slot: usize,
178    pub integrator_settings_slot: usize,
179    pub sampling_correlation_mode: SamplingCorrelationMode,
180}
181
182impl IntegrationWorkspaceManifest {
183    // Version 3 records physical absolute contributions before distinct
184    // sampling-channel points are summed. Older absolute moments cannot resume.
185    pub const CURRENT_VERSION: u32 = 3;
186
187    /// A missing descriptor requests physics. Resume never silently changes that
188    /// request into acceptance, and an unknown reporting convention cannot reuse grids.
189    pub fn validate_reference_config(
190        &self,
191        requested: &[Option<GaussianReferenceFunction>],
192    ) -> Result<()> {
193        if !self.reference_gaussians.is_empty()
194            && self.reference_gaussians.len() != self.slots.len()
195        {
196            return Err(Report::msg(
197                "Workspace reference descriptors must match its slots",
198            ));
199        }
200        for reference in self.reference_gaussians.iter().flatten() {
201            let scale = reference.expected_second_moment::<f64>().0;
202            if reference.center().is_empty() || !scale.is_finite() || scale <= 0.0 {
203                return Err(Report::msg(
204                    "Workspace reference descriptor must contain a resolved center and finite positive moment scale",
205                ));
206            }
207        }
208        let has_reference = requested.iter().any(Option::is_some);
209        let expected_convention =
210            has_reference.then_some(GaussianReferenceFunction::INTEGRATION_CONVENTION);
211        let same_config = if self.reference_gaussians.is_empty() {
212            !has_reference
213        } else {
214            self.reference_gaussians == requested
215        };
216        if !same_config || self.reference_observable_convention.as_deref() != expected_convention {
217            return Err(Report::msg(format!(
218                "Workspace physical/reference configuration does not match the request. Repeat the same --reference-gaussian descriptor to resume acceptance, or use --restart for a different target. Saved descriptors: {}",
219                serde_json::to_string(&self.reference_gaussians)?
220            )));
221        }
222        Ok(())
223    }
224
225    pub fn validate_version(&self) -> Result<()> {
226        if self.version == Self::CURRENT_VERSION {
227            return Ok(());
228        }
229
230        let reason = if self.version == 0 {
231            "is unversioned".to_string()
232        } else if self.version < Self::CURRENT_VERSION {
233            format!("uses the older format version {}", self.version)
234        } else {
235            format!("uses the newer format version {}", self.version)
236        };
237        Err(Report::msg(format!(
238            "Integration workspace {reason} and cannot be resumed by this GammaLoop build (supported version {}); use --restart to create a new workspace",
239            Self::CURRENT_VERSION
240        )))
241    }
242}
243
244impl crate::utils::serde_utils::SmartSerde for IntegrationWorkspaceManifest {}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
247#[serde(rename_all = "snake_case")]
248pub enum SamplingCorrelationMode {
249    Correlated,
250    Uncorrelated,
251}
252
253impl SamplingCorrelationMode {
254    fn state_index(self, slot_index: usize) -> usize {
255        match self {
256            Self::Correlated => 0,
257            Self::Uncorrelated => slot_index,
258        }
259    }
260
261    fn state_count(self, n_slots: usize) -> usize {
262        match self {
263            Self::Correlated => 1,
264            Self::Uncorrelated => n_slots,
265        }
266    }
267}
268
269#[derive(Serialize, Deserialize, Encode, Decode, Clone)]
270pub(crate) struct DiscreteGridAccumulatorSummary {
271    #[bincode(with_serde)]
272    pub(crate) bins: Vec<DiscreteGridBinAccumulatorSummary>,
273}
274
275#[derive(Serialize, Deserialize, Encode, Decode, Clone)]
276pub(crate) struct DiscreteGridBinAccumulatorSummary {
277    #[bincode(with_serde)]
278    pub(crate) accumulator: StatisticsAccumulator<F<f64>>,
279    pub(crate) sub_summary: Option<Box<DiscreteGridAccumulatorSummary>>,
280}
281
282#[derive(Serialize, Deserialize, Encode, Decode, Clone, Default)]
283struct PersistedDiscreteBreakdownMetadata {
284    axis_label: String,
285    #[bincode(with_serde)]
286    fixed_coordinates: Vec<DiscreteCoordinate>,
287    bin_labels: Vec<String>,
288}
289
290#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
291struct SamplingSlotState {
292    #[bincode(with_serde)]
293    grid: Grid<F<f64>>,
294    discrete_axis_labels: Vec<String>,
295}
296
297impl SamplingSlotState {
298    fn new(grid: Grid<F<f64>>, discrete_axis_labels: Vec<String>) -> Self {
299        Self {
300            grid,
301            discrete_axis_labels,
302        }
303    }
304
305    fn from_integrand(integrand: &Integrand) -> Self {
306        let grid = integrand.create_grid();
307        let discrete_axis_labels = match integrand {
308            Integrand::ProcessIntegrand(process_integrand) => {
309                discrete_axis_labels(&process_integrand.get_settings().sampling)
310                    .into_iter()
311                    .map(str::to_string)
312                    .collect_vec()
313            }
314            #[cfg(test)]
315            Integrand::TestProbe(_) => Vec::new(),
316        };
317        Self::new(grid, discrete_axis_labels)
318    }
319}
320
321#[derive(Default)]
322struct MonitoredDiscreteSetup {
323    path: Option<Vec<usize>>,
324    axis_label: Option<String>,
325    bin_descriptions: Option<Vec<String>>,
326    warning: Option<String>,
327}
328
329#[derive(Tabled)]
330pub struct IntegralResult {
331    id: String,
332    n_samples: String,
333    #[tabled(rename = "n_samples[%]")]
334    n_samples_perc: String,
335    #[tabled(rename = "<I>")]
336    integral: String,
337    #[tabled(rename = "sqrt(σ)")]
338    variance: String,
339    err: String,
340    #[tabled(rename = "err[%]")]
341    err_perc: String,
342    #[tabled(rename = "PDF")]
343    pdf: String,
344}
345
346#[derive(Serialize, Deserialize, Encode, Decode, Clone)]
347pub struct ComplexAccumulator {
348    #[bincode(with_serde)]
349    pub re: StatisticsAccumulator<F<f64>>,
350    #[bincode(with_serde)]
351    pub im: StatisticsAccumulator<F<f64>>,
352    #[bincode(with_serde)]
353    pub absolute_re: StatisticsAccumulator<F<f64>>,
354    #[bincode(with_serde)]
355    pub absolute_im: StatisticsAccumulator<F<f64>>,
356}
357
358impl ComplexAccumulator {
359    pub(crate) fn new() -> Self {
360        Self {
361            re: StatisticsAccumulator::new(),
362            im: StatisticsAccumulator::new(),
363            absolute_re: StatisticsAccumulator::new(),
364            absolute_im: StatisticsAccumulator::new(),
365        }
366    }
367
368    /// used to escalate the stability test on high evaluation values
369    pub(crate) fn get_worst_case(&self) -> Complex<F<f64>> {
370        Complex::new(
371            self.re
372                .max_eval_positive
373                .abs()
374                .max(self.re.max_eval_negative.abs()),
375            self.im
376                .max_eval_positive
377                .abs()
378                .max(self.im.max_eval_negative.abs()),
379        )
380    }
381
382    /// add a sample to the accumulator
383    pub(crate) fn add_sample(
384        &mut self,
385        result: Complex<F<f64>>,
386        absolute_result: Option<Complex<F<f64>>>,
387        sample_weight: F<f64>,
388        sample: Option<&Sample<F<f64>>>,
389    ) {
390        let weighted_re = result.re * sample_weight;
391        let weighted_im = result.im * sample_weight;
392        self.re.add_sample(weighted_re, sample);
393        self.im.add_sample(weighted_im, sample);
394        // One outer cube is one statistical sample, including every explicitly
395        // summed channel. Squaring their summed absolute contribution retains
396        // covariance; abs of the signed channel sum would be another integral.
397        let absolute = absolute_result
398            .unwrap_or_else(|| Complex::new(result.re.abs(), result.im.abs()))
399            * sample_weight.abs();
400        self.absolute_re.add_sample(absolute.re, sample);
401        self.absolute_im.add_sample(absolute.im, sample);
402    }
403
404    pub(crate) fn merge(&mut self, other: &Self) {
405        self.re.merge_samples_no_reset(&other.re);
406        self.im.merge_samples_no_reset(&other.im);
407        self.absolute_re.merge_samples_no_reset(&other.absolute_re);
408        self.absolute_im.merge_samples_no_reset(&other.absolute_im);
409    }
410
411    pub(crate) fn update_iter(&mut self, use_weighted_average: bool) {
412        self.re.update_iter(use_weighted_average);
413        self.im.update_iter(use_weighted_average);
414        self.absolute_re.update_iter(use_weighted_average);
415        self.absolute_im.update_iter(use_weighted_average);
416    }
417
418    pub(crate) fn max_weight_rows(
419        &self,
420        slot_meta: &SlotMeta,
421        discrete_axis_labels: &[String],
422    ) -> Vec<[String; 3]> {
423        let max_evals = [
424            &self.re.max_eval_positive,
425            &self.re.max_eval_negative,
426            &self.im.max_eval_positive,
427            &self.im.max_eval_negative,
428        ];
429
430        let max_eval_samples = [
431            &self.re.max_eval_positive_xs,
432            &self.re.max_eval_negative_xs,
433            &self.im.max_eval_positive_xs,
434            &self.im.max_eval_negative_xs,
435        ];
436
437        let sign_strs = ["+", "-", "+", "-"];
438        let phase_strs = ["re", "re", "im", "im"];
439
440        izip!(
441            max_evals.iter(),
442            max_eval_samples.iter(),
443            sign_strs.iter(),
444            phase_strs.iter()
445        )
446        .filter_map(|(max_eval, max_eval_sample, sign_str, phase_str)| {
447            if max_eval.is_non_zero() {
448                Some([
449                    format!(
450                        "{} {} [{}] ",
451                        slot_label(slot_meta),
452                        format!("{:<2}", phase_str).blue(),
453                        format!("{:<1}", sign_str).blue()
454                    ),
455                    format!("{:+.16e}", max_eval),
456                    if let Some(sample) = max_eval_sample {
457                        format_max_eval_sample(sample, discrete_axis_labels, &[])
458                    } else {
459                        "N/A".to_string()
460                    },
461                ])
462            } else {
463                None
464            }
465        })
466        .collect()
467    }
468}
469
470impl DiscreteGridAccumulatorSummary {
471    fn from_grid(grid: &Grid<F<f64>>) -> Option<Self> {
472        match grid {
473            Grid::Discrete(discrete_grid) => Some(Self {
474                bins: discrete_grid
475                    .bins
476                    .iter()
477                    .map(|bin| DiscreteGridBinAccumulatorSummary {
478                        accumulator: StatisticsAccumulator::new(),
479                        sub_summary: bin
480                            .sub_grid
481                            .as_ref()
482                            .and_then(Self::from_grid)
483                            .map(Box::new),
484                    })
485                    .collect(),
486            }),
487            Grid::Continuous(_) | Grid::Uniform(_, _) => None,
488        }
489    }
490
491    fn merge_iteration_grid(&mut self, grid: &Grid<F<f64>>) {
492        let Grid::Discrete(discrete_grid) = grid else {
493            return;
494        };
495
496        for (summary_bin, grid_bin) in self.bins.iter_mut().zip(discrete_grid.bins.iter()) {
497            summary_bin
498                .accumulator
499                .merge_samples_no_reset(&grid_bin.accumulator);
500            if let (Some(sub_summary), Some(sub_grid)) =
501                (summary_bin.sub_summary.as_mut(), grid_bin.sub_grid.as_ref())
502            {
503                sub_summary.merge_iteration_grid(sub_grid);
504            }
505        }
506    }
507
508    fn from_shallow_grid(grid: &DiscreteGrid<F<f64>>) -> Self {
509        Self {
510            bins: grid
511                .bins
512                .iter()
513                .map(|_| DiscreteGridBinAccumulatorSummary {
514                    accumulator: StatisticsAccumulator::new(),
515                    sub_summary: None,
516                })
517                .collect(),
518        }
519    }
520
521    fn merge_shallow_iteration_grid(&mut self, grid: &DiscreteGrid<F<f64>>) {
522        assert_eq!(
523            self.bins.len(),
524            grid.bins.len(),
525            "absolute monitor grid shape changed unexpectedly"
526        );
527        for (summary_bin, grid_bin) in self.bins.iter_mut().zip(&grid.bins) {
528            assert!(
529                summary_bin.sub_summary.is_none() && grid_bin.sub_grid.is_none(),
530                "absolute monitor grids must not contain descendants"
531            );
532            summary_bin
533                .accumulator
534                .merge_samples_no_reset(&grid_bin.accumulator);
535        }
536    }
537
538    fn update_iter(&mut self) {
539        for bin in &mut self.bins {
540            bin.accumulator.update_iter(false);
541            if let Some(sub_summary) = bin.sub_summary.as_mut() {
542                sub_summary.update_iter();
543            }
544        }
545    }
546
547    fn first_non_trivial_breakdown(
548        &self,
549        metadata: &PersistedDiscreteBreakdownMetadata,
550        pdfs: &[F<f64>],
551    ) -> Option<DiscreteBreakdown> {
552        (!self.bins.is_empty()).then(|| DiscreteBreakdown {
553            axis_label: metadata.axis_label.clone(),
554            fixed_coordinates: metadata.fixed_coordinates.clone(),
555            entries: self
556                .bins
557                .iter()
558                .enumerate()
559                .map(|(bin_index, bin)| DiscreteBreakdownEntry {
560                    bin_index,
561                    bin_label: metadata.bin_labels.get(bin_index).cloned(),
562                    pdf: pdfs.get(bin_index).copied().unwrap_or(F(0.0)),
563                    value: bin.accumulator.avg,
564                    error: bin.accumulator.err,
565                    chi_sq: bin.accumulator.chi_sq,
566                    processed_samples: bin.accumulator.processed_samples,
567                })
568                .collect(),
569        })
570    }
571}
572
573struct IntegralResultCells {
574    integrand: String,
575    value: String,
576    relative_error: String,
577    chi_sq: String,
578    delta_sigma: Option<String>,
579    delta_percent: Option<String>,
580    mwi: String,
581}
582
583const DEFAULT_MAX_SHARED_TABLE_WIDTH: usize = 250;
584
585fn slot_label(slot_meta: &SlotMeta) -> String {
586    format!("itg {}", slot_meta.key())
587}
588
589fn slot_key_label(slot_meta: &SlotMeta) -> String {
590    slot_meta.key()
591}
592
593fn format_max_eval_coordinate(value: F<f64>) -> String {
594    let formatted = format!("{:.16e}", value.0);
595    let Some((mantissa, exponent)) = formatted.rsplit_once('e') else {
596        return formatted;
597    };
598    let exponent = exponent.parse::<i32>().unwrap_or_default();
599    format!("{mantissa}e{exponent:+03}")
600}
601
602fn format_max_eval_coordinates(xs: &[F<f64>]) -> String {
603    if xs.len() <= 3 {
604        return format!(
605            "[ {} ]",
606            xs.iter()
607                .map(|value| format_max_eval_coordinate(*value))
608                .join(" ")
609        );
610    }
611
612    let rows = xs
613        .chunks(3)
614        .map(|chunk| {
615            chunk
616                .iter()
617                .map(|value| format_max_eval_coordinate(*value))
618                .join(" ")
619        })
620        .join("\n");
621    format!("[\n{rows} ]")
622}
623
624pub(crate) fn discrete_axis_label(axis_labels: &[String], depth: usize) -> &str {
625    axis_labels.get(depth).map(String::as_str).unwrap_or("idx")
626}
627
628fn append_max_eval_sample_parts(
629    sample: &Sample<F<f64>>,
630    axis_labels: &[String],
631    depth: usize,
632    parts: &mut Vec<String>,
633) {
634    match sample {
635        Sample::Continuous(_, xs) => {
636            parts.push(format!("xs: {}", format_max_eval_coordinates(xs)));
637        }
638        Sample::Discrete(_, index, Some(nested_sample)) => {
639            parts.push(format!(
640                "{}: {}",
641                discrete_axis_label(axis_labels, depth),
642                index
643            ));
644            append_max_eval_sample_parts(nested_sample, axis_labels, depth + 1, parts);
645        }
646        Sample::Discrete(_, index, None) => {
647            parts.push(format!(
648                "{}: {}",
649                discrete_axis_label(axis_labels, depth),
650                index
651            ));
652        }
653        Sample::Uniform(_, indices, xs) => {
654            for (offset, index) in indices.iter().enumerate() {
655                parts.push(format!(
656                    "{}: {}",
657                    discrete_axis_label(axis_labels, depth + offset),
658                    index
659                ));
660            }
661            parts.push(format!("xs: {}", format_max_eval_coordinates(xs)));
662        }
663    }
664}
665
666fn format_max_eval_sample(
667    sample: &Sample<F<f64>>,
668    axis_labels: &[String],
669    prefix_path: &[usize],
670) -> String {
671    let mut parts = prefix_path
672        .iter()
673        .enumerate()
674        .map(|(depth, index)| format!("{}: {}", discrete_axis_label(axis_labels, depth), index))
675        .collect_vec();
676    append_max_eval_sample_parts(sample, axis_labels, prefix_path.len(), &mut parts);
677    if parts.is_empty() {
678        String::from("N/A")
679    } else {
680        parts.join(", ")
681    }
682}
683
684fn format_iteration_points(points: usize) -> String {
685    format_abbreviated_count(points)
686}
687
688fn format_total_points(points: usize) -> String {
689    format_abbreviated_count(points)
690}
691
692#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
693enum ComponentKind {
694    Real,
695    Imag,
696}
697
698impl ComponentKind {
699    fn all_for_display(display: IntegrationStatusPhaseDisplay) -> Vec<Self> {
700        let mut components = Vec::new();
701        if display.shows_real() {
702            components.push(Self::Real);
703        }
704        if display.shows_imag() {
705            components.push(Self::Imag);
706        }
707        components
708    }
709
710    fn tag(self) -> &'static str {
711        match self {
712            Self::Real => "re",
713            Self::Imag => "im",
714        }
715    }
716
717    fn colorized_tag(self) -> String {
718        self.tag().blue().bold().to_string()
719    }
720}
721
722#[derive(Clone, Copy, Debug, Eq, PartialEq)]
723enum ContributionKind {
724    All,
725    Sum,
726    Bin(usize),
727}
728
729impl ContributionKind {
730    fn label(self, integration_state: &IntegrationState) -> String {
731        match self {
732            Self::All => "All".bold().green().to_string(),
733            Self::Sum => "Sum".bold().green().to_string(),
734            Self::Bin(bin_index) => contribution_bin_label(integration_state, bin_index),
735        }
736    }
737}
738
739#[derive(Clone, Debug)]
740struct DiscreteLevelContext {
741    path: Vec<usize>,
742    pdfs: Vec<F<f64>>,
743}
744
745fn first_non_trivial_discrete_path(grid: &Grid<F<f64>>) -> Option<Vec<usize>> {
746    match grid {
747        Grid::Discrete(discrete_grid) => {
748            if discrete_grid.bins.len() > 1 {
749                Some(Vec::new())
750            } else {
751                discrete_grid
752                    .bins
753                    .first()
754                    .and_then(|bin| bin.sub_grid.as_ref())
755                    .and_then(first_non_trivial_discrete_path)
756                    .map(|mut path| {
757                        path.insert(0, 0);
758                        path
759                    })
760            }
761        }
762        Grid::Continuous(_) | Grid::Uniform(_, _) => None,
763    }
764}
765
766fn discrete_grid_at_path<'a>(
767    grid: &'a Grid<F<f64>>,
768    path: &[usize],
769) -> Option<&'a DiscreteGrid<F<f64>>> {
770    match (grid, path.split_first()) {
771        (Grid::Discrete(discrete_grid), None) => Some(discrete_grid),
772        (Grid::Discrete(discrete_grid), Some((bin_index, rest))) => discrete_grid
773            .bins
774            .get(*bin_index)?
775            .sub_grid
776            .as_ref()
777            .and_then(|sub_grid| discrete_grid_at_path(sub_grid, rest)),
778        (Grid::Continuous(_) | Grid::Uniform(_, _), _) => None,
779    }
780}
781
782fn shallow_discrete_monitor_grid(
783    grid: &Grid<F<f64>>,
784    path: &[usize],
785) -> Result<DiscreteGrid<F<f64>>> {
786    let source = discrete_grid_at_path(grid, path).ok_or_else(|| {
787        Report::msg(format!(
788            "monitored discrete path {path:?} does not match the sampling grid"
789        ))
790    })?;
791    if source.bins.is_empty() {
792        return Err(Report::msg(format!(
793            "monitored discrete path {path:?} resolves to an empty grid"
794        )));
795    }
796
797    let mut monitor =
798        DiscreteGrid::new(vec![None; source.bins.len()], F(1.0), false).map_err(Report::msg)?;
799    for (monitor_bin, source_bin) in monitor.bins.iter_mut().zip(&source.bins) {
800        monitor_bin.pdf = source_bin.pdf;
801    }
802    Ok(monitor)
803}
804
805fn discrete_sample_at_path<'a>(
806    sample: &'a Sample<F<f64>>,
807    path: &[usize],
808) -> Result<&'a Sample<F<f64>>> {
809    let mut local_sample = sample;
810    for (depth, expected_index) in path.iter().copied().enumerate() {
811        let Sample::Discrete(_, sampled_index, sub_sample) = local_sample else {
812            return Err(Report::msg(format!(
813                "sample does not contain the discrete monitor path {path:?} at depth {depth}"
814            )));
815        };
816        if *sampled_index != expected_index {
817            return Err(Report::msg(format!(
818                "sample index {sampled_index} does not match monitored path index {expected_index} at depth {depth}"
819            )));
820        }
821        local_sample = sub_sample.as_deref().ok_or_else(|| {
822            Report::msg(format!(
823                "sample ends before the discrete monitor path {path:?} at depth {depth}"
824            ))
825        })?;
826    }
827    Ok(local_sample)
828}
829
830fn add_shallow_monitor_sample(
831    monitor: Option<&mut DiscreteGrid<F<f64>>>,
832    monitored_path: Option<&[usize]>,
833    sample: &Sample<F<f64>>,
834    evaluation: F<f64>,
835) -> Result<()> {
836    let (monitor, path) = match (monitor, monitored_path) {
837        (Some(monitor), Some(path)) => (monitor, path),
838        (None, None) => return Ok(()),
839        _ => {
840            return Err(Report::msg(
841                "absolute monitor grid and monitored path are inconsistent",
842            ));
843        }
844    };
845    let local_sample = discrete_sample_at_path(sample, path)?;
846    let Sample::Discrete(_, bin_index, _) = local_sample else {
847        return Err(Report::msg(format!(
848            "sample at monitored discrete path {path:?} is not discrete"
849        )));
850    };
851    if *bin_index >= monitor.bins.len() {
852        return Err(Report::msg(format!(
853            "sample bin {bin_index} is out of bounds for absolute monitor with {} bins at path {path:?}",
854            monitor.bins.len()
855        )));
856    }
857    monitor
858        .add_training_sample(local_sample, evaluation.abs())
859        .map_err(Report::msg)
860}
861
862fn summary_at_path<'a>(
863    summary: &'a DiscreteGridAccumulatorSummary,
864    path: &[usize],
865) -> Option<&'a DiscreteGridAccumulatorSummary> {
866    match path.split_first() {
867        None => Some(summary),
868        Some((bin_index, rest)) => summary
869            .bins
870            .get(*bin_index)?
871            .sub_summary
872            .as_deref()
873            .and_then(|sub_summary| summary_at_path(sub_summary, rest)),
874    }
875}
876
877fn first_non_trivial_discrete_context(
878    sampling_grid: &Grid<F<f64>>,
879) -> Option<DiscreteLevelContext> {
880    let path = first_non_trivial_discrete_path(sampling_grid)?;
881    let discrete_grid = discrete_grid_at_path(sampling_grid, &path)?;
882    Some(DiscreteLevelContext {
883        path,
884        pdfs: discrete_grid.bins.iter().map(|bin| bin.pdf).collect(),
885    })
886}
887
888fn discrete_axis_labels(sampling: &SamplingSettings) -> Vec<&'static str> {
889    match sampling {
890        SamplingSettings::Default(_) | SamplingSettings::MultiChanneling(_) => Vec::new(),
891        SamplingSettings::DiscreteGraphs(settings) => {
892            let mut labels = vec!["graph"];
893            match &settings.sampling_type {
894                DiscreteGraphSamplingType::Default(_)
895                | DiscreteGraphSamplingType::MultiChanneling(_)
896                | DiscreteGraphSamplingType::TropicalSampling(_) => {
897                    if settings.sample_orientations {
898                        labels.push("orientation");
899                    }
900                }
901                DiscreteGraphSamplingType::SamplingMultiChanneling(_) => {
902                    if settings.sample_orientations {
903                        labels.push("orientation");
904                    }
905                    labels.push("sampling channel");
906                }
907            }
908            labels
909        }
910    }
911}
912
913fn orientation_description(orientation: &EdgeVec<Orientation>) -> String {
914    orientation
915        .iter()
916        .map(|(_, orientation)| match *orientation {
917            Orientation::Default => '+',
918            Orientation::Reversed => '-',
919            Orientation::Undirected => '0',
920        })
921        .collect()
922}
923
924fn graph_group_description<I>(graph_names: I) -> String
925where
926    I: IntoIterator<Item = String>,
927{
928    let graph_names = graph_names.into_iter().collect_vec();
929    if graph_names.len() <= 1 {
930        return graph_names.into_iter().next().unwrap_or_default();
931    }
932
933    format!("[{}]", graph_names.join(","))
934}
935
936fn first_non_trivial_discrete_bin_descriptions_for_process_integrand(
937    integrand: &ProcessIntegrand,
938    path: &[usize],
939    axis_label: &str,
940) -> Option<Vec<String>> {
941    match (integrand, axis_label) {
942        (ProcessIntegrand::Amplitude(integrand), "graph") => {
943            Some(
944                integrand
945                    .data
946                    .graph_group_structure
947                    .iter()
948                    .map(|group| {
949                        graph_group_description(group.into_iter().map(|graph_id| {
950                            integrand.data.graph_terms[graph_id].graph.name.clone()
951                        }))
952                    })
953                    .collect(),
954            )
955        }
956        (ProcessIntegrand::CrossSection(integrand), "graph") => {
957            Some(
958                integrand
959                    .data
960                    .graph_group_structure
961                    .iter()
962                    .map(|group| {
963                        graph_group_description(group.into_iter().map(|graph_id| {
964                            integrand.data.graph_terms[graph_id].graph.name.clone()
965                        }))
966                    })
967                    .collect(),
968            )
969        }
970        (ProcessIntegrand::Amplitude(integrand), "orientation") => {
971            let group_id = GroupId(*path.first()?);
972            let group = integrand.data.graph_group_structure.get(group_id)?;
973            let master = group.master();
974            Some(
975                integrand.data.graph_terms[master]
976                    .orientations
977                    .iter()
978                    .map(orientation_description)
979                    .collect(),
980            )
981        }
982        (ProcessIntegrand::CrossSection(integrand), "orientation") => {
983            let group_id = GroupId(*path.first()?);
984            let group = integrand.data.graph_group_structure.get(group_id)?;
985            let master = group.master();
986            Some(
987                integrand.data.graph_terms[master]
988                    .orientations
989                    .iter()
990                    .map(orientation_description)
991                    .collect(),
992            )
993        }
994        (ProcessIntegrand::Amplitude(integrand), "sampling channel") => {
995            let group_id = GroupId(*path.first()?);
996            let group = integrand.data.graph_group_structure.get(group_id)?;
997            let master = group.master();
998            let graph_term = &integrand.data.graph_terms[master];
999            let parameterization_settings = integrand
1000                .settings
1001                .sampling
1002                .get_parameterization_settings()
1003                .unwrap_or_default();
1004            let channel_ids = graph_term
1005                .sampling_channel_ids(&parameterization_settings)
1006                .ok()?;
1007            Some(
1008                channel_ids
1009                    .iter()
1010                    .map(|&channel_id| {
1011                        graph_term
1012                            .sampling_channel_label(channel_id, &parameterization_settings)
1013                            .ok()
1014                            .flatten()
1015                            .unwrap_or_else(|| format!("#{}", channel_id.index()))
1016                    })
1017                    .collect(),
1018            )
1019        }
1020        (ProcessIntegrand::CrossSection(integrand), "sampling channel") => {
1021            let group_id = GroupId(*path.first()?);
1022            let group = integrand.data.graph_group_structure.get(group_id)?;
1023            let master = group.master();
1024            let graph_term = &integrand.data.graph_terms[master];
1025            let parameterization_settings = integrand
1026                .settings
1027                .sampling
1028                .get_parameterization_settings()
1029                .unwrap_or_default();
1030            let channel_ids = graph_term
1031                .sampling_channel_ids(&parameterization_settings)
1032                .ok()?;
1033            Some(
1034                channel_ids
1035                    .iter()
1036                    .map(|&channel_id| {
1037                        graph_term
1038                            .sampling_channel_label(channel_id, &parameterization_settings)
1039                            .ok()
1040                            .flatten()
1041                            .unwrap_or_else(|| format!("#{}", channel_id.index()))
1042                    })
1043                    .collect(),
1044            )
1045        }
1046        _ => None,
1047    }
1048}
1049
1050fn first_non_trivial_discrete_bin_descriptions_for_integrand(
1051    integrand: &Integrand,
1052    path: &[usize],
1053    axis_label: &str,
1054) -> Option<Vec<String>> {
1055    match integrand {
1056        Integrand::ProcessIntegrand(process_integrand) => {
1057            first_non_trivial_discrete_bin_descriptions_for_process_integrand(
1058                process_integrand,
1059                path,
1060                axis_label,
1061            )
1062        }
1063        #[cfg(test)]
1064        Integrand::TestProbe(_) => None,
1065    }
1066}
1067
1068fn discrete_coordinate_label(
1069    integrand: &Integrand,
1070    path_prefix: &[usize],
1071    axis_label: &str,
1072    bin_index: usize,
1073) -> Option<String> {
1074    first_non_trivial_discrete_bin_descriptions_for_integrand(integrand, path_prefix, axis_label)?
1075        .get(bin_index)
1076        .cloned()
1077}
1078
1079fn build_persisted_discrete_breakdown_metadata(
1080    integrand: &Integrand,
1081    path: &[usize],
1082    discrete_axis_labels: &[String],
1083) -> Option<PersistedDiscreteBreakdownMetadata> {
1084    let axis_label = discrete_axis_labels.get(path.len())?.clone();
1085    let bin_labels =
1086        first_non_trivial_discrete_bin_descriptions_for_integrand(integrand, path, &axis_label)?;
1087
1088    let fixed_coordinates = path
1089        .iter()
1090        .enumerate()
1091        .map(|(axis_index, &bin_index)| {
1092            let fixed_axis_label = discrete_axis_labels.get(axis_index)?.clone();
1093            Some(DiscreteCoordinate {
1094                axis_label: fixed_axis_label.clone(),
1095                bin_index,
1096                bin_label: discrete_coordinate_label(
1097                    integrand,
1098                    &path[..axis_index],
1099                    &fixed_axis_label,
1100                    bin_index,
1101                ),
1102            })
1103        })
1104        .collect::<Option<Vec<_>>>()?;
1105
1106    Some(PersistedDiscreteBreakdownMetadata {
1107        axis_label,
1108        fixed_coordinates,
1109        bin_labels,
1110    })
1111}
1112
1113fn monitored_discrete_layout(
1114    grid: &Grid<F<f64>>,
1115    discrete_axis_labels: &[String],
1116    monitor_explicit_graph_subset: bool,
1117) -> Option<(Vec<usize>, String, usize)> {
1118    let path = if monitor_explicit_graph_subset
1119        && discrete_axis_labels
1120            .first()
1121            .is_some_and(|label| label == "graph")
1122        && matches!(grid, Grid::Discrete(_))
1123    {
1124        Vec::new()
1125    } else {
1126        first_non_trivial_discrete_path(grid)?
1127    };
1128    let axis_label = discrete_axis_labels.get(path.len())?.clone();
1129    let discrete_grid = discrete_grid_at_path(grid, &path)?;
1130    Some((path, axis_label, discrete_grid.bins.len()))
1131}
1132
1133fn coalesce_first_non_trivial_discrete_bin_descriptions(
1134    axis_label: &str,
1135    slot_descriptions: &[(String, Vec<String>)],
1136) -> (Option<Vec<String>>, Option<String>) {
1137    let Some((reference_slot, reference_descriptions)) = slot_descriptions.first() else {
1138        return (None, None);
1139    };
1140
1141    if let Some((slot_key, _)) = slot_descriptions
1142        .iter()
1143        .skip(1)
1144        .find(|(_, descriptions)| descriptions != reference_descriptions)
1145    {
1146        return (
1147            None,
1148            Some(format!(
1149                "Selected integrands do not share the same semantic labels for the monitored discrete dimension '{axis_label}' ({} vs {}). Falling back to raw bin indices in integration reporting.",
1150                reference_slot.blue(),
1151                slot_key.blue()
1152            )),
1153        );
1154    }
1155
1156    (Some(reference_descriptions.clone()), None)
1157}
1158
1159fn resolve_first_non_trivial_discrete_bin_descriptions(
1160    slots: &[IntegrationSlot],
1161    monitored_path: &[usize],
1162    axis_label: &str,
1163) -> (Option<Vec<String>>, Option<String>) {
1164    let Some(slot_descriptions) = slots
1165        .iter()
1166        .map(|slot| {
1167            first_non_trivial_discrete_bin_descriptions_for_integrand(
1168                &slot.integrand,
1169                monitored_path,
1170                axis_label,
1171            )
1172            .map(|descriptions| (slot.meta.key(), descriptions))
1173        })
1174        .collect::<Option<Vec<_>>>()
1175    else {
1176        return (None, None);
1177    };
1178
1179    coalesce_first_non_trivial_discrete_bin_descriptions(axis_label, &slot_descriptions)
1180}
1181
1182fn resolve_monitored_discrete_setup(
1183    sampling_correlation_mode: SamplingCorrelationMode,
1184    slots: &[IntegrationSlot],
1185    sampling_states: &[SamplingSlotState],
1186) -> MonitoredDiscreteSetup {
1187    let Some(reference_state) = sampling_states.first() else {
1188        return MonitoredDiscreteSetup::default();
1189    };
1190    let Some(reference_slot) = slots.first() else {
1191        return MonitoredDiscreteSetup::default();
1192    };
1193    let monitor_explicit_graph_subset = !reference_slot
1194        .settings
1195        .sampling
1196        .selected_graph_names()
1197        .is_empty();
1198
1199    let Some((reference_path, reference_axis_label, reference_bin_count)) =
1200        monitored_discrete_layout(
1201            &reference_state.grid,
1202            &reference_state.discrete_axis_labels,
1203            monitor_explicit_graph_subset,
1204        )
1205    else {
1206        return MonitoredDiscreteSetup::default();
1207    };
1208
1209    if sampling_correlation_mode == SamplingCorrelationMode::Uncorrelated {
1210        for (slot_index, sampling_state) in sampling_states.iter().enumerate().skip(1) {
1211            let Some((path, axis_label, bin_count)) = monitored_discrete_layout(
1212                &sampling_state.grid,
1213                &sampling_state.discrete_axis_labels,
1214                !slots[slot_index]
1215                    .settings
1216                    .sampling
1217                    .selected_graph_names()
1218                    .is_empty(),
1219            ) else {
1220                return MonitoredDiscreteSetup {
1221                    warning: Some(
1222                        "Selected integrands do not all expose a compatible first non-trivial monitored discrete dimension. Shared discrete-bin monitoring tables will be disabled."
1223                            .to_string(),
1224                    ),
1225                    ..MonitoredDiscreteSetup::default()
1226                };
1227            };
1228
1229            if path != reference_path
1230                || axis_label != reference_axis_label
1231                || bin_count != reference_bin_count
1232            {
1233                return MonitoredDiscreteSetup {
1234                    warning: Some(format!(
1235                        "Selected integrands do not share a compatible first non-trivial monitored discrete layout (mismatch at {}). Shared discrete-bin monitoring tables will be disabled.",
1236                        slots[slot_index].meta.key().blue()
1237                    )),
1238                    ..MonitoredDiscreteSetup::default()
1239                };
1240            }
1241        }
1242    }
1243
1244    let (descriptions, label_warning) = resolve_first_non_trivial_discrete_bin_descriptions(
1245        slots,
1246        &reference_path,
1247        &reference_axis_label,
1248    );
1249
1250    MonitoredDiscreteSetup {
1251        path: Some(reference_path),
1252        axis_label: Some(reference_axis_label),
1253        bin_descriptions: descriptions,
1254        warning: label_warning,
1255    }
1256}
1257
1258fn render_orientation_description(description: &str) -> String {
1259    description
1260        .chars()
1261        .map(|sign| match sign {
1262            '+' => "+".green().bold().to_string(),
1263            '-' => "-".red().bold().to_string(),
1264            '0' => "0".dimmed().to_string(),
1265            other => other.to_string(),
1266        })
1267        .join("")
1268}
1269
1270fn render_graph_description(description: &str) -> String {
1271    if let Some(inner) = description
1272        .strip_prefix('[')
1273        .and_then(|trimmed| trimmed.strip_suffix(']'))
1274    {
1275        let names = inner
1276            .split(',')
1277            .filter(|name| !name.is_empty())
1278            .collect_vec();
1279        if names.is_empty() {
1280            return description.bold().green().to_string();
1281        }
1282
1283        let rendered = names
1284            .into_iter()
1285            .enumerate()
1286            .map(|(index, name)| {
1287                if index == 0 {
1288                    name.bold().green().to_string()
1289                } else {
1290                    name.bold().blue().to_string()
1291                }
1292            })
1293            .join(",");
1294        return format!("[{rendered}]");
1295    }
1296
1297    description.bold().green().to_string()
1298}
1299
1300fn render_bin_description(axis_label: &str, description: &str) -> String {
1301    match axis_label {
1302        "orientation" => render_orientation_description(description),
1303        "graph" => render_graph_description(description),
1304        _ => description.bold().green().to_string(),
1305    }
1306}
1307
1308fn contribution_bin_label(integration_state: &IntegrationState, bin_index: usize) -> String {
1309    if let (Some(axis_label), Some(descriptions)) = (
1310        integration_state
1311            .first_non_trivial_discrete_label
1312            .as_deref(),
1313        integration_state
1314            .first_non_trivial_discrete_bin_descriptions
1315            .as_ref(),
1316    ) && let Some(description) = descriptions.get(bin_index)
1317    {
1318        return format!(
1319            "#{bin_index}: {}",
1320            render_bin_description(axis_label, description)
1321        );
1322    }
1323
1324    format!("#{bin_index}")
1325}
1326
1327fn contribution_header_label(
1328    integration_state: &IntegrationState,
1329    discrete_monitoring_enabled: bool,
1330) -> String {
1331    if discrete_monitoring_enabled
1332        && let Some(label) = integration_state
1333            .first_non_trivial_discrete_label
1334            .as_deref()
1335    {
1336        return format!("Contribution (idx={label})")
1337            .bold()
1338            .blue()
1339            .to_string();
1340    }
1341
1342    "Contribution".bold().blue().to_string()
1343}
1344
1345fn format_percentage_sig(value: f64, significant_digits: usize) -> String {
1346    if !value.is_finite() {
1347        return "None".red().to_string();
1348    }
1349
1350    if value == 0.0 {
1351        let decimals = significant_digits.saturating_sub(1);
1352        return format!("{:.*}%", decimals, 0.0);
1353    }
1354
1355    let abs_value = value.abs();
1356    let exponent = abs_value.log10().floor() as i32;
1357    if exponent < -2 || exponent >= significant_digits as i32 {
1358        return format!("{:.*e}%", significant_digits.saturating_sub(1), value);
1359    }
1360
1361    let decimals = (significant_digits as i32 - exponent - 1).max(0) as usize;
1362    format!("{value:.decimals$}%")
1363}
1364
1365#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1366enum UncertaintyNotation {
1367    Dynamic,
1368    Scientific,
1369}
1370
1371fn format_significant_percentage(
1372    value: f64,
1373    significant_digits: usize,
1374    scientific_threshold: Option<(f64, f64)>,
1375) -> String {
1376    if !value.is_finite() {
1377        return "None".red().to_string();
1378    }
1379
1380    if value == 0.0 {
1381        return format!("{:.*}%", significant_digits.saturating_sub(1), 0.0);
1382    }
1383
1384    let abs_value = value.abs();
1385    if scientific_threshold.is_some_and(|(upper, lower)| abs_value >= upper || abs_value < lower) {
1386        return format!("{:.*e}%", significant_digits.saturating_sub(1), value);
1387    }
1388
1389    let exponent = abs_value.log10().floor() as i32;
1390    let decimals = (significant_digits as i32 - exponent - 1).max(0) as usize;
1391    format!("{value:.decimals$}%")
1392}
1393
1394fn should_use_scientific_uncertainty_notation(avg: F<f64>, err: F<f64>) -> bool {
1395    let avg_abs = avg.abs().0;
1396    let err_abs = err.abs().0;
1397    avg_abs >= 1e6
1398        || (avg_abs != 0.0 && avg_abs < 1e-5)
1399        || (avg.is_zero() && !(1e-4..1e5).contains(&err_abs))
1400}
1401
1402fn format_uncertainty_with_notation(
1403    avg: F<f64>,
1404    err: F<f64>,
1405    notation: UncertaintyNotation,
1406) -> String {
1407    if !matches!(notation, UncertaintyNotation::Scientific)
1408        && !should_use_scientific_uncertainty_notation(avg, err)
1409    {
1410        return utils::format_uncertainty(avg, err);
1411    }
1412
1413    if !avg.0.is_finite() || !err.0.is_finite() {
1414        return utils::format_uncertainty(avg, err);
1415    }
1416
1417    let exponent = if avg.is_non_zero() {
1418        avg.abs().0.log10().floor() as i32
1419    } else if err.is_non_zero() {
1420        err.abs().0.log10().floor() as i32
1421    } else {
1422        0
1423    };
1424    let scale = 10_f64.powi(exponent);
1425    let mantissa = utils::format_uncertainty(F(avg.0 / scale), F(err.0 / scale));
1426    format!("{mantissa}e{exponent}")
1427}
1428
1429fn format_signed_uncertainty(avg: F<f64>, err: F<f64>, notation: UncertaintyNotation) -> String {
1430    let formatted = format_uncertainty_with_notation(avg, err, notation);
1431    if avg.0.is_sign_negative() {
1432        formatted
1433    } else {
1434        format!("+{formatted}")
1435    }
1436}
1437
1438fn format_abbreviated_count(value: usize) -> String {
1439    if value < 1_000 {
1440        return value.to_string();
1441    }
1442
1443    let value = value as f64;
1444    if value < 1_000_000.0 {
1445        return format!("{:.2}K", value / 1_000.0);
1446    }
1447    if value < 1_000_000_000.0 {
1448        return format!("{:.2}M", value / 1_000_000.0);
1449    }
1450    if value < 1_000_000_000_000.0 {
1451        return format!("{:.2}B", value / 1_000_000_000.0);
1452    }
1453
1454    format!("{:.3}T", value / 1_000_000_000_000.0)
1455}
1456
1457fn build_integral_result_cells(
1458    itg: &StatisticsAccumulator<F<f64>>,
1459    slot_meta: &SlotMeta,
1460    i_iter: usize,
1461    tag: &str,
1462    trgt: Option<F<f64>>,
1463) -> IntegralResultCells {
1464    let relative_error = format_relative_error_cell(itg);
1465    let chi_sq = format_chi_sq_cell(itg, i_iter);
1466    let (delta_sigma, delta_percent) = format_delta_cells(itg, trgt);
1467    let mwi = format_mwi_cell(itg);
1468
1469    IntegralResultCells {
1470        integrand: format!(
1471            "{} {}:",
1472            slot_label(slot_meta),
1473            format!("{:-2}", tag).blue().bold(),
1474        ),
1475        value: format_signed_uncertainty(itg.avg, itg.err, UncertaintyNotation::Scientific)
1476            .blue()
1477            .bold()
1478            .to_string(),
1479        relative_error,
1480        chi_sq,
1481        delta_sigma,
1482        delta_percent,
1483        mwi,
1484    }
1485}
1486
1487fn format_relative_error_cell(itg: &StatisticsAccumulator<F<f64>>) -> String {
1488    format_relative_error_from_estimate(itg.avg, itg.err)
1489}
1490
1491fn format_relative_error_from_estimate(avg: F<f64>, err: F<f64>) -> String {
1492    if avg.is_zero() {
1493        return String::new();
1494    }
1495
1496    let formatted =
1497        format_significant_percentage((err / avg).abs().0 * 100.0, 3, Some((1.0e4, 1.0e-4)));
1498    if (err / avg).abs().0 > 0.01 {
1499        formatted.red().to_string()
1500    } else {
1501        formatted.green().to_string()
1502    }
1503}
1504
1505fn format_chi_sq_cell(itg: &StatisticsAccumulator<F<f64>>, i_iter: usize) -> String {
1506    let chi_sq = format!("{:.3}", itg.chi_sq.0 / (i_iter as f64));
1507    if itg.chi_sq / F::<f64>::new_from_usize(i_iter) > F(5.) {
1508        chi_sq.red().to_string()
1509    } else {
1510        chi_sq
1511    }
1512}
1513
1514fn format_delta_cells(
1515    itg: &StatisticsAccumulator<F<f64>>,
1516    trgt: Option<F<f64>>,
1517) -> (Option<String>, Option<String>) {
1518    format_delta_cells_from_estimate(itg.avg, itg.err, trgt)
1519}
1520
1521fn format_delta_cells_from_estimate(
1522    avg: F<f64>,
1523    err: F<f64>,
1524    trgt: Option<F<f64>>,
1525) -> (Option<String>, Option<String>) {
1526    let Some(t) = trgt else {
1527        return (None, None);
1528    };
1529
1530    let delta_in_sigmas = if err.is_zero() {
1531        0.0
1532    } else {
1533        (t - avg).abs().0 / err.0
1534    };
1535    let delta_in_percent = if t.abs().is_non_zero() {
1536        (t - avg).abs().0 / t.abs().0 * 100.
1537    } else {
1538        0.
1539    };
1540    let is_outside_target =
1541        delta_in_sigmas > 5. || (t.abs().is_non_zero() && ((t - avg).abs() / t.abs()).0 > 0.01);
1542    let sigma_text = format!("Δ = {:.3}σ", delta_in_sigmas);
1543    let percent_text = format!("Δ = {:.3}%", delta_in_percent);
1544    if is_outside_target {
1545        (
1546            Some(sigma_text.red().to_string()),
1547            Some(percent_text.red().to_string()),
1548        )
1549    } else {
1550        (
1551            Some(sigma_text.green().to_string()),
1552            Some(percent_text.green().to_string()),
1553        )
1554    }
1555}
1556
1557fn format_mwi_cell(itg: &StatisticsAccumulator<F<f64>>) -> String {
1558    let mwi_value = max_weight_impact(itg);
1559    let formatted = format!("{:.4e}", mwi_value.0);
1560    if mwi_value > F(1.) {
1561        formatted.red().to_string()
1562    } else {
1563        formatted
1564    }
1565}
1566
1567fn max_weight_impact(itg: &StatisticsAccumulator<F<f64>>) -> F<f64> {
1568    if itg.avg.abs().0 == 0. || itg.processed_samples == 0 {
1569        return F(0.0);
1570    }
1571
1572    itg.max_eval_negative.abs().max(itg.max_eval_positive.abs())
1573        / (itg.avg.abs() * F::<f64>::new_from_usize(itg.processed_samples))
1574}
1575
1576fn build_table_component_result(
1577    slot_meta: &SlotMeta,
1578    iter: usize,
1579    component: &str,
1580    accumulator: &StatisticsAccumulator<F<f64>>,
1581    target_component: Option<F<f64>>,
1582) -> IntegrationTableComponentResult {
1583    let cells =
1584        build_integral_result_cells(accumulator, slot_meta, iter, component, target_component);
1585    IntegrationTableComponentResult {
1586        component: component.to_string(),
1587        value: accumulator.avg,
1588        error: accumulator.err,
1589        relative_error_percent: accumulator
1590            .avg
1591            .is_non_zero()
1592            .then(|| (accumulator.err / accumulator.avg).abs().0 * 100.0),
1593        chi_sq_per_dof: if iter > 0 {
1594            accumulator.chi_sq.0 / (iter as f64)
1595        } else {
1596            0.0
1597        },
1598        target_delta_sigma: cells.delta_sigma.as_ref().map(|_| {
1599            if accumulator.err.is_zero() {
1600                0.0
1601            } else if let Some(target_value) = target_component {
1602                (target_value - accumulator.avg).abs().0 / accumulator.err.0
1603            } else {
1604                0.0
1605            }
1606        }),
1607        target_delta_percent: target_component.map(|target_value| {
1608            if target_value.is_zero() {
1609                0.0
1610            } else {
1611                (target_value - accumulator.avg).abs().0 / target_value.abs().0 * 100.0
1612            }
1613        }),
1614        max_weight_impact: max_weight_impact(accumulator).0,
1615    }
1616}
1617
1618fn build_table_result_summary(
1619    slot_meta: &SlotMeta,
1620    accumulator: &ComplexAccumulator,
1621    iter: usize,
1622    target: Option<Complex<F<f64>>>,
1623) -> Vec<IntegrationTableComponentResult> {
1624    [
1625        ("re", &accumulator.re, target.as_ref().map(|value| value.re)),
1626        ("im", &accumulator.im, target.as_ref().map(|value| value.im)),
1627    ]
1628    .into_iter()
1629    .map(|(component, accumulator, target_component)| {
1630        build_table_component_result(slot_meta, iter, component, accumulator, target_component)
1631    })
1632    .collect()
1633}
1634
1635fn build_absolute_table_result_summary(
1636    slot_meta: &SlotMeta,
1637    accumulator: &ComplexAccumulator,
1638    iter: usize,
1639) -> Vec<IntegrationTableComponentResult> {
1640    [
1641        ("|re|", &accumulator.absolute_re),
1642        ("|im|", &accumulator.absolute_im),
1643    ]
1644    .into_iter()
1645    .map(|(component, accumulator)| {
1646        build_table_component_result(slot_meta, iter, component, accumulator, None)
1647    })
1648    .collect()
1649}
1650
1651fn build_max_weight_info_summary(
1652    discrete_axis_labels: &[String],
1653    accumulator: &ComplexAccumulator,
1654) -> Vec<MaxWeightInfoEntry> {
1655    [
1656        (
1657            "re",
1658            "+",
1659            accumulator.re.max_eval_positive,
1660            accumulator.re.max_eval_positive_xs.as_ref(),
1661        ),
1662        (
1663            "re",
1664            "-",
1665            accumulator.re.max_eval_negative,
1666            accumulator.re.max_eval_negative_xs.as_ref(),
1667        ),
1668        (
1669            "im",
1670            "+",
1671            accumulator.im.max_eval_positive,
1672            accumulator.im.max_eval_positive_xs.as_ref(),
1673        ),
1674        (
1675            "im",
1676            "-",
1677            accumulator.im.max_eval_negative,
1678            accumulator.im.max_eval_negative_xs.as_ref(),
1679        ),
1680    ]
1681    .into_iter()
1682    .filter_map(|(component, sign, max_eval, sample)| {
1683        if max_eval.is_zero() {
1684            return None;
1685        }
1686
1687        Some(MaxWeightInfoEntry {
1688            component: component.to_string(),
1689            sign: sign.to_string(),
1690            max_eval,
1691            coordinates: sample
1692                .map(|sample| format_max_eval_sample(sample, discrete_axis_labels, &[])),
1693        })
1694    })
1695    .collect()
1696}
1697
1698fn build_absolute_max_weight_info_summary(
1699    discrete_axis_labels: &[String],
1700    accumulator: &ComplexAccumulator,
1701) -> Vec<MaxWeightInfoEntry> {
1702    [
1703        ("re", &accumulator.absolute_re),
1704        ("im", &accumulator.absolute_im),
1705    ]
1706    .into_iter()
1707    .filter_map(|(component, accumulator)| {
1708        if accumulator.max_eval_positive.is_zero() {
1709            return None;
1710        }
1711        Some(MaxWeightInfoEntry {
1712            component: component.to_string(),
1713            sign: "+".to_string(),
1714            max_eval: accumulator.max_eval_positive,
1715            coordinates: accumulator
1716                .max_eval_positive_xs
1717                .as_ref()
1718                .map(|sample| format_max_eval_sample(sample, discrete_axis_labels, &[])),
1719        })
1720    })
1721    .collect()
1722}
1723
1724#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1725struct LiveIterationProgress {
1726    completed_points: usize,
1727    target_points: usize,
1728}
1729
1730fn build_iteration_status_header_left(
1731    elapsed_time: Duration,
1732    iter: usize,
1733    live_progress: Option<LiveIterationProgress>,
1734) -> String {
1735    let iteration_label = if live_progress.is_some() {
1736        format!("Iteration #{:-4} ( running   )", iter)
1737            .bold()
1738            .green()
1739    } else {
1740        format!("Iteration #{:-4} ( completed )", iter)
1741            .bold()
1742            .green()
1743    };
1744
1745    format!(
1746        "[ {} ] {}",
1747        format!(
1748            "{:^7}",
1749            utils::format_wdhms(elapsed_time.as_secs() as usize)
1750        )
1751        .bold(),
1752        iteration_label,
1753    )
1754}
1755
1756fn build_iteration_status_header_middle(
1757    cur_points: usize,
1758    total_points: usize,
1759    live_progress: Option<LiveIterationProgress>,
1760) -> String {
1761    let per_iteration = if let Some(progress) = live_progress {
1762        let percentage = if progress.target_points == 0 {
1763            String::from("0.0%")
1764        } else {
1765            format!(
1766                "{:.1}%",
1767                (progress.completed_points as f64) / (progress.target_points as f64) * 100.0
1768            )
1769            .green()
1770            .to_string()
1771        };
1772        format!(
1773            "Iteration progress {}/{} {}",
1774            format_iteration_points(progress.completed_points),
1775            format_iteration_points(progress.target_points),
1776            percentage
1777        )
1778    } else if cur_points > 0 {
1779        format!(
1780            "# samples per iteration = {}",
1781            format_iteration_points(cur_points)
1782        )
1783        .blue()
1784        .bold()
1785        .to_string()
1786    } else {
1787        String::new()
1788    };
1789
1790    let total = format!("# samples total = {}", format_total_points(total_points))
1791        .bold()
1792        .green()
1793        .to_string();
1794    if per_iteration.is_empty() {
1795        total
1796    } else {
1797        format!("{per_iteration} {total}")
1798    }
1799}
1800
1801fn build_iteration_status_header_tail(
1802    cores: usize,
1803    elapsed_time: Duration,
1804    n_samples_evaluated: usize,
1805) -> String {
1806    let sample_core_time = if n_samples_evaluated == 0 {
1807        "N/A /sample/core".red().to_string()
1808    } else {
1809        format!(
1810            "{} /sample/core",
1811            utils::format_evaluation_time_from_f64(
1812                elapsed_time.as_secs_f64() / (n_samples_evaluated as f64) * (cores as f64),
1813            )
1814            .bold()
1815            .green()
1816        )
1817    };
1818
1819    format!(
1820        "{sample_core_time} {}",
1821        format!("({cores} cores)").bold().blue()
1822    )
1823}
1824
1825fn max_weight_row_descriptors(
1826    phase_display: IntegrationStatusPhaseDisplay,
1827) -> Vec<(ComponentKind, &'static str, bool)> {
1828    let mut rows = Vec::new();
1829    if phase_display.shows_real() {
1830        rows.push((ComponentKind::Real, "+", true));
1831        rows.push((ComponentKind::Real, "-", false));
1832    }
1833    if phase_display.shows_imag() {
1834        rows.push((ComponentKind::Imag, "+", true));
1835        rows.push((ComponentKind::Imag, "-", false));
1836    }
1837    rows
1838}
1839
1840type MaxEvalEntry<'a> = Option<(F<f64>, Option<&'a Sample<F<f64>>>)>;
1841
1842fn max_eval_entry(accumulator: &StatisticsAccumulator<F<f64>>, positive: bool) -> MaxEvalEntry<'_> {
1843    let (value, sample) = if positive {
1844        (
1845            accumulator.max_eval_positive,
1846            accumulator.max_eval_positive_xs.as_ref(),
1847        )
1848    } else {
1849        (
1850            accumulator.max_eval_negative,
1851            accumulator.max_eval_negative_xs.as_ref(),
1852        )
1853    };
1854
1855    if value.is_zero() {
1856        None
1857    } else {
1858        Some((value, sample))
1859    }
1860}
1861
1862/// struct to keep track of state, used in the havana_integrate function
1863/// the idea is to save this to disk after each iteration, so that the integration can be resumed
1864#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
1865pub struct IntegrationState {
1866    pub num_points: usize,
1867    #[bincode(with_serde)]
1868    pub all_integrals: Vec<ComplexAccumulator>,
1869    #[bincode(with_serde)]
1870    slot_re_summaries: Vec<Option<DiscreteGridAccumulatorSummary>>,
1871    #[bincode(with_serde)]
1872    slot_im_summaries: Vec<Option<DiscreteGridAccumulatorSummary>>,
1873    #[bincode(with_serde)]
1874    pub(crate) slot_absolute_re_summaries: Vec<Option<DiscreteGridAccumulatorSummary>>,
1875    #[bincode(with_serde)]
1876    pub(crate) slot_absolute_im_summaries: Vec<Option<DiscreteGridAccumulatorSummary>>,
1877    pub stats: StatisticsCounter,
1878    pub slot_stats: Vec<StatisticsCounter>,
1879    pub slot_metas: Vec<SlotMeta>,
1880    pub sampling_correlation_mode: SamplingCorrelationMode,
1881    sampling_states: Vec<SamplingSlotState>,
1882    monitored_discrete_path: Option<Vec<usize>>,
1883    pub first_non_trivial_discrete_label: Option<String>,
1884    pub first_non_trivial_discrete_bin_descriptions: Option<Vec<String>>,
1885    slot_first_non_trivial_discrete_breakdown_metadata:
1886        Vec<Option<PersistedDiscreteBreakdownMetadata>>,
1887    pub iter: usize,
1888    pub elapsed_seconds: f64,
1889    pub n_cores: usize,
1890}
1891
1892impl IntegrationState {
1893    fn new_from_settings(
1894        sampling_correlation_mode: SamplingCorrelationMode,
1895        sampling_states: Vec<SamplingSlotState>,
1896        slot_metas: Vec<SlotMeta>,
1897        monitored_discrete_path: Option<Vec<usize>>,
1898        first_non_trivial_discrete_label: Option<String>,
1899        first_non_trivial_discrete_bin_descriptions: Option<Vec<String>>,
1900        slot_first_non_trivial_discrete_breakdown_metadata: Vec<
1901            Option<PersistedDiscreteBreakdownMetadata>,
1902        >,
1903    ) -> Self {
1904        let num_points = 0;
1905        let iter = 0;
1906        let all_integrals = vec![ComplexAccumulator::new(); slot_metas.len()];
1907        let slot_re_summaries = (0..slot_metas.len())
1908            .map(|slot_index| {
1909                DiscreteGridAccumulatorSummary::from_grid(
1910                    &sampling_states[sampling_correlation_mode.state_index(slot_index)].grid,
1911                )
1912            })
1913            .collect();
1914        let slot_im_summaries = (0..slot_metas.len())
1915            .map(|slot_index| {
1916                DiscreteGridAccumulatorSummary::from_grid(
1917                    &sampling_states[sampling_correlation_mode.state_index(slot_index)].grid,
1918                )
1919            })
1920            .collect();
1921        let absolute_summary_for_slot = |slot_index| {
1922            monitored_discrete_path.as_deref().map(|path| {
1923                let grid = &sampling_states[sampling_correlation_mode.state_index(slot_index)].grid;
1924                let discrete_grid = discrete_grid_at_path(grid, path)
1925                    .expect("validated monitored discrete path must match every sampling grid");
1926                DiscreteGridAccumulatorSummary::from_shallow_grid(discrete_grid)
1927            })
1928        };
1929        let slot_absolute_re_summaries = (0..slot_metas.len())
1930            .map(&absolute_summary_for_slot)
1931            .collect();
1932        let slot_absolute_im_summaries = (0..slot_metas.len())
1933            .map(absolute_summary_for_slot)
1934            .collect();
1935        let stats = StatisticsCounter::new_empty();
1936        let slot_stats = vec![StatisticsCounter::new_empty(); slot_metas.len()];
1937
1938        Self {
1939            num_points,
1940            all_integrals,
1941            slot_re_summaries,
1942            slot_im_summaries,
1943            slot_absolute_re_summaries,
1944            slot_absolute_im_summaries,
1945            stats,
1946            slot_stats,
1947            slot_metas,
1948            sampling_correlation_mode,
1949            sampling_states,
1950            monitored_discrete_path,
1951            first_non_trivial_discrete_label,
1952            first_non_trivial_discrete_bin_descriptions,
1953            slot_first_non_trivial_discrete_breakdown_metadata,
1954            iter,
1955            elapsed_seconds: 0.0,
1956            n_cores: 1,
1957        }
1958    }
1959
1960    fn update_iter(&mut self, use_weighted_average: bool) {
1961        self.all_integrals
1962            .iter_mut()
1963            .for_each(|acc| acc.update_iter(use_weighted_average));
1964    }
1965
1966    fn sampling_state_for_slot(&self, slot_index: usize) -> &SamplingSlotState {
1967        &self.sampling_states[self.sampling_correlation_mode.state_index(slot_index)]
1968    }
1969
1970    fn sampling_state_for_slot_mut(&mut self, slot_index: usize) -> &mut SamplingSlotState {
1971        let sampling_state_index = self.sampling_correlation_mode.state_index(slot_index);
1972        &mut self.sampling_states[sampling_state_index]
1973    }
1974
1975    fn monitored_discrete_context_for_slot(
1976        &self,
1977        slot_index: usize,
1978    ) -> Option<DiscreteLevelContext> {
1979        let path = self.monitored_discrete_path.clone()?;
1980        let discrete_grid =
1981            discrete_grid_at_path(&self.sampling_state_for_slot(slot_index).grid, &path)?;
1982        Some(DiscreteLevelContext {
1983            path,
1984            pdfs: discrete_grid.bins.iter().map(|bin| bin.pdf).collect(),
1985        })
1986    }
1987
1988    fn monitored_discrete_context(&self) -> Option<DiscreteLevelContext> {
1989        self.monitored_discrete_context_for_slot(0)
1990    }
1991}
1992
1993struct CoreIterationState {
1994    slot_integrands: Vec<Integrand>,
1995    stats: StatisticsCounter,
1996    slot_stats: Vec<StatisticsCounter>,
1997    integrals: Vec<ComplexAccumulator>,
1998    sampling_correlation_mode: SamplingCorrelationMode,
1999    sampling_states: Vec<CoreSamplingSlotState>,
2000    slot_re_grids: Vec<Grid<F<f64>>>,
2001    slot_im_grids: Vec<Grid<F<f64>>>,
2002    monitored_discrete_path: Option<Vec<usize>>,
2003    slot_absolute_re_grids: Vec<Option<DiscreteGrid<F<f64>>>>,
2004    slot_absolute_im_grids: Vec<Option<DiscreteGrid<F<f64>>>>,
2005    remaining_points: usize,
2006    completed_points: usize,
2007    failed_samples: Vec<(usize, Sample<F<f64>>, EvaluationMetaData)>,
2008}
2009
2010struct CoreSamplingSlotState {
2011    sampling_grid: Grid<F<f64>>,
2012    rng: MonteCarloRng,
2013}
2014
2015impl CoreIterationState {
2016    fn new(
2017        slot_integrands: Vec<Integrand>,
2018        sampling_correlation_mode: SamplingCorrelationMode,
2019        sampling_grid_templates: &[Grid<F<f64>>],
2020        monitored_discrete_path: Option<&[usize]>,
2021        seed: u64,
2022        sample_skip: usize,
2023        remaining_points: usize,
2024    ) -> Self {
2025        let n_slots = slot_integrands.len();
2026        let sampling_states = (0..sampling_correlation_mode.state_count(n_slots))
2027            .map(|sampling_state_index| {
2028                let slot_index = match sampling_correlation_mode {
2029                    SamplingCorrelationMode::Correlated => 0,
2030                    SamplingCorrelationMode::Uncorrelated => sampling_state_index,
2031                };
2032                let mut rng = MonteCarloRng::new(slot_seed(seed, slot_index), 0);
2033                let mut sampling_grid =
2034                    sampling_grid_templates[sampling_state_index].clone_without_samples();
2035
2036                for _ in 0..sample_skip {
2037                    let mut sample = Sample::new();
2038                    sampling_grid.sample(&mut rng, &mut sample);
2039                }
2040
2041                CoreSamplingSlotState { sampling_grid, rng }
2042            })
2043            .collect_vec();
2044        let absolute_monitor_for_slot = |slot_index| {
2045            monitored_discrete_path.map(|path| {
2046                shallow_discrete_monitor_grid(
2047                    &sampling_grid_templates[sampling_correlation_mode.state_index(slot_index)],
2048                    path,
2049                )
2050                .expect("validated monitored discrete path must match every sampling grid")
2051            })
2052        };
2053
2054        Self {
2055            slot_integrands,
2056            stats: StatisticsCounter::new_empty(),
2057            slot_stats: vec![StatisticsCounter::new_empty(); n_slots],
2058            integrals: vec![ComplexAccumulator::new(); n_slots],
2059            sampling_correlation_mode,
2060            sampling_states,
2061            slot_re_grids: (0..n_slots)
2062                .map(|slot_index| {
2063                    sampling_grid_templates[sampling_correlation_mode.state_index(slot_index)]
2064                        .clone_without_samples()
2065                })
2066                .collect(),
2067            slot_im_grids: (0..n_slots)
2068                .map(|slot_index| {
2069                    sampling_grid_templates[sampling_correlation_mode.state_index(slot_index)]
2070                        .clone_without_samples()
2071                })
2072                .collect(),
2073            monitored_discrete_path: monitored_discrete_path.map(|path| path.to_vec()),
2074            slot_absolute_re_grids: (0..n_slots).map(&absolute_monitor_for_slot).collect(),
2075            slot_absolute_im_grids: (0..n_slots).map(absolute_monitor_for_slot).collect(),
2076            remaining_points,
2077            completed_points: 0,
2078            failed_samples: Vec::new(),
2079        }
2080    }
2081
2082    fn evaluate_chunk(
2083        &mut self,
2084        slot_settings: &[&RuntimeSettings],
2085        slot_targets: &[EvaluationTarget<'_>],
2086        iter: usize,
2087        current_max_evals: &[Complex<F<f64>>],
2088        current_integral_estimates: &[Option<(f64, f64)>],
2089        chunk_size: usize,
2090    ) -> Result<usize> {
2091        let n_points = chunk_size.min(self.remaining_points);
2092        if n_points == 0 {
2093            return Ok(0);
2094        }
2095
2096        let chunk_start = Instant::now();
2097        let mut batch_evaluation_time = Duration::ZERO;
2098        let mut batch_stats = StatisticsCounter::new_empty();
2099        let mut processed_points = 0;
2100        let mut total_sample_evaluations = 0;
2101        let monitored_discrete_path = self.monitored_discrete_path.clone();
2102
2103        match self.sampling_correlation_mode {
2104            SamplingCorrelationMode::Correlated => {
2105                let mut samples = Vec::with_capacity(n_points);
2106                for _ in 0..n_points {
2107                    if is_interrupted() {
2108                        break;
2109                    }
2110
2111                    let mut sample = Sample::new();
2112                    let sampling_state = &mut self.sampling_states[0];
2113                    sampling_state
2114                        .sampling_grid
2115                        .sample(&mut sampling_state.rng, &mut sample);
2116                    processed_points += 1;
2117                    samples.push(sample);
2118                }
2119
2120                if processed_points == 0 {
2121                    return Ok(0);
2122                }
2123
2124                let mut slot_results = Vec::with_capacity(self.slot_integrands.len());
2125
2126                for (slot_index, integrand) in self.slot_integrands.iter_mut().enumerate() {
2127                    let evaluation_start = Instant::now();
2128                    let raw_batch = integrand.evaluate_samples_raw_with_estimate(
2129                        &samples,
2130                        slot_targets[slot_index],
2131                        iter,
2132                        false,
2133                        true,
2134                        current_max_evals[slot_index],
2135                        current_integral_estimates[slot_index],
2136                    )?;
2137                    if raw_batch.samples.len() < samples.len() {
2138                        return Ok(0);
2139                    }
2140                    batch_evaluation_time += evaluation_start.elapsed();
2141                    total_sample_evaluations += raw_batch.samples.len();
2142                    batch_stats = batch_stats.merged(&raw_batch.statistics);
2143                    self.slot_stats[slot_index] =
2144                        self.slot_stats[slot_index].merged(&raw_batch.statistics);
2145                    slot_results.push(raw_batch.samples);
2146                }
2147
2148                for (sample_index, sample) in samples.iter().enumerate() {
2149                    for (
2150                        slot_index,
2151                        (
2152                            core_accumulator,
2153                            re_grid,
2154                            im_grid,
2155                            absolute_re_grid,
2156                            absolute_im_grid,
2157                            results,
2158                        ),
2159                    ) in izip!(
2160                        self.integrals.iter_mut(),
2161                        self.slot_re_grids.iter_mut(),
2162                        self.slot_im_grids.iter_mut(),
2163                        self.slot_absolute_re_grids.iter_mut(),
2164                        self.slot_absolute_im_grids.iter_mut(),
2165                        slot_results.iter(),
2166                    )
2167                    .enumerate()
2168                    {
2169                        let result = &results[sample_index];
2170                        if result.evaluation_metadata.is_nan {
2171                            self.failed_samples.push((
2172                                slot_index,
2173                                sample.clone(),
2174                                result.evaluation_metadata.clone(),
2175                            ));
2176                        }
2177                        let jacobian = result.parameterization_jacobian.unwrap_or(F(1.0));
2178                        let effective_integrand_result =
2179                            result.integrand_result * Complex::new_re(jacobian);
2180                        let effective_absolute_result = result
2181                            .absolute_integrand_result
2182                            .map(|value| value * jacobian.abs());
2183
2184                        core_accumulator.add_sample(
2185                            effective_integrand_result,
2186                            effective_absolute_result,
2187                            sample.get_weight(),
2188                            Some(sample),
2189                        );
2190                        re_grid
2191                            .add_training_sample(sample, effective_integrand_result.re)
2192                            .map_err(Report::msg)?;
2193                        im_grid
2194                            .add_training_sample(sample, effective_integrand_result.im)
2195                            .map_err(Report::msg)?;
2196                        add_shallow_monitor_sample(
2197                            absolute_re_grid.as_mut(),
2198                            monitored_discrete_path.as_deref(),
2199                            sample,
2200                            effective_absolute_result
2201                                .map_or(effective_integrand_result.re.abs(), |value| value.re),
2202                        )?;
2203                        add_shallow_monitor_sample(
2204                            absolute_im_grid.as_mut(),
2205                            monitored_discrete_path.as_deref(),
2206                            sample,
2207                            effective_absolute_result
2208                                .map_or(effective_integrand_result.im.abs(), |value| value.im),
2209                        )?;
2210
2211                        if slot_index == 0 {
2212                            let training_eval = match slot_settings[0].integrator.integrated_phase {
2213                                IntegratedPhase::Real => effective_integrand_result.re,
2214                                IntegratedPhase::Imag => effective_integrand_result.im,
2215                                IntegratedPhase::Both => unimplemented!(),
2216                            };
2217
2218                            self.sampling_states[0]
2219                                .sampling_grid
2220                                .add_training_sample(sample, training_eval)
2221                                .map_err(Report::msg)?;
2222                        }
2223                    }
2224                }
2225            }
2226            SamplingCorrelationMode::Uncorrelated => {
2227                for slot_index in 0..self.slot_integrands.len() {
2228                    let mut samples = Vec::with_capacity(n_points);
2229                    for _ in 0..n_points {
2230                        if is_interrupted() {
2231                            break;
2232                        }
2233
2234                        let mut sample = Sample::new();
2235                        let sampling_state = &mut self.sampling_states[slot_index];
2236                        sampling_state
2237                            .sampling_grid
2238                            .sample(&mut sampling_state.rng, &mut sample);
2239                        samples.push(sample);
2240                    }
2241
2242                    if slot_index == 0 {
2243                        processed_points = samples.len();
2244                    } else if samples.len() != processed_points {
2245                        return Err(Report::msg(
2246                            "uncorrelated slot batch sizes diverged unexpectedly",
2247                        ));
2248                    }
2249
2250                    if samples.is_empty() {
2251                        continue;
2252                    }
2253
2254                    let evaluation_start = Instant::now();
2255                    let raw_batch = self.slot_integrands[slot_index]
2256                        .evaluate_samples_raw_with_estimate(
2257                            &samples,
2258                            slot_targets[slot_index],
2259                            iter,
2260                            false,
2261                            true,
2262                            current_max_evals[slot_index],
2263                            current_integral_estimates[slot_index],
2264                        )?;
2265                    if raw_batch.samples.len() < samples.len() {
2266                        return Ok(0);
2267                    }
2268                    batch_evaluation_time += evaluation_start.elapsed();
2269                    total_sample_evaluations += raw_batch.samples.len();
2270                    batch_stats = batch_stats.merged(&raw_batch.statistics);
2271                    self.slot_stats[slot_index] =
2272                        self.slot_stats[slot_index].merged(&raw_batch.statistics);
2273
2274                    for (sample, result) in samples.iter().zip(raw_batch.samples.iter()) {
2275                        if result.evaluation_metadata.is_nan {
2276                            self.failed_samples.push((
2277                                slot_index,
2278                                sample.clone(),
2279                                result.evaluation_metadata.clone(),
2280                            ));
2281                        }
2282                        let jacobian = result.parameterization_jacobian.unwrap_or(F(1.0));
2283                        let effective_integrand_result =
2284                            result.integrand_result * Complex::new_re(jacobian);
2285                        let effective_absolute_result = result
2286                            .absolute_integrand_result
2287                            .map(|value| value * jacobian.abs());
2288
2289                        self.integrals[slot_index].add_sample(
2290                            effective_integrand_result,
2291                            effective_absolute_result,
2292                            sample.get_weight(),
2293                            Some(sample),
2294                        );
2295                        self.slot_re_grids[slot_index]
2296                            .add_training_sample(sample, effective_integrand_result.re)
2297                            .map_err(Report::msg)?;
2298                        self.slot_im_grids[slot_index]
2299                            .add_training_sample(sample, effective_integrand_result.im)
2300                            .map_err(Report::msg)?;
2301                        add_shallow_monitor_sample(
2302                            self.slot_absolute_re_grids[slot_index].as_mut(),
2303                            monitored_discrete_path.as_deref(),
2304                            sample,
2305                            effective_absolute_result
2306                                .map_or(effective_integrand_result.re.abs(), |value| value.re),
2307                        )?;
2308                        add_shallow_monitor_sample(
2309                            self.slot_absolute_im_grids[slot_index].as_mut(),
2310                            monitored_discrete_path.as_deref(),
2311                            sample,
2312                            effective_absolute_result
2313                                .map_or(effective_integrand_result.im.abs(), |value| value.im),
2314                        )?;
2315
2316                        let training_eval =
2317                            match slot_settings[slot_index].integrator.integrated_phase {
2318                                IntegratedPhase::Real => effective_integrand_result.re,
2319                                IntegratedPhase::Imag => effective_integrand_result.im,
2320                                IntegratedPhase::Both => unimplemented!(),
2321                            };
2322                        self.sampling_states[slot_index]
2323                            .sampling_grid
2324                            .add_training_sample(sample, training_eval)
2325                            .map_err(Report::msg)?;
2326                    }
2327                }
2328
2329                if processed_points == 0 {
2330                    return Ok(0);
2331                }
2332            }
2333        }
2334
2335        self.remaining_points -= processed_points;
2336        self.completed_points += processed_points;
2337        self.stats = self.stats.merged(&batch_stats);
2338        self.stats.add_integrator_overhead(
2339            chunk_start.elapsed().saturating_sub(batch_evaluation_time),
2340            total_sample_evaluations,
2341        );
2342
2343        Ok(processed_points)
2344    }
2345
2346    fn write_failed_samples(
2347        &mut self,
2348        workspace: Option<&Path>,
2349        slots: &[SlotMeta],
2350        iteration: usize,
2351    ) -> Result<()> {
2352        if self.failed_samples.is_empty() {
2353            return Ok(());
2354        }
2355        if let Some(workspace) = workspace {
2356            let directory = workspace.join("numerical_stability");
2357            fs::create_dir_all(&directory)?;
2358            let mut file = fs::OpenOptions::new()
2359                .create(true)
2360                .append(true)
2361                .open(directory.join("failed_samples.jsonl"))?;
2362            for (slot_index, sample, metadata) in &self.failed_samples {
2363                serde_json::to_writer(
2364                    &mut file,
2365                    &serde_json::json!({
2366                        "schema_version": 1,
2367                        "iteration": iteration,
2368                        "slot": slots[*slot_index],
2369                        "sample": sample,
2370                        "evaluation_metadata": metadata,
2371                    }),
2372                )?;
2373                file.write_all(b"\n")?;
2374            }
2375            file.flush()?;
2376        }
2377        self.failed_samples.clear();
2378        Ok(())
2379    }
2380}
2381
2382fn initial_batch_size(batching: IterationBatchingSettings) -> usize {
2383    batching.batch_size.unwrap_or(100).max(1)
2384}
2385
2386fn next_batch_size(
2387    batching: IterationBatchingSettings,
2388    current_batch_size: usize,
2389    round_elapsed: Duration,
2390) -> usize {
2391    if let Some(batch_size) = batching.batch_size {
2392        return batch_size.max(1);
2393    }
2394
2395    let round_seconds = round_elapsed.as_secs_f64();
2396    if round_seconds <= 0.0 {
2397        return current_batch_size.max(1);
2398    }
2399
2400    let estimated = ((current_batch_size as f64) * batching.batch_timing_seconds / round_seconds)
2401        .round() as usize;
2402    estimated.max(1)
2403}
2404
2405fn total_completed_points(core_states: &[CoreIterationState]) -> usize {
2406    core_states.iter().map(|state| state.completed_points).sum()
2407}
2408
2409fn total_remaining_points(core_states: &[CoreIterationState]) -> usize {
2410    core_states.iter().map(|state| state.remaining_points).sum()
2411}
2412
2413fn slot_seed(global_seed: u64, slot_index: usize) -> u64 {
2414    let slot = (slot_index as u64).wrapping_add(1);
2415    let mut mixed = global_seed.wrapping_add(0x9e37_79b9_7f4a_7c15u64.wrapping_mul(slot));
2416    mixed ^= mixed >> 30;
2417    mixed = mixed.wrapping_mul(0xbf58_476d_1ce4_e5b9);
2418    mixed ^= mixed >> 27;
2419    mixed = mixed.wrapping_mul(0x94d0_49bb_1331_11eb);
2420    mixed ^ (mixed >> 31)
2421}
2422
2423fn apply_iteration_core_states(
2424    integration_state: &mut IntegrationState,
2425    slots: &[IntegrationSlot],
2426    cores: usize,
2427    cur_points: usize,
2428    elapsed_seconds: f64,
2429    core_states: &[CoreIterationState],
2430) {
2431    let n_slots = integration_state.slot_metas.len();
2432
2433    for core_state in core_states {
2434        integration_state.stats = integration_state.stats.merged(&core_state.stats);
2435        for (integral, core_integral) in integration_state
2436            .all_integrals
2437            .iter_mut()
2438            .zip(core_state.integrals.iter())
2439        {
2440            integral.merge(core_integral);
2441        }
2442        for (slot_stats, core_slot_stats) in integration_state
2443            .slot_stats
2444            .iter_mut()
2445            .zip(core_state.slot_stats.iter())
2446        {
2447            *slot_stats = slot_stats.merged(core_slot_stats);
2448        }
2449    }
2450
2451    let mut merged_sampling_grids = integration_state
2452        .sampling_states
2453        .iter()
2454        .map(|sampling_state| sampling_state.grid.clone_without_samples())
2455        .collect_vec();
2456    let mut merged_re_grids = (0..n_slots)
2457        .map(|slot_index| {
2458            integration_state
2459                .sampling_state_for_slot(slot_index)
2460                .grid
2461                .clone_without_samples()
2462        })
2463        .collect_vec();
2464    let mut merged_im_grids = (0..n_slots)
2465        .map(|slot_index| {
2466            integration_state
2467                .sampling_state_for_slot(slot_index)
2468                .grid
2469                .clone_without_samples()
2470        })
2471        .collect_vec();
2472    let mut merged_absolute_re_grids = (0..n_slots)
2473        .map(|slot_index| {
2474            integration_state
2475                .monitored_discrete_path
2476                .as_deref()
2477                .map(|path| {
2478                    shallow_discrete_monitor_grid(
2479                        &integration_state.sampling_state_for_slot(slot_index).grid,
2480                        path,
2481                    )
2482                    .expect("validated monitored discrete path must match every sampling grid")
2483                })
2484        })
2485        .collect_vec();
2486    let mut merged_absolute_im_grids = merged_absolute_re_grids.clone();
2487
2488    for core_state in core_states {
2489        for (sampling_state, merged_grid) in core_state
2490            .sampling_states
2491            .iter()
2492            .zip(merged_sampling_grids.iter_mut())
2493        {
2494            merged_grid
2495                .merge(&sampling_state.sampling_grid)
2496                .expect("could not merge grids");
2497        }
2498        for slot_index in 0..n_slots {
2499            merged_re_grids[slot_index]
2500                .merge(&core_state.slot_re_grids[slot_index])
2501                .expect("could not merge real accumulation grids");
2502            merged_im_grids[slot_index]
2503                .merge(&core_state.slot_im_grids[slot_index])
2504                .expect("could not merge imaginary accumulation grids");
2505            match (
2506                merged_absolute_re_grids[slot_index].as_mut(),
2507                core_state.slot_absolute_re_grids[slot_index].as_ref(),
2508            ) {
2509                (Some(merged), Some(core)) => merged
2510                    .merge(core)
2511                    .expect("could not merge absolute real monitor grids"),
2512                (None, None) => {}
2513                _ => panic!("absolute real monitor grid presence is inconsistent"),
2514            }
2515            match (
2516                merged_absolute_im_grids[slot_index].as_mut(),
2517                core_state.slot_absolute_im_grids[slot_index].as_ref(),
2518            ) {
2519                (Some(merged), Some(core)) => merged
2520                    .merge(core)
2521                    .expect("could not merge absolute imaginary monitor grids"),
2522                (None, None) => {}
2523                _ => panic!("absolute imaginary monitor grid presence is inconsistent"),
2524            }
2525        }
2526    }
2527
2528    for (summary, grid) in integration_state
2529        .slot_re_summaries
2530        .iter_mut()
2531        .zip(merged_re_grids.iter())
2532    {
2533        if let Some(summary) = summary.as_mut() {
2534            summary.merge_iteration_grid(grid);
2535            summary.update_iter();
2536        }
2537    }
2538    for (summary, grid) in integration_state
2539        .slot_im_summaries
2540        .iter_mut()
2541        .zip(merged_im_grids.iter())
2542    {
2543        if let Some(summary) = summary.as_mut() {
2544            summary.merge_iteration_grid(grid);
2545            summary.update_iter();
2546        }
2547    }
2548    for (summary, grid) in integration_state
2549        .slot_absolute_re_summaries
2550        .iter_mut()
2551        .zip(&merged_absolute_re_grids)
2552    {
2553        match (summary.as_mut(), grid.as_ref()) {
2554            (Some(summary), Some(grid)) => {
2555                summary.merge_shallow_iteration_grid(grid);
2556                summary.update_iter();
2557            }
2558            (None, None) => {}
2559            _ => panic!("absolute real summary and monitor grid presence is inconsistent"),
2560        }
2561    }
2562    for (summary, grid) in integration_state
2563        .slot_absolute_im_summaries
2564        .iter_mut()
2565        .zip(&merged_absolute_im_grids)
2566    {
2567        match (summary.as_mut(), grid.as_ref()) {
2568            (Some(summary), Some(grid)) => {
2569                summary.merge_shallow_iteration_grid(grid);
2570                summary.update_iter();
2571            }
2572            (None, None) => {}
2573            _ => panic!("absolute imaginary summary and monitor grid presence is inconsistent"),
2574        }
2575    }
2576
2577    for (slot_index, merged_grid) in merged_sampling_grids.into_iter().enumerate() {
2578        let actual_slot_index = match integration_state.sampling_correlation_mode {
2579            SamplingCorrelationMode::Correlated => 0,
2580            SamplingCorrelationMode::Uncorrelated => slot_index,
2581        };
2582        let discrete_axis_labels = integration_state
2583            .sampling_state_for_slot(actual_slot_index)
2584            .discrete_axis_labels
2585            .clone();
2586        let sampling_state = integration_state.sampling_state_for_slot_mut(actual_slot_index);
2587        *sampling_state = SamplingSlotState {
2588            grid: merged_grid,
2589            discrete_axis_labels,
2590        };
2591        sampling_state.grid.update(
2592            F(slots[actual_slot_index]
2593                .settings
2594                .integrator
2595                .discrete_dim_learning_rate),
2596            F(slots[actual_slot_index]
2597                .settings
2598                .integrator
2599                .continuous_dim_learning_rate),
2600        );
2601    }
2602
2603    integration_state.update_iter(false);
2604    integration_state.iter += 1;
2605    integration_state.elapsed_seconds = elapsed_seconds;
2606    integration_state.n_cores = cores;
2607    integration_state.num_points += cur_points;
2608}
2609
2610fn build_preview_integration_state(
2611    integration_state: &IntegrationState,
2612    slots: &[IntegrationSlot],
2613    cores: usize,
2614    completed_points: usize,
2615    elapsed_seconds: f64,
2616    core_states: &[CoreIterationState],
2617) -> IntegrationState {
2618    let mut preview = integration_state.clone();
2619    apply_iteration_core_states(
2620        &mut preview,
2621        slots,
2622        cores,
2623        completed_points,
2624        elapsed_seconds,
2625        core_states,
2626    );
2627    preview
2628}
2629
2630fn write_interrupted_numerical_stability_outputs(
2631    workspace: Option<&Path>,
2632    integration_state: &IntegrationState,
2633    slots: &[IntegrationSlot],
2634    cores: usize,
2635    completed_points: usize,
2636    elapsed_seconds: f64,
2637    core_states: &[CoreIterationState],
2638) -> Result<Vec<PathBuf>> {
2639    if workspace.is_none() || completed_points == 0 {
2640        return Ok(Vec::new());
2641    }
2642
2643    let preview_state = build_preview_integration_state(
2644        integration_state,
2645        slots,
2646        cores,
2647        completed_points,
2648        elapsed_seconds,
2649        core_states,
2650    );
2651    write_numerical_stability_outputs(
2652        workspace,
2653        &preview_state,
2654        WorkspaceSnapshotControl {
2655            write_iteration_archives: false,
2656        },
2657    )
2658}
2659
2660/// Integrate function used for local runs
2661pub fn havana_integrate<S>(
2662    request: HavanaIntegrateRequest,
2663    mut status_emitter: S,
2664) -> Result<IntegrationResult>
2665where
2666    S: FnMut(StatusUpdate) -> Result<()>,
2667{
2668    let HavanaIntegrateRequest {
2669        mut slots,
2670        sampling_correlation_mode,
2671        n_cores,
2672        state,
2673        workspace,
2674        output_control,
2675        batching,
2676        view_options,
2677    } = request;
2678
2679    if slots.is_empty() {
2680        return Err(Report::msg(
2681            "At least one integrand must be selected for integration",
2682        ));
2683    }
2684
2685    let slot_metas = slots.iter().map(|slot| slot.meta.clone()).collect_vec();
2686    let targets = slots.iter().map(|slot| slot.target).collect_vec();
2687    let sampling_states = match sampling_correlation_mode {
2688        SamplingCorrelationMode::Correlated => {
2689            vec![SamplingSlotState::from_integrand(&slots[0].integrand)]
2690        }
2691        SamplingCorrelationMode::Uncorrelated => slots
2692            .iter()
2693            .map(|slot| SamplingSlotState::from_integrand(&slot.integrand))
2694            .collect_vec(),
2695    };
2696    let monitored_discrete_setup =
2697        resolve_monitored_discrete_setup(sampling_correlation_mode, &slots, &sampling_states);
2698    let slot_first_non_trivial_discrete_breakdown_metadata =
2699        if let Some(path) = monitored_discrete_setup.path.as_deref() {
2700            slots
2701                .iter()
2702                .enumerate()
2703                .map(|(slot_index, slot)| {
2704                    build_persisted_discrete_breakdown_metadata(
2705                        &slot.integrand,
2706                        path,
2707                        &sampling_states[sampling_correlation_mode.state_index(slot_index)]
2708                            .discrete_axis_labels,
2709                    )
2710                })
2711                .collect_vec()
2712        } else {
2713            vec![None; slot_metas.len()]
2714        };
2715    if let Some(label_warning) = monitored_discrete_setup.warning.as_ref() {
2716        warn!("{label_warning}");
2717    }
2718
2719    let mut integration_state = if let Some(integration_state) = state {
2720        integration_state
2721    } else {
2722        IntegrationState::new_from_settings(
2723            sampling_correlation_mode,
2724            sampling_states.clone(),
2725            slot_metas.clone(),
2726            monitored_discrete_setup.path.clone(),
2727            monitored_discrete_setup.axis_label.clone(),
2728            monitored_discrete_setup.bin_descriptions.clone(),
2729            slot_first_non_trivial_discrete_breakdown_metadata,
2730        )
2731    };
2732    integration_state.monitored_discrete_path = monitored_discrete_setup.path;
2733    integration_state.first_non_trivial_discrete_label = monitored_discrete_setup.axis_label;
2734    integration_state.first_non_trivial_discrete_bin_descriptions =
2735        monitored_discrete_setup.bin_descriptions;
2736    if integration_state.slot_metas != slot_metas {
2737        return Err(Report::msg(
2738            "Saved integration state slots do not match the currently selected integrands",
2739        ));
2740    }
2741    if integration_state.sampling_correlation_mode != sampling_correlation_mode {
2742        return Err(Report::msg(
2743            "Saved integration state sampling mode does not match the current integration mode",
2744        ));
2745    }
2746    if integration_state.sampling_states.len()
2747        != sampling_correlation_mode.state_count(slot_metas.len())
2748    {
2749        return Err(Report::msg(
2750            "Saved integration state sampling metadata is inconsistent with the selected slots",
2751        ));
2752    }
2753    if integration_state
2754        .slot_first_non_trivial_discrete_breakdown_metadata
2755        .len()
2756        != slot_metas.len()
2757    {
2758        return Err(Report::msg(
2759            "Saved integration state discrete breakdown metadata is inconsistent with the selected slots",
2760        ));
2761    }
2762    if integration_state.slot_absolute_re_summaries.len() != slot_metas.len()
2763        || integration_state.slot_absolute_im_summaries.len() != slot_metas.len()
2764    {
2765        return Err(Report::msg(
2766            "Saved integration state absolute summaries are inconsistent with the selected slots",
2767        ));
2768    }
2769    for slot_index in 0..slot_metas.len() {
2770        let expected_bin_count = integration_state
2771            .monitored_discrete_path
2772            .as_deref()
2773            .map(|path| {
2774                discrete_grid_at_path(
2775                    &integration_state.sampling_state_for_slot(slot_index).grid,
2776                    path,
2777                )
2778                .map(|grid| grid.bins.len())
2779                .ok_or_else(|| {
2780                    Report::msg(format!(
2781                        "Saved integration state monitor path does not match slot {} sampling grid",
2782                        integration_state.slot_metas[slot_index].key()
2783                    ))
2784                })
2785            })
2786            .transpose()?;
2787        for summary in [
2788            &integration_state.slot_absolute_re_summaries[slot_index],
2789            &integration_state.slot_absolute_im_summaries[slot_index],
2790        ] {
2791            let valid = match (summary, expected_bin_count) {
2792                (None, None) => true,
2793                (Some(summary), Some(bin_count)) => {
2794                    summary.bins.len() == bin_count
2795                        && summary.bins.iter().all(|bin| bin.sub_summary.is_none())
2796                }
2797                _ => false,
2798            };
2799            if !valid {
2800                return Err(Report::msg(format!(
2801                    "Saved integration state absolute summary does not match slot {} monitor grid",
2802                    integration_state.slot_metas[slot_index].key()
2803                )));
2804            }
2805        }
2806    }
2807
2808    let primary = &slots[0];
2809    let sampling_str = primary.settings.sampling.describe_settings();
2810    let dimension = primary.integrand.get_n_dim();
2811    let discrete_depth = primary.settings.sampling.discrete_depth();
2812    let is_tropical_sampling = primary
2813        .settings
2814        .sampling
2815        .get_parameterization_settings()
2816        .is_none();
2817    let use_ltd = primary.settings.general.use_ltd;
2818    let integration_seed = primary.settings.integrator.seed;
2819    let integrated_phase = primary.settings.integrator.integrated_phase;
2820    let target_relative_accuracy = primary.settings.integrator.target_relative_accuracy;
2821    let target_absolute_accuracy = primary.settings.integrator.target_absolute_accuracy;
2822    let n_start = primary.settings.integrator.n_start;
2823    let n_increase = primary.settings.integrator.n_increase;
2824    let n_max = primary.settings.integrator.n_max;
2825
2826    let cont_dim_str = if is_tropical_sampling {
2827        format!("a median continious dimension of {}", dimension)
2828    } else {
2829        format!("{} continuous dimensions", dimension)
2830    };
2831
2832    let graph_string = if discrete_depth > 0 {
2833        let num_graphs = match &integration_state.sampling_state_for_slot(0).grid {
2834            Grid::Discrete(g) => g.bins.len(),
2835            _ => unreachable!(),
2836        };
2837        format!(
2838            "{discrete_depth} nested discrete grids with {} {} and ",
2839            num_graphs,
2840            if num_graphs > 1 { "graphs" } else { "graph" }
2841        )
2842    } else {
2843        String::new()
2844    };
2845
2846    let correlation_str = match sampling_correlation_mode {
2847        SamplingCorrelationMode::Correlated => "correlated",
2848        SamplingCorrelationMode::Uncorrelated => "uncorrelated",
2849    };
2850    let grid_str = format!("{graph_string}{cont_dim_str} using {sampling_str} ({correlation_str})");
2851
2852    let cores = n_cores.max(1);
2853    let n_slots = integration_state.slot_metas.len();
2854    let elapsed_seconds_offset = integration_state.elapsed_seconds;
2855
2856    let t_start = Instant::now();
2857
2858    info!(
2859        "Integrating using {} ltd with {} {} over {} ...",
2860        if use_ltd { "naive" } else { "cff" },
2861        cores,
2862        if cores > 1 { "cores" } else { "core" },
2863        grid_str
2864    );
2865    info!("");
2866
2867    let pool = ThreadPoolBuilder::new().num_threads(cores).build().unwrap();
2868
2869    let mut n_samples_evaluated = 0;
2870    let mut emitted_latest_observable_paths = vec![Vec::new(); n_slots];
2871    let mut emitted_latest_numerical_stability_paths = Vec::new();
2872    clear_iteration_abort_request();
2873    'integrateLoop: while integration_state.num_points < n_max {
2874        // ensure we do not overshoot
2875        let cur_points = {
2876            let cur_points_not_final_iter = n_start + n_increase * integration_state.iter;
2877            if cur_points_not_final_iter + integration_state.num_points > n_max {
2878                n_max - integration_state.num_points
2879            } else {
2880                cur_points_not_final_iter
2881            }
2882        };
2883
2884        // the number of points per core is the same for all cores, except for the last one
2885        let target_points_per_core = (cur_points - 1) / cores + 1;
2886        let n_points_per_core = (0..cores)
2887            .map(|core_id| {
2888                if core_id + 1 == cores {
2889                    cur_points - target_points_per_core * (cores - 1)
2890                } else {
2891                    target_points_per_core
2892                }
2893            })
2894            .collect_vec();
2895
2896        let iteration_start = Instant::now();
2897        let mut current_batch_size = initial_batch_size(batching);
2898        let mut last_live_status = None;
2899        if batching.emit_initial_status_update && !is_interrupted() {
2900            status_emitter(build_status_update(StatusUpdateBuildRequest {
2901                kind: IntegrationStatusKind::Live,
2902                integration_state: &integration_state,
2903                cores,
2904                elapsed_time: t_start.elapsed(),
2905                iteration_elapsed_time: iteration_start.elapsed(),
2906                cur_points: 0,
2907                total_points_display: integration_state.num_points,
2908                n_samples_evaluated,
2909                targets: &targets,
2910                render_options: &view_options,
2911                live_progress: Some(status_update::LiveIterationProgress {
2912                    completed_points: 0,
2913                    target_points: cur_points,
2914                }),
2915            }))?;
2916        }
2917
2918        let current_max_evals = integration_state
2919            .all_integrals
2920            .iter()
2921            .map(ComplexAccumulator::get_worst_case)
2922            .collect_vec();
2923        let current_integral_estimates = integration_state
2924            .all_integrals
2925            .iter()
2926            .zip(&slots)
2927            .map(|(integral, slot)| {
2928                let accumulator = match slot.settings.integrator.integrated_phase {
2929                    IntegratedPhase::Real => &integral.absolute_re,
2930                    IntegratedPhase::Imag => &integral.absolute_im,
2931                    IntegratedPhase::Both => return None,
2932                };
2933                (accumulator.processed_samples > 0)
2934                    .then_some((accumulator.avg.0, accumulator.err.0))
2935            })
2936            .collect_vec();
2937
2938        let mut worker_states = n_points_per_core
2939            .iter()
2940            .enumerate()
2941            .map(|(core_id, &n_points)| {
2942                CoreIterationState::new(
2943                    slots.iter().map(|slot| slot.integrand.clone()).collect(),
2944                    integration_state.sampling_correlation_mode,
2945                    &integration_state
2946                        .sampling_states
2947                        .iter()
2948                        .map(|sampling_state| sampling_state.grid.clone())
2949                        .collect_vec(),
2950                    integration_state.monitored_discrete_path.as_deref(),
2951                    integration_seed + integration_state.iter as u64,
2952                    target_points_per_core * core_id,
2953                    n_points,
2954                )
2955            })
2956            .collect_vec();
2957        while total_remaining_points(&worker_states) > 0 {
2958            let round_started_at = Instant::now();
2959            let processed_per_core: Vec<Result<usize>> = {
2960                let slot_settings = slots.iter().map(|slot| &slot.settings).collect_vec();
2961                let slot_targets = slots
2962                    .iter()
2963                    .map(|slot| {
2964                        slot.reference_gaussian
2965                            .as_ref()
2966                            .map(EvaluationTarget::Reference)
2967                            .unwrap_or(EvaluationTarget::Physical(&slot.model))
2968                    })
2969                    .collect_vec();
2970                pool.install(|| {
2971                    worker_states
2972                        .par_iter_mut()
2973                        .map(|worker_state| {
2974                            worker_state.evaluate_chunk(
2975                                &slot_settings,
2976                                &slot_targets,
2977                                integration_state.iter,
2978                                &current_max_evals,
2979                                &current_integral_estimates,
2980                                current_batch_size,
2981                            )
2982                        })
2983                        .collect()
2984                })
2985            };
2986            // Keep replayable numerical failures independently of iteration
2987            // checkpoints, including rounds interrupted or aborted afterward.
2988            // Only this coordinator writes, so worker records cannot interleave.
2989            for worker_state in &mut worker_states {
2990                worker_state.write_failed_samples(
2991                    workspace.as_deref(),
2992                    &integration_state.slot_metas,
2993                    integration_state.iter + 1,
2994                )?;
2995            }
2996            let processed_this_round = processed_per_core
2997                .into_iter()
2998                .collect::<Result<Vec<_>>>()?
2999                .into_iter()
3000                .sum::<usize>();
3001
3002            if processed_this_round == 0 {
3003                break;
3004            }
3005
3006            let completed_points = total_completed_points(&worker_states);
3007            if batching.emit_live_status_updates
3008                && completed_points < cur_points
3009                && !is_interrupted()
3010                && !is_iteration_abort_requested()
3011            {
3012                let now = Instant::now();
3013                let should_emit_live_status = last_live_status.is_none_or(|previous| {
3014                    now.duration_since(previous).as_secs_f64()
3015                        >= batching.min_time_between_status_updates_seconds
3016                });
3017                if should_emit_live_status {
3018                    let preview_state = build_preview_integration_state(
3019                        &integration_state,
3020                        &slots,
3021                        cores,
3022                        completed_points,
3023                        elapsed_seconds_offset + t_start.elapsed().as_secs_f64(),
3024                        &worker_states,
3025                    );
3026                    status_emitter(build_status_update(StatusUpdateBuildRequest {
3027                        kind: IntegrationStatusKind::Live,
3028                        integration_state: &preview_state,
3029                        cores,
3030                        elapsed_time: t_start.elapsed(),
3031                        iteration_elapsed_time: iteration_start.elapsed(),
3032                        cur_points: completed_points,
3033                        total_points_display: integration_state.num_points + completed_points,
3034                        n_samples_evaluated: n_samples_evaluated + completed_points,
3035                        targets: &targets,
3036                        render_options: &view_options,
3037                        live_progress: Some(status_update::LiveIterationProgress {
3038                            completed_points,
3039                            target_points: cur_points,
3040                        }),
3041                    }))?;
3042                    last_live_status = Some(now);
3043                }
3044            }
3045
3046            if is_interrupted() {
3047                warn!("{}", "Integration interrupted by user".yellow());
3048                emitted_latest_numerical_stability_paths =
3049                    write_interrupted_numerical_stability_outputs(
3050                        workspace.as_deref(),
3051                        &integration_state,
3052                        &slots,
3053                        cores,
3054                        total_completed_points(&worker_states),
3055                        elapsed_seconds_offset + t_start.elapsed().as_secs_f64(),
3056                        &worker_states,
3057                    )?;
3058                break 'integrateLoop;
3059            }
3060
3061            if is_iteration_abort_requested() {
3062                break;
3063            }
3064
3065            if total_remaining_points(&worker_states) > 0 {
3066                current_batch_size =
3067                    next_batch_size(batching, current_batch_size, round_started_at.elapsed());
3068            }
3069        }
3070
3071        let abort_current_iteration = is_iteration_abort_requested();
3072        clear_iteration_abort_request();
3073
3074        if is_interrupted() {
3075            warn!("{}", "Integration interrupted by user".yellow());
3076            emitted_latest_numerical_stability_paths =
3077                write_interrupted_numerical_stability_outputs(
3078                    workspace.as_deref(),
3079                    &integration_state,
3080                    &slots,
3081                    cores,
3082                    total_completed_points(&worker_states),
3083                    elapsed_seconds_offset + t_start.elapsed().as_secs_f64(),
3084                    &worker_states,
3085                )?;
3086            break 'integrateLoop;
3087        }
3088
3089        let completed_points = total_completed_points(&worker_states);
3090        if abort_current_iteration && completed_points < cur_points {
3091            if completed_points == 0 {
3092                warn!(
3093                    "{}",
3094                    "Current iteration abort requested before any samples completed; restarting the iteration."
3095                        .yellow()
3096                );
3097                continue;
3098            }
3099            warn!(
3100                "{}",
3101                format!(
3102                    "Current iteration aborted by user after {} completed samples.",
3103                    completed_points
3104                )
3105                .yellow()
3106            );
3107            continue;
3108        }
3109
3110        n_samples_evaluated += completed_points;
3111        apply_iteration_core_states(
3112            &mut integration_state,
3113            &slots,
3114            cores,
3115            completed_points,
3116            elapsed_seconds_offset + t_start.elapsed().as_secs_f64(),
3117            &worker_states,
3118        );
3119
3120        // merge runtime state per slot into the owner core
3121        let (owner_core_slice, other_cores) = worker_states.split_at_mut(1);
3122        let owner_core = &mut owner_core_slice[0].slot_integrands;
3123        for other_core in other_cores {
3124            for (owner_slot, other_slot) in owner_core
3125                .iter_mut()
3126                .zip(other_core.slot_integrands.iter_mut())
3127            {
3128                owner_slot.merge_runtime_results(other_slot)?;
3129            }
3130        }
3131        for (slot, integrand) in slots.iter_mut().zip(owner_core.iter()) {
3132            slot.integrand = integrand.clone();
3133        }
3134
3135        // Update observable accumulators, persist authoritative resume state, then refresh
3136        // the latest user-facing snapshots.
3137        for slot in &mut slots {
3138            slot.integrand
3139                .update_runtime_results(integration_state.iter);
3140        }
3141
3142        if let Some(ref workspace_path) = workspace {
3143            for (slot_index, slot) in slots.iter().enumerate() {
3144                write_observable_resume_state(
3145                    &slot.integrand,
3146                    Some(workspace_path.as_path()),
3147                    &integration_state.slot_metas[slot_index],
3148                    integration_state.iter,
3149                    output_control,
3150                )?;
3151            }
3152            write_integration_state_to_workspace(workspace_path, &integration_state)?;
3153            write_integration_result_snapshots(
3154                workspace_path,
3155                &integration_state,
3156                &targets,
3157                output_control,
3158            )?;
3159            emitted_latest_numerical_stability_paths = write_numerical_stability_outputs(
3160                Some(workspace_path.as_path()),
3161                &integration_state,
3162                output_control,
3163            )?;
3164        }
3165
3166        for (slot_index, slot) in slots.iter().enumerate() {
3167            let workspace_path = workspace
3168                .as_deref()
3169                .map(|root| slot_workspace_path(root, &integration_state.slot_metas[slot_index]));
3170            emitted_latest_observable_paths[slot_index] =
3171                write_latest_observables_output(&slot.integrand, workspace_path.as_deref())?;
3172            write_observable_snapshot_archive(
3173                &slot.integrand,
3174                workspace_path.as_deref(),
3175                integration_state.iter,
3176                output_control,
3177            )?;
3178        }
3179
3180        status_emitter(build_status_update(StatusUpdateBuildRequest {
3181            kind: IntegrationStatusKind::Iteration,
3182            integration_state: &integration_state,
3183            cores,
3184            elapsed_time: t_start.elapsed(),
3185            iteration_elapsed_time: iteration_start.elapsed(),
3186            cur_points: completed_points,
3187            total_points_display: integration_state.num_points,
3188            n_samples_evaluated,
3189            targets: &targets,
3190            render_options: &view_options,
3191            live_progress: None,
3192        }))?;
3193
3194        let target_accuracy_status = evaluate_target_accuracy(
3195            &integration_state,
3196            integration_state.num_points,
3197            t_start.elapsed(),
3198            &targets,
3199            match integrated_phase {
3200                IntegratedPhase::Real | IntegratedPhase::Both => {
3201                    IntegrationStatusPhaseDisplay::Real
3202                }
3203                IntegratedPhase::Imag => IntegrationStatusPhaseDisplay::Imag,
3204            },
3205            target_relative_accuracy,
3206            target_absolute_accuracy,
3207        );
3208        if target_accuracy_status.is_reached() {
3209            let reached_target = match (
3210                target_accuracy_status.relative_reached,
3211                target_accuracy_status.absolute_reached,
3212            ) {
3213                (true, true) => "relative and absolute",
3214                (true, false) => "relative",
3215                (false, true) => "absolute",
3216                (false, false) => unreachable!(),
3217            };
3218            info!(
3219                "Stopping integration after reaching the configured {reached_target} accuracy target."
3220            );
3221            break;
3222        }
3223    }
3224    // Reset the interrupted flag
3225    set_interrupted(false);
3226    clear_iteration_abort_request();
3227
3228    if integration_state.num_points > 0 {
3229        let final_view_options = view_options.for_final();
3230        status_emitter(build_status_update(StatusUpdateBuildRequest {
3231            kind: IntegrationStatusKind::Final,
3232            integration_state: &integration_state,
3233            cores,
3234            elapsed_time: t_start.elapsed(),
3235            iteration_elapsed_time: Duration::ZERO,
3236            cur_points: 0,
3237            total_points_display: integration_state.num_points,
3238            n_samples_evaluated,
3239            targets: &targets,
3240            render_options: &final_view_options,
3241            live_progress: None,
3242        }))?;
3243    } else {
3244        info!("");
3245        warn!(
3246            "{}",
3247            "No final integration results to display since no iteration completed.".yellow()
3248        );
3249        info!("");
3250    }
3251
3252    if !slots.is_empty() {
3253        emit_results_output_summary(
3254            workspace.as_deref(),
3255            &slots,
3256            &emitted_latest_observable_paths,
3257            &emitted_latest_numerical_stability_paths,
3258        );
3259    }
3260
3261    Ok(build_integration_result(&integration_state, &targets))
3262}
3263
3264/// Batch integrate function used for distributed runs, used by the worker nodes.
3265/// Evaluates a batch of points and returns the results in a manner specified by the user.
3266pub(crate) fn batch_integrate(
3267    integrand: &mut Integrand,
3268    model: &Model,
3269    input: BatchIntegrateInput,
3270) -> Result<BatchResult> {
3271    let samples = match input.samples {
3272        SampleInput::SampleList { samples } => samples,
3273        SampleInput::Grid {
3274            mut grid,
3275            num_points,
3276            seed,
3277            thread_id,
3278        } => {
3279            let mut rng = MonteCarloRng::new(seed, thread_id);
3280
3281            (0..num_points)
3282                .map(|_| {
3283                    let mut sample = Sample::new();
3284                    grid.sample(&mut rng, &mut sample);
3285                    sample
3286                })
3287                .collect_vec()
3288        }
3289    };
3290
3291    let (evaluation_results, metadata_statistics) = evaluate_sample_list(
3292        integrand,
3293        &samples,
3294        model,
3295        input.num_cores,
3296        input.iter,
3297        input.max_eval,
3298    )?;
3299
3300    let integrand_output = generate_integrand_output(
3301        integrand,
3302        &evaluation_results,
3303        &samples,
3304        input.integrand_output_settings,
3305        input.settings.integrator.integrated_phase,
3306    );
3307
3308    let event_output = generate_event_output(
3309        integrand,
3310        evaluation_results,
3311        input.event_output_settings,
3312        input.settings,
3313    );
3314
3315    Ok(BatchResult {
3316        statistics: metadata_statistics,
3317        integrand_data: integrand_output,
3318        event_data: event_output,
3319    })
3320}
3321
3322/// Map the evaluation result on to the right output specified by the user.
3323fn generate_integrand_output(
3324    integrand: &Integrand,
3325    evaluation_results: &[EvaluationResult],
3326    samples: &[Sample<F<f64>>],
3327    integrand_output_settings: IntegralOutputSettings,
3328    integrated_phase: IntegratedPhase,
3329) -> BatchIntegrateOutput {
3330    fn effective_integrand_result(result: &EvaluationResult) -> Complex<F<f64>> {
3331        let jacobian = result.parameterization_jacobian.unwrap_or(F(1.0));
3332        result.integrand_result * Complex::new_re(jacobian)
3333    }
3334
3335    match integrand_output_settings {
3336        IntegralOutputSettings::Default => {
3337            let integrand_values = evaluation_results
3338                .iter()
3339                .map(effective_integrand_result)
3340                .collect();
3341
3342            BatchIntegrateOutput::Default(integrand_values, samples.to_vec())
3343        }
3344        IntegralOutputSettings::Accumulator => {
3345            let mut real_accumulator = StatisticsAccumulator::new();
3346            let mut imag_accumulator = StatisticsAccumulator::new();
3347            let mut grid = integrand.create_grid();
3348
3349            for (result, sample) in evaluation_results.iter().zip(samples.iter()) {
3350                let effective_result = effective_integrand_result(result);
3351                real_accumulator
3352                    .add_sample(effective_result.re * sample.get_weight(), Some(sample));
3353                imag_accumulator
3354                    .add_sample(effective_result.im * sample.get_weight(), Some(sample));
3355
3356                match integrated_phase {
3357                    IntegratedPhase::Real => {
3358                        grid.add_training_sample(sample, effective_result.re)
3359                            .unwrap();
3360                    }
3361                    IntegratedPhase::Imag => {
3362                        grid.add_training_sample(sample, effective_result.im)
3363                            .unwrap();
3364                    }
3365                    IntegratedPhase::Both => {
3366                        unimplemented!()
3367                    }
3368                }
3369            }
3370
3371            BatchIntegrateOutput::Accumulator(
3372                Box::new((real_accumulator, imag_accumulator)),
3373                Box::new(grid),
3374            )
3375        }
3376    }
3377}
3378
3379/// Process events into histograms or event lists, as specified by the user.
3380fn generate_event_output(
3381    integrand: &Integrand,
3382    evaluation_results: Vec<EvaluationResult>,
3383    event_output_settings: EventOutputSettings,
3384    _settings: &RuntimeSettings,
3385) -> EventOutput {
3386    match event_output_settings {
3387        EventOutputSettings::None => EventOutput::None,
3388
3389        EventOutputSettings::EventList => {
3390            let mut event_groups = EventGroupList::default();
3391            for mut result in evaluation_results {
3392                event_groups.append(&mut result.event_groups);
3393            }
3394            EventOutput::EventList { event_groups }
3395        }
3396        EventOutputSettings::Histogram => integrand
3397            .observable_accumulator_bundle()
3398            .map(|histograms| EventOutput::Histogram { histograms })
3399            .unwrap_or(EventOutput::None),
3400    }
3401}
3402
3403pub fn slot_workspace_path(workspace: &Path, slot_meta: &SlotMeta) -> PathBuf {
3404    workspace.join("integrands").join(slot_meta.key())
3405}
3406
3407pub fn workspace_manifest_path(workspace: &Path) -> PathBuf {
3408    workspace.join("manifest.json")
3409}
3410
3411pub fn workspace_state_dir(workspace: &Path) -> PathBuf {
3412    workspace.join("state")
3413}
3414
3415pub fn workspace_state_path(workspace: &Path) -> PathBuf {
3416    workspace_state_dir(workspace).join("integration_state.bin")
3417}
3418
3419fn workspace_observable_state_dir(workspace: &Path, slot_meta: &SlotMeta) -> PathBuf {
3420    workspace_state_dir(workspace)
3421        .join("observables")
3422        .join(slot_meta.key())
3423}
3424
3425pub fn latest_observable_resume_state_path(workspace: &Path, slot_meta: &SlotMeta) -> PathBuf {
3426    workspace_observable_state_dir(workspace, slot_meta).join("latest.json")
3427}
3428
3429fn archived_observable_resume_state_path(
3430    workspace: &Path,
3431    slot_meta: &SlotMeta,
3432    iter: usize,
3433) -> PathBuf {
3434    workspace_observable_state_dir(workspace, slot_meta).join(format!("iter_{iter:04}.json"))
3435}
3436
3437pub fn workspace_result_snapshot_path(workspace: &Path) -> PathBuf {
3438    workspace.join("integration_result.json")
3439}
3440
3441pub fn workspace_result_archive_path(workspace: &Path, iter: usize) -> PathBuf {
3442    workspace
3443        .join("results")
3444        .join(format!("integration_result_iter_{iter:04}.json"))
3445}
3446
3447fn observable_output_extension(format: ObservableFileFormat) -> Option<&'static str> {
3448    match format {
3449        ObservableFileFormat::None => None,
3450        ObservableFileFormat::Hwu => Some("hwu"),
3451        ObservableFileFormat::Json => Some("json"),
3452    }
3453}
3454
3455fn latest_observable_output_path(workspace: &Path, format: ObservableFileFormat) -> PathBuf {
3456    let extension = observable_output_extension(format)
3457        .expect("user-facing observable outputs must use a real file format");
3458    workspace.join(format!("observables_final.{extension}"))
3459}
3460
3461fn archived_observable_output_path(
3462    workspace: &Path,
3463    format: ObservableFileFormat,
3464    iter: usize,
3465) -> PathBuf {
3466    let extension = observable_output_extension(format)
3467        .expect("user-facing observable outputs must use a real file format");
3468    workspace.join(format!("observables_final_iter_{iter:04}.{extension}"))
3469}
3470
3471fn numerical_stability_output_extension(format: ObservableFileFormat) -> Option<&'static str> {
3472    match format {
3473        ObservableFileFormat::None => None,
3474        ObservableFileFormat::Hwu => Some("hwu"),
3475        ObservableFileFormat::Json => Some("json"),
3476    }
3477}
3478
3479fn numerical_stability_scope_file_stem(scope: &str, iter: Option<usize>) -> String {
3480    match iter {
3481        Some(iter) => format!("{scope}_iter_{iter:04}"),
3482        None => scope.to_string(),
3483    }
3484}
3485
3486fn numerical_stability_output_path(
3487    workspace: &Path,
3488    scope: &str,
3489    format: ObservableFileFormat,
3490    iter: Option<usize>,
3491) -> PathBuf {
3492    let extension = numerical_stability_output_extension(format)
3493        .expect("numerical stability outputs must use a real file format");
3494    workspace.join("numerical_stability").join(format!(
3495        "{}.{extension}",
3496        numerical_stability_scope_file_stem(scope, iter)
3497    ))
3498}
3499
3500#[allow(clippy::infallible_destructuring_match)]
3501fn user_facing_observables_output_formats(integrand: &Integrand) -> Vec<ObservableFileFormat> {
3502    let process_integrand = match integrand {
3503        Integrand::ProcessIntegrand(process_integrand) => process_integrand,
3504        #[cfg(test)]
3505        Integrand::TestProbe(_) => return Vec::new(),
3506    };
3507    if !integrand.has_observables() {
3508        return Vec::new();
3509    }
3510
3511    process_integrand
3512        .get_settings()
3513        .integrator
3514        .observables_output
3515        .resolved_formats()
3516}
3517
3518#[allow(clippy::infallible_destructuring_match)]
3519fn user_facing_observables_output_enabled(integrand: &Integrand) -> bool {
3520    let process_integrand = match integrand {
3521        Integrand::ProcessIntegrand(process_integrand) => process_integrand,
3522        #[cfg(test)]
3523        Integrand::TestProbe(_) => return false,
3524    };
3525    if !integrand.has_observables() {
3526        return false;
3527    }
3528
3529    !process_integrand
3530        .get_settings()
3531        .integrator
3532        .observables_output
3533        .resolved_formats()
3534        .is_empty()
3535}
3536
3537fn write_atomic_bytes(path: &Path, bytes: &[u8]) -> Result<()> {
3538    let parent = path
3539        .parent()
3540        .ok_or_else(|| Report::msg("Atomic write target is missing a parent directory"))?;
3541    fs::create_dir_all(parent)?;
3542    let tmp_path = path.with_extension(format!(
3543        "{}.tmp",
3544        path.extension()
3545            .and_then(|ext| ext.to_str())
3546            .unwrap_or("tmp")
3547    ));
3548    fs::write(&tmp_path, bytes)?;
3549    fs::rename(&tmp_path, path)?;
3550    Ok(())
3551}
3552
3553fn write_atomic_json<T: Serialize>(path: &Path, value: &T) -> Result<()> {
3554    write_atomic_bytes(path, &serde_json::to_vec_pretty(value)?)
3555}
3556
3557fn write_observable_resume_state(
3558    integrand: &Integrand,
3559    workspace: Option<&Path>,
3560    slot_meta: &SlotMeta,
3561    iter: usize,
3562    output_control: WorkspaceSnapshotControl,
3563) -> Result<Option<PathBuf>> {
3564    let Some(bundle) = integrand.observable_snapshot_bundle() else {
3565        return Ok(None);
3566    };
3567    let Some(workspace) = workspace else {
3568        return Ok(None);
3569    };
3570    let latest_path = latest_observable_resume_state_path(workspace, slot_meta);
3571    write_atomic_json(&latest_path, &bundle)?;
3572    if output_control.write_iteration_archives {
3573        let archived_path = archived_observable_resume_state_path(workspace, slot_meta, iter);
3574        write_atomic_json(&archived_path, &bundle)?;
3575    }
3576    Ok(Some(latest_path))
3577}
3578
3579fn write_observable_snapshot_archive(
3580    integrand: &Integrand,
3581    workspace: Option<&Path>,
3582    iter: usize,
3583    output_control: WorkspaceSnapshotControl,
3584) -> Result<()> {
3585    if !user_facing_observables_output_enabled(integrand) {
3586        return Ok(());
3587    }
3588    if !output_control.write_iteration_archives {
3589        return Ok(());
3590    }
3591    let Some(workspace) = workspace else {
3592        return Ok(());
3593    };
3594
3595    for format in user_facing_observables_output_formats(integrand) {
3596        let path = archived_observable_output_path(workspace, format, iter);
3597        integrand.write_observable_snapshots(&path, format)?;
3598    }
3599
3600    Ok(())
3601}
3602
3603fn write_latest_observables_output(
3604    integrand: &Integrand,
3605    workspace: Option<&Path>,
3606) -> Result<Vec<PathBuf>> {
3607    if !user_facing_observables_output_enabled(integrand) {
3608        return Ok(Vec::new());
3609    }
3610    let Some(workspace) = workspace else {
3611        return Ok(Vec::new());
3612    };
3613
3614    let mut emitted_paths = Vec::new();
3615    for format in user_facing_observables_output_formats(integrand) {
3616        let path = latest_observable_output_path(workspace, format);
3617        integrand.write_observable_snapshots(&path, format)?;
3618        emitted_paths.push(path);
3619    }
3620
3621    Ok(emitted_paths)
3622}
3623
3624fn write_numerical_stability_snapshot(
3625    bundle: &ObservableSnapshotBundle,
3626    path: &Path,
3627    format: ObservableFileFormat,
3628) -> Result<()> {
3629    match format {
3630        ObservableFileFormat::None => Ok(()),
3631        ObservableFileFormat::Json => write_atomic_json(path, bundle),
3632        ObservableFileFormat::Hwu => {
3633            let parent = path
3634                .parent()
3635                .ok_or_else(|| Report::msg("Numerical stability output is missing a parent"))?;
3636            fs::create_dir_all(parent)?;
3637            bundle.write_hwu_file(path)
3638        }
3639    }
3640}
3641
3642fn write_numerical_stability_outputs(
3643    workspace: Option<&Path>,
3644    integration_state: &IntegrationState,
3645    output_control: WorkspaceSnapshotControl,
3646) -> Result<Vec<PathBuf>> {
3647    let Some(workspace) = workspace else {
3648        return Ok(Vec::new());
3649    };
3650
3651    let mut emitted_paths = Vec::new();
3652    let mut scopes = vec![(
3653        "global".to_string(),
3654        integration_state
3655            .stats
3656            .numerical_stability_snapshot()
3657            .as_observable_snapshot_bundle(),
3658    )];
3659    scopes.extend(
3660        integration_state
3661            .slot_metas
3662            .iter()
3663            .zip(integration_state.slot_stats.iter())
3664            .map(|(slot_meta, stats)| {
3665                (
3666                    slot_meta.key(),
3667                    stats
3668                        .numerical_stability_snapshot()
3669                        .as_observable_snapshot_bundle(),
3670                )
3671            }),
3672    );
3673
3674    for (scope, bundle) in scopes {
3675        for format in [ObservableFileFormat::Json, ObservableFileFormat::Hwu] {
3676            let latest_path = numerical_stability_output_path(workspace, &scope, format, None);
3677            write_numerical_stability_snapshot(&bundle, &latest_path, format)?;
3678            emitted_paths.push(latest_path);
3679            if output_control.write_iteration_archives {
3680                let archive_path = numerical_stability_output_path(
3681                    workspace,
3682                    &scope,
3683                    format,
3684                    Some(integration_state.iter),
3685                );
3686                write_numerical_stability_snapshot(&bundle, &archive_path, format)?;
3687            }
3688        }
3689    }
3690
3691    Ok(emitted_paths)
3692}
3693
3694fn write_integration_state_to_workspace(
3695    workspace_path: &Path,
3696    integration_state: &IntegrationState,
3697) -> Result<()> {
3698    let state_path = workspace_state_path(workspace_path);
3699    let state_bytes = bincode::encode_to_vec(integration_state, bincode::config::standard())
3700        .map_err(|err| {
3701            Report::msg(format!(
3702                "Could not serialize integration state for {}: {err}",
3703                state_path.display()
3704            ))
3705        })?;
3706    write_atomic_bytes(&state_path, &state_bytes)?;
3707    Ok(())
3708}
3709
3710fn write_integration_result_snapshots(
3711    workspace_path: &Path,
3712    integration_state: &IntegrationState,
3713    targets: &[Option<Complex<F<f64>>>],
3714    output_control: WorkspaceSnapshotControl,
3715) -> Result<()> {
3716    let result = build_integration_result(integration_state, targets);
3717    write_atomic_json(&workspace_result_snapshot_path(workspace_path), &result)?;
3718    if output_control.write_iteration_archives {
3719        write_atomic_json(
3720            &workspace_result_archive_path(workspace_path, integration_state.iter),
3721            &result,
3722        )?;
3723    }
3724    Ok(())
3725}
3726
3727fn workspace_relative_display_path(root: &Path, path: &Path) -> String {
3728    path.strip_prefix(root)
3729        .unwrap_or(path)
3730        .display()
3731        .to_string()
3732}
3733
3734fn emit_results_output_summary(
3735    workspace: Option<&Path>,
3736    slots: &[IntegrationSlot],
3737    emitted_paths: &[Vec<PathBuf>],
3738    numerical_stability_paths: &[PathBuf],
3739) {
3740    let rendered = render_results_output_summary_table(
3741        workspace,
3742        slots,
3743        emitted_paths,
3744        numerical_stability_paths,
3745    );
3746    let Some(rendered) = rendered else {
3747        return;
3748    };
3749
3750    info!("\n{rendered}");
3751}
3752
3753fn render_results_output_summary_table(
3754    workspace: Option<&Path>,
3755    slots: &[IntegrationSlot],
3756    emitted_paths: &[Vec<PathBuf>],
3757    numerical_stability_paths: &[PathBuf],
3758) -> Option<String> {
3759    let mut summary_rows = Vec::new();
3760    if let Some(workspace) = workspace {
3761        summary_rows.push((
3762            "workspace path".to_string(),
3763            workspace.display().to_string(),
3764        ));
3765        summary_rows.push((
3766            "results".to_string(),
3767            workspace_relative_display_path(workspace, &workspace_result_snapshot_path(workspace)),
3768        ));
3769    }
3770
3771    for (slot, slot_emitted_paths) in slots.iter().zip(emitted_paths.iter()) {
3772        for final_path in slot_emitted_paths {
3773            if !final_path.exists() {
3774                continue;
3775            }
3776            let display_path = workspace
3777                .map(|root| workspace_relative_display_path(root, final_path))
3778                .unwrap_or_else(|| final_path.display().to_string());
3779            let row_label = if slot_emitted_paths.len() > 1 {
3780                let format_label = final_path
3781                    .extension()
3782                    .and_then(|extension| extension.to_str())
3783                    .unwrap_or("output");
3784                format!("{} ({format_label})", slot_key_label(&slot.meta))
3785            } else {
3786                slot_key_label(&slot.meta)
3787            };
3788            summary_rows.push((row_label, display_path));
3789        }
3790    }
3791
3792    for path in numerical_stability_paths {
3793        if !path.exists() {
3794            continue;
3795        }
3796        let display_path = workspace
3797            .map(|root| workspace_relative_display_path(root, path))
3798            .unwrap_or_else(|| path.display().to_string());
3799        let scope = path
3800            .file_stem()
3801            .and_then(|stem| stem.to_str())
3802            .unwrap_or("numerical_stability");
3803        summary_rows.push((format!("numerical stability ({scope})"), display_path));
3804    }
3805
3806    if summary_rows.is_empty() {
3807        return None;
3808    }
3809
3810    let mut builder = Builder::default();
3811    builder.push_record(["type".to_string(), "path".to_string()]);
3812    for (row_type, row_path) in summary_rows {
3813        builder.push_record([
3814            row_type.blue().bold().to_string(),
3815            row_path.green().to_string(),
3816        ]);
3817    }
3818
3819    let mut table = builder.build();
3820    table.with(Panel::header(
3821        "Integration results emitted".green().bold().to_string(),
3822    ));
3823    table.with(
3824        Style::rounded().horizontals([(
3825            1,
3826            HorizontalLine::new('─')
3827                .intersection('┼')
3828                .left('├')
3829                .right('┤'),
3830        )]),
3831    );
3832    table.with(Modify::new(Rows::first()).with(Alignment::left()));
3833    table.with(Modify::new(Columns::new(0..2)).with(Alignment::left()));
3834
3835    Some(utils::normalize_tabled_separator_rows(&table.to_string()))
3836}
3837
3838/// This function actually evaluates the list of samples in parallel.
3839fn evaluate_sample_list(
3840    integrand: &mut Integrand,
3841    samples: &[Sample<F<f64>>],
3842    model: &Model,
3843    num_cores: usize,
3844    iter: usize,
3845    max_eval: Complex<F<f64>>,
3846) -> Result<(Vec<EvaluationResult>, StatisticsCounter)> {
3847    let list_size = samples.len();
3848    let nvec_per_core = (list_size - 1) / num_cores + 1;
3849    let num_chunks = samples.chunks(nvec_per_core).len();
3850
3851    let sample_chunks = samples.par_chunks(nvec_per_core);
3852    let integrands = (0..num_chunks)
3853        .map(|_| integrand.clone())
3854        .collect_vec()
3855        .into_par_iter();
3856
3857    let evaluation_results_per_core: Vec<
3858        Result<(Vec<EvaluationResult>, StatisticsCounter, Integrand)>,
3859    > = sample_chunks
3860        .zip(integrands)
3861        .map(|(chunk, mut integrand)| {
3862            let raw_batch = integrand.evaluate_samples_raw(
3863                chunk,
3864                EvaluationTarget::Physical(model),
3865                iter,
3866                false,
3867                false,
3868                max_eval,
3869            )?;
3870            Ok((raw_batch.samples, raw_batch.statistics, integrand))
3871        })
3872        .collect();
3873    let mut evaluation_results_per_core: Vec<(
3874        Vec<EvaluationResult>,
3875        StatisticsCounter,
3876        Integrand,
3877    )> = evaluation_results_per_core
3878        .into_iter()
3879        .collect::<Result<_>>()?;
3880
3881    for (_, _, worker_integrand) in evaluation_results_per_core.iter_mut() {
3882        integrand.merge_runtime_results(worker_integrand)?;
3883    }
3884
3885    let (evaluation_results, meta_data_statistics) = evaluation_results_per_core.into_iter().fold(
3886        (Vec::new(), StatisticsCounter::new_empty()),
3887        |(mut all_results, stats), (results, worker_stats, _)| {
3888            all_results.extend(results);
3889            (all_results, stats.merged(&worker_stats))
3890        },
3891    );
3892
3893    Ok((evaluation_results, meta_data_statistics))
3894}
3895
3896/// Different ways of passing samples to the batch_integrate function
3897/// One is simply a list of samples, the other is a grid and the desired number of samples
3898/// The worker then generates the samples itself.
3899#[derive(Serialize, Deserialize)]
3900pub enum SampleInput {
3901    SampleList {
3902        samples: Vec<Sample<F<f64>>>,
3903    },
3904    Grid {
3905        grid: Box<Grid<F<f64>>>,
3906        num_points: usize,
3907        seed: u64,
3908        thread_id: usize,
3909    },
3910}
3911
3912type AccumulatorPair = (StatisticsAccumulator<F<f64>>, StatisticsAccumulator<F<f64>>);
3913
3914#[derive(Serialize, Deserialize)]
3915pub enum BatchIntegrateOutput {
3916    Default(Vec<Complex<F<f64>>>, Vec<Sample<F<f64>>>),
3917    Accumulator(Box<AccumulatorPair>, Box<Grid<F<f64>>>),
3918}
3919
3920/// Different ways of processing events, EventList is a list of events, Histogram does accumulation of events on the worker nodes, so the
3921/// master node only has to merge the histograms.
3922#[derive(Serialize, Deserialize)]
3923pub enum EventOutput {
3924    None,
3925    EventList {
3926        event_groups: EventGroupList,
3927    },
3928    Histogram {
3929        histograms: ObservableAccumulatorBundle,
3930    },
3931}
3932
3933/// The result of evaluating a batch of points
3934#[derive(Serialize, Deserialize, Encode, Decode)]
3935pub struct BatchResult {
3936    pub statistics: StatisticsCounter,
3937    #[bincode(with_serde)]
3938    pub integrand_data: BatchIntegrateOutput,
3939    #[bincode(with_serde)]
3940    pub event_data: EventOutput,
3941}
3942
3943/// Input for the batch_integrate function, created by the master node
3944pub struct BatchIntegrateInput<'a> {
3945    // global run info:
3946    pub max_eval: Complex<F<f64>>,
3947    pub iter: usize,
3948    pub settings: &'a RuntimeSettings,
3949    // input data:
3950    pub samples: SampleInput,
3951    pub integrand_output_settings: IntegralOutputSettings,
3952    pub event_output_settings: EventOutputSettings,
3953    pub num_cores: usize,
3954}
3955
3956/// Choose whether to output the integrand results, or a statistics accumulator
3957#[derive(Serialize, Deserialize)]
3958pub enum IntegralOutputSettings {
3959    Default,
3960    Accumulator,
3961}
3962
3963/// Choose whether to output an eventlist, a histogram, or nothing
3964#[derive(Serialize, Deserialize)]
3965pub enum EventOutputSettings {
3966    None,
3967    EventList,
3968    Histogram,
3969}
3970
3971#[derive(Serialize, Deserialize, Encode, Decode)]
3972pub struct SerializableBatchIntegrateInput {
3973    #[bincode(with_serde)]
3974    pub max_eval: Complex<F<f64>>,
3975    pub iter: usize,
3976    #[bincode(with_serde)]
3977    pub samples: SampleInput,
3978    #[bincode(with_serde)]
3979    pub integrand_output_settings: IntegralOutputSettings,
3980    #[bincode(with_serde)]
3981    pub event_output_settings: EventOutputSettings,
3982    pub num_cores: usize,
3983}
3984
3985impl SerializableBatchIntegrateInput {
3986    pub(crate) fn into_batch_integrate_input(
3987        self,
3988        settings: &RuntimeSettings,
3989    ) -> BatchIntegrateInput<'_> {
3990        BatchIntegrateInput {
3991            max_eval: self.max_eval,
3992            iter: self.iter,
3993            samples: self.samples,
3994            integrand_output_settings: self.integrand_output_settings,
3995            event_output_settings: self.event_output_settings,
3996            num_cores: self.num_cores,
3997            settings,
3998        }
3999    }
4000}
4001
4002/// Master node which accumulates data from jobs, and creates the input for jobs
4003#[derive(Clone)]
4004pub struct MasterNode {
4005    grid: Grid<F<f64>>,
4006    integrator_settings: IntegratorSettings,
4007    master_accumulator_re: StatisticsAccumulator<F<f64>>,
4008    master_accumulator_im: StatisticsAccumulator<F<f64>>,
4009    statistics: StatisticsCounter,
4010    observable_accumulators: Option<ObservableAccumulatorBundle>,
4011    event_groups: EventGroupList,
4012    current_iter: usize,
4013}
4014
4015impl MasterNode {
4016    pub(crate) fn new(grid: Grid<F<f64>>, integrator_settings: IntegratorSettings) -> Self {
4017        MasterNode {
4018            grid,
4019            integrator_settings,
4020            master_accumulator_im: StatisticsAccumulator::new(),
4021            master_accumulator_re: StatisticsAccumulator::new(),
4022            statistics: StatisticsCounter::new_empty(),
4023            observable_accumulators: None,
4024            event_groups: EventGroupList::default(),
4025            current_iter: 0,
4026        }
4027    }
4028
4029    /// Update the grid with the data from another grid.
4030    fn update_grid_with_grid(&mut self, other_grid: &Grid<F<f64>>) -> Result<(), String> {
4031        self.grid.merge(other_grid)
4032    }
4033
4034    /// Update the grid with the data from a set of samples.
4035    fn update_grid_with_samples(
4036        &mut self,
4037        samples_points: &[Sample<F<f64>>],
4038        results: &[Complex<F<f64>>],
4039    ) -> Result<(), String> {
4040        let integrated_phase = self.integrator_settings.integrated_phase;
4041
4042        for (sample_point, result) in samples_points.iter().zip(results.iter()) {
4043            match integrated_phase {
4044                IntegratedPhase::Real => self.grid.add_training_sample(sample_point, result.re)?,
4045                IntegratedPhase::Imag => self.grid.add_training_sample(sample_point, result.im)?,
4046                IntegratedPhase::Both => {
4047                    unimplemented!("integrated phase both not yet implemented")
4048                }
4049            }
4050        }
4051
4052        Ok(())
4053    }
4054
4055    /// Update the accumulators with the data from another set of accumulators.
4056    pub(crate) fn update_accumulators_with_accumulators(
4057        &mut self,
4058        mut real_accumulator: StatisticsAccumulator<F<f64>>,
4059        mut imaginary_accumulator: StatisticsAccumulator<F<f64>>,
4060    ) {
4061        self.master_accumulator_re
4062            .merge_samples(&mut real_accumulator);
4063
4064        self.master_accumulator_im
4065            .merge_samples(&mut imaginary_accumulator);
4066    }
4067
4068    /// Update the accumulators with the data from a set of samples.
4069    fn update_accumuators_with_samples(
4070        &mut self,
4071        sample_points: &[Sample<F<f64>>],
4072        results: &[Complex<F<f64>>],
4073    ) {
4074        for (sample_point, result) in sample_points.iter().zip(results.iter()) {
4075            self.master_accumulator_re
4076                .add_sample(result.re * sample_point.get_weight(), Some(sample_point));
4077
4078            self.master_accumulator_im
4079                .add_sample(result.im * sample_point.get_weight(), Some(sample_point));
4080        }
4081    }
4082
4083    /// Update the metadata statistics with the data from another set of metadata statistics.
4084    fn update_metadata_statistics(&mut self, statistics: StatisticsCounter) {
4085        self.statistics = self.statistics.merged(&statistics);
4086    }
4087
4088    /// Finish the current iteration. This should be called after all jobs have been processed.
4089    pub(crate) fn update_iter(&mut self) {
4090        self.grid.update(
4091            F(self.integrator_settings.discrete_dim_learning_rate),
4092            F(self.integrator_settings.continuous_dim_learning_rate),
4093        );
4094        self.master_accumulator_re.update_iter(false);
4095        self.master_accumulator_im.update_iter(false);
4096        if let Some(observable_accumulators) = self.observable_accumulators.as_mut() {
4097            observable_accumulators.update_results();
4098        }
4099
4100        self.current_iter += 1;
4101    }
4102
4103    /// Write the input for a batch job to a file.
4104    pub(crate) fn write_batch_input(
4105        &mut self,
4106        num_cores: usize,
4107        num_samples: usize,
4108        export_grid: bool,
4109        output_accumulator: bool,
4110        workspace_path: &str,
4111        job_id: usize,
4112    ) -> Result<(), Report> {
4113        let max_eval = Complex::new(
4114            self.master_accumulator_re
4115                .max_eval_positive
4116                .max(self.master_accumulator_re.max_eval_negative),
4117            self.master_accumulator_im
4118                .max_eval_positive
4119                .max(self.master_accumulator_im.max_eval_negative),
4120        );
4121
4122        let samples = if export_grid {
4123            SampleInput::Grid {
4124                grid: Box::new(self.grid.clone()),
4125                num_points: num_samples,
4126                seed: self.integrator_settings.seed,
4127                thread_id: job_id,
4128            }
4129        } else {
4130            let mut rng = rand::rng();
4131            let mut samples_temp = vec![Sample::new(); num_samples];
4132            for sample in samples_temp.iter_mut() {
4133                self.grid.sample(&mut rng, sample);
4134            }
4135            SampleInput::SampleList {
4136                samples: samples_temp,
4137            }
4138        };
4139
4140        let integrand_output_settings = if output_accumulator {
4141            IntegralOutputSettings::Accumulator
4142        } else {
4143            IntegralOutputSettings::Default
4144        };
4145
4146        let input = SerializableBatchIntegrateInput {
4147            num_cores,
4148            max_eval,
4149            iter: self.current_iter,
4150            samples,
4151            integrand_output_settings,
4152            event_output_settings: EventOutputSettings::None,
4153        };
4154
4155        let input_bytes = bincode::encode_to_vec(&input, bincode::config::standard())?;
4156        let job_name = format!("job_{}", job_id);
4157        let job_path = std::path::Path::new(workspace_path).join(job_name);
4158
4159        std::fs::write(job_path, input_bytes)?;
4160
4161        Ok(())
4162    }
4163
4164    /// Process the output of a batch job.
4165    pub(crate) fn process_batch_output(&mut self, output: BatchResult) -> Result<(), String> {
4166        self.update_metadata_statistics(output.statistics);
4167
4168        match output.integrand_data {
4169            BatchIntegrateOutput::Default(results, samples) => {
4170                self.update_accumuators_with_samples(&samples, &results);
4171                self.update_grid_with_samples(&samples, &results)?;
4172            }
4173            BatchIntegrateOutput::Accumulator(accumulators, grid) => {
4174                let (real_accumulator, imag_accumulator) = *accumulators;
4175                self.update_accumulators_with_accumulators(real_accumulator, imag_accumulator);
4176                self.update_grid_with_grid(&grid)?;
4177            }
4178        }
4179
4180        match output.event_data {
4181            EventOutput::None => {}
4182            EventOutput::EventList { mut event_groups } => {
4183                self.event_groups.append(&mut event_groups);
4184            }
4185            EventOutput::Histogram { mut histograms } => {
4186                if let Some(existing) = self.observable_accumulators.as_mut() {
4187                    existing
4188                        .merge_samples(&mut histograms)
4189                        .map_err(|err| err.to_string())?;
4190                } else {
4191                    self.observable_accumulators = Some(histograms);
4192                }
4193            }
4194        }
4195
4196        Ok(())
4197    }
4198
4199    /// Display the current status of the integration. Usually called after each iteration.
4200    pub(crate) fn display_status(&self) {
4201        let status_block = [
4202            render_integral_result(
4203                &self.master_accumulator_re,
4204                "itg",
4205                self.current_iter,
4206                "re",
4207                None,
4208            ),
4209            render_integral_result(
4210                &self.master_accumulator_im,
4211                "itg",
4212                self.current_iter,
4213                "im",
4214                None,
4215            ),
4216            self.statistics.render_status_table(),
4217        ]
4218        .join("\n");
4219
4220        info!("\n{status_block}");
4221    }
4222}
4223
4224pub fn emit_integration_status_via_tracing(
4225    kind: IntegrationStatusKind,
4226    status_block: impl AsRef<str>,
4227) -> Result<()> {
4228    let status_block = status_block.as_ref();
4229    match kind {
4230        IntegrationStatusKind::Live => {}
4231        IntegrationStatusKind::Iteration => {
4232            info!("\n{status_block}");
4233            info!("");
4234        }
4235        IntegrationStatusKind::Final => {
4236            info!("");
4237            info!("{}", "Final integration results:".bold().green());
4238            info!("");
4239            info!("\n{status_block}");
4240            info!("");
4241        }
4242    }
4243
4244    Ok(())
4245}
4246
4247pub fn render_saved_integration_summary(
4248    integration_state: &IntegrationState,
4249    targets: &[Option<Complex<F<f64>>>],
4250    view_options: &IntegrationStatusViewOptions,
4251    tabled_options: &TabledRenderOptions,
4252) -> String {
4253    render_tabled::render_status_update(
4254        &build_saved_status_update(integration_state, targets, view_options),
4255        tabled_options,
4256    )
4257}
4258
4259pub fn render_status_update_tabled(
4260    update: &StatusUpdate,
4261    tabled_options: &TabledRenderOptions,
4262) -> String {
4263    render_tabled::render_status_update(update, tabled_options)
4264}
4265
4266fn integral_estimate(
4267    re: &StatisticsAccumulator<F<f64>>,
4268    im: &StatisticsAccumulator<F<f64>>,
4269) -> IntegralEstimate {
4270    IntegralEstimate {
4271        neval: re.processed_samples,
4272        real_zero: re.num_zero_evaluations,
4273        im_zero: im.num_zero_evaluations,
4274        result: Complex::new(re.avg, im.avg),
4275        error: Complex::new(re.err, im.err),
4276        real_chisq: re.chi_sq,
4277        im_chisq: im.chi_sq,
4278    }
4279}
4280
4281pub fn build_integration_result(
4282    integration_state: &IntegrationState,
4283    targets: &[Option<Complex<F<f64>>>],
4284) -> IntegrationResult {
4285    let integration_statistics = integration_state.stats.snapshot();
4286    let slots = integration_state
4287        .slot_metas
4288        .iter()
4289        .enumerate()
4290        .map(|(slot_index, slot_meta)| {
4291            let accumulator = &integration_state.all_integrals[slot_index];
4292            let discrete_context =
4293                integration_state.monitored_discrete_context_for_slot(slot_index);
4294            SlotIntegrationResult {
4295                key: slot_meta.key(),
4296                process: slot_meta.process_name.clone(),
4297                integrand: slot_meta.integrand_name.clone(),
4298                target: targets[slot_index],
4299                integral: integral_estimate(&accumulator.re, &accumulator.im),
4300                table_results: build_table_result_summary(
4301                    slot_meta,
4302                    accumulator,
4303                    integration_state.iter,
4304                    targets[slot_index],
4305                ),
4306                integration_statistics: integration_statistics.clone(),
4307                max_weight_info: build_max_weight_info_summary(
4308                    &integration_state
4309                        .sampling_state_for_slot(slot_index)
4310                        .discrete_axis_labels,
4311                    accumulator,
4312                ),
4313                grid_breakdown: ComponentDiscreteBreakdown {
4314                    re: discrete_context.as_ref().and_then(|context| {
4315                        integration_state.slot_re_summaries[slot_index]
4316                            .as_ref()
4317                            .zip(
4318                                integration_state
4319                                    .slot_first_non_trivial_discrete_breakdown_metadata[slot_index]
4320                                    .as_ref(),
4321                            )
4322                            .and_then(|(summary, metadata)| {
4323                                summary_at_path(summary, &context.path).and_then(|summary| {
4324                                    summary.first_non_trivial_breakdown(metadata, &context.pdfs)
4325                                })
4326                            })
4327                    }),
4328                    im: discrete_context.as_ref().and_then(|context| {
4329                        integration_state.slot_im_summaries[slot_index]
4330                            .as_ref()
4331                            .zip(
4332                                integration_state
4333                                    .slot_first_non_trivial_discrete_breakdown_metadata[slot_index]
4334                                    .as_ref(),
4335                            )
4336                            .and_then(|(summary, metadata)| {
4337                                summary_at_path(summary, &context.path).and_then(|summary| {
4338                                    summary.first_non_trivial_breakdown(metadata, &context.pdfs)
4339                                })
4340                            })
4341                    }),
4342                },
4343                absolute: AbsoluteIntegrationResult {
4344                    integral: integral_estimate(&accumulator.absolute_re, &accumulator.absolute_im),
4345                    table_results: build_absolute_table_result_summary(
4346                        slot_meta,
4347                        accumulator,
4348                        integration_state.iter,
4349                    ),
4350                    max_weight_info: build_absolute_max_weight_info_summary(
4351                        &integration_state
4352                            .sampling_state_for_slot(slot_index)
4353                            .discrete_axis_labels,
4354                        accumulator,
4355                    ),
4356                    grid_breakdown: ComponentDiscreteBreakdown {
4357                        re: discrete_context.as_ref().and_then(|context| {
4358                            integration_state.slot_absolute_re_summaries[slot_index]
4359                                .as_ref()
4360                                .zip(
4361                                    integration_state
4362                                        .slot_first_non_trivial_discrete_breakdown_metadata
4363                                        [slot_index]
4364                                        .as_ref(),
4365                                )
4366                                .and_then(|(summary, metadata)| {
4367                                    summary.first_non_trivial_breakdown(metadata, &context.pdfs)
4368                                })
4369                        }),
4370                        im: discrete_context.as_ref().and_then(|context| {
4371                            integration_state.slot_absolute_im_summaries[slot_index]
4372                                .as_ref()
4373                                .zip(
4374                                    integration_state
4375                                        .slot_first_non_trivial_discrete_breakdown_metadata
4376                                        [slot_index]
4377                                        .as_ref(),
4378                                )
4379                                .and_then(|(summary, metadata)| {
4380                                    summary.first_non_trivial_breakdown(metadata, &context.pdfs)
4381                                })
4382                        }),
4383                    },
4384                },
4385            }
4386        })
4387        .collect();
4388
4389    IntegrationResult { slots }
4390}
4391
4392pub fn print_integral_result(
4393    itg: &StatisticsAccumulator<F<f64>>,
4394    label: &str,
4395    i_iter: usize,
4396    tag: &str,
4397    trgt: Option<F<f64>>,
4398) {
4399    info!("{}", render_integral_result(itg, label, i_iter, tag, trgt));
4400}
4401
4402#[allow(clippy::format_in_format_args)]
4403fn render_integral_result(
4404    itg: &StatisticsAccumulator<F<f64>>,
4405    label: &str,
4406    i_iter: usize,
4407    tag: &str,
4408    trgt: Option<F<f64>>,
4409) -> String {
4410    let slot_meta = SlotMeta {
4411        process_name: label.to_string(),
4412        integrand_name: String::new(),
4413    };
4414    let mut cells = build_integral_result_cells(itg, &slot_meta, i_iter, tag, trgt);
4415    cells.integrand = format!("{label} {}:", format!("{:-2}", tag).blue().bold());
4416    let delta = match (cells.delta_sigma.as_ref(), cells.delta_percent.as_ref()) {
4417        (Some(delta_sigma), Some(delta_percent)) => format!("{delta_sigma}, {delta_percent}"),
4418        _ => String::new(),
4419    };
4420
4421    format!(
4422        "|  {} {} {} {} {} {}",
4423        cells.integrand, cells.value, cells.relative_error, cells.chi_sq, delta, cells.mwi
4424    )
4425}
4426
4427#[cfg(test)]
4428mod tests {
4429    use super::*;
4430    use crate::integrands::TestProbeIntegrand;
4431    use colored::control;
4432    use ratatui::{Terminal, backend::TestBackend};
4433    use std::fs;
4434    use symbolica::numerical_integration::ContinuousGrid;
4435
4436    fn integration_workspace_manifest(version: Option<u32>) -> IntegrationWorkspaceManifest {
4437        let mut manifest = serde_json::json!({
4438            "slots": [],
4439            "targets": [],
4440            "effective_model_parameters": [],
4441            "integrand_fingerprints": [],
4442            "training_slot": 0,
4443            "integrator_settings_slot": 0,
4444            "sampling_correlation_mode": "correlated"
4445        });
4446        if let Some(version) = version {
4447            manifest["version"] = version.into();
4448        }
4449        serde_json::from_value(manifest).unwrap()
4450    }
4451
4452    #[test]
4453    fn integration_workspace_manifest_accepts_only_the_current_version() {
4454        integration_workspace_manifest(Some(IntegrationWorkspaceManifest::CURRENT_VERSION))
4455            .validate_version()
4456            .unwrap();
4457
4458        let unversioned = integration_workspace_manifest(None);
4459        assert_eq!(unversioned.version, 0);
4460        let unversioned_error = unversioned.validate_version().unwrap_err().to_string();
4461        assert!(unversioned_error.contains("unversioned"));
4462        assert!(unversioned_error.contains("--restart"));
4463
4464        let previous = integration_workspace_manifest(Some(2));
4465        let previous_error = previous.validate_version().unwrap_err().to_string();
4466        assert!(previous_error.contains("--restart"));
4467
4468        let future =
4469            integration_workspace_manifest(Some(IntegrationWorkspaceManifest::CURRENT_VERSION + 1));
4470        let future_error = future.validate_version().unwrap_err().to_string();
4471        assert!(future_error.contains("newer format version"));
4472        assert!(future_error.contains("--restart"));
4473    }
4474
4475    fn make_accumulator(
4476        re_avg: f64,
4477        re_err: f64,
4478        re_chi_sq: f64,
4479        im_avg: f64,
4480        im_err: f64,
4481        im_chi_sq: f64,
4482    ) -> ComplexAccumulator {
4483        let mut accumulator = ComplexAccumulator::new();
4484        accumulator.re.avg = F(re_avg);
4485        accumulator.re.err = F(re_err);
4486        accumulator.re.chi_sq = F(re_chi_sq);
4487        accumulator.re.processed_samples = 100_000;
4488        accumulator.re.max_eval_positive = F(1.0);
4489        accumulator.im.avg = F(im_avg);
4490        accumulator.im.err = F(im_err);
4491        accumulator.im.chi_sq = F(im_chi_sq);
4492        accumulator.im.processed_samples = 100_000;
4493        accumulator.im.max_eval_positive = F(1.0);
4494        accumulator.absolute_re.avg = F(re_avg.abs() * 2.0);
4495        accumulator.absolute_re.err = F(re_err.abs());
4496        accumulator.absolute_re.chi_sq = F(re_chi_sq);
4497        accumulator.absolute_re.processed_samples = 100_000;
4498        accumulator.absolute_re.max_eval_positive = F(2.0);
4499        accumulator.absolute_im.avg = F(im_avg.abs() * 2.0);
4500        accumulator.absolute_im.err = F(im_err.abs());
4501        accumulator.absolute_im.chi_sq = F(im_chi_sq);
4502        accumulator.absolute_im.processed_samples = 100_000;
4503        accumulator.absolute_im.max_eval_positive = F(2.0);
4504        accumulator
4505    }
4506
4507    #[test]
4508    fn absolute_channel_sum_retains_outer_cube_covariance() {
4509        let mut accumulator = ComplexAccumulator::new();
4510        // The two channel points give (+a,-a): signed cancellation is exact,
4511        // but the physical absolute observation is 2a. Its variance includes
4512        // the perfect cross-channel covariance on each shared outer cube.
4513        for a in [1.0, 2.0, 3.0] {
4514            accumulator.add_sample(
4515                Complex::new_zero(),
4516                Some(Complex::new(F(2.0 * a), F(4.0 * a))),
4517                F(1.0),
4518                None,
4519            );
4520        }
4521        accumulator.update_iter(false);
4522        assert_eq!(accumulator.re.avg, F(0.0));
4523        assert_eq!(accumulator.im.avg, F(0.0));
4524        assert_eq!(accumulator.absolute_re.avg, F(4.0));
4525        assert_eq!(accumulator.absolute_im.avg, F(8.0));
4526        assert_eq!(accumulator.absolute_re.processed_samples, 3);
4527        assert!((accumulator.absolute_re.err - F((4.0_f64 / 3.0).sqrt())).abs() < F(1.0e-12));
4528        assert!((accumulator.absolute_im.err - F((16.0_f64 / 3.0).sqrt())).abs() < F(1.0e-12));
4529    }
4530
4531    #[test]
4532    fn complex_accumulator_tracks_componentwise_absolute_weighted_samples_and_merges() {
4533        let first_sample = Sample::Continuous(F(1.0), vec![F(0.25)]);
4534        let second_sample = Sample::Continuous(F(1.0), vec![F(0.75)]);
4535        let mut accumulator = ComplexAccumulator::new();
4536        let mut other = ComplexAccumulator::new();
4537
4538        accumulator.add_sample(
4539            Complex::new(F(-2.0), F(3.0)),
4540            None,
4541            F(2.0),
4542            Some(&first_sample),
4543        );
4544        other.add_sample(
4545            Complex::new(F(1.0), F(-4.0)),
4546            None,
4547            F(2.0),
4548            Some(&second_sample),
4549        );
4550        accumulator.merge(&other);
4551        accumulator.update_iter(false);
4552
4553        assert_eq!(accumulator.re.avg, F(-1.0));
4554        assert_eq!(accumulator.im.avg, F(-1.0));
4555        assert_eq!(accumulator.absolute_re.avg, F(3.0));
4556        assert_eq!(accumulator.absolute_im.avg, F(7.0));
4557        assert!((accumulator.absolute_re.err - F(1.0)).abs() < F(1.0e-12));
4558        assert!((accumulator.absolute_im.err - F(1.0)).abs() < F(1.0e-12));
4559        assert_eq!(accumulator.absolute_re.processed_samples, 2);
4560        assert_eq!(accumulator.absolute_im.processed_samples, 2);
4561        assert_eq!(accumulator.absolute_re.max_eval_positive, F(4.0));
4562        assert_eq!(accumulator.absolute_im.max_eval_positive, F(8.0));
4563        assert_eq!(
4564            accumulator
4565                .absolute_re
4566                .max_eval_positive_xs
4567                .as_ref()
4568                .map(|sample| format!("{sample:?}")),
4569            Some(format!("{first_sample:?}"))
4570        );
4571        assert_eq!(
4572            accumulator
4573                .absolute_im
4574                .max_eval_positive_xs
4575                .as_ref()
4576                .map(|sample| format!("{sample:?}")),
4577            Some(format!("{second_sample:?}"))
4578        );
4579    }
4580
4581    #[test]
4582    fn shallow_absolute_monitor_uses_local_path_pdfs_and_has_no_descendants() {
4583        let continuous_grid =
4584            || Grid::Continuous(ContinuousGrid::new(1, 8, 10, None, false).unwrap());
4585        let monitored_grid = Grid::Discrete(
4586            DiscreteGrid::new(
4587                vec![Some(continuous_grid()), Some(continuous_grid())],
4588                F(10.0),
4589                false,
4590            )
4591            .unwrap(),
4592        );
4593        let mut production_grid =
4594            Grid::Discrete(DiscreteGrid::new(vec![Some(monitored_grid)], F(10.0), false).unwrap());
4595        let Grid::Discrete(root) = &mut production_grid else {
4596            unreachable!()
4597        };
4598        let Some(Grid::Discrete(monitored)) = root.bins[0].sub_grid.as_mut() else {
4599            unreachable!()
4600        };
4601        monitored.bins[0].pdf = F(0.25);
4602        monitored.bins[1].pdf = F(0.75);
4603
4604        let mut monitor = shallow_discrete_monitor_grid(&production_grid, &[0])
4605            .expect("nested discrete path should produce a shallow monitor");
4606        assert_eq!(monitor.bins[0].pdf, F(0.25));
4607        assert_eq!(monitor.bins[1].pdf, F(0.75));
4608        assert!(monitor.bins.iter().all(|bin| bin.sub_grid.is_none()));
4609
4610        let sample = |bin_index, local_weight, x| {
4611            Sample::Discrete(
4612                F(local_weight),
4613                0,
4614                Some(Box::new(Sample::Discrete(
4615                    F(local_weight),
4616                    bin_index,
4617                    Some(Box::new(Sample::Continuous(F(1.0), vec![F(x)]))),
4618                ))),
4619            )
4620        };
4621        for (sample, evaluation) in [
4622            (sample(0, 4.0, 0.1), F(-3.0)),
4623            (sample(0, 4.0, 0.2), F(5.0)),
4624            (sample(1, 4.0 / 3.0, 0.3), F(-6.0)),
4625            (sample(1, 4.0 / 3.0, 0.4), F(10.0)),
4626        ] {
4627            add_shallow_monitor_sample(Some(&mut monitor), Some(&[0]), &sample, evaluation)
4628                .expect("valid local discrete sample should be accumulated");
4629        }
4630
4631        let mut summary = DiscreteGridAccumulatorSummary::from_shallow_grid(&monitor);
4632        summary.merge_shallow_iteration_grid(&monitor);
4633        summary.update_iter();
4634        assert!(summary.bins.iter().all(|bin| bin.sub_summary.is_none()));
4635        assert_eq!(summary.bins[0].accumulator.avg, F(4.0));
4636        assert_eq!(summary.bins[1].accumulator.avg, F(8.0));
4637        assert_eq!(summary.bins[0].accumulator.processed_samples, 2);
4638        assert_eq!(summary.bins[1].accumulator.processed_samples, 2);
4639
4640        let mismatched_sample =
4641            Sample::Discrete(F(1.0), 1, Some(Box::new(Sample::Discrete(F(1.0), 0, None))));
4642        assert!(
4643            add_shallow_monitor_sample(Some(&mut monitor), Some(&[0]), &mismatched_sample, F(1.0),)
4644                .is_err()
4645        );
4646    }
4647
4648    #[test]
4649    fn shallow_monitor_creation_does_not_change_production_sampling_sequence() {
4650        let continuous_grid =
4651            || Grid::Continuous(ContinuousGrid::new(1, 8, 10, None, false).unwrap());
4652        let monitored_grid = Grid::Discrete(
4653            DiscreteGrid::new(
4654                vec![Some(continuous_grid()), Some(continuous_grid())],
4655                F(10.0),
4656                false,
4657            )
4658            .unwrap(),
4659        );
4660        let sampling_grid =
4661            Grid::Discrete(DiscreteGrid::new(vec![Some(monitored_grid)], F(10.0), false).unwrap());
4662        let settings = RuntimeSettings::default();
4663        let integrand = Integrand::TestProbe(TestProbeIntegrand::new(settings, 3));
4664        let mut without_monitor = CoreIterationState::new(
4665            vec![integrand.clone()],
4666            SamplingCorrelationMode::Correlated,
4667            std::slice::from_ref(&sampling_grid),
4668            None,
4669            37,
4670            11,
4671            0,
4672        );
4673        let mut with_monitor = CoreIterationState::new(
4674            vec![integrand],
4675            SamplingCorrelationMode::Correlated,
4676            std::slice::from_ref(&sampling_grid),
4677            Some(&[0]),
4678            37,
4679            11,
4680            0,
4681        );
4682
4683        for _ in 0..16 {
4684            let mut without_sample = Sample::new();
4685            let mut with_sample = Sample::new();
4686            let without_state = &mut without_monitor.sampling_states[0];
4687            without_state
4688                .sampling_grid
4689                .sample(&mut without_state.rng, &mut without_sample);
4690            let with_state = &mut with_monitor.sampling_states[0];
4691            with_state
4692                .sampling_grid
4693                .sample(&mut with_state.rng, &mut with_sample);
4694            assert_eq!(format!("{without_sample:?}"), format!("{with_sample:?}"));
4695        }
4696    }
4697
4698    struct StatisticsFixture {
4699        precision: crate::settings::runtime::Precision,
4700        total_time: Duration,
4701        integrand_time: Duration,
4702        evaluator_time: Duration,
4703        parameterization_time: Duration,
4704        event_time: Duration,
4705        generated_event_count: usize,
4706        accepted_event_count: usize,
4707    }
4708
4709    fn make_statistics_counter(fixture: StatisticsFixture) -> StatisticsCounter {
4710        let mut evaluation = EvaluationResult::zero();
4711        evaluation.evaluation_metadata.total_timing = fixture.total_time;
4712        evaluation.evaluation_metadata.integrand_evaluation_time = fixture.integrand_time;
4713        evaluation.evaluation_metadata.evaluator_evaluation_time = fixture.evaluator_time;
4714        evaluation.evaluation_metadata.parameterization_time = fixture.parameterization_time;
4715        evaluation.evaluation_metadata.event_processing_time = fixture.event_time;
4716        evaluation.evaluation_metadata.generated_event_count = fixture.generated_event_count;
4717        evaluation.evaluation_metadata.accepted_event_count = fixture.accepted_event_count;
4718        let estimated_relative_accuracy = match fixture.precision {
4719            crate::settings::runtime::Precision::Double => Some(F(2.4e-16)),
4720            crate::settings::runtime::Precision::Quad => Some(F(1.3e-26)),
4721            crate::settings::runtime::Precision::Arb => None,
4722        };
4723        evaluation.evaluation_metadata.stability_results.push(
4724            crate::integrands::evaluation::StabilityResult {
4725                precision: fixture.precision,
4726                estimated_relative_accuracy,
4727                estimated_decimal_digits: estimated_relative_accuracy
4728                    .filter(|accuracy| accuracy.0 != 0.0 && accuracy.0.is_finite())
4729                    .map(|accuracy| F(-accuracy.0.abs().log10())),
4730                status: crate::integrands::evaluation::StabilityStatus::Stable(2),
4731                total_time: fixture.total_time,
4732            },
4733        );
4734        StatisticsCounter::from_evaluation_results(&[evaluation])
4735    }
4736
4737    fn make_integration_state() -> IntegrationState {
4738        let sampling_grid = Grid::Continuous(ContinuousGrid::new(1, 64, 100, None, false).unwrap());
4739        let mut state = IntegrationState::new_from_settings(
4740            SamplingCorrelationMode::Correlated,
4741            vec![SamplingSlotState::new(sampling_grid, Vec::new())],
4742            vec![
4743                SlotMeta {
4744                    process_name: "proc_a".to_string(),
4745                    integrand_name: "itg_a".to_string(),
4746                },
4747                SlotMeta {
4748                    process_name: "proc_b".to_string(),
4749                    integrand_name: "itg_b".to_string(),
4750                },
4751            ],
4752            None,
4753            None,
4754            None,
4755            vec![None, None],
4756        );
4757        state.iter = 1;
4758        state.num_points = 100_000;
4759        state.slot_stats = vec![
4760            make_statistics_counter(StatisticsFixture {
4761                precision: crate::settings::runtime::Precision::Double,
4762                total_time: Duration::from_micros(462),
4763                integrand_time: Duration::from_micros(414),
4764                evaluator_time: Duration::from_micros(279),
4765                parameterization_time: Duration::from_nanos(5_800),
4766                event_time: Duration::from_micros(12),
4767                generated_event_count: 7,
4768                accepted_event_count: 5,
4769            }),
4770            make_statistics_counter(StatisticsFixture {
4771                precision: crate::settings::runtime::Precision::Quad,
4772                total_time: Duration::from_micros(900),
4773                integrand_time: Duration::from_micros(810),
4774                evaluator_time: Duration::from_micros(120),
4775                parameterization_time: Duration::from_micros(34),
4776                event_time: Duration::from_micros(40),
4777                generated_event_count: 9,
4778                accepted_event_count: 6,
4779            }),
4780        ];
4781        state.stats = state.slot_stats[0].merged(&state.slot_stats[1]);
4782        state
4783            .stats
4784            .add_integrator_overhead(Duration::from_micros(110), 2);
4785        state.all_integrals = vec![
4786            make_accumulator(7.5e-5, 9.8e-5, 0.394, 3.2e-5, 1.5e-5, 0.378),
4787            make_accumulator(2.1e-5, 2.0e-6, 0.221, -1.7e-5, 3.0e-6, 0.187),
4788        ];
4789        state
4790    }
4791
4792    fn make_preview_test_slots(slot_metas: &[SlotMeta]) -> Vec<IntegrationSlot> {
4793        slot_metas
4794            .iter()
4795            .cloned()
4796            .map(|meta| {
4797                let settings = RuntimeSettings::default();
4798                IntegrationSlot::new(
4799                    meta,
4800                    settings.clone(),
4801                    Model::default(),
4802                    Integrand::TestProbe(TestProbeIntegrand::new(settings, 3)),
4803                    None,
4804                )
4805            })
4806            .collect()
4807    }
4808
4809    fn make_preview_test_core_state(state: &IntegrationState) -> CoreIterationState {
4810        let slot_stats = vec![
4811            make_statistics_counter(StatisticsFixture {
4812                precision: crate::settings::runtime::Precision::Arb,
4813                total_time: Duration::from_millis(8),
4814                integrand_time: Duration::from_millis(7),
4815                evaluator_time: Duration::from_millis(3),
4816                parameterization_time: Duration::from_micros(150),
4817                event_time: Duration::from_micros(500),
4818                generated_event_count: 40,
4819                accepted_event_count: 30,
4820            }),
4821            make_statistics_counter(StatisticsFixture {
4822                precision: crate::settings::runtime::Precision::Arb,
4823                total_time: Duration::from_millis(12),
4824                integrand_time: Duration::from_millis(10),
4825                evaluator_time: Duration::from_millis(2),
4826                parameterization_time: Duration::from_micros(300),
4827                event_time: Duration::from_millis(1),
4828                generated_event_count: 60,
4829                accepted_event_count: 45,
4830            }),
4831        ];
4832        let grid_template = state
4833            .sampling_state_for_slot(0)
4834            .grid
4835            .clone_without_samples();
4836        CoreIterationState {
4837            slot_integrands: Vec::new(),
4838            stats: slot_stats[0].merged(&slot_stats[1]),
4839            slot_stats,
4840            integrals: vec![ComplexAccumulator::new(); state.slot_metas.len()],
4841            sampling_correlation_mode: SamplingCorrelationMode::Correlated,
4842            sampling_states: vec![CoreSamplingSlotState {
4843                sampling_grid: grid_template.clone(),
4844                rng: MonteCarloRng::new(7, 0),
4845            }],
4846            slot_re_grids: (0..state.slot_metas.len())
4847                .map(|_| grid_template.clone())
4848                .collect(),
4849            slot_im_grids: (0..state.slot_metas.len())
4850                .map(|_| grid_template.clone())
4851                .collect(),
4852            monitored_discrete_path: None,
4853            slot_absolute_re_grids: vec![None; state.slot_metas.len()],
4854            slot_absolute_im_grids: vec![None; state.slot_metas.len()],
4855            remaining_points: 0,
4856            completed_points: 12,
4857            failed_samples: Vec::new(),
4858        }
4859    }
4860
4861    #[test]
4862    fn failed_sample_records_preserve_replay_and_append_across_iterations() {
4863        use crate::integrands::evaluation::{StabilityResult, StabilityStatus};
4864        use crate::settings::runtime::Precision;
4865
4866        let directory = std::env::temp_dir().join(format!(
4867            "gammaloop_failed_sample_records_{}",
4868            std::process::id()
4869        ));
4870        if directory.exists() {
4871            fs::remove_dir_all(&directory).unwrap();
4872        }
4873        let state = make_integration_state();
4874        let mut worker = make_preview_test_core_state(&state);
4875        let sample = Sample::Discrete(
4876            F(7.5),
4877            0,
4878            Some(Box::new(Sample::Discrete(
4879                F(2.5),
4880                3,
4881                Some(Box::new(Sample::Continuous(
4882                    F(1.25),
4883                    vec![F(0.12345678901234567), F(0.9)],
4884                ))),
4885            ))),
4886        );
4887        let mut metadata = EvaluationMetaData::new_empty();
4888        metadata.is_nan = true;
4889        metadata.record_threshold_counterterm_error("AlmostPrimalInfeasible: no interior center");
4890        for precision in [Precision::Double, Precision::Quad, Precision::Arb] {
4891            metadata.stability_results.push(StabilityResult {
4892                precision,
4893                estimated_relative_accuracy: None,
4894                estimated_decimal_digits: None,
4895                status: StabilityStatus::Unstable(1),
4896                total_time: Duration::ZERO,
4897            });
4898        }
4899        let path = directory.join("numerical_stability/failed_samples.jsonl");
4900        worker
4901            .write_failed_samples(Some(&directory), &state.slot_metas, 1)
4902            .unwrap();
4903        assert!(
4904            !path.exists(),
4905            "successful batches should not create a failure log"
4906        );
4907        for iteration in [1, 2] {
4908            worker
4909                .failed_samples
4910                .push((1, sample.clone(), metadata.clone()));
4911            worker
4912                .write_failed_samples(Some(&directory), &state.slot_metas, iteration)
4913                .unwrap();
4914            assert!(worker.failed_samples.is_empty());
4915            worker
4916                .write_failed_samples(Some(&directory), &state.slot_metas, iteration)
4917                .unwrap();
4918        }
4919        let records = fs::read_to_string(path).unwrap();
4920        assert_eq!(
4921            records.lines().count(),
4922            2,
4923            "drained records must not be duplicated"
4924        );
4925        for (index, line) in records.lines().enumerate() {
4926            let row: serde_json::Value = serde_json::from_str(line).unwrap();
4927            assert_eq!(row["iteration"], index + 1);
4928            assert_eq!(row["slot"]["process_name"], "proc_b");
4929            assert_eq!(row["evaluation_metadata"]["is_nan"], true);
4930            assert_eq!(
4931                row["evaluation_metadata"]["stability_results"]
4932                    .as_array()
4933                    .unwrap()
4934                    .len(),
4935                3
4936            );
4937            assert!(
4938                row["evaluation_metadata"]["threshold_counterterm_error"]
4939                    .as_str()
4940                    .unwrap()
4941                    .contains("AlmostPrimalInfeasible")
4942            );
4943            let replay: Sample<F<f64>> = serde_json::from_value(row["sample"].clone()).unwrap();
4944            assert_eq!(replay.get_weight(), sample.get_weight());
4945            assert_eq!(
4946                serde_json::to_value(replay).unwrap(),
4947                serde_json::to_value(&sample).unwrap()
4948            );
4949        }
4950        fs::remove_dir_all(directory).unwrap();
4951    }
4952
4953    fn make_discrete_integration_state() -> IntegrationState {
4954        let make_continuous_grid =
4955            || Grid::Continuous(ContinuousGrid::new(1, 64, 100, None, false).unwrap());
4956        let sampling_grid = Grid::Discrete(
4957            DiscreteGrid::new(
4958                vec![Some(make_continuous_grid()), Some(make_continuous_grid())],
4959                F(10.0),
4960                false,
4961            )
4962            .unwrap(),
4963        );
4964        let mut state = IntegrationState::new_from_settings(
4965            SamplingCorrelationMode::Correlated,
4966            vec![SamplingSlotState::new(
4967                sampling_grid,
4968                vec!["graph".to_string()],
4969            )],
4970            vec![
4971                SlotMeta {
4972                    process_name: "proc_a".to_string(),
4973                    integrand_name: "itg_a".to_string(),
4974                },
4975                SlotMeta {
4976                    process_name: "proc_b".to_string(),
4977                    integrand_name: "itg_b".to_string(),
4978                },
4979            ],
4980            Some(vec![]),
4981            Some("graph".to_string()),
4982            Some(vec!["GL0".to_string(), "GL1".to_string()]),
4983            vec![
4984                Some(PersistedDiscreteBreakdownMetadata {
4985                    axis_label: "graph".to_string(),
4986                    fixed_coordinates: Vec::new(),
4987                    bin_labels: vec!["GL0".to_string(), "GL1".to_string()],
4988                }),
4989                Some(PersistedDiscreteBreakdownMetadata {
4990                    axis_label: "graph".to_string(),
4991                    fixed_coordinates: Vec::new(),
4992                    bin_labels: vec!["GL0".to_string(), "GL1".to_string()],
4993                }),
4994            ],
4995        );
4996        state.first_non_trivial_discrete_bin_descriptions =
4997            Some(vec!["GL0".to_string(), "GL1".to_string()]);
4998        state.iter = 2;
4999        state.num_points = 210_000;
5000        state.slot_stats = vec![
5001            make_statistics_counter(StatisticsFixture {
5002                precision: crate::settings::runtime::Precision::Double,
5003                total_time: Duration::from_micros(462),
5004                integrand_time: Duration::from_micros(414),
5005                evaluator_time: Duration::from_micros(279),
5006                parameterization_time: Duration::from_nanos(5_800),
5007                event_time: Duration::from_micros(12),
5008                generated_event_count: 7,
5009                accepted_event_count: 5,
5010            }),
5011            make_statistics_counter(StatisticsFixture {
5012                precision: crate::settings::runtime::Precision::Quad,
5013                total_time: Duration::from_micros(900),
5014                integrand_time: Duration::from_micros(810),
5015                evaluator_time: Duration::from_micros(120),
5016                parameterization_time: Duration::from_micros(34),
5017                event_time: Duration::from_micros(40),
5018                generated_event_count: 9,
5019                accepted_event_count: 6,
5020            }),
5021        ];
5022        state.stats = state.slot_stats[0].merged(&state.slot_stats[1]);
5023        state
5024            .stats
5025            .add_integrator_overhead(Duration::from_micros(110), 2);
5026        state.all_integrals = vec![
5027            make_accumulator(7.5e-5, 9.8e-5, 0.394, 3.2e-5, 1.5e-5, 0.378),
5028            make_accumulator(2.1e-5, 2.0e-6, 0.221, -1.7e-5, 3.0e-6, 0.187),
5029        ];
5030        if let Grid::Discrete(discrete_grid) = &mut state.sampling_state_for_slot_mut(0).grid {
5031            discrete_grid.bins[0].pdf = F(0.75);
5032            discrete_grid.bins[1].pdf = F(0.25);
5033        }
5034
5035        for summaries in [&mut state.slot_re_summaries, &mut state.slot_im_summaries] {
5036            for (slot_index, summary) in summaries.iter_mut().enumerate() {
5037                let summary = summary.as_mut().expect("discrete summary expected");
5038                summary.bins[0].accumulator.avg = F(1.0e-5 * (slot_index as f64 + 1.0));
5039                summary.bins[0].accumulator.err = F(2.0e-6);
5040                summary.bins[0].accumulator.chi_sq = F(0.2);
5041                summary.bins[0].accumulator.processed_samples = 150;
5042                summary.bins[0].accumulator.max_eval_positive = F(0.75);
5043                summary.bins[0].accumulator.max_eval_positive_xs =
5044                    Some(Sample::Continuous(F(1.0), vec![F(0.25)]));
5045                summary.bins[1].accumulator.avg = F(5.0e-6 * (slot_index as f64 + 1.0));
5046                summary.bins[1].accumulator.err = F(1.0e-6);
5047                summary.bins[1].accumulator.chi_sq = F(0.1);
5048                summary.bins[1].accumulator.processed_samples = 50;
5049                summary.bins[1].accumulator.max_eval_positive = F(0.25);
5050                summary.bins[1].accumulator.max_eval_positive_xs =
5051                    Some(Sample::Continuous(F(1.0), vec![F(0.75)]));
5052            }
5053        }
5054        for (summaries, component_scale) in [
5055            (&mut state.slot_absolute_re_summaries, 4.0),
5056            (&mut state.slot_absolute_im_summaries, 3.0),
5057        ] {
5058            for (slot_index, summary) in summaries.iter_mut().enumerate() {
5059                let summary = summary.as_mut().expect("shallow absolute summary expected");
5060                let slot_scale = slot_index as f64 + 1.0;
5061                summary.bins[0].accumulator.avg = F(component_scale * 1.0e-5 * slot_scale);
5062                summary.bins[0].accumulator.err = F(3.0e-6);
5063                summary.bins[0].accumulator.chi_sq = F(0.3);
5064                summary.bins[0].accumulator.processed_samples = 150;
5065                summary.bins[0].accumulator.max_eval_positive = F(1.25);
5066                summary.bins[0].accumulator.max_eval_positive_xs =
5067                    Some(Sample::Continuous(F(1.0), vec![F(0.35)]));
5068                summary.bins[1].accumulator.avg = F(component_scale * 0.5e-5 * slot_scale);
5069                summary.bins[1].accumulator.err = F(1.5e-6);
5070                summary.bins[1].accumulator.chi_sq = F(0.15);
5071                summary.bins[1].accumulator.processed_samples = 50;
5072                summary.bins[1].accumulator.max_eval_positive = F(0.5);
5073                summary.bins[1].accumulator.max_eval_positive_xs =
5074                    Some(Sample::Continuous(F(1.0), vec![F(0.85)]));
5075            }
5076        }
5077
5078        state
5079    }
5080
5081    fn make_single_graph_discrete_integration_state() -> IntegrationState {
5082        let mut state = make_discrete_integration_state();
5083        let Grid::Discrete(grid) = &mut state.sampling_state_for_slot_mut(0).grid else {
5084            unreachable!("discrete fixture must use a discrete grid")
5085        };
5086        grid.bins.truncate(1);
5087        grid.bins[0].pdf = F(1.0);
5088        for summaries in [
5089            &mut state.slot_re_summaries,
5090            &mut state.slot_im_summaries,
5091            &mut state.slot_absolute_re_summaries,
5092            &mut state.slot_absolute_im_summaries,
5093        ] {
5094            for summary in summaries.iter_mut().flatten() {
5095                summary.bins.truncate(1);
5096            }
5097        }
5098        state.first_non_trivial_discrete_bin_descriptions = Some(vec!["GL22".to_string()]);
5099        for metadata in &mut state.slot_first_non_trivial_discrete_breakdown_metadata {
5100            if let Some(metadata) = metadata.as_mut() {
5101                metadata.bin_labels = vec!["GL22".to_string()];
5102            }
5103        }
5104        state
5105    }
5106
5107    fn default_view_options() -> IntegrationStatusViewOptions {
5108        IntegrationStatusViewOptions {
5109            phase_display: IntegrationStatusPhaseDisplay::Both,
5110            training_phase_display: IntegrationStatusPhaseDisplay::Real,
5111            training_slot: 0,
5112            slot_training_phase_displays: vec![IntegrationStatusPhaseDisplay::Real],
5113            per_slot_training_phase: false,
5114            target_relative_accuracy: None,
5115            target_absolute_accuracy: None,
5116            show_statistics: true,
5117            show_max_weight_details: true,
5118            show_top_discrete_grid: false,
5119            show_discrete_contributions_sum: false,
5120            contribution_sort: ContributionSortMode::Error,
5121            show_max_weight_info_for_discrete_bins: false,
5122        }
5123    }
5124
5125    fn default_tabled_options() -> TabledRenderOptions {
5126        TabledRenderOptions {
5127            max_table_width: DEFAULT_MAX_SHARED_TABLE_WIDTH,
5128            show_statistics: true,
5129            show_max_weight_details: true,
5130            show_top_discrete_grid: false,
5131            show_discrete_contributions_sum: false,
5132            show_max_weight_info_for_discrete_bins: false,
5133        }
5134    }
5135
5136    fn tabled_options_for_view(view_options: &IntegrationStatusViewOptions) -> TabledRenderOptions {
5137        TabledRenderOptions {
5138            show_statistics: view_options.show_statistics,
5139            show_max_weight_details: view_options.show_max_weight_details,
5140            show_top_discrete_grid: view_options.show_top_discrete_grid,
5141            show_discrete_contributions_sum: view_options.show_discrete_contributions_sum,
5142            show_max_weight_info_for_discrete_bins: view_options
5143                .show_max_weight_info_for_discrete_bins,
5144            ..default_tabled_options()
5145        }
5146    }
5147
5148    #[test]
5149    fn correlated_core_iteration_state_populates_slot_statistics() {
5150        let settings_a = RuntimeSettings::default();
5151        let settings_b = RuntimeSettings::default();
5152        let integrand_a = Integrand::TestProbe(TestProbeIntegrand::new(settings_a.clone(), 3));
5153        let integrand_b = Integrand::TestProbe(TestProbeIntegrand::new(settings_b.clone(), 3));
5154        let sampling_grid_template = SamplingSlotState::from_integrand(&integrand_a).grid;
5155        let mut core_state = CoreIterationState::new(
5156            vec![integrand_a, integrand_b],
5157            SamplingCorrelationMode::Correlated,
5158            &[sampling_grid_template],
5159            None,
5160            1337,
5161            0,
5162            8,
5163        );
5164        let model = Model::default();
5165        let slot_settings = [&settings_a, &settings_b];
5166        let slot_targets = [EvaluationTarget::Physical(&model); 2];
5167        let current_max_evals = [Complex::new(F(0.0), F(0.0)), Complex::new(F(0.0), F(0.0))];
5168
5169        let processed = core_state
5170            .evaluate_chunk(
5171                &slot_settings,
5172                &slot_targets,
5173                0,
5174                &current_max_evals,
5175                &[None, None],
5176                8,
5177            )
5178            .expect("correlated chunk evaluation should succeed");
5179
5180        assert_eq!(processed, 8);
5181        assert!(
5182            core_state
5183                .slot_stats
5184                .iter()
5185                .all(|stats| stats.snapshot().num_evals == processed),
5186            "{:?}",
5187            core_state
5188                .slot_stats
5189                .iter()
5190                .map(|stats| stats.snapshot().num_evals)
5191                .collect_vec()
5192        );
5193    }
5194
5195    fn render_update(request: StatusUpdateBuildRequest<'_>) -> String {
5196        let tabled_options = tabled_options_for_view(request.render_options);
5197        render_status_update_tabled(&build_status_update(request), &tabled_options)
5198    }
5199
5200    fn render_ratatui_update(
5201        request: StatusUpdateBuildRequest<'_>,
5202        configure: impl FnOnce(&mut RatatuiDashboardState),
5203    ) -> String {
5204        let mut dashboard = RatatuiDashboardState::new();
5205        dashboard.update(build_status_update(request));
5206        configure(&mut dashboard);
5207
5208        let backend = TestBackend::new(180, 48);
5209        let mut terminal = Terminal::new(backend).expect("test terminal");
5210        terminal
5211            .draw(|frame| dashboard.draw(frame))
5212            .expect("ratatui draw");
5213
5214        let buffer = terminal.backend().buffer();
5215        (0..buffer.area.height)
5216            .map(|y| {
5217                let mut line = String::new();
5218                for x in 0..buffer.area.width {
5219                    line.push_str(buffer[(x, y)].symbol());
5220                }
5221                line.trim_end().to_string()
5222            })
5223            .collect::<Vec<_>>()
5224            .join("\n")
5225    }
5226
5227    #[test]
5228    fn max_weight_details_table_renders_titled_table_without_wrapping_parentheses() {
5229        let mut accumulator = ComplexAccumulator::new();
5230        accumulator.re.max_eval_positive = F(2.3840672847728);
5231        accumulator.re.max_eval_positive_xs = Some(Sample::Discrete(
5232            F(1.0),
5233            0,
5234            Some(Box::new(Sample::Discrete(
5235                F(1.0),
5236                0,
5237                Some(Box::new(Sample::Continuous(
5238                    F(1.0),
5239                    vec![
5240                        F(0.9695746085826609),
5241                        F(0.5327714835649985),
5242                        F(0.003410284786539597),
5243                    ],
5244                ))),
5245            ))),
5246        ));
5247
5248        let mut state = IntegrationState::new_from_settings(
5249            SamplingCorrelationMode::Correlated,
5250            vec![SamplingSlotState::new(
5251                Grid::Continuous(ContinuousGrid::new(1, 64, 100, None, false).unwrap()),
5252                Vec::new(),
5253            )],
5254            vec![SlotMeta {
5255                process_name: "proc".to_string(),
5256                integrand_name: "itg".to_string(),
5257            }],
5258            None,
5259            None,
5260            None,
5261            vec![None],
5262        );
5263        state.all_integrals = vec![accumulator];
5264        let view_options = IntegrationStatusViewOptions {
5265            show_statistics: false,
5266            show_top_discrete_grid: false,
5267            show_discrete_contributions_sum: false,
5268            ..default_view_options()
5269        };
5270        let rendered = render_status_update_tabled(
5271            &build_status_update(
5272                StatusUpdateBuildRequest::new(
5273                    IntegrationStatusKind::Iteration,
5274                    &state,
5275                    &[None],
5276                    &view_options,
5277                )
5278                .with_timing(
5279                    1,
5280                    Duration::from_secs(0),
5281                    Duration::from_secs(0),
5282                    0,
5283                    0,
5284                    0,
5285                ),
5286            ),
5287            &default_tabled_options(),
5288        );
5289
5290        assert!(rendered.contains("Maximum weight details"), "{rendered}");
5291        assert!(rendered.contains("Integrand"), "{rendered}");
5292        assert!(rendered.contains("Max eval"), "{rendered}");
5293        assert!(rendered.contains("Max eval coordinates"), "{rendered}");
5294        assert!(rendered.contains("proc@itg"), "{rendered}");
5295        assert!(rendered.contains("re [+]"), "{rendered}");
5296        assert!(rendered.contains("idx: 0, idx: 0, xs: [ "), "{rendered}");
5297        assert!(rendered.contains("e-01"), "{rendered}");
5298        assert!(rendered.contains("e-03 ]"), "{rendered}");
5299        assert!(!rendered.contains(", 0.532"), "{rendered}");
5300        assert!(!rendered.contains("( graph:"), "{rendered}");
5301    }
5302
5303    #[test]
5304    fn iteration_status_block_uses_compact_header_and_optional_statistics() {
5305        let state = make_integration_state();
5306        let view_options = IntegrationStatusViewOptions {
5307            show_statistics: false,
5308            show_max_weight_details: false,
5309            ..default_view_options()
5310        };
5311        let rendered = render_update(
5312            StatusUpdateBuildRequest::new(
5313                IntegrationStatusKind::Iteration,
5314                &state,
5315                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5316                &view_options,
5317            )
5318            .with_timing(
5319                4,
5320                Duration::from_secs(1),
5321                Duration::from_secs(1),
5322                100_000,
5323                100_000,
5324                100_000,
5325            ),
5326        );
5327
5328        assert!(
5329            rendered.contains("Iteration #   1 ( completed )"),
5330            "{rendered}"
5331        );
5332        assert!(
5333            rendered.contains("# samples per iteration = 100.00K # samples total = 100.00K"),
5334            "{rendered}"
5335        );
5336        assert!(rendered.contains("/sample/core (4 cores)"), "{rendered}");
5337        assert!(rendered.contains("Contribution"), "{rendered}");
5338        assert!(rendered.contains("proc_a@itg_a"), "{rendered}");
5339        assert!(rendered.contains("proc_b@itg_b"), "{rendered}");
5340        assert!(rendered.contains("+7.5(9.8)e-5"), "{rendered}");
5341        assert!(rendered.contains("Δ = 0.255σ"), "{rendered}");
5342        assert!(!rendered.contains("Integration statistics"), "{rendered}");
5343        let lines = rendered.lines().collect::<Vec<_>>();
5344        assert_eq!(lines[1].matches('│').count(), 2, "{rendered}");
5345        assert!(lines[2].contains('┬'), "{rendered}");
5346        assert!(
5347            lines.last().is_some_and(|line| line.contains('┴')),
5348            "{rendered}"
5349        );
5350    }
5351
5352    #[test]
5353    fn iteration_status_block_omits_delta_columns_when_no_target_is_provided() {
5354        let state = make_integration_state();
5355        let view_options = IntegrationStatusViewOptions {
5356            show_statistics: true,
5357            show_max_weight_details: false,
5358            ..default_view_options()
5359        };
5360        let rendered = render_update(
5361            StatusUpdateBuildRequest::new(
5362                IntegrationStatusKind::Iteration,
5363                &state,
5364                &[None, None],
5365                &view_options,
5366            )
5367            .with_timing(
5368                4,
5369                Duration::from_secs(1),
5370                Duration::from_secs(1),
5371                100_000,
5372                100_000,
5373                100_000,
5374            ),
5375        );
5376
5377        assert!(!rendered.contains("Δ [σ]"), "{rendered}");
5378        assert!(!rendered.contains("Δ ="), "{rendered}");
5379        assert!(rendered.contains("Integration statistics"), "{rendered}");
5380        assert!(rendered.contains("mwi"), "{rendered}");
5381    }
5382
5383    #[test]
5384    fn tabled_all_rows_insert_value_only_absolute_continuations_and_filter_phases() {
5385        let state = make_discrete_integration_state();
5386        let targets = [Some(Complex::new(F(1.0e-4), F(2.0e-5))), None];
5387        let view_options = IntegrationStatusViewOptions {
5388            show_statistics: true,
5389            show_max_weight_details: true,
5390            show_top_discrete_grid: false,
5391            show_discrete_contributions_sum: false,
5392            ..default_view_options()
5393        };
5394        let update = build_status_update(StatusUpdateBuildRequest::new(
5395            IntegrationStatusKind::Iteration,
5396            &state,
5397            &targets,
5398            &view_options,
5399        ));
5400        let rendered = render_update(StatusUpdateBuildRequest::new(
5401            IntegrationStatusKind::Iteration,
5402            &state,
5403            &targets,
5404            &view_options,
5405        ));
5406        let lines = rendered.lines().collect_vec();
5407
5408        let row_text = |component, view| {
5409            update
5410                .main_results
5411                .find_row(status_update::ContributionKind::All, component)
5412                .and_then(|row| row.slot_cell(0))
5413                .and_then(|cell| cell.statistics(view).value.as_ref())
5414                .map(|value| value.display.to_plain_string())
5415                .expect("All row value should exist")
5416        };
5417        let signed_re = row_text(
5418            status_update::ComponentKind::Real,
5419            status_update::IntegralView::Signed,
5420        );
5421        let absolute_re = row_text(
5422            status_update::ComponentKind::Real,
5423            status_update::IntegralView::Absolute,
5424        );
5425        let signed_im = row_text(
5426            status_update::ComponentKind::Imag,
5427            status_update::IntegralView::Signed,
5428        );
5429        let absolute_im = row_text(
5430            status_update::ComponentKind::Imag,
5431            status_update::IntegralView::Absolute,
5432        );
5433        let line_index = |needle: &str| {
5434            lines
5435                .iter()
5436                .position(|line| line.contains(needle))
5437                .unwrap_or_else(|| panic!("missing {needle:?} in:\n{rendered}"))
5438        };
5439        let signed_re_line = line_index(&signed_re);
5440        let absolute_re_line = line_index(&absolute_re);
5441        let signed_im_line = line_index(&signed_im);
5442        let absolute_im_line = line_index(&absolute_im);
5443        assert!(signed_re_line < absolute_re_line);
5444        assert!(absolute_re_line < signed_im_line);
5445        assert!(signed_im_line < absolute_im_line);
5446        assert!(lines[absolute_re_line].contains("|re|"));
5447        assert!(lines[absolute_im_line].contains("|im|"));
5448
5449        for (component, absolute_line) in [
5450            (status_update::ComponentKind::Real, absolute_re_line),
5451            (status_update::ComponentKind::Imag, absolute_im_line),
5452        ] {
5453            let row = update
5454                .main_results
5455                .find_row(status_update::ContributionKind::All, component)
5456                .expect("All row should exist");
5457            let statistics = row
5458                .slot_cell(0)
5459                .expect("slot cells should exist")
5460                .statistics(status_update::IntegralView::Absolute);
5461            for metadata in [
5462                statistics.relative_error.as_ref(),
5463                statistics.chi_sq.as_ref(),
5464                statistics.max_weight_impact.as_ref(),
5465            ]
5466            .into_iter()
5467            .flatten()
5468            {
5469                let metadata = metadata.display.to_plain_string();
5470                assert!(
5471                    !lines[absolute_line].contains(&metadata),
5472                    "absolute continuation unexpectedly contains {metadata:?}: {}",
5473                    lines[absolute_line]
5474                );
5475            }
5476            let (delta_sigma, delta_percent) =
5477                update.target_deltas_for_row_slot(row, 0, status_update::IntegralView::Absolute);
5478            assert!(delta_sigma.is_none());
5479            assert!(delta_percent.is_none());
5480        }
5481        assert_eq!(rendered.matches("Maximum weight details").count(), 1);
5482        assert_eq!(
5483            rendered.matches("Integration statistics [global]").count(),
5484            1
5485        );
5486
5487        for (phase, included, excluded) in [
5488            (IntegrationStatusPhaseDisplay::Real, "|re|", "|im|"),
5489            (IntegrationStatusPhaseDisplay::Imag, "|im|", "|re|"),
5490        ] {
5491            let phase_options = IntegrationStatusViewOptions {
5492                phase_display: phase,
5493                show_statistics: false,
5494                show_max_weight_details: false,
5495                ..default_view_options()
5496            };
5497            let phase_rendered = render_update(StatusUpdateBuildRequest::new(
5498                IntegrationStatusKind::Iteration,
5499                &state,
5500                &targets,
5501                &phase_options,
5502            ));
5503            assert!(phase_rendered.contains(included), "{phase_rendered}");
5504            assert!(!phase_rendered.contains(excluded), "{phase_rendered}");
5505        }
5506    }
5507
5508    #[test]
5509    fn absolute_sum_status_adds_shallow_means_and_combines_errors_in_quadrature() {
5510        let state = make_discrete_integration_state();
5511        let targets = [Some(Complex::new(F(1.0e-4), F(2.0e-5))), None];
5512        let view_options = IntegrationStatusViewOptions {
5513            show_discrete_contributions_sum: true,
5514            ..default_view_options()
5515        };
5516        let update = build_status_update(StatusUpdateBuildRequest::new(
5517            IntegrationStatusKind::Iteration,
5518            &state,
5519            &targets,
5520            &view_options,
5521        ));
5522        let row = update
5523            .main_results
5524            .find_row(
5525                status_update::ContributionKind::Sum,
5526                status_update::ComponentKind::Real,
5527            )
5528            .expect("absolute Sum row should exist");
5529        let value = row
5530            .slot_cell(0)
5531            .expect("slot cells should exist")
5532            .statistics(status_update::IntegralView::Absolute)
5533            .value
5534            .as_ref()
5535            .expect("absolute Sum estimate should exist")
5536            .raw;
5537
5538        assert!((value.0 - F(6.0e-5)).abs() < F(1.0e-18));
5539        let expected_error = (F(3.0e-6) * F(3.0e-6) + F(1.5e-6) * F(1.5e-6)).sqrt();
5540        assert!((value.1 - expected_error).abs() < F(1.0e-18));
5541        let (delta_sigma, delta_percent) =
5542            update.target_deltas_for_row_slot(row, 0, status_update::IntegralView::Absolute);
5543        assert!(delta_sigma.is_none());
5544        assert!(delta_percent.is_none());
5545    }
5546
5547    #[test]
5548    fn tabled_statistics_panel_uses_global_scope_label() {
5549        let state = make_integration_state();
5550        let view_options = IntegrationStatusViewOptions {
5551            show_statistics: true,
5552            show_max_weight_details: false,
5553            ..default_view_options()
5554        };
5555        let rendered = render_update(
5556            StatusUpdateBuildRequest::new(
5557                IntegrationStatusKind::Iteration,
5558                &state,
5559                &[None, None],
5560                &view_options,
5561            )
5562            .with_timing(
5563                4,
5564                Duration::from_secs(1),
5565                Duration::from_secs(1),
5566                100_000,
5567                100_000,
5568                100_000,
5569            ),
5570        );
5571
5572        assert!(
5573            rendered.contains("Integration statistics [global]"),
5574            "{rendered}"
5575        );
5576    }
5577
5578    #[test]
5579    fn live_iteration_status_block_uses_progress_header() {
5580        let mut state = make_integration_state();
5581        state.iter = 2;
5582        state.num_points = 125_000;
5583        let view_options = IntegrationStatusViewOptions {
5584            show_statistics: false,
5585            show_max_weight_details: false,
5586            ..default_view_options()
5587        };
5588        let rendered = render_update(
5589            StatusUpdateBuildRequest::new(
5590                IntegrationStatusKind::Live,
5591                &state,
5592                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5593                &view_options,
5594            )
5595            .with_timing(
5596                4,
5597                Duration::from_secs(1),
5598                Duration::from_secs(1),
5599                25_000,
5600                125_000,
5601                125_000,
5602            )
5603            .with_live_progress(Some(status_update::LiveIterationProgress {
5604                completed_points: 25_000,
5605                target_points: 100_000,
5606            })),
5607        );
5608
5609        assert!(
5610            rendered.contains("Iteration #   2 ( running   )"),
5611            "{rendered}"
5612        );
5613        assert!(
5614            rendered.contains("Iteration progress 25.00K/100.00K"),
5615            "{rendered}"
5616        );
5617        assert!(rendered.contains("25.0%"), "{rendered}");
5618        assert!(rendered.contains("# samples total = 125.00K"), "{rendered}");
5619    }
5620
5621    #[test]
5622    fn iteration_status_block_shows_discrete_contributions_and_discrete_max_weights() {
5623        let state = make_discrete_integration_state();
5624        let view_options = IntegrationStatusViewOptions {
5625            show_statistics: false,
5626            show_max_weight_details: true,
5627            show_top_discrete_grid: true,
5628            show_discrete_contributions_sum: true,
5629            contribution_sort: ContributionSortMode::Index,
5630            show_max_weight_info_for_discrete_bins: true,
5631            ..default_view_options()
5632        };
5633        let rendered = render_update(
5634            StatusUpdateBuildRequest::new(
5635                IntegrationStatusKind::Iteration,
5636                &state,
5637                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5638                &view_options,
5639            )
5640            .with_timing(
5641                4,
5642                Duration::from_secs(2),
5643                Duration::from_secs(2),
5644                110_000,
5645                210_000,
5646                210_000,
5647            ),
5648        );
5649
5650        assert!(rendered.contains("Sum"), "{rendered}");
5651        assert!(rendered.contains("Contribution (idx=graph)"), "{rendered}");
5652        assert!(
5653            !rendered.contains("│             (idx=graph)             │"),
5654            "{rendered}"
5655        );
5656        assert!(rendered.contains("#0: GL0"), "{rendered}");
5657        assert!(rendered.contains("75.0%"), "{rendered}");
5658        assert!(rendered.contains("25.0%"), "{rendered}");
5659        assert!(
5660            rendered.contains("Maximum weight details by discrete bin"),
5661            "{rendered}"
5662        );
5663        assert!(
5664            rendered.contains("xs: [ 2.5000000000000000e-01 ]"),
5665            "{rendered}"
5666        );
5667    }
5668
5669    #[test]
5670    fn grouped_graph_descriptions_include_all_group_members() {
5671        let description =
5672            graph_group_description(["GL0".to_string(), "GL1".to_string(), "GL2".to_string()]);
5673
5674        assert_eq!(description, "[GL0,GL1,GL2]");
5675    }
5676
5677    #[test]
5678    fn explicit_single_graph_subset_monitors_the_root_graph_axis() {
5679        let grid = Grid::Discrete(
5680            DiscreteGrid::new(
5681                vec![Some(Grid::Continuous(
5682                    ContinuousGrid::new(1, 64, 100, None, false).unwrap(),
5683                ))],
5684                F(10.0),
5685                false,
5686            )
5687            .unwrap(),
5688        );
5689        let labels = vec!["graph".to_string()];
5690
5691        assert!(monitored_discrete_layout(&grid, &labels, false).is_none());
5692        assert_eq!(
5693            monitored_discrete_layout(&grid, &labels, true),
5694            Some((Vec::new(), "graph".to_string(), 1))
5695        );
5696    }
5697
5698    #[test]
5699    fn tabled_single_graph_subset_keeps_the_graph_row() {
5700        let state = make_single_graph_discrete_integration_state();
5701        let view_options = IntegrationStatusViewOptions {
5702            show_statistics: false,
5703            show_max_weight_details: false,
5704            show_top_discrete_grid: true,
5705            contribution_sort: ContributionSortMode::Index,
5706            ..default_view_options()
5707        };
5708        let rendered = render_update(StatusUpdateBuildRequest::new(
5709            IntegrationStatusKind::Iteration,
5710            &state,
5711            &[None, None],
5712            &view_options,
5713        ));
5714
5715        assert!(rendered.contains("Contribution (idx=graph)"), "{rendered}");
5716        assert!(rendered.contains("#0: GL22"), "{rendered}");
5717        assert!(!rendered.contains("GL1"), "{rendered}");
5718    }
5719
5720    #[test]
5721    fn iteration_status_block_hides_spanned_metadata_header_separators() {
5722        let state = make_discrete_integration_state();
5723        let view_options = IntegrationStatusViewOptions {
5724            show_statistics: false,
5725            show_max_weight_details: false,
5726            show_top_discrete_grid: true,
5727            show_discrete_contributions_sum: true,
5728            contribution_sort: ContributionSortMode::Index,
5729            show_max_weight_info_for_discrete_bins: false,
5730            ..default_view_options()
5731        };
5732        let rendered = render_update(
5733            StatusUpdateBuildRequest::new(
5734                IntegrationStatusKind::Iteration,
5735                &state,
5736                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5737                &view_options,
5738            )
5739            .with_timing(
5740                4,
5741                Duration::from_secs(2),
5742                Duration::from_secs(2),
5743                110_000,
5744                210_000,
5745                210_000,
5746            ),
5747        );
5748
5749        let header_line = rendered
5750            .lines()
5751            .find(|line| line.contains("χ²/dof") && line.contains("mwi"))
5752            .expect("expected spanned metadata header line");
5753
5754        let chi_to_mwi =
5755            &header_line[header_line.find("χ²/dof").unwrap()..header_line.find("mwi").unwrap()];
5756        assert!(!chi_to_mwi.contains('│'), "{rendered}");
5757
5758        let delta_sigma_to_percent =
5759            &header_line[header_line.find("Δ [σ]").unwrap()..header_line.find("Δ [%]").unwrap()];
5760        assert!(!delta_sigma_to_percent.contains('│'), "{rendered}");
5761    }
5762
5763    #[test]
5764    fn integration_result_always_contains_first_non_trivial_discrete_breakdown() {
5765        let state = make_discrete_integration_state();
5766        let result =
5767            build_integration_result(&state, &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None]);
5768
5769        let slot = result
5770            .slot("proc_a@itg_a")
5771            .expect("discrete slot must be present");
5772        let breakdown = slot
5773            .grid_breakdown
5774            .re
5775            .as_ref()
5776            .expect("discrete breakdown must be persisted");
5777
5778        assert_eq!(breakdown.axis_label, "graph");
5779        assert!(breakdown.fixed_coordinates.is_empty());
5780        assert_eq!(breakdown.entries.len(), 2);
5781        assert_eq!(breakdown.entries[0].bin_index, 0);
5782        assert_eq!(breakdown.entries[0].bin_label.as_deref(), Some("GL0"));
5783        assert_eq!(breakdown.entries[0].pdf, F(0.75));
5784        assert_eq!(breakdown.entries[0].processed_samples, 150);
5785        assert_eq!(breakdown.entries[1].bin_index, 1);
5786        assert_eq!(breakdown.entries[1].bin_label.as_deref(), Some("GL1"));
5787        assert_eq!(breakdown.entries[1].pdf, F(0.25));
5788        assert_eq!(breakdown.entries[1].processed_samples, 50);
5789    }
5790
5791    #[test]
5792    fn integration_result_persists_absolute_bundle_and_shallow_breakdown() {
5793        let mut state = make_discrete_integration_state();
5794        let accumulator = &mut state.all_integrals[0];
5795        accumulator.absolute_re.avg = F(3.5e-4);
5796        accumulator.absolute_re.err = F(2.5e-5);
5797        accumulator.absolute_re.chi_sq = F(0.42);
5798        accumulator.absolute_re.processed_samples = 200;
5799        accumulator.absolute_re.max_eval_positive = F(8.0);
5800        accumulator.absolute_re.max_eval_positive_xs = Some(Sample::Discrete(
5801            F(4.0),
5802            0,
5803            Some(Box::new(Sample::Continuous(F(1.0), vec![F(0.25)]))),
5804        ));
5805        accumulator.absolute_im.avg = F(1.25e-4);
5806        accumulator.absolute_im.err = F(1.0e-5);
5807        accumulator.absolute_im.chi_sq = F(0.24);
5808        accumulator.absolute_im.processed_samples = 200;
5809        accumulator.absolute_im.max_eval_positive = F(5.0);
5810
5811        let re_summary = state.slot_absolute_re_summaries[0]
5812            .as_mut()
5813            .expect("absolute real summary should exist");
5814        re_summary.bins[0].accumulator.avg = F(2.0e-4);
5815        re_summary.bins[0].accumulator.err = F(2.0e-5);
5816        re_summary.bins[0].accumulator.processed_samples = 150;
5817        re_summary.bins[1].accumulator.avg = F(1.5e-4);
5818        re_summary.bins[1].accumulator.err = F(1.5e-5);
5819        re_summary.bins[1].accumulator.processed_samples = 50;
5820        let im_summary = state.slot_absolute_im_summaries[0]
5821            .as_mut()
5822            .expect("absolute imaginary summary should exist");
5823        im_summary.bins[0].accumulator.avg = F(7.5e-5);
5824        im_summary.bins[0].accumulator.processed_samples = 150;
5825        im_summary.bins[1].accumulator.avg = F(5.0e-5);
5826        im_summary.bins[1].accumulator.processed_samples = 50;
5827
5828        let result =
5829            build_integration_result(&state, &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None]);
5830        let absolute = &result
5831            .slot("proc_a@itg_a")
5832            .expect("absolute result slot should be present")
5833            .absolute;
5834
5835        assert_eq!(absolute.integral.result.re, F(3.5e-4));
5836        assert_eq!(absolute.integral.result.im, F(1.25e-4));
5837        assert_eq!(absolute.integral.neval, 200);
5838        assert_eq!(absolute.table_results[0].component, "|re|");
5839        assert_eq!(absolute.table_results[1].component, "|im|");
5840        assert!(
5841            absolute
5842                .table_results
5843                .iter()
5844                .all(|row| row.target_delta_sigma.is_none() && row.target_delta_percent.is_none())
5845        );
5846        assert_eq!(absolute.max_weight_info.len(), 2);
5847        assert!(
5848            absolute
5849                .max_weight_info
5850                .iter()
5851                .all(|entry| entry.sign == "+" && entry.max_eval > F(0.0))
5852        );
5853        let breakdown = absolute
5854            .grid_breakdown
5855            .re
5856            .as_ref()
5857            .expect("absolute shallow breakdown should be persisted");
5858        assert_eq!(breakdown.entries.len(), 2);
5859        assert_eq!(breakdown.entries[0].pdf, F(0.75));
5860        assert_eq!(breakdown.entries[0].value, F(2.0e-4));
5861        assert_eq!(breakdown.entries[1].pdf, F(0.25));
5862        assert_eq!(breakdown.entries[1].value, F(1.5e-4));
5863
5864        let json = serde_json::to_string(&result).expect("integration result should serialize");
5865        assert!(json.contains("\"absolute\""));
5866        let decoded: IntegrationResult =
5867            serde_json::from_str(&json).expect("integration result should deserialize");
5868        assert_eq!(
5869            decoded
5870                .slot("proc_a@itg_a")
5871                .expect("round-tripped slot should exist")
5872                .absolute
5873                .integral
5874                .result
5875                .re,
5876            F(3.5e-4)
5877        );
5878    }
5879
5880    #[test]
5881    fn integration_state_roundtrip_preserves_absolute_accumulators_and_summaries() {
5882        let mut state = make_discrete_integration_state();
5883        state.all_integrals[0].absolute_re.avg = F(4.5);
5884        state.slot_absolute_re_summaries[0]
5885            .as_mut()
5886            .expect("absolute summary should exist")
5887            .bins[1]
5888            .accumulator
5889            .avg = F(2.25);
5890
5891        let encoded = bincode::encode_to_vec(&state, bincode::config::standard())
5892            .expect("integration state should serialize");
5893        let (decoded, consumed): (IntegrationState, usize) =
5894            bincode::decode_from_slice(&encoded, bincode::config::standard())
5895                .expect("integration state should deserialize");
5896
5897        assert_eq!(consumed, encoded.len());
5898        assert_eq!(decoded.all_integrals[0].absolute_re.avg, F(4.5));
5899        assert_eq!(
5900            decoded.slot_absolute_re_summaries[0]
5901                .as_ref()
5902                .expect("round-tripped absolute summary should exist")
5903                .bins[1]
5904                .accumulator
5905                .avg,
5906            F(2.25)
5907        );
5908        assert!(
5909            decoded.slot_absolute_re_summaries[0]
5910                .as_ref()
5911                .expect("round-tripped absolute summary should exist")
5912                .bins
5913                .iter()
5914                .all(|bin| bin.sub_summary.is_none())
5915        );
5916    }
5917
5918    #[test]
5919    fn orientation_descriptions_use_colored_signs() {
5920        let mut state = make_discrete_integration_state();
5921        state.first_non_trivial_discrete_label = Some("orientation".to_string());
5922        state.first_non_trivial_discrete_bin_descriptions =
5923            Some(vec!["+-0".to_string(), "0++".to_string()]);
5924        for metadata in &mut state.slot_first_non_trivial_discrete_breakdown_metadata {
5925            if let Some(metadata) = metadata.as_mut() {
5926                metadata.axis_label = "orientation".to_string();
5927                metadata.bin_labels = vec!["+-0".to_string(), "0++".to_string()];
5928            }
5929        }
5930
5931        control::set_override(true);
5932        let expected_plus = "+".green().bold().to_string();
5933        let expected_minus = "-".red().bold().to_string();
5934
5935        let view_options = IntegrationStatusViewOptions {
5936            show_statistics: false,
5937            show_max_weight_details: false,
5938            show_top_discrete_grid: true,
5939            show_discrete_contributions_sum: false,
5940            contribution_sort: ContributionSortMode::Index,
5941            show_max_weight_info_for_discrete_bins: false,
5942            ..default_view_options()
5943        };
5944        let ansi = render_tabled::render_status_update(
5945            &build_status_update(
5946                StatusUpdateBuildRequest::new(
5947                    IntegrationStatusKind::Iteration,
5948                    &state,
5949                    &[None, None],
5950                    &view_options,
5951                )
5952                .with_timing(
5953                    1,
5954                    Duration::from_secs(0),
5955                    Duration::from_secs(0),
5956                    0,
5957                    0,
5958                    0,
5959                ),
5960            ),
5961            &tabled_options_for_view(&view_options),
5962        );
5963        control::set_override(false);
5964
5965        assert!(ansi.contains(&expected_plus), "{ansi}");
5966        assert!(ansi.contains(&expected_minus), "{ansi}");
5967        assert!(ansi.contains("0"), "{ansi}");
5968    }
5969
5970    #[test]
5971    fn mismatched_bin_descriptions_fall_back_to_indices_with_warning() {
5972        let (descriptions, warning) = coalesce_first_non_trivial_discrete_bin_descriptions(
5973            "graph",
5974            &[
5975                (
5976                    "proc_a@itg_a".to_string(),
5977                    vec!["GL0".to_string(), "GL1".to_string()],
5978                ),
5979                (
5980                    "proc_b@itg_b".to_string(),
5981                    vec!["GX0".to_string(), "GX1".to_string()],
5982                ),
5983            ],
5984        );
5985
5986        assert!(descriptions.is_none());
5987        let warning = warning.expect("mismatch should produce a warning");
5988        assert!(warning.contains("graph"), "{warning}");
5989        assert!(warning.contains("proc_a@itg_a"), "{warning}");
5990        assert!(warning.contains("proc_b@itg_b"), "{warning}");
5991    }
5992
5993    #[test]
5994    fn format_max_eval_sample_keeps_full_discrete_coordinates() {
5995        let axis_labels = vec!["graph".to_string(), "sampling channel".to_string()];
5996        let full_sample = Sample::Discrete(
5997            F(1.0),
5998            0,
5999            Some(Box::new(Sample::Discrete(
6000                F(1.0),
6001                1,
6002                Some(Box::new(Sample::Continuous(F(1.0), vec![F(0.25)]))),
6003            ))),
6004        );
6005        let nested_sample = Sample::Discrete(
6006            F(1.0),
6007            1,
6008            Some(Box::new(Sample::Continuous(F(1.0), vec![F(0.75)]))),
6009        );
6010
6011        assert_eq!(
6012            display::format_max_eval_sample(&full_sample, &axis_labels, &[]),
6013            "graph: 0, sampling channel: 1, xs: [ 2.5000000000000000e-01 ]"
6014        );
6015        assert_eq!(
6016            display::format_max_eval_sample(&nested_sample, &axis_labels, &[0]),
6017            "graph: 0, sampling channel: 1, xs: [ 7.5000000000000000e-01 ]"
6018        );
6019    }
6020
6021    #[test]
6022    fn format_max_eval_sample_wraps_long_coordinate_lists() {
6023        let axis_labels = vec!["graph".to_string(), "orientation".to_string()];
6024        let sample = Sample::Discrete(
6025            F(1.0),
6026            0,
6027            Some(Box::new(Sample::Discrete(
6028                F(1.0),
6029                9,
6030                Some(Box::new(Sample::Continuous(
6031                    F(1.0),
6032                    vec![
6033                        F(0.125),
6034                        F(0.25),
6035                        F(0.375),
6036                        F(0.5),
6037                        F(0.625),
6038                        F(0.75),
6039                        F(0.875),
6040                    ],
6041                ))),
6042            ))),
6043        );
6044
6045        assert_eq!(
6046            display::format_max_eval_sample(&sample, &axis_labels, &[]),
6047            "graph: 0, orientation: 9, xs: [\n1.2500000000000000e-01 2.5000000000000000e-01 3.7500000000000000e-01\n5.0000000000000000e-01 6.2500000000000000e-01 7.5000000000000000e-01\n8.7500000000000000e-01 ]"
6048        );
6049    }
6050
6051    #[test]
6052    fn discrete_bin_prefixes_use_fixed_width_within_each_range() {
6053        assert_eq!(status_update::format_discrete_bin_prefix(2, 99), "#2: ");
6054        assert_eq!(status_update::format_discrete_bin_prefix(12, 99), "#12:");
6055        assert_eq!(status_update::format_discrete_bin_prefix(2, 999), "#2:  ");
6056        assert_eq!(status_update::format_discrete_bin_prefix(12, 999), "#12: ");
6057        assert_eq!(status_update::format_discrete_bin_prefix(123, 999), "#123:");
6058        assert_eq!(
6059            status_update::format_discrete_bin_prefix(2, 9_999),
6060            "#2:   "
6061        );
6062        assert_eq!(
6063            status_update::format_discrete_bin_prefix(1234, 9_999),
6064            "#1234:"
6065        );
6066    }
6067
6068    #[test]
6069    fn final_summary_omits_discrete_max_weight_block_when_disabled() {
6070        let state = make_discrete_integration_state();
6071        let rendered = render_saved_integration_summary(
6072            &state,
6073            &[None, None],
6074            &IntegrationStatusViewOptions {
6075                show_statistics: true,
6076                show_max_weight_details: true,
6077                show_top_discrete_grid: false,
6078                show_discrete_contributions_sum: false,
6079                contribution_sort: ContributionSortMode::Index,
6080                show_max_weight_info_for_discrete_bins: false,
6081                ..default_view_options()
6082            },
6083            &default_tabled_options(),
6084        );
6085
6086        assert!(rendered.contains("Maximum weight details"), "{rendered}");
6087        assert!(
6088            !rendered.contains("Maximum weight details by discrete bin"),
6089            "{rendered}"
6090        );
6091        assert!(
6092            !rendered.contains("# samples per iteration = 0"),
6093            "{rendered}"
6094        );
6095    }
6096
6097    #[test]
6098    fn results_output_summary_renders_tabled_workspace_rows() {
6099        let workspace = Path::new("/tmp/gl_workspace");
6100        let rendered = render_results_output_summary_table(Some(workspace), &[], &[], &[])
6101            .expect("workspace summary should render");
6102
6103        assert!(
6104            rendered.contains("Integration results emitted"),
6105            "{rendered}"
6106        );
6107        assert!(rendered.contains("type"), "{rendered}");
6108        assert!(rendered.contains("path"), "{rendered}");
6109        assert!(rendered.contains("workspace path"), "{rendered}");
6110        assert!(rendered.contains("/tmp/gl_workspace"), "{rendered}");
6111        assert!(rendered.contains("results"), "{rendered}");
6112        assert!(rendered.contains("integration_result.json"), "{rendered}");
6113        assert!(rendered.contains("├"), "{rendered}");
6114        assert!(!rendered.contains("iteration snapshots"), "{rendered}");
6115        assert!(!rendered.contains("iter_*"), "{rendered}");
6116    }
6117
6118    #[test]
6119    fn results_output_summary_skips_missing_observable_outputs() {
6120        let settings = RuntimeSettings::default();
6121        let slot = IntegrationSlot::new(
6122            SlotMeta {
6123                process_name: "proc".to_string(),
6124                integrand_name: "default".to_string(),
6125            },
6126            settings.clone(),
6127            Model::default(),
6128            Integrand::TestProbe(TestProbeIntegrand::new(settings, 3)),
6129            None,
6130        );
6131        let rendered = render_results_output_summary_table(
6132            None,
6133            &[slot],
6134            &[vec![PathBuf::from(
6135                "/tmp/definitely_missing_observables_final.json",
6136            )]],
6137            &[],
6138        );
6139
6140        assert!(rendered.is_none(), "{rendered:?}");
6141    }
6142
6143    #[test]
6144    fn results_output_summary_lists_all_emitted_observable_files() {
6145        let base = std::env::temp_dir().join(format!(
6146            "gammaloop_results_output_summary_{}",
6147            std::process::id()
6148        ));
6149        if base.exists() {
6150            let _ = fs::remove_dir_all(&base);
6151        }
6152        let workspace = base.join("workspace");
6153        let slot_dir = workspace.join("integrands").join("proc@default");
6154        fs::create_dir_all(&slot_dir).unwrap();
6155        fs::write(workspace_result_snapshot_path(&workspace), b"{}").unwrap();
6156        let json_path = slot_dir.join("observables_final.json");
6157        let hwu_path = slot_dir.join("observables_final.hwu");
6158        fs::write(&json_path, b"{}").unwrap();
6159        fs::write(&hwu_path, b"# hwu\n").unwrap();
6160
6161        let settings = RuntimeSettings::default();
6162        let slot = IntegrationSlot::new(
6163            SlotMeta {
6164                process_name: "proc".to_string(),
6165                integrand_name: "default".to_string(),
6166            },
6167            settings.clone(),
6168            Model::default(),
6169            Integrand::TestProbe(TestProbeIntegrand::new(settings, 3)),
6170            None,
6171        );
6172        let rendered = render_results_output_summary_table(
6173            Some(&workspace),
6174            &[slot],
6175            &[vec![hwu_path.clone(), json_path.clone()]],
6176            &[],
6177        )
6178        .expect("summary should render");
6179
6180        assert!(rendered.contains("proc@default (hwu)"), "{rendered}");
6181        assert!(rendered.contains("proc@default (json)"), "{rendered}");
6182        assert!(rendered.contains("observables_final.hwu"), "{rendered}");
6183        assert!(rendered.contains("observables_final.json"), "{rendered}");
6184
6185        fs::remove_dir_all(&base).unwrap();
6186    }
6187
6188    #[test]
6189    fn ratatui_overview_shows_eta_and_all_slot_metrics() {
6190        let state = make_integration_state();
6191        let view_options = IntegrationStatusViewOptions {
6192            show_statistics: true,
6193            show_max_weight_details: false,
6194            ..default_view_options()
6195        };
6196        let rendered = render_ratatui_update(
6197            StatusUpdateBuildRequest::new(
6198                IntegrationStatusKind::Live,
6199                &state,
6200                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
6201                &view_options,
6202            )
6203            .with_timing(
6204                4,
6205                Duration::from_secs(15),
6206                Duration::from_secs(10),
6207                25_000,
6208                125_000,
6209                125_000,
6210            )
6211            .with_live_progress(Some(status_update::LiveIterationProgress {
6212                completed_points: 25_000,
6213                target_points: 100_000,
6214            })),
6215            |_| {},
6216        );
6217
6218        assert!(rendered.contains("Iteration progress"), "{rendered}");
6219        assert!(rendered.contains("ETA 30s"), "{rendered}");
6220        assert!(rendered.contains("25.00K / 100.00K (25.0%)"), "{rendered}");
6221        assert!(rendered.contains("# samples total 125.00K"), "{rendered}");
6222        assert!(rendered.contains("#samples/s"), "{rendered}");
6223        assert!(
6224            rendered.contains("480µs /sample/core (4 cores)"),
6225            "{rendered}"
6226        );
6227        assert!(rendered.contains("/sample/core (4 cores)"), "{rendered}");
6228        assert!(rendered.contains("Integrands"), "{rendered}");
6229        assert!(rendered.contains("Focused integrand"), "{rendered}");
6230        assert!(rendered.contains("Results summary"), "{rendered}");
6231        assert!(
6232            rendered.contains("Integration statistics [global]"),
6233            "{rendered}"
6234        );
6235        assert!(rendered.contains("% err"), "{rendered}");
6236        assert!(rendered.contains("chi^2"), "{rendered}");
6237        assert!(rendered.contains("m.w.i"), "{rendered}");
6238        assert!(
6239            rendered.contains("Timing composition [global]"),
6240            "{rendered}"
6241        );
6242        assert!(rendered.contains("Precision mix [global]"), "{rendered}");
6243    }
6244
6245    #[test]
6246    fn live_preview_merges_and_finalizes_absolute_accumulators_without_mutating_saved_state() {
6247        let mut state = make_integration_state();
6248        state.all_integrals = vec![ComplexAccumulator::new(); state.slot_metas.len()];
6249        let slots = make_preview_test_slots(&state.slot_metas);
6250        let mut core_state = make_preview_test_core_state(&state);
6251        core_state.integrals[0].add_sample(Complex::new(F(-2.0), F(3.0)), None, F(2.0), None);
6252        core_state.integrals[0].add_sample(Complex::new(F(1.0), F(-4.0)), None, F(2.0), None);
6253
6254        let preview_state = build_preview_integration_state(
6255            &state,
6256            &slots,
6257            4,
6258            core_state.completed_points,
6259            1.5,
6260            &[core_state],
6261        );
6262
6263        assert_eq!(state.all_integrals[0].absolute_re.processed_samples, 0);
6264        assert_eq!(state.all_integrals[0].absolute_im.processed_samples, 0);
6265        assert_eq!(preview_state.all_integrals[0].absolute_re.avg, F(3.0));
6266        assert_eq!(preview_state.all_integrals[0].absolute_im.avg, F(7.0));
6267        assert_eq!(
6268            preview_state.all_integrals[0].absolute_re.processed_samples,
6269            2
6270        );
6271        assert_eq!(
6272            preview_state.all_integrals[0].absolute_im.processed_samples,
6273            2
6274        );
6275    }
6276
6277    #[test]
6278    fn live_status_updates_use_current_batch_statistics_in_bottom_panels() {
6279        let state = make_integration_state();
6280        let slots = make_preview_test_slots(&state.slot_metas);
6281        let core_state = make_preview_test_core_state(&state);
6282        let preview_state = build_preview_integration_state(
6283            &state,
6284            &slots,
6285            4,
6286            core_state.completed_points,
6287            1.5,
6288            &[core_state],
6289        );
6290        let view_options = IntegrationStatusViewOptions {
6291            show_statistics: true,
6292            show_max_weight_details: false,
6293            ..default_view_options()
6294        };
6295
6296        let initial_update = build_status_update(
6297            StatusUpdateBuildRequest::new(
6298                IntegrationStatusKind::Live,
6299                &state,
6300                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
6301                &view_options,
6302            )
6303            .with_timing(
6304                4,
6305                Duration::from_secs(1),
6306                Duration::from_millis(400),
6307                0,
6308                state.num_points,
6309                state.num_points,
6310            )
6311            .with_live_progress(Some(status_update::LiveIterationProgress {
6312                completed_points: 0,
6313                target_points: 100,
6314            })),
6315        );
6316        let preview_update = build_status_update(
6317            StatusUpdateBuildRequest::new(
6318                IntegrationStatusKind::Live,
6319                &preview_state,
6320                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
6321                &view_options,
6322            )
6323            .with_timing(
6324                4,
6325                Duration::from_millis(1500),
6326                Duration::from_millis(900),
6327                12,
6328                preview_state.num_points,
6329                preview_state.num_points,
6330            )
6331            .with_live_progress(Some(status_update::LiveIterationProgress {
6332                completed_points: 12,
6333                target_points: 100,
6334            })),
6335        );
6336
6337        let initial_statistics = initial_update
6338            .statistics
6339            .as_ref()
6340            .expect("statistics should be present");
6341        let preview_statistics = preview_update
6342            .statistics
6343            .as_ref()
6344            .expect("statistics should be present");
6345        let initial_snapshot = initial_statistics.global_snapshot();
6346        let preview_snapshot = preview_statistics.global_snapshot();
6347
6348        assert!(preview_snapshot.num_evals > initial_snapshot.num_evals);
6349        assert!(preview_snapshot.generated_event_count > initial_snapshot.generated_event_count);
6350        assert!(preview_snapshot.accepted_event_count > initial_snapshot.accepted_event_count);
6351
6352        let initial_timing_mix = initial_statistics
6353            .timing_mix_segments(status_update::StatisticsScope::Global)
6354            .iter()
6355            .map(|segment| segment.percentage)
6356            .collect_vec();
6357        let preview_timing_mix = preview_statistics
6358            .timing_mix_segments(status_update::StatisticsScope::Global)
6359            .iter()
6360            .map(|segment| segment.percentage)
6361            .collect_vec();
6362        assert_ne!(preview_timing_mix, initial_timing_mix);
6363
6364        let initial_precision_mix = initial_statistics
6365            .precision_mix_segments(status_update::StatisticsScope::Global)
6366            .iter()
6367            .map(|segment| segment.percentage)
6368            .collect_vec();
6369        let preview_precision_mix = preview_statistics
6370            .precision_mix_segments(status_update::StatisticsScope::Global)
6371            .iter()
6372            .map(|segment| segment.percentage)
6373            .collect_vec();
6374        assert_ne!(preview_precision_mix, initial_precision_mix);
6375        assert!(
6376            preview_precision_mix
6377                .iter()
6378                .any(|percentage| (*percentage - 50.0).abs() < f64::EPSILON),
6379            "{preview_precision_mix:?}"
6380        );
6381
6382        let preview_render = render_ratatui_update(
6383            StatusUpdateBuildRequest::new(
6384                IntegrationStatusKind::Live,
6385                &preview_state,
6386                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
6387                &view_options,
6388            )
6389            .with_timing(
6390                4,
6391                Duration::from_millis(1500),
6392                Duration::from_millis(900),
6393                12,
6394                preview_state.num_points,
6395                preview_state.num_points,
6396            )
6397            .with_live_progress(Some(status_update::LiveIterationProgress {
6398                completed_points: 12,
6399                target_points: 100,
6400            })),
6401            |_| {},
6402        );
6403        let preview_total_time = crate::utils::format_evaluation_time_from_f64(
6404            preview_snapshot.average_total_time_seconds,
6405        );
6406        assert!(
6407            preview_render.contains(&preview_total_time),
6408            "{preview_render}"
6409        );
6410        assert!(
6411            preview_render.contains("Precision mix [global]"),
6412            "{preview_render}"
6413        );
6414        assert!(
6415            preview_render.contains("Timing composition [global]"),
6416            "{preview_render}"
6417        );
6418    }
6419
6420    #[test]
6421    fn ratatui_statistics_panels_can_toggle_to_focused_slot_scope() {
6422        let state = make_integration_state();
6423        let view_options = IntegrationStatusViewOptions {
6424            show_statistics: true,
6425            show_max_weight_details: false,
6426            ..default_view_options()
6427        };
6428        let rendered = render_ratatui_update(
6429            StatusUpdateBuildRequest::new(
6430                IntegrationStatusKind::Live,
6431                &state,
6432                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
6433                &view_options,
6434            )
6435            .with_timing(
6436                4,
6437                Duration::from_secs(15),
6438                Duration::from_secs(10),
6439                25_000,
6440                125_000,
6441                125_000,
6442            ),
6443            |dashboard| {
6444                dashboard.focus_next_slot();
6445                dashboard.toggle_statistics_scope();
6446            },
6447        );
6448
6449        assert!(
6450            rendered.contains("Integration statistics [proc_b@itg_b]"),
6451            "{rendered}"
6452        );
6453        assert!(
6454            rendered.contains("Timing composition [proc_b@itg_b]"),
6455            "{rendered}"
6456        );
6457        assert!(
6458            rendered.contains("Precision mix [proc_b@itg_b]"),
6459            "{rendered}"
6460        );
6461        assert!(rendered.contains("Scope i"), "{rendered}");
6462    }
6463
6464    #[test]
6465    fn slot_scoped_statistics_rows_hide_shared_integrator_overhead() {
6466        let state = make_integration_state();
6467        let update = build_status_update(
6468            StatusUpdateBuildRequest::new(
6469                IntegrationStatusKind::Iteration,
6470                &state,
6471                &[None, None],
6472                &default_view_options(),
6473            )
6474            .with_timing(
6475                4,
6476                Duration::from_secs(1),
6477                Duration::from_secs(1),
6478                100_000,
6479                100_000,
6480                100_000,
6481            ),
6482        );
6483        let statistics = update.statistics.expect("statistics section should exist");
6484        let rows = statistics.table_rows(status_update::StatisticsScope::Slot(1));
6485        let integrator_entry = rows[2]
6486            .entries
6487            .last()
6488            .expect("integrator entry should be present");
6489
6490        assert_eq!(integrator_entry.label.to_plain_string(), "integrator");
6491        assert_eq!(integrator_entry.value.to_plain_string(), "N/A");
6492    }
6493
6494    #[test]
6495    fn ratatui_overview_chart_phase_toggle_updates_convergence_label() {
6496        let state = make_integration_state();
6497        let view_options = IntegrationStatusViewOptions {
6498            show_statistics: false,
6499            show_max_weight_details: false,
6500            ..default_view_options()
6501        };
6502        let rendered = render_ratatui_update(
6503            StatusUpdateBuildRequest::new(
6504                IntegrationStatusKind::Live,
6505                &state,
6506                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
6507                &view_options,
6508            )
6509            .with_timing(
6510                4,
6511                Duration::from_secs(15),
6512                Duration::from_secs(10),
6513                25_000,
6514                125_000,
6515                125_000,
6516            )
6517            .with_live_progress(Some(status_update::LiveIterationProgress {
6518                completed_points: 25_000,
6519                target_points: 100_000,
6520            })),
6521            |dashboard| dashboard.toggle_chart_component(),
6522        );
6523
6524        assert!(
6525            rendered.contains("Convergence ⟨I⟩ : im (not selected for training)"),
6526            "{rendered}"
6527        );
6528    }
6529
6530    #[test]
6531    fn ratatui_integrand_targets_are_trimmed_for_display() {
6532        let state = make_integration_state();
6533        let view_options = IntegrationStatusViewOptions {
6534            show_statistics: false,
6535            show_max_weight_details: false,
6536            ..default_view_options()
6537        };
6538        let rendered = render_ratatui_update(
6539            StatusUpdateBuildRequest::new(
6540                IntegrationStatusKind::Live,
6541                &state,
6542                &[
6543                    Some(Complex::new(
6544                        F(7.600000000000001e-6),
6545                        F(7.430000000000004e-5),
6546                    )),
6547                    Some(Complex::new(F(6.629999999999999e-5), F(0.0))),
6548                ],
6549                &view_options,
6550            )
6551            .with_timing(
6552                4,
6553                Duration::from_secs(15),
6554                Duration::from_secs(10),
6555                25_000,
6556                125_000,
6557                125_000,
6558            )
6559            .with_live_progress(Some(status_update::LiveIterationProgress {
6560                completed_points: 25_000,
6561                target_points: 100_000,
6562            })),
6563            |_| {},
6564        );
6565
6566        assert!(rendered.contains("trgt"), "{rendered}");
6567        assert!(rendered.contains("+7.6e-6"), "{rendered}");
6568        assert!(rendered.contains("+7.43e-5"), "{rendered}");
6569        assert!(rendered.contains("+6.63e-5"), "{rendered}");
6570        assert!(rendered.contains("+0e0"), "{rendered}");
6571    }
6572
6573    #[test]
6574    fn ratatui_overview_shows_eta_to_target_when_configured() {
6575        let state = make_integration_state();
6576        let view_options = IntegrationStatusViewOptions {
6577            target_relative_accuracy: Some(0.05),
6578            show_statistics: false,
6579            show_max_weight_details: false,
6580            ..default_view_options()
6581        };
6582        let rendered = render_ratatui_update(
6583            StatusUpdateBuildRequest::new(
6584                IntegrationStatusKind::Live,
6585                &state,
6586                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6587                &view_options,
6588            )
6589            .with_timing(
6590                4,
6591                Duration::from_secs(25),
6592                Duration::from_secs(10),
6593                25_000,
6594                125_000,
6595                125_000,
6596            )
6597            .with_live_progress(Some(status_update::LiveIterationProgress {
6598                completed_points: 25_000,
6599                target_points: 100_000,
6600            })),
6601            |_| {},
6602        );
6603
6604        assert!(rendered.contains("ETA to ⟨I⟩ target"), "{rendered}");
6605        assert!(rendered.contains("(% err <= 5%)"), "{rendered}");
6606    }
6607
6608    #[test]
6609    fn eta_to_target_specification_formats_relative_and_absolute_targets() {
6610        let state = make_integration_state();
6611        let relative_view_options = IntegrationStatusViewOptions {
6612            target_relative_accuracy: Some(0.05),
6613            ..default_view_options()
6614        };
6615        let update = build_status_update(
6616            StatusUpdateBuildRequest::new(
6617                IntegrationStatusKind::Live,
6618                &state,
6619                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6620                &relative_view_options,
6621            )
6622            .with_timing(
6623                4,
6624                Duration::from_secs(25),
6625                Duration::from_secs(10),
6626                25_000,
6627                125_000,
6628                125_000,
6629            )
6630            .with_live_progress(Some(status_update::LiveIterationProgress {
6631                completed_points: 25_000,
6632                target_points: 100_000,
6633            })),
6634        );
6635        assert_eq!(
6636            update.meta.eta_to_target_specification(),
6637            Some("% err <= 5%")
6638        );
6639
6640        let relative_scientific_view_options = IntegrationStatusViewOptions {
6641            target_relative_accuracy: Some(1.0e-6),
6642            ..default_view_options()
6643        };
6644        let update = build_status_update(
6645            StatusUpdateBuildRequest::new(
6646                IntegrationStatusKind::Live,
6647                &state,
6648                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6649                &relative_scientific_view_options,
6650            )
6651            .with_timing(
6652                4,
6653                Duration::from_secs(25),
6654                Duration::from_secs(10),
6655                25_000,
6656                125_000,
6657                125_000,
6658            )
6659            .with_live_progress(Some(status_update::LiveIterationProgress {
6660                completed_points: 25_000,
6661                target_points: 100_000,
6662            })),
6663        );
6664        assert_eq!(
6665            update.meta.eta_to_target_specification(),
6666            Some("% err <= 1e-4%")
6667        );
6668
6669        let absolute_view_options = IntegrationStatusViewOptions {
6670            target_absolute_accuracy: Some(1.0e-6),
6671            ..default_view_options()
6672        };
6673        let update = build_status_update(
6674            StatusUpdateBuildRequest::new(
6675                IntegrationStatusKind::Live,
6676                &state,
6677                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6678                &absolute_view_options,
6679            )
6680            .with_timing(
6681                4,
6682                Duration::from_secs(25),
6683                Duration::from_secs(10),
6684                25_000,
6685                125_000,
6686                125_000,
6687            )
6688            .with_live_progress(Some(status_update::LiveIterationProgress {
6689                completed_points: 25_000,
6690                target_points: 100_000,
6691            })),
6692        );
6693        assert_eq!(
6694            update.meta.eta_to_target_specification(),
6695            Some("err <= 1e-6")
6696        );
6697
6698        let combined_view_options = IntegrationStatusViewOptions {
6699            target_relative_accuracy: Some(0.05),
6700            target_absolute_accuracy: Some(1.0e-6),
6701            ..default_view_options()
6702        };
6703        let update = build_status_update(
6704            StatusUpdateBuildRequest::new(
6705                IntegrationStatusKind::Live,
6706                &state,
6707                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6708                &combined_view_options,
6709            )
6710            .with_timing(
6711                4,
6712                Duration::from_secs(25),
6713                Duration::from_secs(10),
6714                25_000,
6715                125_000,
6716                125_000,
6717            )
6718            .with_live_progress(Some(status_update::LiveIterationProgress {
6719                completed_points: 25_000,
6720                target_points: 100_000,
6721            })),
6722        );
6723        assert_eq!(
6724            update.meta.eta_to_target_specification(),
6725            Some("% err <= 5% or err <= 1e-6")
6726        );
6727    }
6728
6729    #[test]
6730    fn target_accuracy_status_uses_either_absolute_or_relative_condition() {
6731        let state = make_integration_state();
6732
6733        let reached_by_absolute = status_update::evaluate_target_accuracy(
6734            &state,
6735            100_000,
6736            Duration::from_secs(10),
6737            &[None, None],
6738            IntegrationStatusPhaseDisplay::Real,
6739            None,
6740            Some(1.0e-4),
6741        );
6742        assert!(reached_by_absolute.is_reached());
6743
6744        let reached_by_relative = status_update::evaluate_target_accuracy(
6745            &state,
6746            100_000,
6747            Duration::from_secs(10),
6748            &[Some(Complex::new(F(1.0e-3), F(0.0))), None],
6749            IntegrationStatusPhaseDisplay::Real,
6750            Some(0.1),
6751            None,
6752        );
6753        assert!(reached_by_relative.is_reached());
6754    }
6755
6756    #[test]
6757    fn target_accuracy_status_requires_all_slots_to_reach_relative_target() {
6758        let mut state = make_integration_state();
6759        state.all_integrals[0].re.avg = F(1.0);
6760        state.all_integrals[0].re.err = F(1.0e-5);
6761        state.all_integrals[1].re.avg = F(1.0);
6762        state.all_integrals[1].re.err = F(2.0e-3);
6763
6764        let status = status_update::evaluate_target_accuracy(
6765            &state,
6766            100_000,
6767            Duration::from_secs(10),
6768            &[None, None],
6769            IntegrationStatusPhaseDisplay::Real,
6770            Some(1.0e-3),
6771            None,
6772        );
6773
6774        assert!(!status.is_reached());
6775        assert!(!status.relative_reached);
6776        assert!(status.eta_to_target.is_some());
6777    }
6778
6779    #[test]
6780    fn target_accuracy_status_requires_all_displayed_components_to_reach_target() {
6781        let mut state = make_integration_state();
6782        state.all_integrals[0].re.avg = F(1.0);
6783        state.all_integrals[0].re.err = F(1.0e-5);
6784        state.all_integrals[0].im.avg = F(1.0);
6785        state.all_integrals[0].im.err = F(2.0e-3);
6786
6787        let status = status_update::evaluate_target_accuracy(
6788            &state,
6789            100_000,
6790            Duration::from_secs(10),
6791            &[Some(Complex::new(F(1.0), F(1.0))), None],
6792            IntegrationStatusPhaseDisplay::Both,
6793            Some(1.0e-3),
6794            None,
6795        );
6796
6797        assert!(!status.is_reached());
6798        assert!(!status.relative_reached);
6799        assert!(status.eta_to_target.is_some());
6800    }
6801
6802    #[test]
6803    fn target_accuracy_status_reports_infinite_eta_for_unreachable_absolute_target() {
6804        let state = make_integration_state();
6805        let status = status_update::evaluate_target_accuracy(
6806            &state,
6807            100_000,
6808            Duration::from_secs(10),
6809            &[None, None],
6810            IntegrationStatusPhaseDisplay::Real,
6811            None,
6812            Some(1.0e-99),
6813        );
6814
6815        assert!(!status.is_reached());
6816        assert_eq!(status.eta_to_target, Some(Duration::MAX));
6817    }
6818
6819    #[test]
6820    fn target_accuracy_status_prefers_finite_relative_eta_over_infinite_absolute_eta() {
6821        let state = make_integration_state();
6822        let status = status_update::evaluate_target_accuracy(
6823            &state,
6824            100_000,
6825            Duration::from_secs(10),
6826            &[None, None],
6827            IntegrationStatusPhaseDisplay::Real,
6828            Some(1.0e-3),
6829            Some(1.0e-99),
6830        );
6831
6832        assert!(!status.is_reached());
6833        assert!(status.eta_to_target.is_some());
6834        assert_ne!(status.eta_to_target, Some(Duration::MAX));
6835    }
6836
6837    #[test]
6838    fn ratatui_overview_displays_infinite_eta_to_target() {
6839        let state = make_integration_state();
6840        let view_options = IntegrationStatusViewOptions {
6841            target_absolute_accuracy: Some(1.0e-99),
6842            show_statistics: false,
6843            show_max_weight_details: false,
6844            ..default_view_options()
6845        };
6846        let rendered = render_ratatui_update(
6847            StatusUpdateBuildRequest::new(
6848                IntegrationStatusKind::Live,
6849                &state,
6850                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6851                &view_options,
6852            )
6853            .with_timing(
6854                4,
6855                Duration::from_secs(25),
6856                Duration::from_secs(10),
6857                25_000,
6858                125_000,
6859                125_000,
6860            )
6861            .with_live_progress(Some(status_update::LiveIterationProgress {
6862                completed_points: 25_000,
6863                target_points: 100_000,
6864            })),
6865            |_| {},
6866        );
6867
6868        assert!(rendered.contains("ETA to ⟨I⟩ target"), "{rendered}");
6869        assert!(rendered.contains("∞"), "{rendered}");
6870    }
6871
6872    #[test]
6873    fn ratatui_overview_prefers_finite_relative_eta_over_infinite_absolute_eta() {
6874        let state = make_integration_state();
6875        let view_options = IntegrationStatusViewOptions {
6876            target_relative_accuracy: Some(1.0e-3),
6877            target_absolute_accuracy: Some(1.0e-99),
6878            show_statistics: false,
6879            show_max_weight_details: false,
6880            ..default_view_options()
6881        };
6882        let rendered = render_ratatui_update(
6883            StatusUpdateBuildRequest::new(
6884                IntegrationStatusKind::Live,
6885                &state,
6886                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6887                &view_options,
6888            )
6889            .with_timing(
6890                4,
6891                Duration::from_secs(25),
6892                Duration::from_secs(10),
6893                25_000,
6894                125_000,
6895                125_000,
6896            )
6897            .with_live_progress(Some(status_update::LiveIterationProgress {
6898                completed_points: 25_000,
6899                target_points: 100_000,
6900            })),
6901            |_| {},
6902        );
6903
6904        assert!(rendered.contains("ETA to ⟨I⟩ target"), "{rendered}");
6905        assert!(!rendered.contains("∞"), "{rendered}");
6906    }
6907
6908    #[test]
6909    fn ratatui_recent_history_window_tracks_current_iteration_points() {
6910        let mut dashboard = RatatuiDashboardState::new();
6911        let mut state = make_integration_state();
6912        let view_options = IntegrationStatusViewOptions {
6913            show_statistics: false,
6914            show_max_weight_details: false,
6915            ..default_view_options()
6916        };
6917
6918        state.iter = 1;
6919        dashboard.update(build_status_update(
6920            StatusUpdateBuildRequest::new(
6921                IntegrationStatusKind::Live,
6922                &state,
6923                &[None, None],
6924                &view_options,
6925            )
6926            .with_timing(
6927                4,
6928                Duration::from_secs(10),
6929                Duration::from_secs(10),
6930                500_000,
6931                1_000_000,
6932                1_000_000,
6933            )
6934            .with_live_progress(Some(status_update::LiveIterationProgress {
6935                completed_points: 500_000,
6936                target_points: 1_000_000,
6937            })),
6938        ));
6939
6940        state.iter = 2;
6941        dashboard.update(build_status_update(
6942            StatusUpdateBuildRequest::new(
6943                IntegrationStatusKind::Live,
6944                &state,
6945                &[None, None],
6946                &view_options,
6947            )
6948            .with_timing(
6949                4,
6950                Duration::from_secs(20),
6951                Duration::from_secs(5),
6952                0,
6953                2_000_000,
6954                2_000_000,
6955            )
6956            .with_live_progress(Some(status_update::LiveIterationProgress {
6957                completed_points: 0,
6958                target_points: 1_000_000,
6959            })),
6960        ));
6961        dashboard.update(build_status_update(
6962            StatusUpdateBuildRequest::new(
6963                IntegrationStatusKind::Live,
6964                &state,
6965                &[None, None],
6966                &view_options,
6967            )
6968            .with_timing(
6969                4,
6970                Duration::from_secs(25),
6971                Duration::from_secs(10),
6972                200_000,
6973                2_200_000,
6974                2_200_000,
6975            )
6976            .with_live_progress(Some(status_update::LiveIterationProgress {
6977                completed_points: 200_000,
6978                target_points: 1_000_000,
6979            })),
6980        ));
6981
6982        dashboard.toggle_chart_history_window();
6983        for _ in 0..5 {
6984            dashboard.narrow_chart_history_window();
6985        }
6986
6987        assert_eq!(
6988            dashboard.visible_history_sample_bounds(),
6989            Some((2_000_000, 2_200_000))
6990        );
6991    }
6992
6993    #[test]
6994    fn ratatui_full_history_preserves_origin_after_compaction() {
6995        let mut dashboard = RatatuiDashboardState::new();
6996        let mut state = make_integration_state();
6997        let view_options = IntegrationStatusViewOptions {
6998            show_statistics: false,
6999            show_max_weight_details: false,
7000            ..default_view_options()
7001        };
7002
7003        for index in 0..4_300 {
7004            state.iter = 1 + index / 40;
7005            let total_points = index * 1_000;
7006            dashboard.update(build_status_update(
7007                StatusUpdateBuildRequest::new(
7008                    IntegrationStatusKind::Live,
7009                    &state,
7010                    &[None, None],
7011                    &view_options,
7012                )
7013                .with_timing(
7014                    4,
7015                    Duration::from_secs(index as u64),
7016                    Duration::from_secs(10),
7017                    total_points % 1_000_000,
7018                    total_points,
7019                    total_points,
7020                )
7021                .with_live_progress(Some(status_update::LiveIterationProgress {
7022                    completed_points: total_points % 1_000_000,
7023                    target_points: 1_000_000,
7024                })),
7025            ));
7026        }
7027
7028        dashboard.toggle_chart_history_window();
7029        dashboard.toggle_chart_history_window();
7030
7031        assert_eq!(
7032            dashboard.visible_history_sample_bounds(),
7033            Some((0, 4_299_000))
7034        );
7035    }
7036
7037    #[test]
7038    fn ratatui_convergence_title_shows_active_y_span() {
7039        let state = make_integration_state();
7040        let view_options = IntegrationStatusViewOptions {
7041            show_statistics: false,
7042            show_max_weight_details: false,
7043            ..default_view_options()
7044        };
7045        let rendered = render_ratatui_update(
7046            StatusUpdateBuildRequest::new(
7047                IntegrationStatusKind::Live,
7048                &state,
7049                &[None, None],
7050                &view_options,
7051            )
7052            .with_timing(
7053                4,
7054                Duration::from_secs(15),
7055                Duration::from_secs(10),
7056                25_000,
7057                125_000,
7058                125_000,
7059            )
7060            .with_live_progress(Some(status_update::LiveIterationProgress {
7061                completed_points: 25_000,
7062                target_points: 100_000,
7063            })),
7064            |dashboard| dashboard.widen_chart_y_sigma_span(),
7065        );
7066
7067        assert!(rendered.contains("y ±5σ"), "{rendered}");
7068    }
7069
7070    #[test]
7071    fn ratatui_convergence_axis_uses_small_dimensionful_uncertainty_verbatim() {
7072        let mut state = make_integration_state();
7073        state.all_integrals[0] = make_accumulator(-2.12e-15, 5.2e-16, 1.0, 1.0e-15, 1.0e-16, 1.0);
7074        let view_options = IntegrationStatusViewOptions {
7075            show_statistics: false,
7076            show_max_weight_details: false,
7077            ..default_view_options()
7078        };
7079        let rendered = render_ratatui_update(
7080            StatusUpdateBuildRequest::new(
7081                IntegrationStatusKind::Live,
7082                &state,
7083                &[None, None],
7084                &view_options,
7085            )
7086            .with_timing(
7087                4,
7088                Duration::from_secs(15),
7089                Duration::from_secs(10),
7090                25_000,
7091                125_000,
7092                125_000,
7093            )
7094            .with_live_progress(Some(status_update::LiveIterationProgress {
7095                completed_points: 25_000,
7096                target_points: 100_000,
7097            })),
7098            |_| {},
7099        );
7100
7101        assert!(rendered.contains("-4.200e-15"), "{rendered}");
7102        assert!(rendered.contains("-4.000e-17"), "{rendered}");
7103        assert!(!rendered.contains("e-12"), "{rendered}");
7104    }
7105
7106    #[test]
7107    fn ratatui_convergence_waits_for_non_zero_uncertainty() {
7108        let mut state = make_integration_state();
7109        state.all_integrals[0] = make_accumulator(1.0e-15, 0.0, 1.0, 1.0e-15, 0.0, 1.0);
7110        let view_options = IntegrationStatusViewOptions {
7111            show_statistics: false,
7112            show_max_weight_details: false,
7113            ..default_view_options()
7114        };
7115        let rendered = render_ratatui_update(
7116            StatusUpdateBuildRequest::new(
7117                IntegrationStatusKind::Live,
7118                &state,
7119                &[None, None],
7120                &view_options,
7121            )
7122            .with_timing(
7123                4,
7124                Duration::from_secs(15),
7125                Duration::from_secs(10),
7126                25_000,
7127                125_000,
7128                125_000,
7129            )
7130            .with_live_progress(Some(status_update::LiveIterationProgress {
7131                completed_points: 25_000,
7132                target_points: 100_000,
7133            })),
7134            |_| {},
7135        );
7136
7137        assert!(
7138            rendered.contains("Waiting for a finite central value and non-zero MC uncertainty"),
7139            "{rendered}"
7140        );
7141    }
7142
7143    #[test]
7144    fn ratatui_discrete_tab_renders_selected_bin_detail() {
7145        let state = make_discrete_integration_state();
7146        let view_options = IntegrationStatusViewOptions {
7147            show_statistics: false,
7148            show_max_weight_details: false,
7149            show_top_discrete_grid: false,
7150            show_discrete_contributions_sum: false,
7151            show_max_weight_info_for_discrete_bins: false,
7152            ..default_view_options()
7153        };
7154        let rendered = render_ratatui_update(
7155            StatusUpdateBuildRequest::new(
7156                IntegrationStatusKind::Iteration,
7157                &state,
7158                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
7159                &view_options,
7160            )
7161            .with_timing(
7162                4,
7163                Duration::from_secs(12),
7164                Duration::from_secs(12),
7165                110_000,
7166                210_000,
7167                210_000,
7168            ),
7169            |dashboard| dashboard.select_tab(1),
7170        );
7171
7172        assert!(rendered.contains("Selected bin"), "{rendered}");
7173        assert!(
7174            rendered.contains("Discrete bins ⟨I⟩ for focused integrand"),
7175            "{rendered}"
7176        );
7177        assert!(rendered.contains("sample %"), "{rendered}");
7178        assert!(rendered.contains("GL0"), "{rendered}");
7179        assert!(rendered.contains("GL1"), "{rendered}");
7180        assert!(rendered.contains("pdf"), "{rendered}");
7181        assert!(rendered.contains("Per integrand details"), "{rendered}");
7182        assert!(rendered.contains("# samples"), "{rendered}");
7183    }
7184
7185    #[test]
7186    fn ratatui_single_graph_subset_keeps_the_graph_row() {
7187        let state = make_single_graph_discrete_integration_state();
7188        let view_options = IntegrationStatusViewOptions {
7189            show_statistics: false,
7190            show_max_weight_details: false,
7191            ..default_view_options()
7192        };
7193        let rendered = render_ratatui_update(
7194            StatusUpdateBuildRequest::new(
7195                IntegrationStatusKind::Iteration,
7196                &state,
7197                &[None, None],
7198                &view_options,
7199            ),
7200            |dashboard| dashboard.select_tab(1),
7201        );
7202
7203        assert!(
7204            rendered.contains("Discrete bins ⟨I⟩ for focused integrand"),
7205            "{rendered}"
7206        );
7207        assert!(rendered.contains("GL22"), "{rendered}");
7208        assert!(!rendered.contains("GL1"), "{rendered}");
7209    }
7210
7211    #[test]
7212    fn ratatui_max_weight_tab_preserves_full_scientific_exponent() {
7213        let mut state = make_discrete_integration_state();
7214        let max_eval = 3.8477312059601813_f64;
7215        let expected = format!("{:+.16e}", max_eval);
7216        state.all_integrals[0].re.max_eval_positive = F(max_eval);
7217        state.all_integrals[0].re.max_eval_positive_xs = Some(Sample::Discrete(
7218            F(1.0),
7219            0,
7220            Some(Box::new(Sample::Continuous(
7221                F(1.0),
7222                vec![F(0.4502578156709256)],
7223            ))),
7224        ));
7225
7226        let view_options = IntegrationStatusViewOptions {
7227            show_statistics: false,
7228            ..default_view_options()
7229        };
7230        let rendered = render_ratatui_update(
7231            StatusUpdateBuildRequest::new(
7232                IntegrationStatusKind::Iteration,
7233                &state,
7234                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
7235                &view_options,
7236            )
7237            .with_timing(
7238                4,
7239                Duration::from_secs(12),
7240                Duration::from_secs(12),
7241                110_000,
7242                210_000,
7243                210_000,
7244            ),
7245            |dashboard| dashboard.select_tab(2),
7246        );
7247
7248        assert!(rendered.contains(&expected), "{rendered}");
7249        assert!(rendered.contains("graph: 0, xs: ["), "{rendered}");
7250    }
7251
7252    #[test]
7253    fn target_accuracy_status_treats_zero_relative_reference_as_inactive() {
7254        let state = make_integration_state();
7255        let targets = vec![Some(Complex::new(F(0.0), F(0.0))); state.all_integrals.len()];
7256        let status = status_update::evaluate_target_accuracy(
7257            &state,
7258            100_000,
7259            Duration::from_secs(10),
7260            &targets,
7261            IntegrationStatusPhaseDisplay::Real,
7262            Some(0.05),
7263            None,
7264        );
7265
7266        assert!(!status.relative_reached);
7267        assert!(!status.absolute_reached);
7268        assert_eq!(status.eta_to_target, None);
7269
7270        // A missing reference still uses that integral's nonzero mean.
7271        let status = status_update::evaluate_target_accuracy(
7272            &state,
7273            100_000,
7274            Duration::from_secs(10),
7275            &[Some(Complex::new(F(0.0), F(0.0))), None],
7276            IntegrationStatusPhaseDisplay::Real,
7277            Some(0.05),
7278            None,
7279        );
7280        assert!(status.eta_to_target.is_some());
7281    }
7282}
7283
7284#[test]
7285fn test_threading() {
7286    use symbolica::numerical_integration::ContinuousGrid;
7287
7288    fn test_fn(x: f64) -> f64 {
7289        x * x * (x * 6.).sin()
7290    }
7291
7292    let mut acc_1 = StatisticsAccumulator::<f64>::new();
7293    let mut acc_2 = StatisticsAccumulator::<f64>::new();
7294
7295    let samples_per_sample = 4;
7296    let samples_per_iter = 100000;
7297    let n_iter = 10;
7298
7299    let mut rng = MonteCarloRng::new(42, 0);
7300
7301    let mut grid = Grid::<f64>::Continuous(ContinuousGrid::new(1, 64, 100, None, false).unwrap());
7302
7303    for _i_iter in 0..n_iter {
7304        let mut multiplice_accs = vec![StatisticsAccumulator::<f64>::new(); samples_per_sample];
7305        for _i_sample in 0..samples_per_iter {
7306            let n_samples = (0..samples_per_sample)
7307                .map(|_| {
7308                    let mut sample = Sample::new();
7309                    grid.sample(&mut rng, &mut sample);
7310                    sample
7311                })
7312                .collect_vec();
7313
7314            let n_evals = n_samples
7315                .iter()
7316                .map(|s| match s {
7317                    Sample::Continuous(_, xs) => test_fn(xs[0]),
7318                    _ => unreachable!(),
7319                })
7320                .collect_vec();
7321
7322            for (i_eval, (sample, eval)) in n_samples.iter().zip(&n_evals).enumerate() {
7323                acc_1.add_sample(eval * sample.get_weight(), Some(sample));
7324                multiplice_accs[i_eval].add_sample(eval * sample.get_weight(), Some(sample));
7325            }
7326        }
7327
7328        for acc in multiplice_accs {
7329            acc_2.merge_samples(&mut acc.clone());
7330        }
7331        acc_1.update_iter(false);
7332        acc_2.update_iter(false);
7333    }
7334
7335    println!("acc1: {:?}", acc_1);
7336    println!("acc2: {:?}", acc_2);
7337}