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>
126 lines
4.2 KiB
Python
Executable File
126 lines
4.2 KiB
Python
Executable File
#!/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())
|