acdream/tools/vm6/wind-pixel-proof.py
Erik 26adf75079 docs(render): VM6 code-complete — foliage-wind pixel proof, repeat-floor apparatus, ledger (Campaign VM)
Closes the automated half of slice VM6 at 754d59d9 and records how it was
proven. The first pixel method (clock pin 0 s vs 3 s, wind-off pair as the
control) is recorded as confounded: the treeline silhouette carries a
bimodal 0-or-~280 px rasterisation churn between runs of the same binary,
and two conclusions drawn from it were retracted. The replacement
apparatus (same pin, wind on vs off, two captures per arm, robust mask =
AND of the four cross pairs minus both repeat pairs, shadows off so only
geometry can move) found the round-4 defect (wind welded to the shadow
gate: 1 m wind = 49-65 px vs a 22 px floor while the CPU state was
correct) and, after round 5, gives 14,993 robust wind pixels vs a 65 px
floor, every one on a treeline tree, hillside tree or shoreline bush, with
the wind-off frame matching the pre-round-4 plain pipeline at mean |d|
0.007 (round 4's eec95535: 1.77, the overhead-sun regression the narrow
review caught).

tools/vm6/wind-pixel-proof.py is the analysis command the closeout names;
evidence overlays under docs/research/evidence/vm6. Plan: VM6 ledger row
CODE-COMPLETE 2026-08-23, owner visual gate owed (section VM6 Acceptance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 04:01:18 +02:00

59 lines
2.1 KiB
Python

"""Campaign VM VM6 — robust foliage-wind pixel proof.
Usage: py tools/vm6/wind-pixel-proof.py <capture-root>
<capture-root> holds four run-offline-pixel-gate.ps1 output directories,
all at the SAME ACDREAM_SKY_PHASE_SECONDS pin and sun-shadow-strength=0:
amp-t3, amp-t3b wind on (amplified or default strength)
nowind-t3a, nowind-t3b wind-enabled=false
Each directory contains screenshots/world-offline.png.
Why four captures: the same binary/settings/pin differs run-to-run by 0 or
~280 isolated leaf-edge pixels (rasterisation flips along the treeline), so a
single on-vs-off pair cannot be trusted. The repeat pairs measure that floor;
the robust mask is the AND of all four cross pairs minus both repeat pairs.
See docs/research/2026-08-23-vm6-foliage-wind-pixel-proof.md.
"""
import sys
import numpy as np
from PIL import Image
root = sys.argv[1]
threshold = int(sys.argv[2]) if len(sys.argv) > 2 else 8
def load(name):
return np.asarray(
Image.open(f"{root}/{name}/screenshots/world-offline.png").convert("RGB")
).astype(int)
def strong(a, b):
return np.abs(a - b).max(axis=2) >= threshold
def bands(mask):
ys, _ = np.nonzero(mask)
return np.bincount(ys // 60, minlength=12).tolist() if len(ys) else [0] * 12
A, B, N1, N2 = load("amp-t3"), load("amp-t3b"), load("nowind-t3a"), load("nowind-t3b")
noise = strong(N1, N2)
repeat = strong(A, B)
print(f"threshold |d|>={threshold}")
print("floor wind-off A vs B :", int(noise.sum()), bands(noise))
print("repeat wind-on A vs B :", int(repeat.sum()), bands(repeat))
for name, (x, y) in {
"wind-on A vs off A": (A, N1),
"wind-on A vs off B": (A, N2),
"wind-on B vs off A": (B, N1),
"wind-on B vs off B": (B, N2),
}.items():
m = strong(x, y)
print(f"{name:20s}: {int(m.sum()):6d} {bands(m)}")
robust = strong(A, N1) & strong(A, N2) & strong(B, N1) & strong(B, N2) & ~noise & ~repeat
print("ROBUST wind mask :", int(robust.sum()), bands(robust))
out = A.copy()
out[robust] = [255, 0, 0]
Image.fromarray(out.astype(np.uint8)).save(f"{root}/diff-robust-wind.png")
print(f"overlay: {root}/diff-robust-wind.png")