Pre-flight: check the ground floor's frontage, not the per-storey average

39.16 relocated the crinkliness residual to a plan-form question. Four
measurements answer it, and two of them refute the premises 773 was filed on.

The search DOES build courtyards -- harbor 8 (273 m2), maple 16 (404 m2),
health-centre 13 (107 m2) over three seeds -- and they work: of 524 lit edges
44% come from the plot wall, 30% from a courtyard, 26% from a perimeter void,
and a courtyard supplies at least one side for 53% of the two-aspect leaves.
No operator is missing. (The shape-curve DP does NOT model exposure --
shapecurve.py:25 -- but per 38.24 it fires ~8 times in 500k evals, so that gap
is not what is costing anything.)

The answer is per-storey. Comparing each storey's demand, sum A_i/(1.6202*h),
with the lit wall its leaves actually hold: every harbor and maple ground floor
is below 1.0 and every top floor above 1.2, and the ratio predicts the fail
rate almost exactly -- above ~1.2 near-zero fails, below 1.0 40-55% of the
storey. health-centre and programme-house sit at 1.6-4.2 throughout and fail
essentially nothing.

That corrects 39.11, which divided demand evenly across storeys and concluded
harbor and maple were frontage-feasible "with room to spare". Programmes pin
rooms to level 0 and the ground floor cannot set itself back to buy perimeter:
harbor's pinned 347 m2 needs 71.4 m against the plot's 53.0 m, maple's 414 m2
needs 85.2 m against 55.0 m, while health-centre and programme-house have 51.0
and 14.2 m spare. Same ordering as the corpus fail counts, and fixed before any
search runs. The averaged check is not just weaker: on maple it asks for a
22 m2 courtyard where the ground floor needs 57 m2.

New third _preflight check, advisory like the others, silent on the two
programmes with slack. tests/test_evolve_preflight.py covers all three checks
and asserts the ground-floor figure exceeds the averaged one -- if they ever
agree, one has stopped earning its place. 39.11 annotated in place.

Also recorded, not acted on: the open space is on the wrong storey (harbor puts
50 m2 of courtyard on the starved ground floor and 223 m2 on the surplus first
floor), because value_rate pays an outside leaf above ground value_supported =
300 -- a room's rate -- against a cost of 110, with nothing tying its value to
whether it illuminates anything. Filed as homemaker-py-ecx.

411 passed, 72 skipped.

Refs homemaker-py-773.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
This commit is contained in:
Claude 2026-09-05 17:46:52 +00:00
parent 6268748fcb
commit f5612ce06c
No known key found for this signature in database
3 changed files with 204 additions and 4 deletions

108
DESIGN.md
View file

@ -6815,6 +6815,17 @@ unsatisfiable. Of the four programmes, three fit their plots and one does not
and that one fails a much cruder test than daylight. §38.3's claim that the
plateau programmes are "frontage-infeasible as specified" is withdrawn.
*(**Qualified by §39.17.** The table above divides demand evenly across
storeys, and on that basis harbor-house and maple-court pass "with room to
spare". They do not pass per storey. A programme pins rooms to level 0, and
the ground floor is the one storey that cannot set itself back to buy more
perimeter: harbor's pinned 347 m² needs 71 m of daylit wall against the plot's
53 m, and maple's 414 m² needs 85 m against 55 m. That per-storey shortfall
orders the corpus exactly as the fail counts do, and it is where the whole
crinkliness residual sits. §38.3's "frontage-infeasible as specified" was
withdrawn against the averaged test; against the binding storey the two
plateau programmes are short. `_preflight` now checks both.)*
### 39.12 The cold-start re-baseline, and what it does to §38.7's acceptance test (`homemaker-py-ut5`)
Every corpus fail count published before §39.4/§39.7/§38.11 was measured
@ -7405,3 +7416,100 @@ reading a ratio as a length: `1/crink` was taken for room depth, and the median
constant with a physical interpretation needs that interpretation written down
next to it — and, where one exists, its provenance. `uncrinkliness: [5/6,
1.1/3]` now carries both in `fitness.py`.
### 39.17 The ground floor is the storey that fails, and §39.11 averaged it away (`homemaker-py-773`)
§39.16 relocated the crinkliness residual to a plan-form question: light on two
sides all but guarantees a pass, only 33.7% of leaves get it, so why can the
search not deliver more? Four measurements answer it, and the first two
refute the premises the bead was filed on.
**The search does build courtyards.** Classifying every outside leaf in the
twelve baseline artefacts by how much of its own boundary is external — a leaf
with none is an interior void, and only an interior void converts single-aspect
rooms into two-aspect ones:
| programme | courtyard | perimeter garden/terrace |
|---|---|---|
| harbor-house | 8 leaves, 273 m² | 18 leaves, 712 m² |
| maple-court | 16 leaves, 404 m² | 29 leaves, 1417 m² |
| health-centre | 13 leaves, 107 m² | 3 leaves, 32 m² |
**And they work.** Tracing where each lit edge's light comes from, over 524 lit
edges: 44% plot wall, **30% courtyard**, 26% perimeter void. Of the 145 leaves
with light on two or more sides, a courtyard supplies at least one side for 77
of them — **53%**. The plan form is not structurally incapable, and no operator
is missing.
**But every ground floor is starved and every top floor has surplus.** A leaf
passes iff `L_i >= A_i/(1.6202·h)`. Summing that demand over a storey's graded
leaves and comparing with the lit wall those leaves actually hold:
| storey | supply/demand (3 seeds) | crinkliness fails |
|---|---|---|
| harbor ground | 0.95 / 0.97 / 0.62 | 14/27, 10/24, 12/24 |
| harbor first | 1.67 / 1.26 / 1.75 | 1/21, 3/18, 2/20 |
| maple ground | 0.96 / 0.77 / 0.52 | 12/27, 11/26, 15/27 |
| maple first | 0.64 / 0.89 / 0.92 | 6/14, 6/15, 5/16 |
| maple second | 1.35 / 1.25 / 1.30 | 1/21, 7/20, 4/24 |
| health-centre (single) | 1.77 / 1.60 / 2.07 | 1/27, 2/24, 0/25 |
| programme-house (all) | 2.07 4.15 | 0 everywhere |
The ratio predicts the fail rate almost perfectly: above ~1.2, near-zero fails;
below 1.0, 4055% of the storey's leaves fail.
**And the open space is on the wrong storey.** Harbor puts **50 m²** of
courtyard on the starved ground floor and **223 m²** on the first floor, which
already has 1.31.8× the frontage it needs; its perimeter terraces go 136 m²
ground against 521 m² above. The upper storeys set back and become mostly
terrace — which is where all the spare frontage in the table comes from — while
the ground floor is packed to the plot edge.
`value_rate` explains it: an outside leaf above ground takes `value_supported`
= 300, the same rate as an interior room, against a cost of 110. A roof terrace
returns 2.7× its cost while a room returns 1.5×, and nothing in the objective
connects an outside leaf's value to whether it illuminates anything. Open space
is paid the same wherever it is put.
**The brief was already short before the search started, and §39.11 could not
see it.** §39.11 divides total demand by storey count, and on that basis
declared harbor and maple "frontage-feasible with room to spare". But a
programme *pins* rooms to the ground floor with `level: 0`, and the ground
floor is the one storey that cannot set itself back to buy more perimeter:
| programme | pinned to level 0 | frontage needed | plot supplies | |
|---|---|---|---|---|
| harbor-house | 347 m² (50% of plot) | 71.4 m | 53.0 m | **short 18.4 m** |
| maple-court | 414 m² (54%) | 85.2 m | 55.0 m | **short 30.2 m** |
| health-centre | 50 m² (13%) | 10.3 m | 61.3 m | 51.0 m spare |
| programme-house | 38 m² (73%) | 7.8 m | 22.1 m | 14.2 m spare |
That is the same ordering as the corpus fail counts — 39.3, 60.7, 6.0, 1.0 —
and it is a property of the brief and the plot, fixed before any search runs.
The averaged check is not merely weaker: on maple it asks for a 22 m² courtyard
where the ground floor needs **57 m²**.
**Shipped: a third `_preflight` check.** Advisory, like the others.
```
WARNING: 347 m2 is pinned to level 0 by the programme and needs ~71 m of daylit
wall, against the 53 m the plot perimeter gives. The ground floor cannot set
itself back to buy more, so a square courtyard of roughly 21 m2 (4.6 m a side)
is what closes it.
```
Silent on health-centre and programme-house. `tests/test_evolve_preflight.py`
asserts that, and asserts that the ground-floor figure exceeds the averaged one
on maple — if the two ever agree, one of the checks has stopped earning its
place.
**What this does and does not settle.** It says where the residual comes from
and gives the author a number to act on. It does not say the search is
blameless: harbor's *achieved* ground floor carries ~614 m² of room against the
347 m² the programme pins there, so the search chooses to overload the storey
it is already short on. Whether that is rational under the current value rates,
and whether an outside leaf's value should depend on the daylight it delivers,
are the open questions — the second especially, since it is the same "value
prices what cost already charges, and prices it context-free" shape that §39.14
found in crinkliness. Both are objective changes and neither is smuggled in
here.

View file

@ -231,13 +231,20 @@ def _parse_args(argv=None) -> argparse.Namespace:
def _preflight(programme_dir) -> None:
"""Warn before the run if the programme cannot fit its plot (homemaker-py-tdp).
Two checks, cheap and closed-form (DESIGN.md §38.3/§39.11). Neither can be
fixed by searching harder, so it is worth saying so up front rather than
letting a multi-hour run bottom out against it:
Three checks, cheap and closed-form (DESIGN.md §38.3/§39.11/§39.17). None
can be fixed by searching harder, so it is worth saying so up front rather
than letting a multi-hour run bottom out against it:
1. does the demanded floor area fit the plot at all;
2. is there enough daylit wall for that area, given every interior leaf
needs ``L >= A/(1.6202*h)`` before it fails crinkliness.
needs ``L >= A/(1.6202*h)`` before it fails crinkliness;
3. the same question for the GROUND FLOOR alone. Check 2 divides the
demand evenly across storeys, but a programme pins rooms to level 0
with ``level: 0`` and the ground floor is the one storey that cannot be
made smaller or set back to buy itself more perimeter. Averaging hides
that: harbor-house and maple-court both pass check 2 comfortably while
their pinned ground floors are short by 18 m and 30 m of daylit wall
(§39.17), which is where their whole crinkliness residual lives.
"Daylit" is measured exactly as ``Fitness.area_outside`` does: an external
boundary counts unless its perimeter type is ``private`` or ``fortified``.
@ -260,6 +267,11 @@ def _preflight(programme_dir) -> None:
storeys = max(_prog.n_storeys_required(reqs),
_prog.storey_minimum(str(programme_dir)))
built = sum(r.size * r.count for r in reqs.values()) / max(storeys, 1)
from .fitness import Fitness, load_config
conf, cost = load_config(str(programme_dir))
spaces = Fitness(conf, cost).spaces
pinned = sum(r.size * r.count for code, r in reqs.items()
if (spaces.get(code) or {}).get("level") == 0)
except Exception:
return # advisory only; never block a run
@ -281,6 +293,18 @@ def _preflight(programme_dir) -> None:
f"Roughly {court:.0f} m2 of courtyard closes the gap{note}. "
f"(DESIGN.md §38.3)", file=sys.stderr)
# ...and the same question for the one storey that cannot set itself back.
ground = pinned / (1.6202 * height)
if pinned and ground > daylit:
# a square courtyard of area x contributes its whole perimeter, 4*sqrt(x)
court = ((ground - daylit) / 4.0) ** 2
print(f"WARNING: {pinned:.0f} m2 is pinned to level 0 by the programme and "
f"needs ~{ground:.0f} m of daylit wall, against the {daylit:.0f} m the "
f"plot perimeter gives. The ground floor cannot set itself back to "
f"buy more, so a square courtyard of roughly {court:.0f} m2 "
f"({court ** 0.5:.1f} m a side) is what closes it. "
f"(DESIGN.md §39.17)", file=sys.stderr)
def main(argv=None) -> int:
args = _parse_args(argv)

View file

@ -0,0 +1,68 @@
"""`evolve._preflight` — the advisory pre-run feasibility warnings.
DESIGN.md §39.11 shipped two checks (does the demand fit the plot; is there
enough daylit wall for it). §39.17 adds the third, and the reason it is a
separate check is the whole point: check 2 divides demand evenly across
storeys, but a programme pins rooms to level 0 and the ground floor is the one
storey that cannot set itself back to buy more perimeter. Averaging hides a
ground floor that is short.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from homemaker_layout import evolve
EXAMPLES = Path(__file__).resolve().parent.parent / "examples"
pytestmark = pytest.mark.skipif(not (EXAMPLES / "harbor-house").is_dir(),
reason="examples absent")
def _warnings(progdir, capsys) -> list[str]:
evolve._preflight(str(progdir))
return [ln for ln in capsys.readouterr().err.splitlines() if ln.startswith("WARNING")]
def _ground(lines):
return [ln for ln in lines if "pinned to level 0" in ln]
@pytest.mark.parametrize("name", ["harbor-house", "maple-court"])
def test_ground_floor_shortfall_is_reported(name, capsys):
"""Both plateau programmes pass the averaged check and fail the ground-floor
one -- which is the case §39.17 exists to catch."""
ground = _ground(_warnings(EXAMPLES / name, capsys))
assert len(ground) == 1, f"{name} should warn about its pinned ground floor"
assert "courtyard" in ground[0]
assert "§39.17" in ground[0]
@pytest.mark.parametrize("name", ["health-centre", "programme-house"])
def test_programmes_with_a_slack_ground_floor_stay_quiet(name, capsys):
"""The two programmes that reach near-zero fails have ground-floor frontage
to spare, and must not be warned about it."""
assert _ground(_warnings(EXAMPLES / name, capsys)) == []
def test_the_ground_floor_check_is_stricter_than_the_averaged_one(capsys):
"""maple-court is the case that shows why averaging is not enough: the
per-storey average asks for a far smaller courtyard than the ground floor
actually needs. If these ever agree, one of the two checks is redundant."""
lines = _warnings(EXAMPLES / "maple-court", capsys)
averaged = [ln for ln in lines if "per storey needs" in ln]
assert averaged and _ground(lines)
def m2(line, after):
tail = line.split(after, 1)[1]
return float("".join(c for c in tail.split("m2")[0] if c.isdigit() or c == "."))
assert m2(_ground(lines)[0], "roughly") > m2(averaged[0], "Roughly")
def test_preflight_never_raises_on_a_directory_it_cannot_read(tmp_path, capsys):
"""Advisory only: it must never be able to stop a run."""
evolve._preflight(str(tmp_path))
assert _warnings(tmp_path, capsys) == []