experiments/diag_edge_too_long.py: the 6 harbor edge-too-long fails are 2 locations — a share=3 combined leaf (247 m², aspect 1.2; flat 8 m cap not share-aware, unlike quality_size's k×target) accounting for ~4, and one 1.2×16.7 m narrow sliver (~2, also caught by width/proportion). No corridors. Files homemaker-py-hph (share-aware edge-too-long fix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JygRv4n2dcyDQqMiDRe7TN
125 lines
4.4 KiB
Python
125 lines
4.4 KiB
Python
#!/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):
|
||
c, k = _orig(d); 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}")
|