re(flight): the ramp test is inconclusive, and names the tool the residual needs

To separate "a 1 s burst never reaches the steady rate" from "a per-axis
multiplier", measure inside ONE hold: successive 0.25 s windows of a single 3 s
press, holding speed, attitude and starting conditions constant by construction.

    rep0 rate   16   59  325  209  130  322  238  110  310  151  183
    rep1 rate   52  181  231  246  236  169  368  195  181  255  182

Not usable. At 0.25 s the windows do not contain enough guest updates to average,
so rate and speed both swing 3x window to window -- the same aliasing that once
manufactured a rate-vs-speed curve, reappearing at finer resolution. The first
window is 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. No
claim either way.

Widening the window does not rescue it: 0.5 s averages well enough, but a hold
long enough to contain several 0.5 s windows bleeds speed -- and speed is the
variable under test. The two effects are entangled at this observation rate.

So the residual needs a different INSTRUMENT, not another script. Live-RAM polling
samples an unsynchronised snapshot; the question wants 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.
This commit is contained in:
2026-08-13 23:53:23 +00:00
parent 8b03a52aa6
commit 4f6fcf36dc
3 changed files with 151 additions and 0 deletions

View File

@@ -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
1 rep t speed rate_deg_per_wall_s
2 0 0.27 390.1 15.5
3 0 0.54 233.9 58.9
4 0 0.81 573.5 325.0
5 0 1.07 341.0 209.1
6 0 1.34 222.8 130.3
7 0 1.61 525.5 321.8
8 0 1.87 386.6 237.6
9 0 2.14 190.9 110.4
10 0 2.41 511.3 310.2
11 0 2.66 246.7 151.1
12 0 2.94 312.3 183.0
13 1 0.27 446.6 51.6
14 1 0.54 372.5 181.1
15 1 0.8 377.7 231.0
16 1 1.07 417.7 246.3
17 1 1.32 385.0 236.1
18 1 1.59 232.9 169.4
19 1 1.85 590.3 368.1
20 1 2.11 307.0 194.7
21 1 2.38 311.9 180.7
22 1 2.64 418.6 255.2
23 1 2.9 306.5 181.9

View File

@@ -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 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 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. 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).

94
tools/re-capture/ramp_probe.py Executable file
View File

@@ -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 <roll|pitch> <out.csv> [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())