39.17 left the search's storey choice unexplained and blamed value_rate. It is not the rate, or not only. Measured over the twelve baseline runs, value/cost by leaf kind: outside ground 7.40, roof terrace 2.69, room 0.34, circulation 0.02. A terrace returns 2.7x its cost where a room returns a third of it, so filling upper storeys with terrace is not the search leaving value on the table -- it is by a wide margin the most profitable thing the objective offers. 7% of the corpus area produces 32% of its value. Most of that gap is mean quality: 0.986 for a terrace against 0.223 for a room. Quality is a PRODUCT of factors and the kinds are not asked the same number of questions -- an outside leaf is exempt from size, crinkliness and access, so 3 of 7 factors can ever bite it against a room's 6. Each exemption is individually right (no programme size target; uncovered outside is lit by definition; ground-level outside needs no access). The consequence is not: a leaf exempt from the two harshest factors out-scores one judged on them and doing well, purely by not being asked, and quality multiplies the value rate. Stated generally, and this is not about outside space: under a product, adding any new quality criterion mechanically devalues every leaf it applies to, including leaves that score 1.0 on it. The objective's scale should not depend on how many things it measures. quality_aggregate="geometric_mean" (default OFF, "product" is stock) divides that out. Computed in log space so six small factors cannot underflow the product before the root is taken; a zero factor still gives zero, so a fully buried leaf is worth nothing either way. Telling "exempt" from "asked and scored 1.0" needs factor_is_asked, which restates conditions that live inside the quality_* methods. That duplication can drift, so tests/test_fitness_aggregate.py pins it against every leaf in the corpus: wherever the predicate says exempt, the factor really is 1.0. Fail set byte-identical everywhere, and for a stronger reason than 39.13/39.14 had: evaluate_leaf emits each fail from the factor itself before anything is combined, so no aggregation can move one. Score effect +37% to +169%, reaching all four programmes where the crinkliness changes reached two; room value/cost 0.34 -> 0.66, circulation 0.02 -> 0.07. Deliberately not fixed: a terrace still out-earns a room 4:1, which is the rates (value_supported = value_inside = 300 against costs of 110 and 200), not the aggregation. That is a design judgement for the programme author, and 39.16 is a standing reminder that "this inherited constant looks wrong" has been wrong twice already in this section. Left open on ecx with the numbers. A/B running; verdict to follow. Refs homemaker-py-ecx. 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.