homemaker-layout/src/homemaker/genome.py

158 lines
5.6 KiB
Python
Raw Normal View History

"""Topology genome: base-floor tree + per-storey deltas + type assignment.
The search genome (DESIGN.md §5.2) is the base-floor slicing tree plus, per
additional storey, the *delta* against the storey below: extra divisions,
undivisions, and leaf-type overrides. Below-inheritance does the rest a cut
is owned by the lowest storey where its path is divided, so walls stack for
free and the multi-storey constraint acts as a regulariser.
Corpus reality check (measured): upper-storey nodes carry heavily drifted
*dead* fields 97 inherited-cut divisions and 187 rotations differ from the
owning node below across the 35-file corpus. They are dead because
``geometry.coordinate`` delegates to ``below`` before ever reading them (and
Urb's ``Coordinate`` does the same). ``decode`` therefore canonicalises every
below-linked node's rotation/division/type from the storey below;
``experiments/genome_parity.py`` verifies this is fitness-neutral through the
oracle. Upper level roots carry only ``height`` as metadata.
"""
from __future__ import annotations
import copy
from dataclasses import dataclass, field
from . import dom
_BASE_META = ("node", "node_file", "perimeter", "height", "elevation",
"wall_inner", "wall_outer")
@dataclass
class GNode:
"""Genome tree node; a leaf iff ``division is None``."""
type: str | None = None
rotation: int = 0
division: tuple[float, float] | None = None
left: "GNode | None" = None
right: "GNode | None" = None
@property
def divided(self) -> bool:
return self.division is not None
@dataclass
class StoreyDelta:
"""One storey's difference from the storey below it."""
height: float | None = None
undivides: list[str] = field(default_factory=list) # divided below, leaf here
divides: dict[str, GNode] = field(default_factory=dict) # leaf below, subtree here
retypes: dict[str, str | None] = field(default_factory=dict) # leaf type overrides
@dataclass
class Genome:
base: GNode
base_meta: dict
deltas: list[StoreyDelta] = field(default_factory=list)
@property
def n_storeys(self) -> int:
return 1 + len(self.deltas)
# --------------------------------------------------------------------------- #
# encode: Node tree -> Genome
# --------------------------------------------------------------------------- #
def _gnode_from(n: dom.Node) -> GNode:
g = GNode(type=n.type, rotation=n.rotation)
if n.divided:
g.division = (n.division[0], n.division[1])
g.left = _gnode_from(n.left)
g.right = _gnode_from(n.right)
return g
def _delta_walk(n: dom.Node, below: dom.Node, path: str, delta: StoreyDelta) -> None:
if below.divided and n.divided:
_delta_walk(n.left, below.left, path + "l", delta)
_delta_walk(n.right, below.right, path + "r", delta)
elif below.divided: # merged here
delta.undivides.append(path)
delta.retypes[path] = n.type
elif n.divided: # extra division here; the subtree's cuts are owned here
sub = _gnode_from(n)
# the subtree root still sits on an inherited quad: its rotation is
# dead (geometry delegates to below, and decode keeps the inherited
# value) — store 0 so genomes from drifted sources compare equal
sub.rotation = 0
delta.divides[path] = sub
elif n.type != below.type:
delta.retypes[path] = n.type
def encode(root: dom.Node) -> Genome:
lvls = dom.levels(root)
base_meta = {k: copy.deepcopy(getattr(lvls[0], k)) for k in _BASE_META}
g = Genome(base=_gnode_from(lvls[0]), base_meta=base_meta)
for below, lvl in zip(lvls, lvls[1:]):
delta = StoreyDelta(height=lvl.height)
_delta_walk(lvl, below, "", delta)
g.deltas.append(delta)
return g
# --------------------------------------------------------------------------- #
# decode: Genome -> Node tree
# --------------------------------------------------------------------------- #
def _node_from(g: GNode) -> dom.Node:
n = dom.Node(type=g.type, rotation=g.rotation)
if g.divided:
n.division = [g.division[0], g.division[1]]
n.left = _node_from(g.left)
n.right = _node_from(g.right)
return n
def _copy_storey(below: dom.Node) -> dom.Node:
"""Structural copy of the storey below — the canonical inherited state.
Dead fields (rotation, inherited-cut division, internal type) are synced
from the owning node below rather than preserved, see module docstring.
"""
n = dom.Node(type=below.type, rotation=below.rotation)
if below.divided:
n.division = list(below.division)
n.left = _copy_storey(below.left)
n.right = _copy_storey(below.right)
return n
def decode(genome: Genome) -> dom.Node:
from . import geometry # local import mirrors dom.load (avoids cycle)
base = _node_from(genome.base)
for k, v in genome.base_meta.items():
setattr(base, k, copy.deepcopy(v))
prev = base
for delta in genome.deltas:
lvl = _copy_storey(prev)
for path in delta.undivides:
n = lvl.by_id(path)
n.division = None
n.left = n.right = None
n.type = None # retypes always carries the merged leaf's type
for path, sub in delta.divides.items():
n = lvl.by_id(path)
grafted = _node_from(sub)
n.division, n.left, n.right = grafted.division, grafted.left, grafted.right
n.type = grafted.type
for path, t in delta.retypes.items():
lvl.by_id(path).type = t
lvl.height = delta.height
prev.above = lvl
prev = lvl
dom._link(base)
geometry.clear_cache()
return base