72 lines
3 KiB
Python
72 lines
3 KiB
Python
|
|
"""Outdoor space is a per-level requirement, not a fraction (DESIGN.md §39.25).
|
||
|
|
|
||
|
|
Owner: "Alexander simply says that all levels should have accessible outside
|
||
|
|
space, he doesn't say how much."
|
||
|
|
|
||
|
|
The codebase had both rules and had them the wrong way round: the qualitative
|
||
|
|
one Alexander states (`force_roof_garden`, which fails a level with no outdoor
|
||
|
|
space at all) was switched OFF in every corpus config, while the quantitative
|
||
|
|
one he does not state (`ratio_outside`, a gaussian on the outdoor fraction) was
|
||
|
|
switched on with four mutually contradictory targets.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import copy
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from homemaker_layout import dom as dom_mod
|
||
|
|
from homemaker_layout.fitness import Fitness, load_config
|
||
|
|
|
||
|
|
EXAMPLES = Path(__file__).resolve().parent.parent / "examples"
|
||
|
|
CORPUS = ["harbor-house", "maple-court", "health-centre", "programme-house"]
|
||
|
|
pytestmark = pytest.mark.skipif(not (EXAMPLES / "harbor-house").is_dir(),
|
||
|
|
reason="examples absent")
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("name", CORPUS)
|
||
|
|
def test_the_qualitative_rule_is_on_and_the_quantitative_one_is_off(name):
|
||
|
|
conf, _ = load_config(EXAMPLES / name)
|
||
|
|
assert conf["force_roof_garden"], (
|
||
|
|
"Alexander's requirement -- every level has accessible outside space --"
|
||
|
|
" must actually be enforced")
|
||
|
|
assert conf["ratio_outside"] is None, (
|
||
|
|
"the outdoor FRACTION is not a rule Alexander states (§39.25)")
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_level_with_no_outdoor_space_fails():
|
||
|
|
"""The rule has to bite, or turning it on achieved nothing. programme-house
|
||
|
|
puts no outdoor space on its ground floor in every baseline seed."""
|
||
|
|
d = EXAMPLES / "programme-house"
|
||
|
|
seen = 0
|
||
|
|
for p in sorted(d.glob("coldstart-500000-s*.dom")):
|
||
|
|
conf, cost = load_config(d)
|
||
|
|
_, fails = Fitness(conf, cost).score_with_fails(dom_mod.load(str(p)))
|
||
|
|
assert any("no outside space" in f for f in fails), p.name
|
||
|
|
seen += 1
|
||
|
|
assert seen == 3
|
||
|
|
|
||
|
|
|
||
|
|
def test_that_failure_is_hard():
|
||
|
|
"""It is a structural provision no ratio-solve can supply, so it must tier
|
||
|
|
HARD -- a soft fail would let the search buy it off with shape quality."""
|
||
|
|
from homemaker_layout.fitness import classify_fail_tier
|
||
|
|
assert classify_fail_tier("level 1 no outside space") == "hard"
|
||
|
|
|
||
|
|
|
||
|
|
def test_disabling_the_fraction_removes_no_failure():
|
||
|
|
"""`ratio_outside` was a value multiplier, never a fail source, so the only
|
||
|
|
fail-set movement in §39.25 comes from switching the per-level rule ON."""
|
||
|
|
for name in CORPUS:
|
||
|
|
d = EXAMPLES / name
|
||
|
|
for p in sorted(d.glob("coldstart-500000-s*.dom")):
|
||
|
|
root = dom_mod.load(str(p))
|
||
|
|
conf, cost = load_config(d)
|
||
|
|
with_frac, _ = load_config(
|
||
|
|
d, overrides={"ratio_outside": [0.15, 0.1]})
|
||
|
|
_, f_off = Fitness(conf, cost).score_with_fails(copy.deepcopy(root))
|
||
|
|
_, f_on = Fitness(with_frac, cost).score_with_fails(copy.deepcopy(root))
|
||
|
|
assert f_off == f_on, f"{p.name}: ratio_outside moved a failure"
|