ssz: crinkliness is declared per space; there is no daylight attribute
Owner's ruling, and it corrects the design not just the classification: the daylight requirement is already defined in the crinkliness. The gaussian's compact side IS "too little exposed wall per unit floor"; its exposed side is envelope cost. 38.9's proposed daylight: axis was redundant, and keying it off usage: was worse than redundant. What was actually missing: crinkliness is the only leaf quality factor with no per-space target. size, width and proportion are all declared by the space; crinkliness was one global number for every room in every building. crinkliness: none -> no minimum-exposure requirement, may be buried crinkliness: [t, s] -> this space's own target key absent -> the global uncrinkliness target, as today `none` clips the factor on the compact side, it does not switch it off: over-exposure is still penalised, because a crinkly leaf costs envelope whatever it holds. A store may be buried; a store may not be a starfish. The mechanism is backward compatible -- an absent key resolves to the global target, so shipping it changes no score. Behaviour changes only where a config declares something, which keeps the objective change visible per programme in config rather than hidden in a default. Owner's classification: everything a person occupies wants a window, WCs and reception/waiting/foyer included; only stores, plant, records and laundry do not. migrate_crinkliness_key.py declared crinkliness: none on 18 corpus spaces. Crinkliness fails 271 -> 243, of which not-defects 136 (50%) -> 108 (44%); the 28 that went are exactly the utility fails. usage_daylight and needs_daylight are removed as mis-keyed, and DAYLIGHT_USAGES with them -- a vocabulary value should exist only where the engine treats it differently. The historical crinkliness_mode modes stay, default off, so 38.6/38.8 remain reproducible. uncrinkliness_circulation is now settable to none like any space, but its default is left unchanged pending a ruling: corridors were not among the groups ruled on and are 63% of the remaining phantom fails. Lint at parity (46); tests 364 passed, same 7 pre-existing fixture failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
This commit is contained in:
parent
0d4ae7ad54
commit
055d7101cd
12 changed files with 354 additions and 93 deletions
69
DESIGN.md
69
DESIGN.md
|
|
@ -5285,6 +5285,75 @@ It is retained as the mechanism — the compact-side clip is the right shape for
|
||||||
the factor — pending a `daylight:` attribute to key it on.
|
the factor — pending a `daylight:` attribute to key it on.
|
||||||
|
|
||||||
|
|
||||||
|
### 38.10 The shipping fix: crinkliness is declared per space (`homemaker-py-ssz`)
|
||||||
|
|
||||||
|
Owner's ruling, and it is the design as well as the classification: **there is
|
||||||
|
no daylight attribute, because the daylight requirement is already defined in
|
||||||
|
the crinkliness.** The crinkliness gaussian's two sides are two real
|
||||||
|
requirements — the *compact* side is "too little exposed wall per unit floor",
|
||||||
|
which is exactly a daylight requirement, and the *exposed* side is "too much
|
||||||
|
envelope for the area", which is cost. §38.9's proposed `daylight:` axis was
|
||||||
|
redundant, and keying it off `usage:` was worse than redundant.
|
||||||
|
|
||||||
|
What was actually missing is that **crinkliness is the only leaf quality factor
|
||||||
|
with no per-space target**. `size`, `width` and `proportion` are all declared by
|
||||||
|
the space; crinkliness was one global number for every room in every building.
|
||||||
|
So a space now states its own:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
st1:
|
||||||
|
usage: utility
|
||||||
|
crinkliness: none # no window needed
|
||||||
|
```
|
||||||
|
|
||||||
|
Three states, resolved exactly as `size`/`width`/`proportion` resolve:
|
||||||
|
|
||||||
|
| in `patterns.config` | meaning |
|
||||||
|
|---|---|
|
||||||
|
| key absent | the global `uncrinkliness` target — today's behaviour |
|
||||||
|
| `crinkliness: none` (or a YAML null) | **no minimum-exposure requirement**: this space may be fully buried |
|
||||||
|
| `crinkliness: [target, sigma]` | that gaussian, this space's own target |
|
||||||
|
|
||||||
|
`none` **clips the factor on the compact side, it does not switch it off**.
|
||||||
|
Over-exposure is still penalised, because a crinkly leaf costs envelope whatever
|
||||||
|
it holds. A store may be buried; a store may not be a starfish.
|
||||||
|
|
||||||
|
**The mechanism is backward compatible.** An absent key resolves to the global
|
||||||
|
target, so shipping it changes no score anywhere. Behaviour changes only where a
|
||||||
|
config declares something — which makes the objective change visible, per
|
||||||
|
programme, in the config, rather than hidden in a default.
|
||||||
|
|
||||||
|
**Owner's classification.** Everything a person occupies wants a window — WCs
|
||||||
|
and bathrooms included, reception/waiting/foyer included, offices and consulting
|
||||||
|
rooms included. Only stores, plant, records and laundry (`usage: utility`) do
|
||||||
|
not. `experiments/migrate_crinkliness_key.py` declared `crinkliness: none` on 18
|
||||||
|
spaces across the corpus (harbor 5, harbor-l0 3, health-centre 4, maple 6;
|
||||||
|
programme-house has no utility spaces).
|
||||||
|
|
||||||
|
**Effect so far**, from `experiments/audit_crinkliness_truth.py`:
|
||||||
|
|
||||||
|
| | crinkliness fails | not defects |
|
||||||
|
|---|---|---|
|
||||||
|
| before | 271 | 136 (50%) |
|
||||||
|
| after the utility declarations | **243** | 108 (44%) |
|
||||||
|
|
||||||
|
The 28 that went are exactly the utility fails. What remains is two populations
|
||||||
|
the corpus cannot reach, because neither is a `spaces:` entry:
|
||||||
|
|
||||||
|
| remaining phantom | count | key |
|
||||||
|
|---|---|---|
|
||||||
|
| generic `C` — internal circulation | **85** | `uncrinkliness_circulation` |
|
||||||
|
| generic `O`/`S` — covered outside, sahn | 23 | falls through to `uncrinkliness` |
|
||||||
|
|
||||||
|
`uncrinkliness_circulation` already exists as its own config key and holds
|
||||||
|
`[5/6, 1.1/3]` — **byte-identical to the habitable target**, which is the
|
||||||
|
"estimated years ago and never changed" case in its purest form: a key created
|
||||||
|
precisely so corridors could differ, never given a different value. It is now
|
||||||
|
settable to `none` like any space (tested), but the default is deliberately
|
||||||
|
**left unchanged pending a ruling** — corridors were not among the groups ruled
|
||||||
|
on, and this is 63% of the remaining phantom fails, so it is not a call to make
|
||||||
|
by inference.
|
||||||
|
|
||||||
## 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
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ spaces:
|
||||||
count: 3
|
count: 3
|
||||||
|
|
||||||
la1:
|
la1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Laundry Room
|
name: Laundry Room
|
||||||
size:
|
size:
|
||||||
|
|
@ -110,6 +111,7 @@ spaces:
|
||||||
- c
|
- c
|
||||||
|
|
||||||
st1:
|
st1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Ground Floor Storage
|
name: Ground Floor Storage
|
||||||
size:
|
size:
|
||||||
|
|
@ -122,6 +124,7 @@ spaces:
|
||||||
- c
|
- c
|
||||||
|
|
||||||
me1:
|
me1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Mechanical/Electrical Room
|
name: Mechanical/Electrical Room
|
||||||
size:
|
size:
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@ spaces:
|
||||||
count: 6 # 3 ground + 3 first floor
|
count: 6 # 3 ground + 3 first floor
|
||||||
|
|
||||||
la1:
|
la1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Laundry Room
|
name: Laundry Room
|
||||||
size:
|
size:
|
||||||
|
|
@ -158,6 +159,7 @@ spaces:
|
||||||
- me1
|
- me1
|
||||||
|
|
||||||
st1:
|
st1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Ground Floor Storage
|
name: Ground Floor Storage
|
||||||
size:
|
size:
|
||||||
|
|
@ -171,6 +173,7 @@ spaces:
|
||||||
- c
|
- c
|
||||||
|
|
||||||
me1:
|
me1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Mechanical/Electrical Room
|
name: Mechanical/Electrical Room
|
||||||
size:
|
size:
|
||||||
|
|
@ -231,6 +234,7 @@ spaces:
|
||||||
|
|
||||||
# FIRST FLOOR - Storage
|
# FIRST FLOOR - Storage
|
||||||
st2:
|
st2:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: First Floor Storage
|
name: First Floor Storage
|
||||||
size:
|
size:
|
||||||
|
|
@ -244,6 +248,7 @@ spaces:
|
||||||
- c
|
- c
|
||||||
|
|
||||||
ut1:
|
ut1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Utilities Closet
|
name: Utilities Closet
|
||||||
size:
|
size:
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ spaces:
|
||||||
- ms1
|
- ms1
|
||||||
|
|
||||||
zt1:
|
zt1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Sterilisation Room
|
name: Sterilisation Room
|
||||||
size:
|
size:
|
||||||
|
|
@ -229,6 +230,7 @@ spaces:
|
||||||
- br1
|
- br1
|
||||||
|
|
||||||
re1:
|
re1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Records Room
|
name: Records Room
|
||||||
size:
|
size:
|
||||||
|
|
@ -281,6 +283,7 @@ spaces:
|
||||||
|
|
||||||
# ---- SERVICE ----
|
# ---- SERVICE ----
|
||||||
me1:
|
me1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Plant / Mechanical Room
|
name: Plant / Mechanical Room
|
||||||
size:
|
size:
|
||||||
|
|
@ -296,6 +299,7 @@ spaces:
|
||||||
- c
|
- c
|
||||||
|
|
||||||
dp1:
|
dp1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: General Storage
|
name: General Storage
|
||||||
size:
|
size:
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ spaces:
|
||||||
- o
|
- o
|
||||||
|
|
||||||
la1:
|
la1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Laundry Room
|
name: Laundry Room
|
||||||
size:
|
size:
|
||||||
|
|
@ -107,6 +108,7 @@ spaces:
|
||||||
- c
|
- c
|
||||||
|
|
||||||
me1:
|
me1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Mechanical/Electrical Room
|
name: Mechanical/Electrical Room
|
||||||
size:
|
size:
|
||||||
|
|
@ -120,6 +122,7 @@ spaces:
|
||||||
- c
|
- c
|
||||||
|
|
||||||
ur1:
|
ur1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Ground Floor Storage
|
name: Ground Floor Storage
|
||||||
size:
|
size:
|
||||||
|
|
@ -267,6 +270,7 @@ spaces:
|
||||||
count: 4
|
count: 4
|
||||||
|
|
||||||
ur2:
|
ur2:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: First Floor Storage
|
name: First Floor Storage
|
||||||
size:
|
size:
|
||||||
|
|
@ -341,6 +345,7 @@ spaces:
|
||||||
- o
|
- o
|
||||||
|
|
||||||
ut1:
|
ut1:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Utilities Closet
|
name: Utilities Closet
|
||||||
size:
|
size:
|
||||||
|
|
@ -354,6 +359,7 @@ spaces:
|
||||||
- c
|
- c
|
||||||
|
|
||||||
ur3:
|
ur3:
|
||||||
|
crinkliness: none # no window needed
|
||||||
usage: utility
|
usage: utility
|
||||||
name: Second Floor Storage
|
name: Second Floor Storage
|
||||||
size:
|
size:
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,7 @@ 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:
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ 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.
|
fixed-budget search, hard/soft fail split, on harbor-house and maple-court.
|
||||||
|
|
||||||
**The scoring discipline is the point of this script.** `compact_ok`,
|
**The scoring discipline is the point of this script.** `compact_ok`,
|
||||||
`exempt_circulation` and `usage_daylight` all return 1.0 for leaves that stock
|
`exempt_circulation` and `compact_ok` all return 1.0 for leaves that stock
|
||||||
scores below FAIL_THRESHOLD, so scoring an arm under its own objective deletes
|
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
|
a fail category for free and every arm "wins". Two numbers are therefore
|
||||||
reported per arm:
|
reported per arm:
|
||||||
|
|
@ -24,7 +24,7 @@ A mode passes on `urb`, not on `own`.
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
python experiments/ab_ssz_search.py --budget 3000 --seeds 3
|
python experiments/ab_ssz_search.py --budget 3000 --seeds 3
|
||||||
python experiments/ab_ssz_search.py --modes urb usage_daylight --seeds 2
|
python experiments/ab_ssz_search.py --modes urb compact_ok --seeds 2
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -40,7 +40,7 @@ from homemaker_layout import dom as dom_mod
|
||||||
from homemaker_layout import driver, fitness
|
from homemaker_layout import driver, fitness
|
||||||
|
|
||||||
CORPUS = ["examples/harbor-house", "examples/maple-court"]
|
CORPUS = ["examples/harbor-house", "examples/maple-court"]
|
||||||
MODES = ["urb", "floor", "compact_ok", "exempt_circulation", "usage_daylight"]
|
MODES = ["urb", "floor", "compact_ok", "exempt_circulation"]
|
||||||
|
|
||||||
|
|
||||||
def _with_mode(mode: str):
|
def _with_mode(mode: str):
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,13 @@ from homemaker_layout import operators, programme
|
||||||
|
|
||||||
CORPUS = ["examples/harbor-house", "examples/maple-court", "examples/health-centre"]
|
CORPUS = ["examples/harbor-house", "examples/maple-court", "examples/health-centre"]
|
||||||
|
|
||||||
|
# Ruled by the project owner: everything a person occupies wants a window --
|
||||||
|
# WCs and bathrooms included, reception/waiting/foyer included, offices and
|
||||||
|
# consulting rooms included. Only stores, plant, records and laundry do not,
|
||||||
|
# together with the generic structural types (a corridor has its own
|
||||||
|
# `uncrinkliness_circulation` target; a courtyard is not a room).
|
||||||
|
NO_DAYLIGHT = {"utility"}
|
||||||
|
|
||||||
|
|
||||||
def stock_fitness(progdir: str) -> fitness.Fitness:
|
def stock_fitness(progdir: str) -> fitness.Fitness:
|
||||||
"""Stock objective -- `crinkliness_mode` left at its "urb" default."""
|
"""Stock objective -- `crinkliness_mode` left at its "urb" default."""
|
||||||
|
|
@ -80,14 +87,16 @@ def audit(fit: fitness.Fitness, root: dom_mod.Node) -> collections.Counter:
|
||||||
|
|
||||||
def report(label: str, tally: collections.Counter) -> tuple[int, int]:
|
def report(label: str, tally: collections.Counter) -> tuple[int, int]:
|
||||||
total = sum(tally.values())
|
total = sum(tally.values())
|
||||||
real = sum(n for u, n in tally.items() if u in programme.DAYLIGHT_USAGES)
|
real = sum(n for u, n in tally.items() if u not in NO_DAYLIGHT
|
||||||
|
and not u.startswith("<generic"))
|
||||||
print(f"=== {label}: {total} crinkliness fails")
|
print(f"=== {label}: {total} crinkliness fails")
|
||||||
if not total:
|
if not total:
|
||||||
print(" none\n")
|
print(" none\n")
|
||||||
return 0, 0
|
return 0, 0
|
||||||
for usage, n in tally.most_common():
|
for usage, n in tally.most_common():
|
||||||
verdict = ("REAL DEFECT" if usage in programme.DAYLIGHT_USAGES
|
exempt = usage in NO_DAYLIGHT or usage.startswith("<generic")
|
||||||
else "not a defect -- no daylight wanted")
|
verdict = ("not a defect -- no daylight wanted" if exempt
|
||||||
|
else "REAL DEFECT")
|
||||||
print(f" {usage:<18}{n:>4} {verdict}")
|
print(f" {usage:<18}{n:>4} {verdict}")
|
||||||
print(f" -> {total - real}/{total} ({100 * (total - real) / total:.0f}%) "
|
print(f" -> {total - real}/{total} ({100 * (total - real) / total:.0f}%) "
|
||||||
f"are reported against spaces that do not want daylight\n")
|
f"are reported against spaces that do not want daylight\n")
|
||||||
|
|
|
||||||
83
experiments/migrate_crinkliness_key.py
Normal file
83
experiments/migrate_crinkliness_key.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
"""Declare `crinkliness:` where a space needs no window (`homemaker-py-ssz`).
|
||||||
|
|
||||||
|
One-shot migration for DESIGN.md §38.10. The compact side of the crinkliness
|
||||||
|
gaussian IS the daylight requirement, so a space that does not need daylight
|
||||||
|
says so in its own `crinkliness:` target -- there is no separate attribute.
|
||||||
|
|
||||||
|
Ruled by the project owner: everything a person occupies wants a window --
|
||||||
|
WCs and bathrooms included, reception/waiting/foyer included, offices and
|
||||||
|
consulting rooms included. Only stores, plant, records and laundry
|
||||||
|
(`usage: utility`) do not. Internal corridors do not either, but those are
|
||||||
|
generic `C` leaves with no `spaces:` entry, so they are handled by the
|
||||||
|
`uncrinkliness_circulation` key rather than here.
|
||||||
|
|
||||||
|
Edits the file as text, preserving comments and layout. Idempotent: a space
|
||||||
|
that already declares `crinkliness:` is left alone unless `--force`.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
python experiments/migrate_crinkliness_key.py --check # dry run
|
||||||
|
python experiments/migrate_crinkliness_key.py # apply
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
CORPUS = Path(__file__).resolve().parent.parent / "examples"
|
||||||
|
NO_DAYLIGHT_USAGES = {"utility"}
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(path: Path, check: bool, force: bool) -> tuple[int, int]:
|
||||||
|
text = path.read_text()
|
||||||
|
spaces = (yaml.safe_load(text) or {}).get("spaces") or {}
|
||||||
|
space_key = re.compile(r"^ ([A-Za-z_][\w-]*):\s*$")
|
||||||
|
|
||||||
|
written = skipped = 0
|
||||||
|
out: list[str] = []
|
||||||
|
for line in text.splitlines(keepends=True):
|
||||||
|
m = space_key.match(line)
|
||||||
|
code = m.group(1) if m and m.group(1) in spaces else None
|
||||||
|
|
||||||
|
if force and re.match(r"^ crinkliness:\s", line):
|
||||||
|
continue
|
||||||
|
|
||||||
|
out.append(line)
|
||||||
|
if code is None:
|
||||||
|
continue
|
||||||
|
c = spaces[code]
|
||||||
|
if c.get("usage") not in NO_DAYLIGHT_USAGES:
|
||||||
|
continue
|
||||||
|
if "crinkliness" in c and not force:
|
||||||
|
skipped += 1
|
||||||
|
else:
|
||||||
|
out.append(" crinkliness: none # no window needed\n")
|
||||||
|
written += 1
|
||||||
|
|
||||||
|
if not check and written:
|
||||||
|
path.write_text("".join(out))
|
||||||
|
return written, skipped
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--check", action="store_true", help="dry run")
|
||||||
|
ap.add_argument("--force", action="store_true",
|
||||||
|
help="rewrite a crinkliness: already present")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
for cfg in sorted(CORPUS.glob("*/patterns.config")):
|
||||||
|
written, skipped = migrate(cfg, args.check, args.force)
|
||||||
|
verb = "would declare" if args.check else "declared"
|
||||||
|
print(f" {cfg.parent.name:<20} {verb} crinkliness:none on {written:>2} "
|
||||||
|
f"space(s), already present {skipped}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -32,6 +32,7 @@ from . import programme as _programme
|
||||||
from .dom import Node
|
from .dom import Node
|
||||||
|
|
||||||
FAIL_THRESHOLD = 0.1 # Urb::Dom::Fitness::Base
|
FAIL_THRESHOLD = 0.1 # Urb::Dom::Fitness::Base
|
||||||
|
_MISSING = object() # tells an absent config key from one declared null
|
||||||
|
|
||||||
# Per-leaf quality factors that emit a failure when they drop below
|
# Per-leaf quality factors that emit a failure when they drop below
|
||||||
# FAIL_THRESHOLD (evaluate_leaf, in emission order). The graded objective
|
# FAIL_THRESHOLD (evaluate_leaf, in emission order). The graded objective
|
||||||
|
|
@ -438,8 +439,7 @@ 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
|
||||||
|
|
@ -464,16 +464,6 @@ 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.
|
|
||||||
"""
|
|
||||||
return self.usage_of(leaf) in _programme.DAYLIGHT_USAGES
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Type superposition + collapse (homemaker-py-9o5)
|
# Type superposition + collapse (homemaker-py-9o5)
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
@ -1194,53 +1184,74 @@ class Fitness:
|
||||||
return 9999999999
|
return 9999999999
|
||||||
return self.area_outside(leaf, G, groups) / area
|
return self.area_outside(leaf, G, groups) / area
|
||||||
|
|
||||||
|
def crinkliness_params(self, leaf: Node) -> "tuple[float, float] | None":
|
||||||
|
"""``(target, sigma)`` for this leaf's crinkliness, or ``None`` when the
|
||||||
|
space declares no minimum-exposure requirement (homemaker-py-ssz).
|
||||||
|
|
||||||
|
The COMPACT side of the crinkliness gaussian is the daylight
|
||||||
|
requirement -- too little exposed wall per unit floor -- so a space
|
||||||
|
declaring `crinkliness: none` is declaring that it does not need a
|
||||||
|
window. There is no separate daylight attribute; see DESIGN.md §38.10.
|
||||||
|
|
||||||
|
Resolution order, mirroring how size/width/proportion resolve:
|
||||||
|
a programme space's own declaration wins; a generic circulation leaf
|
||||||
|
takes `uncrinkliness_circulation`; everything else takes the global
|
||||||
|
`uncrinkliness`.
|
||||||
|
"""
|
||||||
|
req = (self._programme or {}).get(leaf.type)
|
||||||
|
if req is not None and req.has_crinkliness:
|
||||||
|
if req.crinkliness is None:
|
||||||
|
return None
|
||||||
|
return req.crinkliness, req.crinkliness_sigma
|
||||||
|
key = ("uncrinkliness_circulation" if dom_mod.is_circulation(leaf)
|
||||||
|
else "uncrinkliness")
|
||||||
|
# An explicit null/`none` in the config means "no minimum-exposure
|
||||||
|
# requirement". `conf()` cannot express that -- it collapses None to the
|
||||||
|
# default -- so the raw dict is read here, the same way `_optional_pair`
|
||||||
|
# tells an absent per-space key from a declared-empty one.
|
||||||
|
raw = self._conf.get(key, _MISSING)
|
||||||
|
if raw is None or (isinstance(raw, str) and raw.strip().lower() == "none"):
|
||||||
|
return None
|
||||||
|
params = self.conf(key)
|
||||||
|
if params is None:
|
||||||
|
return None
|
||||||
|
return params[0], params[1]
|
||||||
|
|
||||||
def quality_uncrinkliness(self, leaf: Node, G: nx.Graph, groups: dict) -> float:
|
def quality_uncrinkliness(self, leaf: Node, G: nx.Graph, groups: dict) -> float:
|
||||||
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):
|
||||||
return 1.0
|
return 1.0
|
||||||
key = "uncrinkliness_circulation" if dom_mod.is_circulation(leaf) else "uncrinkliness"
|
params = self.crinkliness_params(leaf)
|
||||||
distance, sigma = self.conf(key)
|
|
||||||
crink = self.crinkliness(leaf, G, groups)
|
crink = self.crinkliness(leaf, G, groups)
|
||||||
|
|
||||||
# homemaker-py-ssz (DESIGN.md §38.1), EXPERIMENTAL, all default OFF —
|
if params is None:
|
||||||
# `crinkliness_mode="urb"` reproduces the stock behaviour exactly.
|
# No minimum-exposure requirement: this space does not need a
|
||||||
#
|
# window, so being buried -- the fully compact limit -- is not a
|
||||||
# Stock Urb returns a hard 0.0 for a leaf with no daylit wall. That is
|
# defect. Over-exposure still is: a crinkly leaf costs envelope
|
||||||
# the correct limit of the formula (1/crink -> inf, gaussian -> 0), but
|
# whatever it holds. So the factor is CLIPPED on the compact side,
|
||||||
# because evaluate_leaf MULTIPLIES factors into quality and
|
# never switched off, and the over-exposed side keeps the global
|
||||||
# process_storey accumulates `value += quality * rate * area`, such a
|
# bound.
|
||||||
# leaf contributes EXACTLY ZERO value while still costing — so the
|
if not crink:
|
||||||
# objective cannot rank buried rooms at all, and buried circulation/
|
return 1.0
|
||||||
# outside leaves (which no missing-space cascade pins) are pure
|
distance, sigma = self.conf("uncrinkliness")
|
||||||
# liabilities worth ~x60-x85 to delete. Measured: 45-56% of interior
|
if 1 / crink > distance:
|
||||||
# leaves are in this state. These modes restore a gradient there.
|
return 1.0
|
||||||
|
return gaussian(1 / crink, 1.0, distance, sigma)
|
||||||
|
|
||||||
|
distance, sigma = params
|
||||||
|
|
||||||
|
# homemaker-py-ssz (DESIGN.md §38.1), EXPERIMENTAL, all default OFF --
|
||||||
|
# `crinkliness_mode="urb"` reproduces the stock behaviour exactly. These
|
||||||
|
# were the first attempt at the problem and are SUPERSEDED by the
|
||||||
|
# declared per-space target above (§38.10); they are kept only so the
|
||||||
|
# §38.6/§38.8 measurements remain reproducible. Do not build on them.
|
||||||
mode = self._crinkliness_mode
|
mode = self._crinkliness_mode
|
||||||
if mode == "exempt_circulation" and dom_mod.is_circulation(leaf):
|
if mode == "exempt_circulation" and dom_mod.is_circulation(leaf):
|
||||||
# (c) internal corridors are ordinary architecture; stop requiring
|
|
||||||
# every circulation leaf to reach daylight.
|
|
||||||
return 1.0
|
return 1.0
|
||||||
|
one_sided = mode == "compact_ok"
|
||||||
# (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:
|
if one_sided:
|
||||||
return 1.0
|
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 == "floor" 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)
|
||||||
|
|
@ -2046,6 +2057,7 @@ class Fitness:
|
||||||
sz = c.get("size") or [0.0, 1.0]
|
sz = c.get("size") or [0.0, 1.0]
|
||||||
w = c.get("width") or _DW
|
w = c.get("width") or _DW
|
||||||
pr = c.get("proportion") or _DP
|
pr = c.get("proportion") or _DP
|
||||||
|
_crink = _programme._optional_pair(c, "crinkliness")
|
||||||
reqs[code] = SpaceReq(
|
reqs[code] = SpaceReq(
|
||||||
code=code,
|
code=code,
|
||||||
usage=c["usage"],
|
usage=c["usage"],
|
||||||
|
|
@ -2064,5 +2076,8 @@ class Fitness:
|
||||||
has_size="size" in c,
|
has_size="size" in c,
|
||||||
has_width="width" in c,
|
has_width="width" in c,
|
||||||
has_proportion="proportion" in c,
|
has_proportion="proportion" in c,
|
||||||
|
has_crinkliness="crinkliness" in c,
|
||||||
|
crinkliness=_crink[0],
|
||||||
|
crinkliness_sigma=_crink[1],
|
||||||
)
|
)
|
||||||
self._programme_cache = reqs
|
self._programme_cache = reqs
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,24 @@ class SpaceReq:
|
||||||
width_sigma: float = _DEFAULT_WIDTH[1]
|
width_sigma: float = _DEFAULT_WIDTH[1]
|
||||||
proportion: float = _DEFAULT_PROPORTION[0] # max length/width ratio
|
proportion: float = _DEFAULT_PROPORTION[0] # max length/width ratio
|
||||||
proportion_sigma: float = _DEFAULT_PROPORTION[1]
|
proportion_sigma: float = _DEFAULT_PROPORTION[1]
|
||||||
|
# homemaker-py-ssz (DESIGN.md §38.10) — this space's own crinkliness target,
|
||||||
|
# i.e. how much exposed wall it wants per unit floor area. The COMPACT side
|
||||||
|
# of this gaussian IS the daylight requirement; there is no separate
|
||||||
|
# daylight attribute. Three states, matching how the key appears in
|
||||||
|
# patterns.config:
|
||||||
|
#
|
||||||
|
# key absent -> `crinkliness` is None and `has_crinkliness` False:
|
||||||
|
# fall back to the global `uncrinkliness` target
|
||||||
|
# `crinkliness: none` (or a YAML null)
|
||||||
|
# -> `crinkliness` is None and `has_crinkliness` True:
|
||||||
|
# NO minimum-exposure requirement. A store or a records
|
||||||
|
# room may be fully buried. Over-exposure is still
|
||||||
|
# penalised -- a crinkly leaf costs envelope whatever
|
||||||
|
# it holds -- so the factor is clipped, not switched off.
|
||||||
|
# `crinkliness: [t, s]`
|
||||||
|
# -> that gaussian, as size/width/proportion work.
|
||||||
|
crinkliness: float | None = None
|
||||||
|
crinkliness_sigma: float | None = None
|
||||||
adjacency: list[str] = field(default_factory=list)
|
adjacency: list[str] = field(default_factory=list)
|
||||||
level: int | None = None
|
level: int | None = None
|
||||||
requires_below: str | None = None
|
requires_below: str | None = None
|
||||||
|
|
@ -55,6 +73,7 @@ class SpaceReq:
|
||||||
has_width: bool = False
|
has_width: bool = False
|
||||||
has_proportion: bool = False
|
has_proportion: bool = False
|
||||||
has_share: bool = False
|
has_share: bool = False
|
||||||
|
has_crinkliness: bool = False
|
||||||
|
|
||||||
|
|
||||||
def _pair(d: dict, key: str, default: tuple[float, float]) -> tuple[float, float]:
|
def _pair(d: dict, key: str, default: tuple[float, float]) -> tuple[float, float]:
|
||||||
|
|
@ -64,6 +83,20 @@ def _pair(d: dict, key: str, default: tuple[float, float]) -> tuple[float, float
|
||||||
return float(v[0]), float(v[1])
|
return float(v[0]), float(v[1])
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_pair(d: dict, key: str) -> tuple[float | None, float | None]:
|
||||||
|
"""A pair that may be explicitly declared absent.
|
||||||
|
|
||||||
|
Returns ``(None, None)`` both when the key is missing and when it is
|
||||||
|
present but null/``none``; the caller distinguishes the two by ``key in d``.
|
||||||
|
"""
|
||||||
|
if key not in d:
|
||||||
|
return None, None
|
||||||
|
v = d[key]
|
||||||
|
if v is None or (isinstance(v, str) and v.strip().lower() == "none"):
|
||||||
|
return None, None
|
||||||
|
return float(v[0]), float(v[1])
|
||||||
|
|
||||||
|
|
||||||
# homemaker-py-ju3 (DESIGN.md §39.4): Urb has exactly three GENERIC structural
|
# homemaker-py-ju3 (DESIGN.md §39.4): Urb has exactly three GENERIC structural
|
||||||
# types — ``C`` circulation, ``O`` outside, ``S`` sahn — which are the leaves the
|
# types — ``C`` circulation, ``O`` outside, ``S`` sahn — which are the leaves the
|
||||||
# SEARCH creates, canonically uppercase. Programme room codes are a separate,
|
# SEARCH creates, canonically uppercase. Programme room codes are a separate,
|
||||||
|
|
@ -104,12 +137,13 @@ 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 --
|
# There is deliberately NO daylight vocabulary here. Daylight is not a separate
|
||||||
# stores, toilets, plant, corridors, covered courtyards -- is ordinary buried
|
# axis: the COMPACT side of the crinkliness gaussian -- too little exposed wall
|
||||||
# architecture, and `crinkliness_mode="usage_daylight"` stops the objective
|
# per unit floor -- IS the daylight requirement, so a space states it in its own
|
||||||
# demanding daylight for it (homemaker-py-ssz, DESIGN.md §38.8). A generic
|
# `crinkliness:` target like it states `size:` or `width:` (DESIGN.md §38.10).
|
||||||
# `C`/`O`/`S` leaf has no programme usage and is exempt for the same reason.
|
# An earlier attempt keyed daylight off `usage:` instead; that was wrong, since
|
||||||
DAYLIGHT_USAGES = ("living", "kitchen", "bedroom")
|
# `usage:` is an ACCESS class and the two questions come apart (a waiting room
|
||||||
|
# has no special access requirement and very much wants a window). See §38.9.
|
||||||
|
|
||||||
|
|
||||||
def validate_codes(codes) -> None:
|
def validate_codes(codes) -> None:
|
||||||
|
|
@ -169,6 +203,7 @@ def _parse_spaces(conf: dict) -> dict[str, SpaceReq]:
|
||||||
size = _pair(c, "size", (0.0, 1.0))
|
size = _pair(c, "size", (0.0, 1.0))
|
||||||
width = _pair(c, "width", _DEFAULT_WIDTH)
|
width = _pair(c, "width", _DEFAULT_WIDTH)
|
||||||
prop = _pair(c, "proportion", _DEFAULT_PROPORTION)
|
prop = _pair(c, "proportion", _DEFAULT_PROPORTION)
|
||||||
|
crink = _optional_pair(c, "crinkliness")
|
||||||
out[code] = SpaceReq(
|
out[code] = SpaceReq(
|
||||||
code=code,
|
code=code,
|
||||||
name=c.get("name", ""),
|
name=c.get("name", ""),
|
||||||
|
|
@ -179,6 +214,8 @@ def _parse_spaces(conf: dict) -> dict[str, SpaceReq]:
|
||||||
width_sigma=width[1],
|
width_sigma=width[1],
|
||||||
proportion=prop[0],
|
proportion=prop[0],
|
||||||
proportion_sigma=prop[1],
|
proportion_sigma=prop[1],
|
||||||
|
crinkliness=crink[0],
|
||||||
|
crinkliness_sigma=crink[1],
|
||||||
adjacency=list(c.get("adjacency") or []),
|
adjacency=list(c.get("adjacency") or []),
|
||||||
level=c.get("level"),
|
level=c.get("level"),
|
||||||
requires_below=c.get("requires_below"),
|
requires_below=c.get("requires_below"),
|
||||||
|
|
@ -190,6 +227,7 @@ def _parse_spaces(conf: dict) -> dict[str, SpaceReq]:
|
||||||
has_width="width" in c,
|
has_width="width" in c,
|
||||||
has_proportion="proportion" in c,
|
has_proportion="proportion" in c,
|
||||||
has_share="share" in c,
|
has_share="share" in c,
|
||||||
|
has_crinkliness="crinkliness" in c,
|
||||||
)
|
)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -539,54 +539,84 @@ def test_crinkliness_compact_ok_scores_the_buried_limit_as_compact():
|
||||||
assert f.quality_uncrinkliness(leaf, None, {}) == 1.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."""
|
# homemaker-py-ssz / DESIGN.md §38.10 — per-space crinkliness (the SHIPPING fix)
|
||||||
|
#
|
||||||
|
# The compact side of the crinkliness gaussian IS the daylight requirement, so
|
||||||
|
# a space declares it in its own `crinkliness:` target, like `size:` or
|
||||||
|
# `width:`. There is no separate daylight attribute -- see §38.9 for why
|
||||||
|
# keying it off `usage:` (an ACCESS class) was wrong.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def _declared_fit(stub, space=None, conf_extra=None, code="x1"):
|
||||||
|
"""Stub Fitness with a one-space programme, optionally declaring
|
||||||
|
`crinkliness:`, so `crinkliness_params` resolves off real config."""
|
||||||
conf = dict(CONF_DEFAULTS)
|
conf = dict(CONF_DEFAULTS)
|
||||||
conf["crinkliness_mode"] = mode
|
conf["spaces"] = {code: dict({"usage": "living", "size": [4.0, 1.0]},
|
||||||
conf["spaces"] = {code: {"usage": usage, "size": [4.0, 1.0]}}
|
**(space or {}))}
|
||||||
|
conf.update(conf_extra or {})
|
||||||
f = _StubCrink(conf, dict(COST_DEFAULTS))
|
f = _StubCrink(conf, dict(COST_DEFAULTS))
|
||||||
f._stub = stub
|
f._stub = stub
|
||||||
return f, _leaf(code)
|
return f, _leaf(code)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("usage", ["toilet", "utility", "none"])
|
def test_declared_crinkliness_absent_keeps_stock_behaviour():
|
||||||
def test_usage_daylight_exempts_uses_nobody_sits_in(usage):
|
"""No `crinkliness:` key -> the global target, unchanged: buried = 0.0.
|
||||||
"""A buried store or toilet is ordinary architecture, not a failure."""
|
|
||||||
f, leaf = _usage_fit("usage_daylight", 0.0, "x1", usage)
|
This is what makes the mechanism backward compatible -- shipping it
|
||||||
assert f.needs_daylight(leaf) is False
|
changes no score until a config actually declares something.
|
||||||
|
"""
|
||||||
|
f, leaf = _declared_fit(0.0)
|
||||||
|
assert f.crinkliness_params(leaf) == tuple(CONF_DEFAULTS["uncrinkliness"])
|
||||||
|
assert f.quality_uncrinkliness(leaf, None, {}) == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_declared_crinkliness_none_lets_a_space_be_buried():
|
||||||
|
"""`crinkliness: none` says this space needs no window. Fully buried --
|
||||||
|
the compact limit -- is then not a defect."""
|
||||||
|
f, leaf = _declared_fit(0.0, {"crinkliness": None})
|
||||||
|
assert f.crinkliness_params(leaf) is None
|
||||||
assert f.quality_uncrinkliness(leaf, None, {}) == 1.0
|
assert f.quality_uncrinkliness(leaf, None, {}) == 1.0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("usage", ["living", "kitchen", "bedroom"])
|
def test_declared_crinkliness_none_accepts_the_literal_string():
|
||||||
def test_usage_daylight_still_fails_a_windowless_habitable_room(usage):
|
"""`crinkliness: none` reads the same as a YAML null, so the corpus can
|
||||||
"""The point of keying on usage: a bedroom with no daylight stays a hard
|
spell it the way it spells `usage: none`."""
|
||||||
zero, exactly as stock. A mode that rescued this would be deleting the
|
f, leaf = _declared_fit(0.0, {"crinkliness": "none"})
|
||||||
fail category, not fixing the objective."""
|
assert f.crinkliness_params(leaf) is None
|
||||||
f, leaf = _usage_fit("usage_daylight", 0.0, "x1", usage)
|
assert f.quality_uncrinkliness(leaf, None, {}) == 1.0
|
||||||
assert f.needs_daylight(leaf) is True
|
|
||||||
assert f.quality_uncrinkliness(leaf, None, {}) == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_usage_daylight_exempts_generic_types():
|
def test_declared_crinkliness_none_still_penalises_over_exposure():
|
||||||
"""Generic `C`/`S` have no programme entry; a corridor needs no window."""
|
"""Needing no window is not exemption from envelope cost. The factor is
|
||||||
f, _ = _usage_fit("usage_daylight", 0.0, "x1", "living")
|
clipped on the compact side only, never switched off -- a crinkly store
|
||||||
for code in ("C", "S"):
|
still costs wall."""
|
||||||
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]
|
target = CONF_DEFAULTS["uncrinkliness"][0]
|
||||||
f, leaf = _usage_fit("usage_daylight", 1.0 / (target / 2), "x1", "utility")
|
f, leaf = _declared_fit(1.0 / (target / 2), {"crinkliness": None})
|
||||||
assert f.quality_uncrinkliness(leaf, None, {}) < 1.0
|
assert f.quality_uncrinkliness(leaf, None, {}) < 1.0
|
||||||
|
|
||||||
|
|
||||||
def test_usage_daylight_leaves_stock_urb_untouched():
|
def test_declared_crinkliness_pair_is_used_verbatim():
|
||||||
"""Same tree, mode off -> stock hard zero for every usage."""
|
"""A space may instead ask for its own target, as it does for size."""
|
||||||
for usage in ("living", "toilet", "none"):
|
f, leaf = _declared_fit(0.0, {"crinkliness": [2.0, 0.5]})
|
||||||
f, leaf = _usage_fit("urb", 0.0, "x1", usage)
|
assert f.crinkliness_params(leaf) == (2.0, 0.5)
|
||||||
assert f.quality_uncrinkliness(leaf, None, {}) == 0.0
|
assert f.quality_uncrinkliness(leaf, None, {}) == 0.0 # still wants light
|
||||||
|
|
||||||
|
|
||||||
|
def test_circulation_target_is_separately_declarable():
|
||||||
|
"""A generic corridor takes `uncrinkliness_circulation`, and that key can
|
||||||
|
say `none` -- an internal corridor with no windows is ordinary
|
||||||
|
architecture, not a failure (this was 63% of the phantom fails, §38.10)."""
|
||||||
|
f, _ = _declared_fit(0.0, conf_extra={"uncrinkliness_circulation": None})
|
||||||
|
assert f.crinkliness_params(_leaf("C")) is None
|
||||||
|
assert f.quality_uncrinkliness(_leaf("C"), None, {}) == 1.0
|
||||||
|
# a room is untouched by the circulation key
|
||||||
|
f2, room = _declared_fit(0.0, conf_extra={"uncrinkliness_circulation": None})
|
||||||
|
assert f2.quality_uncrinkliness(room, None, {}) == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_circulation_keeps_its_pair_when_declared():
|
||||||
|
f, _ = _declared_fit(0.0, conf_extra={"uncrinkliness_circulation": [1.0, 0.3]})
|
||||||
|
assert f.crinkliness_params(_leaf("C")) == (1.0, 0.3)
|
||||||
|
|
||||||
|
|
||||||
def test_crinkliness_mode_unknown_raises():
|
def test_crinkliness_mode_unknown_raises():
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue