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