homemaker-layout/tests/test_collapse_global.py
Bruno Postle 880a214d96 94g: public-access pin + keep-better wrapper + CLI/finish-hook wiring
Public-access term (preserve_public_access, default on): when the building's
only street access is an l/k ROOM neighbour of a public outside leaf (no
circulation fallback — an existential building-level check the per-leaf
objective can't see), that leaf is pinned (kept, its demand slot decremented)
so the collapse can't drop "no outside public access". Best layout 15→13
becomes 15→12 with zero new fails; sweep total 172→171, still monotone.

collapse_finish(root, **kw) -> (tree, base, coll, applied): keep-better wrapper,
scores on throwaway copies (score_with_fails merges in place), returns the
collapse only if fails don't increase.

Wiring: driver.collapse_best updates result.best (lineage +collapse, canonical
re-score); evolve.py runs it after the sharing polish behind --collapse/
--no-collapse (default on). New homemaker-collapse CLI (collapse_cmd.py) applies
it to an existing .dom, writing <stem>.collapsed.dom.

tests/test_collapse_global.py: demand-set relabel, level hard constraint, c/o/s
exclusion, no-op safety, keep-better/unmerged. 267 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
2026-07-18 10:29:44 +01:00

120 lines
4.5 KiB
Python

"""Tests for the finish-time global cell→room collapse (homemaker-py-94g).
Covers the contracts of Fitness.collapse_global / collapse_finish:
- global relabel to the demand set (larger cell → larger target)
- hard level constraint (never introduce a wrong-level fail)
- c/o/s partition exclusion (circulation/structure cells are never relabelled)
- no-op safety (no programme) and the keep-better wrapper
"""
from homemaker_layout import geometry
from homemaker_layout.dom import Node, _link_subtree
from homemaker_layout.fitness import Fitness
def _two_leaf_root(t_left: str, t_right: str, side: float = 6.0, div: float = 0.4):
geometry.clear_cache()
root = Node(
node=[[0, 0], [side, 0], [side, side], [0, side]],
rotation=0, division=[div, div],
left=Node(type=t_left), right=Node(type=t_right),
)
_link_subtree(root, None, "")
return root
def _conf(spaces, **extra):
return {"spaces": spaces, **extra}
# --------------------------------------------------------------------------- #
# Global relabel
# --------------------------------------------------------------------------- #
def test_relabels_to_demand_set():
# two leaves both typed b1; demand {b1, b2} — collapse spreads them so the
# larger cell takes the larger target (b1=16) and the smaller takes b2=12.
conf = _conf({
"b1": {"size": [16.0, 4.0], "width": [4.0, 1.0], "proportion": [1.5, 0.5]},
"b2": {"size": [12.0, 3.0], "width": [3.5, 0.8], "proportion": [1.5, 0.5]},
})
fit = Fitness(conf=conf)
root = _two_leaf_root("b1", "b1")
left, right = root.leaves()
assert geometry.area(right) > geometry.area(left)
fit.collapse_global(root)
assert sorted(lf.type for lf in root.leaves()) == ["b1", "b2"]
assert right.type == "b1"
assert left.type == "b2"
# --------------------------------------------------------------------------- #
# Hard level constraint
# --------------------------------------------------------------------------- #
def test_level_constraint_never_assigns_wrong_level():
# b2 requires level 1; a single-storey tree is all level 0, so no leaf may
# take b2 — both stay b1 rather than gaining a wrong-level fail.
conf = _conf({
"b1": {"size": [16.0, 4.0]},
"b2": {"size": [12.0, 3.0], "level": 1},
})
fit = Fitness(conf=conf)
root = _two_leaf_root("b1", "b1")
fit.collapse_global(root)
assert all(lf.type == "b1" for lf in root.leaves())
assert "b2" not in {lf.type for lf in root.leaves()}
# --------------------------------------------------------------------------- #
# c/o/s partition exclusion
# --------------------------------------------------------------------------- #
def test_cos_prefixed_cells_are_not_relabelled():
# cr1 collides with the c* (circulation) convention the scorer counts against,
# so it is skeleton — never relabelled and never a demand slot.
conf = _conf({
"cr1": {"size": [20.0, 4.0]},
"b1": {"size": [16.0, 4.0]},
})
fit = Fitness(conf=conf)
root = _two_leaf_root("cr1", "b1")
fit.collapse_global(root)
assert sorted(lf.type for lf in root.leaves()) == ["b1", "cr1"]
# --------------------------------------------------------------------------- #
# No-op safety + defaults
# --------------------------------------------------------------------------- #
def test_no_programme_is_noop():
fit = Fitness(conf={})
root = _two_leaf_root("b1", "b1")
fit.collapse_global(root)
assert [lf.type for lf in root.leaves()] == ["b1", "b1"]
def test_single_code_is_noop():
# one assignable code, count 2 → demand == supply of the same code → no change
conf = _conf({"b1": {"size": [16.0, 4.0], "count": 2}})
fit = Fitness(conf=conf)
root = _two_leaf_root("b1", "b1")
fit.collapse_global(root)
assert [lf.type for lf in root.leaves()] == ["b1", "b1"]
def test_collapse_finish_is_keep_better_and_unmerged():
# collapse_finish returns (tree, base, collapsed, applied); the tree it hands
# back is unmerged (leaves still carry their divisions), and collapsed<=base.
conf = _conf({
"b1": {"size": [16.0, 4.0], "width": [4.0, 1.0], "proportion": [1.5, 0.5]},
"b2": {"size": [12.0, 3.0], "width": [3.5, 0.8], "proportion": [1.5, 0.5]},
})
fit = Fitness(conf=conf)
root = _two_leaf_root("b1", "b1")
tree, base_f, coll_f, applied = fit.collapse_finish(root)
assert coll_f <= base_f
assert applied == (coll_f < base_f) or coll_f == base_f
assert len(tree.leaves()) == 2 # unmerged: both room leaves intact