Phase 3 gate (homemaker-py-uxz): native fitness 35/35 corpus parity; retire oracle from search
Bug fix: _entrance_bid_for_stair now returns None when the stair leaf has an outdoor neighbour with public access — Perl's Entrances function picks the via-outdoor priority (3.5 > 3) which maps the stair to a leaf id rather than a boundary id, so Boundary_Id(edge) eq leaf_id never matches and no entrance corners are added. Without this fix 7 files had an extra 'staircase volume' failure from corners [3,1,2] giving stair_fit=0.718 instead of [3]→1.095. New: Fitness._evaluate_full() extracts the shared pipeline so evaluate() and score_with_fails() both use it. NativeEvaluator added to innerloop.py as a drop-in for OracleEvaluator; optimise() defaults to use_native=True. Gate results: 35/35 score parity (rel_tol=1e-4), 35/35 fail-set identity, native speed ~45ms/eval vs oracle ~1000ms/eval batched = 23x speedup. OracleEvaluator kept for validation; oracle.score_batch unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fc10466966
commit
69d4dcbf23
4 changed files with 206 additions and 22 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
{"id":"homemaker-py-40i","title":"Investigate cf0b8a77e8b2325f ~18% raw_value discrepancy (py lower than oracle)","description":"For prefix cf0b8a77e8b2325f: oracle=1.079112e-03 py=9.133243e-04 ratio=0.8464 (python is ~18% too low). debug_nfails shows py n_fails=5 oracle n_fails=5 (same failures), stair_fits=[1.3145] in python, building_factor=0.1104 (vs oracle's implied ~0.1303). The discrepancy is in raw_value (py=11837 vs oracle implied ~13975) or possibly building_factor. Need to check: (1) per-leaf quality values (crinkliness, area_outside, access) via debug_quality.txt; (2) whether the stair corners differ (cf/rl: py=[2,3] perl=[2,3] — SAME, so corners ok); (3) any quality term not yet ported or computed differently. Run debug_quality.py and compare per-leaf contributions.","status":"closed","priority":1,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-13T18:08:22Z","created_by":"Bruno Postle","updated_at":"2026-06-13T19:05:10Z","started_at":"2026-06-13T18:12:23Z","closed_at":"2026-06-13T19:05:10Z","close_reason":"Root cause found: Python stair_fit misses entrance-door corners. Perl check_stair_fit calls $level-\u003eEntrances($graph) and adds those corners to corners_in_use before calling Stair_Fit. For cf0b8a77/rl(C): stack=[2,3] + entrance=[1,2] → final=[2,3,1] (3 corners, one_turn, going_b=8, sf=0.9751, sf_factor=1.163, bf=0.1305). Python uses only geometry.corners_in_use=[2,3] (2 corners, two_turn, going_b=3, sf=1.3145, sf_factor=0.985, bf=0.1104). Fix tracked in homemaker-py-w1e.","dependencies":[{"issue_id":"homemaker-py-40i","depends_on_id":"homemaker-py-hgg","type":"blocks","created_at":"2026-06-13T19:08:30Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
{"id":"homemaker-py-40i","title":"Investigate cf0b8a77e8b2325f ~18% raw_value discrepancy (py lower than oracle)","description":"For prefix cf0b8a77e8b2325f: oracle=1.079112e-03 py=9.133243e-04 ratio=0.8464 (python is ~18% too low). debug_nfails shows py n_fails=5 oracle n_fails=5 (same failures), stair_fits=[1.3145] in python, building_factor=0.1104 (vs oracle's implied ~0.1303). The discrepancy is in raw_value (py=11837 vs oracle implied ~13975) or possibly building_factor. Need to check: (1) per-leaf quality values (crinkliness, area_outside, access) via debug_quality.txt; (2) whether the stair corners differ (cf/rl: py=[2,3] perl=[2,3] — SAME, so corners ok); (3) any quality term not yet ported or computed differently. Run debug_quality.py and compare per-leaf contributions.","status":"closed","priority":1,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-13T18:08:22Z","created_by":"Bruno Postle","updated_at":"2026-06-13T19:54:04Z","started_at":"2026-06-13T18:12:23Z","closed_at":"2026-06-13T19:54:04Z","close_reason":"Investigation complete: traced 18% discrepancy (cf0b8a77) through entrance corner logic and weighted path length bugs, both now fixed in w1e.","dependencies":[{"issue_id":"homemaker-py-40i","depends_on_id":"homemaker-py-hgg","type":"blocks","created_at":"2026-06-13T19:08:30Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
||||||
{"id":"homemaker-py-w1e","title":"Port Perl entrance-corner logic into Python stair-fit (ca/cb parity)","description":"Perl's check_stair_fit (Leaf.pm:104-142) adds entrance edge corners to corners_in_use before computing stair_fit. Python's process_storey does not. For ca9e80c5c1502f10 and cb93a2d2de7f5d37 the oracle stair leaf 'lr' has corners [1,2,3] (from perl_bf.pl) but debug_corners.py Perl-compatible trace gives [2,3] — the extra corner 1 comes from Entrances(graph)-\u003eBoundary_Id logic. Need to: (1) port dom.Entrances() — returns {leaf_id: boundary_id} for the best-entrance leaf at ground level (Entrances() returns {} if level\u003e0); (2) port leaf.Boundary_Id(side) — returns the node sharing that edge; (3) in fitness.process_storey, after stack_corners_in_use, add entrance-edge corners before computing stair_fit. Acceptance: ca/cb ratio ≈ 1.0 (currently 1.33).","status":"closed","priority":1,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-13T18:08:11Z","created_by":"Bruno Postle","updated_at":"2026-06-13T19:53:40Z","started_at":"2026-06-13T19:09:35Z","closed_at":"2026-06-13T19:53:40Z","close_reason":"Fixed: entrance corner logic via _entrance_bid_for_stair (mirrors Perl Entrances), plus root cause: _avg_path_len_from now uses weighted Dijkstra (centroid distances) matching Perl graph.average_path_length — fixes has_circulation edge removal order. All 4 debug prefixes ratio=1.000, 39 tests pass.","dependencies":[{"issue_id":"homemaker-py-w1e","depends_on_id":"homemaker-py-hgg","type":"blocks","created_at":"2026-06-13T19:08:29Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
{"id":"homemaker-py-w1e","title":"Port Perl entrance-corner logic into Python stair-fit (ca/cb parity)","description":"Perl's check_stair_fit (Leaf.pm:104-142) adds entrance edge corners to corners_in_use before computing stair_fit. Python's process_storey does not. For ca9e80c5c1502f10 and cb93a2d2de7f5d37 the oracle stair leaf 'lr' has corners [1,2,3] (from perl_bf.pl) but debug_corners.py Perl-compatible trace gives [2,3] — the extra corner 1 comes from Entrances(graph)-\u003eBoundary_Id logic. Need to: (1) port dom.Entrances() — returns {leaf_id: boundary_id} for the best-entrance leaf at ground level (Entrances() returns {} if level\u003e0); (2) port leaf.Boundary_Id(side) — returns the node sharing that edge; (3) in fitness.process_storey, after stack_corners_in_use, add entrance-edge corners before computing stair_fit. Acceptance: ca/cb ratio ≈ 1.0 (currently 1.33).","status":"closed","priority":1,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-13T18:08:11Z","created_by":"Bruno Postle","updated_at":"2026-06-13T19:53:40Z","started_at":"2026-06-13T19:09:35Z","closed_at":"2026-06-13T19:53:40Z","close_reason":"Fixed: entrance corner logic via _entrance_bid_for_stair (mirrors Perl Entrances), plus root cause: _avg_path_len_from now uses weighted Dijkstra (centroid distances) matching Perl graph.average_path_length — fixes has_circulation edge removal order. All 4 debug prefixes ratio=1.000, 39 tests pass.","dependencies":[{"issue_id":"homemaker-py-w1e","depends_on_id":"homemaker-py-hgg","type":"blocks","created_at":"2026-06-13T19:08:29Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
||||||
{"id":"homemaker-py-q70","title":"Fix corners_in_use _ib(None,None) bug: triple-at-idx=3 always passes in Perl","description":"In graph.py corners_in_use(), the _ib helper returns False when pa=None or pb=None. Perl's is_between_2d(point, undef, undef) returns True (distance_2d(undef,x)=0 so abs(0-0-0)\u003c1e-6). At triple-check idx=3, c1=corners[4]=None and c2=None, so _ib(w, None, None) must return True to match Perl — meaning the triple always succeeds at idx=3. Fix: add 'if pa is None and pb is None: return True' before the existing 'if pa is None or pb is None: return False'. This is already applied to graph.py. Needs: run 35-file corpus parity test to confirm aa0dcab98927d2c9 passes (corners [0,1,3] → stair_fit=0.878 → sf_factor≈0.570 ≈ oracle).","status":"open","priority":1,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-06-13T18:07:54Z","created_by":"Bruno Postle","updated_at":"2026-06-13T18:07:54Z","dependencies":[{"issue_id":"homemaker-py-q70","depends_on_id":"homemaker-py-hgg","type":"blocks","created_at":"2026-06-13T19:08:28Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
{"id":"homemaker-py-q70","title":"Fix corners_in_use _ib(None,None) bug: triple-at-idx=3 always passes in Perl","description":"In graph.py corners_in_use(), the _ib helper returns False when pa=None or pb=None. Perl's is_between_2d(point, undef, undef) returns True (distance_2d(undef,x)=0 so abs(0-0-0)\u003c1e-6). At triple-check idx=3, c1=corners[4]=None and c2=None, so _ib(w, None, None) must return True to match Perl — meaning the triple always succeeds at idx=3. Fix: add 'if pa is None and pb is None: return True' before the existing 'if pa is None or pb is None: return False'. This is already applied to graph.py. Needs: run 35-file corpus parity test to confirm aa0dcab98927d2c9 passes (corners [0,1,3] → stair_fit=0.878 → sf_factor≈0.570 ≈ oracle).","status":"closed","priority":1,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-06-13T18:07:54Z","created_by":"Bruno Postle","updated_at":"2026-06-13T19:53:53Z","closed_at":"2026-06-13T19:53:53Z","close_reason":"Fixed as part of homemaker-py-w1e: the _avg_path_len_from weighted Dijkstra fix corrects has_circulation edge removal ordering, which was the actual cause of wrong stack corner counts. The _ib(None,None)=True fix was already in place but the weighted path length was the remaining blocker.","dependencies":[{"issue_id":"homemaker-py-q70","depends_on_id":"homemaker-py-hgg","type":"blocks","created_at":"2026-06-13T19:08:28Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
||||||
{"id":"homemaker-py-gp2","title":"Disable occlusion/daylight in Urb oracle (env flag); re-baseline scores","description":"Strategy decision (Bruno, 2026-06-12): occlusion/daylight is orthogonal to whether a better, scalable optimisation system can be built — disable it in Urb rather than port it. Patch Urb behind an env flag (e.g. URB_NO_OCCLUSION=1): quality_daylight returns 1 for outdoor spaces too, and Crinkliness/Area_Outside pins the CIEsky_vertical illumination factor to 1 (simple crinkliness = unweighted external wall area / floor area). Keep the occlusion object plumbing — it carries the Walls/boundaries cache crinkliness needs (ProgrammeDriven.pm:97). Then re-baseline everything once at this clean boundary: corpus .score files, the DESIGN.md $4.5 gains table, accept_innerloop.py gate bars. Also measure oracle s/dom with the flag on — occlusion sampling may be a real slice of the ~1 s/dom cost. The native Python fitness then ships with simple crinkliness only; full occlusion rebuild is deferred post-Phase-5 (homemaker-py-2g5).","acceptance_criteria":"Env-flagged Urb patch; flag on: corpus re-scored, gate bars re-derived, oracle s/dom re-measured; urb-evolve confirmed to respect the flag for the Phase-2 benchmark","status":"closed","priority":1,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-12T07:27:30Z","created_by":"Bruno Postle","updated_at":"2026-06-12T09:31:40Z","closed_at":"2026-06-12T09:31:40Z","close_reason":"URB_NO_OCCLUSION=1 patch in Urb (Leaf.pm quality_daylight -\u003e 1, Dom.pm Area_Outside illumination pinned; flag-off byte-identical, verified). Corpus re-baselined: 35/35 scores shift, one expected crinkliness failure-set change, 0.92 s/dom batched (x1.08). New reference gains recorded in DESIGN §4.7 and accept_innerloop bars (x1.63/x1.70/x1.68, deterministic seed). urb-evolve respects flag by construction. NOTE: Urb working-tree changes left uncommitted in /home/bruno/src/urb for Bruno's review.","dependency_count":0,"dependent_count":3,"comment_count":0}
|
{"id":"homemaker-py-gp2","title":"Disable occlusion/daylight in Urb oracle (env flag); re-baseline scores","description":"Strategy decision (Bruno, 2026-06-12): occlusion/daylight is orthogonal to whether a better, scalable optimisation system can be built — disable it in Urb rather than port it. Patch Urb behind an env flag (e.g. URB_NO_OCCLUSION=1): quality_daylight returns 1 for outdoor spaces too, and Crinkliness/Area_Outside pins the CIEsky_vertical illumination factor to 1 (simple crinkliness = unweighted external wall area / floor area). Keep the occlusion object plumbing — it carries the Walls/boundaries cache crinkliness needs (ProgrammeDriven.pm:97). Then re-baseline everything once at this clean boundary: corpus .score files, the DESIGN.md $4.5 gains table, accept_innerloop.py gate bars. Also measure oracle s/dom with the flag on — occlusion sampling may be a real slice of the ~1 s/dom cost. The native Python fitness then ships with simple crinkliness only; full occlusion rebuild is deferred post-Phase-5 (homemaker-py-2g5).","acceptance_criteria":"Env-flagged Urb patch; flag on: corpus re-scored, gate bars re-derived, oracle s/dom re-measured; urb-evolve confirmed to respect the flag for the Phase-2 benchmark","status":"closed","priority":1,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-12T07:27:30Z","created_by":"Bruno Postle","updated_at":"2026-06-12T09:31:40Z","closed_at":"2026-06-12T09:31:40Z","close_reason":"URB_NO_OCCLUSION=1 patch in Urb (Leaf.pm quality_daylight -\u003e 1, Dom.pm Area_Outside illumination pinned; flag-off byte-identical, verified). Corpus re-baselined: 35/35 scores shift, one expected crinkliness failure-set change, 0.92 s/dom batched (x1.08). New reference gains recorded in DESIGN §4.7 and accept_innerloop bars (x1.63/x1.70/x1.68, deterministic seed). urb-evolve respects flag by construction. NOTE: Urb working-tree changes left uncommitted in /home/bruno/src/urb for Bruno's review.","dependency_count":0,"dependent_count":3,"comment_count":0}
|
||||||
{"id":"homemaker-py-uxz","title":"Native fitness validation: 35-file corpus parity vs oracle; retire oracle (Phase 3 gate)","description":"DESIGN.md §7 Phase 3 gate. Validate the assembled native fitness against urb-fitness.pl across all 35 programme-house .dom files: scores within float tolerance AND identical failure sets. Swap behind the same interface as oracle.score so inner loop and search driver are unchanged; keep the oracle available as validation reference but stop using it in search. Then re-run topology search at scale (separate issue).","acceptance_criteria":"35/35 files: score parity within tolerance, failure sets identical; search runs end-to-end on native fitness with measured speedup vs oracle","status":"open","priority":1,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:27Z","created_by":"Bruno Postle","updated_at":"2026-06-11T23:38:27Z","dependencies":[{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-3y7","type":"blocks","created_at":"2026-06-12T00:39:40Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-40i","type":"blocks","created_at":"2026-06-13T19:08:33Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-gnw","type":"blocks","created_at":"2026-06-12T00:39:41Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-gp2","type":"blocks","created_at":"2026-06-12T08:27:44Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-hgg","type":"blocks","created_at":"2026-06-12T00:39:43Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-q70","type":"blocks","created_at":"2026-06-13T19:08:31Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-w1e","type":"blocks","created_at":"2026-06-13T19:08:32Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":7,"dependent_count":3,"comment_count":0}
|
{"id":"homemaker-py-uxz","title":"Native fitness validation: 35-file corpus parity vs oracle; retire oracle (Phase 3 gate)","description":"DESIGN.md §7 Phase 3 gate. Validate the assembled native fitness against urb-fitness.pl across all 35 programme-house .dom files: scores within float tolerance AND identical failure sets. Swap behind the same interface as oracle.score so inner loop and search driver are unchanged; keep the oracle available as validation reference but stop using it in search. Then re-run topology search at scale (separate issue).","acceptance_criteria":"35/35 files: score parity within tolerance, failure sets identical; search runs end-to-end on native fitness with measured speedup vs oracle","status":"in_progress","priority":1,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:27Z","created_by":"Bruno Postle","updated_at":"2026-06-13T19:57:49Z","started_at":"2026-06-13T19:57:48Z","dependencies":[{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-3y7","type":"blocks","created_at":"2026-06-12T00:39:40Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-40i","type":"blocks","created_at":"2026-06-13T19:08:33Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-gnw","type":"blocks","created_at":"2026-06-12T00:39:41Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-gp2","type":"blocks","created_at":"2026-06-12T08:27:44Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-hgg","type":"blocks","created_at":"2026-06-12T00:39:43Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-q70","type":"blocks","created_at":"2026-06-13T19:08:31Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-uxz","depends_on_id":"homemaker-py-w1e","type":"blocks","created_at":"2026-06-13T19:08:32Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":7,"dependent_count":3,"comment_count":0}
|
||||||
{"id":"homemaker-py-hgg","title":"Native fitness: storey/building checks + missing-space failure stacking","description":"DESIGN.md §6. Port ProgrammeDriven/Storey/Building checks: space-count matching with MISSING-SPACE FAILURE STACKING (2 base failures + 1 per size/width/proportion/adjacency/level requirement, up to ~7 — ProgrammeDriven.pm:192-212; reshaping must preserve this hierarchy), adjacency/level/requires_below checks, staircase fit/volume/min-max, public access, circulation \u0026 outside ratios, min internal area (1.2x programme sum), storey limit/minimum, structural failures (edge too long \u003e8 m both variants, unsupported covered outside, covered outside above ground, level not connected, inaccessible usable space), preprocess_building s-\u003eO conversion, and the 0.5^n penalty over value/cost.","acceptance_criteria":"Failure sets and final scores match the oracle on sample files; failure-stacking counts identical","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:26Z","created_by":"Bruno Postle","updated_at":"2026-06-13T18:11:27Z","started_at":"2026-06-13T08:59:14Z","closed_at":"2026-06-13T18:11:27Z","close_reason":"Implementation complete: storey/building checks, failure stacking, staircase logic, public access, circulation ratios, structural checks all ported. 39 tests pass.","dependency_count":0,"dependent_count":4,"comment_count":0}
|
{"id":"homemaker-py-hgg","title":"Native fitness: storey/building checks + missing-space failure stacking","description":"DESIGN.md §6. Port ProgrammeDriven/Storey/Building checks: space-count matching with MISSING-SPACE FAILURE STACKING (2 base failures + 1 per size/width/proportion/adjacency/level requirement, up to ~7 — ProgrammeDriven.pm:192-212; reshaping must preserve this hierarchy), adjacency/level/requires_below checks, staircase fit/volume/min-max, public access, circulation \u0026 outside ratios, min internal area (1.2x programme sum), storey limit/minimum, structural failures (edge too long \u003e8 m both variants, unsupported covered outside, covered outside above ground, level not connected, inaccessible usable space), preprocess_building s-\u003eO conversion, and the 0.5^n penalty over value/cost.","acceptance_criteria":"Failure sets and final scores match the oracle on sample files; failure-stacking counts identical","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:26Z","created_by":"Bruno Postle","updated_at":"2026-06-13T18:11:27Z","started_at":"2026-06-13T08:59:14Z","closed_at":"2026-06-13T18:11:27Z","close_reason":"Implementation complete: storey/building checks, failure stacking, staircase logic, public access, circulation ratios, structural checks all ported. 39 tests pass.","dependency_count":0,"dependent_count":4,"comment_count":0}
|
||||||
{"id":"homemaker-py-gnw","title":"Native fitness: leaf quality terms + cost model","description":"DESIGN.md §6. Port Leaf.pm quality terms (size, width, proportion, perpendicular, access) with programme-driven parameter lookup (get_space_params fallback chain, generic c/o/s handling, width_inside [4.0,1.0] default), gaussian scoring, FAIL_THRESHOLD=0.1. Also the COST DENOMINATOR — fitness is value/cost: per-leaf area costs, interior/exterior wall edge costs, boundary costs, value rates (Leaf.pm:194-251, Storey.pm:122-147). Cost couples to geometry too.","acceptance_criteria":"Per-leaf quality factors and per-storey cost/value match Perl (float tolerance) on sample corpus files with DEBUG output diffed","notes":"Crinkliness scope (2026-06-12): port SIMPLE crinkliness only — external wall area / floor area with the CIEsky illumination factor pinned to 1 (boundary-overlap geometry from Dom-\u003eWalls stays in scope; the sky model does not). Must match the URB_NO_OCCLUSION-flagged oracle (homemaker-py-gp2), not stock Urb. quality_daylight = 1 for all spaces.","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:24Z","created_by":"Bruno Postle","updated_at":"2026-06-13T07:00:07Z","started_at":"2026-06-12T21:07:06Z","closed_at":"2026-06-13T07:00:07Z","close_reason":"0-mismatch parity: 35 files, 407 leaves, 2849 factors","dependencies":[{"issue_id":"homemaker-py-gnw","depends_on_id":"homemaker-py-gp2","type":"blocks","created_at":"2026-06-12T08:27:46Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
{"id":"homemaker-py-gnw","title":"Native fitness: leaf quality terms + cost model","description":"DESIGN.md §6. Port Leaf.pm quality terms (size, width, proportion, perpendicular, access) with programme-driven parameter lookup (get_space_params fallback chain, generic c/o/s handling, width_inside [4.0,1.0] default), gaussian scoring, FAIL_THRESHOLD=0.1. Also the COST DENOMINATOR — fitness is value/cost: per-leaf area costs, interior/exterior wall edge costs, boundary costs, value rates (Leaf.pm:194-251, Storey.pm:122-147). Cost couples to geometry too.","acceptance_criteria":"Per-leaf quality factors and per-storey cost/value match Perl (float tolerance) on sample corpus files with DEBUG output diffed","notes":"Crinkliness scope (2026-06-12): port SIMPLE crinkliness only — external wall area / floor area with the CIEsky illumination factor pinned to 1 (boundary-overlap geometry from Dom-\u003eWalls stays in scope; the sky model does not). Must match the URB_NO_OCCLUSION-flagged oracle (homemaker-py-gp2), not stock Urb. quality_daylight = 1 for all spaces.","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:24Z","created_by":"Bruno Postle","updated_at":"2026-06-13T07:00:07Z","started_at":"2026-06-12T21:07:06Z","closed_at":"2026-06-13T07:00:07Z","close_reason":"0-mismatch parity: 35 files, 407 leaves, 2849 factors","dependencies":[{"issue_id":"homemaker-py-gnw","depends_on_id":"homemaker-py-gp2","type":"blocks","created_at":"2026-06-12T08:27:46Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
||||||
{"id":"homemaker-py-3y7","title":"Native fitness: adjacency/connectivity graph build + Merge_Divided semantics","description":"DESIGN.md §6 port scope, §7 Phase 3 (native fitness gates topology search at scale — §4.6). Port the door_width (1.2 m) adjacency graph (Urb Dom Graph), Merge_Divided, and the TWO-PHASE build: adjacency/level/vertical checks run on the UNMERGED tree, graphs rebuilt after Merge_Divided for storey processing (ProgrammeDriven.pm:83-103). Port faithfully — including has_vertical_connection's no-spatial-overlap stub (ProgrammeDriven.pm:399-423) unless the fidelity decision (§8.1) says otherwise; record the decision.","acceptance_criteria":"Graph edges/widths and merged structure match Perl on the 35-file corpus; vertical-connectivity fidelity decision recorded","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:23Z","created_by":"Bruno Postle","updated_at":"2026-06-12T20:55:31Z","started_at":"2026-06-12T13:13:09Z","closed_at":"2026-06-12T20:55:31Z","close_reason":"Graph edges/widths match Perl on all 35 corpus files (2 bugs fixed: empty-string boundary excluded by Python 'in' operator substring check, and upper-storey rotation not delegating to below-link). Merge_Divided ported. Vertical-connectivity fidelity decision recorded in graph.py module docstring (faithful stub, no spatial overlap). Tests in test_graph.py.","dependency_count":0,"dependent_count":1,"comment_count":0}
|
{"id":"homemaker-py-3y7","title":"Native fitness: adjacency/connectivity graph build + Merge_Divided semantics","description":"DESIGN.md §6 port scope, §7 Phase 3 (native fitness gates topology search at scale — §4.6). Port the door_width (1.2 m) adjacency graph (Urb Dom Graph), Merge_Divided, and the TWO-PHASE build: adjacency/level/vertical checks run on the UNMERGED tree, graphs rebuilt after Merge_Divided for storey processing (ProgrammeDriven.pm:83-103). Port faithfully — including has_vertical_connection's no-spatial-overlap stub (ProgrammeDriven.pm:399-423) unless the fidelity decision (§8.1) says otherwise; record the decision.","acceptance_criteria":"Graph edges/widths and merged structure match Perl on the 35-file corpus; vertical-connectivity fidelity decision recorded","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-11T23:38:23Z","created_by":"Bruno Postle","updated_at":"2026-06-12T20:55:31Z","started_at":"2026-06-12T13:13:09Z","closed_at":"2026-06-12T20:55:31Z","close_reason":"Graph edges/widths match Perl on all 35 corpus files (2 bugs fixed: empty-string boundary excluded by Python 'in' operator substring check, and upper-storey rotation not delegating to below-link). Merge_Divided ported. Vertical-connectivity fidelity decision recorded in graph.py module docstring (faithful stub, no spatial overlap). Tests in test_graph.py.","dependency_count":0,"dependent_count":1,"comment_count":0}
|
||||||
|
|
@ -19,8 +19,9 @@
|
||||||
{"id":"homemaker-py-yg5","title":"Penalty reshaping: replace 0.5^n while preserving inner-loop protection","description":"DESIGN.md §4.7, §5.4, §7 Phase 4, §8.5. The 0.5^n cliff gives the outer search no gradient and rewards flag-count over geometry, but it also PROTECTS the inner loop from trading into new failures (§4.5). One fitness shape cannot naively be both soft outside and cliff-protected inside. Candidates: cliff-inside-inner-loop only, lexicographic (failure count first, score second), additive/soft, multi-objective Pareto. Must preserve the missing-space failure hierarchy (worse to drop a room than to have a poor one). Measure landscape + search outcomes; this helps Urb today too.","acceptance_criteria":"Chosen scheme documented with measurements: search improves while inner loop still never trades into new failures","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-11T23:39:00Z","created_by":"Bruno Postle","updated_at":"2026-06-11T23:39:00Z","dependencies":[{"issue_id":"homemaker-py-yg5","depends_on_id":"homemaker-py-uxz","type":"blocks","created_at":"2026-06-12T00:39:46Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
{"id":"homemaker-py-yg5","title":"Penalty reshaping: replace 0.5^n while preserving inner-loop protection","description":"DESIGN.md §4.7, §5.4, §7 Phase 4, §8.5. The 0.5^n cliff gives the outer search no gradient and rewards flag-count over geometry, but it also PROTECTS the inner loop from trading into new failures (§4.5). One fitness shape cannot naively be both soft outside and cliff-protected inside. Candidates: cliff-inside-inner-loop only, lexicographic (failure count first, score second), additive/soft, multi-objective Pareto. Must preserve the missing-space failure hierarchy (worse to drop a room than to have a poor one). Measure landscape + search outcomes; this helps Urb today too.","acceptance_criteria":"Chosen scheme documented with measurements: search improves while inner loop still never trades into new failures","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-06-11T23:39:00Z","created_by":"Bruno Postle","updated_at":"2026-06-11T23:39:00Z","dependencies":[{"issue_id":"homemaker-py-yg5","depends_on_id":"homemaker-py-uxz","type":"blocks","created_at":"2026-06-12T00:39:46Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
|
||||||
{"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-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}
|
{"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":"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-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":"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":"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":"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":"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":"correction-to-urb-fitness-bug-memory-bruno-2026","value":"CORRECTION to urb-fitness-bug memory (Bruno, 2026-06-12): 'C' is NOT a 'covered' type — Is_Covered is a geometric predicate (indoor space above). Urb's generic types are canonically UPPERCASE: C=circulation, O=outside, S=sahn (get_space_types qw/C O S/; corpus is 100% uppercase, never 'c'/'o' leaves). The mixed-case designs that fired the latent ratio_type first-match bug were created by homemaker's own operator type pool emitting lowercase 'c'/'o' — fixed: driver/operators now emit uppercase generics only, and class checks use t[0].lower() in 'cos'. The Urb class-sum patch stays as defensive hardening (zero impact on canonical designs). Native port (3y7/gnw): treat type classes case-insensitively, generics canonically uppercase."}
|
||||||
|
{"_type":"memory","key":"homemaker-py-pythonpath-set-pythonpath-home-bruno-src","value":"homemaker-py PYTHONPATH: set PYTHONPATH=/home/bruno/src/homemaker-py/src or use 'python -m pytest' from the project root (which reads pyproject.toml and adds src/ automatically). Never try 'pip show' or 'pip install' — it's not installed as a package."}
|
||||||
|
{"_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)."}
|
||||||
|
|
|
||||||
|
|
@ -692,6 +692,13 @@ class Fitness:
|
||||||
for nb in G.neighbors(other):
|
for nb in G.neighbors(other):
|
||||||
if nb.type and nb.type[0].lower() == "o" and self._public_access(nb, root) is not None:
|
if nb.type and nb.type[0].lower() == "o" and self._public_access(nb, root) is not None:
|
||||||
return None
|
return None
|
||||||
|
# If the stair itself has via-outdoor access (Entrances priority 3.5), Perl's
|
||||||
|
# Entrances maps it to a leaf id, not a boundary id. Boundary_Id(edge) eq
|
||||||
|
# leaf_id never matches → no entrance corners added. Return None here so
|
||||||
|
# Python matches that behaviour.
|
||||||
|
for nb in G.neighbors(stair_leaf):
|
||||||
|
if nb.type and nb.type[0].lower() == "o" and self._public_access(nb, root) is not None:
|
||||||
|
return None
|
||||||
return stair_bid
|
return stair_bid
|
||||||
|
|
||||||
def _public_access_outside(self, leaf: Node, G: nx.Graph, root: Node) -> bool:
|
def _public_access_outside(self, leaf: Node, G: nx.Graph, root: Node) -> bool:
|
||||||
|
|
@ -937,8 +944,15 @@ class Fitness:
|
||||||
|
|
||||||
Returns ``value / cost`` (the final score as in Urb).
|
Returns ``value / cost`` (the final score as in Urb).
|
||||||
"""
|
"""
|
||||||
|
score, _ = self._evaluate_full(root)
|
||||||
|
return score
|
||||||
|
|
||||||
|
def score_with_fails(self, root: Node) -> tuple[float, tuple[str, ...]]:
|
||||||
|
"""Same as ``evaluate`` but also returns the sorted failure strings."""
|
||||||
|
return self._evaluate_full(root)
|
||||||
|
|
||||||
|
def _evaluate_full(self, root: Node) -> tuple[float, tuple[str, ...]]:
|
||||||
from . import graph as graph_mod
|
from . import graph as graph_mod
|
||||||
from .programme import load_programme
|
|
||||||
|
|
||||||
geometry.clear_cache()
|
geometry.clear_cache()
|
||||||
|
|
||||||
|
|
@ -999,12 +1013,10 @@ class Fitness:
|
||||||
value *= building_factor
|
value *= building_factor
|
||||||
|
|
||||||
# 0.5^n failure penalty (programme-driven mode, not 0.1^n)
|
# 0.5^n failure penalty (programme-driven mode, not 0.1^n)
|
||||||
n_fails = len(failures)
|
value *= 0.5 ** len(failures)
|
||||||
value *= 0.5 ** n_fails
|
|
||||||
|
|
||||||
if cost == 0.0:
|
score = value / cost if cost != 0.0 else 0.0
|
||||||
return 0.0
|
return score, tuple(sorted(failures))
|
||||||
return value / cost
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _programme(self) -> dict | None:
|
def _programme(self) -> dict | None:
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,10 @@ ownership) against the FULL fitness. Never a proxy objective — §4.2 falsified
|
||||||
that; the full objective's ``0.5^n`` failure cliff is what protects the inner
|
that; the full objective's ``0.5^n`` failure cliff is what protects the inner
|
||||||
loop from trading into new failures (§4.5).
|
loop from trading into new failures (§4.5).
|
||||||
|
|
||||||
Fitness comes from the Perl oracle for now. The optimiser is a batched compass
|
Fitness defaults to the native Python evaluator (Phase 3). The Perl oracle
|
||||||
(pattern) search: each iteration proposes ``2 × DOF`` candidate points and
|
(``OracleEvaluator``) is kept for validation but is no longer used in search.
|
||||||
scores them in ONE ``oracle.score_batch`` call, so the Perl startup amortises
|
Warm-starting from a parent's optimised ratios is ``x0=`` (§5 decision 6,
|
||||||
across the population (§4.6). Warm-starting from a parent's optimised ratios is
|
Lamarckian inheritance).
|
||||||
just ``x0=`` (§5 decision 6, Lamarckian inheritance).
|
|
||||||
|
|
||||||
Budgets are counted in oracle evaluations (scored ``.dom`` files), the only
|
|
||||||
currency that matters while the oracle is the bottleneck.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -269,12 +265,80 @@ def cma_search(
|
||||||
_METHODS = {"cma": cma_search, "compass": compass_search}
|
_METHODS = {"cma": cma_search, "compass": compass_search}
|
||||||
|
|
||||||
|
|
||||||
|
from dataclasses import dataclass as _dc
|
||||||
|
|
||||||
|
|
||||||
|
@_dc
|
||||||
|
class _NativeScore:
|
||||||
|
"""oracle.Score-compatible result from native fitness."""
|
||||||
|
|
||||||
|
fitness: float
|
||||||
|
fail_lines: tuple
|
||||||
|
|
||||||
|
@property
|
||||||
|
def n_fails(self) -> int:
|
||||||
|
return len(self.fail_lines)
|
||||||
|
|
||||||
|
|
||||||
|
class NativeEvaluator:
|
||||||
|
"""Scores ratio vectors for a frozen topology via the native Python fitness.
|
||||||
|
|
||||||
|
Drop-in replacement for ``OracleEvaluator``; no temp directory, no Perl
|
||||||
|
startup overhead. Each ``evaluate`` call runs ``Fitness.score_with_fails``
|
||||||
|
serially over the batch (all in-process, no parallelism needed at this
|
||||||
|
scale).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, root: dom.Node, programme_dir: str | Path):
|
||||||
|
from . import fitness as fit_mod
|
||||||
|
|
||||||
|
self.root = root
|
||||||
|
self.free = solver.free_branches(root)
|
||||||
|
conf, cost = fit_mod.load_config(programme_dir)
|
||||||
|
self._fit = fit_mod.Fitness(conf, cost)
|
||||||
|
self.n_evals = 0
|
||||||
|
self.n_oracle_calls = 0 # kept for interface parity with OracleEvaluator
|
||||||
|
|
||||||
|
def __enter__(self) -> "NativeEvaluator":
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x_current(self) -> np.ndarray:
|
||||||
|
return np.array(
|
||||||
|
[(b.division[0] + b.division[1]) / 2 for b in self.free], dtype=float
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply(self, x: np.ndarray) -> None:
|
||||||
|
xc = np.clip(x, _EPS, 1 - _EPS)
|
||||||
|
for j, b in enumerate(self.free):
|
||||||
|
b.division = [float(xc[j]), float(xc[j])]
|
||||||
|
|
||||||
|
def evaluate(self, xs: list[np.ndarray]) -> "list[_NativeScore]":
|
||||||
|
"""Score a batch of ratio vectors; returns objects with .fitness /
|
||||||
|
.n_fails / .fail_lines matching the oracle.Score interface."""
|
||||||
|
import copy
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for x in xs:
|
||||||
|
self.apply(x)
|
||||||
|
root_copy = copy.deepcopy(self.root)
|
||||||
|
score, fails = self._fit.score_with_fails(root_copy)
|
||||||
|
results.append(_NativeScore(fitness=score, fail_lines=fails))
|
||||||
|
self.n_evals += len(xs)
|
||||||
|
self.n_oracle_calls += 1
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def optimise(
|
def optimise(
|
||||||
root: dom.Node,
|
root: dom.Node,
|
||||||
programme_dir: str | Path,
|
programme_dir: str | Path,
|
||||||
x0: np.ndarray | None = None,
|
x0: np.ndarray | None = None,
|
||||||
budget: int = 200,
|
budget: int = 200,
|
||||||
method: str = "cma",
|
method: str = "cma",
|
||||||
|
use_native: bool = True,
|
||||||
urb_root: str | Path = oracle.DEFAULT_URB_ROOT,
|
urb_root: str | Path = oracle.DEFAULT_URB_ROOT,
|
||||||
**search_kw,
|
**search_kw,
|
||||||
) -> Result:
|
) -> Result:
|
||||||
|
|
@ -283,8 +347,13 @@ def optimise(
|
||||||
``x0=None`` starts from the topology's current ratios (cold start); pass a
|
``x0=None`` starts from the topology's current ratios (cold start); pass a
|
||||||
parent's optimised ratios for a Lamarckian warm start. On return ``root``
|
parent's optimised ratios for a Lamarckian warm start. On return ``root``
|
||||||
carries the best ratios found.
|
carries the best ratios found.
|
||||||
|
|
||||||
|
``use_native=True`` (default) uses the native Python fitness; set False to
|
||||||
|
fall back to the Perl oracle (kept for validation only).
|
||||||
"""
|
"""
|
||||||
with OracleEvaluator(root, programme_dir, urb_root) as ev:
|
ev_cls = NativeEvaluator if use_native else OracleEvaluator
|
||||||
|
ev_args = (root, programme_dir) if use_native else (root, programme_dir, urb_root)
|
||||||
|
with ev_cls(*ev_args) as ev:
|
||||||
if x0 is None:
|
if x0 is None:
|
||||||
x0 = ev.x_current
|
x0 = ev.x_current
|
||||||
if len(x0) == 0: # undivided topology (e.g. a bare plot): nothing to optimise
|
if len(x0) == 0: # undivided topology (e.g. a bare plot): nothing to optimise
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
"""Corpus-backed tests for dom round-trip and free-branch ownership.
|
"""Corpus-backed tests for dom round-trip, free-branch ownership, and fitness parity.
|
||||||
|
|
||||||
Skipped when the Urb checkout is absent (these need only its .dom files, not
|
Skipped when the Urb checkout is absent (these need only its .dom files, not
|
||||||
perl).
|
perl). The parity tests compare native Python fitness against cached oracle
|
||||||
|
scores and failure sets (generated with URB_NO_OCCLUSION=1).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -55,3 +57,103 @@ def test_free_branches_are_lowest_storey_owners():
|
||||||
for b in solver.free_branches(root):
|
for b in solver.free_branches(root):
|
||||||
assert b.divided
|
assert b.divided
|
||||||
assert b.below is None or not b.below.divided
|
assert b.below is None or not b.below.divided
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 3 gate: native fitness parity vs oracle (homemaker-py-uxz)
|
||||||
|
# Oracle scores and failure sets cached as <file>.dom.score / <file>.dom.fails
|
||||||
|
# generated with URB_NO_OCCLUSION=1 (DESIGN.md §6 descope).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _native_evaluate(src: Path):
|
||||||
|
"""Run native Fitness.evaluate and return (score, frozenset[fail_lines])."""
|
||||||
|
from homemaker import fitness as fit_mod, graph as graph_mod, geometry
|
||||||
|
|
||||||
|
root = dom.load(str(src))
|
||||||
|
conf, cost = fit_mod.load_config(CORPUS)
|
||||||
|
fit = fit_mod.Fitness(conf, cost)
|
||||||
|
|
||||||
|
failures: list[str] = []
|
||||||
|
tracking: dict = {
|
||||||
|
"has_public_access_outside": False,
|
||||||
|
"has_public_access_inside": False,
|
||||||
|
"public_length_all": 0.0,
|
||||||
|
"public_length_outside": 0.0,
|
||||||
|
"private_length_all": 0.0,
|
||||||
|
"private_length_outside": 0.0,
|
||||||
|
"stair_fit": [],
|
||||||
|
"_failures": failures,
|
||||||
|
}
|
||||||
|
|
||||||
|
programme = fit._programme or {}
|
||||||
|
geometry.clear_cache()
|
||||||
|
|
||||||
|
check_f, missing = graph_mod.check_space_counts(root, programme)
|
||||||
|
failures.extend(check_f)
|
||||||
|
fit.preprocess_building(root)
|
||||||
|
_, gcpre = graph_mod.build_graphs_with_circ(root, fit.conf("door_width") or 1.2, failures.append)
|
||||||
|
gbpre = graph_mod.build_graphs(root, fit.conf("door_width") or 1.2)
|
||||||
|
failures.extend(graph_mod.check_adjacency(root, programme, gbpre, missing))
|
||||||
|
failures.extend(graph_mod.check_level_constraints(root, programme, missing))
|
||||||
|
failures.extend(graph_mod.check_vertical_connectivity(root, programme, missing))
|
||||||
|
|
||||||
|
dom.merge_divided(root)
|
||||||
|
geometry.clear_cache()
|
||||||
|
_, gc = graph_mod.build_graphs_with_circ(root, fit.conf("door_width") or 1.2, failures.append)
|
||||||
|
gb = graph_mod.build_graphs(root, fit.conf("door_width") or 1.2)
|
||||||
|
|
||||||
|
cost_v = fit.plot_cost(root)
|
||||||
|
value = 0.0
|
||||||
|
lvls = dom.levels(root)
|
||||||
|
for li, lvl in enumerate(lvls):
|
||||||
|
se = fit.process_storey(
|
||||||
|
lvl, gb[li], li, failures.append,
|
||||||
|
graph_circ=gc, tracking=tracking, lvls=lvls, root=root,
|
||||||
|
)
|
||||||
|
cost_v += se.cost
|
||||||
|
value += se.value
|
||||||
|
|
||||||
|
bf = fit.evaluate_building(root, tracking)
|
||||||
|
value *= bf
|
||||||
|
value *= 0.5 ** len(failures)
|
||||||
|
score = value / cost_v if cost_v else 0.0
|
||||||
|
|
||||||
|
return score, frozenset(failures)
|
||||||
|
|
||||||
|
|
||||||
|
def _oracle_result(src: Path):
|
||||||
|
"""Read cached oracle score and failure set (URB_NO_OCCLUSION=1)."""
|
||||||
|
from homemaker.oracle import Score
|
||||||
|
|
||||||
|
score_file = Path(str(src) + ".score")
|
||||||
|
fails_file = Path(str(src) + ".fails")
|
||||||
|
if not score_file.exists():
|
||||||
|
pytest.skip(f"No cached oracle score for {src.name}")
|
||||||
|
oracle_score = float(score_file.read_text().strip())
|
||||||
|
oracle_fails = Score(
|
||||||
|
fitness=oracle_score,
|
||||||
|
fails=fails_file.read_text() if fails_file.exists() else "",
|
||||||
|
).fail_lines
|
||||||
|
return oracle_score, frozenset(oracle_fails)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("src", sorted(CORPUS.glob("*.dom")), ids=lambda p: p.name)
|
||||||
|
def test_native_fitness_score_parity(src):
|
||||||
|
"""Native score matches oracle within 1e-4 relative tolerance."""
|
||||||
|
native_score, _ = _native_evaluate(src)
|
||||||
|
oracle_score, _ = _oracle_result(src)
|
||||||
|
assert math.isclose(native_score, oracle_score, rel_tol=1e-4, abs_tol=1e-15), (
|
||||||
|
f"{src.name}: native={native_score:.6e} oracle={oracle_score:.6e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("src", sorted(CORPUS.glob("*.dom")), ids=lambda p: p.name)
|
||||||
|
def test_native_fitness_fail_set_parity(src):
|
||||||
|
"""Native failure set matches oracle failure set exactly."""
|
||||||
|
_, native_fails = _native_evaluate(src)
|
||||||
|
_, oracle_fails = _oracle_result(src)
|
||||||
|
only_native = native_fails - oracle_fails
|
||||||
|
only_oracle = oracle_fails - native_fails
|
||||||
|
assert not only_native and not only_oracle, (
|
||||||
|
f"{src.name}: only_native={sorted(only_native)} only_oracle={sorted(only_oracle)}"
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue