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>
Built examples/harbor-house-l0/ (10 explicit level:0 codes, 13 instances,
single-storey constraints) and ran the memetic search from a bare plot. Best
33 fails at 20000 evals; whole population stuck 33–35, deep in the 0.5^n
high-fail regime. Fail histogram is dominated by 'missing' (13/33 = 39%): the
counted space m×3 is never constructed, with adjacency/access/size fails
downstream of the unbuilt room set.
Verdict: per-floor CONSTRUCTION is the bottleneck, not multi-storey coupling —
c4c.2 (programme-aware construction + missing-room repair) is the prerequisite
and staging (c4c.3) alone won't rescue it. Closes homemaker-py-c4c.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diagnosis-driven backlog redo: delivered speedups (native fitness, geometry
inner loop) polish within a failure tier but final design quality is gated by
topology-search quality on full/multi-storey programmes. New epic + children:
construction (c4c.2), staged per-floor search (c4c.3), graded high-fail
objective (c4c.4), topology diversity (c4c.5), plus a premise experiment
(c4c.1). Reframed 9gp (canonical encoding) as the capstone and deprioritised
2g5 (occlusion) as fitness-fidelity, orthogonal to design quality.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Python fitness always pins quality_daylight to 1.0 (URB_NO_OCCLUSION semantics),
but oracle.py was invoking urb-fitness.pl without the flag, causing outside leaves
to receive real sun-model daylight scores and producing a ~9% gap.
Changes:
- oracle.py: add URB_NO_OCCLUSION=1 to score_batch env
- oracle.py: Score.fail_lines now parses structured YAML failures from the
llm-agent-mcp branch and converts them to plain-text equivalents, so
parity tests can compare oracle vs native failure sets regardless of format
- Regenerated all 36 corpus .score/.fails files with URB_NO_OCCLUSION=1
(no .score files are tracked in git; the script generates them locally)
- All 183 tests pass; closes homemaker-py-gpx
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Records: the level-fix deceptive valley, compound operator design,
warm_x0 initialization bug and fix, two-C topology breakthrough,
0-fails geometric impossibility proof, and final 1-fail results
beating the Perl optimiser.
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>
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>
programme.load_programme_dir(directory) mirrors urb-evolve.pl: loads
../patterns.config first, then merges local patterns.config on top (shallow,
local top-level keys win). driver.search now uses load_programme_dir instead
of hardcoding the local path, so the type pool respects parent config.
fitness.load_config already had this behaviour; programme now matches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
python -m homemaker.evolve seed.dom [--budget N] [--pop N] [--workers N] ...
Installed as homemaker-evolve entry point via pyproject.toml.
Takes a single .dom file; infers programme dir from its parent. All parameters
available as --flags or HOMEMAKER_* env vars. Output defaults to
<seed_stem>_evolved.dom; use --output - for stdout. SIGINT/SIGTERM returns
best-so-far via new driver.SearchResult.interrupted flag.
Also adds dom.dumps() for string serialisation and refactors dom.dump() to use it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
When a programme space has no explicit 'width' key, the fallback to
width_inside [4.0, 1.0] is geometrically impossible for small spaces
(e.g. t3 WC at 3 m²). Now compute target = sqrt(size/proportion),
sigma = max(0.1, target * size_sigma / (2 * size_target)).
Effect on 35-file corpus: 32 files score +1–307% (width quality improves
for correctly-sized small rooms); 5 files lose spurious width fail lines.
Upstream Perl fix tracked as homemaker-py-8fe.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
programme-house budget=20000: 1.04e-02 (2 fails), 1.36× over Phase-2
oracle run and 2.60× over urb-evolve p128. Winning topology found via
rotate at eval 10357, unreachable within Phase-2 budget. 71.8 evals/s
(~140× faster than batched oracle).
harbor-house (16 rooms): 3.73e-18 (49 fails) at budget 10000 in 633s.
This programme is beyond the oracle's capability; native fitness makes
it feasible. 638 topologies explored.
Adds experiments/run_search_scaled.py (native-only search runner, no
oracle dependency). DESIGN.md records Phase 3 gate result.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Two root causes found for ca/cb corpus parity failures:
1. _avg_path_len_from used unweighted BFS (hop count) but Perl's
Graph::average_path_length uses weighted Dijkstra with centroid-to-centroid
edge distances. This caused wrong edge removal in has_circulation, giving
wrong stack corner counts (2 instead of 3 for lr in ca9e80c5).
2. Entrance corner logic used _public_access (any street boundary) but Perl's
Entrances() picks the best entrance route — a stair only gets entrance corners
if no higher-priority non-stair C leaf has public access.
Also includes homemaker-py-hgg storey/building checks previously uncommitted:
stair fit, circ connectivity, roof-garden, public-access tracking, has_circulation,
corners_in_use, stack_corners_in_use, check_space_counts with failure stacking.
All 4 debug corpus prefixes: ratio=1.000000. 39 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
benchmark_vs_urbevolve.py results (2026-06-13, budget=2000, URB_NO_OCCLUSION=1):
- Seeded designs: memetic beats urb-evolve 1.91× (c964435) and 1.63× (2f45907)
- Blank slate init.dom: memetic at 18 fails vs urb-evolve at 6 fails (topology
diversity gap from single-seed mutation chain vs random-population init)
Bug fixed: run_search.py was calling oracle.score on out.parent without
patterns.config present — causing the re-score to return near-zero instead of
the correct tracked fitness. Added shutil.copy to propagate patterns.config
alongside the output .dom before the standalone re-score.
Gate recorded in DESIGN.md §7. Closes homemaker-py-way.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
experiments/benchmark_vs_urbevolve.py (homemaker-py-way): 3 seed designs
(init from scratch, c964435 weak, 2f45907 strong) x 2000-eval runs, the
1000-eval tier read from each run's best-so-far log; urb-evolve gets two
population sizes (default 128 = ~16 generations at this budget, and 16 =
~130) and credit for its better one. Counts via the MAX_EVALS counter
patch in urb-evolve.pl (committed in the urb repo); both systems under
URB_NO_OCCLUSION=1; all finals re-scored through urb-fitness.pl as the
common deterministic yardstick.
run_search.py generalised to (budget, rng-seed, seed.dom, out.dom);
innerloop.optimise now handles 0-DOF topologies (an undivided plot like
init.dom scores once instead of crashing CMA).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
Corpus: all 35 scores shift (x1.0-1.24, daylight pinned), one expected
failure-set change (458aa8b8 +2 crinkliness), oracle ~8% faster batched.
New deterministic-seed reference gains become the accept_innerloop bars:
x1.63 / x1.70 / x1.68 at budget 400, ~35 oracle calls per topology.
urb-evolve respects the flag by construction (in-process fitness reads
ENV at call time). Old flag-off numbers kept in DESIGN as historical.
Closes homemaker-py-gp2 (Urb-side patch lives in /home/bruno/src/urb,
uncommitted there pending review).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
experiments/warm_vs_cold.py (homemaker-py-8cs): top-storey divide/undivide
mutations on corpus designs, path-keyed inheritance of the parent's
optimised ratios (surviving cuts keep values, new cuts 0.5), per-evaluation
convergence traces; reports oracle evals to 95% of best final, warm vs
cold. Machinery validated oracle-free (cut survival counts) and one
mutated child scored through the oracle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
Strategy decision (Bruno): occlusion is orthogonal to building a scalable
optimiser. Instead of porting Sun/Occlusion/CIESky, disable it in Urb
behind an env flag (daylight -> 1, illumination factor -> 1 so crinkliness
is unweighted external wall area / floor area). Python occlusion rebuild
deferred until optimisation is fully native.
Tracker: new homemaker-py-gp2 (flag + re-baseline) gates gnw/way/uxz;
homemaker-py-2g5 re-scoped to the post-Phase-5 Python rebuild (P4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
oracle.score_batch() writes/cleans N outputs and runs urb-fitness.pl once
with all file names; oracle.score() is now a thin wrapper. Adds
Score.fail_lines (sorted) because Perl hash-order randomisation shuffles
.fails line order between runs, and documents Urb's ~1-ULP score
nondeterminism (compare with rel tolerance, never ==).
experiments/bench_batch_oracle.py validates batch-vs-single parity on the
35-file corpus and benchmarks: 0.98 s/dom batched vs 1.27 s/dom single
(x1.30), all files identical (fitness to 1e-12 rel, exact failure sets).
Closes homemaker-py-av5.