From f160c6dc9e7c2a8360802220037cec664b66659a Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Fri, 12 Jun 2026 19:01:53 +0100 Subject: [PATCH] Use Urb's canonical UPPERCASE generic types (C/O); case-insensitive class checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bruno's correction: 'C' was never a 'covered' type — Is_Covered is a geometric predicate. Urb generics are canonically uppercase (get_space_types qw/C O S/; corpus 100% uppercase). The driver/operator type pool emitted lowercase 'c'/'o', creating mixed-case designs that fragmented Dom->Ratios class buckets and fired the latent ratio_type first-match nondeterminism (which the search promptly reward-hacked). Operators now emit uppercase generics only and class checks match case-insensitively (t[0].lower() in 'cos', cf. Is_Circulation/Is_Outside). The Urb-side class-sum patch remains as defensive hardening, zero-impact on canonical designs (35/35 parity). Co-Authored-By: Claude Fable 5 --- .beads/issues.jsonl | 2 ++ experiments/operator_locality.py | 2 +- experiments/warm_vs_cold.py | 4 ++-- src/homemaker/driver.py | 6 +++++- src/homemaker/operators.py | 4 +++- tests/test_operators.py | 2 +- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 7a928cb..3a72400 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -17,5 +17,7 @@ {"id":"homemaker-py-9gp","title":"Canonical slicing encoding (normalized Polish expression) + shape feasibility","description":"DESIGN.md §5.5, §7 Phase 5. Representation upgrade once core lands: normalized Polish expression / skewed slicing tree (Wong–Liu) for redundancy-free, high-locality topology moves (M1/M2/M3); bottom-up shape-feasibility checks to prune infeasible topologies before the inner loop. Goal: scale to larger programmes. Excluded representations stay excluded (§2): no sequence-pair/B*-tree (non-slicing).","acceptance_criteria":"Encoding round-trips with the genome; M1/M2/M3 moves implemented; measured search improvement on a larger-than-house programme","status":"open","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-11T23:39:02Z","created_by":"Bruno Postle","updated_at":"2026-06-11T23:39:02Z","dependencies":[{"issue_id":"homemaker-py-9gp","depends_on_id":"homemaker-py-ccw","type":"blocks","created_at":"2026-06-12T00:39:48Z","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.","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-12T07:27:48Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_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":"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":"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."} diff --git a/experiments/operator_locality.py b/experiments/operator_locality.py index d1c61f8..a9d9706 100644 --- a/experiments/operator_locality.py +++ b/experiments/operator_locality.py @@ -49,7 +49,7 @@ def geometry_perturbation(parent_sig: Counter, child: dom.Node) -> float: def main() -> int: - types = sorted(programme.load_programme(str(CORPUS / "patterns.config"))) + ["c", "o"] + types = sorted(programme.load_programme(str(CORPUS / "patterns.config"))) + ["C", "O"] roots = {f: genome.decode(genome.encode(dom.load(str(CORPUS / f)))) for f in FILES} with tempfile.TemporaryDirectory(prefix="op_locality_") as tmp: diff --git a/experiments/warm_vs_cold.py b/experiments/warm_vs_cold.py index 8504912..7bdb475 100644 --- a/experiments/warm_vs_cold.py +++ b/experiments/warm_vs_cold.py @@ -45,14 +45,14 @@ free_with_keys = innerloop.free_with_keys # promoted into the library def divide_leaf(leaf: dom.Node) -> None: leaf.division = [0.5, 0.5] leaf.left = dom.Node(type=leaf.type) - leaf.right = dom.Node(type="c") # circulation absorbs the residual + leaf.right = dom.Node(type="C") # circulation absorbs the residual leaf.type = None def undivide_branch(branch: dom.Node) -> None: # keep the more programme-specific child type (generic = circulation/outside) types = [branch.left.type, branch.right.type] - specific = [t for t in types if t and not t.startswith(("c", "o", "s"))] + specific = [t for t in types if t and t[0].lower() not in "cos"] branch.type = specific[0] if specific else types[0] branch.division = None branch.left = branch.right = None diff --git a/src/homemaker/driver.py b/src/homemaker/driver.py index f62a8b5..b609f59 100644 --- a/src/homemaker/driver.py +++ b/src/homemaker/driver.py @@ -89,7 +89,11 @@ def search( inner_kw = dict(_CHILD_INNER_KW, **(inner_kw or {})) if types is None: reqs = programme.load_programme(str(Path(programme_dir) / "patterns.config")) - types = sorted(reqs) + ["c", "o"] + # Urb's generic types are canonically UPPERCASE (get_space_types: + # qw/C O S/; the corpus is 100% uppercase). Predicates match + # case-insensitively but Dom->Ratios keys raw strings — mixing cases + # fragments the class buckets, so never emit lowercase generics. + types = sorted(reqs) + ["C", "O"] def _log(msg: str) -> None: if log: diff --git a/src/homemaker/operators.py b/src/homemaker/operators.py index 6253791..ad3f7f9 100644 --- a/src/homemaker/operators.py +++ b/src/homemaker/operators.py @@ -81,7 +81,9 @@ def mutate_undivide(root: dom.Node, rng: np.random.Generator, if not cands: return _finalise(child), "undivide noop" li, n = _pick(rng, cands) - keep = [t for t in (n.left.type, n.right.type) if t and not t.startswith(("c", "o", "s"))] + # generic classes (circulation/outside/sahn) match case-insensitively, + # cf. Urb Is_Circulation/Is_Outside + keep = [t for t in (n.left.type, n.right.type) if t and t[0].lower() not in "cos"] n.type = keep[0] if keep else (n.left.type or str(_pick(rng, types))) n.division = None n.left = n.right = None diff --git a/tests/test_operators.py b/tests/test_operators.py index 07074bd..77828df 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -10,7 +10,7 @@ from homemaker import dom, genome, operators CORPUS = Path("/home/bruno/src/urb/examples/programme-house") FILES = ["2f45907abd9accac2a124d311732f749.dom", "candidate-002.dom", "c964435454c459f86c3ed9a5a7621132.dom"] -TYPES = ["k1", "l1", "b1", "b2", "t1", "c", "o"] +TYPES = ["k1", "l1", "b1", "b2", "t1", "C", "O"] pytestmark = pytest.mark.skipif(not CORPUS.is_dir(), reason="Urb corpus not available")