Fitting rate against instantaneous speed produced a tidy "rate rises with speed" relationship, with speeds up to 4795 when the craft's maximum is 1200. It is entirely an artefact: 20 Hz polling is faster than the guest updates these fields, so a per-read delta is either exactly zero (no update yet) or a whole frame's worth divided by a fraction of a frame. 111 of 352 reads were zero on BOTH channels -- position and attitude update on the same frame, so the two are perfectly correlated, and dividing each by the short wall dt produced the correlation out of nothing. Fix: aggregate over windows spanning many frames (0.5 s). A sum of |delta| over such a window is right however the updates fall inside it. This does NOT affect the swept-total probes (roll_axis.py, rate_probe.py) -- they already summed over the whole dwell, immune for the same reason. Only per-sample instantaneous rates were ever wrong, so no earlier number moves. The windowed re-run is NOT yet claimed as a result. It gives plausible magnitudes but still shows rate rising with speed, against the definition's PitchPlus_Min 150 > _Max 70, and it has two disqualifiers: it ran on an instance where the craft was already tumbling from the previous sweep, so pinning reported "WEAK -- craft may not be level", and the sweep started mid-range rather than at maximum. A clean answer needs a fresh flight with pinning CONFIDENT and nothing before it. Since what is in doubt is precisely what _Min/_Max mean, a measurement through a doubtful instrument cannot settle it. Both datasets kept, the bad one labelled, because the aliased curve is a good example of what a manufactured correlation looks like.
118 lines
4.7 KiB
Python
Executable File
118 lines
4.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Angular rate as a FUNCTION of speed — fitted, not sampled at two endpoints.
|
|
|
|
The two-point probe (`rate_probe.py`) assumed the craft held the speed its throttle
|
|
selects. It does not: full pitch bleeds 1 193 -> 589 in eight seconds *with the
|
|
throttle still at maximum*, so an 8-second average is taken over a moving speed and
|
|
lands between two different caps. That is how a measured pitch rate came out 33 %
|
|
ABOVE its own cap.
|
|
|
|
Turn the bug into the instrument. Because the speed bleeds on its own, one long hold
|
|
sweeps the whole range, so sampling position *and* attitude together gives
|
|
`(speed, rate)` pairs across it — the entire curve from a single phase, with no
|
|
assumption about what speed the craft is at.
|
|
|
|
Speed comes from the position deltas (no HUD OCR needed). Both speed and rate are
|
|
per wall-second here; the run's clock ratio converts both to game units afterwards
|
|
and cancels out of the SHAPE of the curve.
|
|
|
|
⚠️ SAMPLE IN WINDOWS, NOT PER READ. Polling at 20 Hz is faster than the guest
|
|
updates these fields, so a per-read delta is either exactly zero (no update yet) or
|
|
a whole frame's worth divided by a fraction of a frame. In a first run 111 of 352
|
|
reads were zero on BOTH channels — position and attitude update on the same frame,
|
|
so the two are perfectly correlated, and dividing each by the short wall dt
|
|
manufactured a clean "rate rises with speed" curve out of pure aliasing. Summing
|
|
|delta| over a window that spans many frames is immune: the total is right however
|
|
the updates fall inside it. (This is also why the swept-total probes were never
|
|
affected — only per-sample instantaneous rates were.)
|
|
|
|
Usage: rate_curve.py <roll|pitch> <out.csv> [dwell_s]
|
|
"""
|
|
import json
|
|
import math
|
|
import os
|
|
import struct
|
|
import subprocess
|
|
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}}
|
|
|
|
|
|
def shot(n):
|
|
subprocess.run(["screenshot", f"/sylph-home/re/shots/{n}.png"], capture_output=True)
|
|
|
|
|
|
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]
|
|
dwell = float(sys.argv[3]) if len(sys.argv) > 3 else 16.0
|
|
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}, sweeping {axis}")
|
|
|
|
# Start at maximum speed and let the turn bleed it: one hold, whole range.
|
|
pad_state(rt=255)
|
|
time.sleep(6.0)
|
|
shot(f"curve_{axis}_a")
|
|
prev_r, prev_p, prev_t = rows_at(w.fd, off, cfg), pos_at(w.fd, off), time.time()
|
|
pad_state(rt=255, **DRIVER[axis])
|
|
t0 = time.time()
|
|
rows = []
|
|
WIN = 0.5 # seconds per emitted point: many guest frames
|
|
win_t0, win_path, win_swept = time.time(), 0.0, 0.0
|
|
while time.time() - t0 < dwell:
|
|
time.sleep(0.02)
|
|
now = time.time()
|
|
cur_r, cur_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
|
|
win_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]))
|
|
win_swept += abs(math.degrees(d))
|
|
prev_r, prev_p = cur_r, cur_p
|
|
if now - win_t0 >= WIN:
|
|
span = now - win_t0
|
|
rows.append((round(now - t0, 2), round(win_path / span, 1),
|
|
round(win_swept / span, 2)))
|
|
win_t0, win_path, win_swept = now, 0.0, 0.0
|
|
pad_state()
|
|
shot(f"curve_{axis}_b")
|
|
|
|
with open(out_csv, "w") as f:
|
|
f.write("t,speed_units_per_wall_s,rate_deg_per_wall_s\n")
|
|
for r in rows:
|
|
f.write(",".join(str(x) for x in r) + "\n")
|
|
|
|
# Bin by speed so the shape is visible without any curve-fitting assumption.
|
|
print(f"# {len(rows)} windows of {WIN}s; rate binned by speed (per WALL second):")
|
|
lo = min(r[1] for r in rows)
|
|
hi = max(r[1] for r in rows)
|
|
print(f"# speed swept {lo:.0f} -> {hi:.0f} units/wall-s")
|
|
nb = 6
|
|
for b in range(nb):
|
|
a = lo + (hi - lo) * b / nb
|
|
z = lo + (hi - lo) * (b + 1) / nb
|
|
sel = [r[2] for r in rows if a <= r[1] < z]
|
|
if sel:
|
|
print(f"# speed {a:7.0f}-{z:7.0f} n={len(sel):3d} rate {sum(sel)/len(sel):6.1f}")
|
|
print(f"# wrote {out_csv}; read HUD TIME off curve_{axis}_[ab] for the clock")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|