homemaker-layout/tests/test_fitness_aggregate.py
Claude 99c85ec4ee
Remove two dead paths from the objective
39.24's sweep listed two entries as DEAD rather than suspect -- inert code that
reads as live. Neither changes a score or a failure on any corpus artefact, and
that is verified rather than asserted: every artefact scores identically to its
39.25 measurement.

ratio_public_outside and ratio_private_outside. evaluate_building read both and
multiplied a gaussian into the building factor for each. Neither key exists in
CONF_DEFAULTS and no patterns.config in the repository declares either, so both
branches were guarded and never ran. Removing them also retires what fed them:
the four public_length_*/private_length_* tracking keys accumulated per leaf in
process_storey, and the _public_length/_private_length helpers, which had no
other caller.

NOT removed, because they are live: _public_access, _public_access_outside,
_public_access_pins and the has_public_access_* tracking flags, which drive real
checks and collapse_global's preserve_public_access. Only the length-ratio
machinery was dead.

The daylight quality factor. evaluate_leaf set factors["daylight"] = 1.0
unconditionally -- pinned since the URB_NO_OCCLUSION descope (6) and unable to
be anything else. It was never in _GRADED_FACTORS, so it contributed nothing to
the graded signal, and 39.18's geometric mean then had to special-case it in
factor_is_asked as a factor that is never asked. A constant that exists only to
be excluded is worth deleting. If 2g5 rebuilds occlusion it reintroduces a real
daylight factor, which would need factor_is_asked to say True anyway.

Two tests referenced the removed factor. test_leaf_grade_ignores_non_graded_keys
now names a key that genuinely does not exist; the aggregate underflow test
dropped its daylight entry, which would otherwise have been counted as asked and
changed the expected geometric mean.

Worth doing despite changing no number: 39.20 and 39.25 were both cases where
something inert looked live -- a parity test that never ran, a per-level rule
switched off in every config -- and in both the misreading cost real time and
produced a wrong conclusion. An objective with fewer things in it that do
nothing is one where "this term does nothing" is informative rather than
routine.

Still open on dpt, each needing a ruling or a rate change rather than a
measurement: quality_size's upper side, the minimum-internal-area factor as a
third statement of "build the rooms", and the 0.5**n_fails curve.

426 passed.

Refs homemaker-py-dpt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-09-06 18:11:09 +00:00

122 lines
5.1 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, overrides={"quality_aggregate": "product"})
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, overrides={"quality_aggregate": "product"})
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}
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")}
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)