test(simulation): the conservation gate's monoculture check was a tautology (T-1213)
D-258 invariant 2 says descending the ladder must reveal COMPOSITION — a cell
reading forest must be able to contain the clearings and rock the vote
suppressed. One assertion stood behind that, and it read:
assert!(tally.len() > 1 || share == 1.0, ...)
A single-class tally has a 100% share by definition, so both branches are always
satisfiable: the check could never fail, including in the exact case its own
message names, "or nothing was composed". The invariant had a test and no gate.
Split into the two bounds the invariant actually has, because it is two-sided:
conservation caps how much may be invented (majority > 50%, already asserted) and
composition sets a floor on how little (minority >= 0.1%). Verified by raising
the floor to 2% and watching it fail on the measured 1.07%, then restoring it —
the floor is a tripwire for "did anything happen", deliberately far below the
measurement rather than tuned to it.
Measured at the descent ladder's own anchor on Ferrath:
conservation: majority class 3 at 98.9% across 2 classes {1: 175, 3: 16209}
So composition IS working in the data and conservation holds. The map is flat
anyway, and tooling/atlas-flatness (added here) says why the eye was not enough:
rung distinct lum p1-p99
Global 1581 145.69
Region 2923 33.59
District 53 13.72
Quarter 46 11.01
Region carries almost TWICE Global's distinct-colour count while holding a
quarter of its structure — the dither pass adds colour noise, not information, so
a colour-count metric would have called the flattest rung the richest. Structure
falls ~92% from Global to Quarter.
The cause is a channel mismatch rather than a missing generator: composition
perturbs moisture_q/slope_q, and the base map draws morphology hue x elev_q
lightness. The ladder scenarios pass no overlays deliberately, so the composed
fields are never rendered in the very shots that judge this work. Recorded on
T-1213 with the three ways forward; the choice touches D-258 and is Jeroen's.
The gate is still #[ignore]d — noted on the ticket as worth moving into a harness
that runs, since believability and window-derivation already load real bodies in
the normal cargo test path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure how much STRUCTURE an Atlas capture carries, per rung.
|
||||
|
||||
"Flat" is the word T-1213/D-258 use for the defect the descent ladder exposes,
|
||||
but a word cannot be an acceptance gate and an eyeball cannot be a regression
|
||||
test. This turns the ladder into numbers.
|
||||
|
||||
WHY NOT JUST COUNT COLOURS
|
||||
--------------------------
|
||||
Because the count goes the wrong way. Measured on the 2026-08-16 cold ladder:
|
||||
|
||||
rung distinct lum p1-p99
|
||||
Global 1581 145.69
|
||||
Region 2923 33.59
|
||||
District 53 13.72
|
||||
Quarter 46 11.01
|
||||
|
||||
Region carries almost TWICE Global's distinct-colour count while holding a
|
||||
quarter of its structure — that is the dither/stipple pass (T-1194) adding colour
|
||||
noise, not information. A metric that rewards speckle would have called the
|
||||
flattest rung the richest. So the headline number here is the 1st-99th percentile
|
||||
luminance spread, which ignores per-pixel noise and measures the large-scale
|
||||
variation a map is actually read for; distinct-count is reported alongside
|
||||
precisely so the two can be seen disagreeing.
|
||||
|
||||
Usage:
|
||||
tooling/atlas-flatness .cache/screenshots/atlas_GJ820Bc_land_Region.png [...]
|
||||
tooling/atlas-flatness --ladder # the standard descent ladder
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError: # pragma: no cover - environment guard
|
||||
print("atlas-flatness: Pillow not installed", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
SHOTS = REPO_ROOT / ".cache" / "screenshots"
|
||||
|
||||
# The standard descent ladder: one body, one world point, once per rung.
|
||||
LADDER = [
|
||||
("Global", "atlas_GJ820Bc_Global.png"),
|
||||
("Region", "atlas_GJ820Bc_land_Region.png"),
|
||||
("District", "atlas_GJ820Bc_land_District.png"),
|
||||
("Quarter", "atlas_GJ820Bc_land_Quarter.png"),
|
||||
]
|
||||
|
||||
# Terrain-only crop. The header/legend panels sit top-left and the overlay chips
|
||||
# top-right; both are flat UI fills that would drag every statistic toward
|
||||
# whatever the panel colour happens to be, and they do not vary with the rung.
|
||||
CROP_LEFT = 700
|
||||
CROP_TOP = 120
|
||||
|
||||
|
||||
def luminance(px) -> float:
|
||||
r, g, b = px[:3]
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
|
||||
|
||||
def measure(path: Path) -> dict:
|
||||
img = Image.open(path).convert("RGB")
|
||||
w, h = img.size
|
||||
if w <= CROP_LEFT or h <= CROP_TOP:
|
||||
raise ValueError(f"{path.name} is {w}x{h}, smaller than the UI crop")
|
||||
img = img.crop((CROP_LEFT, CROP_TOP, w, h))
|
||||
# get_flattened_data() is the Pillow 12+ name; getdata() is deprecated there
|
||||
# and removed in 14, but is all that older Pillows have.
|
||||
reader = getattr(img, "get_flattened_data", None) or img.getdata
|
||||
pixels = list(reader())
|
||||
|
||||
stats = {}
|
||||
for name, ch in zip("RGB", zip(*pixels)):
|
||||
n = len(ch)
|
||||
mean = sum(ch) / n
|
||||
stats[name] = (sum((v - mean) ** 2 for v in ch) / n) ** 0.5
|
||||
|
||||
lums = sorted(luminance(p) for p in pixels)
|
||||
n = len(lums)
|
||||
return {
|
||||
"distinct": len(set(pixels)),
|
||||
"std": stats,
|
||||
"spread": lums[int(n * 0.99)] - lums[int(n * 0.01)],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Measure Atlas capture structure")
|
||||
parser.add_argument("images", nargs="*", type=Path)
|
||||
parser.add_argument(
|
||||
"--ladder",
|
||||
action="store_true",
|
||||
help="measure the standard descent ladder in .cache/screenshots/",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
targets = []
|
||||
if args.ladder:
|
||||
targets = [(label, SHOTS / name) for label, name in LADDER]
|
||||
targets += [(p.stem, p) for p in args.images]
|
||||
|
||||
if not targets:
|
||||
parser.print_help()
|
||||
return 2
|
||||
|
||||
print(f"{'rung':22} {'distinct':>9} {'R std':>7} {'G std':>7} {'B std':>7} {'lum p1-p99':>11}")
|
||||
missing = 0
|
||||
for label, path in targets:
|
||||
if not path.exists():
|
||||
print(f"{label:22} MISSING {path}")
|
||||
missing += 1
|
||||
continue
|
||||
m = measure(path)
|
||||
r, g, b = (m["std"][c] for c in "RGB")
|
||||
print(
|
||||
f"{label:22} {m['distinct']:>9} {r:>7.2f} {g:>7.2f} {b:>7.2f} {m['spread']:>11.2f}"
|
||||
)
|
||||
return 1 if missing else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user