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
|
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.
|
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`)
|
### 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
|
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
|
constant with a physical interpretation needs that interpretation written down
|
||||||
next to it — and, where one exists, its provenance. `uncrinkliness: [5/6,
|
next to it — and, where one exists, its provenance. `uncrinkliness: [5/6,
|
||||||
1.1/3]` now carries both in `fitness.py`.
|
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:
|
def _preflight(programme_dir) -> None:
|
||||||
"""Warn before the run if the programme cannot fit its plot (homemaker-py-tdp).
|
"""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
|
Three checks, cheap and closed-form (DESIGN.md §38.3/§39.11/§39.17). None
|
||||||
fixed by searching harder, so it is worth saying so up front rather than
|
can be fixed by searching harder, so it is worth saying so up front rather
|
||||||
letting a multi-hour run bottom out against it:
|
than letting a multi-hour run bottom out against it:
|
||||||
|
|
||||||
1. does the demanded floor area fit the plot at all;
|
1. does the demanded floor area fit the plot at all;
|
||||||
2. is there enough daylit wall for that area, given every interior leaf
|
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
|
"Daylit" is measured exactly as ``Fitness.area_outside`` does: an external
|
||||||
boundary counts unless its perimeter type is ``private`` or ``fortified``.
|
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),
|
storeys = max(_prog.n_storeys_required(reqs),
|
||||||
_prog.storey_minimum(str(programme_dir)))
|
_prog.storey_minimum(str(programme_dir)))
|
||||||
built = sum(r.size * r.count for r in reqs.values()) / max(storeys, 1)
|
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:
|
except Exception:
|
||||||
return # advisory only; never block a run
|
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"Roughly {court:.0f} m2 of courtyard closes the gap{note}. "
|
||||||
f"(DESIGN.md §38.3)", file=sys.stderr)
|
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:
|
def main(argv=None) -> int:
|
||||||
args = _parse_args(argv)
|
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