From fd9802e499e6dd370bce4d7fd65782410225d6bc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 08:27:49 +0000 Subject: [PATCH] =?UTF-8?q?DESIGN.md=20=C2=A739:=20config=20audit=20?= =?UTF-8?q?=E2=80=94=20programme=20codes=20collide=20with=20the=20c/o/s=20?= =?UTF-8?q?type=20namespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB --- .beads/issues.jsonl | 1 + DESIGN.md | 85 ++++++++++ experiments/audit_programme_config.py | 213 ++++++++++++++++++++++++++ 3 files changed, 299 insertions(+) create mode 100644 experiments/audit_programme_config.py diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index ce79414..c6e5db9 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,3 +1,4 @@ +{"_type":"issue","id":"homemaker-py-ju3","title":"Programme codes share a namespace with the generic c/o/s type prefixes: 14% of harbor-house is silently optional and cr1's declared targets are all discarded","description":"Urb's type system is prefix-based (a type starting with 'c' is circulation, 'o'/'s' is outside) and programme room codes live in the SAME namespace, so any code whose name happens to start with c, o or s is silently reinterpreted as a generic type. Three separate consequences, none announced anywhere in the output:\\n\\n1. graph.check_space_counts line ~530 does 'if code[0].lower() in (\"c\",\"o\",\"s\"): continue' -- the code is SKIPPED ENTIRELY. Never required, never counted, no missing fail, no too-many fail.\\n2. Fitness.get_space_params returns the generic *_circulation / *_outside params BEFORE consulting self.spaces, so declared size/width/proportion are overridden.\\n3. dom.is_circulation / is_outside become true, changing the leaf's value rate, exempting it from crinkliness, and making it supply daylight to neighbours.\\n\\nharbor-house is affected; maple-court, health-centre and programme-house are namespace-clean.\\n\\n cr1 'Common Room with Fireplace' (c): size 80.0 -\u003e 0.0/14.0, width 6.0 -\u003e 2.4, proportion 2.0 -\u003e 1.5, ALL THREE overridden; is_circulation=True so value_rate 50 not 300.\\n of 'Staff Office' x2 (o): width/proportion overridden; is_outside=True; value_rate 100.\\n st1/st2 'Storage' (s): width/proportion overridden; is_outside AND is_circulation True; value_rate 100.\\n\\n5 of 37 room instances (14%) are silently optional. MEASURED CONSEQUENCE: in a 20k-eval run the two cr1 leaves converged to 32.9 and 17.1 m2 against a declared 80 m2, and produced no too-many-spaces fail despite count:1; of/st1/st2 are absent from the result entirely with zero fails, because nothing ever asked for them. Compounds with homemaker-py-2v1: cr1 is the single largest room in the programme and is classified circulation, so the x6 value gap pays the search to shrink it.","design":"Separate the namespaces. Cleanest is an explicit per-space 'class:' key in patterns.config (inside/circulation/outside) defaulting to inside, with the prefix rule used ONLY for untyped generic leaves the search creates -- programme codes then never collide regardless of spelling. A cheaper stopgap is a load-time validation error in programme.load_programme_dir that refuses a programme code starting with c/o/s, which at least converts a silent misread into a loud one. Renaming harbor's four codes would fix that one programme but leaves the trap armed for the next author.","acceptance_criteria":"A programme declaring a code starting with c/o/s either honours its declared params and count, or fails loudly at load. harbor-house re-baselined against its real 37-instance programme, and every DESIGN.md harbor fail count re-stated or annotated as measured against the 32-instance effective programme.","status":"open","priority":0,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-26T08:26:37Z","created_by":"Claude","updated_at":"2026-08-26T08:26:37Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-2v1","title":"Connectivity is under-priced ~3x against the circulation-\u003ehabitable value gap: the objective is net-positive on severing the spine","description":"Sharper root cause than homemaker-py-ssz, isolated by the 38.6 A/B (which showed none of the three crinkliness modes removes the deletion incentive). Deleting a circulation leaf merges it into its sibling, converting corridor into habitable area. value_circulation=50 vs value_inside=300, so that is a flat 6x value gain. The only counter-pressure is the 'level N not connected' fail, worth 0.5x under value *= 0.5**len(failures). Break-even needs 0.5^k \u003c 50/300, i.e. k \u003e 2.58 -- severing must cost AT LEAST 3 fails to be net-negative, and it costs 1. Net incentive to sever = 6 * 0.5 = 3.0x in favour; measured 4.06x on a well-lit (q_crink=0.736) circulation leaf, so this is NOT the zero-exposure effect and is not fixable inside quality_uncrinkliness. This is the cleanest explanation of why 'level 0 not connected' and 'level 1 not connected' are still present in evolved-3M-nols-3, the best layout found after 1.7M evals: the search is being paid 3-4x to create them.","design":"Options: (a) emit connectivity fails with a multiplicity \u003e= 3 (cheapest, but stacks with the 1i8 cascade-weighting problem and is a magic number); (b) make the connectivity penalty multiplicative and explicit rather than riding the generic 0.5^n (a dedicated building_factor term, sized from the value-rate gap so it tracks value_circulation/value_inside instead of being hard-coded); (c) revalue circulation as infrastructure -- its worth is that it makes other rooms reachable, which the current per-leaf value rate cannot express; the principled version credits circulation with the access it provides rather than its own floor area. (c) is the architecturally correct one and the biggest change. Recommend measuring (a) first purely to confirm the mechanism (does the 3x threshold flip the deletion test?), then designing (b) or (c) properly.","acceptance_criteria":"Deletion test (experiments/diag_exposure_frontage.py value + the ssz A/B harness) shows lit and buried C/O deletions are no longer rewarded; then harbor-house reaches the 15-fail floor in materially fewer than 1.7M evals AND without 'level 0/1 not connected' in the result.","status":"open","priority":0,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-26T07:37:39Z","created_by":"Claude","updated_at":"2026-08-26T07:37:39Z","dependency_count":0,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"homemaker-py-hxi","title":"Buried circulation and outside space are negative-value: search is rewarded for deleting the circulation spine","description":"Direct consequence of the zero-exposure bug. Measured on a constructed harbor-house seed: deleting a buried O leaf improved the raw score 85x and removed 7 fails; deleting a buried C leaf improved it 62x and removed 6 fails. Programme rooms are held in place ONLY by the missing-space fail cascade, not by contributing value -- deleting a buried k1/da1/m costs +15 fails, so they stay, but nothing positive keeps them. Circulation and outside leaves carry no missing-space requirement, so nothing keeps them at all. Observed live: in a 20k-eval harbor-house run, undivide/core_undivide account for a large share of recorded improvements (16 occurrences in the log) -- the search is literally deleting circulation to score better. This explains three prior negative results as a single mechanism: 18 graded circulation-connectivity (a tie-break signal cannot beat a 60x scalar gradient), 21/22 bridge_circulation (the operator inserts corridor leaves the objective immediately punishes), and the 'level N not connected' hard fails surviving \u003e1M evals in the 3M run.","notes":"Depends on the zero-exposure fix; may need no separate fix if (a)/(c) there restores a value gradient for circulation. Worth re-running the 18 and 21/22 A/Bs afterwards -- both may have been measuring a broken gradient rather than a bad idea.","status":"open","priority":1,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-25T23:14:40Z","created_by":"Claude","updated_at":"2026-08-25T23:14:40Z","dependencies":[{"issue_id":"homemaker-py-hxi","depends_on_id":"homemaker-py-ssz","type":"blocks","created_at":"2026-08-25T23:15:12Z","created_by":"Claude","metadata":"{}"},{"issue_id":"homemaker-py-hxi","depends_on_id":"homemaker-py-2v1","type":"blocks","created_at":"2026-08-26T07:37:47Z","created_by":"Claude","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"homemaker-py-ssz","title":"Zero-exposure leaves score hard quality=0: the objective assigns no value to any interior room","description":"fitness.quality_uncrinkliness returns a hard 0.0 when a leaf has no daylit wall (area_outside==0 =\u003e crink==0 =\u003e 'if not crink: return 0.0'). Because evaluate_leaf MULTIPLIES factors into quality, and process_storey accumulates 'value += quality * rate * area', such a leaf contributes EXACTLY ZERO value while still adding cost. Measured on the full default construction stack (leaf_sharing, depth_balanced, interior_outside, collapse_insearch), 3 seeds each: harbor-house 46% of interior leaves, health-centre 45%, maple-court 56% are zero-exposure. On a converged 20k-eval harbor-house run (seed 1, 57 fails) 14 of 17 crinkliness fails are zero-exposure, and ~470 m2 of the 721 m2 ground floor plate sits at zero value. This is the mathematically consistent limit of the gaussian (1/crink -\u003e inf), so it is a faithful port, not a porting bug -- but it means the objective's gradient does not describe a good building.","design":"Options, none yet chosen: (a) floor the factor at a small epsilon instead of 0 so buried leaves keep a value gradient and remain rankable; (b) make the gaussian one-sided (clip to 1.0 on the compact side) so being LESS exposed than target is not punished as hard as being over-exposed -- architecturally, a compact well-insulated room is not a defect; (c) exempt circulation/store types from the daylight requirement entirely (uncrinkliness_circulation currently uses the same [5/6, 1.1/3] as habitable rooms, so internal corridors -- completely normal architecture -- are guaranteed failures). Any change here invalidates prior fail-count baselines, so it needs its own A/B and a DESIGN.md section.","acceptance_criteria":"A/B on harbor-house + maple-court at fixed budget showing the chosen variant lowers hard-fail count without inflating soft; DESIGN.md section recording the result; prior baselines re-stated under the new objective.","status":"open","priority":1,"issue_type":"bug","owner":"noreply@anthropic.com","created_at":"2026-08-25T23:14:40Z","created_by":"Claude","updated_at":"2026-08-25T23:14:40Z","dependencies":[{"issue_id":"homemaker-py-ssz","depends_on_id":"homemaker-py-2v1","type":"blocks","created_at":"2026-08-26T07:37:47Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} diff --git a/DESIGN.md b/DESIGN.md index 0465731..a4dd376 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -5079,3 +5079,88 @@ 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 corridor it just inserted will have its work reverted by the next selection step. + +## 39. Config audit: requirements that actively fight the engine (`homemaker-py-ju3`) — measured 2026-08-25 + +The corpus `patterns.config` targets and `costs.config` values were estimated +years ago, on the principle that exact values are irrelevant to getting the +evolution engine working. This section asks the inverse question — *do any of +them actively work against it?* — and finds that one does, badly. + +Reproduce with `experiments/audit_programme_config.py` (two reports: +namespace collisions, and per-room-spec satisfiability). + +### 39.1 Per-room-spec satisfiability — CLEAN (negative result, recorded) + +Modelling each leaf as a rectangle of area `A` and aspect `r = w/h ≥ 1`, and +taking the FAIL_THRESHOLD-inverted bounds from the already-validated +`shapecurve.leaf_constraints` (§37.2), no room spec in any of the four corpus +programmes is internally contradictory: for every code there exists an `(A, r)` +satisfying size, width, proportion *and* crinkliness simultaneously, and none +needs more than one exposed side (i.e. none secretly demands a corner). The +hypothesis that the estimated targets are mutually unsatisfiable is +**falsified**, and the audit is kept so the question stays answered. + +(One instance of exactly this class was already known and handled: +`get_space_params` derives a width from size and proportion for small +programme spaces rather than falling back to `width_inside` `[4.0, 1.0]`, +"which is impossible for small programme spaces (e.g. a 3 m² WC)".) + +### 39.2 Programme codes collide with the generic type namespace — SEVERE + +Urb's type system is **prefix-based** — a type starting with `c` is +circulation, `o`/`s` is outside — and programme room codes live in the **same +namespace**. Any code whose name happens to begin with one of those letters is +silently reinterpreted as a generic type. Three independent 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 produces neither a missing nor a too-many + failure. **It is optional, silently.** +2. **`Fitness.get_space_params` returns the generic `*_circulation` / + `*_outside` parameters *before* consulting `self.spaces`**, so the declared + size/width/proportion are overridden. +3. **`dom.is_circulation` / `is_outside` become true**, changing the leaf's + value rate, exempting it from crinkliness, and making it supply daylight to + its neighbours. + +`harbor-house` — the project's primary benchmark — is affected; +`maple-court`, `health-centre` and `programme-house` are namespace-clean. + +| code | name | declared → effective | flags | value rate | +|---|---|---|---|---| +| `cr1` | Common Room with Fireplace | size **80.0 → 0.0/14.0**, width **6.0 → 2.4**, proportion **2.0 → 1.5** (all three) | `is_circulation` | **50** (vs 300) | +| `of` ×2 | Staff Office | width 2.5 → 3.0, proportion — → 1.5/50 | `is_outside` | 100 | +| `st1` | Ground Floor Storage | width 3.0 → 3.0/0.3, proportion — → 1.5/50 | `is_outside` **and** `is_circulation` | 100 | +| `st2` | First Floor Storage | width 3.0 → 3.0/0.3, proportion — → 1.5/50 | `is_outside` **and** `is_circulation` | 100 | + +**5 of harbor-house's 37 room instances (14%) are silently optional.** + +**Measured consequences**, on the 20 000-eval run of §38: + +- The two `cr1` leaves converged to **32.9 m² and 17.1 m² against a declared + 80 m²** — and produced **no `too many spaces` failure despite `count: 1`**, + because the count check skips the code. +- `of`, `st1` and `st2` are **absent from the result entirely, with zero + failures**, because nothing ever asked for them. + +This compounds directly with §38.2: `cr1` is the single largest room in the +programme, and classifying it as circulation puts it on the wrong side of the +×6 `value_circulation`/`value_inside` gap — so the objective is paid to shrink +the common room, which is exactly what it did. + +**Benchmark-validity consequence.** Every harbor-house fail count in this +document was measured against a **32-instance effective programme, not the +37-instance one its config declares**. Cross-programme comparisons involving +harbor-house (§13.9, §13.11, §17, §20, §23, §37.1, §37.7) are internally +consistent but are not measuring the programme as written. + +**Fix direction** (`homemaker-py-ju3`): separate the namespaces — an explicit +per-space `class:` key (`inside`/`circulation`/`outside`, defaulting to +`inside`), with the prefix rule used *only* for the untyped generic leaves the +search itself creates. A cheaper stopgap is a load-time validation error in +`programme.load_programme_dir` refusing a code that starts with `c`/`o`/`s`, +which at least converts a silent misread into a loud one. Renaming harbor's +four codes fixes that one programme but leaves the trap armed for the next +author. diff --git a/experiments/audit_programme_config.py b/experiments/audit_programme_config.py new file mode 100644 index 0000000..5f416f8 --- /dev/null +++ b/experiments/audit_programme_config.py @@ -0,0 +1,213 @@ +"""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()