From 78cd6df8efd337972a905c8628adf9f94ca5cb29 Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Fri, 22 May 2026 08:22:13 -0500 Subject: [PATCH 01/12] feat: show source provenance and unify console/PNG label format for Optimal P MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add source_events and source_files to OptimalPAnalysis, populated from AircraftProfile.file_count and AxisProfile.event_count after analysis. Console and PNG labels are now identical in content and order: - Td line: includes tolerance (±) and Noise level - Deviation: separate indented line with sign and zone name - Source: Group/Single — N flight(s), M throttle-punch(es) - Current P=N - Recommendation: unified format for all four arms (Conservative / Decrease / Optimal / Investigate) - Reliable/Unreliable: moved after Recommendation; uses ⊢≥/⊢≤ notation All lines under "Optimal P (Experimental, log-derived)" are 2-space indented. PNG no longer says "See console output for details" for Investigate arm. Co-Authored-By: Claude Sonnet 4.6 --- src/data_analysis/optimal_p_estimation.rs | 183 ++++++++++--------- src/data_analysis/torque_inertia_profiler.rs | 3 + src/main.rs | 9 +- src/plot_functions/plot_step_response.rs | 160 ++++++++-------- 4 files changed, 189 insertions(+), 166 deletions(-) diff --git a/src/data_analysis/optimal_p_estimation.rs b/src/data_analysis/optimal_p_estimation.rs index aa28c41..d7857ad 100644 --- a/src/data_analysis/optimal_p_estimation.rs +++ b/src/data_analysis/optimal_p_estimation.rs @@ -221,6 +221,10 @@ pub struct OptimalPAnalysis { pub td_target_ms: f64, /// Actual Td tolerance (in ms) used during analysis (from physics) pub td_tolerance_ms: f64, + /// Number of throttle-punch events used to derive the physics Td target. + pub source_events: usize, + /// Number of flight files that contributed to the physics Td target. + pub source_files: usize, } impl OptimalPAnalysis { @@ -318,6 +322,8 @@ impl OptimalPAnalysis { recommendation, td_target_ms, td_tolerance_ms, + source_events: 0, + source_files: 0, }) } @@ -498,118 +504,115 @@ impl OptimalPAnalysis { /// Format analysis as human-readable console output pub fn format_console_output(&self, axis_name: &str) -> String { - let target_display = format!("{:.1}±{:.1}ms", self.td_target_ms, self.td_tolerance_ms); let mut output = String::new(); - // Compact header - axis name and basic info + // Header: axis name, Td measurement, Noise output.push_str(&format!( - "{}: Td={:.1}ms (target {}, {:+.0}% dev, windows={}), Noise={}\n", + "{}: Td={:.1}ms (target: {:.1}±{:.1}ms, windows={}), Noise={}\n", axis_name, self.td_stats.mean_ms, - target_display, - self.td_deviation_percent, + self.td_target_ms, + self.td_tolerance_ms, self.td_stats.num_samples, self.noise_level.name(), )); - // 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")); - } - } + // Deviation (separate line, matching PNG) + let deviation_sign = if self.td_deviation_percent > 0.0 { + "+" + } else { + "" + }; + output.push_str(&format!( + " Deviation: {}{:.1}% ({})\n", + deviation_sign, + self.td_deviation_percent, + self.td_deviation.name(), + )); - // Compact recommendation + // Source: group/single, flights, punches + let source_label = if self.source_files > 1 { + "Group" + } else { + "Single" + }; + output.push_str(&format!( + " Source: {} — {} flight(s), {} throttle-punch(es)\n", + source_label, self.source_files, self.source_events, + )); + + // Current P output.push_str(&format!(" Current P={}\n", self.current_p)); - match &self.recommendation { - PRecommendation::Optimal { reasoning } => { - output.push_str(" → Optimal (no change recommended)\n"); - output.push_str(&format!(" {}\n", reasoning)); + // Recommendation — shared D-suffix helper + let effective_pd = self.recommended_pd_conservative.or_else(|| { + self.current_d + .filter(|&d| d > 0) + .map(|d| self.current_p as f64 / d as f64) + }); + let d_suffix = |recommended_p: u32| -> String { + if let (Some(current_d), Some(rec_pd)) = (self.current_d, effective_pd) { + if rec_pd > 0.0 && current_d > 0 { + let rec_d = (recommended_p as f64 / rec_pd).round() as u32; + let d_delta = rec_d as i32 - current_d as i32; + return format!(", D≈{} ({:+})", rec_d, d_delta); + } } - PRecommendation::Increase { - conservative_p, - reasoning, - } => { - output.push_str(" → Increase recommended:\n"); - - // Calculate P delta - let conservative_delta = *conservative_p as i32 - self.current_p as i32; + String::new() + }; - // Show P recommendation (conservative only for simplicity) + match &self.recommendation { + PRecommendation::Optimal { .. } => { output.push_str(&format!( - " Conservative: P≈{} ({:+})", - conservative_p, conservative_delta + " Recommendation: Current P is optimal (P={})\n", + self.current_p )); - - // Add D recommendation: prefer step-response P:D, fall back to current P:D. - let effective_pd = self.recommended_pd_conservative.or_else(|| { - self.current_d - .filter(|&d| d > 0) - .map(|d| self.current_p as f64 / d as f64) - }); - if let (Some(current_d), Some(rec_pd)) = (self.current_d, effective_pd) { - if rec_pd > 0.0 && current_d > 0 { - let conservative_d = ((*conservative_p as f64) / rec_pd).round() as u32; - let conservative_d_delta = conservative_d as i32 - current_d as i32; - output.push_str(&format!( - ", D≈{} ({:+})", - conservative_d, conservative_d_delta - )); - } - } - output.push('\n'); - - output.push_str(&format!(" {}\n", reasoning)); } - PRecommendation::Decrease { - recommended_p, - reasoning, - } => { - output.push_str(" → Decrease recommended:\n"); - let decrease_delta = *recommended_p as i32 - self.current_p as i32; - output.push_str(&format!(" P≈{} ({:+})", recommended_p, decrease_delta)); - - // Add D recommendation: prefer step-response P:D, fall back to current P:D. - let effective_pd = self.recommended_pd_conservative.or_else(|| { - self.current_d - .filter(|&d| d > 0) - .map(|d| self.current_p as f64 / d as f64) - }); - if let (Some(current_d), Some(rec_pd)) = (self.current_d, effective_pd) { - if rec_pd > 0.0 && current_d > 0 { - let recommended_d = ((*recommended_p as f64) / rec_pd).round() as u32; - let d_delta = recommended_d as i32 - current_d as i32; - output.push_str(&format!(", D≈{} ({:+})", recommended_d, d_delta)); - } - } - output.push('\n'); - - output.push_str(&format!(" {}\n", reasoning)); + PRecommendation::Increase { conservative_p, .. } => { + let p_delta = *conservative_p as i32 - self.current_p as i32; + output.push_str(&format!( + " Recommendation (Conservative): P≈{} ({:+}){}\n", + conservative_p, + p_delta, + d_suffix(*conservative_p), + )); + } + PRecommendation::Decrease { recommended_p, .. } => { + let p_delta = *recommended_p as i32 - self.current_p as i32; + output.push_str(&format!( + " Recommendation (Decrease): P≈{} ({:+}){}\n", + recommended_p, + p_delta, + d_suffix(*recommended_p), + )); } PRecommendation::Investigate { issue } => { - output.push_str(" → ⚠ INVESTIGATION RECOMMENDED\n"); - output.push_str(&format!(" {}\n", issue)); + output.push_str(&format!(" Recommendation: Investigate — {}\n", issue)); } } + // Reliability — always shown, after recommendation + 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")); + } + output } } diff --git a/src/data_analysis/torque_inertia_profiler.rs b/src/data_analysis/torque_inertia_profiler.rs index 175a159..8a1f502 100644 --- a/src/data_analysis/torque_inertia_profiler.rs +++ b/src/data_analysis/torque_inertia_profiler.rs @@ -113,6 +113,8 @@ impl AxisProfile { pub struct AircraftProfile { /// Per-axis profiles (Roll=0, Pitch=1, Yaw=2). pub axes: [AxisProfile; AXIS_COUNT], + /// Number of flight files that contributed to this profile. + pub file_count: usize, } impl AircraftProfile { @@ -125,6 +127,7 @@ impl AircraftProfile { AxisProfile::from_ratios(r1), AxisProfile::from_ratios(r2), ], + file_count: 0, } } diff --git a/src/main.rs b/src/main.rs index f09fb92..e5b09cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -512,7 +512,9 @@ fn profile_aircraft_group(files: &[String], debug_mode: bool) -> AircraftProfile ); } - AircraftProfile::from_axis_ratios(all_axis_ratios) + let mut profile = AircraftProfile::from_axis_ratios(all_axis_ratios); + profile.file_count = files_profiled; + profile } fn process_file( @@ -1247,7 +1249,10 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." recommended_pd_conservative[axis_index], physics_td, ) { - Ok(analysis) => { + Ok(mut analysis) => { + analysis.source_events = + aircraft_profile.axes[axis_index].event_count; + analysis.source_files = aircraft_profile.file_count; // Print console output println!("{}", analysis.format_console_output(axis_name)); // Store for PNG overlay (move instead of clone) diff --git a/src/plot_functions/plot_step_response.rs b/src/plot_functions/plot_step_response.rs index b71183d..6d7e368 100644 --- a/src/plot_functions/plot_step_response.rs +++ b/src/plot_functions/plot_step_response.rs @@ -587,24 +587,26 @@ pub fn plot_step_response( series.push(PlotSeries { data: vec![], label: "Optimal P (Experimental, log-derived)".to_string(), - color: COLOR_OPTIMAL_P_HEADER, // Blue for section header + color: COLOR_OPTIMAL_P_HEADER, stroke_width: 0, }); - // Td measurement + // Td measurement + Noise (identical to console header content) series.push(PlotSeries { data: vec![], label: format!( - " Td: {:.1}ms (target: {:.1}ms, windows={})", + " Td: {:.1}ms (target: {:.1}±{:.1}ms, windows={}), Noise={}", analysis.td_stats.mean_ms, analysis.td_target_ms, - analysis.td_stats.num_samples + analysis.td_tolerance_ms, + analysis.td_stats.num_samples, + analysis.noise_level.name(), ), color: COLOR_OPTIMAL_P_TEXT, stroke_width: 0, }); - // Deviation: only prefix '+' for strictly positive deviations + // Deviation let deviation_sign = if analysis.td_deviation_percent > 0.0 { "+" } else { @@ -616,116 +618,126 @@ pub fn plot_step_response( " Deviation: {}{:.1}% ({})", deviation_sign, analysis.td_deviation_percent, - analysis.td_deviation.name() + analysis.td_deviation.name(), ), color: COLOR_OPTIMAL_P_TEXT, stroke_width: 0, }); - // Noise level + // Source: group/single, flights, punches + let source_label = if analysis.source_files > 1 { + "Group" + } else { + "Single" + }; series.push(PlotSeries { data: vec![], - label: format!(" Noise: {}", analysis.noise_level.name()), + label: format!( + " Source: {} — {} flight(s), {} throttle-punch(es)", + source_label, analysis.source_files, analysis.source_events, + ), color: COLOR_OPTIMAL_P_TEXT, stroke_width: 0, }); - // Reliability — always shown with both metrics; orange when poor - { - let cv_str = analysis.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}%)", - 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!(" Unreliable: {cons_str}, {cv_str}"), - COLOR_OPTIMAL_P_WARNING, - ) - }; - series.push(PlotSeries { - data: vec![], - label: cons_label, - color: cons_color, - stroke_width: 0, - }); - } - // Recommendation summary - // Helper closure to compute D recommendation suffix. - // Prefers the step-response conservative P:D; falls back to the - // current P:D ratio so D is always shown when D gain is known. + // Current P + series.push(PlotSeries { + data: vec![], + label: format!(" Current P={}", analysis.current_p), + color: COLOR_OPTIMAL_P_TEXT, + stroke_width: 0, + }); + + // Recommendation — D-suffix helper let effective_pd = analysis.recommended_pd_conservative.or_else(|| { analysis .current_d .filter(|&d| d > 0) .map(|d| analysis.current_p as f64 / d as f64) }); - let append_d_recommendation = |recommended_p: u32| -> String { + let d_suffix = |recommended_p: u32| -> String { if let (Some(current_d), Some(rec_pd)) = (analysis.current_d, effective_pd) { if rec_pd > 0.0 && current_d > 0 { - let recommended_d = - ((recommended_p as f64) / rec_pd).round() as u32; - let d_delta = (recommended_d as i64) - (current_d as i64); - return format!(", D≈{} ({:+})", recommended_d, d_delta); + let rec_d = ((recommended_p as f64) / rec_pd).round() as u32; + let d_delta = (rec_d as i64) - (current_d as i64); + return format!(", D≈{} ({:+})", rec_d, d_delta); } } String::new() }; - let rec_summary = match &analysis.recommendation { + let rec_label = match &analysis.recommendation { + PRecommendation::Optimal { .. } => format!( + " Recommendation: Current P is optimal (P={})", + analysis.current_p + ), PRecommendation::Increase { conservative_p, .. } => { let p_delta = (*conservative_p as i64) - (analysis.current_p as i64); - let mut rec = format!( - " Recommendation (Conservative): P≈{} ({:+})", - conservative_p, p_delta - ); - rec.push_str(&append_d_recommendation(*conservative_p)); - rec - } - PRecommendation::Optimal { .. } => { format!( - " Recommendation: Current P is optimal (P = {})", - analysis.current_p + " Recommendation (Conservative): P≈{} ({:+}){}", + conservative_p, + p_delta, + d_suffix(*conservative_p), ) } PRecommendation::Decrease { recommended_p, .. } => { let p_delta = (*recommended_p as i64) - (analysis.current_p as i64); - let mut rec = format!( - " Recommendation: P≈{} ({:+})", - recommended_p, p_delta - ); - rec.push_str(&append_d_recommendation(*recommended_p)); - - rec + format!( + " Recommendation (Decrease): P≈{} ({:+}){}", + recommended_p, + p_delta, + d_suffix(*recommended_p), + ) } - PRecommendation::Investigate { .. } => { - " Recommendation: See console output for details".to_string() + PRecommendation::Investigate { issue } => { + format!(" Recommendation: Investigate — {}", issue) } }; series.push(PlotSeries { data: vec![], - label: rec_summary, - color: COLOR_OPTIMAL_P_RECOMMENDATION, // Green for recommendation + label: rec_label, + color: COLOR_OPTIMAL_P_RECOMMENDATION, stroke_width: 0, }); + + // Reliability — always shown, after recommendation; orange when poor + { + let cv_str = analysis.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}%)", + analysis.td_stats.consistency * 100.0, + TD_CONSISTENCY_MIN_THRESHOLD * 100.0, + ); + let (rel_label, rel_color) = if analysis.td_stats.is_consistent() { + ( + format!(" Reliable: {cons_str}, {cv_str}"), + COLOR_OPTIMAL_P_TEXT, + ) + } else { + ( + format!(" Unreliable: {cons_str}, {cv_str}"), + COLOR_OPTIMAL_P_WARNING, + ) + }; + series.push(PlotSeries { + data: vec![], + label: rel_label, + color: rel_color, + stroke_width: 0, + }); + } } else if let Some(skip_reason) = &optimal_p.skip_reasons[axis_index] { // Show skip reason if analysis failed but we have a reason series.push(PlotSeries { From a08fec0674daa38820e40a7dd8353afe3c952148 Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Fri, 22 May 2026 08:42:28 -0500 Subject: [PATCH 02/12] fix: reorder and relabel Optimal P source line to match mockup layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename 'Source:' → 'Td source:' (lowercase, clarifies it is the Td physics-target's provenance, not the step-response measurements) - Move 'Td source:' line before 'Deviation:' (matches user-specified order) - Add 'Td target: physics-derived...' preamble to PNG legend for parity with console header; parameterize 'log group' vs 'log file' by file_count - Rename Group/Single labels to 'File Group' / 'Single File' - Console header also parameterized: 'in log group' vs 'in log file' Co-Authored-By: Claude Sonnet 4.6 --- src/data_analysis/optimal_p_estimation.rs | 24 ++++++------- src/main.rs | 9 ++++- src/plot_functions/plot_step_response.rs | 44 +++++++++++++++-------- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/data_analysis/optimal_p_estimation.rs b/src/data_analysis/optimal_p_estimation.rs index d7857ad..86839d9 100644 --- a/src/data_analysis/optimal_p_estimation.rs +++ b/src/data_analysis/optimal_p_estimation.rs @@ -517,7 +517,18 @@ impl OptimalPAnalysis { self.noise_level.name(), )); - // Deviation (separate line, matching PNG) + // Td source: group/single, flights, punches + let source_label = if self.source_files > 1 { + "File Group" + } else { + "Single File" + }; + output.push_str(&format!( + " Td source: {} — {} flight(s), {} throttle-punch(es)\n", + source_label, self.source_files, self.source_events, + )); + + // Deviation let deviation_sign = if self.td_deviation_percent > 0.0 { "+" } else { @@ -530,17 +541,6 @@ impl OptimalPAnalysis { self.td_deviation.name(), )); - // Source: group/single, flights, punches - let source_label = if self.source_files > 1 { - "Group" - } else { - "Single" - }; - output.push_str(&format!( - " Source: {} — {} flight(s), {} throttle-punch(es)\n", - source_label, self.source_files, self.source_events, - )); - // Current P output.push_str(&format!(" Current P={}\n", self.current_p)); diff --git a/src/main.rs b/src/main.rs index e5b09cb..c0cf33c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1128,8 +1128,15 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." if analysis_opts.estimate_optimal_p { if let Some(sr) = sample_rate { + let group_or_file = if aircraft_profile.file_count > 1 { + "group" + } else { + "file" + }; println!("\n--- Optimal P Estimation ---"); - println!("Td target: physics-derived from throttle-punch events in log group."); + println!( + "Td target: physics-derived from throttle-punch events in log {group_or_file}." + ); println!(); for axis_index in 0..crate::axis_names::ROLL_PITCH_AXIS_COUNT { diff --git a/src/plot_functions/plot_step_response.rs b/src/plot_functions/plot_step_response.rs index 6d7e368..6ecb6c4 100644 --- a/src/plot_functions/plot_step_response.rs +++ b/src/plot_functions/plot_step_response.rs @@ -591,6 +591,22 @@ pub fn plot_step_response( stroke_width: 0, }); + // Td target preamble (matches console header) + let group_or_file_label = if analysis.source_files > 1 { + "group" + } else { + "file" + }; + series.push(PlotSeries { + data: vec![], + label: format!( + "Td target: physics-derived from throttle-punch events in log {}.", + group_or_file_label + ), + color: COLOR_OPTIMAL_P_TEXT, + stroke_width: 0, + }); + // Td measurement + Noise (identical to console header content) series.push(PlotSeries { data: vec![], @@ -606,35 +622,35 @@ pub fn plot_step_response( stroke_width: 0, }); - // Deviation - let deviation_sign = if analysis.td_deviation_percent > 0.0 { - "+" + // Td source: group/single, flights, punches + let source_label = if analysis.source_files > 1 { + "File Group" } else { - "" + "Single File" }; series.push(PlotSeries { data: vec![], label: format!( - " Deviation: {}{:.1}% ({})", - deviation_sign, - analysis.td_deviation_percent, - analysis.td_deviation.name(), + " Td source: {} — {} flight(s), {} throttle-punch(es)", + source_label, analysis.source_files, analysis.source_events, ), color: COLOR_OPTIMAL_P_TEXT, stroke_width: 0, }); - // Source: group/single, flights, punches - let source_label = if analysis.source_files > 1 { - "Group" + // Deviation + let deviation_sign = if analysis.td_deviation_percent > 0.0 { + "+" } else { - "Single" + "" }; series.push(PlotSeries { data: vec![], label: format!( - " Source: {} — {} flight(s), {} throttle-punch(es)", - source_label, analysis.source_files, analysis.source_events, + " Deviation: {}{:.1}% ({})", + deviation_sign, + analysis.td_deviation_percent, + analysis.td_deviation.name(), ), color: COLOR_OPTIMAL_P_TEXT, stroke_width: 0, From c9b341e4c95db926585eef62c050adc630a94c30 Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Fri, 22 May 2026 09:01:48 -0500 Subject: [PATCH 03/12] fix: drop (s) pluralization markers from Td source line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use plain 'flights' and 'throttle-punches' — always plural in practice. Co-Authored-By: Claude Sonnet 4.6 --- src/data_analysis/optimal_p_estimation.rs | 2 +- src/plot_functions/plot_step_response.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data_analysis/optimal_p_estimation.rs b/src/data_analysis/optimal_p_estimation.rs index 86839d9..2a32f8f 100644 --- a/src/data_analysis/optimal_p_estimation.rs +++ b/src/data_analysis/optimal_p_estimation.rs @@ -524,7 +524,7 @@ impl OptimalPAnalysis { "Single File" }; output.push_str(&format!( - " Td source: {} — {} flight(s), {} throttle-punch(es)\n", + " Td source: {} — {} flights, {} throttle-punches\n", source_label, self.source_files, self.source_events, )); diff --git a/src/plot_functions/plot_step_response.rs b/src/plot_functions/plot_step_response.rs index 6ecb6c4..375f1be 100644 --- a/src/plot_functions/plot_step_response.rs +++ b/src/plot_functions/plot_step_response.rs @@ -631,7 +631,7 @@ pub fn plot_step_response( series.push(PlotSeries { data: vec![], label: format!( - " Td source: {} — {} flight(s), {} throttle-punch(es)", + " Td source: {} — {} flights, {} throttle-punches", source_label, analysis.source_files, analysis.source_events, ), color: COLOR_OPTIMAL_P_TEXT, From aea05802215dd9e3b62bdae7bdaa8369a8c713a5 Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Fri, 22 May 2026 09:04:27 -0500 Subject: [PATCH 04/12] fix: remove redundant 'Td target: physics-derived' line from PNG legend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'Td source: File Group — N flights, M throttle-punches' already conveys the same information. The preamble line is only needed in the console section header, not repeated in every axis's PNG legend. Co-Authored-By: Claude Sonnet 4.6 --- src/plot_functions/plot_step_response.rs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/plot_functions/plot_step_response.rs b/src/plot_functions/plot_step_response.rs index 375f1be..b954a24 100644 --- a/src/plot_functions/plot_step_response.rs +++ b/src/plot_functions/plot_step_response.rs @@ -591,23 +591,7 @@ pub fn plot_step_response( stroke_width: 0, }); - // Td target preamble (matches console header) - let group_or_file_label = if analysis.source_files > 1 { - "group" - } else { - "file" - }; - series.push(PlotSeries { - data: vec![], - label: format!( - "Td target: physics-derived from throttle-punch events in log {}.", - group_or_file_label - ), - color: COLOR_OPTIMAL_P_TEXT, - stroke_width: 0, - }); - - // Td measurement + Noise (identical to console header content) + // Td measurement + Noise series.push(PlotSeries { data: vec![], label: format!( From 1ae393944a327f9b90027778e93cd3abe4150c90 Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Fri, 22 May 2026 09:08:32 -0500 Subject: [PATCH 05/12] fix: restore Noise as independent indented line in console and PNG Noise was incorrectly packed onto the Td header line. Restore it to its own ' Noise: LEVEL' line as it was in master. Co-Authored-By: Claude Sonnet 4.6 --- src/data_analysis/optimal_p_estimation.rs | 6 +++--- src/plot_functions/plot_step_response.rs | 13 ++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/data_analysis/optimal_p_estimation.rs b/src/data_analysis/optimal_p_estimation.rs index 2a32f8f..4990fe8 100644 --- a/src/data_analysis/optimal_p_estimation.rs +++ b/src/data_analysis/optimal_p_estimation.rs @@ -506,16 +506,16 @@ impl OptimalPAnalysis { pub fn format_console_output(&self, axis_name: &str) -> String { let mut output = String::new(); - // Header: axis name, Td measurement, Noise + // Header: axis name and Td measurement output.push_str(&format!( - "{}: Td={:.1}ms (target: {:.1}±{:.1}ms, windows={}), Noise={}\n", + "{}: Td={:.1}ms (target: {:.1}±{:.1}ms, windows={})\n", axis_name, self.td_stats.mean_ms, self.td_target_ms, self.td_tolerance_ms, self.td_stats.num_samples, - self.noise_level.name(), )); + output.push_str(&format!(" Noise: {}\n", self.noise_level.name())); // Td source: group/single, flights, punches let source_label = if self.source_files > 1 { diff --git a/src/plot_functions/plot_step_response.rs b/src/plot_functions/plot_step_response.rs index b954a24..629ca66 100644 --- a/src/plot_functions/plot_step_response.rs +++ b/src/plot_functions/plot_step_response.rs @@ -591,21 +591,28 @@ pub fn plot_step_response( stroke_width: 0, }); - // Td measurement + Noise + // Td measurement series.push(PlotSeries { data: vec![], label: format!( - " Td: {:.1}ms (target: {:.1}±{:.1}ms, windows={}), Noise={}", + " Td: {:.1}ms (target: {:.1}±{:.1}ms, windows={})", analysis.td_stats.mean_ms, analysis.td_target_ms, analysis.td_tolerance_ms, analysis.td_stats.num_samples, - analysis.noise_level.name(), ), color: COLOR_OPTIMAL_P_TEXT, stroke_width: 0, }); + // Noise (independent line) + series.push(PlotSeries { + data: vec![], + label: format!(" Noise: {}", analysis.noise_level.name()), + color: COLOR_OPTIMAL_P_TEXT, + stroke_width: 0, + }); + // Td source: group/single, flights, punches let source_label = if analysis.source_files > 1 { "File Group" From 61cb7018c7a8aba5b1757a7f61f2cbecafad929b Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Fri, 22 May 2026 09:11:51 -0500 Subject: [PATCH 06/12] fix: move Noise line after Td source in console and PNG Noise is a per-flight measurement; placing it after Td source groups context lines (source + noise) before result lines (deviation, recommendation). Co-Authored-By: Claude Sonnet 4.6 --- src/data_analysis/optimal_p_estimation.rs | 3 +-- src/plot_functions/plot_step_response.rs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/data_analysis/optimal_p_estimation.rs b/src/data_analysis/optimal_p_estimation.rs index 4990fe8..b496559 100644 --- a/src/data_analysis/optimal_p_estimation.rs +++ b/src/data_analysis/optimal_p_estimation.rs @@ -515,8 +515,6 @@ impl OptimalPAnalysis { self.td_tolerance_ms, self.td_stats.num_samples, )); - output.push_str(&format!(" Noise: {}\n", self.noise_level.name())); - // Td source: group/single, flights, punches let source_label = if self.source_files > 1 { "File Group" @@ -527,6 +525,7 @@ impl OptimalPAnalysis { " Td source: {} — {} flights, {} throttle-punches\n", source_label, self.source_files, self.source_events, )); + output.push_str(&format!(" Noise: {}\n", self.noise_level.name())); // Deviation let deviation_sign = if self.td_deviation_percent > 0.0 { diff --git a/src/plot_functions/plot_step_response.rs b/src/plot_functions/plot_step_response.rs index 629ca66..fed0e29 100644 --- a/src/plot_functions/plot_step_response.rs +++ b/src/plot_functions/plot_step_response.rs @@ -605,14 +605,6 @@ pub fn plot_step_response( stroke_width: 0, }); - // Noise (independent line) - series.push(PlotSeries { - data: vec![], - label: format!(" Noise: {}", analysis.noise_level.name()), - color: COLOR_OPTIMAL_P_TEXT, - stroke_width: 0, - }); - // Td source: group/single, flights, punches let source_label = if analysis.source_files > 1 { "File Group" @@ -629,6 +621,14 @@ pub fn plot_step_response( stroke_width: 0, }); + // Noise + series.push(PlotSeries { + data: vec![], + label: format!(" Noise: {}", analysis.noise_level.name()), + color: COLOR_OPTIMAL_P_TEXT, + stroke_width: 0, + }); + // Deviation let deviation_sign = if analysis.td_deviation_percent > 0.0 { "+" From 6fa1e62cc3a7a653dcf674e7520cb4f5ab5a4f7c Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Fri, 22 May 2026 09:16:46 -0500 Subject: [PATCH 07/12] docs: update OVERVIEW.md Optimal P output description for source-info changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Expand Output section into a structured list showing each label line (Td, Td source, Noise, Deviation, Current P, Recommendation, Reliable/Unreliable) with descriptions - Update Reliability section: 'CV warning' → 'Unreliable:' label, 'CV = None' → 'CV = N/A', note that Consistent/CV are independent of source flight/punch count - Add Td source and Reliable/Unreliable to the dependability signals summary Co-Authored-By: Claude Sonnet 4.6 --- OVERVIEW.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/OVERVIEW.md b/OVERVIEW.md index d34bb20..9cafcf0 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -233,22 +233,32 @@ Physics-derived P gain optimization using a Torque-Inertia Profiler that measure - **P Decrease:** Td faster than target with high noise → P is too high (rare) - **Investigate:** Measurements suggest mechanical issues or abnormal dynamics -- **Output:** Console report and PNG legend overlay showing: Td mean with `windows=` count (valid step-response windows contributing to the Td mean), target, deviation %, noise level, consistency % (orange warning when CV exceeds `TD_COEFFICIENT_OF_VARIATION_MAX`), `[LOW AUTHORITY]` warning when max setpoint is below `LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S`, and P recommendation with calculated D adjustment. When profiling is skipped (insufficient punch events), a skip reason appears in both outputs. See **Consistency and Reliability Interpretation** below for signal details. +- **Output:** Console and PNG legend overlay (identical content) per axis: + - `Td:` — measured Td mean with target `±tolerance` and `windows=` count + - `Td source:` — `File Group` (multi-file run) or `Single File`, with flight count and throttle-punch count that produced the physics target + - `Noise:` — HF D-term energy level (`LOW` / `MODERATE` / `HIGH` / `UNKNOWN`) for the current flight + - `Deviation:` — % difference between measured Td and physics target, with zone label + - `Current P=` — P gain value from the flight's metadata + - `Recommendation` — one of `(Conservative)`, `(Decrease)`, `Current P is optimal`, or `Investigate —` with reason; includes calculated D adjustment + - `Reliable:` / `Unreliable:` — always shows both `Consistency=N% (⊢≥70%)` and `CV=N% (⊢≤40%)`; `Unreliable` is highlighted in orange when either threshold is not met + - `[LOW AUTHORITY]` warning (console) when max setpoint is below `LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S` + - When profiling is skipped (insufficient punch events), a skip reason replaces the above. See **Consistency and Reliability Interpretation** below. - **Consistency and Reliability Interpretation (CV):** - **CV (Coefficient of Variation)** = standard deviation / mean of individual Td measurements across all valid step-response windows. It quantifies how scattered the measurements are relative to their average. - **Low CV:** Td measurements are tightly clustered — the log contains clean, repeatable dynamics and recommendations are trustworthy. - - **High CV (exceeds `TD_COEFFICIENT_OF_VARIATION_MAX`):** Td measurements vary widely across windows — a consistency warning is shown in both console and PNG. Recommendations should be treated with caution. - - **CV = None:** Fewer than `TD_SAMPLES_MIN_FOR_STDDEV` valid Td samples were available; standard deviation cannot be computed. The mean is still reported but no consistency rating is given. + - **High CV (exceeds `TD_COEFFICIENT_OF_VARIATION_MAX`):** Td measurements vary widely across windows — `Unreliable:` is shown (orange) in both console and PNG. Recommendations should be treated with caution. + - **CV = N/A:** Fewer than `TD_SAMPLES_MIN_FOR_STDDEV` valid Td samples were available; standard deviation cannot be computed. The mean is still reported and `CV=N/A` appears in the reliability line. - **Low-authority flight warning:** When the maximum setpoint across all valid windows is below `LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S`, a `[LOW AUTHORITY]` warning is shown in both console and PNG. Hover tests and slow-cruise logs never produce sharp inputs — step-response analysis and Td measurements from such logs are noise-dominated rather than dynamics-dominated, making all recommendations unreliable regardless of CV. - **Why hover logs produce high CV:** Small setpoint inputs → deconvolution is noise-sensitive → each window captures a different noise realisation. The averaged response may appear plausible (noise averages out) while individual window variance remains high. CV exposes this where the mean alone cannot. - **Over-P limitation:** **When P is already too high and the aircraft oscillates, the profiler may report "Optimal" rather than "Decrease P."** An oscillatory step response produces a short, aggressive measured Td — which, fed into the physics formula, yields a P_optimal close to the current (excessive) P. The profiler cannot reliably distinguish a well-tuned fast response from an over-tuned oscillating one using Td alone. The indirect signal is CV: severe oscillation typically scatters Td samples widely and triggers the consistency warning. **If your gains feel high or the craft exhibits oscillation, start from a lower P before relying on these recommendations.** Optimal P estimation is most accurate when the craft is in a reasonable tuning range — it is a validator and refinement tool, not a recovery tool for badly mis-tuned aircraft. Only experienced pilots are likely to recognise this situation by feel. - **High CV without `[LOW AUTHORITY]`:** If the consistency warning fires but `[LOW AUTHORITY]` is not shown, the scatter is not caused by low-energy hover inputs. Remaining causes include propwash, inconsistent maneuvers, and oscillation from over-P. **If gains feel high, treat this combination as a prompt to verify the craft is not oscillating before acting on any recommendation.** - **Summary of dependability signals in output:** - `windows=` on the Td line — number of valid step-response windows contributing to the Td mean; more windows = more statistical weight - - Consistency % and CV — how repeatable individual measurements are + - `Td source:` — flight and throttle-punch counts that calibrated the physics target; `File Group` means data was pooled across multiple logs + - `Reliable:` / `Unreliable:` with `Consistency %` and `CV` — how repeatable the per-flight step-response measurements are (independent of how many punches fed the physics target) - `[LOW AUTHORITY]` — max setpoint too small for reliable step-response characterisation - - Noise level (`LOW` / `MODERATE` / `HIGH`) — HF D-term energy; high noise limits safe P increase + - Noise level (`LOW` / `MODERATE` / `HIGH`) — HF D-term energy for the current flight; high noise limits safe P increase - **Relationship to P:D Recommendations:** - P:D ratio recommendations: analyze peak overshoot → adjust D relative to P From 78a9cdb9cec509732bf03cd94a1776fcf1aee392 Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Fri, 22 May 2026 09:46:43 -0500 Subject: [PATCH 08/12] fix: gate Moderate/Aggressive on LOW AUTHORITY; add label to Optimal P PNG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Console base P:D: compute is_low_authority from valid_window_max_setpoints (already available in scope); print [LOW AUTHORITY] warning; replace Moderate and Aggressive prints with 'Skipped [LOW AUTHORITY]' - PNG base P:D: gate Moderate block behind !is_low_authority; show 'Recommendation (moderate): Skipped [LOW AUTHORITY]' otherwise (Aggressive is nested inside Moderate so is implicitly gated too) - PNG Optimal P section: add [LOW AUTHORITY] label after section header when is_low_authority fires Conservative is retained in all cases — a small incremental step is safe regardless of measurement precision. Only flights with max setpoint below LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S (100 dps) are affected. Closes #152 Co-Authored-By: Claude Sonnet 4.6 --- src/main.rs | 24 +++++++++++++++++++++--- src/plot_functions/plot_step_response.rs | 24 ++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index c0cf33c..e52cf60 100644 --- a/src/main.rs +++ b/src/main.rs @@ -791,7 +791,7 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." // Only Roll (0) and Pitch (1) let axis_name = crate::axis_names::AXIS_NAMES[axis_index]; - if let Some((response_time, valid_stacked_responses, _valid_window_max_setpoints)) = + if let Some((response_time, valid_stacked_responses, valid_window_max_setpoints)) = &step_response_calculation_results[axis_index] { if valid_stacked_responses.shape()[0] > 0 && !response_time.is_empty() { @@ -947,6 +947,20 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." // Needed in both branches below let dmax_enabled = pid_metadata.is_dmax_enabled(); + // Low-authority check from step-response window setpoints + let max_sp_base = valid_window_max_setpoints + .iter() + .cloned() + .fold(0.0_f32, f32::max); + let is_low_authority_base = max_sp_base + < crate::constants::LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S; + if is_low_authority_base { + println!( + " [LOW AUTHORITY] max={:.0}dps — moderate/aggressive recommendations skipped", + max_sp_base + ); + } + // Show recommendations if they were computed (threshold exceeded) if recommended_pd_conservative[axis_index].is_some() { // Check for extreme overshoot (may indicate deeper issues) @@ -1006,7 +1020,9 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." } // Show secondary (moderate) recommendation - if dmax_enabled + if is_low_authority_base { + println!(" Recommendation (moderate): Skipped [LOW AUTHORITY]"); + } else if dmax_enabled && (recommended_d_min_aggressive[axis_index].is_some() || recommended_d_max_aggressive[axis_index] .is_some()) @@ -1034,7 +1050,9 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." } // Show tertiary (aggressive) recommendation for significant overshoot only - if assessment == "Significant overshoot" { + if !is_low_authority_base + && assessment == "Significant overshoot" + { let aggressive_pd = current_pd_ratio * crate::constants::PD_RATIO_AGGRESSIVE_MULTIPLIER; let (rec_d_agg, rec_d_min_agg, rec_d_max_agg) = diff --git a/src/plot_functions/plot_step_response.rs b/src/plot_functions/plot_step_response.rs index fed0e29..4e61aec 100644 --- a/src/plot_functions/plot_step_response.rs +++ b/src/plot_functions/plot_step_response.rs @@ -509,8 +509,15 @@ pub fn plot_step_response( }); } - // Moderate recommendation - if let Some(rec_pd) = moderate.0.pd_ratios[axis_index] { + // Moderate recommendation — suppressed for low-authority flights + if is_low_authority { + series.push(PlotSeries { + data: vec![], + label: "Recommendation (moderate): Skipped [LOW AUTHORITY]".to_string(), + color: COLOR_OPTIMAL_P_WARNING, + stroke_width: 0, + }); + } else if let Some(rec_pd) = moderate.0.pd_ratios[axis_index] { let recommendation_label = if dmax_enabled { // D-Min/D-Max enabled: show D-Min and D-Max, NOT base D let d_min_str = moderate.0.d_min_values[axis_index] @@ -591,6 +598,19 @@ pub fn plot_step_response( stroke_width: 0, }); + // Low-authority warning in Optimal P section + if is_low_authority { + series.push(PlotSeries { + data: vec![], + label: format!( + "[LOW AUTHORITY] max={:.0}dps — measurement unreliable", + max_sp + ), + color: COLOR_OPTIMAL_P_WARNING, + stroke_width: 0, + }); + } + // Td measurement series.push(PlotSeries { data: vec![], From e8bb921af2f32cc6aafd98398d179d2164128e9c Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Fri, 22 May 2026 11:04:11 -0500 Subject: [PATCH 09/12] feat: replace binary LOW AUTHORITY with Setpoint Authority classification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce SetpointAuthority enum (Low/Moderate/High) and compute_setpoint_authority() in calc_step_response.rs, using the **mean** of per-window max setpoints rather than max, so a single outlier input no longer masks a predominantly hover-style flight. Changes: - constants.rs: add HIGH_AUTHORITY_SETPOINT_THRESHOLD_DEG_S (250 dps), update LOW_AUTHORITY comment to describe all three tiers - calc_step_response.rs: add SetpointAuthority enum with name()/is_low() and compute_setpoint_authority() returning Option<(level, mean)> - main.rs / plot_step_response.rs: replace is_low_authority binary check with compute_setpoint_authority(); always show all recommendations (no more "Skipped — LOW AUTHORITY"); emit always-visible "Setpoint Authority: LEVEL (mean=Xdps ⊢≥100dps)" line (orange for LOW) PNG legend refinements: - "step-response" → "Step-Response Avg", "Peak:" → "Smoothed Peak:" on curve labels; "Peak=" → "Actual Peak=" on Current P:D line - ─────────────────────── separator (consistent with Optimal P) between curve entries and P:D section; 2-space indent on all lines below Current P:D Console alignment with PNG: - "Peak=" → "Actual Peak="; recommendation format uses () not →; (P=N) suffix removed from all recommendation lines; blank line added after axis header Co-Authored-By: Claude Sonnet 4.6 --- src/constants.rs | 8 +- src/data_analysis/calc_step_response.rs | 46 +++++++++- src/main.rs | 87 ++++++++----------- src/plot_functions/plot_step_response.rs | 105 ++++++++++++----------- 4 files changed, 139 insertions(+), 107 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index e0ad3bd..4f2cdba 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -49,10 +49,12 @@ pub const DEFAULT_SETPOINT_THRESHOLD: f64 = 500.0; // Default setpoint threshold // Constants for filtering data based on movement and flight phase. pub const MOVEMENT_THRESHOLD_DEG_S: f64 = 20.0; // Minimum setpoint/gyro magnitude (from PTB/PlasmaTree) -/// Max setpoint (deg/s) below which a flight is considered low-authority. -/// Hover tests and slow-cruise logs never produce sharp inputs, so step-response -/// quality and P:D/optimal-P recommendations from such logs are unreliable. +/// Mean window-max setpoint (deg/s) thresholds for Setpoint Authority classification. +/// LOW < 100 dps : hover/slow-cruise — Moderate/Aggressive recommendations suppressed. +/// MODERATE 100–250 dps : normal flight inputs. +/// HIGH > 250 dps : aggressive/race inputs. pub const LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S: f32 = 100.0; +pub const HIGH_AUTHORITY_SETPOINT_THRESHOLD_DEG_S: f32 = 250.0; pub const EXCLUDE_START_S: f64 = 3.0; // Exclude seconds from the start of the log pub const EXCLUDE_END_S: f64 = 3.0; // Exclude seconds from the end of the log diff --git a/src/data_analysis/calc_step_response.rs b/src/data_analysis/calc_step_response.rs index 49f31e6..9c9ff40 100644 --- a/src/data_analysis/calc_step_response.rs +++ b/src/data_analysis/calc_step_response.rs @@ -8,13 +8,57 @@ use crate::types::StepResponseResult; use crate::constants::{ APPLY_INDIVIDUAL_RESPONSE_Y_CORRECTION, ENABLE_NORMALIZED_STEADY_STATE_MEAN_CHECK, - FRAME_LENGTH_S, INITIAL_GYRO_SMOOTHING_WINDOW, MOVEMENT_THRESHOLD_DEG_S, + FRAME_LENGTH_S, HIGH_AUTHORITY_SETPOINT_THRESHOLD_DEG_S, INITIAL_GYRO_SMOOTHING_WINDOW, + LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S, MOVEMENT_THRESHOLD_DEG_S, NORMALIZED_STEADY_STATE_MAX_VAL, NORMALIZED_STEADY_STATE_MEAN_MAX, NORMALIZED_STEADY_STATE_MEAN_MIN, NORMALIZED_STEADY_STATE_MIN_VAL, RESPONSE_LENGTH_S, STEADY_STATE_END_S, STEADY_STATE_START_S, SUPERPOSITION_FACTOR, TUKEY_ALPHA, Y_CORRECTION_MIN_UNNORMALIZED_MEAN_ABS, }; +/// Setpoint authority classification derived from the mean of per-window max setpoints. +/// Describes how aggressively the pilot flew during the step-response windows. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SetpointAuthority { + Low, // mean < LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S + Moderate, // LOW..HIGH + High, // >= HIGH_AUTHORITY_SETPOINT_THRESHOLD_DEG_S +} + +impl SetpointAuthority { + pub fn name(&self) -> &'static str { + match self { + Self::Low => "LOW", + Self::Moderate => "MODERATE", + Self::High => "HIGH", + } + } + + pub fn is_low(&self) -> bool { + matches!(self, Self::Low) + } +} + +/// Compute SetpointAuthority and mean window-max setpoint from QC-passed window data. +/// Returns None if the slice is empty. +pub fn compute_setpoint_authority( + valid_window_max_setpoints: &[f32], +) -> Option<(SetpointAuthority, f32)> { + if valid_window_max_setpoints.is_empty() { + return None; + } + let mean = + valid_window_max_setpoints.iter().sum::() / valid_window_max_setpoints.len() as f32; + let level = if mean < LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S { + SetpointAuthority::Low + } else if mean < HIGH_AUTHORITY_SETPOINT_THRESHOLD_DEG_S { + SetpointAuthority::Moderate + } else { + SetpointAuthority::High + }; + Some((level, mean)) +} + use crate::data_analysis::fft_utils; // tukeywin, winstacker_contiguous, wiener_deconvolution_window, cumulative_sum, diff --git a/src/main.rs b/src/main.rs index e52cf60..4fb336c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -159,6 +159,7 @@ use crate::pid_context::PidContext; // Data analysis imports use crate::data_analysis::calc_step_response; +use crate::data_analysis::calc_step_response::{compute_setpoint_authority, SetpointAuthority}; /// Expand input paths to a list of CSV files. /// If a path is a file, validate CSV extension before adding. @@ -933,7 +934,8 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." } } - println!("{axis_name}: Peak={peak_value:.3} → {assessment}"); + println!("{axis_name}: Actual Peak={peak_value:.3} → {assessment}"); + println!(); // Always show current P:D ratio with quality assessment let axis_pid = if axis_index == 0 { @@ -942,24 +944,22 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." &pid_metadata.pitch }; - if let Some(p_val) = axis_pid.p { + if axis_pid.p.is_some() { println!(" Current P:D={current_pd_ratio:.2}"); // Needed in both branches below let dmax_enabled = pid_metadata.is_dmax_enabled(); - // Low-authority check from step-response window setpoints - let max_sp_base = valid_window_max_setpoints - .iter() - .cloned() - .fold(0.0_f32, f32::max); - let is_low_authority_base = max_sp_base - < crate::constants::LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S; - if is_low_authority_base { - println!( - " [LOW AUTHORITY] max={:.0}dps — moderate/aggressive recommendations skipped", - max_sp_base - ); - } + // Setpoint Authority from mean of per-window max setpoints + let (authority, authority_mean) = compute_setpoint_authority( + valid_window_max_setpoints.as_slice().unwrap_or(&[]), + ) + .unwrap_or((SetpointAuthority::Low, 0.0)); + println!( + " Setpoint Authority: {} (mean={:.0}dps \u{22a2}\u{2265}{}dps)", + authority.name(), + authority_mean, + crate::constants::LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S as u32 + ); // Show recommendations if they were computed (threshold exceeded) if recommended_pd_conservative[axis_index].is_some() { @@ -1004,25 +1004,22 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." let d_max_str = recommended_d_max_conservative [axis_index] .map_or("N/A".to_string(), |v| v.to_string()); - println!(" Recommendation (conservative): P:D={:.2} → D-Min≈{}, D-Max≈{} (P={})", + println!(" Recommendation (conservative): P:D={:.2} (D-Min≈{}, D-Max≈{})", recommended_pd_conservative[axis_index].unwrap(), - d_min_str, d_max_str, p_val); + d_min_str, d_max_str); } else if let Some(recommended_d) = recommended_d_conservative[axis_index] { // D-Min/D-Max disabled: show only base D println!( - " Recommendation (conservative): P:D={:.2} → D≈{} (P={})", + " Recommendation (conservative): P:D={:.2} (D≈{})", recommended_pd_conservative[axis_index].unwrap(), - recommended_d, - p_val + recommended_d ); } // Show secondary (moderate) recommendation - if is_low_authority_base { - println!(" Recommendation (moderate): Skipped [LOW AUTHORITY]"); - } else if dmax_enabled + if dmax_enabled && (recommended_d_min_aggressive[axis_index].is_some() || recommended_d_max_aggressive[axis_index] .is_some()) @@ -1034,25 +1031,22 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." let d_max_str = recommended_d_max_aggressive [axis_index] .map_or("N/A".to_string(), |v| v.to_string()); - println!(" Recommendation (moderate): P:D={:.2} → D-Min≈{}, D-Max≈{} (P={})", + println!(" Recommendation (moderate): P:D={:.2} (D-Min≈{}, D-Max≈{})", recommended_pd_aggressive[axis_index].unwrap(), - d_min_str, d_max_str, p_val); + d_min_str, d_max_str); } else if let Some(recommended_d_mod) = recommended_d_aggressive[axis_index] { // D-Min/D-Max disabled: show only base D println!( - " Recommendation (moderate): P:D={:.2} → D≈{} (P={})", + " Recommendation (moderate): P:D={:.2} (D≈{})", recommended_pd_aggressive[axis_index].unwrap(), - recommended_d_mod, - p_val + recommended_d_mod ); } // Show tertiary (aggressive) recommendation for significant overshoot only - if !is_low_authority_base - && assessment == "Significant overshoot" - { + if assessment == "Significant overshoot" { let aggressive_pd = current_pd_ratio * crate::constants::PD_RATIO_AGGRESSIVE_MULTIPLIER; let (rec_d_agg, rec_d_min_agg, rec_d_max_agg) = @@ -1075,11 +1069,11 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." .map_or("N/A".to_string(), |v| v.to_string()); let d_max_str = rec_d_max_agg .map_or("N/A".to_string(), |v| v.to_string()); - println!(" Recommendation (aggressive): P:D={:.2} → D-Min≈{}, D-Max≈{} (P={})", - aggressive_pd, d_min_str, d_max_str, p_val); + println!(" Recommendation (aggressive): P:D={:.2} (D-Min≈{}, D-Max≈{})", + aggressive_pd, d_min_str, d_max_str); } else if let Some(rec_d3) = rec_d_agg { - println!(" Recommendation (aggressive): P:D={:.2} → D≈{} (P={})", - aggressive_pd, rec_d3, p_val); + println!(" Recommendation (aggressive): P:D={:.2} (D≈{})", + aggressive_pd, rec_d3); } } } else if assessment == "Near optimal" { @@ -1104,16 +1098,15 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." }) .map_or("N/A".to_string(), |v| v.to_string()); println!( - " Recommendation (conservative): D-Min≈{}, D-Max≈{} (P={}) [optional D−1]", - d_min_str, d_max_str, p_val + " Recommendation (conservative): D-Min≈{}, D-Max≈{} [optional D−1]", + d_min_str, d_max_str ); } else if let Some(current_d) = axis_pid.d { println!( - " Recommendation (conservative): D≈{} (P={}) [optional D−1]", + " Recommendation (conservative): D≈{} [optional D−1]", current_d.saturating_sub( crate::constants::D_STEP_OPTIONAL - ), - p_val + ) ); } } else { @@ -1161,22 +1154,10 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." // Only Roll (0) and Pitch (1) - Yaw excluded by ROLL_PITCH_AXIS_COUNT let axis_name = crate::axis_names::AXIS_NAMES[axis_index]; - if let Some((response_time, valid_stacked_responses, valid_window_max_setpoints)) = + if let Some((response_time, valid_stacked_responses, _valid_window_max_setpoints)) = &step_response_calculation_results[axis_index] { if valid_stacked_responses.shape()[0] > 0 && !response_time.is_empty() { - // Warn when inputs are too gentle to produce reliable step-response data - let max_sp = valid_window_max_setpoints - .iter() - .cloned() - .fold(0.0_f32, f32::max); - if max_sp < crate::constants::LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S { - println!( - " ⚠ {axis_name}: [LOW AUTHORITY] max={:.0}dps — recommendations unreliable", - max_sp - ); - } - // Collect individual Td samples from each valid response window let mut td_samples_ms: Vec = Vec::new(); diff --git a/src/plot_functions/plot_step_response.rs b/src/plot_functions/plot_step_response.rs index 4e61aec..abeae1b 100644 --- a/src/plot_functions/plot_step_response.rs +++ b/src/plot_functions/plot_step_response.rs @@ -15,6 +15,7 @@ use crate::constants::{ TD_CONSISTENCY_MIN_THRESHOLD, }; use crate::data_analysis::calc_step_response; // For average_responses and moving_average_smooth_f64 +use crate::data_analysis::calc_step_response::{compute_setpoint_authority, SetpointAuthority}; use crate::data_analysis::optimal_p_estimation::{OptimalPAnalysis, PRecommendation}; use crate::data_input::pid_metadata::PidMetadata; use crate::plot_framework::{draw_stacked_plot, PlotSeries}; @@ -305,7 +306,7 @@ pub fn plot_step_response( .map(|(&t, &v)| (t, v)) .collect(), label: format!( - "< {} deg/s (Peak: {peak_str}, Td: {latency_str})", + "< {} deg/s (Smoothed Peak: {peak_str}, Td: {latency_str})", display.setpoint_threshold ), color: color_low_sp, @@ -328,7 +329,7 @@ pub fn plot_step_response( .map(|(&t, &v)| (t, v)) .collect(), label: format!( - "\u{2265} {} deg/s (Peak: {peak_str}, Td: {latency_str})", + "\u{2265} {} deg/s (Smoothed Peak: {peak_str}, Td: {latency_str})", display.setpoint_threshold ), color: color_high_sp, @@ -350,7 +351,7 @@ pub fn plot_step_response( .zip(resp.iter()) .map(|(&t, &v)| (t, v)) .collect(), - label: format!("Combined (Peak: {peak_str}, Td: {latency_str})"), // This is the average of all Y-corrected & QC'd responses + label: format!("Combined (Smoothed Peak: {peak_str}, Td: {latency_str})"), // average of all Y-corrected & QC'd responses, post-smoothed color: color_combined, stroke_width: line_stroke_plot, }); @@ -372,7 +373,9 @@ pub fn plot_step_response( .zip(resp.iter()) .map(|(&t, &v)| (t, v)) .collect(), - label: format!("step-response (Peak: {peak_str}, Td: {latency_str})"), // This is the average of all Y-corrected & QC'd responses + label: format!( + "Step-Response Avg (Smoothed Peak: {peak_str}, Td: {latency_str})" + ), // average of all Y-corrected & QC'd responses, post-smoothed color: color_combined, stroke_width: line_stroke_plot, }); @@ -396,19 +399,27 @@ pub fn plot_step_response( // Add current P:D ratio with quality assessment as legend entries for Roll/Pitch if axis_index < 2 { - // Low-authority flight check: max setpoint across all valid windows. - let max_sp = valid_window_max_setpoints - .iter() - .cloned() - .fold(0.0_f32, f32::max); - let is_low_authority = max_sp < LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S; + // Setpoint Authority from mean of per-window max setpoints. + let (authority, authority_mean) = compute_setpoint_authority( + valid_window_max_setpoints.as_slice().unwrap_or(&[]), + ) + .unwrap_or((SetpointAuthority::Low, 0.0)); + let is_low_authority = authority.is_low(); + + // Separator between curve legend entries and P:D section + series.push(PlotSeries { + data: vec![], + label: "─────────────────────".to_string(), + color: COLOR_OPTIMAL_P_DIVIDER, + stroke_width: 0, + }); // Current P:D ratio and assessment if let Some(current_pd) = current.pd_ratios[axis_index] { let current_label = if let Some(assessment) = current.assessments[axis_index] { if let Some(peak) = current.peak_values[axis_index] { format!( - "Current P:D={:.2} (Peak={:.2}, {})", + "Current P:D={:.2} (Actual Peak={:.2}, {})", current_pd, peak, assessment ) } else { @@ -425,14 +436,22 @@ pub fn plot_step_response( }); } - if is_low_authority { + // Setpoint Authority line — always shown, orange for LOW + { + let auth_color = if is_low_authority { + COLOR_OPTIMAL_P_WARNING + } else { + RGBColor(80, 80, 80) + }; series.push(PlotSeries { data: vec![], label: format!( - "[LOW AUTHORITY] max={:.0}dps — recommendations unreliable", - max_sp + " Setpoint Authority: {} (mean={:.0}dps \u{22a2}\u{2265}{}dps)", + authority.name(), + authority_mean, + LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S as u32 ), - color: COLOR_OPTIMAL_P_WARNING, // Orange warning + color: auth_color, stroke_width: 0, }); } @@ -446,17 +465,17 @@ pub fn plot_step_response( let d_max_str = conservative.0.d_max_values[axis_index] .map_or("N/A".to_string(), |v| v.to_string()); format!( - "Recommendation (conservative): P:D={:.2} (D-Min≈{}, D-Max≈{})", + " Recommendation (conservative): P:D={:.2} (D-Min≈{}, D-Max≈{})", rec_pd, d_min_str, d_max_str ) } else if let Some(rec_d) = conservative.0.d_values[axis_index] { // D-Min/D-Max disabled: show only base D format!( - "Recommendation (conservative): P:D={:.2} (D≈{})", + " Recommendation (conservative): P:D={:.2} (D≈{})", rec_pd, rec_d ) } else { - format!("Recommendation (conservative): P:D={:.2}", rec_pd) + format!(" Recommendation (conservative): P:D={:.2}", rec_pd) }; series.push(PlotSeries { data: vec![], @@ -479,16 +498,16 @@ pub fn plot_step_response( .map(|v| v.saturating_sub(crate::constants::D_STEP_OPTIONAL)) .map_or("N/A".to_string(), |v| v.to_string()); format!( - "Recommendation (conservative): D-Min≈{}, D-Max≈{} [optional D−1]", + " Recommendation (conservative): D-Min≈{}, D-Max≈{} [optional D−1]", d_min_str, d_max_str ) } else if let Some(current_d) = axis_pid_data.d { format!( - "Recommendation (conservative): D≈{} [optional D−1]", + " Recommendation (conservative): D≈{} [optional D−1]", current_d.saturating_sub(crate::constants::D_STEP_OPTIONAL) ) } else { - "Recommendation (none): No obvious tuning adjustments needed" + " Recommendation (none): No obvious tuning adjustments needed" .to_string() }; series.push(PlotSeries { @@ -502,22 +521,15 @@ pub fn plot_step_response( // Optimal zone (1.02–1.08): no adjustment needed series.push(PlotSeries { data: vec![], - label: "Recommendation (none): No obvious tuning adjustments needed" + label: " Recommendation (none): No obvious tuning adjustments needed" .to_string(), color: RGBColor(100, 100, 100), stroke_width: 0, }); } - // Moderate recommendation — suppressed for low-authority flights - if is_low_authority { - series.push(PlotSeries { - data: vec![], - label: "Recommendation (moderate): Skipped [LOW AUTHORITY]".to_string(), - color: COLOR_OPTIMAL_P_WARNING, - stroke_width: 0, - }); - } else if let Some(rec_pd) = moderate.0.pd_ratios[axis_index] { + // Moderate recommendation + if let Some(rec_pd) = moderate.0.pd_ratios[axis_index] { let recommendation_label = if dmax_enabled { // D-Min/D-Max enabled: show D-Min and D-Max, NOT base D let d_min_str = moderate.0.d_min_values[axis_index] @@ -525,14 +537,17 @@ pub fn plot_step_response( let d_max_str = moderate.0.d_max_values[axis_index] .map_or("N/A".to_string(), |v| v.to_string()); format!( - "Recommendation (moderate): P:D={:.2} (D-Min≈{}, D-Max≈{})", + " Recommendation (moderate): P:D={:.2} (D-Min≈{}, D-Max≈{})", rec_pd, d_min_str, d_max_str ) } else if let Some(rec_d) = moderate.0.d_values[axis_index] { // D-Min/D-Max disabled: show only base D - format!("Recommendation (moderate): P:D={:.2} (D≈{})", rec_pd, rec_d) + format!( + " Recommendation (moderate): P:D={:.2} (D≈{})", + rec_pd, rec_d + ) } else { - format!("Recommendation (moderate): P:D={:.2}", rec_pd) + format!(" Recommendation (moderate): P:D={:.2}", rec_pd) }; series.push(PlotSeries { data: vec![], @@ -557,16 +572,19 @@ pub fn plot_step_response( let d_max_str = rec_d_max_agg.map_or("N/A".to_string(), |v| v.to_string()); format!( - "Recommendation (aggressive): P:D={:.2} (D-Min≈{}, D-Max≈{})", + " Recommendation (aggressive): P:D={:.2} (D-Min≈{}, D-Max≈{})", aggressive_pd, d_min_str, d_max_str ) } else if let Some(rec_d3) = rec_d_agg { format!( - "Recommendation (aggressive): P:D={:.2} (D≈{})", + " Recommendation (aggressive): P:D={:.2} (D≈{})", aggressive_pd, rec_d3 ) } else { - format!("Recommendation (aggressive): P:D={:.2}", aggressive_pd) + format!( + " Recommendation (aggressive): P:D={:.2}", + aggressive_pd + ) }; series.push(PlotSeries { data: vec![], @@ -598,19 +616,6 @@ pub fn plot_step_response( stroke_width: 0, }); - // Low-authority warning in Optimal P section - if is_low_authority { - series.push(PlotSeries { - data: vec![], - label: format!( - "[LOW AUTHORITY] max={:.0}dps — measurement unreliable", - max_sp - ), - color: COLOR_OPTIMAL_P_WARNING, - stroke_width: 0, - }); - } - // Td measurement series.push(PlotSeries { data: vec![], From 7068aafa1a18144151b77ceb93aff74f17e757e8 Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Fri, 22 May 2026 11:07:22 -0500 Subject: [PATCH 10/12] fix: replace magic RGBColor with COLOR_OPTIMAL_P_TEXT; update OVERVIEW.md - plot_step_response.rs: replace RGBColor(80, 80, 80) magic number with the already-imported COLOR_OPTIMAL_P_TEXT constant for the non-LOW Setpoint Authority label color - OVERVIEW.md: replace outdated [LOW AUTHORITY] binary-flag description with Setpoint Authority classification (LOW/MODERATE/HIGH), update dependability signals table and CV section to match new terminology Co-Authored-By: Claude Sonnet 4.6 --- OVERVIEW.md | 12 ++++++++---- src/plot_functions/plot_step_response.rs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/OVERVIEW.md b/OVERVIEW.md index 9cafcf0..74c3d4e 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -241,7 +241,7 @@ Physics-derived P gain optimization using a Torque-Inertia Profiler that measure - `Current P=` — P gain value from the flight's metadata - `Recommendation` — one of `(Conservative)`, `(Decrease)`, `Current P is optimal`, or `Investigate —` with reason; includes calculated D adjustment - `Reliable:` / `Unreliable:` — always shows both `Consistency=N% (⊢≥70%)` and `CV=N% (⊢≤40%)`; `Unreliable` is highlighted in orange when either threshold is not met - - `[LOW AUTHORITY]` warning (console) when max setpoint is below `LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S` + - `Setpoint Authority:` — always shown; classifies flight inputs as `LOW`, `MODERATE`, or `HIGH` based on the **mean** of per-window max setpoints (see below). Orange for `LOW`. - When profiling is skipped (insufficient punch events), a skip reason replaces the above. See **Consistency and Reliability Interpretation** below. - **Consistency and Reliability Interpretation (CV):** @@ -249,15 +249,19 @@ Physics-derived P gain optimization using a Torque-Inertia Profiler that measure - **Low CV:** Td measurements are tightly clustered — the log contains clean, repeatable dynamics and recommendations are trustworthy. - **High CV (exceeds `TD_COEFFICIENT_OF_VARIATION_MAX`):** Td measurements vary widely across windows — `Unreliable:` is shown (orange) in both console and PNG. Recommendations should be treated with caution. - **CV = N/A:** Fewer than `TD_SAMPLES_MIN_FOR_STDDEV` valid Td samples were available; standard deviation cannot be computed. The mean is still reported and `CV=N/A` appears in the reliability line. - - **Low-authority flight warning:** When the maximum setpoint across all valid windows is below `LOW_AUTHORITY_SETPOINT_THRESHOLD_DEG_S`, a `[LOW AUTHORITY]` warning is shown in both console and PNG. Hover tests and slow-cruise logs never produce sharp inputs — step-response analysis and Td measurements from such logs are noise-dominated rather than dynamics-dominated, making all recommendations unreliable regardless of CV. + - **Setpoint Authority classification:** Always-visible line in both console and PNG. Uses the **mean** of per-window max setpoints (not the maximum) to classify the flight: + - `LOW` (`mean < 100 dps`, orange) — hover/slow-cruise inputs; all P:D recommendations are still shown, but the pilot should treat them with caution. + - `MODERATE` (`100–250 dps`) — normal sport/freestyle inputs. + - `HIGH` (`> 250 dps`) — aggressive or race-pace inputs. + Format: `Setpoint Authority: LOW (mean=68dps ⊢≥100dps)`. Using the mean rather than the max prevents a single high-input window from masking an otherwise gentle hover log. - **Why hover logs produce high CV:** Small setpoint inputs → deconvolution is noise-sensitive → each window captures a different noise realisation. The averaged response may appear plausible (noise averages out) while individual window variance remains high. CV exposes this where the mean alone cannot. - **Over-P limitation:** **When P is already too high and the aircraft oscillates, the profiler may report "Optimal" rather than "Decrease P."** An oscillatory step response produces a short, aggressive measured Td — which, fed into the physics formula, yields a P_optimal close to the current (excessive) P. The profiler cannot reliably distinguish a well-tuned fast response from an over-tuned oscillating one using Td alone. The indirect signal is CV: severe oscillation typically scatters Td samples widely and triggers the consistency warning. **If your gains feel high or the craft exhibits oscillation, start from a lower P before relying on these recommendations.** Optimal P estimation is most accurate when the craft is in a reasonable tuning range — it is a validator and refinement tool, not a recovery tool for badly mis-tuned aircraft. Only experienced pilots are likely to recognise this situation by feel. - - **High CV without `[LOW AUTHORITY]`:** If the consistency warning fires but `[LOW AUTHORITY]` is not shown, the scatter is not caused by low-energy hover inputs. Remaining causes include propwash, inconsistent maneuvers, and oscillation from over-P. **If gains feel high, treat this combination as a prompt to verify the craft is not oscillating before acting on any recommendation.** + - **High CV without LOW authority:** If the consistency warning fires but `Setpoint Authority` is `MODERATE` or `HIGH`, the scatter is not caused by low-energy hover inputs. Remaining causes include propwash, inconsistent maneuvers, and oscillation from over-P. **If gains feel high, treat this combination as a prompt to verify the craft is not oscillating before acting on any recommendation.** - **Summary of dependability signals in output:** - `windows=` on the Td line — number of valid step-response windows contributing to the Td mean; more windows = more statistical weight - `Td source:` — flight and throttle-punch counts that calibrated the physics target; `File Group` means data was pooled across multiple logs - `Reliable:` / `Unreliable:` with `Consistency %` and `CV` — how repeatable the per-flight step-response measurements are (independent of how many punches fed the physics target) - - `[LOW AUTHORITY]` — max setpoint too small for reliable step-response characterisation + - `Setpoint Authority:` — mean setpoint level across valid windows; `LOW` indicates hover/gentle inputs that may reduce step-response quality - Noise level (`LOW` / `MODERATE` / `HIGH`) — HF D-term energy for the current flight; high noise limits safe P increase - **Relationship to P:D Recommendations:** diff --git a/src/plot_functions/plot_step_response.rs b/src/plot_functions/plot_step_response.rs index abeae1b..e811574 100644 --- a/src/plot_functions/plot_step_response.rs +++ b/src/plot_functions/plot_step_response.rs @@ -441,7 +441,7 @@ pub fn plot_step_response( let auth_color = if is_low_authority { COLOR_OPTIMAL_P_WARNING } else { - RGBColor(80, 80, 80) + COLOR_OPTIMAL_P_TEXT }; series.push(PlotSeries { data: vec![], From 88dfd3797febd0487e74582e43424aa154f7a2f9 Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Fri, 22 May 2026 11:15:05 -0500 Subject: [PATCH 11/12] fix: count only files with punch events in file_count; align section header - profile_aircraft_group: increment files_profiled only when total_events > 0, so file_count (and the Td source provenance line) excludes logs that were parsed successfully but produced zero throttle-punch events - Rename console section header from '--- Optimal P Estimation ---' to '--- Optimal P (Experimental, log-derived) ---' to match the PNG legend header text Addresses CodeRabbit review comments on PR #151. Co-Authored-By: Claude Sonnet 4.6 --- src/main.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4fb336c..c76b690 100644 --- a/src/main.rs +++ b/src/main.rs @@ -488,7 +488,9 @@ fn profile_aircraft_group(files: &[String], debug_mode: bool) -> AircraftProfile total_events ); } - files_profiled += 1; + if total_events > 0 { + files_profiled += 1; + } } Ok((_, None, ..)) => { if debug_mode { @@ -1144,7 +1146,7 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." } else { "file" }; - println!("\n--- Optimal P Estimation ---"); + println!("\n--- Optimal P (Experimental, log-derived) ---"); println!( "Td target: physics-derived from throttle-punch events in log {group_or_file}." ); From d5e0372264e75c424e6efa4ef484cd2ac97fa988 Mon Sep 17 00:00:00 2001 From: nerdCopter <56646290+nerdCopter@users.noreply.github.com> Date: Fri, 22 May 2026 11:24:28 -0500 Subject: [PATCH 12/12] fix console spacing --- src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index c76b690..e86c20f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -937,7 +937,6 @@ INFO ({input_file_str}): Skipping Step Response input data filtering: {reason}." } println!("{axis_name}: Actual Peak={peak_value:.3} → {assessment}"); - println!(); // Always show current P:D ratio with quality assessment let axis_pid = if axis_index == 0 {