1use std::cmp::Ordering;
2use std::time::Duration;
3
4use ratatui::{
5 Frame,
6 layout::{Alignment, Constraint, Direction, Layout, Rect},
7 prelude::{Color, Line, Modifier, Span, Style},
8 symbols::Marker,
9 text::Text,
10 widgets::{
11 Axis, Block, Borders, Cell, Chart, Clear, Dataset, Gauge, Paragraph, Row, Table,
12 TableState, Tabs, Wrap,
13 },
14};
15
16use crate::utils;
17
18use super::{
19 StatusUpdate,
20 display::{StyledText, TextColor, TextStyle},
21 status_update::{
22 ComponentKind, ContributionKind, ContributionSortMode, IntegralView, MainResultsRow,
23 MainResultsRowGroupKind, MainTableSlotCells, StatisticsMedianEntry, StatisticsMixSegment,
24 StatisticsScope,
25 },
26};
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29enum DashboardTab {
30 Overview,
31 Discrete,
32 MaxWeight,
33}
34
35impl DashboardTab {
36 fn all() -> [Self; 3] {
37 [Self::Overview, Self::Discrete, Self::MaxWeight]
38 }
39
40 fn index(self) -> usize {
41 match self {
42 Self::Overview => 0,
43 Self::Discrete => 1,
44 Self::MaxWeight => 2,
45 }
46 }
47
48 fn title(self) -> &'static str {
49 match self {
50 Self::Overview => "Overview",
51 Self::Discrete => "Discrete",
52 Self::MaxWeight => "Max Weight",
53 }
54 }
55
56 fn from_index(index: usize) -> Self {
57 Self::all().get(index).copied().unwrap_or(Self::Overview)
58 }
59
60 fn next(self) -> Self {
61 Self::from_index((self.index() + 1) % Self::all().len())
62 }
63
64 fn previous(self) -> Self {
65 Self::from_index((self.index() + Self::all().len() - 1) % Self::all().len())
66 }
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70enum DensityMode {
71 Compact,
72 Metrics,
73 Full,
74}
75
76#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
77enum ChartHistoryWindow {
78 #[default]
79 Full,
80 RecentIterations(usize),
81}
82
83#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
84enum DashboardStatisticsScope {
85 #[default]
86 Global,
87 FocusedSlot,
88}
89
90const MAX_HISTORY_POINTS: usize = 4096;
91
92impl ChartHistoryWindow {
93 fn description(self) -> String {
94 match self {
95 Self::Full => "full".to_string(),
96 Self::RecentIterations(count) => format!("last {count} iters"),
97 }
98 }
99
100 fn toggle(self) -> Self {
101 match self {
102 Self::Full => Self::RecentIterations(6),
103 Self::RecentIterations(_) => Self::Full,
104 }
105 }
106
107 fn widen(self) -> Self {
108 match self {
109 Self::Full => Self::RecentIterations(6),
110 Self::RecentIterations(count) => Self::RecentIterations((count + 1).min(128)),
111 }
112 }
113
114 fn narrow(self) -> Self {
115 match self {
116 Self::Full => Self::RecentIterations(6),
117 Self::RecentIterations(count) => Self::RecentIterations(count.saturating_sub(1).max(1)),
118 }
119 }
120}
121
122impl DensityMode {
123 fn next(self) -> Self {
124 match self {
125 Self::Compact => Self::Metrics,
126 Self::Metrics => Self::Full,
127 Self::Full => Self::Compact,
128 }
129 }
130
131 fn label(self) -> &'static str {
132 match self {
133 Self::Compact => "compact",
134 Self::Metrics => "metrics",
135 Self::Full => "full",
136 }
137 }
138}
139
140#[derive(Clone, Copy, Debug)]
141struct SlotMetricVisibility {
142 relative_error: bool,
143 chi_sq: bool,
144 max_weight_impact: bool,
145}
146
147impl Default for SlotMetricVisibility {
148 fn default() -> Self {
149 Self {
150 relative_error: true,
151 chi_sq: true,
152 max_weight_impact: true,
153 }
154 }
155}
156
157#[derive(Clone, Debug)]
158struct HistoryPoint {
159 iteration: usize,
160 samples: usize,
161 signed_real_slot_values: Vec<Option<(f64, f64)>>,
162 signed_imag_slot_values: Vec<Option<(f64, f64)>>,
163 absolute_real_slot_values: Vec<Option<(f64, f64)>>,
164 absolute_imag_slot_values: Vec<Option<(f64, f64)>>,
165 completed_iteration: bool,
166}
167
168impl HistoryPoint {
169 fn slot_values(&self, component: ComponentKind, view: IntegralView) -> &[Option<(f64, f64)>] {
170 match (view, component) {
171 (IntegralView::Signed, ComponentKind::Real) => &self.signed_real_slot_values,
172 (IntegralView::Signed, ComponentKind::Imag) => &self.signed_imag_slot_values,
173 (IntegralView::Absolute, ComponentKind::Real) => &self.absolute_real_slot_values,
174 (IntegralView::Absolute, ComponentKind::Imag) => &self.absolute_imag_slot_values,
175 }
176 }
177}
178
179#[derive(Clone, Copy, Debug)]
180struct DiscreteRowRef<'a> {
181 row: &'a MainResultsRow,
182}
183
184impl<'a> DiscreteRowRef<'a> {
185 fn component(self) -> ComponentKind {
186 self.row.component.raw
187 }
188
189 fn contribution(self) -> ContributionKind {
190 self.row.contribution.raw
191 }
192
193 fn sort_value(self, mode: ContributionSortMode, slot_index: usize, view: IntegralView) -> f64 {
194 let Some(cell) = self.row.slot_cell(slot_index) else {
195 return 0.0;
196 };
197 let statistics = cell.statistics(view);
198 match mode {
199 ContributionSortMode::Index => match self.contribution() {
200 ContributionKind::Bin(index) => index as f64,
201 ContributionKind::All => -1.0,
202 ContributionKind::Sum => -0.5,
203 },
204 ContributionSortMode::Integral => statistics
205 .value
206 .as_ref()
207 .map(|value| value.raw.0.abs().0)
208 .unwrap_or_default(),
209 ContributionSortMode::Error => statistics
210 .value
211 .as_ref()
212 .map(|value| value.raw.1.abs().0)
213 .unwrap_or_default(),
214 }
215 }
216}
217
218pub struct RatatuiDashboardState {
219 latest_update: Option<StatusUpdate>,
220 integral_view: IntegralView,
221 active_tab: DashboardTab,
222 density: DensityMode,
223 metric_visibility: SlotMetricVisibility,
224 focused_slot: usize,
225 statistics_scope: DashboardStatisticsScope,
226 selected_discrete_row: usize,
227 discrete_sort: ContributionSortMode,
228 discrete_descending: bool,
229 show_help: bool,
230 history: Vec<HistoryPoint>,
231 chart_history_window: ChartHistoryWindow,
232 chart_y_sigma_span: usize,
233 chart_component: Option<ComponentKind>,
234}
235
236impl Default for RatatuiDashboardState {
237 fn default() -> Self {
238 Self {
239 latest_update: None,
240 integral_view: IntegralView::Signed,
241 active_tab: DashboardTab::Overview,
242 density: DensityMode::Metrics,
243 metric_visibility: SlotMetricVisibility::default(),
244 focused_slot: 0,
245 statistics_scope: DashboardStatisticsScope::Global,
246 selected_discrete_row: 0,
247 discrete_sort: ContributionSortMode::Error,
248 discrete_descending: true,
249 show_help: false,
250 history: Vec::new(),
251 chart_history_window: ChartHistoryWindow::default(),
252 chart_y_sigma_span: 4,
253 chart_component: None,
254 }
255 }
256}
257
258impl RatatuiDashboardState {
259 pub fn new() -> Self {
260 Self::default()
261 }
262
263 pub fn has_update(&self) -> bool {
264 self.latest_update.is_some()
265 }
266
267 pub fn is_absolute_view(&self) -> bool {
268 self.integral_view == IntegralView::Absolute
269 }
270
271 pub fn toggle_integral_view(&mut self) {
272 let selected_row = self.selected_discrete_key();
273 self.integral_view = match self.integral_view {
274 IntegralView::Signed => IntegralView::Absolute,
275 IntegralView::Absolute => IntegralView::Signed,
276 };
277 self.restore_discrete_selection(selected_row);
278 }
279
280 pub fn update(&mut self, update: StatusUpdate) {
281 self.push_history_point(&update);
282 self.latest_update = Some(update);
283 self.clamp_state();
284 }
285
286 pub fn next_tab(&mut self) {
287 self.active_tab = self.active_tab.next();
288 }
289
290 pub fn previous_tab(&mut self) {
291 self.active_tab = self.active_tab.previous();
292 }
293
294 pub fn select_tab(&mut self, index: usize) {
295 self.active_tab = DashboardTab::from_index(index);
296 }
297
298 pub fn cycle_density(&mut self) {
299 self.density = self.density.next();
300 }
301
302 pub fn toggle_help(&mut self) {
303 self.show_help = !self.show_help;
304 }
305
306 pub fn toggle_relative_error(&mut self) {
307 self.metric_visibility.relative_error = !self.metric_visibility.relative_error;
308 }
309
310 pub fn toggle_chi_sq(&mut self) {
311 self.metric_visibility.chi_sq = !self.metric_visibility.chi_sq;
312 }
313
314 pub fn toggle_max_weight_impact(&mut self) {
315 self.metric_visibility.max_weight_impact = !self.metric_visibility.max_weight_impact;
316 }
317
318 pub fn focus_next_slot(&mut self) {
319 let slot_count = self
320 .latest_update
321 .as_ref()
322 .map(|update| update.main_results.slot_headers.len())
323 .unwrap_or_default();
324 if slot_count > 0 {
325 self.focused_slot = (self.focused_slot + 1) % slot_count;
326 }
327 self.clamp_state();
328 }
329
330 pub fn focus_previous_slot(&mut self) {
331 let slot_count = self
332 .latest_update
333 .as_ref()
334 .map(|update| update.main_results.slot_headers.len())
335 .unwrap_or_default();
336 if slot_count > 0 {
337 self.focused_slot = (self.focused_slot + slot_count - 1) % slot_count;
338 }
339 self.clamp_state();
340 }
341
342 pub fn toggle_statistics_scope(&mut self) {
343 self.statistics_scope = match self.statistics_scope {
344 DashboardStatisticsScope::Global => DashboardStatisticsScope::FocusedSlot,
345 DashboardStatisticsScope::FocusedSlot => DashboardStatisticsScope::Global,
346 };
347 }
348
349 pub fn select_next_discrete_row(&mut self) {
350 let row_count = self.discrete_rows().len();
351 if row_count > 0 {
352 self.selected_discrete_row = (self.selected_discrete_row + 1) % row_count;
353 }
354 }
355
356 pub fn select_previous_discrete_row(&mut self) {
357 let row_count = self.discrete_rows().len();
358 if row_count > 0 {
359 self.selected_discrete_row = (self.selected_discrete_row + row_count - 1) % row_count;
360 }
361 }
362
363 pub fn cycle_discrete_sort(&mut self) {
364 let selected_row = self.selected_discrete_key();
365 self.discrete_sort = match self.discrete_sort {
366 ContributionSortMode::Index => ContributionSortMode::Integral,
367 ContributionSortMode::Integral => ContributionSortMode::Error,
368 ContributionSortMode::Error => ContributionSortMode::Index,
369 };
370 self.clamp_state();
371 self.restore_discrete_selection(selected_row);
372 }
373
374 pub fn toggle_discrete_sort_direction(&mut self) {
375 let selected_row = self.selected_discrete_key();
376 self.discrete_descending = !self.discrete_descending;
377 self.restore_discrete_selection(selected_row);
378 }
379
380 pub fn toggle_chart_history_window(&mut self) {
381 self.chart_history_window = self.chart_history_window.toggle();
382 }
383
384 pub fn widen_chart_history_window(&mut self) {
385 self.chart_history_window = self.chart_history_window.widen();
386 }
387
388 pub fn narrow_chart_history_window(&mut self) {
389 self.chart_history_window = self.chart_history_window.narrow();
390 }
391
392 pub fn widen_chart_y_sigma_span(&mut self) {
393 self.chart_y_sigma_span = (self.chart_y_sigma_span + 1).min(128);
394 }
395
396 pub fn narrow_chart_y_sigma_span(&mut self) {
397 self.chart_y_sigma_span = self.chart_y_sigma_span.saturating_sub(1).max(1);
398 }
399
400 pub fn reset_chart_y_sigma_span(&mut self) {
401 self.chart_y_sigma_span = 4;
402 }
403
404 pub fn toggle_chart_component(&mut self) {
405 let Some(update) = self.latest_update.as_ref() else {
406 return;
407 };
408 let available = self.available_chart_components(update);
409 if available.len() < 2 {
410 return;
411 }
412
413 let current = self.chart_component(update);
414 self.chart_component = match current {
415 Some(ComponentKind::Real) if available.contains(&ComponentKind::Imag) => {
416 Some(ComponentKind::Imag)
417 }
418 Some(ComponentKind::Imag) if available.contains(&ComponentKind::Real) => {
419 Some(ComponentKind::Real)
420 }
421 _ => available.first().copied(),
422 };
423 }
424
425 pub fn draw(&self, frame: &mut Frame<'_>) {
426 let area = frame.area();
427 if let Some(update) = self.latest_update.as_ref() {
428 self.draw_dashboard(frame, area, update);
429 if self.show_help {
430 self.draw_help_overlay(frame, area);
431 }
432 } else {
433 frame.render_widget(
434 Paragraph::new("Waiting for integration status updates...")
435 .block(titled_block("Integration dashboard")),
436 area,
437 );
438 }
439 }
440
441 fn clamp_state(&mut self) {
442 if let Some(update) = self.latest_update.as_ref() {
443 let slot_count = update.main_results.slot_headers.len();
444 if slot_count == 0 {
445 self.focused_slot = 0;
446 } else {
447 self.focused_slot = self.focused_slot.min(slot_count - 1);
448 }
449 } else {
450 self.focused_slot = 0;
451 }
452
453 if let Some(update) = self.latest_update.as_ref() {
454 let available = self.available_chart_components(update);
455 if available.is_empty() {
456 self.chart_component = None;
457 } else if !self
458 .chart_component
459 .is_some_and(|component| available.contains(&component))
460 {
461 self.chart_component = update
462 .training_component()
463 .filter(|component| available.contains(component))
464 .or_else(|| available.first().copied());
465 }
466 }
467
468 let row_count = self.discrete_rows().len();
469 if row_count == 0 {
470 self.selected_discrete_row = 0;
471 } else {
472 self.selected_discrete_row = self.selected_discrete_row.min(row_count - 1);
473 }
474 }
475
476 fn push_history_point(&mut self, update: &StatusUpdate) {
477 let signed_real_slot_values =
478 self.collect_history_slot_values(update, ComponentKind::Real, IntegralView::Signed);
479 let signed_imag_slot_values =
480 self.collect_history_slot_values(update, ComponentKind::Imag, IntegralView::Signed);
481 let absolute_real_slot_values =
482 self.collect_history_slot_values(update, ComponentKind::Real, IntegralView::Absolute);
483 let absolute_imag_slot_values =
484 self.collect_history_slot_values(update, ComponentKind::Imag, IntegralView::Absolute);
485 if signed_real_slot_values.iter().all(Option::is_none)
486 && signed_imag_slot_values.iter().all(Option::is_none)
487 && absolute_real_slot_values.iter().all(Option::is_none)
488 && absolute_imag_slot_values.iter().all(Option::is_none)
489 {
490 return;
491 }
492
493 let point = HistoryPoint {
494 iteration: update.meta.iteration,
495 samples: update.meta.total_points,
496 signed_real_slot_values,
497 signed_imag_slot_values,
498 absolute_real_slot_values,
499 absolute_imag_slot_values,
500 completed_iteration: !matches!(update.kind(), super::IntegrationStatusKind::Live),
501 };
502
503 if self
504 .history
505 .last()
506 .is_some_and(|previous| previous.samples == point.samples)
507 {
508 let preserve_completed_point = self.history.last().is_some_and(|previous| {
509 previous.completed_iteration
510 && !point.completed_iteration
511 && update.is_initial_live_status()
512 });
513 if !preserve_completed_point {
514 let _ = self.history.pop();
515 }
516 }
517 if self
518 .history
519 .last()
520 .is_some_and(|previous| previous.samples > point.samples)
521 {
522 self.history.clear();
523 }
524 self.history.push(point);
525 while self.history.len() > MAX_HISTORY_POINTS {
526 self.compact_history_preserving_span();
527 }
528 }
529
530 fn compact_history_preserving_span(&mut self) {
531 if self.history.len() <= MAX_HISTORY_POINTS {
532 return;
533 }
534
535 let retain_recent = MAX_HISTORY_POINTS / 2;
536 let split_index = self.history.len().saturating_sub(retain_recent);
537 if split_index == 0 {
538 return;
539 }
540
541 let older = &self.history[..split_index];
542 let recent = &self.history[split_index..];
543 let max_older_points = MAX_HISTORY_POINTS.saturating_sub(recent.len()).max(2);
544 let step = older.len().div_ceil(max_older_points);
545
546 let mut compacted = Vec::with_capacity(MAX_HISTORY_POINTS);
547 for (index, point) in older.iter().enumerate() {
548 if index == 0 || index + 1 == older.len() || index % step == 0 {
549 compacted.push(point.clone());
550 }
551 }
552 compacted.extend(recent.iter().cloned());
553
554 if compacted.len() > MAX_HISTORY_POINTS {
555 let keep_from_older = compacted.len().saturating_sub(recent.len());
556 let extra = compacted.len() - MAX_HISTORY_POINTS;
557 if keep_from_older > extra + 1 {
558 compacted.drain(1..=extra);
559 } else {
560 compacted.truncate(MAX_HISTORY_POINTS);
561 }
562 }
563
564 self.history = compacted;
565 }
566
567 fn collect_history_slot_values(
568 &self,
569 update: &StatusUpdate,
570 component: ComponentKind,
571 view: IntegralView,
572 ) -> Vec<Option<(f64, f64)>> {
573 let Some(row) = update
574 .main_results
575 .find_row(ContributionKind::All, component)
576 else {
577 return vec![None; update.main_results.slot_headers.len()];
578 };
579 (0..update.main_results.slot_headers.len())
580 .map(|slot_index| {
581 row.slot_cell(slot_index)
582 .and_then(|cell| cell.statistics(view).value.as_ref())
583 .map(|value| (value.raw.0.0, value.raw.1.0.abs()))
584 })
585 .collect()
586 }
587
588 fn visible_history(&self) -> Vec<&HistoryPoint> {
589 match self.chart_history_window {
590 ChartHistoryWindow::Full => self.history.iter().collect(),
591 ChartHistoryWindow::RecentIterations(iterations) => {
592 if iterations == 0 || self.history.is_empty() {
593 return self.history.iter().collect();
594 }
595 let latest_iteration = self
596 .history
597 .last()
598 .map(|point| point.iteration)
599 .unwrap_or_default();
600 let start_iteration = latest_iteration
601 .saturating_add(1)
602 .saturating_sub(iterations);
603 self.history
604 .iter()
605 .filter(|point| point.iteration >= start_iteration)
606 .collect()
607 }
608 }
609 }
610
611 #[cfg(test)]
612 pub(crate) fn visible_history_sample_bounds(&self) -> Option<(usize, usize)> {
613 let visible = self.visible_history();
614 Some((visible.first()?.samples, visible.last()?.samples))
615 }
616
617 fn available_chart_components(&self, update: &StatusUpdate) -> Vec<ComponentKind> {
618 [ComponentKind::Real, ComponentKind::Imag]
619 .into_iter()
620 .filter(|component| {
621 update
622 .main_results
623 .find_row(ContributionKind::All, *component)
624 .and_then(|row| row.slot_cell(self.focused_slot))
625 .and_then(|cell| cell.statistics(self.integral_view).value.as_ref())
626 .is_some()
627 })
628 .collect()
629 }
630
631 fn chart_component(&self, update: &StatusUpdate) -> Option<ComponentKind> {
632 let available = self.available_chart_components(update);
633 if available.is_empty() {
634 return None;
635 }
636
637 self.chart_component
638 .filter(|component| available.contains(component))
639 .or_else(|| {
640 update
641 .training_component_for_slot(self.focused_slot)
642 .filter(|component| available.contains(component))
643 })
644 .or_else(|| available.first().copied())
645 }
646
647 fn draw_dashboard(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
648 let vertical = Layout::default()
649 .direction(Direction::Vertical)
650 .constraints([
651 Constraint::Length(3),
652 Constraint::Length(6),
653 Constraint::Length(update.main_results.slot_headers.len().max(1) as u16 + 2),
654 Constraint::Min(12),
655 Constraint::Length(2),
656 ])
657 .split(area);
658
659 self.draw_tabs(frame, vertical[0]);
660 self.draw_progress(frame, vertical[1], update);
661 self.draw_slot_ribbon(frame, vertical[2], update);
662
663 match self.active_tab {
664 DashboardTab::Overview => self.draw_overview_tab(frame, vertical[3], update),
665 DashboardTab::Discrete => self.draw_discrete_tab(frame, vertical[3], update),
666 DashboardTab::MaxWeight => self.draw_max_weight_tab(frame, vertical[3], update),
667 }
668
669 self.draw_footer(frame, vertical[4], update);
670 }
671
672 fn draw_tabs(&self, frame: &mut Frame<'_>, area: Rect) {
673 let titles = DashboardTab::all()
674 .into_iter()
675 .map(|tab| {
676 Line::from(vec![Span::styled(
677 format!(" {} ", tab.title()),
678 Style::default().add_modifier(Modifier::BOLD),
679 )])
680 })
681 .collect::<Vec<_>>();
682
683 let tabs = Tabs::new(titles)
684 .block(titled_block(format!(
685 "GammaLoop status · {}",
686 integral_view_label(self.integral_view)
687 )))
688 .select(self.active_tab.index())
689 .highlight_style(Style::default().add_modifier(Modifier::REVERSED | Modifier::BOLD));
690 frame.render_widget(tabs, area);
691 }
692
693 fn draw_progress(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
694 let block = titled_block("Iteration progress");
695 let inner = Layout::default()
696 .direction(Direction::Vertical)
697 .constraints([
698 Constraint::Length(1),
699 Constraint::Length(1),
700 Constraint::Length(1),
701 Constraint::Length(1),
702 ])
703 .split(block.inner(area));
704 frame.render_widget(block, area);
705
706 frame.render_widget(Paragraph::new(""), inner[0]);
707
708 let mut left = StyledText::new();
709 left.push_text(" ", TextStyle::PLAIN);
710 left.push_text(
711 format!(
712 "[ {:^7} ]",
713 utils::format_wdhms(update.meta.elapsed_time.as_secs() as usize)
714 ),
715 TextStyle::PLAIN.bold(),
716 );
717 left.push_text(" | ", TextStyle::PLAIN);
718 left.push_text("# samples total ", TextStyle::green().bold());
719 left.push_text(
720 super::display::format_total_points(update.meta.total_points),
721 TextStyle::PLAIN.bold(),
722 );
723 left.push_text(" | ", TextStyle::PLAIN);
724 left.push_text("Iteration ", TextStyle::green().bold());
725 left.push_text(
726 format!("#{}", update.meta.iteration),
727 TextStyle::PLAIN.bold(),
728 );
729 left.push_text(
730 format!(
731 " ({})",
732 if matches!(update.kind(), super::IntegrationStatusKind::Live) {
733 "running"
734 } else {
735 "completed"
736 }
737 ),
738 TextStyle::green().bold(),
739 );
740 if matches!(update.kind(), super::IntegrationStatusKind::Live) {
741 left.push_text(" | ", TextStyle::PLAIN);
742 left.push_text("ETA ", TextStyle::green().bold());
743 left.push_text(
744 update
745 .meta
746 .iteration_eta()
747 .map(|duration| utils::format_wdhms(duration.as_secs() as usize))
748 .unwrap_or_else(|| "warming up".to_string()),
749 TextStyle::PLAIN,
750 );
751 if update.meta.show_eta_to_target {
752 left.push_text(" | ", TextStyle::PLAIN);
753 left.push_text("ETA to ⟨I⟩ target ", TextStyle::green().bold());
754 if let Some(specification) = update.meta.eta_to_target_specification() {
755 left.push_text("(", TextStyle::green().bold());
756 left.push_text(specification, TextStyle::green().bold());
757 left.push_text(") ", TextStyle::green().bold());
758 }
759 left.push_text(
760 update
761 .meta
762 .eta_to_target()
763 .map(|duration| {
764 if duration == Duration::MAX {
765 "∞".to_string()
766 } else {
767 utils::format_wdhms(duration.as_secs() as usize)
768 }
769 })
770 .unwrap_or_else(|| "N/A".to_string()),
771 TextStyle::PLAIN,
772 );
773 }
774 }
775
776 let mut right = StyledText::new();
777 right.push_text(
778 update
779 .meta
780 .total_sample_rate_per_second()
781 .map(format_samples_per_second)
782 .unwrap_or_else(|| "N/A".to_string()),
783 TextStyle::PLAIN.bold(),
784 );
785 right.push_text(" ", TextStyle::PLAIN);
786 right.push_text("#samples/s", TextStyle::green().bold());
787 right.push_text(" | ", TextStyle::PLAIN);
788 if let Some(sample_core_time) = update.meta.sample_core_time() {
789 right.push_text(
790 format!("{} /sample/core", sample_core_time.replace(' ', "")),
791 TextStyle::PLAIN.bold(),
792 );
793 } else {
794 right.push_text("N/A /sample/core", TextStyle::red());
795 }
796 right.push_text(" ", TextStyle::PLAIN);
797 right.push_text(
798 format!("({} cores)", update.meta.cores),
799 TextStyle::PLAIN.bold(),
800 );
801 right.push_text(" ", TextStyle::PLAIN);
802
803 let header = Layout::default()
804 .direction(Direction::Horizontal)
805 .constraints([Constraint::Percentage(68), Constraint::Percentage(32)])
806 .split(inner[1]);
807 frame.render_widget(Paragraph::new(text_from_styled_text(&left)), header[0]);
808 frame.render_widget(
809 Paragraph::new(text_from_styled_text(&right)).alignment(Alignment::Right),
810 header[1],
811 );
812
813 let progress = update.meta.iteration_progress_ratio().unwrap_or_else(|| {
814 if matches!(update.kind(), super::IntegrationStatusKind::Live) {
815 0.0
816 } else {
817 1.0
818 }
819 });
820 let gauge = Gauge::default()
821 .gauge_style(
822 Style::default()
823 .fg(Color::Green)
824 .bg(Color::Rgb(70, 70, 70))
825 .add_modifier(Modifier::BOLD),
826 )
827 .ratio(progress.clamp(0.0, 1.0))
828 .label(Span::styled(
829 format!(
830 "{} / {} ({:.1}%)",
831 abbreviate_count(
832 update
833 .meta
834 .live_progress
835 .map(|progress| progress.completed_points)
836 .unwrap_or(update.meta.current_iteration_points),
837 ),
838 abbreviate_count(
839 update
840 .meta
841 .live_progress
842 .map(|progress| progress.target_points)
843 .unwrap_or(update.meta.current_iteration_points),
844 ),
845 progress * 100.0
846 ),
847 Style::default()
848 .fg(Color::White)
849 .add_modifier(Modifier::BOLD),
850 ));
851 frame.render_widget(gauge, inner[2]);
852 frame.render_widget(Paragraph::new(""), inner[3]);
853 }
854
855 fn draw_slot_ribbon(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
856 let slot_name_width = update
857 .main_results
858 .slot_headers
859 .iter()
860 .map(|header| header.to_plain_string().chars().count())
861 .max()
862 .unwrap_or(10)
863 .max(10) as u16;
864 let value_width = (0..update.main_results.slot_headers.len())
865 .flat_map(|slot_index| {
866 [ComponentKind::Real, ComponentKind::Imag]
867 .into_iter()
868 .filter_map(move |component| {
869 update
870 .main_results
871 .find_row(ContributionKind::All, component)
872 .and_then(|row| row.slot_cell(slot_index))
873 .and_then(|cell| cell.statistics(self.integral_view).value.as_ref())
874 .map(|value| value.display.to_plain_string().chars().count())
875 })
876 })
877 .max()
878 .unwrap_or(16)
879 .max(16) as u16;
880 let has_targets = self.integral_view == IntegralView::Signed
881 && (0..update.main_results.slot_headers.len()).any(|slot_index| {
882 update
883 .target_display_for_slot_component(slot_index, ComponentKind::Real)
884 .is_some()
885 || update
886 .target_display_for_slot_component(slot_index, ComponentKind::Imag)
887 .is_some()
888 });
889 let target_value_width = (0..update.main_results.slot_headers.len())
890 .flat_map(|slot_index| {
891 [ComponentKind::Real, ComponentKind::Imag]
892 .into_iter()
893 .filter_map(move |component| {
894 update
895 .target_display_for_slot_component(slot_index, component)
896 .map(|value| value.to_plain_string().chars().count())
897 })
898 })
899 .max()
900 .unwrap_or(16)
901 .max(16) as u16;
902 let rows = (0..update.main_results.slot_headers.len())
903 .map(|slot_index| {
904 let re_value = update
905 .main_results
906 .find_row(ContributionKind::All, ComponentKind::Real)
907 .and_then(|row| row.slot_cell(slot_index))
908 .and_then(|cell| cell.statistics(self.integral_view).value.as_ref())
909 .map(|value| &value.display);
910 let im_value = update
911 .main_results
912 .find_row(ContributionKind::All, ComponentKind::Imag)
913 .and_then(|row| row.slot_cell(slot_index))
914 .and_then(|cell| cell.statistics(self.integral_view).value.as_ref())
915 .map(|value| &value.display);
916 let target_re =
917 update.target_display_for_slot_component(slot_index, ComponentKind::Real);
918 let target_im =
919 update.target_display_for_slot_component(slot_index, ComponentKind::Imag);
920 let mut cells = vec![
921 Cell::from(Span::styled(
922 if slot_index == self.focused_slot {
923 ">"
924 } else {
925 " "
926 },
927 if slot_index == self.focused_slot {
928 Style::default()
929 .fg(Color::White)
930 .add_modifier(Modifier::BOLD)
931 } else {
932 Style::default()
933 },
934 )),
935 cell_from_styled_text(&update.main_results.slot_headers[slot_index]),
936 cell_from_styled_text(&component_label_with_colon(
937 ComponentKind::Real,
938 self.integral_view,
939 )),
940 cell_from_optional_styled_text(re_value),
941 cell_from_styled_text(&component_label_with_colon(
942 ComponentKind::Imag,
943 self.integral_view,
944 )),
945 cell_from_optional_styled_text(im_value),
946 ];
947 if has_targets {
948 cells.push(cell_from_styled_text(&StyledText::plain("|")));
949 cells.push(plain_header_cell("trgt"));
950 cells.push(cell_from_styled_text(&component_label_with_colon(
951 ComponentKind::Real,
952 IntegralView::Signed,
953 )));
954 cells.push(cell_from_optional_styled_text(target_re.as_ref()));
955 cells.push(cell_from_styled_text(&component_label_with_colon(
956 ComponentKind::Imag,
957 IntegralView::Signed,
958 )));
959 cells.push(cell_from_optional_styled_text(target_im.as_ref()));
960 }
961 Row::new(cells)
962 })
963 .collect::<Vec<_>>();
964
965 let mut widths = vec![
966 Constraint::Length(1),
967 Constraint::Length(slot_name_width),
968 Constraint::Length(5),
969 Constraint::Length(value_width),
970 Constraint::Length(5),
971 Constraint::Length(value_width),
972 ];
973 if has_targets {
974 widths.extend([
975 Constraint::Length(1),
976 Constraint::Length(5),
977 Constraint::Length(3),
978 Constraint::Length(target_value_width),
979 Constraint::Length(3),
980 Constraint::Min(target_value_width),
981 ]);
982 }
983
984 frame.render_widget(
985 Table::new(rows, widths)
986 .block(titled_block(format!(
987 "Integrands · {}",
988 integral_view_label(self.integral_view)
989 )))
990 .column_spacing(1),
991 area,
992 );
993 }
994
995 fn draw_overview_tab(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
996 let vertical = if area.width >= 120 {
997 Layout::default()
998 .direction(Direction::Vertical)
999 .constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
1000 .split(area)
1001 } else {
1002 Layout::default()
1003 .direction(Direction::Vertical)
1004 .constraints([
1005 Constraint::Percentage(48),
1006 Constraint::Length(11),
1007 Constraint::Min(10),
1008 ])
1009 .split(area)
1010 };
1011
1012 if area.width >= 120 {
1013 let top = Layout::default()
1014 .direction(Direction::Horizontal)
1015 .constraints([Constraint::Percentage(62), Constraint::Percentage(38)])
1016 .split(vertical[0]);
1017 self.draw_chart(frame, top[0], update);
1018 self.draw_focused_slot_detail(frame, top[1], update);
1019
1020 let bottom = Layout::default()
1021 .direction(Direction::Horizontal)
1022 .constraints([Constraint::Percentage(56), Constraint::Percentage(44)])
1023 .split(vertical[1]);
1024 self.draw_results_summary_table(frame, bottom[0], update);
1025 self.draw_statistics_panel(frame, bottom[1], update);
1026 } else {
1027 self.draw_chart(frame, vertical[0], update);
1028 self.draw_focused_slot_detail(frame, vertical[1], update);
1029 let bottom = Layout::default()
1030 .direction(Direction::Horizontal)
1031 .constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
1032 .split(vertical[2]);
1033 self.draw_results_summary_table(frame, bottom[0], update);
1034 self.draw_statistics_panel(frame, bottom[1], update);
1035 }
1036 }
1037
1038 fn draw_chart(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
1039 let Some(component) = self.chart_component(update) else {
1040 frame.render_widget(
1041 Paragraph::new("Training phase is not a single component.")
1042 .block(titled_block("Convergence")),
1043 area,
1044 );
1045 return;
1046 };
1047
1048 let focused_slot = self
1049 .focused_slot
1050 .min(update.main_results.slot_headers.len().saturating_sub(1));
1051 let visible_history = self.visible_history();
1052 if visible_history.is_empty() {
1053 frame.render_widget(
1054 Paragraph::new("Waiting for history points...").block(titled_block("Convergence")),
1055 area,
1056 );
1057 return;
1058 }
1059
1060 let central_points = visible_history
1061 .iter()
1062 .filter_map(|point| {
1063 (*point)
1064 .slot_values(component, self.integral_view)
1065 .get(focused_slot)
1066 .copied()
1067 .flatten()
1068 .map(|(central, _)| (point.samples as f64, central))
1069 })
1070 .collect::<Vec<_>>();
1071 let upper_points = visible_history
1072 .iter()
1073 .filter_map(|point| {
1074 (*point)
1075 .slot_values(component, self.integral_view)
1076 .get(focused_slot)
1077 .copied()
1078 .flatten()
1079 .map(|(central, error)| (point.samples as f64, central + error))
1080 })
1081 .collect::<Vec<_>>();
1082 let lower_points = visible_history
1083 .iter()
1084 .filter_map(|point| {
1085 (*point)
1086 .slot_values(component, self.integral_view)
1087 .get(focused_slot)
1088 .copied()
1089 .flatten()
1090 .map(|(central, error)| (point.samples as f64, central - error))
1091 })
1092 .collect::<Vec<_>>();
1093 let completed_points = visible_history
1094 .iter()
1095 .filter(|point| point.completed_iteration)
1096 .filter_map(|point| {
1097 (*point)
1098 .slot_values(component, self.integral_view)
1099 .get(focused_slot)
1100 .copied()
1101 .flatten()
1102 .map(|(central, _)| (point.samples as f64, central))
1103 })
1104 .collect::<Vec<_>>();
1105 if central_points.is_empty() {
1106 frame.render_widget(
1107 Paragraph::new("Waiting for focused-integrand history...").block(titled_block(
1108 format!(
1109 "Convergence {} ({})",
1110 integral_view_label(self.integral_view),
1111 component.tag_for_view(self.integral_view)
1112 ),
1113 )),
1114 area,
1115 );
1116 return;
1117 }
1118
1119 let x_min = visible_history
1120 .first()
1121 .map(|point| point.samples as f64)
1122 .unwrap_or(0.0);
1123 let x_max = visible_history
1124 .last()
1125 .map(|point| point.samples as f64)
1126 .unwrap_or(1.0);
1127 let current_value = update
1128 .main_results
1129 .find_row(ContributionKind::All, component)
1130 .and_then(|row| row.slot_cell(focused_slot))
1131 .and_then(|cell| cell.statistics(self.integral_view).value.as_ref())
1132 .map(|value| (value.raw.0.0, value.raw.1.0.abs()))
1133 .or_else(|| {
1134 self.history.last().and_then(|point| {
1135 point
1136 .slot_values(component, self.integral_view)
1137 .get(focused_slot)
1138 .copied()
1139 .flatten()
1140 })
1141 })
1142 .unwrap_or((0.0, 1.0));
1143 if !current_value.0.is_finite() || !current_value.1.is_finite() || current_value.1 <= 0.0 {
1144 frame.render_widget(
1145 Paragraph::new("Waiting for a finite central value and non-zero MC uncertainty...")
1146 .block(titled_block("Convergence")),
1147 area,
1148 );
1149 return;
1150 }
1151 let sigma = current_value.1;
1152 let y_span = self.chart_y_sigma_span as f64;
1153 let y_min = current_value.0 - y_span * sigma;
1154 let y_max = current_value.0 + y_span * sigma;
1155
1156 let mut datasets = vec![
1157 Dataset::default()
1158 .name("central")
1159 .marker(Marker::Braille)
1160 .graph_type(ratatui::widgets::GraphType::Line)
1161 .style(
1162 Style::default()
1163 .fg(Color::Green)
1164 .add_modifier(Modifier::BOLD),
1165 )
1166 .data(¢ral_points),
1167 Dataset::default()
1168 .name("upper")
1169 .graph_type(ratatui::widgets::GraphType::Line)
1170 .style(Style::default().fg(Color::Blue))
1171 .data(&upper_points),
1172 Dataset::default()
1173 .name("lower")
1174 .graph_type(ratatui::widgets::GraphType::Line)
1175 .style(Style::default().fg(Color::Blue))
1176 .data(&lower_points),
1177 Dataset::default()
1178 .name("iter")
1179 .marker(Marker::Bar)
1180 .graph_type(ratatui::widgets::GraphType::Scatter)
1181 .style(Style::default().fg(Color::LightMagenta))
1182 .data(&completed_points),
1183 ];
1184
1185 let target_points = (self.integral_view == IntegralView::Signed)
1186 .then(|| update.target_for_slot_component(focused_slot, component))
1187 .flatten()
1188 .map(|target| vec![(x_min, target.0), (x_max.max(x_min + 1.0), target.0)]);
1189 if let Some(target_points) = target_points.as_ref() {
1190 datasets.push(
1191 Dataset::default()
1192 .name("target")
1193 .graph_type(ratatui::widgets::GraphType::Line)
1194 .style(Style::default().fg(Color::Yellow))
1195 .data(target_points),
1196 );
1197 }
1198
1199 let selected_for_training =
1200 update.slot_component_selected_for_training(focused_slot, component);
1201 let mut title = StyledText::styled(
1202 format!("Convergence {} : ", integral_view_label(self.integral_view)),
1203 TextStyle::green().bold(),
1204 );
1205 title.append(component.label_display_for_view(self.integral_view));
1206 title.push_text(
1207 if selected_for_training {
1208 " (selected for training)"
1209 } else {
1210 " (not selected for training)"
1211 },
1212 TextStyle::green().bold(),
1213 );
1214 title.push_text(" [", TextStyle::green().bold());
1215 title.append(update.main_results.slot_headers[focused_slot].clone());
1216 title.push_text(
1217 format!(
1218 " | {} | y ±{}σ]",
1219 self.chart_history_window.description(),
1220 self.chart_y_sigma_span
1221 ),
1222 TextStyle::green().bold(),
1223 );
1224 let x_bounds = [x_min, x_max.max(x_min + 1.0)];
1225 let chart = Chart::new(datasets)
1226 .block(titled_block_styled(&title))
1227 .x_axis(
1228 Axis::default()
1229 .title("samples")
1230 .bounds(x_bounds)
1231 .labels(count_axis_labels(x_bounds[0], x_bounds[1])),
1232 )
1233 .y_axis(
1234 Axis::default()
1235 .title("central value")
1236 .bounds([y_min, y_max])
1237 .labels(value_axis_labels(y_min, y_max)),
1238 );
1239 frame.render_widget(chart, area);
1240 }
1241
1242 fn draw_focused_slot_detail(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
1243 let focused_slot = self
1244 .focused_slot
1245 .min(update.main_results.slot_headers.len().saturating_sub(1));
1246 let rows = self.overview_summary_rows(update);
1247 let show_target_columns = self.integral_view == IntegralView::Signed
1248 && rows.iter().any(|row| {
1249 update
1250 .target_deltas_for_row_slot(row, focused_slot, self.integral_view)
1251 .0
1252 .is_some()
1253 });
1254 let focused_column_count = 5 + if show_target_columns { 2 } else { 0 };
1255
1256 let mut table_rows = vec![blank_table_row(focused_column_count)];
1257 let mut inserted_sum_gap = false;
1258 for row in rows {
1259 if matches!(row.contribution.raw, ContributionKind::Sum) && !inserted_sum_gap {
1260 table_rows.push(blank_table_row(focused_column_count));
1261 inserted_sum_gap = true;
1262 }
1263 let Some(cell) = row.slot_cell(focused_slot) else {
1264 continue;
1265 };
1266 let (delta_sigma, delta_percent) =
1267 update.target_deltas_for_row_slot(row, focused_slot, self.integral_view);
1268 let statistics = cell.statistics(self.integral_view);
1269 let mut label = row.contribution.display.clone();
1270 label.push_text(" ", TextStyle::PLAIN);
1271 label.append(row.component.raw.label_display_for_view(self.integral_view));
1272 let mut cells = vec![
1273 cell_from_styled_text(&label),
1274 cell_from_optional_styled_text(
1275 statistics.value.as_ref().map(|value| &value.display),
1276 ),
1277 cell_from_optional_styled_text(
1278 statistics
1279 .relative_error
1280 .as_ref()
1281 .map(|value| &value.display),
1282 ),
1283 cell_from_optional_styled_text(
1284 statistics.chi_sq.as_ref().map(|value| &value.display),
1285 ),
1286 cell_from_optional_styled_text(
1287 statistics
1288 .max_weight_impact
1289 .as_ref()
1290 .map(|value| &value.display),
1291 ),
1292 ];
1293 if show_target_columns {
1294 cells.push(cell_from_optional_styled_text(
1295 delta_sigma.as_ref().map(|value| &value.display),
1296 ));
1297 cells.push(cell_from_optional_styled_text(
1298 delta_percent.as_ref().map(|value| &value.display),
1299 ));
1300 }
1301 table_rows.push(Row::new(cells));
1302 }
1303
1304 let mut headers = vec![
1305 plain_header_cell(""),
1306 plain_header_cell("integral"),
1307 plain_header_cell("% err"),
1308 plain_header_cell("chi^2"),
1309 plain_header_cell("m.w.i"),
1310 ];
1311 if show_target_columns {
1312 headers.push(plain_header_cell("Δ [σ]"));
1313 headers.push(plain_header_cell("Δ [%]"));
1314 }
1315
1316 let mut widths = vec![
1317 Constraint::Length(9),
1318 Constraint::Length(16),
1319 Constraint::Length(8),
1320 Constraint::Length(8),
1321 Constraint::Length(10),
1322 ];
1323 if show_target_columns {
1324 widths.push(Constraint::Length(8));
1325 widths.push(Constraint::Length(8));
1326 }
1327
1328 let table = Table::new(table_rows, widths)
1329 .header(Row::new(headers))
1330 .block(titled_block_styled(&scoped_integrand_title(
1331 &format!(
1332 "Focused integrand {} ",
1333 integral_view_label(self.integral_view)
1334 ),
1335 &update.main_results.slot_headers[focused_slot],
1336 "",
1337 )))
1338 .column_spacing(1);
1339 frame.render_widget(table, area);
1340 }
1341
1342 fn draw_results_summary_table(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
1343 let rows = self.overview_summary_rows(update);
1344 if rows.is_empty() {
1345 frame.render_widget(
1346 Paragraph::new("No overview summary rows are available.")
1347 .block(titled_block("Results summary")),
1348 area,
1349 );
1350 return;
1351 }
1352
1353 let contribution_width = update
1354 .main_results
1355 .row_groups
1356 .iter()
1357 .flat_map(|group| group.rows.iter())
1358 .map(|row| row.contribution.display.to_plain_string().chars().count())
1359 .chain(std::iter::once(
1360 update
1361 .main_results
1362 .contribution_header
1363 .to_plain_string()
1364 .chars()
1365 .count(),
1366 ))
1367 .max()
1368 .unwrap_or(18)
1369 .max(18) as u16;
1370 let contribution_title = styled_text_line(&update.main_results.contribution_header, 0);
1371 let contribution_detail = styled_text_line(&update.main_results.contribution_header, 1);
1372 let mut header_top = vec![cell_from_styled_text(&contribution_title), blank_cell()];
1373 let mut header_bottom = vec![cell_from_styled_text(&contribution_detail), blank_cell()];
1374 let mut widths = vec![
1375 Constraint::Length(contribution_width),
1376 Constraint::Length(5),
1377 ];
1378 for (slot_index, slot_header) in update.main_results.slot_headers.iter().enumerate() {
1379 let integral_width = rows
1380 .iter()
1381 .filter_map(|row| {
1382 row.slot_cell(slot_index)
1383 .and_then(|cell| cell.statistics(self.integral_view).value.as_ref())
1384 .map(|value| value.display.to_plain_string().chars().count())
1385 })
1386 .chain(std::iter::once(
1387 slot_header.to_plain_string().chars().count(),
1388 ))
1389 .chain(std::iter::once("integral".chars().count()))
1390 .max()
1391 .unwrap_or(16)
1392 .max(16) as u16;
1393 header_top.push(cell_from_styled_text(slot_header));
1394 header_bottom.push(plain_header_cell("integral"));
1395 widths.push(Constraint::Length(integral_width));
1396 if self.metric_visibility.relative_error {
1397 header_top.push(blank_cell());
1398 header_bottom.push(plain_header_cell("% err"));
1399 widths.push(Constraint::Length(8));
1400 }
1401 if self.metric_visibility.chi_sq {
1402 header_top.push(blank_cell());
1403 header_bottom.push(plain_header_cell("chi^2"));
1404 widths.push(Constraint::Length(8));
1405 }
1406 if self.metric_visibility.max_weight_impact {
1407 header_top.push(blank_cell());
1408 header_bottom.push(plain_header_cell("m.w.i"));
1409 widths.push(Constraint::Length(10));
1410 }
1411 }
1412
1413 let mut table_rows = vec![
1414 blank_table_row(widths.len()),
1415 Row::new(header_top),
1416 Row::new(header_bottom),
1417 blank_table_row(widths.len()),
1418 ];
1419 for group in [MainResultsRowGroupKind::All, MainResultsRowGroupKind::Sum] {
1420 let group_rows = update
1421 .main_results
1422 .row_groups_of_kind(group)
1423 .flat_map(|row_group| row_group.rows.iter())
1424 .collect::<Vec<_>>();
1425 if group_rows.is_empty() {
1426 continue;
1427 }
1428 if table_rows.len() > 1 {
1429 table_rows.push(blank_table_row(widths.len()));
1430 }
1431 for row in group_rows {
1432 let mut cells = vec![
1433 cell_from_styled_text(&row.contribution.display),
1434 cell_from_styled_text(
1435 &row.component.raw.label_display_for_view(self.integral_view),
1436 ),
1437 ];
1438 for slot_index in 0..update.main_results.slot_headers.len() {
1439 let cell = row.slot_cell(slot_index);
1440 cells.push(cell_from_optional_styled_text(
1441 cell.and_then(|cell| cell.statistics(self.integral_view).value.as_ref())
1442 .map(|value| &value.display),
1443 ));
1444 if self.metric_visibility.relative_error {
1445 cells.push(cell_from_optional_styled_text(
1446 cell.and_then(|cell| {
1447 cell.statistics(self.integral_view).relative_error.as_ref()
1448 })
1449 .map(|value| &value.display),
1450 ));
1451 }
1452 if self.metric_visibility.chi_sq {
1453 cells.push(cell_from_optional_styled_text(
1454 cell.and_then(|cell| {
1455 cell.statistics(self.integral_view).chi_sq.as_ref()
1456 })
1457 .map(|value| &value.display),
1458 ));
1459 }
1460 if self.metric_visibility.max_weight_impact {
1461 cells.push(cell_from_optional_styled_text(
1462 cell.and_then(|cell| {
1463 cell.statistics(self.integral_view)
1464 .max_weight_impact
1465 .as_ref()
1466 })
1467 .map(|value| &value.display),
1468 ));
1469 }
1470 }
1471 table_rows.push(Row::new(cells));
1472 }
1473 }
1474
1475 let table = Table::new(table_rows, widths)
1476 .block(titled_block(format!(
1477 "Results summary · {}",
1478 integral_view_label(self.integral_view)
1479 )))
1480 .column_spacing(1);
1481 frame.render_widget(table, area);
1482 }
1483
1484 fn draw_statistics_panel(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
1485 let Some(statistics) = update.statistics.as_ref() else {
1486 frame.render_widget(
1487 Paragraph::new("Statistics unavailable.")
1488 .block(titled_block("Integration statistics")),
1489 area,
1490 );
1491 return;
1492 };
1493 let statistics_scope = self.selected_statistics_scope(update);
1494 let layout = Layout::default()
1495 .direction(Direction::Vertical)
1496 .constraints([
1497 Constraint::Percentage(38),
1498 Constraint::Percentage(31),
1499 Constraint::Percentage(31),
1500 ])
1501 .split(area);
1502
1503 let mut summary_rows = vec![blank_table_row(9)];
1504 summary_rows.extend(
1505 statistics
1506 .table_rows(statistics_scope)
1507 .into_iter()
1508 .map(|row| {
1509 let mut cells = vec![cell_from_styled_text(&row.row_label)];
1510 for entry in row.entries {
1511 cells.push(cell_from_styled_text(&entry.label));
1512 cells.push(cell_from_styled_text(&entry.value));
1513 }
1514 Row::new(cells)
1515 })
1516 .collect::<Vec<_>>(),
1517 );
1518 summary_rows.push(blank_table_row(9));
1519 let title = statistics.statistics_title(statistics_scope);
1520 let summary_table = Table::new(
1521 summary_rows,
1522 [
1523 Constraint::Length(10),
1524 Constraint::Length(12),
1525 Constraint::Length(12),
1526 Constraint::Length(12),
1527 Constraint::Length(12),
1528 Constraint::Length(12),
1529 Constraint::Length(12),
1530 Constraint::Length(13),
1531 Constraint::Min(12),
1532 ],
1533 )
1534 .block(titled_block_styled(&title))
1535 .column_spacing(1);
1536 frame.render_widget(summary_table, layout[0]);
1537
1538 self.draw_mix_panel(
1539 frame,
1540 layout[1],
1541 &statistics.timing_title(statistics_scope),
1542 &statistics.timing_mix_segments(statistics_scope),
1543 None,
1544 );
1545 self.draw_mix_panel(
1546 frame,
1547 layout[2],
1548 &statistics.precision_title(statistics_scope),
1549 &statistics.precision_mix_segments(statistics_scope),
1550 Some(&statistics.numerical_stability_median_entries(statistics_scope)),
1551 );
1552 }
1553
1554 fn draw_discrete_tab(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
1555 let discrete_rows = self.discrete_rows();
1556 if discrete_rows.is_empty() {
1557 frame.render_widget(
1558 Paragraph::new("No monitored discrete breakdown is available for this run.")
1559 .block(titled_block("Discrete breakdown"))
1560 .wrap(Wrap { trim: false }),
1561 area,
1562 );
1563 return;
1564 }
1565
1566 let focused_slot = self
1567 .focused_slot
1568 .min(update.main_results.slot_headers.len().saturating_sub(1));
1569 let horizontal = Layout::default()
1570 .direction(Direction::Horizontal)
1571 .constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
1572 .split(area);
1573
1574 let contribution_header = update.main_results.contribution_header.to_plain_string();
1575 let contribution_width = discrete_rows
1576 .iter()
1577 .map(|entry| {
1578 entry
1579 .row
1580 .contribution
1581 .display
1582 .to_plain_string()
1583 .chars()
1584 .count()
1585 })
1586 .chain(std::iter::once(contribution_header.chars().count()))
1587 .max()
1588 .unwrap_or(24)
1589 .max(24) as u16;
1590 let integral_width = discrete_rows
1591 .iter()
1592 .filter_map(|entry| {
1593 entry
1594 .row
1595 .slot_cell(focused_slot)
1596 .and_then(|cell| cell.statistics(self.integral_view).value.as_ref())
1597 .map(|value| value.display.to_plain_string().chars().count())
1598 })
1599 .chain(std::iter::once("integral".chars().count()))
1600 .max()
1601 .unwrap_or(14)
1602 .max(14) as u16;
1603 let mut rows = vec![blank_table_row(9)];
1604 rows.extend(
1605 discrete_rows
1606 .iter()
1607 .map(|entry| {
1608 let cell = entry.row.slot_cell(focused_slot);
1609 Row::new(vec![
1610 cell_from_styled_text(&entry.row.contribution.display),
1611 cell_from_styled_text(
1612 &entry
1613 .row
1614 .component
1615 .raw
1616 .label_display_for_view(self.integral_view),
1617 ),
1618 cell_from_optional_styled_text(
1619 cell.and_then(|cell| {
1620 cell.statistics(self.integral_view).value.as_ref()
1621 })
1622 .map(|value| &value.display),
1623 ),
1624 cell_from_optional_styled_text(
1625 cell.and_then(|cell| {
1626 cell.statistics(self.integral_view).relative_error.as_ref()
1627 })
1628 .map(|value| &value.display),
1629 ),
1630 cell_from_optional_styled_text(
1631 cell.and_then(|cell| {
1632 cell.statistics(self.integral_view).chi_sq.as_ref()
1633 })
1634 .map(|value| &value.display),
1635 ),
1636 cell_from_optional_styled_text(
1637 cell.and_then(|cell| {
1638 cell.statistics(self.integral_view)
1639 .max_weight_impact
1640 .as_ref()
1641 })
1642 .map(|value| &value.display),
1643 ),
1644 cell_from_optional_styled_text(
1645 cell.and_then(|cell| cell.sample_fraction.as_ref())
1646 .map(|value| &value.display),
1647 ),
1648 cell_from_optional_styled_text(
1649 cell.and_then(|cell| cell.sample_count.as_ref())
1650 .map(|value| &value.display),
1651 ),
1652 cell_from_optional_styled_text(
1653 cell.and_then(|cell| cell.target_pdf.as_ref())
1654 .map(|value| &value.display),
1655 ),
1656 ])
1657 })
1658 .collect::<Vec<_>>(),
1659 );
1660
1661 let mut state = TableState::default().with_selected(Some(self.selected_discrete_row + 1));
1662 let table = Table::new(
1663 rows,
1664 [
1665 Constraint::Length(contribution_width),
1666 Constraint::Length(5),
1667 Constraint::Length(integral_width),
1668 Constraint::Length(8),
1669 Constraint::Length(8),
1670 Constraint::Length(10),
1671 Constraint::Length(8),
1672 Constraint::Length(10),
1673 Constraint::Min(8),
1674 ],
1675 )
1676 .header(Row::new(
1677 update
1678 .main_results
1679 .discrete_headers()
1680 .iter()
1681 .map(cell_from_styled_text)
1682 .collect::<Vec<_>>(),
1683 ))
1684 .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED))
1685 .block(titled_block_styled(&scoped_integrand_title(
1686 &format!(
1687 "Discrete bins {} for focused integrand ",
1688 integral_view_label(self.integral_view)
1689 ),
1690 &update.main_results.slot_headers[focused_slot],
1691 &format!(
1692 " (sort: {} {})",
1693 discrete_sort_label(self.discrete_sort),
1694 if self.discrete_descending {
1695 "desc"
1696 } else {
1697 "asc"
1698 }
1699 ),
1700 )));
1701 frame.render_stateful_widget(table, horizontal[0], &mut state);
1702
1703 let selected = discrete_rows
1704 .get(self.selected_discrete_row)
1705 .copied()
1706 .unwrap_or(discrete_rows[0]);
1707 self.draw_selected_discrete_detail(frame, horizontal[1], update, selected.row);
1708 }
1709
1710 fn draw_selected_discrete_detail(
1711 &self,
1712 frame: &mut Frame<'_>,
1713 area: Rect,
1714 update: &StatusUpdate,
1715 row: &MainResultsRow,
1716 ) {
1717 let layout = Layout::default()
1718 .direction(Direction::Vertical)
1719 .constraints([Constraint::Length(3), Constraint::Min(6)])
1720 .split(area);
1721 let mut selected_line = vec![Span::raw("selected: ")];
1722 selected_line.extend(spans_from_styled_text(&row.contribution.display));
1723 selected_line.push(Span::raw(" "));
1724 selected_line.extend(spans_from_styled_text(
1725 &row.component.raw.label_display_for_view(self.integral_view),
1726 ));
1727 frame.render_widget(
1728 Paragraph::new(Line::from(selected_line)).block(titled_block("Selected bin")),
1729 layout[0],
1730 );
1731
1732 let slot_width = update
1733 .main_results
1734 .slot_headers
1735 .iter()
1736 .map(|slot| slot.to_plain_string().chars().count())
1737 .max()
1738 .unwrap_or(8)
1739 .max(8) as u16;
1740 let integral_width = (0..update.main_results.slot_headers.len())
1741 .filter_map(|slot_index| {
1742 row.slot_cell(slot_index)
1743 .and_then(|cell| cell.statistics(self.integral_view).value.as_ref())
1744 .map(|value| value.display.to_plain_string().chars().count())
1745 })
1746 .chain(std::iter::once("integral".chars().count()))
1747 .max()
1748 .unwrap_or(14)
1749 .max(14) as u16;
1750 let mut rows = vec![blank_table_row(8)];
1751 rows.extend(
1752 (0..update.main_results.slot_headers.len())
1753 .filter_map(|slot_index| {
1754 let cell = row.slot_cell(slot_index)?;
1755 let statistics = cell.statistics(self.integral_view);
1756 Some(Row::new(vec![
1757 cell_from_styled_text(&update.main_results.slot_headers[slot_index]),
1758 cell_from_optional_styled_text(
1759 statistics.value.as_ref().map(|value| &value.display),
1760 ),
1761 cell_from_optional_styled_text(
1762 statistics
1763 .relative_error
1764 .as_ref()
1765 .map(|value| &value.display),
1766 ),
1767 cell_from_optional_styled_text(
1768 statistics.chi_sq.as_ref().map(|value| &value.display),
1769 ),
1770 cell_from_optional_styled_text(
1771 statistics
1772 .max_weight_impact
1773 .as_ref()
1774 .map(|value| &value.display),
1775 ),
1776 cell_from_optional_styled_text(
1777 cell.sample_fraction.as_ref().map(|value| &value.display),
1778 ),
1779 cell_from_optional_styled_text(
1780 cell.sample_count.as_ref().map(|value| &value.display),
1781 ),
1782 cell_from_optional_styled_text(
1783 cell.target_pdf.as_ref().map(|value| &value.display),
1784 ),
1785 ]))
1786 })
1787 .collect::<Vec<_>>(),
1788 );
1789
1790 let table = Table::new(
1791 rows,
1792 [
1793 Constraint::Length(slot_width),
1794 Constraint::Length(integral_width),
1795 Constraint::Length(10),
1796 Constraint::Length(8),
1797 Constraint::Length(10),
1798 Constraint::Length(10),
1799 Constraint::Length(10),
1800 Constraint::Min(8),
1801 ],
1802 )
1803 .header(Row::new(
1804 update
1805 .main_results
1806 .selected_bin_detail_headers()
1807 .iter()
1808 .map(cell_from_styled_text)
1809 .collect::<Vec<_>>(),
1810 ))
1811 .block(titled_block(format!(
1812 "Per integrand details · {}",
1813 integral_view_label(self.integral_view)
1814 )))
1815 .column_spacing(1);
1816 frame.render_widget(table, layout[1]);
1817 }
1818
1819 fn draw_max_weight_tab(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
1820 let focused_slot = self
1821 .focused_slot
1822 .min(update.main_results.slot_headers.len().saturating_sub(1));
1823 let focused_slot_name = update.main_results.slot_headers[focused_slot].to_plain_string();
1824 let top_height = if area.height > 18 {
1825 update
1826 .max_weight_details(self.integral_view)
1827 .map(|section| {
1828 let row_count = section
1829 .rows_by_slot
1830 .iter()
1831 .map(|rows| rows.len())
1832 .sum::<usize>() as u16;
1833 row_count
1834 .saturating_add(6)
1835 .clamp(8, area.height.saturating_sub(10))
1836 })
1837 .unwrap_or(area.height.saturating_mul(2) / 5)
1838 } else {
1839 area.height.saturating_mul(2) / 5
1840 };
1841 let layout = Layout::default()
1842 .direction(Direction::Vertical)
1843 .constraints([Constraint::Length(top_height), Constraint::Min(8)])
1844 .split(area);
1845
1846 if let Some(section) = update.max_weight_details(self.integral_view) {
1847 let headers = section.headers();
1848 let mut title = section.title();
1849 title.push_text(
1850 format!(" · {}", integral_view_label(self.integral_view)),
1851 TextStyle::green().bold(),
1852 );
1853 let max_eval_width = max_styled_text_width(
1854 std::iter::once(&headers[2]).chain(
1855 section
1856 .rows_by_slot
1857 .iter()
1858 .flat_map(|group| group.iter().map(|row| &row.max_eval.display)),
1859 ),
1860 );
1861 let mut rows = vec![blank_table_row(4)];
1862 rows.extend(
1863 section
1864 .rows_by_slot
1865 .iter()
1866 .flat_map(|group| group.iter())
1867 .map(|row| {
1868 Row::new(vec![
1869 cell_from_styled_text(&row.slot.display),
1870 cell_from_styled_text(&row.component_sign.display),
1871 cell_from_styled_text(&row.max_eval.display),
1872 cell_from_styled_text(&row.coordinates.display),
1873 ])
1874 .height(styled_text_line_count(&row.coordinates.display))
1875 })
1876 .collect::<Vec<_>>(),
1877 );
1878 rows.push(blank_table_row(4));
1879 let table = Table::new(
1880 rows,
1881 [
1882 Constraint::Length(24),
1883 Constraint::Length(10),
1884 Constraint::Length(max_eval_width),
1885 Constraint::Min(24),
1886 ],
1887 )
1888 .header(Row::new(
1889 headers
1890 .iter()
1891 .map(cell_from_styled_text)
1892 .collect::<Vec<_>>(),
1893 ))
1894 .block(titled_block_styled(&title));
1895 frame.render_widget(table, layout[0]);
1896 } else {
1897 frame.render_widget(
1898 Paragraph::new("No overall max-weight data.")
1899 .block(titled_block("Overall max weight")),
1900 layout[0],
1901 );
1902 }
1903
1904 if let Some(section) = update.discrete_max_weight_details(self.integral_view) {
1905 let sorted_rows = self.sorted_discrete_max_weight_rows(section);
1906 let lower = Layout::default()
1907 .direction(Direction::Horizontal)
1908 .constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
1909 .split(layout[1]);
1910 let summary_headers = section.summary_headers();
1911 let mut summary_rows = vec![blank_table_row(section.slot_headers.len() + 2)];
1912 summary_rows.extend(
1913 sorted_rows
1914 .iter()
1915 .map(|row| {
1916 let mut cells = vec![
1917 cell_from_styled_text(&row.contribution.display),
1918 cell_from_styled_text(&row.component_sign.display),
1919 ];
1920 cells.extend(
1921 row.slot_values
1922 .iter()
1923 .map(|value| {
1924 cell_from_optional_styled_text(
1925 value.as_ref().map(|field| &field.display),
1926 )
1927 })
1928 .collect::<Vec<_>>(),
1929 );
1930 Row::new(cells)
1931 })
1932 .collect::<Vec<_>>(),
1933 );
1934 let summary_widths = std::iter::once(Constraint::Length(24))
1935 .chain(std::iter::once(Constraint::Length(10)))
1936 .chain(
1937 section
1938 .slot_headers
1939 .iter()
1940 .enumerate()
1941 .map(|(slot_index, header)| {
1942 Constraint::Min(
1943 max_styled_text_width(std::iter::once(header).chain(
1944 sorted_rows.iter().filter_map(|row| {
1945 row.slot_values
1946 .get(slot_index)
1947 .and_then(|value| value.as_ref())
1948 .map(|field| &field.display)
1949 }),
1950 ))
1951 .max(16),
1952 )
1953 }),
1954 )
1955 .collect::<Vec<_>>();
1956 let table = Table::new(summary_rows, summary_widths)
1957 .header(Row::new(
1958 summary_headers
1959 .iter()
1960 .map(cell_from_styled_text)
1961 .collect::<Vec<_>>(),
1962 ))
1963 .block(titled_block(format!(
1964 "Per-bin max weight · {} (sort: {} {})",
1965 integral_view_label(self.integral_view),
1966 discrete_sort_label(self.discrete_sort),
1967 if self.discrete_descending {
1968 "desc"
1969 } else {
1970 "asc"
1971 }
1972 )))
1973 .column_spacing(1);
1974 frame.render_widget(table, lower[0]);
1975
1976 let coordinate_headers = section.coordinate_headers();
1977 let focused_slot_filter = focused_slot_name.clone();
1978 let mut coordinate_rows = vec![blank_table_row(3)];
1979 coordinate_rows.extend(
1980 sorted_rows
1981 .iter()
1982 .flat_map(|row| {
1983 let focused_slot_filter = focused_slot_filter.clone();
1984 row.slot_coordinates
1985 .iter()
1986 .filter(move |entry| entry.slot.raw == focused_slot_filter)
1987 .map(move |entry| {
1988 Row::new(vec![
1989 cell_from_styled_text(&row.contribution.display),
1990 cell_from_styled_text(&row.component_sign.display),
1991 cell_from_styled_text(&entry.coordinates.display),
1992 ])
1993 .height(styled_text_line_count(&entry.coordinates.display))
1994 })
1995 })
1996 .collect::<Vec<_>>(),
1997 );
1998 let coordinates_table = Table::new(
1999 coordinate_rows,
2000 [
2001 Constraint::Length(18),
2002 Constraint::Length(8),
2003 Constraint::Min(24),
2004 ],
2005 )
2006 .header(Row::new(vec![
2007 cell_from_styled_text(&coordinate_headers[0]),
2008 cell_from_styled_text(&coordinate_headers[1]),
2009 cell_from_styled_text(&coordinate_headers[3]),
2010 ]))
2011 .block(titled_block_styled(&scoped_integrand_title(
2012 &format!(
2013 "Maximum weight coordinates {} for integrand ",
2014 integral_view_label(self.integral_view)
2015 ),
2016 &update.main_results.slot_headers[focused_slot],
2017 "",
2018 )));
2019 frame.render_widget(coordinates_table, lower[1]);
2020 } else {
2021 frame.render_widget(
2022 Paragraph::new("No per-bin max-weight data.")
2023 .block(titled_block("Per-bin max weight")),
2024 layout[1],
2025 );
2026 }
2027 }
2028
2029 fn draw_footer(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
2030 let phase = update
2031 .training_component()
2032 .map(|component| match component {
2033 ComponentKind::Real => "re",
2034 ComponentKind::Imag => "im",
2035 })
2036 .unwrap_or("n/a");
2037 let footer = Paragraph::new(Line::from(vec![
2038 Span::styled("Tabs", label_style()),
2039 Span::raw(" 1/2/3 or <- -> "),
2040 Span::styled("View", label_style()),
2041 Span::raw(format!(
2042 " a ({}) ",
2043 integral_view_label(self.integral_view)
2044 )),
2045 Span::styled("Integrand", label_style()),
2046 Span::raw(" [ / ] "),
2047 Span::styled("Scope", label_style()),
2048 Span::raw(" i "),
2049 Span::styled("Bins", label_style()),
2050 Span::raw(" j/k or Up/Down "),
2051 Span::styled("Metrics", label_style()),
2052 Span::raw(" r c w "),
2053 Span::styled("Phase", label_style()),
2054 Span::raw(" p "),
2055 Span::styled("Sort", label_style()),
2056 Span::raw(" s/v "),
2057 Span::styled("History", label_style()),
2058 Span::raw(format!(
2059 " g +/- ({}) ",
2060 self.chart_history_window.description()
2061 )),
2062 Span::styled("Y-axis", label_style()),
2063 Span::raw(format!(" , . 0 (±{}σ) ", self.chart_y_sigma_span)),
2064 Span::styled("Help", label_style()),
2065 Span::raw(" ?/Esc "),
2066 Span::styled("Abort", label_style()),
2067 Span::raw(format!(" x / Ctrl-C training {phase}")),
2068 ]))
2069 .alignment(Alignment::Left)
2070 .block(Block::default().borders(Borders::TOP));
2071 frame.render_widget(footer, area);
2072 }
2073
2074 fn draw_help_overlay(&self, frame: &mut Frame<'_>, area: Rect) {
2075 let popup = centered_rect(72, 65, area);
2076 frame.render_widget(Clear, popup);
2077 frame.render_widget(
2078 Paragraph::new(Text::from(vec![
2079 Line::from("1/2/3 or Left/Right switch tabs"),
2080 Line::from("a / A toggle ⟨I⟩ / ⟨|I|⟩ globally"),
2081 Line::from("[ and ] change focused integrand"),
2082 Line::from("j / k move selected discrete row"),
2083 Line::from("s cycle discrete sort"),
2084 Line::from("v reverse discrete sort order"),
2085 Line::from("i toggle global/focused statistics"),
2086 Line::from("r / c / w toggle rel err, chi^2, mwi columns"),
2087 Line::from("p toggle convergence real/imag phase"),
2088 Line::from("g toggle full/recent chart history"),
2089 Line::from("+ / - widen or narrow recent-history window"),
2090 Line::from(", / . tighten or widen the y-range in σ units"),
2091 Line::from("0 reset the y-range to ±4σ"),
2092 Line::from("x abort the current iteration"),
2093 Line::from("Ctrl-C stop the integration"),
2094 Line::from("? close help"),
2095 ]))
2096 .block(titled_block("Dashboard controls"))
2097 .alignment(Alignment::Left)
2098 .wrap(Wrap { trim: false }),
2099 popup,
2100 );
2101 }
2102
2103 fn draw_mix_panel(
2104 &self,
2105 frame: &mut Frame<'_>,
2106 area: Rect,
2107 title: &StyledText,
2108 segments: &[StatisticsMixSegment],
2109 median_entries: Option<&[StatisticsMedianEntry]>,
2110 ) {
2111 let block = titled_block_styled(title);
2112 let has_median_line = median_entries.is_some_and(|entries| !entries.is_empty());
2113 let inner = Layout::default()
2114 .direction(Direction::Vertical)
2115 .constraints(vec![
2116 Constraint::Length(1);
2117 if has_median_line { 6 } else { 5 }
2118 ])
2119 .split(block.inner(area));
2120 frame.render_widget(block, area);
2121 frame.render_widget(Paragraph::new(""), inner[0]);
2122 frame.render_widget(
2123 Paragraph::new(mix_bar_line(segments, inner[1].width as usize)),
2124 inner[1],
2125 );
2126 frame.render_widget(Paragraph::new(""), inner[2]);
2127 if !segments.is_empty() {
2128 let constraints = (0..segments.len())
2129 .map(|_| Constraint::Percentage((100 / segments.len().max(1)) as u16))
2130 .collect::<Vec<_>>();
2131 let labels = Layout::default()
2132 .direction(Direction::Horizontal)
2133 .constraints(constraints)
2134 .split(inner[3]);
2135 for (segment, label_area) in segments.iter().zip(labels.iter().copied()) {
2136 frame.render_widget(
2137 Paragraph::new(Line::from(vec![Span::styled(
2138 format!(
2139 "{} : {:.1}%",
2140 segment.label.to_plain_string(),
2141 segment.percentage
2142 ),
2143 style_from_text_style(
2144 segment
2145 .label
2146 .spans
2147 .first()
2148 .map(|span| span.style)
2149 .unwrap_or(TextStyle::PLAIN),
2150 )
2151 .add_modifier(Modifier::BOLD),
2152 )]))
2153 .alignment(Alignment::Center),
2154 label_area,
2155 );
2156 }
2157 }
2158 if let Some(entries) = median_entries.filter(|entries| !entries.is_empty()) {
2159 frame.render_widget(
2160 Paragraph::new(numerical_stability_median_line(entries))
2161 .alignment(Alignment::Center),
2162 inner[4],
2163 );
2164 frame.render_widget(Paragraph::new(""), inner[5]);
2165 } else {
2166 frame.render_widget(Paragraph::new(""), inner[4]);
2167 }
2168 }
2169
2170 fn discrete_rows(&self) -> Vec<DiscreteRowRef<'_>> {
2171 let Some(update) = self.latest_update.as_ref() else {
2172 return Vec::new();
2173 };
2174 let focused_slot = self
2175 .focused_slot
2176 .min(update.main_results.slot_headers.len().saturating_sub(1));
2177 self.discrete_rows_for_slot(focused_slot)
2178 }
2179
2180 fn selected_discrete_key(&self) -> Option<(ContributionKind, ComponentKind)> {
2181 self.discrete_rows()
2182 .get(self.selected_discrete_row)
2183 .map(|row| (row.contribution(), row.component()))
2184 }
2185
2186 fn restore_discrete_selection(
2187 &mut self,
2188 selected_row: Option<(ContributionKind, ComponentKind)>,
2189 ) {
2190 let Some(selected_row) = selected_row else {
2191 return;
2192 };
2193 if let Some(index) = self
2194 .discrete_rows()
2195 .iter()
2196 .position(|row| (row.contribution(), row.component()) == selected_row)
2197 {
2198 self.selected_discrete_row = index;
2199 }
2200 }
2201
2202 fn selected_statistics_scope(&self, update: &StatusUpdate) -> StatisticsScope {
2203 match self.statistics_scope {
2204 DashboardStatisticsScope::Global => StatisticsScope::Global,
2205 DashboardStatisticsScope::FocusedSlot => StatisticsScope::Slot(
2206 self.focused_slot
2207 .min(update.main_results.slot_headers.len().saturating_sub(1)),
2208 ),
2209 }
2210 }
2211
2212 fn discrete_rows_for_slot(&self, slot_index: usize) -> Vec<DiscreteRowRef<'_>> {
2213 let Some(update) = self.latest_update.as_ref() else {
2214 return Vec::new();
2215 };
2216 let mut rows = update
2217 .main_results
2218 .row_groups_of_kind(MainResultsRowGroupKind::Bins)
2219 .flat_map(|group| group.rows.iter())
2220 .map(|row| DiscreteRowRef { row })
2221 .collect::<Vec<_>>();
2222
2223 rows.sort_by(|lhs, rhs| {
2224 let lhs_key = lhs.sort_value(self.discrete_sort, slot_index, self.integral_view);
2225 let rhs_key = rhs.sort_value(self.discrete_sort, slot_index, self.integral_view);
2226 let ordering = match self.discrete_sort {
2227 ContributionSortMode::Index => match (lhs.contribution(), rhs.contribution()) {
2228 (ContributionKind::Bin(lhs_index), ContributionKind::Bin(rhs_index)) => {
2229 lhs_index
2230 .cmp(&rhs_index)
2231 .then(lhs.component().cmp(&rhs.component()))
2232 }
2233 _ => Ordering::Equal,
2234 },
2235 ContributionSortMode::Integral | ContributionSortMode::Error => lhs_key
2236 .partial_cmp(&rhs_key)
2237 .unwrap_or(Ordering::Equal)
2238 .then(lhs.component().cmp(&rhs.component())),
2239 };
2240 if self.discrete_descending {
2241 ordering.reverse()
2242 } else {
2243 ordering
2244 }
2245 });
2246 rows
2247 }
2248
2249 fn sorted_discrete_max_weight_rows<'a>(
2250 &self,
2251 section: &'a super::status_update::DiscreteMaxWeightDetailsSection,
2252 ) -> Vec<&'a super::status_update::DiscreteMaxWeightRow> {
2253 let discrete_order = self
2254 .discrete_rows_for_slot(0)
2255 .into_iter()
2256 .map(|row| (row.contribution(), row.component()))
2257 .collect::<Vec<_>>();
2258 let mut all_rows = section
2259 .row_groups
2260 .iter()
2261 .flat_map(|group| group.iter())
2262 .collect::<Vec<_>>();
2263 all_rows.sort_by_key(|row| {
2264 self.max_weight_row_sort_rank(
2265 &discrete_order,
2266 row.contribution.raw,
2267 row.component_sign.raw.0,
2268 )
2269 });
2270 all_rows
2271 }
2272
2273 fn max_weight_row_sort_rank(
2274 &self,
2275 discrete_order: &[(ContributionKind, ComponentKind)],
2276 contribution: ContributionKind,
2277 component: ComponentKind,
2278 ) -> (usize, usize) {
2279 match contribution {
2280 ContributionKind::All => (0, component_rank(component)),
2281 ContributionKind::Bin(_) => (
2282 1,
2283 discrete_order
2284 .iter()
2285 .position(|entry| *entry == (contribution, component))
2286 .unwrap_or(usize::MAX),
2287 ),
2288 ContributionKind::Sum => (2, component_rank(component)),
2289 }
2290 }
2291
2292 fn overview_value_text(&self, update: &StatusUpdate, slot_index: usize) -> Text<'static> {
2293 overview_metric_text(update, slot_index, self.integral_view, |cell| {
2294 cell.statistics(self.integral_view)
2295 .value
2296 .as_ref()
2297 .map(|value| &value.display)
2298 })
2299 }
2300
2301 fn overview_summary_rows<'a>(&self, update: &'a StatusUpdate) -> Vec<&'a MainResultsRow> {
2302 update
2303 .main_results
2304 .row_groups_of_kind(MainResultsRowGroupKind::All)
2305 .chain(
2306 update
2307 .main_results
2308 .row_groups_of_kind(MainResultsRowGroupKind::Sum),
2309 )
2310 .flat_map(|group| group.rows.iter())
2311 .collect()
2312 }
2313
2314 fn results_summary_cell(&self, row: &MainResultsRow, slot_index: usize) -> Text<'static> {
2315 let Some(cell) = row.slot_cell(slot_index) else {
2316 return Text::from("N/A");
2317 };
2318 let statistics = cell.statistics(self.integral_view);
2319
2320 let mut lines = Vec::new();
2321 if let Some(value) = statistics.value.as_ref() {
2322 lines.push(Line::from(spans_from_styled_text(&value.display)));
2323 } else {
2324 lines.push(Line::from("N/A"));
2325 }
2326
2327 let mut metrics = Vec::new();
2328 if self.metric_visibility.relative_error {
2329 metrics.push(labeled_value_line(
2330 "rel err",
2331 statistics
2332 .relative_error
2333 .as_ref()
2334 .map(|value| &value.display),
2335 ));
2336 }
2337 if self.metric_visibility.chi_sq {
2338 metrics.push(labeled_value_line(
2339 "chi^2",
2340 statistics.chi_sq.as_ref().map(|value| &value.display),
2341 ));
2342 }
2343 if self.metric_visibility.max_weight_impact {
2344 metrics.push(labeled_value_line(
2345 "mwi",
2346 statistics
2347 .max_weight_impact
2348 .as_ref()
2349 .map(|value| &value.display),
2350 ));
2351 }
2352 if !metrics.is_empty() && !matches!(self.density, DensityMode::Compact) {
2353 let mut spans = Vec::new();
2354 for (index, metric_line) in metrics.into_iter().enumerate() {
2355 if index > 0 {
2356 spans.push(Span::raw(" "));
2357 }
2358 spans.extend(metric_line.spans);
2359 }
2360 lines.push(Line::from(spans));
2361 }
2362
2363 Text::from(lines)
2364 }
2365}
2366
2367fn overview_metric_text(
2368 update: &StatusUpdate,
2369 slot_index: usize,
2370 view: IntegralView,
2371 select: impl Fn(&MainTableSlotCells) -> Option<&StyledText>,
2372) -> Text<'static> {
2373 let select = &select;
2374 let lines = update
2375 .main_results
2376 .row_groups_of_kind(MainResultsRowGroupKind::All)
2377 .flat_map(|group| group.rows.iter())
2378 .filter_map(|row| {
2379 let text = row.slot_cell(slot_index).and_then(select)?;
2380 let mut spans = spans_from_styled_text(&row.component.raw.label_display_for_view(view));
2381 spans.push(Span::raw(": "));
2382 spans.extend(spans_from_styled_text(text));
2383 Some(Line::from(spans))
2384 })
2385 .collect::<Vec<_>>();
2386
2387 if lines.is_empty() {
2388 Text::from("N/A")
2389 } else {
2390 Text::from(lines)
2391 }
2392}
2393
2394fn component_label_with_colon(component: ComponentKind, view: IntegralView) -> StyledText {
2395 let mut label = component.label_display_for_view(view);
2396 label.push_text(":", component.text_style());
2397 label
2398}
2399
2400fn integral_view_label(view: IntegralView) -> &'static str {
2401 match view {
2402 IntegralView::Signed => "⟨I⟩",
2403 IntegralView::Absolute => "⟨|I|⟩",
2404 }
2405}
2406
2407fn component_rank(component: ComponentKind) -> usize {
2408 match component {
2409 ComponentKind::Real => 0,
2410 ComponentKind::Imag => 1,
2411 }
2412}
2413
2414fn scoped_integrand_title(prefix: &str, slot_header: &StyledText, suffix: &str) -> StyledText {
2415 let mut title = StyledText::styled(prefix, TextStyle::green().bold());
2416 title.push_text("[", TextStyle::green().bold());
2417 title.append(slot_header.clone());
2418 title.push_text("]", TextStyle::green().bold());
2419 title.push_text(suffix, TextStyle::green().bold());
2420 title
2421}
2422
2423fn format_samples_per_second(rate: f64) -> String {
2424 abbreviate_count(rate.max(0.0).round() as usize)
2425}
2426
2427fn labeled_value_line(label: &str, value: Option<&StyledText>) -> Line<'static> {
2428 let mut spans = vec![Span::raw(format!("{label} "))];
2429 if let Some(value) = value {
2430 spans.extend(spans_from_styled_text(value));
2431 } else {
2432 spans.push(Span::raw("N/A"));
2433 }
2434 Line::from(spans)
2435}
2436
2437fn style_from_text_style(style: TextStyle) -> Style {
2438 let mut rendered = Style::default();
2439 if let Some(color) = color_from_text_style(style) {
2440 rendered = rendered.fg(color);
2441 }
2442 if style.bold {
2443 rendered = rendered.add_modifier(Modifier::BOLD);
2444 }
2445 if style.dimmed {
2446 rendered = rendered.add_modifier(Modifier::DIM);
2447 }
2448 rendered
2449}
2450
2451fn spans_from_styled_text(text: &StyledText) -> Vec<Span<'static>> {
2452 text.spans
2453 .iter()
2454 .map(|span| Span::styled(span.text.clone(), style_from_text_style(span.style)))
2455 .collect()
2456}
2457
2458fn line_from_styled_text(text: &StyledText) -> Line<'static> {
2459 Line::from(spans_from_styled_text(text))
2460}
2461
2462fn text_from_styled_text(text: &StyledText) -> Text<'static> {
2463 let mut lines = vec![Line::default()];
2464 for span in &text.spans {
2465 let style = style_from_text_style(span.style);
2466 for (index, segment) in span.text.split('\n').enumerate() {
2467 if index > 0 {
2468 lines.push(Line::default());
2469 }
2470 if !segment.is_empty() {
2471 lines
2472 .last_mut()
2473 .expect("at least one line")
2474 .spans
2475 .push(Span::styled(segment.to_string(), style));
2476 }
2477 }
2478 }
2479 Text::from(lines)
2480}
2481
2482fn styled_text_line_count(text: &StyledText) -> u16 {
2483 text.to_plain_string()
2484 .lines()
2485 .count()
2486 .max(1)
2487 .try_into()
2488 .unwrap_or(u16::MAX)
2489}
2490
2491fn styled_text_max_line_width(text: &StyledText) -> u16 {
2492 text.to_plain_string()
2493 .lines()
2494 .map(|line| line.chars().count())
2495 .max()
2496 .unwrap_or(0)
2497 .max(1)
2498 .try_into()
2499 .unwrap_or(u16::MAX)
2500}
2501
2502fn max_styled_text_width<'a>(texts: impl IntoIterator<Item = &'a StyledText>) -> u16 {
2503 texts
2504 .into_iter()
2505 .map(styled_text_max_line_width)
2506 .max()
2507 .unwrap_or(1)
2508}
2509
2510fn styled_text_line(text: &StyledText, line_index: usize) -> StyledText {
2511 let mut lines = vec![StyledText::new()];
2512 for span in &text.spans {
2513 for (index, segment) in span.text.split('\n').enumerate() {
2514 if index > 0 {
2515 lines.push(StyledText::new());
2516 }
2517 if !segment.is_empty() {
2518 lines
2519 .last_mut()
2520 .expect("at least one styled line")
2521 .push_text(segment, span.style);
2522 }
2523 }
2524 }
2525 lines.get(line_index).cloned().unwrap_or_default()
2526}
2527
2528fn cell_from_styled_text(text: &StyledText) -> Cell<'static> {
2529 Cell::from(text_from_styled_text(text))
2530}
2531
2532fn cell_from_optional_styled_text(text: Option<&StyledText>) -> Cell<'static> {
2533 text.map(cell_from_styled_text).unwrap_or_else(blank_cell)
2534}
2535
2536fn plain_header_cell(text: impl Into<String>) -> Cell<'static> {
2537 Cell::from(Span::styled(text.into(), label_style()))
2538}
2539
2540fn blank_cell() -> Cell<'static> {
2541 Cell::from(String::new())
2542}
2543
2544fn blank_table_row(column_count: usize) -> Row<'static> {
2545 Row::new((0..column_count).map(|_| blank_cell()).collect::<Vec<_>>())
2546}
2547
2548fn stacked_header(title: &StyledText, subtitle: &str) -> StyledText {
2549 let mut text = title.clone();
2550 text.push_text("\n", TextStyle::PLAIN);
2551 text.push_text(subtitle, TextStyle::PLAIN.bold());
2552 text
2553}
2554
2555fn titled_block(title: impl Into<String>) -> Block<'static> {
2556 Block::default()
2557 .borders(Borders::ALL)
2558 .title(Line::from(vec![Span::styled(
2559 title.into(),
2560 Style::default()
2561 .fg(Color::Green)
2562 .add_modifier(Modifier::BOLD),
2563 )]))
2564 .border_style(Style::default().fg(Color::DarkGray))
2565}
2566
2567fn titled_block_styled(title: &StyledText) -> Block<'static> {
2568 Block::default()
2569 .borders(Borders::ALL)
2570 .title(line_from_styled_text(title))
2571 .border_style(Style::default().fg(Color::DarkGray))
2572}
2573
2574fn label_style() -> Style {
2575 Style::default().add_modifier(Modifier::BOLD)
2576}
2577
2578fn abbreviate_count(value: usize) -> String {
2579 super::display::format_abbreviated_count(value)
2580}
2581
2582fn numerical_stability_median_line(entries: &[StatisticsMedianEntry]) -> Line<'static> {
2583 let mut spans = vec![Span::raw("stability: ")];
2584 for (index, entry) in entries.iter().enumerate() {
2585 if index > 0 {
2586 spans.push(Span::raw(" "));
2587 }
2588 spans.extend(spans_from_styled_text(&entry.label));
2589 spans.push(Span::raw(": "));
2590 spans.extend(spans_from_styled_text(&entry.value));
2591 spans.push(Span::raw(" ("));
2592 spans.extend(spans_from_styled_text(&entry.processed_percentage));
2593 spans.push(Span::raw(")"));
2594 }
2595 Line::from(spans)
2596}
2597
2598fn mix_bar_line(segments: &[StatisticsMixSegment], width: usize) -> Line<'static> {
2599 let bar_width = width.max(8);
2600 let mut spans = Vec::new();
2601 let mut used = 0usize;
2602 for (index, segment) in segments.iter().enumerate() {
2603 let color = color_from_styled_text(&segment.label, Color::DarkGray);
2604 let width = if index + 1 == segments.len() {
2605 bar_width.saturating_sub(used)
2606 } else {
2607 let segment_width = ((segment.percentage / 100.0) * bar_width as f64).round() as usize;
2608 used += segment_width;
2609 segment_width
2610 };
2611 spans.push(Span::styled("█".repeat(width), Style::default().fg(color)));
2612 }
2613 Line::from(spans)
2614}
2615
2616fn discrete_sort_label(mode: ContributionSortMode) -> &'static str {
2617 match mode {
2618 ContributionSortMode::Index => "index",
2619 ContributionSortMode::Integral => "integral",
2620 ContributionSortMode::Error => "error",
2621 }
2622}
2623
2624fn color_from_text_style(style: TextStyle) -> Option<Color> {
2625 match style.color {
2626 Some(TextColor::Green) => Some(Color::Green),
2627 Some(TextColor::Blue) => Some(Color::Blue),
2628 Some(TextColor::Red) => Some(Color::Red),
2629 Some(TextColor::Yellow) => Some(Color::Yellow),
2630 Some(TextColor::Pink) => Some(Color::LightMagenta),
2631 None => None,
2632 }
2633}
2634
2635fn color_from_styled_text(text: &StyledText, fallback: Color) -> Color {
2636 text.spans
2637 .iter()
2638 .find_map(|span| color_from_text_style(span.style))
2639 .unwrap_or(fallback)
2640}
2641
2642fn count_axis_labels(min: f64, max: f64) -> Vec<Span<'static>> {
2643 evenly_spaced_values(min, max, 9)
2644 .into_iter()
2645 .map(|value| Span::raw(abbreviate_count(value.max(0.0).round() as usize)))
2646 .collect()
2647}
2648
2649fn value_axis_labels(min: f64, max: f64) -> Vec<Span<'static>> {
2650 evenly_spaced_values(min, max, 9)
2651 .into_iter()
2652 .map(|value| Span::raw(format!("{value:.3e}")))
2653 .collect()
2654}
2655
2656fn evenly_spaced_values(min: f64, max: f64, count: usize) -> Vec<f64> {
2657 if count <= 1 || !min.is_finite() || !max.is_finite() || (max - min).abs() < f64::EPSILON {
2658 return vec![min];
2659 }
2660
2661 let step = (max - min) / (count.saturating_sub(1) as f64);
2662 (0..count).map(|index| min + step * index as f64).collect()
2663}
2664
2665fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
2666 let popup_layout = Layout::default()
2667 .direction(Direction::Vertical)
2668 .constraints([
2669 Constraint::Percentage((100 - percent_y) / 2),
2670 Constraint::Percentage(percent_y),
2671 Constraint::Percentage((100 - percent_y) / 2),
2672 ])
2673 .split(area);
2674 Layout::default()
2675 .direction(Direction::Horizontal)
2676 .constraints([
2677 Constraint::Percentage((100 - percent_x) / 2),
2678 Constraint::Percentage(percent_x),
2679 Constraint::Percentage((100 - percent_x) / 2),
2680 ])
2681 .split(popup_layout[1])[1]
2682}
2683
2684#[cfg(test)]
2685mod tests {
2686 use ratatui::{Terminal, backend::TestBackend};
2687 use spenso::algebra::complex::Complex;
2688
2689 use super::*;
2690 use crate::{
2691 integrate::{
2692 IntegrationStatusKind, IntegrationStatusPhaseDisplay,
2693 display::DisplayField,
2694 status_update::{
2695 DiscreteMaxWeightDetailsSection, DiscreteMaxWeightRow, IntegralStatisticsCells,
2696 MainResultsRowGroup, MainResultsSection, MaxWeightDetailsRow,
2697 MaxWeightDetailsSection, SlotCoordinateEntry, StatusMeta,
2698 },
2699 },
2700 utils::F,
2701 };
2702
2703 fn statistics(avg: f64, err: f64, marker: &str) -> IntegralStatisticsCells {
2704 IntegralStatisticsCells {
2705 value: Some(DisplayField::new(
2706 (F(avg), F(err)),
2707 StyledText::plain(marker),
2708 )),
2709 relative_error: Some(DisplayField::new(
2710 err / avg.abs().max(f64::EPSILON) * 100.0,
2711 StyledText::plain(format!("{marker}_ERR")),
2712 )),
2713 chi_sq: Some(DisplayField::new(
2714 avg.abs(),
2715 StyledText::plain(format!("{marker}_CHI")),
2716 )),
2717 max_weight_impact: Some(DisplayField::new(
2718 err.abs(),
2719 StyledText::plain(format!("{marker}_MWI")),
2720 )),
2721 }
2722 }
2723
2724 fn result_row(
2725 contribution: ContributionKind,
2726 contribution_label: &str,
2727 signed: (f64, f64, &str),
2728 absolute: (f64, f64, &str),
2729 ) -> MainResultsRow {
2730 MainResultsRow {
2731 contribution: DisplayField::new(contribution, StyledText::plain(contribution_label)),
2732 component: DisplayField::new(ComponentKind::Real, StyledText::plain("re")),
2733 slot_cells: vec![MainTableSlotCells {
2734 signed: statistics(signed.0, signed.1, signed.2),
2735 absolute: statistics(absolute.0, absolute.1, absolute.2),
2736 sample_fraction: Some(DisplayField::new(50.0, StyledText::plain("50%"))),
2737 sample_count: Some(DisplayField::new(50, StyledText::plain("50"))),
2738 target_pdf: Some(DisplayField::new(50.0, StyledText::plain("50%"))),
2739 }],
2740 delta_sigma: None,
2741 delta_percent: None,
2742 }
2743 }
2744
2745 fn max_weight_row(component_label: &str, positive: bool, marker: &str) -> MaxWeightDetailsRow {
2746 MaxWeightDetailsRow {
2747 slot: DisplayField::new("slot".to_string(), StyledText::plain("slot")),
2748 component_sign: DisplayField::new(
2749 (ComponentKind::Real, positive),
2750 StyledText::plain(component_label),
2751 ),
2752 max_eval: DisplayField::new(F(42.0), StyledText::plain(marker)),
2753 coordinates: DisplayField::new("x: 0.5".to_string(), StyledText::plain("x: 0.5")),
2754 }
2755 }
2756
2757 fn discrete_max_weight_section(
2758 component_label: &str,
2759 marker: &str,
2760 ) -> DiscreteMaxWeightDetailsSection {
2761 DiscreteMaxWeightDetailsSection {
2762 contribution_header: StyledText::plain("Contribution"),
2763 slot_headers: vec![StyledText::plain("slot")],
2764 row_groups: vec![vec![DiscreteMaxWeightRow {
2765 contribution: DisplayField::new(ContributionKind::Bin(0), StyledText::plain("#0")),
2766 component_sign: DisplayField::new(
2767 (ComponentKind::Real, true),
2768 StyledText::plain(component_label),
2769 ),
2770 slot_values: vec![Some(DisplayField::new(F(42.0), StyledText::plain(marker)))],
2771 slot_coordinates: vec![SlotCoordinateEntry {
2772 slot: DisplayField::new("slot".to_string(), StyledText::plain("slot")),
2773 coordinates: DisplayField::new(
2774 "x: 0.5".to_string(),
2775 StyledText::plain("x: 0.5"),
2776 ),
2777 }],
2778 }]],
2779 }
2780 }
2781
2782 fn status_update() -> StatusUpdate {
2783 StatusUpdate {
2784 kind: IntegrationStatusKind::Live,
2785 meta: StatusMeta {
2786 elapsed_time: Duration::from_secs(10),
2787 iteration_elapsed_time: Duration::from_secs(2),
2788 iteration: 2,
2789 current_iteration_points: 100,
2790 total_points: 100,
2791 n_samples_evaluated: 100,
2792 cores: 1,
2793 training_slot: 0,
2794 training_phase_display: IntegrationStatusPhaseDisplay::Real,
2795 live_progress: None,
2796 show_eta_to_target: true,
2797 eta_to_target_specification: Some("rel ≤ 1%".to_string()),
2798 eta_to_target: Some(Duration::from_secs(5)),
2799 },
2800 targets: vec![Some(Complex::new(F(1.0), F(0.0)))],
2801 slot_training_phase_displays: vec![IntegrationStatusPhaseDisplay::Real],
2802 per_slot_training_phase: false,
2803 main_results: MainResultsSection {
2804 header_left: StyledText::plain(""),
2805 header_middle: StyledText::plain(""),
2806 header_tail: StyledText::plain(""),
2807 contribution_header: StyledText::plain("Contribution\n(idx=graph)"),
2808 slot_headers: vec![StyledText::plain("slot")],
2809 has_discrete_columns: true,
2810 has_target_columns: true,
2811 row_groups: vec![
2812 MainResultsRowGroup {
2813 kind: MainResultsRowGroupKind::All,
2814 rows: vec![result_row(
2815 ContributionKind::All,
2816 "All",
2817 (-2.0, 0.2, "SIGNED_ALL"),
2818 (20.0, 0.4, "ABS_ALL"),
2819 )],
2820 },
2821 MainResultsRowGroup {
2822 kind: MainResultsRowGroupKind::Bins,
2823 rows: vec![
2824 result_row(
2825 ContributionKind::Bin(0),
2826 "#0",
2827 (-1.0, 9.0, "SIGNED_BIN0"),
2828 (12.0, 1.0, "ABS_BIN0"),
2829 ),
2830 result_row(
2831 ContributionKind::Bin(1),
2832 "#1",
2833 (-1.0, 1.0, "SIGNED_BIN1"),
2834 (8.0, 9.0, "ABS_BIN1"),
2835 ),
2836 ],
2837 },
2838 ],
2839 },
2840 max_weight_details: Some(MaxWeightDetailsSection {
2841 rows_by_slot: vec![vec![
2842 max_weight_row("re [+]", true, "SIGNED_MAX_POS"),
2843 max_weight_row("re [-]", false, "SIGNED_MAX_NEG"),
2844 ]],
2845 }),
2846 absolute_max_weight_details: Some(MaxWeightDetailsSection {
2847 rows_by_slot: vec![vec![max_weight_row("|re|", true, "ABS_MAX")]],
2848 }),
2849 discrete_max_weight_details: Some(discrete_max_weight_section(
2850 "re [+]",
2851 "SIGNED_BIN_MAX",
2852 )),
2853 absolute_discrete_max_weight_details: Some(discrete_max_weight_section(
2854 "|re|",
2855 "ABS_BIN_MAX",
2856 )),
2857 statistics: None,
2858 }
2859 }
2860
2861 fn render(dashboard: &RatatuiDashboardState) -> String {
2862 let backend = TestBackend::new(220, 64);
2863 let mut terminal = Terminal::new(backend).expect("test terminal");
2864 terminal
2865 .draw(|frame| dashboard.draw(frame))
2866 .expect("ratatui draw");
2867
2868 let buffer = terminal.backend().buffer();
2869 (0..buffer.area.height)
2870 .map(|y| {
2871 let mut line = String::new();
2872 for x in 0..buffer.area.width {
2873 line.push_str(buffer[(x, y)].symbol());
2874 }
2875 line.trim_end().to_string()
2876 })
2877 .collect::<Vec<_>>()
2878 .join("\n")
2879 }
2880
2881 #[test]
2882 fn absolute_overview_switches_history_and_hides_signed_targets() {
2883 let mut dashboard = RatatuiDashboardState::new();
2884 dashboard.update(status_update());
2885
2886 assert_eq!(
2887 dashboard.history[0].slot_values(ComponentKind::Real, IntegralView::Signed)[0],
2888 Some((-2.0, 0.2))
2889 );
2890 assert_eq!(
2891 dashboard.history[0].slot_values(ComponentKind::Real, IntegralView::Absolute)[0],
2892 Some((20.0, 0.4))
2893 );
2894
2895 let signed = render(&dashboard);
2896 assert!(signed.contains("SIGNED_ALL"), "{signed}");
2897 assert!(signed.contains("trgt"), "{signed}");
2898 assert!(signed.contains("Δ [σ]"), "{signed}");
2899
2900 dashboard.toggle_integral_view();
2901 let absolute = render(&dashboard);
2902 assert!(absolute.contains("ABS_ALL"), "{absolute}");
2903 assert!(!absolute.contains("SIGNED_ALL"), "{absolute}");
2904 assert!(!absolute.contains("trgt"), "{absolute}");
2905 assert!(!absolute.contains("Δ [σ]"), "{absolute}");
2906 assert!(absolute.contains("ETA to ⟨I⟩ target"), "{absolute}");
2907 assert!(absolute.contains("⟨|I|⟩"), "{absolute}");
2908 assert!(
2909 signed.matches("target").count() > absolute.matches("target").count(),
2910 "signed:\n{signed}\nabsolute:\n{absolute}"
2911 );
2912 }
2913
2914 #[test]
2915 fn absolute_view_reaches_every_tab_and_uses_signless_max_weights() {
2916 let mut dashboard = RatatuiDashboardState::new();
2917 dashboard.update(status_update());
2918 dashboard.toggle_integral_view();
2919
2920 for (tab, marker) in [(0, "ABS_ALL"), (1, "ABS_BIN"), (2, "ABS_MAX")] {
2921 dashboard.select_tab(tab);
2922 let rendered = render(&dashboard);
2923 assert!(rendered.contains("⟨|I|⟩"), "tab {tab}:\n{rendered}");
2924 assert!(rendered.contains(marker), "tab {tab}:\n{rendered}");
2925 }
2926
2927 dashboard.select_tab(2);
2928 let max_weight = render(&dashboard);
2929 assert!(max_weight.contains("|re|"), "{max_weight}");
2930 assert!(!max_weight.contains("re [+]"), "{max_weight}");
2931 assert!(!max_weight.contains("re [-]"), "{max_weight}");
2932 assert!(!max_weight.contains("SIGNED_MAX"), "{max_weight}");
2933 }
2934
2935 #[test]
2936 fn active_view_sort_keeps_the_logical_bin_selected() {
2937 let mut dashboard = RatatuiDashboardState::new();
2938 dashboard.update(status_update());
2939
2940 assert_eq!(
2941 dashboard.discrete_rows()[0].contribution(),
2942 ContributionKind::Bin(0)
2943 );
2944 dashboard.select_next_discrete_row();
2945 assert_eq!(
2946 dashboard.selected_discrete_key(),
2947 Some((ContributionKind::Bin(1), ComponentKind::Real))
2948 );
2949
2950 dashboard.toggle_integral_view();
2951 assert_eq!(
2952 dashboard.discrete_rows()[0].contribution(),
2953 ContributionKind::Bin(1)
2954 );
2955 assert_eq!(
2956 dashboard.selected_discrete_key(),
2957 Some((ContributionKind::Bin(1), ComponentKind::Real))
2958 );
2959 }
2960
2961 #[test]
2962 fn numerical_stability_line_shows_processed_percentages() {
2963 let line = numerical_stability_median_line(&[
2964 StatisticsMedianEntry {
2965 label: StyledText::plain("f64"),
2966 value: StyledText::plain("2.4e-16"),
2967 processed_percentage: StyledText::plain("12.34%"),
2968 },
2969 StatisticsMedianEntry {
2970 label: StyledText::plain("f128"),
2971 value: StyledText::plain("1.3e-26"),
2972 processed_percentage: StyledText::plain("1.23e-3%"),
2973 },
2974 StatisticsMedianEntry {
2975 label: StyledText::plain("arb"),
2976 value: StyledText::plain("N/A"),
2977 processed_percentage: StyledText::plain("0.00%"),
2978 },
2979 ]);
2980
2981 assert_eq!(
2982 line.spans
2983 .into_iter()
2984 .map(|span| span.content.into_owned())
2985 .collect::<String>(),
2986 "stability: f64: 2.4e-16 (12.34%) f128: 1.3e-26 (1.23e-3%) arb: N/A (0.00%)"
2987 );
2988 }
2989}