From 3bf507a4836a5b2f475a51b449948394787db745 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Fri, 12 Jun 2026 23:26:22 +0100 Subject: [PATCH] Fix benchmark cell arg order and mutate_swap on undivided trees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run_urbevolve took (seed, budget, pop, cell) but cells call fn(seed, budget, cell, **kw) — every urb-evolve cell died on TypeError, deferred silently by pool.map. mutate_swap lacked the empty-candidates noop guard the other operators have, crashing on init.dom-style bare plots. Regression test: every mutation survives an undivided tree. Co-Authored-By: Claude Fable 5 --- experiments/benchmark_vs_urbevolve.py | 2 +- src/homemaker/operators.py | 5 ++++- tests/test_operators.py | 12 ++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/experiments/benchmark_vs_urbevolve.py b/experiments/benchmark_vs_urbevolve.py index f672049..7f6b008 100644 --- a/experiments/benchmark_vs_urbevolve.py +++ b/experiments/benchmark_vs_urbevolve.py @@ -50,7 +50,7 @@ def run_homemaker(seed: str, budget: int, cell: Path) -> dict: return {"series": series, "best_dom": out, "log_ok": proc.returncode == 0} -def run_urbevolve(seed: str, budget: int, pop: int, cell: Path) -> dict: +def run_urbevolve(seed: str, budget: int, cell: Path, pop: int) -> dict: shutil.copy(EX / "patterns.config", cell) shutil.copy(EX / seed, cell) proc = subprocess.run( diff --git a/src/homemaker/operators.py b/src/homemaker/operators.py index ad3f7f9..23f2796 100644 --- a/src/homemaker/operators.py +++ b/src/homemaker/operators.py @@ -101,7 +101,10 @@ def mutate_retype(root: dom.Node, rng: np.random.Generator, def mutate_swap(root: dom.Node, rng: np.random.Generator, types: list[str]) -> tuple[dom.Node, str]: child = copy.deepcopy(root) - li, n = _pick(rng, _owned_branches(child)) + cands = _owned_branches(child) + if not cands: # undivided topology (e.g. a bare plot seed) + return _finalise(child), "swap noop" + li, n = _pick(rng, cands) n.left, n.right = n.right, n.left return _finalise(child), f"swap {li}/{n.id or 'root'}" diff --git a/tests/test_operators.py b/tests/test_operators.py index 77828df..73795b2 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -83,6 +83,18 @@ def test_relink_clears_stale_below_after_base_undivide(): assert base.by_id(cands[0].id) is not None # parent untouched +def test_all_mutations_survive_undivided_tree(): + # an undivided plot (init.dom-style seed) must never crash an operator + bare = dom.Node(type="O", node=[[0, 0], [10, 0], [10, 8], [0, 8]], + height=2.7, wall_outer=0.25, wall_inner=0.08) + dom._link(bare) + for name, op in operators.MUTATIONS.items(): + for seed in range(3): + child, desc = op(bare, np.random.default_rng(seed), TYPES) + assert desc, name + canonical(child) + + def test_crossover_yields_canonical_pair(): a = genome.decode(genome.encode(dom.load(str(CORPUS / FILES[0])))) b = genome.decode(genome.encode(dom.load(str(CORPUS / FILES[1]))))