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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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], {})))
|
||||
|
|
|
|||
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._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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue