homemaker-py-6xh: wire shape-curve DP into driver.py as an NM warm-start
Promotes the validated shape-curve DP (experiments/shapecurve_spike.py, 2g7.4, DESIGN.md §37.2) from a reference-only spike into src/homemaker_layout/shapecurve.py, and wires it into driver._evaluate as a warm-start for innerloop.optimise: when eligible (single storey, no leaf_sharing/superpose/max_share/multi_use) and no caller-supplied x0, the DP's exact shape-feasible ratio point is written onto the tree before NM runs, off by default (shapecurve_warmstart=/--shapecurve-warmstart). Caught and fixed a latent bug promoting the spike: realise() could leave numpy.float64 in `division`, which yaml.safe_dump can't serialise — the original spike never round-tripped through dom.dumps so this was never hit. A/B on harbor-house-l0 (experiments/ab_shapecurve_warmstart.py, budget=2000, 5 seeds): mean total fails 16.6 (on) vs 19.6 (off), ~3.5x mean fitness improvement; mean hard-fail count alone was a noise-level wash at this sample size. Full writeup in DESIGN.md §37.4. Deliberately deferred to new tracked beads (children of 2g7): DP-exact hard pre-filter (wkh), multi-storey below-link support (koo), leaf_sharing/ co_type modelling (tym), true skew-quad polygon algebra (ekc) — 6xh stays in_progress pending those. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
This commit is contained in:
parent
9296f75cdb
commit
229c3c6933
8 changed files with 793 additions and 27 deletions
|
|
@ -27,7 +27,9 @@
|
||||||
{"id":"homemaker-py-1p0","title":"Geometry inner loop: full-objective equal-offset ratio optimiser","description":"DESIGN.md §5.1, §7 Phase 1. Productionise experiments/optimize_fullfitness.py into homemaker: optimise(topology, x0=None) -\u003e (geometry, fitness). DOF = equal-offset division ratios of free branches (solver.free_branches, lowest-storey cut ownership), clipped to [eps, 1-eps]. Objective = full oracle fitness (never a proxy — §4.2 falsified). Must support warm-start x0 (§5.6) and a population/batch evaluation mode so each iteration scores via one batched oracle call (§4.6).","acceptance_criteria":"Reproduces or exceeds §4.5 gains (x1.24–x1.67, no new failures) on 2f45907, candidate-002, c964435; works as a library call on any corpus .dom","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:58Z","created_by":"Bruno Postle","updated_at":"2026-06-12T08:46:31Z","started_at":"2026-06-12T00:14:19Z","closed_at":"2026-06-12T08:46:31Z","close_reason":"innerloop.optimise() lands: batched CMA-ES sigma ladder (0.05/0.15, IPOP popsize doubling, deterministic seeding) over equal-offset free-branch ratios vs full oracle fitness; warm-start x0 supported. Acceptance vs unprojected originals: x1.65/x1.66/x1.58 against bars x1.24/x1.67/x1.59, no new failures, 46 oracle calls vs NM's 200. Two near-bar results accepted as reproduced-within-noise (1% tol) — draw spread brackets the single-NM-draw bars; approved by Bruno 2026-06-12. Gotchas: equal-offset projection of legacy unequal cuts loses fitness/adds failures (midpoint projection used); pycma seed=0 means clock-seeded.","dependencies":[{"issue_id":"homemaker-py-1p0","depends_on_id":"homemaker-py-av5","type":"blocks","created_at":"2026-06-12T00:39:33Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0}
|
{"id":"homemaker-py-1p0","title":"Geometry inner loop: full-objective equal-offset ratio optimiser","description":"DESIGN.md §5.1, §7 Phase 1. Productionise experiments/optimize_fullfitness.py into homemaker: optimise(topology, x0=None) -\u003e (geometry, fitness). DOF = equal-offset division ratios of free branches (solver.free_branches, lowest-storey cut ownership), clipped to [eps, 1-eps]. Objective = full oracle fitness (never a proxy — §4.2 falsified). Must support warm-start x0 (§5.6) and a population/batch evaluation mode so each iteration scores via one batched oracle call (§4.6).","acceptance_criteria":"Reproduces or exceeds §4.5 gains (x1.24–x1.67, no new failures) on 2f45907, candidate-002, c964435; works as a library call on any corpus .dom","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:58Z","created_by":"Bruno Postle","updated_at":"2026-06-12T08:46:31Z","started_at":"2026-06-12T00:14:19Z","closed_at":"2026-06-12T08:46:31Z","close_reason":"innerloop.optimise() lands: batched CMA-ES sigma ladder (0.05/0.15, IPOP popsize doubling, deterministic seeding) over equal-offset free-branch ratios vs full oracle fitness; warm-start x0 supported. Acceptance vs unprojected originals: x1.65/x1.66/x1.58 against bars x1.24/x1.67/x1.59, no new failures, 46 oracle calls vs NM's 200. Two near-bar results accepted as reproduced-within-noise (1% tol) — draw spread brackets the single-NM-draw bars; approved by Bruno 2026-06-12. Gotchas: equal-offset projection of legacy unequal cuts loses fitness/adds failures (midpoint projection used); pycma seed=0 means clock-seeded.","dependencies":[{"issue_id":"homemaker-py-1p0","depends_on_id":"homemaker-py-av5","type":"blocks","created_at":"2026-06-12T00:39:33Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0}
|
||||||
{"id":"homemaker-py-8cs","title":"Experiment: warm-vs-cold start of inner loop (Lamarckian inheritance)","description":"DESIGN.md §5.6, §4.6. Warm-starting a child topology's inner loop from the parent's optimised ratios is the main lever for cutting per-topology cost (~3 min/topology cold). Apply single topology mutations to optimised corpus designs, re-optimise warm (surviving cuts keep values, new cuts get heuristic defaults) vs cold, compare oracle-call counts to convergence at equal final fitness.","acceptance_criteria":"Speedup factor measured across \u003e=10 mutated topologies; decision recorded (expect order-of-magnitude; if \u003c2x, revisit §4.6 Phase-2 scoping)","notes":"Experiment script committed (experiments/warm_vs_cold.py, 1cc86c8) and machinery validated oracle-free; one mutated child scored through the oracle OK. Waiting on homemaker-py-gp2 reference run to finish, then execute under URB_NO_OCCLUSION=1 (3 parents x 400 evals + 12 children x 2 x 200 evals, ~1.5-2 h oracle time). Default budgets: parent 400, child 200; target = evals to 95% of best final.","status":"closed","priority":1,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:58Z","created_by":"Bruno Postle","updated_at":"2026-06-12T11:44:45Z","closed_at":"2026-06-12T11:44:45Z","close_reason":"Measured (URB_NO_OCCLUSION=1, parent budget 400, child 200, 12 single mutations across 3 designs): cold start reached 95% of warm final in 0/12 cases within budget — speedup unbounded at practical budgets; warm finals beat cold finals x1.2-x4 in 12/12; 6/12 warm starts were within 95% at 1 eval (near-neutral mutations). Decision: Lamarckian warm-starting is MANDATORY in the memetic driver (homemaker-py-b39), not an optimisation; cold starts produce strictly worse geometry at equal budget. Note: 2 undivides were exactly fitness-neutral (same-type merge == Merge_Divided equivalence) — locality datum for homemaker-py-nyb.","dependencies":[{"issue_id":"homemaker-py-8cs","depends_on_id":"homemaker-py-1p0","type":"blocks","created_at":"2026-06-12T00:39:34Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-8cs","title":"Experiment: warm-vs-cold start of inner loop (Lamarckian inheritance)","description":"DESIGN.md §5.6, §4.6. Warm-starting a child topology's inner loop from the parent's optimised ratios is the main lever for cutting per-topology cost (~3 min/topology cold). Apply single topology mutations to optimised corpus designs, re-optimise warm (surviving cuts keep values, new cuts get heuristic defaults) vs cold, compare oracle-call counts to convergence at equal final fitness.","acceptance_criteria":"Speedup factor measured across \u003e=10 mutated topologies; decision recorded (expect order-of-magnitude; if \u003c2x, revisit §4.6 Phase-2 scoping)","notes":"Experiment script committed (experiments/warm_vs_cold.py, 1cc86c8) and machinery validated oracle-free; one mutated child scored through the oracle OK. Waiting on homemaker-py-gp2 reference run to finish, then execute under URB_NO_OCCLUSION=1 (3 parents x 400 evals + 12 children x 2 x 200 evals, ~1.5-2 h oracle time). Default budgets: parent 400, child 200; target = evals to 95% of best final.","status":"closed","priority":1,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:58Z","created_by":"Bruno Postle","updated_at":"2026-06-12T11:44:45Z","closed_at":"2026-06-12T11:44:45Z","close_reason":"Measured (URB_NO_OCCLUSION=1, parent budget 400, child 200, 12 single mutations across 3 designs): cold start reached 95% of warm final in 0/12 cases within budget — speedup unbounded at practical budgets; warm finals beat cold finals x1.2-x4 in 12/12; 6/12 warm starts were within 95% at 1 eval (near-neutral mutations). Decision: Lamarckian warm-starting is MANDATORY in the memetic driver (homemaker-py-b39), not an optimisation; cold starts produce strictly worse geometry at equal budget. Note: 2 undivides were exactly fitness-neutral (same-type merge == Merge_Divided equivalence) — locality datum for homemaker-py-nyb.","dependencies":[{"issue_id":"homemaker-py-8cs","depends_on_id":"homemaker-py-1p0","type":"blocks","created_at":"2026-06-12T00:39:34Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-av5","title":"Batched oracle: score many .dom files per invocation","description":"oracle.py currently scores one .dom per urb-fitness.pl call (~1.65 s/dom). DESIGN.md §4.6: batching amortises Perl startup to ~0.99 s/dom and is required so population/batch optimisers can score a whole generation in one oracle call. Extend oracle.py with a batch API: write N .dom files, one perl invocation, parse N .score/.fails pairs. Keep the single-file path for compatibility.","acceptance_criteria":"Batch of 35 corpus files scores in one perl invocation; per-file results identical to single-file calls; measured s/dom reported","status":"closed","priority":1,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:56Z","created_by":"Bruno Postle","updated_at":"2026-06-12T00:14:06Z","started_at":"2026-06-11T23:50:40Z","closed_at":"2026-06-12T00:14:06Z","close_reason":"score_batch() lands in oracle.py; 35-file corpus parity verified single-vs-batch (1e-12 rel fitness, exact fail sets); 0.98 s/dom batched vs 1.27 single, x1.30","dependency_count":0,"dependent_count":1,"comment_count":0}
|
{"id":"homemaker-py-av5","title":"Batched oracle: score many .dom files per invocation","description":"oracle.py currently scores one .dom per urb-fitness.pl call (~1.65 s/dom). DESIGN.md §4.6: batching amortises Perl startup to ~0.99 s/dom and is required so population/batch optimisers can score a whole generation in one oracle call. Extend oracle.py with a batch API: write N .dom files, one perl invocation, parse N .score/.fails pairs. Keep the single-file path for compatibility.","acceptance_criteria":"Batch of 35 corpus files scores in one perl invocation; per-file results identical to single-file calls; measured s/dom reported","status":"closed","priority":1,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:56Z","created_by":"Bruno Postle","updated_at":"2026-06-12T00:14:06Z","started_at":"2026-06-11T23:50:40Z","closed_at":"2026-06-12T00:14:06Z","close_reason":"score_batch() lands in oracle.py; 35-file corpus parity verified single-vs-batch (1e-12 rel fitness, exact fail sets); 0.98 s/dom batched vs 1.27 single, x1.30","dependency_count":0,"dependent_count":1,"comment_count":0}
|
||||||
{"id":"homemaker-py-6xh","title":"Wire shapecurve DP prototype into driver.py as a real pre-filter + NM warm-start","description":"homemaker-py-2g7.4's prototype (experiments/shapecurve_spike.py, DESIGN.md\n§37.2) validated PASS on harbor-house-l0 (99% agreement vs shape-fail-only NM\nover 200 random topologies, 93.6x speedup at grid_n=150, 0 false negatives).\nIt is not yet wired into the product — it's a reference spike only, same\nstatus as experiments/autodiff_spike.py (§34).\n\nTo productionise per the original plan (DESIGN.md §37 point 2):\n- Replace/augment operators.predicted_shape_fails with the DP as driver.py's\n real per-child pre-filter (a single-sample heuristic today; the DP gives an\n exact yes/no plus a realizing ratio point).\n- Warm-start innerloop.optimise's NM from the DP's realised ratios instead of\n (or in addition to) the current proportion-aware target-geometry seed.\n- Multi-storey support: the DP only walks one level's leaves currently;\n below-linked nodes (wall-stacking across storeys) aren't modelled.\n- leaf_sharing/co_type target-adjustment: not modelled in leaf_constraints,\n needed for any programme that uses either (harbor-house-l0 doesn't).\n- Consider replacing the bounding-box leaf approximation with true skew-quad\n polygon algebra to remove the ~7-12% area approximation error identified\n as the root cause of both measured false positives (§37.2) -- or at least\n characterise it on a LESS rectangular plot than harbor-house-l0's\n near-rectangular trapezoid, where the error is likely worse.\n- A/B against the real driver.search: does DP-pre-filter + warm-start beat\n today's predicted_shape_fails + cold/proportion-aware start on wall-clock\n to N hard fails, on harbor-house (full) and/or a less-rectangular plot?","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-02T22:39:46Z","created_by":"Bruno Postle","updated_at":"2026-08-02T22:39:46Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-koo","title":"Multi-storey (below-link) support for the shape-curve DP","description":"homemaker-py-6xh item 3 (DESIGN.md §37.2/§37.4). src/homemaker_layout/shapecurve.py's solve()/realise() writes division on every divided node under a single level_root unconditionally -- it has no notion of upper-storey below-inherited (wall-stacked) fixed splits (see solver.free_branches: a branch is free only when b.below is None or not b.below.divided). shapecurve.eligible() currently guards this by requiring len(dom.levels(root)) == 1, so the DP warm-start (homemaker-py-6xh) never fires on multi-storey topologies -- which is most real programmes (e.g. examples/programme-house has storey_minimum=2, examples/harbor-house is multi-storey; only the purpose-built examples/harbor-house-l0 de-risk variant is single-storey). Needs: generalise the DP to run bottom-up per storey, treating below-inherited-and-divided branches as FIXED (their (w,h) contribution comes from the level below's already-realised geometry, not chosen by this level's DP) while still composing correctly through them to size the level's own free branches. Validate against the full (multi-storey) examples/harbor-house, the DP's original but not-yet-attempted target.","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-03T17:29:43Z","created_by":"Bruno Postle","updated_at":"2026-08-03T17:29:43Z","dependencies":[{"issue_id":"homemaker-py-koo","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-03T18:31:48Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"homemaker-py-wkh","title":"DP-exact hard pre-filter: replace/augment predicted_shape_fails with shapecurve's boolean infeasibility","description":"homemaker-py-6xh item 1 (DESIGN.md §37.2/§37.4). The shapecurve DP (src/homemaker_layout/shapecurve.py, promoted from experiments/shapecurve_spike.py) gives an EXACT feasible/infeasible verdict for the size/width/proportion family, currently wired only as an NM warm-start (safe: never prunes). operators.predicted_shape_fails' threshold-based prune (driver._evaluate, feasibility_max_shape_fails/best_n_fails) still uses the older heuristic-count proxy. Using shapecurve.solve's infeasible verdict as an ADDITIONAL/replacement hard-prune signal would be stronger (exact, not a graduated heuristic) but riskier: unlike a bad warm-start, a wrong prune permanently discards a topology that could have beaten the incumbent. DESIGN.md §37.2 measured 0/200 false negatives (DP infeasible, NM reaches 0 anyway) on harbor-house-l0, but that is not a proven bound (the rectangle-vs-skew-quad approximation is a known ~7-12% error source, §37.2). Needs: (a) a design for how the DP's boolean signal composes with the existing pred\u003ethreshold\u0026\u0026pred\u003e=best_n_fails guard, (b) a false-negative-risk validation before enabling by default (a larger/less-rectangular topology sweep than the 200-topology harbor-house-l0 one), (c) a driver.search A/B (evals-to-N-hard-fails) against today's predicted_shape_fails-only filter.","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-03T17:29:13Z","created_by":"Bruno Postle","updated_at":"2026-08-03T17:29:13Z","dependencies":[{"issue_id":"homemaker-py-wkh","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-03T18:31:46Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"homemaker-py-6xh","title":"Wire shapecurve DP prototype into driver.py as a real pre-filter + NM warm-start","description":"homemaker-py-2g7.4's prototype (experiments/shapecurve_spike.py, DESIGN.md\n§37.2) validated PASS on harbor-house-l0 (99% agreement vs shape-fail-only NM\nover 200 random topologies, 93.6x speedup at grid_n=150, 0 false negatives).\nIt is not yet wired into the product — it's a reference spike only, same\nstatus as experiments/autodiff_spike.py (§34).\n\nTo productionise per the original plan (DESIGN.md §37 point 2):\n- Replace/augment operators.predicted_shape_fails with the DP as driver.py's\n real per-child pre-filter (a single-sample heuristic today; the DP gives an\n exact yes/no plus a realizing ratio point).\n- Warm-start innerloop.optimise's NM from the DP's realised ratios instead of\n (or in addition to) the current proportion-aware target-geometry seed.\n- Multi-storey support: the DP only walks one level's leaves currently;\n below-linked nodes (wall-stacking across storeys) aren't modelled.\n- leaf_sharing/co_type target-adjustment: not modelled in leaf_constraints,\n needed for any programme that uses either (harbor-house-l0 doesn't).\n- Consider replacing the bounding-box leaf approximation with true skew-quad\n polygon algebra to remove the ~7-12% area approximation error identified\n as the root cause of both measured false positives (§37.2) -- or at least\n characterise it on a LESS rectangular plot than harbor-house-l0's\n near-rectangular trapezoid, where the error is likely worse.\n- A/B against the real driver.search: does DP-pre-filter + warm-start beat\n today's predicted_shape_fails + cold/proportion-aware start on wall-clock\n to N hard fails, on harbor-house (full) and/or a less-rectangular plot?","notes":"2026-08-03: Shipped NM warm-start (item 2) + a scoped A/B (item 5), left\nin_progress -- 3 of 5 description items deliberately deferred to new\ntracked beads (see below). Full details + measured numbers: DESIGN.md\n§37.4.\n\nWhat shipped: promoted experiments/shapecurve_spike.py into\nsrc/homemaker_layout/shapecurve.py (fixed a latent numpy.float64-in-division\nbug caught by round-tripping through dom.dumps in the new tests -- the spike\nnever round-tripped and so never caught it). Added shapecurve.eligible()\n(single storey, no leaf_sharing/superpose/max_share/multi_use). Wired into\ndriver._evaluate as an NM warm-start only (never a prune) behind\nshapecurve_warmstart=False default, threaded through driver.search and\nexposed as `homemaker-evolve --shapecurve-warmstart`. A/B\n(experiments/ab_shapecurve_warmstart.py) on harbor-house-l0, budget=2000,\n5 seeds: mean total-fails 16.6 (on) vs 19.6 (off), ~3.5x mean fitness\nimprovement; mean hard-fail count alone was a noise-level wash (4.6 vs 4.4\nat n=5). Tests: tests/test_shapecurve.py (4), tests/test_driver.py (+3).\nFull suite 388 passed.\n\nDeferred to new tracked beads (children of 2g7, per the epic's own\ndependency ordering):\n- homemaker-py-wkh: DP-exact hard pre-filter (item 1) -- replacing\n predicted_shape_fails' heuristic threshold with the DP's exact\n infeasibility verdict. Needed to actually chase \"evals to N hard fails\"\n rather than just improve soft-fail/fitness convergence.\n- homemaker-py-koo: multi-storey (below-link) DP support (item 3) --\n without this, the warm-start never fires on programme-house\n (storey_minimum=2) or full harbor-house, only the purpose-built\n single-storey harbor-house-l0.\n- homemaker-py-tym: leaf_sharing/co_type modelling (item 4) -- without this,\n the warm-start never fires when leaf_sharing=True, which is\n driver.search's own default.\n- homemaker-py-ekc: true skew-quad polygon algebra (the §37.2-quantified\n ~7-12% rectangle-approximation error) -- not a new bead-description item,\n but the explicit \"consider replacing the bounding-box leaf approximation\"\n bullet.\n\nNet: 6xh's own acceptance (a real evals-to-N-hard-fails win over\npredicted_shape_fails + cold/proportion-aware start) is NOT yet met --\ntoday's result is a safe, positive-but-partial step (soft-fail/fitness\nconvergence, not hard-fail convergence, and only on the single-storey\nno-sharing envelope). Keeping 6xh in_progress rather than closing it.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-08-02T22:39:46Z","created_by":"Bruno Postle","updated_at":"2026-08-03T17:40:52Z","started_at":"2026-08-03T15:51:29Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-2g7.9","title":"Parallel best-of-N + racing harness (use all cores, kill stragglers early)","description":"§14 measured islands \u003c= best-of-N, and the 3M runs used workers=1-2 on a 4-core box — independent seeds are the proven shape and we are not even using the local machine. Build a harness: launch N independent search_staged seeds across all cores (processes, not threads — mind the cvw id()-keyed cache bug), checkpoint fail-counts periodically, successively halve (hyperband-style: kill runs above median hard-fail count at each rung, reallocate budget to survivors). Fix/respect homemaker-py-b8g (parallel non-determinism) and homemaker-py-cvw first or work around with process isolation. This multiplies whatever eval cost the shape-curve DP issue achieves; on its own it is a free 4x locally and scales to any box. Report best + variance across seeds (the seed-variance in §12-§13 tables is huge — 78 vs 97 same config — so best-of-N is worth several levers combined).","acceptance_criteria":"harness runs N=16 seeds on 4 cores with racing; at equal total native-eval budget beats the single-seed mean on harbor by at least the observed seed spread; deterministic per-seed replay","status":"open","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-02T09:15:58Z","created_by":"Bruno Postle","updated_at":"2026-08-02T09:15:58Z","dependencies":[{"issue_id":"homemaker-py-2g7.9","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-02T10:15:58Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-2g7.9","depends_on_id":"homemaker-py-b8g","type":"blocks","created_at":"2026-08-02T10:16:16Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-2g7.9","depends_on_id":"homemaker-py-cvw","type":"blocks","created_at":"2026-08-02T10:16:15Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-2g7.9","title":"Parallel best-of-N + racing harness (use all cores, kill stragglers early)","description":"§14 measured islands \u003c= best-of-N, and the 3M runs used workers=1-2 on a 4-core box — independent seeds are the proven shape and we are not even using the local machine. Build a harness: launch N independent search_staged seeds across all cores (processes, not threads — mind the cvw id()-keyed cache bug), checkpoint fail-counts periodically, successively halve (hyperband-style: kill runs above median hard-fail count at each rung, reallocate budget to survivors). Fix/respect homemaker-py-b8g (parallel non-determinism) and homemaker-py-cvw first or work around with process isolation. This multiplies whatever eval cost the shape-curve DP issue achieves; on its own it is a free 4x locally and scales to any box. Report best + variance across seeds (the seed-variance in §12-§13 tables is huge — 78 vs 97 same config — so best-of-N is worth several levers combined).","acceptance_criteria":"harness runs N=16 seeds on 4 cores with racing; at equal total native-eval budget beats the single-seed mean on harbor by at least the observed seed spread; deterministic per-seed replay","status":"open","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-02T09:15:58Z","created_by":"Bruno Postle","updated_at":"2026-08-02T09:15:58Z","dependencies":[{"issue_id":"homemaker-py-2g7.9","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-02T10:15:58Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-2g7.9","depends_on_id":"homemaker-py-b8g","type":"blocks","created_at":"2026-08-02T10:16:16Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-2g7.9","depends_on_id":"homemaker-py-cvw","type":"blocks","created_at":"2026-08-02T10:16:15Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-2g7.7","title":"LLM repair operator at stagnation (dom+fails -\u003e targeted compound edits)","description":"Generalize the §4.10 lesson: deceptive valleys are crossed by COMPOUND edits (move room + re-home displaced room + fix ratios atomically), which we currently hand-code one per valley (mutate_level_compound_fix). Our fail messages are semantically rich and localized ('me1 on wrong level', 'level 1 not connected', '0/rlrlr proportion') and the .dom is readable — ideal LLM input. Loop: on stagnation (no fail-tier improvement for N evals), serialize best individual + .fails + programme summary -\u003e LLM proposes 3-5 multi-step repairs as structured edit scripts (a small DSL over existing operator primitives: swap/divide/retype/rotate with explicit paths — NOT freeform dom text, so proposals are always well-formed) -\u003e apply, inner-loop, lex-accept as usual. Native fitness disposes; a bad proposal costs one child budget. Cost discipline: one LLM call ~ thousands of native evals, so plateau-only, cache by (signature, fails) key. Benchmark: the 3M-run best sat on 'level 0 not connected' + 'me1 on wrong level' for \u003e1M evals — moves a plan-reader fixes in one edit. Use claude via API (see claude-api skill); temperature\u003e0 for diverse proposals. Later extension (separate issue): AlphaEvolve-style operator-code synthesis using our existing A/B harness as the evaluator.","acceptance_criteria":"on the evolved-3M-nols-3 15-fail plateau seed: repair loop reduces hard-fail count where 1M+ blind evals did not, within \u003c=20 LLM calls; edit-DSL rejects malformed proposals; A/B at equal native-eval budget shows strictly better final fails on \u003e=2/3 seeds","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-02T09:15:54Z","created_by":"Bruno Postle","updated_at":"2026-08-02T09:15:54Z","dependencies":[{"issue_id":"homemaker-py-2g7.7","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-02T10:15:53Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
|
{"id":"homemaker-py-2g7.7","title":"LLM repair operator at stagnation (dom+fails -\u003e targeted compound edits)","description":"Generalize the §4.10 lesson: deceptive valleys are crossed by COMPOUND edits (move room + re-home displaced room + fix ratios atomically), which we currently hand-code one per valley (mutate_level_compound_fix). Our fail messages are semantically rich and localized ('me1 on wrong level', 'level 1 not connected', '0/rlrlr proportion') and the .dom is readable — ideal LLM input. Loop: on stagnation (no fail-tier improvement for N evals), serialize best individual + .fails + programme summary -\u003e LLM proposes 3-5 multi-step repairs as structured edit scripts (a small DSL over existing operator primitives: swap/divide/retype/rotate with explicit paths — NOT freeform dom text, so proposals are always well-formed) -\u003e apply, inner-loop, lex-accept as usual. Native fitness disposes; a bad proposal costs one child budget. Cost discipline: one LLM call ~ thousands of native evals, so plateau-only, cache by (signature, fails) key. Benchmark: the 3M-run best sat on 'level 0 not connected' + 'me1 on wrong level' for \u003e1M evals — moves a plan-reader fixes in one edit. Use claude via API (see claude-api skill); temperature\u003e0 for diverse proposals. Later extension (separate issue): AlphaEvolve-style operator-code synthesis using our existing A/B harness as the evaluator.","acceptance_criteria":"on the evolved-3M-nols-3 15-fail plateau seed: repair loop reduces hard-fail count where 1M+ blind evals did not, within \u003c=20 LLM calls; edit-DSL rejects malformed proposals; A/B at equal native-eval budget shows strictly better final fails on \u003e=2/3 seeds","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-02T09:15:54Z","created_by":"Bruno Postle","updated_at":"2026-08-02T09:15:54Z","dependencies":[{"issue_id":"homemaker-py-2g7.7","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-02T10:15:53Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
|
||||||
{"id":"homemaker-py-2g7.6","title":"Spike: graph-first construction — adjacency-realizing slicing trees / rectangular dualization","description":"Research spike, timeboxed. Literature: rectangular dualization (planar triangulated graph -\u003e rectangular floorplan) and characterizations of slicible adjacency graphs. Our programme already IS an adjacency graph (every room wants c, plus secondary pairs); instead of mutating trees hoping adjacency emerges, construct trees that realize the required adjacency by construction — the direction §11.6/§11.7 crawled toward greedily. Deliverable is a WRITTEN assessment (DESIGN.md section): can harbor's programme graph (16 rooms + spine, 2 storeys with stacking constraint) be dualized into slicing trees, how many, and is enumeration of realizing trees tractable? Prototype only if the answer is clearly yes. Watch for: multi-storey Below-inheritance constrains both floors' trees jointly; circulation spine is a connected dominating set requirement, not a simple adjacency.","acceptance_criteria":"DESIGN.md section with go/no-go verdict, the relevant algorithms named, and complexity estimate for harbor-scale programmes","status":"open","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-02T09:15:07Z","created_by":"Bruno Postle","updated_at":"2026-08-02T09:15:07Z","dependencies":[{"issue_id":"homemaker-py-2g7.6","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-02T10:15:07Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-2g7.6","title":"Spike: graph-first construction — adjacency-realizing slicing trees / rectangular dualization","description":"Research spike, timeboxed. Literature: rectangular dualization (planar triangulated graph -\u003e rectangular floorplan) and characterizations of slicible adjacency graphs. Our programme already IS an adjacency graph (every room wants c, plus secondary pairs); instead of mutating trees hoping adjacency emerges, construct trees that realize the required adjacency by construction — the direction §11.6/§11.7 crawled toward greedily. Deliverable is a WRITTEN assessment (DESIGN.md section): can harbor's programme graph (16 rooms + spine, 2 storeys with stacking constraint) be dualized into slicing trees, how many, and is enumeration of realizing trees tractable? Prototype only if the answer is clearly yes. Watch for: multi-storey Below-inheritance constrains both floors' trees jointly; circulation spine is a connected dominating set requirement, not a simple adjacency.","acceptance_criteria":"DESIGN.md section with go/no-go verdict, the relevant algorithms named, and complexity estimate for harbor-scale programmes","status":"open","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-02T09:15:07Z","created_by":"Bruno Postle","updated_at":"2026-08-02T09:15:07Z","dependencies":[{"issue_id":"homemaker-py-2g7.6","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-02T10:15:07Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
@ -75,6 +77,8 @@
|
||||||
{"id":"homemaker-py-nyb","title":"High-locality topology operators (mutation + subtree crossover)","description":"DESIGN.md §5, §7 Phase 2, §8.4. Mutation moves: divide/undivide leaf, swap children, rotate cut, retype leaf, per-floor delta edits, storey add/delete (cf. Urb Mutate.pm — but geometry sliding belongs to the inner loop, not the operator set). Crossover: area-matched subtree exchange (a subtree = a contiguous region, so crossover is meaningful — Crossover.pm). Operators must be high-locality: small genome change =\u003e small phenotype change, so warm-started inner loops stay cheap.","acceptance_criteria":"Each operator produces valid genomes (oracle scores them without error); locality measured (mean fitness/geometry perturbation per operator)","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:37:27Z","created_by":"Bruno Postle","updated_at":"2026-06-12T13:07:37Z","started_at":"2026-06-12T12:54:23Z","closed_at":"2026-06-12T13:07:37Z","close_reason":"operators.py lands: 7 mutations + area-matched crossover, valid-by-construction via genome.encode repair. 115/115 oracle-valid children; locality measured: geom-pert 0.07-0.33 per op, fitness-pert 0.68-0.99 (0.5^n cliff flags raw moves — warm restart + penalty reshaping confirmed load-bearing). Also fixed dom._link stale below-links on structural mutation.","dependencies":[{"issue_id":"homemaker-py-nyb","depends_on_id":"homemaker-py-k2g","type":"blocks","created_at":"2026-06-12T00:39:36Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
{"id":"homemaker-py-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-ekc","title":"True skew-quad polygon algebra for the shape-curve DP leaf region (remove ~7-12% rectangle approximation error)","description":"homemaker-py-6xh item (DESIGN.md §37.2, 'Remaining approximation error, root-caused'). src/homemaker_layout/shapecurve.py approximates every quad (leaf or internal) as a rectangle with edge-length-derived (w,h) = ((edge0+edge2)/2, (edge1+edge3)/2) -- exact only for a true rectangle/parallelogram. DESIGN.md §37.2's 200-topology harbor-house-l0 validation root-caused both measured false positives to this approximation specifically (not to global rotation or to the rotation-parity composition rule, both already fixed/verified exact): the DP's own realised point had a leaf whose edge-length-approximated area was comfortably inside its feasible bound but whose true geometry.area (a real, slightly non-parallelogram quad) fell just below the true lower bound -- an ~8-12% gap, the same magnitude as harbor-house-l0's own plot-level residual skew. Needs: either (a) replace the rectangle approximation with true skew-quad polygon algebra (a harder closed-form derivation, or a numerically-solved per-leaf feasible region), or (b) at minimum re-characterise the error's magnitude on a LESS rectangular plot than harbor-house-l0's near-rectangular trapezoid (§37.2 flagged this as untested and likely worse elsewhere) so shapecurve_warmstart's real-world false-positive rate is known before wider rollout.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-03T17:30:23Z","created_by":"Bruno Postle","updated_at":"2026-08-03T17:30:23Z","dependencies":[{"issue_id":"homemaker-py-ekc","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-03T18:31:51Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"homemaker-py-tym","title":"leaf_sharing/co_type target-adjustment modelling in shapecurve.leaf_constraints","description":"homemaker-py-6xh item 4 (DESIGN.md §37.2/§37.4). src/homemaker_layout/shapecurve.py's leaf_constraints() uses each leaf's own type's base (target, sigma) params only -- it does not model the leaf-sharing/co_type k-scaling (target*=k, sigma adjustment) that fitness.py's quality_size applies for shared/multi-use leaves. shapecurve.eligible() currently guards this by excluding any run with leaf_sharing/superpose/max_share/multi_use on, so the DP warm-start never fires for those runs -- but leaf_sharing defaults to True in driver.search(), so most real runs are excluded today. Needs: read fitness.py's actual k-scaling formula (quality_size's leaf-sharing branch) and mirror it in leaf_constraints so (amin, amax) reflects a shared leaf's k-multiplied target, then relax shapecurve.eligible's leaf_sharing/max_share guards accordingly (superpose/multi_use may need separate analysis -- check whether either changes the per-leaf target formula the same way share does, or a different one).","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-03T17:30:01Z","created_by":"Bruno Postle","updated_at":"2026-08-03T17:30:01Z","dependencies":[{"issue_id":"homemaker-py-tym","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-03T18:31:49Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-p6t","title":"Convergence-speed A/B for tiered comparator: evals to 0 hard fails, tiered vs flat","description":"homemaker-py-2g7.3 (DESIGN.md §37.1) validated that tiered search (-n_hard,-n_soft,fitness) reaches a strictly lower mean hard-fail count than flat (-n_fails,fitness) at a FIXED budget (20k evals) on harbor-house and maple-court. That measures fail composition at a snapshot, not time-to-solved. The natural follow-up: race the two comparators to '0 hard fails' (or a hard-fail floor) and compare evals/wall-clock to get there, ideally after 2g7.1/2g7.2 ground truth lands so there is a real target to race to instead of an arbitrary floor.","design":"Reuse experiments/tier_ab_2g7_3.py's harness; instead of a fixed budget, run until n_hard==0 or a budget cap, log evals-to-target per seed/scheme, same programmes (harbor-house, maple-court), same 3-seed protocol.","acceptance_criteria":"Report showing evals-to-0-hard-fails (or evals-to-floor) for tiered vs flat, both programmes, 3 seeds; verdict on whether tiering also wins on convergence speed, not just fixed-budget composition.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-02T17:51:38Z","created_by":"Bruno Postle","updated_at":"2026-08-02T17:51:38Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-p6t","title":"Convergence-speed A/B for tiered comparator: evals to 0 hard fails, tiered vs flat","description":"homemaker-py-2g7.3 (DESIGN.md §37.1) validated that tiered search (-n_hard,-n_soft,fitness) reaches a strictly lower mean hard-fail count than flat (-n_fails,fitness) at a FIXED budget (20k evals) on harbor-house and maple-court. That measures fail composition at a snapshot, not time-to-solved. The natural follow-up: race the two comparators to '0 hard fails' (or a hard-fail floor) and compare evals/wall-clock to get there, ideally after 2g7.1/2g7.2 ground truth lands so there is a real target to race to instead of an arbitrary floor.","design":"Reuse experiments/tier_ab_2g7_3.py's harness; instead of a fixed budget, run until n_hard==0 or a budget cap, log evals-to-target per seed/scheme, same programmes (harbor-house, maple-court), same 3-seed protocol.","acceptance_criteria":"Report showing evals-to-0-hard-fails (or evals-to-floor) for tiered vs flat, both programmes, 3 seeds; verdict on whether tiering also wins on convergence speed, not just fixed-budget composition.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-02T17:51:38Z","created_by":"Bruno Postle","updated_at":"2026-08-02T17:51:38Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-2g7.10","title":"MAP-Elites archive over (hard-fail profile, leaf count, circulation fraction)","description":"Quality-diversity as the population-level answer to the §4.10 deceptive-valley problem: an archive keeps the elite per behavior niche, so 'transiently worse but structurally different' stepping stones survive — exactly what lex selection provably discards (§11.4's own analysis). DISTINCT from the failed §11.5/§11.8 niching: that kept diverse individuals under ONE selection pressure; MAP-Elites keeps the BEST individual per niche with no cross-niche competition. Descriptors to try: hard-fail category histogram (bucketed), total leaf count, circulation area fraction, storey balance. Emit from the existing genome.signature/score_with_grade machinery (kept default-off for exactly this reuse, §11.4 verdict). Blocked on the shape-curve DP: archive-filling needs cheap evals to be meaningful. Gate honestly per the ledger discipline: 3 seeds, control = current default stack.","acceptance_criteria":"A/B at equal budget (harbor+maple, 3 seeds): archive best hard-fails \u003c= default-stack best on mean; archive demonstrably contains the stepping stone for at least one accepted valley-crossing (traceable lineage)","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-02T09:16:00Z","created_by":"Bruno Postle","updated_at":"2026-08-02T09:16:00Z","dependencies":[{"issue_id":"homemaker-py-2g7.10","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-02T10:15:59Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-2g7.10","depends_on_id":"homemaker-py-2g7.4","type":"blocks","created_at":"2026-08-02T10:15:59Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-2g7.10","title":"MAP-Elites archive over (hard-fail profile, leaf count, circulation fraction)","description":"Quality-diversity as the population-level answer to the §4.10 deceptive-valley problem: an archive keeps the elite per behavior niche, so 'transiently worse but structurally different' stepping stones survive — exactly what lex selection provably discards (§11.4's own analysis). DISTINCT from the failed §11.5/§11.8 niching: that kept diverse individuals under ONE selection pressure; MAP-Elites keeps the BEST individual per niche with no cross-niche competition. Descriptors to try: hard-fail category histogram (bucketed), total leaf count, circulation area fraction, storey balance. Emit from the existing genome.signature/score_with_grade machinery (kept default-off for exactly this reuse, §11.4 verdict). Blocked on the shape-curve DP: archive-filling needs cheap evals to be meaningful. Gate honestly per the ledger discipline: 3 seeds, control = current default stack.","acceptance_criteria":"A/B at equal budget (harbor+maple, 3 seeds): archive best hard-fails \u003c= default-stack best on mean; archive demonstrably contains the stepping stone for at least one accepted valley-crossing (traceable lineage)","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-02T09:16:00Z","created_by":"Bruno Postle","updated_at":"2026-08-02T09:16:00Z","dependencies":[{"issue_id":"homemaker-py-2g7.10","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-02T10:15:59Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-2g7.10","depends_on_id":"homemaker-py-2g7.4","type":"blocks","created_at":"2026-08-02T10:15:59Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-2g7.8","title":"LLM operator synthesis (AlphaEvolve-style): evolve mutation-operator code against the A/B harness","description":"Second LLM role, after the repair operator proves the plumbing: let the LLM propose new OPERATOR CODE (python functions with the mutate_* signature) and evaluate candidates with the exact experiment discipline DESIGN.md already enforces (control reproduces baseline, 3 seeds, 20k evals, verdict). The project's ledger of 20+ operator experiments with verdicts is unusually good few-shot material: feed it the §11-§13 history so it learns what already failed (niching, grading, annealing...) and why. Sandbox the generated code; acceptance purely empirical via the harness. This is compute-hungry — schedule after the shape-curve DP lands so each A/B is cheap.","acceptance_criteria":"one synthesized operator survives the standard 3-seed A/B gate on harbor or maple (mean fails strictly better, control reproduces baseline)","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-02T09:15:56Z","created_by":"Bruno Postle","updated_at":"2026-08-02T09:15:56Z","dependencies":[{"issue_id":"homemaker-py-2g7.8","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-02T10:15:56Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-2g7.8","depends_on_id":"homemaker-py-2g7.7","type":"blocks","created_at":"2026-08-02T10:15:56Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-2g7.8","title":"LLM operator synthesis (AlphaEvolve-style): evolve mutation-operator code against the A/B harness","description":"Second LLM role, after the repair operator proves the plumbing: let the LLM propose new OPERATOR CODE (python functions with the mutate_* signature) and evaluate candidates with the exact experiment discipline DESIGN.md already enforces (control reproduces baseline, 3 seeds, 20k evals, verdict). The project's ledger of 20+ operator experiments with verdicts is unusually good few-shot material: feed it the §11-§13 history so it learns what already failed (niching, grading, annealing...) and why. Sandbox the generated code; acceptance purely empirical via the harness. This is compute-hungry — schedule after the shape-curve DP lands so each A/B is cheap.","acceptance_criteria":"one synthesized operator survives the standard 3-seed A/B gate on harbor or maple (mean fails strictly better, control reproduces baseline)","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-02T09:15:56Z","created_by":"Bruno Postle","updated_at":"2026-08-02T09:15:56Z","dependencies":[{"issue_id":"homemaker-py-2g7.8","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-02T10:15:56Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-2g7.8","depends_on_id":"homemaker-py-2g7.7","type":"blocks","created_at":"2026-08-02T10:15:56Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||||
|
|
@ -123,27 +127,27 @@
|
||||||
{"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":"island-model-psk-14-is-a-null-priming","value":"Island model (psk, §14) is a NULL: priming a population from N converged independent elites + crossover-heavy migration does not beat best-of-N at equal total budget (maple island 124 vs control 116). The child_probe instrument shows WHY: area-matched crossover across independently-converged elites almost never synthesizes (1-3 of ~64 children beat the better parent, max drop 2-5) because the slicing encoding is non-canonical (9gp), so splices are disruptive not combinatorial. Search-machinery null #3 after graded-objective and niching/restarts; residual stays geometry/shape-bound."}
|
|
||||||
{"_type":"memory","key":"warm-x0-initialization-bug-pattern-when-a-topology","value":"warm_x0 initialization bug pattern: when a topology operator explicitly sets division ratios on a newly-created node (e.g. compound_fix sets node.division=[0.25,0.25] for t3), parent.ratios has no entry for that node (it was a leaf). warm_x0 defaults it to 0.5, corrupting the inner loop's starting point and making the operator invisible to lex comparison. Fix: only propagate child ratios for nodes where the parent node was NOT already divided; stale hidden nodes revealed by structural mutations (swap flipping b.below) must NOT contribute their pre-writeback values. See driver.py lines 259-267 (fixed 2026-06-14)."}
|
|
||||||
{"_type":"memory","key":"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":"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":"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":"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":"adjacency-in-binary-slicing-tree-is-structural-not","value":"Adjacency in binary slicing tree is structural, not geometric: the inner-loop NM cannot fix topological adjacency failures. Two paths exist: (1) tree-sibling adjacency — a node is adjacent to its sibling in the tree; (2) cross-zone geometric adjacency — leaves from different subtrees that happen to share a boundary. Staircase/adjacency fails require a topology mutation that changes which nodes are siblings or which zones touch. This was proved empirically on programme-house: staircase fail from rot=0 layout could not be fixed by NM but was fixed by level_retype creating a two-C topology (2026-06-14/15)."}
|
|
||||||
{"_type":"memory","key":"ld2-13-6-interior-o-seed-diagnostic-all","value":"ld2/§13.6 interior-O seed diagnostic: ALL crinkliness fails in the constructed bal+share seed are UNDER-exposed (crink\u003c0.62, landlocked rooms with no facade + no uncovered-O neighbour) — zero over-exposed sliver fails. So the erc crinkliness residual is genuine under-daylighting, validating the interior light-well premise. Default outside_divisor=6 was too sparse (null: harbor 147-\u003e142, crinkliness even rose). odiv=3 is the seed-optimal joint setting: harbor seed fails 147-\u003e129 (-18), maple 219-\u003e206 (-14), landlocked fails drop, at cost of more leaves (harbor +4, maple +8). Because it ADDS leaves it carries the §13.4 wash-out risk; A/B to convergence pending."}
|
|
||||||
{"_type":"memory","key":"never-use-corpus-filenames-candidate-001-dom-candidate","value":"Never use corpus filenames (candidate-001.dom, candidate-002.dom, generated.dom, init.dom, etc.) as --output targets when running experiments. These are test fixtures. Always write experimental outputs to scratch/ or a timestamped path. Lesson from 2026-06-14: warm-start runs overwrote candidate-001/002.dom and broke graph tests."}
|
|
||||||
{"_type":"memory","key":"programme-house-optimisation-result-2026-06-14-15","value":"Programme-house optimisation result (2026-06-14/15): best achievable is 1 fail (l1 wrong level, score ~0.005). 0 fails is geometrically impossible: l1 (min 27m²) must occupy ll (~23m²) at level 0, which eliminates the t3-adj-C provider; dividing ll into lll(l1)+llr(C) gives llr proportion ~6:1 (fails). Python memetic optimizer achieves 1 fail in 50k evals vs Perl optimiser's 2-3 fails. Winning topology: TWO C nodes at level 0 — ll(C) for t3-adj-C via geometric contact, rl(C) for staircase via tree-sibling adjacency to rrr(O). Best .dom: scratch/from-warmstart-fixed.dom and scratch/from-compound3-fixed.dom."}
|
|
||||||
{"_type":"memory","key":"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":"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":"collapse-global-94g-and-any-label-usage-optimisation","value":"collapse_global (94g) and any label/usage optimisation CANNOT fix geometry-intrinsic fails. The harbor-house 15-fail best layout contains long-thin cells that are useless whatever room usage is assigned — their width/proportion/crinkliness fails are shape-bound, not label slack. Two consequences: (1) do not over-claim collapse gains — only ~2-3 of that layout's fails are reclaimable relabel slack, the rest are geometry- or building-level bound; (2) the threshold objective must not be tuned to 'pass' a degenerate cell via a permissive room type — a metric-pass on a physically useless space is gaming, not a fix. Real remedies for these are geometry/topology search (cell shape) and circulation placement, filed separately, not the collapse."}
|
|
||||||
{"_type":"memory","key":"urb-fitness-bug-found-fixed-2026-06-12","value":"Urb fitness bug found+fixed 2026-06-12 (patch in /home/bruno/src/urb, uncommitted): ProgrammeDriven.pm ratio_o/ratio_type grepped case-insensitively over the ratios hash and took the FIRST key — nondeterministic (x4.5 score swings) for designs with mixed-case type classes (both 'c' circulation and 'C' covered). Fixed to SUM the class (matches Is_Circulation//Is_Outside semantics); 35/35 corpus scores unchanged. CRITICAL for homemaker-py-3y7/gnw: the native port must implement class-SUM ratios. Building.pm has the same unpatched pattern (site-driven path, not used by our oracle). Also: the memetic search reward-hacked this bug before the fix — search results predating it are noise artifacts."}
|
|
||||||
{"_type":"memory","key":"cli-tool-style-prefer-python-m-homemaker-module","value":"CLI tool style: prefer python -m homemaker.module --parameters pattern, installable via pip install -e . with pyproject.toml entry_points. Not standalone bin/ scripts."}
|
|
||||||
{"_type":"memory","key":"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":"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":"correction-to-urb-fitness-bug-memory-bruno-2026","value":"CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase."}
|
|
||||||
{"_type":"memory","key":"experiment-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":"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-3l6-fix-leaf-sharing-evolve-runs","value":"homemaker-py-3l6 fix: leaf-sharing evolve runs now auto-finish before write via driver.polish_finish — unfold_shared_leaves() then a warm-started leaf_sharing=False polish search (--polish-budget, default budget//2). Makes the written .dom honest under canonical homemaker-fitness (internal==canonical when leaf_sharing off). Interrupt path forces polish_budget=0 (unfold+rescore only). This is yaa's unfold-then-polish, made automatic; Schedule B annealing is still kpu."}
|
|
||||||
{"_type":"memory","key":"run-to-run-reproducibility-in-homemaker-layout-serial","value":"Run-to-run reproducibility in homemaker-layout: serial search (workers=1) is byte-for-byte deterministic; parallel (workers\u003e1) is now deterministic too AFTER fixing driver._run_batch to admit futures in submission order (was as_completed/completion order, bug xcy). Reproducibility holds only for a FIXED worker count — serial vs parallel differ because children-per-iteration is 1 vs n_workers (different batch granularity), which is expected, not a bug. The constructive seeder was NEVER nondeterministic: _assign_adjacency_aware has unique idx tiebreaks; comparing topologies with Python builtin hash() of the signature STRING is invalid (PYTHONHASHSEED salts str hashing per process) — use a stable hash (sha1) or genome.signature equality."}
|
{"_type":"memory","key":"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":"collapse-global-s-jacobi-adjacency-relaxation-homemaker-py","value":"collapse_global's Jacobi adjacency relaxation (homemaker-py-94g) is a synchronous per-round linear-assignment re-solve, which can 2-cycle indefinitely between two labellings that each satisfy ZERO adjacency requirements even though a permutation satisfying ALL of them exists -- proven on a minimal 4-cell chain (p1-q1-p2-q2, two disjoint adjacency pairs p1\u003c-\u003ep2/q1\u003c-\u003eq2) in test_two_opt_polish_escapes_jacobi_plateau. homemaker-py-9wi added Fitness._two_opt_adjacency_polish: a same-level pairwise-swap local search run after the Jacobi fixpoint, gated behind collapse_global(local_search=True) (default off, exposed as homemaker-collapse --local-search). Monotone by construction (a swap is kept only if it strictly increases total reward). Empirically on the 11 harbor-house evolved-*.dom/3m.dom/materialised-3M.dom layouts: 10 matched Jacobi-only exactly, 0 regressed, and evolved-anneal-3M.dom improved 21-\u003e19 fails (fixed a genuine mutual da1\u003c-\u003ek1 adjacency miss the Jacobi loop couldn't reach)."}
|
{"_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":"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":"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":"collapse-global-94g-and-any-label-usage-optimisation","value":"collapse_global (94g) and any label/usage optimisation CANNOT fix geometry-intrinsic fails. The harbor-house 15-fail best layout contains long-thin cells that are useless whatever room usage is assigned — their width/proportion/crinkliness fails are shape-bound, not label slack. Two consequences: (1) do not over-claim collapse gains — only ~2-3 of that layout's fails are reclaimable relabel slack, the rest are geometry- or building-level bound; (2) the threshold objective must not be tuned to 'pass' a degenerate cell via a permissive room type — a metric-pass on a physically useless space is gaming, not a fix. Real remedies for these are geometry/topology search (cell shape) and circulation placement, filed separately, not the collapse."}
|
||||||
|
{"_type":"memory","key":"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":"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":"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":"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":"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":"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":"collapse-global-s-jacobi-adjacency-relaxation-homemaker-py","value":"collapse_global's Jacobi adjacency relaxation (homemaker-py-94g) is a synchronous per-round linear-assignment re-solve, which can 2-cycle indefinitely between two labellings that each satisfy ZERO adjacency requirements even though a permutation satisfying ALL of them exists -- proven on a minimal 4-cell chain (p1-q1-p2-q2, two disjoint adjacency pairs p1\u003c-\u003ep2/q1\u003c-\u003eq2) in test_two_opt_polish_escapes_jacobi_plateau. homemaker-py-9wi added Fitness._two_opt_adjacency_polish: a same-level pairwise-swap local search run after the Jacobi fixpoint, gated behind collapse_global(local_search=True) (default off, exposed as homemaker-collapse --local-search). Monotone by construction (a swap is kept only if it strictly increases total reward). Empirically on the 11 harbor-house evolved-*.dom/3m.dom/materialised-3M.dom layouts: 10 matched Jacobi-only exactly, 0 regressed, and evolved-anneal-3M.dom improved 21-\u003e19 fails (fixed a genuine mutual da1\u003c-\u003ek1 adjacency miss the Jacobi loop couldn't reach)."}
|
||||||
|
{"_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":"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":"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":"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-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":"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":"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":"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":"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":"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":"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":"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."}
|
||||||
|
|
|
||||||
100
DESIGN.md
100
DESIGN.md
|
|
@ -4238,3 +4238,103 @@ open: tracing an actual harbor-house or programme-house human plan in
|
||||||
Inkscape and composing/scoring it — needs the user's time, tracked as
|
Inkscape and composing/scoring it — needs the user's time, tracked as
|
||||||
follow-up under `2g7.1`. `2g7.2` (objective calibration against the human
|
follow-up under `2g7.1`. `2g7.2` (objective calibration against the human
|
||||||
reference) stays blocked on that trace landing.
|
reference) stays blocked on that trace landing.
|
||||||
|
|
||||||
|
### 37.4 `homemaker-py-6xh` shape-curve DP wired as NM warm-start — measured 2026-08-03, ACCEPTANCE: PARTIAL
|
||||||
|
|
||||||
|
**What was built.** `2g7.4`'s validated shape-curve DP (§37.2) was still a
|
||||||
|
reference-only spike (`experiments/shapecurve_spike.py`), not wired into the
|
||||||
|
product. This session: (1) promoted it verbatim (plus one bugfix, below) into
|
||||||
|
`src/homemaker_layout/shapecurve.py`; (2) added `shapecurve.eligible(root,
|
||||||
|
leaf_sharing, superpose, max_share, multi_use)`, gating on the DP's actual
|
||||||
|
validated scope — single storey (`len(dom.levels(root)) == 1`) and none of
|
||||||
|
`leaf_sharing`/`superpose`/`max_share`/`multi_use` (none of which
|
||||||
|
`leaf_constraints` models); (3) wired it into `driver._evaluate` as an NM
|
||||||
|
warm-start only: when `shapecurve_warmstart=True`, the caller supplied no
|
||||||
|
explicit `x0` (never override a real Lamarckian warm-start), and the
|
||||||
|
topology is eligible, `shapecurve.solve(root, fit)` writes an exact
|
||||||
|
shape-feasible ratio point onto the tree in place *before*
|
||||||
|
`innerloop.optimise` runs; `optimise`'s existing `x0=None` behaviour (read
|
||||||
|
the tree's current ratios) then picks it up unchanged. On ineligible or
|
||||||
|
DP-infeasible, the tree is left exactly as the cold/proportion-aware seed
|
||||||
|
left it — no new false-negative risk, because nothing is pruned by this
|
||||||
|
change. Threaded through `driver.search` and exposed as
|
||||||
|
`homemaker-evolve --shapecurve-warmstart` (off by default, matching every
|
||||||
|
other experimental search toggle in `evolve.py`). Deliberately **not**
|
||||||
|
built this session (tracked as follow-up beads under `2g7`, see below): the
|
||||||
|
DP-exact hard pre-filter (replacing `predicted_shape_fails`' heuristic
|
||||||
|
threshold), multi-storey (`below`-link) support, `leaf_sharing`/`co_type`
|
||||||
|
modelling, and the true skew-quad polygon algebra to remove the ~7-12%
|
||||||
|
rectangle-approximation error §37.2 already quantified.
|
||||||
|
|
||||||
|
**Bug caught promoting the spike: `realise()` leaked `numpy.float64` into
|
||||||
|
`division`.** `_interp_range`'s interpolation branch computes on a numpy
|
||||||
|
grid, so `t = wl / w` in `realise()` is a `numpy.float64` whenever that
|
||||||
|
branch fires (common — any non-grid-exact point) rather than a plain Python
|
||||||
|
float. `experiments/validate_shapecurve.py` never caught this because it
|
||||||
|
only ever scored the in-memory tree directly, never round-tripped through
|
||||||
|
`dom.dumps` (`yaml.safe_dump` cannot represent `numpy.float64` and raises
|
||||||
|
`RepresenterError`). This session's `tests/test_shapecurve.py` does
|
||||||
|
round-trip (`dom.dumps`/`dom.load` after `solve()`), caught it immediately,
|
||||||
|
and the fix is a one-line `float()` cast on both list elements of
|
||||||
|
`node.division`. This means the shipped `experiments/shapecurve_spike.py`
|
||||||
|
copy silently carries this latent bug too — harmless for the validation
|
||||||
|
harness's own in-memory comparisons, but would break the moment anyone
|
||||||
|
tried to write its output to a `.dom` file.
|
||||||
|
|
||||||
|
**A/B** (`experiments/ab_shapecurve_warmstart.py`, `examples/harbor-house-l0`
|
||||||
|
— the DP's own single-storey validated benchmark; the full multi-storey
|
||||||
|
`examples/harbor-house` is out of scope until the multi-storey follow-up
|
||||||
|
lands): `driver.search(..., leaf_sharing=False)` off vs on, budget=2000,
|
||||||
|
seeds 0-4, same-seed paired runs:
|
||||||
|
|
||||||
|
| seed | off hard | off soft | off fitness | on hard | on soft | on fitness |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 0 | 3 | 13 | 1.222e-08 | 3 | 13 | 1.222e-08 |
|
||||||
|
| 1 | 4 | 13 | 1.234e-08 | 4 | 8 | 6.556e-08 |
|
||||||
|
| 2 | 6 | 15 | 6.95e-10 | 5 | 13 | 9.31e-09 |
|
||||||
|
| 3 | 3 | 18 | 3.954e-10 | 5 | 9 | 2.522e-09 |
|
||||||
|
| 4 | 6 | 17 | 5.6e-11 | 6 | 17 | 5.6e-11 |
|
||||||
|
| **mean** | **4.400** | **15.200** | **5.14e-09** | **4.600** | **12.000** | **1.79e-08** |
|
||||||
|
|
||||||
|
Wall-clock is identical (56.4s vs 57.1s mean, as expected — same budget, the
|
||||||
|
DP adds one cheap solve per eligible child). This run used the default flat
|
||||||
|
comparator (`use_tiers=False`, i.e. `admit()`'s selection pressure is total
|
||||||
|
fail count, not hard/soft-tiered), so the metric that actually drove which
|
||||||
|
children survived is **mean total fails**: OFF 4.4+15.2=19.6 vs ON
|
||||||
|
4.6+12.0=16.6, a ~15% reduction, tracking the ~3.5x mean-fitness improvement.
|
||||||
|
Mean **hard**-fail count alone ticked up slightly (4.6 vs 4.4) — driven
|
||||||
|
entirely by seed 3 (3→5); seed 2 moved the other way (6→5), seeds 0/4 tied.
|
||||||
|
At n=5 seeds this is noise-dominated, not a signal either direction.
|
||||||
|
|
||||||
|
**ACCEPTANCE: PARTIAL — net positive on the metric that drives selection
|
||||||
|
(total fails / fitness), inconclusive on hard fails specifically.** The
|
||||||
|
warm-start is unambiguously safe (verified by the off/on-parity test,
|
||||||
|
`test_shapecurve_warmstart_off_matches_baseline`) and measurably improves
|
||||||
|
soft-fail/fitness convergence on its validated single-storey envelope at
|
||||||
|
this budget/seed-count. It is not yet the "evals to N hard fails" race the
|
||||||
|
`6xh` bead framed as the target metric — reaching that needs either a larger
|
||||||
|
seed count (this A/B's hard-fail delta is within noise at n=5) or the
|
||||||
|
DP-exact hard pre-filter (`homemaker-py-wkh`) doing more than warm-starting.
|
||||||
|
Also unresolved: this envelope (single storey, no sharing) excludes most
|
||||||
|
real programmes by default (`leaf_sharing` defaults `True` in
|
||||||
|
`driver.search`; `programme-house`/`harbor-house` both require ≥2 storeys),
|
||||||
|
so today's win applies only when a caller explicitly opts into both
|
||||||
|
`leaf_sharing=False` and a single-storey seed — the multi-storey
|
||||||
|
(`homemaker-py-koo`) and leaf-sharing (`homemaker-py-tym`) follow-ups are
|
||||||
|
what make this apply to the programmes the search actually runs on day to
|
||||||
|
day.
|
||||||
|
|
||||||
|
**Verification.** `tests/test_shapecurve.py` (4 tests): eligibility guard
|
||||||
|
correctness (multi-storey, each of leaf_sharing/superpose/max_share/
|
||||||
|
multi_use independently disqualifying); a small feasible topology's
|
||||||
|
DP-realised ratios round-trip `dom.dumps`/`dom.load` and independently score
|
||||||
|
zero shape fails via the real `fitness.Fitness`; an obviously-oversized
|
||||||
|
topology (60 leaves on harbor-house-l0's plot) is correctly infeasible;
|
||||||
|
determinism. `tests/test_driver.py` (+3 tests): off/on parity when the flag
|
||||||
|
is off; `shapecurve.solve` is invoked (and its written ratio is visible to
|
||||||
|
`innerloop.optimise`) exactly when eligible; `shapecurve.solve` is never
|
||||||
|
invoked on a multi-storey seed. Full suite: 388 passed. Manual CLI smoke
|
||||||
|
test: `homemaker-evolve init.dom --programme-dir . --no-leaf-sharing
|
||||||
|
--shapecurve-warmstart` in `examples/harbor-house-l0` runs to completion and
|
||||||
|
the emitted `.dom` scores cleanly with `homemaker-fitness` (score matches
|
||||||
|
the run's own reported best).
|
||||||
|
|
|
||||||
70
experiments/ab_shapecurve_warmstart.py
Normal file
70
experiments/ab_shapecurve_warmstart.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
"""A/B: does shapecurve-DP NM warm-start beat today's cold/proportion-aware
|
||||||
|
start on wall-clock/evals-to-fail-count, on the real ``driver.search`` loop
|
||||||
|
(homemaker-py-6xh, DESIGN.md §37.4)?
|
||||||
|
|
||||||
|
Scoped to the DP's validated envelope (DESIGN.md §37.2): single storey, no
|
||||||
|
leaf_sharing/superpose/max_share/multi_use. ``examples/harbor-house-l0`` is
|
||||||
|
the single-storey de-risk variant of harbor-house built for exactly this
|
||||||
|
purpose (storey_minimum=1) -- the full multi-storey ``examples/harbor-house``
|
||||||
|
is out of scope until the multi-storey DP follow-up lands.
|
||||||
|
|
||||||
|
Metric: mean (n_hard, n_soft, fitness) of ``driver.search``'s best individual
|
||||||
|
at a FIXED budget, across several seeds -- same format as §37.1's tiered-
|
||||||
|
comparator A/B table, not an evals-to-zero race (harbor-house-l0's small
|
||||||
|
programme does not reliably reach 0 hard fails within a script-scale budget
|
||||||
|
across all seeds, so a fixed-budget comparison is the fair, reproducible one).
|
||||||
|
|
||||||
|
Usage: python experiments/ab_shapecurve_warmstart.py [budget] [n_seeds]
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from homemaker_layout import dom, driver
|
||||||
|
|
||||||
|
PROGRAMME_DIR = Path(__file__).parent.parent / "examples" / "harbor-house-l0"
|
||||||
|
|
||||||
|
|
||||||
|
def run_arm(seed_root: dom.Node, budget: int, seed: int, warmstart: bool):
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
r = driver.search(
|
||||||
|
seed_root, PROGRAMME_DIR, budget=budget, pop_size=8, child_budget=80,
|
||||||
|
seed_budget=200, seed=seed, leaf_sharing=False,
|
||||||
|
shapecurve_warmstart=warmstart,
|
||||||
|
)
|
||||||
|
elapsed = time.perf_counter() - t0
|
||||||
|
return r, elapsed
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
budget = int(sys.argv[1]) if len(sys.argv) > 1 else 4000
|
||||||
|
n_seeds = int(sys.argv[2]) if len(sys.argv) > 2 else 8
|
||||||
|
|
||||||
|
seed_root = dom.load(str(PROGRAMME_DIR / "init.dom"))
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for seed in range(n_seeds):
|
||||||
|
off, t_off = run_arm(seed_root, budget, seed, warmstart=False)
|
||||||
|
on, t_on = run_arm(seed_root, budget, seed, warmstart=True)
|
||||||
|
rows.append((seed, off.best.n_hard, off.best.n_soft, off.best.fitness, t_off,
|
||||||
|
on.best.n_hard, on.best.n_soft, on.best.fitness, t_on))
|
||||||
|
print(f"seed {seed}: off hard={off.best.n_hard} soft={off.best.n_soft} "
|
||||||
|
f"fit={off.best.fitness:.4g} {t_off:.1f}s | "
|
||||||
|
f"on hard={on.best.n_hard} soft={on.best.n_soft} "
|
||||||
|
f"fit={on.best.fitness:.4g} {t_on:.1f}s", flush=True)
|
||||||
|
|
||||||
|
n = len(rows)
|
||||||
|
mean = lambda idx: sum(r[idx] for r in rows) / n
|
||||||
|
print()
|
||||||
|
print(f"budget={budget} n_seeds={n_seeds} programme={PROGRAMME_DIR}")
|
||||||
|
print(f"OFF: mean hard={mean(1):.3f} soft={mean(2):.3f} fitness={mean(3):.6g} "
|
||||||
|
f"wall={mean(4):.1f}s")
|
||||||
|
print(f"ON : mean hard={mean(5):.3f} soft={mean(6):.3f} fitness={mean(7):.6g} "
|
||||||
|
f"wall={mean(8):.1f}s")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -34,7 +34,7 @@ from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from . import dom, fitness, genome, innerloop, operators, programme
|
from . import dom, fitness, genome, innerloop, operators, programme, shapecurve
|
||||||
|
|
||||||
_CHILD_INNER_KW: dict = {}
|
_CHILD_INNER_KW: dict = {}
|
||||||
|
|
||||||
|
|
@ -173,7 +173,8 @@ def _evaluate(root: dom.Node, programme_dir, urb_root, x0, budget, inner_kw,
|
||||||
max_share: int | None = None,
|
max_share: int | None = None,
|
||||||
conn_grade: bool = False,
|
conn_grade: bool = False,
|
||||||
collapse_insearch: bool = True,
|
collapse_insearch: bool = True,
|
||||||
multi_use: bool = False) -> tuple[Individual, int]:
|
multi_use: bool = False,
|
||||||
|
shapecurve_warmstart: bool = False) -> 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
|
||||||
|
|
@ -183,6 +184,18 @@ def _evaluate(root: dom.Node, programme_dir, urb_root, x0, budget, inner_kw,
|
||||||
# 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, max_share, conn_grade,
|
overrides = _overrides_for(leaf_sharing, superpose, max_share, conn_grade,
|
||||||
collapse_insearch, multi_use)
|
collapse_insearch, multi_use)
|
||||||
|
# §37.4 shape-curve DP warm-start (homemaker-py-6xh, DESIGN.md §37.2/§37.4):
|
||||||
|
# when eligible (single storey, no leaf_sharing/superpose/max_share/multi_use
|
||||||
|
# — none of which the DP models) and no caller-supplied x0 (never override an
|
||||||
|
# explicit Lamarckian warm-start), solve for an exact shape-feasible ratio
|
||||||
|
# point and write it onto the tree in place. `x0=None` below then picks it up
|
||||||
|
# as the inner loop's start point. On infeasible or ineligible, `root` is left
|
||||||
|
# untouched — falls through to today's cold/proportion-aware start exactly.
|
||||||
|
if shapecurve_warmstart and x0 is None and shapecurve.eligible(
|
||||||
|
root, leaf_sharing, superpose, max_share, multi_use):
|
||||||
|
shapecurve.solve(root, _fitness_for(
|
||||||
|
str(programme_dir), leaf_sharing, superpose, max_share,
|
||||||
|
conn_grade, collapse_insearch, multi_use))
|
||||||
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)),
|
||||||
|
|
@ -270,6 +283,7 @@ def search(
|
||||||
max_share: int | None = None,
|
max_share: int | None = None,
|
||||||
seed_pop: list[dom.Node] | None = None,
|
seed_pop: list[dom.Node] | None = None,
|
||||||
collapse_insearch: bool = True,
|
collapse_insearch: bool = True,
|
||||||
|
shapecurve_warmstart: bool = False,
|
||||||
) -> 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``
|
||||||
|
|
@ -519,7 +533,7 @@ def search(
|
||||||
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, max_share, conn_grade,
|
mx, best_nf, leaf_sharing, superpose, max_share, conn_grade,
|
||||||
collapse_insearch, multi_use)
|
collapse_insearch, multi_use, shapecurve_warmstart)
|
||||||
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:
|
||||||
|
|
@ -605,7 +619,8 @@ def search(
|
||||||
max_share=max_share,
|
max_share=max_share,
|
||||||
conn_grade=conn_grade,
|
conn_grade=conn_grade,
|
||||||
collapse_insearch=collapse_insearch,
|
collapse_insearch=collapse_insearch,
|
||||||
multi_use=multi_use)
|
multi_use=multi_use,
|
||||||
|
shapecurve_warmstart=shapecurve_warmstart)
|
||||||
n_evals += used
|
n_evals += used
|
||||||
admit(seed_ind, pop)
|
admit(seed_ind, pop)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,17 @@ def _parse_args(argv=None) -> argparse.Namespace:
|
||||||
"bounds) may be fused onto one leaf at construction time, "
|
"bounds) may be fused onto one leaf at construction time, "
|
||||||
"surviving unchanged into the output (unlike --superpose's "
|
"surviving unchanged into the output (unlike --superpose's "
|
||||||
"per-eval collapse to a single usage) (default: off)")
|
"per-eval collapse to a single usage) (default: off)")
|
||||||
|
p.add_argument("--shapecurve-warmstart", dest="shapecurve_warmstart",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
default=_env_bool("HOMEMAKER_SHAPECURVE_WARMSTART", False),
|
||||||
|
help="homemaker-py-6xh (DESIGN.md §37.2/§37.4): warm-start "
|
||||||
|
"each child's inner-loop ratio search from the exact "
|
||||||
|
"Otten/Stockmeyer shape-curve DP solution instead of "
|
||||||
|
"the proportion-aware target-geometry seed, when the "
|
||||||
|
"topology is eligible (single storey, no leaf-sharing/"
|
||||||
|
"superpose/max-share/multi-use — none of which the DP "
|
||||||
|
"models). Falls through to today's start unchanged when "
|
||||||
|
"ineligible or DP-infeasible (default: off)")
|
||||||
p.add_argument("--conn-grade", dest="conn_grade",
|
p.add_argument("--conn-grade", dest="conn_grade",
|
||||||
action=argparse.BooleanOptionalAction,
|
action=argparse.BooleanOptionalAction,
|
||||||
default=_env_bool("HOMEMAKER_CONN_GRADE", False),
|
default=_env_bool("HOMEMAKER_CONN_GRADE", False),
|
||||||
|
|
@ -235,6 +246,7 @@ def main(argv=None) -> int:
|
||||||
print(f"bridge circulation : {args.bridge_circulation}", file=sys.stderr)
|
print(f"bridge circulation : {args.bridge_circulation}", file=sys.stderr)
|
||||||
print(f"ruin recreate : {args.ruin_recreate}", file=sys.stderr)
|
print(f"ruin recreate : {args.ruin_recreate}", file=sys.stderr)
|
||||||
print(f"collapse in-search : {args.collapse_insearch}", file=sys.stderr)
|
print(f"collapse in-search : {args.collapse_insearch}", file=sys.stderr)
|
||||||
|
print(f"shapecurve warmstart : {args.shapecurve_warmstart}", 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
|
anneal_ladder = None
|
||||||
|
|
@ -290,6 +302,7 @@ def main(argv=None) -> int:
|
||||||
enable_bridge_circulation=args.bridge_circulation,
|
enable_bridge_circulation=args.bridge_circulation,
|
||||||
enable_ruin_recreate=args.ruin_recreate,
|
enable_ruin_recreate=args.ruin_recreate,
|
||||||
collapse_insearch=args.collapse_insearch,
|
collapse_insearch=args.collapse_insearch,
|
||||||
|
shapecurve_warmstart=args.shapecurve_warmstart,
|
||||||
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
|
_finish_sharing = args.leaf_sharing
|
||||||
|
|
|
||||||
378
src/homemaker_layout/shapecurve.py
Normal file
378
src/homemaker_layout/shapecurve.py
Normal file
|
|
@ -0,0 +1,378 @@
|
||||||
|
"""Otten/Stockmeyer shape-curve DP: exact size/width/proportion feasibility
|
||||||
|
for a frozen slicing-tree topology, in one bottom-up pass.
|
||||||
|
|
||||||
|
Promoted from ``experiments/shapecurve_spike.py`` (homemaker-py-2g7.4,
|
||||||
|
DESIGN.md §37.2 — validated PASS: 99% agreement vs shape-fail-minimising NM
|
||||||
|
on harbor-house-l0, 0 false negatives, ~97x speedup) for use as
|
||||||
|
``driver._evaluate``'s NM warm-start (homemaker-py-6xh, DESIGN.md §37.4).
|
||||||
|
|
||||||
|
The inner loop answers "does some equal-offset ratio assignment clear the
|
||||||
|
size/width/proportion FAIL_THRESHOLD for every leaf" by an 80-200-eval
|
||||||
|
Nelder-Mead search per topology. This DP answers the same question exactly:
|
||||||
|
each leaf's feasible (width, height) region is bounded by an area hyperbola
|
||||||
|
(``quality_size``), a min-width line (``quality_width``), and an aspect-ratio
|
||||||
|
wedge (``quality_proportion``) — FAIL_THRESHOLD-inversions of the Gaussian/
|
||||||
|
clipped-Gaussian factors in ``fitness.py`` (see ``leaf_constraints``). These
|
||||||
|
per-leaf regions compose bottom-up through the slicing tree: a node's cut
|
||||||
|
ALWAYS sums its two children's contributions into the node's own "w"
|
||||||
|
(edge0+edge2) dimension, with "h" (edge1+edge3) the shared/cross dimension —
|
||||||
|
a fixed convention of ``geometry.py``'s division formula, no per-node
|
||||||
|
ambiguity. The only variable is which of a CHILD's own (w, h) plays which
|
||||||
|
role, an EXACT function of that child's ``rotation`` parity (``_child_contrib``).
|
||||||
|
|
||||||
|
Explicit scope (see ``eligible``, and DESIGN.md §37.2/§37 point 2):
|
||||||
|
|
||||||
|
* Only size/width/proportion is modelled — crinkliness, access, adjacency,
|
||||||
|
level/vertical connectivity are graph/topology terms, not per-leaf shape.
|
||||||
|
* Every quad (leaf or internal) is approximated by a rectangle with the
|
||||||
|
same edge-length-derived (w, h) as ``geometry.aspect`` uses —
|
||||||
|
``(edge0+edge2)/2`` and ``(edge1+edge3)/2`` — exact only for a true
|
||||||
|
rectangle/parallelogram. Rotation-invariant by construction (unlike a
|
||||||
|
global-axis bounding box); a residual ~7-12% rectangle-vs-true-skewed-
|
||||||
|
quad approximation error remains, quantified in DESIGN.md §37.2.
|
||||||
|
* Composition (which of a child's local w/h sums into its parent's w) is an
|
||||||
|
exact algebraic identity determined purely by ``child.rotation % 2`` (see
|
||||||
|
``_child_contrib``) — not measured or approximated.
|
||||||
|
* ``leaf_sharing``/``co_type`` (multi-use leaves) target-adjustment is NOT
|
||||||
|
modelled — ``leaf_constraints`` uses each leaf's own type's base params
|
||||||
|
only. ``eligible`` excludes runs using either.
|
||||||
|
* Only a single storey is modelled: ``solve`` writes ``division`` on every
|
||||||
|
divided node under the given level root unconditionally, with no notion
|
||||||
|
of upper-storey ``below``-inherited (wall-stacked) fixed splits. Calling
|
||||||
|
it on a multi-storey tree would corrupt wall-stacking. ``eligible``
|
||||||
|
excludes multi-storey topologies (``len(dom.levels(root)) > 1``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
import warnings
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from homemaker_layout import dom as dom_mod
|
||||||
|
from homemaker_layout import geometry
|
||||||
|
|
||||||
|
# sqrt(2*ln(10)): FAIL_THRESHOLD=0.1 inversion of a unit-height Gaussian,
|
||||||
|
# gaussian(x,1,target,sigma) >= 0.1 <=> |x-target| <= K*sigma.
|
||||||
|
_K = math.sqrt(2.0 * math.log(10.0))
|
||||||
|
|
||||||
|
Interval = tuple[float, float] | None # None = infeasible
|
||||||
|
|
||||||
|
|
||||||
|
def eligible(root: dom_mod.Node, leaf_sharing: bool = False,
|
||||||
|
superpose: bool = False, max_share: int | None = None,
|
||||||
|
multi_use: bool = False) -> bool:
|
||||||
|
"""Is ``root`` inside this DP's validated scope for ``solve``?
|
||||||
|
|
||||||
|
Single storey only (no ``below``-inherited wall-stacking to model) and
|
||||||
|
none of ``leaf_sharing``/``superpose``/``max_share``/``multi_use`` (none
|
||||||
|
of which ``leaf_constraints`` models). See the module docstring.
|
||||||
|
"""
|
||||||
|
return (len(dom_mod.levels(root)) == 1
|
||||||
|
and not leaf_sharing and not superpose
|
||||||
|
and max_share is None and not multi_use)
|
||||||
|
|
||||||
|
|
||||||
|
def _interval_add(a: Interval, b: Interval) -> Interval:
|
||||||
|
if a is None or b is None:
|
||||||
|
return None
|
||||||
|
return (a[0] + b[0], a[1] + b[1])
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Per-leaf feasible region (exact closed form; FAIL_THRESHOLD inversion of
|
||||||
|
# fitness.py's quality_size/quality_width/quality_proportion).
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LeafBounds:
|
||||||
|
amin: float
|
||||||
|
amax: float
|
||||||
|
wmin: float
|
||||||
|
rmax: float # max aspect ratio (>= 1)
|
||||||
|
|
||||||
|
def h_range(self, w: float) -> Interval:
|
||||||
|
if w < self.wmin - 1e-12:
|
||||||
|
return None
|
||||||
|
lo = self.wmin
|
||||||
|
if self.amin > 0:
|
||||||
|
lo = max(lo, self.amin / w)
|
||||||
|
lo = max(lo, w / self.rmax)
|
||||||
|
hi = w * self.rmax
|
||||||
|
if self.amax < math.inf:
|
||||||
|
hi = min(hi, self.amax / w)
|
||||||
|
if lo > hi + 1e-12:
|
||||||
|
return None
|
||||||
|
return (lo, hi)
|
||||||
|
|
||||||
|
def w_range(self, h: float) -> Interval:
|
||||||
|
# symmetric in (w, h) -- same box+hyperbola+wedge shape.
|
||||||
|
return self.h_range(h)
|
||||||
|
|
||||||
|
def range_grid(self, grid: np.ndarray) -> list[Interval]:
|
||||||
|
"""Vectorised ``h_range``/``w_range`` (symmetric) over a whole grid."""
|
||||||
|
lo = np.maximum(self.wmin, grid / self.rmax)
|
||||||
|
if self.amin > 0:
|
||||||
|
lo = np.maximum(lo, self.amin / grid)
|
||||||
|
hi = grid * self.rmax
|
||||||
|
if self.amax < math.inf:
|
||||||
|
hi = np.minimum(hi, self.amax / grid)
|
||||||
|
feasible = (grid >= self.wmin - 1e-12) & (lo <= hi + 1e-12)
|
||||||
|
return [(float(lo[i]), float(hi[i])) if feasible[i] else None for i in range(len(grid))]
|
||||||
|
|
||||||
|
|
||||||
|
def leaf_constraints(fit, leaf: dom_mod.Node) -> LeafBounds:
|
||||||
|
"""FAIL_THRESHOLD-inverted (amin, amax, wmin, rmax) for one leaf.
|
||||||
|
|
||||||
|
Mirrors the branching of ``Fitness.quality_size``/``quality_width``/
|
||||||
|
``quality_proportion`` (fitness.py) but returns the (target, sigma)-derived
|
||||||
|
hard bounds instead of evaluating a Gaussian against actual geometry.
|
||||||
|
Ignores leaf-sharing/co_type target adjustment (see module docstring).
|
||||||
|
"""
|
||||||
|
t0 = leaf.type[0].lower() if leaf.type else ""
|
||||||
|
|
||||||
|
# --- size -> (amin, amax) ---
|
||||||
|
if t0 in ("o", "s"):
|
||||||
|
amin, amax = 0.0, math.inf
|
||||||
|
else:
|
||||||
|
params = fit.conf("size_circulation") if t0 == "c" else fit.get_space_params(leaf.type, "size")
|
||||||
|
target, sigma = params[0], params[1]
|
||||||
|
# NB: quality_size's ``target > 0`` gate governs only the leaf-sharing/
|
||||||
|
# co_type k-scaling of (target, sigma) (not modelled here, see module
|
||||||
|
# docstring) -- the underlying gaussian(area, target, sigma) test
|
||||||
|
# always applies, including target==0 (e.g. size_circulation's [0.0,
|
||||||
|
# 14.0] default: a real one-sided "as small as possible" constraint,
|
||||||
|
# not "unconstrained").
|
||||||
|
amin, amax = max(0.0, target - _K * sigma), target + _K * sigma
|
||||||
|
|
||||||
|
# --- width -> wmin ---
|
||||||
|
if (
|
||||||
|
t0 in ("o", "s")
|
||||||
|
and not dom_mod.is_covered(leaf)
|
||||||
|
and not dom_mod.is_supported(leaf)
|
||||||
|
and dom_mod.level_of(leaf)
|
||||||
|
):
|
||||||
|
wmin = 0.0
|
||||||
|
else:
|
||||||
|
if t0 in ("o", "s"):
|
||||||
|
params = fit.conf("width_outside")
|
||||||
|
elif t0 == "c":
|
||||||
|
params = fit.conf("width_circulation")
|
||||||
|
else:
|
||||||
|
params = fit.get_space_params(leaf.type, "width")
|
||||||
|
target, sigma = params[0], params[1]
|
||||||
|
wmin = max(0.0, target - _K * sigma)
|
||||||
|
|
||||||
|
# --- proportion -> rmax ---
|
||||||
|
if t0 in ("o", "s"):
|
||||||
|
params = fit.conf("proportion_outside")
|
||||||
|
elif t0 == "c":
|
||||||
|
params = fit.conf("proportion_circulation")
|
||||||
|
else:
|
||||||
|
params = fit.get_space_params(leaf.type, "proportion")
|
||||||
|
target, sigma = params[0], params[1]
|
||||||
|
rmax = max(1.0 + 1e-9, target + _K * sigma)
|
||||||
|
|
||||||
|
return LeafBounds(amin=amin, amax=amax, wmin=wmin, rmax=rmax)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Local-edge-length dimensions + EXACT rotation-parity composition.
|
||||||
|
#
|
||||||
|
# NB: ``geometry.coordinate()`` applies a node's OWN ``rotation`` field even
|
||||||
|
# when reading corners it inherited from its parent -- a node with odd
|
||||||
|
# rotation has its local edge0/edge2 pair correspond to its PARENT's
|
||||||
|
# edge1/edge3 pair instead of edge0/edge2 (rotation parity selects between a
|
||||||
|
# quad's two possible opposite-edge pairings; ``operators.mutate_divide``
|
||||||
|
# randomises this on every newly-divided node, so it's common, not an edge
|
||||||
|
# case). This is an exact algebraic identity, not something to measure or
|
||||||
|
# approximate: ``left.w + right.h == parent.w`` whenever ``left.rotation`` is
|
||||||
|
# even and ``right.rotation`` is odd (and the symmetric case generally), for
|
||||||
|
# ANY topology, independent of skew or global orientation. ``_child_contrib``
|
||||||
|
# below applies this directly. See DESIGN.md §37.2 "Correction 2" for the
|
||||||
|
# validation history of this rule.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def _dims(n: dom_mod.Node) -> tuple[float, float]:
|
||||||
|
"""Rotation-invariant (w, h) of a quad from its own edge lengths (mirrors
|
||||||
|
the (edge0+edge2) vs (edge1+edge3) pairing ``geometry.aspect`` uses)."""
|
||||||
|
w = (geometry.edge_length(n, 0) + geometry.edge_length(n, 2)) / 2
|
||||||
|
h = (geometry.edge_length(n, 1) + geometry.edge_length(n, 3)) / 2
|
||||||
|
return (w, h)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# The DP itself
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Curve:
|
||||||
|
"""A node's feasible region, both ways: w_of_h[i] is the feasible w-range
|
||||||
|
at h=grid[i]; h_of_w[j] is the feasible h-range at w=grid[j]. Same shared
|
||||||
|
grid at every node, so composition needs no cross-node interpolation."""
|
||||||
|
|
||||||
|
w_of_h: list[Interval]
|
||||||
|
h_of_w: list[Interval]
|
||||||
|
|
||||||
|
|
||||||
|
def _interp_range(grid: np.ndarray, arr: list[Interval], x: float) -> Interval:
|
||||||
|
if x <= grid[0]:
|
||||||
|
return arr[0]
|
||||||
|
if x >= grid[-1]:
|
||||||
|
return arr[-1]
|
||||||
|
j = int(np.searchsorted(grid, x)) - 1
|
||||||
|
j = max(0, min(j, len(grid) - 2))
|
||||||
|
a, b = arr[j], arr[j + 1]
|
||||||
|
if a is None or b is None:
|
||||||
|
return a if x - grid[j] < grid[j + 1] - x else b
|
||||||
|
t = (x - grid[j]) / (grid[j + 1] - grid[j])
|
||||||
|
return (a[0] * (1 - t) + b[0] * t, a[1] * (1 - t) + b[1] * t)
|
||||||
|
|
||||||
|
|
||||||
|
def _invert(grid: np.ndarray, arr: list[Interval]) -> list[Interval]:
|
||||||
|
"""Given arr[i] = feasible cross-range at grid[i], return the inverse:
|
||||||
|
inv[j] = {y : arr's cross-range at y contains grid[j]}, assumed contiguous
|
||||||
|
in y (true for the monotonic hyperbola/line/wedge-composed regions this
|
||||||
|
DP produces). O(N^2) but numpy-vectorised (the naive Python double loop
|
||||||
|
was ~70% of total DP wall-clock, profiled on harbor-house-l0)."""
|
||||||
|
lo_arr = np.array([r[0] if r is not None else np.nan for r in arr])
|
||||||
|
hi_arr = np.array([r[1] if r is not None else np.nan for r in arr])
|
||||||
|
# mask[i, j]: does grid[i]'s range contain grid[j]?
|
||||||
|
mask = (lo_arr[:, None] - 1e-9 <= grid[None, :]) & (hi_arr[:, None] + 1e-9 >= grid[None, :])
|
||||||
|
grid_masked = np.where(mask, grid[:, None], np.nan)
|
||||||
|
any_feasible = mask.any(axis=0)
|
||||||
|
with np.errstate(invalid="ignore"), warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore", category=RuntimeWarning)
|
||||||
|
inv_lo = np.where(any_feasible, np.nanmin(grid_masked, axis=0), np.nan)
|
||||||
|
inv_hi = np.where(any_feasible, np.nanmax(grid_masked, axis=0), np.nan)
|
||||||
|
return [None if np.isnan(lo) else (float(lo), float(hi)) for lo, hi in zip(inv_lo, inv_hi)]
|
||||||
|
|
||||||
|
|
||||||
|
def make_grid(wmax: float, n: int = 400, wmin: float = 0.1) -> np.ndarray:
|
||||||
|
return np.geomspace(wmin, wmax, n)
|
||||||
|
|
||||||
|
|
||||||
|
def _child_contrib(curve: "Curve", rotation: int) -> list[Interval]:
|
||||||
|
"""The child's curve, reinterpreted in the PARENT's frame: parent.w is
|
||||||
|
ALWAYS the sum of its two children's ``_child_contrib`` (see module
|
||||||
|
docstring) -- even rotation contributes the child's own w_of_h directly;
|
||||||
|
odd rotation swaps w<->h (child.h sums; child.w is the one that
|
||||||
|
approximates the parent's shared/cross dimension)."""
|
||||||
|
return curve.w_of_h if rotation % 2 == 0 else curve.h_of_w
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Feasibility:
|
||||||
|
feasible: bool
|
||||||
|
h_range_at_w: Interval
|
||||||
|
w_range_at_h: Interval
|
||||||
|
|
||||||
|
|
||||||
|
def check_feasible(root_curve: Curve, grid: np.ndarray, w_plot: float, h_plot: float) -> Feasibility:
|
||||||
|
hr = _interp_range(grid, root_curve.h_of_w, w_plot)
|
||||||
|
wr = _interp_range(grid, root_curve.w_of_h, h_plot)
|
||||||
|
ok_h = hr is not None and hr[0] - 1e-6 <= h_plot <= hr[1] + 1e-6
|
||||||
|
ok_w = wr is not None and wr[0] - 1e-6 <= w_plot <= wr[1] + 1e-6
|
||||||
|
return Feasibility(feasible=bool(ok_h or ok_w), h_range_at_w=hr, w_range_at_h=wr)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Top-down back-substitution: realise one feasible point as division ratios.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def realise(
|
||||||
|
node: dom_mod.Node,
|
||||||
|
curves: dict[int, tuple[Curve, Curve]],
|
||||||
|
grid: np.ndarray,
|
||||||
|
w: float,
|
||||||
|
h: float,
|
||||||
|
) -> None:
|
||||||
|
"""Write ``division`` on every free branch under ``node`` so its subtree
|
||||||
|
realises the (w, h) target, given each descendant's precomputed curves.
|
||||||
|
``curves[id(n)] = (left_curve, right_curve)`` for internal nodes.
|
||||||
|
|
||||||
|
``node.w`` (the summed dimension) is ALWAYS ``w`` -- the parent-child cut
|
||||||
|
convention is fixed (see module docstring), not orientation-dependent.
|
||||||
|
Only each CHILD's rotation parity determines which of ITS OWN (w, h) the
|
||||||
|
allocated share becomes: even rotation -> child's own w; odd rotation ->
|
||||||
|
child's own h (the two are swapped for that recursive call).
|
||||||
|
"""
|
||||||
|
if not node.divided:
|
||||||
|
return
|
||||||
|
cl, cr = curves[id(node)]
|
||||||
|
contrib_l = _child_contrib(cl, node.left.rotation)
|
||||||
|
contrib_r = _child_contrib(cr, node.right.rotation)
|
||||||
|
rl = _interp_range(grid, contrib_l, h)
|
||||||
|
rr = _interp_range(grid, contrib_r, h)
|
||||||
|
lo = max(rl[0], w - rr[1])
|
||||||
|
hi = min(rl[1], w - rr[0])
|
||||||
|
wl = min(max((lo + hi) / 2.0, rl[0]), rl[1])
|
||||||
|
wl = min(max(wl, w - rr[1]), w - rr[0])
|
||||||
|
wr = w - wl
|
||||||
|
t = wl / w if w > 0 else 0.5
|
||||||
|
# _interp_range's interpolation branch returns numpy float64 (grid is a
|
||||||
|
# numpy array); dom.dumps (yaml.safe_dump) cannot serialise those, so
|
||||||
|
# every division written here must be a plain Python float.
|
||||||
|
node.division = [float(t), float(t)]
|
||||||
|
if node.left.rotation % 2 == 0:
|
||||||
|
realise(node.left, curves, grid, wl, h)
|
||||||
|
else:
|
||||||
|
realise(node.left, curves, grid, h, wl)
|
||||||
|
if node.right.rotation % 2 == 0:
|
||||||
|
realise(node.right, curves, grid, wr, h)
|
||||||
|
else:
|
||||||
|
realise(node.right, curves, grid, h, wr)
|
||||||
|
|
||||||
|
|
||||||
|
def build_curves_with_children(
|
||||||
|
node: dom_mod.Node, fit, grid: np.ndarray,
|
||||||
|
out: dict[int, tuple[Curve, Curve]],
|
||||||
|
) -> Curve:
|
||||||
|
"""Bottom-up: leaf curves are exact closed forms; internal nodes compose
|
||||||
|
on ``grid`` via the EXACT rotation-parity rule (``_child_contrib``), also
|
||||||
|
recording each internal node's (left, right) curves in ``out`` for
|
||||||
|
``realise`` to consume."""
|
||||||
|
if not node.divided:
|
||||||
|
b = leaf_constraints(fit, node)
|
||||||
|
w_of_h = h_of_w = b.range_grid(grid)
|
||||||
|
return Curve(w_of_h=w_of_h, h_of_w=h_of_w)
|
||||||
|
|
||||||
|
cl = build_curves_with_children(node.left, fit, grid, out)
|
||||||
|
cr = build_curves_with_children(node.right, fit, grid, out)
|
||||||
|
out[id(node)] = (cl, cr)
|
||||||
|
contrib_l = _child_contrib(cl, node.left.rotation)
|
||||||
|
contrib_r = _child_contrib(cr, node.right.rotation)
|
||||||
|
w_of_h = [_interval_add(contrib_l[i], contrib_r[i]) for i in range(len(grid))]
|
||||||
|
h_of_w = _invert(grid, w_of_h)
|
||||||
|
return Curve(w_of_h=w_of_h, h_of_w=h_of_w)
|
||||||
|
|
||||||
|
|
||||||
|
def solve(level_root: dom_mod.Node, fit, grid_n: int = 150) -> tuple[bool, dict]:
|
||||||
|
"""End-to-end: compute plot dims, build curves, check root feasibility,
|
||||||
|
and (if feasible) write realising ratios in place. Returns (feasible,
|
||||||
|
info) where info carries timing-relevant intermediates for the caller.
|
||||||
|
|
||||||
|
``level_root`` must be a single storey (see ``eligible``) — the DP has no
|
||||||
|
notion of upper-storey ``below``-inherited fixed splits and will
|
||||||
|
overwrite ``division`` unconditionally on every divided node it walks.
|
||||||
|
"""
|
||||||
|
w_plot, h_plot = _dims(level_root)
|
||||||
|
grid = make_grid(max(w_plot, h_plot) * 1.2, n=grid_n)
|
||||||
|
|
||||||
|
curves_by_node: dict[int, tuple[Curve, Curve]] = {}
|
||||||
|
root_curve = build_curves_with_children(level_root, fit, grid, curves_by_node)
|
||||||
|
|
||||||
|
feas = check_feasible(root_curve, grid, w_plot, h_plot)
|
||||||
|
if feas.feasible:
|
||||||
|
realise(level_root, curves_by_node, grid, w_plot, h_plot)
|
||||||
|
geometry.clear_cache()
|
||||||
|
return feas.feasible, {
|
||||||
|
"w_plot": w_plot, "h_plot": h_plot,
|
||||||
|
"grid": grid, "h_range_at_w": feas.h_range_at_w, "w_range_at_h": feas.w_range_at_h,
|
||||||
|
}
|
||||||
|
|
@ -264,6 +264,102 @@ def test_feasibility_filter_prunes_cheaply(fake_inner, monkeypatch):
|
||||||
assert on.best is not None and not on.best.lineage.startswith("pruned/")
|
assert on.best is not None and not on.best.lineage.startswith("pruned/")
|
||||||
|
|
||||||
|
|
||||||
|
def test_shapecurve_warmstart_off_matches_baseline(fake_inner):
|
||||||
|
"""homemaker-py-6xh: with the flag off (default), the run is identical to
|
||||||
|
one that omits the param — a clean A/B control, mirroring the existing
|
||||||
|
feasibility-filter control test."""
|
||||||
|
init_root = dom.load(str(INIT_FILE))
|
||||||
|
base = driver.search(init_root, CORPUS, budget=600, pop_size=4,
|
||||||
|
child_budget=60, seed_budget=100, seed=9)
|
||||||
|
off = driver.search(init_root, CORPUS, budget=600, pop_size=4,
|
||||||
|
child_budget=60, seed_budget=100, seed=9,
|
||||||
|
shapecurve_warmstart=False)
|
||||||
|
assert off.best.sig == base.best.sig
|
||||||
|
assert off.n_topologies == base.n_topologies
|
||||||
|
assert off.n_evals == base.n_evals
|
||||||
|
|
||||||
|
|
||||||
|
def test_shapecurve_warmstart_seeds_ratios_when_eligible(monkeypatch):
|
||||||
|
"""homemaker-py-6xh: when eligible (single storey, no leaf_sharing/
|
||||||
|
superpose/max_share/multi_use) and no caller-supplied x0, ``shapecurve.
|
||||||
|
solve`` is called and its written ratios are on the tree by the time
|
||||||
|
``innerloop.optimise`` runs — the mechanism the inner loop's own
|
||||||
|
``x0=None`` (tree's current ratios) picks up as the warm start."""
|
||||||
|
from homemaker_layout import shapecurve
|
||||||
|
|
||||||
|
divisions_at_optimise = []
|
||||||
|
|
||||||
|
def fake_optimise(root, programme_dir, x0=None, budget=200, urb_root=None, **kw):
|
||||||
|
divisions_at_optimise.append(
|
||||||
|
[tuple(b.division) for _, b in innerloop.free_with_keys(root)])
|
||||||
|
n_leaves = sum(len(lvl.leaves()) for lvl in dom.levels(root))
|
||||||
|
fit = 1.0 / (1.0 + abs(12 - n_leaves))
|
||||||
|
for _, b in innerloop.free_with_keys(root):
|
||||||
|
b.division = [0.25, 0.25]
|
||||||
|
return innerloop.Result(
|
||||||
|
x=np.array([0.25]), fitness=fit, n_fails=0, fail_lines=(),
|
||||||
|
x0_fitness=fit / 2, x0_n_fails=1, n_evals=budget, n_oracle_calls=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(innerloop, "optimise", fake_optimise)
|
||||||
|
|
||||||
|
solve_calls = []
|
||||||
|
|
||||||
|
def spy_solve(root, fit, grid_n=150):
|
||||||
|
solve_calls.append(len(dom.levels(root)))
|
||||||
|
for _, b in innerloop.free_with_keys(root):
|
||||||
|
b.division = [0.37, 0.37]
|
||||||
|
return True, {}
|
||||||
|
|
||||||
|
monkeypatch.setattr(shapecurve, "solve", spy_solve)
|
||||||
|
|
||||||
|
# harbor-house-l0 (storey_minimum=1) rather than CORPUS (programme-house,
|
||||||
|
# storey_minimum=2) — constructive_topology would otherwise grow a
|
||||||
|
# multi-storey seed and shapecurve.eligible would rightly never fire.
|
||||||
|
harbor_l0 = Path(__file__).parent.parent / "examples" / "harbor-house-l0"
|
||||||
|
if not harbor_l0.is_dir():
|
||||||
|
pytest.skip("harbor-house-l0 not available")
|
||||||
|
init_root = dom.load(str(harbor_l0 / "init.dom"))
|
||||||
|
driver.search(init_root, harbor_l0, budget=300, pop_size=2,
|
||||||
|
child_budget=60, seed_budget=60, seed=3,
|
||||||
|
shapecurve_warmstart=True, leaf_sharing=False)
|
||||||
|
|
||||||
|
assert solve_calls, "shapecurve.solve must be called for eligible children"
|
||||||
|
assert all(n == 1 for n in solve_calls), "only ever called on single-storey trees"
|
||||||
|
# the DP-written ratio (0.37) was on the tree when optimise saw it
|
||||||
|
assert any(
|
||||||
|
any(abs(t[0] - 0.37) < 1e-9 for t in divs)
|
||||||
|
for divs in divisions_at_optimise if divs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_shapecurve_warmstart_skips_multistorey(monkeypatch):
|
||||||
|
"""homemaker-py-6xh: the DP has no notion of ``below``-inherited
|
||||||
|
(wall-stacked) fixed splits, so it must never be invoked on a
|
||||||
|
multi-storey topology — ``shapecurve.eligible`` guards this."""
|
||||||
|
from homemaker_layout import shapecurve
|
||||||
|
|
||||||
|
def fake_optimise(root, programme_dir, x0=None, budget=200, urb_root=None, **kw):
|
||||||
|
for _, b in innerloop.free_with_keys(root):
|
||||||
|
b.division = [0.25, 0.25]
|
||||||
|
return innerloop.Result(
|
||||||
|
x=np.array([0.25]), fitness=0.5, n_fails=0, fail_lines=(),
|
||||||
|
x0_fitness=0.25, x0_n_fails=1, n_evals=budget, n_oracle_calls=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(innerloop, "optimise", fake_optimise)
|
||||||
|
solve_calls = []
|
||||||
|
monkeypatch.setattr(shapecurve, "solve",
|
||||||
|
lambda root, fit, grid_n=150: (solve_calls.append(1), (True, {}))[1])
|
||||||
|
|
||||||
|
multi_root = dom.load(str(SEED_FILE))
|
||||||
|
assert len(dom.levels(multi_root)) > 1
|
||||||
|
driver.search(multi_root, CORPUS, budget=200, pop_size=2,
|
||||||
|
child_budget=60, seed_budget=60, seed=1,
|
||||||
|
bootstrap=False, shapecurve_warmstart=True, leaf_sharing=False)
|
||||||
|
assert not solve_calls
|
||||||
|
|
||||||
|
|
||||||
def test_search_parallel_smoke():
|
def test_search_parallel_smoke():
|
||||||
"""n_workers>1 runs without error and produces valid results."""
|
"""n_workers>1 runs without error and produces valid results."""
|
||||||
init_root = dom.load(str(INIT_FILE))
|
init_root = dom.load(str(INIT_FILE))
|
||||||
|
|
|
||||||
90
tests/test_shapecurve.py
Normal file
90
tests/test_shapecurve.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
"""Tests for the shape-curve DP (homemaker-py-6xh, promoted from
|
||||||
|
experiments/shapecurve_spike.py; see DESIGN.md §37.2/§37.4 for the full
|
||||||
|
200-topology validation this unit scale is a fast smoke check of)."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homemaker_layout import dom, driver, fitness as fit_mod, shapecurve, solver
|
||||||
|
|
||||||
|
HARBOR_L0 = Path(__file__).parent.parent / "examples" / "harbor-house-l0"
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(not HARBOR_L0.is_dir(), reason="harbor-house-l0 not available")
|
||||||
|
|
||||||
|
|
||||||
|
def _fit():
|
||||||
|
conf, cost = fit_mod.load_config(str(HARBOR_L0))
|
||||||
|
return fit_mod.Fitness(conf, cost)
|
||||||
|
|
||||||
|
|
||||||
|
def _small_feasible_topology():
|
||||||
|
"""A tiny 2-leaf (C/O only, no size/adjacency constraints to speak of)
|
||||||
|
topology on harbor-house-l0's plot -- deterministically shape-feasible
|
||||||
|
(verified: driver.random_topology(seed, 2, rng(0), ['C', 'O']))."""
|
||||||
|
seed = dom.load(str(HARBOR_L0 / "init.dom"))
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
return driver.random_topology(seed, 2, rng, ["C", "O"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_eligible_guards_multistorey_and_sharing():
|
||||||
|
root = dom.load(str(HARBOR_L0 / "generated.dom"))
|
||||||
|
assert len(dom.levels(root)) == 1
|
||||||
|
assert shapecurve.eligible(root)
|
||||||
|
assert not shapecurve.eligible(root, leaf_sharing=True)
|
||||||
|
assert not shapecurve.eligible(root, superpose=True)
|
||||||
|
assert not shapecurve.eligible(root, max_share=3)
|
||||||
|
assert not shapecurve.eligible(root, multi_use=True)
|
||||||
|
|
||||||
|
seed = dom.load(str(HARBOR_L0 / "init.dom"))
|
||||||
|
seed.above = dom.Node(rotation=0) # fake a second storey
|
||||||
|
assert len(dom.levels(seed)) == 2
|
||||||
|
assert not shapecurve.eligible(seed)
|
||||||
|
|
||||||
|
|
||||||
|
def test_solve_feasible_root_realises_zero_shape_fails(tmp_path):
|
||||||
|
"""A small, obviously-feasible topology's DP-realised ratios round-trip
|
||||||
|
through dom.dumps/dom.load and independently verify as zero shape fails
|
||||||
|
under the real Fitness scorer."""
|
||||||
|
root = _small_feasible_topology()
|
||||||
|
fit = _fit()
|
||||||
|
|
||||||
|
feasible, info = shapecurve.solve(root, fit)
|
||||||
|
assert feasible is True
|
||||||
|
assert info["w_plot"] > 0 and info["h_plot"] > 0
|
||||||
|
|
||||||
|
out_path = tmp_path / "realised.dom"
|
||||||
|
out_path.write_text(dom.dumps(root))
|
||||||
|
reloaded = dom.load(str(out_path))
|
||||||
|
|
||||||
|
_, fails = fit.score_with_fails(reloaded)
|
||||||
|
shape_fails = [f for f in fails if f.endswith((" size", " width", " proportion"))]
|
||||||
|
assert shape_fails == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_solve_infeasible_topology_leaves_tree_untouched():
|
||||||
|
"""A topology with far more leaves than harbor-house-l0's plot can fit
|
||||||
|
(each needing its own min width/area) is infeasible; solve() must not
|
||||||
|
write partial/bogus ratios in that case."""
|
||||||
|
seed = dom.load(str(HARBOR_L0 / "init.dom"))
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
root = driver.random_topology(seed, 60, rng, ["k1", "l1", "b1", "C", "O"])
|
||||||
|
fit = _fit()
|
||||||
|
|
||||||
|
feasible, info = shapecurve.solve(root, fit)
|
||||||
|
assert feasible is False
|
||||||
|
# infeasible: no realised point to check, but the call must not raise
|
||||||
|
# and must report the same plot dims as the feasible case's mechanism
|
||||||
|
assert info["w_plot"] > 0 and info["h_plot"] > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_solve_is_deterministic():
|
||||||
|
root = _small_feasible_topology()
|
||||||
|
fit = _fit()
|
||||||
|
f1, _ = shapecurve.solve(root, fit)
|
||||||
|
divisions_1 = [tuple(b.division) for b in solver.free_branches(root)]
|
||||||
|
f2, _ = shapecurve.solve(root, fit)
|
||||||
|
divisions_2 = [tuple(b.division) for b in solver.free_branches(root)]
|
||||||
|
assert f1 == f2 is True
|
||||||
|
assert divisions_1 == pytest.approx(divisions_2)
|
||||||
Loading…
Add table
Reference in a new issue