152 lines
5.8 KiB
Python
152 lines
5.8 KiB
Python
|
|
"""A/B the `crinkliness_mode` repairs against the circulation-deletion incentive.
|
|||
|
|
|
|||
|
|
Evidence for `homemaker-py-ssz` / `homemaker-py-2v1` (DESIGN.md §38.6). The
|
|||
|
|
test: on a constructed layout, delete each unpinned circulation/outside leaf
|
|||
|
|
(by undividing its parent, which merges it into its sibling) and re-score. A
|
|||
|
|
healthy objective should not pay you to do that.
|
|||
|
|
|
|||
|
|
Reports the count of deletions that still improve the score, and the median
|
|||
|
|
score ratio, per mode. Splitting the rows by whether the deleted leaf was
|
|||
|
|
buried or lit is what separates the two mechanisms:
|
|||
|
|
|
|||
|
|
* buried leaves are rewarded because ``quality_uncrinkliness`` returns a hard
|
|||
|
|
0.0, so they contribute zero value (§38.1);
|
|||
|
|
* **lit** leaves are rewarded because ``value_circulation`` (50) is one sixth
|
|||
|
|
of ``value_inside`` (300), so merging corridor into room is a flat ×6 gain
|
|||
|
|
against a ×0.5 connectivity penalty (§38.2 refinement) — which no
|
|||
|
|
``crinkliness_mode`` can touch.
|
|||
|
|
|
|||
|
|
Usage::
|
|||
|
|
|
|||
|
|
python experiments/ab_crinkliness_mode_ssz.py
|
|||
|
|
python experiments/ab_crinkliness_mode_ssz.py --seeds 5 --progdir examples/maple-court
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import argparse
|
|||
|
|
import copy
|
|||
|
|
|
|||
|
|
import numpy as np
|
|||
|
|
|
|||
|
|
from homemaker_layout import dom as dom_mod
|
|||
|
|
from homemaker_layout import driver, fitness, geometry
|
|||
|
|
from homemaker_layout import graph as graph_mod
|
|||
|
|
from homemaker_layout import operators, programme
|
|||
|
|
|
|||
|
|
MODES = ("urb", "floor", "compact_ok", "exempt_circulation")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def make_fitness(progdir: str, mode: str) -> fitness.Fitness:
|
|||
|
|
"""Evaluator matching ``driver.search``'s defaults, with one mode override."""
|
|||
|
|
overrides = dict(driver._overrides_for(
|
|||
|
|
leaf_sharing=True, superpose=False, max_share=None, conn_grade=False,
|
|||
|
|
collapse_insearch=True, multi_use=False) or {})
|
|||
|
|
overrides["crinkliness_mode"] = mode
|
|||
|
|
conf, cost = fitness.load_config(progdir, overrides=overrides)
|
|||
|
|
return fitness.Fitness(conf, cost)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def constructed_seed(progdir: str, seed: int) -> dom_mod.Node:
|
|||
|
|
reqs = programme.load_programme_dir(progdir)
|
|||
|
|
return operators.constructive_topology(
|
|||
|
|
dom_mod.load(f"{progdir}/init.dom"), reqs, np.random.default_rng(seed),
|
|||
|
|
sorted(reqs) + ["C", "O"],
|
|||
|
|
min_storeys=programme.storey_minimum(progdir),
|
|||
|
|
adjacency_aware=True, proportion_aware=True, circ_divisor=3,
|
|||
|
|
leaf_sharing=True, leaf_share_factor=3, depth_balanced=True,
|
|||
|
|
interior_outside=True, outside_divisor=3)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def unpinned_leaves(fit: fitness.Fitness, root: dom_mod.Node) -> list[tuple]:
|
|||
|
|
"""(level, id, type, exposed_area) for circulation/outside leaves — the ones
|
|||
|
|
no missing-space cascade pins in place."""
|
|||
|
|
tree = copy.deepcopy(root)
|
|||
|
|
geometry.clear_cache()
|
|||
|
|
dom_mod.canonicalize_shares(tree)
|
|||
|
|
fit.preprocess_building(tree)
|
|||
|
|
dom_mod.merge_divided(tree)
|
|||
|
|
geometry.clear_cache()
|
|||
|
|
graphs = graph_mod.build_graphs(tree, fit.conf("door_width") or 1.2)
|
|||
|
|
|
|||
|
|
out = []
|
|||
|
|
for li, lvl in enumerate(dom_mod.levels(tree)):
|
|||
|
|
for leaf in lvl.leaves():
|
|||
|
|
if dom_mod.is_outside(leaf) and not dom_mod.is_covered(leaf):
|
|||
|
|
continue
|
|||
|
|
if (leaf.type or "")[:1].upper() not in ("C", "O"):
|
|||
|
|
continue
|
|||
|
|
out.append((li, leaf.id, leaf.type,
|
|||
|
|
fit.area_outside(leaf, graphs[li], {})))
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def delete_leaf(root: dom_mod.Node, li: int, lid: str) -> "dom_mod.Node | None":
|
|||
|
|
"""Undivide the leaf's parent, merging it into its sibling. None if the cut
|
|||
|
|
is inherited (not owned at this storey) or the sibling is itself divided."""
|
|||
|
|
cand = copy.deepcopy(root)
|
|||
|
|
lvls = dom_mod.levels(cand)
|
|||
|
|
if li >= len(lvls):
|
|||
|
|
return None
|
|||
|
|
node = lvls[li].by_id(lid)
|
|||
|
|
if node is None or node.parent is None:
|
|||
|
|
return None
|
|||
|
|
parent = node.parent
|
|||
|
|
if parent.below is not None and parent.below.divided:
|
|||
|
|
return None
|
|||
|
|
sibling = parent.right if parent.left is node else parent.left
|
|||
|
|
if sibling is None or sibling.divided:
|
|||
|
|
return None
|
|||
|
|
parent.division = None
|
|||
|
|
parent.left = parent.right = None
|
|||
|
|
parent.type = sibling.type
|
|||
|
|
dom_mod.link(cand)
|
|||
|
|
geometry.clear_cache()
|
|||
|
|
return cand
|
|||
|
|
|
|||
|
|
|
|||
|
|
def run(progdir: str, seeds: int) -> None:
|
|||
|
|
print(f"programme: {progdir}, {seeds} constructed seeds")
|
|||
|
|
print("a healthy objective rewards NO deletions\n")
|
|||
|
|
header = f"{'mode':<20}{'buried':<14}{'lit':<14}{'all':<14}median x"
|
|||
|
|
print(header)
|
|||
|
|
print("-" * len(header))
|
|||
|
|
|
|||
|
|
for mode in MODES:
|
|||
|
|
fit = make_fitness(progdir, mode)
|
|||
|
|
counts = {"buried": [0, 0], "lit": [0, 0]}
|
|||
|
|
ratios = []
|
|||
|
|
for s in range(seeds):
|
|||
|
|
root = constructed_seed(progdir, s)
|
|||
|
|
base, _ = fit.score_with_fails(copy.deepcopy(root))
|
|||
|
|
for li, lid, _typ, exposed in unpinned_leaves(fit, root):
|
|||
|
|
cand = delete_leaf(root, li, lid)
|
|||
|
|
if cand is None:
|
|||
|
|
continue
|
|||
|
|
score, _ = fit.score_with_fails(copy.deepcopy(cand))
|
|||
|
|
bucket = "buried" if exposed == 0 else "lit"
|
|||
|
|
counts[bucket][1] += 1
|
|||
|
|
ratios.append(score / base)
|
|||
|
|
if score > base:
|
|||
|
|
counts[bucket][0] += 1
|
|||
|
|
tot = [counts["buried"][0] + counts["lit"][0],
|
|||
|
|
counts["buried"][1] + counts["lit"][1]]
|
|||
|
|
median = sorted(ratios)[len(ratios) // 2] if ratios else float("nan")
|
|||
|
|
buried = "%d/%d" % tuple(counts["buried"])
|
|||
|
|
lit = "%d/%d" % tuple(counts["lit"])
|
|||
|
|
both = "%d/%d" % tuple(tot)
|
|||
|
|
print(f"{mode:<20}{buried:<14}{lit:<14}{both:<14}x{median:.2f}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main() -> None:
|
|||
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|||
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|||
|
|
ap.add_argument("--progdir", default="examples/harbor-house")
|
|||
|
|
ap.add_argument("--seeds", type=int, default=3)
|
|||
|
|
args = ap.parse_args()
|
|||
|
|
run(args.progdir, args.seeds)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|