homemaker-layout/experiments/diag_leaf_shapefail.py
Bruno Postle 7bd4adf32a erc.1: Diagnostic A — per-leaf shape-fail vs density (§13.1)
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>
2026-06-23 22:06:04 +01:00

150 lines
6 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
"""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())