Skip to content

Commit 7e95c1e

Browse files
authored
Merge pull request #10 from harshabose/feat/ft-defaults-outputs
FT forward-transition study, ArduPilot defaults, results viewer, review follow-ups
2 parents 7b4fa10 + d88d69a commit 7e95c1e

74 files changed

Lines changed: 10197 additions & 257 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
runs-on: ubuntu-latest
88
strategy:
99
matrix:
10-
python-version: ["3.11", "3.13"]
10+
python-version: ["3.13"]
1111
steps:
1212
- uses: actions/checkout@v4
1313
- uses: actions/setup-python@v5

‎PROJECT_REPORT.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ sagetrans/
8585
proposal.md the design (one equation edited, see 5.4)
8686
pyproject.toml dependencies, ruff, mypy --strict, import-linter layering contract
8787
configs/vehicle.yaml the parameter register (5 TBD entries)
88-
.github/workflows/ci.yml ruff, mypy, lint-imports, pytest on 3.11 and 3.13
88+
.github/workflows/ci.yml ruff, mypy, lint-imports, pytest on 3.13
8989
src/sagetrans/ 4,693 lines in 34 files
9090
README.md overview, conventions, atmosphere
9191
atmosphere.py ISA

‎README.md‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ whole workflow: no code changes.
3232
| `configs/controller.yaml` | the ArduPilot parameters (real names) and the assumed controller gains |
3333
| `configs/scenarios.yaml` | the named scenarios and the ensemble ranges |
3434
| `configs/study.yaml` | which analyses to run, effort, draft or release mode, parallelism |
35+
| `configs/ardupilot_defaults.yaml` | optional: the values ArduPilot ships with, used only as a reference column and a simulated comparison case |
3536

3637
Every data entry has a value, a unit (any unit; converted once on load), a source and an evidence
3738
status (`measured`, `datasheet`, `derived`, `documented`, `stated`, `assumed`, `TBD`). The report shows
@@ -52,6 +53,7 @@ how much of each result rests on assumptions. **`configs/README.md` documents ev
5253
| **Q2** | can the pusher reach the transition speed at altitude, against mass? |
5354
| **C** | what can the autopilot be *configured* to do? (ArduPilot-parameter optimisation) |
5455
| **B1** | what is the best open-loop trajectory? (direct collocation, energy against altitude excursion) |
56+
| **FT** | which `AIRSPEED_MIN`, `Q_TRANSITION_MS`, `TKOFF_THR_MAX` and `Q_TRAN_PIT_MAX` take it from hover to wing-borne flight without losing altitude or stalling? |
5557
| sensitivity | which inputs drive each result, and how firmly do we know them? |
5658

5759
## Command line
@@ -62,13 +64,42 @@ python run.py run everything configured
6264
python run.py --only A D E just these analyses
6365
python run.py --mode release refuse unless nothing is TBD or missing
6466
python run.py --force ignore cached results
67+
python run.py --fresh-cache delete the task cache of this run, then compute everything
6568
python run.py --serial one process (easier to debug)
6669
python run.py --verify also run the test suite and feed the release gate
70+
python run.py --view [FOLDER] open existing results as report.html (+ tables.xlsx); runs nothing
6771
```
6872

73+
`--view` is a viewer: it reads a results folder that already exists (default: `output_dir/run_name` of
74+
`study.yaml`), writes `report.html` and `tables.xlsx` next to the tables and opens the page
75+
(`--no-open` to skip that). With no results in the folder it writes and opens nothing and exits 2.
76+
6977
Results are cached per task and keyed by a hash of every result-relevant input, so an unchanged re-run
7078
only rebuilds the report and changing one value only re-runs what depends on it.
7179

80+
## Acceptance run
81+
82+
A full study from nothing, then the viewer. From a fresh clone (the commands are the same in the
83+
working copy):
84+
85+
```bash
86+
git clone <repo> sagetrans-accept && cd sagetrans-accept
87+
git checkout feat/ft-defaults-outputs # the branch under test
88+
python -m venv .venv && . .venv/bin/activate # Windows (Git Bash): . .venv/Scripts/activate
89+
pip install -e ".[dev]"
90+
python run.py --fresh-cache # clean cache, every analysis (hours: FT is the long part)
91+
python run.py --view # report.html and tables.xlsx into results/study, then open
92+
pytest && ruff check . && mypy && lint-imports # the checks the gate reads
93+
```
94+
95+
`--fresh-cache` deletes `<output_dir>/<run_name>/_work` first, so nothing is served from an earlier
96+
run; `--force` only ignores the entries it finds. The cache is also keyed by the source of the analysis
97+
code (`run_record.json` has `source_hash`) and by the versions of Python and the numerical libraries
98+
(`library_versions`, pinned in `pyproject.toml`), so neither a code change nor a library change serves an old
99+
result. Add
100+
`--workers N` to choose the number of processes; B1 solves are memory-heavy, so keep it at 3 or fewer
101+
on a small machine when running `--only B1`.
102+
72103
## More
73104

74105
* **[`PROJECT_REPORT.md`](PROJECT_REPORT.md)** — what was built, what happened, what the results mean,
@@ -78,6 +109,6 @@ only rebuilds the report and changing one value only re-runs what depends on it.
78109
* **[`tests/README.md`](tests/README.md)** — the verification tests (T-01 to T-15).
79110

80111
```bash
81-
pytest # the test suite (about 3 minutes)
112+
pytest # the test suite (about 5 minutes)
82113
ruff check . && mypy && lint-imports
83114
```

‎configs/README.md‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ them with real data as you get it.
2121
| `controller.yaml` | register | the ArduPilot parameters (by their real names) and the assumed controller gains |
2222
| `scenarios.yaml` | plain | the named scenarios (S-01…S-06) and the ensemble ranges |
2323
| `study.yaml` | plain | which analyses to run, how much effort, run name, draft/release mode, parallelism |
24+
| `ardupilot_defaults.yaml` | register (optional) | the values ArduPilot ships with (Plane-4.7.2), same names and units as `controller.yaml`; a reference only, never an input (docs/DEFAULTS_spec.md) |
2425

2526
**Register files** are the *data*: every entry carries a value, a unit, a source and an evidence
2627
status, and the toolkit tracks how much of every result rests on assumptions. **Plain files** are
@@ -188,19 +189,18 @@ a measured table is not supported yet (see "Not supported yet").
188189
| `descent.kappa` | - | 0.25 | vortex-ring fraction of induced velocity. **Unsourced: must come from a sourced boundary (Johnson, NASA).** Analysis F refuses to run without it |
189190
| `descent.v_fragility`, `v_sensor`, `h_slow`, `v_final` | m/s, m/s, m, m/s | 3, *omitted*, 5, 0.5 | payload-fragility and sensor limits; the final slow descent |
190191
| `requirements.range_primary_km`, `range_p95_km` | km | 100, 113 | range requirements the budget is checked against |
191-
| `speeds.entry_eas_ms` | m/s | 22 | equivalent airspeed at the start of the back-transition |
192192

193193
## `controller.yaml` reference
194194

195195
The `ardupilot` group uses the **real parameter names**.
196196

197197
| Entry | Unit | Placeholder | Meaning |
198198
| --- | --- | --- | --- |
199-
| `AIRSPEED_MIN` | m/s | 14 | transition speed. The simulated controller compares *true* airspeed; whether ArduPilot compares EAS or TAS is open (O-02) |
199+
| `AIRSPEED_MIN` | m/s | 14 | transition speed, read as an **equivalent** airspeed by D, C, B1 and FT (`D.entry_convention: eas`; ArduPilot compares EAS, O-14 and O-23). E reads it the same way (`Q2.feed_convention: eas`) |
200200
| `Q_TRANSITION_MS` | ms | 5000 | rotor fade time after `AIRSPEED_MIN` (documented default) |
201201
| `Q_BACKTRANS_MS` | ms | 3000 | back-transition pitch-limit ramp |
202202
| `Q_A_ANGLE_MAX` | deg | 20 | maximum VTOL lean angle |
203-
| `PTCH_LIM_MAX_DEG` | deg | 35 | fixed-wing pitch limit (fixed in analysis C, not searched) |
203+
| `PTCH_LIM_MAX_DEG` | deg | 20 | the ArduPilot default (`PITCH_MAX` 20, Plane-4.7.2); fixed in analysis C, not searched. The pitch cap is `min(Q_A_ANGLE_MAX, PTCH_LIM_MAX_DEG)`, so a `Q_A_ANGLE_MAX` above this does nothing |
204204
| `Q_BCK_PIT_LIM` | deg | *omitted* | back-transition pitch cap; omitted = none |
205205
| `TKOFF_THR_MAX` | - | 0.7 | pusher throttle in AUTO during the forward transition |
206206
| `Q_TRANS_FAIL` | ms | 0 | stuck-transition timeout; 0 disables it |
@@ -232,11 +232,12 @@ recommendation. Add scenarios by adding lines; refer to them by name in `study.y
232232
| `analyses` | which of `A D F E Q2 C B1 sensitivity` to run; `--only` overrides it |
233233
| `switch_test` | `ground` or `air`: the speed used in the switch-point test (O-02) |
234234
| `A`, `D`, `F`, `E`, `Q2` | grids and scenario lists for the quick analyses. `A` also takes `alpha_min_deg` and `cl_frac`: the wing angle-of-attack band the feasibility scan must stay inside (lower end of the lift fit; the angle where C_L = `cl_frac` x C_L,max, provisional because C_L,max is), and `vrs_enforce` (default `false`: the rotor V_c / v_h ratio is only reported; `true`: also infeasible when it is below `-descent.kappa`) |
235-
| `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 |
236-
| `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` |
235+
| `D` (more) | Entry is the AIRBRAKE exit at `AIRSPEED_MIN - 2` (`entry_convention` `tas`/`eas`; `Q_ASSIST_SPEED` is not modelled, the study assumes it is 0, O-20). `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 |
236+
| `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` (with `min` the wait is a constant worst-case acceleration and overstates the wait at altitude); `E.fade_wing_share` (0.4, ASSUMED) is the wing's share of the weight at the hand-over, taken from FT's simulated hand-overs, so E's rotor fade is not an independent check of FT |
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+
| `FT` | analysis FT, the forward transition from hover (docs/FT_spec.md). The start is `mission.profile.clearance_agl_m` above the site; the accepted altitude loss and gain are `mission.profile.fwd_alt_loss_max_m` and `fwd_alt_gain_max_m` (status `assumed`). Ensemble size, seed and `headwind_ms` range; search `popsize` (the seeds and iterations are under `scan` and `grid`); `rotor_lag_s`; `t_end_s` and `t_settle_s`; `tecs_start` (`warm` or `cold`: does TECS run in the VTOL hover? open); `stall_margin` (EAS over the stall speed from the hand-over on, 1.2); `rec_cl_frac`; `proj_is_failure`; `weights`; `bounds` for `AIRSPEED_MIN` (EAS), `Q_TRANSITION_MS`, `TKOFF_THR_MAX` and `Q_TRAN_PIT_MAX`; `scan`, `history_scenarios` and `sens`. The new `controller.yaml` entries (`Q_TRAN_PIT_MAX`, `TKOFF_THR_MIN`, `TRIM_THROTTLE`, `THR_MIN`, `THR_MAX`, `THR_SLEWRATE`, `PTCH_LIM_MIN_DEG` and the `tecs` group) are ArduPilot defaults with status `TBD` |
238239
| `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-
| `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 |
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` (`null` = the register's `PTCH_LIM_MAX_DEG`, which is what `c_envelope` uses, so it follows the register); 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 |
240241
| `sensitivity` | the scenario, the two Morris sizes, and whether to include the energy budget |
241242

242243
Effort knobs worth knowing: `C.maxiter`, `B1.intervals` and `sensitivity.r_large`
@@ -281,5 +282,6 @@ Exit codes: `0` ok, `1` a task failed, `2` the inputs are unusable (or release m
281282
Table-driven maps (CSV over J and μ) and a measured open-circuit-voltage / equivalent-circuit table
282283
need B-spline interpolants in the model and are the next step; the register already accepts `file:`
283284
entries, but nothing reads them.
284-
* **A forward-transition optimisation** and a simulated forward transition in the energy budget.
285+
* **A simulated forward transition in the energy budget.** E uses a quasi-steady estimate; the forward
286+
transition is simulated and optimised by analysis FT, which does not feed E (E's `a_fwd` is not wired to it).
285287
* **Per-scenario vehicle changes** other than mass (a hot-day derating of the motors, say).

‎configs/ardupilot_defaults.yaml‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# ArduPilot factory defaults of the parameters this study decides or assumes. REFERENCE ONLY
2+
# (docs/DEFAULTS_spec.md): never a recommendation, never used as an input. Same format and status
3+
# words as controller.yaml. Every entry was read from the ArduPilot source at tag Plane-4.7.2 on
4+
# 2026-10-27. Names that no analysis decides (THR_SLEWRATE, the TECS_* group, PTCH_LIM_MIN_DEG,
5+
# PTCH_LIM_MAX_DEG) are not listed: their register values in controller.yaml already are the
6+
# defaults.
7+
#
8+
# Two entries store a "use another parameter" value as ArduPilot does and are resolved by
9+
# inputs.resolve_default: TKOFF_THR_MAX 0 means THR_MAX; Q_A_ANGLE_MAX is the attitude controller's
10+
# ANGLE_MAX, whose default is 30 deg.
11+
12+
ardupilot:
13+
AIRSPEED_MIN: {value: 9.0, unit: "m/s", source: "AIRSPEED_FBW_MIN 9 in ArduPlane/config.h (the default of AIRSPEED_MIN in Parameters.cpp), Plane-4.7.2; the same value as Plane-4.6.3", status: documented}
14+
Q_TRANSITION_MS: {value: 5000.0, unit: "ms", source: "quadplane.cpp AP_GROUPINFO TRANSITION_MS, Plane-4.7.2", status: documented}
15+
Q_TRAN_PIT_MAX: {value: 3.0, unit: "deg", source: "quadplane.cpp AP_GROUPINFO TRAN_PIT_MAX, Plane-4.7.2", status: documented}
16+
TKOFF_THR_MAX: {value: 0.0, unit: "-", source: "Parameters.cpp TKOFF_THR_MAX, Plane-4.7.2; 0 means THR_MAX is used (O-17)", status: documented}
17+
Q_BACKTRANS_MS: {value: 3000.0, unit: "ms", source: "quadplane.cpp AP_GROUPINFO BACKTRANS_MS, Plane-4.7.2", status: documented}
18+
Q_TRANS_DECEL: {value: 2.0, unit: "m/s^2", source: "quadplane.cpp AP_GROUPINFO TRANS_DECEL, Plane-4.7.2", status: documented}
19+
Q_BCK_PIT_LIM: {value: 10.0, unit: "deg", source: "quadplane.cpp AP_GROUPINFO BCK_PIT_LIM, Plane-4.7.2", status: documented}
20+
Q_M_SPIN_MIN: {value: 0.15, unit: "-", source: "AP_MOTORS_SPIN_MIN_DEFAULT in libraries/AP_Motors/AP_MotorsMulticopter.h, Plane-4.7.2", status: documented}
21+
Q_A_ANGLE_MAX: {value: 30.0, unit: "deg", source: "ANGLE_MAX of AC_AttitudeControl (Q_A_ANGLE_MAX): AC_ATTITUDE_CONTROL_ANGLE_MAX_DEFAULT 30 deg, libraries/AC_AttitudeControl/AC_AttitudeControl.cpp, Plane-4.7.2", status: documented}
22+
Q_ASSIST_SPEED: {value: 0.0, unit: "m/s", source: "quadplane.cpp AP_GROUPINFO ASSIST_SPEED (assist.speed), Plane-4.7.2; not modelled, listed for the table (O-20)", status: documented}
23+
TRIM_THROTTLE: {value: 0.45, unit: "-", source: "AP_PLANE_TRIM_THROTTLE_DEFAULT 45 percent in ArduPlane/config.h (called THROTTLE_CRUISE before 4.7), Plane-4.7.2", status: documented}
24+
THR_MAX: {value: 1.0, unit: "-", source: "THROTTLE_MAX 100 percent, ArduPlane/config.h, Plane-4.7.2", status: documented}
25+
THR_MIN: {value: 0.0, unit: "-", source: "THROTTLE_MIN 0 percent, ArduPlane/config.h, Plane-4.7.2", status: documented}

0 commit comments

Comments
 (0)