erc/ld2: interior-O light-well seeding — §13.6 positive on dense floors

Seed O as interior light wells (most-landlocked leaves first, count scaled
by room count via outside_divisor) instead of one peripheral O, attacking the
erc crinkliness residual: seed diagnostic confirms every crinkliness fail is
under-exposed (landlocked), none over-exposed.

A/B (20k evals, seeds 0/1/2, bal+share stack, §13.6): control reproduces §13.5;
interior odiv=3 gives harbor -16.4% (all seeds improve) and maple -2.8%
(net-neutral). Default-optimal divisor 3 found by seed sweep (6 was null).

Lever default OFF; default-ON flip tracked as erc.8.

- operators: interior_outside + outside_divisor through constructive_topology,
  lift_base_to_storeys, _assign_adjacency_aware (fix n_circ budget for >1 O)
- driver.search/search_staged threading; run_staged_search.py INTERIORO/ODIV env
- test_interior_outside_seeds_landlocked_wells_and_scales_count
- experiments/run_interioro_ab.sh; DESIGN.md §13.6

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bruno Postle 2026-06-28 07:20:20 +01:00
parent 83b6284045
commit 2491a9be12
7 changed files with 248 additions and 30 deletions

View file

@ -1,4 +1,4 @@
{"id":"homemaker-py-ld2","title":"Interior-O courtyard seeding option","description":"_assign_adjacency_aware (operators.py:528) currently places the single O leaf on the MOST PERIPHERAL leaf, where adjacent rooms already have facade. For dense floors (harbor-house ~19 rooms/floor) this wastes the daylight source. Add an option to seed O INTERIOR (as a light well) and to scale O-leaf count with room count, so landlocked rooms get an adjacent uncovered-outside neighbour by construction -\u003e fewer crinkliness fails in the seed. A/B against current peripheral placement.","notes":"Construction lever (high prior under erc), sibling of erc.3 (leaf-sharing) and erc.4 (plot-filling). Directly attacks the crinkliness residual (erc decomposition: crinkliness 346 on maple): landlocked rooms get an adjacent uncovered-O light well by construction. Follow erc shared protocol (A/B maple+harbor seeds 0/1/2, 20k evals, control reproduces 136.0/74.0, DESIGN.md §13.x).","status":"open","priority":1,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-23T20:40:19Z","created_by":"Bruno Postle","updated_at":"2026-06-23T20:50:24Z","dependencies":[{"issue_id":"homemaker-py-ld2","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T21:49:30Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"homemaker-py-ld2","title":"Interior-O courtyard seeding option","description":"_assign_adjacency_aware (operators.py:528) currently places the single O leaf on the MOST PERIPHERAL leaf, where adjacent rooms already have facade. For dense floors (harbor-house ~19 rooms/floor) this wastes the daylight source. Add an option to seed O INTERIOR (as a light well) and to scale O-leaf count with room count, so landlocked rooms get an adjacent uncovered-outside neighbour by construction -\u003e fewer crinkliness fails in the seed. A/B against current peripheral placement.","notes":"Implemented: interior_outside flag + outside_divisor (default 3) threaded through operators.constructive_topology / lift_base_to_storeys, _assign_adjacency_aware (interior light-well placement: most-landlocked leaves first, greedy spread), driver.search/search_staged, run_staged_search.py (INTERIORO/ODIV env). Test test_interior_outside_seeds_landlocked_wells_and_scales_count. A/B script experiments/run_interioro_ab.sh. Seed diagnostic confirmed mechanism (all crinkliness fails landlocked under-exposure) and tuned odiv 6-\u003e3. Full 20k A/B (maple+harbor seeds 0/1/2, control=peripheral must reproduce §13.5 maple 82.3/harbor 40.0) running; DESIGN.md §13.6 verdict pending results.","status":"closed","priority":1,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-23T20:40:19Z","created_by":"Bruno Postle","updated_at":"2026-06-28T06:19:38Z","started_at":"2026-06-27T20:37:42Z","closed_at":"2026-06-28T06:19:38Z","close_reason":"interior-O light-well seeding implemented + A/B done (§13.6): positive on dense floor (harbor -16.4%, all seeds), marginal/neutral on maple (-2.8%). Default-ON flip tracked as follow-up.","dependencies":[{"issue_id":"homemaker-py-ld2","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T21:49:30Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"homemaker-py-erc.4","title":"Experiment: depth-balanced / giant-splitting construction (re-scoped by Diag B)","description":"Attacks the #2 factor (size/undersize 242) via the §12.3 paradox: rooms are undersize while 56% of the plot is empty. The shape floor is computed at TARGET dims, so construction never spends the slack. Scale leaves up to consume available plot area (proportionally, preserving target aspect) so rooms reach/exceed target — bigger leaves are also easier to keep compact, so this may help crinkliness/width too.\n\nBuilds on leu.2 (proportion-aware splits sized FROM target dims) by adding a fill step that scales the whole layout (or per-region) to the plot envelope instead of leaving slack as empty plot. Implementation in operators construction / _size_divisions_from_targets.\n\nNOTE: exact fix-site (construction vs inner loop) is decided by Diagnostic B — if B shows leaves park at target with unused plot, this construction lever is correct; if B shows the inner loop simply lacks an expansion gradient, prefer the inner-loop slack-expansion sibling instead. A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.4.","notes":"RE-SCOPED by Diagnostic B (§13.2). Original premise (rooms parked at target, scale leaves up into 56%-empty plot) is FALSIFIED: sized rooms already hold 1.4-1.5x aggregate target area; the empty-looking plot is ~46% circulation, not claimable void. Real defect: MALDISTRIBUTION by slicing position — same type/target leaf lands 0.05x..14.7x by binary-tree depth; inner loop cannot fix (frozen topology). NEW SCOPE: construction that balances tree DEPTH so equal-target rooms land at comparable depth and/or splits/caps giant leaves so area tracks target. NOT a uniform scale-to-envelope (that would just inflate the giants further). A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.4. Synergy with erc.3 (leaf-sharing for the starved tail).","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-22T23:16:19Z","created_by":"Bruno Postle","updated_at":"2026-06-26T06:06:51Z","started_at":"2026-06-24T21:18:57Z","closed_at":"2026-06-26T06:06:51Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-erc.4","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T00:16:19Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-erc.4","depends_on_id":"homemaker-py-erc.2","type":"blocks","created_at":"2026-06-23T00:16:45Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"homemaker-py-erc.3","title":"Experiment: leaf-sharing / multi-room leaves in construction","description":"Strongest untried construction lever. §12.3 named 'merge or share leaves across same-class rooms' but c3g never tested it — c3g only coarsened the circulation spine (circ_divisor), trading shape gains for equal access/adjacency damage (null). Leaf-sharing is DIFFERENT: it reduces leaf count by collapsing same-class rooms (e.g. several O/storage, or same-type repeated rooms) into a shared leaf, attacking crinkliness(346)+size(242) directly WITHOUT coarsening circulation — so it should dodge the access penalty that sank c3g.\n\nImplementation sketch: in operators.constructive_topology (+ lift path), allow rooms of the same class/type (and compatible adjacency) to be instantiated as one larger leaf rather than one-leaf-per-room, lowering leaves-per-room from ~1.4 toward 1.0 or below. Honour storey_minimum and required-room presence (a shared leaf must still satisfy each merged room's presence/area in the fitness check, or the merge must be limited to rooms the fitness treats as fungible).\n\nTests the deepest open question: whether 52 rooms simply cannot be well-shaped as 52 leaves at this density. A/B vs §12.2 baseline (maple 136.0, harbor 74.0), seeds 0/1/2, 20000 evals, staged; default-OFF toggle so controls reproduce. Record DESIGN.md §13.3.","notes":"A/B DONE (§13.3): staged 20k, seeds 0/1/2, factor 3. maple 137.0→86.3 (37%), harbor 74.0→50.3 (32%). Baseline arm reproduces §12.2 exactly (maple 137 vs 136, harbor 74.0 vs 74.0). Total separation: every share run beats every baseline run same-programme. ~35% faster (fewer leaves). First Phase-8 floor-mover; 5th construction/seed win. Closing.","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-22T23:16:15Z","created_by":"Bruno Postle","updated_at":"2026-06-24T20:51:20Z","started_at":"2026-06-23T21:51:08Z","closed_at":"2026-06-24T20:51:20Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-erc.3","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T00:16:15Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-erc.3","depends_on_id":"homemaker-py-erc.1","type":"blocks","created_at":"2026-06-23T00:16:42Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"homemaker-py-erc.2","title":"Diagnostic B: undersize-despite-slack localization (construction-target vs inner-loop-fill)","description":"GATES the plot-fill-construction vs inner-loop-expansion decision. The paradox from §12.3: plot utilisation is 0.44 (56% empty) yet size fails are 242 (rooms UNDERSIZE). Where is the slack stranded, and at which stage should it be spent?\n\nMeasure, on constructive seeds for maple-court + harbor (seeds 0/1/2):\n1. After CONSTRUCTION (before inner loop): per-leaf achieved area vs target area, and total occupied vs plot area. Are leaves parked at target with the slack left as unused plot, or is the slack distributed but mis-shaped?\n2. After the INNER LOOP optimises ratios: did size fails drop — i.e. does the ratio solve already expand leaves into slack, or does it have no gradient/incentive to exceed target? Compare predicted_shape_fails (target geometry) vs achieved size fails (post-optimise).\n\nThe §12.3 calibration (floor at TARGET dims ≈ achieved) already hints the inner loop is NOT filling slack — confirm and quantify, and identify whether the gap is (a) construction targets too-small dims given the plot, or (b) the objective gives no reward for exceeding target area. Output: DESIGN.md §13.2.\n\nDECISION RULE: if rooms are parked at target with unused plot → fix in CONSTRUCTION (plot-fill, erc child). If the inner loop has the room to expand but no objective gradient → fix in the INNER LOOP (slack-expansion term, erc child). Reads only; no behaviour change.","notes":"VERDICT (DESIGN.md §13.2): the '56% empty plot' is a misreading. Sized rooms already occupy ~50-54% of plot and hold 1.4-1.5x their aggregate target area (util\u003etgtFill); ~46% of plot is CIRCULATION, not claimable void (out only 3-4%). Size fails are pure MALDISTRIBUTION set by SLICING POSITION: median room at target (a/t~1.0) but long undersize tail (p25~0.35, min 0.05) starves while a few giants balloon (max 6.8x harbor, 14.7x maple). Same type/target lands at BOTH extremes (harbor r t=10: 68m2 \u0026 2.3m2; maple n t=60: ~target \u0026 2.7m2) =\u003e area dictated by binary-tree depth, not target. Inner loop CANNOT repair it: budget-80 size fails move only -1.6/-3.7, %undersize flat-to-worse; frozen-topology ratio DOF + 0.5^n cliff + symmetric size gaussian. =\u003e FALSIFIES plot-fill-as-claim-void (re-scope erc.4 to depth-balanced/giant-splitting construction), DEPRIORITISE erc.6 (wrong DOF). Reinforces erc.3 leaf-sharing for the starved tail. Script: experiments/diag_slack_localization.py","status":"closed","priority":1,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-22T23:15:42Z","created_by":"Bruno Postle","updated_at":"2026-06-23T21:46:34Z","started_at":"2026-06-23T21:17:07Z","closed_at":"2026-06-23T21:46:34Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-erc.2","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T00:15:42Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0}
@ -22,6 +22,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.24x1.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-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-erc.8","title":"Flip interior_outside (odiv=3) default to ON","description":"§13.6/ld2 confirmed interior-O light-well seeding positive on dense floors (harbor -16.4%, all seeds improve) and net-neutral on maple (-2.8%, mean improves, no programme regresses on mean). Mirror the pll flip after erc.7: change interior_outside default False-\u003eTrue in driver.search/search_staged and operators.constructive_topology/lift_base_to_storeys (outside_divisor stays 3). No test asserts fail counts so low-risk. Verify control runs still re-score OK.","status":"open","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-28T06:18:14Z","created_by":"Bruno Postle","updated_at":"2026-06-28T06:18:14Z","dependencies":[{"issue_id":"homemaker-py-erc.8","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-28T07:18:13Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"homemaker-py-pll","title":"Flip depth_balanced + leaf_sharing (factor 3) defaults to ON","description":"erc.7/§13.5 verdict: depth_balanced + leaf_sharing (factor 3) is the winning Phase-8 stack (harbor -21%, maple -4.6% vs share-alone; factor 3 confirmed optimal). Both default OFF today. Make the bal+share stack the default in driver.search/search_staged (leaf_sharing=True, leaf_share_factor=3, depth_balanced=True) and update the affected tests (the §13.4 note records 214 tests pass with depth_balanced OFF — expect ordering/snapshot churn). Keep env-var overrides (DEPTHBAL/LEAFSHARE/LEAFSHAREFAC) for A/B. leaf_share_max stays 4 (covers factor\u003c=4, no missing-fail leak).","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-27T16:12:53Z","created_by":"Bruno Postle","updated_at":"2026-06-27T20:15:26Z","started_at":"2026-06-27T16:14:52Z","closed_at":"2026-06-27T20:15:26Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-pll","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-27T17:13:34Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"homemaker-py-9o5","title":"Multi-use leaves: one leaf satisfies several COMPATIBLE different codes (type superposition)","description":"A leaf that legitimately and simultaneously satisfies several DIFFERENT compatible programme requirements (e.g. study + guest bedroom, or kitchen + dining). Distinct from erc.3 leaf-sharing, which aggregates k instances of the SAME code; this is a strict generalisation across DIFFERENT codes. Idea from Bruno (this corresponds to Stewart Brand's 'How Buildings Learn' — loose-fit / long-life rooms whose use churns over a building's lifetime).\n\nWHY IT MATTERS\n1. Architectural deliverable: adaptable multi-use rooms (Brand loose-fit), not just an optimisation trick.\n2. Generalises the erc.3 floor-lowering lever to the SINGLETON (count:1) long tail that same-type sharing cannot reach: one leaf covering one X AND one Y removes a room-leaf, paying the ~1.8/leaf crinkliness tax (§13.1) once instead of twice. Crinkliness is scale-invariant, so a larger multi-use leaf is not penalised for size.\n\nTWO READINGS\n(a) Superposition as a SEARCH RELAXATION: carry a distribution/set of candidate types per leaf, evaluate a relaxed (expected/best-case) fitness for a smoother landscape, then COLLAPSE (argmax) at the end. Risks: relaxation gap (relaxed optimum need not sit near a good integer solution); collapse is itself a constrained rounding/assignment problem (cannot collapse 5 superposed leaves all to 'kitchen' when 1 is required); and search-machinery bets are 0/3 historically (§11-12) vs construction 4/4 — the floor is geometric, so pure search-easing may fight the wrong battle. LOWER PRIORITY framing.\n(b) Multi-use as the DESIGN GOAL (preferred): the leaf permanently serves a SET of compatible codes; no collapse needed, multi-use survives into the output. Mirrors erc.3's mechanism exactly but with a SET of codes instead of an integer count: stamp leaf with the codes it serves (type-guarded as in erc.3 leaf.share/share_type); fitness count credits each code in the set, size scored against the union/least-upper-bound of targets, width/proportion as today (scale-invariant), adjacency satisfied if the SET satisfies it.\n\nIMPLEMENTATION SKETCH (path b)\n- dom.Node: a set/list of served codes (generalises leaf.share/share_type from erc.3). Survives search via deepcopy; emit in .dom only when non-trivial (as with 'share').\n- graph.check_space_counts: a multi-use leaf credits coverage to EACH code in its set (type-guarded: honoured only while its served set is consistent with its assignment).\n- fitness size/width/proportion: score the multi-use leaf against the combined target (union/LUB) of its served codes; crinkliness/access unchanged.\n- construction: a new constructive option that fuses COMPATIBLE singleton rooms into shared multi-use leaves (analogous to operators._share_rooms but across codes), honouring adjacency/level.\n- default OFF; controls reproduce §12.2 baseline.\n\nKEY OPEN QUESTIONS (Bruno to spec)\n- Who declares type-COMPATIBILITY? A new architectural input, analogous to adjacency (e.g. a 'compatible:' / 'multiuse:' list per space in patterns.config). kitchen+bathroom is nonsensical; study+guestroom is fine.\n- Does the final design COLLAPSE to single uses or stay loose-fit (keep superposition as a deliverable)? Brand argues for keeping it.\n- How exactly to combine size/width/proportion targets for a leaf serving 2+ codes (max? union? a 'dominant use' target?).\n- Interaction with erc.3 same-type sharing and x3b per-code control — composable? (a leaf could be 'k of X' AND 'one Y').\n\nRELATES TO: erc.3 (same-type leaf-sharing, the special case), x3b (per-code shareable flag), erc.7 (factor/synergy sweep), erc epic (lower the geometry floor). Concept only — implement in a future session.","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-24T21:11:03Z","created_by":"Bruno Postle","updated_at":"2026-06-24T21:11:03Z","dependencies":[{"issue_id":"homemaker-py-9o5","depends_on_id":"homemaker-py-erc.3","type":"related","created_at":"2026-06-24T22:11:48Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"homemaker-py-x3b","title":"Per-code shareable flag (SpaceReq.share) + homemaker-evolve CLI wiring","description":"Make leaf-sharing (erc.3, §13.3) safe to default-on by giving the programme author per-code control, and expose it on the real CLI (not just the experiment env var).\n\nDesign (agreed with Bruno, open to refinement — he has follow-up questions):\n- patterns.config per-space optional key 'share: N' -\u003e SpaceReq.share (int, default 1 = not shareable). N\u003e=2 means up to N rooms of this code per shared leaf.\n- Master enable stays the 'leaf_sharing' conf/CLI flag (default OFF -\u003e baseline, controls reproduce).\n- Global grain selector 'leaf_share_factor': 0 =\u003e per-code opt-in only (share a code iff it has share:N\u003e=2); F\u003e=2 =\u003e global mode (share all sized multi-instance codes at grain F) with per-code 'share' overriding (share:1 opts a code OUT). This single knob covers both the safe default-on philosophy (0 + per-code keys) and the §13.3 experiment (F=3, reproducible, no example-programme edits).\n- operators._share_rooms picks grain per code accordingly; fitness honours the explicit leaf.share (type-guarded) as today.\n- homemaker-evolve gains --leaf-sharing / --leaf-share-factor, threaded to driver.search/search_staged (already plumbed).\n- Tests: per-code grain, opt-out, default-OFF parity. NOT editing example programmes so §13.3 stays reproducible.\n\nRelates to dyh (productionise). erc.7 covers the factor/max_share sweep + erc.4 synergy.","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-24T21:03:05Z","created_by":"Bruno Postle","updated_at":"2026-06-24T21:14:56Z","started_at":"2026-06-24T21:03:45Z","dependency_count":0,"dependent_count":0,"comment_count":0}
@ -66,18 +67,19 @@
{"id":"homemaker-py-erc.6","title":"Experiment: inner-loop slack-expansion objective term","description":"Inner-loop counterpart to plot-fill construction. If Diagnostic B shows the inner loop has room to expand leaves into slack but no objective gradient to do so (the scalar rewards hitting target area but not exceeding it where slack exists), add a term/incentive so the ratio optimiser pushes leaf boundaries out to consume neighbouring slack and satisfy size, rather than parking at target.\n\nCONDITIONAL on Diagnostic B: build this only if B localizes the gap to the inner loop (room to expand, no gradient); if B shows construction targets too-small dims, prefer the plot-fill construction sibling. Must preserve the §5.4 inner-loop cliff / §4.9 lexicographic protection — the term sits where it cannot displace the fail-count ordering. A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.6.","notes":"DEPRIORITISED by Diagnostic B (§13.2). B shows the inner loop CANNOT repair undersize: the slack is depth-driven maldistribution baked into the frozen topology, and the equal-offset ratio DOF cannot shrink a 14x leaf to feed a starved one without trading into shape fails (0.5^n cliff). Wrong DOF and wrong direction — the blocker is slicing POSITION, not a missing expansion reward. Fix belongs upstream in construction/topology (erc.4 re-scoped, erc.3). Keep as a low-priority follow-up only if a depth-balanced construction still leaves a residual size gradient the inner loop could pick up.","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-22T23:16:24Z","created_by":"Bruno Postle","updated_at":"2026-06-23T21:47:05Z","dependencies":[{"issue_id":"homemaker-py-erc.6","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T00:16:23Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-erc.6","depends_on_id":"homemaker-py-erc.2","type":"blocks","created_at":"2026-06-23T00:16:47Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"homemaker-py-erc.5","title":"Experiment: compactness-aware cuts (minimize leaf perimeter/area)","description":"Attacks the #1 factor, crinkliness (346) — a per-leaf perimeter/area property DISTINCT from proportion (aspect ratio). Proportion-aware seeding (leu.2) sizes splits but does not bias toward balanced, square-ish subdivision. Add a KD-tree-style 'keep both children compact' cut rule (prefer the cut orientation/position that minimises summed child perimeter/area) in construction.\n\nCONDITIONAL on Diagnostic A: if A shows per-leaf shape-fail is FLAT across densities (floor intrinsic to slicing density), better cuts at the same leaf count will not pay → this should be closed wont-fix in favour of leaf-sharing. Only build if A shows shape-fail RISES with density. A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.5.","notes":"DEPRIORITISED by erc.1 verdict (§13.1): per-leaf shape-fail flat vs slicing density and cuts already squarest (_size_divisions_from_targets picks squarest rotation) yet still ~1.8 fails/leaf =\u003e little compactness headroom at fixed leaf count. Floor is intrinsic to leaf COUNT, not cut quality. Revisit only if leaf-sharing (erc.3) underdelivers.","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-22T23:16:21Z","created_by":"Bruno Postle","updated_at":"2026-06-23T21:00:46Z","dependencies":[{"issue_id":"homemaker-py-erc.5","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T00:16:21Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-erc.5","depends_on_id":"homemaker-py-erc.1","type":"blocks","created_at":"2026-06-23T00:16:43Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"homemaker-py-2g5","title":"Rebuild occlusion/daylight/sun subsystem in Python (post-Phase-5, after optimisation fully native)","description":"DESIGN.md §6 port scope — a whole subsystem, not a term. quality_daylight (Leaf.pm:281-296) needs Urb::Misc::Sun + Urb::Field::Occlusion (+CIESky); quality_uncrinkliness also takes the occlusion object. Indoor spaces return 1 for daylight; cost is outdoor spaces + crinkliness. Port Sun_horizontal (262980-minute normalisation) and the occlusion wall set from Dom-\u003eWalls.","acceptance_criteria":"Daylight and crinkliness factors match Perl (float tolerance) across the corpus, including multi-storey cases","notes":"Re-scoped 2026-06-12: occlusion disabled in the Urb oracle instead of ported (see homemaker-py-gp2). Native fitness ships with simple crinkliness (illumination factor = 1, in homemaker-py-gnw). This issue is now the eventual Python occlusion rebuild, only after optimisation works entirely in Python. Restores outdoor-daylight and shaded-wall selection pressure.\nReframed 2026-06-17: orthogonal to epic homemaker-py-c4c. This is fitness FIDELITY (restoring daylight + shaded-wall selection pressure to match Perl), not search CAPABILITY — it changes what 'good' means, not the search's ability to find good. It will NOT improve final designs in the sense currently sought. Stays P4, deferred until the topology-search-quality epic lands and optimisation is fully native.","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:25Z","created_by":"Bruno Postle","updated_at":"2026-06-17T19:14:48Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"memory","key":"warm-x0-initialization-bug-pattern-when-a-topology","value":"warm_x0 initialization bug pattern: when a topology operator explicitly sets division ratios on a newly-created node (e.g. compound_fix sets node.division=[0.25,0.25] for t3), parent.ratios has no entry for that node (it was a leaf). warm_x0 defaults it to 0.5, corrupting the inner loop's starting point and making the operator invisible to lex comparison. Fix: only propagate child ratios for nodes where the parent node was NOT already divided; stale hidden nodes revealed by structural mutations (swap flipping b.below) must NOT contribute their pre-writeback values. See driver.py lines 259-267 (fixed 2026-06-14)."}
{"_type":"memory","key":"deceptive-valleys-in-topology-search-when-every-single","value":"Deceptive valleys in topology search: when every single-step mutation from a target state passes through a high-fail intermediary (e.g. level_fix displaces a room into 5+ new fails), a compound operator that atomically applies two coordinated changes can escape. Design compound operators to land on the low-fail state directly, bypassing the deceptive gradient. Programme-house example: level_compound_fix atomically moves the level-constrained room AND re-inserts the displaced room adjacent to C in one step (operators.py, 2026-06-14)."}
{"_type":"memory","key":"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":"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":"proportion-aware-constructive-seeding-leu-2-12-2","value":"Proportion-aware constructive seeding (leu.2/§12.2): sizing seed cuts from target AREAS only regresses (thin slivers wreck aspect); you must ALSO pick each cut's rotation for child squareness. It is a convergence ACCELERATOR via a deeper local optimum around the constructed topology: wins where that topology is roughly right and budget is scarce (harbor -13%, maple -10% at 20k evals) but DELAYS small programmes where the seed must be restructured by undivide (programme-house regresses at fixed budget, yet reaches the floor given budget - speed, not asymptote). Default-on. Also: n_storeys must honour storey_minimum, not just level: keys (programme-house storey_minimum:2, all rooms level:0 - was seeded 1 storey short; cq1)."}
{"_type":"memory","key":"cli-tool-style-prefer-python-m-homemaker-module","value":"CLI tool style: prefer python -m homemaker.module --parameters pattern, installable via pip install -e . with pyproject.toml entry_points. Not standalone bin/ scripts."}
{"_type":"memory","key":"correction-to-urb-fitness-bug-memory-bruno-2026","value":"CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase."}
{"_type":"memory","key":"homemaker-py-pythonpath-set-pythonpath-home-bruno-src","value":"homemaker-layout PYTHONPATH: package installed as 'homemaker-layout' via pip install -e . so 'import homemaker_layout' works from anywhere without PYTHONPATH. For running tests use 'python -m pytest' from project root /home/bruno/src/homemaker-layout (pyproject.toml adds src/ automatically). Never try pip show homemaker — that's the old homemaker-addon conflict."}
{"_type":"memory","key":"ld2-13-6-interior-o-seed-diagnostic-all","value":"ld2/§13.6 interior-O seed diagnostic: ALL crinkliness fails in the constructed bal+share seed are UNDER-exposed (crink\u003c0.62, landlocked rooms with no facade + no uncovered-O neighbour) — zero over-exposed sliver fails. So the erc crinkliness residual is genuine under-daylighting, validating the interior light-well premise. Default outside_divisor=6 was too sparse (null: harbor 147-\u003e142, crinkliness even rose). odiv=3 is the seed-optimal joint setting: harbor seed fails 147-\u003e129 (-18), maple 219-\u003e206 (-14), landlocked fails drop, at cost of more leaves (harbor +4, maple +8). Because it ADDS leaves it carries the §13.4 wash-out risk; A/B to convergence pending."}
{"_type":"memory","key":"multi-storey-staircase-consistency-when-dividing-or-retyping","value":"Multi-storey staircase consistency: when dividing or retyping a circulation (C) leaf at one level, the same structural change should be propagated to the matching leaf on ALL other storeys so the stair core path is maintained. The optimizer cannot fix staircase disruptions through trial-and-error geometry alone — it requires a synchronized multi-level operator that applies the same topology change to every storey simultaneously."}
{"_type":"memory","key":"proportion-aware-constructive-seeding-leu-2-12-2","value":"Proportion-aware constructive seeding (leu.2/§12.2): sizing seed cuts from target AREAS only regresses (thin slivers wreck aspect); you must ALSO pick each cut's rotation for child squareness. It is a convergence ACCELERATOR via a deeper local optimum around the constructed topology: wins where that topology is roughly right and budget is scarce (harbor -13%, maple -10% at 20k evals) but DELAYS small programmes where the seed must be restructured by undivide (programme-house regresses at fixed budget, yet reaches the floor given budget - speed, not asymptote). Default-on. Also: n_storeys must honour storey_minimum, not just level: keys (programme-house storey_minimum:2, all rooms level:0 - was seeded 1 storey short; cq1)."}
{"_type":"memory","key":"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":"run-to-run-reproducibility-in-homemaker-layout-serial","value":"Run-to-run reproducibility in homemaker-layout: serial search (workers=1) is byte-for-byte deterministic; parallel (workers\u003e1) is now deterministic too AFTER fixing driver._run_batch to admit futures in submission order (was as_completed/completion order, bug xcy). Reproducibility holds only for a FIXED worker count — serial vs parallel differ because children-per-iteration is 1 vs n_workers (different batch granularity), which is expected, not a bug. The constructive seeder was NEVER nondeterministic: _assign_adjacency_aware has unique idx tiebreaks; comparing topologies with Python builtin hash() of the signature STRING is invalid (PYTHONHASHSEED salts str hashing per process) — use a stable hash (sha1) or genome.signature equality."}
{"_type":"memory","key":"strategy-decision-2026-06-12-bruno-occlusion-daylight","value":"Strategy decision 2026-06-12 (Bruno): occlusion/daylight is ORTHOGONAL to building a scalable optimiser. Disable it in Urb (env flag, homemaker-py-gp2) rather than port it; native fitness uses simple crinkliness (illumination factor = 1); rebuild occlusion in Python only after optimisation is fully native (homemaker-py-2g5, now P4). Consequence: all scores change when the flag flips — re-baseline corpus/.score, DESIGN \\$4.5 gains, gate bars at one clean boundary AFTER homemaker-py-1p0 closes; Phase-2 urb-evolve benchmark must run with the same flag."}
{"_type":"memory","key":"urb-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":"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":"run-to-run-reproducibility-in-homemaker-layout-serial","value":"Run-to-run reproducibility in homemaker-layout: serial search (workers=1) is byte-for-byte deterministic; parallel (workers\u003e1) is now deterministic too AFTER fixing driver._run_batch to admit futures in submission order (was as_completed/completion order, bug xcy). Reproducibility holds only for a FIXED worker count — serial vs parallel differ because children-per-iteration is 1 vs n_workers (different batch granularity), which is expected, not a bug. The constructive seeder was NEVER nondeterministic: _assign_adjacency_aware has unique idx tiebreaks; comparing topologies with Python builtin hash() of the signature STRING is invalid (PYTHONHASHSEED salts str hashing per process) — use a stable hash (sha1) or genome.signature equality."}
{"_type":"memory","key":"urb-oracle-nondeterminism-urb-fitness-pl-output-varies","value":"Urb oracle nondeterminism: urb-fitness.pl output varies run-to-run from Perl hash-order randomisation — .fails line ORDER shuffles (compare sorted, use oracle.Score.fail_lines) and the score float can flip by ~1 ULP (compare with math.isclose rel_tol=1e-12, never ==). Not a batching artifact; affects single runs too. Matters for the Phase 3 native-fitness parity gate (homemaker-py-uxz)."}
{"_type":"memory","key":"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":"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":"adjacency-in-binary-slicing-tree-is-structural-not","value":"Adjacency in binary slicing tree is structural, not geometric: the inner-loop NM cannot fix topological adjacency failures. Two paths exist: (1) tree-sibling adjacency — a node is adjacent to its sibling in the tree; (2) cross-zone geometric adjacency — leaves from different subtrees that happen to share a boundary. Staircase/adjacency fails require a topology mutation that changes which nodes are siblings or which zones touch. This was proved empirically on programme-house: staircase fail from rot=0 layout could not be fixed by NM but was fixed by level_retype creating a two-C topology (2026-06-14/15)."}
{"_type":"memory","key":"cli-tool-style-prefer-python-m-homemaker-module","value":"CLI tool style: prefer python -m homemaker.module --parameters pattern, installable via pip install -e . with pyproject.toml entry_points. Not standalone bin/ scripts."}
{"_type":"memory","key":"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":"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":"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)."}

View file

@ -1821,3 +1821,56 @@ Phase-8 stack (both default OFF today, no test/runtime cost). Scoreboard: the fi
21 %) exceeds either lever alone (share 32 %→ this stacks a further 21 % on top;
depth-balance 3 % alone), confirming the §13.4 thesis that levers the search
cannot erode compound where shape levers do not.
### 13.6 Experiment: interior-O courtyard / light-well seeding (`homemaker-py-ld2`) — DONE (positive on dense floors)
The construction lever aimed at the erc crinkliness residual directly. The
adjacency-aware seeder placed ONE `O` on the most PERIPHERAL leaf — where the
adjacent rooms already have plot facade, wasting the daylight source — while the
landlocked rooms (no facade, no uncovered-`O` neighbour → `area_outside` ≈ 0 →
crinkliness ≈ 0 → fail) get nothing. This arm instead seeds `O` as INTERIOR light
wells (the most-landlocked leaves first, greedily spread so each illuminates a
fresh room set) and scales their count with the room count.
**Seed diagnostic first** (the epic mandate). Decomposing every crinkliness fail
in the bal+share seed by side of the gaussian: **all** are UNDER-exposed
(crink < 0.62, landlocked) **zero** over-exposed slivers (crink > 21.7). So the
residual is genuine under-daylighting, validating the premise (and correcting the
epic's loose "high perimeter/area" wording — the *failing* leaves are starved, not
over-walled). The naive default `outside_divisor=6` was **null** (too few/small
wells; harbor seed 147→142, crinkliness even rose). Sweeping the divisor found
`odiv=3` seed-optimal: harbor seed fails 147→129 (18), maple 219→206 (14),
landlocked fails down — at the cost of more leaves (harbor +4, maple +8). Because
it ADDS leaves it carries the §13.4 wash-out risk, so the convergence A/B decides.
**Setup** (`experiments/run_interioro_ab.sh`, staged search, 20 000 native evals,
seeds 0/1/2, final native re-score). Both arms hold the default stack
`LEAFSHARE=1` (factor 3) + `DEPTHBAL=1`. Control is interior-OFF (peripheral `O`)
— must reproduce §13.5 bal+share; experiment adds `INTERIORO=1` (odiv=3).
| programme | peripheral off (s0/1/2) | mean | interior odiv=3 (s0/1/2) | mean | Δ |
|-----------|-------------------------|-----:|--------------------------|-----:|------:|
| maple-court | 77 / 85 / 86 | 82.7 | 74 / 78 / 89 | 80.3 | 2.8 % |
| harbor-house | 41 / 43 / 38 | 40.7 | 28 / 39 / 35 | 34.0 | **16.4 %** |
The control reproduces §13.5 (maple 82.7 ≈ 82.3, harbor 40.7 ≈ 40.0), so the gap
is the lever, not drift.
**VERDICT — positive on the DENSE floor, marginal elsewhere.** Harbor is the win
the issue targeted (it named "harbor-house ~19 rooms/floor" as where the single
peripheral `O` is wasted): **16.4 %**, every seed improves (13 / 4 / 3), arms
nearly non-overlapping (interior worst 39 ≈ control best 38). Maple is **2.8 %**,
within seed noise — two seeds improve, one regresses (+3), ranges overlap. This is
the §13.4 pattern: the seed advantage (harbor 18, maple 14) survives roughly a
THIRD on harbor but mostly washes out on maple, because a dense floor has enough
landlocked rooms that the daylight gain outweighs the added-leaf tax, whereas on
the sparser maple the +8 leaves nearly cancel it. Unlike depth-balance-alone
(§13.4) which washed out *entirely*, interior-O holds on the dense floor.
Recommendation: make `interior_outside` (odiv=3) a default-ON Phase-8 lever
(default OFF today). Harbor is decisive and maple is net-neutral (mean still
2.8 %, no programme regresses on mean), so the flip is strictly ≥ on both means
and matches the dense-programme target. Follow-up `homemaker-py-*` flips the
default (mirroring `pll` after erc.7). `outside_divisor` left at 3 (seed-optimal
joint); a finer odiv sweep under convergence is low-prior given maple's marginal
response.

51
experiments/run_interioro_ab.sh Executable file
View file

@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Interior-O courtyard / light-well seeding A/B (ld2, DESIGN.md §13.6). The erc
# crinkliness residual is dominated by LANDLOCKED rooms (seed probe: every
# crinkliness fail is under-exposed crink<0.62, none over-exposed) — rooms with
# no plot facade and no uncovered-O neighbour, so area_outside ~ 0. The current
# adjacency-aware seeder places ONE O on the most PERIPHERAL leaf, where adjacent
# rooms already have facade, wasting it. This arm instead seeds O as INTERIOR
# light wells (most-landlocked leaves first) and scales their count with the room
# count (outside_divisor=3). Seed probe (bal+share stack): harbor 147->129,
# maple 219->206 total fails at odiv=3 — but the lever ADDS leaves (harbor +4,
# maple +8), so the §13.4 risk is the seed advantage washing out under search.
# This A/B answers whether it survives to convergence.
#
# Both arms hold the current default stack LEAFSHARE=1 (factor 3) + DEPTHBAL=1
# (the §13.5 winner). The control arm is interior-OFF (peripheral O) — must
# reproduce §13.5 bal+share (maple 82.3, harbor 40.0); the experiment arm adds
# INTERIORO=1 (odiv=3). Seeds 0/1/2, two programmes, 20000 native evals, staged.
set -u
cd "$(dirname "$0")/.."
BUDGET="${1:-20000}"
ODIV="${2:-3}"
OUT=scratch/interioro_ab; mkdir -p "$OUT"
TSV=scratch/interioro_results.tsv
[ -f "$TSV" ] || printf 'programme\tseed\tinterior\todiv\tfails\ttopologies\telapsed_s\n' > "$TSV"
run() { # programme seed interior(0|1)
local prog="$1" seed="$2" interior="$3"
local tag="io${interior}_od${ODIV}"
local log="$OUT/${prog}_${tag}_s${seed}.log"
echo ">>> $prog seed=$seed interior=$interior odiv=$ODIV"
local t0; t0=$(date +%s)
env URB_NO_OCCLUSION=1 LEAFSHARE=1 LEAFSHAREFAC=3 DEPTHBAL=1 \
INTERIORO="$interior" ODIV="$ODIV" \
python3 experiments/run_staged_search.py "examples/$prog" "$BUDGET" "$seed" \
"examples/$prog/init.dom" "$OUT/${prog}_${tag}_s${seed}.dom" > "$log" 2>&1
local t1; t1=$(date +%s)
local fails topos
fails=$(grep 're-scored (native)' "$log" | tail -1 | sed -n 's/.*(\([0-9]*\) fails).*/\1/p')
topos=$(grep -m1 '^evals' "$log" | sed -n 's/.*across \([0-9]*\) topologies.*/\1/p')
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$prog" "$seed" "$interior" "$ODIV" "${fails:-ERR}" "${topos:-?}" "$((t1-t0))" >> "$TSV"
echo " -> ${fails:-ERR} fails, ${topos:-?} topologies, $((t1-t0))s"
}
# peripheral-O control (reproduce §13.5) then the interior-O arm, seeds 0/1/2
for prog in maple-court harbor-house; do
for seed in 0 1 2; do run "$prog" "$seed" 0; done
for seed in 0 1 2; do run "$prog" "$seed" 1; done
done
echo "=== interior-O seeding A/B complete ==="
column -t -s $'\t' "$TSV"

View file

@ -65,6 +65,8 @@ def main() -> int:
leaf_share = os.environ.get("LEAFSHARE", "0") == "1" # erc.3 leaf-sharing A/B
leaf_share_fac = int(os.environ.get("LEAFSHAREFAC", "2"))
depth_bal = os.environ.get("DEPTHBAL", "0") == "1" # erc.4 depth-balanced grow A/B
interior_o = os.environ.get("INTERIORO", "0") == "1" # ld2 interior light-well A/B
out_div = int(os.environ.get("ODIV", "6")) # ld2 outside-leaf-per-room divisor
if leaf_share:
# erc.3 §13.3: the inner-loop and final-score fitness are built from the
@ -94,6 +96,7 @@ def main() -> int:
print(f"feas_filt : {feas} (max_shape={max_shape})")
print(f"circ_div : {circ_div}")
print(f"leaf_share: {leaf_share} (factor={leaf_share_fac})")
print(f"interior_o: {interior_o} (odiv={out_div})")
print(flush=True)
seed_root = dom.load(str(seed_file))
@ -123,6 +126,8 @@ def main() -> int:
leaf_sharing=leaf_share,
leaf_share_factor=leaf_share_fac,
depth_balanced=depth_bal,
interior_outside=interior_o,
outside_divisor=out_div,
)
elapsed = time.perf_counter() - t0

View file

@ -190,6 +190,8 @@ def search(
leaf_sharing: bool = True,
leaf_share_factor: int = 3,
depth_balanced: bool = True,
interior_outside: bool = False,
outside_divisor: int = 3,
) -> SearchResult:
"""Run the memetic loop from ``seed_root`` until ``budget`` oracle
evaluations are consumed. Returns the best individual found; its ``root``
@ -391,7 +393,8 @@ def search(
proportion_aware=seed_proportion_aware,
circ_divisor=circ_divisor,
leaf_sharing=leaf_sharing, leaf_share_factor=leaf_share_factor,
depth_balanced=depth_balanced)
depth_balanced=depth_balanced,
interior_outside=interior_outside, outside_divisor=outside_divisor)
return (topo, None, child_budget, {}, f"construct/{tag}")
n = int(rng.integers(max(1, n_target - 1), n_target + 2))
return (random_topology(seed_root, n, rng, types), None, child_budget,
@ -519,6 +522,8 @@ def search_staged(
leaf_sharing: bool = True,
leaf_share_factor: int = 3,
depth_balanced: bool = True,
interior_outside: bool = False,
outside_divisor: int = 3,
) -> SearchResult:
"""Staged per-floor topology search (DESIGN.md §11.3, ``homemaker-py-c4c.3``).
@ -569,7 +574,9 @@ def search_staged(
circ_divisor=circ_divisor,
leaf_sharing=leaf_sharing,
leaf_share_factor=leaf_share_factor,
depth_balanced=depth_balanced)
depth_balanced=depth_balanced,
interior_outside=interior_outside,
outside_divisor=outside_divisor)
if types is None:
types = sorted(reqs) + ["C", "O"]
@ -602,6 +609,8 @@ def search_staged(
leaf_sharing=leaf_sharing,
leaf_share_factor=leaf_share_factor,
depth_balanced=depth_balanced,
interior_outside=interior_outside,
outside_divisor=outside_divisor,
)
best_base = r1.best.root
_log(f"[staged] stage 1 done: base {r1.best.fitness:.6g} "
@ -621,7 +630,8 @@ def search_staged(
proportion_aware=seed_proportion_aware,
circ_divisor=circ_divisor,
leaf_sharing=leaf_sharing, leaf_share_factor=leaf_share_factor,
depth_balanced=depth_balanced)
depth_balanced=depth_balanced,
interior_outside=interior_outside, outside_divisor=outside_divisor)
_log(f"[staged] stage 2: upper floors as deltas, budget {b2}, base_p {base_p}")
r2 = search(
@ -642,6 +652,8 @@ def search_staged(
leaf_sharing=leaf_sharing,
leaf_share_factor=leaf_share_factor,
depth_balanced=depth_balanced,
interior_outside=interior_outside,
outside_divisor=outside_divisor,
)
# Stitch the two stages into one accounting (total evals, tagged history).

View file

@ -543,9 +543,22 @@ def _size_divisions_from_targets(lvl: dom.Node, reqs, fmin: float = 0.04,
geometry.clear_cache()
def _ext_exposure(leaf: dom.Node) -> int:
"""Number of the leaf's four edges that lie on the external plot perimeter
('a'/'b'/'c'/'d'); 0 means a fully landlocked (interior) leaf. Used by the
interior-``O`` light-well placement (ld2, §13.6) to find the most landlocked
leaves those whose room neighbours have no facade and so would otherwise
fail crinkliness (``area_outside`` ~ 0)."""
from . import geometry
return sum(1 for e in range(4)
if geometry.boundary_id(leaf, e) in geometry._EXTERNAL)
def _assign_adjacency_aware(lvl: dom.Node, room_codes: list[str], reqs,
rng: np.random.Generator, door_width: float = 1.2,
fixed_circ: "list[dom.Node] | None" = None) -> None:
fixed_circ: "list[dom.Node] | None" = None,
interior_outside: bool = False,
n_outside: int = 1) -> None:
"""Assign leaf types so rooms cluster around a connected circulation spine.
s44 (DESIGN.md §11.2 follow-up): random type assignment leaves rooms stranded
@ -574,7 +587,7 @@ def _assign_adjacency_aware(lvl: dom.Node, room_codes: list[str], reqs,
n = len(leaves)
idx = {leaf: i for i, leaf in enumerate(leaves)}
R = len(room_codes)
n_circ = max(1, n - (R + 1)) # leftover after rooms + one outside
n_circ = max(1, n - (R + max(1, n_outside))) # leftover after rooms + outside
seeds = [c for c in (fixed_circ or []) if c in idx]
n_circ = max(n_circ, len(seeds)) # never fewer circ leaves than the fixed core
@ -608,20 +621,46 @@ def _assign_adjacency_aware(lvl: dom.Node, room_codes: list[str], reqs,
for s in circ:
s.type = "C"
# Outside on the most peripheral non-circulation leaf (fewest circulation
# neighbours, then lowest degree) so it does not steal a circulation-adjacent
# slot a room needs.
noncirc = [L for L in leaves if L not in circ]
o_leaf = min(noncirc, key=lambda L: (sum(1 for nb in _nbrs(L) if nb in circ),
deg.get(L, 0), idx[L]))
o_leaf.type = "O"
if interior_outside:
# ld2 (§13.6): seed ``O`` as INTERIOR light wells instead of one
# peripheral leaf. A landlocked room (no plot facade, no uncovered-O
# neighbour) has area_outside ~ 0 → crinkliness ~ 0 → fail (the erc
# crinkliness residual). Placing the outside leaves on the most
# landlocked slots (fewest external edges, then highest degree = most
# room neighbours to illuminate) gives those rooms a daylight source by
# construction. Wells are spread greedily so each covers a fresh set of
# rooms rather than clustering on one over-lit pocket.
o_leaves: list[dom.Node] = []
covered: set = set()
cands = list(noncirc)
for _ in range(max(1, n_outside)):
if not cands:
break
pick = max(cands, key=lambda L: (-_ext_exposure(L),
len(_nbrs(L) - covered),
deg.get(L, 0), -idx[L]))
o_leaves.append(pick)
cands.remove(pick)
covered |= _nbrs(pick) | {pick}
for L in o_leaves:
L.type = "O"
else:
# Outside on the most peripheral non-circulation leaf (fewest circulation
# neighbours, then lowest degree) so it does not steal a circulation-
# adjacent slot a room needs.
o_leaf = min(noncirc, key=lambda L: (sum(1 for nb in _nbrs(L) if nb in circ),
deg.get(L, 0), idx[L]))
o_leaf.type = "O"
o_leaves = [o_leaf]
# Rooms onto the remaining leaves, dominated (circulation-adjacent) slots
# first so adjacency-to-c holds. Codes are placed hardest-constrained first
# (most adjacency requirements), each onto the open slot that satisfies the
# most of its requirements against already-typed neighbours (circulation and
# rooms placed so far) — clustering k1↔da1, da1↔o, etc. Ties broken randomly.
room_slots = [L for L in noncirc if L is not o_leaf]
o_set = set(o_leaves)
room_slots = [L for L in noncirc if L not in o_set]
open_slots = sorted(room_slots,
key=lambda L: (L in dominated, deg.get(L, 0), -idx[L]),
reverse=True)
@ -657,7 +696,9 @@ def constructive_topology(seed_root: dom.Node, reqs, rng: np.random.Generator,
circ_divisor: int = 3,
leaf_sharing: bool = False,
leaf_share_factor: int = 2,
depth_balanced: bool = False) -> dom.Node:
depth_balanced: bool = False,
interior_outside: bool = False,
outside_divisor: int = 3) -> dom.Node:
"""Build a seed that instantiates every required space by construction.
The §11.0 diagnosis: random divide+retype chains leave required programme
@ -718,9 +759,13 @@ def constructive_topology(seed_root: dom.Node, reqs, rng: np.random.Generator,
# tree finalisable and geometry.leaf_graph derives coords on demand.
# c3g granularity knob: ~one circ per `circ_divisor` rooms (default 3).
n_circ = max(1, -(-len(rooms) // circ_divisor))
_grow_leaves(lvl, len(rooms) + 1 + n_circ, rng, balance=depth_balanced)
# ld2 (§13.6): scale the outside-leaf count with the room count when
# seeding interior light wells (default 1 peripheral O otherwise).
n_o = max(1, round(len(rooms) / outside_divisor)) if interior_outside else 1
_grow_leaves(lvl, len(rooms) + n_o + n_circ, rng, balance=depth_balanced)
dom._link(child)
_assign_adjacency_aware(lvl, rooms, reqs, rng)
_assign_adjacency_aware(lvl, rooms, reqs, rng,
interior_outside=interior_outside, n_outside=n_o)
else:
assign = rooms + ["C", "O"] # +core circulation, +outside
_grow_leaves(lvl, len(assign), rng, balance=depth_balanced)
@ -749,7 +794,9 @@ def lift_base_to_storeys(base_root: dom.Node, upper_buckets: list[dict[str, int]
circ_divisor: int = 3,
leaf_sharing: bool = False,
leaf_share_factor: int = 2,
depth_balanced: bool = False) -> dom.Node:
depth_balanced: bool = False,
interior_outside: bool = False,
outside_divisor: int = 3) -> dom.Node:
"""Stack upper storeys onto an evolved single-storey base (DESIGN.md §11.3).
Stage 2 seeder: the Stage-1 base is the credible ground floor and is left
@ -795,7 +842,9 @@ def lift_base_to_storeys(base_root: dom.Node, upper_buckets: list[dict[str, int]
# the geometric leaf graph, seeding the dominating set from the
# inherited vertical core so the spine grows off the core, not anew.
n_circ = max(1, -(-len(rooms) // circ_divisor)) # c3g granularity knob
target_total = len(rooms) + 1 + n_circ
# ld2 (§13.6): scale interior light-well count with room count.
n_o = max(1, round(len(rooms) / outside_divisor)) if interior_outside else 1
target_total = len(rooms) + n_o + n_circ
n_free_target = target_total - (1 if core_node is not None else 0)
while len(_free()) < n_free_target:
frees = _free()
@ -814,7 +863,8 @@ def lift_base_to_storeys(base_root: dom.Node, upper_buckets: list[dict[str, int]
dom._link(child) # link so the upper storey's geometry is computable
_assign_adjacency_aware(
dup, rooms, reqs, rng,
fixed_circ=[core_node] if core_node is not None else None)
fixed_circ=[core_node] if core_node is not None else None,
interior_outside=interior_outside, n_outside=n_o)
else:
assign = rooms + ["O"] # courtyard / outside on the upper floor
if core_node is None:

View file

@ -169,6 +169,51 @@ def test_leaf_sharing_reduces_leaves_and_covers_rooms():
assert len(miss_on) < len(miss_off), f"trial {trial}: sharing didn't cover"
@pytest.mark.skipif(not HARBOR.is_dir(), reason="harbor-house not available")
def test_interior_outside_seeds_landlocked_wells_and_scales_count():
# ld2 §13.6: interior_outside seeds O on the most landlocked leaves (lower
# external-perimeter exposure) instead of the most peripheral one, and scales
# the O count with the room count. Construction must still cover every room.
from homemaker_layout import graph, geometry, programme
reqs = programme.load_programme_dir(str(HARBOR))
types = sorted(reqs) + ["C", "O"]
seed = dom.load(str(HARBOR / "init.dom"))
def _outside_exposure(root):
geometry.clear_cache()
dom._link(root)
exps, n_o = [], 0
for lvl in dom.levels(root):
for leaf in lvl.leaves():
if leaf.type and leaf.type[0].lower() == "o":
n_o += 1
exps.append(operators._ext_exposure(leaf))
return n_o, (sum(exps) / len(exps) if exps else 0.0)
for trial in range(3):
peri = operators.constructive_topology(
seed, reqs, np.random.default_rng(trial), types)
inter = operators.constructive_topology(
seed, reqs, np.random.default_rng(trial), types,
interior_outside=True, outside_divisor=3)
# no missing rooms either way
assert graph.check_space_counts(inter, reqs)[1] == []
n_peri, _exp_peri = _outside_exposure(peri)
n_inter, exp_inter = _outside_exposure(inter)
# the lever adds more outside leaves (scaled with room count)…
assert n_inter > n_peri, f"trial {trial}: {n_inter} !> {n_peri}"
# …and places them on landlocked leaves: a well averaging < 1 external
# plot edge is interior by construction (peripheral mode does not aim
# for this — its single O is chosen by circulation distance, so it can
# land anywhere — hence we assert the absolute landlocked property).
assert exp_inter < 1.0, (
f"trial {trial}: interior O wells not landlocked (mean exp {exp_inter})")
@pytest.mark.skipif(not HARBOR.is_dir(), reason="harbor-house not available")
def test_adjacency_aware_seeding_cuts_adjacency_access_fails():
# s44: adjacency-aware construction clusters rooms around a connected