diff --git a/src/data_analysis/optimal_p_estimation.rs b/src/data_analysis/optimal_p_estimation.rs index e4edec9..aa28c41 100644 --- a/src/data_analysis/optimal_p_estimation.rs +++ b/src/data_analysis/optimal_p_estimation.rs @@ -503,27 +503,37 @@ impl OptimalPAnalysis { // Compact header - axis name and basic info output.push_str(&format!( - "{}: Td={:.1}ms (target {}, {:+.0}% dev, windows={}), Noise={}, Consistency={:.0}%\n", + "{}: Td={:.1}ms (target {}, {:+.0}% dev, windows={}), Noise={}\n", axis_name, self.td_stats.mean_ms, target_display, self.td_deviation_percent, self.td_stats.num_samples, self.noise_level.name(), - self.td_stats.consistency * 100.0 )); - // Warning for low consistency (inline) - if !self.td_stats.is_consistent() { - let cv_percent = self - .td_stats - .coefficient_of_variation - .map_or(0.0, |cv| cv * 100.0); - output.push_str(&format!( - " ⚠ Low consistency (CV={:.1}%) — unreliable (>{:.0}%)\n", - cv_percent, - TD_COEFFICIENT_OF_VARIATION_MAX * 100.0 - )); + // Reliability line — always shown with both metrics + { + let cv_str = self.td_stats.coefficient_of_variation.map_or_else( + || "CV=N/A".to_string(), + |cv| { + format!( + "CV={:.1}% (≤{:.0}%)", + cv * 100.0, + TD_COEFFICIENT_OF_VARIATION_MAX * 100.0, + ) + }, + ); + let cons_str = format!( + "Consistency={:.0}% (≥{:.0}%)", + self.td_stats.consistency * 100.0, + TD_CONSISTENCY_MIN_THRESHOLD * 100.0, + ); + if self.td_stats.is_consistent() { + output.push_str(&format!(" Reliable: {cons_str}, {cv_str}\n")); + } else { + output.push_str(&format!(" Unreliable: {cons_str}, {cv_str}\n")); + } } // Compact recommendation diff --git a/src/plot_functions/plot_step_response.rs b/src/plot_functions/plot_step_response.rs index f73cef2..b71183d 100644 --- a/src/plot_functions/plot_step_response.rs +++ b/src/plot_functions/plot_step_response.rs @@ -12,6 +12,7 @@ use crate::constants::{ FINAL_NORMALIZED_STEADY_STATE_TOLERANCE, LINE_WIDTH_PLOT, LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S, POST_AVERAGING_SMOOTHING_WINDOW, RESPONSE_LENGTH_S, STEADY_STATE_END_S, STEADY_STATE_START_S, TD_COEFFICIENT_OF_VARIATION_MAX, + TD_CONSISTENCY_MIN_THRESHOLD, }; use crate::data_analysis::calc_step_response; // For average_responses and moving_average_smooth_f64 use crate::data_analysis::optimal_p_estimation::{OptimalPAnalysis, PRecommendation}; @@ -629,28 +630,32 @@ pub fn plot_step_response( stroke_width: 0, }); - // Consistency — always shown; orange warning when poor + // Reliability — always shown with both metrics; orange when poor { - let cv_percent = analysis - .td_stats - .coefficient_of_variation - .map_or(0.0, |cv| cv * 100.0); - let consistency_pct = - (analysis.td_stats.consistency * 100.0).round() as u32; - let (cons_label, cons_color) = if !analysis.td_stats.is_consistent() { - ( + let cv_str = analysis.td_stats.coefficient_of_variation.map_or_else( + || "CV=N/A".to_string(), + |cv| { format!( - " Consistency: {}% (CV={:.1}%) — unreliable (>{:.0}%)", - consistency_pct, - cv_percent, - TD_COEFFICIENT_OF_VARIATION_MAX * 100.0 - ), - COLOR_OPTIMAL_P_WARNING, + "CV={:.1}% (≤{:.0}%)", + cv * 100.0, + TD_COEFFICIENT_OF_VARIATION_MAX * 100.0, + ) + }, + ); + let cons_str = format!( + "Consistency={:.0}% (≥{:.0}%)", + analysis.td_stats.consistency * 100.0, + TD_CONSISTENCY_MIN_THRESHOLD * 100.0, + ); + let (cons_label, cons_color) = if analysis.td_stats.is_consistent() { + ( + format!(" Reliable: {cons_str}, {cv_str}"), + COLOR_OPTIMAL_P_TEXT, ) } else { ( - format!(" Consistency: {}%", consistency_pct), - COLOR_OPTIMAL_P_TEXT, + format!(" Unreliable: {cons_str}, {cv_str}"), + COLOR_OPTIMAL_P_WARNING, ) }; series.push(PlotSeries {