Python rewrite of the Urb/Homemaker stack
Find a file
Claude cd392e77c5
Rescale the underflowing crinkliness tail so the failing region has an ordering
quality_uncrinkliness evaluates a gaussian at x = 1/crink, so its exponent
grows like 1/crink^2 and underflows a double to exactly zero below crink ~
1/15. Measured over the twelve 500k cold-start runs (39.12): 430 leaves carry
a minimum-exposure requirement, 112 fail it, and those 112 span quality
1e-300..1e-1 while contributing 0.034% of total value on 23% of the floor
area. Every value in that range is numerically zero beside a passing leaf's
~1, so the search cannot rank two layouts that differ only in how exposed
their under-lit rooms are.

This is wider than the bead's diagnosis (a flat 0.0 for zero-exposure leaves)
and it explains why 38.1's `floor` mode measured as a no-op: max(q, 0.01) maps
110 of the 112 onto one constant, replacing a flat zero with a flat 0.01.

crinkliness_tail="ramp" (default OFF, "gaussian" is stock) replaces the tail --
only the tail, only below FAIL_THRESHOLD, only on the compact side -- with a
straight line in crinkliness meeting the gaussian exactly at the crossing.
_crink_at_fail_threshold inverts the gaussian there using the same truncated
_E the factor is evaluated with.

Deliberately conservative: nothing at or above FAIL_THRESHOLD moves, so no
calibration changes and no leaf crosses the threshold. The fail set is
byte-identical on all 21 committed corpus artefacts, the four init.dom seeds
included -- asserted in tests/test_fitness_crinkliness_tail.py, not assumed.
That invariance is also what makes it legal to score both arms of the A/B
under stock (the 38.9 trap's one exemption). A fully buried leaf still scores
exactly 0; this restores an ordering within the failing region, it does not
forgive it. Composing with 38.1's superseded modes is refused, since both
rewrite the same tail.

Score effect on the baseline artefacts: +0.3%..+2.8% on harbor and maple,
exactly +0.000% on health-centre, programme-house, and every init.dom -- a
programme with no partially-exposed failing rooms has nothing to grade, and
neither does any starting layout. The ramp is a mid-search signal by
construction, so experiments/ab_9gj_ramp.py defaults to seeding each run from
a 500k plateau artefact rather than cold.

The module-level math import replaces a now-redundant local one.

DESIGN.md 39.13 and the A/B verdict follow in a separate commit.

Refs homemaker-py-9gj.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-09-05 06:29:02 +00:00
.beads bd: close homemaker-py-ut5 2026-09-04 17:17:03 +00:00
.claude Scaffold homemaker-py with validated geometry port 2026-06-10 20:50:20 +01:00
examples coldstart maple-court seed 2 @ 500000: 55 fails (11h/44s) 2026-09-03 10:06:06 +01:00
experiments Rescale the underflowing crinkliness tail so the failing region has an ordering 2026-09-05 06:29:02 +00:00
src/homemaker_layout Rescale the underflowing crinkliness tail so the failing region has an ordering 2026-09-05 06:29:02 +00:00
tests Rescale the underflowing crinkliness tail so the failing region has an ordering 2026-09-05 06:29:02 +00:00
.gitignore bd init: initialize beads issue tracking 2026-06-11 23:27:11 +01:00
AGENTS.md §39.7: access requirements become a declared usage: attribute (homemaker-py-sel) 2026-08-26 13:39:41 +00:00
CLAUDE.md §39.7: access requirements become a declared usage: attribute (homemaker-py-sel) 2026-08-26 13:39:41 +00:00
DESIGN.md Replace the stale 15-fail acceptance target with the cold-start baseline 2026-09-04 17:16:10 +00:00
pyproject.toml homemaker-py-2g7.5: CP-SAT exact room-code assignment (seeder + reassign op) 2026-08-04 09:19:36 +01:00
README.md §39.7: access requirements become a declared usage: attribute (homemaker-py-sel) 2026-08-26 13:39:41 +00:00

homemaker-layout

Programme-driven building-layout search over slicing trees. A clean-room Python successor to the Perl Urb project, intended to eventually be 100% Python.

Why a rewrite

Urb represents a building as a binary slicing tree where room sizes are derived top-down from division ratios. That makes room area an emergent property of every cut above it, which:

  • gives the genome low locality (a cut near the root rescales every descendant),
  • makes target room sizes nearly impossible to hit, so the gaussian size penalty dominates fitness, and
  • defeats crossover (transplanted subtrees lose their proportions).

homemaker inverts this: leaves carry target dimensions from the programme and division ratios are solved bottom-up for a fixed topology. The evolutionary search then only explores topology + types + adjacency.

Phase plan

  1. Solver experiment: port Urb's geometry, re-solve ratios from programme targets, score the result against the original via the Perl oracle.
  2. Native Python fitness (retire the Perl oracle).
  3. Memetic search: canonical slicing genome + high-locality operators + Nelder-Mead inner loop.
  4. Penalty reshaping: lexicographic (-n_fails, fitness) outer-search comparison.
  5. Representation upgrade: canonical slicing encoding + bottom-up shape feasibility, scaled to larger programmes.
  6. Search-quality experiments (current): a long running series of opt-in levers tried against the harbor-house, health-centre, and programme-house example corpora — leaf-sharing, finish-time cell→room collapse, ruin-and-recreate LNS, 2-opt polish, multi-use/co-located leaves, adjacency-graph and bubble-diagram fitness signals, and more. Most of these are negative/null results kept as opt-in flags or reference code rather than defaults. See DESIGN.md §11 onward for the full, numbered experiment log with methodology and results for each.

Layout

  • src/homemaker_layout/dom.py — read/write Urb .dom YAML into a Node tree.
  • src/homemaker_layout/geometry.py — faithful port of Urb's top-down geometry.
  • src/homemaker_layout/programme.py — parse patterns.config space requirements.
  • src/homemaker_layout/solver.py — bottom-up ratio solve (scipy).
  • src/homemaker_layout/fitness.py — native Python fitness evaluator.
  • src/homemaker_layout/fitness_cmd.pyhomemaker-fitness CLI (drop-in for urb-fitness.pl).
  • src/homemaker_layout/collapse_cmd.pyhomemaker-collapse CLI: finish-time global cell→room relabel of a .dom.
  • src/homemaker_layout/graph.py — leaf-adjacency graph for programme-driven checks.
  • src/homemaker_layout/genome.py — topology genome: base-floor tree + per-storey deltas.
  • src/homemaker_layout/operators.py — high-locality mutation and subtree crossover.
  • src/homemaker_layout/innerloop.py — ratio optimisation inner loop (Nelder-Mead / CMA-ES).
  • src/homemaker_layout/driver.py — memetic search outer loop.
  • src/homemaker_layout/evolve.pyhomemaker-evolve CLI entry point.
  • src/homemaker_layout/oracle.py — legacy Perl shim, kept for cross-validation only.
  • src/homemaker_layout/bubble.py — 3D bubble-diagram adjacency fitness-signal prototype (DESIGN.md §27); validated null, not wired into fitness.py — reference only.

Room codes and reserved names

Leaf types live in three namespaces that share a first character. Only the first is enforced; the other two are conventions the fitness function reads, so a room's spelling can change how it is scored.

1. Generic structural types — C, O, S (reserved). The leaves the search itself creates: C circulation, O outside, S sahn (an outside court that also serves as circulation). Always uppercase. A programme code spelled exactly C, O or S is rejected at load.

2. Programme room codes — anything else, lowercase. k1, b1, cr1, of, and single-character codes like r or t. These may start with any letter: since DESIGN.md §39.4 the generic tests match C/O/S exactly, so naming a room cr1 no longer makes it circulation. (Before that fix it did — and silently dropped it from the required-space check entirely.)

3. Access requirements — the usage: attribute. Every space declares one of living, kitchen, bedroom, toilet, utility, none. Mandatory, no fallback, and a missing or unknown value is a load error. It replaced a first-character convention (b/t/l/k) under which a room silently inherited another room's connectivity rules from its spelling — la1 "Laundry Room" was trimmed as a living room (DESIGN.md §39.7).

spaces:
  la1:
    usage: utility          # controlled, drives engine behaviour
    name: Laundry Room      # free text, building-specific

A usage value exists only where the engine treats it differently, so the vocabulary is closed: a new access class means new code, not new config. Check a programme with:

python experiments/audit_programme_config.py

which reports reserved-name collisions, the usage class each code picks up, and whether each room's size/width/proportion/crinkliness targets are mutually satisfiable at all.