Owner's ruling: "as long as circulation is more expensive to build than it has value then we have a linear ramp. a gaussian ramp is probably not appropriate here as double the amount of corridor is simply twice as bad, so it should score the same as two half size corridors". Both halves check out. The linear ramp is already there -- value_circulation 50 against a build cost of 200, so every m2 of corridor is worth -150 and the objective pushes for less of it without needing a cap. And the AMOUNT of circulation is separately governed at building level by ratio_circulation [0.00, 0.20], a gaussian on the circulation fraction, which is where that question belongs. The per-leaf size gaussian was a third charge on the same thing. It was also the only one of the three that depended on how the corridor was cut up. One 20 m2 corridor scored gaussian(20,0,14) = 0.360 and contributed 360; two 10 m2 halves scored 0.775 each and contributed 775 between them. Splitting a corridor in half multiplied its value by 2.15x -- an artefact of where the tree happened to cut, rewarding the search for fragmenting its own spine. The ruling's test (one 2A leaf must score as two A leaves) is exactly what a gaussian on an amount cannot satisfy, and is now a test. size_circulation = None; quality_size returns 1.0 for circulation and shapecurve gives amin, amax = 0, inf. BUG this exposed: get_space_params falls through to a habitable default when a generic family key is missing and could not tell "missing" from "present but null", so a corridor silently inherited a room's 16 m2 size target. _generic_param now returns (found, value); pinned by a test. The same trap applied to 39.22's proportion_circulation. Fail-set effect of 39.22 and 39.23 together: 16 corridor size fails and 7 proportion fails removed, none added. harbor 33/43/42 -> 32/40/38, maple 54/73/55 -> 51/65/52, health-centre 4/9/5 -> 3/9/5, programme-house unchanged. The layouts are identical -- these are failures the objective should never have been reporting. Two shape-curve tests moved fixture: both built an infeasible upper storey from a 'C' leaf, infeasible precisely because of the bounds now removed. The fixture is a cr1 leaf, whose infeasibility is a contradiction between two of its own bounds (needs >= 180 m2 for its aspect bound, <= 101.5 m2 for its size bound across the box's fixed 23.52 m span) rather than a tight fit. The invariants they test are unchanged. Left open on hxi: the rate gap, value_circulation 50 against value_inside 300 on identical build cost. Whether a corridor is worth a sixth of a room per m2 is a design judgement, and the linear ramp is only as steep as that number. 419 passed. Refs homemaker-py-hxi. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB |
||
|---|---|---|
| .beads | ||
| .claude | ||
| examples | ||
| experiments | ||
| src/homemaker_layout | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| DESIGN.md | ||
| pyproject.toml | ||
| README.md | ||
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
Solver experiment: port Urb's geometry, re-solve ratios from programme targets, score the result against the original via the Perl oracle.✓Native Python fitness (retire the Perl oracle).✓Memetic search: canonical slicing genome + high-locality operators + Nelder-Mead inner loop.✓Penalty reshaping: lexicographic✓(-n_fails, fitness)outer-search comparison.Representation upgrade: canonical slicing encoding + bottom-up shape feasibility, scaled to larger programmes.✓- Search-quality experiments (current): a long running series of
opt-in levers tried against the
harbor-house,health-centre, andprogramme-houseexample 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. SeeDESIGN.md§11 onward for the full, numbered experiment log with methodology and results for each.
Layout
src/homemaker_layout/dom.py— read/write Urb.domYAML into aNodetree.src/homemaker_layout/geometry.py— faithful port of Urb's top-down geometry.src/homemaker_layout/programme.py— parsepatterns.configspace 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.py—homemaker-fitnessCLI (drop-in forurb-fitness.pl).src/homemaker_layout/collapse_cmd.py—homemaker-collapseCLI: 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.py—homemaker-evolveCLI 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 intofitness.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.