Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ When `--step` flag is not used, all plots below are generated:
- **`*_Throttle_Freq_Heatmap_comparative.png`** — System noise characteristics across throttle levels and frequencies
- **`*_PID_Activity_stacked.png`** — P, I, D term activity over time for each axis (Roll, Pitch, Yaw). Displays all three PID components on the same time-domain plot with unified Y-axis scaling for visual comparison. Each term shows min/avg/max statistics in the legend. Useful for visualizing PID contribution balance during flight and identifying control issues (persistent P-term offset, I-term wind direction, D-term phase lag).

#### Generated Reports

- **`*_report.md`** — Structured markdown flight report written alongside PNGs on every run. Content is assembled from typed result structs returned by each analysis pass — no CSV re-reading. Sections: Metadata (firmware revision, craft name, PIDs, sample rate, gyroUnfilt source warning), Filter Configuration (per-axis LPF1/LPF2/IMUF/Pseudo-Kalman table, Dynamic Notch, RPM filter), PID Tuning P:D ratios, Step Response Analysis (Roll/Pitch: peak value, assessment, setpoint authority, P:D recommendations), Gyro Analysis (per-axis filtering delay with confidence, spectrum peaks), D-Term Analysis (per-axis filtering delay with N/A disambiguation, spectrum peaks), Motor Oscillation table, and relative links to all generated PNGs. Optimal P Estimation and Bode Analysis sections are included when those features produce results.


#### P:D Ratio Recommendations

The system provides intelligent P:D tuning recommendations based on step-response peak analysis:
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ Arguments can be in any order. Wildcards (e.g., *.csv) are shell-expanded and wo
- `*_Gyro_PSD_Spectrogram_comparative.png` — Gyro spectrogram (PSD vs. time)
- `*_Throttle_Freq_Heatmap_comparative.png` — Throttle/frequency heatmap analysis

#### Markdown Report (always generated)

- `*_report.md` — Structured flight report written alongside PNGs on every run. Sections: Metadata (firmware, PIDs, sample rate, gyroUnfilt source), Filter Configuration (LPF1/LPF2/IMUF/Pseudo-Kalman table, Dynamic Notch, RPM filter), PID Tuning, Step Response Analysis (Roll/Pitch with P:D assessment and setpoint authority), Gyro Analysis (filtering delay, confidence, spectrum peaks per axis), D-Term Analysis (filtering delay with N/A reason, spectrum peaks), Motor Oscillation, and links to all generated PNGs. Optimal P Estimation and Bode Analysis sections appear when those features are active.

#### Console Output:
- Current P:D ratio and peak analysis with response assessment
- Conservative and Moderate tuning recommendations (with D/D-Min/D-Max values)
Expand Down
57 changes: 46 additions & 11 deletions src/data_analysis/d_term_delay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ use crate::data_analysis::derivative::calculate_derivative;
use crate::data_analysis::filter_delay;
use crate::data_input::log_data::LogRowData;

/// Per-axis outcome from D-term delay calculation, including why delay is unavailable.
pub struct DTermAxisDelay {
pub result: Option<filter_delay::DelayResult>,
/// Human-readable reason when result is None (None when result is Some).
pub na_reason: Option<&'static str>,
}

/// Calculate filtering delay comparison between unfiltered and filtered D-terms.
///
/// This function computes the delay between the unfiltered D-term (calculated as derivative of gyroUnfilt)
Expand Down Expand Up @@ -44,23 +51,36 @@ use crate::data_input::log_data::LogRowData;
pub fn calculate_d_term_filtering_delay_comparison(
log_data: &[LogRowData],
sample_rate: f64,
) -> Vec<Option<filter_delay::DelayResult>> {
// Validate input parameters
) -> Vec<DTermAxisDelay> {
let make_empty = |reason: &'static str| -> Vec<DTermAxisDelay> {
(0..AXIS_NAMES.len())
.map(|_| DTermAxisDelay {
result: None,
na_reason: Some(reason),
})
.collect()
};

if !sample_rate.is_finite() || sample_rate <= 0.0 {
eprintln!(
"Error: Invalid sample rate ({}) for D-term delay analysis",
sample_rate
);
return vec![None; AXIS_NAMES.len()];
return make_empty("Invalid sample rate");
}

if log_data.is_empty() {
eprintln!("Error: Empty log data provided for D-term delay analysis");
return vec![None; AXIS_NAMES.len()];
return make_empty("No log data");
}

// Initialize with None for all axes to preserve axis alignment
let mut results: Vec<Option<filter_delay::DelayResult>> = vec![None; AXIS_NAMES.len()];
// Initialize with no-data reason; overwritten when data is found
let mut results: Vec<DTermAxisDelay> = (0..AXIS_NAMES.len())
.map(|_| DTermAxisDelay {
result: None,
na_reason: Some("No D-term data"),
})
.collect();

// First, check data availability for diagnosis
let mut gyro_unfilt_available = [false; 3];
Expand Down Expand Up @@ -112,8 +132,12 @@ pub fn calculate_d_term_filtering_delay_comparison(

// Use AXIS_NAMES.iter().enumerate() for consistency with other parts of the codebase
for (axis_idx, axis_name) in AXIS_NAMES.iter().enumerate() {
// Skip if either data type is unavailable
if !gyro_unfilt_available[axis_idx] || !d_term_available[axis_idx] {
if !gyro_unfilt_available[axis_idx] {
results[axis_idx].na_reason = Some("No gyroUnfilt data");
continue;
}
if !d_term_available[axis_idx] {
// na_reason already "No D-term data" from init
continue;
}

Expand Down Expand Up @@ -141,6 +165,7 @@ pub fn calculate_d_term_filtering_delay_comparison(
gyro_unfilt_data.len(),
d_term_filtered_data.len()
);
results[axis_idx].na_reason = Some("Insufficient samples");
continue;
}

Expand All @@ -157,6 +182,7 @@ pub fn calculate_d_term_filtering_delay_comparison(
" {}: D-term appears disabled (max abs value: {:.2e}, likely D gain = 0)",
axis_name, d_term_max_abs
);
results[axis_idx].na_reason = Some("D gain disabled");
continue;
}

Expand All @@ -175,6 +201,7 @@ pub fn calculate_d_term_filtering_delay_comparison(
" {}: D-term has no variation (std dev: {:.2e}, likely D gain = 0)",
axis_name, d_term_std_dev
);
results[axis_idx].na_reason = Some("D gain disabled");
continue;
}

Expand Down Expand Up @@ -203,12 +230,16 @@ pub fn calculate_d_term_filtering_delay_comparison(
result.delay_ms,
result.confidence * 100.0
);
results[axis_idx] = Some(result);
results[axis_idx] = DTermAxisDelay {
result: Some(result),
na_reason: None,
};
} else {
println!(
" {}: D-term delay calculation failed - correlation below D-term threshold",
axis_name
);
results[axis_idx].na_reason = Some("Low signal correlation");
}
} else if let Some(result) = calculate_d_term_filtering_delay_enhanced_xcorr(
&Array1::from_vec(d_term_filtered_data),
Expand All @@ -221,12 +252,16 @@ pub fn calculate_d_term_filtering_delay_comparison(
result.delay_ms,
result.confidence * 100.0
);
results[axis_idx] = Some(result);
results[axis_idx] = DTermAxisDelay {
result: Some(result),
na_reason: None,
};
} else {
println!(
" {}: D-term delay calculation failed - correlation below D-term threshold",
axis_name
);
results[axis_idx].na_reason = Some("Low signal correlation");
}
}

Expand All @@ -235,7 +270,7 @@ pub fn calculate_d_term_filtering_delay_comparison(
.iter()
.enumerate()
.filter_map(|(idx, result)| {
if result.is_some() {
if result.result.is_some() {
Some(AXIS_NAMES[idx])
} else {
None
Expand Down
13 changes: 13 additions & 0 deletions src/data_analysis/filter_delay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ pub fn calculate_average_filtering_delay_comparison(
sample_rate: f64,
) -> DelayAnalysisResult {
let mut all_results: Vec<DelayResult> = Vec::new();
let mut axis_delays: Vec<Option<(f32, f32)>> = vec![None; AXIS_NAMES.len()];

// First, diagnose data availability
println!("=== Gyro Data Availability Diagnostic ===");
Expand Down Expand Up @@ -274,6 +275,13 @@ pub fn calculate_average_filtering_delay_comparison(
}
}

// Capture per-axis delay for callers that need it (e.g. report)
if let Some(r) = axis_results
.iter()
.find(|r| r.method == "Enhanced Cross-Correlation")
{
axis_delays[axis] = Some((r.delay_ms, r.confidence));
}
all_results.extend(axis_results);
}
}
Expand All @@ -298,17 +306,20 @@ pub fn calculate_average_filtering_delay_comparison(
DelayAnalysisResult {
average_delay: Some(avg_delay),
results: method_summaries,
axis_delays,
}
} else {
DelayAnalysisResult {
average_delay: None,
results: method_summaries,
axis_delays,
}
}
} else {
DelayAnalysisResult {
average_delay: None,
results: Vec::new(),
axis_delays,
}
}
}
Expand All @@ -325,6 +336,8 @@ pub struct DelayResult {
pub struct DelayAnalysisResult {
pub average_delay: Option<f32>,
pub results: Vec<DelayResult>,
/// Per-axis (delay_ms, confidence 0–1); index matches AXIS_NAMES order
pub axis_delays: Vec<Option<(f32, f32)>>,
}

/// Calculate filtering delay using enhanced cross-correlation method only
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ pub mod font_config;
pub mod pid_context;
pub mod plot_framework;
pub mod plot_functions;
pub mod report;
pub mod types;
Loading
Loading