Skip to main content

entrenar/train/tui/
mod.rs

1//! Real-Time Terminal Monitoring and Visualization (ENT-054 through ENT-067)
2//!
3//! Terminal-based training visualization using trueno-viz exclusively.
4//!
5//! # Features
6//!
7//! - `MetricsBuffer`: O(1) ring buffer for streaming metrics (ENT-055)
8//! - `Sparkline`: Unicode sparklines for inline metrics (ENT-057)
9//! - `ProgressBar`: Progress bar with Kalman-filtered ETA (ENT-058)
10//! - `RefreshPolicy`: Adaptive refresh rate control (ENT-060)
11//! - `AndonSystem`: Health monitoring with NaN/Inf detection (ENT-066)
12//! - `TerminalMonitorCallback`: Unified callback for training loop (ENT-054)
13//!
14//! # References
15//!
16//! - Tufte, E. R. (2006). *Beautiful Evidence*. Graphics Press. (Sparklines)
17//! - Welch, G., & Bishop, G. (1995). "An Introduction to the Kalman Filter." (ETA)
18
19mod andon;
20mod buffer;
21mod callback;
22mod capability;
23mod charts;
24mod config;
25mod progress;
26mod reference;
27mod refresh;
28mod sparkline;
29
30// Re-export all public types
31pub use andon::{Alert, AlertLevel, AndonSystem};
32pub use buffer::MetricsBuffer;
33pub use callback::TerminalMonitorCallback;
34pub use capability::{DashboardLayout, TerminalCapabilities, TerminalMode};
35pub use charts::{
36    FeatureImportanceChart, GradientFlowHeatmap, LossCurveDisplay, SeriesSummaryTuple,
37};
38pub use config::MonitorConfig;
39pub use progress::{format_duration, KalmanEta, ProgressBar};
40pub use reference::ReferenceCurve;
41pub use refresh::RefreshPolicy;
42pub use sparkline::{sparkline, sparkline_range, SPARK_CHARS};
43
44// =============================================================================
45// Property Tests
46// =============================================================================
47
48#[cfg(test)]
49mod proptests {
50    use super::*;
51    use proptest::prelude::*;
52
53    proptest! {
54        /// MetricsBuffer length never exceeds capacity
55        #[test]
56        fn metrics_buffer_bounded(values in prop::collection::vec(-1000.0f32..1000.0, 0..1000)) {
57            let mut buf = MetricsBuffer::new(100);
58            for v in &values {
59                buf.push(*v);
60            }
61            prop_assert!(buf.len() <= buf.capacity());
62        }
63
64        /// MetricsBuffer values are in chronological order
65        #[test]
66        fn metrics_buffer_order(values in prop::collection::vec(0.0f32..1000.0, 1..100)) {
67            let mut buf = MetricsBuffer::new(values.len());
68            for v in &values {
69                buf.push(*v);
70            }
71            prop_assert_eq!(buf.values(), values);
72        }
73
74        /// Sparkline length matches input (or width if subsampled)
75        #[test]
76        fn sparkline_length(
77            values in prop::collection::vec(-100.0f32..100.0, 1..100),
78            width in 1usize..50
79        ) {
80            let result = sparkline(&values, width);
81            let expected_len = values.len().min(width);
82            prop_assert_eq!(result.chars().count(), expected_len);
83        }
84
85        /// Sparkline chars are valid
86        #[test]
87        fn sparkline_valid_chars(values in prop::collection::vec(-100.0f32..100.0, 1..100)) {
88            let result = sparkline(&values, values.len());
89            for c in result.chars() {
90                prop_assert!(SPARK_CHARS.contains(&c));
91            }
92        }
93
94        /// Progress percentage is bounded [0, 100]
95        #[test]
96        fn progress_bar_bounded(current in 0usize..1000, total in 1usize..1000) {
97            let mut bar = ProgressBar::new(total, 20);
98            // Use the update method to set current
99            for _ in 0..current {
100                bar.update(current);
101            }
102            let pct = bar.percent();
103            prop_assert!(pct >= 0.0);
104            // Can exceed 100% if current > total
105            prop_assert!(pct <= 100.0 || current > total);
106        }
107
108        /// Kalman ETA is non-negative
109        #[test]
110        fn kalman_eta_nonnegative(
111            durations in prop::collection::vec(0.001f64..10.0, 1..100),
112            remaining in 0usize..1000
113        ) {
114            let mut kalman = KalmanEta::new();
115            for d in durations {
116                kalman.update(d);
117            }
118            prop_assert!(kalman.eta_seconds(remaining) >= 0.0);
119        }
120
121        /// Andon doesn't false positive on normal losses
122        #[test]
123        fn andon_no_false_positive(values in prop::collection::vec(0.0f32..100.0, 1..100)) {
124            let mut andon = AndonSystem::new().with_stop_on_critical(false);
125            for v in values {
126                andon.check_loss(v);
127            }
128            // Normal losses should not trigger critical
129            prop_assert!(!andon.has_critical());
130        }
131    }
132}