multimodalrouter 0.1.3__py3-none-any.whl → 0.1.5__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.
Potentially problematic release.
This version of multimodalrouter might be problematic. Click here for more details.
- multimodalrouter/__init__.py +2 -2
- multimodalrouter/graph/__init__.py +1 -1
- multimodalrouter/graph/dataclasses.py +35 -1
- multimodalrouter/graph/graph.py +76 -3
- multimodalrouter/graphics/__init__.py +1 -0
- multimodalrouter/graphics/graphicsWrapper.py +323 -0
- {multimodalrouter-0.1.3.dist-info → multimodalrouter-0.1.5.dist-info}/METADATA +13 -1
- multimodalrouter-0.1.5.dist-info/RECORD +18 -0
- multimodalrouter-0.1.5.dist-info/licenses/NOTICE.md +44 -0
- multimodalrouter-0.1.3.dist-info/RECORD +0 -15
- {multimodalrouter-0.1.3.dist-info → multimodalrouter-0.1.5.dist-info}/WHEEL +0 -0
- {multimodalrouter-0.1.3.dist-info → multimodalrouter-0.1.5.dist-info}/entry_points.txt +0 -0
- {multimodalrouter-0.1.3.dist-info → multimodalrouter-0.1.5.dist-info}/licenses/LICENSE.md +0 -0
- {multimodalrouter-0.1.3.dist-info → multimodalrouter-0.1.5.dist-info}/top_level.txt +0 -0
multimodalrouter/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .graph import RouteGraph, Hub, EdgeMetadata, OptimizationMetric, Route, VerboseRoute
|
|
1
|
+
from .graph import RouteGraph, Hub, EdgeMetadata, OptimizationMetric, Route, VerboseRoute, Filter
|
|
2
2
|
from .utils import preprocessor
|
|
3
3
|
|
|
4
|
-
__all__ = ["RouteGraph", "Hub", "EdgeMetadata", "OptimizationMetric", "Route", "VerboseRoute", "preprocessor"]
|
|
4
|
+
__all__ = ["RouteGraph", "Hub", "EdgeMetadata", "OptimizationMetric", "Route", "VerboseRoute", "preprocessor", "Filter"]
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
from .graph import RouteGraph # noqa: F401
|
|
2
|
-
from .dataclasses import Hub, EdgeMetadata, OptimizationMetric, Route, VerboseRoute # noqa: F401
|
|
2
|
+
from .dataclasses import Hub, EdgeMetadata, OptimizationMetric, Route, VerboseRoute, Filter # noqa: F401
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from enum import Enum
|
|
8
|
+
from abc import abstractmethod, ABC
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class OptimizationMetric(Enum):
|
|
@@ -49,7 +50,8 @@ class Hub:
|
|
|
49
50
|
self.coords: list[float] = coords
|
|
50
51
|
self.id = id
|
|
51
52
|
self.hubType = hubType
|
|
52
|
-
|
|
53
|
+
# dict like {mode -> {dest_id -> EdgeMetadata}}
|
|
54
|
+
self.outgoing: dict[str, dict[str, EdgeMetadata]] = {}
|
|
53
55
|
|
|
54
56
|
def addOutgoing(self, mode: str, dest_id: str, metrics: EdgeMetadata):
|
|
55
57
|
if mode not in self.outgoing:
|
|
@@ -104,3 +106,35 @@ class Route:
|
|
|
104
106
|
class VerboseRoute(Route):
|
|
105
107
|
"""Uses base Route class but adds additional info to hold the edge metadata for every leg"""
|
|
106
108
|
path: list[tuple[str, str, EdgeMetadata]]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Filter(ABC):
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def filterEdge(self, edge: EdgeMetadata) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Return True if you want to keep the edge else False
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
edge (EdgeMetadata): Edge to filter
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
bool: True if you want to keep the edge
|
|
123
|
+
"""
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
def filterHub(self, hub: Hub) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Return True if you want to keep the hub else False
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
hub (Hub): Hub to filter
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
bool: True if you want to keep the hub
|
|
136
|
+
"""
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
def filter(self, start: Hub, end: Hub, edge: EdgeMetadata) -> bool:
|
|
140
|
+
return self.filterHub(start) and self.filterHub(end) and self.filterEdge(edge)
|
multimodalrouter/graph/graph.py
CHANGED
|
@@ -9,8 +9,9 @@ import dill
|
|
|
9
9
|
import heapq
|
|
10
10
|
import os
|
|
11
11
|
import pandas as pd
|
|
12
|
-
from .dataclasses import Hub, EdgeMetadata, OptimizationMetric, Route
|
|
12
|
+
from .dataclasses import Hub, EdgeMetadata, OptimizationMetric, Route, Filter
|
|
13
13
|
from threading import Lock
|
|
14
|
+
from collections import deque
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class RouteGraph:
|
|
@@ -371,10 +372,11 @@ class RouteGraph:
|
|
|
371
372
|
self,
|
|
372
373
|
start_id: str,
|
|
373
374
|
end_id: str,
|
|
374
|
-
allowed_modes: list[str],
|
|
375
|
+
allowed_modes: list[str] = None,
|
|
375
376
|
optimization_metric: OptimizationMetric | str = OptimizationMetric.DISTANCE,
|
|
376
377
|
max_segments: int = 10,
|
|
377
|
-
verbose: bool = False
|
|
378
|
+
verbose: bool = False,
|
|
379
|
+
custom_filter: Filter = None,
|
|
378
380
|
) -> Route | None:
|
|
379
381
|
"""
|
|
380
382
|
Find the optimal path between two hubs using Dijkstra
|
|
@@ -399,6 +401,9 @@ class RouteGraph:
|
|
|
399
401
|
if end_hub is None:
|
|
400
402
|
raise ValueError(f"End hub '{end_id}' not found in graph")
|
|
401
403
|
|
|
404
|
+
if allowed_modes is None:
|
|
405
|
+
allowed_modes = list(self.TransportModes.values())
|
|
406
|
+
|
|
402
407
|
if start_id == end_id:
|
|
403
408
|
# create a route with only the start hub
|
|
404
409
|
# no verbose since no edges are needed
|
|
@@ -460,6 +465,15 @@ class RouteGraph:
|
|
|
460
465
|
if connection_metrics is None: # skip if the connection has no metrics
|
|
461
466
|
continue
|
|
462
467
|
|
|
468
|
+
try:
|
|
469
|
+
next_hub = self.getHubById(next_hub_id)
|
|
470
|
+
except KeyError:
|
|
471
|
+
raise ValueError(
|
|
472
|
+
f"Hub with ID '{next_hub_id}' not found in graph! But it is connected to hub '{current_hub_id}' via mode '{mode}'." # noqa: E501
|
|
473
|
+
)
|
|
474
|
+
if custom_filter is not None and not custom_filter.filter(current_hub, next_hub, connection_metrics):
|
|
475
|
+
continue
|
|
476
|
+
|
|
463
477
|
# get the selected metric alue for this connection
|
|
464
478
|
connection_value = connection_metrics.getMetric(optimization_metric)
|
|
465
479
|
new_metric_value = current_metric_value + connection_value
|
|
@@ -488,6 +502,65 @@ class RouteGraph:
|
|
|
488
502
|
|
|
489
503
|
return None
|
|
490
504
|
|
|
505
|
+
def radial_search(
|
|
506
|
+
self,
|
|
507
|
+
hub_id: str,
|
|
508
|
+
radius: float,
|
|
509
|
+
optimization_metric: OptimizationMetric | str = OptimizationMetric.DISTANCE,
|
|
510
|
+
allowed_modes: list[str] = None,
|
|
511
|
+
custom_filter: Filter = None,
|
|
512
|
+
) -> list[float, Hub]:
|
|
513
|
+
"""
|
|
514
|
+
Find all hubs within a given radius of a given hub
|
|
515
|
+
(Note: distance is measured from the connecting paths not direct)
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
hub_id: ID of the center hub
|
|
519
|
+
radius: maximum distance from the center hub
|
|
520
|
+
optimization_metric: metric to optimize for (e.g. distance, time, cost)
|
|
521
|
+
allowed_modes: list of allowed transport modes (default: None => all modes)
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
list of tuples containing the metric value and the corresponding hub object
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
center = self.getHubById(hub_id)
|
|
528
|
+
if center is None:
|
|
529
|
+
return [center]
|
|
530
|
+
|
|
531
|
+
if allowed_modes is None:
|
|
532
|
+
allowed_modes = list(self.TransportModes.values())
|
|
533
|
+
|
|
534
|
+
hubsToSearch = deque([center])
|
|
535
|
+
queued = set([hub_id])
|
|
536
|
+
reachableHubs: dict[str, tuple[float, Hub]] = {hub_id: (0.0, center)}
|
|
537
|
+
|
|
538
|
+
while hubsToSearch:
|
|
539
|
+
hub = hubsToSearch.popleft() # get the current hub to search
|
|
540
|
+
currentMetricVal, _ = reachableHubs[hub.id] # get the current metric value
|
|
541
|
+
for mode in allowed_modes:
|
|
542
|
+
outgoing = hub.outgoing.get(mode, {}) # find all outgoing connections
|
|
543
|
+
# dict like {dest_id: EdgeMetadata}
|
|
544
|
+
for id, edgemetadata in outgoing.items(): # iter over outgoing connections
|
|
545
|
+
thisMetricVal = edgemetadata.getMetric(optimization_metric)
|
|
546
|
+
if thisMetricVal is None:
|
|
547
|
+
continue
|
|
548
|
+
nextMetricVal = currentMetricVal + thisMetricVal
|
|
549
|
+
if nextMetricVal > radius:
|
|
550
|
+
continue
|
|
551
|
+
knownMetric = reachableHubs.get(id, None)
|
|
552
|
+
destHub = self.getHubById(id)
|
|
553
|
+
if custom_filter is not None and not custom_filter.filter(hub, destHub, edgemetadata):
|
|
554
|
+
continue
|
|
555
|
+
# only save smaller metric values
|
|
556
|
+
if knownMetric is None or knownMetric[0] > nextMetricVal:
|
|
557
|
+
reachableHubs.update({id: (nextMetricVal, destHub)})
|
|
558
|
+
if id not in queued:
|
|
559
|
+
queued.add(id)
|
|
560
|
+
hubsToSearch.append(destHub)
|
|
561
|
+
|
|
562
|
+
return [v for v in reachableHubs.values()]
|
|
563
|
+
|
|
491
564
|
def compare_routes(
|
|
492
565
|
self,
|
|
493
566
|
start_id: str,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .graphicsWrapper import GraphDisplay # noqa: F401
|
|
@@ -0,0 +1,323 @@
|
|
|
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 ..graph import RouteGraph
|
|
7
|
+
import plotly.graph_objects as go
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GraphDisplay():
|
|
11
|
+
|
|
12
|
+
def __init__(self, graph: RouteGraph, name: str = "Graph", iconSize: int = 10) -> None:
|
|
13
|
+
self.graph: RouteGraph = graph
|
|
14
|
+
self.name: str = name
|
|
15
|
+
self.iconSize: int = iconSize
|
|
16
|
+
|
|
17
|
+
def _toPlotlyFormat(
|
|
18
|
+
self,
|
|
19
|
+
nodeTransform=None,
|
|
20
|
+
edgeTransform=None
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
transform the graph data into plotly format.to use the display function
|
|
24
|
+
|
|
25
|
+
args:
|
|
26
|
+
- nodeTransform: function to transform the node coordinates (default = None)
|
|
27
|
+
- edgeTransform: function to transform the edge coordinates (default = None)
|
|
28
|
+
returns:
|
|
29
|
+
- None (modifies self.nodes and self.edges)
|
|
30
|
+
"""
|
|
31
|
+
self.nodes = {
|
|
32
|
+
f"{hub.hubType}-{hub.id}": {
|
|
33
|
+
"coords": hub.coords,
|
|
34
|
+
"hubType": hub.hubType,
|
|
35
|
+
"id": hub.id
|
|
36
|
+
}
|
|
37
|
+
for hub in self.graph._allHubs()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
self.edges = [
|
|
41
|
+
{
|
|
42
|
+
"from": f"{hub.hubType}-{hub.id}",
|
|
43
|
+
"to": f"{self.graph.getHubById(dest).hubType}-{dest}",
|
|
44
|
+
**edge.allMetrics
|
|
45
|
+
}
|
|
46
|
+
for hub in self.graph._allHubs()
|
|
47
|
+
for _, edge in hub.outgoing.items()
|
|
48
|
+
for dest, edge in edge.items()
|
|
49
|
+
]
|
|
50
|
+
self.dim = max(len(node.get("coords")) for node in self.nodes.values())
|
|
51
|
+
|
|
52
|
+
if nodeTransform is not None:
|
|
53
|
+
expandedCoords = [node.get("coords") + [0] * (self.dim - len(node.get("coords"))) for node in self.nodes.values()]
|
|
54
|
+
transformedCoords = nodeTransform(expandedCoords)
|
|
55
|
+
for node, coords in zip(self.nodes.values(), transformedCoords):
|
|
56
|
+
node["coords"] = coords
|
|
57
|
+
|
|
58
|
+
self.dim = max(len(node.get("coords")) for node in self.nodes.values())
|
|
59
|
+
|
|
60
|
+
if edgeTransform is not None:
|
|
61
|
+
starts = [edge["from"] for edge in self.edges]
|
|
62
|
+
startCoords = [self.nodes[start]["coords"] for start in starts]
|
|
63
|
+
ends = [edge["to"] for edge in self.edges]
|
|
64
|
+
endCoords = [self.nodes[end]["coords"] for end in ends]
|
|
65
|
+
|
|
66
|
+
transformedEdges = edgeTransform(startCoords, endCoords)
|
|
67
|
+
for edge, transformedEdge in zip(self.edges, transformedEdges):
|
|
68
|
+
edge["curve"] = transformedEdge
|
|
69
|
+
|
|
70
|
+
def display(
|
|
71
|
+
self,
|
|
72
|
+
nodeTransform=None,
|
|
73
|
+
edgeTransform=None,
|
|
74
|
+
displayEarth=False
|
|
75
|
+
):
|
|
76
|
+
"""
|
|
77
|
+
function to display any 2D or 3D RouteGraph
|
|
78
|
+
|
|
79
|
+
args:
|
|
80
|
+
- nodeTransform: function to transform the node coordinates (default = None)
|
|
81
|
+
- edgeTransform: function to transform the edge coordinates (default = None)
|
|
82
|
+
- displayEarth: whether to display the earth as a background (default = False, only in 3D)
|
|
83
|
+
|
|
84
|
+
returns:
|
|
85
|
+
- None (modifies self.nodes and self.edges opens the plot in a browser)
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
# transform the graph
|
|
89
|
+
self._toPlotlyFormat(nodeTransform, edgeTransform)
|
|
90
|
+
# init plotly placeholders
|
|
91
|
+
node_x, node_y, node_z, text, colors = [], [], [], [], []
|
|
92
|
+
edge_x, edge_y, edge_z, edge_text = [], [], [], []
|
|
93
|
+
|
|
94
|
+
# add all the nodes
|
|
95
|
+
for node_key, node_data in self.nodes.items():
|
|
96
|
+
x, y, *rest = node_data["coords"]
|
|
97
|
+
node_x.append(x)
|
|
98
|
+
node_y.append(y)
|
|
99
|
+
if self.dim == 3:
|
|
100
|
+
node_z.append(node_data["coords"][2])
|
|
101
|
+
text.append(f"{node_data['id']}<br>Type: {node_data['hubType']}")
|
|
102
|
+
colors.append(hash(node_data['hubType']) % 10)
|
|
103
|
+
|
|
104
|
+
# add all the edges
|
|
105
|
+
for edge in self.edges:
|
|
106
|
+
# check if edge has been transformed
|
|
107
|
+
if "curve" in edge:
|
|
108
|
+
curve = edge["curve"]
|
|
109
|
+
# add all the points of the edge
|
|
110
|
+
for point in curve:
|
|
111
|
+
edge_x.append(point[0])
|
|
112
|
+
edge_y.append(point[1])
|
|
113
|
+
if self.dim == 3:
|
|
114
|
+
edge_z.append(point[2])
|
|
115
|
+
edge_x.append(None)
|
|
116
|
+
edge_y.append(None)
|
|
117
|
+
# if 3d add the extra none to close the edge
|
|
118
|
+
if self.dim == 3:
|
|
119
|
+
edge_z.append(None)
|
|
120
|
+
else:
|
|
121
|
+
source = self.nodes[edge["from"]]["coords"]
|
|
122
|
+
target = self.nodes[edge["to"]]["coords"]
|
|
123
|
+
|
|
124
|
+
edge_x += [source[0], target[0], None]
|
|
125
|
+
edge_y += [source[1], target[1], None]
|
|
126
|
+
|
|
127
|
+
if self.dim == 3:
|
|
128
|
+
edge_z += [source[2], target[2], None]
|
|
129
|
+
|
|
130
|
+
# add text and hover display
|
|
131
|
+
hover = f"{edge['from']} → {edge['to']}"
|
|
132
|
+
metrics = {k: v for k, v in edge.items() if k not in ("from", "to", "curve")}
|
|
133
|
+
if metrics:
|
|
134
|
+
hover += "<br>" + "<br>".join(f"{k}: {v}" for k, v in metrics.items())
|
|
135
|
+
edge_text.append(hover)
|
|
136
|
+
|
|
137
|
+
if self.dim == 2:
|
|
138
|
+
# ceate the plot in 2d
|
|
139
|
+
node_trace = go.Scatter(
|
|
140
|
+
x=node_x,
|
|
141
|
+
y=node_y,
|
|
142
|
+
mode="markers",
|
|
143
|
+
hoverinfo="text",
|
|
144
|
+
text=text,
|
|
145
|
+
marker=dict(
|
|
146
|
+
size=self.iconSize,
|
|
147
|
+
color=colors,
|
|
148
|
+
colorscale="Viridis",
|
|
149
|
+
showscale=True
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
edge_trace = go.Scatter(
|
|
154
|
+
x=edge_x,
|
|
155
|
+
y=edge_y,
|
|
156
|
+
line=dict(width=2, color="#888"),
|
|
157
|
+
hoverinfo="text",
|
|
158
|
+
text=edge_text,
|
|
159
|
+
mode="lines"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
elif self.dim == 3:
|
|
163
|
+
# create the plot in 3d
|
|
164
|
+
node_trace = go.Scatter3d(
|
|
165
|
+
x=node_x,
|
|
166
|
+
y=node_y,
|
|
167
|
+
z=node_z,
|
|
168
|
+
mode="markers",
|
|
169
|
+
hoverinfo="text",
|
|
170
|
+
text=text,
|
|
171
|
+
marker=dict(
|
|
172
|
+
size=self.iconSize,
|
|
173
|
+
color=colors,
|
|
174
|
+
colorscale="Viridis",
|
|
175
|
+
showscale=True
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
edge_trace = go.Scatter3d(
|
|
180
|
+
x=edge_x,
|
|
181
|
+
y=edge_y,
|
|
182
|
+
z=edge_z,
|
|
183
|
+
line=dict(width=1, color="#888"),
|
|
184
|
+
hoverinfo="text",
|
|
185
|
+
text=edge_text,
|
|
186
|
+
mode="lines",
|
|
187
|
+
opacity=0.6
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# create the plotly figure
|
|
191
|
+
fig = go.Figure(data=[edge_trace, node_trace])
|
|
192
|
+
# render earth / sphere in 3d
|
|
193
|
+
if self.dim == 3 and displayEarth:
|
|
194
|
+
try:
|
|
195
|
+
import numpy as np
|
|
196
|
+
R = 6369.9 # sphere radius
|
|
197
|
+
u = np.linspace(0, 2 * np.pi, 50) # azimuthal angle
|
|
198
|
+
v = np.linspace(0, np.pi, 50) # polar angle
|
|
199
|
+
u, v = np.meshgrid(u, v)
|
|
200
|
+
|
|
201
|
+
# Cartesian coordinates
|
|
202
|
+
x = R * np.cos(u) * np.sin(v)
|
|
203
|
+
y = R * np.sin(u) * np.sin(v)
|
|
204
|
+
z = R * np.cos(v)
|
|
205
|
+
except ImportError:
|
|
206
|
+
raise ImportError("numpy is required to display the earth")
|
|
207
|
+
|
|
208
|
+
sphere_surface = go.Surface(
|
|
209
|
+
x=x, y=y, z=z,
|
|
210
|
+
colorscale='Blues',
|
|
211
|
+
opacity=1,
|
|
212
|
+
showscale=False,
|
|
213
|
+
hoverinfo='skip'
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
fig.add_trace(sphere_surface)
|
|
217
|
+
|
|
218
|
+
fig.update_layout(title="Interactive Graph", showlegend=False, hovermode="closest")
|
|
219
|
+
fig.show()
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def degreesToCartesian3D(coords):
|
|
223
|
+
try:
|
|
224
|
+
import torch
|
|
225
|
+
C = torch.tensor(coords)
|
|
226
|
+
if C.dim() == 1:
|
|
227
|
+
C = C.unsqueeze(0)
|
|
228
|
+
R = 6371.0
|
|
229
|
+
lat = torch.deg2rad(C[:, 0])
|
|
230
|
+
lng = torch.deg2rad(C[:, 1])
|
|
231
|
+
x = R * torch.cos(lat) * torch.cos(lng)
|
|
232
|
+
y = R * torch.cos(lat) * torch.sin(lng)
|
|
233
|
+
z = R * torch.sin(lat)
|
|
234
|
+
return list(torch.stack((x, y, z), dim=1).numpy())
|
|
235
|
+
except ImportError:
|
|
236
|
+
import math
|
|
237
|
+
R = 6371.0
|
|
238
|
+
output = []
|
|
239
|
+
for lat, lng in coords:
|
|
240
|
+
lat = math.radians(lat)
|
|
241
|
+
lng = math.radians(lng)
|
|
242
|
+
x = R * math.cos(lat) * math.cos(lng)
|
|
243
|
+
y = R * math.cos(lat) * math.sin(lng)
|
|
244
|
+
z = R * math.sin(lat)
|
|
245
|
+
output.append([x, y, z])
|
|
246
|
+
return output
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def curvedEdges(start, end, R=6371.0, H=0.05, n=20):
|
|
250
|
+
try:
|
|
251
|
+
# if torch and np are available calc vectorized graeter circle curves
|
|
252
|
+
import numpy as np
|
|
253
|
+
import torch
|
|
254
|
+
|
|
255
|
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
256
|
+
|
|
257
|
+
start_np = np.array(start, dtype=np.float32)
|
|
258
|
+
end_np = np.array(end, dtype=np.float32)
|
|
259
|
+
|
|
260
|
+
start = torch.tensor(start_np, device=device)
|
|
261
|
+
end = torch.tensor(end_np, device=device)
|
|
262
|
+
start = start.float()
|
|
263
|
+
end = end.float()
|
|
264
|
+
|
|
265
|
+
# normalize to sphere
|
|
266
|
+
start_norm = R * start / start.norm(dim=1, keepdim=True)
|
|
267
|
+
end_norm = R * end / end.norm(dim=1, keepdim=True)
|
|
268
|
+
|
|
269
|
+
# compute angle between vectors
|
|
270
|
+
dot = (start_norm * end_norm).sum(dim=1, keepdim=True) / (R**2)
|
|
271
|
+
dot = torch.clamp(dot, -1.0, 1.0)
|
|
272
|
+
theta = torch.acos(dot).unsqueeze(2) # shape: (num_edges,1,1)
|
|
273
|
+
|
|
274
|
+
# linear interpolation along great circle
|
|
275
|
+
t = torch.linspace(0, 1, n, device=device).view(1, n, 1)
|
|
276
|
+
one_minus_t = 1 - t
|
|
277
|
+
sin_theta = torch.sin(theta)
|
|
278
|
+
sin_theta[sin_theta == 0] = 1e-6
|
|
279
|
+
|
|
280
|
+
factor_start = torch.sin(one_minus_t * theta) / sin_theta
|
|
281
|
+
factor_end = torch.sin(t * theta) / sin_theta
|
|
282
|
+
|
|
283
|
+
curve = factor_start * start_norm.unsqueeze(1) + factor_end * end_norm.unsqueeze(1)
|
|
284
|
+
|
|
285
|
+
# normalize to radius
|
|
286
|
+
curve = R * curve / curve.norm(dim=2, keepdim=True)
|
|
287
|
+
|
|
288
|
+
# apply radial lift at curve center using sin weight
|
|
289
|
+
weight = torch.sin(torch.pi * t) # 0 at endpoints, 1 at center
|
|
290
|
+
curve = curve * (1 + H * weight)
|
|
291
|
+
|
|
292
|
+
return curve
|
|
293
|
+
except ImportError:
|
|
294
|
+
# fallback to calculating quadratic bezier curves with math
|
|
295
|
+
import math
|
|
296
|
+
curves_all = []
|
|
297
|
+
|
|
298
|
+
def multiply_vec(vec, factor):
|
|
299
|
+
return [factor * x for x in vec]
|
|
300
|
+
|
|
301
|
+
def add_vec(*vecs):
|
|
302
|
+
return [sum(items) for items in zip(*vecs)]
|
|
303
|
+
|
|
304
|
+
for startP, endP in zip(start, end):
|
|
305
|
+
mid = [(s + e) / 2 for s, e in zip(startP, endP)]
|
|
306
|
+
norm = math.sqrt(sum(c ** 2 for c in mid))
|
|
307
|
+
mid_proj = [R * c / norm for c in mid]
|
|
308
|
+
mid_arch = [c * (1 + H) for c in mid_proj]
|
|
309
|
+
|
|
310
|
+
curve = []
|
|
311
|
+
for i in range(n):
|
|
312
|
+
t_i = i / (n - 1)
|
|
313
|
+
one_minus_t = 1 - t_i
|
|
314
|
+
point = add_vec(
|
|
315
|
+
multiply_vec(startP, one_minus_t ** 2),
|
|
316
|
+
multiply_vec(mid_arch, 2 * one_minus_t * t_i),
|
|
317
|
+
multiply_vec(endP, t_i ** 2)
|
|
318
|
+
)
|
|
319
|
+
curve.append(point)
|
|
320
|
+
|
|
321
|
+
curves_all.append(curve)
|
|
322
|
+
|
|
323
|
+
return curves_all
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multimodalrouter
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: A graph-based routing library for dynamic routing.
|
|
5
5
|
Author-email: Tobias Karusseit <karusseittobi@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -19,6 +19,7 @@ Project-URL: Repository, https://github.com/K-T0BIAS/MultiModalRouter
|
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE.md
|
|
22
|
+
License-File: NOTICE.md
|
|
22
23
|
Requires-Dist: colorama>=0.4.6
|
|
23
24
|
Requires-Dist: dill>=0.4.0
|
|
24
25
|
Requires-Dist: filelock>=3.19.1
|
|
@@ -45,6 +46,9 @@ Provides-Extra: torch
|
|
|
45
46
|
Requires-Dist: torch>=2.8.0; extra == "torch"
|
|
46
47
|
Provides-Extra: dev
|
|
47
48
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
49
|
+
Requires-Dist: plotly>=6.3.0; extra == "dev"
|
|
50
|
+
Provides-Extra: plotly
|
|
51
|
+
Requires-Dist: plotly>=6.3.0; extra == "plotly"
|
|
48
52
|
Dynamic: license-file
|
|
49
53
|
|
|
50
54
|
# Multi Modal Router
|
|
@@ -95,6 +99,12 @@ The graph can be build from any data aslong as the required fields are present (
|
|
|
95
99
|
|
|
96
100
|

|
|
97
101
|
|
|
102
|
+
## graph visualizations
|
|
103
|
+
|
|
104
|
+
Use the build-in [visualization](./docs/visualization.md) tool to plot any `2D` or `3D` Graph.
|
|
105
|
+
|
|
106
|
+

|
|
107
|
+
|
|
98
108
|
## Important considerations for your usecase
|
|
99
109
|
|
|
100
110
|
Depending on your usecase and datasets some features may not be usable see solutions below
|
|
@@ -116,4 +126,6 @@ Depending on your usecase and datasets some features may not be usable see solut
|
|
|
116
126
|
|
|
117
127
|
[see here](./LICENSE.md)
|
|
118
128
|
|
|
129
|
+
[dependencies](./NOTICE.md)
|
|
130
|
+
|
|
119
131
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
multimodalrouter/__init__.py,sha256=Kva-wGkYdCi4GveSLe6emkTjd13C3K_ynhihADTQxZY,256
|
|
2
|
+
multimodalrouter/graph/__init__.py,sha256=zvKvSpZ8PbuOpG6DL14CJPV34ZLYderNJV4Amw1jsLI,150
|
|
3
|
+
multimodalrouter/graph/dataclasses.py,sha256=-vCA3VRGBeLTEOZpYyYrGM5O24jnGRS36eGwZWDKISQ,4464
|
|
4
|
+
multimodalrouter/graph/graph.py,sha256=k20ynHXPwVdZcjjp8a3dzCOuEHBPtEjnpauHtcvry9Y,23314
|
|
5
|
+
multimodalrouter/graphics/__init__.py,sha256=F_KttIwUySzJs_C9OCDR2Zptu-tsuaF3gauSZyZyimo,56
|
|
6
|
+
multimodalrouter/graphics/graphicsWrapper.py,sha256=8k6XidKINOVpe7sPQqjGkRNVsYgS78_ex4l-4iKCFGk,11850
|
|
7
|
+
multimodalrouter/router/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
multimodalrouter/router/build.py,sha256=jrGcFyVS7-Qg6CXAVVwiXM_krLBHBoRH85Slknmutns,2843
|
|
9
|
+
multimodalrouter/router/route.py,sha256=5RZGFgrTYm930RJX6i26d8f8_k7CGnp5lKZJBM1gcKk,1956
|
|
10
|
+
multimodalrouter/utils/__init__.py,sha256=jsZ7cBB-9otxzMiN9FVviGuknxnOPmaG2RBbZuiezeg,53
|
|
11
|
+
multimodalrouter/utils/preprocessor.py,sha256=45ya0cg0PGCV3YMk680_HZUve1QGJ7JHPSHoRvYdleY,6333
|
|
12
|
+
multimodalrouter-0.1.5.dist-info/licenses/LICENSE.md,sha256=CRtvaQsLnzHvSIzusV5sHHw-e8w8gytXq8R7AP1GBmE,1092
|
|
13
|
+
multimodalrouter-0.1.5.dist-info/licenses/NOTICE.md,sha256=Fv3AK0KmlZGgmf0_4yM8ZtOTbv2mIfQVHQkZ4hFZXb8,3313
|
|
14
|
+
multimodalrouter-0.1.5.dist-info/METADATA,sha256=gjV64EK-1p_dUxfW02Y0bJqEbzpvAx4GNU6xvQ74vek,6333
|
|
15
|
+
multimodalrouter-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
+
multimodalrouter-0.1.5.dist-info/entry_points.txt,sha256=vp177Z2KMWPGJkS_dVpX05LVtBYssdlyYhTCG0kYmjo,138
|
|
17
|
+
multimodalrouter-0.1.5.dist-info/top_level.txt,sha256=4RYMG9hyl8mDNJ_gTrlh8QdYjZNXLDBzFVK1PcTpAYg,17
|
|
18
|
+
multimodalrouter-0.1.5.dist-info/RECORD,,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Dependencies and Licenses
|
|
2
|
+
|
|
3
|
+
This project `MultiModalRouter` depends on the following libraries. All licenses are permissive and compatible with MIT licensing for this project.
|
|
4
|
+
|
|
5
|
+
| Package | Version | License | License Link |
|
|
6
|
+
|---------|---------|---------|--------------|
|
|
7
|
+
| colorama | >=0.4.6 | BSD 3-Clause | [License](https://github.com/tartley/colorama/blob/master/LICENSE) |
|
|
8
|
+
| dill | >=0.4.0 | BSD | [License](https://github.com/uqfoundation/dill/blob/main/LICENSE) |
|
|
9
|
+
| filelock | >=3.19.1 | MIT | [License](https://github.com/tox-dev/py-filelock/blob/main/LICENSE) |
|
|
10
|
+
| fsspec | >=2025.9.0 | Apache 2.0 | [License](https://github.com/fsspec/filesystem_spec/blob/main/LICENSE) |
|
|
11
|
+
| Jinja2 | >=3.1.6 | BSD-3-Clause | [License](https://github.com/pallets/jinja/blob/main/LICENSE) |
|
|
12
|
+
| MarkupSafe | >=3.0.2 | BSD-3-Clause | [License](https://github.com/pallets/markupsafe/blob/main/LICENSE) |
|
|
13
|
+
| mpmath | >=1.3.0 | BSD | [License](https://github.com/fredrik-johansson/mpmath/blob/master/LICENSE) |
|
|
14
|
+
| networkx | >=3.5 | BSD | [License](https://github.com/networkx/networkx/blob/main/LICENSE.txt) |
|
|
15
|
+
| numpy | >=2.3.3 | BSD | [License](https://github.com/numpy/numpy/blob/main/LICENSE.txt) |
|
|
16
|
+
| pandas | >=2.3.2 | BSD-3-Clause | [License](https://github.com/pandas-dev/pandas/blob/main/LICENSE) |
|
|
17
|
+
| parquet | >=1.3.1 | Apache 2.0 | [License](https://github.com/urschrei/parquet-python/blob/master/LICENSE) |
|
|
18
|
+
| ply | >=3.11 | BSD | [License](https://github.com/dabeaz/ply/blob/master/LICENSE.txt) |
|
|
19
|
+
| pyarrow | >=21.0.0 | Apache 2.0 | [License](https://github.com/apache/arrow/blob/master/LICENSE) |
|
|
20
|
+
| python-dateutil | >=2.9.0.post0 | BSD | [License](https://github.com/dateutil/dateutil/blob/master/LICENSE.txt) |
|
|
21
|
+
| pytz | >=2025.2 | MIT | [License](https://github.com/stub42/pytz/blob/master/LICENSE) |
|
|
22
|
+
| setuptools | >=80.9.0 | MIT | [License](https://github.com/pypa/setuptools/blob/main/LICENSE) |
|
|
23
|
+
| six | >=1.17.0 | MIT | [License](https://github.com/benjaminp/six/blob/master/LICENSE) |
|
|
24
|
+
| sympy | >=1.14.0 | BSD | [License](https://github.com/sympy/sympy/blob/master/LICENSE) |
|
|
25
|
+
| thriftpy2 | >=0.5.3 | MIT | [License](https://github.com/Thriftpy/thriftpy2/blob/master/LICENSE) |
|
|
26
|
+
| tqdm | >=4.67.1 | MPL 2.0 | [License](https://github.com/tqdm/tqdm/blob/master/LICENSE) |
|
|
27
|
+
| typing_extensions | >=4.15.0 | PSF | [License](https://github.com/python/typing_extensions/blob/main/LICENSE) |
|
|
28
|
+
| tzdata | >=2025.2 | Public Domain | [License](https://github.com/python/tzdata) |
|
|
29
|
+
|
|
30
|
+
## Optional Dependencies
|
|
31
|
+
|
|
32
|
+
| Package | Version | License | License Link |
|
|
33
|
+
|---------|---------|---------|--------------|
|
|
34
|
+
| torch | >=2.8.0 | BSD | [License](https://github.com/pytorch/pytorch/blob/master/LICENSE) |
|
|
35
|
+
| plotly | >=6.3.0 | MIT | [License](https://github.com/plotly/plotly.py/blob/master/LICENSE) |
|
|
36
|
+
| pytest | >=8.0 | MIT | [License](https://github.com/pytest-dev/pytest/blob/main/LICENSE) |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
### Notes
|
|
41
|
+
|
|
42
|
+
1. All packages listed above are permissively licensed (MIT, BSD, Apache 2.0, or Public Domain), so they are compatible with MIT licensing for this project.
|
|
43
|
+
2. If distributing this library, include this `DEPENDENCIES.md` file and your own MIT license file to give proper attribution.
|
|
44
|
+
3. Optional dependencies should be listed in documentation or `pyproject.toml` extras.
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
multimodalrouter/__init__.py,sha256=3O9ft058b5x09ORYD8Luw1qyEYoAx4kFPMjBs2HkfBM,238
|
|
2
|
-
multimodalrouter/graph/__init__.py,sha256=FSIR6ZN-IM0r-NRcM81AiPn2FVe9fA8rkc9r2746Lr4,142
|
|
3
|
-
multimodalrouter/graph/dataclasses.py,sha256=Bmcg3NFfFyPJS2sJO-IO0TnNCJ_pCG_n9lfJKVvFjxI,3535
|
|
4
|
-
multimodalrouter/graph/graph.py,sha256=UPFcFIxhlJ1gfpHG8mPWAGiRjxVgEy_VTv16AneSjYM,19944
|
|
5
|
-
multimodalrouter/router/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
multimodalrouter/router/build.py,sha256=jrGcFyVS7-Qg6CXAVVwiXM_krLBHBoRH85Slknmutns,2843
|
|
7
|
-
multimodalrouter/router/route.py,sha256=5RZGFgrTYm930RJX6i26d8f8_k7CGnp5lKZJBM1gcKk,1956
|
|
8
|
-
multimodalrouter/utils/__init__.py,sha256=jsZ7cBB-9otxzMiN9FVviGuknxnOPmaG2RBbZuiezeg,53
|
|
9
|
-
multimodalrouter/utils/preprocessor.py,sha256=45ya0cg0PGCV3YMk680_HZUve1QGJ7JHPSHoRvYdleY,6333
|
|
10
|
-
multimodalrouter-0.1.3.dist-info/licenses/LICENSE.md,sha256=CRtvaQsLnzHvSIzusV5sHHw-e8w8gytXq8R7AP1GBmE,1092
|
|
11
|
-
multimodalrouter-0.1.3.dist-info/METADATA,sha256=5nQMehbzF0Hg6LUBHdTGUiwRpdO5ss7LXjbpsM98Y2w,5971
|
|
12
|
-
multimodalrouter-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
-
multimodalrouter-0.1.3.dist-info/entry_points.txt,sha256=vp177Z2KMWPGJkS_dVpX05LVtBYssdlyYhTCG0kYmjo,138
|
|
14
|
-
multimodalrouter-0.1.3.dist-info/top_level.txt,sha256=4RYMG9hyl8mDNJ_gTrlh8QdYjZNXLDBzFVK1PcTpAYg,17
|
|
15
|
-
multimodalrouter-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|