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:
parent
8522576467
commit
189efdbfc7
7 changed files with 367 additions and 12 deletions
101
DESIGN.md
101
DESIGN.md
|
|
@ -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
|
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
|
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
|
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
|
programme alone.
|
||||||
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
|
*(As first written this paragraph continued "two of the four corpus programmes
|
||||||
part, an unsatisfiable constraint.
|
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
|
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
|
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
|
pre-flight bound and re-baseline the corpus), and only then resume
|
||||||
`2g7.9`/`2g7.10`.
|
`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
|
**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
|
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
|
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
|
corridor it just inserted will have its work reverted by the next selection
|
||||||
step.
|
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
|
## 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
|
The corpus `patterns.config` targets and `costs.config` values were estimated
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,17 @@ buried or lit is what separates the two mechanisms:
|
||||||
against a ×0.5 connectivity penalty (§38.2 refinement) — which no
|
against a ×0.5 connectivity penalty (§38.2 refinement) — which no
|
||||||
``crinkliness_mode`` can touch.
|
``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::
|
Usage::
|
||||||
|
|
||||||
python experiments/ab_crinkliness_mode_ssz.py
|
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 graph as graph_mod
|
||||||
from homemaker_layout import operators, programme
|
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:
|
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():
|
for leaf in lvl.leaves():
|
||||||
if dom_mod.is_outside(leaf) and not dom_mod.is_covered(leaf):
|
if dom_mod.is_outside(leaf) and not dom_mod.is_covered(leaf):
|
||||||
continue
|
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
|
continue
|
||||||
out.append((li, leaf.id, leaf.type,
|
out.append((li, leaf.id, leaf.type,
|
||||||
fit.area_outside(leaf, graphs[li], {})))
|
fit.area_outside(leaf, graphs[li], {})))
|
||||||
|
|
|
||||||
145
experiments/ab_ssz_search.py
Normal file
145
experiments/ab_ssz_search.py
Normal 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()
|
||||||
3
experiments/results/ab_ssz_search.csv
Normal file
3
experiments/results/ab_ssz_search.csv
Normal 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
|
||||||
|
|
|
@ -438,7 +438,8 @@ class Fitness:
|
||||||
self._connectivity_weight = float(cw)
|
self._connectivity_weight = float(cw)
|
||||||
self._crinkliness_mode = str(self.conf("crinkliness_mode") or "urb")
|
self._crinkliness_mode = str(self.conf("crinkliness_mode") or "urb")
|
||||||
if self._crinkliness_mode not in (
|
if self._crinkliness_mode not in (
|
||||||
"urb", "floor", "compact_ok", "exempt_circulation"):
|
"urb", "floor", "compact_ok", "exempt_circulation",
|
||||||
|
"usage_daylight"):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"unknown crinkliness_mode: {self._crinkliness_mode!r}")
|
f"unknown crinkliness_mode: {self._crinkliness_mode!r}")
|
||||||
# The floored value stays BELOW FAIL_THRESHOLD, so a buried leaf still
|
# 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)
|
req = (self._programme or {}).get(leaf.type)
|
||||||
return req.usage if req else ""
|
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)
|
# Type superposition + collapse (homemaker-py-9o5)
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
@ -1207,15 +1219,33 @@ class Fitness:
|
||||||
# (c) internal corridors are ordinary architecture; stop requiring
|
# (c) internal corridors are ordinary architecture; stop requiring
|
||||||
# every circulation leaf to reach daylight.
|
# every circulation leaf to reach daylight.
|
||||||
return 1.0
|
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:
|
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
|
# (a) floor: keep buried leaves rankable by their other factors
|
||||||
# instead of collapsing the whole quality product to zero.
|
# 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)
|
q = gaussian(1 / crink, 1.0, distance, sigma)
|
||||||
if mode == "compact_ok" and 1 / crink > distance:
|
if one_sided 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.
|
|
||||||
return 1.0
|
return 1.0
|
||||||
return max(q, self._crinkliness_floor) if mode in ("floor", "compact_ok") else q
|
return max(q, self._crinkliness_floor) if mode in ("floor", "compact_ok") else q
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,12 @@ TOILET_STRIPS = ("living", "kitchen", "toilet")
|
||||||
# Sociable rooms keep their MOST central circulation neighbour; terminal rooms
|
# Sociable rooms keep their MOST central circulation neighbour; terminal rooms
|
||||||
# and toilets keep their LEAST central one.
|
# and toilets keep their LEAST central one.
|
||||||
SOCIABLE_USAGES = ("living", "kitchen")
|
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:
|
def validate_codes(codes) -> None:
|
||||||
|
|
|
||||||
|
|
@ -528,6 +528,67 @@ def test_crinkliness_exempt_circulation_only_exempts_circulation():
|
||||||
assert f.quality_uncrinkliness(room, None, {}) == 0.0
|
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():
|
def test_crinkliness_mode_unknown_raises():
|
||||||
with pytest.raises(ValueError, match="crinkliness_mode"):
|
with pytest.raises(ValueError, match="crinkliness_mode"):
|
||||||
_stub_fit("nonsense")
|
_stub_fit("nonsense")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue