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 bincode::Decode;
14use bincode::Encode;
15use color_eyre::{Report, Result};
16use colored::Colorize;
17use itertools::Itertools;
18use itertools::izip;
19use linnet::half_edge::involution::{EdgeVec, Orientation};
20use rayon::ThreadPoolBuilder;
21use serde::Deserialize;
22use serde::Serialize;
23use spenso::algebra::algebraic_traits::IsZero;
24use symbolica::domains::float::Constructible;
25use symbolica::numerical_integration::{
26    DiscreteGrid, Grid, MonteCarloRng, Sample, StatisticsAccumulator,
27};
28
29use crate::Integrand;
30use crate::graph::{GroupId, LoopMomentumBasis};
31use crate::integrands::HasIntegrand;
32use crate::integrands::evaluation::EvaluationResult;
33use crate::integrands::evaluation::StatisticsCounter;
34use crate::integrands::process::ProcessIntegrand;
35use crate::model::{Model, SerializableInputParamCard};
36use crate::observables::{EventGroupList, ObservableAccumulatorBundle, ObservableFileFormat};
37use crate::settings::IntegratorSettings;
38use crate::settings::RuntimeSettings;
39use crate::settings::runtime::{
40    ComponentDiscreteBreakdown, DiscreteBreakdown, DiscreteBreakdownEntry, DiscreteCoordinate,
41    DiscreteGraphSamplingType, IntegralEstimate, IntegratedPhase, IntegrationResult,
42    IntegrationTableComponentResult, MaxWeightInfoEntry, SamplingSettings, SlotIntegrationResult,
43};
44use crate::utils;
45use crate::utils::F;
46use crate::{
47    clear_iteration_abort_request, is_interrupted, is_iteration_abort_requested, set_interrupted,
48};
49use rayon::prelude::*;
50pub use render_ratatui::RatatuiDashboardState;
51pub use render_tabled::TabledRenderOptions;
52use spenso::algebra::complex::Complex;
53pub use status_update::{
54    ContributionSortMode, IntegrationStatusKind, IntegrationStatusPhaseDisplay,
55    IntegrationStatusViewOptions, StatusUpdate,
56};
57use status_update::{
58    StatusUpdateBuildRequest, build_saved_status_update, build_status_update,
59    evaluate_target_accuracy,
60};
61use std::fs;
62use std::path::Path;
63use std::path::PathBuf;
64use std::time::Duration;
65use std::time::Instant;
66use tabled::{
67    Tabled,
68    builder::Builder,
69    settings::{
70        Alignment, Modify, Panel, Style,
71        object::{Columns, Rows},
72        style::HorizontalLine,
73    },
74};
75#[allow(unused_imports)]
76use tracing::{debug, error, info, trace, warn};
77
78#[derive(Clone, Copy, Debug, Default)]
79pub struct WorkspaceSnapshotControl {
80    pub write_iteration_archives: bool,
81}
82
83#[derive(Clone, Copy, Debug)]
84pub struct IterationBatchingSettings {
85    pub batch_size: Option<usize>,
86    pub batch_timing_seconds: f64,
87    pub min_time_between_status_updates_seconds: f64,
88    pub emit_live_status_updates: bool,
89    pub emit_initial_status_update: bool,
90}
91
92impl Default for IterationBatchingSettings {
93    fn default() -> Self {
94        Self {
95            batch_size: None,
96            batch_timing_seconds: 5.0,
97            min_time_between_status_updates_seconds: 0.0,
98            emit_live_status_updates: true,
99            emit_initial_status_update: true,
100        }
101    }
102}
103
104pub struct IntegrationSlot {
105    pub meta: SlotMeta,
106    pub settings: RuntimeSettings,
107    pub model: Model,
108    pub integrand: Integrand,
109    pub target: Option<Complex<F<f64>>>,
110}
111
112impl IntegrationSlot {
113    pub fn new(
114        meta: SlotMeta,
115        settings: RuntimeSettings,
116        model: Model,
117        integrand: Integrand,
118        target: Option<Complex<F<f64>>>,
119    ) -> Self {
120        Self {
121            meta,
122            settings,
123            model,
124            integrand,
125            target,
126        }
127    }
128}
129
130pub struct HavanaIntegrateRequest {
131    pub slots: Vec<IntegrationSlot>,
132    pub sampling_correlation_mode: SamplingCorrelationMode,
133    pub n_cores: usize,
134    pub state: Option<IntegrationState>,
135    pub workspace: Option<PathBuf>,
136    pub output_control: WorkspaceSnapshotControl,
137    pub batching: IterationBatchingSettings,
138    pub view_options: IntegrationStatusViewOptions,
139}
140
141// const N_INTEGRAND_ACCUMULATORS: usize = 2;
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
144pub struct SlotMeta {
145    pub process_name: String,
146    pub integrand_name: String,
147}
148
149impl SlotMeta {
150    pub fn key(&self) -> String {
151        format!("{}@{}", self.process_name, self.integrand_name)
152    }
153}
154
155#[derive(Serialize, Deserialize, Clone)]
156pub struct IntegrationWorkspaceManifest {
157    pub slots: Vec<SlotMeta>,
158    pub targets: Vec<Option<Complex<F<f64>>>>,
159    pub effective_model_parameters: Vec<SerializableInputParamCard<F<f64>>>,
160    pub integrand_fingerprints: Vec<String>,
161    pub training_slot: usize,
162    pub integrator_settings_slot: usize,
163    pub sampling_correlation_mode: SamplingCorrelationMode,
164}
165
166impl crate::utils::serde_utils::SmartSerde for IntegrationWorkspaceManifest {}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
169#[serde(rename_all = "snake_case")]
170pub enum SamplingCorrelationMode {
171    Correlated,
172    Uncorrelated,
173}
174
175impl SamplingCorrelationMode {
176    fn state_index(self, slot_index: usize) -> usize {
177        match self {
178            Self::Correlated => 0,
179            Self::Uncorrelated => slot_index,
180        }
181    }
182
183    fn state_count(self, n_slots: usize) -> usize {
184        match self {
185            Self::Correlated => 1,
186            Self::Uncorrelated => n_slots,
187        }
188    }
189}
190
191#[derive(Serialize, Deserialize, Encode, Decode, Clone)]
192struct DiscreteGridAccumulatorSummary {
193    #[bincode(with_serde)]
194    bins: Vec<DiscreteGridBinAccumulatorSummary>,
195}
196
197#[derive(Serialize, Deserialize, Encode, Decode, Clone)]
198struct DiscreteGridBinAccumulatorSummary {
199    #[bincode(with_serde)]
200    accumulator: StatisticsAccumulator<F<f64>>,
201    sub_summary: Option<Box<DiscreteGridAccumulatorSummary>>,
202}
203
204#[derive(Serialize, Deserialize, Encode, Decode, Clone, Default)]
205struct PersistedDiscreteBreakdownMetadata {
206    axis_label: String,
207    #[bincode(with_serde)]
208    fixed_coordinates: Vec<DiscreteCoordinate>,
209    bin_labels: Vec<String>,
210}
211
212#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
213struct SamplingSlotState {
214    #[bincode(with_serde)]
215    grid: Grid<F<f64>>,
216    discrete_axis_labels: Vec<String>,
217}
218
219impl SamplingSlotState {
220    fn new(grid: Grid<F<f64>>, discrete_axis_labels: Vec<String>) -> Self {
221        Self {
222            grid,
223            discrete_axis_labels,
224        }
225    }
226
227    fn from_integrand(integrand: &Integrand) -> Self {
228        let grid = integrand.create_grid();
229        let discrete_axis_labels = match integrand {
230            Integrand::ProcessIntegrand(process_integrand) => {
231                discrete_axis_labels(&process_integrand.get_settings().sampling)
232                    .into_iter()
233                    .map(str::to_string)
234                    .collect_vec()
235            }
236            _ => Vec::new(),
237        };
238        Self::new(grid, discrete_axis_labels)
239    }
240}
241
242#[derive(Default)]
243struct MonitoredDiscreteSetup {
244    path: Option<Vec<usize>>,
245    axis_label: Option<String>,
246    bin_descriptions: Option<Vec<String>>,
247    warning: Option<String>,
248}
249
250#[derive(Tabled)]
251pub struct IntegralResult {
252    id: String,
253    n_samples: String,
254    #[tabled(rename = "n_samples[%]")]
255    n_samples_perc: String,
256    #[tabled(rename = "<I>")]
257    integral: String,
258    #[tabled(rename = "sqrt(σ)")]
259    variance: String,
260    err: String,
261    #[tabled(rename = "err[%]")]
262    err_perc: String,
263    #[tabled(rename = "PDF")]
264    pdf: String,
265}
266
267#[derive(Serialize, Deserialize, Encode, Decode, Clone)]
268pub struct ComplexAccumulator {
269    #[bincode(with_serde)]
270    pub re: StatisticsAccumulator<F<f64>>,
271    #[bincode(with_serde)]
272    pub im: StatisticsAccumulator<F<f64>>,
273}
274
275impl ComplexAccumulator {
276    pub(crate) fn new() -> Self {
277        Self {
278            re: StatisticsAccumulator::new(),
279            im: StatisticsAccumulator::new(),
280        }
281    }
282
283    /// used to escalate the stability test on high evaluation values
284    pub(crate) fn get_worst_case(&self) -> Complex<F<f64>> {
285        Complex::new(
286            self.re
287                .max_eval_positive
288                .abs()
289                .max(self.re.max_eval_negative.abs()),
290            self.im
291                .max_eval_positive
292                .abs()
293                .max(self.im.max_eval_negative.abs()),
294        )
295    }
296
297    /// add a sample to the accumulator
298    pub(crate) fn add_sample(
299        &mut self,
300        result: Complex<F<f64>>,
301        sample_weight: F<f64>,
302        sample: Option<&Sample<F<f64>>>,
303    ) {
304        self.re.add_sample(result.re * sample_weight, sample);
305        self.im.add_sample(result.im * sample_weight, sample);
306    }
307
308    pub(crate) fn merge(&mut self, other: &Self) {
309        self.re.merge_samples_no_reset(&other.re);
310        self.im.merge_samples_no_reset(&other.im);
311    }
312
313    pub(crate) fn update_iter(&mut self, use_weighted_average: bool) {
314        self.re.update_iter(use_weighted_average);
315        self.im.update_iter(use_weighted_average);
316    }
317
318    pub(crate) fn max_weight_rows(
319        &self,
320        slot_meta: &SlotMeta,
321        discrete_axis_labels: &[String],
322    ) -> Vec<[String; 3]> {
323        let max_evals = [
324            &self.re.max_eval_positive,
325            &self.re.max_eval_negative,
326            &self.im.max_eval_positive,
327            &self.im.max_eval_negative,
328        ];
329
330        let max_eval_samples = [
331            &self.re.max_eval_positive_xs,
332            &self.re.max_eval_negative_xs,
333            &self.im.max_eval_positive_xs,
334            &self.im.max_eval_negative_xs,
335        ];
336
337        let sign_strs = ["+", "-", "+", "-"];
338        let phase_strs = ["re", "re", "im", "im"];
339
340        izip!(
341            max_evals.iter(),
342            max_eval_samples.iter(),
343            sign_strs.iter(),
344            phase_strs.iter()
345        )
346        .filter_map(|(max_eval, max_eval_sample, sign_str, phase_str)| {
347            if max_eval.is_non_zero() {
348                Some([
349                    format!(
350                        "{} {} [{}] ",
351                        slot_label(slot_meta),
352                        format!("{:<2}", phase_str).blue(),
353                        format!("{:<1}", sign_str).blue()
354                    ),
355                    format!("{:+.16e}", max_eval),
356                    if let Some(sample) = max_eval_sample {
357                        format_max_eval_sample(sample, discrete_axis_labels, &[])
358                    } else {
359                        "N/A".to_string()
360                    },
361                ])
362            } else {
363                None
364            }
365        })
366        .collect()
367    }
368}
369
370impl DiscreteGridAccumulatorSummary {
371    fn from_grid(grid: &Grid<F<f64>>) -> Option<Self> {
372        match grid {
373            Grid::Discrete(discrete_grid) => Some(Self {
374                bins: discrete_grid
375                    .bins
376                    .iter()
377                    .map(|bin| DiscreteGridBinAccumulatorSummary {
378                        accumulator: StatisticsAccumulator::new(),
379                        sub_summary: bin
380                            .sub_grid
381                            .as_ref()
382                            .and_then(Self::from_grid)
383                            .map(Box::new),
384                    })
385                    .collect(),
386            }),
387            Grid::Continuous(_) | Grid::Uniform(_, _) => None,
388        }
389    }
390
391    fn merge_iteration_grid(&mut self, grid: &Grid<F<f64>>) {
392        let Grid::Discrete(discrete_grid) = grid else {
393            return;
394        };
395
396        for (summary_bin, grid_bin) in self.bins.iter_mut().zip(discrete_grid.bins.iter()) {
397            summary_bin
398                .accumulator
399                .merge_samples_no_reset(&grid_bin.accumulator);
400            if let (Some(sub_summary), Some(sub_grid)) =
401                (summary_bin.sub_summary.as_mut(), grid_bin.sub_grid.as_ref())
402            {
403                sub_summary.merge_iteration_grid(sub_grid);
404            }
405        }
406    }
407
408    fn update_iter(&mut self) {
409        for bin in &mut self.bins {
410            bin.accumulator.update_iter(false);
411            if let Some(sub_summary) = bin.sub_summary.as_mut() {
412                sub_summary.update_iter();
413            }
414        }
415    }
416
417    fn first_non_trivial_breakdown(
418        &self,
419        metadata: &PersistedDiscreteBreakdownMetadata,
420        pdfs: &[F<f64>],
421    ) -> Option<DiscreteBreakdown> {
422        (!self.bins.is_empty()).then(|| DiscreteBreakdown {
423            axis_label: metadata.axis_label.clone(),
424            fixed_coordinates: metadata.fixed_coordinates.clone(),
425            entries: self
426                .bins
427                .iter()
428                .enumerate()
429                .map(|(bin_index, bin)| DiscreteBreakdownEntry {
430                    bin_index,
431                    bin_label: metadata.bin_labels.get(bin_index).cloned(),
432                    pdf: pdfs.get(bin_index).copied().unwrap_or(F(0.0)),
433                    value: bin.accumulator.avg,
434                    error: bin.accumulator.err,
435                    chi_sq: bin.accumulator.chi_sq,
436                    processed_samples: bin.accumulator.processed_samples,
437                })
438                .collect(),
439        })
440    }
441}
442
443struct IntegralResultCells {
444    integrand: String,
445    value: String,
446    relative_error: String,
447    chi_sq: String,
448    delta_sigma: Option<String>,
449    delta_percent: Option<String>,
450    mwi: String,
451}
452
453const DEFAULT_MAX_SHARED_TABLE_WIDTH: usize = 250;
454
455fn slot_label(slot_meta: &SlotMeta) -> String {
456    format!("itg {}", slot_meta.key())
457}
458
459fn slot_key_label(slot_meta: &SlotMeta) -> String {
460    slot_meta.key()
461}
462
463fn format_max_eval_coordinate(value: F<f64>) -> String {
464    let formatted = format!("{:.16e}", value.0);
465    let Some((mantissa, exponent)) = formatted.rsplit_once('e') else {
466        return formatted;
467    };
468    let exponent = exponent.parse::<i32>().unwrap_or_default();
469    format!("{mantissa}e{exponent:+03}")
470}
471
472fn format_max_eval_coordinates(xs: &[F<f64>]) -> String {
473    if xs.len() <= 3 {
474        return format!(
475            "[ {} ]",
476            xs.iter()
477                .map(|value| format_max_eval_coordinate(*value))
478                .join(" ")
479        );
480    }
481
482    let rows = xs
483        .chunks(3)
484        .map(|chunk| {
485            chunk
486                .iter()
487                .map(|value| format_max_eval_coordinate(*value))
488                .join(" ")
489        })
490        .join("\n");
491    format!("[\n{rows} ]")
492}
493
494pub(crate) fn discrete_axis_label(axis_labels: &[String], depth: usize) -> &str {
495    axis_labels.get(depth).map(String::as_str).unwrap_or("idx")
496}
497
498fn append_max_eval_sample_parts(
499    sample: &Sample<F<f64>>,
500    axis_labels: &[String],
501    depth: usize,
502    parts: &mut Vec<String>,
503) {
504    match sample {
505        Sample::Continuous(_, xs) => {
506            parts.push(format!("xs: {}", format_max_eval_coordinates(xs)));
507        }
508        Sample::Discrete(_, index, Some(nested_sample)) => {
509            parts.push(format!(
510                "{}: {}",
511                discrete_axis_label(axis_labels, depth),
512                index
513            ));
514            append_max_eval_sample_parts(nested_sample, axis_labels, depth + 1, parts);
515        }
516        Sample::Discrete(_, index, None) => {
517            parts.push(format!(
518                "{}: {}",
519                discrete_axis_label(axis_labels, depth),
520                index
521            ));
522        }
523        Sample::Uniform(_, indices, xs) => {
524            for (offset, index) in indices.iter().enumerate() {
525                parts.push(format!(
526                    "{}: {}",
527                    discrete_axis_label(axis_labels, depth + offset),
528                    index
529                ));
530            }
531            parts.push(format!("xs: {}", format_max_eval_coordinates(xs)));
532        }
533    }
534}
535
536fn format_max_eval_sample(
537    sample: &Sample<F<f64>>,
538    axis_labels: &[String],
539    prefix_path: &[usize],
540) -> String {
541    let mut parts = prefix_path
542        .iter()
543        .enumerate()
544        .map(|(depth, index)| format!("{}: {}", discrete_axis_label(axis_labels, depth), index))
545        .collect_vec();
546    append_max_eval_sample_parts(sample, axis_labels, prefix_path.len(), &mut parts);
547    if parts.is_empty() {
548        String::from("N/A")
549    } else {
550        parts.join(", ")
551    }
552}
553
554fn format_iteration_points(points: usize) -> String {
555    format_abbreviated_count(points)
556}
557
558fn format_total_points(points: usize) -> String {
559    format_abbreviated_count(points)
560}
561
562#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
563enum ComponentKind {
564    Real,
565    Imag,
566}
567
568impl ComponentKind {
569    fn all_for_display(display: IntegrationStatusPhaseDisplay) -> Vec<Self> {
570        let mut components = Vec::new();
571        if display.shows_real() {
572            components.push(Self::Real);
573        }
574        if display.shows_imag() {
575            components.push(Self::Imag);
576        }
577        components
578    }
579
580    fn tag(self) -> &'static str {
581        match self {
582            Self::Real => "re",
583            Self::Imag => "im",
584        }
585    }
586
587    fn colorized_tag(self) -> String {
588        self.tag().blue().bold().to_string()
589    }
590}
591
592#[derive(Clone, Copy, Debug, Eq, PartialEq)]
593enum ContributionKind {
594    All,
595    Sum,
596    Bin(usize),
597}
598
599impl ContributionKind {
600    fn label(self, integration_state: &IntegrationState) -> String {
601        match self {
602            Self::All => "All".bold().green().to_string(),
603            Self::Sum => "Sum".bold().green().to_string(),
604            Self::Bin(bin_index) => contribution_bin_label(integration_state, bin_index),
605        }
606    }
607}
608
609#[derive(Clone, Debug)]
610struct DiscreteLevelContext {
611    path: Vec<usize>,
612    pdfs: Vec<F<f64>>,
613}
614
615fn first_non_trivial_discrete_path(grid: &Grid<F<f64>>) -> Option<Vec<usize>> {
616    match grid {
617        Grid::Discrete(discrete_grid) => {
618            if discrete_grid.bins.len() > 1 {
619                Some(Vec::new())
620            } else {
621                discrete_grid
622                    .bins
623                    .first()
624                    .and_then(|bin| bin.sub_grid.as_ref())
625                    .and_then(first_non_trivial_discrete_path)
626                    .map(|mut path| {
627                        path.insert(0, 0);
628                        path
629                    })
630            }
631        }
632        Grid::Continuous(_) | Grid::Uniform(_, _) => None,
633    }
634}
635
636fn discrete_grid_at_path<'a>(
637    grid: &'a Grid<F<f64>>,
638    path: &[usize],
639) -> Option<&'a DiscreteGrid<F<f64>>> {
640    match (grid, path.split_first()) {
641        (Grid::Discrete(discrete_grid), None) => Some(discrete_grid),
642        (Grid::Discrete(discrete_grid), Some((bin_index, rest))) => discrete_grid
643            .bins
644            .get(*bin_index)?
645            .sub_grid
646            .as_ref()
647            .and_then(|sub_grid| discrete_grid_at_path(sub_grid, rest)),
648        _ => None,
649    }
650}
651
652fn summary_at_path<'a>(
653    summary: &'a DiscreteGridAccumulatorSummary,
654    path: &[usize],
655) -> Option<&'a DiscreteGridAccumulatorSummary> {
656    match path.split_first() {
657        None => Some(summary),
658        Some((bin_index, rest)) => summary
659            .bins
660            .get(*bin_index)?
661            .sub_summary
662            .as_deref()
663            .and_then(|sub_summary| summary_at_path(sub_summary, rest)),
664    }
665}
666
667fn first_non_trivial_discrete_context(
668    sampling_grid: &Grid<F<f64>>,
669) -> Option<DiscreteLevelContext> {
670    let path = first_non_trivial_discrete_path(sampling_grid)?;
671    let discrete_grid = discrete_grid_at_path(sampling_grid, &path)?;
672    Some(DiscreteLevelContext {
673        path,
674        pdfs: discrete_grid.bins.iter().map(|bin| bin.pdf).collect(),
675    })
676}
677
678fn discrete_axis_labels(sampling: &SamplingSettings) -> Vec<&'static str> {
679    match sampling {
680        SamplingSettings::Default(_) | SamplingSettings::MultiChanneling(_) => Vec::new(),
681        SamplingSettings::DiscreteGraphs(settings) => {
682            let mut labels = vec!["graph"];
683            match &settings.sampling_type {
684                DiscreteGraphSamplingType::Default(_)
685                | DiscreteGraphSamplingType::MultiChanneling(_)
686                | DiscreteGraphSamplingType::TropicalSampling(_) => {
687                    if settings.sample_orientations {
688                        labels.push("orientation");
689                    }
690                }
691                DiscreteGraphSamplingType::DiscreteMultiChanneling(_) => {
692                    if settings.sample_orientations {
693                        labels.push("orientation");
694                    }
695                    labels.push("LMB channel");
696                }
697            }
698            labels
699        }
700    }
701}
702
703fn orientation_description(orientation: &EdgeVec<Orientation>) -> String {
704    orientation
705        .iter()
706        .map(|(_, orientation)| match *orientation {
707            Orientation::Default => '+',
708            Orientation::Reversed => '-',
709            Orientation::Undirected => '0',
710        })
711        .collect()
712}
713
714fn graph_group_description<I>(graph_names: I) -> String
715where
716    I: IntoIterator<Item = String>,
717{
718    let graph_names = graph_names.into_iter().collect_vec();
719    if graph_names.len() <= 1 {
720        return graph_names.into_iter().next().unwrap_or_default();
721    }
722
723    format!("[{}]", graph_names.join(","))
724}
725
726fn lmb_channel_description(lmb: &LoopMomentumBasis) -> String {
727    format!(
728        "({})",
729        lmb.loop_edges
730            .iter()
731            .map(|edge_id| edge_id.0.to_string())
732            .join(",")
733    )
734}
735
736fn first_non_trivial_discrete_bin_descriptions_for_process_integrand(
737    integrand: &ProcessIntegrand,
738    path: &[usize],
739    axis_label: &str,
740) -> Option<Vec<String>> {
741    match (integrand, axis_label) {
742        (ProcessIntegrand::Amplitude(integrand), "graph") => {
743            Some(
744                integrand
745                    .data
746                    .graph_group_structure
747                    .iter()
748                    .map(|group| {
749                        graph_group_description(group.into_iter().map(|graph_id| {
750                            integrand.data.graph_terms[graph_id].graph.name.clone()
751                        }))
752                    })
753                    .collect(),
754            )
755        }
756        (ProcessIntegrand::CrossSection(integrand), "graph") => {
757            Some(
758                integrand
759                    .data
760                    .graph_group_structure
761                    .iter()
762                    .map(|group| {
763                        graph_group_description(group.into_iter().map(|graph_id| {
764                            integrand.data.graph_terms[graph_id].graph.name.clone()
765                        }))
766                    })
767                    .collect(),
768            )
769        }
770        (ProcessIntegrand::Amplitude(integrand), "orientation") => {
771            let group_id = GroupId(*path.first()?);
772            let group = integrand.data.graph_group_structure.get(group_id)?;
773            let master = group.master();
774            Some(
775                integrand.data.graph_terms[master]
776                    .orientations
777                    .iter()
778                    .map(orientation_description)
779                    .collect(),
780            )
781        }
782        (ProcessIntegrand::CrossSection(integrand), "orientation") => {
783            let group_id = GroupId(*path.first()?);
784            let group = integrand.data.graph_group_structure.get(group_id)?;
785            let master = group.master();
786            Some(
787                integrand.data.graph_terms[master]
788                    .orientations
789                    .iter()
790                    .map(orientation_description)
791                    .collect(),
792            )
793        }
794        (ProcessIntegrand::Amplitude(integrand), "LMB channel") => {
795            let group_id = GroupId(*path.first()?);
796            let group = integrand.data.graph_group_structure.get(group_id)?;
797            let master = group.master();
798            let graph_term = &integrand.data.graph_terms[master];
799            let parameterization_settings = integrand
800                .settings
801                .sampling
802                .get_parameterization_settings()
803                .unwrap_or_default();
804            let effective_channels = graph_term
805                .multi_channeling_setup
806                .effective_channels(&graph_term.graph.name, &parameterization_settings)
807                .ok()?;
808            Some(
809                effective_channels
810                    .iter()
811                    .map(|&channel_lmb| {
812                        lmb_channel_description(
813                            &graph_term.multi_channeling_setup.all_bases[channel_lmb],
814                        )
815                    })
816                    .collect(),
817            )
818        }
819        (ProcessIntegrand::CrossSection(integrand), "LMB channel") => {
820            let group_id = GroupId(*path.first()?);
821            let group = integrand.data.graph_group_structure.get(group_id)?;
822            let master = group.master();
823            let graph_term = &integrand.data.graph_terms[master];
824            let parameterization_settings = integrand
825                .settings
826                .sampling
827                .get_parameterization_settings()
828                .unwrap_or_default();
829            let effective_channels = graph_term
830                .multi_channeling_setup
831                .effective_channels(&graph_term.graph.name, &parameterization_settings)
832                .ok()?;
833            Some(
834                effective_channels
835                    .iter()
836                    .map(|&channel_lmb| {
837                        lmb_channel_description(
838                            &graph_term.multi_channeling_setup.all_bases[channel_lmb],
839                        )
840                    })
841                    .collect(),
842            )
843        }
844        _ => None,
845    }
846}
847
848fn first_non_trivial_discrete_bin_descriptions_for_integrand(
849    integrand: &Integrand,
850    path: &[usize],
851    axis_label: &str,
852) -> Option<Vec<String>> {
853    match integrand {
854        Integrand::ProcessIntegrand(process_integrand) => {
855            first_non_trivial_discrete_bin_descriptions_for_process_integrand(
856                process_integrand,
857                path,
858                axis_label,
859            )
860        }
861        _ => None,
862    }
863}
864
865fn discrete_coordinate_label(
866    integrand: &Integrand,
867    path_prefix: &[usize],
868    axis_label: &str,
869    bin_index: usize,
870) -> Option<String> {
871    first_non_trivial_discrete_bin_descriptions_for_integrand(integrand, path_prefix, axis_label)?
872        .get(bin_index)
873        .cloned()
874}
875
876fn build_persisted_discrete_breakdown_metadata(
877    integrand: &Integrand,
878    path: &[usize],
879    discrete_axis_labels: &[String],
880) -> Option<PersistedDiscreteBreakdownMetadata> {
881    let axis_label = discrete_axis_labels.get(path.len())?.clone();
882    let bin_labels =
883        first_non_trivial_discrete_bin_descriptions_for_integrand(integrand, path, &axis_label)?;
884
885    let fixed_coordinates = path
886        .iter()
887        .enumerate()
888        .map(|(axis_index, &bin_index)| {
889            let fixed_axis_label = discrete_axis_labels.get(axis_index)?.clone();
890            Some(DiscreteCoordinate {
891                axis_label: fixed_axis_label.clone(),
892                bin_index,
893                bin_label: discrete_coordinate_label(
894                    integrand,
895                    &path[..axis_index],
896                    &fixed_axis_label,
897                    bin_index,
898                ),
899            })
900        })
901        .collect::<Option<Vec<_>>>()?;
902
903    Some(PersistedDiscreteBreakdownMetadata {
904        axis_label,
905        fixed_coordinates,
906        bin_labels,
907    })
908}
909
910fn monitored_discrete_layout(
911    grid: &Grid<F<f64>>,
912    discrete_axis_labels: &[String],
913    monitor_explicit_graph_subset: bool,
914) -> Option<(Vec<usize>, String, usize)> {
915    let path = if monitor_explicit_graph_subset
916        && discrete_axis_labels
917            .first()
918            .is_some_and(|label| label == "graph")
919        && matches!(grid, Grid::Discrete(_))
920    {
921        Vec::new()
922    } else {
923        first_non_trivial_discrete_path(grid)?
924    };
925    let axis_label = discrete_axis_labels.get(path.len())?.clone();
926    let discrete_grid = discrete_grid_at_path(grid, &path)?;
927    Some((path, axis_label, discrete_grid.bins.len()))
928}
929
930fn coalesce_first_non_trivial_discrete_bin_descriptions(
931    axis_label: &str,
932    slot_descriptions: &[(String, Vec<String>)],
933) -> (Option<Vec<String>>, Option<String>) {
934    let Some((reference_slot, reference_descriptions)) = slot_descriptions.first() else {
935        return (None, None);
936    };
937
938    if let Some((slot_key, _)) = slot_descriptions
939        .iter()
940        .skip(1)
941        .find(|(_, descriptions)| descriptions != reference_descriptions)
942    {
943        return (
944            None,
945            Some(format!(
946                "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.",
947                reference_slot.blue(),
948                slot_key.blue()
949            )),
950        );
951    }
952
953    (Some(reference_descriptions.clone()), None)
954}
955
956fn resolve_first_non_trivial_discrete_bin_descriptions(
957    slots: &[IntegrationSlot],
958    monitored_path: &[usize],
959    axis_label: &str,
960) -> (Option<Vec<String>>, Option<String>) {
961    let Some(slot_descriptions) = slots
962        .iter()
963        .map(|slot| {
964            first_non_trivial_discrete_bin_descriptions_for_integrand(
965                &slot.integrand,
966                monitored_path,
967                axis_label,
968            )
969            .map(|descriptions| (slot.meta.key(), descriptions))
970        })
971        .collect::<Option<Vec<_>>>()
972    else {
973        return (None, None);
974    };
975
976    coalesce_first_non_trivial_discrete_bin_descriptions(axis_label, &slot_descriptions)
977}
978
979fn resolve_monitored_discrete_setup(
980    sampling_correlation_mode: SamplingCorrelationMode,
981    slots: &[IntegrationSlot],
982    sampling_states: &[SamplingSlotState],
983) -> MonitoredDiscreteSetup {
984    let Some(reference_state) = sampling_states.first() else {
985        return MonitoredDiscreteSetup::default();
986    };
987    let Some(reference_slot) = slots.first() else {
988        return MonitoredDiscreteSetup::default();
989    };
990    let monitor_explicit_graph_subset = !reference_slot
991        .settings
992        .sampling
993        .selected_graph_names()
994        .is_empty();
995
996    let Some((reference_path, reference_axis_label, reference_bin_count)) =
997        monitored_discrete_layout(
998            &reference_state.grid,
999            &reference_state.discrete_axis_labels,
1000            monitor_explicit_graph_subset,
1001        )
1002    else {
1003        return MonitoredDiscreteSetup::default();
1004    };
1005
1006    if sampling_correlation_mode == SamplingCorrelationMode::Uncorrelated {
1007        for (slot_index, sampling_state) in sampling_states.iter().enumerate().skip(1) {
1008            let Some((path, axis_label, bin_count)) = monitored_discrete_layout(
1009                &sampling_state.grid,
1010                &sampling_state.discrete_axis_labels,
1011                !slots[slot_index]
1012                    .settings
1013                    .sampling
1014                    .selected_graph_names()
1015                    .is_empty(),
1016            ) else {
1017                return MonitoredDiscreteSetup {
1018                    warning: Some(
1019                        "Selected integrands do not all expose a compatible first non-trivial monitored discrete dimension. Shared discrete-bin monitoring tables will be disabled."
1020                            .to_string(),
1021                    ),
1022                    ..MonitoredDiscreteSetup::default()
1023                };
1024            };
1025
1026            if path != reference_path
1027                || axis_label != reference_axis_label
1028                || bin_count != reference_bin_count
1029            {
1030                return MonitoredDiscreteSetup {
1031                    warning: Some(format!(
1032                        "Selected integrands do not share a compatible first non-trivial monitored discrete layout (mismatch at {}). Shared discrete-bin monitoring tables will be disabled.",
1033                        slots[slot_index].meta.key().blue()
1034                    )),
1035                    ..MonitoredDiscreteSetup::default()
1036                };
1037            }
1038        }
1039    }
1040
1041    let (descriptions, label_warning) = resolve_first_non_trivial_discrete_bin_descriptions(
1042        slots,
1043        &reference_path,
1044        &reference_axis_label,
1045    );
1046
1047    MonitoredDiscreteSetup {
1048        path: Some(reference_path),
1049        axis_label: Some(reference_axis_label),
1050        bin_descriptions: descriptions,
1051        warning: label_warning,
1052    }
1053}
1054
1055fn render_orientation_description(description: &str) -> String {
1056    description
1057        .chars()
1058        .map(|sign| match sign {
1059            '+' => "+".green().bold().to_string(),
1060            '-' => "-".red().bold().to_string(),
1061            '0' => "0".dimmed().to_string(),
1062            other => other.to_string(),
1063        })
1064        .join("")
1065}
1066
1067fn render_graph_description(description: &str) -> String {
1068    if let Some(inner) = description
1069        .strip_prefix('[')
1070        .and_then(|trimmed| trimmed.strip_suffix(']'))
1071    {
1072        let names = inner
1073            .split(',')
1074            .filter(|name| !name.is_empty())
1075            .collect_vec();
1076        if names.is_empty() {
1077            return description.bold().green().to_string();
1078        }
1079
1080        let rendered = names
1081            .into_iter()
1082            .enumerate()
1083            .map(|(index, name)| {
1084                if index == 0 {
1085                    name.bold().green().to_string()
1086                } else {
1087                    name.bold().blue().to_string()
1088                }
1089            })
1090            .join(",");
1091        return format!("[{rendered}]");
1092    }
1093
1094    description.bold().green().to_string()
1095}
1096
1097fn render_bin_description(axis_label: &str, description: &str) -> String {
1098    match axis_label {
1099        "orientation" => render_orientation_description(description),
1100        "graph" => render_graph_description(description),
1101        _ => description.bold().green().to_string(),
1102    }
1103}
1104
1105fn contribution_bin_label(integration_state: &IntegrationState, bin_index: usize) -> String {
1106    if let (Some(axis_label), Some(descriptions)) = (
1107        integration_state
1108            .first_non_trivial_discrete_label
1109            .as_deref(),
1110        integration_state
1111            .first_non_trivial_discrete_bin_descriptions
1112            .as_ref(),
1113    ) && let Some(description) = descriptions.get(bin_index)
1114    {
1115        return format!(
1116            "#{bin_index}: {}",
1117            render_bin_description(axis_label, description)
1118        );
1119    }
1120
1121    format!("#{bin_index}")
1122}
1123
1124fn contribution_header_label(
1125    integration_state: &IntegrationState,
1126    discrete_monitoring_enabled: bool,
1127) -> String {
1128    if discrete_monitoring_enabled
1129        && let Some(label) = integration_state
1130            .first_non_trivial_discrete_label
1131            .as_deref()
1132    {
1133        return format!("Contribution (idx={label})")
1134            .bold()
1135            .blue()
1136            .to_string();
1137    }
1138
1139    "Contribution".bold().blue().to_string()
1140}
1141
1142fn format_percentage_sig(value: f64, significant_digits: usize) -> String {
1143    if !value.is_finite() {
1144        return "None".red().to_string();
1145    }
1146
1147    if value == 0.0 {
1148        let decimals = significant_digits.saturating_sub(1);
1149        return format!("{:.*}%", decimals, 0.0);
1150    }
1151
1152    let abs_value = value.abs();
1153    let exponent = abs_value.log10().floor() as i32;
1154    if exponent < -2 || exponent >= significant_digits as i32 {
1155        return format!("{:.*e}%", significant_digits.saturating_sub(1), value);
1156    }
1157
1158    let decimals = (significant_digits as i32 - exponent - 1).max(0) as usize;
1159    format!("{value:.decimals$}%")
1160}
1161
1162#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1163enum UncertaintyNotation {
1164    Dynamic,
1165    Scientific,
1166}
1167
1168fn format_significant_percentage(
1169    value: f64,
1170    significant_digits: usize,
1171    scientific_threshold: Option<(f64, f64)>,
1172) -> String {
1173    if !value.is_finite() {
1174        return "None".red().to_string();
1175    }
1176
1177    if value == 0.0 {
1178        return format!("{:.*}%", significant_digits.saturating_sub(1), 0.0);
1179    }
1180
1181    let abs_value = value.abs();
1182    if scientific_threshold.is_some_and(|(upper, lower)| abs_value >= upper || abs_value < lower) {
1183        return format!("{:.*e}%", significant_digits.saturating_sub(1), value);
1184    }
1185
1186    let exponent = abs_value.log10().floor() as i32;
1187    let decimals = (significant_digits as i32 - exponent - 1).max(0) as usize;
1188    format!("{value:.decimals$}%")
1189}
1190
1191fn should_use_scientific_uncertainty_notation(avg: F<f64>, err: F<f64>) -> bool {
1192    let avg_abs = avg.abs().0;
1193    let err_abs = err.abs().0;
1194    avg_abs >= 1e6
1195        || (avg_abs != 0.0 && avg_abs < 1e-5)
1196        || (avg.is_zero() && !(1e-4..1e5).contains(&err_abs))
1197}
1198
1199fn format_uncertainty_with_notation(
1200    avg: F<f64>,
1201    err: F<f64>,
1202    notation: UncertaintyNotation,
1203) -> String {
1204    if !matches!(notation, UncertaintyNotation::Scientific)
1205        && !should_use_scientific_uncertainty_notation(avg, err)
1206    {
1207        return utils::format_uncertainty(avg, err);
1208    }
1209
1210    if !avg.0.is_finite() || !err.0.is_finite() {
1211        return utils::format_uncertainty(avg, err);
1212    }
1213
1214    let exponent = if avg.is_non_zero() {
1215        avg.abs().0.log10().floor() as i32
1216    } else if err.is_non_zero() {
1217        err.abs().0.log10().floor() as i32
1218    } else {
1219        0
1220    };
1221    let scale = 10_f64.powi(exponent);
1222    let mantissa = utils::format_uncertainty(F(avg.0 / scale), F(err.0 / scale));
1223    format!("{mantissa}e{exponent}")
1224}
1225
1226fn format_signed_uncertainty(avg: F<f64>, err: F<f64>, notation: UncertaintyNotation) -> String {
1227    let formatted = format_uncertainty_with_notation(avg, err, notation);
1228    if avg.0.is_sign_negative() {
1229        formatted
1230    } else {
1231        format!("+{formatted}")
1232    }
1233}
1234
1235fn format_abbreviated_count(value: usize) -> String {
1236    if value < 1_000 {
1237        return value.to_string();
1238    }
1239
1240    let value = value as f64;
1241    if value < 1_000_000.0 {
1242        return format!("{:.2}K", value / 1_000.0);
1243    }
1244    if value < 1_000_000_000.0 {
1245        return format!("{:.2}M", value / 1_000_000.0);
1246    }
1247    if value < 1_000_000_000_000.0 {
1248        return format!("{:.2}B", value / 1_000_000_000.0);
1249    }
1250
1251    format!("{:.3}T", value / 1_000_000_000_000.0)
1252}
1253
1254fn build_integral_result_cells(
1255    itg: &StatisticsAccumulator<F<f64>>,
1256    slot_meta: &SlotMeta,
1257    i_iter: usize,
1258    tag: &str,
1259    trgt: Option<F<f64>>,
1260) -> IntegralResultCells {
1261    let relative_error = format_relative_error_cell(itg);
1262    let chi_sq = format_chi_sq_cell(itg, i_iter);
1263    let (delta_sigma, delta_percent) = format_delta_cells(itg, trgt);
1264    let mwi = format_mwi_cell(itg);
1265
1266    IntegralResultCells {
1267        integrand: format!(
1268            "{} {}:",
1269            slot_label(slot_meta),
1270            format!("{:-2}", tag).blue().bold(),
1271        ),
1272        value: format_signed_uncertainty(itg.avg, itg.err, UncertaintyNotation::Scientific)
1273            .blue()
1274            .bold()
1275            .to_string(),
1276        relative_error,
1277        chi_sq,
1278        delta_sigma,
1279        delta_percent,
1280        mwi,
1281    }
1282}
1283
1284fn format_relative_error_cell(itg: &StatisticsAccumulator<F<f64>>) -> String {
1285    format_relative_error_from_estimate(itg.avg, itg.err)
1286}
1287
1288fn format_relative_error_from_estimate(avg: F<f64>, err: F<f64>) -> String {
1289    if avg.is_zero() {
1290        return String::new();
1291    }
1292
1293    let formatted =
1294        format_significant_percentage((err / avg).abs().0 * 100.0, 3, Some((1.0e4, 1.0e-4)));
1295    if (err / avg).abs().0 > 0.01 {
1296        formatted.red().to_string()
1297    } else {
1298        formatted.green().to_string()
1299    }
1300}
1301
1302fn format_chi_sq_cell(itg: &StatisticsAccumulator<F<f64>>, i_iter: usize) -> String {
1303    let chi_sq = format!("{:.3}", itg.chi_sq.0 / (i_iter as f64));
1304    if itg.chi_sq / F::<f64>::new_from_usize(i_iter) > F(5.) {
1305        chi_sq.red().to_string()
1306    } else {
1307        chi_sq
1308    }
1309}
1310
1311fn format_delta_cells(
1312    itg: &StatisticsAccumulator<F<f64>>,
1313    trgt: Option<F<f64>>,
1314) -> (Option<String>, Option<String>) {
1315    format_delta_cells_from_estimate(itg.avg, itg.err, trgt)
1316}
1317
1318fn format_delta_cells_from_estimate(
1319    avg: F<f64>,
1320    err: F<f64>,
1321    trgt: Option<F<f64>>,
1322) -> (Option<String>, Option<String>) {
1323    let Some(t) = trgt else {
1324        return (None, None);
1325    };
1326
1327    let delta_in_sigmas = if err.is_zero() {
1328        0.0
1329    } else {
1330        (t - avg).abs().0 / err.0
1331    };
1332    let delta_in_percent = if t.abs().is_non_zero() {
1333        (t - avg).abs().0 / t.abs().0 * 100.
1334    } else {
1335        0.
1336    };
1337    let is_outside_target =
1338        delta_in_sigmas > 5. || (t.abs().is_non_zero() && ((t - avg).abs() / t.abs()).0 > 0.01);
1339    let sigma_text = format!("Δ = {:.3}σ", delta_in_sigmas);
1340    let percent_text = format!("Δ = {:.3}%", delta_in_percent);
1341    if is_outside_target {
1342        (
1343            Some(sigma_text.red().to_string()),
1344            Some(percent_text.red().to_string()),
1345        )
1346    } else {
1347        (
1348            Some(sigma_text.green().to_string()),
1349            Some(percent_text.green().to_string()),
1350        )
1351    }
1352}
1353
1354fn format_mwi_cell(itg: &StatisticsAccumulator<F<f64>>) -> String {
1355    let mwi_value = max_weight_impact(itg);
1356    let formatted = format!("{:.4e}", mwi_value.0);
1357    if mwi_value > F(1.) {
1358        formatted.red().to_string()
1359    } else {
1360        formatted
1361    }
1362}
1363
1364fn max_weight_impact(itg: &StatisticsAccumulator<F<f64>>) -> F<f64> {
1365    if itg.avg.abs().0 == 0. || itg.processed_samples == 0 {
1366        return F(0.0);
1367    }
1368
1369    itg.max_eval_negative.abs().max(itg.max_eval_positive.abs())
1370        / (itg.avg.abs() * F::<f64>::new_from_usize(itg.processed_samples))
1371}
1372
1373fn build_table_result_summary(
1374    slot_meta: &SlotMeta,
1375    accumulator: &ComplexAccumulator,
1376    iter: usize,
1377    target: Option<Complex<F<f64>>>,
1378) -> Vec<IntegrationTableComponentResult> {
1379    [
1380        ("re", &accumulator.re, target.as_ref().map(|value| value.re)),
1381        ("im", &accumulator.im, target.as_ref().map(|value| value.im)),
1382    ]
1383    .into_iter()
1384    .map(|(component, accumulator, target_component)| {
1385        let cells =
1386            build_integral_result_cells(accumulator, slot_meta, iter, component, target_component);
1387        IntegrationTableComponentResult {
1388            component: component.to_string(),
1389            value: accumulator.avg,
1390            error: accumulator.err,
1391            relative_error_percent: accumulator
1392                .avg
1393                .is_non_zero()
1394                .then(|| (accumulator.err / accumulator.avg).abs().0 * 100.0),
1395            chi_sq_per_dof: if iter > 0 {
1396                accumulator.chi_sq.0 / (iter as f64)
1397            } else {
1398                0.0
1399            },
1400            target_delta_sigma: cells.delta_sigma.as_ref().map(|_| {
1401                if accumulator.err.is_zero() {
1402                    0.0
1403                } else if let Some(target_value) = target_component {
1404                    (target_value - accumulator.avg).abs().0 / accumulator.err.0
1405                } else {
1406                    0.0
1407                }
1408            }),
1409            target_delta_percent: target_component.map(|target_value| {
1410                if target_value.is_zero() {
1411                    0.0
1412                } else {
1413                    (target_value - accumulator.avg).abs().0 / target_value.abs().0 * 100.0
1414                }
1415            }),
1416            max_weight_impact: max_weight_impact(accumulator).0,
1417        }
1418    })
1419    .collect()
1420}
1421
1422fn build_max_weight_info_summary(
1423    discrete_axis_labels: &[String],
1424    accumulator: &ComplexAccumulator,
1425) -> Vec<MaxWeightInfoEntry> {
1426    [
1427        (
1428            "re",
1429            "+",
1430            accumulator.re.max_eval_positive,
1431            accumulator.re.max_eval_positive_xs.as_ref(),
1432        ),
1433        (
1434            "re",
1435            "-",
1436            accumulator.re.max_eval_negative,
1437            accumulator.re.max_eval_negative_xs.as_ref(),
1438        ),
1439        (
1440            "im",
1441            "+",
1442            accumulator.im.max_eval_positive,
1443            accumulator.im.max_eval_positive_xs.as_ref(),
1444        ),
1445        (
1446            "im",
1447            "-",
1448            accumulator.im.max_eval_negative,
1449            accumulator.im.max_eval_negative_xs.as_ref(),
1450        ),
1451    ]
1452    .into_iter()
1453    .filter_map(|(component, sign, max_eval, sample)| {
1454        if max_eval.is_zero() {
1455            return None;
1456        }
1457
1458        Some(MaxWeightInfoEntry {
1459            component: component.to_string(),
1460            sign: sign.to_string(),
1461            max_eval,
1462            coordinates: sample
1463                .map(|sample| format_max_eval_sample(sample, discrete_axis_labels, &[])),
1464        })
1465    })
1466    .collect()
1467}
1468
1469#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1470struct LiveIterationProgress {
1471    completed_points: usize,
1472    target_points: usize,
1473}
1474
1475fn build_iteration_status_header_left(
1476    elapsed_time: Duration,
1477    iter: usize,
1478    live_progress: Option<LiveIterationProgress>,
1479) -> String {
1480    let iteration_label = if live_progress.is_some() {
1481        format!("Iteration #{:-4} ( running   )", iter)
1482            .bold()
1483            .green()
1484    } else {
1485        format!("Iteration #{:-4} ( completed )", iter)
1486            .bold()
1487            .green()
1488    };
1489
1490    format!(
1491        "[ {} ] {}",
1492        format!(
1493            "{:^7}",
1494            utils::format_wdhms(elapsed_time.as_secs() as usize)
1495        )
1496        .bold(),
1497        iteration_label,
1498    )
1499}
1500
1501fn build_iteration_status_header_middle(
1502    cur_points: usize,
1503    total_points: usize,
1504    live_progress: Option<LiveIterationProgress>,
1505) -> String {
1506    let per_iteration = if let Some(progress) = live_progress {
1507        let percentage = if progress.target_points == 0 {
1508            String::from("0.0%")
1509        } else {
1510            format!(
1511                "{:.1}%",
1512                (progress.completed_points as f64) / (progress.target_points as f64) * 100.0
1513            )
1514            .green()
1515            .to_string()
1516        };
1517        format!(
1518            "Iteration progress {}/{} {}",
1519            format_iteration_points(progress.completed_points),
1520            format_iteration_points(progress.target_points),
1521            percentage
1522        )
1523    } else if cur_points > 0 {
1524        format!(
1525            "# samples per iteration = {}",
1526            format_iteration_points(cur_points)
1527        )
1528        .blue()
1529        .bold()
1530        .to_string()
1531    } else {
1532        String::new()
1533    };
1534
1535    let total = format!("# samples total = {}", format_total_points(total_points))
1536        .bold()
1537        .green()
1538        .to_string();
1539    if per_iteration.is_empty() {
1540        total
1541    } else {
1542        format!("{per_iteration} {total}")
1543    }
1544}
1545
1546fn build_iteration_status_header_tail(
1547    cores: usize,
1548    elapsed_time: Duration,
1549    n_samples_evaluated: usize,
1550) -> String {
1551    let sample_core_time = if n_samples_evaluated == 0 {
1552        "N/A /sample/core".red().to_string()
1553    } else {
1554        format!(
1555            "{} /sample/core",
1556            utils::format_evaluation_time_from_f64(
1557                elapsed_time.as_secs_f64() / (n_samples_evaluated as f64) * (cores as f64),
1558            )
1559            .bold()
1560            .green()
1561        )
1562    };
1563
1564    format!(
1565        "{sample_core_time} {}",
1566        format!("({cores} cores)").bold().blue()
1567    )
1568}
1569
1570fn max_weight_row_descriptors(
1571    phase_display: IntegrationStatusPhaseDisplay,
1572) -> Vec<(ComponentKind, &'static str, bool)> {
1573    let mut rows = Vec::new();
1574    if phase_display.shows_real() {
1575        rows.push((ComponentKind::Real, "+", true));
1576        rows.push((ComponentKind::Real, "-", false));
1577    }
1578    if phase_display.shows_imag() {
1579        rows.push((ComponentKind::Imag, "+", true));
1580        rows.push((ComponentKind::Imag, "-", false));
1581    }
1582    rows
1583}
1584
1585type MaxEvalEntry<'a> = Option<(F<f64>, Option<&'a Sample<F<f64>>>)>;
1586
1587fn max_eval_entry(accumulator: &StatisticsAccumulator<F<f64>>, positive: bool) -> MaxEvalEntry<'_> {
1588    let (value, sample) = if positive {
1589        (
1590            accumulator.max_eval_positive,
1591            accumulator.max_eval_positive_xs.as_ref(),
1592        )
1593    } else {
1594        (
1595            accumulator.max_eval_negative,
1596            accumulator.max_eval_negative_xs.as_ref(),
1597        )
1598    };
1599
1600    if value.is_zero() {
1601        None
1602    } else {
1603        Some((value, sample))
1604    }
1605}
1606
1607/// struct to keep track of state, used in the havana_integrate function
1608/// the idea is to save this to disk after each iteration, so that the integration can be resumed
1609#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
1610pub struct IntegrationState {
1611    pub num_points: usize,
1612    #[bincode(with_serde)]
1613    pub all_integrals: Vec<ComplexAccumulator>,
1614    #[bincode(with_serde)]
1615    slot_re_summaries: Vec<Option<DiscreteGridAccumulatorSummary>>,
1616    #[bincode(with_serde)]
1617    slot_im_summaries: Vec<Option<DiscreteGridAccumulatorSummary>>,
1618    pub stats: StatisticsCounter,
1619    pub slot_stats: Vec<StatisticsCounter>,
1620    pub slot_metas: Vec<SlotMeta>,
1621    pub sampling_correlation_mode: SamplingCorrelationMode,
1622    sampling_states: Vec<SamplingSlotState>,
1623    monitored_discrete_path: Option<Vec<usize>>,
1624    pub first_non_trivial_discrete_label: Option<String>,
1625    pub first_non_trivial_discrete_bin_descriptions: Option<Vec<String>>,
1626    slot_first_non_trivial_discrete_breakdown_metadata:
1627        Vec<Option<PersistedDiscreteBreakdownMetadata>>,
1628    pub iter: usize,
1629    pub elapsed_seconds: f64,
1630    pub n_cores: usize,
1631}
1632
1633impl IntegrationState {
1634    fn new_from_settings(
1635        sampling_correlation_mode: SamplingCorrelationMode,
1636        sampling_states: Vec<SamplingSlotState>,
1637        slot_metas: Vec<SlotMeta>,
1638        monitored_discrete_path: Option<Vec<usize>>,
1639        first_non_trivial_discrete_label: Option<String>,
1640        first_non_trivial_discrete_bin_descriptions: Option<Vec<String>>,
1641        slot_first_non_trivial_discrete_breakdown_metadata: Vec<
1642            Option<PersistedDiscreteBreakdownMetadata>,
1643        >,
1644    ) -> Self {
1645        let num_points = 0;
1646        let iter = 0;
1647        let all_integrals = vec![ComplexAccumulator::new(); slot_metas.len()];
1648        let slot_re_summaries = (0..slot_metas.len())
1649            .map(|slot_index| {
1650                DiscreteGridAccumulatorSummary::from_grid(
1651                    &sampling_states[sampling_correlation_mode.state_index(slot_index)].grid,
1652                )
1653            })
1654            .collect();
1655        let slot_im_summaries = (0..slot_metas.len())
1656            .map(|slot_index| {
1657                DiscreteGridAccumulatorSummary::from_grid(
1658                    &sampling_states[sampling_correlation_mode.state_index(slot_index)].grid,
1659                )
1660            })
1661            .collect();
1662        let stats = StatisticsCounter::new_empty();
1663        let slot_stats = vec![StatisticsCounter::new_empty(); slot_metas.len()];
1664
1665        Self {
1666            num_points,
1667            all_integrals,
1668            slot_re_summaries,
1669            slot_im_summaries,
1670            stats,
1671            slot_stats,
1672            slot_metas,
1673            sampling_correlation_mode,
1674            sampling_states,
1675            monitored_discrete_path,
1676            first_non_trivial_discrete_label,
1677            first_non_trivial_discrete_bin_descriptions,
1678            slot_first_non_trivial_discrete_breakdown_metadata,
1679            iter,
1680            elapsed_seconds: 0.0,
1681            n_cores: 1,
1682        }
1683    }
1684
1685    fn update_iter(&mut self, use_weighted_average: bool) {
1686        self.all_integrals
1687            .iter_mut()
1688            .for_each(|acc| acc.update_iter(use_weighted_average));
1689    }
1690
1691    fn sampling_state_for_slot(&self, slot_index: usize) -> &SamplingSlotState {
1692        &self.sampling_states[self.sampling_correlation_mode.state_index(slot_index)]
1693    }
1694
1695    fn sampling_state_for_slot_mut(&mut self, slot_index: usize) -> &mut SamplingSlotState {
1696        let sampling_state_index = self.sampling_correlation_mode.state_index(slot_index);
1697        &mut self.sampling_states[sampling_state_index]
1698    }
1699
1700    fn monitored_discrete_context_for_slot(
1701        &self,
1702        slot_index: usize,
1703    ) -> Option<DiscreteLevelContext> {
1704        let path = self.monitored_discrete_path.clone()?;
1705        let discrete_grid =
1706            discrete_grid_at_path(&self.sampling_state_for_slot(slot_index).grid, &path)?;
1707        Some(DiscreteLevelContext {
1708            path,
1709            pdfs: discrete_grid.bins.iter().map(|bin| bin.pdf).collect(),
1710        })
1711    }
1712
1713    fn monitored_discrete_context(&self) -> Option<DiscreteLevelContext> {
1714        self.monitored_discrete_context_for_slot(0)
1715    }
1716}
1717
1718struct CoreIterationState {
1719    slot_integrands: Vec<Integrand>,
1720    stats: StatisticsCounter,
1721    slot_stats: Vec<StatisticsCounter>,
1722    integrals: Vec<ComplexAccumulator>,
1723    sampling_correlation_mode: SamplingCorrelationMode,
1724    sampling_states: Vec<CoreSamplingSlotState>,
1725    slot_re_grids: Vec<Grid<F<f64>>>,
1726    slot_im_grids: Vec<Grid<F<f64>>>,
1727    remaining_points: usize,
1728    completed_points: usize,
1729}
1730
1731struct CoreSamplingSlotState {
1732    sampling_grid: Grid<F<f64>>,
1733    rng: MonteCarloRng,
1734}
1735
1736impl CoreIterationState {
1737    fn new(
1738        slot_integrands: Vec<Integrand>,
1739        sampling_correlation_mode: SamplingCorrelationMode,
1740        sampling_grid_templates: &[Grid<F<f64>>],
1741        seed: u64,
1742        sample_skip: usize,
1743        remaining_points: usize,
1744    ) -> Self {
1745        let n_slots = slot_integrands.len();
1746        let sampling_states = (0..sampling_correlation_mode.state_count(n_slots))
1747            .map(|sampling_state_index| {
1748                let slot_index = match sampling_correlation_mode {
1749                    SamplingCorrelationMode::Correlated => 0,
1750                    SamplingCorrelationMode::Uncorrelated => sampling_state_index,
1751                };
1752                let mut rng = MonteCarloRng::new(slot_seed(seed, slot_index), 0);
1753                let mut sampling_grid =
1754                    sampling_grid_templates[sampling_state_index].clone_without_samples();
1755
1756                for _ in 0..sample_skip {
1757                    let mut sample = Sample::new();
1758                    sampling_grid.sample(&mut rng, &mut sample);
1759                }
1760
1761                CoreSamplingSlotState { sampling_grid, rng }
1762            })
1763            .collect_vec();
1764
1765        Self {
1766            slot_integrands,
1767            stats: StatisticsCounter::new_empty(),
1768            slot_stats: vec![StatisticsCounter::new_empty(); n_slots],
1769            integrals: vec![ComplexAccumulator::new(); n_slots],
1770            sampling_correlation_mode,
1771            sampling_states,
1772            slot_re_grids: (0..n_slots)
1773                .map(|slot_index| {
1774                    sampling_grid_templates[sampling_correlation_mode.state_index(slot_index)]
1775                        .clone_without_samples()
1776                })
1777                .collect(),
1778            slot_im_grids: (0..n_slots)
1779                .map(|slot_index| {
1780                    sampling_grid_templates[sampling_correlation_mode.state_index(slot_index)]
1781                        .clone_without_samples()
1782                })
1783                .collect(),
1784            remaining_points,
1785            completed_points: 0,
1786        }
1787    }
1788
1789    fn evaluate_chunk(
1790        &mut self,
1791        slot_settings: &[&RuntimeSettings],
1792        slot_models: &[&Model],
1793        iter: usize,
1794        current_max_evals: &[Complex<F<f64>>],
1795        chunk_size: usize,
1796    ) -> Result<usize> {
1797        let n_points = chunk_size.min(self.remaining_points);
1798        if n_points == 0 {
1799            return Ok(0);
1800        }
1801
1802        let chunk_start = Instant::now();
1803        let mut batch_evaluation_time = Duration::ZERO;
1804        let mut batch_stats = StatisticsCounter::new_empty();
1805        let mut processed_points = 0;
1806        let mut total_sample_evaluations = 0;
1807
1808        match self.sampling_correlation_mode {
1809            SamplingCorrelationMode::Correlated => {
1810                let mut samples = Vec::with_capacity(n_points);
1811                for _ in 0..n_points {
1812                    if is_interrupted() {
1813                        break;
1814                    }
1815
1816                    let mut sample = Sample::new();
1817                    let sampling_state = &mut self.sampling_states[0];
1818                    sampling_state
1819                        .sampling_grid
1820                        .sample(&mut sampling_state.rng, &mut sample);
1821                    processed_points += 1;
1822                    samples.push(sample);
1823                }
1824
1825                if processed_points == 0 {
1826                    return Ok(0);
1827                }
1828
1829                let mut slot_results = Vec::with_capacity(self.slot_integrands.len());
1830
1831                for (slot_index, integrand) in self.slot_integrands.iter_mut().enumerate() {
1832                    let evaluation_start = Instant::now();
1833                    let raw_batch = integrand.evaluate_samples_raw(
1834                        &samples,
1835                        slot_models[slot_index],
1836                        iter,
1837                        false,
1838                        true,
1839                        current_max_evals[slot_index],
1840                    )?;
1841                    if raw_batch.samples.len() < samples.len() {
1842                        return Ok(0);
1843                    }
1844                    batch_evaluation_time += evaluation_start.elapsed();
1845                    total_sample_evaluations += raw_batch.samples.len();
1846                    batch_stats = batch_stats.merged(&raw_batch.statistics);
1847                    self.slot_stats[slot_index] =
1848                        self.slot_stats[slot_index].merged(&raw_batch.statistics);
1849                    slot_results.push(raw_batch.samples);
1850                }
1851
1852                for (sample_index, sample) in samples.iter().enumerate() {
1853                    for (slot_index, (((core_accumulator, re_grid), im_grid), results)) in self
1854                        .integrals
1855                        .iter_mut()
1856                        .zip(self.slot_re_grids.iter_mut())
1857                        .zip(self.slot_im_grids.iter_mut())
1858                        .zip(slot_results.iter())
1859                        .enumerate()
1860                    {
1861                        let result = &results[sample_index];
1862                        let jacobian = result.parameterization_jacobian.unwrap_or(F(1.0));
1863                        let effective_integrand_result =
1864                            result.integrand_result * Complex::new_re(jacobian);
1865
1866                        core_accumulator.add_sample(
1867                            effective_integrand_result,
1868                            sample.get_weight(),
1869                            Some(sample),
1870                        );
1871                        re_grid
1872                            .add_training_sample(sample, effective_integrand_result.re)
1873                            .map_err(Report::msg)?;
1874                        im_grid
1875                            .add_training_sample(sample, effective_integrand_result.im)
1876                            .map_err(Report::msg)?;
1877
1878                        if slot_index == 0 {
1879                            let training_eval = match slot_settings[0].integrator.integrated_phase {
1880                                IntegratedPhase::Real => effective_integrand_result.re,
1881                                IntegratedPhase::Imag => effective_integrand_result.im,
1882                                IntegratedPhase::Both => unimplemented!(),
1883                            };
1884
1885                            self.sampling_states[0]
1886                                .sampling_grid
1887                                .add_training_sample(sample, training_eval)
1888                                .map_err(Report::msg)?;
1889                        }
1890                    }
1891                }
1892            }
1893            SamplingCorrelationMode::Uncorrelated => {
1894                for slot_index in 0..self.slot_integrands.len() {
1895                    let mut samples = Vec::with_capacity(n_points);
1896                    for _ in 0..n_points {
1897                        if is_interrupted() {
1898                            break;
1899                        }
1900
1901                        let mut sample = Sample::new();
1902                        let sampling_state = &mut self.sampling_states[slot_index];
1903                        sampling_state
1904                            .sampling_grid
1905                            .sample(&mut sampling_state.rng, &mut sample);
1906                        samples.push(sample);
1907                    }
1908
1909                    if slot_index == 0 {
1910                        processed_points = samples.len();
1911                    } else if samples.len() != processed_points {
1912                        return Err(Report::msg(
1913                            "uncorrelated slot batch sizes diverged unexpectedly",
1914                        ));
1915                    }
1916
1917                    if samples.is_empty() {
1918                        continue;
1919                    }
1920
1921                    let evaluation_start = Instant::now();
1922                    let raw_batch = self.slot_integrands[slot_index].evaluate_samples_raw(
1923                        &samples,
1924                        slot_models[slot_index],
1925                        iter,
1926                        false,
1927                        true,
1928                        current_max_evals[slot_index],
1929                    )?;
1930                    if raw_batch.samples.len() < samples.len() {
1931                        return Ok(0);
1932                    }
1933                    batch_evaluation_time += evaluation_start.elapsed();
1934                    total_sample_evaluations += raw_batch.samples.len();
1935                    batch_stats = batch_stats.merged(&raw_batch.statistics);
1936                    self.slot_stats[slot_index] =
1937                        self.slot_stats[slot_index].merged(&raw_batch.statistics);
1938
1939                    for (sample, result) in samples.iter().zip(raw_batch.samples.iter()) {
1940                        let jacobian = result.parameterization_jacobian.unwrap_or(F(1.0));
1941                        let effective_integrand_result =
1942                            result.integrand_result * Complex::new_re(jacobian);
1943
1944                        self.integrals[slot_index].add_sample(
1945                            effective_integrand_result,
1946                            sample.get_weight(),
1947                            Some(sample),
1948                        );
1949                        self.slot_re_grids[slot_index]
1950                            .add_training_sample(sample, effective_integrand_result.re)
1951                            .map_err(Report::msg)?;
1952                        self.slot_im_grids[slot_index]
1953                            .add_training_sample(sample, effective_integrand_result.im)
1954                            .map_err(Report::msg)?;
1955
1956                        let training_eval =
1957                            match slot_settings[slot_index].integrator.integrated_phase {
1958                                IntegratedPhase::Real => effective_integrand_result.re,
1959                                IntegratedPhase::Imag => effective_integrand_result.im,
1960                                IntegratedPhase::Both => unimplemented!(),
1961                            };
1962                        self.sampling_states[slot_index]
1963                            .sampling_grid
1964                            .add_training_sample(sample, training_eval)
1965                            .map_err(Report::msg)?;
1966                    }
1967                }
1968
1969                if processed_points == 0 {
1970                    return Ok(0);
1971                }
1972            }
1973        }
1974
1975        self.remaining_points -= processed_points;
1976        self.completed_points += processed_points;
1977        self.stats = self.stats.merged(&batch_stats);
1978        self.stats.add_integrator_overhead(
1979            chunk_start.elapsed().saturating_sub(batch_evaluation_time),
1980            total_sample_evaluations,
1981        );
1982
1983        Ok(processed_points)
1984    }
1985}
1986
1987fn initial_batch_size(batching: IterationBatchingSettings) -> usize {
1988    batching.batch_size.unwrap_or(100).max(1)
1989}
1990
1991fn next_batch_size(
1992    batching: IterationBatchingSettings,
1993    current_batch_size: usize,
1994    round_elapsed: Duration,
1995) -> usize {
1996    if let Some(batch_size) = batching.batch_size {
1997        return batch_size.max(1);
1998    }
1999
2000    let round_seconds = round_elapsed.as_secs_f64();
2001    if round_seconds <= 0.0 {
2002        return current_batch_size.max(1);
2003    }
2004
2005    let estimated = ((current_batch_size as f64) * batching.batch_timing_seconds / round_seconds)
2006        .round() as usize;
2007    estimated.max(1)
2008}
2009
2010fn total_completed_points(core_states: &[CoreIterationState]) -> usize {
2011    core_states.iter().map(|state| state.completed_points).sum()
2012}
2013
2014fn total_remaining_points(core_states: &[CoreIterationState]) -> usize {
2015    core_states.iter().map(|state| state.remaining_points).sum()
2016}
2017
2018fn slot_seed(global_seed: u64, slot_index: usize) -> u64 {
2019    let slot = (slot_index as u64).wrapping_add(1);
2020    let mut mixed = global_seed.wrapping_add(0x9e37_79b9_7f4a_7c15u64.wrapping_mul(slot));
2021    mixed ^= mixed >> 30;
2022    mixed = mixed.wrapping_mul(0xbf58_476d_1ce4_e5b9);
2023    mixed ^= mixed >> 27;
2024    mixed = mixed.wrapping_mul(0x94d0_49bb_1331_11eb);
2025    mixed ^ (mixed >> 31)
2026}
2027
2028fn apply_iteration_core_states(
2029    integration_state: &mut IntegrationState,
2030    slots: &[IntegrationSlot],
2031    cores: usize,
2032    cur_points: usize,
2033    elapsed_seconds: f64,
2034    core_states: &[CoreIterationState],
2035) {
2036    let n_slots = integration_state.slot_metas.len();
2037
2038    for core_state in core_states {
2039        integration_state.stats = integration_state.stats.merged(&core_state.stats);
2040        for (integral, core_integral) in integration_state
2041            .all_integrals
2042            .iter_mut()
2043            .zip(core_state.integrals.iter())
2044        {
2045            integral.merge(core_integral);
2046        }
2047        for (slot_stats, core_slot_stats) in integration_state
2048            .slot_stats
2049            .iter_mut()
2050            .zip(core_state.slot_stats.iter())
2051        {
2052            *slot_stats = slot_stats.merged(core_slot_stats);
2053        }
2054    }
2055
2056    let mut merged_sampling_grids = integration_state
2057        .sampling_states
2058        .iter()
2059        .map(|sampling_state| sampling_state.grid.clone_without_samples())
2060        .collect_vec();
2061    let mut merged_re_grids = (0..n_slots)
2062        .map(|slot_index| {
2063            integration_state
2064                .sampling_state_for_slot(slot_index)
2065                .grid
2066                .clone_without_samples()
2067        })
2068        .collect_vec();
2069    let mut merged_im_grids = (0..n_slots)
2070        .map(|slot_index| {
2071            integration_state
2072                .sampling_state_for_slot(slot_index)
2073                .grid
2074                .clone_without_samples()
2075        })
2076        .collect_vec();
2077
2078    for core_state in core_states {
2079        for (sampling_state, merged_grid) in core_state
2080            .sampling_states
2081            .iter()
2082            .zip(merged_sampling_grids.iter_mut())
2083        {
2084            merged_grid
2085                .merge(&sampling_state.sampling_grid)
2086                .expect("could not merge grids");
2087        }
2088        for slot_index in 0..n_slots {
2089            merged_re_grids[slot_index]
2090                .merge(&core_state.slot_re_grids[slot_index])
2091                .expect("could not merge real accumulation grids");
2092            merged_im_grids[slot_index]
2093                .merge(&core_state.slot_im_grids[slot_index])
2094                .expect("could not merge imaginary accumulation grids");
2095        }
2096    }
2097
2098    for (summary, grid) in integration_state
2099        .slot_re_summaries
2100        .iter_mut()
2101        .zip(merged_re_grids.iter())
2102    {
2103        if let Some(summary) = summary.as_mut() {
2104            summary.merge_iteration_grid(grid);
2105            summary.update_iter();
2106        }
2107    }
2108    for (summary, grid) in integration_state
2109        .slot_im_summaries
2110        .iter_mut()
2111        .zip(merged_im_grids.iter())
2112    {
2113        if let Some(summary) = summary.as_mut() {
2114            summary.merge_iteration_grid(grid);
2115            summary.update_iter();
2116        }
2117    }
2118
2119    for (slot_index, merged_grid) in merged_sampling_grids.into_iter().enumerate() {
2120        let actual_slot_index = match integration_state.sampling_correlation_mode {
2121            SamplingCorrelationMode::Correlated => 0,
2122            SamplingCorrelationMode::Uncorrelated => slot_index,
2123        };
2124        let discrete_axis_labels = integration_state
2125            .sampling_state_for_slot(actual_slot_index)
2126            .discrete_axis_labels
2127            .clone();
2128        let sampling_state = integration_state.sampling_state_for_slot_mut(actual_slot_index);
2129        *sampling_state = SamplingSlotState {
2130            grid: merged_grid,
2131            discrete_axis_labels,
2132        };
2133        sampling_state.grid.update(
2134            F(slots[actual_slot_index]
2135                .settings
2136                .integrator
2137                .discrete_dim_learning_rate),
2138            F(slots[actual_slot_index]
2139                .settings
2140                .integrator
2141                .continuous_dim_learning_rate),
2142        );
2143    }
2144
2145    integration_state.update_iter(false);
2146    integration_state.iter += 1;
2147    integration_state.elapsed_seconds = elapsed_seconds;
2148    integration_state.n_cores = cores;
2149    integration_state.num_points += cur_points;
2150}
2151
2152fn build_preview_integration_state(
2153    integration_state: &IntegrationState,
2154    slots: &[IntegrationSlot],
2155    cores: usize,
2156    completed_points: usize,
2157    elapsed_seconds: f64,
2158    core_states: &[CoreIterationState],
2159) -> IntegrationState {
2160    let mut preview = integration_state.clone();
2161    apply_iteration_core_states(
2162        &mut preview,
2163        slots,
2164        cores,
2165        completed_points,
2166        elapsed_seconds,
2167        core_states,
2168    );
2169    preview
2170}
2171
2172/// Integrate function used for local runs
2173pub fn havana_integrate<S>(
2174    request: HavanaIntegrateRequest,
2175    mut status_emitter: S,
2176) -> Result<IntegrationResult>
2177where
2178    S: FnMut(StatusUpdate) -> Result<()>,
2179{
2180    let HavanaIntegrateRequest {
2181        mut slots,
2182        sampling_correlation_mode,
2183        n_cores,
2184        state,
2185        workspace,
2186        output_control,
2187        batching,
2188        view_options,
2189    } = request;
2190
2191    if slots.is_empty() {
2192        return Err(Report::msg(
2193            "At least one integrand must be selected for integration",
2194        ));
2195    }
2196
2197    let slot_metas = slots.iter().map(|slot| slot.meta.clone()).collect_vec();
2198    let targets = slots.iter().map(|slot| slot.target).collect_vec();
2199    let sampling_states = match sampling_correlation_mode {
2200        SamplingCorrelationMode::Correlated => {
2201            vec![SamplingSlotState::from_integrand(&slots[0].integrand)]
2202        }
2203        SamplingCorrelationMode::Uncorrelated => slots
2204            .iter()
2205            .map(|slot| SamplingSlotState::from_integrand(&slot.integrand))
2206            .collect_vec(),
2207    };
2208    let monitored_discrete_setup =
2209        resolve_monitored_discrete_setup(sampling_correlation_mode, &slots, &sampling_states);
2210    let slot_first_non_trivial_discrete_breakdown_metadata =
2211        if let Some(path) = monitored_discrete_setup.path.as_deref() {
2212            slots
2213                .iter()
2214                .enumerate()
2215                .map(|(slot_index, slot)| {
2216                    build_persisted_discrete_breakdown_metadata(
2217                        &slot.integrand,
2218                        path,
2219                        &sampling_states[sampling_correlation_mode.state_index(slot_index)]
2220                            .discrete_axis_labels,
2221                    )
2222                })
2223                .collect_vec()
2224        } else {
2225            vec![None; slot_metas.len()]
2226        };
2227    if let Some(label_warning) = monitored_discrete_setup.warning.as_ref() {
2228        warn!("{label_warning}");
2229    }
2230
2231    let mut integration_state = if let Some(integration_state) = state {
2232        integration_state
2233    } else {
2234        IntegrationState::new_from_settings(
2235            sampling_correlation_mode,
2236            sampling_states.clone(),
2237            slot_metas.clone(),
2238            monitored_discrete_setup.path.clone(),
2239            monitored_discrete_setup.axis_label.clone(),
2240            monitored_discrete_setup.bin_descriptions.clone(),
2241            slot_first_non_trivial_discrete_breakdown_metadata,
2242        )
2243    };
2244    integration_state.monitored_discrete_path = monitored_discrete_setup.path;
2245    integration_state.first_non_trivial_discrete_label = monitored_discrete_setup.axis_label;
2246    integration_state.first_non_trivial_discrete_bin_descriptions =
2247        monitored_discrete_setup.bin_descriptions;
2248    if integration_state.slot_metas != slot_metas {
2249        return Err(Report::msg(
2250            "Saved integration state slots do not match the currently selected integrands",
2251        ));
2252    }
2253    if integration_state.sampling_correlation_mode != sampling_correlation_mode {
2254        return Err(Report::msg(
2255            "Saved integration state sampling mode does not match the current integration mode",
2256        ));
2257    }
2258    if integration_state.sampling_states.len()
2259        != sampling_correlation_mode.state_count(slot_metas.len())
2260    {
2261        return Err(Report::msg(
2262            "Saved integration state sampling metadata is inconsistent with the selected slots",
2263        ));
2264    }
2265    if integration_state
2266        .slot_first_non_trivial_discrete_breakdown_metadata
2267        .len()
2268        != slot_metas.len()
2269    {
2270        return Err(Report::msg(
2271            "Saved integration state discrete breakdown metadata is inconsistent with the selected slots",
2272        ));
2273    }
2274
2275    let primary = &slots[0];
2276    let sampling_str = primary.settings.sampling.describe_settings();
2277    let dimension = primary.integrand.get_n_dim();
2278    let discrete_depth = primary.settings.sampling.discrete_depth();
2279    let is_tropical_sampling = primary
2280        .settings
2281        .sampling
2282        .get_parameterization_settings()
2283        .is_none();
2284    let use_ltd = primary.settings.general.use_ltd;
2285    let integration_seed = primary.settings.integrator.seed;
2286    let integrated_phase = primary.settings.integrator.integrated_phase;
2287    let target_relative_accuracy = primary.settings.integrator.target_relative_accuracy;
2288    let target_absolute_accuracy = primary.settings.integrator.target_absolute_accuracy;
2289    let n_start = primary.settings.integrator.n_start;
2290    let n_increase = primary.settings.integrator.n_increase;
2291    let n_max = primary.settings.integrator.n_max;
2292
2293    let cont_dim_str = if is_tropical_sampling {
2294        format!("a median continious dimension of {}", dimension)
2295    } else {
2296        format!("{} continuous dimensions", dimension)
2297    };
2298
2299    let graph_string = if discrete_depth > 0 {
2300        let num_graphs = match &integration_state.sampling_state_for_slot(0).grid {
2301            Grid::Discrete(g) => g.bins.len(),
2302            _ => unreachable!(),
2303        };
2304        format!(
2305            "{discrete_depth} nested discrete grids with {} {} and ",
2306            num_graphs,
2307            if num_graphs > 1 { "graphs" } else { "graph" }
2308        )
2309    } else {
2310        String::new()
2311    };
2312
2313    let correlation_str = match sampling_correlation_mode {
2314        SamplingCorrelationMode::Correlated => "correlated",
2315        SamplingCorrelationMode::Uncorrelated => "uncorrelated",
2316    };
2317    let grid_str = format!("{graph_string}{cont_dim_str} using {sampling_str} ({correlation_str})");
2318
2319    let cores = n_cores.max(1);
2320    let n_slots = integration_state.slot_metas.len();
2321    let elapsed_seconds_offset = integration_state.elapsed_seconds;
2322
2323    let t_start = Instant::now();
2324
2325    info!(
2326        "Integrating using {} ltd with {} {} over {} ...",
2327        if use_ltd { "naive" } else { "cff" },
2328        cores,
2329        if cores > 1 { "cores" } else { "core" },
2330        grid_str
2331    );
2332    info!("");
2333
2334    let pool = ThreadPoolBuilder::new().num_threads(cores).build().unwrap();
2335
2336    let mut n_samples_evaluated = 0;
2337    let mut emitted_latest_observable_paths = vec![Vec::new(); n_slots];
2338    clear_iteration_abort_request();
2339    'integrateLoop: while integration_state.num_points < n_max {
2340        // ensure we do not overshoot
2341        let cur_points = {
2342            let cur_points_not_final_iter = n_start + n_increase * integration_state.iter;
2343            if cur_points_not_final_iter + integration_state.num_points > n_max {
2344                n_max - integration_state.num_points
2345            } else {
2346                cur_points_not_final_iter
2347            }
2348        };
2349
2350        // the number of points per core is the same for all cores, except for the last one
2351        let target_points_per_core = (cur_points - 1) / cores + 1;
2352        let n_points_per_core = (0..cores)
2353            .map(|core_id| {
2354                if core_id + 1 == cores {
2355                    cur_points - target_points_per_core * (cores - 1)
2356                } else {
2357                    target_points_per_core
2358                }
2359            })
2360            .collect_vec();
2361
2362        let iteration_start = Instant::now();
2363        let mut current_batch_size = initial_batch_size(batching);
2364        let mut last_live_status = None;
2365        if batching.emit_initial_status_update && !is_interrupted() {
2366            status_emitter(build_status_update(StatusUpdateBuildRequest {
2367                kind: IntegrationStatusKind::Live,
2368                integration_state: &integration_state,
2369                cores,
2370                elapsed_time: t_start.elapsed(),
2371                iteration_elapsed_time: iteration_start.elapsed(),
2372                cur_points: 0,
2373                total_points_display: integration_state.num_points,
2374                n_samples_evaluated,
2375                targets: &targets,
2376                render_options: &view_options,
2377                live_progress: Some(status_update::LiveIterationProgress {
2378                    completed_points: 0,
2379                    target_points: cur_points,
2380                }),
2381            }))?;
2382        }
2383
2384        let current_max_evals = integration_state
2385            .all_integrals
2386            .iter()
2387            .map(ComplexAccumulator::get_worst_case)
2388            .collect_vec();
2389
2390        let mut worker_states = n_points_per_core
2391            .iter()
2392            .enumerate()
2393            .map(|(core_id, &n_points)| {
2394                CoreIterationState::new(
2395                    slots.iter().map(|slot| slot.integrand.clone()).collect(),
2396                    integration_state.sampling_correlation_mode,
2397                    &integration_state
2398                        .sampling_states
2399                        .iter()
2400                        .map(|sampling_state| sampling_state.grid.clone())
2401                        .collect_vec(),
2402                    integration_seed + integration_state.iter as u64,
2403                    target_points_per_core * core_id,
2404                    n_points,
2405                )
2406            })
2407            .collect_vec();
2408        while total_remaining_points(&worker_states) > 0 {
2409            let round_started_at = Instant::now();
2410            let processed_per_core: Vec<Result<usize>> = {
2411                let slot_settings = slots.iter().map(|slot| &slot.settings).collect_vec();
2412                let slot_models = slots.iter().map(|slot| &slot.model).collect_vec();
2413                pool.install(|| {
2414                    worker_states
2415                        .par_iter_mut()
2416                        .map(|worker_state| {
2417                            worker_state.evaluate_chunk(
2418                                &slot_settings,
2419                                &slot_models,
2420                                integration_state.iter,
2421                                &current_max_evals,
2422                                current_batch_size,
2423                            )
2424                        })
2425                        .collect()
2426                })
2427            };
2428            let processed_this_round = processed_per_core
2429                .into_iter()
2430                .collect::<Result<Vec<_>>>()?
2431                .into_iter()
2432                .sum::<usize>();
2433
2434            if processed_this_round == 0 {
2435                break;
2436            }
2437
2438            let completed_points = total_completed_points(&worker_states);
2439            if batching.emit_live_status_updates
2440                && completed_points < cur_points
2441                && !is_interrupted()
2442                && !is_iteration_abort_requested()
2443            {
2444                let now = Instant::now();
2445                let should_emit_live_status = last_live_status.is_none_or(|previous| {
2446                    now.duration_since(previous).as_secs_f64()
2447                        >= batching.min_time_between_status_updates_seconds
2448                });
2449                if should_emit_live_status {
2450                    let preview_state = build_preview_integration_state(
2451                        &integration_state,
2452                        &slots,
2453                        cores,
2454                        completed_points,
2455                        elapsed_seconds_offset + t_start.elapsed().as_secs_f64(),
2456                        &worker_states,
2457                    );
2458                    status_emitter(build_status_update(StatusUpdateBuildRequest {
2459                        kind: IntegrationStatusKind::Live,
2460                        integration_state: &preview_state,
2461                        cores,
2462                        elapsed_time: t_start.elapsed(),
2463                        iteration_elapsed_time: iteration_start.elapsed(),
2464                        cur_points: completed_points,
2465                        total_points_display: integration_state.num_points + completed_points,
2466                        n_samples_evaluated: n_samples_evaluated + completed_points,
2467                        targets: &targets,
2468                        render_options: &view_options,
2469                        live_progress: Some(status_update::LiveIterationProgress {
2470                            completed_points,
2471                            target_points: cur_points,
2472                        }),
2473                    }))?;
2474                    last_live_status = Some(now);
2475                }
2476            }
2477
2478            if is_interrupted() {
2479                warn!("{}", "Integration interrupted by user".yellow());
2480                break 'integrateLoop;
2481            }
2482
2483            if is_iteration_abort_requested() {
2484                break;
2485            }
2486
2487            if total_remaining_points(&worker_states) > 0 {
2488                current_batch_size =
2489                    next_batch_size(batching, current_batch_size, round_started_at.elapsed());
2490            }
2491        }
2492
2493        let abort_current_iteration = is_iteration_abort_requested();
2494        clear_iteration_abort_request();
2495
2496        if is_interrupted() {
2497            warn!("{}", "Integration interrupted by user".yellow());
2498            break 'integrateLoop;
2499        }
2500
2501        let completed_points = total_completed_points(&worker_states);
2502        if abort_current_iteration && completed_points < cur_points {
2503            if completed_points == 0 {
2504                warn!(
2505                    "{}",
2506                    "Current iteration abort requested before any samples completed; restarting the iteration."
2507                        .yellow()
2508                );
2509                continue;
2510            }
2511            warn!(
2512                "{}",
2513                format!(
2514                    "Current iteration aborted by user after {} completed samples.",
2515                    completed_points
2516                )
2517                .yellow()
2518            );
2519            continue;
2520        }
2521
2522        n_samples_evaluated += completed_points;
2523        apply_iteration_core_states(
2524            &mut integration_state,
2525            &slots,
2526            cores,
2527            completed_points,
2528            elapsed_seconds_offset + t_start.elapsed().as_secs_f64(),
2529            &worker_states,
2530        );
2531
2532        // merge runtime state per slot into the owner core
2533        let (owner_core_slice, other_cores) = worker_states.split_at_mut(1);
2534        let owner_core = &mut owner_core_slice[0].slot_integrands;
2535        for other_core in other_cores {
2536            for (owner_slot, other_slot) in owner_core
2537                .iter_mut()
2538                .zip(other_core.slot_integrands.iter_mut())
2539            {
2540                owner_slot.merge_runtime_results(other_slot)?;
2541            }
2542        }
2543        for (slot, integrand) in slots.iter_mut().zip(owner_core.iter()) {
2544            slot.integrand = integrand.clone();
2545        }
2546
2547        // Update observable accumulators, persist authoritative resume state, then refresh
2548        // the latest user-facing snapshots.
2549        for slot in &mut slots {
2550            slot.integrand
2551                .update_runtime_results(integration_state.iter);
2552        }
2553
2554        if let Some(ref workspace_path) = workspace {
2555            for (slot_index, slot) in slots.iter().enumerate() {
2556                write_observable_resume_state(
2557                    &slot.integrand,
2558                    Some(workspace_path.as_path()),
2559                    &integration_state.slot_metas[slot_index],
2560                    integration_state.iter,
2561                    output_control,
2562                )?;
2563            }
2564            write_integration_state_to_workspace(workspace_path, &integration_state)?;
2565            write_integration_result_snapshots(
2566                workspace_path,
2567                &integration_state,
2568                &targets,
2569                output_control,
2570            )?;
2571        }
2572
2573        for (slot_index, slot) in slots.iter().enumerate() {
2574            let workspace_path = workspace
2575                .as_deref()
2576                .map(|root| slot_workspace_path(root, &integration_state.slot_metas[slot_index]));
2577            emitted_latest_observable_paths[slot_index] =
2578                write_latest_observables_output(&slot.integrand, workspace_path.as_deref())?;
2579            write_observable_snapshot_archive(
2580                &slot.integrand,
2581                workspace_path.as_deref(),
2582                integration_state.iter,
2583                output_control,
2584            )?;
2585        }
2586
2587        status_emitter(build_status_update(StatusUpdateBuildRequest {
2588            kind: IntegrationStatusKind::Iteration,
2589            integration_state: &integration_state,
2590            cores,
2591            elapsed_time: t_start.elapsed(),
2592            iteration_elapsed_time: iteration_start.elapsed(),
2593            cur_points: completed_points,
2594            total_points_display: integration_state.num_points,
2595            n_samples_evaluated,
2596            targets: &targets,
2597            render_options: &view_options,
2598            live_progress: None,
2599        }))?;
2600
2601        let target_accuracy_status = evaluate_target_accuracy(
2602            &integration_state,
2603            integration_state.num_points,
2604            t_start.elapsed(),
2605            &targets,
2606            match integrated_phase {
2607                IntegratedPhase::Real | IntegratedPhase::Both => {
2608                    IntegrationStatusPhaseDisplay::Real
2609                }
2610                IntegratedPhase::Imag => IntegrationStatusPhaseDisplay::Imag,
2611            },
2612            target_relative_accuracy,
2613            target_absolute_accuracy,
2614        );
2615        if target_accuracy_status.is_reached() {
2616            let reached_target = match (
2617                target_accuracy_status.relative_reached,
2618                target_accuracy_status.absolute_reached,
2619            ) {
2620                (true, true) => "relative and absolute",
2621                (true, false) => "relative",
2622                (false, true) => "absolute",
2623                (false, false) => unreachable!(),
2624            };
2625            info!(
2626                "Stopping integration after reaching the configured {reached_target} accuracy target."
2627            );
2628            break;
2629        }
2630    }
2631    // Reset the interrupted flag
2632    set_interrupted(false);
2633    clear_iteration_abort_request();
2634
2635    if integration_state.num_points > 0 {
2636        let final_view_options = view_options.for_final();
2637        status_emitter(build_status_update(StatusUpdateBuildRequest {
2638            kind: IntegrationStatusKind::Final,
2639            integration_state: &integration_state,
2640            cores,
2641            elapsed_time: t_start.elapsed(),
2642            iteration_elapsed_time: Duration::ZERO,
2643            cur_points: 0,
2644            total_points_display: integration_state.num_points,
2645            n_samples_evaluated,
2646            targets: &targets,
2647            render_options: &final_view_options,
2648            live_progress: None,
2649        }))?;
2650    } else {
2651        info!("");
2652        warn!(
2653            "{}",
2654            "No final integration results to display since no iteration completed.".yellow()
2655        );
2656        info!("");
2657    }
2658
2659    if !slots.is_empty() {
2660        emit_results_output_summary(
2661            workspace.as_deref(),
2662            &slots,
2663            &emitted_latest_observable_paths,
2664        );
2665    }
2666
2667    Ok(build_integration_result(&integration_state, &targets))
2668}
2669
2670/// Batch integrate function used for distributed runs, used by the worker nodes.
2671/// Evaluates a batch of points and returns the results in a manner specified by the user.
2672pub(crate) fn batch_integrate(
2673    integrand: &mut Integrand,
2674    model: &Model,
2675    input: BatchIntegrateInput,
2676) -> Result<BatchResult> {
2677    let samples = match input.samples {
2678        SampleInput::SampleList { samples } => samples,
2679        SampleInput::Grid {
2680            mut grid,
2681            num_points,
2682            seed,
2683            thread_id,
2684        } => {
2685            let mut rng = MonteCarloRng::new(seed, thread_id);
2686
2687            (0..num_points)
2688                .map(|_| {
2689                    let mut sample = Sample::new();
2690                    grid.sample(&mut rng, &mut sample);
2691                    sample
2692                })
2693                .collect_vec()
2694        }
2695    };
2696
2697    let (evaluation_results, metadata_statistics) = evaluate_sample_list(
2698        integrand,
2699        &samples,
2700        model,
2701        input.num_cores,
2702        input.iter,
2703        input.max_eval,
2704    )?;
2705
2706    let integrand_output = generate_integrand_output(
2707        integrand,
2708        &evaluation_results,
2709        &samples,
2710        input.integrand_output_settings,
2711        input.settings.integrator.integrated_phase,
2712    );
2713
2714    let event_output = generate_event_output(
2715        integrand,
2716        evaluation_results,
2717        input.event_output_settings,
2718        input.settings,
2719    );
2720
2721    Ok(BatchResult {
2722        statistics: metadata_statistics,
2723        integrand_data: integrand_output,
2724        event_data: event_output,
2725    })
2726}
2727
2728/// Map the evaluation result on to the right output specified by the user.
2729fn generate_integrand_output(
2730    integrand: &Integrand,
2731    evaluation_results: &[EvaluationResult],
2732    samples: &[Sample<F<f64>>],
2733    integrand_output_settings: IntegralOutputSettings,
2734    integrated_phase: IntegratedPhase,
2735) -> BatchIntegrateOutput {
2736    fn effective_integrand_result(result: &EvaluationResult) -> Complex<F<f64>> {
2737        let jacobian = result.parameterization_jacobian.unwrap_or(F(1.0));
2738        result.integrand_result * Complex::new_re(jacobian)
2739    }
2740
2741    match integrand_output_settings {
2742        IntegralOutputSettings::Default => {
2743            let integrand_values = evaluation_results
2744                .iter()
2745                .map(effective_integrand_result)
2746                .collect();
2747
2748            BatchIntegrateOutput::Default(integrand_values, samples.to_vec())
2749        }
2750        IntegralOutputSettings::Accumulator => {
2751            let mut real_accumulator = StatisticsAccumulator::new();
2752            let mut imag_accumulator = StatisticsAccumulator::new();
2753            let mut grid = integrand.create_grid();
2754
2755            for (result, sample) in evaluation_results.iter().zip(samples.iter()) {
2756                let effective_result = effective_integrand_result(result);
2757                real_accumulator
2758                    .add_sample(effective_result.re * sample.get_weight(), Some(sample));
2759                imag_accumulator
2760                    .add_sample(effective_result.im * sample.get_weight(), Some(sample));
2761
2762                match integrated_phase {
2763                    IntegratedPhase::Real => {
2764                        grid.add_training_sample(sample, effective_result.re)
2765                            .unwrap();
2766                    }
2767                    IntegratedPhase::Imag => {
2768                        grid.add_training_sample(sample, effective_result.im)
2769                            .unwrap();
2770                    }
2771                    IntegratedPhase::Both => {
2772                        unimplemented!()
2773                    }
2774                }
2775            }
2776
2777            BatchIntegrateOutput::Accumulator(
2778                Box::new((real_accumulator, imag_accumulator)),
2779                Box::new(grid),
2780            )
2781        }
2782    }
2783}
2784
2785/// Process events into histograms or event lists, as specified by the user.
2786fn generate_event_output(
2787    integrand: &Integrand,
2788    evaluation_results: Vec<EvaluationResult>,
2789    event_output_settings: EventOutputSettings,
2790    _settings: &RuntimeSettings,
2791) -> EventOutput {
2792    match event_output_settings {
2793        EventOutputSettings::None => EventOutput::None,
2794
2795        EventOutputSettings::EventList => {
2796            let mut event_groups = EventGroupList::default();
2797            for mut result in evaluation_results {
2798                event_groups.append(&mut result.event_groups);
2799            }
2800            EventOutput::EventList { event_groups }
2801        }
2802        EventOutputSettings::Histogram => integrand
2803            .observable_accumulator_bundle()
2804            .map(|histograms| EventOutput::Histogram { histograms })
2805            .unwrap_or(EventOutput::None),
2806    }
2807}
2808
2809pub fn slot_workspace_path(workspace: &Path, slot_meta: &SlotMeta) -> PathBuf {
2810    workspace.join("integrands").join(slot_meta.key())
2811}
2812
2813pub fn workspace_manifest_path(workspace: &Path) -> PathBuf {
2814    workspace.join("manifest.json")
2815}
2816
2817pub fn workspace_state_dir(workspace: &Path) -> PathBuf {
2818    workspace.join("state")
2819}
2820
2821pub fn workspace_state_path(workspace: &Path) -> PathBuf {
2822    workspace_state_dir(workspace).join("integration_state.bin")
2823}
2824
2825fn workspace_observable_state_dir(workspace: &Path, slot_meta: &SlotMeta) -> PathBuf {
2826    workspace_state_dir(workspace)
2827        .join("observables")
2828        .join(slot_meta.key())
2829}
2830
2831pub fn latest_observable_resume_state_path(workspace: &Path, slot_meta: &SlotMeta) -> PathBuf {
2832    workspace_observable_state_dir(workspace, slot_meta).join("latest.json")
2833}
2834
2835fn archived_observable_resume_state_path(
2836    workspace: &Path,
2837    slot_meta: &SlotMeta,
2838    iter: usize,
2839) -> PathBuf {
2840    workspace_observable_state_dir(workspace, slot_meta).join(format!("iter_{iter:04}.json"))
2841}
2842
2843pub fn workspace_result_snapshot_path(workspace: &Path) -> PathBuf {
2844    workspace.join("integration_result.json")
2845}
2846
2847fn workspace_result_archive_path(workspace: &Path, iter: usize) -> PathBuf {
2848    workspace
2849        .join("results")
2850        .join(format!("integration_result_iter_{iter:04}.json"))
2851}
2852
2853fn observable_output_extension(format: ObservableFileFormat) -> Option<&'static str> {
2854    match format {
2855        ObservableFileFormat::None => None,
2856        ObservableFileFormat::Hwu => Some("hwu"),
2857        ObservableFileFormat::Json => Some("json"),
2858    }
2859}
2860
2861fn latest_observable_output_path(workspace: &Path, format: ObservableFileFormat) -> PathBuf {
2862    let extension = observable_output_extension(format)
2863        .expect("user-facing observable outputs must use a real file format");
2864    workspace.join(format!("observables_final.{extension}"))
2865}
2866
2867fn archived_observable_output_path(
2868    workspace: &Path,
2869    format: ObservableFileFormat,
2870    iter: usize,
2871) -> PathBuf {
2872    let extension = observable_output_extension(format)
2873        .expect("user-facing observable outputs must use a real file format");
2874    workspace.join(format!("observables_final_iter_{iter:04}.{extension}"))
2875}
2876
2877fn user_facing_observables_output_formats(integrand: &Integrand) -> Vec<ObservableFileFormat> {
2878    let Integrand::ProcessIntegrand(process_integrand) = integrand else {
2879        return Vec::new();
2880    };
2881    if !integrand.has_observables() {
2882        return Vec::new();
2883    }
2884
2885    process_integrand
2886        .get_settings()
2887        .integrator
2888        .observables_output
2889        .resolved_formats()
2890}
2891
2892fn user_facing_observables_output_enabled(integrand: &Integrand) -> bool {
2893    let Integrand::ProcessIntegrand(process_integrand) = integrand else {
2894        return false;
2895    };
2896    if !integrand.has_observables() {
2897        return false;
2898    }
2899
2900    !process_integrand
2901        .get_settings()
2902        .integrator
2903        .observables_output
2904        .resolved_formats()
2905        .is_empty()
2906}
2907
2908fn write_atomic_bytes(path: &Path, bytes: &[u8]) -> Result<()> {
2909    let parent = path
2910        .parent()
2911        .ok_or_else(|| Report::msg("Atomic write target is missing a parent directory"))?;
2912    fs::create_dir_all(parent)?;
2913    let tmp_path = path.with_extension(format!(
2914        "{}.tmp",
2915        path.extension()
2916            .and_then(|ext| ext.to_str())
2917            .unwrap_or("tmp")
2918    ));
2919    fs::write(&tmp_path, bytes)?;
2920    fs::rename(&tmp_path, path)?;
2921    Ok(())
2922}
2923
2924fn write_atomic_json<T: Serialize>(path: &Path, value: &T) -> Result<()> {
2925    write_atomic_bytes(path, &serde_json::to_vec_pretty(value)?)
2926}
2927
2928fn write_observable_resume_state(
2929    integrand: &Integrand,
2930    workspace: Option<&Path>,
2931    slot_meta: &SlotMeta,
2932    iter: usize,
2933    output_control: WorkspaceSnapshotControl,
2934) -> Result<Option<PathBuf>> {
2935    let Some(bundle) = integrand.observable_snapshot_bundle() else {
2936        return Ok(None);
2937    };
2938    let Some(workspace) = workspace else {
2939        return Ok(None);
2940    };
2941    let latest_path = latest_observable_resume_state_path(workspace, slot_meta);
2942    write_atomic_json(&latest_path, &bundle)?;
2943    if output_control.write_iteration_archives {
2944        let archived_path = archived_observable_resume_state_path(workspace, slot_meta, iter);
2945        write_atomic_json(&archived_path, &bundle)?;
2946    }
2947    Ok(Some(latest_path))
2948}
2949
2950fn write_observable_snapshot_archive(
2951    integrand: &Integrand,
2952    workspace: Option<&Path>,
2953    iter: usize,
2954    output_control: WorkspaceSnapshotControl,
2955) -> Result<()> {
2956    if !user_facing_observables_output_enabled(integrand) {
2957        return Ok(());
2958    }
2959    if !output_control.write_iteration_archives {
2960        return Ok(());
2961    }
2962    let Some(workspace) = workspace else {
2963        return Ok(());
2964    };
2965
2966    for format in user_facing_observables_output_formats(integrand) {
2967        let path = archived_observable_output_path(workspace, format, iter);
2968        integrand.write_observable_snapshots(&path, format)?;
2969    }
2970
2971    Ok(())
2972}
2973
2974fn write_latest_observables_output(
2975    integrand: &Integrand,
2976    workspace: Option<&Path>,
2977) -> Result<Vec<PathBuf>> {
2978    if !user_facing_observables_output_enabled(integrand) {
2979        return Ok(Vec::new());
2980    }
2981    let Some(workspace) = workspace else {
2982        return Ok(Vec::new());
2983    };
2984
2985    let mut emitted_paths = Vec::new();
2986    for format in user_facing_observables_output_formats(integrand) {
2987        let path = latest_observable_output_path(workspace, format);
2988        integrand.write_observable_snapshots(&path, format)?;
2989        emitted_paths.push(path);
2990    }
2991
2992    Ok(emitted_paths)
2993}
2994
2995fn write_integration_state_to_workspace(
2996    workspace_path: &Path,
2997    integration_state: &IntegrationState,
2998) -> Result<()> {
2999    write_atomic_bytes(
3000        &workspace_state_path(workspace_path),
3001        &bincode::encode_to_vec(integration_state, bincode::config::standard())
3002            .unwrap_or_else(|_| panic!("failed to serialize the integration state")),
3003    )?;
3004    Ok(())
3005}
3006
3007fn write_integration_result_snapshots(
3008    workspace_path: &Path,
3009    integration_state: &IntegrationState,
3010    targets: &[Option<Complex<F<f64>>>],
3011    output_control: WorkspaceSnapshotControl,
3012) -> Result<()> {
3013    let result = build_integration_result(integration_state, targets);
3014    write_atomic_json(&workspace_result_snapshot_path(workspace_path), &result)?;
3015    if output_control.write_iteration_archives {
3016        write_atomic_json(
3017            &workspace_result_archive_path(workspace_path, integration_state.iter),
3018            &result,
3019        )?;
3020    }
3021    Ok(())
3022}
3023
3024fn workspace_relative_display_path(root: &Path, path: &Path) -> String {
3025    path.strip_prefix(root)
3026        .unwrap_or(path)
3027        .display()
3028        .to_string()
3029}
3030
3031fn emit_results_output_summary(
3032    workspace: Option<&Path>,
3033    slots: &[IntegrationSlot],
3034    emitted_paths: &[Vec<PathBuf>],
3035) {
3036    let rendered = render_results_output_summary_table(workspace, slots, emitted_paths);
3037    let Some(rendered) = rendered else {
3038        return;
3039    };
3040
3041    info!("\n{rendered}");
3042}
3043
3044fn render_results_output_summary_table(
3045    workspace: Option<&Path>,
3046    slots: &[IntegrationSlot],
3047    emitted_paths: &[Vec<PathBuf>],
3048) -> Option<String> {
3049    let mut summary_rows = Vec::new();
3050    if let Some(workspace) = workspace {
3051        summary_rows.push((
3052            "workspace path".to_string(),
3053            workspace.display().to_string(),
3054        ));
3055        summary_rows.push((
3056            "results".to_string(),
3057            workspace_relative_display_path(workspace, &workspace_result_snapshot_path(workspace)),
3058        ));
3059    }
3060
3061    for (slot, slot_emitted_paths) in slots.iter().zip(emitted_paths.iter()) {
3062        for final_path in slot_emitted_paths {
3063            if !final_path.exists() {
3064                continue;
3065            }
3066            let display_path = workspace
3067                .map(|root| workspace_relative_display_path(root, final_path))
3068                .unwrap_or_else(|| final_path.display().to_string());
3069            let row_label = if slot_emitted_paths.len() > 1 {
3070                let format_label = final_path
3071                    .extension()
3072                    .and_then(|extension| extension.to_str())
3073                    .unwrap_or("output");
3074                format!("{} ({format_label})", slot_key_label(&slot.meta))
3075            } else {
3076                slot_key_label(&slot.meta)
3077            };
3078            summary_rows.push((row_label, display_path));
3079        }
3080    }
3081
3082    if summary_rows.is_empty() {
3083        return None;
3084    }
3085
3086    let mut builder = Builder::default();
3087    builder.push_record(["type".to_string(), "path".to_string()]);
3088    for (row_type, row_path) in summary_rows {
3089        builder.push_record([
3090            row_type.blue().bold().to_string(),
3091            row_path.green().to_string(),
3092        ]);
3093    }
3094
3095    let mut table = builder.build();
3096    table.with(Panel::header(
3097        "Integration results emitted".green().bold().to_string(),
3098    ));
3099    table.with(
3100        Style::rounded().horizontals([(
3101            1,
3102            HorizontalLine::new('─')
3103                .intersection('┼')
3104                .left('├')
3105                .right('┤'),
3106        )]),
3107    );
3108    table.with(Modify::new(Rows::first()).with(Alignment::left()));
3109    table.with(Modify::new(Columns::new(0..2)).with(Alignment::left()));
3110
3111    Some(utils::normalize_tabled_separator_rows(&table.to_string()))
3112}
3113
3114/// This function actually evaluates the list of samples in parallel.
3115fn evaluate_sample_list(
3116    integrand: &mut Integrand,
3117    samples: &[Sample<F<f64>>],
3118    model: &Model,
3119    num_cores: usize,
3120    iter: usize,
3121    max_eval: Complex<F<f64>>,
3122) -> Result<(Vec<EvaluationResult>, StatisticsCounter)> {
3123    let list_size = samples.len();
3124    let nvec_per_core = (list_size - 1) / num_cores + 1;
3125    let num_chunks = samples.chunks(nvec_per_core).len();
3126
3127    let sample_chunks = samples.par_chunks(nvec_per_core);
3128    let integrands = (0..num_chunks)
3129        .map(|_| integrand.clone())
3130        .collect_vec()
3131        .into_par_iter();
3132
3133    let evaluation_results_per_core: Vec<
3134        Result<(Vec<EvaluationResult>, StatisticsCounter, Integrand)>,
3135    > = sample_chunks
3136        .zip(integrands)
3137        .map(|(chunk, mut integrand)| {
3138            let raw_batch =
3139                integrand.evaluate_samples_raw(chunk, model, iter, false, false, max_eval)?;
3140            Ok((raw_batch.samples, raw_batch.statistics, integrand))
3141        })
3142        .collect();
3143    let mut evaluation_results_per_core: Vec<(
3144        Vec<EvaluationResult>,
3145        StatisticsCounter,
3146        Integrand,
3147    )> = evaluation_results_per_core
3148        .into_iter()
3149        .collect::<Result<_>>()?;
3150
3151    for (_, _, worker_integrand) in evaluation_results_per_core.iter_mut() {
3152        integrand.merge_runtime_results(worker_integrand)?;
3153    }
3154
3155    let (evaluation_results, meta_data_statistics) = evaluation_results_per_core.into_iter().fold(
3156        (Vec::new(), StatisticsCounter::new_empty()),
3157        |(mut all_results, stats), (results, worker_stats, _)| {
3158            all_results.extend(results);
3159            (all_results, stats.merged(&worker_stats))
3160        },
3161    );
3162
3163    Ok((evaluation_results, meta_data_statistics))
3164}
3165
3166/// Different ways of passing samples to the batch_integrate function
3167/// One is simply a list of samples, the other is a grid and the desired number of samples
3168/// The worker then generates the samples itself.
3169#[derive(Serialize, Deserialize)]
3170pub enum SampleInput {
3171    SampleList {
3172        samples: Vec<Sample<F<f64>>>,
3173    },
3174    Grid {
3175        grid: Box<Grid<F<f64>>>,
3176        num_points: usize,
3177        seed: u64,
3178        thread_id: usize,
3179    },
3180}
3181
3182type AccumulatorPair = (StatisticsAccumulator<F<f64>>, StatisticsAccumulator<F<f64>>);
3183
3184#[derive(Serialize, Deserialize)]
3185pub enum BatchIntegrateOutput {
3186    Default(Vec<Complex<F<f64>>>, Vec<Sample<F<f64>>>),
3187    Accumulator(Box<AccumulatorPair>, Box<Grid<F<f64>>>),
3188}
3189
3190/// Different ways of processing events, EventList is a list of events, Histogram does accumulation of events on the worker nodes, so the
3191/// master node only has to merge the histograms.
3192#[derive(Serialize, Deserialize)]
3193pub enum EventOutput {
3194    None,
3195    EventList {
3196        event_groups: EventGroupList,
3197    },
3198    Histogram {
3199        histograms: ObservableAccumulatorBundle,
3200    },
3201}
3202
3203/// The result of evaluating a batch of points
3204#[derive(Serialize, Deserialize, Encode, Decode)]
3205pub struct BatchResult {
3206    pub statistics: StatisticsCounter,
3207    #[bincode(with_serde)]
3208    pub integrand_data: BatchIntegrateOutput,
3209    #[bincode(with_serde)]
3210    pub event_data: EventOutput,
3211}
3212
3213/// Input for the batch_integrate function, created by the master node
3214pub struct BatchIntegrateInput<'a> {
3215    // global run info:
3216    pub max_eval: Complex<F<f64>>,
3217    pub iter: usize,
3218    pub settings: &'a RuntimeSettings,
3219    // input data:
3220    pub samples: SampleInput,
3221    pub integrand_output_settings: IntegralOutputSettings,
3222    pub event_output_settings: EventOutputSettings,
3223    pub num_cores: usize,
3224}
3225
3226/// Choose whether to output the integrand results, or a statistics accumulator
3227#[derive(Serialize, Deserialize)]
3228pub enum IntegralOutputSettings {
3229    Default,
3230    Accumulator,
3231}
3232
3233/// Choose whether to output an eventlist, a histogram, or nothing
3234#[derive(Serialize, Deserialize)]
3235pub enum EventOutputSettings {
3236    None,
3237    EventList,
3238    Histogram,
3239}
3240
3241#[derive(Serialize, Deserialize, Encode, Decode)]
3242pub struct SerializableBatchIntegrateInput {
3243    #[bincode(with_serde)]
3244    pub max_eval: Complex<F<f64>>,
3245    pub iter: usize,
3246    #[bincode(with_serde)]
3247    pub samples: SampleInput,
3248    #[bincode(with_serde)]
3249    pub integrand_output_settings: IntegralOutputSettings,
3250    #[bincode(with_serde)]
3251    pub event_output_settings: EventOutputSettings,
3252    pub num_cores: usize,
3253}
3254
3255impl SerializableBatchIntegrateInput {
3256    pub(crate) fn into_batch_integrate_input(
3257        self,
3258        settings: &RuntimeSettings,
3259    ) -> BatchIntegrateInput<'_> {
3260        BatchIntegrateInput {
3261            max_eval: self.max_eval,
3262            iter: self.iter,
3263            samples: self.samples,
3264            integrand_output_settings: self.integrand_output_settings,
3265            event_output_settings: self.event_output_settings,
3266            num_cores: self.num_cores,
3267            settings,
3268        }
3269    }
3270}
3271
3272/// Master node which accumulates data from jobs, and creates the input for jobs
3273#[derive(Clone)]
3274pub struct MasterNode {
3275    grid: Grid<F<f64>>,
3276    integrator_settings: IntegratorSettings,
3277    master_accumulator_re: StatisticsAccumulator<F<f64>>,
3278    master_accumulator_im: StatisticsAccumulator<F<f64>>,
3279    statistics: StatisticsCounter,
3280    observable_accumulators: Option<ObservableAccumulatorBundle>,
3281    event_groups: EventGroupList,
3282    current_iter: usize,
3283}
3284
3285impl MasterNode {
3286    pub(crate) fn new(grid: Grid<F<f64>>, integrator_settings: IntegratorSettings) -> Self {
3287        MasterNode {
3288            grid,
3289            integrator_settings,
3290            master_accumulator_im: StatisticsAccumulator::new(),
3291            master_accumulator_re: StatisticsAccumulator::new(),
3292            statistics: StatisticsCounter::new_empty(),
3293            observable_accumulators: None,
3294            event_groups: EventGroupList::default(),
3295            current_iter: 0,
3296        }
3297    }
3298
3299    /// Update the grid with the data from another grid.
3300    fn update_grid_with_grid(&mut self, other_grid: &Grid<F<f64>>) -> Result<(), String> {
3301        self.grid.merge(other_grid)
3302    }
3303
3304    /// Update the grid with the data from a set of samples.
3305    fn update_grid_with_samples(
3306        &mut self,
3307        samples_points: &[Sample<F<f64>>],
3308        results: &[Complex<F<f64>>],
3309    ) -> Result<(), String> {
3310        let integrated_phase = self.integrator_settings.integrated_phase;
3311
3312        for (sample_point, result) in samples_points.iter().zip(results.iter()) {
3313            match integrated_phase {
3314                IntegratedPhase::Real => self.grid.add_training_sample(sample_point, result.re)?,
3315                IntegratedPhase::Imag => self.grid.add_training_sample(sample_point, result.im)?,
3316                IntegratedPhase::Both => {
3317                    unimplemented!("integrated phase both not yet implemented")
3318                }
3319            }
3320        }
3321
3322        Ok(())
3323    }
3324
3325    /// Update the accumulators with the data from another set of accumulators.
3326    pub(crate) fn update_accumulators_with_accumulators(
3327        &mut self,
3328        mut real_accumulator: StatisticsAccumulator<F<f64>>,
3329        mut imaginary_accumulator: StatisticsAccumulator<F<f64>>,
3330    ) {
3331        self.master_accumulator_re
3332            .merge_samples(&mut real_accumulator);
3333
3334        self.master_accumulator_im
3335            .merge_samples(&mut imaginary_accumulator);
3336    }
3337
3338    /// Update the accumulators with the data from a set of samples.
3339    fn update_accumuators_with_samples(
3340        &mut self,
3341        sample_points: &[Sample<F<f64>>],
3342        results: &[Complex<F<f64>>],
3343    ) {
3344        for (sample_point, result) in sample_points.iter().zip(results.iter()) {
3345            self.master_accumulator_re
3346                .add_sample(result.re * sample_point.get_weight(), Some(sample_point));
3347
3348            self.master_accumulator_im
3349                .add_sample(result.im * sample_point.get_weight(), Some(sample_point));
3350        }
3351    }
3352
3353    /// Update the metadata statistics with the data from another set of metadata statistics.
3354    fn update_metadata_statistics(&mut self, statistics: StatisticsCounter) {
3355        self.statistics = self.statistics.merged(&statistics);
3356    }
3357
3358    /// Finish the current iteration. This should be called after all jobs have been processed.
3359    pub(crate) fn update_iter(&mut self) {
3360        self.grid.update(
3361            F(self.integrator_settings.discrete_dim_learning_rate),
3362            F(self.integrator_settings.continuous_dim_learning_rate),
3363        );
3364        self.master_accumulator_re.update_iter(false);
3365        self.master_accumulator_im.update_iter(false);
3366        if let Some(observable_accumulators) = self.observable_accumulators.as_mut() {
3367            observable_accumulators.update_results();
3368        }
3369
3370        self.current_iter += 1;
3371    }
3372
3373    /// Write the input for a batch job to a file.
3374    pub(crate) fn write_batch_input(
3375        &mut self,
3376        num_cores: usize,
3377        num_samples: usize,
3378        export_grid: bool,
3379        output_accumulator: bool,
3380        workspace_path: &str,
3381        job_id: usize,
3382    ) -> Result<(), Report> {
3383        let max_eval = Complex::new(
3384            self.master_accumulator_re
3385                .max_eval_positive
3386                .max(self.master_accumulator_re.max_eval_negative),
3387            self.master_accumulator_im
3388                .max_eval_positive
3389                .max(self.master_accumulator_im.max_eval_negative),
3390        );
3391
3392        let samples = if export_grid {
3393            SampleInput::Grid {
3394                grid: Box::new(self.grid.clone()),
3395                num_points: num_samples,
3396                seed: self.integrator_settings.seed,
3397                thread_id: job_id,
3398            }
3399        } else {
3400            let mut rng = rand::rng();
3401            let mut samples_temp = vec![Sample::new(); num_samples];
3402            for sample in samples_temp.iter_mut() {
3403                self.grid.sample(&mut rng, sample);
3404            }
3405            SampleInput::SampleList {
3406                samples: samples_temp,
3407            }
3408        };
3409
3410        let integrand_output_settings = if output_accumulator {
3411            IntegralOutputSettings::Accumulator
3412        } else {
3413            IntegralOutputSettings::Default
3414        };
3415
3416        let input = SerializableBatchIntegrateInput {
3417            num_cores,
3418            max_eval,
3419            iter: self.current_iter,
3420            samples,
3421            integrand_output_settings,
3422            event_output_settings: EventOutputSettings::None,
3423        };
3424
3425        let input_bytes = bincode::encode_to_vec(&input, bincode::config::standard())?;
3426        let job_name = format!("job_{}", job_id);
3427        let job_path = std::path::Path::new(workspace_path).join(job_name);
3428
3429        std::fs::write(job_path, input_bytes)?;
3430
3431        Ok(())
3432    }
3433
3434    /// Process the output of a batch job.
3435    pub(crate) fn process_batch_output(&mut self, output: BatchResult) -> Result<(), String> {
3436        self.update_metadata_statistics(output.statistics);
3437
3438        match output.integrand_data {
3439            BatchIntegrateOutput::Default(results, samples) => {
3440                self.update_accumuators_with_samples(&samples, &results);
3441                self.update_grid_with_samples(&samples, &results)?;
3442            }
3443            BatchIntegrateOutput::Accumulator(accumulators, grid) => {
3444                let (real_accumulator, imag_accumulator) = *accumulators;
3445                self.update_accumulators_with_accumulators(real_accumulator, imag_accumulator);
3446                self.update_grid_with_grid(&grid)?;
3447            }
3448        }
3449
3450        match output.event_data {
3451            EventOutput::None => {}
3452            EventOutput::EventList { mut event_groups } => {
3453                self.event_groups.append(&mut event_groups);
3454            }
3455            EventOutput::Histogram { mut histograms } => {
3456                if let Some(existing) = self.observable_accumulators.as_mut() {
3457                    existing
3458                        .merge_samples(&mut histograms)
3459                        .map_err(|err| err.to_string())?;
3460                } else {
3461                    self.observable_accumulators = Some(histograms);
3462                }
3463            }
3464        }
3465
3466        Ok(())
3467    }
3468
3469    /// Display the current status of the integration. Usually called after each iteration.
3470    pub(crate) fn display_status(&self) {
3471        let status_block = [
3472            render_integral_result(
3473                &self.master_accumulator_re,
3474                "itg",
3475                self.current_iter,
3476                "re",
3477                None,
3478            ),
3479            render_integral_result(
3480                &self.master_accumulator_im,
3481                "itg",
3482                self.current_iter,
3483                "im",
3484                None,
3485            ),
3486            self.statistics.render_status_table(),
3487        ]
3488        .join("\n");
3489
3490        info!("\n{status_block}");
3491    }
3492}
3493
3494pub fn emit_integration_status_via_tracing(
3495    kind: IntegrationStatusKind,
3496    status_block: impl AsRef<str>,
3497) -> Result<()> {
3498    let status_block = status_block.as_ref();
3499    match kind {
3500        IntegrationStatusKind::Live => {}
3501        IntegrationStatusKind::Iteration => {
3502            info!("\n{status_block}");
3503            info!("");
3504        }
3505        IntegrationStatusKind::Final => {
3506            info!("");
3507            info!("{}", "Final integration results:".bold().green());
3508            info!("");
3509            info!("\n{status_block}");
3510            info!("");
3511        }
3512    }
3513
3514    Ok(())
3515}
3516
3517pub fn render_saved_integration_summary(
3518    integration_state: &IntegrationState,
3519    targets: &[Option<Complex<F<f64>>>],
3520    view_options: &IntegrationStatusViewOptions,
3521    tabled_options: &TabledRenderOptions,
3522) -> String {
3523    render_tabled::render_status_update(
3524        &build_saved_status_update(integration_state, targets, view_options),
3525        tabled_options,
3526    )
3527}
3528
3529pub fn render_status_update_tabled(
3530    update: &StatusUpdate,
3531    tabled_options: &TabledRenderOptions,
3532) -> String {
3533    render_tabled::render_status_update(update, tabled_options)
3534}
3535
3536pub fn build_integration_result(
3537    integration_state: &IntegrationState,
3538    targets: &[Option<Complex<F<f64>>>],
3539) -> IntegrationResult {
3540    let integration_statistics = integration_state.stats.snapshot();
3541    let slots = integration_state
3542        .slot_metas
3543        .iter()
3544        .enumerate()
3545        .map(|(slot_index, slot_meta)| {
3546            let accumulator = &integration_state.all_integrals[slot_index];
3547            let discrete_context =
3548                integration_state.monitored_discrete_context_for_slot(slot_index);
3549            SlotIntegrationResult {
3550                key: slot_meta.key(),
3551                process: slot_meta.process_name.clone(),
3552                integrand: slot_meta.integrand_name.clone(),
3553                target: targets[slot_index],
3554                integral: IntegralEstimate {
3555                    neval: accumulator.re.processed_samples,
3556                    real_zero: accumulator.re.num_zero_evaluations,
3557                    im_zero: accumulator.im.num_zero_evaluations,
3558                    result: Complex::new(accumulator.re.avg, accumulator.im.avg),
3559                    error: Complex::new(accumulator.re.err, accumulator.im.err),
3560                    real_chisq: accumulator.re.chi_sq,
3561                    im_chisq: accumulator.im.chi_sq,
3562                },
3563                table_results: build_table_result_summary(
3564                    slot_meta,
3565                    accumulator,
3566                    integration_state.iter,
3567                    targets[slot_index],
3568                ),
3569                integration_statistics: integration_statistics.clone(),
3570                max_weight_info: build_max_weight_info_summary(
3571                    &integration_state
3572                        .sampling_state_for_slot(slot_index)
3573                        .discrete_axis_labels,
3574                    accumulator,
3575                ),
3576                grid_breakdown: ComponentDiscreteBreakdown {
3577                    re: discrete_context.as_ref().and_then(|context| {
3578                        integration_state.slot_re_summaries[slot_index]
3579                            .as_ref()
3580                            .zip(
3581                                integration_state
3582                                    .slot_first_non_trivial_discrete_breakdown_metadata[slot_index]
3583                                    .as_ref(),
3584                            )
3585                            .and_then(|(summary, metadata)| {
3586                                summary_at_path(summary, &context.path).and_then(|summary| {
3587                                    summary.first_non_trivial_breakdown(metadata, &context.pdfs)
3588                                })
3589                            })
3590                    }),
3591                    im: discrete_context.as_ref().and_then(|context| {
3592                        integration_state.slot_im_summaries[slot_index]
3593                            .as_ref()
3594                            .zip(
3595                                integration_state
3596                                    .slot_first_non_trivial_discrete_breakdown_metadata[slot_index]
3597                                    .as_ref(),
3598                            )
3599                            .and_then(|(summary, metadata)| {
3600                                summary_at_path(summary, &context.path).and_then(|summary| {
3601                                    summary.first_non_trivial_breakdown(metadata, &context.pdfs)
3602                                })
3603                            })
3604                    }),
3605                },
3606            }
3607        })
3608        .collect();
3609
3610    IntegrationResult { slots }
3611}
3612
3613pub fn print_integral_result(
3614    itg: &StatisticsAccumulator<F<f64>>,
3615    label: &str,
3616    i_iter: usize,
3617    tag: &str,
3618    trgt: Option<F<f64>>,
3619) {
3620    info!("{}", render_integral_result(itg, label, i_iter, tag, trgt));
3621}
3622
3623#[allow(clippy::format_in_format_args)]
3624fn render_integral_result(
3625    itg: &StatisticsAccumulator<F<f64>>,
3626    label: &str,
3627    i_iter: usize,
3628    tag: &str,
3629    trgt: Option<F<f64>>,
3630) -> String {
3631    let slot_meta = SlotMeta {
3632        process_name: label.to_string(),
3633        integrand_name: String::new(),
3634    };
3635    let mut cells = build_integral_result_cells(itg, &slot_meta, i_iter, tag, trgt);
3636    cells.integrand = format!("{label} {}:", format!("{:-2}", tag).blue().bold());
3637    let delta = match (cells.delta_sigma.as_ref(), cells.delta_percent.as_ref()) {
3638        (Some(delta_sigma), Some(delta_percent)) => format!("{delta_sigma}, {delta_percent}"),
3639        _ => String::new(),
3640    };
3641
3642    format!(
3643        "|  {} {} {} {} {} {}",
3644        cells.integrand, cells.value, cells.relative_error, cells.chi_sq, delta, cells.mwi
3645    )
3646}
3647
3648#[cfg(test)]
3649mod tests {
3650    use super::*;
3651    use crate::{UnitVolumeIntegrand, UnitVolumeSettings};
3652    use colored::control;
3653    use ratatui::{Terminal, backend::TestBackend};
3654    use std::fs;
3655    use symbolica::numerical_integration::ContinuousGrid;
3656
3657    fn make_accumulator(
3658        re_avg: f64,
3659        re_err: f64,
3660        re_chi_sq: f64,
3661        im_avg: f64,
3662        im_err: f64,
3663        im_chi_sq: f64,
3664    ) -> ComplexAccumulator {
3665        let mut accumulator = ComplexAccumulator::new();
3666        accumulator.re.avg = F(re_avg);
3667        accumulator.re.err = F(re_err);
3668        accumulator.re.chi_sq = F(re_chi_sq);
3669        accumulator.re.processed_samples = 100_000;
3670        accumulator.re.max_eval_positive = F(1.0);
3671        accumulator.im.avg = F(im_avg);
3672        accumulator.im.err = F(im_err);
3673        accumulator.im.chi_sq = F(im_chi_sq);
3674        accumulator.im.processed_samples = 100_000;
3675        accumulator.im.max_eval_positive = F(1.0);
3676        accumulator
3677    }
3678
3679    struct StatisticsFixture {
3680        precision: crate::settings::runtime::Precision,
3681        total_time: Duration,
3682        integrand_time: Duration,
3683        evaluator_time: Duration,
3684        parameterization_time: Duration,
3685        event_time: Duration,
3686        generated_event_count: usize,
3687        accepted_event_count: usize,
3688    }
3689
3690    fn make_statistics_counter(fixture: StatisticsFixture) -> StatisticsCounter {
3691        let mut evaluation = EvaluationResult::zero();
3692        evaluation.evaluation_metadata.total_timing = fixture.total_time;
3693        evaluation.evaluation_metadata.integrand_evaluation_time = fixture.integrand_time;
3694        evaluation.evaluation_metadata.evaluator_evaluation_time = fixture.evaluator_time;
3695        evaluation.evaluation_metadata.parameterization_time = fixture.parameterization_time;
3696        evaluation.evaluation_metadata.event_processing_time = fixture.event_time;
3697        evaluation.evaluation_metadata.generated_event_count = fixture.generated_event_count;
3698        evaluation.evaluation_metadata.accepted_event_count = fixture.accepted_event_count;
3699        evaluation.evaluation_metadata.stability_results.push(
3700            crate::integrands::evaluation::StabilityResult {
3701                precision: fixture.precision,
3702                estimated_relative_accuracy: None,
3703                status: crate::integrands::evaluation::StabilityStatus::Stable(2),
3704                total_time: fixture.total_time,
3705            },
3706        );
3707        StatisticsCounter::from_evaluation_results(&[evaluation])
3708    }
3709
3710    fn make_integration_state() -> IntegrationState {
3711        let sampling_grid = Grid::Continuous(ContinuousGrid::new(1, 64, 100, None, false));
3712        let mut state = IntegrationState::new_from_settings(
3713            SamplingCorrelationMode::Correlated,
3714            vec![SamplingSlotState::new(sampling_grid, Vec::new())],
3715            vec![
3716                SlotMeta {
3717                    process_name: "proc_a".to_string(),
3718                    integrand_name: "itg_a".to_string(),
3719                },
3720                SlotMeta {
3721                    process_name: "proc_b".to_string(),
3722                    integrand_name: "itg_b".to_string(),
3723                },
3724            ],
3725            None,
3726            None,
3727            None,
3728            vec![None, None],
3729        );
3730        state.iter = 1;
3731        state.num_points = 100_000;
3732        state.slot_stats = vec![
3733            make_statistics_counter(StatisticsFixture {
3734                precision: crate::settings::runtime::Precision::Double,
3735                total_time: Duration::from_micros(462),
3736                integrand_time: Duration::from_micros(414),
3737                evaluator_time: Duration::from_micros(279),
3738                parameterization_time: Duration::from_nanos(5_800),
3739                event_time: Duration::from_micros(12),
3740                generated_event_count: 7,
3741                accepted_event_count: 5,
3742            }),
3743            make_statistics_counter(StatisticsFixture {
3744                precision: crate::settings::runtime::Precision::Quad,
3745                total_time: Duration::from_micros(900),
3746                integrand_time: Duration::from_micros(810),
3747                evaluator_time: Duration::from_micros(120),
3748                parameterization_time: Duration::from_micros(34),
3749                event_time: Duration::from_micros(40),
3750                generated_event_count: 9,
3751                accepted_event_count: 6,
3752            }),
3753        ];
3754        state.stats = state.slot_stats[0].merged(&state.slot_stats[1]);
3755        state
3756            .stats
3757            .add_integrator_overhead(Duration::from_micros(110), 2);
3758        state.all_integrals = vec![
3759            make_accumulator(7.5e-5, 9.8e-5, 0.394, 3.2e-5, 1.5e-5, 0.378),
3760            make_accumulator(2.1e-5, 2.0e-6, 0.221, -1.7e-5, 3.0e-6, 0.187),
3761        ];
3762        state
3763    }
3764
3765    fn make_preview_test_slots(slot_metas: &[SlotMeta]) -> Vec<IntegrationSlot> {
3766        slot_metas
3767            .iter()
3768            .cloned()
3769            .map(|meta| {
3770                let settings = RuntimeSettings::default();
3771                IntegrationSlot::new(
3772                    meta,
3773                    settings.clone(),
3774                    Model::default(),
3775                    Integrand::UnitVolume(UnitVolumeIntegrand::new(
3776                        settings,
3777                        UnitVolumeSettings { n_3d_momenta: 1 },
3778                    )),
3779                    None,
3780                )
3781            })
3782            .collect()
3783    }
3784
3785    fn make_preview_test_core_state(state: &IntegrationState) -> CoreIterationState {
3786        let slot_stats = vec![
3787            make_statistics_counter(StatisticsFixture {
3788                precision: crate::settings::runtime::Precision::Arb,
3789                total_time: Duration::from_millis(8),
3790                integrand_time: Duration::from_millis(7),
3791                evaluator_time: Duration::from_millis(3),
3792                parameterization_time: Duration::from_micros(150),
3793                event_time: Duration::from_micros(500),
3794                generated_event_count: 40,
3795                accepted_event_count: 30,
3796            }),
3797            make_statistics_counter(StatisticsFixture {
3798                precision: crate::settings::runtime::Precision::Arb,
3799                total_time: Duration::from_millis(12),
3800                integrand_time: Duration::from_millis(10),
3801                evaluator_time: Duration::from_millis(2),
3802                parameterization_time: Duration::from_micros(300),
3803                event_time: Duration::from_millis(1),
3804                generated_event_count: 60,
3805                accepted_event_count: 45,
3806            }),
3807        ];
3808        let grid_template = state
3809            .sampling_state_for_slot(0)
3810            .grid
3811            .clone_without_samples();
3812        CoreIterationState {
3813            slot_integrands: Vec::new(),
3814            stats: slot_stats[0].merged(&slot_stats[1]),
3815            slot_stats,
3816            integrals: vec![ComplexAccumulator::new(); state.slot_metas.len()],
3817            sampling_correlation_mode: SamplingCorrelationMode::Correlated,
3818            sampling_states: vec![CoreSamplingSlotState {
3819                sampling_grid: grid_template.clone(),
3820                rng: MonteCarloRng::new(7, 0),
3821            }],
3822            slot_re_grids: (0..state.slot_metas.len())
3823                .map(|_| grid_template.clone())
3824                .collect(),
3825            slot_im_grids: (0..state.slot_metas.len())
3826                .map(|_| grid_template.clone())
3827                .collect(),
3828            remaining_points: 0,
3829            completed_points: 12,
3830        }
3831    }
3832
3833    fn make_discrete_integration_state() -> IntegrationState {
3834        let make_continuous_grid =
3835            || Grid::Continuous(ContinuousGrid::new(1, 64, 100, None, false));
3836        let sampling_grid = Grid::Discrete(DiscreteGrid::new(
3837            vec![Some(make_continuous_grid()), Some(make_continuous_grid())],
3838            F(10.0),
3839            false,
3840        ));
3841        let mut state = IntegrationState::new_from_settings(
3842            SamplingCorrelationMode::Correlated,
3843            vec![SamplingSlotState::new(
3844                sampling_grid,
3845                vec!["graph".to_string()],
3846            )],
3847            vec![
3848                SlotMeta {
3849                    process_name: "proc_a".to_string(),
3850                    integrand_name: "itg_a".to_string(),
3851                },
3852                SlotMeta {
3853                    process_name: "proc_b".to_string(),
3854                    integrand_name: "itg_b".to_string(),
3855                },
3856            ],
3857            Some(vec![]),
3858            Some("graph".to_string()),
3859            Some(vec!["GL0".to_string(), "GL1".to_string()]),
3860            vec![
3861                Some(PersistedDiscreteBreakdownMetadata {
3862                    axis_label: "graph".to_string(),
3863                    fixed_coordinates: Vec::new(),
3864                    bin_labels: vec!["GL0".to_string(), "GL1".to_string()],
3865                }),
3866                Some(PersistedDiscreteBreakdownMetadata {
3867                    axis_label: "graph".to_string(),
3868                    fixed_coordinates: Vec::new(),
3869                    bin_labels: vec!["GL0".to_string(), "GL1".to_string()],
3870                }),
3871            ],
3872        );
3873        state.first_non_trivial_discrete_bin_descriptions =
3874            Some(vec!["GL0".to_string(), "GL1".to_string()]);
3875        state.iter = 2;
3876        state.num_points = 210_000;
3877        state.slot_stats = vec![
3878            make_statistics_counter(StatisticsFixture {
3879                precision: crate::settings::runtime::Precision::Double,
3880                total_time: Duration::from_micros(462),
3881                integrand_time: Duration::from_micros(414),
3882                evaluator_time: Duration::from_micros(279),
3883                parameterization_time: Duration::from_nanos(5_800),
3884                event_time: Duration::from_micros(12),
3885                generated_event_count: 7,
3886                accepted_event_count: 5,
3887            }),
3888            make_statistics_counter(StatisticsFixture {
3889                precision: crate::settings::runtime::Precision::Quad,
3890                total_time: Duration::from_micros(900),
3891                integrand_time: Duration::from_micros(810),
3892                evaluator_time: Duration::from_micros(120),
3893                parameterization_time: Duration::from_micros(34),
3894                event_time: Duration::from_micros(40),
3895                generated_event_count: 9,
3896                accepted_event_count: 6,
3897            }),
3898        ];
3899        state.stats = state.slot_stats[0].merged(&state.slot_stats[1]);
3900        state
3901            .stats
3902            .add_integrator_overhead(Duration::from_micros(110), 2);
3903        state.all_integrals = vec![
3904            make_accumulator(7.5e-5, 9.8e-5, 0.394, 3.2e-5, 1.5e-5, 0.378),
3905            make_accumulator(2.1e-5, 2.0e-6, 0.221, -1.7e-5, 3.0e-6, 0.187),
3906        ];
3907        if let Grid::Discrete(discrete_grid) = &mut state.sampling_state_for_slot_mut(0).grid {
3908            discrete_grid.bins[0].pdf = F(0.75);
3909            discrete_grid.bins[1].pdf = F(0.25);
3910        }
3911
3912        for summaries in [&mut state.slot_re_summaries, &mut state.slot_im_summaries] {
3913            for (slot_index, summary) in summaries.iter_mut().enumerate() {
3914                let summary = summary.as_mut().expect("discrete summary expected");
3915                summary.bins[0].accumulator.avg = F(1.0e-5 * (slot_index as f64 + 1.0));
3916                summary.bins[0].accumulator.err = F(2.0e-6);
3917                summary.bins[0].accumulator.chi_sq = F(0.2);
3918                summary.bins[0].accumulator.processed_samples = 150;
3919                summary.bins[0].accumulator.max_eval_positive = F(0.75);
3920                summary.bins[0].accumulator.max_eval_positive_xs =
3921                    Some(Sample::Continuous(F(1.0), vec![F(0.25)]));
3922                summary.bins[1].accumulator.avg = F(5.0e-6 * (slot_index as f64 + 1.0));
3923                summary.bins[1].accumulator.err = F(1.0e-6);
3924                summary.bins[1].accumulator.chi_sq = F(0.1);
3925                summary.bins[1].accumulator.processed_samples = 50;
3926                summary.bins[1].accumulator.max_eval_positive = F(0.25);
3927                summary.bins[1].accumulator.max_eval_positive_xs =
3928                    Some(Sample::Continuous(F(1.0), vec![F(0.75)]));
3929            }
3930        }
3931
3932        state
3933    }
3934
3935    fn make_single_graph_discrete_integration_state() -> IntegrationState {
3936        let mut state = make_discrete_integration_state();
3937        let Grid::Discrete(grid) = &mut state.sampling_state_for_slot_mut(0).grid else {
3938            unreachable!("discrete fixture must use a discrete grid")
3939        };
3940        grid.bins.truncate(1);
3941        grid.bins[0].pdf = F(1.0);
3942        for summaries in [&mut state.slot_re_summaries, &mut state.slot_im_summaries] {
3943            for summary in summaries.iter_mut().flatten() {
3944                summary.bins.truncate(1);
3945            }
3946        }
3947        state.first_non_trivial_discrete_bin_descriptions = Some(vec!["GL22".to_string()]);
3948        for metadata in &mut state.slot_first_non_trivial_discrete_breakdown_metadata {
3949            if let Some(metadata) = metadata.as_mut() {
3950                metadata.bin_labels = vec!["GL22".to_string()];
3951            }
3952        }
3953        state
3954    }
3955
3956    fn default_view_options() -> IntegrationStatusViewOptions {
3957        IntegrationStatusViewOptions {
3958            phase_display: IntegrationStatusPhaseDisplay::Both,
3959            training_phase_display: IntegrationStatusPhaseDisplay::Real,
3960            training_slot: 0,
3961            slot_training_phase_displays: vec![IntegrationStatusPhaseDisplay::Real],
3962            per_slot_training_phase: false,
3963            target_relative_accuracy: None,
3964            target_absolute_accuracy: None,
3965            show_statistics: true,
3966            show_max_weight_details: true,
3967            show_top_discrete_grid: false,
3968            show_discrete_contributions_sum: false,
3969            contribution_sort: ContributionSortMode::Error,
3970            show_max_weight_info_for_discrete_bins: false,
3971        }
3972    }
3973
3974    fn default_tabled_options() -> TabledRenderOptions {
3975        TabledRenderOptions {
3976            max_table_width: DEFAULT_MAX_SHARED_TABLE_WIDTH,
3977            show_statistics: true,
3978            show_max_weight_details: true,
3979            show_top_discrete_grid: false,
3980            show_discrete_contributions_sum: false,
3981            show_max_weight_info_for_discrete_bins: false,
3982        }
3983    }
3984
3985    fn tabled_options_for_view(view_options: &IntegrationStatusViewOptions) -> TabledRenderOptions {
3986        TabledRenderOptions {
3987            show_statistics: view_options.show_statistics,
3988            show_max_weight_details: view_options.show_max_weight_details,
3989            show_top_discrete_grid: view_options.show_top_discrete_grid,
3990            show_discrete_contributions_sum: view_options.show_discrete_contributions_sum,
3991            show_max_weight_info_for_discrete_bins: view_options
3992                .show_max_weight_info_for_discrete_bins,
3993            ..default_tabled_options()
3994        }
3995    }
3996
3997    #[test]
3998    fn correlated_core_iteration_state_populates_slot_statistics() {
3999        let settings_a = RuntimeSettings::default();
4000        let settings_b = RuntimeSettings::default();
4001        let integrand_a = Integrand::UnitVolume(UnitVolumeIntegrand::new(
4002            settings_a.clone(),
4003            UnitVolumeSettings { n_3d_momenta: 1 },
4004        ));
4005        let integrand_b = Integrand::UnitVolume(UnitVolumeIntegrand::new(
4006            settings_b.clone(),
4007            UnitVolumeSettings { n_3d_momenta: 1 },
4008        ));
4009        let sampling_grid_template = SamplingSlotState::from_integrand(&integrand_a).grid;
4010        let mut core_state = CoreIterationState::new(
4011            vec![integrand_a, integrand_b],
4012            SamplingCorrelationMode::Correlated,
4013            &[sampling_grid_template],
4014            1337,
4015            0,
4016            8,
4017        );
4018        let model = Model::default();
4019        let slot_settings = [&settings_a, &settings_b];
4020        let slot_models = [&model, &model];
4021        let current_max_evals = [Complex::new(F(0.0), F(0.0)), Complex::new(F(0.0), F(0.0))];
4022
4023        let processed = core_state
4024            .evaluate_chunk(&slot_settings, &slot_models, 0, &current_max_evals, 8)
4025            .expect("correlated chunk evaluation should succeed");
4026
4027        assert_eq!(processed, 8);
4028        assert!(
4029            core_state
4030                .slot_stats
4031                .iter()
4032                .all(|stats| stats.snapshot().num_evals == processed),
4033            "{:?}",
4034            core_state
4035                .slot_stats
4036                .iter()
4037                .map(|stats| stats.snapshot().num_evals)
4038                .collect_vec()
4039        );
4040    }
4041
4042    fn render_update(request: StatusUpdateBuildRequest<'_>) -> String {
4043        let tabled_options = tabled_options_for_view(request.render_options);
4044        render_status_update_tabled(&build_status_update(request), &tabled_options)
4045    }
4046
4047    fn render_ratatui_update(
4048        request: StatusUpdateBuildRequest<'_>,
4049        configure: impl FnOnce(&mut RatatuiDashboardState),
4050    ) -> String {
4051        let mut dashboard = RatatuiDashboardState::new();
4052        dashboard.update(build_status_update(request));
4053        configure(&mut dashboard);
4054
4055        let backend = TestBackend::new(180, 48);
4056        let mut terminal = Terminal::new(backend).expect("test terminal");
4057        terminal
4058            .draw(|frame| dashboard.draw(frame))
4059            .expect("ratatui draw");
4060
4061        let buffer = terminal.backend().buffer();
4062        (0..buffer.area.height)
4063            .map(|y| {
4064                let mut line = String::new();
4065                for x in 0..buffer.area.width {
4066                    line.push_str(buffer[(x, y)].symbol());
4067                }
4068                line.trim_end().to_string()
4069            })
4070            .collect::<Vec<_>>()
4071            .join("\n")
4072    }
4073
4074    #[test]
4075    fn max_weight_details_table_renders_titled_table_without_wrapping_parentheses() {
4076        let mut accumulator = ComplexAccumulator::new();
4077        accumulator.re.max_eval_positive = F(2.3840672847728);
4078        accumulator.re.max_eval_positive_xs = Some(Sample::Discrete(
4079            F(1.0),
4080            0,
4081            Some(Box::new(Sample::Discrete(
4082                F(1.0),
4083                0,
4084                Some(Box::new(Sample::Continuous(
4085                    F(1.0),
4086                    vec![
4087                        F(0.9695746085826609),
4088                        F(0.5327714835649985),
4089                        F(0.003410284786539597),
4090                    ],
4091                ))),
4092            ))),
4093        ));
4094
4095        let mut state = IntegrationState::new_from_settings(
4096            SamplingCorrelationMode::Correlated,
4097            vec![SamplingSlotState::new(
4098                Grid::Continuous(ContinuousGrid::new(1, 64, 100, None, false)),
4099                Vec::new(),
4100            )],
4101            vec![SlotMeta {
4102                process_name: "proc".to_string(),
4103                integrand_name: "itg".to_string(),
4104            }],
4105            None,
4106            None,
4107            None,
4108            vec![None],
4109        );
4110        state.all_integrals = vec![accumulator];
4111        let view_options = IntegrationStatusViewOptions {
4112            show_statistics: false,
4113            show_top_discrete_grid: false,
4114            show_discrete_contributions_sum: false,
4115            ..default_view_options()
4116        };
4117        let rendered = render_status_update_tabled(
4118            &build_status_update(
4119                StatusUpdateBuildRequest::new(
4120                    IntegrationStatusKind::Iteration,
4121                    &state,
4122                    &[None],
4123                    &view_options,
4124                )
4125                .with_timing(
4126                    1,
4127                    Duration::from_secs(0),
4128                    Duration::from_secs(0),
4129                    0,
4130                    0,
4131                    0,
4132                ),
4133            ),
4134            &default_tabled_options(),
4135        );
4136
4137        assert!(rendered.contains("Maximum weight details"), "{rendered}");
4138        assert!(rendered.contains("Integrand"), "{rendered}");
4139        assert!(rendered.contains("Max eval"), "{rendered}");
4140        assert!(rendered.contains("Max eval coordinates"), "{rendered}");
4141        assert!(rendered.contains("proc@itg"), "{rendered}");
4142        assert!(rendered.contains("re [+]"), "{rendered}");
4143        assert!(rendered.contains("idx: 0, idx: 0, xs: [ "), "{rendered}");
4144        assert!(rendered.contains("e-01"), "{rendered}");
4145        assert!(rendered.contains("e-03 ]"), "{rendered}");
4146        assert!(!rendered.contains(", 0.532"), "{rendered}");
4147        assert!(!rendered.contains("( graph:"), "{rendered}");
4148    }
4149
4150    #[test]
4151    fn iteration_status_block_uses_compact_header_and_optional_statistics() {
4152        let state = make_integration_state();
4153        let view_options = IntegrationStatusViewOptions {
4154            show_statistics: false,
4155            show_max_weight_details: false,
4156            ..default_view_options()
4157        };
4158        let rendered = render_update(
4159            StatusUpdateBuildRequest::new(
4160                IntegrationStatusKind::Iteration,
4161                &state,
4162                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4163                &view_options,
4164            )
4165            .with_timing(
4166                4,
4167                Duration::from_secs(1),
4168                Duration::from_secs(1),
4169                100_000,
4170                100_000,
4171                100_000,
4172            ),
4173        );
4174
4175        assert!(
4176            rendered.contains("Iteration #   1 ( completed )"),
4177            "{rendered}"
4178        );
4179        assert!(
4180            rendered.contains("# samples per iteration = 100.00K # samples total = 100.00K"),
4181            "{rendered}"
4182        );
4183        assert!(rendered.contains("/sample/core (4 cores)"), "{rendered}");
4184        assert!(rendered.contains("Contribution"), "{rendered}");
4185        assert!(rendered.contains("proc_a@itg_a"), "{rendered}");
4186        assert!(rendered.contains("proc_b@itg_b"), "{rendered}");
4187        assert!(rendered.contains("+7.5(9.8)e-5"), "{rendered}");
4188        assert!(rendered.contains("Δ = 0.255σ"), "{rendered}");
4189        assert!(!rendered.contains("Integration statistics"), "{rendered}");
4190        let lines = rendered.lines().collect::<Vec<_>>();
4191        assert_eq!(lines[1].matches('│').count(), 2, "{rendered}");
4192        assert!(lines[2].contains('┬'), "{rendered}");
4193        assert!(
4194            lines.last().is_some_and(|line| line.contains('┴')),
4195            "{rendered}"
4196        );
4197    }
4198
4199    #[test]
4200    fn iteration_status_block_omits_delta_columns_when_no_target_is_provided() {
4201        let state = make_integration_state();
4202        let view_options = IntegrationStatusViewOptions {
4203            show_statistics: true,
4204            show_max_weight_details: false,
4205            ..default_view_options()
4206        };
4207        let rendered = render_update(
4208            StatusUpdateBuildRequest::new(
4209                IntegrationStatusKind::Iteration,
4210                &state,
4211                &[None, None],
4212                &view_options,
4213            )
4214            .with_timing(
4215                4,
4216                Duration::from_secs(1),
4217                Duration::from_secs(1),
4218                100_000,
4219                100_000,
4220                100_000,
4221            ),
4222        );
4223
4224        assert!(!rendered.contains("Δ [σ]"), "{rendered}");
4225        assert!(!rendered.contains("Δ ="), "{rendered}");
4226        assert!(rendered.contains("Integration statistics"), "{rendered}");
4227        assert!(rendered.contains("mwi"), "{rendered}");
4228    }
4229
4230    #[test]
4231    fn tabled_statistics_panel_uses_global_scope_label() {
4232        let state = make_integration_state();
4233        let view_options = IntegrationStatusViewOptions {
4234            show_statistics: true,
4235            show_max_weight_details: false,
4236            ..default_view_options()
4237        };
4238        let rendered = render_update(
4239            StatusUpdateBuildRequest::new(
4240                IntegrationStatusKind::Iteration,
4241                &state,
4242                &[None, None],
4243                &view_options,
4244            )
4245            .with_timing(
4246                4,
4247                Duration::from_secs(1),
4248                Duration::from_secs(1),
4249                100_000,
4250                100_000,
4251                100_000,
4252            ),
4253        );
4254
4255        assert!(
4256            rendered.contains("Integration statistics [global]"),
4257            "{rendered}"
4258        );
4259    }
4260
4261    #[test]
4262    fn live_iteration_status_block_uses_progress_header() {
4263        let mut state = make_integration_state();
4264        state.iter = 2;
4265        state.num_points = 125_000;
4266        let view_options = IntegrationStatusViewOptions {
4267            show_statistics: false,
4268            show_max_weight_details: false,
4269            ..default_view_options()
4270        };
4271        let rendered = render_update(
4272            StatusUpdateBuildRequest::new(
4273                IntegrationStatusKind::Live,
4274                &state,
4275                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4276                &view_options,
4277            )
4278            .with_timing(
4279                4,
4280                Duration::from_secs(1),
4281                Duration::from_secs(1),
4282                25_000,
4283                125_000,
4284                125_000,
4285            )
4286            .with_live_progress(Some(status_update::LiveIterationProgress {
4287                completed_points: 25_000,
4288                target_points: 100_000,
4289            })),
4290        );
4291
4292        assert!(
4293            rendered.contains("Iteration #   2 ( running   )"),
4294            "{rendered}"
4295        );
4296        assert!(
4297            rendered.contains("Iteration progress 25.00K/100.00K"),
4298            "{rendered}"
4299        );
4300        assert!(rendered.contains("25.0%"), "{rendered}");
4301        assert!(rendered.contains("# samples total = 125.00K"), "{rendered}");
4302    }
4303
4304    #[test]
4305    fn iteration_status_block_shows_discrete_contributions_and_discrete_max_weights() {
4306        let state = make_discrete_integration_state();
4307        let view_options = IntegrationStatusViewOptions {
4308            show_statistics: false,
4309            show_max_weight_details: true,
4310            show_top_discrete_grid: true,
4311            show_discrete_contributions_sum: true,
4312            contribution_sort: ContributionSortMode::Index,
4313            show_max_weight_info_for_discrete_bins: true,
4314            ..default_view_options()
4315        };
4316        let rendered = render_update(
4317            StatusUpdateBuildRequest::new(
4318                IntegrationStatusKind::Iteration,
4319                &state,
4320                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4321                &view_options,
4322            )
4323            .with_timing(
4324                4,
4325                Duration::from_secs(2),
4326                Duration::from_secs(2),
4327                110_000,
4328                210_000,
4329                210_000,
4330            ),
4331        );
4332
4333        assert!(rendered.contains("Sum"), "{rendered}");
4334        assert!(rendered.contains("Contribution (idx=graph)"), "{rendered}");
4335        assert!(
4336            !rendered.contains("│             (idx=graph)             │"),
4337            "{rendered}"
4338        );
4339        assert!(rendered.contains("#0: GL0"), "{rendered}");
4340        assert!(rendered.contains("75.0%"), "{rendered}");
4341        assert!(rendered.contains("25.0%"), "{rendered}");
4342        assert!(
4343            rendered.contains("Maximum weight details by discrete bin"),
4344            "{rendered}"
4345        );
4346        assert!(
4347            rendered.contains("xs: [ 2.5000000000000000e-01 ]"),
4348            "{rendered}"
4349        );
4350    }
4351
4352    #[test]
4353    fn grouped_graph_descriptions_include_all_group_members() {
4354        let description =
4355            graph_group_description(["GL0".to_string(), "GL1".to_string(), "GL2".to_string()]);
4356
4357        assert_eq!(description, "[GL0,GL1,GL2]");
4358    }
4359
4360    #[test]
4361    fn explicit_single_graph_subset_monitors_the_root_graph_axis() {
4362        let grid = Grid::Discrete(DiscreteGrid::new(
4363            vec![Some(Grid::Continuous(ContinuousGrid::new(
4364                1, 64, 100, None, false,
4365            )))],
4366            F(10.0),
4367            false,
4368        ));
4369        let labels = vec!["graph".to_string()];
4370
4371        assert!(monitored_discrete_layout(&grid, &labels, false).is_none());
4372        assert_eq!(
4373            monitored_discrete_layout(&grid, &labels, true),
4374            Some((Vec::new(), "graph".to_string(), 1))
4375        );
4376    }
4377
4378    #[test]
4379    fn tabled_single_graph_subset_keeps_the_graph_row() {
4380        let state = make_single_graph_discrete_integration_state();
4381        let view_options = IntegrationStatusViewOptions {
4382            show_statistics: false,
4383            show_max_weight_details: false,
4384            show_top_discrete_grid: true,
4385            contribution_sort: ContributionSortMode::Index,
4386            ..default_view_options()
4387        };
4388        let rendered = render_update(StatusUpdateBuildRequest::new(
4389            IntegrationStatusKind::Iteration,
4390            &state,
4391            &[None, None],
4392            &view_options,
4393        ));
4394
4395        assert!(rendered.contains("Contribution (idx=graph)"), "{rendered}");
4396        assert!(rendered.contains("#0: GL22"), "{rendered}");
4397        assert!(!rendered.contains("GL1"), "{rendered}");
4398    }
4399
4400    #[test]
4401    fn iteration_status_block_hides_spanned_metadata_header_separators() {
4402        let state = make_discrete_integration_state();
4403        let view_options = IntegrationStatusViewOptions {
4404            show_statistics: false,
4405            show_max_weight_details: false,
4406            show_top_discrete_grid: true,
4407            show_discrete_contributions_sum: true,
4408            contribution_sort: ContributionSortMode::Index,
4409            show_max_weight_info_for_discrete_bins: false,
4410            ..default_view_options()
4411        };
4412        let rendered = render_update(
4413            StatusUpdateBuildRequest::new(
4414                IntegrationStatusKind::Iteration,
4415                &state,
4416                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4417                &view_options,
4418            )
4419            .with_timing(
4420                4,
4421                Duration::from_secs(2),
4422                Duration::from_secs(2),
4423                110_000,
4424                210_000,
4425                210_000,
4426            ),
4427        );
4428
4429        let header_line = rendered
4430            .lines()
4431            .find(|line| line.contains("χ²/dof") && line.contains("mwi"))
4432            .expect("expected spanned metadata header line");
4433
4434        let chi_to_mwi =
4435            &header_line[header_line.find("χ²/dof").unwrap()..header_line.find("mwi").unwrap()];
4436        assert!(!chi_to_mwi.contains('│'), "{rendered}");
4437
4438        let delta_sigma_to_percent =
4439            &header_line[header_line.find("Δ [σ]").unwrap()..header_line.find("Δ [%]").unwrap()];
4440        assert!(!delta_sigma_to_percent.contains('│'), "{rendered}");
4441    }
4442
4443    #[test]
4444    fn integration_result_always_contains_first_non_trivial_discrete_breakdown() {
4445        let state = make_discrete_integration_state();
4446        let result =
4447            build_integration_result(&state, &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None]);
4448
4449        let slot = result
4450            .slot("proc_a@itg_a")
4451            .expect("discrete slot must be present");
4452        let breakdown = slot
4453            .grid_breakdown
4454            .re
4455            .as_ref()
4456            .expect("discrete breakdown must be persisted");
4457
4458        assert_eq!(breakdown.axis_label, "graph");
4459        assert!(breakdown.fixed_coordinates.is_empty());
4460        assert_eq!(breakdown.entries.len(), 2);
4461        assert_eq!(breakdown.entries[0].bin_index, 0);
4462        assert_eq!(breakdown.entries[0].bin_label.as_deref(), Some("GL0"));
4463        assert_eq!(breakdown.entries[0].pdf, F(0.75));
4464        assert_eq!(breakdown.entries[0].processed_samples, 150);
4465        assert_eq!(breakdown.entries[1].bin_index, 1);
4466        assert_eq!(breakdown.entries[1].bin_label.as_deref(), Some("GL1"));
4467        assert_eq!(breakdown.entries[1].pdf, F(0.25));
4468        assert_eq!(breakdown.entries[1].processed_samples, 50);
4469    }
4470
4471    #[test]
4472    fn orientation_descriptions_use_colored_signs() {
4473        let mut state = make_discrete_integration_state();
4474        state.first_non_trivial_discrete_label = Some("orientation".to_string());
4475        state.first_non_trivial_discrete_bin_descriptions =
4476            Some(vec!["+-0".to_string(), "0++".to_string()]);
4477        for metadata in &mut state.slot_first_non_trivial_discrete_breakdown_metadata {
4478            if let Some(metadata) = metadata.as_mut() {
4479                metadata.axis_label = "orientation".to_string();
4480                metadata.bin_labels = vec!["+-0".to_string(), "0++".to_string()];
4481            }
4482        }
4483
4484        control::set_override(true);
4485        let expected_plus = "+".green().bold().to_string();
4486        let expected_minus = "-".red().bold().to_string();
4487
4488        let view_options = IntegrationStatusViewOptions {
4489            show_statistics: false,
4490            show_max_weight_details: false,
4491            show_top_discrete_grid: true,
4492            show_discrete_contributions_sum: false,
4493            contribution_sort: ContributionSortMode::Index,
4494            show_max_weight_info_for_discrete_bins: false,
4495            ..default_view_options()
4496        };
4497        let ansi = render_tabled::render_status_update(
4498            &build_status_update(
4499                StatusUpdateBuildRequest::new(
4500                    IntegrationStatusKind::Iteration,
4501                    &state,
4502                    &[None, None],
4503                    &view_options,
4504                )
4505                .with_timing(
4506                    1,
4507                    Duration::from_secs(0),
4508                    Duration::from_secs(0),
4509                    0,
4510                    0,
4511                    0,
4512                ),
4513            ),
4514            &tabled_options_for_view(&view_options),
4515        );
4516        control::set_override(false);
4517
4518        assert!(ansi.contains(&expected_plus), "{ansi}");
4519        assert!(ansi.contains(&expected_minus), "{ansi}");
4520        assert!(ansi.contains("0"), "{ansi}");
4521    }
4522
4523    #[test]
4524    fn mismatched_bin_descriptions_fall_back_to_indices_with_warning() {
4525        let (descriptions, warning) = coalesce_first_non_trivial_discrete_bin_descriptions(
4526            "graph",
4527            &[
4528                (
4529                    "proc_a@itg_a".to_string(),
4530                    vec!["GL0".to_string(), "GL1".to_string()],
4531                ),
4532                (
4533                    "proc_b@itg_b".to_string(),
4534                    vec!["GX0".to_string(), "GX1".to_string()],
4535                ),
4536            ],
4537        );
4538
4539        assert!(descriptions.is_none());
4540        let warning = warning.expect("mismatch should produce a warning");
4541        assert!(warning.contains("graph"), "{warning}");
4542        assert!(warning.contains("proc_a@itg_a"), "{warning}");
4543        assert!(warning.contains("proc_b@itg_b"), "{warning}");
4544    }
4545
4546    #[test]
4547    fn format_max_eval_sample_keeps_full_discrete_coordinates() {
4548        let axis_labels = vec!["graph".to_string(), "LMB channel".to_string()];
4549        let full_sample = Sample::Discrete(
4550            F(1.0),
4551            0,
4552            Some(Box::new(Sample::Discrete(
4553                F(1.0),
4554                1,
4555                Some(Box::new(Sample::Continuous(F(1.0), vec![F(0.25)]))),
4556            ))),
4557        );
4558        let nested_sample = Sample::Discrete(
4559            F(1.0),
4560            1,
4561            Some(Box::new(Sample::Continuous(F(1.0), vec![F(0.75)]))),
4562        );
4563
4564        assert_eq!(
4565            display::format_max_eval_sample(&full_sample, &axis_labels, &[]),
4566            "graph: 0, LMB channel: 1, xs: [ 2.5000000000000000e-01 ]"
4567        );
4568        assert_eq!(
4569            display::format_max_eval_sample(&nested_sample, &axis_labels, &[0]),
4570            "graph: 0, LMB channel: 1, xs: [ 7.5000000000000000e-01 ]"
4571        );
4572    }
4573
4574    #[test]
4575    fn format_max_eval_sample_wraps_long_coordinate_lists() {
4576        let axis_labels = vec!["graph".to_string(), "orientation".to_string()];
4577        let sample = Sample::Discrete(
4578            F(1.0),
4579            0,
4580            Some(Box::new(Sample::Discrete(
4581                F(1.0),
4582                9,
4583                Some(Box::new(Sample::Continuous(
4584                    F(1.0),
4585                    vec![
4586                        F(0.125),
4587                        F(0.25),
4588                        F(0.375),
4589                        F(0.5),
4590                        F(0.625),
4591                        F(0.75),
4592                        F(0.875),
4593                    ],
4594                ))),
4595            ))),
4596        );
4597
4598        assert_eq!(
4599            display::format_max_eval_sample(&sample, &axis_labels, &[]),
4600            "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 ]"
4601        );
4602    }
4603
4604    #[test]
4605    fn discrete_bin_prefixes_use_fixed_width_within_each_range() {
4606        assert_eq!(status_update::format_discrete_bin_prefix(2, 99), "#2: ");
4607        assert_eq!(status_update::format_discrete_bin_prefix(12, 99), "#12:");
4608        assert_eq!(status_update::format_discrete_bin_prefix(2, 999), "#2:  ");
4609        assert_eq!(status_update::format_discrete_bin_prefix(12, 999), "#12: ");
4610        assert_eq!(status_update::format_discrete_bin_prefix(123, 999), "#123:");
4611        assert_eq!(
4612            status_update::format_discrete_bin_prefix(2, 9_999),
4613            "#2:   "
4614        );
4615        assert_eq!(
4616            status_update::format_discrete_bin_prefix(1234, 9_999),
4617            "#1234:"
4618        );
4619    }
4620
4621    #[test]
4622    fn final_summary_omits_discrete_max_weight_block_when_disabled() {
4623        let state = make_discrete_integration_state();
4624        let rendered = render_saved_integration_summary(
4625            &state,
4626            &[None, None],
4627            &IntegrationStatusViewOptions {
4628                show_statistics: true,
4629                show_max_weight_details: true,
4630                show_top_discrete_grid: false,
4631                show_discrete_contributions_sum: false,
4632                contribution_sort: ContributionSortMode::Index,
4633                show_max_weight_info_for_discrete_bins: false,
4634                ..default_view_options()
4635            },
4636            &default_tabled_options(),
4637        );
4638
4639        assert!(rendered.contains("Maximum weight details"), "{rendered}");
4640        assert!(
4641            !rendered.contains("Maximum weight details by discrete bin"),
4642            "{rendered}"
4643        );
4644        assert!(
4645            !rendered.contains("# samples per iteration = 0"),
4646            "{rendered}"
4647        );
4648    }
4649
4650    #[test]
4651    fn results_output_summary_renders_tabled_workspace_rows() {
4652        let workspace = Path::new("/tmp/gl_workspace");
4653        let rendered = render_results_output_summary_table(Some(workspace), &[], &[])
4654            .expect("workspace summary should render");
4655
4656        assert!(
4657            rendered.contains("Integration results emitted"),
4658            "{rendered}"
4659        );
4660        assert!(rendered.contains("type"), "{rendered}");
4661        assert!(rendered.contains("path"), "{rendered}");
4662        assert!(rendered.contains("workspace path"), "{rendered}");
4663        assert!(rendered.contains("/tmp/gl_workspace"), "{rendered}");
4664        assert!(rendered.contains("results"), "{rendered}");
4665        assert!(rendered.contains("integration_result.json"), "{rendered}");
4666        assert!(rendered.contains("├"), "{rendered}");
4667        assert!(!rendered.contains("iteration snapshots"), "{rendered}");
4668        assert!(!rendered.contains("iter_*"), "{rendered}");
4669    }
4670
4671    #[test]
4672    fn results_output_summary_skips_missing_observable_outputs() {
4673        let settings = RuntimeSettings::default();
4674        let slot = IntegrationSlot::new(
4675            SlotMeta {
4676                process_name: "proc".to_string(),
4677                integrand_name: "default".to_string(),
4678            },
4679            settings.clone(),
4680            Model::default(),
4681            Integrand::UnitVolume(UnitVolumeIntegrand::new(
4682                settings,
4683                UnitVolumeSettings { n_3d_momenta: 1 },
4684            )),
4685            None,
4686        );
4687        let rendered = render_results_output_summary_table(
4688            None,
4689            &[slot],
4690            &[vec![PathBuf::from(
4691                "/tmp/definitely_missing_observables_final.json",
4692            )]],
4693        );
4694
4695        assert!(rendered.is_none(), "{rendered:?}");
4696    }
4697
4698    #[test]
4699    fn results_output_summary_lists_all_emitted_observable_files() {
4700        let base = std::env::temp_dir().join(format!(
4701            "gammaloop_results_output_summary_{}",
4702            std::process::id()
4703        ));
4704        if base.exists() {
4705            let _ = fs::remove_dir_all(&base);
4706        }
4707        let workspace = base.join("workspace");
4708        let slot_dir = workspace.join("integrands").join("proc@default");
4709        fs::create_dir_all(&slot_dir).unwrap();
4710        fs::write(workspace_result_snapshot_path(&workspace), b"{}").unwrap();
4711        let json_path = slot_dir.join("observables_final.json");
4712        let hwu_path = slot_dir.join("observables_final.hwu");
4713        fs::write(&json_path, b"{}").unwrap();
4714        fs::write(&hwu_path, b"# hwu\n").unwrap();
4715
4716        let settings = RuntimeSettings::default();
4717        let slot = IntegrationSlot::new(
4718            SlotMeta {
4719                process_name: "proc".to_string(),
4720                integrand_name: "default".to_string(),
4721            },
4722            settings.clone(),
4723            Model::default(),
4724            Integrand::UnitVolume(UnitVolumeIntegrand::new(
4725                settings,
4726                UnitVolumeSettings { n_3d_momenta: 1 },
4727            )),
4728            None,
4729        );
4730        let rendered = render_results_output_summary_table(
4731            Some(&workspace),
4732            &[slot],
4733            &[vec![hwu_path.clone(), json_path.clone()]],
4734        )
4735        .expect("summary should render");
4736
4737        assert!(rendered.contains("proc@default (hwu)"), "{rendered}");
4738        assert!(rendered.contains("proc@default (json)"), "{rendered}");
4739        assert!(rendered.contains("observables_final.hwu"), "{rendered}");
4740        assert!(rendered.contains("observables_final.json"), "{rendered}");
4741
4742        fs::remove_dir_all(&base).unwrap();
4743    }
4744
4745    #[test]
4746    fn ratatui_overview_shows_eta_and_all_slot_metrics() {
4747        let state = make_integration_state();
4748        let view_options = IntegrationStatusViewOptions {
4749            show_statistics: true,
4750            show_max_weight_details: false,
4751            ..default_view_options()
4752        };
4753        let rendered = render_ratatui_update(
4754            StatusUpdateBuildRequest::new(
4755                IntegrationStatusKind::Live,
4756                &state,
4757                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4758                &view_options,
4759            )
4760            .with_timing(
4761                4,
4762                Duration::from_secs(15),
4763                Duration::from_secs(10),
4764                25_000,
4765                125_000,
4766                125_000,
4767            )
4768            .with_live_progress(Some(status_update::LiveIterationProgress {
4769                completed_points: 25_000,
4770                target_points: 100_000,
4771            })),
4772            |_| {},
4773        );
4774
4775        assert!(rendered.contains("Iteration progress"), "{rendered}");
4776        assert!(rendered.contains("ETA 30s"), "{rendered}");
4777        assert!(rendered.contains("25.00K / 100.00K (25.0%)"), "{rendered}");
4778        assert!(rendered.contains("# samples total 125.00K"), "{rendered}");
4779        assert!(rendered.contains("#samples/s"), "{rendered}");
4780        assert!(
4781            rendered.contains("480µs /sample/core (4 cores)"),
4782            "{rendered}"
4783        );
4784        assert!(rendered.contains("/sample/core (4 cores)"), "{rendered}");
4785        assert!(rendered.contains("Integrands"), "{rendered}");
4786        assert!(rendered.contains("Focused integrand"), "{rendered}");
4787        assert!(rendered.contains("Results summary"), "{rendered}");
4788        assert!(
4789            rendered.contains("Integration statistics [global]"),
4790            "{rendered}"
4791        );
4792        assert!(rendered.contains("% err"), "{rendered}");
4793        assert!(rendered.contains("chi^2"), "{rendered}");
4794        assert!(rendered.contains("m.w.i"), "{rendered}");
4795        assert!(
4796            rendered.contains("Timing composition [global]"),
4797            "{rendered}"
4798        );
4799        assert!(rendered.contains("Precision mix [global]"), "{rendered}");
4800    }
4801
4802    #[test]
4803    fn live_status_updates_use_current_batch_statistics_in_bottom_panels() {
4804        let state = make_integration_state();
4805        let slots = make_preview_test_slots(&state.slot_metas);
4806        let core_state = make_preview_test_core_state(&state);
4807        let preview_state = build_preview_integration_state(
4808            &state,
4809            &slots,
4810            4,
4811            core_state.completed_points,
4812            1.5,
4813            &[core_state],
4814        );
4815        let view_options = IntegrationStatusViewOptions {
4816            show_statistics: true,
4817            show_max_weight_details: false,
4818            ..default_view_options()
4819        };
4820
4821        let initial_update = build_status_update(
4822            StatusUpdateBuildRequest::new(
4823                IntegrationStatusKind::Live,
4824                &state,
4825                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4826                &view_options,
4827            )
4828            .with_timing(
4829                4,
4830                Duration::from_secs(1),
4831                Duration::from_millis(400),
4832                0,
4833                state.num_points,
4834                state.num_points,
4835            )
4836            .with_live_progress(Some(status_update::LiveIterationProgress {
4837                completed_points: 0,
4838                target_points: 100,
4839            })),
4840        );
4841        let preview_update = build_status_update(
4842            StatusUpdateBuildRequest::new(
4843                IntegrationStatusKind::Live,
4844                &preview_state,
4845                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4846                &view_options,
4847            )
4848            .with_timing(
4849                4,
4850                Duration::from_millis(1500),
4851                Duration::from_millis(900),
4852                12,
4853                preview_state.num_points,
4854                preview_state.num_points,
4855            )
4856            .with_live_progress(Some(status_update::LiveIterationProgress {
4857                completed_points: 12,
4858                target_points: 100,
4859            })),
4860        );
4861
4862        let initial_statistics = initial_update
4863            .statistics
4864            .as_ref()
4865            .expect("statistics should be present");
4866        let preview_statistics = preview_update
4867            .statistics
4868            .as_ref()
4869            .expect("statistics should be present");
4870        let initial_snapshot = initial_statistics.global_snapshot();
4871        let preview_snapshot = preview_statistics.global_snapshot();
4872
4873        assert!(preview_snapshot.num_evals > initial_snapshot.num_evals);
4874        assert!(preview_snapshot.generated_event_count > initial_snapshot.generated_event_count);
4875        assert!(preview_snapshot.accepted_event_count > initial_snapshot.accepted_event_count);
4876
4877        let initial_timing_mix = initial_statistics
4878            .timing_mix_segments(status_update::StatisticsScope::Global)
4879            .iter()
4880            .map(|segment| segment.percentage)
4881            .collect_vec();
4882        let preview_timing_mix = preview_statistics
4883            .timing_mix_segments(status_update::StatisticsScope::Global)
4884            .iter()
4885            .map(|segment| segment.percentage)
4886            .collect_vec();
4887        assert_ne!(preview_timing_mix, initial_timing_mix);
4888
4889        let initial_precision_mix = initial_statistics
4890            .precision_mix_segments(status_update::StatisticsScope::Global)
4891            .iter()
4892            .map(|segment| segment.percentage)
4893            .collect_vec();
4894        let preview_precision_mix = preview_statistics
4895            .precision_mix_segments(status_update::StatisticsScope::Global)
4896            .iter()
4897            .map(|segment| segment.percentage)
4898            .collect_vec();
4899        assert_ne!(preview_precision_mix, initial_precision_mix);
4900        assert!(
4901            preview_precision_mix
4902                .iter()
4903                .any(|percentage| (*percentage - 50.0).abs() < f64::EPSILON),
4904            "{preview_precision_mix:?}"
4905        );
4906
4907        let preview_render = render_ratatui_update(
4908            StatusUpdateBuildRequest::new(
4909                IntegrationStatusKind::Live,
4910                &preview_state,
4911                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4912                &view_options,
4913            )
4914            .with_timing(
4915                4,
4916                Duration::from_millis(1500),
4917                Duration::from_millis(900),
4918                12,
4919                preview_state.num_points,
4920                preview_state.num_points,
4921            )
4922            .with_live_progress(Some(status_update::LiveIterationProgress {
4923                completed_points: 12,
4924                target_points: 100,
4925            })),
4926            |_| {},
4927        );
4928        let preview_total_time = crate::utils::format_evaluation_time_from_f64(
4929            preview_snapshot.average_total_time_seconds,
4930        );
4931        assert!(
4932            preview_render.contains(&preview_total_time),
4933            "{preview_render}"
4934        );
4935        assert!(
4936            preview_render.contains("Precision mix [global]"),
4937            "{preview_render}"
4938        );
4939        assert!(
4940            preview_render.contains("Timing composition [global]"),
4941            "{preview_render}"
4942        );
4943    }
4944
4945    #[test]
4946    fn ratatui_statistics_panels_can_toggle_to_focused_slot_scope() {
4947        let state = make_integration_state();
4948        let view_options = IntegrationStatusViewOptions {
4949            show_statistics: true,
4950            show_max_weight_details: false,
4951            ..default_view_options()
4952        };
4953        let rendered = render_ratatui_update(
4954            StatusUpdateBuildRequest::new(
4955                IntegrationStatusKind::Live,
4956                &state,
4957                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4958                &view_options,
4959            )
4960            .with_timing(
4961                4,
4962                Duration::from_secs(15),
4963                Duration::from_secs(10),
4964                25_000,
4965                125_000,
4966                125_000,
4967            ),
4968            |dashboard| {
4969                dashboard.focus_next_slot();
4970                dashboard.toggle_statistics_scope();
4971            },
4972        );
4973
4974        assert!(
4975            rendered.contains("Integration statistics [proc_b@itg_b]"),
4976            "{rendered}"
4977        );
4978        assert!(
4979            rendered.contains("Timing composition [proc_b@itg_b]"),
4980            "{rendered}"
4981        );
4982        assert!(
4983            rendered.contains("Precision mix [proc_b@itg_b]"),
4984            "{rendered}"
4985        );
4986    }
4987
4988    #[test]
4989    fn slot_scoped_statistics_rows_hide_shared_integrator_overhead() {
4990        let state = make_integration_state();
4991        let update = build_status_update(
4992            StatusUpdateBuildRequest::new(
4993                IntegrationStatusKind::Iteration,
4994                &state,
4995                &[None, None],
4996                &default_view_options(),
4997            )
4998            .with_timing(
4999                4,
5000                Duration::from_secs(1),
5001                Duration::from_secs(1),
5002                100_000,
5003                100_000,
5004                100_000,
5005            ),
5006        );
5007        let statistics = update.statistics.expect("statistics section should exist");
5008        let rows = statistics.table_rows(status_update::StatisticsScope::Slot(1));
5009        let integrator_entry = rows[2]
5010            .entries
5011            .last()
5012            .expect("integrator entry should be present");
5013
5014        assert_eq!(integrator_entry.label.to_plain_string(), "integrator");
5015        assert_eq!(integrator_entry.value.to_plain_string(), "N/A");
5016    }
5017
5018    #[test]
5019    fn ratatui_overview_chart_phase_toggle_updates_convergence_label() {
5020        let state = make_integration_state();
5021        let view_options = IntegrationStatusViewOptions {
5022            show_statistics: false,
5023            show_max_weight_details: false,
5024            ..default_view_options()
5025        };
5026        let rendered = render_ratatui_update(
5027            StatusUpdateBuildRequest::new(
5028                IntegrationStatusKind::Live,
5029                &state,
5030                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5031                &view_options,
5032            )
5033            .with_timing(
5034                4,
5035                Duration::from_secs(15),
5036                Duration::from_secs(10),
5037                25_000,
5038                125_000,
5039                125_000,
5040            )
5041            .with_live_progress(Some(status_update::LiveIterationProgress {
5042                completed_points: 25_000,
5043                target_points: 100_000,
5044            })),
5045            |dashboard| dashboard.toggle_chart_component(),
5046        );
5047
5048        assert!(
5049            rendered.contains("Convergence : imag (not selected for training)"),
5050            "{rendered}"
5051        );
5052    }
5053
5054    #[test]
5055    fn ratatui_integrand_targets_are_trimmed_for_display() {
5056        let state = make_integration_state();
5057        let view_options = IntegrationStatusViewOptions {
5058            show_statistics: false,
5059            show_max_weight_details: false,
5060            ..default_view_options()
5061        };
5062        let rendered = render_ratatui_update(
5063            StatusUpdateBuildRequest::new(
5064                IntegrationStatusKind::Live,
5065                &state,
5066                &[
5067                    Some(Complex::new(
5068                        F(7.600000000000001e-6),
5069                        F(7.430000000000004e-5),
5070                    )),
5071                    Some(Complex::new(F(6.629999999999999e-5), F(0.0))),
5072                ],
5073                &view_options,
5074            )
5075            .with_timing(
5076                4,
5077                Duration::from_secs(15),
5078                Duration::from_secs(10),
5079                25_000,
5080                125_000,
5081                125_000,
5082            )
5083            .with_live_progress(Some(status_update::LiveIterationProgress {
5084                completed_points: 25_000,
5085                target_points: 100_000,
5086            })),
5087            |_| {},
5088        );
5089
5090        assert!(rendered.contains("trgt"), "{rendered}");
5091        assert!(rendered.contains("+7.6e-6"), "{rendered}");
5092        assert!(rendered.contains("+7.43e-5"), "{rendered}");
5093        assert!(rendered.contains("+6.63e-5"), "{rendered}");
5094        assert!(rendered.contains("+0e0"), "{rendered}");
5095    }
5096
5097    #[test]
5098    fn ratatui_overview_shows_eta_to_target_when_configured() {
5099        let state = make_integration_state();
5100        let view_options = IntegrationStatusViewOptions {
5101            target_relative_accuracy: Some(0.05),
5102            show_statistics: false,
5103            show_max_weight_details: false,
5104            ..default_view_options()
5105        };
5106        let rendered = render_ratatui_update(
5107            StatusUpdateBuildRequest::new(
5108                IntegrationStatusKind::Live,
5109                &state,
5110                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5111                &view_options,
5112            )
5113            .with_timing(
5114                4,
5115                Duration::from_secs(25),
5116                Duration::from_secs(10),
5117                25_000,
5118                125_000,
5119                125_000,
5120            )
5121            .with_live_progress(Some(status_update::LiveIterationProgress {
5122                completed_points: 25_000,
5123                target_points: 100_000,
5124            })),
5125            |_| {},
5126        );
5127
5128        assert!(rendered.contains("ETA to target"), "{rendered}");
5129        assert!(rendered.contains("(% err <= 5%)"), "{rendered}");
5130    }
5131
5132    #[test]
5133    fn eta_to_target_specification_formats_relative_and_absolute_targets() {
5134        let state = make_integration_state();
5135        let relative_view_options = IntegrationStatusViewOptions {
5136            target_relative_accuracy: Some(0.05),
5137            ..default_view_options()
5138        };
5139        let update = build_status_update(
5140            StatusUpdateBuildRequest::new(
5141                IntegrationStatusKind::Live,
5142                &state,
5143                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5144                &relative_view_options,
5145            )
5146            .with_timing(
5147                4,
5148                Duration::from_secs(25),
5149                Duration::from_secs(10),
5150                25_000,
5151                125_000,
5152                125_000,
5153            )
5154            .with_live_progress(Some(status_update::LiveIterationProgress {
5155                completed_points: 25_000,
5156                target_points: 100_000,
5157            })),
5158        );
5159        assert_eq!(
5160            update.meta.eta_to_target_specification(),
5161            Some("% err <= 5%")
5162        );
5163
5164        let relative_scientific_view_options = IntegrationStatusViewOptions {
5165            target_relative_accuracy: Some(1.0e-6),
5166            ..default_view_options()
5167        };
5168        let update = build_status_update(
5169            StatusUpdateBuildRequest::new(
5170                IntegrationStatusKind::Live,
5171                &state,
5172                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5173                &relative_scientific_view_options,
5174            )
5175            .with_timing(
5176                4,
5177                Duration::from_secs(25),
5178                Duration::from_secs(10),
5179                25_000,
5180                125_000,
5181                125_000,
5182            )
5183            .with_live_progress(Some(status_update::LiveIterationProgress {
5184                completed_points: 25_000,
5185                target_points: 100_000,
5186            })),
5187        );
5188        assert_eq!(
5189            update.meta.eta_to_target_specification(),
5190            Some("% err <= 1e-4%")
5191        );
5192
5193        let absolute_view_options = IntegrationStatusViewOptions {
5194            target_absolute_accuracy: Some(1.0e-6),
5195            ..default_view_options()
5196        };
5197        let update = build_status_update(
5198            StatusUpdateBuildRequest::new(
5199                IntegrationStatusKind::Live,
5200                &state,
5201                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5202                &absolute_view_options,
5203            )
5204            .with_timing(
5205                4,
5206                Duration::from_secs(25),
5207                Duration::from_secs(10),
5208                25_000,
5209                125_000,
5210                125_000,
5211            )
5212            .with_live_progress(Some(status_update::LiveIterationProgress {
5213                completed_points: 25_000,
5214                target_points: 100_000,
5215            })),
5216        );
5217        assert_eq!(
5218            update.meta.eta_to_target_specification(),
5219            Some("err <= 1e-6")
5220        );
5221
5222        let combined_view_options = IntegrationStatusViewOptions {
5223            target_relative_accuracy: Some(0.05),
5224            target_absolute_accuracy: Some(1.0e-6),
5225            ..default_view_options()
5226        };
5227        let update = build_status_update(
5228            StatusUpdateBuildRequest::new(
5229                IntegrationStatusKind::Live,
5230                &state,
5231                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5232                &combined_view_options,
5233            )
5234            .with_timing(
5235                4,
5236                Duration::from_secs(25),
5237                Duration::from_secs(10),
5238                25_000,
5239                125_000,
5240                125_000,
5241            )
5242            .with_live_progress(Some(status_update::LiveIterationProgress {
5243                completed_points: 25_000,
5244                target_points: 100_000,
5245            })),
5246        );
5247        assert_eq!(
5248            update.meta.eta_to_target_specification(),
5249            Some("% err <= 5% or err <= 1e-6")
5250        );
5251    }
5252
5253    #[test]
5254    fn target_accuracy_status_uses_either_absolute_or_relative_condition() {
5255        let state = make_integration_state();
5256
5257        let reached_by_absolute = status_update::evaluate_target_accuracy(
5258            &state,
5259            100_000,
5260            Duration::from_secs(10),
5261            &[None, None],
5262            IntegrationStatusPhaseDisplay::Real,
5263            None,
5264            Some(1.0e-4),
5265        );
5266        assert!(reached_by_absolute.is_reached());
5267
5268        let reached_by_relative = status_update::evaluate_target_accuracy(
5269            &state,
5270            100_000,
5271            Duration::from_secs(10),
5272            &[Some(Complex::new(F(1.0e-3), F(0.0))), None],
5273            IntegrationStatusPhaseDisplay::Real,
5274            Some(0.1),
5275            None,
5276        );
5277        assert!(reached_by_relative.is_reached());
5278    }
5279
5280    #[test]
5281    fn target_accuracy_status_requires_all_slots_to_reach_relative_target() {
5282        let mut state = make_integration_state();
5283        state.all_integrals[0].re.avg = F(1.0);
5284        state.all_integrals[0].re.err = F(1.0e-5);
5285        state.all_integrals[1].re.avg = F(1.0);
5286        state.all_integrals[1].re.err = F(2.0e-3);
5287
5288        let status = status_update::evaluate_target_accuracy(
5289            &state,
5290            100_000,
5291            Duration::from_secs(10),
5292            &[None, None],
5293            IntegrationStatusPhaseDisplay::Real,
5294            Some(1.0e-3),
5295            None,
5296        );
5297
5298        assert!(!status.is_reached());
5299        assert!(!status.relative_reached);
5300        assert!(status.eta_to_target.is_some());
5301    }
5302
5303    #[test]
5304    fn target_accuracy_status_requires_all_displayed_components_to_reach_target() {
5305        let mut state = make_integration_state();
5306        state.all_integrals[0].re.avg = F(1.0);
5307        state.all_integrals[0].re.err = F(1.0e-5);
5308        state.all_integrals[0].im.avg = F(1.0);
5309        state.all_integrals[0].im.err = F(2.0e-3);
5310
5311        let status = status_update::evaluate_target_accuracy(
5312            &state,
5313            100_000,
5314            Duration::from_secs(10),
5315            &[Some(Complex::new(F(1.0), F(1.0))), None],
5316            IntegrationStatusPhaseDisplay::Both,
5317            Some(1.0e-3),
5318            None,
5319        );
5320
5321        assert!(!status.is_reached());
5322        assert!(!status.relative_reached);
5323        assert!(status.eta_to_target.is_some());
5324    }
5325
5326    #[test]
5327    fn target_accuracy_status_reports_infinite_eta_for_unreachable_absolute_target() {
5328        let state = make_integration_state();
5329        let status = status_update::evaluate_target_accuracy(
5330            &state,
5331            100_000,
5332            Duration::from_secs(10),
5333            &[None, None],
5334            IntegrationStatusPhaseDisplay::Real,
5335            None,
5336            Some(1.0e-99),
5337        );
5338
5339        assert!(!status.is_reached());
5340        assert_eq!(status.eta_to_target, Some(Duration::MAX));
5341    }
5342
5343    #[test]
5344    fn target_accuracy_status_prefers_finite_relative_eta_over_infinite_absolute_eta() {
5345        let state = make_integration_state();
5346        let status = status_update::evaluate_target_accuracy(
5347            &state,
5348            100_000,
5349            Duration::from_secs(10),
5350            &[None, None],
5351            IntegrationStatusPhaseDisplay::Real,
5352            Some(1.0e-3),
5353            Some(1.0e-99),
5354        );
5355
5356        assert!(!status.is_reached());
5357        assert!(status.eta_to_target.is_some());
5358        assert_ne!(status.eta_to_target, Some(Duration::MAX));
5359    }
5360
5361    #[test]
5362    fn ratatui_overview_displays_infinite_eta_to_target() {
5363        let state = make_integration_state();
5364        let view_options = IntegrationStatusViewOptions {
5365            target_absolute_accuracy: Some(1.0e-99),
5366            show_statistics: false,
5367            show_max_weight_details: false,
5368            ..default_view_options()
5369        };
5370        let rendered = render_ratatui_update(
5371            StatusUpdateBuildRequest::new(
5372                IntegrationStatusKind::Live,
5373                &state,
5374                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5375                &view_options,
5376            )
5377            .with_timing(
5378                4,
5379                Duration::from_secs(25),
5380                Duration::from_secs(10),
5381                25_000,
5382                125_000,
5383                125_000,
5384            )
5385            .with_live_progress(Some(status_update::LiveIterationProgress {
5386                completed_points: 25_000,
5387                target_points: 100_000,
5388            })),
5389            |_| {},
5390        );
5391
5392        assert!(rendered.contains("ETA to target"), "{rendered}");
5393        assert!(rendered.contains("∞"), "{rendered}");
5394    }
5395
5396    #[test]
5397    fn ratatui_overview_prefers_finite_relative_eta_over_infinite_absolute_eta() {
5398        let state = make_integration_state();
5399        let view_options = IntegrationStatusViewOptions {
5400            target_relative_accuracy: Some(1.0e-3),
5401            target_absolute_accuracy: Some(1.0e-99),
5402            show_statistics: false,
5403            show_max_weight_details: false,
5404            ..default_view_options()
5405        };
5406        let rendered = render_ratatui_update(
5407            StatusUpdateBuildRequest::new(
5408                IntegrationStatusKind::Live,
5409                &state,
5410                &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5411                &view_options,
5412            )
5413            .with_timing(
5414                4,
5415                Duration::from_secs(25),
5416                Duration::from_secs(10),
5417                25_000,
5418                125_000,
5419                125_000,
5420            )
5421            .with_live_progress(Some(status_update::LiveIterationProgress {
5422                completed_points: 25_000,
5423                target_points: 100_000,
5424            })),
5425            |_| {},
5426        );
5427
5428        assert!(rendered.contains("ETA to target"), "{rendered}");
5429        assert!(!rendered.contains("∞"), "{rendered}");
5430    }
5431
5432    #[test]
5433    fn ratatui_recent_history_window_tracks_current_iteration_points() {
5434        let mut dashboard = RatatuiDashboardState::new();
5435        let mut state = make_integration_state();
5436        let view_options = IntegrationStatusViewOptions {
5437            show_statistics: false,
5438            show_max_weight_details: false,
5439            ..default_view_options()
5440        };
5441
5442        state.iter = 1;
5443        dashboard.update(build_status_update(
5444            StatusUpdateBuildRequest::new(
5445                IntegrationStatusKind::Live,
5446                &state,
5447                &[None, None],
5448                &view_options,
5449            )
5450            .with_timing(
5451                4,
5452                Duration::from_secs(10),
5453                Duration::from_secs(10),
5454                500_000,
5455                1_000_000,
5456                1_000_000,
5457            )
5458            .with_live_progress(Some(status_update::LiveIterationProgress {
5459                completed_points: 500_000,
5460                target_points: 1_000_000,
5461            })),
5462        ));
5463
5464        state.iter = 2;
5465        dashboard.update(build_status_update(
5466            StatusUpdateBuildRequest::new(
5467                IntegrationStatusKind::Live,
5468                &state,
5469                &[None, None],
5470                &view_options,
5471            )
5472            .with_timing(
5473                4,
5474                Duration::from_secs(20),
5475                Duration::from_secs(5),
5476                0,
5477                2_000_000,
5478                2_000_000,
5479            )
5480            .with_live_progress(Some(status_update::LiveIterationProgress {
5481                completed_points: 0,
5482                target_points: 1_000_000,
5483            })),
5484        ));
5485        dashboard.update(build_status_update(
5486            StatusUpdateBuildRequest::new(
5487                IntegrationStatusKind::Live,
5488                &state,
5489                &[None, None],
5490                &view_options,
5491            )
5492            .with_timing(
5493                4,
5494                Duration::from_secs(25),
5495                Duration::from_secs(10),
5496                200_000,
5497                2_200_000,
5498                2_200_000,
5499            )
5500            .with_live_progress(Some(status_update::LiveIterationProgress {
5501                completed_points: 200_000,
5502                target_points: 1_000_000,
5503            })),
5504        ));
5505
5506        dashboard.toggle_chart_history_window();
5507        for _ in 0..5 {
5508            dashboard.narrow_chart_history_window();
5509        }
5510
5511        assert_eq!(
5512            dashboard.visible_history_sample_bounds(),
5513            Some((2_000_000, 2_200_000))
5514        );
5515    }
5516
5517    #[test]
5518    fn ratatui_full_history_preserves_origin_after_compaction() {
5519        let mut dashboard = RatatuiDashboardState::new();
5520        let mut state = make_integration_state();
5521        let view_options = IntegrationStatusViewOptions {
5522            show_statistics: false,
5523            show_max_weight_details: false,
5524            ..default_view_options()
5525        };
5526
5527        for index in 0..4_300 {
5528            state.iter = 1 + index / 40;
5529            let total_points = index * 1_000;
5530            dashboard.update(build_status_update(
5531                StatusUpdateBuildRequest::new(
5532                    IntegrationStatusKind::Live,
5533                    &state,
5534                    &[None, None],
5535                    &view_options,
5536                )
5537                .with_timing(
5538                    4,
5539                    Duration::from_secs(index as u64),
5540                    Duration::from_secs(10),
5541                    total_points % 1_000_000,
5542                    total_points,
5543                    total_points,
5544                )
5545                .with_live_progress(Some(status_update::LiveIterationProgress {
5546                    completed_points: total_points % 1_000_000,
5547                    target_points: 1_000_000,
5548                })),
5549            ));
5550        }
5551
5552        dashboard.toggle_chart_history_window();
5553        dashboard.toggle_chart_history_window();
5554
5555        assert_eq!(
5556            dashboard.visible_history_sample_bounds(),
5557            Some((0, 4_299_000))
5558        );
5559    }
5560
5561    #[test]
5562    fn ratatui_convergence_title_shows_active_y_span() {
5563        let state = make_integration_state();
5564        let view_options = IntegrationStatusViewOptions {
5565            show_statistics: false,
5566            show_max_weight_details: false,
5567            ..default_view_options()
5568        };
5569        let rendered = render_ratatui_update(
5570            StatusUpdateBuildRequest::new(
5571                IntegrationStatusKind::Live,
5572                &state,
5573                &[None, None],
5574                &view_options,
5575            )
5576            .with_timing(
5577                4,
5578                Duration::from_secs(15),
5579                Duration::from_secs(10),
5580                25_000,
5581                125_000,
5582                125_000,
5583            )
5584            .with_live_progress(Some(status_update::LiveIterationProgress {
5585                completed_points: 25_000,
5586                target_points: 100_000,
5587            })),
5588            |dashboard| dashboard.widen_chart_y_sigma_span(),
5589        );
5590
5591        assert!(rendered.contains("y ±5σ"), "{rendered}");
5592    }
5593
5594    #[test]
5595    fn ratatui_discrete_tab_renders_selected_bin_detail() {
5596        let state = make_discrete_integration_state();
5597        let view_options = IntegrationStatusViewOptions {
5598            show_statistics: false,
5599            show_max_weight_details: false,
5600            show_top_discrete_grid: false,
5601            show_discrete_contributions_sum: false,
5602            show_max_weight_info_for_discrete_bins: false,
5603            ..default_view_options()
5604        };
5605        let rendered = render_ratatui_update(
5606            StatusUpdateBuildRequest::new(
5607                IntegrationStatusKind::Iteration,
5608                &state,
5609                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5610                &view_options,
5611            )
5612            .with_timing(
5613                4,
5614                Duration::from_secs(12),
5615                Duration::from_secs(12),
5616                110_000,
5617                210_000,
5618                210_000,
5619            ),
5620            |dashboard| dashboard.select_tab(1),
5621        );
5622
5623        assert!(rendered.contains("Selected bin"), "{rendered}");
5624        assert!(
5625            rendered.contains("Discrete bins for focused integrand"),
5626            "{rendered}"
5627        );
5628        assert!(rendered.contains("sample %"), "{rendered}");
5629        assert!(rendered.contains("GL0"), "{rendered}");
5630        assert!(rendered.contains("GL1"), "{rendered}");
5631        assert!(rendered.contains("pdf"), "{rendered}");
5632        assert!(rendered.contains("Per integrand details"), "{rendered}");
5633        assert!(rendered.contains("# samples"), "{rendered}");
5634    }
5635
5636    #[test]
5637    fn ratatui_single_graph_subset_keeps_the_graph_row() {
5638        let state = make_single_graph_discrete_integration_state();
5639        let view_options = IntegrationStatusViewOptions {
5640            show_statistics: false,
5641            show_max_weight_details: false,
5642            ..default_view_options()
5643        };
5644        let rendered = render_ratatui_update(
5645            StatusUpdateBuildRequest::new(
5646                IntegrationStatusKind::Iteration,
5647                &state,
5648                &[None, None],
5649                &view_options,
5650            ),
5651            |dashboard| dashboard.select_tab(1),
5652        );
5653
5654        assert!(
5655            rendered.contains("Discrete bins for focused integrand"),
5656            "{rendered}"
5657        );
5658        assert!(rendered.contains("GL22"), "{rendered}");
5659        assert!(!rendered.contains("GL1"), "{rendered}");
5660    }
5661
5662    #[test]
5663    fn ratatui_max_weight_tab_preserves_full_scientific_exponent() {
5664        let mut state = make_discrete_integration_state();
5665        let max_eval = 3.8477312059601813_f64;
5666        let expected = format!("{:+.16e}", max_eval);
5667        state.all_integrals[0].re.max_eval_positive = F(max_eval);
5668        state.all_integrals[0].re.max_eval_positive_xs = Some(Sample::Discrete(
5669            F(1.0),
5670            0,
5671            Some(Box::new(Sample::Continuous(
5672                F(1.0),
5673                vec![F(0.4502578156709256)],
5674            ))),
5675        ));
5676
5677        let view_options = IntegrationStatusViewOptions {
5678            show_statistics: false,
5679            ..default_view_options()
5680        };
5681        let rendered = render_ratatui_update(
5682            StatusUpdateBuildRequest::new(
5683                IntegrationStatusKind::Iteration,
5684                &state,
5685                &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5686                &view_options,
5687            )
5688            .with_timing(
5689                4,
5690                Duration::from_secs(12),
5691                Duration::from_secs(12),
5692                110_000,
5693                210_000,
5694                210_000,
5695            ),
5696            |dashboard| dashboard.select_tab(2),
5697        );
5698
5699        assert!(rendered.contains(&expected), "{rendered}");
5700        assert!(rendered.contains("graph: 0, xs: ["), "{rendered}");
5701    }
5702
5703    mod failing {
5704        use super::*;
5705
5706        #[test]
5707        fn target_accuracy_status_treats_zero_relative_reference_as_inactive() {
5708            let state = make_integration_state();
5709            let status = status_update::evaluate_target_accuracy(
5710                &state,
5711                100_000,
5712                Duration::from_secs(10),
5713                &[Some(Complex::new(F(0.0), F(0.0))), None],
5714                IntegrationStatusPhaseDisplay::Real,
5715                Some(0.05),
5716                None,
5717            );
5718
5719            assert!(!status.relative_reached);
5720            assert!(!status.absolute_reached);
5721            assert_eq!(status.eta_to_target, None);
5722        }
5723    }
5724}
5725
5726#[test]
5727fn test_threading() {
5728    use symbolica::numerical_integration::ContinuousGrid;
5729
5730    fn test_fn(x: f64) -> f64 {
5731        x * x * (x * 6.).sin()
5732    }
5733
5734    let mut acc_1 = StatisticsAccumulator::<f64>::new();
5735    let mut acc_2 = StatisticsAccumulator::<f64>::new();
5736
5737    let samples_per_sample = 4;
5738    let samples_per_iter = 100000;
5739    let n_iter = 10;
5740
5741    let mut rng = MonteCarloRng::new(42, 0);
5742
5743    let mut grid = Grid::<f64>::Continuous(ContinuousGrid::new(1, 64, 100, None, false));
5744
5745    for _i_iter in 0..n_iter {
5746        let mut multiplice_accs = vec![StatisticsAccumulator::<f64>::new(); samples_per_sample];
5747        for _i_sample in 0..samples_per_iter {
5748            let n_samples = (0..samples_per_sample)
5749                .map(|_| {
5750                    let mut sample = Sample::new();
5751                    grid.sample(&mut rng, &mut sample);
5752                    sample
5753                })
5754                .collect_vec();
5755
5756            let n_evals = n_samples
5757                .iter()
5758                .map(|s| match s {
5759                    Sample::Continuous(_, xs) => test_fn(xs[0]),
5760                    _ => unreachable!(),
5761                })
5762                .collect_vec();
5763
5764            for (i_eval, (sample, eval)) in n_samples.iter().zip(&n_evals).enumerate() {
5765                acc_1.add_sample(eval * sample.get_weight(), Some(sample));
5766                multiplice_accs[i_eval].add_sample(eval * sample.get_weight(), Some(sample));
5767            }
5768        }
5769
5770        for acc in multiplice_accs {
5771            acc_2.merge_samples(&mut acc.clone());
5772        }
5773        acc_1.update_iter(false);
5774        acc_2.update_iter(false);
5775    }
5776
5777    println!("acc1: {:?}", acc_1);
5778    println!("acc2: {:?}", acc_2);
5779}