homemaker-layout/experiments/diag_edge_too_long.py
Bruno Postle dba85094ea hph: dissect §13.7 edge-too-long — leaf-sharing artifact + narrow sliver
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
2026-06-28 14:54:55 +01:00

125 lines
4.4 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):
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}")