78 lines
3.1 KiB
Python
78 lines
3.1 KiB
Python
|
|
"""A terrace must not be worth more per square metre than a real internal room.
|
||
|
|
|
||
|
|
Owner's ruling, DESIGN.md §39.19. It is an invariant of the objective rather
|
||
|
|
than a property of any one layout, and it needs BOTH halves of §39.18/§39.19 to
|
||
|
|
hold — the rate alone leaves a terrace 1.46x a room, the aggregation alone
|
||
|
|
2.23x. This asserts it end to end on the corpus, so that flipping either back
|
||
|
|
fails loudly here rather than quietly in a six-hour run.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from collections import defaultdict
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from homemaker_layout import dom as dom_mod, geometry
|
||
|
|
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")
|
||
|
|
|
||
|
|
|
||
|
|
def _value_per_m2(overrides=None):
|
||
|
|
"""(room, terrace) mean value per m2 over the committed baseline layouts."""
|
||
|
|
acc = defaultdict(lambda: [0.0, 0.0])
|
||
|
|
for name in CORPUS:
|
||
|
|
d = EXAMPLES / name
|
||
|
|
for p in sorted(d.glob("coldstart-500000-s*.dom")):
|
||
|
|
conf, cost = load_config(d, overrides=overrides)
|
||
|
|
fit = Fitness(conf, cost)
|
||
|
|
orig = Fitness.evaluate_leaf
|
||
|
|
|
||
|
|
def ev(self, leaf, G, level_id, groups, fail, _o=orig):
|
||
|
|
q, f = _o(self, leaf, G, level_id, groups, fail)
|
||
|
|
if dom_mod.is_outside(leaf):
|
||
|
|
kind = "terrace" if dom_mod.level_of(leaf) else None
|
||
|
|
elif dom_mod.is_circulation(leaf):
|
||
|
|
kind = None
|
||
|
|
else:
|
||
|
|
kind = "room"
|
||
|
|
if kind:
|
||
|
|
a = geometry.area(leaf)
|
||
|
|
acc[kind][0] += a
|
||
|
|
acc[kind][1] += q * self.value_rate(leaf) * a
|
||
|
|
return q, f
|
||
|
|
|
||
|
|
Fitness.evaluate_leaf = ev
|
||
|
|
try:
|
||
|
|
fit.score_with_fails(dom_mod.load(str(p)))
|
||
|
|
finally:
|
||
|
|
Fitness.evaluate_leaf = orig
|
||
|
|
return tuple(acc[k][1] / acc[k][0] for k in ("room", "terrace"))
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_terrace_is_worth_less_per_m2_than_a_room():
|
||
|
|
room, terrace = _value_per_m2()
|
||
|
|
assert terrace < room, (
|
||
|
|
f"terrace {terrace:.1f}/m2 >= room {room:.1f}/m2 — §39.19 ruling broken")
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_rate_alone_would_not_be_enough():
|
||
|
|
"""Documents why the aggregation default moved with the rate: revert the
|
||
|
|
aggregation and the ruling breaks again, so neither half is optional."""
|
||
|
|
room, terrace = _value_per_m2({"quality_aggregate": "product"})
|
||
|
|
assert terrace > room, (
|
||
|
|
"the product aggregation no longer violates the ruling — if that is a "
|
||
|
|
"real improvement, §39.19's reasoning needs revisiting")
|
||
|
|
|
||
|
|
|
||
|
|
def test_value_supported_is_the_outdoor_rate_not_the_indoor_one():
|
||
|
|
for name in CORPUS:
|
||
|
|
conf, _ = load_config(EXAMPLES / name)
|
||
|
|
assert conf["value_supported"] == conf["value_outside"], name
|
||
|
|
assert conf["value_supported"] < conf["value_inside"], name
|