Splits the flat outer-search comparator (-n_fails, fitness) into a tiered (-n_hard, -n_soft, fitness) so search budget stops being spent polishing SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/ staircase-volume) while HARD structural fails (missing space, wrong/ required level, level/circulation/vertical connectivity, adjacency, stairs, covered-outside, storey limits, public access) remain unfixed. fitness.classify_fail_tier/tier_counts classify every fail string emitted across fitness.py and graph.py, raising on anything unrecognised so new fail sites must declare a tier. Validated against all real fail strings in the checked-in corpus plus every fail-emission call site read from source. driver.Individual gains n_hard/n_soft (populated from innerloop.Result. fail_lines); search(use_tiers=...) swaps the comparator when set (default off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched). evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS. experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3 seeds, 20k evals) in the background; results pending. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
106 lines
4.2 KiB
Python
106 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Hard/soft fail tiering A/B (homemaker-py-2g7.3, DESIGN.md §37).
|
|
|
|
Acceptance criteria: "tiered comparator behind a flag with A/B on harbor+maple
|
|
(3 seeds, 20k evals): hard-fail count at budget strictly better or equal on
|
|
mean, no §4.9 regression; report shows hard/soft split."
|
|
|
|
Compares the outer comparator (-n_fails, fitness) [use_tiers=False, the
|
|
existing default] against (-n_hard, -n_soft, fitness) [use_tiers=True] on
|
|
harbor-house and maple-court, 3 seeds each, budget=20000 native evals/run.
|
|
Reports mean hard/soft/total fail counts per config and the per-seed deltas.
|
|
|
|
Usage:
|
|
URB_NO_OCCLUSION=1 python3 experiments/tier_ab_2g7_3.py \
|
|
[budget] [n_seeds] [workers] [out_dir]
|
|
|
|
Defaults: budget=20000, n_seeds=3, workers=4, scratch/tier_ab_2g7_3.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
from homemaker_layout import dom, driver # noqa: E402
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
PROGRAMMES = ["harbor-house", "maple-court"]
|
|
|
|
|
|
def _run(programme_dir: Path, seed: int, budget: int, workers: int, use_tiers: bool):
|
|
seed_root = dom.load(str(programme_dir / "init.dom"))
|
|
t0 = time.perf_counter()
|
|
r = driver.search(
|
|
seed_root, programme_dir, budget=budget, pop_size=16, child_budget=80,
|
|
seed_budget=300, p_crossover=0.2, seed=seed, n_workers=workers,
|
|
leaf_sharing=True, use_tiers=use_tiers,
|
|
)
|
|
dt = time.perf_counter() - t0
|
|
return {
|
|
"n_fails": r.best.n_fails, "n_hard": r.best.n_hard, "n_soft": r.best.n_soft,
|
|
"fitness": r.best.fitness, "n_evals": r.n_evals, "wall_s": dt,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
budget = int(sys.argv[1]) if len(sys.argv) > 1 else 20000
|
|
n_seeds = int(sys.argv[2]) if len(sys.argv) > 2 else 3
|
|
workers = int(sys.argv[3]) if len(sys.argv) > 3 else 4
|
|
out_dir = Path(sys.argv[4]) if len(sys.argv) > 4 else (REPO / "scratch" / "tier_ab_2g7_3")
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
print(f"budget : {budget}")
|
|
print(f"n_seeds : {n_seeds}")
|
|
print(f"workers : {workers}")
|
|
print(f"programmes: {PROGRAMMES}")
|
|
print(flush=True)
|
|
|
|
t_start = time.perf_counter()
|
|
results: dict[str, dict[str, list[dict]]] = {}
|
|
|
|
for prog_name in PROGRAMMES:
|
|
programme_dir = REPO / "examples" / prog_name
|
|
results[prog_name] = {"flat": [], "tiered": []}
|
|
print(f"=== {prog_name} ===", flush=True)
|
|
for seed in range(n_seeds):
|
|
for label, use_tiers in (("flat", False), ("tiered", True)):
|
|
res = _run(programme_dir, seed, budget, workers, use_tiers)
|
|
results[prog_name][label].append(res)
|
|
print(f" seed {seed} {label:6s}: hard={res['n_hard']} "
|
|
f"soft={res['n_soft']} total={res['n_fails']} "
|
|
f"fitness={res['fitness']:.6g} evals={res['n_evals']} "
|
|
f"({res['wall_s']:.0f}s)", flush=True)
|
|
|
|
print()
|
|
print("=" * 72)
|
|
print("SUMMARY (mean over seeds)")
|
|
print("=" * 72)
|
|
overall_ok = True
|
|
for prog_name in PROGRAMMES:
|
|
for label in ("flat", "tiered"):
|
|
rows = results[prog_name][label]
|
|
mh = sum(r["n_hard"] for r in rows) / len(rows)
|
|
ms = sum(r["n_soft"] for r in rows) / len(rows)
|
|
mt = sum(r["n_fails"] for r in rows) / len(rows)
|
|
print(f" {prog_name:14s} {label:6s}: hard={mh:.2f} soft={ms:.2f} "
|
|
f"total={mt:.2f}")
|
|
flat_hard = sum(r["n_hard"] for r in results[prog_name]["flat"]) / n_seeds
|
|
tiered_hard = sum(r["n_hard"] for r in results[prog_name]["tiered"]) / n_seeds
|
|
ok = tiered_hard <= flat_hard
|
|
overall_ok = overall_ok and ok
|
|
print(f" {prog_name:14s} hard-fail mean: flat={flat_hard:.2f} "
|
|
f"tiered={tiered_hard:.2f} -> {'PASS' if ok else 'FAIL'}")
|
|
print()
|
|
|
|
print(f"ACCEPTANCE (hard-fail mean strictly better-or-equal, both "
|
|
f"programmes): {'PASS' if overall_ok else 'FAIL'}")
|
|
print(f"wall: {time.perf_counter() - t_start:.0f}s")
|
|
print("=" * 72, flush=True)
|
|
return 0 if overall_ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|