re(flight): LT mirrors RT, and roll does not depend on speed

One flight, two measurements (flight_law2.py, binding early so the moving-craft scan
can see the player).

LT curve: 436, 379, 289, 209, 126 units/s across LT 0.00 -> 1.00 — a straight ramp,
whose endpoints after the ~1.2 time-base factor are CruisingVelocity 350 and
MinimumVelocity 100. So the law is symmetric:

  RT: target = Cruising + RT * (Maximum - Cruising)
  LT: target = Cruising - LT * (Cruising - Minimum)

Roll (measured on a non-forward matrix row, since roll turns about the forward axis):
~144 deg/s at minimum speed and ~150 at maximum — no speed dependence, where pitch
dropped by a third to a half between the same regimes. After the time-base factor
that is ~121, i.e. AV_Roll_Max 125 in BOTH regimes.

So _Min/_Max does not mean the same thing for every axis: pitch interpolates with
speed, roll appears pinned at Max. A reimplementation applying one rule to all axes
would get low-speed roll wrong by ~60%.

Caveat recorded: the two roll phases were 2 s of settling apart, marginal for a
126 -> 1342 speed change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
This commit is contained in:
2026-08-13 16:30:19 +00:00
parent 4dbd4f6cef
commit 6ebbbeff65
4 changed files with 696 additions and 6 deletions

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Two measurements the first flight-model pass left open, in ONE flight.
1. **The `LT` half of the throttle curve** — `RT` was walked 0→1 and gave
`target = CruisingVelocity + RT·(MaximumVelocity CruisingVelocity)`; `LT`
should mirror it down to `MinimumVelocity`.
2. **Roll rate** vs `AV_Roll_{Min,Max}` (200 / 125 °/s). Roll turns the craft about
its own forward axis, so the forward vector barely moves (35 °/s residual, which
is how `LX` was identified as roll at all) — measure a DIFFERENT matrix row.
Binding happens first and fast: the entity scan searches *changing* position
triples, so it only sees the craft while it still has speed. A mission left idling
drops out of the scan entirely.
Usage: flight_law2.py <out-prefix> [dwell_s]
"""
import json, math, os, struct, subprocess, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import speed_law
def pad(*a):
subprocess.run(["vgamepad", *a], capture_output=True)
def row_at(fd, off, cfg, row):
at = off + cfg["rot_delta"] + row * cfg["rot_stride"]
v = struct.unpack(">3f", os.pread(fd, 12, at))
n = math.sqrt(sum(c * c for c in v)) or 1.0
return tuple(c / n for c in v)
def ang(a, b):
d = max(-1.0, min(1.0, sum(a[i] * b[i] for i in range(3))))
return math.degrees(math.acos(d))
def main():
prefix = sys.argv[1]
dwell = float(sys.argv[2]) if len(sys.argv) > 2 else 4.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 after retries")
print(f"# locked on {nm}")
# ── 1. LT curve (speed from position) ─────────────────────────────────────
lt_rows = []
for v in (0.0, 0.25, 0.5, 0.75, 1.0):
pad("trig", "RT", "0.0"); pad("trig", "LT", str(v))
time.sleep(2.0)
seq, t0 = [], time.time()
while time.time() - t0 < dwell:
seq.append((round(time.time() - t0, 3), *speed_law.pos_at(w.fd, off)))
time.sleep(0.05)
lt_rows += [(v, *s) for s in seq]
a, b = seq[0], seq[-1]
print(f"# LT={v:<5} speed {math.dist(a[1:], b[1:]) / (b[0] - a[0]):7.0f}/s")
# ── 2. Roll rate, measured on a non-forward row ───────────────────────────
roll_rows = []
other = 1 if cfg.get("fwd_row", 0) != 1 else 2
for label, trig in (("slow_roll", "LT"), ("fast_roll", "RT")):
pad("trig", "RT", "0.0"); pad("trig", "LT", "0.0")
pad("trig", trig, "1.0")
time.sleep(2.0)
pad("axis", "LX", "1.0")
seq, t0 = [], time.time()
while time.time() - t0 < dwell:
seq.append((round(time.time() - t0, 3), *row_at(w.fd, off, cfg, other)))
time.sleep(0.05)
pad("axis", "LX", "0.0")
roll_rows += [(label, *s) for s in seq]
rates = []
for t in range(int(dwell) - 1):
a = [s for s in seq if s[0] >= t][0]
b = [s for s in seq if s[0] <= t + 1][-1]
rates.append(ang(a[1:], b[1:]) / (b[0] - a[0]))
print(f"# {label:<10} row {other} turn rate: " + " ".join(f"{r:6.1f}" for r in rates) + " deg/s")
pad("trig", "RT", "0.0"); pad("trig", "LT", "0.0"); pad("reset")
with open(prefix + "-lt.csv", "w") as f:
f.write("lt,t,x,y,z\n")
for r in lt_rows:
f.write(",".join(str(x) for x in r) + "\n")
with open(prefix + "-roll.csv", "w") as f:
f.write("phase,t,ux,uy,uz\n")
for r in roll_rows:
f.write(",".join(str(x) for x in r) + "\n")
print(f"# wrote {prefix}-lt.csv and {prefix}-roll.csv")
if __name__ == "__main__":
main()