docs: Säuberung des Graphen gegen quadratische Laufzeit absichern
Die Fassung im Plan suchte je Kante den Grad über alle Kanten und begann nach jedem Fund von vorn. Am echten Skelett mit rund 12.000 Rohästen wäre das nicht langsam, sondern praktisch stehengeblieben. Jetzt wird der Grad einmal gezählt und fortgeschrieben, ein Durchgang räumt alle Fransen auf einmal weg, und ein Test mit über 1.500 Kanten nagelt es fest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3fe3cd5c45
commit
1300dc9a2a
1 file changed
+69
-13
@@ -532,6 +532,29 @@ def test_breite_ignoriert_die_kreuzungsenden():
|
|||||||
assert out.edges[0].klass == "wohnstrasse"
|
assert out.edges[0].klass == "wohnstrasse"
|
||||||
|
|
||||||
|
|
||||||
|
def test_saeubern_bleibt_bei_vielen_fransen_schnell():
|
||||||
|
"""Wache gegen quadratische Laufzeit.
|
||||||
|
|
||||||
|
Das echte Skelett hat rund 12.000 Rohäste, die allermeisten davon Fransen. Eine Fassung, die
|
||||||
|
je Kante den Grad über alle Kanten sucht und nach jedem Fund von vorn beginnt, rechnet daran
|
||||||
|
Stunden — hier fällt sie sofort auf.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
nodes = [Node(punkt(x, 0)) for x in range(0, 2001, 2)]
|
||||||
|
edges = [Edge(i, i + 1, gerade(nodes[i].point, nodes[i + 1].point))
|
||||||
|
for i in range(len(nodes) - 1)]
|
||||||
|
for i in range(1, len(nodes) - 1, 2): # an jeden zweiten Knoten eine Franse
|
||||||
|
nodes.append(Node(punkt(nodes[i].point[0], 3)))
|
||||||
|
edges.append(Edge(i, len(nodes) - 1, gerade(nodes[i].point, nodes[-1].point)))
|
||||||
|
graph = Graph(nodes=nodes, edges=edges)
|
||||||
|
assert len(graph.edges) > 1500
|
||||||
|
start = time.perf_counter()
|
||||||
|
out = clean(graph)
|
||||||
|
assert time.perf_counter() - start < 5.0
|
||||||
|
assert len(out.edges) == 1, "nach den Fransen bleibt eine durchgehende Kante"
|
||||||
|
|
||||||
|
|
||||||
def test_klasse_ist_die_naechstliegende_regelbreite():
|
def test_klasse_ist_die_naechstliegende_regelbreite():
|
||||||
distance = np.full((200, 200), 8.0) # 16 m
|
distance = np.full((200, 200), 8.0) # 16 m
|
||||||
graph = Graph(nodes=[Node(punkt(SLICE["x0"] + 20, SLICE["y1"] - 100)),
|
graph = Graph(nodes=[Node(punkt(SLICE["x0"] + 20, SLICE["y1"] - 100)),
|
||||||
@@ -553,6 +576,8 @@ Erwartet: `ImportError: cannot import name 'clean'`
|
|||||||
An `tools/extract/leonida/roads.py` anhängen:
|
An `tools/extract/leonida/roads.py` anhängen:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
from shapely.geometry import LineString
|
from shapely.geometry import LineString
|
||||||
|
|
||||||
from .coords import SLICE
|
from .coords import SLICE
|
||||||
@@ -595,25 +620,55 @@ def _length(edge: Edge) -> float:
|
|||||||
return LineString(edge.points).length
|
return LineString(edge.points).length
|
||||||
|
|
||||||
|
|
||||||
|
def _degrees(graph: Graph) -> Counter:
|
||||||
|
grade = Counter()
|
||||||
|
for edge in graph.edges:
|
||||||
|
grade[edge.a] += 1
|
||||||
|
grade[edge.b] += 1
|
||||||
|
return grade
|
||||||
|
|
||||||
|
|
||||||
def _drop_spurs(graph: Graph) -> None:
|
def _drop_spurs(graph: Graph) -> None:
|
||||||
changed = True
|
"""Fransen weg, in Durchgängen statt Fund für Fund.
|
||||||
while changed:
|
|
||||||
changed = False
|
Der Grad wird einmal gezählt und beim Entfernen fortgeschrieben, statt ihn je Kante neu über
|
||||||
for k, edge in enumerate(graph.edges):
|
alle Kanten zu suchen, und ein Durchgang räumt alle Fransen auf einmal weg, statt nach jedem
|
||||||
frei = graph.degree(edge.a) == 1 or graph.degree(edge.b) == 1
|
Fund von vorn zu beginnen. Beides zusammen macht aus einer quadratischen eine lineare
|
||||||
if frei and _length(edge) < SPUR_MIN:
|
Rechnung je Durchgang — das echte Skelett hat rund 12.000 Rohäste, und die naive Fassung
|
||||||
del graph.edges[k]
|
rechnete daran Stunden. Die Zahl der Durchgänge ist die Tiefe der Fransenketten, also klein.
|
||||||
changed = True
|
"""
|
||||||
break
|
grade = _degrees(graph)
|
||||||
|
while True:
|
||||||
|
bleibt, entfernt = [], 0
|
||||||
|
for edge in graph.edges:
|
||||||
|
if (grade[edge.a] == 1 or grade[edge.b] == 1) and _length(edge) < SPUR_MIN:
|
||||||
|
grade[edge.a] -= 1
|
||||||
|
grade[edge.b] -= 1
|
||||||
|
entfernt += 1
|
||||||
|
else:
|
||||||
|
bleibt.append(edge)
|
||||||
|
graph.edges = bleibt
|
||||||
|
if not entfernt:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def _incident_index(graph: Graph) -> dict[int, list[int]]:
|
||||||
|
index: dict[int, list[int]] = {}
|
||||||
|
for k, edge in enumerate(graph.edges):
|
||||||
|
index.setdefault(edge.a, []).append(k)
|
||||||
|
if edge.b != edge.a:
|
||||||
|
index.setdefault(edge.b, []).append(k)
|
||||||
|
return index
|
||||||
|
|
||||||
|
|
||||||
def _merge_chains(graph: Graph) -> None:
|
def _merge_chains(graph: Graph) -> None:
|
||||||
|
"""Knoten mit genau zwei Kanten auflösen. Der Index entsteht je Verschmelzung einmal neu —
|
||||||
|
das ist vertretbar, weil es nach dem Fransenschnitt nur noch wenige solcher Knoten gibt."""
|
||||||
changed = True
|
changed = True
|
||||||
while changed:
|
while changed:
|
||||||
changed = False
|
changed = False
|
||||||
for node in range(len(graph.nodes)):
|
for node, incident in _incident_index(graph).items():
|
||||||
incident = graph.incident(node)
|
if len(incident) != 2:
|
||||||
if len(incident) != 2 or graph.degree(node) != 2:
|
|
||||||
continue
|
continue
|
||||||
first, second = (graph.edges[i] for i in incident)
|
first, second = (graph.edges[i] for i in incident)
|
||||||
points = _join(first, second, graph.nodes[node].point)
|
points = _join(first, second, graph.nodes[node].point)
|
||||||
@@ -640,10 +695,11 @@ def _contract_short(graph: Graph) -> None:
|
|||||||
changed = True
|
changed = True
|
||||||
while changed:
|
while changed:
|
||||||
changed = False
|
changed = False
|
||||||
|
grade = _degrees(graph)
|
||||||
for k, edge in enumerate(graph.edges):
|
for k, edge in enumerate(graph.edges):
|
||||||
if edge.a == edge.b or _length(edge) >= CONTRACT_MAX:
|
if edge.a == edge.b or _length(edge) >= CONTRACT_MAX:
|
||||||
continue
|
continue
|
||||||
if graph.degree(edge.a) < 3 or graph.degree(edge.b) < 3:
|
if grade[edge.a] < 3 or grade[edge.b] < 3:
|
||||||
continue
|
continue
|
||||||
ax, ay = graph.nodes[edge.a].point
|
ax, ay = graph.nodes[edge.a].point
|
||||||
bx, by = graph.nodes[edge.b].point
|
bx, by = graph.nodes[edge.b].point
|
||||||
|
|||||||
Reference in new issue
Block a user