§38.2 refinement: connectivity is under-priced ~3x, not just a crinkliness bug
Follow-up measurement corrects the first draft of §38 in two ways. 1. Harbor-house's floor is 15 fails (evolved-3M-nols-3, 1.7M evals), not the 30-40 I quoted from §13.11's 20k-budget runs. Frontage deficit predicts the COST of solving, not impossibility: ~150x budget gap between a frontage-short and a frontage-surplus programme. Table corrected. 2. Zero-exposure is only half the mechanism, and not the dominant half. Splitting the deletion test by lit vs buried shows a WELL-DAYLIT corridor (q_crink=0.736) is still worth x4.06 to delete. Cause: value_circulation=50 vs value_inside=300, so merging corridor into room is a flat x6 gain, while 'level N not connected' costs only x0.5. Break-even needs 0.5^k < 50/300, i.e. k > 2.58 -- severing must cost at least 3 fails and costs 1. Net x3.0 predicted, x4.06 measured. The objective is net-positive on severing the spine even when the circulation is perfectly lit, which explains why both 'level N not connected' fails survive in the best layout after 1.7M evals. Adds fitness.quality_uncrinkliness crinkliness_mode (EXPERIMENTAL, default "urb" = stock hard 0.0, byte-identical: 336 passed vs 331 before, same 7 pre-existing fixture failures). A/B harness ab_crinkliness_mode_ssz.py shows none of the three modes removes the incentive, and the lit column is 3/8 under every mode including stock -- clean isolation of the two mechanisms. Filed homemaker-py-2v1 (P0) for the pricing fix; ssz/hxi now depend on it. Acceptance test recorded up front: harbor must reach 15 fails in materially fewer than 1.7M evals AND without either not-connected fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
This commit is contained in:
parent
753356e47e
commit
50b5cfd476
4 changed files with 359 additions and 18 deletions
119
DESIGN.md
119
DESIGN.md
|
|
@ -4835,7 +4835,7 @@ future effort on that plateau (`homemaker-py-2g7.7`'s LLM repair operator, or
|
||||||
a level-connectivity-targeted operator) is better aimed than a graph-dual
|
a level-connectivity-targeted operator) is better aimed than a graph-dual
|
||||||
construction pass would have been.
|
construction pass would have been.
|
||||||
|
|
||||||
## 38. The plateau is an objective-gradient problem, not a search problem (`homemaker-py-ssz`/`hxi`/`tdp`/`gvb`/`1i8`) — measured 2026-08-25
|
## 38. The plateau is an objective-gradient problem, not a search problem (`homemaker-py-2v1`/`ssz`/`hxi`/`tdp`/`gvb`/`1i8`) — measured 2026-08-25
|
||||||
|
|
||||||
Independent review of why the search "finds solutions that are clearly not the
|
Independent review of why the search "finds solutions that are clearly not the
|
||||||
best and gets stuck in local minima", prompted by the §37 scoreboard: *every*
|
best and gets stuck in local minima", prompted by the §37 scoreboard: *every*
|
||||||
|
|
@ -4898,8 +4898,41 @@ Measured on a constructed harbor-house seed (`value` report):
|
||||||
circulation spine.** Observed live: in the 20 000-eval harbor-house run above,
|
circulation spine.** Observed live: in the 20 000-eval harbor-house run above,
|
||||||
`undivide`/`core_undivide` appear 16 times in the improvement log.
|
`undivide`/`core_undivide` appear 16 times in the improvement log.
|
||||||
|
|
||||||
This retro-explains three prior results as one mechanism, and suggests two of
|
**Refinement (measured 2026-08-25, after the first draft of this section):
|
||||||
them were measuring a broken gradient rather than a bad idea:
|
zero-exposure is only half of it, and not the half that matters most.**
|
||||||
|
Repeating the deletion test separately for *lit* and *buried* circulation:
|
||||||
|
|
||||||
|
| leaf | exposed wall | `q_crink` | deleting it |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `0/llll` `O` | buried | 0 | **×85.6 rewarded** |
|
||||||
|
| `0/lllr` `C` | buried | 0 | **×61.6 rewarded** |
|
||||||
|
| `1/rlrr` `C` | buried | 0 | **×6.9 rewarded** |
|
||||||
|
| `0/rlll` `C` | 27.4 m² | 0.105 | ×0.01 kept |
|
||||||
|
| `0/lrrl` `O` | 21.2 m² | 0.0004 | ×0.61 kept |
|
||||||
|
| `0/rrll` `C` | 40.7 m² | **0.736** | **×4.1 rewarded** |
|
||||||
|
|
||||||
|
The last row is the important one: a **well-lit** circulation leaf, scoring
|
||||||
|
0.736 on the very factor §38.1 is about, is *still* worth ×4 to delete. So
|
||||||
|
there is a second, independent mechanism, and it is the structural one:
|
||||||
|
|
||||||
|
**Circulation is priced at `value_circulation = 50` against
|
||||||
|
`value_inside = 300`** — one sixth the value per m² of the habitable space it
|
||||||
|
could become. Deleting a circulation leaf merges it into its sibling, which
|
||||||
|
converts corridor into room: a flat **×6 value gain**. The only thing pushing
|
||||||
|
back is the `level N not connected` fail, worth **×0.5**. Break-even needs
|
||||||
|
`0.5^k < 50/300`, i.e. **k > 2.58 — severing must cost at least 3 fails to be
|
||||||
|
net-negative. It costs 1.** Net incentive to sever: `6 × 0.5 = ×3.0` in
|
||||||
|
favour, against a measured ×4.06. **The connectivity fail is under-priced by
|
||||||
|
roughly 3×, so the objective is net-positive on destroying the circulation
|
||||||
|
spine even when the circulation is perfectly daylit.**
|
||||||
|
|
||||||
|
That is the cleanest available explanation of why `level 0 not connected` and
|
||||||
|
`level 1 not connected` are still present in the best layout found after
|
||||||
|
1.7 M evals: the search is not failing to fix them, it is being paid ×3–4 to
|
||||||
|
create them.
|
||||||
|
|
||||||
|
Together these retro-explain three prior results as one mechanism, and suggest
|
||||||
|
two of them were measuring a broken gradient rather than a bad idea:
|
||||||
|
|
||||||
- **§18 graded circulation-connectivity — NEGATIVE.** A secondary comparator
|
- **§18 graded circulation-connectivity — NEGATIVE.** A secondary comparator
|
||||||
key cannot beat a ×60 primary-scalar gradient pulling the other way.
|
key cannot beat a ×60 primary-scalar gradient pulling the other way.
|
||||||
|
|
@ -4917,16 +4950,33 @@ so every interior leaf needs exposed wall `L ≥ A/(1.6202·h)` — per storey,
|
||||||
`fortified` perimeter edges, and harbor-house/maple-court mark **half their
|
`fortified` perimeter edges, and harbor-house/maple-court mark **half their
|
||||||
plot perimeter `private`**:
|
plot perimeter `private`**:
|
||||||
|
|
||||||
| programme | daylit frontage | needed per built storey | verdict | observed floor |
|
| programme | daylit frontage | needed per built storey | verdict | floor @20k evals | best known |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| harbor-house | 54 m | 148 m | **2.7× short** | plateaus 30–40 fails |
|
| harbor-house | 54 m | 148 m | **2.7× short** | 30–40 fails (§13.11) | 15 (`evolved-3M-nols-3`, **1.7 M evals / 2.4 days**) |
|
||||||
| maple-court | 56 m | 162 m | **2.9× short** | plateaus 74–84 fails |
|
| maple-court | 56 m | 162 m | **2.9× short** | 74–84 fails (§13.11) | — |
|
||||||
| health-centre | 43 m | 41 m | feasible | §32 clean null |
|
| health-centre | 43 m | 41 m | feasible | §32 clean null | — |
|
||||||
| programme-house | 24 m | 12 m | 2× surplus | **1 fail** (12k evals) |
|
| programme-house | 24 m | 12 m | 2× surplus | — | **1 fail @ 12k evals** |
|
||||||
|
|
||||||
**The corpus fail-count plateau is predicted by frontage deficit alone.** The
|
**Frontage deficit predicts the COST of solving, not impossibility.** An
|
||||||
two programmes that are frontage-short are exactly the two that plateau; the
|
earlier draft of this section claimed the deficit predicts the plateau
|
||||||
two with surplus are the two that effectively solve. Causal check
|
outright, quoting §13.11's 20k-budget figure as harbor's floor; that was
|
||||||
|
wrong. Harbor-house *does* reach 15 fails — it just needs 1.7 M evals and 2.4
|
||||||
|
days to get there, against programme-house's 1 fail in 12 k. That ~150×
|
||||||
|
budget gap between a frontage-short and a frontage-surplus programme is the
|
||||||
|
real signature, and it is what §38.1/§38.2 predict: the deficit forces the
|
||||||
|
search to find a specific courtyard topology, and the objective punishes every
|
||||||
|
intermediate step toward one.
|
||||||
|
|
||||||
|
**The best-known harbor layout corroborates the mechanism directly.** Its
|
||||||
|
residual (§37.8) is 8 geometry fails plus 4 structural — and two of those four
|
||||||
|
are `level 0 not connected` and `level 1 not connected`. After 1.7 M evals,
|
||||||
|
the best layout ever found still has a **severed circulation spine on both
|
||||||
|
storeys**. That is not a search failure; it is §38.2 working as designed: the
|
||||||
|
objective pays ×60 to delete buried circulation, so connectivity is the one
|
||||||
|
thing that never survives to the end. Any fix to §38.1 should be judged first
|
||||||
|
on whether those two fails disappear.
|
||||||
|
|
||||||
|
Causal check
|
||||||
(`experiments/diag_exposure_frontage.py`, 6 seeds): relabelling harbor's two
|
(`experiments/diag_exposure_frontage.py`, 6 seeds): relabelling harbor's two
|
||||||
`private` edges as open — identical geometry, identical programme, perimeter
|
`private` edges as open — identical geometry, identical programme, perimeter
|
||||||
labels only — cuts zero-exposure leaves **52% → 19%** and seeder crinkliness
|
labels only — cuts zero-exposure leaves **52% → 19%** and seeder crinkliness
|
||||||
|
|
@ -4971,7 +5021,36 @@ fitness weight between two single rooms**. In programme-house, missing `b1`
|
||||||
3 fails = 1/8. The tiered comparator inherits it: `n_hard` is dominated by
|
3 fails = 1/8. The tiered comparator inherits it: `n_hard` is dominated by
|
||||||
these cascades, so the primary search key is weighted by config verbosity.
|
these cascades, so the primary search key is weighted by config verbosity.
|
||||||
|
|
||||||
### 38.6 Consequences for the Phase 9 plan
|
### 38.6 First repair attempt: three crinkliness modes — NOT SUFFICIENT ALONE
|
||||||
|
|
||||||
|
`fitness.quality_uncrinkliness` gained `crinkliness_mode` (config key,
|
||||||
|
EXPERIMENTAL, default `"urb"` = stock hard 0.0, byte-identical to all prior
|
||||||
|
runs). Three candidate repairs, A/B'd on the §38.2 deletion test (harbor-house,
|
||||||
|
3 constructed seeds, unpinned `C`/`O` leaves only):
|
||||||
|
|
||||||
|
Splitting the deleted leaves by whether they were buried or lit is what makes
|
||||||
|
the result legible (`experiments/ab_crinkliness_mode_ssz.py`):
|
||||||
|
|
||||||
|
| mode | buried rewarded | **lit rewarded** | all | median × |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `urb` (stock) | 5/8 | **3/8** | 8/16 | ×1.02 |
|
||||||
|
| `floor` (clamp to 0.01, keep the fail) | 5/8 | **3/8** | 8/16 | ×1.02 |
|
||||||
|
| `compact_ok` (one-sided: compact is not a defect) | 5/8 | **3/8** | 8/16 | ×1.00 |
|
||||||
|
| `exempt_circulation` (corridors need no daylight) | **4/8** | **3/8** | 7/16 | ×0.61 |
|
||||||
|
|
||||||
|
**None of them removes the incentive, and the `lit` column does not move at
|
||||||
|
all — 3/8 under every mode, including stock.** That column is mechanism (2)
|
||||||
|
in isolation: deleting a *well-daylit* corridor is rewarded for reasons that
|
||||||
|
have nothing to do with crinkliness, so nothing written inside
|
||||||
|
`quality_uncrinkliness` can ever reach it. `floor` is inert (0.01 of a unit
|
||||||
|
quality is still ~zero against the cost saving); `exempt_circulation` removes
|
||||||
|
exactly one buried case.
|
||||||
|
|
||||||
|
The modes are kept, default off, as one half of a fix that needs both halves.
|
||||||
|
**Do not ship any of them as a standalone lever and expect the connectivity
|
||||||
|
fails to move** — `homemaker-py-2v1` is the half that matters.
|
||||||
|
|
||||||
|
### 38.7 Consequences for the Phase 9 plan
|
||||||
|
|
||||||
§37 track 1 ("no ground truth … the residual taxonomy may be miscalibrated
|
§37 track 1 ("no ground truth … the residual taxonomy may be miscalibrated
|
||||||
rather than unmet") was aimed at the right target, and §38.3 supplies a cheap
|
rather than unmet") was aimed at the right target, and §38.3 supplies a cheap
|
||||||
|
|
@ -4985,9 +5064,17 @@ part, an unsatisfiable constraint.
|
||||||
Tracks 2 and 3 (cheaper evaluation, exact sub-solvers) remain sound but are
|
Tracks 2 and 3 (cheaper evaluation, exact sub-solvers) remain sound but are
|
||||||
orthogonal: making an evaluation 97× faster, or a labelling exact, does not
|
orthogonal: making an evaluation 97× faster, or a labelling exact, does not
|
||||||
change which direction the objective points. Recommended ordering is now
|
change which direction the objective points. Recommended ordering is now
|
||||||
`ssz` → `hxi`/`gvb` (restore a value gradient for interior space, re-tier),
|
`2v1` (price connectivity above the ×6 circulation→habitable value gap — the
|
||||||
then `tdp` (ship the pre-flight bound and re-baseline the corpus), and only
|
dominant mechanism, and the one the §38.6 A/B isolated) → `ssz`/`hxi`/`gvb`
|
||||||
then resume `2g7.9`/`2g7.10`. In particular `2g7.7` (LLM repair operator at
|
(restore a value gradient for interior space, re-tier), then `tdp` (ship the
|
||||||
|
pre-flight bound and re-baseline the corpus), and only then resume
|
||||||
|
`2g7.9`/`2g7.10`.
|
||||||
|
|
||||||
|
**Acceptance test for the combined fix, stated up front so it cannot be
|
||||||
|
moved:** harbor-house must reach its known 15-fail floor in materially fewer
|
||||||
|
than 1.7 M evals, *and* `level 0 not connected` / `level 1 not connected` must
|
||||||
|
be absent from the result. Fail-count parity alone is not a pass — the whole
|
||||||
|
claim of §38 is that those two fails are bought, not missed. In particular `2g7.7` (LLM repair operator at
|
||||||
stagnation) is worth deferring until after `ssz`: an LLM asked to propose a
|
stagnation) is worth deferring until after `ssz`: an LLM asked to propose a
|
||||||
valley-crossing multi-edit against an objective that pays ×85 to delete the
|
valley-crossing multi-edit against an objective that pays ×85 to delete the
|
||||||
corridor it just inserted will have its work reverted by the next selection
|
corridor it just inserted will have its work reverted by the next selection
|
||||||
|
|
|
||||||
151
experiments/ab_crinkliness_mode_ssz.py
Normal file
151
experiments/ab_crinkliness_mode_ssz.py
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
"""A/B the `crinkliness_mode` repairs against the circulation-deletion incentive.
|
||||||
|
|
||||||
|
Evidence for `homemaker-py-ssz` / `homemaker-py-2v1` (DESIGN.md §38.6). The
|
||||||
|
test: on a constructed layout, delete each unpinned circulation/outside leaf
|
||||||
|
(by undividing its parent, which merges it into its sibling) and re-score. A
|
||||||
|
healthy objective should not pay you to do that.
|
||||||
|
|
||||||
|
Reports the count of deletions that still improve the score, and the median
|
||||||
|
score ratio, per mode. Splitting the rows by whether the deleted leaf was
|
||||||
|
buried or lit is what separates the two mechanisms:
|
||||||
|
|
||||||
|
* buried leaves are rewarded because ``quality_uncrinkliness`` returns a hard
|
||||||
|
0.0, so they contribute zero value (§38.1);
|
||||||
|
* **lit** leaves are rewarded because ``value_circulation`` (50) is one sixth
|
||||||
|
of ``value_inside`` (300), so merging corridor into room is a flat ×6 gain
|
||||||
|
against a ×0.5 connectivity penalty (§38.2 refinement) — which no
|
||||||
|
``crinkliness_mode`` can touch.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
python experiments/ab_crinkliness_mode_ssz.py
|
||||||
|
python experiments/ab_crinkliness_mode_ssz.py --seeds 5 --progdir examples/maple-court
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from homemaker_layout import dom as dom_mod
|
||||||
|
from homemaker_layout import driver, fitness, geometry
|
||||||
|
from homemaker_layout import graph as graph_mod
|
||||||
|
from homemaker_layout import operators, programme
|
||||||
|
|
||||||
|
MODES = ("urb", "floor", "compact_ok", "exempt_circulation")
|
||||||
|
|
||||||
|
|
||||||
|
def make_fitness(progdir: str, mode: str) -> fitness.Fitness:
|
||||||
|
"""Evaluator matching ``driver.search``'s defaults, with one mode override."""
|
||||||
|
overrides = dict(driver._overrides_for(
|
||||||
|
leaf_sharing=True, superpose=False, max_share=None, conn_grade=False,
|
||||||
|
collapse_insearch=True, multi_use=False) or {})
|
||||||
|
overrides["crinkliness_mode"] = mode
|
||||||
|
conf, cost = fitness.load_config(progdir, overrides=overrides)
|
||||||
|
return fitness.Fitness(conf, cost)
|
||||||
|
|
||||||
|
|
||||||
|
def constructed_seed(progdir: str, seed: int) -> dom_mod.Node:
|
||||||
|
reqs = programme.load_programme_dir(progdir)
|
||||||
|
return operators.constructive_topology(
|
||||||
|
dom_mod.load(f"{progdir}/init.dom"), reqs, np.random.default_rng(seed),
|
||||||
|
sorted(reqs) + ["C", "O"],
|
||||||
|
min_storeys=programme.storey_minimum(progdir),
|
||||||
|
adjacency_aware=True, proportion_aware=True, circ_divisor=3,
|
||||||
|
leaf_sharing=True, leaf_share_factor=3, depth_balanced=True,
|
||||||
|
interior_outside=True, outside_divisor=3)
|
||||||
|
|
||||||
|
|
||||||
|
def unpinned_leaves(fit: fitness.Fitness, root: dom_mod.Node) -> list[tuple]:
|
||||||
|
"""(level, id, type, exposed_area) for circulation/outside leaves — the ones
|
||||||
|
no missing-space cascade pins in place."""
|
||||||
|
tree = copy.deepcopy(root)
|
||||||
|
geometry.clear_cache()
|
||||||
|
dom_mod.canonicalize_shares(tree)
|
||||||
|
fit.preprocess_building(tree)
|
||||||
|
dom_mod.merge_divided(tree)
|
||||||
|
geometry.clear_cache()
|
||||||
|
graphs = graph_mod.build_graphs(tree, fit.conf("door_width") or 1.2)
|
||||||
|
|
||||||
|
out = []
|
||||||
|
for li, lvl in enumerate(dom_mod.levels(tree)):
|
||||||
|
for leaf in lvl.leaves():
|
||||||
|
if dom_mod.is_outside(leaf) and not dom_mod.is_covered(leaf):
|
||||||
|
continue
|
||||||
|
if (leaf.type or "")[:1].upper() not in ("C", "O"):
|
||||||
|
continue
|
||||||
|
out.append((li, leaf.id, leaf.type,
|
||||||
|
fit.area_outside(leaf, graphs[li], {})))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def delete_leaf(root: dom_mod.Node, li: int, lid: str) -> "dom_mod.Node | None":
|
||||||
|
"""Undivide the leaf's parent, merging it into its sibling. None if the cut
|
||||||
|
is inherited (not owned at this storey) or the sibling is itself divided."""
|
||||||
|
cand = copy.deepcopy(root)
|
||||||
|
lvls = dom_mod.levels(cand)
|
||||||
|
if li >= len(lvls):
|
||||||
|
return None
|
||||||
|
node = lvls[li].by_id(lid)
|
||||||
|
if node is None or node.parent is None:
|
||||||
|
return None
|
||||||
|
parent = node.parent
|
||||||
|
if parent.below is not None and parent.below.divided:
|
||||||
|
return None
|
||||||
|
sibling = parent.right if parent.left is node else parent.left
|
||||||
|
if sibling is None or sibling.divided:
|
||||||
|
return None
|
||||||
|
parent.division = None
|
||||||
|
parent.left = parent.right = None
|
||||||
|
parent.type = sibling.type
|
||||||
|
dom_mod.link(cand)
|
||||||
|
geometry.clear_cache()
|
||||||
|
return cand
|
||||||
|
|
||||||
|
|
||||||
|
def run(progdir: str, seeds: int) -> None:
|
||||||
|
print(f"programme: {progdir}, {seeds} constructed seeds")
|
||||||
|
print("a healthy objective rewards NO deletions\n")
|
||||||
|
header = f"{'mode':<20}{'buried':<14}{'lit':<14}{'all':<14}median x"
|
||||||
|
print(header)
|
||||||
|
print("-" * len(header))
|
||||||
|
|
||||||
|
for mode in MODES:
|
||||||
|
fit = make_fitness(progdir, mode)
|
||||||
|
counts = {"buried": [0, 0], "lit": [0, 0]}
|
||||||
|
ratios = []
|
||||||
|
for s in range(seeds):
|
||||||
|
root = constructed_seed(progdir, s)
|
||||||
|
base, _ = fit.score_with_fails(copy.deepcopy(root))
|
||||||
|
for li, lid, _typ, exposed in unpinned_leaves(fit, root):
|
||||||
|
cand = delete_leaf(root, li, lid)
|
||||||
|
if cand is None:
|
||||||
|
continue
|
||||||
|
score, _ = fit.score_with_fails(copy.deepcopy(cand))
|
||||||
|
bucket = "buried" if exposed == 0 else "lit"
|
||||||
|
counts[bucket][1] += 1
|
||||||
|
ratios.append(score / base)
|
||||||
|
if score > base:
|
||||||
|
counts[bucket][0] += 1
|
||||||
|
tot = [counts["buried"][0] + counts["lit"][0],
|
||||||
|
counts["buried"][1] + counts["lit"][1]]
|
||||||
|
median = sorted(ratios)[len(ratios) // 2] if ratios else float("nan")
|
||||||
|
buried = "%d/%d" % tuple(counts["buried"])
|
||||||
|
lit = "%d/%d" % tuple(counts["lit"])
|
||||||
|
both = "%d/%d" % tuple(tot)
|
||||||
|
print(f"{mode:<20}{buried:<14}{lit:<14}{both:<14}x{median:.2f}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--progdir", default="examples/harbor-house")
|
||||||
|
ap.add_argument("--seeds", type=int, default=3)
|
||||||
|
args = ap.parse_args()
|
||||||
|
run(args.progdir, args.seeds)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -372,6 +372,21 @@ class Fitness:
|
||||||
# target combines both codes' area (quality_size).
|
# target combines both codes' area (quality_size).
|
||||||
self._multi_use = bool(self.conf("multi_use"))
|
self._multi_use = bool(self.conf("multi_use"))
|
||||||
self._colocate_pairs: list | None = None # lazily derived
|
self._colocate_pairs: list | None = None # lazily derived
|
||||||
|
# homemaker-py-ssz (DESIGN.md §38.1): how quality_uncrinkliness treats a
|
||||||
|
# leaf with no daylit wall. "urb" (default) = stock hard 0.0, byte-
|
||||||
|
# identical to every prior run. "floor"/"compact_ok"/"exempt_circulation"
|
||||||
|
# are the three candidate repairs — see quality_uncrinkliness.
|
||||||
|
self._crinkliness_mode = str(self.conf("crinkliness_mode") or "urb")
|
||||||
|
if self._crinkliness_mode not in (
|
||||||
|
"urb", "floor", "compact_ok", "exempt_circulation"):
|
||||||
|
raise ValueError(
|
||||||
|
f"unknown crinkliness_mode: {self._crinkliness_mode!r}")
|
||||||
|
# The floored value stays BELOW FAIL_THRESHOLD, so a buried leaf still
|
||||||
|
# emits its crinkliness failure and the fail count is unchanged — only
|
||||||
|
# the value gradient is restored. Raising this above FAIL_THRESHOLD
|
||||||
|
# would silently delete a whole fail category.
|
||||||
|
self._crinkliness_floor = float(
|
||||||
|
self.conf("crinkliness_floor") or 0.01)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Type superposition + collapse (homemaker-py-9o5)
|
# Type superposition + collapse (homemaker-py-9o5)
|
||||||
|
|
@ -1093,9 +1108,35 @@ class Fitness:
|
||||||
key = "uncrinkliness_circulation" if dom_mod.is_circulation(leaf) else "uncrinkliness"
|
key = "uncrinkliness_circulation" if dom_mod.is_circulation(leaf) else "uncrinkliness"
|
||||||
distance, sigma = self.conf(key)
|
distance, sigma = self.conf(key)
|
||||||
crink = self.crinkliness(leaf, G, groups)
|
crink = self.crinkliness(leaf, G, groups)
|
||||||
|
|
||||||
|
# homemaker-py-ssz (DESIGN.md §38.1), EXPERIMENTAL, all default OFF —
|
||||||
|
# `crinkliness_mode="urb"` reproduces the stock behaviour exactly.
|
||||||
|
#
|
||||||
|
# Stock Urb returns a hard 0.0 for a leaf with no daylit wall. That is
|
||||||
|
# the correct limit of the formula (1/crink -> inf, gaussian -> 0), but
|
||||||
|
# because evaluate_leaf MULTIPLIES factors into quality and
|
||||||
|
# process_storey accumulates `value += quality * rate * area`, such a
|
||||||
|
# leaf contributes EXACTLY ZERO value while still costing — so the
|
||||||
|
# objective cannot rank buried rooms at all, and buried circulation/
|
||||||
|
# outside leaves (which no missing-space cascade pins) are pure
|
||||||
|
# liabilities worth ~x60-x85 to delete. Measured: 45-56% of interior
|
||||||
|
# leaves are in this state. These modes restore a gradient there.
|
||||||
|
mode = self._crinkliness_mode
|
||||||
|
if mode == "exempt_circulation" and dom_mod.is_circulation(leaf):
|
||||||
|
# (c) internal corridors are ordinary architecture; stop requiring
|
||||||
|
# every circulation leaf to reach daylight.
|
||||||
|
return 1.0
|
||||||
if not crink:
|
if not crink:
|
||||||
return 0.0
|
# (a) floor: keep buried leaves rankable by their other factors
|
||||||
return gaussian(1 / crink, 1.0, distance, sigma)
|
# instead of collapsing the whole quality product to zero.
|
||||||
|
return self._crinkliness_floor if mode in ("floor", "compact_ok") else 0.0
|
||||||
|
q = gaussian(1 / crink, 1.0, distance, sigma)
|
||||||
|
if mode == "compact_ok" and 1 / crink > distance:
|
||||||
|
# (b) one-sided: being MORE compact than target is not a defect the
|
||||||
|
# way over-exposure is, so clip to 1.0 on the compact side rather
|
||||||
|
# than decaying symmetrically into a fail.
|
||||||
|
return 1.0
|
||||||
|
return max(q, self._crinkliness_floor) if mode in ("floor", "compact_ok") else q
|
||||||
|
|
||||||
# --- access --- #
|
# --- access --- #
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -468,3 +468,65 @@ def test_classify_fail_tier_covers_full_corpus():
|
||||||
classify_fail_tier(line) # raises on failure
|
classify_fail_tier(line) # raises on failure
|
||||||
checked += 1
|
checked += 1
|
||||||
assert checked > 0
|
assert checked > 0
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# homemaker-py-ssz / DESIGN.md §38.1 — crinkliness_mode (EXPERIMENTAL)
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
class _StubCrink(Fitness):
|
||||||
|
"""Fitness with ``crinkliness`` stubbed, so the modes can be tested without
|
||||||
|
building a real tree/graph (the value under test is the branch, not the
|
||||||
|
geometry)."""
|
||||||
|
|
||||||
|
_stub = 0.0
|
||||||
|
|
||||||
|
def crinkliness(self, leaf, G, groups): # noqa: D102 - test stub
|
||||||
|
return self._stub
|
||||||
|
|
||||||
|
|
||||||
|
def _stub_fit(mode=None, stub=0.0, type_="t1"):
|
||||||
|
conf = dict(CONF_DEFAULTS)
|
||||||
|
if mode is not None:
|
||||||
|
conf["crinkliness_mode"] = mode
|
||||||
|
f = _StubCrink(conf, dict(COST_DEFAULTS))
|
||||||
|
f._stub = stub
|
||||||
|
return f, _leaf(type_)
|
||||||
|
|
||||||
|
|
||||||
|
def test_crinkliness_mode_defaults_to_urb_and_reproduces_hard_zero():
|
||||||
|
"""Default must be byte-identical to stock Urb: buried leaf -> exactly 0.0."""
|
||||||
|
f, leaf = _stub_fit()
|
||||||
|
assert f._crinkliness_mode == "urb"
|
||||||
|
assert f.quality_uncrinkliness(leaf, None, {}) == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_crinkliness_floor_restores_gradient_but_keeps_the_failure():
|
||||||
|
"""The floor must stay BELOW FAIL_THRESHOLD: it restores a value gradient
|
||||||
|
without silently deleting a whole fail category."""
|
||||||
|
f, leaf = _stub_fit("floor")
|
||||||
|
q = f.quality_uncrinkliness(leaf, None, {})
|
||||||
|
assert q > 0.0, "buried leaf should no longer be worth exactly nothing"
|
||||||
|
assert q < FAIL_THRESHOLD, "buried leaf must still emit its crinkliness fail"
|
||||||
|
|
||||||
|
|
||||||
|
def test_crinkliness_compact_ok_clips_on_the_compact_side_only():
|
||||||
|
"""Being more compact than target is not a defect; being over-exposed is."""
|
||||||
|
target = CONF_DEFAULTS["uncrinkliness"][0]
|
||||||
|
# 1/crink > target => more compact than target => clipped to 1.0
|
||||||
|
f, leaf = _stub_fit("compact_ok", stub=1.0 / (target * 2))
|
||||||
|
assert f.quality_uncrinkliness(leaf, None, {}) == 1.0
|
||||||
|
# 1/crink < target => over-exposed => still decays
|
||||||
|
f, leaf = _stub_fit("compact_ok", stub=1.0 / (target / 2))
|
||||||
|
assert f.quality_uncrinkliness(leaf, None, {}) < 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_crinkliness_exempt_circulation_only_exempts_circulation():
|
||||||
|
f, circ = _stub_fit("exempt_circulation", type_="C")
|
||||||
|
assert f.quality_uncrinkliness(circ, None, {}) == 1.0
|
||||||
|
f, room = _stub_fit("exempt_circulation", type_="t1")
|
||||||
|
assert f.quality_uncrinkliness(room, None, {}) == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_crinkliness_mode_unknown_raises():
|
||||||
|
with pytest.raises(ValueError, match="crinkliness_mode"):
|
||||||
|
_stub_fit("nonsense")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue