diff --git a/docs/re/captures/roll-ramp-inconclusive.csv b/docs/re/captures/roll-ramp-inconclusive.csv new file mode 100644 index 0000000..d6cfa00 --- /dev/null +++ b/docs/re/captures/roll-ramp-inconclusive.csv @@ -0,0 +1,23 @@ +rep,t,speed,rate_deg_per_wall_s +0,0.27,390.1,15.5 +0,0.54,233.9,58.9 +0,0.81,573.5,325.0 +0,1.07,341.0,209.1 +0,1.34,222.8,130.3 +0,1.61,525.5,321.8 +0,1.87,386.6,237.6 +0,2.14,190.9,110.4 +0,2.41,511.3,310.2 +0,2.66,246.7,151.1 +0,2.94,312.3,183.0 +1,0.27,446.6,51.6 +1,0.54,372.5,181.1 +1,0.8,377.7,231.0 +1,1.07,417.7,246.3 +1,1.32,385.0,236.1 +1,1.59,232.9,169.4 +1,1.85,590.3,368.1 +1,2.11,307.0,194.7 +1,2.38,311.9,180.7 +1,2.64,418.6,255.2 +1,2.9,306.5,181.9 diff --git a/docs/re/flight-speed-law.md b/docs/re/flight-speed-law.md index 9916215..82761a1 100644 --- a/docs/re/flight-speed-law.md +++ b/docs/re/flight-speed-law.md @@ -702,3 +702,37 @@ so the row pin came back **WEAK** (margin 0.109). That refusal is correct for pi and yaw — but **roll is immune to the up/right labelling**, which is exactly what the guard's own message says, so `ALLOW_WEAK_PIN=1` is the documented and legitimate override there. It is worth having a guard that states its own exception. + + +## The ramp test is inconclusive — and says what the next tool must be 🟡 + +To separate "a 1 s burst never reaches the steady rate" from "a per-axis multiplier", +the cheapest test measures *inside* one hold: successive 0.25 s windows of a single +3 s press, so speed, attitude and starting conditions are held constant by +construction. Two repeats, roll, at cruise: + +``` +rep0 rate 16 59 325 209 130 322 238 110 310 151 183 + speed 390 234 574 341 223 526 387 191 511 247 312 +rep1 rate 52 181 231 246 236 169 368 195 181 255 182 + speed 447 372 378 418 385 233 590 307 312 419 306 +``` + +**Not usable.** At 0.25 s the windows do not contain enough guest updates to average, +so both channels swing by 3× window to window — the same aliasing that once +manufactured a rate-vs-speed curve, reappearing at finer resolution. The first window +is the lowest in *both* repeats, which is what a ramp would look like, but the +sequence never plateaus, so the signal cannot be separated from the sampling. + +**Widening the window does not rescue it**: 0.5 s windows average well enough (that is +what the earlier sweep used), but a hold long enough to contain several of them bleeds +speed, and speed is the very variable under test. The two effects are entangled at +this observation rate. + +**So this residual needs a different instrument, not another script.** Live-RAM polling +samples an unsynchronised snapshot; what the question wants is the craft's angular +velocity *as the guest computes it, once per frame*. That is a Canary-side hook — +the same shape as the existing F10 ship-capture patch — and the rebuild toolchain +already makes it cheap. Recorded as the recommendation rather than attempted as a +seventh variation of the same measurement. +Data: [`captures/roll-ramp-inconclusive.csv`](captures/roll-ramp-inconclusive.csv). diff --git a/tools/re-capture/ramp_probe.py b/tools/re-capture/ramp_probe.py new file mode 100755 index 0000000..7e496fc --- /dev/null +++ b/tools/re-capture/ramp_probe.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Does the angular rate RAMP after the stick goes over? + +Burst measurements came out a strikingly flat 0.60-0.64x of the roll cap and +1.07-1.21x of the pitch cap. A clock error cannot do that (it would move both the +same way), so something per-axis is going on, and the cheapest candidate is that a +short burst never reaches the steady rate — an angular-acceleration ramp would make +a 1-second burst under-read. + +Rather than compare separate bursts (which differ in speed, attitude and starting +conditions), measure INSIDE one hold: successive 0.25 s windows of a single 3 s +press. A ramp shows up as the first windows reading low and the later ones +plateauing, with everything else held constant by construction. + +Windows, never per-read deltas: polling outruns the guest's update of these fields. + +Usage: ramp_probe.py [hold_s] [repeats] +""" +import json +import math +import os +import struct +import sys +import time + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import speed_law # noqa: E402 +from axis_probe import alive, pad_state, pin_rows, rows_at, dot # noqa: E402 + +DRIVER = {"roll": {"lx": 32767}, "pitch": {"ly": 32767}} +WIN = 0.25 + + +def pos_at(fd, off): + return struct.unpack(">3f", os.pread(fd, 12, off)) + + +def main(): + axis, out_csv = sys.argv[1], sys.argv[2] + hold = float(sys.argv[3]) if len(sys.argv) > 3 else 3.0 + reps = int(sys.argv[4]) if len(sys.argv) > 4 else 2 + cfg = json.load(open("/tmp/nav-live.json")) + w, off, nm = speed_law.find_player() + if not w: + sys.exit("player entity not found") + f_i = cfg["fwd_row"] + if not alive(w, off, cfg): + sys.exit("craft is not moving") + u_i, w_i = pin_rows(w, off, cfg) + print(f"# locked on {nm}, {axis} ramp over {hold}s in {WIN}s windows") + + rows = [] + for rep in range(reps): + pad_state() # cruise: least speed bleed of the three + time.sleep(6.0) + prev_r, prev_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off) + pad_state(**DRIVER[axis]) + t0 = wt0 = time.time() + path = swept = 0.0 + seq = [] + while time.time() - t0 < hold: + time.sleep(0.02) + now = time.time() + cur_r, cur_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off) + path += math.dist(cur_p, prev_p) + if axis == "roll": + d = math.atan2(dot(cur_r[u_i], prev_r[w_i]), dot(cur_r[u_i], prev_r[u_i])) + else: + d = math.atan2(dot(cur_r[f_i], prev_r[u_i]), dot(cur_r[f_i], prev_r[f_i])) + swept += abs(math.degrees(d)) + prev_r, prev_p = cur_r, cur_p + if now - wt0 >= WIN: + span = now - wt0 + seq.append((round(now - t0, 2), round(path / span, 1), round(swept / span, 1))) + wt0, path, swept = now, 0.0, 0.0 + pad_state() + for t, sp, rt in seq: + rows.append((rep, t, sp, rt)) + print(f"# rep{rep}: " + " ".join(f"{rt:.0f}" for _, _, rt in seq)) + print(f"# speed " + " ".join(f"{sp:.0f}" for _, sp, _ in seq)) + if not alive(w, off, cfg): + print("# CRAFT STOPPED MOVING — aborting") + break + time.sleep(2.0) + + with open(out_csv, "w") as f: + f.write("rep,t,speed,rate_deg_per_wall_s\n") + for r in rows: + f.write(",".join(str(x) for x in r) + "\n") + print(f"# wrote {out_csv}") + + +if __name__ == "__main__": + raise SystemExit(main())