§39.9: level-not-connected is destroyed by the resize, not by the search
Answers homemaker-py-yql. §39.8 established the search is not PAID to sever circulation; this establishes where connectivity actually goes. CONSTRUCTED, THEN LOST -- at construction time, in the resize. _assign_adjacency_aware picks circulation as a CONNECTED dominating set and succeeds every time. _size_divisions_from_targets then moves every wall to hit the programme's area targets and destroys it. Measured over 20 constructed seeds per programme, fully-connected seeds: harbor-house 1/20, health-centre 1/20, maple-court 0/20. The control -- same seeds with proportion_aware=False, i.e. no resize -- is 100% connected on all three. Mechanism confirmed 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 door_width=1.2, so they stop counting as edges. This is the failure mode §37.7 recorded for CP-SAT assignment, never looked for in connectivity, where it costs 35-95 points. §39.7 COST CHECK: zero. Identical rates under prefix-inferred vs declared usages -- has_circulation never trims C-C edges, so last commit's usage change could not and did not make connectivity harder to achieve. REPAIR MEASURED NEGATIVE. operators.repair_circulation_settled applies §37.7's own alternating-minimisation fix (re-connect against the settled geometry by retyping the cheapest bridging leaves to C). It restores 100% connectivity on all three programmes -- and is still the wrong trade: connectivity fails fall 0.8-1.7 per seed while missing-room fails rise 5.0-8.5, because every retyped leaf displaces a required room at a 3-5 fail cascade (§38.5). Kept default off with the write-up, per house style for a null lever, plus a byte-identical default test and a test asserting it does reconnect every storey. NEXT LEVER, FILED: preserve the connection during the resize (constrain _size_divisions_from_targets so a shared C-C boundary cannot fall below door_width) rather than rebuild it afterwards at the programme's expense -- a constraint on an existing solve, not a new repair pass. solver.py's existing min_width_generic is the same idea applied to leaf width rather than to a shared boundary, so it may belong beside it. Adds experiments/diag_connectivity_yql.py (construct / cost / survive reports). 355 passed (+2 new), same 7 pre-existing fixture failures, lint unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ84Feep79Hhm3E4zZJmnB
This commit is contained in:
parent
3aee813ccd
commit
7c41069226
5 changed files with 392 additions and 2 deletions
File diff suppressed because one or more lines are too long
83
DESIGN.md
83
DESIGN.md
|
|
@ -5494,3 +5494,86 @@ question (`homemaker-py-yql`) — the evidence now says it is a reachability pro
|
|||
(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.
|
||||
|
|
|
|||
167
experiments/diag_connectivity_yql.py
Normal file
167
experiments/diag_connectivity_yql.py
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
"""Why does `level N not connected` persist? (`homemaker-py-yql`, DESIGN.md §39.9)
|
||||
|
||||
`homemaker-py-2v1` closed NULL: severing a level's circulation is already
|
||||
punished, so the fail is not something the search is paid to create. This asks
|
||||
the follow-on question — is a connected layout **rarely constructed**, or
|
||||
**constructed and then lost**?
|
||||
|
||||
`level N not connected` fires from `graph.connected_circulation`, which keeps
|
||||
only the generic circulation leaves (`C`/`S`) and asks whether *they* form one
|
||||
connected component. It runs on `graph_circ`, i.e. AFTER
|
||||
`graph.has_circulation` has trimmed edges, so §39.7's usage change can in
|
||||
principle reach it — report (b) measures whether it did.
|
||||
|
||||
Three reports:
|
||||
|
||||
construct what fraction of constructed seeds start connected, per level
|
||||
cost the same, prefix-inferred usages vs declared (the §39.7 cost)
|
||||
survive from a CONNECTED layout, how often does one mutation break
|
||||
connectivity, and would the outer comparator keep the mutant
|
||||
|
||||
Usage::
|
||||
|
||||
python experiments/diag_connectivity_yql.py construct
|
||||
python experiments/diag_connectivity_yql.py cost
|
||||
python experiments/diag_connectivity_yql.py survive --seeds 40
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from homemaker_layout import dom as dom_mod
|
||||
from homemaker_layout import driver, fitness, geometry
|
||||
from homemaker_layout import graph as graph_mod
|
||||
from homemaker_layout import operators, programme
|
||||
|
||||
CORPUS = ["examples/harbor-house", "examples/health-centre", "examples/maple-court"]
|
||||
LEGACY_PREFIX = {"b": "bedroom", "t": "toilet", "l": "living", "k": "kitchen"}
|
||||
|
||||
|
||||
def make_fitness(progdir: str) -> fitness.Fitness:
|
||||
overrides = driver._overrides_for(
|
||||
leaf_sharing=True, superpose=False, max_share=None, conn_grade=False,
|
||||
collapse_insearch=True, multi_use=False)
|
||||
conf, cost = fitness.load_config(progdir, overrides=dict(overrides or {}))
|
||||
return fitness.Fitness(conf, cost)
|
||||
|
||||
|
||||
def constructed_seed(progdir: str, seed: int) -> dom_mod.Node:
|
||||
reqs = programme.load_programme_dir(progdir)
|
||||
return operators.constructive_topology(
|
||||
dom_mod.load(f"{progdir}/init.dom"), reqs, np.random.default_rng(seed),
|
||||
sorted(reqs) + ["C", "O"],
|
||||
min_storeys=programme.storey_minimum(progdir),
|
||||
adjacency_aware=True, proportion_aware=True, circ_divisor=3,
|
||||
leaf_sharing=True, leaf_share_factor=3, depth_balanced=True,
|
||||
interior_outside=True, outside_divisor=3)
|
||||
|
||||
|
||||
def connectivity(root: dom_mod.Node, usages: dict[str, str]) -> tuple[int, int]:
|
||||
"""``(levels_connected, levels_total)`` for one tree.
|
||||
|
||||
Mirrors the scorer: build the circ graphs, then ask
|
||||
``connected_circulation`` per level on a copy, exactly as
|
||||
``process_storey`` does.
|
||||
"""
|
||||
tree = copy.deepcopy(root)
|
||||
geometry.clear_cache()
|
||||
dom_mod.canonicalize_shares(tree)
|
||||
_, circ = graph_mod.build_graphs_with_circ(tree, 1.2, lambda _f: None, usages)
|
||||
connected = sum(1 for gc in circ
|
||||
if graph_mod.connected_circulation(gc.copy()))
|
||||
return connected, len(circ)
|
||||
|
||||
|
||||
def report_construct(seeds: int) -> None:
|
||||
print(f"how often does a CONSTRUCTED seed start connected? ({seeds} seeds)\n")
|
||||
print(f" {'programme':<18}{'levels connected':<20}{'seeds fully connected'}")
|
||||
print(" " + "-" * 62)
|
||||
for progdir in CORPUS:
|
||||
fit = make_fitness(progdir)
|
||||
usages = fit.usages()
|
||||
ok = tot = full = 0
|
||||
for s in range(seeds):
|
||||
c, n = connectivity(constructed_seed(progdir, s), usages)
|
||||
ok += c
|
||||
tot += n
|
||||
full += (c == n)
|
||||
print(f" {Path(progdir).name:<18}{f'{ok}/{tot} ({100*ok/max(tot,1):.0f}%)':<20}"
|
||||
f"{full}/{seeds}")
|
||||
|
||||
|
||||
def report_cost(seeds: int) -> None:
|
||||
"""Did §39.7's usage change make level connectivity harder to achieve?"""
|
||||
print("§39.7 cost check — prefix-inferred usages vs declared "
|
||||
f"({seeds} seeds)\n")
|
||||
print(f" {'programme':<18}{'prefix-inferred':<20}{'declared':<20}delta")
|
||||
print(" " + "-" * 68)
|
||||
for progdir in CORPUS:
|
||||
reqs = programme.load_programme_dir(progdir)
|
||||
declared = {c: r.usage for c, r in reqs.items()}
|
||||
legacy = {c: LEGACY_PREFIX.get(c[:1].lower(), "none") for c in reqs}
|
||||
res = {}
|
||||
for label, usages in (("legacy", legacy), ("declared", declared)):
|
||||
ok = tot = 0
|
||||
for s in range(seeds):
|
||||
c, n = connectivity(constructed_seed(progdir, s), usages)
|
||||
ok += c
|
||||
tot += n
|
||||
res[label] = (ok, tot)
|
||||
(a, ta), (b, tb) = res["legacy"], res["declared"]
|
||||
delta = 100 * b / max(tb, 1) - 100 * a / max(ta, 1)
|
||||
print(f" {Path(progdir).name:<18}"
|
||||
f"{f'{a}/{ta} ({100*a/max(ta,1):.0f}%)':<20}"
|
||||
f"{f'{b}/{tb} ({100*b/max(tb,1):.0f}%)':<20}{delta:+.0f} pts")
|
||||
|
||||
|
||||
def report_survive(seeds: int) -> None:
|
||||
"""From a CONNECTED level, how fragile is that connectivity under one
|
||||
mutation — and would the comparator keep the mutant anyway?"""
|
||||
print(f"survival of connectivity under one mutation ({seeds} trials)\n")
|
||||
print(f" {'programme':<18}{'started connected':<20}{'broken by mutation':<22}"
|
||||
f"{'…and kept by comparator'}")
|
||||
print(" " + "-" * 82)
|
||||
for progdir in CORPUS:
|
||||
fit = make_fitness(progdir)
|
||||
usages = fit.usages()
|
||||
reqs = programme.load_programme_dir(progdir)
|
||||
types = sorted(reqs) + ["C", "O"]
|
||||
started = broken = kept = 0
|
||||
rng = np.random.default_rng(0)
|
||||
for s in range(seeds):
|
||||
root = constructed_seed(progdir, s)
|
||||
c, n = connectivity(root, usages)
|
||||
if c != n:
|
||||
continue # only study layouts that ARE connected
|
||||
started += 1
|
||||
base_score, base_fails = fit.score_with_fails(copy.deepcopy(root))
|
||||
child, _desc = operators.mutate(root, rng, types, reqs=reqs)
|
||||
c2, n2 = connectivity(child, usages)
|
||||
if c2 == n2:
|
||||
continue
|
||||
broken += 1
|
||||
# would the outer loop admit it? lexicographic (-n_fails, fitness)
|
||||
score, fails = fit.score_with_fails(copy.deepcopy(child))
|
||||
if (-len(fails), score) > (-len(base_fails), base_score):
|
||||
kept += 1
|
||||
print(f" {Path(progdir).name:<18}{f'{started}/{seeds}':<20}"
|
||||
f"{f'{broken}/{max(started,1)}':<22}{kept}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("report", choices=("construct", "cost", "survive"))
|
||||
ap.add_argument("--seeds", type=int, default=20)
|
||||
args = ap.parse_args()
|
||||
{"construct": report_construct, "cost": report_cost,
|
||||
"survive": report_survive}[args.report](args.seeds)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -460,6 +460,80 @@ def mutate_bridge_circulation(root: dom.Node, rng: np.random.Generator,
|
|||
return _finalise(child), f"bridge_circulation lvl{li}: {names} -> C"
|
||||
|
||||
|
||||
def repair_circulation_settled(lvl: dom.Node, reqs, max_bridges: int = 8) -> int:
|
||||
"""Reconnect a storey's circulation AFTER the geometry has settled.
|
||||
|
||||
homemaker-py-yql (DESIGN.md §39.9). ``_assign_adjacency_aware`` picks
|
||||
circulation as a CONNECTED dominating set, but it does so against the
|
||||
pre-resize geometry; ``_size_divisions_from_targets`` then moves every wall
|
||||
to hit the programme's area targets and the shared boundaries the dominating
|
||||
set relied on shrink below ``door_width`` or vanish outright. Measured on
|
||||
health-centre: 41 of 49 circulation-to-circulation edges destroyed by the
|
||||
resize, surviving shared walls squeezed to 0.54-1.11 m against a 1.2 m
|
||||
threshold — so only 5% of constructed seeds started connected, against 100%
|
||||
with the resize disabled.
|
||||
|
||||
This is the same alternating-minimisation fix §37.7 applied to room
|
||||
assignment (``_cpsat_relabel_settled``): re-run the step against the
|
||||
geometry that actually resulted. Retypes the cheapest bridging leaves to
|
||||
``C``, preferring generic outside, then unassigned, and crossing a required
|
||||
room last — the cost model ``mutate_bridge_circulation`` already uses.
|
||||
|
||||
Returns the number of leaves retyped. Idempotent once connected.
|
||||
"""
|
||||
import networkx as nx
|
||||
|
||||
from . import geometry as _geo, graph as _graph
|
||||
|
||||
def _cost(node: dom.Node) -> int:
|
||||
if dom.is_circulation(node):
|
||||
return 0
|
||||
if not node.type:
|
||||
return 1
|
||||
if node.type in dom.GENERIC_OUTSIDE:
|
||||
return 0
|
||||
if reqs and node.type in reqs:
|
||||
return 5
|
||||
return 1
|
||||
|
||||
retyped = 0
|
||||
for _ in range(max_bridges):
|
||||
_geo.clear_cache()
|
||||
G = _geo.leaf_graph(lvl, _graph.DOOR_WIDTH)
|
||||
circ = [x for x in G.nodes() if dom.is_circulation(x)]
|
||||
if not circ:
|
||||
return retyped
|
||||
comps = list(nx.connected_components(G.subgraph(circ)))
|
||||
if len(comps) <= 1:
|
||||
return retyped
|
||||
weighted = G.copy()
|
||||
for u, v, data in weighted.edges(data=True):
|
||||
data["bridge_weight"] = (_cost(u) + _cost(v)) / 2.0
|
||||
best_path = None
|
||||
best_weight = None
|
||||
for i in range(len(comps)):
|
||||
for j in range(i + 1, len(comps)):
|
||||
for a in comps[i]:
|
||||
for b in comps[j]:
|
||||
try:
|
||||
path = nx.shortest_path(weighted, a, b,
|
||||
weight="bridge_weight")
|
||||
except nx.NetworkXNoPath:
|
||||
continue
|
||||
w = sum(_cost(x) for x in path[1:-1])
|
||||
if best_weight is None or w < best_weight:
|
||||
best_weight, best_path = w, path
|
||||
if not best_path:
|
||||
return retyped
|
||||
middle = [x for x in best_path[1:-1] if not dom.is_circulation(x)]
|
||||
if not middle:
|
||||
return retyped # components already touch; nothing to retype
|
||||
for leaf in middle:
|
||||
leaf.type = "C"
|
||||
retyped += 1
|
||||
return retyped
|
||||
|
||||
|
||||
def _shape_failing(leaf: dom.Node, fit) -> bool:
|
||||
"""A named-room leaf whose width or proportion factor actually fails
|
||||
(``< fitness.FAIL_THRESHOLD``) under ``fit``, the same Gaussian quality
|
||||
|
|
@ -1207,7 +1281,8 @@ def constructive_topology(seed_root: dom.Node, reqs, rng: np.random.Generator,
|
|||
outside_divisor: int = 3,
|
||||
construction_beam_width: int = 1,
|
||||
multi_use: bool = False,
|
||||
assign_solver: str = "greedy") -> dom.Node:
|
||||
assign_solver: str = "greedy",
|
||||
repair_circulation: bool = False) -> dom.Node:
|
||||
"""Build a seed that instantiates every required space by construction.
|
||||
|
||||
The §11.0 diagnosis: random divide+retype chains leave required programme
|
||||
|
|
@ -1317,6 +1392,8 @@ def constructive_topology(seed_root: dom.Node, reqs, rng: np.random.Generator,
|
|||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -937,3 +937,65 @@ 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}"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue