multimodalrouter 0.1.14__py3-none-any.whl
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.
- multimodalrouter/__init__.py +7 -0
- multimodalrouter/graph/__init__.py +2 -0
- multimodalrouter/graph/dataclasses.py +220 -0
- multimodalrouter/graph/graph.py +798 -0
- multimodalrouter/graphics/__init__.py +1 -0
- multimodalrouter/graphics/graphicsWrapper.py +323 -0
- multimodalrouter/router/__init__.py +0 -0
- multimodalrouter/router/build.py +97 -0
- multimodalrouter/router/route.py +71 -0
- multimodalrouter/utils/__init__.py +1 -0
- multimodalrouter/utils/preprocessor.py +177 -0
- multimodalrouter-0.1.14.dist-info/METADATA +131 -0
- multimodalrouter-0.1.14.dist-info/RECORD +18 -0
- multimodalrouter-0.1.14.dist-info/WHEEL +5 -0
- multimodalrouter-0.1.14.dist-info/entry_points.txt +3 -0
- multimodalrouter-0.1.14.dist-info/licenses/LICENSE.md +10 -0
- multimodalrouter-0.1.14.dist-info/licenses/NOTICE.md +44 -0
- multimodalrouter-0.1.14.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .graph import RouteGraph, Hub, EdgeMetadata, OptimizationMetric, Route, VerboseRoute, Filter, PathNode
|
|
2
|
+
from .utils import preprocessor
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"RouteGraph", "Hub", "EdgeMetadata",
|
|
6
|
+
"OptimizationMetric", "Route", "VerboseRoute", "preprocessor", "Filter", "PathNode"
|
|
7
|
+
]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# dataclasses.py
|
|
2
|
+
# Copyright (c) 2025 Tobias Karusseit
|
|
3
|
+
# Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from abc import abstractmethod, ABC
|
|
9
|
+
from typing import Iterable, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OptimizationMetric(Enum):
|
|
13
|
+
DISTANCE = "distance"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EdgeMetadata:
|
|
17
|
+
__slots__ = ['transportMode', 'metrics']
|
|
18
|
+
|
|
19
|
+
def __init__(self, transportMode: str = None, **metrics):
|
|
20
|
+
self.transportMode = transportMode
|
|
21
|
+
self.metrics = metrics # e.g., {"distance": 12.3, "time": 15} NOTE distance is required by the graph
|
|
22
|
+
|
|
23
|
+
def getMetric(self, metric: OptimizationMetric | str):
|
|
24
|
+
if isinstance(metric, str):
|
|
25
|
+
value = self.metrics.get(metric)
|
|
26
|
+
if value is None:
|
|
27
|
+
raise KeyError(f"Metric '{metric}' not found in EdgeMetadata")
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
value = self.metrics.get(metric.value)
|
|
31
|
+
if value is None:
|
|
32
|
+
raise KeyError(f"Metric '{metric.value}' not found in EdgeMetadata")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
def copy(self):
|
|
36
|
+
return EdgeMetadata(transportMode=self.transportMode, **self.metrics)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def allMetrics(self):
|
|
40
|
+
return self.metrics.copy()
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
return f"transportMode={self.transportMode}, metrics={self.metrics}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Hub:
|
|
47
|
+
"""Base hub class - using regular class instead of dataclass for __slots__ compatibility"""
|
|
48
|
+
__slots__ = ['coords', 'id', 'outgoing', 'hubType']
|
|
49
|
+
|
|
50
|
+
def __init__(self, coords: list[float], id: str, hubType: str):
|
|
51
|
+
self.coords: list[float] = coords
|
|
52
|
+
self.id = id
|
|
53
|
+
self.hubType = hubType
|
|
54
|
+
# dict like {mode -> {dest_id -> EdgeMetadata}}
|
|
55
|
+
self.outgoing: dict[str, dict[str, EdgeMetadata]] = {}
|
|
56
|
+
|
|
57
|
+
def addOutgoing(self, mode: str, dest_id: str, metrics: EdgeMetadata):
|
|
58
|
+
if mode not in self.outgoing:
|
|
59
|
+
self.outgoing[mode] = {}
|
|
60
|
+
self.outgoing[mode][dest_id] = metrics
|
|
61
|
+
|
|
62
|
+
def getMetrics(self, mode: str, dest_id: str) -> EdgeMetadata:
|
|
63
|
+
return self.outgoing.get(mode, {}).get(dest_id, None)
|
|
64
|
+
|
|
65
|
+
def getMetric(self, mode: str, dest_id: str, metric: str) -> float:
|
|
66
|
+
connection = self.outgoing.get(mode, {}).get(dest_id)
|
|
67
|
+
return getattr(connection, metric, None) if connection else None
|
|
68
|
+
|
|
69
|
+
def clone(self) -> "Hub":
|
|
70
|
+
new = Hub(self.coords[:], self.id, self.hubType)
|
|
71
|
+
|
|
72
|
+
for mode, dests in self.outgoing.items():
|
|
73
|
+
for dest_id, meta in dests.items():
|
|
74
|
+
new.addOutgoing(mode, dest_id, meta.copy())
|
|
75
|
+
|
|
76
|
+
return new
|
|
77
|
+
|
|
78
|
+
def __hash__(self):
|
|
79
|
+
return hash((self.hubType, self.id))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class Route:
|
|
84
|
+
"""Route class can use dataclass since it doesn't need __slots__"""
|
|
85
|
+
path: list[tuple[str, str]]
|
|
86
|
+
totalMetrics: EdgeMetadata
|
|
87
|
+
optimizedMetric: OptimizationMetric
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def optimizedValue(self):
|
|
91
|
+
return self.totalMetrics.getMetric(self.optimizedMetric)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def flatPath(self, toStr=True):
|
|
95
|
+
"""Flatten the path into a list of hub IDs"""
|
|
96
|
+
if not self.path:
|
|
97
|
+
return []
|
|
98
|
+
# get all source hubs plus the final destination
|
|
99
|
+
path = [edge for edge in self.path]
|
|
100
|
+
if not toStr:
|
|
101
|
+
return path
|
|
102
|
+
pathStr = ""
|
|
103
|
+
for i, edge in enumerate(path):
|
|
104
|
+
if i == 0:
|
|
105
|
+
pathStr += f"Start: {edge[0]}"
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if len(edge) > 2 and isinstance(edge[2], EdgeMetadata):
|
|
109
|
+
pathStr += f"\n\tEdge: ({str(edge[2])})\n-> {edge[0]}"
|
|
110
|
+
else:
|
|
111
|
+
pathStr += f"{edge[0]} -> {edge[1]}"
|
|
112
|
+
return pathStr
|
|
113
|
+
|
|
114
|
+
def asGraph(self, graph):
|
|
115
|
+
"""
|
|
116
|
+
Creates a new RouteGraph with only the hubs in the route.
|
|
117
|
+
It replicates the settings from the original graph.
|
|
118
|
+
|
|
119
|
+
### NOTE:
|
|
120
|
+
* the graph in the argument should be the same as the graph from which the route was created.
|
|
121
|
+
* hubs that are present in the route, but not found in the graph will be skipped
|
|
122
|
+
|
|
123
|
+
:param:
|
|
124
|
+
graph: the graph to replicate settings from
|
|
125
|
+
|
|
126
|
+
:returns:
|
|
127
|
+
a new RouteGraph with the specified settings and the added route
|
|
128
|
+
"""
|
|
129
|
+
from . import RouteGraph
|
|
130
|
+
subGraph = RouteGraph(
|
|
131
|
+
maxDistance=graph.maxDrivingDistance,
|
|
132
|
+
transportModes=graph.TransportModes,
|
|
133
|
+
compressed=graph.compressed,
|
|
134
|
+
extraMetricsKeys=graph.extraMetricsKeys,
|
|
135
|
+
drivingEnabled=graph.drivingEnabled,
|
|
136
|
+
sourceCoordKeys=graph.sourceCoordKeys,
|
|
137
|
+
destCoordKeys=graph.destCoordKeys
|
|
138
|
+
)
|
|
139
|
+
# gets the hubs from the route (if not present in graph the hub will be dropped)
|
|
140
|
+
hubs: list[Hub] = [graph.getHubById(edge[0]) for edge in self.path if graph.getHubById(edge[0])]
|
|
141
|
+
|
|
142
|
+
copies = [hub.clone() for hub in hubs]
|
|
143
|
+
|
|
144
|
+
# add all hubs to subGraph
|
|
145
|
+
for hub in copies:
|
|
146
|
+
subGraph.addHub(hub)
|
|
147
|
+
|
|
148
|
+
# add links between consecutive hubs
|
|
149
|
+
for prev, curr in zip(copies, copies[1:]):
|
|
150
|
+
transpMode = graph.TransportModes[prev.hubType]
|
|
151
|
+
|
|
152
|
+
meta = prev.getMetrics(transpMode, curr.id)
|
|
153
|
+
if meta is None:
|
|
154
|
+
# recompute distance
|
|
155
|
+
distance = graph._hubToHubDistances([curr], [prev])[0][0].item()
|
|
156
|
+
meta = EdgeMetadata(transportMode=transpMode, distance=distance)
|
|
157
|
+
else:
|
|
158
|
+
meta = meta.copy()
|
|
159
|
+
|
|
160
|
+
subGraph._addLink(prev, curr, transpMode, **meta.metrics)
|
|
161
|
+
|
|
162
|
+
return subGraph
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class VerboseRoute(Route):
|
|
167
|
+
"""Uses base Route class but adds additional info to hold the edge metadata for every leg"""
|
|
168
|
+
path: list[tuple[str, str, EdgeMetadata]]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class PathNode:
|
|
173
|
+
hub_id: str
|
|
174
|
+
mode: str
|
|
175
|
+
edge: EdgeMetadata
|
|
176
|
+
prev: Optional["PathNode"]
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def length(self) -> int:
|
|
180
|
+
return 0 if self.prev is None else self.prev.length + 1
|
|
181
|
+
|
|
182
|
+
def __iter__(self) -> Iterable["PathNode"]:
|
|
183
|
+
node = self
|
|
184
|
+
stack = []
|
|
185
|
+
while node:
|
|
186
|
+
stack.append(node)
|
|
187
|
+
node = node.prev
|
|
188
|
+
yield from reversed(stack)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class Filter(ABC):
|
|
192
|
+
|
|
193
|
+
@abstractmethod
|
|
194
|
+
def filterEdge(self, edge: EdgeMetadata) -> bool:
|
|
195
|
+
"""
|
|
196
|
+
Return True if you want to keep the edge else False
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
edge (EdgeMetadata): Edge to filter
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
bool: True if you want to keep the edge
|
|
203
|
+
"""
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
@abstractmethod
|
|
207
|
+
def filterHub(self, hub: Hub) -> bool:
|
|
208
|
+
"""
|
|
209
|
+
Return True if you want to keep the hub else False
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
hub (Hub): Hub to filter
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
bool: True if you want to keep the hub
|
|
216
|
+
"""
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
def filter(self, start: Hub, end: Hub, edge: EdgeMetadata, path: PathNode | None = None) -> bool:
|
|
220
|
+
return self.filterHub(start) and self.filterHub(end) and self.filterEdge(edge)
|