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]))))