Skip to content

Commit b49ef81

Browse files
authored
Merge pull request #8 from harshabose/feat/b1-airbrake-exit
Analysis B1 from the AIRBRAKE exit: stated wing limits, energy-against-distance front, gap to C
2 parents 3b037e8 + bc8ccdc commit b49ef81

9 files changed

Lines changed: 780 additions & 109 deletions

File tree

‎configs/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ recommendation. Add scenarios by adding lines; refer to them by name in `study.y
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` |
238238
| `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 |
239239
| `B2` | ensemble size and seed, search seeds, population, iterations |
240-
| `B1` | scenarios, the altitude bands to sweep, the distance cap, the collocation mesh |
240+
| `B1` | B1 from the AIRBRAKE exit (entry rotor start and sink tolerance from `C`, entry speed rule from `D`). `scenarios`, the collocation mesh (`intervals`, `refine_from`, `max_iter`), `eps_h_m` (the headline altitude band, a list), `eps_x_m` (the distance caps swept: the front), `end_vh_band_ms` (null = `C.sink_tol_ms`), `variants` (the stated wing limits: each has `name`, exactly one of `alpha_margin` or `alpha_max_deg`, and `theta_cmd_max_deg`; there is no "no limit" variant), `band_sweep` and `sens` (sensitivities), and `gap_c` (the C parameters the same-problem gap uses; null = the register values at q = 2). A scalar `eps_x_m` (the old format) is rejected |
241241
| `sensitivity` | the scenario, the two Morris sizes, and whether to include the energy budget |
242242

243243
Effort knobs worth knowing: `C.maxiter`, `B2.maxiter`, `B1.intervals` and `sensitivity.r_large`

‎configs/study.yaml‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,21 @@ B2: # speed-scheduled law, started from a B1 solution
124124
sigma0: 0.1
125125
start: {scenario_index: 0, eps_h_m: 10, eps_x_m: 300}
126126

127-
B1: # open-loop optimum by direct collocation
127+
B1: # open-loop least-energy back-transition from the AIRBRAKE exit (docs/B1_fix_spec.md)
128+
# The entry (rotor start, sink tolerance) comes from the C block, the entry speed rule from the D block.
128129
scenarios: [S-01, S-02, S-03, S-04, S-05, S-06]
129-
eps_h_m: [20, 10, 6, 3]
130-
eps_x_m: 300
131130
intervals: 40
132131
refine_from: [20]
133132
max_iter: 800
133+
eps_h_m: [1.0] # headline altitude band (C holds about 0.5 to 1 m)
134+
eps_x_m: [40, 50, 60, 75, 100, 300] # distance caps swept: the energy-against-distance front
135+
end_vh_band_ms: null # null = C.sink_tol_ms
136+
variants: # the wing limit is stated, never silent; no "no limit" variant
137+
- {name: attached, alpha_margin: 0.85, theta_cmd_max_deg: 34.4} # |alpha_w| <= 0.85 alpha_s
138+
- {name: c_envelope, alpha_max_deg: 30.0, theta_cmd_max_deg: 30.0} # what C's ramp can reach
139+
band_sweep: {eps_h_m: [3.0, 10.0], variant: c_envelope} # sensitivity at the largest distance cap
140+
sens: {rotor_start_flip: true, end_vh_band_ms: 0.5, c_envelope_theta_cmd_deg: 34.4} # first scenario only
141+
gap_c: null # C parameters for the gap: {T_bt, theta_A_deg, spin_min, q}; null = register at q = 2
134142

135143
sensitivity:
136144
scenario: S-01 # the scenario the sensitivity is run in

‎docs/B1_fix_spec.md‎

Lines changed: 191 additions & 0 deletions
Large diffs are not rendered by default.

‎src/sagetrans/analysis/README.md‎

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -707,8 +707,35 @@ most 1 %** (see `io/README.md`).
707707

708708
## B1 — open-loop optimum by direct collocation (`collocation.py`)
709709

710-
**Question.** The best trajectory — the physical bound — for one scenario, as a Pareto surface of
711-
energy, altitude excursion and distance.
710+
**Question.** What is the least bus energy for the AIRBRAKE-exit-to-hover problem, as a function of
711+
distance, under a **stated wing limit**? It is the energy-against-distance front to set beside C's
712+
`C_q_curve`. It is not "the shape of the schedule" and not a strict lower bound (B-2).
713+
714+
**Entry.** B1 starts at the AIRBRAKE exit, like D and C: `make_problem_exit` builds the problem from
715+
`braking.airbrake_entry_state` (all nine states fixed to it, the rotor speed included; `prespin_max`
716+
stays 0), at the entry speed of D's rule `max(AIRSPEED_MIN - 2, Q_ASSIST_SPEED)`. The rotor start
717+
(`carry` or `idle`) and the end sink tolerance are read from the **C block**, the entry-speed rule and
718+
`theta_fw0` from the **D block**; the B1 block does not repeat them. The old level-cruise problem
719+
(`make_problem`, 22 m/s) is kept for B2, which still starts from it (`stages._b1_problem_cruise`), so
720+
**B2 and B1 are not comparable yet** and B-9 stays open until B2 migrates. `with_options` now keeps the
721+
problem's entry state (it used to rebuild a cruise problem).
722+
723+
**The wing limit is stated, never silent.** Two variants are run and labelled in every table:
724+
`attached` (|alpha_w| <= 0.85 alpha_s, pitch command <= 34.4 deg: today's constraint) and `c_envelope`
725+
(|alpha_w| <= 30 deg, pitch command <= 30 deg: what C's ramp can reach, `theta_A` <= 30 deg).
726+
`CollocationOptions.alpha_max` replaces `stall_margin x alpha_s` when set. There is **no "no limit"
727+
variant**: without one the solver puts the wing at about 51 deg on the blended post-stall model
728+
(eqs. 6.2 to 6.5), which is not credible without validation. **The wing limit decides the number**: S-01
729+
at a 1 m band costs 8.9 kJ over 73 m with `attached`, about 8.2 kJ over 40 m with `c_envelope`, and
730+
about 7.1 kJ with no limit at all. `attached` cannot go much below 73 m at that band; a point that does
731+
not converge from the three starts is kept in the table with its energy blank, and it means the solver
732+
failed, not that the point is infeasible.
733+
734+
**The band.** The headline altitude band is 1 m (C holds 0.5 to 1 m with the ArduPilot Z loop), symmetric.
735+
A 10 m band is an energy store: B1 dives to gain speed and zoom-climbs while braking, which says nothing
736+
about the autopilot. 3 m and 10 m are a sensitivity (`B1_sens`). The end band on `|v_h|` is C's sink
737+
tolerance (1.76 m/s), plus a 0.5 m/s sensitivity row; B1 bounds climb as well as sink, where C flags only
738+
sink, so B1 can be very slightly less cheap than a sink-only bound.
712739

713740
**Formulation.** Hermite–Simpson collocation of the single CasADi vehicle (with smoothing), free final
714741
time, solved with IPOPT through CasADi's `Opti`. States are the 9 vehicle states; the unknowns are the
@@ -717,12 +744,15 @@ rotor-throttle and pitch-command histories and the final time. The pusher stays
717744
```
718745
minimise E = integral V_bus I_bus dt (+ a small control-rate regulariser)
719746
subject to dynamics (HS defects), |h - h_ref| <= eps_h at nodes AND midpoints,
720-
ground distance <= eps_x, vx(T_f) = V_f, |vh(T_f)| <= 0.5,
721-
0 <= delta_r <= 0.95, |theta_cmd| <= 0.6, throttle and pitch slew limits,
747+
ground distance <= eps_x, vx(T_f) = V_f, |vh(T_f)| <= vh_band (C's sink tol),
748+
0 <= delta_r <= 0.95, |theta_cmd| <= theta_cmd_max, throttle and pitch slew limits,
722749
V_bus >= V_min, |vh| <= 3 m/s, rotor speed >= 0,
723-
stall margin |alpha_w| <= 0.85 alpha_s while V >= V_s (nodes AND midpoints)
750+
wing limit |alpha_w| <= alpha_max (or 0.85 alpha_s) while V >= V_s (nodes AND midpoints)
724751
```
725752

753+
There is no equivalent of C's `rotor_lag_s`: B1's rotor state and its throttle slew limit (5 per s) play
754+
that role, so the gap to C includes that difference.
755+
726756
The second objective, `"excursion"`, minimises the maximum altitude deviation instead (the
727757
`epsilon_h*` of the Pareto front).
728758

@@ -763,18 +793,46 @@ the epsilon-constraint front this way; `min_excursion` is the least reliable mod
763793
excursion-objective solution once reported 0.0 m at the nodes while the replay showed several metres)
764794
and its result must be checked via `Verified.verified`.
765795

766-
### Comparing with the baseline, C and B2
767-
768-
`bound_check(prob)` solves B1 at the baseline's own excursion and distance **under B1's operational
769-
limits** and reports the energy margin; `compare(prob, b1, c_params, b2_params)` tabulates B1, C and B2.
796+
### Comparing with C and B2
797+
798+
**`bound_check_c(prob, c)`** solves B1 at C's own excursion and distance (`eps_h = max(c.excursion x
799+
1.02, 0.3)`, `eps_x = c.distance x 1.02`, objective energy, the three starts of `solve_verified`) and
800+
reports `energy_margin = (C energy - B1 energy) / C energy`. `c` is `restricted.evaluate_c` for the same
801+
scenario. Read it as the **same-problem gap**: B1 is clairvoyant (it knows the scenario exactly), open
802+
loop (no disturbance) and has no rotor lag, so the gap is an upper bound on what the structure of the
803+
ArduPilot controller costs. It is one scenario and one variant at a time (`B1_gap` has a row per
804+
variant). Each B1 task is an independent process, so it cannot read C's optimum from the C task: the gap
805+
evaluates C at `B1.gap_c` (paste the optimum from `C_params`), or, if that is null, at the register
806+
parameters with `q` = 2, and the table says which. `bound_check` (against the 20 deg / 3 s guess
807+
controller with the repo altitude hold, B-1) stays as legacy. `compare(prob, b1, c_params, b2_params)`
808+
tabulates B1, C (`evaluate_c`) and B2; the B2 row is at the level-cruise entry and is not comparable.
809+
810+
`baseline_run` and `baseline_guess` and the three `(theta_A, T_bt)` starts of `solve_verified` only
811+
generate initial guesses; they are not a baseline for comparison.
812+
813+
**Outputs.** `B1_front` (per scenario, variant and distance cap: converged, replay-verified, attempts
814+
converged of 3, energy, excursion, distance, `t_f`, entry speeds, `below_vs`, replay errors, the shape
815+
of the replayed schedule from `schedule_summary` (when the rotors come in, `V / V_s` then, peak wing
816+
angle, peaks), D's VRS ratio and alpha-above-band time (reporting only), `J_point` = C's cost of the
817+
point with C's normalisers, and `j_best` marking the least per scenario and variant), `B1_gap`,
818+
`B1_sens` (the 3 m and 10 m bands on every scenario; the other rotor start, the 0.5 m/s end band and
819+
the 34.4 deg pitch limit on the first) and `B1_schedule` (the replay of the `j_best` point of the first
820+
scenario, per variant, every fifth sample). A point that is not replay-verified has its energy blank.
821+
`V_0` at 5000 m is below the 1g stall speed (S-02: 17.4 m/s), where the wing limit is relaxed by the
822+
smooth gate; `below_vs` says so. VRS is only reported, not a constraint (B-3).
823+
824+
**Cost.** 60 to 105 s per converged point (three starts, two meshes) and about 100 solves with the
825+
shipped block (2 to 3 hours on one core). B1 tasks run in parallel across scenarios; the exact-Hessian
826+
solves are memory hungry (eleven at once ran out of memory with `std::bad_alloc` on a 24-core
827+
workstation), so limit `parallel.max_workers` if memory is short.
770828

771829
**B1 is *not* a strict lower bound.** It keeps limits the baseline and B2 do not respect (`|vh|`, stall
772830
margin, slew limits), so a controller that breaks them can be cheaper at the same excursion — and
773831
tuned B2 was, marginally. Relaxing the limits makes the NLP much harder (none of three retries
774832
converged). The claim that *can* be made is one-sided: if B1 under its tighter limits already beats the
775833
baseline, the true bound is at least that good.
776834

777-
**What it found (placeholder, 22 m/s, sea level).**
835+
**What it found (placeholder, level-cruise entry at 22 m/s, sea level; the legacy problem B2 starts from).**
778836

779837
| Case | Excursion | Energy | Distance |
780838
| --- | --- | --- | --- |

0 commit comments

Comments
 (0)