diff --git a/DESIGN.md b/DESIGN.md index 1a54c3e..f341c0a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -8126,3 +8126,47 @@ where the fraction penalty lifted and fall where a new hard fail landed: | maple-court | 51→**52**, 65, 52 | −50% (new fail), +1%, +0.4% | | health-centre | 3/9/5 unchanged | +29% … +156% | | programme-house | 1→**2** all seeds | −43%, +24%, −50% | + +### 39.26 Two dead paths in the objective (`homemaker-py-dpt`) + +§39.24's sweep listed two entries as DEAD rather than suspect — inert code that +reads as live. Both are removed here. Neither changes a score or a failure on +any corpus artefact; that is what "dead" meant, and it 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 by `if conf_po and isinstance(...)` +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. + +Note what is **not** removed: `_public_access`, `_public_access_outside` and +`_public_access_pins`, and the `has_public_access_inside` / `..._outside` +tracking flags. Those are live — they 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, a factor that has been pinned since the URB_NO_OCCLUSION +descope (§6) and can never be anything else. It was never in `_GRADED_FACTORS`, +so it contributed nothing to the graded signal; §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 the occlusion subsystem is ever rebuilt (`homemaker-py-2g5`), it reintroduces +a real daylight factor; nothing here forecloses that, and a real one would need +`factor_is_asked` to say `True` for it anyway. + +**Why this is worth doing at all**, given neither changes a 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 an objective where "this term does +nothing" is informative rather than routine. + +Still open on `dpt`, and all three need 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. diff --git a/src/homemaker_layout/fitness.py b/src/homemaker_layout/fitness.py index 09fa539..f41fe93 100644 --- a/src/homemaker_layout/fitness.py +++ b/src/homemaker_layout/fitness.py @@ -1,7 +1,7 @@ """Native port of Urb's programme-driven fitness: leaf quality terms + cost model. Scope (homemaker-py-gnw): per-leaf quality factors (perpendicular, proportion, -size, width, crinkliness, daylight, access), the programme-driven parameter +size, width, crinkliness, access), the programme-driven parameter lookup chain (``get_space_params``), value rates, and the cost denominator (per-leaf area costs, interior/exterior wall edge costs, boundary costs). Storey/building checks, staircases, failure stacking and final assembly are @@ -1544,9 +1544,6 @@ class Fitness: factors["crinkliness"] = f quality *= f - # Daylight pinned to 1 — URB_NO_OCCLUSION semantics (DESIGN.md §6). - factors["daylight"] = 1.0 - if len(self.access(leaf, G)) > 0: f = 1.0 elif not dom_mod.level_of(leaf) and dom_mod.is_outside(leaf): @@ -1570,8 +1567,6 @@ class Fitness: `tests/test_fitness_aggregate.py` asserts the invariant this duplication rests on: whenever this returns False, the factor really is 1.0. """ - if name == "daylight": - return False # pinned to 1.0, URB_NO_OCCLUSION §6 if name == "size": return _generic_class(leaf) not in ("o", "s") if name == "crinkliness": @@ -1923,38 +1918,6 @@ class Fitness: return True return False - def _public_length(self, leaf: Node, root: Node) -> float: - """Non-private external boundary metres; mirrors ``Urb::Dom::Public_Length``.""" - if dom_mod.level_of(leaf) != 0: - return 0.0 - total = 0.0 - for edge in range(4): - bid = geometry.boundary_id(leaf, edge) - if bid not in frozenset("abcd"): - continue - if self._perimeter_type(root, bid).lower() == "private": - continue - total += geometry.edge_length(leaf, edge) - return total - - def _private_length(self, leaf: Node, root: Node) -> float: - """Private external boundary metres; mirrors ``Urb::Dom::Private_Length``.""" - if dom_mod.level_of(leaf) != 0: - return 0.0 - total = 0.0 - for edge in range(4): - bid = geometry.boundary_id(leaf, edge) - if bid not in frozenset("abcd"): - continue - if self._perimeter_type(root, bid).lower() != "private": - continue - total += geometry.edge_length(leaf, edge) - return total - - # ----------------------------------------------------------------------- # - # Extended process_storey (adds circ, stair, tracking) - # ----------------------------------------------------------------------- # - def process_storey( self, level_root: Node, @@ -2040,15 +2003,6 @@ class Fitness: and self._public_access(leaf, root) is not None): tracking["has_public_access_inside"] = True - pub = self._public_length(leaf, root) - tracking["public_length_all"] = tracking.get("public_length_all", 0.0) + pub - if dom_mod.is_outside(leaf): - tracking["public_length_outside"] = tracking.get("public_length_outside", 0.0) + pub - priv = self._private_length(leaf, root) - tracking["private_length_all"] = tracking.get("private_length_all", 0.0) + priv - if dom_mod.is_outside(leaf): - tracking["private_length_outside"] = tracking.get("private_length_outside", 0.0) + priv - for a, b in G.edges(): cost += self.edge_cost(G, a, b, fail) for leaf in level_root.leaves(): @@ -2096,19 +2050,6 @@ class Fitness: f2 = gaussian(actual_internal, 1.0, min_required, min_required * 0.15) factor *= f2 - # Public/private ratios (optional config) - pub_all = tracking.get("public_length_all", 0.0) - pub_ratio = tracking.get("public_length_outside", 0.0) / pub_all if pub_all else 0.0 - conf_po = self.conf("ratio_public_outside") - if conf_po and isinstance(conf_po, list): - factor *= gaussian(pub_ratio, 1.0, conf_po[0], conf_po[1]) - - priv_all = tracking.get("private_length_all", 0.0) - priv_ratio = tracking.get("private_length_outside", 0.0) / priv_all if priv_all else 0.0 - conf_pr = self.conf("ratio_private_outside") - if conf_pr and isinstance(conf_pr, list): - factor *= gaussian(priv_ratio, 1.0, conf_pr[0], conf_pr[1]) - # Staircase volume (multi-level only) lvls = dom_mod.levels(root) if len(lvls) > 1: @@ -2188,10 +2129,6 @@ class Fitness: tracking: dict = { "has_public_access_outside": False, "has_public_access_inside": False, - "public_length_all": 0.0, - "public_length_outside": 0.0, - "private_length_all": 0.0, - "private_length_outside": 0.0, "stair_fit": [], "_failures": failures, } diff --git a/tests/test_dom_corpus.py b/tests/test_dom_corpus.py index 44b79c4..6f2199a 100644 --- a/tests/test_dom_corpus.py +++ b/tests/test_dom_corpus.py @@ -76,10 +76,6 @@ def _native_evaluate(src: Path): tracking: dict = { "has_public_access_outside": False, "has_public_access_inside": False, - "public_length_all": 0.0, - "public_length_outside": 0.0, - "private_length_all": 0.0, - "private_length_outside": 0.0, "stair_fit": [], "_failures": failures, } diff --git a/tests/test_fitness.py b/tests/test_fitness.py index 6148046..cde78cf 100644 --- a/tests/test_fitness.py +++ b/tests/test_fitness.py @@ -335,8 +335,10 @@ def test_leaf_grade_sums_over_failing_factors(): def test_leaf_grade_ignores_non_graded_keys(): - # daylight is pinned and never a graded factor even if below threshold. - assert _leaf_grade({"daylight": 0.0}) == 0.0 + # Only _GRADED_FACTORS contribute; anything else is ignored however low. + # (This used to name "daylight", a factor pinned to 1.0 since the + # URB_NO_OCCLUSION descope and removed entirely in §39.26.) + assert _leaf_grade({"not_a_factor": 0.0}) == 0.0 # --------------------------------------------------------------------------- # diff --git a/tests/test_fitness_aggregate.py b/tests/test_fitness_aggregate.py index adb4657..c24569c 100644 --- a/tests/test_fitness_aggregate.py +++ b/tests/test_fitness_aggregate.py @@ -87,7 +87,7 @@ def test_geometric_mean_is_the_product_when_every_factor_is_asked(): 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, "daylight": 1.0} + "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) @@ -111,7 +111,6 @@ def test_it_does_not_underflow_where_the_product_would(): tiny = 1e-60 factors = {k: tiny for k in ("perpendicular", "proportion", "size", "width", "crinkliness", "access")} - factors["daylight"] = 1.0 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)