Controlled synthetic sweep (maple-court, room set fixed, circ_divisor 2->9) shows per-leaf shape-fail is FLAT vs slicing density (1.72-1.94, no trend) while TOTAL shape fails track leaf count linearly (139->116). Crinkliness dominates (~0.8/leaf) and is flat; cuts are already squarest yet still pay ~1.8 fails/leaf. Floor is INTRINSIC to per-leaf slicing, not cut quality. Verdict: prioritise leaf-sharing (erc.3); deprioritise compactness-cuts (erc.5 -> P4). Adds experiments/diag_leaf_shapefail.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
150 lines
6 KiB
Python
150 lines
6 KiB
Python
#!/usr/bin/env python3
|
||
"""Diagnostic A (homemaker-py-erc.1, DESIGN.md §13.1): per-leaf shape-fail vs
|
||
density / granularity.
|
||
|
||
GATES the leaf-sharing vs compactness-cuts decision. Open question from §12.3:
|
||
is the shape floor INTRINSIC to slicing at this leaf density (→ fewer leaves is
|
||
the only lever → leaf-sharing), or fixable by better-shaped cuts at the SAME
|
||
leaf count (→ compactness-cuts can pay)?
|
||
|
||
Reads, does not change behaviour. For each programme × seed it builds the §12.2
|
||
constructive seed (adjacency-aware, proportion-aware), lays it out at the
|
||
proportion-aware TARGET geometry — the squarest geometry the inner loop warm
|
||
starts from, exactly as operators.predicted_shape_fails does — then counts
|
||
size/width/proportion/crinkliness fails per leaf and reports them against
|
||
leaves-per-room and plot utilisation.
|
||
|
||
Two views:
|
||
(1) CROSS-PROGRAMME density sweep: programmes spanning 6→52 rooms.
|
||
(2) SYNTHETIC granularity sweep: one programme, circ_divisor varied so leaf
|
||
count changes while the room set is held fixed.
|
||
|
||
DECISION RULE: if per-leaf shape-fail is FLAT across densities → floor is
|
||
intrinsic to slicing density → prioritise leaf-sharing (erc.3), deprioritise
|
||
compactness-cuts (erc.5). If it RISES with density → better cuts can pay → keep
|
||
compactness-cuts.
|
||
|
||
Usage:
|
||
URB_NO_OCCLUSION=1 python3 experiments/diag_leaf_shapefail.py
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import copy
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import numpy as np
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||
from homemaker_layout import dom, fitness, geometry, operators, programme # noqa: E402
|
||
|
||
SHAPE = ("size", "width", "proportion", "crinkliness")
|
||
PROGRAMMES = ["programme-house", "harbor-house-l0", "harbor-house", "maple-court"]
|
||
SEEDS = (0, 1, 2)
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
|
||
|
||
def _shape_breakdown(fails) -> dict[str, int]:
|
||
out = {k: 0 for k in SHAPE}
|
||
for f in fails:
|
||
for k in SHAPE:
|
||
if f.endswith(" " + k):
|
||
out[k] += 1
|
||
break
|
||
return out
|
||
|
||
|
||
def _layout_at_target(topo: dom.Node, reqs) -> dom.Node:
|
||
"""Mirror operators.predicted_shape_fails: squarest target-proportional geom."""
|
||
child = copy.deepcopy(topo)
|
||
dom._link(child)
|
||
for lvl in dom.levels(child):
|
||
operators._size_divisions_from_targets(lvl, reqs)
|
||
return child
|
||
|
||
|
||
def _measure(programme_dir: Path, fit, reqs, types, seed_root, circ_divisor, s):
|
||
rng = np.random.default_rng(s)
|
||
topo = operators.constructive_topology(
|
||
seed_root, reqs, rng, types,
|
||
adjacency_aware=True, proportion_aware=True, circ_divisor=circ_divisor)
|
||
laid = _layout_at_target(topo, reqs)
|
||
geometry.clear_cache()
|
||
_score, fails = fit.score_with_fails(copy.deepcopy(laid))
|
||
bd = _shape_breakdown(fails)
|
||
|
||
leaves = [lf for lvl in dom.levels(laid) for lf in lvl.leaves()]
|
||
n_leaves = len(leaves)
|
||
n_rooms = sum(r.count for r in reqs.values())
|
||
|
||
# plot utilisation: sized-room achieved area / total plot area
|
||
sized = {lf for lf in leaves if lf.type in reqs and reqs[lf.type].size > 0}
|
||
geometry.clear_cache()
|
||
occupied = sum(geometry.area(lf) for lf in sized)
|
||
plot = sum(geometry.area(lvl) for lvl in dom.levels(laid))
|
||
util = occupied / plot if plot else float("nan")
|
||
|
||
return {
|
||
"n_leaves": n_leaves, "n_rooms": n_rooms,
|
||
"lpr": n_leaves / n_rooms, "util": util,
|
||
"shape_total": sum(bd.values()), **bd,
|
||
}
|
||
|
||
|
||
def _avg(rows, key):
|
||
return sum(r[key] for r in rows) / len(rows)
|
||
|
||
|
||
def main() -> int:
|
||
print("Diagnostic A — per-leaf shape-fail vs density (§13.1)\n")
|
||
print("Layout: proportion-aware TARGET geometry (predicted_shape_fails proxy)")
|
||
print(f"Seeds: {SEEDS} per-leaf rate = shape-fails / leaves\n")
|
||
|
||
# ---- (1) cross-programme density sweep ----
|
||
print("(1) CROSS-PROGRAMME density sweep")
|
||
hdr = (f"{'programme':<18}{'rooms':>6}{'leaves':>7}{'l/room':>7}{'util':>6}"
|
||
f"{'shape':>7}{'/leaf':>7} {'siz/lf':>7}{'wid/lf':>7}{'prp/lf':>7}{'crk/lf':>7}")
|
||
print(hdr)
|
||
print("-" * len(hdr))
|
||
for name in PROGRAMMES:
|
||
pdir = ROOT / "examples" / name
|
||
reqs = programme.load_programme_dir(pdir)
|
||
types = sorted(reqs) + ["C", "O"]
|
||
conf, cost = fitness.load_config(pdir)
|
||
fit = fitness.Fitness(conf, cost)
|
||
seed_root = dom.load(str(pdir / "init.dom"))
|
||
rows = [_measure(pdir, fit, reqs, types, seed_root, 3, s) for s in SEEDS]
|
||
nl = _avg(rows, "n_leaves")
|
||
print(f"{name:<18}{_avg(rows,'n_rooms'):>6.0f}{nl:>7.1f}"
|
||
f"{_avg(rows,'lpr'):>7.2f}{_avg(rows,'util'):>6.2f}"
|
||
f"{_avg(rows,'shape_total'):>7.1f}{_avg(rows,'shape_total')/nl:>7.3f}"
|
||
f" {_avg(rows,'size')/nl:>7.3f}{_avg(rows,'width')/nl:>7.3f}"
|
||
f"{_avg(rows,'proportion')/nl:>7.3f}{_avg(rows,'crinkliness')/nl:>7.3f}")
|
||
|
||
# ---- (2) synthetic granularity sweep on maple-court ----
|
||
print("\n(2) SYNTHETIC granularity sweep — maple-court, circ_divisor varied")
|
||
print(" (room set fixed, leaf count varied via the c3g circ knob)")
|
||
name = "maple-court"
|
||
pdir = ROOT / "examples" / name
|
||
reqs = programme.load_programme_dir(pdir)
|
||
types = sorted(reqs) + ["C", "O"]
|
||
conf, cost = fitness.load_config(pdir)
|
||
fit = fitness.Fitness(conf, cost)
|
||
seed_root = dom.load(str(pdir / "init.dom"))
|
||
hdr2 = (f"{'circ_div':>9}{'leaves':>7}{'l/room':>7}{'util':>6}"
|
||
f"{'shape':>7}{'/leaf':>7} {'siz/lf':>7}{'wid/lf':>7}{'prp/lf':>7}{'crk/lf':>7}")
|
||
print(hdr2)
|
||
print("-" * len(hdr2))
|
||
for cd in (2, 3, 4, 6, 9):
|
||
rows = [_measure(pdir, fit, reqs, types, seed_root, cd, s) for s in SEEDS]
|
||
nl = _avg(rows, "n_leaves")
|
||
print(f"{cd:>9}{nl:>7.1f}{_avg(rows,'lpr'):>7.2f}{_avg(rows,'util'):>6.2f}"
|
||
f"{_avg(rows,'shape_total'):>7.1f}{_avg(rows,'shape_total')/nl:>7.3f}"
|
||
f" {_avg(rows,'size')/nl:>7.3f}{_avg(rows,'width')/nl:>7.3f}"
|
||
f"{_avg(rows,'proportion')/nl:>7.3f}{_avg(rows,'crinkliness')/nl:>7.3f}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|