Compare commits
No commits in common. "0d4ae7ad5442a7c3c5a292cc8f32e1f7b7e206cc" and "03aadec1ba0d8b3912acbcb178ee811d71764941" have entirely different histories.
0d4ae7ad54
...
03aadec1ba
18 changed files with 55 additions and 2532 deletions
File diff suppressed because one or more lines are too long
492
DESIGN.md
492
DESIGN.md
|
|
@ -4929,18 +4929,10 @@ 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.**
|
||||
|
||||
**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.
|
||||
That is the cleanest available explanation of why `level 0 not connected` and
|
||||
`level 1 not connected` are still present in the best layout found after
|
||||
1.7 M evals: the search is not failing to fix them, it is being paid ×3–4 to
|
||||
create them.
|
||||
|
||||
Together these retro-explain three prior results as one mechanism, and suggest
|
||||
two of them were measuring a broken gradient rather than a bad idea:
|
||||
|
|
@ -4968,14 +4960,6 @@ 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
|
||||
|
|
@ -5075,16 +5059,10 @@ 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.
|
||||
|
||||
*(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.)*
|
||||
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.
|
||||
|
||||
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
|
||||
|
|
@ -5095,12 +5073,6 @@ 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
|
||||
|
|
@ -5111,180 +5083,6 @@ 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
|
||||
|
|
@ -5624,277 +5422,3 @@ 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.
|
||||
|
|
|
|||
|
|
@ -1,600 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,336 +0,0 @@
|
|||
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,17 +16,6 @@ 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
|
||||
|
|
@ -45,8 +34,7 @@ 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",
|
||||
"usage_daylight")
|
||||
MODES = ("urb", "floor", "compact_ok", "exempt_circulation")
|
||||
|
||||
|
||||
def make_fitness(progdir: str, mode: str) -> fitness.Fitness:
|
||||
|
|
@ -86,12 +74,7 @@ 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
|
||||
# §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):
|
||||
if (leaf.type or "")[:1].upper() not in ("C", "O"):
|
||||
continue
|
||||
out.append((li, leaf.id, leaf.type,
|
||||
fit.area_outside(leaf, graphs[li], {})))
|
||||
|
|
|
|||
|
|
@ -1,145 +0,0 @@
|
|||
"""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()
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
"""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()
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
"""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,6 +35,7 @@ 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
|
||||
|
|
@ -175,53 +176,6 @@ 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) "
|
||||
|
|
@ -229,23 +183,35 @@ 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:
|
||||
b = frontage_budget(progdir)
|
||||
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)
|
||||
|
||||
print(f"=== {Path(progdir).name}")
|
||||
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()
|
||||
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")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
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
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
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,7 +316,6 @@ 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``
|
||||
|
|
@ -631,8 +630,7 @@ 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,
|
||||
preserve_circulation=preserve_circulation)
|
||||
multi_use=multi_use, assign_solver=assign_solver)
|
||||
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,60 +223,6 @@ 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)
|
||||
|
||||
|
|
@ -300,8 +246,6 @@ 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,40 +118,6 @@ _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.
|
||||
|
||||
|
|
@ -424,22 +390,9 @@ 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",
|
||||
"usage_daylight"):
|
||||
"urb", "floor", "compact_ok", "exempt_circulation"):
|
||||
raise ValueError(
|
||||
f"unknown crinkliness_mode: {self._crinkliness_mode!r}")
|
||||
# The floored value stays BELOW FAIL_THRESHOLD, so a buried leaf still
|
||||
|
|
@ -464,16 +417,6 @@ 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)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
|
@ -1218,33 +1161,15 @@ 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 == "floor" else 0.0
|
||||
|
||||
return self._crinkliness_floor if mode in ("floor", "compact_ok") else 0.0
|
||||
q = gaussian(1 / crink, 1.0, distance, sigma)
|
||||
if one_sided and 1 / crink > distance:
|
||||
if mode == "compact_ok" and 1 / crink > distance:
|
||||
# (b) one-sided: being MORE compact than target is not a defect the
|
||||
# way over-exposure is, so clip to 1.0 on the compact side rather
|
||||
# than decaying symmetrically into a fail.
|
||||
return 1.0
|
||||
return max(q, self._crinkliness_floor) if mode in ("floor", "compact_ok") else q
|
||||
|
||||
|
|
@ -2000,23 +1925,8 @@ class Fitness:
|
|||
building_factor = self.evaluate_building(root, tracking)
|
||||
value *= building_factor
|
||||
|
||||
# 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)
|
||||
# 0.5^n failure penalty (programme-driven mode, not 0.1^n)
|
||||
value *= 0.5 ** len(failures)
|
||||
|
||||
score = value / cost if cost != 0.0 else 0.0
|
||||
return score, tuple(sorted(failures)), grade
|
||||
|
|
|
|||
|
|
@ -460,80 +460,6 @@ 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
|
||||
|
|
@ -876,129 +802,6 @@ 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);
|
||||
|
|
@ -1404,9 +1207,7 @@ 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",
|
||||
repair_circulation: bool = False,
|
||||
preserve_circulation: bool = False) -> dom.Node:
|
||||
assign_solver: str = "greedy") -> dom.Node:
|
||||
"""Build a seed that instantiates every required space by construction.
|
||||
|
||||
The §11.0 diagnosis: random divide+retype chains leave required programme
|
||||
|
|
@ -1511,14 +1312,11 @@ 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}
|
||||
_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)
|
||||
_size_divisions_from_targets(
|
||||
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,12 +104,6 @@ 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,93 +528,6 @@ 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,89 +937,3 @@ 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