From 0a84c1358ecb9ea73dff292f74623c7ef6e7fad9 Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Thu, 13 Aug 2026 23:00:59 +0000 Subject: [PATCH] re(flight): polling faster than the guest updates manufactures a clean curve 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. --- docs/re/captures/rate-curve-aliased-BAD.csv | 353 ++++++++++++++++++++ docs/re/captures/rate-curve-windowed.csv | 40 +++ docs/re/flight-speed-law.md | 40 +++ tools/re-capture/rate_curve.py | 117 +++++++ 4 files changed, 550 insertions(+) create mode 100644 docs/re/captures/rate-curve-aliased-BAD.csv create mode 100644 docs/re/captures/rate-curve-windowed.csv create mode 100755 tools/re-capture/rate_curve.py diff --git a/docs/re/captures/rate-curve-aliased-BAD.csv b/docs/re/captures/rate-curve-aliased-BAD.csv new file mode 100644 index 0000000..55b94bf --- /dev/null +++ b/docs/re/captures/rate-curve-aliased-BAD.csv @@ -0,0 +1,353 @@ +t,speed_units_per_wall_s,rate_deg_per_wall_s +0.05,0.0,0.0 +0.101,4682.4,0.0 +0.154,1865.9,8.21 +0.205,1915.4,22.53 +0.273,1722.2,32.72 +0.323,2309.7,64.27 +0.374,0.0,0.0 +0.426,0.0,0.0 +0.476,0.0,0.0 +0.526,3781.4,166.09 +0.576,1133.2,56.22 +0.626,2228.8,125.61 +0.676,735.8,46.29 +0.726,2161.2,150.9 +0.777,0.0,0.0 +0.827,0.0,0.0 +0.877,0.0,0.0 +0.927,4795.2,363.58 +0.978,1400.3,105.58 +1.028,2080.9,159.73 +1.078,688.0,53.58 +1.129,1018.2,79.65 +1.179,0.0,0.0 +1.229,0.0,0.0 +1.279,3169.4,272.63 +1.332,1891.3,159.33 +1.382,656.8,55.51 +1.432,1958.3,167.87 +1.485,311.4,26.7 +1.535,0.0,0.0 +1.585,0.0,0.0 +1.637,2189.9,217.26 +1.687,1470.3,152.38 +1.737,1832.8,178.53 +1.788,611.6,57.44 +1.838,311.7,29.03 +1.889,0.0,0.0 +1.939,0.0,0.0 +1.989,2568.6,290.03 +2.039,1303.4,156.28 +2.09,1571.3,188.96 +2.173,987.0,114.66 +2.223,1389.3,157.19 +2.274,0.0,0.0 +2.324,0.0,0.0 +2.374,0.0,0.0 +2.425,3014.5,437.25 +2.476,851.8,137.78 +2.526,1720.0,282.99 +2.576,855.8,142.04 +2.626,1272.7,213.71 +2.676,420.4,71.25 +2.726,0.0,0.0 +2.776,0.0,0.0 +2.827,2337.7,393.95 +2.879,605.4,103.05 +2.929,1229.4,216.06 +2.979,798.9,145.37 +3.029,1356.7,257.13 +3.081,183.1,35.66 +3.131,0.0,0.0 +3.181,0.0,0.0 +3.231,1858.0,372.22 +3.281,541.0,112.82 +3.332,1372.1,301.88 +3.382,335.4,76.89 +3.432,658.3,154.84 +3.482,162.0,38.8 +3.536,0.0,0.0 +3.586,0.0,0.0 +3.636,1591.8,390.72 +3.686,311.3,78.69 +3.736,1215.5,316.75 +3.786,602.4,159.01 +3.839,429.8,113.96 +3.889,0.0,0.0 +3.939,0.0,0.0 +3.989,0.0,0.0 +4.04,1835.6,469.81 +4.09,311.3,78.92 +4.14,1251.2,315.27 +4.19,317.9,78.47 +4.24,320.5,78.42 +4.29,0.0,0.0 +4.341,0.0,0.0 +4.391,1352.5,313.43 +4.441,850.5,192.03 +4.492,1033.9,227.09 +4.542,1073.0,227.87 +4.592,551.1,113.43 +4.642,0.0,0.0 +4.692,0.0,0.0 +4.743,1735.5,337.79 +4.793,779.7,147.59 +4.843,1186.4,220.17 +4.893,808.4,146.17 +4.943,1018.0,181.41 +4.993,204.4,36.08 +5.043,0.0,0.0 +5.093,0.0,0.0 +5.144,0.0,0.0 +5.194,3438.2,575.96 +5.244,853.2,142.26 +5.294,1685.8,285.28 +5.344,1250.1,215.19 +5.394,825.0,144.01 +5.445,202.2,35.56 +5.495,0.0,0.0 +5.545,0.0,0.0 +5.596,2042.5,361.76 +5.674,762.2,138.77 +5.725,1171.7,219.95 +5.775,384.3,73.55 +5.826,0.0,0.0 +5.877,0.0,0.0 +5.927,0.0,0.0 +5.977,1528.0,296.2 +6.028,1281.9,256.26 +6.078,365.6,75.03 +6.128,1073.3,226.52 +6.178,176.0,37.88 +6.231,0.0,0.0 +6.281,0.0,0.0 +6.332,1720.1,372.76 +6.382,518.3,114.37 +6.432,1009.5,230.82 +6.484,480.0,112.96 +6.534,807.9,194.56 +6.584,159.3,38.98 +6.675,0.0,0.0 +6.725,1449.1,352.93 +6.775,477.0,117.26 +6.826,927.2,232.38 +6.877,306.8,77.6 +6.927,928.5,236.59 +6.977,154.5,39.37 +7.027,0.0,0.0 +7.077,0.0,0.0 +7.131,1353.2,333.31 +7.181,963.9,234.69 +7.231,1304.5,311.86 +7.281,332.6,77.62 +7.331,506.1,116.36 +7.381,0.0,0.0 +7.432,0.0,0.0 +7.482,0.0,0.0 +7.534,2615.1,556.47 +7.584,1101.8,225.72 +7.634,1108.3,222.17 +7.686,929.5,182.14 +7.736,576.3,111.11 +7.786,0.0,0.0 +7.836,0.0,0.0 +7.886,0.0,0.0 +7.936,2840.4,515.81 +7.987,807.7,144.56 +8.037,1214.6,217.75 +8.087,809.7,145.24 +8.137,606.3,108.89 +8.187,0.0,0.0 +8.237,0.0,0.0 +8.288,0.0,0.0 +8.339,2628.6,463.46 +8.389,811.4,144.9 +8.439,1198.5,218.35 +8.489,392.4,72.53 +8.539,784.5,146.63 +8.59,0.0,0.0 +8.64,0.0,0.0 +8.69,0.0,0.0 +8.741,2111.3,400.11 +8.791,946.6,185.46 +8.841,1110.6,224.19 +8.891,364.4,75.15 +8.941,720.3,150.86 +8.991,0.0,0.0 +9.042,0.0,0.0 +9.092,1792.7,378.77 +9.142,529.6,113.67 +9.193,1022.7,226.47 +9.273,624.6,143.36 +9.325,644.6,151.82 +9.375,0.0,0.0 +9.425,0.0,0.0 +9.476,1289.1,307.07 +9.526,1122.5,273.41 +9.577,311.7,76.8 +9.627,789.7,196.14 +9.677,156.4,38.99 +9.727,0.0,0.0 +9.778,0.0,0.0 +9.828,2131.2,510.02 +9.878,653.9,155.64 +9.928,983.1,233.48 +9.978,330.0,77.72 +10.028,496.6,116.32 +10.079,0.0,0.0 +10.129,0.0,0.0 +10.179,0.0,0.0 +10.23,2470.2,540.62 +10.282,672.7,144.51 +10.333,1067.2,227.85 +10.383,1076.8,227.17 +10.433,724.0,150.8 +10.483,0.0,0.0 +10.533,0.0,0.0 +10.583,0.0,0.0 +10.633,2272.0,451.3 +10.683,759.7,148.67 +10.734,762.5,148.52 +10.784,756.3,146.57 +10.834,770.1,148.16 +10.885,0.0,0.0 +10.935,0.0,0.0 +10.985,0.0,0.0 +11.035,2168.6,406.08 +11.085,984.6,183.61 +11.136,1178.3,220.07 +11.186,590.5,110.24 +11.236,393.0,73.44 +11.286,0.0,0.0 +11.336,0.0,0.0 +11.386,2007.6,367.45 +11.436,597.4,109.35 +11.486,1180.4,219.81 +11.537,1160.8,220.86 +11.587,575.3,111.02 +11.639,368.0,71.56 +11.689,0.0,0.0 +11.739,0.0,0.0 +11.789,2275.4,445.88 +11.879,415.2,83.5 +11.929,1448.9,300.69 +11.979,356.5,75.48 +12.031,0.0,0.0 +12.081,0.0,0.0 +12.131,1612.8,341.73 +12.184,504.1,108.0 +12.234,1042.0,228.85 +12.284,683.1,153.38 +12.334,674.3,153.83 +12.386,0.0,0.0 +12.473,0.0,0.0 +12.524,1701.7,386.01 +12.574,672.1,154.19 +12.625,974.9,227.26 +12.675,330.4,77.54 +12.725,986.1,232.99 +12.775,163.8,38.73 +12.825,0.0,0.0 +12.878,0.0,0.0 +12.928,0.0,0.0 +12.978,2405.7,542.85 +13.029,1022.4,230.5 +13.079,681.6,153.81 +13.129,1354.2,305.37 +13.179,343.9,76.77 +13.229,692.5,153.38 +13.28,0.0,0.0 +13.33,0.0,0.0 +13.38,0.0,0.0 +13.43,2362.5,496.01 +13.483,693.6,143.63 +13.533,1466.7,300.97 +13.583,555.0,112.79 +13.633,184.3,37.4 +13.683,0.0,0.0 +13.735,0.0,0.0 +13.785,1917.4,375.17 +13.838,542.8,105.29 +13.888,1144.2,222.78 +13.938,1513.8,297.05 +13.988,568.4,111.69 +14.038,378.0,74.42 +14.088,0.0,0.0 +14.139,0.0,0.0 +14.189,1933.6,372.51 +14.239,955.5,184.91 +14.289,756.9,148.61 +14.374,662.1,132.25 +14.424,554.7,112.16 +14.474,0.0,0.0 +14.524,0.0,0.0 +14.577,1603.7,323.84 +14.627,1276.0,263.01 +14.677,542.3,113.23 +14.727,714.5,150.87 +14.777,354.7,75.59 +14.83,0.0,0.0 +14.88,0.0,0.0 +14.93,1795.9,379.83 +14.98,355.3,75.74 +15.031,1386.0,301.97 +15.083,330.9,73.04 +15.133,688.3,153.24 +15.183,170.9,38.24 +15.234,0.0,0.0 +15.284,0.0,0.0 +15.334,0.0,0.0 +15.384,2304.1,499.06 +15.435,1027.8,225.69 +15.485,1024.9,230.47 +15.536,1000.9,229.21 +15.586,664.6,153.64 +15.636,665.2,154.54 +15.687,0.0,0.0 +15.737,0.0,0.0 +15.788,0.0,0.0 +15.84,2131.8,478.6 +15.89,856.1,192.06 +15.941,1025.4,230.39 +15.991,342.4,76.74 +16.043,663.2,148.34 +16.093,0.0,0.0 +16.143,0.0,0.0 +16.193,0.0,0.0 +16.246,2555.4,545.64 +16.296,895.5,189.56 +16.373,694.2,146.49 +16.424,1443.5,302.27 +16.476,0.0,0.0 +16.526,0.0,0.0 +16.577,0.0,0.0 +16.627,2074.2,414.87 +16.677,751.3,149.12 +16.727,1123.6,223.97 +16.777,374.5,74.66 +16.827,1122.8,223.93 +16.88,0.0,0.0 +16.93,0.0,0.0 +16.98,0.0,0.0 +17.03,2296.0,447.62 +17.08,379.9,74.23 +17.13,1503.9,297.32 +17.18,563.4,111.98 +17.233,538.0,107.34 +17.283,0.0,0.0 +17.333,0.0,0.0 +17.383,1890.3,373.65 +17.433,1119.5,223.93 +17.485,361.3,72.88 +17.535,1651.2,337.72 +17.585,365.1,75.24 +17.635,547.2,113.12 +17.686,0.0,0.0 +17.736,0.0,0.0 +17.786,0.0,0.0 +17.839,2490.6,503.11 +17.892,1033.4,213.33 +17.942,1073.2,226.83 +17.995,670.1,143.94 +18.045,1223.2,266.8 diff --git a/docs/re/captures/rate-curve-windowed.csv b/docs/re/captures/rate-curve-windowed.csv new file mode 100644 index 0000000..ce55746 --- /dev/null +++ b/docs/re/captures/rate-curve-windowed.csv @@ -0,0 +1,40 @@ +t,speed_units_per_wall_s,rate_deg_per_wall_s +0.52,766.4,15.56 +1.06,875.3,81.57 +1.56,319.7,79.47 +2.08,589.5,124.0 +2.6,833.9,118.23 +3.1,846.4,144.01 +3.62,682.9,147.83 +4.12,442.3,114.32 +4.62,782.3,183.76 +5.13,869.9,160.12 +5.66,691.9,117.79 +6.18,770.1,146.5 +6.68,659.7,149.99 +7.19,546.7,135.85 +7.7,760.5,163.7 +8.2,645.7,121.72 +8.72,884.3,159.2 +9.26,546.3,107.3 +9.78,633.0,139.66 +10.28,716.1,169.56 +10.8,607.5,129.02 +11.32,866.0,156.23 +11.82,677.8,119.63 +12.32,537.0,103.57 +12.82,821.2,187.87 +13.34,545.6,133.44 +13.86,608.8,128.22 +14.38,778.1,141.08 +14.88,543.6,93.59 +15.38,935.1,176.36 +15.9,708.2,159.77 +16.4,439.7,105.71 +16.9,847.5,185.58 +17.42,584.5,112.34 +17.94,874.4,158.62 +18.44,733.0,149.31 +18.96,527.8,128.4 +19.46,642.2,156.88 +19.98,737.8,149.45 diff --git a/docs/re/flight-speed-law.md b/docs/re/flight-speed-law.md index f4f145f..cec9ce0 100644 --- a/docs/re/flight-speed-law.md +++ b/docs/re/flight-speed-law.md @@ -545,3 +545,43 @@ better and gives the whole rate-vs-speed curve rather than two points. reimplementation that treats the throttle as setting a speed the craft simply holds will be wrong during manoeuvres. Data: [`captures/pitch-rate-speed-bleed.csv`](captures/pitch-rate-speed-bleed.csv). + + +## โš ๏ธ Polling faster than the guest updates manufactures a curve + +Trying to fit rate against *instantaneous* speed (`rate_curve.py`, one long hold so +the speed-bleed sweeps the range) produced a beautifully clean result that is +entirely an artefact: + +``` +speed 0- 599 rate 29.4 speed 1199-1798 rate 279.8 +speed 599-1199 rate 174.9 speed 1798-2398 rate 317.5 +``` + +Rate rising with speed, and speeds to **4 795** when the craft's maximum is 1 200. +The cause: **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. In that 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` produced a tidy +correlation out of nothing. + +**Fix: aggregate over windows spanning many frames** (0.5 s here). The sum of +`|ฮ”|` over 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, which is immune for the same reason. Only +per-sample instantaneous rates were ever wrong. + +๐ŸŸก **The windowed re-run is not yet a result.** It gives plausible magnitudes +(speed 320โ€“935, rate 80โ€“149 ยฐ/wall-s) but still shows rate *rising* with speed, +against the definition's `AV_PitchPlus_Min` 150 > `_Max` 70. Two disqualifiers: it +ran on an instance where the craft was already tumbling from the previous sweep, so +the row pinning reported **WEAK โ€” craft may not be level**, and the starting speed +was mid-range rather than maximum. A clean answer needs a **fresh flight**, pinning +CONFIDENT, one sweep, nothing before it. Recorded as an open question rather than a +finding โ€” the definitions' `_Min`/`_Max` meaning is exactly what is in doubt, so a +measurement taken through a doubtful instrument cannot settle it. + +Data: [aliased, for reference](captures/rate-curve-aliased-BAD.csv) ยท +[windowed](captures/rate-curve-windowed.csv). diff --git a/tools/re-capture/rate_curve.py b/tools/re-capture/rate_curve.py new file mode 100755 index 0000000..4fab511 --- /dev/null +++ b/tools/re-capture/rate_curve.py @@ -0,0 +1,117 @@ +#!/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 [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())