2026-06-14 07:48:13 +01:00
|
|
|
"""homemaker-evolve — memetic layout search starting from a .dom file.
|
|
|
|
|
|
|
|
|
|
Usage (module):
|
|
|
|
|
python -m homemaker.evolve seed.dom [options]
|
|
|
|
|
|
|
|
|
|
When installed via pip install -e .:
|
|
|
|
|
homemaker-evolve seed.dom [options]
|
|
|
|
|
|
|
|
|
|
Positional arguments:
|
|
|
|
|
seed.dom starting design file
|
|
|
|
|
|
|
|
|
|
Options:
|
|
|
|
|
--programme-dir DIR programme directory with patterns.config / costs.config
|
|
|
|
|
(default: parent directory of seed.dom)
|
|
|
|
|
--budget N evaluation budget (default: $HOMEMAKER_BUDGET or 20000)
|
|
|
|
|
--pop N population size (default: $HOMEMAKER_POP or 16)
|
|
|
|
|
--child-budget N per-child budget (default: $HOMEMAKER_CHILD_BUDGET or 80)
|
|
|
|
|
--workers N parallel workers (default: $HOMEMAKER_WORKERS or 1)
|
|
|
|
|
--seed N RNG seed (default: $HOMEMAKER_SEED or 0)
|
2026-07-15 10:21:58 +01:00
|
|
|
--polish-budget N after a leaf-sharing run, unfold the shared leaves and
|
|
|
|
|
run N extra no-sharing evals so the written .dom is honest
|
|
|
|
|
under the canonical scorer (homemaker-py-3l6). -1 = auto
|
|
|
|
|
(budget//2); 0 = unfold + rescore only. Ignored with
|
|
|
|
|
--no-leaf-sharing. (default: $HOMEMAKER_POLISH_BUDGET or -1)
|
2026-06-14 07:48:13 +01:00
|
|
|
--output PATH output .dom path (default: <seed_stem>_evolved.dom
|
|
|
|
|
next to seed; use - for stdout)
|
|
|
|
|
|
|
|
|
|
Progress is printed to stderr; the .dom is written on completion or interrupt.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import os
|
|
|
|
|
import signal
|
|
|
|
|
import sys
|
|
|
|
|
import time
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from . import dom, driver
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _env_int(name: str, default: int) -> int:
|
|
|
|
|
v = os.environ.get(name)
|
|
|
|
|
return int(v) if v is not None else default
|
|
|
|
|
|
|
|
|
|
|
2026-06-28 22:04:35 +01:00
|
|
|
def _env_bool(name: str, default: bool) -> bool:
|
|
|
|
|
v = os.environ.get(name)
|
|
|
|
|
if v is None:
|
|
|
|
|
return default
|
|
|
|
|
return v.strip().lower() in ("1", "true", "yes", "on")
|
|
|
|
|
|
|
|
|
|
|
2026-06-14 07:48:13 +01:00
|
|
|
def _parse_args(argv=None) -> argparse.Namespace:
|
|
|
|
|
p = argparse.ArgumentParser(
|
|
|
|
|
prog="homemaker-evolve",
|
|
|
|
|
description="Memetic building-layout search over slicing trees.",
|
|
|
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
|
|
|
)
|
|
|
|
|
p.add_argument("seed_dom", type=Path,
|
|
|
|
|
help="seed .dom file to start search from")
|
|
|
|
|
p.add_argument("--programme-dir", type=Path, default=None, metavar="DIR",
|
|
|
|
|
help="programme directory (default: parent of seed.dom)")
|
|
|
|
|
p.add_argument("--budget", type=int,
|
|
|
|
|
default=_env_int("HOMEMAKER_BUDGET", 20000),
|
|
|
|
|
metavar="N", help="evaluation budget")
|
|
|
|
|
p.add_argument("--pop", type=int,
|
|
|
|
|
default=_env_int("HOMEMAKER_POP", 16),
|
|
|
|
|
metavar="N", help="population size")
|
|
|
|
|
p.add_argument("--child-budget", type=int,
|
|
|
|
|
default=_env_int("HOMEMAKER_CHILD_BUDGET", 80),
|
|
|
|
|
metavar="N", help="per-child evaluation budget")
|
|
|
|
|
p.add_argument("--workers", type=int,
|
|
|
|
|
default=_env_int("HOMEMAKER_WORKERS", 1),
|
|
|
|
|
metavar="N", help="parallel worker processes")
|
|
|
|
|
p.add_argument("--seed", type=int,
|
|
|
|
|
default=_env_int("HOMEMAKER_SEED", 0),
|
|
|
|
|
metavar="N", help="RNG seed")
|
2026-06-28 22:04:35 +01:00
|
|
|
p.add_argument("--leaf-sharing", action=argparse.BooleanOptionalAction,
|
|
|
|
|
default=_env_bool("HOMEMAKER_LEAF_SHARING", True),
|
|
|
|
|
help="collapse same-code rooms into fewer, larger shared "
|
|
|
|
|
"leaves (erc.3, §13.3); --no-leaf-sharing for the strict "
|
|
|
|
|
"per-leaf baseline (default: on)")
|
|
|
|
|
p.add_argument("--leaf-share-factor", type=int,
|
|
|
|
|
default=_env_int("HOMEMAKER_LEAF_SHARE_FACTOR", 3),
|
|
|
|
|
metavar="N",
|
|
|
|
|
help="global sharing grain: 0 = per-code opt-in only (share a "
|
|
|
|
|
"code iff its programme entry sets 'share: N>=2'); N>=2 = "
|
|
|
|
|
"share every sized code at grain N, with a code's explicit "
|
|
|
|
|
"'share' overriding (share:1 opts out) (default: 3)")
|
9o5: type superposition + per-eval collapse (multi-use leaves)
Interchangeable codes (similar size/width/proportion, compatible level/stack,
no adjacency edge) form equivalence classes derived from the programme. With
--superpose (default off), each fitness eval COLLAPSES every superposed leaf to
its best in-class usage via an optimal supply->demand assignment (brute force
<=C! within cap C=4, scipy Hungarian beyond), then scores the condensed types.
Because collapse re-types on the unmerged tree before all checks, counts /
adjacency / quality are unchanged downstream -- no Node field, no graph/operator
changes -- and default OFF is bit-identical.
- programme.py: derive_interchange_classes + interchangeable (S1-S4, locked
thresholds R_SIZE=1.5/R_WIDTH=1.3/R_PROP=1.5, CLASS_CAP=4)
- fitness.py: collapse_superposition, _best_assignment, _usage_quality;
superpose/superpose_class_cap conf knobs; collapse hooked into _evaluate_full
- driver.py/evolve.py: superpose flag plumbed beside leaf_sharing; --superpose
- tests/test_superposition.py: 17 tests (derivation, assignment, end-to-end)
Closes homemaker-py-9o5 (build); validation A/B is homemaker-py-xi7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 07:08:46 +01:00
|
|
|
p.add_argument("--superpose", action=argparse.BooleanOptionalAction,
|
|
|
|
|
default=_env_bool("HOMEMAKER_SUPERPOSE", False),
|
|
|
|
|
help="type superposition (9o5): interchangeable codes (similar "
|
|
|
|
|
"requirements) form equivalence classes and each candidate "
|
|
|
|
|
"collapses every superposed leaf to its best in-class usage "
|
|
|
|
|
"before scoring (default: off)")
|
2026-07-31 00:16:12 +01:00
|
|
|
p.add_argument("--multi-use", action=argparse.BooleanOptionalAction,
|
|
|
|
|
default=_env_bool("HOMEMAKER_MULTI_USE", False),
|
|
|
|
|
help="homemaker-py-1s3 (§26 path b): multi-use leaves as a "
|
|
|
|
|
"PERMANENT design goal. Codes declaring a mutual "
|
|
|
|
|
"'co_locate' (and passing interchangeable()'s S1-S4 "
|
|
|
|
|
"bounds) may be fused onto one leaf at construction time, "
|
|
|
|
|
"surviving unchanged into the output (unlike --superpose's "
|
|
|
|
|
"per-eval collapse to a single usage) (default: off)")
|
homemaker-py-6xh: wire shape-curve DP into driver.py as an NM warm-start
Promotes the validated shape-curve DP (experiments/shapecurve_spike.py,
2g7.4, DESIGN.md §37.2) from a reference-only spike into
src/homemaker_layout/shapecurve.py, and wires it into driver._evaluate as a
warm-start for innerloop.optimise: when eligible (single storey, no
leaf_sharing/superpose/max_share/multi_use) and no caller-supplied x0, the
DP's exact shape-feasible ratio point is written onto the tree before NM
runs, off by default (shapecurve_warmstart=/--shapecurve-warmstart).
Caught and fixed a latent bug promoting the spike: realise() could leave
numpy.float64 in `division`, which yaml.safe_dump can't serialise — the
original spike never round-tripped through dom.dumps so this was never hit.
A/B on harbor-house-l0 (experiments/ab_shapecurve_warmstart.py, budget=2000,
5 seeds): mean total fails 16.6 (on) vs 19.6 (off), ~3.5x mean fitness
improvement; mean hard-fail count alone was a noise-level wash at this
sample size. Full writeup in DESIGN.md §37.4.
Deliberately deferred to new tracked beads (children of 2g7): DP-exact hard
pre-filter (wkh), multi-storey below-link support (koo), leaf_sharing/
co_type modelling (tym), true skew-quad polygon algebra (ekc) — 6xh stays
in_progress pending those.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-03 18:43:28 +01:00
|
|
|
p.add_argument("--shapecurve-warmstart", dest="shapecurve_warmstart",
|
|
|
|
|
action=argparse.BooleanOptionalAction,
|
|
|
|
|
default=_env_bool("HOMEMAKER_SHAPECURVE_WARMSTART", False),
|
|
|
|
|
help="homemaker-py-6xh (DESIGN.md §37.2/§37.4): warm-start "
|
|
|
|
|
"each child's inner-loop ratio search from the exact "
|
|
|
|
|
"Otten/Stockmeyer shape-curve DP solution instead of "
|
|
|
|
|
"the proportion-aware target-geometry seed, when the "
|
|
|
|
|
"topology is eligible (single storey, no leaf-sharing/"
|
|
|
|
|
"superpose/max-share/multi-use — none of which the DP "
|
|
|
|
|
"models). Falls through to today's start unchanged when "
|
|
|
|
|
"ineligible or DP-infeasible (default: off)")
|
2026-08-03 21:10:28 +01:00
|
|
|
p.add_argument("--shapecurve-prune", dest="shapecurve_prune",
|
|
|
|
|
action=argparse.BooleanOptionalAction,
|
|
|
|
|
default=_env_bool("HOMEMAKER_SHAPECURVE_PRUNE", False),
|
|
|
|
|
help="homemaker-py-wkh (DESIGN.md §37.5): use the shape-curve "
|
|
|
|
|
"DP's exact feasible/infeasible verdict alongside the "
|
|
|
|
|
"existing predicted_shape_fails pre-filter (a no-op "
|
|
|
|
|
"unless the driver.search()-level feasibility_filter is "
|
|
|
|
|
"also on -- not yet exposed as its own CLI flag). A "
|
|
|
|
|
"DP-feasible verdict vetoes a heuristic-triggered prune "
|
|
|
|
|
"outright (a real shape-feasible point exists, so the "
|
|
|
|
|
"heuristic's high count was a false signal); a "
|
|
|
|
|
"DP-infeasible verdict prunes immediately only when the "
|
|
|
|
|
"incumbent already has zero total fails (exact: "
|
|
|
|
|
"DP-infeasible proves the shape-fail floor is >=1); "
|
|
|
|
|
"otherwise falls through to today's heuristic-count "
|
|
|
|
|
"decision unchanged (default: off)")
|
2026-07-18 18:44:24 +01:00
|
|
|
p.add_argument("--conn-grade", dest="conn_grade",
|
|
|
|
|
action=argparse.BooleanOptionalAction,
|
|
|
|
|
default=_env_bool("HOMEMAKER_CONN_GRADE", False),
|
|
|
|
|
help="homemaker-py-qi6 (§18): graded circulation-connectivity "
|
|
|
|
|
"signal. Adds a secondary comparator key (beneath fail "
|
|
|
|
|
"count, above fitness) = per-level largest-circ-component "
|
|
|
|
|
"fraction, giving the search a gradient toward connected "
|
|
|
|
|
"circulation that the binary 'not connected' fail lacks. "
|
|
|
|
|
"Does not change the scalar fitness or fail count "
|
|
|
|
|
"(default: off)")
|
homemaker-py-2g7.3: hard/soft fail tiering behind --use-tiers flag
Splits the flat outer-search comparator (-n_fails, fitness) into a tiered
(-n_hard, -n_soft, fitness) so search budget stops being spent polishing
SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/
staircase-volume) while HARD structural fails (missing space, wrong/
required level, level/circulation/vertical connectivity, adjacency,
stairs, covered-outside, storey limits, public access) remain unfixed.
fitness.classify_fail_tier/tier_counts classify every fail string emitted
across fitness.py and graph.py, raising on anything unrecognised so new
fail sites must declare a tier. Validated against all real fail strings in
the checked-in corpus plus every fail-emission call site read from source.
driver.Individual gains n_hard/n_soft (populated from innerloop.Result.
fail_lines); search(use_tiers=...) swaps the comparator when set (default
off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched).
evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS.
experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3
seeds, 20k evals) in the background; results pending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-02 16:00:39 +01:00
|
|
|
p.add_argument("--use-tiers", dest="use_tiers",
|
|
|
|
|
action=argparse.BooleanOptionalAction,
|
|
|
|
|
default=_env_bool("HOMEMAKER_USE_TIERS", False),
|
|
|
|
|
help="homemaker-py-2g7.3 (DESIGN.md §37): hard/soft fail "
|
|
|
|
|
"tiering. Outer comparator becomes (-n_hard, -n_soft, "
|
|
|
|
|
"fitness) instead of (-n_fails, fitness), so budget "
|
|
|
|
|
"stops being spent polishing SOFT shape fails "
|
|
|
|
|
"(crinkliness/proportion/size/width/edge-too-long/"
|
|
|
|
|
"staircase-volume) while HARD structural fails (missing "
|
|
|
|
|
"space, wrong/required level, level/circulation/vertical "
|
|
|
|
|
"connectivity, adjacency, stairs, covered-outside, "
|
|
|
|
|
"storey limits, public access) remain unfixed. Does not "
|
|
|
|
|
"change the scalar fitness or total fail count "
|
|
|
|
|
"(default: off)")
|
2026-07-24 19:48:13 +01:00
|
|
|
p.add_argument("--bridge-circulation", dest="bridge_circulation",
|
|
|
|
|
action=argparse.BooleanOptionalAction,
|
|
|
|
|
default=_env_bool("HOMEMAKER_BRIDGE_CIRCULATION", False),
|
|
|
|
|
help="homemaker-py-8sh (qi6 mechanism (a) follow-on): "
|
|
|
|
|
"explicit repair mutation that retypes the cheapest "
|
|
|
|
|
"path between two disconnected circulation components "
|
|
|
|
|
"to circulation, directly clearing a 'level N not "
|
|
|
|
|
"connected' fail instead of relying on the qi6 graded "
|
|
|
|
|
"comparator key (measured negative, §18) (default: off)")
|
2026-07-26 09:31:42 +01:00
|
|
|
p.add_argument("--ruin-recreate", dest="ruin_recreate",
|
|
|
|
|
action=argparse.BooleanOptionalAction,
|
|
|
|
|
default=_env_bool("HOMEMAKER_RUIN_RECREATE", False),
|
|
|
|
|
help="homemaker-py-f1d: large-neighbourhood-search repair "
|
|
|
|
|
"mutation that un-divides one wing of a storey and "
|
|
|
|
|
"rebuilds it with the adjacency-aware constructor "
|
|
|
|
|
"(seeded from the surviving circulation bordering the "
|
|
|
|
|
"wing), applying the one construction technique with a "
|
|
|
|
|
"track record repeatedly during search instead of only "
|
|
|
|
|
"at seeding (default: off)")
|
2026-07-19 20:35:18 +01:00
|
|
|
p.add_argument("--collapse-insearch", dest="collapse_insearch",
|
|
|
|
|
action=argparse.BooleanOptionalAction,
|
2026-07-24 09:55:34 +01:00
|
|
|
default=_env_bool("HOMEMAKER_COLLAPSE_INSEARCH", True),
|
|
|
|
|
help="homemaker-py-qpk (§17 follow-on): run the 94g global "
|
|
|
|
|
"cell→room collapse inside every fitness eval instead of "
|
|
|
|
|
"once at finish time, so search optimises the collapsed "
|
|
|
|
|
"objective directly. A/B-validated positive on both "
|
|
|
|
|
"harbor-house (3/3, mean fails 80.3→72.0) and, after the "
|
|
|
|
|
"homemaker-py-1ph larger-N sweep, programme-house (11/17 "
|
|
|
|
|
"non-tied wins, mean fails 7.95→7.10) (default: on)")
|
2026-07-16 08:38:08 +01:00
|
|
|
p.add_argument("--anneal-grain", type=str,
|
|
|
|
|
default=os.environ.get("HOMEMAKER_ANNEAL_GRAIN"),
|
|
|
|
|
metavar="LADDER",
|
|
|
|
|
help="homemaker-py-kpu (Schedule B): in-run leaf-share grain "
|
|
|
|
|
"annealing. A descending comma-separated grain ladder (e.g. "
|
|
|
|
|
"'4,3,2') ramped down across phases within one run, "
|
|
|
|
|
"unfolding leaves that exceed each new cap and carrying the "
|
|
|
|
|
"population across steps, then a de-share polish. Implies "
|
|
|
|
|
"leaf-sharing; --budget is split across the sharing phases "
|
|
|
|
|
"and --polish-budget funds the final de-share phase. Unset "
|
|
|
|
|
"(default) = the single-transition §15 finish.")
|
2026-07-15 10:21:58 +01:00
|
|
|
p.add_argument("--polish-budget", type=int,
|
|
|
|
|
default=_env_int("HOMEMAKER_POLISH_BUDGET", -1),
|
|
|
|
|
metavar="N",
|
|
|
|
|
help="homemaker-py-3l6: after a leaf-sharing run, unfold the "
|
|
|
|
|
"shared leaves and run this many extra evals of "
|
|
|
|
|
"no-sharing local search to clean up the materialised "
|
|
|
|
|
"rooms before write, so the output is honest under the "
|
|
|
|
|
"canonical scorer. -1 = auto (budget//2); 0 = unfold + "
|
|
|
|
|
"rescore only, no polish. Ignored with --no-leaf-sharing "
|
|
|
|
|
"(default: -1)")
|
94g: public-access pin + keep-better wrapper + CLI/finish-hook wiring
Public-access term (preserve_public_access, default on): when the building's
only street access is an l/k ROOM neighbour of a public outside leaf (no
circulation fallback — an existential building-level check the per-leaf
objective can't see), that leaf is pinned (kept, its demand slot decremented)
so the collapse can't drop "no outside public access". Best layout 15→13
becomes 15→12 with zero new fails; sweep total 172→171, still monotone.
collapse_finish(root, **kw) -> (tree, base, coll, applied): keep-better wrapper,
scores on throwaway copies (score_with_fails merges in place), returns the
collapse only if fails don't increase.
Wiring: driver.collapse_best updates result.best (lineage +collapse, canonical
re-score); evolve.py runs it after the sharing polish behind --collapse/
--no-collapse (default on). New homemaker-collapse CLI (collapse_cmd.py) applies
it to an existing .dom, writing <stem>.collapsed.dom.
tests/test_collapse_global.py: demand-set relabel, level hard constraint, c/o/s
exclusion, no-op safety, keep-better/unmerged. 267 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
2026-07-18 10:29:44 +01:00
|
|
|
p.add_argument("--collapse", action=argparse.BooleanOptionalAction,
|
|
|
|
|
default=True,
|
|
|
|
|
help="homemaker-py-94g: finish-time global cell→room collapse — "
|
|
|
|
|
"relabel the best layout's room cells to the programme "
|
|
|
|
|
"rooms they fit best (level + adjacency + public-access "
|
|
|
|
|
"constrained), kept only if it does not increase the fail "
|
|
|
|
|
"count. Labels only, never geometry (default: on)")
|
2026-07-26 23:33:40 +01:00
|
|
|
p.add_argument("--collapse-local-search", dest="collapse_local_search",
|
|
|
|
|
action=argparse.BooleanOptionalAction, default=True,
|
|
|
|
|
help="homemaker-py-9wi/cdl: run the 2-opt adjacency polish "
|
|
|
|
|
"past collapse_global's Jacobi fixpoint as part of "
|
|
|
|
|
"--collapse. A 46-file A/B sweep (harbor-house + "
|
|
|
|
|
"programme-house) found 0 regressions, 2 improvements "
|
|
|
|
|
"(default: on). Ignored if --no-collapse")
|
2026-06-14 07:48:13 +01:00
|
|
|
p.add_argument("--output", type=Path, default=None, metavar="PATH",
|
|
|
|
|
help="output .dom path (- for stdout)")
|
|
|
|
|
return p.parse_args(argv)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
|
|
|
args = _parse_args(argv)
|
|
|
|
|
|
|
|
|
|
seed_file = args.seed_dom.resolve()
|
|
|
|
|
if not seed_file.exists():
|
|
|
|
|
print(f"ERROR: seed not found: {seed_file}", file=sys.stderr)
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
programme_dir = (args.programme_dir or seed_file.parent).resolve()
|
|
|
|
|
if not programme_dir.is_dir():
|
|
|
|
|
print(f"ERROR: not a directory: {programme_dir}", file=sys.stderr)
|
|
|
|
|
return 1
|
|
|
|
|
if not (programme_dir / "patterns.config").exists():
|
|
|
|
|
print(f"ERROR: no patterns.config in {programme_dir}", file=sys.stderr)
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
if args.output is None:
|
|
|
|
|
out: Path | None = seed_file.parent / (seed_file.stem + "_evolved.dom")
|
|
|
|
|
elif str(args.output) == "-":
|
|
|
|
|
out = None
|
|
|
|
|
else:
|
|
|
|
|
out = args.output.resolve()
|
|
|
|
|
|
|
|
|
|
print(f"seed : {seed_file}", file=sys.stderr)
|
|
|
|
|
print(f"programme : {programme_dir.name}", file=sys.stderr)
|
|
|
|
|
print(f"budget : {args.budget}", file=sys.stderr)
|
|
|
|
|
print(f"pop : {args.pop}", file=sys.stderr)
|
|
|
|
|
print(f"child_budget : {args.child_budget}", file=sys.stderr)
|
|
|
|
|
print(f"workers : {args.workers}", file=sys.stderr)
|
|
|
|
|
print(f"rng seed : {args.seed}", file=sys.stderr)
|
2026-06-28 22:04:35 +01:00
|
|
|
print(f"leaf sharing : {args.leaf_sharing} (factor={args.leaf_share_factor})",
|
|
|
|
|
file=sys.stderr)
|
9o5: type superposition + per-eval collapse (multi-use leaves)
Interchangeable codes (similar size/width/proportion, compatible level/stack,
no adjacency edge) form equivalence classes derived from the programme. With
--superpose (default off), each fitness eval COLLAPSES every superposed leaf to
its best in-class usage via an optimal supply->demand assignment (brute force
<=C! within cap C=4, scipy Hungarian beyond), then scores the condensed types.
Because collapse re-types on the unmerged tree before all checks, counts /
adjacency / quality are unchanged downstream -- no Node field, no graph/operator
changes -- and default OFF is bit-identical.
- programme.py: derive_interchange_classes + interchangeable (S1-S4, locked
thresholds R_SIZE=1.5/R_WIDTH=1.3/R_PROP=1.5, CLASS_CAP=4)
- fitness.py: collapse_superposition, _best_assignment, _usage_quality;
superpose/superpose_class_cap conf knobs; collapse hooked into _evaluate_full
- driver.py/evolve.py: superpose flag plumbed beside leaf_sharing; --superpose
- tests/test_superposition.py: 17 tests (derivation, assignment, end-to-end)
Closes homemaker-py-9o5 (build); validation A/B is homemaker-py-xi7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 07:08:46 +01:00
|
|
|
print(f"superpose : {args.superpose}", file=sys.stderr)
|
2026-07-31 00:16:12 +01:00
|
|
|
print(f"multi_use : {args.multi_use}", file=sys.stderr)
|
2026-07-18 18:44:24 +01:00
|
|
|
print(f"conn grade : {args.conn_grade}", file=sys.stderr)
|
homemaker-py-2g7.3: hard/soft fail tiering behind --use-tiers flag
Splits the flat outer-search comparator (-n_fails, fitness) into a tiered
(-n_hard, -n_soft, fitness) so search budget stops being spent polishing
SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/
staircase-volume) while HARD structural fails (missing space, wrong/
required level, level/circulation/vertical connectivity, adjacency,
stairs, covered-outside, storey limits, public access) remain unfixed.
fitness.classify_fail_tier/tier_counts classify every fail string emitted
across fitness.py and graph.py, raising on anything unrecognised so new
fail sites must declare a tier. Validated against all real fail strings in
the checked-in corpus plus every fail-emission call site read from source.
driver.Individual gains n_hard/n_soft (populated from innerloop.Result.
fail_lines); search(use_tiers=...) swaps the comparator when set (default
off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched).
evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS.
experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3
seeds, 20k evals) in the background; results pending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-02 16:00:39 +01:00
|
|
|
print(f"use tiers : {args.use_tiers}", file=sys.stderr)
|
2026-07-24 19:48:13 +01:00
|
|
|
print(f"bridge circulation : {args.bridge_circulation}", file=sys.stderr)
|
2026-07-26 09:31:42 +01:00
|
|
|
print(f"ruin recreate : {args.ruin_recreate}", file=sys.stderr)
|
2026-07-19 20:35:18 +01:00
|
|
|
print(f"collapse in-search : {args.collapse_insearch}", file=sys.stderr)
|
homemaker-py-6xh: wire shape-curve DP into driver.py as an NM warm-start
Promotes the validated shape-curve DP (experiments/shapecurve_spike.py,
2g7.4, DESIGN.md §37.2) from a reference-only spike into
src/homemaker_layout/shapecurve.py, and wires it into driver._evaluate as a
warm-start for innerloop.optimise: when eligible (single storey, no
leaf_sharing/superpose/max_share/multi_use) and no caller-supplied x0, the
DP's exact shape-feasible ratio point is written onto the tree before NM
runs, off by default (shapecurve_warmstart=/--shapecurve-warmstart).
Caught and fixed a latent bug promoting the spike: realise() could leave
numpy.float64 in `division`, which yaml.safe_dump can't serialise — the
original spike never round-tripped through dom.dumps so this was never hit.
A/B on harbor-house-l0 (experiments/ab_shapecurve_warmstart.py, budget=2000,
5 seeds): mean total fails 16.6 (on) vs 19.6 (off), ~3.5x mean fitness
improvement; mean hard-fail count alone was a noise-level wash at this
sample size. Full writeup in DESIGN.md §37.4.
Deliberately deferred to new tracked beads (children of 2g7): DP-exact hard
pre-filter (wkh), multi-storey below-link support (koo), leaf_sharing/
co_type modelling (tym), true skew-quad polygon algebra (ekc) — 6xh stays
in_progress pending those.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-03 18:43:28 +01:00
|
|
|
print(f"shapecurve warmstart : {args.shapecurve_warmstart}", file=sys.stderr)
|
2026-08-03 21:10:28 +01:00
|
|
|
print(f"shapecurve prune : {args.shapecurve_prune}", file=sys.stderr)
|
2026-06-14 07:48:13 +01:00
|
|
|
print(f"output : {out or 'stdout'}", file=sys.stderr, flush=True)
|
|
|
|
|
|
2026-07-16 08:38:08 +01:00
|
|
|
anneal_ladder = None
|
|
|
|
|
if args.anneal_grain:
|
|
|
|
|
anneal_ladder = tuple(int(g) for g in args.anneal_grain.split(",")
|
|
|
|
|
if g.strip())
|
|
|
|
|
|
2026-06-14 07:48:13 +01:00
|
|
|
seed_root = dom.load(str(seed_file))
|
|
|
|
|
t0 = time.perf_counter()
|
|
|
|
|
|
|
|
|
|
# SIGTERM → KeyboardInterrupt so the driver's interrupt handler fires.
|
|
|
|
|
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt()))
|
|
|
|
|
|
2026-07-16 08:38:08 +01:00
|
|
|
if anneal_ladder:
|
|
|
|
|
# homemaker-py-kpu (Schedule B): in-run grain annealing already ends with a
|
|
|
|
|
# de-share polish, so it is self-finishing — the §15 unfold+polish is not
|
|
|
|
|
# applied on top.
|
|
|
|
|
polish_budget = args.budget // 2 if args.polish_budget < 0 else args.polish_budget
|
|
|
|
|
print(f"anneal grain : {anneal_ladder} → off (polish {polish_budget})",
|
|
|
|
|
file=sys.stderr, flush=True)
|
|
|
|
|
r = driver.search_annealed(
|
|
|
|
|
seed_root,
|
|
|
|
|
programme_dir,
|
|
|
|
|
budget=args.budget,
|
|
|
|
|
polish_budget=polish_budget,
|
|
|
|
|
grain_ladder=anneal_ladder,
|
|
|
|
|
pop_size=args.pop,
|
|
|
|
|
child_budget=args.child_budget,
|
|
|
|
|
p_crossover=0.2,
|
|
|
|
|
seed=args.seed,
|
|
|
|
|
n_workers=args.workers,
|
|
|
|
|
superpose=args.superpose,
|
2026-07-31 00:16:12 +01:00
|
|
|
multi_use=args.multi_use,
|
2026-07-16 08:38:08 +01:00
|
|
|
log=lambda m: print(m, file=sys.stderr, flush=True),
|
|
|
|
|
)
|
|
|
|
|
_finish_sharing = False
|
|
|
|
|
else:
|
|
|
|
|
r = driver.search(
|
|
|
|
|
seed_root,
|
|
|
|
|
programme_dir,
|
|
|
|
|
budget=args.budget,
|
|
|
|
|
pop_size=args.pop,
|
|
|
|
|
child_budget=args.child_budget,
|
|
|
|
|
p_crossover=0.2,
|
|
|
|
|
seed=args.seed,
|
|
|
|
|
n_workers=args.workers,
|
|
|
|
|
leaf_sharing=args.leaf_sharing,
|
|
|
|
|
leaf_share_factor=args.leaf_share_factor,
|
|
|
|
|
superpose=args.superpose,
|
2026-07-31 00:16:12 +01:00
|
|
|
multi_use=args.multi_use,
|
2026-07-18 18:44:24 +01:00
|
|
|
conn_grade=args.conn_grade,
|
homemaker-py-2g7.3: hard/soft fail tiering behind --use-tiers flag
Splits the flat outer-search comparator (-n_fails, fitness) into a tiered
(-n_hard, -n_soft, fitness) so search budget stops being spent polishing
SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/
staircase-volume) while HARD structural fails (missing space, wrong/
required level, level/circulation/vertical connectivity, adjacency,
stairs, covered-outside, storey limits, public access) remain unfixed.
fitness.classify_fail_tier/tier_counts classify every fail string emitted
across fitness.py and graph.py, raising on anything unrecognised so new
fail sites must declare a tier. Validated against all real fail strings in
the checked-in corpus plus every fail-emission call site read from source.
driver.Individual gains n_hard/n_soft (populated from innerloop.Result.
fail_lines); search(use_tiers=...) swaps the comparator when set (default
off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched).
evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS.
experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3
seeds, 20k evals) in the background; results pending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-02 16:00:39 +01:00
|
|
|
use_tiers=args.use_tiers,
|
2026-07-24 19:48:13 +01:00
|
|
|
enable_bridge_circulation=args.bridge_circulation,
|
2026-07-26 09:31:42 +01:00
|
|
|
enable_ruin_recreate=args.ruin_recreate,
|
2026-07-19 20:35:18 +01:00
|
|
|
collapse_insearch=args.collapse_insearch,
|
homemaker-py-6xh: wire shape-curve DP into driver.py as an NM warm-start
Promotes the validated shape-curve DP (experiments/shapecurve_spike.py,
2g7.4, DESIGN.md §37.2) from a reference-only spike into
src/homemaker_layout/shapecurve.py, and wires it into driver._evaluate as a
warm-start for innerloop.optimise: when eligible (single storey, no
leaf_sharing/superpose/max_share/multi_use) and no caller-supplied x0, the
DP's exact shape-feasible ratio point is written onto the tree before NM
runs, off by default (shapecurve_warmstart=/--shapecurve-warmstart).
Caught and fixed a latent bug promoting the spike: realise() could leave
numpy.float64 in `division`, which yaml.safe_dump can't serialise — the
original spike never round-tripped through dom.dumps so this was never hit.
A/B on harbor-house-l0 (experiments/ab_shapecurve_warmstart.py, budget=2000,
5 seeds): mean total fails 16.6 (on) vs 19.6 (off), ~3.5x mean fitness
improvement; mean hard-fail count alone was a noise-level wash at this
sample size. Full writeup in DESIGN.md §37.4.
Deliberately deferred to new tracked beads (children of 2g7): DP-exact hard
pre-filter (wkh), multi-storey below-link support (koo), leaf_sharing/
co_type modelling (tym), true skew-quad polygon algebra (ekc) — 6xh stays
in_progress pending those.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
2026-08-03 18:43:28 +01:00
|
|
|
shapecurve_warmstart=args.shapecurve_warmstart,
|
2026-08-03 21:10:28 +01:00
|
|
|
shapecurve_prune=args.shapecurve_prune,
|
2026-07-16 08:38:08 +01:00
|
|
|
log=lambda m: print(m, file=sys.stderr, flush=True),
|
|
|
|
|
)
|
|
|
|
|
_finish_sharing = args.leaf_sharing
|
2026-06-14 07:48:13 +01:00
|
|
|
|
2026-07-15 10:21:58 +01:00
|
|
|
# homemaker-py-3l6: a leaf-sharing run's internal best is scored against a
|
|
|
|
|
# sharing-credited objective (a shared leaf counts as k programme rooms), so
|
|
|
|
|
# r.best is dishonest under the canonical scorer — it is k-1 rooms short per
|
|
|
|
|
# shared leaf. Unfold those leaves and warm-start a no-sharing polish so the
|
|
|
|
|
# written .dom is honest AND its materialised rooms are cleaned up (yaa: the
|
|
|
|
|
# unfold-then-polish path catches the direct no-sharing route). After this,
|
|
|
|
|
# r.best.fitness is the canonical score (leaf_sharing off ⇒ internal == canon).
|
2026-07-16 08:38:08 +01:00
|
|
|
if _finish_sharing and r.best is not None:
|
2026-07-15 10:21:58 +01:00
|
|
|
polish_budget = args.budget // 2 if args.polish_budget < 0 else args.polish_budget
|
|
|
|
|
# An interrupted sharing run still needs an honest output, but the user
|
|
|
|
|
# asked to stop — unfold and rescore only, skip the long polish phase.
|
|
|
|
|
if r.interrupted:
|
|
|
|
|
polish_budget = 0
|
|
|
|
|
print(file=sys.stderr)
|
|
|
|
|
print(f"--- finishing (homemaker-py-3l6): unfold + polish "
|
|
|
|
|
f"{polish_budget} evals ---", file=sys.stderr, flush=True)
|
|
|
|
|
r = driver.polish_finish(
|
|
|
|
|
r, programme_dir,
|
|
|
|
|
polish_budget=polish_budget,
|
|
|
|
|
pop_size=args.pop,
|
|
|
|
|
child_budget=args.child_budget,
|
|
|
|
|
p_crossover=0.2,
|
|
|
|
|
seed=args.seed,
|
|
|
|
|
n_workers=args.workers,
|
|
|
|
|
superpose=args.superpose,
|
2026-07-31 00:16:12 +01:00
|
|
|
multi_use=args.multi_use,
|
2026-07-19 20:35:18 +01:00
|
|
|
collapse_insearch=args.collapse_insearch,
|
2026-07-15 10:21:58 +01:00
|
|
|
log=lambda m: print(m, file=sys.stderr, flush=True),
|
94g: public-access pin + keep-better wrapper + CLI/finish-hook wiring
Public-access term (preserve_public_access, default on): when the building's
only street access is an l/k ROOM neighbour of a public outside leaf (no
circulation fallback — an existential building-level check the per-leaf
objective can't see), that leaf is pinned (kept, its demand slot decremented)
so the collapse can't drop "no outside public access". Best layout 15→13
becomes 15→12 with zero new fails; sweep total 172→171, still monotone.
collapse_finish(root, **kw) -> (tree, base, coll, applied): keep-better wrapper,
scores on throwaway copies (score_with_fails merges in place), returns the
collapse only if fails don't increase.
Wiring: driver.collapse_best updates result.best (lineage +collapse, canonical
re-score); evolve.py runs it after the sharing polish behind --collapse/
--no-collapse (default on). New homemaker-collapse CLI (collapse_cmd.py) applies
it to an existing .dom, writing <stem>.collapsed.dom.
tests/test_collapse_global.py: demand-set relabel, level hard constraint, c/o/s
exclusion, no-op safety, keep-better/unmerged. 267 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
2026-07-18 10:29:44 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# homemaker-py-94g: finish-time global cell→room collapse. Relabels the best
|
|
|
|
|
# layout's room cells to the programme rooms they fit best (label search only,
|
|
|
|
|
# no geometry change), kept only if it does not increase the fail count. Runs
|
|
|
|
|
# after the sharing polish so it acts on the canonical (materialised) best.
|
|
|
|
|
if args.collapse and r.best is not None:
|
|
|
|
|
print(file=sys.stderr)
|
|
|
|
|
print("--- collapse (homemaker-py-94g): finish-time cell→room relabel ---",
|
|
|
|
|
file=sys.stderr, flush=True)
|
|
|
|
|
r = driver.collapse_best(
|
|
|
|
|
r, programme_dir,
|
|
|
|
|
superpose=args.superpose,
|
2026-07-31 00:16:12 +01:00
|
|
|
multi_use=args.multi_use,
|
2026-07-26 23:33:40 +01:00
|
|
|
local_search=args.collapse_local_search,
|
94g: public-access pin + keep-better wrapper + CLI/finish-hook wiring
Public-access term (preserve_public_access, default on): when the building's
only street access is an l/k ROOM neighbour of a public outside leaf (no
circulation fallback — an existential building-level check the per-leaf
objective can't see), that leaf is pinned (kept, its demand slot decremented)
so the collapse can't drop "no outside public access". Best layout 15→13
becomes 15→12 with zero new fails; sweep total 172→171, still monotone.
collapse_finish(root, **kw) -> (tree, base, coll, applied): keep-better wrapper,
scores on throwaway copies (score_with_fails merges in place), returns the
collapse only if fails don't increase.
Wiring: driver.collapse_best updates result.best (lineage +collapse, canonical
re-score); evolve.py runs it after the sharing polish behind --collapse/
--no-collapse (default on). New homemaker-collapse CLI (collapse_cmd.py) applies
it to an existing .dom, writing <stem>.collapsed.dom.
tests/test_collapse_global.py: demand-set relabel, level hard constraint, c/o/s
exclusion, no-op safety, keep-better/unmerged. 267 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
2026-07-18 10:29:44 +01:00
|
|
|
log=lambda m: print(m, file=sys.stderr, flush=True),
|
2026-07-15 10:21:58 +01:00
|
|
|
)
|
|
|
|
|
|
2026-06-14 07:48:13 +01:00
|
|
|
elapsed = time.perf_counter() - t0
|
|
|
|
|
|
|
|
|
|
print(file=sys.stderr)
|
|
|
|
|
status = "interrupted" if r.interrupted else "done"
|
|
|
|
|
print(f"--- {status} ---", file=sys.stderr)
|
|
|
|
|
print(f"elapsed : {elapsed:.1f}s", file=sys.stderr)
|
|
|
|
|
print(f"evals : {r.n_evals} across {r.n_topologies} topologies",
|
|
|
|
|
file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
if r.best is None:
|
|
|
|
|
print("ERROR: no result produced", file=sys.stderr)
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
print(f"best : {r.best.fitness:.6g} ({r.best.n_fails} fails) "
|
|
|
|
|
f"via {r.best.lineage}", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
if r.history:
|
|
|
|
|
print("\nimprovement history:", file=sys.stderr)
|
|
|
|
|
for ev, fit_val, lin in r.history:
|
|
|
|
|
print(f" [{ev:6d}] {fit_val:.6g} ({lin})", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
if out is None:
|
|
|
|
|
sys.stdout.write(dom.dumps(r.best.root))
|
|
|
|
|
else:
|
|
|
|
|
dom.dump(r.best.root, str(out))
|
|
|
|
|
print(f"written : {out}", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(main())
|