homemaker-layout/tests/test_fitness_crinkliness.py

209 lines
9.1 KiB
Python
Raw Permalink Normal View History

Rescale the underflowing crinkliness tail so the failing region has an ordering quality_uncrinkliness evaluates a gaussian at x = 1/crink, so its exponent grows like 1/crink^2 and underflows a double to exactly zero below crink ~ 1/15. Measured over the twelve 500k cold-start runs (39.12): 430 leaves carry a minimum-exposure requirement, 112 fail it, and those 112 span quality 1e-300..1e-1 while contributing 0.034% of total value on 23% of the floor area. Every value in that range is numerically zero beside a passing leaf's ~1, so the search cannot rank two layouts that differ only in how exposed their under-lit rooms are. This is wider than the bead's diagnosis (a flat 0.0 for zero-exposure leaves) and it explains why 38.1's `floor` mode measured as a no-op: max(q, 0.01) maps 110 of the 112 onto one constant, replacing a flat zero with a flat 0.01. crinkliness_tail="ramp" (default OFF, "gaussian" is stock) replaces the tail -- only the tail, only below FAIL_THRESHOLD, only on the compact side -- with a straight line in crinkliness meeting the gaussian exactly at the crossing. _crink_at_fail_threshold inverts the gaussian there using the same truncated _E the factor is evaluated with. Deliberately conservative: nothing at or above FAIL_THRESHOLD moves, so no calibration changes and no leaf crosses the threshold. The fail set is byte-identical on all 21 committed corpus artefacts, the four init.dom seeds included -- asserted in tests/test_fitness_crinkliness_tail.py, not assumed. That invariance is also what makes it legal to score both arms of the A/B under stock (the 38.9 trap's one exemption). A fully buried leaf still scores exactly 0; this restores an ordering within the failing region, it does not forgive it. Composing with 38.1's superseded modes is refused, since both rewrite the same tail. Score effect on the baseline artefacts: +0.3%..+2.8% on harbor and maple, exactly +0.000% on health-centre, programme-house, and every init.dom -- a programme with no partially-exposed failing rooms has nothing to grade, and neither does any starting layout. The ramp is a mid-search signal by construction, so experiments/ab_9gj_ramp.py defaults to seeding each run from a 500k plateau artefact rather than cold. The module-level math import replaces a now-redundant local one. DESIGN.md 39.13 and the A/B verdict follow in a separate commit. Refs homemaker-py-9gj. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-09-05 06:29:02 +00:00
"""The `crinkliness_tail="ramp"` rescale (homemaker-py-9gj, DESIGN.md §39.13).
The whole design rests on one invariant: the ramp rewrites the failing compact
tail and NOTHING else, so no leaf crosses FAIL_THRESHOLD and the fail set is
byte-identical to stock. That is what makes it legal to score both arms of the
A/B under the stock objective (the §38.9 trap's one exemption). It is asserted
here on every committed corpus artefact rather than assumed.
"""
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 (
FAIL_THRESHOLD, Fitness, _crink_at_fail_threshold, gaussian, load_config,
)
EXAMPLES = Path(__file__).resolve().parent.parent / "examples"
PROGRAMMES = ["harbor-house", "maple-court", "health-centre", "programme-house"]
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")) + \
sorted(d.glob("evolved-3M*.dom")) + [d / "init.dom"]:
if p.exists():
yield d, p
def test_crossing_is_continuous_at_the_fail_threshold():
"""The ramp meets the gaussian exactly at FAIL_THRESHOLD, so the factor is
continuous there and the ordering across the boundary is preserved."""
for distance, sigma in ((5.0 / 6, 1.1 / 3), (1.2, 0.25), (0.5, 0.5)):
c0 = _crink_at_fail_threshold(distance, sigma)
assert gaussian(1 / c0, 1.0, distance, sigma) == pytest.approx(
FAIL_THRESHOLD, rel=1e-12)
# and it is the COMPACT-side root: less exposure than c0, not more
assert 1 / c0 > distance
def test_ramp_is_strictly_monotone_where_the_gaussian_has_underflowed():
"""The point of the change. Stock assigns the same double -- 0.0 -- to
every leaf below crink ~= 1/15; the ramp separates them."""
distance, sigma = 5.0 / 6, 1.1 / 3
c0 = _crink_at_fail_threshold(distance, sigma)
crinks = [0.0, 0.001, 0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, c0 * 0.999]
stock = [gaussian(1 / c, 1.0, distance, sigma) if c else 0.0 for c in crinks]
ramp = [FAIL_THRESHOLD * c / c0 for c in crinks]
assert len(set(stock)) < len(set(ramp)), "stock should collapse values the ramp keeps"
assert stock.count(0.0) > 1, "the flat-zero region is what this fixes"
assert all(b > a for a, b in zip(ramp, ramp[1:])), "ramp must be strictly increasing"
assert ramp[0] == 0.0, "a fully buried leaf is still worth nothing"
assert all(q < FAIL_THRESHOLD for q in ramp), "the ramp must never lift a leaf out of failing"
@pytest.mark.skipif(not (EXAMPLES / "harbor-house").is_dir(),
reason="examples absent")
def test_fail_set_is_byte_identical_across_the_corpus():
seen = 0
for d, p in _artefacts():
root = dom_mod.load(str(p))
c_stock, cost = load_config(d)
c_ramp, _ = load_config(d, overrides={"crinkliness_tail": "ramp"})
_, f_stock = Fitness(c_stock, cost).score_with_fails(copy.deepcopy(root))
_, f_ramp = Fitness(c_ramp, cost).score_with_fails(copy.deepcopy(root))
assert f_stock == f_ramp, f"{p} changed its fail set under the ramp"
seen += 1
assert seen >= 4, "expected to have checked several corpus artefacts"
@pytest.mark.skipif(not (EXAMPLES / "harbor-house").is_dir(),
reason="examples absent")
def test_ramp_never_lowers_the_score():
"""Every affected factor rises (0 or ~0 -> a representable fraction of
FAIL_THRESHOLD), and quality is a product with value accumulating
positively, so the scalar can only go up or stay put."""
for d, p in _artefacts():
root = dom_mod.load(str(p))
c_stock, cost = load_config(d)
c_ramp, _ = load_config(d, overrides={"crinkliness_tail": "ramp"})
s_stock, _ = Fitness(c_stock, cost).score_with_fails(copy.deepcopy(root))
s_ramp, _ = Fitness(c_ramp, cost).score_with_fails(copy.deepcopy(root))
assert s_ramp >= s_stock, f"{p} scored lower under the ramp"
def test_ramp_refuses_to_compose_with_the_superseded_modes():
"""§38.1's modes rewrite the same tail; stacking them would give a shape
neither was measured under."""
d = EXAMPLES / "harbor-house"
if not d.is_dir():
pytest.skip("examples absent")
conf, cost = load_config(d, overrides={"crinkliness_tail": "ramp",
"crinkliness_mode": "floor"})
with pytest.raises(ValueError, match="incompatible"):
Fitness(conf, cost)
def test_unknown_tail_is_rejected():
d = EXAMPLES / "harbor-house"
if not d.is_dir():
pytest.skip("examples absent")
conf, cost = load_config(d, overrides={"crinkliness_tail": "linear"})
with pytest.raises(ValueError, match="unknown crinkliness_tail"):
Fitness(conf, cost)
Make the crinkliness factor one-sided: stop billing the daylit wall twice The tail rescale shipped in cd392e7 is a measured NULL as a search intervention -- 12 of 12 pairs byte-identical on harbor and maple, 8000 evals from a plateau, not merely underpowered. Of course it is: the whole failing tail is 0.034% of corpus value. Looking at the rest of the factor, prompted by the owner, found something much larger above the threshold. crink = area_outside/area = (L*h)/A, so 1/crink = A/(L*h) is the room's mean depth from its daylit wall in storey-heights. That is the right variable for a daylight rule, and the fail boundary it implies (1/crink = 1.62, i.e. 4.86 m at h=3) is a sensible one that agrees with 38.3's frontage bound derived independently. What is wrong is hanging a TWO-sided gaussian on it: * The near side penalises a room for having MORE daylit wall than target -- while leaf_cost's siblings edge_cost and outside_edge_cost already charge that same wall at exterior_wall=100 and boundary_wall=133.3 per m2. The wall is billed once in cost and again as lost value. * It never earns its keep as a failure either: the over-exposed branch only reaches FAIL_THRESHOLD above crinkliness 21.5, and the corpus maximum is 3.95. It has never produced a single fail; it only removes value. * 133 of the 318 passing graded leaves in the 500k baseline (42%) sit on that side, mean quality 0.810. crinkliness_shape="daylight" (default OFF, "gaussian" is stock) clips it: a room shallower than the gaussian's peak scores 1.0, because daylight is a sufficiency requirement and surplus is the cost model's business, not this factor's. Clipping at the PEAK rather than at FAIL_THRESHOLD is deliberate -- it keeps the factor continuous and preserves the graded approach to the daylight limit, where clipping at the threshold would put a 10x cliff on the exact boundary the 0.5**n fail multiplier already steps on. Fail set byte-identical on all 21 corpus artefacts for all four shape/tail combinations, so stock stays a valid yardstick for every arm. Area-weighted crinkliness quality 0.480 -> 0.513, leaf quality product 0.2722 -> 0.2831; per-artefact score +0.2%..+19.6%, and unlike the ramp it reaches health-centre and programme-house, where the tail change was 0.000%. Note "daylight" clips the OPPOSITE side from 38.1's superseded compact_ok, which forgives being buried; composing either with those modes is refused. ab_9gj_ramp.py becomes ab_9gj_crinkliness.py and takes named arms, since it now covers both changes; its first arm is the baseline and the yardstick. Refs homemaker-py-9gj. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-09-05 07:27:02 +00:00
# --------------------------------------------------------------------------- #
# crinkliness_shape="daylight" (homemaker-py-9gj, DESIGN.md §39.14)
# --------------------------------------------------------------------------- #
def _q(distance, sigma, crink, **conf):
"""quality_uncrinkliness for a synthetic leaf at a given crinkliness.
A bare `Node` with no type: `is_outside`/`is_covered` must both be False so
the factor is actually evaluated rather than short-circuited to 1.0 for an
uncovered outside leaf.
"""
d = EXAMPLES / "harbor-house"
c, cost = load_config(d, overrides=conf)
fit = Fitness(c, cost)
fit.crinkliness_params = lambda leaf: (distance, sigma)
fit.crinkliness = lambda leaf, G, groups: crink
leaf = dom_mod.Node(type="x1")
assert not dom_mod.is_outside(leaf)
return Fitness.quality_uncrinkliness(fit, leaf, None, None)
@pytest.mark.skipif(not (EXAMPLES / "harbor-house").is_dir(), reason="examples absent")
def test_daylight_shape_stops_penalising_surplus_daylight():
"""`1/crink` is depth-in-storey-heights; below `distance` the room is
shallower than the stock peak, i.e. better lit than asked for. Stock
decays from there; "daylight" does not."""
b, s = 5.0 / 6, 1.1 / 3
for crink in (1 / b, 1.5, 2.0, 4.0, 20.0): # 1/crink <= b
assert _q(b, s, crink, crinkliness_shape="daylight") == 1.0
if crink > 1 / b:
assert _q(b, s, crink) < 1.0, "stock should penalise surplus daylight"
@pytest.mark.skipif(not (EXAMPLES / "harbor-house").is_dir(), reason="examples absent")
def test_daylight_shape_leaves_the_under_lit_side_alone():
"""Only the surplus side is clipped. The graded approach to the daylight
limit is the part that still does useful work, so it must not move."""
b, s = 5.0 / 6, 1.1 / 3
for crink in (0.62, 0.7, 0.9, 1.0, 1.19): # 1/crink > b, passing
assert _q(b, s, crink, crinkliness_shape="daylight") == _q(b, s, crink)
@pytest.mark.skipif(not (EXAMPLES / "harbor-house").is_dir(), reason="examples absent")
def test_daylight_shape_is_continuous_at_the_clip():
"""Clipping at the gaussian's peak rather than at FAIL_THRESHOLD is what
keeps this continuous. Clipping at the threshold would put a 10x cliff on
the exact boundary the 0.5**n fail multiplier already steps on."""
b, s = 5.0 / 6, 1.1 / 3
just_under = _q(b, s, 1 / b - 1e-9, crinkliness_shape="daylight")
assert just_under == pytest.approx(1.0, abs=1e-6)
@pytest.mark.skipif(not (EXAMPLES / "harbor-house").is_dir(), reason="examples absent")
def test_every_combination_keeps_the_fail_set_byte_identical():
"""The invariant that makes stock scoring a valid yardstick for all arms.
The over-exposed branch of the stock gaussian only fires above crinkliness
21.5, and the corpus maximum is 3.95, so clipping that side removes no
failure that any corpus artefact actually incurs.
"""
combos = [{"crinkliness_tail": "ramp"},
{"crinkliness_shape": "daylight"},
{"crinkliness_shape": "daylight", "crinkliness_tail": "ramp"}]
seen = 0
for d, p in _artefacts():
root = dom_mod.load(str(p))
c_stock, cost = load_config(d)
_, f_stock = Fitness(c_stock, cost).score_with_fails(copy.deepcopy(root))
for ov in combos:
c, _ = load_config(d, overrides=ov)
_, f = Fitness(c, cost).score_with_fails(copy.deepcopy(root))
assert f == f_stock, f"{p} changed its fail set under {ov}"
seen += 1
assert seen >= 4
def test_unknown_shape_is_rejected():
d = EXAMPLES / "harbor-house"
if not d.is_dir():
pytest.skip("examples absent")
conf, cost = load_config(d, overrides={"crinkliness_shape": "onesided"})
with pytest.raises(ValueError, match="unknown crinkliness_shape"):
Fitness(conf, cost)
def test_daylight_shape_refuses_to_compose_with_the_superseded_modes():
d = EXAMPLES / "harbor-house"
if not d.is_dir():
pytest.skip("examples absent")
conf, cost = load_config(d, overrides={"crinkliness_shape": "daylight",
"crinkliness_mode": "compact_ok"})
with pytest.raises(ValueError, match="incompatible"):
Fitness(conf, cost)