homemaker-layout/experiments/diag_edge_too_long.py
Bruno Postle bb9b355f14 x3b/§13.10: productionise leaf-sharing — per-code share grain + CLI wiring
Make the §13.3 lever a first-class feature, not experiment-only.

- programme.py: SpaceReq.share (default 1) + has_share, parsed from
  patterns.config 'share: N'.
- operators._share_grain: resolve per-code grain from leaf_share_factor
  selector — 0 = per-code opt-in (share iff share:N>=2), >=2 = global with
  per-code override (share:1 opts OUT, share:N sets grain). _share_rooms
  groups per resolved grain.
- End-to-end conf injection without monkeypatch: load_config(overrides=)
  merges run-level keys last; driver.search / innerloop.optimise /
  NativeEvaluator / _fitness_for thread conf_overrides={leaf_sharing:True}
  through both inner-loop and off-tree scorers when sharing is on.
- homemaker-evolve: --leaf-sharing/--no-leaf-sharing + --leaf-share-factor
  (env HOMEMAKER_LEAF_SHARING / HOMEMAKER_LEAF_SHARE_FACTOR).
- Example programmes untouched (§13.3/§13.9 stay reproducible). Experiment
  load_config monkeypatches updated to accept overrides=.

Tests: grain modes, opt-out, default-OFF parity, load_config overrides,
programme parse, CLI parse. 233 pass. Smoke: harbor 37 vs 95 fails on/off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:04:35 +01:00

126 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Dissect the edge-too-long fails in the harbor probe best (§13.7 follow-up).
Tests three hypotheses for why edge-too-long is now harbor's top fail class:
(1) COMBINED leaf — a shared leaf (share>1) holds k rooms, so it is ~k× a
room's area and its walls run long; would vanish if actually subdivided.
(2) CORRIDOR — a circulation leaf, long+thin; long edge inevitable.
(3) NARROW ROOM — a non-shared room at/near target AREA but high aspect
(one wall >8m because the room is too narrow); a real layout problem.
(4) OVERSIZE ROOM — a non-shared room well above target area.
For each flagged leaf prints: type, circ?, share k, area vs (k×)target,
narrowest width, the long edge length(s), aspect, and a verdict tag.
"""
from __future__ import annotations
import copy, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from homemaker_layout import dom, 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"
BEST = REPO / "scratch" / "harbor_floor_probe" / "harbor_fullstack_s0.dom"
# same relaxed-config injection the probe used, so fails match
_orig = fitness.load_config
def _load(d, overrides=None):
c, k = _orig(d, overrides=overrides); c = dict(c)
c["leaf_sharing"] = True; c["max_share"] = 3
return c, k
fitness.load_config = _load
conf, cost = fitness.load_config(HARBOR)
fit = fitness.Fitness(conf, cost)
root = dom.load(str(BEST))
score, fails = fit.score_with_fails(copy.deepcopy(root))
edge_fails = [f for f in fails if "edge too long" in f]
print(f"{BEST.name}: {len(fails)} fails, {len(edge_fails)} edge-too-long\n")
for f in edge_fails:
print(" ", f)
print()
# rebuild merged tree + base graphs exactly as _evaluate_full does
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)
by_id = {} # 'level/id' -> (leaf, level)
for li, lvl in enumerate(lvls):
for leaf in lvl.leaves():
by_id[f"{li}/{leaf.id}"] = (leaf, li)
def edges(leaf):
return [geometry.edge_length(leaf, e) for e in range(4)]
def t0(leaf):
return (leaf.type or "?")[0].lower()
def target_area(leaf):
try:
p = fit2.get_space_params(leaf.type, "size")
return p[0]
except Exception:
return None
def describe(leaf, li):
ty = leaf.type or "?"
circ = dom_mod.is_circulation(leaf)
out = dom_mod.is_outside(leaf)
share = graph_mod.leaf_share(leaf, 3)
area = geometry.area(leaf)
narrow = geometry.length_narrowest(leaf)
es = edges(leaf)
longest = max(es)
aspect = longest / narrow if narrow else float("inf")
tgt = target_area(leaf)
ke = share if share > 1 else 1
tgt_eff = (tgt * ke) if tgt else None
# classify
if share > 1:
tag = f"COMBINED (share={share})"
elif circ:
tag = "CORRIDOR (circulation)"
elif tgt and area >= 0.85 * tgt and aspect > 1.8:
tag = "NARROW ROOM (area ok, aspect bad)"
elif tgt and area > 1.3 * tgt:
tag = "OVERSIZE ROOM"
else:
tag = "other"
ts = f"{tgt:.0f}" if tgt else "n/a"
tes = f"{tgt_eff:.0f}" if tgt_eff else "n/a"
print(f" {li}/{leaf.id:10s} type={ty:5s} circ={int(circ)} out={int(out)} "
f"share={share} area={area:5.1f} (tgt {ts}, k*tgt {tes})")
print(f" narrowest={narrow:4.1f} edges={[round(x,1) for x in es]} "
f"longest={longest:4.1f} aspect={aspect:4.1f} -> {tag}")
return tag
tags = []
seen = set()
for f in edge_fails:
outside = "outside edge too long" in f
parts = f.split()
key = parts[0] # 'level/id'
if outside:
ent = by_id.get(key)
if ent:
tags.append(describe(*ent))
else:
# '{level}/{a.id} {b.id} edge too long' — both leaves border the long wall
li_a = key
b_id = parts[1]
li = li_a.split("/")[0]
for k in (li_a, f"{li}/{b_id}"):
if k in by_id and k not in seen:
seen.add(k)
tags.append(describe(*by_id[k]))
print("\n=== classification tally ===")
from collections import Counter
for tag, n in Counter(tags).most_common():
print(f" {n:2d} {tag}")