kpu: Schedule B in-run leaf-share grain annealing (search_annealed)
Ramp the leaf-share grain down within one continuous run (e.g. 4->3->2->off), carrying the whole population across each step — graduated non-convexity over the single hard sharing->off transition of the §15 finish. - operators.unfold_shared_leaves(above=cap): unfold only leaves whose share exceeds the new grain cap, leaving smaller-share leaves collapsed for the next step. above=1 (default) keeps the full-unfold §15 behaviour. - driver: max_share override threaded through _overrides_for/_fitness_for/ _evaluate so a phase can rebuild the evaluator at a lower leaf_share_max cap; search(seed_pop=) evaluates an explicit initial population so a phase hands its whole population to the next instead of restarting from a single best. - driver.search_annealed: one phase per descending grain then a de-share polish; unfold-above-cap between steps; cumulative accounting + grain-tagged history; honest canonical best (byte-for-byte verified vs homemaker-fitness). - evolve: --anneal-grain LADDER CLI (self-finishing; §15 finish not applied). 8iv settled the primitive (grid unfold beat the circulation-aware slice), so the ramp reuses the plain balanced-grid unfold at every step. Tests: unfold above-cap selectivity, seed_pop seeding, search_annealed phase stitching / honest finish / degenerate-ladder fallback. 258 pass. DESIGN §16. Head-to-head A/B on harbor-house still to run; verdict pending (issue open). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
This commit is contained in:
parent
3d0250817c
commit
3b3eef68a7
7 changed files with 400 additions and 45 deletions
|
|
@ -52,7 +52,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-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-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-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-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":"8iv resolved NEGATIVE (2026-07-16): the circulation-aware unfold it 'wants' was\nbuilt + A/B-tested and LOST to the plain grid unfold (slice 41 fails vs grid 25\nat 150k evals, grid leading throughout). So Schedule B should use the EXISTING\noperators.unfold_shared_leaves (balanced grid) at each grain transition — do NOT\nreintroduce slicing. Access is left to local search on the squarer grid seed,\nwhich yaa already showed reaches 4.19e-06. kpu is now unblocked.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-07-15T06:48:11Z","created_by":"Bruno Postle","updated_at":"2026-07-16T06:36:56Z","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-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":"IMPLEMENTED (2026-07-16): driver.search_annealed + CLI --anneal-grain. Grain ramp lowers evaluator leaf_share_max (new max_share override) per phase; unfold_shared_leaves(above=cap) materialises only leaves exceeding the new cap; search(seed_pop=) carries the whole population across each step; de-share polish finish (honest, canonical byte-for-byte verified). 8iv settled the primitive: reuse plain grid unfold (slice lost). Tests: 258 pass (+4). DESIGN §16. Head-to-head A/B RUNNING on harbor-house (3M total: 500k/grain x3 + 1.5M polish, seed 0, pop 16) vs baseline 5.14e-06 and warm-chain 4.19e-06 — verdict pending.","status":"in_progress","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-16T07:35:57Z","started_at":"2026-07-16T06:47:57Z","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}
|
{"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}
|
||||||
{"id":"homemaker-py-yaa","title":"Investigate leaf-sharing annealing: shared early, materialise-and-de-share later","description":"Follow-up to homemaker-py-3l6. Leaf sharing is a fitness-evaluation knob over an identical genome representation (fitness.py:415; driver caches one evaluator per (dir, sharing)), and leaf_share_factor is a *grain* (0/1=off, N\u003e=2=share at grain N). That makes a coarse-to-fine / continuation schedule feasible: keep sharing on early to fix gross topology (level connectivity, adjacencies, massing) on a smaller effective problem, then reduce sharing to polish per-room size/proportion/width.\n\nTwo schedules to evaluate:\n A. Two-phase warm-start (no code): run sharing to convergence, feed the output .dom as seed to a --no-leaf-sharing run.\n B. In-run annealing (modest driver change): ramp leaf_share_factor down (e.g. 4-\u003e3-\u003e2-\u003eoff) at eval thresholds, rebuilding the cached evaluator and re-evaluating the population at each transition. Gradual grain ramp avoids a single fitness cliff (graduated non-convexity).\n\nKEY IDEA (Bruno): at the sharing-\u003eno-sharing transition, do not rely on place_missing/divide to rediscover the missing rooms. Instead PROGRAMMATICALLY SUBDIVIDE each shared leaf into the correct number of distinct spaces as an explicit 'unfold' operation. This directly pays down the materialisation deficit that otherwise makes a sharing-run seed start deep in the fail hole (evolved-3M.dom was missing ~12 rooms). The unfold turns a shared leaf of code X (share=k) into k sibling leaves of code X splitting its footprint, so the de-shared genome already satisfies the per-room count before local search resumes. This is also option 3 in 3l6 (materialise shared leaves on write) but applied mid-search at the phase change.\n\nRisk to characterise: sharing re-centres size targets on k*target, so the sharing-optimal massing (fewer, larger rooms) is geometrically different from the per-leaf optimum; the transferable value may be the adjacency/topology skeleton, not the sizing. The unfold subdivision needs to produce children with sensible individual proportions/widths, not just area.\n\nBaseline for comparison (harbor-house, init.dom, 3M, niced 1-worker warm chain): direct --no-leaf-sharing reached canonical 4.19e-06, 15 fails, 0 critical, still climbing. A head-to-head warm-start-from-sharing run (seed evolved-3M.dom) is running now (evolved-warmshare.dom) to measure whether the sharing topology, once forced honest, catches the direct route.","notes":"CONCLUSIVE (warm chain complete, ~2.67M total evals): evolved-unfold.dom = 4.19e-06, 15 fails, 0 critical (breakdown: 6 level, 5 size, 1 width). This MATCHES the direct --no-leaf-sharing baseline (evolved-3M-nols-2 4.19e-06/15 fails; -nols-3 5.14e-06/15 fails).\n\nVERDICT for yaa:\n- Schedule A NAIVE (warm-start from raw sharing seed): FAILS — stalls at 8.66e-08, 70 fails; place_missing/divide cannot dig out the ~15-room count deficit.\n- Schedule A + UNFOLD (operators.unfold_shared_leaves at the transition): CATCHES the direct route (4.19e-06, 15 fails). Bruno's key idea validated: the sharing-phase adjacency/topology skeleton is transferable; the materialisation (count) deficit — not the k*target sizing mismatch — was the sole blocker. Unfold pays it down so de-share local search resumes from a competitive genome.\n\nREMAINING: Schedule B (in-run annealing: ramp leaf_share_factor 4-\u003e3-\u003e2-\u003eoff mid-run, rebuilding+re-evaluating the population and unfolding at each grain step) is the still-unimplemented driver change. Now well-motivated: the unfold primitive it needs is built and proven. Recommend spinning Schedule B into its own implementation issue and closing yaa as the investigation it was scoped as. Circulation-routing refinement to unfold tracked in 8iv.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-05T16:33:35Z","created_by":"Bruno Postle","updated_at":"2026-07-15T13:37:07Z","started_at":"2026-07-12T14:52:21Z","closed_at":"2026-07-15T13:37:07Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-yaa","depends_on_id":"homemaker-py-3l6","type":"blocks","created_at":"2026-07-05T17:33:56Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0}
|
{"id":"homemaker-py-yaa","title":"Investigate leaf-sharing annealing: shared early, materialise-and-de-share later","description":"Follow-up to homemaker-py-3l6. Leaf sharing is a fitness-evaluation knob over an identical genome representation (fitness.py:415; driver caches one evaluator per (dir, sharing)), and leaf_share_factor is a *grain* (0/1=off, N\u003e=2=share at grain N). That makes a coarse-to-fine / continuation schedule feasible: keep sharing on early to fix gross topology (level connectivity, adjacencies, massing) on a smaller effective problem, then reduce sharing to polish per-room size/proportion/width.\n\nTwo schedules to evaluate:\n A. Two-phase warm-start (no code): run sharing to convergence, feed the output .dom as seed to a --no-leaf-sharing run.\n B. In-run annealing (modest driver change): ramp leaf_share_factor down (e.g. 4-\u003e3-\u003e2-\u003eoff) at eval thresholds, rebuilding the cached evaluator and re-evaluating the population at each transition. Gradual grain ramp avoids a single fitness cliff (graduated non-convexity).\n\nKEY IDEA (Bruno): at the sharing-\u003eno-sharing transition, do not rely on place_missing/divide to rediscover the missing rooms. Instead PROGRAMMATICALLY SUBDIVIDE each shared leaf into the correct number of distinct spaces as an explicit 'unfold' operation. This directly pays down the materialisation deficit that otherwise makes a sharing-run seed start deep in the fail hole (evolved-3M.dom was missing ~12 rooms). The unfold turns a shared leaf of code X (share=k) into k sibling leaves of code X splitting its footprint, so the de-shared genome already satisfies the per-room count before local search resumes. This is also option 3 in 3l6 (materialise shared leaves on write) but applied mid-search at the phase change.\n\nRisk to characterise: sharing re-centres size targets on k*target, so the sharing-optimal massing (fewer, larger rooms) is geometrically different from the per-leaf optimum; the transferable value may be the adjacency/topology skeleton, not the sizing. The unfold subdivision needs to produce children with sensible individual proportions/widths, not just area.\n\nBaseline for comparison (harbor-house, init.dom, 3M, niced 1-worker warm chain): direct --no-leaf-sharing reached canonical 4.19e-06, 15 fails, 0 critical, still climbing. A head-to-head warm-start-from-sharing run (seed evolved-3M.dom) is running now (evolved-warmshare.dom) to measure whether the sharing topology, once forced honest, catches the direct route.","notes":"CONCLUSIVE (warm chain complete, ~2.67M total evals): evolved-unfold.dom = 4.19e-06, 15 fails, 0 critical (breakdown: 6 level, 5 size, 1 width). This MATCHES the direct --no-leaf-sharing baseline (evolved-3M-nols-2 4.19e-06/15 fails; -nols-3 5.14e-06/15 fails).\n\nVERDICT for yaa:\n- Schedule A NAIVE (warm-start from raw sharing seed): FAILS — stalls at 8.66e-08, 70 fails; place_missing/divide cannot dig out the ~15-room count deficit.\n- Schedule A + UNFOLD (operators.unfold_shared_leaves at the transition): CATCHES the direct route (4.19e-06, 15 fails). Bruno's key idea validated: the sharing-phase adjacency/topology skeleton is transferable; the materialisation (count) deficit — not the k*target sizing mismatch — was the sole blocker. Unfold pays it down so de-share local search resumes from a competitive genome.\n\nREMAINING: Schedule B (in-run annealing: ramp leaf_share_factor 4-\u003e3-\u003e2-\u003eoff mid-run, rebuilding+re-evaluating the population and unfolding at each grain step) is the still-unimplemented driver change. Now well-motivated: the unfold primitive it needs is built and proven. Recommend spinning Schedule B into its own implementation issue and closing yaa as the investigation it was scoped as. Circulation-routing refinement to unfold tracked in 8iv.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-07-05T16:33:35Z","created_by":"Bruno Postle","updated_at":"2026-07-15T13:37:07Z","started_at":"2026-07-12T14:52:21Z","closed_at":"2026-07-15T13:37:07Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-yaa","depends_on_id":"homemaker-py-3l6","type":"blocks","created_at":"2026-07-05T17:33:56Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0}
|
||||||
{"id":"homemaker-py-b3v","title":"9o5 veto hatch: interchange:false to suppress harmful auto-derived classes (harbor-house 8-code chain)","description":"Deferred escape hatch from the 9o5 spec (§2 'escape hatch' / §7.5 open item), now JUSTIFIED by the xi7 validation run. Auto-derivation chains harbor-house into a transitive 8-code class {da1,ef1,k1,la1,m,me1,n,ws1} spanning a 6x size range (Meeting 10 m2 .. Dining/Neighbourhood 60 m2) — semantically nonsensical (Meeting\u003c-\u003eDining\u003c-\u003eKitchen\u003c-\u003eMechanical are not interchangeable). xi7 A/B (3 seeds, budget 2500) shows superpose ON HURTS: OFF wins 2/3 on collapsed score, and ON ADDS fails in both losses (38v33, 48v43), i.e. the collapse re-typing perturbs feasibility. ACTION: add a per-space 'interchange: false' opt-out in patterns.config that removes a code from class derivation (programme.interchangeable / derive_interchange_classes honour the flag). Lets the architect veto a misgroup without disabling superposition globally. NOTE: superpose default stays OFF regardless (xi7 verdict null/negative overall), so this only matters if/when superpose is used on real configs. Lower priority.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-30T07:23:03Z","created_by":"Bruno Postle","updated_at":"2026-06-30T07:23:03Z","dependencies":[{"issue_id":"homemaker-py-b3v","depends_on_id":"homemaker-py-xi7","type":"related","created_at":"2026-06-30T08:24:17Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-b3v","title":"9o5 veto hatch: interchange:false to suppress harmful auto-derived classes (harbor-house 8-code chain)","description":"Deferred escape hatch from the 9o5 spec (§2 'escape hatch' / §7.5 open item), now JUSTIFIED by the xi7 validation run. Auto-derivation chains harbor-house into a transitive 8-code class {da1,ef1,k1,la1,m,me1,n,ws1} spanning a 6x size range (Meeting 10 m2 .. Dining/Neighbourhood 60 m2) — semantically nonsensical (Meeting\u003c-\u003eDining\u003c-\u003eKitchen\u003c-\u003eMechanical are not interchangeable). xi7 A/B (3 seeds, budget 2500) shows superpose ON HURTS: OFF wins 2/3 on collapsed score, and ON ADDS fails in both losses (38v33, 48v43), i.e. the collapse re-typing perturbs feasibility. ACTION: add a per-space 'interchange: false' opt-out in patterns.config that removes a code from class derivation (programme.interchangeable / derive_interchange_classes honour the flag). Lets the architect veto a misgroup without disabling superposition globally. NOTE: superpose default stays OFF regardless (xi7 verdict null/negative overall), so this only matters if/when superpose is used on real configs. Lower priority.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-30T07:23:03Z","created_by":"Bruno Postle","updated_at":"2026-06-30T07:23:03Z","dependencies":[{"issue_id":"homemaker-py-b3v","depends_on_id":"homemaker-py-xi7","type":"related","created_at":"2026-06-30T08:24:17Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
@ -76,25 +76,25 @@
|
||||||
{"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.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-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}
|
{"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":"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":"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":"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":"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-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":"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":"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":"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":"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":"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":"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":"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":"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."}
|
|
||||||
{"_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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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":"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-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":"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":"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":"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":"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":"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":"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":"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)."}
|
||||||
|
|
|
||||||
62
DESIGN.md
62
DESIGN.md
|
|
@ -2251,3 +2251,65 @@ fitness `4.79788e-27` **matches the canonical `homemaker-fitness` byte-for-byte*
|
||||||
copies are materialised). Small budget so absolute quality is low, but the
|
copies are materialised). Small budget so absolute quality is low, but the
|
||||||
honesty — the point of the bug — is restored. Tests: `driver.polish_finish` ×3
|
honesty — the point of the bug — is restored. Tests: `driver.polish_finish` ×3
|
||||||
(unfold+rescore stitching, polish-search accounting, no-best noop); 254 pass.
|
(unfold+rescore stitching, polish-search accounting, no-best noop); 254 pass.
|
||||||
|
|
||||||
|
|
||||||
|
## 16. In-run leaf-share grain annealing — Schedule B (`homemaker-py-kpu`) — IN PROGRESS
|
||||||
|
|
||||||
|
**Premise.** §15's finish crosses the sharing→off objective cliff in a *single*
|
||||||
|
hard transition (unfold every shared leaf at once, then polish). Schedule B (yaa's
|
||||||
|
still-open option) instead **ramps the grain down within one continuous run** —
|
||||||
|
e.g. `4 → 3 → 2 → off` — carrying the whole population across each step. Graduated
|
||||||
|
non-convexity: the coarse early grain fixes gross topology/adjacency on a small
|
||||||
|
effective problem (few, large rooms); each step materialises a little more and
|
||||||
|
refines per-room size/proportion/width; no single fitness cliff is crossed at once.
|
||||||
|
The question (kpu): **does a graduated ramp beat the single hard unfold transition**
|
||||||
|
(yaa's warm-chain 4.19e-06) and the direct no-sharing baseline (5.14e-06)?
|
||||||
|
|
||||||
|
**8iv settled the unfold primitive first (NEGATIVE).** kpu originally "wanted" the
|
||||||
|
circulation-aware unfold from `homemaker-py-8iv` (route the materialised subtree's
|
||||||
|
access through interior children). 8iv built and A/B-tested it and it **lost** to the
|
||||||
|
plain balanced-grid `unfold_shared_leaves` (slice 41 fails vs grid 25 at 150k evals,
|
||||||
|
grid leading throughout). So Schedule B reuses the **existing grid unfold** at every
|
||||||
|
grain step — no slicing reintroduced; access is left to local search on the squarer
|
||||||
|
grid seed (which yaa showed reaches 4.19e-06).
|
||||||
|
|
||||||
|
**Mechanism (`driver.search_annealed`).** One phase per descending grain in
|
||||||
|
`grain_ladder` (default `(4, 3, 2)`), then a de-share polish:
|
||||||
|
|
||||||
|
1. **Phase 0** (`grain = ladder[0]`): a normal `search` — constructs the population
|
||||||
|
at `leaf_share_factor = cap` with the evaluator's `leaf_share_max` capped to
|
||||||
|
`cap` (new `max_share` override, threaded through `_overrides_for`/`_fitness_for`/
|
||||||
|
`_evaluate`).
|
||||||
|
2. **Each grain step** (`cap` lowered): before resuming, unfold every population leaf
|
||||||
|
whose `share` *exceeds* the new cap — `operators.unfold_shared_leaves(root,
|
||||||
|
above=cap)` — so the leaves the lower cap would under-credit become real rooms
|
||||||
|
instead of fresh missing fails; the rest stay collapsed for the next step. The
|
||||||
|
whole population is then handed to the next `search` via the new `seed_pop`
|
||||||
|
argument (each root re-optimised and re-scored under the lower cap), preserving
|
||||||
|
topology/adjacency continuity rather than restarting from a single best.
|
||||||
|
3. **Finish** (`grain off`): unfold all remaining shared leaves (`above=1`) and run a
|
||||||
|
`leaf_sharing=False` search (or a single rescore when `polish_budget <= 0` / on
|
||||||
|
interrupt), so the returned `best.fitness` is the honest canonical score exactly
|
||||||
|
as §15 guarantees (verified: annealed output re-scored by `homemaker-fitness`
|
||||||
|
matches the reported best byte-for-byte).
|
||||||
|
|
||||||
|
`budget` is split evenly across the sharing phases; `polish_budget` funds the finish.
|
||||||
|
Phases are stitched with cumulative eval/topology accounting and a grain-tagged
|
||||||
|
history (`g4:`/`g3:`/`g2:`/`polish:`) — objectives differ across grains so histories
|
||||||
|
are concatenated, never merged. CLI: `homemaker-evolve --anneal-grain 4,3,2` (implies
|
||||||
|
sharing; self-finishing, so the §15 finish is not applied on top).
|
||||||
|
|
||||||
|
**Verification (plumbing).** harbor-house, budget 900 (300/phase) + polish 300,
|
||||||
|
4 workers: unfolds 33 → 22 → 9 leaf-copies across the ramp, population carried
|
||||||
|
(`anneal-seed/*` lineages), honest share-free output whose reported best
|
||||||
|
`3.36672e-29` **matches `homemaker-fitness` byte-for-byte**. (Fails rise at this toy
|
||||||
|
budget — 64 leaves materialised with almost no recovery budget — so absolute quality
|
||||||
|
is meaningless here; the head-to-head below runs at the baselines' budget.) Tests:
|
||||||
|
`unfold_shared_leaves(above=)` grain-cap selectivity; `search(seed_pop=)` population
|
||||||
|
seeding; `search_annealed` phase stitching / honest finish / degenerate-ladder
|
||||||
|
fallback; 258 pass.
|
||||||
|
|
||||||
|
**Head-to-head (RUNNING).** harbor-house, `init.dom`, seed 0, pop 16, child 80, grain
|
||||||
|
`4,3,2`, budget 1.5M (500k/phase) + polish 1.5M = **3M total**, matched to the yaa
|
||||||
|
baselines. Targets: (a) direct `--no-leaf-sharing` 5.14e-06; (b) manual unfold
|
||||||
|
warm-chain 4.19e-06. Verdict pending run completion.
|
||||||
|
|
|
||||||
|
|
@ -39,19 +39,27 @@ from . import dom, fitness, genome, innerloop, operators, programme
|
||||||
_CHILD_INNER_KW: dict = {}
|
_CHILD_INNER_KW: dict = {}
|
||||||
|
|
||||||
|
|
||||||
def _overrides_for(leaf_sharing: bool, superpose: bool) -> dict | None:
|
def _overrides_for(leaf_sharing: bool, superpose: bool,
|
||||||
"""Run-level conf overrides for the native evaluator (None when all off)."""
|
max_share: int | None = None) -> dict | None:
|
||||||
|
"""Run-level conf overrides for the native evaluator (None when all off).
|
||||||
|
|
||||||
|
``max_share`` (homemaker-py-kpu) overrides the evaluator's ``leaf_share_max``
|
||||||
|
grain cap for the in-run annealing ramp; ``None`` leaves the config default.
|
||||||
|
"""
|
||||||
ov: dict = {}
|
ov: dict = {}
|
||||||
if leaf_sharing:
|
if leaf_sharing:
|
||||||
ov["leaf_sharing"] = True
|
ov["leaf_sharing"] = True
|
||||||
if superpose:
|
if superpose:
|
||||||
ov["superpose"] = True
|
ov["superpose"] = True
|
||||||
|
if max_share is not None:
|
||||||
|
ov["leaf_share_max"] = int(max_share)
|
||||||
return ov or None
|
return ov or None
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=None)
|
@functools.lru_cache(maxsize=None)
|
||||||
def _fitness_for(programme_dir: str, leaf_sharing: bool = False,
|
def _fitness_for(programme_dir: str, leaf_sharing: bool = False,
|
||||||
superpose: bool = False) -> "fitness.Fitness":
|
superpose: bool = False,
|
||||||
|
max_share: int | None = None) -> "fitness.Fitness":
|
||||||
"""Cached Fitness evaluator per (programme dir, leaf_sharing) (config load is
|
"""Cached Fitness evaluator per (programme dir, leaf_sharing) (config load is
|
||||||
the cost).
|
the cost).
|
||||||
|
|
||||||
|
|
@ -62,7 +70,7 @@ def _fitness_for(programme_dir: str, leaf_sharing: bool = False,
|
||||||
inner loop instead of reading the on-disk (sharing-free) patterns.config.
|
inner loop instead of reading the on-disk (sharing-free) patterns.config.
|
||||||
Cached per process — workers fork their own copy.
|
Cached per process — workers fork their own copy.
|
||||||
"""
|
"""
|
||||||
overrides = _overrides_for(leaf_sharing, superpose)
|
overrides = _overrides_for(leaf_sharing, superpose, max_share)
|
||||||
conf, cost = fitness.load_config(programme_dir, overrides=overrides)
|
conf, cost = fitness.load_config(programme_dir, overrides=overrides)
|
||||||
return fitness.Fitness(conf, cost)
|
return fitness.Fitness(conf, cost)
|
||||||
|
|
||||||
|
|
@ -137,7 +145,8 @@ def _evaluate(root: dom.Node, programme_dir, urb_root, x0, budget, inner_kw,
|
||||||
feasibility_max_shape_fails: int | None = None,
|
feasibility_max_shape_fails: int | None = None,
|
||||||
best_n_fails: int | None = None,
|
best_n_fails: int | None = None,
|
||||||
leaf_sharing: bool = False,
|
leaf_sharing: bool = False,
|
||||||
superpose: bool = False) -> tuple[Individual, int]:
|
superpose: bool = False,
|
||||||
|
max_share: int | None = None) -> tuple[Individual, int]:
|
||||||
# §12.3 shape-feasibility pre-filter (homemaker-py-9gp.1): if even the best
|
# §12.3 shape-feasibility pre-filter (homemaker-py-9gp.1): if even the best
|
||||||
# achievable (proportion-aware) geometry of this topology already has at least
|
# achievable (proportion-aware) geometry of this topology already has at least
|
||||||
# as many shape fails as the incumbent's TOTAL fails — and exceeds the tunable
|
# as many shape fails as the incumbent's TOTAL fails — and exceeds the tunable
|
||||||
|
|
@ -145,11 +154,11 @@ def _evaluate(root: dom.Node, programme_dir, urb_root, x0, budget, inner_kw,
|
||||||
# eval instead of spending the full inner-loop budget. The best_n_fails guard
|
# eval instead of spending the full inner-loop budget. The best_n_fails guard
|
||||||
# makes the proxy safe: a topology whose shape-fail floor is still below the
|
# makes the proxy safe: a topology whose shape-fail floor is still below the
|
||||||
# incumbent is never discarded. Pruned individuals are tagged and never admitted.
|
# incumbent is never discarded. Pruned individuals are tagged and never admitted.
|
||||||
overrides = _overrides_for(leaf_sharing, superpose)
|
overrides = _overrides_for(leaf_sharing, superpose, max_share)
|
||||||
if (feasibility_max_shape_fails is not None and best_n_fails is not None):
|
if (feasibility_max_shape_fails is not None and best_n_fails is not None):
|
||||||
pred = operators.predicted_shape_fails(
|
pred = operators.predicted_shape_fails(
|
||||||
root, _reqs_for(str(programme_dir)),
|
root, _reqs_for(str(programme_dir)),
|
||||||
_fitness_for(str(programme_dir), leaf_sharing, superpose))
|
_fitness_for(str(programme_dir), leaf_sharing, superpose, max_share))
|
||||||
if pred > feasibility_max_shape_fails and pred >= best_n_fails:
|
if pred > feasibility_max_shape_fails and pred >= best_n_fails:
|
||||||
ind = Individual(root=root, fitness=0.0, n_fails=pred, ratios={},
|
ind = Individual(root=root, fitness=0.0, n_fails=pred, ratios={},
|
||||||
lineage=f"pruned/{lineage}", grade=0.0,
|
lineage=f"pruned/{lineage}", grade=0.0,
|
||||||
|
|
@ -164,7 +173,7 @@ def _evaluate(root: dom.Node, programme_dir, urb_root, x0, budget, inner_kw,
|
||||||
grade = 0.0
|
grade = 0.0
|
||||||
if want_grade:
|
if want_grade:
|
||||||
_, _, grade = _fitness_for(
|
_, _, grade = _fitness_for(
|
||||||
str(programme_dir), leaf_sharing, superpose).score_with_grade(
|
str(programme_dir), leaf_sharing, superpose, max_share).score_with_grade(
|
||||||
copy.deepcopy(root))
|
copy.deepcopy(root))
|
||||||
ind = Individual(root=root, fitness=r.fitness, n_fails=r.n_fails,
|
ind = Individual(root=root, fitness=r.fitness, n_fails=r.n_fails,
|
||||||
ratios=innerloop.ratio_map(root), lineage=lineage,
|
ratios=innerloop.ratio_map(root), lineage=lineage,
|
||||||
|
|
@ -216,6 +225,8 @@ def search(
|
||||||
depth_balanced: bool = True,
|
depth_balanced: bool = True,
|
||||||
interior_outside: bool = True,
|
interior_outside: bool = True,
|
||||||
outside_divisor: int = 3,
|
outside_divisor: int = 3,
|
||||||
|
max_share: int | None = None,
|
||||||
|
seed_pop: list[dom.Node] | None = None,
|
||||||
) -> SearchResult:
|
) -> SearchResult:
|
||||||
"""Run the memetic loop from ``seed_root`` until ``budget`` oracle
|
"""Run the memetic loop from ``seed_root`` until ``budget`` oracle
|
||||||
evaluations are consumed. Returns the best individual found; its ``root``
|
evaluations are consumed. Returns the best individual found; its ``root``
|
||||||
|
|
@ -251,6 +262,12 @@ def search(
|
||||||
(mean 12.3 → 12.7) and harbor (95 → 94), with restarts strictly worse. The
|
(mean 12.3 → 12.7) and harbor (95 → 94), with restarts strictly worse. The
|
||||||
high-fail plateau is therefore not a population-diversity deficit; the lever
|
high-fail plateau is therefore not a population-diversity deficit; the lever
|
||||||
is the canonical encoding (``homemaker-py-9gp``) and richer operators.
|
is the canonical encoding (``homemaker-py-9gp``) and richer operators.
|
||||||
|
|
||||||
|
``max_share`` (homemaker-py-kpu) overrides the evaluator's ``leaf_share_max``
|
||||||
|
grain cap for this phase; ``None`` uses the config default. ``seed_pop`` (also
|
||||||
|
kpu) supplies an explicit initial population of decoded roots — evaluated
|
||||||
|
under this phase's evaluator instead of bootstrapping or single-seeding — so a
|
||||||
|
grain-anneal ramp can hand a whole population from one phase to the next.
|
||||||
"""
|
"""
|
||||||
from .oracle import DEFAULT_URB_ROOT
|
from .oracle import DEFAULT_URB_ROOT
|
||||||
|
|
||||||
|
|
@ -385,7 +402,7 @@ def search(
|
||||||
best_nf = result.best.n_fails if result.best is not None else None
|
best_nf = result.best.n_fails if result.best is not None else None
|
||||||
full = [
|
full = [
|
||||||
(root, programme_dir, urb_root, x0, budget_, kw_, lin, use_grade,
|
(root, programme_dir, urb_root, x0, budget_, kw_, lin, use_grade,
|
||||||
mx, best_nf, leaf_sharing, superpose)
|
mx, best_nf, leaf_sharing, superpose, max_share)
|
||||||
for root, x0, budget_, kw_, lin in tasks
|
for root, x0, budget_, kw_, lin in tasks
|
||||||
]
|
]
|
||||||
if _pool is not None:
|
if _pool is not None:
|
||||||
|
|
@ -441,7 +458,15 @@ def search(
|
||||||
|
|
||||||
interrupted = False
|
interrupted = False
|
||||||
try:
|
try:
|
||||||
if do_bootstrap:
|
if seed_pop is not None:
|
||||||
|
# homemaker-py-kpu (Schedule B): carry a whole population across a
|
||||||
|
# grain-anneal phase change. Each root is re-optimised and re-scored
|
||||||
|
# under THIS phase's evaluator (leaf_sharing/max_share) as the initial
|
||||||
|
# population, so gross topology/adjacency continuity is preserved while
|
||||||
|
# the effective problem is refined — not restarted from a single best.
|
||||||
|
_run_batch([(copy.deepcopy(r), None, seed_budget, {},
|
||||||
|
f"anneal-seed/{i}") for i, r in enumerate(seed_pop)])
|
||||||
|
elif do_bootstrap:
|
||||||
# Bootstrap: diverse initial population from random topologies.
|
# Bootstrap: diverse initial population from random topologies.
|
||||||
# Each individual is a cold start, so use the exploratory sigma
|
# Each individual is a cold start, so use the exploratory sigma
|
||||||
# schedule (inner_kw={} → cma_search defaults: sigmas=(0.05, 0.15)).
|
# schedule (inner_kw={} → cma_search defaults: sigmas=(0.05, 0.15)).
|
||||||
|
|
@ -457,7 +482,8 @@ def search(
|
||||||
inner_kw={}, lineage="seed",
|
inner_kw={}, lineage="seed",
|
||||||
want_grade=use_grade,
|
want_grade=use_grade,
|
||||||
leaf_sharing=leaf_sharing,
|
leaf_sharing=leaf_sharing,
|
||||||
superpose=superpose)
|
superpose=superpose,
|
||||||
|
max_share=max_share)
|
||||||
n_evals += used
|
n_evals += used
|
||||||
admit(seed_ind, pop)
|
admit(seed_ind, pop)
|
||||||
|
|
||||||
|
|
@ -619,6 +645,154 @@ def polish_finish(
|
||||||
return r2
|
return r2
|
||||||
|
|
||||||
|
|
||||||
|
def search_annealed(
|
||||||
|
seed_root: dom.Node,
|
||||||
|
programme_dir: str | Path,
|
||||||
|
*,
|
||||||
|
budget: int,
|
||||||
|
polish_budget: int,
|
||||||
|
grain_ladder: tuple[int, ...] = (4, 3, 2),
|
||||||
|
pop_size: int = 8,
|
||||||
|
child_budget: int = 80,
|
||||||
|
seed_budget: int = 200,
|
||||||
|
p_crossover: float = 0.2,
|
||||||
|
seed: int = 0,
|
||||||
|
types: list[str] | None = None,
|
||||||
|
inner_kw: dict | None = None,
|
||||||
|
n_workers: int = 1,
|
||||||
|
superpose: bool = False,
|
||||||
|
log=None,
|
||||||
|
**search_kw,
|
||||||
|
) -> SearchResult:
|
||||||
|
"""homemaker-py-kpu (DESIGN.md §16): in-run leaf-share grain annealing.
|
||||||
|
|
||||||
|
Schedule B from ``homemaker-py-yaa``. Instead of a single hard sharing→off
|
||||||
|
transition (§15's unfold+polish finish), ramp the leaf-share grain **down**
|
||||||
|
across phases within one continuous run — e.g. ``grain_ladder=(4, 3, 2)`` then
|
||||||
|
off — carrying the whole population across each step. This is graduated
|
||||||
|
non-convexity: the coarse early grain fixes gross topology/adjacency on a
|
||||||
|
small effective problem; each step refines it, so no single fitness cliff has
|
||||||
|
to be crossed at once.
|
||||||
|
|
||||||
|
Each grain step lowers the evaluator's ``leaf_share_max`` cap and, *before*
|
||||||
|
resuming, unfolds every population leaf whose ``share`` exceeds the new cap
|
||||||
|
(:func:`operators.unfold_shared_leaves` with ``above=cap``) so the carried
|
||||||
|
population stays materialised — the leaves the lower cap would under-credit
|
||||||
|
become real rooms instead of fresh missing fails. The final phase de-shares
|
||||||
|
entirely (``leaf_sharing=False``, unfold ``above=1``) and polishes under the
|
||||||
|
canonical objective, so the returned ``best.fitness`` is the honest canonical
|
||||||
|
score exactly as §15's finish guarantees.
|
||||||
|
|
||||||
|
``grain_ladder`` is deduped and sorted descending; entries < 2 are dropped
|
||||||
|
(no-op grain). ``budget`` is split evenly across the sharing phases (remainder
|
||||||
|
to the first); ``polish_budget`` funds the final de-share phase (``<= 0`` or an
|
||||||
|
interrupt ⇒ unfold + single rescore only, no search — honest but unpolished).
|
||||||
|
Extra keyword args forward to :func:`search`.
|
||||||
|
"""
|
||||||
|
def _log(msg: str) -> None:
|
||||||
|
if log:
|
||||||
|
log(msg)
|
||||||
|
|
||||||
|
ladder = sorted({int(g) for g in grain_ladder if int(g) >= 2}, reverse=True)
|
||||||
|
if not ladder:
|
||||||
|
# Degenerate ladder (all grains < 2) ⇒ nothing to anneal: a plain
|
||||||
|
# no-sharing search over the full budget, honest by construction.
|
||||||
|
return search(
|
||||||
|
seed_root, programme_dir, budget=budget + max(0, polish_budget),
|
||||||
|
pop_size=pop_size, child_budget=child_budget, seed_budget=seed_budget,
|
||||||
|
p_crossover=p_crossover, seed=seed, types=types, inner_kw=inner_kw,
|
||||||
|
n_workers=n_workers, leaf_sharing=False, superpose=superpose, log=log,
|
||||||
|
**search_kw)
|
||||||
|
|
||||||
|
n_phases = len(ladder)
|
||||||
|
base = budget // n_phases
|
||||||
|
phase_budgets = [base] * n_phases
|
||||||
|
phase_budgets[0] += budget - base * n_phases # remainder to phase 0
|
||||||
|
|
||||||
|
def _stitch(acc: "SearchResult | None", r: SearchResult, tag: str) -> SearchResult:
|
||||||
|
"""Concatenate phase ``r`` onto ``acc`` with cumulative accounting and a
|
||||||
|
tagged history (objectives differ across grains, so histories are tagged
|
||||||
|
and concatenated, never merged linearly — as §15's finish does)."""
|
||||||
|
r.history = [(e, f, f"{tag}:{lin}") for e, f, lin in r.history]
|
||||||
|
r.diversity_history = list(r.diversity_history)
|
||||||
|
if acc is None:
|
||||||
|
return r
|
||||||
|
prev = acc.n_evals
|
||||||
|
r.n_evals += prev
|
||||||
|
r.n_topologies += acc.n_topologies
|
||||||
|
r.n_distinct_signatures += acc.n_distinct_signatures
|
||||||
|
r.n_restarts += acc.n_restarts
|
||||||
|
r.interrupted = r.interrupted or acc.interrupted
|
||||||
|
r.history = acc.history + [(e + prev, f, lin) for e, f, lin in r.history]
|
||||||
|
r.diversity_history = (
|
||||||
|
acc.diversity_history
|
||||||
|
+ [(e + prev, d, c) for e, d, c in r.diversity_history])
|
||||||
|
return r
|
||||||
|
|
||||||
|
combined: SearchResult | None = None
|
||||||
|
prev_pop: list[Individual] = []
|
||||||
|
|
||||||
|
for i, cap in enumerate(ladder):
|
||||||
|
if i == 0:
|
||||||
|
_log(f"[anneal] phase 1/{n_phases}: grain {cap}, budget "
|
||||||
|
f"{phase_budgets[0]} (construct population)")
|
||||||
|
r = search(
|
||||||
|
seed_root, programme_dir, budget=phase_budgets[0], pop_size=pop_size,
|
||||||
|
child_budget=child_budget, seed_budget=seed_budget,
|
||||||
|
p_crossover=p_crossover, seed=seed, types=types, inner_kw=inner_kw,
|
||||||
|
n_workers=n_workers, leaf_sharing=True, leaf_share_factor=cap,
|
||||||
|
max_share=cap, superpose=superpose, log=log, **search_kw)
|
||||||
|
else:
|
||||||
|
roots = [copy.deepcopy(ind.root) for ind in prev_pop]
|
||||||
|
created = sum(operators.unfold_shared_leaves(rt, above=cap) for rt in roots)
|
||||||
|
_log(f"[anneal] phase {i + 1}/{n_phases}: grain {cap}, budget "
|
||||||
|
f"{phase_budgets[i]} — unfolded {created} leaf-"
|
||||||
|
f"{'copy' if created == 1 else 'copies'} (share>{cap})")
|
||||||
|
r = search(
|
||||||
|
seed_root, programme_dir, budget=phase_budgets[i], pop_size=pop_size,
|
||||||
|
child_budget=child_budget, seed_budget=seed_budget,
|
||||||
|
p_crossover=p_crossover, seed=seed, types=types, inner_kw=inner_kw,
|
||||||
|
n_workers=n_workers, leaf_sharing=True, leaf_share_factor=cap,
|
||||||
|
max_share=cap, superpose=superpose, log=log, seed_pop=roots,
|
||||||
|
**search_kw)
|
||||||
|
combined = _stitch(combined, r, tag=f"g{cap}")
|
||||||
|
prev_pop = r.population
|
||||||
|
if r.interrupted:
|
||||||
|
break
|
||||||
|
|
||||||
|
if combined is None or combined.best is None:
|
||||||
|
return combined or SearchResult(
|
||||||
|
best=None, population=[], n_evals=0, n_topologies=0)
|
||||||
|
|
||||||
|
# Final honesty phase: de-share entirely. Unfold ALL remaining shared leaves
|
||||||
|
# and polish (or just rescore) under the canonical sharing-off objective, so
|
||||||
|
# the returned best is the honest canonical score (§15's guarantee).
|
||||||
|
if polish_budget > 0 and not combined.interrupted:
|
||||||
|
roots = [copy.deepcopy(ind.root) for ind in prev_pop]
|
||||||
|
created = sum(operators.unfold_shared_leaves(rt, above=1) for rt in roots)
|
||||||
|
_log(f"[anneal] finish: de-share (grain off), polish {polish_budget} "
|
||||||
|
f"evals — unfolded {created} leaf-"
|
||||||
|
f"{'copy' if created == 1 else 'copies'}")
|
||||||
|
r = search(
|
||||||
|
seed_root, programme_dir, budget=polish_budget, pop_size=pop_size,
|
||||||
|
child_budget=child_budget, seed_budget=seed_budget,
|
||||||
|
p_crossover=p_crossover, seed=seed, types=types, inner_kw=inner_kw,
|
||||||
|
n_workers=n_workers, leaf_sharing=False, superpose=superpose, log=log,
|
||||||
|
seed_pop=roots, **search_kw)
|
||||||
|
else:
|
||||||
|
best_root = copy.deepcopy(combined.best.root)
|
||||||
|
created = operators.unfold_shared_leaves(best_root, above=1)
|
||||||
|
_log(f"[anneal] finish: de-share (grain off), rescore only — unfolded "
|
||||||
|
f"{created} leaf-{'copy' if created == 1 else 'copies'}")
|
||||||
|
ind, used = _evaluate(
|
||||||
|
best_root, programme_dir, None, x0=None, budget=seed_budget,
|
||||||
|
inner_kw={}, lineage="unfold", leaf_sharing=False, superpose=superpose)
|
||||||
|
r = SearchResult(best=ind, population=[ind], n_evals=used, n_topologies=1)
|
||||||
|
r.n_distinct_signatures = 1
|
||||||
|
r.history = [(0, ind.fitness, ind.lineage)]
|
||||||
|
return _stitch(combined, r, tag="polish")
|
||||||
|
|
||||||
|
|
||||||
def search_staged(
|
def search_staged(
|
||||||
seed_root: dom.Node,
|
seed_root: dom.Node,
|
||||||
programme_dir: str | Path,
|
programme_dir: str | Path,
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,17 @@ def _parse_args(argv=None) -> argparse.Namespace:
|
||||||
"requirements) form equivalence classes and each candidate "
|
"requirements) form equivalence classes and each candidate "
|
||||||
"collapses every superposed leaf to its best in-class usage "
|
"collapses every superposed leaf to its best in-class usage "
|
||||||
"before scoring (default: off)")
|
"before scoring (default: off)")
|
||||||
|
p.add_argument("--anneal-grain", type=str,
|
||||||
|
default=os.environ.get("HOMEMAKER_ANNEAL_GRAIN"),
|
||||||
|
metavar="LADDER",
|
||||||
|
help="homemaker-py-kpu (Schedule B): in-run leaf-share grain "
|
||||||
|
"annealing. A descending comma-separated grain ladder (e.g. "
|
||||||
|
"'4,3,2') ramped down across phases within one run, "
|
||||||
|
"unfolding leaves that exceed each new cap and carrying the "
|
||||||
|
"population across steps, then a de-share polish. Implies "
|
||||||
|
"leaf-sharing; --budget is split across the sharing phases "
|
||||||
|
"and --polish-budget funds the final de-share phase. Unset "
|
||||||
|
"(default) = the single-transition §15 finish.")
|
||||||
p.add_argument("--polish-budget", type=int,
|
p.add_argument("--polish-budget", type=int,
|
||||||
default=_env_int("HOMEMAKER_POLISH_BUDGET", -1),
|
default=_env_int("HOMEMAKER_POLISH_BUDGET", -1),
|
||||||
metavar="N",
|
metavar="N",
|
||||||
|
|
@ -145,12 +156,40 @@ def main(argv=None) -> int:
|
||||||
print(f"superpose : {args.superpose}", file=sys.stderr)
|
print(f"superpose : {args.superpose}", file=sys.stderr)
|
||||||
print(f"output : {out or 'stdout'}", file=sys.stderr, flush=True)
|
print(f"output : {out or 'stdout'}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
anneal_ladder = None
|
||||||
|
if args.anneal_grain:
|
||||||
|
anneal_ladder = tuple(int(g) for g in args.anneal_grain.split(",")
|
||||||
|
if g.strip())
|
||||||
|
|
||||||
seed_root = dom.load(str(seed_file))
|
seed_root = dom.load(str(seed_file))
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
|
|
||||||
# SIGTERM → KeyboardInterrupt so the driver's interrupt handler fires.
|
# SIGTERM → KeyboardInterrupt so the driver's interrupt handler fires.
|
||||||
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt()))
|
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt()))
|
||||||
|
|
||||||
|
if anneal_ladder:
|
||||||
|
# homemaker-py-kpu (Schedule B): in-run grain annealing already ends with a
|
||||||
|
# de-share polish, so it is self-finishing — the §15 unfold+polish is not
|
||||||
|
# applied on top.
|
||||||
|
polish_budget = args.budget // 2 if args.polish_budget < 0 else args.polish_budget
|
||||||
|
print(f"anneal grain : {anneal_ladder} → off (polish {polish_budget})",
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
r = driver.search_annealed(
|
||||||
|
seed_root,
|
||||||
|
programme_dir,
|
||||||
|
budget=args.budget,
|
||||||
|
polish_budget=polish_budget,
|
||||||
|
grain_ladder=anneal_ladder,
|
||||||
|
pop_size=args.pop,
|
||||||
|
child_budget=args.child_budget,
|
||||||
|
p_crossover=0.2,
|
||||||
|
seed=args.seed,
|
||||||
|
n_workers=args.workers,
|
||||||
|
superpose=args.superpose,
|
||||||
|
log=lambda m: print(m, file=sys.stderr, flush=True),
|
||||||
|
)
|
||||||
|
_finish_sharing = False
|
||||||
|
else:
|
||||||
r = driver.search(
|
r = driver.search(
|
||||||
seed_root,
|
seed_root,
|
||||||
programme_dir,
|
programme_dir,
|
||||||
|
|
@ -165,6 +204,7 @@ def main(argv=None) -> int:
|
||||||
superpose=args.superpose,
|
superpose=args.superpose,
|
||||||
log=lambda m: print(m, file=sys.stderr, flush=True),
|
log=lambda m: print(m, file=sys.stderr, flush=True),
|
||||||
)
|
)
|
||||||
|
_finish_sharing = args.leaf_sharing
|
||||||
|
|
||||||
# homemaker-py-3l6: a leaf-sharing run's internal best is scored against a
|
# homemaker-py-3l6: a leaf-sharing run's internal best is scored against a
|
||||||
# sharing-credited objective (a shared leaf counts as k programme rooms), so
|
# sharing-credited objective (a shared leaf counts as k programme rooms), so
|
||||||
|
|
@ -173,7 +213,7 @@ def main(argv=None) -> int:
|
||||||
# written .dom is honest AND its materialised rooms are cleaned up (yaa: the
|
# written .dom is honest AND its materialised rooms are cleaned up (yaa: the
|
||||||
# unfold-then-polish path catches the direct no-sharing route). After this,
|
# unfold-then-polish path catches the direct no-sharing route). After this,
|
||||||
# r.best.fitness is the canonical score (leaf_sharing off ⇒ internal == canon).
|
# r.best.fitness is the canonical score (leaf_sharing off ⇒ internal == canon).
|
||||||
if args.leaf_sharing and r.best is not None:
|
if _finish_sharing and r.best is not None:
|
||||||
polish_budget = args.budget // 2 if args.polish_budget < 0 else args.polish_budget
|
polish_budget = args.budget // 2 if args.polish_budget < 0 else args.polish_budget
|
||||||
# An interrupted sharing run still needs an honest output, but the user
|
# An interrupted sharing run still needs an honest output, but the user
|
||||||
# asked to stop — unfold and rescore only, skip the long polish phase.
|
# asked to stop — unfold and rescore only, skip the long polish phase.
|
||||||
|
|
|
||||||
|
|
@ -618,7 +618,7 @@ def _size_subtree_equal(node: dom.Node) -> None:
|
||||||
_rec(node)
|
_rec(node)
|
||||||
|
|
||||||
|
|
||||||
def unfold_shared_leaves(root: dom.Node) -> int:
|
def unfold_shared_leaves(root: dom.Node, above: int = 1) -> int:
|
||||||
"""Materialise every live shared leaf into ``k`` distinct sibling leaves.
|
"""Materialise every live shared leaf into ``k`` distinct sibling leaves.
|
||||||
|
|
||||||
homemaker-py-yaa: at the sharing→no-sharing phase change a leaf carrying
|
homemaker-py-yaa: at the sharing→no-sharing phase change a leaf carrying
|
||||||
|
|
@ -631,14 +631,21 @@ def unfold_shared_leaves(root: dom.Node) -> int:
|
||||||
leaves of the SAME code, splitting the footprint into k equal-target children,
|
leaves of the SAME code, splitting the footprint into k equal-target children,
|
||||||
then sizes each new subtree for squarest proportions. Share stamps are
|
then sizes each new subtree for squarest proportions. Share stamps are
|
||||||
cleared and topology (adjacency skeleton) is otherwise preserved. Returns the
|
cleared and topology (adjacency skeleton) is otherwise preserved. Returns the
|
||||||
number of extra leaves created (materialisation deficit paid down)."""
|
number of extra leaves created (materialisation deficit paid down).
|
||||||
|
|
||||||
|
``above`` (homemaker-py-kpu, Schedule B grain ramp): unfold only leaves whose
|
||||||
|
``share`` *exceeds* this grain cap, leaving smaller-share leaves collapsed for
|
||||||
|
the next (lower) grain. ``above=1`` (default) unfolds every shared leaf, the
|
||||||
|
full materialisation the single-transition finish (§15) uses. At grain step
|
||||||
|
``g``, ``above=g`` materialises exactly the leaves the new evaluator cap
|
||||||
|
(``leaf_share_max=g``) would under-credit, so no fresh missing fail appears."""
|
||||||
from . import geometry
|
from . import geometry
|
||||||
|
|
||||||
grown: list[dom.Node] = []
|
grown: list[dom.Node] = []
|
||||||
created = 0
|
created = 0
|
||||||
for lvl in dom.levels(root):
|
for lvl in dom.levels(root):
|
||||||
for leaf in lvl.leaves():
|
for leaf in lvl.leaves():
|
||||||
if leaf.share > 1 and leaf.share_type == leaf.type and leaf.type:
|
if leaf.share > above and leaf.share_type == leaf.type and leaf.type:
|
||||||
created += leaf.share - 1
|
created += leaf.share - 1
|
||||||
_grow_balanced(leaf, leaf.type, leaf.share)
|
_grow_balanced(leaf, leaf.type, leaf.share)
|
||||||
grown.append(leaf)
|
grown.append(leaf)
|
||||||
|
|
|
||||||
|
|
@ -314,3 +314,49 @@ def test_polish_finish_runs_polish_search(fake_inner):
|
||||||
def test_polish_finish_noop_without_best():
|
def test_polish_finish_noop_without_best():
|
||||||
empty = driver.SearchResult(best=None, population=[], n_evals=0, n_topologies=0)
|
empty = driver.SearchResult(best=None, population=[], n_evals=0, n_topologies=0)
|
||||||
assert driver.polish_finish(empty, CORPUS, polish_budget=100) is empty
|
assert driver.polish_finish(empty, CORPUS, polish_budget=100) is empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_seed_pop_evaluates_given_population(fake_inner):
|
||||||
|
# homemaker-py-kpu: seed_pop supplies an explicit initial population; each
|
||||||
|
# given root is evaluated (not bootstrapped/single-seeded) before the loop.
|
||||||
|
pop_roots = [dom.load(str(SEED_FILE)) for _ in range(3)]
|
||||||
|
r = driver.search(dom.load(str(INIT_FILE)), CORPUS, budget=0, pop_size=3,
|
||||||
|
child_budget=80, seed_budget=100, seed=0, seed_pop=pop_roots)
|
||||||
|
# budget 0 ⇒ only the 3 seed-pop evals run (100 each), no children
|
||||||
|
assert r.n_evals == 300
|
||||||
|
assert r.n_topologies == 3
|
||||||
|
assert all(ind.lineage.startswith("anneal-seed/") for ind in r.population)
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_annealed_stitches_phases_and_finishes_honest(fake_inner):
|
||||||
|
# homemaker-py-kpu (Schedule B): the grain ramp runs one phase per ladder
|
||||||
|
# step plus a de-share polish, with cumulative accounting, a grain-tagged
|
||||||
|
# history, and a materialised (share-free) honest best.
|
||||||
|
r = driver.search_annealed(
|
||||||
|
dom.load(str(INIT_FILE)), CORPUS, budget=600, polish_budget=200,
|
||||||
|
grain_ladder=(3, 2), pop_size=3, child_budget=80, seed_budget=80, seed=0)
|
||||||
|
|
||||||
|
assert r.best is not None
|
||||||
|
# honest output: every shared leaf is materialised before the polish phase
|
||||||
|
assert all(lf.share == 1 for lf in r.best.root.leaves())
|
||||||
|
# accounting is cumulative across both sharing phases + polish
|
||||||
|
assert r.n_evals >= 600 + 200 - 80
|
||||||
|
# history is grain-tagged and ordered: first phase g3, then g2, then polish
|
||||||
|
tags = [lin.split(":", 1)[0] for *_, lin in r.history]
|
||||||
|
assert tags[0] == "g3"
|
||||||
|
assert "g2" in tags
|
||||||
|
assert tags[-1] == "polish"
|
||||||
|
# eval offsets are monotone non-decreasing across the stitched phases
|
||||||
|
evs = [e for e, *_ in r.history]
|
||||||
|
assert evs == sorted(evs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_annealed_degenerate_ladder_falls_back(fake_inner):
|
||||||
|
# A ladder with no grain >= 2 has nothing to anneal: a plain no-sharing search
|
||||||
|
# over the full budget (+ polish), and the best is honest (share-free).
|
||||||
|
r = driver.search_annealed(
|
||||||
|
dom.load(str(INIT_FILE)), CORPUS, budget=300, polish_budget=100,
|
||||||
|
grain_ladder=(1,), pop_size=3, child_budget=80, seed_budget=80, seed=0)
|
||||||
|
assert r.best is not None
|
||||||
|
assert r.n_evals >= 300
|
||||||
|
assert all(lf.share == 1 for lf in r.best.root.leaves())
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,32 @@ def test_unfold_shared_leaves_materialises_deficit():
|
||||||
canonical(root) # genome round-trips
|
canonical(root) # genome round-trips
|
||||||
|
|
||||||
|
|
||||||
|
def test_unfold_shared_leaves_above_grain_cap():
|
||||||
|
# homemaker-py-kpu (Schedule B): ``above=cap`` unfolds only leaves whose
|
||||||
|
# share EXCEEDS the grain cap, leaving smaller-share leaves collapsed for the
|
||||||
|
# next lower grain. A share=4 leaf unfolds under above=3; a share=3 leaf does
|
||||||
|
# not — it stays a single shared leaf.
|
||||||
|
from homemaker_layout import geometry
|
||||||
|
|
||||||
|
root = dom.Node(node=[[0, 0], [12, 0], [12, 8], [0, 8]],
|
||||||
|
height=2.7, wall_outer=0.25, wall_inner=0.08,
|
||||||
|
rotation=0, division=[0.5, 0.5])
|
||||||
|
root.left = dom.Node(type="n", share=4, share_type="n") # exceeds cap 3
|
||||||
|
root.right = dom.Node(type="m", share=3, share_type="m") # at cap 3, kept
|
||||||
|
dom._link(root)
|
||||||
|
geometry.clear_cache()
|
||||||
|
|
||||||
|
created = operators.unfold_shared_leaves(root, above=3)
|
||||||
|
|
||||||
|
assert created == 3 # only the share=4 leaf
|
||||||
|
leaves = root.leaves()
|
||||||
|
assert sum(1 for lf in leaves if lf.type == "n") == 4 # materialised
|
||||||
|
assert all(lf.share == 1 for lf in leaves if lf.type == "n")
|
||||||
|
m = [lf for lf in leaves if lf.type == "m"]
|
||||||
|
assert len(m) == 1 and m[0].share == 3 # kept collapsed
|
||||||
|
canonical(root)
|
||||||
|
|
||||||
|
|
||||||
HARBOR = Path(__file__).parent.parent / "examples" / "harbor-house"
|
HARBOR = Path(__file__).parent.parent / "examples" / "harbor-house"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue