2026-06-28 14:23:34 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""High-budget harbor-house floor probe on the CURRENT full default stack
|
|
|
|
|
(homemaker-py-71d.1).
|
|
|
|
|
|
|
|
|
|
Decides 71d (failure-directed repair operator) go/no-go. 71d's premise: the
|
|
|
|
|
harbor 3M-eval plateau (27 fails, 3m.dom) is dominated by LANDLOCKED crinkliness
|
|
|
|
|
(leaf area_outside==0 -> crink==0 -> quality_uncrinkliness hits the
|
|
|
|
|
`if not crink: return 0.0` branch, fitness.py:355 -> guaranteed fail for ALL
|
|
|
|
|
ratios), fixable only by topology (interior O courtyards). That fix
|
|
|
|
|
(interior_outside, odiv=3) has since shipped DEFAULT-ON (erc.8, §13.6). So:
|
|
|
|
|
re-run harbor at high budget on the full default stack and split the residual
|
|
|
|
|
crinkliness fails into LANDLOCKED (area_outside==0, 71d's target) vs
|
|
|
|
|
UNDER-EXPOSED (0 < crink < target, reachable by ratios/seeding). If landlocked
|
|
|
|
|
still dominates -> 71d worth it; if interior-O dissolved it -> 71d redundant.
|
|
|
|
|
|
|
|
|
|
Run SERIAL (n_workers=1) — the leaf-share relaxed objective is injected by
|
|
|
|
|
monkeypatching fitness.load_config, which does NOT propagate into
|
|
|
|
|
ProcessPoolExecutor workers (they re-import fitness fresh, scoring strict ->
|
|
|
|
|
fail-count MISMATCH). The whole §13.x ladder was run serial for this reason.
|
|
|
|
|
URB_NO_OCCLUSION=1 python3 experiments/probe_harbor_floor.py [budget] [seed]
|
|
|
|
|
Defaults: budget=1_000_000, seed=0. Serial ~84 ev/s => 1M ~ 3.3 h.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import copy
|
|
|
|
|
import math
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import time
|
|
|
|
|
from collections import Counter
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
|
|
from homemaker_layout import dom, driver, fitness, geometry # noqa: E402
|
|
|
|
|
from homemaker_layout import graph as graph_mod # noqa: E402
|
|
|
|
|
from homemaker_layout import dom as dom_mod # noqa: E402
|
|
|
|
|
|
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
|
|
|
HARBOR = REPO / "examples" / "harbor-house"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def classify_crinkliness(root, conf, cost):
|
|
|
|
|
"""For the scored (merged) tree, return per-crinkliness-fail leaf classes.
|
|
|
|
|
|
|
|
|
|
Mirrors _evaluate_full Phase-2 graph setup so area_outside matches what the
|
|
|
|
|
evaluator saw. Returns (fails, classes) where classes maps
|
|
|
|
|
'level/leafid' -> ('landlocked'|'under-exposed', crink, area_outside, type).
|
|
|
|
|
"""
|
|
|
|
|
fit = fitness.Fitness(conf, cost)
|
|
|
|
|
# score_with_fails merges the tree in place and yields canonical fails
|
|
|
|
|
score, fails = fit.score_with_fails(copy.deepcopy(root))
|
|
|
|
|
|
|
|
|
|
# Re-derive the merged tree + base graphs exactly as the evaluator does,
|
|
|
|
|
# so area_outside/crinkliness reproduce the scored values.
|
|
|
|
|
work = copy.deepcopy(root)
|
|
|
|
|
fit2 = fitness.Fitness(conf, cost)
|
|
|
|
|
fit2.preprocess_building(work)
|
|
|
|
|
geometry.clear_cache()
|
|
|
|
|
dom_mod.merge_divided(work)
|
|
|
|
|
geometry.clear_cache()
|
|
|
|
|
door_w = fit2.conf("door_width") or 1.2
|
|
|
|
|
graph_base = graph_mod.build_graphs(work, door_w)
|
|
|
|
|
lvls = dom_mod.levels(work)
|
|
|
|
|
|
|
|
|
|
leaf_metric = {} # 'level/id' -> (crink, area_outside, type)
|
|
|
|
|
for li, lvl in enumerate(lvls):
|
|
|
|
|
G = graph_base[li]
|
|
|
|
|
groups = geometry.boundary_groups(lvl)
|
|
|
|
|
for leaf in lvl.leaves():
|
|
|
|
|
if not dom_mod.is_usable(leaf):
|
|
|
|
|
continue
|
|
|
|
|
if dom_mod.is_outside(leaf) and not dom_mod.is_covered(leaf):
|
|
|
|
|
continue # the O leaves themselves are exempt (quality 1.0)
|
|
|
|
|
ao = fit2.area_outside(leaf, G, groups)
|
|
|
|
|
crink = fit2.crinkliness(leaf, G, groups)
|
|
|
|
|
leaf_metric[f"{li}/{leaf.id}"] = (crink, ao, leaf.type or "")
|
|
|
|
|
|
|
|
|
|
classes = {}
|
|
|
|
|
for f in fails:
|
|
|
|
|
if not f.endswith(" crinkliness"):
|
|
|
|
|
continue
|
|
|
|
|
key = f[: -len(" crinkliness")]
|
|
|
|
|
crink, ao, ltype = leaf_metric.get(key, (None, None, "?"))
|
|
|
|
|
if ao is None:
|
|
|
|
|
cls = "unknown"
|
|
|
|
|
elif ao <= 1e-9:
|
|
|
|
|
cls = "landlocked"
|
|
|
|
|
else:
|
|
|
|
|
cls = "under-exposed"
|
|
|
|
|
classes[key] = (cls, crink, ao, ltype)
|
|
|
|
|
return score, fails, classes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
budget = int(sys.argv[1]) if len(sys.argv) > 1 else 1_000_000
|
|
|
|
|
seed = int(sys.argv[2]) if len(sys.argv) > 2 else 0
|
|
|
|
|
out = REPO / "scratch" / "harbor_floor_probe" / f"harbor_fullstack_s{seed}.dom"
|
|
|
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
os.environ.setdefault("URB_NO_OCCLUSION", "1")
|
|
|
|
|
|
|
|
|
|
# Full default stack, leaf-share config injected into the WHOLE pipeline so
|
|
|
|
|
# the search and the re-score share one relaxed objective (§13.3), matching
|
|
|
|
|
# how every §13.x floor number was produced.
|
|
|
|
|
_orig_load = fitness.load_config
|
|
|
|
|
|
2026-06-28 22:04:35 +01:00
|
|
|
def _load_with_sharing(directory, overrides=None):
|
|
|
|
|
conf, cost = _orig_load(directory, overrides=overrides)
|
2026-06-28 14:23:34 +01:00
|
|
|
conf = dict(conf)
|
|
|
|
|
conf["leaf_sharing"] = True
|
|
|
|
|
conf["max_share"] = 3
|
|
|
|
|
return conf, cost
|
|
|
|
|
|
|
|
|
|
fitness.load_config = _load_with_sharing
|
|
|
|
|
conf, cost = fitness.load_config(HARBOR)
|
|
|
|
|
|
|
|
|
|
seed_root = dom.load(str(HARBOR / "init.dom"))
|
|
|
|
|
print(f"=== harbor floor probe: budget={budget} seed={seed} serial ===",
|
|
|
|
|
flush=True)
|
|
|
|
|
print("stack: leaf_sharing(3) + depth_balanced + interior_outside(odiv=3) "
|
|
|
|
|
"+ circ_divisor=3 + proportion-aware (current defaults)", flush=True)
|
|
|
|
|
t0 = time.perf_counter()
|
|
|
|
|
|
|
|
|
|
r = driver.search_staged(
|
|
|
|
|
seed_root, HARBOR,
|
|
|
|
|
budget=budget, pop_size=16, child_budget=80, seed_budget=300,
|
|
|
|
|
stage1_frac=0.4, base_p=0.15, p_crossover=0.2, seed=seed,
|
|
|
|
|
log=lambda m: print(m, flush=True),
|
|
|
|
|
seed_adjacency_aware=True, seed_proportion_aware=True,
|
|
|
|
|
circ_divisor=3,
|
|
|
|
|
leaf_sharing=True, leaf_share_factor=3,
|
|
|
|
|
depth_balanced=True,
|
|
|
|
|
interior_outside=True, outside_divisor=3,
|
|
|
|
|
)
|
|
|
|
|
elapsed = time.perf_counter() - t0
|
|
|
|
|
print(f"\n--- done in {elapsed:.0f}s ({r.n_evals/elapsed:.1f} ev/s), "
|
|
|
|
|
f"{r.n_evals} evals across {r.n_topologies} topologies ---", flush=True)
|
|
|
|
|
print(f"best: {r.best.fitness:.6g} ({r.best.n_fails} fails) via {r.best.lineage}",
|
|
|
|
|
flush=True)
|
|
|
|
|
|
|
|
|
|
dom.dump(r.best.root, str(out))
|
|
|
|
|
|
|
|
|
|
score, fails, classes = classify_crinkliness(r.best.root, conf, cost)
|
|
|
|
|
ok = math.isclose(score, r.best.fitness, rel_tol=1e-9)
|
|
|
|
|
print(f"\nre-scored: {score:.6g} ({len(fails)} fails) "
|
|
|
|
|
f"{'OK' if ok else 'MISMATCH'}", flush=True)
|
|
|
|
|
|
|
|
|
|
# Fail-type histogram (last token of each fail string).
|
|
|
|
|
types = Counter(f.split()[-1] if " " in f else f for f in fails)
|
|
|
|
|
print("\nfail-type histogram:", flush=True)
|
|
|
|
|
for t, n in types.most_common():
|
|
|
|
|
print(f" {n:3d} {t}", flush=True)
|
|
|
|
|
|
|
|
|
|
# Crinkliness landlocked split — the 71d decision metric.
|
|
|
|
|
cls_count = Counter(v[0] for v in classes.values())
|
|
|
|
|
n_crink = len(classes)
|
|
|
|
|
print(f"\ncrinkliness fails: {n_crink} total", flush=True)
|
|
|
|
|
for c in ("landlocked", "under-exposed", "unknown"):
|
|
|
|
|
if cls_count.get(c):
|
|
|
|
|
print(f" {cls_count[c]:3d} {c}", flush=True)
|
|
|
|
|
print("\nper-crinkliness-leaf detail (key | class | crink | area_outside | type):",
|
|
|
|
|
flush=True)
|
|
|
|
|
for key, (cls, crink, ao, ltype) in sorted(classes.items()):
|
|
|
|
|
cs = f"{crink:.3f}" if crink is not None else "?"
|
|
|
|
|
aos = f"{ao:.2f}" if ao is not None else "?"
|
|
|
|
|
print(f" {key:18s} {cls:13s} crink={cs:>7s} ao={aos:>7s} type={ltype}",
|
|
|
|
|
flush=True)
|
|
|
|
|
|
|
|
|
|
landlocked = cls_count.get("landlocked", 0)
|
|
|
|
|
print(f"\nVERDICT INPUT: {landlocked}/{n_crink} crinkliness fails are "
|
|
|
|
|
f"LANDLOCKED (71d's ratio-invariant target); total fails {len(fails)}.",
|
|
|
|
|
flush=True)
|
|
|
|
|
return 0 if ok else 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|