Skip to content

Add ESO gain optimization + report generation#136

Open
nerdCopter wants to merge 6 commits intomasterfrom
20260402_Quickflash-ESO
Open

Add ESO gain optimization + report generation#136
nerdCopter wants to merge 6 commits intomasterfrom
20260402_Quickflash-ESO

Conversation

@nerdCopter
Copy link
Copy Markdown
Owner

@nerdCopter nerdCopter commented Apr 2, 2026

How to Use the ESO (Extended State Observer) Feature

This guide explains how to run the 2nd-order Linear Extended State Observer (LESO) gain optimization and generate statistical reports using the BlackBox_CSV_Render tool.

Prerequisites

  • Build the project: cargo build --release

Running ESO Optimization and Report Generation

Use the release binary from ./target/release/:

./target/release/BlackBox_CSV_Render  --eso --report --output-dir ./output/
  • --eso enables 2nd-order LESO gain optimization for all axes (Roll, Pitch, Yaw).
  • --report writes a markdown statistical report (*_report.md) to the output directory.
  • --output-dir specifies where to save plots and reports.

Optional Flags

  • --eso-axis roll,pitch,yaw — Restrict optimization to specific axes (comma-separated, e.g. --eso-axis roll,pitch). Implies --eso.
  • --eso-b0 <value> — Set the control effectiveness parameter (default: 1.0). Implies --eso.

Output Files

  • Plots: Standard PNG plots for step response, spectra, etc.
  • Report: Markdown file (*_report.md) with:
    • Metadata (sample rate, duration, firmware, etc.)
    • Per-axis signal statistics (mean, std dev, min, max, RMS)
    • ESO optimization results (omega_0, beta1, beta2, MSE, etc.)

Example

./target/release/BlackBox_CSV_Render ./input/BTFL_BLACKBOX_LOG_20251125_161343_STELLARH7DEV_ICM45686_7a3622427.csv --eso --report --output-dir ./output/

Interpreting ESO Results

  • omega_0 (optimal): The observer bandwidth (rad/s) that minimizes tracking error.
  • beta1, beta2: LESO gains (beta1 = 2*omega_0, beta2 = omega_0^2).
  • MSE: Mean squared error between estimated and measured gyro rate.
  • Stability: Ensure omega_0 < sample_rate / 3 (enforced automatically).
  • b0: Control effectiveness (default 1.0, tune for physical accuracy if needed).

Use the optimal gains in your ADRC/LESO implementation. For absolute accuracy, co-tune b0 using known frame inertia.

Troubleshooting

  • If no report or plots are generated, check for errors in the terminal output.
  • Ensure the input CSV is a full flight log (not a header or partial file).
  • For EmuFlight, make sure the firmware revision is correctly detected in the log header.

For more details, see the OVERVIEW.md and the generated report files in ./output/.


Summary by CodeRabbit

  • New Features

    • ESO Gain Optimization: offline per-axis LESO bandwidth search with printed results and optional per-axis ESO plots; CLI flags to enable ESO, choose axes, and set b0.
    • Statistical Report Output: generates a Markdown report (_report.md) with metadata, firmware/config excerpts, per-axis signal statistics, and ESO result summaries when available.
  • Documentation

    • Overview updated to document the new ESO and report options and generated report outputs.

CodeRabbitAI Review Fixes (commit 6e600ca)

  • OVERVIEW.md — Moved *_report.md bullet out of the "Generated PNG Plots" list into a new "Generated Reports" subsection (was self-contradictory).

  • src/eso.rs — Added upfront input validation to the public run_eso_optimization API:

    • axis >= AXIS_COUNT returns a descriptive error instead of panicking.
    • Non-finite or non-positive sample_rate returns an error.
    • Invalid EsoConfig fields (omega0_min/omega0_max/b0) return an error.
    • Added control-input excitation guard: axes whose pid_sum is all-zero or sub-epsilon are rejected before the optimizer runs.
  • src/report.rs — Fixed PID term collection in extract_axis_signals:

    • Replaced unwrap_or(0.0) with per-field Option guards so absent P/I/D/F terms are not silently counted as real zero samples in statistics.
    • Replaced hardcoded axis < 4 magic literal with row.setpoint.get(axis).copied().flatten() (bounds-safe).
  • src/plot_functions/plot_eso.rs + src/constants.rs — Fixed f_hat visual scaling:

    • Added ESO_FHAT_Y_FRACTION = 0.5 constant (eliminates magic 0.5 literal).
    • Scale is now computed after half_range is known (inside the draw closure), using (half_range * ESO_FHAT_Y_FRACTION) / fhat_max_abs. Previously used UNIFIED_Y_AXIS_MIN_SCALE as the reference, which squashed the trace when real gyro data expanded half_range beyond the floor value.
  • src/main.rs--eso-axis and --eso-b0 now imply --eso; previously they were silently ignored when --eso was omitted.

Summary by CodeRabbit

  • New Features
    • Added Extended State Observer (ESO) bandwidth optimization via --eso flag with per-axis control and optional effectiveness parameter (--eso-b0).
    • Added markdown statistical report generation with --report flag, including signal statistics and ESO results summary.
    • Added ESO output plots displaying estimated versus measured angular rates with disturbance estimates.

- Add src/eso.rs: 2nd-order LESO bandwidth optimization via golden-section search
- Add src/report.rs: Per-axis signal statistics and ESO results in markdown format
- Add CLI flags: --eso, --eso-axis, --eso-b0, --report
- Add ESO constants to src/constants.rs (omega_0 bounds, GSS tolerance)
- Update lib.rs and main.rs to wire modules and implement ESO/report execution
- Update OVERVIEW.md with ESO and report sections
- Add INFORMATION/ESO_HOWTO.md user guide
- Tested on Betaflight 2025.12 and EmuFlight 0.4.3 full flights
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 2, 2026

Warning

Rate limit exceeded

@nerdCopter has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 29 minutes and 20 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 29 minutes and 20 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9a1b67e5-ca6f-4f22-b7bc-2899a1071c33

📥 Commits

Reviewing files that changed from the base of the PR and between 89448be and 9c4f38e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • OVERVIEW.md
  • src/constants.rs
  • src/data_input/pid_metadata.rs
  • src/eso.rs
  • src/lib.rs
  • src/main.rs
  • src/plot_functions/mod.rs
  • src/plot_functions/plot_eso.rs
  • src/report.rs

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an optional 2nd-order LESO bandwidth optimizer and optional Markdown statistical reporting, new eso and report modules with plotting support, CLI flags (--eso, --eso-axis, --eso-b0, --report), new ESO constants/colors, per-axis ESO execution in main, and an argmin dependency.

Changes

Cohort / File(s) Summary
Documentation
OVERVIEW.md
Documented ESO flags (--eso, --eso-axis, --eso-b0), --report output and <stem>_report.md; added sections describing src/eso.rs behavior and src/report.rs output; listed *_report.md among generated outputs.
Constants
src/constants.rs
Added ESO tuning/search constants (ESO_OMEGA0_MIN, ESO_OMEGA0_MAX, ESO_GSS_TOLERANCE, ESO_GSS_MAX_ITER, ESO_DEFAULT_B0, ESO_N_AHEAD_STEPS, ESO_WARMUP_FRACTION, ESO_B0_MIN_CONTROL_THRESHOLD, ESO_B0_MIN_OLS_SAMPLES, ESO_B0_ESTIMATE_MIN_POSITIVE, ESO_FHAT_Y_FRACTION) and plot color aliases (COLOR_ESO_MEAS, COLOR_ESO_HAT, COLOR_ESO_FHAT).
ESO module
src/eso.rs
New module implementing per-axis 2nd-order LESO bandwidth optimization. Adds EsoConfig, EsoResult, and run_eso_optimization(...) which validates inputs, optionally auto-estimates b0 (OLS), constrains search by sample rate, performs golden-section search over omega0 minimizing N-step-ahead MSE, computes gains, and returns metrics and traces.
Report module
src/report.rs
New module to generate Markdown statistical reports. Adds SignalStats, compute_signal_stats, and generate_markdown_report(...) to produce metadata, filtered firmware/config section, per-axis signal statistics tables, and per-axis ESO result sections when provided; writes <stem>_report.md.
Public API
src/lib.rs
Exported new submodules pub mod eso; and pub mod report;.
CLI & Main
src/main.rs
Extended PlotConfig with ESO/report fields, added CLI parsing for --eso, --eso-axis, --eso-b0, --report; process_file() runs run_eso_optimization per selected axis (requires sample_rate), collects EsoResults, optionally produces ESO plot and <stem>_report.md, and preserves ESO/report flags when resetting plot-only flags.
Plotting
src/plot_functions/mod.rs, src/plot_functions/plot_eso.rs
Exported plot_eso module and added plot_eso_output(...) which aligns/truncates ESO traces, computes unified Y scale and optional f_hat scaling, and draws annotated stacked plots per axis (omega_hat, omega_meas, optional f_hat).
Data input tweak
src/data_input/pid_metadata.rs
Refactored INAV-style FF assignment to a conditional expression (Some(values[3]) when > 0), preserving prior behavior.
Build / Dependencies
Cargo.toml
Added dependency argmin = "0.11" for golden-section search optimization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main changes: adding ESO gain optimization and report generation, which are the primary features introduced across multiple new modules and CLI flags.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 20260402_Quickflash-ESO

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@OVERVIEW.md`:
- Line 207: The list under the "Generated PNG Plots" heading incorrectly
includes the bullet "`*_report.md`" (a Markdown statistical report); remove that
bullet from this PNG outputs list and either move the "`*_report.md`" entry into
a new or existing separate outputs subsection (e.g., "Other outputs" or
"Generated Reports") or rename the "Generated PNG Plots" heading to a more
general title so the report file logically belongs there; ensure the change
updates only the OV ERVIEW.md section referencing "Generated PNG Plots" and the
"`*_report.md`" bullet so the PNG list contains only image outputs.

In `@src/eso.rs`:
- Around line 151-158: The loop blindly zero-fills missing P/I/D/F terms and
accepts axes with no real control excitation; change the logic in the loop that
builds omega_meas and pid_sum (references: row.gyro[axis], row.p_term[axis],
row.i_term[axis], row.d_term[axis], row.f_term[axis], omega_meas, pid_sum) to
detect and reject axes where the control input is not usable:
skip/presume-invalid if all of p_term/i_term/d_term/f_term are None or if the
resulting pid_sum is effectively constant/zero (e.g., all entries equal or
variance below a small threshold), and surface that fact to the caller (return
an error or None) instead of returning an ESO result computed from zero-filled
inputs. Ensure this guard runs before running the optimizer so only axes with
real excitation are optimized.
- Around line 177-199: The public run_eso_optimization must validate its inputs
before using them: check that axis is within valid range (0 <= axis <
AXIS_COUNT) and return a descriptive Err instead of allowing extract_axis_data
to panic; validate sample_rate is finite and > 0; validate numeric config fields
(EsoConfig::omega0_min, EsoConfig::omega0_max, EsoConfig::b0) are finite and
that omega0_max > omega0_min; then compute omega0_max_stable as before and
proceed to call extract_axis_data and mse_cost. Update run_eso_optimization to
perform these checks (referencing run_eso_optimization, AXIS_COUNT,
extract_axis_data, and EsoConfig::omega0_min/omega0_max/b0) and return early Err
results with clear messages when validations fail.

In `@src/report.rs`:
- Around line 67-76: The code currently uses unwrap_or(0.0) on
row.p_term/i_term/d_term/f_term inside the gyro gate so missing PID fields are
recorded as real zeros; remove the unwrap_or(0.0) usage and instead test each
Option and only push PID samples when the Option is Some (e.g. if let Some(p) =
row.p_term[axis] { pid_p.push(p) } ), and compute/push pid_sum only when all
required components are present (e.g. only when p, i, d, f are Some); this
ensures row.gyro is still collected but missing PID fields are not misreported
as zero-valued samples.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 08ac2d4c-95fd-4948-88ac-a47b23f153b8

📥 Commits

Reviewing files that changed from the base of the PR and between 89448be and 5315727.

📒 Files selected for processing (6)
  • OVERVIEW.md
  • src/constants.rs
  • src/eso.rs
  • src/lib.rs
  • src/main.rs
  • src/report.rs

Comment thread OVERVIEW.md
Comment thread src/eso.rs
Comment thread src/eso.rs Outdated
Comment thread src/report.rs Outdated
Comment on lines +67 to +76
if let Some(g) = row.gyro[axis] {
let p = row.p_term[axis].unwrap_or(0.0);
let i_val = row.i_term[axis].unwrap_or(0.0);
let d = row.d_term[axis].unwrap_or(0.0);
let f_val = row.f_term[axis].unwrap_or(0.0);
gyro.push(g);
pid_p.push(p);
pid_i.push(i_val);
pid_d.push(d);
pid_sum.push(p + i_val + d + f_val);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't report missing PID fields as zero-valued samples.

unwrap_or(0.0) here turns absent P/I/D/F values into real zeroes, and the surrounding gyro gate gives them a full sample count whenever gyro exists. That makes the markdown report unable to distinguish "signal missing" from "signal logged and zero", which is misleading for tuning/debugging.

Possible fix
     for row in log_data {
         if let Some(g) = row.gyro[axis] {
-            let p = row.p_term[axis].unwrap_or(0.0);
-            let i_val = row.i_term[axis].unwrap_or(0.0);
-            let d = row.d_term[axis].unwrap_or(0.0);
-            let f_val = row.f_term[axis].unwrap_or(0.0);
             gyro.push(g);
-            pid_p.push(p);
-            pid_i.push(i_val);
-            pid_d.push(d);
-            pid_sum.push(p + i_val + d + f_val);
+        }
+        if let Some(p) = row.p_term[axis] {
+            pid_p.push(p);
+        }
+        if let Some(i_val) = row.i_term[axis] {
+            pid_i.push(i_val);
+        }
+        if let Some(d) = row.d_term[axis] {
+            pid_d.push(d);
+        }
+        let pid_terms = [
+            row.p_term[axis],
+            row.i_term[axis],
+            row.d_term[axis],
+            row.f_term[axis],
+        ];
+        if pid_terms.iter().any(|term| term.is_some()) {
+            pid_sum.push(pid_terms.iter().map(|term| term.unwrap_or(0.0)).sum());
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/report.rs` around lines 67 - 76, The code currently uses unwrap_or(0.0)
on row.p_term/i_term/d_term/f_term inside the gyro gate so missing PID fields
are recorded as real zeros; remove the unwrap_or(0.0) usage and instead test
each Option and only push PID samples when the Option is Some (e.g. if let
Some(p) = row.p_term[axis] { pid_p.push(p) } ), and compute/push pid_sum only
when all required components are present (e.g. only when p, i, d, f are Some);
this ensures row.gyro is still collected but missing PID fields are not
misreported as zero-valued samples.

…-domain plot

- Add argmin 0.11 dependency; implement CostFunction trait for ESO bandwidth search
- Replace monotone 1-step MSE cost with N-step-ahead open-loop prediction cost
  (unimodal: low omega0 = stale f_hat; high omega0 = noise amplification)
  Constants: ESO_N_AHEAD_STEPS=5, ESO_WARMUP_FRACTION=0.20
- EsoResult now carries omega_meas_trace, omega_hat_trace, f_hat_trace, timestamps
- Add plot_eso.rs: stacked time-domain ESO output plot per axis
  (omega_meas blue thin over omega_hat orange thick, f_hat green scaled)
- Wire ESO output plot into main.rs after optimization; generated as *_ESO_output_stacked.png
- Update report.rs: MSE label clarified to N-step-ahead prediction
- Add ESO plot colors to constants.rs (COLOR_ESO_MEAS/HAT/FHAT)
- ESO_GSS_MAX_ITER type corrected to u64 for argmin compatibility
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/main.rs (1)

1316-1331: Minor: any_valid guard is effectively unreachable.

Any unknown/empty axis token already triggers print_usage_and_exit via the other arm, so the if any_valid check at line 1329 never fires for the false case. It's harmless, but you could drop it and just always assign plot_config.eso_axes = axes; for clarity. Not a bug.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 1316 - 1331, The any_valid boolean guard is
redundant because unknown tokens call print_usage_and_exit in the match's other
arm; remove the any_valid flag and its conditional and always assign
plot_config.eso_axes = axes after parsing. Specifically, delete the any_valid
variable and the if any_valid { ... } check, leaving axes to be set directly
into plot_config.eso_axes (referencing the axes array, the any_valid variable,
and plot_config.eso_axes and the print_usage_and_exit call to locate the code).
src/report.rs (2)

176-176: Use AXIS_COUNT instead of AXIS_NAMES.len().min(3).

axis_names.rs already defines AXIS_COUNT = 3 with a compile-time assertion that it equals AXIS_NAMES.len(). The .min(3) embeds a magic number that duplicates that invariant.

-    for axis_idx in 0..AXIS_NAMES.len().min(3) {
+    for axis_idx in 0..crate::axis_names::AXIS_COUNT {

As per coding guidelines: "Axis indices (0=Roll, 1=Pitch, 2=Yaw) must use the src/axis_names.rs module with AXIS_COUNT, AXIS_NAMES, and axis_name() function instead of creating redundant axis index constants".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/report.rs` at line 176, The loop uses a magic-number expression
AXIS_NAMES.len().min(3); replace that with the canonical constant AXIS_COUNT
(from axis_names.rs) so axis indexing relies on the single authoritative value;
update the for loop range to for axis_idx in 0..AXIS_COUNT and ensure any
related uses rely on AXIS_NAMES and axis_name() rather than duplicated
constants.

78-82: Dead condition and magic number.

The outer loop iterates 0..AXIS_NAMES.len().min(3) (= 0..3), so axis < 4 is always true and the check is dead. If the intent was to also report Throttle (setpoint index 3), the loop bound needs to change; otherwise drop the guard. The literal 4 is also a magic number per the repo guideline.

-        if axis < 4 {
-            if let Some(sp) = row.setpoint[axis] {
-                setpoint.push(sp);
-            }
+        if let Some(sp) = row.setpoint[axis] {
+            setpoint.push(sp);
         }

As per coding guidelines: "All constants go in src/constants.rs — no hardcoded magic numbers in function code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/report.rs` around lines 78 - 82, The condition `if axis < 4` in the block
using `axis` and `row.setpoint[axis]` is dead because the loop already iterates
`0..AXIS_NAMES.len().min(3)`; either extend the loop bound to include Throttle
(index 3) or remove the guard entirely. Replace the hardcoded `4` with a named
constant (e.g., MAX_REPORTED_AXIS) defined in src/constants.rs and use that
constant to control the loop or the check; update the loop range to
`0..AXIS_NAMES.len().min(MAX_REPORTED_AXIS)` if you intend to include index 3,
otherwise delete the `axis < ...` guard. Ensure references are to `AXIS_NAMES`,
the loop variable `axis`, and `row.setpoint` so the change is localized to the
existing logic.
src/eso.rs (1)

244-253: Minor: move the / 3.0 stability divisor into constants.rs.

The sample_rate / 3.0 stability bound is a domain-specific constant (LESO discrete-time stability limit) repeated inline. Putting it in src/constants.rs (e.g. ESO_STABILITY_DIVISOR: f64 = 3.0) keeps the rule in one place and matches how the other ESO knobs are defined.

As per coding guidelines: "All constants go in src/constants.rs — no hardcoded magic numbers in function code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/eso.rs` around lines 244 - 253, Replace the hardcoded divisor 3.0 with a
named constant defined in src/constants.rs (e.g. ESO_STABILITY_DIVISOR: f64 =
3.0) and use that constant in the omega0_max_stable calculation in the function
that computes omega0_max_stable (references: omega0_max_stable, sample_rate,
config.omega0_max, config.omega0_min); update the error message computation that
multiplies config.omega0_min * 3.0 to use the same constant (config.omega0_min *
ESO_STABILITY_DIVISOR) so the stability rule is centralized in constants.rs.
src/plot_functions/plot_eso.rs (1)

51-54: Nit: p95_idx name is misleading.

The index is computed from UNIFIED_Y_AXIS_PERCENTILE, not a hardcoded 95th percentile. Rename to pctl_idx (or similar) so it doesn't imply a specific value if the constant is later tuned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plot_functions/plot_eso.rs` around lines 51 - 54, The local variable name
p95_idx is misleading because the index is derived from
UNIFIED_Y_AXIS_PERCENTILE (a tunable constant); rename p95_idx to a neutral name
like pctl_idx (or percentile_idx) in the block that computes half_range and
update any subsequent uses in the same scope to match (referencing all_abs,
UNIFIED_Y_AXIS_PERCENTILE, UNIFIED_Y_AXIS_HEADROOM_SCALE,
UNIFIED_Y_AXIS_MIN_SCALE and the half_range calculation) so the code reflects
the generic percentile-based computation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plot_functions/plot_eso.rs`:
- Around line 98-108: The f_hat scaling uses UNIFIED_Y_AXIS_MIN_SCALE instead of
the actual plot half_range, so compute fhat_scale after half_range is known
(i.e., inside draw_stacked_plot or by passing half_range into where fhat_scale
is built) and base it on data.fhat_max_abs and half_range so f_hat fills the
intended fraction of the Y range; replace the magic 0.5 with a new constant
ESO_FHAT_Y_FRACTION placed in src/constants.rs, and update usages of
fhat_scale/fhat_max_abs in plot_eso.rs (references: build_axis_data,
draw_stacked_plot, data.fhat_max_abs, fhat_scale, UNIFIED_Y_AXIS_MIN_SCALE) to
use the new calculation and constant.

---

Nitpick comments:
In `@src/eso.rs`:
- Around line 244-253: Replace the hardcoded divisor 3.0 with a named constant
defined in src/constants.rs (e.g. ESO_STABILITY_DIVISOR: f64 = 3.0) and use that
constant in the omega0_max_stable calculation in the function that computes
omega0_max_stable (references: omega0_max_stable, sample_rate,
config.omega0_max, config.omega0_min); update the error message computation that
multiplies config.omega0_min * 3.0 to use the same constant (config.omega0_min *
ESO_STABILITY_DIVISOR) so the stability rule is centralized in constants.rs.

In `@src/main.rs`:
- Around line 1316-1331: The any_valid boolean guard is redundant because
unknown tokens call print_usage_and_exit in the match's other arm; remove the
any_valid flag and its conditional and always assign plot_config.eso_axes = axes
after parsing. Specifically, delete the any_valid variable and the if any_valid
{ ... } check, leaving axes to be set directly into plot_config.eso_axes
(referencing the axes array, the any_valid variable, and plot_config.eso_axes
and the print_usage_and_exit call to locate the code).

In `@src/plot_functions/plot_eso.rs`:
- Around line 51-54: The local variable name p95_idx is misleading because the
index is derived from UNIFIED_Y_AXIS_PERCENTILE (a tunable constant); rename
p95_idx to a neutral name like pctl_idx (or percentile_idx) in the block that
computes half_range and update any subsequent uses in the same scope to match
(referencing all_abs, UNIFIED_Y_AXIS_PERCENTILE, UNIFIED_Y_AXIS_HEADROOM_SCALE,
UNIFIED_Y_AXIS_MIN_SCALE and the half_range calculation) so the code reflects
the generic percentile-based computation.

In `@src/report.rs`:
- Line 176: The loop uses a magic-number expression AXIS_NAMES.len().min(3);
replace that with the canonical constant AXIS_COUNT (from axis_names.rs) so axis
indexing relies on the single authoritative value; update the for loop range to
for axis_idx in 0..AXIS_COUNT and ensure any related uses rely on AXIS_NAMES and
axis_name() rather than duplicated constants.
- Around line 78-82: The condition `if axis < 4` in the block using `axis` and
`row.setpoint[axis]` is dead because the loop already iterates
`0..AXIS_NAMES.len().min(3)`; either extend the loop bound to include Throttle
(index 3) or remove the guard entirely. Replace the hardcoded `4` with a named
constant (e.g., MAX_REPORTED_AXIS) defined in src/constants.rs and use that
constant to control the loop or the check; update the loop range to
`0..AXIS_NAMES.len().min(MAX_REPORTED_AXIS)` if you intend to include index 3,
otherwise delete the `axis < ...` guard. Ensure references are to `AXIS_NAMES`,
the loop variable `axis`, and `row.setpoint` so the change is localized to the
existing logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 209c4fdb-6a38-4c19-8c97-10636f7e228e

📥 Commits

Reviewing files that changed from the base of the PR and between 5315727 and 4105394.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • src/constants.rs
  • src/eso.rs
  • src/main.rs
  • src/plot_functions/mod.rs
  • src/plot_functions/plot_eso.rs
  • src/report.rs
✅ Files skipped from review due to trivial changes (3)
  • Cargo.toml
  • src/plot_functions/mod.rs
  • src/constants.rs

Comment thread src/plot_functions/plot_eso.rs
- estimate_b0(): OLS closed-form b0 = sum(u*d_omega) / (Ts * sum(u^2))
  on samples where |PID sum| >= ESO_B0_MIN_CONTROL_THRESHOLD (10.0)
- b0 auto-estimated from data when user has not overridden --eso-b0;
  falls back to ESO_DEFAULT_B0 (1.0) when too few valid samples
- EsoResult.b0_auto: true when estimated, false when user-supplied
- main.rs: print shows b0=X.XXXX (estimated|user)
- report.rs: b0 row labels (auto-estimated|user-supplied); updated guidance
- Add ESO_B0_MIN_CONTROL_THRESHOLD = 10.0 to constants.rs
- Fix pre-existing clippy collapsible_match in pid_metadata.rs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/main.rs (2)

1279-1337: Missing duplicate-argument detection for --eso-b0 and --eso-axis.

Unlike --dps (line 1221) and --output-dir (line 1245), these two new options silently let a later occurrence overwrite an earlier one. For consistency and to catch typos in scripts, mirror the existing "specified more than once" check.

Proposed guard
+        } else if arg == "--eso-b0" {
+            // add a tracking bool near other flags, e.g. `let mut eso_b0_flag_present = false;`
+            if eso_b0_flag_present {
+                eprintln!("Error: --eso-b0 argument specified more than once.");
+                print_usage_and_exit(program_name);
+            }
+            eso_b0_flag_present = true;

(Same pattern for --eso-axis.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 1279 - 1337, The new --eso-b0 and --eso-axis
branches need the same "specified more than once" guard used by
--dps/--output-dir: check whether the option was already set before accepting a
new value and if so call print_usage_and_exit(program_name) with an error; for
--eso-b0 test the existing value on plot_config.eso_b0 (or a companion boolean
like plot_config.eso_b0_set) and for --eso-axis test plot_config.eso_axes (or
plot_config.eso_axes_set) and emit a clear error message that the option was
specified more than once, then exit, before parsing the new value.

1334-1337: Empty --eso-axis value silently keeps defaults.

If val contains only whitespace/commas (e.g. --eso-axis "," or --eso-axis ""), every part.trim() is "" and hits the other => arm, which already exits — good. But the explicit any_valid branch here is then unreachable for truly empty input; the if any_valid wrap ends up being a no-op because an empty value would already have errored on the first empty part. Minor: the conditional assignment can be dropped, or add an explicit "at least one axis required" error to make intent clearer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 1334 - 1337, The parsing currently silently keeps
defaults when `--eso-axis` is given an empty/whitespace/comma-only value because
the loop returns early on the first empty `part` and the `if any_valid {
plot_config.eso_axes = axes; }` path is effectively unreachable for truly empty
input; change the logic in the `--eso-axis` parsing loop (the code that builds
`axes` and tracks `any_valid`) to explicitly detect "no valid axes found" after
processing `val` and return an error like "at least one axis required for
--eso-axis" instead of silently keeping defaults, and then either assign
`plot_config.eso_axes = axes` unconditionally or assert `any_valid` before
assignment so the intent is clear (update the block that references `any_valid`,
`axes`, and `plot_config.eso_axes`).
src/report.rs (2)

176-176: Use AXIS_COUNT instead of hardcoded 3.

AXIS_NAMES.len() is already 3 by the project convention, so .min(3) is redundant and the 3 is a magic number. Prefer the canonical constant from src/axis_names.rs.

Proposed change
-use crate::axis_names::AXIS_NAMES;
+use crate::axis_names::{AXIS_COUNT, AXIS_NAMES};
@@
-    for axis_idx in 0..AXIS_NAMES.len().min(3) {
+    for axis_idx in 0..AXIS_COUNT {

As per coding guidelines: "Axis indices (0=Roll, 1=Pitch, 2=Yaw) must use the src/axis_names.rs module with AXIS_COUNT, AXIS_NAMES, and axis_name() function instead of creating redundant axis index constants" and "no hardcoded magic numbers in function code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/report.rs` at line 176, Replace the hardcoded magic number by iterating
using the canonical axis count: change the loop header currently using
AXIS_NAMES.len().min(3) to use AXIS_COUNT (e.g. for axis_idx in 0..AXIS_COUNT)
so the code relies on the src/axis_names.rs constants (AXIS_COUNT / AXIS_NAMES)
and removes the magic `3`; keep using axis_idx and axis_name() elsewhere as
before.

32-37: Variance uses population divisor (N) rather than sample (N-1).

For descriptive stats of a sampled time series, reporting sample std‑dev (divide by count - 1, with a guard for count < 2) is more conventional and matches what most tuning tools produce. Not a correctness bug, but may cause small discrepancies if users cross-check against NumPy/Pandas defaults.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/report.rs` around lines 32 - 37, The variance currently divides by count
(population); change the variance calculation in src/report.rs so sample
variance is used: compute var by dividing the sum of squared deviations by
(count - 1) when count > 1 and set var (and consequently std_dev) to NaN (or
another sentinel) when count < 2 to avoid divide-by-zero; update std_dev =
var.sqrt() to use that var. Refer to the mean, var, std_dev variables in the
diff and add the guard around the variance/std_dev computation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main.rs`:
- Around line 1275-1278: The flags --eso-axis and --eso-b0 currently set their
values but are ignored unless plot_config.run_eso is true; update the argument
handling so when parsing "--eso-axis" or "--eso-b0" you also set
plot_config.run_eso = true (or alternatively emit a warning if
plot_config.run_eso is false) so the ESO pipeline controlled by
plot_config.run_eso is executed when those related flags are supplied; make this
change in the same arg-parsing logic that handles "--eso-axis" and "--eso-b0"
alongside the existing "--eso" case so behavior matches user expectations.

In `@src/report.rs`:
- Around line 78-82: The guard "axis < 4" in the block that accesses
row.setpoint[axis] is a hardcoded magic number and inconsistent with the caller
that iterates 0..AXIS_NAMES.len().min(3); change the guard to a length-based
check (e.g., axis < row.setpoint.len()) or compare against a named constant
(add/used a constant in src/constants.rs like SETPOINT_SLOTS) and use that
instead so the check is accurate and consistent with the row.gyro indexing and
avoids the magic literal.

---

Nitpick comments:
In `@src/main.rs`:
- Around line 1279-1337: The new --eso-b0 and --eso-axis branches need the same
"specified more than once" guard used by --dps/--output-dir: check whether the
option was already set before accepting a new value and if so call
print_usage_and_exit(program_name) with an error; for --eso-b0 test the existing
value on plot_config.eso_b0 (or a companion boolean like plot_config.eso_b0_set)
and for --eso-axis test plot_config.eso_axes (or plot_config.eso_axes_set) and
emit a clear error message that the option was specified more than once, then
exit, before parsing the new value.
- Around line 1334-1337: The parsing currently silently keeps defaults when
`--eso-axis` is given an empty/whitespace/comma-only value because the loop
returns early on the first empty `part` and the `if any_valid {
plot_config.eso_axes = axes; }` path is effectively unreachable for truly empty
input; change the logic in the `--eso-axis` parsing loop (the code that builds
`axes` and tracks `any_valid`) to explicitly detect "no valid axes found" after
processing `val` and return an error like "at least one axis required for
--eso-axis" instead of silently keeping defaults, and then either assign
`plot_config.eso_axes = axes` unconditionally or assert `any_valid` before
assignment so the intent is clear (update the block that references `any_valid`,
`axes`, and `plot_config.eso_axes`).

In `@src/report.rs`:
- Line 176: Replace the hardcoded magic number by iterating using the canonical
axis count: change the loop header currently using AXIS_NAMES.len().min(3) to
use AXIS_COUNT (e.g. for axis_idx in 0..AXIS_COUNT) so the code relies on the
src/axis_names.rs constants (AXIS_COUNT / AXIS_NAMES) and removes the magic `3`;
keep using axis_idx and axis_name() elsewhere as before.
- Around line 32-37: The variance currently divides by count (population);
change the variance calculation in src/report.rs so sample variance is used:
compute var by dividing the sum of squared deviations by (count - 1) when count
> 1 and set var (and consequently std_dev) to NaN (or another sentinel) when
count < 2 to avoid divide-by-zero; update std_dev = var.sqrt() to use that var.
Refer to the mean, var, std_dev variables in the diff and add the guard around
the variance/std_dev computation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a56e0eb8-8077-4118-b86c-db70872a4c64

📥 Commits

Reviewing files that changed from the base of the PR and between 4105394 and 31a14f3.

📒 Files selected for processing (5)
  • src/constants.rs
  • src/data_input/pid_metadata.rs
  • src/eso.rs
  • src/main.rs
  • src/report.rs
✅ Files skipped from review due to trivial changes (3)
  • src/data_input/pid_metadata.rs
  • src/constants.rs
  • src/eso.rs

Comment thread src/main.rs
Comment thread src/report.rs Outdated
- OVERVIEW.md: move *_report.md bullet out of 'Generated PNG Plots'
  into a new 'Generated Reports' subsection (was self-contradictory)

- src/eso.rs: add upfront validation in run_eso_optimization
  * axis >= AXIS_COUNT → descriptive Err instead of panic
  * non-finite / non-positive sample_rate → Err
  * invalid EsoConfig fields (omega0_min/max, b0) → Err
  * add control-input excitation guard: reject axes where pid_sum
    is all-zero / sub-epsilon so flat-input axes are not 'optimised'

- src/report.rs: fix PID term collection in extract_axis_signals
  * replace unwrap_or(0.0) with per-field Option guards so absent
    P/I/D/F terms are not silently reported as real zero samples
  * replace hardcoded 'axis < 4' magic literal with
    row.setpoint.get(axis).copied().flatten() (bounds-safe)

- src/plot_functions/plot_eso.rs + src/constants.rs:
  * add ESO_FHAT_Y_FRACTION = 0.5 constant (no more magic 0.5)
  * store fhat_max_abs in AxisEsoData instead of pre-computed scale
  * compute fhat_scale inside draw_stacked_plot after half_range is
    known: (half_range * ESO_FHAT_Y_FRACTION) / fhat_max_abs
    fixes f_hat being squashed when half_range > UNIFIED_Y_AXIS_MIN_SCALE

- src/main.rs: --eso-axis and --eso-b0 now imply --eso
  (previously silently ignored when --eso was omitted)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/report.rs (1)

184-184: Redundant .min(3) on AXIS_NAMES.len().

AXIS_NAMES has a compile-time-enforced length of AXIS_COUNT = 3 (see src/axis_names.rs), so .min(3) never trims anything. Prefer AXIS_COUNT for consistency with src/eso.rs, which already imports it.

Suggested simplification
-    for axis_idx in 0..AXIS_NAMES.len().min(3) {
+    for axis_idx in 0..crate::axis_names::AXIS_COUNT {

As per coding guidelines: "Axis indices (0=Roll, 1=Pitch, 2=Yaw) must use the src/axis_names.rs module with AXIS_COUNT, AXIS_NAMES, and axis_name() function instead of creating redundant axis index constants".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/report.rs` at line 184, Replace the redundant range expression
"0..AXIS_NAMES.len().min(3)" with "0..AXIS_COUNT" in the loop over axis_idx;
ensure AXIS_COUNT is imported alongside AXIS_NAMES (and prefer using axis_name()
for human-readable labels if needed) so the loop uses the compile-time axis
count rather than a hardcoded 3.
src/main.rs (1)

385-385: Help text for --eso-axis omits the supported all token.

The parser accepts roll | pitch | yaw | 0 | 1 | 2 | all (lines 1310–1325), but the CLI help on line 385 and the error message on line 1328 only advertise roll,pitch,yaw. Either document all (and numeric aliases) or drop them from the parser to keep the surface area consistent with the documentation.

Suggested minimal fix
-    eprintln!(
-        "  --eso-axis <axes>: Axes to optimize (comma-separated: roll,pitch,yaw). Default: all."
-    );
+    eprintln!(
+        "  --eso-axis <axes>: Axes to optimize (comma-separated: roll,pitch,yaw or all). Default: all."
+    );

Also applies to: 1322-1325

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` at line 385, The CLI help string for "--eso-axis" currently
lists only "roll,pitch,yaw" but the parsing code also accepts "all" and numeric
aliases (0,1,2); update the help text and the related error message to document
the supported tokens (e.g., "roll,pitch,yaw,all (or 0,1,2)") so the CLI and
parser match. Locate the help string literal containing "--eso-axis" and the
parser branch that handles axis tokens (the parsing logic around the
"all"/numeric aliases) and make their user-facing text consistent.
src/eso.rs (1)

229-233: Magic numbers in estimate_b0 violate the repository's constants rule.

count < 10, den.abs() < 1e-12, and b0.abs() > 1e-9 are all hardcoded in function code. VALUE_EPSILON (already imported on line 21) fits den.abs() < 1e-12 directly; the minimum-sample-count threshold and the b0 significance floor should be named constants in src/constants.rs.

Suggested refactor
-    if count < 10 || den.abs() < 1e-12 {
+    if count < ESO_B0_MIN_OLS_SAMPLES || den.abs() < VALUE_EPSILON {
         return None;
     }
     let b0 = num / den;
-    if b0.is_finite() && b0.abs() > 1e-9 {
+    if b0.is_finite() && b0 > ESO_B0_ESTIMATE_MIN_POSITIVE {
         Some(b0)
     } else {
         None
     }

Then in src/constants.rs:

/// Minimum high-excitation samples required for OLS b0 estimation.
pub const ESO_B0_MIN_OLS_SAMPLES: usize = 10;
/// Minimum |b0| to accept as a meaningful estimate (rejects ~0 and ill-conditioned fits).
pub const ESO_B0_ESTIMATE_MIN_POSITIVE: f64 = 1e-9;

As per coding guidelines: "All constants go in src/constants.rs — no hardcoded magic numbers in function code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/eso.rs` around lines 229 - 233, Replace the hardcoded magic numbers in
the estimate_b0 function: replace `count < 10` with a new constant
ESO_B0_MIN_OLS_SAMPLES, replace `den.abs() < 1e-12` to use the already-imported
VALUE_EPSILON, and replace `b0.abs() > 1e-9` with a new constant
ESO_B0_ESTIMATE_MIN_POSITIVE; add the two new constants to src/constants.rs
(names as above with the suggested values: 10 and 1e-9), update the estimate_b0
function to reference these constants, and add any necessary use/import so the
constants are visible where estimate_b0 is defined.
src/constants.rs (1)

262-276: LGTM — new ESO constants are well-documented and consistent with file conventions.

Small consideration: ESO_OMEGA0_MAX = 500.0 rad/s (≈80 Hz) will be the binding upper bound for any loop rate ≥ ~1.5 kHz, since eso.rs uses min(sample_rate/3, config.omega0_max). For 4–8 kHz logs the discrete-stability cap would otherwise allow ~1300–2660 rad/s; consider raising ESO_OMEGA0_MAX (or documenting the intentional conservative ceiling) if users are expected to get stuck at the boundary on fast loops.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/constants.rs` around lines 262 - 276, The ESO_OMEGA0_MAX constant
(ESO_OMEGA0_MAX = 500.0) may unintentionally bind the observer bandwidth for
high sample rates because eso.rs clamps omega0 to min(sample_rate/3,
config.omega0_max); update the constant or docs: either increase ESO_OMEGA0_MAX
to a higher value appropriate for target high-rate logs (e.g., to accommodate
sample_rate/3 for 4–8 kHz) or add a clear comment near ESO_OMEGA0_MAX explaining
the intentional conservative ceiling and linking to the min(sample_rate/3,
config.omega0_max) behavior in eso.rs so users won’t be surprised when they hit
the bound.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/eso.rs`:
- Around line 323-332: The code currently detects a user-supplied b0 by
comparing config.b0 to ESO_DEFAULT_B0 with a magic epsilon; instead add an
explicit boolean flag (e.g., b0_user_override) to EsoConfig (or PlotConfig) that
the CLI sets when --eso-b0 is provided, then change the branching here to check
config.b0_user_override instead of the floating-point comparison, preserving the
existing fallback to estimate_b0(&omega_meas, &pid_sum, ts) when the flag is
false and still returning (config.b0, false) when the flag is true.
- Around line 211-238: The function estimate_b0 currently accepts negative b0
values despite the docstring saying non-positive estimates should return None;
update the final guard in estimate_b0 to enforce a strictly positive estimate
(e.g., replace the b0.abs() check with b0 > 1e-9 while keeping b0.is_finite()),
so negative b0 is rejected and None returned; ensure the logic still returns
None when count < 10 or den is effectively zero (existing checks on count and
den remain), and keep or update the docstring to reflect this strict positivity
requirement; relevant symbols: function estimate_b0, variable b0, den, count,
and ESO_B0_MIN_CONTROL_THRESHOLD.

---

Nitpick comments:
In `@src/constants.rs`:
- Around line 262-276: The ESO_OMEGA0_MAX constant (ESO_OMEGA0_MAX = 500.0) may
unintentionally bind the observer bandwidth for high sample rates because eso.rs
clamps omega0 to min(sample_rate/3, config.omega0_max); update the constant or
docs: either increase ESO_OMEGA0_MAX to a higher value appropriate for target
high-rate logs (e.g., to accommodate sample_rate/3 for 4–8 kHz) or add a clear
comment near ESO_OMEGA0_MAX explaining the intentional conservative ceiling and
linking to the min(sample_rate/3, config.omega0_max) behavior in eso.rs so users
won’t be surprised when they hit the bound.

In `@src/eso.rs`:
- Around line 229-233: Replace the hardcoded magic numbers in the estimate_b0
function: replace `count < 10` with a new constant ESO_B0_MIN_OLS_SAMPLES,
replace `den.abs() < 1e-12` to use the already-imported VALUE_EPSILON, and
replace `b0.abs() > 1e-9` with a new constant ESO_B0_ESTIMATE_MIN_POSITIVE; add
the two new constants to src/constants.rs (names as above with the suggested
values: 10 and 1e-9), update the estimate_b0 function to reference these
constants, and add any necessary use/import so the constants are visible where
estimate_b0 is defined.

In `@src/main.rs`:
- Line 385: The CLI help string for "--eso-axis" currently lists only
"roll,pitch,yaw" but the parsing code also accepts "all" and numeric aliases
(0,1,2); update the help text and the related error message to document the
supported tokens (e.g., "roll,pitch,yaw,all (or 0,1,2)") so the CLI and parser
match. Locate the help string literal containing "--eso-axis" and the parser
branch that handles axis tokens (the parsing logic around the "all"/numeric
aliases) and make their user-facing text consistent.

In `@src/report.rs`:
- Line 184: Replace the redundant range expression "0..AXIS_NAMES.len().min(3)"
with "0..AXIS_COUNT" in the loop over axis_idx; ensure AXIS_COUNT is imported
alongside AXIS_NAMES (and prefer using axis_name() for human-readable labels if
needed) so the loop uses the compile-time axis count rather than a hardcoded 3.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 63a172d8-e215-4544-b783-1d8959e256ca

📥 Commits

Reviewing files that changed from the base of the PR and between 31a14f3 and 6e600ca.

📒 Files selected for processing (6)
  • OVERVIEW.md
  • src/constants.rs
  • src/eso.rs
  • src/main.rs
  • src/plot_functions/plot_eso.rs
  • src/report.rs

Comment thread src/eso.rs Outdated
Comment thread src/eso.rs
- src/eso.rs: fix estimate_b0 doc/behavior mismatch and magic numbers
  * replace 'count < 10' with ESO_B0_MIN_OLS_SAMPLES constant
  * replace 'den.abs() < 1e-12' with VALUE_EPSILON (already imported)
  * replace 'b0.abs() > 1e-9' with 'b0 > ESO_B0_ESTIMATE_MIN_POSITIVE'
    to enforce strictly positive estimates (negative b0 = inverted sign
    convention, was accepted silently despite docstring saying otherwise)
  * update docstring to reflect strict positivity requirement

- src/eso.rs + src/main.rs: replace fragile float epsilon b0 detection
  * add b0_user_override: bool to EsoConfig
  * add eso_b0_user_override: bool to PlotConfig
  * --eso-b0 CLI flag now sets eso_b0_user_override = true
  * run_eso_optimization branches on config.b0_user_override instead of
    (config.b0 - ESO_DEFAULT_B0).abs() < 1e-12 so an explicit
    '--eso-b0 1.0' is respected rather than silently overridden by OLS

- src/constants.rs: add two new constants for estimate_b0 thresholds
  * ESO_B0_MIN_OLS_SAMPLES: usize = 10
  * ESO_B0_ESTIMATE_MIN_POSITIVE: f64 = 1e-9
  * add explanatory comment on ESO_OMEGA0_MAX conservative ceiling
    (links to min(sample_rate/3, config.omega0_max) clamping behavior)

- src/report.rs: replace AXIS_NAMES.len().min(3) with AXIS_COUNT
  * imports AXIS_COUNT from axis_names alongside AXIS_NAMES
  * eliminates redundant .min(3) magic number

- src/main.rs: fix --eso-axis help text and error message
  * help now lists 'roll,pitch,yaw,all (or 0,1,2)'
  * error message lists 'roll, pitch, yaw, all, or 0, 1, 2'
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
src/main.rs (2)

55-55: Use AXIS_COUNT for the eso_axes array size.

The eso_axes field is declared as [bool; 3] at Lines 55, and initialized as [true; 3] / [false; 3] at Lines 80, 107, 1311, and 1328. Per the centralized axis-naming system already used elsewhere in this file (e.g., crate::axis_names::AXIS_NAMES), prefer [bool; AXIS_COUNT] and [_; AXIS_COUNT] so axis-count changes don't require touching every declaration.

As per coding guidelines: "Axis indices (0=Roll, 1=Pitch, 2=Yaw) must use the src/axis_names.rs module with AXIS_COUNT, AXIS_NAMES, and axis_name() function instead of creating redundant axis index constants".

Proposed fix
+use crate::axis_names::AXIS_COUNT;
@@
-    pub eso_axes: [bool; 3],
+    pub eso_axes: [bool; AXIS_COUNT],
@@
-            eso_axes: [true; 3],
+            eso_axes: [true; AXIS_COUNT],
@@
-            eso_axes: [true; 3],
+            eso_axes: [true; AXIS_COUNT],
@@
-            let mut axes = [false; 3];
+            let mut axes = [false; AXIS_COUNT];
@@
-                    "all" => {
-                        axes = [true; 3];
+                    "all" => {
+                        axes = [true; AXIS_COUNT];

Also applies to: 80-80, 107-107, 1311-1311, 1328-1328

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` at line 55, The eso_axes field and its initializations use a
hard-coded length of 3; change the field declaration from "pub eso_axes: [bool;
3]" to use the centralized axis count (e.g., "pub eso_axes: [bool; AXIS_COUNT]")
and replace all array initializers "[true; 3]" and "[false; 3]" with "[true;
AXIS_COUNT]" and "[false; AXIS_COUNT]" respectively, importing or referencing
AXIS_COUNT from crate::axis_names (or axis_names::AXIS_COUNT) so the code uses
the shared axis sizing used elsewhere.

1356-1367: Fragile save/restore pattern for ESO/report settings.

This manual save-and-restore of five eso_* / run_report fields around PlotConfig::none() is easy to break when new eso_* fields are added later (any forgotten field would silently regress to its none() default). Consider either:

  1. Adding an impl PlotConfig { fn disable_plots(&mut self) { /* zeroes only plot_* flags */ } } method that leaves run_eso, run_report, eso_b0, eso_b0_user_override, and eso_axes untouched, or
  2. Grouping the ESO/report fields into a sub-struct so a single field assignment preserves all of them.
Example (option 1)
-        // Preserve ESO/report settings through the plot-type reset
-        let saved_run_eso = plot_config.run_eso;
-        let saved_run_report = plot_config.run_report;
-        let saved_eso_b0 = plot_config.eso_b0;
-        let saved_eso_b0_user_override = plot_config.eso_b0_user_override;
-        let saved_eso_axes = plot_config.eso_axes;
-        plot_config = PlotConfig::none();
-        plot_config.run_eso = saved_run_eso;
-        plot_config.run_report = saved_run_report;
-        plot_config.eso_b0 = saved_eso_b0;
-        plot_config.eso_b0_user_override = saved_eso_b0_user_override;
-        plot_config.eso_axes = saved_eso_axes;
+        plot_config.disable_all_plots(); // preserves run_eso/run_report/eso_* fields
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 1356 - 1367, Replace the fragile manual
save/restore around PlotConfig::none() by adding a PlotConfig method that clears
plot-related flags but preserves ESO/report fields: implement e.g. impl
PlotConfig { fn disable_plots(&mut self) { /* reset plot_* flags and other
non-ESO report state, but do NOT touch run_eso, run_report, eso_b0,
eso_b0_user_override, eso_axes */ } } and then replace the save/restore block
with plot_config.disable_plots(); alternatively, group the ESO/report fields
into a sub-struct on PlotConfig so you can assign PlotConfig::none() while
preserving that single sub-struct (e.g. let saved = plot_config.eso_report;
plot_config = PlotConfig::none(); plot_config.eso_report = saved;).
src/eso.rs (1)

316-325: Consider naming the sample_rate / 3.0 stability factor.

The divisor 3.0 (used both for the upper-bound computation at Line 317 and the error threshold at Line 322) encodes the LESO discrete-time stability ratio. Extracting it to a named constant (e.g., ESO_OMEGA0_STABILITY_RATIO = 3.0) in src/constants.rs would eliminate the magic number and keep the two related usages in sync if the ratio ever needs to change.

As per coding guidelines: "All constants go in src/constants.rs — no hardcoded magic numbers in function code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/eso.rs` around lines 316 - 325, Replace the hardcoded stability divisor
3.0 with a named constant: add ESO_OMEGA0_STABILITY_RATIO = 3.0 to
src/constants.rs and import it into src/eso.rs, then compute omega0_max_stable
as (sample_rate / ESO_OMEGA0_STABILITY_RATIO).min(config.omega0_max) and use
ESO_OMEGA0_STABILITY_RATIO when computing the error threshold (config.omega0_min
* ESO_OMEGA0_STABILITY_RATIO) so both uses (omega0_max_stable and the error
message computation) reference the single constant instead of the magic number.
src/report.rs (2)

124-124: Use AXIS_COUNT in the eso_results parameter type.

The parameter eso_results: &[Option<EsoResult>; 3] hardcodes the axis count as 3. For consistency with the centralized axis-naming system already imported on Line 10 and used on Line 184, prefer &[Option<EsoResult>; AXIS_COUNT]. (The corresponding call site at src/main.rs Line 1112 would benefit from the same change.)

As per coding guidelines: "Axis indices (0=Roll, 1=Pitch, 2=Yaw) must use the src/axis_names.rs module with AXIS_COUNT, AXIS_NAMES, and axis_name() function instead of creating redundant axis index constants".

Proposed change
-    eso_results: &[Option<EsoResult>; 3],
+    eso_results: &[Option<EsoResult>; AXIS_COUNT],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/report.rs` at line 124, The parameter type eso_results:
&[Option<EsoResult>; 3] hardcodes the axis count; change it to use the
centralized constant by replacing the array size with AXIS_COUNT (i.e.,
&[Option<EsoResult>; AXIS_COUNT]) and update any call sites that construct or
pass that fixed-size array to match the new type; ensure you import or reference
AXIS_COUNT from the axis_names module consistent with other uses (e.g., where
axis_name()/AXIS_NAMES are used).

33-34: Document population vs. sample std-dev choice.

var divides by count (population variance), not count - 1 (sample variance). For signal RMS/statistics of a full recorded time-series this is appropriate, but readers of a "statistical report" often expect the sample form. Consider adding a brief doc note on SignalStats::std_dev or on compute_signal_stats clarifying the convention so the markdown values aren't interpreted ambiguously.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/report.rs` around lines 33 - 34, The code currently computes variance as
population variance (dividing by count) in compute_signal_stats (var = ... /
count as f64) and exposes std_dev via SignalStats::std_dev; add a brief doc
comment on either SignalStats::std_dev and/or compute_signal_stats stating
explicitly that the implementation uses the population (N) variance convention
(not the sample N-1 estimator), and update any generated markdown/report text to
call out the convention so readers won't misinterpret the reported RMS/std-dev
values; if desired, mention the alternative (sample variance using count - 1)
and where to change the computation to switch conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/eso.rs`:
- Around line 316-325: Replace the hardcoded stability divisor 3.0 with a named
constant: add ESO_OMEGA0_STABILITY_RATIO = 3.0 to src/constants.rs and import it
into src/eso.rs, then compute omega0_max_stable as (sample_rate /
ESO_OMEGA0_STABILITY_RATIO).min(config.omega0_max) and use
ESO_OMEGA0_STABILITY_RATIO when computing the error threshold (config.omega0_min
* ESO_OMEGA0_STABILITY_RATIO) so both uses (omega0_max_stable and the error
message computation) reference the single constant instead of the magic number.

In `@src/main.rs`:
- Line 55: The eso_axes field and its initializations use a hard-coded length of
3; change the field declaration from "pub eso_axes: [bool; 3]" to use the
centralized axis count (e.g., "pub eso_axes: [bool; AXIS_COUNT]") and replace
all array initializers "[true; 3]" and "[false; 3]" with "[true; AXIS_COUNT]"
and "[false; AXIS_COUNT]" respectively, importing or referencing AXIS_COUNT from
crate::axis_names (or axis_names::AXIS_COUNT) so the code uses the shared axis
sizing used elsewhere.
- Around line 1356-1367: Replace the fragile manual save/restore around
PlotConfig::none() by adding a PlotConfig method that clears plot-related flags
but preserves ESO/report fields: implement e.g. impl PlotConfig { fn
disable_plots(&mut self) { /* reset plot_* flags and other non-ESO report state,
but do NOT touch run_eso, run_report, eso_b0, eso_b0_user_override, eso_axes */
} } and then replace the save/restore block with plot_config.disable_plots();
alternatively, group the ESO/report fields into a sub-struct on PlotConfig so
you can assign PlotConfig::none() while preserving that single sub-struct (e.g.
let saved = plot_config.eso_report; plot_config = PlotConfig::none();
plot_config.eso_report = saved;).

In `@src/report.rs`:
- Line 124: The parameter type eso_results: &[Option<EsoResult>; 3] hardcodes
the axis count; change it to use the centralized constant by replacing the array
size with AXIS_COUNT (i.e., &[Option<EsoResult>; AXIS_COUNT]) and update any
call sites that construct or pass that fixed-size array to match the new type;
ensure you import or reference AXIS_COUNT from the axis_names module consistent
with other uses (e.g., where axis_name()/AXIS_NAMES are used).
- Around line 33-34: The code currently computes variance as population variance
(dividing by count) in compute_signal_stats (var = ... / count as f64) and
exposes std_dev via SignalStats::std_dev; add a brief doc comment on either
SignalStats::std_dev and/or compute_signal_stats stating explicitly that the
implementation uses the population (N) variance convention (not the sample N-1
estimator), and update any generated markdown/report text to call out the
convention so readers won't misinterpret the reported RMS/std-dev values; if
desired, mention the alternative (sample variance using count - 1) and where to
change the computation to switch conventions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f2bf4684-96db-40eb-b134-a77d7f272849

📥 Commits

Reviewing files that changed from the base of the PR and between 6e600ca and 287d515.

📒 Files selected for processing (4)
  • src/constants.rs
  • src/eso.rs
  • src/main.rs
  • src/report.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/constants.rs

- src/constants.rs: add ESO_OMEGA0_STABILITY_RATIO = 3.0
  * named constant for the LESO discrete-time stability divisor
    (omega_0 < sample_rate / ESO_OMEGA0_STABILITY_RATIO)

- src/eso.rs: replace hardcoded 3.0 stability divisor with constant
  * omega0_max_stable = (sample_rate / ESO_OMEGA0_STABILITY_RATIO).min(...)
  * error threshold = config.omega0_min * ESO_OMEGA0_STABILITY_RATIO
  * import ESO_OMEGA0_STABILITY_RATIO from constants

- src/main.rs: replace all [bool; 3] / [true; 3] / [false; 3] with AXIS_COUNT
  * PlotConfig::eso_axes: [bool; AXIS_COUNT]
  * Default / none() initializers: [true; AXIS_COUNT]
  * --eso-axis parser: [false; AXIS_COUNT] and [true; AXIS_COUNT] for 'all'
  * eso_results declaration: [Option<eso::EsoResult>; AXIS_COUNT]
  * import crate::axis_names::AXIS_COUNT at top level

- src/main.rs: replace fragile save/restore with PlotConfig::disable_plots()
  * add disable_plots(&mut self) method that zeroes all plot-type flags
    but leaves run_eso, run_report, eso_b0, eso_b0_user_override, eso_axes
    untouched — prevents future ESO field additions from silently regressing
  * remove now-unused PlotConfig::none() (was only called in the
    save/restore block)
  * replace the 9-line save/restore block with plot_config.disable_plots()

- src/report.rs: change eso_results parameter type from [_; 3] to [_; AXIS_COUNT]

- src/report.rs: document population-variance convention on compute_signal_stats
  * add doc comment stating N (not N-1) divisor is intentional for a
    complete time-series and showing how to switch to sample variance
@nerdCopter
Copy link
Copy Markdown
Owner Author

@coderabbitai, all nitpicks resolved? full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

@nerdCopter Sure! I'll kick off a full review of PR #136 now and will post detailed findings shortly.


🧠 Learnings used
Learnt from: nerdCopter
Repo: nerdCopter/BlackBox_CSV_Render PR: 55
File: LICENSE_COMMERCIAL:1-7
Timestamp: 2025-09-10T13:56:40.607Z
Learning: nerdCopter prefers to use minimal commercial license templates that are consistent across his projects, similar to what was used in the bbl_parser repository PR `#2`.

Learnt from: nerdCopter
Repo: nerdCopter/BlackBox_CSV_Render PR: 18
File: src/data_analysis/calc_step_response.rs:0-0
Timestamp: 2025-06-03T18:01:54.978Z
Learning: The user nerdCopter prefers to defer unit testing implementation when suggested for the BlackBox_CSV_Render project.

Learnt from: nerdCopter
Repo: nerdCopter/BlackBox_CSV_Render PR: 0
File: :0-0
Timestamp: 2026-01-08T14:48:17.022Z
Learning: For the BlackBox_CSV_Render project, Bode plot transfer function estimation (PR `#118`) is mathematically correct but fundamentally incompatible with normal operational flight logs due to low coherence (requires controlled system-identification inputs like chirp/PRBS on secured craft). Normal flights produce unreliable results due to nonlinearities, closed-loop conditions, and nonstationary maneuvers. The feature should be opt-in only (via --bode flag) with documentation clarifying it's for formal system-identification experiments, not routine flight log analysis. Existing PSD/spectrum plots are the appropriate tool for operational flight logs.

Learnt from: nerdCopter
Repo: nerdCopter/BlackBox_CSV_Render PR: 134
File: src/main.rs:0-0
Timestamp: 2026-04-02T13:13:52.459Z
Learning: In the BlackBox_CSV_Render project, the preferred user-facing description for the --bode CLI flag is: "Bode plot analysis (requires chirp/sweep system-id test flight, not normal logs)." — this wording is preferred over "system-identification test flights only" because it explicitly mentions chirp/sweep and is more immediately understandable to FPV pilots. Both src/main.rs (CLI help) and README.md must use identical wording for this flag.

Learnt from: nerdCopter
Repo: nerdCopter/BlackBox_CSV_Render PR: 0
File: :0-0
Timestamp: 2025-08-31T20:17:05.654Z
Learning: The user nerdCopter successfully implemented a minimal, targeted fix for replacing "Axis 0/1/2" labels with "Roll/Pitch/Yaw" by using a simple axis_names array lookup in main.rs and plot_framework.rs, avoiding over-engineering and scope creep.

Learnt from: nerdCopter
Repo: nerdCopter/BlackBox_CSV_Render PR: 0
File: :0-0
Timestamp: 2026-01-14T21:56:11.756Z
Learning: RC command and setpoint smoothness analysis (time-domain metrics like derivative, jerk, total variation, and spectral energy) is fundamentally different from Bode plot transfer function estimation and IS VALID for operational Betaflight flight logs. Unlike Bode plots which require controlled system-identification inputs, setpoint smoothness analysis works with normal flight data by measuring command quality through time-domain and spectral metrics without estimating transfer functions. The existing plot_setpoint_derivative.rs only visualizes raw derivatives and does not calculate quantitative smoothness metrics.
✅ Actions performed

Full review triggered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant