From 07a4739576aab85f37d2c8f88f02df02c16c0595 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sun, 19 Jul 2026 11:06:56 +0100 Subject: [PATCH] 7fm: targeted shape-repair operators (shape_rotate/deslim), negative finish-time result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagnosed the geometry-intrinsic residual from 94g's collapse: ratio re-optimisation isn't the bottleneck (1500-eval NM makes zero difference on the 12-fail collapsed best layout); the causes are upstream area starvation and cut-orientation mismatch. Added mutate_shape_rotate/mutate_deslim targeting each, gated on a Fitness instance like the existing reqs-gated repair ops. Evaluated as a finish-time exhaustive hill-climb on the same 6-layout harbor-house sweep 94g used: zero improving moves found anywhere — every candidate move traded the shape fail for a new adjacency/access fail on the co-evolved layout (§4.2's lesson, now confirmed for topology repair). Closes homemaker-py-7fm; spun homemaker-py-161 for the open in-search-GA question. See DESIGN.md §19 for the full writeup. --- .beads/issues.jsonl | 41 ++++++------- DESIGN.md | 70 ++++++++++++++++++++++ src/homemaker_layout/operators.py | 99 ++++++++++++++++++++++++++++++- tests/test_operators.py | 95 +++++++++++++++++++++++++++++ 4 files changed, 284 insertions(+), 21 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 3d4ee5a..60db122 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -23,7 +23,7 @@ {"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-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. PENDING: full-budget A/B to confirm the gradient actually pulls runs toward connected circulation and clears 'not connected' fails; if graded key alone insufficient, follow-on is an insert/relocate-circulation mutation operator (mechanism a) which now has a gradient to climb. Keeping in_progress until the A/B verdict.","status":"in_progress","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-18T17:06:53Z","started_at":"2026-07-18T12:17:08Z","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).","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-07-18T10:11:43Z","created_by":"Bruno Postle","updated_at":"2026-07-18T10:11:43Z","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-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} {"id":"homemaker-py-3l6","title":"Leaf-sharing default makes internal fitness diverge from canonical homemaker-fitness score","description":"Default is --leaf-sharing (on). Leaf sharing is a fitness-evaluation knob (fitness.py:415 quality_size, plus edge cap and count check): a shared leaf of code X is credited as satisfying k programme entries, with its size Gaussian re-centred on k*target. The evolve internal objective therefore rewards genomes that under-materialise the programme. When the winning .dom is re-scored by the canonical homemaker-fitness (leaf_sharing off), those un-materialised copies become 'missing required space ... (critical)' fails.\n\nObserved on examples/harbor-house (init.dom, budget 3M, workers 2):\n - leaf sharing ON (default): internal best 1.03e-05, but canonical score 6.73e-29 with 90 fails (15 critical missing-room).\n - --no-leaf-sharing (warm-started to full budget): internal and canonical agree at 4.19e-06, 15 fails, 0 critical -- ~9500x better than the prior best 3m.dom (4.41e-10).\n\nSo the default silently optimises an objective the canonical scorer does not credit, and writes a catastrophically worse .dom than its reported internal fitness implies.\n\nOptions to consider:\n 1. Make --no-leaf-sharing the default (strict per-leaf baseline agrees with canonical scorer).\n 2. Before writing output, re-score best-so-far with leaf_sharing off and warn (or refuse) if it regresses vs internal fitness.\n 3. Materialise/unfold shared leaves into k distinct rooms when writing the .dom, so the output satisfies the per-room programme.\n 4. Keep sharing as an early-phase relaxation only and anneal leaf_share_factor down to 0 before finishing (see related annealing investigation).","notes":"FIXED (option 3+2 combined, auto-finish): leaf-sharing runs now unfold+polish+rescore before write so output is honest under the canonical scorer.\n\nImplementation:\n- driver.polish_finish(result, programme_dir, polish_budget, ...): deep-copies best, operators.unfold_shared_leaves() to materialise the count deficit, then warm-starts a leaf_sharing=False search (bootstrap=False) from the unfolded genome. polish_budget\u003c=0 -\u003e single rescore eval only (used on interrupt). Stitches evals/topologies/sigs/restarts/history onto the sharing run; history tagged share:/polish: since the two objectives are not comparable. Returned best.fitness is canonical (leaf_sharing off =\u003e internal==canonical).\n- evolve.py: new --polish-budget flag (env HOMEMAKER_POLISH_BUDGET, default -1=auto=budget//2, 0=unfold+rescore only). main() calls polish_finish when --leaf-sharing on; interrupt forces polish_budget=0 for a fast honest output.\n\nVerified end-to-end (harbor-house, budget 3000 + polish 1500): reported polish fitness 4.79788e-27 EXACTLY matches canonical homemaker-fitness, 0 critical fails (was: internal 1.03e-05 vs canonical 6.73e-29 w/ 15 critical). Tiny budget so absolute quality low but honesty restored. Tests: driver.polish_finish x3 (test_driver.py), full suite 254 pass.\n\nDefault kept --leaf-sharing on per decision (sharing's topology-search speed retained; output made honest by the finish). Schedule B in-run annealing remains kpu.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-05T16:24:34Z","created_by":"Bruno Postle","updated_at":"2026-07-15T08:00:26Z","started_at":"2026-07-15T06:56:29Z","closed_at":"2026-07-15T08:00:26Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"homemaker-py-rq2","title":"Flip share_edge_cap default-ON + rebaseline §13.x floor (hph follow-up)","description":"hph/§13.8 A/B confirmed the share-aware edge-too-long cap is positive and monotone-harmless (maple 80.3→74.0, harbor 34.7→31.0, zero regressions across 6 seeds). The fix shipped behind the SHAREEDGE/share_edge_cap knob (default OFF) so controls reproduce. This issue flips the default ON for leaf-sharing runs — it completes the §13.3 leaf-share objective relaxation on the wall measure, mirroring the pll/interior_outside default flips. Rebaselines the §13.x full-stack floor numbers (harbor 34.7→31.0, maple 80.3→74.0 become the new baseline). Couple with INTERIORO/odiv3 if those are also being default-flipped. Verify the test suite + a control re-score still reproduce post-flip.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-28T20:01:11Z","created_by":"Bruno Postle","updated_at":"2026-06-28T20:39:00Z","started_at":"2026-06-28T20:32:40Z","closed_at":"2026-06-28T20:39:00Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -55,6 +55,7 @@ {"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-161","title":"In-search evaluation of shape_rotate/deslim GA operators (7fm follow-up)","description":"homemaker-py-7fm's finish-time hill-climb found zero improving moves for\nmutate_shape_rotate/mutate_deslim (operators.py) on the 6-layout harbor-house\nsweep — every candidate move traded a shape fail for a new adjacency/access\nfail on the already co-evolved layout. That's a different regime from\nin-search use: a full multi-generation GA run gives selection pressure and\npopulation diversity a chance to accept a locally-worse move that a later\nstep or recombination completes.\n\nTo test: thread `fit` through driver.search (currently only reqs is passed to\noperators.mutate; shape_rotate/deslim need `fit` and currently no-op inside\nthe GA). Gate with an enable_shape_repair-style flag, mirroring how\nenable_reassociate (§12.3) let 9gp.2 do a clean A/B. Run full-budget\nharbor-house search with/without across seeds, compare final fail counts.\n\nIf negative again, the geometry-intrinsic residual on harbor-house-scale\nprogrammes may be a genuine floor for this representation, not a repairable\ninefficiency (consistent with §17/§19's framing). See DESIGN.md §19 and bd\nmemory collapse-global-94g-and-any-label-usage-optimisation for full context.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-07-19T10:05:51Z","created_by":"Bruno Postle","updated_at":"2026-07-19T10:05:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-qpk","title":"In-search WFC collapse: run collapse_global per-eval during search (A/B vs finish-time)","description":"94g landed and validated the FINISH-TIME global cell-\u003eroom collapse (label search\nover a fixed geometry, monotone, best layout 15-\u003e12). The original 94g thrust was\na per-eval IN-SEARCH collapse: evolution searches unlabelled floorplans and the\nfitness collapses (optimally labels) each candidate before scoring, so search\noptimises the condensed objective directly. This issue is that step.\n\nMECHANISM: call collapse_global (or a cheaper incremental variant) inside\n_evaluate_full before the checks, same as the 9o5 collapse_superposition hook\n(fitness.py:_evaluate_full gates on self._superpose). Reuse the 94g substrate:\nc/o/s partition, level hard constraint, adjacency relaxation, public-access pin,\nthreshold objective.\n\nRISK (carried from homemaker-py-xi7, why 9o5 went negative): fitness =\nmax-over-labellings flattens/roughens the landscape — many topologies collapse to\nsimilar best scores, removing the gradient evolution climbs. AMPLIFIED at full\n(global) scope. The finish-time result does NOT de-risk this: finish-time is\nstrictly cannot-worsen by construction; in-search changes the objective every\neval. MUST A/B superpose-global ON vs OFF with a relaxation-gap log, exactly like\nxi7 did for 9o5, before adopting.\n\nCOST: collapse_global builds graphs + an assignment relaxation per eval — far more\nthan 9o5's per-class collapse. Needs an incremental/cheap variant or caching to be\naffordable in the inner loop; profile first.\n\nPrereq ordering: circulation placement (homemaker-py-qi6) and shape repair change\nthe skeleton/geometry the collapse labels over, so ideally sequence those first.\nRelated: 94g (finish-time, done), xi7 (9o5 A/B + relaxation-gap log), 9o5.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-07-18T10:12:54Z","created_by":"Bruno Postle","updated_at":"2026-07-18T10:12:54Z","dependencies":[{"issue_id":"homemaker-py-qpk","depends_on_id":"homemaker-py-94g","type":"discovered-from","created_at":"2026-07-18T11:12:54Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-kpu","title":"Schedule B: in-run leaf-sharing annealing (ramp grain down, unfold at each step)","description":"Spun out of homemaker-py-yaa, whose investigation is complete. yaa proved Schedule A (two-phase warm-start) works ONLY when shared leaves are unfolded at the sharing-\u003eno-sharing transition: naive warm-start stalls at 8.66e-08/70 fails, but unfold-then-de-share reaches 4.19e-06/15 fails — matching the direct --no-leaf-sharing baseline. operators.unfold_shared_leaves() is built, tested, and proven.\n\nSchedule B is the in-run variant: instead of a manual two-phase chain, anneal leaf_share_factor down within a single driver run (e.g. 4-\u003e3-\u003e2-\u003eoff) at eval thresholds. At each grain transition: (1) rebuild the cached (dir,sharing) evaluator at the new grain, (2) UNFOLD shared leaves that drop below the new grain so the population stays materialised (reuse operators.unfold_shared_leaves), (3) re-evaluate the whole population under the new evaluator, (4) resume local search. Gradual grain ramp = graduated non-convexity: avoids a single fitness cliff, keeps gross topology fixed on the smaller effective problem early, polishes per-room size/proportion/width late.\n\nDriver hooks needed (driver.py): the evaluator is cached per (dir, sharing) at fitness.py:415 and driver caches one per worker; the ramp must rebuild it and re-score the pop at each threshold. Modest change. Compare head-to-head vs (a) direct baseline 5.14e-06 and (b) the manual unfold warm-chain 4.19e-06 from yaa — does a graduated ramp beat a single hard unfold transition?\n\nWants the circulation-aware unfold from homemaker-py-8iv once available.","notes":"A/B DONE — NEGATIVE (2026-07-17). harbor-house 3M (500k/grain x3 + 1.5M polish, workers 4, ~22h): Schedule B = 1.26e-08 / 23 fails (canonical byte-for-byte). Loses decisively to both targets: direct baseline 5.14e-06/15 and warm-chain 4.19e-06/15 (~400x worse, +8 fails). Graduated ramp FALSIFIED: each grain step spikes fails (phase-end 19-\u003e21-\u003e27, final unfold 27-\u003e36); per-phase budget re-polishes partially-materialised states that the next step materialises further, so coarse-grain gains don't carry forward; polish started from a deeper hole (36) than the warm chain's single clean transition and only reached 23. The sharing-phase topology skeleton (yaa) is best cashed in ONCE at full grain, not annealed. Machinery retained (search_annealed, --anneal-grain, unfold above=, seed_pop, max_share override) — correct/tested/honest — but §15 single-transition finish stays the default. DESIGN §16 updated. Closing.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-15T06:48:11Z","created_by":"Bruno Postle","updated_at":"2026-07-17T15:33:01Z","started_at":"2026-07-16T06:47:57Z","closed_at":"2026-07-17T15:33:01Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-kpu","depends_on_id":"homemaker-py-8iv","type":"blocks","created_at":"2026-07-15T07:48:30Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-kpu","depends_on_id":"homemaker-py-yaa","type":"blocks","created_at":"2026-07-15T07:48:28Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} {"id":"homemaker-py-8iv","title":"unfold: route circulation to interior children (access/adjacency fails)","description":"Follow-up to homemaker-py-yaa. operators.unfold_shared_leaves() materialises a share=k leaf into a BALANCED binary subtree of k equal-target children. This closes the count deficit (all critical missing-room fails) but the balanced split creates interior children with no direct edge onto a corridor, so it introduces access/adjacency polish fails. Measured on harbor-house evolved-3M.dom: after unfold, 0 critical but 59 total fails, of which ~14 access + ~12 adjacency are attributable to the unrouted interior rooms (18 size / 2 width / 2 proportion are the k*target-\u003eper-leaf sizing mismatch, a separate concern).\n\nIdea: make the unfold subdivision circulation-aware instead of purely balanced — bias each cut so every new child retains an edge onto the shared leaf's original access boundary (or onto a sibling circulation leaf), mirroring the adjacency-aware constructive seeder (§11.6). Options: (a) orient/order the k-leaf subtree so children fan off the corridor side rather than nesting inward; (b) reserve a thin circulation spine within the unfolded block; (c) let a few post-unfold local-search evals fix it (cheaper, but that is exactly what the warm-start already does). Compare final endpoint with/without circulation-aware unfold against the 3M direct baseline (5.14e-06).\n\nRelates to the in-run annealing driver change (Schedule B, option B in yaa): if annealing rebuilds+re-evaluates the population at each grain transition, the unfold used there wants the same circulation-aware subdivision.","notes":"A/B VERDICT (seed 0, budget 150k, 4 workers, warm-start no-sharing polish from\nevolved-3M.dom): GRID WINS DECISIVELY. Circulation-aware slice LOSES.\n slice: 41 fails, fitness 3.52e-14\n grid : 25 fails, fitness 2.36e-09 (~5 orders better, 16 fewer fails)\nGrid led at EVERY milestone and the gap widened, not a near-tie:\n ~12k evals slice 67 / grid 49; ~36k slice 56 / grid 39;\n ~85k slice 46 / grid 30; ~130k slice 41 / grid 25.\nSlice never crossed. The thin-slab geometric debt (proportion/long/width) from\nforcing all k rooms onto one corridor wall costs MORE than the access routing\nsaves: local search re-routes access via topology moves (level_retype,\nplace_missing, level_fix) faster than it can widen thin slices (which it can't,\nwithout topology change — k equal slices of a compact leaf are intrinsically\nthin). Grid's squarer children are the better warm-start; yaa already showed grid\nreaches 4.19e-06.\n\nCONCLUSION: circulation-aware slicing is the WRONG trade. Retain the grid unfold.\nThe 8iv hypothesis (route access at unfold time) is falsified for the warm-start\nregime — access is better left to local search on a squarer seed. n=1 but the\ngap is large and monotone across the whole 150k-eval trajectory.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-12T15:38:34Z","created_by":"Bruno Postle","updated_at":"2026-07-16T06:35:46Z","started_at":"2026-07-15T13:46:49Z","closed_at":"2026-07-16T06:35:46Z","close_reason":"Investigated and falsified. Circulation-aware unfold (slice shared leaves perpendicular to their access edge so every child touches the corridor) was implemented + unit-tested, but the warm-start A/B (evolved-3M seed, 150k-eval no-sharing polish) shows it LOSES decisively to the existing balanced grid: slice 41 fails/3.5e-14 vs grid 25 fails/2.4e-09, grid leading monotonically at every milestone. Forcing k rooms onto one wall makes intrinsically thin slices whose geometric debt (proportion/long/width) local search cannot pay down without topology change, whereas grid's squarer children let local search re-route access cheaply via level_retype/place_missing/level_fix. Conclusion: retain grid unfold; access is better left to local search on a squarer seed. Code reverted (operators.py, test_operators.py back to grid). Findings in issue notes; A/B traces in examples/harbor-house/ab-8iv-*.","dependencies":[{"issue_id":"homemaker-py-8iv","depends_on_id":"homemaker-py-yaa","type":"blocks","created_at":"2026-07-12T16:48:16Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} @@ -80,26 +81,26 @@ {"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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."} diff --git a/DESIGN.md b/DESIGN.md index c93aad4..ce18723 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -2465,3 +2465,73 @@ toward connected circulation and clear `not connected` fails — needs full-budg pending (short 60-eval smoke run confirms the plumbing only). If the graded key alone is insufficient, the follow-on is an insert/relocate-circulation mutation operator (mechanism (a), still `homemaker-py-qi6`) that now has a gradient to climb. 276 tests pass. + +## 19. Geometry/topology repair for shape-intrinsic fails (`homemaker-py-7fm`) — DONE (negative) + +**Motivation.** §17 established that ~12 of the harbor-house best layout's 15 residual fails +survive the label-only collapse — long-thin cells (`width`/`proportion`/`crinkliness`) whose +geometry, not room assignment, is wrong. `bd memory collapse-global-94g-and-any-label-usage- +optimisation` spun this out as its own problem: a mechanism that moves *geometry*, evaluated for +net fail-count effect on the same 6-layout sweep §17 used. + +**Diagnosis (rules out mechanism (a)).** Re-ran the full-fitness ratio inner loop +(`innerloop.optimise`, Nelder-Mead, 1500 evals, warm-started from the evolved ratios — far above +the ~80-200/child budget search actually spends) on the 12-fail collapsed best layout: **zero +change**, byte-identical fail lines. These are not local optima of the ratio search reachable +with more budget. Tracing two representative fails back through the tree found two distinct +structural causes, neither fixable by re-solving ratios on the existing cuts: (1) **area +starvation** — a leaf's *defining branch* (several levels up) was allocated too little total +area for what it has to share with its siblings (a storage leaf wanting 18m² sat in a 6.4m² +branch whose sibling got 52.8m² of outside space); (2) **orientation mismatch** — a leaf is the +correctly-area-sized-but-thin remainder of a cut whose *rotation* runs parallel to its parent +rectangle's long axis, so no ratio value on that axis avoids a sliver. + +**Mechanism (`operators.mutate_shape_rotate`, `operators.mutate_deslim`).** Two targeted repair +operators addressing each cause, in the `mutate_level_fix` style (structural, not blind-random): +`_shape_failing(leaf, fit)` identifies a named-room leaf whose width or proportion factor +actually fails (`< FAIL_THRESHOLD` under `Fitness.quality_width`/`quality_proportion` — not a +geometric proxy, which over-flags leaves the Gaussian tail still passes). `mutate_shape_rotate` +re-orients the live cut that produced a failing leaf (targets cause 2); `mutate_deslim` merges a +failing leaf into its sibling, undoing the division that starved it (targets cause 1), leaving +the displaced room for `mutate_place_missing` (already in `MUTATIONS`) to re-insert elsewhere. +Both are registered in `MUTATIONS`/`mutate()`, gated on a `fit` argument (a new `fit_ops` class +alongside the existing `reqs_ops`) so they no-op — and are excluded from the outer search's +`weights` — wherever a `Fitness` instance isn't threaded through, exactly as `place_missing` etc. +gate on `reqs`. `driver.search`/`evolve.py` do **not** yet pass `fit` through (see Status below), +so the operators exist but are currently unreachable from the GA — they were evaluated instead +as a finish-time greedy hill-climb (below). + +**Verification (measured, negative).** A finish-time hill-climb applied both operators +exhaustively — for every live cut driving a shape fail, all 3 alternate rotations were tried +(not just `mutate_shape_rotate`'s single random draw) alongside a `deslim` + `place_missing` + +ratio-resolve, keeping the best only if it did not increase the fail count — on the same 6 +harbor-house evolved layouts as §17 (total fails 187): **0 improving moves found on any layout, +on any candidate cut, under any of the 4 tried variants.** Manually inspecting the rejected +candidates for the representative case (harbor-house evolved-3M-nols-3, leaf `0/rlrlr` "la1", +the proportion fail traced above) shows why: every one of the 3 rotations and the deslim+ +reinsert produced a **worse** layout — new `no outside public access`, `not adjacent to c`, +`access`, or `edge too long` fails, in every trial. This is §4.2's core lesson (proxy/partial- +objective repair of a co-evolved local optimum "is structurally unable to win" — every cut +position is *simultaneously* a size/shape knob **and** an adjacency/access/circulation knob) now +confirmed for structural topology repair, not just ratio-solving: on a tightly co-evolved +layout, the cut that makes a leaf thin is *also* the cut providing some other leaf's public- +access or adjacency, so straightening it elsewhere is not free. The residual geometry-intrinsic +fails on the harbor-house best layout appear to be close to a genuine Pareto floor for this +topology, not a repairable inefficiency — consistent with §17's own framing ("geometry-/ +building-bound"). + +**Status / next.** `mutate_shape_rotate`/`mutate_deslim` land in `operators.py`, default-excluded +from `mutate()` (no `fit` threaded through the outer search yet), with dedicated tests +(`tests/test_operators.py`: fail detection, noop-without-`fit`, targeted-cut selection, merge + +`place_missing` repairability) plus automatic coverage via the existing +`test_mutations_yield_canonical_genomes` parametrisation. 282 tests pass. The finish-time +hill-climb script is **not** productionised (unlike §17's `collapse_cmd.py`) because it never +found an improving move to apply — there is nothing to wire up. Not tested: whether these +operators help as *in-search* GA moves (mechanism (c)) — a full multi-generation run gives +selection pressure and population diversity a chance to accept a locally-worse move that a later +step or recombination completes, a fundamentally different regime from single-step greedy +hill-climbing on an already-finished layout. That A/B (thread `fit` through `driver.search`, +gate with an `enable_shape_repair`-style flag as §12.3 did for `reassociate`, run full-budget +with/without) is the remaining open question and would need to be its own measured experiment +before further code changes — this session's finding is that the *finish-time* half of the +issue's candidate mechanisms is a dead end, not that geometry repair is impossible in general. diff --git a/src/homemaker_layout/operators.py b/src/homemaker_layout/operators.py index a975fe8..3d2c7b6 100644 --- a/src/homemaker_layout/operators.py +++ b/src/homemaker_layout/operators.py @@ -373,6 +373,94 @@ def mutate_place_missing(root: dom.Node, rng: np.random.Generator, return _finalise(child), f"place_missing {code} -> {host_id}" +def _shape_failing(leaf: dom.Node, fit) -> bool: + """A named-room leaf whose width or proportion factor actually fails + (``< fitness.FAIL_THRESHOLD``) under ``fit``, the same Gaussian quality + functions the scorer uses (``Fitness.quality_width``/``quality_proportion``) + — not a geometric proxy, which over-flags leaves the gaussian tail still + passes. Generic circulation/outside/sahn leaves are never candidates — + they absorb slack by design (solver.py ``min_width_generic``), not a + repair target.""" + if not leaf.type or leaf.type[0].lower() in "cos": + return False + from . import fitness as _fit_mod + + return (fit.quality_width(leaf) < _fit_mod.FAIL_THRESHOLD + or fit.quality_proportion(leaf) < _fit_mod.FAIL_THRESHOLD) + + +def mutate_shape_rotate(root: dom.Node, rng: np.random.Generator, + types: list[str], fit=None) -> tuple[dom.Node, str]: + """Repair operator (homemaker-py-7fm): re-orient the cut that produced a + shape-failing (long-thin) leaf. + + Diagnosis (bd memory, 7fm): re-running the full-fitness ratio inner loop + with a large budget does not clear these fails — they are not local optima + of the ratio, because the offending leaf is the *thin* side of a cut whose + orientation runs parallel to its parent rectangle's long axis, so any ratio + value on that axis yields a thin sliver. Rotating the defining (live) cut + changes which axis the ratio divides; the inner loop then re-tunes the + ratio on the new axis. Targets only the cut that actually produced a + failing leaf, unlike the untargeted ``mutate_rotate``. Requires ``fit`` + (a ``fitness.Fitness``) to identify genuinely failing leaves. + """ + if fit is None: + return _finalise(copy.deepcopy(root)), "shape_rotate noop" + child = copy.deepcopy(root) + cands: list[tuple[int, dom.Node, dom.Node]] = [] + for li, n in _owned_branches(child): + if n.below is not None: + continue + for side in ("l", "r"): + leaf = n.left if side == "l" else n.right + if not leaf.divided and _shape_failing(leaf, fit): + cands.append((li, n, leaf)) + if not cands: + return _finalise(child), "shape_rotate noop" + li, n, leaf = _pick(rng, cands) + n.rotation = (n.rotation + int(rng.integers(1, 4))) % 4 + return _finalise(child), f"shape_rotate {li}/{n.id or 'root'} (fixing {leaf.id})" + + +def mutate_deslim(root: dom.Node, rng: np.random.Generator, + types: list[str], fit=None) -> tuple[dom.Node, str]: + """Repair operator (homemaker-py-7fm): merge a shape-failing (long-thin) + leaf into its sibling, undoing the division that starved it. + + Unlike ``mutate_shape_rotate`` this addresses cuts whose *area* share is + wrong (an upstream branch several levels up gave the whole subtree too + little area to satisfy every leaf inside it — no ratio or rotation on the + local cut can fix that, bd memory 7fm), not just its orientation. The + displaced room becomes a missing-space fail that ``mutate_place_missing`` + (already in ``MUTATIONS``) re-inserts elsewhere on a later step. Requires + ``fit`` (a ``fitness.Fitness``) to identify genuinely failing leaves. + """ + if fit is None: + return _finalise(copy.deepcopy(root)), "deslim noop" + from . import geometry as _geo + + child = copy.deepcopy(root) + cands = [ + (li, n) for li, n in _owned_branches(child) + if not n.left.divided and not n.right.divided + and (_shape_failing(n.left, fit) or _shape_failing(n.right, fit)) + ] + if not cands: + return _finalise(child), "deslim noop" + li, n = _pick(rng, cands) + l_fail, r_fail = _shape_failing(n.left, fit), _shape_failing(n.right, fit) + if l_fail and not r_fail: + survivor = n.right + elif r_fail and not l_fail: + survivor = n.left + else: + survivor = max((n.left, n.right), key=_geo.area) + n.type = survivor.type if survivor.type and survivor.type[0].lower() not in "cos" else "C" + n.division = None + n.left = n.right = None + return _finalise(child), f"deslim {li}/{n.id or 'root'} (kept {n.type})" + + def _leaves_with_depth(n: dom.Node, d: int = 0) -> list[tuple[dom.Node, int]]: """Every leaf under ``n`` paired with its depth below ``n``.""" if not n.divided: @@ -1239,6 +1327,8 @@ MUTATIONS = { "level_retype": mutate_level_retype, "level_add": mutate_level_add, "level_delete": mutate_level_delete, + "shape_rotate": mutate_shape_rotate, + "deslim": mutate_deslim, } @@ -1251,20 +1341,27 @@ _BASE_P_OPS = ("divide", "undivide", "retype", "swap", "rotate") def mutate(root: dom.Node, rng: np.random.Generator, types: list[str], weights: dict[str, float] | None = None, - reqs=None, base_p: float = 1.0) -> tuple[dom.Node, str]: + reqs=None, base_p: float = 1.0, fit=None) -> tuple[dom.Node, str]: """Apply one random mutation drawn from MUTATIONS.""" 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") + # these need a Fitness instance to identify genuinely shape-failing leaves + fit_ops = ("shape_rotate", "deslim") if reqs is None: for op in reqs_ops: p[names.index(op)] = 0.0 + if fit is None: + for op in fit_ops: + p[names.index(op)] = 0.0 if p.sum() == 0: p[:] = 1.0 name = str(rng.choice(names, p=p / p.sum())) if name in reqs_ops: return MUTATIONS[name](root, rng, types, reqs=reqs) + if name in fit_ops: + return MUTATIONS[name](root, rng, types, fit=fit) if name in _BASE_P_OPS: return MUTATIONS[name](root, rng, types, base_p=base_p) return MUTATIONS[name](root, rng, types) diff --git a/tests/test_operators.py b/tests/test_operators.py index bc726bf..1595ac7 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -1,5 +1,6 @@ """Operator tests (oracle-free): every child is a valid, canonical genome.""" +import copy from pathlib import Path import numpy as np @@ -512,3 +513,97 @@ def test_predicted_shape_fails_is_nonneg_and_pure(): assert sum(len(lvl.leaves()) for lvl in dom.levels(root)) == n_leaves # deterministic assert operators.predicted_shape_fails(root, reqs, fit) == pred + + +# --------------------------------------------------------------------------- # +# 7fm — targeted shape repair (shape_rotate / deslim) +# --------------------------------------------------------------------------- # +@pytest.mark.skipif(not HARBOR.is_dir(), reason="harbor-house not available") +def test_shape_failing_flags_known_fail_only(): + from homemaker_layout import fitness, programme + + conf, cost = fitness.load_config(str(HARBOR)) + fit = fitness.Fitness(conf, cost) + root = dom.load(str(HARBOR / "generated.dom")) + lvl0 = dom.levels(root)[0] + + # generated.dom/0/rr (type "r") has a real proportion fail (fixture, + # verified via homemaker-fitness); an outside leaf is never a candidate + # regardless of its geometry. + assert operators._shape_failing(lvl0.by_id("rr"), fit) + assert not operators._shape_failing(lvl0.by_id("lllrl"), fit) # type O + + +@pytest.mark.skipif(not HARBOR.is_dir(), reason="harbor-house not available") +def test_mutate_shape_rotate_noop_without_fit(): + root = dom.load(str(HARBOR / "generated.dom")) + child, desc = operators.mutate_shape_rotate(root, np.random.default_rng(0), TYPES) + assert "noop" in desc + canonical(child) + + +def _with_forced_slim_leaf(root: dom.Node, code: str = "r") -> tuple[dom.Node, str]: + """Force a real, deterministic shape fail: divide the largest outside leaf + 95/5 into (``code``, "C"). The 5% side is narrow/high-aspect on any real + plot, and both sides are fresh leaves (a valid deslim candidate too), + unlike the fixture's organic fails which may not have a mergeable sibling.""" + from homemaker_layout import geometry + + child = copy.deepcopy(root) + lvl0 = dom.levels(child)[0] + host = max((lf for lf in lvl0.leaves() if lf.type == "O"), key=geometry.area) + host_id = host.id + host.division = [0.05, 0.05] + host.rotation = 0 + host.left = dom.Node(type=code) + host.right = dom.Node(type="C") + host.type = None + child = operators._finalise(child) + leaf_id = (host_id + "l") if host_id else "l" + return child, leaf_id + + +@pytest.mark.skipif(not HARBOR.is_dir(), reason="harbor-house not available") +def test_mutate_shape_rotate_targets_a_failing_cut(): + from homemaker_layout import fitness, programme + + reqs = programme.load_programme_dir(str(HARBOR)) + types = sorted(reqs) + ["C", "O"] + conf, cost = fitness.load_config(str(HARBOR)) + fit = fitness.Fitness(conf, cost) + root, leaf_id = _with_forced_slim_leaf(dom.load(str(HARBOR / "generated.dom"))) + assert operators._shape_failing(dom.levels(root)[0].by_id(leaf_id), fit) + + child, desc = operators.mutate_shape_rotate(root, np.random.default_rng(0), types, fit=fit) + assert "noop" not in desc + canonical(child) + # only the rotation of the targeted cut changes; leaf multiset preserved + assert _leaf_types(child) == _leaf_types(root) + + +@pytest.mark.skipif(not HARBOR.is_dir(), reason="harbor-house not available") +def test_mutate_deslim_merges_failing_leaf_and_is_repairable(): + from homemaker_layout import fitness, graph, programme + + reqs = programme.load_programme_dir(str(HARBOR)) + types = sorted(reqs) + ["C", "O"] + conf, cost = fitness.load_config(str(HARBOR)) + fit = fitness.Fitness(conf, cost) + root, _leaf_id = _with_forced_slim_leaf(dom.load(str(HARBOR / "generated.dom"))) + n_leaves = sum(len(lvl.leaves()) for lvl in dom.levels(root)) + + child, desc = operators.mutate_deslim(root, np.random.default_rng(0), types, fit=fit) + assert "noop" not in desc + canonical(child) + # a merge strictly reduces the leaf count... + assert sum(len(lvl.leaves()) for lvl in dom.levels(child)) == n_leaves - 1 + # ...and the displaced room is repairable by the existing place_missing op + _, missing = graph.check_space_counts(child, reqs) + assert missing + rng = np.random.default_rng(0) + for _ in range(len(missing) + 5): + child, _ = operators.mutate_place_missing(child, rng, types, reqs=reqs) + _, missing = graph.check_space_counts(child, reqs) + if not missing: + break + assert missing == []