homemaker-layout/tests/test_circulation_proportion.py

97 lines
3.8 KiB
Python
Raw Normal View History

A corridor may be corridor-shaped: no aspect cap on circulation Owner's ruling: "there should be no cap on the proportion of a corridor, especially for big buildings, the crinkliness rule is there to prevent these becoming unpleasant spaces." proportion_circulation was [1.5, 0.5], failing a corridor above aspect 2.57 -- at the 1.97 m minimum width the width factor allows, a corridor 5.1 m long; at the 2.4 m target width, 6.2 m. The corpus shows the consequence: median circulation leaf 14.3 m2 at aspect 1.67, a stubby room rather than a corridor. The justification checks out arithmetically. The unpleasant space the cap was standing in for is a long BURIED corridor -- and crinkliness already sends that to zero, since a buried leaf has crink == 0. A long corridor along a facade scores 0.90. Aspect cannot tell those two apart; crinkliness can, so the cap was duplicating a rule that already exists and does the job better. Shipped: proportion_circulation = None (no aspect requirement). quality_proportion returns 1.0 for circulation, and shapecurve.leaf_constraints yields rmax = inf so the DP agrees with the fitness instead of pruning topologies the objective would accept. A habitable room's aspect target is untouched. The narrow side still holds -- width_circulation keeps a corridor >= 1.97 m, and "edge too long" still caps a single wall at 8 m. Unlike 39.14/39.18/39.19 this DOES change the fail set, which is the point. Across the twelve baseline artefacts it removes exactly 7 corridor proportion fails and adds none: harbor 33/43/42 -> 33/42/40, maple 54/73/55 -> 54/71/54, health-centre 4/9/5 -> 3/9/5, programme-house unchanged. Also recorded in 39.22, and a retraction: hxi was titled "search is rewarded for deleting the circulation spine", which 39.8 had already measured and refuted -- 0 of the 7 connectivity-breaking deletions sampled were rewarded. My own earlier comment on the bead restated that retracted claim; corrected, and the bead retitled. What binds next: removing the cap roughly doubles a corridor leaf's reach, from proportion at ~6.2 m to size_circulation at 12.5 m (2.4 m wide hits the 30 m2 fail edge there). size_circulation's target area is ZERO, the other half of the double-charge -- circulation priced as overhead once in value_circulation = 50 and again in a size factor whose optimum is non-existence. Not changed: it is a distinct parameter with its own rationale, unruled, and 39.16 is a standing reminder about inherited constants. 415 passed. Refs homemaker-py-hxi. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
2026-09-06 12:56:39 +00:00
"""Corridors have no aspect requirement (homemaker-py-hxi, DESIGN.md §39.22).
Owner's ruling: "there should be no cap on the proportion of a corridor,
especially for big buildings, the crinkliness rule is there to prevent these
becoming unpleasant spaces."
The tests below pin both halves of that the cap is gone, AND crinkliness
still does the job the cap was wrongly doing, so removing it did not leave
long buried corridors unpunished.
"""
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 (
CONF_DEFAULTS, FAIL_THRESHOLD, Fitness, gaussian, load_config,
)
EXAMPLES = Path(__file__).resolve().parent.parent / "examples"
pytestmark = pytest.mark.skipif(not (EXAMPLES / "harbor-house").is_dir(),
reason="examples absent")
def _fit(**ov):
conf, cost = load_config(EXAMPLES / "harbor-house", overrides=ov)
return Fitness(conf, cost)
def test_there_is_no_corridor_aspect_requirement():
assert CONF_DEFAULTS["proportion_circulation"] is None
fit = _fit()
assert dom_mod.is_circulation(dom_mod.Node(type="C"))
# any aspect at all scores 1.0 -- checked through the real code path
for aspect in (1.0, 3.0, 12.0, 40.0):
fit_leaf = dom_mod.Node(
type="C", node=[[0.0, 0.0], [aspect, 0.0], [aspect, 1.0], [0.0, 1.0]])
assert fit.quality_proportion(fit_leaf) == 1.0, aspect
def test_a_room_still_has_one():
"""The ruling is about corridors. A habitable room's aspect target stands."""
fit = _fit()
room = dom_mod.Node(type="r", node=[[0.0, 0.0], [12.0, 0.0], [12.0, 1.0], [0.0, 1.0]])
assert fit.quality_proportion(room) < FAIL_THRESHOLD
def test_crinkliness_is_what_punishes_an_unpleasant_corridor():
"""The ruling's own justification, as arithmetic.
A long BURIED corridor is exactly the unpleasant space the aspect cap was
being used to prevent; crinkliness sends it to zero. A long corridor along
a facade is a perfectly good corridor, and crinkliness likes it.
"""
target, sigma = CONF_DEFAULTS["uncrinkliness_circulation"]
h, width, length = 3.0, 2.4, 30.0
area = width * length
buried = 0.0 # no illuminated wall at all
assert buried == 0.0
lit_one_side = (length * h) / area # the long wall is a facade
q = gaussian(1.0 / lit_one_side, 1.0, target, sigma)
assert q > 0.85, "a daylit corridor along a facade should score well"
def test_removing_the_cap_only_ever_removes_proportion_fails():
"""It cannot introduce a failure, and it touches nothing but corridors."""
old = {"proportion_circulation": [1.5, 0.5]}
seen = 0
for name in ("harbor-house", "maple-court", "health-centre", "programme-house"):
d = EXAMPLES / name
for p in sorted(d.glob("coldstart-500000-s*.dom")):
root = dom_mod.load(str(p))
c_old, cost = load_config(d, overrides=old)
c_new, _ = load_config(d)
_, f_old = Fitness(c_old, cost).score_with_fails(copy.deepcopy(root))
_, f_new = Fitness(c_new, cost).score_with_fails(copy.deepcopy(root))
assert not (set(f_new) - set(f_old)), "must not add a failure"
assert all(f.endswith(" proportion") for f in set(f_old) - set(f_new))
seen += 1
assert seen >= 4
def test_the_shape_curve_dp_accepts_an_unbounded_aspect():
"""`leaf_constraints` feeds rmax into the DP; None must become inf, not crash
and not silently fall back to a finite cap."""
from homemaker_layout import shapecurve
fit = _fit()
leaf = dom_mod.Node(type="C", node=[[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]])
assert math.isinf(shapecurve.leaf_constraints(fit, leaf).rmax)