Skip to content

Commit ccaad92

Browse files
nerdCopterclaude
andcommitted
feat: ESO gain optimization with argmin GSS and at_ceiling marker
Add 2nd-order LESO (Linear Extended State Observer) bandwidth optimizer: - src/eso.rs: golden-section search via argmin over omega_0 ∈ [ESO_OMEGA0_MIN, min(sample_rate/ESO_OMEGA0_STABILITY_RATIO, ESO_OMEGA0_MAX)]. N-step-ahead open-loop prediction cost (unimodal: low omega_0 = stale f_hat, high = noise). b0 auto-estimated via OLS on rate derivatives (QuickFlash method); falls back to ESO_DEFAULT_B0 when too few high-excitation samples. at_ceiling: bool set when omega0_opt >= omega0_max_stable - ESO_GSS_TOLERANCE. - src/plot_functions/plot_eso.rs: stacked time-domain ESO output plot per axis (omega_meas blue, omega_hat orange, f_hat green scaled to ESO_FHAT_Y_FRACTION). Legend shows [at ceiling] when optimizer hit its stability-constrained bound. - src/constants.rs: ESO_OMEGA0_MIN/MAX, GSS_TOLERANCE, GSS_MAX_ITER, DEFAULT_B0, N_AHEAD_STEPS, WARMUP_FRACTION, B0_* thresholds, OMEGA0_STABILITY_RATIO, FHAT_Y_FRACTION, COLOR_ESO_*. - src/main.rs: --eso flag (eso_requested bool); --eso-b0 sets b0 with eso_b0_user_override; plot_config.run_eso/eso_b0/eso_b0_user_override applied after arg loop; --eso alone triggers PlotConfig::none() so only ESO output is generated. - Cargo.toml/Cargo.lock: add argmin 0.11 dependency. - OVERVIEW.md: ESO section and TOC entry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0d47da9 commit ccaad92

10 files changed

Lines changed: 835 additions & 11 deletions

File tree

Cargo.lock

Lines changed: 126 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ ndarray = "0.15"
1515
ndarray-stats = "0.5"
1616
colorous = "1.0.16"
1717
rusttype = "0.9"
18+
argmin = "0.11"
1819

1920
[build-dependencies]
2021
anyhow = "1.0"

OVERVIEW.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- [Implementation Details](#implementation-details)
1010
- [Filter Response Curves](#filter-response-curves)
1111
- [Bode Plot Analysis (Optional)](#bode-plot-analysis-optional)
12+
- [ESO Gain Optimization (Optional)](#eso-gain-optimization-optional)
1213
- [Optimal P Estimation (Optional, Experimental)](#optimal-p-estimation-optional-experimental)
1314
- [Step-Response Comparison with Other Analysis Tools](#step-response-comparison-with-other-analysis-tools)
1415
- [Compared to PIDtoolbox/Matlab (PTstepcalc.m)](#compared-to-pidtoolboxmatlab-ptstepcalcm)
@@ -27,6 +28,7 @@ All analysis parameters, thresholds, plot dimensions, and algorithmic constants
2728
* Additional options include `--help` and `--version` for user assistance.
2829
* The `--output-dir` parameter now requires a directory path when specified. If omitted, plots are saved in the source folder (input file's directory).
2930
* Handles multiple input files and determines if a directory prefix should be added to output filenames to avoid collisions when processing files from different directories.
31+
* **ESO flags:** `--eso` enables 2nd-order LESO bandwidth optimization; `--eso-b0 <value>` sets control effectiveness (default: 1.0).
3032

3133
2. **File Processing (`src/main.rs:process_file`):**
3234
* For each input CSV:
@@ -163,7 +165,24 @@ All analysis parameters, thresholds, plot dimensions, and algorithmic constants
163165
* **Limitations:** Normal operational flight logs produce low coherence due to nonlinearities, closed-loop feedback, and nonstationary maneuvers. Results in such cases are unreliable and not recommended for tuning decisions.
164166
* **Warning:** A runtime warning is displayed when `--bode` is used to inform users of these requirements and recommend spectrum analysis for normal flights.
165167

166-
### Output and Tuning Recommendations
168+
### ESO Gain Optimization (Optional)
169+
170+
* **Purpose:** Offline system identification of 2nd-order LESO (Linear Extended State Observer) bandwidth (omega_0) from recorded flight data. Finds observer gains that minimise tracking error against measured gyro rate.
171+
* **Activation:** Disabled by default; enable with `--eso`. Set control effectiveness with `--eso-b0 <value>`.
172+
* **Algorithm (`src/eso.rs`):**
173+
* Extracts filtered gyro (omega) and PID sum (P+I+D+F) per axis as measured output and control input respectively.
174+
* Simulates a discrete Euler-forward 2nd-order LESO at each candidate omega_0:
175+
* `e = omega_meas[k] - omega_hat`
176+
* `omega_hat += Ts * (f_hat + b0 * u[k] + beta1 * e)`
177+
* `f_hat += Ts * (beta2 * e)`
178+
* Bandwidth parameterisation (Gao 2003): `beta1 = 2*omega_0`, `beta2 = omega_0^2`.
179+
* Minimises MSE(omega_hat, omega_meas) via golden-section search over `[ESO_OMEGA0_MIN, min(sample_rate/3, ESO_OMEGA0_MAX)]`.
180+
* **Stability constraint:** omega_0 < sample_rate / 3 (enforced automatically).
181+
* **Output:** Prints optimal omega_0, beta1, beta2, and MSE per axis to console.
182+
* **Limitations:** `b0=1.0` (default) is dimensionless. For absolute accuracy co-tune b0 using known frame inertia. The cost function is MSE on the closed-loop observer output; unimodality is assumed over the search range.
183+
184+
185+
167186

168187
#### Generated PNG Plots
169188

src/constants.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,29 @@ pub const PSD_EPSILON: f64 = 1e-12; // Guard against division by zero for PSD va
273273
pub const MAGNITUDE_PLOT_MARGIN_DB: f64 = 10.0; // Padding above/below magnitude data for plot range
274274
pub const PHASE_PLOT_MARGIN_DEG: f64 = 30.0; // Padding above/below phase data for plot range
275275

276+
// ESO (Extended State Observer) optimization constants
277+
pub const ESO_OMEGA0_MIN: f64 = 50.0; // Lower bound for observer bandwidth search (rad/s)
278+
// Conservative ceiling kept intentionally below sample_rate/3 for typical 1–2 kHz logs.
279+
// At ≥4 kHz the discrete-stability cap (sample_rate/3) would allow ~1300–2660 rad/s, but
280+
// empirical tuning shows gains above ~500 rad/s rarely improve MSE and amplify noise.
281+
// Override per-run with --eso-b0 or raise this if higher bandwidths are needed.
282+
pub const ESO_OMEGA0_MAX: f64 = 500.0; // Upper bound for observer bandwidth search (rad/s); conservative ceiling (~80 Hz) — even at high loop rates where sample_rate/3 would allow more, this cap avoids instability in noisy logs
283+
pub const ESO_GSS_TOLERANCE: f64 = 0.01; // Golden-section search convergence tolerance (rad/s)
284+
pub const ESO_GSS_MAX_ITER: u64 = 100; // Maximum iterations for golden-section search
285+
pub const ESO_DEFAULT_B0: f64 = 1.0; // Default control effectiveness (dimensionless)
286+
pub const ESO_N_AHEAD_STEPS: usize = 5; // Steps ahead for open-loop prediction cost (unimodal objective)
287+
pub const ESO_WARMUP_FRACTION: f64 = 0.20; // Fraction of data used for observer spin-up before cost evaluation
288+
pub const ESO_B0_MIN_CONTROL_THRESHOLD: f64 = 10.0; // Minimum |PID sum| to include a sample in b0 OLS estimation
289+
pub const ESO_B0_MIN_OLS_SAMPLES: usize = 10; // Minimum high-excitation samples required for OLS b0 estimation
290+
pub const ESO_B0_ESTIMATE_MIN_POSITIVE: f64 = 1e-9; // Minimum strictly-positive b0 to accept (rejects ~0 and negative estimates)
291+
pub const ESO_OMEGA0_STABILITY_RATIO: f64 = 3.0; // LESO discrete-time stability divisor: omega_0 < sample_rate / ESO_OMEGA0_STABILITY_RATIO
292+
pub const ESO_FHAT_Y_FRACTION: f64 = 0.5; // f_hat is scaled to fill this fraction of the Y half-range in the ESO plot
293+
294+
// ESO output plot colors
295+
pub const COLOR_ESO_MEAS: &RGBColor = &LIGHTBLUE; // Measured gyro rate
296+
pub const COLOR_ESO_HAT: &RGBColor = &ORANGE; // ESO estimated rate (omega_hat)
297+
pub const COLOR_ESO_FHAT: &RGBColor = &GREEN; // ESO disturbance estimate (f_hat, scaled)
298+
276299
// High-frequency noise analysis for P headroom estimation
277300
// D-term energy above this frequency threshold indicates noise constraints
278301
pub const DTERM_HF_CUTOFF_HZ: f64 = 200.0; // Frequency above which high-frequency noise is measured

src/data_input/log_parser.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ pub fn parse_log_file(input_file_path: &Path, debug_mode: bool) -> LogParseResul
455455
.and_then(|(_, v)| v.parse::<u32>().ok());
456456
match debug_mode_val {
457457
Some(DEBUG_MODE_GYRO_SCALED) => {
458+
// GYRO_SCALED: debug[0-2] contains raw unfiltered gyro
458459
println!(
459460
" ⚠️ Using debug[0-2] as gyroUnfilt fallback (debug_mode=GYRO_SCALED)"
460461
);

0 commit comments

Comments
 (0)