diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 3e10a51..5d5786f 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -107,8 +107,8 @@ {"_type":"issue","id":"homemaker-py-5bv","title":"CP-SAT post-collapse repair (Fitness.collapse_global's Jacobi+2-opt QAP relaxation)","description":"homemaker-py-2g7.5 item (c), deferred (DESIGN.md §37.7). Fitness.collapse_global (fitness.py:655-847) approximates a finish-time cell\u003c-\u003eroom relabelling QAP with a Jacobi-style fixpoint iteration (_best_assignment, linear_sum_assignment warm-started each round from the previous round's neighbour labels) plus _two_opt_adjacency_polish to escape 2-cycle plateaus. DESIGN.md §25 explicitly considered and rejected OR-Tools for this exact problem 'because the project has no ortools' -- 2g7.5 has now added that dependency (for a simpler, different problem: assignment on a FIXED topology, not this finish-time relabel). This bead: replace or augment collapse_global's Jacobi+2-opt loop with an exact CP-SAT solve of the same cell\u003c-\u003eroom assignment (reusing _collapse_value's per-(leaf,code) value function so both stay consistent), verified safe against the 94g keep-better guard. Riskier than 2g7.5's seeder/reassign work since collapse_global is delicate, heavily tested, and runs inside every in-search eval when collapse_insearch=True (driver.py default) -- correctness and wall-clock regressions would be felt everywhere, not just in an opt-in flag.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-04T07:43:33Z","created_by":"Bruno Postle","updated_at":"2026-08-04T07:43:33Z","dependencies":[{"issue_id":"homemaker-py-5bv","depends_on_id":"homemaker-py-2g7.5","type":"parent-child","created_at":"2026-08-04T08:44:03Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-5bv","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-04T08:44:25Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-v4s","title":"driver.search A/B for shapecurve warm-start/prune on real multi-storey programmes","description":"Follow-up from homemaker-py-koo (DESIGN.md §37.6): koo generalised shapecurve.py's DP to handle below-inherited multi-storey trees and validated it (DP-vs-NM agreement/false-negative bar, 200 topologies on the real examples/harbor-house, 99.5% agreement, 0 false negatives, 117.7x speedup, DESIGN.md §37.6). Not measured: the search-level payoff of shapecurve_warmstart/shapecurve_prune on a real multi-storey programme at the 6xh/wkh A/B protocol (budget=2000, seeds 0-4, driver.search mean hard/soft/fitness fails, off vs on). Best sized as a single A/B once homemaker-py-tym (leaf_sharing/co_type modelling in shapecurve.leaf_constraints) also lands, since leaf_sharing defaults True in driver.search and both programme-house and harbor-house require it by default -- measuring the combined win (multi-storey + leaf_sharing) in one pass avoids two partial A/Bs that each only apply with a flag most real runs don't use.","notes":"Depends on homemaker-py-tym landing first for the combined measurement to be meaningful.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-03T22:23:50Z","created_by":"Bruno Postle","updated_at":"2026-08-03T22:24:36Z","dependencies":[{"issue_id":"homemaker-py-v4s","depends_on_id":"homemaker-py-tym","type":"blocks","created_at":"2026-08-03T23:24:37Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-ekc","title":"True skew-quad polygon algebra for the shape-curve DP leaf region (remove ~7-12% rectangle approximation error)","description":"homemaker-py-6xh item (DESIGN.md §37.2, 'Remaining approximation error, root-caused'). src/homemaker_layout/shapecurve.py approximates every quad (leaf or internal) as a rectangle with edge-length-derived (w,h) = ((edge0+edge2)/2, (edge1+edge3)/2) -- exact only for a true rectangle/parallelogram. DESIGN.md §37.2's 200-topology harbor-house-l0 validation root-caused both measured false positives to this approximation specifically (not to global rotation or to the rotation-parity composition rule, both already fixed/verified exact): the DP's own realised point had a leaf whose edge-length-approximated area was comfortably inside its feasible bound but whose true geometry.area (a real, slightly non-parallelogram quad) fell just below the true lower bound -- an ~8-12% gap, the same magnitude as harbor-house-l0's own plot-level residual skew. Needs: either (a) replace the rectangle approximation with true skew-quad polygon algebra (a harder closed-form derivation, or a numerically-solved per-leaf feasible region), or (b) at minimum re-characterise the error's magnitude on a LESS rectangular plot than harbor-house-l0's near-rectangular trapezoid (§37.2 flagged this as untested and likely worse elsewhere) so shapecurve_warmstart's real-world false-positive rate is known before wider rollout.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-03T17:30:23Z","created_by":"Bruno Postle","updated_at":"2026-08-03T17:30:23Z","dependencies":[{"issue_id":"homemaker-py-ekc","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-03T18:31:51Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"homemaker-py-tym","title":"leaf_sharing/co_type target-adjustment modelling in shapecurve.leaf_constraints","description":"homemaker-py-6xh item 4 (DESIGN.md §37.2/§37.4). src/homemaker_layout/shapecurve.py's leaf_constraints() uses each leaf's own type's base (target, sigma) params only -- it does not model the leaf-sharing/co_type k-scaling (target*=k, sigma adjustment) that fitness.py's quality_size applies for shared/multi-use leaves. shapecurve.eligible() currently guards this by excluding any run with leaf_sharing/superpose/max_share/multi_use on, so the DP warm-start never fires for those runs -- but leaf_sharing defaults to True in driver.search(), so most real runs are excluded today. Needs: read fitness.py's actual k-scaling formula (quality_size's leaf-sharing branch) and mirror it in leaf_constraints so (amin, amax) reflects a shared leaf's k-multiplied target, then relax shapecurve.eligible's leaf_sharing/max_share guards accordingly (superpose/multi_use may need separate analysis -- check whether either changes the per-leaf target formula the same way share does, or a different one).","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-03T17:30:01Z","created_by":"Bruno Postle","updated_at":"2026-08-03T17:30:01Z","dependencies":[{"issue_id":"homemaker-py-tym","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-03T18:31:49Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} -{"_type":"issue","id":"homemaker-py-p6t","title":"Convergence-speed A/B for tiered comparator: evals to 0 hard fails, tiered vs flat","description":"homemaker-py-2g7.3 (DESIGN.md §37.1) validated that tiered search (-n_hard,-n_soft,fitness) reaches a strictly lower mean hard-fail count than flat (-n_fails,fitness) at a FIXED budget (20k evals) on harbor-house and maple-court. That measures fail composition at a snapshot, not time-to-solved. The natural follow-up: race the two comparators to '0 hard fails' (or a hard-fail floor) and compare evals/wall-clock to get there, ideally after 2g7.1/2g7.2 ground truth lands so there is a real target to race to instead of an arbitrary floor.","design":"Reuse experiments/tier_ab_2g7_3.py's harness; instead of a fixed budget, run until n_hard==0 or a budget cap, log evals-to-target per seed/scheme, same programmes (harbor-house, maple-court), same 3-seed protocol.","acceptance_criteria":"Report showing evals-to-0-hard-fails (or evals-to-floor) for tiered vs flat, both programmes, 3 seeds; verdict on whether tiering also wins on convergence speed, not just fixed-budget composition.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-02T17:51:38Z","created_by":"Bruno Postle","updated_at":"2026-08-02T17:51:38Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"homemaker-py-tym","title":"leaf_sharing/co_type target-adjustment modelling in shapecurve.leaf_constraints","description":"homemaker-py-6xh item 4 (DESIGN.md §37.2/§37.4). src/homemaker_layout/shapecurve.py's leaf_constraints() uses each leaf's own type's base (target, sigma) params only -- it does not model the leaf-sharing/co_type k-scaling (target*=k, sigma adjustment) that fitness.py's quality_size applies for shared/multi-use leaves. shapecurve.eligible() currently guards this by excluding any run with leaf_sharing/superpose/max_share/multi_use on, so the DP warm-start never fires for those runs -- but leaf_sharing defaults to True in driver.search(), so most real runs are excluded today. Needs: read fitness.py's actual k-scaling formula (quality_size's leaf-sharing branch) and mirror it in leaf_constraints so (amin, amax) reflects a shared leaf's k-multiplied target, then relax shapecurve.eligible's leaf_sharing/max_share guards accordingly (superpose/multi_use may need separate analysis -- check whether either changes the per-leaf target formula the same way share does, or a different one).","status":"closed","priority":3,"issue_type":"feature","assignee":"Claude","owner":"bruno@postle.net","created_at":"2026-08-03T17:30:01Z","created_by":"Bruno Postle","updated_at":"2026-08-29T21:36:44Z","started_at":"2026-08-29T21:22:42Z","closed_at":"2026-08-29T21:36:44Z","close_reason":"Done (DESIGN.md §38.23).\n\nleaf_constraints derived each leaf's feasible area from its own type's base\n(target, sigma). quality_size does not: a leaf holding k same-code rooms is\ncentred on k*target with sigma*k, and a co-typed leaf adds both codes' targets.\nThe DP modelled neither, so eligible() excluded leaf_sharing/max_share/multi_use\n-- and leaf_sharing defaults True in driver.search, so the guard excluded\nessentially every real run. The DP was correct and unreachable.\n\nWhy the guard could not just be dropped, measured before touching it: on 6\nharbor constructed seeds, 24 of 24 shared leaves (100%) have a real area OUTSIDE\nthe unscaled single-room bounds. Relaxing eligible without modelling k would\nhave made the DP call every one of those topologies infeasible -- false\nnegatives that prune feasible topologies and misdirect the NM warm-start. The\nguard was load-bearing.\n\nFix: mirror quality_size by asking the SAME Fitness object -- k =\ngraph.leaf_share(leaf, fit._max_share) when fit._leaf_sharing, then target*k /\nsigma*k, else fit._leaf_co_type for the additive case. Same object, same flags,\nsame branch order. Deliberately not re-derived: §39.5's cpsat._matches bug was a\nsolver optimising a relation the scorer had moved, and this is the same hazard\nclass.\n\nVerified as an EXACT inversion, not an approximation: for every shared leaf in a\nreal seed, quality_size evaluated at the DP's amin and amax returns\nFAIL_THRESHOLD to 1e-9 (k=3 n-leaf: bounds [128.50, 231.50], both 0.100000).\n\nsuperpose stays excluded, for a different reason than the others: it does not\nrescale a target, it changes WHICH TYPE the leaf is scored as, and that collapse\nhappens after the DP has read leaf.type. Bounding the wrong code is not fixable\nby a rescale.\n\nshapecurve_warmstart/shapecurve_prune remain default off, so no current run\nchanges -- including the cold-start baseline in progress. What changes is that\nthey are now applicable, which unblocks homemaker-py-v4s.\n\nThree new tests plus an updated legacy one (test_eligible_guards_sharing_not_\nstorey_count asserted the old guard; rewritten to the new contract rather than\ndeleted).\n","dependencies":[{"issue_id":"homemaker-py-tym","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-03T18:31:49Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"_type":"issue","id":"homemaker-py-p6t","title":"Convergence-speed A/B for tiered comparator: evals to 0 hard fails, tiered vs flat","description":"homemaker-py-2g7.3 (DESIGN.md §37.1) validated that tiered search (-n_hard,-n_soft,fitness) reaches a strictly lower mean hard-fail count than flat (-n_fails,fitness) at a FIXED budget (20k evals) on harbor-house and maple-court. That measures fail composition at a snapshot, not time-to-solved. The natural follow-up: race the two comparators to '0 hard fails' (or a hard-fail floor) and compare evals/wall-clock to get there, ideally after 2g7.1/2g7.2 ground truth lands so there is a real target to race to instead of an arbitrary floor.","design":"Reuse experiments/tier_ab_2g7_3.py's harness; instead of a fixed budget, run until n_hard==0 or a budget cap, log evals-to-target per seed/scheme, same programmes (harbor-house, maple-court), same 3-seed protocol.","acceptance_criteria":"Report showing evals-to-0-hard-fails (or evals-to-floor) for tiered vs flat, both programmes, 3 seeds; verdict on whether tiering also wins on convergence speed, not just fixed-budget composition.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-08-02T17:51:38Z","created_by":"Bruno Postle","updated_at":"2026-08-02T17:51:38Z","comments":[{"id":"01a04f66-b19a-7e1f-b14c-e6a592452e7d","issue_id":"homemaker-py-p6t","author":"Claude","text":"NOT RUNNABLE AS SPECIFIED -- checked before spending compute. Two blockers.\n\n1. THE TARGET IS UNREACHABLE ON THE PROGRAMMES IT NAMES. p6t races to \"0 hard\nfails\" on harbor-house and maple-court. Under the current objective:\n\n harbor evolved-3M-nols-3.dom (3M evals, our best-ever) 44 hard / 45 soft\n harbor coldstart-500000-s0.dom (partial run) 29 hard / 56 soft\n maple coldstart-500000-s0.dom (partial run) 36 hard / 90 soft\n programme-house coldstart-500000-s0.dom 0 hard / 1 soft\n\n0 hard has never been achieved on harbor or maple and is not close. Every arm\nwould hit the budget cap with the metric undefined -- a fully censored race\nproducing no data. Only programme-house reaches the target, and it is not in\np6t's protocol.\n\n2. THE 3-SEED PROTOCOL CANNOT RESOLVE ANYTHING. §38.21: harbor's paired sd is\n6.19 fails, so n=3 resolves nothing finer than ~15.4 fails, and 25% of 3-seed\nsubsets show a clean sweep by chance. p6t's design says \"same 3-seed protocol\"\n-- it would generate exactly the kind of verdict this log has spent the session\nretracting.\n\nREDESIGN NEEDED before this is worth running:\n - race to a REACHABLE target: best-known hard-fail count per programme, or\n \"evals to reach within k of it\", rather than an absolute 0;\n - size N from the observed sd via experiments/ab_report.py (homemaker-py-tco)\n and state the minimum detectable difference BEFORE running;\n - note the prerequisite p6t itself flags is still unmet: 2g7.1/2g7.2 ground\n truth would give a principled target instead of an arbitrary floor.\n\nLeft open -- the question is still worth answering, the protocol is not.","created_at":"2026-08-29T21:22:05Z"}],"dependency_count":0,"dependent_count":0,"comment_count":1} {"_type":"issue","id":"homemaker-py-2g7.10","title":"MAP-Elites archive over (hard-fail profile, leaf count, circulation fraction)","description":"Quality-diversity as the population-level answer to the §4.10 deceptive-valley problem: an archive keeps the elite per behavior niche, so 'transiently worse but structurally different' stepping stones survive — exactly what lex selection provably discards (§11.4's own analysis). DISTINCT from the failed §11.5/§11.8 niching: that kept diverse individuals under ONE selection pressure; MAP-Elites keeps the BEST individual per niche with no cross-niche competition. Descriptors to try: hard-fail category histogram (bucketed), total leaf count, circulation area fraction, storey balance. Emit from the existing genome.signature/score_with_grade machinery (kept default-off for exactly this reuse, §11.4 verdict). Blocked on the shape-curve DP: archive-filling needs cheap evals to be meaningful. Gate honestly per the ledger discipline: 3 seeds, control = current default stack.","acceptance_criteria":"A/B at equal budget (harbor+maple, 3 seeds): archive best hard-fails \u003c= default-stack best on mean; archive demonstrably contains the stepping stone for at least one accepted valley-crossing (traceable lineage)","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-02T09:16:00Z","created_by":"Bruno Postle","updated_at":"2026-08-02T09:16:00Z","dependencies":[{"issue_id":"homemaker-py-2g7.10","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-02T10:15:59Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-2g7.10","depends_on_id":"homemaker-py-2g7.4","type":"blocks","created_at":"2026-08-02T10:15:59Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-2g7.8","title":"LLM operator synthesis (AlphaEvolve-style): evolve mutation-operator code against the A/B harness","description":"Second LLM role, after the repair operator proves the plumbing: let the LLM propose new OPERATOR CODE (python functions with the mutate_* signature) and evaluate candidates with the exact experiment discipline DESIGN.md already enforces (control reproduces baseline, 3 seeds, 20k evals, verdict). The project's ledger of 20+ operator experiments with verdicts is unusually good few-shot material: feed it the §11-§13 history so it learns what already failed (niching, grading, annealing...) and why. Sandbox the generated code; acceptance purely empirical via the harness. This is compute-hungry — schedule after the shape-curve DP lands so each A/B is cheap.","acceptance_criteria":"one synthesized operator survives the standard 3-seed A/B gate on harbor or maple (mean fails strictly better, control reproduces baseline)","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-08-02T09:15:56Z","created_by":"Bruno Postle","updated_at":"2026-08-02T09:15:56Z","dependencies":[{"issue_id":"homemaker-py-2g7.8","depends_on_id":"homemaker-py-2g7.7","type":"blocks","created_at":"2026-08-02T10:15:56Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"homemaker-py-2g7.8","depends_on_id":"homemaker-py-2g7","type":"parent-child","created_at":"2026-08-02T10:15:56Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-pek","title":"fitness.py: delete the dead first process_storey definition (silently shadowed)","description":"Found by the homemaker-py-zrx expert review. class Fitness defines process_storey TWICE: the original gnw-scope version at fitness.py:1146 and the extended hgg version at fitness.py:1452. Python keeps only the second; the first ~45 lines are dead code that still reads as live. This is a silent-bug vector: an edit to the first definition (e.g. a fix to the covered-outside failure emission, which is duplicated verbatim in both) changes nothing at runtime and no test would notice. Delete the first definition (its docstring notes are preserved in the second). No behaviour change; run the suite to confirm 337 pass.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-08-02T08:19:56Z","created_by":"Bruno Postle","updated_at":"2026-08-05T15:57:28Z","started_at":"2026-08-05T14:43:30Z","closed_at":"2026-08-05T15:57:28Z","close_reason":"Deleted dead first process_storey definition; verified second is a strict superset; 405 tests pass (5 pre-existing unrelated failures confirmed present on main before this change)","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/DESIGN.md b/DESIGN.md index 160016a..941914a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -6082,6 +6082,54 @@ Wired into `experiments/ab_ssz_search.py`, which prints a power report per arm before its summary table. `rerun_1ph_protocol.sh` writes a TSV the CLI reads directly: `python experiments/ab_report.py off on`. +### 38.23 The shape-curve DP now models leaf-sharing, so it can fire on real runs (`homemaker-py-tym`) + +`shapecurve.leaf_constraints` derived each leaf's feasible area from its own +type's base `(target, sigma)`. `Fitness.quality_size` does not: for a leaf +holding `k` same-code rooms it centres the gaussian on `k × target` with +`sigma × k`, and for a co-typed leaf it adds the two codes' targets. The DP +modelled neither, so `shapecurve.eligible` excluded any run with +`leaf_sharing`/`max_share`/`multi_use` — and **`leaf_sharing` defaults `True` in +`driver.search`**, so the guard excluded essentially every real run. The DP was +correct and unreachable. + +**Why the guard could not simply be dropped.** On six harbor-house constructed +seeds, **24 of 24 shared leaves (100%)** have a real area outside the unscaled +single-room bounds. Relaxing `eligible` without modelling `k` would have made +the DP call every one of those topologies infeasible — false negatives that +prune feasible topologies and send the NM warm-start to a bad point. The guard +was load-bearing. + +**Fix: mirror `quality_size`, by asking the same `Fitness`.** `leaf_constraints` +now computes `k = graph.leaf_share(leaf, fit._max_share)` when `fit._leaf_sharing` +is set, applies `target·k, sigma·k`, and otherwise consults `fit._leaf_co_type` +for the additive co_type case — the same object, the same flags, the same +branch order as the evaluator. It deliberately does **not** re-derive the rule: +§39.5's `cpsat._matches` bug was exactly a solver optimising a relation the +scorer had since moved, and this is the same hazard class. + +Verified as an exact inversion, not an approximation: for every shared leaf in a +real seed, `quality_size` evaluated at the DP's `amin` and `amax` returns +`FAIL_THRESHOLD` to 1e-9. + +| leaf | type | k | DP amin | DP amax | q(amin) | q(amax) | +|---|---|---|---|---|---|---| +| `llrlr` | m | 3 | 17.12 | 42.88 | 0.100000 | 0.100000 | +| `lrlr` | n | 3 | 128.50 | 231.50 | 0.100000 | 0.100000 | +| `rlrrl` | of | 2 | 14.27 | 35.73 | 0.100000 | 0.100000 | +| `rrlr` | t | 3 | 8.34 | 27.66 | 0.100000 | 0.100000 | + +**`superpose` stays excluded, and for a different reason than the rest.** The +others rescale a leaf's target; superposition changes *which type the leaf is +scored as*, and the collapse happens inside evaluation, after the DP has read +`leaf.type`. Bounding the wrong code is not something a rescale can fix — that +needs the collapse modelled. + +`shapecurve_warmstart`/`shapecurve_prune` remain default **off**, so this +changes no current run. What it changes is that they are now *applicable*: +`homemaker-py-v4s`'s A/B, which its own issue said to defer until this landed, +is unblocked. + ## 39. Config audit: requirements that actively fight the engine (`homemaker-py-ju3`) — measured 2026-08-25 The corpus `patterns.config` targets and `costs.config` values were estimated diff --git a/src/homemaker_layout/shapecurve.py b/src/homemaker_layout/shapecurve.py index c1b5e61..2f588c7 100644 --- a/src/homemaker_layout/shapecurve.py +++ b/src/homemaker_layout/shapecurve.py @@ -77,6 +77,7 @@ import numpy as np from homemaker_layout import dom as dom_mod from homemaker_layout import geometry +from homemaker_layout import graph as _graph # sqrt(2*ln(10)): FAIL_THRESHOLD=0.1 inversion of a unit-height Gaussian, # gaussian(x,1,target,sigma) >= 0.1 <=> |x-target| <= K*sigma. @@ -91,11 +92,23 @@ def eligible(root: dom_mod.Node, leaf_sharing: bool = False, """Is ``root`` inside this DP's validated scope for ``solve``? Any storey count (homemaker-py-koo — ``solve``/``is_feasible`` handle - ``below``-inherited wall-stacking directly, see the module docstring) but - none of ``leaf_sharing``/``superpose``/``max_share``/``multi_use`` (none - of which ``leaf_constraints`` models). + ``below``-inherited wall-stacking directly, see the module docstring). + + homemaker-py-tym: ``leaf_sharing``/``max_share``/``multi_use`` are now IN + scope — ``leaf_constraints`` mirrors ``quality_size``'s k-scaling and + co_type adjustment for them, reading the evaluator's own ``fit`` so it + cannot diverge. This matters because ``leaf_sharing`` defaults True in + ``driver.search``, so the guard previously excluded most real runs and the + DP never fired where it would actually be used. + + ``superpose`` stays OUT of scope, and for a different reason than the + others: it does not adjust a leaf's target, it changes WHICH TYPE the leaf + is scored as. The collapse happens inside the evaluation, after the DP has + already read ``leaf.type``, so the DP would be bounding the wrong code. + That needs the collapse modelled, not a target rescaled. """ - return not leaf_sharing and not superpose and max_share is None and not multi_use + del leaf_sharing, max_share, multi_use # modelled; kept for call-site clarity + return not superpose def _interval_add(a: Interval, b: Interval) -> Interval: @@ -153,7 +166,13 @@ def leaf_constraints(fit, leaf: dom_mod.Node) -> LeafBounds: Mirrors the branching of ``Fitness.quality_size``/``quality_width``/ ``quality_proportion`` (fitness.py) but returns the (target, sigma)-derived hard bounds instead of evaluating a Gaussian against actual geometry. - Ignores leaf-sharing/co_type target adjustment (see module docstring). + + homemaker-py-tym: the size bound now also mirrors ``quality_size``'s + leaf-sharing / co_type adjustment. It does so by asking the SAME ``fit`` + object the evaluator uses, rather than re-deriving the rule, so the DP + cannot drift from the objective it is meant to predict -- the failure mode + that made §39.5's `cpsat._matches` optimise a different relation than the + scorer checked. """ # §39.4: classify by the GENERIC type set, mirroring get_space_params -- # a programme code takes its declared params whatever letter it starts with. @@ -169,11 +188,24 @@ def leaf_constraints(fit, leaf: dom_mod.Node) -> LeafBounds: params = fit.conf("size_circulation") if t0 == "c" else fit.get_space_params(leaf.type, "size") target, sigma = params[0], params[1] # NB: quality_size's ``target > 0`` gate governs only the leaf-sharing/ - # co_type k-scaling of (target, sigma) (not modelled here, see module - # docstring) -- the underlying gaussian(area, target, sigma) test - # always applies, including target==0 (e.g. size_circulation's [0.0, - # 14.0] default: a real one-sided "as small as possible" constraint, - # not "unconstrained"). + # co_type k-scaling of (target, sigma) -- the underlying + # gaussian(area, target, sigma) test always applies, including + # target==0 (e.g. size_circulation's [0.0, 14.0] default: a real + # one-sided "as small as possible" constraint, not "unconstrained"). + if t0 != "c" and target > 0: + # homemaker-py-tym: mirror quality_size exactly. A shared leaf holds + # k same-code rooms, so the gaussian is centred on k*target with + # sigma*k (fractional tolerance preserved); a co_typed leaf serves + # two codes' requirements additively. A leaf never carries both -- + # construction never stamps both, and quality_size encodes that by + # only consulting co_type when k == 1. + k = _graph.leaf_share(leaf, fit._max_share) if fit._leaf_sharing else 1 + co_type = None if k > 1 else fit._leaf_co_type(leaf) + if k > 1: + target, sigma = target * k, sigma * k + elif co_type: + co = fit.get_space_params(co_type, "size") + target, sigma = target + co[0], sigma + co[1] amin, amax = max(0.0, target - _K * sigma), target + _K * sigma # --- width -> wmin --- diff --git a/tests/test_shapecurve.py b/tests/test_shapecurve.py index 643b695..dff5a67 100644 --- a/tests/test_shapecurve.py +++ b/tests/test_shapecurve.py @@ -72,23 +72,33 @@ def _two_storey_mixed_topology(child_types=("O", "O")): return level0, target -def test_eligible_guards_sharing_not_storey_count(): - """homemaker-py-koo: multi-storey is now handled (below-inherited fixed - splits, DESIGN.md §37.6), so ``eligible`` only guards the still-unmodelled - leaf_sharing/superpose/max_share/multi_use family.""" +def test_eligible_guards_superpose_not_storey_count_or_sharing(): + """`eligible` guards only what `leaf_constraints` cannot model. + + homemaker-py-koo removed the storey-count guard (below-inherited fixed + splits, §37.6). homemaker-py-tym removed the leaf_sharing/max_share/ + multi_use guards by MODELLING them: `leaf_constraints` now mirrors + `quality_size`'s k-scaling and co_type adjustment, reading the evaluator's + own Fitness so it cannot drift. + + `superpose` remains excluded, for a different reason than the others: it + does not rescale a target, it changes WHICH TYPE the leaf is scored as, and + that collapse happens after the DP has read `leaf.type`. + """ root = dom.load(str(HARBOR_L0 / "generated.dom")) assert len(dom.levels(root)) == 1 assert shapecurve.eligible(root) - assert not shapecurve.eligible(root, leaf_sharing=True) + assert shapecurve.eligible(root, leaf_sharing=True) + assert shapecurve.eligible(root, max_share=3) + assert shapecurve.eligible(root, multi_use=True) assert not shapecurve.eligible(root, superpose=True) - assert not shapecurve.eligible(root, max_share=3) - assert not shapecurve.eligible(root, multi_use=True) seed = dom.load(str(HARBOR_L0 / "init.dom")) seed.above = dom.Node(rotation=0) # fake a second storey assert len(dom.levels(seed)) == 2 assert shapecurve.eligible(seed) - assert not shapecurve.eligible(seed, leaf_sharing=True) + assert shapecurve.eligible(seed, leaf_sharing=True) + assert not shapecurve.eligible(seed, superpose=True) def test_solve_feasible_root_realises_zero_shape_fails(tmp_path): @@ -264,3 +274,99 @@ def test_is_feasible_multistorey_never_writes(): assert shapecurve.is_feasible(infeasible_root, fit) is False after = [tuple(b.division) for b in solver.free_branches(infeasible_root)] assert before == after + + +# --------------------------------------------------------------------------- # +# homemaker-py-tym / DESIGN.md §38.23 — leaf_sharing / co_type target modelling +# --------------------------------------------------------------------------- # +def _shared_seed(): + """A real harbor-house constructed seed, which stamps share>1 leaves.""" + from homemaker_layout import geometry, operators, programme + + d = "examples/harbor-house" + reqs = programme.load_programme_dir(d) + conf, cost = fit_mod.load_config(d, overrides={"leaf_sharing": True}) + fit = fit_mod.Fitness(conf, cost) + root = operators.constructive_topology( + dom.load(f"{d}/init.dom"), reqs, np.random.default_rng(0), + sorted(reqs) + ["C", "O"], min_storeys=programme.storey_minimum(d), + adjacency_aware=True, proportion_aware=True, circ_divisor=3, + leaf_sharing=True, leaf_share_factor=3, depth_balanced=True, + interior_outside=True, outside_divisor=3) + geometry.clear_cache() + dom.canonicalize_shares(root) + return fit, root + + +@pytest.mark.skipif(not Path("examples/harbor-house").is_dir(), + reason="harbor-house not available") +def test_leaf_constraints_inverts_quality_size_for_shared_leaves(): + """The DP's (amin, amax) must be the exact FAIL_THRESHOLD inversion of + quality_size -- INCLUDING its k-scaling for a shared leaf. + + quality_size centres the gaussian on k*target with sigma*k for a leaf + holding k same-code rooms. leaf_constraints ignored that, which is why + `eligible` excluded leaf_sharing outright -- and leaf_sharing defaults True + in driver.search, so the DP never fired on a real run. + """ + from homemaker_layout import geometry + + fit, root = _shared_seed() + shared = [lf for lf in root.leaves() if (getattr(lf, "share", 1) or 1) > 1] + assert shared, "seed carries no shared leaves -- test would be vacuous" + + orig_area = geometry.area + try: + for lf in shared: + b = shapecurve.leaf_constraints(fit, lf) + for bound in (b.amin, b.amax): + geometry.area = lambda _n, _a=bound: _a + assert fit.quality_size(lf) == pytest.approx( + fit_mod.FAIL_THRESHOLD, abs=1e-9), ( + f"leaf {lf.id} (share={lf.share}): DP bound {bound} is not " + f"on the fail threshold of quality_size") + finally: + geometry.area = orig_area + + +@pytest.mark.skipif(not Path("examples/harbor-house").is_dir(), + reason="harbor-house not available") +def test_unscaled_bounds_would_reject_every_shared_leaf(): + """Guard the reason `eligible` could not simply be relaxed. + + Without the k-scaling, a shared leaf's real area sits far outside the + single-room bounds, so the DP would call a feasible topology infeasible -- + a false negative that prunes good topologies and misdirects the NM + warm-start. Measured: 100% of shared leaves, 6 seeds. + """ + from homemaker_layout import geometry + + fit, root = _shared_seed() + K = shapecurve._K + checked = would_reject = 0 + for lf in root.leaves(): + if (getattr(lf, "share", 1) or 1) <= 1 or lf.type not in (fit._programme or {}): + continue + checked += 1 + area = geometry.area(lf) + t, sg = fit.get_space_params(lf.type, "size")[:2] + b = shapecurve.leaf_constraints(fit, lf) + assert b.amin <= area <= b.amax, "scaled bounds should accept the real area" + if not (max(0.0, t - K * sg) <= area <= t + K * sg): + would_reject += 1 + assert checked, "no shared leaves -- test would be vacuous" + assert would_reject == checked, ( + f"expected the unscaled bounds to reject every shared leaf; " + f"{would_reject}/{checked}") + + +def test_eligible_admits_sharing_but_still_excludes_superpose(): + """superpose is excluded for a DIFFERENT reason than the others: it does + not rescale a target, it changes which TYPE is scored, and the collapse + happens after the DP has read leaf.type.""" + assert shapecurve.eligible(None, leaf_sharing=True) + assert shapecurve.eligible(None, max_share=3) + assert shapecurve.eligible(None, multi_use=True) + assert shapecurve.eligible(None, leaf_sharing=True, max_share=4, multi_use=True) + assert not shapecurve.eligible(None, superpose=True) + assert not shapecurve.eligible(None, leaf_sharing=True, superpose=True)