Skip to content

Commit 8ec381c

Browse files
harshaboseclaude
andcommitted
Analysis C from the AIRBRAKE exit: Q_TRANS_DECEL searched, ArduPilot Z loop, overshoot and sink flags (C spec 5-10)
restricted.py: run_c / evaluate_c / capability_ceiling / optimise_c on C_BOUNDS (T_bt, theta_A, spin_min, q). q places the stopping point and is the cascade limit; an overshoot past os_tol_m and an end sink beyond sink_tol_ms are failures. The old scopes stay, marked legacy. stages.py: exit_params shared by D and C; task_c writes C_params, C_search, C_runs, C_q_curve, C_sens. settings.py: CCfg (a_plan_source removed and rejected with a message). assemble.py: the C section. configs, READMEs and the frozen placeholder study.yaml follow. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018QKS75nmeebgQ4roJCLSfM
1 parent fa63073 commit 8ec381c

8 files changed

Lines changed: 618 additions & 95 deletions

File tree

‎configs/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,8 @@ recommendation. Add scenarios by adding lines; refer to them by name in `study.y
235235
| `D` (more) | Entry is the AIRBRAKE exit at `max(AIRSPEED_MIN - 2, q_assist_speed_ms)` (`entry_convention` `tas`/`eas`). `ff_mode` (`lift_aware` is an oracle at tau 0 and scale 1), `theta_fw0_deg` (placeholder), `pit_lim_mode`, `enforce_alpha` (report-only checks always run), `alpha_gate_ms`, `sink_tol_ms`, `rotor_lag_s`, `v0_eas_ms` (diagnostic grid), `sens` (sensitivity sweeps), `recommendation` (`ensemble_n` a power of two, `percentile`, `corners`, `n_boot`, `design_ground_speed_ms` inside the curve or null), `curve.ground_speeds_ms` (the `D_a_rec_curve` speeds, each above the 1 m/s end speed), `trade`. `E.a_plan_fallback_ms2` is used when D gives no recommendation |
236236
| `Q2`, `E` (more) | `Q2` also takes `soc`, `conventions` (`tas`, `eas`: how `AIRSPEED_MIN` is read) and `feed_convention` (the one E reads), `isa_offset_k`, `n_speed`, `write_profile`. `E.a_fwd_mode` is `case` (Q2 at E's own site), `worst` (the Q2 grid) or `mission` (`a_fwd` in `mission.yaml`); `E.a_fwd_metric` is `min` or `equiv` |
237237
| `E` (sites) | `E.sites_amsl_m` (airfield elevations; the cruise altitude is the site plus `mission.profile.climb_height_m`) and `E.missions` (`round_trip`, `one_way`). The range is an output; `ranges_km` and the requirements are report points. `mission.yaml` no longer holds `cruise_amsl_m`, `ground_amsl_m`, `v_climb` or `v_glide` (omit the last two to use the cruise speed) and gains `climb_height_m`, `v_cruise_ref_amsl_m`, `payload_kg`, `t_drop_s` and `battery_window.soc_start` |
238-
| `C`, `B2` | ensemble size and seed, search seeds, population, iterations (`C` also `a_plan_source`) |
238+
| `C` | analysis C from the AIRBRAKE exit (entry from the `D` block). Ensemble size and seed, search `seeds`, `popsize`, `maxiter`; `rotor_lag_s` (0.25, D's lumped lag); `rotor_start` (`idle` is O-13, unverified, or `carry`); `os_tol_m` and `sink_tol_ms` (the overshoot and end-sink tolerances that make a run fail; PROPOSED, and the overshoot tolerance decides the answer); `weights` (the normalisers `h_ref`, `E_ref`, `x_ref`); `bounds` for `T_bt`, `theta_A_deg`, `spin_min` and `q` (`Q_TRANS_DECEL`, a searched variable); `z` (ArduPilot's vertical-loop gains); `q_curve` and `sens` (the grids of `C_q_curve` and `C_sens`). `a_plan_source` was removed and is rejected |
239+
| `B2` | ensemble size and seed, search seeds, population, iterations |
239240
| `B1` | scenarios, the altitude bands to sweep, the distance cap, the collocation mesh |
240241
| `sensitivity` | the scenario, the two Morris sizes, and whether to include the energy budget |
241242

‎configs/study.yaml‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,33 @@ Q2: # forward acceleration of the pusher at TKOFF_THR_MAX
8787
conventions: [tas, eas] # AIRSPEED_MIN read as true or equivalent airspeed (DEC-1 open): both reported
8888
feed_convention: tas # the one E reads (the register compares AIRSPEED_MIN as TAS)
8989

90-
C: # ArduPilot-parameter optimisation (back-transition)
90+
C: # ArduPilot parameters from the AIRBRAKE exit (docs/C_fix_spec.md)
91+
# The entry (theta_fw0_deg, pit_lim_mode, q_assist_speed_ms, entry_convention) comes from the D block.
92+
# Four variables are searched: Q_BACKTRANS_MS, Q_A_ANGLE_MAX, Q_M_SPIN_MIN and Q_TRANS_DECEL (q).
9193
ensemble_n: 32
9294
ensemble_seed: 8 # same ensemble as B2 so the two can be compared
9395
seeds: [1, 2, 3, 4, 5, 6, 7, 8]
9496
popsize: 8
9597
maxiter: 20
96-
a_plan_source: simulated # simulated | ideal
98+
rotor_lag_s: 0.25 # D's lumped actuator lag (D spec section 14); C's own value
99+
rotor_start: idle # idle | carry: rotors at the AIRBRAKE exit; idle is O-13, UNVERIFIED (SITL or log)
100+
os_tol_m: 2.0 # overshoot past x_tgt that counts as a failure (PROPOSED; decides the answer)
101+
sink_tol_ms: 1.76 # end sink rate that counts as sinking (PROPOSED: low end of F's descent cap)
102+
weights: {h_ref: 2.0, E_ref: 15000.0, x_ref: 100.0} # normalisers; weights 1, 0.3, 0.5 and penalty 10 stay
103+
bounds: # search bounds [low, high]
104+
T_bt: [1.0, 6.0]
105+
theta_A_deg: [10.0, 30.0] # 30 is DEC-7 and binding
106+
spin_min: [0.06, 0.25]
107+
q: [1.0, 3.0] # Q_TRANS_DECEL, m/s^2
108+
z: # ArduPilot vertical loop (O-12): master 4.8.0-dev Plane defaults, not a pinned tag (O-08)
109+
{pos_p: 1.0, vel_p: 5.0, acc_p: 0.03, acc_i: 0.1, acc_imax: 0.8, vmax: 2.5, vel_flt_hz: 5.0, acc_flt_hz: 10.0}
110+
q_curve: [1.0, 1.5, 2.0, 2.25, 2.5, 2.75, 3.0] # C_q_curve: the optimum's ramp with q varied
111+
sens: # C_sens: one at a time at the optimum
112+
rotor_lag_s: [0, 0.25, 0.5]
113+
rotor_start: [idle, carry]
114+
z_vel_p: [2.5, 5, 10]
115+
z_acc_p: [0.015, 0.03, 0.06]
116+
theta_fw0_deg: [0, 8]
97117

98118
B2: # speed-scheduled law, started from a B1 solution
99119
ensemble_n: 32

‎src/sagetrans/analysis/README.md‎

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -573,45 +573,98 @@ register value and the result is very sensitive to it. The lift share is E's rul
573573

574574
**Question.** What can the autopilot be *configured* to do, and how far is that from the bound?
575575

576-
Decision vector, with bounds:
576+
**Entry.** C starts at the AIRBRAKE exit, like D: the same entry state (`braking.airbrake_entry_state`),
577+
the same entry speed rule (`braking.entry_airspeed`, `max(AIRSPEED_MIN - 2, Q_ASSIST_SPEED)`) and
578+
t = 0 at the exit. `theta_fw0_deg`, `pit_lim_mode`, `q_assist_speed_ms` and `entry_convention` are
579+
read from the **D block** (they describe the exit, not C); `io.stages.exit_params` builds the
580+
controller parameters for both so they cannot drift. There is no forward transition, no cruise and
581+
no planner simulation. The position cascade runs (`brake_at_limit = False`); the at-limit run is
582+
used only to report a ceiling (below).
583+
584+
**Four variables** (`restricted.C_BOUNDS`, widened or narrowed in `C.bounds`):
577585

578586
| Name | ArduPilot parameter | Range |
579587
| --- | --- | --- |
580588
| `T_bt` | `Q_BACKTRANS_MS` | 1–6 s |
581-
| `theta_A` | `Q_A_ANGLE_MAX` | 10°–30° |
589+
| `theta_A` | `Q_A_ANGLE_MAX` | 10°–30° (30° is DEC-7 and binding) |
582590
| `spin_min` | `Q_M_SPIN_MIN` | 0.06–0.25 |
583-
| `airspeed_min` (full scope) | `AIRSPEED_MIN` | 11–18 m/s |
584-
| `q_transition` (full scope) | `Q_TRANSITION_MS` | 2–8 s |
585-
| `tkoff_thr_max` (full scope) | `TKOFF_THR_MAX` | 0.5–1.0 |
586-
587-
**Two scopes.** `back` evaluates the back-transition only from an idealised level cruise (directly
588-
comparable with B2). `full` flies the whole sequence from hover (`evaluate_full`): forward
589-
transition, cruise stub, back-transition, with the cost taken as the worst excursion of the two
590-
transitions, summed energy, and the back-transition distance.
591-
592-
**Not in the decision vector:** `PTCH_LIM_MAX_DEG` (fixed at 35° so `Q_A_ANGLE_MAX` binds),
593-
`Q_TRANS_FAIL` (off), `BATT_WATT_MAX` (not modelled). **`Q_TRANS_DECEL` is not decided by the
594-
search; it comes from analysis D.** `plan_decel(vps, scen, prm, x0, v_f, source)` sets it inside every
595-
evaluation, as the proposal asks (§10C step 4). With `a_plan_source="simulated"` (the default) it is
596-
the worst-case `a_eq` that `braking.measure_a_eq` simulates **for the same parameters, scenario and
597-
entry state**, so the planner is self-consistent with what the aircraft can do. `"ideal"` uses the
598-
ideal-pitch planner (`braking.ideal_stopping`) instead: no extra simulation, but it ignores wing
599-
drag, lift and pitch lag, so it is not a bound on the simulated figure in either direction (at 22 m/s
600-
with default parameters the two differ: 2.35 simulated against 2.42 ideal m/s²). An earlier version
601-
used the ideal planner only; this was changed after review. The simulated source roughly doubles the
602-
cost of an evaluation.
603-
604-
`optimise(vp, scens, scope, weights, seeds, popsize, maxiter, a_plan_source)` runs CMA-ES from a
605-
mid-range default and returns the best parameters, the cost, the default's cost, per-scenario
606-
metrics and the search. `evaluate_back` also takes `controller_overrides` (used by the sensitivity
607-
analysis to vary controller gains; they are applied to the planning run as well as the main run).
608-
609-
**What it found (placeholder, 4-scenario ensemble, simulated `Q_TRANS_DECEL`).** The cost fell from
610-
2.09 to 2.00. The optimiser drove `T_bt` and `spin_min` toward their lower bounds; the altitude
611-
excursion stayed near 12–14 m, i.e. the parameters the autopilot offers barely move it. **Three
612-
seeds agreed to a spread of 0.10 %** (T-12 satisfied for C; 2e-5 with the earlier ideal planner). A
613-
3-seed run took about 5 minutes. Record `result.search.spread` in the run record as
614-
`search_spreads={"C": …}` so the release gate can check it.
591+
| `q` | `Q_TRANS_DECEL` | 1.0–3.0 m/s² |
592+
593+
**`q` does both jobs it does in ArduPilot.** It places the stopping point, `x_tgt = V_ground² /
594+
(2 q)` (O-02), and it is the cascade's velocity-profile acceleration limit `a_lim = q` (O-11). The
595+
old split (a planner run placing the target, a fixed `a_lim` braking along it) is gone. Left alone the
596+
search would pick a `q` above what the pitch ramp can deliver, so two flags are added to the usual
597+
failures (cost penalty 10 each): **`overshoot`** (the run stopped more than `C.os_tol_m` past
598+
`x_tgt`) and **`sinking`** (the end climb rate is below `-C.sink_tol_ms`). Both tolerances are
599+
proposals, and **the overshoot tolerance decides the answer**: the optimum sits on it. The ramp is
600+
time-driven, so what it can deliver depends on the ground speed; `capability_ceiling` runs the same
601+
case with the pitch at the ramp limit and reports `a_cap = (V_ground² - 1) / (2 x_end)`, D's `a_eq`
602+
under the same Z loop and lag, once per ensemble member at the optimum. `C_runs` has `a_cap_ms2` and
603+
`q_over_a_cap` so `q` can be read against D's `a_rec` curve (`D_a_rec_curve`) by eye; C's `q` does
604+
not feed E, which keeps taking D's recommendation.
605+
606+
**The vertical loop is ArduPilot's structure with ArduPilot's default values** (`z_mode =
607+
"ardupilot"`, open question O-12; the repo's `K_h`, `K_vh`, `K_i` hold stays the dataclass default
608+
for every other caller): position P → velocity P → an acceleration PID on the *measured*
609+
vertical acceleration, giving a normalised thrust that is turned into a rotor command through a
610+
thrust table (`LiftFF.thrust_for_delta`) with tilt compensation. It has no lift or hover
611+
feed-forward, so `ff_mode` is ignored. Its equivalent velocity gain is about 0.16 to 0.18 throttle
612+
per m/s, eight to nine times the repo hold's `K_vh` (0.02). The defaults are ArduPilot master (4.8.0-dev)
613+
Plane values read on 2026-10-27, **not a pinned tag** (O-08). It is a mapping, not ArduPilot:
614+
`thrust_norm` is linear in rotor thrust (no `MOT_THST_EXPO`), the acceleration is the exact vertical
615+
acceleration of the state (no noise, bias or delay), and the integrator starts at the entry thrust.
616+
`C_sens` shows the sensitivity to the two gains that matter (`z_vel_p`, `z_acc_p`).
617+
618+
**Rotors at the exit.** `C.rotor_start = "idle"` starts the rotors at the thrust of
619+
`Q_M_SPIN_MIN` (O-13: ArduPilot relaxes the Z controller in APPROACH and AIRBRAKE, so the throttle has
620+
decayed towards idle by the time POSITION1 starts). **O-13 is read from source and not checked in SITL
621+
or a log**; `"carry"` (D's assumption, W − L) is the sensitivity. The result barely depends on it.
622+
`braking.airbrake_entry_state(..., rotor_start=)` and `braking.entry_rotor_thrust` implement both;
623+
D's default stays `"carry"`.
624+
625+
**Rotor lag.** `C.rotor_lag_s` (0.25 s) is D's one lumped actuator lag (D, section 14), C's own value
626+
because the Z loop is fast.
627+
628+
**Cost.** (10.1) with the normalisers `h_ref` 2 m, `E_ref` 15 kJ, `x_ref` 100 m (`C.weights`; the old
629+
10 m / 5 kJ / 150 m make a sub-metre excursion irrelevant). The weights (1, 0.3, 0.5) and the
630+
penalty 10 are unchanged. `ensemble.ensemble_cost` and `RunMetrics` are used as they are.
631+
632+
**Search.** `optimise_c(vp, scens, cev, base_bp, weights, start, bounds=…, seeds, popsize, maxiter)`
633+
is CMA-ES from the register values with `q = 2`. `result.at_bound` names every variable within 10 %
634+
of its range of a bound (`theta_A` and `q` are the ones to watch). Evaluations are serial (CasADi
635+
functions are not picklable across processes) and cached per vector; one ensemble evaluation of 32 runs
636+
takes 5 to 14 s, so the shipped search (8 seeds, `popsize` 8, `maxiter` 20) is a few hours on one core.
637+
Record `result.search.spread` as `search_spreads={"C": …}` so the release gate can check it.
638+
639+
**Tables.**
640+
641+
| Table | Content |
642+
| --- | --- |
643+
| `C_params` | the optimum and the register values at `q = 2`: the four variables, `J`, `J_start`, seed spread, `at_bound`, p95 excursion, mean energy (kJ), p95 distance, max overshoot, flag counts |
644+
| `C_search` | per seed: best cost, evaluations |
645+
| `C_runs` | per ensemble member at the optimum: inputs, `x_tgt`, `x_end`, overshoot, energy, excursion, end sink rate, flags, `a_cap_ms2`, `q_over_a_cap`, and D's `braking_flags` (alpha above the 0.8 C_L,max band, VRS ratio; reporting only, not in `J`) |
646+
| `C_q_curve` | the optimum's `T_bt`, `theta_A`, `spin_min` with `q` varied (`C.q_curve`): the energy-against-distance curve, from which any overshoot tolerance can be read |
647+
| `C_sens` | one at a time at the optimum: `rotor_lag_s`, `rotor_start`, `z_vel_p`, `z_acc_p`, `theta_fw0_deg` |
648+
649+
**What is no longer optimised:** `AIRSPEED_MIN`, `Q_TRANSITION_MS` and `TKOFF_THR_MAX` (Q2 and E
650+
cover part of the forward side; nothing optimises `AIRSPEED_MIN` now, and if it comes back it is a
651+
fifth variable of this search, with the entry speed following it). Also not in the vector:
652+
`PTCH_LIM_MAX_DEG` (fixed at 35°), `Q_TRANS_FAIL` (off), `BATT_WATT_MAX` (not modelled).
653+
654+
**Legacy.** `evaluate_back`, `plan_decel`, `evaluate_full`, `optimise`, `BACK_BOUNDS`, `FULL_BOUNDS`,
655+
`DEFAULT_PARAMS` and `THETA_P` are untouched and marked legacy in their docstrings.
656+
`analysis/sensitivity.py` and `analysis/collocation.py` still call `evaluate_back`, `plan_decel`,
657+
`DEFAULT_PARAMS` and `THETA_P`; their migration is a separate step. **B and C are not comparable yet**:
658+
the B-to-C comparison (B-9: B2 2.341 against patched C 2.256) stays on the old level-cruise entry until
659+
B1 and B2 start at the AIRBRAKE exit. C's `x` is the distance from the AIRBRAKE exit to the stop; it
660+
leaves out the APPROACH/AIRBRAKE leg and the `2 * V_closing` term of the switch test (O-02), which belong
661+
to E's budget.
662+
663+
**Limits.** The end state is only a sink-rate check (DEC-5 lean, 1.76 m/s): there is no landing-error
664+
criterion (DEC-4) and no check on position or heading at the stop. The entry state still has
665+
placeholders (`theta_fw0_deg` = 0; `AIRSPEED_MIN` read as a true airspeed, DEC-1). `theta_A` against its
666+
30° bound is a stalled-wing brake as in D: read D's alpha and VRS flags next to it, and do not read the
667+
result as a recommendation to fly at 30°.
615668

616669
---
617670

0 commit comments

Comments
 (0)