Commit graph

30 commits

Author SHA1 Message Date
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
fadb4f191f Close homemaker-py-hqw in beads 2026-06-13 23:40:00 +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
6b22e154f8 Close homemaker-py-8fe: Fix Urb programme width default (Perl oracle)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 23:17:07 +01:00
027904c452 Add issues: cold-start, NM inner loop, parallelism, tests, package install
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 22:53:04 +01:00
7f82c03f80 Fix programme width default: derive from sqrt(size/proportion) (homemaker-py-can)
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>
2026-06-13 22:23:03 +01:00
3940746270 Close homemaker-py-ccw: Phase 3 scaled search gate PASS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 22:13:08 +01:00
7796e795a5 Phase 3 gate (homemaker-py-ccw): scaled search on native fitness
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>
2026-06-13 22:10:38 +01:00
9fde9226ff bd: update sync.remote 2026-06-13 21:47:40 +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
fc10466966 Fix stair-fit parity: entrance corners + weighted has_circulation (homemaker-py-w1e/q70)
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>
2026-06-13 20:55:25 +01:00
8e762b80d8 Phase-2 gate results: 2/3 seeds → REVIEW; fix patterns.config re-score bug
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>
2026-06-13 09:56:01 +01:00
deb15c3082 Close homemaker-py-d0s: inner-loop optimiser bake-off complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 09:48:35 +01:00
e2b3e20070 Phase-2 gate benchmark: memetic loop vs urb-evolve at equal eval budgets
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>
2026-06-12 22:22:16 +01:00
6459949e41 Close homemaker-py-b39: memetic driver acceptance passed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 22:10:06 +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
4ed809a47c Update beads issue state (nyb closed, b39 unblocked)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:08:44 +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
5f0c159112 Re-baseline under URB_NO_OCCLUSION: new reference gains, DESIGN §4.7
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>
2026-06-12 10:31:38 +01:00
1cc86c8a7e Warm-vs-cold experiment: topology mutations + Lamarckian ratio inheritance
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>
2026-06-12 10:00:51 +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
d4266f46dc Descope occlusion/daylight: disable in Urb, port simple crinkliness only
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>
2026-06-12 08:31:20 +01:00
2ef2b15fe3 Batched oracle: score many .dom files per perl invocation
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.
2026-06-12 01:13:55 +01:00
16f1704bdc Add beads issues for DESIGN.md phases 1-5 with dependency graph 2026-06-12 00:40:28 +01:00