homemaker-py-6xh: wire shape-curve DP into driver.py as an NM warm-start
Promotes the validated shape-curve DP (experiments/shapecurve_spike.py,
2g7.4, DESIGN.md §37.2) from a reference-only spike into
src/homemaker_layout/shapecurve.py, and wires it into driver._evaluate as a
warm-start for innerloop.optimise: when eligible (single storey, no
leaf_sharing/superpose/max_share/multi_use) and no caller-supplied x0, the
DP's exact shape-feasible ratio point is written onto the tree before NM
runs, off by default (shapecurve_warmstart=/--shapecurve-warmstart).
Caught and fixed a latent bug promoting the spike: realise() could leave
numpy.float64 in `division`, which yaml.safe_dump can't serialise — the
original spike never round-tripped through dom.dumps so this was never hit.
A/B on harbor-house-l0 (experiments/ab_shapecurve_warmstart.py, budget=2000,
5 seeds): mean total fails 16.6 (on) vs 19.6 (off), ~3.5x mean fitness
improvement; mean hard-fail count alone was a noise-level wash at this
sample size. Full writeup in DESIGN.md §37.4.
Deliberately deferred to new tracked beads (children of 2g7): DP-exact hard
pre-filter (wkh), multi-storey below-link support (koo), leaf_sharing/
co_type modelling (tym), true skew-quad polygon algebra (ekc) — 6xh stays
in_progress pending those.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-03 18:43:28 +01:00
|
|
|
"""Tests for the shape-curve DP (homemaker-py-6xh, promoted from
|
|
|
|
|
experiments/shapecurve_spike.py; see DESIGN.md §37.2/§37.4 for the full
|
|
|
|
|
200-topology validation this unit scale is a fast smoke check of)."""
|
|
|
|
|
|
2026-08-03 23:30:18 +01:00
|
|
|
import copy
|
homemaker-py-6xh: wire shape-curve DP into driver.py as an NM warm-start
Promotes the validated shape-curve DP (experiments/shapecurve_spike.py,
2g7.4, DESIGN.md §37.2) from a reference-only spike into
src/homemaker_layout/shapecurve.py, and wires it into driver._evaluate as a
warm-start for innerloop.optimise: when eligible (single storey, no
leaf_sharing/superpose/max_share/multi_use) and no caller-supplied x0, the
DP's exact shape-feasible ratio point is written onto the tree before NM
runs, off by default (shapecurve_warmstart=/--shapecurve-warmstart).
Caught and fixed a latent bug promoting the spike: realise() could leave
numpy.float64 in `division`, which yaml.safe_dump can't serialise — the
original spike never round-tripped through dom.dumps so this was never hit.
A/B on harbor-house-l0 (experiments/ab_shapecurve_warmstart.py, budget=2000,
5 seeds): mean total fails 16.6 (on) vs 19.6 (off), ~3.5x mean fitness
improvement; mean hard-fail count alone was a noise-level wash at this
sample size. Full writeup in DESIGN.md §37.4.
Deliberately deferred to new tracked beads (children of 2g7): DP-exact hard
pre-filter (wkh), multi-storey below-link support (koo), leaf_sharing/
co_type modelling (tym), true skew-quad polygon algebra (ekc) — 6xh stays
in_progress pending those.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-03 18:43:28 +01:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from homemaker_layout import dom, driver, fitness as fit_mod, shapecurve, solver
|
|
|
|
|
|
|
|
|
|
HARBOR_L0 = Path(__file__).parent.parent / "examples" / "harbor-house-l0"
|
|
|
|
|
|
|
|
|
|
pytestmark = pytest.mark.skipif(not HARBOR_L0.is_dir(), reason="harbor-house-l0 not available")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fit():
|
|
|
|
|
conf, cost = fit_mod.load_config(str(HARBOR_L0))
|
|
|
|
|
return fit_mod.Fitness(conf, cost)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _small_feasible_topology():
|
|
|
|
|
"""A tiny 2-leaf (C/O only, no size/adjacency constraints to speak of)
|
|
|
|
|
topology on harbor-house-l0's plot -- deterministically shape-feasible
|
|
|
|
|
(verified: driver.random_topology(seed, 2, rng(0), ['C', 'O']))."""
|
|
|
|
|
seed = dom.load(str(HARBOR_L0 / "init.dom"))
|
|
|
|
|
rng = np.random.default_rng(0)
|
|
|
|
|
return driver.random_topology(seed, 2, rng, ["C", "O"])
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 23:30:18 +01:00
|
|
|
def _two_storey_feasible_topology():
|
|
|
|
|
"""Level 0: the whole plot as one undivided 'O' room (trivially
|
|
|
|
|
feasible, below is always None at level 0). Level 1: an independent
|
|
|
|
|
fresh 2-leaf 'C'/'O' topology whose root inherits the *whole plot* as
|
|
|
|
|
its fixed box (its below -- level 0's root -- exists but is undivided,
|
|
|
|
|
so the root itself is a free-region-root per ``shapecurve._region_roots``,
|
|
|
|
|
pinned to the same box ``_small_feasible_topology`` already validates as
|
|
|
|
|
shape-feasible for a single storey)."""
|
|
|
|
|
base_seed = dom.load(str(HARBOR_L0 / "init.dom"))
|
|
|
|
|
level0 = copy.deepcopy(base_seed)
|
|
|
|
|
level0.type = "O"
|
|
|
|
|
rng = np.random.default_rng(0)
|
|
|
|
|
level1 = driver.random_topology(dom.load(str(HARBOR_L0 / "init.dom")), 2, rng, ["C", "O"])
|
|
|
|
|
level0.above = level1
|
|
|
|
|
dom.link(level0)
|
|
|
|
|
return level0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _two_storey_mixed_topology(child_types=("O", "O")):
|
|
|
|
|
"""Level 0: the same 2-leaf 'C'/'O' topology ``_small_feasible_topology``
|
|
|
|
|
validates. Level 1: an exact structural copy (so its root and both
|
|
|
|
|
leaves start out below-inherited/FIXED, wall-stacked on level 0), with
|
|
|
|
|
one of its leaves (``target``, id 'l') further divided into two brand
|
|
|
|
|
new leaves of ``child_types`` -- a genuine below-fixed-box/free-split
|
|
|
|
|
(case B) fringe node nested under a below-fixed-divided (case A) root,
|
|
|
|
|
the mixed scenario ``homemaker-py-koo`` adds support for. Returns
|
|
|
|
|
``(root, target)``."""
|
|
|
|
|
seed = dom.load(str(HARBOR_L0 / "init.dom"))
|
|
|
|
|
rng = np.random.default_rng(0)
|
|
|
|
|
level0 = driver.random_topology(seed, 2, rng, ["C", "O"])
|
|
|
|
|
level1 = copy.deepcopy(level0)
|
|
|
|
|
level0.above = level1
|
|
|
|
|
dom.link(level0)
|
|
|
|
|
|
|
|
|
|
target = level1.left
|
|
|
|
|
target.division = [0.5, 0.5]
|
|
|
|
|
target.rotation = 0
|
|
|
|
|
target.left = dom.Node(rotation=0, type=child_types[0])
|
|
|
|
|
target.right = dom.Node(rotation=0, type=child_types[1])
|
|
|
|
|
dom.link(level0)
|
|
|
|
|
return level0, target
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_eligible_guards_sharing_not_storey_count():
|
|
|
|
|
"""homemaker-py-koo: multi-storey is now handled (below-inherited fixed
|
|
|
|
|
splits, DESIGN.md §37.6), so ``eligible`` only guards the still-unmodelled
|
|
|
|
|
leaf_sharing/superpose/max_share/multi_use family."""
|
homemaker-py-6xh: wire shape-curve DP into driver.py as an NM warm-start
Promotes the validated shape-curve DP (experiments/shapecurve_spike.py,
2g7.4, DESIGN.md §37.2) from a reference-only spike into
src/homemaker_layout/shapecurve.py, and wires it into driver._evaluate as a
warm-start for innerloop.optimise: when eligible (single storey, no
leaf_sharing/superpose/max_share/multi_use) and no caller-supplied x0, the
DP's exact shape-feasible ratio point is written onto the tree before NM
runs, off by default (shapecurve_warmstart=/--shapecurve-warmstart).
Caught and fixed a latent bug promoting the spike: realise() could leave
numpy.float64 in `division`, which yaml.safe_dump can't serialise — the
original spike never round-tripped through dom.dumps so this was never hit.
A/B on harbor-house-l0 (experiments/ab_shapecurve_warmstart.py, budget=2000,
5 seeds): mean total fails 16.6 (on) vs 19.6 (off), ~3.5x mean fitness
improvement; mean hard-fail count alone was a noise-level wash at this
sample size. Full writeup in DESIGN.md §37.4.
Deliberately deferred to new tracked beads (children of 2g7): DP-exact hard
pre-filter (wkh), multi-storey below-link support (koo), leaf_sharing/
co_type modelling (tym), true skew-quad polygon algebra (ekc) — 6xh stays
in_progress pending those.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-03 18:43:28 +01:00
|
|
|
root = dom.load(str(HARBOR_L0 / "generated.dom"))
|
|
|
|
|
assert len(dom.levels(root)) == 1
|
|
|
|
|
assert shapecurve.eligible(root)
|
|
|
|
|
assert not shapecurve.eligible(root, leaf_sharing=True)
|
|
|
|
|
assert not shapecurve.eligible(root, superpose=True)
|
|
|
|
|
assert not shapecurve.eligible(root, max_share=3)
|
|
|
|
|
assert not shapecurve.eligible(root, multi_use=True)
|
|
|
|
|
|
|
|
|
|
seed = dom.load(str(HARBOR_L0 / "init.dom"))
|
|
|
|
|
seed.above = dom.Node(rotation=0) # fake a second storey
|
|
|
|
|
assert len(dom.levels(seed)) == 2
|
2026-08-03 23:30:18 +01:00
|
|
|
assert shapecurve.eligible(seed)
|
|
|
|
|
assert not shapecurve.eligible(seed, leaf_sharing=True)
|
homemaker-py-6xh: wire shape-curve DP into driver.py as an NM warm-start
Promotes the validated shape-curve DP (experiments/shapecurve_spike.py,
2g7.4, DESIGN.md §37.2) from a reference-only spike into
src/homemaker_layout/shapecurve.py, and wires it into driver._evaluate as a
warm-start for innerloop.optimise: when eligible (single storey, no
leaf_sharing/superpose/max_share/multi_use) and no caller-supplied x0, the
DP's exact shape-feasible ratio point is written onto the tree before NM
runs, off by default (shapecurve_warmstart=/--shapecurve-warmstart).
Caught and fixed a latent bug promoting the spike: realise() could leave
numpy.float64 in `division`, which yaml.safe_dump can't serialise — the
original spike never round-tripped through dom.dumps so this was never hit.
A/B on harbor-house-l0 (experiments/ab_shapecurve_warmstart.py, budget=2000,
5 seeds): mean total fails 16.6 (on) vs 19.6 (off), ~3.5x mean fitness
improvement; mean hard-fail count alone was a noise-level wash at this
sample size. Full writeup in DESIGN.md §37.4.
Deliberately deferred to new tracked beads (children of 2g7): DP-exact hard
pre-filter (wkh), multi-storey below-link support (koo), leaf_sharing/
co_type modelling (tym), true skew-quad polygon algebra (ekc) — 6xh stays
in_progress pending those.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-03 18:43:28 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_solve_feasible_root_realises_zero_shape_fails(tmp_path):
|
|
|
|
|
"""A small, obviously-feasible topology's DP-realised ratios round-trip
|
|
|
|
|
through dom.dumps/dom.load and independently verify as zero shape fails
|
|
|
|
|
under the real Fitness scorer."""
|
|
|
|
|
root = _small_feasible_topology()
|
|
|
|
|
fit = _fit()
|
|
|
|
|
|
|
|
|
|
feasible, info = shapecurve.solve(root, fit)
|
|
|
|
|
assert feasible is True
|
|
|
|
|
assert info["w_plot"] > 0 and info["h_plot"] > 0
|
|
|
|
|
|
|
|
|
|
out_path = tmp_path / "realised.dom"
|
|
|
|
|
out_path.write_text(dom.dumps(root))
|
|
|
|
|
reloaded = dom.load(str(out_path))
|
|
|
|
|
|
|
|
|
|
_, fails = fit.score_with_fails(reloaded)
|
|
|
|
|
shape_fails = [f for f in fails if f.endswith((" size", " width", " proportion"))]
|
|
|
|
|
assert shape_fails == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_solve_infeasible_topology_leaves_tree_untouched():
|
|
|
|
|
"""A topology with far more leaves than harbor-house-l0's plot can fit
|
|
|
|
|
(each needing its own min width/area) is infeasible; solve() must not
|
|
|
|
|
write partial/bogus ratios in that case."""
|
|
|
|
|
seed = dom.load(str(HARBOR_L0 / "init.dom"))
|
|
|
|
|
rng = np.random.default_rng(0)
|
|
|
|
|
root = driver.random_topology(seed, 60, rng, ["k1", "l1", "b1", "C", "O"])
|
|
|
|
|
fit = _fit()
|
|
|
|
|
|
|
|
|
|
feasible, info = shapecurve.solve(root, fit)
|
|
|
|
|
assert feasible is False
|
|
|
|
|
# infeasible: no realised point to check, but the call must not raise
|
|
|
|
|
# and must report the same plot dims as the feasible case's mechanism
|
|
|
|
|
assert info["w_plot"] > 0 and info["h_plot"] > 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_solve_is_deterministic():
|
|
|
|
|
root = _small_feasible_topology()
|
|
|
|
|
fit = _fit()
|
|
|
|
|
f1, _ = shapecurve.solve(root, fit)
|
|
|
|
|
divisions_1 = [tuple(b.division) for b in solver.free_branches(root)]
|
|
|
|
|
f2, _ = shapecurve.solve(root, fit)
|
|
|
|
|
divisions_2 = [tuple(b.division) for b in solver.free_branches(root)]
|
|
|
|
|
assert f1 == f2 is True
|
|
|
|
|
assert divisions_1 == pytest.approx(divisions_2)
|
2026-08-03 21:10:28 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_is_feasible_agrees_with_solve_but_never_writes(monkeypatch):
|
|
|
|
|
"""homemaker-py-wkh: the hard-prune caller needs the boolean verdict
|
|
|
|
|
without solve()'s tree mutation, so ``is_feasible`` must (a) agree with
|
|
|
|
|
``solve``'s own verdict and (b) never write ``division`` -- verified on
|
|
|
|
|
both the feasible and infeasible fixtures already exercised above."""
|
|
|
|
|
fit = _fit()
|
|
|
|
|
|
|
|
|
|
feasible_root = _small_feasible_topology()
|
|
|
|
|
before = [tuple(b.division) for b in solver.free_branches(feasible_root)]
|
|
|
|
|
assert shapecurve.is_feasible(feasible_root, fit) is True
|
|
|
|
|
after = [tuple(b.division) for b in solver.free_branches(feasible_root)]
|
|
|
|
|
assert before == after
|
|
|
|
|
|
|
|
|
|
seed = dom.load(str(HARBOR_L0 / "init.dom"))
|
|
|
|
|
rng = np.random.default_rng(0)
|
|
|
|
|
infeasible_root = driver.random_topology(seed, 60, rng, ["k1", "l1", "b1", "C", "O"])
|
|
|
|
|
before = [tuple(b.division) for b in solver.free_branches(infeasible_root)]
|
|
|
|
|
assert shapecurve.is_feasible(infeasible_root, fit) is False
|
|
|
|
|
after = [tuple(b.division) for b in solver.free_branches(infeasible_root)]
|
|
|
|
|
assert before == after
|
2026-08-03 23:30:18 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Multi-storey (homemaker-py-koo, DESIGN.md §37.6)
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_solve_multistorey_feasible_realises_zero_shape_fails(tmp_path):
|
|
|
|
|
"""A 2-storey topology whose upper storey is a fresh, independently-free
|
|
|
|
|
2-leaf split (pinned to the whole plot, since the ground storey below it
|
|
|
|
|
is a single undivided room) round-trips to zero size/width/proportion
|
|
|
|
|
fails at every level, exactly like the single-storey case."""
|
|
|
|
|
root = _two_storey_feasible_topology()
|
|
|
|
|
fit = _fit()
|
|
|
|
|
|
|
|
|
|
feasible, info = shapecurve.solve(root, fit)
|
|
|
|
|
assert feasible is True
|
|
|
|
|
assert info["w_plot"] > 0 and info["h_plot"] > 0
|
|
|
|
|
assert info["n_levels"] == 2
|
|
|
|
|
|
|
|
|
|
out_path = tmp_path / "realised.dom"
|
|
|
|
|
out_path.write_text(dom.dumps(root))
|
|
|
|
|
reloaded = dom.load(str(out_path))
|
|
|
|
|
|
|
|
|
|
_, fails = fit.score_with_fails(reloaded)
|
|
|
|
|
shape_fails = [f for f in fails if f.endswith((" size", " width", " proportion"))]
|
|
|
|
|
assert shape_fails == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_solve_multistorey_matches_free_branches(tmp_path):
|
|
|
|
|
"""Mixed fixture: level 1 is a structural copy of level 0 (so its root
|
|
|
|
|
and both original leaves are below-fixed) with one leaf further divided
|
|
|
|
|
into two brand new 'O' leaves (a below-fixed-box/free-split fringe node
|
|
|
|
|
nested under a below-fixed-divided root). ``solve`` must write ratios on
|
|
|
|
|
exactly ``solver.free_branches`` -- the pre-existing single-storey
|
|
|
|
|
invariant this generalises -- and leave every below-fixed node's own
|
|
|
|
|
``division`` byte-identical, even though it sits on a realised subtree."""
|
|
|
|
|
root, target = _two_storey_mixed_topology(child_types=("O", "O"))
|
|
|
|
|
level1 = root.above
|
|
|
|
|
fit = _fit()
|
|
|
|
|
|
|
|
|
|
all_nodes_before = [(n, list(n.division))
|
|
|
|
|
for lvl in dom.levels(root) for n in shapecurve._divided_nodes(lvl)]
|
|
|
|
|
free_before = [b for b in solver.free_branches(root)]
|
|
|
|
|
|
|
|
|
|
feasible, _ = shapecurve.solve(root, fit)
|
|
|
|
|
assert feasible is True
|
|
|
|
|
|
|
|
|
|
# level 1's own root is below-fixed (its below, level 0's root, is
|
|
|
|
|
# divided) so it must never appear as a free branch, and 'target' (a
|
|
|
|
|
# fresh split introduced only at level 1) must.
|
|
|
|
|
assert any(b is target for b in solver.free_branches(root))
|
|
|
|
|
assert not any(b is level1 for b in solver.free_branches(root))
|
|
|
|
|
|
|
|
|
|
for node, before in all_nodes_before:
|
|
|
|
|
if any(node is b for b in free_before):
|
|
|
|
|
continue
|
|
|
|
|
assert node.division == before, "below-fixed node's division must never be written"
|
|
|
|
|
|
|
|
|
|
out_path = tmp_path / "realised.dom"
|
|
|
|
|
out_path.write_text(dom.dumps(root))
|
|
|
|
|
reloaded = dom.load(str(out_path))
|
|
|
|
|
_, fails = fit.score_with_fails(reloaded)
|
|
|
|
|
shape_fails = [f for f in fails if f.endswith((" size", " width", " proportion"))]
|
|
|
|
|
assert shape_fails == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_solve_multistorey_infeasible_restores_every_level():
|
|
|
|
|
"""When an upper-storey free split is infeasible (a 'C' leaf forced into
|
|
|
|
|
a below-fixed box too tall for its proportion/size bounds -- verified by
|
|
|
|
|
inspection, not tuned to just barely fail), ``solve`` must roll back
|
|
|
|
|
ALL levels, including the ground storey it already realised earlier in
|
|
|
|
|
the same call -- not just the storey where infeasibility was detected."""
|
|
|
|
|
root, target = _two_storey_mixed_topology(child_types=("C", "O"))
|
|
|
|
|
level1 = root.above
|
|
|
|
|
fit = _fit()
|
|
|
|
|
|
|
|
|
|
before = {
|
|
|
|
|
id(n): list(n.division)
|
|
|
|
|
for lvl in dom.levels(root) for n in shapecurve._divided_nodes(lvl)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
feasible, _ = shapecurve.solve(root, fit)
|
|
|
|
|
assert feasible is False
|
|
|
|
|
|
|
|
|
|
after = {
|
|
|
|
|
id(n): list(n.division)
|
|
|
|
|
for lvl in dom.levels(root) for n in shapecurve._divided_nodes(lvl)
|
|
|
|
|
}
|
|
|
|
|
assert after == before, "an infeasible upper storey must not leave the ground storey mutated"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_is_feasible_multistorey_never_writes():
|
|
|
|
|
fit = _fit()
|
|
|
|
|
|
|
|
|
|
feasible_root = _two_storey_feasible_topology()
|
|
|
|
|
before = [tuple(b.division) for b in solver.free_branches(feasible_root)]
|
|
|
|
|
assert shapecurve.is_feasible(feasible_root, fit) is True
|
|
|
|
|
after = [tuple(b.division) for b in solver.free_branches(feasible_root)]
|
|
|
|
|
assert before == after
|
|
|
|
|
|
|
|
|
|
infeasible_root, _ = _two_storey_mixed_topology(child_types=("C", "O"))
|
|
|
|
|
before = [tuple(b.division) for b in solver.free_branches(infeasible_root)]
|
|
|
|
|
assert shapecurve.is_feasible(infeasible_root, fit) is False
|
|
|
|
|
after = [tuple(b.division) for b in solver.free_branches(infeasible_root)]
|
|
|
|
|
assert before == after
|