From 0d94e58119bd2ac77480e366cebac06afb08776d Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sun, 26 Jul 2026 09:31:42 +0100 Subject: [PATCH] f1d: ruin-and-recreate LNS operator, validated positive on programme-house MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds operators.mutate_ruin_recreate: un-divides one wing of a storey and rebuilds it with the same adjacency-aware constructor the seeders use (_assign_adjacency_aware, generalised with a new `scope` param), seeded from the surviving circulation bordering the wing. Gated behind enable_ruin_recreate (default off) / --ruin-recreate, same pattern as reassociate/bridge_circulation. A/B (qpk protocol, DESIGN.md §23): initial uniform-weight run was underpowered (fired ~1/32 children), null. A weight=3.0 follow-up (_MUTATION_WEIGHTS["ruin_recreate"]) showed a statistically significant win on programme-house across 15 seeds (8W/1L/6T, mean fails 7.07->6.00, Wilcoxon p=0.041) but no consistent effect on harbor-house across 8 seeds (3W/2L/3T). Kept default off pending a size-threshold follow-up. Co-Authored-By: Claude Sonnet 5 --- .beads/issues.jsonl | 36 +++++----- DESIGN.md | 94 ++++++++++++++++++++++++++ experiments/run_f1d_ab.sh | 53 +++++++++++++++ experiments/run_f1d_larger_n.sh | 50 ++++++++++++++ experiments/run_f1d_w3_ab.sh | 45 +++++++++++++ src/homemaker_layout/driver.py | 20 +++++- src/homemaker_layout/evolve.py | 12 ++++ src/homemaker_layout/operators.py | 106 +++++++++++++++++++++++++++--- 8 files changed, 391 insertions(+), 25 deletions(-) create mode 100755 experiments/run_f1d_ab.sh create mode 100755 experiments/run_f1d_larger_n.sh create mode 100755 experiments/run_f1d_w3_ab.sh diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 6b85e09..b73fe4a 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -22,6 +22,9 @@ {"id":"homemaker-py-1p0","title":"Geometry inner loop: full-objective equal-offset ratio optimiser","description":"DESIGN.md §5.1, §7 Phase 1. Productionise experiments/optimize_fullfitness.py into homemaker: optimise(topology, x0=None) -\u003e (geometry, fitness). DOF = equal-offset division ratios of free branches (solver.free_branches, lowest-storey cut ownership), clipped to [eps, 1-eps]. Objective = full oracle fitness (never a proxy — §4.2 falsified). Must support warm-start x0 (§5.6) and a population/batch evaluation mode so each iteration scores via one batched oracle call (§4.6).","acceptance_criteria":"Reproduces or exceeds §4.5 gains (x1.24–x1.67, no new failures) on 2f45907, candidate-002, c964435; works as a library call on any corpus .dom","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:58Z","created_by":"Bruno Postle","updated_at":"2026-06-12T08:46:31Z","started_at":"2026-06-12T00:14:19Z","closed_at":"2026-06-12T08:46:31Z","close_reason":"innerloop.optimise() lands: batched CMA-ES sigma ladder (0.05/0.15, IPOP popsize doubling, deterministic seeding) over equal-offset free-branch ratios vs full oracle fitness; warm-start x0 supported. Acceptance vs unprojected originals: x1.65/x1.66/x1.58 against bars x1.24/x1.67/x1.59, no new failures, 46 oracle calls vs NM's 200. Two near-bar results accepted as reproduced-within-noise (1% tol) — draw spread brackets the single-NM-draw bars; approved by Bruno 2026-06-12. Gotchas: equal-offset projection of legacy unequal cuts loses fitness/adds failures (midpoint projection used); pycma seed=0 means clock-seeded.","dependencies":[{"issue_id":"homemaker-py-1p0","depends_on_id":"homemaker-py-av5","type":"blocks","created_at":"2026-06-12T00:39:33Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} {"id":"homemaker-py-8cs","title":"Experiment: warm-vs-cold start of inner loop (Lamarckian inheritance)","description":"DESIGN.md §5.6, §4.6. Warm-starting a child topology's inner loop from the parent's optimised ratios is the main lever for cutting per-topology cost (~3 min/topology cold). Apply single topology mutations to optimised corpus designs, re-optimise warm (surviving cuts keep values, new cuts get heuristic defaults) vs cold, compare oracle-call counts to convergence at equal final fitness.","acceptance_criteria":"Speedup factor measured across \u003e=10 mutated topologies; decision recorded (expect order-of-magnitude; if \u003c2x, revisit §4.6 Phase-2 scoping)","notes":"Experiment script committed (experiments/warm_vs_cold.py, 1cc86c8) and machinery validated oracle-free; one mutated child scored through the oracle OK. Waiting on homemaker-py-gp2 reference run to finish, then execute under URB_NO_OCCLUSION=1 (3 parents x 400 evals + 12 children x 2 x 200 evals, ~1.5-2 h oracle time). Default budgets: parent 400, child 200; target = evals to 95% of best final.","status":"closed","priority":1,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:58Z","created_by":"Bruno Postle","updated_at":"2026-06-12T11:44:45Z","closed_at":"2026-06-12T11:44:45Z","close_reason":"Measured (URB_NO_OCCLUSION=1, parent budget 400, child 200, 12 single mutations across 3 designs): cold start reached 95% of warm final in 0/12 cases within budget — speedup unbounded at practical budgets; warm finals beat cold finals x1.2-x4 in 12/12; 6/12 warm starts were within 95% at 1 eval (near-neutral mutations). Decision: Lamarckian warm-starting is MANDATORY in the memetic driver (homemaker-py-b39), not an optimisation; cold starts produce strictly worse geometry at equal budget. Note: 2 undivides were exactly fitness-neutral (same-type merge == Merge_Divided equivalence) — locality datum for homemaker-py-nyb.","dependencies":[{"issue_id":"homemaker-py-8cs","depends_on_id":"homemaker-py-1p0","type":"blocks","created_at":"2026-06-12T00:39:34Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-av5","title":"Batched oracle: score many .dom files per invocation","description":"oracle.py currently scores one .dom per urb-fitness.pl call (~1.65 s/dom). DESIGN.md §4.6: batching amortises Perl startup to ~0.99 s/dom and is required so population/batch optimisers can score a whole generation in one oracle call. Extend oracle.py with a batch API: write N .dom files, one perl invocation, parse N .score/.fails pairs. Keep the single-file path for compatibility.","acceptance_criteria":"Batch of 35 corpus files scores in one perl invocation; per-file results identical to single-file calls; measured s/dom reported","status":"closed","priority":1,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:56Z","created_by":"Bruno Postle","updated_at":"2026-06-12T00:14:06Z","started_at":"2026-06-11T23:50:40Z","closed_at":"2026-06-12T00:14:06Z","close_reason":"score_batch() lands in oracle.py; 35-file corpus parity verified single-vs-batch (1e-12 rel fitness, exact fail sets); 0.98 s/dom batched vs 1.27 single, x1.30","dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"homemaker-py-9wi","title":"Adjacency-aware discrete assignment for finish-time collapse (QAP/CP-SAT)","description":"fitness.collapse_superposition (homemaker-py-9o5/94g family) already does exact optimal relabelling of superposed leaves via brute-force permutation (CLASS_CAP\u003c=4) or Hungarian (linear_sum_assignment) beyond that -- but _best_assignment's docstring is explicit that the objective is deliberately SEPARABLE per leaf (quality_size * quality_width * quality_proportion only); perpendicular/crinkliness/access/adjacency are assumed usage-invariant within a class and left out, because adjacency quality depends on PAIRS of leaf-label assignments, not one leaf at a time, which breaks the exact separable solve.\n\nProposal: extend the collapse step to account for adjacency between candidate labels -- either a quadratic-assignment-style local search (2-opt swaps over the current Hungarian solution, accepting swaps that improve total adjacency satisfaction) or a CP-SAT (OR-Tools) encoding of the labelling problem with pairwise adjacency terms. This directly extends the one search-adjacent technique (exact/near-exact discrete assignment) that has actually paid off in this project, into territory the current separable solve cannot reach.\n\nMeasure against the current Hungarian-only collapse on harbor-house (heavy interchange-class usage: neighborhoods, meeting rooms, individual rooms) where adjacency-blind relabelling is most likely to leave adjacency fails on the table.","status":"open","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-07-25T20:18:24Z","created_by":"Bruno Postle","updated_at":"2026-07-25T20:18:24Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"homemaker-py-f1d","title":"Ruin-and-recreate LNS: rebuild wings with the adjacency-aware constructor mid-search","description":"DESIGN.md's own experiment log shows every 'search machinery' change (niching+restarts 11.5, graded objective 11.4, Wong-Liu reassociation+shape-feasibility 12.3, granularity 12.4, island model 14, grain annealing 16, circulation-repair ops 21/22) has come back null-to-negative, while construction/seeding quality (adjacency-aware seeding 11.6/11.7, proportion-aware seeding 12.2) is the only lever that has ever moved the fail count. operators._assign_adjacency_aware currently only runs once, at seeding.\n\nProposal: a large-neighbourhood-search move that periodically un-divides a whole wing/subtree of the CURRENT BEST individual and reconstructs just that region using the same proven adjacency-aware constructive heuristic (seeded from the surviving circulation spine as fixed_circ, same mechanism lift_base_to_storeys already uses for upper floors), instead of relying only on small local mutation operators to find improvements. Reuses the one technique with a real track record, applied repeatedly during search rather than once at initialisation.\n\nA/B against the current baseline on programme-house and harbor-house at a fixed worker count (see 12.4's determinism-fix note about serial vs parallel admission order before trusting sub-±3 effects).","notes":"Implementation landed (uncommitted, pending A/B): operators.mutate_ruin_recreate — picks a divided, live-cut wing of one storey (2..half its leaves), un-divides it, regrows+retypes it via a scope-generalised operators._assign_adjacency_aware (new 'scope' param restricts retyping to a leaf subset while fixed_circ seeds can be border leaves outside that subset), seeded from already-typed circulation leaves bordering the wing. Room-code budget inside the wing is preserved exactly; circ/outside counts rebuilt at circ_divisor=3/outside_divisor=3. Gated like reassociate/bridge_circulation: mutation_weights['ruin_recreate']=0.0 unless driver.search(enable_ruin_recreate=True); CLI flag --ruin-recreate/--no-ruin-recreate added to evolve.py (default off). 297 existing tests pass unchanged; added smoke coverage (200 applications on harbor-house constructive seeds: zero missing-space regressions, all canonical). A/B now running in background: experiments/run_f1d_ab.sh, qpk protocol (harbor-house budget=2500 seeds 1-3, programme-house budget=3000 seeds 1-5, workers=4, both arms finished with standard --collapse), results -\u003e scratch/f1d_ab_results.tsv.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-25T20:18:02Z","created_by":"Bruno Postle","updated_at":"2026-07-26T08:30:13Z","started_at":"2026-07-25T20:20:56Z","closed_at":"2026-07-26T08:30:13Z","close_reason":"DONE (positive, size-dependent). operators.mutate_ruin_recreate landed: un-divides one wing of a storey (2..half its leaves), rebuilds it via a scope-generalised _assign_adjacency_aware seeded from bordering circulation, mirroring lift_base_to_storeys' core-inheritance mechanism. Gated like reassociate/bridge_circulation (enable_ruin_recreate, default off; --ruin-recreate CLI flag). Initial uniform-weight A/B was null but underpowered (op fired ~1/32 children); a weight=3.0 follow-up (_MUTATION_WEIGHTS['ruin_recreate']=3.0, kept in source) showed a statistically significant win on programme-house across 15 seeds (8W/1L/6T, mean fails 7.07-\u003e6.00, Wilcoxon p=0.041, sign-test p=0.020) but no consistent effect on harbor-house across 8 seeds (3W/2L/3T, mean fails 73.0-\u003e74.5, slight negative lean). Kept default OFF pending a size-threshold follow-up (not filed) -- same conservative bar qpk/1ph applied before its own larger-N confirmation. Full writeup: DESIGN.md §23.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"homemaker-py-mi7","title":"Prototype: 3D bubble-diagram relaxation of programme adjacency as a fitness signal","description":"Explore building a spring/force relaxation over the programme's required-space adjacency graph (multiple random-restart solutions), then score how well an actual Dom layout's real adjacency-graph distances correlate with a relaxed target's distances. Goal: an additional fitness term / search-guidance signal beyond the existing binary adjacency checks in graph.py. Prototype module: bubble.py. Validate by correlating similarity score against existing fitness .score on examples/programme-house's 36 scored .dom files.","notes":"Harbor-house real trajectory (driver.search, budget=6000, n=75 recorded individuals, fitness 3e-28 -\u003e 3.9e-17, fails 83-\u003e51):\n- embedding similarity(): spearman=0.164 p=0.16 (n.s.)\n- topological_similarity(): spearman=-0.160 p=0.17 (n.s.)\n\nFINAL PICTURE across 4 tests (2 programmes x 2 metric formulations, plus 2 canned-batch tests earlier): no statistically significant correlation anywhere between either the spring/embedding bubble-diagram similarity or the pure topological/abstract-graph-fitting similarity, and existing programme-driven fitness score. programme-house's real-trajectory result (rho=0.05 / rho=-0.06, n=100) is the cleanest data point — that programme has zero multi-count anonymous codes, so matched_leaves' known centroid-order matching heuristic cannot be confounding it, and it's still flat. Harbor-house is noisier (heavy anonymous counts: n x5, m x3, t x6, r x10, of x2 — the fixed centroid-order matching there is a real, uncontrolled confound) but tells the same story.\n\nRecommendation: do not pursue graph-relaxation-derived or pure-topological adjacency-matching as a fitness signal for this project without a fundamentally different formulation — two independent formulations, tested on two programmes with real evolved trajectories (not just static examples), both came back null. If revisited later, the harbor-house confound (anonymous-code instance matching) would need a real assignment solver (Hungarian/brute-force per homemaker-py-9o5's CLASS_CAP pattern) before drawing any conclusion there specifically, but programme-house's clean null already argues against the core idea. bubble.py is left in the repo (uncommitted) as a documented, working prototype/reference — not wired into fitness.py.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-25T12:42:02Z","created_by":"Bruno Postle","updated_at":"2026-07-25T20:12:20Z","started_at":"2026-07-25T12:42:26Z","closed_at":"2026-07-25T20:12:20Z","close_reason":"Two independent formulations (spring/embedding bubble-diagram, pure topological hop-distance) both tested null against real evolve trajectories on programme-house (n=100, clean — no anonymous-code confound) and harbor-house (n=75). No positive correlation with existing fitness score found. bubble.py left in repo, uncommitted, as documented reference; not wired into fitness.py. Consistent with the project's broader pattern (see DESIGN.md §11.4/11.5/12.3/12.4/14/16/21/22): 'search machinery' / fitness-shaping changes have been null-to-negative essentially every time they've been tried; only construction/seeding quality and representation-relaxation changes (leaf-sharing, type superposition, global collapse) have ever moved the needle. This session's result is another data point for that pattern, not an exception.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-qi6","title":"Circulation placement to clear not-connected / access fails","description":"The 94g finish-time collapse cannot touch the \"not-connected\" (level N not\nconnected) and related access/inaccessible fails: they are properties of the\nCIRCULATION skeleton (c/o/s cells), which the collapse deliberately never\nrelabels (they form the structure the room assignment is layered onto). On the\nharbor-house best layout 2 of the 15 fails are not-connected (levels 0 and 1);\nthese are out of scope for any label optimisation.\n\nGOAL: a search/repair step that places or reshapes circulation so every usable\nspace is reachable and each storey's circulation graph is connected. Candidate\nmechanisms: (a) a mutation/operator that inserts a circulation cell to bridge a\ndisconnected component (graph.py already computes connected components +\nconnected_circulation); (b) a finish-time repair that re-types a boundary cell to\ncirculation where it reconnects the graph at least net-fail cost; (c) bias the\nouter search toward connected topologies via the graded signal.\n\nInteracts with 94g: circulation placement changes which cells are skeleton vs\nassignable, so it should run BEFORE the label collapse (collapse then optimises\nlabels over the improved skeleton). Also interacts with the public-access pin\n(94g) — better circulation placement can supply invariant inside public access,\nremoving the need to pin a room provider.\n\nMeasure on the 6 evolved layouts from the 94g sweep (not-connected + access +\ninaccessible fail counts). Related: 94g, homemaker-py-2g5.","notes":"LANDED (2026-07-18) mechanism (c) — graded circulation-connectivity signal (DESIGN §18). graph.circulation_connectivity(G) = largest-circ-component fraction [0,1]; summed over storeys it rides the score_with_grade proximity channel, gated by conf flag conn_grade (replaces the §11.4 leaf-grade on that channel). Secondary comparator key (-n_fails, grade, fitness) only — scalar fitness and fail count byte-identical (verified). Wired conn_grade through driver _overrides_for/_fitness_for/_evaluate/search (enabling it implies the grade key); evolve --conn-grade (HOMEMAKER_CONN_GRADE, default OFF). 9 new tests (tests/test_conn_grade.py), 276 pass; 60-eval CLI smoke confirms plumbing.\n\nA/B VERDICT (2026-07-22, qpk protocol, experiments/run_qi6_ab.sh) — NEGATIVE. conn_grade ON vs OFF, full-budget, both finished with --collapse: harbor-house (budget 2500, seeds 1-3) byte-identical output in every seed — the grade never fired. programme-house (budget 3000, seeds 1-5) 3/5 seeds tie exactly; seeds 1/2 diverge to a different topology with one fewer total fail, but the diff is adjacency/crinkliness/width/access/size, not connectivity. Zero cases (of 4) where a not-connected fail was present and cleared by the grade. Mechanism (b)/(c) (graded proximity as tertiary comparator key) is falsified, not just unconfirmed. Kept default OFF (already was). DESIGN.md §18 updated with full verdict.\n\nRemaining candidate: mechanism (a), an explicit insert/relocate-circulation mutation/repair operator that doesn't depend on the search stumbling onto a fail-count tie. Not started — filing as follow-on if this gets picked up; otherwise low priority (fitness fidelity, not search capability, per 94g framing).","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-18T10:12:27Z","created_by":"Bruno Postle","updated_at":"2026-07-23T17:22:14Z","started_at":"2026-07-18T12:17:08Z","closed_at":"2026-07-23T17:22:14Z","close_reason":"A/B measured negative (see notes + DESIGN.md §18); mechanism (a) follow-on filed as homemaker-py-8sh","dependencies":[{"issue_id":"homemaker-py-qi6","depends_on_id":"homemaker-py-94g","type":"discovered-from","created_at":"2026-07-18T11:12:27Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-7fm","title":"Geometry/topology search for shape-intrinsic fails (long-thin useless cells)","description":"The finish-time collapse (94g) proved that a large share of the harbor-house best\nlayout's residual fails are GEOMETRY-INTRINSIC, not label slack: long-thin cells\nthat are useless whatever room usage is assigned. Their width / proportion /\ncrinkliness fails cannot be cleared by any relabelling (94g searches labels only,\nnever geometry) — confirmed by collapse_global clearing only ~2-3 of 15 fails on\nthe best layout, the rest shape- or building-level bound.\n\nGOAL: a search/repair operator that reshapes such cells so the space becomes\nusable — e.g. re-solving a subtree's division ratios, merging a sliver into a\nneighbour, or a targeted division-ratio mutation biased by the offending factor\n(narrowest-width, aspect, crinkliness). Unlike the collapse this MUST move\ngeometry (division ratios / tree shape), and must be evaluated for net fail-count\neffect (a reshape that fixes width may add size elsewhere — same shuffle risk the\n94g threshold objective addressed for labels).\n\nSCOPE: width/proportion/crinkliness fails on inside room cells whose geometry no\nroom type can satisfy. Explicitly NOT relabelling (that is 94g, done). Candidate\nmechanisms: (a) inner-loop solve already optimises ratios — check why it leaves\ndegenerate cells (local optimum? target-dim conflict?); (b) a finish-time\n\"deslim\" operator + re-solve; (c) an operators.py mutation weighted toward high-\naspect leaves. Measure on the same 6 evolved layouts used for the 94g sweep.\n\nSee bd memory collapse-global-94g-and-any-label-usage-optimisation for the\nlabel-vs-geometry boundary. Related: 94g (label collapse), homemaker-py-2g5\n(occlusion/daylight rebuild, which feeds crinkliness).","design":"DONE (negative) — see DESIGN.md §19 for full writeup.\n\nDiagnosis ruled out mechanism (a): re-running the ratio inner loop with 1500\nevals (vs ~80-200 in real search) on the 12-fail collapsed best layout made\nzero difference. Traced two structural causes instead: (1) area starvation\nseveral levels up the tree, (2) cut orientation parallel to the parent's long\naxis. Neither is a ratio problem.\n\nImplemented mutate_shape_rotate + mutate_deslim (operators.py) targeting each\ncause, gated on a new `fit` argument (mirrors the existing `reqs` gate\npattern). Tested as a finish-time exhaustive hill-climb (all 3 rotations +\ndeslim+reinsert per failing cut, keep-better) on the same 6 harbor-house\nlayouts §17 (94g) swept: 0 improving moves found, anywhere, under any\nvariant. Root cause: on a co-evolved layout the cut that makes a leaf thin is\nalso providing some other leaf's adjacency/public-access — straightening it\nelsewhere isn't free. This is §4.2's lesson (partial-objective repair of a\nco-evolved optimum can't win) confirmed for topology repair, not just ratio\nsolving.\n\nCode landed: operators.py (mutate_shape_rotate, mutate_deslim, _shape_failing,\nMUTATIONS/mutate() wiring), tests/test_operators.py (4 new tests + automatic\ncoverage via test_mutations_yield_canonical_genomes). 282 tests pass. Both\noperators are currently unreachable from driver.search/evolve.py (no `fit`\nthreaded through) since the finish-time evaluation found nothing worth\nwiring up further.\n\nOpen question spun out separately: whether these operators help as in-search\nGA moves, where selection pressure across generations might accept a\nlocally-worse move a later step completes — a different regime from\nsingle-step greedy hill-climbing. See follow-up issue.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-18T10:11:43Z","created_by":"Bruno Postle","updated_at":"2026-07-19T10:06:01Z","started_at":"2026-07-19T07:00:32Z","closed_at":"2026-07-19T10:06:01Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-7fm","depends_on_id":"homemaker-py-94g","type":"discovered-from","created_at":"2026-07-18T11:11:42Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-94g","title":"Global cell↔room collapse: generalise 9o5 matching to all spaces (WFC-style)","description":"Generalise the 9o5 superposition/collapse from interchange-classes to a GLOBAL cell↔room assignment: evolution searches unlabelled floorplans (tree shape + circulation/room/outside), a collapse function optimally LABELS each candidate. Mechanically an assignment problem — N cells (computed area/width/proportion/level/graph-pos) ↔ M required rooms (target dims + level + adjacency) — minimising total fail-cost; reuse Fitness._best_assignment (Hungarian/brute) but over the whole leaf set instead of one class. WFC framing = the constructive algorithm: each cell a distribution over types, PROPAGATE hard constraints exactly (level, requires_below service stacks, must-have adjacency) to prune, OBSERVE lowest-entropy cell, collapse to best-fitting room weighted by fit-quality, backtrack on contradiction.\n\nMOTIVATION (harbor-house evolved-3M-nols-3.dom, best layout, 15 fails): ~11 of 15 are LABEL-RELATIVE — 4 size (0/lrrll,0/rllll,1/lrlr,1/lrrll), 3 width (0/rlrrlr,0/rrllrr,0/rrrr), 2 proportion (0/rlrlr,1/rlrlr), 2 wrong-level (me1 L1-\u003e0, r L0-\u003e1). A cell fails size/width/proportion because the room ASSIGNED to it wants dims the cell lacks; relabel to a room it fits and the fail vanishes. Wrong-level is a HARD constraint a level-respecting collapse honours for free. Only 4 are shape-intrinsic and out of scope: crinkliness x2 (0/llll,0/rlllr) + not-connected x2 (level 0/1). Realistic target: 15 -\u003e ~4-6.\n\nWHY IT MAY SUCCEED WHERE 9o5 WAS FALSIFIED (xi7): 9o5 went negative because (1) auto-derived classes were semantically wrong (harbor-house 8-code chain, see homemaker-py-b3v) and (2) collapse perturbed feasibility (ON ADDS fails, 38v33/48v43). A GLOBAL, hard-constraint-respecting collapse sidesteps both: no fragile similarity classes; never violates level/adjacency/stack so it cannot ADD feasibility fails — only improve or match.\n\nRISK: search-landscape flattening. fitness = max-over-labellings makes the objective flatter/noisier (many topologies collapse to similar best scores), removing the gradient evolution climbs — the likely cause of 9o5's negative verdict, AMPLIFIED at full scope. Mitigations to A/B: (a) collapse only at FINISH (search on committed types, one relabel pass at end — cheap, strictly cannot worsen final score); (b) warm-start collapse from evolved labels as a local polish.\n\nRECOMMENDED FIRST STEP (cheapest, ~1 day, strictly cannot worsen final layout): FINISH-TIME global collapse — after a normal run, one optimal cell-\u003eroom assignment over the full leaf set with hard constraints enforced, then re-score. Measures empirically how many of the 11 label-relative fails are real slack vs already-optimal. If it clears a meaningful chunk, justifies the in-search WFC collapse + the landscape-flattening A/B. Does NOT fix crinkliness/connectivity (need geometry + circulation-placement work, file separately).\n\nFiles: fitness.py (_best_assignment, collapse_superposition), programme.py (constraints), driver.py (finish hook), graph.py (adjacency for propagation).","notes":"WIRED + PUBLIC-ACCESS TERM DONE.\n1. Public-access pin (preserve_public_access=True, default): when the building's\n ONLY street access is an l/k ROOM neighbour of a public outside leaf (no\n circulation fallback — the existential building check the per-leaf objective\n can't see), that room leaf is PINNED (kept + its demand slot decremented) so\n the collapse can't drop \"no outside public access\". On the best layout this\n turns 15-\u003e13 into 15-\u003e12 (the lone regression removed, zero new fails). Sweep\n total 172-\u003e171, still monotone across all 6.\n2. Keep-better wrapper Fitness.collapse_finish(root, **kw) -\u003e (tree, base, coll,\n applied): scores on throwaway copies (score_with_fails merges in place),\n returns collapsed only if fails don't increase. Safety belt (config already\n monotone here, not proven so in general).\n3. Wiring: driver.collapse_best(result, programme_dir, ...) updates result.best\n (lineage +collapse, canonical re-score). evolve.py runs it after the sharing\n polish behind --collapse/--no-collapse (default ON). Standalone CLI\n homemaker-collapse file.dom (pyproject entry) writes \u003cstem\u003e.collapsed.dom;\n flags --adjacency/--public-access/--objective/--keep-better. Verified the\n written dom independently re-scores 15-\u003e12.\n4. Tests: tests/test_collapse_global.py (6) — demand-set relabel, level hard\n constraint, c/o/s exclusion, no-op safety, keep-better/unmerged. 267 pass.\n\nSTILL OPEN (separate work, not label slick): geometry-intrinsic fails — long-thin\nuseless cells (width/proportion/crinkliness) and not-connected — need geometry/\ntopology + circulation-placement search, NOT the collapse (see bd memory\ncollapse-global-94g...). In-search WFC collapse A/B also still open (xi7 risk).","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-17T16:17:12Z","created_by":"Bruno Postle","updated_at":"2026-07-18T10:13:43Z","started_at":"2026-07-17T21:07:05Z","closed_at":"2026-07-18T10:13:43Z","close_reason":"FINISH-TIME global cell→room collapse DELIVERED (commits d52cce6, da18ef7, 880a214):\nFitness.collapse_global (c/o/s partition, level hard constraint, adjacency\nrelaxation, threshold objective, public-access pin) + collapse_finish keep-better\nwrapper + driver.collapse_best + evolve --collapse hook + homemaker-collapse CLI +\n6 tests (267 pass). Best harbor-house layout 15→12 fails; monotone across 6 evolved\nlayouts (sweep 195→171). Label search only — cannot fix geometry/building-intrinsic\nfails.\n\nRemaining scope spun out: homemaker-py-7fm (shape-intrinsic reshape), homemaker-py-qi6\n(circulation placement / not-connected), homemaker-py-qpk (in-search WFC collapse\nA/B, xi7 landscape-flattening risk). Closing 94g as the finish-time thrust is done.","dependencies":[{"issue_id":"homemaker-py-94g","depends_on_id":"homemaker-py-b3v","type":"related","created_at":"2026-07-17T17:23:20Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-94g","depends_on_id":"homemaker-py-xi7","type":"related","created_at":"2026-07-17T17:23:06Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -55,6 +58,8 @@ {"id":"homemaker-py-nyb","title":"High-locality topology operators (mutation + subtree crossover)","description":"DESIGN.md §5, §7 Phase 2, §8.4. Mutation moves: divide/undivide leaf, swap children, rotate cut, retype leaf, per-floor delta edits, storey add/delete (cf. Urb Mutate.pm — but geometry sliding belongs to the inner loop, not the operator set). Crossover: area-matched subtree exchange (a subtree = a contiguous region, so crossover is meaningful — Crossover.pm). Operators must be high-locality: small genome change =\u003e small phenotype change, so warm-started inner loops stay cheap.","acceptance_criteria":"Each operator produces valid genomes (oracle scores them without error); locality measured (mean fitness/geometry perturbation per operator)","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:37:27Z","created_by":"Bruno Postle","updated_at":"2026-06-12T13:07:37Z","started_at":"2026-06-12T12:54:23Z","closed_at":"2026-06-12T13:07:37Z","close_reason":"operators.py lands: 7 mutations + area-matched crossover, valid-by-construction via genome.encode repair. 115/115 oracle-valid children; locality measured: geom-pert 0.07-0.33 per op, fitness-pert 0.68-0.99 (0.5^n cliff flags raw moves — warm restart + penalty reshaping confirmed load-bearing). Also fixed dom._link stale below-links on structural mutation.","dependencies":[{"issue_id":"homemaker-py-nyb","depends_on_id":"homemaker-py-k2g","type":"blocks","created_at":"2026-06-12T00:39:36Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"homemaker-py-k2g","title":"Topology genome: base-floor tree + per-floor deltas + type assignment","description":"DESIGN.md §5.2, §7 Phase 2. Genome = base-floor slicing topology (primary) + per-leaf type assignment + per-floor divide/undivide deltas (Below-inheritance as regulariser; cut owned by lowest storey where its path is divided — §10). Must round-trip to/from dom.py Node trees so the oracle and inner loop consume it directly. Includes storey count and per-floor type overrides.","acceptance_criteria":"Genome \u003c-\u003e .dom round-trip on all 35 corpus files preserves fitness; multi-storey wall stacking preserved","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:37:26Z","created_by":"Bruno Postle","updated_at":"2026-06-12T12:52:34Z","started_at":"2026-06-12T10:55:21Z","closed_at":"2026-06-12T12:52:34Z","close_reason":"genome.py encode/decode lands. 35/35 oracle fitness parity after round-trip (flag-on); genome fixed-point + owned-projection tests. Dead-field discovery: corpus upper storeys carry drifted dead divisions (97) and rotations (187) — canonicalised by decode, validated fitness-neutral.","dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"homemaker-py-d0s","title":"Experiment: inner-loop optimiser bake-off at equal oracle budgets","description":"DESIGN.md §7 Phase 1, §8.3. DOF is only ~rooms-1 (6–7 on corpus). Compare Nelder-Mead vs CMA-ES vs batched multi-start pattern search at equal oracle-call budgets, measuring fitness gained per oracle call and wall-clock (batch-friendliness matters — §4.6). Measure, don't commit blind.","acceptance_criteria":"Table of fitness-per-budget across \u003e=3 candidates; one optimiser chosen and recorded in DESIGN.md","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:59Z","created_by":"Bruno Postle","updated_at":"2026-06-13T08:48:13Z","started_at":"2026-06-12T21:22:15Z","closed_at":"2026-06-13T08:48:13Z","close_reason":"Bake-off complete: CMA-ES confirmed as Phase 1/2 optimiser. NM wins quality per eval but sequential architecture incompatible with batching (§4.6). Compass stalls on narrow valleys. Results in DESIGN.md §8.3 and experiments/bakeoff_innerloop.*","dependencies":[{"issue_id":"homemaker-py-d0s","depends_on_id":"homemaker-py-1p0","type":"blocks","created_at":"2026-06-12T00:39:35Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"homemaker-py-2ax","title":"Spike: autodiff/gradient-based inner-loop ratio optimisation","description":"innerloop.py's default ratio optimiser is multi-start Nelder-Mead (nm_search), chosen because it 'outperforms CMA-ES across harbor-house scale' -- both derivative-free, a legacy of the Perl-subprocess oracle era when the fitness function was not differentiable. Fitness is now a native Python port (fitness.py) with geometry (geometry.py) built from ordinary arithmetic (Heron's-formula areas etc.) that is plausibly differentiable end-to-end. Nobody has revisited derivative-based optimisation now that this is possible.\n\nReal risk to test rather than assume: the deliberately-preserved 0.5^n failure-count penalty cliff (DESIGN.md 4.5, kept specifically to protect the inner loop from trading into new failures) is a sharp discontinuity by design, which could make raw gradients unreliable or misleading near failure boundaries.\n\nScope as a SPIKE: implement the ratio-to-fitness path in a differentiable form (JAX or PyTorch, autodiff over cut ratios for a frozen topology), compare convergence speed/quality against nm_search on a handful of frozen topologies from programme-house and harbor-house before deciding whether to invest further. If gradients are usable, the payoff is a much faster inner loop, which frees budget for more topology exploration elsewhere -- but per 12.3's finding that the current residual is NOT search/eval-count bound, this may only pay off in future phases at larger scale, not on the current residual.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-07-25T20:18:27Z","created_by":"Bruno Postle","updated_at":"2026-07-25T20:18:27Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"homemaker-py-c94","title":"Beam/best-first search over adjacency-aware construction decisions","description":"operators._assign_adjacency_aware builds a seed topology via a single greedy pass: greedy connected-dominating-set circulation spine, then constraint-hardest-first room placement onto dominated leaves. Since construction quality is the one lever with a consistent positive track record (11.6, 11.7, 12.2), a width-K beam/best-first search over the SAME construction decisions (which leaf a room lands on, tie-broken today by stochastic order) -- keeping several partial constructions alive and expanding the most promising by a cheap proxy (e.g. operators.predicted_shape_fails or partial fail count) -- may find meaningfully better seeds than one greedy pass per bootstrap individual, without touching the outer topology-search loop at all.\n\nScope: prototype as an alternate constructive_topology path (env-flagged, default off, matching the project's existing A/B convention), measure raw-seed quality (adjacency-to-c / access fail counts, per 11.6's own before/after table) and end-to-end fails at budget on programme-house + harbor-house before considering it as a default.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-07-25T20:18:26Z","created_by":"Bruno Postle","updated_at":"2026-07-25T20:18:26Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-lj3","title":"Try higher _MUTATION_WEIGHTS for bridge_circulation to fire more often (homemaker-py-8sh)","description":"homemaker-py-8sh's A/B (DESIGN.md §21) found two of three harbor-house seeds\nnever even fired mutate_bridge_circulation (byte-identical fitness to 6 sig\nfigs ON vs OFF) within a 2500-eval budget -- at the uniform default weight in\ndriver._MUTATION_WEIGHTS the operator is drawn ~1-in-17 times a mutation\nfires, same priority as cosmetic ops like rotate, despite a 'not connected'\nfail being exactly as fatal to the fail-count-first comparator key as a\nmissing required space (which place_missing already gets a 2.0 weight for).\n\nGOAL: re-run the qi6-protocol A/B (experiments/run_8sh_ab.sh) with\nbridge_circulation's _MUTATION_WEIGHTS entry raised (try 2.0, matching\nplace_missing) to see whether firing it more often increases the\nnot-connected clear rate (currently 2/5) without amplifying the\ntrajectory-divergence noise seen on harbor-house seed 1. Depends on / should\nbe run alongside the larger-N confirmation sweep (homemaker-py-qjg) --\nweight and sample-size are separate variables, ideally tested together in\none combined larger-N sweep with the higher weight, not two separate small\nsweeps.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-24T18:38:40Z","created_by":"Bruno Postle","updated_at":"2026-07-25T09:15:54Z","started_at":"2026-07-24T18:59:06Z","closed_at":"2026-07-25T09:15:54Z","close_reason":"Measured negative (DESIGN.md §22): raising bridge_circulation's _MUTATION_WEIGHTS to 2.0 (matching place_missing) tested together with the qjg larger-N sweep. No total-fail benefit (p=0.71 programme-house N=20, p=0.69 harbor-house N=12) and MORE trajectory-divergence-induced new not-connected fails than at uniform weight (3/20 vs 0/5 at the original small-N weight). Reverted to implicit uniform weight.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-qjg","title":"Larger-N seed sweep to confirm bridge_circulation's mixed A/B (homemaker-py-8sh)","description":"homemaker-py-8sh landed operators.mutate_bridge_circulation (qi6 mechanism (a):\nexplicit repair op that bridges disconnected circulation components) gated\nbehind driver.search's enable_bridge_circulation flag, default off. The\nqi6-protocol A/B (N=3 harbor-house seeds, N=5 programme-house seeds,\nexperiments/run_8sh_ab.sh, DESIGN.md §21) was directionally positive --\ntotal fails never regressed (4 wins/4 ties/0 losses) and 2/5 baseline\nnot-connected fails were cleared (vs 0/4 for qi6's graded-signal mechanism)\n-- but one seed (harbor-house seed 1) saw 2 NEW not-connected fails appear\nalongside its biggest fail-count win, attributed to RNG-trajectory\ndivergence rather than the operator itself (it can only ever convert a leaf\nTO circulation, never away).\n\nGOAL: resolve whether the mean fail-count improvement (harbor 72.0-\u003e69.7,\nprogramme 7.8-\u003e6.8) is a true small positive or small-sample noise, the same\nquestion homemaker-py-1ph answered for collapse_insearch/qpk. Re-run the\nprogramme-house arm (and/or harbor-house) at ~4x the sample (e.g. 20 seeds)\nusing the same protocol (experiments/run_8sh_ab.sh, or a copy), paired\nt-test on per-seed fail-count diffs. If confirmed positive, flip\nenable_bridge_circulation's default to on in driver.py/evolve.py, mirroring\n1ph's flip of collapse_insearch.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-24T18:37:15Z","created_by":"Bruno Postle","updated_at":"2026-07-25T09:16:17Z","started_at":"2026-07-24T18:59:28Z","closed_at":"2026-07-25T09:16:17Z","close_reason":"Measured null (DESIGN.md §22), opposite of 8sh's directional signal: larger-N sweep (programme-house N=20, harbor-house N=12, 1ph protocol) found the 8sh A/B's small positive was small-sample noise, not a true effect -- paired t-test p=0.71 (programme-house) and p=0.69 (harbor-house), both indistinguishable from zero. enable_bridge_circulation stays default OFF.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-1ph","title":"Larger-N seed sweep on programme-house for collapse_insearch default","description":"Follow-on from homemaker-py-qpk (DESIGN.md §20). The collapse_insearch A/B was\nmeasured POSITIVE overall (combined head-to-head ON 6, OFF 2) but kept default\nOFF because the programme-house arm was mixed on a small sample: budget 3000,\nseeds 1-5, ON won 3/5 (mean fails 8.4 -\u003e 7.8, s1 8-\u003e5, s2 8-\u003e7, s4 10-\u003e9 win;\ns3 8-\u003e9, s5 8-\u003e9 loss by one fail). harbor-house was a clean 3/3 win (mean\n80.3 -\u003e 72.0).\n\nGOAL: a larger-N seed sweep on programme-house alone (same budget=3000,\n--collapse-insearch vs baseline, both finished with --collapse) to determine\nwhether the mixed 3/5 result is small-sample noise around a true small\npositive, or a genuine size threshold below which in-search collapse doesn't\npay for its ~1.1-1.9x per-eval cost. If the larger sample confirms a net\npositive on programme-house too, flip collapse_insearch's default from OFF to\nON in evolve.py / driver.py.\n\nNot urgent — qpk's opt-in (--collapse-insearch) is already usable and\ndocumented for harbor-house-scale programmes today; this only decides the\ndefault.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-23T22:12:55Z","created_by":"Bruno Postle","updated_at":"2026-07-24T08:54:46Z","started_at":"2026-07-24T08:26:37Z","closed_at":"2026-07-24T08:54:46Z","close_reason":"Larger-N (20 seed) programme-house sweep confirms POSITIVE: mean fails 7.95-\u003e7.10 (~10.7%), 11W/6L/3T, paired t-test p~0.028. Flipped collapse_insearch default OFF-\u003eON in evolve.py/driver.py. See DESIGN.md section20.","dependencies":[{"issue_id":"homemaker-py-1ph","depends_on_id":"homemaker-py-qpk","type":"discovered-from","created_at":"2026-07-23T23:13:53Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -82,29 +87,30 @@ {"id":"homemaker-py-8fe","title":"Fix Urb programme width default (upstream of homemaker-py-can fix)","description":"The native fitness fix in homemaker-py-can derives a sane width from sqrt(size/proportion) when a programme space has no explicit width. The same bug exists upstream in Perl Urb: Fitness/Base.pm and ProgrammeDriven.pm fall back to width_inside [4.0, 1.0] for any programme space without an explicit width key. Fix the Perl oracle to match the native behaviour (same sqrt(size/proportion) formula).","status":"closed","priority":3,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-13T21:18:19Z","created_by":"Bruno Postle","updated_at":"2026-06-13T22:14:17Z","started_at":"2026-06-13T21:43:33Z","closed_at":"2026-06-13T22:14:17Z","close_reason":"Fixed: get_space_params now derives width from sqrt(size/proportion) when no explicit width key is present. 34/36 corpus files score higher with the fix; all 111 tests pass after rescoring with URB_NO_OCCLUSION=1.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-can","title":"Programme width defaults: t3 contradiction (impossible width_inside default)","description":"DESIGN.md §8.2, confirmed in source. t3 (3 m2 WC) has no width spec so inherits width_inside [4.0, 1.0] (Fitness/Base.pm:60) — geometrically impossible; designs 'pass' only by failing size instead. Fix AFTER faithful-port validation (port-faithfully-first policy, §8.1): a sane width default scaled to area (e.g. sqrt(area/proportion)) or per-room widths in patterns.config. Applies to native fitness; optionally upstream to Urb.","acceptance_criteria":"No programme space has a default width incompatible with its target area; corpus re-scored and effect documented","status":"closed","priority":3,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:39:01Z","created_by":"Bruno Postle","updated_at":"2026-06-13T21:21:37Z","started_at":"2026-06-13T21:16:11Z","closed_at":"2026-06-13T21:21:37Z","close_reason":"Fixed in get_space_params: when a programme space has no explicit 'width', derive target from sqrt(size/proportion) instead of falling back to width_inside [4.0, 1.0]. Re-scored 35-file corpus: 32 files improved (+1-121%), 5 files lost spurious width fails. All 109 tests pass. Upstream Perl fix tracked as homemaker-py-8fe.","dependencies":[{"issue_id":"homemaker-py-can","depends_on_id":"homemaker-py-uxz","type":"blocks","created_at":"2026-06-12T00:39:47Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-yg5","title":"Penalty reshaping: replace 0.5^n while preserving inner-loop protection","description":"DESIGN.md §4.7, §5.4, §7 Phase 4, §8.5. The 0.5^n cliff gives the outer search no gradient and rewards flag-count over geometry, but it also PROTECTS the inner loop from trading into new failures (§4.5). One fitness shape cannot naively be both soft outside and cliff-protected inside. Candidates: cliff-inside-inner-loop only, lexicographic (failure count first, score second), additive/soft, multi-objective Pareto. Must preserve the missing-space failure hierarchy (worse to drop a room than to have a poor one). Measure landscape + search outcomes; this helps Urb today too.","acceptance_criteria":"Chosen scheme documented with measurements: search improves while inner loop still never trades into new failures","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:39:00Z","created_by":"Bruno Postle","updated_at":"2026-06-14T08:16:14Z","started_at":"2026-06-14T07:55:32Z","closed_at":"2026-06-14T08:16:14Z","close_reason":"Implemented lexicographic outer-search comparison (-n_fails, fitness). Inner loop unchanged (0.5^n cliff protection preserved). Experiment penalty_reshape.py confirms 0/9 fail regressions in inner loop and shows lex avoids the 3-fail trap that scalar hits 1/3 of the time. Fixed stale _CHILD_INNER_KW sigmas entry.","dependencies":[{"issue_id":"homemaker-py-yg5","depends_on_id":"homemaker-py-uxz","type":"blocks","created_at":"2026-06-12T00:39:46Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"homemaker-py-1lj","title":"True multi-objective Pareto EA (NSGA-II style) for the outer topology search","description":"DESIGN.md 4.9 explicitly considered 'genuine multi-objective Pareto' as a resolution for the failure-penalty reshaping problem, alongside lexicographic ordering (failure count first, score second) -- lexicographic was chosen and is the current default; true Pareto multi-objective search (maintaining a non-dominated front across e.g. {fail count, cost, value} rather than one scalar/lex key, à la NSGA-II) was never implemented or measured.\n\nLowest priority of the batch: this is another change to outer-loop SEARCH MACHINERY, and every prior change in that category (structural niching+restarts 11.5, graded objective 11.4, Wong-Liu reassociation + shape-feasibility pruning 12.3, construction granularity 12.4, island model 14, in-run grain annealing 16, circulation-repair operators 21/22) has come back null-to-negative. Worth keeping on the board as a genuinely untried technique, but should not be picked up before the higher-conviction construction/assignment-focused items on this board, and should be A/B'd with the same rigour (fixed worker count per 12.4's determinism note, clean baseline control) rather than assumed to help.","status":"open","priority":4,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-07-25T20:18:29Z","created_by":"Bruno Postle","updated_at":"2026-07-25T20:18:29Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-erc.6","title":"Experiment: inner-loop slack-expansion objective term","description":"Inner-loop counterpart to plot-fill construction. If Diagnostic B shows the inner loop has room to expand leaves into slack but no objective gradient to do so (the scalar rewards hitting target area but not exceeding it where slack exists), add a term/incentive so the ratio optimiser pushes leaf boundaries out to consume neighbouring slack and satisfy size, rather than parking at target.\n\nCONDITIONAL on Diagnostic B: build this only if B localizes the gap to the inner loop (room to expand, no gradient); if B shows construction targets too-small dims, prefer the plot-fill construction sibling. Must preserve the §5.4 inner-loop cliff / §4.9 lexicographic protection — the term sits where it cannot displace the fail-count ordering. A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.6.","notes":"DEPRIORITISED by Diagnostic B (§13.2). B shows the inner loop CANNOT repair undersize: the slack is depth-driven maldistribution baked into the frozen topology, and the equal-offset ratio DOF cannot shrink a 14x leaf to feed a starved one without trading into shape fails (0.5^n cliff). Wrong DOF and wrong direction — the blocker is slicing POSITION, not a missing expansion reward. Fix belongs upstream in construction/topology (erc.4 re-scoped, erc.3). Keep as a low-priority follow-up only if a depth-balanced construction still leaves a residual size gradient the inner loop could pick up.","status":"closed","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-22T23:16:24Z","created_by":"Bruno Postle","updated_at":"2026-06-28T13:22:22Z","closed_at":"2026-06-28T13:22:22Z","close_reason":"wont-fix (DESIGN §13.7): Diag B (§13.2) showed the inner loop cannot repair undersize (wrong DOF — slicing position, frozen-topology ratios). Superseded by depth-balanced construction (erc.4). Condition unmet.","dependencies":[{"issue_id":"homemaker-py-erc.6","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T00:16:23Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-erc.6","depends_on_id":"homemaker-py-erc.2","type":"blocks","created_at":"2026-06-23T00:16:47Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-erc.5","title":"Experiment: compactness-aware cuts (minimize leaf perimeter/area)","description":"Attacks the #1 factor, crinkliness (346) — a per-leaf perimeter/area property DISTINCT from proportion (aspect ratio). Proportion-aware seeding (leu.2) sizes splits but does not bias toward balanced, square-ish subdivision. Add a KD-tree-style 'keep both children compact' cut rule (prefer the cut orientation/position that minimises summed child perimeter/area) in construction.\n\nCONDITIONAL on Diagnostic A: if A shows per-leaf shape-fail is FLAT across densities (floor intrinsic to slicing density), better cuts at the same leaf count will not pay → this should be closed wont-fix in favour of leaf-sharing. Only build if A shows shape-fail RISES with density. A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.5.","notes":"DEPRIORITISED by erc.1 verdict (§13.1): per-leaf shape-fail flat vs slicing density and cuts already squarest (_size_divisions_from_targets picks squarest rotation) yet still ~1.8 fails/leaf =\u003e little compactness headroom at fixed leaf count. Floor is intrinsic to leaf COUNT, not cut quality. Revisit only if leaf-sharing (erc.3) underdelivers.","status":"closed","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-22T23:16:21Z","created_by":"Bruno Postle","updated_at":"2026-06-28T13:22:17Z","closed_at":"2026-06-28T13:22:17Z","close_reason":"wont-fix (DESIGN §13.7): Diag A (§13.1) showed the floor is intrinsic to leaf COUNT not cut quality; revisit condition was 'only if leaf-sharing underdelivers' but leaf-sharing OVER-delivered (−32…−39%, §13.3). Condition unmet.","dependencies":[{"issue_id":"homemaker-py-erc.5","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T00:16:21Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-erc.5","depends_on_id":"homemaker-py-erc.1","type":"blocks","created_at":"2026-06-23T00:16:43Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-2g5","title":"Rebuild occlusion/daylight/sun subsystem in Python (post-Phase-5, after optimisation fully native)","description":"DESIGN.md §6 port scope — a whole subsystem, not a term. quality_daylight (Leaf.pm:281-296) needs Urb::Misc::Sun + Urb::Field::Occlusion (+CIESky); quality_uncrinkliness also takes the occlusion object. Indoor spaces return 1 for daylight; cost is outdoor spaces + crinkliness. Port Sun_horizontal (262980-minute normalisation) and the occlusion wall set from Dom-\u003eWalls.","acceptance_criteria":"Daylight and crinkliness factors match Perl (float tolerance) across the corpus, including multi-storey cases","notes":"Re-scoped 2026-06-12: occlusion disabled in the Urb oracle instead of ported (see homemaker-py-gp2). Native fitness ships with simple crinkliness (illumination factor = 1, in homemaker-py-gnw). This issue is now the eventual Python occlusion rebuild, only after optimisation works entirely in Python. Restores outdoor-daylight and shaded-wall selection pressure.\nReframed 2026-06-17: orthogonal to epic homemaker-py-c4c. This is fitness FIDELITY (restoring daylight + shaded-wall selection pressure to match Perl), not search CAPABILITY — it changes what 'good' means, not the search's ability to find good. It will NOT improve final designs in the sense currently sought. Stays P4, deferred until the topology-search-quality epic lands and optimisation is fully native.","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:25Z","created_by":"Bruno Postle","updated_at":"2026-06-17T19:14:48Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"memory","key":"9o5-multi-use-leaves-is-path-a-superposition","value":"9o5 multi-use leaves is path (a) — superposition as SEARCH RELAXATION that COLLAPSES to specific usage at the end, NOT path (b) loose-fit/no-collapse. Bruno's intent: codes with SIMILAR leaf requirements form an interchangeable equivalence class; during evolution the solver doesn't commit which leaf serves which specific usage (smoother landscape, no fighting over exact leaf usage); at the end the layout is CONDENSED to specific usages by brute-forcing the in-class assignment (3 interchangeable usages over 3 leaves = 3! = 6 combinations to check, pick best). 'Derive automatically' compatibility = requirement-similarity grouping. This reverses the issue's stated 'path b preferred' note."} +{"_type":"memory","key":"correction-to-urb-fitness-bug-memory-bruno-2026","value":"CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase."} +{"_type":"memory","key":"homemaker-py-3l6-fix-leaf-sharing-evolve-runs","value":"homemaker-py-3l6 fix: leaf-sharing evolve runs now auto-finish before write via driver.polish_finish — unfold_shared_leaves() then a warm-started leaf_sharing=False polish search (--polish-budget, default budget//2). Makes the written .dom honest under canonical homemaker-fitness (internal==canonical when leaf_sharing off). Interrupt path forces polish_budget=0 (unfold+rescore only). This is yaa's unfold-then-polish, made automatic; Schedule B annealing is still kpu."} +{"_type":"memory","key":"ld2-13-6-interior-o-seed-diagnostic-all","value":"ld2/§13.6 interior-O seed diagnostic: ALL crinkliness fails in the constructed bal+share seed are UNDER-exposed (crink\u003c0.62, landlocked rooms with no facade + no uncovered-O neighbour) — zero over-exposed sliver fails. So the erc crinkliness residual is genuine under-daylighting, validating the interior light-well premise. Default outside_divisor=6 was too sparse (null: harbor 147-\u003e142, crinkliness even rose). odiv=3 is the seed-optimal joint setting: harbor seed fails 147-\u003e129 (-18), maple 219-\u003e206 (-14), landlocked fails drop, at cost of more leaves (harbor +4, maple +8). Because it ADDS leaves it carries the §13.4 wash-out risk; A/B to convergence pending."} +{"_type":"memory","key":"run-to-run-reproducibility-in-homemaker-layout-serial","value":"Run-to-run reproducibility in homemaker-layout: serial search (workers=1) is byte-for-byte deterministic; parallel (workers\u003e1) is now deterministic too AFTER fixing driver._run_batch to admit futures in submission order (was as_completed/completion order, bug xcy). Reproducibility holds only for a FIXED worker count — serial vs parallel differ because children-per-iteration is 1 vs n_workers (different batch granularity), which is expected, not a bug. The constructive seeder was NEVER nondeterministic: _assign_adjacency_aware has unique idx tiebreaks; comparing topologies with Python builtin hash() of the signature STRING is invalid (PYTHONHASHSEED salts str hashing per process) — use a stable hash (sha1) or genome.signature equality."} +{"_type":"memory","key":"strategy-decision-2026-06-12-bruno-occlusion-daylight","value":"Strategy decision 2026-06-12 (Bruno): occlusion/daylight is ORTHOGONAL to building a scalable optimiser. Disable it in Urb (env flag, homemaker-py-gp2) rather than port it; native fitness uses simple crinkliness (illumination factor = 1); rebuild occlusion in Python only after optimisation is fully native (homemaker-py-2g5, now P4). Consequence: all scores change when the flag flips — re-baseline corpus/.score, DESIGN \\$4.5 gains, gate bars at one clean boundary AFTER homemaker-py-1p0 closes; Phase-2 urb-evolve benchmark must run with the same flag."} +{"_type":"memory","key":"never-use-corpus-filenames-candidate-001-dom-candidate","value":"Never use corpus filenames (candidate-001.dom, candidate-002.dom, generated.dom, init.dom, etc.) as --output targets when running experiments. These are test fixtures. Always write experimental outputs to scratch/ or a timestamped path. Lesson from 2026-06-14: warm-start runs overwrote candidate-001/002.dom and broke graph tests."} +{"_type":"memory","key":"adjacency-in-binary-slicing-tree-is-structural-not","value":"Adjacency in binary slicing tree is structural, not geometric: the inner-loop NM cannot fix topological adjacency failures. Two paths exist: (1) tree-sibling adjacency — a node is adjacent to its sibling in the tree; (2) cross-zone geometric adjacency — leaves from different subtrees that happen to share a boundary. Staircase/adjacency fails require a topology mutation that changes which nodes are siblings or which zones touch. This was proved empirically on programme-house: staircase fail from rot=0 layout could not be fixed by NM but was fixed by level_retype creating a two-C topology (2026-06-14/15)."} +{"_type":"memory","key":"experiment-seeding-pitfall-run-search-scaled-py-s","value":"Experiment seeding pitfall: run_search_scaled.py's default PH_SEED (c964…dom) is a FINISHED programme-house design — passing it warm-starts and floors at ~3 fails, NOT a blank-slate topology search. For blank-slate runs comparable to §11.5/§11.6 baselines, seed from examples/programme-house/init.dom (a bare undivided plot; driver bootstrap auto-triggers only on bare plots). Bit the 6zy sweep — first pass used c964 and falsely showed 3-fail floor across the whole grid."} {"_type":"memory","key":"deceptive-valleys-in-topology-search-when-every-single","value":"Deceptive valleys in topology search: when every single-step mutation from a target state passes through a high-fail intermediary (e.g. level_fix displaces a room into 5+ new fails), a compound operator that atomically applies two coordinated changes can escape. Design compound operators to land on the low-fail state directly, bypassing the deceptive gradient. Programme-house example: level_compound_fix atomically moves the level-constrained room AND re-inserts the displaced room adjacent to C in one step (operators.py, 2026-06-14)."} {"_type":"memory","key":"urb-oracle-nondeterminism-urb-fitness-pl-output-varies","value":"Urb oracle nondeterminism: urb-fitness.pl output varies run-to-run from Perl hash-order randomisation — .fails line ORDER shuffles (compare sorted, use oracle.Score.fail_lines) and the score float can flip by ~1 ULP (compare with math.isclose rel_tol=1e-12, never ==). Not a batching artifact; affects single runs too. Matters for the Phase 3 native-fitness parity gate (homemaker-py-uxz)."} -{"_type":"memory","key":"ld2-13-6-interior-o-seed-diagnostic-all","value":"ld2/§13.6 interior-O seed diagnostic: ALL crinkliness fails in the constructed bal+share seed are UNDER-exposed (crink\u003c0.62, landlocked rooms with no facade + no uncovered-O neighbour) — zero over-exposed sliver fails. So the erc crinkliness residual is genuine under-daylighting, validating the interior light-well premise. Default outside_divisor=6 was too sparse (null: harbor 147-\u003e142, crinkliness even rose). odiv=3 is the seed-optimal joint setting: harbor seed fails 147-\u003e129 (-18), maple 219-\u003e206 (-14), landlocked fails drop, at cost of more leaves (harbor +4, maple +8). Because it ADDS leaves it carries the §13.4 wash-out risk; A/B to convergence pending."} -{"_type":"memory","key":"never-use-corpus-filenames-candidate-001-dom-candidate","value":"Never use corpus filenames (candidate-001.dom, candidate-002.dom, generated.dom, init.dom, etc.) as --output targets when running experiments. These are test fixtures. Always write experimental outputs to scratch/ or a timestamped path. Lesson from 2026-06-14: warm-start runs overwrote candidate-001/002.dom and broke graph tests."} -{"_type":"memory","key":"urb-fitness-bug-found-fixed-2026-06-12","value":"Urb fitness bug found+fixed 2026-06-12 (patch in /home/bruno/src/urb, uncommitted): ProgrammeDriven.pm ratio_o/ratio_type grepped case-insensitively over the ratios hash and took the FIRST key — nondeterministic (x4.5 score swings) for designs with mixed-case type classes (both 'c' circulation and 'C' covered). Fixed to SUM the class (matches Is_Circulation//Is_Outside semantics); 35/35 corpus scores unchanged. CRITICAL for homemaker-py-3y7/gnw: the native port must implement class-SUM ratios. Building.pm has the same unpatched pattern (site-driven path, not used by our oracle). Also: the memetic search reward-hacked this bug before the fix — search results predating it are noise artifacts."} -{"_type":"memory","key":"user-preference-bruno-this-is-a-fedora-system","value":"User preference (Bruno): this is a Fedora system — NEVER install Python packages via pip without asking first; always ask whether to install the rpm via dnf (e.g. python3-cma) before considering pip. Applies to any dependency additions."} -{"_type":"memory","key":"correction-to-urb-fitness-bug-memory-bruno-2026","value":"CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase."} -{"_type":"memory","key":"experiment-seeding-pitfall-run-search-scaled-py-s","value":"Experiment seeding pitfall: run_search_scaled.py's default PH_SEED (c964…dom) is a FINISHED programme-house design — passing it warm-starts and floors at ~3 fails, NOT a blank-slate topology search. For blank-slate runs comparable to §11.5/§11.6 baselines, seed from examples/programme-house/init.dom (a bare undivided plot; driver bootstrap auto-triggers only on bare plots). Bit the 6zy sweep — first pass used c964 and falsely showed 3-fail floor across the whole grid."} -{"_type":"memory","key":"homemaker-py-3l6-fix-leaf-sharing-evolve-runs","value":"homemaker-py-3l6 fix: leaf-sharing evolve runs now auto-finish before write via driver.polish_finish — unfold_shared_leaves() then a warm-started leaf_sharing=False polish search (--polish-budget, default budget//2). Makes the written .dom honest under canonical homemaker-fitness (internal==canonical when leaf_sharing off). Interrupt path forces polish_budget=0 (unfold+rescore only). This is yaa's unfold-then-polish, made automatic; Schedule B annealing is still kpu."} -{"_type":"memory","key":"strategy-decision-2026-06-12-bruno-occlusion-daylight","value":"Strategy decision 2026-06-12 (Bruno): occlusion/daylight is ORTHOGONAL to building a scalable optimiser. Disable it in Urb (env flag, homemaker-py-gp2) rather than port it; native fitness uses simple crinkliness (illumination factor = 1); rebuild occlusion in Python only after optimisation is fully native (homemaker-py-2g5, now P4). Consequence: all scores change when the flag flips — re-baseline corpus/.score, DESIGN \\$4.5 gains, gate bars at one clean boundary AFTER homemaker-py-1p0 closes; Phase-2 urb-evolve benchmark must run with the same flag."} {"_type":"memory","key":"experiment-harness-gotcha-the-leaf-sharing-relaxed-objective","value":"Experiment harness gotcha: the leaf-sharing RELAXED objective (§13.3) is injected ONLY by monkeypatching fitness.load_config in the parent process (run_staged_search.py / probe scripts). This is parent-process-only and does NOT propagate into ProcessPoolExecutor workers (n_workers\u003e1), which re-import fitness fresh and score under the STRICT on-disk patterns.config -\u003e r.n_fails MISMATCH (worker strict vs parent relaxed re-score). ALL §13.x floor runs were therefore SERIAL. Any future PARALLEL leaf-sharing experiment will silently mis-score until leaf_sharing lives on disk/CLI (tracked: homemaker-py-x3b). The parallel driver itself is correct; both paths score via load_config(programme_dir)."} -{"_type":"memory","key":"programme-house-optimisation-result-2026-06-14-15","value":"Programme-house optimisation result (2026-06-14/15): best achievable is 1 fail (l1 wrong level, score ~0.005). 0 fails is geometrically impossible: l1 (min 27m²) must occupy ll (~23m²) at level 0, which eliminates the t3-adj-C provider; dividing ll into lll(l1)+llr(C) gives llr proportion ~6:1 (fails). Python memetic optimizer achieves 1 fail in 50k evals vs Perl optimiser's 2-3 fails. Winning topology: TWO C nodes at level 0 — ll(C) for t3-adj-C via geometric contact, rl(C) for staircase via tree-sibling adjacency to rrr(O). Best .dom: scratch/from-warmstart-fixed.dom and scratch/from-compound3-fixed.dom."} -{"_type":"memory","key":"adjacency-in-binary-slicing-tree-is-structural-not","value":"Adjacency in binary slicing tree is structural, not geometric: the inner-loop NM cannot fix topological adjacency failures. Two paths exist: (1) tree-sibling adjacency — a node is adjacent to its sibling in the tree; (2) cross-zone geometric adjacency — leaves from different subtrees that happen to share a boundary. Staircase/adjacency fails require a topology mutation that changes which nodes are siblings or which zones touch. This was proved empirically on programme-house: staircase fail from rot=0 layout could not be fixed by NM but was fixed by level_retype creating a two-C topology (2026-06-14/15)."} -{"_type":"memory","key":"collapse-global-94g-and-any-label-usage-optimisation","value":"collapse_global (94g) and any label/usage optimisation CANNOT fix geometry-intrinsic fails. The harbor-house 15-fail best layout contains long-thin cells that are useless whatever room usage is assigned — their width/proportion/crinkliness fails are shape-bound, not label slack. Two consequences: (1) do not over-claim collapse gains — only ~2-3 of that layout's fails are reclaimable relabel slack, the rest are geometry- or building-level bound; (2) the threshold objective must not be tuned to 'pass' a degenerate cell via a permissive room type — a metric-pass on a physically useless space is gaming, not a fix. Real remedies for these are geometry/topology search (cell shape) and circulation placement, filed separately, not the collapse."} -{"_type":"memory","key":"run-to-run-reproducibility-in-homemaker-layout-serial","value":"Run-to-run reproducibility in homemaker-layout: serial search (workers=1) is byte-for-byte deterministic; parallel (workers\u003e1) is now deterministic too AFTER fixing driver._run_batch to admit futures in submission order (was as_completed/completion order, bug xcy). Reproducibility holds only for a FIXED worker count — serial vs parallel differ because children-per-iteration is 1 vs n_workers (different batch granularity), which is expected, not a bug. The constructive seeder was NEVER nondeterministic: _assign_adjacency_aware has unique idx tiebreaks; comparing topologies with Python builtin hash() of the signature STRING is invalid (PYTHONHASHSEED salts str hashing per process) — use a stable hash (sha1) or genome.signature equality."} +{"_type":"memory","key":"homemaker-py-pythonpath-set-pythonpath-home-bruno-src","value":"homemaker-layout PYTHONPATH: package installed as 'homemaker-layout' via pip install -e . so 'import homemaker_layout' works from anywhere without PYTHONPATH. For running tests use 'python -m pytest' from project root /home/bruno/src/homemaker-layout (pyproject.toml adds src/ automatically). Never try pip show homemaker — that's the old homemaker-addon conflict."} {"_type":"memory","key":"warm-x0-initialization-bug-pattern-when-a-topology","value":"warm_x0 initialization bug pattern: when a topology operator explicitly sets division ratios on a newly-created node (e.g. compound_fix sets node.division=[0.25,0.25] for t3), parent.ratios has no entry for that node (it was a leaf). warm_x0 defaults it to 0.5, corrupting the inner loop's starting point and making the operator invisible to lex comparison. Fix: only propagate child ratios for nodes where the parent node was NOT already divided; stale hidden nodes revealed by structural mutations (swap flipping b.below) must NOT contribute their pre-writeback values. See driver.py lines 259-267 (fixed 2026-06-14)."} {"_type":"memory","key":"island-model-psk-14-is-a-null-priming","value":"Island model (psk, §14) is a NULL: priming a population from N converged independent elites + crossover-heavy migration does not beat best-of-N at equal total budget (maple island 124 vs control 116). The child_probe instrument shows WHY: area-matched crossover across independently-converged elites almost never synthesizes (1-3 of ~64 children beat the better parent, max drop 2-5) because the slicing encoding is non-canonical (9gp), so splices are disruptive not combinatorial. Search-machinery null #3 after graded-objective and niching/restarts; residual stays geometry/shape-bound."} -{"_type":"memory","key":"proportion-aware-constructive-seeding-leu-2-12-2","value":"Proportion-aware constructive seeding (leu.2/§12.2): sizing seed cuts from target AREAS only regresses (thin slivers wreck aspect); you must ALSO pick each cut's rotation for child squareness. It is a convergence ACCELERATOR via a deeper local optimum around the constructed topology: wins where that topology is roughly right and budget is scarce (harbor -13%, maple -10% at 20k evals) but DELAYS small programmes where the seed must be restructured by undivide (programme-house regresses at fixed budget, yet reaches the floor given budget - speed, not asymptote). Default-on. Also: n_storeys must honour storey_minimum, not just level: keys (programme-house storey_minimum:2, all rooms level:0 - was seeded 1 storey short; cq1)."} -{"_type":"memory","key":"homemaker-py-pythonpath-set-pythonpath-home-bruno-src","value":"homemaker-layout PYTHONPATH: package installed as 'homemaker-layout' via pip install -e . so 'import homemaker_layout' works from anywhere without PYTHONPATH. For running tests use 'python -m pytest' from project root /home/bruno/src/homemaker-layout (pyproject.toml adds src/ automatically). Never try pip show homemaker — that's the old homemaker-addon conflict."} +{"_type":"memory","key":"programme-house-optimisation-result-2026-06-14-15","value":"Programme-house optimisation result (2026-06-14/15): best achievable is 1 fail (l1 wrong level, score ~0.005). 0 fails is geometrically impossible: l1 (min 27m²) must occupy ll (~23m²) at level 0, which eliminates the t3-adj-C provider; dividing ll into lll(l1)+llr(C) gives llr proportion ~6:1 (fails). Python memetic optimizer achieves 1 fail in 50k evals vs Perl optimiser's 2-3 fails. Winning topology: TWO C nodes at level 0 — ll(C) for t3-adj-C via geometric contact, rl(C) for staircase via tree-sibling adjacency to rrr(O). Best .dom: scratch/from-warmstart-fixed.dom and scratch/from-compound3-fixed.dom."} {"_type":"memory","key":"unfold-strategy-for-shared-leaves-homemaker-py-8iv","value":"Unfold strategy for shared leaves (homemaker-py-8iv, resolved 2026-07-16): use the BALANCED GRID (operators._grow_balanced/_size_subtree_equal), NOT circulation-aware slicing. Slicing a shared leaf perpendicular to its access edge so every child touches the corridor was implemented + A/B-tested and LOST decisively (150k-eval warm-start polish from evolved-3M: slice 41 fails/3.5e-14 vs grid 25 fails/2.4e-09, grid ahead at every milestone). Reason: k rooms all touching one wall are intrinsically thin slices; that geometric debt (proportion/long/width) is unfixable without topology change, while the grid's squarer children let local search re-route access cheaply via level_retype/place_missing/level_fix. Lesson: at the sharing-\u003eno-sharing transition, prioritise squarer children and leave access to local search; do not reintroduce slicing in Schedule B (kpu)."} -{"_type":"memory","key":"cli-tool-style-prefer-python-m-homemaker-module","value":"CLI tool style: prefer python -m homemaker.module --parameters pattern, installable via pip install -e . with pyproject.toml entry_points. Not standalone bin/ scripts."} +{"_type":"memory","key":"user-preference-bruno-this-is-a-fedora-system","value":"User preference (Bruno): this is a Fedora system — NEVER install Python packages via pip without asking first; always ask whether to install the rpm via dnf (e.g. python3-cma) before considering pip. Applies to any dependency additions."} {"_type":"memory","key":"multi-storey-staircase-consistency-when-dividing-or-retyping","value":"Multi-storey staircase consistency: when dividing or retyping a circulation (C) leaf at one level, the same structural change should be propagated to the matching leaf on ALL other storeys so the stair core path is maintained. The optimizer cannot fix staircase disruptions through trial-and-error geometry alone — it requires a synchronized multi-level operator that applies the same topology change to every storey simultaneously."} +{"_type":"memory","key":"proportion-aware-constructive-seeding-leu-2-12-2","value":"Proportion-aware constructive seeding (leu.2/§12.2): sizing seed cuts from target AREAS only regresses (thin slivers wreck aspect); you must ALSO pick each cut's rotation for child squareness. It is a convergence ACCELERATOR via a deeper local optimum around the constructed topology: wins where that topology is roughly right and budget is scarce (harbor -13%, maple -10% at 20k evals) but DELAYS small programmes where the seed must be restructured by undivide (programme-house regresses at fixed budget, yet reaches the floor given budget - speed, not asymptote). Default-on. Also: n_storeys must honour storey_minimum, not just level: keys (programme-house storey_minimum:2, all rooms level:0 - was seeded 1 storey short; cq1)."} +{"_type":"memory","key":"urb-fitness-bug-found-fixed-2026-06-12","value":"Urb fitness bug found+fixed 2026-06-12 (patch in /home/bruno/src/urb, uncommitted): ProgrammeDriven.pm ratio_o/ratio_type grepped case-insensitively over the ratios hash and took the FIRST key — nondeterministic (x4.5 score swings) for designs with mixed-case type classes (both 'c' circulation and 'C' covered). Fixed to SUM the class (matches Is_Circulation//Is_Outside semantics); 35/35 corpus scores unchanged. CRITICAL for homemaker-py-3y7/gnw: the native port must implement class-SUM ratios. Building.pm has the same unpatched pattern (site-driven path, not used by our oracle). Also: the memetic search reward-hacked this bug before the fix — search results predating it are noise artifacts."} +{"_type":"memory","key":"cli-tool-style-prefer-python-m-homemaker-module","value":"CLI tool style: prefer python -m homemaker.module --parameters pattern, installable via pip install -e . with pyproject.toml entry_points. Not standalone bin/ scripts."} +{"_type":"memory","key":"collapse-global-94g-and-any-label-usage-optimisation","value":"collapse_global (94g) and any label/usage optimisation CANNOT fix geometry-intrinsic fails. The harbor-house 15-fail best layout contains long-thin cells that are useless whatever room usage is assigned — their width/proportion/crinkliness fails are shape-bound, not label slack. Two consequences: (1) do not over-claim collapse gains — only ~2-3 of that layout's fails are reclaimable relabel slack, the rest are geometry- or building-level bound; (2) the threshold objective must not be tuned to 'pass' a degenerate cell via a permissive room type — a metric-pass on a physically useless space is gaming, not a fix. Real remedies for these are geometry/topology search (cell shape) and circulation placement, filed separately, not the collapse."} diff --git a/DESIGN.md b/DESIGN.md index 0ffad49..a5cf4f7 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -2801,3 +2801,97 @@ uniform weight (not present in `_MUTATION_WEIGHTS`), matching pre-`lj3` behaviou (`--bridge-circulation`/`HOMEMAKER_BRIDGE_CIRCULATION`) for anyone who wants the connectivity- targeting behaviour despite the neutral aggregate measurement, but is not a candidate for a default flip on the current evidence. + +## 23. Ruin-and-recreate LNS: rebuild a wing with the adjacency-aware constructor (`homemaker-py-f1d`) — DONE (positive, size-dependent) + +**Motivation.** DESIGN.md's own experiment log by this point is one-sided: every "search machinery" +change tried (§11.5 niching+restarts, §11.4 graded objective, §12.3 Wong-Liu reassociation + +shape-feasibility, §12.4 granularity, §14 island model, §16 grain annealing, §18 graded +connectivity, §19 shape repair, §21/§22 circulation-repair ops) has come back null-to-negative, +while construction/seeding QUALITY (§11.6/§11.7 adjacency-aware seeding, §12.2 proportion-aware +seeding) is the only lever that has ever moved the fail count. `operators._assign_adjacency_aware` +— the constructor behind both `constructive_topology` and `lift_base_to_storeys` — currently only +ever runs once, at seeding. The proposal: reuse it repeatedly DURING search as a large-neighbourhood- +search (LNS) ruin-and-recreate move, betting that the one technique with a real track record +generalises better than another new comparator-key or population-management idea. + +**Mechanism (build).** `operators.mutate_ruin_recreate`: pick a divided, live-cut subtree ("wing") +of one storey holding a genuine partial neighbourhood of that storey's leaves (>=2, <= half — not a +single-leaf relabel already covered by `retype`/`swap`, not a whole-floor rebuild already covered by +the initial seed), un-divide it back to one leaf, then regrow and retype it with +`_assign_adjacency_aware`, seeded (`fixed_circ`) from whichever already-typed circulation leaves +border the wing — the same mechanism `lift_base_to_storeys` uses to grow an upper storey off an +inherited core (§11.7), so the rebuilt interior spine reconnects to the surviving one instead of +growing a disconnected island. The wing's required-space room-code budget is preserved exactly +(same multiset); only its internal circulation/outside counts and split are rebuilt, at the same +circ_divisor=3/outside_divisor=3 ratio the constructive seeders default to (not threaded from the +run config — kept parameter-light, like `bridge_circulation`). + +`_assign_adjacency_aware` gained a new `scope` parameter (leaves eligible for retyping; `fixed_circ` +may then name border leaves OUTSIDE `scope` as dominating-set seeds only, never retyped) so the wing +rebuild can share the exact constructor code without touching the rest of the storey. `scope=None` +(every existing caller) reproduces the prior unrestricted behaviour exactly — verified no other +caller's output changed. Gated like `reassociate`/`bridge_circulation`: zero mutation weight unless +`enable_ruin_recreate=True` (`driver.search`/`search_staged`, `evolve.py +--ruin-recreate`/`HOMEMAKER_RUIN_RECREATE`, default off). + +**Verified (build-time).** 200 applications of `mutate_ruin_recreate` chained onto fresh +`constructive_topology` harbor-house seeds (40 seeds × 5 steps): zero missing-space regressions +(`graph.check_space_counts`), every child a canonical genome (`encode(decode(encode(x))) == encode(x)`). +297 existing tests pass unchanged (the new op is exercised by the existing +`test_mutations_yield_canonical_genomes` parametrization, which calls it with `reqs=None` and gets +the documented noop). A `child_probe`-instrumented `driver.search` run confirmed the operator is +actually selected by `mutate()` at its configured weight (not dead code). + +**Initial A/B (measured, 2026-07-25/26, qpk protocol) — NULL, but underpowered.** Equal-budget +`enable_ruin_recreate` ON (implicit uniform mutation weight, ~7.5% draw probability among ~13 active +ops) vs OFF, both arms finished with the standard finish-time `--collapse` (94g), 4 workers: + +- **harbor-house** (budget 2500, seeds 1–3): 1 loss (74→81), 2 ties. +- **programme-house** (budget 3000, seeds 1–5): 4 ties, 1 win (9→8). +- **Combined: 1 win / 1 loss / 6 ties out of 8**, mean fails 31.9 (OFF) → 32.6 (ON) — indistinguishable + from zero, in the same direction as most of this log's other null results. +- A direct `child_probe` instrumentation of one of the tied harbor-house runs found + `ruin_recreate` fired **once in 32 children** — the initial sample is dominated by trajectories + where the operator simply never got a turn, not by turns it lost. Six of the eight exact ties + (fitness scalar identical to 6 significant figures, not just fail count) are consistent with + this: the op's rare draws mostly didn't survive tournament selection into the recorded lineage. + +**Weight follow-up (measured, 2026-07-26) — reran the ON arm only** with +`_MUTATION_WEIGHTS["ruin_recreate"] = 3.0` (matching `place_missing`, mirroring the `lj3` weight-bump +precedent) at the same seeds/budgets, directly comparable to the existing OFF baseline: + +- **programme-house** (seeds 1–5): **4 wins, 1 tie, 0 losses** — 7→1, 9→7, 9→8, 9→7, 5→5. A striking, + one-sided result, including one seed dropping from 7 fails to 1 (verified deterministic on rerun). +- **harbor-house** (seeds 1–3): 1 win (77→73), 1 loss (74→82), 1 tie — still mixed. + +**Larger-N confirmation (measured, 2026-07-26)** — extended both arms to 10 fresh programme-house +seeds (6–15) and 5 fresh harbor-house seeds (4–8) at the same weight=3.0, same protocol: + +- **programme-house, all 15 seeds combined: 8 wins / 1 loss / 6 ties.** Mean fails **7.07 (OFF) → + 6.00 (ON)**, a ~15% reduction. Wilcoxon signed-rank p≈0.041; sign-test p≈0.020 (one-sided) — holds + up at conventional significance, not small-sample noise around zero (the 8sh/1ph/qi6/lj3 pattern + this log warns about). +- **harbor-house, all 8 seeds combined: 3 wins / 2 losses / 3 ties.** Mean fails **73.0 (OFF) → 74.5 + (ON)** — no consistent effect, if anything a very slight negative lean, echoing §20's + (`collapse_insearch`) opposite-direction size split but with the SMALLER building this time as + the one that benefits. + +**Interpretation.** A rare case in this log where a search-machinery idea shows a real, +statistically-supported effect — but only on the smaller/simpler example programme. Plausible +reading: programme-house's smaller room count means a wing rebuild samples a much larger fraction of +the whole floor's topology per move (higher effective locality-vs-scope ratio), so the constructor's +proven adjacency-aware placement quality dominates; harbor-house's much larger room count means the +same wing size is a small, noisier perturbation relative to the whole building, and correlates with +the ~2× per-op cost of `_assign_adjacency_aware` (leaf-graph rebuild + dominating-set search) not +translating into more useful search steps within the same eval budget on that scale. + +**Status (2026-07-26).** `enable_ruin_recreate` stays **default OFF** — harbor-house shows no +benefit and the two example programmes disagree on direction, so flipping the global default is not +supported by this evidence (same conservative bar §20 applied before its own larger-N confirmation). +`_MUTATION_WEIGHTS["ruin_recreate"] = 3.0` is kept in the source (only takes effect when the flag is +on) since it is the validated-effective setting. `--ruin-recreate`/`HOMEMAKER_RUIN_RECREATE` is +documented and ready to use today on programme-house-scale (smaller/simpler) programmes; a natural +follow-up (not filed, low priority) would be a third or fourth example programme at a size between +the two tested here, to locate the size threshold this result implies rather than inferring it from +just two data points. diff --git a/experiments/run_f1d_ab.sh b/experiments/run_f1d_ab.sh new file mode 100755 index 0000000..b47ac02 --- /dev/null +++ b/experiments/run_f1d_ab.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# f1d A/B: does the ruin-and-recreate LNS move (un-divide one wing of a +# storey, rebuild it with the adjacency-aware constructor seeded from the +# surviving circulation bordering the wing) reduce the fail count relative +# to the current baseline (small local mutation operators only)? Same qpk +# protocol as 8sh/qi6: equal-budget ON vs OFF, both arms finished with the +# standard finish-time --collapse (94g) so the comparison is apples-to-apples +# on the final collapsed score. +# +# Authoritative metrics: total fail count read from the .fails file +# homemaker-fitness writes. Each run appends one TSV row so partial results +# survive an interrupt. +# +# Usage: experiments/run_f1d_ab.sh +set -u +cd "$(dirname "$0")/.." + +WORKERS=4 +OUT=scratch/f1d_ab; mkdir -p "$OUT" +TSV=scratch/f1d_ab_results.tsv +[ -f "$TSV" ] || printf 'programme\tseed\truin\tbudget\tfails\tfitness\telapsed_s\n' > "$TSV" + +run() { # programme seed ruin(0|1) budget + local prog="$1" seed="$2" rr="$3" budget="$4" + local tag="rr${rr}" + local dom="$OUT/${prog}_${tag}_s${seed}.dom" + local log="$OUT/${prog}_${tag}_s${seed}.log" + local flag="--no-ruin-recreate"; [ "$rr" = 1 ] && flag="--ruin-recreate" + echo ">>> $prog seed=$seed ruin_recreate=$rr budget=$budget" + local t0; t0=$(date +%s) + homemaker-evolve "examples/$prog/init.dom" \ + --budget "$budget" --workers "$WORKERS" --seed "$seed" \ + $flag --output "$dom" > "$log" 2>&1 + local t1; t1=$(date +%s) + local fitness fails + fitness=$(sed -n 's/^best *: \([0-9.e+-]*\) .*/\1/p' "$log") + fails=$(sed -n 's/^best *: [0-9.e+-]* (\([0-9]*\) fails).*/\1/p' "$log") + ( cd "examples/$prog" && homemaker-fitness "$(realpath "../../$dom")" > /dev/null 2>&1 ) + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$prog" "$seed" "$rr" "$budget" "${fails:-ERR}" "${fitness:-ERR}" "$((t1-t0))" >> "$TSV" + echo " -> ${fails:-ERR} fails, fitness=${fitness:-ERR}, $((t1-t0))s" +} + +# harbor-house: budget 2500, seeds 1-3 (qpk protocol) +for seed in 1 2 3; do run harbor-house "$seed" 0 2500; done +for seed in 1 2 3; do run harbor-house "$seed" 1 2500; done + +# programme-house: budget 3000, seeds 1-5 (qpk protocol) +for seed in 1 2 3 4 5; do run programme-house "$seed" 0 3000; done +for seed in 1 2 3 4 5; do run programme-house "$seed" 1 3000; done + +echo "=== f1d ruin_recreate A/B complete ===" +column -t -s $'\t' "$TSV" diff --git a/experiments/run_f1d_larger_n.sh b/experiments/run_f1d_larger_n.sh new file mode 100755 index 0000000..15dfa7e --- /dev/null +++ b/experiments/run_f1d_larger_n.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# f1d larger-N confirmation: the weight=3.0 follow-up (run_f1d_w3_ab.sh) +# showed a striking, consistent programme-house improvement (4 wins + 1 tie, +# 0 losses, mean fails 8.6 -> 5.75 across seeds 1-5) but a mixed harbor-house +# result (1 win, 1 tie, 1 loss). Extends BOTH arms to seeds 6-15 on +# programme-house and 4-8 on harbor-house so the N=5/N=3 initial reads are +# confirmed or falsified on fresh seeds, mirroring the project's own +# 1ph/qjg larger-N-confirmation pattern. driver._MUTATION_WEIGHTS still +# carries the temporary "ruin_recreate": 3.0 from the w3 follow-up. +# +# Usage: experiments/run_f1d_larger_n.sh +set -u +cd "$(dirname "$0")/.." + +WORKERS=4 +OUT=scratch/f1d_ln; mkdir -p "$OUT" +TSV=scratch/f1d_ln_results.tsv +[ -f "$TSV" ] || printf 'programme\tseed\truin\tbudget\tfails\tfitness\telapsed_s\n' > "$TSV" + +run() { # programme seed ruin(0|1) budget + local prog="$1" seed="$2" rr="$3" budget="$4" + local tag="rr${rr}" + local dom="$OUT/${prog}_${tag}_s${seed}.dom" + local log="$OUT/${prog}_${tag}_s${seed}.log" + local flag="--no-ruin-recreate"; [ "$rr" = 1 ] && flag="--ruin-recreate" + echo ">>> $prog seed=$seed ruin_recreate=$rr budget=$budget" + local t0; t0=$(date +%s) + homemaker-evolve "examples/$prog/init.dom" \ + --budget "$budget" --workers "$WORKERS" --seed "$seed" \ + $flag --output "$dom" > "$log" 2>&1 + local t1; t1=$(date +%s) + local fitness fails + fitness=$(sed -n 's/^best *: \([0-9.e+-]*\) .*/\1/p' "$log") + fails=$(sed -n 's/^best *: [0-9.e+-]* (\([0-9]*\) fails).*/\1/p' "$log") + ( cd "examples/$prog" && homemaker-fitness "$(realpath "../../$dom")" > /dev/null 2>&1 ) + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$prog" "$seed" "$rr" "$budget" "${fails:-ERR}" "${fitness:-ERR}" "$((t1-t0))" >> "$TSV" + echo " -> ${fails:-ERR} fails, fitness=${fitness:-ERR}, $((t1-t0))s" +} + +# programme-house: seeds 6-15, both arms +for seed in $(seq 6 15); do run programme-house "$seed" 0 3000; done +for seed in $(seq 6 15); do run programme-house "$seed" 1 3000; done + +# harbor-house: seeds 4-8, both arms +for seed in $(seq 4 8); do run harbor-house "$seed" 0 2500; done +for seed in $(seq 4 8); do run harbor-house "$seed" 1 2500; done + +echo "=== f1d larger-N confirmation complete ===" +column -t -s $'\t' "$TSV" diff --git a/experiments/run_f1d_w3_ab.sh b/experiments/run_f1d_w3_ab.sh new file mode 100755 index 0000000..7b3950a --- /dev/null +++ b/experiments/run_f1d_w3_ab.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# f1d weight follow-up: the initial run_f1d_ab.sh A/B (uniform uneabled weight, +# ~7.5% activation among active ops) came back essentially null (1 win, 1 +# loss, 6 ties out of 8 paired seeds) with a directly-instrumented run showing +# ruin_recreate fired only ~1/32 children -- likely underpowered rather than +# a clean negative. driver._MUTATION_WEIGHTS now carries a temporary +# "ruin_recreate": 3.0 (matching place_missing's weight, mirroring the lj3 +# weight-bump precedent) to raise the activation rate; this reruns the ON arm +# only at the SAME seeds/budgets as run_f1d_ab.sh so it is directly +# comparable against the existing rr=0 baseline rows in +# scratch/f1d_ab_results.tsv. +# +# Usage: experiments/run_f1d_w3_ab.sh +set -u +cd "$(dirname "$0")/.." + +WORKERS=4 +OUT=scratch/f1d_w3_ab; mkdir -p "$OUT" +TSV=scratch/f1d_w3_ab_results.tsv +[ -f "$TSV" ] || printf 'programme\tseed\truin\tweight\tbudget\tfails\tfitness\telapsed_s\n' > "$TSV" + +run() { # programme seed budget + local prog="$1" seed="$2" budget="$3" + local dom="$OUT/${prog}_rr1w3_s${seed}.dom" + local log="$OUT/${prog}_rr1w3_s${seed}.log" + echo ">>> $prog seed=$seed ruin_recreate=1 weight=3.0 budget=$budget" + local t0; t0=$(date +%s) + homemaker-evolve "examples/$prog/init.dom" \ + --budget "$budget" --workers "$WORKERS" --seed "$seed" \ + --ruin-recreate --output "$dom" > "$log" 2>&1 + local t1; t1=$(date +%s) + local fitness fails + fitness=$(sed -n 's/^best *: \([0-9.e+-]*\) .*/\1/p' "$log") + fails=$(sed -n 's/^best *: [0-9.e+-]* (\([0-9]*\) fails).*/\1/p' "$log") + ( cd "examples/$prog" && homemaker-fitness "$(realpath "../../$dom")" > /dev/null 2>&1 ) + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$prog" "$seed" "1" "3.0" "$budget" "${fails:-ERR}" "${fitness:-ERR}" "$((t1-t0))" >> "$TSV" + echo " -> ${fails:-ERR} fails, fitness=${fitness:-ERR}, $((t1-t0))s" +} + +for seed in 1 2 3; do run harbor-house "$seed" 2500; done +for seed in 1 2 3 4 5; do run programme-house "$seed" 3000; done + +echo "=== f1d weight=3.0 follow-up complete ===" +column -t -s $'\t' "$TSV" diff --git a/src/homemaker_layout/driver.py b/src/homemaker_layout/driver.py index 76e8d9e..2066faa 100644 --- a/src/homemaker_layout/driver.py +++ b/src/homemaker_layout/driver.py @@ -103,7 +103,8 @@ def _reqs_for(programme_dir: str) -> dict: # (homemaker-py-qjg, DESIGN.md §22) found no total-fail benefit and MORE # trajectory-divergence-induced new not-connected fails than at the # uniform default weight -- reverted, left at implicit uniform weight. -_MUTATION_WEIGHTS = {"level_add": 0.2, "level_delete": 0.2, "place_missing": 2.0} +_MUTATION_WEIGHTS = {"level_add": 0.2, "level_delete": 0.2, "place_missing": 2.0, + "ruin_recreate": 3.0} def _worker_init() -> None: @@ -241,6 +242,7 @@ def search( enable_reassociate: bool = False, enable_shape_repair: bool = False, enable_bridge_circulation: bool = False, + enable_ruin_recreate: bool = False, feasibility_filter: bool = False, feasibility_max_shape_fails: int | None = None, circ_divisor: int = 3, @@ -323,6 +325,16 @@ def search( because it needs no ``fitness.Fitness`` instance — only the tree's own adjacency graph — so it is otherwise unconditionally live once landed in ``operators.MUTATIONS``. + + ``enable_ruin_recreate`` (homemaker-py-f1d, EXPERIMENTAL, default off) un- + mutes ``operators.mutate_ruin_recreate``: a large-neighbourhood-search move + that un-divides one wing of a storey and rebuilds it with the same + adjacency-aware constructor the seeders use (``operators. + _assign_adjacency_aware``, seeded from the surviving circulation bordering + the wing), instead of relying only on the small local mutation operators to + discover an improving rearrangement. Gated like ``reassociate`` (zero + mutation weight unless enabled) — it needs only ``reqs``, no + ``fitness.Fitness`` instance. """ from .oracle import DEFAULT_URB_ROOT @@ -337,6 +349,8 @@ def search( mutation_weights["reassociate"] = 0.0 if not enable_bridge_circulation: mutation_weights["bridge_circulation"] = 0.0 + if not enable_ruin_recreate: + mutation_weights["ruin_recreate"] = 0.0 # homemaker-py-161: shape_rotate/deslim are gated by operators.mutate itself # (fit_ops go to zero probability when fit=None) — only build the Fitness # instance, and thus only let them fire, when explicitly enabled. @@ -936,6 +950,7 @@ def search_staged( enable_reassociate: bool = False, enable_shape_repair: bool = False, enable_bridge_circulation: bool = False, + enable_ruin_recreate: bool = False, feasibility_filter: bool = False, feasibility_max_shape_fails: int | None = None, circ_divisor: int = 3, @@ -993,6 +1008,7 @@ def search_staged( enable_reassociate=enable_reassociate, enable_shape_repair=enable_shape_repair, enable_bridge_circulation=enable_bridge_circulation, + enable_ruin_recreate=enable_ruin_recreate, feasibility_filter=feasibility_filter, feasibility_max_shape_fails=feasibility_max_shape_fails, circ_divisor=circ_divisor, @@ -1031,6 +1047,7 @@ def search_staged( enable_reassociate=enable_reassociate, enable_shape_repair=enable_shape_repair, enable_bridge_circulation=enable_bridge_circulation, + enable_ruin_recreate=enable_ruin_recreate, feasibility_filter=feasibility_filter, feasibility_max_shape_fails=feasibility_max_shape_fails, circ_divisor=circ_divisor, @@ -1078,6 +1095,7 @@ def search_staged( enable_reassociate=enable_reassociate, enable_shape_repair=enable_shape_repair, enable_bridge_circulation=enable_bridge_circulation, + enable_ruin_recreate=enable_ruin_recreate, feasibility_filter=feasibility_filter, feasibility_max_shape_fails=feasibility_max_shape_fails, circ_divisor=circ_divisor, diff --git a/src/homemaker_layout/evolve.py b/src/homemaker_layout/evolve.py index 0d6cbc8..060d581 100644 --- a/src/homemaker_layout/evolve.py +++ b/src/homemaker_layout/evolve.py @@ -114,6 +114,16 @@ def _parse_args(argv=None) -> argparse.Namespace: "to circulation, directly clearing a 'level N not " "connected' fail instead of relying on the qi6 graded " "comparator key (measured negative, §18) (default: off)") + 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)") p.add_argument("--collapse-insearch", dest="collapse_insearch", action=argparse.BooleanOptionalAction, default=_env_bool("HOMEMAKER_COLLAPSE_INSEARCH", True), @@ -192,6 +202,7 @@ def main(argv=None) -> int: print(f"superpose : {args.superpose}", file=sys.stderr) print(f"conn grade : {args.conn_grade}", file=sys.stderr) print(f"bridge circulation : {args.bridge_circulation}", file=sys.stderr) + print(f"ruin recreate : {args.ruin_recreate}", file=sys.stderr) print(f"collapse in-search : {args.collapse_insearch}", file=sys.stderr) print(f"output : {out or 'stdout'}", file=sys.stderr, flush=True) @@ -243,6 +254,7 @@ def main(argv=None) -> int: superpose=args.superpose, conn_grade=args.conn_grade, enable_bridge_circulation=args.bridge_circulation, + enable_ruin_recreate=args.ruin_recreate, collapse_insearch=args.collapse_insearch, log=lambda m: print(m, file=sys.stderr, flush=True), ) diff --git a/src/homemaker_layout/operators.py b/src/homemaker_layout/operators.py index 3b12e7b..f264d0d 100644 --- a/src/homemaker_layout/operators.py +++ b/src/homemaker_layout/operators.py @@ -849,7 +849,8 @@ def _assign_adjacency_aware(lvl: dom.Node, room_codes: list[str], reqs, rng: np.random.Generator, door_width: float = 1.2, fixed_circ: "list[dom.Node] | None" = None, interior_outside: bool = False, - n_outside: int = 1) -> None: + n_outside: int = 1, + scope: "set[dom.Node] | None" = None) -> None: """Assign leaf types so rooms cluster around a connected circulation spine. s44 (DESIGN.md §11.2 follow-up): random type assignment leaves rooms stranded @@ -870,13 +871,23 @@ def _assign_adjacency_aware(lvl: dom.Node, room_codes: list[str], reqs, ``lvl`` already has the right number of leaves grown; their types are (re)written in place. Stochastic where it is free (room order, tie-breaks) so a bootstrap batch stays diverse. + + ``scope`` (homemaker-py-f1d): restrict retyping to this subset of ``lvl``'s + leaves — used by the ruin-and-recreate LNS move to rebuild one wing of an + already-typed storey in place. ``fixed_circ`` may then name leaves OUTSIDE + ``scope`` (the surviving circulation bordering the wing) purely as + dominating-set seeds; they anchor the spine but are never retyped, and the + dominating-set growth and room/outside placement only ever touch ``scope``. + ``None`` (default) reproduces the unrestricted whole-``lvl`` behaviour + exactly — every existing caller is unaffected. """ from . import geometry reqs = reqs or {} leaves = lvl.leaves() - n = len(leaves) idx = {leaf: i for i, leaf in enumerate(leaves)} + assignable = scope if scope is not None else set(leaves) + n = len(assignable) R = len(room_codes) n_circ = max(1, n - (R + max(1, n_outside))) # leftover after rooms + outside seeds = [c for c in (fixed_circ or []) if c in idx] @@ -893,16 +904,19 @@ def _assign_adjacency_aware(lvl: dom.Node, room_codes: list[str], reqs, # Greedy connected dominating set of size n_circ: seed from the fixed core (or # the most central leaf), then repeatedly add the frontier leaf that newly - # dominates the most leaves (keeping the set connected). - circ = set(seeds) if seeds else {max(leaves, key=lambda L: (deg.get(L, 0), -idx[L]))} + # dominates the most leaves (keeping the set connected). Growth is confined to + # ``assignable`` so a scoped call never annexes a leaf outside the wing. + circ = (set(seeds) if seeds + else {max(assignable, key=lambda L: (deg.get(L, 0), -idx[L]))}) dominated = set().union(*( _nbrs(s) | {s} for s in circ)) while len(circ) < n_circ: - frontier = (set().union(*(_nbrs(s) for s in circ)) - circ) if circ else set() + frontier = ((set().union(*(_nbrs(s) for s in circ)) - circ) & assignable + if circ else set()) if frontier: pick = max(frontier, key=lambda L: (len(_nbrs(L) - dominated), deg.get(L, 0), -idx[L])) else: # disconnected remainder — seed a new component by degree - rest = [L for L in leaves if L not in circ] + rest = [L for L in assignable if L not in circ] if not rest: break pick = max(rest, key=lambda L: (deg.get(L, 0), -idx[L])) @@ -910,9 +924,10 @@ def _assign_adjacency_aware(lvl: dom.Node, room_codes: list[str], reqs, dominated |= _nbrs(pick) | {pick} for s in circ: - s.type = "C" + if s in assignable: # never retype a fixed_circ seed outside scope + s.type = "C" - noncirc = [L for L in leaves if L not in circ] + noncirc = [L for L in assignable if L not in circ] if interior_outside: # ld2 (§13.6): seed ``O`` as INTERIOR light wells instead of one # peripheral leaf. A landlocked room (no plot facade, no uncovered-O @@ -1196,6 +1211,78 @@ def lift_base_to_storeys(base_root: dom.Node, upper_buckets: list[dict[str, int] return _finalise(child) +def mutate_ruin_recreate(root: dom.Node, rng: np.random.Generator, + types: list[str], reqs=None) -> tuple[dom.Node, str]: + """LNS ruin-and-recreate: rebuild one wing of a storey with the constructor. + + homemaker-py-f1d (DESIGN.md's experiment log): every "search machinery" + change tried so far (niching+restarts, graded objective, Wong-Liu + reassociation, granularity, island model, grain annealing, circulation- + repair ops) has come back null-to-negative, while construction/seeding + quality (adjacency-aware seeding, proportion-aware seeding) is the only + lever that has ever moved the fail count. ``_assign_adjacency_aware`` + currently only runs once, at seeding. This move reuses it repeatedly + during search: pick a divided, live-cut subtree ("wing") of one storey + holding a genuine partial neighbourhood of that storey's leaves (at least + 2, at most half), un-divide it back to a single leaf, then regrow and + retype it with the same adjacency-aware constructor the seeders use — + seeded (``fixed_circ``) from whichever already-typed circulation leaves + border the wing, exactly the mechanism ``lift_base_to_storeys`` uses to + grow an upper storey off an inherited core (ld5, §11.7), so the rebuilt + interior spine reconnects to the surviving one instead of growing a + disconnected island. + + The wing's programme room-code budget (the multiset of required-space + types already inside it) is preserved exactly; only its internal + circulation/outside counts and split are rebuilt, at the same + circ_divisor=3/outside_divisor=3 ratio the constructive seeders default + to (not threaded from the run config — an experimental repair op, like + ``bridge_circulation``, kept parameter-light). + """ + if not reqs: + return _finalise(copy.deepcopy(root)), "ruin_recreate noop" + from . import geometry + + child = copy.deepcopy(root) + _finalise(child) + lvls = dom.levels(child) + totals = {li: len(lvl.leaves()) for li, lvl in enumerate(lvls)} + cands = [(li, n) for li, n in _owned_branches(child) + if totals[li] >= 4 and 2 <= len(n.leaves()) <= max(2, totals[li] // 2)] + if not cands: + return _finalise(child), "ruin_recreate noop" + li, wing = _pick(rng, cands) + lvl = lvls[li] + + G = geometry.leaf_graph(lvl) + wing_leaves = set(wing.leaves()) + border_circ = sorted( + {nb for lf in wing_leaves for nb in G.neighbors(lf) + if nb not in wing_leaves and nb.type and nb.type[0].lower() == "c"}, + key=lambda n: n.id or "") + + rooms = [lf.type for lf in wing.leaves() if lf.type in reqs] + n_circ_total = max(1, -(-len(rooms) // 3)) # circ_divisor=3 + n_o = max(1, round(len(rooms) / 3)) # outside_divisor=3 + n_new = len(rooms) + n_o + max(0, n_circ_total - len(border_circ)) + + wing.left = wing.right = None + wing.division = None + wing.type = None + _grow_leaves(wing, max(1, n_new), rng, balance=True) + dom._link(child) + + _assign_adjacency_aware( + lvl, rooms, reqs, rng, fixed_circ=border_circ or None, + interior_outside=True, n_outside=n_o, scope=set(wing.leaves())) + dom._link(child) + _size_divisions_from_targets(wing, reqs) + + return _finalise(child), ( + f"ruin_recreate {li}/{wing.id or 'root'} " + f"({len(rooms)} rooms, {len(border_circ)} anchors)") + + def mutate_reassociate(root: dom.Node, rng: np.random.Generator, types: list[str]) -> tuple[dom.Node, str]: """Wong-Liu M3 associativity move: ``(a|b)|c <-> a|(b|c)`` on parallel cuts. @@ -1418,6 +1505,7 @@ MUTATIONS = { "level_delete": mutate_level_delete, "shape_rotate": mutate_shape_rotate, "deslim": mutate_deslim, + "ruin_recreate": mutate_ruin_recreate, } @@ -1435,7 +1523,7 @@ def mutate(root: dom.Node, rng: np.random.Generator, types: list[str], names = sorted(MUTATIONS) p = np.array([(weights or {}).get(n, 1.0) for n in names], dtype=float) # these operators need programme reqs; disable them when not available - reqs_ops = ("level_fix", "level_compound_fix", "place_missing") + reqs_ops = ("level_fix", "level_compound_fix", "place_missing", "ruin_recreate") # also takes reqs (to avoid displacing a required room) but works without # it — never zero-weighted, unlike reqs_ops above reqs_optional_ops = ("bridge_circulation",)