Phase 7 §12.2: proportion-aware constructive seeding + storey_minimum fix (leu.2, cq1)
Size each constructive-seed cut from leaf TARGET areas (division=[f,f] gives left area-fraction f) and pick each cut's rotation for child squareness — both derived from target dims, topology/type assignment untouched. Area-only regressed (slivers); rotation choice is what makes it pay. End-to-end (20000 evals, 3 seeds, staged): harbor 85.3->74.0 (-13%, best 69), maple-court 151.7->136.0 (-10%, best 126). PROP=0 reproduces the §11.7/§12.1 baselines exactly. programme-house regresses at fixed budget (deeper local optimum walls off the undivide restructuring path) but a budget sweep shows it's convergence speed, not a worse asymptote (PROP=1 reaches 1 fail at 150k). Default-on (seed_proportion_aware=True, env PROP=1). cq1: n_storeys now honours storey_minimum, not just level: keys — programme-house (storey_minimum:2, all rooms level:0) was seeded one storey short and fell through to plain search. New programme.storey_minimum()/n_storeys_for(); driver.search passes min_storeys to the seeder; search_staged routes on the max. No-op for harbor/maple; programme-house single-stage 8.0->5.0. New maple-court best (126) saved as generated.dom. 204 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7d7994e7a3
commit
995342d0a4
10 changed files with 825 additions and 440 deletions
|
|
@ -16,7 +16,8 @@
|
||||||
{"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-leu.2","title":"Proportion-aware constructive seeding (size splits from target dims)","description":"Follow-up to §11.6/§11.7. Adjacency-aware seeding cut the topology load (adjacency-to-c / access) but §11.6 explicitly noted the seed still splits at 0.5, producing 'more, smaller leaves' whose size/proportion/crinkliness fails the inner loop then has to recover. With topology fails now cut by seeding, this GEOMETRY residual is the dominant remaining term (§11.7 verdict). Attacking it at the seed is the proven-productive (construction) direction and is far cheaper than the 9gp encoding rewrite.\n\nIdea: when constructive_topology / lift_base_to_storeys place a cut, size the division ratio from the leaves' TARGET dimensions (programme target areas/widths) instead of 0.5, so the raw seed geometry already sits near feasible proportions and the inner loop starts inside (or much closer to) the size/width/proportion basins. Keep adjacency-aware placement (§11.6/§11.7) unchanged; this only changes split RATIOS, not topology or type assignment. Behind a flag for clean A/B, default-on if it wins.\n\nMeasure raw-seed geometry fails (size/width/proportion/crinkliness) before/after AND end-to-end total fails at budget on harbor, programme-house, AND the new leu.1 benchmark, same protocol as §11.6. Record in DESIGN.md §12.2 + bead notes (incl. negative result if it does not win).","acceptance_criteria":"Proportion-aware split sizing implemented behind a flag; raw-seed geometry-fail reduction quantified; end-to-end total-fail change measured on harbor, programme-house, and the leu.1 benchmark (\u003e=3 seeds each); result (positive or negative) recorded in DESIGN.md.","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-19T11:14:15Z","created_by":"Bruno Postle","updated_at":"2026-06-19T11:14:15Z","dependencies":[{"issue_id":"homemaker-py-leu.2","depends_on_id":"homemaker-py-leu","type":"parent-child","created_at":"2026-06-19T12:14:15Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-leu.2","depends_on_id":"homemaker-py-leu.1","type":"blocks","created_at":"2026-06-19T12:14:45Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
{"id":"homemaker-py-cq1","title":"Constructive seeder + staged dispatch ignored storey_minimum","description":"n_storeys_required only reads level: keys, so a programme with storey_minimum\u003emax(level)+1 (e.g. programme-house: storey_minimum:2, all rooms level:0) was seeded one storey short by constructive_topology and routed to plain (non-staged) search. Fitness then fired a 'storey minimum' fail the search had to repair structurally. Surfaced while measuring leu.2 (proportion-aware seeding deepened the basin around the wrong-storey-count seed). Fix: programme.storey_minimum()/n_storeys_for(); driver.search passes min_storeys to constructive_topology; search_staged routes on max(n_storeys_required, storey_minimum). Independent win: programme-house single-stage baseline 8.0 -\u003e 5.0 fails with correct 2-storey seed.","status":"open","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-06-20T08:59:31Z","created_by":"Bruno Postle","updated_at":"2026-06-20T08:59:31Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"homemaker-py-leu.2","title":"Proportion-aware constructive seeding (size splits from target dims)","description":"Follow-up to §11.6/§11.7. Adjacency-aware seeding cut the topology load (adjacency-to-c / access) but §11.6 explicitly noted the seed still splits at 0.5, producing 'more, smaller leaves' whose size/proportion/crinkliness fails the inner loop then has to recover. With topology fails now cut by seeding, this GEOMETRY residual is the dominant remaining term (§11.7 verdict). Attacking it at the seed is the proven-productive (construction) direction and is far cheaper than the 9gp encoding rewrite.\n\nIdea: when constructive_topology / lift_base_to_storeys place a cut, size the division ratio from the leaves' TARGET dimensions (programme target areas/widths) instead of 0.5, so the raw seed geometry already sits near feasible proportions and the inner loop starts inside (or much closer to) the size/width/proportion basins. Keep adjacency-aware placement (§11.6/§11.7) unchanged; this only changes split RATIOS, not topology or type assignment. Behind a flag for clean A/B, default-on if it wins.\n\nMeasure raw-seed geometry fails (size/width/proportion/crinkliness) before/after AND end-to-end total fails at budget on harbor, programme-house, AND the new leu.1 benchmark, same protocol as §11.6. Record in DESIGN.md §12.2 + bead notes (incl. negative result if it does not win).","acceptance_criteria":"Proportion-aware split sizing implemented behind a flag; raw-seed geometry-fail reduction quantified; end-to-end total-fail change measured on harbor, programme-house, and the leu.1 benchmark (\u003e=3 seeds each); result (positive or negative) recorded in DESIGN.md.","notes":"DONE (positive), default-on. End-to-end (20000 evals, 3 seeds, staged): harbor 85.3-\u003e74.0 (-13%, best 69), maple-court 151.7-\u003e136.0 (-10%, best 126). PROP=0 reproduces 11.7/12.1 baselines exactly. programme-house regresses at fixed budget (deeper-local-optimum: well-fitted seed walls off the undivide restructuring path) but a budget sweep shows it's convergence-SPEED not asymptote (PROP=1 reaches 1 fail at 150k, beating PROP=0's 2; floor is 2). Win requires rotation+ratio sizing from target dims (area-only regressed via slivers). Surfaced + fixed storey_minimum bug (cq1). Default flipped on: driver.search/search_staged seed_proportion_aware=True, harness PROP=1. DESIGN.md 12.2. 204 tests pass. New maple best 126 saved as generated.dom.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-19T11:14:15Z","created_by":"Bruno Postle","updated_at":"2026-06-20T12:32:09Z","started_at":"2026-06-19T13:03:27Z","dependencies":[{"issue_id":"homemaker-py-leu.2","depends_on_id":"homemaker-py-leu","type":"parent-child","created_at":"2026-06-19T12:14:15Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-leu.2","depends_on_id":"homemaker-py-leu.1","type":"blocks","created_at":"2026-06-19T12:14:45Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
||||||
{"id":"homemaker-py-s44","title":"Adjacency-aware constructive seeding (cut adjacency/access fails)","description":"Follow-up to homemaker-py-c4c.2. constructive_topology currently assigns room types to leaves at RANDOM, ignoring each space's adjacency requirement. On harbor this leaves 8 adjacency + 13 access fails in the seeded design. Cluster each required room near its required neighbour (esp. circulation c) at construction time — e.g. assign rooms to leaves whose sibling/parent is C, or grow the tree so each room lands adjacent to a circulation spine. Should directly cut the adjacency+access fail load that now dominates the complete-design quality-fail regime (DESIGN.md §11.2 verdict).","notes":"DONE (positive), DESIGN.md §11.6. _assign_adjacency_aware: greedy connected-dominating-set of circulation leaves on the geometric leaf_graph so every room borders a connected circulation spine; rooms on dominated leaves, O peripheral. Default-on via constructive_topology(adjacency_aware=True), threaded driver.search(seed_adjacency_aware). Seed quality (harbor 10 seeds): adjacency 29-\u003e12, access 27-\u003e8. End-to-end single-stage 20000 evals total fails mean: harbor 110.0-\u003e90.7 (-17.5%, ADJ=0 seed0 reproduces §11.2 105 baseline exactly), programme-house 12.3-\u003e9.3 (-24%); adjacency-aware single-stage harbor (mean 90.7, best 85) beats the §11.3 staged 95. Follow-ups filed: lift_base_to_storeys adjacency-awareness + secondary adjacencies.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-17T21:50:01Z","created_by":"Bruno Postle","updated_at":"2026-06-19T08:12:43Z","started_at":"2026-06-18T22:52:25Z","closed_at":"2026-06-19T08:12:43Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-s44","title":"Adjacency-aware constructive seeding (cut adjacency/access fails)","description":"Follow-up to homemaker-py-c4c.2. constructive_topology currently assigns room types to leaves at RANDOM, ignoring each space's adjacency requirement. On harbor this leaves 8 adjacency + 13 access fails in the seeded design. Cluster each required room near its required neighbour (esp. circulation c) at construction time — e.g. assign rooms to leaves whose sibling/parent is C, or grow the tree so each room lands adjacent to a circulation spine. Should directly cut the adjacency+access fail load that now dominates the complete-design quality-fail regime (DESIGN.md §11.2 verdict).","notes":"DONE (positive), DESIGN.md §11.6. _assign_adjacency_aware: greedy connected-dominating-set of circulation leaves on the geometric leaf_graph so every room borders a connected circulation spine; rooms on dominated leaves, O peripheral. Default-on via constructive_topology(adjacency_aware=True), threaded driver.search(seed_adjacency_aware). Seed quality (harbor 10 seeds): adjacency 29-\u003e12, access 27-\u003e8. End-to-end single-stage 20000 evals total fails mean: harbor 110.0-\u003e90.7 (-17.5%, ADJ=0 seed0 reproduces §11.2 105 baseline exactly), programme-house 12.3-\u003e9.3 (-24%); adjacency-aware single-stage harbor (mean 90.7, best 85) beats the §11.3 staged 95. Follow-ups filed: lift_base_to_storeys adjacency-awareness + secondary adjacencies.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-17T21:50:01Z","created_by":"Bruno Postle","updated_at":"2026-06-19T08:12:43Z","started_at":"2026-06-18T22:52:25Z","closed_at":"2026-06-19T08:12:43Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-c4c.5","title":"Topology diversity: structural niching + restarts (replace fitness-scalar dedup)","description":"The population dedups on the FITNESS SCALAR (driver.py:174, abs(fitness) within 1e-9) and replaces worst-by-key. There is no structural/topological diversity preservation, no restarts, no islands. On a rugged combinatorial landscape this converges prematurely — and it is the root cause of the blank-slate gap (§7 Phase 2 verdict): a single mutation chain loses to urb-evolve's random-population diversity (init.dom: memetic 18 fails vs urb-evolve 6).\nAdd: (1) a topology signature (canonical tree hash / partition signature) so 'same topology, different geometry' is detectable and niching is by STRUCTURE not score; (2) diversity-preserving replacement (crowding / niching); (3) restarts or a small island model so blank-slate exploration matches urb-evolve's upfront diversity.","design":"A cheap topology-signature hash (string-encode the per-level tree + types) unblocks niching without waiting for the full canonical encoding; the canonical Polish encoding (homemaker-py-9gp) is the principled long-term signature and makes (a|b)|c == a|(b|c) collapse exactly. Wire signature into admit() in place of / alongside the fitness-scalar guard.","acceptance_criteria":"On blank-slate programme-house, memetic reaches \u003c=6 fails (matching/beating urb-evolve) at equal native-fitness budget; population structural diversity quantified (distinct topology signatures over time) before/after; recorded in DESIGN.md §11.x + bead notes.","notes":"DONE (negative), DESIGN.md §11.5. Implemented genome.signature (ratio-invariant structural topology hash), structural niching (niche_by_signature) replacing the fitness-scalar dedup, and soft restarts (restart_patience); SearchResult gained n_distinct_signatures/diversity_history/n_restarts. Diversity criterion MET: final-pop distinct topologies ~5/16 -\u003e 16/16, ~30% more topologies seen with restarts. Gate NOT met: blank-slate programme-house total fails (20000 evals) before/niche/restart = seed0 11/14/12, seed1 11/11/14, seed2 15/13/13 (mean 12.3/12.7/13.0); harbor staged seed0 = 95/94/108 (legacy 95 reproduces §11.3). Niching is a tie within seed noise, restarts strictly worse. Falsifies the epic's premise that the fitness-scalar dedup is the premature-convergence root cause: legacy already holds 14/16 distinct on harbor; the plateau is a reachability (operator/encoding) problem, not population-management. Both flags default-off, kept for reuse; genome.signature is the cheap stand-in for the 9gp canonical Polish encoding.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-17T19:12:54Z","created_by":"Bruno Postle","updated_at":"2026-06-18T22:41:58Z","started_at":"2026-06-18T21:52:37Z","closed_at":"2026-06-18T22:41:58Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-c4c.5","depends_on_id":"homemaker-py-9gp","type":"relates-to","created_at":"2026-06-17T20:14:46Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-c4c.5","depends_on_id":"homemaker-py-c4c","type":"parent-child","created_at":"2026-06-17T20:12:53Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-c4c.5","depends_on_id":"homemaker-py-c4c.2","type":"blocks","created_at":"2026-06-17T20:12:54Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-c4c.5","title":"Topology diversity: structural niching + restarts (replace fitness-scalar dedup)","description":"The population dedups on the FITNESS SCALAR (driver.py:174, abs(fitness) within 1e-9) and replaces worst-by-key. There is no structural/topological diversity preservation, no restarts, no islands. On a rugged combinatorial landscape this converges prematurely — and it is the root cause of the blank-slate gap (§7 Phase 2 verdict): a single mutation chain loses to urb-evolve's random-population diversity (init.dom: memetic 18 fails vs urb-evolve 6).\nAdd: (1) a topology signature (canonical tree hash / partition signature) so 'same topology, different geometry' is detectable and niching is by STRUCTURE not score; (2) diversity-preserving replacement (crowding / niching); (3) restarts or a small island model so blank-slate exploration matches urb-evolve's upfront diversity.","design":"A cheap topology-signature hash (string-encode the per-level tree + types) unblocks niching without waiting for the full canonical encoding; the canonical Polish encoding (homemaker-py-9gp) is the principled long-term signature and makes (a|b)|c == a|(b|c) collapse exactly. Wire signature into admit() in place of / alongside the fitness-scalar guard.","acceptance_criteria":"On blank-slate programme-house, memetic reaches \u003c=6 fails (matching/beating urb-evolve) at equal native-fitness budget; population structural diversity quantified (distinct topology signatures over time) before/after; recorded in DESIGN.md §11.x + bead notes.","notes":"DONE (negative), DESIGN.md §11.5. Implemented genome.signature (ratio-invariant structural topology hash), structural niching (niche_by_signature) replacing the fitness-scalar dedup, and soft restarts (restart_patience); SearchResult gained n_distinct_signatures/diversity_history/n_restarts. Diversity criterion MET: final-pop distinct topologies ~5/16 -\u003e 16/16, ~30% more topologies seen with restarts. Gate NOT met: blank-slate programme-house total fails (20000 evals) before/niche/restart = seed0 11/14/12, seed1 11/11/14, seed2 15/13/13 (mean 12.3/12.7/13.0); harbor staged seed0 = 95/94/108 (legacy 95 reproduces §11.3). Niching is a tie within seed noise, restarts strictly worse. Falsifies the epic's premise that the fitness-scalar dedup is the premature-convergence root cause: legacy already holds 14/16 distinct on harbor; the plateau is a reachability (operator/encoding) problem, not population-management. Both flags default-off, kept for reuse; genome.signature is the cheap stand-in for the 9gp canonical Polish encoding.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-17T19:12:54Z","created_by":"Bruno Postle","updated_at":"2026-06-18T22:41:58Z","started_at":"2026-06-18T21:52:37Z","closed_at":"2026-06-18T22:41:58Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-c4c.5","depends_on_id":"homemaker-py-9gp","type":"relates-to","created_at":"2026-06-17T20:14:46Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-c4c.5","depends_on_id":"homemaker-py-c4c","type":"parent-child","created_at":"2026-06-17T20:12:53Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-c4c.5","depends_on_id":"homemaker-py-c4c.2","type":"blocks","created_at":"2026-06-17T20:12:54Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-c4c.4","title":"Graded high-fail objective (gradient in the high-fail regime)","description":"Phase 4 (homemaker-py-yg5) chose lexicographic (-n_fails, fitness) — correct for not being FOOLED by the 0.5^n cliff (§4.9). But lexicographic-by-TOTAL-count gives almost zero selection signal in the high-fail regime: on harbor every candidate sits at ~49-74 fails, so neighbours are indistinguishable and the search has no gradient to climb. There is no partial credit for a size-fail that is nearly in range, nor for covering one more required requirement. §7 predicted penalty reshaping would 'flatten the fail cliff' for blank-slate; lexicographic did not deliver that for high counts.\nAdd a graded objective for the high-fail regime: continuous proximity per unsatisfied constraint (how close a size/width/proportion is to its band) and/or count of DISTINCT unsatisfied requirements with sub-credit, used as a tie/secondary key beneath fail-count. Must preserve: (a) inner-loop 0.5^n cliff protection (§5.4) — inner loop unchanged; (b) the missing-space hierarchy (§6) — must not make dropping a room attractive.","design":"Likely a third comparison key: (-n_fails, -n_distinct_unsatisfied_or_proximity_sum, fitness). Or a soft margin inside fail counting only in the outer comparator. Keep the scalar fitness (with 0.5^n) untouched so the inner loop is unaffected. Extends homemaker-py-yg5; reuse experiments/penalty_reshape.py harness.","acceptance_criteria":"Measured escape from a high-fail plateau on harbor and/or blank-slate programme-house that the current lex comparator cannot escape at equal budget; before/after best-fail trajectory recorded in DESIGN.md §11.x + bead notes. Inner-loop cliff protection verified unchanged (re-run the §4.9 inner-loop 0/9-regression check).","notes":"NEGATIVE RESULT (DESIGN.md §11.4). Implemented graded proximity key (-n_fails, grade, fitness) behind use_grade flag (default off): fitness._leaf_grade / score_with_grade sum f/FAIL_THRESHOLD over failing per-leaf quality factors; scalar fitness + fail count untouched. Inner-loop 0/9 regression: PASS (re-ran §4.9 part 1). Harbor staged A/B, 20000 evals, seeds 0/1/2 total fails: lex 95/96/106 (mean 99.0) vs lex+grade 99/98/102 (mean 99.7). Grade wins 1/3, loses 2/3, slightly worse on mean, NO plateau escape. Acceptance criterion (escape lex cannot achieve) NOT met. Root cause: premise falsified — within a fixed fail-tier 0.5^n is constant so fitness still spans ~6 orders of magnitude (1e-37..1e-31), giving lex's secondary fitness key a strong gradient already; grade above fitness DISPLACES it (stalls fail-reducing restructurings), below fitness is inert. High-fail plateau is a topology-basin problem -\u003e defer to §11.5 niching/restarts + 9gp canonical encoding. Code kept default-off for reproducibility / possible reuse as a §11.5 diversity signal.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-17T19:12:18Z","created_by":"Bruno Postle","updated_at":"2026-06-18T21:20:49Z","started_at":"2026-06-18T05:31:17Z","closed_at":"2026-06-18T21:20:49Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-c4c.4","depends_on_id":"homemaker-py-c4c","type":"parent-child","created_at":"2026-06-17T20:12:18Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-c4c.4","title":"Graded high-fail objective (gradient in the high-fail regime)","description":"Phase 4 (homemaker-py-yg5) chose lexicographic (-n_fails, fitness) — correct for not being FOOLED by the 0.5^n cliff (§4.9). But lexicographic-by-TOTAL-count gives almost zero selection signal in the high-fail regime: on harbor every candidate sits at ~49-74 fails, so neighbours are indistinguishable and the search has no gradient to climb. There is no partial credit for a size-fail that is nearly in range, nor for covering one more required requirement. §7 predicted penalty reshaping would 'flatten the fail cliff' for blank-slate; lexicographic did not deliver that for high counts.\nAdd a graded objective for the high-fail regime: continuous proximity per unsatisfied constraint (how close a size/width/proportion is to its band) and/or count of DISTINCT unsatisfied requirements with sub-credit, used as a tie/secondary key beneath fail-count. Must preserve: (a) inner-loop 0.5^n cliff protection (§5.4) — inner loop unchanged; (b) the missing-space hierarchy (§6) — must not make dropping a room attractive.","design":"Likely a third comparison key: (-n_fails, -n_distinct_unsatisfied_or_proximity_sum, fitness). Or a soft margin inside fail counting only in the outer comparator. Keep the scalar fitness (with 0.5^n) untouched so the inner loop is unaffected. Extends homemaker-py-yg5; reuse experiments/penalty_reshape.py harness.","acceptance_criteria":"Measured escape from a high-fail plateau on harbor and/or blank-slate programme-house that the current lex comparator cannot escape at equal budget; before/after best-fail trajectory recorded in DESIGN.md §11.x + bead notes. Inner-loop cliff protection verified unchanged (re-run the §4.9 inner-loop 0/9-regression check).","notes":"NEGATIVE RESULT (DESIGN.md §11.4). Implemented graded proximity key (-n_fails, grade, fitness) behind use_grade flag (default off): fitness._leaf_grade / score_with_grade sum f/FAIL_THRESHOLD over failing per-leaf quality factors; scalar fitness + fail count untouched. Inner-loop 0/9 regression: PASS (re-ran §4.9 part 1). Harbor staged A/B, 20000 evals, seeds 0/1/2 total fails: lex 95/96/106 (mean 99.0) vs lex+grade 99/98/102 (mean 99.7). Grade wins 1/3, loses 2/3, slightly worse on mean, NO plateau escape. Acceptance criterion (escape lex cannot achieve) NOT met. Root cause: premise falsified — within a fixed fail-tier 0.5^n is constant so fitness still spans ~6 orders of magnitude (1e-37..1e-31), giving lex's secondary fitness key a strong gradient already; grade above fitness DISPLACES it (stalls fail-reducing restructurings), below fitness is inert. High-fail plateau is a topology-basin problem -\u003e defer to §11.5 niching/restarts + 9gp canonical encoding. Code kept default-off for reproducibility / possible reuse as a §11.5 diversity signal.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-17T19:12:18Z","created_by":"Bruno Postle","updated_at":"2026-06-18T21:20:49Z","started_at":"2026-06-18T05:31:17Z","closed_at":"2026-06-18T21:20:49Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-c4c.4","depends_on_id":"homemaker-py-c4c","type":"parent-child","created_at":"2026-06-17T20:12:18Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
@ -42,16 +43,16 @@
|
||||||
{"id":"homemaker-py-can","title":"Programme width defaults: t3 contradiction (impossible width_inside default)","description":"DESIGN.md §8.2, confirmed in source. t3 (3 m2 WC) has no width spec so inherits width_inside [4.0, 1.0] (Fitness/Base.pm:60) — geometrically impossible; designs 'pass' only by failing size instead. Fix AFTER faithful-port validation (port-faithfully-first policy, §8.1): a sane width default scaled to area (e.g. sqrt(area/proportion)) or per-room widths in patterns.config. Applies to native fitness; optionally upstream to Urb.","acceptance_criteria":"No programme space has a default width incompatible with its target area; corpus re-scored and effect documented","status":"closed","priority":3,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:39:01Z","created_by":"Bruno Postle","updated_at":"2026-06-13T21:21:37Z","started_at":"2026-06-13T21:16:11Z","closed_at":"2026-06-13T21:21:37Z","close_reason":"Fixed in get_space_params: when a programme space has no explicit 'width', derive target from sqrt(size/proportion) instead of falling back to width_inside [4.0, 1.0]. Re-scored 35-file corpus: 32 files improved (+1-121%), 5 files lost spurious width fails. All 109 tests pass. Upstream Perl fix tracked as homemaker-py-8fe.","dependencies":[{"issue_id":"homemaker-py-can","depends_on_id":"homemaker-py-uxz","type":"blocks","created_at":"2026-06-12T00:39:47Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-can","title":"Programme width defaults: t3 contradiction (impossible width_inside default)","description":"DESIGN.md §8.2, confirmed in source. t3 (3 m2 WC) has no width spec so inherits width_inside [4.0, 1.0] (Fitness/Base.pm:60) — geometrically impossible; designs 'pass' only by failing size instead. Fix AFTER faithful-port validation (port-faithfully-first policy, §8.1): a sane width default scaled to area (e.g. sqrt(area/proportion)) or per-room widths in patterns.config. Applies to native fitness; optionally upstream to Urb.","acceptance_criteria":"No programme space has a default width incompatible with its target area; corpus re-scored and effect documented","status":"closed","priority":3,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:39:01Z","created_by":"Bruno Postle","updated_at":"2026-06-13T21:21:37Z","started_at":"2026-06-13T21:16:11Z","closed_at":"2026-06-13T21:21:37Z","close_reason":"Fixed in get_space_params: when a programme space has no explicit 'width', derive target from sqrt(size/proportion) instead of falling back to width_inside [4.0, 1.0]. Re-scored 35-file corpus: 32 files improved (+1-121%), 5 files lost spurious width fails. All 109 tests pass. Upstream Perl fix tracked as homemaker-py-8fe.","dependencies":[{"issue_id":"homemaker-py-can","depends_on_id":"homemaker-py-uxz","type":"blocks","created_at":"2026-06-12T00:39:47Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-yg5","title":"Penalty reshaping: replace 0.5^n while preserving inner-loop protection","description":"DESIGN.md §4.7, §5.4, §7 Phase 4, §8.5. The 0.5^n cliff gives the outer search no gradient and rewards flag-count over geometry, but it also PROTECTS the inner loop from trading into new failures (§4.5). One fitness shape cannot naively be both soft outside and cliff-protected inside. Candidates: cliff-inside-inner-loop only, lexicographic (failure count first, score second), additive/soft, multi-objective Pareto. Must preserve the missing-space failure hierarchy (worse to drop a room than to have a poor one). Measure landscape + search outcomes; this helps Urb today too.","acceptance_criteria":"Chosen scheme documented with measurements: search improves while inner loop still never trades into new failures","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:39:00Z","created_by":"Bruno Postle","updated_at":"2026-06-14T08:16:14Z","started_at":"2026-06-14T07:55:32Z","closed_at":"2026-06-14T08:16:14Z","close_reason":"Implemented lexicographic outer-search comparison (-n_fails, fitness). Inner loop unchanged (0.5^n cliff protection preserved). Experiment penalty_reshape.py confirms 0/9 fail regressions in inner loop and shows lex avoids the 3-fail trap that scalar hits 1/3 of the time. Fixed stale _CHILD_INNER_KW sigmas entry.","dependencies":[{"issue_id":"homemaker-py-yg5","depends_on_id":"homemaker-py-uxz","type":"blocks","created_at":"2026-06-12T00:39:46Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-yg5","title":"Penalty reshaping: replace 0.5^n while preserving inner-loop protection","description":"DESIGN.md §4.7, §5.4, §7 Phase 4, §8.5. The 0.5^n cliff gives the outer search no gradient and rewards flag-count over geometry, but it also PROTECTS the inner loop from trading into new failures (§4.5). One fitness shape cannot naively be both soft outside and cliff-protected inside. Candidates: cliff-inside-inner-loop only, lexicographic (failure count first, score second), additive/soft, multi-objective Pareto. Must preserve the missing-space failure hierarchy (worse to drop a room than to have a poor one). Measure landscape + search outcomes; this helps Urb today too.","acceptance_criteria":"Chosen scheme documented with measurements: search improves while inner loop still never trades into new failures","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:39:00Z","created_by":"Bruno Postle","updated_at":"2026-06-14T08:16:14Z","started_at":"2026-06-14T07:55:32Z","closed_at":"2026-06-14T08:16:14Z","close_reason":"Implemented lexicographic outer-search comparison (-n_fails, fitness). Inner loop unchanged (0.5^n cliff protection preserved). Experiment penalty_reshape.py confirms 0/9 fail regressions in inner loop and shows lex avoids the 3-fail trap that scalar hits 1/3 of the time. Fixed stale _CHILD_INNER_KW sigmas entry.","dependencies":[{"issue_id":"homemaker-py-yg5","depends_on_id":"homemaker-py-uxz","type":"blocks","created_at":"2026-06-12T00:39:46Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-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":"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":"deceptive-valleys-in-topology-search-when-every-single","value":"Deceptive valleys in topology search: when every single-step mutation from a target state passes through a high-fail intermediary (e.g. level_fix displaces a room into 5+ new fails), a compound operator that atomically applies two coordinated changes can escape. Design compound operators to land on the low-fail state directly, bypassing the deceptive gradient. Programme-house example: level_compound_fix atomically moves the level-constrained room AND re-inserts the displaced room adjacent to C in one step (operators.py, 2026-06-14)."}
|
|
||||||
{"_type":"memory","key":"homemaker-py-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":"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":"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":"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":"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":"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":"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":"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":"user-preference-bruno-this-is-a-fedora-system","value":"User preference (Bruno): this is a Fedora system — NEVER install Python packages via pip without asking first; always ask whether to install the rpm via dnf (e.g. python3-cma) before considering pip. Applies to any dependency additions."}
|
||||||
|
{"_type":"memory","key":"warm-x0-initialization-bug-pattern-when-a-topology","value":"warm_x0 initialization bug pattern: when a topology operator explicitly sets division ratios on a newly-created node (e.g. compound_fix sets node.division=[0.25,0.25] for t3), parent.ratios has no entry for that node (it was a leaf). warm_x0 defaults it to 0.5, corrupting the inner loop's starting point and making the operator invisible to lex comparison. Fix: only propagate child ratios for nodes where the parent node was NOT already divided; stale hidden nodes revealed by structural mutations (swap flipping b.below) must NOT contribute their pre-writeback values. See driver.py lines 259-267 (fixed 2026-06-14)."}
|
||||||
|
{"_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":"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":"correction-to-urb-fitness-bug-memory-bruno-2026","value":"CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase."}
|
||||||
|
{"_type":"memory","key":"homemaker-py-pythonpath-set-pythonpath-home-bruno-src","value":"homemaker-layout PYTHONPATH: package installed as 'homemaker-layout' via pip install -e . so 'import homemaker_layout' works from anywhere without PYTHONPATH. For running tests use 'python -m pytest' from project root /home/bruno/src/homemaker-layout (pyproject.toml adds src/ automatically). Never try pip show homemaker — that's the old homemaker-addon conflict."}
|
||||||
|
{"_type":"memory","key":"adjacency-in-binary-slicing-tree-is-structural-not","value":"Adjacency in binary slicing tree is structural, not geometric: the inner-loop NM cannot fix topological adjacency failures. Two paths exist: (1) tree-sibling adjacency — a node is adjacent to its sibling in the tree; (2) cross-zone geometric adjacency — leaves from different subtrees that happen to share a boundary. Staircase/adjacency fails require a topology mutation that changes which nodes are siblings or which zones touch. This was proved empirically on programme-house: staircase fail from rot=0 layout could not be fixed by NM but was fixed by level_retype creating a two-C topology (2026-06-14/15)."}
|
||||||
|
{"_type":"memory","key":"cli-tool-style-prefer-python-m-homemaker-module","value":"CLI tool style: prefer python -m homemaker.module --parameters pattern, installable via pip install -e . with pyproject.toml entry_points. Not standalone bin/ scripts."}
|
||||||
|
{"_type":"memory","key":"multi-storey-staircase-consistency-when-dividing-or-retyping","value":"Multi-storey staircase consistency: when dividing or retyping a circulation (C) leaf at one level, the same structural change should be propagated to the matching leaf on ALL other storeys so the stair core path is maintained. The optimizer cannot fix staircase disruptions through trial-and-error geometry alone — it requires a synchronized multi-level operator that applies the same topology change to every storey simultaneously."}
|
||||||
|
{"_type":"memory","key":"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":"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":"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."}
|
||||||
|
|
|
||||||
110
DESIGN.md
110
DESIGN.md
|
|
@ -1156,7 +1156,8 @@ URB_NO_OCCLUSION=1 python3 experiments/run_staged_search.py \
|
||||||
|
|
||||||
Each run executed exactly 20000 native evals across 250 topologies (~36 min,
|
Each run executed exactly 20000 native evals across 250 topologies (~36 min,
|
||||||
~9.1 evals/s) and re-scored native-consistent (`→ OK`). The best layout (seed 0,
|
~9.1 evals/s) and re-scored native-consistent (`→ OK`). The best layout (seed 0,
|
||||||
145 fails) is saved as `examples/maple-court/generated.dom` with its `.fails`.
|
145 fails) was saved as `examples/maple-court/generated.dom` with its `.fails`
|
||||||
|
(superseded in §12.2 by the proportion-aware 126-fail layout).
|
||||||
The single-stage harness (`run_search_scaled.py`) also accepts the programme
|
The single-stage harness (`run_search_scaled.py`) also accepts the programme
|
||||||
unchanged. The score prints near-zero (`0.5^145` fail cliff) — the **fail count**
|
unchanged. The score prints near-zero (`0.5^145` fail cliff) — the **fail count**
|
||||||
is the yardstick.
|
is the yardstick.
|
||||||
|
|
@ -1166,3 +1167,110 @@ is the yardstick.
|
||||||
harbor's ~85; this is the scaling yardstick `leu.2` (proportion-aware seeding)
|
harbor's ~85; this is the scaling yardstick `leu.2` (proportion-aware seeding)
|
||||||
and the re-scoped `9gp` are measured against. The residual character is the same
|
and the re-scoped `9gp` are measured against. The residual character is the same
|
||||||
geometry/shape family flagged at the close of §11.7.
|
geometry/shape family flagged at the close of §11.7.
|
||||||
|
|
||||||
|
### 12.2 Proportion-aware constructive seeding (`homemaker-py-leu.2`) — DONE (positive)
|
||||||
|
|
||||||
|
Premise (follow-up to §11.6/§11.7). The constructive seeders grow geometry with
|
||||||
|
uniform `[0.5, 0.5]` cuts *before* types are assigned, so the raw seed is "more,
|
||||||
|
smaller leaves" of equal area: a room with a large programme target comes out too
|
||||||
|
small, a small room too big, and the inner loop must recover all of
|
||||||
|
size/width/proportion from scratch. With the adjacency load now cut by seeding
|
||||||
|
(§11.6/§11.7), this geometry residual is the dominant remaining term. Attacking it
|
||||||
|
at the seed — in the proven *construction* direction — is far cheaper than the
|
||||||
|
`9gp` encoding rewrite.
|
||||||
|
|
||||||
|
**Implementation (`operators._size_divisions_from_targets`, flag
|
||||||
|
`seed_proportion_aware`, env `PROP`, default-on per the A/B below).** After the
|
||||||
|
adjacency-aware type assignment (§11.6/§11.7, left exactly as is), each leaf
|
||||||
|
carries a target area — a sized room's programme `size`; circulation/outside
|
||||||
|
absorb the plot slack (floored at `0.4 ×` mean room area so a circulation leaf
|
||||||
|
never shrinks below door-width and undoes the §11.6 adjacency win). Because
|
||||||
|
`division=[f, f]` cuts off left area-fraction `f` (rotation-independent —
|
||||||
|
verified), bottom-up subtree-target sums compose multiplicatively to give every
|
||||||
|
leaf area ∝ its target. **Area alone regressed the raw seed**, though: choosing
|
||||||
|
only the cut *fraction* to hit a target *area* slices thin slivers with terrible
|
||||||
|
aspect (proportion/width/edge-too-long fails swamp the size gain — measured
|
||||||
|
below). So each cut also picks the **rotation** (the two distinct cut directions)
|
||||||
|
that makes its two children squarest; rotation depends on realised parent
|
||||||
|
geometry, so the pass runs top-down. Both ratio and rotation derive from the
|
||||||
|
target dims; neither touches topology or type assignment. Threaded through
|
||||||
|
`driver.search`/`search_staged(seed_proportion_aware=…)`.
|
||||||
|
|
||||||
|
- *Raw-seed fails (10 seeds, single-stage constructive, before optimisation),
|
||||||
|
area-only vs area+rotation:*
|
||||||
|
|
||||||
|
| family | harbor before | area-only | area+rot |
|
||||||
|
|-------------|--------------:|----------:|---------:|
|
||||||
|
| geometry | 123.0 | 135.9 | **99.9** |
|
||||||
|
| access/adj | 19.1 | 23.8 | 20.4 |
|
||||||
|
| total | 144.1 | 162.1 | **123.7** |
|
||||||
|
|
||||||
|
Area-only makes geometry *worse* (slivers); area+rotation drops the geometry
|
||||||
|
family on every programme — harbor **123.0 → 99.9 (−19 %)**, programme-house
|
||||||
|
**13.1 → 8.7 (−34 %)**, maple-court **200.5 → 164.1 (−18 %)**. Access/adjacency
|
||||||
|
regresses slightly (rotation shifts the leaf graph the adjacency assignment was
|
||||||
|
computed against): harbor +1.3, prog-house +2.4, maple +3.4 — far smaller than
|
||||||
|
the geometry gain. The size family in particular falls as intended
|
||||||
|
(harbor size 31.4 → 22.0), and proportion flips from a regression to a win
|
||||||
|
(21.3 → 12.8) once rotation is co-chosen.
|
||||||
|
|
||||||
|
- *End-to-end (total fails at budget, 20000 evals, 3 seeds, PROP=0 vs PROP=1;
|
||||||
|
harbor & maple-court staged):*
|
||||||
|
|
||||||
|
| seed | harbor PROP=0 | harbor PROP=1 | maple PROP=0 | maple PROP=1 |
|
||||||
|
|-----:|--------------:|--------------:|-------------:|-------------:|
|
||||||
|
| 0 | 97 | 72 | 145 | 126 |
|
||||||
|
| 1 | 78 | 81 | 158 | 148 |
|
||||||
|
| 2 | 81 | 69 | 152 | 134 |
|
||||||
|
| mean | **85.3** | **74.0** | **151.7** | **136.0** |
|
||||||
|
|
||||||
|
Harbor **−13 % (best 69, was 78)**, maple-court **−10 % (best 126, was 145)**.
|
||||||
|
PROP=0 reproduces the §11.7 staged harbor (85.3) and §12.1 maple baseline
|
||||||
|
(151.7) *exactly* — clean controls. Proportion-aware seeding is the first
|
||||||
|
Phase-7 lever to move the fail count on the larger-than-house benchmark.
|
||||||
|
|
||||||
|
- *A storey-count bug surfaced (`homemaker-py-cq1`).* programme-house has
|
||||||
|
`storey_minimum: 2` but all rooms `level: 0`, and `n_storeys_required` only read
|
||||||
|
`level:` keys — so the constructive seeder built a **1-storey** seed for a
|
||||||
|
2-storey programme and `search_staged` fell through to plain search. Fixed
|
||||||
|
(`programme.storey_minimum`/`n_storeys_for`; `driver.search` passes `min_storeys`
|
||||||
|
to the seeder; `search_staged` routes on `max(level-derived, storey_minimum)`).
|
||||||
|
No-op for harbor/maple (level-derived already ≥ storey_minimum); independent win
|
||||||
|
on programme-house (single-stage baseline **8.0 → 5.0** with a correct 2-storey
|
||||||
|
seed).
|
||||||
|
|
||||||
|
- *programme-house regresses, but it is a convergence-speed artifact, not a worse
|
||||||
|
optimum.* On the 6-room programme proportion-aware seeding loses at 20000 evals
|
||||||
|
on every path tested (single-stage 1-storey 8.0→11.7, single-stage 2-storey
|
||||||
|
5.0→8.3, staged 2-storey 4.3→6.0). The mechanism is a *deeper local optimum*:
|
||||||
|
the equal-area PROP=0 seed has badly-proportioned leaves, so `undivide` moves —
|
||||||
|
the route to programme-house's simpler optimum — are accepted as improvements;
|
||||||
|
the well-fitted PROP=1 seed makes `undivide` an immediate fitness drop (merging
|
||||||
|
two good leaves yields one bad one), walling off the restructuring path. A
|
||||||
|
budget sweep (staged, storey-fixed) shows this is *reachability speed*, not an
|
||||||
|
asymptotic trap:
|
||||||
|
|
||||||
|
| budget | PROP=0 (s0/s1) | PROP=1 (s0/s1) |
|
||||||
|
|-------:|---------------:|---------------:|
|
||||||
|
| 20000 | 4 / 5 | 8 / 6 |
|
||||||
|
| 60000 | 2 / 2 | 4 / 3 |
|
||||||
|
| 150000 | 2 / 0 | **1** / 10 |
|
||||||
|
|
||||||
|
PROP=1 reaches **1 fail** (seed 0, 150k — beating PROP=0's 2; best-known is 2),
|
||||||
|
so it is not trapped; the gap narrows with budget and crosses over. (Staged
|
||||||
|
splits budget by *fraction*, so runs at different budgets evolve different
|
||||||
|
Stage-1 bases and are not nested — hence the high variance, e.g. PROP=1 seed 1
|
||||||
|
swinging 3→10.) The same "deeper basin" that *helps* where the constructed
|
||||||
|
topology is roughly right (large programmes, scarce budget) *delays* convergence
|
||||||
|
where the seed must be restructured (small programmes).
|
||||||
|
|
||||||
|
- *Verdict: keep proportion-aware split sizing, default-on (`seed_proportion_aware`
|
||||||
|
default `True`, env `PROP=1`).* It is a measured win on both larger programmes —
|
||||||
|
harbor −13 %, the maple-court scaling benchmark −10 % — exactly the regime
|
||||||
|
Phase 7 targets and the basis the re-scoped `9gp` is measured on. The only
|
||||||
|
regression is a small-programme convergence-speed effect that washes out with
|
||||||
|
budget (PROP=1 reaches the known floor), with no evidence of an asymptotic
|
||||||
|
penalty, so default-on is not paid for by a worse optimum anywhere. The win is
|
||||||
|
rotation-and-ratio sizing from target dims; the bare ratio is not enough
|
||||||
|
(area-only regressed). Area sizing assumes total target ≈ plot area; choosing
|
||||||
|
the cut *direction* for aspect is what makes it pay.
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
83
experiments/measure_seed_geometry.py
Normal file
83
experiments/measure_seed_geometry.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Raw-seed fail breakdown: proportion-aware split sizing A/B (homemaker-py-leu.2).
|
||||||
|
|
||||||
|
Builds the *constructive* seed (operators.constructive_topology, single-stage,
|
||||||
|
adjacency-aware) with proportion-aware split sizing OFF then ON, scores each raw
|
||||||
|
seed with native fitness BEFORE the inner loop, and tallies fails by family. The
|
||||||
|
claim under test (DESIGN.md §12.2): sizing cuts from target areas drops the
|
||||||
|
geometry family (size/width/proportion/crinkliness) at the raw seed without
|
||||||
|
regressing the adjacency/access family that §11.6/§11.7 fixed.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
URB_NO_OCCLUSION=1 python3 experiments/measure_seed_geometry.py <programme_dir> [n_seeds]
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||||
|
from homemaker_layout import dom, fitness, operators, programme # noqa: E402
|
||||||
|
|
||||||
|
GEOM = ("perpendicular", "proportion", "size", "width", "crinkliness")
|
||||||
|
|
||||||
|
|
||||||
|
def _categorise(fails: tuple[str, ...]) -> dict[str, int]:
|
||||||
|
cats = {"geometry": 0, "access_adj": 0, "missing": 0, "other": 0}
|
||||||
|
for f in fails:
|
||||||
|
if f.endswith(GEOM) or "edge too long" in f:
|
||||||
|
cats["geometry"] += 1
|
||||||
|
elif "access" in f or "inaccessible" in f or "adjacent" in f or "adjacency" in f:
|
||||||
|
cats["access_adj"] += 1
|
||||||
|
elif "missing" in f:
|
||||||
|
cats["missing"] += 1
|
||||||
|
else:
|
||||||
|
cats["other"] += 1
|
||||||
|
return cats
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
programme_dir = Path(sys.argv[1])
|
||||||
|
n_seeds = int(sys.argv[2]) if len(sys.argv) > 2 else 10
|
||||||
|
|
||||||
|
reqs = programme.load_programme_dir(programme_dir)
|
||||||
|
types = sorted(reqs) + ["C", "O"]
|
||||||
|
conf, cost = fitness.load_config(programme_dir)
|
||||||
|
fit = fitness.Fitness(conf, cost)
|
||||||
|
|
||||||
|
seed_file = programme_dir / "init.dom"
|
||||||
|
seed_root = dom.load(str(seed_file))
|
||||||
|
|
||||||
|
print(f"programme : {programme_dir.name} ({len(reqs)} codes)")
|
||||||
|
print(f"seeds : {n_seeds} (single-stage constructive, adjacency-aware)\n")
|
||||||
|
|
||||||
|
agg = {0: {"total": 0, "geometry": 0, "access_adj": 0, "missing": 0, "other": 0},
|
||||||
|
1: {"total": 0, "geometry": 0, "access_adj": 0, "missing": 0, "other": 0}}
|
||||||
|
|
||||||
|
for s in range(n_seeds):
|
||||||
|
for prop in (False, True):
|
||||||
|
rng = np.random.default_rng(s)
|
||||||
|
topo = operators.constructive_topology(
|
||||||
|
seed_root, reqs, rng, types,
|
||||||
|
adjacency_aware=True, proportion_aware=prop)
|
||||||
|
_score, fails = fit.score_with_fails(copy.deepcopy(topo))
|
||||||
|
cats = _categorise(fails)
|
||||||
|
agg[int(prop)]["total"] += len(fails)
|
||||||
|
for k, v in cats.items():
|
||||||
|
agg[int(prop)][k] += v
|
||||||
|
|
||||||
|
def _avg(d, k):
|
||||||
|
return d[k] / n_seeds
|
||||||
|
|
||||||
|
print(f"{'family':<12} {'PROP=0 (before)':>16} {'PROP=1 (after)':>16}")
|
||||||
|
for k in ("geometry", "access_adj", "missing", "other", "total"):
|
||||||
|
print(f"{k:<12} {_avg(agg[0], k):>16.1f} {_avg(agg[1], k):>16.1f}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
49
experiments/run_leu2_ab.sh
Executable file
49
experiments/run_leu2_ab.sh
Executable file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# leu.2 end-to-end A/B: proportion-aware split sizing (PROP=0 vs PROP=1).
|
||||||
|
# programme-house is single-stage (1 storey); harbor & maple-court are staged.
|
||||||
|
# Concurrency capped at 2 (machine has ~2 GB free). Results -> scratch/leu2/summary.tsv
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
OUT=scratch/leu2
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
SUMMARY="$OUT/summary.tsv"
|
||||||
|
: > "$SUMMARY"
|
||||||
|
BUDGET=${BUDGET:-20000}
|
||||||
|
SEEDS=${SEEDS:-"0 1 2"}
|
||||||
|
MAXJOBS=${MAXJOBS:-2}
|
||||||
|
|
||||||
|
run_one() {
|
||||||
|
local prog=$1 harness=$2 prop=$3 seed=$4
|
||||||
|
local tag="${prog}_p${prop}_s${seed}"
|
||||||
|
local log="$OUT/${tag}.log"
|
||||||
|
URB_NO_OCCLUSION=1 PROP=$prop python3 "experiments/$harness" \
|
||||||
|
"examples/$prog" "$BUDGET" "$seed" "examples/$prog/init.dom" \
|
||||||
|
"$OUT/${tag}.dom" > "$log" 2>&1
|
||||||
|
local best
|
||||||
|
best=$(grep -oE 'best *: [0-9.eE+-]+ \([0-9]+ fails\)' "$log" | grep -oE '\([0-9]+ fails\)' | grep -oE '[0-9]+' | tail -1)
|
||||||
|
printf '%s\t%s\t%s\t%s\t%s\n' "$prog" "$prop" "$seed" "${best:-ERR}" "$tag" >> "$SUMMARY"
|
||||||
|
echo "DONE $tag -> ${best:-ERR} fails"
|
||||||
|
}
|
||||||
|
|
||||||
|
JOBS=()
|
||||||
|
for prog_h in "programme-house:run_search_scaled.py" \
|
||||||
|
"harbor-house:run_staged_search.py" \
|
||||||
|
"maple-court:run_staged_search.py"; do
|
||||||
|
prog=${prog_h%%:*}; harness=${prog_h##*:}
|
||||||
|
for prop in 0 1; do
|
||||||
|
for seed in $SEEDS; do
|
||||||
|
JOBS+=("$prog|$harness|$prop|$seed")
|
||||||
|
done
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "queued ${#JOBS[@]} jobs, budget=$BUDGET, maxjobs=$MAXJOBS"
|
||||||
|
for job in "${JOBS[@]}"; do
|
||||||
|
IFS='|' read -r prog harness prop seed <<< "$job"
|
||||||
|
while [ "$(jobs -rp | wc -l)" -ge "$MAXJOBS" ]; do wait -n; done
|
||||||
|
run_one "$prog" "$harness" "$prop" "$seed" &
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
echo "ALL DONE"
|
||||||
|
sort "$SUMMARY" -o "$SUMMARY"
|
||||||
|
cat "$SUMMARY"
|
||||||
|
|
@ -74,6 +74,7 @@ def main() -> int:
|
||||||
rp = os.environ.get("RESTART_PATIENCE")
|
rp = os.environ.get("RESTART_PATIENCE")
|
||||||
restart_patience = int(rp) if rp else None
|
restart_patience = int(rp) if rp else None
|
||||||
adj = os.environ.get("ADJ", "1") == "1" # s44 adjacency-aware seeding
|
adj = os.environ.get("ADJ", "1") == "1" # s44 adjacency-aware seeding
|
||||||
|
prop = os.environ.get("PROP", "1") == "1" # leu.2 proportion-aware split sizing (default-on)
|
||||||
|
|
||||||
print(f"programme : {programme_dir.name}")
|
print(f"programme : {programme_dir.name}")
|
||||||
print(f"seed : {seed_file.name}")
|
print(f"seed : {seed_file.name}")
|
||||||
|
|
@ -83,6 +84,7 @@ def main() -> int:
|
||||||
print(f"niche : {niche}")
|
print(f"niche : {niche}")
|
||||||
print(f"restart_p : {restart_patience}")
|
print(f"restart_p : {restart_patience}")
|
||||||
print(f"adj_aware : {adj}")
|
print(f"adj_aware : {adj}")
|
||||||
|
print(f"prop_aware: {prop}")
|
||||||
print(flush=True)
|
print(flush=True)
|
||||||
|
|
||||||
seed_root = dom.load(str(seed_file))
|
seed_root = dom.load(str(seed_file))
|
||||||
|
|
@ -102,6 +104,7 @@ def main() -> int:
|
||||||
niche_by_signature=niche,
|
niche_by_signature=niche,
|
||||||
restart_patience=restart_patience,
|
restart_patience=restart_patience,
|
||||||
seed_adjacency_aware=adj,
|
seed_adjacency_aware=adj,
|
||||||
|
seed_proportion_aware=prop,
|
||||||
# urb_root not needed: use_native=True is the default
|
# urb_root not needed: use_native=True is the default
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ def main() -> int:
|
||||||
rp = os.environ.get("RESTART_PATIENCE")
|
rp = os.environ.get("RESTART_PATIENCE")
|
||||||
restart_patience = int(rp) if rp else None
|
restart_patience = int(rp) if rp else None
|
||||||
adj = os.environ.get("ADJ", "1") == "1" # s44/ld5 adjacency-aware seeding A/B
|
adj = os.environ.get("ADJ", "1") == "1" # s44/ld5 adjacency-aware seeding A/B
|
||||||
|
prop = os.environ.get("PROP", "1") == "1" # leu.2 proportion-aware split sizing (default-on)
|
||||||
|
|
||||||
print(f"programme : {programme_dir.name}")
|
print(f"programme : {programme_dir.name}")
|
||||||
print(f"seed : {seed_file.name}")
|
print(f"seed : {seed_file.name}")
|
||||||
|
|
@ -65,6 +66,7 @@ def main() -> int:
|
||||||
print(f"niche : {niche}")
|
print(f"niche : {niche}")
|
||||||
print(f"restart_p : {restart_patience}")
|
print(f"restart_p : {restart_patience}")
|
||||||
print(f"adj_aware : {adj}")
|
print(f"adj_aware : {adj}")
|
||||||
|
print(f"prop_aware: {prop}")
|
||||||
print(flush=True)
|
print(flush=True)
|
||||||
|
|
||||||
seed_root = dom.load(str(seed_file))
|
seed_root = dom.load(str(seed_file))
|
||||||
|
|
@ -86,6 +88,7 @@ def main() -> int:
|
||||||
niche_by_signature=niche,
|
niche_by_signature=niche,
|
||||||
restart_patience=restart_patience,
|
restart_patience=restart_patience,
|
||||||
seed_adjacency_aware=adj,
|
seed_adjacency_aware=adj,
|
||||||
|
seed_proportion_aware=prop,
|
||||||
)
|
)
|
||||||
|
|
||||||
elapsed = time.perf_counter() - t0
|
elapsed = time.perf_counter() - t0
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@ def search(
|
||||||
restart_patience: int | None = None,
|
restart_patience: int | None = None,
|
||||||
restart_elite: int = 1,
|
restart_elite: int = 1,
|
||||||
seed_adjacency_aware: bool = True,
|
seed_adjacency_aware: bool = True,
|
||||||
|
seed_proportion_aware: bool = True,
|
||||||
) -> 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``
|
||||||
|
|
@ -227,6 +228,8 @@ def search(
|
||||||
_key = lambda ind: _rank_fitness(ind)
|
_key = lambda ind: _rank_fitness(ind)
|
||||||
# Always load reqs so bootstrap_n_leaves can be auto-derived from programme.
|
# Always load reqs so bootstrap_n_leaves can be auto-derived from programme.
|
||||||
reqs = programme.load_programme_dir(programme_dir)
|
reqs = programme.load_programme_dir(programme_dir)
|
||||||
|
# Constructive seed must honour storey_minimum, not just level: keys (§12.2).
|
||||||
|
min_storeys = programme.storey_minimum(programme_dir)
|
||||||
if types is None:
|
if types is None:
|
||||||
# Urb's generic types are canonically UPPERCASE (get_space_types:
|
# Urb's generic types are canonically UPPERCASE (get_space_types:
|
||||||
# qw/C O S/; the corpus is 100% uppercase). Predicates match
|
# qw/C O S/; the corpus is 100% uppercase). Predicates match
|
||||||
|
|
@ -327,7 +330,9 @@ def search(
|
||||||
return (seed_factory(rng), None, child_budget, {}, f"lift/{tag}")
|
return (seed_factory(rng), None, child_budget, {}, f"lift/{tag}")
|
||||||
if prog:
|
if prog:
|
||||||
topo = operators.constructive_topology(
|
topo = operators.constructive_topology(
|
||||||
seed_root, reqs, rng, types, adjacency_aware=seed_adjacency_aware)
|
seed_root, reqs, rng, types, min_storeys=min_storeys,
|
||||||
|
adjacency_aware=seed_adjacency_aware,
|
||||||
|
proportion_aware=seed_proportion_aware)
|
||||||
return (topo, None, child_budget, {}, f"construct/{tag}")
|
return (topo, None, child_budget, {}, f"construct/{tag}")
|
||||||
n = int(rng.integers(max(1, n_target - 1), n_target + 2))
|
n = int(rng.integers(max(1, n_target - 1), n_target + 2))
|
||||||
return (random_topology(seed_root, n, rng, types), None, child_budget,
|
return (random_topology(seed_root, n, rng, types), None, child_budget,
|
||||||
|
|
@ -447,6 +452,7 @@ def search_staged(
|
||||||
restart_patience: int | None = None,
|
restart_patience: int | None = None,
|
||||||
restart_elite: int = 1,
|
restart_elite: int = 1,
|
||||||
seed_adjacency_aware: bool = True,
|
seed_adjacency_aware: bool = True,
|
||||||
|
seed_proportion_aware: bool = True,
|
||||||
) -> SearchResult:
|
) -> SearchResult:
|
||||||
"""Staged per-floor topology search (DESIGN.md §11.3, ``homemaker-py-c4c.3``).
|
"""Staged per-floor topology search (DESIGN.md §11.3, ``homemaker-py-c4c.3``).
|
||||||
|
|
||||||
|
|
@ -471,7 +477,11 @@ def search_staged(
|
||||||
from . import graph
|
from . import graph
|
||||||
|
|
||||||
reqs = programme.load_programme_dir(programme_dir)
|
reqs = programme.load_programme_dir(programme_dir)
|
||||||
n_storeys = programme.n_storeys_required(reqs)
|
# Honour storey_minimum even when no room is pinned to an upper level (§12.2):
|
||||||
|
# e.g. programme-house is storey_minimum:2 with all rooms level:0, so its
|
||||||
|
# valid solutions are multi-storey and it must stage, not fall through.
|
||||||
|
n_storeys = max(programme.n_storeys_required(reqs),
|
||||||
|
programme.storey_minimum(programme_dir))
|
||||||
|
|
||||||
def _log(msg: str) -> None:
|
def _log(msg: str) -> None:
|
||||||
if log:
|
if log:
|
||||||
|
|
@ -485,7 +495,8 @@ def search_staged(
|
||||||
inner_kw=inner_kw, log=log, n_workers=n_workers,
|
inner_kw=inner_kw, log=log, n_workers=n_workers,
|
||||||
use_grade=use_grade, niche_by_signature=niche_by_signature,
|
use_grade=use_grade, niche_by_signature=niche_by_signature,
|
||||||
restart_patience=restart_patience, restart_elite=restart_elite,
|
restart_patience=restart_patience, restart_elite=restart_elite,
|
||||||
seed_adjacency_aware=seed_adjacency_aware)
|
seed_adjacency_aware=seed_adjacency_aware,
|
||||||
|
seed_proportion_aware=seed_proportion_aware)
|
||||||
|
|
||||||
if types is None:
|
if types is None:
|
||||||
types = sorted(reqs) + ["C", "O"]
|
types = sorted(reqs) + ["C", "O"]
|
||||||
|
|
@ -510,6 +521,7 @@ def search_staged(
|
||||||
niche_by_signature=niche_by_signature,
|
niche_by_signature=niche_by_signature,
|
||||||
restart_patience=restart_patience, restart_elite=restart_elite,
|
restart_patience=restart_patience, restart_elite=restart_elite,
|
||||||
seed_adjacency_aware=seed_adjacency_aware,
|
seed_adjacency_aware=seed_adjacency_aware,
|
||||||
|
seed_proportion_aware=seed_proportion_aware,
|
||||||
)
|
)
|
||||||
best_base = r1.best.root
|
best_base = r1.best.root
|
||||||
_log(f"[staged] stage 1 done: base {r1.best.fitness:.6g} "
|
_log(f"[staged] stage 1 done: base {r1.best.fitness:.6g} "
|
||||||
|
|
@ -525,7 +537,8 @@ def search_staged(
|
||||||
def _seed_factory(rng2):
|
def _seed_factory(rng2):
|
||||||
return operators.lift_base_to_storeys(
|
return operators.lift_base_to_storeys(
|
||||||
best_base, upper, rng2, types, reqs=reqs,
|
best_base, upper, rng2, types, reqs=reqs,
|
||||||
adjacency_aware=seed_adjacency_aware)
|
adjacency_aware=seed_adjacency_aware,
|
||||||
|
proportion_aware=seed_proportion_aware)
|
||||||
|
|
||||||
_log(f"[staged] stage 2: upper floors as deltas, budget {b2}, base_p {base_p}")
|
_log(f"[staged] stage 2: upper floors as deltas, budget {b2}, base_p {base_p}")
|
||||||
r2 = search(
|
r2 = search(
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,79 @@ def _grow_leaves(lvl: dom.Node, n_leaves: int, rng: np.random.Generator) -> None
|
||||||
leaf.type = None
|
leaf.type = None
|
||||||
|
|
||||||
|
|
||||||
|
def _size_divisions_from_targets(lvl: dom.Node, reqs, fmin: float = 0.04,
|
||||||
|
fmax: float = 0.96) -> None:
|
||||||
|
"""Resize each divided node's split ratio from its leaves' TARGET areas.
|
||||||
|
|
||||||
|
leu.2 (DESIGN.md §12.2, follow-up to §11.6/§11.7): the constructive seeders
|
||||||
|
grow geometry with uniform ``[0.5, 0.5]`` cuts *before* types are assigned, so
|
||||||
|
the raw seed is "more, smaller leaves" of equal area — rooms with a large
|
||||||
|
programme target come out too small, small rooms too big, and the inner loop
|
||||||
|
must recover all of size/width/proportion from scratch. Once types are known,
|
||||||
|
every leaf carries a target area (a sized room's ``size``; circulation/outside
|
||||||
|
absorb the slack), and because ``division=[f, f]`` cuts off left area-fraction
|
||||||
|
``f`` (rotation-independent), bottom-up target sums compose multiplicatively to
|
||||||
|
give every leaf area ∝ its target.
|
||||||
|
|
||||||
|
Area alone is not enough: choosing only the cut *fraction* to hit a target
|
||||||
|
*area* slices thin slivers with terrible aspect (proportion/width/edge-too-long
|
||||||
|
fails swamp the size gain — measured, §12.2). So each cut also picks the
|
||||||
|
**rotation** (the two distinct cut directions) that makes its two children
|
||||||
|
squarest. Rotation depends on the realised parent geometry, so the pass runs
|
||||||
|
*top-down*; both the ratio and the rotation derive from the target dims, and
|
||||||
|
neither touches topology or type assignment (§11.6/§11.7 placement is intact).
|
||||||
|
|
||||||
|
Generic (non-sized) leaves get a nominal target: the per-leaf share of the
|
||||||
|
plot slack, floored at ``0.4 ×`` mean room target so a circulation leaf never
|
||||||
|
shrinks to a sub-door-width sliver (which would undo the §11.6 adjacency win).
|
||||||
|
"""
|
||||||
|
from . import geometry
|
||||||
|
|
||||||
|
reqs = reqs or {}
|
||||||
|
geometry.clear_cache()
|
||||||
|
leaves = lvl.leaves()
|
||||||
|
if len(leaves) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
sized = {lf: reqs[lf.type].size for lf in leaves
|
||||||
|
if lf.type in reqs and reqs[lf.type].size > 0}
|
||||||
|
mean_sized = (sum(sized.values()) / len(sized)) if sized else 1.0
|
||||||
|
n_generic = len(leaves) - len(sized)
|
||||||
|
slack = geometry.area(lvl) - sum(sized.values())
|
||||||
|
floor = 0.4 * mean_sized # keep circulation/outside above door-width scale
|
||||||
|
generic_t = max(floor, slack / n_generic) if n_generic else floor
|
||||||
|
target = {lf: sized.get(lf, generic_t) for lf in leaves}
|
||||||
|
|
||||||
|
def _subtree_target(n: dom.Node) -> float:
|
||||||
|
if not n.divided:
|
||||||
|
return max(target.get(n, floor), 1e-6)
|
||||||
|
return _subtree_target(n.left) + _subtree_target(n.right)
|
||||||
|
|
||||||
|
def _rec(n: dom.Node) -> None:
|
||||||
|
if not n.divided:
|
||||||
|
return
|
||||||
|
left = _subtree_target(n.left)
|
||||||
|
f = min(max(left / (left + _subtree_target(n.right)), fmin), fmax)
|
||||||
|
# Pick the cut direction (rotation 0 vs 1; 2/3 mirror these for aspect)
|
||||||
|
# that makes the worse child squarest, given this node's settled geometry.
|
||||||
|
best_rot, best_aspect = n.rotation, None
|
||||||
|
for rot in (0, 1):
|
||||||
|
n.rotation = rot
|
||||||
|
n.division = [f, f]
|
||||||
|
geometry.clear_cache()
|
||||||
|
worst = max(geometry.aspect(n.left), geometry.aspect(n.right))
|
||||||
|
if best_aspect is None or worst < best_aspect:
|
||||||
|
best_aspect, best_rot = worst, rot
|
||||||
|
n.rotation = best_rot
|
||||||
|
n.division = [f, f]
|
||||||
|
geometry.clear_cache()
|
||||||
|
_rec(n.left)
|
||||||
|
_rec(n.right)
|
||||||
|
|
||||||
|
_rec(lvl)
|
||||||
|
geometry.clear_cache()
|
||||||
|
|
||||||
|
|
||||||
def _assign_adjacency_aware(lvl: dom.Node, room_codes: list[str], reqs,
|
def _assign_adjacency_aware(lvl: dom.Node, room_codes: list[str], reqs,
|
||||||
rng: np.random.Generator, door_width: float = 1.2,
|
rng: np.random.Generator, door_width: float = 1.2,
|
||||||
fixed_circ: "list[dom.Node] | None" = None) -> None:
|
fixed_circ: "list[dom.Node] | None" = None) -> None:
|
||||||
|
|
@ -493,7 +566,8 @@ def _assign_adjacency_aware(lvl: dom.Node, room_codes: list[str], reqs,
|
||||||
|
|
||||||
def constructive_topology(seed_root: dom.Node, reqs, rng: np.random.Generator,
|
def constructive_topology(seed_root: dom.Node, reqs, rng: np.random.Generator,
|
||||||
types: list[str], min_storeys: int = 1,
|
types: list[str], min_storeys: int = 1,
|
||||||
adjacency_aware: bool = True) -> dom.Node:
|
adjacency_aware: bool = True,
|
||||||
|
proportion_aware: bool = True) -> dom.Node:
|
||||||
"""Build a seed that instantiates every required space by construction.
|
"""Build a seed that instantiates every required space by construction.
|
||||||
|
|
||||||
The §11.0 diagnosis: random divide+retype chains leave required programme
|
The §11.0 diagnosis: random divide+retype chains leave required programme
|
||||||
|
|
@ -556,12 +630,22 @@ def constructive_topology(seed_root: dom.Node, reqs, rng: np.random.Generator,
|
||||||
for slot, leaf_idx in enumerate(order):
|
for slot, leaf_idx in enumerate(order):
|
||||||
leaves[int(leaf_idx)].type = assign[slot] if slot < len(assign) else "O"
|
leaves[int(leaf_idx)].type = assign[slot] if slot < len(assign) else "O"
|
||||||
|
|
||||||
|
if proportion_aware:
|
||||||
|
# leu.2: now that leaves are typed, replace the uniform 0.5 cuts with
|
||||||
|
# target-proportional ratios so the raw seed sits near feasible size/
|
||||||
|
# width/proportion. Topology and type assignment are unchanged. Link
|
||||||
|
# first so upper-storey roots resolve geometry (the else branch above
|
||||||
|
# does not link, unlike the adjacency-aware branch).
|
||||||
|
dom._link(child)
|
||||||
|
_size_divisions_from_targets(lvl, reqs)
|
||||||
|
|
||||||
return _finalise(child)
|
return _finalise(child)
|
||||||
|
|
||||||
|
|
||||||
def lift_base_to_storeys(base_root: dom.Node, upper_buckets: list[dict[str, int]],
|
def lift_base_to_storeys(base_root: dom.Node, upper_buckets: list[dict[str, int]],
|
||||||
rng: np.random.Generator, types: list[str],
|
rng: np.random.Generator, types: list[str],
|
||||||
reqs=None, adjacency_aware: bool = True) -> dom.Node:
|
reqs=None, adjacency_aware: bool = True,
|
||||||
|
proportion_aware: bool = True) -> dom.Node:
|
||||||
"""Stack upper storeys onto an evolved single-storey base (DESIGN.md §11.3).
|
"""Stack upper storeys onto an evolved single-storey base (DESIGN.md §11.3).
|
||||||
|
|
||||||
Stage 2 seeder: the Stage-1 base is the credible ground floor and is left
|
Stage 2 seeder: the Stage-1 base is the credible ground floor and is left
|
||||||
|
|
@ -635,6 +719,15 @@ def lift_base_to_storeys(base_root: dom.Node, upper_buckets: list[dict[str, int]
|
||||||
core_node.type = "C" # keep the inherited core as circulation
|
core_node.type = "C" # keep the inherited core as circulation
|
||||||
prev.above = dup
|
prev.above = dup
|
||||||
|
|
||||||
|
if proportion_aware:
|
||||||
|
# leu.2: size the upper-floor cuts from target areas too. The base is
|
||||||
|
# the evolved Stage-1 ground floor and is left untouched; only the
|
||||||
|
# constructed upper storey's ratios are rewritten. (Cuts inherited from
|
||||||
|
# the base via below-links are no-ops here — their geometry is fixed
|
||||||
|
# below — so this best-effort sizes the floor's own new divisions.)
|
||||||
|
dom._link(child)
|
||||||
|
_size_divisions_from_targets(dup, reqs)
|
||||||
|
|
||||||
prev = dup
|
prev = dup
|
||||||
|
|
||||||
return _finalise(child)
|
return _finalise(child)
|
||||||
|
|
|
||||||
|
|
@ -169,12 +169,8 @@ def write_stage1_programme(
|
||||||
return out_dir
|
return out_dir
|
||||||
|
|
||||||
|
|
||||||
def load_programme_dir(directory: str | Path) -> dict[str, SpaceReq]:
|
def _load_merged_conf(directory: "str | Path") -> dict:
|
||||||
"""Load programme from a directory, merging parent patterns.config as base.
|
"""Merge ``../patterns.config`` then the local one (mirrors load_config)."""
|
||||||
|
|
||||||
Mirrors urb-evolve.pl: ../patterns.config loaded first, then the local
|
|
||||||
file's top-level keys override it (same shallow-merge as fitness.load_config).
|
|
||||||
"""
|
|
||||||
from pathlib import Path as _Path
|
from pathlib import Path as _Path
|
||||||
directory = _Path(directory)
|
directory = _Path(directory)
|
||||||
conf: dict = {}
|
conf: dict = {}
|
||||||
|
|
@ -182,4 +178,32 @@ def load_programme_dir(directory: str | Path) -> dict[str, SpaceReq]:
|
||||||
if p.is_file():
|
if p.is_file():
|
||||||
with open(p) as fh:
|
with open(p) as fh:
|
||||||
conf.update(yaml.safe_load(fh) or {})
|
conf.update(yaml.safe_load(fh) or {})
|
||||||
return _parse_spaces(conf)
|
return conf
|
||||||
|
|
||||||
|
|
||||||
|
def load_programme_dir(directory: str | Path) -> dict[str, SpaceReq]:
|
||||||
|
"""Load programme from a directory, merging parent patterns.config as base.
|
||||||
|
|
||||||
|
Mirrors urb-evolve.pl: ../patterns.config loaded first, then the local
|
||||||
|
file's top-level keys override it (same shallow-merge as fitness.load_config).
|
||||||
|
"""
|
||||||
|
return _parse_spaces(_load_merged_conf(directory))
|
||||||
|
|
||||||
|
|
||||||
|
def storey_minimum(directory: str | Path) -> int:
|
||||||
|
"""Minimum storey count the programme requires (``storey_minimum`` key).
|
||||||
|
|
||||||
|
Independent of ``level:`` keys: a programme can demand N storeys via
|
||||||
|
``storey_minimum`` without pinning any room to an upper floor (e.g.
|
||||||
|
programme-house: ``storey_minimum: 2`` but all rooms ``level: 0``). The
|
||||||
|
constructive seeder and the staged/plain dispatch must honour it, else the
|
||||||
|
seed is built one storey short and fitness fires a ``storey minimum`` fail the
|
||||||
|
search has to repair structurally (DESIGN.md §12.2).
|
||||||
|
"""
|
||||||
|
return int(_load_merged_conf(directory).get("storey_minimum") or 1)
|
||||||
|
|
||||||
|
|
||||||
|
def n_storeys_for(directory: str | Path) -> int:
|
||||||
|
"""Storeys the programme implies: the max of level-derived and storey_minimum."""
|
||||||
|
reqs = load_programme_dir(directory)
|
||||||
|
return max(n_storeys_required(reqs), storey_minimum(directory))
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue