Splits the flat outer-search comparator (-n_fails, fitness) into a tiered
(-n_hard, -n_soft, fitness) so search budget stops being spent polishing
SOFT shape fails (crinkliness/proportion/size/width/edge-too-long/
staircase-volume) while HARD structural fails (missing space, wrong/
required level, level/circulation/vertical connectivity, adjacency,
stairs, covered-outside, storey limits, public access) remain unfixed.
fitness.classify_fail_tier/tier_counts classify every fail string emitted
across fitness.py and graph.py, raising on anything unrecognised so new
fail sites must declare a tier. Validated against all real fail strings in
the checked-in corpus plus every fail-emission call site read from source.
driver.Individual gains n_hard/n_soft (populated from innerloop.Result.
fail_lines); search(use_tiers=...) swaps the comparator when set (default
off, so existing runs are unaffected — inner-loop 0.5^n cliff untouched).
evolve.py exposes --use-tiers / HOMEMAKER_USE_TIERS.
experiments/tier_ab_2g7_3.py runs the acceptance A/B (harbor+maple, 3
seeds, 20k evals) in the background; results pending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSwQwpEaHFBkeVSDDWd75S
Builds path (b) from §26 -- a leaf permanently serving two DIFFERENT
compatible programme codes at once, extending leaf-sharing's same-code
multiplicity mechanism to different-but-compatible codes. Architect-declared
`co_locate` pairs (validated against interchangeable()'s S1-S4 bounds, no
transitive closure so the b3v chain problem can't recur), threaded through
graph.py's checks via a new leaf_codes() resolver and fitness.py's quality
terms (additive size, stricter-of-both width/proportion). Construction-time
only, gated behind `multi_use` (default OFF, bit-identical when off).
End-to-end A/B (20k evals x 3 seeds x 2 programmes) came back net negative:
harbor-house -4.0% but health-centre +24.5% worse (3/3 seeds), because
fusing different codes' shape targets via stricter-of-both can impose a
tighter joint constraint than either code needed alone, which the tightly-
packed health-centre programme can't absorb. Written up as DESIGN.md §33;
multi_use stays default OFF, no default-flip recommended.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8agJBT2ZpmF3ErW7wi2wY
operators._assign_adjacency_aware gains beam_width (default 1 = exact
prior greedy behaviour), threaded through constructive_topology/
lift_base_to_storeys/driver.search/search_staged as
construction_beam_width. Verified functioning on an adversarial
synthetic case, but byte-identical raw-seed output to greedy at every
width tested (1/4/8/20) on both example programmes -- no headroom for
the beam to find on this repo's programmes. DESIGN.md section 29.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8agJBT2ZpmF3ErW7wi2wY
Adds operators.mutate_ruin_recreate: un-divides one wing of a storey and
rebuilds it with the same adjacency-aware constructor the seeders use
(_assign_adjacency_aware, generalised with a new `scope` param), seeded
from the surviving circulation bordering the wing. Gated behind
enable_ruin_recreate (default off) / --ruin-recreate, same pattern as
reassociate/bridge_circulation.
A/B (qpk protocol, DESIGN.md §23): initial uniform-weight run was
underpowered (fired ~1/32 children), null. A weight=3.0 follow-up
(_MUTATION_WEIGHTS["ruin_recreate"]) showed a statistically significant
win on programme-house across 15 seeds (8W/1L/6T, mean fails 7.07->6.00,
Wilcoxon p=0.041) but no consistent effect on harbor-house across 8 seeds
(3W/2L/3T). Kept default off pending a size-threshold follow-up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Combined follow-up to 8sh (DESIGN.md §22): raised bridge_circulation's
_MUTATION_WEIGHTS entry to 2.0 (lj3) and re-ran the qi6/qpk-protocol A/B
at 4x the sample size (qjg) in one sweep, since the two variables were
confounded if tested separately. Result is null in the opposite direction
from 8sh's small-N signal -- no total-fail benefit (p=0.71 programme-house
N=20, p=0.69 harbor-house N=12) and a higher rate of trajectory-divergence
-induced new not-connected fails than at the original uniform weight.
Reverted the weight bump; enable_bridge_circulation stays default off.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDZjAATDWW1xFfc7xnJqSt
Adds operators.mutate_bridge_circulation: retypes the cheapest path between
two disconnected circulation components to circulation, directly clearing a
'level N not connected' fail instead of relying on the qi6 graded comparator
key (measured negative, DESIGN.md §18). Gated off by default via
driver.search's enable_bridge_circulation flag and evolve.py
--bridge-circulation, mirroring enable_reassociate's clean-toggle pattern.
qi6/qpk-protocol A/B (DESIGN.md §21) is directionally positive but mixed at
N=3/N=5 (never worse on total fails; clears 2/5 baseline not-connected fails
vs qi6's 0/4; one seed's RNG-trajectory divergence adds 2 new not-connected
fails) — kept default off pending a larger-N confirmation sweep
(homemaker-py-qjg) and a mutation-weight bump experiment (homemaker-py-lj3).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDZjAATDWW1xFfc7xnJqSt
20-seed programme-house sweep (vs the original 5) resolves the qpk A/B's
mixed 3/5 result as small-sample noise around a true small positive: mean
fails 7.95->7.10 (~10.7%), 11W/6L/3T, paired t-test p~0.028. Flips
collapse_insearch's default from OFF to ON in evolve.py and driver.py
(_overrides_for/_fitness_for/_evaluate/search/polish_finish); opt out with
--no-collapse-insearch. fitness.Fitness itself is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDZjAATDWW1xFfc7xnJqSt
driver.search()/search_staged() gain enable_shape_repair (default off),
mirroring the enable_reassociate clean-toggle pattern: only builds a
fitness.Fitness instance and passes it to operators.mutate() when
enabled, so shape_rotate/deslim (7fm) can actually be selected mid-GA
instead of always no-opping on fit=None.
Full A/B sweep (harbor-house, budget=1M, 4 seeds) shows no improvement:
mean fails 14.50 (off) vs 14.75 (on), within seed noise. Confirms 7fm's
finish-time finding at in-search scale — these operators don't rescue
harbor-house's residual fails even with GA selection pressure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDZjAATDWW1xFfc7xnJqSt
Runs the 94g finish-time cell↔room collapse inside every fitness eval
(collapse_insearch conf flag, default off, bit-identical when off) instead
of once at the end, so search optimises the collapsed objective directly.
Plumbed through fitness.py/driver.py/evolve.py the same way superpose/
conn_grade are; --collapse-insearch CLI flag.
A/B validated against the xi7 protocol (equal budget, both arms finished
with standard finish-time --collapse): POSITIVE, opposite of the 9o5/xi7
prior. harbor-house ON wins 3/3 (mean fails 80.3->72.0); programme-house
mixed 3/5 (mean fails 8.4->7.8). Kept default off pending a larger
programme-house sample; documented as a working opt-in for harbor-house-
scale-or-larger programmes. Full writeup in DESIGN.md §20.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The dominant post-collapse fail is the binary "level N not connected",
which is flat across fragmentation (a 7-component storey scores the same
as a 2-component one), so the outer search has no gradient toward
connected circulation. A finish-time convert-to-circulation repair was
prototyped and measured NEGATIVE (195->560 fails: bridging needed rooms
costs more missing-room fails than the one binary fail it clears).
Instead add graph.circulation_connectivity(G) = largest-circ-component
fraction, summed over storeys onto the score_with_grade proximity channel
(conf flag conn_grade; replaces the §11.4 leaf-grade there). It is a
secondary comparator key only — scalar fitness and fail count stay
byte-identical — restoring the gradient the binary fail lacks. Threaded
through driver (_overrides_for/_fitness_for/_evaluate/search; enabling it
implies the grade key) and evolve --conn-grade (default off).
A/B on full-budget runs pending; short smoke run confirms plumbing.
Tests: tests/test_conn_grade.py x9 (fraction contract, non-circ ignored,
monotone under (dis)connection, score/fail invariance); 276 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
Public-access term (preserve_public_access, default on): when the building's
only street access is an l/k ROOM neighbour of a public outside leaf (no
circulation fallback — an existential building-level check the per-leaf
objective can't see), that leaf is pinned (kept, its demand slot decremented)
so the collapse can't drop "no outside public access". Best layout 15→13
becomes 15→12 with zero new fails; sweep total 172→171, still monotone.
collapse_finish(root, **kw) -> (tree, base, coll, applied): keep-better wrapper,
scores on throwaway copies (score_with_fails merges in place), returns the
collapse only if fails don't increase.
Wiring: driver.collapse_best updates result.best (lineage +collapse, canonical
re-score); evolve.py runs it after the sharing polish behind --collapse/
--no-collapse (default on). New homemaker-collapse CLI (collapse_cmd.py) applies
it to an existing .dom, writing <stem>.collapsed.dom.
tests/test_collapse_global.py: demand-set relabel, level hard constraint, c/o/s
exclusion, no-op safety, keep-better/unmerged. 267 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
Ramp the leaf-share grain down within one continuous run (e.g. 4->3->2->off),
carrying the whole population across each step — graduated non-convexity over
the single hard sharing->off transition of the §15 finish.
- operators.unfold_shared_leaves(above=cap): unfold only leaves whose share
exceeds the new grain cap, leaving smaller-share leaves collapsed for the
next step. above=1 (default) keeps the full-unfold §15 behaviour.
- driver: max_share override threaded through _overrides_for/_fitness_for/
_evaluate so a phase can rebuild the evaluator at a lower leaf_share_max cap;
search(seed_pop=) evaluates an explicit initial population so a phase hands
its whole population to the next instead of restarting from a single best.
- driver.search_annealed: one phase per descending grain then a de-share
polish; unfold-above-cap between steps; cumulative accounting + grain-tagged
history; honest canonical best (byte-for-byte verified vs homemaker-fitness).
- evolve: --anneal-grain LADDER CLI (self-finishing; §15 finish not applied).
8iv settled the primitive (grid unfold beat the circulation-aware slice), so
the ramp reuses the plain balanced-grid unfold at every step.
Tests: unfold above-cap selectivity, seed_pop seeding, search_annealed phase
stitching / honest finish / degenerate-ladder fallback. 258 pass. DESIGN §16.
Head-to-head A/B on harbor-house still to run; verdict pending (issue open).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
Leaf-sharing evolve runs optimised a sharing-credited objective (a shared
leaf of code X with share=k counts as k programme rooms, size re-centred on
k*target) but wrote the un-materialised genome, which the canonical
homemaker-fitness rates catastrophically worse (harbor-house: internal
1.03e-05 vs canonical 6.73e-29, 15 critical missing-room fails).
Fix: before write, driver.polish_finish() unfolds every live shared leaf
into k distinct rooms (operators.unfold_shared_leaves — pays down the count
deficit) then warm-starts a leaf_sharing=False polish search from the
unfolded genome so the materialised rooms get proportion/width/size cleanup.
Returned best.fitness is the canonical score (leaf_sharing off => internal
== canonical). This is yaa's proven unfold-then-polish path, made automatic.
evolve.py: new --polish-budget (env HOMEMAKER_POLISH_BUDGET; -1=auto=
budget//2, 0=unfold+rescore only). Interrupt forces polish_budget=0 for a
fast honest output. Default stays --leaf-sharing on (its topology-search
speed retained; output made honest by the finish). Schedule B in-run
annealing remains homemaker-py-kpu.
Verified (harbor-house 3000+1500): reported polish fitness 4.79788e-27
matches canonical homemaker-fitness exactly, 0 critical fails. Tests: 254
pass (+3 polish_finish).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8566xAxTnwtJTkpXjYNZm
Interchangeable codes (similar size/width/proportion, compatible level/stack,
no adjacency edge) form equivalence classes derived from the programme. With
--superpose (default off), each fitness eval COLLAPSES every superposed leaf to
its best in-class usage via an optimal supply->demand assignment (brute force
<=C! within cap C=4, scipy Hungarian beyond), then scores the condensed types.
Because collapse re-types on the unmerged tree before all checks, counts /
adjacency / quality are unchanged downstream -- no Node field, no graph/operator
changes -- and default OFF is bit-identical.
- programme.py: derive_interchange_classes + interchangeable (S1-S4, locked
thresholds R_SIZE=1.5/R_WIDTH=1.3/R_PROP=1.5, CLASS_CAP=4)
- fitness.py: collapse_superposition, _best_assignment, _usage_quality;
superpose/superpose_class_cap conf knobs; collapse hooked into _evaluate_full
- driver.py/evolve.py: superpose flag plumbed beside leaf_sharing; --superpose
- tests/test_superposition.py: 17 tests (derivation, assignment, end-to-end)
Closes homemaker-py-9o5 (build); validation A/B is homemaker-py-xi7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expose tournament_k (default 2) on search()/search_staged(), threaded into
both _tournament call sites and the staged path's internal search() calls;
HOMEMAKER_TOURNAMENT_K env knob in the scaled/staged harnesses; run_6zy_ab.sh
joint niche×k grid (RESUME-able).
Result (negative, acceptable): no (niche,k) cell beats the legacy (off,k=2)
baseline. Blank-slate programme-house (5 seeds) baseline mean 4.80 fails is the
best of the 6-cell grid; every k>2 and every niche=on cell is 6.0-7.0. Niching
bites (pop_distinct 16/16 vs 4-11) but sharper pressure does not convert it to
lower fails — §11.5 'diffuses effort' null is robust to selection pressure;
plateau stays reachability-bound (confirms §11.4/§11.5).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prime a population from N independent converged elites + crossover-heavy
migration phase, vs best-of-N at equal total budget. Island does NOT win:
harbor 68 vs control 67 (within parallel noise), maple 124 vs control 116
(decisive). Default-off child_probe hook on driver.search instruments the
deciding mechanism: area-matched crossover across independently-converged
elites rarely synthesizes (1/65 harbor, 3/63 maple beat the better parent,
max fail-drop 2-5), confirming the alignment hypothesis (non-canonical 9gp
encoding -> disruptive splice). Search-machinery null #3; residual stays
geometry/shape-bound. 233 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
§13.6/ld2 verdict: interior-O light-well seeding is net-positive — harbor
-16.4% (all seeds improve), maple net-neutral (-2.8% mean, no programme
regresses). Mirror the pll bal+share flip: default interior_outside
False->True in driver.search/search_staged and operators.constructive_topology/
lift_base_to_storeys (outside_divisor stays 3). The experiments INTERIORO
A/B override is unchanged. test_interior_outside_… now pins the peripheral
baseline to interior_outside=False explicitly. 215 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seed O as interior light wells (most-landlocked leaves first, count scaled
by room count via outside_divisor) instead of one peripheral O, attacking the
erc crinkliness residual: seed diagnostic confirms every crinkliness fail is
under-exposed (landlocked), none over-exposed.
A/B (20k evals, seeds 0/1/2, bal+share stack, §13.6): control reproduces §13.5;
interior odiv=3 gives harbor -16.4% (all seeds improve) and maple -2.8%
(net-neutral). Default-optimal divisor 3 found by seed sweep (6 was null).
Lever default OFF; default-ON flip tracked as erc.8.
- operators: interior_outside + outside_divisor through constructive_topology,
lift_base_to_storeys, _assign_adjacency_aware (fix n_circ budget for >1 O)
- driver.search/search_staged threading; run_staged_search.py INTERIORO/ODIV env
- test_interior_outside_seeds_landlocked_wells_and_scales_count
- experiments/run_interioro_ab.sh; DESIGN.md §13.6
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
erc.7/§13.5 verdict: depth_balanced + leaf_sharing (factor 3) is the
winning Phase-8 stack. Flip the three knobs to default-on so
homemaker-evolve inherits them; env-var A/B overrides (DEPTHBAL/
LEAFSHARE/LEAFSHAREFAC) unchanged. 214 tests pass, no snapshot churn.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_grow_leaves grew a random caterpillar, so equal-target rooms landed at
wildly different binary-tree depths — the depth-driven size maldistribution
Diagnostic B (§13.2) localized (same code at 0.05x and 14.7x target). The
depth_balanced flag always splits a shallowest leaf instead, growing a
near-complete tree so the proportion-aware sizing pass hits each target with
cut fractions near their proportional value.
Floor probe (diag_depth_balance.py): depth spread collapses 7->1, the giant
ratio falls (maxR 12->8 harbor / 16->6 maple), %undersize 54->25 / 42->22,
and the achievable floor drops -12% harbor / -11% maple at EQUAL leaf count.
Additive with leaf-sharing (bal+sh3 beats §13.3 share3-alone). Default OFF,
214 tests pass; threaded through driver.search/search_staged and exposed via
DEPTHBAL in run_staged_search.py. End-to-end 20k A/B running.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the area-derived share recovery with explicit, type-guarded per-leaf
multiplicity: construction stamps leaf.share=k and leaf.share_type=code; the
fitness (graph.leaf_share) honours k only while leaf.type==share_type, so any
retype/undivide auto-invalidates a stale share — no operator resets, and a
small leaf cannot retype its way into covering rooms it does not provide. Two
Node fields survive the whole search via deepcopy (genome.decode is unused in
the hot path); .dom emits `share` only on a live shared leaf.
This closes the §13.3 missing-fail leak: floor probe missing 17–44 → 0, and the
achievable floor drops −39% harbor (120.3→73.3) / −32% maple (194.7→133.0) with
no re-emergence as size fails.
Flag threaded through driver.search/search_staged → constructive_topology /
lift_base_to_storeys, exposed via LEAFSHARE/LEAFSHAREFAC in run_staged_search.py
(injects the objective into inner-loop + final-score fitness so both A/B arms
share one programme dir). run_leafshare_ab.sh runs the staged 20k A/B.
Smoke-tested end-to-end (harbor, factor 3, re-score OK). 214 tests pass;
default-OFF reproduces baseline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The constructive seeder was never nondeterministic: _assign_adjacency_aware
ends every max/min with a unique leaf-idx tiebreak and uses set unions only
for membership, so iteration order never leaks. constructive_topology(seed=0)
is byte-identical across processes for every example programme. The cited
"sig 4480 vs 16064" was a measurement artifact — Python's builtin hash() of a
str is salted per process (PYTHONHASHSEED), so an identical signature hashes to
different ints run-to-run.
The real run-to-run noise was parallel-only: driver._run_batch admitted futures
via as_completed (completion order), and admit() is order-sensitive (accrues
n_evals per result; keeps the first individual of an equal-key tie as best). A
long parallel run diverged 167 vs 161 fails (maple seed 0). Fix: admit futures
in submission order (block on each result in turn; all still run concurrently),
reproducing the serial admission sequence. Two workers=4 runs are now
byte-identical. Serial (workers=1) was already byte-for-byte reproducible.
Per-seed numbers are reproducible only at a fixed worker count; serial != parallel
is expected (children/iteration 1 vs n_workers changes batch granularity).
- driver: iterate futs in submission order, not as_completed
- test: test_search_parallel_is_reproducible (fails on pre-fix, passes on fix)
- DESIGN.md §12.4: corrected the reproducibility note
Closes homemaker-py-xcy
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Threads circ_divisor (default 3 = unchanged) through
operators.constructive_topology/lift_base_to_storeys and
driver.search/search_staged; env CIRCDIV in run_staged_search.py. Adds
experiments/run_c3g_ab.sh.
Motivation (DESIGN.md §12.3 diagnostic): the maple shape residual is
over-granular construction (73 small leaves -> crinkliness+size). Cheap raw-seed
probe: a coarser spine lowers the SHAPE floor (maple 135->110, harbor 83->66)
but raises access/adjacency, leaving the raw TOTAL floor flat-to-worse. Because
§12.3 showed shape is the HARD residual and access/adjacency are cheap to
repair, only an end-to-end A/B settles whether trading them pays — this is the
plumbing for that run. Tests green (default path byte-identical).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Land the two evidence-supported parts of the re-scoped 9gp capstone as
operators on the existing decoded Node tree (no Polish-expression rewrite),
each default-OFF and measured against the §12.2 leu.2 baseline.
9gp.1 shape-feasibility pre-filter: operators.predicted_shape_fails lays a
topology out at its proportion-aware target geometry and counts shape fails
(size/width/proportion/crinkliness); driver._evaluate prunes clearly-infeasible
topologies before the inner loop (1 eval vs ~80), guarded so nothing that could
beat the incumbent is discarded. search/search_staged feasibility_filter,
feasibility_max_shape_fails (env FEAS/MAXSHAPE), default OFF.
9gp.2 M3 Wong-Liu reassociate: operators.mutate_reassociate adds associativity
(a|b)|c <-> a|(b|c) on same-orientation live cuts — the canonical-slicing move
missing from swap(M1)/rotate(M2), attacking the §11.4/§11.5 reachability
bottleneck. enable_reassociate (env REASSOC), default OFF (weight 0 -> baseline
byte-identical).
Unit tests (operators + driver) green, full suite 211 passed; maple-court smoke
run clean under native fitness. A/B sweep handed off per the plan; DESIGN.md
§12.3 documents the design and the pending measurement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Size each constructive-seed cut from leaf TARGET areas (division=[f,f] gives
left area-fraction f) and pick each cut's rotation for child squareness — both
derived from target dims, topology/type assignment untouched. Area-only
regressed (slivers); rotation choice is what makes it pay.
End-to-end (20000 evals, 3 seeds, staged): harbor 85.3->74.0 (-13%, best 69),
maple-court 151.7->136.0 (-10%, best 126). PROP=0 reproduces the §11.7/§12.1
baselines exactly. programme-house regresses at fixed budget (deeper local
optimum walls off the undivide restructuring path) but a budget sweep shows
it's convergence speed, not a worse asymptote (PROP=1 reaches 1 fail at 150k).
Default-on (seed_proportion_aware=True, env PROP=1).
cq1: n_storeys now honours storey_minimum, not just level: keys — programme-house
(storey_minimum:2, all rooms level:0) was seeded one storey short and fell
through to plain search. New programme.storey_minimum()/n_storeys_for();
driver.search passes min_storeys to the seeder; search_staged routes on the max.
No-op for harbor/maple; programme-house single-stage 8.0->5.0.
New maple-court best (126) saved as generated.dom. 204 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_assign_adjacency_aware gains fixed_circ (seed the connected-dominating-set from
given circulation leaves) and secondary-adjacency-aware room placement: codes
with the most non-c adjacency requirements are placed first, each onto the open
slot satisfying the most of its requirements against already-typed neighbours
(clustering k1<->da1, da1<->o). lift_base_to_storeys(reqs, adjacency_aware=True)
grows the upper-floor circulation spine off the inherited vertical core and
assigns rooms around it; threaded through driver.search_staged
(seed_adjacency_aware) and run_staged_search.py (ADJ env).
End-to-end staged harbor, 20000 evals, mean total fails over 3 seeds:
ADJ=0 99.0 (reproduces the §11.4 staged lex baseline exactly), ADJ=1 85.3
(-13.7, -14%; best 78). New best harbor configuration overall: staged baseline
99.0 -> single-stage adjacency-aware (§11.6) 90.7 -> staged + adjacency-aware
lift 85.3. Staging and adjacency-aware seeding compose.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
operators._assign_adjacency_aware spends ~one extra leaf per three rooms on a
greedy connected-dominating-set of circulation leaves (read from the geometric
leaf_graph, type-independent), so every room borders a connected circulation
spine and adjacency-to-c + access are satisfied by construction. Default-on via
constructive_topology(adjacency_aware=True), threaded through
driver.search(seed_adjacency_aware) and run_search_scaled.py (ADJ env).
End-to-end single-stage, 20000 evals, mean total fails over 3 seeds:
harbor 110.0 -> 90.7 (-17.5%; ADJ=0 reproduces the §11.2 105 baseline exactly),
programme-house 12.3 -> 9.3 (-24%). Adjacency-aware single-stage harbor (mean
90.7, best 85) beats the §11.3 staged best of 95 — the first Phase-6 fail-count
reduction from seeding. Follow-ups (lift_base_to_storeys, secondary adjacencies)
filed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
genome.signature: ratio-invariant structural topology hash (per-storey tree
shape + cut orientation + leaf types), the cheap stand-in for the 9gp canonical
encoding. driver gains niche_by_signature (one individual per topology, replaces
the fitness-scalar dedup) and restart_patience (soft restart: keep elites,
refill with fresh seeds); SearchResult gains n_distinct_signatures /
diversity_history / n_restarts.
Diversity criterion MET (final-pop distinct ~5/16 -> 16/16). Gate NOT met:
blank-slate programme-house mean fails 12.3(legacy)/12.7(niche)/13.0(restart)
over 3 seeds at 20000 evals; harbor staged 95/94/108. Niching is a tie within
seed noise, restarts strictly worse — falsifies the premise that the
fitness-scalar dedup causes premature convergence. Both flags default-off,
kept for reuse. Epic c4c complete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement a graded proximity comparator key (-n_fails, grade, fitness) behind
a default-off use_grade flag: fitness._leaf_grade / score_with_grade sum
f/FAIL_THRESHOLD over failing per-leaf quality factors; scalar fitness and fail
count stay untouched so the inner-loop 0.5^n cliff (§5.4) is unaffected (0/9
regression check: PASS). Read once per child in driver._evaluate off the
already-optimised tree; threaded through search_staged (Stage 2 only).
Harbor staged A/B (20000 evals, seeds 0/1/2): lex 95/96/106 (mean 99.0) vs
lex+grade 99/98/102 (mean 99.7) — grade wins 1/3, no plateau escape. Premise
falsified: within a fixed fail-tier 0.5^n is constant so fitness still spans
~6 orders of magnitude; grade above fitness displaces that working signal.
Verdict: reject; lexicographic (-n_fails, fitness) stands. Flag kept default-off
for reproducibility / possible reuse as a §11.5 diversity signal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Search the genome in causal dependency order. Stage 1 evolves a single-storey
base over the level-0 room set (programme auto-derived to a tempdir), ranked
with a substrate-readiness bonus (reserved core × divisible capacity) so the
base is selected as a good substrate, not just a good ground floor (anti-§4.2).
Stage 2 lifts the best base into a full multi-storey design — preserving the
inherited core, instantiating each upper storey's required set by construction —
and searches the deltas with the base mutable at low probability (base_p=0.15).
New: programme.{n_storeys_required,partition_rooms_by_storey,write_stage1_programme},
graph.substrate_readiness, operators.{lift_base_to_storeys,_pick_weighted_by_storey},
base_p threading, driver.search rank_bonus_fn/seed_factory/base_p hooks +
search_staged orchestrator, experiments/run_staged_search.py, tests/test_staging.py.
Result (harbor, 20000 evals, seed 0): staged 95 fails vs single-stage 105
(-10, -9.5%), gain in crinkliness 27->18 + edge 12->8. Anti-bungalow confirmed
(Stage-2 core moves all noop — core inherited, not carved). Programme-house
regression PASS (warmstart-2f4 still reaches whole-pop 1-fail).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the required programme room set a constructive invariant instead of
something the topology search must stumble onto by random divide+retype.
- operators.constructive_topology: bootstrap seeder that sizes each storey to
its required rooms (partitioned by level; level-free rooms distributed),
+1 core C and +1 O per storey, then assigns types. Stochastic for population
diversity. Wired into driver bootstrap when the programme has required spaces.
- operators.mutate_place_missing: repair op that inserts a missing required
space by dividing a host leaf into [room | remainder]. Lex-safe host ranking
(generic O first, never displace a required room); honours required level.
Weight 2.0 in the mutation mix; noops cheaply once the set is complete.
A/B on harbor-house (20k evals, seed 0, identical config):
old random-bootstrap 133 fails (103 missing, 77%)
new constructive 105 fails ( 12 missing, 11%) -21% total, missing-stack
collapsed; seed head-start 163->139.
§4.10 regression PASS: warmstart-2f4 still reaches a 1-fail population at 50k.
Verdict (DESIGN.md §11.2): construction is necessary and reframes the
bottleneck to quality-fail packing of a complete dense design (crinkliness/
size/access/edge) -> unblocks §11.3 staging, motivates §11.4 graded objective.
Follow-up filed (homemaker-py-s44): adjacency-aware seeding.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a compound operator (e.g. level_compound_fix) creates a new
internal node and explicitly sets its division ratio, that ratio was
silently overridden: warm_x0 received parent.ratios which had no entry
for the new node, so Nelder-Mead started at the default 0.5 instead
of the operator's intended 0.25 (for rrl/rrr). Result: NM evaluated
the compound topology at the wrong geometry and scored 3 fails instead
of 1 — so lex always rejected the compound child, making
level_compound_fix invisible to the outer search.
Fix: for nodes that are genuinely newly divided (not divided in the
parent tree at the same path), inherit the child's operator-set ratio
rather than defaulting to 0.5. Structural mutations (e.g. swap) can
reveal hidden level-N nodes that retain stale pre-writeback ratios —
those are correctly excluded by checking parent_node.divided.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
core_divide: divides a C leaf simultaneously on ALL storeys that share that
path, maintaining staircase consistency as an atomic invariant rather than
requiring multi-step recovery.
core_undivide: reverses core_divide consistently across all floors, merging
a C sub-core back into a single C leaf everywhere.
level_fix: atomically moves a level-constrained room to its required floor
by retyping the largest leaf there and vacating the wrong-floor leaf to C.
Requires `reqs` (SpaceReq dict); disabled (zero probability) without it.
mutate() gains `reqs=None` parameter; driver.search() passes its already-
loaded reqs so level_fix fires during the main memetic loop.
Together these let the optimiser escape the deceptive valley around the
2-fail warmstart: level_fix moves l1 to level 0 (reducing fails 2→1),
then core_divide can split the C core to accommodate the displaced t3.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Outer search now ranks individuals by (-n_fails, fitness) instead of raw
fitness scalar. This prevents high-score 3-fail designs from displacing
2-fail designs in tournament selection and population replacement — the
root cause of the §4.8 pathology where flag count dominates geometry.
Inner loop is unchanged: it still optimises against the raw 0.5^n fitness
scalar, so the cliff that prevents trading into new failures remains intact
(0/9 regressions in experiments/penalty_reshape.py).
Also removes stale _CHILD_INNER_KW = {"sigmas": (0.05,)}: this was left
over from the CMA-ES era; the NM inner loop default (homemaker-py-d6d)
does not accept a sigmas parameter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>