hph/§13.8: share-aware edge-too-long cap — shared leaves no longer penalised for aggregate wall length
§13.7 flagged edge-too-long as harbor's top fail class. Dissection showed the bulk are a leaf-sharing REPRESENTATION ARTIFACT: a share=k leaf aggregates k same-code rooms, so its walls run ~k× the flat 8 m cap purely for being big — the same §13.3 leak (size/missing relaxed for shared leaves) on the wall measure, since edge_cost/outside_edge_cost ignored leaf.share. Fix: Fitness._edge_cap(*leaves) scales the 8 m cap by the largest type-guarded leaf_share among adjoining leaves, mirroring quality_size's k×target; non-shared leaves keep the flat cap so genuine narrow/oversize pathologies stay flagged. Gated behind a share_edge_cap config knob (SHAREEDGE env), default OFF so the §13.x controls reproduce. A/B (full Phase-8 stack, staged, 20k evals, seeds 0/1/2): control reproduces §13.7 (maple 80.3 exact, harbor 34.7≈34.0); share-aware arm maple 80.3→74.0 (−7.9%), harbor 34.7→31.0 (−10.6%), zero regressions across 6 seeds. Positive and monotone-harmless (only ever removes a false-positive fail). Verdict: recommend default-ON; follow-up issue flips the default + rebaselines the floor. Tests: 6 new unit tests for _edge_cap (221 pass). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JygRv4n2dcyDQqMiDRe7TN
This commit is contained in:
parent
3f8cf0c313
commit
393183b356
6 changed files with 192 additions and 15 deletions
|
|
@ -22,7 +22,8 @@
|
||||||
{"id":"homemaker-py-1p0","title":"Geometry inner loop: full-objective equal-offset ratio optimiser","description":"DESIGN.md §5.1, §7 Phase 1. Productionise experiments/optimize_fullfitness.py into homemaker: optimise(topology, x0=None) -\u003e (geometry, fitness). DOF = equal-offset division ratios of free branches (solver.free_branches, lowest-storey cut ownership), clipped to [eps, 1-eps]. Objective = full oracle fitness (never a proxy — §4.2 falsified). Must support warm-start x0 (§5.6) and a population/batch evaluation mode so each iteration scores via one batched oracle call (§4.6).","acceptance_criteria":"Reproduces or exceeds §4.5 gains (x1.24–x1.67, no new failures) on 2f45907, candidate-002, c964435; works as a library call on any corpus .dom","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:58Z","created_by":"Bruno Postle","updated_at":"2026-06-12T08:46:31Z","started_at":"2026-06-12T00:14:19Z","closed_at":"2026-06-12T08:46:31Z","close_reason":"innerloop.optimise() lands: batched CMA-ES sigma ladder (0.05/0.15, IPOP popsize doubling, deterministic seeding) over equal-offset free-branch ratios vs full oracle fitness; warm-start x0 supported. Acceptance vs unprojected originals: x1.65/x1.66/x1.58 against bars x1.24/x1.67/x1.59, no new failures, 46 oracle calls vs NM's 200. Two near-bar results accepted as reproduced-within-noise (1% tol) — draw spread brackets the single-NM-draw bars; approved by Bruno 2026-06-12. Gotchas: equal-offset projection of legacy unequal cuts loses fitness/adds failures (midpoint projection used); pycma seed=0 means clock-seeded.","dependencies":[{"issue_id":"homemaker-py-1p0","depends_on_id":"homemaker-py-av5","type":"blocks","created_at":"2026-06-12T00:39:33Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0}
|
{"id":"homemaker-py-1p0","title":"Geometry inner loop: full-objective equal-offset ratio optimiser","description":"DESIGN.md §5.1, §7 Phase 1. Productionise experiments/optimize_fullfitness.py into homemaker: optimise(topology, x0=None) -\u003e (geometry, fitness). DOF = equal-offset division ratios of free branches (solver.free_branches, lowest-storey cut ownership), clipped to [eps, 1-eps]. Objective = full oracle fitness (never a proxy — §4.2 falsified). Must support warm-start x0 (§5.6) and a population/batch evaluation mode so each iteration scores via one batched oracle call (§4.6).","acceptance_criteria":"Reproduces or exceeds §4.5 gains (x1.24–x1.67, no new failures) on 2f45907, candidate-002, c964435; works as a library call on any corpus .dom","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:58Z","created_by":"Bruno Postle","updated_at":"2026-06-12T08:46:31Z","started_at":"2026-06-12T00:14:19Z","closed_at":"2026-06-12T08:46:31Z","close_reason":"innerloop.optimise() lands: batched CMA-ES sigma ladder (0.05/0.15, IPOP popsize doubling, deterministic seeding) over equal-offset free-branch ratios vs full oracle fitness; warm-start x0 supported. Acceptance vs unprojected originals: x1.65/x1.66/x1.58 against bars x1.24/x1.67/x1.59, no new failures, 46 oracle calls vs NM's 200. Two near-bar results accepted as reproduced-within-noise (1% tol) — draw spread brackets the single-NM-draw bars; approved by Bruno 2026-06-12. Gotchas: equal-offset projection of legacy unequal cuts loses fitness/adds failures (midpoint projection used); pycma seed=0 means clock-seeded.","dependencies":[{"issue_id":"homemaker-py-1p0","depends_on_id":"homemaker-py-av5","type":"blocks","created_at":"2026-06-12T00:39:33Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0}
|
||||||
{"id":"homemaker-py-8cs","title":"Experiment: warm-vs-cold start of inner loop (Lamarckian inheritance)","description":"DESIGN.md §5.6, §4.6. Warm-starting a child topology's inner loop from the parent's optimised ratios is the main lever for cutting per-topology cost (~3 min/topology cold). Apply single topology mutations to optimised corpus designs, re-optimise warm (surviving cuts keep values, new cuts get heuristic defaults) vs cold, compare oracle-call counts to convergence at equal final fitness.","acceptance_criteria":"Speedup factor measured across \u003e=10 mutated topologies; decision recorded (expect order-of-magnitude; if \u003c2x, revisit §4.6 Phase-2 scoping)","notes":"Experiment script committed (experiments/warm_vs_cold.py, 1cc86c8) and machinery validated oracle-free; one mutated child scored through the oracle OK. Waiting on homemaker-py-gp2 reference run to finish, then execute under URB_NO_OCCLUSION=1 (3 parents x 400 evals + 12 children x 2 x 200 evals, ~1.5-2 h oracle time). Default budgets: parent 400, child 200; target = evals to 95% of best final.","status":"closed","priority":1,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:58Z","created_by":"Bruno Postle","updated_at":"2026-06-12T11:44:45Z","closed_at":"2026-06-12T11:44:45Z","close_reason":"Measured (URB_NO_OCCLUSION=1, parent budget 400, child 200, 12 single mutations across 3 designs): cold start reached 95% of warm final in 0/12 cases within budget — speedup unbounded at practical budgets; warm finals beat cold finals x1.2-x4 in 12/12; 6/12 warm starts were within 95% at 1 eval (near-neutral mutations). Decision: Lamarckian warm-starting is MANDATORY in the memetic driver (homemaker-py-b39), not an optimisation; cold starts produce strictly worse geometry at equal budget. Note: 2 undivides were exactly fitness-neutral (same-type merge == Merge_Divided equivalence) — locality datum for homemaker-py-nyb.","dependencies":[{"issue_id":"homemaker-py-8cs","depends_on_id":"homemaker-py-1p0","type":"blocks","created_at":"2026-06-12T00:39:34Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-8cs","title":"Experiment: warm-vs-cold start of inner loop (Lamarckian inheritance)","description":"DESIGN.md §5.6, §4.6. Warm-starting a child topology's inner loop from the parent's optimised ratios is the main lever for cutting per-topology cost (~3 min/topology cold). Apply single topology mutations to optimised corpus designs, re-optimise warm (surviving cuts keep values, new cuts get heuristic defaults) vs cold, compare oracle-call counts to convergence at equal final fitness.","acceptance_criteria":"Speedup factor measured across \u003e=10 mutated topologies; decision recorded (expect order-of-magnitude; if \u003c2x, revisit §4.6 Phase-2 scoping)","notes":"Experiment script committed (experiments/warm_vs_cold.py, 1cc86c8) and machinery validated oracle-free; one mutated child scored through the oracle OK. Waiting on homemaker-py-gp2 reference run to finish, then execute under URB_NO_OCCLUSION=1 (3 parents x 400 evals + 12 children x 2 x 200 evals, ~1.5-2 h oracle time). Default budgets: parent 400, child 200; target = evals to 95% of best final.","status":"closed","priority":1,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:58Z","created_by":"Bruno Postle","updated_at":"2026-06-12T11:44:45Z","closed_at":"2026-06-12T11:44:45Z","close_reason":"Measured (URB_NO_OCCLUSION=1, parent budget 400, child 200, 12 single mutations across 3 designs): cold start reached 95% of warm final in 0/12 cases within budget — speedup unbounded at practical budgets; warm finals beat cold finals x1.2-x4 in 12/12; 6/12 warm starts were within 95% at 1 eval (near-neutral mutations). Decision: Lamarckian warm-starting is MANDATORY in the memetic driver (homemaker-py-b39), not an optimisation; cold starts produce strictly worse geometry at equal budget. Note: 2 undivides were exactly fitness-neutral (same-type merge == Merge_Divided equivalence) — locality datum for homemaker-py-nyb.","dependencies":[{"issue_id":"homemaker-py-8cs","depends_on_id":"homemaker-py-1p0","type":"blocks","created_at":"2026-06-12T00:39:34Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-av5","title":"Batched oracle: score many .dom files per invocation","description":"oracle.py currently scores one .dom per urb-fitness.pl call (~1.65 s/dom). DESIGN.md §4.6: batching amortises Perl startup to ~0.99 s/dom and is required so population/batch optimisers can score a whole generation in one oracle call. Extend oracle.py with a batch API: write N .dom files, one perl invocation, parse N .score/.fails pairs. Keep the single-file path for compatibility.","acceptance_criteria":"Batch of 35 corpus files scores in one perl invocation; per-file results identical to single-file calls; measured s/dom reported","status":"closed","priority":1,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:56Z","created_by":"Bruno Postle","updated_at":"2026-06-12T00:14:06Z","started_at":"2026-06-11T23:50:40Z","closed_at":"2026-06-12T00:14:06Z","close_reason":"score_batch() lands in oracle.py; 35-file corpus parity verified single-vs-batch (1e-12 rel fitness, exact fail sets); 0.98 s/dom batched vs 1.27 single, x1.30","dependency_count":0,"dependent_count":1,"comment_count":0}
|
{"id":"homemaker-py-av5","title":"Batched oracle: score many .dom files per invocation","description":"oracle.py currently scores one .dom per urb-fitness.pl call (~1.65 s/dom). DESIGN.md §4.6: batching amortises Perl startup to ~0.99 s/dom and is required so population/batch optimisers can score a whole generation in one oracle call. Extend oracle.py with a batch API: write N .dom files, one perl invocation, parse N .score/.fails pairs. Keep the single-file path for compatibility.","acceptance_criteria":"Batch of 35 corpus files scores in one perl invocation; per-file results identical to single-file calls; measured s/dom reported","status":"closed","priority":1,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:36:56Z","created_by":"Bruno Postle","updated_at":"2026-06-12T00:14:06Z","started_at":"2026-06-11T23:50:40Z","closed_at":"2026-06-12T00:14:06Z","close_reason":"score_batch() lands in oracle.py; 35-file corpus parity verified single-vs-batch (1e-12 rel fitness, exact fail sets); 0.98 s/dom batched vs 1.27 single, x1.30","dependency_count":0,"dependent_count":1,"comment_count":0}
|
||||||
{"id":"homemaker-py-hph","title":"edge-too-long not share-aware: shared leaves (share\u003e1) penalised for aggregate wall length (§13.7 follow-up)","description":"DESIGN §13.7 flagged edge-too-long as harbor's top fail class (6). Dissection (experiments/diag_edge_too_long.py on the 500k probe best) shows the 6 fails are only 2 distinct locations:\n\n(1) DOMINANT ~4/6: leaf 'lllr' on both levels is a share=3 leaf (one quad = 3 rooms, 247 m2, edges 15-17 m, aspect 1.2 NEARLY SQUARE). Its walls exceed the flat 8 m cap purely because it aggregates 3 rooms — a leaf-sharing REPRESENTATION ARTIFACT, not a design flaw. §13.3 relaxed size/missing for shared leaves (quality_size centres on k*target) but edge_cost (fitness.py:474) and outside_edge_cost (fitness.py:490) still use a flat 8.0 m regardless of leaf.share. So a shared leaf is penalised for being big — the same leak §13.3 closed, on a different measure.\n\n(2) ~2/6: leaf 'llll' is a 1.2 m x 16.7 m sliver (aspect 14) at correct area — a REAL narrow-room pathology, already caught by width/proportion. Its edge-too-long is the wall it shares with lllr.\n\nNo corridors involved.\n\nPROPOSED FIX: make edge-too-long share-aware — exempt or scale the 8 m cap by leaf.share (type-guarded, as graph.leaf_share does) in edge_cost/outside_edge_cost, mirroring quality_size's k*target. Clears the ~4 artifact fails without masking the narrow sliver. Optional separate lever: lift/parametrise the flat 8 m cap for non-domestic programmes (harbor-house) — blunter, lower priority. A/B under §13 protocol (controls reproduce harbor 34.0 / maple 80.3); record verdict. Repro: experiments/diag_edge_too_long.py.","status":"open","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-06-28T13:51:26Z","created_by":"Bruno Postle","updated_at":"2026-06-28T13:53:14Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-rq2","title":"Flip share_edge_cap default-ON + rebaseline §13.x floor (hph follow-up)","description":"hph/§13.8 A/B confirmed the share-aware edge-too-long cap is positive and monotone-harmless (maple 80.3→74.0, harbor 34.7→31.0, zero regressions across 6 seeds). The fix shipped behind the SHAREEDGE/share_edge_cap knob (default OFF) so controls reproduce. This issue flips the default ON for leaf-sharing runs — it completes the §13.3 leaf-share objective relaxation on the wall measure, mirroring the pll/interior_outside default flips. Rebaselines the §13.x full-stack floor numbers (harbor 34.7→31.0, maple 80.3→74.0 become the new baseline). Couple with INTERIORO/odiv3 if those are also being default-flipped. Verify the test suite + a control re-score still reproduce post-flip.","status":"open","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-28T20:01:11Z","created_by":"Bruno Postle","updated_at":"2026-06-28T20:01:11Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"id":"homemaker-py-hph","title":"edge-too-long not share-aware: shared leaves (share\u003e1) penalised for aggregate wall length (§13.7 follow-up)","description":"DESIGN §13.7 flagged edge-too-long as harbor's top fail class (6). Dissection (experiments/diag_edge_too_long.py on the 500k probe best) shows the 6 fails are only 2 distinct locations:\n\n(1) DOMINANT ~4/6: leaf 'lllr' on both levels is a share=3 leaf (one quad = 3 rooms, 247 m2, edges 15-17 m, aspect 1.2 NEARLY SQUARE). Its walls exceed the flat 8 m cap purely because it aggregates 3 rooms — a leaf-sharing REPRESENTATION ARTIFACT, not a design flaw. §13.3 relaxed size/missing for shared leaves (quality_size centres on k*target) but edge_cost (fitness.py:474) and outside_edge_cost (fitness.py:490) still use a flat 8.0 m regardless of leaf.share. So a shared leaf is penalised for being big — the same leak §13.3 closed, on a different measure.\n\n(2) ~2/6: leaf 'llll' is a 1.2 m x 16.7 m sliver (aspect 14) at correct area — a REAL narrow-room pathology, already caught by width/proportion. Its edge-too-long is the wall it shares with lllr.\n\nNo corridors involved.\n\nPROPOSED FIX: make edge-too-long share-aware — exempt or scale the 8 m cap by leaf.share (type-guarded, as graph.leaf_share does) in edge_cost/outside_edge_cost, mirroring quality_size's k*target. Clears the ~4 artifact fails without masking the narrow sliver. Optional separate lever: lift/parametrise the flat 8 m cap for non-domestic programmes (harbor-house) — blunter, lower priority. A/B under §13 protocol (controls reproduce harbor 34.0 / maple 80.3); record verdict. Repro: experiments/diag_edge_too_long.py.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-28T13:51:26Z","created_by":"Bruno Postle","updated_at":"2026-06-28T20:03:00Z","started_at":"2026-06-28T14:46:18Z","closed_at":"2026-06-28T20:03:00Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-71d.1","title":"Diagnostic: high-budget harbor floor on full default stack — does landlocked crinkliness still dominate after interior-O?","description":"71d go/no-go probe. 71d targets landlocked crinkliness (area_outside=0, ratio-invariant) which its named fix (interior O courtyards) addresses. interior_outside now ships default-ON (erc.8), so re-measure: run harbor full default stack at high budget (1M evals, n_workers=4, seed 0) and break down the at-convergence residual — fail-type histogram + landlocked-vs-under-exposed split of crinkliness fails. If landlocked still dominates -\u003e 71d worth it; if interior-O dissolved it -\u003e 71d redundant. Verdict to DESIGN.md.","notes":"VERDICT (DESIGN §13.7): NO-GO on 71d. 500k serial full-stack harbor probe (seed 0) -\u003e 20 fails. Crinkliness collapsed 13-\u003e4, landlocked crinkliness ~13-\u003e2 of 20. Interior-O (now default) IS 71d's named fix (interior O courtyards) and already dissolved the target block. Residual now diffuse (top class edge-too-long 6), no concentrated ratio-invariant block for a targeted operator. Recommend close 71d + 7u5/jrb/u8x as superseded-by-construction.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-28T06:57:44Z","created_by":"Bruno Postle","updated_at":"2026-06-28T13:19:08Z","started_at":"2026-06-28T06:58:10Z","closed_at":"2026-06-28T13:19:08Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-71d.1","depends_on_id":"homemaker-py-71d","type":"parent-child","created_at":"2026-06-28T07:57:44Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-71d.1","title":"Diagnostic: high-budget harbor floor on full default stack — does landlocked crinkliness still dominate after interior-O?","description":"71d go/no-go probe. 71d targets landlocked crinkliness (area_outside=0, ratio-invariant) which its named fix (interior O courtyards) addresses. interior_outside now ships default-ON (erc.8), so re-measure: run harbor full default stack at high budget (1M evals, n_workers=4, seed 0) and break down the at-convergence residual — fail-type histogram + landlocked-vs-under-exposed split of crinkliness fails. If landlocked still dominates -\u003e 71d worth it; if interior-O dissolved it -\u003e 71d redundant. Verdict to DESIGN.md.","notes":"VERDICT (DESIGN §13.7): NO-GO on 71d. 500k serial full-stack harbor probe (seed 0) -\u003e 20 fails. Crinkliness collapsed 13-\u003e4, landlocked crinkliness ~13-\u003e2 of 20. Interior-O (now default) IS 71d's named fix (interior O courtyards) and already dissolved the target block. Residual now diffuse (top class edge-too-long 6), no concentrated ratio-invariant block for a targeted operator. Recommend close 71d + 7u5/jrb/u8x as superseded-by-construction.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-28T06:57:44Z","created_by":"Bruno Postle","updated_at":"2026-06-28T13:19:08Z","started_at":"2026-06-28T06:58:10Z","closed_at":"2026-06-28T13:19:08Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-71d.1","depends_on_id":"homemaker-py-71d","type":"parent-child","created_at":"2026-06-28T07:57:44Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"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":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-28T06:18:14Z","created_by":"Bruno Postle","updated_at":"2026-06-28T06:29:48Z","started_at":"2026-06-28T06:26:42Z","closed_at":"2026-06-28T06:29:48Z","close_reason":"Closed","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-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":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-28T06:18:14Z","created_by":"Bruno Postle","updated_at":"2026-06-28T06:29:48Z","started_at":"2026-06-28T06:26:42Z","closed_at":"2026-06-28T06:29:48Z","close_reason":"Closed","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-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}
|
||||||
|
|
@ -69,20 +70,20 @@
|
||||||
{"id":"homemaker-py-erc.6","title":"Experiment: inner-loop slack-expansion objective term","description":"Inner-loop counterpart to plot-fill construction. If Diagnostic B shows the inner loop has room to expand leaves into slack but no objective gradient to do so (the scalar rewards hitting target area but not exceeding it where slack exists), add a term/incentive so the ratio optimiser pushes leaf boundaries out to consume neighbouring slack and satisfy size, rather than parking at target.\n\nCONDITIONAL on Diagnostic B: build this only if B localizes the gap to the inner loop (room to expand, no gradient); if B shows construction targets too-small dims, prefer the plot-fill construction sibling. Must preserve the §5.4 inner-loop cliff / §4.9 lexicographic protection — the term sits where it cannot displace the fail-count ordering. A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.6.","notes":"DEPRIORITISED by Diagnostic B (§13.2). B shows the inner loop CANNOT repair undersize: the slack is depth-driven maldistribution baked into the frozen topology, and the equal-offset ratio DOF cannot shrink a 14x leaf to feed a starved one without trading into shape fails (0.5^n cliff). Wrong DOF and wrong direction — the blocker is slicing POSITION, not a missing expansion reward. Fix belongs upstream in construction/topology (erc.4 re-scoped, erc.3). Keep as a low-priority follow-up only if a depth-balanced construction still leaves a residual size gradient the inner loop could pick up.","status":"closed","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-22T23:16:24Z","created_by":"Bruno Postle","updated_at":"2026-06-28T13:22:22Z","closed_at":"2026-06-28T13:22:22Z","close_reason":"wont-fix (DESIGN §13.7): Diag B (§13.2) showed the inner loop cannot repair undersize (wrong DOF — slicing position, frozen-topology ratios). Superseded by depth-balanced construction (erc.4). Condition unmet.","dependencies":[{"issue_id":"homemaker-py-erc.6","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T00:16:23Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-erc.6","depends_on_id":"homemaker-py-erc.2","type":"blocks","created_at":"2026-06-23T00:16:47Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-erc.6","title":"Experiment: inner-loop slack-expansion objective term","description":"Inner-loop counterpart to plot-fill construction. If Diagnostic B shows the inner loop has room to expand leaves into slack but no objective gradient to do so (the scalar rewards hitting target area but not exceeding it where slack exists), add a term/incentive so the ratio optimiser pushes leaf boundaries out to consume neighbouring slack and satisfy size, rather than parking at target.\n\nCONDITIONAL on Diagnostic B: build this only if B localizes the gap to the inner loop (room to expand, no gradient); if B shows construction targets too-small dims, prefer the plot-fill construction sibling. Must preserve the §5.4 inner-loop cliff / §4.9 lexicographic protection — the term sits where it cannot displace the fail-count ordering. A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.6.","notes":"DEPRIORITISED by Diagnostic B (§13.2). B shows the inner loop CANNOT repair undersize: the slack is depth-driven maldistribution baked into the frozen topology, and the equal-offset ratio DOF cannot shrink a 14x leaf to feed a starved one without trading into shape fails (0.5^n cliff). Wrong DOF and wrong direction — the blocker is slicing POSITION, not a missing expansion reward. Fix belongs upstream in construction/topology (erc.4 re-scoped, erc.3). Keep as a low-priority follow-up only if a depth-balanced construction still leaves a residual size gradient the inner loop could pick up.","status":"closed","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-22T23:16:24Z","created_by":"Bruno Postle","updated_at":"2026-06-28T13:22:22Z","closed_at":"2026-06-28T13:22:22Z","close_reason":"wont-fix (DESIGN §13.7): Diag B (§13.2) showed the inner loop cannot repair undersize (wrong DOF — slicing position, frozen-topology ratios). Superseded by depth-balanced construction (erc.4). Condition unmet.","dependencies":[{"issue_id":"homemaker-py-erc.6","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T00:16:23Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-erc.6","depends_on_id":"homemaker-py-erc.2","type":"blocks","created_at":"2026-06-23T00:16:47Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-erc.5","title":"Experiment: compactness-aware cuts (minimize leaf perimeter/area)","description":"Attacks the #1 factor, crinkliness (346) — a per-leaf perimeter/area property DISTINCT from proportion (aspect ratio). Proportion-aware seeding (leu.2) sizes splits but does not bias toward balanced, square-ish subdivision. Add a KD-tree-style 'keep both children compact' cut rule (prefer the cut orientation/position that minimises summed child perimeter/area) in construction.\n\nCONDITIONAL on Diagnostic A: if A shows per-leaf shape-fail is FLAT across densities (floor intrinsic to slicing density), better cuts at the same leaf count will not pay → this should be closed wont-fix in favour of leaf-sharing. Only build if A shows shape-fail RISES with density. A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.5.","notes":"DEPRIORITISED by erc.1 verdict (§13.1): per-leaf shape-fail flat vs slicing density and cuts already squarest (_size_divisions_from_targets picks squarest rotation) yet still ~1.8 fails/leaf =\u003e little compactness headroom at fixed leaf count. Floor is intrinsic to leaf COUNT, not cut quality. Revisit only if leaf-sharing (erc.3) underdelivers.","status":"closed","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-22T23:16:21Z","created_by":"Bruno Postle","updated_at":"2026-06-28T13:22:17Z","closed_at":"2026-06-28T13:22:17Z","close_reason":"wont-fix (DESIGN §13.7): Diag A (§13.1) showed the floor is intrinsic to leaf COUNT not cut quality; revisit condition was 'only if leaf-sharing underdelivers' but leaf-sharing OVER-delivered (−32…−39%, §13.3). Condition unmet.","dependencies":[{"issue_id":"homemaker-py-erc.5","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T00:16:21Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-erc.5","depends_on_id":"homemaker-py-erc.1","type":"blocks","created_at":"2026-06-23T00:16:43Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-erc.5","title":"Experiment: compactness-aware cuts (minimize leaf perimeter/area)","description":"Attacks the #1 factor, crinkliness (346) — a per-leaf perimeter/area property DISTINCT from proportion (aspect ratio). Proportion-aware seeding (leu.2) sizes splits but does not bias toward balanced, square-ish subdivision. Add a KD-tree-style 'keep both children compact' cut rule (prefer the cut orientation/position that minimises summed child perimeter/area) in construction.\n\nCONDITIONAL on Diagnostic A: if A shows per-leaf shape-fail is FLAT across densities (floor intrinsic to slicing density), better cuts at the same leaf count will not pay → this should be closed wont-fix in favour of leaf-sharing. Only build if A shows shape-fail RISES with density. A/B vs §12.2 baseline, seeds 0/1/2, 20000 evals, staged, default-OFF. Record DESIGN.md §13.5.","notes":"DEPRIORITISED by erc.1 verdict (§13.1): per-leaf shape-fail flat vs slicing density and cuts already squarest (_size_divisions_from_targets picks squarest rotation) yet still ~1.8 fails/leaf =\u003e little compactness headroom at fixed leaf count. Floor is intrinsic to leaf COUNT, not cut quality. Revisit only if leaf-sharing (erc.3) underdelivers.","status":"closed","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-22T23:16:21Z","created_by":"Bruno Postle","updated_at":"2026-06-28T13:22:17Z","closed_at":"2026-06-28T13:22:17Z","close_reason":"wont-fix (DESIGN §13.7): Diag A (§13.1) showed the floor is intrinsic to leaf COUNT not cut quality; revisit condition was 'only if leaf-sharing underdelivers' but leaf-sharing OVER-delivered (−32…−39%, §13.3). Condition unmet.","dependencies":[{"issue_id":"homemaker-py-erc.5","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-23T00:16:21Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-erc.5","depends_on_id":"homemaker-py-erc.1","type":"blocks","created_at":"2026-06-23T00:16:43Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||||
{"id":"homemaker-py-2g5","title":"Rebuild occlusion/daylight/sun subsystem in Python (post-Phase-5, after optimisation fully native)","description":"DESIGN.md §6 port scope — a whole subsystem, not a term. quality_daylight (Leaf.pm:281-296) needs Urb::Misc::Sun + Urb::Field::Occlusion (+CIESky); quality_uncrinkliness also takes the occlusion object. Indoor spaces return 1 for daylight; cost is outdoor spaces + crinkliness. Port Sun_horizontal (262980-minute normalisation) and the occlusion wall set from Dom-\u003eWalls.","acceptance_criteria":"Daylight and crinkliness factors match Perl (float tolerance) across the corpus, including multi-storey cases","notes":"Re-scoped 2026-06-12: occlusion disabled in the Urb oracle instead of ported (see homemaker-py-gp2). Native fitness ships with simple crinkliness (illumination factor = 1, in homemaker-py-gnw). This issue is now the eventual Python occlusion rebuild, only after optimisation works entirely in Python. Restores outdoor-daylight and shaded-wall selection pressure.\nReframed 2026-06-17: orthogonal to epic homemaker-py-c4c. This is fitness FIDELITY (restoring daylight + shaded-wall selection pressure to match Perl), not search CAPABILITY — it changes what 'good' means, not the search's ability to find good. It will NOT improve final designs in the sense currently sought. Stays P4, deferred until the topology-search-quality epic lands and optimisation is fully native.","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:25Z","created_by":"Bruno Postle","updated_at":"2026-06-17T19:14:48Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-2g5","title":"Rebuild occlusion/daylight/sun subsystem in Python (post-Phase-5, after optimisation fully native)","description":"DESIGN.md §6 port scope — a whole subsystem, not a term. quality_daylight (Leaf.pm:281-296) needs Urb::Misc::Sun + Urb::Field::Occlusion (+CIESky); quality_uncrinkliness also takes the occlusion object. Indoor spaces return 1 for daylight; cost is outdoor spaces + crinkliness. Port Sun_horizontal (262980-minute normalisation) and the occlusion wall set from Dom-\u003eWalls.","acceptance_criteria":"Daylight and crinkliness factors match Perl (float tolerance) across the corpus, including multi-storey cases","notes":"Re-scoped 2026-06-12: occlusion disabled in the Urb oracle instead of ported (see homemaker-py-gp2). Native fitness ships with simple crinkliness (illumination factor = 1, in homemaker-py-gnw). This issue is now the eventual Python occlusion rebuild, only after optimisation works entirely in Python. Restores outdoor-daylight and shaded-wall selection pressure.\nReframed 2026-06-17: orthogonal to epic homemaker-py-c4c. This is fitness FIDELITY (restoring daylight + shaded-wall selection pressure to match Perl), not search CAPABILITY — it changes what 'good' means, not the search's ability to find good. It will NOT improve final designs in the sense currently sought. Stays P4, deferred until the topology-search-quality epic lands and optimisation is fully native.","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:25Z","created_by":"Bruno Postle","updated_at":"2026-06-17T19:14:48Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"memory","key":"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":"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":"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":"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":"experiment-harness-gotcha-the-leaf-sharing-relaxed-objective","value":"Experiment harness gotcha: the leaf-sharing RELAXED objective (§13.3) is injected ONLY by monkeypatching fitness.load_config in the parent process (run_staged_search.py / probe scripts). This is parent-process-only and does NOT propagate into ProcessPoolExecutor workers (n_workers\u003e1), which re-import fitness fresh and score under the STRICT on-disk patterns.config -\u003e r.n_fails MISMATCH (worker strict vs parent relaxed re-score). ALL §13.x floor runs were therefore SERIAL. Any future PARALLEL leaf-sharing experiment will silently mis-score until leaf_sharing lives on disk/CLI (tracked: homemaker-py-x3b). The parallel driver itself is correct; both paths score via load_config(programme_dir)."}
|
|
||||||
{"_type":"memory","key":"never-use-corpus-filenames-candidate-001-dom-candidate","value":"Never use corpus filenames (candidate-001.dom, candidate-002.dom, generated.dom, init.dom, etc.) as --output targets when running experiments. These are test fixtures. Always write experimental outputs to scratch/ or a timestamped path. Lesson from 2026-06-14: warm-start runs overwrote candidate-001/002.dom and broke graph tests."}
|
|
||||||
{"_type":"memory","key":"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":"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":"strategy-decision-2026-06-12-bruno-occlusion-daylight","value":"Strategy decision 2026-06-12 (Bruno): occlusion/daylight is ORTHOGONAL to building a scalable optimiser. Disable it in Urb (env flag, homemaker-py-gp2) rather than port it; native fitness uses simple crinkliness (illumination factor = 1); rebuild occlusion in Python only after optimisation is fully native (homemaker-py-2g5, now P4). Consequence: all scores change when the flag flips — re-baseline corpus/.score, DESIGN \\$4.5 gains, gate bars at one clean boundary AFTER homemaker-py-1p0 closes; Phase-2 urb-evolve benchmark must run with the same flag."}
|
||||||
{"_type":"memory","key":"user-preference-bruno-this-is-a-fedora-system","value":"User preference (Bruno): this is a Fedora system — NEVER install Python packages via pip without asking first; always ask whether to install the rpm via dnf (e.g. python3-cma) before considering pip. Applies to any dependency additions."}
|
|
||||||
{"_type":"memory","key":"urb-fitness-bug-found-fixed-2026-06-12","value":"Urb fitness bug found+fixed 2026-06-12 (patch in /home/bruno/src/urb, uncommitted): ProgrammeDriven.pm ratio_o/ratio_type grepped case-insensitively over the ratios hash and took the FIRST key — nondeterministic (x4.5 score swings) for designs with mixed-case type classes (both 'c' circulation and 'C' covered). Fixed to SUM the class (matches Is_Circulation//Is_Outside semantics); 35/35 corpus scores unchanged. CRITICAL for homemaker-py-3y7/gnw: the native port must implement class-SUM ratios. Building.pm has the same unpatched pattern (site-driven path, not used by our oracle). Also: the memetic search reward-hacked this bug before the fix — search results predating it are noise artifacts."}
|
{"_type":"memory","key":"urb-fitness-bug-found-fixed-2026-06-12","value":"Urb fitness bug found+fixed 2026-06-12 (patch in /home/bruno/src/urb, uncommitted): ProgrammeDriven.pm ratio_o/ratio_type grepped case-insensitively over the ratios hash and took the FIRST key — nondeterministic (x4.5 score swings) for designs with mixed-case type classes (both 'c' circulation and 'C' covered). Fixed to SUM the class (matches Is_Circulation//Is_Outside semantics); 35/35 corpus scores unchanged. CRITICAL for homemaker-py-3y7/gnw: the native port must implement class-SUM ratios. Building.pm has the same unpatched pattern (site-driven path, not used by our oracle). Also: the memetic search reward-hacked this bug before the fix — search results predating it are noise artifacts."}
|
||||||
{"_type":"memory","key":"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":"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":"cli-tool-style-prefer-python-m-homemaker-module","value":"CLI tool style: prefer python -m homemaker.module --parameters pattern, installable via pip install -e . with pyproject.toml entry_points. Not standalone bin/ scripts."}
|
||||||
|
{"_type":"memory","key":"ld2-13-6-interior-o-seed-diagnostic-all","value":"ld2/§13.6 interior-O seed diagnostic: ALL crinkliness fails in the constructed bal+share seed are UNDER-exposed (crink\u003c0.62, landlocked rooms with no facade + no uncovered-O neighbour) — zero over-exposed sliver fails. So the erc crinkliness residual is genuine under-daylighting, validating the interior light-well premise. Default outside_divisor=6 was too sparse (null: harbor 147-\u003e142, crinkliness even rose). odiv=3 is the seed-optimal joint setting: harbor seed fails 147-\u003e129 (-18), maple 219-\u003e206 (-14), landlocked fails drop, at cost of more leaves (harbor +4, maple +8). Because it ADDS leaves it carries the §13.4 wash-out risk; A/B to convergence pending."}
|
||||||
|
{"_type":"memory","key":"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":"proportion-aware-constructive-seeding-leu-2-12-2","value":"Proportion-aware constructive seeding (leu.2/§12.2): sizing seed cuts from target AREAS only regresses (thin slivers wreck aspect); you must ALSO pick each cut's rotation for child squareness. It is a convergence ACCELERATOR via a deeper local optimum around the constructed topology: wins where that topology is roughly right and budget is scarce (harbor -13%, maple -10% at 20k evals) but DELAYS small programmes where the seed must be restructured by undivide (programme-house regresses at fixed budget, yet reaches the floor given budget - speed, not asymptote). Default-on. Also: n_storeys must honour storey_minimum, not just level: keys (programme-house storey_minimum:2, all rooms level:0 - was seeded 1 storey short; cq1)."}
|
{"_type":"memory","key":"proportion-aware-constructive-seeding-leu-2-12-2","value":"Proportion-aware constructive seeding (leu.2/§12.2): sizing seed cuts from target AREAS only regresses (thin slivers wreck aspect); you must ALSO pick each cut's rotation for child squareness. It is a convergence ACCELERATOR via a deeper local optimum around the constructed topology: wins where that topology is roughly right and budget is scarce (harbor -13%, maple -10% at 20k evals) but DELAYS small programmes where the seed must be restructured by undivide (programme-house regresses at fixed budget, yet reaches the floor given budget - speed, not asymptote). Default-on. Also: n_storeys must honour storey_minimum, not just level: keys (programme-house storey_minimum:2, all rooms level:0 - was seeded 1 storey short; cq1)."}
|
||||||
|
{"_type":"memory","key":"adjacency-in-binary-slicing-tree-is-structural-not","value":"Adjacency in binary slicing tree is structural, not geometric: the inner-loop NM cannot fix topological adjacency failures. Two paths exist: (1) tree-sibling adjacency — a node is adjacent to its sibling in the tree; (2) cross-zone geometric adjacency — leaves from different subtrees that happen to share a boundary. Staircase/adjacency fails require a topology mutation that changes which nodes are siblings or which zones touch. This was proved empirically on programme-house: staircase fail from rot=0 layout could not be fixed by NM but was fixed by level_retype creating a two-C topology (2026-06-14/15)."}
|
||||||
|
{"_type":"memory","key":"correction-to-urb-fitness-bug-memory-bruno-2026","value":"CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase."}
|
||||||
|
{"_type":"memory","key":"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":"user-preference-bruno-this-is-a-fedora-system","value":"User preference (Bruno): this is a Fedora system — NEVER install Python packages via pip without asking first; always ask whether to install the rpm via dnf (e.g. python3-cma) before considering pip. Applies to any dependency additions."}
|
||||||
|
{"_type":"memory","key":"warm-x0-initialization-bug-pattern-when-a-topology","value":"warm_x0 initialization bug pattern: when a topology operator explicitly sets division ratios on a newly-created node (e.g. compound_fix sets node.division=[0.25,0.25] for t3), parent.ratios has no entry for that node (it was a leaf). warm_x0 defaults it to 0.5, corrupting the inner loop's starting point and making the operator invisible to lex comparison. Fix: only propagate child ratios for nodes where the parent node was NOT already divided; stale hidden nodes revealed by structural mutations (swap flipping b.below) must NOT contribute their pre-writeback values. See driver.py lines 259-267 (fixed 2026-06-14)."}
|
||||||
|
{"_type":"memory","key":"deceptive-valleys-in-topology-search-when-every-single","value":"Deceptive valleys in topology search: when every single-step mutation from a target state passes through a high-fail intermediary (e.g. level_fix displaces a room into 5+ new fails), a compound operator that atomically applies two coordinated changes can escape. Design compound operators to land on the low-fail state directly, bypassing the deceptive gradient. Programme-house example: level_compound_fix atomically moves the level-constrained room AND re-inserts the displaced room adjacent to C in one step (operators.py, 2026-06-14)."}
|
||||||
|
{"_type":"memory","key":"experiment-harness-gotcha-the-leaf-sharing-relaxed-objective","value":"Experiment harness gotcha: the leaf-sharing RELAXED objective (§13.3) is injected ONLY by monkeypatching fitness.load_config in the parent process (run_staged_search.py / probe scripts). This is parent-process-only and does NOT propagate into ProcessPoolExecutor workers (n_workers\u003e1), which re-import fitness fresh and score under the STRICT on-disk patterns.config -\u003e r.n_fails MISMATCH (worker strict vs parent relaxed re-score). ALL §13.x floor runs were therefore SERIAL. Any future PARALLEL leaf-sharing experiment will silently mis-score until leaf_sharing lives on disk/CLI (tracked: homemaker-py-x3b). The parallel driver itself is correct; both paths score via load_config(programme_dir)."}
|
||||||
|
{"_type":"memory","key":"homemaker-py-pythonpath-set-pythonpath-home-bruno-src","value":"homemaker-layout PYTHONPATH: package installed as 'homemaker-layout' via pip install -e . so 'import homemaker_layout' works from anywhere without PYTHONPATH. For running tests use 'python -m pytest' from project root /home/bruno/src/homemaker-layout (pyproject.toml adds src/ automatically). Never try pip show homemaker — that's the old homemaker-addon conflict."}
|
||||||
|
{"_type":"memory","key":"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."}
|
||||||
|
|
|
||||||
51
DESIGN.md
51
DESIGN.md
|
|
@ -1936,3 +1936,54 @@ robust signal is the **composition collapse** (crinkliness 13→4, landlocked
|
||||||
harbor landlocked fails). Follow-up observation, not part of this verdict:
|
harbor landlocked fails). Follow-up observation, not part of this verdict:
|
||||||
edge-too-long is now the single largest harbor class (6) — a candidate seed for
|
edge-too-long is now the single largest harbor class (6) — a candidate seed for
|
||||||
any future floor work, distinct from the crinkliness regime Phase-8 addressed.
|
any future floor work, distinct from the crinkliness regime Phase-8 addressed.
|
||||||
|
|
||||||
|
### 13.8 Experiment: share-aware edge-too-long cap (`homemaker-py-hph`) — DONE (positive, harmless)
|
||||||
|
|
||||||
|
§13.7's follow-up observation (edge-too-long = harbor's top class, 6 fails) is the
|
||||||
|
seed. **Dissection first** (`experiments/diag_edge_too_long.py` on the 500k probe
|
||||||
|
best): the 6 fails are only **2 distinct locations**. (1) DOMINANT ~4/6: leaf
|
||||||
|
`lllr` is a `share=3` leaf — one quad holding 3 rooms (247 m², edges 15–17 m,
|
||||||
|
aspect 1.2, NEARLY SQUARE). Its walls exceed the flat 8 m cap purely because it
|
||||||
|
*aggregates 3 rooms* — a leaf-sharing REPRESENTATION ARTIFACT, not a design flaw.
|
||||||
|
§13.3 relaxed size/missing for shared leaves (`quality_size` centres on k×target)
|
||||||
|
but `edge_cost` (fitness.py) and `outside_edge_cost` still used a flat 8 m
|
||||||
|
regardless of `leaf.share` — the same §13.3 leak on a different measure. (2) ~2/6:
|
||||||
|
leaf `llll`, a 1.2 m × 16.7 m sliver (aspect 14) — a REAL narrow-room pathology,
|
||||||
|
already independently caught by width/proportion; its edge-too-long is the wall it
|
||||||
|
shares with `lllr`. No corridors involved.
|
||||||
|
|
||||||
|
**Fix.** New `Fitness._edge_cap(*leaves)` scales the 8 m cap by the largest
|
||||||
|
type-guarded `leaf_share` (graph.leaf_share, §13.3's helper) among the adjoining
|
||||||
|
leaves, mirroring `quality_size`'s k×target; non-shared leaves keep the flat cap.
|
||||||
|
Used by both `edge_cost` (interior wall, max share of the two leaves) and
|
||||||
|
`outside_edge_cost` (one leaf). Gated behind a new `share_edge_cap` config knob
|
||||||
|
(`SHAREEDGE` env), default OFF, so the §13.x controls reproduce. On the probe best
|
||||||
|
the lever clears all 6 edge-too-long (20→14 total fails); the `llll` sliver stays
|
||||||
|
flagged via width/proportion.
|
||||||
|
|
||||||
|
**Setup** (`experiments/run_shareedge_ab.sh`, full Phase-8 default stack
|
||||||
|
LEAFSHARE=1/fac3 + DEPTHBAL=1 + INTERIORO=1/odiv3, staged, 20 000 native evals,
|
||||||
|
seeds 0/1/2, final native re-score). Control SHAREEDGE=0 (flat cap) — must
|
||||||
|
reproduce §13.6/§13.7; experiment SHAREEDGE=1.
|
||||||
|
|
||||||
|
| programme | flat cap off (s0/1/2) | mean | share-aware on (s0/1/2) | mean | Δ |
|
||||||
|
|-----------|-----------------------|-----:|-------------------------|-----:|------:|
|
||||||
|
| maple-court | 74 / 78 / 89 | 80.3 | 73 / 78 / 71 | 74.0 | −7.9 % |
|
||||||
|
| harbor-house | 28 / 41 / 35 | 34.7 | 27 / 39 / 27 | 31.0 | −10.6 % |
|
||||||
|
|
||||||
|
The control reproduces §13.7 (maple 80.3 *exactly*, harbor 34.7 ≈ 34.0), so the
|
||||||
|
gap is the lever, not drift.
|
||||||
|
|
||||||
|
**VERDICT — positive and HARMLESS; recommend default-ON.** Both programmes improve
|
||||||
|
on the mean with **zero regressions across all 6 seeds**: harbor every seed
|
||||||
|
(−1/−2/−8), maple two flat/down + one −18 (seed2). The asymmetry of magnitude
|
||||||
|
(maple's big seed2 swing) is search noise, but the *direction* is structural: the
|
||||||
|
lever only ever *removes* a false-positive fail on an aggregate shared leaf — it
|
||||||
|
cannot add one (non-shared leaves are untouched), so it is monotone-harmless on the
|
||||||
|
objective. This is unlike the §13.4-family construction levers that trade leaves
|
||||||
|
for fails; there is no tax to wash out. Recommendation: flip `share_edge_cap`
|
||||||
|
default-ON for leaf-sharing runs (it is the §13.3 relaxation completed on the wall
|
||||||
|
measure), mirroring the `pll`/`interior_outside` default flips. A follow-up issue
|
||||||
|
flips the default + rebaselines the §13.x floor numbers (harbor 34.7→31.0,
|
||||||
|
maple 80.3→74.0 become the new full-stack baseline). Repro:
|
||||||
|
`experiments/diag_edge_too_long.py`, `experiments/run_shareedge_ab.sh`.
|
||||||
|
|
|
||||||
49
experiments/run_shareedge_ab.sh
Executable file
49
experiments/run_shareedge_ab.sh
Executable file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Share-aware edge-too-long cap A/B (hph, DESIGN.md §13.7 follow-up). §13.7 named
|
||||||
|
# edge-too-long harbor's top fail class; diag_edge_too_long.py showed the bulk are
|
||||||
|
# a leaf-sharing REPRESENTATION ARTIFACT — a share=k leaf aggregates k same-code
|
||||||
|
# rooms, so its walls run ~k× the flat 8 m cap purely for being big. That is the
|
||||||
|
# same leak §13.3 closed for quality_size (k×target centring) on a different
|
||||||
|
# measure: edge_cost / outside_edge_cost still used a flat 8 m regardless of
|
||||||
|
# leaf.share. The fix scales the cap by the (type-guarded) share of the adjoining
|
||||||
|
# leaf/leaves, mirroring k×target; non-shared leaves keep the flat cap so genuine
|
||||||
|
# narrow/oversize pathologies stay flagged.
|
||||||
|
#
|
||||||
|
# Both arms hold the full Phase-8 default stack (leaf-share factor 3, depth-bal,
|
||||||
|
# interior-O odiv=3, circ_divisor 3, proportion-aware). Control is SHAREEDGE=0
|
||||||
|
# (flat cap) — must reproduce §13.6/§13.7 (maple 80.3, harbor 34.0); experiment
|
||||||
|
# adds SHAREEDGE=1 (share-aware cap). Seeds 0/1/2, two programmes, 20000 native
|
||||||
|
# evals, staged, final native re-score.
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
BUDGET="${1:-20000}"
|
||||||
|
OUT=scratch/shareedge_ab; mkdir -p "$OUT"
|
||||||
|
TSV=scratch/shareedge_results.tsv
|
||||||
|
[ -f "$TSV" ] || printf 'programme\tseed\tshareedge\tfails\ttopologies\telapsed_s\n' > "$TSV"
|
||||||
|
|
||||||
|
run() { # programme seed shareedge(0|1)
|
||||||
|
local prog="$1" seed="$2" se="$3"
|
||||||
|
local tag="se${se}"
|
||||||
|
local log="$OUT/${prog}_${tag}_s${seed}.log"
|
||||||
|
echo ">>> $prog seed=$seed shareedge=$se"
|
||||||
|
local t0; t0=$(date +%s)
|
||||||
|
env URB_NO_OCCLUSION=1 LEAFSHARE=1 LEAFSHAREFAC=3 DEPTHBAL=1 \
|
||||||
|
INTERIORO=1 ODIV=3 SHAREEDGE="$se" \
|
||||||
|
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\n' "$prog" "$seed" "$se" "${fails:-ERR}" "${topos:-?}" "$((t1-t0))" >> "$TSV"
|
||||||
|
echo " -> ${fails:-ERR} fails, ${topos:-?} topologies, $((t1-t0))s"
|
||||||
|
}
|
||||||
|
|
||||||
|
# flat-cap control (reproduce §13.6/§13.7) then the share-aware 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 "=== share-aware edge cap A/B complete ==="
|
||||||
|
column -t -s $'\t' "$TSV"
|
||||||
|
|
@ -64,6 +64,7 @@ def main() -> int:
|
||||||
circ_div = int(os.environ.get("CIRCDIV", "3")) # c3g circ-per-room granularity knob
|
circ_div = int(os.environ.get("CIRCDIV", "3")) # c3g circ-per-room granularity knob
|
||||||
leaf_share = os.environ.get("LEAFSHARE", "0") == "1" # erc.3 leaf-sharing A/B
|
leaf_share = os.environ.get("LEAFSHARE", "0") == "1" # erc.3 leaf-sharing A/B
|
||||||
leaf_share_fac = int(os.environ.get("LEAFSHAREFAC", "2"))
|
leaf_share_fac = int(os.environ.get("LEAFSHAREFAC", "2"))
|
||||||
|
share_edge = os.environ.get("SHAREEDGE", "0") == "1" # hph §13.7 share-aware edge cap A/B
|
||||||
depth_bal = os.environ.get("DEPTHBAL", "0") == "1" # erc.4 depth-balanced grow A/B
|
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
|
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
|
out_div = int(os.environ.get("ODIV", "6")) # ld2 outside-leaf-per-room divisor
|
||||||
|
|
@ -79,6 +80,8 @@ def main() -> int:
|
||||||
conf, cost = _orig_load(directory)
|
conf, cost = _orig_load(directory)
|
||||||
conf = dict(conf)
|
conf = dict(conf)
|
||||||
conf["leaf_sharing"] = True
|
conf["leaf_sharing"] = True
|
||||||
|
if share_edge: # hph §13.7: share-aware edge-too-long cap
|
||||||
|
conf["share_edge_cap"] = True
|
||||||
return conf, cost
|
return conf, cost
|
||||||
|
|
||||||
fitness.load_config = _load_with_sharing
|
fitness.load_config = _load_with_sharing
|
||||||
|
|
@ -96,6 +99,7 @@ def main() -> int:
|
||||||
print(f"feas_filt : {feas} (max_shape={max_shape})")
|
print(f"feas_filt : {feas} (max_shape={max_shape})")
|
||||||
print(f"circ_div : {circ_div}")
|
print(f"circ_div : {circ_div}")
|
||||||
print(f"leaf_share: {leaf_share} (factor={leaf_share_fac})")
|
print(f"leaf_share: {leaf_share} (factor={leaf_share_fac})")
|
||||||
|
print(f"share_edge: {share_edge}")
|
||||||
print(f"interior_o: {interior_o} (odiv={out_div})")
|
print(f"interior_o: {interior_o} (odiv={out_div})")
|
||||||
print(flush=True)
|
print(flush=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,10 @@ class Fitness:
|
||||||
# to k×target counts as k same-code rooms (count check + size centring).
|
# to k×target counts as k same-code rooms (count check + size centring).
|
||||||
self._leaf_sharing = bool(self.conf("leaf_sharing"))
|
self._leaf_sharing = bool(self.conf("leaf_sharing"))
|
||||||
self._max_share = int(self.conf("leaf_share_max") or 4)
|
self._max_share = int(self.conf("leaf_share_max") or 4)
|
||||||
|
# erc.hph §13.7: scale the edge-too-long cap by a shared leaf's share k so
|
||||||
|
# an aggregate (k-room) leaf is not penalised for long walls — the §13.3
|
||||||
|
# leak on a different measure. A/B knob, default OFF until the verdict.
|
||||||
|
self._share_edge_cap = bool(self.conf("share_edge_cap"))
|
||||||
|
|
||||||
def conf(self, key: str):
|
def conf(self, key: str):
|
||||||
v = self._conf.get(key)
|
v = self._conf.get(key)
|
||||||
|
|
@ -459,6 +463,23 @@ class Fitness:
|
||||||
rate = self.cost("inside")
|
rate = self.cost("inside")
|
||||||
return rate * geometry.area(leaf)
|
return rate * geometry.area(leaf)
|
||||||
|
|
||||||
|
def _edge_cap(self, *leaves: Node) -> float:
|
||||||
|
"""Wall-length cap before 'edge too long' fires (erc.hph/§13.7).
|
||||||
|
|
||||||
|
Default flat 8 m, as Urb. A shared leaf (share=k, type-guarded) holds k
|
||||||
|
same-code rooms, so its walls run ~k× longer purely as a leaf-sharing
|
||||||
|
representation artifact — the same leak §13.3 closed for size. Scale the
|
||||||
|
cap by the largest share among the adjoining leaves, mirroring
|
||||||
|
quality_size's k×target. Non-shared leaves keep the flat cap, so genuine
|
||||||
|
narrow/oversize pathologies stay flagged."""
|
||||||
|
cap = 8.0
|
||||||
|
if self._leaf_sharing and self._share_edge_cap:
|
||||||
|
from . import graph as _graph
|
||||||
|
k = max(_graph.leaf_share(leaf, self._max_share) for leaf in leaves)
|
||||||
|
if k > 1:
|
||||||
|
cap *= k
|
||||||
|
return cap
|
||||||
|
|
||||||
def edge_cost(self, G: nx.Graph, a: Node, b: Node, fail) -> float:
|
def edge_cost(self, G: nx.Graph, a: Node, b: Node, fail) -> float:
|
||||||
"""Interior/exterior wall cost for one graph edge
|
"""Interior/exterior wall cost for one graph edge
|
||||||
(``Storey.pm::calculate_edge_cost``)."""
|
(``Storey.pm::calculate_edge_cost``)."""
|
||||||
|
|
@ -471,7 +492,7 @@ class Fitness:
|
||||||
else:
|
else:
|
||||||
rate = self.cost("exterior_wall")
|
rate = self.cost("exterior_wall")
|
||||||
width = G[a][b]["width"]
|
width = G[a][b]["width"]
|
||||||
if width > 8.0 and rate > 0.0:
|
if width > self._edge_cap(a, b) and rate > 0.0:
|
||||||
fail(f"{dom_mod.level_of(a)}/{a.id} {b.id} edge too long")
|
fail(f"{dom_mod.level_of(a)}/{a.id} {b.id} edge too long")
|
||||||
return rate * width * height
|
return rate * width * height
|
||||||
|
|
||||||
|
|
@ -479,6 +500,7 @@ class Fitness:
|
||||||
"""Plot-boundary cost for a leaf's external edges
|
"""Plot-boundary cost for a leaf's external edges
|
||||||
(``Leaf.pm::calculate_outside_edge_cost``)."""
|
(``Leaf.pm::calculate_outside_edge_cost``)."""
|
||||||
rate = self.cost("boundary") if dom_mod.is_outside(leaf) else self.cost("boundary_wall")
|
rate = self.cost("boundary") if dom_mod.is_outside(leaf) else self.cost("boundary_wall")
|
||||||
|
cap = self._edge_cap(leaf)
|
||||||
length = 0.0
|
length = 0.0
|
||||||
for e in range(4):
|
for e in range(4):
|
||||||
if geometry.boundary_id(leaf, e) not in geometry._EXTERNAL:
|
if geometry.boundary_id(leaf, e) not in geometry._EXTERNAL:
|
||||||
|
|
@ -487,7 +509,7 @@ class Fitness:
|
||||||
length += edge_len
|
length += edge_len
|
||||||
if dom_mod.is_outside(leaf):
|
if dom_mod.is_outside(leaf):
|
||||||
continue
|
continue
|
||||||
if edge_len > 8.0:
|
if edge_len > cap:
|
||||||
fail(f"{dom_mod.level_of(leaf)}/{leaf.id} outside edge too long")
|
fail(f"{dom_mod.level_of(leaf)}/{leaf.id} outside edge too long")
|
||||||
return rate * length * _height(leaf)
|
return rate * length * _height(leaf)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,56 @@ def test_leaf_cost_inside():
|
||||||
assert Fitness().leaf_cost(leaf) == pytest.approx(COST_DEFAULTS["inside"] * 16.0)
|
assert Fitness().leaf_cost(leaf) == pytest.approx(COST_DEFAULTS["inside"] * 16.0)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Share-aware edge-too-long cap (hph §13.7)
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def _shared_leaf(type_: str = "k1", k: int = 3) -> Node:
|
||||||
|
leaf = _leaf(type_)
|
||||||
|
leaf.share = k
|
||||||
|
leaf.share_type = type_
|
||||||
|
return leaf
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_cap_flat_by_default():
|
||||||
|
# no leaf_sharing → flat 8 m regardless of any share stamp
|
||||||
|
fit = Fitness()
|
||||||
|
assert fit._edge_cap(_shared_leaf(k=3)) == pytest.approx(8.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_cap_flat_when_lever_off_even_with_sharing():
|
||||||
|
# leaf_sharing on but the hph lever off → still flat (control arm)
|
||||||
|
fit = Fitness(conf={"leaf_sharing": True})
|
||||||
|
assert fit._edge_cap(_shared_leaf(k=3)) == pytest.approx(8.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_cap_scales_by_share_when_lever_on():
|
||||||
|
fit = Fitness(conf={"leaf_sharing": True, "share_edge_cap": True})
|
||||||
|
assert fit._edge_cap(_shared_leaf(k=3)) == pytest.approx(24.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_cap_unshared_leaf_keeps_flat_cap():
|
||||||
|
# a non-shared leaf (the narrow-sliver pathology) is never relaxed
|
||||||
|
fit = Fitness(conf={"leaf_sharing": True, "share_edge_cap": True})
|
||||||
|
assert fit._edge_cap(_leaf("k1")) == pytest.approx(8.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_cap_stale_share_type_ignored():
|
||||||
|
# retyped leaf whose stamp no longer matches type → share invalid → flat
|
||||||
|
fit = Fitness(conf={"leaf_sharing": True, "share_edge_cap": True})
|
||||||
|
leaf = _shared_leaf("k1", k=3)
|
||||||
|
leaf.type = "b1" # retyped; share_type still "k1"
|
||||||
|
assert fit._edge_cap(leaf) == pytest.approx(8.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_cap_uses_largest_share_among_adjoining_leaves():
|
||||||
|
# an interior wall takes the max share of the two leaves it separates
|
||||||
|
fit = Fitness(conf={"leaf_sharing": True, "share_edge_cap": True})
|
||||||
|
cap = fit._edge_cap(_leaf("k1"), _shared_leaf("b1", k=2))
|
||||||
|
assert cap == pytest.approx(16.0)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Stair helpers
|
# Stair helpers
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue