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
This commit is contained in:
Claude 2026-09-05 07:27:02 +00:00
parent 6d387bbe3b
commit 366a047a60
No known key found for this signature in database
4 changed files with 222 additions and 30 deletions

View file

@ -1,11 +1,24 @@
"""Search A/B for the crinkliness tail rescale (`homemaker-py-9gj`). """Search A/B for the crinkliness reformulation (`homemaker-py-9gj`).
`quality_uncrinkliness` evaluates a gaussian at `x = 1/crink`, so its exponent Two independent changes to `quality_uncrinkliness`, either or both:
grows like `1/crink^2` as exposure falls. Measured over the 500k cold-start
baseline (DESIGN.md §39.12), the FAILING compact tail spans crinkliness crinkliness_tail="ramp" below FAIL_THRESHOLD. The factor evaluates a
0.12..0.59 and quality 1e-300..1e-1 -- every value of which is numerically gaussian at `x = 1/crink`, whose exponent
zero beside a passing leaf's ~1. `crinkliness_tail="ramp"` replaces that tail, grows like 1/crink^2, so the failing tail
and only that tail, with a straight line in crinkliness. spans quality 1e-300..1e-1 -- all of it
numerically zero beside a passing leaf's ~1.
The ramp makes that tail a straight line in
crinkliness. (DESIGN.md §39.13. Measured a
complete null on its own: 12 of 12 pairs
byte-identical.)
crinkliness_shape="daylight" above it. `1/crink` is the room's mean depth
from its daylit wall in storey-heights, and
the stock gaussian is TWO-sided on it, so a
room with more daylight than target is
penalised for it -- while the cost model
already charges that wall via
`exterior_wall`/`boundary_wall`. "daylight"
clips that side to 1.0. (DESIGN.md §39.14.)
**Why stock scoring is valid here** (the §38.9 trap, and the one case the **Why stock scoring is valid here** (the §38.9 trap, and the one case the
`9gj` bead flags as exempt): the ramp is continuous at FAIL_THRESHOLD and `9gj` bead flags as exempt): the ramp is continuous at FAIL_THRESHOLD and
@ -30,18 +43,18 @@ gives N*M paired samples per programme.
Usage:: Usage::
python experiments/ab_9gj_ramp.py --budget 8000 --seeds 2 --starts 3 python experiments/ab_9gj_crinkliness.py --budget 8000 --seeds 2 --starts 3
python experiments/ab_9gj_ramp.py --start init --budget 8000 --seeds 6 python experiments/ab_9gj_crinkliness.py --start init --budget 8000 --seeds 6
Sharding, because one run is minutes and the job list is 4x that. Each shard Sharding, because one run is minutes and the job list is 4x that. Each shard
keeps BOTH arms of a pair together, so the two halves of a comparison never keeps BOTH arms of a pair together, so the two halves of a comparison never
land on differently-loaded processes:: land on differently-loaded processes::
for i in 0 1 2 3; do for i in 0 1 2 3; do
python experiments/ab_9gj_ramp.py --budget 8000 --seeds 2 --starts 3 \ python experiments/ab_9gj_crinkliness.py --budget 8000 --seeds 2 --starts 3 \
--shard $i --nshards 4 & --shard $i --nshards 4 &
done; wait done; wait
python experiments/ab_9gj_ramp.py --report python experiments/ab_9gj_crinkliness.py --report
A POWERED run needs more than the in-session pilot could afford. §39.12 puts A POWERED run needs more than the in-session pilot could afford. §39.12 puts
harbor's minimum detectable difference at n=3 at 13.7 fails; the plateau-escape harbor's minimum detectable difference at n=3 at 13.7 fails; the plateau-escape
@ -50,7 +63,7 @@ deltas here are single-digit, so budget for n >= 8 pairs per programme
move at all -- the pilot's 8000 evals is 1.6% of what produced the plateau:: move at all -- the pilot's 8000 evals is 1.6% of what produced the plateau::
for i in $(seq 0 3); do for i in $(seq 0 3); do
python experiments/ab_9gj_ramp.py --budget 100000 --seeds 3 --starts 3 \ python experiments/ab_9gj_crinkliness.py --budget 100000 --seeds 3 --starts 3 \
--shard $i --nshards 4 & --shard $i --nshards 4 &
done; wait done; wait
""" """
@ -68,21 +81,33 @@ from homemaker_layout import dom as dom_mod
from homemaker_layout import driver, fitness from homemaker_layout import driver, fitness
CORPUS = ["examples/harbor-house", "examples/maple-court"] CORPUS = ["examples/harbor-house", "examples/maple-court"]
ARMS = ["gaussian", "ramp"]
# Each arm names a crinkliness configuration. "stock" must stay first: it is
# the baseline every other arm is paired against, and the yardstick all arms
# are SCORED under.
ARM_CONF = {
"stock": {},
"ramp": {"crinkliness_tail": "ramp"},
"daylight": {"crinkliness_shape": "daylight"},
"daylight+ramp": {"crinkliness_shape": "daylight",
"crinkliness_tail": "ramp"},
}
ARMS = ["stock", "daylight", "daylight+ramp"]
def _with_tail(tail: str): def _with_arm(arm: str):
"""Patch `fitness.load_config` so every evaluator built during the run -- """Patch `fitness.load_config` so every evaluator built during the run --
the driver's, the inner loop's, the seeder's -- sees `crinkliness_tail`. the driver's, the inner loop's, the seeder's -- sees the arm's overrides.
`driver.search` has no parameter for it and `driver._fitness_for` is `driver.search` has no parameter for them and `driver._fitness_for` is
lru_cached, so the cache is cleared around the patch (see ab_ssz_search). lru_cached, so the cache is cleared around the patch (see ab_ssz_search).
""" """
orig = fitness.load_config orig = fitness.load_config
arm_ov = ARM_CONF[arm]
def patched(directory, overrides=None): def patched(directory, overrides=None):
ov = dict(overrides or {}) ov = dict(overrides or {})
ov["crinkliness_tail"] = tail ov.update(arm_ov)
return orig(directory, overrides=ov) return orig(directory, overrides=ov)
return orig, patched return orig, patched
@ -95,7 +120,7 @@ def tiers(fails) -> tuple[int, int]:
def run_arm(progdir: str, start: Path, seed: int, tail: str, budget: int, def run_arm(progdir: str, start: Path, seed: int, tail: str, budget: int,
child_budget: int) -> dict: child_budget: int) -> dict:
orig, patched = _with_tail(tail) orig, patched = _with_arm(tail)
fitness.load_config = patched fitness.load_config = patched
driver._fitness_for.cache_clear() driver._fitness_for.cache_clear()
t0 = time.perf_counter() t0 = time.perf_counter()
@ -143,7 +168,9 @@ def main() -> None:
help="plateau layouts to start from (plateau mode only)") help="plateau layouts to start from (plateau mode only)")
ap.add_argument("--start", choices=("plateau", "init"), default="plateau") ap.add_argument("--start", choices=("plateau", "init"), default="plateau")
ap.add_argument("--corpus", nargs="+", default=CORPUS) ap.add_argument("--corpus", nargs="+", default=CORPUS)
ap.add_argument("--out", default="experiments/results/ab_9gj_ramp.csv") ap.add_argument("--arms", nargs="+", default=ARMS,
choices=sorted(ARM_CONF), help="first arm is the baseline")
ap.add_argument("--out", default="experiments/results/ab_9gj_crinkliness.csv")
ap.add_argument("--shard", type=int, default=0, ap.add_argument("--shard", type=int, default=0,
help="run only jobs i where i %% nshards == shard") help="run only jobs i where i %% nshards == shard")
ap.add_argument("--nshards", type=int, default=1, ap.add_argument("--nshards", type=int, default=1,
@ -173,7 +200,7 @@ def main() -> None:
w = csv.DictWriter(fh, fieldnames=list(rows[0])) w = csv.DictWriter(fh, fieldnames=list(rows[0]))
w.writeheader() w.writeheader()
w.writerows(rows) w.writerows(rows)
_report(rows, args.corpus) _report(rows, args.corpus, args.arms)
print(f"\nmerged {len(rows)} runs from {len(shards)} file(s) into {out}") print(f"\nmerged {len(rows)} runs from {len(shards)} file(s) into {out}")
return return
@ -193,7 +220,7 @@ def main() -> None:
for i, (progdir, start, seed) in enumerate(jobs): for i, (progdir, start, seed) in enumerate(jobs):
if i % args.nshards != args.shard: if i % args.nshards != args.shard:
continue continue
for tail in ARMS: for tail in args.arms:
r = run_arm(progdir, start, seed, tail, args.budget, r = run_arm(progdir, start, seed, tail, args.budget,
args.child_budget) args.child_budget)
rows.append(r) rows.append(r)
@ -205,26 +232,33 @@ def main() -> None:
w.writeheader() w.writeheader()
w.writerows(rows) w.writerows(rows)
if args.nshards == 1: if args.nshards == 1:
_report(rows, args.corpus) _report(rows, args.corpus, args.arms)
print(f"\nwrote {dest}") print(f"\nwrote {dest}")
def _report(rows, corpus) -> None: def _report(rows, corpus, arms=None) -> None:
"""homemaker-py-tco: state what this N could resolve, beside the result.""" """homemaker-py-tco: state what this N could resolve, beside the result."""
from ab_report import format_report, paired_report from ab_report import format_report, paired_report
seen = []
for r in rows:
if r["tail"] not in seen:
seen.append(r["tail"])
arms = [a for a in (arms or seen) if a in seen] or seen
base = arms[0]
for progdir in corpus: for progdir in corpus:
name = Path(progdir).name name = Path(progdir).name
by: dict[tuple, dict] = {} by: dict[tuple, dict] = {}
for r in rows: for r in rows:
if r["programme"] == name: if r["programme"] == name:
by.setdefault((r["start"], r["seed"]), {})[r["tail"]] = r["total"] by.setdefault((r["start"], r["seed"]), {})[r["tail"]] = r["total"]
keys = sorted(k for k, v in by.items() if len(v) == 2) for arm in arms[1:]:
keys = sorted(k for k, v in by.items() if base in v and arm in v)
if len(keys) < 2: if len(keys) < 2:
continue continue
print(f"\n--- {name}: ramp vs gaussian (stock-scored total fails) ---") print(f"\n--- {name}: {arm} vs {base} (stock-scored total fails) ---")
print(format_report(paired_report( print(format_report(paired_report(
[by[k]["gaussian"] for k in keys], [by[k]["ramp"] for k in keys], [by[k][base] for k in keys], [by[k][arm] for k in keys],
"gaussian", "ramp"))) base, arm)))
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -0,0 +1,25 @@
programme,start,seed,tail,hard,soft,total,score,elapsed_s
harbor-house,coldstart-500000-s0.dom,0,gaussian,8,25,33,1.85176240526207e-11,403.6
harbor-house,coldstart-500000-s0.dom,0,ramp,8,25,33,1.85176240526207e-11,413.5
harbor-house,coldstart-500000-s2.dom,0,gaussian,12,30,42,3.0491865573276097e-14,364.4
harbor-house,coldstart-500000-s2.dom,0,ramp,12,30,42,3.0491865573276097e-14,361.9
maple-court,coldstart-500000-s1.dom,0,gaussian,17,56,73,4.626623187133073e-25,445.0
maple-court,coldstart-500000-s1.dom,0,ramp,17,56,73,4.626281900486264e-25,443.8
harbor-house,coldstart-500000-s0.dom,1,gaussian,8,24,32,3.749216704165142e-11,417.1
harbor-house,coldstart-500000-s0.dom,1,ramp,8,24,32,3.749216704165142e-11,425.6
harbor-house,coldstart-500000-s2.dom,1,gaussian,12,30,42,3.0491865573276097e-14,355.6
harbor-house,coldstart-500000-s2.dom,1,ramp,12,30,42,3.0491865573276097e-14,364.3
maple-court,coldstart-500000-s1.dom,1,gaussian,17,56,73,4.629596572120101e-25,442.4
maple-court,coldstart-500000-s1.dom,1,ramp,17,56,73,4.629661078538839e-25,444.8
harbor-house,coldstart-500000-s1.dom,0,gaussian,7,36,43,1.4676873187056738e-15,373.2
harbor-house,coldstart-500000-s1.dom,0,ramp,7,36,43,1.4676873187056738e-15,375.2
maple-court,coldstart-500000-s0.dom,0,gaussian,18,35,53,1.1458353761153645e-17,483.1
maple-court,coldstart-500000-s0.dom,0,ramp,18,35,53,1.1458353761153645e-17,462.6
maple-court,coldstart-500000-s2.dom,0,gaussian,12,43,55,4.278297968488281e-18,481.2
maple-court,coldstart-500000-s2.dom,0,ramp,12,43,55,4.267745195711144e-18,481.8
harbor-house,coldstart-500000-s1.dom,1,gaussian,7,36,43,1.4676873187056738e-15,378.6
harbor-house,coldstart-500000-s1.dom,1,ramp,7,36,43,1.4676873187056738e-15,380.1
maple-court,coldstart-500000-s0.dom,1,gaussian,17,37,54,6.645342661051106e-18,456.4
maple-court,coldstart-500000-s0.dom,1,ramp,17,37,54,6.645342661051106e-18,478.6
maple-court,coldstart-500000-s2.dom,1,gaussian,12,43,55,4.258152742657858e-18,517.1
maple-court,coldstart-500000-s2.dom,1,ramp,12,43,55,4.258152742657858e-18,515.6
1 programme start seed tail hard soft total score elapsed_s
2 harbor-house coldstart-500000-s0.dom 0 gaussian 8 25 33 1.85176240526207e-11 403.6
3 harbor-house coldstart-500000-s0.dom 0 ramp 8 25 33 1.85176240526207e-11 413.5
4 harbor-house coldstart-500000-s2.dom 0 gaussian 12 30 42 3.0491865573276097e-14 364.4
5 harbor-house coldstart-500000-s2.dom 0 ramp 12 30 42 3.0491865573276097e-14 361.9
6 maple-court coldstart-500000-s1.dom 0 gaussian 17 56 73 4.626623187133073e-25 445.0
7 maple-court coldstart-500000-s1.dom 0 ramp 17 56 73 4.626281900486264e-25 443.8
8 harbor-house coldstart-500000-s0.dom 1 gaussian 8 24 32 3.749216704165142e-11 417.1
9 harbor-house coldstart-500000-s0.dom 1 ramp 8 24 32 3.749216704165142e-11 425.6
10 harbor-house coldstart-500000-s2.dom 1 gaussian 12 30 42 3.0491865573276097e-14 355.6
11 harbor-house coldstart-500000-s2.dom 1 ramp 12 30 42 3.0491865573276097e-14 364.3
12 maple-court coldstart-500000-s1.dom 1 gaussian 17 56 73 4.629596572120101e-25 442.4
13 maple-court coldstart-500000-s1.dom 1 ramp 17 56 73 4.629661078538839e-25 444.8
14 harbor-house coldstart-500000-s1.dom 0 gaussian 7 36 43 1.4676873187056738e-15 373.2
15 harbor-house coldstart-500000-s1.dom 0 ramp 7 36 43 1.4676873187056738e-15 375.2
16 maple-court coldstart-500000-s0.dom 0 gaussian 18 35 53 1.1458353761153645e-17 483.1
17 maple-court coldstart-500000-s0.dom 0 ramp 18 35 53 1.1458353761153645e-17 462.6
18 maple-court coldstart-500000-s2.dom 0 gaussian 12 43 55 4.278297968488281e-18 481.2
19 maple-court coldstart-500000-s2.dom 0 ramp 12 43 55 4.267745195711144e-18 481.8
20 harbor-house coldstart-500000-s1.dom 1 gaussian 7 36 43 1.4676873187056738e-15 378.6
21 harbor-house coldstart-500000-s1.dom 1 ramp 7 36 43 1.4676873187056738e-15 380.1
22 maple-court coldstart-500000-s0.dom 1 gaussian 17 37 54 6.645342661051106e-18 456.4
23 maple-court coldstart-500000-s0.dom 1 ramp 17 37 54 6.645342661051106e-18 478.6
24 maple-court coldstart-500000-s2.dom 1 gaussian 12 43 55 4.258152742657858e-18 517.1
25 maple-court coldstart-500000-s2.dom 1 ramp 12 43 55 4.258152742657858e-18 515.6

View file

@ -477,6 +477,23 @@ class Fitness:
if self._crinkliness_tail not in ("gaussian", "ramp"): if self._crinkliness_tail not in ("gaussian", "ramp"):
raise ValueError( raise ValueError(
f"unknown crinkliness_tail: {self._crinkliness_tail!r}") f"unknown crinkliness_tail: {self._crinkliness_tail!r}")
# homemaker-py-9gj (DESIGN.md §39.14): what the factor rewards ABOVE the
# fail threshold, orthogonal to `crinkliness_tail` below it.
# "gaussian" (default) is stock: two-sided, so a room with MORE daylit
# wall than target is penalised for it. "daylight" clips that side to
# 1.0 -- daylight is a sufficiency requirement, and the envelope a
# well-lit room costs is already charged by `exterior_wall` and
# `boundary_wall` in the cost model, so penalising it again in value
# bills the same wall twice.
self._crinkliness_shape = str(self.conf("crinkliness_shape") or "gaussian")
if self._crinkliness_shape not in ("gaussian", "daylight"):
raise ValueError(
f"unknown crinkliness_shape: {self._crinkliness_shape!r}")
if self._crinkliness_shape == "daylight" and self._crinkliness_mode != "urb":
raise ValueError(
"crinkliness_shape='daylight' is incompatible with "
f"crinkliness_mode={self._crinkliness_mode!r} (§38.1's modes are "
"superseded; use one or the other, not both)")
if self._crinkliness_tail == "ramp" and self._crinkliness_mode != "urb": if self._crinkliness_tail == "ramp" and self._crinkliness_mode != "urb":
# Both rewrite the same tail; composing them would give a shape # Both rewrite the same tail; composing them would give a shape
# neither was measured under. # neither was measured under.
@ -1266,6 +1283,13 @@ class Fitness:
# whatever it holds. So the factor is CLIPPED on the compact side, # whatever it holds. So the factor is CLIPPED on the compact side,
# never switched off, and the over-exposed side keeps the global # never switched off, and the over-exposed side keeps the global
# bound. # bound.
#
# ...unless `crinkliness_shape="daylight"`, under which the
# over-exposed side is not this factor's business at all (the cost
# model charges that wall). A space with no daylight requirement
# then has nothing left to be judged on. (§39.14)
if self._crinkliness_shape == "daylight":
return 1.0
if not crink: if not crink:
return 1.0 return 1.0
distance, sigma = self.conf("uncrinkliness") distance, sigma = self.conf("uncrinkliness")
@ -1290,6 +1314,20 @@ class Fitness:
return 1.0 return 1.0
return self._crinkliness_floor if mode == "floor" else 0.0 return self._crinkliness_floor if mode == "floor" else 0.0
if self._crinkliness_shape == "daylight" and 1 / crink <= distance:
# homemaker-py-9gj (DESIGN.md §39.14). `1/crink` is the room's mean
# depth from its daylit wall in storey-heights, so `1/crink <=
# distance` means comfortably lit -- shallower than the point the
# stock gaussian peaks at. Stock decays from there as if surplus
# daylight were a defect; it is not one this factor should price,
# because the extra exterior wall is already billed in `cost`.
# Clipping here (rather than at the fail threshold) keeps the
# factor CONTINUOUS: the graded approach to the daylight limit
# survives, and no 10x cliff is introduced at the very boundary
# the fail multiplier already steps on. Note this is the OPPOSITE
# side from §38.1's `compact_ok`, which forgives being buried.
return 1.0
q = gaussian(1 / crink, 1.0, distance, sigma) q = gaussian(1 / crink, 1.0, distance, sigma)
if one_sided and 1 / crink > distance: if one_sided and 1 / crink > distance:
return 1.0 return 1.0

View file

@ -111,3 +111,98 @@ def test_unknown_tail_is_rejected():
conf, cost = load_config(d, overrides={"crinkliness_tail": "linear"}) conf, cost = load_config(d, overrides={"crinkliness_tail": "linear"})
with pytest.raises(ValueError, match="unknown crinkliness_tail"): with pytest.raises(ValueError, match="unknown crinkliness_tail"):
Fitness(conf, cost) Fitness(conf, cost)
# --------------------------------------------------------------------------- #
# 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)