124 lines
5 KiB
Python
124 lines
5 KiB
Python
|
|
"""`quality_aggregate="geometric_mean"` (homemaker-py-ecx, DESIGN.md §39.18).
|
||
|
|
|
||
|
|
Quality is a product over the factors, and leaf kinds face different numbers of
|
||
|
|
them: a room is judged on size, crinkliness and access, an outside leaf is
|
||
|
|
exempt from all three. Exemption alone therefore buys a higher quality, and
|
||
|
|
quality multiplies the value rate. The geometric mean divides that out.
|
||
|
|
|
||
|
|
Two invariants matter and both are asserted here:
|
||
|
|
|
||
|
|
* the fail set cannot move, because `evaluate_leaf` emits each fail from the
|
||
|
|
factor itself before anything is combined;
|
||
|
|
* `factor_is_asked` must agree with the `quality_*` methods -- whenever it says
|
||
|
|
a factor is exempt, that factor really is exactly 1.0. It is a separate
|
||
|
|
statement of the same conditions, so it can drift; this pins it.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import copy
|
||
|
|
import math
|
||
|
|
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"
|
||
|
|
PROGRAMMES = ["harbor-house", "maple-court", "health-centre", "programme-house"]
|
||
|
|
pytestmark = pytest.mark.skipif(not (EXAMPLES / "harbor-house").is_dir(),
|
||
|
|
reason="examples absent")
|
||
|
|
|
||
|
|
|
||
|
|
def _artefacts():
|
||
|
|
for name in PROGRAMMES:
|
||
|
|
d = EXAMPLES / name
|
||
|
|
if not d.is_dir():
|
||
|
|
continue
|
||
|
|
for p in sorted(d.glob("coldstart-500000-s*.dom")) + [d / "init.dom"]:
|
||
|
|
if p.exists():
|
||
|
|
yield d, p
|
||
|
|
|
||
|
|
|
||
|
|
def test_exempt_factors_really_are_one():
|
||
|
|
"""The invariant `factor_is_asked` rests on, checked against every leaf in
|
||
|
|
the corpus rather than assumed from reading the code."""
|
||
|
|
checked = 0
|
||
|
|
for d, p in _artefacts():
|
||
|
|
conf, cost = load_config(d)
|
||
|
|
fit = Fitness(conf, cost)
|
||
|
|
seen = []
|
||
|
|
orig = Fitness.evaluate_leaf
|
||
|
|
|
||
|
|
def ev(self, leaf, G, level_id, groups, fail, _o=orig, _s=seen):
|
||
|
|
q, f = _o(self, leaf, G, level_id, groups, fail)
|
||
|
|
_s.append((leaf, dict(f)))
|
||
|
|
return q, f
|
||
|
|
|
||
|
|
Fitness.evaluate_leaf = ev
|
||
|
|
try:
|
||
|
|
fit.score_with_fails(dom_mod.load(str(p)))
|
||
|
|
finally:
|
||
|
|
Fitness.evaluate_leaf = orig
|
||
|
|
|
||
|
|
for leaf, factors in seen:
|
||
|
|
for name, value in factors.items():
|
||
|
|
if not fit.factor_is_asked(name, leaf):
|
||
|
|
assert value == 1.0, (
|
||
|
|
f"{p.name}: {name} is marked exempt for leaf "
|
||
|
|
f"{leaf.id!r} ({leaf.type!r}) but scored {value}")
|
||
|
|
checked += 1
|
||
|
|
assert checked > 100, "expected plenty of exempt factors to check"
|
||
|
|
|
||
|
|
|
||
|
|
def test_fail_set_is_byte_identical():
|
||
|
|
for d, p in _artefacts():
|
||
|
|
root = dom_mod.load(str(p))
|
||
|
|
c_prod, cost = load_config(d)
|
||
|
|
c_geo, _ = load_config(d, overrides={"quality_aggregate": "geometric_mean"})
|
||
|
|
_, f_prod = Fitness(c_prod, cost).score_with_fails(copy.deepcopy(root))
|
||
|
|
_, f_geo = Fitness(c_geo, cost).score_with_fails(copy.deepcopy(root))
|
||
|
|
assert f_prod == f_geo, f"{p} changed its fail set under the geometric mean"
|
||
|
|
|
||
|
|
|
||
|
|
def test_geometric_mean_is_the_product_when_every_factor_is_asked():
|
||
|
|
"""No free lunch: a leaf asked all six should agree with `prod ** (1/6)`."""
|
||
|
|
fit = Fitness(*load_config(EXAMPLES / "harbor-house"))
|
||
|
|
leaf = dom_mod.Node(type="r")
|
||
|
|
factors = {"perpendicular": 0.9, "proportion": 0.8, "size": 0.5,
|
||
|
|
"width": 0.95, "crinkliness": 0.4, "access": 1.0, "daylight": 1.0}
|
||
|
|
asked = [v for k, v in factors.items() if fit.factor_is_asked(k, leaf)]
|
||
|
|
expected = math.prod(asked) ** (1.0 / len(asked))
|
||
|
|
assert fit._aggregate_geometric(leaf, factors) == pytest.approx(expected)
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_zero_factor_still_makes_the_leaf_worthless():
|
||
|
|
"""A fully buried leaf is worth nothing under either aggregation -- the
|
||
|
|
geometric mean must not launder a zero into 0.4-ish."""
|
||
|
|
fit = Fitness(*load_config(EXAMPLES / "harbor-house"))
|
||
|
|
leaf = dom_mod.Node(type="r")
|
||
|
|
factors = {"perpendicular": 1.0, "proportion": 1.0, "size": 1.0,
|
||
|
|
"width": 1.0, "crinkliness": 0.0, "access": 1.0, "daylight": 1.0}
|
||
|
|
assert fit._aggregate_geometric(leaf, factors) == 0.0
|
||
|
|
|
||
|
|
|
||
|
|
def test_it_does_not_underflow_where_the_product_would():
|
||
|
|
"""The point of computing in log space: six small factors multiply to a
|
||
|
|
denormal, but their geometric mean is an ordinary number."""
|
||
|
|
fit = Fitness(*load_config(EXAMPLES / "harbor-house"))
|
||
|
|
leaf = dom_mod.Node(type="r")
|
||
|
|
tiny = 1e-60
|
||
|
|
factors = {k: tiny for k in ("perpendicular", "proportion", "size",
|
||
|
|
"width", "crinkliness", "access")}
|
||
|
|
factors["daylight"] = 1.0
|
||
|
|
assert math.prod(factors[k] for k in factors) == 0.0 # product underflows
|
||
|
|
assert fit._aggregate_geometric(leaf, factors) == pytest.approx(tiny, rel=1e-6)
|
||
|
|
|
||
|
|
|
||
|
|
def test_unknown_aggregate_is_rejected():
|
||
|
|
conf, cost = load_config(EXAMPLES / "harbor-house",
|
||
|
|
overrides={"quality_aggregate": "mean"})
|
||
|
|
with pytest.raises(ValueError, match="unknown quality_aggregate"):
|
||
|
|
Fitness(conf, cost)
|