Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[flake8]
max-line-length = 130
exclude = .git,__pycache__,build,dist
ignore = E261
ignore = E261, W504
44 changes: 44 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Dependencies and Licenses

This project `MultiModalRouter` depends on the following libraries. All licenses are permissive and compatible with MIT licensing for this project.

| Package | Version | License | License Link |
|---------|---------|---------|--------------|
| colorama | >=0.4.6 | BSD 3-Clause | [License](https://github.com/tartley/colorama/blob/master/LICENSE) |
| dill | >=0.4.0 | BSD | [License](https://github.com/uqfoundation/dill/blob/main/LICENSE) |
| filelock | >=3.19.1 | MIT | [License](https://github.com/tox-dev/py-filelock/blob/main/LICENSE) |
| fsspec | >=2025.9.0 | Apache 2.0 | [License](https://github.com/fsspec/filesystem_spec/blob/main/LICENSE) |
| Jinja2 | >=3.1.6 | BSD-3-Clause | [License](https://github.com/pallets/jinja/blob/main/LICENSE) |
| MarkupSafe | >=3.0.2 | BSD-3-Clause | [License](https://github.com/pallets/markupsafe/blob/main/LICENSE) |
| mpmath | >=1.3.0 | BSD | [License](https://github.com/fredrik-johansson/mpmath/blob/master/LICENSE) |
| networkx | >=3.5 | BSD | [License](https://github.com/networkx/networkx/blob/main/LICENSE.txt) |
| numpy | >=2.3.3 | BSD | [License](https://github.com/numpy/numpy/blob/main/LICENSE.txt) |
| pandas | >=2.3.2 | BSD-3-Clause | [License](https://github.com/pandas-dev/pandas/blob/main/LICENSE) |
| parquet | >=1.3.1 | Apache 2.0 | [License](https://github.com/urschrei/parquet-python/blob/master/LICENSE) |
| ply | >=3.11 | BSD | [License](https://github.com/dabeaz/ply/blob/master/LICENSE.txt) |
| pyarrow | >=21.0.0 | Apache 2.0 | [License](https://github.com/apache/arrow/blob/master/LICENSE) |
| python-dateutil | >=2.9.0.post0 | BSD | [License](https://github.com/dateutil/dateutil/blob/master/LICENSE.txt) |
| pytz | >=2025.2 | MIT | [License](https://github.com/stub42/pytz/blob/master/LICENSE) |
| setuptools | >=80.9.0 | MIT | [License](https://github.com/pypa/setuptools/blob/main/LICENSE) |
| six | >=1.17.0 | MIT | [License](https://github.com/benjaminp/six/blob/master/LICENSE) |
| sympy | >=1.14.0 | BSD | [License](https://github.com/sympy/sympy/blob/master/LICENSE) |
| thriftpy2 | >=0.5.3 | MIT | [License](https://github.com/Thriftpy/thriftpy2/blob/master/LICENSE) |
| tqdm | >=4.67.1 | MPL 2.0 | [License](https://github.com/tqdm/tqdm/blob/master/LICENSE) |
| typing_extensions | >=4.15.0 | PSF | [License](https://github.com/python/typing_extensions/blob/main/LICENSE) |
| tzdata | >=2025.2 | Public Domain | [License](https://github.com/python/tzdata) |

## Optional Dependencies

| Package | Version | License | License Link |
|---------|---------|---------|--------------|
| torch | >=2.8.0 | BSD | [License](https://github.com/pytorch/pytorch/blob/master/LICENSE) |
| plotly | >=6.3.0 | MIT | [License](https://github.com/plotly/plotly.py/blob/master/LICENSE) |
| pytest | >=8.0 | MIT | [License](https://github.com/pytest-dev/pytest/blob/main/LICENSE) |

---

### Notes

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.
2. If distributing this library, include this `DEPENDENCIES.md` file and your own MIT license file to give proper attribution.
3. Optional dependencies should be listed in documentation or `pyproject.toml` extras.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ The graph can be build from any data aslong as the required fields are present (

![example from the maze solver](./docs/solvedMaze1.png)

## graph visualizations

Use the build-in [visualization](./docs/visualization.md) tool to plot any `2D` or `3D` Graph.

![example plot of flight paths](./docs/FlightPathPlot.png)

## Important considerations for your usecase

Depending on your usecase and datasets some features may not be usable see solutions below
Expand All @@ -67,4 +73,6 @@ Depending on your usecase and datasets some features may not be usable see solut

[see here](./LICENSE.md)

[dependencies](./NOTICE.md)


Binary file added docs/FlightPathPlot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 7 additions & 6 deletions docs/examples/flightRouter/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from multimodalrouter import RouteGraph
import os


def main():
path = os.path.dirname(os.path.abspath(__file__))
# initialize the graph
Expand All @@ -17,21 +18,21 @@ def main():
# build the graph
graph.build()
# set start and end points
start = [60.866699,-162.272996] # Atmautluak Airport
end = [60.872747,-162.5247] #Kasigluk Airport
start = [60.866699, -162.272996] # Atmautluak Airport
end = [60.872747, -162.5247] # Kasigluk Airport

start_hub = graph.findClosestHub(["airport"], start) # find the hubs
end_hub = graph.findClosestHub(["airport"], end)
# find the route
route = graph.find_shortest_path(
start_hub.id,
start_hub.id,
end_hub.id,
allowed_modes=["plane","car"],
allowed_modes=["plane", "car"],
verbose=True
)
)
# print the route
print(route.flatPath if route else "No route found")


if __name__ == "__main__":
main()
main()
25 changes: 25 additions & 0 deletions docs/examples/flightRouter/plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# dataclasses.py
# Copyright (c) 2025 Tobias Karusseit
# Licensed under the MIT License. See LICENSE file in the project root for full license information.


from multimodalrouter import RouteGraph
from multimodalrouter.graphics import GraphDisplay
import os

if __name__ == "__main__":
path = os.path.dirname(os.path.abspath(__file__))
graph = RouteGraph(
maxDistance=50,
transportModes={"airport": "fly", },
dataPaths={"airport": os.path.join(path, "data", "fullDataset.csv")},
compressed=False,
)

graph.build()
display = GraphDisplay(graph)
display.display(
displayEarth=True,
nodeTransform=GraphDisplay.degreesToCartesian3D,
edgeTransform=GraphDisplay.curvedEdges
)
18 changes: 15 additions & 3 deletions docs/examples/mazePathfinder/data/createMaze.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
import random
import pandas as pd


# simple cell class for the maze
class Cell:
def __init__(self, x, y):
self.id = f"cell-{x,y}"
self.id = f"cell-{x, y}"
self.x = x
self.y = y
self.visited = False
self.connected = []


def main():
# init a 10x10 maze
mazeHeight = 10
Expand Down Expand Up @@ -53,12 +55,22 @@ def main():
cellStack.pop()

# init the dataframe
data = pd.DataFrame(columns=["source", "destination", "distance", "source_lat", "source_lng", "destination_lat", "destination_lng"])
data = pd.DataFrame(columns=[
"source",
"destination",
"distance",
"source_lat",
"source_lng",
"destination_lat",
"destination_lng"
])
# add the edges to the dataframe
for cell in cells:
for neighbor in cell.connected:
data.loc[len(data)] = [cell.id, neighbor.id, 1, cell.x, cell.y, neighbor.x, neighbor.y]
# save the dataframe
data.to_csv("docs/examples/mazePathfinder/data/maze.csv", index=False)

if __name__ == "__main__": main()

if __name__ == "__main__":
main()
32 changes: 21 additions & 11 deletions docs/examples/mazePathfinder/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@
import os
import pandas as pd


def main():
try:
import matplotlib.pyplot as plt
except ImportError:
raise ImportError("matplotlib is not installed. Please install matplotlib to use this example.")

path = os.path.dirname(os.path.abspath(__file__))
# init the maze df for the plot
mazeDf = pd.read_csv(os.path.join(path, "data", "maze.csv"))
# init the plot
plt.figure(figsize=(10,10))
plt.figure(figsize=(10, 10))
# draw the maze
# draw the maze (grid lines)
for _, row in mazeDf.iterrows():
plt.plot([row.source_lat, row.destination_lat],
[row.source_lng, row.destination_lng],
"k-") # black line for edge
plt.plot(
[row.source_lng, row.destination_lng], # x = "lng" column
[row.source_lat, row.destination_lat], # y = "lat" column
"k-"
)

# initialize the graph
graph = RouteGraph(
Expand All @@ -35,7 +39,7 @@ def main():
graph.build()
# find the shortest route
route = graph.find_shortest_path(
start_id="cell-(0, 0)",
start_id="cell-(0, 0)",
end_id="cell-(0, 9)",
allowed_modes=["walk"],
verbose=True,
Expand All @@ -49,11 +53,17 @@ def main():
if s_prev is not None:
h1 = graph.getHubById(s_prev)
h2 = graph.getHubById(s)
plt.plot([h1.coords[0], h2.coords[0]],
[h1.coords[1], h2.coords[1]],
"b-")
# Swap coords so x=column, y=row
plt.plot(
[h1.coords[1], h2.coords[1]], # x-axis
[h1.coords[0], h2.coords[0]], # y-axis
"b-"
)
s_prev = s

# display the plot
plt.show()

if __name__ == "__main__": main()


if __name__ == "__main__":
main()
32 changes: 32 additions & 0 deletions docs/examples/mazePathfinder/plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# dataclasses.py
# Copyright (c) 2025 Tobias Karusseit
# Licensed under the MIT License. See LICENSE file in the project root for full license information.


from multimodalrouter import RouteGraph
from multimodalrouter.graphics import GraphDisplay
import os


# custom transform to make lat lng to x y (-> lng lat)
def NodeTransform(coords):
for coord in coords:
yield list((coord[0], coord[1]))


if __name__ == "__main__":
path = os.path.dirname(os.path.abspath(__file__))
# initialize the graph
graph = RouteGraph(
maxDistance=50,
transportModes={"cell": "walk", },
dataPaths={"cell": os.path.join(path, "data", "maze.csv")},
compressed=False,
drivingEnabled=False
)

graph.build()
# init the display
display = GraphDisplay(graph)
# display the graph (uses the transform to swap lat lng to x y)
display.display(nodeTransform=NodeTransform)
108 changes: 108 additions & 0 deletions docs/visualization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
[HOME](../README.md)

# Graph Plotting

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.

## GraphDisplay

```python
def __init__(
self,
graph: RouteGraph,
name: str = "Graph",
iconSize: int = 10
) -> None:
```

#### args:
- graph: RouteDisplay = the graph instance you want to plot
- name: str = (not in use at the moment)
- iconSize: int = the size of the nodes in the plot

#### example

```
gd = GraphDisplay(myGraphInstance)
```

[flight path CODE example on sphere](./examples/flightRouter/plot.py)


### display()

The display function will collect data from your Graph and create a [plotly](https://plotly.com/python/) plot from it.

```python
def display(
self,
nodeTransform=None,
edgeTransform=None,
displayEarth=False
):
```

#### args:

- nodeTransform: function = a [transformation](#transformations) function that transformes all node coordinates
- edgeTransform: funstion = a function that [transformes](#transformations) all your edges
- displayEarth: bool = if True -> will display a sphere that (roughly) matches earth

#### example:

this call will create the plot for your graph while mapping all coords onto the surface of the earth

```python
gd.display(
nodeTransform = gd.degreesToCartesian3D,
displayEarth: True
)
```

### transformations

#### base function style

IF you want to implement your own transformation function note that the call must adhere to the following parameters:

```python
def customNodeTrandsform(coords: list[list[float]]):
return list[list[float]]

def customEdgeTransform(start: list[list[float]], end: list[list[float]]):
return list[list[list[float]]]
```

#### args

- coords: list[list[float]] = a nested list of coordinates for all nodes
- start: list[list[float]] = a nested list of all start coordinates
- end: list[list[float]] = a nested list of all end coordinates

#### returns:

- list[list[float]] = a list of all transformed node coordinates
- list[list[list[float]]] = a list of curves whare each curve / edge can have n points defining it

### build-in Node Transforms:

#### degreesToCartesian3D

```python
@staticmethod
def degreesToCartesian3D(coords):
```
This function maps any valid `2D` coordinates (best if in degrees) to spherical coords on the surface of earth

### build-in Edge Transformations

```python
@staticmethod
def curvedEdges(start, end, R=6371.0, H=0.05, n=20):
```

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).

If torch is installed this will use great-circle distance for the curves

> 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
Loading