RampantTrackGeneration 0.0.1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. rampanttrackgeneration-0.0.1.0.0/PKG-INFO +18 -0
  2. rampanttrackgeneration-0.0.1.0.0/README.md +1 -0
  3. rampanttrackgeneration-0.0.1.0.0/TrackGenerationDemo.py +57 -0
  4. rampanttrackgeneration-0.0.1.0.0/pyproject.toml +29 -0
  5. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/before_preprocessing.png +0 -0
  6. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/intersection_0.png +0 -0
  7. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/intersection_1.png +0 -0
  8. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/intersection_2.png +0 -0
  9. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/intersection_3.png +0 -0
  10. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/intersection_4.png +0 -0
  11. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/post_postprocessing0.png +0 -0
  12. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/post_postprocessing1.png +0 -0
  13. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/post_postprocessing2.png +0 -0
  14. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/post_postprocessing3.png +0 -0
  15. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/pre_postprocessing2.png +0 -0
  16. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/pre_postprocessing3.png +0 -0
  17. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/pre_postprocessing_0.png +0 -0
  18. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/pre_postprocessing_1.png +0 -0
  19. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/reconnection_of_edges_0.png +0 -0
  20. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/reconnection_of_edges_1.png +0 -0
  21. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/reconnection_of_edges_2.png +0 -0
  22. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/reconnection_of_edges_3.png +0 -0
  23. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/track.png +0 -0
  24. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/track_2.png +0 -0
  25. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/track_2_actual.png +0 -0
  26. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/track_3.png +0 -0
  27. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/track_4.png +0 -0
  28. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/voronoi_diagram.png +0 -0
  29. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/voronoi_diagram2.png +0 -0
  30. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/voronoi_diagram3.png +0 -0
  31. rampanttrackgeneration-0.0.1.0.0/screenshots_for_writeup/voronoi_diagram4.png +0 -0
  32. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/Track.py +13 -0
  33. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/TrackGenerator.py +326 -0
  34. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/__init__.py +2 -0
  35. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/edges/Ops.py +87 -0
  36. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/edges/__init__.py +1 -0
  37. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/edges/data/EdgeIntersectionInfo.py +10 -0
  38. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/edges/data/EdgeVertexInfo.py +10 -0
  39. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/edges/data/EdgesMakingAngle.py +8 -0
  40. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/edges/data/__init__.py +5 -0
  41. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/graphs/Ops.py +63 -0
  42. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/graphs/__init__.py +1 -0
  43. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/json/flask/JSON.py +24 -0
  44. rampanttrackgeneration-0.0.1.0.0/src/RampantTrackGeneration/json/flask/__init__.py +1 -0
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: RampantTrackGeneration
3
+ Version: 0.0.1.0.0
4
+ Summary: Rampant on the Tracks's Track generation logic, leveraging Voronout and optimized for a web service.
5
+ Project-URL: Homepage, https://github.com/jpshankar/RampantTrackGeneration
6
+ Project-URL: Issues, https://github.com/jpshankar/RampantTrackGeneration/issues
7
+ Author-email: Javas Shankar <javasshankar@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.13
12
+ Requires-Dist: flask
13
+ Requires-Dist: networkx
14
+ Requires-Dist: numpy
15
+ Requires-Dist: voronout
16
+ Description-Content-Type: text/markdown
17
+
18
+ RampantTrackGeneration
@@ -0,0 +1 @@
1
+ RampantTrackGeneration
@@ -0,0 +1,57 @@
1
+ from math import floor
2
+
3
+ from matplotlib import pyplot, patches
4
+
5
+ from RampantTrackGeneration import TrackGenerator
6
+
7
+ baseWidth = 600
8
+ baseHeight = 600
9
+
10
+ numWalkers = 3
11
+ numDestinations = 3
12
+
13
+ numRegions = (numWalkers * numDestinations) * 2
14
+ #numRegions = 10
15
+
16
+ minWinSteps = floor(numRegions / 4)
17
+ #print(f"minWinSteps {minWinSteps}")
18
+
19
+ percentToProcess = 0.50
20
+
21
+ newConnectionAngleMinQuantile = 0.45
22
+ lonelyConnectionMinLengthQuantile = 0.25
23
+
24
+ connectionEdgeVertexPadding = 0.20
25
+ connectionEdgeNodeBuffer = 0.10
26
+
27
+ track = TrackGenerator.generateTrack(
28
+ diagramWidth = baseWidth,
29
+ diagramHeight = baseHeight,
30
+ numWalkersOnTrack = numWalkers,
31
+ numDestinationsOnTrack = numDestinations,
32
+ diagramEdgePercentageToProcess = percentToProcess,
33
+ newConnectionAngleMinQuantile = newConnectionAngleMinQuantile,
34
+ lonelyConnectionMinLengthQuantile = lonelyConnectionMinLengthQuantile,
35
+ connectionLengthVertexPadding = connectionEdgeVertexPadding,
36
+ connectionLengthNodeBuffer = connectionEdgeNodeBuffer
37
+ )
38
+
39
+ pyplot.figure(0)
40
+ (_, ax) = pyplot.subplots()
41
+
42
+ for (edge, nodes) in track.stops.items():
43
+ node0Id = edge.vertex0Id
44
+ node1Id = edge.vertex1Id
45
+
46
+ node0 = track.nodes[node0Id]
47
+ node1 = track.nodes[node1Id]
48
+
49
+ #print(f"drawing figure {edgesNum} {vertex0Id}, {vertex1Id}: {vertex0}, {vertex1}")
50
+
51
+ pyplot.plot([node0.x, node1.x], [node0.y, node1.y])
52
+
53
+ for node in nodes:
54
+ nodeCircle = patches.Circle(xy = (node.x, node.y), radius = 1.5, color = 'black')
55
+ ax.add_patch(nodeCircle)
56
+
57
+ pyplot.show()
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling >= 1.26"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "RampantTrackGeneration"
7
+ version = "0.0.1.0.0"
8
+ authors = [
9
+ { name="Javas Shankar", email="javasshankar@gmail.com" },
10
+ ]
11
+ description = "Rampant on the Tracks's Track generation logic, leveraging Voronout and optimized for a web service."
12
+ readme = "README.md"
13
+ requires-python = ">=3.13"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ license = "MIT"
19
+ license-files = ["LICEN[CS]E*"]
20
+ dependencies = [
21
+ "flask",
22
+ "networkx",
23
+ "numpy",
24
+ "Voronout",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/jpshankar/RampantTrackGeneration"
29
+ Issues = "https://github.com/jpshankar/RampantTrackGeneration/issues"
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass
2
+
3
+ from .edges.data import EdgeVertexInfo
4
+
5
+ from uuid import uuid4
6
+
7
+ from voronout.Point import Point
8
+
9
+ @dataclass(frozen=True)
10
+ class Track:
11
+ nodes: dict[uuid4, Point]
12
+ stops: dict[EdgeVertexInfo, tuple[Point]]
13
+ edges: tuple[EdgeVertexInfo]
@@ -0,0 +1,326 @@
1
+ from .edges.data import EdgesMakingAngle, EdgeVertexInfo
2
+
3
+ from .edges.Ops import Ops as EdgesOps
4
+ from .graphs.Ops import Ops as GraphOps
5
+
6
+ from math import floor
7
+
8
+ from networkx import Graph, all_node_cuts as networkXAllNodeCuts
9
+
10
+ from voronout.Point import Point
11
+ from voronout.VoronoiDiagram import VoronoiDiagram
12
+
13
+ from .Track import Track
14
+
15
+ import numpy
16
+ import random
17
+
18
+ from uuid import uuid4
19
+
20
+ class TrackGenerator:
21
+ @staticmethod
22
+ def _edgeAngleViability(angle: float, minAcceptableAngle: float) -> bool:
23
+ # neither angle nor its complement should be < minAcceptableAngle
24
+ return angle >= minAcceptableAngle and (180 - angle) >= minAcceptableAngle
25
+
26
+ @staticmethod
27
+ def _calculateEdgeAnglesWithGraphVertexEdges(graph: Graph, edge: EdgeVertexInfo, vertexId: uuid4) -> dict[EdgesMakingAngle, float]:
28
+ vertexNeighborIds = graph.neighbors(n = vertexId)
29
+ vertexEdgesMakingAngles = []
30
+ for vertexNeighborId in vertexNeighborIds:
31
+ otherEdge = EdgeVertexInfo(vertex0Id = vertexId, vertex1Id = vertexNeighborId)
32
+ edgesMakingAngle = EdgesMakingAngle(edge0 = edge, edge1 = otherEdge)
33
+ vertexEdgesMakingAngles.append(edgesMakingAngle)
34
+
35
+ return EdgesOps.calculateEdgeAnglesWithExtantVertexEdges(edgeGraph = graph, edgesMakingAngles = tuple(vertexEdgesMakingAngles))
36
+
37
+ @staticmethod
38
+ def _findPossibleViableConnection(graph: Graph, possibleConnections: tuple[EdgeVertexInfo], minAcceptableAngle: float) -> EdgeVertexInfo:
39
+ possibleViableConnections = []
40
+ for possibleConnection in possibleConnections:
41
+ zeroConnections = TrackGenerator._calculateEdgeAnglesWithGraphVertexEdges(graph = graph, edge = possibleConnection, vertexId = possibleConnection.vertex0Id)
42
+ oneConnections = TrackGenerator._calculateEdgeAnglesWithGraphVertexEdges(graph = graph, edge = possibleConnection, vertexId = possibleConnection.vertex1Id)
43
+
44
+ allZeroConnectionsValid = all((TrackGenerator._edgeAngleViability(angle = angle, minAcceptableAngle = minAcceptableAngle) for angle in zeroConnections.values()))
45
+ allOneConnectionsValid = all((TrackGenerator._edgeAngleViability(angle = angle, minAcceptableAngle = minAcceptableAngle) for angle in oneConnections.values()))
46
+
47
+ if allZeroConnectionsValid and allOneConnectionsValid:
48
+ possibleViableConnections.append(possibleConnection)
49
+
50
+ return random.choice(possibleViableConnections) if possibleViableConnections else None
51
+
52
+ @staticmethod
53
+ def _updateVertexPotentialConnections(existingConnectionsGraph: Graph, potentialConnectionsGraph: Graph, vertexToUpdateId: uuid4):
54
+ verticesConnectedTo = existingConnectionsGraph.neighbors(n = vertexToUpdateId)
55
+ potentialConnectingVertexIds = tuple((potentialConnectingVertexId for potentialConnectingVertexId in existingConnectionsGraph.nodes if potentialConnectingVertexId not in verticesConnectedTo and potentialConnectingVertexId != vertexToUpdateId))
56
+
57
+ vertexToUpdate = existingConnectionsGraph.nodes[vertexToUpdateId]
58
+ GraphOps.addVertexToGraph(graph = potentialConnectionsGraph, vertexId = vertexToUpdateId, vertexX = vertexToUpdate["x"], vertexY = vertexToUpdate["y"])
59
+
60
+ for potentialConnectingVertexId in potentialConnectingVertexIds:
61
+ potentialConnectionVertex = existingConnectionsGraph.nodes[potentialConnectingVertexId]
62
+ GraphOps.addVertexToGraph(graph = existingConnectionsGraph, vertexId = potentialConnectingVertexId, vertexX = potentialConnectionVertex["x"], vertexY = potentialConnectionVertex["y"])
63
+
64
+ potentialConnection = EdgeVertexInfo(vertex0Id = vertexToUpdateId, vertex1Id = potentialConnectingVertexId)
65
+
66
+ potentialConnectionP1 = Point(x = vertexToUpdate["x"], y = vertexToUpdate["y"])
67
+ potentialConnectionP2 = Point(x = potentialConnectionVertex["x"], y = potentialConnectionVertex["y"])
68
+
69
+ potentialConnectionDistance = GraphOps.scaledGraphPointDistance(graph = potentialConnectionsGraph, p1 = potentialConnectionP1, p2 = potentialConnectionP2)
70
+
71
+ GraphOps.addConnectionToGraph(graph = potentialConnectionsGraph, connection = potentialConnection, vertexDistance = potentialConnectionDistance)
72
+
73
+ @staticmethod
74
+ def _pruneIntersectionEdgesFromExistingConnectionsGraph(existingConnectionsGraph: Graph, intersectionEdges: tuple[EdgeVertexInfo]):
75
+ existingConnectionsNodeCutSets = networkXAllNodeCuts(G = existingConnectionsGraph)
76
+ nodesThatWouldDisconnect = set()
77
+
78
+ for existingConnectionNodeCutSet in existingConnectionsNodeCutSets:
79
+ for existingConnectionNodeCut in existingConnectionNodeCutSet:
80
+ nodesThatWouldDisconnect.add(existingConnectionNodeCut)
81
+
82
+ safeToRemoveEdges = tuple((intersectionEdge for intersectionEdge in intersectionEdges if intersectionEdge.vertex0Id not in nodesThatWouldDisconnect and intersectionEdge.vertex1Id not in nodesThatWouldDisconnect))
83
+
84
+ for safeToRemoveEdge in safeToRemoveEdges:
85
+ safeToRemoveEdgeLength = GraphOps.graphEdgeLength(graph = existingConnectionsGraph, edge = safeToRemoveEdge)
86
+ (disconnectedVertex0, disconnectedVertex1) = GraphOps.removeEdgeAndReturnDisconnected(edgeGraph = existingConnectionsGraph, existingEdge = safeToRemoveEdge)
87
+ if len(disconnectedVertex0) > 0 or len(disconnectedVertex1) > 0:
88
+ # Need to disconnect/reconnect like this because refreshing existingConnectionsNodeCutSets after every removal is too expensive
89
+ existingConnectionsGraph.add_edge(u_of_edge = safeToRemoveEdge.vertex0Id, v_of_edge = safeToRemoveEdge.vertex1Id, edgeLength = safeToRemoveEdgeLength)
90
+
91
+ @staticmethod
92
+ def _handleLonelyExistingConnections(existingConnectionsGraph: Graph, lonelyConnectionMinLengthQuantile: float):
93
+ existingConnectionsEdgeLengthMapping = {}
94
+ for (ev0Id, ev1Id) in existingConnectionsGraph.edges:
95
+ existingConnectionEdgeInfo = EdgeVertexInfo(vertex0Id = ev0Id, vertex1Id = ev1Id)
96
+ existingConnectionsEdgeLengthMapping[existingConnectionEdgeInfo] = GraphOps.graphEdgeLength(graph = existingConnectionsGraph, edge = existingConnectionEdgeInfo)
97
+
98
+ existingConnectionEdgeMinQuantile = numpy.quantile(tuple(existingConnectionsEdgeLengthMapping.values()), lonelyConnectionMinLengthQuantile)
99
+
100
+ for (existingConnectionEdgeInfo, existingConnectionEdgeLength) in existingConnectionsEdgeLengthMapping.items():
101
+ existingEdgeVertex0Id = existingConnectionEdgeInfo.vertex0Id
102
+ existingEdgeVertex1Id = existingConnectionEdgeInfo.vertex1Id
103
+
104
+ zeroNeighbors = tuple(existingConnectionsGraph.neighbors(existingEdgeVertex0Id))
105
+ oneNeighbors = tuple(existingConnectionsGraph.neighbors(existingEdgeVertex1Id))
106
+
107
+ if not len(zeroNeighbors) > 1 or not len(oneNeighbors) > 1:
108
+ if existingConnectionEdgeLength < existingConnectionEdgeMinQuantile:
109
+ existingConnectionsGraph.remove_edge(u = existingEdgeVertex0Id, v = existingEdgeVertex1Id)
110
+
111
+ # https://math.stackexchange.com/questions/134112/find-a-point-on-a-line-segment-located-at-a-distance-d-from-one-endpoint
112
+ @staticmethod
113
+ def _generateStopsOnConnection(connectionGraph: Graph, connection: EdgeVertexInfo, connectionLengthVertexPadding: float, connectionLengthNodeBuffer: float) -> tuple[Point]:
114
+ connectionVertexZeroId = connection.vertex0Id
115
+ connectionVertexZero = connectionGraph.nodes[connectionVertexZeroId]
116
+
117
+ connectionVertexOneId = connection.vertex1Id
118
+ connectionVertexOne = connectionGraph.nodes[connectionVertexOneId]
119
+
120
+ zeroIsFirstVertex = connectionVertexZero["x"] < connectionVertexOne["x"]
121
+
122
+ firstVertex = connectionVertexZero if zeroIsFirstVertex else connectionVertexOne
123
+ secondVertex = connectionVertexZero if not zeroIsFirstVertex else connectionVertexOne
124
+
125
+ connectionLength = GraphOps.graphEdgeLength(graph = connectionGraph, edge = connection)
126
+
127
+ connectionXRangeMinInterval = (connectionLengthVertexPadding + connectionLengthNodeBuffer) * connectionLength
128
+ connectionXRangeMaxInterval = connectionLength - connectionXRangeMinInterval
129
+
130
+ connectionNodes = []
131
+
132
+ while connectionXRangeMinInterval < connectionXRangeMaxInterval:
133
+ d = random.uniform(connectionXRangeMinInterval, connectionXRangeMaxInterval)
134
+ td = d / connectionLength
135
+
136
+ nextX = firstVertex["x"] + ((secondVertex["x"] - firstVertex["x"]) * td)
137
+ nextY = firstVertex["y"] + ((secondVertex["y"] - firstVertex["y"]) * td)
138
+
139
+ nextPoint = Point(x = nextX, y = nextY)
140
+ connectionNodes.append(nextPoint)
141
+
142
+ connectionXRangeMinInterval = d + (connectionLengthNodeBuffer * connectionLength)
143
+
144
+ return tuple(connectionNodes)
145
+
146
+ @staticmethod
147
+ def generateTrack(
148
+ diagramWidth: int,
149
+ diagramHeight: int,
150
+ numWalkersOnTrack: int,
151
+ numDestinationsOnTrack: int,
152
+ diagramEdgePercentageToProcess: float,
153
+ newConnectionAngleMinQuantile: float,
154
+ lonelyConnectionMinLengthQuantile: float,
155
+ connectionLengthVertexPadding: float,
156
+ connectionLengthNodeBuffer: float
157
+ ) -> Track:
158
+ numVoronoiDiagramRegions = (numWalkersOnTrack * numDestinationsOnTrack) * 2
159
+
160
+ diagramRegionSites = tuple((Point(x = random.random(), y = random.random()) for _ in range(numVoronoiDiagramRegions)))
161
+
162
+ voronoiDiagram = VoronoiDiagram(basePoints = diagramRegionSites, planeWidth = diagramWidth, planeHeight = diagramHeight)
163
+
164
+ voronoiDiagramEdgesToProcess = []
165
+ for voronoiDiagramEdge in voronoiDiagram.diagramEdges:
166
+ edge = EdgeVertexInfo(vertex0Id = voronoiDiagramEdge.vertex0Id, vertex1Id = voronoiDiagramEdge.vertex1Id)
167
+ if edge not in voronoiDiagramEdgesToProcess:
168
+ voronoiDiagramEdgesToProcess.append(edge)
169
+
170
+ voronoiDiagramEdges = tuple(voronoiDiagramEdgesToProcess)
171
+
172
+ voronoiVertices = voronoiDiagram.vertices
173
+
174
+ existingConnections = Graph(width = diagramWidth, height = diagramHeight)
175
+ potentialConnections = Graph(width = diagramWidth, height = diagramHeight)
176
+
177
+ for vertexId in voronoiVertices.keys():
178
+ edgesWithVertex = set((maybeEdgeWithPoint for maybeEdgeWithPoint in voronoiDiagramEdges if maybeEdgeWithPoint.vertex0Id == vertexId or maybeEdgeWithPoint.vertex1Id == vertexId))
179
+
180
+ if not len(edgesWithVertex) > 0:
181
+ print(f"discarding {vertexId}")
182
+ pass
183
+
184
+ for edgeWithPoint in edgesWithVertex:
185
+ pointEdgeVertex0Id = edgeWithPoint.vertex0Id
186
+ pointEdgeVertex0 = voronoiVertices[pointEdgeVertex0Id]
187
+
188
+ pointEdgeVertex1Id = edgeWithPoint.vertex1Id
189
+ pointEdgeVertex1 = voronoiVertices[pointEdgeVertex1Id]
190
+
191
+ GraphOps.addVertexToGraph(graph = existingConnections, vertexId = pointEdgeVertex0Id, vertexX = pointEdgeVertex0.x, vertexY = pointEdgeVertex0.y)
192
+ GraphOps.addVertexToGraph(graph = existingConnections, vertexId = pointEdgeVertex1Id, vertexX = pointEdgeVertex1.x, vertexY = pointEdgeVertex1.y)
193
+
194
+ pointEdgeVertexDistance = GraphOps.scaledGraphPointDistance(graph = existingConnections, p1 = pointEdgeVertex0, p2 = pointEdgeVertex1)
195
+ GraphOps.addConnectionToGraph(graph = existingConnections, connection = edgeWithPoint, vertexDistance = pointEdgeVertexDistance)
196
+
197
+ existingEdgeVertexAngles = {}
198
+
199
+ for (vertex0Id, vertex1Id) in existingConnections.edges:
200
+ existingEdge = EdgeVertexInfo(vertex0Id = vertex0Id, vertex1Id = vertex1Id)
201
+
202
+ vertex0EdgeAngles = TrackGenerator._calculateEdgeAnglesWithGraphVertexEdges(graph = existingConnections, edge = existingEdge, vertexId = vertex0Id)
203
+ for vertex0AngleEdges in vertex0EdgeAngles:
204
+ if vertex0AngleEdges not in existingEdgeVertexAngles:
205
+ existingEdgeVertexAngles[vertex0AngleEdges] = vertex0EdgeAngles[vertex0AngleEdges]
206
+
207
+ vertex1EdgeAngles = TrackGenerator._calculateEdgeAnglesWithGraphVertexEdges(graph = existingConnections, edge = existingEdge, vertexId = vertex1Id)
208
+ for vertex1AngleEdges in vertex1EdgeAngles:
209
+ if vertex1AngleEdges not in existingEdgeVertexAngles:
210
+ existingEdgeVertexAngles[vertex1AngleEdges] = vertex1EdgeAngles[vertex1AngleEdges]
211
+
212
+ initialDiagramEdgeAngles = tuple(existingEdgeVertexAngles.values())
213
+ initialDiagramMinAcceptableAngle = numpy.quantile(a = initialDiagramEdgeAngles, q = newConnectionAngleMinQuantile)
214
+
215
+ for vertexId in existingConnections.nodes:
216
+ TrackGenerator._updateVertexPotentialConnections(existingConnectionsGraph = existingConnections, potentialConnectionsGraph = potentialConnections, vertexToUpdateId = vertexId)
217
+
218
+ diagramEdgesSorted = sorted(voronoiDiagramEdges, key = lambda vde: GraphOps.graphEdgeLength(graph = existingConnections, edge = vde))
219
+ numDiagramEdgesToProcess = floor(len(diagramEdgesSorted) * diagramEdgePercentageToProcess)
220
+
221
+ diagramEdgesToProcess = diagramEdgesSorted[:numDiagramEdgesToProcess]
222
+
223
+ intersectionEdges = []
224
+
225
+ for diagramEdgeToProcess in diagramEdgesToProcess:
226
+ edgeVertex0Id = diagramEdgeToProcess.vertex0Id
227
+ edgeVertex1Id = diagramEdgeToProcess.vertex1Id
228
+
229
+ if existingConnections.has_edge(u = edgeVertex0Id, v = edgeVertex1Id):
230
+ edgeToProcessLength = GraphOps.graphEdgeLength(graph = existingConnections, edge = diagramEdgeToProcess)
231
+
232
+ (disconnectedByDeletionVertex0, disconnectedByDeletionVertex1) = GraphOps.removeEdgeAndReturnDisconnected(edgeGraph = existingConnections, existingEdge = diagramEdgeToProcess)
233
+ possibleConnections = []
234
+
235
+ for vertexDisconnected0 in disconnectedByDeletionVertex0:
236
+ for vertexDisconnected1 in disconnectedByDeletionVertex1:
237
+ connectionAlreadyAdded = existingConnections.has_edge(u = vertexDisconnected0, v = vertexDisconnected1)
238
+
239
+ if not connectionAlreadyAdded:
240
+ potentialConnection = EdgeVertexInfo(vertex0Id = vertexDisconnected0, vertex1Id = vertexDisconnected1)
241
+
242
+ if GraphOps.graphEdgeLength(graph = potentialConnections, edge = potentialConnection) > edgeToProcessLength:
243
+ possibleConnections.append(potentialConnection)
244
+
245
+ maybeViableConnection = TrackGenerator._findPossibleViableConnection(graph = existingConnections, possibleConnections = tuple(possibleConnections), minAcceptableAngle = initialDiagramMinAcceptableAngle)
246
+
247
+ if maybeViableConnection:
248
+ print(f"reconnecting to {maybeViableConnection}")
249
+ maybeViableVertexLength = GraphOps.graphEdgeLength(graph = potentialConnections, edge = maybeViableConnection)
250
+
251
+ maybeViableVertex0Id = maybeViableConnection.vertex0Id
252
+ maybeViableVertex1Id = maybeViableConnection.vertex1Id
253
+
254
+ potentialConnections.remove_edge(u = maybeViableVertex0Id, v = maybeViableVertex1Id)
255
+
256
+ # calculate intersections
257
+ potentialIntersections = tuple((EdgeVertexInfo(vertex0Id = vertex0Id, vertex1Id = vertex1Id) for (vertex0Id, vertex1Id) in existingConnections.edges if (vertex0Id != maybeViableVertex0Id and vertex0Id != maybeViableVertex1Id) and (vertex1Id != maybeViableVertex0Id and vertex1Id != maybeViableVertex1Id)))
258
+ edgeIntersectionsInfo = EdgesOps.calculateEdgesIntersectionInfo(edgeGraph = existingConnections, intersectingEdge = maybeViableConnection, otherEdges = potentialIntersections)
259
+
260
+ if edgeIntersectionsInfo:
261
+ # We expect this to be sorted along the v0 -> v1 path.
262
+ precedingVertexId = maybeViableVertex0Id
263
+ precedingVertex = GraphOps.graphVertex(graph = existingConnections, vertexId = maybeViableVertex0Id)
264
+
265
+ for edgeIntersectionInfo in edgeIntersectionsInfo:
266
+ edgeIntersectionPoint = edgeIntersectionInfo.intersectionPoint
267
+ distanceToIntersection = GraphOps.scaledGraphPointDistance(graph = existingConnections, p1 = precedingVertex, p2 = edgeIntersectionPoint)
268
+
269
+ edgeIntersectionPointId = uuid4()
270
+ print(f"creating intersection point {edgeIntersectionPointId}")
271
+
272
+ existingConnections.add_node(node_for_adding = edgeIntersectionPointId, x = edgeIntersectionPoint.x, y = edgeIntersectionPoint.y)
273
+
274
+ existingConnections.add_edge(u_of_edge = precedingVertexId, v_of_edge = edgeIntersectionPointId, edgeLength = distanceToIntersection)
275
+ intersectionEdges.append(EdgeVertexInfo(vertex0Id = precedingVertexId, vertex1Id = edgeIntersectionPointId))
276
+
277
+ edgeIntersected = edgeIntersectionInfo.intersectedEdge
278
+
279
+ edgeIntersectedVertex0Id = edgeIntersected.vertex0Id
280
+ edgeIntersectedVertex1Id = edgeIntersected.vertex1Id
281
+
282
+ print(f"handling intersected edge {edgeIntersectedVertex0Id}, {edgeIntersectedVertex1Id}")
283
+
284
+ edgeIntersectedVertex0 = GraphOps.graphVertex(graph = existingConnections, vertexId = edgeIntersectedVertex0Id)
285
+ edgeIntersectedVertex1 = GraphOps.graphVertex(graph = existingConnections, vertexId = edgeIntersectedVertex1Id)
286
+
287
+ iv0DistanceToIntersection = GraphOps.scaledGraphPointDistance(graph = existingConnections, p1 = edgeIntersectedVertex0, p2 = edgeIntersectionPoint)
288
+ iv1DistanceToIntersection = GraphOps.scaledGraphPointDistance(graph = existingConnections, p1 = edgeIntersectionPoint, p2 = edgeIntersectedVertex1)
289
+
290
+ existingConnections.remove_edge(u = edgeIntersectedVertex0Id, v = edgeIntersectedVertex1Id)
291
+ if edgeIntersected in intersectionEdges:
292
+ intersectionEdges.remove(edgeIntersected)
293
+
294
+ existingConnections.add_edge(u_of_edge = edgeIntersectedVertex0Id, v_of_edge = edgeIntersectionPointId, edgeLength = iv0DistanceToIntersection)
295
+ intersectionEdges.append(EdgeVertexInfo(vertex0Id = edgeIntersectedVertex0Id, vertex1Id = edgeIntersectionPointId))
296
+
297
+ existingConnections.add_edge(u_of_edge = edgeIntersectionPointId, v_of_edge = edgeIntersectedVertex1Id, edgeLength = iv1DistanceToIntersection)
298
+ intersectionEdges.append(EdgeVertexInfo(vertex0Id = edgeIntersectionPointId, vertex1Id = edgeIntersectedVertex1Id))
299
+
300
+ TrackGenerator._updateVertexPotentialConnections(existingConnectionsGraph = existingConnections, potentialConnectionsGraph = potentialConnections, vertexToUpdateId = edgeIntersectionPointId)
301
+
302
+ precedingVertexId = edgeIntersectionPointId
303
+ precedingVertex = edgeIntersectionPoint
304
+
305
+ # finish up with precedingVertex (last intersection point) -> vertex1
306
+ maybeViableVertex1 = GraphOps.graphVertex(graph = existingConnections, vertexId = maybeViableVertex1Id)
307
+
308
+ distanceToEnd = GraphOps.scaledGraphPointDistance(graph = existingConnections, p1 = precedingVertex, p2 = maybeViableVertex1)
309
+
310
+ existingConnections.add_edge(u_of_edge = precedingVertexId, v_of_edge = maybeViableVertex1Id, edgeLength = distanceToEnd)
311
+ intersectionEdges.append(EdgeVertexInfo(vertex0Id = precedingVertexId, vertex1Id = maybeViableVertex1Id))
312
+ else:
313
+ existingConnections.add_edge(u_of_edge = maybeViableVertex0Id, v_of_edge = maybeViableVertex1Id, edgeLength = maybeViableVertexLength)
314
+ else:
315
+ print(f"skipping edge {diagramEdgesToProcess}")
316
+
317
+ TrackGenerator._pruneIntersectionEdgesFromExistingConnectionsGraph(existingConnectionsGraph = existingConnections, intersectionEdges = intersectionEdges)
318
+ TrackGenerator._handleLonelyExistingConnections(existingConnectionsGraph = existingConnections, lonelyConnectionMinLengthQuantile = lonelyConnectionMinLengthQuantile)
319
+
320
+ edges = tuple((EdgeVertexInfo(vertex0Id = vertex0Id, vertex1Id = vertex1Id) for (vertex0Id, vertex1Id) in existingConnections.edges))
321
+
322
+ stops = { edge: TrackGenerator._generateStopsOnConnection(connectionGraph = existingConnections, connection = edge, connectionLengthVertexPadding = connectionLengthVertexPadding, connectionLengthNodeBuffer = connectionLengthNodeBuffer) for edge in edges}
323
+
324
+ nodes = { nodeId: GraphOps.graphVertex(graph = existingConnections, vertexId = nodeId) for nodeId in existingConnections.nodes}
325
+
326
+ return Track(nodes = nodes, stops = stops, edges = edges)
@@ -0,0 +1,2 @@
1
+ from .Track import Track
2
+ from .TrackGenerator import TrackGenerator
@@ -0,0 +1,87 @@
1
+ from .data import EdgesMakingAngle, EdgeIntersectionInfo, EdgeVertexInfo
2
+
3
+ from ..graphs.Ops import Ops as GraphOps
4
+
5
+ from math import atan, degrees
6
+
7
+ from networkx import Graph
8
+
9
+ from voronout.Point import Point
10
+
11
+ class Ops:
12
+ @staticmethod
13
+ def _edgeSlope(edgePoint1: Point, edgePoint2: Point) -> float:
14
+ if edgePoint2.x == edgePoint1.x:
15
+ return 0.0
16
+ else:
17
+ return (edgePoint2.y - edgePoint1.y)/(edgePoint2.x - edgePoint1.x)
18
+
19
+ @staticmethod
20
+ def _angleBetweenEdges(edgeGraph: Graph, edge0: EdgeVertexInfo, edge1: EdgeVertexInfo) -> float:
21
+ edge0Slope = Ops._edgeSlope(edgePoint1 = GraphOps.graphVertex(graph = edgeGraph, vertexId = edge0.vertex0Id), edgePoint2 = GraphOps.graphVertex(graph = edgeGraph, vertexId = edge0.vertex1Id))
22
+ edge1Slope = Ops._edgeSlope(edgePoint1 = GraphOps.graphVertex(graph = edgeGraph, vertexId = edge1.vertex0Id), edgePoint2 = GraphOps.graphVertex(graph = edgeGraph, vertexId = edge1.vertex1Id))
23
+
24
+ slopeNumerator = edge0Slope - edge1Slope
25
+ slopeDenominator = 1 + (edge0Slope * edge1Slope)
26
+
27
+ return degrees(atan(abs(slopeNumerator / slopeDenominator)))
28
+
29
+ @staticmethod
30
+ def _calculateEdgeAngles(edgeGraph: Graph, angleEdges: EdgesMakingAngle) -> float:
31
+ return Ops._angleBetweenEdges(edgeGraph = edgeGraph, edge0 = angleEdges.edge0, edge1 = angleEdges.edge1)
32
+
33
+ def calculateEdgeAnglesWithExtantVertexEdges(edgeGraph: Graph, edgesMakingAngles: tuple[EdgesMakingAngle]) -> dict[EdgesMakingAngle, float]:
34
+ return { edgesMakingAngle: Ops._calculateEdgeAngles(edgeGraph = edgeGraph, angleEdges = edgesMakingAngle) for edgesMakingAngle in edgesMakingAngles }
35
+
36
+ # https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line
37
+ @staticmethod
38
+ def _calculateEdgeIntersectionInfo(edgeGraph: Graph, intersectingEdge: EdgeVertexInfo, intersectedEdge: EdgeVertexInfo) -> EdgeIntersectionInfo:
39
+ intersectingVertex0 = GraphOps.graphVertex(graph = edgeGraph, vertexId = intersectingEdge.vertex0Id)
40
+ intersectingVertex1 = GraphOps.graphVertex(graph = edgeGraph, vertexId = intersectingEdge.vertex1Id)
41
+
42
+ intersectedVertex0 = GraphOps.graphVertex(graph = edgeGraph, vertexId = intersectedEdge.vertex0Id)
43
+ intersectedVertex1 = GraphOps.graphVertex(graph = edgeGraph, vertexId = intersectedEdge.vertex1Id)
44
+
45
+ x1dx3 = intersectingVertex0.x - intersectedVertex0.x
46
+ y3dy4 = intersectedVertex0.y - intersectedVertex1.y
47
+
48
+ y1dy3 = intersectingVertex0.y - intersectedVertex0.y
49
+ x3dx4 = intersectedVertex0.x - intersectedVertex1.x
50
+
51
+ x1dx2 = intersectingVertex0.x - intersectingVertex1.x
52
+ y1dy2 = intersectingVertex0.y - intersectingVertex1.y
53
+
54
+ denominator = (x1dx2 * y3dy4) - (y1dy2 * x3dx4)
55
+
56
+ if denominator != 0.0:
57
+ tNumerator = (x1dx3 * y3dy4) - (y1dy3 * x3dx4)
58
+ t = tNumerator / denominator
59
+
60
+ uNumerator = (x1dx2 * y1dy3) - (y1dy2 * x1dx3)
61
+ u = -(uNumerator / denominator)
62
+
63
+ if 0 <= t <= 1 and 0 <= u <= 1:
64
+ intersectionX = intersectingVertex0.x + (t * -x1dx2)
65
+ intersectionY = intersectingVertex0.y + (t * -y1dy2)
66
+
67
+ intersectionPoint = Point(x = intersectionX, y = intersectionY)
68
+ return EdgeIntersectionInfo(intersectionPoint = intersectionPoint, intersectedEdge = intersectedEdge)
69
+ else:
70
+ return None
71
+ else:
72
+ return None
73
+
74
+ @staticmethod
75
+ def calculateEdgesIntersectionInfo(edgeGraph: Graph, intersectingEdge: EdgeVertexInfo, otherEdges: tuple[EdgeVertexInfo]) -> tuple[EdgeIntersectionInfo]:
76
+ edgeIntersectionData = []
77
+ for otherEdge in otherEdges:
78
+ edgeIntersectionInfo = Ops._calculateEdgeIntersectionInfo(edgeGraph = edgeGraph, intersectingEdge = intersectingEdge, intersectedEdge = otherEdge)
79
+ if edgeIntersectionInfo:
80
+ edgeIntersectionData.append(edgeIntersectionInfo)
81
+
82
+ intersectingVertex0Id = intersectingEdge.vertex0Id
83
+ intersectingVertex0 = GraphOps.graphVertex(graph = edgeGraph, vertexId = intersectingVertex0Id)
84
+
85
+ edgeIntersectionData.sort(key = lambda eii: GraphOps.scaledGraphPointDistance(graph = edgeGraph, p1 = intersectingVertex0, p2 = eii.intersectionPoint))
86
+
87
+ return edgeIntersectionData
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+
3
+ from .EdgeVertexInfo import EdgeVertexInfo
4
+
5
+ from voronout.Point import Point
6
+
7
+ @dataclass(frozen=True)
8
+ class EdgeIntersectionInfo:
9
+ intersectionPoint: Point
10
+ intersectedEdge: EdgeVertexInfo
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+ from uuid import uuid4
3
+
4
+ @dataclass(frozen=True)
5
+ class EdgeVertexInfo:
6
+ vertex0Id: uuid4
7
+ vertex1Id: uuid4
8
+
9
+ def __eq__(self, other) -> bool:
10
+ return (self.vertex0Id == other.vertex0Id and self.vertex1Id == other.vertex1Id) or (self.vertex0Id == other.vertex1Id and self.vertex1Id == other.vertex0Id)
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+ from .EdgeVertexInfo import EdgeVertexInfo
4
+
5
+ @dataclass(frozen=True)
6
+ class EdgesMakingAngle:
7
+ edge0: EdgeVertexInfo
8
+ edge1: EdgeVertexInfo
@@ -0,0 +1,5 @@
1
+ from .EdgeIntersectionInfo import EdgeIntersectionInfo
2
+
3
+ from .EdgesMakingAngle import EdgesMakingAngle
4
+
5
+ from .EdgeVertexInfo import EdgeVertexInfo
@@ -0,0 +1,63 @@
1
+ from ..edges.data import EdgeVertexInfo
2
+
3
+ from networkx import Graph
4
+ from networkx.algorithms import approximation as nxApproximation
5
+
6
+ from uuid import uuid4
7
+
8
+ from voronout.Point import Point
9
+
10
+ class Ops:
11
+ @staticmethod
12
+ def graphVertex(graph: Graph, vertexId: uuid4) -> Point:
13
+ if graph.has_node(n = vertexId):
14
+ vertex = graph.nodes[vertexId]
15
+ return Point(x = vertex["x"], y = vertex["y"])
16
+ else:
17
+ return None
18
+
19
+ @staticmethod
20
+ def graphEdgeLength(graph: Graph, edge: EdgeVertexInfo) -> float:
21
+ return graph.edges[edge.vertex0Id, edge.vertex1Id]["edgeLength"]
22
+
23
+ @staticmethod
24
+ def scaledGraphPointDistance(graph: Graph, p1: Point, p2: Point):
25
+ graphWidthScalar = 1 / graph.graph["width"]
26
+ graphHeightScalar = 1 / graph.graph["height"]
27
+
28
+ scaledP1 = p1.scale(widthScalar = graphWidthScalar, heightScalar = graphHeightScalar)
29
+ scaledP2 = p2.scale(widthScalar = graphWidthScalar, heightScalar = graphHeightScalar)
30
+
31
+ return Point.distance(p1 = scaledP1, p2 = scaledP2)
32
+
33
+ @staticmethod
34
+ def addVertexToGraph(graph: Graph, vertexId: uuid4, vertexX: float, vertexY: float):
35
+ if not graph.has_node(n = vertexId):
36
+ graph.add_node(node_for_adding = vertexId, x = vertexX, y = vertexY)
37
+
38
+ @staticmethod
39
+ def addConnectionToGraph(graph: Graph, connection: EdgeVertexInfo, vertexDistance: float):
40
+ vertex0Id = connection.vertex0Id
41
+ vertex1Id = connection.vertex1Id
42
+
43
+ if not graph.has_edge(u = vertex0Id, v = vertex1Id):
44
+ graph.add_edge(u_of_edge = vertex0Id, v_of_edge = vertex1Id, edgeLength = vertexDistance)
45
+
46
+ @staticmethod
47
+ def removeEdgeAndReturnDisconnected(edgeGraph: Graph, existingEdge: EdgeVertexInfo) -> tuple[tuple[EdgeVertexInfo]]:
48
+ existingEdgeVertex0Id = existingEdge.vertex0Id
49
+ existingEdgeVertex1Id = existingEdge.vertex1Id
50
+
51
+ beforeExistingConnectivities = nxApproximation.all_pairs_node_connectivity(G = edgeGraph)
52
+
53
+ vertex0ConnectivityBeforeDeletion = beforeExistingConnectivities[existingEdgeVertex0Id]
54
+ vertex1ConnectivityBeforeDeletion = beforeExistingConnectivities[existingEdgeVertex1Id]
55
+
56
+ edgeGraph.remove_edge(u = existingEdgeVertex0Id, v = existingEdgeVertex1Id)
57
+ afterExistingConnectivities = nxApproximation.all_pairs_node_connectivity(G = edgeGraph)
58
+
59
+ disconnectedByDeletionVertex0 = tuple((maybeDisconnectedVertex for (maybeDisconnectedVertex, numConnectionsLeft) in afterExistingConnectivities[existingEdgeVertex0Id].items() if numConnectionsLeft == 0 and vertex0ConnectivityBeforeDeletion[maybeDisconnectedVertex] > 0))
60
+ disconnectedByDeletionVertex1 = tuple((maybeDisconnectedVertex for (maybeDisconnectedVertex, numConnectionsLeft) in afterExistingConnectivities[existingEdgeVertex1Id].items() if numConnectionsLeft == 0 and vertex1ConnectivityBeforeDeletion[maybeDisconnectedVertex] > 0))
61
+
62
+ return (disconnectedByDeletionVertex0, disconnectedByDeletionVertex1)
63
+
@@ -0,0 +1,24 @@
1
+ from flask.json.provider import DefaultJSONProvider
2
+
3
+ from ... import Track
4
+
5
+ from uuid import UUID
6
+
7
+ from voronout.Point import Point
8
+
9
+ class FlaskRampantTrackGenerationJSONProvider(DefaultJSONProvider):
10
+ def _handlePointDict(self, pointDict: dict[UUID, Point]) -> dict[str, dict[str, float]]:
11
+ return {str(key): self.loads(repr(value)) for (key, value) in pointDict.items()}
12
+
13
+
14
+ def dumps(self, obj, **kw):
15
+ if isinstance(obj, Track):
16
+ stopObj = {self.loads(repr(stopLine)): list((self.loads(repr(lineStop)) for lineStop in lineStops)) for (stopLine, lineStops) in obj.stops.items()}
17
+
18
+ return {
19
+ 'nodes': self._handlePointDict(obj.nodes),
20
+ 'stops': stopObj,
21
+ 'edges': list(stopObj.values())
22
+ }
23
+ else:
24
+ return super().dumps(obj, **kw)
@@ -0,0 +1 @@
1
+ from .JSON import FlaskRampantTrackGenerationJSONProvider