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:
parent
6d387bbe3b
commit
366a047a60
4 changed files with 222 additions and 30 deletions
|
|
@ -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
|
||||
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
|
||||
0.12..0.59 and quality 1e-300..1e-1 -- every value of which is numerically
|
||||
zero beside a passing leaf's ~1. `crinkliness_tail="ramp"` replaces that tail,
|
||||
and only that tail, with a straight line in crinkliness.
|
||||
Two independent changes to `quality_uncrinkliness`, either or both:
|
||||
|
||||
crinkliness_tail="ramp" below FAIL_THRESHOLD. The factor evaluates a
|
||||
gaussian at `x = 1/crink`, whose exponent
|
||||
grows like 1/crink^2, so the failing tail
|
||||
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
|
||||
`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::
|
||||
|
||||
python experiments/ab_9gj_ramp.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 --budget 8000 --seeds 2 --starts 3
|
||||
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
|
||||
keeps BOTH arms of a pair together, so the two halves of a comparison never
|
||||
land on differently-loaded processes::
|
||||
|
||||
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 &
|
||||
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
|
||||
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::
|
||||
|
||||
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 &
|
||||
done; wait
|
||||
"""
|
||||
|
|
@ -68,21 +81,33 @@ from homemaker_layout import dom as dom_mod
|
|||
from homemaker_layout import driver, fitness
|
||||
|
||||
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 --
|
||||
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).
|
||||
"""
|
||||
orig = fitness.load_config
|
||||
arm_ov = ARM_CONF[arm]
|
||||
|
||||
def patched(directory, overrides=None):
|
||||
ov = dict(overrides or {})
|
||||
ov["crinkliness_tail"] = tail
|
||||
ov.update(arm_ov)
|
||||
return orig(directory, overrides=ov)
|
||||
|
||||
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,
|
||||
child_budget: int) -> dict:
|
||||
orig, patched = _with_tail(tail)
|
||||
orig, patched = _with_arm(tail)
|
||||
fitness.load_config = patched
|
||||
driver._fitness_for.cache_clear()
|
||||
t0 = time.perf_counter()
|
||||
|
|
@ -143,7 +168,9 @@ def main() -> None:
|
|||
help="plateau layouts to start from (plateau mode only)")
|
||||
ap.add_argument("--start", choices=("plateau", "init"), default="plateau")
|
||||
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,
|
||||
help="run only jobs i where i %% nshards == shard")
|
||||
ap.add_argument("--nshards", type=int, default=1,
|
||||
|
|
@ -173,7 +200,7 @@ def main() -> None:
|
|||
w = csv.DictWriter(fh, fieldnames=list(rows[0]))
|
||||
w.writeheader()
|
||||
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}")
|
||||
return
|
||||
|
||||
|
|
@ -193,7 +220,7 @@ def main() -> None:
|
|||
for i, (progdir, start, seed) in enumerate(jobs):
|
||||
if i % args.nshards != args.shard:
|
||||
continue
|
||||
for tail in ARMS:
|
||||
for tail in args.arms:
|
||||
r = run_arm(progdir, start, seed, tail, args.budget,
|
||||
args.child_budget)
|
||||
rows.append(r)
|
||||
|
|
@ -205,26 +232,33 @@ def main() -> None:
|
|||
w.writeheader()
|
||||
w.writerows(rows)
|
||||
if args.nshards == 1:
|
||||
_report(rows, args.corpus)
|
||||
_report(rows, args.corpus, args.arms)
|
||||
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."""
|
||||
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:
|
||||
name = Path(progdir).name
|
||||
by: dict[tuple, dict] = {}
|
||||
for r in rows:
|
||||
if r["programme"] == name:
|
||||
by.setdefault((r["start"], r["seed"]), {})[r["tail"]] = r["total"]
|
||||
keys = sorted(k for k, v in by.items() if len(v) == 2)
|
||||
if len(keys) < 2:
|
||||
continue
|
||||
print(f"\n--- {name}: ramp vs gaussian (stock-scored total fails) ---")
|
||||
print(format_report(paired_report(
|
||||
[by[k]["gaussian"] for k in keys], [by[k]["ramp"] for k in keys],
|
||||
"gaussian", "ramp")))
|
||||
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:
|
||||
continue
|
||||
print(f"\n--- {name}: {arm} vs {base} (stock-scored total fails) ---")
|
||||
print(format_report(paired_report(
|
||||
[by[k][base] for k in keys], [by[k][arm] for k in keys],
|
||||
base, arm)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
25
experiments/results/ab_9gj_ramp.csv
Normal file
25
experiments/results/ab_9gj_ramp.csv
Normal 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
|
||||
|
|
|
@ -477,6 +477,23 @@ class Fitness:
|
|||
if self._crinkliness_tail not in ("gaussian", "ramp"):
|
||||
raise ValueError(
|
||||
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":
|
||||
# Both rewrite the same tail; composing them would give a shape
|
||||
# neither was measured under.
|
||||
|
|
@ -1266,6 +1283,13 @@ class Fitness:
|
|||
# whatever it holds. So the factor is CLIPPED on the compact side,
|
||||
# never switched off, and the over-exposed side keeps the global
|
||||
# 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:
|
||||
return 1.0
|
||||
distance, sigma = self.conf("uncrinkliness")
|
||||
|
|
@ -1290,6 +1314,20 @@ class Fitness:
|
|||
return 1.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)
|
||||
if one_sided and 1 / crink > distance:
|
||||
return 1.0
|
||||
|
|
|
|||
|
|
@ -111,3 +111,98 @@ def test_unknown_tail_is_rejected():
|
|||
conf, cost = load_config(d, overrides={"crinkliness_tail": "linear"})
|
||||
with pytest.raises(ValueError, match="unknown crinkliness_tail"):
|
||||
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)
|
||||
Loading…
Add table
Reference in a new issue