homemaker-layout/experiments/measure_seed_geometry.py
Bruno Postle 995342d0a4 Phase 7 §12.2: proportion-aware constructive seeding + storey_minimum fix (leu.2, cq1)
Size each constructive-seed cut from leaf TARGET areas (division=[f,f] gives
left area-fraction f) and pick each cut's rotation for child squareness — both
derived from target dims, topology/type assignment untouched. Area-only
regressed (slivers); rotation choice is what makes it pay.

End-to-end (20000 evals, 3 seeds, staged): harbor 85.3->74.0 (-13%, best 69),
maple-court 151.7->136.0 (-10%, best 126). PROP=0 reproduces the §11.7/§12.1
baselines exactly. programme-house regresses at fixed budget (deeper local
optimum walls off the undivide restructuring path) but a budget sweep shows
it's convergence speed, not a worse asymptote (PROP=1 reaches 1 fail at 150k).
Default-on (seed_proportion_aware=True, env PROP=1).

cq1: n_storeys now honours storey_minimum, not just level: keys — programme-house
(storey_minimum:2, all rooms level:0) was seeded one storey short and fell
through to plain search. New programme.storey_minimum()/n_storeys_for();
driver.search passes min_storeys to the seeder; search_staged routes on the max.
No-op for harbor/maple; programme-house single-stage 8.0->5.0.

New maple-court best (126) saved as generated.dom. 204 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 14:04:42 +01:00

83 lines
3 KiB
Python

#!/usr/bin/env python3
"""Raw-seed fail breakdown: proportion-aware split sizing A/B (homemaker-py-leu.2).
Builds the *constructive* seed (operators.constructive_topology, single-stage,
adjacency-aware) with proportion-aware split sizing OFF then ON, scores each raw
seed with native fitness BEFORE the inner loop, and tallies fails by family. The
claim under test (DESIGN.md §12.2): sizing cuts from target areas drops the
geometry family (size/width/proportion/crinkliness) at the raw seed without
regressing the adjacency/access family that §11.6/§11.7 fixed.
Usage:
URB_NO_OCCLUSION=1 python3 experiments/measure_seed_geometry.py <programme_dir> [n_seeds]
"""
from __future__ import annotations
import copy
import sys
from pathlib import Path
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from homemaker_layout import dom, fitness, operators, programme # noqa: E402
GEOM = ("perpendicular", "proportion", "size", "width", "crinkliness")
def _categorise(fails: tuple[str, ...]) -> dict[str, int]:
cats = {"geometry": 0, "access_adj": 0, "missing": 0, "other": 0}
for f in fails:
if f.endswith(GEOM) or "edge too long" in f:
cats["geometry"] += 1
elif "access" in f or "inaccessible" in f or "adjacent" in f or "adjacency" in f:
cats["access_adj"] += 1
elif "missing" in f:
cats["missing"] += 1
else:
cats["other"] += 1
return cats
def main() -> int:
programme_dir = Path(sys.argv[1])
n_seeds = int(sys.argv[2]) if len(sys.argv) > 2 else 10
reqs = programme.load_programme_dir(programme_dir)
types = sorted(reqs) + ["C", "O"]
conf, cost = fitness.load_config(programme_dir)
fit = fitness.Fitness(conf, cost)
seed_file = programme_dir / "init.dom"
seed_root = dom.load(str(seed_file))
print(f"programme : {programme_dir.name} ({len(reqs)} codes)")
print(f"seeds : {n_seeds} (single-stage constructive, adjacency-aware)\n")
agg = {0: {"total": 0, "geometry": 0, "access_adj": 0, "missing": 0, "other": 0},
1: {"total": 0, "geometry": 0, "access_adj": 0, "missing": 0, "other": 0}}
for s in range(n_seeds):
for prop in (False, True):
rng = np.random.default_rng(s)
topo = operators.constructive_topology(
seed_root, reqs, rng, types,
adjacency_aware=True, proportion_aware=prop)
_score, fails = fit.score_with_fails(copy.deepcopy(topo))
cats = _categorise(fails)
agg[int(prop)]["total"] += len(fails)
for k, v in cats.items():
agg[int(prop)][k] += v
def _avg(d, k):
return d[k] / n_seeds
print(f"{'family':<12} {'PROP=0 (before)':>16} {'PROP=1 (after)':>16}")
for k in ("geometry", "access_adj", "missing", "other", "total"):
print(f"{k:<12} {_avg(agg[0], k):>16.1f} {_avg(agg[1], k):>16.1f}")
return 0
if __name__ == "__main__":
sys.exit(main())