Phase 6 §11.2: programme-aware construction + missing-room repair (c4c.2)
Make the required programme room set a constructive invariant instead of something the topology search must stumble onto by random divide+retype. - operators.constructive_topology: bootstrap seeder that sizes each storey to its required rooms (partitioned by level; level-free rooms distributed), +1 core C and +1 O per storey, then assigns types. Stochastic for population diversity. Wired into driver bootstrap when the programme has required spaces. - operators.mutate_place_missing: repair op that inserts a missing required space by dividing a host leaf into [room | remainder]. Lex-safe host ranking (generic O first, never displace a required room); honours required level. Weight 2.0 in the mutation mix; noops cheaply once the set is complete. A/B on harbor-house (20k evals, seed 0, identical config): old random-bootstrap 133 fails (103 missing, 77%) new constructive 105 fails ( 12 missing, 11%) -21% total, missing-stack collapsed; seed head-start 163->139. §4.10 regression PASS: warmstart-2f4 still reaches a 1-fail population at 50k. Verdict (DESIGN.md §11.2): construction is necessary and reframes the bottleneck to quality-fail packing of a complete dense design (crinkliness/ size/access/edge) -> unblocks §11.3 staging, motivates §11.4 graded objective. Follow-up filed (homemaker-py-s44): adjacency-aware seeding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
43be2fe5ab
commit
de60200bbc
5 changed files with 300 additions and 27 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{"id":"homemaker-py-c4c.3","title":"Staged per-floor search (curriculum: credible base floor, then upper floors as deltas)","description":"Search the genome in its causal dependency order. The base-floor tree is the master; upper storeys are deltas (Below-inheritance). The programme partitions rooms by required level (harbor: 10 L0, 4 L1, 2 free), so each floor's target room set is known up front. Today the search discovers both floors simultaneously via random typing + the rare/drastic level_add (weighted 0.2) — an uncontrolled, degenerate version of staging.\nStage 1 — base floor: search the single-storey tree over the level-0 room set, dimensionality reduced (one tree, no deltas).\nStage 2 — upper floors as deltas: seed each upper storey with ITS required room set (via the construction op, homemaker-py-c4c.2), search the deltas; keep the base MUTABLE at low probability so it can adapt to upper-floor pressure.\nCRITICAL non-goal: do NOT hard-freeze the base. A base optimised purely as ground floor is a §4.2-style partial objective and can be a bad SUBSTRATE. Stage 1 objective must include (a) a reserved, vertically-alignable circulation core and (b) a substrate-readiness term: enough divisible area/cut structure to host the level-1 room set later.","design":"Premise gated by homemaker-py-c4c.1: only high-value if single-storey construction already reaches low fails. Substrate-readiness proxy candidates: count of base leaves large enough to subdivide for L1 rooms; presence of a core node with vertical continuity. Stage transition: when stage-1 base hits a fails/score threshold or budget fraction, freeze-soft and open the delta genome. Composes with canonical encoding (homemaker-py-9gp) — deltas are where redundancy/coarse moves hurt most.","acceptance_criteria":"Staged search beats single-stage on harbor-house (best fails/score), measured at equal native-fitness budget and recorded in DESIGN.md §11.x + bead notes. Reserved-core + substrate-readiness shown to prevent the bungalow trap (stage-2 does not have to carve a core from scratch — track core-carving moves). No regression on programme-house.","status":"open","priority":1,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-17T19:01:01Z","created_by":"Bruno Postle","updated_at":"2026-06-17T19:01:01Z","dependencies":[{"issue_id":"homemaker-py-c4c.3","depends_on_id":"homemaker-py-c4c","type":"parent-child","created_at":"2026-06-17T20:01:00Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-c4c.3","depends_on_id":"homemaker-py-c4c.1","type":"blocks","created_at":"2026-06-17T20:01:00Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-c4c.3","depends_on_id":"homemaker-py-c4c.2","type":"blocks","created_at":"2026-06-17T20:01:01Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-c4c.3","title":"Staged per-floor search (curriculum: credible base floor, then upper floors as deltas)","description":"Search the genome in its causal dependency order. The base-floor tree is the master; upper storeys are deltas (Below-inheritance). The programme partitions rooms by required level (harbor: 10 L0, 4 L1, 2 free), so each floor's target room set is known up front. Today the search discovers both floors simultaneously via random typing + the rare/drastic level_add (weighted 0.2) — an uncontrolled, degenerate version of staging.\nStage 1 — base floor: search the single-storey tree over the level-0 room set, dimensionality reduced (one tree, no deltas).\nStage 2 — upper floors as deltas: seed each upper storey with ITS required room set (via the construction op, homemaker-py-c4c.2), search the deltas; keep the base MUTABLE at low probability so it can adapt to upper-floor pressure.\nCRITICAL non-goal: do NOT hard-freeze the base. A base optimised purely as ground floor is a §4.2-style partial objective and can be a bad SUBSTRATE. Stage 1 objective must include (a) a reserved, vertically-alignable circulation core and (b) a substrate-readiness term: enough divisible area/cut structure to host the level-1 room set later.","design":"Premise gated by homemaker-py-c4c.1: only high-value if single-storey construction already reaches low fails. Substrate-readiness proxy candidates: count of base leaves large enough to subdivide for L1 rooms; presence of a core node with vertical continuity. Stage transition: when stage-1 base hits a fails/score threshold or budget fraction, freeze-soft and open the delta genome. Composes with canonical encoding (homemaker-py-9gp) — deltas are where redundancy/coarse moves hurt most.","acceptance_criteria":"Staged search beats single-stage on harbor-house (best fails/score), measured at equal native-fitness budget and recorded in DESIGN.md §11.x + bead notes. Reserved-core + substrate-readiness shown to prevent the bungalow trap (stage-2 does not have to carve a core from scratch — track core-carving moves). No regression on programme-house.","status":"open","priority":1,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-17T19:01:01Z","created_by":"Bruno Postle","updated_at":"2026-06-17T19:01:01Z","dependencies":[{"issue_id":"homemaker-py-c4c.3","depends_on_id":"homemaker-py-c4c","type":"parent-child","created_at":"2026-06-17T20:01:00Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-c4c.3","depends_on_id":"homemaker-py-c4c.1","type":"blocks","created_at":"2026-06-17T20:01:00Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-c4c.3","depends_on_id":"homemaker-py-c4c.2","type":"blocks","created_at":"2026-06-17T20:01:01Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-c4c.2","title":"Programme-aware construction + missing-room repair operator","description":"Highest-leverage fix for the epic's diagnosis. Today mutate_divide (operators.py:71) types new leaves at RANDOM, so required programme spaces go missing -\u003e 'missing' stacking dominates fitness on full programmes (harbor: 6 missing-room records stacking critical+size+width+adjacency+level). Make the required room set a constructive invariant rather than something the search must stumble onto.\nTwo parts:\n1. Constructive seeder: generate initial topologies that instantiate each required space (respecting count/level/type) by construction, instead of random divide+retype chains.\n2. Repair operator mutate_place_missing: detect a required-but-absent space and insert it (divide a compatible leaf, type the new leaf to the missing code, prefer a slot satisfying its adjacency). Complements mutate_level_compound_fix (which repairs level, not presence).\nWire the seeder into driver bootstrap and the repair op into mutate() weights.","design":"Seeder must place generic C (circulation/core) and O (outside) too, not just programme codes. Keep it stochastic (diverse population) but biased to cover the required set + correct levels. Repair op should be lex-safe: prefer insertions that don't create more new fails than the missing-stack it removes (cf. the §4.10 deceptive-valley lesson — a naive insert dumps a room into a bad slot and nets worse).","acceptance_criteria":"On harbor-house, 'missing'-type failures collapse to ~0 across the population (record before/after fail histograms); measured net-fail improvement vs current 74-fail out1.dom baseline, recorded in DESIGN.md §11.x + bead notes. No regression on seeded programme-house (still reaches 1-fail optimum, §4.10).","status":"open","priority":1,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-17T18:51:21Z","created_by":"Bruno Postle","updated_at":"2026-06-17T18:51:21Z","dependencies":[{"issue_id":"homemaker-py-c4c.2","depends_on_id":"homemaker-py-c4c","type":"parent-child","created_at":"2026-06-17T19:51:20Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":3,"comment_count":0}
|
{"id":"homemaker-py-c4c.2","title":"Programme-aware construction + missing-room repair operator","description":"Highest-leverage fix for the epic's diagnosis. Today mutate_divide (operators.py:71) types new leaves at RANDOM, so required programme spaces go missing -\u003e 'missing' stacking dominates fitness on full programmes (harbor: 6 missing-room records stacking critical+size+width+adjacency+level). Make the required room set a constructive invariant rather than something the search must stumble onto.\nTwo parts:\n1. Constructive seeder: generate initial topologies that instantiate each required space (respecting count/level/type) by construction, instead of random divide+retype chains.\n2. Repair operator mutate_place_missing: detect a required-but-absent space and insert it (divide a compatible leaf, type the new leaf to the missing code, prefer a slot satisfying its adjacency). Complements mutate_level_compound_fix (which repairs level, not presence).\nWire the seeder into driver bootstrap and the repair op into mutate() weights.","design":"Seeder must place generic C (circulation/core) and O (outside) too, not just programme codes. Keep it stochastic (diverse population) but biased to cover the required set + correct levels. Repair op should be lex-safe: prefer insertions that don't create more new fails than the missing-stack it removes (cf. the §4.10 deceptive-valley lesson — a naive insert dumps a room into a bad slot and nets worse).","acceptance_criteria":"On harbor-house, 'missing'-type failures collapse to ~0 across the population (record before/after fail histograms); measured net-fail improvement vs current 74-fail out1.dom baseline, recorded in DESIGN.md §11.x + bead notes. No regression on seeded programme-house (still reaches 1-fail optimum, §4.10).","notes":"Empirically confirmed as the prerequisite by c4c.1: single-storey harbor-l0 (13 rooms, no coupling) stalls at 33 fails, 39% of them 'missing' (the m×3 counted space is never constructed). Highest-leverage Phase-6 fix. See DESIGN.md §11.1.","status":"in_progress","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-17T18:51:21Z","created_by":"Bruno Postle","updated_at":"2026-06-17T20:19:39Z","started_at":"2026-06-17T20:19:39Z","dependencies":[{"issue_id":"homemaker-py-c4c.2","depends_on_id":"homemaker-py-c4c","type":"parent-child","created_at":"2026-06-17T19:51:20Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":3,"comment_count":0}
|
||||||
{"id":"homemaker-py-c4c.1","title":"Experiment: single-storey harbor premise test (per-floor construction vs multi-storey coupling)","description":"De-risk the staged-search and construction work BEFORE building either. Strip harbor-house to its 10 level-0 rooms as a single-storey programme; run the current memetic search from a bare plot; record best fails/score and the fail-type histogram. This isolates the question: is the bottleneck per-floor CONSTRUCTION (placing the right room set on one floor) or the multi-storey COUPLING (deltas, core alignment, level constraints)?\n- If single-storey 10-room reaches near-zero fails: the difficulty is coupling -\u003e staged per-floor search (homemaker-py-\u003cstaging\u003e) is the high-value lever.\n- If it still stalls at many fails (esp. 'missing'): per-floor construction itself is the bottleneck -\u003e programme-aware construction (homemaker-py-\u003cconstruction\u003e) is required first and staging alone won't rescue it.\nRun from blank-slate (init.dom equivalent) AND from a bootstrap population; report both.","design":"Build examples/harbor-house-l0/ from harbor's level-0 spaces only (drop level: keys or set all to 0; keep adjacency among the retained codes). Reuse experiments/run_search_scaled.py harness. Cheap (~minutes at native-fitness throughput).","acceptance_criteria":"Single-storey 10-room harbor variant created and committed under examples/; current search run and best fails/score + fail histogram recorded in DESIGN.md (new §11.x) and bead notes; explicit verdict on construction-vs-coupling.","notes":"VERDICT: per-floor CONSTRUCTION is the bottleneck, not multi-storey coupling.\nBuilt examples/harbor-house-l0/ (10 explicit level:0 codes = 13 room instances, single-storey constraints), seeded from bare init.dom.\nRun: URB_NO_OCCLUSION=1 python3 experiments/run_search_scaled.py examples/harbor-house-l0 20000 0 examples/harbor-house-l0/init.dom examples/harbor-house-l0/generated.dom\nResult: 20000 evals / 250 topologies / 234s. Best 33 fails (fitness 2.25e-12, deep in 0.5^n regime); whole pop stuck 33-35. 40-\u003e33 over full budget. NOT near zero.\nFail histogram: 13 missing (all 3 m meeting rooms never built) + 6 adjacency + 4 access + 4 size + 2 edge-too-long + 2 crinkliness + 1 proportion + 1 too-few-stairs(single-storey artifact). Missing = 39% — matches the 'still stalls esp. missing' branch.\n=\u003e c4c.2 (programme-aware construction + missing-room repair) is the prerequisite; staging (c4c.3) alone won't rescue it. c4c.3 already correctly depends on both. Full writeup in DESIGN.md §11.1.","status":"in_progress","priority":1,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-17T18:49:43Z","created_by":"Bruno Postle","updated_at":"2026-06-17T20:15:23Z","started_at":"2026-06-17T19:25:49Z","dependencies":[{"issue_id":"homemaker-py-c4c.1","depends_on_id":"homemaker-py-c4c","type":"parent-child","created_at":"2026-06-17T19:49:43Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
|
{"id":"homemaker-py-c4c.1","title":"Experiment: single-storey harbor premise test (per-floor construction vs multi-storey coupling)","description":"De-risk the staged-search and construction work BEFORE building either. Strip harbor-house to its 10 level-0 rooms as a single-storey programme; run the current memetic search from a bare plot; record best fails/score and the fail-type histogram. This isolates the question: is the bottleneck per-floor CONSTRUCTION (placing the right room set on one floor) or the multi-storey COUPLING (deltas, core alignment, level constraints)?\n- If single-storey 10-room reaches near-zero fails: the difficulty is coupling -\u003e staged per-floor search (homemaker-py-\u003cstaging\u003e) is the high-value lever.\n- If it still stalls at many fails (esp. 'missing'): per-floor construction itself is the bottleneck -\u003e programme-aware construction (homemaker-py-\u003cconstruction\u003e) is required first and staging alone won't rescue it.\nRun from blank-slate (init.dom equivalent) AND from a bootstrap population; report both.","design":"Build examples/harbor-house-l0/ from harbor's level-0 spaces only (drop level: keys or set all to 0; keep adjacency among the retained codes). Reuse experiments/run_search_scaled.py harness. Cheap (~minutes at native-fitness throughput).","acceptance_criteria":"Single-storey 10-room harbor variant created and committed under examples/; current search run and best fails/score + fail histogram recorded in DESIGN.md (new §11.x) and bead notes; explicit verdict on construction-vs-coupling.","notes":"VERDICT: per-floor CONSTRUCTION is the bottleneck, not multi-storey coupling.\nBuilt examples/harbor-house-l0/ (10 explicit level:0 codes = 13 room instances, single-storey constraints), seeded from bare init.dom.\nRun: URB_NO_OCCLUSION=1 python3 experiments/run_search_scaled.py examples/harbor-house-l0 20000 0 examples/harbor-house-l0/init.dom examples/harbor-house-l0/generated.dom\nResult: 20000 evals / 250 topologies / 234s. Best 33 fails (fitness 2.25e-12, deep in 0.5^n regime); whole pop stuck 33-35. 40-\u003e33 over full budget. NOT near zero.\nFail histogram: 13 missing (all 3 m meeting rooms never built) + 6 adjacency + 4 access + 4 size + 2 edge-too-long + 2 crinkliness + 1 proportion + 1 too-few-stairs(single-storey artifact). Missing = 39% — matches the 'still stalls esp. missing' branch.\n=\u003e c4c.2 (programme-aware construction + missing-room repair) is the prerequisite; staging (c4c.3) alone won't rescue it. c4c.3 already correctly depends on both. Full writeup in DESIGN.md §11.1.","status":"closed","priority":1,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-17T18:49:43Z","created_by":"Bruno Postle","updated_at":"2026-06-17T20:15:36Z","started_at":"2026-06-17T19:25:49Z","closed_at":"2026-06-17T20:15:36Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-c4c.1","depends_on_id":"homemaker-py-c4c","type":"parent-child","created_at":"2026-06-17T19:49:43Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
|
||||||
{"id":"homemaker-py-c4c","title":"Phase 6: topology-search quality for full/multi-storey programmes","description":"Diagnosis (survey 2026-06-17): the delivered speedups (native fitness ~140x, geometry inner loop ~1.6x) landed in the two layers that were never the bottleneck. The geometry inner loop polishes WITHIN a failure tier (DESIGN.md §4.5/§4.7: 0 fail changes, by design — the 0.5^n cliff protects it). But final design quality is dominated by FAILURE COUNT, which is almost entirely a topology property. Topology search on full programmes is the weakness:\n- blank-slate programme-house (init.dom): memetic stalls at 18 fails vs urb-evolve 6 (§7 Phase 2 verdict);\n- harbor-house (16 rooms): out1.dom=74 fails, generated.dom=130 fails, both at ~machine-epsilon score; fails dominated by 'missing' room stacking (each missing room stacks critical+size+width+adjacency+level, §6).\nSmoking gun: operators.mutate_divide (operators.py:71) assigns each new leaf a RANDOM type from programme-codes+C+O. Nothing guarantees the required programme spaces are instantiated, so on a large programme required rooms go missing -\u003e catastrophic 0.5^n stacking, and the search is a random walk over type assignments with a flat/catastrophic gradient in the high-fail regime.\nThis epic groups the topology-search-quality work: programme-aware construction, staged per-floor search, graded high-fail objective, topology diversity, then the canonical-encoding capstone. Everything experiment-driven; results recorded in DESIGN.md sections + bead notes.","design":"Causal frame: base-floor tree is the master genome; upper storeys are divide/undivide deltas (Below-inheritance); the programme partitions rooms by required level (harbor: 10 on L0, 4 on L1, 2 free). So construction and search should follow the genome's dependency order: credible base floor first, upper floors as deltas, with required-room sets known per floor from the programme. Do NOT hard-freeze the base when adding floors — that recreates the §4.2 partial-objective trap at the topology level (a base optimised purely as ground floor can be a bad SUBSTRATE: vertical core must stay aligned, load-bearing walls must stack). Curriculum, not freeze.","acceptance_criteria":"Memetic search reaches a competitive low-fail design on harbor-house (16 rooms, multi-storey) and on blank-slate programme-house, beating the current 74/18-fail plateaus; each child bead lands its experiment with results recorded in DESIGN.md.","status":"open","priority":1,"issue_type":"epic","owner":"bruno@postle.net","created_at":"2026-06-17T18:45:39Z","created_by":"Bruno Postle","updated_at":"2026-06-17T18:45:39Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-c4c","title":"Phase 6: topology-search quality for full/multi-storey programmes","description":"Diagnosis (survey 2026-06-17): the delivered speedups (native fitness ~140x, geometry inner loop ~1.6x) landed in the two layers that were never the bottleneck. The geometry inner loop polishes WITHIN a failure tier (DESIGN.md §4.5/§4.7: 0 fail changes, by design — the 0.5^n cliff protects it). But final design quality is dominated by FAILURE COUNT, which is almost entirely a topology property. Topology search on full programmes is the weakness:\n- blank-slate programme-house (init.dom): memetic stalls at 18 fails vs urb-evolve 6 (§7 Phase 2 verdict);\n- harbor-house (16 rooms): out1.dom=74 fails, generated.dom=130 fails, both at ~machine-epsilon score; fails dominated by 'missing' room stacking (each missing room stacks critical+size+width+adjacency+level, §6).\nSmoking gun: operators.mutate_divide (operators.py:71) assigns each new leaf a RANDOM type from programme-codes+C+O. Nothing guarantees the required programme spaces are instantiated, so on a large programme required rooms go missing -\u003e catastrophic 0.5^n stacking, and the search is a random walk over type assignments with a flat/catastrophic gradient in the high-fail regime.\nThis epic groups the topology-search-quality work: programme-aware construction, staged per-floor search, graded high-fail objective, topology diversity, then the canonical-encoding capstone. Everything experiment-driven; results recorded in DESIGN.md sections + bead notes.","design":"Causal frame: base-floor tree is the master genome; upper storeys are divide/undivide deltas (Below-inheritance); the programme partitions rooms by required level (harbor: 10 on L0, 4 on L1, 2 free). So construction and search should follow the genome's dependency order: credible base floor first, upper floors as deltas, with required-room sets known per floor from the programme. Do NOT hard-freeze the base when adding floors — that recreates the §4.2 partial-objective trap at the topology level (a base optimised purely as ground floor can be a bad SUBSTRATE: vertical core must stay aligned, load-bearing walls must stack). Curriculum, not freeze.","acceptance_criteria":"Memetic search reaches a competitive low-fail design on harbor-house (16 rooms, multi-storey) and on blank-slate programme-house, beating the current 74/18-fail plateaus; each child bead lands its experiment with results recorded in DESIGN.md.","status":"open","priority":1,"issue_type":"epic","owner":"bruno@postle.net","created_at":"2026-06-17T18:45:39Z","created_by":"Bruno Postle","updated_at":"2026-06-17T18:45:39Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-mz5","title":"Python native fitness evaluation (port urb-fitness.pl)","description":"We need a Python implementation of the urb-fitness scoring tool that is faithful to the Perl oracle (urb-fitness.pl / ProgrammeDriven.pm). This is the 'native fitness' component identified in DESIGN.md §6 as gating topology search at scale — the oracle requires a subprocess+file roundtrip per eval which is too slow for large populations.\n\nThe native fitness must reproduce all scoring terms from the Perl source:\n- size, width, proportion (per-space Gaussian scoring)\n- adjacency, access/inaccessible, crinkliness, perpendicular\n- level, staircase volume/count, public access\n- circulation \u0026 outside ratios, min internal area\n\nSource of truth: /home/bruno/src/urb/lib/Urb/Dom/Fitness/ProgrammeDriven.pm and the Storey/Building/Leaf/Base submodules.\n\nValidation target: match oracle scores on the programme-house corpus (35+ .dom files) to within the ~3.7% gap documented in homemaker-py-gpx.","status":"closed","priority":1,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-15T22:18:06Z","created_by":"Bruno Postle","updated_at":"2026-06-17T17:51:53Z","closed_at":"2026-06-17T17:51:53Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-mz5","title":"Python native fitness evaluation (port urb-fitness.pl)","description":"We need a Python implementation of the urb-fitness scoring tool that is faithful to the Perl oracle (urb-fitness.pl / ProgrammeDriven.pm). This is the 'native fitness' component identified in DESIGN.md §6 as gating topology search at scale — the oracle requires a subprocess+file roundtrip per eval which is too slow for large populations.\n\nThe native fitness must reproduce all scoring terms from the Perl source:\n- size, width, proportion (per-space Gaussian scoring)\n- adjacency, access/inaccessible, crinkliness, perpendicular\n- level, staircase volume/count, public access\n- circulation \u0026 outside ratios, min internal area\n\nSource of truth: /home/bruno/src/urb/lib/Urb/Dom/Fitness/ProgrammeDriven.pm and the Storey/Building/Leaf/Base submodules.\n\nValidation target: match oracle scores on the programme-house corpus (35+ .dom files) to within the ~3.7% gap documented in homemaker-py-gpx.","status":"closed","priority":1,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-15T22:18:06Z","created_by":"Bruno Postle","updated_at":"2026-06-17T17:51:53Z","closed_at":"2026-06-17T17:51:53Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-40i","title":"Investigate cf0b8a77e8b2325f ~18% raw_value discrepancy (py lower than oracle)","description":"For prefix cf0b8a77e8b2325f: oracle=1.079112e-03 py=9.133243e-04 ratio=0.8464 (python is ~18% too low). debug_nfails shows py n_fails=5 oracle n_fails=5 (same failures), stair_fits=[1.3145] in python, building_factor=0.1104 (vs oracle's implied ~0.1303). The discrepancy is in raw_value (py=11837 vs oracle implied ~13975) or possibly building_factor. Need to check: (1) per-leaf quality values (crinkliness, area_outside, access) via debug_quality.txt; (2) whether the stair corners differ (cf/rl: py=[2,3] perl=[2,3] — SAME, so corners ok); (3) any quality term not yet ported or computed differently. Run debug_quality.py and compare per-leaf contributions.","status":"closed","priority":1,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-13T18:08:22Z","created_by":"Bruno Postle","updated_at":"2026-06-13T19:54:04Z","started_at":"2026-06-13T18:12:23Z","closed_at":"2026-06-13T19:54:04Z","close_reason":"Investigation complete: traced 18% discrepancy (cf0b8a77) through entrance corner logic and weighted path length bugs, both now fixed in w1e.","dependencies":[{"issue_id":"homemaker-py-40i","depends_on_id":"homemaker-py-hgg","type":"blocks","created_at":"2026-06-13T19:08:30Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
{"id":"homemaker-py-40i","title":"Investigate cf0b8a77e8b2325f ~18% raw_value discrepancy (py lower than oracle)","description":"For prefix cf0b8a77e8b2325f: oracle=1.079112e-03 py=9.133243e-04 ratio=0.8464 (python is ~18% too low). debug_nfails shows py n_fails=5 oracle n_fails=5 (same failures), stair_fits=[1.3145] in python, building_factor=0.1104 (vs oracle's implied ~0.1303). The discrepancy is in raw_value (py=11837 vs oracle implied ~13975) or possibly building_factor. Need to check: (1) per-leaf quality values (crinkliness, area_outside, access) via debug_quality.txt; (2) whether the stair corners differ (cf/rl: py=[2,3] perl=[2,3] — SAME, so corners ok); (3) any quality term not yet ported or computed differently. Run debug_quality.py and compare per-leaf contributions.","status":"closed","priority":1,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-13T18:08:22Z","created_by":"Bruno Postle","updated_at":"2026-06-13T19:54:04Z","started_at":"2026-06-13T18:12:23Z","closed_at":"2026-06-13T19:54:04Z","close_reason":"Investigation complete: traced 18% discrepancy (cf0b8a77) through entrance corner logic and weighted path length bugs, both now fixed in w1e.","dependencies":[{"issue_id":"homemaker-py-40i","depends_on_id":"homemaker-py-hgg","type":"blocks","created_at":"2026-06-13T19:08:30Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
{"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-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).","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-17T21:50:01Z","created_by":"Bruno Postle","updated_at":"2026-06-17T21:50:01Z","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.","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-17T19:12:54Z","created_by":"Bruno Postle","updated_at":"2026-06-17T19:12:54Z","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.","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-17T19:12:54Z","created_by":"Bruno Postle","updated_at":"2026-06-17T19:12:54Z","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).","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-17T19:12:18Z","created_by":"Bruno Postle","updated_at":"2026-06-17T19:12:18Z","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).","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-17T19:12:18Z","created_by":"Bruno Postle","updated_at":"2026-06-17T19:12:18Z","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-g0b","title":"homemaker-fitness: native Python CLI replacement for urb-fitness.pl","description":"We need a Python CLI tool that replicates the behaviour of urb-fitness.pl so we can score .dom files without shelling out to Perl. The tool should: accept .dom file paths as arguments (or glob *.dom in cwd if none given), load patterns.config and costs.config from cwd and parent dir (local overrides project-level), skip scoring if .score and .fails files are already newer than the .dom (unless FORCE_UPDATE env var is set), score each .dom using fitness.Fitness.score_with_fails(), write the score to \u003cdom\u003e.score (40-digit float format), write the failures to \u003cdom\u003e.fails, print the score to stderr. Expose as homemaker-fitness entry point in pyproject.toml and as python -m homemaker_layout.fitness_cmd module. This replaces the oracle.py shelling-out path for Phase 3 native fitness.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-14T12:32:29Z","created_by":"Bruno Postle","updated_at":"2026-06-14T16:17:21Z","started_at":"2026-06-14T12:32:52Z","closed_at":"2026-06-14T16:17:21Z","close_reason":"Implemented as homemaker_layout/fitness_cmd.py with homemaker-fitness entry point; exact score parity verified against urb-fitness.pl on corpus","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-g0b","title":"homemaker-fitness: native Python CLI replacement for urb-fitness.pl","description":"We need a Python CLI tool that replicates the behaviour of urb-fitness.pl so we can score .dom files without shelling out to Perl. The tool should: accept .dom file paths as arguments (or glob *.dom in cwd if none given), load patterns.config and costs.config from cwd and parent dir (local overrides project-level), skip scoring if .score and .fails files are already newer than the .dom (unless FORCE_UPDATE env var is set), score each .dom using fitness.Fitness.score_with_fails(), write the score to \u003cdom\u003e.score (40-digit float format), write the failures to \u003cdom\u003e.fails, print the score to stderr. Expose as homemaker-fitness entry point in pyproject.toml and as python -m homemaker_layout.fitness_cmd module. This replaces the oracle.py shelling-out path for Phase 3 native fitness.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-14T12:32:29Z","created_by":"Bruno Postle","updated_at":"2026-06-14T16:17:21Z","started_at":"2026-06-14T12:32:52Z","closed_at":"2026-06-14T16:17:21Z","close_reason":"Implemented as homemaker_layout/fitness_cmd.py with homemaker-fitness entry point; exact score parity verified against urb-fitness.pl on corpus","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
@ -37,16 +38,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":"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":"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":"urb-fitness-bug-found-fixed-2026-06-12","value":"Urb fitness bug found+fixed 2026-06-12 (patch in /home/bruno/src/urb, uncommitted): ProgrammeDriven.pm ratio_o/ratio_type grepped case-insensitively over the ratios hash and took the FIRST key — nondeterministic (x4.5 score swings) for designs with mixed-case type classes (both 'c' circulation and 'C' covered). Fixed to SUM the class (matches Is_Circulation//Is_Outside semantics); 35/35 corpus scores unchanged. CRITICAL for homemaker-py-3y7/gnw: the native port must implement class-SUM ratios. Building.pm has the same unpatched pattern (site-driven path, not used by our oracle). Also: the memetic search reward-hacked this bug before the fix — search results predating it are noise artifacts."}
|
||||||
|
{"_type":"memory","key":"user-preference-bruno-this-is-a-fedora-system","value":"User preference (Bruno): this is a Fedora system — NEVER install Python packages via pip without asking first; always ask whether to install the rpm via dnf (e.g. python3-cma) before considering pip. Applies to any dependency additions."}
|
||||||
{"_type":"memory","key":"programme-house-optimisation-result-2026-06-14-15","value":"Programme-house optimisation result (2026-06-14/15): best achievable is 1 fail (l1 wrong level, score ~0.005). 0 fails is geometrically impossible: l1 (min 27m²) must occupy ll (~23m²) at level 0, which eliminates the t3-adj-C provider; dividing ll into lll(l1)+llr(C) gives llr proportion ~6:1 (fails). Python memetic optimizer achieves 1 fail in 50k evals vs Perl optimiser's 2-3 fails. Winning topology: TWO C nodes at level 0 — ll(C) for t3-adj-C via geometric contact, rl(C) for staircase via tree-sibling adjacency to rrr(O). Best .dom: scratch/from-warmstart-fixed.dom and scratch/from-compound3-fixed.dom."}
|
{"_type":"memory","key":"programme-house-optimisation-result-2026-06-14-15","value":"Programme-house optimisation result (2026-06-14/15): best achievable is 1 fail (l1 wrong level, score ~0.005). 0 fails is geometrically impossible: l1 (min 27m²) must occupy ll (~23m²) at level 0, which eliminates the t3-adj-C provider; dividing ll into lll(l1)+llr(C) gives llr proportion ~6:1 (fails). Python memetic optimizer achieves 1 fail in 50k evals vs Perl optimiser's 2-3 fails. Winning topology: TWO C nodes at level 0 — ll(C) for t3-adj-C via geometric contact, rl(C) for staircase via tree-sibling adjacency to rrr(O). Best .dom: scratch/from-warmstart-fixed.dom and scratch/from-compound3-fixed.dom."}
|
||||||
|
{"_type":"memory","key":"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":"homemaker-py-pythonpath-set-pythonpath-home-bruno-src","value":"homemaker-layout PYTHONPATH: package installed as 'homemaker-layout' via pip install -e . so 'import homemaker_layout' works from anywhere without PYTHONPATH. For running tests use 'python -m pytest' from project root /home/bruno/src/homemaker-layout (pyproject.toml adds src/ automatically). Never try pip show homemaker — that's the old homemaker-addon conflict."}
|
||||||
{"_type":"memory","key":"urb-oracle-nondeterminism-urb-fitness-pl-output-varies","value":"Urb oracle nondeterminism: urb-fitness.pl output varies run-to-run from Perl hash-order randomisation — .fails line ORDER shuffles (compare sorted, use oracle.Score.fail_lines) and the score float can flip by ~1 ULP (compare with math.isclose rel_tol=1e-12, never ==). Not a batching artifact; affects single runs too. Matters for the Phase 3 native-fitness parity gate (homemaker-py-uxz)."}
|
{"_type":"memory","key":"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":"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":"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":"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":"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":"deceptive-valleys-in-topology-search-when-every-single","value":"Deceptive valleys in topology search: when every single-step mutation from a target state passes through a high-fail intermediary (e.g. level_fix displaces a room into 5+ new fails), a compound operator that atomically applies two coordinated changes can escape. Design compound operators to land on the low-fail state directly, bypassing the deceptive gradient. Programme-house example: level_compound_fix atomically moves the level-constrained room AND re-inserts the displaced room adjacent to C in one step (operators.py, 2026-06-14)."}
|
|
||||||
{"_type":"memory","key":"multi-storey-staircase-consistency-when-dividing-or-retyping","value":"Multi-storey staircase consistency: when dividing or retyping a circulation (C) leaf at one level, the same structural change should be propagated to the matching leaf on ALL other storeys so the stair core path is maintained. The optimizer cannot fix staircase disruptions through trial-and-error geometry alone — it requires a synchronized multi-level operator that applies the same topology change to every storey simultaneously."}
|
|
||||||
{"_type":"memory","key":"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":"correction-to-urb-fitness-bug-memory-bruno-2026","value":"CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase."}
|
{"_type":"memory","key":"correction-to-urb-fitness-bug-memory-bruno-2026","value":"CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase."}
|
||||||
{"_type":"memory","key":"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":"deceptive-valleys-in-topology-search-when-every-single","value":"Deceptive valleys in topology search: when every single-step mutation from a target state passes through a high-fail intermediary (e.g. level_fix displaces a room into 5+ new fails), a compound operator that atomically applies two coordinated changes can escape. Design compound operators to land on the low-fail state directly, bypassing the deceptive gradient. Programme-house example: level_compound_fix atomically moves the level-constrained room AND re-inserts the displaced room adjacent to C in one step (operators.py, 2026-06-14)."}
|
||||||
{"_type":"memory","key":"urb-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."}
|
||||||
|
{"_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)."}
|
||||||
|
|
|
||||||
73
DESIGN.md
73
DESIGN.md
|
|
@ -734,16 +734,73 @@ set is an unambiguous single floor. Seeded from the bare plot (`init.dom`).
|
||||||
**§11.2 programme-aware construction + missing-room repair is the prerequisite,
|
**§11.2 programme-aware construction + missing-room repair is the prerequisite,
|
||||||
and staging alone (§11.3) will not rescue it.** §11.3 stays blocked on §11.2.
|
and staging alone (§11.3) will not rescue it.** §11.3 stays blocked on §11.2.
|
||||||
|
|
||||||
### 11.2 Programme-aware construction + missing-room repair (`homemaker-py-c4c.2`)
|
### 11.2 Programme-aware construction + missing-room repair (`homemaker-py-c4c.2`) — DONE
|
||||||
|
|
||||||
*Stub.* Constructive seeder that instantiates each required space
|
Two changes (`operators.py`, wired in `driver.py`):
|
||||||
(count/level/type) + `mutate_place_missing` repair operator. Highest-leverage
|
|
||||||
fix for the §11.0 diagnosis.
|
|
||||||
|
|
||||||
- *Gate:* `missing`-type failures collapse to ~0 across the harbor population;
|
1. **`constructive_topology`** — bootstrap seeder that makes the required room
|
||||||
net-fail improvement vs the 74-fail `out1.dom` baseline; no regression on the
|
set a *constructive invariant*. It sizes each storey to its required rooms
|
||||||
seeded programme-house 1-fail optimum (§4.10).
|
(partitioning by `level`; level-free rooms distributed round-robin over a
|
||||||
- *Result:* TODO — before/after fail histograms, numbers, verdict.
|
shuffled order), plus one circulation `C` and one outside `O` per storey,
|
||||||
|
grows the slicing tree to that leaf count, and assigns the types. Stochastic
|
||||||
|
(random splits/rotations, shuffled type→leaf assignment) so a bootstrap batch
|
||||||
|
is still a diverse population. Replaces the random `random_topology` bootstrap
|
||||||
|
whenever the programme has required spaces.
|
||||||
|
2. **`mutate_place_missing`** — repair operator. Detects a required-but-absent
|
||||||
|
space (`graph.check_space_counts`) and inserts one by dividing a host leaf
|
||||||
|
into `[room | remainder]`. Lex-safe host ranking (cf. §4.10): generic `O`
|
||||||
|
leaves first (unbounded, nothing displaced), then other non-required leaves,
|
||||||
|
circulation/stairs only as last resort; a required room is never displaced.
|
||||||
|
Forced onto the room's required storey when the programme constrains its
|
||||||
|
level. Weight 2.0 in the mutation mix (noops cheaply once complete).
|
||||||
|
|
||||||
|
- *Gate:* `missing`-type failures collapse to ~0; net-fail improvement vs the
|
||||||
|
blank-slate baseline; no regression on the seeded programme-house 1-fail
|
||||||
|
optimum (§4.10).
|
||||||
|
- *Commands (reproduce):*
|
||||||
|
```bash
|
||||||
|
# A/B at identical budget+seed (old = git HEAD before this change):
|
||||||
|
URB_NO_OCCLUSION=1 python3 experiments/run_search_scaled.py \
|
||||||
|
examples/harbor-house 20000 0 examples/harbor-house/init.dom out.dom
|
||||||
|
# §4.10 regression: warmstart-2f4 seed, 50000 evals, pop 8, 4 workers
|
||||||
|
```
|
||||||
|
- *Result (harbor-house, 20000 native evals, seed 0, identical config):*
|
||||||
|
|
||||||
|
| metric | OLD (random bootstrap) | NEW (constructive) |
|
||||||
|
|--------|-----------------------:|-------------------:|
|
||||||
|
| seed best fails | 163 | 139 |
|
||||||
|
| final total fails | 133 | **105** |
|
||||||
|
| `missing` fails | **103** (77 %) | **12** (11 %) |
|
||||||
|
| missing-records | 22 | 2 |
|
||||||
|
| dominant remaining | `missing` | crinkliness 27, size 23, access 13, edge 12 |
|
||||||
|
|
||||||
|
Constructive seeding alone gives a **24-fail head start at the seed**
|
||||||
|
(163 → 139) and the run ends at **105 vs 133 (−21 %)**, with the
|
||||||
|
`missing` stack collapsed **103 → 12**. **§4.10 regression: PASS** — the
|
||||||
|
warmstart-2f4 seed still reaches a **1-fail** population (whole pop 1f at
|
||||||
|
50 040 evals; `place_missing` noops harmlessly when the set is complete).
|
||||||
|
|
||||||
|
- *Verdict: construction works and is necessary, but reframes the bottleneck.*
|
||||||
|
Making the required set a constructive invariant removes the catastrophic
|
||||||
|
`missing`-room stacking that dominated the blank-slate baseline (77 % → 11 %
|
||||||
|
of fails). But a *complete* 36-room harbor design then carries a large
|
||||||
|
**quality-fail load** — crinkliness/size/access/edge-too-long packing of two
|
||||||
|
fully-populated floors — that the current geometry inner loop + topology
|
||||||
|
operators reduce only partway in 20k evals. So total fails improve but stay
|
||||||
|
high. The dominant categories are now exactly what **§11.4 (graded objective,
|
||||||
|
to navigate the dense quality-fail regime)** and **§11.3 (staging — build one
|
||||||
|
credible floor at a time instead of cramming both)** target; §11.3 is
|
||||||
|
unblocked by this result. A concrete next seeder refinement (filed): the
|
||||||
|
type→leaf assignment is currently random, ignoring adjacency — clustering each
|
||||||
|
room near its required `c`/neighbour at construction time should cut the
|
||||||
|
adjacency (8) and downstream access (13) fails directly.
|
||||||
|
|
||||||
|
*Note on the baseline:* DESIGN cited a "74-fail `out1.dom`", but the on-disk
|
||||||
|
`out1.dom` is untracked and was overwritten by a prior experiment (it now
|
||||||
|
re-scores to 37 fails; the committed `out1.dom.fails` of 74 lines belongs to
|
||||||
|
the superseded `.dom`). The honest, reproducible comparison is therefore the
|
||||||
|
identical-config A/B against the pre-change code (133 fails), not the stale
|
||||||
|
`out1.dom` number.
|
||||||
|
|
||||||
### 11.3 Staged per-floor search (`homemaker-py-c4c.3`)
|
### 11.3 Staged per-floor search (`homemaker-py-c4c.3`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,11 @@ from . import dom, innerloop, operators, programme
|
||||||
_CHILD_INNER_KW: dict = {}
|
_CHILD_INNER_KW: dict = {}
|
||||||
|
|
||||||
# storey add/delete are drastic (geometry perturbation 0.25-0.33 and a
|
# storey add/delete are drastic (geometry perturbation 0.25-0.33 and a
|
||||||
# deleted storey stacks missing-space failures) — sample them rarely
|
# deleted storey stacks missing-space failures) — sample them rarely.
|
||||||
_MUTATION_WEIGHTS = {"level_add": 0.2, "level_delete": 0.2}
|
# place_missing is the high-leverage §11.2 repair: it noops cheaply once the
|
||||||
|
# required set is complete, so over-sampling it costs little and directly
|
||||||
|
# attacks the dominant missing-space failure mode.
|
||||||
|
_MUTATION_WEIGHTS = {"level_add": 0.2, "level_delete": 0.2, "place_missing": 2.0}
|
||||||
|
|
||||||
|
|
||||||
def _worker_init() -> None:
|
def _worker_init() -> None:
|
||||||
|
|
@ -218,12 +221,22 @@ def search(
|
||||||
# Each individual is a cold start, so use the exploratory sigma
|
# Each individual is a cold start, so use the exploratory sigma
|
||||||
# schedule (inner_kw={} → cma_search defaults: sigmas=(0.05, 0.15)).
|
# schedule (inner_kw={} → cma_search defaults: sigmas=(0.05, 0.15)).
|
||||||
# Leaf count varied ±1 around the target to increase structural diversity.
|
# Leaf count varied ±1 around the target to increase structural diversity.
|
||||||
|
# Programme-aware constructive seeding (§11.2): when the programme
|
||||||
|
# has required spaces, instantiate each by construction so the seed
|
||||||
|
# population starts with ~zero missing-space failures instead of a
|
||||||
|
# random divide+retype walk that leaves required rooms absent.
|
||||||
|
prog = {c: r for c, r in reqs.items() if c[0].lower() not in "cos"}
|
||||||
n_target = bootstrap_n_leaves or max(len(reqs), 3)
|
n_target = bootstrap_n_leaves or max(len(reqs), 3)
|
||||||
tasks = []
|
tasks = []
|
||||||
for i in range(pop_size):
|
for i in range(pop_size):
|
||||||
|
if prog:
|
||||||
|
topo = operators.constructive_topology(seed_root, reqs, rng, types)
|
||||||
|
lineage = f"construct/{i}"
|
||||||
|
else:
|
||||||
n = int(rng.integers(max(1, n_target - 1), n_target + 2))
|
n = int(rng.integers(max(1, n_target - 1), n_target + 2))
|
||||||
topo = random_topology(seed_root, n, rng, types)
|
topo = random_topology(seed_root, n, rng, types)
|
||||||
tasks.append((topo, None, child_budget, {}, f"bootstrap/{i}"))
|
lineage = f"bootstrap/{i}"
|
||||||
|
tasks.append((topo, None, child_budget, {}, lineage))
|
||||||
_run_batch(tasks)
|
_run_batch(tasks)
|
||||||
else:
|
else:
|
||||||
seed_ind, used = _evaluate(copy.deepcopy(seed_root), programme_dir, urb_root,
|
seed_ind, used = _evaluate(copy.deepcopy(seed_root), programme_dir, urb_root,
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,153 @@ def mutate_level_compound_fix(root: dom.Node, rng: np.random.Generator,
|
||||||
return _finalise(child), desc
|
return _finalise(child), desc
|
||||||
|
|
||||||
|
|
||||||
|
def _programme_codes(reqs) -> dict:
|
||||||
|
"""Required programme spaces only (drop generic circulation/outside/sahn)."""
|
||||||
|
return {c: r for c, r in reqs.items() if c[0].lower() not in "cos"}
|
||||||
|
|
||||||
|
|
||||||
|
def mutate_place_missing(root: dom.Node, rng: np.random.Generator,
|
||||||
|
types: list[str], reqs=None) -> tuple[dom.Node, str]:
|
||||||
|
"""Repair operator: insert a required-but-absent space (DESIGN.md §11.2).
|
||||||
|
|
||||||
|
Detects a missing required room via ``graph.check_space_counts`` and inserts
|
||||||
|
one instance by dividing a host leaf into ``[new room | remainder]``. Lex-
|
||||||
|
safety (cf. the §4.10 deceptive-valley lesson): the host is chosen to *not*
|
||||||
|
create more new fails than the missing-stack it removes — generic ``O``
|
||||||
|
leaves are preferred (unbounded, no "too many", nothing displaced), then
|
||||||
|
other non-required leaves; a required room is never displaced. The new room
|
||||||
|
is forced onto its required storey when the programme constrains its level.
|
||||||
|
"""
|
||||||
|
if not reqs:
|
||||||
|
return _finalise(copy.deepcopy(root)), "place_missing noop"
|
||||||
|
|
||||||
|
from . import geometry as _geo, graph as _graph
|
||||||
|
|
||||||
|
child = copy.deepcopy(root)
|
||||||
|
_failures, missing = _graph.check_space_counts(child, reqs)
|
||||||
|
if not missing:
|
||||||
|
return _finalise(child), "place_missing noop"
|
||||||
|
|
||||||
|
mid = _pick(rng, missing)
|
||||||
|
code = mid.split("#")[0]
|
||||||
|
req = reqs.get(code)
|
||||||
|
target_level = getattr(req, "level", None)
|
||||||
|
lvls = dom.levels(child)
|
||||||
|
if target_level is not None and target_level < len(lvls):
|
||||||
|
host_levels = [target_level]
|
||||||
|
else:
|
||||||
|
host_levels = list(range(len(lvls)))
|
||||||
|
|
||||||
|
# Rank candidate hosts: 0 = generic outside (safest — nothing displaced),
|
||||||
|
# 1 = other non-required leaf, 2 = circulation/stair (carve only as last
|
||||||
|
# resort — disrupts the core). Required rooms are never candidates.
|
||||||
|
cands: list[tuple[int, float, dom.Node]] = []
|
||||||
|
for li in host_levels:
|
||||||
|
for leaf in lvls[li].leaves():
|
||||||
|
if not leaf.type:
|
||||||
|
continue
|
||||||
|
t0 = leaf.type[0].lower()
|
||||||
|
if t0 == "o":
|
||||||
|
pref = 0
|
||||||
|
elif t0 in ("c", "s"):
|
||||||
|
pref = 2
|
||||||
|
elif leaf.type in reqs:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
pref = 1
|
||||||
|
cands.append((pref, _geo.area(leaf), leaf))
|
||||||
|
|
||||||
|
if cands:
|
||||||
|
best_pref = min(p for p, _, _ in cands)
|
||||||
|
pool = [(a, lf) for p, a, lf in cands if p == best_pref]
|
||||||
|
_, host = max(pool, key=lambda x: x[0])
|
||||||
|
keep = host.type if host.type and host.type[0].lower() != "o" else "O"
|
||||||
|
else:
|
||||||
|
# No safe host on the required storey — split its largest leaf and
|
||||||
|
# preserve that leaf's type on the large side.
|
||||||
|
all_leaves = [lf for li in host_levels for lf in lvls[li].leaves()]
|
||||||
|
if not all_leaves:
|
||||||
|
return _finalise(child), "place_missing noop"
|
||||||
|
host = max(all_leaves, key=_geo.area)
|
||||||
|
keep = host.type or "O"
|
||||||
|
|
||||||
|
host_id = host.id or "root"
|
||||||
|
# New room small (left, adjacent to remainder); inner NM tunes the ratio.
|
||||||
|
host.division = [0.3, 0.3]
|
||||||
|
host.rotation = int(rng.integers(4))
|
||||||
|
host.left = dom.Node(type=code)
|
||||||
|
host.right = dom.Node(type=keep)
|
||||||
|
host.type = None
|
||||||
|
return _finalise(child), f"place_missing {code} -> {host_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def _grow_leaves(lvl: dom.Node, n_leaves: int, rng: np.random.Generator) -> None:
|
||||||
|
"""Subdivide ``lvl``'s subtree in place until it has ``n_leaves`` leaves."""
|
||||||
|
while len(lvl.leaves()) < n_leaves:
|
||||||
|
leaf = _pick(rng, lvl.leaves())
|
||||||
|
leaf.division = [0.5, 0.5]
|
||||||
|
leaf.rotation = int(rng.integers(4))
|
||||||
|
leaf.left = dom.Node(type=leaf.type)
|
||||||
|
leaf.right = dom.Node(type=leaf.type)
|
||||||
|
leaf.type = None
|
||||||
|
|
||||||
|
|
||||||
|
def constructive_topology(seed_root: dom.Node, reqs, rng: np.random.Generator,
|
||||||
|
types: list[str], min_storeys: int = 1) -> dom.Node:
|
||||||
|
"""Build a seed that instantiates every required space by construction.
|
||||||
|
|
||||||
|
The §11.0 diagnosis: random divide+retype chains leave required programme
|
||||||
|
rooms missing on large programmes, so ``missing`` stacking dominates fitness.
|
||||||
|
This seeder makes the required room set a *constructive invariant*: it sizes
|
||||||
|
each storey to its required rooms (partitioning by ``level``; level-free
|
||||||
|
rooms distributed across storeys), plus one circulation ``C`` and one
|
||||||
|
outside ``O`` per storey, then assigns the types. Stochastic (random split
|
||||||
|
ratios/rotations and a shuffled type assignment) so a bootstrap batch is
|
||||||
|
still a diverse population.
|
||||||
|
|
||||||
|
Returns a finalised deep copy; ``seed_root`` is unchanged.
|
||||||
|
"""
|
||||||
|
from . import genome as _g
|
||||||
|
|
||||||
|
child = copy.deepcopy(seed_root)
|
||||||
|
prog = _programme_codes(reqs)
|
||||||
|
levels_needed = [r.level for r in prog.values() if r.level is not None]
|
||||||
|
n_storeys = max((max(levels_needed) + 1) if levels_needed else 1, min_storeys)
|
||||||
|
|
||||||
|
# grow storeys from the bare base by duplicating the top storey (cf.
|
||||||
|
# mutate_level_add / genome._copy_storey), inheriting floor height.
|
||||||
|
while len(dom.levels(child)) < n_storeys:
|
||||||
|
top = dom.levels(child)[-1]
|
||||||
|
dup = _g._copy_storey(top)
|
||||||
|
dup.height = top.height
|
||||||
|
top.above = dup
|
||||||
|
lvls = dom.levels(child)
|
||||||
|
|
||||||
|
# Partition required instances across storeys: level-constrained rooms to
|
||||||
|
# their storey, level-free rooms round-robin over a shuffled order.
|
||||||
|
buckets: list[list[str]] = [[] for _ in range(n_storeys)]
|
||||||
|
free: list[str] = []
|
||||||
|
for code, req in prog.items():
|
||||||
|
for _ in range(req.count):
|
||||||
|
if req.level is not None and req.level < n_storeys:
|
||||||
|
buckets[req.level].append(code)
|
||||||
|
else:
|
||||||
|
free.append(code)
|
||||||
|
free = [free[i] for i in rng.permutation(len(free))]
|
||||||
|
for i, code in enumerate(free):
|
||||||
|
buckets[i % n_storeys].append(code)
|
||||||
|
|
||||||
|
for li, lvl in enumerate(lvls):
|
||||||
|
assign = list(buckets[li]) + ["C", "O"] # +core circulation, +outside
|
||||||
|
_grow_leaves(lvl, len(assign), rng)
|
||||||
|
leaves = lvl.leaves()
|
||||||
|
order = rng.permutation(len(leaves))
|
||||||
|
for slot, leaf_idx in enumerate(order):
|
||||||
|
leaves[int(leaf_idx)].type = assign[slot] if slot < len(assign) else "O"
|
||||||
|
|
||||||
|
return _finalise(child)
|
||||||
|
|
||||||
|
|
||||||
def mutate_core_divide(root: dom.Node, rng: np.random.Generator,
|
def mutate_core_divide(root: dom.Node, rng: np.random.Generator,
|
||||||
types: list[str]) -> tuple[dom.Node, str]:
|
types: list[str]) -> tuple[dom.Node, str]:
|
||||||
"""Divide a circulation leaf at the same path across ALL storeys at once.
|
"""Divide a circulation leaf at the same path across ALL storeys at once.
|
||||||
|
|
@ -416,6 +563,7 @@ MUTATIONS = {
|
||||||
"core_undivide": mutate_core_undivide,
|
"core_undivide": mutate_core_undivide,
|
||||||
"level_fix": mutate_level_fix,
|
"level_fix": mutate_level_fix,
|
||||||
"level_compound_fix": mutate_level_compound_fix,
|
"level_compound_fix": mutate_level_compound_fix,
|
||||||
|
"place_missing": mutate_place_missing,
|
||||||
"level_retype": mutate_level_retype,
|
"level_retype": mutate_level_retype,
|
||||||
"level_add": mutate_level_add,
|
"level_add": mutate_level_add,
|
||||||
"level_delete": mutate_level_delete,
|
"level_delete": mutate_level_delete,
|
||||||
|
|
@ -428,13 +576,15 @@ def mutate(root: dom.Node, rng: np.random.Generator, types: list[str],
|
||||||
"""Apply one random mutation drawn from MUTATIONS."""
|
"""Apply one random mutation drawn from MUTATIONS."""
|
||||||
names = sorted(MUTATIONS)
|
names = sorted(MUTATIONS)
|
||||||
p = np.array([(weights or {}).get(n, 1.0) for n in names], dtype=float)
|
p = np.array([(weights or {}).get(n, 1.0) for n in names], dtype=float)
|
||||||
# level_fix needs programme reqs; disable it silently when not available
|
# these operators need programme reqs; disable them when not available
|
||||||
|
reqs_ops = ("level_fix", "level_compound_fix", "place_missing")
|
||||||
if reqs is None:
|
if reqs is None:
|
||||||
p[names.index("level_fix")] = 0.0
|
for op in reqs_ops:
|
||||||
|
p[names.index(op)] = 0.0
|
||||||
if p.sum() == 0:
|
if p.sum() == 0:
|
||||||
p[:] = 1.0
|
p[:] = 1.0
|
||||||
name = str(rng.choice(names, p=p / p.sum()))
|
name = str(rng.choice(names, p=p / p.sum()))
|
||||||
if name in ("level_fix", "level_compound_fix"):
|
if name in reqs_ops:
|
||||||
return MUTATIONS[name](root, rng, types, reqs=reqs)
|
return MUTATIONS[name](root, rng, types, reqs=reqs)
|
||||||
return MUTATIONS[name](root, rng, types)
|
return MUTATIONS[name](root, rng, types)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,58 @@ def test_all_mutations_survive_undivided_tree():
|
||||||
canonical(child)
|
canonical(child)
|
||||||
|
|
||||||
|
|
||||||
|
HARBOR = Path(__file__).parent.parent / "examples" / "harbor-house"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not HARBOR.is_dir(), reason="harbor-house not available")
|
||||||
|
def test_constructive_topology_has_no_missing_spaces():
|
||||||
|
# §11.2: the constructive seeder must instantiate every required space by
|
||||||
|
# construction (count + level), so check_space_counts reports zero missing.
|
||||||
|
from homemaker_layout import graph, programme
|
||||||
|
|
||||||
|
reqs = programme.load_programme_dir(str(HARBOR))
|
||||||
|
types = sorted(reqs) + ["C", "O"]
|
||||||
|
seed = dom.load(str(HARBOR / "init.dom"))
|
||||||
|
for trial in range(5):
|
||||||
|
root = operators.constructive_topology(
|
||||||
|
seed, reqs, np.random.default_rng(trial), types)
|
||||||
|
_, missing = graph.check_space_counts(root, reqs)
|
||||||
|
assert missing == [], f"trial {trial} left {missing}"
|
||||||
|
# required level partition respected: level-N rooms land on storey N
|
||||||
|
lvls = dom.levels(root)
|
||||||
|
for code, req in reqs.items():
|
||||||
|
if code[0].lower() in "cos" or req.level is None:
|
||||||
|
continue
|
||||||
|
for li, lvl in enumerate(lvls):
|
||||||
|
for leaf in lvl.leaves():
|
||||||
|
if leaf.type == code:
|
||||||
|
assert li == req.level
|
||||||
|
canonical(root)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not HARBOR.is_dir(), reason="harbor-house not available")
|
||||||
|
def test_place_missing_repairs_deficient_tree():
|
||||||
|
# §11.2 repair: iterating mutate_place_missing drives a deficient design's
|
||||||
|
# missing-space count to zero, then noops once the required set is complete.
|
||||||
|
from homemaker_layout import graph, programme
|
||||||
|
|
||||||
|
reqs = programme.load_programme_dir(str(HARBOR))
|
||||||
|
types = sorted(reqs) + ["C", "O"]
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
root = dom.load(str(HARBOR / "generated.dom"))
|
||||||
|
_, missing0 = graph.check_space_counts(root, reqs)
|
||||||
|
assert missing0, "fixture should start deficient"
|
||||||
|
for _ in range(len(missing0) + 5):
|
||||||
|
root, desc = operators.mutate_place_missing(root, rng, types, reqs=reqs)
|
||||||
|
canonical(root)
|
||||||
|
_, missing = graph.check_space_counts(root, reqs)
|
||||||
|
if not missing:
|
||||||
|
break
|
||||||
|
assert missing == []
|
||||||
|
_, desc = operators.mutate_place_missing(root, rng, types, reqs=reqs)
|
||||||
|
assert desc == "place_missing noop"
|
||||||
|
|
||||||
|
|
||||||
def test_crossover_yields_canonical_pair():
|
def test_crossover_yields_canonical_pair():
|
||||||
a = genome.decode(genome.encode(dom.load(str(CORPUS / FILES[0]))))
|
a = genome.decode(genome.encode(dom.load(str(CORPUS / FILES[0]))))
|
||||||
b = genome.decode(genome.encode(dom.load(str(CORPUS / FILES[1]))))
|
b = genome.decode(genome.encode(dom.load(str(CORPUS / FILES[1]))))
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue