151 lines
6 KiB
Python
151 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())
|