39.24's sweep listed two entries as DEAD rather than suspect -- inert code that reads as live. Neither changes a score or a failure on any corpus artefact, and that is verified rather than asserted: every artefact scores identically to its 39.25 measurement. ratio_public_outside and ratio_private_outside. evaluate_building read both and multiplied a gaussian into the building factor for each. Neither key exists in CONF_DEFAULTS and no patterns.config in the repository declares either, so both branches were guarded and never ran. Removing them also retires what fed them: the four public_length_*/private_length_* tracking keys accumulated per leaf in process_storey, and the _public_length/_private_length helpers, which had no other caller. NOT removed, because they are live: _public_access, _public_access_outside, _public_access_pins and the has_public_access_* tracking flags, which drive real checks and collapse_global's preserve_public_access. Only the length-ratio machinery was dead. The daylight quality factor. evaluate_leaf set factors["daylight"] = 1.0 unconditionally -- pinned since the URB_NO_OCCLUSION descope (6) and unable to be anything else. It was never in _GRADED_FACTORS, so it contributed nothing to the graded signal, and 39.18's geometric mean then had to special-case it in factor_is_asked as a factor that is never asked. A constant that exists only to be excluded is worth deleting. If 2g5 rebuilds occlusion it reintroduces a real daylight factor, which would need factor_is_asked to say True anyway. Two tests referenced the removed factor. test_leaf_grade_ignores_non_graded_keys now names a key that genuinely does not exist; the aggregate underflow test dropped its daylight entry, which would otherwise have been counted as asked and changed the expected geometric mean. Worth doing despite changing no number: 39.20 and 39.25 were both cases where something inert looked live -- a parity test that never ran, a per-level rule switched off in every config -- and in both the misreading cost real time and produced a wrong conclusion. An objective with fewer things in it that do nothing is one where "this term does nothing" is informative rather than routine. Still open on dpt, each needing a ruling or a rate change rather than a measurement: quality_size's upper side, the minimum-internal-area factor as a third statement of "build the rooms", and the 0.5**n_fails curve. 426 passed. Refs homemaker-py-dpt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
122 lines
4.3 KiB
Python
122 lines
4.3 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). Parity against the Perl oracle used to live here; the oracle is gone
|
|
(DESIGN.md §39.21) and those tests never ran, so they went with it.
|
|
"""
|
|
|
|
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,
|
|
"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, fit.usages())
|
|
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, fit.usages())
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|