- src/homemaker/ → src/homemaker_layout/; all imports updated - pyproject.toml: name = homemaker-layout, entry point updated - .beads/config.yaml: dolt sync.remote updated to homemaker-layout.git - Delete temporary debug/perl scripts from project root - README.md, DESIGN.md: package path references updated - GitHub repo renamed; git remote updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
159 lines
6 KiB
Python
159 lines
6 KiB
Python
"""Corpus-backed tests for dom round-trip, free-branch ownership, and fitness parity.
|
|
|
|
Skipped when the Urb checkout is absent (these need only its .dom files, not
|
|
perl). The parity tests compare native Python fitness against cached oracle
|
|
scores and failure sets (generated with URB_NO_OCCLUSION=1).
|
|
"""
|
|
|
|
import math
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from homemaker_layout import dom, solver
|
|
|
|
CORPUS = Path(__file__).parent.parent / "examples" / "programme-house"
|
|
|
|
pytestmark = pytest.mark.skipif(not CORPUS.is_dir(), reason="Corpus not available")
|
|
|
|
|
|
def test_roundtrip_idempotent_and_area_preserving(tmp_path):
|
|
# dump() does not reproduce the source bytes (different YAML style); the
|
|
# real invariants are that a dumped file reloads to the same dump (stable
|
|
# fixed point) and that per-leaf geometry survives the trip (§4.1 is the
|
|
# area validation against Urb itself).
|
|
from homemaker_layout import geometry
|
|
|
|
for src in sorted(CORPUS.glob("*.dom")):
|
|
root = dom.load(str(src))
|
|
areas = [geometry.area(leaf) for lvl in dom.levels(root) for leaf in lvl.leaves()]
|
|
once = tmp_path / ("once_" + src.name)
|
|
dom.dump(root, str(once))
|
|
|
|
root2 = dom.load(str(once))
|
|
areas2 = [geometry.area(leaf) for lvl in dom.levels(root2) for leaf in lvl.leaves()]
|
|
assert areas == pytest.approx(areas2, abs=1e-9), src.name
|
|
|
|
twice = tmp_path / ("twice_" + src.name)
|
|
dom.dump(root2, str(twice))
|
|
assert twice.read_bytes() == once.read_bytes(), src.name
|
|
|
|
|
|
def test_free_branches_known_dof():
|
|
# DOF figures from DESIGN.md §4.5
|
|
expected = {
|
|
"2f45907abd9accac2a124d311732f749.dom": 7,
|
|
"candidate-002.dom": 6,
|
|
"c964435454c459f86c3ed9a5a7621132.dom": 6,
|
|
}
|
|
for name, dof in expected.items():
|
|
root = dom.load(str(CORPUS / name))
|
|
assert len(solver.free_branches(root)) == dof, name
|
|
|
|
|
|
def test_free_branches_are_lowest_storey_owners():
|
|
for src in sorted(CORPUS.glob("*.dom")):
|
|
root = dom.load(str(src))
|
|
for b in solver.free_branches(root):
|
|
assert b.divided
|
|
assert b.below is None or not b.below.divided
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 3 gate: native fitness parity vs oracle (homemaker-py-uxz)
|
|
# Oracle scores and failure sets cached as <file>.dom.score / <file>.dom.fails
|
|
# generated with URB_NO_OCCLUSION=1 (DESIGN.md §6 descope).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _native_evaluate(src: Path):
|
|
"""Run native Fitness.evaluate and return (score, frozenset[fail_lines])."""
|
|
from homemaker_layout import fitness as fit_mod, graph as graph_mod, geometry
|
|
|
|
root = dom.load(str(src))
|
|
conf, cost = fit_mod.load_config(CORPUS)
|
|
fit = fit_mod.Fitness(conf, cost)
|
|
|
|
failures: list[str] = []
|
|
tracking: dict = {
|
|
"has_public_access_outside": False,
|
|
"has_public_access_inside": False,
|
|
"public_length_all": 0.0,
|
|
"public_length_outside": 0.0,
|
|
"private_length_all": 0.0,
|
|
"private_length_outside": 0.0,
|
|
"stair_fit": [],
|
|
"_failures": failures,
|
|
}
|
|
|
|
programme = fit._programme or {}
|
|
geometry.clear_cache()
|
|
|
|
check_f, missing = graph_mod.check_space_counts(root, programme)
|
|
failures.extend(check_f)
|
|
fit.preprocess_building(root)
|
|
_, gcpre = graph_mod.build_graphs_with_circ(root, fit.conf("door_width") or 1.2, failures.append)
|
|
gbpre = graph_mod.build_graphs(root, fit.conf("door_width") or 1.2)
|
|
failures.extend(graph_mod.check_adjacency(root, programme, gbpre, missing))
|
|
failures.extend(graph_mod.check_level_constraints(root, programme, missing))
|
|
failures.extend(graph_mod.check_vertical_connectivity(root, programme, missing))
|
|
|
|
dom.merge_divided(root)
|
|
geometry.clear_cache()
|
|
_, gc = graph_mod.build_graphs_with_circ(root, fit.conf("door_width") or 1.2, failures.append)
|
|
gb = graph_mod.build_graphs(root, fit.conf("door_width") or 1.2)
|
|
|
|
cost_v = fit.plot_cost(root)
|
|
value = 0.0
|
|
lvls = dom.levels(root)
|
|
for li, lvl in enumerate(lvls):
|
|
se = fit.process_storey(
|
|
lvl, gb[li], li, failures.append,
|
|
graph_circ=gc, tracking=tracking, lvls=lvls, root=root,
|
|
)
|
|
cost_v += se.cost
|
|
value += se.value
|
|
|
|
bf = fit.evaluate_building(root, tracking)
|
|
value *= bf
|
|
value *= 0.5 ** len(failures)
|
|
score = value / cost_v if cost_v else 0.0
|
|
|
|
return score, frozenset(failures)
|
|
|
|
|
|
def _oracle_result(src: Path):
|
|
"""Read cached oracle score and failure set (URB_NO_OCCLUSION=1)."""
|
|
from homemaker_layout.oracle import Score
|
|
|
|
score_file = Path(str(src) + ".score")
|
|
fails_file = Path(str(src) + ".fails")
|
|
if not score_file.exists():
|
|
pytest.skip(f"No cached oracle score for {src.name}")
|
|
oracle_score = float(score_file.read_text().strip())
|
|
oracle_fails = Score(
|
|
fitness=oracle_score,
|
|
fails=fails_file.read_text() if fails_file.exists() else "",
|
|
).fail_lines
|
|
return oracle_score, frozenset(oracle_fails)
|
|
|
|
|
|
@pytest.mark.parametrize("src", sorted(CORPUS.glob("*.dom")), ids=lambda p: p.name)
|
|
def test_native_fitness_score_parity(src):
|
|
"""Native score matches oracle within 1e-4 relative tolerance."""
|
|
native_score, _ = _native_evaluate(src)
|
|
oracle_score, _ = _oracle_result(src)
|
|
assert math.isclose(native_score, oracle_score, rel_tol=1e-4, abs_tol=1e-15), (
|
|
f"{src.name}: native={native_score:.6e} oracle={oracle_score:.6e}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("src", sorted(CORPUS.glob("*.dom")), ids=lambda p: p.name)
|
|
def test_native_fitness_fail_set_parity(src):
|
|
"""Native failure set matches oracle failure set exactly."""
|
|
_, native_fails = _native_evaluate(src)
|
|
_, oracle_fails = _oracle_result(src)
|
|
only_native = native_fails - oracle_fails
|
|
only_oracle = oracle_fails - native_fails
|
|
assert not only_native and not only_oracle, (
|
|
f"{src.name}: only_native={sorted(only_native)} only_oracle={sorted(only_oracle)}"
|
|
)
|