homemaker-layout/tests/test_genome.py
Bruno Postle 13f73be771 Topology genome: base tree + per-storey deltas + type assignment
genome.py (homemaker-py-k2g): Genome = base-floor GNode tree + per-storey
StoreyDelta (undivides, divide subtrees, leaf retypes, height) + base
metadata. encode/decode round-trips dom.py Node trees.

Key empirical finding baked into the design: upper-storey nodes carry
heavily drifted DEAD fields (97 inherited-cut divisions, 187 rotations
differ from the owning node below across the corpus) — dead because
geometry delegates to below before reading them. decode canonicalises
them; encode stores only owned state, so genomes from drifted sources
compare equal (fixed-point test).

Acceptance: 35/35 corpus files fitness-identical after round-trip through
the oracle (experiments/genome_parity.py, URB_NO_OCCLUSION=1); owned-cut
projection + genome fixed-point + storey counts in tests/test_genome.py
(16 tests pass).

Closes homemaker-py-k2g.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:52:32 +01:00

68 lines
2.1 KiB
Python

"""Genome encode/decode tests (oracle-free; corpus-backed)."""
from pathlib import Path
import pytest
from homemaker import dom, genome, solver
CORPUS = Path("/home/bruno/src/urb/examples/programme-house")
pytestmark = pytest.mark.skipif(not CORPUS.is_dir(), reason="Urb corpus not available")
def corpus():
return sorted(CORPUS.glob("*.dom"))
def owned_projection(root: dom.Node):
"""Everything fitness can see: per-level shape+leaf types, owned cut
divisions, heights, base metadata. Dead fields excluded by construction."""
def shape(n: dom.Node):
if not n.divided:
return n.type
return (shape(n.left), shape(n.right))
lvls = dom.levels(root)
owned = {}
for li, lvl in enumerate(lvls):
for b in solver._branches(lvl):
if b.below is None or not b.below.divided:
owned[(li, b.id)] = tuple(b.division)
return {
"shapes": [shape(lvl) for lvl in lvls],
"owned_cuts": owned,
"heights": [lvl.height for lvl in lvls],
"base_meta": {k: getattr(lvls[0], k) for k in
("node", "node_file", "perimeter", "elevation",
"wall_inner", "wall_outer")},
}
def test_roundtrip_preserves_owned_projection():
for f in corpus():
root = dom.load(str(f))
root2 = genome.decode(genome.encode(root))
assert owned_projection(root2) == owned_projection(root), f.name
def test_genome_is_a_fixed_point():
# encode(decode(g)) == g: nothing fitness-relevant is lost or invented
for f in corpus():
g1 = genome.encode(dom.load(str(f)))
g2 = genome.encode(genome.decode(g1))
assert g2 == g1, f.name
def test_decoded_tree_dumps_and_reloads():
for f in corpus():
root2 = genome.decode(genome.encode(dom.load(str(f))))
dom.dump(root2, "/tmp/genome_rt.dom")
root3 = dom.load("/tmp/genome_rt.dom")
assert owned_projection(root3) == owned_projection(root2), f.name
def test_storey_counts():
for f in corpus():
root = dom.load(str(f))
assert genome.encode(root).n_storeys == len(dom.levels(root)), f.name