Use Urb's canonical UPPERCASE generic types (C/O); case-insensitive class checks

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 <noreply@anthropic.com>
This commit is contained in:
Bruno Postle 2026-06-12 19:01:53 +01:00
parent 0beb005a23
commit f160c6dc9e
6 changed files with 14 additions and 6 deletions

View file

@ -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 (WongLiu) 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."}

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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")