Skip to content
Open
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
11 changes: 11 additions & 0 deletions __main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pathfinder.city import City
from pathfinder.graphs import graph
from pathfinder.pathfinder import Pathfinder
from pathfinder.types import Path

pathfinder = Pathfinder(graph)

path: Path = pathfinder.get_shortest_path(City.BORDEAUX, City.STRASBOURG)

print(path["total"])
print(path["steps"])
27 changes: 27 additions & 0 deletions pathfinder/astar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from pathfinder.city import City
from pathfinder.graphs import Graph
from pathfinder.pathfinder import Pathfinder

class AStar(Pathfinder):
"""
The AStar class inherited from pathfinding.
"""
def __init__(self, graph: Graph, heuristics: dict) -> None:
super().__init__(graph)
self._heuristics = heuristics

def update_neighbor(self, neighbor: City, current_city: City, weight: float) -> None:
current_distance = self.city_info[current_city]['distance']
total_distance = current_distance + weight

if total_distance < self.city_info[neighbor]['distance'] + self._heuristics[neighbor]:
self.city_info[neighbor]['closest_city'] = current_city
self.city_info[neighbor]['distance'] = total_distance

def get_next_city(self) -> City:
self.unvisited_cities = {
city: info['distance'] + self._heuristics[city]
for city, info in self.city_info.items()
if city not in self.visited_cities
}
return min(self.unvisited_cities, key=self.unvisited_cities.get)
15 changes: 15 additions & 0 deletions pathfinder/city.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from enum import Enum

class City(Enum):
BORDEAUX = "Bordeaux"
DIJON = "Dijon"
LILLE = "Lille"
LYON = "Lyon"
MARSEILLE = "Marseille"
TOULOUSE = "Toulouse"
PARIS = "Paris"
NANTES = "Nantes"
RENNES = "Rennes"
STRASBOURG = "Strasbourg"
ROUEN = "Rouen"
ORLEANS = "Orléans"
121 changes: 121 additions & 0 deletions pathfinder/graphs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from pathfinder.city import City
Graph = dict[City, dict[City, float]]


graph: Graph = {
City.BORDEAUX: {
City.LYON: 31,
City.NANTES: 19,
City.ORLEANS: 24,
City.TOULOUSE: 14
},
City.DIJON: {
City.LYON: 11,
City.PARIS: 16,
City.ORLEANS: 15,
City.STRASBOURG: 20
},
City.LYON: {
City.DIJON: 11,
City.MARSEILLE: 18,
City.ORLEANS: 20,
City.BORDEAUX: 31,
City.TOULOUSE: 28
},
City.LILLE: {
City.PARIS: 13,
City.ROUEN: 12,
City.STRASBOURG: 29
},
City.MARSEILLE: {
City.LYON: 18,
City.TOULOUSE: 22
},
City.NANTES: {
City.BORDEAUX: 19,
City.ORLEANS: 16,
City.RENNES: 6,
City.ROUEN: 19
},
City.PARIS: {
City.LILLE: 13,
City.DIJON: 16,
City.ORLEANS: 7,
City.ROUEN: 7,
City.STRASBOURG: 26
},
City.RENNES: {
City.NANTES: 6,
City.ROUEN: 17
},
City.ORLEANS: {
City.BORDEAUX: 24,
City.DIJON: 15,
City.LYON: 20,
City.NANTES: 16,
City.PARIS: 7,
City.ROUEN: 11
},
City.ROUEN: {
City.ORLEANS: 11,
City.LILLE: 12,
City.NANTES: 19,
City.PARIS: 7,
City.RENNES: 17
},
City.STRASBOURG: {
City.DIJON: 20,
City.PARIS: 26,
City.LILLE: 29
},
City.TOULOUSE: {
City.BORDEAUX: 14,
City.MARSEILLE: 22,
City.LYON: 28
},
}

spfa_graph: Graph = {
City.BORDEAUX: {
City.TOULOUSE: 50,
City.NANTES: 50,
},
City.DIJON: {
City.STRASBOURG: 30,
},
City.LILLE: {
},
City.LYON: {
City.DIJON: 20,
},
City.MARSEILLE: {
City.LYON: 30
},
City.PARIS: {
City.ORLEANS: -30,
City.LILLE: 50,
City.STRASBOURG: -10,
},
City.ORLEANS: {
City.STRASBOURG: 15,
City.PARIS: 40,
},
City.NANTES: {
City.RENNES: 20,
City.ORLEANS: 10,
},
City.RENNES: {
City.PARIS: 20,
City.ROUEN: 10,
},
City.STRASBOURG: {
City.LILLE: 50,
},
City.ROUEN: {
City.PARIS: -50,
},
City.TOULOUSE: {
City.LYON: -75,
City.MARSEILLE: 40,
}
}
16 changes: 16 additions & 0 deletions pathfinder/heuristics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from pathfinder.city import City

heuristics: dict[City, float] = {
City.BORDEAUX: 47,
City.DIJON: 15,
City.LILLE: 25,
City.PARIS: 25,
City.LYON: 24,
City.NANTES: 44,
City.ORLEANS: 27,
City.MARSEILLE: 39,
City.STRASBOURG: 0,
City.RENNES: 43,
City.ROUEN: 31,
City.TOULOUSE: 46
}
100 changes: 100 additions & 0 deletions pathfinder/pathfinder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from pathfinder.graphs import Graph
from pathfinder.types import City, Path, ShortestPathInfo


class Pathfinder:
"""The main class is the parent Dijkstra's algorithm used to
find the shortest path between two cities in a graph."""

def __init__(self, graph: Graph):
self.graph: Graph = graph
self.start: City # The start city
self.end: City # The end city
self.path: Path # The shortest path found
self.finished: bool = False # Whether the pathfinding process is finished
self.city_info: dict[City, ShortestPathInfo] = {} # Information about each city's shortest path
self.visited_cities: list[City] = [] # The cities that have been visited
self.unvisited_cities = {} # The cities that haven't been visited

def get_shortest_path(self, start: City, end: City) -> Path:
"""This method finds the shortest path between the start and end cities,
it is the main method of the whole algorithm."""
self.start = start
self.end = end
self.finished = False
self.initialize_city_info()
self.city_info[start]['distance'] = 0
self.visited_cities.clear()
current_city = start

# Process cities until we reach the end city.
while current_city != end:
current_city = self.process_city(current_city)
if self.finished:
break

# Reconstruct the shortest path when the main algorithm is done
return self.reconstruct_path()

def initialize_city_info(self) -> None:
"""This method initializes the city's distance to origin
and the current closest city once the algorithm starts"""
for city in self.graph:
self.city_info[city] = {
'closest_city': None,
'distance': float('inf') # float(inf) means infinity
}

def process_city(self, current_city: City) -> None:
"""This method processes a city by visiting it
and then getting the next city to visit"""
self.visit_city(current_city)
if not self.finished:
current_city = self.get_next_city()

return current_city

def get_next_city(self) -> City:
"""This method determines the next city to visit
which is the unvisited city with the smallest distance"""
self.unvisited_cities = {
city: info['distance']
for city, info in self.city_info.items()
if city not in self.visited_cities
}
return min(self.unvisited_cities, key=self.unvisited_cities.get)

def visit_city(self, current_city: City) -> None:
self.visited_cities.append(current_city)
self.process_neighbors(current_city)
if current_city == self.end:
self.finished = True

def process_neighbors(self, current_city: City) -> None:
for neighbor, weight in self.graph[current_city].items():
if neighbor not in self.visited_cities:
self.update_neighbor(neighbor, current_city, weight)

def update_neighbor(self, neighbor: City, current_city: City, weight: float) -> None:
"""This method updates a neighbor's shortest path information"""
current_distance = self.city_info[current_city]['distance']
total_distance = current_distance + weight

if total_distance < self.city_info[neighbor]['distance']:
self.city_info[neighbor]['closest_city'] = current_city
self.city_info[neighbor]['distance'] = total_distance

def reconstruct_path(self) -> Path:
"""This method reconstructs the shortest path from the start city to the end city"""
shortest_path: Path = {
"total": self.city_info[self.end]['distance'],
"steps": []
}
current_city = self.end

while current_city != self.start:
shortest_path['steps'].insert(0, current_city)
current_city = self.city_info[current_city]['closest_city']

shortest_path['steps'].insert(0, self.start)
return shortest_path
11 changes: 11 additions & 0 deletions pathfinder/spfa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from collections import deque
from pathfinder.city import City
from pathfinder.graphs import Graph
from pathfinder.pathfinder import Pathfinder
from pathfinder.types import Path

class SPFA(Pathfinder):

def __init__(self, graph: Graph) -> None:
super().__init__(graph)
self.queue = deque()
10 changes: 10 additions & 0 deletions pathfinder/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing import TypedDict, List
from pathfinder.city import City

class Path(TypedDict):
total: float
steps: List[City]

class ShortestPathInfo(TypedDict):
closest_city: City
distance: float