Skip to main content

gammalooprs/integrate/
status_update.rs

1use std::time::Duration;
2
3use itertools::Itertools;
4use spenso::algebra::algebraic_traits::IsZero;
5use spenso::algebra::complex::Complex;
6use symbolica::numerical_integration::StatisticsAccumulator;
7
8use crate::{
9    integrands::evaluation::StatisticsCounter, settings::runtime::IntegrationStatisticsSnapshot,
10    utils, utils::F,
11};
12
13use super::{
14    ComplexAccumulator, DiscreteGridAccumulatorSummary, IntegrationState,
15    display::{
16        DisplayField, StyledText, TextStyle, UncertaintyNotation, format_abbreviated_count,
17        format_iteration_points, format_max_eval_sample, format_signed_uncertainty,
18        format_significant_percentage, format_total_points, styled_bin_description,
19    },
20    max_eval_entry, max_weight_impact, summary_at_path,
21};
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum IntegrationStatusKind {
25    Live,
26    Iteration,
27    Final,
28}
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub enum IntegrationStatusPhaseDisplay {
32    Both,
33    Real,
34    Imag,
35}
36
37impl IntegrationStatusPhaseDisplay {
38    pub(crate) fn shows_real(self) -> bool {
39        matches!(self, Self::Both | Self::Real)
40    }
41
42    pub(crate) fn shows_imag(self) -> bool {
43        matches!(self, Self::Both | Self::Imag)
44    }
45}
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub enum ContributionSortMode {
49    Index,
50    Integral,
51    Error,
52}
53
54#[derive(Clone, Debug, PartialEq)]
55pub struct IntegrationStatusViewOptions {
56    pub phase_display: IntegrationStatusPhaseDisplay,
57    pub training_phase_display: IntegrationStatusPhaseDisplay,
58    pub training_slot: usize,
59    pub slot_training_phase_displays: Vec<IntegrationStatusPhaseDisplay>,
60    pub per_slot_training_phase: bool,
61    pub target_relative_accuracy: Option<f64>,
62    pub target_absolute_accuracy: Option<f64>,
63    pub show_statistics: bool,
64    pub show_max_weight_details: bool,
65    pub show_top_discrete_grid: bool,
66    pub show_discrete_contributions_sum: bool,
67    pub contribution_sort: ContributionSortMode,
68    pub show_max_weight_info_for_discrete_bins: bool,
69}
70
71impl IntegrationStatusViewOptions {
72    pub(crate) fn for_final(self) -> Self {
73        Self {
74            show_statistics: true,
75            ..self
76        }
77    }
78}
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub(crate) struct LiveIterationProgress {
82    pub(crate) completed_points: usize,
83    pub(crate) target_points: usize,
84}
85
86pub(crate) struct StatusUpdateBuildRequest<'a> {
87    pub(crate) kind: IntegrationStatusKind,
88    pub(crate) integration_state: &'a IntegrationState,
89    pub(crate) cores: usize,
90    pub(crate) elapsed_time: Duration,
91    pub(crate) iteration_elapsed_time: Duration,
92    pub(crate) cur_points: usize,
93    pub(crate) total_points_display: usize,
94    pub(crate) n_samples_evaluated: usize,
95    pub(crate) targets: &'a [Option<Complex<F<f64>>>],
96    pub(crate) render_options: &'a IntegrationStatusViewOptions,
97    pub(crate) live_progress: Option<LiveIterationProgress>,
98}
99
100impl<'a> StatusUpdateBuildRequest<'a> {
101    pub(crate) fn new(
102        kind: IntegrationStatusKind,
103        integration_state: &'a IntegrationState,
104        targets: &'a [Option<Complex<F<f64>>>],
105        render_options: &'a IntegrationStatusViewOptions,
106    ) -> Self {
107        Self {
108            kind,
109            integration_state,
110            cores: 0,
111            elapsed_time: Duration::ZERO,
112            iteration_elapsed_time: Duration::ZERO,
113            cur_points: 0,
114            total_points_display: 0,
115            n_samples_evaluated: 0,
116            targets,
117            render_options,
118            live_progress: None,
119        }
120    }
121
122    pub(crate) fn with_timing(
123        mut self,
124        cores: usize,
125        elapsed_time: Duration,
126        iteration_elapsed_time: Duration,
127        cur_points: usize,
128        total_points_display: usize,
129        n_samples_evaluated: usize,
130    ) -> Self {
131        self.cores = cores;
132        self.elapsed_time = elapsed_time;
133        self.iteration_elapsed_time = iteration_elapsed_time;
134        self.cur_points = cur_points;
135        self.total_points_display = total_points_display;
136        self.n_samples_evaluated = n_samples_evaluated;
137        self
138    }
139
140    pub(crate) fn with_live_progress(
141        mut self,
142        live_progress: Option<LiveIterationProgress>,
143    ) -> Self {
144        self.live_progress = live_progress;
145        self
146    }
147}
148
149#[derive(Clone, Debug, Eq, PartialEq)]
150pub(crate) struct StatusMeta {
151    pub(crate) elapsed_time: Duration,
152    pub(crate) iteration_elapsed_time: Duration,
153    pub(crate) iteration: usize,
154    pub(crate) current_iteration_points: usize,
155    pub(crate) total_points: usize,
156    pub(crate) n_samples_evaluated: usize,
157    pub(crate) cores: usize,
158    pub(crate) training_slot: usize,
159    pub(crate) training_phase_display: IntegrationStatusPhaseDisplay,
160    pub(crate) live_progress: Option<LiveIterationProgress>,
161    pub(crate) show_eta_to_target: bool,
162    pub(crate) eta_to_target_specification: Option<String>,
163    pub(crate) eta_to_target: Option<Duration>,
164}
165
166#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
167pub(crate) struct TargetAccuracyStatus {
168    pub(crate) relative_reached: bool,
169    pub(crate) absolute_reached: bool,
170    pub(crate) eta_to_target: Option<Duration>,
171}
172
173impl TargetAccuracyStatus {
174    pub(crate) fn is_reached(self) -> bool {
175        self.relative_reached || self.absolute_reached
176    }
177}
178
179#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
180pub(crate) enum ComponentKind {
181    Real,
182    Imag,
183}
184
185impl ComponentKind {
186    pub(crate) fn from_training_phase_display(
187        display: IntegrationStatusPhaseDisplay,
188    ) -> Option<Self> {
189        match display {
190            IntegrationStatusPhaseDisplay::Real => Some(Self::Real),
191            IntegrationStatusPhaseDisplay::Imag => Some(Self::Imag),
192            IntegrationStatusPhaseDisplay::Both => None,
193        }
194    }
195
196    pub(crate) fn all_for_display(display: IntegrationStatusPhaseDisplay) -> Vec<Self> {
197        let mut components = Vec::new();
198        if display.shows_real() {
199            components.push(Self::Real);
200        }
201        if display.shows_imag() {
202            components.push(Self::Imag);
203        }
204        components
205    }
206
207    pub(crate) fn tag(self) -> &'static str {
208        match self {
209            Self::Real => "re",
210            Self::Imag => "im",
211        }
212    }
213
214    pub(crate) fn phase_name(self) -> &'static str {
215        match self {
216            Self::Real => "real",
217            Self::Imag => "imag",
218        }
219    }
220
221    pub(crate) fn text_style(self) -> TextStyle {
222        match self {
223            Self::Real => TextStyle::pink().bold(),
224            Self::Imag => TextStyle::yellow().bold(),
225        }
226    }
227
228    pub(crate) fn label_display(self) -> StyledText {
229        StyledText::styled(self.tag(), self.text_style())
230    }
231
232    fn display_field(self) -> DisplayField<Self> {
233        DisplayField::new(self, self.label_display())
234    }
235}
236
237#[derive(Clone, Copy, Debug, Eq, PartialEq)]
238pub(crate) enum ContributionKind {
239    All,
240    Sum,
241    Bin(usize),
242}
243
244#[derive(Clone, Debug)]
245pub struct StatusUpdate {
246    pub(crate) kind: IntegrationStatusKind,
247    pub(crate) meta: StatusMeta,
248    pub(crate) targets: Vec<Option<Complex<F<f64>>>>,
249    pub(crate) slot_training_phase_displays: Vec<IntegrationStatusPhaseDisplay>,
250    pub(crate) per_slot_training_phase: bool,
251    pub(crate) main_results: MainResultsSection,
252    pub(crate) max_weight_details: Option<MaxWeightDetailsSection>,
253    pub(crate) discrete_max_weight_details: Option<DiscreteMaxWeightDetailsSection>,
254    pub(crate) statistics: Option<StatisticsSection>,
255}
256
257impl StatusUpdate {
258    pub fn kind(&self) -> IntegrationStatusKind {
259        self.kind
260    }
261
262    pub fn is_initial_live_status(&self) -> bool {
263        self.kind == IntegrationStatusKind::Live
264            && self
265                .meta
266                .live_progress
267                .is_some_and(|progress| progress.completed_points == 0)
268    }
269
270    pub(crate) fn training_component(&self) -> Option<ComponentKind> {
271        ComponentKind::from_training_phase_display(self.meta.training_phase_display)
272    }
273
274    pub(crate) fn statistics_snapshot(&self) -> Option<IntegrationStatisticsSnapshot> {
275        self.statistics
276            .as_ref()
277            .map(StatisticsSection::global_snapshot)
278    }
279
280    pub(crate) fn training_target(&self) -> Option<F<f64>> {
281        self.target_for_slot(self.meta.training_slot)
282    }
283
284    pub(crate) fn target_for_slot(&self, slot_index: usize) -> Option<F<f64>> {
285        let component = self.training_component()?;
286        self.target_for_slot_component(slot_index, component)
287    }
288
289    pub(crate) fn target_for_slot_component(
290        &self,
291        slot_index: usize,
292        component: ComponentKind,
293    ) -> Option<F<f64>> {
294        let target = self.targets.get(slot_index)?.as_ref()?;
295        Some(match component {
296            ComponentKind::Real => target.re,
297            ComponentKind::Imag => target.im,
298        })
299    }
300
301    pub(crate) fn target_display_for_slot_component(
302        &self,
303        slot_index: usize,
304        component: ComponentKind,
305    ) -> Option<StyledText> {
306        self.target_for_slot_component(slot_index, component)
307            .map(|value| {
308                StyledText::styled(
309                    format_target_value_for_display(value.0),
310                    TextStyle::blue().bold(),
311                )
312            })
313    }
314
315    pub(crate) fn training_component_for_slot(&self, slot_index: usize) -> Option<ComponentKind> {
316        let phase = if self.per_slot_training_phase {
317            self.slot_training_phase_displays
318                .get(slot_index)
319                .copied()
320                .unwrap_or(self.meta.training_phase_display)
321        } else if slot_index == self.meta.training_slot {
322            self.meta.training_phase_display
323        } else {
324            return None;
325        };
326        match phase {
327            IntegrationStatusPhaseDisplay::Real => Some(ComponentKind::Real),
328            IntegrationStatusPhaseDisplay::Imag => Some(ComponentKind::Imag),
329            IntegrationStatusPhaseDisplay::Both => None,
330        }
331    }
332
333    pub(crate) fn slot_component_selected_for_training(
334        &self,
335        slot_index: usize,
336        component: ComponentKind,
337    ) -> bool {
338        self.training_component_for_slot(slot_index) == Some(component)
339    }
340
341    pub(crate) fn target_deltas_for_row_slot(
342        &self,
343        row: &MainResultsRow,
344        slot_index: usize,
345    ) -> (Option<DisplayField<f64>>, Option<DisplayField<f64>>) {
346        let Some(cell) = row.slot_cell(slot_index) else {
347            return (None, None);
348        };
349        let Some(value) = cell.value.as_ref() else {
350            return (None, None);
351        };
352        let target = self.target_for_slot_component(slot_index, row.component.raw);
353        format_delta_fields_from_estimate(value.raw.0, value.raw.1, target)
354    }
355}
356
357#[derive(Clone, Copy, Debug, Eq, PartialEq)]
358pub(crate) enum StatisticsScope {
359    Global,
360    Slot(usize),
361}
362
363impl StatusMeta {
364    pub(crate) fn iteration_progress_ratio(&self) -> Option<f64> {
365        self.live_progress.map(|progress| {
366            if progress.target_points == 0 {
367                0.0
368            } else {
369                progress.completed_points as f64 / progress.target_points as f64
370            }
371        })
372    }
373
374    pub(crate) fn iteration_eta(&self) -> Option<Duration> {
375        let progress = self.live_progress?;
376        if progress.completed_points == 0 || self.iteration_elapsed_time.is_zero() {
377            return None;
378        }
379
380        let processed_per_second =
381            progress.completed_points as f64 / self.iteration_elapsed_time.as_secs_f64();
382        if processed_per_second <= 0.0 {
383            return None;
384        }
385
386        let remaining_points = progress
387            .target_points
388            .saturating_sub(progress.completed_points);
389        Some(utils::duration_from_secs_f64_saturating(
390            remaining_points as f64 / processed_per_second,
391        ))
392    }
393
394    pub(crate) fn total_sample_rate_per_second(&self) -> Option<f64> {
395        if self.total_points == 0 || self.elapsed_time.is_zero() {
396            return None;
397        }
398        Some(self.total_points as f64 / self.elapsed_time.as_secs_f64())
399    }
400
401    pub(crate) fn sample_core_time(&self) -> Option<String> {
402        if self.n_samples_evaluated == 0 {
403            return None;
404        }
405
406        Some(utils::format_evaluation_time_from_f64(
407            self.elapsed_time.as_secs_f64() / (self.n_samples_evaluated as f64)
408                * (self.cores as f64),
409        ))
410    }
411
412    pub(crate) fn eta_to_target(&self) -> Option<Duration> {
413        self.eta_to_target
414    }
415
416    pub(crate) fn eta_to_target_specification(&self) -> Option<&str> {
417        self.eta_to_target_specification.as_deref()
418    }
419}
420
421pub(crate) fn evaluate_target_accuracy(
422    integration_state: &IntegrationState,
423    total_points: usize,
424    elapsed_time: Duration,
425    targets: &[Option<Complex<F<f64>>>],
426    training_phase_display: IntegrationStatusPhaseDisplay,
427    target_relative_accuracy: Option<f64>,
428    target_absolute_accuracy: Option<f64>,
429) -> TargetAccuracyStatus {
430    if target_relative_accuracy.is_none() && target_absolute_accuracy.is_none() {
431        return TargetAccuracyStatus::default();
432    }
433    if total_points == 0 {
434        return TargetAccuracyStatus::default();
435    }
436
437    let components = ComponentKind::all_for_display(training_phase_display);
438    if components.is_empty() {
439        return TargetAccuracyStatus::default();
440    }
441    if integration_state.all_integrals.is_empty() {
442        return TargetAccuracyStatus::default();
443    }
444
445    let absolute_target_error = target_absolute_accuracy.filter(|target| *target >= 0.0);
446    let relative_target_accuracy = target_relative_accuracy.filter(|target| *target >= 0.0);
447
448    let mut absolute_reached = absolute_target_error.is_some();
449    let mut relative_reached = relative_target_accuracy.is_some();
450    let mut saw_absolute_constraint = false;
451    let mut saw_relative_constraint = false;
452    let mut absolute_eta_to_target = None;
453    let mut relative_eta_to_target = None;
454
455    for (slot_index, accumulator) in integration_state.all_integrals.iter().enumerate() {
456        let target = targets.get(slot_index).and_then(|target| target.as_ref());
457        for component in &components {
458            let (avg, err) = match component {
459                ComponentKind::Real => (accumulator.re.avg.0, accumulator.re.err.0),
460                ComponentKind::Imag => (accumulator.im.avg.0, accumulator.im.err.0),
461            };
462
463            if let Some(target_error) = absolute_target_error {
464                saw_absolute_constraint = true;
465                absolute_reached &= err <= target_error;
466                absolute_eta_to_target = max_duration_option(
467                    absolute_eta_to_target,
468                    estimate_eta_to_target(total_points, elapsed_time, err, Some(target_error)),
469                );
470            }
471
472            if let Some(target_accuracy) = relative_target_accuracy {
473                let reference = target
474                    .map(|target| match component {
475                        ComponentKind::Real => target.re.0,
476                        ComponentKind::Imag => target.im.0,
477                    })
478                    .unwrap_or(avg)
479                    .abs();
480                if reference != 0.0 {
481                    saw_relative_constraint = true;
482                    let target_error = target_accuracy * reference;
483                    relative_reached &= err <= target_error;
484                    relative_eta_to_target = max_duration_option(
485                        relative_eta_to_target,
486                        estimate_eta_to_target(total_points, elapsed_time, err, Some(target_error)),
487                    );
488                }
489            }
490        }
491    }
492
493    if !saw_absolute_constraint {
494        absolute_reached = false;
495        absolute_eta_to_target = None;
496    }
497    if !saw_relative_constraint {
498        relative_reached = false;
499        relative_eta_to_target = None;
500    }
501
502    let eta_to_target = min_duration_option(absolute_eta_to_target, relative_eta_to_target);
503
504    TargetAccuracyStatus {
505        relative_reached,
506        absolute_reached,
507        eta_to_target,
508    }
509}
510
511impl MainResultsSection {
512    pub(crate) fn all_rows(&self) -> impl Iterator<Item = &MainResultsRow> {
513        self.row_groups.iter().flat_map(|group| group.rows.iter())
514    }
515
516    pub(crate) fn row_groups_of_kind(
517        &self,
518        kind: MainResultsRowGroupKind,
519    ) -> impl Iterator<Item = &MainResultsRowGroup> {
520        self.row_groups
521            .iter()
522            .filter(move |group| group.kind == kind)
523    }
524
525    pub(crate) fn find_row(
526        &self,
527        contribution: ContributionKind,
528        component: ComponentKind,
529    ) -> Option<&MainResultsRow> {
530        self.all_rows()
531            .find(|row| row.contribution.raw == contribution && row.component.raw == component)
532    }
533
534    pub(crate) fn metadata_headers(&self) -> Vec<StyledText> {
535        let mut headers = vec![
536            styled_colored("χ²/dof", TextStyle::blue().bold()),
537            styled_colored("mwi", TextStyle::blue().bold()),
538        ];
539        if self.has_target_columns {
540            headers.push(styled_colored("Δ [σ]", TextStyle::blue().bold()));
541            headers.push(styled_colored("Δ [%]", TextStyle::blue().bold()));
542        }
543        headers
544    }
545
546    pub(crate) fn component_header(&self) -> StyledText {
547        styled_plain("")
548    }
549
550    pub(crate) fn summary_headers(&self) -> Vec<StyledText> {
551        let mut headers = vec![self.contribution_header.clone(), self.component_header()];
552        headers.extend(self.slot_headers.iter().cloned());
553        headers
554    }
555
556    pub(crate) fn discrete_headers(&self) -> Vec<StyledText> {
557        vec![
558            self.contribution_header.clone(),
559            self.component_header(),
560            styled_plain("integral"),
561            styled_plain("% err"),
562            styled_plain("χ^2"),
563            styled_plain("m.w.i"),
564            styled_plain("sample %"),
565            styled_plain("# samples"),
566            styled_plain("pdf"),
567        ]
568    }
569
570    pub(crate) fn selected_bin_detail_headers(&self) -> Vec<StyledText> {
571        vec![
572            styled_plain("Integrand"),
573            styled_plain("integral"),
574            styled_plain("% err"),
575            styled_plain("χ^2"),
576            styled_plain("m.w.i"),
577            styled_plain("sample %"),
578            styled_plain("# samples"),
579            styled_plain("pdf"),
580        ]
581    }
582}
583
584impl MainResultsRow {
585    pub(crate) fn slot_cell(&self, slot_index: usize) -> Option<&MainTableSlotCells> {
586        self.slot_cells.get(slot_index)
587    }
588}
589
590impl MaxWeightDetailsSection {
591    pub(crate) fn title(&self) -> StyledText {
592        styled_colored("Maximum weight details", TextStyle::green().bold())
593    }
594
595    pub(crate) fn headers(&self) -> Vec<StyledText> {
596        vec![
597            styled_colored("Integrand", TextStyle::blue().bold()),
598            styled_plain(""),
599            styled_colored("Max eval", TextStyle::blue().bold()),
600            styled_colored("Max eval coordinates", TextStyle::blue().bold()),
601        ]
602    }
603}
604
605impl DiscreteMaxWeightDetailsSection {
606    pub(crate) fn title(&self) -> StyledText {
607        styled_colored(
608            "Maximum weight details by discrete bin",
609            TextStyle::green().bold(),
610        )
611    }
612
613    pub(crate) fn summary_headers(&self) -> Vec<StyledText> {
614        let mut headers = vec![self.contribution_header.clone(), styled_plain("")];
615        headers.extend(self.slot_headers.iter().cloned());
616        headers
617    }
618
619    pub(crate) fn coordinates_header(&self) -> StyledText {
620        styled_colored("Max eval coordinates", TextStyle::blue().bold())
621    }
622
623    pub(crate) fn coordinate_headers(&self) -> Vec<StyledText> {
624        vec![
625            self.contribution_header.clone(),
626            styled_plain(""),
627            styled_plain("Integrand"),
628            self.coordinates_header(),
629        ]
630    }
631}
632
633impl StatisticsSection {
634    pub(crate) fn global_snapshot(&self) -> IntegrationStatisticsSnapshot {
635        self.global.snapshot()
636    }
637
638    fn scoped_counter(&self, scope: StatisticsScope) -> Option<&StatisticsCounter> {
639        match scope {
640            StatisticsScope::Global => Some(&self.global),
641            StatisticsScope::Slot(slot_index) => self.slot_counters.get(slot_index),
642        }
643    }
644
645    fn scope_label(&self, scope: StatisticsScope) -> StyledText {
646        let label = match scope {
647            StatisticsScope::Global => "[global]".to_string(),
648            StatisticsScope::Slot(slot_index) => self
649                .slot_labels
650                .get(slot_index)
651                .map(|label| format!("[{label}]"))
652                .unwrap_or_else(|| "[global]".to_string()),
653        };
654        styled_colored(format!(" {label}"), TextStyle::blue().bold())
655    }
656
657    fn scoped_title(&self, prefix: &str, scope: StatisticsScope) -> StyledText {
658        let mut title = styled_colored(prefix, TextStyle::green().bold());
659        title.append(self.scope_label(scope));
660        title
661    }
662
663    fn uses_global_integrator_scope(scope: StatisticsScope) -> bool {
664        matches!(scope, StatisticsScope::Global)
665    }
666
667    pub(crate) fn statistics_title(&self, scope: StatisticsScope) -> StyledText {
668        self.scoped_title("Integration statistics", scope)
669    }
670
671    pub(crate) fn timing_title(&self, scope: StatisticsScope) -> StyledText {
672        self.scoped_title("Timing composition", scope)
673    }
674
675    pub(crate) fn precision_title(&self, scope: StatisticsScope) -> StyledText {
676        self.scoped_title("Precision mix", scope)
677    }
678
679    pub(crate) fn table_rows(&self, scope: StatisticsScope) -> Vec<StatisticsTableRow> {
680        let snapshot = self
681            .scoped_counter(scope)
682            .map(StatisticsCounter::snapshot)
683            .unwrap_or_else(|| self.global.snapshot());
684        let integrator_value = if Self::uses_global_integrator_scope(scope) {
685            styled_colored(
686                utils::format_evaluation_time_from_f64(snapshot.average_integrator_time_seconds),
687                TextStyle::green(),
688            )
689        } else {
690            styled_plain("N/A")
691        };
692        vec![
693            StatisticsTableRow {
694                row_label: styled_colored("  timing", TextStyle::blue().bold()),
695                entries: vec![
696                    StatisticsTableEntry {
697                        label: styled_plain("total"),
698                        value: styled_colored(
699                            utils::format_evaluation_time_from_f64(
700                                snapshot.average_total_time_seconds,
701                            ),
702                            TextStyle::green(),
703                        ),
704                    },
705                    StatisticsTableEntry {
706                        label: styled_plain("param"),
707                        value: styled_colored(
708                            utils::format_evaluation_time_from_f64(
709                                snapshot.average_parameterization_time_seconds,
710                            ),
711                            TextStyle::green(),
712                        ),
713                    },
714                    StatisticsTableEntry {
715                        label: styled_plain("itg"),
716                        value: styled_colored(
717                            utils::format_evaluation_time_from_f64(
718                                snapshot.average_integrand_time_seconds,
719                            ),
720                            TextStyle::green(),
721                        ),
722                    },
723                    StatisticsTableEntry {
724                        label: styled_plain("evaluators"),
725                        value: styled_colored(
726                            utils::format_evaluation_time_from_f64(
727                                snapshot.average_evaluator_time_seconds,
728                            ),
729                            TextStyle::green(),
730                        ),
731                    },
732                ],
733            },
734            StatisticsTableRow {
735                row_label: styled_colored("  evals", TextStyle::blue().bold()),
736                entries: vec![
737                    StatisticsTableEntry {
738                        label: styled_plain("f64"),
739                        value: styled_colored(
740                            format!("{:.2}%", snapshot.f64_percentage),
741                            TextStyle::green(),
742                        ),
743                    },
744                    StatisticsTableEntry {
745                        label: styled_plain("f128"),
746                        value: styled_colored(
747                            format!("{:.2}%", snapshot.f128_percentage),
748                            TextStyle::blue(),
749                        ),
750                    },
751                    StatisticsTableEntry {
752                        label: styled_plain("arb"),
753                        value: styled_plain(format!("{:.2}%", snapshot.arb_percentage)),
754                    },
755                    StatisticsTableEntry {
756                        label: styled_plain("nans+unstable"),
757                        value: styled_colored(
758                            format!("{:.2}%", snapshot.nan_or_unstable_percentage),
759                            if snapshot.nan_or_unstable_percentage > 0.0 {
760                                TextStyle::red()
761                            } else {
762                                TextStyle::green()
763                            },
764                        ),
765                    },
766                ],
767            },
768            StatisticsTableRow {
769                row_label: styled_colored("  events", TextStyle::blue().bold()),
770                entries: vec![
771                    StatisticsTableEntry {
772                        label: styled_plain("evts #"),
773                        value: styled_colored(
774                            format_abbreviated_count(snapshot.generated_event_count),
775                            TextStyle::green(),
776                        ),
777                    },
778                    StatisticsTableEntry {
779                        label: styled_plain("sel. %"),
780                        value: snapshot
781                            .selection_efficiency_percentage
782                            .map(|value| styled_colored(format!("{value:.2}%"), TextStyle::green()))
783                            .unwrap_or_else(|| styled_plain("N/A")),
784                    },
785                    StatisticsTableEntry {
786                        label: styled_plain("obs"),
787                        value: styled_colored(
788                            utils::format_evaluation_time_from_f64(
789                                snapshot.average_observable_time_seconds,
790                            ),
791                            TextStyle::green(),
792                        ),
793                    },
794                    StatisticsTableEntry {
795                        label: styled_plain("integrator"),
796                        value: integrator_value,
797                    },
798                ],
799            },
800        ]
801    }
802
803    pub(crate) fn timing_mix_segments(&self, scope: StatisticsScope) -> Vec<StatisticsMixSegment> {
804        let snapshot = self
805            .scoped_counter(scope)
806            .map(StatisticsCounter::snapshot)
807            .unwrap_or_else(|| self.global.snapshot());
808        let parameterization = snapshot.average_parameterization_time_seconds.max(0.0);
809        let evaluator = snapshot.average_evaluator_time_seconds.max(0.0);
810        let observable = snapshot.average_observable_time_seconds.max(0.0);
811        let integrand_core =
812            (snapshot.average_integrand_time_seconds.max(0.0) - observable - evaluator).max(0.0);
813        let mut raw_segments = vec![
814            (
815                styled_colored("evaluators", TextStyle::green().bold()),
816                evaluator,
817            ),
818            (
819                styled_colored("itg_core", TextStyle::blue().bold()),
820                integrand_core,
821            ),
822            (
823                styled_colored("obs", TextStyle::yellow().bold()),
824                observable,
825            ),
826            (
827                styled_colored("param", TextStyle::red().bold()),
828                parameterization,
829            ),
830        ];
831        if Self::uses_global_integrator_scope(scope) {
832            raw_segments.push((
833                styled_plain("integrator"),
834                snapshot.average_integrator_time_seconds.max(0.0),
835            ));
836        }
837        let mut segments = normalize_mix_segments(raw_segments);
838        segments.sort_by(|lhs, rhs| {
839            rhs.percentage
840                .partial_cmp(&lhs.percentage)
841                .unwrap_or(std::cmp::Ordering::Equal)
842        });
843        segments
844    }
845
846    pub(crate) fn precision_mix_segments(
847        &self,
848        scope: StatisticsScope,
849    ) -> Vec<StatisticsMixSegment> {
850        let snapshot = self
851            .scoped_counter(scope)
852            .map(StatisticsCounter::snapshot)
853            .unwrap_or_else(|| self.global.snapshot());
854        normalize_mix_segments(vec![
855            (
856                styled_colored("f64", TextStyle::green()),
857                snapshot.f64_percentage,
858            ),
859            (
860                styled_colored("f128", TextStyle::blue()),
861                snapshot.f128_percentage,
862            ),
863            (styled_plain("arb"), snapshot.arb_percentage),
864            (
865                styled_colored("unstbl.+nan.", TextStyle::red()),
866                snapshot.nan_or_unstable_percentage,
867            ),
868        ])
869    }
870
871    pub(crate) fn stability_mix_segments(
872        &self,
873        scope: StatisticsScope,
874    ) -> Vec<StatisticsMixSegment> {
875        let snapshot = self
876            .scoped_counter(scope)
877            .map(StatisticsCounter::snapshot)
878            .unwrap_or_else(|| self.global.snapshot());
879        let unstable = (snapshot.nan_or_unstable_percentage - snapshot.nan_percentage).max(0.0);
880        let stable = (100.0 - snapshot.nan_or_unstable_percentage).max(0.0);
881        normalize_mix_segments(vec![
882            (styled_colored("stable", TextStyle::green()), stable),
883            (styled_colored("unstable", TextStyle::red()), unstable),
884            (
885                styled_colored("nan", TextStyle::red()),
886                snapshot.nan_percentage,
887            ),
888        ])
889    }
890}
891
892#[derive(Clone, Debug)]
893pub(crate) struct MainResultsSection {
894    pub(crate) header_left: StyledText,
895    pub(crate) header_middle: StyledText,
896    pub(crate) header_tail: StyledText,
897    pub(crate) contribution_header: StyledText,
898    pub(crate) slot_headers: Vec<StyledText>,
899    pub(crate) has_discrete_columns: bool,
900    pub(crate) has_target_columns: bool,
901    pub(crate) row_groups: Vec<MainResultsRowGroup>,
902}
903
904#[derive(Clone, Copy, Debug, Eq, PartialEq)]
905pub(crate) enum MainResultsRowGroupKind {
906    All,
907    Sum,
908    Bins,
909}
910
911#[derive(Clone, Debug)]
912pub(crate) struct MainResultsRowGroup {
913    pub(crate) kind: MainResultsRowGroupKind,
914    pub(crate) rows: Vec<MainResultsRow>,
915}
916
917#[derive(Clone, Debug)]
918pub(crate) struct MainResultsRow {
919    pub(crate) contribution: DisplayField<ContributionKind>,
920    pub(crate) component: DisplayField<ComponentKind>,
921    pub(crate) slot_cells: Vec<MainTableSlotCells>,
922    pub(crate) chi_sq: Option<DisplayField<f64>>,
923    pub(crate) delta_sigma: Option<DisplayField<f64>>,
924    pub(crate) delta_percent: Option<DisplayField<f64>>,
925    pub(crate) max_weight_impact: Option<DisplayField<f64>>,
926}
927
928#[derive(Clone, Debug, Default)]
929pub(crate) struct MainTableSlotCells {
930    pub(crate) value: Option<DisplayField<(F<f64>, F<f64>)>>,
931    pub(crate) relative_error: Option<DisplayField<f64>>,
932    pub(crate) chi_sq: Option<DisplayField<f64>>,
933    pub(crate) max_weight_impact: Option<DisplayField<f64>>,
934    pub(crate) sample_fraction: Option<DisplayField<f64>>,
935    pub(crate) sample_count: Option<DisplayField<usize>>,
936    pub(crate) target_pdf: Option<DisplayField<f64>>,
937}
938
939#[derive(Clone, Debug)]
940pub(crate) struct MaxWeightDetailsSection {
941    pub(crate) rows_by_slot: Vec<Vec<MaxWeightDetailsRow>>,
942}
943
944#[derive(Clone, Debug)]
945pub(crate) struct MaxWeightDetailsRow {
946    pub(crate) slot: DisplayField<String>,
947    pub(crate) component_sign: DisplayField<(ComponentKind, bool)>,
948    pub(crate) max_eval: DisplayField<F<f64>>,
949    pub(crate) coordinates: DisplayField<String>,
950}
951
952#[derive(Clone, Debug)]
953pub(crate) struct DiscreteMaxWeightDetailsSection {
954    pub(crate) contribution_header: StyledText,
955    pub(crate) slot_headers: Vec<StyledText>,
956    pub(crate) row_groups: Vec<Vec<DiscreteMaxWeightRow>>,
957}
958
959#[derive(Clone, Debug)]
960pub(crate) struct DiscreteMaxWeightRow {
961    pub(crate) contribution: DisplayField<ContributionKind>,
962    pub(crate) component_sign: DisplayField<(ComponentKind, bool)>,
963    pub(crate) slot_values: Vec<Option<DisplayField<F<f64>>>>,
964    pub(crate) slot_coordinates: Vec<SlotCoordinateEntry>,
965}
966
967#[derive(Clone, Debug)]
968pub(crate) struct SlotCoordinateEntry {
969    pub(crate) slot: DisplayField<String>,
970    pub(crate) coordinates: DisplayField<String>,
971}
972
973#[derive(Clone, Debug)]
974pub(crate) struct StatisticsSection {
975    pub(crate) global: StatisticsCounter,
976    pub(crate) slot_counters: Vec<StatisticsCounter>,
977    pub(crate) slot_labels: Vec<String>,
978}
979
980#[derive(Clone, Debug)]
981pub(crate) struct StatisticsTableRow {
982    pub(crate) row_label: StyledText,
983    pub(crate) entries: Vec<StatisticsTableEntry>,
984}
985
986#[derive(Clone, Debug)]
987pub(crate) struct StatisticsTableEntry {
988    pub(crate) label: StyledText,
989    pub(crate) value: StyledText,
990}
991
992#[derive(Clone, Debug)]
993pub(crate) struct StatisticsMixSegment {
994    pub(crate) label: StyledText,
995    pub(crate) percentage: f64,
996}
997
998fn component_accumulator(
999    accumulator: &ComplexAccumulator,
1000    component: ComponentKind,
1001) -> &StatisticsAccumulator<F<f64>> {
1002    match component {
1003        ComponentKind::Real => &accumulator.re,
1004        ComponentKind::Imag => &accumulator.im,
1005    }
1006}
1007
1008fn slot_component_summary(
1009    integration_state: &IntegrationState,
1010    slot_index: usize,
1011    component: ComponentKind,
1012) -> Option<&DiscreteGridAccumulatorSummary> {
1013    match component {
1014        ComponentKind::Real => integration_state.slot_re_summaries[slot_index].as_ref(),
1015        ComponentKind::Imag => integration_state.slot_im_summaries[slot_index].as_ref(),
1016    }
1017}
1018
1019fn sum_estimate_error(summary: &DiscreteGridAccumulatorSummary) -> (F<f64>, F<f64>) {
1020    let (avg, err_sq) = summary
1021        .bins
1022        .iter()
1023        .fold((F(0.0), F(0.0)), |(avg, err_sq), bin| {
1024            (
1025                avg + bin.accumulator.avg,
1026                err_sq + bin.accumulator.err * bin.accumulator.err,
1027            )
1028        });
1029    (avg, F(err_sq.0.sqrt()))
1030}
1031
1032fn total_processed_samples(summary: &DiscreteGridAccumulatorSummary) -> usize {
1033    summary
1034        .bins
1035        .iter()
1036        .map(|bin| bin.accumulator.processed_samples)
1037        .sum()
1038}
1039
1040fn normalize_mix_segments(segments: Vec<(StyledText, f64)>) -> Vec<StatisticsMixSegment> {
1041    let total: f64 = segments.iter().map(|(_, value)| value.max(0.0)).sum();
1042    if total <= f64::EPSILON {
1043        return segments
1044            .into_iter()
1045            .map(|(label, _)| StatisticsMixSegment {
1046                label,
1047                percentage: 0.0,
1048            })
1049            .collect();
1050    }
1051
1052    segments
1053        .into_iter()
1054        .map(|(label, value)| StatisticsMixSegment {
1055            label,
1056            percentage: value.max(0.0) / total * 100.0,
1057        })
1058        .collect()
1059}
1060
1061fn estimate_eta_to_target(
1062    total_points: usize,
1063    elapsed_time: Duration,
1064    current_error: f64,
1065    target_error: Option<f64>,
1066) -> Option<Duration> {
1067    let target_error = target_error?;
1068    if target_error < 0.0 {
1069        return None;
1070    }
1071    if current_error <= target_error {
1072        return Some(Duration::ZERO);
1073    }
1074    if total_points == 0 || elapsed_time.is_zero() || current_error <= 0.0 {
1075        return None;
1076    }
1077    if target_error == 0.0 {
1078        return Some(Duration::MAX);
1079    }
1080
1081    let rate_per_second = total_points as f64 / elapsed_time.as_secs_f64();
1082    if rate_per_second <= 0.0 {
1083        return None;
1084    }
1085
1086    let target_points = total_points as f64 * (current_error / target_error).powi(2);
1087    if !target_points.is_finite() {
1088        return Some(Duration::MAX);
1089    }
1090    let remaining_points = (target_points - total_points as f64).max(0.0);
1091    Some(utils::duration_from_secs_f64_saturating(
1092        remaining_points / rate_per_second,
1093    ))
1094}
1095
1096fn min_duration_option(lhs: Option<Duration>, rhs: Option<Duration>) -> Option<Duration> {
1097    match (lhs, rhs) {
1098        (Some(lhs), Some(rhs)) => Some(lhs.min(rhs)),
1099        (Some(lhs), None) => Some(lhs),
1100        (None, Some(rhs)) => Some(rhs),
1101        (None, None) => None,
1102    }
1103}
1104
1105fn max_duration_option(lhs: Option<Duration>, rhs: Option<Duration>) -> Option<Duration> {
1106    match (lhs, rhs) {
1107        (Some(lhs), Some(rhs)) => Some(lhs.max(rhs)),
1108        (Some(lhs), None) => Some(lhs),
1109        (None, Some(rhs)) => Some(rhs),
1110        (None, None) => None,
1111    }
1112}
1113
1114fn styled_plain(text: impl Into<String>) -> StyledText {
1115    StyledText::plain(text)
1116}
1117
1118fn styled_colored(text: impl Into<String>, style: TextStyle) -> StyledText {
1119    StyledText::styled(text, style)
1120}
1121
1122fn format_eta_to_target_specification(
1123    target_relative_accuracy: Option<f64>,
1124    target_absolute_accuracy: Option<f64>,
1125) -> Option<String> {
1126    let relative_spec = target_relative_accuracy
1127        .filter(|target| *target >= 0.0)
1128        .map(|target| {
1129            format!(
1130                "% err <= {}",
1131                format_percentage_target_value_for_display(target * 100.0)
1132            )
1133        });
1134    let absolute_spec = target_absolute_accuracy
1135        .filter(|target| *target >= 0.0)
1136        .map(|target| {
1137            format!(
1138                "err <= {}",
1139                format_positive_target_value_for_display(target)
1140            )
1141        });
1142
1143    match (relative_spec, absolute_spec) {
1144        (Some(relative), Some(absolute)) => Some(format!("{relative} or {absolute}")),
1145        (Some(relative), None) => Some(relative),
1146        (None, Some(absolute)) => Some(absolute),
1147        (None, None) => None,
1148    }
1149}
1150
1151fn format_target_value_for_display(value: f64) -> String {
1152    if value == 0.0 {
1153        return String::from("+0e0");
1154    }
1155
1156    for precision in 0..=16 {
1157        let candidate = normalize_scientific_exponent(&format!("{:+.*e}", precision, value));
1158        let Ok(parsed) = candidate.parse::<f64>() else {
1159            continue;
1160        };
1161        if ulp_distance(parsed, value) <= 8 {
1162            return candidate;
1163        }
1164    }
1165
1166    normalize_scientific_exponent(&format!("{:+.16e}", value))
1167}
1168
1169fn normalize_scientific_exponent(formatted: &str) -> String {
1170    let Some((mantissa, exponent)) = formatted.rsplit_once('e') else {
1171        return formatted.to_string();
1172    };
1173    let exponent = exponent.parse::<i32>().unwrap_or_default();
1174    format!("{mantissa}e{exponent:+}")
1175}
1176
1177fn ulp_distance(lhs: f64, rhs: f64) -> u64 {
1178    if lhs.is_nan() || rhs.is_nan() {
1179        return u64::MAX;
1180    }
1181    ordered_f64_bits(lhs).abs_diff(ordered_f64_bits(rhs))
1182}
1183
1184fn ordered_f64_bits(value: f64) -> u64 {
1185    let bits = value.to_bits();
1186    if (bits >> 63) != 0 {
1187        !bits
1188    } else {
1189        bits | (1_u64 << 63)
1190    }
1191}
1192
1193fn format_positive_target_value_for_display(value: f64) -> String {
1194    format_target_value_for_display(value)
1195        .trim_start_matches('+')
1196        .to_string()
1197}
1198
1199fn format_percentage_target_value_for_display(value: f64) -> String {
1200    let formatted = if value.abs() <= 1.0e-4 {
1201        format_positive_target_value_for_display(value)
1202    } else {
1203        format_fixed_target_value_for_display(value)
1204    };
1205    format!("{formatted}%")
1206}
1207
1208fn format_fixed_target_value_for_display(value: f64) -> String {
1209    if value == 0.0 {
1210        return String::from("0");
1211    }
1212
1213    for precision in 0..=16 {
1214        let candidate = normalize_fixed_decimal(&format!("{value:.precision$}"));
1215        let Ok(parsed) = candidate.parse::<f64>() else {
1216            continue;
1217        };
1218        if ulp_distance(parsed, value) <= 8 {
1219            return candidate;
1220        }
1221    }
1222
1223    format_positive_target_value_for_display(value)
1224}
1225
1226fn normalize_fixed_decimal(formatted: &str) -> String {
1227    let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
1228    if trimmed.is_empty() || trimmed == "-0" {
1229        String::from("0")
1230    } else {
1231        trimmed.to_string()
1232    }
1233}
1234
1235fn format_value_field(avg: F<f64>, err: F<f64>) -> DisplayField<(F<f64>, F<f64>)> {
1236    let formatted = format_signed_uncertainty(avg, err, UncertaintyNotation::Scientific);
1237    let uncertainty_style = format_relative_error_field_from_estimate(avg, err)
1238        .map(|field| {
1239            field
1240                .display
1241                .spans
1242                .first()
1243                .map(|span| span.style)
1244                .unwrap_or(TextStyle::PLAIN)
1245                .bold()
1246        })
1247        .unwrap_or(TextStyle::PLAIN.bold());
1248
1249    let display = if let Some(start) = formatted.find('(') {
1250        if let Some(end_offset) = formatted[start..].find(')') {
1251            let end = start + end_offset + 1;
1252            let mut styled = StyledText::new();
1253            styled.push_text(&formatted[..start], TextStyle::blue().bold());
1254            styled.push_text(&formatted[start..end], uncertainty_style);
1255            styled.push_text(&formatted[end..], TextStyle::blue().bold());
1256            styled
1257        } else {
1258            styled_colored(formatted, TextStyle::blue().bold())
1259        }
1260    } else {
1261        styled_colored(formatted, TextStyle::blue().bold())
1262    };
1263
1264    DisplayField::new((avg, err), display)
1265}
1266
1267fn format_relative_error_field_from_estimate(
1268    avg: F<f64>,
1269    err: F<f64>,
1270) -> Option<DisplayField<f64>> {
1271    if avg.is_zero() {
1272        return None;
1273    }
1274
1275    let raw = (err / avg).abs().0 * 100.0;
1276    let formatted = format_significant_percentage(raw, 3, Some((1.0e4, 1.0e-4)));
1277    let style = if raw > 1.0 {
1278        TextStyle::red()
1279    } else {
1280        TextStyle::green()
1281    };
1282    Some(DisplayField::new(raw, styled_colored(formatted, style)))
1283}
1284
1285fn format_relative_error_field(itg: &StatisticsAccumulator<F<f64>>) -> Option<DisplayField<f64>> {
1286    format_relative_error_field_from_estimate(itg.avg, itg.err)
1287}
1288
1289fn format_chi_sq_field(
1290    itg: &StatisticsAccumulator<F<f64>>,
1291    i_iter: usize,
1292) -> Option<DisplayField<f64>> {
1293    if i_iter == 0 {
1294        return None;
1295    }
1296    let raw = itg.chi_sq.0 / (i_iter as f64);
1297    let style = if raw > 5.0 {
1298        TextStyle::red()
1299    } else {
1300        TextStyle::PLAIN
1301    };
1302    Some(DisplayField::new(
1303        raw,
1304        styled_colored(format!("{raw:.3}"), style),
1305    ))
1306}
1307
1308fn format_delta_fields_from_estimate(
1309    avg: F<f64>,
1310    err: F<f64>,
1311    target: Option<F<f64>>,
1312) -> (Option<DisplayField<f64>>, Option<DisplayField<f64>>) {
1313    let Some(target) = target else {
1314        return (None, None);
1315    };
1316
1317    let delta_sigma = if err.is_zero() {
1318        0.0
1319    } else {
1320        (target - avg).abs().0 / err.0
1321    };
1322    let delta_percent = if target.abs().is_non_zero() {
1323        (target - avg).abs().0 / target.abs().0 * 100.0
1324    } else {
1325        0.0
1326    };
1327    let is_outside_target = delta_sigma > 5.0
1328        || (target.abs().is_non_zero() && ((target - avg).abs() / target.abs()).0 > 0.01);
1329    let style = if is_outside_target {
1330        TextStyle::red()
1331    } else {
1332        TextStyle::green()
1333    };
1334
1335    (
1336        Some(DisplayField::new(
1337            delta_sigma,
1338            styled_colored(format!("Δ = {:.3}σ", delta_sigma), style),
1339        )),
1340        Some(DisplayField::new(
1341            delta_percent,
1342            styled_colored(format!("Δ = {:.3}%", delta_percent), style),
1343        )),
1344    )
1345}
1346
1347fn format_delta_fields(
1348    itg: &StatisticsAccumulator<F<f64>>,
1349    target: Option<F<f64>>,
1350) -> (Option<DisplayField<f64>>, Option<DisplayField<f64>>) {
1351    format_delta_fields_from_estimate(itg.avg, itg.err, target)
1352}
1353
1354fn format_mwi_field(itg: &StatisticsAccumulator<F<f64>>) -> Option<DisplayField<f64>> {
1355    let raw = max_weight_impact(itg).0;
1356    let style = if raw > 1.0 {
1357        TextStyle::red()
1358    } else {
1359        TextStyle::PLAIN
1360    };
1361    Some(DisplayField::new(
1362        raw,
1363        styled_colored(format!("{raw:.4e}"), style),
1364    ))
1365}
1366
1367fn contribution_display_field(
1368    contribution: ContributionKind,
1369    integration_state: &IntegrationState,
1370) -> DisplayField<ContributionKind> {
1371    let display = match contribution {
1372        ContributionKind::All => styled_colored("All", TextStyle::green().bold()),
1373        ContributionKind::Sum => styled_colored("Sum", TextStyle::green().bold()),
1374        ContributionKind::Bin(bin_index) => {
1375            if let (Some(axis_label), Some(descriptions)) = (
1376                integration_state
1377                    .first_non_trivial_discrete_label
1378                    .as_deref(),
1379                integration_state
1380                    .first_non_trivial_discrete_bin_descriptions
1381                    .as_ref(),
1382            ) && let Some(description) = descriptions.get(bin_index)
1383            {
1384                let mut text =
1385                    StyledText::plain(format_discrete_bin_prefix(bin_index, descriptions.len()));
1386                text.append(styled_bin_description(axis_label, description));
1387                text
1388            } else {
1389                styled_plain(format!("#{bin_index}"))
1390            }
1391        }
1392    };
1393    DisplayField::new(contribution, display)
1394}
1395
1396pub(crate) fn format_discrete_bin_prefix(bin_index: usize, bin_count: usize) -> String {
1397    let width = if bin_count < 100 {
1398        4
1399    } else if bin_count < 1_000 {
1400        5
1401    } else {
1402        6
1403    };
1404    format!("{:<width$}", format!("#{bin_index}:"))
1405}
1406
1407fn contribution_header(
1408    integration_state: &IntegrationState,
1409    discrete_monitoring_enabled: bool,
1410) -> StyledText {
1411    if discrete_monitoring_enabled
1412        && let Some(label) = integration_state
1413            .first_non_trivial_discrete_label
1414            .as_deref()
1415    {
1416        let mut header = StyledText::styled("Contribution", TextStyle::blue().bold());
1417        header.push_text("\n(idx=", TextStyle::PLAIN);
1418        header.push_text(label, TextStyle::blue().bold());
1419        header.push_text(")", TextStyle::PLAIN);
1420        return header;
1421    }
1422
1423    styled_colored("Contribution", TextStyle::blue().bold())
1424}
1425
1426fn header_left(
1427    elapsed_time: Duration,
1428    iter: usize,
1429    live_progress: Option<LiveIterationProgress>,
1430) -> StyledText {
1431    let mut text = StyledText::plain("[ ");
1432    text.push_text(
1433        format!(
1434            "{:^7}",
1435            utils::format_wdhms(elapsed_time.as_secs() as usize)
1436        ),
1437        TextStyle::PLAIN.bold(),
1438    );
1439    text.push_text(" ] ", TextStyle::PLAIN);
1440    let iteration_label = if live_progress.is_some() {
1441        format!("Iteration #{:-4} ( running   )", iter)
1442    } else {
1443        format!("Iteration #{:-4} ( completed )", iter)
1444    };
1445    text.push_text(iteration_label, TextStyle::green().bold());
1446    text
1447}
1448
1449fn header_middle(
1450    cur_points: usize,
1451    total_points: usize,
1452    live_progress: Option<LiveIterationProgress>,
1453) -> StyledText {
1454    let mut text = StyledText::new();
1455    if let Some(progress) = live_progress {
1456        text.push_text(
1457            format!(
1458                "Iteration progress {}/{} ",
1459                format_iteration_points(progress.completed_points),
1460                format_iteration_points(progress.target_points),
1461            ),
1462            TextStyle::PLAIN,
1463        );
1464        let percentage = if progress.target_points == 0 {
1465            String::from("0.0%")
1466        } else {
1467            format!(
1468                "{:.1}%",
1469                (progress.completed_points as f64) / (progress.target_points as f64) * 100.0
1470            )
1471        };
1472        text.push_text(percentage, TextStyle::green());
1473        text.push_text(" ", TextStyle::PLAIN);
1474    } else if cur_points > 0 {
1475        text.push_text(
1476            format!(
1477                "# samples per iteration = {} ",
1478                format_iteration_points(cur_points)
1479            ),
1480            TextStyle::blue().bold(),
1481        );
1482    }
1483    text.push_text(
1484        format!("# samples total = {}", format_total_points(total_points)),
1485        TextStyle::green().bold(),
1486    );
1487    text
1488}
1489
1490fn header_tail(cores: usize, elapsed_time: Duration, n_samples_evaluated: usize) -> StyledText {
1491    let mut text = StyledText::new();
1492    if n_samples_evaluated == 0 {
1493        text.push_text("N/A /sample/core", TextStyle::red());
1494    } else {
1495        text.push_text(
1496            format!(
1497                "{} /sample/core",
1498                utils::format_evaluation_time_from_f64(
1499                    elapsed_time.as_secs_f64() / (n_samples_evaluated as f64) * (cores as f64),
1500                )
1501            ),
1502            TextStyle::green().bold(),
1503        );
1504    }
1505    text.push_text(" ", TextStyle::PLAIN);
1506    text.push_text(format!("({cores} cores)"), TextStyle::blue().bold());
1507    text
1508}
1509
1510fn main_results_row(
1511    integration_state: &IntegrationState,
1512    targets: &[Option<Complex<F<f64>>>],
1513    monitored_path: Option<&[usize]>,
1514    contribution: ContributionKind,
1515    component: ComponentKind,
1516    has_discrete_columns: bool,
1517) -> Option<MainResultsRow> {
1518    let slot_cells = integration_state
1519        .slot_metas
1520        .iter()
1521        .enumerate()
1522        .map(|(slot_index, _)| match contribution {
1523            ContributionKind::All => {
1524                let accumulator =
1525                    component_accumulator(&integration_state.all_integrals[slot_index], component);
1526                Some(MainTableSlotCells {
1527                    value: Some(format_value_field(accumulator.avg, accumulator.err)),
1528                    relative_error: format_relative_error_field(accumulator),
1529                    chi_sq: format_chi_sq_field(accumulator, integration_state.iter),
1530                    max_weight_impact: format_mwi_field(accumulator),
1531                    sample_fraction: None,
1532                    sample_count: None,
1533                    target_pdf: None,
1534                })
1535            }
1536            ContributionKind::Sum => {
1537                let summary = slot_component_summary(integration_state, slot_index, component)
1538                    .and_then(|summary| {
1539                        monitored_path.and_then(|path| summary_at_path(summary, path))
1540                    })?;
1541                let (avg, err) = sum_estimate_error(summary);
1542                Some(MainTableSlotCells {
1543                    value: Some(format_value_field(avg, err)),
1544                    relative_error: format_relative_error_field_from_estimate(avg, err),
1545                    chi_sq: None,
1546                    max_weight_impact: None,
1547                    sample_fraction: None,
1548                    sample_count: None,
1549                    target_pdf: None,
1550                })
1551            }
1552            ContributionKind::Bin(bin_index) => {
1553                let slot_context =
1554                    integration_state.monitored_discrete_context_for_slot(slot_index);
1555                let summary = slot_component_summary(integration_state, slot_index, component)
1556                    .and_then(|summary| {
1557                        monitored_path.and_then(|path| summary_at_path(summary, path))
1558                    })?;
1559                let bin = summary.bins.get(bin_index)?;
1560                let total_samples = total_processed_samples(summary);
1561                let sample_fraction = if has_discrete_columns && total_samples > 0 {
1562                    let raw =
1563                        bin.accumulator.processed_samples as f64 / total_samples as f64 * 100.0;
1564                    Some(DisplayField::new(
1565                        raw,
1566                        styled_colored(
1567                            format_significant_percentage(raw, 3, None),
1568                            TextStyle::blue(),
1569                        ),
1570                    ))
1571                } else {
1572                    None
1573                };
1574                let sample_count = if has_discrete_columns {
1575                    Some(DisplayField::new(
1576                        bin.accumulator.processed_samples,
1577                        styled_colored(
1578                            format_abbreviated_count(bin.accumulator.processed_samples),
1579                            TextStyle::blue(),
1580                        ),
1581                    ))
1582                } else {
1583                    None
1584                };
1585                let target_pdf = if has_discrete_columns {
1586                    slot_context
1587                        .as_ref()
1588                        .and_then(|ctx| ctx.pdfs.get(bin_index).copied())
1589                        .map(|pdf| {
1590                            DisplayField::new(
1591                                pdf.0 * 100.0,
1592                                styled_plain(format_significant_percentage(pdf.0 * 100.0, 3, None)),
1593                            )
1594                        })
1595                } else {
1596                    None
1597                };
1598                Some(MainTableSlotCells {
1599                    value: Some(format_value_field(bin.accumulator.avg, bin.accumulator.err)),
1600                    relative_error: format_relative_error_field(&bin.accumulator),
1601                    chi_sq: format_chi_sq_field(&bin.accumulator, integration_state.iter),
1602                    max_weight_impact: format_mwi_field(&bin.accumulator),
1603                    sample_fraction,
1604                    sample_count,
1605                    target_pdf,
1606                })
1607            }
1608        })
1609        .collect::<Option<Vec<_>>>()?;
1610
1611    let slot0_target = targets
1612        .first()
1613        .and_then(|target| target.as_ref())
1614        .map(|target| match component {
1615            ComponentKind::Real => target.re,
1616            ComponentKind::Imag => target.im,
1617        });
1618    let (chi_sq, delta_sigma, delta_percent, max_weight_impact) = match contribution {
1619        ContributionKind::All => {
1620            let accumulator = component_accumulator(&integration_state.all_integrals[0], component);
1621            let (delta_sigma, delta_percent) = format_delta_fields(accumulator, slot0_target);
1622            (
1623                format_chi_sq_field(accumulator, integration_state.iter),
1624                delta_sigma,
1625                delta_percent,
1626                format_mwi_field(accumulator),
1627            )
1628        }
1629        ContributionKind::Sum => {
1630            let summary =
1631                slot_component_summary(integration_state, 0, component).and_then(|summary| {
1632                    monitored_path.and_then(|path| summary_at_path(summary, path))
1633                })?;
1634            let (avg, err) = sum_estimate_error(summary);
1635            let (delta_sigma, delta_percent) =
1636                format_delta_fields_from_estimate(avg, err, slot0_target);
1637            (None, delta_sigma, delta_percent, None)
1638        }
1639        ContributionKind::Bin(bin_index) => {
1640            let summary =
1641                slot_component_summary(integration_state, 0, component).and_then(|summary| {
1642                    monitored_path.and_then(|path| summary_at_path(summary, path))
1643                })?;
1644            let bin = summary.bins.get(bin_index)?;
1645            (
1646                format_chi_sq_field(&bin.accumulator, integration_state.iter),
1647                None,
1648                None,
1649                format_mwi_field(&bin.accumulator),
1650            )
1651        }
1652    };
1653
1654    Some(MainResultsRow {
1655        contribution: contribution_display_field(contribution, integration_state),
1656        component: component.display_field(),
1657        slot_cells,
1658        chi_sq,
1659        delta_sigma,
1660        delta_percent,
1661        max_weight_impact,
1662    })
1663}
1664
1665fn discrete_sort_key(
1666    integration_state: &IntegrationState,
1667    monitored_path: &[usize],
1668    component: ComponentKind,
1669    bin_index: usize,
1670    sort_mode: ContributionSortMode,
1671) -> f64 {
1672    let Some(summary) = slot_component_summary(integration_state, 0, component)
1673        .and_then(|summary| summary_at_path(summary, monitored_path))
1674    else {
1675        return 0.0;
1676    };
1677    let Some(bin) = summary.bins.get(bin_index) else {
1678        return 0.0;
1679    };
1680    match sort_mode {
1681        ContributionSortMode::Integral => bin.accumulator.avg.abs().0,
1682        ContributionSortMode::Error => bin.accumulator.err.0,
1683        ContributionSortMode::Index => bin_index as f64,
1684    }
1685}
1686
1687fn build_main_results_section(request: &StatusUpdateBuildRequest<'_>) -> MainResultsSection {
1688    let components = ComponentKind::all_for_display(request.render_options.phase_display);
1689    let discrete_context = request.integration_state.monitored_discrete_context();
1690    let monitored_path = request.integration_state.monitored_discrete_path.as_deref();
1691    let has_discrete_columns = monitored_path.is_some();
1692    let has_target_columns = request.targets.first().is_some_and(Option::is_some);
1693
1694    let mut row_groups = vec![MainResultsRowGroup {
1695        kind: MainResultsRowGroupKind::All,
1696        rows: components
1697            .iter()
1698            .filter_map(|component| {
1699                main_results_row(
1700                    request.integration_state,
1701                    request.targets,
1702                    monitored_path,
1703                    ContributionKind::All,
1704                    *component,
1705                    has_discrete_columns,
1706                )
1707            })
1708            .collect_vec(),
1709    }];
1710
1711    if let Some(discrete_context) = discrete_context.as_ref() {
1712        let sum_rows = components
1713            .iter()
1714            .filter_map(|component| {
1715                main_results_row(
1716                    request.integration_state,
1717                    request.targets,
1718                    monitored_path,
1719                    ContributionKind::Sum,
1720                    *component,
1721                    has_discrete_columns,
1722                )
1723            })
1724            .collect_vec();
1725        if !sum_rows.is_empty() {
1726            row_groups.push(MainResultsRowGroup {
1727                kind: MainResultsRowGroupKind::Sum,
1728                rows: sum_rows,
1729            });
1730        }
1731
1732        let bin_count = discrete_context.pdfs.len();
1733        match request.render_options.contribution_sort {
1734            ContributionSortMode::Index => {
1735                let mut rows = Vec::new();
1736                for bin_index in 0..bin_count {
1737                    for component in &components {
1738                        if let Some(row) = main_results_row(
1739                            request.integration_state,
1740                            request.targets,
1741                            monitored_path,
1742                            ContributionKind::Bin(bin_index),
1743                            *component,
1744                            has_discrete_columns,
1745                        ) {
1746                            rows.push(row);
1747                        }
1748                    }
1749                }
1750                if !rows.is_empty() {
1751                    row_groups.push(MainResultsRowGroup {
1752                        kind: MainResultsRowGroupKind::Bins,
1753                        rows,
1754                    });
1755                }
1756            }
1757            ContributionSortMode::Integral | ContributionSortMode::Error => {
1758                for component in &components {
1759                    let mut bin_indices = (0..bin_count).collect_vec();
1760                    bin_indices.sort_by(|lhs, rhs| {
1761                        discrete_sort_key(
1762                            request.integration_state,
1763                            &discrete_context.path,
1764                            *component,
1765                            *rhs,
1766                            request.render_options.contribution_sort,
1767                        )
1768                        .partial_cmp(&discrete_sort_key(
1769                            request.integration_state,
1770                            &discrete_context.path,
1771                            *component,
1772                            *lhs,
1773                            request.render_options.contribution_sort,
1774                        ))
1775                        .unwrap_or(std::cmp::Ordering::Equal)
1776                    });
1777                    let rows = bin_indices
1778                        .into_iter()
1779                        .filter_map(|bin_index| {
1780                            main_results_row(
1781                                request.integration_state,
1782                                request.targets,
1783                                monitored_path,
1784                                ContributionKind::Bin(bin_index),
1785                                *component,
1786                                has_discrete_columns,
1787                            )
1788                        })
1789                        .collect_vec();
1790                    if !rows.is_empty() {
1791                        row_groups.push(MainResultsRowGroup {
1792                            kind: MainResultsRowGroupKind::Bins,
1793                            rows,
1794                        });
1795                    }
1796                }
1797            }
1798        }
1799    }
1800
1801    MainResultsSection {
1802        header_left: header_left(
1803            request.elapsed_time,
1804            request.integration_state.iter,
1805            request.live_progress,
1806        ),
1807        header_middle: header_middle(
1808            request.cur_points,
1809            request.total_points_display,
1810            request.live_progress,
1811        ),
1812        header_tail: header_tail(
1813            request.cores,
1814            request.elapsed_time,
1815            request.n_samples_evaluated,
1816        ),
1817        contribution_header: contribution_header(request.integration_state, has_discrete_columns),
1818        slot_headers: request
1819            .integration_state
1820            .slot_metas
1821            .iter()
1822            .map(|slot_meta| styled_colored(slot_meta.key(), TextStyle::blue().bold()))
1823            .collect(),
1824        has_discrete_columns,
1825        has_target_columns,
1826        row_groups,
1827    }
1828}
1829
1830fn max_weight_row_descriptors(
1831    phase_display: IntegrationStatusPhaseDisplay,
1832) -> Vec<(ComponentKind, &'static str, bool)> {
1833    let mut rows = Vec::new();
1834    if phase_display.shows_real() {
1835        rows.push((ComponentKind::Real, "+", true));
1836        rows.push((ComponentKind::Real, "-", false));
1837    }
1838    if phase_display.shows_imag() {
1839        rows.push((ComponentKind::Imag, "+", true));
1840        rows.push((ComponentKind::Imag, "-", false));
1841    }
1842    rows
1843}
1844
1845fn styled_component_sign(
1846    component: ComponentKind,
1847    sign: &'static str,
1848    positive: bool,
1849) -> DisplayField<(ComponentKind, bool)> {
1850    let mut display = StyledText::new();
1851    display.append(component.label_display());
1852    display.push_text(" [", TextStyle::PLAIN);
1853    display.push_text(sign, TextStyle::blue());
1854    display.push_text("]", TextStyle::PLAIN);
1855    DisplayField::new((component, positive), display)
1856}
1857
1858fn build_max_weight_details_section(
1859    integration_state: &IntegrationState,
1860    render_options: &IntegrationStatusViewOptions,
1861) -> Option<MaxWeightDetailsSection> {
1862    let rows_by_slot = integration_state
1863        .slot_metas
1864        .iter()
1865        .enumerate()
1866        .zip(integration_state.all_integrals.iter())
1867        .filter_map(|((slot_index, slot_meta), integral)| {
1868            let rows = max_weight_row_descriptors(render_options.phase_display)
1869                .into_iter()
1870                .filter_map(|(component, sign, positive)| {
1871                    let accumulator = match component {
1872                        ComponentKind::Real => &integral.re,
1873                        ComponentKind::Imag => &integral.im,
1874                    };
1875                    let (value, coordinates) = max_eval_entry(accumulator, positive)?;
1876                    Some(MaxWeightDetailsRow {
1877                        slot: DisplayField::new(
1878                            slot_meta.key(),
1879                            styled_colored(slot_meta.key(), TextStyle::green()),
1880                        ),
1881                        component_sign: styled_component_sign(component, sign, positive),
1882                        max_eval: DisplayField::new(
1883                            value,
1884                            styled_plain(format!("{:+.16e}", value)),
1885                        ),
1886                        coordinates: DisplayField::new(
1887                            coordinates
1888                                .map(|sample| {
1889                                    format_max_eval_sample(
1890                                        sample,
1891                                        &integration_state
1892                                            .sampling_state_for_slot(slot_index)
1893                                            .discrete_axis_labels,
1894                                        &[],
1895                                    )
1896                                })
1897                                .unwrap_or_else(|| "N/A".to_string()),
1898                            styled_plain(
1899                                coordinates
1900                                    .map(|sample| {
1901                                        format_max_eval_sample(
1902                                            sample,
1903                                            &integration_state
1904                                                .sampling_state_for_slot(slot_index)
1905                                                .discrete_axis_labels,
1906                                            &[],
1907                                        )
1908                                    })
1909                                    .unwrap_or_else(|| "N/A".to_string()),
1910                            ),
1911                        ),
1912                    })
1913                })
1914                .collect_vec();
1915            (!rows.is_empty()).then_some(rows)
1916        })
1917        .collect_vec();
1918
1919    if rows_by_slot.is_empty() {
1920        None
1921    } else {
1922        Some(MaxWeightDetailsSection { rows_by_slot })
1923    }
1924}
1925
1926fn build_discrete_max_weight_details_section(
1927    integration_state: &IntegrationState,
1928    render_options: &IntegrationStatusViewOptions,
1929) -> Option<DiscreteMaxWeightDetailsSection> {
1930    let discrete_context = integration_state.monitored_discrete_context()?;
1931    let contributions = std::iter::once(ContributionKind::All)
1932        .chain((0..discrete_context.pdfs.len()).map(ContributionKind::Bin))
1933        .collect_vec();
1934
1935    let mut row_groups = Vec::new();
1936    for contribution in contributions {
1937        let mut rows = Vec::new();
1938        for (component, sign, positive) in max_weight_row_descriptors(render_options.phase_display)
1939        {
1940            let slot_values = integration_state
1941                .slot_metas
1942                .iter()
1943                .enumerate()
1944                .map(|(slot_index, _)| {
1945                    let accumulator = match contribution {
1946                        ContributionKind::All => {
1947                            let integral = &integration_state.all_integrals[slot_index];
1948                            match component {
1949                                ComponentKind::Real => &integral.re,
1950                                ComponentKind::Imag => &integral.im,
1951                            }
1952                        }
1953                        ContributionKind::Bin(bin_index) => {
1954                            let summary =
1955                                slot_component_summary(integration_state, slot_index, component)
1956                                    .and_then(|summary| {
1957                                        summary_at_path(summary, &discrete_context.path)
1958                                    })?;
1959                            &summary.bins.get(bin_index)?.accumulator
1960                        }
1961                        ContributionKind::Sum => return None,
1962                    };
1963                    Some(max_eval_entry(accumulator, positive).map(|(value, _)| {
1964                        DisplayField::new(value, styled_plain(format!("{:+.16e}", value)))
1965                    }))
1966                })
1967                .collect::<Option<Vec<_>>>()?;
1968
1969            let slot_coordinates = integration_state
1970                .slot_metas
1971                .iter()
1972                .enumerate()
1973                .filter_map(|(slot_index, slot_meta)| {
1974                    let coordinates = match contribution {
1975                        ContributionKind::All => {
1976                            let accumulator = match component {
1977                                ComponentKind::Real => {
1978                                    &integration_state.all_integrals[slot_index].re
1979                                }
1980                                ComponentKind::Imag => {
1981                                    &integration_state.all_integrals[slot_index].im
1982                                }
1983                            };
1984                            max_eval_entry(accumulator, positive)
1985                                .and_then(|(_, sample)| sample)
1986                                .map(|sample| {
1987                                    format_max_eval_sample(
1988                                        sample,
1989                                        &integration_state
1990                                            .sampling_state_for_slot(slot_index)
1991                                            .discrete_axis_labels,
1992                                        &[],
1993                                    )
1994                                })
1995                        }
1996                        ContributionKind::Bin(bin_index) => {
1997                            let slot_context = integration_state
1998                                .monitored_discrete_context_for_slot(slot_index)?;
1999                            let summary =
2000                                slot_component_summary(integration_state, slot_index, component)
2001                                    .and_then(|summary| {
2002                                        summary_at_path(summary, &slot_context.path)
2003                                    })?;
2004                            max_eval_entry(&summary.bins.get(bin_index)?.accumulator, positive)
2005                                .and_then(|(_, sample)| sample)
2006                                .map(|sample| {
2007                                    format_max_eval_sample(
2008                                        sample,
2009                                        &integration_state
2010                                            .sampling_state_for_slot(slot_index)
2011                                            .discrete_axis_labels,
2012                                        &slot_context.path,
2013                                    )
2014                                })
2015                        }
2016                        ContributionKind::Sum => None,
2017                    }?;
2018
2019                    Some(SlotCoordinateEntry {
2020                        slot: DisplayField::new(
2021                            slot_meta.key(),
2022                            styled_colored(slot_meta.key(), TextStyle::green()),
2023                        ),
2024                        coordinates: DisplayField::new(
2025                            coordinates.clone(),
2026                            styled_plain(coordinates),
2027                        ),
2028                    })
2029                })
2030                .collect_vec();
2031
2032            if slot_values.iter().all(|value| value.is_none()) && slot_coordinates.is_empty() {
2033                continue;
2034            }
2035
2036            rows.push(DiscreteMaxWeightRow {
2037                contribution: contribution_display_field(contribution, integration_state),
2038                component_sign: styled_component_sign(component, sign, positive),
2039                slot_values,
2040                slot_coordinates,
2041            });
2042        }
2043        if !rows.is_empty() {
2044            row_groups.push(rows);
2045        }
2046    }
2047
2048    if row_groups.is_empty() {
2049        return None;
2050    }
2051
2052    Some(DiscreteMaxWeightDetailsSection {
2053        contribution_header: contribution_header(integration_state, true),
2054        slot_headers: integration_state
2055            .slot_metas
2056            .iter()
2057            .map(|slot_meta| styled_colored(slot_meta.key(), TextStyle::blue().bold()))
2058            .collect(),
2059        row_groups,
2060    })
2061}
2062
2063pub(crate) fn build_status_update(request: StatusUpdateBuildRequest<'_>) -> StatusUpdate {
2064    let target_accuracy_status = evaluate_target_accuracy(
2065        request.integration_state,
2066        request.total_points_display,
2067        request.elapsed_time,
2068        request.targets,
2069        request.render_options.training_phase_display,
2070        request.render_options.target_relative_accuracy,
2071        request.render_options.target_absolute_accuracy,
2072    );
2073    StatusUpdate {
2074        kind: request.kind,
2075        meta: StatusMeta {
2076            elapsed_time: request.elapsed_time,
2077            iteration_elapsed_time: request.iteration_elapsed_time,
2078            iteration: request.integration_state.iter,
2079            current_iteration_points: request.cur_points,
2080            total_points: request.total_points_display,
2081            n_samples_evaluated: request.n_samples_evaluated,
2082            cores: request.cores,
2083            training_slot: request.render_options.training_slot,
2084            training_phase_display: request.render_options.training_phase_display,
2085            live_progress: request.live_progress,
2086            show_eta_to_target: request.render_options.target_relative_accuracy.is_some()
2087                || request.render_options.target_absolute_accuracy.is_some(),
2088            eta_to_target_specification: format_eta_to_target_specification(
2089                request.render_options.target_relative_accuracy,
2090                request.render_options.target_absolute_accuracy,
2091            ),
2092            eta_to_target: target_accuracy_status.eta_to_target,
2093        },
2094        targets: request.targets.to_vec(),
2095        slot_training_phase_displays: request.render_options.slot_training_phase_displays.clone(),
2096        per_slot_training_phase: request.render_options.per_slot_training_phase,
2097        main_results: build_main_results_section(&request),
2098        max_weight_details: build_max_weight_details_section(
2099            request.integration_state,
2100            request.render_options,
2101        ),
2102        discrete_max_weight_details: build_discrete_max_weight_details_section(
2103            request.integration_state,
2104            request.render_options,
2105        ),
2106        statistics: Some(StatisticsSection {
2107            global: request.integration_state.stats,
2108            slot_counters: request.integration_state.slot_stats.clone(),
2109            slot_labels: request
2110                .integration_state
2111                .slot_metas
2112                .iter()
2113                .map(|slot_meta| slot_meta.key())
2114                .collect(),
2115        }),
2116    }
2117}
2118
2119pub(crate) fn build_saved_status_update(
2120    integration_state: &IntegrationState,
2121    targets: &[Option<Complex<F<f64>>>],
2122    render_options: &IntegrationStatusViewOptions,
2123) -> StatusUpdate {
2124    let final_render_options = render_options.clone().for_final();
2125    build_status_update(StatusUpdateBuildRequest {
2126        kind: IntegrationStatusKind::Final,
2127        integration_state,
2128        cores: integration_state.n_cores.max(1),
2129        elapsed_time: utils::duration_from_secs_f64_saturating(integration_state.elapsed_seconds),
2130        iteration_elapsed_time: Duration::ZERO,
2131        cur_points: 0,
2132        total_points_display: integration_state.num_points,
2133        n_samples_evaluated: integration_state.num_points,
2134        targets,
2135        render_options: &final_render_options,
2136        live_progress: None,
2137    })
2138}