ssz: daylight is required of rooms that do not need it

DESIGN.md 38.6 concluded the three crinkliness modes were inert against the
circulation-deletion incentive. Two things were wrong with that measurement.
Its premise, 38.2, is retracted. And its script selected leaves with the
pre-39.4 prefix rule `type[:1].upper() in ("C","O")`, which sweeps every
programme room starting with c or o -- cr1, of1 -- in as circulation.

The simpler problem is that none of the three modes ever touched the leaves
ssz is about. quality_uncrinkliness reaches `if not crink` before any mode
logic that matters, so for a zero-exposure leaf: floor returns 0.01 (one
percent of a unit quality, multiplied into a product and weighed against a
whole leaf's cost -- inert); compact_ok is self-contradictory, announcing
that compact is not a defect and then returning the floor for the most
compact case of all; exempt_circulation reaches at most a third of them.
Measured: 0% / 0% / 0% / 21-33% of buried leaves rescued.

What the buried leaves are, now that 39.7 gives every space a usage: two
thirds of them are spaces that architecturally do not want a window --
stores, WCs, plant, corridors, covered courtyards -- scored identically
with a windowless bedroom. harbor 22/33, maple 33/46, health 9/18.

  - crinkliness_mode="usage_daylight": daylight required of the uses a
    person occupies (programme.DAYLIGHT_USAGES) and nothing else. Elsewhere
    the factor is clipped on the compact side only, so being buried stops
    being a defect while over-exposure still costs -- a crinkly leaf costs
    envelope whatever it is used for. A windowless bedroom stays the hard
    zero it is under stock: 11/11, 13/13, 9/9 still failing.
  - compact_ok repaired to score the buried limit as compact, the behaviour
    its name always claimed. It now rescues 100% including bedrooms, and is
    kept as the upper-bound control, not a candidate.
  - ab_ssz_search.py: the fixed-budget search A/B ssz's acceptance criteria
    actually asks for. Every arm is optimised under its own objective and
    re-scored under stock urb, because the permissive modes return 1.0
    where stock fails and would otherwise win by deleting a fail category.
  - ab_crinkliness_mode_ssz.py: prefix rule fixed, retracted premise
    flagged in its docstring.
  - 38.7's remaining claims from the retracted 38.2/38.3 corrected.

Default is unchanged ("urb"), byte-identical to all prior runs. Lint at
parity (46 pre-existing); tests 366 passed, 10 new, same 7 pre-existing
fixture failures (homemaker-py-bdf).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
This commit is contained in:
Claude 2026-08-26 16:45:10 +00:00
parent 8522576467
commit 189efdbfc7
No known key found for this signature in database
7 changed files with 367 additions and 12 deletions

101
DESIGN.md
View file

@ -5075,10 +5075,16 @@ fails to move** — `homemaker-py-2v1` is the half that matters.
rather than unmet") was aimed at the right target, and §38.3 supplies a cheap
way to test it that does **not** need `2g7.1`'s traced human plans: the
frontage bound is a pre-flight feasibility check computable from a plot and a
programme alone. Two of the four corpus programmes fail it by ~3×, which means
a share of the residual those runs are being judged on **is not reachable at
all** — and any A/B measured against that residual has been measuring, in
part, an unsatisfiable constraint.
programme alone.
*(As first written this paragraph continued "two of the four corpus programmes
fail it by ~3×, which means a share of the residual those runs are being judged
on is not reachable at all". **That is withdrawn** — see §39.11. The ~3× came
from applying the bound to a fully built plot rather than to the area each
programme demands; harbor-house and maple-court are frontage-feasible with room
to spare. One programme is unsatisfiable, health-centre, and for a cruder
reason: it demands 131% of its plot. So the residual the other runs are judged
on is reachable, and the plateau is not explained by an unsatisfiable brief.)*
Tracks 2 and 3 (cheaper evaluation, exact sub-solvers) remain sound but are
orthogonal: making an evaluation 97× faster, or a labelling exact, does not
@ -5089,6 +5095,12 @@ dominant mechanism, and the one the §38.6 A/B isolated) → `ssz`/`hxi`/`gvb`
pre-flight bound and re-baseline the corpus), and only then resume
`2g7.9`/`2g7.10`.
*(Both halves of that ordering's rationale have since been measured and did not
survive. `2v1` closed NULL — severing the spine is already punished, §38.2 is
retracted — and `tdp`'s infeasibility claim is retracted above. What the
ordering got right is that `ssz` comes before `2g7.7`; see §38.8 for what `ssz`
turned out to be.)*
**Acceptance test for the combined fix, stated up front so it cannot be
moved:** harbor-house must reach its known 15-fail floor in materially fewer
than 1.7 M evals, *and* `level 0 not connected` / `level 1 not connected` must
@ -5099,6 +5111,87 @@ valley-crossing multi-edit against an objective that pays ×85 to delete the
corridor it just inserted will have its work reverted by the next selection
step.
### 38.8 What `ssz` actually was: the objective demands daylight for rooms that do not need it (`homemaker-py-ssz`)
**First, why §38.6's A/B does not stand.** It measured the three modes against
the §38.2 *deletion test*, and it did so with a script that predates §39.4:
`experiments/ab_crinkliness_mode_ssz.py` selected "unpinned" leaves with
`(leaf.type or "")[:1].upper() in ("C", "O")`, the first-character prefix rule,
so every programme room whose code happens to begin with c or o — `cr1`, `of1`
— was swept in as circulation. Both the premise (§38.2, retracted) and the
selection were wrong. The script is kept, with the prefix rule fixed and a
warning in its docstring, but nothing is decided on its numbers.
**Second, and simpler: none of the three modes ever touched the leaves `ssz` is
about.** `quality_uncrinkliness` reaches `if not crink: return ...` *before* any
of the mode logic that matters, so for a zero-exposure leaf:
| mode | buried leaves | rescued to ≥ `FAIL_THRESHOLD` | windowless habitable rooms still failing |
|---|---|---|---|
| `urb` (stock) | 33 / 46 / 18 | 0% | 11/11, 13/13, 9/9 |
| `floor` | 33 / 46 / 18 | 0% | 11/11, 13/13, 9/9 |
| `compact_ok` *(as measured in §38.6)* | 33 / 46 / 18 | 0% | 11/11, 13/13, 9/9 |
| `exempt_circulation` | 33 / 46 / 18 | 21% / 24% / 33% | 11/11, 13/13, 9/9 |
*(harbor-house / maple-court / health-centre, 3 constructed seeds each, full
default stack.)*
`floor` returns 0.01 — one percent of a unit quality, multiplied into a product
and weighed against a whole leaf's cost, which is why it reads as inert.
`compact_ok` is worse than inert, it is **self-contradictory**: it announces
that being more compact than target is not a defect, and then returns the floor
for the most compact case of all, because `if not crink` fires before its clip
is ever reached. Only `exempt_circulation` moves anything, and it reaches at
most a third of the population. So §38.6's "none of them removes the incentive"
was reading a null that the modes' own implementation guaranteed.
**What the buried leaves actually are.** §39.7 gave every space a declared
`usage:`, which lets the question be asked properly for the first time — of the
leaves scoring a hard zero for want of daylight, how many are rooms a person
sits in?
| programme | buried | habitable (`living`/`kitchen`/`bedroom`) | store, toilet, plant, corridor, covered court |
|---|---|---|---|
| harbor-house | 33 | 11 (33%) | **22 (67%)** |
| maple-court | 46 | 13 (28%) | **33 (72%)** |
| health-centre | 18 | 9 (50%) | 9 (50%) |
**Roughly two thirds of the zero-value leaves are spaces that architecturally
do not want a window at all** — a broom cupboard, a WC, a plant room, an
internal corridor, a covered courtyard. The objective scores them identically
with a windowless bedroom. That is the miscalibration, and it is not a gradient
problem to be patched with an epsilon; it is the wrong requirement applied to
the wrong rooms.
**The repair: `crinkliness_mode="usage_daylight"`.** Daylight is required of
the uses a person occupies (`programme.DAYLIGHT_USAGES` =
`living`/`kitchen`/`bedroom`) and of nothing else. For every other usage, and
for the generic `C`/`O`/`S` types which carry no programme entry, the factor is
clipped on the **compact side only** — being buried stops being a defect, while
over-exposure still costs, because a crinkly leaf costs envelope whatever it is
used for. A windowless bedroom remains exactly the hard zero it is under stock.
| programme | buried | rescued by `usage_daylight` | windowless habitable rooms still failing |
|---|---|---|---|
| harbor-house | 33 | 22 (67%) | 11/11 |
| maple-court | 46 | 33 (72%) | 13/13 |
| health-centre | 18 | 9 (50%) | 9/9 |
`compact_ok` was also repaired to score the buried limit as compact (1.0), the
behaviour its name always claimed; it now rescues 100% and is kept as the
**upper-bound control** — the mode that deletes the daylight requirement
outright, including for bedrooms. It is not a candidate.
**How this is scored, stated before the result.** `compact_ok`,
`exempt_circulation` and `usage_daylight` all return 1.0 where stock returns
below `FAIL_THRESHOLD`, so scoring an arm under its own objective deletes a fail
category for free and every arm "wins". Every arm below is therefore optimised
under its own objective and **re-scored under stock `urb`** — the comparable
yardstick, and the only one that answers *did optimising under this variant
steer the search to a better building?* The arm's own-objective count is
reported alongside solely to show the size of the definitional discount. A mode
passes on the stock column. `experiments/ab_ssz_search.py`.
## 39. Config audit: requirements that actively fight the engine (`homemaker-py-ju3`) — measured 2026-08-25
The corpus `patterns.config` targets and `costs.config` values were estimated

View file

@ -16,6 +16,17 @@ buried or lit is what separates the two mechanisms:
against a ×0.5 connectivity penalty (§38.2 refinement) which no
``crinkliness_mode`` can touch.
.. warning::
**The premise of this test is RETRACTED see §38.2 and §38.8.** The ×6-vs-
×0.5 arithmetic assumed severing the spine costs one connectivity fail;
measured, the connectivity count is unchanged in every rewarded deletion, so
this script is not measuring what its docstring says. It is kept because the
per-mode buried/lit split is still a useful description of what each mode
touches, but a mode does **not** pass or fail on these numbers. The A/B that
decides `ssz` is ``experiments/ab_ssz_search.py`` (fixed-budget search,
scored under the stock objective).
Usage::
python experiments/ab_crinkliness_mode_ssz.py
@ -34,7 +45,8 @@ from homemaker_layout import driver, fitness, geometry
from homemaker_layout import graph as graph_mod
from homemaker_layout import operators, programme
MODES = ("urb", "floor", "compact_ok", "exempt_circulation")
MODES = ("urb", "floor", "compact_ok", "exempt_circulation",
"usage_daylight")
def make_fitness(progdir: str, mode: str) -> fitness.Fitness:
@ -74,7 +86,12 @@ def unpinned_leaves(fit: fitness.Fitness, root: dom_mod.Node) -> list[tuple]:
for leaf in lvl.leaves():
if dom_mod.is_outside(leaf) and not dom_mod.is_covered(leaf):
continue
if (leaf.type or "")[:1].upper() not in ("C", "O"):
# §39.4: generic structural types are EXACT `C`/`O`/`S`, never a
# first-character prefix -- `cr1` is a programme room. This script
# predates that rule and its original `type[:1].upper() in ("C","O")`
# test swept programme rooms into the "unpinned" set, which is one
# reason the §38.6 numbers do not reproduce.
if not dom_mod.is_generic(leaf.type):
continue
out.append((li, leaf.id, leaf.type,
fit.area_outside(leaf, graphs[li], {})))

View file

@ -0,0 +1,145 @@
"""Fixed-budget search A/B for the crinkliness modes (`homemaker-py-ssz`).
DESIGN.md §38.6 A/B'd the modes against the §38.2 *deletion test*, which has
since been retracted, and it used the pre-§39.4 `type[:1] in ("C","O")` prefix
rule that mislabels programme rooms as circulation. So the modes have never
been measured against what `ssz`'s acceptance criteria actually asks for: a
fixed-budget search, hard/soft fail split, on harbor-house and maple-court.
**The scoring discipline is the point of this script.** `compact_ok`,
`exempt_circulation` and `usage_daylight` all return 1.0 for leaves that stock
scores below FAIL_THRESHOLD, so scoring an arm under its own objective deletes
a fail category for free and every arm "wins". Two numbers are therefore
reported per arm:
urb the arm's final layout re-scored under the STOCK objective. This is
the comparable yardstick, and the one that answers "did optimising
under this variant steer the search to a better building?"
own the same layout under the arm's own objective. Lower than `urb` by
construction for the permissive modes; it is reported only so the
size of the definitional discount is visible, never as the result.
A mode passes on `urb`, not on `own`.
Usage::
python experiments/ab_ssz_search.py --budget 3000 --seeds 3
python experiments/ab_ssz_search.py --modes urb usage_daylight --seeds 2
"""
from __future__ import annotations
import argparse
import collections
import copy
import csv
import time
from pathlib import Path
from homemaker_layout import dom as dom_mod
from homemaker_layout import driver, fitness
CORPUS = ["examples/harbor-house", "examples/maple-court"]
MODES = ["urb", "floor", "compact_ok", "exempt_circulation", "usage_daylight"]
def _with_mode(mode: str):
"""Patch `fitness.load_config` so every evaluator built during the run --
the driver's, the inner loop's, the seeder's -- sees `crinkliness_mode`.
`driver.search` has no parameter for it, and `driver._fitness_for` is
lru_cached on its arguments, so the cache is cleared around the patch or a
later arm would silently reuse the previous arm's evaluator.
"""
orig = fitness.load_config
def patched(directory, overrides=None):
ov = dict(overrides or {})
ov["crinkliness_mode"] = mode
return orig(directory, overrides=ov)
return orig, patched
def tiers(fails) -> tuple[int, int]:
c = collections.Counter(fitness.classify_fail_tier(f) for f in fails)
return c["hard"], c["soft"]
def run_arm(progdir: str, seed: int, mode: str, budget: int,
child_budget: int) -> dict:
orig, patched = _with_mode(mode)
fitness.load_config = patched
driver._fitness_for.cache_clear()
t0 = time.perf_counter()
try:
res = driver.search(
dom_mod.load(f"{progdir}/init.dom"), progdir,
budget=budget, seed=seed, child_budget=child_budget, n_workers=1)
root = copy.deepcopy(res.best.root)
own_conf, own_cost = patched(progdir, overrides={"leaf_sharing": True,
"collapse_insearch": True})
_, own_fails = fitness.Fitness(own_conf, own_cost).score_with_fails(
copy.deepcopy(root))
finally:
fitness.load_config = orig
driver._fitness_for.cache_clear()
# the comparable yardstick: stock objective, same layout
conf, cost = orig(progdir, overrides={"leaf_sharing": True,
"collapse_insearch": True})
_, urb_fails = fitness.Fitness(conf, cost).score_with_fails(copy.deepcopy(root))
uh, us = tiers(urb_fails)
oh, os_ = tiers(own_fails)
return dict(programme=Path(progdir).name, seed=seed, mode=mode,
urb_hard=uh, urb_soft=us, urb_total=uh + us,
own_hard=oh, own_soft=os_, own_total=oh + os_,
elapsed_s=round(time.perf_counter() - t0, 1))
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--budget", type=int, default=3000)
ap.add_argument("--child-budget", type=int, default=80)
ap.add_argument("--seeds", type=int, default=3)
ap.add_argument("--modes", nargs="+", default=MODES)
ap.add_argument("--corpus", nargs="+", default=CORPUS)
ap.add_argument("--out", default="experiments/results/ab_ssz_search.csv")
args = ap.parse_args()
rows = []
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
for progdir in args.corpus:
for mode in args.modes:
for seed in range(args.seeds):
r = run_arm(progdir, seed, mode, args.budget, args.child_budget)
rows.append(r)
print(f" {r['programme']:<14} {mode:<20} seed={seed} "
f"urb {r['urb_hard']}h/{r['urb_soft']}s "
f"(own {r['own_hard']}h/{r['own_soft']}s) "
f"{r['elapsed_s']}s", flush=True)
with out.open("w", newline="") as fh:
w = csv.DictWriter(fh, fieldnames=list(rows[0]))
w.writeheader()
w.writerows(rows)
print(f"\n=== stock-objective (urb) fail counts, budget {args.budget} ===")
print(f" {'programme':<14}{'mode':<22}{'hard':<14}{'soft':<14}total")
print(" " + "-" * 70)
for progdir in args.corpus:
name = Path(progdir).name
for mode in args.modes:
sel = [r for r in rows if r["programme"] == name and r["mode"] == mode]
if not sel:
continue
h = sum(r["urb_hard"] for r in sel) / len(sel)
s = sum(r["urb_soft"] for r in sel) / len(sel)
print(f" {name:<14}{mode:<22}{h:<14.1f}{s:<14.1f}{h + s:.1f}")
print(f"\nwrote {out}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,3 @@
programme,seed,mode,urb_hard,urb_soft,urb_total,own_hard,own_soft,own_total,elapsed_s
harbor-house,0,urb,16,39,55,16,39,55,80.2
harbor-house,1,urb,22,45,67,22,45,67,88.7
1 programme seed mode urb_hard urb_soft urb_total own_hard own_soft own_total elapsed_s
2 harbor-house 0 urb 16 39 55 16 39 55 80.2
3 harbor-house 1 urb 22 45 67 22 45 67 88.7

View file

@ -438,7 +438,8 @@ class Fitness:
self._connectivity_weight = float(cw)
self._crinkliness_mode = str(self.conf("crinkliness_mode") or "urb")
if self._crinkliness_mode not in (
"urb", "floor", "compact_ok", "exempt_circulation"):
"urb", "floor", "compact_ok", "exempt_circulation",
"usage_daylight"):
raise ValueError(
f"unknown crinkliness_mode: {self._crinkliness_mode!r}")
# The floored value stays BELOW FAIL_THRESHOLD, so a buried leaf still
@ -463,6 +464,17 @@ class Fitness:
req = (self._programme or {}).get(leaf.type)
return req.usage if req else ""
def needs_daylight(self, leaf: Node) -> bool:
"""Does this leaf's declared usage want a window? (homemaker-py-ssz)
True only for uses a person occupies (``programme.DAYLIGHT_USAGES``).
A generic ``C``/``O``/``S`` leaf has no programme entry and so is False,
which is the intended reading: a corridor or a covered courtyard is not
failing when it has no daylit wall.
"""
from . import programme as _pr
return self.usage_of(leaf) in _pr.DAYLIGHT_USAGES
# ------------------------------------------------------------------ #
# Type superposition + collapse (homemaker-py-9o5)
# ------------------------------------------------------------------ #
@ -1207,15 +1219,33 @@ class Fitness:
# (c) internal corridors are ordinary architecture; stop requiring
# every circulation leaf to reach daylight.
return 1.0
# (b)/(d) one-sided: being MORE compact than target is not a defect the
# way over-exposure is. Over-exposure still is one -- a crinkly leaf
# costs envelope whatever it is used for -- so this clips the compact
# side only, it does not switch the factor off.
#
# `compact_ok` applies that to every leaf; `usage_daylight` applies it
# only where nobody is sitting -- a store, a toilet, plant, a corridor,
# a covered courtyard -- and leaves habitable rooms on stock behaviour,
# so a windowless bedroom is still the hard failure it should be.
one_sided = mode == "compact_ok" or (
mode == "usage_daylight" and not self.needs_daylight(leaf))
if not crink:
# Zero exposure IS the compact limit (1/crink -> inf), so a
# one-sided factor has to score it 1.0. Reaching here and returning
# the floor instead was the flaw in the first `compact_ok`: it
# announced that compact is not a defect and then punished the most
# compact case of all hardest (§38.8).
if one_sided:
return 1.0
# (a) floor: keep buried leaves rankable by their other factors
# instead of collapsing the whole quality product to zero.
return self._crinkliness_floor if mode in ("floor", "compact_ok") else 0.0
return self._crinkliness_floor if mode == "floor" else 0.0
q = gaussian(1 / crink, 1.0, distance, sigma)
if mode == "compact_ok" and 1 / crink > distance:
# (b) one-sided: being MORE compact than target is not a defect the
# way over-exposure is, so clip to 1.0 on the compact side rather
# than decaying symmetrically into a fail.
if one_sided and 1 / crink > distance:
return 1.0
return max(q, self._crinkliness_floor) if mode in ("floor", "compact_ok") else q

View file

@ -104,6 +104,12 @@ TOILET_STRIPS = ("living", "kitchen", "toilet")
# Sociable rooms keep their MOST central circulation neighbour; terminal rooms
# and toilets keep their LEAST central one.
SOCIABLE_USAGES = ("living", "kitchen")
# Uses a person OCCUPIES, and which therefore want a window. Everything else --
# stores, toilets, plant, corridors, covered courtyards -- is ordinary buried
# architecture, and `crinkliness_mode="usage_daylight"` stops the objective
# demanding daylight for it (homemaker-py-ssz, DESIGN.md §38.8). A generic
# `C`/`O`/`S` leaf has no programme usage and is exempt for the same reason.
DAYLIGHT_USAGES = ("living", "kitchen", "bedroom")
def validate_codes(codes) -> None:

View file

@ -528,6 +528,67 @@ def test_crinkliness_exempt_circulation_only_exempts_circulation():
assert f.quality_uncrinkliness(room, None, {}) == 0.0
def test_crinkliness_compact_ok_scores_the_buried_limit_as_compact():
"""Regression (§38.8): a zero-exposure leaf IS the compact limit.
The first `compact_ok` returned the floor here, i.e. it announced that
being compact is not a defect and then punished the most compact case of
all hardest -- which is why it measured inert on buried leaves.
"""
f, leaf = _stub_fit("compact_ok", stub=0.0)
assert f.quality_uncrinkliness(leaf, None, {}) == 1.0
def _usage_fit(mode, stub, code, usage):
"""Stub Fitness carrying a one-space programme, so `usage_of` resolves."""
conf = dict(CONF_DEFAULTS)
conf["crinkliness_mode"] = mode
conf["spaces"] = {code: {"usage": usage, "size": [4.0, 1.0]}}
f = _StubCrink(conf, dict(COST_DEFAULTS))
f._stub = stub
return f, _leaf(code)
@pytest.mark.parametrize("usage", ["toilet", "utility", "none"])
def test_usage_daylight_exempts_uses_nobody_sits_in(usage):
"""A buried store or toilet is ordinary architecture, not a failure."""
f, leaf = _usage_fit("usage_daylight", 0.0, "x1", usage)
assert f.needs_daylight(leaf) is False
assert f.quality_uncrinkliness(leaf, None, {}) == 1.0
@pytest.mark.parametrize("usage", ["living", "kitchen", "bedroom"])
def test_usage_daylight_still_fails_a_windowless_habitable_room(usage):
"""The point of keying on usage: a bedroom with no daylight stays a hard
zero, exactly as stock. A mode that rescued this would be deleting the
fail category, not fixing the objective."""
f, leaf = _usage_fit("usage_daylight", 0.0, "x1", usage)
assert f.needs_daylight(leaf) is True
assert f.quality_uncrinkliness(leaf, None, {}) == 0.0
def test_usage_daylight_exempts_generic_types():
"""Generic `C`/`S` have no programme entry; a corridor needs no window."""
f, _ = _usage_fit("usage_daylight", 0.0, "x1", "living")
for code in ("C", "S"):
assert f.quality_uncrinkliness(_leaf(code), None, {}) == 1.0
def test_usage_daylight_still_punishes_over_exposure():
"""Exempt from needing daylight is not exempt from envelope cost: the
factor is clipped on the compact side only, never switched off."""
target = CONF_DEFAULTS["uncrinkliness"][0]
f, leaf = _usage_fit("usage_daylight", 1.0 / (target / 2), "x1", "utility")
assert f.quality_uncrinkliness(leaf, None, {}) < 1.0
def test_usage_daylight_leaves_stock_urb_untouched():
"""Same tree, mode off -> stock hard zero for every usage."""
for usage in ("living", "toilet", "none"):
f, leaf = _usage_fit("urb", 0.0, "x1", usage)
assert f.quality_uncrinkliness(leaf, None, {}) == 0.0
def test_crinkliness_mode_unknown_raises():
with pytest.raises(ValueError, match="crinkliness_mode"):
_stub_fit("nonsense")