homemaker-layout/experiments/diag_leaf_shapefail.py

151 lines
6 KiB
Python
Raw Permalink Normal View History

#!/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 652 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())