Commit graph

25 commits

Author SHA1 Message Date
bf3ff43837 erc.3: leaf-sharing mechanism + floor probe (§13.3)
Same-code rooms collapse into fewer, larger SHARED leaves so the ~1.8/leaf
shape tax (§13.1) is paid once per group. Multiplicity k is recovered from
area (k=clamp(round(area/target),1,max_share)) — no genome change — and used
in two default-OFF sites: graph.check_space_counts counts coverage (Σk vs
req.count) so one leaf covers several rooms without a missing fail, and
fitness.quality_size centres on k×target (σ scaled by k). Construction:
operators._share_rooms groups instances; _size_divisions_from_targets sizes
shared leaves to k×target via leaf_mult.

Floor probe (experiments/diag_leaf_sharing.py, harbor+maple, seeds 0/1/2,
+innerloop): total fails −27% harbor / −16% maple at share3, shape factors
fall ~linearly with leaf count (confirms §13.1). Cap: 17–44 missing fails
leak because depth maldistribution (§13.2) keeps shared leaves below k×target
so round() undercounts; inner loop can't close it. Net still positive.

Default-OFF reproduces baseline exactly (214 tests pass). Driver plumbing +
staged 20k A/B remain; §13.3 records the next design fork.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 08:30:26 +01:00
e95a3477a8 Fix parallel search nondeterminism; re-diagnose homemaker-py-xcy
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>
2026-06-22 23:25:50 +01:00
6ee5d4b4ae Phase 7 §12.3: re-scoped 9gp — shape-feasibility filter + M3 reassociate (9gp.1, 9gp.2)
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>
2026-06-20 18:54:48 +01:00
d004e4c937 Phase 6 §11.7: adjacency-aware lift + secondary adjacencies (ld5)
_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>
2026-06-19 11:47:40 +01:00
c1586237ca Phase 6 §11.6: adjacency-aware constructive seeding (s44)
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>
2026-06-19 09:23:12 +01:00
059964ee05 Phase 6 §11.5: structural niching + restarts — negative result (c4c.5)
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>
2026-06-18 23:42:39 +01:00
ed2869074b Phase 6 §11.4: graded high-fail objective — negative result (c4c.4)
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>
2026-06-18 22:33:29 +01:00
6ed9e0b4b1 Phase 6 §11.3: staged per-floor search (c4c.3)
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>
2026-06-18 06:05:53 +01:00
de60200bbc Phase 6 §11.2: programme-aware construction + missing-room repair (c4c.2)
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>
2026-06-17 22:51:58 +01:00
3c8f7aba07 Lexicographic outer-search comparison, preserve inner-loop cliff (homemaker-py-yg5)
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>
2026-06-14 09:20:03 +01:00
0e5e607c4f Swap inner loop default from CMA-ES to Nelder-Mead (homemaker-py-d6d)
Bakeoff with native fitness shows NM wins at all DOF sizes: +9% at
child_budget=80 for programme-house (6-7 DOF), and decisively at
harbor-house scale (35-40 DOF) where CMA-ES exhausts its convergence
detector after ~3 generations (46 evals) and adds failures on 12/15
runs.  NM uses the full budget, is parameter-free, and has zero new
failures across all test cases.

- Add nm_search() to innerloop.py; change optimise() default to "nm"
- Add nm_search to parametrised test cases
- Add bakeoff_native.py and bakeoff_harbor.py experiments with results

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 08:51:22 +01:00
646ee30ab6 Rename package: homemaker → homemaker-layout
- src/homemaker/ → src/homemaker_layout/; all imports updated
- pyproject.toml: name = homemaker-layout, entry point updated
- .beads/config.yaml: dolt sync.remote updated to homemaker-layout.git
- Delete temporary debug/perl scripts from project root
- README.md, DESIGN.md: package path references updated
- GitHub repo renamed; git remote updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 08:18:06 +01:00
7ee51caf62 Parallelise outer search population evaluation (homemaker-py-5l6)
Add n_workers parameter to driver.search(). When n_workers > 1, a
ProcessPoolExecutor evaluates the bootstrap batch and main-loop children
in parallel, giving near-linear speedup with core count. The geometry
module-level cache is cleared in each worker after fork to prevent stale
id-keyed entries. Serial behaviour (n_workers=1, default) is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 06:55:58 +01:00
304d514573 Add unit tests for geometry and fitness modules
26 tests for geometry (area, angles, aspect, boundary ids, centroid,
offset, etc.) and 35 tests for fitness (gaussian, config lookup,
quality terms, value rates, costs, stair helpers). Suite: 175 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 00:06:02 +01:00
c37f03f1a1 Close homemaker-py-hqw: make project standalone (no Perl/Urb dependency)
Copy programme-house corpus (36 .dom + .score + .fails + patterns.config)
into examples/ and update all 5 test files to use project-relative paths.
Native Python fitness (use_native=True) was already the default; tests now
run without /home/bruno/src/urb present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 23:39:20 +01:00
33d79be3fe Cold-start bootstrap: diverse initial population for blank-slate search
When the seed is an undivided bare plot (init.dom), auto-generate pop_size
random topologies before the memetic loop starts, each evaluated at
child_budget.  This crosses the zero-feasibility region that single-seed
chaining cannot escape — the programme-house cold start was stalling at 18
fails after 2000 evals vs urb-evolve's 6.

Auto-detection via seed_root.divided preserves the existing single-seed
path for warm starts from existing designs; all previous tests pass unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 23:29:12 +01:00
69d4dcbf23 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>
2026-06-13 21:44:42 +01:00
3bf507a483 Fix benchmark cell arg order and mutate_swap on undivided trees
run_urbevolve took (seed, budget, pop, cell) but cells call
fn(seed, budget, cell, **kw) — every urb-evolve cell died on TypeError,
deferred silently by pool.map. mutate_swap lacked the empty-candidates
noop guard the other operators have, crashing on init.dom-style bare
plots. Regression test: every mutation survives an undivided tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 23:26:22 +01:00
9e020f4769 Native fitness: adjacency graph + merge_divided (homemaker-py-3y7)
Two bugs fixed in boundary_id / leaf_graph:
1. 'bid not in "abcd"' used Python substring check, silently dropping the
   root-division boundary (empty-string id).  Fixed to frozenset membership.
2. Upper-storey nodes store their own rotation in the YAML but Urb::Quad::Rotation
   delegates to Below->Rotation.  boundary_id now walks the below-chain to the
   ground-floor rotation, matching Perl exactly.

After fixes all 35 corpus files produce edge counts matching Perl oracle.

Added:
- src/homemaker/graph.py: build_graphs (two-phase pattern), has_adjacency,
  has_vertical_connection (faithful no-overlap stub per DESIGN §8.1),
  find_missing_spaces, check_adjacency, check_level_constraints,
  check_vertical_connectivity
- src/homemaker/dom.py: @dataclass(eq=False) on Node for NetworkX hashability;
  is_outside, is_supported, is_unsupported, merge_divided
- tests/test_graph.py: 7 tests, edge counts vs Perl oracle on all 35 files,
  exact widths for 2f45907, merge_divided smoke, two-phase independence

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 21:57:45 +01:00
f160c6dc9e Use Urb's canonical UPPERCASE generic types (C/O); case-insensitive class checks
Bruno's correction: 'C' was never a 'covered' type — Is_Covered is a
geometric predicate. Urb generics are canonically uppercase (get_space_types
qw/C O S/; corpus 100% uppercase). The driver/operator type pool emitted
lowercase 'c'/'o', creating mixed-case designs that fragmented Dom->Ratios
class buckets and fired the latent ratio_type first-match nondeterminism
(which the search promptly reward-hacked). Operators now emit uppercase
generics only and class checks match case-insensitively (t[0].lower() in
'cos', cf. Is_Circulation/Is_Outside). The Urb-side class-sum patch remains
as defensive hardening, zero-impact on canonical designs (35/35 parity).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 19:01:53 +01:00
0beb005a23 Memetic search driver: steady-state GA over topology, warm-started inner loop
driver.py (homemaker-py-b39): tournament selection, operators.mutate (storey
ops down-weighted) + area-matched crossover, every child's geometry
delegated to the warm-started inner loop (Lamarckian write-back; children
use a single local CMA phase - the exploratory ladder phase exists for cold
projections children never face). Budget stated and accounted in oracle
evaluations; near-duplicate fitness guard against population collapse
(neutral mutations are common, per 8cs).

free_with_keys/ratio_map/warm_x0 promoted from the 8cs experiment into
innerloop.py as the Lamarckian inheritance API; alignment with
solver.free_branches asserted across the corpus.

tests/test_driver.py fakes the inner loop: budget accounting, monotone
improvement history, warm-start + sigma plumbing, valid .dom output.
31 tests pass. experiments/run_search.py is the end-to-end acceptance run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:22:26 +01:00
92cc63348e Topology operators: 7 mutations + area-matched subtree crossover
operators.py (homemaker-py-nyb): divide/undivide/retype/swap/rotate/
level_add/level_delete + Urb-style area-matched base-storey crossover.
Operators edit the decoded Node tree; genome.encode absorbs all repair
(dangling deltas, storey misalignment) so every child is a valid genome
by construction. Geometry moves deliberately absent — the inner loop owns
continuous DOF, and 8cs made Lamarckian re-optimisation mandatory.

Fixes dom._link to CLEAR stale below-links when a path vanishes from the
storey below (undividing a base branch left upper nodes pointing at
orphaned quads; oracle scoring unaffected but in-process geometry crashed).

Acceptance (experiments/operator_locality.py, flag-on): 115/115 children
scored without error; geometry perturbation small for core ops (retype
0.07, divide/undivide 0.14, swap/crossover 0.16-0.17), fitness
perturbation large for all (0.68-0.99 rel) — the 0.5^n cliff flags most
raw moves, confirming warm-started re-optimisation + penalty reshaping
as the load-bearing design choices. 27 tests pass.

Closes homemaker-py-nyb.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:07:35 +01:00
13f73be771 Topology genome: base tree + per-storey deltas + type assignment
genome.py (homemaker-py-k2g): Genome = base-floor GNode tree + per-storey
StoreyDelta (undivides, divide subtrees, leaf retypes, height) + base
metadata. encode/decode round-trips dom.py Node trees.

Key empirical finding baked into the design: upper-storey nodes carry
heavily drifted DEAD fields (97 inherited-cut divisions, 187 rotations
differ from the owning node below across the corpus) — dead because
geometry delegates to below before reading them. decode canonicalises
them; encode stores only owned state, so genomes from drifted sources
compare equal (fixed-point test).

Acceptance: 35/35 corpus files fitness-identical after round-trip through
the oracle (experiments/genome_parity.py, URB_NO_OCCLUSION=1); owned-cut
projection + genome fixed-point + storey counts in tests/test_genome.py
(16 tests pass).

Closes homemaker-py-k2g.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:52:32 +01:00
511c86c4dc Fix cma convergence test: assert basin convergence, not final-digit polish
The sigma-ladder default splits the budget into restart phases, so a
400-eval run reaches ~0.996 on the smooth test objective rather than
0.999+. Test now matches the component's contract. (Previous commit
landed with this failing because piping pytest to tail masked its exit
code.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 09:46:13 +01:00
0dcdf1f29f Geometry inner loop: batched full-objective ratio optimiser (CMA-ES)
innerloop.py: optimise(root, programme_dir, x0=None, budget, method) ->
Result, optimising equal-offset free-branch ratios (midpoint projection of
legacy unequal cuts) against full oracle fitness. OracleEvaluator scores
each population in one batched perl call. Methods: cma (default) — multi-
start sigma ladder (0.05 local, 0.15 exploratory) with IPOP-style popsize
doubling and deterministic seeding (pycma treats seed 0 as clock!) — and
compass with Hooke-Jeeves pattern moves, kept for the d0s bake-off.

Acceptance (experiments/accept_innerloop.py, §4.5 bars vs unprojected
originals, within-noise tolerance 1%): x1.65 / x1.66 / x1.58 against bars
x1.24 / x1.67 / x1.59, no new failures, 46 oracle calls vs Nelder-Mead's
200. The two near-bar results are statistically indistinguishable from the
single-NM-draw bars (measured draw spread brackets them); decision approved
by Bruno 2026-06-12.

Also: tests/ scaffold (12 oracle-free unit tests, pytest pythonpath=src),
rebaseline_no_occlusion.py for homemaker-py-gp2, cma>=3.0 dependency
(installed via dnf), dead-variable cleanup in solver.py.

Closes homemaker-py-1p0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 09:42:24 +01:00