"""N4 — white-tile washout certification (highlight-preserving shading). Certifies apply_shade (golden_render.py, mirrored as applyShade in canvas-engine.ts): brightening (shade > 1) must preserve pale-tile detail instead of slamming it into the 255 ceiling / soft-clip shoulder. Checks: 1. dimming path is the exact physical multiply (m <= 1 unchanged behaviour) 2. continuity at m = 1 (no curve seam between dim and brighten) 3. white-tile contrast: tile 245 / grout 200 under m = 1.3 keeps >= 60% of the unshaded contrast (the old multiply+clip path collapses it) 4. headroom by construction: any texel under any m in (1, 2] stays < 255 5. monotonic in texel value for fixed m (no detail inversions) 6. documents the bug it fixes: the legacy soft_clip(multiply) path keeps < 25% of the same contrast """ import numpy as np from golden_render import apply_shade, soft_clip def main(): ok = True # 1 — dimming is a pure multiply s = np.linspace(0, 255, 256) for m in (0.4, 0.7, 1.0): got = apply_shade(s, np.full_like(s, m)) want = s * m good = np.allclose(got, want, atol=1e-9) print(f" [{'PASS' if good else 'FAIL'}] dim path m={m}: exact multiply") ok &= good # 2 — continuity at m = 1 below = apply_shade(s, np.full_like(s, 1.0 - 1e-9)) above = apply_shade(s, np.full_like(s, 1.0 + 1e-9)) good = float(np.max(np.abs(above - below))) < 1e-3 print(f" [{'PASS' if good else 'FAIL'}] continuity at m=1: max jump " f"{float(np.max(np.abs(above - below))):.2e}") ok &= good # 3 — white-tile contrast retention under brightening tile, grout, m = 245.0, 200.0, 1.3 base_contrast = tile - grout new = apply_shade(np.array([tile, grout]), np.array([m, m])) new_contrast = float(new[0] - new[1]) retained = new_contrast / base_contrast good = retained >= 0.60 and float(new.max()) < 255.0 print(f" [{'PASS' if good else 'FAIL'}] white-tile contrast: {base_contrast:.0f} -> " f"{new_contrast:.1f} ({retained * 100:.0f}% retained), max {new.max():.1f} < 255") ok &= good # 4 — headroom by construction: any texel BELOW white stays below white # (pure 255 in -> 255 out is correct: white can't get whiter). sub = s[s < 255.0] worst = 0.0 worst_white = 0.0 for m in np.linspace(1.001, 2.0, 40): worst = max(worst, float(apply_shade(sub, np.full_like(sub, m)).max())) worst_white = max(worst_white, float(apply_shade(np.array([255.0]), np.array([m]))[0])) good = worst < 255.0 and worst_white <= 255.0 print(f" [{'PASS' if good else 'FAIL'}] headroom: max output {worst:.2f} < 255 " f"for texel<255, white -> {worst_white:.2f} <= 255, m in (1, 2]") ok &= good # 5 — monotonic in texel for fixed m mono_ok = True for m in (1.1, 1.35, 1.8): got = apply_shade(s, np.full_like(s, m)) mono_ok &= bool(np.all(np.diff(got) > -1e-9)) print(f" [{'PASS' if mono_ok else 'FAIL'}] monotonic in texel for m=1.1/1.35/1.8") ok &= mono_ok # 6 — the legacy path really was the bug legacy = soft_clip(np.array([tile, grout]) * m) legacy_contrast = float(legacy[0] - legacy[1]) good = legacy_contrast / base_contrast < 0.25 print(f" [{'PASS' if good else 'FAIL'}] legacy multiply+clip collapses the same " f"contrast to {legacy_contrast:.1f} ({legacy_contrast / base_contrast * 100:.0f}%)") ok &= good print("\n" + ("ALL N4 SIM CHECKS PASSED" if ok else "N4 SIM CHECKS FAILED")) return 0 if ok else 1 if __name__ == "__main__": raise SystemExit(main())