Files
Sylpheed/tools/re-capture/stick_sign.py
Sylpheed RE agent 1ddb699d12 tools+docs: the pitch stick sign was inverted - the pilot fires now
Measured, not argued: command a 45-degree-off-the-nose error and watch whether it
shrinks, both axes, both sides, two pulse widths, with the opposite sign as a
control. Yaw's sign is correct (45 -> 11.3/32.7 at 0.6 s, 41.6/33.8 at 1.2 s).
Pitch's is inverted - the pilot's own sign GREW the error every time
(48.3/55.6/70.7/91.7) and the opposite shrank it every time (30.7/41.5/15.7/9.6).

A method artefact is recorded because it gave the opposite answer first: a 3 s
full-deflection pulse overshoots a 45-degree error so far that BOTH signs look
wrong (45 -> 164 and 45 -> 178). A long pulse cannot answer a sign question.

Verified against the game rather than by inspection. Before: fire=1 in 0 of 13521
samples, |aim yaw| pinned at 90.0, target 36-43 km away. After: 43 of 1732, aim
down to 2.3 degrees, range median 6.3 km, and the HUD's own ammunition counters
moving - NOSE BM 06000 -> 05723, MAIN MPM 00300 -> 00298.

Also fixed a leftover of the same FIFO era: pilot.py called pad.f.write("tap A
90") for the target-select double tap, which raised AttributeError once Pad
stopped having an `f`. Pad gained tap()/dpad(); ctrl_probe.py and target_probe.py
still use pad.f and now say so in place.

Still open: YOU KILLED is 0000 after 250 s of firing and REMAINING OB is still
012. The craft shoots, closes and selects; whether it destroys anything is next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-23 22:39:33 +00:00

113 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Does `pilot.py`'s stick sign REDUCE its own aim error, or grow it?
With the pad finally reaching the game (`pilot-never-fires.md`), the pilot still
fires 0 times in 150 s and `|aim yaw|` stays pinned at exactly 90° — the `ez < 0`
branch, i.e. the target is astern and stays astern. A controller whose stick sign
is inverted looks exactly like that: it turns away from the error it is nulling,
so the target never comes round the front.
The test is direct and needs no target. Pick a direction 45° off the nose in the
ship's own frame, freeze it in WORLD coordinates, command the deflection
`sticks()` would command for that error, and see whether the error falls or
rises. Both signs are tested, because one of them agreeing by luck would prove
nothing.
`sticks()`'s convention, replicated exactly:
fwd = M[fwd_row] * fwd_sign
right = M[(fwd_row + 1) % 3]
up = cross(fwd, right)
yaw = atan2(dot(want, right), dot(want, fwd))
sx = KP*yaw - KD*(body rate about up)
sy = -(KP*pitch - KD*(body rate about right))
The KD terms are dropped here: the P term saturates the stick anyway, and a
damping term cannot flip a sign.
Usage: stick_sign.py <nav-live.json> [seconds_per_phase]
"""
import json
import math
import os
import sys
import time
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import frozen # noqa: E402
import navigator # noqa: E402
from flight_probe import Pad # noqa: E402
def main():
cfg = json.load(open(sys.argv[1]))
secs = float(sys.argv[2]) if len(sys.argv) > 2 else 3.0
W = navigator.World(cfg)
W.scan()
pad = Pad()
def frame():
ents = W.sample(time.time())
me = next((e for e in ents if "Player" in e[1]), None)
if me is None:
return None
M = W.rot(me[0])
if M is None:
return None
fwd = M[W.fwd_row] * W.fwd_sign
right = M[(W.fwd_row + 1) % 3]
return fwd, right, np.cross(fwd, right)
def err(want, f):
fwd, right, up = f
yaw = math.atan2(float(np.dot(want, right)), float(np.dot(want, fwd)))
pitch = math.atan2(float(np.dot(want, up)), float(np.dot(want, fwd)))
return math.degrees(yaw), math.degrees(pitch)
def trial(axis, side):
f0 = frame()
if f0 is None:
print(f"{axis}{side:+d}: no player/orientation")
return
fwd, right, up = f0
base = right if axis == "yaw" else up
want = fwd * math.cos(math.radians(45)) + base * side * math.sin(math.radians(45))
want = want / np.linalg.norm(want)
y0, p0 = err(want, f0)
# what sticks() would command for this error
if axis == "yaw":
pad.axis("LX", math.copysign(1.0, y0))
cmd = f"LX={math.copysign(1.0, y0):+.0f}"
else:
pad.axis("LY", -math.copysign(1.0, p0))
cmd = f"LY={-math.copysign(1.0, p0):+.0f}"
time.sleep(secs)
pad.reset()
f1 = frame()
froze = frozen.frozen(3.0)[0]
if f1 is None:
print(f"{axis}{side:+d}: lost the player")
return
y1, p1 = err(want, f1)
e0, e1 = (abs(y0), abs(y1)) if axis == "yaw" else (abs(p0), abs(p1))
verdict = "REDUCES (sign correct)" if e1 < e0 - 1.0 else (
"GROWS (sign INVERTED)" if e1 > e0 + 1.0 else "no change")
print(f"{axis} want{side:+d} {cmd}: |err| {e0:6.2f} -> {e1:6.2f} deg "
f"{verdict}{' *** FROZE - DISCARD ***' if froze else ''}")
if frozen.frozen(3.0)[0]:
print("# GUEST IS ALREADY FROZEN — nothing to measure")
return 3
for axis in ("yaw", "pitch"):
for side in (+1, -1):
trial(axis, side)
time.sleep(1.0)
pad.reset()
return 0
if __name__ == "__main__":
raise SystemExit(main())