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:
parent
6268748fcb
commit
f5612ce06c
3 changed files with 204 additions and 4 deletions
108
DESIGN.md
108
DESIGN.md
|
|
@ -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, 40–55% 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.3–1.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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
68
tests/test_evolve_preflight.py
Normal file
68
tests/test_evolve_preflight.py
Normal 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) == []
|
||||
Loading…
Add table
Reference in a new issue