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.
@@ -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,2 @@
1
+ from .graph import RouteGraph # noqa: F401
2
+ from .dataclasses import Hub, EdgeMetadata, OptimizationMetric, Route, VerboseRoute, Filter, PathNode # noqa: F401
@@ -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)