re(flight): the rate probe measures a MOVING speed -- pitching bleeds it hard
Measured pitch with the rows properly pinned, against this craft's own disc caps
(AV_PitchPlus_Min 150, AV_PitchPlus_Max 70):
min speed (LT) 1391.0 deg / 8.00 s, clock x1.326 -> 131.1 deg/game-s vs 150
max speed (RT) 989.0 deg / 8.05 s, clock x1.318 -> 93.2 deg/game-s vs 70
A rate 33% ABOVE a cap is not a finding, it is a broken instrument. The HUD speed
is in the same bracketing screenshots that give the clock, so read it:
slow phase 102 -> 18
fast phase 1193 -> 589
The speed is NOT constant during the dwell -- pitching halves it in 8 seconds.
The cap is speed-dependent, so as the craft slowed its cap rose, and an 8-second
average necessarily lands between the max-speed cap and a mid-speed one. The 133%
is entirely the instrument.
This also weakens the roll result committed earlier: same method, so 120.9 vs
AV_Roll_Max 125 is CONSISTENT but is not a tight test -- the true cap could be
lower and still produce that average. Said plainly in the doc rather than left
standing as a clean confirmation. Min-speed figures are less affected; there is
little speed left to lose.
Proper fix, not yet done: dwell ~1-2 s so speed barely moves, or sample HUD speed
continuously and fit rate against INSTANTANEOUS speed -- which yields the whole
rate-vs-speed curve instead of two points.
Separately this is a flight-model finding: TURNING COSTS SPEED, steeply, with the
throttle still at maximum. A reimplementation treating the throttle as a speed the
craft simply holds will be wrong during manoeuvres.
This commit is contained in:
94
tools/re-capture/rate_probe.py
Executable file
94
tools/re-capture/rate_probe.py
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Angular rate cap for one axis, at minimum and maximum speed.
|
||||
|
||||
Generalises `roll_axis.py`. Two things it does that the probes before it did not:
|
||||
|
||||
* **Pins the matrix rows.** `entities2` measures row 2 = forward against velocity,
|
||||
but up-vs-right was previously taken from the D3D convention — and measuring
|
||||
world-Y in flight showed the convention is *backwards* here (up = row 1, right =
|
||||
row 0). Roll is immune to that (rotation of either non-forward row within their
|
||||
shared plane is roll either way), but **pitch is not**, so a pitch number taken
|
||||
without this pinning is naming an axis by assumption.
|
||||
* **Brackets each phase with the HUD clock.** Rates are per GAME second and the
|
||||
clock ratio is a property of the moment — 1.260, 1.311, 1.383 and 1.247/1.218
|
||||
across five flights — so it cannot be assumed and must be read per phase.
|
||||
|
||||
roll driven by lx, measured as rotation of UP about forward
|
||||
pitch driven by ly, measured as rotation of FORWARD toward up
|
||||
|
||||
Usage: rate_probe.py <roll|pitch> <out.csv> [dwell_s]
|
||||
Then read HUD TIME off the `rate_<axis>_<phase>_[ab]` screenshots for the ratio.
|
||||
"""
|
||||
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 main():
|
||||
axis = sys.argv[1]
|
||||
out_csv = sys.argv[2]
|
||||
dwell = float(sys.argv[3]) if len(sys.argv) > 3 else 8.0
|
||||
if axis not in DRIVER:
|
||||
sys.exit(f"axis must be one of {list(DRIVER)}")
|
||||
cfg = json.load(open("/tmp/nav-live.json"))
|
||||
w, off, nm = speed_law.find_player()
|
||||
if not w:
|
||||
sys.exit("player entity not found")
|
||||
print(f"# locked on {nm}, measuring {axis}")
|
||||
f_i = cfg["fwd_row"]
|
||||
if not alive(w, off, cfg):
|
||||
sys.exit("craft is not moving — not in flight, or already dead")
|
||||
u_i, w_i = pin_rows(w, off, cfg)
|
||||
|
||||
rows = []
|
||||
for label, trig in (("slow", "lt"), ("fast", "rt")):
|
||||
pad_state(**{trig: 255})
|
||||
time.sleep(5.0) # settle: 2 s was shown wrong twice
|
||||
shot(f"rate_{axis}_{label}_a")
|
||||
prev = rows_at(w.fd, off, cfg)
|
||||
pad_state(**{trig: 255}, **DRIVER[axis])
|
||||
t0, swept, seq = time.time(), 0.0, []
|
||||
while time.time() - t0 < dwell:
|
||||
time.sleep(0.05)
|
||||
cur = rows_at(w.fd, off, cfg)
|
||||
if axis == "roll":
|
||||
d = math.atan2(dot(cur[u_i], prev[w_i]), dot(cur[u_i], prev[u_i]))
|
||||
else:
|
||||
d = math.atan2(dot(cur[f_i], prev[u_i]), dot(cur[f_i], prev[f_i]))
|
||||
d = math.degrees(d)
|
||||
swept += abs(d)
|
||||
seq.append((round(time.time() - t0, 3), round(d, 4)))
|
||||
prev = cur
|
||||
pad_state(**{trig: 255})
|
||||
wall = seq[-1][0]
|
||||
shot(f"rate_{axis}_{label}_b")
|
||||
rows += [(label, *s) for s in seq]
|
||||
print(f"# {label:<5} wall {wall:5.2f}s {axis} swept {swept:7.1f}deg "
|
||||
f"rate {swept / wall:6.1f} deg/wall-s")
|
||||
if not alive(w, off, cfg):
|
||||
print("# CRAFT STOPPED MOVING — aborting rather than reporting zeros")
|
||||
break
|
||||
pad_state()
|
||||
with open(out_csv, "w") as f:
|
||||
f.write(f"phase,t,d{axis}_deg\n")
|
||||
for r in rows:
|
||||
f.write(",".join(str(x) for x in r) + "\n")
|
||||
print(f"# wrote {out_csv} — read HUD TIME off rate_{axis}_*_[ab] for the clock")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user