multimodalrouter 0.1.4__tar.gz → 0.1.5__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.

Potentially problematic release.


This version of multimodalrouter might be problematic. Click here for more details.

Files changed (42) hide show
  1. multimodalrouter-0.1.5/NOTICE.md +44 -0
  2. {multimodalrouter-0.1.4/src/multiModalRouter.egg-info → multimodalrouter-0.1.5}/PKG-INFO +13 -3
  3. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/README.md +8 -2
  4. multimodalrouter-0.1.5/docs/FlightPathPlot.png +0 -0
  5. multimodalrouter-0.1.5/docs/examples/flightRouter/__pycache__/plot.cpython-313.pyc +0 -0
  6. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/docs/examples/flightRouter/main.py +7 -6
  7. multimodalrouter-0.1.5/docs/examples/flightRouter/plot.py +25 -0
  8. multimodalrouter-0.1.5/docs/examples/mazePathfinder/__pycache__/main.cpython-313.pyc +0 -0
  9. multimodalrouter-0.1.5/docs/examples/mazePathfinder/__pycache__/plot.cpython-313.pyc +0 -0
  10. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/docs/examples/mazePathfinder/data/createMaze.py +15 -3
  11. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/docs/examples/mazePathfinder/main.py +21 -11
  12. multimodalrouter-0.1.5/docs/examples/mazePathfinder/plot.py +32 -0
  13. multimodalrouter-0.1.5/docs/visualization.md +108 -0
  14. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/pyproject.toml +5 -3
  15. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5/src/multiModalRouter.egg-info}/PKG-INFO +13 -3
  16. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multiModalRouter.egg-info/SOURCES.txt +10 -0
  17. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multiModalRouter.egg-info/requires.txt +4 -0
  18. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multimodalrouter/graph/dataclasses.py +1 -0
  19. multimodalrouter-0.1.5/src/multimodalrouter/graphics/__init__.py +1 -0
  20. multimodalrouter-0.1.5/src/multimodalrouter/graphics/graphicsWrapper.py +323 -0
  21. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/LICENSE.md +0 -0
  22. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/MANIFEST.in +0 -0
  23. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/docs/cli.md +0 -0
  24. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/docs/examples/demoData.csv +0 -0
  25. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/docs/examples/flightRouter/data/fullDataset.csv +0 -0
  26. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/docs/examples/mazePathfinder/data/maze.csv +0 -0
  27. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/docs/graph.md +0 -0
  28. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/docs/installation.md +0 -0
  29. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/docs/solvedMaze1.png +0 -0
  30. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/docs/utils.md +0 -0
  31. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/setup.cfg +0 -0
  32. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multiModalRouter.egg-info/dependency_links.txt +0 -0
  33. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multiModalRouter.egg-info/entry_points.txt +0 -0
  34. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multiModalRouter.egg-info/top_level.txt +0 -0
  35. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multimodalrouter/__init__.py +0 -0
  36. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multimodalrouter/graph/__init__.py +0 -0
  37. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multimodalrouter/graph/graph.py +0 -0
  38. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multimodalrouter/router/__init__.py +0 -0
  39. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multimodalrouter/router/build.py +0 -0
  40. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multimodalrouter/router/route.py +0 -0
  41. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multimodalrouter/utils/__init__.py +0 -0
  42. {multimodalrouter-0.1.4 → multimodalrouter-0.1.5}/src/multimodalrouter/utils/preprocessor.py +0 -0
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multimodalrouter
3
- Version: 0.1.4
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,14 +46,15 @@ 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
51
55
 
52
56
  The Multi Modal Router is a graph-based routing engine that allows you to build and query any hub-based network. It supports multiple transport modes like driving, flying, or shipping, and lets you optimize routes by distance, time, or custom metrics. It can be expanded to any n-dimensional space making it versatile in any coordinate space
53
57
 
54
- > NEWS: v0.1.3 now on pypi ([installation guide](./docs/installation.md))
55
-
56
58
  > NOTE: This project is a work in progress and features might be added and or changed
57
59
 
58
60
  # In depth Documentation
@@ -97,6 +99,12 @@ The graph can be build from any data aslong as the required fields are present (
97
99
 
98
100
  ![example from the maze solver](./docs/solvedMaze1.png)
99
101
 
102
+ ## graph visualizations
103
+
104
+ Use the build-in [visualization](./docs/visualization.md) tool to plot any `2D` or `3D` Graph.
105
+
106
+ ![example plot of flight paths](./docs/FlightPathPlot.png)
107
+
100
108
  ## Important considerations for your usecase
101
109
 
102
110
  Depending on your usecase and datasets some features may not be usable see solutions below
@@ -118,4 +126,6 @@ Depending on your usecase and datasets some features may not be usable see solut
118
126
 
119
127
  [see here](./LICENSE.md)
120
128
 
129
+ [dependencies](./NOTICE.md)
130
+
121
131
 
@@ -2,8 +2,6 @@
2
2
 
3
3
  The Multi Modal Router is a graph-based routing engine that allows you to build and query any hub-based network. It supports multiple transport modes like driving, flying, or shipping, and lets you optimize routes by distance, time, or custom metrics. It can be expanded to any n-dimensional space making it versatile in any coordinate space
4
4
 
5
- > NEWS: v0.1.3 now on pypi ([installation guide](./docs/installation.md))
6
-
7
5
  > NOTE: This project is a work in progress and features might be added and or changed
8
6
 
9
7
  # In depth Documentation
@@ -48,6 +46,12 @@ The graph can be build from any data aslong as the required fields are present (
48
46
 
49
47
  ![example from the maze solver](./docs/solvedMaze1.png)
50
48
 
49
+ ## graph visualizations
50
+
51
+ Use the build-in [visualization](./docs/visualization.md) tool to plot any `2D` or `3D` Graph.
52
+
53
+ ![example plot of flight paths](./docs/FlightPathPlot.png)
54
+
51
55
  ## Important considerations for your usecase
52
56
 
53
57
  Depending on your usecase and datasets some features may not be usable see solutions below
@@ -69,4 +73,6 @@ Depending on your usecase and datasets some features may not be usable see solut
69
73
 
70
74
  [see here](./LICENSE.md)
71
75
 
76
+ [dependencies](./NOTICE.md)
77
+
72
78
 
@@ -5,6 +5,7 @@
5
5
  from multimodalrouter import RouteGraph
6
6
  import os
7
7
 
8
+
8
9
  def main():
9
10
  path = os.path.dirname(os.path.abspath(__file__))
10
11
  # initialize the graph
@@ -17,21 +18,21 @@ def main():
17
18
  # build the graph
18
19
  graph.build()
19
20
  # set start and end points
20
- start = [60.866699,-162.272996] # Atmautluak Airport
21
- end = [60.872747,-162.5247] #Kasigluk Airport
21
+ start = [60.866699, -162.272996] # Atmautluak Airport
22
+ end = [60.872747, -162.5247] # Kasigluk Airport
22
23
 
23
24
  start_hub = graph.findClosestHub(["airport"], start) # find the hubs
24
25
  end_hub = graph.findClosestHub(["airport"], end)
25
26
  # find the route
26
27
  route = graph.find_shortest_path(
27
- start_hub.id,
28
+ start_hub.id,
28
29
  end_hub.id,
29
- allowed_modes=["plane","car"],
30
+ allowed_modes=["plane", "car"],
30
31
  verbose=True
31
- )
32
+ )
32
33
  # print the route
33
34
  print(route.flatPath if route else "No route found")
34
35
 
35
36
 
36
37
  if __name__ == "__main__":
37
- main()
38
+ main()
@@ -0,0 +1,25 @@
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 multimodalrouter import RouteGraph
7
+ from multimodalrouter.graphics import GraphDisplay
8
+ import os
9
+
10
+ if __name__ == "__main__":
11
+ path = os.path.dirname(os.path.abspath(__file__))
12
+ graph = RouteGraph(
13
+ maxDistance=50,
14
+ transportModes={"airport": "fly", },
15
+ dataPaths={"airport": os.path.join(path, "data", "fullDataset.csv")},
16
+ compressed=False,
17
+ )
18
+
19
+ graph.build()
20
+ display = GraphDisplay(graph)
21
+ display.display(
22
+ displayEarth=True,
23
+ nodeTransform=GraphDisplay.degreesToCartesian3D,
24
+ edgeTransform=GraphDisplay.curvedEdges
25
+ )
@@ -5,15 +5,17 @@
5
5
  import random
6
6
  import pandas as pd
7
7
 
8
+
8
9
  # simple cell class for the maze
9
10
  class Cell:
10
11
  def __init__(self, x, y):
11
- self.id = f"cell-{x,y}"
12
+ self.id = f"cell-{x, y}"
12
13
  self.x = x
13
14
  self.y = y
14
15
  self.visited = False
15
16
  self.connected = []
16
17
 
18
+
17
19
  def main():
18
20
  # init a 10x10 maze
19
21
  mazeHeight = 10
@@ -53,7 +55,15 @@ def main():
53
55
  cellStack.pop()
54
56
 
55
57
  # init the dataframe
56
- data = pd.DataFrame(columns=["source", "destination", "distance", "source_lat", "source_lng", "destination_lat", "destination_lng"])
58
+ data = pd.DataFrame(columns=[
59
+ "source",
60
+ "destination",
61
+ "distance",
62
+ "source_lat",
63
+ "source_lng",
64
+ "destination_lat",
65
+ "destination_lng"
66
+ ])
57
67
  # add the edges to the dataframe
58
68
  for cell in cells:
59
69
  for neighbor in cell.connected:
@@ -61,4 +71,6 @@ def main():
61
71
  # save the dataframe
62
72
  data.to_csv("docs/examples/mazePathfinder/data/maze.csv", index=False)
63
73
 
64
- if __name__ == "__main__": main()
74
+
75
+ if __name__ == "__main__":
76
+ main()
@@ -6,22 +6,26 @@ from multimodalrouter import RouteGraph
6
6
  import os
7
7
  import pandas as pd
8
8
 
9
+
9
10
  def main():
10
11
  try:
11
12
  import matplotlib.pyplot as plt
12
13
  except ImportError:
13
14
  raise ImportError("matplotlib is not installed. Please install matplotlib to use this example.")
14
-
15
+
15
16
  path = os.path.dirname(os.path.abspath(__file__))
16
17
  # init the maze df for the plot
17
18
  mazeDf = pd.read_csv(os.path.join(path, "data", "maze.csv"))
18
19
  # init the plot
19
- plt.figure(figsize=(10,10))
20
+ plt.figure(figsize=(10, 10))
20
21
  # draw the maze
22
+ # draw the maze (grid lines)
21
23
  for _, row in mazeDf.iterrows():
22
- plt.plot([row.source_lat, row.destination_lat],
23
- [row.source_lng, row.destination_lng],
24
- "k-") # black line for edge
24
+ plt.plot(
25
+ [row.source_lng, row.destination_lng], # x = "lng" column
26
+ [row.source_lat, row.destination_lat], # y = "lat" column
27
+ "k-"
28
+ )
25
29
 
26
30
  # initialize the graph
27
31
  graph = RouteGraph(
@@ -35,7 +39,7 @@ def main():
35
39
  graph.build()
36
40
  # find the shortest route
37
41
  route = graph.find_shortest_path(
38
- start_id="cell-(0, 0)",
42
+ start_id="cell-(0, 0)",
39
43
  end_id="cell-(0, 9)",
40
44
  allowed_modes=["walk"],
41
45
  verbose=True,
@@ -49,11 +53,17 @@ def main():
49
53
  if s_prev is not None:
50
54
  h1 = graph.getHubById(s_prev)
51
55
  h2 = graph.getHubById(s)
52
- plt.plot([h1.coords[0], h2.coords[0]],
53
- [h1.coords[1], h2.coords[1]],
54
- "b-")
56
+ # Swap coords so x=column, y=row
57
+ plt.plot(
58
+ [h1.coords[1], h2.coords[1]], # x-axis
59
+ [h1.coords[0], h2.coords[0]], # y-axis
60
+ "b-"
61
+ )
55
62
  s_prev = s
63
+
56
64
  # display the plot
57
65
  plt.show()
58
-
59
- if __name__ == "__main__": main()
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
@@ -0,0 +1,32 @@
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 multimodalrouter import RouteGraph
7
+ from multimodalrouter.graphics import GraphDisplay
8
+ import os
9
+
10
+
11
+ # custom transform to make lat lng to x y (-> lng lat)
12
+ def NodeTransform(coords):
13
+ for coord in coords:
14
+ yield list((coord[0], coord[1]))
15
+
16
+
17
+ if __name__ == "__main__":
18
+ path = os.path.dirname(os.path.abspath(__file__))
19
+ # initialize the graph
20
+ graph = RouteGraph(
21
+ maxDistance=50,
22
+ transportModes={"cell": "walk", },
23
+ dataPaths={"cell": os.path.join(path, "data", "maze.csv")},
24
+ compressed=False,
25
+ drivingEnabled=False
26
+ )
27
+
28
+ graph.build()
29
+ # init the display
30
+ display = GraphDisplay(graph)
31
+ # display the graph (uses the transform to swap lat lng to x y)
32
+ display.display(nodeTransform=NodeTransform)
@@ -0,0 +1,108 @@
1
+ [HOME](../README.md)
2
+
3
+ # Graph Plotting
4
+
5
+ Using the build-in graph plotting tool you can [plotly](https://plotly.com/python/) plot any graph in `2D` or `3D`, while defining [transformations](#transformations) for your coordiante space or even path curvature etc.
6
+
7
+ ## GraphDisplay
8
+
9
+ ```python
10
+ def __init__(
11
+ self,
12
+ graph: RouteGraph,
13
+ name: str = "Graph",
14
+ iconSize: int = 10
15
+ ) -> None:
16
+ ```
17
+
18
+ #### args:
19
+ - graph: RouteDisplay = the graph instance you want to plot
20
+ - name: str = (not in use at the moment)
21
+ - iconSize: int = the size of the nodes in the plot
22
+
23
+ #### example
24
+
25
+ ```
26
+ gd = GraphDisplay(myGraphInstance)
27
+ ```
28
+
29
+ [flight path CODE example on sphere](./examples/flightRouter/plot.py)
30
+
31
+
32
+ ### display()
33
+
34
+ The display function will collect data from your Graph and create a [plotly](https://plotly.com/python/) plot from it.
35
+
36
+ ```python
37
+ def display(
38
+ self,
39
+ nodeTransform=None,
40
+ edgeTransform=None,
41
+ displayEarth=False
42
+ ):
43
+ ```
44
+
45
+ #### args:
46
+
47
+ - nodeTransform: function = a [transformation](#transformations) function that transformes all node coordinates
48
+ - edgeTransform: funstion = a function that [transformes](#transformations) all your edges
49
+ - displayEarth: bool = if True -> will display a sphere that (roughly) matches earth
50
+
51
+ #### example:
52
+
53
+ this call will create the plot for your graph while mapping all coords onto the surface of the earth
54
+
55
+ ```python
56
+ gd.display(
57
+ nodeTransform = gd.degreesToCartesian3D,
58
+ displayEarth: True
59
+ )
60
+ ```
61
+
62
+ ### transformations
63
+
64
+ #### base function style
65
+
66
+ IF you want to implement your own transformation function note that the call must adhere to the following parameters:
67
+
68
+ ```python
69
+ def customNodeTrandsform(coords: list[list[float]]):
70
+ return list[list[float]]
71
+
72
+ def customEdgeTransform(start: list[list[float]], end: list[list[float]]):
73
+ return list[list[list[float]]]
74
+ ```
75
+
76
+ #### args
77
+
78
+ - coords: list[list[float]] = a nested list of coordinates for all nodes
79
+ - start: list[list[float]] = a nested list of all start coordinates
80
+ - end: list[list[float]] = a nested list of all end coordinates
81
+
82
+ #### returns:
83
+
84
+ - list[list[float]] = a list of all transformed node coordinates
85
+ - list[list[list[float]]] = a list of curves whare each curve / edge can have n points defining it
86
+
87
+ ### build-in Node Transforms:
88
+
89
+ #### degreesToCartesian3D
90
+
91
+ ```python
92
+ @staticmethod
93
+ def degreesToCartesian3D(coords):
94
+ ```
95
+ This function maps any valid `2D` coordinates (best if in degrees) to spherical coords on the surface of earth
96
+
97
+ ### build-in Edge Transformations
98
+
99
+ ```python
100
+ @staticmethod
101
+ def curvedEdges(start, end, R=6371.0, H=0.05, n=20):
102
+ ```
103
+
104
+ curves edges for coordinates on spheres (here earth) so that the edges curve along the spherical surface with a curvature that places the midpoint of the curve at $H \dot R$ above the surface. (great for displaying flights).
105
+
106
+ If torch is installed this will use great-circle distance for the curves
107
+
108
+ > Note if torch is not installed this will fall back to using `math` with quadratic bezier curves -> some curves may end up inside the sphere to bezier inaccuracy
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "multimodalrouter"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "A graph-based routing library for dynamic routing."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE.md" }
@@ -46,12 +46,14 @@ Repository = "https://github.com/K-T0BIAS/MultiModalRouter"
46
46
  [project.optional-dependencies]
47
47
  torch = ["torch>=2.8.0"]
48
48
  dev = [
49
- "pytest>=8.0"
49
+ "pytest>=8.0",
50
+ "plotly>=6.3.0"
50
51
  ]
52
+ plotly = ["plotly>=6.3.0"]
51
53
 
52
54
  [tool.setuptools]
53
55
  package-dir = {"" = "src"}
54
- packages = ["multimodalrouter", "multimodalrouter.graph", "multimodalrouter.router", "multimodalrouter.utils"]
56
+ packages = ["multimodalrouter", "multimodalrouter.graph", "multimodalrouter.router", "multimodalrouter.utils", "multimodalrouter.graphics"]
55
57
 
56
58
  [project.scripts]
57
59
  multiModalRouter-build = "multimodalrouter.router.build:main"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multimodalrouter
3
- Version: 0.1.4
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,14 +46,15 @@ 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
51
55
 
52
56
  The Multi Modal Router is a graph-based routing engine that allows you to build and query any hub-based network. It supports multiple transport modes like driving, flying, or shipping, and lets you optimize routes by distance, time, or custom metrics. It can be expanded to any n-dimensional space making it versatile in any coordinate space
53
57
 
54
- > NEWS: v0.1.3 now on pypi ([installation guide](./docs/installation.md))
55
-
56
58
  > NOTE: This project is a work in progress and features might be added and or changed
57
59
 
58
60
  # In depth Documentation
@@ -97,6 +99,12 @@ The graph can be build from any data aslong as the required fields are present (
97
99
 
98
100
  ![example from the maze solver](./docs/solvedMaze1.png)
99
101
 
102
+ ## graph visualizations
103
+
104
+ Use the build-in [visualization](./docs/visualization.md) tool to plot any `2D` or `3D` Graph.
105
+
106
+ ![example plot of flight paths](./docs/FlightPathPlot.png)
107
+
100
108
  ## Important considerations for your usecase
101
109
 
102
110
  Depending on your usecase and datasets some features may not be usable see solutions below
@@ -118,4 +126,6 @@ Depending on your usecase and datasets some features may not be usable see solut
118
126
 
119
127
  [see here](./LICENSE.md)
120
128
 
129
+ [dependencies](./NOTICE.md)
130
+
121
131
 
@@ -1,16 +1,24 @@
1
1
  LICENSE.md
2
2
  MANIFEST.in
3
+ NOTICE.md
3
4
  README.md
4
5
  pyproject.toml
6
+ docs/FlightPathPlot.png
5
7
  docs/cli.md
6
8
  docs/graph.md
7
9
  docs/installation.md
8
10
  docs/solvedMaze1.png
9
11
  docs/utils.md
12
+ docs/visualization.md
10
13
  docs/examples/demoData.csv
11
14
  docs/examples/flightRouter/main.py
15
+ docs/examples/flightRouter/plot.py
16
+ docs/examples/flightRouter/__pycache__/plot.cpython-313.pyc
12
17
  docs/examples/flightRouter/data/fullDataset.csv
13
18
  docs/examples/mazePathfinder/main.py
19
+ docs/examples/mazePathfinder/plot.py
20
+ docs/examples/mazePathfinder/__pycache__/main.cpython-313.pyc
21
+ docs/examples/mazePathfinder/__pycache__/plot.cpython-313.pyc
14
22
  docs/examples/mazePathfinder/data/createMaze.py
15
23
  docs/examples/mazePathfinder/data/maze.csv
16
24
  src/multiModalRouter.egg-info/PKG-INFO
@@ -29,6 +37,8 @@ src/multimodalrouter.egg-info/top_level.txt
29
37
  src/multimodalrouter/graph/__init__.py
30
38
  src/multimodalrouter/graph/dataclasses.py
31
39
  src/multimodalrouter/graph/graph.py
40
+ src/multimodalrouter/graphics/__init__.py
41
+ src/multimodalrouter/graphics/graphicsWrapper.py
32
42
  src/multimodalrouter/router/__init__.py
33
43
  src/multimodalrouter/router/build.py
34
44
  src/multimodalrouter/router/route.py
@@ -23,6 +23,10 @@ tzdata>=2025.2
23
23
 
24
24
  [dev]
25
25
  pytest>=8.0
26
+ plotly>=6.3.0
27
+
28
+ [plotly]
29
+ plotly>=6.3.0
26
30
 
27
31
  [torch]
28
32
  torch>=2.8.0
@@ -50,6 +50,7 @@ class Hub:
50
50
  self.coords: list[float] = coords
51
51
  self.id = id
52
52
  self.hubType = hubType
53
+ # dict like {mode -> {dest_id -> EdgeMetadata}}
53
54
  self.outgoing: dict[str, dict[str, EdgeMetadata]] = {}
54
55
 
55
56
  def addOutgoing(self, mode: str, dest_id: str, metrics: EdgeMetadata):
@@ -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