x3b/§13.10: productionise leaf-sharing — per-code share grain + CLI wiring

Make the §13.3 lever a first-class feature, not experiment-only.

- programme.py: SpaceReq.share (default 1) + has_share, parsed from
  patterns.config 'share: N'.
- operators._share_grain: resolve per-code grain from leaf_share_factor
  selector — 0 = per-code opt-in (share iff share:N>=2), >=2 = global with
  per-code override (share:1 opts OUT, share:N sets grain). _share_rooms
  groups per resolved grain.
- End-to-end conf injection without monkeypatch: load_config(overrides=)
  merges run-level keys last; driver.search / innerloop.optimise /
  NativeEvaluator / _fitness_for thread conf_overrides={leaf_sharing:True}
  through both inner-loop and off-tree scorers when sharing is on.
- homemaker-evolve: --leaf-sharing/--no-leaf-sharing + --leaf-share-factor
  (env HOMEMAKER_LEAF_SHARING / HOMEMAKER_LEAF_SHARE_FACTOR).
- Example programmes untouched (§13.3/§13.9 stay reproducible). Experiment
  load_config monkeypatches updated to accept overrides=.

Tests: grain modes, opt-out, default-OFF parity, load_config overrides,
programme parse, CLI parse. 233 pass. Smoke: harbor 37 vs 95 fails on/off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bruno Postle 2026-06-28 22:04:35 +01:00
parent 923329da85
commit bb9b355f14
16 changed files with 308 additions and 49 deletions

View file

@ -28,7 +28,7 @@
{"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-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}
{"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.","notes":"DONE (§13.10). Per-code SpaceReq.share + has_share (programme.py). operators._share_grain resolves grain from leaf_share_factor selector: 0=per-code opt-in (share iff share:N\u003e=2), \u003e=2=global with per-code override (share:1 opts OUT, share:N sets grain). End-to-end conf injection productionised (no monkeypatch): load_config(overrides=) merged last; driver.search/innerloop.optimise/NativeEvaluator/_fitness_for thread conf_overrides={leaf_sharing:True}. CLI: homemaker-evolve --leaf-sharing/--no-leaf-sharing + --leaf-share-factor. Example programmes untouched (13.3/13.9 reproducible). Tests added: grain modes, opt-out, default-OFF parity, load_config overrides, programme parse, CLI parse. 233 pass. Smoke: harbor 37 vs 95 fails. Experiment monkeypatches updated to accept overrides=.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-24T21:03:05Z","created_by":"Bruno Postle","updated_at":"2026-06-28T21:02:18Z","started_at":"2026-06-24T21:03:45Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"homemaker-py-erc.7","title":"Leaf-sharing × erc.4 depth-balancing synergy + factor/max_share sweep","description":"With the missing-fail leak closed by explicit multiplicity (§13.3), revisit the erc.3↔erc.4 synergy the diagnostics predicted: depth-balanced construction lands shared leaves at their correct absolute k×target area, which should further cut size+crinkliness. Also sweep leaf_share_factor (3 won here; try 2/4) and leaf_share_max (default 4) on maple+harbor, seeds 0/1/2, staged 20k, vs the §13.3 factor-3 result (maple 86.3, harbor 50.3).","notes":"FACTOR SWEEP DONE (§13.5): factor 3 confirmed default under bal+share. maple f2=92.7 f3=82.3 f4=83.3; harbor f2=53.0 f3=40.0 f4=39.7. Factor 2 regresses both; f3/f4 tied within noise (f3 wins maple +1.0, f4 wins harbor +0.3). leaf_share_max=4 covers factor\u003c=4, no missing-fail leak (re-score OK all runs). erc.7 complete.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-24T20:51:43Z","created_by":"Bruno Postle","updated_at":"2026-06-27T09:55:56Z","started_at":"2026-06-26T07:39:54Z","closed_at":"2026-06-27T09:55:56Z","close_reason":"Closed","dependencies":[{"issue_id":"homemaker-py-erc.7","depends_on_id":"homemaker-py-erc","type":"parent-child","created_at":"2026-06-24T21:51:42Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"homemaker-py-dyh","title":"Productionise leaf-sharing: evolve CLI flag + patterns.config key","description":"erc.3 (§13.3) proved leaf-sharing lowers the floor 37% maple / 32% harbor end-to-end, but the flag is only reachable via the LEAFSHARE env in run_staged_search.py. For real runs: (1) expose --leaf-sharing / --leaf-share-factor on homemaker-evolve (evolve.py), threading to driver.search/search_staged (already plumbed); (2) optionally read a leaf_sharing key from patterns.config so the fitness + construction stay consistent without env injection (fitness already reads conf; construction would read it in evolve). Consider whether to default it ON given the decisive win. Also: the genome.signature ignores leaf.share, so a shared vs unshared leaf of the same type/structure collide — assess if niching needs share in the signature.","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-24T20:51:41Z","created_by":"Bruno Postle","updated_at":"2026-06-24T21:03:47Z","closed_at":"2026-06-24T21:03:47Z","close_reason":"Superseded by x3b (per-code shareable flag + CLI wiring), which is the concrete implementation of dyh's 'CLI flag + patterns.config key' scope with the per-code opt-in design.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"homemaker-py-7u5","title":"Thread parent failure strings onto Individual","description":"Store the sorted .fails tuple on driver.Individual so operators can read which constraints the parent violates. The score is already recomputed per child (driver.py:146 want_grade path / innerloop result); capture score_with_fails output instead of discarding the strings. Near-zero cost. Prereq for the repair operator (homemaker-py-71d).","notes":"Also feeds erc.1 (per-leaf shape-fail vs density/granularity profile): storing the sorted .fails on Individual makes per-leaf fail attribution available to the diagnostic without re-scoring. Cheap, generically useful — promote ahead of the Tier-3 operator work if erc.1 is picked up first.","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-23T20:40:17Z","created_by":"Bruno Postle","updated_at":"2026-06-28T13:21:50Z","closed_at":"2026-06-28T13:21:50Z","close_reason":"Superseded by construction (DESIGN §13.7): interior-O (default-ON, erc.8) is 71d's named fix (interior O courtyards) and collapsed landlocked crinkliness ~13-\u003e2 of 20 in the high-budget probe. Residual now diffuse, no concentrated ratio-invariant block for a targeted repair operator. Reopen/refile if a future floor probe shows a concentrated ratio-invariant class return.","dependencies":[{"issue_id":"homemaker-py-7u5","depends_on_id":"homemaker-py-71d","type":"parent-child","created_at":"2026-06-23T21:49:52Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
@ -70,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.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}
{"_type":"memory","key":"adjacency-in-binary-slicing-tree-is-structural-not","value":"Adjacency in binary slicing tree is structural, not geometric: the inner-loop NM cannot fix topological adjacency failures. Two paths exist: (1) tree-sibling adjacency — a node is adjacent to its sibling in the tree; (2) cross-zone geometric adjacency — leaves from different subtrees that happen to share a boundary. Staircase/adjacency fails require a topology mutation that changes which nodes are siblings or which zones touch. This was proved empirically on programme-house: staircase fail from rot=0 layout could not be fixed by NM but was fixed by level_retype creating a two-C topology (2026-06-14/15)."}
{"_type":"memory","key":"ld2-13-6-interior-o-seed-diagnostic-all","value":"ld2/§13.6 interior-O seed diagnostic: ALL crinkliness fails in the constructed bal+share seed are UNDER-exposed (crink\u003c0.62, landlocked rooms with no facade + no uncovered-O neighbour) — zero over-exposed sliver fails. So the erc crinkliness residual is genuine under-daylighting, validating the interior light-well premise. Default outside_divisor=6 was too sparse (null: harbor 147-\u003e142, crinkliness even rose). odiv=3 is the seed-optimal joint setting: harbor seed fails 147-\u003e129 (-18), maple 219-\u003e206 (-14), landlocked fails drop, at cost of more leaves (harbor +4, maple +8). Because it ADDS leaves it carries the §13.4 wash-out risk; A/B to convergence pending."}
{"_type":"memory","key":"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":"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":"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":"deceptive-valleys-in-topology-search-when-every-single","value":"Deceptive valleys in topology search: when every single-step mutation from a target state passes through a high-fail intermediary (e.g. level_fix displaces a room into 5+ new fails), a compound operator that atomically applies two coordinated changes can escape. Design compound operators to land on the low-fail state directly, bypassing the deceptive gradient. Programme-house example: level_compound_fix atomically moves the level-constrained room AND re-inserts the displaced room adjacent to C in one step (operators.py, 2026-06-14)."}
{"_type":"memory","key":"multi-storey-staircase-consistency-when-dividing-or-retyping","value":"Multi-storey staircase consistency: when dividing or retyping a circulation (C) leaf at one level, the same structural change should be propagated to the matching leaf on ALL other storeys so the stair core path is maintained. The optimizer cannot fix staircase disruptions through trial-and-error geometry alone — it requires a synchronized multi-level operator that applies the same topology change to every storey simultaneously."}
{"_type":"memory","key":"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":"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":"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":"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":"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":"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-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":"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":"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":"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":"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":"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":"correction-to-urb-fitness-bug-memory-bruno-2026","value":"CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase."}
{"_type":"memory","key":"experiment-harness-gotcha-the-leaf-sharing-relaxed-objective","value":"Experiment harness gotcha: the leaf-sharing RELAXED objective (§13.3) is injected ONLY by monkeypatching fitness.load_config in the parent process (run_staged_search.py / probe scripts). This is parent-process-only and does NOT propagate into ProcessPoolExecutor workers (n_workers\u003e1), which re-import fitness fresh and score under the STRICT on-disk patterns.config -\u003e r.n_fails MISMATCH (worker strict vs parent relaxed re-score). ALL §13.x floor runs were therefore SERIAL. Any future PARALLEL leaf-sharing experiment will silently mis-score until leaf_sharing lives on disk/CLI (tracked: homemaker-py-x3b). The parallel driver itself is correct; both paths score via load_config(programme_dir)."}
{"_type":"memory","key":"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."}

View file

@ -2005,3 +2005,43 @@ seeds 0/1/2): **maple-court 80.3 → 74.0, harbor-house 34.7 → 31.0** — the
share-aware arm from §13.8 becomes the baseline. `test_edge_cap_flat_when_lever_off_even_with_sharing`
now pins `share_edge_cap=False`; `test_edge_cap_defaults_on_under_leaf_sharing`
guards the flip. 222 tests pass.
### 13.10 Productionise leaf-sharing: per-code `share` + CLI wiring (`homemaker-py-x3b`) — DONE
Make the §13.3 lever a first-class, programme-author-controllable feature instead
of an experiment-only env var + monkeypatch. Three pieces:
**1. Per-code grain (`SpaceReq.share`).** `patterns.config` spaces accept an
optional `share: N``SpaceReq.share` (int, default 1 = not shareable; a
`has_share` flag distinguishes an explicit `share: 1` from the default).
`operators._share_grain(req, leaf_share_factor)` resolves each code's grain from
the global selector:
- `leaf_share_factor == 0`**per-code opt-in**: a code shares iff it sets
`share: N≥2`; this is the safe default-on philosophy (sharing off unless the
author asks, per space).
- `leaf_share_factor ≥ 2`**global mode**: every sized code shares at the
factor, with an explicit `share` overriding (`share: 1` opts a code OUT,
`share: N` sets that code's grain to N). Reproduces the §13.3 experiment with
**no edits to example programmes** (so §13.3/§13.9 baselines stay reproducible).
Only sized codes are ever shareable (an unsized c/o/s absorbs slack — no target
to centre `k` rooms on). `_share_rooms` now groups per resolved grain.
**2. End-to-end conf injection.** The §13.3 scoring sites gate on a `leaf_sharing`
conf key, but example `patterns.config` files don't set it — the experiment
harness monkeypatched `fitness.load_config` to inject it. Productionised cleanly:
`load_config(dir, overrides=None)` merges run-level keys last, and
`driver.search` / `innerloop.optimise` / `NativeEvaluator` / `_fitness_for` thread
`conf_overrides={"leaf_sharing": True}` through both the inner-loop scorer and the
off-tree grade/feasibility scorer when sharing is on. So the whole pipeline scores
under the relaxed objective the shared seed targets, with no monkeypatch and no
on-disk edits. (`share_edge_cap`'s §13.9 default-ON-under-sharing derivation in
`Fitness.__init__` rides along automatically.)
**3. CLI.** `homemaker-evolve` gains `--leaf-sharing/--no-leaf-sharing` (default
ON, `HOMEMAKER_LEAF_SHARING`) and `--leaf-share-factor N` (default 3,
`HOMEMAKER_LEAF_SHARE_FACTOR`), threaded to `driver.search`.
Default-OFF parity holds: `overrides=None` leaves `load_config` byte-identical and
`_share_rooms` is never reached. Smoke-checked end-to-end on harbor-house (sharing
on 37 fails vs `--no-leaf-sharing` 95 at budget 160). 233 tests pass.

View file

@ -82,8 +82,8 @@ class _force_sharing:
def __enter__(self):
self._orig = fitness.load_config
if self.on:
def patched(directory, _orig=self._orig):
conf, cost = _orig(directory)
def patched(directory, overrides=None, _orig=self._orig):
conf, cost = _orig(directory, overrides=overrides)
conf = dict(conf)
conf["leaf_sharing"] = True
return conf, cost

View file

@ -26,8 +26,9 @@ BEST = REPO / "scratch" / "harbor_floor_probe" / "harbor_fullstack_s0.dom"
# same relaxed-config injection the probe used, so fails match
_orig = fitness.load_config
def _load(d):
c, k = _orig(d); c = dict(c); c["leaf_sharing"] = True; c["max_share"] = 3
def _load(d, overrides=None):
c, k = _orig(d, overrides=overrides); c = dict(c)
c["leaf_sharing"] = True; c["max_share"] = 3
return c, k
fitness.load_config = _load
conf, cost = fitness.load_config(HARBOR)

View file

@ -50,8 +50,8 @@ class _force_sharing:
def __enter__(self):
self._orig = fitness.load_config
if self.on:
def patched(directory):
conf, cost = self._orig(directory)
def patched(directory, overrides=None):
conf, cost = self._orig(directory, overrides=overrides)
conf = dict(conf)
conf["leaf_sharing"] = True
return conf, cost

View file

@ -104,8 +104,8 @@ def main() -> int:
# how every §13.x floor number was produced.
_orig_load = fitness.load_config
def _load_with_sharing(directory):
conf, cost = _orig_load(directory)
def _load_with_sharing(directory, overrides=None):
conf, cost = _orig_load(directory, overrides=overrides)
conf = dict(conf)
conf["leaf_sharing"] = True
conf["max_share"] = 3

View file

@ -76,8 +76,10 @@ def main() -> int:
# constructed shared leaves target. Keeps both A/B arms on one dir.
_orig_load = fitness.load_config
def _load_with_sharing(directory):
conf, cost = _orig_load(directory)
def _load_with_sharing(directory, overrides=None):
# x3b: driver/innerloop now pass overrides={"leaf_sharing": True}; honour
# the kwarg, then pin the experiment's extra A/B knob on top.
conf, cost = _orig_load(directory, overrides=overrides)
conf = dict(conf)
conf["leaf_sharing"] = True
# hph §13.8: share-aware edge-too-long cap now defaults ON under

View file

@ -40,14 +40,19 @@ _CHILD_INNER_KW: dict = {}
@functools.lru_cache(maxsize=None)
def _fitness_for(programme_dir: str) -> "fitness.Fitness":
"""Cached Fitness evaluator per programme dir (config load is the cost).
def _fitness_for(programme_dir: str, leaf_sharing: bool = False) -> "fitness.Fitness":
"""Cached Fitness evaluator per (programme dir, leaf_sharing) (config load is
the cost).
Used only to read the graded proximity scalar (§11.4) off an already-
optimised tree in :func:`_evaluate`; the inner loop's own NativeEvaluator is
untouched. Cached per process workers fork their own copy.
Used only to read the graded proximity scalar (§11.4) and the shape-fail
feasibility proxy off an already-optimised tree in :func:`_evaluate`; the
inner loop's own NativeEvaluator is untouched. ``leaf_sharing`` (homemaker-py-
x3b) injects the run-level flag so this off-tree scorer agrees with the
inner loop instead of reading the on-disk (sharing-free) patterns.config.
Cached per process workers fork their own copy.
"""
conf, cost = fitness.load_config(programme_dir)
overrides = {"leaf_sharing": True} if leaf_sharing else None
conf, cost = fitness.load_config(programme_dir, overrides=overrides)
return fitness.Fitness(conf, cost)
@ -119,7 +124,8 @@ def random_topology(seed_root: dom.Node, n_leaves: int,
def _evaluate(root: dom.Node, programme_dir, urb_root, x0, budget, inner_kw,
lineage: str, want_grade: bool = False,
feasibility_max_shape_fails: int | None = None,
best_n_fails: int | None = None) -> tuple[Individual, int]:
best_n_fails: int | None = None,
leaf_sharing: bool = False) -> tuple[Individual, int]:
# §12.3 shape-feasibility pre-filter (homemaker-py-9gp.1): if even the best
# achievable (proportion-aware) geometry of this topology already has at least
# as many shape fails as the incumbent's TOTAL fails — and exceeds the tunable
@ -127,23 +133,25 @@ def _evaluate(root: dom.Node, programme_dir, urb_root, x0, budget, inner_kw,
# eval instead of spending the full inner-loop budget. The best_n_fails guard
# makes the proxy safe: a topology whose shape-fail floor is still below the
# incumbent is never discarded. Pruned individuals are tagged and never admitted.
overrides = {"leaf_sharing": True} if leaf_sharing else None
if (feasibility_max_shape_fails is not None and best_n_fails is not None):
pred = operators.predicted_shape_fails(
root, _reqs_for(str(programme_dir)), _fitness_for(str(programme_dir)))
root, _reqs_for(str(programme_dir)),
_fitness_for(str(programme_dir), leaf_sharing))
if pred > feasibility_max_shape_fails and pred >= best_n_fails:
ind = Individual(root=root, fitness=0.0, n_fails=pred, ratios={},
lineage=f"pruned/{lineage}", grade=0.0,
sig=genome.signature(root))
return ind, 1
r = innerloop.optimise(root, programme_dir, x0=x0, budget=budget,
urb_root=urb_root, **inner_kw)
urb_root=urb_root, conf_overrides=overrides, **inner_kw)
# §11.4: read the graded proximity scalar off the optimised tree. The inner
# loop left ``root`` at the optimum (Lamarckian write-back), so re-scoring a
# copy reproduces r.fitness/r.n_fails exactly and adds the grade. One extra
# native eval per child (~1/child_budget overhead); skipped unless requested.
grade = 0.0
if want_grade:
_, _, grade = _fitness_for(str(programme_dir)).score_with_grade(
_, _, grade = _fitness_for(str(programme_dir), leaf_sharing).score_with_grade(
copy.deepcopy(root))
ind = Individual(root=root, fitness=r.fitness, n_fails=r.n_fails,
ratios=innerloop.ratio_map(root), lineage=lineage,
@ -350,7 +358,8 @@ def search(
mx = feasibility_max_shape_fails if (filter_on and feasibility_filter) else None
best_nf = result.best.n_fails if result.best is not None else None
full = [
(root, programme_dir, urb_root, x0, budget_, kw_, lin, use_grade, mx, best_nf)
(root, programme_dir, urb_root, x0, budget_, kw_, lin, use_grade,
mx, best_nf, leaf_sharing)
for root, x0, budget_, kw_, lin in tasks
]
if _pool is not None:
@ -416,7 +425,8 @@ def search(
seed_ind, used = _evaluate(copy.deepcopy(seed_root), programme_dir, urb_root,
x0=None, budget=seed_budget,
inner_kw={}, lineage="seed",
want_grade=use_grade)
want_grade=use_grade,
leaf_sharing=leaf_sharing)
n_evals += used
admit(seed_ind, pop)

View file

@ -40,6 +40,13 @@ def _env_int(name: str, default: int) -> int:
return int(v) if v is not None else default
def _env_bool(name: str, default: bool) -> bool:
v = os.environ.get(name)
if v is None:
return default
return v.strip().lower() in ("1", "true", "yes", "on")
def _parse_args(argv=None) -> argparse.Namespace:
p = argparse.ArgumentParser(
prog="homemaker-evolve",
@ -65,6 +72,18 @@ def _parse_args(argv=None) -> argparse.Namespace:
p.add_argument("--seed", type=int,
default=_env_int("HOMEMAKER_SEED", 0),
metavar="N", help="RNG seed")
p.add_argument("--leaf-sharing", action=argparse.BooleanOptionalAction,
default=_env_bool("HOMEMAKER_LEAF_SHARING", True),
help="collapse same-code rooms into fewer, larger shared "
"leaves (erc.3, §13.3); --no-leaf-sharing for the strict "
"per-leaf baseline (default: on)")
p.add_argument("--leaf-share-factor", type=int,
default=_env_int("HOMEMAKER_LEAF_SHARE_FACTOR", 3),
metavar="N",
help="global sharing grain: 0 = per-code opt-in only (share a "
"code iff its programme entry sets 'share: N>=2'); N>=2 = "
"share every sized code at grain N, with a code's explicit "
"'share' overriding (share:1 opts out) (default: 3)")
p.add_argument("--output", type=Path, default=None, metavar="PATH",
help="output .dom path (- for stdout)")
return p.parse_args(argv)
@ -100,6 +119,8 @@ def main(argv=None) -> int:
print(f"child_budget : {args.child_budget}", file=sys.stderr)
print(f"workers : {args.workers}", file=sys.stderr)
print(f"rng seed : {args.seed}", file=sys.stderr)
print(f"leaf sharing : {args.leaf_sharing} (factor={args.leaf_share_factor})",
file=sys.stderr)
print(f"output : {out or 'stdout'}", file=sys.stderr, flush=True)
seed_root = dom.load(str(seed_file))
@ -117,6 +138,8 @@ def main(argv=None) -> int:
p_crossover=0.2,
seed=args.seed,
n_workers=args.workers,
leaf_sharing=args.leaf_sharing,
leaf_share_factor=args.leaf_share_factor,
log=lambda m: print(m, file=sys.stderr, flush=True),
)

View file

@ -122,10 +122,16 @@ def gaussian(x: float, a: float, b: float, c: float) -> float:
return a * (_E ** (0 - ((x - b) ** 2 / (2 * c * c))))
def load_config(directory: str | Path) -> tuple[dict, dict]:
def load_config(directory: str | Path,
overrides: dict | None = None) -> tuple[dict, dict]:
"""Load (patterns, costs) config for a corpus directory, mirroring
``urb-fitness.pl``: project-level ``../<name>.config`` first, then the
local file's keys override it."""
local file's keys override it.
``overrides`` (homemaker-py-x3b) are merged into the patterns conf last, so a
caller can switch on run-level knobs (e.g. ``leaf_sharing``) without editing
any ``patterns.config`` on disk keeping the §13.3 example programmes
reproducible while the CLI/driver drives sharing programmatically."""
directory = Path(directory)
conf: dict = {}
cost: dict = {}
@ -134,6 +140,8 @@ def load_config(directory: str | Path) -> tuple[dict, dict]:
if p.is_file():
with open(p) as fh:
target.update(yaml.safe_load(fh) or {})
if overrides:
conf.update(overrides)
return conf, cost

View file

@ -346,12 +346,13 @@ class NativeEvaluator:
scale).
"""
def __init__(self, root: dom.Node, programme_dir: str | Path):
def __init__(self, root: dom.Node, programme_dir: str | Path,
conf_overrides: dict | None = None):
from . import fitness as fit_mod
self.root = root
self.free = solver.free_branches(root)
conf, cost = fit_mod.load_config(programme_dir)
conf, cost = fit_mod.load_config(programme_dir, overrides=conf_overrides)
self._fit = fit_mod.Fitness(conf, cost)
self.n_evals = 0
self.n_oracle_calls = 0 # kept for interface parity with OracleEvaluator
@ -397,6 +398,7 @@ def optimise(
method: str = "nm",
use_native: bool = True,
urb_root: str | Path = oracle.DEFAULT_URB_ROOT,
conf_overrides: dict | None = None,
**search_kw,
) -> Result:
"""Optimise the free division ratios of ``root`` in place; return the best.
@ -409,7 +411,8 @@ def optimise(
fall back to the Perl oracle (kept for validation only).
"""
ev_cls = NativeEvaluator if use_native else OracleEvaluator
ev_args = (root, programme_dir) if use_native else (root, programme_dir, urb_root)
ev_args = ((root, programme_dir, conf_overrides) if use_native
else (root, programme_dir, urb_root))
with ev_cls(*ev_args) as ev:
if x0 is None:
x0 = ev.x_current

View file

@ -407,17 +407,40 @@ def _grow_leaves(lvl: dom.Node, n_leaves: int, rng: np.random.Generator,
leaf.type = None
def _share_grain(req, share_factor: int) -> int:
"""Per-code leaf-sharing grain (homemaker-py-x3b, §13.3).
Returns the maximum number of same-code rooms that may collapse into one
shared leaf, or 1 when the code must not be shared. Only sized codes are ever
shareable (an unsized circulation/outside code absorbs slack and has no target
to centre k rooms on). ``share_factor`` is the global selector:
- ``0`` per-code opt-in: a code is shared iff it carries an explicit
``share: N`` (>=2); everything else stays unshared. This is the safe
default-on philosophy the programme author chooses per space.
- ``>=2`` global mode: every sized code shares at grain ``share_factor``,
except codes with an explicit ``share`` which overrides it (``share: 1``
opts the code OUT, ``share: N`` sets that code's grain to N). This
reproduces the §13.3 experiment without editing example programmes.
"""
if req is None or not (req.has_size and req.size > 0):
return 1
if share_factor == 0:
return req.share if req.has_share else 1
return req.share if req.has_share else share_factor
def _share_rooms(rooms: list[str], reqs,
share_factor: int) -> tuple[list[str], dict[str, list[int]]]:
"""Collapse same-code room instances into fewer, larger shared leaves (erc.3).
Each sized, multi-instance code in ``rooms`` is grouped into runs of up to
``share_factor`` instances one leaf per run carrying that run's
Each shareable code in ``rooms`` (grain from :func:`_share_grain`) is grouped
into runs of up to its grain one leaf per run carrying that run's
multiplicity. Returns ``(reduced_codes, mult_plan)`` where ``reduced_codes``
is the new per-leaf code list (fewer entries) and ``mult_plan[code]`` lists
the multiplicities of that code's leaves (summing to the original count).
Circulation/outside and single-instance or non-sized codes are untouched
(multiplicity 1), so they cannot incur a missing fail under sharing.
Circulation/outside and single-instance, non-sized, or opted-out codes are
untouched (multiplicity 1), so they cannot incur a missing fail under sharing.
"""
from collections import Counter
@ -426,15 +449,13 @@ def _share_rooms(rooms: list[str], reqs,
plan: dict[str, list[int]] = {}
for code in counts:
c = counts[code]
req = reqs.get(code) if reqs else None
shareable = (req is not None and req.has_size and req.size > 0
and share_factor >= 2 and c >= 2)
if not shareable:
grain = _share_grain(reqs.get(code) if reqs else None, share_factor)
if grain < 2 or c < 2:
mults = [1] * c
else:
mults, remaining = [], c
while remaining > 0:
m = min(share_factor, remaining)
m = min(grain, remaining)
mults.append(m)
remaining -= m
plan[code] = mults

View file

@ -29,10 +29,16 @@ class SpaceReq:
level: int | None = None
requires_below: str | None = None
count: int = 1
# erc.3 §13.3 leaf-sharing grain (homemaker-py-x3b): how many rooms of this
# code may collapse into one shared leaf. Default 1 = not shareable. Under the
# global ``leaf_share_factor`` selector an explicit value overrides the global
# grain (share:1 opts a code OUT, share:N>=2 sets that code's grain to N).
share: int = 1
# Whether each quality param was explicitly in the config (not a default)
has_size: bool = False
has_width: bool = False
has_proportion: bool = False
has_share: bool = False
def _pair(d: dict, key: str, default: tuple[float, float]) -> tuple[float, float]:
@ -62,9 +68,11 @@ def _parse_spaces(conf: dict) -> dict[str, SpaceReq]:
level=c.get("level"),
requires_below=c.get("requires_below"),
count=int(c.get("count") or 1),
share=int(c.get("share") or 1),
has_size="size" in c,
has_width="width" in c,
has_proportion="proportion" in c,
has_share="share" in c,
)
return out

33
tests/test_evolve_cli.py Normal file
View file

@ -0,0 +1,33 @@
"""CLI argument wiring for homemaker-evolve (homemaker-py-x3b)."""
from pathlib import Path
from homemaker_layout.evolve import _parse_args
def test_leaf_sharing_defaults_on():
args = _parse_args(["seed.dom"])
assert args.leaf_sharing is True
assert args.leaf_share_factor == 3
assert args.seed_dom == Path("seed.dom")
def test_no_leaf_sharing_flag():
args = _parse_args(["seed.dom", "--no-leaf-sharing"])
assert args.leaf_sharing is False
def test_leaf_share_factor_opt_in_mode():
args = _parse_args(["seed.dom", "--leaf-share-factor", "0"])
assert args.leaf_share_factor == 0
assert args.leaf_sharing is True
def test_leaf_sharing_env_default(monkeypatch):
monkeypatch.setenv("HOMEMAKER_LEAF_SHARING", "0")
monkeypatch.setenv("HOMEMAKER_LEAF_SHARE_FACTOR", "5")
args = _parse_args(["seed.dom"])
assert args.leaf_sharing is False
assert args.leaf_share_factor == 5
# explicit flag still wins over the env default
assert _parse_args(["seed.dom", "--leaf-sharing"]).leaf_sharing is True

View file

@ -331,3 +331,47 @@ def test_leaf_grade_sums_over_failing_factors():
def test_leaf_grade_ignores_non_graded_keys():
# daylight is pinned and never a graded factor even if below threshold.
assert _leaf_grade({"daylight": 0.0}) == 0.0
# --------------------------------------------------------------------------- #
# load_config overrides (homemaker-py-x3b)
# --------------------------------------------------------------------------- #
def test_load_config_overrides_merge_last(tmp_path):
# The CLI/driver injects run-level knobs (leaf_sharing) without editing any
# on-disk patterns.config, so §13.3 example programmes stay reproducible.
import yaml
from homemaker_layout.fitness import load_config
(tmp_path / "patterns.config").write_text(
yaml.safe_dump({"spaces": {"b": {"size": [12.0, 1.0]}}}))
conf, _ = load_config(tmp_path)
assert "leaf_sharing" not in conf # absent on disk
conf2, _ = load_config(tmp_path, overrides={"leaf_sharing": True})
assert conf2["leaf_sharing"] is True
assert conf2["spaces"]["b"] == {"size": [12.0, 1.0]} # disk content preserved
# None / empty overrides are a no-op (default-OFF parity).
assert "leaf_sharing" not in load_config(tmp_path, overrides=None)[0]
assert "leaf_sharing" not in load_config(tmp_path, overrides={})[0]
def test_programme_parses_per_code_share(tmp_path):
# homemaker-py-x3b: SpaceReq carries the optional per-code 'share' grain and a
# has_share flag distinguishing an explicit share:1 (opt out) from the default.
import yaml
from homemaker_layout.programme import load_programme
p = tmp_path / "patterns.config"
p.write_text(yaml.safe_dump({"spaces": {
"b": {"size": [12.0, 1.0], "share": 3},
"k": {"size": [20.0, 1.0]}, # no share key
}}))
reqs = load_programme(str(p))
assert reqs["b"].share == 3 and reqs["b"].has_share is True
assert reqs["k"].share == 1 and reqs["k"].has_share is False

View file

@ -138,6 +138,72 @@ def test_leaf_share_explicit_and_type_guarded():
assert leaf_share(plain, 4) == 1
def _reqs(**share_kw):
"""Build a tiny programme: sized 'b' (share per kwarg), sized 'k', unsized 'C'."""
from homemaker_layout.programme import SpaceReq
b = SpaceReq(code="b", size=12.0, has_size=True, count=5)
if "b" in share_kw:
b.share, b.has_share = share_kw["b"], True
k = SpaceReq(code="k", size=20.0, has_size=True, count=4)
if "k" in share_kw:
k.share, k.has_share = share_kw["k"], True
c = SpaceReq(code="C", size=0.0, has_size=False, count=3) # unsized circulation
return {"b": b, "k": k, "C": c}
def _mults(plan_entry):
return sorted(plan_entry)
def test_share_grain_opt_in_mode():
# homemaker-py-x3b: factor 0 = per-code opt-in. A code shares iff it carries an
# explicit share:N>=2; sized codes without the key, and unsized codes, do not.
reqs = _reqs(b=3)
assert operators._share_grain(reqs["b"], 0) == 3 # explicit opt-in
assert operators._share_grain(reqs["k"], 0) == 1 # sized but no key → unshared
assert operators._share_grain(reqs["C"], 0) == 1 # unsized → never shareable
assert operators._share_grain(_reqs(b=1)["b"], 0) == 1 # share:1 stays unshared
def test_share_grain_global_mode_with_per_code_override():
# factor>=2 = global: every sized code shares at the factor unless its entry
# overrides — share:1 opts OUT, share:N sets that code's grain to N.
reqs = _reqs(b=1, k=4)
assert operators._share_grain(reqs["b"], 3) == 1 # explicit share:1 → opt out
assert operators._share_grain(reqs["k"], 3) == 4 # explicit share:4 → grain 4
assert operators._share_grain(_reqs()["k"], 3) == 3 # no key → global factor 3
assert operators._share_grain(_reqs()["C"], 3) == 1 # unsized → never shareable
def test_share_rooms_opt_in_groups_only_flagged_code():
# factor 0: only 'b' (share:3) collapses into runs of 3; 'k' and 'C' untouched.
rooms = ["b"] * 5 + ["k"] * 4 + ["C"] * 3
reduced, plan = operators._share_rooms(rooms, _reqs(b=3), 0)
assert _mults(plan["b"]) == [2, 3] # 5 rooms → runs of 3 + 2
assert plan["k"] == [1, 1, 1, 1] # no share key → unshared
assert plan["C"] == [1, 1, 1] # unsized → unshared
assert reduced.count("b") == 2 and reduced.count("k") == 4
def test_share_rooms_global_with_opt_out():
# factor 3 global: 'k' shares at 3 (no key), 'b' opted OUT via share:1.
rooms = ["b"] * 5 + ["k"] * 4
reduced, plan = operators._share_rooms(rooms, _reqs(b=1), 3)
assert plan["b"] == [1, 1, 1, 1, 1] # share:1 → opt out, stays 5 leaves
assert _mults(plan["k"]) == [1, 3] # 4 rooms → run of 3 + 1
# multiplicities always sum back to the original room counts (no rooms lost)
assert sum(plan["b"]) == 5 and sum(plan["k"]) == 4
def test_share_rooms_default_off_parity():
# Master switch off path: callers never invoke _share_rooms, but a single
# instance or grain<2 must yield the identity plan regardless of factor.
rooms = ["b", "k", "k", "C"]
reduced, plan = operators._share_rooms(rooms, _reqs(), 0) # opt-in, no keys
assert reduced == rooms and all(m == 1 for ms in plan.values() for m in ms)
@pytest.mark.skipif(not HARBOR.is_dir(), reason="harbor-house not available")
def test_leaf_sharing_reduces_leaves_and_covers_rooms():
# erc.3 §13.3: leaf_sharing builds fewer leaves, and coverage-counting lets