Files
Sylpheed/docs/re/pilot-never-fires.md
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

217 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `pilot.py` never pulls the trigger — the proximal cause, measured
**Status: ✅ ROOT CAUSE FOUND AND FIXED (2026-08-23, later the same day) — the
flight tools' pad wrote to a FIFO nothing has read since the uinput pad was
removed, so every stick, trigger and button they sent went nowhere.** The gate
measured below is real; it was open because the craft was never being flown at
all. 🟡 The pilot still does not converge on a target in 150 s with the input
working — see the last section.
## The observation
Over a 900 s Stage 02 run, `fire=1` appears in **0 of 13 521** logged samples.
The loop flies, chases, evades and retires; the guns are never fired. Every
"kill something and watch X" experiment in this corpus has therefore been leaning
entirely on the wingmen.
## Which gate closes
`pilot.py`'s trigger needs all of: mode `ENGAGE`/`DEFEND`, a target, the aim error
inside the firing cone, range < `FIRE_RANGE` (`SHELL_MAX_RANGE` = 4 000), and a
small avoidance push. Over the 3 004 samples that had a target:
| quantity | median | min | inside its gate |
|---|---|---|---|
| `\|aim yaw\|` | **90.0°** | **90.0°** | 0 of 3 004 below 25° |
| `\|aim pitch\|` | 179.2° | 166.4° | 0 of 3 004 below 25° |
| range | 49 634 | 45 078 | 0 below 4 000, 0 below 10 000 |
`|aim yaw|` being **exactly** 90.0 in every sample is not a coincidence, it is a
branch: `sticks()` contains
```python
if ez < 0: # target behind: commit to a full turn
yaw = math.copysign(math.pi / 2, ex if ex else 1.0)
```
So the committed target was **behind the ship in 100 % of samples**, with a pitch
error near 180° — directly astern — and the range to it grew monotonically:
```
t= 0 s 22 265 t= 231 s 33 632
t=510 s 49 475 t= 884 s 48 200 (plateau)
```
The craft flies *away* from the thing it is chasing for fifteen minutes and the
turn never completes. Nothing else needs to be wrong for the trigger to stay
cold.
## What is NOT established
Three explanations fit that shape and this pass did not separate them:
1. the attitude matrix the pilot reads is **stale**, so the ship turns and the
controller cannot see it;
2. the matrix is live but `fwd_row`/`fwd_sign` name the wrong axis;
3. both are fine and the **yaw stick sign** is inverted, so the loop turns away
from the error it is nulling.
🔴 **An attempt to separate them was invalidated and is withdrawn.** `aim_probe.py`
watched the forward vector under neutral, full-left and full-right stick and
reported it pinned at `[-1, 0, 0]` with `|turn| = 0.00 °/s` in every phase — which
looks like explanation (1). It is not evidence: **the guest had frozen** partway
through, confirmed immediately afterwards by `frozen.py` (max pixel delta 0) and
by the player's position being byte-identical across 3 s. A dead world holds every
matrix still. The probe is committed because it is the right experiment; its
result is not.
## One confusion resolved on the way
Today's repeated `entities2.py self` failures — `# 0 moving triples`, "player
entity not found" — are **the freeze**, not a tool defect. `moving()` types
entities by their position *changing* between two samples, so a frozen world
yields nothing by construction. Runs where the bind failed were runs that had
already stopped.
## Next step
Re-run `aim_probe.py` on a run confirmed to be animating **at the end of the
probe as well as the start** — the tool should check `frozen.py` itself and
discard the phase otherwise. If the matrix does move under stick, the question
becomes the sign; if it does not, the binding is what to chase.
---
# ✅ ROOT CAUSE — the flight tools' pad was writing into a dead file
`flight_probe.Pad` opened **`/tmp/sylph-vgamepad.fifo`** and wrote
`axis LX 0.850`-style lines to it, described in its own docstring as "the vgamepad
server's FIFO". That server does not exist any anymore: the uinput pad was removed
because a uinput device is **not namespaced** and every scripted press leaked to
the *host's* desktop, and it was replaced by Canary's `--hid=file` driver. The
FIFO is now an ordinary 91-byte file that nothing reads.
Meanwhile `/tmp/xenia_pad.txt` — the file the emulator actually polls — was
**0 bytes** while the "autopilot" was supposedly flying.
So every axis, trigger and button from `pilot.py`, `autopilot3.py`,
`aim_probe.py` and `flight_probe.py` itself went into a dead file, silently, and
the craft flew on its own for the whole of every run. That is why the target sat
astern at 90.0° forever and the range grew to 49 km: **the ship was never being
turned.**
**The corpus already carried this trap — for the other half of the toolkit.**
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) records
"pad.py, NOT `vgamepad`: that command no longer exists … and every call here
failed silently, so the whole scripted route pressed nothing". The **shell**
scripts were fixed then. This class was not, and every flight tool imports it.
## Measured, before and after
Same probe, on runs confirmed animating at both ends of every phase
([`aim_probe.py`](../../tools/re-capture/aim_probe.py) brackets each phase with
`frozen.py` now, after a freeze voided the first attempt):
| | heading change under full stick | attitude matrix `pos-0x70` |
|---|---|---|
| **before** | `0.00°` in every phase, over 4 s, while travelling 350735 units | `d 0.0000` — unchanged |
| **after** | `12.72°` on `LX=-1`; `LX=+1` swings the flight direction from `[1,0,0]` to `[0.13,-0.14,-0.98]` | `d 0.4438` / `d 0.3108` under stick, `0.0000` at neutral |
🔴 **So the "stale attitude matrix" reading is refuted**, and with it the earlier
suspicion that the binding was wrong: `pos-0x70` is live and tracks the ship. It
only looked dead because nothing was turning the ship.
## ✅ AND THE SECOND HALF: the PITCH stick sign was inverted
(The section below is what was open before this was measured; it is kept because
the sequence matters.)
With the pad reaching the game, the sign question became testable and was
settled by commanding a **45°-off-the-nose error** and watching whether it
shrank — both axes, both sides, two pulse widths, plus the opposite sign as a
control:
| pulse | axis | pilot's own sign | opposite sign |
|---|---|---|---|
| 0.6 s | yaw | 45° → **11.3** / **32.7** ✅ reduces | — |
| 1.2 s | yaw | 45° → **41.6** / **33.8** ✅ reduces | — |
| 0.6 s | pitch | 45° → 48.3 / 55.6 🔴 grows | 45° → **30.7** / **41.5** reduces |
| 1.2 s | pitch | 45° → 70.7 / 91.7 🔴 grows | 45° → **15.7** / **9.6** reduces |
So `sticks()`'s `sy = -(KP*pitch - KD*…)` had the **wrong sign**; yaw's is right.
🔴 **A method artefact worth recording, because it gave the opposite answer
first.** The first attempt used a **3 s** full-deflection pulse and reported
pitch growing under *both* signs (45° → 164° and 45° → 178°). That is
overshoot: three seconds of full pitch swings the ship far past a 45° error, so
the frozen world-direction ends up behind it whichever way it turns. Pulse
short, or measure the initial rotation direction — a long pulse cannot answer a
sign question.
### Verified: the pilot fires
Same mission, same binding, sign flipped:
| | before | after |
|---|---|---|
| `fire=1` samples | **0** of 13 521 | **43** of 1 732 |
| `\|aim yaw\|` | median 90.0°, min 90.0° | min **2.3°**, 7 of 14 inside 25° in the first pass |
| range to target | median 43 km, min 36 km | median **6.3 km**, min 5.3 km |
| ammunition | untouched | `NOSE BM 06000 → 05723`, `MAIN MPM 00300 → 00298` |
![the pilot firing](captures/stage02-pilot-firing-after-sign-fix.png)
One more leftover of the same FIFO era was fixed on the way: `pilot.py` called
`self.pad.f.write("tap A 90")` for the target-select double tap, which raised
`AttributeError` the moment the class 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 even so:** `YOU KILLED WARSHIPS/WARPLANES` is still `0000` after
250 s of firing, and `REMAINING OB` is still `012`. The craft shoots, closes and
selects; whether it actually *destroys* anything is the next thing to measure,
and it is what the objective-counter work is waiting on.
## 🟡 The state before that measurement — the pilot does not converge
With the pad fixed, a 150 s `pilot.py` run still fired **0** times: over its 86
targeted samples `|aim yaw|` is *still* exactly 90.0° and the range is
3643 km. Steering now works, so what remains is one of:
* the **sign** of the yaw stick against the pilot's own error convention — a
controller that turns away from the error would hold `ez < 0` forever, which is
exactly the observed signature;
* target selection that keeps committing to something 40 km away, which at
~300 units/s cannot be closed inside the window.
Both are now testable, because the input path finally works. The measurement to
make is the direct one: command the pilot's own `sx` for a known error and see
whether `|yaw|` falls or rises.
## Not fixed, and flagged in place
`findrot_global.py`, `findself.py`, `findspeed.py` and `selfstate.py` still write
to the dead FIFO. Each now carries a comment saying so and pointing here. They
are not repaired because none of them has been re-run since, and quietly changing
the input plumbing under a tool whose results are unverified would only
manufacture new unverified results.
## Which earlier results this puts at risk — dated, so it can be checked rather than worried about
The uinput pad was replaced on **2026-08-13** (canary `cbfad1ab3`), which is the
day `flight_probe.Pad` stopped reaching the game. So:
***Safe:** anything measured before that. `flight-controls-runtime.md` was
last touched **2026-07-30**, comfortably on the working side.
* ⚠️ **Ambiguous, and worth re-checking:** `flight-speed-law.md` was last touched
**2026-08-13** — the same day — so whether its numbers were taken before or
after the FIFO went dead cannot be told from the dates alone.
* 🔴 **Anything that used `flight_probe.Pad` on or after 2026-08-13 measured a
craft that was not being flown.**
Stated as a rule for whoever picks this up: a flight result is only as good as a
demonstration that the stick moved the ship in the same session. `aim_probe.py`
is that demonstration, and it now brackets itself with a liveness check.