Compare commits
10 commits
03aadec1ba
...
0d4ae7ad54
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d4ae7ad54 | |||
|
|
bb27a5e548 | ||
|
|
6405ca1a59 | ||
|
|
f1831687c1 | ||
|
|
02967d5de7 | ||
|
|
189efdbfc7 | ||
|
|
8522576467 | ||
|
|
024311a2c2 | ||
|
|
7c41069226 | ||
|
|
3aee813ccd |
18 changed files with 2532 additions and 55 deletions
File diff suppressed because one or more lines are too long
492
DESIGN.md
492
DESIGN.md
|
|
@ -4929,10 +4929,18 @@ 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.
|
||||
**RETRACTED — see §39.8.** The inference above ("the objective is net-positive
|
||||
on severing the spine") does not survive measurement. It assumed severing costs
|
||||
exactly one failure; it does not. Every deletion that actually breaks
|
||||
connectivity is already punished — measured ×0.00 to ×0.58 across harbor-house
|
||||
and maple-court, not one rewarded. The ×4.06 figure above is real but was
|
||||
measured on a deletion that did **not** change the connectivity fail count, so
|
||||
it is not evidence for this mechanism. The deletions that are rewarded are
|
||||
rewarded because they remove the deleted leaf's OWN quality failures (7–9 of
|
||||
them), which is §38.1's zero-value finding, not a connectivity mispricing.
|
||||
|
||||
Why `level 0/1 not connected` persist in the best layout is therefore still
|
||||
open, but it is not that the search is paid 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:
|
||||
|
|
@ -4960,6 +4968,14 @@ plot perimeter `private`**:
|
|||
| health-centre | 43 m | 41 m | feasible | §32 clean null | — |
|
||||
| programme-house | 24 m | 12 m | 2× surplus | — | **1 fail @ 12k evals** |
|
||||
|
||||
**PARTLY RETRACTED — see §39.11.** The "2.7× / 2.9× short" figures below are
|
||||
computed for a **fully built plot**, which is not what these programmes ask
|
||||
for. Against actual demand the deficits are far smaller and both are closable:
|
||||
harbor needs 86 m against 54 m supplied (a 48 m² courtyard, with 304 m² of plot
|
||||
spare), maple 70 m against 56 m (20 m²). Neither is infeasible. The one
|
||||
programme that genuinely does not fit is health-centre, for an unrelated and
|
||||
much simpler reason: it demands 240 m² on a 197 m² plot.
|
||||
|
||||
**Frontage deficit predicts the COST of solving, not impossibility.** An
|
||||
earlier draft of this section claimed the deficit predicts the plateau
|
||||
outright, quoting §13.11's 20k-budget figure as harbor's floor; that was
|
||||
|
|
@ -5059,10 +5075,16 @@ fails to move** — `homemaker-py-2v1` is the half that matters.
|
|||
rather than unmet") was aimed at the right target, and §38.3 supplies a cheap
|
||||
way to test it that does **not** need `2g7.1`'s traced human plans: the
|
||||
frontage bound is a pre-flight feasibility check computable from a plot and a
|
||||
programme alone. Two of the four corpus programmes fail it by ~3×, which means
|
||||
a share of the residual those runs are being judged on **is not reachable at
|
||||
all** — and any A/B measured against that residual has been measuring, in
|
||||
part, an unsatisfiable constraint.
|
||||
programme alone.
|
||||
|
||||
*(As first written this paragraph continued "two of the four corpus programmes
|
||||
fail it by ~3×, which means a share of the residual those runs are being judged
|
||||
on is not reachable at all". **That is withdrawn** — see §39.11. The ~3× came
|
||||
from applying the bound to a fully built plot rather than to the area each
|
||||
programme demands; harbor-house and maple-court are frontage-feasible with room
|
||||
to spare. One programme is unsatisfiable, health-centre, and for a cruder
|
||||
reason: it demands 131% of its plot. So the residual the other runs are judged
|
||||
on is reachable, and the plateau is not explained by an unsatisfiable brief.)*
|
||||
|
||||
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
|
||||
|
|
@ -5073,6 +5095,12 @@ dominant mechanism, and the one the §38.6 A/B isolated) → `ssz`/`hxi`/`gvb`
|
|||
pre-flight bound and re-baseline the corpus), and only then resume
|
||||
`2g7.9`/`2g7.10`.
|
||||
|
||||
*(Both halves of that ordering's rationale have since been measured and did not
|
||||
survive. `2v1` closed NULL — severing the spine is already punished, §38.2 is
|
||||
retracted — and `tdp`'s infeasibility claim is retracted above. What the
|
||||
ordering got right is that `ssz` comes before `2g7.7`; see §38.8 for what `ssz`
|
||||
turned out to be.)*
|
||||
|
||||
**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
|
||||
|
|
@ -5083,6 +5111,180 @@ 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.
|
||||
|
||||
### 38.8 What `ssz` actually was: the objective demands daylight for rooms that do not need it (`homemaker-py-ssz`)
|
||||
|
||||
**First, why §38.6's A/B does not stand.** It measured the three modes against
|
||||
the §38.2 *deletion test*, and it did so with a script that predates §39.4:
|
||||
`experiments/ab_crinkliness_mode_ssz.py` selected "unpinned" leaves with
|
||||
`(leaf.type or "")[:1].upper() in ("C", "O")`, the first-character prefix rule,
|
||||
so every programme room whose code happens to begin with c or o — `cr1`, `of1`
|
||||
— was swept in as circulation. Both the premise (§38.2, retracted) and the
|
||||
selection were wrong. The script is kept, with the prefix rule fixed and a
|
||||
warning in its docstring, but nothing is decided on its numbers.
|
||||
|
||||
**Second, and simpler: none of the three modes ever touched the leaves `ssz` is
|
||||
about.** `quality_uncrinkliness` reaches `if not crink: return ...` *before* any
|
||||
of the mode logic that matters, so for a zero-exposure leaf:
|
||||
|
||||
| mode | buried leaves | rescued to ≥ `FAIL_THRESHOLD` | windowless habitable rooms still failing |
|
||||
|---|---|---|---|
|
||||
| `urb` (stock) | 33 / 46 / 18 | 0% | 11/11, 13/13, 9/9 |
|
||||
| `floor` | 33 / 46 / 18 | 0% | 11/11, 13/13, 9/9 |
|
||||
| `compact_ok` *(as measured in §38.6)* | 33 / 46 / 18 | 0% | 11/11, 13/13, 9/9 |
|
||||
| `exempt_circulation` | 33 / 46 / 18 | 21% / 24% / 33% | 11/11, 13/13, 9/9 |
|
||||
|
||||
*(harbor-house / maple-court / health-centre, 3 constructed seeds each, full
|
||||
default stack.)*
|
||||
|
||||
`floor` returns 0.01 — one percent of a unit quality, multiplied into a product
|
||||
and weighed against a whole leaf's cost, which is why it reads as inert.
|
||||
`compact_ok` is worse than inert, it is **self-contradictory**: it announces
|
||||
that being more compact than target is not a defect, and then returns the floor
|
||||
for the most compact case of all, because `if not crink` fires before its clip
|
||||
is ever reached. Only `exempt_circulation` moves anything, and it reaches at
|
||||
most a third of the population. So §38.6's "none of them removes the incentive"
|
||||
was reading a null that the modes' own implementation guaranteed.
|
||||
|
||||
**What the buried leaves actually are.** §39.7 gave every space a declared
|
||||
`usage:`, which lets the question be asked properly for the first time — of the
|
||||
leaves scoring a hard zero for want of daylight, how many are rooms a person
|
||||
sits in?
|
||||
|
||||
| programme | buried | habitable (`living`/`kitchen`/`bedroom`) | store, toilet, plant, corridor, covered court |
|
||||
|---|---|---|---|
|
||||
| harbor-house | 33 | 11 (33%) | **22 (67%)** |
|
||||
| maple-court | 46 | 13 (28%) | **33 (72%)** |
|
||||
| health-centre | 18 | 9 (50%) | 9 (50%) |
|
||||
|
||||
**Roughly two thirds of the zero-value leaves are spaces that architecturally
|
||||
do not want a window at all** — a broom cupboard, a WC, a plant room, an
|
||||
internal corridor, a covered courtyard. The objective scores them identically
|
||||
with a windowless bedroom. That is the miscalibration, and it is not a gradient
|
||||
problem to be patched with an epsilon; it is the wrong requirement applied to
|
||||
the wrong rooms.
|
||||
|
||||
**The repair: `crinkliness_mode="usage_daylight"`.** Daylight is required of
|
||||
the uses a person occupies (`programme.DAYLIGHT_USAGES` =
|
||||
`living`/`kitchen`/`bedroom`) and of nothing else. For every other usage, and
|
||||
for the generic `C`/`O`/`S` types which carry no programme entry, the factor is
|
||||
clipped on the **compact side only** — being buried stops being a defect, while
|
||||
over-exposure still costs, because a crinkly leaf costs envelope whatever it is
|
||||
used for. A windowless bedroom remains exactly the hard zero it is under stock.
|
||||
|
||||
| programme | buried | rescued by `usage_daylight` | windowless habitable rooms still failing |
|
||||
|---|---|---|---|
|
||||
| harbor-house | 33 | 22 (67%) | 11/11 |
|
||||
| maple-court | 46 | 33 (72%) | 13/13 |
|
||||
| health-centre | 18 | 9 (50%) | 9/9 |
|
||||
|
||||
`compact_ok` was also repaired to score the buried limit as compact (1.0), the
|
||||
behaviour its name always claimed; it now rescues 100% and is kept as the
|
||||
**upper-bound control** — the mode that deletes the daylight requirement
|
||||
outright, including for bedrooms. It is not a candidate.
|
||||
|
||||
**How this is scored, stated before the result.** `compact_ok`,
|
||||
`exempt_circulation` and `usage_daylight` all return 1.0 where stock returns
|
||||
below `FAIL_THRESHOLD`, so scoring an arm under its own objective deletes a fail
|
||||
category for free and every arm "wins". Every arm below is therefore optimised
|
||||
under its own objective and **re-scored under stock `urb`** — the comparable
|
||||
yardstick, and the only one that answers *did optimising under this variant
|
||||
steer the search to a better building?* The arm's own-objective count is
|
||||
reported alongside solely to show the size of the definitional discount. A mode
|
||||
passes on the stock column. `experiments/ab_ssz_search.py`.
|
||||
|
||||
**The result: NOT A PASS at this budget, and n=3 cannot decide it.** Budget
|
||||
3000, 3 seeds, paired per-seed deltas against `urb` (negative = fewer fails):
|
||||
|
||||
| mode | harbor hard Δ | mean | maple hard Δ | mean |
|
||||
|---|---|---|---|---|
|
||||
| `floor` | [0, −1, −2] | −1.0 | [+5, −2, −1] | +0.7 |
|
||||
| `compact_ok` | [0, −2, **−12**] | −4.7 | [0, +2, +1] | +1.0 |
|
||||
| `exempt_circulation` | [+2, −3, −1] | −0.7 | [0, +2, +1] | +1.0 |
|
||||
| `usage_daylight` | [0, −1, **−10**] | −3.7 | [0, +2, **−10**] | −2.7 |
|
||||
|
||||
The means flatter every mode. **The whole signal is seed 2**, in both
|
||||
programmes, and seed 2 is the seed where stock itself does worst (harbor 22
|
||||
hard against 16/22; maple 51 against 30/23). Two seeds in three are flat or
|
||||
slightly worse. What this says is "on a bad run, the permissive modes do less
|
||||
badly" — which is not nothing, but it is not the claim.
|
||||
|
||||
And on that seed the soft count rises by as much as the hard count falls:
|
||||
harbor seed 2 −10 hard / +9 soft, maple seed 2 −10 hard / +15 soft. **Totals**:
|
||||
harbor 62 → 61, maple 120 → **125**. Because every arm is scored under stock
|
||||
`urb`, this is a real change of layout and not a relabelling — the search
|
||||
genuinely traded hard failures for soft ones. Under the project's tiered
|
||||
comparator, where `n_hard` is the primary key, that trade is progress. Under
|
||||
`ssz`'s acceptance criterion — *lowers hard without inflating soft* — it is
|
||||
not. The criterion is stricter than the comparator the search actually uses;
|
||||
which of the two is the right yardstick is now the live question, and it is
|
||||
`homemaker-py-gvb`'s question as much as this one.
|
||||
|
||||
`usage_daylight` stays **default off** pending a higher-powered run
|
||||
(`urb` vs `usage_daylight` only, more seeds, both programmes). Nothing here
|
||||
justifies shipping it as a default, and nothing here refutes it either: at
|
||||
n=3 with one dominant seed, the honest reading is *undecided*.
|
||||
|
||||
**What is decided** is the diagnostic half, which does not depend on the search
|
||||
A/B: the daylight requirement is applied to rooms that architecturally do not
|
||||
want daylight, in two thirds of the buried population, and §38.6's contrary
|
||||
null was an artefact of three modes that never touched those leaves.
|
||||
|
||||
### 38.9 Two corrections to §38.8, and the measurement that matters (`homemaker-py-ssz`)
|
||||
|
||||
**Correction 1 — the A/B yardstick above is wrong.** §38.8 scores every arm
|
||||
under stock `urb`, reasoning that a permissive mode must not be allowed to win
|
||||
by deleting a fail category. That is sound only if stock is ground truth, and
|
||||
stock is exactly what this section shows is miscalibrated. Scoring the repair
|
||||
under the objective it repairs penalises it for repairing: stock counts a
|
||||
windowless broom cupboard as a failure, and the repair's whole purpose is to
|
||||
stop counting it. *Does the fix score well on the broken yardstick* is not a
|
||||
question worth answering, and the §38.8 A/B result should not be read as
|
||||
evidence against `usage_daylight`.
|
||||
|
||||
**The measurement that does matter** asks whether the emitted failures are
|
||||
*true*, and needs no search at all (`experiments/audit_crinkliness_truth.py`).
|
||||
Of the `crinkliness` failures the stock objective reports, classified by the
|
||||
leaf's declared usage:
|
||||
|
||||
| layout | crinkliness fails | on spaces that want no daylight |
|
||||
|---|---|---|
|
||||
| harbor-house, 3 constructed seeds | 67 | 41 (61%) |
|
||||
| maple-court, 3 constructed seeds | 112 | 68 (61%) |
|
||||
| health-centre, 3 constructed seeds | 20 | 10 (50%) |
|
||||
| harbor-house `generated.dom` (evolved) | 5 | 2 (40%) |
|
||||
| maple-court `generated.dom` (evolved) | 67 | 43 (64%) |
|
||||
| **overall** | **271** | **164 (61%)** |
|
||||
|
||||
**61% of the crinkliness failures the objective reports are not defects**, and
|
||||
it holds on evolved artefacts, not just seeds. Under
|
||||
`value *= 0.5 ** len(failures)` every one of them halves the fitness of a design
|
||||
that has done nothing wrong — a design is punished for putting the store in the
|
||||
middle of the plan, which is what a competent architect does. That is a
|
||||
correctness fault, and it is not contingent on any A/B.
|
||||
|
||||
**Correction 2 — `usage:` is the wrong key, and `usage_daylight` as written in
|
||||
§38.8 mis-keys it.** §39.7 established `usage:` as an *access-requirement*
|
||||
class. "Needs no special access" and "needs no window" are different questions,
|
||||
and the corpus separates them plainly:
|
||||
|
||||
- `usage: none` is **Waiting Room**, **Reception**, **Reception Office**,
|
||||
**Entrance Foyer** — a waiting room is a space people sit in for long
|
||||
stretches and plainly wants daylight, yet `DAYLIGHT_USAGES` exempts it;
|
||||
- `usage: bedroom` is where the GP consulting rooms, counselling rooms and
|
||||
staff offices live — all of which do want daylight, so that half is right,
|
||||
but it is right by luck of how the access axis happened to fall.
|
||||
|
||||
The audit is robust to the error (reclassifying `none` as wanting daylight
|
||||
moves the headline 61% → 57%), so the finding stands; the *design* does not.
|
||||
Daylight needs its own declared axis, per space, decided by the programme
|
||||
author exactly as `usage:` was — not derived from a different question that
|
||||
happens to correlate.
|
||||
|
||||
`usage_daylight` therefore stays default off and is **not** the shipping fix.
|
||||
It is retained as the mechanism — the compact-side clip is the right shape for
|
||||
the factor — pending a `daylight:` attribute to key it on.
|
||||
|
||||
|
||||
## 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
|
||||
|
|
@ -5422,3 +5624,277 @@ missed, because consulting rooms and storage stood in for them. With that
|
|||
substitution gone, `homemaker-py-2v1` (connectivity priced at ×0.5 against a ×6
|
||||
circulation→habitable value gap) is the remaining half of the same problem —
|
||||
and now measurable, because the fails it should be preventing actually fire.
|
||||
|
||||
### 39.8 `homemaker-py-2v1` connectivity weighting — MEASURED NULL, premise retracted
|
||||
|
||||
§38.2 concluded that the objective is net-positive on severing a level's
|
||||
circulation: merging a corridor into a habitable sibling gains
|
||||
`value_inside / value_circulation` = ×6, while `level N not connected` costs
|
||||
only ×0.5, so break-even needs `0.5^w < 50/300`, i.e. w > 2.58 — "severing must
|
||||
cost at least 3 fails and costs 1". **The arithmetic is right and the premise is
|
||||
wrong.**
|
||||
|
||||
**What shipped anyway** (EXPERIMENTAL, default off, byte-identical):
|
||||
`fitness.connectivity_weight_for(value_inside, value_circulation)` returns the
|
||||
smallest weight making severing net-negative — 3.0 at the defaults, *derived*
|
||||
from the rates rather than hard-coded, so it tracks them if either is retuned.
|
||||
`conf["connectivity_weight"]` takes `1.0` (default, the flat rule), `"auto"`, or
|
||||
an explicit number, and counts each connectivity failure as w failures in the
|
||||
`0.5^n` penalty. `is_connectivity_fail` identifies the two strings.
|
||||
|
||||
**The measurement.** At `auto` (=3) the §38.2 deletion test does not move at
|
||||
all: 5/25 deletions rewarded either way, median ×0.26 vs ×0.27. The reason is
|
||||
immediate once looked for — **the connectivity fail count is unchanged in every
|
||||
rewarded deletion**:
|
||||
|
||||
| seed | deleted | | score | fails | connectivity fails |
|
||||
|---|---|---|---|---|---|
|
||||
| 0 | `rlrrr` `O` | buried | ×238 | 115 → 107 | 5 → **5** |
|
||||
| 0 | `rrrl` `O` | lit | ×346 | 115 → 106 | 5 → **5** |
|
||||
| 1 | `lrlll` `cr1` | lit | ×257 | 107 → 99 | 3 → **3** |
|
||||
| 2 | `rlrrl` `O` | buried | ×127 | 78 → 71 | 3 → **3** |
|
||||
|
||||
Weighting a failure that never fires changes nothing. And when the deletion
|
||||
*does* break connectivity, the objective already punishes it — every such case
|
||||
across harbor-house and maple-court, 4 seeds each:
|
||||
|
||||
| programme | deletions sampled | break connectivity | of those, rewarded |
|
||||
|---|---|---|---|
|
||||
| harbor-house | 32 | 2 | **0** (×0.00, ×0.01) |
|
||||
| maple-court | 32 | 5 | **0** (×0.58 … ×0.07) |
|
||||
|
||||
So severing is already net-negative: it costs 1–2 connectivity failures *plus*
|
||||
the cascade that follows them (inaccessible space, broken adjacency), and that
|
||||
total already outweighs the ×6 value gain. The flat rule was never the problem.
|
||||
|
||||
**Where §38.2 went wrong.** The ×4.06 "well-daylit circulation leaf" that
|
||||
motivated the whole bead was a deletion that did **not** change the
|
||||
connectivity fail count. It was rewarded for removing the leaf's own quality
|
||||
failures — §38.1's zero-value finding — and was misread as evidence for a
|
||||
pricing mechanism. Two lessons, both cheap to state and expensive to learn: a
|
||||
plausible closed-form arithmetic is not a measurement, and when a fix produces
|
||||
*exactly* no effect, suspect the premise before the implementation.
|
||||
|
||||
**What is still true from §38.** §38.1 (buried leaves score a hard quality of
|
||||
zero and contribute no value) and §38.3 (the frontage budget) are direct
|
||||
measurements and stand. §39.7's finding — that the connectivity model was ~4×
|
||||
too permissive — also stands and is the more useful lever: it made the fails
|
||||
*fire*, where this bead would only have made them *cost more*.
|
||||
|
||||
**Verdict: NULL.** The flag stays default off with this write-up, per house
|
||||
style for a measured-null lever. `homemaker-py-2v1` is closed. Why
|
||||
`level 0/1 not connected` survive in the best-known layout is re-opened as a
|
||||
question (`homemaker-py-yql`) — the evidence now says it is a reachability problem
|
||||
(connected topologies are hard to construct and hold onto), not an incentive
|
||||
one. It is newly measurable: §39.7 made the fails fire on constructed seeds
|
||||
instead of being hidden by routes through store cupboards.
|
||||
|
||||
### 39.9 Why `level N not connected` persists: the resize destroys it (`homemaker-py-yql`)
|
||||
|
||||
§39.8 closed `2v1` NULL — severing circulation is already punished, so the fail
|
||||
is not something the search is paid to create. That left the real question: is a
|
||||
connected layout **rarely constructed**, or **constructed and then lost**?
|
||||
|
||||
**Answer: constructed, then lost — at construction time, in the resize.**
|
||||
|
||||
`level N not connected` fires from `graph.connected_circulation`, which keeps
|
||||
only the generic `C`/`S` leaves and asks whether *they* form one component.
|
||||
Measured over 20 constructed seeds per programme
|
||||
(`experiments/diag_connectivity_yql.py`):
|
||||
|
||||
| programme | levels connected | seeds fully connected |
|
||||
|---|---|---|
|
||||
| harbor-house | 21/40 (52%) | **1/20** |
|
||||
| health-centre | 1/20 (5%) | **1/20** |
|
||||
| maple-court | 39/60 (65%) | **0/20** |
|
||||
|
||||
Then the decisive control — the same seeds with `proportion_aware=False`, i.e.
|
||||
skipping `_size_divisions_from_targets`:
|
||||
|
||||
| programme | with resize | **without resize** |
|
||||
|---|---|---|
|
||||
| harbor-house | 52% | **100%** |
|
||||
| health-centre | 5% | **100%** |
|
||||
| maple-court | 65% | **100%** |
|
||||
|
||||
`_assign_adjacency_aware` picks circulation as a **connected** dominating set —
|
||||
and it succeeds every time. The resize then moves every wall to hit the
|
||||
programme's area targets, and the shared boundaries the dominating set relied on
|
||||
shrink or vanish. On health-centre, **41 of 49 circulation-to-circulation edges
|
||||
are destroyed by the resize**, and surviving shared walls are squeezed to
|
||||
0.54–1.11 m against a 1.2 m `door_width`, so they stop counting as edges at all.
|
||||
|
||||
This is exactly the failure mode §37.7 recorded for CP-SAT room assignment —
|
||||
"resizing can shrink a shared-wall segment below the door-width adjacency
|
||||
threshold, silently invalidating an edge the exact solve relied on" — but nobody
|
||||
had looked for it in **circulation connectivity**, where it costs 35–95 points.
|
||||
|
||||
**§39.7 cost check: zero.** The same measurement under prefix-inferred vs
|
||||
declared usages is identical (52/5/65% both ways). `has_circulation` never trims
|
||||
`C`–`C` edges, so the usage change could not and did not make connectivity
|
||||
harder to achieve.
|
||||
|
||||
#### The obvious repair is a net loss — measured
|
||||
|
||||
`operators.repair_circulation_settled` applies §37.7's own alternating-
|
||||
minimisation fix: after the geometry settles, re-connect circulation by retyping
|
||||
the cheapest bridging leaves to `C` (preferring generic outside, then
|
||||
unassigned, crossing a required room last — `mutate_bridge_circulation`'s cost
|
||||
model). It works, completely:
|
||||
|
||||
| programme | levels connected, repair OFF | repair ON |
|
||||
|---|---|---|
|
||||
| harbor-house | 52% (1/20 seeds full) | **100% (20/20)** |
|
||||
| health-centre | 5% (1/20) | **100% (20/20)** |
|
||||
| maple-court | 65% (0/20) | **100% (20/20)** |
|
||||
|
||||
And it is still the wrong trade. Mean fails per constructed seed, 12 seeds:
|
||||
|
||||
| programme | total | hard | connectivity | missing-room |
|
||||
|---|---|---|---|---|
|
||||
| harbor-house | 96.6 → **108.9** | 46.0 → 56.2 | 3.7 → 2.0 | 14.2 → **19.2** |
|
||||
| health-centre | 62.8 → **84.2** | 24.2 → 46.8 | 2.9 → 2.2 | 2.0 → **10.5** |
|
||||
| maple-court | 141.8 → **156.6** | 57.4 → 69.2 | 4.7 → 3.5 | 14.8 → **19.8** |
|
||||
|
||||
Connectivity failures fall by 0.8–1.7; missing-room failures rise by 5.0–8.5,
|
||||
because every leaf retyped to `C` displaces a required room and each displacement
|
||||
costs a 3–5 fail cascade (§38.5). **Robbing Peter to pay Paul.** Kept default
|
||||
off with this write-up, per house style for a measured-null lever.
|
||||
|
||||
**The lever is upstream, not downstream.** The repair is treating a symptom: the
|
||||
connection should never be destroyed in the first place. The named next move is
|
||||
to *preserve* it during the resize — constrain `_size_divisions_from_targets` so
|
||||
a shared boundary between two circulation leaves cannot fall below `door_width`
|
||||
— rather than to rebuild it afterwards at the cost of the programme. That is a
|
||||
constraint on an existing solve rather than a new repair pass. Filed as
|
||||
`homemaker-py-3z0`. Worth noting while there: `solver.py` already carries
|
||||
`min_width_generic` (default 1.2) to stop generic leaves collapsing to slivers
|
||||
— the same idea applied to a leaf's WIDTH rather than to a shared BOUNDARY
|
||||
between two specific leaves, so the new constraint may belong beside it.
|
||||
|
||||
### 39.10 Preserving constructed connectivity through the resize (`homemaker-py-3z0`) — NULL, and it reframes §39.9
|
||||
|
||||
§39.9 established that `_size_divisions_from_targets` destroys the connected
|
||||
circulation the seeder builds, and named the upstream fix: keep the connection
|
||||
*during* the resize rather than rebuilding it after. Built and measured. **It
|
||||
does not help, and the reason matters more than the lever.**
|
||||
|
||||
**Both halves of the re-cut do damage, in different proportions per programme.**
|
||||
The resize changes each node's ratio *and* re-picks its rotation. Freezing the
|
||||
rotations and letting only the ratios move (12 seeds, % of levels connected):
|
||||
|
||||
| programme | no resize | ratio only | full resize |
|
||||
|---|---|---|---|
|
||||
| harbor-house | 100% | 71% | 50% |
|
||||
| health-centre | 100% | **8%** | 8% |
|
||||
| maple-court | 100% | 92% | 67% |
|
||||
|
||||
health-centre is destroyed entirely by the ratio; maple-court mostly by the
|
||||
rotation. So any fix has to be able to give back either.
|
||||
|
||||
**`operators._size_divisions_preserving_circulation`** snapshots every cut,
|
||||
resizes, then reverts the cuts on the tree path between each
|
||||
circulation-to-circulation pair the resize broke. It keeps the programme
|
||||
completely intact — no retyping, no displacement, only geometry given back —
|
||||
and it works on connectivity:
|
||||
|
||||
| programme | connected, OFF | ON | fails OFF → ON | hard OFF → ON |
|
||||
|---|---|---|---|---|
|
||||
| harbor-house | 50% | **92%** | 96.6 → **141.5** | 46.0 → 65.1 |
|
||||
| health-centre | 8% | 17% | 62.8 → **76.9** | 24.2 → 25.8 |
|
||||
| maple-court | 67% | **97%** | 141.8 → **175.8** | 57.4 → 70.9 |
|
||||
|
||||
(A first attempt reverted greedily — whichever single cut most reduced the
|
||||
component count — and barely moved: it stalls on the plateau where no *one*
|
||||
revert helps though two would. Targeting the specific broken pairs is what
|
||||
made connectivity work.)
|
||||
|
||||
Reverting a cut gives back that subtree's area accuracy, and size failures
|
||||
roughly double on harbor-house (5.2 → 11.4). The obvious defence is that these
|
||||
are raw constructed seeds and the inner loop has not run yet — the resize exists
|
||||
to *warm-start* the ratio optimiser, so a worse warm start might cost nothing
|
||||
once it converges. **Tested, and the defence fails.** Full search, harbor-house,
|
||||
12 000 evals, seed 1, both arms:
|
||||
|
||||
| | fails | hard | soft | connectivity |
|
||||
|---|---|---|---|---|
|
||||
| `preserve_circulation` OFF | **43** | **9** | 34 | **3** |
|
||||
| `preserve_circulation` ON | 65 | 26 | 39 | 4 |
|
||||
|
||||
Worse on every axis — including connectivity itself, the thing it was built to
|
||||
fix.
|
||||
|
||||
**The reframing.** §39.9's finding stands as a fact (the resize really does
|
||||
destroy 41 of 49 circulation edges) but is **not actionable, because
|
||||
construction-time connectivity is not what determines final connectivity**. The
|
||||
search reaches 43 fails with 3 connectivity fails starting from a 50%-connected
|
||||
seed; forcing the seed to 92% connected yields 65 fails and 4 connectivity
|
||||
fails. The seeder's circulation topology is not the bottleneck — the search
|
||||
discards and rebuilds it either way, and constraining the seed only spends area
|
||||
quality the search then cannot recover.
|
||||
|
||||
That also retires the framing this whole thread inherited from §38: connectivity
|
||||
was never a construction problem *or* an incentive problem (§39.8). Both flags
|
||||
(`repair_circulation`, `preserve_circulation`) stay default off with these
|
||||
numbers recorded. **Do not revisit either without a new formulation** — the same
|
||||
standing this document gives `bubble.py`.
|
||||
|
||||
### 39.11 The frontage bound, computed correctly (`homemaker-py-tdp`) — shipped as a pre-flight check
|
||||
|
||||
§38.3 derived a real constraint — every interior leaf needs
|
||||
`L >= A/(1.6202·h)` of daylit wall — and then applied it to the wrong quantity.
|
||||
It measured what a **fully built plot** would need. These programmes do not ask
|
||||
for a fully built plot.
|
||||
|
||||
Recomputed against the area each programme actually demands:
|
||||
|
||||
| programme | demanded/storey | % of plot | frontage needed | supplied | gap | courtyard to close | spare plot | |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| harbor-house | 418 m² | 60% | 86 m | 53 m | +33 m | 49 m² | 277 m² | **OK** |
|
||||
| maple-court | 338 m² | 44% | 70 m | 55 m | +15 m | 22 m² | 424 m² | **OK** |
|
||||
| programme-house | 38 m² | 72% | 8 m | 22 m | −14 m | none | — | **OK** |
|
||||
| health-centre | 240 m² | **131%** | 49 m | 41 m | +8 m | 12 m² | **−57 m²** | **DOES NOT FIT** |
|
||||
|
||||
Plot area and frontage are measured through `geometry`, not from the raw
|
||||
`init.dom` corners, so they carry the `wall_outer` inset and the plot rotation —
|
||||
these are the metres and the square metres the leaves actually get. "Daylit"
|
||||
means what `Fitness.area_outside` means by it: an external boundary counts
|
||||
unless its perimeter type is `private` or `fortified`.
|
||||
|
||||
So harbor-house and maple-court are **not** frontage-infeasible; they need a
|
||||
courtyard of 49 m² and 22 m² respectively, against 277 m² and 424 m² of spare
|
||||
plot. §38.3's "2.7× short" overstated it by comparing against a building nobody
|
||||
asked for.
|
||||
|
||||
**The one genuinely infeasible programme is health-centre, and not for daylight
|
||||
reasons: it demands 240 m² of floor on a 183 m² plot.** That shows up
|
||||
unmistakably in the geometry — every room comes out at **0.60×** its declared
|
||||
target, 100% of them undersized, uniformly, no matter what the search does.
|
||||
Contrast harbor-house and maple-court, where the seeder hits targets almost
|
||||
exactly (median area / (target × share) = **1.01×**).
|
||||
|
||||
*(An intermediate measurement suggested rooms were systematically inflated to
|
||||
1.26–1.65× target. That was an artefact of not dividing by a shared leaf's
|
||||
multiplicity — a leaf covering k rooms is legitimately k× a single target.
|
||||
Corrected above; the seeder's sizing is accurate where the plot allows.)*
|
||||
|
||||
**Shipped: `evolve._preflight`.** Both checks now run at startup and print a
|
||||
warning before a multi-hour run bottoms out against something no amount of
|
||||
searching can fix:
|
||||
|
||||
```
|
||||
WARNING: programme demands 240 m2 per storey on a 183 m2 plot (131%). Every room
|
||||
will be squeezed below its target however long the search runs.
|
||||
WARNING: 418 m2 per storey needs ~86 m of daylit wall; the plot's non-private
|
||||
perimeter gives 53 m. Roughly 49 m2 of courtyard closes the gap.
|
||||
```
|
||||
|
||||
Advisory only — it never blocks a run, since an author may be deliberately
|
||||
exploring an over-tight brief. Silent on programme-house. The same numbers are
|
||||
available in full from `experiments/diag_exposure_frontage.py frontage`.
|
||||
|
||||
**What this means for §38.** The frontage bound survives as a *diagnostic* and
|
||||
is now correctly calibrated, but it does **not** say the corpus is
|
||||
unsatisfiable. Of the four programmes, three fit their plots and one does not —
|
||||
and that one fails a much cruder test than daylight. §38.3's claim that the
|
||||
plateau programmes are "frontage-infeasible as specified" is withdrawn.
|
||||
|
|
|
|||
600
examples/harbor-house/evolved-3M-nols-3.dom
Normal file
600
examples/harbor-house/evolved-3M-nols-3.dom
Normal file
|
|
@ -0,0 +1,600 @@
|
|||
node:
|
||||
- - 0.0
|
||||
- 0.0
|
||||
- - 25.0
|
||||
- 2.0
|
||||
- - 23.0
|
||||
- 31.0
|
||||
- - 0.0
|
||||
- 31.0
|
||||
perimeter:
|
||||
a: private
|
||||
b: private
|
||||
c: null
|
||||
d: null
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.5453707551767608
|
||||
- 0.5453707551767608
|
||||
height: 3.0
|
||||
elevation: 0.0
|
||||
wall_inner: 0.08
|
||||
wall_outer: 0.25
|
||||
l:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.5522887920141253
|
||||
- 0.5522887920141253
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.5422240475759911
|
||||
- 0.5422240475759911
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.5884031332206187
|
||||
- 0.5884031332206187
|
||||
l:
|
||||
type: da1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.4351188444584838
|
||||
- 0.4351188444584838
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.5
|
||||
- 0.5
|
||||
l:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
type: st1
|
||||
rotation: 3
|
||||
r:
|
||||
type: k1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.5858843654327277
|
||||
- 0.5858843654327277
|
||||
l:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.6692443578136734
|
||||
- 0.6692443578136734
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.7222802898935807
|
||||
- 0.7222802898935807
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5791570312500001
|
||||
- 0.5791570312500001
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.5
|
||||
- 0.5
|
||||
l:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
type: t
|
||||
rotation: 0
|
||||
r:
|
||||
type: cr1
|
||||
rotation: 0
|
||||
r:
|
||||
type: cr1
|
||||
rotation: 0
|
||||
r:
|
||||
type: O
|
||||
rotation: 1
|
||||
r:
|
||||
type: ws1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.6234470574437853
|
||||
- 0.6234470574437853
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5902842616284559
|
||||
- 0.5902842616284559
|
||||
l:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.7385208215717542
|
||||
- 0.7385208215717542
|
||||
l:
|
||||
type: n
|
||||
rotation: 0
|
||||
r:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
type: n
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.7824086560875603
|
||||
- 0.7824086560875603
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.7215682159962162
|
||||
- 0.7215682159962162
|
||||
l:
|
||||
type: r
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.5936359570312502
|
||||
- 0.5936359570312502
|
||||
l:
|
||||
type: st1
|
||||
rotation: 0
|
||||
r:
|
||||
type: cr1
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 1
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.6570350194962238
|
||||
- 0.6570350194962238
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.38409579126711796
|
||||
- 0.38409579126711796
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.7722513353285644
|
||||
- 0.7722513353285644
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.629026904149584
|
||||
- 0.629026904149584
|
||||
l:
|
||||
type: m
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 3
|
||||
r:
|
||||
type: ef1
|
||||
rotation: 3
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.5923752795527412
|
||||
- 0.5923752795527412
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.8429374722623478
|
||||
- 0.8429374722623478
|
||||
l:
|
||||
type: n
|
||||
rotation: 0
|
||||
r:
|
||||
type: la1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.6090773265385678
|
||||
- 0.6090773265385678
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.9561064910184571
|
||||
- 0.9561064910184571
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.6385206269531251
|
||||
- 0.6385206269531251
|
||||
l:
|
||||
type: st2
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 3
|
||||
r:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
type: O
|
||||
rotation: 1
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.646127856061397
|
||||
- 0.646127856061397
|
||||
l:
|
||||
rotation: 2
|
||||
division:
|
||||
- 0.23548715480909932
|
||||
- 0.23548715480909932
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.6084768559570314
|
||||
- 0.6084768559570314
|
||||
l:
|
||||
type: m
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.9564169654832019
|
||||
- 0.9564169654832019
|
||||
l:
|
||||
type: t
|
||||
rotation: 0
|
||||
r:
|
||||
type: st2
|
||||
rotation: 0
|
||||
r:
|
||||
type: O
|
||||
rotation: 3
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.8784173200136786
|
||||
- 0.8784173200136786
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.4161469597784059
|
||||
- 0.4161469597784059
|
||||
l:
|
||||
type: m
|
||||
rotation: 1
|
||||
r:
|
||||
type: C
|
||||
rotation: 1
|
||||
r:
|
||||
type: O
|
||||
rotation: 1
|
||||
above:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5326764625682993
|
||||
- 0.5326764625682993
|
||||
height: 3.0
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5536579163160164
|
||||
- 0.5536579163160164
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.4853471066909839
|
||||
- 0.4853471066909839
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.7987297048921393
|
||||
- 0.7987297048921393
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.7297117619482362
|
||||
- 0.7297117619482362
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.7579431477526928
|
||||
- 0.7579431477526928
|
||||
l:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.6236887773559573
|
||||
- 0.6236887773559573
|
||||
l:
|
||||
type: t
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.5650312500000001
|
||||
- 0.5650312500000001
|
||||
l:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5
|
||||
- 0.5
|
||||
l:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.5
|
||||
- 0.5
|
||||
l:
|
||||
type: r
|
||||
rotation: 0
|
||||
r:
|
||||
type: st1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.551578125
|
||||
- 0.551578125
|
||||
l:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
type: r
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.43356671101589384
|
||||
- 0.43356671101589384
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.13133577297899704
|
||||
- 0.13133577297899704
|
||||
l:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.5
|
||||
- 0.5
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5797285677939966
|
||||
- 0.5797285677939966
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5
|
||||
- 0.5
|
||||
l:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
type: t
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.551578125
|
||||
- 0.551578125
|
||||
l:
|
||||
type: st1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.670845733692627
|
||||
- 0.670845733692627
|
||||
l:
|
||||
type: li1
|
||||
rotation: 0
|
||||
r:
|
||||
type: st1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.781888627381648
|
||||
- 0.781888627381648
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.38499157916891213
|
||||
- 0.38499157916891213
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.2012702951078606
|
||||
- 0.2012702951078606
|
||||
l:
|
||||
type: n
|
||||
rotation: 0
|
||||
r:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.4444444444444444
|
||||
- 0.4444444444444444
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.9730225242044375
|
||||
- 0.9730225242044375
|
||||
l:
|
||||
type: r
|
||||
rotation: 2
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.5004934210526315
|
||||
- 0.5004934210526315
|
||||
l:
|
||||
type: st2
|
||||
rotation: 0
|
||||
r:
|
||||
type: t
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5945754854305073
|
||||
- 0.5945754854305073
|
||||
l:
|
||||
type: r
|
||||
rotation: 0
|
||||
r:
|
||||
type: r
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.4902664331159802
|
||||
- 0.4902664331159802
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.6499007975748095
|
||||
- 0.6499007975748095
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5193481864706366
|
||||
- 0.5193481864706366
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.26078324488789595
|
||||
- 0.26078324488789595
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5136069351999384
|
||||
- 0.5136069351999384
|
||||
l:
|
||||
type: ut1
|
||||
rotation: 3
|
||||
r:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
type: me1
|
||||
rotation: 3
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5
|
||||
- 0.5
|
||||
l:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
type: r
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.2873334582149999
|
||||
- 0.2873334582149999
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.375
|
||||
- 0.375
|
||||
l:
|
||||
type: n
|
||||
rotation: 0
|
||||
r:
|
||||
type: r
|
||||
rotation: 0
|
||||
r:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.4595786624530838
|
||||
- 0.4595786624530838
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.9210526315789473
|
||||
- 0.9210526315789473
|
||||
l:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.7294856269372924
|
||||
- 0.7294856269372924
|
||||
l:
|
||||
type: st1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.7039689912158207
|
||||
- 0.7039689912158207
|
||||
l:
|
||||
type: r
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5559478988311726
|
||||
- 0.5559478988311726
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.2012702951078606
|
||||
- 0.2012702951078606
|
||||
l:
|
||||
type: r
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5936359570312502
|
||||
- 0.5936359570312502
|
||||
l:
|
||||
type: C
|
||||
rotation: 3
|
||||
r:
|
||||
type: t
|
||||
rotation: 1
|
||||
r:
|
||||
type: O
|
||||
rotation: 2
|
||||
336
examples/harbor-house/evolved-3M.dom
Normal file
336
examples/harbor-house/evolved-3M.dom
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
node:
|
||||
- - 0.0
|
||||
- 0.0
|
||||
- - 25.0
|
||||
- 2.0
|
||||
- - 23.0
|
||||
- 31.0
|
||||
- - 0.0
|
||||
- 31.0
|
||||
perimeter:
|
||||
a: private
|
||||
b: private
|
||||
c: null
|
||||
d: null
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.4888900105484264
|
||||
- 0.4888900105484264
|
||||
height: 3.0
|
||||
elevation: 0.0
|
||||
wall_inner: 0.08
|
||||
wall_outer: 0.25
|
||||
l:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.5938247521970288
|
||||
- 0.5938247521970288
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5472887522186323
|
||||
- 0.5472887522186323
|
||||
l:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.7482565199547276
|
||||
- 0.7482565199547276
|
||||
l:
|
||||
rotation: 2
|
||||
division:
|
||||
- 0.7634740326506355
|
||||
- 0.7634740326506355
|
||||
l:
|
||||
type: da1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.5
|
||||
- 0.5
|
||||
l:
|
||||
type: st1
|
||||
rotation: 0
|
||||
r:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
type: k1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.48227518337297415
|
||||
- 0.48227518337297415
|
||||
l:
|
||||
type: ws1
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 2
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.2876274429075646
|
||||
- 0.2876274429075646
|
||||
l:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.4977615341676925
|
||||
- 0.4977615341676925
|
||||
l:
|
||||
type: C
|
||||
rotation: 3
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.5260108852851195
|
||||
- 0.5260108852851195
|
||||
l:
|
||||
type: ef1
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.6397932334327496
|
||||
- 0.6397932334327496
|
||||
l:
|
||||
type: t
|
||||
share: 3
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.554484404426573
|
||||
- 0.554484404426573
|
||||
l:
|
||||
type: la1
|
||||
rotation: 0
|
||||
r:
|
||||
type: st1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.8502200490349778
|
||||
- 0.8502200490349778
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.8442580239222437
|
||||
- 0.8442580239222437
|
||||
l:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.3758880797422988
|
||||
- 0.3758880797422988
|
||||
l:
|
||||
type: n
|
||||
share: 3
|
||||
rotation: 0
|
||||
r:
|
||||
type: of
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.5154341184740678
|
||||
- 0.5154341184740678
|
||||
l:
|
||||
type: m
|
||||
share: 3
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.2616194760651602
|
||||
- 0.2616194760651602
|
||||
l:
|
||||
type: st1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 2
|
||||
division:
|
||||
- 0.5989008688013605
|
||||
- 0.5989008688013605
|
||||
l:
|
||||
type: me1
|
||||
rotation: 0
|
||||
r:
|
||||
type: O
|
||||
rotation: 0
|
||||
above:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5662620679201393
|
||||
- 0.5662620679201393
|
||||
height: 3.0
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5300569106355991
|
||||
- 0.5300569106355991
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.0863852684329759
|
||||
- 0.0863852684329759
|
||||
l:
|
||||
type: t
|
||||
share: 3
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.369644807975687
|
||||
- 0.369644807975687
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5
|
||||
- 0.5
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.5125
|
||||
- 0.5125
|
||||
l:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 1
|
||||
division:
|
||||
- 0.6910915096691898
|
||||
- 0.6910915096691898
|
||||
l:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
type: n
|
||||
share: 2
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.4566953630047795
|
||||
- 0.4566953630047795
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.16593799911395812
|
||||
- 0.16593799911395812
|
||||
l:
|
||||
type: ut1
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.525234375
|
||||
- 0.525234375
|
||||
l:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.2988974834269737
|
||||
- 0.2988974834269737
|
||||
l:
|
||||
type: r
|
||||
share: 3
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5
|
||||
- 0.5
|
||||
l:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
type: O
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5670341721910087
|
||||
- 0.5670341721910087
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5878540477469637
|
||||
- 0.5878540477469637
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.2988974834269737
|
||||
- 0.2988974834269737
|
||||
l:
|
||||
type: r
|
||||
share: 3
|
||||
rotation: 0
|
||||
r:
|
||||
type: st2
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.5
|
||||
- 0.5
|
||||
l:
|
||||
rotation: 3
|
||||
division:
|
||||
- 0.6580826806640626
|
||||
- 0.6580826806640626
|
||||
l:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.5
|
||||
- 0.5
|
||||
l:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
type: C
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.6164728327412322
|
||||
- 0.6164728327412322
|
||||
l:
|
||||
type: r
|
||||
rotation: 0
|
||||
r:
|
||||
rotation: 0
|
||||
division:
|
||||
- 0.6
|
||||
- 0.6
|
||||
l:
|
||||
type: r
|
||||
share: 3
|
||||
rotation: 0
|
||||
r:
|
||||
type: li1
|
||||
rotation: 0
|
||||
|
|
@ -16,6 +16,17 @@ buried or lit is what separates the two mechanisms:
|
|||
against a ×0.5 connectivity penalty (§38.2 refinement) — which no
|
||||
``crinkliness_mode`` can touch.
|
||||
|
||||
.. warning::
|
||||
|
||||
**The premise of this test is RETRACTED — see §38.2 and §38.8.** The ×6-vs-
|
||||
×0.5 arithmetic assumed severing the spine costs one connectivity fail;
|
||||
measured, the connectivity count is unchanged in every rewarded deletion, so
|
||||
this script is not measuring what its docstring says. It is kept because the
|
||||
per-mode buried/lit split is still a useful description of what each mode
|
||||
touches, but a mode does **not** pass or fail on these numbers. The A/B that
|
||||
decides `ssz` is ``experiments/ab_ssz_search.py`` (fixed-budget search,
|
||||
scored under the stock objective).
|
||||
|
||||
Usage::
|
||||
|
||||
python experiments/ab_crinkliness_mode_ssz.py
|
||||
|
|
@ -34,7 +45,8 @@ 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")
|
||||
MODES = ("urb", "floor", "compact_ok", "exempt_circulation",
|
||||
"usage_daylight")
|
||||
|
||||
|
||||
def make_fitness(progdir: str, mode: str) -> fitness.Fitness:
|
||||
|
|
@ -74,7 +86,12 @@ def unpinned_leaves(fit: fitness.Fitness, root: dom_mod.Node) -> list[tuple]:
|
|||
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"):
|
||||
# §39.4: generic structural types are EXACT `C`/`O`/`S`, never a
|
||||
# first-character prefix -- `cr1` is a programme room. This script
|
||||
# predates that rule and its original `type[:1].upper() in ("C","O")`
|
||||
# test swept programme rooms into the "unpinned" set, which is one
|
||||
# reason the §38.6 numbers do not reproduce.
|
||||
if not dom_mod.is_generic(leaf.type):
|
||||
continue
|
||||
out.append((li, leaf.id, leaf.type,
|
||||
fit.area_outside(leaf, graphs[li], {})))
|
||||
|
|
|
|||
145
experiments/ab_ssz_search.py
Normal file
145
experiments/ab_ssz_search.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
"""Fixed-budget search A/B for the crinkliness modes (`homemaker-py-ssz`).
|
||||
|
||||
DESIGN.md §38.6 A/B'd the modes against the §38.2 *deletion test*, which has
|
||||
since been retracted, and it used the pre-§39.4 `type[:1] in ("C","O")` prefix
|
||||
rule that mislabels programme rooms as circulation. So the modes have never
|
||||
been measured against what `ssz`'s acceptance criteria actually asks for: a
|
||||
fixed-budget search, hard/soft fail split, on harbor-house and maple-court.
|
||||
|
||||
**The scoring discipline is the point of this script.** `compact_ok`,
|
||||
`exempt_circulation` and `usage_daylight` all return 1.0 for leaves that stock
|
||||
scores below FAIL_THRESHOLD, so scoring an arm under its own objective deletes
|
||||
a fail category for free and every arm "wins". Two numbers are therefore
|
||||
reported per arm:
|
||||
|
||||
urb the arm's final layout re-scored under the STOCK objective. This is
|
||||
the comparable yardstick, and the one that answers "did optimising
|
||||
under this variant steer the search to a better building?"
|
||||
own the same layout under the arm's own objective. Lower than `urb` by
|
||||
construction for the permissive modes; it is reported only so the
|
||||
size of the definitional discount is visible, never as the result.
|
||||
|
||||
A mode passes on `urb`, not on `own`.
|
||||
|
||||
Usage::
|
||||
|
||||
python experiments/ab_ssz_search.py --budget 3000 --seeds 3
|
||||
python experiments/ab_ssz_search.py --modes urb usage_daylight --seeds 2
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import copy
|
||||
import csv
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from homemaker_layout import dom as dom_mod
|
||||
from homemaker_layout import driver, fitness
|
||||
|
||||
CORPUS = ["examples/harbor-house", "examples/maple-court"]
|
||||
MODES = ["urb", "floor", "compact_ok", "exempt_circulation", "usage_daylight"]
|
||||
|
||||
|
||||
def _with_mode(mode: str):
|
||||
"""Patch `fitness.load_config` so every evaluator built during the run --
|
||||
the driver's, the inner loop's, the seeder's -- sees `crinkliness_mode`.
|
||||
|
||||
`driver.search` has no parameter for it, and `driver._fitness_for` is
|
||||
lru_cached on its arguments, so the cache is cleared around the patch or a
|
||||
later arm would silently reuse the previous arm's evaluator.
|
||||
"""
|
||||
orig = fitness.load_config
|
||||
|
||||
def patched(directory, overrides=None):
|
||||
ov = dict(overrides or {})
|
||||
ov["crinkliness_mode"] = mode
|
||||
return orig(directory, overrides=ov)
|
||||
|
||||
return orig, patched
|
||||
|
||||
|
||||
def tiers(fails) -> tuple[int, int]:
|
||||
c = collections.Counter(fitness.classify_fail_tier(f) for f in fails)
|
||||
return c["hard"], c["soft"]
|
||||
|
||||
|
||||
def run_arm(progdir: str, seed: int, mode: str, budget: int,
|
||||
child_budget: int) -> dict:
|
||||
orig, patched = _with_mode(mode)
|
||||
fitness.load_config = patched
|
||||
driver._fitness_for.cache_clear()
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
res = driver.search(
|
||||
dom_mod.load(f"{progdir}/init.dom"), progdir,
|
||||
budget=budget, seed=seed, child_budget=child_budget, n_workers=1)
|
||||
root = copy.deepcopy(res.best.root)
|
||||
own_conf, own_cost = patched(progdir, overrides={"leaf_sharing": True,
|
||||
"collapse_insearch": True})
|
||||
_, own_fails = fitness.Fitness(own_conf, own_cost).score_with_fails(
|
||||
copy.deepcopy(root))
|
||||
finally:
|
||||
fitness.load_config = orig
|
||||
driver._fitness_for.cache_clear()
|
||||
|
||||
# the comparable yardstick: stock objective, same layout
|
||||
conf, cost = orig(progdir, overrides={"leaf_sharing": True,
|
||||
"collapse_insearch": True})
|
||||
_, urb_fails = fitness.Fitness(conf, cost).score_with_fails(copy.deepcopy(root))
|
||||
|
||||
uh, us = tiers(urb_fails)
|
||||
oh, os_ = tiers(own_fails)
|
||||
return dict(programme=Path(progdir).name, seed=seed, mode=mode,
|
||||
urb_hard=uh, urb_soft=us, urb_total=uh + us,
|
||||
own_hard=oh, own_soft=os_, own_total=oh + os_,
|
||||
elapsed_s=round(time.perf_counter() - t0, 1))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--budget", type=int, default=3000)
|
||||
ap.add_argument("--child-budget", type=int, default=80)
|
||||
ap.add_argument("--seeds", type=int, default=3)
|
||||
ap.add_argument("--modes", nargs="+", default=MODES)
|
||||
ap.add_argument("--corpus", nargs="+", default=CORPUS)
|
||||
ap.add_argument("--out", default="experiments/results/ab_ssz_search.csv")
|
||||
args = ap.parse_args()
|
||||
|
||||
rows = []
|
||||
out = Path(args.out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
for progdir in args.corpus:
|
||||
for mode in args.modes:
|
||||
for seed in range(args.seeds):
|
||||
r = run_arm(progdir, seed, mode, args.budget, args.child_budget)
|
||||
rows.append(r)
|
||||
print(f" {r['programme']:<14} {mode:<20} seed={seed} "
|
||||
f"urb {r['urb_hard']}h/{r['urb_soft']}s "
|
||||
f"(own {r['own_hard']}h/{r['own_soft']}s) "
|
||||
f"{r['elapsed_s']}s", flush=True)
|
||||
with out.open("w", newline="") as fh:
|
||||
w = csv.DictWriter(fh, fieldnames=list(rows[0]))
|
||||
w.writeheader()
|
||||
w.writerows(rows)
|
||||
|
||||
print(f"\n=== stock-objective (urb) fail counts, budget {args.budget} ===")
|
||||
print(f" {'programme':<14}{'mode':<22}{'hard':<14}{'soft':<14}total")
|
||||
print(" " + "-" * 70)
|
||||
for progdir in args.corpus:
|
||||
name = Path(progdir).name
|
||||
for mode in args.modes:
|
||||
sel = [r for r in rows if r["programme"] == name and r["mode"] == mode]
|
||||
if not sel:
|
||||
continue
|
||||
h = sum(r["urb_hard"] for r in sel) / len(sel)
|
||||
s = sum(r["urb_soft"] for r in sel) / len(sel)
|
||||
print(f" {name:<14}{mode:<22}{h:<14.1f}{s:<14.1f}{h + s:.1f}")
|
||||
print(f"\nwrote {out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
132
experiments/audit_crinkliness_truth.py
Normal file
132
experiments/audit_crinkliness_truth.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
"""Are the crinkliness failures the objective emits real defects? (`homemaker-py-ssz`)
|
||||
|
||||
Not an A/B. This asks a correctness question the search cannot answer: of the
|
||||
`crinkliness` failures the STOCK objective reports, how many are on a space
|
||||
that architecturally wants daylight at all?
|
||||
|
||||
A `crinkliness` fail says "this leaf has too little exposed wall for its area".
|
||||
For a bedroom or a living room that is a real defect. For a broom cupboard, a
|
||||
WC, a plant room, an internal corridor or a covered courtyard it is not -- those
|
||||
are ordinary buried architecture, and the fail is an artefact of applying one
|
||||
daylight requirement to every space regardless of use (DESIGN.md §38.8).
|
||||
|
||||
Every fail is classified by the leaf's DECLARED `usage:` (§39.7), so nothing
|
||||
here rests on how a code is spelled.
|
||||
|
||||
Usage::
|
||||
|
||||
python experiments/audit_crinkliness_truth.py
|
||||
python experiments/audit_crinkliness_truth.py --seeds 5
|
||||
python experiments/audit_crinkliness_truth.py --dom examples/harbor-house/generated.dom
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import copy
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
CORPUS = ["examples/harbor-house", "examples/maple-court", "examples/health-centre"]
|
||||
|
||||
|
||||
def stock_fitness(progdir: str) -> fitness.Fitness:
|
||||
"""Stock objective -- `crinkliness_mode` left at its "urb" default."""
|
||||
ov = dict(driver._overrides_for(
|
||||
leaf_sharing=True, superpose=False, max_share=None, conn_grade=False,
|
||||
collapse_insearch=True, multi_use=False) or {})
|
||||
conf, cost = fitness.load_config(progdir, overrides=ov)
|
||||
return fitness.Fitness(conf, cost)
|
||||
|
||||
|
||||
def constructed(progdir: str, s: 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(s),
|
||||
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 audit(fit: fitness.Fitness, root: dom_mod.Node) -> collections.Counter:
|
||||
"""usage -> count, over the leaves that emit a stock `crinkliness` fail."""
|
||||
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: collections.Counter = collections.Counter()
|
||||
for li, lvl in enumerate(dom_mod.levels(tree)):
|
||||
groups = geometry.boundary_groups(lvl)
|
||||
for leaf in lvl.leaves():
|
||||
if dom_mod.is_outside(leaf) and not dom_mod.is_covered(leaf):
|
||||
continue
|
||||
if fit.quality_uncrinkliness(leaf, graphs[li], groups) >= fitness.FAIL_THRESHOLD:
|
||||
continue # not a failure
|
||||
out[fit.usage_of(leaf) or f"<generic {leaf.type}>"] += 1
|
||||
return out
|
||||
|
||||
|
||||
def report(label: str, tally: collections.Counter) -> tuple[int, int]:
|
||||
total = sum(tally.values())
|
||||
real = sum(n for u, n in tally.items() if u in programme.DAYLIGHT_USAGES)
|
||||
print(f"=== {label}: {total} crinkliness fails")
|
||||
if not total:
|
||||
print(" none\n")
|
||||
return 0, 0
|
||||
for usage, n in tally.most_common():
|
||||
verdict = ("REAL DEFECT" if usage in programme.DAYLIGHT_USAGES
|
||||
else "not a defect -- no daylight wanted")
|
||||
print(f" {usage:<18}{n:>4} {verdict}")
|
||||
print(f" -> {total - real}/{total} ({100 * (total - real) / total:.0f}%) "
|
||||
f"are reported against spaces that do not want daylight\n")
|
||||
return real, total
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--seeds", type=int, default=3)
|
||||
ap.add_argument("--corpus", nargs="+", default=CORPUS)
|
||||
ap.add_argument("--dom", nargs="*", default=[],
|
||||
help="also audit these evolved .dom files (programme dir inferred)")
|
||||
args = ap.parse_args()
|
||||
|
||||
print("Stock objective. Every fail classified by the leaf's declared usage.\n")
|
||||
grand_real = grand_total = 0
|
||||
|
||||
for progdir in args.corpus:
|
||||
fit = stock_fitness(progdir)
|
||||
tally: collections.Counter = collections.Counter()
|
||||
for s in range(args.seeds):
|
||||
tally += audit(fit, constructed(progdir, s))
|
||||
r, t = report(f"{Path(progdir).name} ({args.seeds} constructed seeds)", tally)
|
||||
grand_real += r
|
||||
grand_total += t
|
||||
|
||||
for dom_path in args.dom:
|
||||
progdir = str(Path(dom_path).parent)
|
||||
fit = stock_fitness(progdir)
|
||||
r, t = report(f"{dom_path} (evolved)", audit(fit, dom_mod.load(dom_path)))
|
||||
grand_real += r
|
||||
grand_total += t
|
||||
|
||||
if grand_total:
|
||||
print(f"OVERALL: {grand_total - grand_real}/{grand_total} "
|
||||
f"({100 * (grand_total - grand_real) / grand_total:.0f}%) of the "
|
||||
f"crinkliness failures the objective reports are not defects.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
167
experiments/diag_connectivity_yql.py
Normal file
167
experiments/diag_connectivity_yql.py
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
"""Why does `level N not connected` persist? (`homemaker-py-yql`, DESIGN.md §39.9)
|
||||
|
||||
`homemaker-py-2v1` closed NULL: severing a level's circulation is already
|
||||
punished, so the fail is not something the search is paid to create. This asks
|
||||
the follow-on question — is a connected layout **rarely constructed**, or
|
||||
**constructed and then lost**?
|
||||
|
||||
`level N not connected` fires from `graph.connected_circulation`, which keeps
|
||||
only the generic circulation leaves (`C`/`S`) and asks whether *they* form one
|
||||
connected component. It runs on `graph_circ`, i.e. AFTER
|
||||
`graph.has_circulation` has trimmed edges, so §39.7's usage change can in
|
||||
principle reach it — report (b) measures whether it did.
|
||||
|
||||
Three reports:
|
||||
|
||||
construct what fraction of constructed seeds start connected, per level
|
||||
cost the same, prefix-inferred usages vs declared (the §39.7 cost)
|
||||
survive from a CONNECTED layout, how often does one mutation break
|
||||
connectivity, and would the outer comparator keep the mutant
|
||||
|
||||
Usage::
|
||||
|
||||
python experiments/diag_connectivity_yql.py construct
|
||||
python experiments/diag_connectivity_yql.py cost
|
||||
python experiments/diag_connectivity_yql.py survive --seeds 40
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
CORPUS = ["examples/harbor-house", "examples/health-centre", "examples/maple-court"]
|
||||
LEGACY_PREFIX = {"b": "bedroom", "t": "toilet", "l": "living", "k": "kitchen"}
|
||||
|
||||
|
||||
def make_fitness(progdir: str) -> fitness.Fitness:
|
||||
overrides = driver._overrides_for(
|
||||
leaf_sharing=True, superpose=False, max_share=None, conn_grade=False,
|
||||
collapse_insearch=True, multi_use=False)
|
||||
conf, cost = fitness.load_config(progdir, overrides=dict(overrides or {}))
|
||||
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 connectivity(root: dom_mod.Node, usages: dict[str, str]) -> tuple[int, int]:
|
||||
"""``(levels_connected, levels_total)`` for one tree.
|
||||
|
||||
Mirrors the scorer: build the circ graphs, then ask
|
||||
``connected_circulation`` per level on a copy, exactly as
|
||||
``process_storey`` does.
|
||||
"""
|
||||
tree = copy.deepcopy(root)
|
||||
geometry.clear_cache()
|
||||
dom_mod.canonicalize_shares(tree)
|
||||
_, circ = graph_mod.build_graphs_with_circ(tree, 1.2, lambda _f: None, usages)
|
||||
connected = sum(1 for gc in circ
|
||||
if graph_mod.connected_circulation(gc.copy()))
|
||||
return connected, len(circ)
|
||||
|
||||
|
||||
def report_construct(seeds: int) -> None:
|
||||
print(f"how often does a CONSTRUCTED seed start connected? ({seeds} seeds)\n")
|
||||
print(f" {'programme':<18}{'levels connected':<20}{'seeds fully connected'}")
|
||||
print(" " + "-" * 62)
|
||||
for progdir in CORPUS:
|
||||
fit = make_fitness(progdir)
|
||||
usages = fit.usages()
|
||||
ok = tot = full = 0
|
||||
for s in range(seeds):
|
||||
c, n = connectivity(constructed_seed(progdir, s), usages)
|
||||
ok += c
|
||||
tot += n
|
||||
full += (c == n)
|
||||
print(f" {Path(progdir).name:<18}{f'{ok}/{tot} ({100*ok/max(tot,1):.0f}%)':<20}"
|
||||
f"{full}/{seeds}")
|
||||
|
||||
|
||||
def report_cost(seeds: int) -> None:
|
||||
"""Did §39.7's usage change make level connectivity harder to achieve?"""
|
||||
print("§39.7 cost check — prefix-inferred usages vs declared "
|
||||
f"({seeds} seeds)\n")
|
||||
print(f" {'programme':<18}{'prefix-inferred':<20}{'declared':<20}delta")
|
||||
print(" " + "-" * 68)
|
||||
for progdir in CORPUS:
|
||||
reqs = programme.load_programme_dir(progdir)
|
||||
declared = {c: r.usage for c, r in reqs.items()}
|
||||
legacy = {c: LEGACY_PREFIX.get(c[:1].lower(), "none") for c in reqs}
|
||||
res = {}
|
||||
for label, usages in (("legacy", legacy), ("declared", declared)):
|
||||
ok = tot = 0
|
||||
for s in range(seeds):
|
||||
c, n = connectivity(constructed_seed(progdir, s), usages)
|
||||
ok += c
|
||||
tot += n
|
||||
res[label] = (ok, tot)
|
||||
(a, ta), (b, tb) = res["legacy"], res["declared"]
|
||||
delta = 100 * b / max(tb, 1) - 100 * a / max(ta, 1)
|
||||
print(f" {Path(progdir).name:<18}"
|
||||
f"{f'{a}/{ta} ({100*a/max(ta,1):.0f}%)':<20}"
|
||||
f"{f'{b}/{tb} ({100*b/max(tb,1):.0f}%)':<20}{delta:+.0f} pts")
|
||||
|
||||
|
||||
def report_survive(seeds: int) -> None:
|
||||
"""From a CONNECTED level, how fragile is that connectivity under one
|
||||
mutation — and would the comparator keep the mutant anyway?"""
|
||||
print(f"survival of connectivity under one mutation ({seeds} trials)\n")
|
||||
print(f" {'programme':<18}{'started connected':<20}{'broken by mutation':<22}"
|
||||
f"{'…and kept by comparator'}")
|
||||
print(" " + "-" * 82)
|
||||
for progdir in CORPUS:
|
||||
fit = make_fitness(progdir)
|
||||
usages = fit.usages()
|
||||
reqs = programme.load_programme_dir(progdir)
|
||||
types = sorted(reqs) + ["C", "O"]
|
||||
started = broken = kept = 0
|
||||
rng = np.random.default_rng(0)
|
||||
for s in range(seeds):
|
||||
root = constructed_seed(progdir, s)
|
||||
c, n = connectivity(root, usages)
|
||||
if c != n:
|
||||
continue # only study layouts that ARE connected
|
||||
started += 1
|
||||
base_score, base_fails = fit.score_with_fails(copy.deepcopy(root))
|
||||
child, _desc = operators.mutate(root, rng, types, reqs=reqs)
|
||||
c2, n2 = connectivity(child, usages)
|
||||
if c2 == n2:
|
||||
continue
|
||||
broken += 1
|
||||
# would the outer loop admit it? lexicographic (-n_fails, fitness)
|
||||
score, fails = fit.score_with_fails(copy.deepcopy(child))
|
||||
if (-len(fails), score) > (-len(base_fails), base_score):
|
||||
kept += 1
|
||||
print(f" {Path(progdir).name:<18}{f'{started}/{seeds}':<20}"
|
||||
f"{f'{broken}/{max(started,1)}':<22}{kept}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("report", choices=("construct", "cost", "survive"))
|
||||
ap.add_argument("--seeds", type=int, default=20)
|
||||
args = ap.parse_args()
|
||||
{"construct": report_construct, "cost": report_cost,
|
||||
"survive": report_survive}[args.report](args.seeds)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -35,7 +35,6 @@ import math
|
|||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import yaml
|
||||
|
||||
from homemaker_layout import dom as dom_mod
|
||||
from homemaker_layout import driver, fitness, geometry
|
||||
|
|
@ -176,6 +175,53 @@ def report_value(progdir: str, seed: int, limit: int) -> None:
|
|||
f"({verdict}), fails {len(base_fails)} -> {len(fails)}")
|
||||
|
||||
|
||||
def frontage_budget(progdir: str) -> dict:
|
||||
"""Feasibility of a programme on its plot, before any search runs.
|
||||
|
||||
Two independent checks, in the order they bite:
|
||||
|
||||
1. **Does the programme fit the plot at all?** ``demand / storeys`` against
|
||||
the plot area. health-centre asks for 240 m² on a 183 m² plot — 131% —
|
||||
and every room comes out at 0.60x its target no matter what the search
|
||||
does.
|
||||
2. **Is there enough daylit wall for the area it does demand?** Every
|
||||
interior leaf needs ``L >= A/(X*h)`` (§38.3), so a storey building
|
||||
``A_built`` needs ``A_built/(X*h)`` metres. The plot's non-``private``
|
||||
perimeter supplies some; interior courtyard supplies the rest, at roughly
|
||||
``2 * area / width`` metres per courtyard slot.
|
||||
|
||||
NB this must be computed against the area the programme actually DEMANDS,
|
||||
not a fully built plot — see §39.11 for the correction.
|
||||
"""
|
||||
root = dom_mod.load(f"{progdir}/init.dom")
|
||||
per = root.perimeter or {}
|
||||
height = root.height or 3.0
|
||||
# measured exactly as `Fitness.area_outside` does: an external boundary is
|
||||
# daylit unless its perimeter type is `private` or `fortified`. Going
|
||||
# through `geometry` rather than the raw YAML corners also picks up the
|
||||
# `wall_outer` inset and the plot rotation, so these are the metres and the
|
||||
# square metres the leaves actually get.
|
||||
daylit = sum(geometry.edge_length(root, e) for e in range(4)
|
||||
if (per.get(geometry.boundary_id(root, e)) or "").lower()
|
||||
not in ("private", "fortified"))
|
||||
plot = geometry.area(root)
|
||||
reqs = programme.load_programme_dir(progdir)
|
||||
storeys = max(programme.n_storeys_required(reqs),
|
||||
programme.storey_minimum(progdir))
|
||||
demand = sum(r.size * r.count for r in reqs.values())
|
||||
built = demand / storeys
|
||||
x_buried, _ = fail_bounds()
|
||||
needed = built / (x_buried * height)
|
||||
gap = needed - daylit
|
||||
court = max(0.0, gap) * 3.0 / 2.0 # 3 m courtyard slots
|
||||
spare = plot - built
|
||||
return dict(plot=plot, daylit=daylit, height=height, storeys=storeys,
|
||||
demand=demand, built=built, needed=needed, gap=gap,
|
||||
court=court, spare=spare,
|
||||
fits_plot=built <= plot,
|
||||
frontage_ok=court <= spare)
|
||||
|
||||
|
||||
def report_frontage(progdirs: list[str]) -> None:
|
||||
x_buried, x_exposed = fail_bounds()
|
||||
print(f"crinkliness fails when 1/crink > {x_buried:.4f} (buried) "
|
||||
|
|
@ -183,35 +229,23 @@ def report_frontage(progdirs: list[str]) -> None:
|
|||
print(f"=> every interior leaf needs exposed wall L >= A / ({x_buried:.4f} * h)\n")
|
||||
|
||||
for progdir in progdirs:
|
||||
seed = yaml.safe_load(open(f"{progdir}/init.dom"))
|
||||
corners, per = seed["node"], (seed.get("perimeter") or {})
|
||||
height = seed.get("height") or 3.0
|
||||
n = len(corners)
|
||||
edges = [math.hypot(corners[(i + 1) % n][0] - corners[i][0],
|
||||
corners[(i + 1) % n][1] - corners[i][1]) for i in range(n)]
|
||||
daylit = sum(e for k, e in zip("abcd", edges)
|
||||
if (per.get(k) or "").lower() not in ("private", "fortified"))
|
||||
area = abs(sum(corners[i][0] * corners[(i + 1) % n][1]
|
||||
- corners[(i + 1) % n][0] * corners[i][1]
|
||||
for i in range(n))) / 2
|
||||
reqs = programme.load_programme_dir(progdir)
|
||||
n_storeys = max(programme.n_storeys_required(reqs),
|
||||
programme.storey_minimum(progdir))
|
||||
demand = sum(r.size * r.count for r in reqs.values())
|
||||
needed = area / (x_buried * height)
|
||||
|
||||
b = frontage_budget(progdir)
|
||||
print(f"=== {Path(progdir).name}")
|
||||
print(f" plot {area:.0f} m2, perimeter {sum(edges):.0f} m, "
|
||||
f"{n_storeys} storeys, h={height}")
|
||||
print(f" perimeter {per} -> daylit frontage {daylit:.0f} m")
|
||||
print(f" a fully built storey needs {needed:.0f} m exposed wall; "
|
||||
f"plot supplies {daylit:.0f} m "
|
||||
f"-> {needed / max(daylit, 1e-9):.1f}x short"
|
||||
if needed > daylit else
|
||||
f" a fully built storey needs {needed:.0f} m exposed wall; "
|
||||
f"plot supplies {daylit:.0f} m -> FEASIBLE")
|
||||
print(f" programme demands {demand:.0f} m2 over {n_storeys} storeys "
|
||||
f"({demand / n_storeys:.0f} m2/storey of {area:.0f} m2 plot)\n")
|
||||
print(f" plot {b['plot']:.0f} m2, daylit perimeter {b['daylit']:.0f} m, "
|
||||
f"{b['storeys']} storeys, h={b['height']:g}")
|
||||
pct = 100 * b["built"] / b["plot"]
|
||||
verdict = "OK" if b["fits_plot"] else "DOES NOT FIT THE PLOT"
|
||||
print(f" 1. programme demands {b['demand']:.0f} m2 -> {b['built']:.0f} m2 "
|
||||
f"per storey = {pct:.0f}% of the plot [{verdict}]")
|
||||
print(f" 2. that needs {b['needed']:.0f} m of daylit wall; perimeter gives "
|
||||
f"{b['daylit']:.0f} m -> gap {b['gap']:+.0f} m")
|
||||
if b["gap"] > 0:
|
||||
print(f" closing it takes ~{b['court']:.0f} m2 of 3 m courtyard; "
|
||||
f"spare plot {b['spare']:.0f} m2 "
|
||||
f"[{'OK' if b['frontage_ok'] else 'NOT ENOUGH ROOM'}]")
|
||||
else:
|
||||
print(" perimeter alone is sufficient")
|
||||
print()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
|
|
|||
7
experiments/results/ab_ssz_power.csv
Normal file
7
experiments/results/ab_ssz_power.csv
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
programme,seed,mode,urb_hard,urb_soft,urb_total,own_hard,own_soft,own_total,elapsed_s
|
||||
harbor-house,0,urb,16,39,55,16,39,55,76.4
|
||||
harbor-house,1,urb,22,45,67,22,45,67,80.8
|
||||
harbor-house,2,urb,22,40,62,22,40,62,76.6
|
||||
harbor-house,3,urb,15,48,63,15,48,63,75.2
|
||||
harbor-house,4,urb,17,42,59,17,42,59,83.9
|
||||
harbor-house,5,urb,29,42,71,29,42,71,81.7
|
||||
|
31
experiments/results/ab_ssz_search.csv
Normal file
31
experiments/results/ab_ssz_search.csv
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
programme,seed,mode,urb_hard,urb_soft,urb_total,own_hard,own_soft,own_total,elapsed_s
|
||||
harbor-house,0,urb,16,39,55,16,39,55,80.2
|
||||
harbor-house,1,urb,22,45,67,22,45,67,88.7
|
||||
harbor-house,2,urb,22,40,62,22,40,62,79.8
|
||||
harbor-house,0,floor,16,38,54,16,38,54,78.9
|
||||
harbor-house,1,floor,21,46,67,21,46,67,82.9
|
||||
harbor-house,2,floor,20,41,61,20,41,61,79.0
|
||||
harbor-house,0,compact_ok,16,39,55,16,22,38,78.7
|
||||
harbor-house,1,compact_ok,20,44,64,20,20,40,82.6
|
||||
harbor-house,2,compact_ok,10,55,65,10,34,44,81.4
|
||||
harbor-house,0,exempt_circulation,18,37,55,18,34,52,77.2
|
||||
harbor-house,1,exempt_circulation,19,49,68,19,42,61,81.0
|
||||
harbor-house,2,exempt_circulation,21,42,63,21,37,58,79.0
|
||||
harbor-house,0,usage_daylight,16,39,55,16,30,46,79.9
|
||||
harbor-house,1,usage_daylight,21,51,72,21,30,51,82.3
|
||||
harbor-house,2,usage_daylight,12,49,61,12,35,47,81.8
|
||||
maple-court,0,urb,30,77,107,30,77,107,108.5
|
||||
maple-court,1,urb,23,83,106,23,83,106,109.0
|
||||
maple-court,2,urb,51,69,120,51,69,120,110.1
|
||||
maple-court,0,floor,35,74,109,35,74,109,109.1
|
||||
maple-court,1,floor,21,81,102,21,81,102,107.9
|
||||
maple-court,2,floor,50,69,119,50,69,119,110.3
|
||||
maple-court,0,compact_ok,30,78,108,30,47,77,109.8
|
||||
maple-court,1,compact_ok,25,82,107,25,43,68,108.6
|
||||
maple-court,2,compact_ok,52,70,122,52,33,85,105.9
|
||||
maple-court,0,exempt_circulation,30,72,102,30,66,96,105.1
|
||||
maple-court,1,exempt_circulation,25,81,106,25,70,95,106.6
|
||||
maple-court,2,exempt_circulation,52,70,122,52,61,113,112.6
|
||||
maple-court,0,usage_daylight,30,69,99,30,52,82,110.3
|
||||
maple-court,1,usage_daylight,25,73,98,25,56,81,107.9
|
||||
maple-court,2,usage_daylight,41,84,125,41,59,100,113.2
|
||||
|
|
|
@ -316,6 +316,7 @@ def search(
|
|||
shapecurve_prune: bool = False,
|
||||
assign_solver: str = "greedy",
|
||||
enable_reassign: bool = False,
|
||||
preserve_circulation: bool = False,
|
||||
) -> SearchResult:
|
||||
"""Run the memetic loop from ``seed_root`` until ``budget`` oracle
|
||||
evaluations are consumed. Returns the best individual found; its ``root``
|
||||
|
|
@ -630,7 +631,8 @@ def search(
|
|||
depth_balanced=depth_balanced,
|
||||
interior_outside=interior_outside, outside_divisor=outside_divisor,
|
||||
construction_beam_width=construction_beam_width,
|
||||
multi_use=multi_use, assign_solver=assign_solver)
|
||||
multi_use=multi_use, assign_solver=assign_solver,
|
||||
preserve_circulation=preserve_circulation)
|
||||
return (topo, None, child_budget, {}, f"construct/{tag}")
|
||||
n = int(rng.integers(max(1, n_target - 1), n_target + 2))
|
||||
return (random_topology(seed_root, n, rng, types), None, child_budget,
|
||||
|
|
|
|||
|
|
@ -223,6 +223,60 @@ def _parse_args(argv=None) -> argparse.Namespace:
|
|||
return p.parse_args(argv)
|
||||
|
||||
|
||||
def _preflight(programme_dir) -> None:
|
||||
"""Warn before the run if the programme cannot fit its plot (homemaker-py-tdp).
|
||||
|
||||
Two checks, cheap and closed-form (DESIGN.md §38.3/§39.11). Neither can be
|
||||
fixed by searching harder, so it is worth saying so up front rather than
|
||||
letting a multi-hour run bottom out against it:
|
||||
|
||||
1. does the demanded floor area fit the plot at all;
|
||||
2. is there enough daylit wall for that area, given every interior leaf
|
||||
needs ``L >= A/(1.6202*h)`` before it fails crinkliness.
|
||||
|
||||
"Daylit" is measured exactly as ``Fitness.area_outside`` does: an external
|
||||
boundary counts unless its perimeter type is ``private`` or ``fortified``.
|
||||
|
||||
Advisory only — it never blocks a run, since an author may deliberately be
|
||||
exploring an over-tight brief.
|
||||
"""
|
||||
from . import geometry
|
||||
from . import programme as _prog
|
||||
|
||||
try:
|
||||
root = dom.load(f"{programme_dir}/init.dom")
|
||||
per = root.perimeter or {}
|
||||
daylit = sum(geometry.edge_length(root, e) for e in range(4)
|
||||
if (per.get(geometry.boundary_id(root, e)) or "").lower()
|
||||
not in ("private", "fortified"))
|
||||
plot = geometry.area(root)
|
||||
height = root.height or 3.0
|
||||
reqs = _prog.load_programme_dir(str(programme_dir))
|
||||
storeys = max(_prog.n_storeys_required(reqs),
|
||||
_prog.storey_minimum(str(programme_dir)))
|
||||
built = sum(r.size * r.count for r in reqs.values()) / max(storeys, 1)
|
||||
except Exception:
|
||||
return # advisory only; never block a run
|
||||
|
||||
if not plot or not daylit:
|
||||
return
|
||||
|
||||
if built > plot:
|
||||
print(f"WARNING: programme demands {built:.0f} m2 per storey on a "
|
||||
f"{plot:.0f} m2 plot ({100 * built / plot:.0f}%). Every room will "
|
||||
f"be squeezed below its target however long the search runs. "
|
||||
f"(DESIGN.md §39.11)", file=sys.stderr)
|
||||
needed = built / (1.6202 * height)
|
||||
if needed > daylit:
|
||||
court = (needed - daylit) * 1.5
|
||||
note = (f", but only {plot - built:.0f} m2 of plot is spare"
|
||||
if court > plot - built else "")
|
||||
print(f"WARNING: {built:.0f} m2 per storey needs ~{needed:.0f} m of daylit "
|
||||
f"wall; the plot's non-private perimeter gives {daylit:.0f} m. "
|
||||
f"Roughly {court:.0f} m2 of courtyard closes the gap{note}. "
|
||||
f"(DESIGN.md §38.3)", file=sys.stderr)
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
args = _parse_args(argv)
|
||||
|
||||
|
|
@ -246,6 +300,8 @@ def main(argv=None) -> int:
|
|||
else:
|
||||
out = args.output.resolve()
|
||||
|
||||
_preflight(programme_dir)
|
||||
|
||||
print(f"seed : {seed_file}", file=sys.stderr)
|
||||
print(f"programme : {programme_dir.name}", file=sys.stderr)
|
||||
print(f"budget : {args.budget}", file=sys.stderr)
|
||||
|
|
|
|||
|
|
@ -118,6 +118,40 @@ _SOFT_FAIL_MARKERS = (
|
|||
)
|
||||
|
||||
|
||||
# homemaker-py-2v1 (DESIGN.md §39.8) — the fails that punish severing a level's
|
||||
# circulation. These are the ONLY counter-pressure against a structural x6 gain:
|
||||
# deleting a circulation leaf merges it into its sibling, converting corridor
|
||||
# into habitable area, and value_inside/value_circulation is 300/50.
|
||||
_CONNECTIVITY_FAIL_MARKERS = ("not connected", "inaccessible usable space")
|
||||
|
||||
|
||||
def is_connectivity_fail(fail: str) -> bool:
|
||||
"""True for a level-connectivity failure (``level N not connected`` /
|
||||
``N inaccessible usable space``)."""
|
||||
return any(m in fail for m in _CONNECTIVITY_FAIL_MARKERS)
|
||||
|
||||
|
||||
def connectivity_weight_for(value_inside: float, value_circulation: float) -> float:
|
||||
"""Smallest integer weight at which severing circulation is net-NEGATIVE.
|
||||
|
||||
Merging a circulation leaf into a habitable sibling multiplies value by
|
||||
``value_inside / value_circulation`` (x6 at the defaults). One failure costs
|
||||
x0.5. So the penalty only outweighs the gain once
|
||||
``0.5**w < value_circulation / value_inside``, i.e.
|
||||
``w > log(vc/vi) / log(0.5)`` — 2.58 at the defaults, hence 3.
|
||||
|
||||
Derived from the value rates rather than hard-coded, so the two stay in step
|
||||
if either rate is ever retuned.
|
||||
"""
|
||||
import math
|
||||
if value_inside <= 0 or value_circulation <= 0:
|
||||
return 1.0
|
||||
ratio = value_circulation / value_inside
|
||||
if ratio >= 1.0: # circulation already worth as much
|
||||
return 1.0
|
||||
return float(math.ceil(math.log(ratio) / math.log(0.5)))
|
||||
|
||||
|
||||
def classify_fail_tier(fail: str) -> str:
|
||||
"""Return ``"hard"`` or ``"soft"`` for one failure string.
|
||||
|
||||
|
|
@ -390,9 +424,22 @@ class Fitness:
|
|||
# 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.
|
||||
# homemaker-py-2v1 (§39.8), EXPERIMENTAL: 1.0 (default) is the flat rule,
|
||||
# byte-identical to every prior run. "auto" derives the smallest weight
|
||||
# that makes severing circulation net-negative; a number sets it explicitly.
|
||||
cw = self.conf("connectivity_weight")
|
||||
if cw is None:
|
||||
self._connectivity_weight = 1.0
|
||||
elif isinstance(cw, str) and cw.lower() == "auto":
|
||||
self._connectivity_weight = connectivity_weight_for(
|
||||
float(self.conf("value_inside")),
|
||||
float(self.conf("value_circulation")))
|
||||
else:
|
||||
self._connectivity_weight = float(cw)
|
||||
self._crinkliness_mode = str(self.conf("crinkliness_mode") or "urb")
|
||||
if self._crinkliness_mode not in (
|
||||
"urb", "floor", "compact_ok", "exempt_circulation"):
|
||||
"urb", "floor", "compact_ok", "exempt_circulation",
|
||||
"usage_daylight"):
|
||||
raise ValueError(
|
||||
f"unknown crinkliness_mode: {self._crinkliness_mode!r}")
|
||||
# The floored value stays BELOW FAIL_THRESHOLD, so a buried leaf still
|
||||
|
|
@ -417,6 +464,16 @@ class Fitness:
|
|||
req = (self._programme or {}).get(leaf.type)
|
||||
return req.usage if req else ""
|
||||
|
||||
def needs_daylight(self, leaf: Node) -> bool:
|
||||
"""Does this leaf's declared usage want a window? (homemaker-py-ssz)
|
||||
|
||||
True only for uses a person occupies (``programme.DAYLIGHT_USAGES``).
|
||||
A generic ``C``/``O``/``S`` leaf has no programme entry and so is False,
|
||||
which is the intended reading: a corridor or a covered courtyard is not
|
||||
failing when it has no daylit wall.
|
||||
"""
|
||||
return self.usage_of(leaf) in _programme.DAYLIGHT_USAGES
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Type superposition + collapse (homemaker-py-9o5)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
|
@ -1161,15 +1218,33 @@ class Fitness:
|
|||
# (c) internal corridors are ordinary architecture; stop requiring
|
||||
# every circulation leaf to reach daylight.
|
||||
return 1.0
|
||||
|
||||
# (b)/(d) one-sided: being MORE compact than target is not a defect the
|
||||
# way over-exposure is. Over-exposure still is one -- a crinkly leaf
|
||||
# costs envelope whatever it is used for -- so this clips the compact
|
||||
# side only, it does not switch the factor off.
|
||||
#
|
||||
# `compact_ok` applies that to every leaf; `usage_daylight` applies it
|
||||
# only where nobody is sitting -- a store, a toilet, plant, a corridor,
|
||||
# a covered courtyard -- and leaves habitable rooms on stock behaviour,
|
||||
# so a windowless bedroom is still the hard failure it should be.
|
||||
one_sided = mode == "compact_ok" or (
|
||||
mode == "usage_daylight" and not self.needs_daylight(leaf))
|
||||
|
||||
if not crink:
|
||||
# Zero exposure IS the compact limit (1/crink -> inf), so a
|
||||
# one-sided factor has to score it 1.0. Reaching here and returning
|
||||
# the floor instead was the flaw in the first `compact_ok`: it
|
||||
# announced that compact is not a defect and then punished the most
|
||||
# compact case of all hardest (§38.8).
|
||||
if one_sided:
|
||||
return 1.0
|
||||
# (a) floor: keep buried leaves rankable by their other factors
|
||||
# instead of collapsing the whole quality product to zero.
|
||||
return self._crinkliness_floor if mode in ("floor", "compact_ok") else 0.0
|
||||
return self._crinkliness_floor if mode == "floor" 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.
|
||||
if one_sided and 1 / crink > distance:
|
||||
return 1.0
|
||||
return max(q, self._crinkliness_floor) if mode in ("floor", "compact_ok") else q
|
||||
|
||||
|
|
@ -1925,8 +2000,23 @@ class Fitness:
|
|||
building_factor = self.evaluate_building(root, tracking)
|
||||
value *= building_factor
|
||||
|
||||
# 0.5^n failure penalty (programme-driven mode, not 0.1^n)
|
||||
value *= 0.5 ** len(failures)
|
||||
# 0.5^n failure penalty (programme-driven mode, not 0.1^n).
|
||||
#
|
||||
# homemaker-py-2v1: connectivity failures may carry EXTRA weight. Under
|
||||
# the flat rule every failure costs x0.5, but severing a level's
|
||||
# circulation *gains* value_inside/value_circulation = x6 (the corridor
|
||||
# becomes habitable area when it merges into its sibling), so the
|
||||
# objective was net-positive on destroying the spine — measured x4.06 on
|
||||
# a well-daylit circulation leaf. ``connectivity_weight`` counts each
|
||||
# connectivity fail as w failures; ``"auto"`` derives the smallest w that
|
||||
# makes severing net-negative from the value rates themselves.
|
||||
w = self._connectivity_weight
|
||||
if w != 1.0:
|
||||
n_conn = sum(1 for f in failures if is_connectivity_fail(f))
|
||||
n_other = len(failures) - n_conn
|
||||
value *= 0.5 ** (n_other + w * n_conn)
|
||||
else:
|
||||
value *= 0.5 ** len(failures)
|
||||
|
||||
score = value / cost if cost != 0.0 else 0.0
|
||||
return score, tuple(sorted(failures)), grade
|
||||
|
|
|
|||
|
|
@ -460,6 +460,80 @@ def mutate_bridge_circulation(root: dom.Node, rng: np.random.Generator,
|
|||
return _finalise(child), f"bridge_circulation lvl{li}: {names} -> C"
|
||||
|
||||
|
||||
def repair_circulation_settled(lvl: dom.Node, reqs, max_bridges: int = 8) -> int:
|
||||
"""Reconnect a storey's circulation AFTER the geometry has settled.
|
||||
|
||||
homemaker-py-yql (DESIGN.md §39.9). ``_assign_adjacency_aware`` picks
|
||||
circulation as a CONNECTED dominating set, but it does so against the
|
||||
pre-resize geometry; ``_size_divisions_from_targets`` then moves every wall
|
||||
to hit the programme's area targets and the shared boundaries the dominating
|
||||
set relied on shrink below ``door_width`` or vanish outright. Measured on
|
||||
health-centre: 41 of 49 circulation-to-circulation edges destroyed by the
|
||||
resize, surviving shared walls squeezed to 0.54-1.11 m against a 1.2 m
|
||||
threshold — so only 5% of constructed seeds started connected, against 100%
|
||||
with the resize disabled.
|
||||
|
||||
This is the same alternating-minimisation fix §37.7 applied to room
|
||||
assignment (``_cpsat_relabel_settled``): re-run the step against the
|
||||
geometry that actually resulted. Retypes the cheapest bridging leaves to
|
||||
``C``, preferring generic outside, then unassigned, and crossing a required
|
||||
room last — the cost model ``mutate_bridge_circulation`` already uses.
|
||||
|
||||
Returns the number of leaves retyped. Idempotent once connected.
|
||||
"""
|
||||
import networkx as nx
|
||||
|
||||
from . import geometry as _geo, graph as _graph
|
||||
|
||||
def _cost(node: dom.Node) -> int:
|
||||
if dom.is_circulation(node):
|
||||
return 0
|
||||
if not node.type:
|
||||
return 1
|
||||
if node.type in dom.GENERIC_OUTSIDE:
|
||||
return 0
|
||||
if reqs and node.type in reqs:
|
||||
return 5
|
||||
return 1
|
||||
|
||||
retyped = 0
|
||||
for _ in range(max_bridges):
|
||||
_geo.clear_cache()
|
||||
G = _geo.leaf_graph(lvl, _graph.DOOR_WIDTH)
|
||||
circ = [x for x in G.nodes() if dom.is_circulation(x)]
|
||||
if not circ:
|
||||
return retyped
|
||||
comps = list(nx.connected_components(G.subgraph(circ)))
|
||||
if len(comps) <= 1:
|
||||
return retyped
|
||||
weighted = G.copy()
|
||||
for u, v, data in weighted.edges(data=True):
|
||||
data["bridge_weight"] = (_cost(u) + _cost(v)) / 2.0
|
||||
best_path = None
|
||||
best_weight = None
|
||||
for i in range(len(comps)):
|
||||
for j in range(i + 1, len(comps)):
|
||||
for a in comps[i]:
|
||||
for b in comps[j]:
|
||||
try:
|
||||
path = nx.shortest_path(weighted, a, b,
|
||||
weight="bridge_weight")
|
||||
except nx.NetworkXNoPath:
|
||||
continue
|
||||
w = sum(_cost(x) for x in path[1:-1])
|
||||
if best_weight is None or w < best_weight:
|
||||
best_weight, best_path = w, path
|
||||
if not best_path:
|
||||
return retyped
|
||||
middle = [x for x in best_path[1:-1] if not dom.is_circulation(x)]
|
||||
if not middle:
|
||||
return retyped # components already touch; nothing to retype
|
||||
for leaf in middle:
|
||||
leaf.type = "C"
|
||||
retyped += 1
|
||||
return retyped
|
||||
|
||||
|
||||
def _shape_failing(leaf: dom.Node, fit) -> bool:
|
||||
"""A named-room leaf whose width or proportion factor actually fails
|
||||
(``< fitness.FAIL_THRESHOLD``) under ``fit``, the same Gaussian quality
|
||||
|
|
@ -802,6 +876,129 @@ def _size_divisions_from_targets(lvl: dom.Node, reqs, fmin: float = 0.04,
|
|||
geometry.clear_cache()
|
||||
|
||||
|
||||
def _circ_components(lvl: dom.Node) -> int:
|
||||
"""Number of connected components among this storey's circulation leaves."""
|
||||
import networkx as nx
|
||||
|
||||
from . import geometry as _geo, graph as _graph
|
||||
|
||||
_geo.clear_cache()
|
||||
G = _geo.leaf_graph(lvl, _graph.DOOR_WIDTH)
|
||||
circ = [x for x in G.nodes() if dom.is_circulation(x)]
|
||||
if not circ:
|
||||
return 0
|
||||
return nx.number_connected_components(G.subgraph(circ))
|
||||
|
||||
|
||||
def _circ_edges(lvl: dom.Node) -> list[tuple]:
|
||||
"""Circulation-to-circulation adjacencies on this storey, as leaf pairs."""
|
||||
from . import geometry as _geo, graph as _graph
|
||||
|
||||
_geo.clear_cache()
|
||||
G = _geo.leaf_graph(lvl, _graph.DOOR_WIDTH)
|
||||
return [(a, b) for a, b in G.edges()
|
||||
if dom.is_circulation(a) and dom.is_circulation(b)]
|
||||
|
||||
|
||||
def _circ_edge_absent(lvl: dom.Node, a: dom.Node, b: dom.Node) -> bool:
|
||||
from . import geometry as _geo, graph as _graph
|
||||
|
||||
_geo.clear_cache()
|
||||
G = _geo.leaf_graph(lvl, _graph.DOOR_WIDTH)
|
||||
return not (G.has_node(a) and G.has_node(b) and G.has_edge(a, b))
|
||||
|
||||
|
||||
def _size_divisions_preserving_circulation(lvl: dom.Node, reqs,
|
||||
max_reverts: int = 12, **kw) -> int:
|
||||
"""Resize toward the programme's area targets WITHOUT severing circulation.
|
||||
|
||||
homemaker-py-3z0 (DESIGN.md §39.10). ``_assign_adjacency_aware`` picks
|
||||
circulation as a CONNECTED dominating set, then
|
||||
``_size_divisions_from_targets`` re-cuts every node — new ratio *and* new
|
||||
rotation — and the shared boundaries the dominating set relied on shrink
|
||||
below ``door_width`` or vanish. Measured (§39.9): fully-connected constructed
|
||||
seeds 1/20, 1/20, 0/20 across the corpus, against 100% with the resize
|
||||
skipped entirely. Both halves of the re-cut do damage, in different
|
||||
proportions per programme — on health-centre it is entirely the ratio, on
|
||||
maple-court mostly the rotation — so a fix has to be able to give back
|
||||
either.
|
||||
|
||||
Rather than rebuild the connection afterwards by retyping rooms to ``C``
|
||||
(measured a net loss — it displaces required rooms at a 3-5 fail cascade
|
||||
each, §39.9), this gives back the *geometry* and keeps the programme intact:
|
||||
snapshot every cut, resize, then greedily revert whichever single cut most
|
||||
reduces the circulation component count until the storey is connected again.
|
||||
Reverting a cut costs only the area-target accuracy of that one subtree, and
|
||||
the inner loop optimises ratios anyway — nothing is displaced and no label
|
||||
changes.
|
||||
|
||||
Returns the number of cuts reverted.
|
||||
"""
|
||||
from . import geometry as _geo
|
||||
|
||||
nodes = []
|
||||
|
||||
def _walk(node: dom.Node) -> None:
|
||||
if node.divided:
|
||||
nodes.append(node)
|
||||
_walk(node.left)
|
||||
_walk(node.right)
|
||||
|
||||
_walk(lvl)
|
||||
before = {id(x): (x.rotation, list(x.division) if x.division else None)
|
||||
for x in nodes}
|
||||
_pre_circ_edges = _circ_edges(lvl)
|
||||
|
||||
_size_divisions_from_targets(lvl, reqs, **kw)
|
||||
|
||||
if _circ_components(lvl) <= 1:
|
||||
_geo.clear_cache()
|
||||
return 0
|
||||
|
||||
# Which circulation pairs were adjacent BEFORE the re-cut and are not now?
|
||||
# Those are the connections the resize broke, and the cuts that govern each
|
||||
# are exactly the ones between the two leaves — so revert those, rather than
|
||||
# hunting for a single cut that happens to reduce the component count. A
|
||||
# plain greedy gets stuck: often no ONE revert helps even though two would.
|
||||
def _paths_between(a: dom.Node, b: dom.Node) -> list[dom.Node]:
|
||||
"""Divided nodes on the tree path joining two leaves (via their LCA)."""
|
||||
def _chain(x: dom.Node) -> list[dom.Node]:
|
||||
out = []
|
||||
while x is not None:
|
||||
out.append(x)
|
||||
x = x.parent
|
||||
return out
|
||||
ca, cb = _chain(a), _chain(b)
|
||||
common = set(map(id, cb))
|
||||
lca = next((x for x in ca if id(x) in common), None)
|
||||
if lca is None:
|
||||
return []
|
||||
seen, out = set(), []
|
||||
for chain in (ca, cb):
|
||||
for x in chain:
|
||||
if x.divided and id(x) not in seen:
|
||||
out.append(x)
|
||||
seen.add(id(x))
|
||||
if x is lca:
|
||||
break
|
||||
return out
|
||||
|
||||
broken = [(a, b) for a, b in _pre_circ_edges
|
||||
if _circ_edge_absent(lvl, a, b)]
|
||||
reverted = 0
|
||||
for a, b in broken:
|
||||
if _circ_components(lvl) <= 1 or reverted >= max_reverts:
|
||||
break
|
||||
for node in _paths_between(a, b):
|
||||
rot, div = before.get(id(node), (None, None))
|
||||
if div is None or (node.rotation == rot and node.division == div):
|
||||
continue
|
||||
node.rotation, node.division = rot, list(div)
|
||||
reverted += 1
|
||||
_geo.clear_cache()
|
||||
return reverted
|
||||
|
||||
|
||||
def _grow_balanced(node: dom.Node, code: str, k: int) -> None:
|
||||
"""Turn ``node`` (a leaf) into a balanced binary subtree of ``k`` leaves, all
|
||||
typed ``code``. Split ratio/rotation are placeholders ([0.5,0.5], rot 0);
|
||||
|
|
@ -1207,7 +1404,9 @@ def constructive_topology(seed_root: dom.Node, reqs, rng: np.random.Generator,
|
|||
outside_divisor: int = 3,
|
||||
construction_beam_width: int = 1,
|
||||
multi_use: bool = False,
|
||||
assign_solver: str = "greedy") -> dom.Node:
|
||||
assign_solver: str = "greedy",
|
||||
repair_circulation: bool = False,
|
||||
preserve_circulation: bool = False) -> dom.Node:
|
||||
"""Build a seed that instantiates every required space by construction.
|
||||
|
||||
The §11.0 diagnosis: random divide+retype chains leave required programme
|
||||
|
|
@ -1312,11 +1511,14 @@ def constructive_topology(seed_root: dom.Node, reqs, rng: np.random.Generator,
|
|||
leaf_co = _leaf_colocate_from_plan(lvl, colocate_plan, reqs) if multi_use else {}
|
||||
leaf_extra = {lf: reqs[co].size for lf, co in leaf_co.items()
|
||||
if co in reqs and reqs[co].size > 0}
|
||||
_size_divisions_from_targets(
|
||||
lvl, reqs, leaf_mult=_leaf_mult_from_plan(lvl, share_plan),
|
||||
leaf_extra=leaf_extra)
|
||||
_resize = (_size_divisions_preserving_circulation if preserve_circulation
|
||||
else _size_divisions_from_targets)
|
||||
_resize(lvl, reqs, leaf_mult=_leaf_mult_from_plan(lvl, share_plan),
|
||||
leaf_extra=leaf_extra)
|
||||
if adjacency_aware and assign_solver == "cpsat":
|
||||
_cpsat_relabel_settled(lvl, reqs)
|
||||
if repair_circulation:
|
||||
repair_circulation_settled(lvl, reqs)
|
||||
|
||||
return _finalise(child)
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,12 @@ TOILET_STRIPS = ("living", "kitchen", "toilet")
|
|||
# Sociable rooms keep their MOST central circulation neighbour; terminal rooms
|
||||
# and toilets keep their LEAST central one.
|
||||
SOCIABLE_USAGES = ("living", "kitchen")
|
||||
# Uses a person OCCUPIES, and which therefore want a window. Everything else --
|
||||
# stores, toilets, plant, corridors, covered courtyards -- is ordinary buried
|
||||
# architecture, and `crinkliness_mode="usage_daylight"` stops the objective
|
||||
# demanding daylight for it (homemaker-py-ssz, DESIGN.md §38.8). A generic
|
||||
# `C`/`O`/`S` leaf has no programme usage and is exempt for the same reason.
|
||||
DAYLIGHT_USAGES = ("living", "kitchen", "bedroom")
|
||||
|
||||
|
||||
def validate_codes(codes) -> None:
|
||||
|
|
|
|||
|
|
@ -528,6 +528,93 @@ def test_crinkliness_exempt_circulation_only_exempts_circulation():
|
|||
assert f.quality_uncrinkliness(room, None, {}) == 0.0
|
||||
|
||||
|
||||
def test_crinkliness_compact_ok_scores_the_buried_limit_as_compact():
|
||||
"""Regression (§38.8): a zero-exposure leaf IS the compact limit.
|
||||
|
||||
The first `compact_ok` returned the floor here, i.e. it announced that
|
||||
being compact is not a defect and then punished the most compact case of
|
||||
all hardest -- which is why it measured inert on buried leaves.
|
||||
"""
|
||||
f, leaf = _stub_fit("compact_ok", stub=0.0)
|
||||
assert f.quality_uncrinkliness(leaf, None, {}) == 1.0
|
||||
|
||||
|
||||
def _usage_fit(mode, stub, code, usage):
|
||||
"""Stub Fitness carrying a one-space programme, so `usage_of` resolves."""
|
||||
conf = dict(CONF_DEFAULTS)
|
||||
conf["crinkliness_mode"] = mode
|
||||
conf["spaces"] = {code: {"usage": usage, "size": [4.0, 1.0]}}
|
||||
f = _StubCrink(conf, dict(COST_DEFAULTS))
|
||||
f._stub = stub
|
||||
return f, _leaf(code)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("usage", ["toilet", "utility", "none"])
|
||||
def test_usage_daylight_exempts_uses_nobody_sits_in(usage):
|
||||
"""A buried store or toilet is ordinary architecture, not a failure."""
|
||||
f, leaf = _usage_fit("usage_daylight", 0.0, "x1", usage)
|
||||
assert f.needs_daylight(leaf) is False
|
||||
assert f.quality_uncrinkliness(leaf, None, {}) == 1.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("usage", ["living", "kitchen", "bedroom"])
|
||||
def test_usage_daylight_still_fails_a_windowless_habitable_room(usage):
|
||||
"""The point of keying on usage: a bedroom with no daylight stays a hard
|
||||
zero, exactly as stock. A mode that rescued this would be deleting the
|
||||
fail category, not fixing the objective."""
|
||||
f, leaf = _usage_fit("usage_daylight", 0.0, "x1", usage)
|
||||
assert f.needs_daylight(leaf) is True
|
||||
assert f.quality_uncrinkliness(leaf, None, {}) == 0.0
|
||||
|
||||
|
||||
def test_usage_daylight_exempts_generic_types():
|
||||
"""Generic `C`/`S` have no programme entry; a corridor needs no window."""
|
||||
f, _ = _usage_fit("usage_daylight", 0.0, "x1", "living")
|
||||
for code in ("C", "S"):
|
||||
assert f.quality_uncrinkliness(_leaf(code), None, {}) == 1.0
|
||||
|
||||
|
||||
def test_usage_daylight_still_punishes_over_exposure():
|
||||
"""Exempt from needing daylight is not exempt from envelope cost: the
|
||||
factor is clipped on the compact side only, never switched off."""
|
||||
target = CONF_DEFAULTS["uncrinkliness"][0]
|
||||
f, leaf = _usage_fit("usage_daylight", 1.0 / (target / 2), "x1", "utility")
|
||||
assert f.quality_uncrinkliness(leaf, None, {}) < 1.0
|
||||
|
||||
|
||||
def test_usage_daylight_leaves_stock_urb_untouched():
|
||||
"""Same tree, mode off -> stock hard zero for every usage."""
|
||||
for usage in ("living", "toilet", "none"):
|
||||
f, leaf = _usage_fit("urb", 0.0, "x1", usage)
|
||||
assert f.quality_uncrinkliness(leaf, None, {}) == 0.0
|
||||
|
||||
|
||||
def test_crinkliness_mode_unknown_raises():
|
||||
with pytest.raises(ValueError, match="crinkliness_mode"):
|
||||
_stub_fit("nonsense")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# homemaker-py-2v1 / DESIGN.md §39.8 — connectivity_weight (EXPERIMENTAL, NULL)
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_connectivity_weight_defaults_to_flat_rule():
|
||||
"""Default must reproduce the flat 0.5^n penalty exactly."""
|
||||
assert Fitness(conf={})._connectivity_weight == 1.0
|
||||
|
||||
|
||||
def test_connectivity_weight_auto_is_derived_from_the_value_gap():
|
||||
"""Not a magic number: the smallest w making 0.5^w < value_circulation /
|
||||
value_inside, so it tracks the rates if either is retuned."""
|
||||
from homemaker_layout.fitness import connectivity_weight_for
|
||||
assert connectivity_weight_for(300.0, 50.0) == 3.0 # 0.5^3 < 1/6 < 0.5^2
|
||||
assert connectivity_weight_for(100.0, 100.0) == 1.0 # no gap, no extra weight
|
||||
assert connectivity_weight_for(400.0, 50.0) == 3.0 # 1/8 -> exactly 3
|
||||
assert Fitness(conf={"connectivity_weight": "auto"})._connectivity_weight == 3.0
|
||||
|
||||
|
||||
def test_is_connectivity_fail_matches_both_strings():
|
||||
from homemaker_layout.fitness import is_connectivity_fail
|
||||
assert is_connectivity_fail("level 0 not connected")
|
||||
assert is_connectivity_fail("1 inaccessible usable space")
|
||||
assert not is_connectivity_fail("0/llr crinkliness")
|
||||
assert not is_connectivity_fail("missing required space: b1")
|
||||
|
|
|
|||
|
|
@ -937,3 +937,89 @@ def test_assign_cpsat_beats_greedy_on_a_namespace_clean_programme():
|
|||
return total
|
||||
|
||||
assert secondary_fails("cpsat") < secondary_fails("greedy")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# homemaker-py-yql / DESIGN.md §39.9 — settled-geometry circulation repair
|
||||
# --------------------------------------------------------------------------- #
|
||||
@pytest.mark.skipif(not HARBOR.is_dir(), reason="harbor-house not available")
|
||||
def test_repair_circulation_default_off_reproduces_prior_seeds():
|
||||
"""Default off must be byte-identical, like every other experimental flag."""
|
||||
from homemaker_layout import programme
|
||||
|
||||
reqs = programme.load_programme_dir(str(HARBOR))
|
||||
types = sorted(reqs) + ["C", "O"]
|
||||
seed = dom.load(str(HARBOR / "init.dom"))
|
||||
kw = dict(min_storeys=programme.storey_minimum(str(HARBOR)),
|
||||
adjacency_aware=True, proportion_aware=True, circ_divisor=3)
|
||||
|
||||
def sig(**extra):
|
||||
root = operators.constructive_topology(
|
||||
seed, reqs, np.random.default_rng(3), types, **kw, **extra)
|
||||
return tuple(lf.type for lvl in dom.levels(root) for lf in lvl.leaves())
|
||||
|
||||
assert sig() == sig(repair_circulation=False)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HARBOR.is_dir(), reason="harbor-house not available")
|
||||
def test_repair_circulation_reconnects_every_storey():
|
||||
"""§39.9: the constructed circulation dominating set is connected, but
|
||||
_size_divisions_from_targets then moves every wall and the shared
|
||||
boundaries it relied on drop below door_width. Repairing against the
|
||||
SETTLED geometry restores connectivity — measured 52% -> 100% of levels on
|
||||
harbor-house. (Whether that is a net WIN is a different question: it is
|
||||
not, see §39.9 — it displaces required rooms. Hence default off.)
|
||||
"""
|
||||
import networkx as nx
|
||||
|
||||
from homemaker_layout import geometry, graph as graph_mod, programme
|
||||
|
||||
reqs = programme.load_programme_dir(str(HARBOR))
|
||||
types = sorted(reqs) + ["C", "O"]
|
||||
seed = dom.load(str(HARBOR / "init.dom"))
|
||||
|
||||
def levels_connected(repair: bool) -> tuple[int, int]:
|
||||
ok = tot = 0
|
||||
for s in range(6):
|
||||
root = operators.constructive_topology(
|
||||
seed, reqs, np.random.default_rng(s), types,
|
||||
min_storeys=programme.storey_minimum(str(HARBOR)),
|
||||
adjacency_aware=True, proportion_aware=True, circ_divisor=3,
|
||||
repair_circulation=repair)
|
||||
for lvl in dom.levels(root):
|
||||
geometry.clear_cache()
|
||||
G = geometry.leaf_graph(lvl, graph_mod.DOOR_WIDTH)
|
||||
circ = [n for n in G.nodes() if dom.is_circulation(n)]
|
||||
tot += 1
|
||||
if circ and nx.is_connected(G.subgraph(circ)):
|
||||
ok += 1
|
||||
return ok, tot
|
||||
|
||||
off_ok, off_tot = levels_connected(False)
|
||||
on_ok, on_tot = levels_connected(True)
|
||||
assert on_ok == on_tot, f"repair left {on_tot - on_ok} storeys disconnected"
|
||||
assert on_ok > off_ok, f"repair did not help: {off_ok}/{off_tot} -> {on_ok}/{on_tot}"
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HARBOR.is_dir(), reason="harbor-house not available")
|
||||
def test_preserve_circulation_default_off_reproduces_prior_seeds():
|
||||
"""§39.10 measured NULL, so the default must stay byte-identical."""
|
||||
from homemaker_layout import geometry, programme
|
||||
|
||||
reqs = programme.load_programme_dir(str(HARBOR))
|
||||
types = sorted(reqs) + ["C", "O"]
|
||||
seed = dom.load(str(HARBOR / "init.dom"))
|
||||
kw = dict(min_storeys=programme.storey_minimum(str(HARBOR)),
|
||||
adjacency_aware=True, proportion_aware=True, circ_divisor=3)
|
||||
|
||||
def sig(**extra):
|
||||
geometry.clear_cache()
|
||||
root = operators.constructive_topology(
|
||||
seed, reqs, np.random.default_rng(5), types, **kw, **extra)
|
||||
geometry.clear_cache()
|
||||
return tuple((lf.type, round(geometry.area(lf), 6))
|
||||
for lvl in dom.levels(root) for lf in lvl.leaves())
|
||||
|
||||
assert sig() == sig(preserve_circulation=False)
|
||||
# ...and it does change something when enabled, or the A/B measured nothing
|
||||
assert sig() != sig(preserve_circulation=True)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue