Answers "are any config requirements actively fighting the engine". One is. §39.1 NEGATIVE (recorded): no room spec in any corpus programme is internally contradictory. Using shapecurve.leaf_constraints' validated FAIL_THRESHOLD inversions, every code admits an (area, aspect) satisfying size, width, proportion and crinkliness at once, and none needs more than one exposed side. The "estimated targets are mutually unsatisfiable" hypothesis is falsified. §39.2 SEVERE: Urb's type system is prefix-based (c = circulation, o/s = outside) and programme codes share that namespace. A code starting with those letters is silently reinterpreted, with three unannounced consequences: check_space_counts SKIPS it outright (never required, no missing or too-many fail); get_space_params returns generic *_circulation/*_outside params before consulting self.spaces; and is_circulation/is_outside flip, changing value rate, crinkliness exemption, and whether it supplies daylight to neighbours. harbor-house is affected (maple-court, health-centre, programme-house are clean): cr1 "Common Room with Fireplace" has all three declared targets overridden (size 80.0 -> 0.0/14.0) and is valued at 50/m2 not 300; of x2 and st1/st2 lose width/proportion and are treated as outside space. 5 of 37 room instances (14%) are silently optional. Measured: the two cr1 leaves converged to 32.9 and 17.1 m2 against a declared 80, with no too-many-spaces fail despite count:1; of/st1/st2 are absent from the result with zero fails. Compounds with §38.2 -- the largest room in the programme sits on the wrong side of the x6 circulation value gap, so the objective is paid to shrink it. Benchmark validity: every harbor-house fail count in this document was measured against a 32-instance effective programme, not the 37 its config declares. Adds experiments/audit_programme_config.py (namespace + satisfiability reports). Filed homemaker-py-ju3 (P0). No src changes; 336 passed, same 7 pre-existing fixture failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
213 lines
8.9 KiB
Python
213 lines
8.9 KiB
Python
"""Per-room-spec satisfiability audit of `patterns.config` targets.
|
|
|
|
Evidence for `homemaker-py-2v1`/`ssz`/`tdp` (DESIGN.md §38/§39). The corpus
|
|
configs were estimated years ago on the principle that exact values do not
|
|
matter for getting the engine working. This asks the opposite question: **does
|
|
any individual room spec make itself impossible to satisfy?**
|
|
|
|
For one room code, model the leaf as a rectangle of area ``A`` and aspect
|
|
``r = w/h >= 1`` (``h`` is `length_narrowest`, the width metric). The
|
|
FAIL_THRESHOLD-inverted bounds come from the already-validated
|
|
``shapecurve.leaf_constraints`` (§37.2), so this is not a reimplementation of
|
|
the Gaussians:
|
|
|
|
* size ``amin <= A <= amax``
|
|
* width ``h >= wmin`` => ``r <= A / wmin^2``
|
|
* proportion ``r <= rmax``
|
|
* crinkliness ``L_exposed >= A / (X * height)`` with ``X = 1.6202`` (§38.3)
|
|
|
|
The last one depends on how much of the leaf's boundary is external, which is a
|
|
*placement* property, not a spec property — so the audit reports the **minimum
|
|
number of exposed sides** each spec needs. A spec needing 2 adjacent sides is
|
|
demanding a corner; a rectangular storey has only four corners, so a programme
|
|
wanting more corner rooms than the plot has corners is over-subscribed before
|
|
the search starts.
|
|
|
|
Usage::
|
|
|
|
python experiments/audit_programme_config.py
|
|
python experiments/audit_programme_config.py examples/harbor-house --verbose
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import math
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import yaml
|
|
|
|
from homemaker_layout import dom, fitness, programme, shapecurve
|
|
from homemaker_layout.dom import Node
|
|
|
|
# 1/crink bounds from §38.3; recomputed from the live conf, never hard-coded.
|
|
def crink_bounds(fit: fitness.Fitness, circulation: bool = False) -> tuple[float, float]:
|
|
key = "uncrinkliness_circulation" if circulation else "uncrinkliness"
|
|
target, sigma = fit.conf(key)
|
|
k = math.sqrt(-2 * sigma * sigma
|
|
* math.log(fitness.FAIL_THRESHOLD) / math.log(fitness._E))
|
|
return target + k, max(1e-12, target - k)
|
|
|
|
|
|
# Exposure patterns, cheapest first: name -> exposed length given (w, h), w >= h.
|
|
EXPOSURE = [
|
|
("1 short side", lambda w, h: h),
|
|
("1 long side", lambda w, h: w),
|
|
("2 adjacent (corner)", lambda w, h: w + h),
|
|
("2 opposite long", lambda w, h: 2 * w),
|
|
("3 sides", lambda w, h: 2 * h + w),
|
|
("4 sides (freestanding)", lambda w, h: 2 * (w + h)),
|
|
]
|
|
|
|
|
|
def _synthetic_leaf(code: str) -> Node:
|
|
"""A bare typed leaf — ``leaf_constraints`` reads only its type/flags."""
|
|
return Node(node=[[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]], type=code)
|
|
|
|
|
|
def audit_code(fit: fitness.Fitness, code: str, height: float,
|
|
grid: int = 240) -> dict:
|
|
"""Feasibility of one room spec, and the exposure it needs."""
|
|
bounds = shapecurve.leaf_constraints(fit, _synthetic_leaf(code))
|
|
amin, amax, wmin, rmax = bounds.amin, bounds.amax, bounds.wmin, bounds.rmax
|
|
if not math.isfinite(amax):
|
|
amax = max(amin * 4, 200.0)
|
|
|
|
hi, lo = crink_bounds(fit, circulation=code[:1].lower() == "c")
|
|
areas = np.linspace(max(amin, 1e-6), amax, grid)
|
|
ratios = np.linspace(1.0, max(rmax, 1.0), grid)
|
|
A, R = np.meshgrid(areas, ratios, indexing="ij")
|
|
W, H = np.sqrt(A * R), np.sqrt(A / R)
|
|
|
|
swp = (H >= wmin) & (R <= rmax) # size is satisfied by construction
|
|
result = {"code": code, "amin": amin, "amax": amax, "wmin": wmin,
|
|
"rmax": rmax, "swp": bool(swp.any()), "needs": None,
|
|
"swp_only_at": None}
|
|
if not result["swp"]:
|
|
return result
|
|
|
|
# smallest square-ish area that satisfies width at r=1, for the report
|
|
result["swp_only_at"] = float(max(amin, wmin * wmin))
|
|
|
|
for name, length_of in EXPOSURE:
|
|
L = length_of(W, H)
|
|
crink_ok = (L >= A / (hi * height)) & (L <= A / (lo * height))
|
|
if bool((swp & crink_ok).any()):
|
|
result["needs"] = name
|
|
break
|
|
return result
|
|
|
|
|
|
def audit_namespace(progdir: str) -> int:
|
|
"""Report programme codes that collide with the generic type prefixes.
|
|
|
|
Urb's type system is prefix-based — a type starting with ``c`` is
|
|
circulation, ``o``/``s`` is outside — and programme codes live in the *same
|
|
namespace*. So a room code that happens to start with one of those letters
|
|
is silently reinterpreted as a generic type. Three separate consequences,
|
|
none of them announced anywhere in the output:
|
|
|
|
1. ``graph.check_space_counts`` **skips the code entirely**
|
|
(``if code[0].lower() in ("c", "o", "s"): continue``) — the room is never
|
|
required, never counted, and never produces a missing/too-many failure.
|
|
2. ``Fitness.get_space_params`` returns the generic
|
|
``*_circulation``/``*_outside`` parameters *before* consulting the
|
|
programme, so declared size/width/proportion are overridden.
|
|
3. ``dom.is_circulation``/``is_outside`` become true, changing the leaf's
|
|
value rate, its crinkliness treatment, and whether it supplies daylight
|
|
to its neighbours.
|
|
"""
|
|
reqs = programme.load_programme_dir(progdir)
|
|
conf, cost = fitness.load_config(progdir)
|
|
fit = fitness.Fitness(conf, cost)
|
|
spaces = conf.get("spaces") or {}
|
|
|
|
hits = [c for c in sorted(reqs) if c[:1].lower() in ("c", "o", "s")]
|
|
total = sum(r.count for r in reqs.values())
|
|
if not hits:
|
|
print(f"=== {Path(progdir).name}: namespace clean "
|
|
f"({len(reqs)} codes / {total} instances)\n")
|
|
return 0
|
|
|
|
skipped = sum(reqs[c].count for c in hits)
|
|
print(f"=== {Path(progdir).name}: {len(hits)} code(s) collide with the "
|
|
f"generic c/o/s type prefixes")
|
|
print(f" {skipped} of {total} room instances ({100 * skipped / total:.0f}%) "
|
|
f"are SILENTLY OPTIONAL — check_space_counts skips them\n")
|
|
for code in hits:
|
|
spec = spaces.get(code, {})
|
|
leaf = _synthetic_leaf(code)
|
|
print(f" {code} \"{reqs[code].name}\" (count {reqs[code].count})")
|
|
for param in ("size", "width", "proportion"):
|
|
declared = spec.get(param)
|
|
effective = fit.get_space_params(code, param)
|
|
flag = "" if declared == effective else " <-- OVERRIDDEN"
|
|
print(f" {param:<11} declared={str(declared):<16} "
|
|
f"effective={effective}{flag}")
|
|
print(f" is_circulation={dom.is_circulation(leaf)} "
|
|
f"is_outside={dom.is_outside(leaf)} "
|
|
f"value_rate={fit.value_rate(leaf)} (inside={fit.conf('value_inside')})")
|
|
print()
|
|
return skipped
|
|
|
|
|
|
def audit(progdir: str, verbose: bool) -> tuple[int, int]:
|
|
reqs = programme.load_programme_dir(progdir)
|
|
conf, cost = fitness.load_config(progdir)
|
|
fit = fitness.Fitness(conf, cost)
|
|
seed = yaml.safe_load(open(f"{progdir}/init.dom"))
|
|
height = seed.get("height") or 3.0
|
|
|
|
print(f"=== {Path(progdir).name} (height {height} m)")
|
|
hdr = f" {'code':<7}{'count':<7}{'area ok':<18}{'min width':<11}{'max aspect':<12}needs"
|
|
print(hdr)
|
|
print(" " + "-" * (len(hdr) - 2))
|
|
|
|
impossible = corner_demand = 0
|
|
for code in sorted(reqs) + ["C", "O"]:
|
|
count = reqs[code].count if code in reqs else 0
|
|
r = audit_code(fit, code, height)
|
|
if not r["swp"]:
|
|
verdict = "IMPOSSIBLE (size/width/proportion contradict)"
|
|
impossible += max(count, 1)
|
|
elif r["needs"] is None:
|
|
verdict = "IMPOSSIBLE even fully exposed (crinkliness)"
|
|
impossible += max(count, 1)
|
|
else:
|
|
verdict = r["needs"]
|
|
if "corner" in verdict or "opposite" in verdict or "3 sides" in verdict \
|
|
or "4 sides" in verdict:
|
|
corner_demand += max(count, 1)
|
|
area_col = "%.1f-%.1f m2" % (r["amin"], r["amax"])
|
|
width_col = "%.2f m" % r["wmin"]
|
|
aspect_col = "%.2f" % r["rmax"]
|
|
count_col = str(count) if count else "-"
|
|
print(f" {code:<7}{count_col:<7}{area_col:<18}"
|
|
f"{width_col:<11}{aspect_col:<12}{verdict}")
|
|
|
|
print(f"\n room instances that are impossible as specified : {impossible}")
|
|
print(f" room instances requiring >=2 exposed sides : {corner_demand}"
|
|
f" (a rectangular storey has 4 corners)\n")
|
|
return impossible, corner_demand
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("progdir", nargs="?", default=None)
|
|
ap.add_argument("--verbose", action="store_true")
|
|
args = ap.parse_args()
|
|
dirs = [args.progdir] if args.progdir else [
|
|
"examples/harbor-house", "examples/maple-court",
|
|
"examples/health-centre", "examples/programme-house"]
|
|
print("### namespace collisions (generic c/o/s type prefixes)\n")
|
|
for d in dirs:
|
|
audit_namespace(d)
|
|
print("### per-room-spec satisfiability\n")
|
|
for d in dirs:
|
|
audit(d, args.verbose)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|